译文如有不妥,或有任何反馈意见可发豆邮给我。
第2章 基本概念
本章旨在向LINUX和UNIX“生手”们介绍一系列与LINUX系统编程有关的概念。
2.1 操作系统的核心——内核
术语“操作系统”通常包含两种不同涵义:
1、指完整的软件包,这包括用来管理计算机资源的核心层软件,以及附带的所有标准软件工具,诸如,命令行解释器、图形用户界面、文件操作工具和文本编辑器等。
2、在更狭义的范围内,是指管理和分配计算机资源(即CPU、RAM和设备)的核心层软件。
术语“内核”通常是第二种涵义,本书中的“操作系统”一词也是这层意思。
虽然在没有内核的情况下,计算机也能运行程序,但有了内核会极大简化其它程序的编写和使用,令程序员“功力”大进、游刃有余。这要归功于内核为管理计算机的有限资源所提供的软件层。
一般情况下,LINUX内核可执行文件采用/boot/vmlinuz或与之类似的路径名。而文件名的来历也颇有渊源。早期的UNIX实现称其内核为UNIX。在后续实现了虚拟内存机制的UNIX系统中,其内核名称变更为vmunix。对LINUX来说,文件名称中的系统名需要调整,而以“z”替换“linux”末尾的“x”,意在表明内核是经过压缩的可执行文件。
内核的职责
内核所能执行的主要任务有:
•进程调度:计算机均配有一个或多个CPU(中央处理单元),以执行程序指令。与其它UNIX系统一样,LINUX属于抢占式多任务操作系统。“多任务”意指多个进程(即运行中的程序)可同时驻留于内存,且每个进程都能获得对CPU的使用权。“抢占”则是指一组规则:这组规则控制着哪些进程获得对CPU的使用,以及每个进程能使用多长时间,这两者都由内核进程调度程序(而非进程本身)决定。
•内存管理:以一、二十年前的标准来看,如今计算机的内存容量可谓相当可观,但软件的规模也保持了相应地增长,故而物理内存(RAM)仍然属于有限资源,内核必须以公平、高效地方式在进程间共享这一资源。与大多数现代操作系统一样,LINUX也采用了虚拟内存管理机制(6.4节),这项技术主要具有以下两方面的优势:
——进程与进程之间、进程与内核之间彼此隔离,因此一个进程无法读取/修改内核或其他进程的内存内容。
——只需将进程的一部分保存在内存中,这不但降低了每个进程对内存的需求量,而且还能在RAM中同时加载更多的进程。这也大幅提升了如下事件的发生概率:在任一时刻,CPU都有至少一个进程可以执行——从而使得对CPU资源的利用更加充分。
•提供了文件系统:内核在磁盘之上提供有文件系统,允许对文件执行创建、获取、更新以及删除等操作。
•创建和终止进程:内核可将新程序载入内存,为其提供运行所需的资源(比如,CPU、内存以及对文件的访问等)。这样一个运行中的程序我们称之为“进程”。一旦进程执行完毕,内核还要确保释放其占用的资源,以供后续程序重新使用。
•对设备的访问:计算机外接设备(鼠标、键盘,磁盘和磁带驱动器等)可实现计算机与外部世界的通信,这一通信机制包括:输入、输出或是两者兼而有之。内核既为程序访问设备提供了简化版的标准接口,同时还要仲裁多个进程对每一设备的访问。
•联网:内核以用户进程的名义收发网络消息(数据包)。该任务包括将网络数据包路由至目标系统。
•提供系统调用应用编程接口(API):进程可利用内核入口点(也称为系统调用)请求内核去执行各种任务。LINUX系统调用API是本书的主题。3.1节会详细描述进程在执行系统调用时所经历的步骤。
除了上述特性外,一般而言,诸如LINUX之类的多用户操作系统会为每个用户营造一种抽象:虚拟私有计算机(virtual private computer),这就是说,每个用户都可登录进系统、独立操作,而与其他用户大致无干。例如,每个用户都有属于自己的磁盘存储空间(主目录)。再者,用户能够运行程序,而每一程序都能从CPU资源中“分得一杯羹”、运转于自有的虚拟地址空间中。而且这些程序还能独立访问设备,并通过网络传递信息。内核负责解决(多进程)访问硬件资源时可能引发的冲突,用户和进程对此则往往一无所知。
内核态和用户态
现代处理器架构一般允许CPU至少在两种不同状态下运行,即:用户态和核心态(有时也称之为监管态[supervisor mode])。执行硬件指令可使CPU在两种状态间来回切换。与之对应,可将虚拟内存区域划分(标记)为用户空间部分或内核空间部分。在用户态下运行时,CPU只能访问被标记为用户空间的内存;试图访问属于内核空间的内存会引发硬件异常。当运行于核心态时,CPU既能访问用户空间内存,也能访问内核空间内存。
仅当处理器在核心态运行时,才能执行某些特定操作。这样的例子包括:执行宕机(halt)指令去关闭系统、访问内存管理硬件,以及设备I/O操作的初始化等。实现者们利用这一硬件设计,将操作系统置于内核空间——这确保了用户进程既不能访问内核指令和数据结构,也无法执行不利于系统运行的操作。
以进程及内核视角检视系统
在完成诸多日常编程任务时,程序员们习惯于以面向进程(process-oriented)的思维方式来考虑编程问题。然而,在研究本书后续所涵盖的各种主题时,读者有必要转换视角——站在内核的角度上来看问题。为凸显二者间的差异,本书接下来会分别从进程和内核视角来检视系统。
一个运行系统通常会有多个进程并行其中。对进程来说,许多事件的发生都无法预期。执行中的进程不清楚自己对CPU的占用何时“到期”、系统随之又会调度哪个进程来使用CPU(以及以何种顺序来调度),也不知道自己何时会再次获得对CPU的使用。信号的传递和进程间通信事件的触发由内核统一协调,对进程而言,随时可能发生。诸如此类,进程都一无所知。进程不清楚自己在RAM中的位置,或者换种更通用的说法:进程内存空间的某块特定部分如今到底是驻留在内存中还是被保存在交换空间(磁盘空间中的保留区域,作为计算机RAM的补充)里,进程本身并不知晓。与之类似,进程也闹不清自己所访问的文件“居于”磁盘驱动器的何处;进程只是通过名称来引用文件而已。进程的运作方式堪称“与世隔绝”——进程间彼此不能直接通信。进程本身无法创建出新进程,哪怕“自行了断”都不行。最后还有一点,进程也不能与计算机外接的输入输出设备直接通信。
相形之下,内核则是运行系统的中枢所在,对于系统的一切无所不知、无所不能,为系统上所有进程的运行提供便利。由哪个进程来接掌对CPU的使用、何时“接任”、“任期”多久,都由内核说了算。在内核维护的数据结构中,包含了与所有正在运行的进程有关的信息;随着进程的创建、状态发生变化或者终结,内核会及时更新这些数据结构。内核所维护的底层数据结构可将程序使用的文件名转换为磁盘的物理位置。此外,每个进程的虚拟内存与计算机物理内存及磁盘交换区之间的映射关系,也在内核维护的数据结构之列。进程间的所有通信都要通过内核提供的通信机制来完成。响应进程发出的请求,内核会创建新的进程、终结现有进程。最后,由内核(特别是设备驱动程序)来执行与输入/输出设备之间的所有直接通信,按需与用户进程交互信息。
本书后续内容中会出现如下措辞,例如:“某进程可创建另一个进程”、“某进程可创建管道”、“某进程可将数据写入文件”,以及“调用exit()以终止某进程”。请务必牢记,以上所有动作都是由内核来居中“调停”,上面的说法不过是“某进程可以请求内核创建另一个进程”的缩略语,以此类推。
进阶阅读
涵盖操作系统概念和设计、尤其是对UNIX操作系统加以重点关注的现代教科书包括:[Tanenbaum, 2007]、[Tanenbaum & Woodhull,2006]以及[Vahalia, 1996],最后一本包含了与虚拟内存架构有关的详细内容。[Goodheart & Cox, 1994]详细介绍了System V Release 4。[Maxwell, 1999]则是有选择性的针对Linux 2.2.5的部分内核源码进行了注释。[Lions, 1996]对第六版UNIX源码进行了详尽阐释,并一直是研究UNIX操作系统内幕的入门级经典。[Bovet & Cesati, 2005]描述了LINUX2.6内核的实现。
2.2 Shell
Shell是一种具有特殊用途的程序,主要用于读取用户输入的命令,并执行相应的程序以响应命令。有时,人们也称之为命令解释器。
术语登录shell(login shell)是指:用户刚登录系统时,由系统创建、用以运行shell的进程。尽管某些操作系统将命令解释器集成于内核中,而对UNIX系统而言,shell只是一个用户进程。shell的种类繁多,登入同一台计算机的不同用户可同时使用不同的shell(就单个用户来说,情况也一样)。纵观UNIX历史,出现过以下几种重要的shell:
•Bourne shell (sh):这款由Steve Bourne编写的shell历史最为悠久,且应用广泛,曾是第七版UNIX的标配shell。Bourne shell包含了在其他shell中常见的许多特性:I/O重定向、管道、文件名生成(通配符)、变量、环境变量处理、命令替换、后台命令执行以及函数。对于所有问世于第七版UNIX之后的实现而言,除了可能提供有其他shell之外,都附带了Bourne shell。
•C shell (csh):由Bill Joy于加州大学伯克利分校编写而成。其命名则源于该脚本语言的流控制语法与C语言有着许多相似之处。C shell当时提供了若干极为实用的交互式特性,这些特性并不为Bourne shell所支持,其中包括:命令的历史记录、命令行编辑功能、任务控制和别名等。C shell与Bourne shell并不兼容。尽管C shell曾是BSD系统标配的交互式shell,但一般情况下,人们还是喜欢针对Bourne shell编写shell脚本(稍后介绍),以便其能够在所有UNIX实现上移植。
•Korn shell (ksh): AT&T贝尔实验室的David Korn编写了这款shell,作为Bourne shell的“继任者”。在保持与Bourne shell兼容的同时,Korn shell还吸收了那些与C shell相类似的交互式特性。
•Bourne again shell (bash):这款shell是GNU项目对Bourne shell的重新实现。Bash提供了与C shell和Korn shel所类似的交互式特性。Brian Fox 和Chet Ramey是bash的主要作者。bash或许是LINUX上应用最为广泛的shell了。(在LINUX上,Bourne shell,(sh)其实正是由bash仿真提供的。)
POSIX.2-1992基于当时的Korn shell版本定义了一个shell标准。如今,Korn shell和bash都符合POSIX规范,但两者都提供了大量对标准的扩展,其扩展之间存在许多差异。
设计shell的目的不仅仅是用于人机交互,对shell脚本(包含shell命令的文本文件)进行解释也是其用途之一。为实现这一目的,每款shell都内置有许多通常与编程语言相关的功能,其中包括:变量、循环和条件语句、I/O命令以及函数等。
尽管在语法方面有所差异,每款shell执行的任务都大致相同。除非指明是某款特定shell的操作,否则书中的“shell” 都会按所描述的方式运作。本书绝大多数需要用到shell的示例都会使用bash,若无其他说明,读者可假定这些示例也能以相同方式在其他类Bourne的shell上运行。
2.3 用户和组
系统会对每个用户的身份做唯一标识,用户可隶属于多个组。
用户
系统的每个用户都拥有唯一的登录名(用户名)和与之相对应的整数型用户ID (UID)。系统密码文件/etc/passwd为每个用户都定义有一行记录,除了上述两项信息外,该记录还包含如下信息:
•组ID: 用户所属第一个组的整数型组ID。
•主目录:用户登录后所居于的初始目录。
•登录shell:执行以解释用户命令的程序名称。
该记录还能以加密形式保存用户密码。然而,出于安全考虑,用户密码往往存储于单独的shadow密码文件中,仅供特权用户阅读。
组
出于管理目的,尤其是为了控制对文件和其它资源的访问,将多个用户分组是非常实用的做法。例如,某项目的开发团队人员需要共享同一组文件,就可以将他们编为同一组的成员。在早期的UNIX实现中,一个用户只能隶属于一个组。BSD率先允许一个用户同时属于多个组,这一理念后来被其它UNIX实现纷纷效仿,并最终成为POSIX.1-1990标准。每个用户组都对应着系统组文件/etc/group中的一行记录,该记录包含如下信息:
•组名:组名称(具有唯一性)。
•组ID (GID):与组相关的整数型ID。
•用户列表:隶属于该组的用户登录名列表(通过密码文件记录的group ID字段未能标识出的该组其他成员,也在此列),以逗号分隔。
超级用户
超级用户在系统中享有特权。超级用户帐号的用户ID为0,通常登录名为root。在一般的UNIX系统上,超级用户凌驾于系统的权限检查之上。因此,无论对文件施以何种访问权限限制,超级用户都可以访问系统中的任何文件,也能发送信号干预系统运行的所有用户进程。系统管理员可以使用超级用户账号来执行各种系统管理任务。
2.4 单根目录层级、目录、链接及文件
内核维护着一套单根目录结构,以放置系统的所有文件。(这与微软windows之类的操作系统形成了鲜明对照,windows系统的每个磁盘设备都有各自的目录层级。)这一目录层级的根基就是名为“/”的根目录。所有的文件和目录都是根目录的“子孙”。图1-2所示为这种文件层级结构的示例。
图2-1 Linux单根目录层级的一部分
文件类型
在文件系统内,会对文件类型进行标记,以表明其种类。其中一种用来表示普通数据文件,人们常称之为“普通文件”或“纯文本文件”,以示与其他种类的文件有所区别。其他文件类型包括:设备、管道、套接字、目录以及符号链接。
术语“文件”常用来指代任意类型的文件,不仅仅指普通文件。
路径和链接
目录是一种特殊类型的文件,内容采用表格形式,数据项包括文件名以及对相应文件的引用。这一“文件名+引用”的组合被称为链接。每个文件都可以有多条链接,因而也可以有多个名称,在相同或不同的目录中出现。
目录可包含指向文件或其他目录的链接。路径间的链接建立起如图2-1所示的目录层级。
每个目录至少包含两条记录:.和..,前者是指向目录自身的链接,后者是指向其上级目录——父目录的链接。除根目录外,每个目录都有父目录。对于根目录而言,..是指向根目录自身的链接(因此,/..等于/)。
符号链接
类似于普通链接,符号链接给文件起了一个“别号”(alternative name)。在目录列表中,普通链接是内容为“文件名+指针”的一条记录,而符号链接则是经过特殊标记的文件,内容包含了另一文件的名称。(换言之,一个符号链接对应着目录中内容为“文件名+指针”的一条记录,指针指向的文件内容(译注:及该文件所属数据块存储的内容)为另一个文件名的字符串。)所谓“另一文件”通常被称为符号链接的目标,人们一般会说:符号链接“指向”或“引用”目标文件。 在多数情况下,只要系统调用用到了路径名,内核会自动解除(换言之,按照)该路径名中符号链接的引用,以符号链接所指向的文件名来替换符号链接。若符号链接的目标文件自身也是一个符号链接,那么上述过程会以递归方式重复下去。(为了应对可能出现的循环引用,内核对解除引用的次数作了限制。)如果符号链接指向的文件并不存在,那么可将该链接视为空链接(dangling link)。
通常,人们会分别使用硬链接(hard link)或软链接(soft link)这样的术语来指代正常链接和符号链接。之所以存在这两种不同类型的链接,将在第十八章做出解释。
文件名
在大多数Linux文件系统上,文件名最长可达255个字符。文件名可以包含除“/”和空字符( )外的所有字符。但是,只建议使用字母、数字、点(“.”)、下划线(“_”)以及连字符(“-”)。SUSv3将这一65个字符的集合——[-._a-zA-Z0-9]称为可移植文件名字符集(portable filename character set)。
对于可移植文件名字符集以外的字符,由于其可能会在shell、正则表达式或其他场景中具有特殊含义,故而应避免在文件名中使用。如在上述环境中出现了包含特殊含义字符的文件名,则需要进行转义,即对此类字符进行特殊标记(一般会在特殊字符前插入一个“”),以指明不应以特殊含义对其进行解释。若场境不支持转义机制,则不能使用此类文件名。
此外,还应避免以连字符(“-”)作为文件名的起始字符,因为一旦在shell命令中使用这种文件名,会被误认为命令行选项开关。
路径名
路径名是由一系列文件名组成的字符串,彼此以“/”分隔,首字符可以为“/”(非强制)。(译注:此处“文件”的含义中包括了目录——如前文所述:“目录是一种特殊类型的文件”)除却最后一个文件名外,该系列文件名均为目录名称(或为指向目录的符号链接)。路径名的尾部(译注:即最后一个文件名)可标识任意类型的文件,包括目录在内。有时将该字符串中最后一个“/”字符之前的部分称为路径名的目录部分,将其之后的部分称为路径名的文件部分或基础部分。
路径名应按从左至右的顺序阅读,路径名中每个文件名之前的部分,即为该文件所处目录。可在路径名中任意位置后引入字符串".."(译注:需以“/”分隔),用以指代路径名中当前位置的父目录。
路径名描述了单根目录层级下的文件位置,又可分为绝对路径名和相对路径名:
•绝对路径名 以“/”开始,指明文件相对于根目录的位置。图2-1中的/home/mtk/.bashrc、/usr/include以及/(根路径的路径名)都是绝对路径名的例子。
•相对路径名 定义了相对于进程当前工作目录(见下文)的文件位置,与绝对路径名相比,相对路径名缺少了起始的“/”。如图2-1所示,在目录usr下,可使用相对路径名include/sys/types.h来引用文件types.h;在目录avr下,可使用相对路径名../mtk/.bashrc来访问文件.bashrc。
当前工作目录
每个进程都有一个当前工作目录(有时简称为进程工作目录或当前目录)。这就是单根目录层级下进程的“当前位置”,也是进程解释相对路径名的参照点。
进程的当前工作目录继承自其父进程。对登录shell来说,其初始当前工作目录,是依据密码文件中该用户记录的主目录字段来设置。可使用cd命令来改变shell的当前工作目录。
文件的所有权和权限
每个文件都有一个与之相关的用户ID和组ID,分别定义文件的属主和属组。系统根据文件的所有权来判定用户对文件的访问权限。
为了访问文件,系统把用户分为三类:文件的属主(有时,也称为文件的用户)、(与文件组(group)ID相匹配的)属组成员用户、以及其他用户。可为以上三类用户分别设置三种权限(共计9种权限位):只允许查看文件内容的读权限;允许修改文件内容的写权限;允许执行文件的执行权限——这里的文件要么指程序,要么是交由某种解释程序(通常指shell的一种,但也有例外)处理的脚本。
也可针对目录进行上述权限设置,但意义稍有不同:读权限允许列出目录内容(即该目录下的文件名);写权限允许对目录内容进行更改(比如,添加、修改或删除文件名);执行(有时也称为搜索)权限允许对目录中的文件进行访问(但需受文件自身访问权限的约束)。
2.5 文件I/O模型
UNIX系统I/O模型最为显著的特性之一是其I/O通用性概念。也就是说,同一套系统调用(open()、read()、write()、close()等)所执行的I/O操作,可施之于所有文件类型,包括设备文件在内。(应用程序发起的I/O请求,内核会将其转化为相应的文件系统操作、或者设备驱动程序操作,以此来执行针对目标文件或设备的I/O操作。)因此,采用这些系统调用的程序能够处理任何类型的文件。
就本质而言,内核只提供一种文件类型:字节流序列,在处理磁盘文件、磁盘或磁带设备时,可通过lseek()系统调用来随机访问。
许多应用程序和函数库都将新行符(十进制ASCII码为10,有时亦称其为换行)视为文本中一行的结束和另一行的开始。UNIX系统没有文件结束符的概念;读取文件时如无数据返回,便会认定抵达文件末尾。
文件描述符
I/O系统调用使用文件描述符——(往往是数值很小的)非负整数——来指代打开的文件。获取文件描述符的常用手法是:调用open(),在参数中指定I/O操作目标文件的路径名。
通常,由shell启动的进程会继承三个已打开的文件描述符:描述符0为标准输入,指代为进程提供输入的文件;描述符1为标准输出,指代供进程写入输出的文件;描述符2为标准错误,指代供进程写入错误消息或异常通告的文件。在交互式shell或程序中,上述三者一般都指向终端。在stdio函数库中,这几种描述符分别与文件流stdin、stdout和stderr相对应。
stdio函数库
C编程语言在执行文件I/O操作时,往往会调用C语言标准库的I/O函数。也将这样一组I/O函数称为stdio函数库,其中包括:fopen(), fclose(), scanf(), printf(), fgets(), fputs()等。stdio函数位于I/O系统调用层(open(), close(), read(), write()等)之上。
本书假定读者已经了解了C语言的标准I/O (stdio)函数,因此也不会介绍这方面的内容。更多与stdio函数库有关的信息请参考[Kernighan & Ritchie, 1988], [Harbison & Steele,2002], [Plauger, 1992], 和[Stevens & Rago, 2005]。
2.6 程序
程序通常以两种面目示人。其一为源码形式,由使用编程语言(比如,C语言)写成的一系列语句组成,是人类可以阅读的文本文件。要想执行程序,则需将源码转换为第二种形式——计算机可以理解的二进制机器语言指令。(这与脚本形成了鲜明对照,脚本是包含命令的文本文件,可以由shell或其它命令解释器之类的程序直接处理。)一般认为,术语“程序”的上述两种含义几近相同,因为经过编译和链接处理,会将源码转换为语义相同的二进制机器码。
过滤器
从stdin读取输入、加以转换,再将转换后的数据输出到stdout——常常将拥有上述行为的程序称为过滤器,cat,grep,tr,sort,wc,sed,awk均在其列。
命令行参数
C语言程序可以访问命令行参数——即程序运行时在命令行中输入的内容。要访问命令行参数,程序的main()函数需做如下声明:
int main(int argc, char *argv[])
argc变量包含命令行参数的总个数,argv指针数组的成员指针则逐一指向每个命令行参数字符串。首个字符串,argv[0],标识程序名本身。