linux 内核—进程的地址空间(1)

来源:互联网 发布:iapp免杀源码 编辑:程序博客网 时间:2024/06/01 10:16

系统中所运行的程序,被叫做进程。每个进程在执行的过程中都会有自己独立的地址空间,而系统是如何确定哪块地址空间是该进程的?所以创建了有关进程的地址空间的数据结构体,通过这些结构体来创建、查找、删除与进程对应的地址空间。

什么是地址空间?由进程的所有线性地址构成的空间叫做地址空间。但是地址空间又是由n(n>=0)个线性区构成的,也就是说,地址空间中的线性地址并不是连续的。而线性区的线性地址却是连续的。

所以,在内核中创建了两个和进程的地址空间相关的结构体。

struct mm_struct {    struct vm_area_struct *mmap; //指向线性区对象的链表头    struct vm_area_struct *mmap_cache; //指向最后一个引用的线性区对象    struct rb_root mm_rb; //指向线性区对象的红-黑树的根    struct list_head mmlist; //指向内存描述符链表中的相邻元素}

该结构体叫做内存描述符,列出了其中主要的几条成员变量,它包含了所有与进程的地址空间相关的信息。

struct vm_area_struct {    struct mm_struct *vm_mm; //指向线性区所在的内存描述符     struct vm_area_struct *vm_next; //线性区链表中的下一个线性区    struct rb_node vm_rb; //用于红-黑树的数据    unsigned long vm_start; //线性区内的第一个线性地址    unsigned long vm_end; //线性区之后的第一个线性地址    strcut vm_operations_struct  *vm_ops; //指向线性区的方法    struct file *vm_file; //指向映射文件的文件对象}

该结构体叫做线性区,列出其中的主要的成员变量。进程所拥有的线性区从来不重叠,并且内核尽力把新分配的线性区与紧邻的现有线性区合并。如果两个相邻区的访问权限相匹配,就能把它们合并在一起。

下来说一下这两个结构体和进程之间是如何挂钩的,进程的结构体是:

struct task_struct;

关系如下图所示:
这里写图片描述

另外,在线性区中的结构体中既然已经定义了链表结构,为什么还要创建红-黑树?因为链表的查找、删除、插入操作在时间复杂度上都是很大的耗费,所以链表只适合在线性区个数存在20以内的情况下使用,当超过该数目时,则并不适用。这时候,使用红黑树这个结构就是最有效率的一种选择。

所以,进程所占用的线性区数目较多时,采用红-黑树数据结构来存储。在mm_struct中的mm_rb变量中存储红-黑树的根节点的地址。在vm_area_struct中的rb_node成员中存储红-黑树各个节点的地址。

知道了这三者的关系,就可以对进程的地址空间进行操作了。但是,系统对内存的操作是以页为单位的,而进程的地址空间最小单位是线性区,所以,可以知道的是线性区是由多个页组成的,并且这些页的页号是连续的。

所以,在最初为进程分配内存的时候,使用的是页框分配器,主要的功能函数是:

__get_free_pages();alloc_pages();

后来,出现了slab分配器,可以对内存中的碎片进行回收,合并,再利用,该分配器主要的功能函数是:

kmem_cache_alloc();kmalloc();

而另外在内核模块的编程中,常用的内存申请函数有两个,分别是:

vmalloc();vmalloc_32();

这些函数的主要目的是为进程申请一块地址空间,而该操作需要对内核中的相关数据结构作出调整,主要是线性区结构体。

为一个进程分配线性区有三种方法,分别是查找给定地址的最邻近区;查找一个与给定的地址区间相重叠的线性区;查找一个空闲的地址区间。下来按照顺序,挨个说明。

第一个由函数 find_vma() 处理,该函数有两个参数值:进程内存描述符的地址 mm 和线性地址 addr。它查找线性区的 vm_end 大于 addr 的第一个线性区的位置,并返回这个线性区描述符的地址;如果没有这样的线性区存在,就返回以个 NULL 指针。注意由 find_vma() 函数锁选择的线性区并不一定要包含 addr ,因为 addr 可能位于任何线性区之外。

下来介绍该函数中的主要代码行:

vma = mm->mmap_cache;if(vma && vma->vm_end > addr && vma->vm_start <= addr)    return vma;

代码中重要的字段为 mmap_cache,该字段保存进程最后一次引用的线性区的描述符地址。引进这个附加的字段是为了减少查找一个给定线性地址所在线性区而花费的时间。

程序中引用地址的局部性使下面这种情况出现的可能性很大:如果检查的最后一个线性地址属于某一给定的线性区,那么,下一个要检查的线性地址也属于这一个线性区。

所以,上边的代码是在判断最后一个线性区是否存在地址 addr,存在,则返回该线性区描述符地址,否则,在线性区的红-黑树存储结构中查找符合条件的线性区。处理代码如下:

rb_node = mm->mm_rb.rb_node;vma = NULL;while(rb_node) {    vma_tmp = rb_entry(rb_node, struct vm_area_struct, vm_rb);    if(vma_tmp->vm_end > addr) {        vma = vma_tmp;        if(vma_tmp->vm_start <= addr)            break;        rb_node = rb_node->left;    }    else {        rb_node = rb_node->rb_right;    }    if(vma)         mm_mmap_cache = vma;    return vma;}

该函数中有一个重要的操作,是宏操作 rb_entry,它的作用是得到线性区结构体的入口地址。这个操作和 list_entry 的操作是一样的,只是结构体变成了 struct vm_area_struct,计算点变成了 rb_node。

第二个方法,查找一个与给定的地址区间相重叠的线性区。由函数
find_vma_intersection() 来完成。处理代码行如下:

vma = find_vma(mm, start_addr);if(vma && end_addr <= vma->vm_start)    vma = NULL;return vma;

可以看到的是,该函数主要还是利用 find_vma() 函数来完成。 其中的参数 mm 指向进程的内存描述符,参数 start_addr 和 end_addr 指定了区间的线性地址。

第三个方法,查找一个空闲的地址区间,该方法主要由 get_unmapped_area() 函数完成。该函数搜查进程的地址空间以找到一个可以使用的线性地址空间。从这儿,可以看出来。进程的地址空间是已经创建好的,这三种方法只是在创建好的地址空间中查找对应的空闲的线性区间。

该函数的参数分别代表的含义是,参数 len 指定区间的长度,参数 addr 指定必须从哪个地址开始进行查找。如果 addr 不为空,则函数就检查指定的地址是否在用户态并且与页边界对齐,因为内存的操作单位为页。接下来,如果线性地址被用于文件内存映射,则函数会调用 get_unmapped_area 文件操作;如果是匿名内存映射,则调用 呢村描述符结构体中的 get_unmapped_area 方法。

下来,先看一下匿名内存映射的情况,执行到上一步后,接下来先检查进程线性区的类型,根据类型调用不同的方法,这两个方法才是真正干活的函数。而线性区的类型分为两种:一种从线性地址 0x40000000 开始并向高端地址增长,另一种正好从用户态堆栈开始并向低端地址增长。

这里分析一下 arch_get_unmapped_area() 函数主要的代码行,也就是第一种情况:

if(len > TASK_SIZE)    return -ENOMEM;addr = (addr + 0xfff) & 0xffff0000;if(addr && addr + len <= TASK_SIZE) {    vma = find_vma(current->mm, addr);    if(!vma || addr + len <= vma->vm_start)        return addr;}start_addr = addr = mm->free_area_cache;for(vma = find_vma(current->mm, addr); ; vma = vma->next;) {    if(addr + len > TASK_SIZE) {        if(start_addr == (TASK_SIZE/3+0xfff)&0xfffff000)            return -ENOMEM;        start_addr = addr = (TASK_SIZE/3+0xfff)&0xfffff000;        vma = find_vma(current->mm, addr);    }    if(!vma || addr + len <= vma->mm_start) {        mm->free_area_cache = addr + len;        return addr;    }    addr = vma->vm_end;}       

该函数首先检查区间的长度是否在用户态下线性地址区间的限制长度之内,这个限制标准是3GB,用宏定义进行了声明,变量名为 TASK_SIZE。
为了安全起见,在函数中把参数 addr 调整为4KB 的倍数。因为,线性区的操作单位是页,每页的大小为4KB。

如果 addr 等于0或前面的搜索失败,函数arch_get_unmapped_area() 就扫描用户态线性地址空间,查找一个可以包含新申请的线性区的足够大的线性地址范围,但任何已有的线性区都不包括这个地址范围。

为了提高搜索的速度,让搜索从最近被分配的线性区后面的线性地址开始。把内存描述符的字段 mm->free_area_cache 初始化为用户态线性地址空间的三分之一(通常是1GB),并在以后创建新线性区时对它进行更新。如果函数找不到一个合适的线性地址范围,就从用户态线性地址空间的三分之一的开始处重新开始搜索。

该函数调用 find_vma() 以确定搜索起点之后第一个线性区终点的位置。可能出现三种情况:

  • 如果所请求的区间大于正待扫描的线性地址空间部分(addr + len > TASK_SIZE),函数就从用户态地址空间的三分之一处重新开始搜索,如果已经完成第二次搜索,就返回 -ENOMEM(没有足够的线性地址空间来满足这个请求)。
  • 刚刚扫描过的线性区后面的空闲区没有足够的大小
    (vma != NULL && vma->start < addr + len)。此时,继续考虑下一个线性区。

  • 如果以上两种情况都有发生,则找到一个足够大的空闲区,此时,函数返回 addr。

既然,已经找到了空闲区,下来要做的就是如何将该空闲区分配给进程。

阅读全文
0 0
原创粉丝点击