第12章 内存管理
一、内存空间的描述
1. 页
内核用struct page结构体表示系统中的每个物理页。内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址的硬件)以页为单位进行处理。大多数32位体系结构支持4KB的页。
必须要理解的一点是,page结构与物理页相关,而并非与虚拟页相关。因此,该结构对页的描述只是暂时的。即使页中所包含的数据继续存在,由于交换等原因,它们可能并不再和同一个page结构相关联。这个数据结构的目的在于描述物理内存本身,而不是包含在其中的数据。内核用此结构体来管理系统中所有的页。
三个比较重要的域:
a. flag域存放页的状态;
b. _count域存放页的引用计数;
c. virtual域存放页的虚拟地址。
-
/*
-
* Each physical page in the system has a struct page associated with
-
* it to keep track of whatever it is we are using the page for at the
-
* moment. Note that we have no way to track which tasks are using
-
* a page, though if it is a pagecache page, rmap structures can tell us
-
* who is mapping it.
-
*/
-
struct page {
-
unsigned long flags; /* Atomic flags, some possibly
-
* updated asynchronously */
-
atomic_t _count; /* Usage count, see below. */
-
... //省略
-
#if defined(WANT_PAGE_VIRTUAL)
-
void *virtual; /* Kernel virtual address (NULL if
-
not kmapped, ie. highmem) */
-
#endif /* WANT_PAGE_VIRTUAL */
-
... //省略
-
};
2. 区
内核使用区(zone)对具有相似性的页进行分组。区的实际使用和分布是与体系结构相关的。区的划分没有任何物理上的意义,只不过内核为了管理页而采取的一种逻辑上的分组。
Linux必须处理如下两种由于硬件存在缺陷而引起的内存寻址问题:
a. 一些硬件只能用某些特定的内存地址来执行DMA;
b. 一些体系结构的内存的物理地址寻址范围比虚拟寻址范围大得多,有一些内存不能永久地映射到内核空间上(存在高端内存映射)。
因为存在这些制约条件,Linux主要使用了4种区:
a. ZONE_DMA:执行DMA操作;
b. ZONE_DMA32: 所标记内存区只能被32位设备访问,在64位系统中,此标志才有用;
c. ZONE_NORMAl: 能正常映射的页;
d. ZONE_HIGHMEM: 包含“高端内存”,区中的页并不能永久地映射到内核地址空间。
某些分配可能要从特定的区中获取页,比如用于DMA的内存必须从ZONE_DMA中进行获取;而有些分配则可以从多个区中获取页,比如一般用途的内存即可从ZONE_NORMAL中获取页,也可从ZONE_DMA中获取页。分配不能够跨区界限,所以可供分配的内存不够了才会考虑占用其它可用区的内存。
每个区都用struct zone结构体表示,其中几个重要的域:
a. lock域:自旋锁,防止结构本身被并发访问,但不保护驻留在这个区中的所有页;
b. watermark数组:内核使用水位为每个区设置合适的内存消耗基准,该数组持有该区的最小值、最低和最高水位值;
c. name域:区的名字,以NULL结束的字符串。
二、内存分配机制
1. 低级页分配方法
A. alloc_pages(gfp_mask,order): 分配2^order个物理地址连续的页,返回指向第一个页的page结构体的指针;由于不返回逻辑地址,可以用page_address(struct page *page)函数把给定的页转换成它的逻辑地址;
B. __get_free_pages(gfp_mask,order): 分配2^order个物理地址连续的页,返回指向第一个页逻辑地址的指针;
C. alloc_page(gfp_mask) : 只分配一页,返回指向页结构的指针;
__get_free_page(gfp_mask):只分配一页,返回指向页逻辑地址的指针;
__get_zeroed_page(gfp_mask):同上,只是把分配好的页都填充成了0。
注:这些函数适用于需要以页为单位的一族地址连续的物理页时。
2. kmalloc()和gfp_mask标志
kmalloc()函数可以获得以字节为单位的一块物理地址连续的内核内存。函数返回指向内存快的指针,内存块至少是指定大小。
gfp_mask标志可分为三类:行为修饰符、区修饰符和类型,具体可查资料。
类型标志指定所需行为和区描述符以完成特殊类型的处理。常用的标志有:
a. GFP_KERNEL:只用在可以重新安全调度的进程上下文中(也就是没有锁被持有的的情况),成功率高;
b. GFP_ATOMIC:在当前代码不能睡眠时(比如中断、软中断、tasklet等),只能选GFP_ATOMIC,该标志表示不能睡眠的内存分配,成功率较GFP_KERNEL低;
c. GFP_DMA:表示分配器必须满足从ZONE_DMA进行分配的请求,该标志一般会和GFP_KERNEL或GFP_ATOMIC组合起来使用。
3. vmalloc()
硬件设备用到的任何内存区都必须是物理上连续的块,而仅供软件使用的内存块(比如与进程相关的缓冲区)就可以使用只有虚拟地址连续的内存块。
vmalloc()函数只确保页在虚拟地址空间内是连续的,它通过分配非连续的物理内存块,再“修正”页表,把内存映射到逻辑地址空间的连续区域中,就能做到这点。
出于性能考虑,很多代码都是用kmalloc()来获取内存,而不是vmalloc()。因为vmalloc()函数为了把物理上不连续的页转换为虚拟地址空间上连续的页,必须专门建立页表项,开销较大。糟糕的是,所获得的页必须一个一个地进行映射(因为他们物理地址上是不连续的),这会导致比直接内存映射大得多的TLB抖动。因为这些原因,只有在不得已时才会使用vmalloc()函数(典型的就是为了获得大块内存时,比如当模块被动态加载到内核中时,就会把模块加载到由vmalloc()分配的内存上)。
4. slab层
Linux的slab层在设计与实现上充分考虑了下述原则:
a. 频繁使用的数据结构也会频繁分配和释放,因此应当缓存他们;
b. 频繁分批和回收必然导致内存碎片,为了避免这种现象,空闲链表的缓存会连续地存放;
c. 如果让部门缓存专属于单个处理器(对系统上的每个处理器独立而唯一),那么,分配和释放就可以在不加SMP锁的情况下进行;
d. 回收的对象可以立即投入到下一次分配;
e. 如果分配器知道对象的大小、页大小、总的高速缓存的大小这样的概念,它会做出更明智的决策;
f. 对存放的对象着色(color),以防止多个对象映射到相同的高速缓存行(cache line)。
slab层把不同的对象划分为不同的高速缓存组,每个缓存组存放不同类型的对象,每种对象类型对应一个高速缓存。kmalloc()接口建立在slab层之上,使用了一组通用高速缓存。
每个slab处于三种状态之一:满、部分满、空。当内核某一部分需要一个新对象时,先从部分满的slab中进行分配。如果没有部分满的slab,就从空的slab中进行分配。如果没有空的slab,就要创建一个slab了。这种策略能够减少碎片。
slab层的关键是避免频繁分配和释放页。slab层只有当给定的高速缓存中既没有部分满的slab也没有空的slab时,才会调用页分配函数。当内存变得紧缺时或者高速缓存显式地被撤销时,会调用kmem_freepages()释放内存。
高速缓存的创建、销毁、分配、释放函数:kmem_cache_create()/kmem_cache_destroy()/kmem_cache_alloc()/kmem_cache_free()。
5. 高端内存映射
根据定义,在高端内存中的页不能永久地映射到内核地址空间。因此,通过alloc_pages()函数以__GFP_HIGHMEM标志获得的页不可能有逻辑地址。在X86体系结构上,高于896MB的所有物理内存的范围大都是高端内存,它并不会永久地或自动地映射到内核地址空间。
A. 永久映射
映射一个给定page结构到内核地址空间,可以使用kmap函数。该函数在高端内存和低端内存上都能用。如果映射低端内存,返回页的虚拟地址;如果映射高端内存,则会建立一个永久映射,再返回地址。该函数可以睡眠,只能用在进程上下文中。因为允许永久映射的数量是有限的,当不再需要高端内存时,应该通过kunmap()函数解除映射。
B. 临时映射
当必须创建一个映射而当前的上下文又不能睡眠时,可以使用临时映射(也就是所谓的原子映射)。kmap_atomic()函数就是实现临时映射的函数,该函数不会阻塞,可以用在中断上下文和其他不能重新调度的地方,它也禁止内核抢占。kunmap_atomic()函数解除映射。
6. per-CPU变量
使用per-CPU变量数据具有不少好处:
a. 减少数据锁定;
b. 大大减少缓存失效,失效发生在处理器试图使它们的缓存保持同步时;
换句话说,就是使用per-CPU变量,省去许多(或最小化)数据上锁,它唯一的安全要求是禁止内核抢占,而这点代价相比上锁小得多,而且接口会自动帮你完成。要注意的是,不能在访问per-CPU数据过程中睡眠,否则,你就可能醒来后已经在其它处理器上了。
例子:
-
void *percpu_ptr;
-
unsigned long *foo;
-
-
percpu_ptr = alloc_percpu(unsigned long);
-
if (!ptr)
-
/* error allocating memory .. */
-
-
foo = get_cpu_var(percpu_ptr); //获取值,并且会禁止内核抢占
-
/* manipulate foo .. */
-
put_cpu_var(percpu_ptr); //设置值,并且恢复内核抢占
7. 分配函数的选择
如果你需要连续的物理页,可以使用某个低级页分配器或kmalloc()函数;
如果你想从高端内存分配,就是用alloc_pages(),该函数返回一个指向struct page结构体的指针,为了获得真正的指针,继续调用kmap()函数,把高端内存映射到内核的逻辑地址;
vmalloc()函数分配的内存虚拟地址是连续的,并不保证物理地址连续,且有一定的性能损耗,在分配较大内存时考虑使用该函数。
如果你要创建和撤销很多大的数据结构,那么考虑建立slab高速缓存。
虚拟文件系统作为内核子系统,为用户空间程序提供文件和文件系统相关的接口。
VFS提供了一个通用的文件系统模型,该模型囊括了任何文件系统的常用功能集和行为。VFS抽象层之所以能够衔接各种各样的文件系统,是因为它定义了所有文件系统都支持的、基本的、概念上的接口和数据结构。内核通过抽象层能够方便、简单地支持各种类型的文件系统。实际文件系统通过编程实现VFS所期望的抽象接口和数据结构。