内存管理
在内核里分配内存可不像在其他地方分配内存那么容易。造成这种局面的因素很多。从根本上讲,是因为内核本身不能像用户空间那样奢侈地使用内存。内核与用户空间不同,它不具备这种能力,它不支持简单便捷的内存分配方式。比如,内核一般不能睡眠。此外,处理内存分配错误对内核来说也绝非易事。正是由于这种限制,再加上内存分配机制不能太复杂,所以在内核中获取内存要比在用户空间复杂得多。不过,从程序开发者角度来看,也不是说内核的内存分配就困难得不得了,只是在用户空间中的内存分配不太一样而已。
本章讨论的是在内核之中获取内存的方法。在深入探究实际的分配接口之前,我们需要理解内核是如何管理内存的。
页
内核把物理页作为内存管理的基本单位。尽管处理器的最小可寻址单位通常为字(甚至是字节),但是,内存管理单元(MMU,管理内存并把物理内存转换为虚拟内存的硬件)通常以页为单位进行处理。正因为如此,MMU以页(page)大小为单位来管理系统中的页表(这也是页表名的来由)。从虚拟内存的角度来看,页就是最小单位。
在第19章我们将会看到,体系结构不同,支持的页的大小也不尽相同,还有些体系结构甚至支持几种不同的页大小。大多数32位体系结构支持4KB的页,而64位体系结构一般会支持8KB的页。这就意味着,在支持4KB页大小并有1GB物理内存的机器上,物理内存会被划分为262144个页。
内核用struct page
结构表示系统中的每个物理页,该结构位于<linux/mm_types.h>中—-我简化了定义,去除了两个容易混淆我们讨论主题的联合结构体:
1 | struct page { |
让我们看一下其中比较重要的域。flag域用来存放页的状态。这些状态包括页是不是脏的,是不是被锁定在内存中等。flag的每一位单独表示一种状态,所以它至少可以同时表示出32种不同的状态。这些标志定义在<linux/page-flags>中。
_count域存放页的引用计数—-也就是这一页被引用了多少次。当计数值变为-1时,就说明当前内核并没有引用这一页,于是,在新的分配中就可以使用它。内核代码不应当直接检查该域,而是调用page_count()函数检查,该函数唯一的参数就是page结构。当页空闲时,尽管该结构内部的_count值是负的,但是对page_count()函数而言,返回0表示页空闲,返回一个正整数表示页在使用。一个页可以由页缓存使用(这时,mapping域指向和这个页关联的address_space对象),或者作为私有数据(由private指向),或者作为进程页表中的映射。
virtual域是页的虚拟地址。通常情况下,它就是页在虚拟内存中的地址。有些内存(即所谓的高端内存)并不永久地映射到内核地址空间上。在这种情况下,这个域的值为NULL,需要的时候,必须动态地映射这些页。稍后我们将谈论高端内存。
必须要理解的一点是page结构与物理页有关,而并非与虚拟页相关。因此,该结构对页的描述只是短暂的。即使页中所包含的数据继续存在,由于交换等原因,它们也可能并不再和同一个page结构相关联。内核仅仅用这个数据结构来描述当前时刻在相关物理页中存放的东西。这个数据结构的目的在于描述物理内存本身,而不是描述包含在其中的数据。
内核用这一结构来管理系统中所有的页,因为内核需要直到一个页是否空闲(也就是页有没有被分配)。如果页已经被分配,内核还需要直到谁拥有这个页。拥有者可能是用户空间进程、动态分配的内核数据、静态内核代码或页高速缓存等
。
系统中的每个物理页都要分配一个这样的结构体,开发者常常对此感到惊讶。他们会想“这得浪费多少内存呀”!让我们来算算对所有这些页都这么做,到底要消耗多少内存。就算struct page
占40字节的内存吧,假定系统的物理页为8KB大小,系统有4GB物理内存。那么,系统中共有页面524288个,而描述这么多页面的page结构体消耗的内存只不过是20MB:也许绝对值不小,而相对系统4GB内存而言,仅是很小的一部分罢了。因此,要管理系统中这么多物理页面,这个代价并不算太高。
区
由于硬件的限制,内核并不能对所有的页一视同仁。有些页位于内存中特定的物理地址上,所以不能将其用于一些特定的任务。由于存在这种限制,所以内核把页划分为不同的区(zone)。内核使用区对具有相似特性的页进行分组。Linux必须处理如下两种由于硬件存在缺陷而引起的内存寻址问题:
- 一些硬件只能用某些特定的内存地址来执行DMA(直接内存访问)。
- 一些体系结构的内存的物理寻址范围比虚拟寻址范围大得多。这样,就有一些内存不能永久地映射到内核空间上。
因为存在这些制约条件,Linux主要使用了四种区:
- ZONE_DMA—-这个区包含的页能用来执行DMA操作。
- ZONE_DMA32—-和ZONE_DMA类似,该区包含的页面可用来执行DMA操作;而和ZONE_DMA不同之处在于,这些页面只能被32位设备访问。在某些体系结构中,该区将比ZONE_DMA更大。
- ZONE_NORMAL—-这个区包含的都是能正常映射的页。
- ZONE_HIGHMEM—-这个区包含“高端内存”,其中的页并不能永久地映射到内核地址空间。
这些区(还有两种不大重要的)在<linux/mmzone.h>中定义。
区的实际使用和分布是与体系结构相关的。例如,某些体系结构在内存的任何地址上执行DMA都没有问题。在这些体系结构中,ZONE_DMA为空,ZONE_NORMAL就可以直接用于分配。与此相反,在x86体系结构上,ISA设备就不能在整个32位的地址空间中执行DMA,因为ISA设备只能访问物理内存的前16MB。因此,ZONE_DMA在x86上包含的页都在0-16MB的内存范围里。
ZONE_HIGHMEM的工作方式也差不多。能否直接映射取决于体系结构。在32位x86系统上,ZONE_HIGHMEM为高于896MB的所有物理内存。在其他体系结构上,由于所有内存都被直接映射,所以ZONE_HIGHMEM为空。ZONE_HIGHMEM所在的内存就是所谓的高端内存(high memory)。系统的其余内存就是所谓的低端内存(low memory)。
前两个区各取所需后,剩余的就由ZONE_NORMAL区独享了。在x86上,ZONE_NORMAL是从16MB到896MB的所有物理内存。在其他的体系结构上,ZONE_NORMAL是所有的可用物理地址。下表是每个区及其在x86-32上所占的页的列表。
区 | 描述 | 物理内存 |
---|---|---|
ZONE_DMA | DMA使用的页 | <16MB |
ZONE_NORMAL | 正常可寻址的页 | 16-896MB |
ZONE_HIGHMEM | 动态映射的页 | >896MB |
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>中定义:
1 | struct zone { |
这个结构体很大,但是,系统中只有三个区,因此,也只有三个这样的结构。让我们看一下其中一些重要的域。
lock域是一个自旋锁,它防止该结构被并发访问。注意,这个域只保护结构,而不保护驻留在这个区中的所有页。没有特定的锁来保护单个页,但是,部分内核可以锁住在页中驻留的数据。
watermark数值持有该区的最小值、最低和最高水位值。内核使用水位为每个内存区设置合适的内存消耗基准。该水位随空闲内存的多少而变化。
name域是一个以NULL结束的字符串表示这个区的名字。内核启动期间初始化这个值,其代码位于mm/page_alloc.c
中。三个区的名字分别为“DMA”、“Normal”和“HighMem”。
获得页
我们已经对内核如果管理内存(页、区等)有所了解了,现在让我们看一下内核实现的接口,我们正是通过这些接口在内核内分配和释放内存的。
内核提供了一种请求内存地底层机制,并提供了对它进行访问的几个接口。所有这些接口都以页为单位分配内存,定义于<linux/gfp.h>中。最核心的函数是:
1 | struct page *alloc_pages(gfp_t gfp_mask,unsigned int order); |
该函数分配2的order(1 << order)次方个连续的物理页,并返回一个指针,该指针指向第一个页page结构体;如果出错,就返回NULL。在12.4节中我们再研究gft_t类型和gft_mask参数,你可以用下面这个函数把给定的页转换为它的逻辑地址:
1 | void *page_address(struct page *page); |
该函数返回一个指针,指向给定物理页当前所在的逻辑地址。如果你无须用到struct page
,你可以调用:
1 | unsigned long __get_free_pages(gfp_t gfp_mask,unsigned int order); |
这个函数与alloc_pages()作用相同,不过它直接返回所请求的第一个页的逻辑地址。因为页是连续的,所以其他页也会紧随其后。
如果你只需要一页,就可以用下面两个封装好的函数,它能让你少敲几下键盘:
1 | struct page *alloc_page(gfp_t gfp_mask); |
这两个函数与其兄弟函数工作方式相同,只不过传递给order的值为0。
获得填充为0的页
如果你需要让返回的页的内容全为0,请用下面的这个函数:
1 | unsigned long get_zeroed_page(unsigned int gfp_mask); |
这个函数与__get_free_page()工作方式相同,只不过把分配好的页都填充成了0—-字节中的每一位都要取消设置。如果分配的页是给用户空间的,这个函数就非常有用了。虽说分配好的页应该包含的都是随机产生的垃圾信息,但其实这些信息可能并不是完全随机的—-它很可能“随机地”包含某些敏感数据。用户空间的页在返回之前,所有数据必须填充为0,或做其他清理工作,在保障系统安全这一点上,我们绝不妥协。下表是所有底层的页分配方法的列表。
低级页分配方法
标志 | 描述 |
---|---|
alloc_page(gfp_mask) | 只分配一页,返回指向页结构的指针 |
alloc_pages(gfp_mask,order) | 分配2的order次方个页,返回指向第一页页机构的指针 |
__get_free_page(gfp_mask) | 只分配一页,返回指向其逻辑地址的指针 |
__get_free_pages(gfp_mask,order) | 分配2的order次方个页,返回指向第一页逻辑地址的指针 |
get_zeroed_page(gfp_mask) | 只分配一页,让其内容填充0,返回指向其逻辑地址的指针 |
释放页
当你不再需要页时可以用下面的函数释放它们:
1 | void __free_pages(struct page *page,unsigned int order); |
释放页时要谨慎,只能释放属于你的页。传递了错误的struct page
或地址,用了错误的order值,这些都可能导致系统崩溃。请记住,内核是完全信赖自己的。这点与用户空间不同,如果你有非法操作,内核会开开心心地把自己挂起来,停止运行。
让我们看一个例子。其中,我们想得到8个页:
1 | unsigned long page; |
在此,我们使用完这8个页之后释放它们:
1 | free_page(page,3); |
GFP_KERNEL参数是gfp_mask标志中的一个例子。前面我们已经简要讨论过了。
调用__get_free_pages()之后要注意进行错误检查。内核分配可能失败,因此你的代码必须进行检查并做相应的处理。这意味在此之前,你所做的所有工作可能前功尽弃,甚至还需要回归到原来的状态。正因为如此,在程序开始时就先进行内存分配是很有意义的,这能让错误处理得容易一点。如果你不这么做,那么在你想要分配内存的时候如果失败了,局面可能就难以控制了。
在你需要以页为单位的一族连续物理页时,尤其是在你只需要一两页时,这些低级页函数就很有用。对于常用的以字节为单位的分配来说,内核提供的函数是kmalloc()。
kmalloc()
kmalloc()函数与用户空间的malloc()一族函数非常类似,只不过它多了一个flags参数。kmalloc()函数是一个简单的接口,用它可以获得以字节为单位的一块内核内存。如果你需要整个页,那么,前面讨论的页分配接口可能是更好的选择。但是,对于大多数内存来说,kmalloc()接口用的更多。
kmalloc()在<linux/slab.h>中声明:
1 | void *kmalloc(size_t size,gfp_t flags); |
这个函数返回的一个指向内存块的指针,其内存块至少要有size大小。所分配的内存区在物理上是连续的。在出错时,它返回NULL。除非没有足够的内存可用,否则内核总能分配成功。在对kmalloc()调用之后,你必须检查返回的是不是NULL,如果是,要适当地处理错误。
让我们看一个例子。我们随便假定存在一个dog结构体,现在需要为它动态地分配足够的空间:
1 | struct dog *p; |
如果kmalloc()调用成功,那么,ptr现在指向一个内存块,内存块的大小至少为所请求的大小。GFP_KERNEL标志表示在试图获取内存并返回给kmalloc()的调用者的过程中,内存分配器将要采取的行为。
gfp_mask标志
我们已经看过几个例子,发现不管是在低级页分配函数中,还是在kmalloc()函数中,都用到了分配器的标志。现在我们就深入讨论一下这些标志。
这些标志可分为三类:行为修饰符、区修饰符及类型。行为修饰符表示内核应当如何分配所需的内存。在某种特定情况下,只能使用某些特定的方法分配内存。例如,中断处理程序就要求内存在分配内存的过程中不能睡眠(因为中断处理程序不能被重新调度)。区修饰符表示从哪儿分配内存。前面我们已经看到,内核把物理内存分为多个区,每个区用于不同的目的。区修饰符表示从哪儿分配内存。前面我们已经看到,内核把物理内存分为多个区,每个区用于不同的目的。区修饰符指明到底从这些区中的哪一区进行分配。类型标志组合了行为修饰符和区修饰符,将各种可能用到的组合归纳为不同类型,简化了修饰符的使用;这样,你只需指定一个类型标志就可以了。GFP_KERNEL就是一种类型标志,内核中进程上下文相关的代码可以使用它。我们来看一下这些标志。
行为修饰符
所有这些标志,包括行为描述符都是在<linux/gfp.h>中声明的。不过,在<linux/slab.h>中包含有这个头文件,因此,你一般不必直接包含引用它。实际上,一般只使用类型修饰符就够了,我们随后会看到这点。因此,最好对每个标志都有所了解。下表是行为修饰符的列表。
行为修饰符
标志 | 描述 |
---|---|
_GFP_WAIT | 分配器可以睡眠 |
_GFP_HIGH | 分配器可以访问紧急事件缓冲池 |
_GFP_IO | 分配器可以启动磁盘I/O |
_GFP_FS | 分配器可以启动文件系统I/O |
_GFP_COLD | 分配器应该使用高速缓存中快要淘汰出去的页 |
_GFP_NOWARN | 分配器将不打印失败警告 |
_GFP_REPEAT | 分配器在分配失败时重复进行分配,但是这次分配还存在失败的可能 |
_GFP_NOFALL | 分配器将无限地重复进行分配。分配不能失败 |
_GFP_NORETRY | 分配器在分配失败时绝不会重新分配 |
_GFP_NO_GROW | 在slab层内部使用 |
_GFP_COMP | 添加混合页元数据,在hugetlb的代码内部使用 |
可以同时指定这些分配标志。例如:
1 | ptr = kmalloc(size,_GFP_WAIT | _GFP_IO | _GFP_FS); |
说明页分配器(最终调用alloc_pages())在分配时可以阻塞、执行I/O。在必要时还可以执行文件系统操作。这就让内核有很大的自由度,以便它尽可能找到空闲的内存来满足分配请求。
大部分分配都会指定这些修饰符,但一般不是这样指定的,而是采用我们随后讨论的类型标志。别担心,你不会在分配内存时为怎样使用这些标志而犯愁的。
区修饰符
区修饰符表示内存区应当从何处分配。通常,分配可以从任何区开始。不过,内核优先从ZONE_NORMAL开始,这样可以确保其他区在需要时有足够的空闲页可供使用。
实际上只有两个区修饰符,因为除了ZONE_NORMAL之外只有两个区(默认都是从ZONE_NORMAL区进行分配)。下表是区修饰符的列表。
区修饰符
标志 | 描述 |
---|---|
_GFP_DMA | 从ZONE_DMA分配 |
_GFP_DMA32 | 只在ZONE_DMA32分配 |
_GFP_HIGHMEM | 从ZONE_HIGHMEM或ZONE_NORMAL分配 |
指定以上标志中的一个就可以改变内核试图进行分配的区。_GFP_DMA标志强制内核从ZONE_DMA分配。这个标志在说,有了这种奇怪的标识,我们绝对可以拥有进行DMA的内存。相反,如果指定_GFP_HIGHMEN标志,则从ZONE_HIGHMEM(优先)或ZONE_NORMAL分配。这个标志在说,我可以使用高端内存,因此,我可以是一个玩偶,给你退还一些内存,但是,常规内存还照常工作。如果没有指定任何标志,则内核从ZONE_DMA或ZONE_NORMAL进行分配,当然优先从ZONE_NORMAL进行分配。不管区标志说什么了,只要它行为正常,我就不关心了。
不能给_get_free_pages()或kmalloc()指定ZONE_HIGHMEM,因为这两个函数返回的都是逻辑地址,而不是page结构,这两个函数分配的内存当前有可能还没有映射到内核的虚拟地址空间,因此,也可能根本没有逻辑地址。只有alloc_pages()才能分配高端内存。实际上,你的分配在大多数情况下都不必指定修饰符,ZONE_NORMAL就足矣。
类型标志
类型标志指定所需的行为和区描述符以完成特殊类型的处理。正因为这一点,内核代码趋向于使用正确的类型标志,而不是一味地指定它可能需要用到的多个描述符。这么做既简单又不容易出错误。下表是类型标志的列表,下下表显示了每个类型标志与哪些修饰符相关联。
类型标志
标志 | 描述 |
---|---|
GFP_ATOMIC | 这个标志用在中断处理程序、下半部、持有自旋锁以及其他不能睡眠的地方。 |
GFP_NOWAIT | 与GFP_ATOMIC类似,不同之处在于。调用不会退给紧急内存池。这就增加了内存分配失败的可能性。 |
GFP_NOIO | 这种分配可以阻塞,但不会启动磁盘I/O。这个标志在不能引发更多磁盘I/O时能阻塞I/O代码,这可能导致令人不愉快的递归。 |
GFP_NOFS | 这种分配在必要时可能阻塞,也可能启动磁盘I/O,但是不会启动文件系统操作。这个标志在你不能再启动另一个文件系统的操作时,用在文件系统部分的代码中。 |
GFP_KERNEL | 这是一种常规分配方式,可能会阻塞。这个标志在睡眠安全时用在进程上下文代码中。为了获得调用者所需的内存,内核会尽力而为。这个标志应当是首选标志。 |
GFP_USER | 这是一种常规分配方式,可能会阻塞。这个标志用于为用户空间进程分配内存。 |
GFP_HIGHUSER | 这是从ZONE_HIGHMEM进行分配,可能会阻塞。这个标志用于为用户空间进程分配内存。 |
GFP_DMA | 这是从ZONE_DMA进行分配。需要获取能供DMA使用的内存的设备驱动程序使用这个标志,通常与以上的某个标志组合在一起使用。 |
在每种类型标志后隐含的修饰符列表
标志 | 修饰符标志 |
---|---|
GFP_ATOMIC | _GFP_HIGH |
GFP_NOWAIT | 0 |
GFP_NOIO | _GFP_WAIT |
GFP_NOFS | (_GFP_WAIT |
GFP_KERNEL | (_GFP_WAIT |
GFP_USER | (_GFP_WAIT |
GFP_HIGHUSER | (_GFP_WAIT |
GFP_DMA | _GFP_DMA |
让我们看一下做常用的标志以及你什么时候、为什么需要使用它们。内核中最常用的标志是GFP_KERNEL。这种分配可能会引起睡眠,它使用的是普通优先级。因为调用可能阻塞,因此这个标志只用在可以重新安全调度的进程上下文中(也就是没有锁被持有的等情况)。因为这个标志对内核如何获取请求的内存没有任何约束,所以内存分配成功的可能性很高。
另一个截然相反的标志是GFP_ATOMIC。因为这个标志表示不能睡眠的内存分配,因此想要满足调用者获取内存的请求将会受到很严格的限制,即使没有足够的连续内存块可供使用,内核也很可能释放出可用内存来,因为内核不能让调用者睡眠。相反,GFP_KERNEL分配可以让调用者睡眠、交换、刷新一些页到硬盘等。因为GFP_ATOMIC不能执行以上任何操作,因此与GFP_KERNEL相比较,它分配成功的机会比较小(尤其在内存短缺时)。即便如此,在当前代码(例如中断处理程序、软中断和tasklet)不能睡眠时,也只能选择GFP_ATOMIC。
在以上两种标志中间的是GFP_NOIO和GFP_NOFS。以这两个标志进行的分配可能会引起阻塞,但它们会避免执行某些其他操作。GFP_NOIO分配绝不启动任何磁盘I/O来帮助满足请求。而GFP_NOFS可能会启动磁盘I/O,但是它不会启动文件系统I/O。你为什么需要这些标志?它们分别用在某些低级块I/O或文件系统的代码中。设想,如果文件系统代码中需要分配内存,但没有使用GFP_NOFS。这种分配可能会引起更多的文件系统操作,而这些操作又会导致另外的分配,从而再引起更多的文件系统操作!这会一直持续下去。这样的代码在调用分配器的时候。必须确保分配器不会再执行到代码本身,否则,分配就可能产生死锁。也别紧张,内核使用这两个标志的地方是极少的。
GFP_DMA标志表示分配器必须满足从ZONE_DMA进行分配的请求。这个标志用在需要DMA的内存的设备驱动程序中。一般你会把这个标志与GFP_ATOMIC和GFP_KERNEL结合起来使用。
在你编写的绝大多数代码中,用到的要么是GFP_KERNEL,要么是GFP_ATOMIC。下表是通常情形和所用标志的列表。不管使用哪种分配类型,你都必须进行检查,并对错误进行处理。
什么时候用哪种标志
情形 | 相应标志 |
---|---|
进程上下文,可以睡眠 | 使用GFP_KERNEL |
进程上下文,不可以睡眠 | 使用GFP_ATOMIC,在你睡眠之前或之后以GFP_KERNEL执行内存分配 |
中断处理程序 | 使用GFP_ATOMIC |
软中断 | 使用GFP_ATOMIC |
tasklet | 使用GFP_ATOMIC |
需要用于DMA的内存,可以睡眠 | 使用(GFP_DMA |
需要用于DMA的内存,不可以睡眠 | 使用(GFP_DMA |
kfree()
kmalloc()的另一端就是kfree(),kfree()声明于<linux/slab.h>中:
1 | void kfree(const void *ptr); |
kfree()函数释放由kmalloc()分配出来的内存块。如果想要释放的内存不是由kmalloc()分配的,或者想要释放的内存早就被释放了,比如说释放属于内核其他部分的内存,调用这个函数就会导致严重的后果。与用户空间类似,分配和回收要注意配对使用,以避免内存泄漏和其他bug。注意,调用kfree(NULL)是安全的
。
让我们看一个在中断处理程序中分配内存的例子。在这个例子中,中断处理程序想分配一个缓冲区来保存输入数据。BUF_SIZE预定义为以字节为单位的缓冲区长度,它应该是大于两个字节的。
1 | char *buf; |
之后,当我们不再需要这个内存时,别忘了释放它:
1 | kfree(buf); |
vmalloc()
vmalloc()函数的工作方式类似于kmalloc(),只不过前者分配的内存虚拟地址是连续的,而物理地址则无须连续。这也是用户空间分配函数的工作方式:由malloc()返回的页在进程的虚拟地址空间内是连续的,但是,这并不保证它们在物理RAM中也是连续的。kmalloc()函数确保页在物理地址上是连续的(虚拟地址自然也是连续的)。
vmalloc()函数只确保页在虚拟地址空间内是连续的。它通过分配非连续的物理内存块,再修正页表,把内存映射到逻辑地址空间的连续区域中,就能做到这点。
大多数情况下,只有硬件设备需要得到物理地址连续的内存。在很多体系结构中,硬件设备存在于内存管理单元之外,它们根本不理解什么是虚拟地址。因此,硬件设备用到的任何内存区都必须是物理上连续的块,而不仅仅是虚拟地址上连续的内存块
。而仅供软件使用的内存块(例如与进程相关的缓冲区)就可以使用只有虚拟地址连续的内存块。但在你的编程中,根本察觉不到这种差异。对内核而言,所有内存看起来都是逻辑上连续的
。
尽管在某些情况下才需要物理上连续的内存块,但是,很多内核代码都用kmalloc()来获得内存,而不是vmalloc()。这主要是处于性能的考虑。vmalloc()函数为了把物理上不连续的页转化为虚拟地址空间上连续的页,必须专门建立页表项。糟糕的是,通过vmalloc()获得的页必须一个一个地进行映射(因为它们物理上是不连续的),这就会导致比直接内存映射大得多的TLB抖动。因为这些原因,vmalloc()仅在不得已时才会使用----典型的就是为了获得大块内存时
,例如,当模块被动态插入到内核中时,就把模块装载到由vmalloc()分配的内存上。
vmalloc()函数声明在<linux/vmalloc.h>中,定义在<mm/vmalloc.c>中。用法与用户空间的malloc()相同:
1 | void *vmalloc(unsigned long size); |
该函数返回一个指针,指向逻辑上连续的一块内存区,其大小至少为size。在发生错误时,函数返回NULL。函数可能睡眠,因此,不能从中断上下文中进行调用,也不能从其他不允许阻塞的情况下进行调用
。
要释放通过vmalloc()所获得的内存,使用下面的函数:
1 | void vfree(const void *addr); |
这个函数会释放从addr开始的内存块,其中addr是以前由vmalloc()分配的内存块的地址。这个函数也可以睡眠,因此,不能从中断上下文中调用。它没有返回值。
这个函数用起来比较简单:
1 | char *buf; |
在分配内存之后,一定要释放它:
1 | vfree(buf); |
slab层
分配和释放数据结构是所有内核中最普遍的操作之一。为了便于数据的频繁分配和回收,编程人员常常会用到空闲链表。空闲链表包含可供使用的、已经分配好的数据结构块。当代码需要一个新的数据结构实例时,就可以从空闲链表抓取一个,而不需要分配内存,再把数据放进去。以后,当不再需要这个数据结构的实例时,就把它放回空闲链表而不是释放它。从这个意义上说,空闲链表相当于对象高速缓存—-快速存储频繁使用的对象类型。
在内核中,空闲链表面临的主要问题之一是不能全局控制。当可用内存变得紧缺时,内核无法通知每个空闲链表,让其收缩缓存的大小以便释放出一些内存来。实际上,内核根本就不知道存在任何空闲链表。为了弥补这一缺陷,也为了使代码更加稳固,Linux内核提供了slab层(也就是所谓的slab分配器)。slab分配器扮演了通用数据结构缓存层的角色。
slab分配器的概念首先在Sun公司的SunOS 5.4操作系统中得以实现。Linux数据结构缓存层具有同样的名字和基本设计思想。
slab分配器试图在几个基本原则之间寻求一种平衡:
- 频繁使用的数据结构也会频繁分配和释放,因此应当缓存它们。
- 频繁分配和回收必然导致内存碎片(难以找到大块连续的可用内存)。为了避免这种现象,空闲链表的缓存会连续地存放。因为已释放的数据结构又会放回空闲链表,因此不会导致碎片。
- 回收的对象可以立即投入下一次分配,因此,对于频繁的分配和释放,空闲链表能够提高其性能。
- 如果分配器知道对象大小、页大小和总的高速缓存的大小这样的概念,它会做出更明智的决策。
- 如果让部分缓存专属于单个处理器(对系统上的每个处理器独立而唯一),那么,分配和释放就可以不加SMP锁的情况下进行。
- 如果分配器是与NUMA相关的,它就可以从相同的内存节点为请求者进行分配。
- 对存放的对象进行着色,以防止多个对象映射到相同的高速缓存行。
Linux的slab层在设计和实现时充分考虑了上述原则。
slab层的设计
slab层把不同的对象划分为所谓高速缓存组,其中每个高速缓存组都存放不同类型的对象。每种对象类型对应一个高速缓存。例如,一个高速缓存用于存放进程描述符(task_struct结构的一个空闲链表),而另一个高速缓存存放索引节点对象(struct inode)。有趣的是,kmalloc()接口建立在slab层上,使用了一组通用高速缓存。
然后,这些高速缓存又被划分为slab。slab由一个或多个物理上连续的页组成。一般情况下,slab也就仅仅由一页组成。每个高速缓存可以由多个slab组成。
每个slab都包含一些对象成员,这里的对象指的是被缓存的数据结构。每个slab处于三种状态之一:满、部分满或空。一个满的slab没有空闲的对象(slab中所有对象都已经被分配)。一个空的slab没有分配出任何对象(slab中的所有对象都是空闲的)。一个部分满的slab有一些对象已分配出去,有些对象还空闲着。当内核的某一部分需要一个新的对象时,先从部分满的slab中进行分配。如果没有部分满的slab,就从空的slab中进行分配。如果没有空的slab,就要创建一个slab了。显然,满的slab无法满足请求,因为它根本就没有空闲的对象。这种策略能减少碎片。
作为一个例子,让我们考察一下inode结构,该结构是磁盘索引节点在内存中的体现。这些数据结构会频繁地创建和释放,因此,用slab分配器来管理它们就很必要。因而struct inode
就由inode_cachep
高速缓存进行分配。这种高速缓存由一个或多个slab组成—-由多个slab组成的可能性大一些,因为这样的对象数量很大。每个slab包含尽可能多的struct inode
对象。当内核请求分配一个新的inode结构时,内核就从部分满的slab或空的slab返回一个指向已分配但未使用的结构的指针。当内核用完inode对象后,slab分配器就把该对象标记为空闲。下图显示了高速缓存、slab及对象之间的关系。
每个高速缓存都使用kmem_cache
结构来表示。这个结构包含三个链表:slab_full、slabs_partial和slabs_empty,均存放在kmem_list3结构内,该结构在mm/slab.c
中定义。这些链表包含高速缓存中的所有slab。slab描述符struct slab
用来描述每个slab:
1 | struct slab { |
slab描述符要么在slab之外另行分配,要么就放在slab自身开始的地方。如果slab很小,或者slab内部有足够的空间容纳slab描述符,那么描述符就存放在slab里面。
slab分配器可以创建新的slab,这是通过__get_free_pages()低级内核页分配器进行的:
1 | static void *kmem_getpages(struct kmem_cache *cachep,gfp_t flags,int nodeid) |
该函数使用__get_free_pages()来为高速缓存分配足够多的内存。该函数的第一个参数就指向需要很多页的特定高速缓存。第二个参数是要传给__get_free_pages()的标志,注意这个标志是如何与另一个值进行二进制“或”运算的,这相当于把高速缓存需要的缺省标志加到flags参数上。分配的页大小为2的幂次方,存放在cachep->gfporder中。由于与分配器NUMA相关的代码的关系前面的这个函数比想象的要复杂一些。当nodeid是一个非负数时,分配器就试图对从相同的内存节点给发出的请求进行分配。这在NUMA系统上提供了较好的性能,但是访问节点之外的内存会导致性能的损失。
为了便于理解,我们可以忽略与NUMA相关的代码,写一个简单的kmem_getpages()函数:
1 | static inline void *kmem_getpages(struct kmem_cache *cachep,gfp_t flags) |
接着,调用kmem_freepages()释放内存,而对给定的高速缓存页,kmem_freepages()最终调用的是free_pages()。当然,slab层的关键就是避免频繁分配和释放页。由此可知,slab层只有当给定的高速缓存部分中既没有满也没有空的slab时才会调用页分配函数。而只有在下列情况下才会调用释放函数:当可用内存变得紧缺时,系统企图释放出更多内存以供使用;或者当高速缓存显式地被撤销时。
slab层的管理是在每个高速缓存的基础上,通过提供给整个内核一个简答接口来完成的。通过接口就可以创建和撤销新的高速缓存,并在高速缓存内分配和释放对象。高速缓存及其内部slab的复杂管理完全通过slab层的内部机制来处理。当你创建了一个高速缓存后,slab层所起的作用就像是一个专用的分配器,可以为具体的对象类型进行分配。
slab分配器的接口
一个新的高速缓存通过以下函数创建:
1 | struct kmem_cache *kmem_cache_create(const char *name, |
第一个参数是一个字符串,存放着高速缓存的名字;第二个参数是高速缓存中每个元素的大小;第三个参数是slab内第一个对象的偏移,它用来确保在页内进行特定的对齐。通常情况下,0就可以满足要求,也就是标准对齐。flags参数是可选的设置项,用来控制高速缓存的行为。它可以为0,表示没有特殊的行为,或者与以下标志中的一个或多个进行“或”运算:
- SLAB_HWCACHE_ALIGN 这个标志命令slab层把一个slab内的所有对象按高速缓存行对齐。这就防止了“错误的共享”(两个或多个对象尽管位于不同的内存地址,但映射到相同的高速缓存行)。这可以提高性能,但以增加内存开销为代价,因为对齐越严格,浪费的内存就越多。到底会浪费多少内存,取决于对象的大小以及对象相对于系统高速缓存行对齐的方式。对于会频繁使用高速缓存,而且代码本身对性能要求又很严格的情况,设置该选项是理想的选择;否则,请三思而后行。
- SLAB_POISON 这个标志使slab层用已知的值(a5a5a5a5)填充slab。这就是所谓的“中毒”,有利于对未初始化内存的访问。
- SLAB_RED_ZONE 这个标志导致slab层在已分配的内存周围插入“红色警戒区”以探测缓冲越界。
- SLAB_PANIC 这个标志当分配失败时提醒slab层。这在要求分配只能成功的时候非常有用。比如,在系统初启动时分配一个VMA结构的高速缓存。
- SLAB_CACHE_DMA 这个标志命令slab层使用可以执行DMA的内存给每个slab分配空间。只有在分配的对象用于DMA,而且必须驻留在ZONE_DMA区时才需要这个标志。否则,你既不需要也不应该设置这个标志。
最后一个参数ctor是高速缓存的构造函数,只有在新的页追加到高速缓存时,构造函数才被调用。实际上,Linux内核的高速缓存不使用构造函数。事实上这里曾经还有过一个析构函数参数,但是由于内核代码并不需要它,因此已经抛弃掉了。你可以将ctor参数赋值为NULL。kmem_cache_create()
在成功时会返回一个指向所创建高速缓存的指针;否则,返回NULL。这个函数不能在中断上下文中调用,因为它可能会睡眠。
要撤销一个高速缓存,则调用:
1 | int kmem_cache_destroy(struct kmem_cache *cachep); |
顾名思义,这样就可以撤销给定的高速缓存。这个函数通常在模块的注销代码中被调用,当然,这里指创建了自己的高速缓存的模块。同样,也不能从中断上下文中调用这个函数,因为它也可能睡眠。调用该函数之前必须确保存在以下两个条件:
- 高速缓存中的所有slab都必须为空。其实,不管哪个slab中,只要还有一个对象被分配出去并正在使用的话,那怎么可能撤销这个高速缓存呢?
- 在调用
kmem_cache_destroy()
过程中(更不用说在调用之后了)不再访问这个高速缓存。调用者必须确保这种同步。
该函数在成功时返回0,否则返回非0值。
从缓存中分配
创建高速缓存之后,就可以通过下列函数获取对象:
1 | void *kmem_cache_alloc(struct kemem_cache *cachep,gfp_t flags) |
该函数从给定的高速缓存cachep中返回一个指向对象的指针。如果高速缓存的所有slab中都没有空闲的对象,那么slab层必须通过kmem_geipages()获取新的页,flags的值传递给_get_free_pages()。这与我们前面看到的标志相同,你用到的应该是GFP_KERNEL或GFP_ATOMIC。
最后一个释放对象,并把它返回给原先的slab,可以使用下面这个函数:
1 | void kmem_cache_free(struct kmem_cache *cachep,void *objp); |
这样就能把cachep中的对象objp标记为空闲。
slab分配器的使用实例
让我们考察一个鲜活的实例,这个例子用的是task_struct结构(进程描述符)。代码稍微有些复杂,取自kernel/fork.c
。
首先,内核用一个全局变量存放指向task_struct高速缓存的指针:
1 | struct kmem_cache *task_struct_cachep; |
在内核初始化期间,在定义于kernel/fork.c
的fork_init()中创建高速缓存:
1 | task_struct_cachep = kmem_cache_create("task_struct", |
这样就创建了一个名为task_struct的高速缓存,其中存放的就是类型为struct task_struct的对象。该对象被创建后存放在slab中偏移量为ARCH_MIN_TASKALIGN个字节的地方,ARCH_MIN_TASKALIGN预定义值于体系结构相关。通常将它定义为L1_CACHE_BYTES—-L1高速缓存的字节大小。没有构造函数或析构函数。注意不用检查返回值是否为失败标记NULL,因为SLAB_PANIC标志已经被设置了。如果分配失败,slab分配器就调用panic()函数。如果没有提供SLAB_PANIC标志,就必须自己检查返回值。SLAB_PANIC标志用在这儿是因为这是系统操作必不可少的高速缓存。
每当进程调用fork()时,一定会创建一个新的进程描述符。这是在dup_task_struct()中完成的,而该函数会被do_fork()调用:
1 | struct task_struct *tsk; |
进程执行完后,如果没有子进程在等待的话,它的进程描述符就会被释放,并返回给task_struct_cachep slab高速缓存。这是在free_task_struct()中执行的(这里,tsk是现有的进程):
1 | kmem_cache_free(task_struct_cachep,tsk); |
由于进程描述符是内核的核心组成部分,时刻要用到,因此task_struct_cachep高速缓存绝不会被撤销掉。即使真能撤销,我们也要通过下列函数阻止其被撤销:
1 | int err; |
很容易吧?slab层负责内存紧缺情况下所有底层的对齐、着色、分配、释放和回收等。如果你要频繁创建很多相同类型的对象,那么,就应该考虑使用slab高速缓存。也就是说,不要自己去实现空闲链表。
在栈上的静态分配
在用户空间,我们以前所讨论到的那些分配的例子,有不少都可以在栈上发生。因为我们毕竟可以实现知道所分配空间的大小。用户空间能够奢侈地负担起非常大的栈,而且栈空间还可以动态增长,相反,内核却不能这么奢侈—-内核栈小而且固定。当给每个进程分配一个固定大小的小栈后,不但可以减少内存的消耗,而且内核也无须负担太重的栈管理任务。
每个进程的内核栈大小既依赖体系结构,也与编译时的选项有关。历史上,每个进程都有两页的内核栈。因为32位和64位体系结构的页面大小分别是4KB和8KB,所以通常它们的内核栈的大小分别是8KB和16KB。
单页内核栈
但是,在2.6系列内核的早期,引入了一个选项设置单页内核栈。当激活这个选项时,每个进程的内核栈只有一页那么大,根据体系结构的不同,或为4KB,或为8KB。这么做出于两个原因:首先,可以让每个进程减少内存消耗。其次,也是最重要的,随着机器运行时间的增加,寻找两个未分配的、连续的页变得越来约困难。物理内存渐渐变为碎片,因此,给一个新进程分配虚拟内存(VM)的压力也在增大。
还有一个更复杂的原因。继续跟随我:我们几乎掌握了关于内核栈的全部知识。现在,每个进程的整个调用链必须放在自己的内核栈中。不过,中断处理程序也曾经使用它们所中断的进程的内核栈,这样,中断处理程序也要放在内核栈中。这当然有效而简单,但是,这同时会把更严格的约束条件加在这可怜的内核栈上。当我们转而使用只有一个页面的内核栈时,中断处理程序就不放在栈中了。
为了矫正这个问题,内核开发者们实现了一个新功能:中断栈。中断栈为每个进程通过一个用于中断处理程序的栈。有了这个选项,中断处理程序不用再和被中断进程共享一个内核栈,它们可以使用自己的栈了。对每个进程来说仅仅耗费了一页而已。
总的来说,内核栈可以是1页,也可以是2页,这取决于编译时配置选项。栈大小因此在4-16KB的范围内。历史上,中断处理程序和被中断进程共享一个栈。当1页栈的选项激活时,中断处理程序获得了自己的栈。在任何情况下,无限制的递归和alloca()显然是不被允许的。
在栈上光明正大的工作
在任意一个函数中,你都必须节省栈资源。这并不难,也没有什么窍门,只需要在具体的函数中让所有局部变量所占空间之和不要超过几百字节
。在栈上进行大量的静态分配是很危险的。要不然,在内核中和在用户空间中进行栈分配就没什么差别了。栈溢出时悄无声息,但势必会引起严重的问题。因为内核没有在管理内核栈上做足工作,因此,当栈溢出时,多出的数据就会直接溢出来,覆盖掉紧邻堆栈末端的东西。首先面临考验的就是thread_info结构。在堆栈之外,任何内核数据都可能存在潜在的风险。当栈溢出时,最好情况是机器宕机,最坏的情况是悄无声息地破坏数据。因此,进行动态分配是一种明智的选择,本章前面有关大块内存的分配就是采用这种方式。
高端内存的映射
根据定义,在高端内存中的页不能永久地映射到内核地址空间上。因此,通过alloc_pages()函数以__GFP_HIGHMEM标志获得的页贝不可能有逻辑地址。
在x86体系结构上,高于896MB的所有物理内存的范围大都是高端内存,它并不会永久地或自动地映射到内核地址空间,尽管x86处理器能够寻址物理RAM的范围达到4GB。一旦这些页被分配,就必须映射到内核的逻辑地址空间上。在x86上,高端内存中的页被映射到3GB-4GB。
永久映射
要映射一个给定的page结构到内核地址空间,可以使用定义在文件<linux/highmem.h>中的这个函数:
1 | void *kmap(struct page *page) |
这个函数在高端内存和低端内存上都能用。如果page结构对应的是低端内存中的一页,函数只会单纯地返回该页的虚拟地址。如果页位于高端内存,则会建立一个永久映射,再返回地址。这个函数可以睡眠,因此kmap()只能用在进程上下文中。
因为允许永久映射的数量是有限的,当不再需要高端内存时,应该解除映射,这可以通过下列函数完成:
1 | void kunmap(struct page *page); |
临时映射
当必须创建一个映射而当前的上下文又不能睡眠时,内核提供了临时映射(也就是所谓的原子映射)。有一组保留的映射,它们可以存放新创建的临时映射。内核可以原子地把高端内存中的一个页映射到某个保留的映射中。因此,临时映射可以用再不能睡眠的地方,比如中断处理程序中,因为获取映射时绝不会阻塞。
通过下列函数建立一个临时映射:
1 | void *kmap_atomic(struct page *page,enum km_type type); |
参数type是下列枚举类型之一,这些枚举类型描述了临时映射的目的。它们定义于<asm/kmap_types.h>中:
1 | enum km_type { |
这个函数不会阻塞,因此可以用在中断上下文和其他不能重新调度的地方。它也禁止内核抢占,这是有必要的,因为映射对每个处理器都是唯一的。通过下列函数取消映射:
1 | void kunmap_atomic(void *kvaddr,enum km_type type); |
这个函数也不会阻塞。在很多体系结构中,除非激活了内核抢占,否则kmap_atomic()根本就无事可做,因为只有在下一个临时映射到来前上一个临时映射才有效。因此,内核完全可以“忘掉”kmap_atomic()映射,kunmap_atomic()也无须做什么实际的事情。下一个原子映射将自动覆盖前一个映射。
每个CPU的分配
支持SMP的现代操作系统使用每个CPU上的数据,对于给定的处理器其数据是唯一的。一般来说,每个CPU的数据存放在一个数组中。数组的每一项对应着系统上一个存在的处理器。按当前处理器号确定这个数组的当前元素这就是2.4内核处理每个CPU数据的方式。这种方式还不错,因此,2.6内核的很多代码依然用它。可以声明数据如下:
1 | unsigned long my_percpu[NR_CPUS]; |
然后,按如下方式访问它:
1 | int cpu; |
注意,上面的代码中并没有出现锁,这是因为所操作的数据对当前处理器来说是唯一的。除了当前处理器之外,没有其他处理器可接触到这个数据,不存在并发访问问题,所以当前处理器可以在不用锁的情况下安全访问它。
现在,内核抢占成为了唯一需要关注的问题了,内核抢占会引起下面提到的两个问题:
- 如果你的代码被其他处理器抢占并重新调度,那么这时CPU变量就会无效,因为它指向的是错误的处理器(通常,代码获得当前处理器之后是不可以睡眠的)。
- 如果另一个任务抢占了你的代码,那么就有可能在同一个处理器上发生并发访问my_percpu的情况,显然这属于一个竞争条件。
虽然如此,但是你大可不必惊慌,因为在获取当前处理器号,即调用get_cpu()时,就已经禁止了内核抢占。相应的在调用put_cpu()时又会重新激活当前处理器号。注意,只要你总使用上述方法来保护数据安全,那么,内核抢占就不需要你自己去禁止。
新的每个CPU接口
2.6内核为了方便创建和操作每个CPU数据,而引进了新的操作接口,称作percpu。该接口归纳了前面所述的操作行为,简化了创建和操作每个CPU的数据。
但前面我们讨论的创建和访问每个CPU的方法依然有效,不过大型对称多处理器计算机要求对每个CPU数据操作更简单,功能更强大,正是在这种背景下,新接口应运而生。
头文件<linux/percpu.h>下声明了所有的接口操作例程,你可以在文件mm/slab.c
和<asm/percpu.h>中找到它们的定义。
编译时的每个CPU数据
在编译时定义每个CPU变量易如反掌:
1 | DEFINE_PER_CPU(type,name); |
这个语句为系统中的每一个处理器都创建了一个类型为type,名字为name的变量实例,如果你需要在别处声明变量,以防范编译时警告,那么下面的宏将是你的好帮手:
1 | DECLARE_PER_CPU(type,name); |
你可以利用get_cpu_var()和put_cpu_var()例程操作变量。调用get_cpu_var()返回当前处理器上的指定变量,同时它将禁止抢占;另一方面put_cpu_var()将相应的重新抢占激活。
1 | get_cpu_var(name)++; //增加该处理器上的name变量的值 |
你也可以获得别的处理器上的每个CPU数据:
1 | per_cpu(name,cpu)++; //增加指定处理器上的name变量的值 |
使用此方法你需要格外小心,因为per_cpu()函数既不会禁止内核抢占,也不会提供任何形式的锁保护。如果一些处理器可以接触到其他处理器的数据,那么你就必须要给数据上锁。
另外还有一个需要提醒的问题:这些编译时每个CPU数据的例子并不能在模块内使用,因为连接程序实际上将它们创建在一个唯一的可执行段中(.data.percpu)。如果你需要从模块中访问每个CPU数据,或者如果你需要动态创建这些数据,那还是有希望的。
运行时的每个CPU数据
内核实现每个CPU数据的动态分配方法类似于kmalloc()。该例程为系统上的每个处理器创建所需内存的实例,其原型在文件<linux/percpu.h>中:
1 | void *alloc_percpu(type); //一个宏 |
宏alloc_percpu()给系统中的每个处理器分配一个指定类型对象的实例。它其实是宏__alloc_percpu()的一个封装,这个原始宏接收的参数有两个:一个是要分配的实际字节数,一个是分配时要按多少字节对齐。而封装后的alloc_percpu()按照单字节对齐—-按照给定类型的自然边界对齐。这种对齐方式最为常用。比如:
1 | struct rabid_cheetah = alloc_percpu(struct rabid_cheetah); |
它等价于
1 | struct rabid_cheetah = __alloc_percpu(sizeof(struct rabid_cheetah), |
__align__是gcc的一个功能,它会返回指定类型或lvalue所需的对齐字节数。它的语义和sizeof一样,比如,下列程序在x86体系中将返回4:
1 | __alignof__(unsigned long) |
如果指定一个value,那么将返回lvalue的最大对齐字节数。比如一个结构中的lvalue相比结构外的lvalue可能有更大的对齐字节需求,这是结构本身的对齐要求的缘故。
相应的调用free_percpu()将释放所有处理器上的指定的每个CPU数据。
无论是alloc_percpu()或是__alloc_percpu()都会返回一个指针,它用来间接引用动态创建的每个CPU数据,内核提供了两个宏来利用指针获取每个CPU数据:
1 | get_cpu_var(ptr); //返回一个void类型指针,该指针指向处理器的ptr的拷贝 |
get_cpu_var()宏返回了一个指向当前处理器数据的特殊实例,它同时会禁止内核抢占;而在get_cpu_var()宏中会重新激活内核抢占。
我们来看一个使用这些函数的完整例子。当然这个例子有点无聊,因为你通常会一次分配够内存,就可以在各种地方使用它,或再一次释放。不过,这个例子可清楚地说明如何使用这些函数。
1 | void *percpu_ptr; |
使用每个CPU数据的原因
使用每个CPU数据具有不少好处。首先是减少了数据锁定。因为按照每个处理器访问每个CPU数据的逻辑,你可以不再需要任何锁。记住“只有这个处理器能访问这个数据”的规则纯粹是一个编程约定。你需要确保本地处理器只会访问它自己的唯一数据。系统本身并不存在任何措施禁止你从事欺骗活动。
第二个好处是使用每个CPU数据可以大大减少缓存失效。失效发生在处理器试图使它们的缓存保持同步时。如果一个处理器操作某个数据,而该数据又存放在其他处理器缓存中,那么存放该数据的那个处理器必须清理或刷新自己的缓存。持续不断的缓存失效称为缓存抖动,这样对系统性能影响颇大。使用每个CPU数据将使得缓存影响降至最低,因为理想情况下只会访问自己的数据。percpu接口缓存—-对齐(cache_align)所有数据,以便确保在访问一个处理器的数据时,不会将另一个处理器的数据带入同一个缓存线上。
综上所述,使用每个CPU数据会省去许多数据上锁,它唯一的安全要求就是要禁止内核抢占。而这点的代价相比上锁要小得多,而且接口会自动帮你完成这个步骤。每个CPU数据在中断上下文或进程上下文中使用都很安全。但要注意,不能在访问每个CPU数据过程中睡眠—-否则,你就可能醒来后已经到了其他处理器上了。
目前并不要求必须使用每个CPU的新接口。只要你禁止了内核抢占,用手动方法就很好,但是新接口在将来更容易使用,而且功能也会得到长足的优化。如果确实决定在你的内核中使用每个CPU数据,请考虑使用新接口。但我要提醒的是—-新接口并不向后兼容之前的内核。
分配函数的选择
在这么多分配函数和方法中,有时并不能搞清楚到底该选择哪种方式分配—-但这确实很重要。如果你需要连续的物理页,就可以使用某个低级页分配器或kmalloc()。这是内核中内存分配的常用方式,也是大多数情况下你自己应该使用地内存分配方式。回忆一下,传递给这些函数的两个最常用的标志是GFP_ATOMIC和GFP_KERNEL。GFP_ATOMIC表示进行不睡眠高优先级分配,这是中断处理程序和其他不能睡眠的代码段的需要。对于可以睡眠的代码(比如没有持自旋锁的进程上下文代码),则应该使用GFP_KERNEL获取所需的内存。这个标志表示如果有必要,分配时可以睡眠。如果你想从高端内存进行分配
,就使用alloc_pages()。alloc_pages()函数返回一个指向struct page结构的指针,而不是一个指向某个逻辑地址的指针。因为高端内存很可能并没有映射,因此,访问它的唯一方式就是通过相应的struct page结构。为了获得真正的指针,应该调用kmap(),把高端内存映射到内核的逻辑地址空间。如果你不需要物理上连续的页
,而仅仅需要虚拟地址上连续的页,那么就使用vmalloc()(不过要记住vmalloc()相对kmalloc()来说,有一定的性能损失)。vmalloc()函数分配的内存虚拟地址是连续的,但它本身并不保证物理上的连续。这与用户空间的分配非常类似,它也是把物理内存块映射到连续的逻辑地址空间上。如果你要创建和撤销很多大的数据结构
,那么考虑建立slab高速缓存。slab层会给每个处理器维持一个对象高速缓存(空闲链表),这种高速缓存会极大地提高对象分配和回收的性能。slab层不是频繁地分配和释放内存,而是为你把事先分配好的对象存放到高速缓存中。当你需要一块新的内存来存放数据结构时,slab层一般无须另外去分配内存,而只需要从高速缓存中得到一个对象就可以了。
小结
本章中,我们学习了Linux内核任何管理内存。我们首先看到了内存空间的各种不同的描述单位,包括字节、页面和区。我们接着讨论了各种内存分配机制,其中包括页分配器和slab分配器。在内核中分配内存并非总是轻而易举,因为你必须小心地确保分配过程遵从内核特定的约束状态。比如分配过程中不得堵塞,或者访问文件系统等约束。为此我们讨论了gfp标识以及使用每个标识地针对场景。分配内存相对复杂是内核开发和用户程序开发的最大区别之一
。本章使用大量篇幅描述内存分配的各种不同接口—-通过这些不同调用接口,你应该能感觉到内核中分配内存为什么更复杂的原因。在本章基础上,在第13章我们讨论虚拟文件系统(VFS)—-负责管理文件系统且为用户空间程序提供一致性接口的内核子系统。我们继续深入!