《Linux驱动基础篇》- Linux内存管理篇

参考:http://os.51cto.com/art/201309/411937.htm

                   http://dongxicheng.org/os/linux-memory-management-basic/

————————————————————————————————————————————————————

内存管理基本单元:

        MMU内存管理单元,主要的作用是管理物理内存,当然涉及到虚拟内存和物理内存之间的转化。

        内存管理的基本单元是物理页:尽管CPU最小的可寻址单元是字(通常是字节),但是MMU(管理内存并实现虚拟地址到物理地址的转化)通常是以也页单位进行处理的。同时从虚拟内存的角度来看,最小管理单位也是页。不同的体系架构支持的页大小是不一样的。大多数32位的体系架构支持4KB的页,而64位体系结构一般会支持8KB的页。

        内核中使用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;			//经典的LRU呀  
    void			*virtual;		//物理页对应的虚拟页地址
}

       page结构只与物理页相关,而并非和虚拟页相关。这个数据结构仅用来描述当前时刻,物理内存也中存放的东西,它是目的是用来描述物理内存本身,而不是其中的数据。

       系统为了管理物理内存,每个物理页都必须分配这样一个结构体(假设占40字节)。例如:某64位体系架构,系统物理页大小为8KB,系统有4G的内存,那么一共有50多万个物理页面。如用struct page来描述的话,需要40字节 * 50w = 20M的空间来存放page结构,相对于4G内存而言可以说微不足道了,因此并不算浪费内存。

————————————————————————————————————————————————————

分区: 

      有于硬件的限制,物理页被分组为三个区:DMA、NORMAL、HIGHMEM。处在不同区的页不能用于一些特定的任务,也就是说各个区有专用的用途。而区的划分是与体系结构相关的,例如X86-32架构的上有DMA、NORMAL、HIGHMEM三个区,而X86-64架构上没有HIGHMEM区,所有的内存都能用于DMA和NORMAL。

X86-32上的区
描述物理内存
ZONE_DMADMA使用的页<16MB
ZONE_NORMAL正常可寻址的页16~896MB
ZONE_HIGHMEM动态映射的页>896MB

————————————————————————————————————————————————————

页的获取:

        内核提供了内存请求的机制,这些接口都是以页为单位来分配的。

        下面的函数定义与<linux/gfp.h>中,是页分配的最核心的函数:

struct page *alloc_pages(gfp_t gfp_mask, unsigned int order);

         将页转化为逻辑地址(其中“页地址 = 页号 * 页大小 + 页内偏移”, 只不过这里逻辑页号和物理页号不是相等的,而是它们之间是维持了一个映射的关系)

void *page_address(struct page *page);

        如果不需要要得到struct page也可以调用:

unsigned long  __get_free_pages(gfp_t gfp_mask, unsigned int order);
        如果你只需要一页,就可以用下面的两个封装好的函数:

struct page *alloc_page(gfp_t gfp_mask);

unsigned long __get_free_page(gfp_t 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);
        释放页时只能释放属于自己的页,传错了struct page或地址,用错了order值,这些都可能导致系统崩溃。

        下面是一个示例,这里用来获得8个页, 使用之后释放:

unsigned long page = __get_free_pages(GFP_KERNEL, 3);
//使用获取的8个页
free_pages(page, 3);

        调用__get_free_pages()之后要注意进程错误检查,内核可能分配失败,因此代码必须进行检查并做相应的处理。

        当你需要以页为单位的一族连续物理页时,尤其在你只需要一两页时,这些低级页函数就很有用。而对于常用的以直接为单位的分配来说,内核提供了函数kmalloc()。

字节获取:

        kmalloc()函数与用户空间的malloc()一族函数非常类似,只不过它多了一个flags参数。kmalloc()函数是一个简单的接口,用来以字节为单位获取一块内核内存。如果需要的是页,有gfp类型的函数更适合。对于大多数内核分配来说,kmalloc()接口用得更多。

        kmalloc()在<linux/slab.h>中声明:

void *kmalloc(size_t size, gfp_t flags);
        这个函数返回一个指向内存的指针,分配的内存是连续的物理空间,并且必须至少有size个字节。

字节释放:

        kmalloc()的另一端是kfree(),声明在<linux/slab.h>中:

void kfree(const void *ptr);

gfp_mask分配标志:

        这些标志大致分为:行为修饰符、区修饰符、类型。行为修饰符表示内核应当如何分配说需要的内存,区修饰符指明在哪个区进行分配,类型表示符是前面两者的组合。GFP_KERNEL为类型标志,内核中进程上下文相关的代码可以使用它。

        1.行为修饰符



        2.区修饰符


  

        3.类型标志



        其中内核最常用的标志之一是GFP_KERNEL,这种分配可能引起睡眠,它使用的是普通优先级,因此这个标志只可用在可以重新安全调度的进程上下文中。

        另外一个截然相反的标志是GFP_ATOMIC,这个标志不能引起睡眠,因此调度者在获取内存请求时受到严格限制。

        在以上连个标志中间的是GFP_NOIO和GFP_NOFS,这两个标志可能会引起阻塞。GFP_NOIO分配绝不会启动任何磁盘I/O来帮助满足请求;而GFP_ONFS可能会启动磁盘I/O,但是它不会启动文件系统I/O。



vmalloc函数

        vmalloc函数的工作方式类似于kmalloc(),只不过前者分配的内存虚拟地址是连续的,而后者分配的内存物理地址是练习的。这是由用户空间的分配函数工作方式决定的:

        1.malloc()返回的页在进程的虚拟地址空间内是连续的,当不保证它们在物理地址上是连续的
        2.kmalloc保证页在物理地址上是连续的,当然虚拟地址也是连续的
        3.vmalloc返回的页确保页在虚拟地址空间中连续,在同过非连续的物理页与虚拟页映射
        大多数情况下,只有硬件设备需要得到物理地址连续的内存,很多体系结构上硬件设备存在于MMC之外,它根本不理解什么是虚拟地址。因此硬件设备用到的任何内存区都必须是物理上连续的块;而供软件使用的内存页就可以只要求他们在虚拟地址上连续。当在编程过程中,基本察觉不到这种差异。

        尽管某些情况下需要物理上连续的内存页,当时内核中常用kmalloc()来获取内存页,而不是vmalloc()。原因是前者性能较高,vmalloc为了把不连续的物理页转化为虚拟地址空间上连续的页,必须专门建立页表项。糟糕的是,vmalloc得到的页必须一个一个的进行映射。

void  *vmalloc(unsigned long size);
void vfree(const void *addr);
        vmalloc得到的是虚拟地址,可能会引起睡眠,因此不能在中断的上下文中使用。

slab层介绍

        上面介绍了内存管理的基本单元为页,内存获取和释放也都是以页为单位的(struct page是如何管理的,在心中形成了巨大的以为,难道是传说中的红黑树吗?)。内核提供了get_free_page、kmalloc、vmalloc,它们都是以分配页为基础的(得到的都是逻辑地址,需要映射的逻辑地址、和不需要映射的逻辑地址它们的区别是什么呢?)。

        然而内核当中会频繁的分配、释放数据结构,为了便于数据的分配和释放,往往会使用类似线程池的链表结构。链表中包含:可使用的、已分配的数据结构。当需要一个新的数据结构实例,从链表结构中获取一个就可以实现;当需要释放一个数据结构时,这个数据仍然留在链表结构中,并标记为可使用。

        这上面的描述看来,空闲链表相当于一个对象的高速缓存池,用于快速分配和释放对象。 内核中提供了slab层来扮演数据结构高速缓存的角色,个人喜欢把它叫做对象高速缓存池。之所以使用slab层是基于下面的理由来考虑的:

        1.频繁的分配和释放数据结构,因此应当缓存它们
        2.频繁分配和释放必然导致碎片
        3.作为数据结构缓存,明显能提高效率
        4.如果slab分配器知道对象的大小、页大小和高速缓存的大小,它会做出更优的决定

slab层设计

        其核心思想是:分组 + 缓存。slab层不同对象划分为不同的高速缓存组,其中每个高速缓存组都存放不同类型的对象,每一个类型对应一个高速缓存组。这些高速缓存组又划分为不同的slab,slab由物理页组成(不同对象组成)。

        其中每个slab都包含一些对象(放在页中,所以也可以说是物理页)。每个slab处于三种状态:       

满         部分满        空

        当内核需要获取一个对象时,先从部分满中获取;如果没有部分满就从空当中获取;如果全满那么这个时候就创建一个新的slab。(和进程池一样,应该有个上限根据实际情况调整)。以磁盘索引节点为例,由于其频繁的创建和释放,因此使用slab分配器来管理就很有必要(还是缓存的思想),struct inode就可以由inode_cachep高速缓存来分配,这种高速缓存有一个或多个slab,每个slab尽可能多的包含struct inode对象(原因是这样的对象很多)。当内核请求获取一个新的inode结构时,内核就从slab中返回一个未被使用的结构体指针;当内核使用完inode对象后,slab分配器就把该对象标记为空闲。

       下图显示了高速缓存、slab以及对象之间的关系:


        其中每个高速缓存都使用kmem_cache结构来表示,这个结构包含三个链表:slabs_full、slabs_partial、slabs_empty,它们均放在kmem_list3结构中。

struct kmem_cache {
	struct array_cache *array[NR_CPUS];
	unsigned int batchcount; 	//要转移本地高速缓存的大批对象的数量
	unsigned int limit;		//本地高速缓存中空闲对象的最大数目
	unsigned int shared;
	unsigned int buffer_size;	//高速缓存的大小
	u32 reciprocal_buffer_size;
	
	unsigned int flags;	//描述高速缓存永久属性的一组标志
	unsigned int num;	//封装在一个单独slab中的对象个数
	unsigned int 	gfporder; // 一个单独slab中包含的连续页框数目的对数
	gfp_t gfpflags;
	size_t colour;		//slab使用的颜色个数
	unsigned_int colour_off; //slab中的基本对齐偏移

	struct kmem_cache *slabp_cache;
	
	unsigned int slab_size; 	//slab的大小
	unsigned int dflags; 		//动态标志
	
	void (*ctor)(void *,struct kmem_cache *,unsigned long);			//构造函数
	const char *name;		//存放高速缓存名字的字符数组
	struct list_head next;		//高速缓存描述符双向链表使用的指针
	...
	struct kmem_list3 *nodelists[MAX_NUMNODES];//高速缓存中的slab链表
		
	//下面三个参数待定	
	unsigned int objsize;	//高速缓存中包含的对象的大小
	unsigned int free_limit;//整个slab高速缓存中空闲对象的上限
	spinlock_t	spinlock;//高速缓存自旋锁
}


       这些链表包含高速缓存中所有的slab,struct slab用来表述每个slab:

struct slab {
    struct list_head	list;		//满、部分满、空
    unsigned long	colouroff;	//slab着色的偏移量
    void  		*s_mem;		//slab中的第一个对象
    unsigned int 	inuse;		//slab中已分配的对象数
    kmem_bufctl_t	free;		//slab中第一个空闲对象
}

slab分配器接口:

         创建一个高速缓存:

struct kmem_cache *kmem_cache_create(const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *));
         第一个参数是一个字符串,存放高速缓存的名字;第二个参数代表高速缓存中每个元素的大小;第三个参数是slab内第一个对象的偏移;第四个参数可选,是用来控制高速缓存的行为;最后一个参数是高速缓存的构造参数。kmem_cache_create()函数返回一个创建高速缓存的指针,若不成功则返回NULL。这个函数不能在中断上下文中使用,因为可能会引起睡眠。

        销毁一个高速缓存:

int kmem_cache_destory(struct kmem_cache *cachep);
        该函数调用的两个条件:

1.高速缓存中的slab都为空
2.高速缓存不再需要
        获取一个对象:
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
        该函数从高速缓存cachep中返回一个对象的指针,如果slab中全是满的,那么就需要使用kmem_getpages()

        释放一个对象:

void *kmem_cache_free(struct kmem_cache *cachep, void *objp);
        这样就把一个高速缓存中的对象标记为空。

slab分配器实例:        

        下面以task_struct结构为例,实现slab分配器分配对象。

struct kmem_cache *task_struct_cachep;
task_struct_cachep = kmem_cache_create("task_struct", sizeof(structtask_struct), ARCH_MIN_TASKALIGN, SLAB_PANIC | SLAB_NOTRUCK, NULL);
        应用程序通常调用fork()创建一个进程描述符:

struct task_struct *task;
task = kmem_cache_alloc(task_struct_cachep, GFP_KERNEL);
        当用完一个进程描述符后需要释放:

kmem_cache_free(task_struct_cachep, task);
        看完用法后,注意只有当需要频繁创建某一类对象时,才需要用到slab分配器。



         
        



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值