现在的位置: 首页 > 综合 > 正文

Linux设备驱动开发-linux内存管理简介(二)

2013年12月03日 ⁄ 综合 ⁄ 共 3639字 ⁄ 字号 评论关闭

前面讲了linux内核空间于用户空间的管理,下面我们就来说说内存管理中,物理内存的分配机制。

前面说过,linux主要是通过页表进行映射的,如果给出的页表不同,那么cpu由某一虚拟地址空间中的地址转化成的物理地址就会不同,所以需要为每个进程建立相应的页表。而内核也需要对每个页面当前的状态进行记录,这些信息就被保存在了一个page结构体数组中,每个页面对应对应一个数组元素。page结构体里记录了进程数、页面状态等信息。大家都知道,随着进程的执行,系统要不断的分配、释放页面,而且频繁的分配、释放,势必会产生内存碎片的问题,所以一个高效、稳定的分配策略是必须的。linux采用伙伴算法来解决碎片的问题,那么下面我们就讲讲这个算法。

伙伴算法将所有空闲的页面分为了10个块链表,而每一个链表上块都有2的幂次个页面,如:第0个链表上的块大小为1个页面,第1个链表上的块大小为2个页面,第2个链表上的块大小为4个页面.。。。其工作原理为:假设分配的块大小为128个页面,那么算法就在128个页面(第7个)的这个链表上查找空闲块,如果找到,就将其分配,如果没有,那么就继续在256个页面大小的这个链表上查找,如果有空闲块,就将其中的128个页面分配给这个块,剩下的128个页面大小的空闲块就插入到上一级链表中,以此类推。而释放块的过程为,假设要释放大小为128个页面的块,释放完毕后,再看与其物理地址相邻,并且在一个链表上的块,如果是空闲块的话,就将其合并,然后挂到下一级链表上,以此类推。而我们刚才合并的那两个块就称为伙伴。

当然,也由它的工作原理可以看出,伙伴算法分配的块,其都是由连续的物理页面组成的,而__get_free_pages()函数就是用于分配物理页块的,函数原型如下:

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)

其中gfp_mask,表示对所分配内存的要求常用的参数有GFP_KERNEL(分配内存期间可以睡眠)GFP_ATOMIC(不可以睡眠,用于中断处理程序)。

order表示其所请求的页块大小为2的order次幂个页面。

该函数首先检查order是否10的范围内,继而检查空闲的物理页是否低于许可范围的下界。低于下界,则通过函数:

wake_up_process(struct task_struct *p)

将守护进程唤醒,如:

wake_up_process(kswapd_process);

其将内核中的某些页交换到外存,保证系统有足够的空闲块。换页是通过函数:

unsigned long try_to_free_pages(struct zonelist *zonelist, int order,
                                gfp_t gfp_mask, nodemask_t *nodemask)

完成的。

页面的分配说完,下面看下页块的释放,页块一般是通过

void free_pages(unsigned long addr, unsigned int order)

来进行释放的。addr表示要回收的页块的首地址,order和前面所说的定义相同。回收的过程中先要检查是否有别的进程使用此页块,有的话就不能回收,回收之后的页块则就插入到相应页块大小的链表中,再看是否可以合并。

前面所说的伙伴算法,每次分配至少一个页面,但是当要分配的空闲区很小时该如何处理?这就引出了slab分配机制。

当给用户态的进程分配页面时,内核调用__get_free_pages()函数,并将分配的页面填充为0。而给内核的数据结构分配页面,首先要对数据结构所在的内存区进行初始化,不使用时就要回收所占内存,这里slab主要引入了对象的概念,这里的对象就是存放一组数据结构的内存区,对象中包含构造和析构函数,所以就将初始化和回收的任务交给这两个函数去完成,为了避免重复初始化对象,slab模式并不丢弃已分配的对象,回收之后依然保留在内存区,当再次请求分配同一对象时,就可在内存中获取而不用再进行初始化。在linux中,对其进行了改进,并不调用构造和析构函数,而是将指向这两个函数的指针都置为空,但是想表达的思想是一致的。linux引用slab的目的就是减少伙伴算法的调用次数。

下面看下slab专用缓冲区的建立与释放,对于预期会频繁使用的内存区,就可以创建一组特定大小的专用缓冲区进行处理,以避免碎片的产生。如用于task_struct、mm_struct等频繁使用的数据结构。专用缓冲区是通过下面的函数

struct kmem_cache *
       kmem_cache_create (const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void *))

name:缓冲区的名字;

size:对象大小;

align:第一个对象的偏移量,为0时表示标准对齐;

flags:对缓冲区设置的标志;

ctor:构造函数。

对应的,如果想要撤销为这个模块中的数据结构所建立的缓冲区,是通过下面的函数完成的:

void kmem_cache_destroy(struct kmem_cache *cachep)

创建缓冲区之后,就可以通过下面的函数获取对象:

void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)

该函数返回一个指向对象的指针,当缓冲区没有空闲的对象,那么就调用__get_free_pages()函数获取新的页面。

释放对象时,就用下面的函数,释放后,将对象标记为空闲。

void kmem_cache_free(struct kmem_cache *cachep, void *objp)

通用缓冲区的建立与释放,对于初始化开销不大的数据结构,预期较少使用内存区,就可以合并使用一个通用的缓存区,其类似于物理页面分配中的大小分区,最小为32字节,然后是64、128....,最大为128KB(32个页面),但其管理方式是slab。通过下面的函数建立和释放缓冲区的:

static __always_inline void *kmalloc(size_t size, gfp_t flags)
void kfree(const void *objp)

在内核中,尤其是驱动程序中,大量的数据结构是一次性使用的,而且占用内存很少,一般情况用kmalloc()分配内存就足够了。前面的字符设备驱动的例子中已经使用了这个方法,大家可以去看看。

还有一种使用vmalloc的方法分配内存去的方法,如下:

void *vmalloc(unsigned long size)

void vfree(const void *addr)

他与kmalloc的区别是:kmalloc分配的的页在物理上是连续的,而vmalloc分配的物理地址无须连续。但是很多内核代码都调用kmalloc的方法,主要是出于性能上的考虑,vmalloc只在绝对必要时才使用,如要获得大块内存时。

下面看一个内存分配的小例子:

#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/mm.h>
#include <linux/vmalloc.h>

unsigned long pagemem;
void *kmem;
void *vmem;

static int __init mymem_init(void)
{
	printk("mem_init is running..\n");

	pagemem = __get_free_page(GFP_KERNEL);
	if (!pagemem) {
		printk("pagemem failed\n");
		return -1;
	}
	printk("pagemem = 0x%lx\n", pagemem);

	kmem = kmalloc(1000, GFP_KERNEL);
	if (!kmem) {
		printk("kmem failed\n");
		return -1;
	}
	printk("kmem = 0x%p\n", kmem);

	vmem = vmalloc(1000);
	if (!vmem) {
		printk("vmem failed\n");
		return -1;
	}
	printk("vmem = 0x%p\n", vmem);

	return 0;
}

static void __exit mymem_exit(void)
{
	printk("mem_exit is leaving..\n");
	free_page(pagemem);
	vfree(vmem);
	kfree(kmem);
}

MODULE_LICENSE("GPL");
module_init(mymem_init);
module_exit(mymem_exit);

大家自己运行查看结果即可。

上面只是内存管理的一个大概的讲解,其实内存管理的东西还是很多很深的,需要下来自己再进行探索研究。

抱歉!评论已关闭.