linux内核内存管理

linux内存分布

整个linux虚拟内存发布如下:3G以上是内核地址,0~3G是进程地址空间。
linux内核内存管理
在x86结构中,内核地址分为三种,这三种类型的区域如下:
ZONE_DMA 内存开始的16MB
ZONE_NORMAL 16MB~896MB
ZONE_HIGHMEM 896MB ~ 结束

进程地址空间的3G,通过MMU,随机映射到物理地址896M之后的地址上。
内核地址的ZONE_DMA+ZONE_NORMAL,内核3G~3G+896M位置,则一一映射到物理地址的0-~896M。
而内核地址的ZONE_HIGHMEM,即3G+896M~4G位置,和进程地址空间一样,可以随机映射到物理内存896M之后的地址上。
linux内核内存管理
所以,内核借助ZONE_HIGHMEM位置,可以访问896M之后的所有物理内存,再加上ZONE_DMA+ZONE_NOMAL和物理内存0~896M一样映射,所以,内核可以访问所有的物理内存,而进程地址空间最多只可以访问物理地址896M外的所有地址。

linux用户内存

32位系统,理论上可以访问的虚拟内存时4G,也就是说,每个进程的页目录,页表填的项,加起来是4G,但是其中从3G-4G的高地址,权限比较高,只能在进程进入内核态的时候才能访问,这3G-4G的映射,每个进程都是一样的,都是来自fork时从内核线程copy过来的。所以进程可以在用户态访问,只有3G虚拟内存。

伙伴系统

进程启动时,操作系统只是为其分配了虚拟内存,而这些虚拟内存并未和物理内存绑定,所以,运行中,会发生缺页中断,这时候,操作系统需要将找到空闲物理内存,然后,将这个空闲物理内存和虚拟内存绑定。
由此可见,操作系统是需要有一个结构,对所有物理内存进行管理的。操作系统将每4k物理内存作为一个单位,叫做页框,进行管理。
Linux内核中引入了伙伴系统算法(buddy system)。把所有的空闲页框分组为11个块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页框块。最大可以申请1024个连续页框,对应4MB大小的连续内存。每个页框块的第一个页框的物理地址是该块大小的整数倍。例如,大小为16个页框的块,其起始地址是16*2^12(2^12=4096)的整数倍。

假设要申请一个256个页框的块,先从256个页框的链表中查找空闲块,如果没有,就去512个页框的链表中找,找到了则将页框块分为2个256个页框的块,一个分配给应用,另外一个移到256个页框的链表中。如果512个页框的链表中仍没有空闲块,继续向1024个页框的链表查找。如果1024块存在,则将其中的256页框作为请求返回,剩余的768分成256块和512块分别插到相应的链表中。如果仍然没有,则返回错误。
页框块在释放时,会主动大小为相同的一个空闲伙伴块合成为2倍大小的单独块较大的页框块。两个块称为伙伴需要满足一下条件:
(1)两个块具有相同的大小
(2)它们的物理地址是连连续的。
(3)第一块的第一个页框的物理地址是2*b*2^12的倍数。
结构如图所示:第i个块链表中,num表示大小为(2^i)页块的数目,address表示大小为(2^i)页块的首地址。
linux内核内存管理
伙伴系统,除了在缺页中断中,为虚拟内存页分配一个物理内存页外,在linux内核中,有时候需要连续的大物理内存,也可以用伙伴系统。

slab分配器

伙伴系统管理着所有的物理内存,当我们需要物理内存时,便向它申请。假如现在内核代码需要申请一个5byte的空间,但是伙伴系统分配给我们的,至少都是4k的物理内存,这样就会至少浪费了4k-5byte的空就,造成严重的内存内碎片。所以,内核需要一个小物理内存分配管理器。
这个就是slab分配器。
slab分配器,其实就是向伙伴系统申请了一个较大的物理内存,再把这个物理内存分得很小,可以分配很小的物理内存。
除了能够分配小内存,slab分配器还可以用于对象缓存,其实就是在slab分配器回收内存时,不做其他清理操作,直接将其放回,这样之前在这内存上的对象的初始化信息还是在的,下次有这种对象申请内存时,直接把这个内存给它,这个对象获得这个内存后就不需要初始化了,因为上次对象的初始化信息依旧还在。

kmalloc vmalloc malloc

vmalloc和malloc其实是很像的,不过一个在进程地址空间申请的,一个是在内核高地址位置申请的,由上面分析,我们可以知道,这两个申请的内存,都是虚拟地址上连续,而物理地址上是不连续的。
kmallo是基于slab分配器实现的,所以,其申请的位置是物理内存连续的。