找回密码
 立即注册
首页 业界区 安全 Linux系统编程1核心概念1.2: Unix 的基本概念 ...

Linux系统编程1核心概念1.2: Unix 的基本概念

庞悦 11 小时前
1.2 Unix 的基本概念

本节介绍构成 Unix 操作系统设计基础的核心概念。Unix 从诞生之初就围绕着一小套巧妙的理念而设计,正如其创始人 Dennis Ritchie 和 Ken Thompson 所说:“UNIX 的成功并非在于新的发明,而在于充分利用精心挑选的一套富有成果的理念,尤其在于证明它们可以成为实现一个精巧而强大的操作系统的关键”。这些“富有成果的理念”按讨论顺序包括:可编程 Shell、用户和组、特权和非特权指令、环境、文件和目录层次结构、设备无关的输入和输出,以及最重要的进程。本节将简要介绍这些概念,但不会过于详细,首先会简要概述这个“精巧而强大的操作系统”(现在称为 Unix 内核)。在此过程中,我会介绍一些 Unix 命令来演示这些概念。
1.2.1 Unix 内核

“操作系统(operating system)”一词没有一个统一的、普遍认可的定义,这或许令人遗憾。如果你查阅几乎所有关于操作系统的教科书,你会发现关于操作系统构成有两种不同的观点:

  • 操作系统是所有软件的集合,这些软件为应用程序和用户提供服务,并管理和保护所有硬件资源。在这种观点下,用户界面和浏览器等工具是操作系统的一部分。
  • 操作系统只是在启动时加载到内存中并一直保留在内存中,控制所有计算机资源,直到计算机关闭的程序。
无论你决定采用哪种定义,“内核”一词都毫无疑问地被用作第二种定义的另一个名称。这是一个恰当的名称,因为它是 Unix 系统的核心。在关于 4.4 BSD 操作系统设计的开创性著作《4.4BSD 操作系统的设计和实现》中,McKusick 及其合著者将内核定义为“一个小型的软件核心,它仅提供实现其他操作系统服务所需的最低限度的功能”。在本书中,我使用了操作系统的狭义定义,即它就是内核,仅此而已。
内核是一个程序,或者说是一组交互程序的集合,具体取决于 Unix 的具体实现,它具有多个入口点。入口点是程序中可以开始执行的指令。每个入口点都提供内核执行的一项服务。如果您习惯于认为程序总是从第一行开始,这可能会让您感到困惑。
您目前编写的程序很可能只有一个入口点,即 main() 函数。但是,您可以创建具有多个入口点的代码。软件库是具有多个入口点的代码模块。您可以将入口点视为可被其他程序调用的函数。它们执行诸如打开、读取和写入文件、创建新进程、分配内存等服务。每个函数都需要一定数量、特定类型的参数,并产生定义明确的结果。内核入口点的集合构成了其 API 的很大一部分。实际上,您可以将内核视为包含一组独立函数的集合,这些函数捆绑在一起,构成一个大包,而其 API 则是这些函数的签名或原型的集合。
1.2.1.1 内核角色和职责

当 Unix 系统启动时,固件和软件会将内核加载到称为系统空间或内核空间的内存部分,并驻留在该空间直到机器关闭。用户程序不会允许访问系统空间。如果它们尝试访问,内核会终止它们。
内核对连接到计算机的所有硬件拥有完全访问权限。内核维护各种系统资源,以便为用户程序提供服务。这些系统资源包括许多不同的数据结构,例如,用于跟踪输入/输出 (I/O)、内存和设备使用情况的数据结构。
Unix 内核管理和保护所有这些资源,并提供一个允许所有用户高效、安全、愉快地工作的操作环境。它阻止用户及其运行的程序直接访问任何硬件资源。换句话说,如果用户正在运行的程序想要读取或写入磁盘,它必须请求内核代表它执行此操作,而不是自行执行。内核将执行该任务,并将任何数据传输到用户程序可以访问的内存区域或从中传输数据。
要理解为什么这是必要的,不妨想象一下,如果用户的程序可以直接访问硬盘会发生什么。用户可以运行一个程序,该程序可能会试图获取所有磁盘空间,甚至更糟的是,试图擦除磁盘,从而破坏内核保护其资源的能力。
Unix 内核还保护用户之间免受其他用户的侵害,并保护自身免受用户的侵害,同时让用户觉得他们每个人都拥有完全属于自己的计算机。每个人都可以运行程序,感觉就像他们拥有了计算机,就好像没有其他人在使用这台机器一样。用户拥有自己的磁盘空间、自己的私有内存、公平的 CPU 使用时间份额等等。为了实现这些目标,Unix 的发明者在其设计中融入了以下几项关键原则:

  • 系统指定了两级权限(用户权限和内核权限),以确保某些指令只能在内核权限下执行。
  • 每个用户都有唯一的身份。特权用户可以创建用户组,这些用户组也拥有唯一的身份。这些用户和组标识符被赋予了所有用户资源(例如磁盘存储、正在运行的程序等)的权限和保护。
  • 文件系统支持创建、修改、检索和删除持久化数据和程序,以及隐私、保护和共享软件和数据的能力。
  • 物理内存分为两个区域:用户空间(用于加载普通用户程序)和系统空间(用于存储操作系统本身)。
  • 内核对处理器的使用拥有独占控制权,并且它随时决定下一步运行什么。
  • 内核拥有将程序加载到内存、运行和终止程序的独占能力。正在运行的程序甚至无法自行终止;它所能做的最好的事情就是请求内核终止它!
  • 内核对所有计算机硬件拥有完全且独占的控制权。
1.2.1.2 内核服务

服务类型:

  • 进程调度和管理
  • I/O 处理
  • 物理和虚拟内存管理
  • 设备管理
  • 文件系统管理
  • 信令和进程间通信
  • 多线程
  • 保护和安全
  • 网络服务
1.png

内核区域内的每个方框代表不同的服务类别。标有“系统调用”的方框代表程序用来请求和获取这些服务的 API 部分,而标有“系统程序”的方框则代表用户可以运行以获取这些服务的独立程序集合。
1.2.2 Shell 和命令

内核为正在运行的程序提供服务,但不直接向用户提供服务;用户通过在终端窗口中运行的命令行解释器输入命令或与图形用户界面 (GUI) 交互来与 Unix 交互,本书不讨论图形用户界面 (GUI)。命令行解释器是一个读取命令并执行命令的程序。
1.2.2.1 命令

命令是通过输入文本(通常(但并非总是)使用键盘)输入的指令。命令后面可能包含选项和参数。选项修改命令的行为,而参数是命令的输入。例如:
  1. $ gcc -g -o myprog myprog.c
复制代码
以下列表解释了该命令行的各个部分:

  • gcc 命令名称(GNU 编译器集合:he GNU Compiler Collectio)。
  • -g: gcc 的一个选项,指示其在生成的可执行文件中包含调试信息。
  • -o myprog带有选项参数的选项,myprog。-o 选项指示 gcc 将输出放入紧随其后的文件中,在本例中为 myprog,这是它的参数。
  • myprog.c 该命令的唯一参数,即其输入文件的名称。
命令行是您输入的所有内容,但不包括按 Enter 键时产生的换行符。在此示例中,
命令是整个命令行,但有时一行可以包含多个命令,这些命令由命令分隔符(例如分号)分隔,例如:
  1. $ gcc -g -o myprog myprog.c ; gcc -g -o hello hello.c
复制代码
从技术上讲,简单命令是单个命令,而不是一系列命令。当我们使用术语“命令”时,通常指的是简单命令。
在 GNU/Linux 和其他一些 Unix 系统中,某些命令有两种命令选项:短选项和长选项:

  • 短选项以单短划线 (-) 开头,包含单个字符,例如 -a 和 -H。
  • 长选项以双短划线 (--) 开头,可以是单词,例如 --date 和 --file-type。
POSIX.1-2024没要求提供长选项,但 GNU/Linux 提供了长选项。
两种类型的选项都可以包含选项参数。例如,在
  1. $ gcc -g -o myprog myprog.c ; gcc -g -o hello hello.c
复制代码
-o 选项包含 myprog 参数。
在符合 POSIX.1-2024 标准的 Unix 系统中,如果选项包含参数,则该参数是必需的;您不能省略它。另一方面,GNU/Linux 允许命令包含带有非必需参数的选项。例如,您可以在 GNU/Linux 的命令行中输入 Firefox 浏览器的名称来启动它:
  1. $ firefox
复制代码
如果您为其指定 -P myprofile 选项,它将使用名为 myprofile 的用户配置文件启动。如果您只输入
  1. $ firefox -P
复制代码
它会显示一个对话框,要求您从列表中选择一个配置文件。配置文件名称是 -P 选项的非必需参数。
选项参数的规则如下:

  • 短选项的参数紧跟在选项后面,中间可能包含空格或制表符,例如 -ohello 或 -o hello。唯一的例外是可选参数前面不能有空格。
  • 长选项的参数紧跟在 = 运算符后面,中间不能包含空格,例如--date='Jan 01,1970'。
    典型的命令由命令名、选项和参数组成,但有些命令允许选项和参数混合使用。例如:
  1. $ gcc -g myprog.c
  2. $ gcc myprog.c -g
复制代码
这些命令行是等效的。
1.2.2.2 Shell

shell 一词是 Unix 术语,指一种特定类型的命令行解释器。命令行解释器自操作系统诞生之日起就已提供。早期的大型机和个人计算机操作系统要求人们只能通过命令行解释器与其交互。例如,DOS 就提供了一个命令行解释器,它成为了 Microsoft 命令窗口(Microsoft Command Window)的基础,而 Microsoft 命令窗口只是一个 DOS 模拟器。
命令行解释器会显示某种提示符,表示它正在等待您输入命令。在提示符下,您输入命令并按 Enter 键,命令就会被执行,之后提示符会重新出现:
  1. $ hostname
  2. harpo
  3. $
复制代码
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。
1.2.3 用户和组

历史上,Unix 中的用户是指被授予系统访问权限、可以运行程序和拥有文件的人。Unix 的安全性部分基于以下原则:每个系统用户都必须经过身份验证。身份验证是一种安全审查形式,就像进入建筑物前出示身份证或在机场通过扫描仪一样。
Unix 中的传统身份验证方法为每个用户提供一个唯一的用户名和一个关联的唯一的非负整数用户 ID(简称 UID)。用户名是用户登录系统时输入的名称。每个用户还有一个关联的密码。 Unix 使用用户名/密码对来验证尝试登录的用户。如果用户名不存在或密码不匹配,系统将拒绝该用户。系统文件以加密形式存储密码。
登录系统就是登录到系统中。动词“login”的字典含义之一早在计算机出现之前就存在,意思是像船长或飞行员那样,将某事记录在日志中。“login”一词表达了该操作被记录在日志中的意思。在 Unix 中,登录记录在一个类似于日志的文件中。系统维护着一个允许登录的用户列表。我们认为这个术语是理所当然的。我们之所以将名词“login”单独使用,只是因为它已经成为全球数百万个登录屏幕上的单独单词。“login”作为动词,其真正含义是登录到某个地方;它需要一个间接宾语。
确切地说,在现代 Unix 系统中,用户是任何能够运行程序和拥有文件的实体。这个实体不必是实际的人。由于各种原因,用户的定义被广义化,允许抽象实体以及程序成为用户。例如,root、syslog 和 lp 都是非个人用户。
组是一组用户。正如每个用户都有用户名和用户 ID 一样,每个组都有一个唯一的组名和一个关联的唯一的非负整数组 ID,简称 GID。Unix 使用组来提供一种资源共享的方式。例如,一个文件可以与一个组关联,并且该组中的所有用户都拥有对该文件的相同访问权限。由于程序只是一个可执行文件,程序也是如此;一个可执行程序可以与一个组关联,以便该组的所有成员都拥有运行该程序的相同权限。
每个用户至少属于一个组,称为用户的主组。
您可以使用 id 命令打印您的用户名、用户 ID 以及您所属的所有组的组名和组 ID:
  1. $ id
  2. uid=500(stewart) gid=500(stewart)
  3. groups=500(stewart),4(adm),24(cdrom),27(sudo)
复制代码
实际上,您可以为 id 指定任何用户名,它会列出这些用户名的信息:
  1. $ id syslog
  2. uid=102(syslog) gid=106(syslog) groups=106(syslog),4(adm),5(tty)
复制代码
或者,您可以使用 groups 命令打印您(或其他用户)所属的组列表:
  1. $ groups
  2. stewart adm cdrom sudo
  3. $ groups syslog
  4. syslog : syslog adm tty
复制代码
在 Unix 中,超级用户是一位尊贵的用户,其用户名为(通常)root 权限,UID 为 0。超级用户可以执行普通用户无法执行的操作,例如更改用户名或修改操作系统配置。任何能够以 root 身份登录 Unix 系统的人都拥有对该系统的绝对控制权。因此,大多数 Unix 系统会记录每次以 root 身份登录的尝试,以便系统管理员可以监控并捕获入侵尝试。
1.2.4 特权和非特权指令

为了防止普通用户及其程序访问硬件并执行其他可能破坏计算机系统状态的操作,Unix 要求处理器支持两种操作模式,即特权模式和非特权模式。这两种模式也分别称为管理员模式和用户模式。特权指令可以直接或间接更改系统资源的指令。比如:

  • 获取更多内存
  • 更改系统时间
  • 提高正在运行进程的优先级
  • 读取或写入磁盘
  • 进入特权模式
只有内核才被允许执行特权指令。普通用户运行的程序只能执行非特权指令。操作系统的安全性、可靠性和完整性取决于这种权力的划分。
参考资料


  • 软件测试精品书籍文档下载持续更新 https://github.com/china-testing/python-testing-examples 请点赞,谢谢!
  • 本文涉及的python测试开发库 谢谢点赞! https://github.com/china-testing/python_cn_resouce
  • python精品书籍下载 https://github.com/china-testing/python_cn_resouce/blob/main/python_good_books.md
  • Linux精品书籍下载 https://www.cnblogs.com/testing-/p/17438558.html
  • python八字排盘 https://github.com/china-testing/bazi
  • 联系方式:钉ding或V信: pythontesting
1.2.5 环境

在 Unix 系统中,当程序运行时,内核在运行程序之前执行的步骤之一是向其提供一个由“名称-值”对组成的数组,该数组称为环境列表,或简称为环境。列表中的每个“名称-值”对都是一个形式为“名称=值”的字符串,其中“值”是以 NULL 结尾的 C 字符串,并且“=”字符周围没有空格。名称称为环境变量,而“名称=值”称为环境字符串。例如
  1. LOGNAME=stewart
复制代码
它指定名为 LOGNAME 的变量的值为 stewart。变量名不允许包含 = 字符,除此之外,没有任何限制。但是,为了使用这些变量的程序的可移植性,按照惯例,变量名应该只包含大写字母、数字和下划线,并且不能以数字开头(参见 The Open Group Base Specifications,2018 年第 7 期,第 8 章)。
在此示例中,
  1. COLUMNS=80
复制代码
COLUMNS 是一个环境变量,其值为 80。即使 80 是一个数字,它也会以字符串形式存储在环境列表中。如果此环境变量存在,它会存储当前打开的终端窗口中的列数,并且当您调整窗口大小时,其值也会相应变化。
环境变量会影响许多程序的行为,包括 Shell 本身。当您登录 Unix 系统时,操作系统会使用系统中各种文件的配置信息为您创建环境。从那时起,每当您运行程序时,它都会继承当前环境值的副本。该程序可以使用环境变量来自定义其行为,也可以修改其自身的环境副本。在本书在线资源 https://nostarch.com/introduction-system-programming-linux 中的在线章节“使用命令界面”中,我解释了环境变量如何传递给程序、如何影响 Shell 的行为以及如何自定义环境变量。在第 10 章中,我将详细解释程序运行时环境变量的表示方式及其在内存中的存储位置。您可以通过多种方式从命令行查看环境变量的值。printenv 命令和 env 命令都可以显示所有环境变量的值。这两种方法产生的行数都可能超过一个屏幕的显示范围。很快您将看到如何一次输出一屏内容。如果您想查看所选环境变量的值,请将它们的名称作为 printenv 命令的参数:
  1. $ printenv LINES COLUMNS SHELL
  2. 23
  3. 80
  4. /bin/bash
复制代码
程序可以调用 getenv() 函数来检索特定的环境字符串。为了演示,以下名为 getenv_demo.c 的小程序打印出用户 shell 的名称:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main()
  4. {
  5.     char* shell = getenv("SHELL");
  6.     printf("The current shell is %s.\n", shell);
  7. };
复制代码
该程序需要包含 stdio.h 头文件,因为它调用了 printf() 函数;也需要包含 stdlib.h 头文件,因为它调用了 getenv() 函数,而 getenv() 函数在头文件中声明。我们编译并运行该程序,如下所示:
  1. $ gcc getenv_demo.c -o getenv_demo
  2. $ ./getenv_demo
  3. The current shell is /bin/bash.
复制代码
这预示了如何使用 GNU gcc 编译器编译代码。我们为 gcc 指定源代码文件的名称 getenv_demo.c,并使用 -o getenv_demo 选项将编译器的输出存储在名为 getenv_demo 的可执行文件中。如果没有该选项,可执行文件将存储在名为 a.out 的文件中。
1.2.6 文件、目录和单目录层次结构

Ritchie 和 Thompson 在他们开创性的文章《The UNIX Time-Sharing System》中指出,操作系统最重要的作用是提供文件系统 。Kernighan 和 Pike 在他们如今已声名显赫的 Unix 环境编程著作《UNIX编程环境》中指出,Ritchie 和 Thompson 在设计 Unix 系统时讨论的第一个方面就是其文件系统的结构,因为这决定了其他所有事物的工作方式;他们甚至声称“UNIX 系统中的一切都是一个文件”。
1.2.6.1 文件

对于大多数使用计算机的人来说,文件只是存储信息的对象。这些对象通常驻留在非易失性存储设备上,这些存储设备即使在断电的情况下也能保留数据,例如磁带、磁盘、光盘和电子磁盘。(相比之下,易失性存储器,例如主存储器,在断电时不会保留数据。)这些非易失性存储设备被称为辅助存储设备或外部存储设备,即使它们在你看来似乎位于计算机“内部”。这种命名法源于历史。
在许多非 Unix 系统中,操作系统可以识别不同类型的文件,每种文件都有其特定的结构,例如文字处理文档、图像文件或电子表格。事实上,在这些系统中,文件通常具有名称或扩展名,可以用来推断其结构,甚至导致特定程序加载它们。
然而,在 Unix 中,情况却大不相同。从内核的角度来看,普通文件只是一个包含线性字节序列的对象。它不会对此类文件的内容强加任何结构;任何可能的结构都由创建它的用户或程序赋予。这些文件被称为常规文件或纯文本文件。其中一些文件就是我们通常所说的文本文件,因为打开它们时,我们看到的是纯文本。这些文件包含字符序列,行由换行符分隔;用于显示它们的程序使用嵌入的换行符在屏幕上创建行结构。二进制文件是包含不一定是文本字符的字节序列的文件,例如程序的可执行代码。
1.2.6.2 文件类型

除了这些常规文件之外,Unix 内核还定义了一小部分文件类型:

  • 目录
  • 设备文件
  • 管道
  • 套接字
  • 符号链接
设备文件、管道和套接字统称为特殊文件。特殊文件是 Unix 文件系统的一个特殊特性。它们的发明是为了提供一种以设备无关的方式进行 I/O 编程的方法。套接字是一种允许进程之间通信的设备文件,主要用于网络通信。
1.2.6.3 文件属性、权限和内容

所有文件,无论其类型如何,都具有属性。属性包含有关文件的所有重要信息,例如文件上次修改时间、上次访问时间、所有者的用户 ID、文件大小(以字节数表示)、允许哪些人对文件进行各种类型的访问等等。描述文件访问限制的属性称为文件模式或文件权限。
文件的属性统称为文件状态。“状态”一词可能听起来有点误导,但它是 Ritchie 和 Thompson 在原始 UNIX 系统中使用的词。另一个经常用于描述文件属性或状态的词是元数据。 Unix 系统对文件的内容和状态进行了明确的区分。内容是文件的数据;大多数(但并非所有)文件都有内容。某些文件,例如设备文件和某些其他特殊文件,没有内容;它们不存储数据。它们是内核用来实现设备无关的输入和输出的接口。
文件的内容不包含任何状态信息。例如,它们没有文件结束符来表示文件结束,也没有任何其他表示文件长度的方式。内容和状态甚至不存储在一起。状态存储在称为 inode 的数据结构中,而内容可能分散在与 inode 相同的存储设备上的多个块中。
关于文件的一个重要事实是,文件名不属于文件状态。事实上,非目录文件可以有多个名称,这些名称并非文件本身的固有属性,而是包含它们的目录的固有属性。
1.2.6.4 目录

目录,在其他操作系统中通常称为文件夹,是一种文件类型,从用户的角度来看,它似乎包含其他文件。
2.png

这只是一种假象;目录并不包含文件,就像目录并不包含书籍的章节一样。那么,什么是目录?准确地说,目录是一个包含目录条目表的文件,这些条目是实际称为链接。链接是一个将文件名与实际文件关联的对象。它包含两个部分:文件名和对文件 inode 的引用。链接可以引用任何类型的文件,包括目录,这意味着目录可以是目录的成员。但是,链接不能引用与目录本身位于不同设备上的文件。目录永远不会为空,因为每个目录都包含两个链接,分别名为 .(点)和 ..(点-点)。这些条目具有预定义的含义:. 是指向目录本身的链接,.. 是指向包含此目录的目录的链接,该目录称为父目录。
3.png

左侧列中的数字仅供参考,表示对给定文件 inode 的引用。例如,drivers 是此目录中 inode 编号为 185 的文件的名称。
当您在 shell 中工作时,它会为您维护一个唯一的目录,称为当前工作目录。
ls 命令可以显示目录的内容。输入不带参数的 ls 会显示当前工作目录的内容:
4.png

或者,我们可以将一个或多个目录名作为 ls 的参数来查看其内容:
5.png

请注意,每个目录的名称都先出现,然后是该目录中的文件。 ls 使用的列数取决于目录中名称的数量及其长度。
我们可以使用 cd 命令更改当前工作目录:
6.png

请注意,现在 ls 命令显示的是新工作目录(即 chapters)的内容。我们可以通过.. 链接返回上一个目录:
7.png

ls 的输出显示工作目录再次成为 chapters 的父目录,因为文件名列表与我们将目录更改为 chapters 之前相同。
1.2.6.5 文件名

如前所述,文件和文件名是不同的东西。文件名是用于命名文件的字符串。它是目录中链接的一部分。一个非目录文件可能在不同的目录中(在同一逻辑设备上)拥有名称,因此看起来像是多个目录的成员。
但是,文件的存在与它们所在的目录无关。如果同一个文件在不同的目录中拥有名称,则链接中与这些名称关联的引用都指向同一个 inode,即该文件的唯一 inode。这就像一个人带着几本护照旅行。这些护照可能代表不同的人,并且在不同的国家/地区使用,但它们代表同一个人。
8.png

在此图中,一个文件有三个不同的名称,每个名称都是指向不同目录的链接。文件名可以很长。文件名的最大字符数由系统相关的常量 NAME_MAX 定义,通常为 255 个字符。文件名可以包含除正斜杠 (/) 和空字符 (\0) 之外的几乎任何字符,但即使允许,也不应该在文件名中使用某些字符。例如,文件名可以包含空格和换行符,但如果包含空格和换行符,通常需要用引号将文件名括起来,以便将其用作命令的参数。某些字符(例如 $、&、* 等)对不同的程序具有不同的含义,如果在这些环境中使用,则必须在它们前面加上反斜杠进行转义,所以最好避免使用它们。惯例是在文件名中只使用字母数字字符、下划线和连字符。
Unix 区分大小写,因此 source 和 Source 会被视为两个不同的文件名。
与大多数其他操作系统不同,Unix 不使用文件扩展名用于任何目的,尽管编译器和文字处理器等用户级软件可能会使用它们作为参考。GNOME 和 KDE 等桌面环境可以基于文件扩展名创建关联,其方式与 Windows 和 macOS 大致相同,但 Unix 本身没有基于内容的文件类型概念,它为所有文件提供相同的操作集,无论其类型如何。在 Unix 中,我们使用单词suffix 来表示文件名中句点后的部分,例如 myprog.c 中的 c。
1.2.6.6 目录层次结构

Unix 将文件组织成树状层次结构,大多数人错误地将其称为文件系统。更准确地说,它应该称为目录层次结构,因为“文件系统”一词指的是写入非结构化磁盘设备上的一组数据结构,用于创建和管理文件和目录。这个树状层次结构中的每个节点要么是非目录文件,要么是目录。每条边都是一条有向边,从非空目录到该目录包含的每个文件(包括目录文件),我们将包含的文件称为该目录的子节点。
目录被称为这些子节点的父节点。这个层次结构的基目录是一个根目录,其名称为 / 字符。即使基目录名为 /,当人们提到这个目录时,他们通常称之为根目录,因为“斜杠”不太直观,而且读起来也比较拗口。
由于单个文件可以在不同的目录中使用同一个名称,因此一个文件可能有多个父节点。这就是为什么层次结构类似于树形结构但并非真正的树形结构,因为在树形结构中,每个节点都有一个唯一的父节点。
在典型的现代 Unix 系统中,目录层次结构是一个有向无环图,即一个不含环路的有向图。它之所以不含环路,是因为目录与非目录文件不同,它不能拥有多个名称,这意味着它只能位于一个父目录中。这意味着任何后代节点都不能指向它,因此该图不含环路。某些 Unix 实现允许超级用户为目录赋予多个名称,在这种情况下,层次结构可能存在环路。
单一目录层次结构的概念是 Unix 的一个定义性特征。其他操作系统,例如 Microsoft Windows,为每个不同的设备都设置了单独的目录层次结构。在 Unix 中,即使这棵树中的文件可能位于不同的设备上,任何设备上的目录层次结构都可以通过一个称为挂载的过程连接到这棵树上。
挂载到树上后,其文件就可以像访问所有其他文件一样被访问。典型的 Unix 目录层次结构在根目录下有多个目录。这些目录被称为顶级目录。
9.png

以下列表描述了大多数 Unix 系统中存在的顶级目录。 POSIX.1-2024 实际需要的目录只有/dev 和 /tmp。

  • bin: 所有必需的二进制可执行文件,包括计算机在单用户模式(类似于 Windows 中的安全模式)下运行时必须可用的 Shell 命令。
  • boot 引导加载程序的静态文件。
  • dev 必需的设备文件。
  • etc: 几乎所有主机配置文件,大致类似于 Windows 的注册表文件。
  • home 所有用户的主目录(如果存在)。
  • lib: 必需的共享库和内核模块。
  • media:可移动介质的挂载点。
    mnt
  • 临时挂载文件系统的挂载点。
  • opt: 附加应用软件包。
  • sbin: 必需的系统二进制文件。
  • srv: 系统提供的服务数据。
  • tmp: 应用程序创建的临时文件。
  • usr: 最初,它是用户数据文件层次结构的顶层,但现在它位于包含非必要二进制文件、库和源代码的层次结构的顶层。典型的子目录有:/usr/bin 和 /usr/sbin,包含二进制文件;/usr/lib,包含库文件;以及 /usr/local,位于第三层本地程序和数据的顶层。
  • var: 可变文件,指内容可以更改的文件。
所有文件,包括目录,都可以通过两个独立的二进制属性来表征:可共享性和可变性。可共享文件可以存储在一台主机上,并在其他主机上使用。不可共享文件则不可共享。例如,用户主目录是可共享的,因为它们不依赖于它们的存储位置,而引导加载程序文件特定于给定的机器,并且不可共享。
可变文件是内容可以更改的文件,而静态文件是内容不能更改的文件。例如,它们包括可执行二进制文件、库、文档文件以及其他在计算机日常运行中通常不会更改的文件。在现代 Unix 系统中,文件的可共享性和可变性是决定哪些文件位于层次结构的哪些部分的因素。具有这两种属性之一的文件被放置在不同的目录中,这使得在不同文件系统上存储具有不同使用特性的文件变得容易,也使备份更容易。例如,/etc 目录是不可共享的——它包含特定于特定计算机的文件——并且它是静态的,因为它的内容是配置文件,只有在我们应用更新、安装新软件或超级用户决定更改配置时才会修改。 /var 目录之所以如此命名,是因为它是可变的。它包含许多不同类型的日志文件,内核和应用程序会定期更新这些日志文件。它的某些子目录(例如 /var/mail)可能是可共享的,而其他一些子目录(例如 /var/log)则可能是不可共享的。/usr 目录是可共享的静态目录。它包含应用程序二进制文件、库和静态数据。
1.2.6.7 符号链接

普通链接是指向文件 inode 的目录条目,而符号链接的内容仅仅是另一个文件的名称。链接指向的文件称为链接的目标。符号链接的 inode 将该文件标识为符号链接。它类似于 Windows 操作系统中的快捷方式。符号链接通常称为软链接,而普通链接则称为硬链接。
通常,当命令、程序和内核本身在预期文件名的情况下被赋予符号链接时,它们将对链接的目标而不是链接本身进行操作。它们很容易看出该文件是符号链接,因为 inode 指示了这一点。当打开链接访问其目标时,我们说链接被取消引用或被跟踪。
符号链接会给操作系统和应用程序带来风险,因为它可能存在循环引用和无限循环。危险在于符号链接可能指向目录,这意味着如果程序跟踪符号链接,它可能会返回到它已经访问过的目录,最终陷入循环。
1.2.6.8 路径名

路径名是标识文件的字符串。路径名有两种类型:绝对路径名和相对路径名。绝对路径名从目录层次结构的根目录开始,并以斜杠 / 开头。以斜杠开头的斜杠后跟零个或多个文件名,例如/data/jammy/kernel/sched/sched.h。除最后一个文件名外,所有文件名都必须是目录名或以目录名为目标的符号链接。示例路径名中除 sched.h 之外的每个名称都是目录。路径中的最后一个名称可以是任何类型的文件。其他绝对路径名示例包括 /usr/bin/、/usr/local/share/man 和 /home/stewart/unixbook/figures/figure01.png。
如果路径名中的最后一个文件名是目录,则可以用斜杠终止路径名,例如路径名 /usr/bin/。
如果您在路径中的名称之间意外插入了多个斜杠,它将被忽略。两个绝对路径名 /usr/local/share/man和 /usr/local///share/man 是相同的。
如果路径名不以斜杠开头,则称为相对路径名。相对路径名从当前工作目录开始,我们现在可以准确地定义它了。当前工作目录(也称为
当前工作目录)是任何正在运行的程序用来解析不以 / 开头的路径名的目录。例如,如果当前工作目录是/home/stewart/unix_book,则路径名 chapters/chapter_01指向一个绝对路径名为 /home/stewart/unix_book/chapters/chapter_01 的文件。
环境变量 PWD 包含当前工作目录的绝对路径名。 pwd 命令打印 PWD 的值:
  1. $ pwd
  2. /home/stewart/unix_book
  3. $ printenv PWD
  4. /home/stewart/unix_book
复制代码
如果路径名包含符号链接,路径名可能会变得很长,而Unix 系统会限制路径名的长度,以字节为单位。POSIX.1-2024 规定,常量 PATH_MAX 是路径名允许的最大字节数,包括终止 NULL 字节。在许多 Linux 系统上,该值是 4096 字节。
1.2.6.9 进程

程序是用编程语言编写的一系列指令序列,这些指令序列发送给计算机。编程语言可以是高级语言,例如 C 或 C++,也可以是低级语言,例如汇编语言。通常,程序不能以其编写的形式直接执行;必须先将其转换为可执行文件。例外情况是用脚本语言编写的程序,例如 JavaScript、PHP 和 BASIC。这些程序不会被转换为可执行文件;解释器程序会直接读取源代码,并逐行执行其中的指令。我们将程序的第一种形式称为源代码,将第二种形式称为可执行代码,或者简称为可执行文件。
大多数程序的可执行形式并不是我们真正可以运行的。我们不能直接将其加载到内存中,然后告诉机器从它的第一个字节开始运行该文件。该文件通常由可执行代码、各种表以及链接器/加载器指令组成。当你输入命令 $ ./hello_world 时,会发生一系列操作,使链接器/加载器使用 hello_world 可执行文件中的信息将文件及其所需的任何共享对象加载到内存中,准备执行程序,并运行该程序。
多个用户可以在给定的机器上同时运行一个程序,或者单个用户可以在不同的终端窗口中多次运行一个程序。无论哪种方式,都意味着一个可执行文件可以有多个正在运行的实例,这引导我们区分程序和进程。进程是正在运行的程序的一个实例。每个单独的实例都是一个不同的进程,尽管它们都在执行同一个可执行文件。
这个进程的正式定义并没有真正告诉你进程的具体含义这就像将棒球比赛定义为亚历山大·卡特赖特 (Alexander Cartwright) 于 1845 年创建的规则集的一个实现实例,该规则集规定两支球队在运动场上相互竞争。这两种定义都无法让您清楚地了解所定义的内容。让我们更具体一点。
当程序在计算机上运行时,它会使用各种资源,例如主内存和辅助存储空间;内核内存(内核空间),用于各种映射和表,例如它使用主内存哪些部分的表;权限,例如读取或写入某些文件或设备的权限;等等。因此,在任何时刻,一个进程都与分配给正在运行的程序实例的所有资源的集合相关联,以及表征该实例的任何其他属性和设置,例如处理器寄存器的值。因此,尽管进程的概念听起来很抽象,但实际上它是一个非常具体的事物,操作系统必须对其进行管理。
Unix 系统为每个进程分配一个唯一的非负整数,称为进程标识符,简称 PID。我们可以使用 ps 命令来了解一些有关进程的知识,该命令可以显示正在运行的进程列表,以及每个进程的特定信息。它有各种选项来控制显示哪些进程以及输出哪些信息。在最简单的形式下,即不使用任何选项,我们可以使用它来查看我们自己正在运行的进程的 PID:
10.png

这列出了两个进程:一个运行 bash,另一个运行 ps 命令本身。它们耗时极短,显示为零,它们的 PID 分别为 10278 和 11087。它们都运行在设备名为 pts/0 的终端中。
我们可以调用 getpid() 函数来获取调用它的进程的 PID。我们在 getpid_demo.c 程序中演示了这一点:getpid_demo.c。
  1. #include <stdio.h>
  2. #include <unistd.h>
  3. int main()
  4. {
  5.     printf("I am the process with process-id %d\n", getpid());
  6. }
复制代码
系统调用声明位于  中。这是我们第一个进行系统调用的程序。getpid() 的返回值是调用它的进程的 PID。由于 PID 在 printf() 的格式字符串中是整数,因此我们使用 %d 格式规范将返回值打印为固定的十进制数字。
  1. # gcc getpid_demo.c -o getpid_demo
  2. # ./getpid_demo
  3. I am the process with process-id 59592
复制代码
如果我们再次运行同一个程序,它将打印不同的 PID,这证明每次运行它时都会创建一个新的进程。
1.2.6.10 线程

控制线程是程序执行过程中逐条执行的单个指令序列。最初,所有程序都只有一个控制线程。随着计算机处理器成本越来越低,硬件供应商开始构建包含多个处理器的计算机,计算机科学家也开始探索利用这项新技术的方法。他们设计并创建了编程语言和库,允许程序包含多个控制线程,每个控制线程可以在不同的处理器上同时运行。为了简单起见,这些控制线程被称为线程。
POSIX.1-2024 将线程正式定义为进程的单个控制流,以及支持该控制流所需的系统资源。传统的 Unix 进程是单线程的,但在现代操作系统中,进程通常可以包含多个线程。当一个进程拥有多个线程时,它被称为多线程进程。多线程进程有两种类型的资源:其所有线程共享的资源,通常称为全局或共享;以及每个线程独有的资源,通常称为线程本地资源、私有资源或单线程资源。
Unix系统通常支持多线程,Linux尤其支持几种不同类型的线程。Linux以一种有趣的方式处理线程;它将所有线程视为标准进程。它没有为线程提供任何特殊的调度或数据结构。对于Linux内核来说,进程和线程都称为任务,并且在内部都由同一个数据结构task_struct 表示。在Linux中,任务是一个被分配了系统资源并可以在处理器上进行调度的实体。Linux中线程和普通进程的区别在于,线程可以共享资源,例如它们的地址空间,而进程不共享任何资源。
在许多 Unix 实现中,每个线程都有一个线程标识符 (TID),该标识符在操作系统中是唯一的,但 POSIX.1-2024 对此没有要求。它只要求在单个进程中,每个线程的 TID 都是唯一的。Linux 采用双管齐下的方法处理 TID:在单线程进程中,TID 等于进程 ID;而在多线程进程中,所有线程具有相同的 PID,但每个线程都有一个唯一的 TID。在 Linux 中,线程可以调用 gettid() 函数来获取其线程 ID。gettid_demo.c 程序演示了这一思路:
  1. #include <stdio.h>
  2. #include <unistd.h>
  3. #include <sys/types.h>
  4. int main()
  5. {
  6.     printf("I am a thread with thread-id %d\n", gettid());
  7. }
复制代码
该程序使用 C 预处理器 #define 指令来定义符号 _GNU_SOURCE。除非定义了此符号,否则编译器将无法识别头文件中程序调用 gettid() 所需的各种声明。这是一个功能测试宏的示例。#define 指令必须出现在所有 include 指令之前。我们可以按照以下示例会话所示编译并运行该程序:
  1. # gcc gettid_demo.c -o gettid_demo
  2. # ./gettid_demo
  3. I am a thread with thread-id 59608
复制代码
如果我们再次运行该程序,它每次也会显示不同的 TID,原因与之前相同:一个新的进程运行,当它只有一个线程时,它的 TID 与它的 PID 相同。
1.2.6.11 在线文档

Unix 系统提供了几种不同类型的在线文档。在此上下文中,“在线”指的是你正在使用的电脑,而不是万维网。

  • 手册页
1971年,在UNIX第一版发布后不久,Dennis Ritchie和
Ken Thompson在Joseph Ossanna和Robert Morris的帮助下编写了第一本UNIX程序员手册,该手册至今仍可在线获取 (https://
www.nokia.com/bell-labs/about/dennis-m-ritchie/1stEdman.html)。这本手册初只有一卷,但很快就发展成为一套七卷本,按主题组织。
随着时间的推移,它的规模不断扩大。现在每个 Unix 发行版都附带这套手册页,简称 man 页面。截至撰写本文时,典型的 Unix 系统中的手册页通常包含八个编号的章节,如表 1-1 所示。某些 Unix 系统还会添加其他章节。
11.png

手册页是 Unix 文档的重要组成部分。当你想了解 Unix 系统的任何部分时,它们可以作为在线参考,例如某个命令、某个库中的函数、系统调用、设备接口、系统文件、各种文件格式等等。
虽然文档非常详尽,但通常并非教程性质。有时,它可能会让人不知所措,但许多页面都包含可以编译、修改和运行的代码示例。
在我教授 Unix 系统编程的这些年里,学生们有时会说,他们不需要学习如何使用手册页,因为所有信息都在网络上,他们只需谷歌一下即可。诚然,您可以在许多网站上找到手册页的副本,并在讨论区阅读帖子,但阅读您自己安装的 Unix 系统上的手册页的原因远不止于此:

  • 您系统上的手册页版本是在安装其所记录的软件时安装的,并且每当您更新软件本身以及软件有更新需要应用时,手册页都会更新。
  • 手册页由编写和维护软件的人员编写,值得信赖且准确。
  • 您系统上的手册页是独立的,因为其中的任何交叉引用也都存储在您的系统中。
  • 即使您的互联网连接不可用,您也可以阅读它们。
要查看给定主题的手册页,请输入 man,后跟您感兴趣的主题,即命令名称、函数名称等等。例如,输入 man man 即可阅读 man 命令本身的手册页:
12.png

输出仅包含该页面的前几行。第一行显示,man 命令位于手册页的第 1 部分,因为标题包含 MAN(1)。文本 Manual pager utils 并非第 1 部分的名称;我们将其称为手册页标题或页眉(含义清晰时)。第 1 部分中的不同手册页可能具有不同的页眉。NAME 之后是命令的名称,其后是该命令功能的简要描述。这是您应该阅读的第一个手册页,我们稍后会再次讨论它。
所有符合 POSIX 标准的 Unix 系统都必须包含内核 API 函数可能包含的所有头文件的手册页。更准确地说,POSIX.1-2024 系统接口卷中的每个函数都指定了应用程序必须包含的头文件才能使用该函数,并且符合 POSIX 标准的系统必须为每个头文件提供相应的手册页。这些头文件可能未安装在您使用的系统上,但它们是可用的。只有系统管理员安装了应用程序开发文件后,才会安装它们。
scanf() 函数的手册页以以下几行开头:
13.png

它告诉我们需要头文件 stdio.h 才能使用 scanf()。我们可以输入man stdio.h 来查看该头文件,输出如下内容:
14.png

请注意,本手册页位于编号为 7posix 的章节中。在您的系统上,该页面可能位于其他章节,例如第 0 节。
使用手册页的一个挑战是,您需要知道您感兴趣的命令或函数的名称,这样它们才能对您有所帮助。手册页确实有一个相对简单的搜索机制,但它们实际上是为那些已经知道需要查找什么的人提供的参考手册,所以如果你知道你想要什么如果想做,但不知道命令名称,那么难点在于如何找到它。
手册页在帮助您自行解决问题方面发挥着关键作用。我教授如何编写系统程序的方法是基于使用手册页来指导学习过程。本书中,手册页与学习系统编程密不可分,因此我在第34页单独设立了一个章节“使用手册页”,更深入地解释了手册页的结构以及如何使用它们,包括用于指定选项和参数的语法。

  • Info 文档系统
由于手册页存在一些缺陷,GNU 项目开发了一个名为 Info 的替代文档系统,该系统基于 Texinfo 文档系统。Texinfo(发音为“Tekinfo”)是一个使用单个源文件生成在线和打印输出的文档系统。它基于 Richard M. Stallman 于 1975 年和 1976 年为 Emacs 文本编辑器创建的帮助系统 (https://www.gnu.org/software/texinfo/manual/texinfo/html_node/History.html)。它的各种命令和实用程序的信息页有时比其手册页包含的信息多得多。在某些情况下,某个命令的手册页会引导读者查看信息页。要阅读信息页,请输入 info 命令(小写)。例如,要了解 ls 命令,请输入 info ls:
15.png

当信息系统中没有某个主题的页面时,信息阅读器会打开该主题的手册页。
信息页面使用类似于 Emacs 中的导航方法,但人们通常觉得这种方法难以使用。有一种读取信息文档的方法,并通过将其输出通过管道传输到寻呼机程序(例如 more 或 less)中来绕过其中的导航,如下所示:
16.png

显示相同的信息,但还会提及包含该信息的文件:coreutils.info。

  • 应用程序提供的文档
    有时,您还可以在 /usr/share/doc 中的某个目录中找到有关特定应用程序或程序的信息。许多应用程序和更高级别的程序安装程序将其文档放在那里。这些文档有时包含大量的使用示例、开发说明以及有关在何处查找更多信息的提示。
    某些命令可以显示其自身的帮助,通常通过提供诸如 --help 之类的选项来显示:
17.png

其余输出主要描述 ls 的各种选项及其参数。

  • Shell 帮助
    某些 Shell 具有内置命令的帮助功能。特别是,bash 有一个 help 命令,当不带参数输入时,它会打印一个包含所有 bash 内置命令的两列列表,其中包含选项和参数:
18.png

当给定特定 bash 内建命令的名称时,它会打印该命令的简短使用方法:
$
19.png

help 命令使用与man相同的语法。

  • 其他文档来源
    您可以从编写和维护代码的组织下载许多手册。其中最重要的手册是《GNU C 库参考手册》,可在 https://www.gnu.org/software/libc/manual/ 获取。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册