标签:too signed config pes sig 内存管理 包括 border modify
http://blog.csdn.net/qq_26626709/article/details/52742470
内存是通过指针寻址的,因而CPU的字长决定了CPU所能管理的地址空间的大小,该地址空间就被称为虚拟地址空间,因此32位CPU的虚拟地址空间大小为4G,这和实际的物理内存数量无关。
Linux内核将虚拟地址空间分成了两部分:
与之相关的一些宏:
用户进程可用的部分在进程切换时会发生改变,但是由内核保留使用的部分在进程切换时是不变的。在32位系统上,两部分的典型划分比为3:1(该比例可修改),即4G虚拟地址空间中的3G是用户进程可访问的,而另外1G是保留给内核使用的,在这种划分下用户进程可用的虚拟地址空间是0x00000000-0xbfffffff,内核的虚拟地址空间是0xc0000000-0xffffffff。
不同的进程使用不同的用户空间可以使得不同进程的用户空间部分相互隔离,从而保护进程的用户空间部分。
内核空间的保护是通过CPU的特权等级实现的,所有现代CPU都提供了多个特权等级,每个特权等级可以获得的权限是不同的,当CPU处在某个权限等级时就只能执行符合这个等级的权限限制的操作。Linux使用了两个权限等级,分别对应于内核权限和用户权限,并且给属于内核的内存空间添加了权限限制,使得只有处于内核权限等级时CPU才能访问这些内存区域,这就将内核空间也保护了起来。
可用的物理内存会被映射到内核虚拟地址空间中。在32位系统中,内核会将一部分物理内存直接映射到内核的虚拟地址空间中,如果访问内存时所使用的虚拟地址与内核虚拟地址起始值的偏移量不超过该部分内存的大小,则该虚拟地址会被直接关联到物理页帧;否则就必须借助”高端内存“来访问,因此也可以看出之所以使用“高端内存”是因为CPU可寻址的虚拟地址可能小于实际的物理内存,因而不得不借助其它机制(“高端内存”)来访问所有的内存。在IA-32系统上,这部分空间大小为896M。
64位系统不使用高端内存,这是因为64位的系统理论上可寻址的地址空间远大于实际的物理内存(至少现在是如此),因而就不必借助“高端内存”了。而对于用户进程来说,由于它的所有内存访问都通过页表进行,不会直接进行,因而对用户进程来说也不存在高端内存之说。
高端内存由32位架构的内核使用,在32位架构的内核中,要使用高端内存必须首先使用kmap将高端内存映射进内核的虚拟地址空间。
从硬件角度来说存在两种不同类型的机器,分别用不同的方式来管理内存。
lnux中如果要支持NUMA系统,则需要打开CONFIG_NUMA选项。
linux内核对一致和不一致的内存访问系统使用了同样的数据结构,因此对于不同的内存布局,内存的管理算法几乎没有区别。对于UMA系统,将其看作只有一个NUMA节点的NUMA系统,即将其看成NUMA的特例。这样就将简化了内存管理的其它部分,其它部分都可以认为它们是在处理NUMA系统。
linux引入了一个概念称为node,一个node对应一个内存bank,对于UMA系统,只有一个node。其对应的数据结构为“struct pglist_data”。
对于NUMA系统来讲, 整个系统的内存由一个名为node_data 的struct pglist_data(page_data_t) 指针数组来管理。NUMA系统的内存划分如图所示:
每个node又被分成多个zone,每个zone对应一片内存区域。内核引入了枚举常量 zone_type 来描述zone的类型:
它们之间的用途是不一样的:
很显然根据内核配置项的不同,zone的类型是有变化的。每个zone都和一个数组关联在一起,该数组用于组织管理属于该zone的物理内存页。
zone用数据结构struct zone来表示。
所有的node都被保存在一个链表中。在使用时,内核总是尝试从与进程所运行的CPU所关联的NUMA节点申请内存。这是就要用到备用列表,每个节点都通过struct zonelist提供了备用列表,该列表包含了其它节点,可用于代替本节点进行内存分配,其顺序代表了分配的优先级,越靠前优先级越高。
当系统中可用内存很少的时候,内核线程kswapd被唤醒,开始回收释放page。pages_min, pages_low and pages_high这些参数影响着回收行为。
每个zone有三个阈值标准:pages_min, pages_low and pages_high,帮助确定zone中内存分配使用的压力状态。kswapd和这3个参数的互动关系如下图:
在最新的内核中这三个变量变成了watermark数组的成员,分别对应于WMARK_MIN,WMARK_LOW和WMARK_HIGH。
内核在计算这几个值之前会首先计算一个关键参数min_free_kbytes,它是为关键性分配保留的内存空间的最小值。该关键参数有一个约束:不能小于128k,不能大于64M。其计算公式:
阈值的计算由init_per_zone_pages_min( 最新内核中是init_per_zone_wmark_min)完成。该函数还会完成每个zone的lowmem_reserve的计算,该数组用于为不能失败的关键分配预留的内存页。这几个阈值的含义:
当对一个page做I/O操作的时候,page需要被锁住,以防止不正确的数据被访问。做法是:
每个page都可以有一个等待队列,但是太多的分离的等待队列使得花费太多的内存访问周期。也可以让一个zone中的所有page都使用同一个队列,但是这就意味着,当一个page unlock的时候,访问这个zone里内存page的所有休眠的进程将都被唤醒,这样就会出现惊群效应(thundering herd)。
内核的解决方法是将所有的队列放在struct zone数据结构中,并通过哈希表zone->wait_table来管理zone中的等待队列。哈希表的方法可能会造成一些进程不必要的唤醒,但是这个是小概率事件是可以容忍的。
等待队列的哈希表的分配和建立在free_area_init_core()函数中(最终是在zone_wait_table_init()函数中)进行。
zone中的pageset用于实现冷热分配器。热页指的是已经加载到CPU高速缓存的页,这种页的访问速度比在主存中的快。冷页就是不在高速缓存中的页。SMP系统中每个CPU都有一个或多个高速缓存,各个CPU的管理必须是独立的(即便在NUMA中每个CPU也都可以访问所有的内存页,因而其高速缓存也可能缓存所有的内存页)。
每个CPU都有一个struct per_cpu_pages结构,其定义如下:
在这些列表中,热页放在列表头部,冷页放在尾部。
内核使用struct page作为基本单位来管理物理内存,在内核看来,所有的RAM都被划分成了固定长度的页帧。每一个页帧包含了一个页,也就是说一个页帧的长度和一个页的长度相同。页帧是主存的一部分,是一个存储区域。页和页帧的区别在于,页是抽象的数据结构,可以存放在任意地方,而页帧是真实的存储区域。
struct page包含了跟踪一个物理页帧当前被用于什么的有信息。比如页面计数,标志等等。
内核使用struct page的flags中的字段来保存页所属于的zone以及node。这是通过set_page_zone和set_page_node,这两个函数由函数set_page_links调用。
CPU管理虚拟地址,因而物理地址需要映射到虚拟地址才能给CPU使用。用于将虚拟地址空间映射到物理地址空间的数据结构称为页表。
在使用4k大小页的情况下,4k地址空间需要2的20次方个页表项。即便每个页表项大小为4字节也需要4M内存,而每个进程都需要有自己的页表,这就成了一个极大的内存开销。而且在大多数情况下,虚拟地址空间的大部分区域都是没有被使用的,因而没必要为虚拟地址空间中的每个页都分配管理结构,因而实际中采用的是如下方案:
页表中包含了关于该页的信息,例如是否存在于主存中,是否是“脏”的,访问所需权限等级,读写标志,cache策略等等。内核的页表保存在全局变量swapper_pg_dir中,应用进程的页表保存在task_struct->mm->pgd中,在应用进程切换时,会切换进程的页表(schedule-->__schedule-->context_switch-->switch_mm-->switch_mmu_context-->local_flush_tlb_mm)。
linux中采用了4级分页模型。如下:
PGD | PUD | PMD | PTE | OFFSET |
虽然采用了4级模型,但是:
在linux中,每个进程都有自己的页全局目录表(PGD),以及自己的页表集。当发生进程切换时,linux会完成页表的切换。
使用该方案后,每个虚拟地址都划分为相应的比特分组,其中PGD用于索引每个进程所专有的页全局表,以找到PUD,PUD用于索引进程的页上级目录表,以找到PMD依次类推直到找到PTE。PTE即页表数组,该表的表项包含了指向页帧的指针以及页的访问控制相关的信息,比如权限,是否在主存中,是否包含“脏”数据等等,OFFSET用做表内偏移。
使用该机制后,虚拟地址空间中不存在的内存区域对应的PUD,PMD,PTE将不被创建,这就节省了地址空间。
但是使用该机制后每次寻址都需要多次查表,才能找到对应的物理地址,因而降低了速递,CPU使用高速缓存和TLB来加速寻址过程。在访问内存时,如果虚拟地址对应的TLB存在,也就是TLB 命中了,则直接访问,否则就要使用相关的页表项更新TLB(此时可能需要创建新的页表项)然后再继续进行访问。下图是一个CPU的虚拟地址到实地址的转换过程:
当被访问的地址不存在对应的TLB表项时,就会产生TLB中断。在TLB中断中,会:
首先查找访问地址对应的页表,如果找不到对应的页表,就会生成相应的页表项(powerpc通过调用读写异常的处理函数完成该过程)。
使用PTE的内容更新TLB。
在TLB的内容更新完后,仍可能产生读写异常(也就是通常说的page fault),因为页表项虽然存在,但是其内容可能是非法的(比如页表并不在内存中),。
当使用x86时,必须区分以下三种不同的地址:
X86中的MMU包含两个部件,一个是分段部件,一个是分页部件,分段部件(段机制)把一个逻辑地址转换为线性地址;接着,分页部件(分页机制)把一个线性地址转换为物理地址。转化过程如图所示:
在x86段机制中,逻辑地址由两部分组成,即段部分(选择符)及偏移部分。
段是形成逻辑地址到线性地址转换的基础。如果我们把段看成一个对象的话,那么对它的描述如下:
段的界限定义逻辑地址空间中段的大小。段内在偏移量从0到limit范围内的逻辑地址,对应于从Base到Base+Limit范围内的线性地址。在一个段内,偏移量大于段界限的逻辑地址将没有意义,使用这样的逻辑地址,系统将产生异常。另外,如果要对一个段进行访问,系统会根据段的属性检查访问者是否具有访问权限,如果没有,则产生异常。例如,在80386中,如果要在只读段中进行写入,80386将根据该段的属性检测到这是一种违规操作,则产生异常。
下图表示一个段如何从逻辑地址空间,重新定位到线性地址空间。图的左侧表示逻辑地址空间,定义了A,B及C三个段,段容量分别为LimitA、LimitB及LimitC。图中虚线把逻辑地址空间中的段A、B及C与线性地址空间区域连接起来表示了这种转换。
段的基地址、界限及保护属性存储在段的描述符表中,在虚拟—线性地址转换过程中要对描述符进行访问。段描述符又存储在存储器的段描述符表中,该描述符表是段描述符的一个数组。简单的说段描述符表里存储了段描述符,而段描述符又包含了硬件进行逻辑地址到线性地址转换所需的所有信息。
每个段描述符都定义了线性地址空间中的一段地址,它的属性以及它和逻辑地址空间之间的映射关系,实际上是如何从逻辑地址空间映射到线性地址空间。
各种段描述符都存放于段描述符表中,要么在GDT中,要么在LDT中。
描述符表(即段表)定义了386系统的所有段的情况。所有的描述符表本身都占据一个字节为8的倍数的存储器空间,空间大小在8个字节(至少含一个描述符)到64K字节(至多含8K)个描述符之间。
每一个任务的局部描述符表LDT本身也用一个描述符来表示,称为LDT描述符,它包含了有关局部描述符表的信息,被放在全局描述符表GDT中。
但是linux很少使用分段机制,这是因为,分段和分页都能用于将物理地址划分为小的地址片段的功能,因而它们是相互冗余的。分段可以为不同的进程分配不同的线性地址空间,而分页可以将相同的线性地址空间映射到不同的物理地址空间。linux采用了分页机制,原因是:
在linux中,所有运行在用户模式的进程都使用相同的指令和数据段,因此这两个段也被成为用户数据段和用户指令段。类似的,内核使用自己的内核数据段和内核数据段。这几个段分别用宏_ _USER_CS,_ _USER_DS,_ _KERNEL_CS, and_ _KERNEL_DS定义。这些段都从0开始,并且大小都相同,因而linux中,线性地址和逻辑地址是相同的,而且内核和用户进程都可以使用相同的逻辑地址,逻辑地址也就是虚拟地址,这就和其它架构统一起来了。
单处理器系统只有一个GDT,而多处理器系统中每个CPU都有一个GDT,GDT存放在cpu_gdt_table中GDT包含了用户数据段,用户指令段,内核数据段内核指令段以及一些其他段的信息。
绝大多数的linux用户程序并不使用LDT,内核定义了一个缺省的LDT给大多数进程共享。它存放于default_ldt中。如果应用程序需要创建自己的局部描述附表,可以通过modify_ldt系统调用来实现。使用该系统调用创建的LDT需要自己的段。应用程序也可以通过modify_ldt来创建自己的段。
内存初始化关键是page_data_t数据结构以及其下级数据结构(zone,page)的初始化。
宏NODE_DATA用于获取指定节点对应的page_data_t,在多节点系统中,节点数据结构为struct pglist_data *node_data[];该宏获取对应节点所对应的数据结构,如果是单节点系统,节点的数据结构为struct pglist_data contig_page_data;该宏直接返回它。
系统启动代码中与内存管理相关的初始化代码如图:
其功能分别为:
build_all_zonelists会遍历系统中所有的节点,并为每个节点的内存域生成数据结构。它最终会使用节点数据结构调用build_zonelists,该函数会在该节点和系统中其它节点的内存之间建立一种距离关系,距离表达的是从其它节点分配的代价,因而距离越大,分配代价也越大;之后的内存分配会依据这种距离进行,优先选择本地的,如果本地的不可用,则按照距离从近到远来分配,直到成功或者所有的都失败。
在一个节点的内存域中:
当分配内存时,假设指定的内存区域的昂贵程度为A,则分配过程为:
在启动装载器将内核复制到内存,并且初始化代码的汇编部分执行完后,内存布局如图所示:
这是一种默认布局,也存在一些例外:
默认情况下,内核安装在RAM中从物理地址0x00100000开始的地方。也就是第2M开始的那个。没有安装在第1M地址空间开始的地方的原因:
从_edata到_end之间的初始化数据部分所占用的内存在初始化完成后有些是不再需要的,可以回收利用,可以控制哪些部分可以回收,哪些部分不能回收。
内核占用的内存分为几段,其边界保存在变量中,可以通过System.map查看相关的信息,在系统启动后也可以通过/proc/iomem查看相关的信息。
在start_kernel,在其中会调用setup_arch来进行架构相关的初始化。setup_arch会完成启动分配器的初始化以及各个内存域的初始化(paging_init)。paging_init最终会调用free_area_init_node这是个架构无关的函数,它会完成节点以及zone的数据结构的初始化。
Linux内核将虚拟地址空间分成了两部分:用户空间和内核空间。用户进程可用的部分在进程切换时会发生改变,但是由内核保留使用的部分在进程切换时是不变的。在32位系统上,两部分的典型划分比为3:1(该比例可修改),即4G虚拟地址空间中的3G是用户进程可访问的,而另外1G是保留给内核使用的。
32位系统中,内核地址空间又被分为几部分,其图示如下:
3.1 直接映射
其中第一部分用于将一部分物理内存直接映射到内核的虚拟地址空间中,如果访问内存时所使用的虚拟地址与内核虚拟地址起始值的偏移量不超过该部分内存的大小,则该虚拟地址会被直接关联到物理页帧;否则就必须借助”高端内存“来访问,在IA-32系统上,这部分空间大小为896M。
对于直接映射部分的内存,内核提供了两个宏:
剩余部分被内核用作其它用途:
内存的各个区域边界由图中所示的常数定义。high_memory定义了直接映射区域的边界。
系统中定义了与页相关的一些常量:
在直接映射的内存区域和用于vmalloc的内存区域之间有一个大小为VMALLOC_OFFSET的缺口,它用于对内核进行地址保护,防止内核进行越界访问(越过了直接映射区域)。
3.2 vmalloc区
vmalloc区域的起始位置取决于high_memory和VMALLOC_OFFSET。而其结束位置则取决于是否启用了高端内存支持。如果没有启用高端内存支持,就不需要持久映射区域,因为所有内存都可以直接映射。
3.3 持久映射区
持久映射页则开始于PKMAP_BASE,其大小由LAST_PKMAP表示有多少个页。
3.4 固定映射区
固定映射开始于FIXADDR_START结束于FIXADDR_END。这部分区域指向物理内存的随机位置。在该映射中,虚拟地址和物理地址之间的关联是可以自由定义的,但是定义后就不能更改。该区域一直延伸到虚拟地址空间的顶端。
固定映射的优势在于编译时,对该类地址的处理类似于常数,内核一旦启动即为它分配了物理地址。对此类地址的引用比普通指针要快。在上下文切换期间,内核不会将对应于固定地址映射的TLB刷新出去,因此对这类地址的访问总是通过高速缓存。
对于每一个固定地址,都必须创建一个常数并添加到称为fixed_addresses的枚举列表里。内核提供了virt_to_fix和fix_to_virt用于虚拟地址和固定地址常数之间的转换。
set_fixmap用于建立固定地址常量和物理页之间的对应关系。
3.5 冷热页
free_area_init_node最终会调到zone_pcp_init,它会为该zone计算一个batch值。而setup_per_cpu_pageset则会完成冷热缓存的初始化。
bootmem分配器用于内核在启动过程中分配和内存。这是一个很简单的最先适配的分配器。它使用位图来管理页面,比特1表示页忙,0表示空闲。需要分配内存时就扫描位图,直到找到第一个能够满足需求的内存区域。
内核为每个节点都分配了一个struct bootmem_data结构的实例用来管理该node的内存。
在不同的架构下初始化的代码不尽相同,但是都是在paging_int中被调用。
alloc_bootmem*用于分配内存free_bootmem*用于释放内存
当slab系统完成初始化,能够承担内存分配工作时,需要停掉该分配器,这是通过free_all_bootmem(UMA系统)或free_all_bootmem_node(NUMA系统)来完成的
内核提供了两个属性__init用于标记初始化函数,__initdata用于标记初始化数据,这意味着这个函数/数据在初始化完成后其内存就不需了,可以进行回收利用。
以powerpc为例,内核页表的初始化由MMU_init来完成,它在start_kernel之前被调用:
MMU_init->mapin_ram->__mapin_ram_chunk->map_page,
map_page的代码如下:
再看下init_mm的相关定义:
因此可见,kernel的页表是保存在swapper_pg_dir中的。它是init_task的active_mm:
init_task是内核代码开始位置被执行的:
start_here在start_kernel之前被执行。在start_kernel里rest_init会启动kernel_init来启动一个init进程,init_task并不是init进程,init_task是内核启动主代码所在的上下文,该进程最后停在了cpu_idle中(start_kernel->rest_init->cpu_idle),好吧,它的真面目出来了,它就是创世界的进程,并且最后变成了无所事事的idle了。
标签:too signed config pes sig 内存管理 包括 border modify
原文地址:http://www.cnblogs.com/jinanxiaolaohu/p/7992235.html