Linux系统基本概念

内核的职责

内核所能执行的主要任务如下所示。

  • 进程调度:计算机内均配备有一个或多个CPU(中央处理单元),以执行程序指令。
    与其他 UNIX系统一样,Linux 属于抢占式多任务操作系统。"多任务"意指多个进程(即运行中的程序)可同时驻留于内存,且每个进程都能获得对 CPU的使用权。"抢占"则是指一组规则。这组规则控制着哪些进程获得对 CPU 的使用,以及每个进程能使用多长时间,这两者都由内核进程调度程序(而非进程本身)决定。
  • 内存管理:以一二十年前的标准来看,如今计算机的内存容量可谓相当可观,但软件的规模也保持了相应地增长,故而物理内存(RAM)仍然属于有限资源,内核必须以公平、高效地方式在进程间共享这一资源。与大多数现代操作系统一样,Linux也采用了虚拟内存管理机制,这项技术主要具有以下两方面的优势。进程与进程之间、进程与内核之间彼此隔离,因此一个进程无法读取或修改内核或其他进程的内存内容。
    只需将进程的一部分保持在内存中,这不但降低了每个进程对内存的需求量,而且还能在 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 命令以及函数等。

尽管在语法方面有所差异,每款 shell执行的任务都大致相同。除非指明是某款特定 shel的操作,否则书中的"shell"都会按所描述的方式运作。本书绝大多数需要用到shell的示例都会使用 bash,若无其他说明,读者可假定这些示例也能以相同方式在其他类Boume的shl上运行。

文件的所有权和权限

每个文件都有一个与之相关的用户ID 和组ID,分别定义文件的属主和属组。系统根据文件的所有权来判定用户对文件的访问权限。

为了访问文件,系统把用户分为3类:文件的属主(有时,也称为文件的用户)、与文件组(group)ID相匹配的属组成员用户以及其他用户。

可为以上3类用户分别设置3种权限(共计9种权限位):只允许查看文件内容的读权限;允许修改文件内容的写权限;允许执行文件的执行权限。这里的文件要么指程序,要么是交由某种解释程序(通常指shell的一种,但也有例外)处理的脚本。

也可针对目录进行上述权限设置,但意义稍有不同。读权限允许列出目录内容(即该目录下的文件名),写权限允许对目录内容进行更改(比如,添加、修改或删除文件名),执行(有时也称为搜索)权限允许对目录中的文件进行访问(但需受文件自身访问权限的约束)。

进程

简而言之,进程是正在执行的程序实例。执行程序时,内核会将程序代码载入虚拟内存,为程序变量分配空间,建立内核记账(bookkeeping)数据结构,以记录与进程有关的各种信息(比如,进程 ID、用户ID、组 ID 以及终止状态等)。

在内核看来,进程是一个个实体,内核必须在它们之间共享各种计算机资源。对于像内存这样的受限资源来说,内核一开始会为进程分配一定数量的资源,并在进程的生命周期内,统筹该进程和整个系统对资源的需求,对这一分配进行调整。程序终止时,内核会释放所有此类资源,供其他进程重新使用。其他资源(如 CPU、网络带宽等)都属于可再生资源,但必须在所有进程间平等共享。

进程的内存布局

逻辑上将一个进程划分为以下几部分(也称为段)。

  • 文本∶ 程序的指令。
  • 数据∶ 程序使用的静态变量。
  • 堆∶程序可从该区域动态分配额外内存。
  • 栈∶ 随函数调用、返回而增减的一片内存,用于为局部变量和函数调用链接信息分配存储空间。

创建进程和执行程序

进程可使用系统调用 fork()来创建一个新进程。

调用 fork()的进程被称为父进程,新创建的进程则被称为子进程。

内核通过对父进程的复制来创建子进程。子进程从父进程处继承数据段、栈段以及堆段的副本后,可以修改这些内容,不会影响父进程的"原版"内容。(在内存中被标记为只读的程序文本段则由父、子进程共享。)

然后,子进程要么去执行与父进程共享代码段中的另一组不同函数,或者,更为常见的情况是使用系统调用 execve()去加载并执行一个全新程序。

execve()会销毁现有的文本段、数据段、栈段及堆段,并根据新程序的代码,创建新段来替换它们。

以execve()为基础,C 语言库还提供了几个相关函数,接口虽然略有不同,但功能全都相同。以上所有库函数的名称均以字符串"exec"打头。

进程 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。

init 进程

系统引导时,内核会创建一个名为 init的特殊进程,即"所有进程之父",该进程的相应程序文件为/sbin/init。

系统的所有进程不是由 init(使用frok))"亲自"创建,就是由其后代进程创建。init 进程的进程号总为1,且总是以超级用户权限运行。谁(哪怕是超级用户)都不能"杀死"init 进程,只有关闭系统才能终止该进程。init 的主要任务是创建并监控系统运行所需的一系列进程。(手册页 init(8)中包含了init 进程的详细信息。)

守护进程

守护进程指的是具有特殊用途的进程,系统创建和处理此类进程的方式与其他进程相同,但以下特征是其所独有的∶

  • "长生不老"。守护进程通常在系统引导时启动,直至系统关闭前,会一直"健在"。
  • 守护进程在后台运行,且无控制终端供其读取或写入数据。
    守护进程中的例子有syslogd(在系统日志中记录消息)和htpd(利用HTTP分发 Web 页面)。

环境列表

每个进程都有一份环境列表,即在进程用户空间内存中维护的一组环境变量。

这份列表的每一元素都由一个名称及其相关值组成。由fork()创建的新进程,会继承父进程的环境副本。这也为父子进程间通信提供了一种机制。当进程调用 exec()替换当前正在运行的程序时,新程序要么继承老程序的环境,要么在exec()调用的参数中指定新环境并加以接收。

在绝大多数shell中,可使用export命令来创建环境变量(Cshell使用setenv命令),如下所示∶

$ export MYVAR='Hello world'

C 语言程序可使用外部变量(char **environ)来访问环境,而库函数也允许进程去获取或修改自己环境中的值。

环境变量的用途多种多样。例如,shell定义并使用了一系列变量,供shell执行的脚本和程序访问。其中包括∶变量 HOME(明确定义了用户登录目录的路径名)、变量 PATH(指明了用户输入命令后,shell 查找与之相应程序时所搜索的目录列表)

内存映射

调用系统函数 mmap()的进程,会在其虚拟地址空间中创建一个新的内存映射。映射分为两类。

  • 文件映射∶将文件的部分区域映射入调用进程的虚拟内存。映射一旦完成,对文件映射内容的访问则转化为对相应内存区域的字节操作。映射页面会按需自动从文件中加载。
  • 相映成趣的是并无文件与之 相对应的匿名映射,其映射页面的内容会被初始化为0。

由某一进程所映射的内存可以与其他进程的映射共享。

达成共享的方式有二∶其一是两个进程都针对某一文件的相同部分加以映射,其二是由 fork(创建的子进程自父进程处继承映射。

当两个或多个进程共享的页面相同时,进程之一对页面内容的改动是否为其他进程所见呢?

这取决于创建映射时所传入的标志参数。若传入标志为私有,则某进程对映射内容的修改对于其他进程是不可见的,而且这些改动也不会真地落实到文件上;

若传入标志为共享,对映射内容的修改就会为其他进程所见,并且这些修改也会造成对文件的改动。

内存映射用途很多,其中包括∶以可执行文件的相应段来初始化进程的文本段、内存(内容填充为0)分配、文件I/O(即映射内存 I/O)以及进程间通信(通过共享映射)

静态库和共享库

所谓目标库是这样一种文件∶将(通常是逻辑相关的)一组函数代码加以编译,并置于一个文件中,供其他应用程序调用。这一做法有利于程序的开发和维护。现代 UNIX 系统提供两种类型的对象库∶ 静态库和共享库。

静态库

静态库(有时,也称之为档案文件【archives】)是早期 UNIX 系统中唯一的一种目标库。本质上说来,静态库是对已编译目标模块的一种结构化整合。要使用静态库中的函数,需要在创建程序的链接命令中指定相应的库。主程序会对静态库中隶属于各目标模块的不同函数加以引用。链接器在解析了引用情况后,会从库中抽取所需目标模块的副本,将其复制到最终的可执行文件中,这就是所谓静态链接。

对于所需库内的各目标模块,采用静态链接方式生成的程序都存有一份副本。这会引起诸多不便。其一,在不同的可执行文件中,可能都存有相同目标代码的副本,这是对磁盘空间的浪费。同理,调用同一库函数的程序,若均以静态链接方式生成,且又于同时加以执行,这会造成内存浪费,因为每个程序所调用的函数都各有一份副本驻留在内存中,此其二。此外,如果对库函数进行了修改,需要重新加以编译、生成新的静态库,而所有需要调用该函数"更新版"的应用,都必须与新生成的静态库重新链接。

共享库

设计共享库的目的是为了解决静态库所存在的问题。

如果将程序链接到共享库,那么链接器就不会把库中的目标模块复制到可执行文件中,而是在可执行文件中写入一条记录,以表明可执行文件在运行时需要使用该共享库。

一旦在运行时将可执行文件载入内存,一款名为"动态链接器"的程序会确保将可执行文件所需的动态库找到,并载入内存,随后实施运行时链接,解析可执行文件中的函数调用,将其与共享库中相应的函数定义关联起来。在运行时,共享库代码在内存中只需保留一份,且可供所有运行中的程序使用。

经过编译处理的函数仅在共享库内保存一份,从而节约了磁盘空间。另外,这一设计还能确保各类程序及时使用到函数的最新版本,功莫大焉,只需将带有函数新定义体的共享库重新加以编译即可,程序会在下次执行时自动使用新函数。

进程间通信及同步

Linux 系统上运行有多个进程,其中许多都是独立运行。然而,有些进程必须相互合作以达成预期目的,因此彼此间需要通信和同步机制。

读写磁盘文件中的信息是进程间通信的方法之一。可是,对许多程序来说,这种方法既慢又缺乏灵活性。因此,像所有现代UNIX实现那样,Linux也提供了丰富的进程间通信(IPC)机制,如下所示。

  • 信号(signal),用来表示事件的发生。
  • 管道(亦即 shell用户所熟悉的"|"操作符)和FIFO,用于在进程间传递数据。
  • 套接字,供同一台主机或是联网的不同主机上所运行的进程之间传递数据。
  • 文件锁定,为防止其他进程读取或更新文件内容,允许某进程对文件的部分区域加以锁定。
  • 消息队列,用于在进程间交换消息(数据包)。
  • 信号量(semaphore),用来同步进程动作。
  • 共享内存,允许两个及两个以上进程共享一块内存。当某进程改变了共享内存的内容时,其他所有进程会立即了解到这一变化。

UNIX系统的IPC 机制种类如此繁多,有些功能还互有重叠,部分原因是由于各种IPC 机制是在不同的UNIX实现上演变而来的,需要遵循的标准也各不相同。

例如,就本质而言,FIFO和 UNIX 套接字功能相同,允许同一系统上并无关联的进程彼此交换数据。二者之所以并存于现代 UNIX系统之中,是由于FIFO来自 System V,而套接字则源于 BSD。

信号

尽管上一节将信号视为 IPC的方法之一,但其在其他方面的广泛应用则更为普遍,因此值得深入讨论。

人们往往将信号称为"软件中断"。进程收到信号,就意味着某一事件或异常情况的发生。信号的类型很多,每一种分别标识不同的事件或情况。采用不同的整数来标识各种信号类型,并以 SIGxxxx 形式的符号名加以定义。

内核、其他进程(只要具有相应的权限)或进程自身均可向进程发送信号。

例如,发生下列情况之一时,内核可向进程发送信号。

  • 用户键入中断字符(通常为 Control-C)。
  • 进程的子进程之一已经终止。
  • 由进程设定的定时器(告警时钟)已经到期。
  • 进程尝试访问无效的内存地址。

在 shell中,可使用 kill命令向进程发送信号。在程序内部,系统调用kill)可提供相同的功能。
收到信号时,进程会根据信号采取如下动作之一。

  • 忽略信号。
  • 被信号"杀死"。
  • 先挂起,之后再被专用信号唤醒。

就大多数信号类型而言,程序可选择不采取默认的信号动作,而是忽略信号(当信号的默认处理行为并非忽略此信号时,会派上用场)或者建立自己的信号处理器。信号处理器是由程序员定义的函数,会在进程收到信号时自动调用,根据信号的产生条件执行相应动作。

信号从产生直至送达进程期间,一直处于挂起状态。通常,系统会在接收进程下次获得调度时,将处于挂起状态的信号同时送达。如果接收进程正在运行,则会立即将信号送达。然而,程序可以将信号纳入所谓"信号屏蔽"'以求阻塞该信号。如果产生的信号处于"信号屏蔽"之列,那么此信号将一直保持挂起状态,直至解除对该信号的阻塞。(亦即从信号屏蔽中移除。)

线程

在现代 UNIX 实现中,每个进程都可执行多个线程。可将线程想象为共享同一虚拟内存及一干其他属性的进程。每个线程都会执行相同的程序代码,共享同一数据区域和堆。

可是,每个线程都拥有属于自己的栈,用来装载本地变量和函数调用链接信息。

线程之间可通过共享的全局变量进行通信。借助于线程API所提供的条件变量和互斥机制,进程所属的线程之间得以相互通信并同步行为——尤其是在对共享变量的使用方面。

此外,利用 IPC 和同步机制,线程间也能彼此通信。

线程的主要优点在于协同线程之间的数据共享(通过全局变量)更为容易,而且就某些算法而论,以多线程来实现比之以多进程实现要更加自然。再者,显而易见,多线程应用能从多处理器硬件的并行处理中获益匪浅。

进程组和 shell 任务控制

shell执行的每个程序都会在一个新进程内发起。

比如,shell创建了3个进程来执行以下管道命令(在当前的工作目录下,根据文件大小对文件进行排序并显示)∶

$ ls -1|sort -k5n|less

除 Bourne shell以外,几乎所有的主流 shell都提供了一种交互式特性,名为任务控制

该特性允许用户同时执行并操纵多条命令或管道。在支持任务控制的 shell中,会将管道内的所有进程置于一个新进程组或任务中。(如果情况很简单,shel命令行只包含一条命令,那么就会创建一个只包含单个进程的新进程组。)进程组中的每个进程都具有相同的进程组标识符(以整数形式),其实就是进程组中某个进程(也称为进程组组长 processgroupleader)的进程 ID。

内核可对进程组中的所有成员执行各种动作,尤其是信号的传递。支持任务控制的shell 会利用这一特性,以挂起或恢复执行管道中的所有进程。

会话、控制终端和控制进程

会话指的是一组进程组(任务)。

会话中的所有进程都具有相同的会话标识符。会话首进程(session leader)是指创建会话的进程,其进程 ID会成为会话 ID。

使用会话最多的是支持任务控制的 shell,由 shell 创建的所有进程组与 shell 自身隶属于同一会话,shell是此会话的会话首进程。

通常,会话都会与某个控制终端相关。控制终端建立于会话首进程初次打开终端设备之时。对于由交互式shell所创建的会话,这恰恰是用户的登录终端。一个终端至多只能成为一个会话的控制终端。

打开控制终端会致使会话首进程成为终端的控制进程。一旦断开了与终端的连接(比如,关闭了终端窗口),控制进程将会收到 SIGHUP信号。

在任一时点,会话中总有一个前台进程组(前台任务),可以从终端中读取输入,向终端发送输出。如果用户在控制终端中输入了"中断"(通常是Control-C)或"挂起"字符(通常是 Control-Z),那么终端驱动程序会发送信号以终止或挂起(亦即停止)前台进程组。

一个会话可以拥有任意数量的后台进程组(后台任务),由以"&"字符结尾的行命令来创建。
支持任务控制的 shell提供如下命令∶列出所有任务,向任务发送信号,以及在前后台任务之间来回切换。

伪终端

伪终端是一对相互连接的虚拟设备,也称为主从设备。在这对设备之间,设有一条 IPC信道,可供数据进行双向传递。

从设备(slave device)所提供的接口,其行为方式与终端相类似,基于这一特点,可以将某个为终端编写的程序与从设备连接起来,然后,再利用连接到主设备的另一程序来驱动这一"面向终端"的程序,这是伪终端的一个关键用途。

由"驱动程序"'所产生的输出,在经由终端驱动程序的常规输入处理(例如,默认情况下,会把回车符映射为换行符)后,会作为输入传递给与从设备相连的面向终端的程序。

而由面向终端的程序向从设备写入的任何数据又作为"驱动程序"的输入来传递(在执行完所有常规的终端输入处理后)。换句话说,"驱动程序"所履行的功能,在效果上等同于用户通常在传统终端上所执行的操作。

伪终端广泛应用于各种应用领域,最知名的要数 telnet 和ssh之类提供网络登录服务的应用,以及XWindow系统所提供的终端窗口实现。

实时性

实时性应用程序是指那些需要对输入做出及时响应的程序。此类输入往往来自于外接的传感器或某些专门的输入设备,而输出则会去控制外接硬件。具有实时性需求的应用程序示例包括自动化装配流水线、银行 ATM机,以及飞机导航系统等。

虽然许多实时性应用程序都要求对输入做出快速响应,但决定性因素却在于要在事件触发后的一定时限内,保证响应的交付。

要提供实时响应,特别是在短时间内加以响应,就需要底层操作系统的支持。由于实时响应的需求与多用户分时操作系统的需求存在冲突,大多数操作系统"天生"并不提供这样的支持。虽然已经设计出不少实时性的UNIX变体,但传统的UNIX实现都不是实时操作系统。Linux 的实时性变体也早已诞生,而近期的Linux 内核正转向对实时性应用原生而全面的支持。

为支持实时性应用,POSIX.1b定义了多个POSIX.1扩展,其中包括异步I/O、共享内存、内存映射文件、内存锁定、实时性时钟和定时器、备选调度策略、实时性信号、消息队列,以及信号量等。

虽然这些扩展还不具备严格意义上的"实时性",但当今的大多数 UNIX实现都支持上面提到的全部或部分扩展。

posted @ 2021-02-21 13:28  DearLeslie  阅读(507)  评论(0编辑  收藏  举报