在学习本章之前,需要先看这篇博客:Linux的进程地址空间 了解基本的知识。然后再看本章内容。
在内核中获取内存要比在用户空间复杂得多。不过,从程序开发者角度来看,也不是说内核的内存分配就困难得不得了,只是和用户空间中的内存分配不太一样而已。
本章讨论的是在内核之中获取内存的方法。在深入研究实际的分配接口之前,我们需要理解内核是如何管理内存的。
12.1 页
内核把物理页作为内存管理的基本单位。尽管处理器的最小可寻址单位通常为字(甚至字节),但是,内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址的硬件)通常以页为单位进行处理。正因为如此,MMU以页(page)大小为单位来管理系统中的页表(这也是页表名的来由)。从虚拟内存的角度来看,页就是最小单位。
体系结构不同,支持的页大小也不尽相同,还有些体系结构甚至支持几种不同的页大小。大多数32位体系结构支持4KB的页,而64位体系结构一般会支持8KB的页。
内核用struct page结构表示系统中的每个物理页,该结构位于<linux/mmtypes.h>中——我简化了定义,去除了两个容易混淆我们讨论主题的联合结构体:
struct page {
unsigned long flags;
atomic_t _count;
atomic_t _mapcount;
unsigned long private;
struct address_space *mapping;
pgoff_t index;
struct list_head lru;
void *virtual;
};
让我们看一下其中比较重要的域。fag域用来存放页的状态。这些状态包括页是不是脏的,是不是被锁定在内存中等。flag的每一位单独表示一种状态,所以它至少可以同时表示出32种不同的状态。这些标志定义在<linux/page-flags.h>中。
-count域存放页的引用计数-也就是这一页被引用了多少次。当计数值变为-1时,就说明当前内核并没有引用这一页,于是,在新的分配中就可以使用它。内核代码不应当直接检查该域,而是调用page_count()函数进行检查,该函数唯一的参数就是page结构。当页空闲时,尽管该结构内部的_count值是负的,但是对page_count()函数而言,返回0表示页空闲,返回一个正整数表示页在使用。
virtual域是页的虚拟地址。通常情况下,它就是页在虚拟内存中的地址。有些内存(即所谓的高端内存)并不永久地映射到内核地址空间上。在这种情况下,这个域的值为NULL,需要的时候,必须动态地映射这些页。
必须要理解的一点是page结构与物理页相关,而并非与虚拟页相关。因此,该结构对页的描述只是短暂的。即使页中所包含的数据继续存在,由于交换等原因,它们也可能并不再和同一个page结构相关联。内核仅仅用这个数据结构来描述当前时刻在相关的物理页中存放的东西。这种数据结构的目的在于描述物理内存本身,而不是描述包含在其中的数据。
内核用这一结构来管理系统中所有的页,因为内核需要知道一个页是否空闲(也就是页有没有被分配),如果页已经被分配,内核还需要知道谁拥有这个页。拥有者可能是用户空间进程、动态分配的内核数据、静态内核代码或页高速缓存等。
系统中的每个物理页都要分配一个这样的结构体,开发者常常对此感到惊讶。他们会想“这得浪费多少内存呀”!让我们来算算对所有这些页都这么做,到底要消耗掉多少内存。就算struct page占40字节的内存吧,假定系统的物理页为8KB大小,系统有4GB物理内存。那么,系统中共有页面524 288个,而描述这么多页面的page结构体消耗的内存只不过是20MB:也许绝对值不小,但是相对系统4GB内存而言,仅是很小的一部分罢了。因此,要管理系统中这么多物理页面,这个代价并不算太高。
12.2 区
由于硬件的限制,内核并不能对所有的页一视同仁。有些页位于内存中特定的物理地址上,所以不能将其用于一些特定的任务。由于存在这种限制,所以内核把页划分为不同的区(zone)。内核使用区对具有相似特性的页进行分组。Linux必须处理如下两种由于硬件存在缺陷而引起的内存寻址问题:
- 一些硬件只能用某些特定的内存地址来执行DMA(直接内存访问)
- 一些体系结构的内存的物理寻址范围比虚拟寻址范围大得多。这样,就有一些内存不能永久地映射到内核空间上。
因为存在这些制约条件,Linux主要使用了四种区:
- ZONE_DMA——这个区包含的页能用来执行DMA操作。
- ZONE_DMA32——和ZOME_DMA类似,该区包含的页面可用来执行DMA操作;而和ZONE-DMA不同之处在于,这些页面只能被32位设备访问。在某些体系结构中,该区将比ZONE_DMA更大。
- ZONE_NORMAL——这个区包含的都是能正常映射的页。
- ZONE_HIGHEM——这个区包含“高端内存”,其中的页并不能永久地映射到内核地址空间。
Linux把系统的页划分为区,形成不同的内存池,这样就可以根据用途进行分配了。例如,ZONE_DMA内存池让内核有能力为DMA分配所需的内存。如果需要这样的内存,那么,内核就可以从ZONE_DMA中按照请求的数目取出页。注意,区的划分没有任何物理意义,这只不过是内核为了管理页而采取的一种逻辑上的分组。
某些分配可能需要从特定的区中获取页,而另外一些分配则可以从多个区中获取页。比如,尽管用于DMA的内存必须从ZONE_DMA中进行分配,但是一般用途的内存却既能从ZONE_DMA分配,也能从ZONE_NORMAL分配,不过不可能同时从两个区分配,因为分配是不能跨区界限的。当然,内核更希望一般用途的内存从常规区分配,这样能节省ZONE_DMA中的页,保证满足DMA的使用需求。但是,如果可供分配的资源不够用了(如果内存已经变得很少了),那么,内核就会去占用其他可用区的内存。
不是所有的体系结构都定义了全部区,有些64位的体系结构,如Intel的x86-64体系结构可以映射和处理64位的内存空间,所以x86-64没有ZONE_HIGHMEM区,所有的物理内存都处于ZONE_DMA和ZONE_NORMAL区。
每个区都用struct zone表示,在<linux/mmzone.h>中定义。这个结构体很大,但是,系统中只有三个区,因此,也只有三个这样的结构。让我们看一下其中一些重要的域。
-
lock域是一个自旋锁,它防止该结构被并发访问。注意,这个域只保护结构,而不保护驻留在这个区中的所有页。没有特定的锁来保护单个页,但是,部分内核可以锁住在页中驻留的数据.
-
watermark数组持有该区的最小值、最低和最高水位值。内核使用水位为每个内存区设置合适的内存消耗基准。该水位随空闲内存的多少而变化。
-
name域是一个以NULL结束的字符串表示这个区的名字。内核启动期间初始化这个值,其代码位于mm/page-alloc.c中。三个区的名字分别为"DMA"、“Normal"和"HighMem”
12.3获得页
我们已经对内核如何管理内存(页、区等)有所了解了,现在让我们看一下内核实现的接口,我们正是通过这些接口在内核内分配和释放内存的。
内核提供了一种请求内存的底层机制,并提供了对它进行访问的几个接口。所有这些接口都以页为单位分配内存,定义于<linux/gfp.h>中。最核心的函数是:
struct page * alloc_pages(gfp_t gfp mask,unsigned int order)
该函数分配 2 o r d e r 2^{order} 2order(1<<order)个连续的物理页,并返回一个指针,该指针指向第一个页的page结构体;如果出错,就返回NULL,在12.4节我们再研究gft_t类型和gft_mask参数。你可以用下面这个函数把给定的页转换成它的逻辑地址:
void * page_address(struct page *page)
该函数返回一个指针,指向给定物理页当前所在的逻辑地址。如果你无须用到struct page,你可以调用:
unsigned long __get_free_pages(gfp_t gfp_mask,unsigned int order)
这个函数与alloc_pages()作用相同,不过它直接返回所请求的第一个页的逻辑地址。因为页是连续的,所以其他页也会紧随其后。
如果你只需一页,就可以用下面两个封装好的函数,它能让你少敲几下键盘:
struct page * alloc_page(gfp_t gfp_mask)
unsigned long __get_free_page(gfp_t gfp_mask)
这两个函数与其兄弟函数工作方式相同,只不过传递给order的值为 2 0 2^{0} 20( 2 0 2^{0} 20=1页)。
如果你需要让返回的页的内容全为0,请用下面这个函数:
unsigned long get_zeroed_page(unsigned int gfp_mask)
这个函数与__get_free_pages()工作方式相同,只不过把分配好的页都填充成了0。可以保证信息安全。
调用__get_free_pages()之后要注意进行错误检查。内核分配可能失败,因此你的代码必须进行检查并做相应的处理。这意味在此之前,你所做的所有工作可能前功尽弃,甚至还需要回归到原来的状态。正因为如此,在程序开始时就先进行内存分配是很有意义的,这能让错误处理得容易一点。如果你不这么做,那么在你想要分配内存的时候如果失败了,局面可能就难以控制了。
当你不再需要页时可以用下面的函数释放它们:
void __free_pages(struct page *page,unsigned int order)
void free_pages(unsigned long addr,unsigned int order)
void free_page(unsigned long addr)
释放页时要谨慎,只能释放属于你的页。传递了错误的struct page或地址,用了错误的order值,这些都可能导致系统崩溃。请记住,内核是完全信赖自己的。这点与用户空间不同,如果你有非法操作,内核会开开心心地把自己挂起来,停止运行。
12.4 kmalloc()
kmalloc() 函数与用户空间的malloc() 一族函数非常类似,只不过它多了一个fags参数。kmalloc() 函数是一个简单的接口,用它可以获得以字节为单位的一块内核内存。如果你需要整个页,那么,前面讨论的页分配接口可能是更好的选择。但是,对于大多数内核分配来说,kmalloc() 接口用得更多。
kmalloc() 在<linux/slab.h>中声明:
void * kmalloc(size t size,gfp_t flags)
这个函数返回一个指向内存块的指针,其内存块至少要有size大小。所分配的内存区在物理上是连续的。在出错时,它返回NULL。除非没有足够的内存可用,否则内核总能分配成功。在对kmalloc() 调用之后,你必须检查返回的是不是NULL,如果是,要适当地处理错误。
12.4.1 gfp_mask 标志
我们已经看过了几个例子,发现不管是在低级页分配函数中,还是在kmalloc() 中,都用到了分配器标志。现在,我们就深入讨论一下这些标志。
这些标志可分为三类:行为修饰符、区修饰符及类型。行为修饰符表示内核应当如何分配所需的内存。在某些特定情况下,只能使用某些特定的方法分配内存。例如,中断处理程序就要求内核在分配内存的过程中不能睡眠(因为中断处理程序不能被重新调度),区修饰符表示从哪儿分配内存。前面我们已经看到,内核把物理内存分为多个区,每个区用于不同的目的。区修饰符指明到底从这些区中的哪一区中进行分配。类型标志组合了行为修饰符和区修饰符,将各种可能用到的组合归纳为不同类型,简化了修饰符的使用;这样,你只需指定一个类型标志就可以了。
- 行为修饰符
所有这些标志,包括行为描述符都是在<linux/gfp.h>中声明的。实际上,一般只使用类型修饰符就够了,我们随后会看到这点。因此,最好对每个标志都有所了解。表12-3是行为修饰符的列表。
可以同时指定这些分配标志。例如:
ptr = kmalloc(size,__GFP_WAIT|__GFP_IO|__GFP_FS);
- 区修饰符
区修饰符表示内存区应当从何处分配。通常,分配可以从任何区开始。不过,内核优先从ZONENORMAL开始,这样可以确保其他区在需要时有足够的空闲页可供使用。
3. 类型标志
类型标志指定所需的行为和区描述符以完成特殊类型的处理。正因为这一点,内核代码趋向于使用正确的类型标志,而不是一味地指定它可能需要用到的多个描述符。这么做既简单又不容易出错误。表12-5是类型标志的列表。
12.4.2 kfree()
kmalloc()的另一端就是kfree(),kfree()声明于<linux/slab.h>中:
void kfree(const void *ptr)
kfree() 函数释放由kmalloc() 分配出来的内存块。
12.5 vmalloc()
vmalloc() 函数的工作方式类似于kmalloc() ,只不过前者分配的内存虚拟地址是连续的,而物理地址则无须连续。这也是用户空间分配函数的工作方式。kmalloc() 函数确保页在物理地址上是连续的(虚拟地址自然也是连续的)。
大多数情况下,只有硬件设备需要得到物理地址连续的内存。在很多体系结构上,硬件设备存在于内存管理单元以外,它根本不理解什么是虚拟地址。因此,硬件设备用到的任何内存区都必须是物理上连续的块,而不仅仅是虚拟地址连续上的块。而仅供软件使用的内存块(例如与进程相关的缓冲区)就可以使用只有虚拟地址连续的内存块。
vmalloc() 函数声明在<linux/vmalloc.h>中,定义在<mm/vmalloc.c>中。用法与用户空间的malloc() 相同:
void * vmalloc(unsigned long size)
该函数返回一个指针,指向逻辑上连续的一块内存区,其大小至少为size。在发生错误时,函数返回NUL.函数可能睡眠,因此,不能从中断上下文中进行调用,也不能从其他不允许阻塞的情况下进行调用。
要释放通过vmalloc() 所获得的内存,使用下面的函数:
void vfree(const void *addr)
这个函数会释放从addr开始的内存块,其中addr是以前由vmalloc() 分配的内存块的地址这个函数也可以睡眠,因此,不能从中断上下文中调用。
12.6 slab层
分配和释放数据结构是所有内核中最普遍的操作之一。空闲链表包含可供使用的、已经分配好的数据结构块。当代码需要一个新的数据结构实例时,就可以从空闲链表中抓取一个,而不需要分配内存,再把数据放进去。以后,当不再需要这个数据结构的实例时,就把它放回空闲链表,而不是释放它。Linux内核提供了slab层(也就是所谓的slab分配器),slab分配器扮演了通用数据结构缓存层的角色。
12.6.1 slab层的设计
slab层把不同的对象划分为所谓高速缓存组,其中每个高速缓存组都存放不同类型的对象。每种对象类型对应一个高速缓存。例如,一个高速缓存用于存放进程描述符(task-struct结构的一个空闲链表),而另一个高速缓存存放索引节点对象(struct inode)。
然后,这些高速缓存又被划分为slab(这也是这个子系统名字的来由),slab由一个或多个物理上连续的页组成。一般情况下,slab也就仅仅由一页组成。每个高速缓存可以由多个slab组成。
每个slab都包含一些对象成员,这里的对象指的是被缓存的数据结构。每个slab处于三种状态之一:满、部分满或空。一个满的slab没有空闲的对象(slab中的所有对象都已被分配)。一个空的slab没有分配出任何对象(slab中的所有对象都是空闲的)。一个部分满的slab有一些对象已分配出去,有些对象还空闲着。当内核的某一部分需要一个新的对象时,先从部分满的slab中进行分配。如果没有部分满的slab,就从空的slab中进行分配。如果没有空的slab,就要创建一个slab了。显然,满的slab无法满足请求,因为它根本就没有空闲的对象。这种策略能减少碎片。
每个高速缓存都使用 kmem_cache 结构来表示。这个结构包含三个链表:slabs_full,slabs_partial和slabs_empty,均存放在kmem_list3 结构内,该结构在 mm/slab.c 中定义。这些链表包含高速缓存中的所有slab,slab描述符 struct slab 用来描述每个slab:
struct slab {
struct list_head list; /*满、部分滿或空链表*/
unsigned long colouroff; /*slab着色的偏移量*/
void *s_mem; /*在slab中的第一个对象*/
unsigned int inuse; /*slab中已分配的对象数*/
kmem_bufctl_t free; /*第一个空闲对象(如果有的话)*/
};
slab描述符要么在slab之外另行分配,要么就放在slab自身开始的地方。如果slab很小,或者slab内部有足够的空间容纳slab描述符,那么描述符就存放在slab里面。
slab层的管理是在每个高速缓存的基础上,通过提供给整个内核一个简单的接口来完成的。通过接口就可以创建和撤销新的高速缓存,并在高速缓存内分配和释放对象。高速缓存及其内slab的复杂管理完全通过slab层的内部机制来处理。当你创建了一个高速缓存后,slab层所起的作用就像一个专用的分配器,可以为具体的对象类型进行分配。
12.6.2 slab分配器的接口
一个新的高速缓存通过以下函数创建
struct kmem_cache * kmem_cache_create(const char *name,
sizet size,
size_t align,
unsigned long flags,
void(*ctor)(void*));
第一个参数是一个字符串,存放着高速缓存的名字;第二个参数是高速缓存中每个对象的大小;第三个参数是slab内第一个对象的偏移,它用来确保在页内进行特定的对齐。通常情况下,0就可以满足要求,也就是标准对齐。fags参数是可选的设置项,用来控制高速缓存的行为。它可以为0,表示没有特殊的行为,或者与以下标志中的一个或多个进行“或”运算:
- SLAB_HWCACHE_ALIGN——这个标志命令slab层把一个slab内的所有对象按高速缓存行对齐。这就防止了“错误的共享”(两个或多个对象尽管位于不同的内存地址,但映射到相同的高速缓存行),这可以提高性能,但以增加内存开销为代价,因为对齐越严格,浪费的内存就越多。到底会耗费掉多少内存,取决于对象的大小以及对象相对于系统高速缓存行对齐的方式。对于会频繁使用的高速缓存,而且代码本身对性能要求又很严格的情况,设置该选项是理想的选择;否则,请三思而后行。
- SLAB_POISON——这个标志使slab层用已知的值(a5a5a5a5)填充slab,这就是所谓的“中毒”,有利于对未初始化内存的访问。
- SLAB_RED_ZONE——这个标志导致slab层在已分配的内存周围插入“红色警界区”以探测缓冲越界。
- SLAB_PANIC——这个标志当分配失败时提醒slab层。这在要求分配只能成功的时候非常有用。比如,在系统初启时分配一个VMA结构的高速缓存(参见第15章)。
- SLAB_CACHE_DMA——这个标志命令slab层使用可以执行DMA的内存给每个slab分配空间。只有在分配的对象用于DMA,而且必须驻留在ZONE_DMA区时才需要这个标志。否则,你既不需要也不应该设置这个标志。
最后一个参数ctor是高速缓存的构造函数。只有在新的页追加到高速缓存时,构造函数才被调用。实际上,Linux内核的高速缓存不使用构造函数。你可以将ctor参数赋值为NULL 。
kmem_cache_create() 在成功时会返回一个指向所创建高速缓存的指针;否则,返回NULL。这个函数不能在中断上下文中调用,因为它可能会睡眠。
要撤销一个高速缓存,则调用:
int kmem_cache_destroy(struct kmem_cache *cachep)
同样,也不能从中断上下文中调用这个函数,因为它也可能睡眠。调用该函数之前必须确保存在以下两个条件:
- 高速缓存中的所有slab都必须为空。其实,不管哪个slab中,只要还有一个对象被分配出去并正在使用的话,那怎么可能撤销这个高速缓存呢?
- 在调用kmem_cache_destroy()过程中(更不用说在调用之后了)不再访问这个高速缓存。调用者必须确保这种同步。
该函数在成功时返回0,否则返回非0值。
创建高速缓存之后,就可以通过下列函数获取对象:
void kmem_cache_alloc(struct kmem_cache *cachep,gfp_t flags)
该函数从给定的高速缓存cachep中返回一个指向对象的指针。如果高速缓存的所有slab中都没有空闲的对象,那么slab层必须通过kmem_getpages() 获取新的页,fags的值传递给_getfree_pages()。这与我们前面看到的标志相同,你用到的应该是GFP_KERNEL或GFP_ATOMIC。
最后释放一个对象,并把它返回给原先的slab,可以使用下面这个函数:
void kmem_cache_free(struct kmem cache*cachep,void *objp) //objp指向当前对象的指针
这样就能把cachep中的对象objp标记为空闲。
很容易吧?slab层负责内存紧缺情况下所有底层的对齐、着色、分配、释放和回收等。如果你要频繁创建很多相同类型的对象,那么,就应该考虑使用slab高速缓存。也就是说,不要自己去实现空闲链表!
12.7 内核栈的分配
总的来说,内核栈可以是1页,也可以是2页,这取决于编译时配置选项。栈大小因此在4 ~ 16KB的范围内。历史上,中断处理程序和被中断进程共享一个栈。当1页栈的选项激活时中断处理程序获得了自己的栈,中断栈。中断栈为每个进程提供一个用于中断处理程序的栈。有了这个选项,中断处理程序不用再和被中断进程共享一个内核栈,它们可以使用自己的栈了。对每个进程来说仅仅耗费了一页而已。
12.8 高端内存的映射
根据定义,在高端内存中的页不能永久地映射到内核地址空间上。因此,通过alloc_pages() 函数以 __GFP_HIGHMEM标志获得的页不可能有逻辑地址。
在x86体系结构上,高于896MB的所有物理内存的范围大都是高端内存,它并不会永久地或自动地映射到内核地址空间,尽管x86处理器能够寻址物理RAM的范围达到4GB(启用PAEe可以寻址到64GB),一旦这些页被分配,就必须映射到内核的逻辑地址空间上。在x86上,高端内存中的页被映射到3GB~4GB。
内核有三种方式管理高端内存。第一种是非连续映射。这里我们只简单提一下,在vmalloc中请求页面的时候,如果请求的是高端内存,则映射到VMALLOC_START与VMALLOC_END之间。第二种方式是永久内核映射。最后一种方式叫临时内核映射。
接下来,详细的分析一下第二种和第三种方式。
12.8.1 永久映射
永久内核映射允许内核建立高端页框到内核地址空间的长期映射。它们只使用主内核页表中一个专门的页表(注意,只有一个页表) ,其地址存放在pkmap_page_table变量中。页表中的表项数由LAST_PKMAP宏产生。页表照样包含512或1024项,这取决于PAE是否被激活;因此内核一次最多只能访问2MB或4MB固定内存映射的高端内存。也就是说,这个空间是 4M 或 2M 大小,因此仅仅需要一个页表即可。
要映射一个给定的page结构到内核地址空间,可以使用定义在文件<linux/highmem.h>中的这个函数:
void *kmap(struct page *page)
这个函数在高端内存或低端内存上都能用。如果page结构对应的是低端内存中的一页,函数只会单纯地返回该页的虚拟地址。如果页位于高端内存,则会建立一个永久映射,再返回地址。这个函数可以睡眠,因此kmap() 只能用在进程上下文中。
因为允许永久映射的数量是有限的(如果没有这个限制,我们就不必搞得这么复杂,把所有内存通通映射为永久内存就行了),当不再需要高端内存时,应该解除映射,这可以通过下列函数完成:
void kunmap(struct page *page)
12.8.2 临时映射
内核在FIXADDR_START到FIXADDR_TOP之间保留了一些线性空间用于特殊需求。这个空间称为“固定映射空间”在这个空间中,有一部分用于高端内存的临时映射。
这块空间具有如下特点:
- 每个CPU占用一块空间
- 在每个CPU占用的那块空间中,又分为多个小空间,每个小空间大小是1个page,每个小空间用于一个目的,这些目的定义在kmap_types.h中的km_type中。
当要进行一次临时映射的时候,需要指定映射的目的,根据映射目的,可以找到对应的小空间,然后把这个空间的地址作为映射地址。这意味着一次临时映射会导致以前的映射被覆盖。
当必须创建一个映射而当前的上下文又不能睡眠时,内核提供了临时映射(也就是所谓的原子映射),有一组保留的映射,它们可以存放新创建的临时映射。内核可以原子地把高端内存中的一个页映射到某个保留的映射中。因此,临时映射可以用在不能睡眠的地方,比如中断处理程序中,因为获取映射时绝不会阻塞。
通过下列函数建立一个临时映射:
void *kmap_atomic(struct page *page,enum km_type type)
参数type是下列枚举类型之一,这些校举类型描述了临时映射的目的。它们定义于<asm/kmap_types.h>中。
通过下列函数取消映射:
void kunmap_atomic(void*kvaddr,enum km_type type)
这个函数也不会阻塞。在很多体系结构中,除非激活了内核抢占,否则kmap_atomic()根本就无事可做,因为只有在下一个临时映射到来前上一个临时映射才有效。因此,内核完全可以“忘掉”kmap_atomic()映射,kunmap_atomic()也无须做什么实际的事情。下一个原子映射将自动覆盖前一个映射。