本文站的角度更底层,基本都是从Linux内核出发,会更深入。所以当你都读完,然后再次审视这些功能的实现和设计时,我相信你会有种豁然开朗的感觉。
1、页
内核把物理页作为内存管理的基本单元。
尽管处理器的最小处理单位是字(或者字节),但是MMU(内存管理单元,管理内存并把虚拟地址转换为物理地址的硬件)通常以页为单位进行处理。所以从虚拟内存看,页也是最小单元。
体系不同,支持的页大小不同。大多数32位体系结构支持4KB的页,而64位体系结构一般会支持8KB的页。
内核用struct page结构体表示系统中的每个页,包含很多项比如页的状态(有没有脏,有没有被锁定)、引用计数(-1表示没有使用)等等。
page结构和物理页相关,和虚拟内存无关。所以它的描述是短暂的,仅仅记录当前的使用状况,当然也不会描述其中的数据。
内核用这个结构来管理系统中所有的页,所以内核知道哪些页是空闲的,如果在使用中拥有者又是谁。
这个拥有者有四种:用户空间进程、动态分配内存的内核数据、静态内核代码以及页高速缓存。
2、区
有些页是有特定用途的。比如内存中有些页是专门用于DMA的。
内核使用区的概念将具有相似特性的页进行分组。区是一种逻辑上的分组的概念,而没有物理上的意义。
区的实际使用和分布是与体系结构相关的。在x86体系结构中主要分为3个区:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。
ZONE_DMA区中的页用来进行DMA(直接内存访问)时使用。
ZONE_HIGHMEM是高端内存,其中的页不能永久的映射到内核地址空间,也就是说,没有虚拟地址。
剩余的内存就属于ZONE_NORMAL区,叫低端内存。
不是所有体系都定义全部区,有些体系结构,比如x86-64可以映射和处理64位的内存空间,所以它没有ZONE_HIGHMEM区,所有的物理内存都都处于ZONE_DMA和ZONE_NORMAL区。
每个区都用结构体struct zone表示。
3、接口
获得页
获得页使用的接口是alloc_pages函数与__get_free_page函数。后者也是调用了前者,只不过在获得了struct page结构体后使用page_address函数获得了虚拟地址。
我们在使用这些接口获取页的时候可能会面对一个问题,我们获得的这些页若是给用户态用,虽然这些页中的数据都是随机产生的垃圾数据,不过,虽然概率很低,但是也有可能会包含某些敏感信息。所以,更谨慎些,我们可以将获得的页都填充为0。这会用到get_zeroed_page函数。而这个函数又用到了__get_free_pages函数。
所以这三个函数最终都是使用了alloc_pages函数。
释放页
当我们不再需要某些页时可以使用下面的函数释放它们:
__free_pages(struct page *page, unsigned int order)
free_pages(unsigned long addr, unsigned int order)
free_page(unsigned long addr)
以上这些接口都是以页为单位进行内存分配与释放的。
kmalloc与vmalloc
在实际中内核需要的内存不一定是整个页,可能只是以字节为单位的一片区域。这两个函数就是实现这样的目的。
不同之处在于,kmalloc分配的是虚拟地址连续,物理地址也连续的一片区域,vmalloc分配的是虚拟地址连续,物理地址不一定连续的一片区域。
对应的释放内存的函数是kfree与vfree。
4、slab层
以页为最小单位分配内存对于内核管理系统中的物理内存来说的确比较方便,但内核自身最常使用的内存却往往是很小的内存块——比如存放文件描述符、进程描述符、虚拟内存区域描述符等行为所需的内存都远不及一页,一个整页中可以聚集多个这些小块内存。
为了满足内核对这种小内存块的需要,Linux系统采用了一种被称为slab分配器(也称作slab层)的技术。slab分配器的实现相当复杂,但原理不难,其核心思想就是“存储池”的运用。内存片段(小块内存)被看作对象,当被使用完后,并不直接释放而是被缓存到“存储池”里,留做下次使用,这无疑避免了频繁创建与销毁对象所带来的额外负载。
slab分配器扮演了通用数据结构缓存层的角色。
slab层把不同的对象划分为所谓高速缓存组,其中每个高速缓存组都存放不同类型的对象,每种对象对应一个高速缓存。
常见的高速缓存组有:进程描述符(task_struct结构体),索引节点对象(struct inode),目录项对象(struct dentry),通用页对象等等。
这些高速缓存又被划分为slab。slab由