一直以来,我们都在讨论是该学VC还是 BCB还是 VB 还是JAVA。其实,对于真正要立志于从事程序设计或信息安全事业的同志来说,这些讨论根本就没有意义!我们到底该学什么?我们 应该从何开始?我个人认为,应该从操作系统开始!——去真正的认识操作系统。了解、分析它的工作是怎么样实现的。我们可以这样说 ,操作系统就是一个最庞大功能最强的程序。那么,抛开各种功利的诱惑,定下心来认认真真地从操作系统学起,才能掌握编程的真谛。 在操作统种,几乎每一个知识点都被用上了。当你真正研究了它、阅读了它的代码后,那么,象算法、数据结构、内存管理、网络协议、 进程管理。。。。之类编程的核心问题也就被你了如指掌了。到那时,写出高质量的代码不就不在话下了吗?(建议装liunx 或unix) :(各位初学者:看不懂也要硬着头皮看下去。切记!切记!): 一。硬件基础: 1.1 CPU CPU,或者微处理器,是计算机系统的核心。微处理器进行计算或者逻辑操作并且管理来自主存的指令并执行它。在计算机的早期时代 ,微处理器的功能部件使用的是分立元件(外型很大)。 这就是中央处理单元这一名词的由来。现代微处理器将部件结合到小型硅片上的集成电路中。在本书中CPU和微处理器及处理器有相同 的意义。 微处理器的操作对象是二进制数据;数据由0和1组成。 1和0对应着电子开关的开路与断路状态。正如十进制的42表示有4个10和一个2一样,一个二进制数是一系列表示2的次幂的二进 制数字组成。二进制0001对应十进制的1,二进制的0010对应十进制的2,二进制的0011表示3,而0100对应4。十进 制42的二进制表示为101010。但是在计算机程序中, 人们常用十进制来表示数而不是直接使用二进制。 在需要使用二进制数时,人们往往使用16进制数。如十进制数只能从0到9一样,16进制数可以从0疏导15,其中10到15分别 用字母A、B、C、D、E及F来表示。这样16进制的2A的十进制表示为42-2*16+10=42。在C程序语言中,16进制 数的前缀为"0x";16进制的2A写成0x2A。 微处理器可以执行如加、乘和除以及象"X是否比Y大"这种逻辑运算。 处理器的执行由外部时钟来监控。这个时钟称为系统时钟,它每隔相同的时间间隔就向CPU发送一个脉冲。在每个时钟脉冲上,处理器 都会做一些工作。比如,处理器每个时钟脉冲上执行一条指令。处理器的速度一般以系统时钟的速率来描叙。一个100MHz的处理器 每秒将接收100,000,000 个时钟滴答。但是用CPU的时钟频率来描叙CPU的工作能力是不正确的,因为它们执行的指令不相同。 然而,快速的时钟可以在某种程度上代表高性能的CPU。处理器执行的指令是非常简单的;例如"将内存X处的内容读入寄存器Y"。 寄存器是微处理器的内部存储部件,用来存储数据并对数据执行某些指令。有些指令有可能使处理器停止当前的工作而跳转到内存中另外 一条指令执行。现代微处理器的紧凑设计使得它有可能每秒执行上百万甚至亿条指令。 指令执行前必须从内存中取出来。指令自身要使用的数据也必须从内存中取出来并放置在适当的地方。 微处理器中寄存器的大小、数量以及类型都取决于微处理器的类型。Intel 80486处理器和Alpha AXP 有迥然不同的寄存器,最明显的区别在于Intel 寄存器为32位而Alpha AXP为64位。一般来说,任何处理器都有许多通用寄存器和少量专用寄存器。许多微处理器有以下几种特定的寄存器。 程序计数器(PC) 此寄存器包含下条指令执行的地址。每当取回一条指令时,PC的内容将自动增加。 堆栈指针(SP) 微处理器经常需要访问存储临时数据的外部RAM。堆栈是一种便捷的存放临时数据的方法,处理器提供了特殊指令来将数值压入堆栈然 后将其从堆栈中弹出。 堆栈以后进先出(LIFO)的方式工作。换句话说,如果你压入两个值X和Y,然后执行弹栈操作,你将取到Y的值。 有些处理器的堆栈从内存顶部向下增长而有些相反。但有的处理器同时支持这两种方式,如ARM。 处理机状态字(PS) 指令的执行将得到执行结果;比如"寄存器X中的内容要大于寄存器Y中的内容?"将得到正确或错误作为结果。PS寄存器包含着这些 信息及有关处理器当前状态的其他信息。例如大多数处理器至少有两种执行方式,核心(或管态)与用户方式。PS寄存器包含表示当前 执行方式的信息。 1.2 内存 所有计算机系统都有一个由不同速度与大小的存储器组成的层次结构。最快的的存储器是高速缓存,它被用来暂存主存中的内容。这种存 储器速度非常快但非常昂贵,大多数处理器都有少量的片上高速缓存或者将其放在主板上。有些处理器的高速缓存既包含数据也包含指令 ,但有些将其分成两部分。 Alpha AXP处理器有两个内部高速缓存,一个用来缓存数据(D-Cache)而另一个用来缓存指令(I- Cache)。而外部高速缓存(B-Cache)将两者混合。这样,相对外部高速缓存存储器,主存的速度非常慢。 高速缓存与主存中的内容必须保持一致。换句话说,对应于地址空间的同一个位置,如果该位置的数据被缓存入高速缓存,则其内容必须 和主存中的一致。保证高速缓存一致性的工作由硬件和操作系统共同分担。 这就是在系统中硬件和软件必须紧密协作的原因。 1.3 总线 主板上分立的部件通过称为总线的线路连接在一起。系统总线的功能在逻辑上被划分为三部分: 地址总线、数据总线和控制总线。地址总线为数据传输指明内存位置(地址)。数据总线包含传输的数据。数据总线是双向的;它允许数 据读入CPU也支持从CPU读出来。控制总线则包含几条表示路由分时和系统的控制信号。当然还有其他一些总线存在,例如ISA和 PCI总线是将外设连接到系统的常用方式。 1.4 控制器与外设 外设是一些物理设备,比如说图象卡或者磁盘,它们受控于位于主板或者主板上插槽中的控制芯片。 IDE磁盘被IDE控制器芯片控制而SCSI磁盘由SCSI磁盘控制器芯片控制。这些控制器通过各种总线连接到CPU上或相互间 互连。目前制造的大多数系统使用PCI和ISA总线来连接主要系统部件。控制器是一些类似CPU的处理器,它们可以看做CPU的 智能帮手。CPU则是系统的总控。 虽然所有这些控制器互不相同,但是它们的寄存器的功能类似。运行在CPU上的软件必须能读出或者写入这些控制寄存器。其中有一个 寄存器可能包含指示错误的状态码。另一个则用于控制目的,用来改变控制器的运行模式。在总线上的每个控制器可以被CPU所单独寻 址,这是软件设备驱动程序能写入寄存器并能控制这些控制器的原因。 1.5 地址空间 系统总线将CPU与主存连接在一起并且和连接CPU与系统硬件外设的总线隔离开。一般来说,硬件外设存在的主存空间叫I/O空间 。I/O空间还可以进一步细分,但这里我们不再深究。CPU既可以访问系统内存空间又可以访问I/O空间内存,而控制器自身只能 在CPU协助下间接的访问系统内存。从设备的角度来看,比如说软盘控制器,它只能看到在ISA总线上的控制寄存器而不是系统内存 。典型的CPU使用不同指令来访问内存与I/O空间。例如,可能有一条指令"将I/O地址0x3F0的内容读入到寄存器X"。这 正是CPU控制系统硬件设备的方式:通过读写I/O地址空间上的外设寄存器。在I/O空间中通用外设(IDE控制器、串行口、软 盘控制器等等)上的寄存器经过多年的PC体系结构发展基本保持不变。I/O地址空间0x3f0是串行口(COM1)的控制寄存器 之一。 有时控制器需要直接从系统主存中读写大量数据。例如当用户将数据写入硬盘时。在这种情况下,直接内存访问(DMA)控制器将用来 允许硬件外设直接访问系统主存,不过这将处于CPU的严格监控下。 1.6 时钟 所有的操作系统都必须准确的得到当前时间,所以现代PC包含一个特殊的外设称为实时时钟(RTC)。它提供了两种服务:可靠的日 期和时间以及精确的时间间隔。RTC有其自身的电池这样即使PC掉电时它照样可以工作,这就是PC总是"知道"正确时间和日期的 原因。而时间间隔定时器使得操作系统能进行准确的调度工作。 二。 软件基础 程序是执行某个特定任务的计算机指令集合。程序可以用多种程序语言来编写:从低级计算机语言-汇编语言到高级的、与机器本身无关 的语言入C程序语言。操作系统是一个允许用户运行如电子表格或者字处理软件等应用程序的特殊程序。本章将介绍程序设计的基本原则 ,同时给出操作系统设计目标与功能的概述。 2.1 计算机编程语言 2.1.1 汇编语言 那些CPU从主存读取出来执行的指令对人类来说是根本不可理解的。它们是告诉计算机如何准确动作的机器代码。在Intel 80486指令中16进制数0x89E5表示将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 第一行语句将寄存器15所指示的地址中的值加载到寄存器16中。接下来将邻接单元内容加载到寄存器17中。 第三行语句比较寄存器16和寄存器17中的值,如果相等则跳转到标号100处,否则继续执行第四行语句:将寄存器17的内容存入 内存中。如果寄存器中值相等则无须保存。汇编级程序一般冗长并且很难编写,同时还容易出错。 Linux核心中只有很少一部分是用汇编语言编写,并且这些都是为了提高效率或者是需要兼容不同的CPU。 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支持许多类型的变量,变量是一个通过符号名称引用的内存位置。在以上的例子中,X和Y都是内存中的位置。程序员并不关心变量放 在什么地方,这些工作由连接程序来完成。有些变量包含不同类型的数据,整数和浮点数,以及指针。 指针是那些包含其他数据内存位置或者地址的变量。假设有变量X,位于内存地址0x80010000处。你可以使用指针变量px来 指向X,则px的值为0x80010000。 C语言允许相关变量组合起来形成数据结构,例如: struct { int i ; char b ; } my_struct ; 这是一个叫做my_struct的结构,它包含两个元素,一个是32位的整数i,另外一个是8位的字符b。 2.1.3 连接程序 连接程序是一个将几个目标模块和库过程连接起来形成单一程序的应用。目标模块是从汇编器或者编译器中产生的机器代码,它包含可执 行代码和数据,模块结合在一起形成程序。例如一个模块可能包含程序中所有的数据库函数而另一个主要处理命令行参数。连接程序修改 目标模块之间的引用关系,使得在某一模块中引用的数据或者子程序的确存在于其他模块中。Linux核心是由许多目标模块连接形成 的庞大程序。 2.2 操作系统概念 如果没有软件,计算机只不过是一堆发热的电子器件。如果将硬件比做计算机的心脏则软件就是它的灵魂。操作系统是一组系统程序的集 合,它提供给用户运行应用软件的功能。操作系统对系统硬件进行抽象,它提供给系统用户一台虚拟的机器。大多数PC可以运行一种或 者多种操作系统,每个操作系统都有不同的外观。Linux由许多独立的功能段组成。比如Linux内核,如果没有库函数和外壳程 序,内核是没有什么用的。 为了理解操作系统到底是什么,思考一下当你敲入一个简单命令时,系统中发生了什么: $ ls Mail c images perl docs tcl $ $符号是由用户登录外壳(这里指Bash)提供的提示符。它表示正在等待用户敲入一些命令。敲入ls命令,首先键盘驱动程序识别 出敲入的内容。然后键盘驱动将它们传递给外壳程序,由外壳程序来负责查找同名的可执行程序(ls)。 如果在/bin/ls目录中找到了ls,则调用核心服务将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一样,系统中的独立文件系统不是通过设备标志符来访问,而是通过表示文件系统的层次树结构来访问。当Lin ux添加一个新的文件系统到系统中时,会将它mount到一个目录下,比如说/mnt/cdrom。 Linux的一个重要特征就是支持多种文件系统。这使得它非常灵活并且可与其他操作系统并存。Linux中最常用的文件系统是E XT2文件系统,它在大多数Linux分发版本中都得到了支持。文件系统提供给用户一个关于系统的硬盘上文件和目录的总体映象, 而不管文件的类型和底层物理设备的特性。 Linux透明地支持多种文件系统并将当前安装的所有文件和文件系统集成到虚拟文件系统中去。所以,用户和进程一般都不知道某个 文件位于哪种文件系统中,他们只是使用它。 块设备驱动将物理块设备类型(例如IDE和SCSI)和文件系统中的差别隐藏起来,物理设备只是数据块的线性存储集合。设备的不 同导致块大小的不同,从软盘设备的512字节到IDE磁盘的1024字节。这些都隐藏了起来,对系统用户来说这都是不可见的。不 管设备类型如何,EXT2文件系统看起来总是一样。 2.3 核心数据结构 操作系统可能包含许多关于系统当前状态的信息。当系统发生变化时,这些数据结构必须做相应的改变以反映这些情况。例如,当用户登 录进系统时将产生一个新的进程。核心必须创建表示新进程的数据结构,同时将它和系统中其他进程的数据结构连接在一起。 大多数数据结构存在于物理内存中并只能由核心或者其子系统来访问。数据结构包括数据和指针;还有其他数据结构的地址或者子程序的 地址。它们混在一起让Linux核心数据结构看上去非常混乱。尽管可能被几个核心子系统同时用到,每个数据结构都有其专门的用途 。理解Linux核心的关键是理解它的数据结构以及Linux核心中操纵这些数据结构的各种函数。本书把Linux核心的描叙重 点放在数据结构上,主要讨论每个核心子系统的算法,完成任务的途径以及对核心数据结构的使用。 2.3.1 连接列表 Linux使用的许多软件工程的技术来连接它的数据结构。在许多场合下,它使用linked或者chained数据结构。每个数 据结构描叙某一事物,比如某个进程或网络设备,核心必须能够访问到所有这些结构。在链表结构中,个根节点指针包含第一个结构的地 址,而在每个结构中又包含表中下一个结构的指针。表的最后一项必须是0或者NULL,以表明这是表的尾部。在双向链表中,每个结 构包含着指向表中前一结构和后一结构的指针。使用双向链表的好处在于更容易在表的中部添加与删除节点,但需要更多的内存操作。这 是一种典型的操作系统开销与CPU循环之间的折中。 2.3.2 散列表 链表用来连接数据结构比较方便,但链表的操作效率不高。如果要搜寻某个特定内容,我们可能不得不遍历整个链表。Linux使用另 外一种技术:散列表来提高效率。散列表是指针的数组或向量,指向内存中连续的相邻数据集合。散列表中每个指针元素指向一个独立链 表。如果你使用数据结构来描叙村子里的人,则你可以使用年龄作为索引。为了找到某个人的数据,可以在人口散列表中使用年龄作为索 引,找到包含此人特定数据的数据结构。但是在村子里有很多人的年龄相同,这样散列表指针变成了一个指向具有相同年龄的人数据链表 的指针。搜索这个小链表的速度显然要比搜索整个数据链表快得多。 由于散列表加快了对数据结构的访问速度,Linux经常使用它来实现Caches。Caches是保存经常访问的信息的子集。经 常被核心使用的数据结构将被放入Cache中保存。Caches的缺点是比使用和维护单一链表和散列表更复杂。寻找某个数据结构 时,如果在Cache中能够找到(这种情况称为cache 命中),这的确很不错。但是如果没有找到,则必须找出它,并且添加到Cache中去。如果Cache空间已经用完则Linux必 须决定哪一个结构将从其中抛弃,但是有可能这个要抛弃的数据就是Linux下次要使用的数据。 2.3.3 抽象接口 Linux核心常将其接口抽象出来。接口指一组以特定方式执行的子程序和数据结构的集合。例如,所有的网络设备驱动必须提供对某 些特定数据结构进行操作的子程序。通用代码可能会使用底层的某些代码。例如网络层代码是通用的,它得到遵循标准接口的特定设备相 关代码的支持。 通常在系统启动时,底层接口向更高层接口注册(Register)自身。这些注册操作包括向链表中加入结构节点。例如,构造进核 心的每个文件系统在系统启动时将其自身向核心注册。文件/proc/filesysems中可以看到已经向核心注册过的文件系统 。注册数据结构通常包括指向函数的指针,以文件系统注册为例,它向Linux核心注册时必须将那些mount文件系统连接时使用 的一些相关函数的地址传入。 |
|