配套教材:
Operating Systems: Three Easy Pieces Remzi H. Arpaci-Dusseau Andrea C. Arpaci-Dusseau Peter Reiher
参考书目:
1、计算机操作系统(第4版) 汤小丹 梁红兵 哲凤屏 汤子瀛 编著 西安电子科技大学出版社

在线阅读:
http://pages.cs.wisc.edu/~remzi/OSTEP/
University of Wisconsin Madison 教授 Remzi Arpaci-Dusseau 认为课本应该是免费的。
————————————————————————————————————————
这是专业必修课《操作系统原理》的复习指引。
需要掌握的概念在文档中以蓝色标识,并用可读性更好的字体显示 Linux 命令和代码。代码部分语法高亮。
文档下载地址:
链接:https://pan.baidu.com/s/1NnhIvmRTfoijjBndFbL8NA
提取码:0000

附录一 虚拟内存子系统案例选讲

下面我们以VAX / VMS系统和Linux系统两个实例为例,来梳理一遍虚拟内存子系统。VAX / VMS开发于1970年代到1980年代,但里面有许多方法和技巧一直被应用于今天。Linux系统则应用于多个行业。小到嵌入式系统、手机,大到数据中心,都能见到Linux的身影。这充分反映了Linux的虚拟内存子系统(VM System)能够灵活适应各种规模、各种场景。VAX-11小型机(mini-computer)在1970年代末由DEC(Digital Equipment Corporation)开发。在小型机时代,DEC是计算机行业的佼佼者。但很遗憾,由于一系列错误决定和PC(个人计算机)时代逐渐到来,DEC最终于1998年被Compaq收购。而Compaq在2002年又被HP(Hewlett Packard)并购。VAX / VMS是VAX系列计算机运行的操作系统,Dave Cutler为首席设计师。后来Cutler参与开发了Windows NT。VMS当年也在各种不同价格的计算机上运行得很好。
VMS通过软件上的一系列创新来隐藏硬件问题。虽然VMS常常依赖硬件来建立各种抽象和隔离机制,但硬件设计师常常没法把硬件设计得比较完美。在接下来的学习中,我们可以看到一些VMS如何改进硬件缺陷的例子。VAX-11支持32-bit的地址空间,每页512 B。因此VPN(虚拟页号)有23位,剩下9位是偏移。VPN的高两位用于刻画一个页位于哪个段。因此VAX属于前面介绍过的把分页机制和分段机制混合运用的系统。进程空间(process space)占地址空间的一半。用户程序存在于进程空间,堆向上生长。两者都在进程空间的前一半(P0)地址。在进程空间的后一半(P1)地址中,则是栈空间,且栈向下生长。被保护的操作系统代码和数据都放在地址空间的后半部分(高地址),为所有进程共享。
VAX的页大小是历史遗留问题。因此VMS中每个进程的页表可能很大。VMS的设计师为此很头疼,他们要保证页表不会占用全部内存空间。所以进程空间被分成两段P0和P1,每段一个页表。这样栈和堆之间的空白部分就不用被记在页表中。VMS已经有基址寄存器和界限寄存器了。用户部分的页表也位于内核的虚拟地址,且VMS已经具有页交换机制,页表也可以被写入交换区。VMS进行地址翻译时,需要先获取页表的物理地址,然后再到页表中查询相应的虚拟地址的物理地址。虽然这个过程看起来很慢,但是在硬件管理TLB的加速下,常常能避免这种复杂的查询带来的速率降低。这是VMS的地址空间的简化模型。首先0地址被设定为不可访问,用于为空指针访问的检测提供支持。且每个进程空间的高半部分都是内核的虚拟地址空间。上下文切换时,只改变P0和P1寄存器,不会改变系统段(S段)的基址寄存器和界限寄存器。
把内核映射到全体进程的地址空间的理由有多个。首先,这样做使得内核更容易工作。例如:当OS获得一个来自用户程序的指针(如,进行write()系统调用时),很容易就可以从那个指针开始复制数据到OS自己的数据结构中。在编写、编译OS时可以更加自然,因为不用考虑正准备访问的数据从哪里来。如果不这样做而是单独给内核划分地址空间,那么交换页面、在用户进程和内核之间交换数据之类的操作就相当困难。内核映射到地址空间的后半部分后,内核就显得像是所有进程都可以调用的一个库(虽然是受保护的)。这种处理方法也是被广泛使用的。VAX的页表含有保护位。在访问内存之前,会考察相应页面的保护位和CPU的运行模式。系统代码和数据显然具有更高的保护级别,当非法访问时,就会产生陷阱,一般导致进行非法访问的进程被结束。
VAX的页表项含有如下内容:有效位1位、保护位4位、修改位1位、给OS保留的5位,以及物理页帧号(PFN)。页表项里没有使用位(引用位),因此进行交换时不能直接得知一页是否被访问过,必须专门设法得到其访问情况。
1980年代早期,O. Babaoglu和W. N. Joy发现,可以通过保护位来模拟引用位。如果一页经常被访问,那么可以将页表中的所有页都标记为不可访问,然后用额外的信息记录哪些页其实是可以被进程访问的(为操作系统保留的那些位可能有这个作用)。当进程访问一页时,就会触发陷阱,操作系统检查这一页是否可以访问。如果是,就撤销其不可访问标记,恢复原来的权限(例如,只读或读写)。在需要页面交换时,操作系统可以检查出哪些页依然是不可访问的,于是就可以判断哪些页面在最近并没有被使用。注意,将所有页面统一标记为不可访问的操作不能太频繁,否则带来的开销会过高。如果这样的操作进行得过少,自然就很容易得出几乎所有的页面都在最近有被使用的误判。许多交换页面的策略无法应对进程大量占用内存的情况。例如,LRU不能确保进程之间的内存分配相对公平。VMS使用分段FIFO(segmented FIFO)算法来进行页交换。每个进程在内存中保留的页数,记为保留集大小(resident set size,RSS),会受到限制(剩下的页在交换区或者根本没被加载)。当进程在内存中的页数达到RSS后,就把最先进入内存的页移走。这个算法不用硬件支持,易于实现。
纯的FIFO算法表现不太好。所以VMS引入了二次机会列表(second-chance lists),将准备移到交换区的页面暂时保留在此内存区域。如果被移到列表中的页没被修改过,就移到clean-page列表的末端;否则,移到dirty-page列表的末端。
当一个进程Q需要新的页面时,就从clean-page list中直接获取第一页。如果在Q获取该页之前,该页所属的进程P先尝试访问了,P就把这一页回收(从clean-page list去除)。这就避免了P要从磁盘上把这一页重新读入内存的情况。如果Q先获取了该页,而后P又需要该页的内容,再从硬盘上将该页读入内存(该页在内存中已经为Q所有,内容可能已被覆盖)。当这种列表比较大时,分段FIFO的表现会接近LRU。
VMS的页面大小只有512 Bytes。因此,如果直接与磁盘的交换区进行页交换,效率是非常低的。将页面写入交换区时,VMS尽量写入一组已修改页面(这些页面的修改位要置零)。这种思想早已普及到现代操作系统中。VMS有两个优化技巧,现在仍为许多系统所使用。这些优化我们称为懒优化(lazy optimizations)。
第一个技巧是按需清零(demand zeroing),即访问时清零。当没有这个优化时,假设你要为堆扩充一页,那么OS从物理内存中找到未使用的页,将其清零(防止正在运行的程序获得原来使用此页的程序残存的数据),然后映射到你的程序的地址空间(在页表中写入相应的物理页号)。那么,如果新申请的这一页暂时未被使用,这个过程就显得比较浪费。
这个优化的实现方法是:不要清零,而是直接将新的页写入页表,但标记其为不可访问。如果尝试访问,就产生陷阱,找到对应的物理页后将其清零后再标记为可访问。这通常借助页表中为操作系统保留的字段来实现。
第二个技巧是写入时复制(copy-on-write,COW)。它的历史可以追溯到TENEX系统。其思想很简单:当OS需要把一页从一个地址空间复制到另一个时,不要复制,而是直接映射到另一个地址空间,但要在两个进程的页表中标记该页为只读。如果有进程尝试写入这一页,就产生陷阱,当操作系统注意到该页面实际上是COW页面时,就分配新的页并执行真正的内容复制(当然要把相关权限的标记也修改)。如果进程对这一页只读不写,节省的用时和内存空间就很可观了。Linux也使用了这个技术。
在UNIX系统中,使用fork()和exec()时,也会用到这个机制。而且使用exec()时,由于先调用fork()创建了当前进程的副本,但是子进程的代码段会被用户要求运行的新程序的代码段覆盖,所以如果不采用COW机制,fork()就复制了一大堆完全不会用到的代码。COW的引入避免了大量不必要的复制操作。
在其它方面,这种思想也十分有用:例如,复制文件的时候,系统可能很快会提示复制完毕,但实际上文件被暂存到(系统的和 / 或磁盘的)写入缓存中,后续才会完全写入到磁盘。写入文件的操作也可以被适度延缓:万一用户马上要把这个文件删除呢?如果用户修改这个文件后不久就将其删除,那么就可以放弃暂缓的写入操作了。对Linux的虚拟内存机制,我们不会把每个方面都细讲,而是只讲最重要的部分。下面的讲解基于运行在x86架构上的Linux系统,因为从桌面到服务器几乎都使用x86架构的CPU运行Linux。这是Linux地址空间的简化模型。用户部分主要包括代码、栈和堆。内核部分包括内核代码、内核栈、内核堆等。同样,上下文切换时,只有与地址空间的非内核部分相关的寄存器才会被一同切换。用户模式下不允许访问内核空间。
32-bit Linux中,虚拟地址是32位的,内核部分从0xC0000000开始,占地址空间的1 / 4。64-bit Linux则有些不同。
内核的虚拟地址有两种。一种是内核逻辑地址(kernel logical address)。要申请新的这部分空间,需要调用kmalloc。这部分包含许多内核的数据结构,包括页表、每个进程的内核栈等。这部分空间也不允许交换到硬盘上。
事实上,地址空间的内核部分经常被映射到物理内存的低地址。可能是直接映射的,例如0xC0000000映射到物理内存的0x0,0xC0000FFF映射到物理内存的0xFFF。这样直接映射有两个好处:一是地址翻译更简单,二是如果在这里构造了一段连续的内存空间,那么其在物理内存中也连续,使得总体性能更好,并且有些操作是要求在连续的内存中进行的,例如通过DMA(见第11章)进行的I / O操作。
另一种地址是内核虚拟地址(kernel virtual address)。如果要申请新的这部分空间,需要调用vmalloc。不同于内核逻辑内存,内核虚拟内存一般是不连续的。不过,不要求连续,意味着分配这部分空间比较容易。只是,如果想在这里面的数据在物理内存上连续分布,就比较困难了。
32-bit Linux中,引入内核虚拟地址的一个目的是:让内核可以访问大于约1 GB的内存空间。当然在64-bit的Linux中,已经没有这个限制了。前面我们提到,页表是需要硬软配合的。x86提供了硬件管理的多级页表,因此运行在x86上的操作系统只需要在内存层面启用映射,并将一个特权寄存器指向页目录的开头,剩下的就可以交由硬件处理。OS负责进程的创建与删除、上下文切换,保证在任何情况下MMU都能访问到页表。
32位系统不能获得64位处理器带来的性能提升。如果你还在使用32位的系统,应该尽快升级至64位。
64-bit下,地址空间最多也可以达到64位。但是目前x86只启用了48位:由图可见,页表分为4级,必须一级一级翻译,才能找到相应的页。每一页的大小是4 KB。日后,当内存继续增大,就会启用5级页表。当然,x86不止支持4 KB的页大小,还支持2 MB甚至1 GB的页大小。Linux也允许应用程序使用很大的页大小。
如果页大小非常大,页表的占用空间就可以降低。但是,这并不是大页面的主要推进原因。如果一个程序占用的内存非常多,那么TLB很快就会用尽。这时只有很小一部分页面能够在TLB中直接获得翻译结果。当大量页面(例如,总大小达到GB级)的访问都需要查询页表时,性能开销就不能忽略了。研究表明,一些应用程序的运行时间中,解决TLB缺失的过程占用达到10%。如果增加页面大小,那么就有更大比例的内存块的翻译结果可以写入TLB。而且TLB缺失发生时,查找页表也更快(页表中的条目大幅减少了)。而且在特定场景下,分配内存的速率也会变得很快。向操作系统引入新特性时,要充分考虑其优缺点,不要盲目改进,而应当缓慢过渡。
起初,Linux开发者发现大页面只对少数应用有效,例如对性能需求异常高的大型数据库。所以,分配大页面的权利交给了应用程序,由应用程序在需要时通过mmap()或shmget()来申请。
后来,因为许多程序都需要TLB具有更佳的表现,Linux开发者添加了透明的大页面机制。当该特性启用时,OS寻找机会分配大的页面(通常为2 MB,但有些系统上分配1 GB)给应用程序,而无需程序手工请求。
但是大页面的代价也是有的。最大的影响就是较多的内部碎片:一页很大,但可能很大一部分没有被应用程序使用。交换机制无法解决这类问题,还会徒增非常多的I / O。不过有一点可以确定:4 KB的页大小虽然已经在OS中应用多年,但不一定总是最好的方案。事实上,如果Linux开始慢慢采纳基于硬件的新技术,那么这通常预示着一个重大改变即将来临。许多系统包含主动式的缓存子系统(caching subsystem),用于减少对存储系统的访问。Linux的页缓存(page cache)统一把内存页按三个不同的来源维护:映射到内存的文件(一段虚拟内存,是文件或类似文件的资源的一部分。这个资源通常是磁盘上的文件,但也可以是设备或共享内存的对象,也可以是其它可以由OS通过文件描述符引用的资源。),文件数据和设备的元数据(metadata)(一般用read()和write()访问),以及每个进程的栈和堆的页面(有时称作匿名内存(anonymous memory),因为这里面没有任何已命名的文件)。这些实体用哈希表索引,允许快速查询需要的数据。
页缓存追踪每一项是否已修改。已修改的数据通过后台进程(pdflush)周期性回写到磁盘(写入到文件或交换区),且在已修改的页数累积到一定数量或经过一定时间之后才会写入(可以修改相关参数来调整)。Linux采用2Q替换算法来进行页交换。
虽然LRU算法很高效,但是在一些常见的情况里,其表现也不佳。如果一个进程重复访问一个大文件(文件大小接近于可用内存空间或更大),那么如果采用LRU算法,就会踢掉内存中的其余全部文件。更坏的情况是:这个新载入的文件有很大一部分在被踢出内存之前从来不会被再次访问。
2Q替换法维护两个列表,并在它们之间划分内存。当第一次访问时,页被放入非活动列表(inactive list)。当这一页再次被访问时,就放到活动列表(active list)。当需要进行交换时,先从非活动列表交换。活动列表中的一些页会被周期性移动至非活动列表,并保持页面缓存中活动列表占用约2 / 3。
理想状态下,这两个列表用LRU算法维护。但是用LRU算法获得最优解的花费不小,所以Linux采用LRU的近似算法。很明显,对于循环访问一个大文件的情况,2Q替换带来的性能提升不容小视。现代的支持虚拟内存机制的系统(Linux,BSD等)与早期的VAX / VMS等的最大不同,很可能是对安全的格外重视。
当今系统面临的最大威胁之一,就是缓冲区溢出攻击(buffer overflow attacks)。这可以用来攻击用户程序和内核。其思想是:利用系统漏洞来向目标地址空间注入任意数据。这个漏洞的常见起因是:开发者想当然地认为输入一定不会溢出,于是信任任何输入和复制操作。当输入超过缓冲区的长度时,超出的部分就会写入到缓冲区以外的区域去。例如strcpy函数就是引发缓冲区溢出攻击的一个常见函数。
许多情况下,缓冲区溢出的后果不是灾难性的,顶多只是导致一个程序或系统崩溃,无其它影响。但是恶意程序可以构造特定的输入,使得溢出部分为可以被解析为代码并被执行。例如,构造一段远大于栈空间限制的数据,末端有一部分是恶意代码。如果OS不在栈溢出时结束进程,恶意代码随着栈的生长而进入代码段,就可以被计算机执行。当然也有其它的实现方式。可见,一旦攻击成功,理论上就可以执行任意代码。如果被攻击的程序是联网的,那么还可能波及网络上的其它计算机。如果针对操作系统进行攻击,还可能获得执行特权指令的权限。
一个最简单的预防办法是:不要把代码区以外的空间的任何数据识别为代码。AMD、Intel、ARM处理器中,分别用NX(Non-execute) bit、XD(execute disable) bit和XN(execute never) bit来标识。如果一个页的执行禁用位被设为1,那么任何试图将该页的内容识别为代码(机器语言)并执行的操作都会被拒绝。另一种攻击方式是返回导向编程(return-oriented programming,ROP)。在汇编语言课上,我们学习过调用子程序的过程。返回地址会在执行子程序前先被压入栈中,待子程序执行完毕后返回时,返回地址就会被从栈中弹出来,将指向下一条指令的地址的寄存器内容修改为原先压入栈中的返回地址。正常情况下,返回地址应该是调用子程序的指令的下一条指令的地址。但是黑客如果设法修改了返回地址为自己编写的恶意代码的开头,那么在函数执行完毕并返回时,就会开始执行指定的恶意代码。
为了防止ROP攻击,Linux和Windows均引入了地址空间布局随机化(address space layout randomization,ASLR),使得代码、栈、堆等各个区域不但在物理内存中的位置无法得知,在地址空间中的位置也不是固定的。

如果你编译并执行如下的C / C++程序:
int main(int argc, char* argv[]) {
int stack = 0; printf("%p\n", &stack);
return 0;
}
在非ASLR的系统上,每次显示的值都是一样的;而在ASLR的系统上,显示的值一般都不同。

对于内核,也有内核地址空间布局随机化(kernel ASLR),防止与内核协作的用户程序恶意破坏内核或通过内核执行任意代码。2018年8月,世界的系统安全局势遭遇巨大振荡。Meltdown和Spectre两个硬件级漏洞先后被多个团队的科研人员或工程师曝光。这些事件引发了对硬件和操作系统提供的保护机制的深切拷问。
这两个致命漏洞与CPU用于提升性能的机制有关。预测执行(speculative execution)是高性能CPU的必备技术。CPU通过猜测接下来将要执行哪些指令并提前执行。如果预测正确,性能就因为提前执行了指令而得到提升;如果预测错误,就需要重试指令。
预测执行会在缓存、分支预测器等众多部件中留下痕迹。我们常常认为这些部件会被MMU保护好,但实际上确实可以通过这些短板进行攻击。
一个加强保护内核的方法是:从用户进程中尽可能移除内核的地址空间,并用专门的内核页表来管理内核的内存数据。这称为内核页表隔离(kernel page table isolation,KPTI)。这时候,如果从用户进程的代码切换至内核的代码并执行,为了使用正确的页表,额外的工作就要做了。虽然这样确实提高了安全性,但是损失了性能。因为切换页表的开销是很大的,而且也为编程带来了不方便。
不幸的是,KPTI不能解决曝光的全部安全问题。但是又不能关闭预测执行,因为这甚至可能导致在一些极端场景下,性能损失到只有原来的数千分之一。

【2021/7/19 更新】【梳理】简明操作系统原理 附录一 虚拟内存子系统案例选讲(VAX/VMS + Linux)(docx)相关推荐

  1. 【2021/7/19更新】【梳理】简明操作系统原理 附录五 RAID(docx)

    配套教材: Operating Systems: Three Easy Pieces Remzi H. Arpaci-Dusseau Andrea C. Arpaci-Dusseau Peter Re ...

  2. 【2021/7/19更新】【梳理】简明操作系统原理 第十二章 机械硬盘 磁盘I / O的调度(docx)

    配套教材: Operating Systems: Three Easy Pieces Remzi H. Arpaci-Dusseau Andrea C. Arpaci-Dusseau Peter Re ...

  3. 【2021-11-08 更新】【梳理】简明操作系统原理 第二十章 加密(docx)

    配套教材: Operating Systems: Three Easy Pieces Remzi H. Arpaci-Dusseau Andrea C. Arpaci-Dusseau Peter Re ...

  4. 【2021-07-31 更新】【梳理】简明操作系统原理 第十九章 身份认证和访问控制(docx)

    配套教材: Operating Systems: Three Easy Pieces Remzi H. Arpaci-Dusseau Andrea C. Arpaci-Dusseau Peter Re ...

  5. 【梳理】简明操作系统原理:银行家算法(内附文档高清截图)

    银行家算法 W. Dijkstra的银行家算法是一种避免死锁的算法.命名原因是该算法原本为银行设计,确保银行发放贷款时,不会发生不能满足全部客户的需求的情况. 1.银行家算法需要的数据结构 设1个CP ...

  6. 自动控制原理【2021/12/19更新】

    目录 写在前面 (一) 控制系统导论 (二) 系统数学模型 (1) 微分方程 (2) 传递函数 (3) 方框图 (4) 信号流图 (三) 反馈控制系统的特性与性能 (1) 误差信号 (2) 二阶系统性 ...

  7. 视频教程-2021考研专业课《计算机操作系统原理》精讲视频课程-操作系统

    2021考研专业课<计算机操作系统原理>精讲视频课程 河北师范大学软件学院优秀讲师,项目经理资质,担任操作系统原理.软件工程.项目管理等课程教学工作.参与十个以上百万级软件项目管理及系统设 ...

  8. [渝粤教育] 中国地质大学 操作系统原理(新) 复习题

    <操作系统原理>模拟题 选择题 1.UNIX操作系统是一个()操作系统. A.实时 B.单用户多任务 C.多道批处理 D.多用户多任务 2.Unix系统采用的文件目录结构是(). A.一级 ...

  9. 全国计算机四级——操作系统原理笔记

    学习建议:结合书和笔记把知识过一遍 -> 买题库刷试卷 -> 始终学不明白的题目去刷章节题目 -> 背新增试卷题目 关于本笔记:写者参加2022年5月的考试,参考<全国计算机等 ...

  10. STM32全链路开发实战教程专栏总目录(2022.10.19更新)

    文章目录 专栏说明 一.开发环境相关 二.STM32裸机开发 STM32CubeMX系列教程 玩转嵌入式屏幕显示 嵌入式开源小组件的使用 mbedtls开源安全库 DW1000 UWB芯片开发笔记 L ...

最新文章

  1. 解决uni-app ios唤起扫码操作,总是要刷新才可以唤起的问题
  2. Discuz!常用函数解析(续)
  3. 常用String方法
  4. 生信入门必须掌握的 30 个 Linux 命令
  5. 【Android自定义控件】支持多层嵌套RadioButton的RadioGroup
  6. Ubuntu命令行下安装、卸载、管理软件包的方法
  7. 拓展欧几里得模板/求逆元模板(java)
  8. linux mono apache2,如何利用Mono创建Apache+mono环境(2)
  9. OpenCV4每日一练day14:光流法跟踪移动物体
  10. Windows Server 2012 DHCP故障转移
  11. THAAD反导必将部署,各方已接受事实
  12. 管理感悟:正确认识自己的工作
  13. 当我谈缓存的时候,我谈些什么
  14. C#使用Aforge对uvc协议摄像头亮度属性的更改
  15. 黑苹果开启硬件加速(Clover)
  16. The <Router /> component appears to be a function component that returns报错解决方式
  17. 推荐一款开源的ICO制作神器——greenfish
  18. 《复联4》在中国首映的 阴谋
  19. 【怀旧】利用Altair 8800模拟器加载4K Basic解释器(附下载连接)
  20. 安全知识普及:如何让您的计算机上网安全,无忧冲浪

热门文章

  1. 计算机硬件工程师主要干什么,计算机硬件工程师主要学习什么内容
  2. Chrome浏览器的翻译插件开发
  3. PHP第一季视频教程.李炎恢.学习笔记(五)(第3章 操作符与控制结构(1)(2))
  4. 南京大学计算机学院英才计划,2020年“英才计划”工作实施方案
  5. 批量打印软件导入Excel时如何保留两位小数
  6. vulnhub-Odin
  7. 2020微博热点数据简析
  8. linux安装2870无线网卡,『求助』RaLink雷凌RT2870 无线网卡怎样安装驱动?
  9. 解决应用程序无法正常启动0xc0150002问题(转)
  10. 终极QQ-ZONE技巧