Linux内核第12章
内核不能像用户空间那样奢侈地使用内存,内核与用户空间不同,它不具备这种能力,它不支持简单便捷的内存分配方式。比如,内核一般不能睡眠,此外处理内存分配错误对内核来说也很困难。正是因为这些限制和内存分配机制不能太复杂,所以在内核中获取内存要比在用户空间复杂得多。
12.1 页
内核把物理页作为内存管理的基本单位。尽管处理器的最小可寻址单位通常为字(甚至字节,一般一个字等于4字节),但是,内存管理单元(MMU,管理内存并把虚拟地址转换为物理地址的硬件)通常以页为单位进行处理。MMU以页page大小为单位来管理系统中的页表(这也是页表名称由来)。从虚拟内存的角度来看,页就是最小单位。
大多数32位系统结构支持4KB的页,64位支持8KB的页。
内核用struct page结构表示系统中的每个物理页,该结构位于<linux/mm_types.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;
};
flags域用来存放页的状态。这些状态包括页是不是脏的,是不是被锁定在内存中等。flags的每一位单独表示一种状态,所以至少可表示32种状态,这些标志定义在<linux/page-flags.h>中。
_count域存放页的引用计数----也就是这一页被引用了多少次。当计数值变为-1时,就说明当前内核并没有引用这一页,于是,在新的分配中就可以使用它。一个页可以由页缓存使用(这时,mapping域指向和这个页关联的address_space对象),或者作为私有数据(由private指向),或者作为进程页表中的映射。
virtual域是页的虚拟地址。通常情况下,它就是页在虚拟内存中的地址。有些内存(即高端内存)并不永久映射到内核地址空间上,这时这个域的值为NULL,需要的时候,必须动态地映射这些页。
page结构与物理页相关,而并非与虚拟页相关。因此,该结构对页的描述只是短暂的。内核仅仅用这个数据结构来描述当前时刻在相关的物理页中存放的东西。这种数据结构的目的在于描述物理系统内存本身,而不是描述包含在其中的数据。
内核用page来管理系统中的所有页,因为内核需要知道一个页是否空闲(是否被分配)。如果页已分配,内核还需要直到谁拥有这个页,拥有者可能是用户空间进程、动态分配的内核数据、静态内核代码或页高速缓存等。系统中的每个物理页都要分配一个这样的结构体。
12.2 区
由于硬件的限制,内核并不能对所有的页一视同仁。有些页位于内存中特定的物理地址上,所以不能将其用于一些特定的任务。由于存在这种限制,所以内核吧页划分为不同的区(zone),内核使用区对具有相似特性的页进行分组。
Linux必须处理如下两种由于硬件存在缺陷而引起的内存寻址问题:
-一些硬件只能用某些特定的内存地址来执行DMA(直接内存访问)。
-一些体系结构的内存的物理寻址范围比虚拟寻址范围大得多。这样,就有一些内存不能永久地映射到内核空间上。
因为存在上述限制,Linux主要使用了4种区:
-ZONE_DMA----这个区包含的页能用来执行DMA操作。
-ZONE_DMA32-----和ZONE_DMA类似,该区包含的页面可用来执行DMA操作;而和ZONE_DMA不同之处在于,这些页面只能被32位设备访问。在某些体系结构中,该区将比ZONE_DMA更大。
-ZONE_NORMAL------这个区包含的都是能正常映射的页。
-ZONE_HIGHMEM-----这个区包含“高端内存”,其中的页并不能永久地映射到内核地址空间。
这些区在<linux/mmzone.h>中定义。
ZONE_HIGHMEM是高端内存,其余是低端内存。
Linux把页划分为区,形成不同的内存池,这样就可以根据用途进行分配了。
某些分配可能需要从特定的区中获取页,而另一些分配则可以从多个区中获取页。但分配可以使用不同区各自分配,却不能直接跨区分配。
每个区都用struct zone表示,在<linux/mmzone.h>中定义:
struct zone{
unsigned long watermark[NR_WMARK];
unsigned long lowmem_reserve[MAX_NR_ZONES];
struct per_cpu_pageset pageset[NR_CPUS];
spinlock_t lock;
struct free_area free_area[MAX_ORDER];
spinlock_t lru_lock;
struct zone_lru{
struct list_head list;
unsigned long nr_saved_scan;
}lru[NR_LRU_LISTS];
struct zone_reclaim_stat reclaim_stat;
unsigned long pages_scanned;
unsigned long flags;
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
int prev_priority;
unsigned int inactive_ratio;
wait_queue_head_t *wait_table;
unsigned long wait_table_hash_nr_entries;
unsigned long wait_table_bits;
struct pglist_data *zone_pgdat;
unsigned long zone_start_pfn;
unsigned long spanned_pages;
unsigned long present_pages;
const char *name;
};
一般系统只有三个区,所以只需要三个zone结构。
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的order次方个连续的物理页,并返回一个指针,该指针指向第一个页的page结构体;如果出错,就返回NULL。可以用下面这个函数把给定的页转换成它的逻辑地址:
void *page_address(struct page *page)
该函数返回一个指针,指向给定物理页当前所在的逻辑地址
以上两个函数有一个等同的操作:
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
它直接返回所请求的第一个页的逻辑地址,因为是连续的,所以其它页也会紧随其后。
如果只需要一页,可用如下函数:
struct page (alloc_page(gfp_t gfp_mask)
unsigned long __get_free_page(gfp_t gfp_mask)
12.3.1 获得填充为0的页
如果需要让返回的页的内容全为0,用下面这个函数:
unsigned long get_zeroed_page(unsigned int gfp_mask)
用户空间的页在返回之前,把分配好的页都填充为0,或者其它清理工作,以保障系统安全。
12.3.2 释放页
当不再需要页时可以用下面的函数释放它们:
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值,这些都可能导致系统崩溃。
对于常用的以字节为单位的分配来说,内核提供的函数是kmalloc()。
12.4 kmalloc()
kmalloc()函数与用户空间的malloc()一族函数非常类似,只不过它多了一个flags参数。kmalloc()函数是一个简单的接口,用它可以获得以字节为单位的一块内核内存。
kmalloc()在<linux/slab.h>中声明:
void *kmalloc(size_t size, gfp_t flags)
这个函数返回一个指向内存块的指针,其内存块至少要有size大小。所分配的内存区在物理上是连续的。在出错时,它返回NULL。
12.4.1 gfp_mask标志(gfp_t类型数据)
这些标志可分为3类:行为修饰符、区修饰符及类型。
行为修饰符表示内核应当如何分配所需的内存。在某些特定的情况下,只能使用某些特定的方法分配内存。例如中断处理程序就要求内核在分配内存的过程中不能睡眠,因为中断中断处理程序不能被重新调度。
区修饰符表示从那里分配内存。内核把物理内存分为了多个区,每个区用于不同的目的。
类型标志组合了行为修饰符和区修饰符,将各种可能用到的组合归纳为不同类型,简化了修饰符的使用;这样,你只需要指定一个类型标志就可以了。
1.行为修饰符
标志-----描述
__GFP_WAIT-----分配器可以睡眠
__GFP_HIGH-----分配器可以访问紧急事件缓冲池
__GFP_IO-----分配器可以启动磁盘I/O
__GFP_FS-----分配器可以启动文件系统I/O
__GFP_COLD-----分配器应该使用高速缓存中快要淘汰出去的页
__GFP_NOWARM-----分配器将不打印失败警告
__GFP_REPEAT-----分配器在分配失败时重复进行分配,但是这次分配还存在失败的可能
__GFP_NOFALL-----分配器将无限地重复进行分配,分配不能失败。
__GFP_NORETRY-----分配器在分配失败时绝不会重新分配
__GFP_NO_GROW-----由slab层内部使用
__GFP_COMP-----添加混合页元数据,在hugetlb的代码内部使用
可以用位或指定多个修饰符
2.区修饰符
通常分配可以从任何区开始,不过,内核优先从ZONE_NORMAL开始,这样可以确保其它区在需要时有足够的空闲页可供使用。
标志-----描述
__GFP_DMA-----从ZONE_DMA分配
__GFP_DMA32-----只在ZONE_DMA32分配
__GFP_HIGHMEM-----从ZONE_HIGHMEM或ZONE_NORMAL分配
默认是从ZONE_NORMAL开始分配。
不能给_get_free_pages()或kmalloc()指定ZONE_HIGHMEM,因为这两个函数返回的都是逻辑地址,而不是page结构,这两个函数分配的内存当前有可能还没有映射到内核的虚拟地址空间,因此,也可能根本就没有逻辑地址。只有alloc_pages()才能分配高端内存。
3.类型标志
类型标志指定所需的行为和区描述符以完成特殊类型的处理。
标志-----描述
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_IO)
GFP_KERNEL-----(__GFP_WAIT | __GFP_IO | __GFP_FS)
GFP_USER-----(__GFP_WAIT | __GFP_IO | __GFP_FS)
GFP_HIGHUSER-----(__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HIGHMEM)
GFP_DMA-----__GFP_DMA
内核中最常用的标志是GFP_KERNEL。
12.4.2 kfree()
kmalloc()的另一端就是kfree(),声明在<linux/slab.h>中:
void kfree(const char*ptr)
kfree()函数释放由kmalloc()分配出来的内存块。如果想要释放的内存不是由kmalloc()分配的,或者想要释放的内存早被释放了,比如说释放属于内核其它部分的内存,调用这个函数就会导致严重的后果。但kfree(NULL)是安全的。
12.5 vmalloc()
vmalloc()函数的工作方式类似于kmalloc(),只不过前者分配的内存虚拟地址是连续的,而物理地址则无须连续。这也是用户空间分配函数的工作方式:由malloc()返回的页在进程的虚拟地址空间内是连续的,但是,这并不保证它们在物理RAM中也是连续的。kmalloc()函数确保页在物理地址上是连续的(虚拟地址自然也是连续的)。vmalloc()函数只确保页在虚拟地址空间内是连续的。它通过分配非连续的物理内存快,再修正页表,把内存映射到逻辑地址空间的连续区域中,就能做到这点。
大多数情况下,只有硬件设备需要得到物理地址连续的内存。在很多体系结构上,硬件设备存在于内存管理单元以外,她根本不理解什么是虚拟地址。因此,硬件设备用到的任何内存区域必须是物理上连续的块。
kmalloc()性能高于vmalloc(),vmalloc()函数为了把物理上不连续的页转换为虚拟地址空间上连续的页,必须建立页表项。而且,通过vmalloc()获得的页必须一个一个地进行映射(因为它们物理上是不连续的),这就会导致比直接内存映射大得多的TLB抖动。因此vmalloc()仅在不得已时才会使用---典型的就是为了获得大块内存时。
vmalloc()函数声明在<linux/vmalloc.h>中,定义在<mm/vmalloc.c>中。用法与malloc()相同:
void* vmalloc(unsigned long size)
该函数返回一个指针,指向逻辑上连续的一块内存区,大小至少为size。发生错误时返回NULL。函数可能睡眠,因此,不能从中断上下文中进行调用,也不能从其他不允许阻塞的情况下进行调用。
要释放通过vmalloc()所获得的内存,使用下面的函数:
void vfree(const void *ptr)
这个函数会释放从ptr开始的内存块,其中ptr是以前由vmalloc()分配的内存块的地址。这个函数也可以睡眠,因此,不能从中断上下文中调用。没有返回值。
12.6 slab层
分配和释放数据结构是所有内核中最普遍的操作之一。为了便于数据的频繁分配和回收,编程人员常常用到空闲链表。空闲链表包含可供使用的、已经分配好的数据结构快。当代码需要一个新的数据结构实例时,就可以从空闲链表中抓取一个,而不需要分配内存,再把数据放进去。以后,当不再需要这个数据结构的实例时,就把它放回空闲链表,而不是释放它。------此时空闲链表相当于对象高速缓存--快速存储频繁使用的对象类型。
在内核中,空闲链表面临的主要问题之一是不能全局控制。
Linux内核提供了slab层(也就是所谓的slab分配器)。slab分配器扮演了通用数据结构缓存层的角色。
slab分配器试图在几个基本原则之间寻求一种平衡:
-频繁使用的数据结构也会频繁分配和释放,因此应当缓存它们。
-频繁分配和回收必然会导致内存碎片(难以找到大块连续的可用内存)。为了避免这种现象,空闲链表的缓存会连续地存放。因为已释放的数据结构又会放回空闲链表,因此不会导致碎片。
-回收的对象可以立即投入下一次分配,因此,对于频繁的分配和释放,空闲链表能够提高其性能。
-如果分配器知道对象大小、页大小和总的高速缓存的大小这样的概念,它会做出更明智的决策。
-如果让部分缓存专属于单个处理器(对系统上的每个处理器独立而唯一),那么,分配和释放就可以在不加SMP的情况下进行。
-如果分配器是与NUMA相关的,它就可以从相同的内存节点为请求者进行分配。
-对存放的对象进行着色(color),以防止多个对象映射到相同的高速缓存行(cache line)。
12.6.1 slab层的设计
slab层把不同的对象划分为所谓高速缓存组,其中每个高速缓存组都存放不同类型的对象。每种对象类型对应一个高速缓存。例如一个高速缓存用于存放进程描述符(task_struct结构的一个空闲链表),而另一个高速缓存存放索引节点对象(struct inode)。kmalloc()接口建立在slab层之上,使用了一组通用高速缓存。
然后这些高速缓存又被划分为slab。slab由一个或多个物理上连续的页组成。一般情况下,slab也就仅仅由一页组成。每个高速缓存可以由多个slab组成。
每个slab都包含一些对象成员,这里的对象指的是被缓存的数据结构。每个slab处于3种状态之一:满、部分满或空。一个满的slab没有空闲的对象,一个空的slab没有分配出任何对象,一个部分满的slab有一些对象已分配出去,有些对象还空闲着。
当内核的某一部分需要一个新的对象时,先从部分满的slab中进行分配。如果没有部分满的slab,就从空的slab中进行分配。如果没有空的slab,就创建一个slab。
例如inode结构,该结构是磁盘索引节点在内存中的体现。这些数据结构会频繁地创建和释放,因此,用slab分配起来管理它们就很有必要。因而struct inode就由inode_cachep(命名规范)高速缓存进行分配。这种高速缓存由一个或多个slab组成----由多个slab组成的可能性大一些,因为这样的对象数量很大。每个slab包含尽可能多的struct inode对象。当内核请求分配一个新的inode结构时,内核就从部分满的slab或空的slab返回一个指向已分配但未使用的结构的指针。当内核用完inode对象后,slab分配器就把该对象标记为空闲。
每个高速缓存都使用kmem_cache结构来表示。这个结构包含3个链表: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,这是通过__get_free_pages()低级内核页分配器进行的。
static void *kmem_getpages(struct kmem_cache *cachep, gfp_t flags, int nodeid)
释放slab,过程中调用了free_pages():
static void *kmem_freepages(struct kmem_cache *cachep, gfp_t flags)
12.6.2 slab分配器的接口
一个新的高速缓存通过以下函数创建:
struct kmem_cache *kmem_cache_create(const char*name, size_t size, size_t align, unsigned long flags, void (*ctor)(void*) );
第一个参数是一个字符串,存放着高速缓存的名字;第二个参数是高速缓存中每个元素的大小;第三个参数是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。这个函数不能在中断上下文中调用,因为他可能睡眠。
要撤销一个高速缓存,则调用:
int kmem_cache_destroy(struct kmem_cache *cachep)
也不能在中断上下文中调用。调用该函数前必须确保存在以下两个条件:
1)高速缓存中所有slab都必须为空。
2)在调用kmem_cache_destroy()的过程中不再访问这个高速缓存。
该函数在成功时返回0,否则返回非0值。
1.从缓存中分配
创建高速缓存之后,就可以通过下列函数获取对象:
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
该函数从高速缓存cachep中返回一个指向对象的指针。如果高速缓存中的所有slab都没有空闲的对象,那么slab层必须通过kmem_getpages()获取新的页,flags的值传递给_get_free_pages()。
最后释放一个对象,并把它返回给原先的slab,可以使用下面这个函数:
void kmem_cache_free( struct kmem_cache *cachep, void *objp)
这样就能把cachep中的对象objp标记为空闲。
2.slab分配器的使用实例
12.7在栈上的静态分配
用户空间能够奢侈地负担起非常大的栈,而且占空间还可以动态增长,相反,内核却不能这么奢侈---内核栈小而且固定。当给每个进程分配一个固定大小的小栈后,不但可以减少内存的消耗,而且内核也无须负担太重的栈管理任务。
12.7.1 单页内核栈
内核使用了“中断栈”。中断栈为每个进程提供一个用于中断处理程序的栈。有了这个选项,中断处理程序不用再和被中断进程共享一个内核栈,它们可以使用自己的栈了。对每个进程来说仅仅耗费了一页。
12.7.2 在栈上光明正大地工作
在任意一个函数中,都要尽量节省栈资源,因为内核没有在管理内核栈上做足工作,因此,当栈溢出时,多出的数据就会直接溢出来,覆盖掉紧邻堆栈末端的东西,且首先面临考验的就是内核栈底的thread_info结构。
12.8 高端内存的映射
根据定义,在高端内存中的页不能永久地映射到内核地址空间上。因此通过alloc_pages()函数以__GFP_HIGHMEM标志获得的页不可能有逻辑地址。
12.8.1 永久映射
要映射一个给定的page结构到内核地址空间,可以使用定义在文件linux/highmem.h中的这个函数:
void *kmap(struct page*page)
这个函数在高端内存或低端内存上都能用。如果page结构对应的是低端内存中的一页,函数只会单纯地返回该页的虚拟地址。如果页位于高端内存,则会建立一个永久映射,再返回地址。这个函数可以睡眠,因此kmap()只能用在进程上下文中。
因为允许永久映射的数量是有限的,当不再需要高端内存时,应该解除映射,这可以通过下列函数完成:
void kunmap( struct page *page)
12.8.2 临时映射
当必须创建一个映射而当前的上下文又不能睡眠时,内核提供了临时映射(也就是原子映射)。有一组保留的映射,它们可以存放新创建的临时映射。内核可以原子地把高端内存中的一个页映射到某个保留的映射中。因此,临时映射可以用在不能睡眠的地方,比如中断处理程序中,因为获取映射时绝不会阻塞。
通过下列函数建立一个临时映射:
void *kmap_atomic( struct page *page, enum km_type type)
参数type是下列枚举类型之一,
。。。
通过以下函数取消映射:
void kunmap(_atomic( void *kvaddr, enum km_type type)
这个函数也不会阻塞。
12.9 每个CPU的分配
支持SMP的现代操作系统使用每个CPU上的数据,对于给定的处理器其数据是唯一的。一般来说,每个CPU的数据存放在一个数组中。数组中的每一项对应着系统上一个存在的处理器,按当前处理器号确定这个数组的当前元素。
因为每个处理器有自己的数据,所以处理器间不会之间导致数据同步问题。
此时内核抢占会引起下面提到的两个问题:
1)如果你的代码被其它处理器抢占并重新调度,那么这时CPU变量(代码被抢占前获取当时的CPU变量存放本地)就会无效,因为它指向的错误的处理器。所以(通常代码获得当前处理器后是不可以睡眠的)
2)如果另一个任务抢占了你的代码,那么有可能在同一个处理器上发生并发访问(两个任务都访问当前处理器上的同一个数据),这时存在竞争条件。
不过在获取当前处理器号,即调用get_cpu()时,就已经禁止了内核抢占。相应的在调用put_cpu()时又重新激活当前处理器号。
12.10 新的每个CPU接口
2.6内核为了方便创建和操作每个CPU数据,而引进了新的操作接口,称作percpu。
12.10.1 编译时的每个CPU数据
在编译时定义每个CPU数据变量:
DEFINE_PER_CPU(type, name);
这个语句为系统中的每一个处理器都创建了一个类型为type,名字为name的变量实例。如果需要在别处声明变量,以防止编译时警告,可以使用:
DECLARE_PER_CPU(type, name);
可以利用get_cpu_var()和put_cpu_var()例程操作变量。调用get_cpu_var()返回当前处理器上的指定变量,同时将它禁止抢占;另一方面put_cpu_var()将相应的重新激活抢占。
get_cpu_var(name)++;
put_cpu_var(name);
12.10.2 运行时的每个CPU数据
内核实现每个CPU数据的动态分配方法类似于kmalloc()。该例程为系统上的每个处理器创建所需内存的实例:
void *alloc_percpu(type);
void *__alloc_percpu(size_t size, size_t align);
void free_percpu(const void *);
宏alloc_percpu()给系统中的每个处理器分配一个指定类型对象的实例。它其实是宏__alloc_percpu()的一个封装,这个原始宏接收的参数有两个:一个是要分配的实际字节数,一个是分配时要按多少字节对齐。而封装后的alloc_percpu()按照单字节对齐----按照给定类型的自然边界对齐。
相应的free_percpu()将释放所有处理器上指定的每个CPU数据。
12.11 使用每个CPU数据的原因
使用每个CPU数据具有不少好处。首先是减少了数据锁定(省去一些锁操作)。
第二个好处是使用每个CPU数据可以大大减少缓存失效。失效发生在处理器试图使它们的缓存保持同步时。
唯一的安全要求是禁止内核抢占。还有不能在访问每个CPU数据过程中睡眠----否则,可能醒来后已经到了其它处理器上。