From: 全面解析Linux 内核 3.10.x - 内存管理
上节我们简单的描述了地址管理的几个基本点,包含页框,也大小,页表等。这一节我们站在这些基本概念上,升入了解一下内存使用的一些问题以及Linux Kernel 针对此问题的解决方法.
大家可能都听过内存碎片,但是对于为什么出现这种问题并没有去深入研究,我简单的描述一下:
内存碎片分为内部碎片以及外部碎片 - 内部碎片就是已经被分配出去的内存地址空间不能被”利用”的内存空间,而外部碎片瞧好相反,只没有被分配出的,主要是因为太小无法分配给被申请者。
想象一下:
假如有一个进程在频繁的请求和释放不同大小的一组连续页框,那么肯定就会导致在已经分配的块内分散许多小块空闲页框,那么假如你现在像要一块连续且很大的页框,虽热有空闲的页框,但是由于是碎片而导致不能满足你的申请要求。
本质上的解决方法有两种:
- 1.利用页单元把一组非连续的页框映射到连续的线性地址空间。
- 2.开发一种适当的技术来记录现存的空闲页框块的情况,尽量的避免为满足小块的请求而分割大的空闲块。
那么显然我们内核使用了第二种解决方法。
基于什么考虑使得内核选用了第二种解决方法呢?
a.首先,连续的页框确实是由需求的,连续的线性地址并不足以满足请求。
一个典型的案例是:
给DMA分配缓冲区的内存请求,在做数据传输的时候DMA直接忽略了页单元而直接访问地址总线,故而请求的缓冲区必须在连续的页框中。
b.即使连续页框并不是必须的,但是保持页表不变的方面起到的作用也是不容忽视的,如果频繁的修改页表势必会导致访问内存,从而使得CPU频繁的刷新TLB表项的内容。
假如内核需要访问大块连续内存,那么需要通过4MB的页,这样子的好处是减少TLB的失效了
此处关于TLB的部分会单独罗列…
基于以上的种种需求,内接提供了一种解决碎片问题的方法,称之为伙伴系统算法(buddy system),伙伴系统算法将所有空闲的页框分为11个块链表,每个块链表分别包含大小1,2,4,8,16,32,64,128,256,512,1024个连续的页框。对1024个页框的最大请求对应着4M大小的连续RAM块,每个块的第一个页框的物理地址是该块大小的整数倍。如16页框的块, 其起始地址是16 x 2^12 的倍数。
start_kernel
mm_init
mem_init -- 获取可用的全部页数
free_all_bootmem -- 遍历bootmem_data_t 得到每次返回的页数
free_all_bootmem_core
__free_pages_bootmem
__free_pages
__free_pages_ok
free_one_page
__free_one_page
注意在内核跟函数的时候只看与当前体系架构相关的函数即可。。
页大小根据内核配置决定,当前我们使用的是大页表,单页为64KB。
假如当前我们需要16个页框,也就是1MB,伙伴系统算法首先会在16个页框的链表中检查是否有一个空闲块,如果没有这样的块,那么就查找上一级大块,也就是32页框链表,假如满足后,会将此块分离,一般用作满足申请需求,另一半填充到64KB的页框链表中。。假如不满足,就继续往上一级走,假如走到1024页框的链表中还不满足的话?那么就表示无能为力,伙伴系统算法就放弃并返回申请错误信号。
上面只是申请过程,释放过程则反之。
由上述可见命名为伙伴系统算法也是有原因的,内核总是试图将一个大小为N的一堆空闲块合并为一个大小为2N的单独块。
称之为伙伴的两个块需要满足下面几个条件:
- a.两个块的大小一致。
- b.物理地址必须是;连续的。
- c.第一块的第一个页框的物理地址是2 x 块大小 x 2^12 的倍数。
Ps..
2.2.1 、在使用一个图文并茂的详细描述看此问题。
已知最小的连续块可能是64KB,order的上限为4(只提供1,2,4,8,16的页框链表)。
也就是最大可以申请1MB的大小块的时候,下图表示了所有可能出现的情况。
可能的方式有:
- 1 最初的情况.
- 2 程序A申请34K,使用order 0.
- 1.1 没有可用的order 0连续块,开始准备分割,先将1个order 4分割为2个order 3.
- 1.2 依然没有可用的order 0连续块,将order 3分割为2个order 2.
- 1.3 还是没有可用的order 0连续块,将order 2分割为2个order 1.
- 1.4 还是没有可用的order 0连续块,将order 1 分割为2 个order 0.
- 1.5 现在终于有可用的order 0连续块了,返回给程序A。
正如上述我们所看到的一样,申请和释放的过程可简单总结为:
- 1.如果内存被申请分配
- 寻找合适的块大小。
- 如果发现,分配给内存。
- 如果没有发现,往上寻找一直到发现合适的块大小后,然后分割。
- 2.如果内存你被要求释放
- 直接释放
- 满足伙伴要求,合并。
Ps..
通过示例推导出来的原理与上述描述的伙伴系统算法的工作原理是一致的!
#define alloc_pages(gfp_mask, order) \
alloc_pages_node(numa_node_id(), gfp_mask, order)
请求2^order个连续的页框,返回第一个所分配页框描述符的地址,如果申请失败,返回NULL。
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
与上述的唯一区别在于,此处仅仅申请1个单独的页框。
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order);
申请2^order个页框,返回第一个所分配页得线性地址。注意与第一个的区别。
#define __get_free_page(gfp_mask) \
__get_free_pages((gfp_mask), 0)
与上面的函数区别在仅仅只是在于此处为申请1个单独的页框。
unsigned long get_zeroed_page(gfp_t gfp_mask);
获取填满全0的页框线性地址。
#define __get_dma_pages(gfp_mask, order) \
__get_free_pages((gfp_mask) | GFP_DMA, (order))
申请使用于可DMA的页框(当然64位系统中64地址都可DMA)。
上述需要明确的有页描述符地址,以及页框线性地址。两者是不一样的!
页描述返回的为一个struct page。
上述我们已经了解并透析了伙伴系统算法的初始化流程以及工作原理。
下面我们侧重描述一下内存管理区域使用不同伙伴系统的具体原因。
待续….
By: Keven - 点滴积累