操作系统导论

雷姆兹·H.阿帕希杜塞尔 安德莉亚·C.阿帕希杜塞尔

前言

  • 抽象是计算机科学各个方面的基础,因此它在操作系统中也是必不可少的。
  • 叶芝有一句名言:“教育不是注满一桶水,而是点燃一把火。”
  • 教育的真正要点是让你对某些事情感兴趣,可以独立学习更多关于这个主题的东西,而不仅仅是你需要消化什么才能在某些课程上取得好成绩。
  • 教育过程的真正意义在于:前进,学习许多新的和引人入胜的主题,通过学习不断成熟,最重要的是,找到能为你点火的东西。[插图]

第1章 关于本书的对话

  • 虚拟化(virtualization)、并发(concurrency)和持久性(persistence)。
  • “不闻不若闻之,闻之不若见之,见之不若知之,知之不若行之。”
  • 有的时候将自己从叙述中抽离出来,然后进行一些思考会更有用。这些对话就是思考。我们将协作探究

第2章 操作系统介绍

  • 一个正在运行的程序会做一件非常简单的事情:执行指令。处理器从内存中获取(fetch)一条指令,对其进行解码(decode)(弄清楚这是哪条指令),然后执行(execute)它(做它应该做的事情,如两个数相加、访问内存、检查条件、跳转到函数等)。完成这条指令后,处理器继续执行下一条指令,依此类推,直到程序最终完成[插图]。
  • 操作系统通过哪些机制和策略来实现虚拟化?操作系统如何有效地实现虚拟化?需要哪些硬件支持?
  • 也就是说,操作系统将物理(physical)资源(如处理器、内存或磁盘)转换为更通用、更强大且更易于使用的虚拟形式。因此,我们有时将操作系统称为虚拟机(virtual machine)。
  • 将单个CPU(或其中一小部分)转换为看似无限数量的CPU,从而让许多程序看似同时运行,这就是所谓的虚拟化CPU(virtualizing the CPU),这是本书第一大部分的关注点。
  • 操作系统承担了资源管理器(resource manager)的角色。
  • 内存就是一个字节数组。要读取(read)内存,必须指定一个地址(address),才能访问存储在那里的数据。要写入(write)或更新(update)内存,还必须指定要写入给定地址的数据。
  • 每个进程访问自己的私有虚拟地址空间(virtual address space)(有时称为地址空间,address space),操作系统以某种方式映射到机器的物理内存上。一个正在运行的程序中的内存引用不会影响其他进程(或操作系统本身)的地址空间。对于正在运行的程序,它完全拥有自己的物理内存。
  • 你可以将线程看作与其他函数在同一内存空间中运行的函数,并且每次都有多个线程处于活动状态。
  • 如何构建正确的并发程序
  • 一条将计数器的值从内存加载到寄存器,一条将其递增,另一条将其保存回内存。因为这3条指令并不是以原子方式(atomically)执行(所有的指令一次性执行)的,所以奇怪的事情可能会发生。
  • 操作系统中管理磁盘的软件通常称为文件系统(file system)
  • 不像操作系统为CPU和内存提供的抽象,操作系统不会为每个应用程序创建专用的虚拟磁盘。相反,它假设用户经常需要共享(share)文件中的信息。
  • 持久性需要哪些技术才能正确地实现?需要哪些机制和策略才能高性能地实现?面对硬件和软件故障,可靠性如何实现?
  • 出于性能方面的原因,大多数文件系统首先会延迟这些写操作一段时间,希望将其批量分组为较大的组。
  • 它取得CPU、内存或磁盘等物理资源(resources),并对它们进行虚拟化(virtualize)。它处理与并发(concurrency)有关的麻烦且棘手的问题。它持久地(persistently)存储文件,从而使它们长期安全
  • 抽象使得编写一个大型程序成为可能,将其划分为小而且容易理解的部分,用C这样的高级语言编写这样的程序不用考虑汇编,用汇编写代码不用考虑逻辑门,用逻辑门来构建处理器不用太多考虑晶体管。
  • 设计和实现操作系统的一个目标,是提供高性能(performance)。
  • 保护是操作系统基本原理之一的核心,这就是隔离(isolation)。让进程彼此隔离是保护的关键,因此决定了OS必须执行的大部分任务。
  • 早期操作系统:只是一些库
  • 假设允许任何应用程序从磁盘上的任何地方读取。因为任何程序都可以读取任何文件,所以隐私的概念消失了。
  • 系统调用和过程调用之间的关键区别在于,系统调用将控制转移(跳转)到OS中,同时提高硬件特权级别(hardware privilege level)。

第4章 抽象:进程

  • 进程就是运行中的程序。程序本身是没有生命周期的,它只是存在磁盘上面的一些指令(也可能是一些静态数据)。
  • 关键问题:如何提供有许多CPU的假象?
  • 通过让一个进程只运行一个时间片,然后切换到其他进程,操作系统提供了存在多个虚拟CPU的假象。这就是时分共享(time sharing)CPU技术
  • 时分共享(time sharing)是操作系统共享资源所使用的最基本的技术之一。通过允许资源由一个实体使用一小段时间,然后由另一个实体使用一小段时间,如此下去,所谓的资源(例如,CPU或网络链接)可以被许多人共享。
  • 策略是在操作系统内做出某种决定的算法。
  • 操作系统为正在运行的程序提供的抽象,就是所谓的进程(process)
  • 进程的机器状态的另一部分是寄存器。
  • 机制看成为系统的“如何(how)”问题提供答案。例如,操作系统如何执行上下文切换?策略为“哪个(which)”问题提供答案。
  • 操作系统运行程序必须做的第一件事是将代码和所有静态数据(例如初始化变量)加载(load)到内存中,加载到进程的地址空间中。
  • 现代操作系统惰性(lazily)执行该过程,即仅在程序执行期间需要加载的代码或数据片段,才会加载。
  • 将代码和静态数据加载到内存后,操作系统在运行此进程之前还需要执行其他一些操作。必须为程序的运行时栈(run-time stack或stack)分配一些内存。
  • 在UNIX系统中,默认情况下每个进程都有3个打开的文件描述符(file descriptor),用于标准输入、输出和错误。
  • 通过将代码和静态数据加载到内存中,通过创建和初始化栈以及执行与I/O设置相关的其他工作,OS现在(终于)为程序执行搭好了舞台。然后它有最后一项任务:启动程序,在入口处运行,即main()
  • 当一个进程停止时,它的寄存器将被保存到这个内存位置。通过恢复这些寄存器(将它们的值放回实际的物理寄存器中),操作系统可以恢复运行该进程。
  • 一个进程可以处于已退出但尚未清理的最终(final)状态(在基于UNIX的系统中,这称为僵尸状态[插图])。
  • 有时候人们会将存储关于进程的信息的个体结构称为进程控制块(Process Control Block,PCB)

第5章 插叙:进程API

  • UNIX系统采用了一种非常有趣的创建新进程的方式,即通过一对系统调用:fork()和exec()。进程还可以通过第三个系统调用wait(),来等待其创建的子进程执行完成。
  • 父进程获得的返回值是新创建子进程的PID,而子进程获得的返回值是0。
  • CPU调度程序(scheduler)决定了某个时刻哪个进程被执行
  • 这个系统调用可以让子进程执行与父进程不同的程序。
  • exec()会从可执行程序中加载代码和静态数据,并用它覆写自己的代码段(以及静态数据),堆、栈及其他内存空间也会被重新初始化。然后操作系统就执行该程序,将参数通过argv传递给该进程
  • 这种分离fork()及exec()的做法在构建UNIX shell的时候非常有用,因为这给了shell在fork之后exec之前运行代码的机会,这些代码可以在运行新程序前改变环境,从而让一系列有趣的功能很容易实现。
  • shell实现结果重定向的方式也很简单,当完成子进程的创建后,shell在调用exec()之前先关闭了标准输出(standard output),打开了文件newfile.txt。这样,即将运行的程序wc的输出结果就被发送到该文件,而不是打印在屏幕上。

第6章 机制:受限直接执行

  • 基本思想很简单:运行一个进程一段时间,然后运行另一个进程,如此轮换。通过以这种方式时分共享(time sharing)CPU,就实现了虚拟化。
  • 操作系统必须以高性能的方式虚拟化CPU,同时保持对系统的控制
  • 受限的直接执行(limited directexecution)
  • 第一个问题很简单:如果我们只运行一个程序,操作系统怎么能确保程序不做任何我们不希望它做的事,同时仍然高效地运行它?第二个问题:当我们运行一个进程时,操作系统如何让它停下来并切换到另一个进程,从而实现虚拟化CPU所需的时分共享?
  • 如果对运行程序没有限制,操作系统将无法控制任何事情,因此会成为“仅仅是一个库”
  • 一个进程必须能够执行I/O和其他一些受限制的操作,但又不能让进程完全控制系统。操作系统和硬件如何协作实现这一点?
  • 硬件通过提供不同的执行模式来协助操作系统。在用户模式(user mode)下,应用程序不能完全访问硬件资源。在内核模式(kernel mode)下,操作系统可以访问机器的全部资源。还提供了陷入(trap)内核和从陷阱返回(return-from-trap)到用户模式程序的特别说明,以及一些指令,让操作系统告诉硬件陷阱表(trap table)在内存中的位置。
  • 要执行系统调用,程序必须执行特殊的陷阱(trap)指令。该指令同时跳入内核并将特权级别提升到内核模式。
  • 因此,C库中进行系统调用的部分是用汇编手工编码的,因为它们需要仔细遵循约定,以便正确处理参数和返回值,以及执行硬件特定的陷阱指令。
  • 陷阱如何知道在OS内运行哪些代码?
  • 内核通过在启动时设置陷阱表(trap table)来实现
  • 最后再插一句:能够执行指令来告诉硬件陷阱表的位置是一个非常强大的功能
  • 如果一个进程在CPU上运行,这就意味着操作系统没有运行。如果操作系统没有运行,它怎么能做事情?
  • 操作系统如何重新获得CPU的控制权(regain control),以便它可以在进程之间切换?
  • 像这样的系统通常包括一个显式的yield系统调用,它什么都不干,只是将控制权交给操作系统,以便系统可以运行其他进程。
  • 如果应用程序执行了某些非法操作,也会将控制转移给操作系统
  • 在协作调度系统中,OS通过等待系统调用,或某种非法操作发生,从而重新获得CPU的控制权。
  • 时钟设备可以编程为每隔几毫秒产生一次中断。产生中断时,当前正在运行的进程停止,操作系统中预先配置的中断处理程序(interrupt handler)会运行。此时,操作系统重新获得CPU的控制权
  • 首先,正如我们之前讨论过的系统调用一样,操作系统必须通知硬件哪些代码在发生时钟中断时运行。因此,在启动时,操作系统就是这样做的。其次,在启动过程中,操作系统也必须启动时钟,这当然是一项特权操作。一旦时钟开始运行,操作系统就感到安全了,因为控制权最终会归还给它,因此操作系统可以自由运行用户程序。
  • 硬件在发生中断时有一定的责任,尤其是在中断发生时,要为正在运行的程序保存足够的状态,以便随后从陷阱返回指令能够正确恢复正在运行的程序。
  • 上下文切换在概念上很简单:操作系统要做的就是为当前正在执行的进程保存一些寄存器的值(例如,到它的内核栈),并为即将执行的进程恢复一些寄存器的值(从它的内核栈)
  • 为了保存当前正在运行的进程的上下文,操作系统会执行一些底层汇编代码,来保存通用寄存器、程序计数器,以及当前正在运行的进程的内核栈指针,然后恢复寄存器、程序计数器,并切换内核栈,供即将运行的进程使用。
  • 就让你想运行的程序在CPU上运行,但首先确保设置好硬件,以便在没有操作系统帮助的情况下限制进程可以执行的操作。

第7章 进程调度:介绍

  • 1.每一个工作运行相同的时间。 2.所有的工作同时到达。 3.一旦开始,每个工作保持运行直到完成。 4.所有的工作只是用CPU(即它们不执行IO操作)。 5.每个工作的运行时间是已知的。
  • 性能和公平在调度系统中往往是矛盾的
  • 先进先出(First In First Out或FIFO)调度
  • 这个问题通常被称为护航效应(convoy effect)[B+79],一些耗时较少的潜在资源消费者被排在重量级的资源消费者之后。
  • 最短任务优先(Shortest Job First,SJF)
  • 事实上,考虑到所有工作同时到达的假设,我们可以证明SJF确实是一个最优(optimal)调度算法
  • 几乎所有现代化的调度程序都是抢占式的(preemptive),非常愿意停止一个进程以运行另一个进程。
  • 最短完成时间优先(STCF)
  • 向SJF添加抢占,称为最短完成时间优先(Shortest Time-to-Completion First,STCF)或抢占式最短作业优先(Preemptive Shortest Job First ,PSJF)调度程序[CK68]。
  • 响应时间定义为从任务到达系统到首次运行的时间
  • 如何构建对响应时间敏感的调度程序?
  • 时间片长度必须是时钟中断周期的倍数
  • 我们开发了两种调度程序。第一种类型(SJF、STCF)优化周转时间,但对响应时间不利。第二种类型(RR)优化响应时间,但对周转时间不利。
  • 重叠(overlap)操作可以最大限度地提高系统的利用率
  • 第一类是运行最短的工作,从而优化周转时间。第二类是交替运行所有工作,从而优化响应时间。但很难做到“鱼与熊掌兼得”,这是系统中常见的、固有的折中。

第8章 调度:多级反馈队列

  • 多级反馈队列(Multi-level Feedback Queue,MLFQ)
  • 多级反馈队列是用历史经验预测未来的一个典型的例子,操作系统中有很多地方采用了这种技术(同样存在于计算机科学领域的很多其他地方,比如硬件的分支预测及缓存算法)
  • MLFQ中有许多独立的队列(queue),每个队列有不同的优先级(priority level)。任何时刻,一个工作只能存在于一个队列中。MLFQ总是优先执行较高优先级的工作(即在较高级队列中的工作)。
  • ·规则1:如果A的优先级 > B的优先级,运行A(不运行B)。·规则2:如果A的优先级 = B的优先级,轮转运行A和B。
  • ·规则3:工作进入系统时,放在最高优先级(最上层队列)。·规则4a:工作用完整个时间片后,降低其优先级(移入下一个队列)。·规则4b:如果工作在其时间片以内主动释放CPU,则优先级不变。
  • 如果不知道工作是短工作还是长工作,那么就在开始的时候假设其是短工作,并赋予最高优先级。如果确实是短工作,则很快会执行完毕,否则将被慢慢移入低优先级队列,而这时该工作也被认为是长工作了。通过这种方式,MLFQ近似于SJF。
  • 一个计算密集的进程可能在某段时间表现为一个交互型的进程。
  • 规则4:一旦工作用完了其在某一层中的时间配额(无论中间主动放弃了多少次CPU),就降低其优先级(移入低一级队列)。
  • 大多数的MLFQ变体都支持不同队列可变的时间片长度
  • ·规则1:如果A的优先级 > B的优先级,运行A(不运行B)。 ·规则2:如果A的优先级 = B的优先级,轮转运行A和B。 ·规则3:工作进入系统时,放在最高优先级(最上层队列)。 ·规则 4:一旦工作用完了其在某一层中的时间配额(无论中间主动放弃了多少次CPU),就降低其优先级(移入低一级队列)。 ·规则5:经过一段时间S,就将系统中所有工作重新加入最高优先级队列。

第9章 调度:比例份额

  • 比例份额算法基于一个简单的想法:调度程序的最终目标,是确保每个工作获得一定比例的CPU时间,而不是优化周转时间和响应时间。
  • 彩票调度最精彩的地方在于利用了随机性(randomness)。当你需要做出决定时,采用随机的方式常常是既可靠又简单的选择。
  • 将列表项按照彩票数递减排序。这个顺序并不会影响算法的正确性,但能保证用最小的迭代次数找到需要的节点,尤其当大多数彩票被少数进程掌握时。
  • 步长调度(stride scheduling),一个确定性的公平分配算法
  • 彩票调度有一个步长调度没有的优势——不需要全局状态

第10章 多处理器调度(高级)

  • 区别的核心在于对硬件缓存(cache)的使用(见图10.1),以及多处理器之间共享数据的方式。
  • 缓存一致性(cache coherence)问题
  • 因此多处理器调度应该考虑到这种缓存亲和性,并尽可能将进程保持在同一个CPU上。
  • 由于每个CPU都简单地从全局共享的队列中选取下一个工作执行,因此每个工作都不断在不同CPU之间转移,这与缓存亲和的目标背道而驰。
  • 一个基本的方法是采用一种技术,名为工作窃取(work stealing)[FLR98]。通过这种方法,工作量较少的(源)队列不定期地“偷看”其他(目标)队列是不是比自己的工作多。如果目标队列比源队列(显著地)更满,就从目标队列“窃取”一个或多个工作,实现负载均衡。
  • 构建一个通用的调度程序仍是一项令人生畏的任务,因为即使很小的代码变动,也有可能导致巨大的行为差异。除非很清楚自己在做什么,或者有人付你很多钱,否则别干这种事。

第12章 关于内存虚拟化的对话

  • 隔离(isolation)和保护(protection)也是大事

第13章 抽象:地址空间

  • 因此操作系统需要提供一个易用(easy to use)的物理内存抽象。这个抽象叫作地址空间(address space),是运行的程序看到的系统中的内存
  • 一个进程的地址空间包含运行的程序的所有内存状态。
  • 操作系统如何在单一的物理内存上为多个运行的进程(所有进程共享内存)构建一个私有的、可能很大的地址空间的抽象?
  • 虚拟内存(VM)系统的一个主要目标是透明(transparency)[插图]。

第14章 插叙:内存操作API

  • 第一种称为栈内存,它的申请和释放操作是编译器来隐式管理的,所以有时也称为自动(automatic)内存。
  • sizeof() 被正确地认为是一个操作符,而不是一个函数调用(函数调用在运行时发生)。
  • 分配区域的大小不会被用户传入,必须由内存分配库本身记录追踪。
  • 有时候程序会在用完之前释放内存,这种错误称为悬挂指针(dangling pointer)
  • 第一级是由操作系统执行的内存管理,操作系统在进程运行时将内存交给进程,并在进程退出(或以其他方式结束)时将其回收。第二级管理在每个进程中,例如在调用malloc()和free()时,在堆内管理。即使你没有调用free()(并因此泄露了堆中的内存),操作系统也会在程序结束运行时,收回进程的所有内存(包括用于代码、栈,以及相关堆的内存页)。无论地址空间中堆的状态如何,操作系统都会在进程终止时收回所有这些页面,从而确保即使没有释放内存,也不会丢失内存。
  • realloc()创建一个新的更大的内存区域,将旧区域复制到其中,并返回新区域的指针。

第15章 机制:地址转换

  • 让程序运行的大部分指令直接访问硬件,只在一些关键点(如进程发起系统调用或发生时钟中断)由操作系统介入来确保“在正确时间,正确的地点,做正确的事”。
  • 关键问题:如何高效、灵活地虚拟化内存
  • 硬件对每次内存访问进行处理(即指令获取、数据读取或写入),将指令中的虚拟(virtual)地址转换为数据实际存储的物理(physical)地址
  • 每个CPU需要两个硬件寄存器:基址(base)寄存器和界限(bound)寄存器,有时称为限制(limit)寄存器。
  • 进程中使用的内存引用都是虚拟地址(virtual address),硬件接下来将虚拟地址加上基址寄存器中的内容,得到物理地址(physical address),再发给内存系统。
  • 由于这种重定位是在运行时发生的,而且我们甚至可以在进程开始运行后改变其地址空间,这种技术一般被称为动态重定位
  • 一个基址寄存器将虚拟地址转换为物理地址,一个界限寄存器确保这个地址在进程地址空间的范围内。它们一起提供了既简单又高效的虚拟内存机制。
  • 硬件还必须提供基址和界限寄存器(base and bounds register),因此每个CPU的内存管理单元(Memory Management Unit,MMU)都需要这两个额外的寄存器。
  • 硬件应该提供一些特殊的指令,用于修改基址寄存器和界限寄存器,允许操作系统在切换进程时改变它们。这些指令是特权(privileged)指令,只有在内核模式下,才能修改这些寄存器。
  • 第一,在进程创建时,操作系统必须采取行动,为进程的地址空间找到内存空间。
  • 第二,在进程终止时(正常退出,或因行为不端被强制终止),操作系统也必须做一些工作,回收它的所有内存,给其他进程或者操作系统使用。
  • 在切换进程时,操作系统必须保存和恢复基础和界限寄存器
  • 地址转换过程完全由硬件处理,没有操作系统的介入

第16章 分段

  • 栈和堆之间,有一大块“空闲”空间。
  • 段错误指的是在支持分段的机器上发生了非法的内存访问。
  • 一般会遇到的问题是,物理内存很快充满了许多空闲空间的小洞,因而很难分配给新的段,或扩大已有的段。这种问题被称为外部碎片(external fragmentation)[R69],如图16.3(左边)所示。
  • 一种更简单的做法是利用空闲列表管理算法,试图保留大的内存块用于分配。
  • 唯一真正的解决办法就是(我们会在后续章节看到),完全避免这个问题,永远不要分配不同大小的内存块。
  • 分段还有一个附加的好处:代码共享。如果代码放在独立的段中,这样的段就可能被多个运行的程序共享。

第17章 空闲空间管理

  • 如果分配程序给出的内存块超出请求的大小,在这种块中超出请求的空间(因此而未使用)就被认为是内部碎片(因为浪费发生在已分配单元的内部),这是另一种形式的空间浪费。
  • 如果请求的空间大小小于某块空闲块,分配程序通常会进行分割。
  • 分配程序会在释放一块内存时合并可用空间
  • 如果用户请求N字节的内存,库不是寻找大小为N的空闲块,而是寻找N加上头块大小的空闲块。
  • 最差匹配(worst fit)方法与最优匹配相反,它尝试找最大的空闲块,分割并满足用户需求后,将剩余的块(很大)加入空闲列表。
  • 就会发现每对互为伙伴的块只有一位不同,正是这一位决定了它们在整个伙伴树中的层次

第18章 分页:介绍

  • 分页不是将一个进程的地址空间分割成几个不同长度的逻辑段(即代码、堆、段),而是分割成固定大小的单元,每个单元称为一页。相应地,我们把物理内存看成是定长槽块的阵列,叫作页帧(page frame)。每个这样的页帧包含一个虚拟内存页。
  • 为了记录地址空间的每个虚拟页放在物理内存中的位置,操作系统通常为每个进程保存一个数据结构,称为页表(page table)。页表的主要作用是为地址空间的每个虚拟页面保存地址转换(address translation),从而让我们知道每个页在物理内存中的位置。
  • 这个页表是一个每进程的数据结构
  • 虚拟页面号(virtual page number,VPN)和页内的偏移量(offset)
  • 页表就是一种数据结构,用于将虚拟地址(或者实际上,是虚拟页号)映射到物理地址(物理帧号)。
  • 脏位(dirty bit)也很常见,表明页面被带入内存后是否被修改过。
  • 当它运行时,每个获取指将产生两个内存引用:一个访问页表以查找指令所在的物理框架,另一个访问指令本身将其提取到CPU进行处理

第19章 分页:快速地址转换(TLB)

  • 典型页的大小一般为4KB,这种情况下,密集的、基于数组的访问会实现极好的TLB性能,每页的访问只会遇到一次未命中。
  • 类似其他缓存,TLB的成功依赖于空间和时间局部性。如果某个程序表现出这样的局部性(许多程序是这样),TLB的命中率可能很高。
  • 典型的TLB有32项、64项或128项,并且是全相联的(fully associative)
  • TLB的有效位不同,只是指出TLB项是不是有效的地址映射。
  • 通过将所有TLB项设置为无效,系统可以确保将要运行的进程不会错误地使用前一个进程的虚拟到物理地址转换映射。

第20章 分页:较小的表

  • 我们的杂合方法不是为进程的整个地址空间提供单个页表,而是为每个逻辑分段提供一个
  • 首先,也许最明显的是,多级页表分配的页表空间,与你正在使用的地址空间内存量成比例。因此它通常很紧凑,并且支持稀疏的地址空间。

第21章 超越物理内存:机制

  • 操作系统如何利用大而慢的设备,透明地提供巨大虚拟地址空间的假象?
  • 访问不在物理内存中的页,这种行为通常被称为页错误(page fault)
  • 选择哪些页被交换出或被替换(replace)的过程,被称为页交换策略(page-replacement policy)。

第22章 超越物理内存:策略

  • 即替换内存中在最远将来才会被访问到的页,可以达到缓存未命中率最低。

第23章 VAX/VMS虚拟内存系统

  • 写时复制(copy-on-write,COW)

第2部分 并发

  • 操作系统必须用锁(lock)和条件变量(condition variable)这样的原语,来支持多线程应用程序

第26章 并发:介绍

  • 每个线程类似于独立的进程,只有一点区别:它们共享地址空间,从而能够访问相同的数据
  • 临界区是访问共享变量(或更一般地说,共享资源)的代码片段,一定不能由多个线程同时执行。

第27章 插叙:线程API

  • 永远不要返回一个指针,并让它指向线程调用栈上分配的东西
  • 当线程之间必须发生某种信号时,如果一个线程在等待另一个线程继续执行某些操作,条件变量就很有用

第28章 锁

  • 第一是锁是否能完成它的基本任务,即提供互斥(mutual exclusion)
  • 最早提供的互斥解决方案之一,就是在临界区关闭中断。
  • 第二,这种方案不支持多处理器
  • 最简单的硬件支持是测试并设置指令(test-and-set instruction),也叫作原子交换(atomic exchange)
  • 自旋锁不提供任何公平性保证。实际上,自旋的线程在竞争条件下可能会永远自旋。自旋锁没有公平性,可能会导致饿死。
  • 虽然比原来的浪费99个时间片的自旋方案要好,但这种方法仍然成本很高,上下文切换的成本是实实在在的,因此浪费很大。

第30章 条件变量

  • 条件变量是一个显式队列,当某些执行状态(即条件,condition)不满足时,线程可以把自己加入队列,等待(waiting)该条件
  • 调用signal和wait时要持有锁(hold the lock when calling signal orwait),你会保持身心健康的。

第31章 信号量

  • 当信号量的值为负数时,这个值就是等待线程的个数

第32章 常见并发问题

  • 违反原子性(atomicity violation)缺陷和错误顺序(order violation)缺陷。
  • 当线程之间的顺序很重要时,条件变量(或信号量)能够解决问题。
  • ·互斥:线程对于需要的资源进行互斥的访问(例如一个线程抢到锁)。 ·持有并等待:线程持有了资源(例如已将持有的锁),同时又在等待其他资源(例如,需要获得的锁)。 ·非抢占:线程获得的资源(例如锁),不能被抢占。 ·循环等待:线程之间存在一个环路,环路上每个线程都额外持有一个资源,而这个资源又是下一个线程要申请的。

第36章 I/O设备

  • 另一个最好不要使用中断的场景是网络。网络端收到大量数据包,如果每一个包都发生一次中断,那么有可能导致操作系统发生活锁(livelock),即不断处理中断而无法处理用户层的请求。
  • DMA引擎是系统中的一个特殊设备,它可以协调完成内存和设备间的数据传递,不需要CPU介入。
  • 操作系统是唯一可以直接与设备交互的实体
  • 第二种方法是内存映射I/O(memory- mapped I/O)。通过这种方式,硬件将设备寄存器作为内存地址提供。当需要访问设备寄存器时,操作系统装载(读取)或者存入(写入)到该内存地址;然后硬件会将装载/存入转移到设备上,而不是物理内存。
  • 如何实现一个设备无关的操作系统
  • 在最底层,操作系统的一部分软件清楚地知道设备如何工作,我们将这部分软件称为设备驱动程序(device driver),所有设备交互的细节都封装在其中。

第37章 磁盘驱动器

  • 寻道,以及旋转,是最昂贵的磁盘操作之一。
  • TI/O = T寻道 + T旋转 + T传输 (37.1)
  • 磁盘调度程序执行的另一个重要相关任务是I/O合并(I/O merging)

第38章 廉价冗余磁盘阵列(RAID)

  • 廉价冗余磁盘阵列(Redundant Array of Inexpensive Disks)

第39章 插叙:文件和目录

  • strace的作用就是跟踪程序在运行时所做的每个系统调用,然后将跟踪结果显示在屏幕上供你查看。
  • 创建一个文件时,实际上做了两件事。首先,要构建一个结构(inode),它将跟踪几乎所有关于文件的信息,包括其大小、文件块在磁盘上的位置等等。其次,将人类可读的名称链接到该文件,并将该链接放入目录中。
  • mount的作用很简单:以现有目录作为目标挂载点(mount point),本质上是将新的文件系统粘贴到目录树的这个点上。

第40章 文件系统实现

  • 第一个方面是文件系统的数据结构(data structure)
  • 文件系统的第二个方面是访问方法(access method)

第42章 崩溃一致性:FSCK和日志

  • 基本思路如下。更新磁盘时,在覆写结构之前,首先写下一点小注记(在磁盘上的其他地方,在一个众所周知的位置),描述你将要做的事情。写下这个注记就是“预写”部分,我们把它写入一个结构,并组织成“日志”。因此,就有了预写日志。