认识x86_64与Linux哲学
使用 Linux 像 MacOS / Windows 一样无脑可能会出大问题,为了更好地使用和理解 Linux,可能需要了解硬件和 Linux 的部分核心哲学
什么粉丝向操作系统由于这是一篇相对新人向的文章,读此文章前请至少了解操作系统的概念,基本知道一些硬件是做什么的
本文可能写嗨了穿插了过多的硬件科普,仅了解概念即可,不必过度关注
内核态和用户态
内核是操作系统的根本,它承担了调度程序实例、联结各种软硬件的作用。利用了现代 CPU 的特权级,内核通常拥有较高的权限(x86 上为最高特权级 ring0),并在较低的特权级上运行非内核的程序,这些运行实例称为用户进程。
内核不是与用户进程完全分离的,利用硬件的中断(interrupt)、异常(fault / exception)、系统调用机制,内核和用户程序在相同的 CPU 核心上交替执行。设计内核时,通常会预留一些 trap,它们可能是系统调用,也可能是可控的异常,使得用户程序可以陷入内核,由内核处理特定逻辑并返回,由此为用户进程提供诸如硬件交互、进程通信等软件支持,同时控制用户进程的行为。
这种执行进程主要逻辑、权限较低的状态称为用户态,陷入内核执行内核代码的状态称为内核态。
存储介质 分区表 文件系统
— 一个盆装满了沙子,它满了吗?— 可以把沙子炼成硅存满数据,,
存储介质应该都不陌生,磁带、光盘、软盘、机械硬盘、U盘、sdcard、固态硬盘…它们像一堆本子(正经记东西的),只提供了记录信息的一种物理媒介,而如何定义一个目录来分节,如何记录章节数据,则对应了分区表和文件系统。
关于如何在本子上记东西(由于可擦写,你用了铅笔),我们可以试想以下两种方式:
- 通过擦写保证每个文件是连续的,找文件就读一遍(或者加个分目录)
- 预留一些页面,在其他页面上将相邻的几页放在一起(页数固定),称为簇,作为最小单位。插空记录数据,然后再预留的页面上记录一个文件由哪几页组成,用来寻址
- 将相邻的几页放在一起(页数固定)称为块,作为最小单位,固定数量的连续的块组成块组,块组中可能有一部分用来记录全局配置,一定有一部分用来记录该块组的整体信息和数据分布,可能有一部分记录所有块组整体信息和数据分布,一定有记录文件目录包括寻址在内的元数据的inode表和记录块组占用情况的结构,以及记录数据的块
以上三种组织数据的方式分别模拟了:一种非常暴力性能非常低下几乎没有工程价值的文件系统、fat32、ext2。这种定义了如何存文件的“系统”称为文件系统,虽然ext2可能已经没有人在使用了,但是其上的ext3和ext4的原理更加复杂,与本篇几乎无关且占用篇幅太多(稍微没那么复杂的ext3也几乎没人用了)。
即使不是很了解电脑,也可能听说过类似“固态不用分区”的说法,这种行为其实是只分一个分区并挂载,如果是严格意义上的不分区,电脑此时更大概率是一块砖,因为 x86_64 的启动过程本身依赖分区表,因此如果连分区表都没有,即使硬盘里有程序,固件也不知道去哪里加载。x86_64有 MBR(Master Boot Record,主引导记录)和 GPT(GUID Partition Table,全局唯一标识分区表),GPT 是比 MBR 更先进的标准,它支持备份和更多的分区(MBR 最多支持四个主分区简直不够看)。分区表承担了划分存储介质上从哪里到哪里到连续部分属于一个分区的任务,有了分区就可以将分区格式化为某个的文件系统用来存数据了,另外,分区表在电脑启动时的作用将在下面细说。
不是所有存储介质都需要分区表,也不是不存在分区表电脑一定无法从存储介质启动。光盘、软盘、直接从头写入文件系统的U盘、闪存卡等没有分区表,但他们有文件系统,可以读写其中的数据;光盘和软盘虽然没有分区表,但他们可以承载引导程序并用于启动(软盘现在大概也快不行了)。对于大多数使用来说,操作系统装在硬盘上,安装系统时通常用U盘作为启动盘,这些情况下通常是有且需要分区表的。
目录结构
本文中统一采用类 UNIX 的目录表示方式,使用符号 / 分隔上下级,使用 . 表示当前目录,.. 表示上级目录,位于开头的 / 表示根目录,根目录是整个文件系统的顶级,例如:
./a表示当前目录下的文件a,这是一个相对路径Documents/a表示当前目录下子目录Documents中的文件a,前面的./可以省略,这是一个相对路径/bin/bash表示根目录下的目录bin下的文件bash,开头的/表示根目录,第二个/分隔上下级,这是一个绝对路径
此处的文件系统不是存储介质上的文件系统,而是对所有受到内核管理的目录和文件一个抽象概念,它是以根目录为树根的一个树形结构,后面也需要区分这两个概念。
Linux 的文件系统组织形式不同于 Windows,它没有什么“某盘”的概念,所有要访问的分区或没有分区的存储介质都是挂载到某个目录上,挂载后,该目录的子目录和文件都将替换为对应位置下的目录和文件
地址和内存
CPU 通过物理地址访问内存和外设,但在操作系统和软件层面,现在多使用虚拟地址(也称线性地址)来处理运行逻辑。x86_64 CPU 通过 MMU 来处理虚拟地址到物理地址的映射,它的输入为 48 位或 57 位的虚拟地址,输出为物理地址,映射关系被储存在内存里一种叫页表的树形结构中。由于页表是可以被操作系统内核修改或切换的,不同进程相同的虚拟地址可以不被映射到同一个物理地址,也并非所有的虚拟地址都被映射到有效的或目前有权访问的物理地址。
Linux 是高半核操作系统,即通过虚拟地址最高有效位是 0 还是 1 将虚拟地址空间分成两半,内核地址空间占最高有效位为1的一半。
虚拟地址可以通过页表设置分区权限,Linux 内核会严格控制这些权限,不能在用户态访问内核特权区域,读写执行操作也要和符合权限,否则会发生段错误,严重会导致出错的程序崩溃。
XX, 启动 !!
x86_64有两种启动引导模式:Legacy BIOS 和 UEFI。Legacy 模式支持 MBR 分区表,通过执行分区表中的启动代码进入 16 位的实模式(可能有人使用 Legacy + GPT 的组合,但依然是通过在 GPT 中的兼容性 MBR 头部中插入启动代码实现);UEFI 模式支持 MBR 和 GPT 分区表,但都是通过执行特定标识的 fat32 / fat16 文件系统的 EFI 系统分区(ESP)中的可引导文件来引导启动,而不会执行 MBR 启动代码,进入的是 64 位的长模式(现在更常见)或 32 位的保护模式,且通常开启了分页并初始化了平坦的全局描述符表(GDT)和用于引导过程的中断描述符表(IDT)。
加载内核
在 Legacy BIOS 时代,Linux 内核 vmlinuz 文件在启动早期是个非常庞大的东西,它需要加载了最基本文件系统驱动的额外的引导程序(boot loader)将它从文件系统搬运到内存中并跳转到内核代码开始执行,到现在依然常见且在 UEFI 模式下依然可用的 boot loader 包括 grub2、syslinux 等。
相比 Legacy 模式,UEFI 固件中的程序承担了启动早期的大部分脏活累活,能识别 ESP 分区 fat32 / fat16 格式的文件系统,因此内核可以位于 ESP 分区,直接由 UEFI 加载到内存中。现在的 Linux 内核通常编译了 EFI stub,即内核本身就是一个合法的 .efi 可引导文件,放进 ESP 并配置引导条目或作为默认启动项 EFI/BOOT/BOOTX64.EFI (或 32 位的 EFI/BOOT/BOOTIA32.EFI,ESP fat 文件系统文件和目录名称不区分大小写) 就可以直接由 UEFI 加载并运行。
initramfs
initramfs是一个文件,全称为 initial RAM filesystem,初始内存文件系统,它解压到内存中(通常压缩,也可以不压缩)作为 Linux 内核的根文件系统(rootfs),这是一个临时的文件系统,为启动提供了必要的目录结构、将要运行的第一个程序(通常是其中的 /init,它将以 1 号进程运行)、必要的其他文件(包括程序、脚本和驱动内核模块等)。
initramfs 看起来可能有些多此一举,内核似乎可以自己捏一个初始文件系统,然后挂载分区,从里面找到要运行的程序和要使用的驱动。但是,需要挂载的分区可能是各种各样的文件系统,要访问的存储介质也可能是不同类型的硬件,加密的文件系统需要解密,它们的驱动通常以内核模块的形式存在,如果要让内核自己去找这些模块,就会陷入需要去找模块而找模块需要先能用这些模块的矛盾之中;或者,就需要把所有有关驱动全部编译到内核中而不是以模块的形式存在,但这样内核就会变得更加庞大,而且代码会混入很多根本不需要又难以动态卸载的东西,有些树外驱动(未进入内核源码树的驱动)也很难以内置(builtin)的方式编译。
安全启动
安全启动是 UEFI 引入的一项功能,可以通过验证所加载文件的数字签名防止电脑运行不受信任的可引导程序,避免被劫持启动早期的恶意程序破坏或窃取数据。UEFI 固件中的程序承担了启动早期的很多杂活,加载分区中的具体文件而非某个分区表结构,这使得启动过程可以避免像 Legacy BIOS 一样破碎,从而有条件形成完整的信任链。
为了缩短执行链条和减少攻击面,有人提倡将内核、内核命令行和 initramfs 打包在一起制作成 efi 可引导文件,这被称为 UKI(unified kernel image)。
Linux
Linux 只是一个内核,完整的操作系统通常作为 GNU/Linux 发行版呈现,它使用 Linux 内核和来自 GNU 的包括初始化程序、shell、编译工具链等核心工具。
一切皆文件
这大概是 Linux 这种 unix-like 系统的最出圈的特性。Linux 将一切抽象对象作为文件来管理,Linux 中的文件类型有以下几种:
| 文件类型 | 字母缩写 | 用途 |
|---|---|---|
| 普通文件 | - (- 并非没有) | 通常意义上的文件,包括可执行文件、存储各种数据的文件、空文件等 |
| 目录 | d | 构成与上文目录章节相同的树形结构,用来组织文件 |
| 符号链接 | l | 指向其他文件(广义的)的链接,可通过此访问目标,目标可能不存在) |
| 管道 | p | 常用于进程间通信的单向数据传输设施,有唯一的写端和唯一的读端,遵循先入先出规则 |
| 字符设备 | c | 映射物理或虚拟的设备,一般以串行方式传输字节流,例如键鼠驱动会创建字符设备 |
| 块设备 | b | 映射物理或虚拟的设备,提供随机存取功能,例如硬盘驱动会创建块设备 |
| socket 文件(套接字文件) | s | 通常用于进程间通信,可以被多个进程绑定,与网络 socket 不同,socket 文件提供的通信经过内存而非物理网络 |
对于初识 Linux 来说,文件的很多概念会相对抽象,一般需要重点了解普通文件、目录、符号链接、设备(可以不用区分字符设备块设备),其余的日常使用几乎不会用到,但编程依然常用。
文件权限
Linux 中,有用户和用户组的概念,一个用户可以属于多个用户组,有一个特殊的用户 root,拥有内核之外的最高权限。用户执行程序所创建的进程的初始权限默认与执行者相同。
文件(一切皆文件所表达的抽象文件)可以属于一个用户和一个用户组,其基础权限可以表示为 所属用户-所属组内用户-其他人 三类用户分别拥有的权限,每一类用户分别可以拥有 rwx 的权限,表示读、写、执行。例如,rwxr----- 表示所属用户有完全权限,所属组内用户只读,其他人无任何权限。可以将 rwx 写成三位二进制,有该权限记作 1,无记作 0,然后将其写为八进制表示,上述例子中的权限可以记作 740。
对于普通文件,这些权限就对应了对于某个用户该文件是否可读是否可写是否可执行;而对于目录,x 权限对应用户能否进入或经过这个目录,没有目录的 x 权限内核将拒绝该用户遍历该目录下的数据,因此几乎无法直接以各种形式访问该目录下的所有文件,相比之下,目录没有 r 权限只会导致该目录不可读,如果拥有了该目录下的其他文件的完整路径,是否有 r 权限不影响操作。另外,删除文件需要拥有所在目录的 w 权限和路径上所有目录的 x 权限,而非文件本身的权限。
除了这些基本的权限之外,还有 suid、sgid、sbit 三个特殊权限。suid 仅对可执行二进制文件生效,对可执行脚本或其他文件均不生效,可以使创建的进程拥有文件所属用户的实际权限,与具体执行者无关。这使得有些需要访问权限更严格的文件的程序被不拥有该权限的用户执行时可以临时获得这些权限,例如 passwd 命令需要访问 /etc/shadow,sudo 命令需要使后面的命令以 root 或部分 root 权限运行,这类权限与系统安全性密切相关,应尽量控制其使用。sgid 权限可以对可执行二进制文件和目录生效,作用于可执行二进制文件时和 suid 相似,使进程拥有所属组的权限,作用于目录时可以使新创建的子目录继承其所属组和 sgid 权限,使新创建的非目录文件继承其所属组(不继承 sgid),而非按照创建者的默认组设置所属组。sbit 权限仅对目录有效,使其中创建的文件只能被所有者和 root 删除,即使其他人对该目录也有 w 和 x 权限,该权限无论创建的文件是否是子目录均不继承。这三个权限也可以合并为一位八进制数字,其三位二进制顺序分别是 suid sgid sbit,写在普通权限的三位八进制数字前方;表示为字符串时,suid 权限用 s 替换所属用户权限中的 x,sgid 用 s 替换所属组权限中的 x,sbit 用 t 替换其他人权限中的 x。
文件还可以拥有隐藏属性,这些隐藏属性对 root 同样生效。i 属性保护文件不被删除、修改、重命名、建立软硬链接。a 属性使文件只能追加内容,不能覆盖和删除已有内容。
文件所属用户可以修改文件的权限,常用命令 chmod;可以将文件所属组转移到自己所在的其他组但不能转移所属用户,常用命令 chown 或 chgrp。root 访问文件忽略普通权限的限制,但受 suid 和 sgid 的影响可能使实际执行或创建的权限降级;会受到隐藏属性的限制;可以随意更改文件权限,随意转移文件所属组和所属用户,可以使用 chattr 设置隐藏属性。
文件链接
分为硬链接和软链接。
硬链接与普通文件非常相似,文件类型为普通文件,创建硬链接时创建的目标与源文件引用同一份存储结构,逻辑上位于不同的路径,修改任意一份其他同步更改,且未将所有引用到这块内容的硬链接或源文件全部删除时这块空间不会释放。硬链接创建后地位与源文件相等,内容包括元数据、权限完全相同,没有主次、先后,甚至可以造成定位困难(无法分清哪些普通文件其实引用了同一份存储结构)。常用没有 -s 参数的 ln 命令创建。硬链接通常不支持跨越底层文件系统创建,仅支持链接普通文件。
软链接本身就是一类特殊的文件,指向了所链接的文件,可以跨文件系统,可以链接设备、目录、管道、socket,也可以链接其他软链接。软链接不保证所链接对象是存在的,没有独立的权限,ls -l 显示 lrwxrwxrwx 类型和权限,鉴权只会验证被链接的源文件。与硬链接不同,软链接的作用方式与底层文件系统无关,类似网址仅记录一个数据,不关心服务器是否可达,是否允许访问,是否继续跳转。
console、TTY 和 PTY
它们是 Linux 最主要的几个可交互设备。TTY 名称来自历史上的电传打字机,现在多翻译为终端或虚拟终端,用来处理键盘输入和回显(将打的字显示出来),打印输出(通常通过 ANSI 转义序列控制行为和字符外观),提供基本的文本交互。console 是一个特殊的 TTY,用来打印内核日志,启动时若没有图形界面(如 plymouth)一般会自动挂到某个 TTY 上,也可以通过内核命令行控制 console 的输出位置,比如挂到串口上。
TTY 同时也承担了作为图形外壳的显示载体的作用。程序可以通过系统调用设置某个 TTY 设备到图形模式,然后在其上拉起 X server 或 Wayland 混成器。图形模式下不通过 TTY 本身来传递数据,而是直接通过 evdev 读取键盘事件,通过 DRM 显示图形界面。
PTY 被称为伪终端,为 SSH、图形外壳下的终端模拟器(如 konsole、gnome-terminal、kitty 等)这种拿不到真实 TTY 的情景提供类似 TTY 终端的功能。每个 PTY 有 master 和 slave 两个设备节点,称为 ptm 和 pts,master 端由需要使用终端功能的程序如 sshd、终端模拟器持有,slave 端由需要在终端上运行的程序如各种 shell 持有。
shell
Linux 本身提供的系统调用是一种二进制接口(ABI),shell 则是一类可以把命令、信号或脚本翻译为内核能执行的系统调用的终端工具。
有的 shell 工具基本兼容 POSIX shell 标准,比如 bash 和 zsh,有的 shell 工具则完全不兼容,比如 fish 和 nushell。POSIX shell 标准规定了包括环境变量、函数声明、控制语句、管道、参数、命令替换、重定向等在内的具体语法,至少包含的内置命令和以文本传输的数据流形式。
信号
信号主要承担了进程间交互、内核告知进程运行状态、进程生命周期管理等作用,可以由终端中的键盘事件(如 Ctrl + c 发送 2 号信号 SIGINT 尝试终止进程,Ctrl + Z 发送 20 号 SIGTSTP 暂停进程)或 kill() 系统调用产生。
命令 kill 封装了 kill() 调用,可以在 shell 中向进程发送任意信号,默认无参数的 kill 命令发送的是 15 号 SIGTERM,用来尝试终止进程,最常用的用法是 kill -9 进程ID(PID) 来发送 9 号 SIGKILL 强制终止进程。
命令可以被进程注册捕获或忽略,唯二不可捕获和忽略的信号为 SIGKILL 和 SIGSTOP。有些信号默认会终止进程,但可以被捕获或忽略,正常情况下需要捕获这些信号的程序通常是为了更文明地终止,避免设施损坏、传输不完整数据等情况。有些信号属于异常信号,即运行出错时由内核发送给进程,如访问非法内存、读写失效管道、整数运算除以 0,这类信号不注册将导致程序终止;若注册捕获并正确处理,程序可以继续运行,若处理不正确导致程序刚在异常处恢复执行就再次产生相同异常,内核将终止程序;对于这类信号注册忽略是未定义行为,因为不会有任何代码来修复错误,异常将持续在恢复执行时产生。
环境变量
环境变量是用来存储系统状态程序配置的键值对。由于环境变量在父子进程存在继承机制,因此环境变量被上游的服务进程从配置文件读出后或根据运行状态设置后,能在下游反映系统的配置和状态。
有些环境变量是被 POSIX 标准保留的,如
-
HOME存储用户家目录路径 -
PATH为 shell 指示查找可执行文件的目录(将一个可执行文件放进 PATH 包含的目录中可以直接在 shell 中使用文件名执行,查找顺序依赖系统目录和 PATH 中目录的顺序) -
LC_TIMELANGLANGUAGELC_MESSAGELC_ALL等用来存储本地化(locale)配置 -
LD_LIBRARY_PATH为动态连接器 ld.so 指示查找动态库的目录
环境变量名(键)必须由区分大小写的字母、下划线和数字构成,不能以数字开头。被标准保留的环境变量中一定不存在小写字母。
本地化(locale)
Linux 发行版中通常有本地化程序,在 shell 中执行即可根据本地化配置编译对应的本地化数据库文件;同时存在一个本地化配置文件,通常为 /etc/locale.conf,用来持久化对应的环境变量。本地化配置包括日期格式(不是时区)、货币符号、语言、语言所使用字符集等。
支持本地化的程序可以读环境变量获取当前本地化配置,某些程序可能自行维护本地化配置或某些或全部方面不支持本地化而不响应系统配置,如基于 systemd 的发行版 journalctl 不加参数永远使用美式日期格式。
本地化可以被“置空”,即设置为 C 或 POSIX,可选 UTF-8 支持。
时区
内核本身不支持时区,只有 UNIX 时间戳用来表示 UTC 时间。大多发行版使用 /etc/localtime 软链接到时区配置文件来设置时区和夏令时偏移,记录在主板硬件中的默认为 UTC 时间(不同于 Windows 一般根据时区记录当地时间,因此双系统未经配置可能时间差一个时区偏移)。
内存交换(swap)
当系统内存不足时,内存中的部分数据可以由内存交换换出到硬盘上,使用 swapfile 或 swap 分区来容纳被换出的数据,当这部分内存页面被访问时再换入内存中。
swapfile 是硬盘文件系统中的一个文件,是一个普通文件;swap 分区是硬盘上的一个主分区。它们必须含有特定的签名头才能被识别为合法的 swapfile 或 swap 分区,通常使用 mkswap 命令来完成这个工作。
只有物理内存紧缺且内存中缺少非脏页面(初始化后没有被写入过的页面)可以回收才会发生内存交换。比如一个 32GiB 内存 8GiB 显存的普通家用机使用 ktransformers 跑一个内存根本装不下的大模型时,内存交换大概率不会发生,因为神经网络权重是只读的,它们从硬盘读入内存后不会被修改,因此内存不足时直接回收不需要的页面而非被换出。这种情况和内存交换一样,都会和正常内存访问相比明显降低读写速度,因为都涉及到了硬盘读写。
挂载文件系统
通常使用 mount 命令挂载分区,分区中的文件系统必须是内核可识别的。挂载到特定目录后该目录下的结构会变成所挂载文件系统中的结构,该目录下原先的结构不可见,但还存在于该目录本身所在的分区文件系统中。
Linux 中有两种常用方式定位要挂载的文件系统,一种是使用文件系统所在分区(若没有分区使用整个存储介质则是整个存储介质)位于 /dev 目录下的设备节点;另一种是使用文件系统本身的 uuid。设备节点文件名可能由于同类型设备不止一个发生枚举号的改变,因此对于长期应用使用文件系统 uuid 更加稳妥,uuid 会在文件系统被格式化时产生并伴随其整个使用周期。
可以编辑 /etc/fstab 实现启动自动挂载,但对于挂载到根目录的分区,通常由内核命令行中的 root 参数指定,fstab 中的根目录可能改变了根目录的挂载参数。
包管理器
每个发行版都有自己的包管理器来管理组件包,包可能是一个完整的应用程序、一组依赖库、内核、一个树外驱动、一组文档等。
相对进阶的
伪文件系统
它们存在于内存中而非硬盘上,例如挂载于 /proc 的 procfs,挂载于 /sys 的 sysfs,挂载于 /run /dev/shm 和部分发行版的 /tmp 的 tmpfs,挂载于 /dev 的 devtmpfs。
tmpfs 主要用来存储各种运行时文件、内存对象等,它们的生命周期仅限到下次关机之前,不必要或不适合在硬盘中存储。所有伪文件系统中只有 tmpfs 可以被换出到硬盘上。
devtmpfs 由驱动在其中生成对应硬件的设备文件,作为硬件交互接口
procfs 用来抽象进程和线程,提供系统与硬件信息和运行状态。/proc/PID 目录表示对应 PID 的进程。
configfs 提供用来管理内核对象的文件系统抽象。
sysfs 提供内核功能接口抽象,接口文件为普通文件(虽然是普通文件,但与一般的普通文件功能不同,仅作抽象接口使用),通过读写接口文件管理内核功能。
会话(session)
会话是一个内核概念,用来为一组进程分配终端和管理生命周期。很多时候,这个概念与实际的终端会话、图形外壳会话、SSH 会话时深度绑定的,最常见的例子就是使用 SSH 运行了一个后台程序,它没设置会话,因此仍然依附于 SSH 打开的 shell,当用户从 SSH 退出了,这个程序也会因为会话结束被发送 SIGHUP,导致其终止。
有些有特殊需求的程序(比如 daemon)会通过设置会话成为新会话组组长进程,避免因为会话结束而退出,也避免进程挂到某个终端上
内核模块
加载内核模块本质上是将可链接文件加载到内存,然后根据内核自己的符号表去重定位其中的符号,建立模块和内核的联系,拓展内核功能。
对于树外驱动,包管理器通常不提供适用于所有内核的模块(或者根本不提供需要手动下载源码),为了在更新内核或使用其他支线的内核时自动编译安装这些内核模块,可以使用 DKMS 管理它们。
构建 initramfs
发行版提供了构建 initramfs 的工具,如 Arch 的 mkinitcpio,它们通过配置文件按一定的顺序打包启动早期所需功能。
一些特定的功能需要通过编辑配置文件生成特定 initramfs 完成,比如 plymouth 启动动画或文件系统加密。
可以通过不同的 initramfs 调整启动流程,例如在多数现代机型上 Arch 使用 systemd 系列钩子比早期的使用 udev 系列钩子有更快的启动速度。
kvm
用于提供硬件虚拟化的内核子系统,使相同架构的虚拟机可以直接在开启虚拟化的 CPU 上执行指令而不用使用软件模拟。流行的虚拟机宿主程序几乎都能使用 kvm 加速。一般认为最灵活的,能配合 kvm 实现类似真机性能的开源程序是 qemu 和 libvirt(qemu 可以结合 libvirt 实现高级管理,qemu 只是一个启动和运行虚拟机的 shell 工具)。
从手动安装 Arch Linux 体验 Linux
阅读本章请打开 Arch Wiki 的安装指南页面对照阅读。如果真的要实践,请至少学会使用 nano 和 vim 工具以便在终端编辑文本文件。
获取安装介质
把 iso 文件下载下来,查看校验和是个好习惯,如果来源可靠(正常镜像站或种子)大部分时候看不看不会影响安全性。
如何制作安装介质 Wiki 没有细说,如果你现在就在使用 Linux,那么可以直接使用 dd 命令将 iso 文件 dump 到一个闲置的没有任何有用数据的U盘(不用预先格式化)或空光盘。或者按照 Wiki 推荐使用 Ventoy,如果你在使用 Windows,除了 Ventoy 之外 Rufus 也是不错的选择。
一定要把U盘有用的数据拷出来,小心使用 dd,避免把它用成 disk destroyer。。
初始化 live 环境
遵循 Wiki 的指引继续,直到看到 zsh 的提示符,写着“root@忘了后面一串什么东西”,空敲回车会在下方一行再生成一个提示符,可以随便打一些字,按箭头左右移动光标,backspace删除光标左边的字符,delete删除光标上的字符(光标是个块),在边界上操作可能会听到蜂鸣器吱吱叫,如果瞎写的最后一个字符不是 \ 且按了回车,zsh 大概看不懂你写了啥,会用英语报错然后再弹一个提示符,这个和你交互的程序就是作者目前在用的 shell —— zsh,你看到的界面挂在某个 TTY 上。
更改键盘布局和字体大概是没有必要的,应该没有使用非美式键盘的读者开着翻译软件读本文 不过如果嫌弃字体不适合高分屏,可以试试 Wiki 建议的 setfont ter-132b 命令,这个 live 环境完全运行在内存上,非故意一般不会把烧好的盘玩坏,不会搞了重启重插就好。
对了,重启命令是 reboot,关机可以用 poweroff,输完命令按回车它就执行了,执行完毕会再弹一个提示符,没有输出且 zsh 的新提示符右边没有红色的错误码一般就是执行成功了。
现在一般的电脑都使用了 UEFI 引导,且可能已经逐渐抛弃了 CSM 模式,如果你发现你处于 Wiki 描述的 Legacy BIOS 引导模式,那大概的确是个老古董了,虽然问题不大。
对于一些处于过渡时期的设备,它们支持 UEFI 和 CSM 兼容模式,同时也支持完全的 Legacy BIOS,当你使用较新的U盘制作启动盘时,可能不能以 UEFI 启动 live 系统,但它们支持从硬盘里以 UEFI 模式启动。此时建议在主板设置中选择带 CSM 的 UEFI,然后尝试坚持以 UEFI 的方式完成基础安装,然后验证引导硬盘中的系统时是否使用正确的 UEFI 方式。
参考 Wiki 内容检查网络连接并连接网络,如果使用无线网,检查 rfkill 状态大概有必要(曾经有被 block 过),iwctl 进入 iwd 的交互式命令行,没用过的需要看看 Wiki 上对应页面的连接网络部分,要退出可以输入 quit 或 exit 后回车,也可以在空行按 Ctrl + d(刷新输入缓冲区,通常规范的交互式 shell 程序读到空的缓冲区会认为输入结束自行退出)。
开始真的操作
这部分的操作必须格外小心,否则可能损伤硬盘上已有的数据(大事),或者白白写好大一块数据(相对小事)。相关内容由于涉及到自行规划硬盘布局可能更难理解,可以寻求可靠的 AI chatbot 或周围人的帮助。
如果你的硬盘上有 Windows 的 NTFS 分区且你需要缩小它,请先启动 Windows 把它压缩卷,获得你认为合适的空闲空间。
然后需要明确你想要把硬盘分成什么样子,Wiki 推荐将 ESP 分区作为 /boot(用来存储引导工具、内核、initramfs的分区),如果你的 ESP 后方紧接着不方便移动的数据,那么笔者更推荐将 ESP 挂载为 /efi 或 /boot/efi,使用单独的 fat32 分区作为 /boot,如果你打算使用 grub2 作为引导工具,那么不单独分区挂载到 /boot,将内核、grub2、initramfs 放在根目录分区的 /boot 目录下也是可行的,但还是建议分离 /boot,万一之后要搞分区加密呢。swap 分区也不是刚需,如果你的物理内存在 16GiB 及以上,甚至 8GiB 及以上,可能你前期根本用不上内存交换,后期有需求可以在根目录下新建一个 swapfile。
对于包括根目录分区这种主要数据分区,不单单要想清楚怎么分区,还要想清楚选择什么文件系统,甚至文件系统的选择可能改变你的分区方案。如果使用 btrfs 这种带有子卷功能的文件系统,那么在大多数情况下家目录 /home 不用单独分区挂载,只需要挂载到 btrfs 分区(确切而形象地说是池子,这个东西非常强大可以把好多物理分区当一个来用)的子卷上,如果你使用 xfs、f2fs、ext4,那么建议要单独分区一个 /home,这可能由于分配不合理造成后期管理相对困难,需要仔细斟酌。
(只是建议,并非传教)对于拿不定如何分区的,认为硬盘硬件合适,且能接受 btrfs 管理门槛的,建议使用btrfs,或将大幅降低后期重构硬盘布局的负担。笔者曾经在 2T+1T nvme 的笔电上删了 Windows 重构硬盘布局时开着 Arch 看着番在后台把 Arch 本身的 btrfs 跨硬盘开走了(
上一段写完了只是笔者也是括号星人
当你大概有“要多大的分区 - 挂载目录 - 文件系统”的清晰思路了,就可以在硬盘上施工了,推荐使用 cfdisk /dev/硬盘设备节点 命令,它提供了 tui 交互,且能够清晰地看出分区之间的位置关系,同时提供扇区数和给人看的控件大小视图。仅当你已经凭借 gparted、Windows 磁盘管理等 gui 工具摸清已有布局,并完全想好分区方案时,才建议使用 fdisk /dev/硬盘设备节点 命令来分区。
存储设备节点的命名通常依赖设备类型,nvme 硬盘节点为 nvmeXnY,其中 X 为从 0 开始的枚举号,Y 是通常为 1 的命名空间编号;SCSI / SATA / IDE 硬盘和U盘,即除了 nvme 的大多数存储介质,节点名为 sdX,其中 X 为从小写 a 开始的枚举号; MMC / eMMC / SD 卡节点名为 mmcblkX,其中 X 为从 0 开始的枚举号;较少见的虚拟硬盘节点号为 vdX,X 为从小写 a 开始的枚举号。存储设备上的分区名在整块盘的设备名的后面多了字母 p 和从 1 开始的数字。
在正式分区之前,你需要确认硬盘的分区表,如果是新盘或准备好数据备份要转换为 GPT 分区表的 MBR 硬盘(写入分区表一般需要破坏硬盘所有数据),请使用 cfdisk 删除所有分区并转换分区表,或使用 parted 的 mklabel 指令创建 GPT 分区表(除非你的设备只支持 Legacy BIOS,此时应选择 MBR 分区表)。如果你的设备没有 ESP 分区,请在分区工具中标记将作为 ESP 的分区为 ESP(EFI System)。
分区工具的改动不会立即应用,需要完成配置后一次性写入或丢弃更改。
当分区工作完成后,需要对新分区进行格式化,注意是新分区,对已有分区格式化会损坏分区,尤其注意 ESP 是新建的还是已有的,这点 Wiki 也有提到。格式化数据分区使用 mkfs 系列命令,格式化交换分区使用 mkswap 命令。
For btrfs
btrfs 通过子卷(subvolume)分离不同类型的数据、管理快照(保护和恢复数据的方式)和实现部分挂载,整个文件系统被称为顶级子卷,在日常使用中,一般不推荐直接使用顶级子卷,而是在顶级子卷下创建子卷来挂载和使用。
子卷在形式上表现为一个目录,不同子卷的目录间存在嵌套时,它们依然作为不同的子卷管理,如果 /mnt 挂载了 btrfs 的顶级子卷,/mnt/@A/@B 中 @A 和 @B 分别是两个子卷,对 @A 创建快照时 @B 会被视为一个空目录,即使 @B 子卷中有内容,它也不会随着这种“形式上”的上下级关系被 @A 管理,同理,快照顶级子卷也会忽略其他所有子卷中的内容。
可以使用 btrfs subvolume create 子卷对应目录位置 来使用类似创建目录的方式创建子卷,在挂载子卷时,使用 subvol 或 subvolid 挂载子卷,subvol 是子卷对应目录相对顶级子卷的路径,顶级子卷为 /,subvolid 是子卷的 id,可以使用 btrfs subvolume list 挂载目录 查看该挂载目录下哪些目录是子卷以及对应的 id,顶级子卷 id 固定为 5,使用 subvol 或 subvolid 通常都是稳定的,但使用 subvol 需要注意在对子卷目录使用 mv 移动或重命名以及使用 btrfs subvolume rename 重命名后需要同步修改。
笔者使用平铺子卷的方式,就是将所有子卷作为顶级子卷的子目录,且顶级子卷的子目录全是子卷。与之对应的方式叫做嵌套子卷,利用了子卷的目录可嵌套但依然独立管理的特性,将子卷目录作为另一个子卷目录下级的目录。笔者认为嵌套子卷不如平铺子卷直观,平铺子卷依然使用笔者更习惯的类似挂载不同文件系统的方式挂载不同子卷,下面演示一下在安装系统时面对崭新的 btrfs 如何用平铺子卷的方式去分离完成最常见的分离 / 和 /home 所在子卷的方法(设备名瞎编的,所有命令以 root 用户运行)。
首先挂载这个新 btrfs 的顶级子卷 mount -o subvol=/ /dev/nvme0n1p2 /mnt,然后新建两个子卷,平铺子卷流行以 @ 开头以作用命名,btrfs subvolume create /mnt/@ btrfs subvolume create /mnt/@home,其中 @ 用于以后挂载到 /,@home 用于以后挂载到 /home。然后解除挂载 umount /mnt,挂载根目录子卷 mount -o subvol=@ /dev/nvme0n1p2 /mnt,创建家目录挂载位置 mkdir /mnt/home,嵌套挂载家目录 mount -o subvol=@home /dev/nvme0n1p2 /mnt/home,完成。使用 btrfs 的读者可以参考这个步骤完成后续安装步骤。
嵌套子卷则需要在 @ 子卷下创建子卷home btrfs subvolume create /mnt/@/home,此时或许可以不使用 @ 子卷了,直接用顶级子卷作为根目录挂载,这样也许更贴合目录结构,但笔者认为模糊了目录和子卷的界限,个人感觉不习惯。
另外,btrfs 支持跨越多个物理分区(因此笔者习惯称其为池子),它们将被识别为同一个文件系统,拥有同一个 uuid,用设备节点挂载时可以使用任何一个分区的设备节点。可以使用 btrfs device add 设备节点 添加分区,添加的分区无需格式化,如果已经格式化,那么这个操作将直接覆盖,因此需要先保证目标分区上没有有价值的数据。同一个池子可以通过多种方式组织数据,默认的方式是 single,数据只存一份且不保证不同设备间均衡也不保证一定按某种顺序占用不同设备,但能利用多个设备的容量。
笔者采用的是双 nvme,两个池子分别在两个 nvme 上各占用一个分区,同一个池子的两个分区等大,其中一个池子使用 raid1 策略进行冗余,另一个池子的数据使用 raid 0 策略进行容量合并和加速,元数据 raid1。将根目录和个人主用户家目录下的某些目录分别放在 raid0 池子的平铺子卷上,把 /home 放在 raid1 池子的子卷上。
安装
前面已经完成了基本准备工作,已经可以准备安装系统了,对于挂载那块,btrfs 用户请参考上一节。
挂载你的根目录分区到 /mnt,现在这个目录下的结构就和你根目录分区上的等价了,然后创建所有后续要挂载的目录,并挂载它们。对于交换分区,这里不是必要的,它只是方便 genfstab 工具去检测它并且自动写入 fstab 配置中。
然后按照 Wiki 的指导配置镜像,国内很多在使用清华和中科大,live 的镜像列表里大概已经包含了。到了使用 pacstrap 那一步,linux 包可以替换为 linux-lts 或 linux-zen,它们是不同的内核支线,具体区别参考网上,为了避免歧义,linux 包后面成为主线内核。通常 shell 工具让使用 [Y/n] 确认,y 是 yes,n 是 no,大写的一个是默认选项不输入直接回车就选的是那个
fstab 配置第一次很难一眼看懂,可以查阅相关页面,btrfs 用户注意检查 subvol 是否是对的,其他的挂载参数参考网上。
chroot 操作可以将根目录临时切换到其他目录上去,arch-chroot 可以帮你同时把某些需要的伪文件系统也挂上去,省去了很多麻烦,完成这一步骤后,形式上你已经处于新的操作系统里了。
在继续之前,笔者建议先给新操作系统配置 pacman 镜像源,然后安装常用软件包比如 vim(图形外壳用户安装 gvim 以在 vim 里使用系统剪贴板) 或 nano 编辑器,无线用户安装 iwd 或 networkmanager(可以给 networkmanager 配置 iwd 作为后端,参考相关页面和网上),避免拔掉 live 重启还需要插网线安装,btrfs 用户安装 btrfs 用户态工具(必须有否则 initramfs 里的 fsck 过不了),nvidia 用户安装 nvidia-open(如果使用的不是主线内核请安装 nvidia-open-dkms)和 nvidia-utils 组合包。如果要安装图形外壳,比如 plasma 桌面、gnome 桌面、niri 混成器、hyprland 混成器等,还有显示管理器如 sddm、gdm 等,sddm 基本适用于所有情况。安装使用 pacman -S 软件包名,使用 -Ss 查询相关软件包,-Syu 更新索引并安装所有更新的软件包。具体查看相关 Wiki 页面,nvidia 用户还要 ban 掉 nouveau。
后面就是设置时区和本地化了。时区一般选择 Asia 下的 Shanghai 设置无夏令时的 UTC+8。如果要安装图形外壳(完整桌面环境或 wayland 混成器),建议进行详细本地化,如果只用到终端,也不想看到中文,可以仅检查是否选择 C.UTF-8,en_US 和 C 几乎完全相同,另外英语笔者更倾向于 en_GB,因为笔者不喜欢倒过来写的日期日月和距离的一次方之外的英制单位,不过它们似乎并不会真的影响到什么。如果你要在 TTY 下看到中文,那是另外的任务,不在本文讨论范围之内。 如果你已经设置了中文,那么默认情况下中文在 TTY 下会显示为方块,此时请使用 export LANG=C.UTF-8 export LANGUAGE=C.UTF-8 来临时使用标准语言,确保你能够操作,export 的环境变量作用范围只在这个 shell 中,不用担心影响语言设置。
mkinitcpio 可以跑一下,验证是否有问题。
然后设置 root 密码,可以顺便创建一个个人用户,并 passwd 用户名 为它设置密码,如果你希望某个普通用户作为管理员,能够临时获得 root 权限,请安装 sudo 包,然后 visudo 解除 wheel 组的引用,usermod +aG wheel username 给用户添加到 wheel 用户组,具体操作可以查看 sudo 页面。
后面安装引导程序,任何情况都可以试试 grub,具体参考 Wiki 页面,本站也有相关 blog。
安装工作完成,重启从硬盘启动验证新系统。
最后
Arch Wiki 是个好东西,不管你是否用 Arch。