第1章:全景概述
乍一看,像 Linux 这样的现代操作系统非常复杂,有大量部件同时运行和通信。例如,一个 web 服务器可以与一个数据库服务器通信,而数据库服务器又可能使用许多其他程序共享的库。这一切是如何运作的?你又是如何理解它的?
理解操作系统工作原理最有效的方式是通过抽象 —— 这是一种花哨的说法,即你可以忽略你试图理解的部件的大部分细节,而专注于其基本目的和操作。例如,当你乘坐汽车时,你通常不需要考虑诸如固定发动机的安装螺栓、建造和维护道路的工人等细节。你真正需要知道的是汽车的作用(把你运送到其他地方)以及使用它的一些基础知识(如何操作车门和安全带)。
1.1 Linux 系统的抽象级别与层次
使用抽象将计算系统拆分成组件使事情更容易理解,但如果没有组织,这就不起作用。我们将组件安排成层或级别,即根据组件处于用户与硬件之间的位置对组件进行分类(或分组)。Web 浏览器、游戏等位于顶层;在底层,我们有计算机硬件中的内存——0 和 1。操作系统占据中间的许多层。
一个 Linux 系统有三个主要级别。图 1-1 展示了这些级别以及每个级别中的一些组件。硬件位于底层。硬件包括内存以及一个或多个中央处理单元(CPU),用于执行计算并从内存读取和写入数据。磁盘和网络接口等设备也是硬件的一部分。
下一层是内核,它是操作系统的核心。内核是驻留在内存中的软件,它告诉 CPU 在哪里寻找下一个任务。作为中介,内核管理硬件(尤其是主内存),并且是硬件与任何正在运行的程序之间的主要接口。
由内核管理的运行程序(称为进程)共同构成系统的上层,称为用户空间。(对进程更具体的术语是用户进程,无论用户是否直接与该进程交互。例如,所有 Web 服务器都作为用户进程运行。)
graph TD subgraph 用户空间 UserProcesses[用户进程] GUI[图形用户界面] Servers[服务器] Shell[Shell] SystemCalls[系统调用] end subgraph Linux内核 Kernel[Linux内核] ProcessMgmt[进程管理] MemoryMgmt[内存管理] DeviceDrivers[设备驱动] end subgraph 硬件 CPU[处理器 (CPU)] RAM[主内存 (RAM)] Disks[磁盘] Network[网络端口] end UserProcesses --> Kernel GUI --> Kernel Servers --> Kernel Shell --> Kernel SystemCalls --> Kernel Kernel --> ProcessMgmt Kernel --> MemoryMgmt Kernel --> DeviceDrivers ProcessMgmt --> CPU MemoryMgmt --> RAM DeviceDrivers --> Disks DeviceDrivers --> Network
图 1-1:Linux 系统的一般组织结构
内核和用户进程的运行方式存在一个关键区别:内核运行在内核模式下,而用户进程运行在用户模式下。运行在内核模式下的代码对处理器和主内存有不受限制的访问权限。这是一个强大但危险的特权,它允许内核轻易地损坏并使整个系统崩溃。只有内核可以访问的内存区域称为内核空间。
相比之下,用户模式限制对内存的一个(通常非常小的)子集和安全的 CPU 操作的访问。用户空间指的是用户进程可以访问的主内存部分。如果一个进程出错并崩溃,后果是有限的,并且可以被内核清理。这意味着如果你的 Web 浏览器崩溃,它可能不会导致已经在后台运行了数天的科学计算终止。
理论上,一个失控的用户进程不能对系统的其余部分造成严重损害。实际上,这取决于你如何定义“严重损害”,以及该进程的特定权限,因为某些进程被允许执行比其他进程更多的操作。例如,用户进程能否完全摧毁磁盘上的数据?在拥有正确权限的情况下,是的——你可能认为这相当危险。然而,有一些保护机制可以防止这种情况,大多数进程根本不允许以这种方式造成严重破坏。
NOTE
Linux 内核可以运行内核线程,它们看起来很像进程,但可以访问内核空间。一些例子包括
kthreadd和kblockd。
1.2 硬件:理解主内存
在计算机系统的所有硬件中,主内存可能是最重要的。在最原始的形式中,主内存只是一个存储大量 0 和 1 的大区域。每个存储 0 或 1 的槽位称为一个比特。运行中的内核和进程就驻留在这里——它们只是一大堆比特。来自外围设备的所有输入和输出都流经主内存,同样也是一堆比特。CPU 仅仅是一个内存操作器;它从内存读取指令和数据,并将数据写回内存。
你经常会听到状态这个词,用于指代内存、进程、内核以及计算机系统的其他部分。严格来说,状态是比特的特定排列。例如,如果你的内存中有四个比特,那么 0110、0001 和 1011 代表三种不同的状态。
当你考虑到一个单个进程可能由内存中的数百万比特组成时,用抽象术语来谈论状态通常更容易。你不是用比特来描述状态,而是描述某物在那一刻已经做了什么或正在做什么。例如,你可能会说:“进程正在等待输入”或“进程正在执行其启动的第二阶段”。
NOTE
因为通常用抽象术语而不是实际的比特来指代状态,所以映像这个术语指代比特的特定物理排列。
1.3 内核
为什么我们要谈论主内存和状态?内核所做的一切几乎都围绕主内存。内核的任务之一是将内存划分成许多子区域,并且它必须始终保持关于这些子区域的某些状态信息。每个进程获得自己的一份内存,内核必须确保每个进程不超出自己的份额。
内核负责管理四个通用系统区域中的任务:
- 进程:内核负责决定哪些进程被允许使用 CPU。
- 内存:内核需要跟踪所有内存——当前分配给特定进程的内存、进程之间可能共享的内存以及空闲内存。
- 设备驱动:内核充当硬件(例如磁盘)与进程之间的接口。通常,内核的工作是操作硬件。
- 系统调用和支持:进程通常使用系统调用与内核通信。
我们现在将简要探讨这些区域中的每一个。
NOTE
如果你对内核的详细工作原理感兴趣,有两本很好的教科书:《操作系统概念》第10版,Abraham Silberschatz、Peter B. Galvin 和 Greg Gagne 著(Wiley, 2018),以及《现代操作系统》第4版,Andrew S. Tanenbaum 和 Herbert Bos 著(Prentice Hall, 2014)。
第1章:全景概述
1.3.1 进程管理
进程管理描述进程的启动、暂停、恢复、调度和终止。启动和终止进程的概念相当直接,但描述进程在正常运行过程中如何使用CPU则稍显复杂。
在任何现代操作系统上,许多进程看似“同时”运行。例如,你可以在台式电脑上同时打开一个网页浏览器和一个电子表格。然而,事情并非看上去那样:这些应用程序背后的进程通常并不会在同一时刻精确运行。
考虑一个单核CPU的系统。许多进程可能能够使用CPU,但在任何给定时刻,只有一个进程能实际使用CPU。在实践中,每个进程使用CPU一小段时间(几分之一秒),然后暂停;接着另一个进程使用CPU另一个几分之一秒;然后另一个进程轮流使用,依此类推。一个进程将CPU控制权交给另一个进程的行为称为上下文切换。
每一小段时间——称为时间片——给予进程足够的时间完成重要的计算(实际上,进程通常会在单个时间片内完成当前任务)。然而,由于时间片非常短,人类无法感知,系统看起来像是同时运行多个进程(这种能力称为多任务)。
内核负责上下文切换。为了理解其工作原理,让我们考虑一个进程在用户模式下运行但其时间片已用完的情况。以下是具体发生的过程:
- CPU(实际硬件)基于内部定时器中断当前进程,切换到内核模式,并将控制权交回内核。
- 内核记录CPU和内存的当前状态,这对于稍后恢复被中断的进程至关重要。
- 内核执行在前一个时间片期间可能出现的任何任务(例如从输入输出操作收集数据)。
- 内核现在准备让另一个进程运行。内核分析已准备好运行的进程列表,并选择一个。
- 内核为新进程准备内存,然后准备CPU。
- 内核告诉CPU新进程的时间片将持续多久。
- 内核将CPU切换到用户模式,并将CPU控制权交给该进程。
上下文切换回答了一个重要问题:内核何时运行?答案是:它在进程时间片之间、上下文切换期间运行。
对于多CPU系统(大多数现代机器都是如此),情况会稍微复杂一些,因为内核不需要释放其当前CPU的控制权来允许另一个进程在不同CPU上运行,而且多个进程可以同时运行。然而,为了最大化所有可用CPU的利用率,内核通常仍然执行这些步骤(并且可能使用某些技巧为自己争取一点额外的CPU时间)。
1.3.2 内存管理
在上下文切换期间,内核必须管理内存,这可能是一项复杂的工作。必须满足以下条件:
- 内核必须拥有自己的私有内存区域,用户进程无法访问。
- 每个用户进程需要自己的内存段。
- 一个用户进程不能访问另一个进程的私有内存。
- 用户进程可以共享内存。
- 用户进程中的某些内存可以是只读的。
- 系统可以通过使用磁盘空间作为辅助,使用比物理内存更多的内存。
幸运的是,内核有帮手。现代CPU包含一个内存管理单元(MMU),它支持一种称为虚拟内存的内存访问方案。使用虚拟内存时,进程并不直接通过硬件中的物理位置访问内存。相反,内核设置每个进程,使其表现得好像拥有一整台机器。当进程访问其部分内存时,MMU拦截该访问,并使用内存地址映射将进程视角的内存位置转换为机器中的实际物理内存位置。内核仍然需要初始化并持续维护和修改此内存地址映射。例如,在上下文切换期间,内核必须将映射从离开的进程切换到进入的进程。
注意
内存地址映射的实现称为页表。你将在第8章了解有关如何查看内存性能的更多信息。
1.3.3 设备驱动与管理
内核在设备方面的角色相对简单。设备通常只能在内核模式下访问,因为不当的访问(例如用户进程要求关闭电源)可能导致机器崩溃。一个显著的困难是,不同的设备几乎从来不具有相同的编程接口,即使这些设备执行相同的任务(例如,两个不同的网卡)。因此,设备驱动程序传统上属于内核的一部分,它们努力向用户进程呈现统一的接口,以简化软件开发者的工作。
1.3.4 系统调用与支持
用户进程还可以使用其他几种内核特性。例如,系统调用(或syscall)执行用户进程单独无法很好完成或根本无法完成的特定任务。例如,打开、读取和写入文件这些操作都涉及系统调用。
两个重要的系统调用 fork() 和 exec() 对于理解进程如何启动至关重要:
fork():当一个进程调用fork()时,内核创建一个几乎与该进程完全相同的副本。exec():当一个进程调用exec(program)时,内核加载并启动program,替换当前进程。
除 init 之外(见第6章),Linux 系统上所有新的用户进程都通过 fork() 启动,而大多数情况下,你也会运行 exec() 来启动一个新程序,而不是运行现有进程的副本。一个非常简单的例子是你在命令行运行的任何程序,例如显示目录内容的 ls 命令。当你在终端窗口中输入 ls 时,终端窗口内运行的 shell 会调用 fork() 创建一个 shell 的副本,然后该 shell 副本调用 exec(ls) 来运行 ls。图1-2展示了启动 ls 这类程序时的进程和系统调用流程。
flowchart LR shell[shell] --> fork["fork()"] fork --> copy["shell 的副本"] copy --> exec["exec(ls)"] exec --> ls[ls]
注意
系统调用通常用括号表示。在图1-2所示的示例中,请求内核创建另一个进程的进程必须执行
fork()系统调用。这种表示法源于C编程语言中的调用方式。你无需了解C语言也能理解本书;只需记住系统调用是进程与内核之间的交互即可。此外,本书会对某些系统调用组进行简化。例如,exec()指的是执行类似任务但在编程上有所不同的整个系统调用族。还有一种称为线程的进程变体,我们将在第8章讨论。
内核还支持用户进程使用传统系统调用之外的其他特性,最常见的是伪设备。伪设备对用户进程而言看起来像设备,但它们是纯粹用软件实现的。这意味着它们在技术上不一定需要在内核中,但出于实际原因通常出现在内核中。例如,内核随机数生成器设备(/dev/random)如果通过用户进程实现,将很难保证安全。
注意
从技术上讲,访问伪设备的用户进程必须使用系统调用来打开该设备,因此进程无法完全避免系统调用。
1.4 用户空间
如前所述,内核分配给用户进程的主内存称为用户空间。由于进程只是内存中的一个状态(或映像),用户空间也指所有运行进程集合所占据的内存。(你可能还会听到更非正式的术语用户态(userland)用于指代用户空间;有时它也指运行在用户空间中的程序。)
Linux 系统上大多数真正的操作发生在用户空间。虽然从内核的角度看所有进程本质上是平等的,但它们为用户执行不同的任务。用户进程所代表的系统组件存在一种基本的服务层次(或层)结构。图1-3展示了如何在Linux系统上,一组示例组件如何组合在一起并相互交互。基本服务位于底层(最接近内核),实用服务位于中间层,用户接触的应用程序位于顶层。图1-3是一个极大简化的示意图,因为只展示了六个组件,但你可以看到顶层组件最接近用户(用户界面和网页浏览器);中间层组件包括一个域名缓存服务器,供网页浏览器使用;底层则有多个较小的组件。
flowchart LR subgraph 用户进程 direction LR top[用户界面] web[网页浏览器] end subgraph 中间层 name[域名缓存服务器] net[网络配置] end subgraph 底层 bus[通信总线] log[诊断日志记录] end top --> web web --> name name --> net net --> bus bus --> log
底层往往由执行单一、简单任务的小型组件组成。中间层有较大的组件,例如邮件、打印和数据库服务。最后,顶层组件执行复杂的任务,用户通常直接控制这些任务。组件也会使用其他组件。通常,如果一个组件想使用另一个组件,那么第二个组件要么处于相同的服务层,要么处于较低层。
然而,图1-3只是用户空间布局的一种近似。实际上,用户空间中没有规则。例如,大多数应用程序和服务会写入称为日志的诊断消息。大多数程序使用标准的 syslog 服务来写入日志消息,但有些程序宁愿自己完成所有日志记录。
此外,很难对某些用户空间组件进行分类。诸如 Web 服务器和数据库服务器之类的服务器组件可以被视为非常高级的应用程序,因为它们的任务通常很复杂,因此你可能会将它们放在图1-3中的顶层。然而,用户应用程序可能依赖这些服务器来执行它们自己宁愿不做的任务,因此你也可以有理由将它们放在中间层。
1.5 用户
Linux 内核支持传统的 Unix 用户概念。用户是一个实体,可以运行进程并拥有文件。用户通常与用户名关联;例如,系统可以有一个名为 billyjoe 的用户。然而,内核并不管理用户名;相反,它通过简单的数字标识符(称为用户ID)来识别用户。(你将在第7章中了解更多关于用户名如何对应到用户ID的内容。)
用户的存在主要是为了支持权限和边界。每个用户空间进程都有一个用户所有者,并且进程被认为是以所有者的身份运行。用户可以终止或修改自己进程的行为(在一定限度内),但不能干涉其他用户的进程。此外,用户可以拥有文件,并选择是否与其他用户共享。
Linux 系统通常除了对应实际使用系统的人类用户之外,还有许多其他用户。你将在第3章中详细了解这些内容,但其中最重要的用户是 root。root 用户是上述规则的例外,因为 root 可以终止和修改其他用户的进程,并访问本地系统上的任何文件。因此,root 被称为超级用户。在传统 Unix 系统中,能够以 root 身份操作(即拥有 root 访问权限)的人是管理员。
WARNING
以 root 身份操作可能很危险。由于系统允许你做任何事情,即使是对系统有害的操作,因此很难识别和纠正错误。为此,系统设计者不断尝试使 root 访问尽可能不必要——例如,在笔记本上切换无线网络时不需要 root 访问。此外,尽管 root 用户非常强大,但它仍然运行在操作系统的用户模式下,而不是内核模式下。
10 Chapter 1
组是用户的集合。组的主要目的是允许用户与其他组成员共享文件访问权限。
1.6 展望未来
到目前为止,你已经了解了构成一个运行中的 Linux 系统的要素。用户进程构成了你直接与之交互的环境;内核管理进程和硬件。内核和进程都驻留在内存中。
这些是很棒的背景信息,但仅靠阅读无法学会 Linux 系统的细节;你需要亲自动手。下一章将开始你的旅程,教你一些用户空间基础知识。在此过程中,你将学习本章未讨论的 Linux 系统的一个重要组成部分:长期存储(磁盘、文件等)。毕竟,你需要将程序和资料存放在某处。