目录, 显示框架, 无框架

第 2 章
软件基础


程序是一组执行特定任务的计算机指令。该程序可以用汇编语言(一种非常底层的计算机语言)编写,也可以用高级的、机器无关的语言(如 C 编程语言)编写。操作系统是一种特殊的程序,它允许用户运行诸如电子表格和文字处理器之类的应用程序。本章介绍基本的编程原则,并概述操作系统的目标和功能。

2.1  计算机语言

2.1.1  汇编语言

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 内核中只有极少部分是用汇编语言编写的,而那些部分编写汇编语言只是为了效率,并且它们是特定于特定微处理器的。

2.1.2  C 编程语言和编译器

用汇编语言编写大型程序是一项困难且耗时的任务。它容易出错,并且生成的程序是不可移植的,被绑定到一个特定的处理器系列。最好使用像 C 这样的机器无关语言。C 允许你根据程序的逻辑算法和它们操作的数据来描述程序。称为编译器的特殊程序读取 C 程序并将其翻译成汇编语言,从中生成特定于机器的代码。一个好的编译器可以生成几乎与优秀的汇编程序员编写的汇编指令一样高效的指令。Linux 内核的大部分是用 C 语言编写的。以下 C 代码片段

        if (x != y)
                x = y ;

执行与前面的汇编代码示例完全相同的操作。如果变量的内容x与变量的内容不同y那么y的内容将被复制到x。C 代码被组织成例程,每个例程执行一个任务。例程可以返回 C 支持的任何值或数据类型。像 Linux 内核这样的大型程序包含许多独立的 C 源代码模块,每个模块都有自己的例程和数据结构。这些 C 源代码模块将逻辑功能(如文件系统处理代码)组合在一起。

C 支持多种类型的变量,变量是内存中的一个位置,可以通过符号名称引用。在上面的 C 代码片段中xy指的是内存中的位置。程序员不关心变量放在内存中的哪个位置,链接器(见下文)必须担心这一点。有些变量包含不同类型的数据,整数和浮点数,还有一些是指针。

指针是包含地址的变量,即内存中其他数据的位置。考虑一个名为 x 的变量,它可能位于内存地址 0x80010000。你可以有一个指针,名为 px,它指向 xpx 可能位于地址 0x80010030px 的值将是 0x80010000:变量 x 的地址。

C 允许你将相关的变量捆绑到数据结构中。例如,

        struct {
                int i ;
                char b ;
        } my_struct ;

是一个名为my_struct的数据结构,它包含两个元素,一个名为i的整数(32 位数据存储)和一个名为b.

2.1.3  链接器

链接器是将多个目标模块和库链接在一起以形成单个、连贯的程序的程序。目标模块是来自汇编器或编译器的机器代码输出,包含可执行的机器代码和数据,以及允许链接器将模块组合在一起以形成程序的信息。例如,一个模块可能包含程序的所有数据库功能,另一个模块包含其命令行参数处理功能。链接器修复这些目标模块之间的引用,其中在一个模块中引用的例程或数据结构实际上存在于另一个模块中。Linux 内核是一个大型程序,由其许多组成目标模块链接在一起。

2.2  什么是操作系统?

没有软件,计算机只是一堆散发热量的电子元件。如果说硬件是计算机的心脏,那么软件就是它的灵魂。操作系统是系统程序的集合,它允许用户运行应用软件。操作系统抽象了系统的真实硬件,并为系统的用户及其应用程序提供了一个虚拟机。在非常真实的意义上,软件提供了系统的特性。大多数 PC 可以运行一个或多个操作系统,并且每个操作系统都可能具有非常不同的外观和感觉。Linux 由许多功能上独立的部件组成,这些部件共同构成了操作系统。Linux 的一个明显部分是内核本身;但即使没有库或 shell,内核也是无用的。

为了开始理解什么是操作系统,请考虑当您键入一个看似简单的命令时会发生什么

$ ls
Mail            c               images          perl
docs            tcl
$ 

$ 是登录 shell(在本例中是bash)发出的提示符。这意味着它正在等待您(用户)键入一些命令。键入ls会导致键盘驱动程序识别到已键入字符。键盘驱动程序将它们传递给 shell,shell 通过查找同名的可执行映像来处理该命令。它找到该映像,在/bin/ls中。内核服务被调用以将ls可执行映像拉入虚拟内存并开始执行它。ls映像调用内核的文件子系统以找出哪些文件可用。文件系统可能会使用缓存的文件系统信息,或者使用磁盘设备驱动程序从磁盘读取此信息。它甚至可能导致网络驱动程序与远程机器交换信息,以找出此系统有权访问的远程文件的详细信息(文件系统可以通过网络文件系统或 NFS 远程挂载)。无论信息位于何处,ls都会写出该信息,并且视频驱动程序会在屏幕上显示它。

以上所有内容看起来都相当复杂,但它表明,即使是最简单的命令也揭示了操作系统实际上是一组协同工作的函数,它们共同为您(用户)提供了系统的一致视图。

2.2.1  内存管理

如果资源是无限的,例如内存,那么操作系统必须做的许多事情将是多余的。任何操作系统的基本技巧之一是能够使少量物理内存表现得像更多的内存。这种表面上很大的内存被称为虚拟内存。其思想是,系统上运行的软件被愚弄,认为它正在大量的内存中运行。系统将内存划分为易于处理的页面,并在系统运行时将这些页面交换到硬盘上。由于另一个技巧——多进程处理,软件不会注意到这一点。

2.2.2  进程

进程可以被认为是正在运行的程序,每个进程都是一个独立的实体,它正在运行一个特定的程序。如果您查看 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,因此操作系统再次求助于欺骗,即依次为每个进程运行一小段时间。这段时间称为时间片。这种技巧被称为多进程处理或调度,它欺骗每个进程,使其认为自己是唯一的进程。进程之间相互保护,因此如果一个进程崩溃或发生故障,它不会影响任何其他进程。操作系统通过为每个进程提供一个单独的地址空间来实现这一点,只有它们才能访问该地址空间。

2.2.3  设备驱动程序

设备驱动程序构成了 Linux 内核的主要部分。与操作系统的其他部分一样,它们在高度特权的环境中运行,如果它们出错,可能会造成灾难。设备驱动程序控制操作系统与其控制的硬件设备之间的交互。例如,文件系统在向 IDE 磁盘写入块时,会使用通用的块设备接口。驱动程序负责细节,并使设备特定的事情发生。设备驱动程序特定于它们驱动的控制器芯片,这就是为什么,例如,如果您的系统有 NCR810 SCSI 控制器,则需要 NCR810 SCSI 驱动程序的原因。

2.2.4  文件系统

在 Linux 中,就像在 Unix TM 中一样,系统可能使用的单独文件系统不是通过设备标识符(例如驱动器号或驱动器名称)访问的,而是将它们组合成一个单一的层次树结构,该结构将文件系统表示为一个单一实体。Linux 将每个新的文件系统添加到这个单一的文件系统树中,因为它们被挂载到挂载目录上,例如/mnt/cdrom。Linux 最重要的功能之一是它支持许多不同的文件系统。这使其非常灵活,并且能够很好地与其他操作系统共存。Linux 最流行的文件系统是EXT2文件系统,这也是大多数 Linux 发行版支持的文件系统。

文件系统为用户提供了系统硬盘上保存的文件和目录的合理视图,而不管文件系统类型或底层物理设备的特性如何。Linux 透明地支持许多不同的文件系统(例如MS-DOSEXT2),并将所有挂载的文件和文件系统呈现为一个集成的虚拟文件系统。因此,一般来说,用户和进程不需要知道任何文件属于哪种文件系统,他们只需使用它们即可。

块设备驱动程序隐藏了物理块设备类型之间的差异(例如,IDESCSI),因此就每个文件系统而言,物理设备只是线性的数据块集合。块大小可能因设备而异,例如,软盘设备通常为 512 字节,而 IDE 设备通常为 1024 字节,但这又对系统用户隐藏了。一个EXT2文件系统看起来都是一样的,无论它位于哪个设备上。

2.3  内核数据结构

操作系统必须保存大量关于系统当前状态的信息。当系统内发生事情时,必须更改这些数据结构以反映当前的现实。例如,当用户登录到系统时,可能会创建一个新进程。内核必须创建一个表示新进程的数据结构,并将其与表示系统中所有其他进程的数据结构链接起来。

这些数据结构主要存在于物理内存中,并且只能由内核及其子系统访问。数据结构包含数据和指针;其他数据结构的地址或例程的地址。总而言之,Linux 内核使用的数据结构可能看起来非常混乱。每个数据结构都有一个用途,虽然有些数据结构被多个内核子系统使用,但它们比乍看起来更简单。

理解 Linux 内核的关键在于理解其数据结构以及 Linux 内核中各种函数对它们的使用。本书基于 Linux 内核的数据结构来描述 Linux 内核。它从算法、完成事情的方法以及内核数据结构的使用方面来讨论每个内核子系统。

2.3.1  链表

Linux 使用许多软件工程技术将其数据结构链接在一起。在很多情况下,它使用链接链式数据结构。如果每个数据结构描述了某个事物的单个实例或发生,例如进程或网络设备,则内核必须能够找到所有实例。在链表中,根指针包含列表中第一个数据结构或元素的地址,并且每个数据结构都包含指向列表中下一个元素的指针。最后一个元素的下一个指针将为 0 或 NULL,以表明它是列表的末尾。在双向链表中,每个元素都包含指向列表中下一个元素的指针,以及指向列表中前一个元素的指针。使用双向链表可以更容易地从列表的中间添加或删除元素,尽管您需要更多的内存访问。这是一个典型的操作系统权衡:内存访问与 CPU 周期。

2.3.2  哈希表

链表是将数据结构捆绑在一起的便捷方法,但导航链表可能效率低下。如果您正在搜索特定的元素,您可能需要查看整个列表才能找到您需要的元素。Linux 使用另一种技术,哈希来绕过这种限制。哈希表是一个指针数组向量。数组或向量只是内存中一个接一个排列的一组事物。书架可以说是一个书的数组。数组通过索引访问,索引是数组中的偏移量。将书架类比更进一步,您可以用书在书架上的位置来描述每本书;您可以要求第 5 本书。

哈希表是指向数据结构的指针数组,其索引来自这些数据结构中的信息。如果您有描述村庄人口的数据结构,那么您可以使用一个人的年龄作为索引。要查找特定人员的数据,您可以使用他们的年龄作为人口哈希表的索引,然后跟随指针到达包含该人详细信息的数据结构。不幸的是,村庄中的许多人可能具有相同的年龄,因此哈希表指针成为指向链或数据结构列表的指针,每个数据结构都描述了相同年龄的人。但是,搜索这些较短的链仍然比搜索所有数据结构更快。

由于哈希表加快了对常用数据结构的访问速度,Linux 经常使用哈希表来实现缓存。缓存是需要快速访问的便捷信息,通常是可用信息的完整子集。数据结构被放入缓存并保存在那里,因为内核经常访问它们。缓存的缺点在于,与简单的链表或哈希表相比,它们的使用和维护更加复杂。如果在缓存中可以找到数据结构(这被称为缓存命中),那么一切都很好。如果找不到,则必须搜索所有相关的数据结构,并且如果数据结构确实存在,则必须将其添加到缓存中。在将新数据结构添加到缓存中时,可能需要丢弃旧的缓存条目。Linux 必须决定丢弃哪个,危险在于丢弃的数据结构可能是 Linux 下一个需要的。

2.3.3  抽象接口

Linux 内核经常抽象其接口。接口是以特定方式运行的例程和数据结构的集合。例如,所有网络设备驱动程序都必须提供某些例程,在这些例程中操作特定的数据结构。这样,就可以有通用的代码层,使用较低层的特定代码的服务(接口)。网络层是通用的,它由符合标准接口的设备特定代码支持。

通常,这些较低层在启动时向较高层注册自己。此注册通常涉及将数据结构添加到链表中。例如,每个内置到内核中的文件系统在启动时向内核注册自己,或者,如果您正在使用模块,则在首次使用文件系统时注册。您可以通过查看文件来查看哪些文件系统已注册自己/proc/filesystems。注册数据结构通常包括指向函数的指针。这些是指向执行特定任务的软件函数的地址。再次以文件系统注册为例,每个文件系统在注册时传递给 Linux 内核的数据结构包括一个文件系统特定例程的地址,每当挂载该文件系统时都必须调用该例程。


文件由 TEX 翻译而来,通过 TTH, 版本 1.0。
章节顶部, 目录, 显示框架, 无框架
� 1996-1999 David A Rusling 版权声明