操作系统导论(读书笔记)

前言


用Kindle阅读操作系统导论读书时所做的笔记导出

操作系统导论

Author:Demon


标注(黄色) - 位置 406

教育的真正要点是让你对某些事情感兴趣,可以独立学习更多关于这个主题的东西,而不仅仅是你需要消化什么才能在某些课程上取得好成绩。

标注(黄色) - 位置 494

“不闻不若闻之,闻之不若见之,见之不若知之,知之不若行之。”

标注(黄色) - 位置 521

这些软件称为操作系统( Operating System, OS)[ 3],因为它们负责确保系统既易于使用又正确高效地运行。

标注(黄色) - 位置 530

要做到这一点,操作系统主要利用一种通用的技术,我们称之为虚拟化( virtualization)。也就是说,操作系统将物理( physical)资源(如处理器、内存或磁盘)转换为更通用、更强大且更易于使用的虚拟形式。

标注(黄色) - 位置 532

为了让用户可以告诉操作系统做什么,从而利用虚拟机的功能(如运行程序、分配内存或访问文件),操作系统还提供了一些接口( API),供你调用。实际上,典型的操作系统会提供几百个系统调用( system call),让应用程序调用。

标注(黄色) - 位置 608

每个进程访问自己的私有虚拟地址空间( virtual address space)(有时称为地址空间, address space),操作系统以某种方式映射到机器的物理内存上。一个正在运行的程序中的内存引用不会影响其他进程(或操作系统本身)的地址空间。对于正在运行的程序,它完全拥有自己的物理内存。

笔记 - 位置 610

虚拟地址空间是操作系统和硬件共同提供的一种机制,它有以下几个重要用途: 1. 内存隔离:虚拟地址空间使每个运行的程序都拥有自己独立的地址空间,因此它们不会相互干扰。这提供了内存隔离,防止一个程序的错误影响其他程序。 2. 内存管理:操作系统可以更有效地管理物理内存,将虚拟地址映射到物理内存中。这允许多个程序共享物理内存,并在需要时将数据从磁盘加载到内存中。 3. 安全性:虚拟地址空间可以实现内存保护,防止程序读取或修改其它程序的内存,从而提高系统的安全性。 4. 虚拟内存:虚拟地址空间使得操作系统能够将不常用的数据移到磁盘上,从而提高内存利用率。这被称为虚拟内存,允许程序处理比物理内存更大的数据集。 总之,虚拟地址空间在现代操作系统中发挥着关键的作用,提供了隔离、管理和安全性等多种好处。

标注(黄色) - 位置 661

操作系统中管理磁盘的软件通常称为文件系统( file system)。因此它负责以可靠和高效的方式,将用户创建的任何文件( file)存储在系统的磁盘上。

标注(黄色) - 位置 693

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

笔记 - 位置 693

写时复制(Copy-On-Write,简称COW)是一种内存管理技术,用于优化内存使用和提高性能。它的核心思想是在需要修改数据时才进行实际的复制操作,而在没有修改之前,多个引用共享相同的数据。 以下是写时复制的工作原理: 1. 初始阶段:多个引用指向相同的内存数据,它们共享相同的物理内存块。 2. 写操作:当某个引用需要修改数据时,系统会检测到这个修改操作,并执行以下步骤: - 复制:系统会为要修改的数据创建一个独立的拷贝,通常是在新的内存块中。 - 更新引用:修改引用,使其指向新的拷贝而不是原始数据。其他引用仍然指向原始数据。 这种方式有几个优点: - 节省内存:在大多数情况下,不需要立即复制数据,从而节省内存。 - 高效性:只有在实际写操作发生时才进行复制,因此避免了不必要的数据复制开销。 - 并发性:多个进程或线程可以同时访问相同的数据,只有在写操作时才需要复制,从而提高并发性能。 写时复制通常用于操作系统内存管理、虚拟内存、多进程/多线程环境以及编程语言中的数据结构,如字符串和数组,以提高效率和资源利用率。

标注(黄色) - 位置 698

现在你已经了解了操作系统实际上做了什么:它取得 CPU、内存或磁盘等物理资源( resources),并对它们进行虚拟化( virtualize)。它处理与并发( concurrency)有关的麻烦且棘手的问题。它持久地( persistently)存储文件,从而使它们长期安全。

标注(黄色) - 位置 701

一个最基本的目标,是建立一些抽象( abstraction),让系统方便和易于使用。抽象对我们在计算机科学中做的每件事都很有帮助。抽象使得编写一个大型程序成为可能,将其划分为小而且容易理解的部分,用 C[ 9]这样的高级语言编写这样的程序不用考虑汇编,用汇编写代码不用考虑逻辑门,用逻辑门来构建处理器不用太多考虑晶体管。

标注(黄色) - 位置 707

设计和实现操作系统的一个目标,是提供高性能( performance)。换言之,我们的目标是最小化操作系统的开销( minimize the overhead)。

标注(黄色) - 位置 712

另一个目标是在应用程序之间以及在 OS和应用程序之间提供保护( protection)。因为我们希望让许多程序同时运行,所以要确保一个程序的恶意或偶然的不良行为不会损害其他程序。我们当然不希望应用程序能够损害操作系统本身(因为这会影响系统上运行的所有程序)。保护是操作系统基本原理之一的核心,这就是隔离( isolation)。

标注(黄色) - 位置 716

操作系统也必须不间断运行。当它失效时,系统上运行的所有应用程序也会失效。由于这种依赖性,操作系统往往力求提供高度的可靠性( reliability)。

标注(黄色) - 位置 728

一开始,操作系统并没有做太多事情。基本上,它只是一组常用函数库。

标注(黄色) - 位置 743

添加一些特殊的硬件指令和硬件状态,让向操作系统过渡变为更正式的、受控的过程。

标注(黄色) - 位置 744

系统调用和过程调用之间的关键区别在于,系统调用将控制转移(跳转)到 OS中,同时提高硬件特权级别( hardware privilege level)。用户应用程序以所谓的用户模式( user mode)运行,这意味着硬件限制了应用程序的功能。例如,以用户模式运行的应用程序通常不能发起对磁盘的 I/ O请求,不能访问任何物理内存页或在网络上发送数据包。在发起系统调用时[通常通过一个称为陷阱( trap)的特殊硬件指令],硬件将控制转移到预先指定的陷阱处理程序( trap handler)(即预先设置的操作系统),并同时将特权级别提升到内核模式( kernel mode)。在内核模式下,操作系统可以完全访问系统的硬件,因此可以执行诸如发起 I/ O请求或为程序提供更多内存等功能。当操作系统完成请求的服务时,它通过特殊的陷阱返回( return-from-trap)指令将控制权交还给用户,该指令返回到用户模式,同时将控制权交还给应用程序,回到应用离开的地方。

标注(黄色) - 位置 919

通过让一个进程只运行一个时间片,然后切换到其他进程,操作系统提供了存在多个虚拟 CPU的假象。这就是时分共享( time sharing) CPU技术,允许用户如愿运行多个并发进程。

标注(黄色) - 位置 922

要实现 CPU的虚拟化,要实现得好,操作系统就需要一些低级机制以及一些高级智能。我们将低级机制称为机制( mechanism)。机制是一些低级方法或协议,实现了所需的功能。

标注(黄色) - 位置 927

时分共享( time sharing)是操作系统共享资源所使用的最基本的技术之一。通过允许资源由一个实体使用一小段时间,然后由另一个实体使用一小段时间,如此下去,所谓的资源(例如, CPU或网络链接)可以被许多人共享。时分共享的自然对应技术是空分共享,资源在空间上被划分给希望使用它的人。例如,磁盘空间自然是一个空分共享资源,因为一旦将块分配给文件,在用户删除文件之前,不可能将它分配给其他文件。

标注(黄色) - 位置 938

为了理解构成进程的是什么,我们必须理解它的机器状态( machine state):程序在运行时可以读取或更新的内容。

标注(黄色) - 位置 940

进程的机器状态有一个明显组成部分,就是它的内存。指令存在内存中。

标注(黄色) - 位置 942

进程的机器状态的另一部分是寄存器。许多指令明确地读取或更新寄存器,因此显然,它们对于执行该进程很重要。

标注(黄色) - 位置 952

最后,程序也经常访问持久存储设备。

标注(黄色) - 位置 967

操作系统运行程序必须做的第一件事是将代码和所有静态数据(例如初始化变量)加载( load)到内存中,加载到进程的地址空间中。

标注(黄色) - 位置 976

将代码和静态数据加载到内存后,操作系统在运行此进程之前还需要执行其他一些操作。必须为程序的运行时栈( run-time stack或 stack)分配一些内存。

标注(黄色) - 位置 980

操作系统也可能为程序的堆( heap)分配一些内存。在 C程序中,堆用于显式请求的动态分配数据。

标注(黄色) - 位置 984

在 UNIX系统中,默认情况下每个进程都有 3个打开的文件描述符( file descriptor),用于标准输入、输出和错误。这些描述符让程序轻松读取来自终端的输入以及打印输出到屏幕。

标注(黄色) - 位置 987

通过将代码和静态数据加载到内存中,通过创建和初始化栈以及执行与 I/ O设置相关的其他工作, OS现在(终于)为程序执行搭好了舞台。然后它有最后一项任务:启动程序,在入口处运行,即 main()。通过跳转到 main()例程(第 5章讨论的专门机制), OS将 CPU的控制权转移到新创建的进程中,从而程序开始执行。

标注(黄色) - 位置 1082

操作系统是一个程序,和其他程序一样,它有一些关键的数据结构来跟踪各种相关的信息。例如,为了跟踪每个进程的状态,操作系统可能会为所有就绪的进程保留某种进程列表( process list),以及跟踪当前正在运行的进程的一些附加信息。操作系统还必须以某种方式跟踪被阻塞的进程。当 I/ O事件完成时,操作系统应确保唤醒正确的进程,让它准备好再次运行。

标注(黄色) - 位置 1109

一个进程可以处于已退出但尚未清理的最终( final)状态(在基于 UNIX的系统中,这称为僵尸状态[ 1])。这个最终状态非常有用,因为它允许其他进程(通常是创建进程的父进程)检查进程的返回代码,并查看刚刚完成的进程是否成功执行(通常,在基于 UNIX的系统中,程序成功完成任务时返回零,否则返回非零)。完成后,父进程将进行最后一次调用(例如, wait()),以等待子进程的完成,并告诉操作系统它可以清理这个正在结束的进程的所有相关数据结构。

标注(黄色) - 位置 1118

有时候人们会将存储关于进程的信息的个体结构称为进程控制块( Process Control Block, PCB),

标注(黄色) - 位置 1120

我们已经介绍了操作系统的最基本抽象:进程。它很简单地被视为一个正在运行的程序。有了这个概念,接下来将继续讨论具体细节:实现进程所需的低级机制和以智能方式调度这些进程所需的高级策略。结合机制和策略,我们将加深对操作系统如何虚拟化 CPU的理解。

标注(黄色) - 位置 1273

shell也是一个用户程序[ 4],它首先显示一个提示符( prompt),然后等待用户输入。你可以向它输入一个命令(一个可执行程序的名称及需要的参数),大多数情况下, shell可以在文件系统中找到这个可执行程序,调用 fork()创建新进程,并调用 exec()的某个变体来执行这个可执行程序,调用 wait()等待该命令完成。子进程执行结束后, shell从 wait()返回并再次输出一个提示符,等待用户输入下一条命令。

标注(黄色) - 位置 1385

操作系统必须以高性能的方式虚拟化 CPU,同时保持对系统的控制。为此,需要硬件和操作系统支持。操作系统通常会明智地利用硬件支持,以便高效地实现其工作。

标注(黄色) - 位置 1389

这个概念的“直接执行”部分很简单:只需直接在 CPU上运行程序即可。因此,当 OS希望启动程序运行时,它会在进程列表中为其创建一个进程条目,为其分配一些内存,将程序代码(从磁盘)加载到内存中,找到入口点( main()函数或类似的),跳转到那里,并开始运行用户的代码。

标注(黄色) - 位置 1419

硬件通过提供不同的执行模式来协助操作系统。在用户模式( user mode)下,应用程序不能完全访问硬件资源。在内核模式( kernel mode)下,操作系统可以访问机器的全部资源。还提供了陷入( trap)内核和从陷阱返回( return-from-trap)到用户模式程序的特别说明,以及一些指令,让操作系统告诉硬件陷阱表( trap table)在内存中的位置。

标注(黄色) - 位置 1444

为什么对系统调用的调用(如 open()或 read())看起来完全就像 C中的典型过程调用。也就是说,如果它看起来像一个过程调用,系统如何知道这是一个系统调用,并做所有正确的事情?原因很简单:它是一个过程调用,但隐藏在过程调用内部的是著名的陷阱指令。更具体地说,当你调用 open()(举个例子)时,你正在执行对 C库的过程调用。其中,无论是对于 open()还是提供的其他系统调用,库都使用与内核一致的调用约定来将参数放在众所周知的位置(例如,在栈中或特定的寄存器中),将系统调用号也放入一个众所周知的位置(同样,放在栈或寄存器中),然后执行上述的陷阱指令。库中陷阱之后的代码准备好返回值,并将控制权返回给发出系统调用的程序。因此, C库中进行系统调用的部分是用汇编手工编码的,因为它们需要仔细遵循约定,以便正确处理参数和返回值,以及执行硬件特定的陷阱指令。现在你知道为什么你自己不必写汇编代码来陷入操作系统了,因为有人已经为你写了这些汇编。

标注(黄色) - 位置 1502

LDE协议有两个阶段。第一个阶段(在系统引导时),内核初始化陷阱表,并且 CPU记住它的位置以供随后使用。内核通过特权指令来执行此操作(所有特权指令均以粗体突出显示)。第二个阶段(运行进程时),在使用从陷阱返回指令开始执行进程之前,内核设置了一些内容(例如,在进程列表中分配一个节点,分配内存)。这会将 CPU切换到用户模式并开始运行该进程。当进程希望发出系统调用时,它会重新陷入操作系统,然后再次通过从陷阱返回,将控制权还给进程。该进程然后完成它的工作,并从 main()返回。这通常会返回到一些存根代码,它将正确退出该程序(例如,通过调用 exit()系统调用,这将陷入 OS中)。此时, OS清理干净,任务完成了。

标注(黄色) - 位置 1510

在进程之间切换应该很简单,对吧?操作系统应该决定停止一个进程并开始另一个进程。有什么大不了的?但实际上这有点棘手,特别是,如果一个进程在 CPU上运行,这就意味着操作系统没有运行。如果操作系统没有运行,它怎么能做事情?(提示:它不能)虽然这听起来几乎是哲学,但这是真正的问题——如果操作系统没有在 CPU上运行,那么操作系统显然没有办法采取行动。

标注(黄色) - 位置 1527

如果应用程序执行了某些非法操作,也会将控制转移给操作系统。例如,如果应用程序以 0为除数,或者尝试访问应该无法访问的内存,就会陷入( trap)操作系统。操作系统将再次控制 CPU(并可能终止违规进程)。

标注(黄色) - 位置 1529

因此,在协作调度系统中, OS通过等待系统调用,或某种非法操作发生,从而重新获得 CPU的控制权。

标注(黄色) - 位置 1537

如何在没有协作的情况下获得控制权  

标注(黄色) - 位置 1540

时钟中断( timer interrupt)[ M + 63]。时钟设备可以编程为每隔几毫秒产生一次中断。产生中断时,当前正在运行的进程停止,操作系统中预先配置的中断处理程序( interrupt handler)会运行。此时,操作系统重新获得 CPU的控制权,因此可以做它想做的事:停止当前进程,并启动另一个进程。

标注(黄色) - 位置 1544

即使进程以非协作的方式运行,添加时钟中断( timer interrupt)也让操作系统能够在 CPU上重新运行。因此,该硬件功能对于帮助操作系统维持机器的控制权至关重要。

标注(黄色) - 位置 1554

既然操作系统已经重新获得了控制权,无论是通过系统调用协作,还是通过时钟中断更强制执行,都必须决定:是继续运行当前正在运行的进程,还是切换到另一个进程。

标注(黄色) - 位置 1557

上下文切换在概念上很简单:操作系统要做的就是为当前正在执行的进程保存一些寄存器的值(例如,到它的内核栈),并为即将执行的进程恢复一些寄存器的值(从它的内核栈)。这样一来,操作系统就可以确保最后执行从陷阱返回指令时,不是返回到之前运行的进程,而是继续执行另一个进程。

后记


未完待续…