程序是一组执行特定任务的计算机指令。该程序可以用汇编语言(一种非常底层的计算机语言)编写,也可以用高级的、机器无关的语言(如 C 编程语言)编写。操作系统是一种特殊的程序,它允许用户运行诸如电子表格和文字处理器之类的应用程序。本章介绍基本的编程原则,并概述操作系统的目标和功能。
CPU 从内存中获取并执行的指令对人类来说是完全无法理解的。它们是机器代码,精确地告诉计算机要做什么。十六进制数 0x89E5 是一条 Intel 80486 指令,它将 ESP 寄存器的内容复制到 EBP 寄存器。汇编器是最早为早期计算机发明的软件工具之一,它是一个程序,接受人类可读的源文件,并将其汇编成机器代码。汇编语言显式地处理寄存器和对数据的操作,并且它们是特定于特定微处理器的。Intel X86 微处理器的汇编语言与 Alpha AXP 微处理器的汇编语言非常不同。以下 Alpha AXP 汇编代码展示了一个程序可以执行的这类操作
ldr r16, (r15) ; Line 1 ldr r17, 4(r15) ; Line 2 beq r16,r17,100 ; Line 3 str r17, (r15) ; Line 4 100: ; Line 5
第一条语句(第 1 行)从寄存器 15 中保存的地址加载寄存器 16。下一条指令从内存中的下一个位置加载寄存器 17。第 3 行比较寄存器 16 和寄存器 17 的内容,如果它们相等,则分支到标签 100。如果寄存器不包含相同的值,则程序继续到第 4 行,在那里 r17 的内容被保存到内存中。如果寄存器包含相同的值,则不需要保存数据。汇编级程序编写起来既繁琐又棘手,而且容易出错。Linux 内核中只有极少部分是用汇编语言编写的,而那些部分编写汇编语言只是为了效率,并且它们是特定于特定微处理器的。
用汇编语言编写大型程序是一项困难且耗时的任务。它容易出错,并且生成的程序是不可移植的,被绑定到一个特定的处理器系列。最好使用像 C 这样的机器无关语言。C 允许你根据程序的逻辑算法和它们操作的数据来描述程序。称为编译器的特殊程序读取 C 程序并将其翻译成汇编语言,从中生成特定于机器的代码。一个好的编译器可以生成几乎与优秀的汇编程序员编写的汇编指令一样高效的指令。Linux 内核的大部分是用 C 语言编写的。以下 C 代码片段
if (x != y) x = y ;
执行与前面的汇编代码示例完全相同的操作。如果变量的内容x与变量的内容不同y那么y的内容将被复制到x。C 代码被组织成例程,每个例程执行一个任务。例程可以返回 C 支持的任何值或数据类型。像 Linux 内核这样的大型程序包含许多独立的 C 源代码模块,每个模块都有自己的例程和数据结构。这些 C 源代码模块将逻辑功能(如文件系统处理代码)组合在一起。
C 支持多种类型的变量,变量是内存中的一个位置,可以通过符号名称引用。在上面的 C 代码片段中x和y指的是内存中的位置。程序员不关心变量放在内存中的哪个位置,链接器(见下文)必须担心这一点。有些变量包含不同类型的数据,整数和浮点数,还有一些是指针。
指针是包含地址的变量,即内存中其他数据的位置。考虑一个名为 x 的变量,它可能位于内存地址 0x80010000。你可以有一个指针,名为 px,它指向 x。px 可能位于地址 0x80010030。px 的值将是 0x80010000:变量 x 的地址。
C 允许你将相关的变量捆绑到数据结构中。例如,
struct { int i ; char b ; } my_struct ;
是一个名为my_struct的数据结构,它包含两个元素,一个名为i的整数(32 位数据存储)和一个名为b.
链接器是将多个目标模块和库链接在一起以形成单个、连贯的程序的程序。目标模块是来自汇编器或编译器的机器代码输出,包含可执行的机器代码和数据,以及允许链接器将模块组合在一起以形成程序的信息。例如,一个模块可能包含程序的所有数据库功能,另一个模块包含其命令行参数处理功能。链接器修复这些目标模块之间的引用,其中在一个模块中引用的例程或数据结构实际上存在于另一个模块中。Linux 内核是一个大型程序,由其许多组成目标模块链接在一起。
没有软件,计算机只是一堆散发热量的电子元件。如果说硬件是计算机的心脏,那么软件就是它的灵魂。操作系统是系统程序的集合,它允许用户运行应用软件。操作系统抽象了系统的真实硬件,并为系统的用户及其应用程序提供了一个虚拟机。在非常真实的意义上,软件提供了系统的特性。大多数 PC 可以运行一个或多个操作系统,并且每个操作系统都可能具有非常不同的外观和感觉。Linux 由许多功能上独立的部件组成,这些部件共同构成了操作系统。Linux 的一个明显部分是内核本身;但即使没有库或 shell,内核也是无用的。
为了开始理解什么是操作系统,请考虑当您键入一个看似简单的命令时会发生什么
$ ls Mail c images perl docs tcl $
$ 是登录 shell(在本例中是bash)发出的提示符。这意味着它正在等待您(用户)键入一些命令。键入ls会导致键盘驱动程序识别到已键入字符。键盘驱动程序将它们传递给 shell,shell 通过查找同名的可执行映像来处理该命令。它找到该映像,在/bin/ls中。内核服务被调用以将ls可执行映像拉入虚拟内存并开始执行它。ls映像调用内核的文件子系统以找出哪些文件可用。文件系统可能会使用缓存的文件系统信息,或者使用磁盘设备驱动程序从磁盘读取此信息。它甚至可能导致网络驱动程序与远程机器交换信息,以找出此系统有权访问的远程文件的详细信息(文件系统可以通过网络文件系统或 NFS 远程挂载)。无论信息位于何处,ls都会写出该信息,并且视频驱动程序会在屏幕上显示它。
以上所有内容看起来都相当复杂,但它表明,即使是最简单的命令也揭示了操作系统实际上是一组协同工作的函数,它们共同为您(用户)提供了系统的一致视图。
如果资源是无限的,例如内存,那么操作系统必须做的许多事情将是多余的。任何操作系统的基本技巧之一是能够使少量物理内存表现得像更多的内存。这种表面上很大的内存被称为虚拟内存。其思想是,系统上运行的软件被愚弄,认为它正在大量的内存中运行。系统将内存划分为易于处理的页面,并在系统运行时将这些页面交换到硬盘上。由于另一个技巧——多进程处理,软件不会注意到这一点。
进程可以被认为是正在运行的程序,每个进程都是一个独立的实体,它正在运行一个特定的程序。如果您查看 Linux 系统上的进程,您会看到有很多进程。例如,键入ps会在我的系统上显示以下进程
$ ps PID TTY STAT TIME COMMAND 158 pRe 1 0:00 -bash 174 pRe 1 0:00 sh /usr/X11R6/bin/startx 175 pRe 1 0:00 xinit /usr/X11R6/lib/X11/xinit/xinitrc -- 178 pRe 1 N 0:00 bowman 182 pRe 1 N 0:01 rxvt -geometry 120x35 -fg white -bg black 184 pRe 1 < 0:00 xclock -bg grey -geometry -1500-1500 -padding 0 185 pRe 1 < 0:00 xload -bg grey -geometry -0-0 -label xload 187 pp6 1 9:26 /bin/bash 202 pRe 1 N 0:00 rxvt -geometry 120x35 -fg white -bg black 203 ppc 2 0:00 /bin/bash 1796 pRe 1 N 0:00 rxvt -geometry 120x35 -fg white -bg black 1797 v06 1 0:00 /bin/bash 3056 pp6 3 < 0:02 emacs intro/introduction.tex 3270 pp6 3 0:00 ps $
如果我的系统有多个 CPU,那么每个进程都可以在不同的 CPU 上运行(至少理论上是这样)。不幸的是,只有一个 CPU,因此操作系统再次求助于欺骗,即依次为每个进程运行一小段时间。这段时间称为时间片。这种技巧被称为多进程处理或调度,它欺骗每个进程,使其认为自己是唯一的进程。进程之间相互保护,因此如果一个进程崩溃或发生故障,它不会影响任何其他进程。操作系统通过为每个进程提供一个单独的地址空间来实现这一点,只有它们才能访问该地址空间。
设备驱动程序构成了 Linux 内核的主要部分。与操作系统的其他部分一样,它们在高度特权的环境中运行,如果它们出错,可能会造成灾难。设备驱动程序控制操作系统与其控制的硬件设备之间的交互。例如,文件系统在向 IDE 磁盘写入块时,会使用通用的块设备接口。驱动程序负责细节,并使设备特定的事情发生。设备驱动程序特定于它们驱动的控制器芯片,这就是为什么,例如,如果您的系统有 NCR810 SCSI 控制器,则需要 NCR810 SCSI 驱动程序的原因。
在 Linux 中,就像在 Unix TM 中一样,系统可能使用的单独文件系统不是通过设备标识符(例如驱动器号或驱动器名称)访问的,而是将它们组合成一个单一的层次树结构,该结构将文件系统表示为一个单一实体。Linux 将每个新的文件系统添加到这个单一的文件系统树中,因为它们被挂载到挂载目录上,例如/mnt/cdrom。Linux 最重要的功能之一是它支持许多不同的文件系统。这使其非常灵活,并且能够很好地与其他操作系统共存。Linux 最流行的文件系统是EXT2文件系统,这也是大多数 Linux 发行版支持的文件系统。
文件系统为用户提供了系统硬盘上保存的文件和目录的合理视图,而不管文件系统类型或底层物理设备的特性如何。Linux 透明地支持许多不同的文件系统(例如MS-DOS和EXT2),并将所有挂载的文件和文件系统呈现为一个集成的虚拟文件系统。因此,一般来说,用户和进程不需要知道任何文件属于哪种文件系统,他们只需使用它们即可。
块设备驱动程序隐藏了物理块设备类型之间的差异(例如,IDE和SCSI),因此就每个文件系统而言,物理设备只是线性的数据块集合。块大小可能因设备而异,例如,软盘设备通常为 512 字节,而 IDE 设备通常为 1024 字节,但这又对系统用户隐藏了。一个EXT2文件系统看起来都是一样的,无论它位于哪个设备上。
这些数据结构主要存在于物理内存中,并且只能由内核及其子系统访问。数据结构包含数据和指针;其他数据结构的地址或例程的地址。总而言之,Linux 内核使用的数据结构可能看起来非常混乱。每个数据结构都有一个用途,虽然有些数据结构被多个内核子系统使用,但它们比乍看起来更简单。
理解 Linux 内核的关键在于理解其数据结构以及 Linux 内核中各种函数对它们的使用。本书基于 Linux 内核的数据结构来描述 Linux 内核。它从算法、完成事情的方法以及内核数据结构的使用方面来讨论每个内核子系统。
哈希表是指向数据结构的指针数组,其索引来自这些数据结构中的信息。如果您有描述村庄人口的数据结构,那么您可以使用一个人的年龄作为索引。要查找特定人员的数据,您可以使用他们的年龄作为人口哈希表的索引,然后跟随指针到达包含该人详细信息的数据结构。不幸的是,村庄中的许多人可能具有相同的年龄,因此哈希表指针成为指向链或数据结构列表的指针,每个数据结构都描述了相同年龄的人。但是,搜索这些较短的链仍然比搜索所有数据结构更快。
由于哈希表加快了对常用数据结构的访问速度,Linux 经常使用哈希表来实现缓存。缓存是需要快速访问的便捷信息,通常是可用信息的完整子集。数据结构被放入缓存并保存在那里,因为内核经常访问它们。缓存的缺点在于,与简单的链表或哈希表相比,它们的使用和维护更加复杂。如果在缓存中可以找到数据结构(这被称为缓存命中),那么一切都很好。如果找不到,则必须搜索所有相关的数据结构,并且如果数据结构确实存在,则必须将其添加到缓存中。在将新数据结构添加到缓存中时,可能需要丢弃旧的缓存条目。Linux 必须决定丢弃哪个,危险在于丢弃的数据结构可能是 Linux 下一个需要的。
通常,这些较低层在启动时向较高层注册自己。此注册通常涉及将数据结构添加到链表中。例如,每个内置到内核中的文件系统在启动时向内核注册自己,或者,如果您正在使用模块,则在首次使用文件系统时注册。您可以通过查看文件来查看哪些文件系统已注册自己/proc/filesystems。注册数据结构通常包括指向函数的指针。这些是指向执行特定任务的软件函数的地址。再次以文件系统注册为例,每个文件系统在注册时传递给 Linux 内核的数据结构包括一个文件系统特定例程的地址,每当挂载该文件系统时都必须调用该例程。