EDA365欢迎您登录!
您需要 登录 才可以下载或查看,没有帐号?注册
x
内核的内存使用不像用户空间那样随意,内核的内存出现错误时也只有靠自己来解决(用户空间的内存错误可以抛给内核来解决)。 所有内核的内存管理必须要简洁而且高效。 主要内容: - 内存的管理单元
- 获取内存的方法
- 获取高端内存
- 内核内存的分配方式
- 总结
# B) }0 R" S: J7 M4 ]6 J6 J
1 E& ^' o1 n2 l1. 内存的管理单元内存最基本的管理单元是页,同时按照内存地址的大小,大致分为3个区。
8 y' f6 v5 e# a4 K" O2 `1.1 页页的大小与体系结构有关,在 x86 结构中一般是 4KB或者8KB。 可以通过 getconf 命令来查看系统的page的大小: [wangyubin@localhost ]$ getconf -a | grep -i 'page'PAGESIZE 4096PAGE_SIZE 4096_AVPHYS_PAGES 637406_PHYS_PAGES 20128638 L6 V) P) l; ~( u% S3 T/ [
以上的 PAGESIZE 就是当前机器页大小,即 4KB " J& t/ `+ v& s
页的结构体头文件是: <linux/mm_types.h> 位置:include/linux/mm_types.h . B& U' l, _6 B+ E6 \7 c
/* * 页中包含的成员非常多,还包含了一些联合体 * 其中有些字段我暂时还不清楚含义,以后再补上。。。 */struct page { unsigned long flags; /* 存放页的状态,各种状态参见<linux/page-flags.h> */ atomic_t _count; /* 页的引用计数 */ union { atomic_t _mapcount; /* 已经映射到mms的pte的个数 */ struct { /* 用于slab层 */ u16 inuse; u16 objects; }; }; union { struct { unsigned long private; /* 此page作为私有数据时,指向私有数据 */ struct address_space *mapping; /* 此page作为页缓存时,指向关联的address_space */ };#if USE_SPLIT_PTLOCKS spinlock_t ptl;#endif struct kmem_cache *slab; /* 指向slab层 */ struct page *first_page; /* 尾部复合页中的第一个页 */ }; union { pgoff_t index; /* Our offset within mapping. */ void *freelist; /* SLUB: freelist req. slab lock */ }; struct list_head lru; /* 将页关联起来的链表项 */#if defined(WANT_PAGE_VIRTUAL) void *virtual; /* 页的虚拟地址 */#endif /* WANT_PAGE_VIRTUAL */#ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS unsigned long debug_flags; /* Use atomic bitops on this */#endif#ifdef CONFIG_KMEMCHECK /* * kmemcheck wants to track the status of each byte in a page; this * is a pointer to such a status block. NULL if not tracked. */ void *shadow;#endif};![]()
2 c- L& A4 ~. P0 @ V1 z. o: g$ B& J6 }; G$ j/ |
物理内存的每个页都有一个对应的 page 结构,看似会在管理上浪费很多内存,其实细细算来并没有多少。 比如上面的page结构体,每个字段都算4个字节的话,总共40多个字节。(union结构只算一个字段) ! o. n6 S/ B# p7 X ?! ?. R) h9 }
那么对于一个页大小 4KB 的 4G内存来说,一个有 4*1024*1024 / 4 = 1048576 个page, 一个page 算40个字节,在管理内存上共消耗内存 40MB左右。
1 P1 n) ?9 |8 o5 i! Q! o如果页的大小是 8KB 的话,消耗的内存只有 20MB 左右。相对于 4GB 来说并不算很多。
8 O7 ^$ H# z6 K+ ^* w1.2 区页是内存管理的最小单元,但是并不是所有的页对于内核都一样。 内核将内存按地址的顺序分成了不同的区,有的硬件只能访问有专门的区。
" a% F' l* B) f! i% N$ a1 _* ~内核中分的区定义在头文件 <linux/mmzone.h> 位置:include/linux/mmzone.h 内存区的种类参见 enum zone_type 中的定义。 ) G% V- \. P1 q8 L+ n- R+ R# B
内存区的结构体定义也在 <linux/mmzone.h> 中。 具体参考其中 struct zone 的定义。
" ^! ?4 w5 j2 ?5 q) C其实一般主要关注的区只有3个: 区 | 描述 | 物理内存 | ZONE_DMA | DMA使用的页 | <16MB | ZONE_NORMAL | 正常可寻址的页 | 16~896MB | ZONE_HIGHMEM | 动态映射的页 | >896MB |
/ ?6 D- }! Z. H+ }某些硬件只能直接访问内存地址,不支持内存映射,对于这些硬件内核会分配 ZONE_DMA 区的内存。 某些硬件的内存寻址范围很广,比虚拟寻址范围还要大的多,那么就会用到 ZONE_HIGHMEM 区的内存, 对于 ZONE_HIGHMEM 区的内存,后面还会讨论。 对于大部分的内存申请,只要用 ZONE_NORMAL 区的内存即可。
' @# Z5 N* ^+ {2. 获取内存的方法内核中提供了多种获取内存的方法,了解各种方法的特点,可以恰当的将其用于合适的场景。
$ n0 w+ C- N# M, z2.1 按页获取 - 最原始的方法,用于底层获取内存的方式以下分配内存的方法参见:<linux/gfp.h> 方法 | 描述 | 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,返回指向其逻辑地址的指针 | + a) N* o) _! F5 F
alloc** 方法和 get** 方法的区别在于,一个返回的是内存的物理地址,一个返回内存物理地址映射后的逻辑地址。 如果无须直接操作物理页结构体的话,一般使用 get** 方法。 & w, I$ z) o) k
相应的释放内存的函数如下:也是在 <linux/gfp.h> 中定义的 extern void __free_pages(struct page *page, unsigned int order);extern void free_pages(unsigned long addr, unsigned int order);extern void free_hot_page(struct page *page);, ~1 H) h4 M/ @7 @) a" H
在请求内存时,参数中有个 gfp_mask 标志,这个标志是控制分配内存时必须遵守的一些规则。 gfp_mask 标志有3类:(所有的 GFP 标志都在 <linux/gfp.h> 中定义) - 行为标志 :控制分配内存时,分配器的一些行为
- 区标志 :控制内存分配在那个区(ZONE_DMA, ZONE_NORMAL, ZONE_HIGHMEM 之类)
- 类型标志 :由上面2种标志组合而成的一些常用的场景
5 E$ L. D1 f( u- T3 _1 y% [
( { d4 g7 |, R8 H. c行为标志主要有以下几种: 行为标志 | 描述 | __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 的代码内部使用 |
( \. c6 m: `/ K4 o% D8 l区标志主要以下3种: 区标志 | 描述 | __GFP_DMA | 从 ZONE_DMA 分配 | __GFP_DMA32 | 只在 ZONE_DMA32 分配 (注1) | __GFP_HIGHMEM | 从 ZONE_HIGHMEM 或者 ZONE_NORMAL 分配 (注2) |
注1:ZONE_DMA32 和 ZONE_DMA 类似,该区包含的页也可以进行DMA操作。 9 ~0 H5 |% ~. n
唯一不同的地方在于,ZONE_DMA32 区的页只能被32位设备访问。
3 T, X9 x5 U# w @7 J5 {8 |注2:优先从 ZONE_HIGHMEM 分配,如果 ZONE_HIGHMEM 没有多余的页则从 ZONE_NORMAL 分配。
" r, v' H; _6 _. @6 `) [类型标志是编程中最常用的,在使用标志时,应首先看看类型标志中是否有合适的,如果没有,再去自己组合 行为标志和区标志。 类型标志 | 实际标志 | 描述 | GFP_ATOMIC | __GFP_HIGH | 这个标志用在中断处理程序,下半部,持有自旋锁以及其他不能睡眠的地方 | GFP_NOWAIT | 0 | 与 GFP_ATOMIC 类似,不同之处在于,调用不会退给紧急内存池。
8 X9 C' } z X: ]' f# A0 K: P& q这就增加了内存分配失败的可能性 | GFP_NOIO | __GFP_WAIT | 这种分配可以阻塞,但不会启动磁盘I/O。
. ^. v+ d: Q, F4 ]3 L这个标志在不能引发更多磁盘I/O时能阻塞I/O代码,可能会导致递归 | GFP_NOFS | (__GFP_WAIT | __GFP_IO) | 这种分配在必要时可能阻塞,也可能启动磁盘I/O,但不会启动文件系统操作。
6 T$ H% F& S+ d! L这个标志在你不能再启动另一个文件系统的操作时,用在文件系统部分的代码中 | GFP_KERNEL | (__GFP_WAIT | __GFP_IO | __GFP_FS ) | 这是常规的分配方式,可能会阻塞。这个标志在睡眠安全时用在进程上下文代码中。 " x+ V( r9 K- c4 }
为了获得调用者所需的内存,内核会尽力而为。这个标志应当为首选标志 | GFP_USER | (__GFP_WAIT | __GFP_IO | __GFP_FS ) | 这是常规的分配方式,可能会阻塞。用于为用户空间进程分配内存时 | GFP_HIGHUSER | (__GFP_WAIT | __GFP_IO | __GFP_FS )|__GFP_HIGHMEM) | 从 ZONE_HIGHMEM 进行分配,可能会阻塞。用于为用户空间进程分配内存 | GFP_DMA | __GFP_DMA | 从 ZONE_DMA 进行分配。需要获取能供DMA使用的内存的设备驱动程序使用这个标志 - e; U1 h* S1 O6 `* i. ^' q$ e
通常与以上的某个标志组合在一起使用。 |
% R( X$ I* a4 |2 w6 R以上各种类型标志的使用场景总结: 场景 | 相应标志 | 进程上下文,可以睡眠 | 使用 GFP_KERNEL | 进程上下文,不可以睡眠 | 使用 GFP_ATOMIC,在睡眠之前或之后以 GFP_KERNEL 执行内存分配 | 中断处理程序 | 使用 GFP_ATOMIC | 软中断 | 使用 GFP_ATOMIC | tasklet | 使用 GFP_ATOMIC | 需要用于DMA的内存,可以睡眠 | 使用 (GFP_DMA|GFP_KERNEL) | 需要用于DMA的内存,不可以睡眠 | 使用 (GFP_DMA|GFP_ATOMIC),或者在睡眠之前执行内存分配 |
! z1 R3 k8 ?. o! b1 ^ t+ k- I" R; w2.2 按字节获取 - 用的最多的获取方法这种内存分配方法是平时使用比较多的,主要有2种分配方法:kmalloc()和vmalloc() kmalloc的定义在 <linux/slab_def.h> 中 ![]()
! \$ _, x( W7 a, |! `/** * @size - 申请分配的字节数 * @flags - 上面讨论的各种 gfp_mask */static __always_inline void *kmalloc(size_t size, gfp_t flags)#+end_srcvmalloc的定义在 mm/vmalloc.c 中#+begin_src C/** * @size - 申请分配的字节数 */void *vmalloc(unsigned long size) , r; r$ X4 `7 C# V" U: G% o8 h
k" ^9 y1 Z' @$ e" L) a; _3 T$ rkmalloc 和 vmalloc 区别在于: - kmalloc 分配的内存物理地址是连续的,虚拟地址也是连续的
- vmalloc 分配的内存物理地址是不连续的,虚拟地址是连续的
* ]' b4 ]3 e- f o0 h / {/ Q$ e% P9 F. `7 t
因此在使用中,用的较多的还是 kmalloc,因为kmalloc 的性能较好。 因为kmalloc的物理地址和虚拟地址之间的映射比较简单,只需要将物理地址的第一页和虚拟地址的第一页关联起来即可。 而vmalloc由于物理地址是不连续的,所以要将物理地址的每一页都和虚拟地址关联起来才行。 & L) C% D) c- \- _" N& K8 ?. {
kmalloc 和 vmalloc 所对应的释放内存的方法分别为: void kfree(const void *)void vfree(const void *)
: R1 [5 m: w1 ~$ A% ?* Q/ P4 R. h% q& l. ^, Q+ P$ P
2.3 slab层获取 - 效率最高的获取方法频繁的分配/释放内存必然导致系统性能的下降,所以有必要为频繁分配/释放的对象内心建立缓存。 而且,如果能为每个处理器建立专用的高速缓存,还可以避免 SMP锁带来的性能损耗。
) c4 O, e+ Z! l' f+ l0 y4 I4 V2.3.1 slab层实现原理linux中的高速缓存是用所谓 slab 层来实现的,slab层即内核中管理高速缓存的机制。 整个slab层的原理如下: - 可以在内存中建立各种对象的高速缓存(比如进程描述相关的结构 task_struct 的高速缓存)
- 除了针对特定对象的高速缓存以外,也有通用对象的高速缓存
- 每个高速缓存中包含多个 slab,slab用于管理缓存的对象
- slab中包含多个缓存的对象,物理上由一页或多个连续的页组成* r7 u. g" \ U
5 Z& n1 ]5 N) k+ S
高速缓存->slab->缓存对象之间的关系如下图: w8 {4 t9 a. U6 e* h
2.3.2 slab层的应用slab结构体的定义参见:mm/slab.c ![]()
/ B- W( u; c) j; R+ w' q5 P4 |6 xstruct slab { struct list_head list; /* 存放缓存对象,这个链表有 满,部分满,空 3种状态 */ unsigned long colouroff; /* slab 着色的偏移量 */ void *s_mem; /* 在 slab 中的第一个对象 */ unsigned int inuse; /* slab 中已分配的对象数 */ kmem_bufctl_t free; /* 第一个空闲对象(如果有的话) */ unsigned short nodeid; /* 应该是在 NUMA 环境下使用 */}; ! g( r( t+ s# F+ Q+ v
# B8 e: Z$ U6 i$ V! o s [8 f) X, w
" S I) s+ ^0 o2 ?: O, l. G1 Dslab层的应用主要有四个方法: - 高速缓存的创建
- 从高速缓存中分配对象
- 向高速缓存释放对象
- 高速缓存的销毁
! r, ~1 [3 _- w+ ^
3 H& [& V. T o& t3 l i. v O
/** * 创建高速缓存 * 参见文件: mm/slab.c * 这个函数的注释很详细,这里就不多说了。 */struct kmem_cache *kmem_cache_create (const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *))/** * 从高速缓存中分配对象也很简单 * 函数参见文件:mm/slab.c * @cachep - 指向高速缓存指针 * @flags - 之前讨论的 gfp_mask 标志,只有在高速缓存中所有slab都没有空闲对象时, * 需要申请新的空间时,这个标志才会起作用。 * * 分配成功时,返回指向对象的指针 */void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)/** * 向高速缓存释放对象 * @cachep - 指向高速缓存指针 * @objp - 要释放的对象的指针 */void kmem_cache_free(struct kmem_cache *cachep, void *objp)/** * 销毁高速缓存 * @cachep - 指向高速缓存指针 */void kmem_cache_destroy(struct kmem_cache *cachep)![]()
4 f: H+ d, Y7 L
+ V: X7 D0 e2 C0 j* s+ s6 n
6 E4 m) Y! t, x我做了创建高速缓存的例子,来尝试使用上面的几个函数。 测试代码如下:(其中用到的 kn_common.h 和 kn_common.c 参见之前的博客《Linux内核设计与实现》读书笔记(六)- 内核数据结构) - c9 a9 t2 w1 {5 C4 x4 ^% G# H" J: t. @
#include <linux/slab.h>#include <linux/slab_def.h>#include "kn_common.h"MODULE_LICENSE("Dual BSD/GPL");#define MYSLAB "testslab"static struct kmem_cache *myslab;/* 申请内存时调用的构造函数 */static void ctor(void* obj){ printk(KERN_ALERT "constructor is running....\n");}struct student{ int id; char* name;};static void print_student(struct student *);static int testslab_init(void){ struct student *stu1, *stu2; /* 建立slab高速缓存,名称就是宏 MYSLAB */ myslab = kmem_cache_create(MYSLAB, sizeof(struct student), 0, 0, ctor); /* 高速缓存中分配2个对象 */ printk(KERN_ALERT "alloc one student....\n"); stu1 = (struct student*)kmem_cache_alloc(myslab, GFP_KERNEL); stu1->id = 1; stu1->name = "wyb1"; print_student(stu1); printk(KERN_ALERT "alloc one student....\n"); stu2 = (struct student*)kmem_cache_alloc(myslab, GFP_KERNEL); stu2->id = 2; stu2->name = "wyb2"; print_student(stu2); /* 释放高速缓存中的对象 */ printk(KERN_ALERT "free one student....\n"); kmem_cache_free(myslab, stu1); printk(KERN_ALERT "free one student....\n"); kmem_cache_free(myslab, stu2); /* 执行完后查看 /proc/slabinfo 文件中是否有名称为 “testslab”的缓存 */ return 0;}static void testslab_exit(void){ /* 删除建立的高速缓存 */ printk(KERN_ALERT "*************************\n"); print_current_time(0); kmem_cache_destroy(myslab); printk(KERN_ALERT "testslab is exited!\n"); printk(KERN_ALERT "*************************\n"); /* 执行完后查看 /proc/slabinfo 文件中是否有名称为 “testslab”的缓存 */}static void print_student(struct student *stu){ if (stu != NULL) { printk(KERN_ALERT "**********student info***********\n"); printk(KERN_ALERT "student id is: %d\n", stu->id); printk(KERN_ALERT "student name is: %s\n", stu->name); printk(KERN_ALERT "*********************************\n"); } else printk(KERN_ALERT "the student info is null!!\n"); }module_init(testslab_init);module_exit(testslab_exit);![]()
0 z1 P$ V# y' l% d" m/ Y8 Y. `) T7 E7 A& A2 ~, \7 L! P2 f) [* S7 B
0 S% ~0 A4 y% u8 cMakefile文件如下: 2 ^! R9 _4 Z/ a0 N* J8 m/ I
# must complile on customize kernelobj-m += myslab.omyslab-objs := testslab.o kn_common.o#generate the pathCURRENT_PATH:=$(shell pwd)#the current kernel version numberLINUX_KERNEL:=$(shell uname -r)#the absolute pathLINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL)#complie objectall: make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules rm -RF modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned#cleanclean: rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned![]()
: a/ r1 W+ ^5 V' n% X8 C4 a
& Q }! w1 R$ ~& u- t+ w' ^: c
8 a; ]8 Y% [+ t8 G执行测试代码:(我是在 centos6.3 x64 上实验的) ; t# g' k0 o9 L" o6 v- u
[root@vbox chap12]# make[root@vbox chap12]# insmod myslab.ko [root@vbox chap12]# dmesg | tail -220 # 可以看到第一次申请内存时,系统一次分配很多内存用于缓存(构造函数执行了多次)[root@vbox chap12]# cat /proc/slabinfo | grep test #查看我们建立的缓存名在不在系统中testslab 0 0 16 202 1 : tunables 120 60 0 : slabdata 0 0 0[root@vbox chap12]# rmmod myslab.ko #卸载内核模块[root@vbox chap12]# cat /proc/slabinfo | grep test #我们的缓存名已经不在系统中了 " e9 b8 J& t5 I D* |
6 b7 U% p5 r& k$ U; }: c9 h* G; R9 X7 l& N3 R% t: I P0 |
3. 获取高端内存高端内存就是之前提到的 ZONE_HIGHMEM 区的内存。 在x86体系结构中,这个区的内存不能映射到内核地址空间上,也就是没有逻辑地址, 为了使用 ZONE_HIGHMEM 区的内存,内核提供了永久映射和临时映射2种手段: 3.1 永久映射永久映射的函数是可以睡眠的,所以只能用在进程上下文中。 ![]()
. |* [" g% a; v* G/* 将 ZONE_HIGHMEM 区的一个page永久的映射到内核地址空间 * 返回值即为这个page对应的逻辑地址 */static inline void *kmap(struct page *page)/* 允许永久映射的数量是有限的,所以不需要高端内存时,应该及时的解除映射 */static inline void kunmap(struct page *page) 1 ^/ n+ e( D* W1 H( \4 O2 H0 m# Y( X
) D7 N6 x; X& S
9 L- l2 z/ a) d& `/ Y3.2 临时映射临时映射不会阻塞,也禁止了内核抢占,所以可以用在中断上下文和其他不能重新调度的地方。 # l" k0 }1 A$ a+ H' v" C, I
/** * 将 ZONE_HIGHMEM 区的一个page临时映射到内核地址空间 * 其中的 km_type 表示映射的目的, * enum kn_type 的定义参见:<asm/kmap_types.h> */static inline void *kmap_atomic(struct page *page, enum km_type idx)/* 相应的解除映射是个宏 */#define kunmap_atomic(addr, idx) do { pagefault_enable(); } while (0)![]()
, H: j% U4 g9 v1 Y* w1 m, D2 b" [6 Y1 _+ H
以上的函数都在 <linux/highmem.h> 中定义的。
x+ h! N# S0 _7 ^+ t4. 内核内存的分配方式内核的内存分配和用户空间的内存分配相比有着更多的限制条件,同时也有着更高的性能要求。 下面讨论2个和用户空间不同的内存分配方式。
! F# d' v7 p P. }2 U* T5 i4 v6 d4.1 内核栈上的静态分配用户空间中一般不用担心栈上的内存不足,也不用担心内存的管理问题(比如内存越界之类的), 即使出了异常也有内核来保证系统的正常运行。
! Y5 I$ L7 i4 P7 O0 m; i3 }% c而在内核空间则完全不一样,不仅栈空间有限,而且为了管理的效率和尽量减少问题的发生, 内核栈一般都是小而且固定的。
) p: Z9 V1 N8 y7 O8 x3 G在x86体系结构中,内核栈的大小一般就是1页或2页,即 4KB ~ 8KB 内核栈可以在编译内核时通过配置选项将内核栈配置为1页, 配置为1页的好处是分配时比较简单,只有一页,不存在内存碎片的情况,因为一页是本就是分配的最小单位。 当有中断发生时,如果共享内核栈,中断程序和被中断程序共享一个内核栈会可能导致空间不足, 于是,每个进程除了有个内核栈之外,还有一个中断栈,中断栈一般也就1页大小。
" e {3 r/ D) l& y& k$ C$ Y' l查看当前系统内核栈大小的方法: [xxxxx@localhost ~]$ ulimit -a | grep 'stack'stack size (kbytes, -s) 8192
. B W$ h2 j, y; ?) h
# o1 M% n$ W5 V% O: B( j/ R4.2 按CPU分配与单CPU环境不同,SMP环境下的并行是真正的并行。单CPU环境是宏观并行,微观串行。 真正并行时,会有更多的并发问题。
; l6 L& R* l! c0 c4 F+ E假定有如下场景: ![]()
+ Q- `7 E+ B' D Z2 N6 Lvoid* p;if (p == NULL){/* 对 P 进行相应的操作,最终 P 不是NULL了 */}else{/* P 不是NULL,继续对 P 进行相应的操作 */}![]()
. X" u1 U B6 z: `3 i p1 B3 c
$ N0 C8 z( N: t7 M+ ~在上述场景下,可能会有以下的执行流程: - 刚开始 p == NULL
- 线程A 执行到 [if (p == NULL)] ,刚进入 if 内的代码时被线程B 抢占
. \. i0 \+ M0 D 由于线程A 还没有执行 if 内的代码,所以 p 仍然是 NULL - 线程B 抢占到CPU后开始执行,执行到 [if (p == NULL)]时, 发现 p 是 NULL,执行 if 内的代码
- 线程B 执行完后,线程A 重新被调度,继续执行 if 的代码
( [# a0 @8 j p0 a* \ L 其实此时由于线程B 已经执行完,p 已经不是 NULL了,线程A 可能会破坏线程B 已经完成的处理,导致数据不一致1 C) g0 E3 P6 P
1 u7 _7 @9 ?7 W+ d/ v在单CPU环境下,上述情况无需加锁,只需在 if 处理之前禁止内核抢占,在 else 处理之后恢复内核抢占即可。 而在SMP环境下,上述情况必须加锁,因为禁止内核抢占只能禁止当前CPU的抢占,其他的CPU仍然调度线程B 来抢占线程A 的执行
- x# R) [! L- U4 U" j& j$ o i. \SMP环境下加锁过多的话,会严重影响并行的效率,如果是自旋锁的话,还会浪费其他CPU的执行时间。 所以内核中才有了按CPU分配数据的接口。 按CPU分配数据之后,每个CPU自己的数据不会被其他CPU访问,虽然浪费了一点内存,但是会使系统更加的简洁高效。
) R2 L. P% L+ k8 y4.2.1 按CPU分配的优势按CPU来分配数据主要有2个优点: - 最直接的效果就是减少了对数据的锁,提高了系统的性能
- 由于每个CPU有自己的数据,所以处理器切换时可以大大减少缓存失效的几率 (*注1)
& ]) Z, v( p( T; a0 q% ^8 c % E5 Z* i) j& v3 O0 X0 h
注1:如果一个处理器操作某个数据,而这个数据在另一个处理器的缓存中时,那么存放这个数据的那个 处理器必须清理或刷新自己的缓存。持续的缓存失效成为缓存抖动,对系统性能影响很大。
* o: v: o% n. y. P0 i- S. w% e# w, N' o4.2.2 编译时分配可以在编译时就定义分配给每个CPU的变量,其分配的接口参见:<linux/percpu-defs.h> /* 给每个CPU声明一个类型为 type,名称为 name 的变量 */DECLARE_PER_CPU(type, name)/* 给每个CPU定义一个类型为 type,名称为 name 的变量 */DEFINE_PER_CPU(type, name)2 J- q/ k0 x! Y q% _
注意上面两个宏,一个是声明,一个是定义。 其实也就是 DECLARE_PER_CPU 中多了个 extern 的关键字
$ Q( ]5 n( ` G* n分配好变量后,就可以在代码中使用这个变量 name 了。 DEFINE_PER_CPU(int, name); /* 为每个CPU定义一个 int 类型的name变量 */get_cpu_var(name)++; /* 当前处理器上的name变量 +1 */put_cpu_var(name); /* 完成对name的操作后,激活当前处理器的内核抢占 */, B! ^& T! h6 w& D, S6 H# C
) H" I& \$ i2 J通过 get_cpu_var 和 put_cpu_var 的代码,我们可以发现其中有禁止和激活内核抢占的函数。 相关代码在 <linux/percpu.h> 中 #define get_cpu_var(var) (*({ \ extern int simple_identifier_##var(void); \ preempt_disable();/* 这句就是禁止当前处理器上的内核抢占 */ \ &__get_cpu_var(var); }))#define put_cpu_var(var) preempt_enable() /* 这句就是激活当前处理器上的内核抢占 */4 V3 t7 s- k* G- W0 B
9 c$ g Y- ]0 _* A1 J0 g. M4.2.3 运行时分配除了像上面那样静态的给每个CPU分配数据,还可以以指针的方式在运行时给每个CPU分配数据。 动态分配参见:<linux/percpu.h> ![]()
7 u0 P, s8 ^( A, x/* 给每个处理器分配一个 size 字节大小的对象,对象的偏移量是 align */extern void *__alloc_percpu(size_t size, size_t align);/* 释放所有处理器上已分配的变量 __pdata */extern void free_percpu(void *__pdata);/* 还有一个宏,是按对象类型 type 来给每个CPU分配数据的, * 其实本质上还是调用了 __alloc_percpu 函数 */#define alloc_percpu(type) (type *)__alloc_percpu(sizeof(type), \ __alignof__(type))![]()
) l8 [. c: t/ n6 \" M8 V
+ y5 M0 J9 W3 v8 I3 z# `) h& u/ ?$ a9 T- C4 ]4 P( |+ h
动态分配的一个使用例子如下: ![]()
6 q+ o3 G% j& a) f' |" s; m& Q( Svoid *percpu_ptr;unsigned long *foo;percpu_ptr = alloc_percpu(unsigned long);if (!percpu_ptr) /* 内存分配错误 */foo = get_cpu_var(percpu_ptr);/* 操作foo ... */put_cpu_var(percpu_ptr);![]()
+ _4 G; R6 P3 t' @& A# U9 x$ d' F' q( C; H; O* d
+ j% s- L; G7 F4 ~+ V5. 总结在众多的内存分配函数中,如何选择合适的内存分配函数很重要,下面总结了一些选择的原则: 应用场景 | 分配函数选择 | 如果需要物理上连续的页 | 选择低级页分配器或者 kmalloc 函数 | 如果kmalloc分配是可以睡眠 | 指定 GFP_KERNEL 标志 | 如果kmalloc分配是不能睡眠 | 指定 GFP_ATOMIC 标志 | 如果不需要物理上连续的页 | vmalloc 函数 (vmalloc 的性能不如 kmalloc) | 如果需要高端内存 | alloc_pages 函数获取 page 的地址,在用 kmap 之类的函数进行映射 | 如果频繁撤销/创建教导的数据结构 | 建立slab高速缓存 | ! P" W7 S0 H# h
|