http://blog.sina.com.cn/s/blog_65373f1401019dtz.html
linux kernel学习笔记-5 内存管理
1. 相关的数据结构
相比用户空间而言,在内核中分配内存往往受到更多的限制,比如内核中很多情况下不能睡眠,此外处理内存分配失败也不像用户空间那么容易。内核使用了页和区两种数据结构来管理内存:
1.1 页
内核把物理页作为内存管理的基本单位。尽管CPU的最小可寻址单位通常为字(甚至字节),但是MMU(内存管理单元,管理内存并把虚拟地址转换为物理地址的硬件)通常以页为单位进行处理;MMU以页为单位管理系统中的页表。从虚拟内存的角度而言,页就是最小单位。
不同体系结构下的页大小也不同,一般32位体系结构支持4KB的页,64位体系结构支持8KB的页。内核用struct page结构表示系统中的每个物理页:
struct page{
unsigned long flags; // 存放页的状态,包括页是不是脏的,是不是被锁定在内存等等;
// flags可以同时表示32种独立的状态
atomic_t _count; // 页的引用计数,当_count=-1时,表明该页没有被内核引用,
// 新的分配可以使用它。内核代码通常不直接检查该域,
// 而是调用page_count()函数进行检查,该函数的唯一参数是
// struct page结构。page_count()返回0代表页空闲,
// 返回正整数代表页被占用。
atomic_t _mapcount;
unsigned long private;
struct address_space *mapping; // 当页被页缓存使用时,maping域指向和这个页关联
// 的address_space对象
pgoff_t index;
struct list_head lru;
void *virtual; // 页在虚拟内存中的地址。高端内存并不永久的映射在内核地址
// 空间上,这时viutual域为NULL。
};
需要强调的是,struct page跟物理页相关,而非跟虚拟页相关。因此该结构对页的描述是短暂的,及时页中所包含的数据仍然存在,由于交换的原因,它们也可能不再和同一个struct page结构相关联。内核仅仅用这个数据结构来描述当前时刻在相关物理页中存放的数据——也就是说struct page的目的在于描述物理内存本身,而不是其中的数据。
内核用struct page来管理系统中所有的页。假如系统有4GB内存,每个物理页4KB的话,就有1M个页;每个struct page按40字节算,就需要40MB的空间来存放所有物理页的struct page。内核需要知道一个页是否空闲,如果某个页已经被分配,内核还要知道谁拥有这个页,拥有者可能是用户空间进程、动态分配的内核数据、静态内核代码或页高速缓存等等。
1.2 区
由于硬件的限制,内核不能对所有的页一视同仁。有些页位于内存中某些特殊的位置上,因此不能用于一些特定的用途。正因如此,内核把所有物理页划分为若干个区,同一个区中的物理页具有相似的特性。内核必须处理如下两种由于硬件缺陷而引起的内存寻址问题:
i> 一些硬件只能用某些特定的内存地址来执行DMA(直接内存访问);
ii> 一些体系结构的内存的物理寻址范围比虚拟寻址范围大得多,所以有些内存不能永久的映射到内核空间上
正因如此,内核把物理页分为下面的不同区(以x86-32体系结构为例):
区 描述 物理内存
ZONE_DMA DMA使用的页 <16MB
ZONE_NORMAL 正常可寻址的页 16~896MB
ZONE_HIGHMEM 所谓的高端内存,其中的页不能永久的映射到内核地址空间 >896MB
不同体系结构下,区的分布不同。例如某些体系结构上可以在内存任何地址上执行DMA,所以在这些体系结构上,ZONE_DMA为空,ZONE_NORMAL就可以直接用来分配;与此相反的是,在x86体系结构上,ISA设备只能对物理内存的前16MB执行DMA。ZONE_HIGHMEM也跟体系结构相关,有些体系结构中所有内存都可以被直接映射,所以ZONE_HIGHMEM为空;而32位x86系统中,ZONE_HIGHMEM为高于896MB的所有物理内存。
区的划分没有任何物理意义,只是内核为了方便管理页而进行的逻辑划分。某些分配可能需要从特定的区中获取页,比如DMA操作只能从ZONE_DMA中取得;但用于一般用途的内存既能从ZONE_DMA也可以从ZONE_NORMAL中获取,当然内核更应该让一般用途的内存从ZONE_NORMAL获取,以节省稀有的ZONE_DMA。
2. 内存管理接口
2.1 针对页的内存管理接口
接口函数原型 描述
struct page *alloc_pages
(gfp_t gfp_mask, unsigned int order); 该函数分配2^order(一般情况下order远大于1)个连续的物理页,并返回一个指针,该指针指向第一个页的struct page结构体,如果出错就返回NULL
struct page *alloc_page
(gfp_t gfp_mask); 只分配一页,返回该页的页结构指针
unsigned long __get_free_pages
(gfp_t gfp_mask, unsigned int order); 分配2^order个连续的物理页,返回第一个页的逻辑地址
unsigned long __get_free_page
(gfp_t gfp_mask); 只分配一页,返回该页的逻辑地址
unsigned long get_zeroed_page
(gfp_t gfp_mask); 跟__get_free_page()相同,只是该页的每个字节的每个比特都被填为0。这样是为了防止用户进程意外获取到某些敏感信息
void __free_pages
(struct page *page, unsigned int order); 释放从*page开始的连续2^order个物理页
void free_pages
(unsigned long addr,
unsigned int order); 释放从addr开始的连续2^order个物理页
void free_page
(unsigned long addr); 释放从addr开始的一页
2.2 kmalloc
当需要以页为单位进行分配时,上文的低级页分配函数很方便;但是如果需要以字节为单位的分配,则可以使用kmalloc()。kmalloc()可以获得以字节为单位的一块连续物理内存。
接口函数原型 描述
void * kmalloc
(size_t size, gfp_t gfp_mask); 该函数返回一个指向内存块的指针,这个内存块至少有size个字节,并且在物理上是连续的。
如果没有分配成功,则返回NULL。
void *kfree
(const void *ptr); 释放由kmalloc()分配得到的内存块
2.3 vmalloc
vmalloc类似于kmalloc,但是vmalloc分配的内存只有虚拟地址是确定连续的,物理地址不一定连续;而kmalloc则保证物理地址和虚拟地址都连续。
接口函数原型 描述
void * vmalloc
(unsigned long size); 该函数返回一个指向内存块的指针,这个内存块至少有size个字节,并且在逻辑上是连续的。
如果没有分配成功,则返回NULL。
该函数可能会睡眠。
void *vfree
(const void *ptr); 释放由vmalloc()分配得到的内存块。
该函数可能会睡眠
大部分情况下,只有硬件设备需要得到物理地址连续的内存,他们甚至不理解虚拟地址;而仅供软件使用的内存块就可以只是虚拟地址连续,因为软件只关心逻辑地址。
不过内核中很多地方本可以用vmalloc的却用了kmalloc,主要是因为kmalloc更快。vmalloc为了把物理上不连续的内存转换为虚拟地址空间上连续的页,需要专门建立页表项。所以vmalloc只是在万不得已时采用,典型的是需要获得超大块的内存时,例如当模块被动态插入到内核中时,就把模块装载到由vmalloc分配的内存上。
2.4 gfp_mask标志
在低级页分配函数以及kmalloc中都涉及了gfp_mask。gfp_mask是一些标志的集合,这些标志可以分为两大类:行为修饰符和区修饰符。行为修饰符表示内核如何分配所需的内存。区修饰符表示从哪里分配内存。例如下面的表格:
常用的行为修饰符
标志 描述
__GFP_WAIT 分配器可以睡眠
__GFP_IO 分配器可以启动磁盘I/O
__GFP_FS 分配器可以启动文件系统I/O
__GFP_HIGH 分配器可以访问紧急事件缓冲池
常用的区修饰符
标志 描述
__GFP_DMA 只从ZONE_DMA分配
__GFP_HIGHMEM 从ZONE_HIGHMEM或ZONE_NORMAL分配
为了方便使用,行为修饰符和区修饰符被组合成不同的类型标志,通常你只需要使用类型标志即可。
常用的类型标志
标志 修饰符组合 描述
GFP_KERNEL (__GFP_WAIT |
__GFP_IO |
__GFP_FS) 这个标志可以用于有可能睡眠的进程上下文代码中。这种分配可能会阻塞,也可能启动磁盘I/O。这个标志对内核如何请求内存没有任何约束,可以睡眠、交换、刷新一些页到硬盘等等,因而分配成功的概率较高。
GFP_ATOMIC __GFP_HIGH 这个标志用于ISR、下半部、持有自旋锁等不能睡眠的地方。调用者需要满足较多的限制,因此分配成功率没有GFP_KERNEL高,特别是当内存短缺时。
3. slab分配器
参见linux kernel学习笔记-2 slab分配器
4. 在栈上的分配
4.1 内核栈、用户栈和中断栈
内核在创建进程的时候,在创建task_struct的同时,会为进程创建相应的堆栈。每个进程会有两个栈:一个用户栈,存在于用户空间,一个内核栈,存在于内核空间。当进程在用户空间运行时,cpu堆栈指针寄存器里面的内容是用户堆栈地址,使用用户栈;当进程在内核空间时,cpu堆栈指针寄存器里面的内容是内核栈空间地址,使用内核栈。根据体系结构的不同,内核栈可以是1页也可以是连续的2页,大小在4KB到16KB之间。
除了用户栈和内核栈之外,2.6还实现了一个新的中断栈,它存在于内核栈为1页的情况下。中断栈为每个进程提供了一个用于中断处理程序的栈,有了它之后,中断处理程序就不需要再和被中断进程共享内核栈了。
4.2 进程用户栈和内核栈的切换
当进程因为中断或者系统调用而陷入内核态时,进程所使用的堆栈也要从用户栈转到内核栈。
进程陷入内核态后,先把用户态堆栈的地址保存在内核栈之中,然后设置cpu堆栈指针寄存器的内容为内核栈的地址,这样就完成了用户栈向内核栈的转换;当进程从内核态恢复到用户态之行时,在内核态之行的最后将保存在内核栈里面的用户栈的地址恢复到cpu堆栈指针寄存器即可。这样就实现了内核栈和用户栈的互转。
那么,我们知道从内核转到用户态时用户栈的地址是在陷入内核的时候保存在内核栈里面的,但是在陷入内核的时候,我们是如何知道内核栈的地址的呢?
关键在进程从用户态转到内核态的时候,进程的内核栈总是空的。这是因为,当进程在用户态运行时,使用的是用户栈,当进程陷入到内核态时,内核栈保存进程在内核态运行的相关信息,但是一旦进程返回到用户态后,内核栈中保存的信息无效,会全部恢复,因此每次进程从用户态陷入内核的时候得到的内核栈都是空的。所以在进程陷入内核的时候,直接把内核栈的栈顶地址给堆栈指针寄存器就可以了。
5. 高端内存的映射
高端内存就是ZONE_HIGHMEM中的物理页。不可以通过__get_free_pages()或者kmalloc()来获取高端内存中的页,因为这两个函数的返回值都是逻辑地址,而高端内存中的物理页不能永久的映射到内核地址空间,因而可能根本就没有逻辑地址。你可以通过alloc_pages()或alloc_page()以__GFP_HIGHMEM标志来获得高端内存中的页,这两个函数的返回值是struct page指针。
要想映射一个给定的page到内核地址空间,可以使用下面的函数:
void *kmap(struct page *page);
这个函数在高端内存、低端内存中都可以用。如果page结构对应的是低端内存中的页,函数只是单纯返回其逻辑地址;如果对应高端内存中的一页,则会建立一个永久映射,再返回地址。这个函数可能会睡眠。
因为允许永久映射的数量有限(否则的话就没必要这么麻烦,直接将所有内存统统永久映射一下就好了),当不再需要高端内存时,应该解除映射,通过:
void kumap(struct page *page);
关于高端内存更多的内容,可以参考Linux用户空间与内核空间数据传递,此文写得很棒
6. per-CPU的内存管理
参见linux kernel学习笔记-3 内核同步机制,4.2.
补充一下使用per-CPU的好处和注意事项。好处是:首先,减少了数据锁定,因为每个CPU只访问它自己的per-CPU数据;其次,使用per-CPU数据将提高缓存命中率。不过也要注意,per-CPU需要禁止内核抢占,不过接口会自动完成这个步骤;另外不能在访问per-CPU数据时睡眠,否则醒来时也许已经到了其他处理器了。
7. 不同分配函数的比较和小结
如果你需要连续的物理页,可以使用某个低级页分配函数或者kmalloc(),这是内核中常用的内存分配方式。传递给这些函数的最常见的标志是GFP_ATOMIC和GFP_KERNEL,前者代表禁止睡眠的内存分配,可用于ISR或其他不可睡眠的地方。
如果想从高端内存进行分配,则使用alloc_pages()或者alloc_page()。它们返回指向一个struct page结构的指针,而不是某个逻辑地址。由于高端内存很可能没有被映射,所以只能通过struct page来访问。为了获得真正的逻辑地址,需要调用kmap()来将高端内存映射到内核的逻辑地址空间。
如果不需要物理上连续的页,仅仅需要逻辑地址上连续的页,就可以使用vmalloc(),但是要承担一定的性能损失。
如果要创建和撤销很多同类型的数据结构,可以建立slab高速缓存。slab分配器会给每个CPU维护一个对象高速缓存(空闲链表),这种高速缓存可以极大地提高对象分配和回收的性能。slab层并不是频繁的分配和释放内存,而是把事先分配好的对象存放到高速缓存中,当你需要一块新的内存来存放数据结构时,slab层一般无需重新分配内存,只需要从高速缓存中取出一个对象就可以了。
8. 参考文献
linux kernel development, 3rd edtion, robert love
进程内核栈、用户栈
Linux用户空间与内核空间数据传递
解惑-linux内核空间