本文共 4071 字,大约阅读时间需要 13 分钟。
tags: Linux源码
本文旨在以较容易理解的水平讨论Linux的内存交换机制。文中尽量不涉及具体的代码,不涉及一些边边角角的情况,通过一些图示和比喻更好的让初学者理解内存交换机制的工作原理和作用,但其具体的实现请参考深入Linux内核架构、深入理解Linux内核与Linux内核情景分析。本文讨论的Linux内核版本为2.6.24。
文中的前半部分提及了很多老生常谈的东西,如果对此有所了解可以借助目录直接跳转到关键的部分。不考虑一些大型机和专用计算机,当今通用计算机结构中存储器一般设计为多层,从最靠近CPU的cache一直到磁盘,如下图示:
可以看到从上到下速度越来越慢,但价钱却越来越便宜。所以现在通用计算机存储结构之所以搞得这么复杂从上到下分了这么多层,根本原因还是成本。如果制作cache的成本特别低,那也就不会有内存卡,磁盘(磁带等)的存在了,直接来1T的chache好了,又快又好。所以在这样大的约束下,计算机(操作)系统设计人员费劲脑汁设计了缓存和交换机制来充分利用计算机硬件,发挥最大的性能。上述工作看似简单,但要想真正实现着实有些困难,第一个比较关键的问题就是在访问内存的时候如何知道这个目的内存数据是真的在内存中还是在磁盘上呢。这里的访问内存不仅仅是指的从高级程序设计角度理解的读写内存中的一个变量值,譬如int a;a=10;
,还包含根据内存地址读取一条指令的情况。(如果缺乏相关的知识请参照这篇文章。) CPU中有一个专门的组件叫做MMU(),无论是执行汇编中类似于load、xchange
这样操作数据的指令,还是根据 PC 值取下一条指令的时候,CPU都会把要读取内存的地址交给MMU,让其做一次转换,完成从线性地址->物理地址
转化。也就是说,CPU执行的汇编代码中的地址和 PC 寄存器中保存的指令地址并不是物理地址。
(下面为了叙述的方便,用“数据”来统一代表要去内存中读的“二进制串”,不管这个二进制串是常规意义上的数据,还是一条指令)
简单说一下和这篇文章相关的内存映射的部分。物理地址就是真正到内存中取数据时用的地址,每个内存单元(字节)拥有一个唯一的地址,而线性地址存在的一大原因就是为了完成这篇文章讨论的交换机制(还有其他一些原因)。由于交换机制的存在,给定一个线性地址不一定立马就能够找到一个对应的物理地址,因为数据可能被交换到磁盘上去了。那么如何标记一个数据是不是在内存中呢,以及如何建立一个线性地址到物理地址的映射呢?请耐心往下看。
具体的数据结构请搜索逻辑地址、线性地址与物理地址
–无视分段机制即可,Linux不像Windows,在实现中也没有利用分段机制。要干的事情其实很简单,只不过和硬件相关感觉起来有点绕。
线性地址->物理地址
映射关系的一个表,同时页表中还记录了一个线性地址对应的数据是在内存中还是被交换了出去。现在假定是在内存中的物理地址 p1 处,这样的话就万事大吉了。在步骤③中物理地址 p1 通过总线发送给内存模块然后再步骤④中将数据返回给CPU。这些操作的大部分都是有硬件自动完成的,对应用程序猿来说是透明的,而且速度是很快的。这里还要说明一下,页表是操作系统创建维护的,每个进程有着自己的页表,在进程切换的时候会进行页表的切换。这也是所谓的地址空间的由来,开启页式管理的情况下,每个进程能够拥有完整的地址空间。这就可以看出,想要实现完整的交换机制需要硬件和操作系统的支持,缺一不可。
上面描述的读取操作的第二中情况基于这样一种假设“目的数据在磁盘上而不在内存中”。那么有哪些情况下页表中有对应的项,而且项被标记为“不在内存中”呢?从大的角度上讲有以下两种情况
内核联合使用了如下两种机制
1. 一个周期性的守护进程(kswapd)在后台运行,该进程不断检查当前的内存使用情况,以使在达到特定的阀值时发起页的交换操作。使用该方法,确保了不会出现突然需要换出大量页的情况。这种情况将导致系统出现很长的等待时间,必须不惜一切代价防止。 2. 但内核在某些情况下,必须能够预期可能突然出现的严重内存不足,例如在通过伙伴系统(Linux分配内存的一种机制)分配一大块内存时,或创建缓冲区时。如果没有足够的物理内存可用来满足对内存的请求,内核必须尽快换出页,以期释放一些内存空间。在紧急情况下的换出操作,属于直接回收(direct reclamin)的一部分。上面这两种方式的底层实现是相似的,只不过对于第二种方式来说如果内存不足的时候采取的措施更强硬一些。毕竟第一种方式只是例行检查,尽量保证内存使用量在一个健康的水平;而第二种方式则是在“有内存需要的时候采取的紧急措施“,所以方式会更强硬一些,譬如函数会一直等待内存数据写入到了磁盘上之后才返回、回收一些并不是那个“不经常使用”的内存。
如果内核无法满足对内存的请求,甚至在换出页以后也是如此,内么虚拟内存子系统(默认的选择)将通过OOM(out of memory,内存不足)killer了结束一个进程。虽然OOM killer有时候可能导致严重的损失,但比系统完全崩溃要好。如果在内存不足的情况下不采取措施,很可能导致系统崩溃。
上面说的叙述中可以看出交换和回写(writeback,又做写回)其实是差不多的,都是在操作系统感受到内存空间不足的时候,腾出来内存空间的一种方式。区别的地方在于:
交换是针对那些由进程动态产生的、不存在对应的磁盘文件的数据;而回写则是有对应的磁盘文件的数据,还需要说明一下上面提到的根据 PC 中存放的地址去内存中取指令(也就是数据,也就是数据了)的操作过程中,如果指令不在内存中,那么指令一定是被写回到磁盘上去了。因为,一个进程的指令总是在创建进程的时候通过分析加载可执行文件读入到内存中的。(不考虑一些特殊的情况,譬如堆栈溢出攻击)所以交换机制要做的很重要的一个事情就是图三步骤③中的,建立原来在内存中的数据到磁盘中的数据的映射。即给定一个进程的线性地址 L1 能够找到与之对应的磁盘逻辑地址 D1 (如果数据不再内存中的话)。从操作系统程序猿的角度去看,一个磁盘就相当于非常非常大的数组,磁盘的逻辑地址 D1 就相当于该数组的索引值,每个数组元素都是一个扇区的大小,这是磁盘寻址的最小单元,通常为 512byte 。
那么,步骤③更详细的叙述为 : 操作系统通过交换机制建立起来的映射,找到 L1 对应的 D1 。然后分配一块至少能够放得下一个扇区的内存,内存的物理地址为 P1 。下一步将二元组 (D1,P1) 交给下层的虚拟文件层。虚拟文件层需要上层传递的这两个主要的参数,即可将磁盘逻辑地址 D1 中的数据读入到内存物理地址 P1 去。到此为止内核就完成了步骤③的工作。不了解虚拟文件层没有关系,只需要知道其上述功能即可;而且,也就是说交换机制不会涉及数据是如何写入到磁盘中去的,又是如何从磁盘中读出来的,这些都是文件系统和驱动的工作。
内核本身使用的内存页绝不会换出。如果这样做的话将会显著增加内核代码的复杂度。由于和其他用户进程相比内核不需要太多的内存,而且与付出的额外工作量相比,将内核页换出的潜在收益实在是太低了。
通过上面的叙述,应该基本上说清楚了换出机制是什么、在内核中的作用、属于哪个子系统。具体的实现请参照Linux内核三本经典书籍。
文章的引用都已超链接的形式给出,此处就不一一罗列出来了,还有一部分是引自文章开始处推荐的书籍。侵删。
转载地址:http://ypdws.baihongyu.com/