来自:蓝森林自由软件
Chapter 2
Software Basic( 软件基础 )
Linux Kernel核心中文手册
来自:蓝森林自由软件
Chapter 2
Software Basic( 软件基础 )
程序是用于执行特定任务的计算机指令组合。程序可以用汇编语言,一种非常低级的计算机语言来编写,也可以使用和机器无关的高级语言,比如 C 语言编写。操作系统是一个特殊的程序,允许用户通过它运行应用程序,比如电子表和文字处理等等。本章介绍了基本的编程原理,并简介操作系统的目的和功能。
2.1 Computer Languages( 计算机语言 )
2.1.1. 汇编语言
CPU 从内存中读取和执行的指令对于人类来讲无法理解。它们是机器代码,精确的告诉计算机要做什么。比如十六进制数 0x89E5 ,是 Intel 80486 的指令,将寄存器 ESP 的内容拷贝到寄存器 EBP 中。早期计算机中最初的软件工具之一是汇编程序,它读入人类可以阅读的源文件,将其装配成机器代码。汇编语言明确地处理对寄存器和对数据的操作,而这种操作对于特定的微处理器而言是特殊的。 Intel X86 微处理器的汇编语言和 Alpha AXP 微处理器的汇编语言完全不同。以下 Alpha AXP 汇编代码演示了程序可以执行的操作类型:
Ldr r16, (r15) ; 第一行
Ldr r17, 4(r15) ; 第二行
Beq r16,r17,100; 第三行
Str r17, (r15); 第四行
100: ; 第五行
第一条语句(第一行)将寄存器 15 指定的地址中的内容加载到寄存器 16 中。第二条指令将紧接着的内存中的内容加载到寄存器 17 中。第三行比较寄存器 16 和寄存器 17 ,如果相等,分支到标号 100 ,否则,继续执行第四行,将寄存器 17 的内容存到内存中。如果内存中的数据相同,就不必存储数据。编写汇编级的程序需要技巧而且十分冗长,容易出错。 Linux 系统的核心很少的一部分是用汇编语言编写,而这些部分之所以使用汇编语言只是为了提高效率,并且和具体的微处理器相关。
2.1.2 The C Programming Language and Compiler (C 语言和编译器 )
使用汇编语言编写大型程序十分困难,消耗时间,容易出错而且生成的程序不能移植,只能束缚在特定的处理器家族。更好的选择是使用和机器无关的语言,例如 C 。 C 允许你用逻辑算法描述程序和要处理的数据。被称为编译程序( compiler )的特殊程序读入 C 程序,并将它转换为汇编语言,进而产生机器相关的代码。好的编译器生成的汇编指令可以和好的汇编程序员编写的程序效率接近。大部分 Linux 核心是用 C 语言编写的。以下的 C 片断:
if (x != y)
x = y;
执行了和前面示例中汇编代码完全一样的操作。如果变量 x 的内容和变量 y 的内容不一样,变量 y 的内容被拷贝到变量 x 。 C 代码用例程( routine )进行组合,每一个例程执行一项任务。例程可以返回 C 所支持的任意的数值或数据类型。大型程序比如 Linux 核心分别由许多的 C 语言模块组成,每一个模块有自己的例程和数据结构。这些 C 源代码模块共同构成了逻辑功能比如文件系统的处理代码。
C 支持多种类型的变量。一个变量是内存中的特定位置,可用符号名引用。上述的 C 片断中, x 和 y 引用了内存中的位置。程序员不需要关心变量在内存中的具体位置,这是连接程序(下述)必须处理的。一些变量包含不同的数据例如整数、浮点数等和另一些则包含指针。
指针是包含其它数据在内存中的地址的变量。假设一个变量 x ,位于内存地址 0x80010000 , 你可能有一个指针 px ,指向 x 。 Px 可能位于地址 0x80010030 。 Px 的值则是变量 x 的地址, 0x80010000 。
C 允许你将相关的变量集合成为结构。例如:
Struct {
Int I;
Char b;
} my_struct;
是一个叫做 my_struct 的数据结构,包括两个元素:一个整数( 32 位) I 和一个字符( 8 位数据) b 。
2.1.3 Linkers (连接程序)
连接程序将几个目标模块和库文件连接在一起成为一个单独的完整程序。目标模块是汇编程序或编译程序的机器码输出,它包括机器码、数据和供连接程序使用的连接信息。比如:一个目标模块可能包括程序的所有数据库功能,而另一个目标模块则包括处理命令行参数的函数。连接程序确定目标模块之间的引用关系,即确定一个模块所引用的例程和数据在另一个模块中的实际位置。 Linux 核心是由多个目标模块连接而成的独立的大程序。
2.2 What is an Operating System (什么是操作系统?)
没有软件,计算机只是一堆发热的电子元件。如果说硬件是计算机的心脏,则软件就是它的灵魂。操作系统是允许用户运行应用程序的一组系统程序。操作系统将系统的硬件抽象,呈现在用户和应用程序之前的是一个虚拟的机器。是软件造就了计算机系统的特点。大多数 PC 可以运行一到多个操作系统,而每一个操作系统从外观和感觉上都大不相同。 Linux 由不同功能的部分构成,这些部分总体组合构成了 Linux 操作系统。 Linux 最明显的部分就是 Kernel 自身,但是如果没有 shell 或 libraries 一样没有用处。
为了了解什么是操作系统,看一看在你输入最简单的命令时发生了什么:
$ls
Mail c images perl
Docs tcl
$
这里的 $ 是登录的 shell 输出的提示符(此例是 bash ):表示 shell 在等候你(用户)输入命令。输入 ls 引发键盘驱动程序识别输入的字符,键盘驱动程序将识别的字符传递给 shell 去处理。 shell 先查找同名的可执行映象,它找到了 /bin/ls, 然后调用核心服务将 ls 执行程序加载到虚拟内存中并开始执行。 ls 执行程序通过执行核心的文件子系统的系统调用查找文件。文件系统可能使用缓存的文件系统信息或通过磁盘设备驱动程序从磁盘上读取文件信息 , 也可能是通过网络设备驱动程序同远程主机交换信息而读取本系统所访问的远程文件的详细信息(文件系统可以通过 NFS 网络文件系统远程安装)。不管文件信息是如何得到的, ls 都将信息输出,通过显示驱动程序显示在屏幕上。
以上的过程看起来相当复杂,但是它说明了即使是最简单的命令也是操作系统各个功能模块之间共同协作的结果,只有这样才能提供给你(用户)一个完整的系统视图。
2.2.1 Memory management (内存管理)
如果拥有无限的资源,例如内存,那么操作系统所必须做的很多事情可能都是多余的。所有操作系统的一个基本技巧就是让少量的物理内存工作起来好像有相当多的内存。这种表面看起来的大内存叫做虚拟内存,就是当软件运行的时候让它相信它拥有很多内存。系统将内存分为容易处理的页,在系统运行时将这些页交换到硬盘上。而应用软件并不知道,因为操作系统还使用了另一项技术:多进程。
2.2.2 Processes ( 进程 )
进程可以看作一个在执行的程序,每一个进程都是正在运行的特定的程序的独立实体。如果你观察一下你的 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 上运行。不幸的是,只有一个,所以操作系统又使用技巧,在短时间内依次运行每一个进程。这个时间段叫做时间片。这种技巧叫做多进程或调度,它欺骗了每一个进程,好像它们是唯一的进程。进程相互之间受到保护,所以如果一个进程崩溃或不能工作,不会影响其他进程。操作系统通过给每一个进程一个独立的地址空间来实现保护,进程只能访问它自己的地址空间。
2.2.3 Device Drivers (设备驱动程序)
设备驱动程序组成了 Linux 核心的主要部分。象操作系统的其他部分一样,它们在一个高优先级的环境下工作,如果发生错误,可能会引发严重问题。设备驱动程序控制了操作系统和它控制的硬件设备之间的交互。比如:文件系统向 IDE 磁盘写数据块是使用通用块设备接口。驱动程序控制细节,并处理和设备相关的部分。设备驱动程序和它驱动的具体的控制器芯片相关,所以,如果你的系统有一个 NCR810 的 SCSI 控制器,那么你需要 NCR810 的驱动程序。
2.2.4 The Filesystems (文件系统)
象 Unix 一样,在 Linux 里,系统对独立的文件系统不是用设备标示符来存取(比如驱动器编号或驱动器名称),而是连接成为一个树型结构。 Linux 在安装新的文件系统时,把它安装到指定的安装目录,比如 /mnt/cdrom ,从而合并到这个单一的文件系统树上。 Linux 的一个重要特征是它支持多种不同的文件系统。这使它非常灵活而且可以和其他操作系统良好共存。 Linux 最常用的文件系统是 EXT2 ,大多数 Linux 发布版都支持。
文件系统将存放在系统硬盘上的文件和目录用可以理解的统一的形式提供给用户,让用户不必考虑文件系统的类型或底层物理设备的特性。 Linux 透明的支持多种文件系统(如 MS-DOS 和 EXT2 ),将所有安装的文件和文件系统集合成为一个虚拟的文件系统。所以,用户和进程通常不需要确切知道所使用的文件所在的文件系统的类型,用就是了。
块设备驱动程序掩盖了物理块设备类型的区别(如 IDE 和 SCSI )。对于文件系统来讲,物理设备就是线性的数据块的集合。不同设备的块大小可能不同,如软驱一般是 512 字节,而 IDE 设备通常是 1024 字节,同样,对于系统的用户,这些区别又被掩盖。 EXT2 文件系统不管它用什么设备,看起来都是一样的。
2.3 Kernet Data Structures (核心数据结构)
操作系统必须纪录关于系统当前状态的许多信息。如果系统中发生了事情,这些数据结构就必须相应改变以反映当前的实际情况。例如:用户登录到系统中的时候,需要创建一个新的进程。核心必须相应地创建表示此新进程的数据结构,并和表示系统中其他进程的数据结构联系在一起。
这样的数据结构多数在物理内存中,而且只能由核心和它的子系统访问。数据结构包括数据和指针(其他数据结构或例程的地址)。乍一看, Linux 核心所用的数据结构可能非常混乱。其实,每一个数据结构都有其目的,虽然有些数据结构在多个的子系统中都会用到,但是实际上它们比第一次看到时的感觉要简单的多。
理解 Linux 核心的关键在于理解它的数据结构和核心处理这些数据结构所用到的大量的函数。本书以数据结构为基础描述 Linux 核心。论及每一个核心子系统的算法,处理的方式和它们对核心数据结构的使用。
2.3.1 Linked Lists (连接表)
Linux 使用一种软件工程技术将它的数据结构连接在一起。多数情况下它使用链表数据结构。如果每一个数据结构描述一个物体或者发生的事件的单一的实例,比如一个进程或一个网络设备,核心必须能够找出所有的实例。在链表中,根指针包括第一个数据结构或单元的地址,列表中的每一个数据结构包含指向列表下一个元素的指针。最后元素的下一个指针可能使 0 或 NULL ,表示这是列表的结尾。在双向链表结构中,每一个元素不仅包括列表中下一个元素的指针,还包括列表中前一个元素的指针。使用双向链表可以比较容易的在列表中间增加或删除元素,但是这需要更多的内存存取。这是典型的操作系统的两难情况:内存存取数还是 CPU 的周期数。
2.3.2 Hash Tables
链接表是常用的数据结构,但是游历链接表的效率可能并不高。如果你要寻找指定的元素, 可能必须查找完整个表才能找到。 Linux 使用另一种技术: Hashing 来解决这种局限。 Hash table 是指针的数组或者说向量表。数组或向量表是在内存中依次存放的对象。书架可以说是书的数组。数组用索引来访问,索引是数组中的偏移量。再来看书架的例子,你可以使用在书架上的位置来描述每一本书:比如第 5 本书。
Hash table 是一个指向数据结构的指针的数组,它的索引来源于数据结构中的信息。如果你用一个数据结构来描述一个村庄的人口,你可以用年龄作为索引。要找出一个指定的人的数据,你可以用他的年龄作为索引在人口散列表中查找,通过指针找到包括详细信息的数据结构。不幸的是,一个村庄中可能很多人年龄相同,所以散列表的指针指向另一个链表数据结构,每一个元素描述同龄人。即使这样,查找这些较小的链表仍然比查找所有的数据结构要快。
Hash table 可用于加速常用的数据结构的访问,在 Linux 里常用 hash table 来实现缓冲。缓冲是需要快速存取的信息,是全部可用信息的一个子集。数据结构被放在缓冲区并保留在那里,因为核心经常访问这些结构。使用缓冲区也有副作用,因为使用起来比简单链表或者散列表更加复杂。如果数据结构可以在缓冲区找到(这叫做缓冲命中),那么一切很完美。但是如果数据结构不在缓冲区中,那么必须查找所用的相关的数据结构,如果找到,那么就加到缓冲区中。增加新的数据结构到缓冲区中可能需要废弃一个旧的缓冲入口。 Linux 必须决定废弃那一个数据结构,风险在于废弃的可能使 Linux 下一个要访问的数据结构。
2.3.3 Abstract Interfaces (抽象接口)
Linux 核心经常将它的接口抽象化。接口是以特定方式工作的一系列例程和数据结构。比如:所有的网络设备驱动程序都必须提供特定的例程来处理特定的数据结构。用抽象接口的方式可以用通用的代码层来使用底层特殊代码提供的服务(接口)。例如网络层是通用的,而它由底层符合标准接口的同设备相关的代码提供支持。
通常这些底层在启动时向高一层登记。这个登记过程常通过在链接表中增加一个数据结构来实现。例如,每一个连结到核心的文件系统在核心启动时进行登记(或者如果你使用模块,在文件系统第一次使用时向核心登记)。你可以查看文件 /proc/filesystems 来检查那些文件系统进行了登记。登记所用的数据结构通常包括指向函数的指针。这是执行特定任务的软件函数的地址。再一次用文件系统登记的例子,每一个文件系统登记时传递给 Linux 核心的数据结构都包括一个和具体文件系统相关的例程地址,在安装文件系统时必须调用。
文章来源于领测软件测试网 https://www.ltesting.net/