study,record,share
分类: 嵌入式
2013-02-19 13:55:00
原文地址: linux内核内存管理 作者:td1442911376
1、kmalloc()/kfree()
static __always_inline void *kmalloc(size_t size, gfp_t flags)
内核空间申请指定大小的内存区域,返回内核空间虚拟地址。在函数实现中,如果申请的内存空间较大的话,会从buddy系统申请若干内存页面,如果申请的内存空间大小较小的话,会从slab系统中申请内存空间。有关buddy和slab,请参见《linux内核之内存管理.doc》
gfp_t flags 的选项较多。参考内核文件gfp.h。
在函数kmalloc()实现中,如果申请的空间较小,会根据申请空间的大小从slab中获取;如果申请的空间较大,如超过一个页面,会直接从buddy系统中获取。
2、vmalloc()/vfree()
void *vmalloc(unsigned long size)
函数作用:从高端(如果存在,优先从高端)申请内存页面,并把申请的内存页面映射到内核的动态映射空间。vmalloc()函数的功能和alloc_pages(_GFP_HIGHMEM)+kmap() 的功能相似,只所以说是相似而不是相同,原因在于用vmalloc()申请的物理内存页面映射到内核的动态映射区(见下图),并且,用vmalloc()申请的页面的物理地址可能是不连续的。而alloc_pages(_GFP_HIGHMEM)+kmap()申请的页面的物理地址是连续的,被映射到内核的KMAP区。
vmalloc分配的地址则限于vmalloc_start与vmalloc_end之间。每一块vmalloc分配的内核虚拟内存都对应一个vm_struct结构体(可别和vm_area_struct搞混,那可是进程虚拟内存区域的结构),不同的内核虚拟地址被4k大小的空闲区间隔,以防止越界——见下图)。与进程虚拟地址的特性一样,这些虚拟地址与物理内存没有简单的位移关系,必须通过内核页表才可转换为物理地址或物理页。它们有可能尚未被映射,在发生缺页时才真正分配物理页面。
如果内存紧张,连续区域无法满足,调用vmalloc分配是必须的,因为它可以将物理不连续的空间组合后分配,所以更能满足分配要求。vmalloc可以映射高端页框,也可以映射底端页框。vmalloc的作用只是为了提供逻辑上连续的地址。。。
注意:在申请页面时,如果注明_GFP_HIGHMEM,即从高端申请。则实际是优先从高端内存申请,顺序为(分配顺序是HIGH, NORMAL, DMA )。
3、alloc_pages()/free_pages()
内核空间申请指定个数的内存页,内存页数必须是2^order个页。
alloc_pages(gfp_mask, order) 中,gfp_mask 是flag标志,其中可以为_ _GFP_DMA、_GFP_HIGHMEM 分别对应DMA和高端内存。
注:该函数基于buddy系统申请内存,申请的内存空间大小为2^order个内存页面。
参见《linux内核之内存管理.doc》
通过函数alloc_pages()申请的内存,需要使用kmap()函数分配内核的虚拟地址。
4、__get_free_pages()/__free_pages()
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
作用相当于alloc_pages(NORMAL)+kmap(),但不能申请高端内存页面。
__get_free_page()只申请一个页面。
5、kmap()/kunmap()
返回指定页面对应内核空间的虚拟地址。
#include
void *kmap(struct page *page);
void kunmap(struct page *page);
kmap 为系统中的任何页返回一个内核虚拟地址.
对于低端内存页,它只返回页的逻辑地址;
对于高端内存页, kmap在“内核永久映射空间”中创建一个特殊的映射. 这样的映射数目是有限, 因此最好不要持有过长的时间.
使用 kmap 创建的映射应当使用 kunmap 来释放;
kmap 调用维护一个计数器, 因此若2个或多个函数都在同一个页上调用kmap也是允许的.
通常情况下,“内核永久映射空间”是 4M 大小,因此仅仅需要一个页表即可,内核通过来pkmap_page_table 寻找这个页表。
注意:不用时及时释放。
kmalloc()和vmalloc()相比,kmalloc()总是从ZONE_NORMAL(下图中的直接映射区)申请内存。kmalloc()分配的内存空间通常用于linux内核的系统数据结构和链表。因内核需要经常访问其数据结构和链表,使用固定映射的ZONE_NORMAL空间的内存有利于提高效率。
使用vmalloc()可以申请非连续的物理内存页,并组成虚拟连续内存空间。vmalloc()优先从高端内存(下图中的动态映射区)申请。内核在分配那些不经常使用的内存时,都用高端内存空间(如果有),所谓不经常使用是相对来说的,比如内核的一些数据结构就属于经常使用的,而用户的一些数据就属于不经常使用的。
alloc_pages(_GFP_HIGHMEM)+kmap() 方式申请的内存使用内核永久映射空间(下图中的KMAP区),空间较小(通常4M线性空间),不用时需要及时释放。另外,可以指定alloc_pages()从直接映射区申请内存,需要使用_GFP_NORMAL属性指定。
__get_free_pages()/__free_pages() 不能申请高端内存页面,操作区域和kmalloc()相同(下图中的动态映射区)。
6、virt_to_page()
其作用是由内核空间的虚拟地址得到页结构。见下面的宏定义。
#define virt_to_pfn(kaddr) (__pa(kaddr) >> PAGE_SHIFT)
#define pfn_to_virt(pfn) __va((pfn) << PAGE_SHIFT)
#define virt_to_page(addr) pfn_to_page(virt_to_pfn(addr))
#define page_to_virt(page) pfn_to_virt(page_to_pfn(page))
#define __pfn_to_page(pfn) (mem_map + ((pfn) - ARCH_PFN_OFFSET))
#define __page_to_pfn(page) ((unsigned long)((page) - mem_map) + \
ARCH_PFN_OFFSET)
7、物理地址和虚拟地址之间转换
#ifdef CONFIG_BOOKE
#define __va(x) ((void *)(unsigned long)((phys_addr_t)(x) + VIRT_PHYS_OFFSET))
#define __pa(x) ((unsigned long)(x) - VIRT_PHYS_OFFSET)
#else
#define __va(x) ((void *)(unsigned long)((phys_addr_t)(x) + PAGE_OFFSET - MEMORY_START))
#define __pa(x) ((unsigned long)(x) - PAGE_OFFSET + MEMORY_START)
#endif
8、ioremap()/iounmap()
ioremap()的作用是把device寄存器和内存的物理地址区域映射到内核虚拟区域,返回值为内核的虚拟地址。
注明:在内核中操作内存空间时使用的都是内核虚拟地址,必须把device的空间映射到内核虚拟空间。
#include
void *ioremap(unsigned long phys_addr, unsigned long size);
void *ioremap_nocache(unsigned long phys_addr, unsigned long size); 映射非cache的io内存区域
void iounmap(void * addr);
为了增加可移植性,最好使用下面的接口函数读写io内存区域,
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);
如果你必须读和写一系列值到一个给定的 I/O 内存地址, 你可以使用这些函数的重复版本:
void ioread8_rep(void *addr, void *buf, unsigned long count);
void ioread16_rep(void *addr, void *buf, unsigned long count);
void ioread32_rep(void *addr, void *buf, unsigned long count);
void iowrite8_rep(void *addr, const void *buf, unsigned long count);
void iowrite16_rep(void *addr, const void *buf, unsigned long count);
void iowrite32_rep(void *addr, const void *buf, unsigned long count);
这些函数读或写 count 值从给定的 buf 到 给定的 addr. 注意 count 表达为在被写入的数据大小; ioread32_rep 读取count 32-位值从 buf 开始.
9、request_mem_region()
本函数的作用是:外设的io端口映射到io memory region中。在本函数实现中会检查输入到本函数的参数所描述的空间(下面成为本io空间)是否和io memory region中已存在的空间冲突等,并设置本io空间的parent字段等(把本io空间插入到io 空间树种)。
注明:io memory region 空间中是以树形结构组织的,默认的根为iomem_resource描述的io空间,其name为"PCI mem"。
request_mem_region(start,n,name) 输入的参数依次是设备的物理地址,字节长度,设备名字。函数返回类型如下
struct resource {
resource_size_t start;
resource_size_t end;
const char *name;
unsigned long flags;
struct resource *parent, *sibling, *child;
};
10、SetPageReserved()
随着linux的长时间运行,空闲页面会越来越少,为了防止linux内核进入请求页面的僵局中,Linux内核采用页面回收算法(PFRA)从用户进程和内核高速缓存中回收内存页框,并根据需要把要回收页框的内容交换到磁盘上的交换区。调用该函数可以使页面不被交换。
#define SetPageReserved(page) set_bit(PG_reserved, &(page)->flags)
PG_reserved 的标志说明如下。
* PG_reserved is set for special pages, which can never be swapped out. Some
* of them might not even exist (eg empty_bad_page)...
可参考下面的文章
http://blog.csdn.net/bullbat/article/details/7311205
http://blog.csdn.net/cxylaf/article/details/1626534
11、do_mmap()/do_ummap()
内核使用do_mmap()函数为进程创建一个新的线性地址区间。但是说该函数创建了一个新VMA并不非常准确,因为如果创建的地址区间和一个已经存在的地址区间相邻,并且它们具有相同的访问权限的话,那么两个区间将合并为一个。如果不能合并,那么就确实需要创建一个新的VMA了。但无论哪种情况, do_mmap()函数都会将一个地址区间加入到进程的地址空间中--无论是扩展已存在的内存区域还是创建一个新的区域。
同样,释放一个内存区域应使用函数do_ummap(),它会销毁对应的内存区域。
12、get_user_pages()
作用是在内核空间获取用户空间内存的page 描述,之后可以通过函数kmap() 获取page 对应到内核的虚拟地址。
int get_user_pages(struct task_struct *tsk, struct mm_struct *mm,
unsigned long start, int len, int write, int force,
struct page **pages, struct vm_area_struct **vmas)
参数说明
参数tsk:指示用户空间对应进程的task_struct数据结构。只是为了记录错误信息用,该参数可以为空。
参数mm:从该mm struct中获取start 指示的若干页面。
参数start:参数mm空间的起始地址,即用户空间的虚拟地址。
参数len:需要映射的页数。
参数write:可以写标志。
参数force:强制可以写标志。
参数pages:输出的页数据结构。
参数vmas:对应的需要存储区,(没有看明白对应的代码)
返回值:数返回实际获取的页数,貌似对每个实际获取的页都是给页计数值增1,如果实际获取的页不等于请求的页,要放弃操作则必须对已获取的页计数值减1。
13、copy_from_user()和copy_to_user()
主要应用于设备驱动中读写函数中,通过系统调用触发,在当前进程上下文内核态运行(即当前进程通过系统调用触发)。
copy_from_user的目的是防止用户程序欺骗内核,将一个非法的地址传进去,如果没有它,这一非法地址就检测不到,内和就会访问这个地址指向的数据。因为在内核中访问任何地址都没有保护,如果不幸访问一个错误的内存地址会搞死内核或发生更严重的问题
copy_from_user调用了access_ok,所以才有“自己判断功能“
access_ok(),可以检查访问的空间是否合法。
注意:中断代码时不能用copy_from_user,因为其调用了might_sleep()函数,会导致睡眠。
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n)
通常用在设备读函数或ioctl 中获取参数的函数中:其中“to”是用户空间的buffer地址,在本函数中将内核buffer“from”除的n个字节拷贝到用户空间的“to”buffer。
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n)
通常用在设备写函数或ioctl中设置参数的函数中:“to”是内核空间的buffer指针,要写入的buffer;“from”是用户空间的指针,数据源buffer。
14、get_user(x, ptr)
本函数的作用是获取用户空间指定地址的数值并保存到内核变量x中,ptr为用户空间的地址。用法举例如下。
get_user(val, (int __user *)arg)
注明:函数用户进程上下文内核态,即通常在系统调用函数中使用该函数。
15、put_user(x, ptr)
本函数的作用是将内核空间的变量x的数值保存到用户空间指定地址处,prt为用户空间地址。用法举例如下。
put_user(val, (int __user *)arg)
注明:函数用户进程上下文内核态,即通常在系统调用函数中使用该函数
内存管理,不用多说,言简意赅。在内核里分配内存还真不是件容易的事情,根本上是因为内核不能想用户空间那样奢侈的使用内存。
先来说说内存管理。内核把物理页作为内存管理的基本单位。尽管处理器的最小可寻址单位通常是字,但是,内存管理单元MMU通常以页为单位进行处理。因此, 从虚拟内存的交代来看,页就是最小单位。内核用struct page(linux/mm.h)结构表示系统中的每个物理页:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
struct page
{
unsigned long flags;
atomic_t
count;
unsigned int mapcount;
unsigned long private;
struct address_space
*mapping;
pgoff_t
index;
struct list_head
lru;
union{
struct pte_chain;
pte_addr_t;
}
void *virtual;
};
|
flag用来存放页的状态,每一位代表一种状态,所以至少可以同时表示出32中不同的状态,这些状态定义在linux/page-flags.h中。 count记录了该页被引用了多少次。mapping指向与该页相关的address_space对象。virtual是页的虚拟地址,它就是页在虚拟内 存中的地址。要理解的一点是page结构与物理页相关,而并非与虚拟页相关。因此,该结构对页的描述是短暂的。内核仅仅用这个结构来描述当前时刻在相关的 物理页中存放的东西。这种数据结构的目的在于描述物理内存本身,而不是描述包含在其中的数据。
在linux中,内核也不是对所有的也都一视同仁,内核而是把页分为不同的区,使用区来对具有相似特性的页进行分组。Linux必须处理如下两种硬件存在缺陷而引起的内存寻址问题:
1.一些硬件只能用某些特定的内存地址来执行DMA 2.一些体系结构其内存的物理寻址范围比虚拟寻址范围大的多。这样,就有一些内存不能永久地映射在内核空间上。 为了解决这些制约条件,Linux使用了三种区: 1.ZONE_DMA:这个区包含的页用来执行DMA操作。 2.ZONE_NOMAL:这个区包含的都是能正常映射的页。 3.ZONE_HIGHEM:这个区包"高端内存",其中的页能不永久地映射到内核地址空间。 |
区的实际使用与体系结构是相关的。linux 把系统的页划分区,形成不同的内存池,这样就可以根据用途进行分配了。需要说明的是,区的划分没有任何物理意义,只不过是内核为了管理页而采取的一种逻辑 上的分组。尽管某些分配可能需要从特定的区中获得页,但这并不是说,某种用途的内存一定要从对应的区来获取,如果这种可供分配的资源不够用了,内核就会占 用其他可用去的内存。下表给出每个区及其在X86上所占的列表:
每个区都用定义在linux/mmzone.h中的struct zone表示,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
struct zone
{
spinlock_t
lock;
unsigned long free_pages;
unsigned long pages_min,
pages_low, pages_high;
unsigned long protection[MAX_NR_ZONES];
spinlock_t
lru_lock;
struct list_head
active_list;
struct list_head
inactive_list;
unsigned long nr_scan_active;
unsigned long nr_scan_inactive;
unsigned long nr_active;
unsigned long nr_inactive;
int all_unreclaimable;
unsigned long pages_scanned;
struct free_area
free_area[MAX_ORDER];
wait_queue_head_t
* wait_table;
unsigned long wait_table_size;
unsigned long wait_table_bits;
struct per_cpu_pageset
pageset[NR_CPUS];
struct pglist_data
*zone_pgdat;
struct page
*zone_mem_map;
unsigned long zone_start_pfn;
char *name;
unsigned long spanned_pages;
unsigned long present_pages;
};
|
其中的lock域是一个自旋锁,这个域只保护结构,而不是保护驻留在这个区中的所有页。没有特定的锁来保护单个页。free_pages域是这个区中空闲 页的个数。内核尽可能的保护有pages_min个空闲页可用。name域是一个以NULL结束的字符串,表示这个区的名字。内核启动期间初始化这个值, 其代码位于mm/page_alloc.h中,三个区的名字分别是"DMA","Normal","HighMem"。
内核提供了一种请求内层的底层机制,并提供了对它进行访问的几个接口。所有这些接口都是以页为单位进行操作的。下表给出所有底层的页分配方法:
当你不再需要页时可以用下列函数释放它们,只是提醒:仅能释放属于你的页,否则可能导致系统崩溃。内核是完全信任自己的,如果有非法操作,内核会开心的把自己挂起来,停止运行。列表如下:
上面提到都是以页为单位的分配方式,那么对于常用的以字节为单位的分配来说,内核通供的函数是kmalloc(),和mallloc很像吧,其实还真是这 样,只不过多了一个flags参数。用它可以获得以字节为单位的一块内核内存。如果需要的是页----尤其是在你的需求总量接近2的幂次方的时候---- 那么,前面讨论的页分配接口可能是更好的选择。
接下来,注意的话,可能会发现无论是页分配接口还是kmalloc都有一个分配器标志(如GFP_KERNEL这样的)。这些标志可分为三类:行为修饰符,区修饰符及类型.下面就来讨论个问题.
1.行为修饰符(linux/gfp.h):表示内核应当如何分配所需的内存。在某些特定的情况下,只能使用某些特定的方法分配内存。可以同时使用这些标志,用|链接。列表如下:
2.区分配符:它只关心去应当从何处分配。通常,分配可以从任何区开始。不过,内核优先从ZONE_NORMAL开始,这样可以确保其他区在需要时有足够的空闲页可以使用。区修饰符如下:
不能给_get_free_pages()指定ZONE_HIGHMEM,因为这个函数返回都是逻辑地址,而不是page结构。这两个函数分配的内存当前 可能有可能还没有映射到内核的虚拟地址空间,因此,也可能根本就没有逻辑地址。只有alloc_pages()才能分配高端内存。实际上,大多数 ZONE_NORMAL就已经足够了。
3.类型标志:指定所需的行为和区描述符以完成特殊类型的处理。正因为这点,内核代码趋向于使用正确的类型标志,而不是一味地指定它可能需要用到的多个描述符。下面两个表分别给出了类型标志的列表和每个类型标志与哪些修饰符相关联:
上表中,左边是类型标志,右边是每种类型标志后隐含的修饰符列表。在编写的大多数代码中,用到的要么是GFP_KERNEL,要么是GFP_ATOMIC。下表是通常情形和所用标志的列表,不管使用那种分配类型,你都必须进行检查,并对错误进行处理:
有了kmalloc,当然就有kfree()(linux/slab.h),释放由kmalloc()分配出来的内存块。如果想要释放的内存不是由 kmalloc()分配的,或者想要释放的内存早就被释放了,在这种情况下调用这个函数会导致严重的后果。特别说明kfree(NULL)是安全的。
vmalloc()和kmalloc是一样的作用,不同在于前者分配的内存虚拟地址是连续的,而物理地址则无需连续。这也是用户空间分配函数的工作方式, 如malloc().kmalloc()可以保证在物理地址上都是连续的(当然,虚拟地址当然也是连续的)。vmalloc()函数只确保页在虚拟机地址 空间内是连续的。它通过分配非联系的物理内存块,再“修正”页表,把内存映射到逻辑地址空间的连续区域中,就能做到这点。但很显然这样会降低处理性能,因 为内核不得不做“拼接”的工作。所以这也是为什么不得已才使用vmalloc()的原因(比如获得大内存时)。大多数情况下,只有硬件设备需要得到物理地 址连续的内存。硬件设备存在于内存管理单元以外,它根本不懂什么是虚拟地址。因此,硬件设备用到的任何内存区都必须是物理上连续的块,而不仅仅是虚地址连 续的块。最后需要说明的是,vmalloc()可能睡眠,不能从中断上下文中进行调用,也不能从其他不允许阻塞的情况下进行调用。释放时必须使用 vfree().
分配和释放数据结构是所有内核中最普遍的操作之一。为了便于数据的频繁分配和回收,常常会用到一个空间链表。它就相当于对象高速缓存以便快速存储频繁使用 的对象类型。在内核中,空闲链表面临的主要问题之一是不能全局控制。当可用内存变得紧张的时候,内核无法通知每个空闲链表,让其收缩缓存的大小以便释放一 些内存来。实际上,内核根本不知道有这样的空闲离岸边。为了弥补这一缺陷,也为了是代码更加稳固,linux内核提供了slab层(也就是所谓的slab 分类器),slab分类器扮演了通用数据结构缓存层的角色。slab分配器试图在如下几个原则中寻求一种平衡:
1.频繁使用的数据结构也会频繁分配和释放,因此应当缓存它们。 2.频繁分配和回收必然会导致内存碎片。为了避免这种情况,空闲链表的缓存会连续地存放。因为已释放的数据结构又会放回空闲链表,不会导致碎片。 3.回收的对象可以立即投入下一次分配,因此,对于频繁的分配和释放,空闲链表能够提高其性能。 4.如果让部分缓存专属于单个处理器,那么,分配和释放就可以在不加SMP锁的情况下进行。 5.对存放的对象进行着色,以防止多个对象映射到相同的高速缓存行。 |
slab层把不同的对象划分为所谓的高速缓存组,其中每个高速缓存都存放不同类型的对象,每种对象类型对应一个高速缓存。kmalloc()接口建立在 slab层上,使用了一组通用高速缓存。这些缓存又被分为slabs,slab由一个或多个物理上连续的页组成,一般情况下,slab也就仅仅由一页组 成。每个高速缓存可以由多个slab组成。每个slab都包含一些对象成员,这里的对象指的是被缓存的数据结构,每个slab处于三种状态之一:满,部分 满,空。当内核的某一部分需要一个新的对象时,先从部分满的slab中进行分配。如果没有部分满的slab,就从空的slab中进行分配。如果没有空的 slab,就要创建一个slab了。下图给出高速缓存,slab及对象之间的关系:
上图中的每个cache由kmem_cache_s结构表示,这个结构包含三个链表slabs_full,slab_partial和 slabs_empty,均存放在kmem_list3结构内,这些链表包含高速缓存中的所有slab,slab描述符struct slab:
1
2
3
4
5
6
7
|
struct slab
{
struct list_head
list; /*满,部分满或空链表*/
unsigned long colouroff; /*slab着色的偏移量*/
void *s_mem; /*在slab中的第一个对象*/
unsigned int inuse; /*已分配的对象数*/
kmem_bufctl_t free; /*第一个空闲对象*/
};
|
slab描述符要么在slab之外另行分配,要么就在slab自身最开始的地方。如果slab很小或者slab内核有足够的空间容纳slab描述符,那么 描述符就存放在slab里面.slab分配器创建新的slab是通过__get_free_pages()低级内存分配器进行的:
1
2
3
4
5
6
7
|
static inline void *
kmem_getpages(kmem_cache_t *cachep, unsigned long flags)
{
void *addr;
flags
|= cachep->gfpflags;
addr
= (void*)__get_free_pages(flags,
cachep->gfporder);
return addr;
}
|
上面的是一个描述原理的简化版。接着,调用kmem_freepages()释放内存,而对给定的高速缓存页,kmem_freepages()最终调用 的是free_pages().当然,slab层的关键就是避免频繁分配和释放页。由此可知,slab页只有当给定的高速缓存中既没有部分满也没有空的 slab时候才会调用页分配函数。而只有在下列情况下才会调用释放函数:当可用内存变得紧缺时,系统试图释放出更多内存以供使用,或者当高速缓存显式地被 销毁时。slab层的管理是在每个高速缓存的基础上,通过提供个整个内核一个简单的接口来完成的。通过接口就可以创建和销毁新的高速缓存,并在高速缓存内 分配和释放对象。高速缓存及slab的复杂管理完全通过slab层的内部机制来处理。当创建一个高速缓存后,slab层所起的作用就像一个专用的分配器, 可以为具体的对象类型进行分配。一个新的高速缓存是通过一下接口进行创建的:
1
|
kmem_cache_t
* kmem_cache_create(const char *name, size_t size,size_t align,
unsigned long flags,
|
1
2
|
void (*ctor)(void*,
kmem_cache_t *, unsigned long),
void (*dtor)(void*,
kmem_cache_t *, unsigned long));
|
1
|
有关这个函数的说明,我就省略了,需要的网上一大堆。这个函数成功时会返回一个执行所创建高速缓存的指针,否则,返回空。这个函数由于会睡眠,因此不能在中断上下文中使用。要销毁一个高速缓存,调用:int kmem_cache_destroy(kmem_cache_t
*cachep),同样,也是不能在中断上下文中使用。调用该函数之前必须确保存在以下两个条件:
|
1.高速缓存中的所有slab都必须为空。 2.在调用kmem_cache_destory()期间不再访问这个高速缓存,调用者必须确保这种同步。 |
创建了高速缓存以后,就可以通过下列函数从中获取对象:void * kmem_cache_alloc(kmem_cache_t *cachep, int flags)。该函数从高速缓存cachep中返回一个指向对象的指针。如果高速缓存的所有slab中都没有空闲的对象,那么slab层必须通过 kmem_getpages()获取新的页,flags的值传递给__get_free_pages().最后,释放一个对象,并把它返回给原来的 slab,可以使用下面的函数:
1
|
void kmem_cache_free(kmem_cache_t
*cachep,void *objp)
|
这样就能把cachep中的对象objp标记为空闲了,关于slab分配器的使用实例,参考资料上有,我就不说了。相比较以前的用户空间栈而言,内核栈是 非常小的。每个进程都有自己的内核栈进程在内核执行期间的整个调用链必须放在自己的内核栈上。中断处理程序也使用被它们打断的进程的堆栈。这就意味着,在 最恶劣的情况下,8kB的内核栈可能会由多个函数的嵌套调用链和几个中断处理程序来共享。显然,深度的嵌套会导致溢出。
根据定义,在高端内存中的页不能永久地映射到内核地址空间上。因此,通过alloc_pages()函数以__GFP_HIGHMEM标志获得的页不可能 有逻辑地址。一旦这些页被分配,就必须映射到内核的逻辑地址空间上。要映射一个给定的page结构到内核地址空间,可以使用void *kmap(struct page *page) 这个函数在高端内存或低端内存上都能用。如果page结构对应的是低端内存中的一页,函数只会单纯地返回该页的虚拟地址,如果页位于高端内存,则会建立一 个永久映射,在返回地址。这个函数可以睡眠,所以kmap()只能用在进程上下文中。当不再需要内存映射的时候,就用下列函数进行解除映射:
1
|
void kunmem(struct page*
page)
|
当必须创建一个映射而当前的上下文又不能睡眠时,内核提供了临时睡眠(也就是原子睡眠)。只要有一组保留的永久映射,它们就可以临时持有新创建的一个映 射。内核可以原子地把高端内存中的一个页映射到某个保留的映射中。因此,临时映射可以用在不能睡眠的地方。建立临时映射:void *kmap_atomic(struct page *page,enum km_type type).参数type是下列枚举类型之一,描述了临时映射的目的,如下:
这个函数不会阻塞,它也禁止内核抢占,通过函数void *kunmap_atomic(void *kvaddr,enum km_type type).这个函数还是不会映射。
最后,我们总结一下,说说分配函数的选择吧,总结如下:
1.如果需要连续的物理页,就可以使用某个低级页分配器或kmalloc(). 2.如果想从高端内存进行分配,使用alloc_pages(). 3.如果不需要物理上连续的页,而仅仅是虚拟地址上连续的页,那么就是用vmalloc 4.如果要创建和销毁很多大的数据结构,那么考虑建立slab高速缓存。 |
把linux内存管理分为下面四个层面
(一)硬件辅助的虚实地址转换
(二)内核管理的内存相关
(三)单个进程的内存管理
(四)malloc软件
(一) 处理器硬件辅助的虚实地址转换(以x86为例)
在x86中虚实地址转换分为段式转换和页转换。段转换过程是由逻辑地址(或称为虚拟地址)转换为线性地址;页转换过程则是将线性地址转换为物理地址。段转换示意图如下
X86支持两种段,gdt和ldt(全局描述段表和局部描述符段表),在linux中只使用了4个全局描述符表,内核空间和用户空间分别两个gdt,分别对应各自的代码段和数据段。也可以认为在linux中变相地disable了x86的段式转换功能。
在linux中x86 的cr3寄存器(页表基地址寄存器)保存在进程的上下文中,在进程切换时会保存或回复该寄存器的内容,这样每个进程都有自己的转换页表,从而保证了每个进程有自己的虚拟空间。
(二) 内核管理的内存相关
从几个概念展开内存管理:node、zone、buddy、slab
1、Node
如上图,NUMA系统的结点通常是由一组CPU(如,SGI Altix 3000是2个Itanium2 CPU)和本地内存组成。由于每个结点都有自己的本地内存,因此全系统的内存在物理上是分布的,每个结点访问本地内存和访问其它结点的远地内存的延迟是不同的,为了优化对NUMA 系统的支持,引进了Node 来将NUMA 物理内存进行划分为不同的Node。而操作系统也必须能感知硬件的拓扑结构,优化系统的访存。
但是Intel x86 系统不是NUMA 系统。为了保持代码的一致性,在x86 平台上,Linux 将所有物理内存都划分到同一个Node。事实上,对于非NUMA 体系结构,也是如此处理的。
Linux系统用定义了数组pg_data_t node_data[MAX_NUMNODES] 来管理各个node。
2、Zone
Linux中Node、Zone和页的关系
每个结点的内存被分为多个块,称为zones,它表 示内存中一段区域。一个zone用struct zone结构描述,zone的类型主要有ZONE_DMA、ZONE_NORMAL和ZONE_HIGHMEM。ZONE_DMA位于低端的内存空间,用 于某些旧的ISA设备。ZONE_NORMAL的内存直接映射到Linux内核线性地址空间的高端部分,ZONE_HIGHMEM位于物理地址高于 896MB的区域。例如,在X86中,zone的物理地址如下:
内核空间只有1GB线性地址,如果使用大于1GB的 物理内存就没法直接映射到内核线性空间了。当系统中的内存大于896MB时,把内核线性空间分为两部分,内核中低于896MB线性地址空间直接映射到低 896MB的物理地址空间;高于896MB的128MB内核线性空间用于动态映射ZONE_HIGHMEM内存区域(即物理地址高于896MB的物理空 间)。
3、Buddy
如上图所示,每个zone区域都采用伙伴系统(buddy system)来管理空闲内存页面。把所有的空闲页框分组为11个块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续的页框。链表编号分别为0,1,2,3,… k… 10。
从buddy system中申请页面过程:
1、根据申请存储区域大小查找对应的编号为K的块链表。
2、如果编号K的链表为空,则向编号为k+1的链表申请一个存储区域。如果编号为k+1链表不为空,系统从编号为k+1的链表上拆下一个区域,并将拆下的区域分为两个2^k的区域,一个返还给申请者,另一个则挂到编号为k的链表。
3、如果编号为k+1的链表也为空,编号为k+2的链表不为空。则从k+2的链表中拆下一个区域变为两个2^(k+1)区域,一个挂到编号为k+1的链表上,把另一个拆为两个2^k的区域,一个返还给申请者,把另一个挂到编号为k的链表上。
4、如果k+2的链表也为空,则一直向上迭代,直到编号为10的链表为止,如果编号为10的链表还为空,则申请失败。
向buddy system中释放页面过程:
在向buddy system 释放页面时,总会检测释放的页面和链表中其他页面是否可以组成一个更大一级的页面,如果可以组成,则把这两个区域组成一个并挂到更高一级的 链表中。这个过程是迭代的,释放过程会一层层向上找伙伴,然后合并成更大的,再向上找伙伴,实在找不到了就停止了!
疑问:按照上面的说法,是否会出现这种情况,在释放某个页面导致所有页面都组成了标号为10的连续页面了。等到再需要分配1个页面时,又要一级一级地拆分。这样的话效率是否很低??
是否在buddy system 每个链表结构中设一个门限值会更好?释放时标记一下可以组成buddy的两个连续区域,只有该级空闲的区域个数超过门限后才组成buddy并挂到上一级链表上。当然,这个门限值可以由内核根据目前总的空闲页面数量进行动态调整。
4、Slab
下图中给出了 slab 结构的高层组织结构。在最高层是 cache_chain,这是一个 slab 缓存的链接列表。可以用来查找最适合所需要的分配大小的缓存。cache_chain 的每个元素都是一个kmem_cache 结构的引用。一个kmem_cache中的所有object大小都相同。
slab是基于buddy system的, 每个slab占用一个或多个连续页,即一个buddy链中的1个或多个页面。
每个缓存都包含了一个 slabs 列表,这是一段连续的内存块(通常都是页面)。存在 3 种slab:
slabs_full
完全分配的 slab ,即其维护的空闲object链表为空
slabs_partial
部分分配的 slab
slabs_empty
空 slab,或者没有对象被分配,即其inuse标志位0.
注意 slabs_empty 列表中的 slab 是进行回收的主要备选对象。正是通过此过程,slab 所使用的内存被返回给操作系统供其他用户使用。
slab 列表中的每个 slab 都是一个连续的内存块(从buddy申请的一个或多个连续页),它们被划分成一个个 对象,这些对象是分配和释放的基本元素。在slab扩展时或把slab占用的内存块释放到buddy系统时, slab是最小分配单位。通常来说,每 个 slab 被分配为多个对象。由于对象是从 slab 中进行分配和释放的,因此单个 slab 可以在 slab 列表之间进行移动。例如,当一 个 slab 中的所有对象都被使用完时,就从 slabs_partial 列表中移动到 slabs_full 列表中。当一个 slab 完全被分 配并且有对象被释放后,就从 slabs_full 列表中移动到slabs_partial 列表中。当所有对象都被释放之后,就 从 slabs_partial 列表移动到slabs_empty 列表中。
slab 背后的动机
与传统的内存管理模式相比, slab 缓存分配器提供了很多优点。首先,内核通常依赖于对小对象的分配,它们会在系统 生命周期内进行无数次分配。slab 缓存分配器通过对类似大小的对象进行缓存而提供这种功能,从而避免了常见的碎片问题。slab 分配器还支持通用对 象的初始化,从而避免了为同一目而对一个对象重复进行初始化。最后,slab 分配器还可以支持硬件缓存对齐和着色,这允许不同缓存中的对象占用相同的缓 存行,从而提高缓存的利用率并获得更好的性能。
(三) 单个进程的内存管理
每个进程的task_struct中都有一个active_mm成员,类型为struct mm_struct,内核就是利用该成员管理进程虚拟空间的。参见数据结构task_struct,为了方便阅读,删除了该结构中无关的成员变量。
struct task_struct{
struct mm_struct *mm, *active_mm;
}
参考下面的数据结构定义。数据结构struct mm_struct 中的成员mm_rb指向了一棵红黑树的根,该进程的所有申请的虚拟空间都以起始虚拟地址为红黑树的key值挂到了这棵红黑树上。 mm_struct 中的成员map_count指示该进程拥有的虚拟空间的个数,pgd指向该进程的页转换表。
struct mm_struct{
struct vm_area_struct * mmap; /* list of VMAs 指向若干个VMA组成的链表 */
struct rb_root mm_rb; 指向一棵红黑树
struct vm_area_struct * mmap_cache; 指向最近找到的虚拟存储区域
int map_count; /* number of VMAs */ 虚拟区间的个数
pgd_t * pgd; 指向页转换表
}
数据结构struct vm_area_struct定义了一个连续的虚拟地址空间,包括起始地址和结束地址,以及红黑树节点vm_rb。内核就是以vm_start为key值把vm_rb挂到进程内存红黑树上的。
struct vm_area_struct{
struct mm_struct * vm_mm; /* The address space we belong to. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address within vm_mm. */
struct rb_node vm_rb; 这个虚拟区域对应的红黑树的节点
}
内核在给进程分配了一块虚拟地址内存块后,就将该区域挂接到进程的红黑树上,此时内核尚未给该进程分配实际的内存。在进程访问该区域时则产生缺页中 断,在中断中检查访问的区域已经分配给进程后,则分配实际内存页面,并更新该进程的页转换查找表。中断返回,进程重新执行触发中断的指令,并继续运行。
当进程释放一块内存区域后,内核会立即收回分配给该区域的物理内存页面。
(四) malloc软件
下面内容节选自文章《内存相关分享》,连接为http://www.cppblog.com/CppExplore/archive/2010/03/30/111049.html
应用层面的开发并不是直接调用sbrk/mmap之类的函数,而是调用malloc/free等malloc子系统提供的函数,linux上安装的大多为DougLea的dlmalloc或者其变形ptmalloc。下面以dlmalloc为例说明malloc工作的原理。
1 dlmalloc下名词解释:
boundary tag: 边界标记,每个空闲内存块均有头部表识和尾部标识,尾部表识的作为是合并空闲内存块时更快。这部分空间属于无法被应用层面使用浪费的内存空间。
smallbins: 小内存箱。dlmalloc将8,16,24......512大小的内存分箱,相临箱子中的内存相差8字节。每个箱子中的内存大小均相同,并且以双向链表连接。
treebins: 树结构箱。大于512字节的内存不再是每8字节1箱,而是一个范围段一箱。比如512~640,
640~896.....每个箱子的范围段依次是128,256,512......。每箱中的结构不再是双向链表,而是树形结构。
dv chunk: 当申请内存而在对应大小的箱中找不到大小合适的内存,则从更大的箱中找一块内存,划分出需要的内存,剩余的内存称之为dv chunk.
top chunk: 当dlmalloc中管理的内存都找不到合适的内存时,则调用sbrk从系统申请内存,可以增长内存方向的chunk称为top
chunk.
2 内存分配算法
从合适的箱子中寻找内存块-->从相临的箱子中寻找内存块-->从dv chunk分配内存-->从其他可行的箱子中分配内存-->从top
chunk中分配内存-->调用sbrk/mmap申请内存
3 内存释放算法
临近内存合并-->如属于top chunk,判断top chunk>128k,是则归还系统
-->不属于chunk,则归相应的箱子
dlmalloc还有小内存缓存等其他机制。可以看出经过dlmalloc,频繁调用malloc/free并不会产生内存碎片,只要后续还有相同的内存大小的内存被申请,仍旧会使用以前的合适内存,除非大量调用malloc之后少量释放free,并且新的malloc又大于以前free的内存大小,造成dlmalloc不停的从系统申请内存,而free掉的小内存因被使用的内存割断,而使top
chunk<128k,不能归还给系统。即便如此,占用的总内存量也小于的确被使用的内存量的2倍(使用的内存和空闲的内存交叉分割,并且空闲的内存总是小于使用的内存大小)。因此可以说,在没有内存泄露的情况,常规频繁调用malloc/free并不会产生内存碎片。
1.原理说明
Linux内核中采 用了一种同时适用于32位和64位系统的内 存分页模型,对于32位系统来说,两级页表足够用了,而在x86_64系 统中,用到了四级页表,如图2-1所示四级页表分别为:
* 页全局目录(Page Global Directory)
* 页上级目录(Page Upper Directory)
* 页中间目录(Page Middle Directory)
* 页表(Page Table)
页全局目录包含若干页上级目录的地址,页上级目录又依次包含若干页中间目录的地址,而页中间目录又包含若干页表的地址,每一个页表项指 向一个页框Linux中采用4KB大小的 页框作为标准的内存分配单元
多级分页目录结构
1.1.伙伴系统算法
在实际应用中,经常需要分配一组连续的页框,而频繁地申请和释放不同大小的连续页框,必然导致在已分配页框的内存块中分散了许多小块的 空闲页框这样,即使这些页框是空闲的,其他需要分配连续页框的应用也很难得到满足
为了避免出现这种情况,Linux内核中引入了伙伴系统算法(buddy system)把所有的空闲页框分组为11个 块链表,每个块链表分别包含大小为1,2,4,8,16,32,64,128,256,512和1024个连续页框的页框块最大可以申请1024个连 续页框,对应4MB大小的连续内存每个页框块的第一个页框的物理地址是该块大小的整数倍
假设要申请一个256个页框的块,先从256个页框的链表中查找空闲块,如果没有,就去512个 页框的链表中找,找到了则将页框块分为2个256个 页框的块,一个分配给应用,另外一个移到256个页框的链表中如果512个页框的链表中仍没有空闲块,继续向1024个页 框的链表查找,如果仍然没有,则返回错误
页框块在释放时,会主动将两个连续的页框块合并为一个较大的页框块
1.2.slab分 配器
slab分配器源于 Solaris 2.4 的 分配算法,工作于物理内存页框分配器之上,管理特定大小对象的缓存,进行快速而高效的内存分配
slab分配器为每种使用的内核对象建立单独的缓冲区Linux 内核已经采用了伙伴系统管理物理内存页框,因此 slab分配器直接工作于伙伴系 统之上每种缓冲区由多个 slab 组成,每个 slab就是一组连续的物理内存页框,被划分成了固定数目的对象根据对象大小的不同,缺省情况下一个 slab 最多可以由 1024个页框构成出于对齐 等其它方面的要求,slab 中分配给对象的内存可能大于用户要求的对象实际大小,这会造成一定的 内存浪费
2.常用内存分配函数
2.1.__get_free_pages
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
__get_free_pages函数是最原始的内存分配方式,直接从伙伴系统中获取原始页框,返回值为第一个页框的起始地址__get_free_pages在实现上只是封装了alloc_pages函
数,从代码分析,alloc_pages函数会分配长度为1<
2.2.kmem_cache_alloc
struct kmem_cache *kmem_cache_create(const char *name, size_t size,
size_t align, unsigned long flags,
void (*ctor)(void*, struct kmem_cache *, unsigned long),
void (*dtor)(void*, struct kmem_cache *, unsigned long))
void *kmem_cache_alloc(struct kmem_cache *c, gfp_t flags)
kmem_cache_create/ kmem_cache_alloc是基于slab分配器的一种内存分配方式,适用于反复分配释放同一大小内存块的场合首先用kmem_cache_create创建一个高速缓存区域,然后用kmem_cache_alloc从 该高速缓存区域中获取新的内存块 kmem_cache_alloc一次能分配的最大内存由mm/slab.c文件中的MAX_OBJ_ORDER宏 定义,在默认的2.6.18内核版本中,该宏定义为5, 于是一次最多能申请1<<5 * 4KB也就是128KB的 连续物理内存分析内核源码发现,kmem_cache_create函数的size参数大于128KB时会调用BUG()结果验证了分析结果,用kmem_cache_create分 配超过128KB的内存时使内核崩溃
2.3.kmalloc
void *kmalloc(size_t size, gfp_t flags)
kmalloc是内核中最常用的一种内存分配方式,它通过调用kmem_cache_alloc函 数来实现kmalloc一次最多能申请的内存大小由include/Linux/Kmalloc_size.h的 内容来决定,在默认的2.6.18内核版本中,kmalloc一 次最多能申请大小为131702B也就是128KB字 节的连续物理内存测试结果表明,如果试图用kmalloc函数分配大于128KB的内存,编译不能通过
2.4.vmalloc
void *vmalloc(unsigned long size)
前面几种内存分配方式都是物理连续的,能保证较低的平均访问时间但是在某些场合中,对内存区的请求不是很频繁,较高的内存访问时间也 可以接受,这是就可以分配一段线性连续,物理不连续的地址,带来的好处是一次可以分配较大块的内存图3-1表 示的是vmalloc分配的内存使用的地址范围vmalloc对 一次能分配的内存大小没有明确限制出于性能考虑,应谨慎使用vmalloc函数在测试过程中, 最大能一次分配1GB的空间
Linux内核部分内存分布
2.5.dma_alloc_coherent
void *dma_alloc_coherent(struct device *dev, size_t size,
ma_addr_t *dma_handle, gfp_t gfp)
DMA是一种硬件机制,允许外围设备和主存之间直接传输IO数据,而不需要CPU的参与,使用DMA机制能大幅提高与设备通信的 吞吐量DMA 操作中,涉及到CPU高速缓 存和对应的内存数据一致性的问题,必须保证两者的数据一致,在x86_64体系结构中,硬件已经很 好的解决了这个问题, dma_alloc_coherent和__get_free_pages函数实现差别不大,前者实际是调用__alloc_pages函 数来分配内存,因此一次分配内存的大小限制和后者一样__get_free_pages分配的内 存同样可以用于DMA操作测试结果证明,dma_alloc_coherent函 数一次能分配的最大内存也为4M
2.6.ioremap
void * ioremap (unsigned long offset, unsigned long size)
ioremap是一种更直接的内存“分配”方式,使用时直接指定物理起始地址和需要分配内存的大小,然后将该段 物理地址映射到内核地址空间ioremap用到的物理地址空间都是事先确定的,和上面的几种内存 分配方式并不太一样,并不是分配一段新的物理内存ioremap多用于设备驱动,可以让CPU直接访问外部设备的IO空间ioremap能映射的内存由原有的物理内存空间决定,所以没有进行测试
2.7.Boot Memory
如果要分配大量的连续物理内存,上述的分配函数都不能满足,就只能用比较特殊的方式,在Linux内 核引导阶段来预留部分内存
2.7.1.在内核引导时分配内存
void* alloc_bootmem(unsigned long size)
可以在Linux内核引导过程中绕过伙伴系统来分配大块内存使用方法是在Linux内核引导时,调用mem_init函数之前 用alloc_bootmem函数申请指定大小的内存如果需要在其他地方调用这块内存,可以将alloc_bootmem返回的内存首地址通过EXPORT_SYMBOL导 出,然后就可以使用这块内存了这种内存分配方式的缺点是,申请内存的代码必须在链接到内核中的代码里才能使用,因此必须重新编译内核,而且内存管理系统 看不到这部分内存,需要用户自行管理测试结果表明,重新编译内核后重启,能够访问引导时分配的内存块
2.7.2.通过内核引导参数预留顶部内存
在Linux内核引导时,传入参数“mem=size”保留顶部的内存区间比如系统有256MB内 存,参数“mem=248M”会预留顶部的8MB内存,进入系统后可以调用ioremap(0xF800000,0x800000)来申请这段内存
3.几种分配函数的比较
分配原理 |
最大内存 |
其他 |
|
__get_free_pages |
直接对页框进行操作 |
4MB |
适用于分配较大量的连续物理内存 |
kmem_cache_alloc |
基于slab机制实现 |
128KB |
适合需要频繁申请释放相同大小内存块时使用 |
kmalloc |
基于kmem_cache_alloc实现 |
128KB |
最常见的分配方式,需要小于页框大小的内存时可以使用 |
vmalloc |
建立非连续物理内存到虚拟地址的映射 |
|
物理不连续,适合需要大内存,但是对地址连续性没有要求的场合 |
dma_alloc_coherent |
基于__alloc_pages实现 |
4MB |
适用于DMA操 作 |
ioremap |
实现已知物理地址到虚拟地址的映射 |
|
适用于物理地址已知的场合,如设备驱动 |
alloc_bootmem |
在启动kernel时,预留一段内存,内核看不见 |
|
小于物理内存大小,内存管理要求较高 |