文章目录
内存管理
内核不能像用户空间那样奢侈的使用内存,获取内存币用户空间复杂很多
页
内核把物理页作为内核管理的基本单元,内存管理单元(MMU)是管理内存并将虚拟内存转换为物理内存的硬件,它以页为单位来管理系统中的页表
结构体struct page表示系统中的每个物理页
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
域,用来存放页的状态(包括是不是脏的,是不是锁定在内存),每一位单独表示一种状态,至少可以表示32中不同的状态_count
域,存放页的引用计数,-1时内核没有引用该页,在新的内存分配中可以使用。内核调用page_count()
检查该域,返回0表示页空闲,返回正整数表示正在使用- 页可以由页缓存使用(此时,mapping域指向页关联的address_space对象),或者作为私有数据(
private
指向),或者作为进程页表中的映射 virtual
域,页的虚拟地址,当页在高端内存(不会永久映射到内核空间)中时,这个域为NULL
注意:
page
结构与物理页相关,并非与虚拟页相关,它仅仅描述当前时刻在相关物理中存放的数据(由于交换等原因,关联的数据继续存在,但是和当前物理页不再关联),它对于页的描述是短暂的- 页的拥有者可能是用户空间进程、动态分配的内核数据,静态内核数据或者页高速缓存等
区
Linux主要使用四种区:
- ZONE_DMA,其中包含的页只能进行DMA操作(直接内存访问)
- ZONE_DMA32,和ZONE_DMA类似,不同之处是只能被32位设备访问
- ZONE_NORMAL,包含能够正常映射的页
- ZONE_HIGHMEM,包含“高端内存”,其中的页不能永久地映射到内核空间
高端内存,由于一些体系结构的物理内存比虚拟内存大的多,为了充分利用物理内存,将物理内存中的部分区域划分为高端内存,他们不能永久地映射到内核空间,而是动态的映射
在32位x86体系中,ZONE_HIGHMEM为高于896MB的所有物理内存,其余内存为低端内存,其中ZONE_NORMAL为16MB到896MB的物理内存,ZONE_DMA为小于16MB的物理内存
x86-64系统没有高端内存区
区 | 描述 | 物理内存 |
---|---|---|
ZONE_DMA | DMA使用的页 | < 16MB |
ZONE_NORMAL | 正常可寻址的页 | 16~896MB |
ZONE_HIGHMEM | 动态映射的页 | > 896MB |
每个区使用结构体zone
表示,具体结构详见 Linux内核设计与实现 P189
域说明:
lock
域,是一个自旋锁,防止结构被并发访问,这个域只保护结构,不保护驻留在这个区中的页watermark
域,水位值,为每个内存区设置合理的内存消耗基准name
域,表示区的名字,三个区的名字分别为"DMA",“Normal”,“HighMem”
获得页
- 分配2order(1<<order)个连续的物理内存页,返回的指针指向第一个页的page结构体
struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);
- 将指定的物理页转换为它的逻辑地址(虚拟内存地址),返回的指针指向物理页所在的逻辑地址
void *page_address(struct page *page);
- 和
alloc_pages()
功能类似,不过它直接返回请求的第一个页的逻辑地址
unsigned long __get_free_pages(gfp_t fp_mask, unsigned int order)
- 只分配一页的函数
struct page *alloc_page(gfp_t gfp_mask);
unsigned long __get_free_page(gfp_t fp_mask)
获得填充为0的页
- 分配的所有页内容全为0,返回执行逻辑地址的指针
unsigned long get_zeroed_page(unsigned int gfp_mask)
注意:为了防止页中留下一般随机的垃圾信息包含一些敏感信息,一般用户空间在获取页的时候,内容最好全部填充为0
释放页
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()
- 以字节为单位分配内存
- 可以获得以字节为单位的一块内核内存
void *kmalloc(size_t size, gfp_t flags);
gfp_mask标志
标志分为三类:
- 行为修饰符
- 区修饰符
- 类型修饰符
标志具体说明详见P192
kfree()
- 释放由
kmalloc()
分配的内存
void kfree(const void *ptr);
vmalloc()
和kmalloc()
类似,不同之处在于vmalloc()
分配的内存虚拟内存连续,但是物理内存不一定连续,而kmalloc()
分配的物理内存也是连续的
vmalloc()
正是用户空间分配内存的方式:有malloc()
分配的内存页在进程的虚拟内存中是连续的,但是物理内存不保证连续。大多情况下,只有硬件设备才需要连续的物理内存,他们不理解什么是虚拟内存
slab层
Linux内核提供slab层(即slab分配器),作为通用数据结构缓存层
slab的设计
slab层将不同的对象划分为高速缓存组,每个高速缓存组存放不同类型的对象,例如,分别存放进程描述符(task_struct结构的空闲链表),索引节点对象(struct inode)
kmalloc()
建立在slab层之上,使用了一组通用高速缓存- 一般slab仅仅由一页组成,每个高速缓存由多个slab组成
- 每个slab包含一些对象成员,对象指的是被缓存的数据结构
- slab包含三种状态:满、部分满或空
高速缓存使用结构体kmem_cache
表示,这个结构包括三个链表:slabs_full
, slabs_partial
和slabs_empty
,这些链表包含高速缓存中的所有slab
slab使用slab描述符表示,详见P216
struct slab {
...
};
slab分配器使用__get_free_pages()
创建新的slab
slab分配器的接口
- 创建一个新的高速缓存
name
:高速缓存的名字size
:高速缓存中每个元素的大小align
:slab内第一个对象的偏移量,用来确保在页内进行特定的对齐flags
:可选的设置项,控制高速缓存的行为,详见P218ctor
:高速缓存的构造函数(Linux的高速缓存不使用构造函数)- 返回指向高速缓存的指针
- 函数调用可能会睡眠,不能再中断上下文使用
struct kmem_cache *kmem_cache_create(const char *name,
size_t size,
size_t align,
unsigned long flags,
void (*ctor)(void *));
- 撤销一个高速缓存(可能睡眠,不能再中断上下文使用)
int kmem_cache_destroy(struct kmem_cache *cachep);
注意:调用kmem_cache_destroy
之前要确保两个条件:
- 高速缓存中的所有slab为空
- 在调用此函数过程中不在访问这个高速缓存
- 从已经创建的缓存中分配释放对象,使用示例详见P219
void kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
void kmem_cache_free(struct kmem_cache *cachep, void *objp);
栈上的静态分配
不同于用户栈,内核栈小而且固定,内核栈一般是两个页大小
单页内核栈
2.6内核之后,引入选项可以设置单页内核栈,激活这个选项,每个进程的内核栈只有一页大小
引入的原因有两点:
- 可以让每个进程减少内存消耗,另外随着机器运行时间增加,寻找两个连续的物理页变得越来越困难
- 当内核栈使用两页时,中断处理程序使用它所中断进程的内核栈,而当进程使用单页的内核栈时,中断处理程序不放在进程内核栈中,而是放在中段栈中。
中断栈:为每个进程提供运行中断处理程序的栈,一页大小。
总之,历史上,进程和中断处理程序共享一个栈空间,当1页栈的选项激活之后,中断处理程序获得了自己的栈。
在栈上工作
在任何函数,都要尽量节省内核栈的使用,让所有局部变量大小不要超过几百字节。栈溢出非常危险,所出的数据会直接覆盖紧邻堆栈末端的数据(例如thread_info
结构就是紧邻进程堆栈末端)。
因此,推荐使用动态分配。
高端内存的映射
永久映射
映射给定的page结构到内核地址空间,使用如下函数
void *kmap(struct page *page);
函数在对于高端内存或者低端内存都能使用:
- 如果page对应低端内存的一页,函数会单纯返回该物理页对应的虚拟地址
- 如果page对应高端内存页,函数会建立一个永久映射,在返回对应的虚拟地址
- 函数可以睡眠,只能在进程上下文中使用
当不再需要高端内存中的这一个页时,使用如下函数解除映射
void kunmap(struct page *page);
临时映射
当必须创建映射而上下文不能睡眠是,内核提供了临时映射(原子映射)
临时映射可以用在像中断上下文一样的不能睡眠的地方,使用如下函数建立心是映射:
void *kmap_atomic(struct page *page, enum km_type type);
- 函数禁止了内核抢占(因为映射对每个处理器都是唯一的???)
每个CPU的分配
SMP定义:一个操作系统的实例可以同时管理所有CPU内核,且应用并不绑定某一个内核。
支持SMP的操作系统使用每个CPU上的数据,对于给定的处理器其数据是唯一的,每个CPU的数据存放在一个数组中,数组的每一个元素对应一个存在的处理器
unsigned long my_percpu[NR_CPUS];
访问cpu数据过程:
int cpu;
cpu = get_cpu(); //获取当前CPU,并且禁止内核抢占
data = my_percpu[cpu];
.... // 使用data的过程
put_cpu();
代码中没有出现锁,因为数据对当前处理器是唯一的,没有其他处理器可以接触这个数据,没有多个处理器并发访问的问题,但是会有内核抢占的问题:
- 如果代码被其他处理器抢占并重新调度,这是cpu变量data会变成无效,因为它对应了错误的处理器
- 如果另一个进程抢占了代码,有可能在一个处理器上并发访问data数据的问题
因此,在获取当前cpu时,就已经禁止了内核抢占。
新的每个CPU接口
描述了一些为每个CPU分配内存的接口,详见P223
使用每个CPU数据的原因
使用每个CPU的好处有:
- 减少数据锁定,每个处理器访问每个CPU数据,不用加锁
- 使用每个CPU数据大大减少了缓存失效,失效发生在处理器试图使他们的缓存保持同步时,如果一个处理器操作数据时,该数据又存放在其他处理器缓存中,那么存放该数据的那个处理器必须刷新或者清理自己的缓存,频繁的缓存失效会造成缓存抖动
而使用每个CPU数据唯一的要求是需要禁止内核抢占
参考资料
- Linux内核设计与实现
- 深入Linux内核架构