Notes on Understanding the Linux Kernel

第七章进程地址空间

内核中的函数可以直接获得动态内存,调用__get_free_pages()使用伙伴系统获得页框,kmem_cache_alloc()或kmalloc()使用slab分配器为专用或通用对象分配块,以及vmalloc()获得一块非连续的内存区。如果锁清秋的内存区能被满足,这些函数都返回一个线性地址,这个线性地址就是所分配的动态内存区的起始地址。

内核使用一种新的资源成功实现了对进程动态内存的推迟分配,当用户态进程请求动态内存时,并没有获得另外的页框,而仅仅获得对一个新的线性地址区间的使用权,而这一线性地址区间就成为进程地址空间的一部分。这一区间被称为线性区。


进程的地址空间

进程的地址空间由允许进程使用的全部线性地址组成。每个进程所看到的线性地址集合是不同的,一个进程所使用的地址与另外一个进程所使用的地址之间没有什么关系。内核可以通过增加或删除某些线性地址区间而动态的修改进程的地址空间。

内核通过一种称为线性区的资源来标识线性地址区间,线性区是由起始线性地址,长度和一些存取权限来描述的。为了效率期间,起始地址和线性区的长度都必须是4096的倍数,以便用每个线性区所识别的数据完全填满分配给它的页框。

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

  • 由编程错误引发的无效线性地址
  • 由缺页引发的无效线性地址,即使这个线性地址属于进程的地址空间,对应于这个地址的页框仍然有待分配。

从进程观点看,后一种地址不是无效的。内核通过提供页框来处理这种缺页,并让进程继续执行。

线性区数据结构

进程所拥有的所有线性区是通过一个简单的链表链接在一起。出现在链表中的线性区是按内存地址的升序排列的;每两个线性区可能是由未用的内存地址分开的。

每个vm_area_struct元素的vm_next域指向链表的下一个元素,内核通过进程的内存描述符的mmap域来寻找特定的线性区,而mmap域指向链表中的第一个线性区描述符vm_next域。内存描述符的map_count域存放进程所拥有的线性区数目。一个进程可以最多拥有MAX_MAP_COUNT个不同的线性区。

内核频繁执行的一个操作就是查找指定线性地址所在的线性区。由于链表是经过排序的,因此只要在指定线性地址之后找到一个线性区,搜索可以结束。

仅当进程有非常少的线性区时,使用这种链表是很方便的。在链表中查找元素,插入元素删除元素涉及许多操作,这些操作所花费的时间与链表的长度成线性比。面对对象的数据库那样的大型应用程序可以有成百上千的线性区。这种情况下,线性区链表的管理变得很低效。因此与内存相关的系统调用的性能就降低到令人无法容忍的程度。

当有大量线性区时,Linux把它们的描述符存放在称为AVL树的数据结构。
AVL树,每个元素或节点有两个孩子,左孩子小于节点N,右孩子大于节点N。每个节点有一个平衡因子。平衡因子是节点N左孩子树的深度减去右孩子树的深度。只可能是0,-1,+1。
在AVL树中查找一个元素很高效,因为它需要的操作时间和输的大小的对数成线性比例。

线性区存取权限

页即表示一组线性地址又表示这组地址中存放的数据。0-4095的线性地址为第0页,4096-8191位第一页。

  • 每个页表的表项中存放的几个标志,read、write、present
  • 存放在每个页描述符flags中的一组标志。

第一种标志用于Intel8086硬件检查苏哦哦请求的寻址类型是否可执行。第二种由Linux用于许多不同的目的。

第三种标志,与线性区的页相关的标志。

线性区的处理

对控制内存处理所用的数据结构和状态信息有几本了解之后,我们来看一组线性区描述符进行操作的底层函数。

寻找给定地址的最近线性区

释放线性地址区间

第一阶段:扫描线性区链表,并删除与制定线性地址区间相重叠的所有区

第二阶段,更新进程的页表,并重新调整线性区链表

缺页异常处理程序

栈是如何映射到线性区上的。每个向低地址扩展的栈所在的区,它的VM_GROWSDOWN标志被设置,这样当VM_START域的值可能被见效的时候而VM_END的值保持不变,这个线性区确定的范围涵盖用户态栈当前的大小但不正好是这个大小。

  • 线性区的大小是4KB的倍数,而栈的大小却是任意的
  • 分给一个线性区的页框在这个线性区被删除前永远不被释放。

当填满分配给线性区以外的最后一个页框后,这个进程可能引起一个缺页异常,即push指向这个线性区以外一个地址。指向一个不存在的页框。

请求调页

指的是一种动态内存分配技术,把页框的分配推迟到不能再推迟为止。一直推迟到要访问的页不在RAM中为止,由此引发一个缺页异常。
请求调页技术背后的动机是,进程开始运行的时候并不访问其他地址空间中的全部地址。事实上有一些地址进程永远不适用。程序的局部性原理保证了在程序执行的每个阶段,真正使用的进程只有一小部分,因此临时用不着的页所在的页框可以由其他进程来使用。因此,对于全局分配来说,请求调页是首选,因为它增加了系统中的空闲页框的平均数,从而更好地利用空闲内存。从另一个观点看,在内存总数保持不变的情况下,请求调页总体上能使系统有更大的吞吐量。

为这个有点付出代价的是系统额外的开销:由请求调页所引发的每个缺页异常必须由内核处理,这将浪费CPU时钟周期。局部性原理保证了一旦进程开始在一组页上运行,接下来相当长的一段时间内会停留在这些页上而不去访问其他的页,我们可以认为缺页异常是一种稀有事件。

写时复制

当发出fork()系统调用时,内核原样复制父进程的整个地址空间并把复制的那一份分配给子进程。这种是非常耗时的:

  • 为子进程的页表分配页框
  • 为子进程的页分配页框
  • 初始化子进程的页表
  • 把父进程的页复制到子进程相应的页中
    这种创建一个地址空间的方法涉及许多内存访问,消耗许多CPU周期,并且完全破坏了高速缓存中 的内容。在大多数情况下,这样做是毫无意义的,因为许多子进程通过装入一个新的程序开始它们的执行,这样就完全丢弃了所继承的地址空间。

更为有效的办法是,写时复制。父进程和子进程共享页框,只要页框被共享,它们就不能被修改,无论父进程和子进程何时试图写一个共享的页框,就产生一个异常,这时内核就把这个页复制到一个新的页框中标记为科协。原来的页框仍然是写保护的,当其他进程试图写入时,内核检查谢金成是否是这个页框的唯一属主,如果是,就把这个页框标记为对这个进程是可写的

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值