一、概述
本文将会介绍 GNU/Linux 操作系统的组成部分。
本节内容不涉及到 GNU/Linux 的具体安装,只涉及到概况,这能帮助我们剥去 GNU/Linux 的神秘面纱,使我们对其有一个更好的了解。
GNU/Linux 是一个类 Unix 操作系统,它是由多个应用程序、系统库、开发工具等构成的程序集合。因此同 Android、Windows 相比,它是十分松散的,各个组件之间相互独立解耦。它无法提供 Windows 那样子的一揽子服务。所以,必须注意 GNU/Linux 的技术细节,这样我们才能够按照其使用方式使用它。
1.1 linux内核
GNU 操作系统是一系列运行在用户空间的程序的集合。而内核空间中运行的是 Linux。
内核是硬件与软件之间的一个中间层。作用是应用层序的请求传递给硬件,并充当底层驱动程序,对系统中的各种设备和组件进行寻址。
众所周知,程序的运行是在 CPU 中进行的。程序的实质,是一串二进制数据。而这些数据同人类的语言一样,不同的排列方式具有不同的意思,它可以指示 CPU 进行特定的操作。CPU 的核心访问它的寄存器,访问的速度决定 CPU 的频率。比如说对于一块 3GHz 的 CPU,其访问寄存器的速度是每秒三十亿次。访问寄存器,即可对寄存器内的数据做加、减运算,数据便得到处理了。二进制数据进入 CPU,经过了多个环节。首先,它可能来自某块磁盘,随后通过CPU自身的请求,进入了内存,再进入 CPU 的高速缓存,最终进入 CPU 的寄存器。这些存储器的擦写速度依次增加,但是容量也依次减小,这主要是由不同存储器时钟频率不一致的特征决定的。越快的存储设备数据保留时间越短,价格也越贵。其中,数据在内存之后的阶段,由CPU进行抽象。
内存就像一个大的餐桌,汇聚了五湖四海的数据。这些数据可能是程序,也可能是一些其它的东西。那么该怎么区分它们,怎么访问并处理它们呢?直接访问内存的物理地址肯定不可以,因为内存的大小不一,跑了大量程序后,这个“餐桌”也会变得非常杂乱。这时候,便要轮到我们的内核登场了。
我们可以尝试编译并运行以下程序:
#include <stdio.h>
#include <stdlib.h>
int a = 1, b = 255;
int main(){
int *pa = &a;
printf("pa = %#X, &b = %#X\n", pa, &b);
system("pause");
return 0;
}
这里的“*pa = &a”是“指针”的形式,指将 a 对应的内存地址以正整数的形式(int 是正整数格式)赋予指针 pa。&运算符的作用也是取变量的内存地址。后面的“%
#X”指将该数据转换为十六进制的形式。 printf 负责将其输出。
此代码将“a“(1)、”b“(255)两个正整数变量所在的内存地址以十六进制输出到终端上。
代码中的 a、b 是全局变量,它们的内存地址在链接时就已经决定了,以后再也不能改变,该程序无论在何时运行,结果都是一样的。
那么问题来了,如果物理内存中的这两个地址被其他程序占用了怎么办,我们的程序岂不是无法运行了?
幸运的是,这些内存地址都是假的,不是真实的物理内存地址,而是虚拟地址。虚拟地址通过CPU的转换才能对应到物理地址,而且每次程序运行时,操作系统都会重新安排虚拟地址和物理地址的对应关系,哪一段物理内存空闲就使用哪一段。
为内存提供这样的虚拟地址到物理地址的硬件抽象,便是内核最重要的作用。内核不仅抽象出相关的地址,还把不同的程序隔离在不同的抽象地址空间里面,这就是一个个进程。这样,对于一个应用程序,它与硬件是不会直接发生关系的,而只与内核有联系,内核是应用程序知道的层次中的最底层。在实际工作中内核抽象了相关细节。
内核还在一个进程的地址空间内,分配函数、变量和字符串。
除此之外,内核还是一个资源管理程序。负责将可用的共享资源(CPU 时间、磁盘空间、网络连接等)分配得到各个系统进程。它还提供了一组面向系统的命令。系统调用对于应用程序来说,就像调用普通函数一样。
内核为自己分配出一块受保护的内存虚拟空间,称为”内核空间“。剩下的就是归用户级别程序使用的“用户空间”。
GNU/Linux 操作系统的内核是 Linux。Linux 是一个宏内核。”宏内核“意味着它把很多内存、资源管理之外的组件放在了内核空间里面。比如说,GNU/Linux 的命令行界面(tty)就运行在内核空间中。很多驱动也运行在内核空间中。
1.2 UEFI 硬件抽象和操作系统引导加载器 GNU GRUB
PC 同 Android 手机那样的嵌入式系统不同。PC 提供了全局的硬件抽象――BIOS或UEFI。
GNU/Linux的启动可以分为四个阶段:固件引导、中间引导、内核启动,以及启动init。
在计算机启动后、内核接管计算机硬件的管理前,计算机由一个程序管理,这个程序就是UEFI或BiOS。它通常通过闪存刻录在计算机主板上的某一个芯片中,是计算机最先加载的程序。它负责硬件的识别、自检及初始化。
常见的 X86-64 UEFI 普遍是私有软件。目前市面上只有一些老的、使用传统 BIOS 的设备可以刷自由的固件。如果你有十分重要的保密需求,你可以使用这些老的设备。CPU 先在 16 位实模式启动(即能访问到16位的真实地址空间),经由 UEFI 的操控,启动到 32 位保护模式(访问由 UEFI 抽象的 32 位虚拟内存空间),在这个模式下完成它的任务,之后启动 64 位保护模式,并拉起 GRUB。
GRUB 是 GNU 开发的引导加载程序。GRUB 负责加载内核镜像 vmlinuxz 以及 initramfs 到内存。vmlinuxz 是压缩过的系统内核文件。至此,系统控制权接管到内核。而 initramfs 是一个特殊的临时根文件系统(“/”)。加载临时根文件系统的目的,是为挂载正式的根文件系统,做物质上的准备。例如,通过挂载临时根文件系统,便可以访问磁盘驱动、引导加密磁盘(这里后面的安装教程会讲)等。此外,如果根文件系统加载前需要加载某些内核模块(即某些需要加载到内核空间的程序)时也需要加载它。一句话,如果磁盘不能直接被访问,那么 initramfs 就是必要的。
如果有 initramfs,Linux内核会加载这里面的 init 程序。如果没有,内核会直接尝试加载根文件系统,并加载 init 程序。
在常见的 GNU/Linux 发行版 Debian GNU/Linux 12 和 Arch GNU/Linux 中,GRUB2 的配置文件位于 /boot/grub2/grub.cfg,你可以自己调整启动的参数,但是笔者并不建议你调整。
1.3 init 和 SystemD
init 进程是第一个由内核启动的用户级进程。内核自行启动(已经被载入内存,开始运行,并已初始化所有的设备驱动程序和数据结构等)之后,就通过启动一个用户级程序 init 的方式,完成引导进程。init 进程和 init 程序(linuxrc 程序,也就是我们前面提到的那个)是有区别的。init 进程一开始就有,它运行于内核态,属于一个内核线程。后来 init 进程挂载根文件系统,并运行应用程序 init 程序后, init 进程才从内核态转变为用户态。init始终是第一个进程,其进程编号始终为1。
在GNU/Linux操作系统中,其他所有的用户进程都直接或者间接派生自 init 进程。
内核会在过去曾使用过init程序的几个地方查找它,它的正确位置(对 GNU/Linux 系统来说)是 /sbin/init。如果内核找不到 init,它就会试着运行 /bin/sh,如果运行失败,系统的启动也会失败。
传统的init进程有如下运行级:
0 - 停机
1 - 单用户模式
2 - 多用户,没有 NFS
3 - 完全多用户模式(标准的运行级)
4 - 没有用到
5 - X11 (xwindow)
6 - 重新启动
这套级别系统在 Systemd 替代传统 init 的当下仍旧被使用着。以超级用户(root)权限在命令行窗口输入 init [运行级序号] 即可切换运行级别。相应的,你的系统状态会改变。
除了固件和 GRUB 的启动,Android 操作系统的启动实质上也遵循上述流程。但是,Android 操作系统的 init 进程用于启动 Zygote 进程。Zygote 进程是真正的直接父进程,它加载 Java 虚拟机和其他的一些类库。著名的 Zygisk 就是通过 root 用户将程序注入到 Zygote 进程的地址空间中实行软件修改的。如果你在 Android 运行有 root 权限的终端,键入 init 0 命令,系统并不会有反应,这是因为 Android 的 init 进程和 GNU 的 init 进程根本不同。正因为 Android 要加载的东西比 GNU/Linux 多和复杂,所以 Android 才会更卡。
现代 GNU/Linux 操作系统使用的 init 程序主要是 Systemd。它是操作系统的一套守护进程。在历史上的 GNU/Linux 操作系统中,init 进程只是执行启动脚本,这使得启动过程极端漫长,不稳定因素很多。Systemd 是一套二进制可执行程序,它解决了此问题。
使用了 Systemd,就不需要再用 init 了。Systemd 取代了 initd,成为系统的第一个进程(PID 等于 1),其他进程都是它的子进程。
您可在终端中键入 systemctl --version 命令检查systemd版本。
1.5 运行库
GlibC(GNU LibC)是一个 C/C++ 标准库。它提供了很多头文件,你写程序引用它们,就可以在采用 GlibC的GNU/Linux 操作系统(Alpine Linux 一类系统采用 Musl 做标准库)上运行该程序。
GlibC 提供了一系列的二进制工具。GlibC 的工作原理主要基于动态链接。 当程序在运行时,操作系统会将 Glibc 中的函数库动态加载到进程的地址空间中,程序可以直接调用这些函数库中的函数。 这种方式允许程序在运行时根据需要加载不同的函数库版本,从而提供了更大的灵活性和可定制性。
除了 GlibC 库外,GNU/Linux 采用了诸多的库。这些库和GlibC相仿,提供了程序运行的必要组件。GNU/Linux的库以软件包形式提供,包括 Python 和 Java 的库。这保证了 GNU/Linux 的可拓展性。
1.6 Shell
在 GNU/Linux 操作系统中,字符界面是老大。这同历史上的 Windows 95 等基于 Dos 内核的操作系统十分相似。GNU/Linux 操作系统启动后,会率先进入命令行界面。命令行界面是 Linux 内核自带的功能。在加载显卡之后就可用。由于 Unix 被设计为一个多用户操作系统,所以人们会在计算机上连接多个终端(在当时,这些终端全都是电传打字机)。Unix 系统为了支持这些电传打字机,就设计了名为 tty 的子系统,将具体的硬件设备抽象为操作系统内部位于 /dev/tty* 的设备文件。在键盘按下ctrl+alt+F* 即可
在内核之上,有一个 Shell 程序。像它的名字一样,它提供了一个内核的外壳。它接受用户输入的命令,然后帮我们与内核沟通,最后让内核完成我们的任务。Shell 程序既可以在字符终端 TTY 中使用,也可以在图形界面 GUI 中使用。对于现代的图形接口,终端模拟器会“假装”成一个 GUI 程序。一个终端模拟器的标准工作流程是这样的:
捕获你的键盘输入;
将输入发送给命令行程序(程序会认为这是从一个真正的终端设备输入的);
拿到命令行程序的输出结果(STDOUT 以及 STDERR);
调用图形接口(比如 X11),将输出结果渲染至显示器。
这个提供用户界面的程序被叫做 Shell (壳层)。常见的 Shell 有 Bash 和 Zsh。后面按照 GNU/Linux,我们会学习如何安装 Zsh,并在图形界面下美化你的终端。
1.7 权限管理
尽管不如 Windows ,但是GNU/Linux的权限管理也十分复杂。它主要是通过自主访问控制、强制访问控制等多种安全模型实现的,是网络安全斗争成果的结晶。在这里,我们只讨论用户权限,不讨论其原理。
GNU/Linux有两种用户:即超级用户(root)和普通用户。超级用户可以访问任意文件。即便该文件的权限不允许访问,超级用户也可以任意修改其权限本身。普通用户访问和执行特定文件必须要提升到root权限。sudo可以帮助用户短暂的提升。而su命令可以切换终端的持有者,使其易手到root账户上。此时你可以藉由这个提权的终端实行root权限。
超级用户的命令提示符是"#”,普通用户的是“$“(或“>“)
在GNU/Linux下,文件也有自己的权限。具体见下图。
![](https://i.postimg.cc/26pH04Nc/Screenshot-20240726-235223-Fennec-2.png)
1.8 X和Wayland图形接口
X11是X协议,版本号为11。X协议是专门被设计为类Unix的桌面管理服务的,GNU/Linux桌面环境不像windows那样作为系统内核的一部分,而是作为一个普通程序运行在用户态上。该协议的设计初衷是为了GNU/linux的图形界面满足跨平台、跨网络、与具体硬件剥离、同时被多个用户使用的需求,因此该协议被设计成客户端-服务器的模式,即由xserver和xclient组成,xserver和xclient通过网络架构来进行图形接口的通信和执行绘制。
- X Server(X 服务器)管理主机上与显示相关的硬件设置(如显卡、硬盘、鼠标等),它负责屏幕画面的绘制与显示,以及将输入设置(如键盘、鼠标)的动作告知 X client。
- X Client(X 客户端)即主机,负责处理逻辑。
比如如果用户点击了鼠标左键,X Server 捕捉到鼠标点击这个动作,然后它将这个动作告诉 X Client,然后X Client 就根据程序预先设定的逻辑(例如画一个圆),告诉 X Server说:“请在屏幕XX位置(鼠标左键点击的位置)画一个圆”。最后,X Server 就响应 X Client 的请求,在鼠标点击的位置,绘制并显示出一个圆。
随着时代的发展,X11服务端、客户端分离的结构严重地影响了性能,人们开发出了Wayland。Wayland将客户端和服务端合二为一,解决了X11的问题。此外,Wayland 提供对X程序的兼容。
是否进入用户界面是由 init 进程决定的。只需把 init 默认工作态调为5,即可自动开启用户界面。
人们基于X协议开发了 qt 与 gtk+ 两个图形库。在它们之上,开发者分别开发出了kde与gnome桌面;kde和gnome是集成了窗口管理器及一些应用程序的套件;其他的 X 应用程序则跑在这些窗口管理器里。
1.7目录树结构和UNIX文件观
登录系统后,在当前命令窗口下输入命令:
ls /
你会看到如下图所示:
![](https://i.postimg.cc/KYCWC6B5/4-20.png)
在目录的根节点(/)下面有众多的目录,这就是类Unix系统著名的树状文件结构。类Unix操作系统奉行“一切皆文件”的原则,系统的一切,包括磁盘、打印机等设备都以文件的形式抽象于根节点(/)下面。
![](https://i.postimg.cc/x14pbwxS/d0c50-linux2bfile2bsystem2bhierarchy.jpg)
尽管GNU/Linux操作系统具有丰富的可定制性,但是其根植于GNU的土壤,是GNU为它的一切创造了一个体系,在目录树上也一样。GNU/Linux遵循FHS标准。FHS帮助发行版和程序的设计者和开发人员来规划他们的工具的各个组件应该存放的位置。
通过将各个程序的所有文件、二进制文件和帮助手册保存在一致的组织结构中,FHS 让对它们的学习、调试或修改更加容易。
一般的GNU/Linux目录如下。
/bin 该目录是存放所有核心系统二进制文件的地方,其包含的命令可以在 shell (解释终端指令的程序)中使用。没有这个目录的内容,你的系统就基本没法使用。
/boot 该目录存储了您的计算机启动所需的所有东西。其中最重要的是引导程序和内核。引导程序是一个通过初始化一些基础工具,使引导过程得以继续的程序。在初始化结束时,引导程序会加载内核,内核允许计算机与所有其它硬件和固件进行接口。从这一点看,它可以使整个操作系统工作起来。
/dev 该目录用于存储类似文件的对象来表示被系统识别为“设备”的各种东西。这里包括许多显式的设备,如计算机的硬件组件:键盘、屏幕、硬盘驱动器等。/dev 还包含被系统视为“设备”的数据流的伪文件。一个例子是流入和流出您的终端的数据,可以分为三个“流”。它读取的信息被称为“标准输入”。命令或进程的输出是“标准输出”。最后,被分类为调试信息的辅助性输出指向到“标准错误”。终端本身作为文件也可以在这里找到(就是前面提到的tty*)。
/etc 许多程序在这里存储它们的配置文件,用于改变它们的设置。一些程序存储这里的是默认配置的副本,这些副本将在修改之前复制到另一个位置。其它的程序在这里存储配置的唯一副本,并期望用户可以直接修改。为 root 用户保留的许多程序常用一种配置模式。
/home 该目录是用户个人文件所在的位置。对于桌面用户来说,这是您花费大部分时间的地方。对于每个非特权用户,这里都有一个具有相应名称的目录(/home/username)。
/lib 该目录是您的系统赖以运行的许多库的所在地。许多程序都会重复使用一个或多个功能或子程序。
/media 该目录中可以访问像 USB 闪存驱动器或摄像机这样的可移动媒体。虽然它并不是所有系统上都有,但在一些专注于直观的桌面系统中还是比较普遍的,如 Debian(Manjaro 上似乎没有)。具有存储能力的媒体在此处被“挂载”,这意味着当设备中的原始位流位于 /dev 目录下时,用户通常可以在这里访问那些可交互的文件对象。
/proc 该目录是一个动态显示系统数据的虚拟文件系统。这意味着系统可以即时地创建 /proc 的内容,用包含运行时生成的系统信息(如硬件统计信息)的文件进行填充。
/tmp 该目录用于放置缓存数据等临时信息。不做其他更多的事情。
1.8 磁盘
GNU/Linux的磁盘是可以分区的,有EXT3、EXT4、Brtfs等多种文件系统可供选择,它们的分区表和读写方式有不同。这里笔者推荐EXT4。
磁盘的分区体现为 /dev/sd[a-z](对于 SATA/SCSI/NVMe 磁盘)或 /dev/hd[a-z](对于 IDE 磁盘)。除非使用分区工具(如 KDE 分区管理器),否则你看不到分区的具体名称。你通常可以在root权限下使用 mount /dev/sd[](你的分区编号)来挂在该分区,这样你就可以在文件管理器中打开该磁盘的文件。如果将事情处理完毕,可使用 unmount /dev/sd[](你的分区编号)来解除挂载,此时它便会从你的设备上取消加载。挂载后的新磁盘文件夹一般在 /run 或 /media 中。
1.9 包管理器和软件包
在 GNU/Linux,操作系统和实用软件是没有一个明确界限的。你的桌面环境、Shell、办公套件LibreOffice,甚至系统守护进程 Systemd 和内核 Linux 等等都具有同等的地位。此外,系统的库也是以软件包的形式提供的。库、内核、Systemd 之外的软件普遍安装在 “/usr/share” 和 “/usr/local” 这两个路径下。
不同的包管理器提供对不同软件包的支持。软件包一般通过包管理器,从软件源,即储存有大量软件包的服务器上现装现下。这些软件源普遍位于中国境外,但你可以从中国境内的镜像站下载这些软件。
基于不同的包管理器衍生出了不同的 GNU/Linux 发行版。Debian 及其衍生发行版以 apt/dpkg 为包管理器,而 Arch Linux 以 pacman 为包管理器。您可以在终端上运行这些包管理器。这些包大多依赖于系统中的其它资源。它们也会在系统生成各种配置文件。请十分注意!不当地删改软件可能导致你的系统崩溃和数据丢失!
Flatpak 是一类特殊的软件包,它已经包含了该软件所需的一切依赖。Flatpak 可以在任何 GNU/Linux 发行版上安装,但代价是安装它们耗费大量的空间。