1.3 UNIX基本概念
在详细讲述UNIX编程接口之前,我们需要先了解一些UNIX的基本概念和术语。UNIX中的概念和术语很多,它们之中有很多是相互依赖的,尽管本书的安排基本上是循序渐进的方式,但免不了会遇到一些还未介绍的术语和概念,如果涉及还未讲述的内容,我们会以索引的方式指出讲述它们的章节。
1.3.1 程序和进程
程序是包含计算机要执行指令集合的文件,它分为源程序和可执行程序。源程序是用程序设计语言编写的,在计算机中以正文形式保存在文件中。UNIX系统可以使用各种各样的程序设计语言编程,如C、C++、Fortran、Perl、sh、Java、SQL等。每一种程序设计语言通常适用于某一特定应用领域,例如,C常用于系统软件程序设计,Fortran主要用于科学计算,SQL专门用于数据库应用,网络应用常用Java、Perl。在本书中我们使用C语言,因为UNIX系统编程接口都以C函数形式给出,用C语言来讲解更直观。
源程序一般要经过编译器的编译后才能在计算机上运行,这种经过编译后生成的二进制代码文件称为可执行文件,也即可执行程序。UNIX系统还有另外一种可执行程序,即shell脚本程序,例如用sh编写的脚本程序。脚本程序在执行过程中需要经过其他程序(如shell程序)的解释。无论是哪一种程序—源程序或可执行程序,它们都以文件形式存储在UNIX文件系统中。可执行程序可以提交给计算机运行。
从用户的观点来看,进程是程序的一个执行实例。例如,当我们在终端键入执行某个程序的命令时便创建了该程序对应的进程。从UNIX系统内部来看,进程是运行程序并为程序提供执行环境的实体,是系统进行资源分配和调度运行的一个独立单位。程序和进程的区别是:程序是静止的,它只是一个文件;进程是动态的,它具有生存期。具体地说,进程有以下三个特点:
1)进程有一个控制点和自己的独立地址空间。进程的控制点通过程序计数器跟踪着程序的指令序列,进程的地址空间由进程可以引用或访问的存储单元组成。进程在其地址空间内执行程序的指令序列。
2)进程具有生存期。进程从创建到消亡的过程称为进程的生存期。一个进程的生存期可以分为一组状态,这些状态刻画了进程从创建、活动到消亡的过程。从用户的观点粗略地划分,UNIX系统中进程主要有以下一些状态:
新建:进程正在被创建。
就绪:进程正在等待被执行。
运行:进程正在被执行。
阻塞:进程正在等待一个事件,例如I/O。
僵死:进程已经结束,并等待释放资源。这是进程的最后状态。
进程的状态转换如图1-1所示。当程序提交执行时,系统会创建一个处于新建状态的进程。当创建完成后,操作系统将把这个进程放在就绪状态的进程队列中。进程调度程序将在某个时刻选择这个进程运行。当进程在CPU上实际运行时,它就处于运行状态了。
处于阻塞状态的进程正在等待某个事件的发生并且不会被调度执行。通过执行一个如sleep的命令,进程可以自愿地进入阻塞状态。通常,进程执行一个I/O请求时也会进入阻塞状态直至I/O操作完成。当I/O完成时,硬件会中断CPU,操作系统的中断处理程序将唤醒该进程使它进入就绪状态并等待重新被调度运行。
最后,当进程完成时,它会发出系统调用exit()而进入僵死状态。当系统释放了进程占用的所有资源后,该进程便消亡了。
3)处于运行状态的进程有两种执行状态:用户态和核态。若进程运行中执行的是用户程序中的指令,则进程处于用户态执行。当进程发出系统调用时,便进入核态执行。进程在核态下执行的是操作系统的指令,这些指令完成用户的请求,如输入输出、存储分配等。
1.3.2 内核
任何一个计算机系统中,硬件和软件是相互依存的。硬件包括CPU、存储器、硬盘以及其他设备,它们一起构成了计算机的硬件系统。但是没有软件的管理,硬件系统本身是无法使用的。实现这种管理任务的软件叫做操作系统,在UNIX术语中称为UNIX内核。UNIX内核是一个底层的、直接运行于硬件的程序,它控制着硬件,并创建、销毁和控制所有进程。在Linux中,内核驻存在名为/root/vmLinux的磁盘文件中。当系统启动时,一个称为“自举”(bootstrapping)的特殊过程会将内核从磁盘加载至内存并启动它运行。内核初始化整个系统、设置进程运行的环境,然后创建几个初始进程。这些进程随后将创建其他进程。一旦加载,内核便驻存在内存,直到系统关机。在此期间,它管理着所有进程并为它们提供各种服务。
从更广的角度来看,操作系统不仅仅是这个内核,它还有其他许多程序和例程,它们一起提供一个友好的工作环境。图1-2给出的是UNIX系统的体系结构。从中可以看出,UNIX系统是层次模块结构,处在最内层的是UNIX内核。内核直接与硬件交互,向外提供UNIX系统调用接口,如read()、write()等。外层的程序,如shell以及date、ls和who之类的实用程序,通过系统调用请求内核完成各种操作,并在内核与调用程序之间交换数据。其他应用程序则在低层程序和工具的基础上构筑而成。
显然,单独一个内核的功能是有限的,完整、友好的操作系统必须有内核外层的这些实用程序的支持。但是,内核在许多方面是特殊的,它定义了程序设计与系统的接口,是唯一一个必不可少的程序,没有它任何程序都不能运行。尽管同时可有几个shell或编译程序运行,但一次只能装入一个内核。
1.3.3 shell
UNIX内核虽然负责管理和创建进程,但它并不执行用户输入的命令。用户输入的命令由shell来执行。shell是UNIX系统中的一个命令解释程序,它处在用户和UNIX系统之间,起着协调用户与系统间的一致性、在用户与系统之间进行交互的作用。shell在UNIX系统中的地位和作用如图1-3所示。
用户一旦在UNIX中注册成功,系统就将为其创建一个进程来执行shell命令解释程序。这个shell称为注册shell,它负责读入并执行用户输入的命令。shell通常可以接收两种形式的输入:来自终端的单条命令或者是来自一个文件的批处理命令,这种文件也称为脚本文件或命令文件。
shell接收的命令有一些是内部命令,它们执行的是shell内部的实用程序;另外一些则执行其他的实用程序,这些命令由shell将控制转交给对应的实用程序并启动它们执行。
UNIX系统有几种不同的shell,最常见的有:
sh:Bourne Shell,也称为标准shell。它以其创建者Steve Bourne的名字命名,是UNIX shell中最老的,并且几乎所有UNIX系统都提供它。Bourne Shell稍有点原始并且缺乏作业控制能力(将作业从前台移至后台的能力),不过它非常适合于shell程序设计和编写命令文件。
csh:C shell,它是美国加州大学伯克利分校作为BSD UNIX的一部分而开发的,并且一直是使用最广泛的交互shell。C shell有许多sh没有的特点,其中包括作业控制和历史机制(重复以前输入的命令的能力)。
ksh:Korn shell,它以其创建者David Korn而命名。Korn shell与sh兼容,但它还有许多csh的特征和另外一些新的特征,如历史编辑—回忆以前输入的命令并在执行之前编辑它们。另外,它比C shell更为可靠。Korn shell是UNIX系统V版本4标准的一部分,在其他UNIX实现中也包含它。
bash:bash也是与sh兼容的一种shell,它融合了ksh和csh的优点并服从IEEE POSIX P1003.2/ISO 9945.2标准,支持命令行编辑、无限制的命令行历史记录、作业控制以及其他一些功能。相比其他shell,bash更为方便,功能也更为强大。
当一个系统同时存在多种shell时,系统从/etc/passwd文件中用户注册账号登记项的最后一个字段可以知道应当使用哪一种shell。
1.3.4 用户名与用户ID、用户组与组ID
UNIX中的每一个用户有一个账号,该账号有一个用户名和一个唯一的用户ID。用户名也称为注册名,它是一个字符串;用户ID是一个整数。用户在登录时通过用户名向系统标识自己,用户ID则主要用于检测该用户是否有某项操作的权限。
UNIX系统中,用户可以组织成组,一个用户可以是一个或多个组的成员,其中一个组是用户的初始组,简称为组。如果该用户还属于其他组,则这些组称为该用户的附加组。用户组是由系统管理员建立的,系统文件/etc/group中登记了系统中存在的所有组和组ID,以及组内的用户。
口令文件/etc/passwd记录了系统中的所有注册用户,每一个用户有一个登记项,其中包含用户名、用户ID、用户的组ID等信息,其中,用户的组ID指明了该用户的初始组。例如,如果在/etc/passwd文件包含如下内容:
%grep zkj /etc/passwd
zkj:x:500:15::/home/zkj:/bin/bash
同时,在/etc/group文件包含如下内容:
% grep zkj /etc/group
users:x:15:zkj
research:x:17: Yang, zkj, Hc, zhang
Lib: :x:20: wang, zkj
那么,用户zkj的用户ID为500,组ID为15。他是三个组的成员,这三个组是:users、research和Lib。其中,users是他的初始组,research和Lib是他的附加组。
1.3.5 特权用户
在UNIX中,只有进程的主人(即创建它的人)可以撤销一个正在执行的程序。同样,只有文件或作业的主人才能删除一个文件或者从打印队列中清除一个作业。但有一个特殊的用户可以不受这种限制,这就是名为root的根用户,也称为特权用户或超级用户。
特权用户的用户名是root,其用户ID是0。UNIX内核识别用户ID为0的用户并允许他不受限制地做任何事情,他可以超越系统施加的所有文件访问和执行权限。更重要的是,特权用户有控制整个系统的权力,如关闭系统,甚至不经意地用一条简单的命令破坏整个系统!
正由于特权用户具有这种特殊的权力,因此,能够以特权用户身份访问系统的人必须知道自己所做的事情。特权用户具有监管系统运行、维护系统安全、配置系统、增加和删除用户以及对系统进行正常备份等责任。
除了特权用户之外,UNIX内核不区分其他任何用户,其他所有用户都视为是同等的,并且只能够做权限许可之内的事情。
1.3.6 系统调用与库函数
顾名思义,系统调用是对操作系统的一种请求,它请求操作系统为用户程序完成某种工作。例如,read()是一个系统调用,它请求操作系统将存储在磁盘设备上的数据读入缓冲区。
为了避免用户按自己的方式随意访问设备而导致混乱,用户必须向操作系统请求服务,由操作系统统一管理涉及每一种设备的所有请求并提供服务。所有操作系统都提供了定义很好的、有限个数且直接进入内核的这类服务点,这些服务点就是系统调用,也称为程序设计与系统的接口。通过这些服务点,应用程序可以向内核请求各种服务。
在老式操作系统中,传统的做法是用机器汇编语言定义系统调用。UNIX操作系统不同,在每个特定的系统中无论采用什么技术实现系统调用,它的定义总是用C函数来表示。UNIX让每一个系统调用有一个相同名字的C函数,应用程序使用与标准C库函数相同的方法调用这些函数。这些函数然后用系统要求的技术来启动适当的内核服务。例如,这些函数可以将一至多个C参数放置在通用寄存器中,然后执行一条导致软中断的指令而进入内核。也就是说,系统调用一定会导致程序进入核态执行内核代码。
库函数与系统调用不同,它可以不需要操作系统的介入来完成工作,并且也不是进入操作系统内核的入口点,尽管它们可以调用一至多个系统调用。例如,printf()需要调用write()系统调用来完成输出,而复制字符串函数strcpy()和计算任意角正弦函数sin()则完全不需要操作系统的帮助。
应用程序可以调用系统调用或库函数,同时我们还应注意到许多库函数也涉及了系统调用,它们之间的关系如图1-4所示。
系统调用总是在《UNIX程序员手册》的第2部分中说明,而通用库函数则总是在第3部分中说明。因此,UNIX约定用函数名后随“(2)”表示系统调用,“(3)”表示库函数。例如,read(2)表示read是系统调用,而printf(3)则表示printf是库函数。
从实现者的角度来看,系统调用和库函数之间的区别是根本性的:系统调用是UNIX内核提供的服务,而库函数处在内核之外。从用户的角度来看,它们没有很大的区别。在本书中,为了说明大部分程序员要用到的UNIX程序设计接口,我们既讲述系统调用也讲述库函数。事实上,系统调用通常只提供最基本的、最小的接口集合,而库函数常常提供更完全且更强大的功能。在本书中,除特别必要才区别系统调用之外,我们视系统调用和库函数均为普通的C函数,它们两者都服务于应用程序。不过,应当认识到,系统调用是我们无法更改的一种特征,如果我们愿意可以替换库函数,但是却不能替换系统调用。