LINUX内核设计思想之内存管理

11.1 

内核把物理页作为内存管理单元.大多数32全体系结构支持4KB的页.从虚拟内存角度来说,页就是最小单位.struct page结构体定义于<linux/mm.h>:

 

Page结构与物理页相关的,而并非与虚拟页相关.因此,该结构对页的描述只是短暂的.即使页中所包含的数据继续存在,由于交换等原因,它们可能不再和同一个page结构关联.内核仅仅用这个数据结构来描述当前时刻在相关的物理页存放的东西.这种数据结构的目的在于描述物理内存本身,而不是描述包含其中的数据.

Flag:存放页状态,这些状态包括页是不是脏的,是不是锁定在内存中等等;

_count:存放页的引用计数.当计数为0,说明当前内核没引用这一页,在新分配中可以使用它.

Virtual:页的虚拟地址.它的页在虚拟内存中的地址.有些内存(所谓高端内存)并不永久映射到内核地址空间.

 

11.2 

由于硬件的限制,内核并不能对所有的页一视同仁.有些页位于内存特定的物理地址上,所以不能将其用于一些特定的任务.由于存在这种限制,内核把页划分为不同的区(ZONE).LINUX必须处理如下两种由于硬件缺陷引起的内存寻址问题:

.一些硬件只能用某些特定的内存地址来执行DMA(直接内存访问);

.一些体系结构其内存的物理寻址范围比虚拟寻址范围大得多.这样就有一些内存不能永久地映射到内核空间上.

LINUX使用了三种区:

.ZONE_DMA--这个区包含的页用来执行DMA操作.

.ZONE_NORMAL--这个区包含的都是能正常映射的页(即可用于分配);

.ZONE_HIGHMEM--这个区包含"高端内存",其中的页并不能永久地映射到内核地址空间.

这些区定义于<linux/mmzone.h>.区的划分没有任何物理意义,只是方便内核管理页而采用的一种逻辑分组.

LINUX把系统的页划分为区,形成不同的内存池,这样就可以根据用途进行分配了.例如,ZONE_DMA中按照请求的数目取出页.

内核用struct zone结构表征一个区.此结构中比较重要的域如下:

Lock:自旋锁,防止该结构被并发访问;

Free_pages:区中空闲页的个数.内核为保证交换目的,至少保留pages_min个空闲页可用;

Name:是一个以NULL结束的字符串,表示这个区的名字.内核启动期间初始化这个值.代码位于mm/page_alloc.c.三个区的名字分别为"DMA""Normal""HighMem".

[阅读材料:]

逻辑地址:机器语言指令中出现的内存地址.例如下面的反汇编码:

Mov 0x80495b0,%eax

0x80495b0就是一个逻辑地址.

线性地址:是对逻辑地址的一个偏移量,也叫虚拟地址;

物理地址:实际的物理地址.由线性地址通过MMU得到;

CPU将一个虚拟内存空间中的地址转换为物理地址分两步:

一、逻辑地址-->线性地址(偏移量计算,线性算术);

二、线性地址-->物理地址(MMU映射,一定的算法机制);

 

11.3 获得页

内核对内存分配和释放的API.这些API都是以页为单位分配的,定义于<linux/gfp.h>. 分配:

Struct page *alloc_pages(unsigned int gfp_mask,unsigned int order)

分配2^order个连续的物理页.出错返回NULL.

Void *page_address(struct page *page)

将给定的物理页转换为其所在的逻辑地址.

Unsigned long __get_free_pages(unsigned int gfp_mask,unsigned int order)

等于上述两函数功能和.

如果只需要一个页.可以直接用下面两个封装好的函数:

Struct page *alloc_page(unsigned int gfp_mask)

Unsigned long __get_free_page(unsigned injt gfp_mask)

让返回的页的内容全部清0可调用下面这个函数

Unsigned long get_zeroed_page(unsigned int gfp_mask)

 

释放:

Void __free_pages(struct page *page,unsigned int order)

Void free_pages(unsigned long addr,unsigned int order)

Void free_page(unsigned long addr)

 

以上都是以页为单位的内存操作,以字节为单位的内存操作调用kmalloc()函数.它在<linux/slab.h>中声明:

Void *kmalloc(size_t size,int flags)

其分配的内存大小写为size,并且物理上连续.

Flags:分配器标志.内核中分配器标志有三类.如下:
行为修饰符:内核应当如何分配所需要的内存.在某些特定的情况下,只能用某特定的方法分配内存.,ISA就要求内核在分配内存过程中不能睡眠(因为中断处理程序不能被重新调度).行为修饰符如下表所示:

 

区修饰符:表示从哪分配内存.上面我们说了LINUX把内存分为了三个区.通过此标志告诉内核我要在哪个区分配内存.内核优先选择从ZONE_NORMAL开始.如下表所示:

 

[:]_get_free_pages()kmalloc()不能指定_GFP_HIGHMEM标志,因为这两函数返回的都是逻辑地址,而不是page结构.只有alloc_pages()才可以分配高端内存.

类型:指定上述两个根本标志符以完成特殊类型的处理.如下表所示:

 

 

最常用的标志:

GFP_KERNEL:这种分配会引起睡眠.只能在进程上下文(没有锁被持有)的情况下使用;

GFP_ATOMIC:表示不能睡眠的内存分配.应用于中断处理程序、软中断、和tasklet等场合;

GFP_DMA:表示分配器必须满足从ZONE_DMA进行分配的请求.一般配合GFP_ATOMICGFP_KERNEL使用.

什么时候用哪种标志,如下表所示:

 

 

使用kmalloc()分配的内存由kfree()释放.

 

Vmalloc():分配的内存块的虚拟地址是连续的,而物理地址无需连续.出于性能的考虑,内核中一般用kmalloc()来分配内存,除非不得已--例如为了获得大块内存时,需要用valloc()函数.在日常操作时,把模块动态插入到内核时,就把模块装载到由vmalloc()分配的内存上.

vmalloc()分配的内存块由vfree()函数释放.

 

[:]内存分配有可能失败,一定要对分配内存失败的情况进行处理!

 

10.4 slab

分配和释放数据结构是所有内核最普遍的操作之一.为了便于数据的频繁分配和回收,编程者常常会用到一个空闲链表.该链表包含有可供使用的、已经分配好的数据结构块.当代码需要一个新的数据结构实例时,直接可以从空闲链表中抓取一个而不需要重新分配.因此,此空闲链表的性质等同于对象高速缓存以便快速存储频繁使用的对象类型.LINUX下的slab层便是扮演了通用数据结构缓存层的角色.

Slab层的设计思想:

Slab层把不同对象划分为高速缓存组,每个高速缓存都存放不同类型的对象.每种对象类型对应一个高速缓存.例如,一个高速缓存用于存放进程描述符,而另一个高速缓存存放索引节点对象.而这些高速缓存又划分由多个slab组成.每一个slab都包含一些对象,这些对象就是被缓存的数据结构.高速缓存、slab和对象关系如下:

 

Slab有三种状态:满、部分满或空.slab没有空闲对象;slab没有分配出去的对象;部分满处于两者之间.当内核的某一部分需要一个新的对象时,先从部分满的slab分配,如果没有得到满足,就从空的slab中分配.如果没有空的slab,只能创建一个slab.下面以一个具体的例子进行说明:

Linux中最常见的就是inode的操作.inode结构由inode_cachep高速缓存进行分配,这种高速缓存由一个或多个slab组成.每个slab包含尽可能多的struct inode对象.当内核请求分配一个新的inode结构时,内核就从部分满或空的slab返回一个指向已经分配但是未使用的结构指针.当内核用完inode对象后,slab分配器就把该对象标记为空闲.

高速缓存:

每个高速缓存都是用kmem_chche_s结构表示.这个结构包含三个链表:slabs_fullslabs_partialslabs_empty,均存放在kmem_list3结构体内.如下:

struct kmem_cache_s {

... ...;

struct kmem_list3lists;

... ...;

}

struct kmem_list3 {

struct list_headslabs_partial;/* partial list first, better asm code */

struct list_headslabs_full;

struct list_headslabs_free;

unsigned long free_objects;

int free_touched;

unsigned long next_reap;

struct array_cache *shared;

};

Slab描述符:

 

高速缓存的分配:

kmem_cache_t *

kmem_cache_create (const char *name, size_t size, size_t align,

unsigned long flags, void (*ctor)(void*, kmem_cache_t *, unsigned long),

void (*dtor)(void*, kmem_cache_t *, unsigned long))

各参数的意义如下:

Name:存放高速缓存的名字;

Size:高速缓存中每个元素大小;

Align:高速缓存第一个对象的偏移;

Flags:控制高速缓存的行为,详见<<linux内核设计与实现(2版 P158)>>

Ctor:高速缓存的构造函数,只有新页追加到高速缓存时,构造函数才被调用;

Dtor:高速缓存的析构函数,只有从高速缓存中删去页,析构函数才被调用.

销毁一个高速缓存:

Int kmem_cache_destroy(kmem_cache_t *cachep)

创建高速缓存后,可以通过下列函数从中获取对象:

Void *kmem_cache_alloc(kmem_cache_t *cachep,int flags);

该函数从给定的高速缓存cachep中返回一个指向对象的指针.

Void kmem_cache_free(kmem_cache_t *cachep,void *objp)

释放一个对象,并把它返回给原先的slab,并把cachep的对象objp标记为空闲.

 

Slab分配器的使用实例:

task_struct结构为例说明slab分配器的使用.代码位于kernel/fork.c.

首先,内核用一个全局变量存放指向task_struct高速缓存指针:
static kmem_cache_t *task_struct_cachep;

内核初始化期间,fork_init()会创建高速缓存:

task_struct_cachep =

kmem_cache_create("task_struct", sizeof(struct task_struct),

ARCH_MIN_TASKALIGN, SLAB_PANIC, NULL, NULL);

这样就创建了一个名字为task_struct的高速缓存,其中存放的就是类型为struct task_struct的对象.该对象被创建后存放在slab中偏移量为ARCH_MIN_TASKALIGN个字节的地方,ARCH_MIN_TASKALIGN与体系结构相关.当我们调用fork()函数时,会创建一个新的描述符.fork()会引发do_fork()函数的调用.如下:

long do_fork(unsigned long clone_flags,

      unsigned long stack_start,

      struct pt_regs *regs,

      unsigned long stack_size,

      int __user *parent_tidptr,

      int __user *child_tidptr)

{

... ...;

p = copy_process(clone_flags, stack_start, regs, stack_size, parent_tidptr, child_tidptr, pid);

... ...;

}

static task_t *copy_process(unsigned long clone_flags,

 unsigned long stack_start,

 struct pt_regs *regs,

 unsigned long stack_size,

 int __user *parent_tidptr,

 int __user *child_tidptr,

 int pid)

{

... ...;

p = dup_task_struct(current);

... ...;

}

static struct task_struct *dup_task_struct(struct task_struct *orig)

{

... ...;

tsk = alloc_task_struct();

... ...;

}

# define alloc_task_struct() kmem_cache_alloc(task_struct_cachep, GFP_KERNEL)

可见,fork()会去高速缓存task_struct_cachep摘一个task_struct对象下来.

销毁高速缓存,如下代码:

Kmem_cache_destroy(task_struct_cachep);

 

10.5 内核栈的静态分配

使用内核栈需要注意的是内核栈是有限的且是固定的,不能奢侈的使用.因此,实际的内核编程中需要注意以下几点:

1).在内核代码函数里面,所有局部变量所占空间之和不要超过几百字节;

2).在栈上进行大量静态分配,如大型数组和大型结构体是很危险的;

3).因此,需要在内核代码中获取一定内存区域,进行动态分配是一种明智的选择.

 

10.6 高端内存的映射

高端内存中的页不能永久地映射到内核的地址空间上.因此,通过alloc_pages()函数以__GFP_HIGHMEM标志获得的页不可能有逻辑地址.

 

10.6.1 永久映射

要映射一个给定的page结构到内核地址空间,使用下面的函数:

Void *kmap(struct page *page)

此函数可用于高端内存和低端内存.如果页位于高端内存时,会建立一个永久映射,再返回地址.这个函数可以睡眠,只能在进程上下文使用.永久性映射的数量是有限的.

解除高端内存映射:

Void kunmap(struct page *page)

10.6.2 临时映射

当必须创建一个映射而上下文又不能睡眠时,内核提供了临时映射.临时映射不能睡眠,因此,它可以被用于中断上下文.

建立临时映射:

Void *kmap_atomic(struct page *page,enum km_type type)

Type用于描述临时映射的目的.定义于<asm/kmap_types.h>.如下:

 

取消临时映射:

Void knumap_atomic(void *kvaddr,enum km_type type)

 

11.7 每个CPU的分配

为了支持SMP的现代操作系统使用每个CPU上的数据,对于给定的处理器其数据唯一.LINUX的做法是,每个CPU的数据存放在一个数组中.数组中的每一项对应着系统上一个存在的处理器.当前处理器号确定这个数组的当前元素.示意代码如下:

Unsigned long my_percpu[NR_CPUS];

按如下方式访问它:

Int cpu;

Cpu = get_cpu(); //获取当前处理器,并禁止内核抢占

My_percpu[cpu]++;//这里实现自己的代码

Printk("my_percpu on cpu = %d is %lu\n",cpu,my_percpu[cpu]);

Put_cpu(); //激活内核抢占

 

11.8 新的每个CPU接口

11.8.1 编译时的每个CPU数据

在编译时定义每个cpu变量:

DEFINE_PER_CPU(type,name);

这样变为系统中每个处理器都创建了一个类型为type,名字为name的变量.类型的功能函数还有:

DECLARE_PER_CPU(type,name);

操作处理器变量:

Get_cpu_var(name)++; //增加当前处理器上的name变量的值,同时禁止内核抢占

Put_cpu_var(namd); //完成,重新激活内核抢占

也可以获取别的处理器上的每个CPU数据,如下:

Per_cpu(name,cpu)++; //增加指定cpu上的name变量的值

如果一些处理器接触到其他处理器上的数据,必须给数据上锁!

11.8.2 运行时的每个CPU数据

内核实现每个CPU数据的动态分配方法类似于kmalloc().如下:

Void *allloc_percpu(type);

Void *__alloc_percpu(size_t size,size_t align);

Void free_percpu(const void *);

Free_percpu();

引用每个CPU数据:

Get_cpu_ptr(ptr);

Put_cpu_ptr(ptr);

运行时每个CPU数据的使用示意代码如下:

Void *percpu_ptr;

Unsigned long *foo;

 

Percpu_ptr = alloc_percpu(unsigned long );

If(!ptr)

//内存分配错误处理

Foo = get_cpu_ptr(percpu_ptr);

//操作foo

Put_cpu_ptr(percpu_ptr);

最后,操作指定处理器上的唯一数据:

Per_cpu_ptr(ptr,cpu);

 

11.9 使用每个CPU数据的原因

好处:

一、减少数据的锁定."只有指定的处理器能访问其属的数据"是一种编程约定;

二、大大减少缓存失效.比如说,一个处理器操作某数据,而该数据又存放在其他处理器缓存中,那么存放该数据的那个处理器必须清理或刷新它自己的缓存以保证数据同步.这样对系统性能影响很大;

需要注意的点是,使用每个CPU数据的唯一安全要求就是禁止内核抢占.不能在访问每个CPU数据过程中睡眠,否则,醒来后已经到了其他处理器上了.

 

11.10 分配函数的选择

上述分析了不少内存操作的函数.这里对这些内存操作函数作一个小结.

需要连续的物理页,可以使用某个低级页分配器或kmalloc().主要区分GFP_ATOMICGFP_KERNEL标志的意义;

从高端内存分配:使用alloc_pages().此函数返回一个指向struct page结构的指针,而不是一个指向某逻辑地址的指针.因为高端内存很可能间没有被映射,因此,访问它的唯一方式是通过相应的struct page结构.为了获取真正的指针,应该调用kmap()把高端内存映射到内核的逻辑地址空间.

如果不需要物理上连续的页,可以使用vmallo(),只是此函数会带来一定的性能损失;

如果你要创建和销毁很多较大的数据结构,优先考虑slab高速缓存.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值