一个毫无毅力之人的自勉
分类: LINUX
2011-08-24 11:28:54
页描述符由struct page表示,它是用来管理物理内存的页框的。而所有的page存放在mem_map数组中: 。因为每个struct page小于64Byte,所以每MB的RAM,大约需要4个页框。
其中count表示页的引用计数器,当count=0时表示页框空闲。
Memory Zone(存储器管理区)由于一些硬件的制约,限制了页框可以使用的方式。Linux就把物理存储器分为三个管理区(zone):
ZONE_DMA < 16MB
ZONE_NORMAL between 16MB and 896MB
ZONE_HIGHMEM > 896MB (64位体系架构下没有ZONE_HIGHMEM)
每个存储器管理区有struct zone_struct表示
NUMA(非一致存储器访问)NUMA是指当给定的CPU对不同存储器单元的访问时间可能不一样,这时系统的物理内存被划分为几个节点(node)。在一个单独的节点内,任一给定CPU访问页所需时间都是相同的。然而,对不同的CPU,这个时间可能就不同。
这里的节点由struct pg_data_t来表示,在80x86体系结构中,使用了UMA模型,所以NUMA没有真正被用到,不过Linux对80x86架构还是使用了节点,不过只有一个单独的节点,它包含了系统中所有的物理存储器,这单个节点的对应描述符存放在contig_page_data变量中。
存储器处理数据结构的初始化paging_init() 除了初始化内核页表外,还初始化每一个页框,和zone描述符等。paging_init一开始会把所有页框设置为PG_reserved,然后 mem_init函数会扫描与动态内存相关的所有页框,把相应的描述符的count置为1,重置PG_reserved。如果页属于 ZONE_HIGHMEM,就设置PG_highmem,并调用free_page函数来释放页框。如果不是动态使用的页框,而是被内核使用或保留的页 框,其PG_reserved=1。
高端存储器页框的内核映射有三种不同的映射机制:
1. 永久内核映射(Permanent Kernel Mappings)
这是一种长期的映射,它使用一个专门的页表,其地址放在pkmap_page_table变量中(在highmem.c中):
它的大小由LAST_PKMAP宏确定,当有PAE时为512项(即2MB),当没有PAE时为1024项(即4MB)。
该页表映射的线性地址从PKMAP_BASE(0xfe000000)开始。
pkmap_count是用来对pkmap做管理的,其中的每一项如下面的注释所示,表示页表内相应的页框的计数。不过这里也不是单纯的计数,
0 -- 没有被映射并且TLB被刷新,可以使用
1 -- 没有被映射并且TLB没有被刷新,不可以使用
n – 被(n-1)个用户映射
这样的设计就决定了建立pkmap的函数的实现。
void* kmap(struct page* page)
就 是会先遍历pkmap_count数组找count=0的,如果找不到,就把所有count=1的page的TLB刷新,然后置count=0,再遍历一 遍找count=0的,如果还是找不到,就把当前进程加到pkmap_map_wait的等待队列中去,状态设置为 TASK+UNITERRUPTIBLE。然后调用schedule()把自己切换出去。所以pkmap不能用在中断处理程序中。
2. 临时内核映射
每个CPU都有自己8个窗口的集合,其线性地址用enum km_type表示。
在km_type中的每个符号(除了最后一个)都是固定映射的线性地址的一个下标:
km_type 就是在FIX_KMAP_BEGIN和FIX_KMAP_END之间的下标。我们看到这个下标的大小是用KM_TYPE_NR*NR_CPUS计算出来 的,也就是每个CPU有一组。那使用临时内核映射的方式也就是调用固定映射的fix_to_virtual函数来进行转换。要注意的是内核必须确保同一个 窗口下标永不会被两个不同的控制路径同时使用。因此,每个下标都是根据使用相应窗口的内核成分命名的。
Buddy Algorightm(伙伴算法)伙伴算法的目的是为物理内存建立一种稳定高效的页框分配策略,来减少external fragmentation(外碎片)。它把所有的空闲页框分组为10个块链表,每个块链表分别包含大小为1,2,43,8,16,32,64,128,256,512个连续的页框。
Linux为每个管理区(zone)使用不同的伙伴系统,也就是DMA,NORMAL,HIGHMEM都有各自不同的伙伴系统。
伙伴系统用到的数据结构
1. mem_map是一个包含所有struct page的数组,也就是说这里存放着所有的页框,每个管理区都关系到mem_map的一个子集。zone中有的zone_mem_map这个成员记录了在该管理区中对应的第一个页框元素,size指定元素的个数:
2. free_area_t的数组
在zone中记录了free_area的数组,就对应着不同的大小的页框,其类型为free_area_t。其中MAX_ORDER=10
free_area_struct记录着一组相同大小页框的信息,free_list就是一个块的第一个页框描述符。这个链表是通过struct page的list成员链接的。如下图所示,这个list成员会是下一个块的第一个页框描述符。
而map是记录了当前组中的页框分配的bitmap,每一个bit描述大小为2^k个页框的两个伙伴块的状态,当位图某位=0时,表示一对兄弟块都忙或者都闲。如果=1,就表示一个忙。所以只要其中的一个改变状态,整个位的状态就要翻转。
它 的大小为RAM的大小除以当前组的页框数*2*PAGE_SIZE。比如一个管理区包含128MB的RAM。128MB可以分成32768个单独页,那页 框大小为1的组就有32768/(1*2)=16384个,页框大小为2的组就有32768/(1*2)=8192个。
下图描述了整体的结构。
__alloc_pages
__alloc_pages是伙伴系统分配算法的核心:
从这个函数中我们可以看到Linux内核是怎么分配页框的,在什么情况下分配页框会失败等等。所以,下面我们将会详细介绍这个过程。
1. 首先从contig_page_data.node_zonelists中找到gfp_mask修饰符对应的管理区描述符列表,也就是说我们的页框会从符合要求的zone中去分配。
2. 从第一个zone开始,如果当前zone的空闲页框 > 所请求的大小 + 界限值,就调用rmqueue来分配页框。如果不是就不断查找下一个zone,直到遍历完。
3. 如果所有的zone都不符合要求。当gfp_mask没有__GFP_WAIT(就是不能wait),就会return NULL。而有__GFP_WAIT的话,那么就会调用balance_classzone()来回收足够的页框以满足要求。这个会在回收页框的部分详细 讲述。这里要注意的是如果调用balance_classzone()来回收页框,进程就有可能被堵塞。
Question:如果请求的order超过了512个页,那返回的页框就不会连续,那__alloc_pages如何返回第一个页框的?
Answer: 当order>MAX_ORDER,也就是10的时候,直接返回NULL。也就是说可以请求的最大连续页大小为2^9*4KB=2MB。
4. 如果找到了一个zone符合条件,那就调用requeue()来分配空间,下面是rmqueue的主要代码:
它会从当前order的free_area开始查找,如果有空闲的(通过curr != head判断),就从链表中删除,然后设置bitmap
Question: 当curr_order != MAX_ORDER-1时才设置bitmap,当curr_order为9的时候为什么不设置的呢??
5. 然后rmqueue调用expand来处理order 将最后的部分分配出去,前面的部分分别插入4和2的free_area中。 __free_pages_ok() 下面来讨论下释放块的过程。 这里有一个基于代码的分析:http://blog.chinaunix.net/u1/38994/showart_357790.html 在释放块的过程中有一个递归处理问题的思想,假设我们调用__free_pages_ok()来释放order为k的块B1 1. 如果此时当B1和B2组成的bitmap为0,就表示两个都在使用(不可能两个都空闲),此时就不存在合并的问题,直接把B1加入order k的空闲列表中,改变B1和B2的bitmap位。 2. 如果B1和B2的bitmap=1,表示B1忙,B2空闲,那说明这两个连续的块都空闲,此时就把B1B2两个块从order k的空闲列表中删除,这样就可以把B1和B2块看成一个整体块B。这样就把问题变成了在order k+1中删除块B。 这样不断经过这种递归的合并就可以把空闲相邻的伙伴块不断地合并成更大的伙伴块。 下图是高位1GB的内存分布: 首先是896MB的内存映射,然后是就是high_memory区域,由三部分组成: 1. Noncontiguous Memory Area(从VMALLOC_START开始到VMALLOC_END结束) 2. Persistent kernel mappings(由一个固定的页表pkmap_page_table映射) 3. Fix-mapped linear addresses(包含内核临时映射) 后两个前面已经介绍过了,现在说一下非连续区。 一个vmalloc_area由struct vm_struct表示,addr表示起始的线性地址,size是vm_struct的大小(不过要加上4096作为安全区间)。 内核通过get_vm_area()来创建vm_struct,方法就是在vmlist链表上从头遍历,看是否有符合大小:size+PGSIZE的空闲空间。 内核通过vmalloc()来分配非连续存储器区(vm area) vmalloc的过程分为两个部分: 1. get_vm_area()取得线性地址和相应的vm_area结构。 2.
调用vmalloc_area_pages()通过伙伴算法分配一个个单独的页,并把这些单独的页放到相应的页目录项中。值得注意的是这里是每次分配一个
页,而不是连续的页。并且对页表项的改动都是对swapper_page_dir(主内核页表),而不触及当前进程。当当前进程需要访问这个地址时就会引
发缺页中断,缺页中断会把内核页表的内容同步到当前进程。 内核通过vfree()来释放非连续内存区。