linux内核——进程地址空间

内核的函数以相当直接了当的方式直接获得动态内存,这是因为:

1、内核是操作系统中优先级最高的成分,如果某个内核函数请求动态内存,那么,必定有正当的理由发出那个请求,因此没有道理推迟这个请求。

2、内核信任自己。所有的内核函数假定是没有错的。

当给用户态进程分配内存时情况完全不一样:

1、进程对动态内存的请求被认为不是紧迫的,例如,当进程的可执行文件被装入时,进程并不一定立即对又有的代码页进行访问,类似的当进程调用malloc以获得动态内存时,也并不意味着进程很快就会访问所有所获得的内存。因此,一般来说,内核总是尽量推迟给用户态进程分配动态内存。

2、由于用户进程时不可信任的,因此,内核必须能随时准备捕获用户态进程引起的所有寻址错误。

进程的地址空间:

进程地址空间由允许进程使用的全部线性地址组成。每个进程所看到的线性地址集合是不同的,一个进程所使用的地址与另一个进程所使用的地址之间没有什么关系。

内核通过所谓线性区的资源表示线性地址空间,线性区是由起始线性地址、长度和一些访问权限来描述的。为了效率,起始地址和线性区长度是4096的整数倍,以便每个线性区所识别的线性区完全填满分配给它的页框。下面是获得线性区的一些典型情况:

1、用户在控制台输入了一条命令,shell进程创建一个新的进程去执行这个命令。

2、正在运行的进程有可能决定装入一个完全不同的程序。

3、正在运行的进程可能对一个文件执行内存映射。

4、进程可能持续向他的用户态堆栈增加数据,知道映射这个堆栈的线性区用完为止。

5、创建IPC共享线性区和其他线程通信

6、进程调用malloc扩展动态区

确定一个进程当前所拥有的线性区是内核的基本任务,因为这可以让缺页异常处理程序有效的区分印发这个异常处理程序的两种不同类型的无效线性地址。

内存描述符:与进程地址空间有关的全部信息都包含在一个内存描述符的数据结构中(实际上就是描述进程虚拟内存的数据结构),这个结构类型为mm_struct,进程描述符的mm字段就指向这个结构。

所有的内存描述符存放在一个双向链表中。每个描述符在么mmlist字段存放链表相邻元素的地址。

mm_users字段存放mm_struct数据结构中轻量级进程的个数。mm_count字段是内存描述符的主使用计数器,在mm_user次使用计数器中的所有用户在mm_count中只作为一个单位。每当mm_count递减时,内核都要检查它是否变为0,如果是,就要解除这个内存描述符,因为不再有用户使用它。

我们使用一个例子来解释mm_user和mm_count的不同。考虑一个内存描述符由两个轻量级进程使用,他的mm_user为2,而mm_count字段1。如果把内存描述符暂时借给一个内核线程,那么,内核就增加mm_count。这样即使两个轻量级进程死亡,且mm_user为0,这个内存描述符也不释放,直到内核线程使用完为止,因为mm_cout字段扔大于0.

如果内核想确保内存描述符在一个长操作的中间不被释放,那么,就应该增加mm_user字段而不是mm_count字段。最终结果是相同的,因为mm_user的增加确保了mm_count不变为0,即使拥有这个内存描述符的所有轻量级进程全部死亡。

内核线程的内存描述符:

内核线程仅仅运行在内核态,因此,他们永远不能访问低于TASK_SIZE-0XC0000000的地址。与普通进程相反,内核线程不使用线性区,因此内存描述符的很多字段对内核线程是没有意义的。

因为大约TASK_SIZE线性地址的相应页表项都应该总是相同的,因此,一个内核线程到底使用什么样的页表集根本就没有关系。为了避免无用的TLB和高速缓存刷新,内核线程使用一组最近运行的普通进程页表。结果,每个进程描述符中包含了两种内存指针描述符:mm和active_mm

进程描述符中的mm字段指向进程所拥有的内存描述符,而active_mm字段指向进程运行时所使用的内存描述符。对于普通进程而言,这两个字段存放相同的指针。但是,内核线程不拥有任何内存描述符,因此它的mm字段总是为空,它的active_mm字段被初始化为前一个运行进程的active_mm字段。

然而,事情有点复杂。只要处于内核态的一个进程为高端线性地址修改了页表项,那么他就应当更新系统中所有进程页表集合中相应的页表项。事实上,一旦内核态的一个进程进行了设置,那么,映射应该对内核态的其他所有进程都有效。触及所有进程的页表集合是相当费时的操作,因此Linux采用一种延迟方式。

每当一个高端地址必须被重新映射时,内核就更新根目录在swapper_pg_dir朱内核也全局目录中的常规页表集合。这个页全局目录有主内存描述符的pgd字段指向,而

所有的内存描述符存放在一个双向链表中。每个描述符在么mmlist字段存放链表相邻元素的地址。

mm_users字段存放mm_struct数据结构中轻量级进程的个数。mm_count字段是内存描述符的主使用计数器,在mm_user次使用计数器中的所有用户在mm_count中只作为一个单位。每当mm_count递减时,内核都要检查它是否变为0,如果是,就要解除这个内存描述符,因为不再有用户使用它。

我们使用一个例子来解释mm_user和mm_count的不同。考虑一个内存描述符由两个轻量级进程使用,他的mm_user为2,而mm_count字段1。如果把内存描述符暂时借给一个内核线程,那么,内核就增加mm_count。这样即使两个轻量级进程死亡,且mm_user为0,这个内存描述符也不释放,直到内核线程使用完为止,因为mm_cout字段扔大于0.

如果内核想确保内存描述符在一个长操作的中间不被释放,那么,就应该增加mm_user字段而不是mm_count字段。最终结果是相同的,因为mm_user的增加确保了mm_count不变为0,即使拥有这个内存描述符的所有轻量级进程全部死亡。

存放在init_mm变量中。

线性区:

linux通过类型为vm_area_struct的对象实现线性区,每个线性区描述符表示一个线性地址空间。vm_start-vm_end表示线性区的长度,vm_mm字段表示拥有这个区间的进程的mm_struct内存描述符。vm_next表示下一个线性区。

进程所有的线性区从来不叠加,并且内核尽力把新分配的线性区与相邻的现有线性区进行合并。如果两个相邻区的访问权限相同,就能把他们合并。当一个新的线性地址空间加入到进程的地址空间时,内核检查一个已经存在的线性区是否可以扩大。如果不能就创建一个新的线性区。类似地,如果从进程的地址空间删除一个线性区,内核就要调整线性区的大小。

线性区数据结构:

进程所拥有的所有线性区是通过一个简单的链表链接在一起的。出现在链表中的线性区是按照内存地址的升序排列的,不过每个线性区可以由未使用的内存地址隔开。vm_next指向链表中的下一个元素。内核频繁执行的一个操作就是要查找包含指定线性地址的线性区。由于链表已经有序,只要在指定的线性地址之后找到线性区,停止查找。

然而当进程的线性区比较少时使用这种链表才是方便。因此,linux把内存描述符放在叫做红黑树的数据结构中,为了存放进程的线性区,linux即使用了链表,又使用了红黑树。这两种数据结构包含指向同一线性区描述符的指针,当插入或删除一个线性区描述符时,内核通过红黑树搜索前后元素,并用收索结果快速更新链表。

链表的头由内存描述符的mmap字段指向。任何线性区对象都在vm_next字段存放指向链表下一个元素的指针。红黑树的首部由内存描述符的mm_rb指向。一般来说,红黑树用来确定含有指定地址的线性区,而链表通常在扫描整个线性区集合时使用。

 缺页异常处理程序:Linux的缺页异常处理程序必须区分以下两种情况:由编程错误所引起的异常,及由引用属于进程地址空间但还未分配物理页框的页所引起的异常。

写时复制技术:第一代unix系统实现了一种傻瓜式的进程创建:当发出fork()调用时,内核原样复制父进程的整个地址空间并把复制的那一份给子进程。

现在的unix系统采用了一种更有效的方法,称之为写时复制。这种思想相对简单:父进程和子进程共享页框而不是复制页框。然而重要页框被共享,它们就不能被修改。无论父进程还是子进程何时试图写一个共享页,就产生一个异常,这是内核就把这个页复制到一个新的页框中并标记为可写。原来的页框仍是写保护的:当其他进程试图写入时,内核检查写进程是否是这个页框的唯一拥有者,如果是,这个页框是可写的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值