术语”内核“通常指管理和分配计算机资源(即CPU、RAM和设备)的核心层软件,本书中”操作系统”一词也是这个意思。
在没有内核的情况下,计算机也能运行程序,但有了内核会极大简化其他程序的编写和使用。这要归功于内核为管理计算机的有限资源所提供的软件层。
计算机内均配备有一个或多个CPU(中央处理单元),以执行程序指令。与其他UNIX 系统一样,Linux 属于抢占式多任务操作系统。
物理内存(RAM)仍然属于有限资源,内核必须以公平、高效地方式在进程间共享这一资源。与大多数现代操作系统一样,Linux也采用了虚拟内存管理机制,这项技术主要具有以下两方面的优势。
内核在磁盘之上提供有文件系统,允许对文件执行创建、获取、更新以及删除等操作。
内核可将新程序载入内存,为其提供运行所需的资源(比如,CPU、内存以及对文件的访问等)。这样一个运行中的程序我们称之为“进程”。一旦进程执行完毕,内核还要确保释放其占用资源,以供后续程序重新使用。
计算机外接设备(鼠标、键盘、磁盘和磁带驱动器等)可实现计算机与外部世界的通信,这一通信机制包括输入、输出或是两者兼而有之。内核既为程序访问设备提供了简化版的标准接口,同时还要仲裁多个进程对每一个设备的访问。
内核以用户进程的名义收发网络消息(数据包)。该任务包括将网络数据包路由至目标系统。
进程可利用内核入口点(也称为系统调用)请求内核去执行各种任务。
一般而言,诸如Linux 之类的多用户操作系统会为每个用户营造一种抽象:虚拟私有计算机(virtual private computer)。这就是说,每个用户都可以登录进入系统,独立操作,而与其他用户大致无干。例如,每个用户都有属于自己的磁盘存储空间(主目录)。再者,用户能够运行程序,而每一程序都能从CPU 资源中“分得一杯羹”,运转于自有的虚拟地址空间中。而且这些程序还能独立访问设备,并通过网络传递信息。内核负责解决(多进程)访问硬件资源时可能引发的冲突,用户和进程对此则往往一无所知。
现代处理器架构一般允许CPU 至少在两种不同状态下运行,即:用户态和核心态。执行硬件指令可使CPU在两种状态间来回切换。
与之对应,可将虚拟内存区域划分(标记)为用户空间部分或内核空间部分。
仅当处理器在核心态运行时,才能执行某些特定操作。这样的例子包括:执行宕机(halt)指令去关闭系统,访问内存管理硬件,以及设备I/O 操作的初始化等。
实现者们利用这一硬件设计,将操作系统置于内核空间。这确保了用户进程既不能访问内核指令和数据结构,也无法执行不利于系统运行的操作。
一个运行系统通常会有多个进程并行其中。对进程来说,许多事件的发生都无法预期。执行中的进程不清楚自己对CPU 的占用何时“到期”,系统随之又会调度哪个进程来使用CPU(以及以何种顺序来调度),也不知道自己何时会再次获得对CPU 的使用。信号的传递和进程间通信事件的触发由内核统一协调,对进程而言,随时可能发生。诸如此类,进程都一无所知。
进程不清楚自己在RAM 中的位置。或者换种更通用的说法,进程内存空间的某块特定部分如今到底是驻留在内存中还是被保存在交换空间(磁盘空间中的保留区域,作为计算机RAM 的补充)里,进程本身并不知晓。与之类似,进程也闹不清自己所访问的文件“居于”磁盘驱动器的何处,只是通过名称来引用文件而已。进程的运作方式堪称“与世隔绝”—进程间彼此不能直接通信。进程本身无法创建出新进程,哪怕“自行了断”都不行。最后还有一点,进程也不能与计算机外接的输入输出设备直接通信。
(简而言之,进程啥也不知道?)
内核是运行系统的中枢所在,对于系统的一切无所不知、无所不能,为系统上所有进程的运行提供便利。
shell 是一种具有特殊用途的程序,主要用于读取用户输入的命令,并执行相应的程序以响应命令。有时,人们也称之为命令解释器。
术语登录shell(login shell)是指用户刚登录系统时,由系统创建,用以运行shell 的进程。对 UNIX 系统而言,shell 只是一个用户进程.
shell 的种类繁多,登入同一台计算机的不同用户同时可使用不同的shell(就单个用户来说,情况也一样)
设计shell 的目的不仅仅是用于人机交互,对shell 脚本(包含shell 命令的文本文件)进行解释也是其用途之一。为实现这一目的,每款shell 都内置有许多通常与编程语言相关的功能,其中包括变量、循环和条件语句、I/O 命令以及函数等。
系统会对每个用户的身份做唯一标识,用户可隶属于多个组。
系统的每个用户都拥有唯一的登录名(用户名)和与之相对应的整数型用户ID(UID)。系统密码文件/etc/passwd 为每个用户都定义有一行记录,除了上述两项信息外,该记录还包含如下信息。
出于管理目的,尤其是为了控制对文件和其他资源的访问,将多个用户分组是非常实用的做法。
每个用户组都对应着系统组文件/etc/group 中的一行记录,该记录包含如下信息。
超级用户在系统中享有特权。超级用户账号的用户ID 为0,通常登录名为root。
在一般的UNIX 系统上,超级用户凌驾于系统的权限检查之上。因此,无论对文件施以何种访问权限限制,超级用户都可以访问系统中的任何文件,也能发送信号干预
内核维护着一套单根目录结构,以放置系统的所有文件。这一目录层级的根基就是名为“/”的根目录。所有的文件和目录都是根目录的“子孙”。
其中一种用来表示普通数据文件,人们常称之为“普通文件”或“纯文本文件”,以示与其他种类的文件有所区别。
其他文件类型包括设备、管道、套接字、目录以及符号链接。
术语“文件”常用来指代任意类型的文件,不仅仅指普通文件。
目录是一种特殊类型的文件,内容采用表格形式,数据项包括文件名以及对相应文件的引用。
这一“文件名+引用”的组合被称为链接。
每个文件都可以有多条链接,因而也可以有多个名称,在相同或不同的目录中出现。
目录可包含指向文件或其他目录的链接。
每个目录至少包含两条记录:.和…,前者是指向目录自身的链接,后者是指向其上级目录—父目录的链接。除根目录外,每个目录都有父目录。对于根目录而言,…是指向根目录自身的链接(因此,/…等于/)。
通常,人们会分别使用硬链接(hard link)或软链接(soft link)这样的术语来指代正常链接和符号链接。
具体解释见18章
在大多数Linux 文件系统上,文件名最长可达255 个字符。只建议使用字母、数字、点(“.”)、下划线(“_”)以及连字符(“−”)。SUSv3 将这65 个字符的集合[-._a-zA-Z0-9]称为可移植文件名字符集(portablefilename character set)。
每个进程都有一个当前工作目录(有时简称为进程工作目录或当前目录)。这就是单根目录层级下进程的“当前位置”,也是进程解释相对路径名的参照点。
进程的当前工作目录继承自其父进程。对登录shell 来说,其初始当前工作目录,是依据密码文件中该用户记录的主目录字段来设置。
每个文件都有一个与之相关的用户ID和组ID,分别定义文件的属主和属组.
为了访问文件,系统把用户分为3 类:
可为以上3 类用户分别设置3 种权限(共计9 种权限位):
也可针对目录进行上述权限设置.
UNIX 系统I/O 模型最为显著的特性之一是其I/O通用性概念。也就是说,同一套系统调用(open()
、read()、write()、close()等)所执行的I/O 操作,可施之于所有文件类型,包括设备文件在内。
就本质而言,内核只提供一种文件类型:字节流序列,在处理磁盘文件、磁盘或磁带设备时,可通过lseek()
系统调用来随机访问。
UNIX 系统没有文件结束符的概念,读取文件时如无数据返回,便会认定抵达文件末尾。
I/O 系统调用使用文件描述符(往往是数值很小的)非负整数来指代打开的文件。获取文件描述符的常用手法是调用 open()
,在参数中指定 I/O 操作目标文件的路径名。
通常,由shell 启动的进程会继承3 个已打开的文件描述符:
在交互式shell或程序中,上述三者一般都指向终端。在stdio
函数库中,这几种描述符分别与文件流stdin
、stdout
和stderr
相对应。
C编程语言在执行文件I/O操作时,往往会调用C 语言标准库的I/O 函数。也将这样一组I/O 函数称为stdio函数库,其中包括fopen()、fclose()、scanf()、printf()、fgets()、fputs()等。
stdio 函数位于I/O 系统调用层(open()、close()、read()、write()等)之上。
一般有两种形式:
一般认为,术语“程序”的上述两种含义几近相同,因为经过编译和链接处理,会将源码转换为语义相同的二进制机器码。
从stdin 读取输入,加以转换,再将转换后的数据输出到stdout,常常将拥有上述行为的程序称为过滤器,cat、grep、tr、sort、wc、sed、awk 均在其列。
C 语言程序可以访问命令行参数,main中需要做如下的声明:
int main(int argc, char *argv[])
argc 变量包含命令行参数的总个数,
argv 指针数组的成员指针逐一指向每个命令行参数字符串。首个字符串argv[0],标识程序名本身。
进程是正在执行的程序实例。
执行程序时,内核会将程序代码载入虚拟内存,为程序变量分配空间,建立内核记账(bookkeeping)数据结构,以记录与进程有关的各种信息(比如,进程ID、用户ID、组ID 以及终止状态等)。
在内核看来,进程是一个个实体,内核必须在它们之间共享各种计算机资源。
逻辑上将一个进程划分为以下几部分(也称为段)。
使用系统调用fork()
创建一个新进程。调用fork()的进程被称为父进程,新创建的进程则被称为子进程。
内核通过对父进程的复制来创建子进程。子进程从父进程处继承数据段、栈段以及堆段的副本后,可以修改这些内容,不会影响父进程的“原版”内容。(在内存中被标记为只读的程序文本段则由父、子进程共享。)
然后,子进程要么去执行与父进程共享代码段中的另一组不同函数,或者,更为常见的情况是使用系统调用execve()
去加载并执行一个全新程序。execve()会销毁现有的文本段、数据段、栈段及堆段,并根据新程序的代码,创建新段来替换它们。
每一进程都有一个唯一的**整数型进程标识符(PID)和一个父进程标识符(PPID)**属性,用以标识请求内核创建自己的进程。
可使用以下两种方式之一来终止一个进程:
_exit()
系统调用(或相关的exit()库函数),请求退出无论以何种方式退出,进程都会生成**“终止状态”**,一个非负小整数,可供父进程的wait()
系统调用检测。
(有时会将传递进_exit()的参数称为进程的**“退出状态”**,以示与终止状态有所不同,后者要么指传递给_exit()的参数值,要么表示“杀死”进程的信号。)
终止状态为0 表示进程“功成身退”,非0 则表示有错误发生。大多数shell 会将前一执行程序的终止状态保存于shell 变量$?
中。
每个进程都有一组与之相关的用户ID (UID)和组ID (GID),如下所示。
成为特权进程:
Linux 把传统上赋予超级用户的权限划分为一组相互独立的单元(称之为
“能力”)。
仅当进程具有特定能力时,才能执行相应操作。传统意义上的超级用户进程(有效用户ID 为0)则相应开启了所有能力。
系统引导时,内核会创建一个名为init 的特殊进程,即“所有进程之父”,该进程的相应程序文件为/sbin/init
。
系统的所有进程不是由init(使用 frok())“亲自”创建,就是由其后代进程创建。
init 进程的进程号总为1,且总是以超级用户权限运行。谁(哪怕是超级用户)都不能“杀死”init 进程,只有关闭系统才能终止该进程。
init 的主要任务是创建并监控系统运行所需的一系列进程
系统创建和处理此类进程的方式与其他进程相同,但以下特征是其所独有的:
每个进程都有一份环境列表,即在进程用户空间内存中维护的一组环境变量。这份列表的每一元素都由一个名称及其相关值组成。
由fork()创建的新进程,会继承父进程的环境副本。这也为父子进程间通信提供了一种机制。
当进程调用exec()替换当前正在运行的程序时,新程序要么继承老程序的环境,要么在exec()调用的参数中指定新环境并加以接收。
在绝大多数shell 中,可使用export 命令来创建环境变量:
export MYVAR='Hello World'
C 语言程序可使用外部变量(char **environ
)来访问环境,而库函数也允许进程去获取或修改自己环境中的值。
环境变量的用途多种多样。例如,shell 定义并使用了一系列变量,供shell执行的脚本和程序访问。其中包括:变量HOME(明确定义了用户登录目录的路径名)、变量PATH(指明了用户输入命令后,shell查找与之相应程序时所搜索的目录列表)。
每个进程都会消耗诸如打开文件、内存以及CPU 时间之类的资源。使用系统调用setrlimit()
,进程可为自己消耗的各类资源设定一个上限。
此类资源限制的每一项均有两个相关值:
非特权进程在针对特定资源调整软限制值时,可将其设置为0 到相应硬限制值之间的任意值,但硬限制值则只能调低,不能调高。
由fork()创建的新进程,会继承其父进程对资源限制的设置。
使用ulimit
命令(在C shell 中为limit)可调整shell 的资源限制。shell 为执行命令所创建的子进程会继承上述资源设置。
调用系统函数mmap()
的进程,会在其虚拟地址空间中创建一个新的内存映射。
映射分为两类:
由某一进程所映射的内存可以与其他进程的映射共享。达成共享的方式有二:
当两个或多个进程共享的页面相同时,进程之一对页面内容的改动是否为其他进程所见呢?
这取决于创建映射时所传入的标志参数。
内存映射用途很多,其中包括:以可执行文件的相应段来初始化进程的文本段、内存(内容填充为0)分配、文件I/O(即映射内存I/O)以及进程间通信(通过共享映射)。
目标库:将(通常是逻辑相关的)一组函数代码加以编译,并置于一个文件中,供其他应用程序调用。这一做法有利于程序的开发和维护。
现代UNIX 系统提供两种类型的对象库:静态库和共享库。
静态库是对已编译目标模块的一种结构化整合。要使用静态库中的函数,需要在创建程序的链接命令中指定相应的库。主程序会对静态库中隶属于各目标模块的不同函数加以引用。链接器在解析了引用情况后,会从库中抽取所需目标模块的副本,将其复制到最终的可执行文件中,这就是所谓静态链接。
对于所需库内的各目标模块,采用静态链接方式生成的程序都存有一份副本。这会引起诸多不便。
目的是为了解决静态库存在的问题
如果将程序链接到共享库,那么链接器就不会把库中的目标模块复制到可执行文件中,而是在可执行文件中写入一条记录,以表明可执行文件在运行时需要使用该共享库。一旦在运行时将可执行文件载入内存,一款名为“动态链接器”的程序会确保将可执行文件所需的动态库找到,并载入内存,随后实施运行时链接,解析可执行文件中的函数调用,将其与共享库中相应的函数
定义关联起来。在运行时,共享库代码在内存中只需保留一份,且可供所有运行中的程序使用。
经过编译处理的函数仅在共享库内保存一份,从而节约了磁盘空间。另外,这一设计还能确保各类程序及时使用到函数的最新版本,只需将带有函数新定义体的共享库重新加以编译即可,程序会在下次执行时自动使用新函数。
Linux 系统上运行有多个进程,其中许多都是独立运行。然而有些进程必须相互合作以达成预期目的,因此彼此间需要通信和同步机制。
读写磁盘文件的方式既慢又缺乏灵活性
Linux 提供了丰富的进程间通信(IPC)机制:
|
操作符)和FIFO:在进程间传递数据就本质而言,FIFO和UNIX 套接字功能相同,允许同一系统上并无关联的进程彼此交换数据。二者之所以并存于现代UNIX系统之中,是由于FIFO 来自System V,而套接字则源于BSD。