乍一看,Linux是非常复杂的,有许多令人眼花缭乱的部件同时运行和通信。例如网络服务器可以与数据库服务器对话,而数据库服务器又可以使用许多其他程序使用的共享库。所有这些是如何运作的,以及你如何能够理解其中的任何内容?
理解操作系统如何工作的最有效方法是通过抽象--即你可以忽略构成你试图理解的部分的大多数细节,而专注于其基本目的和操作。例如,当你乘坐汽车时,你通常不需要考虑诸如固定汽车内部马达的安装螺栓或建造和维护汽车行驶道路的人等细节。你真正需要知道的是汽车的作用(把你运送到别的地方)和一些关于如何使用它的基本知识(如何操作车门和安全带)。
如果你只是乘客,这种抽象程度管用。但如果你还需要驾驶它,你就必须深入挖掘,把你的抽象概念分成几个部分。你现在把你的知识扩展到三个方面:汽车本身(比如它的尺寸和性能),如何操作控制装置(方向盘、加速踏板等),以及道路的特点。
当你试图寻找和修复问题时,抽象化有很大的帮助。例如,假设你正在驾驶汽车,行驶过程中很不顺利。你可以快速评估刚才提到的三个与汽车相关的基本抽象,以确定问题的来源。如果前两个抽象概念(你的车或你的驾驶方式)都不是问题,那么排除这两个抽象概念应该相当容易,这样你就可以把问题缩小到道路本身。你可能会发现,道路是颠簸的。现在,如果你愿意,你可以更深入地挖掘你对道路的抽象,找出道路恶化的原因,或者,如果道路是新的,为什么建筑工人做了糟糕的工作。
软件开发人员在构建操作系统及其应用程序时,将抽象作为工具。在计算机软件中,有许多抽象的细分术语--包括子系统、模块和包--但我们在本章将使用组件这个术语,因为它很简单。在构建一个软件组件时,开发人员通常不会过多考虑其他组件的内部结构,但他们会考虑他们可以使用的其他组件(这样他们就不必再编写任何额外的不必要的软件)以及如何使用它们。
本章对构成Linux系统的组件作了高层次的概述。尽管每个组件的内部构成都有大量的技术细节,但我们将忽略这些细节,而专注于这些组件对整个系统的作用。我们将在随后的章节中研究这些细节。
1.1 Linux系统中的抽象级别和层数
使用抽象将计算系统分割成组件,使事情更容易理解,但没有组织也不行。我们将组件排列成层或级别,根据组件在用户和硬件之间的位置,对组件进行分类(或分组)。网络浏览器、游戏等位于顶层;在底层,我们有计算机硬件中的内存--0和1。操作系统占据了中间的许多层。
Linux系统有三个主要层次。图1-1显示了这些层次和每个层次中的一些组件。硬件处于底层。硬件包括内存以及一个或多个中央处理单元(CPU),用于执行计算和从内存读写。磁盘和网络接口等设备也是硬件的一部分。
下一个层次是内核,它是操作系统的核心。内核是驻留在内存中的软件,它告诉CPU去哪里寻找下一个任务。内核作为中介,管理硬件(尤其是主内存),是硬件和任何运行程序之间的主要接口。
进程--由内核管理的运行程序--共同构成了系统的上层,称为用户空间。(进程的一个更具体的术语是用户进程,不管用户是否直接与该进程进行交互。例如,所有的网络服务器都作为用户进程运行)。
图1-1:一般的Linux系统组织
内核和用户进程的运行方式有一个重要区别:内核在内核模式下运行,而用户进程在用户模式下运行。在内核模式下运行的代码可以不受限制地访问处理器和主内存。这是强大但危险的特权,允许内核轻易地破坏和崩溃整个系统。只有内核可以访问的内存区域被称为内核空间。
相比之下,用户模式限制了对(通常是相当小的)内存子集的访问和CPU的安全操作。用户空间指的是用户进程可以访问的主内存的部分。如果进程犯了错误而崩溃,其后果是有限的,可以由内核来清理。这意味着,如果你的网络浏览器崩溃了,它可能不会使已经在后台运行了几天的科学计算崩溃。
理论上用户进程失控不会对系统的其他部分造成严重损害。在现实中,这取决于你认为什么是 "严重破坏",以及进程的特定权限,因为有些进程被允许做得比其他进程多。例如,用户进程可以完全破坏磁盘上的数据吗?如果有权限,可以,而且这是相当危险的。然而,有一些保障措施来防止这种情况,大多数进程根本不允许以这种方式进行破坏。
注意
Linux内核可以运行内核线程,它们看起来很像进程,但可以访问内核空间,比如kthreadd和kblockd。
1.2 硬件: 了解主内存
在计算机系统的所有硬件中,主内存可能是最重要的。在其最原始的形式中,主内存只是大的存储区,用于存储一堆0和1。每个0或1的插槽被称为比特。这就是运行中的内核和进程所在的地方--它们只是大量的比特集合。所有来自外围设备的输入和输出都流经主内存,也是一堆比特。CPU只是内存的一个操作者;它从内存中读取指令和数据,并将数据写回内存。
在提到内存、进程、内核和计算机系统的其他部分时,你会经常听到状态这个词。严格说来,状态是特定的比特排列。例如,如果你的内存中有四个比特,0110、0001和1011代表三种不同的状态。
当你考虑到进程可以很容易地由内存中的数百万比特组成时,在谈论状态时,使用抽象的术语往往更容易。与其用比特来描述一态,不如用比特来描述一个东西在这一刻已经做了什么或正在做什么。例如,你可以说,"该进程正在等待输入 "或 "该进程正在执行其启动的第二阶段"。
注意:因为人们通常用抽象的术语而不是实际的比特来指代状态,所以术语image指的是比特的特定物理排列。
1.3 内核
为什么我们要讨论主存和状态?内核所做的一切几乎都是围绕着主存展开的。内核的任务之一是将内存分割成许多子区,它必须在任何时候都保持这些子区的某些状态信息。每个进程都有自己的内存份额,而内核必须确保每个进程都保持自己的份额。
内核负责管理四个一般系统领域的任务:
-
进程 内核负责确定哪些进程被允许使用CPU。
-
内存 内核需要跟踪所有的内存--哪些是当前分配给特定进程的,哪些可能是进程间共享的,哪些是空闲的。
-
设备驱动程序 内核作为硬件(如磁盘)和进程之间的接口。通常内核的工作是操作硬件。
-
系统调用和支持 进程通常使用系统调用来与内核通信。
现在我们将简要地探讨这些领域的每一个问题。
注意
如果你对内核的详细工作原理感兴趣,有两本好的教科书:《Operating System Concepts》第10版,作者是Abraham Silberschatz、Peter B. Galvin和Greg Gagne(Wiley,2018);《Modern Operating Systems》第4版,作者是Andrew S. Tanenbaum和Herbert Bos(Prentice Hall,2014)。
1.3.1 进程管理
进程管理描述了进程的启动、暂停、恢复、调度和终止。启动和终止进程背后的概念是相当直接的,但描述进程在正常运行过程中如何使用CPU就比较复杂了。
在任何现代操作系统上,许多进程都是 "同时 "运行的。例如,你可能在一台台式电脑上同时打开网络浏览器和电子表格。然而,事情并不像他们所看到的那样:这些应用程序背后的进程通常不会完全在同一时间运行。
考虑单核CPU的系统。许多进程可能能够使用CPU,但在任何时候只有一个进程能够实际使用CPU。在实践中,每个进程使用CPU一小部分时间,然后暂停;然后另一个进程使用CPU另一小部分时间;然后另一个进程轮流使用,如此反复。一个进程将CPU的控制权交给另一个进程的行为被称为上下文切换。
每一块时间被称为一个时间片,为进程提供足够的时间进行重要的计算(事实上,进程经常在一个时间片内完成其当前任务)。然而,由于时间片非常小,人类无法感知它们,而且系统似乎在同时运行多个进程(一种被称为多任务的能力)。
内核负责上下文切换。为了理解它是如何工作的,让我们想想这样一种情况:进程在用户模式下运行,但它的时间片已经到了。这就是发生的情况:
- CPU(实际的硬件)根据一个内部计时器中断当前进程,切换到内核模式,并将控制权交还给内核。
- 内核记录了CPU和内存的当前状态,这对于恢复刚刚被中断的进程至关重要。
- 内核执行在前一个时间片中可能出现的任何任务(比如从输入和输出(I/O)操作中收集数据)。
- 内核现在已经准备好让另一个进程运行。内核分析准备运行的进程列表并选择一个。
- 内核为这个新进程准备好内存,然后为CPU做准备。
- 内核告诉CPU新进程的时间片将持续多长时间。
- 内核将CPU切换到用户模式,并将CPU的控制权交给该进程。
上下文切换回答了内核何时运行这一重要问题。答案是,在上下文切换期间,它在进程的时间片之间运行。
在多CPU系统的情况下,就像目前大多数机器一样,事情变得稍微复杂一些,因为内核不需要放弃对当前CPU的控制,就可以让一个进程在不同的CPU上运行,而且一次可以运行不止一个进程。然而,为了最大限度地利用所有可用的CPU,内核通常会执行这些步骤(并可能使用某些技巧来为自己多争取一点CPU时间)。
1.3.2 内存管理
内核必须在上下文切换期间管理内存,这可能是一项复杂的工作。以下条件必须成立:
- 内核必须在内存中拥有自己的私有区域,用户进程不能访问。
- 每个用户进程都需要自己的内存区域。
- 用户进程不能访问另一个进程的私有内存。
- 用户进程可以共享内存。
- 用户进程中的一些内存可以是只读的。
- 系统可以通过使用磁盘空间作为辅助来使用比实际存在的更多的内存。
现代CPU包括一个内存管理单元(MMU memory management unit ),它可以实现一种叫做虚拟内存的内存访问方案。当使用虚拟内存时,进程并不直接通过其在硬件中的物理位置来访问内存。相反,内核将每个进程设置成好像它自己有一整台机器的样子。当进程访问它的一些内存时,MMU会拦截访问,并使用内存地址图将内存位置从进程的角度转换为机器中的实际物理内存位置。内核仍然必须初始化并持续维护和改变这个内存地址图。例如,在上下文切换过程中,内核必须将该地图从离开的进程改变为进入的进程。
注意: 内存地址映射的实现被称为页表。
你将在第8章中了解更多关于如何查看内存性能的信息。
1.3.3 设备驱动和管理
内核对设备的作用相对简单。设备通常只能在内核模式下访问,因为不适当的访问(比如用户进程要求关闭电源)会使机器崩溃。值得注意的困难是,不同的设备很少有相同的编程接口,即使这些设备执行相同的任务(例如,两个不同的网卡)。因此,设备驱动历来是内核的一部分,它们努力为用户进程提供统一的接口,以简化软件开发者的工作。
系统调用和支持
还有一些其他类型的内核功能可供用户进程使用。例如,系统调用(或称syscalls)执行一些特定的任务,而这些任务单靠用户进程是不能很好地完成的,或者根本就不能完成。例如,打开、读取和写入文件的行为都涉及系统调用。
两个系统调用,fork()和exec(),对于理解进程如何启动很重要:
fork() 当进程调用fork()时,内核会创建一个几乎相同的进程副本。
exec() 当一个进程调用exec(program)时,内核加载并启动程序,取代当前进程。
除了init(见第6章),Linux系统中所有新的用户进程都是由于fork()而启动的,大多数时候,你也会运行exec()来启动一个新的程序,而不是运行一个现有进程的副本。非常简单的例子是你在命令行上运行的任何程序,例如显示一个目录内容的ls命令。当你在终端窗口中输入ls时,在终端窗口中运行的shell调用fork()来创建一个shell的副本,然后这个shell的新副本调用exec(ls)来运行ls。图1-2显示了启动像ls这样的程序的进程和系统调用的流程。
图1-2: 启动一个新的进程
注意
系统调用通常用括号来表示。在图1-2所示的例子中,要求内核创建另一个进程的进程必须执行fork()系统调用。这个符号来自于C语言中调用的写法。你不需要知道C语言来理解本书,只要记住系统调用是进程和内核之间的交互。此外,本书还简化了某些组的系统调用。例如,exec()指的是整个系统调用家族,它们都执行类似的任务,但在编程上有所不同。还有一个称为线程的进程的变种,我们将在第8章中介绍。
内核还支持具有传统系统调用以外的功能的用户进程,其中最常见的是伪设备(pseudodevices)。伪设备对用户进程来说看起来像设备,但它们是纯粹用软件实现的。这意味着它们在技术上不需要在内核中出现,但它们通常出于实际原因而出现。例如,内核的随机数生成器设备(/dev/random)就很难在用户进程中安全实现。
注意:从技术上讲,访问伪设备的用户进程必须使用系统调用来打开设备,所以进程不能完全避免系统调用。
1.4 用户空间
如前所述,内核为用户进程分配的主内存被称为用户空间。因为进程只是内存中的一个状态(或image),所以用户空间也指整个运行中的进程集合的内存。(你也可能听到用更非正式的术语userland来表示用户空间;有时这也意味着在用户空间运行的程序。)
Linux系统中的大部分实际操作都发生在用户空间。尽管从内核的角度来看,所有的进程本质上是平等的,但它们为用户执行不同的任务。对于用户进程所代表的系统组件的种类,有基本的服务级别(或层)结构。图1-3显示了一组组件在Linux系统中是如何配合和互动的。基本服务在最底层(最接近内核),实用服务在中间,而用户接触的应用程序在最上面。图1-3是一个大大简化的图,因为只显示了六个组件,但是你可以看到顶部的组件是最接近用户的(用户界面和网络浏览器);中间层的组件包括网络浏览器使用的域名缓存服务器;底部还有几个较小的组件。
底层往往由小的组件组成,执行单一的、不复杂的任务。中间层有较大的组件,如邮件、打印和数据库服务。最后,顶层的组件执行复杂的任务,用户经常直接控制。组件也使用其他组件。一般来说,如果一个组件想使用另一个组件,第二个组件要么处于同一服务级别,要么低于这个级别。
然而,图1-3只是对用户空间安排的近似描述。在现实中,用户空间并没有什么规则。例如,大多数应用程序和服务都会写被称为日志的诊断信息。大多数程序使用标准的syslog服务来写日志信息,但有些程序喜欢自己做所有的日志。
此外,对一些用户空间的组件也很难进行分类。服务器组件,如Web和数据库服务器,可以被认为是非常高级的应用程序,因为它们的任务往往很复杂,所以你可能会把这些放在图1-3的最高层。然而,用户应用程序可能依赖于这些服务器来执行他们不愿自己做的任务,所以你也可以为把它们放在中间层提供理由。
1.5 用户
Linux内核支持Unix用户的传统概念。用户是可以运行进程和拥有文件的实体。用户通常与用户名相关联,例如,系统可以有名为billyjoe的用户。然而,内核并不管理用户名;相反,它通过简单的数字标识符(称为用户ID)来识别用户。(你将在第7章中了解更多关于用户名与用户ID的对应关系)。
用户的存在主要是为了支持权限和边界。每个用户空间的进程都有一个用户所有者,而进程据说是作为所有者运行的。用户可以终止或修改自己进程的行为(在一定范围内),但它不能干扰其他用户的进程。此外,用户可以拥有文件,并选择是否与其他用户分享这些文件。
Linux系统除了对应于使用该系统的真实人类的用户外,通常还有一些用户。你将在第三章中详细了解这些用户,但最重要的用户是root。root用户是前述规则的一个例外,因为root可以终止和改变其他用户的进程,并访问本地系统中的任何文件。由于这个原因,root被称为超级用户。在传统的Unix系统中,能够以root身份操作的人,也就是有root权限的人,就是管理员。
注意:以root身份操作可能是危险的。因为系统会让你做任何事情,即使它对系统有害,也很难识别和纠正错误。出于这个原因,系统设计者不断尝试使root权限尽可能不被需要--例如,不需要root权限就可以在笔记本上切换无线网络。此外,尽管root用户很强大,但它仍然运行在操作系统的用户模式,而不是内核模式。
组是用户的集合。组的主要目的是允许一个用户与组内的其他成员共享文件访问。
1.6 展望未来
到目前为止,你已经看到了什么构成了运行中的Linux系统。用户进程构成了你直接与之交互的环境;内核管理着进程和硬件。内核和进程都驻留在内存中。
这些都是很好的背景信息,但你不能仅仅通过阅读来了解Linux系统的细节,你需要亲身体验。下一章将通过教授你一些用户空间的基础知识开始你的旅程。在这一过程中,你会了解到本章没有讨论的Linux系统的主要部分:长期存储(磁盘、文件等)。毕竟,你需要把你的程序和数据储存在某个地方。
评论
查看更多