本节介绍构成 Unix 操作系统设计基础的核心概念。Unix 从诞生之初就围绕着一小套巧妙的理念而设计,正如其创始人 Dennis Ritchie 和 Ken Thompson 所说:“UNIX 的成功并非在于新的发明,而在于充分利用精心挑选的一套富有成果的理念,尤其在于证明它们可以成为实现一个精巧而强大的操作系统的关键”。这些“富有成果的理念”按讨论顺序包括:可编程 Shell、用户和组、特权和非特权指令、环境、文件和目录层次结构、设备无关的输入和输出,以及最重要的进程。本节将简要介绍这些概念,但不会过于详细,首先会简要概述这个“精巧而强大的操作系统”(现在称为 Unix 内核)。在此过程中,我会介绍一些 Unix 命令来演示这些概念。
“操作系统(operating system)”一词没有一个统一的、普遍认可的定义,这或许令人遗憾。如果你查阅几乎所有关于操作系统的教科书,你会发现关于操作系统构成有两种不同的观点:
无论你决定采用哪种定义,“内核”一词都毫无疑问地被用作第二种定义的另一个名称。这是一个恰当的名称,因为它是 Unix 系统的核心。在关于 4.4 BSD 操作系统设计的开创性著作《4.4BSD 操作系统的设计和实现》中,McKusick 及其合著者将内核定义为“一个小型的软件核心,它仅提供实现其他操作系统服务所需的最低限度的功能”。在本书中,我使用了操作系统的狭义定义,即它就是内核,仅此而已。
内核是一个程序,或者说是一组交互程序的集合,具体取决于 Unix 的具体实现,它具有多个入口点。入口点是程序中可以开始执行的指令。每个入口点都提供内核执行的一项服务。如果您习惯于认为程序总是从第一行开始,这可能会让您感到困惑。
您目前编写的程序很可能只有一个入口点,即 main() 函数。但是,您可以创建具有多个入口点的代码。软件库是具有多个入口点的代码模块。您可以将入口点视为可被其他程序调用的函数。它们执行诸如打开、读取和写入文件、创建新进程、分配内存等服务。每个函数都需要一定数量、特定类型的参数,并产生定义明确的结果。内核入口点的集合构成了其 API 的很大一部分。实际上,您可以将内核视为包含一组独立函数的集合,这些函数捆绑在一起,构成一个大包,而其 API 则是这些函数的签名或原型的集合。
当 Unix 系统启动时,固件和软件会将内核加载到称为系统空间或内核空间的内存部分,并驻留在该空间直到机器关闭。用户程序不会允许访问系统空间。如果它们尝试访问,内核会终止它们。
内核对连接到计算机的所有硬件拥有完全访问权限。内核维护各种系统资源,以便为用户程序提供服务。这些系统资源包括许多不同的数据结构,例如,用于跟踪输入/输出 (I/O)、内存和设备使用情况的数据结构。
Unix 内核管理和保护所有这些资源,并提供一个允许所有用户高效、安全、愉快地工作的操作环境。它阻止用户及其运行的程序直接访问任何硬件资源。换句话说,如果用户正在运行的程序想要读取或写入磁盘,它必须请求内核代表它执行此操作,而不是自行执行。内核将执行该任务,并将任何数据传输到用户程序可以访问的内存区域或从中传输数据。
要理解为什么这是必要的,不妨想象一下,如果用户的程序可以直接访问硬盘会发生什么。用户可以运行一个程序,该程序可能会试图获取所有磁盘空间,甚至更糟的是,试图擦除磁盘,从而破坏内核保护其资源的能力。
Unix 内核还保护用户之间免受其他用户的侵害,并保护自身免受用户的侵害,同时让用户觉得他们每个人都拥有完全属于自己的计算机。每个人都可以运行程序,感觉就像他们拥有了计算机,就好像没有其他人在使用这台机器一样。用户拥有自己的磁盘空间、自己的私有内存、公平的 CPU 使用时间份额等等。为了实现这些目标,Unix 的发明者在其设计中融入了以下几项关键原则:
服务类型:
内核区域内的每个方框代表不同的服务类别。标有“系统调用”的方框代表程序用来请求和获取这些服务的 API 部分,而标有“系统程序”的方框则代表用户可以运行以获取这些服务的独立程序集合。
内核为正在运行的程序提供服务,但不直接向用户提供服务;用户通过在终端窗口中运行的命令行解释器输入命令或与图形用户界面 (GUI) 交互来与 Unix 交互,本书不讨论图形用户界面 (GUI)。命令行解释器是一个读取命令并执行命令的程序。
命令是通过输入文本(通常(但并非总是)使用键盘)输入的指令。命令后面可能包含选项和参数。选项修改命令的行为,而参数是命令的输入。例如:
$ gcc -g -o myprog myprog.c
以下列表解释了该命令行的各个部分:
命令行是您输入的所有内容,但不包括按 Enter 键时产生的换行符。在此示例中,
命令是整个命令行,但有时一行可以包含多个命令,这些命令由命令分隔符(例如分号)分隔,例如:
$ gcc -g -o myprog myprog.c ; gcc -g -o hello hello.c
从技术上讲,简单命令是单个命令,而不是一系列命令。当我们使用术语“命令”时,通常指的是简单命令。
在 GNU/Linux 和其他一些 Unix 系统中,某些命令有两种命令选项:短选项和长选项:
POSIX.1-2024没要求提供长选项,但 GNU/Linux 提供了长选项。
两种类型的选项都可以包含选项参数。例如,在
$ gcc -g -o myprog myprog.c ; gcc -g -o hello hello.c
-o 选项包含 myprog 参数。
在符合 POSIX.1-2024 标准的 Unix 系统中,如果选项包含参数,则该参数是必需的;您不能省略它。另一方面,GNU/Linux 允许命令包含带有非必需参数的选项。例如,您可以在 GNU/Linux 的命令行中输入 Firefox 浏览器的名称来启动它:
$ firefox
如果您为其指定 -P myprofile 选项,它将使用名为 myprofile 的用户配置文件启动。如果您只输入
$ firefox -P
它会显示一个对话框,要求您从列表中选择一个配置文件。配置文件名称是 -P 选项的非必需参数。
选项参数的规则如下:
$ gcc -g myprog.c
$ gcc myprog.c -g
这些命令行是等效的。
shell 一词是 Unix 术语,指一种特定类型的命令行解释器。命令行解释器自操作系统诞生之日起就已提供。早期的大型机和个人计算机操作系统要求人们只能通过命令行解释器与其交互。例如,DOS 就提供了一个命令行解释器,它成为了 Microsoft 命令窗口(Microsoft Command Window)的基础,而 Microsoft 命令窗口只是一个 DOS 模拟器。
命令行解释器会显示某种提示符,表示它正在等待您输入命令。在提示符下,您输入命令并按 Enter 键,命令就会被执行,之后提示符会重新出现:
$ hostname
harpo
$
shell 会持续运行,直到您给它一个终止自身的命令,例如 exit。
在 Unix 中,shell 不仅仅是一个命令行解释器;它也是一个编程语言解释器。你可以使用它来定义变量、计算表达式的值、执行 I/O、使用条件控制流语句(例如循环和分支语句)、定义和调用函数等等。
简而言之,它具备高级编程语言(例如 C 语言)的大部分特性。你可以将一系列 Shell 命令保存到一个文件中,以便在下次执行。这样的文件称为 Shell 脚本。你可以用几种不同的方式安排 Shell 执行这些 Shell 脚本。
大多数 Shell 还将各种常用命令实现为 Shell 内部的函数,这些函数被称为 Shell 内置函数(或简称为内置函数)。直接在 Shell 中内置命令可以加快其执行速度,因为调用函数比启动单独的程序(需要内核干预)所需的时间要少得多。
在典型的 Unix 系统中,你可以根据自己的喜好从多个 Shell 中选择你想要使用的 Shell。
最古老的的 Shell 是 Bourne Shell,它是第七版 UNIX(贝尔实验室于 1979 年发布)的一部分,之所以如此命名,是因为它是由 Stephen Bourne [编写的。该 Shell 程序名为 sh,运行它时必须输入 sh。它是 Ken Thompson 编写的原始 UNIX Shell 的第一个扩展。Bourne Shell 非常重要,因为它始终是所有 Unix 发行版的一部分,并且许多管理脚本都是用它编写的,需要安装它。如果在系统中找不到它,某些命令将会失败。其他存在已久的常见 Shell 包括 C Shell (csh) 和 Korn Shell (ksh)。
然而,GNU/Linux 系统中最常用的 Shell 是 Bourne Again Shell,其程序名称为 bash,本书将使用此 Shell。 GNU 项目通过扩展 Bourne shell 并引入 Korn shell 和 C shell 的功能(https://www.gnu.org/software/bash/) 创建了 bash。
历史上,Unix 中的用户是指被授予系统访问权限、可以运行程序和拥有文件的人。Unix 的安全性部分基于以下原则:每个系统用户都必须经过身份验证。身份验证是一种安全审查形式,就像进入建筑物前出示身份证或在机场通过扫描仪一样。
Unix 中的传统身份验证方法为每个用户提供一个唯一的用户名和一个关联的唯一的非负整数用户 ID(简称 UID)。用户名是用户登录系统时输入的名称。每个用户还有一个关联的密码。 Unix 使用用户名/密码对来验证尝试登录的用户。如果用户名不存在或密码不匹配,系统将拒绝该用户。系统文件以加密形式存储密码。
登录系统就是登录到系统中。动词“login”的字典含义之一早在计算机出现之前就存在,意思是像船长或飞行员那样,将某事记录在日志中。“login”一词表达了该操作被记录在日志中的意思。在 Unix 中,登录记录在一个类似于日志的文件中。系统维护着一个允许登录的用户列表。我们认为这个术语是理所当然的。我们之所以将名词“login”单独使用,只是因为它已经成为全球数百万个登录屏幕上的单独单词。“login”作为动词,其真正含义是登录到某个地方;它需要一个间接宾语。
确切地说,在现代 Unix 系统中,用户是任何能够运行程序和拥有文件的实体。这个实体不必是实际的人。由于各种原因,用户的定义被广义化,允许抽象实体以及程序成为用户。例如,root、syslog 和 lp 都是非个人用户。
组是一组用户。正如每个用户都有用户名和用户 ID 一样,每个组都有一个唯一的组名和一个关联的唯一的非负整数组 ID,简称 GID。Unix 使用组来提供一种资源共享的方式。例如,一个文件可以与一个组关联,并且该组中的所有用户都拥有对该文件的相同访问权限。由于程序只是一个可执行文件,程序也是如此;一个可执行程序可以与一个组关联,以便该组的所有成员都拥有运行该程序的相同权限。
每个用户至少属于一个组,称为用户的主组。
您可以使用 id 命令打印您的用户名、用户 ID 以及您所属的所有组的组名和组 ID:
$ id
uid=500(stewart) gid=500(stewart)
groups=500(stewart),4(adm),24(cdrom),27(sudo)
实际上,您可以为 id 指定任何用户名,它会列出这些用户名的信息:
$ id syslog
uid=102(syslog) gid=106(syslog) groups=106(syslog),4(adm),5(tty)
或者,您可以使用 groups 命令打印您(或其他用户)所属的组列表:
$ groups
stewart adm cdrom sudo
$ groups syslog
syslog : syslog adm tty
在 Unix 中,超级用户是一位尊贵的用户,其用户名为(通常)root 权限,UID 为 0。超级用户可以执行普通用户无法执行的操作,例如更改用户名或修改操作系统配置。任何能够以 root 身份登录 Unix 系统的人都拥有对该系统的绝对控制权。因此,大多数 Unix 系统会记录每次以 root 身份登录的尝试,以便系统管理员可以监控并捕获入侵尝试。
为了防止普通用户及其程序访问硬件并执行其他可能破坏计算机系统状态的操作,Unix 要求处理器支持两种操作模式,即特权模式和非特权模式。这两种模式也分别称为管理员模式和用户模式。特权指令可以直接或间接更改系统资源的指令。比如:
只有内核才被允许执行特权指令。普通用户运行的程序只能执行非特权指令。操作系统的安全性、可靠性和完整性取决于这种权力的划分。
在 Unix 系统中,当程序运行时,内核在运行程序之前执行的步骤之一是向其提供一个由“名称-值”对组成的数组,该数组称为环境列表,或简称为环境。列表中的每个“名称-值”对都是一个形式为“名称=值”的字符串,其中“值”是以 NULL 结尾的 C 字符串,并且“=”字符周围没有空格。名称称为环境变量,而“名称=值”称为环境字符串。例如
LOGNAME=stewart
它指定名为 LOGNAME 的变量的值为 stewart。变量名不允许包含 = 字符,除此之外,没有任何限制。但是,为了使用这些变量的程序的可移植性,按照惯例,变量名应该只包含大写字母、数字和下划线,并且不能以数字开头(参见 The Open Group Base Specifications,2018 年第 7 期,第 8 章)。
在此示例中,
COLUMNS=80
COLUMNS 是一个环境变量,其值为 80。即使 80 是一个数字,它也会以字符串形式存储在环境列表中。如果此环境变量存在,它会存储当前打开的终端窗口中的列数,并且当您调整窗口大小时,其值也会相应变化。
环境变量会影响许多程序的行为,包括 Shell 本身。当您登录 Unix 系统时,操作系统会使用系统中各种文件的配置信息为您创建环境。从那时起,每当您运行程序时,它都会继承当前环境值的副本。该程序可以使用环境变量来自定义其行为,也可以修改其自身的环境副本。在本书在线资源 https://nostarch.com/introduction-system-programming-linux 中的在线章节“使用命令界面”中,我解释了环境变量如何传递给程序、如何影响 Shell 的行为以及如何自定义环境变量。在第 10 章中,我将详细解释程序运行时环境变量的表示方式及其在内存中的存储位置。您可以通过多种方式从命令行查看环境变量的值。printenv 命令和 env 命令都可以显示所有环境变量的值。这两种方法产生的行数都可能超过一个屏幕的显示范围。很快您将看到如何一次输出一屏内容。如果您想查看所选环境变量的值,请将它们的名称作为 printenv 命令的参数:
$ printenv LINES COLUMNS SHELL
23
80
/bin/bash
程序可以调用 getenv() 函数来检索特定的环境字符串。为了演示,以下名为 getenv_demo.c 的小程序打印出用户 shell 的名称:
#include
#include
int main()
{
char* shell = getenv("SHELL");
printf("The current shell is %s.n", shell);
};
该程序需要包含 stdio.h 头文件,因为它调用了 printf() 函数;也需要包含 stdlib.h 头文件,因为它调用了 getenv() 函数,而 getenv() 函数在头文件中声明。我们编译并运行该程序,如下所示:
$ gcc getenv_demo.c -o getenv_demo
$ ./getenv_demo
The current shell is /bin/bash.
这预示了如何使用 GNU gcc 编译器编译代码。我们为 gcc 指定源代码文件的名称 getenv_demo.c,并使用 -o getenv_demo 选项将编译器的输出存储在名为 getenv_demo 的可执行文件中。如果没有该选项,可执行文件将存储在名为 a.out 的文件中。
Ritchie 和 Thompson 在他们开创性的文章《The UNIX Time-Sharing System》中指出,操作系统最重要的作用是提供文件系统 。Kernighan 和 Pike 在他们如今已声名显赫的 Unix 环境编程著作《UNIX编程环境》中指出,Ritchie 和 Thompson 在设计 Unix 系统时讨论的第一个方面就是其文件系统的结构,因为这决定了其他所有事物的工作方式;他们甚至声称“UNIX 系统中的一切都是一个文件”。
对于大多数使用计算机的人来说,文件只是存储信息的对象。这些对象通常驻留在非易失性存储设备上,这些存储设备即使在断电的情况下也能保留数据,例如磁带、磁盘、光盘和电子磁盘。(相比之下,易失性存储器,例如主存储器,在断电时不会保留数据。)这些非易失性存储设备被称为辅助存储设备或外部存储设备,即使它们在你看来似乎位于计算机“内部”。这种命名法源于历史。
在许多非 Unix 系统中,操作系统可以识别不同类型的文件,每种文件都有其特定的结构,例如文字处理文档、图像文件或电子表格。事实上,在这些系统中,文件通常具有名称或扩展名,可以用来推断其结构,甚至导致特定程序加载它们。
然而,在 Unix 中,情况却大不相同。从内核的角度来看,普通文件只是一个包含线性字节序列的对象。它不会对此类文件的内容强加任何结构;任何可能的结构都由创建它的用户或程序赋予。这些文件被称为常规文件或纯文本文件。其中一些文件就是我们通常所说的文本文件,因为打开它们时,我们看到的是纯文本。这些文件包含字符序列,行由换行符分隔;用于显示它们的程序使用嵌入的换行符在屏幕上创建行结构。二进制文件是包含不一定是文本字符的字节序列的文件,例如程序的可执行代码。
除了这些常规文件之外,Unix 内核还定义了一小部分文件类型:
设备文件、管道和套接字统称为特殊文件。特殊文件是 Unix 文件系统的一个特殊特性。它们的发明是为了提供一种以设备无关的方式进行 I/O 编程的方法。套接字是一种允许进程之间通信的设备文件,主要用于网络通信。
所有文件,无论其类型如何,都具有属性。属性包含有关文件的所有重要信息,例如文件上次修改时间、上次访问时间、所有者的用户 ID、文件大小(以字节数表示)、允许哪些人对文件进行各种类型的访问等等。描述文件访问限制的属性称为文件模式或文件权限。
文件的属性统称为文件状态。“状态”一词可能听起来有点误导,但它是 Ritchie 和 Thompson 在原始 UNIX 系统中使用的词。另一个经常用于描述文件属性或状态的词是元数据。 Unix 系统对文件的内容和状态进行了明确的区分。内容是文件的数据;大多数(但并非所有)文件都有内容。某些文件,例如设备文件和某些其他特殊文件,没有内容;它们不存储数据。它们是内核用来实现设备无关的输入和输出的接口。
文件的内容不包含任何状态信息。例如,它们没有文件结束符来表示文件结束,也没有任何其他表示文件长度的方式。内容和状态甚至不存储在一起。状态存储在称为 inode 的数据结构中,而内容可能分散在与 inode 相同的存储设备上的多个块中。
关于文件的一个重要事实是,文件名不属于文件状态。事实上,非目录文件可以有多个名称,这些名称并非文件本身的固有属性,而是包含它们的目录的固有属性。
目录,在其他操作系统中通常称为文件夹,是一种文件类型,从用户的角度来看,它似乎包含其他文件。
这只是一种假象;目录并不包含文件,就像目录并不包含书籍的章节一样。那么,什么是目录?准确地说,目录是一个包含目录条目表的文件,这些条目是实际称为链接。链接是一个将文件名与实际文件关联的对象。它包含两个部分:文件名和对文件 inode 的引用。链接可以引用任何类型的文件,包括目录,这意味着目录可以是目录的成员。但是,链接不能引用与目录本身位于不同设备上的文件。目录永远不会为空,因为每个目录都包含两个链接,分别名为 .(点)和 ..(点-点)。这些条目具有预定义的含义:. 是指向目录本身的链接,.. 是指向包含此目录的目录的链接,该目录称为父目录。
左侧列中的数字仅供参考,表示对给定文件 inode 的引用。例如,drivers 是此目录中 inode 编号为 185 的文件的名称。
当您在 shell 中工作时,它会为您维护一个唯一的目录,称为当前工作目录。
ls 命令可以显示目录的内容。输入不带参数的 ls 会显示当前工作目录的内容:
或者,我们可以将一个或多个目录名作为 ls 的参数来查看其内容:
请注意,每个目录的名称都先出现,然后是该目录中的文件。 ls 使用的列数取决于目录中名称的数量及其长度。
我们可以使用 cd 命令更改当前工作目录:
请注意,现在 ls 命令显示的是新工作目录(即 chapters)的内容。我们可以通过.. 链接返回上一个目录:
ls 的输出显示工作目录再次成为 chapters 的父目录,因为文件名列表与我们将目录更改为 chapters 之前相同。
如前所述,文件和文件名是不同的东西。文件名是用于命名文件的字符串。它是目录中链接的一部分。一个非目录文件可能在不同的目录中(在同一逻辑设备上)拥有名称,因此看起来像是多个目录的成员。
但是,文件的存在与它们所在的目录无关。如果同一个文件在不同的目录中拥有名称,则链接中与这些名称关联的引用都指向同一个 inode,即该文件的唯一 inode。这就像一个人带着几本护照旅行。这些护照可能代表不同的人,并且在不同的国家/地区使用,但它们代表同一个人。
在此图中,一个文件有三个不同的名称,每个名称都是指向不同目录的链接。文件名可以很长。文件名的最大字符数由系统相关的常量 NAME_MAX 定义,通常为 255 个字符。文件名可以包含除正斜杠 (/) 和空字符 (
参与评论
手机查看
返回顶部