内核
两种含义:
- 指完整的软件包,这包括用来管理计算机资源的核心层软件,以及附带的所有标准软件工具,诸如命令行解释器、图形用户界面、文件操作工具和文本编辑器等
- 在更狭义的范围内,是指管理和分配计算机资源(即CPU、RAM和设备)的核心层软件。术语“内核”通常是第二种含义,本书中的“操作系统”一词也是这层意思。
Linux 内核可执行文件采用/boot/vmlinuz 或与之类似的路径名。
内核职责
- 进程调度。
Linux 属于抢占式多任务操作系统。“多任务”意指多个进
程(即运行中的程序)可同时驻留于内存,且每个进程都能获得对 CPU 的使用权。
“抢占”则是指一组规则。这组规则控制着哪些进程获得对 CPU 的使用,以及每个
进程能使用多长时间,这两者都由内核进程调度程序(而非进程本身)决定。
- 内存管理
进程与进程之间、进程与内核之间彼此隔离,因此一个进程无法读取或修改内核
或其他进程的内存内容。
只需将进程的一部分保持在内存中,这不但降低了每个进程对内存的需求量,而
且还能在 RAM 中同时加载更多的进程。这也大幅提升了如下事件的发生概率,
在任一时刻,CPU 都有至少一个进程可以执行,从而使得对 CPU 资源的利用更
加充分。
- 文件系统
内核在磁盘之上提供有文件系统,允许对文件执行创建、获取、更新
以及删除等操作。
- 创建和终止进程
内核可将新程序载入内存,为其提供运行所需的资源(比如,CPU、
内存以及对文件的访问等)。这样一个运行中的程序我们称之为“进程”。一旦进程执行
完毕,内核还要确保释放其占用资源,以供后续程序重新使用。
- 设备访问
内核既为程序访
问设备提供了简化版的标准接口,同时还要仲裁多个进程对每一个设备的访问。
- 联网
内核以用户进程的名义收发网络消息(数据包)。该任务包括将网络数据包路
由至目标系统。
- 提供系统调用应用编程接口(API)
进程可利用内核入口点(也称为系统调用)请求
内核去执行各种任务
核态 和 用户态
现代处理器架构一般允许 CPU 至少在两种不同状态下运行,即:用户态和核心态(有时也称之为监管态 supervisor mode)。执行硬件指令可使 CPU 在两种状态间来回切换。与之对应,可将虚拟内存区域划分(标记)为用户空间部分或内核空间部分。在用户态下运行时,CPU 只能访问被标记为用户空间的内存,试图访问属于内核空间的内存会引发硬件
异常。当运行于核心态时,CPU 既能访问用户空间内存,也能访问内核空间内存。仅当处理器在核心态运行时,才能执行某些特定操作。这样的例子包括:执行宕机(halt)指令去关闭系统,访问内存管理硬件,以及设备 I/O 操作的初始化等。实现者们利用这一硬件设计,将操作系统置于内核空间。这确保了用户进程既不能访问内核指令和数据结构,也无法
执行不利于系统运行的操作。
以进程及内核视角检视系统
一个运行系统通常会有多个进程并行其中。对进程来说,许多事件的发生都无法预期。执行中的进程不清楚自己对 CPU 的占用何时“到期”,系统随之又会调度哪个进程来使用CPU(以及以何种顺序来调度),也不知道自己何时会再次获得对 CPU 的使用。信号的传递和进程间通信事件的触发由内核统一协调,对进程而言,随时可能发生。诸如此类,进程都
一无所知。进程不清楚自己在 RAM 中的位置。或者换种更通用的说法,进程内存空间的某块特定部分如今到底是驻留在内存中还是被保存在交换空间(磁盘空间中的保留区域,作为计算机 RAM 的补充)里,进程本身并不知晓。与之类似,进程也闹不清自己所访问的文件“居于”磁盘驱动器的何处,只是通过名称来引用文件而已。进程的运作方式堪称“与世隔
绝”—进程间彼此不能直接通信。进程本身无法创建出新进程,哪怕“自行了断”都不行。
最后还有一点,进程也不能与计算机外接的输入输出设备直接通信。相形之下,内核则是运行系统的中枢所在,对于系统的一切无所不知、无所不能,为系统上所有进程的运行提供便利。由哪个进程来接掌对 CPU 的使用,何时“接任”,“任期”多久,都由内核说了算。在内核维护的数据结构中,包含了与所有正在运行的进程有关的信息。随着进程的创建、状态发生变化或者终结,内核会及时更新这些数据结构。内核所维护的底层数据结构可将程序使用的文件名转换为磁盘的物理位置。此外,每个进程的虚拟内存与计算机物理内存及磁盘交换区之间的映射关系,也在内核维护的数据结构之列。进程间的所有通信都要通过内核提供的通信机制来完成。响应进程发出的请求,内核会创建新的进程,终结现有进程。最后,由内核(特别是设备驱动程序)来执行与输入/输出设备之间的所有直接通信,按需与用户进程交互信息。
shell
shell 是一种具有特殊用途的程序,主要用于读取用户输入的命令,并执行相应的程序以
响应命令。有时,人们也称之为命令解释器。
设计 shell 的目的不仅仅是用于人机交互,对 shell 脚本(包含 shell 命令的文本文件)进行解释也是其用途之一。为实现这一目的,每款 shell 都内置有许多通常与编程语言相关的功能,其中包括变量、循环和条件语句、I/O 命令以及函数等
用户和组
系统会对每个用户的身份做唯一标识,用户可隶属于多个组。
用户
系统的每个用户都拥有唯一的登录名(用户名)和与之相对应的整数型用户ID(UID)。系统密码文件/etc/passwd 为每个用户都定义有一行记录,除了上述两项信息外,该记录还包含如下信息。
- 组 ID:用户所属第一个组的整数型组 ID。
- 主目录:用户登录后所居于的初始目录。
- 登录 shell:执行以解释用户命令的程序名称。
###组
每个用户
组都对应着系统组文件/etc/group 中的一行记录,该记录包含如下信息。
1. 组名:(唯一的)组名称。
2. 组 ID(GID):与组相关的整数型 ID。
3. 用户列表:隶属于该组的用户登录名列表(通过密码文件记录的 group ID 字段未能标 识出的该组其他成员,也在此列),以逗号分隔。
超级用户
超级用户账号的用户 ID 为 0,通常登录名为 root。在一般 的 UNIX 系统上,超级用户凌驾于系统的权限检查之上。因此,无论对文件施以何种访问权限限制,超级用户都可以访问系统中的任何文件,也能发送信号干预系统运行的所有用户进程。系统管理员可以使用超级用户账号来执行各种系统管理任务。
单根目录层级、目录、链接及文件
内核维护着一套单根目录结构,以放置系统的所有文件。
文件类型
在文件系统内,会对文件类型进行标记,以表明其种类。其中一种用来表示普通数据文件,人们常称之为“普通文件”或“纯文本文件”,以示与其他种类的文件有所区别。其他文件类型包括设备、管道、套接字、目录以及符号链接。
术语“文件”常用来指代任意类型的文件,不仅仅指普通文件。
路径和链接
目录是一种特殊类型的文件,内容采用表格形式,数据项包括文件名以及对相应文件的引用。这一“文件名+引用”的组合被称为链接。每个文件都可以有多条链接,因而也可以有多个名称,在相同或不同的目录中出现。
符号链接
在目录列表中,普通链接是内容为“文件名+指针”的一条记录,而符号链接则是经过特殊标记的文件,内容包
含了另一文件的名称。所谓“另一文件”通常被称为符号链
接的目标,人们一般会说符号链接“指向”或“引用”目标文件。在多数情况下,只要系统调用用到了路径名,内核会自动解除(换言之,按照)该路径名中符号链接的引用,以符号链接所指向的文件名来替换符号链接。若符号链接的目标文件自身也是一个符号链接,那么上述过程会以递归方式重复下去。(为了应对可能出现的循环引用,内核对解除引用的次数作了限制。)如果符号链接指向的文件并不存在,那么可将该链接视为空链接(dangling link)。通常,人们会分别使用硬链接(hard link)或软链接(soft link)这样的术语来指代正常链 接和符号链接。之所以存在这两种不同类型的链接,将在第 18 章做出解释。
文件名
对于可移植文件名字符集以外的字符,由于其可能会在 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,分别定义文件的属主和属组。系统根据文件的所有权来判定用户对文件的访问权限。
为了访问文件,系统把用户分为 3 类:文件的属主(有时,也称为文件的用户)、与文件组(group)ID 相匹配的属组成员用户以及其他用户。可为以上 3 类用户分别设置 3 种权限(共计 9 种权限位):只允许查看文件内容的读权限;允许修改文件内容的写权限;允许执行文件的执行权限。这里的文件要么指程序,要么是交由某种解释程序(通常指 shell 的一种,但也有例外)处理的脚本。
也可针对目录进行上述权限设置,但意义稍有不同。读权限允许列出目录内容(即该目 录下的文件名),写权限允许对目录内容进行更改(比如,添加、修改或删除文件名),执行(有时也称为搜索)权限允许对目录中的文件进行访问(但需受文件自身访问权限的约束)。
文件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 启动的进程会继承 3 个已打开的文件描述符:描述符 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 语言)写成的一系列语句组成,是人类可以阅读的文本文件。要想执行程序,则需将源码转换为第二种形式—计算机可以理解的二进制机器语言指令。一般认为,术语“程序”的上述两种含义几近相同,因为经过编译和链接处理,会将源码转换为语义相同的二进制机器码。
过滤器
从 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以及终止状态等)。 在内核看来,进程是一个个实体,内核必须在它们之间共享各种计算机资源。对于像内 存这样的受限资源来说,内核一开始会为进程分配一定数量的资源,并在进程的生命周期内,统筹该进程和整个系统对资源的需求,对这一分配进行调整。程序终止时,内核会释放所有此类资源供其他进程重新使用。其他资源(如 CPU、网络带宽等)都属于可再生资源,但 必须在所有进程间平等共享。
进程的内存分布
逻辑上将一个进程划分为以下几部分(也称为段)。
- 文本:程序的指令
- 数据:程序使用的静态变量
- 堆:程序可以从该区域动态分配额外内存
- 栈:随函数调用、返回而增减的一片内存,用于为局部变量和函数调用链接信息分配存储空间
创建进程和执行程序
进程可使用系统调用 fork()来创建一个新进程。调用 fork()的进程被称为父进程,新创建的进程则被称为子进程。内核通过对父进程的复制来创建子进程。子进程从父进程处继承数据段、栈段以及堆段的副本后,可以修改这些内容,不会影响父进程的“原版”内容。(在内存中被标记为只读的程序文本段则由父、子进程共享。)
然后,子进程要么去执行与父进程共享代码段中的另一组不同函数,或者,更为常见的情况是使用系统调用 execve()去加载并执行一个全新程序。execve()会销毁现有的文本段、数据段、栈段及堆段,并根据新程序的代码,创建新段来替换它们。
以 execve()为基础,C 语言库还提供了几个相关函数,接口虽然略有不同,但功能全都相同。以上所有库函数的名称均以字符串“exec”打头,在函数间差异无关宏旨的场合,本书会用符号exec()作为这些库函数的统称。不过,请读者牢记,实际上根本不存在名为 exec()的库函数。一般情况下,书中会使用“执行”一词来指代 execve()及其衍生函数所实施的操作。
进程id和父进程id
每一进程都有一个唯一的整数型进程标识符(PID)。此外,每一进程还具有一个父进程 标识符(PPID)属性,用以标识请求内核创建自己的进程。
进程终止和终止状态
可使用以下两种方式之一来终止一个进程:其一,进程可使用_exit()系统调用(或相关的exit()库函数),请求退出;其二,向进程传递信号,将其“杀死”。无论以何种方式退出,进程 都会生成“终止状态”,一个非负小整数,可供父进程的 wait()系统调用检测。在调用_exit()的 情况下,进程会指明自己的终止状态。若由信号来“杀死”进程,则会根据导致进程“死亡” 的信号类型来设置进程的终止状态。(有时会将传递进_exit()的参数称为进程的“退出状态”,以 示与终止状态有所不同,后者要么指传递给_exit()的参数值,要么表示“杀死”进程的信号。)
根据惯例,终止状态为 0 表示进程“功成身退”,非 0 则表示有错误发生。大多数 shell 会将前一执行程序的终止状态保存于 shell 变量$?中。
进程的用户和组标识符(凭证)
每个进程都有一组与之相关的用户 ID (UID)和组 ID (GID),如下所示。
-
真实用户 ID 和组 ID:用来标识进程所属的用户和组。新进程从其父进程处继承这些 ID。登录 shell 则会从系统密码文件的相应字段中获取其真实用户 ID 和组 ID。
-
有效用户 ID 和组 ID:进程在访问受保护资源(比如,文件和进程间通信对象)时,会使用这两个 ID(并结合下述的补充组 ID)来确定访问权限。一般情况下,进程的有效 ID 与相应的真实 ID 值相同。正如即将讨论的那样,改变进程的有效 ID 实为一种机制,可使进程具有其他用户或组的权限。
-
补充组 ID:用来标识进程所属的额外组。新进程从其父进程处继承补充组 ID。登录 shell 则从系统组文件中获取其补充组 ID。
特权进程
在 UNIX 系统上,就传统意义而言,特权进程是指有效用户 ID 为 0(超级用户)的进程。通常由内核所施加的权限限制对此类进程无效。与之相反,术语“无特权”(或非特权)进程是指由其他用户运行的进程。此类进程的有效用户 ID 为非 0 值,且必须遵守由内核所强加的权限规则。
由某一特权进程创建的进程,也可以是特权进程。例如,一个由 root(超级用户)发起的登录 shell。成为特权进程的另一方法是利用 set-user-ID 机制,该机制允许某进程的有效用户ID 等同于该进程所执行程序文件的用户 ID。
能力(Capabilities)
始于内核 2.2,Linux 把传统上赋予超级用户的权限划分为一组相互独立的单元(称之为“能力”)。每次特权操作都与特定的能力相关,仅当进程具有特定能力时,才能执行相应操作。传统意义上的超级用户进程(有效用户 ID 为 0)则相应开启了所有能力。赋予某进程部分能力,使得其既能够执行某些特权级操作,又防止其执行其他特权级操作。本书第 39 章会对能力做深入讨论。在本书后文中,当述及只能由特权进程执行的特殊操作时,一般都会在括号中标明其具体能力。能力的命名以 CAP_为前缀,例如,CAP_KILL。
init 进程
系统引导时,内核会创建一个名为 init 的特殊进程,即“所有进程之父”,该进程的相应程序文件为/sbin/init。系统的所有进程不是由 init(使用 frok())“亲自”创建,就是由其后代进程创建。init 进程的进程号总为 1,且总是以超级用户权限运行。谁(哪怕是超级用户)都不能“杀死”init 进程,只有关闭系统才能终止该进程。init 的主要任务是创建并监控系统运行所需的一系列进程。(手册页 init(8)中包含了 init 进程的详细信息。)
守护进程
守护进程指的是具有特殊用途的进程,系统创建和处理此类进程的方式与其他进程相同, 但以下特征是其所独有的:
- “长生不老”。守护进程通常在系统引导时启动,直至系统关闭前,会一直“健在”。
- 守护进程在后台运行,且无控制终端供其读取或写入数据。守护进程中的例子有 syslogd(在系统日志中记录消息)和 httpd(利用 HTTP 分发 Web 页面)。
环境列表
每个进程都有一份环境列表,即在进程用户空间内存中维护的一组环境变量。这份列表的每一元素都由一个名称及其相关值组成。由 fork()创建的新进程,会继承父进程的环境副本。这也为父子进程间通信提供了一种机制。当进程调用 exec()替换当前正在运行的程序时,新程序要么继承老程序的环境,要么在 exec()调用的参数中指定新环境并加以接收。 在绝大多数shell 中,可使用export 命令来创建环境变量(C shell 使用setenv 命令),如下所示:
export MYVAR='Hello,world'
C 语言程序可使用外部变量(char **environ)来访问环境,而库函数也允许进程去获取或修改自己环境中的值。
环境变量的用途多种多样。例如,shell 定义并使用了一系列变量,供 shell 执行的脚本和程序访问。其中包括:变量 HOME(明确定义了用户登录目录的路径名)、变量 PATH(指明了用户输入命令后,shell 查找与之相应程序时所搜索的目录列表)。
资源限制
每个进程都会消耗诸如打开文件、内存以及 CPU 时间之类的资源。使用系统调用 setrlimit(),进程可为自己消耗的各类资源设定一个上限。此类资源限制的每一项均有两个相关值:软限制(soft limit)限制了进程可以消耗的资源总量,硬限制(hard limit)软限制的调整上限。非特权进程在针对特定资源调整软限制值时,可将其设置为 0 到相应硬限制值之间的任意值,但硬限制值则只能调低,不能调高。
由 fork()创建的新进程,会继承其父进程对资源限制的设置。
使用 ulimit 命令(在 C shell 中为 limit)可调整 shell 的资源限制。shell 为执行命令所创建的子进程会继承上述资源设置。
内存映射
调用系统函数 mmap()的进程,会在其虚拟地址空间中创建一个新的内存映射。
映射分为两类。
- 文件映射:将文件的部分区域映射入调用进程的虚拟内存。映射一旦完成,对文件映射 内容的访问则转化为对相应内存区域的字节操作。映射页面会按需自动从文件中加载。
- 相映成趣的是并无文件与之相对应的匿名映射,其映射页面的内容会被初始化为 0。
由某一进程所映射的内存可以与其他进程的映射共享。达成共享的方式有二:
其一是两个进程都针对某一文件的相同部分加以映射,
其二是由 fork()创建的子进程自父进程处继承映射。
当两个或多个进程共享的页面相同时,进程之一对页面内容的改动是否为其他进程所见呢?这取决于创建映射时所传入的标志参数。若传入标志为私有,则某进程对映射内容的修改对于其他进程是不可见的,而且这些改动也不会真地落实到文件上;若传入标志为共享,对映射内容的修改就会为其他进程所见,并且这些修改也会造成对文件的改动。内存映射用途很多,其中包括:以可执行文件的相应段来初始化进程的文本段、内存(内容填充为 0)分配、文件 I/O(即映射内存 I/O)以及进程间通信(通过共享映射)。
静态库和共享库
所谓目标库是这样一种文件:将(通常是逻辑相关的)一组函数代码加以编译,并置于一个文件中,供其他应用程序调用。这一做法有利于程序的开发和维护。现代 UNIX 系统提供两种类型的对象库:静态库和共享库。
静态库
静态库(有时,也称之为档案文件[archives])是早期 UNIX 系统中唯一的一种目标库。本质上说来,静态库是对已编译目标模块的一种结构化整合。要使用静态库中的函数,需要在创建程序的链接命令中指定相应的库。主程序会对静态库中隶属于各目标模块的不同函数加以引用。链接器在解析了引用情况后,会从库中抽取所需目标模块的副本,将其复制到最终的可执行文件中,这就是所谓静态链接。对于所需库内的各目标模块,采用静态链接方式生成的程序都存有一份副本。这会引起诸多不便。
其一,在不同的可执行文件中,可能都存有相同目标代码的副本,这是对磁盘空间的浪费。同理,调用同一库函数的程序,若均以静态链接方式生成,且又于同时加以执行,这会造成内存浪费,因为每个程序所调用的函数都各有一份副本驻留在内存中,此其二。此外,如果对库函数进行了修改,需要重新加以编译、生成新的静态库,而所有需要调用该函数“更新版”的应用,都必须与新生成的静态库重新链接。
共享库
设计共享库的目的是为了解决静态库所存在的问题。如果将程序链接到共享库,那么链接器就不会把库中的目标模块复制到可执行文件中,而是在可执行文件中写入一条记录,以表明可执行文件在运行时需要使用该共享库。一旦在运行时将可执行文件载入内存,一款名为“动态链接器”的程序会确保将可执行文件所需的动态库找到,并载入内存,随后实施运行时链接,解析可执行文件中的函数调用,将其与共享库中相应的函数定义关联起来。在运行时,共享库代码在内存中只需保留一份,且可供所有运行中的程序使用。经过编译处理的函数仅在共享库内保存一份,从而节约了磁盘空间。另外,这一设计还能确保各类程序及时使用到函数的最新版本,功莫大焉,只需将带有函数新定义体的共享库重新加以编译即可,程序会在下次执行时自动使用新函数。
浙公网安备 33010602011771号