Linux 系统基础
Linux 系统的组成与练习环境
Linux 内核负责管理处理器、内存和设备,并为进程提供文件、网络等访问接口。安装 Ubuntu Server 后得到的完整系统,还包含命令行工具、系统库、软件包管理器和服务管理器。
一台运行 Ubuntu Server 的机器
├─ 用户空间
│ ├─ 应用程序:Java 服务、数据库、编辑器等
│ ├─ Shell 与命令:Bash、ls、cp 等
│ ├─ 系统库:为程序提供常用函数及操作系统接口封装
│ └─ 系统服务:服务管理、网络配置、日志收集等
├─ Linux 内核
│ ├─ 调度进程和线程,管理内存
│ ├─ 提供文件系统、网络协议栈和设备驱动
│ └─ 执行访问检查及资源限制
└─ 硬件或虚拟硬件:CPU、内存、磁盘、网卡读取一个文件时,命令或应用程序向内核请求打开文件、读取内容;内核解析路径、检查访问权限,再通过相应文件系统取得数据。程序可以经由系统库发起这些请求。这个过程中的“系统调用”就是用户空间进入内核服务的接口。
Shell 负责解释命令输入。它识别命令名、参数和重定向,执行内建命令,或启动外部程序。Bash 是一种 Shell;终端则负责呈现文字和接收键盘输入。同一个终端可以运行不同的 Shell,程序也可以由服务管理器启动,无须有人打开终端一直守着。
发行版把内核与用户空间软件组织成可安装、可更新的系统,决定默认软件组合、配置方式、软件仓库及支持周期。Ubuntu 与 Rocky Linux 都使用 Linux 内核,但软件包工具、包名、默认安全机制和服务配置可能不同。发行版版本与内核版本分别描述这两层,诊断时需要分别查看。
物理机、虚拟机与容器
| 运行方式 | 内核在哪里运行 | 适合观察的对象 |
|---|---|---|
| 物理机 | 操作系统内核管理实际硬件 | 完整系统启动、真实设备和主机资源 |
| 虚拟机 | 客体内核运行在虚拟化平台提供的硬件上 | 客体系统安装、服务、自启、虚拟磁盘和网络 |
| 普通 Linux 容器 | 容器进程使用所在 Linux 主机的内核 | 隔离后的进程、文件与网络视图,以及容器应用 |
虚拟机有自己的启动过程和客体内核,可以单独关机、换内核或重装系统。普通容器中的 /etc/os-release 描述镜像内的发行版文件,uname 返回的却是运行容器的内核信息。在 Windows、macOS 上运行 Linux 容器时,中间通常还有一层 Linux 虚拟机。
因此,服务自启、挂载和重启练习使用专用虚拟机。WSL 适合进行不少命令行练习,但它的启动、网络和存储还受到 Windows 集成方式影响;WSL 中的成功结果不能直接用于判断一台普通服务器的行为。
安装一台独立的 Ubuntu Server
练习系统采用 Ubuntu Server 24.04 LTS。它是一条固定的发行线,维护范围和支持期限可查 Ubuntu 发布周期。已有独立测试虚拟机的读者,可以直接在控制台登录并执行下一节的系统识别命令。
新建虚拟机使用以下配置即可开始文件、进程和小型服务练习。这是练习配置,不是生产容量估算:
虚拟机名称:linux-lab
处理器:2 个虚拟 CPU
内存:4 GiB
磁盘:新建 20 GiB 虚拟磁盘,仅供本次安装使用
网络:虚拟化软件提供的 NAT 网络,能够自动分配地址
安装介质:Ubuntu Server 24.04 LTS ISO
首次入口:虚拟机控制台,不依赖 SSHNAT 让虚拟机借助宿主网络访问软件仓库,不要求为练习机分配公网地址。它仍然能够访问网络上的其他系统,不能把 NAT 当作恶意代码沙箱。不要挂载宿主业务目录、直通真实磁盘,或把已有服务器磁盘接入这台练习机。
下载与检查介质。 从 Ubuntu 24.04 官方下载目录选择 Server install image。amd64 对应 x86-64,适用于相应的 Intel、AMD 虚拟 CPU;ARM64 虚拟机使用其他架构目录中的 arm64 介质。
把 ISO 和同目录的 SHA256SUMS 下载到同一个新文件夹。以 ubuntu-24.04.4-live-server-amd64.iso 为例,在下载文件夹中计算摘要;文件名必须与实际下载一致。
Linux 下载机的普通用户执行:
sha256sum ubuntu-24.04.4-live-server-amd64.isoWindows 下载机可以在文件夹中打开 PowerShell,执行:
Get-FileHash -LiteralPath .\ubuntu-24.04.4-live-server-amd64.iso -Algorithm SHA256将输出的 64 位十六进制值与 SHA256SUMS 中同一完整文件名对应的值比较,大小写不影响比较。不同则停止安装,重新检查文件名、下载是否完成,以及 ISO 与摘要清单是否来自同一介质目录。Get-FileHash 的参数说明可用于其他下载文件的摘要检查。
摘要用于检查 ISO 与清单是否一致;清单本身的真实性还可以通过同目录的 SHA256SUMS.gpg 校验。签名验证需要可信的 Ubuntu 镜像签名公钥和 GnuPG,完整步骤见 Ubuntu 安装镜像真实性校验。使用镜像站转存文件时,尤其要核对签名公钥来源,不能只比较随下载文件一起提供的摘要。
创建虚拟机并接入 ISO。 在已有虚拟化软件中创建新机器,分配上述资源,新增专用虚拟磁盘,将 ISO 接入虚拟光驱,启动后打开控制台。不要使用无人值守模板替你选择磁盘和账号,首次安装需要看清这些设置。
以已经启用 Hyper-V 的 Windows x86-64 计算机为例,在 Hyper-V 管理器中选择“新建 → 虚拟机”,创建第 2 代虚拟机,设置内存、新虚拟硬盘和 ISO。网络可选择已有的 Default Switch;没有这个交换机时,需要先准备带地址分配能力的 NAT 网络。在启动前进入该虚拟机设置,将处理器数量设为 2,并在安全启动模板中选择 Microsoft UEFI Certificate Authority。菜单位置与安全启动设置见 Ubuntu 在 Hyper-V 上的安装说明,创建向导见 Hyper-V 创建虚拟机。
如果管理器报告权限不足,应先由宿主管理员提供虚拟机管理权限或准备测试实例;这不是客体 Linux 中运行 sudo 能解决的问题。没有启用虚拟化功能的宿主,还可能涉及功能安装和宿主重启,应按虚拟化软件的安装要求单独安排。
在控制台完成安装。 Ubuntu Server 使用文字界面的安装器,方向键移动,Tab 切换区域,Enter 确认。主要选择如下:
- 选择熟悉的语言和实际键盘布局。系统类型选择普通 Ubuntu Server,暂不选精简安装。
- 网络接口使用自动获取地址。没有地址时,回到虚拟机设置确认网卡已连接 NAT 网络,再返回安装器;不要直接照抄其他机器的静态 IP。
- 没有下载代理时留空代理地址。企业网络要求代理时填写实际代理,并等待软件仓库连接检查通过。
- 在存储界面选择新建的 20 GiB 虚拟磁盘。可以使用整个磁盘的引导布局,简单练习可取消 LVM;提交写入前再次检查磁盘容量和变更清单。
Use an entire disk会替换所选盘上的分区和数据,不能对含有文件的现有磁盘照做。 - 主机名填写
linux-lab,用户名例如learner,设置独立密码。主机名标识机器,用户名标识登录身份,两者可以不同。 - SSH 服务和推荐的额外软件均可暂不选择。控制台登录不需要开放远程端口。
- 安装完成后重启这台虚拟机,并按提示弹出虚拟光驱中的 ISO。再次出现安装界面时,先检查启动顺序和 ISO 是否仍连接。
安装器不同版本的界面细节可查 Subiquity 逐屏说明,磁盘布局与加密选项可查存储配置说明。
看到 login: 后输入安装时创建的用户名,再输入密码。密码输入期间通常不显示星号或字符;按 Enter 提交即可。安装器创建的普通用户可以通过 sudo 执行获准的系统管理命令,日常文件练习使用普通身份。
登录后识别当前系统
在虚拟机控制台逐行执行以下只读命令。代码块不包含提示符,不要把终端显示的 $ 或 # 一起复制进去。
cat /etc/os-release
uname -r
uname -m
id -un
pwd| 命令 | 读取什么 | 在本次安装中的判断 |
|---|---|---|
cat /etc/os-release | 发行版标识文件 | ID 应为 ubuntu,VERSION_ID 应为 24.04 |
uname -r | 正在运行的内核发布版本 | 记录实际输出;安装介质、内核包和更新会影响它 |
uname -m | 内核报告的机器架构 | x86-64 环境通常为 x86_64,ARM64 为 aarch64 |
id -un | 当前有效用户的名称 | 使用安装时创建的普通用户,例如 learner |
pwd | 当前工作目录 | 普通用户登录后通常位于自己的家目录 |
cat 将文件内容显示出来;uname 查询内核运行信息。即使系统安装了新内核,尚未重启时 uname -r 仍显示当前运行的内核。遇到与文章不同的结果,先确认打开的是目标虚拟机控制台,而非宿主终端、容器或另一台服务器。
结束练习时,exit 退出当前 Shell 或登录会话,虚拟机仍会继续运行。需要关机时,只在 linux-lab 的控制台执行 sudo poweroff,等待虚拟化管理器显示已关闭;不要用强制断电作为日常退出方式。
终端、路径与文件操作
输入一条命令
常见的提示符类似 learner@linux-lab:~$:它提示登录用户、主机及当前目录,最后的 $ 表示可以输入命令。提示符可以定制,判断实际身份仍使用 id。
ls -l /etc/os-release
│ │ └─ 参数:要查看的路径
│ └──── 选项:使用详细格式
└─────── 命令名:列出文件信息Shell 按空格等分隔符解析这条输入,再把选项和路径交给 ls。选项含义由具体命令决定;另一个命令的 -l 未必也表示详细格式。
Ubuntu 普通用户的默认交互 Shell 通常是 Bash。执行 bash --version 可以查看安装的 Bash,printf '%s\n' "$BASH_VERSION" 可以查看当前 Bash 的版本变量;如果后者为空,应先确认当前 Shell。以下引用、变量和管道操作使用 Bash 语法。Ubuntu 的 Bash 手册包含完整的输入解析规则。
输入过程中,Tab 可以补全命令或路径,上下方向键可以调出历史输入,Ctrl+C 可以取消当前输入。命令已经运行时,Ctrl+C 通常向前台作业发送中断信号,具体退出行为由程序处理。
建立自己的练习目录
Linux 把文件组织在以 / 为起点的目录树中。每个进程都有当前工作目录,相对路径从这里开始解释;以 / 开头的绝对路径则从根目录开始。
/ 根目录
└─ home
└─ learner 普通用户的家目录
└─ linux-lab.随机后缀 本次练习目录
└─ notes
└─ lesson.txt~ 在常见的 Bash 输入位置会展开为当前用户的家目录。它与根目录 /、root 用户的家目录 /root 分别指向不同对象。. 表示当前目录,.. 表示上一级目录。
以下命令在刚登录的普通用户 Bash 中执行,不需要 sudo。逐行执行;任何一行报错都先停下处理,不继续复制后面的写入命令。
LINUX_LAB=$(mktemp -d "$HOME/linux-lab.XXXXXX")
printf '%s\n' "$LINUX_LAB"
cd "$LINUX_LAB"
pwd
mkdir notes backup
printf '%s\n' 'topic=linux' 'status=created' > notes/lesson.txt
cat notes/lesson.txtmktemp -d 创建一个新目录,末尾的 XXXXXX 被替换成随机字符,避免覆盖旧练习。$(...) 取出命令的输出,赋给变量 LINUX_LAB;$HOME 是当前用户的家目录变量,$LINUX_LAB 则保存刚创建的路径。双引号使展开后的路径作为一个参数传递。mktemp 手册还说明了模板和默认访问权限。
前两次路径输出应相同,随后看到文件的两行内容:
topic=linux
status=createdprintf 按格式输出文字,%s 对应一个字符串,\n 表示换行。> 由 Shell 处理,把输出写入右侧文件:文件不存在时创建,已存在时先清空。因此这里只把它用于新练习目录中的 lesson.txt。mkdir 成功时通常没有文字输出。
如果 mktemp 提示无法创建目录,先用 id -un 和 ls -ld "$HOME" 核对当前用户与家目录,不要用 sudo 创建一个由 root 持有的练习目录。cat 报找不到文件时,执行 pwd、ls 和 ls notes,确认创建步骤是否成功及当前路径是否正确。
变量只保存在当前 Shell 中。重新登录后,进入前面打印出的实际目录再继续;linux-lab.随机后缀 是形态说明,不能原样作为目录名输入,也不要用通配符随意选择多个历史练习目录。
查看路径与系统目录
在练习目录执行:
pwd
ls
ls -l notes/lesson.txt
cd notes
pwd
cat ./lesson.txt
cd ..进入 notes 后,同一文件可以用 ./lesson.txt 访问;返回上一层后,路径又变为 notes/lesson.txt。cd 改变的是当前 Shell 的工作目录,没有移动任何文件。
ls -l 的第一列开头表示类型:- 是普通文件,d 是目录,l 是符号链接。后续列包含权限、属主、属组和大小等信息。Linux 文件名通常区分大小写,lesson.txt 与 Lesson.txt 可以是两个文件;以 . 开头的名字默认被 ls 隐藏,使用 ls -a 才会列出。这种隐藏约定不限制其他用户访问。
系统中的常见目录承担不同用途:
| 路径 | 常见内容 | 查看时需要分清的对象 |
|---|---|---|
/etc | 系统和服务配置 | 修改这里通常需要管理权限 |
/home、/root | 普通用户及 root 的家目录 | 用户文件与系统配置分开保存 |
/usr | 发行版安装的程序、库和共享数据 | /usr/bin 中可找到大量普通命令 |
/var | 日志、缓存、数据库等可变数据 | 文件可能随服务运行持续增长 |
/run | 本次启动期间的运行状态 | 不用于保存需要跨重启保留的业务数据 |
/tmp | 临时文件 | 可能被定期清理,不能当长期存储 |
/dev | 设备节点等设备访问入口 | 不是一个可以随便编辑的普通数据目录 |
/proc、/sys | 内核提供的进程、系统及设备视图 | 许多内容是读取时生成的,写入部分节点会改变系统状态 |
/opt、/srv | 附加软件、服务提供的数据等 | 具体用途仍取决于软件和本机部署约定 |
这些名称描述目录用途。实际系统可能把 /bin 链接到 /usr/bin,也可能在某个目录上挂载独立文件系统;路径相邻不代表数据在同一块磁盘。完整目录约定见 Filesystem Hierarchy Standard。
查帮助、编辑并保存文件
短选项说明可以直接查看:
ls --help
help cdcd 是 Bash 内建命令,因此使用 help cd。外部程序通常提供 --help 或自己的帮助入口;man 则按名称打开手册页,例如 man ls。手册若在分页器中显示,空格翻页,/pattern 搜索,q 退出。屏幕仍显示手册时,输入的新文字不会作为 Shell 命令执行。
文件编辑使用 nano。在 Ubuntu 24.04 练习机的普通用户终端先检查工具:
command -v nano有输出路径即可使用。若没有找到,先安装;同样地,只有需要 man 或 less 且检查发现缺失时,才安装对应包:
sudo apt update
sudo apt install nano man-db lesssudo 临时以获准的管理身份运行这条命令,可能要求输入登录密码。apt update 更新软件包索引,不是升级全部已安装软件;确认它成功后再执行安装。安装后重新执行 command -v nano,应返回命令路径。
apt 无法连接软件仓库时
在当前虚拟机中用普通用户查看地址与路由:
ip -brief address
ip -4 route show default
ip -6 route show defaultlo 是本机回环接口;还应有接入虚拟网络的接口及其地址。默认路由中的 via 表示下一跳网关,dev 表示使用的接口。在这台通过 NAT 访问公网仓库的练习机中,只有回环地址或缺少可用出口时,应先回到虚拟化管理器检查网卡连接与自动地址分配;不要根据别人的输出添加固定网关。ip 手册解释了这些查询参数。
如果错误是无法解析仓库名称,从 apt 错误中取主机名,例如 archive.ubuntu.com,查询系统是否能返回地址:
getent ahosts archive.ubuntu.com实际失败的是代理主机时,查询那个代理名称。无结果时先核对虚拟网络提供的 DNS 设置;有地址但连接仍失败,则保留 apt 中的目标、超时或拒绝信息,核对安装时使用的代理与网络访问要求。getent走系统配置的名称查询机制,返回地址不代表已经连接到软件仓库。修复后重新执行 sudo apt update,确认相关仓库不再报错,再重试安装;签名或证书错误不能通过关闭校验处理。
在练习目录打开刚创建的文件:
nano notes/lesson.txt用方向键把 status=created 改成 status=edited,按 Ctrl+O 写入,确认底部文件名仍是 notes/lesson.txt 后按 Enter,再按 Ctrl+X 退出。nano 底部的 ^O 表示 Ctrl 与 O 的组合键,M- 表示 Meta 键组合。退出时若询问是否保存,确认当前文件及修改内容再选择,不要为了退出盲目接受其他文件名。更多按键见 nano 手册。
返回 Shell 后执行:
cat notes/lesson.txt应看到第二行变为 status=edited。仍显示旧内容时,重新打开文件,核对是否保存成功以及是否编辑了另一个路径。
复制、比较、检索和删除
在同一个练习目录保存副本,再向原文件追加一行:
cp notes/lesson.txt backup/lesson.before.txt
printf '%s\n' 'owner=learner' >> notes/lesson.txt
diff -u backup/lesson.before.txt notes/lesson.txtcp 创建副本,>> 把输出追加到文件末尾。diff -u 比较修改前后,输出中以 +owner=learner 表示新增行;开头 ---、+++ 标识两份文件。它报告的是内容差异,不会替你合并文件。这里选择了尚不存在的副本名;复制或移动到已有路径时,要先确认是否允许覆盖。
文件名和文件内容使用不同的搜索工具:
find notes -type f -name '*.txt'
grep -nF 'status=' notes/lesson.txtfind 从 notes 向下查找普通文件,并用 *.txt 匹配名称。单引号让星号原样交给 find,避免 Bash 提前按当前目录展开。grep 检查文件内容,-n 显示行号,-F 使用固定字符串;这里应输出 2:status=edited。按文件名找到目标后再搜索内容,可以避免从 / 开始扫描不相关目录。
grep 找到匹配时退出状态为 0,没有匹配为 1,执行出错通常为 2。没有匹配是一次有效的查询结果,缺少文件或无法读取才需要改变查询路径或权限。grep 手册列出了匹配方式与退出状态。
创建一个可删除的副本,观察移动与删除:
cp notes/lesson.txt notes/disposable.txt
mv notes/disposable.txt notes/renamed.txt
ls notes
rm -i notes/renamed.txt
ls notes移动后列表包含 renamed.txt;rm -i 询问删除这个明确文件,确认后它从列表消失。命令行删除通常不经过桌面回收站,因此只操作这里新建的副本。删除失败时先看错误和目标路径,不要顺手加上 sudo、-r 或 -f。
空目录可以用 rmdir 删除。它会拒绝非空目录,这个行为适合检查练习后的残留文件。原文件和 backup 副本继续保留,后面仍会使用。
文本、文件名与归档
扩展名方便人和程序识别用途,Linux 不会因为文件名以 .txt 结尾,就保证里面一定是文本。可用 file notes/lesson.txt 检查内容特征;若没有 file 命令,在 Ubuntu 练习机执行 sudo apt install file 后再查看。对当前内容,输出应将它识别为 ASCII 文本,而不是可执行文件或压缩数据。
中文配置通常使用 UTF-8,Linux 文本行常用 LF 换行。从 Windows 复制来的 CRLF 文件可能残留回车字符,尤其容易影响脚本首行的解释器路径。file 能给出线索,但不能替代应用对编码的实际要求;不要对未知二进制文件批量执行字符转换。
文件名带空格时,用引号把它作为一个参数:
cp notes/lesson.txt 'notes/lesson copy.txt'
ls -l 'notes/lesson copy.txt'不加引号,Bash 会把空格两侧解析为不同参数。单引号内基本按原文处理;双引号仍允许 $变量 和命令替换。使用路径变量时通常写成 "$LINUX_LAB",不要依靠“目前目录名没有空格”来省略必要引用。
归档把多份文件及目录组织成一个文件,压缩再减少数据体积。.tar.gz 常表示先按 tar 格式归档,再用 gzip 压缩。在 Ubuntu 练习机先用 command -v tar 和 command -v gzip 检查;缺失时执行 sudo apt install tar gzip。然后在练习目录运行:
tar -czf notes.tar.gz notes
tar -tzf notes.tar.gz
mkdir restored
tar -xzf notes.tar.gz -C restored
diff -r notes restored/notes-c 创建归档,-t 列出成员,-x 解包;-z 使用 gzip,-f 后跟归档文件名,-C 指定解包目录。列表应包含 notes/ 下的文件,最后的比较应没有差异输出。解包到新目录可以在不覆盖原文件的情况下检查结果;不可信归档还可能含异常路径、链接和权限,不能直接以 root 身份解压进系统目录。GNU tar 手册提供完整的归档格式和安全说明。
标准输入、输出和错误
进程通常通过三个约定的通道接收输入、输出结果和报告错误。Shell 的重定向可以改变这些通道连接的对象。
键盘或上游输出 ──→ 标准输入 stdin (0)
│
命令
├──→ 标准输出 stdout (1) ──→ 终端、文件或下游命令
└──→ 标准错误 stderr (2) ──→ 终端或单独的错误文件括号中的数字是文件描述符编号。标准输出和标准错误默认常常显示在同一个终端,所以只看屏幕不能区分它们。继续使用练习目录,执行:
cat notes/lesson.txt notes/missing.txt > output.txt 2> error.txt
printf 'exit=%s\n' "$?"
cat output.txt
cat error.txtnotes/missing.txt 是故意指定的不存在路径。已有文件的内容进入 output.txt,找不到文件的诊断进入 error.txt,cat 返回非零状态。错误文字可能随系统语言变化,关键是它指向 notes/missing.txt。
$? 读取上一条命令的退出状态,必须紧接着检查。执行其他命令后,它就会被更新。0 通常表示命令按自己的规则成功,非零值的具体含义由命令规定;前面的 grep 和 diff 都把“有无匹配或差异”编码在退出状态中。
没有 2> 时,即使标准输出已经写入文件,错误仍可能出现在终端。要把两个通道写到同一文件,可以使用 > combined.txt 2>&1。顺序有意义:
cat notes/lesson.txt notes/missing.txt > combined.txt 2>&1
cat combined.txt
cat notes/lesson.txt notes/missing.txt 2>&1 > only-output.txt
cat only-output.txt第一条命令先把描述符 1 指向文件,再让描述符 2 使用描述符 1 的当前去向,于是内容和错误都进入 combined.txt。第二种顺序先把描述符 2 指向原本的标准输出,也就是终端,之后才改变描述符 1;因此错误显示在终端,only-output.txt 中只有文件内容。
输入也可以来自文件。wc -l < notes/lesson.txt 把该文件接到标准输入,wc 统计其中的换行符;当前文件有三行,每行都以换行结束,因此结果为 3。
管道传递数据,退出状态决定后续动作
| 把左侧命令的标准输出接到右侧命令的标准输入:
grep -F 'status=' notes/lesson.txt | wc -l这里先筛出包含 status= 的行,再统计行数,结果为 1。标准错误没有自动进入这条管道。管道中的命令还可能并行运行,不必等左侧把全部输出生成完,右侧才开始读取。
Bash 默认使用最后一个命令的状态作为整条管道的状态。下面特意查询不存在的文件,并用独立的 Bash 运行两次,排除当前会话是否已经设置 pipefail 的影响:
bash --noprofile --norc -c 'set +o pipefail; grep -F "status=" notes/missing.txt | wc -l'
printf 'pipeline_exit=%s\n' "$?"
bash --noprofile --norc -c 'set -o pipefail; grep -F "status=" notes/missing.txt | wc -l'
printf 'pipeline_exit=%s\n' "$?"两次都会出现找不到文件的错误,wc 都会打印 0。未启用 pipefail 的管道返回 0,因为 wc 正常读完了空输入;启用后返回 2,来自失败的 grep。这里的“零行”是上游读取失败的结果,不能解释成成功查到零条记录。
pipefail 让管道返回最右侧非零命令的状态,并不会自动取消管道中的其他命令,也不会让整个脚本自动退出。&& 可以约束下一条命令只在前一条成功后执行,例如 mkdir new-notes && cp notes/lesson.txt new-notes/lesson.txt;|| 则用于前一条非零时执行备选动作。检查失败后应保留原始错误,不要用无条件输出“成功”掩盖它。
用户身份与访问权限
文件保存了内容,也保存了属主、属组和访问权限。一个进程能否读写它,要结合进程身份、文件权限以及到达文件所经过的目录判断。
用户、组与进程身份
在普通用户的 Bash 中执行:
id
getent passwd "$(id -un)"id 显示当前身份的 UID、主组 GID 和附加组。用户名便于人识别,UID、GID 是系统使用的数字标识;两台机器上同名账号可以对应不同 UID。将文件复制到另一台机器或挂载进容器时,需要核对数字映射,而不只是看名字是否相同。
getent passwd 按系统配置查询账号信息。一条记录通常由七个字段组成:
learner:x:UID:GID:备注:/home/learner:/bin/bash
│ │ │ │ │ │ └─ 登录 Shell
│ │ │ │ │ └─────────────── 家目录
│ │ │ │ └──────────────────── 描述信息
│ │ │ └──────────────────────── 主组标识
│ │ └──────────────────────────── 用户标识
│ └────────────────────────────── 密码字段占位,x 不是密码
└────────────────────────────────────── 用户名普通命令创建的子进程继承当前身份。文件访问通常使用与有效 UID/GID 一致的 filesystem UID/GID,并考虑附加组;特殊程序可以改变这些身份。因此,登录账号、环境变量 $USER 和某个服务实际使用的身份需要分别确认。credentials(7)解释了这些标识的关系。
id 不带用户名时观察当前进程身份;id learner 查询账号配置。管理员把账号加入新组后,已经运行的 Shell 和服务不会因此自动获得新附加组,通常需要重新登录或重启相应进程。遇到“已经加组,当前终端仍然无权限”,先比较这两种查询结果。
sudo -l 可以查看当前账号获准执行的管理操作。sudo 默认尝试以 root 身份执行命令,sudo -u 用户名 -- 命令 则指定另一身份;是否允许由 sudo 策略决定。root 常具有绕过传统文件权限检查的能力,但仍可能受到只读挂载、系统安全策略等限制。权限实验必须使用普通账号观察拒绝结果。sudo 手册说明了身份选择和授权行为。
读写执行位怎样参与判断
传统权限模式把访问者分为文件属主、文件所属组及其他人三类,每类都有 r、w、x 三个位。
-rw-r-----
│└┬┘└┬┘└┬┘
│ │ │ └─ other:无权限
│ │ └──── group:读取
│ └─────── owner:读取、写入
└────────── 普通文件
八进制写法:0640
r = 4,w = 2,x = 1;每一类分别组合在没有扩展 ACL、没有特权绕过的情况下,系统先判断是否为属主,再判断所属组是否匹配进程的主组或附加组,最后使用其他人权限。一旦选中某一类,就按该类决定允许或拒绝。例如属主只有读权限时,即使他同时属于一个具有写权限的文件组,也不会从组借到写权限。
文件和目录对同一个权限位的解释不同:
| 权限位 | 普通文件 | 目录 |
|---|---|---|
r | 读取内容 | 读取目录中的条目名称 |
w | 修改内容 | 修改目录条目,例如创建、删除、重命名;通常还需要目录的 x |
x | 允许尝试执行,仍需有效格式或解释器 | 按名字搜索目录、穿过该层访问下级路径 |
读取 /a/b/config,需要能够搜索 /、/a、/a/b,并有权读取最终文件。只有目录的 r 而没有 x,可能列出名称,却无法取得条目详情或打开其中的文件;反过来,只有 x 时,知道确切名称的进程仍可能访问文件,但不能枚举目录名称。路径解析规则给出了逐层检查过程。
删除文件名修改的是父目录,所以一个内容只读的文件仍可能被有权修改父目录的用户删除。sticky bit、只读文件系统和不可变标志等还会增加限制;/tmp 常设置 sticky bit,防止不同普通用户任意删除彼此的文件。rm 对只读文件的交互询问属于工具行为,不能代替内核的访问规则。相关条件见 unlink(2)。
chmod 改变权限位,chgrp 改变属组,chown 改变属主或属组。属主可以修改自己文件的模式,但更换属主通常需要管理权限;普通用户更换文件属组也受自身组成员身份限制。chmod 640 file 直接设置权限组合,chmod g-w file 只移除组写位,后者适合表达一次具体调整。
文件可读,父目录仍能阻止访问
在 Ubuntu 24.04 专用练习机上,准备一个不用于人工登录的账号 linuxlab-svc,让它读取配置文件。操作者仍是安装时创建、具有 sudo 管理权限的普通用户。
先检查同名账号和组:
getent passwd linuxlab-svc
getent group linuxlab-svc新练习机上,两条查询应都无记录;getent 查询不到指定记录时返回 2。已有条目就停止创建,核对它是否属于其他程序,不能接管现有账号。确认没有同名对象后执行:
sudo useradd --system --user-group --no-create-home \
--home-dir /nonexistent --shell /usr/sbin/nologin linuxlab-svc
id linuxlab-svc
sudo -u linuxlab-svc -- id--system 使用系统账号分配规则,--user-group 创建同名主组,--no-create-home 不创建家目录。UID/GID 以 id 实际输出为准,不需要手工固定为某个数字。useradd 手册列出了默认值及选项。
nologin 拒绝通过该登录 Shell 建立普通交互会话,账号仍能成为进程的运行身份。最后一条命令应显示 linuxlab-svc,这也是服务管理器能够指定该身份启动程序的基础。详见 nologin。
为权限实验另建一个临时目录。它放在 /tmp,避免把整个个人家目录开放给服务账号。以下步骤只适用于新建的普通练习目录,不改已有配置:
ACCESS_LAB=$(mktemp -d /tmp/linux-access.XXXXXX)
printf '%s\n' "$ACCESS_LAB"
mkdir "$ACCESS_LAB/config"
printf '%s\n' 'mode=practice' > "$ACCESS_LAB/config/app.conf"
sudo chgrp linuxlab-svc "$ACCESS_LAB" "$ACCESS_LAB/config" "$ACCESS_LAB/config/app.conf"
chmod 750 "$ACCESS_LAB" "$ACCESS_LAB/config"
chmod 640 "$ACCESS_LAB/config/app.conf"
sudo -u linuxlab-svc -- cat "$ACCESS_LAB/config/app.conf"应输出 mode=practice。配置文件仍由操作者拥有,服务账号通过属组取得读权限;两层目录的组权限 r-x 允许它到达文件,且没有开放给其他普通用户。
下面用 namei 查看路径每一层的权限。先执行 command -v namei;若没有输出,在 Ubuntu 练习机执行 sudo apt install util-linux,确认工具可用后再继续。现在只移除中间目录的组搜索权限:
chmod g-x "$ACCESS_LAB/config"
sudo -u linuxlab-svc -- cat "$ACCESS_LAB/config/app.conf"
printf 'read_exit=%s\n' "$?"
namei -l "$ACCESS_LAB/config/app.conf"读取应报告 Permission denied 并返回非零状态。namei -l 逐层显示路径的类型、权限和属主属组,可以看到 config 的组权限变为 r--,最终文件仍是 rw-r-----。这里缺的是到达文件的权限,给文件增加 r 无法修复。namei 手册说明了路径展开方式。
恢复刚才改动的位,再用同一身份读取:
chmod g+x "$ACCESS_LAB/config"
sudo -u linuxlab-svc -- cat "$ACCESS_LAB/config/app.conf"再次得到 mode=practice 后,保留目录用于下面的 ACL 练习。出现与预期不同的权限结果,先确认读取命令确实以 linuxlab-svc 执行,再查看 ACL,不要递归改成 777。
新文件的默认权限与 ACL
程序创建文件时会请求一组权限,umask 从中屏蔽部分位。没有父目录默认 ACL 时,结果可以写成 请求模式 & ~umask,这是按位运算。例如常规文件请求 0666、umask 为 0027 时得到 0640;目录请求 0777 时得到 0750。umask 不会为程序补上没有请求的执行位,也不改变已存在的文件。
在普通用户终端中,用独立子 Shell 观察,避免改变当前会话之后的创建习惯:
(
umask 027
printf '%s\n' 'private note' > "$ACCESS_LAB/umask.txt"
mkdir "$ACCESS_LAB/umask-dir"
stat -c '%a %n' "$ACCESS_LAB/umask.txt" "$ACCESS_LAB/umask-dir"
)在没有默认 ACL 的练习位置,新文件和目录应分别显示 640、750。括号中的 umask 只影响这个子 Shell 及它启动的程序。
ACL 可以给指定用户或组设置权限,突破“一个属主、一个属组、其他人”的表达范围。在 Ubuntu 练习机先用 command -v getfacl 和 command -v setfacl 检查,缺失时执行 sudo apt install acl。查看刚才的配置:
getfacl -p "$ACCESS_LAB/config/app.conf"无扩展 ACL 时主要是 user::rw-、group::r--、other::---。给 linuxlab-svc 增加一条命名用户权限,再设置可写的 mask:
setfacl -m u:linuxlab-svc:rw-,m::rw- "$ACCESS_LAB/config/app.conf"
getfacl -p "$ACCESS_LAB/config/app.conf"
sudo -u linuxlab-svc -- test -w "$ACCESS_LAB/config/app.conf"
printf 'writable_exit=%s\n' "$?"test -w 不修改文件,只检查当前身份是否可写,成功返回 0。ACL 中应出现 user:linuxlab-svc:rw- 和 mask::rw-。
mask 限制命名用户、文件所属组和命名组的有效权限,不限制文件属主与其他人。只缩小 mask 后再次检查:
setfacl -m m::r-- "$ACCESS_LAB/config/app.conf"
getfacl -p "$ACCESS_LAB/config/app.conf"
sudo -u linuxlab-svc -- test -w "$ACCESS_LAB/config/app.conf"
printf 'writable_exit=%s\n' "$?"写入检查应返回 1。命名用户条目仍保留 rw-,但旁边的 #effective:r-- 表明有效权限被 mask 限制。存在 mask 时,ls -l 中的组权限位显示的是 mask;后续执行 chmod g+w 可能重新放开这些保留的写权限,而非只影响文件的原属组。
使用 setfacl -m 时,若未显式指定 mask、也未使用 -n,工具会重新计算 mask。增加一条授权可能由此改变其他保留条目的有效权限,修改后需要再次读取 ACL。更完整的判断规则见 acl(5)及 setfacl。
目录还可以保存 default ACL,供以后创建的子对象继承。有 default ACL 时,创建流程改为“继承默认 ACL,再按程序请求的模式裁剪”,不再额外套用 umask。因此,前面的默认权限实验若结果不同,应查看父目录的 getfacl -p 输出中是否存在 default: 条目。两种创建分支见 umask(2)。
完成练习后,把这个新配置恢复为普通的组只读文件,并重新读取:
setfacl -x u:linuxlab-svc "$ACCESS_LAB/config/app.conf"
setfacl -b "$ACCESS_LAB/config/app.conf"
chmod 640 "$ACCESS_LAB/config/app.conf"
sudo -u linuxlab-svc -- cat "$ACCESS_LAB/config/app.conf"setfacl -b 在这里移除的是本次新建文件的扩展 ACL,不适用于随手清空现有共享目录的授权。移除命名条目后,linuxlab-svc 又通过所属组读取配置,仍应得到 mode=practice。
权限位之外的限制与管理写入
Ubuntu 常使用 AppArmor 对具体程序增加访问约束,Rocky Linux 常使用 SELinux。传统文件权限允许某次操作后,安全策略仍可能拒绝;需要同时确定目标进程是否受策略约束、处于什么模式,以及本次操作涉及的路径或安全标签。
在 Ubuntu 主机可用 sudo aa-status 查看已加载的 AppArmor profile 及模式,工具缺失时安装 apparmor-utils。在 Rocky 主机可用 getenforce、sestatus 查看 SELinux 状态,ls -Z 路径 查看标签。命令的位置和输出应在真正的主机环境观察,普通容器可能看不到完整策略信息。
AppArmor 的 complain 模式仍可能执行显式 deny 规则,拒绝也未必都有日志;SELinux 的 Enforcing 与 Permissive 同样不能混为一谈。排障应针对实际程序和被拒绝操作调整合法路径、标签或规则,保留失败及修复后的同身份验证。不要把关闭 AppArmor、SELinux 或删除策略当成通用修复。具体入口见 AppArmor 主机管理、AppArmor 模式说明及 Rocky SELinux 指南。
管理身份还要与 Shell 重定向区分。例如 sudo printf ... > /etc/某配置 中,打开目标文件的是外层普通 Shell;sudo 只作用于后面的命令。编辑受保护配置可使用获准的 sudoedit,从标准输入写入时可让 sudo tee 承担实际写入。写入前仍要明确目标和覆盖方式,不能为解决重定向问题就把整个日常会话切成 root。
这组权限文件不再需要时,在保存了 ACCESS_LAB 的同一个 Shell 中先执行 printf '%s\n' "$ACCESS_LAB" 和 find "$ACCESS_LAB" -maxdepth 2 -type f,核对它确实是本次打印出的 linux-access.随机后缀 目录,且只包含本轮文件。然后删除明确目标:
rm -i "$ACCESS_LAB/config/app.conf" "$ACCESS_LAB/umask.txt"
rmdir "$ACCESS_LAB/config" "$ACCESS_LAB/umask-dir"
rmdir "$ACCESS_LAB"非空目录会被 rmdir 保留;此时查看残留对象,不改用递归删除。linuxlab-svc 账号保留给后面的服务练习,尚未启动任何常驻服务。
软件包、命令与运行环境
发行版把程序、库、配置和安装信息组织成软件包。安装一个工具时,包管理器还会解析依赖关系、检查已有版本,并可能执行安装脚本或启动服务。即使目标只是一个命令,也应先看清安装计划。
仓库索引与已安装软件
Ubuntu 使用 .deb 软件包。dpkg 管理本机软件包及其安装状态;APT 读取软件源和包索引,选择版本、取得依赖,再交给底层工具完成安装。三个位置保存着不同信息:
软件源配置 去哪些仓库取得哪个发行版、哪些组件
↓ 更新索引
本机 APT 包索引 仓库目前提供哪些包、版本和依赖
↓ 解析并安装
dpkg 安装数据库 本机已经安装或配置过哪些包刚修改软件源,或者隔了较长时间没有更新索引时,APT 的本地记录可能已经落后于仓库。反过来,更新索引只改变可供选择的信息,已有程序要经过安装或升级操作才会改变。
在 Ubuntu 24.04 的普通用户终端中查看架构与软件源文件:
dpkg --print-architecture
ls -l /etc/apt/sources.list /etc/apt/sources.list.d架构常见值为 amd64 或 arm64,与软件包是否适配本机有关。新安装的 Ubuntu 24.04 通常使用 /etc/apt/sources.list.d/ubuntu.sources;传统的 /etc/apt/sources.list 可能不存在,这种单一路径的缺失无需修复。以上一条列表中的实际文件为准,再用 less 阅读,不覆盖现有配置。
.sources 使用 deb822 格式,主要字段如下:
| 字段 | 含义 |
|---|---|
Types | deb 表示二进制包,deb-src 表示源码包 |
URIs | 仓库服务地址 |
Suites | 发行版与更新渠道,例如 noble、noble-updates |
Components | 仓库组件,例如 main、universe |
Signed-By | 为这个源限定可用的签名密钥 |
不要把其他发行版代号直接写进当前系统,试图取得一个较新的包;解析成功的依赖也可能改变系统中大量基础组件。源格式与密钥位置见 sources.list(5)。
APT 验证仓库签名及索引、包文件之间的摘要关系,以检查下载内容是否来自受信任的仓库并保持完整。接受第三方仓库密钥,意味着信任它提供的软件;签名本身不审查包内程序和安装脚本是否安全。应从供应商官方入口确认发行版、架构、仓库和密钥,按源限定信任范围,不通过关闭验证来消除签名错误。apt-secure(8)说明了这条验证链。
安装 tree 并确认程序来自哪个包
tree 可以按层次显示目录。下面继续使用 Ubuntu 24.04 专用练习机:查询由普通用户执行,更新索引和安装需要该用户获准使用 sudo。
先查看它是否已安装,保留结果,后面卸载时要用到:
dpkg-query -W -f='${binary:Package}\t${Version}\t${Status}\n' tree出现 install ok installed 表示已经安装;没有匹配记录时会报告找不到包并返回非零状态。数据库中也可能只剩配置文件记录,因此不能只凭出现包名就认定安装完成。
更新索引并查看候选版本:
sudo apt-get --error-on=any update
apt-cache policy tree
sudo apt-get --simulate install tree第一条命令成功后再执行后两条。--error-on=any 要求索引刷新遇到任何错误时失败,包括暂时性错误,避免在部分索引仍然陈旧时继续推断仓库情况。临时重试后最终成功,与持续失败的结果不同,应看命令最终诊断和退出状态。
apt-cache policy 中,Installed 是当前安装版本,Candidate 是按本机源和优先级选出的候选版本;版本表还列出来源。候选版本随仓库更新而变化,应以本机输出为准。若 Candidate 为 (none),先确认索引刷新成功、仓库组件启用且提供本机架构的包。查询方式见 apt-cache 手册。
--simulate 只打印计划,不执行安装。检查它准备新增、升级或移除哪些包;一个小工具引出意外的大范围变更时,先核对软件源和依赖。模拟使用当前索引及安装状态,尚未验证后续下载、磁盘写入和安装脚本,也不会锁住包数据库阻止其他管理进程改变状态。apt-get 手册给出了刷新和模拟选项。
计划合理后安装。已有旧版 tree 时,这一步也可能升级它;如果出现确认提示,核对实际计划后再选择继续:
sudo apt install tree
tree --version
dpkg-query -W -f='${binary:Package}\t${Version}\t${Architecture}\t${Status}\n' tree
dpkg-query -L tree
dpkg-query -S /usr/bin/treetree --version 应正常显示工具版本,安装状态应为 install ok installed。-L 列出该包登记的文件,-S 查询谁登记了指定路径;Ubuntu 的 tree 包会登记 /usr/bin/tree。安装脚本后来生成的文件,以及软件运行产生的数据,不一定出现在 -L 的清单中。
这几种查询适合回答“版本是什么”“文件放在哪”“这个路径归哪个包管理”。包数据库的归属记录与磁盘文件是否被手工替换,还需要分别检查。dpkg-query 手册列出了查询字段和限制。
命令名怎样变成一次执行
在刚才的终端中查询:
type -a cd printf tree
command -v tree
printf '%s\n' "$PATH"cd 是 Shell 内建命令,它必须改变当前 Shell 的工作目录。printf 既可能由 Bash 内建实现,也有外部程序版本;tree 通常解析到 /usr/bin/tree。type -a 能列出别名、函数、内建命令或外部文件等候选,适合解释同一个命令名为何在不同终端表现不同。
对于需要查找的外部命令,Bash 按 PATH 中的目录顺序寻找;PATH 是用冒号分隔的目录列表。显式路径 ./程序 或 /某目录/程序 不依靠 PATH 查找。当前目录只有被写入 PATH 才会参与这类搜索,空的 PATH 分量也表示当前目录,日常环境不宜这样配置。
给一个新脚本设置执行位,可以同时观察文件格式和路径查找。普通用户在家目录下创建独立练习位置,所有后续命令都使用本次变量:
RUN_LAB=$(mktemp -d "$HOME/linux-run.XXXXXX")
printf '%s\n' "$RUN_LAB"
mkdir "$RUN_LAB/bin" "$RUN_LAB/config" "$RUN_LAB/elsewhere"
printf '%s\n' 'source=practice' > "$RUN_LAB/config/mode.conf"
nano "$RUN_LAB/bin/show-context"在 nano 中写入完整内容,使用前面介绍的 Ctrl+O、回车保存,再用 Ctrl+X 退出:
#!/bin/bash
printf 'mode=%s\n' "${APP_MODE:-unset}"
printf 'cwd=%s\n' "$PWD"
cat config/mode.conf第一行指定执行脚本的解释器;${APP_MODE:-unset} 在变量未设置或为空时显示 unset。这里故意保留一个相对配置路径,下一节会观察它如何解析。
先只保留属主读写权限,再尝试直接执行:
chmod 600 "$RUN_LAB/bin/show-context"
(
cd "$RUN_LAB" || exit
./bin/show-context
)
printf 'execute_exit=%s\n' "$?"应出现 Permission denied,在 Bash 中这次执行返回 126。文件虽然可读,直接执行还缺少执行位。保持权限不变,改为由 Bash 读取它:
(
cd "$RUN_LAB" || exit
APP_MODE=interpreted bash ./bin/show-context
)此时应依次输出 mode=interpreted、实际工作目录,以及 source=practice。执行的是 Bash 程序,脚本只是它读取的文件,因此这条路径不要求脚本自身具有执行位。bash 脚本 还会明确使用 Bash,不能用它替代对其他语言解释器的选择。
给脚本补上属主执行位,再通过 PATH 查找:
chmod u+x "$RUN_LAB/bin/show-context"
(
cd "$RUN_LAB" || exit
PATH="$RUN_LAB/bin:$PATH" APP_MODE=direct show-context
)
tree -L 2 "$RUN_LAB"应看到 mode=direct 和配置内容;tree 会列出 bin/show-context、config/mode.conf 与空目录 elsewhere。这次 PATH 赋值只用于该命令,不会永久改写登录环境。
Bash 会缓存部分外部命令的查找结果。安装、移走程序或调整位置后,如果仍命中旧路径,可执行 hash -r 清除当前 Shell 的命令缓存,再用 type -a 核对。命令查找、赋值和启动文件规则见 Bash 手册。
环境变量和工作目录分别影响什么
Shell 变量用于当前 Shell 的计算和展开;导出的变量才进入后续子进程的环境。下面在子 Shell 中实验,先清除同名变量,结束后不改变原终端的 APP_MODE:
(
cd "$RUN_LAB" || exit
unset APP_MODE
APP_MODE=local
./bin/show-context
export APP_MODE
./bin/show-context
APP_MODE=once ./bin/show-context
printf 'shell_mode=%s\n' "$APP_MODE"
)三次脚本中的 mode 依次是 unset、local、once,最后当前 Shell 仍输出 shell_mode=local。第一次变量尚未导出,第二次进入子进程环境,第三次只为这一次外部命令覆盖值。修改当前 Shell 的变量,不会回头改变已经运行的服务进程。
工作目录是另一项进程属性,程序通常从父进程继承它。刚才脚本中的 config/mode.conf 相对于工作目录解析,而不是相对于脚本所在目录。换到专门创建的空目录后,用脚本的绝对路径执行:
(
cd "$RUN_LAB/elsewhere" || exit
APP_MODE=practice "$RUN_LAB/bin/show-context"
)
printf 'run_exit=%s\n' "$?"脚本可以启动,前两行也能输出,但 cat 会报告找不到 config/mode.conf。此时它寻找的是 elsewhere/config/mode.conf,最后一条 cat 的非零状态也成为脚本状态。回到配置所在的工作目录即可恢复:
(
cd "$RUN_LAB" || exit
APP_MODE=practice "$RUN_LAB/bin/show-context"
)应重新得到 source=practice。实际服务可以使用明确的配置绝对路径,也可以由服务配置规定工作目录;仅把启动命令写成绝对路径,仍不会改变程序内部相对文件名的含义。
交互 Shell、登录 Shell、定时任务和系统服务的启动方式不同,读取的配置也不同。Bash 的交互非登录会话通常读取 ~/.bashrc;登录会话按规则读取 profile 文件。systemd 直接执行程序时不会自动启动交互 Bash 读取这些文件。把变量追加到个人 .bashrc,只会影响按相应方式启动、并实际读取它的会话;服务环境应在服务配置中明确提供。也不要通过打印整份环境来排查含口令或令牌的服务,只查询需要的变量。
安装与启动失败时检查哪些对象
APT 操作失败时先保留完整错误,区分连接、索引、签名、依赖和本机配置阶段。前面已有基础网络检查;不同阶段的后续动作不能混用:
| 现象 | 先查什么 | 后续处理 |
|---|---|---|
| 仓库名称解析失败、连接超时 | 错误指向的实际仓库、网络和允许使用的代理 | 恢复访问后重新刷新索引;APT 的代理配置与其他工具分别核对 |
| 签名、密钥或 Release 信息错误 | 系统时间、发行版代号、源地址及官方密钥变更说明 | 确认原因后更正对应配置,不关闭验证 |
| 包无法定位、没有安装候选 | 刷新是否完整成功、组件、架构、源是否支持本发行版 | 使用受支持的软件源或兼容的分发方式 |
| 锁被其他进程持有 | 是否有软件更新或另一个包管理操作正在运行 | 等待正常完成;不删除锁文件制造并发写入 |
在企业网络中,镜像站或内部仓库仍需保留可信签名和适配的发行版、架构。代理凭据应使用获准的本机配置方式管理,不能粘贴进公开终端输出。apt download 包名 只下载指定包,依赖是否齐全还取决于目标系统已有状态;完整离线安装需要准备并校验依赖集合,不能用一个 .deb 文件概括。
程序已在磁盘上时,再按“命令解析到了哪里 → 是否可访问和执行 → 解释器或加载器能否启动 → 配置从哪里读取”检查。例如本次脚本可以用 type -a show-context、ls -l "$RUN_LAB/bin/show-context"、namei -l "$RUN_LAB/bin/show-context" 检查入口与权限。前面的 PATH 只临时生效,因此在原终端中查不到 show-context 是正常结果,应改用保存的完整路径。
file 路径 可以辅助识别文件类型,Ubuntu 中缺少该工具时安装 file 包。对于脚本再检查第一行解释器路径;对于二进制程序核对 CPU 架构与动态加载器。目标文件存在却报告 No such file or directory,可能缺的是解释器或加载器,而非目标文件本身。执行位、挂载的 noexec、安全策略和文件格式都可能影响直接执行;错误条件见 execve(2)。
更新、移除与练习清理
日常管理中几个相近命令会改变不同对象:
| 操作 | 作用及需要检查的内容 |
|---|---|
apt update | 更新包索引,检查最后是否有失败的源 |
apt upgrade | 升级已安装包,可安装新依赖,但不移除已有包;需要移除包的升级可能被保留 |
apt full-upgrade | 为完成升级允许移除已安装包,必须逐项看计划 |
apt remove 包名 | 移除软件包,通常保留其受管理的配置文件 |
apt purge 包名 | 进一步清除该包受管理的配置;用户目录中的文件仍需单独判断 |
apt autoremove | 移除被标为自动安装、如今不再被需要的包,执行前核对完整列表 |
升级可能运行维护脚本、重启服务,内核或基础库变更还可能要求安排系统重启。生产主机应先确认维护窗口和恢复办法,再执行已审核的计划。交互操作适合使用 apt;需要稳定脚本接口时使用 apt-get、apt-cache 等专用接口,并按各命令含义处理状态。详见 apt(8)。
如果 tree 是本轮首次安装、且只用于练习,可以演练移除;开始前已经存在的工具继续保留:
sudo apt-get --simulate remove tree
sudo apt remove tree
hash -r
command -v tree
dpkg-query -W -f='${binary:Package}\t${Status}\n' tree先检查模拟计划,再确认实际移除。若没有另外安装同名程序,command -v 应无结果并返回非零;包查询应不再显示 install ok installed。若命令仍存在,用 type -a tree 查看它是否来自另一安装位置。需要恢复时执行 sudo apt install tree,再用 tree --version 和包状态查询确认。
Rocky Linux 9 使用 RPM 软件包及 DNF。相应任务可以从 dnf info、dnf install、dnf upgrade、dnf remove,以及 rpm -q、rpm -ql、rpm -qf 入手;APT 的发行版代号、源文件和选项不能直接搬过去。软件源与操作入口见 Rocky 软件管理指南。
脚本练习结束后,在保存了 RUN_LAB 的终端打印 printf '%s\n' "$RUN_LAB",再用 find "$RUN_LAB" -maxdepth 2 -type f 核对路径与文件。确认是本次新建位置后删除:
rm -i "$RUN_LAB/bin/show-context" "$RUN_LAB/config/mode.conf"
rmdir "$RUN_LAB/bin" "$RUN_LAB/config" "$RUN_LAB/elsewhere"
rmdir "$RUN_LAB"文件删除需要逐项确认;目录非空时保留并检查残留,不扩大删除范围。
进程与系统服务
程序文件保存指令,进程则是这些指令的一次运行。相同程序可以同时运行多次,各有 PID、内存和打开的文件;一个进程也可以包含多个线程,共享进程资源,由内核分别调度。
PID、前后台与信号
在普通用户的交互 Bash 中创建一个只等待、不读写业务文件的进程:
sleep 300 &
LAB_PID=$!
printf 'pid=%s\n' "$LAB_PID"
ps -p "$LAB_PID" -o pid,ppid,user,stat,etime,args
jobs -l& 让命令在后台运行,Shell 随即接受下一条输入;$! 是最近启动的后台任务 PID。ps 中的 PPID 表示父进程,USER 表示运行身份,STAT 是采样时的进程状态,ETIME 是已运行时长,ARGS 显示启动参数。这里应能找到自己创建的 sleep 300。PID 以实际输出为准,进程结束后这个数字可以被系统重新分配。
jobs 只列当前 Shell 管理的作业。一个包含管道的作业可能包含多个进程,作业号与 PID 是两套标识。前台作业通常能够从终端读取输入,并接收终端产生的信号;后台运行没有改变用户权限,也没有自动获得服务重启能力。
常见状态中,R 表示正在运行或等待 CPU,S 表示可中断睡眠,D 表示不可中断等待,T 表示停止,Z 表示进程已经退出但父进程尚未回收退出信息。进程在等待输入或定时器时进入 S 很正常;D 常与内核中的 IO 等待有关,不能只凭这个字母确定是哪块磁盘。Z 已经没有可继续执行的业务代码,处理重点是父进程的回收行为。字段见 ps 手册。
确认刚才的进程仍然存在且命令为自己的 sleep 300 后,可以暂停、恢复并结束它。不要对从旧日志抄来的 PID 发送信号:
kill -STOP "$LAB_PID"
jobs -l
kill -CONT "$LAB_PID"
kill -TERM "$LAB_PID"
wait "$LAB_PID"
printf 'wait_exit=%s\n' "$?"暂停后作业应显示 Stopped;CONT 使它恢复,TERM 请求终止。该 sleep 按 TERM 退出时,Bash 的 wait 返回 143,即 128 + 15。wait 在这里等待当前 Shell 的子进程,并取得退出状态;它不能等待任意其他终端的进程。
Ctrl+C 通常向前台进程组发送 INT,Ctrl+Z 通常发送 TSTP;暂停的作业可按 jobs 显示的实际作业号使用 fg %作业号 或 bg %作业号 恢复。TERM、INT 可以由程序捕获并安排清理,KILL、STOP 不能被捕获或忽略。KILL 会跳过应用的正常退出处理,而且仍无法让陷在内核不可中断等待中的任务立即完成退出,因此不作为第一步。signal(7)列出了信号及默认动作。
把程序放到后台,解决的是当前终端是否等待它;nohup 主要改变对挂断信号的处理,输出去向还要另看。需要固定身份、自动重启、统一日志和开机启动时,应交给服务管理器管理。
系统启动与 unit
一台普通服务器的启动可以按交接对象理解:
固件
→ 引导程序或固件直接加载内核
→ 内核与早期用户空间 initramfs:准备驱动、寻找根文件系统
→ 切换到真正的根文件系统
→ PID 1 系统管理器
→ 按依赖关系启动文件系统、网络及应用服务具体引导器、磁盘加密和 initramfs 的内容随机器而变。Ubuntu 24.04 的系统管理器是 systemd。它使用 unit 描述受管理对象:service 管理服务进程,socket 管理激活用的 socket,mount 管理挂载,timer 提供触发器,target 把一组 unit 组织在一起。unit 可以并行启动,也可以带有启动顺序和需求依赖,并非每台机器都经历同一条服务列表。systemd 启动过程给出了更完整的关系。
先在练习虚拟机检查:
ps -p 1 -o pid,comm,args
systemctl --version
systemctl get-defaultPID 1 应为 systemd,默认 target 常为 multi-user.target。普通容器的 PID 1 可能只是应用或 Shell;在这种环境中出现“未使用 systemd 启动”时,需要返回真正的练习主机,不能通过增加 sudo 解决。
系统软件包通常在 /usr/lib/systemd/system 提供 unit,本机管理员在 /etc/systemd/system 创建或覆盖配置;部分系统的 /lib 与 /usr/lib 通过目录链接兼容。systemctl cat 名称 展示主文件及 drop-in,systemctl show 则适合查询已加载的属性。不要直接修改软件包提供的文件,以免后续升级覆盖改动。
准备一个可独立运行的 HTTP 程序
下面在 Ubuntu 24.04 专用虚拟机使用 OpenJDK 21。它只需要 JDK 自带的 HTTP Server API,不需要 Maven、Spring 或数据库。普通用户先查看候选包,再安装编译和访问工具:
sudo apt-get --error-on=any update
apt-cache policy openjdk-21-jdk-headless
sudo apt install openjdk-21-jdk-headless curl
java -version
javac -version
readlink -f /usr/bin/java
readlink -f /usr/bin/javac在新练习机中,java 与 javac 应来自 OpenJDK 21。已有多个 JDK 时,安装不一定改变当前默认版本;两条 readlink 会显示 alternatives 最终指向的位置,应先核对再编译。包的架构及依赖见 Ubuntu OpenJDK 21 软件包。
为源码新建普通用户可写的目录:
SERVICE_BUILD=$(mktemp -d "$HOME/linux-build.XXXXXX")
printf '%s\n' "$SERVICE_BUILD"
mkdir "$SERVICE_BUILD/classes"
nano "$SERVICE_BUILD/LinuxHello.java"保存以下完整源码:
import com.sun.net.httpserver.HttpServer;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.Properties;
import java.util.concurrent.Executors;
public class LinuxHello {
public static void main(String[] args) throws Exception {
if (args.length != 1) {
throw new IllegalArgumentException("usage: LinuxHello /path/to/app.properties");
}
var config = new Properties();
try (var reader = Files.newBufferedReader(Path.of(args[0]), StandardCharsets.UTF_8)) {
config.load(reader);
}
String bind = config.getProperty("bind", "127.0.0.1");
int port = Integer.parseInt(config.getProperty("port", "18080"));
String message = config.getProperty("message", "hello linux");
var server = HttpServer.create(new InetSocketAddress(InetAddress.getByName(bind), port), 0);
var executor = Executors.newFixedThreadPool(2);
server.setExecutor(executor);
server.createContext("/", exchange -> {
try {
String method = exchange.getRequestMethod();
String path = exchange.getRequestURI().getPath();
int status = !method.equals("GET") ? 405
: path.equals("/") || path.equals("/hello") ? 200 : 404;
String body = status == 200 ? message + "\n" : "request rejected\n";
if (status == 200) {
Files.writeString(Path.of("requests.log"), method + " " + path + "\n",
StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=utf-8");
if (status == 405) exchange.getResponseHeaders().set("Allow", "GET");
exchange.sendResponseHeaders(status, bytes.length);
exchange.getResponseBody().write(bytes);
System.out.println(method + " " + path + " status=" + status);
} catch (java.io.IOException error) {
System.err.println("request failed: " + error);
} finally {
exchange.close();
}
});
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
server.stop(1);
executor.shutdown();
System.out.println("stopped");
}));
server.start();
System.out.println("listening=" + bind + ":" + port);
System.out.println("user=" + System.getProperty("user.name") + " cwd=" + Path.of("").toAbsolutePath());
}
}程序启动时读取一个 properties 文件,再创建监听 socket。GET / 或 /hello 返回 message,成功请求向当前工作目录的 requests.log 追加一行;其他路径返回 404,其他方法返回 405。控制台输出记录启动身份、工作目录和请求结果,停止钩子关闭 HTTP Server 与线程池。这里使用两个工作线程处理少量实验请求,不把它作为生产服务器容量方案。JDK HttpServer API说明了绑定、执行器与停止行为。
编译不需要管理权限:
javac --add-modules jdk.httpserver -d "$SERVICE_BUILD/classes" "$SERVICE_BUILD/LinuxHello.java"
ls -l "$SERVICE_BUILD/classes/LinuxHello.class"应生成 class 文件。编译失败就停在此处,按错误行核对完整源码和 JDK,不把空目录继续安装到系统位置。
继续使用权限章节创建的 linuxlab-svc,确认 id linuxlab-svc 能找到该账号。以下目录专属于本次练习;先用 sudo ls -ld /opt/linux-lab /etc/linux-lab /var/lib/linuxlab /etc/systemd/system/linuxlab.service 检查它们均未被占用。若已有对象,先确定来源,不覆盖现有服务。
sudo install -d -o root -g root -m 755 /opt/linux-lab /opt/linux-lab/app
sudo install -m 644 "$SERVICE_BUILD/classes/LinuxHello.class" /opt/linux-lab/app/LinuxHello.class
sudo install -d -o root -g linuxlab-svc -m 750 /etc/linux-lab
sudo install -d -o linuxlab-svc -g linuxlab-svc -m 750 /var/lib/linuxlab
nano "$SERVICE_BUILD/app.properties"配置内容为:
bind=127.0.0.1
port=18080
message=hello linux保存后安装配置,并检查服务身份能读取:
sudo install -o root -g linuxlab-svc -m 640 "$SERVICE_BUILD/app.properties" /etc/linux-lab/app.properties
sudo -u linuxlab-svc -- cat /etc/linux-lab/app.properties代码和配置由管理员持有,服务只能读取;运行产生的文件放在服务可写的 /var/lib/linuxlab。这样即使程序需要写日志,也无须获得修改 class 或启动配置的权限。install 的目录、属主和模式选项在 install 手册中说明。
先手动启动,再交给服务管理器
在第一个虚拟机终端以前面的服务身份运行。此处明确使用 shell 只是为了先切换工作目录,再用 exec 将它替换成 Java 进程:
sudo -u linuxlab-svc -- /bin/sh -c 'cd /var/lib/linuxlab && exec /usr/bin/java --add-modules jdk.httpserver -cp /opt/linux-lab/app LinuxHello /etc/linux-lab/app.properties'应看到 listening=127.0.0.1:18080,随后显示服务用户名和 /var/lib/linuxlab。命令保持前台运行。在第二个虚拟机终端执行:
curl -q --noproxy '*' --fail-with-body http://127.0.0.1:18080/hello
sudo cat /var/lib/linuxlab/requests.log响应应为 hello linux,文件增加 GET /hello。返回成功需要监听、配置读取及数据目录写入共同成立。requests.log 在这里是少量练习记录,不是带持久性承诺的业务日志。
若连接被拒绝,先回第一个终端读启动错误:配置路径或权限错误会发生在监听前,Address already in use 表示地址端口已被占用。可用 sudo ss -lntp 'sport = :18080' 查询占用者;不要杀掉来源未知的进程。若已连接却收不到完整响应,查看控制台的 request failed 及异常路径;例如 AccessDeniedException: requests.log 指向记录文件写入被拒绝,应检查数据文件、/var/lib/linuxlab 及完整路径,而不是改动代码目录权限。
确认请求成功后,在第一个终端按 Ctrl+C,应看到 stopped 并返回提示符。再次执行 curl 会连接失败,ss 不再显示本次监听,然后才安装 unit,避免手动进程和服务争抢端口。
用 sudoedit /etc/systemd/system/linuxlab.service 写入:
[Unit]
Description=Linux practice HTTP service
[Service]
Type=exec
User=linuxlab-svc
Group=linuxlab-svc
WorkingDirectory=/var/lib/linuxlab
ExecStart=/usr/bin/java --add-modules jdk.httpserver -cp /opt/linux-lab/app LinuxHello /etc/linux-lab/app.properties
Restart=on-failure
SuccessExitStatus=143
RestartSec=3s
TimeoutStopSec=10s
UMask=0027
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/linuxlab
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.targetsudoedit 根据本机编辑器设置打开配置,选择 nano 时仍按前面的快捷键保存退出。unit 各字段组合出完整运行条件:User/Group 决定身份,WorkingDirectory 决定相对文件位置,ExecStart 给出程序和参数。ExecStart 默认不经 Shell 解释,不能直接往里面写管道、重定向或 &&,也不读取个人 .bashrc。
Type=exec 等到执行程序这一步成功后才认为启动命令成功;Java 后续读取配置、建立监听仍可能失败。Type=simple 的确认点更早;Type=notify 则要求应用按协议通知就绪。HTTP 响应还要单独检查,不能用 active 代替。
Restart=on-failure 对异常退出安排重启;通过 systemctl stop 停止服务时不因此重新拉起。这个 Java 程序收到 SIGTERM 后执行关闭钩子,JVM 可能以 143 退出,SuccessExitStatus=143 将这种预期退出也记为成功。其他程序应按各自的退出约定配置。TimeoutStopSec 给停止过程留出时间,超时后服务管理器可能强制结束剩余进程。应用是否完成业务清理取决于自身退出逻辑。类型、重启和停止条件见 systemd.service。
这组隔离选项限制服务能够修改的系统位置:ProtectSystem=strict 提供只读文件系统视图,ReadWritePaths 为数据目录保留写入;该例同时依靠真实目录的属主权限授权写入。PrivateTmp 给服务单独的临时目录视图,ProtectHome 隔离个人目录,NoNewPrivileges 阻止后续执行取得新的权限。它们会改变服务看到的环境,调试时应同时查看 unit 和路径,不能只在登录终端测试一次。详见 systemd.exec。
保存后检查配置并启动:
sudo systemd-analyze verify /etc/systemd/system/linuxlab.service
sudo systemctl daemon-reload
sudo systemctl start linuxlab.service
systemctl status linuxlab.service --no-pager -l
sudo journalctl -u linuxlab.service -b -n 20 --no-pager -o cat
curl -q --noproxy '*' --fail-with-body http://127.0.0.1:18080/helloverify 能发现部分语法及可执行文件问题,但不会代替实际启动。daemon-reload 让管理器重新加载 unit;start 启动当前服务。看到日志中的 listening 后请求应返回 hello linux。如果 curl 早于程序就绪,先观察日志再重试;持续失败则按错误处理,不把重复请求当成修复。
systemctl show linuxlab.service -p MainPID -p User -p WorkingDirectory -p ActiveState -p SubState应能看到非零 MainPID、服务用户、指定目录,以及 active、running。管理器通过 unit 及其 cgroup 管理进程,不只是记住一个可随时复用的 PID。
启动、开机启动和配置重新加载
几个常用操作处理的状态不同:
| 命令 | 改变什么 |
|---|---|
systemctl start / stop | 当前运行状态 |
systemctl restart | 停止旧进程,再启动新进程 |
systemctl reload | 请求服务按自身支持的方法重新加载配置,服务须提供相应能力 |
systemctl daemon-reload | 管理器重新读取 unit 文件,不替应用重读配置 |
systemctl enable / disable | 安装或移除用于激活的链接,单独执行不必改变当前运行状态 |
此 Java 程序只在启动时读取配置,没有 reload 实现。修改 properties 后使用 restart;修改 unit 后先 daemon-reload,再按需要 restart。仅重读 unit 不会改变已经运行进程的目录或参数。systemctl 手册解释了各操作。
希望练习机开机后运行这个服务时执行:
sudo systemctl enable linuxlab.service
systemctl is-enabled linuxlab.service应显示 enabled。WantedBy=multi-user.target 指定 enable 要建立的激活关系,不能把所有 enabled unit 都理解成必然成功运行的进程。只有在专用虚拟机控制台确认当前机器为 linux-lab、没有其他重要任务时,才执行 sudo reboot;不要在宿主终端或生产机照做。
重新登录这台虚拟机后,重复 status、journalctl -u linuxlab.service -b 和本机 curl。-b 限定当前启动周期,MainPID 应以这次运行实际值为准。重新启动操作本身成功,仍可能留下一个启动失败的服务,所以最终继续检查响应。
复现一次准备阶段失败并恢复
在这个新服务上临时设置一个不存在的工作目录。先确认 /var/lib/linuxlab-missing 不存在,并确认下面的 drop-in 文件名未使用;不修改其他服务的配置。
sudo install -d -m 755 /etc/systemd/system/linuxlab.service.d
sudoedit /etc/systemd/system/linuxlab.service.d/90-practice-failure.conf写入:
[Service]
WorkingDirectory=/var/lib/linuxlab-missing
Restart=noRestart=no 让这次失败停留在可观察状态,不持续重试。随后执行:
sudo systemctl daemon-reload
sudo systemctl restart linuxlab.service
systemctl status linuxlab.service --no-pager -l
sudo journalctl -u linuxlab.service -b -n 20 --no-pager -o catUbuntu 的 systemd 会报告 200/CHDIR,进程准备在切换工作目录时失败,Java 尚未执行。相比之下,203/EXEC 指向执行程序阶段,217/USER 指向身份准备;如果是 Java 启动后读取配置失败,则通常能看到应用异常及它自己的退出状态。准备阶段状态定义见前面的 systemd.exec 手册。
读取 systemctl cat linuxlab.service,确认失败来自刚添加的文件,再移除这个唯一 drop-in:
sudo rm -i /etc/systemd/system/linuxlab.service.d/90-practice-failure.conf
sudo systemctl daemon-reload
sudo systemctl reset-failed linuxlab.service
sudo systemctl start linuxlab.service
sudo journalctl -u linuxlab.service -b -n 12 --no-pager -o cat
curl -q --noproxy '*' --fail-with-body http://127.0.0.1:18080/hello应恢复为正常响应。reset-failed 清除失败状态及相关启动限速计数,本身不会修复文件内容;必须先纠正配置。保留正常服务,后面的资源、网络和日志查询会继续使用它。
文件系统与存储空间
从设备到目录中的文件
文件路径位于目录树中,文件内容则由路径所在的文件系统管理。磁盘、分区、文件系统和挂载点处在不同层次。一个简单的虚拟机布局可以表示为:
虚拟磁盘:/dev/vda
├─ 分区:/dev/vda1
│ └─ 文件系统:ext4
│ └─ 挂载到 /,提供根目录及其中的文件
└─ 分区:/dev/vda2
└─ 文件系统:ext4
└─ 挂载到 /srv/data,提供该路径下的数据这里的设备名仅表示一种布局。实际机器还可能使用 /dev/sda、/dev/nvme0n1,或者在磁盘与文件系统之间加入 LVM 逻辑卷、加密映射及 RAID。分区划分设备上的区域,文件系统管理文件名、文件内容和可分配空间;挂载则把一个文件系统接到当前可见目录树的某个位置。设备可以直接承载文件系统,也可以有多层映射,不能仅靠名字中的数字判断用途。
在 Ubuntu 24.04 练习机的普通用户终端执行以下只读查询;lsblk、findmnt 属于 util-linux,df 属于 coreutils。精简环境缺少工具时,先按软件包章节的方法安装对应包。
lsblk -o NAME,TYPE,SIZE,FSTYPE,UUID,MOUNTPOINTS
findmnt -T / -o TARGET,SOURCE,FSTYPE,OPTIONS
findmnt -T /var -o TARGET,SOURCE,FSTYPE,OPTIONS
df -hT /var先在 lsblk 中看 TYPE:disk 表示磁盘,part 表示分区,lvm 等类型提示还有映射层。SIZE 是设备容量,FSTYPE 是识别出的文件系统类型,MOUNTPOINTS 显示相关挂载位置。空的 FSTYPE 不能直接解释成“空盘可以格式化”,它也可能是未识别、未取得信息或承载其他数据的设备。lsblk 手册说明了这些字段的来源。
findmnt -T /var 查找覆盖 /var 路径的挂载。如果结果的 TARGET 是 /,说明 /var 当前使用根文件系统;结果为 /var,才表示该路径本身是一个挂载点。df -hT /var 随后报告这个文件系统的容量,而非 /var 目录里文件的总大小。查询规则见 findmnt。
ext4、XFS 是常见的本地文件系统。它们的日志机制帮助文件系统在非正常中断后恢复结构一致性,业务文件的误删与覆盖仍需要备份处理;两者的实现可分别查 ext4 journal和 XFS logging。NFS 等网络文件系统把存储交给远端;tmpfs 使用内存以及允许使用的交换空间;/proc、/sys 主要呈现内核信息。这些对象都能接入目录树,却没有相同的容量、持久性和故障方式。
挂载视图还属于进程的运行环境。普通容器可以拥有自己的挂载视图,systemd 的 PrivateTmp=true 也会让服务看到独立的临时目录。前面 linuxlab.service 使用了这个设置,所以不能在登录终端的 /tmp 中放一个文件,就假定服务会在同一路径看到它。
临时挂载与开机挂载配置
一次 mount 操作改变当前的挂载关系;要在启动时重建它,还需要对应的持久配置。Ubuntu 常从 /etc/fstab 读取静态配置,systemd 再将相关记录转为挂载单元。普通用户可以先查看而不改变配置:
cat /etc/fstab
findmnt --fstab
findmnt --verify --verbosefindmnt --verify 检查记录能否解析及其可用性,不执行实际挂载。报告缺失设备、无效路径或选项问题时,先核对它指向的记录;检查通过后,真实挂载仍可能因设备、网络或文件系统状态失败。
fstab 的一行由六个字段组成。下面是结构示例,其中的 UUID 和挂载点不能原样当作本机配置:
UUID=实际文件系统UUID /srv/data ext4 defaults 0 2
│ │ │ │ │ └─ 启动检查顺序
│ │ │ │ └──── dump 备份字段
│ │ │ └───────────── 挂载选项
│ │ └─────────────────── 文件系统类型
│ └────────────────────────────── 挂载点
└──────────────────────────────────────────────────── 存储来源UUID 标识具体文件系统,通常比可能随设备探测顺序变化的 /dev/sdX 更适合持久配置。克隆文件系统可能复制 UUID,仍需检查它是否唯一。第五项常为 0;第六项 0 表示不安排这项文件系统检查,根文件系统常用 1,其他需要检查的文件系统常用 2,具体行为还取决于文件系统检查程序。字段与转义规则见 fstab(5)。
ro 挂载为只读,rw 允许写入;noexec 限制从该挂载直接执行程序,却不阻止解释器读取可读脚本;nodev 不解释其中的设备节点,nosuid 限制提权位等机制。选项要按数据用途选择,不能把它们当作完整的恶意代码隔离方案。mount 手册列出了通用与文件系统专用选项。
错误的开机挂载配置可能使启动等待设备,或进入维护模式。nofail 允许系统在该挂载不可用时继续启动,但依赖它的程序还需要单独处理:例如 /srv/data 没挂上时,写入这个目录可能落到根文件系统。在已有对应挂载单元或 fstab 记录时,服务可在 [Unit] 段通过 RequiresMountsFor=/srv/data 建立所需挂载的依赖与启动顺序,详见 systemd.unit。真正修改 fstab 时,应先保留原配置、校验具体记录,再只测试对应挂载点;mount -a 会处理多条记录,不适合拿来试探陌生主机。
在独立虚拟机上观察一次挂载
下面只用于专用 Ubuntu 练习虚拟机,需要 sudo;不在宿主机、生产机或普通容器里执行。它创建上限为 32 MiB 的 tmpfs,不格式化磁盘,不修改 /etc/fstab。tmpfs 按实际写入分配内存,大小上限不是预先保留的内存;卸载后内容消失,启用交换空间时其中部分页还可能被换出。内核 tmpfs 文档说明了容量和持久性。
普通用户创建独立目录,保留打印出的完整路径,逐行确认成功后再继续:
MOUNT_LAB=$(mktemp -d /tmp/linux-mount.XXXXXX)
printf '%s\n' "$MOUNT_LAB"
mkdir "$MOUNT_LAB/mnt"
printf '%s\n' 'layer=underlying' > "$MOUNT_LAB/mnt/under.txt"
findmnt -T "$MOUNT_LAB/mnt" -o TARGET,SOURCE,FSTYPE目前 mnt 只是原文件系统中的普通目录。挂上新的 tmpfs,再查看目录:
sudo mount -t tmpfs \
-o "size=32M,nr_inodes=128,mode=0700,uid=$(id -u),gid=$(id -g),nosuid,nodev,noexec" \
linuxlab-tmpfs "$MOUNT_LAB/mnt"
findmnt -M "$MOUNT_LAB/mnt" -o TARGET,SOURCE,FSTYPE,OPTIONS
ls -A "$MOUNT_LAB/mnt"
printf '%s\n' 'layer=tmpfs' > "$MOUNT_LAB/mnt/upper.txt"
cat "$MOUNT_LAB/mnt/upper.txt"
df -hT "$MOUNT_LAB/mnt"findmnt -M 应精确找到这个挂载点,FSTYPE 为 tmpfs;首次 ls -A 没有输出,随后可以读到 layer=tmpfs。原来的 under.txt 被新挂载遮住,内容没有删除。uid、gid 取当前操作者的数字身份,因此挂载后的根目录仍允许这位普通用户写入。
若 mount 失败,停在该处查看诊断,不继续向目录写文件,否则写入的仍是下层文件系统。清理前保持终端工作目录在挂载点之外,并关闭其他终端或程序对它的使用:
sudo umount "$MOUNT_LAB/mnt"
findmnt -M "$MOUNT_LAB/mnt"
cat "$MOUNT_LAB/mnt/under.txt"卸载成功后,精确挂载查询应无结果并返回非零,under.txt 又显示 layer=underlying;tmpfs 中的 upper.txt 已随卸载消失。若提示 target is busy,先检查是否还有 Shell 停留在其中、是否有打开文件或子挂载,不直接使用强制或延迟卸载掩盖占用。确认已卸载后,只删除本轮明确文件和空目录:
rm -i "$MOUNT_LAB/mnt/under.txt"
rmdir "$MOUNT_LAB/mnt"
rmdir "$MOUNT_LAB"文件名、inode 与打开引用
目录保存文件名与文件对象之间的对应关系;inode 保存文件类型、权限、属主、大小、链接计数等元数据,并关联文件的数据。inode 编号只在所属文件系统内有意义,不是全机永久唯一编号;文件删除后,编号还可能被重新分配。inode(7)给出了常见元数据字段。
硬链接为同一个文件对象增加名字,因此共享内容、权限和链接计数。符号链接则保存一个路径,访问时沿该路径继续查找;目标路径失效后,符号链接本身仍可存在。硬链接不能跨文件系统,通常也不能为目录创建;符号链接可以跨文件系统并指向目录。操作规则见 ln。
original.txt ──目录项──┐
├── inode A ── 文件内容
hard.txt ─────目录项──┘ ↑
│
当前 Bash 的文件描述符 ── 打开引用
soft.txt ── inode B ── 保存路径 "original.txt" ── 再解析目标在普通用户 Bash 中创建一个新的练习目录,使用 Linux 本地文件系统,不放在 Windows 共享目录或特殊网络挂载中:
STORAGE_LAB=$(mktemp -d /tmp/linux-storage.XXXXXX)
printf '%s\n' "$STORAGE_LAB"
printf '%s\n' 'version=one' > "$STORAGE_LAB/original.txt"
ln "$STORAGE_LAB/original.txt" "$STORAGE_LAB/hard.txt"
ln -s original.txt "$STORAGE_LAB/soft.txt"
stat -c '%n inode=%i links=%h size=%s' \
"$STORAGE_LAB/original.txt" "$STORAGE_LAB/hard.txt" "$STORAGE_LAB/soft.txt"original.txt 与 hard.txt 应显示相同 inode,链接计数都为 2。默认 stat 查看符号链接本身,因此 soft.txt 有自己的 inode;stat -L 才跟随它查看目标。ln -s 中的相对目标 original.txt 相对于符号链接所在目录解释,所以从其他工作目录访问 $STORAGE_LAB/soft.txt 仍可找到目标。stat 手册说明了字段和跟随链接选项。
从硬链接追加一行,再读取原名字:
printf '%s\n' 'extra=line' >> "$STORAGE_LAB/hard.txt"
cat "$STORAGE_LAB/original.txt"应同时看到 version=one 和 extra=line。硬链接没有创建第二份内容,所以它不适合作为独立备份;通过一个名字修改,另一个名字也会看到修改。
现在让 Bash 保持文件打开,再移除两个名字。exec {STORAGE_FD}<... 在当前 Bash 中打开一个读描述符,并把自动分配的编号存入 STORAGE_FD;这次 exec 没有执行另一个程序:
exec {STORAGE_FD}< "$STORAGE_LAB/original.txt"
printf 'open_fd=%s\n' "$STORAGE_FD"
rm -- "$STORAGE_LAB/original.txt"
cat "$STORAGE_LAB/soft.txt"
printf 'soft_read_exit=%s\n' "$?"
cat "$STORAGE_LAB/hard.txt"
stat -Lc 'inode=%i links=%h size=%s' "/proc/$$/fd/$STORAGE_FD"符号链接读取应失败,因为它保存的原路径已不存在;hard.txt 仍可读取,inode 的硬链接计数变为 1。$$ 是当前 Bash 的进程号,/proc/$$/fd/编号 可用于观察它已打开的文件。
rm -- "$STORAGE_LAB/hard.txt"
stat -Lc 'inode=%i links=%h size=%s' "/proc/$$/fd/$STORAGE_FD"
readlink "/proc/$$/fd/$STORAGE_FD"
cat <&"$STORAGE_FD"
exec {STORAGE_FD}<&-
test -e "/proc/$$/fd/$STORAGE_FD"
printf 'fd_exists_exit=%s\n' "$?"两份硬链接都被移除后,计数为 0,readlink 通常显示原路径并附加 (deleted),但通过描述符仍能读到那两行内容。最后关闭描述符,再检查这个 /proc 入口,应返回 1。这次读取会推进文件偏移;若在关闭前重复 cat <&"$STORAGE_FD",已经到达文件末尾时没有输出,不能据此判断文件提前消失。
删除文件名、关闭打开引用和释放存储是不同动作。普通文件最后一个硬链接消失后,尚存的打开引用可以继续使用它;所有相关使用结束后,空间才能回收。unlink(2)说明了删除名称后的行为。文件内存映射还可以在关闭原描述符后继续存在,因此硬链接计数、打开描述符与映射需要分别考虑;映射生命周期见 mmap(2)。
这也解释了日志文件的一个常见现象:删除正在写入的日志后,程序仍向旧文件对象写数据,磁盘空间没有立即回升。正确处理要使用程序支持的日志轮转和重新打开机制;仅重新创建同名空文件,不会让已有描述符自动转向新文件。需要停止或重启程序时,也应按该服务的正常停止流程操作。
容量、inode 与 df/du 的差异
继续在保存 STORAGE_LAB 的终端查看空间:
df -hT "$STORAGE_LAB"
df -i "$STORAGE_LAB"
du -x -h --max-depth=1 "$STORAGE_LAB"df 查询文件系统整体的已用与可用空间,df -i 查询 inode 的使用情况;du 沿文件名遍历目录,累计可见文件分配的存储块。-x 限制遍历不跨入其他文件系统。相应字段和选项见 df及 du。
容量还有两个常被混用的口径:文件的逻辑长度,与已经分配的空间。创建一个逻辑长度为 16 MiB 的稀疏文件:
truncate -s 16M "$STORAGE_LAB/sparse.bin"
stat -c 'size=%s blocks=%b block_unit=%B' "$STORAGE_LAB/sparse.bin"
du -B1 "$STORAGE_LAB/sparse.bin"
du -B1 --apparent-size "$STORAGE_LAB/sparse.bin"在支持稀疏文件的本地文件系统中,逻辑长度应为 16777216 字节,实际分配空间可能为零或远小于这个值。尚未写入的空洞读起来是零字节序列;后续真正写入,才可能分配更多数据块。复制、归档或传输工具怎样处理空洞,会影响副本的实际占用,所以不能仅凭 ls -l 的大小推算备份需要多少存储。
空间异常可以按下面的顺序收窄:
- 确认写入位置。 对实际失败的目录执行
findmnt -T 路径和df -hT 路径,先排除数据盘未挂载、写入根分区或 tmpfs 的情况。文件系统只读时还应查看挂载选项及内核日志,不能直接强制改回读写而忽略底层错误。 - 区分容量与 inode。 字节仍充足但 inode 接近耗尽,常见于大量小文件;应确定文件由哪个缓存、队列或程序产生,再按其保留策略清理。inode 的分配方式随文件系统而异,不能把 ext4 的容量安排套给所有实现。用户或项目配额也可能先于整盘容量阻止写入。
- 缩小目录扫描。 对相关路径逐层执行
du -x -h --max-depth=1 路径,需要读取系统服务目录时才使用获准的 sudo。读取权限错误意味着统计不完整,扫描期间发生写入或删除也会改变结果。不要把一次遍历当成整个文件系统的原子快照。 - 检查已删除但仍被使用的文件。
df明显高于相关目录统计时,这只是一个候选原因;元数据、保留空间、挂载遮盖、快照或统计范围不同也会造成差异。
对于前面已启动的专用服务,可以限定到它的主进程查询打开文件。lsof 缺失时在 Ubuntu 练习机安装 lsof 包。先取得 PID,并确认它是正整数且 ps 显示目标程序;服务未运行、PID 为 0 或查询失败时,先返回服务启动检查:
SERVICE_PID=$(systemctl show linuxlab.service -p MainPID --value)
printf 'service_pid=%s\n' "$SERVICE_PID"
ps -p "$SERVICE_PID" -o pid,user,comm
sudo lsof -nP -a -p "$SERVICE_PID" +L1+L1 选择硬链接计数小于 1 的打开文件,-a 让它与 PID 条件同时成立;没有匹配记录时可以无输出并返回非零。若有结果,再核对 TYPE、SIZE/OFF、NODE 与文件名,区分普通日志和其他用途,不能把整列大小直接相加当作可回收空间。文件可能被多个描述符重复引用,服务也可能还有子进程。lsof 手册给出了筛选与输出规则。
确认是目标日志且程序支持重新打开时,执行该程序规定的轮转或重开动作,再查旧引用、df 和新日志写入;需要重启才释放时,先安排服务停止与恢复。对仍有业务用途的数据文件,不应通过截断 /proc/PID/fd/N 来腾空间。
链接练习只剩符号链接和稀疏文件。先打印并核对目录中的实际内容,再删除本轮目标:
printf '%s\n' "$STORAGE_LAB"
find "$STORAGE_LAB" -maxdepth 1 -mindepth 1 -printf '%f\n'
rm -i "$STORAGE_LAB/soft.txt" "$STORAGE_LAB/sparse.bin"
rmdir "$STORAGE_LAB"rmdir 遇到残留会拒绝;此时保留目录并检查剩余文件,不扩大删除范围。
CPU、内存与运行限制
CPU 利用率与负载描述不同状态
Linux 调度的对象是可运行的任务,程序中的线程可以分别参与调度。逻辑 CPU 是调度器可使用的处理器位置;一台有两个逻辑 CPU 的虚拟机,可以同时执行两个线程,但每个逻辑 CPU 的实际速度还受宿主、虚拟化方式和硬件影响。
在练习机的普通终端先看处理器视图和负载,再做一个短时间采样。lscpu 属于 util-linux,free、vmstat、ps、top 属于 procps;工具缺失时安装相应包。
lscpu -e=CPU,CORE,SOCKET,ONLINE
nproc
cat /proc/loadavg
vmstat 1 5lscpu 显示客体系统看到的拓扑,不应据此反推宿主机的物理布局。nproc返回当前进程可用的处理单元数量,服务的 CPU 配额还需另查;容器或受限制服务不能只拿主机 CPU 数量作为可用计算预算。
/proc/loadavg 的前三项对应 1、5、15 分钟负载平均值。Linux 负载包含正在运行、等待 CPU,以及处于不可中断等待中的任务。它没有百分号,也未除以 CPU 数量。两个逻辑 CPU 上,若负载主要来自可运行任务且长期远高于 2,才提示 CPU 队列值得检查;负载升高同时 CPU 仍较空闲,则要查看不可中断等待及其原因。负载的统计对象见 uptime 手册。
vmstat 1 5 显示五次报告。第一行的 CPU 与速率字段主要反映开机以来的平均值,后续行覆盖相邻一秒;其中进程与内存字段仍是观察时刻的状态。判断当前问题以连续后续行和更长趋势为依据,不把第一行与其他行当成同一窗口。
| 字段 | 观察含义 |
|---|---|
r | 正在运行或等待运行的任务数量 |
b | 等待 I/O 完成而阻塞的任务数量 |
us、sy | 用户态、内核态执行所占的 CPU 时间比例 |
id | 空闲 CPU 时间比例 |
wa | I/O 等待相关的空闲时间统计,不是磁盘利用率 |
st | 虚拟 CPU 被宿主调度占走的时间比例,依赖虚拟化平台的报告能力 |
字段定义见 vmstat。wa 单独升高不足以确定哪块设备慢,它也不是所有等待 I/O 的线程耗时之和;多核和异步 I/O 都会影响观察。应把它与进程状态、设备延迟及请求变慢的时间放在一起看。
查看进程使用情况可运行 top,按 P 按 CPU 排序,按 1 查看各逻辑 CPU,按 q 退出。常见 Irix 显示模式中,进程占满一个逻辑 CPU 约为 100%,多线程进程可以超过 100%;Solaris 模式会再除以 CPU 数量,交互键 I 可切换。top 的进程 %CPU 主要使用刷新间隔,而 ps 的 %CPU 通常是进程整个生存期的平均值。两者不同不一定是采样出错。top和 ps分别说明了计算方式。
短时峰值要结合具体工作判断:编译、压缩或垃圾回收可能短时消耗 CPU。持续高占用且处理量不再增长,再查热点线程、锁竞争或重试;持续排队但进程用不到预期 CPU,还应查 CPU 配额和可运行 CPU 集合。基于软件模拟的虚拟机适合验证命令和状态,不适合用来测算生产吞吐量。
进程内存、页缓存与可用内存
进程看到虚拟地址空间,内核再把使用到的地址映射到物理页、文件或其他后备存储。查看前面已经运行的 linuxlab.service;服务重启后需要重新取得 PID:
SERVICE_PID=$(systemctl show linuxlab.service -p MainPID --value)
printf 'service_pid=%s\n' "$SERVICE_PID"
ps -p "$SERVICE_PID" -o pid,user,comm,rss,vsz,nlwp
free -h只在 PID 大于 0 且确认为目标程序时继续查询 /proc;没有该行意味着进程可能已退出,应先查服务状态。
RSS 是进程当前驻留在物理内存中的部分,VSZ 是虚拟内存大小,这条 ps 命令的两列以 KiB 表示。JVM 可能预留较大的地址范围,所以 VSZ 很大时,先看驻留量及其增长,不直接当成“占用了同样大的物理内存”。RSS 又可能包含与其他进程共享的映射,把多个进程的 RSS 相加会重复计入这些页。
需要进一步区分时,以获准的管理身份读取目标进程的汇总:
sudo grep -E '^(Rss|Pss|Private_Clean|Private_Dirty|Shared_Clean|Shared_Dirty|Swap):' \
"/proc/$SERVICE_PID/smaps_rollup"PSS 按共享者数量分摊共享映射,适合辅助理解进程之间的内存分布;它仍不等于该服务的全部内核与文件缓存费用。/proc 的部分 RSS 统计为可伸缩性采用近似更新,扫描映射获得的统计也只是读取时刻的观察。字段或文件缺失时核对内核支持、目标进程是否仍在,以及读取权限,不用零值填补缺失结果。内核 /proc 文档解释了这些字段。
free -h 则汇总系统内存。重点区分:
free是当前完全未使用的内存。buff/cache包含内核缓冲及文件页缓存、可回收的部分内核对象。Linux 会利用暂时不用的内存缓存文件,减少重复读盘。available估计在不发生交换的情况下还能供新应用使用的内存,考虑可回收缓存和内核保留要求。shared主要反映 tmpfs 等共享内存使用,不是“所有进程共享库占用之和”。
因此空闲值较小而 available 充足,可能只是缓存正在发挥作用。缓存也不是随时都能无成本释放:脏页可能需要先回写,正在使用的页不能全部回收。free 手册列出了各列来源。排查应用内存时,不应通过清空系统缓存制造好看的 free 数值;应查看同样负载下的 available、进程增长、回收压力及服务延迟。
交换活动与存储 I/O
交换空间可以存放从内存换出的页,为物理内存腾出位置。Swap 已用量是当前存量;是否正在频繁交换,要观察 vmstat 后续采样的 si、so。过去换出的页可能仍留在 swap,当前两个速率却都接近零。反过来,持续换入换出并伴随 available 低、服务延迟升高,才值得进一步调查内存压力。
没有 swap 的机器不会发生同样的交换活动,但内存仍可能耗尽。增加 swap 也不会让工作集超出物理内存的程序保持原有速度;应用是否允许使用、需要多大交换空间,应结合延迟要求、负载和磁盘条件判断。
普通文件的缓冲写入通常先改变页缓存,再由内核回写。应用写入速度、进程产生的 I/O 和块设备完成的 I/O 因此未必在每秒内严格相等;涉及掉电后的持久性时,还要看应用使用的同步机制和存储设备保证。
Ubuntu 的 sysstat 包提供 iostat、pidstat。需要这两个工具时,在练习机执行 sudo apt install sysstat,检查命令存在即可进行即时采样,不需要为此启用历史采集服务。保持前面的服务正常运行,在两个终端同一观察时段分别执行:
iostat -xz -y 1 5SERVICE_PID=$(systemctl show linuxlab.service -p MainPID --value)
printf 'service_pid=%s\n' "$SERVICE_PID"
sudo pidstat -u -r -d -p "$SERVICE_PID" 1 5第二个终端仍需先确认 PID 为目标进程的正整数。服务属于 linuxlab-svc,这里使用获准的 sudo 读取跨用户的进程 I/O;以普通登录用户查询时,I/O 列可能显示 -1,表示未取得统计,不能按零或负流量解读。iostat 的 -x 显示扩展指标,-z 隐藏没有活动的设备,-y 跳过开机以来的首份累计报告;pidstat 分别采样 CPU、内存和进程 I/O。空闲服务可能所有速率都很低,设备列表也可能因 -z 为空,这是正常的空闲观察。
r/s、w/s 表示设备每秒完成的请求数,读写速率表示数据量;r_await、w_await 包含相应 I/O 的排队与服务时间,aqu-sz 反映平均队列长度。需要一起判断:延迟升高时,队列、请求大小与吞吐是否也发生变化。%util 接近 100% 对串行处理请求的设备有参考价值,现代 SSD、RAID 等能并行处理请求,不能仅靠这一列断言性能已经耗尽。iostat 手册列出了单位和计算方法。
pidstat 的 kB_rd/s、kB_wr/s 是归到该任务的存储读写,不是 HTTP 流量,也不覆盖所有缓存命中后的逻辑读写;进程级 iodelay 还依赖内核延迟统计能力。读取受限或统计未启用时,应先确认工具的诊断,不能把没有有效统计解释成完全没有等待。pidstat 手册解释了进程采样字段。
当 CPU、内存和 I/O 指标都难以解释停顿时,可以补查内核压力统计:
cat /proc/pressure/cpu
cat /proc/pressure/memory
cat /proc/pressure/iosome 表示至少有任务因该资源停顿的时间比例,内存和 I/O 的 full 表示全部非空闲任务同时停顿的比例。avg10、avg60、avg300 使用 10、60、300 秒窗口,total 是累计停顿微秒数,适合与服务延迟对照。系统级 CPU 的 full 没有相同定义,兼容输出为零,不能据此推断没有 CPU 压力。没有这些文件时先核对内核支持,不为基础观察临时修改宿主启动参数。细节见 PSI 文档。
文件描述符与进程限制
进程打开文件、建立 socket、创建管道,都会使用文件描述符。描述符是当前进程内的编号,同一个数字在两个进程中可以指向不同对象;它与文件系统中可创建多少 inode 也无关。
在普通用户 Bash 中先用 ulimit -Sn、ulimit -Hn 查看打开文件软、硬限制。仅当原软限制高于 64 时执行下面的降低实验,原值更低则保留原设置。改动只发生在子 Shell 中:
ulimit -Sn
ulimit -Hn
(
ulimit -Sn 64
ulimit -Sn
grep 'Max open files' /proc/self/limits
)
ulimit -Sn子 Shell 应显示软限制为 64,最后回到外层仍为原值。没有修改硬限制,也没有创建大量描述符来耗尽资源。
软限制是实际约束,硬限制约束普通进程能够提高到的上限;子进程继承相应设置。RLIMIT_NOFILE 要求新分配的描述符编号小于软限制,因此通常表述为可打开文件数量限制;失败可能以 EMFILE、Too many open files 出现。系统级文件表不足则是另一类 ENFILE。具体行为见 getrlimit(2)。
对已确认仍在运行的目标服务,查询其实际限制与描述符:
sudo grep 'Max open files' "/proc/$SERVICE_PID/limits"
sudo ls -l "/proc/$SERVICE_PID/fd"
(
set -o pipefail
sudo find "/proc/$SERVICE_PID/fd" -mindepth 1 -maxdepth 1 -printf '%f\n' | wc -l
)
printf 'count_exit=%s\n' "$?"数字条目指向文件、socket、管道等对象。计数管道在独立子 Shell 中启用 pipefail,退出状态非零时先处理查询错误,不接受 wc 打印的数值。查询成功也只代表当前数量;进程正在打开和关闭描述符时,数量会变化。lsof 的输出还包含工作目录、可执行映射等记录,因此不能直接把它的总行数当作打开 FD 数。
数量随同样请求量持续增长时,先查没有关闭的文件、连接和连接池状态;确实需要更多并发连接,再按服务容量提高限制。使用 select() 的旧程序还受到高编号描述符的约束,提高上限前需确认程序支持。只在登录终端中执行 ulimit,不会改变 systemd 已运行服务的设置。systemd 服务可通过 LimitNOFILE= 设定它启动进程时的限制,LimitRSS= 则在 Linux 上未实现,不能用于限制驻留内存;相关选项见 systemd.exec。
服务、cgroup 与 OOM
一个服务可以启动多个进程和线程。只看 MainPID 的 RSS 或 CPU,会漏掉其他成员;systemd 使用 cgroup 把它们组织起来,内核可以按整组统计并施加限制。namespace 主要改变进程看到的资源名称和视图,cgroup 则负责这类分组与资源控制,容器会组合使用两者。
在专用 Ubuntu 主机查看当前模式与服务属性,普通用户执行即可;查询权限受限时再使用获准的 sudo:
stat -fc %T /sys/fs/cgroup
systemctl show linuxlab.service \
-p ControlGroup -p MemoryCurrent -p MemoryHigh -p MemoryMax \
-p CPUQuotaPerSecUSec -p TasksCurrent -p TasksMax \
-p LimitNOFILE -p LimitNOFILESoftcgroup2fs 表示此处使用统一 cgroup v2 层级。MemoryCurrent 是整组按内核规则计费的内存,可能包含文件缓存和内核资源,与 MainPID 的 RSS 不同。infinity 表示当前属性没有设置相应有限值,祖先 slice 的限制、CPU 可用集合等仍可能收紧实际资源。
常见的服务设置如下,位置为该服务 unit 的 [Service] 段或专属 drop-in;数值仅用于解释含义,不能直接当作所有应用的容量配置:
[Service]
CPUQuota=100%
MemoryHigh=768M
MemoryMax=1G
TasksMax=128
LimitNOFILE=4096CPUQuota=100% 允许整组在配额周期内使用最多相当于一个逻辑 CPU 的时间,并非“允许使用整机全部 CPU”;它限制累计时间,不要求线程绑定在同一个 CPU。MemoryHigh 触发较积极的回收与节流,允许必要的超出;MemoryMax 是内存硬上限,回收无法满足时可能在组内触发 OOM。TasksMax 统计线程在内的任务数,多线程服务可能比进程数更早触及它。LimitNOFILE 则属于各进程启动时继承的限制,不能和整组限制混为一项。机制与设置见 systemd.resource-control。
真实调整应先记录当前 unit 和测得的峰值,保留 JVM 堆外内存、线程栈、缓存及子进程余量。按前面的配置变更流程加载后,再检查 systemctl show、新进程的 /proc/PID/limits 和同样请求下的行为。降低内存上限可能立即制造回收或 OOM,不能把它当作无影响的只读诊断。
确认服务运行、ControlGroup 为该服务的非空路径,且主机使用 cgroup v2 后,可进一步读取具体计数:
SERVICE_CGROUP=$(systemctl show linuxlab.service -p ControlGroup --value)
printf '%s\n' "$SERVICE_CGROUP"路径通常类似 /system.slice/linuxlab.service;为空或为 / 时先停止,不改查根组冒充服务数据。确认后读取:
sudo cat "/sys/fs/cgroup$SERVICE_CGROUP/memory.events"
sudo cat "/sys/fs/cgroup$SERVICE_CGROUP/cpu.stat"
sudo cat "/sys/fs/cgroup$SERVICE_CGROUP/pids.events"memory.events 中,high 增长表示越过高水位后的回收或节流,max、oom、oom_kill 对应不同阶段,触及上限与真正杀死进程需要分别看。cpu.stat 的节流计数增长说明 CPU 配额正在影响调度;pids.events 的 max 反映任务数量上限阻止创建任务的事件。应比较同一个组在故障前后的增量,并核对祖先组;组被删除重建后不能沿用旧累计值。字段不存在也可能是相应控制器未启用。完整定义见 cgroup v2 内核文档。
OOM 也有不同来源:整机内存压力可能触发内核 OOM;服务自己的 cgroup 限额可能在主机仍有空闲内存时触发组内 OOM;启用的 systemd-oomd 还可以根据压力主动终止选中的 cgroup。后者运行在用户空间,应单独查看其日志,不能只找内核 OOM 记录。systemd-oomd说明了触发依据。
服务异常退出时,先保留状态和日志,再考虑重启,避免丢失当时的进程与组信息:
systemctl show linuxlab.service -p Result -p ExecMainCode -p ExecMainStatus
sudo journalctl -u linuxlab.service -b -n 80 --no-pager
sudo journalctl -k -b -g 'oom|Out of memory|Killed process' --no-pager
sudo journalctl -u systemd-oomd.service -b -n 50 --no-pager没有部署或启用 systemd-oomd 时,它可能没有记录。SIGKILL 或 Shell 常见的退出状态 137 也可能来自人工、编排器或停止超时,不能单独判为 OOM。JVM 自己报告 OutOfMemoryError 又属于语言运行时失败,即使内核没有杀进程也可能发生。
确认原因后再处理对应对象:持续泄漏先控制增长来源,配额偏小则按实际峰值调整并检查祖先限制,突发并发过高则减少并发或排队量。恢复后重新取得 PID,验证服务响应,再观察同样负载下的内存、节流和错误计数;只看到进程重新出现,还不足以判断原来的资源问题已经消失。
网络配置与服务访问
一个请求能否到达服务,取决于几个不同的条件:客户端把名称解析成了什么地址,内核决定从哪个接口发出数据,沿途是否允许通行,最后是否有进程在那个地址和端口上接收连接。服务进程存在,只能说明其中很小的一部分已经成立。
前面建立的 linuxlab.service 默认监听 127.0.0.1:18080。它适合从虚拟机内部验证程序,也恰好可以用来观察:同一个程序没有改变,监听地址一变,能够访问它的机器范围便不同了。
地址属于接口,路由决定下一跳
先在练习虚拟机查看接口和两套地址的路由:
ip -brief link
ip -brief address
ip -4 route
ip -6 routelink 关注网卡这一层:接口名、是否启用、链路状态;address 才显示分配给接口的 IP 地址。接口名可能是 enp0s3、ens18 等,不能从别人的命令里照搬。接口显示 UP 也不代表 DNS、默认路由和远端服务都正常。
IPv4 地址由 32 位组成。192.168.56.20/24 中的 /24 表示前 24 位是网络前缀,不是端口号;在这个例子中,同一前缀是 192.168.56.0/24。IPv6 地址使用 128 位,常见局域网前缀长度是 /64,但不能把 /64 当成所有 IPv6 地址的固定属性。一个接口可以同时拥有多个 IPv4、IPv6 地址。
常见的 IPv4 私有地址来自 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16,不会作为普通公网目的地址在互联网中直接路由;它们仍可能被同一内部网络中的其他机器访问。地址是私有还是公网、链路是否可达、端口是否放行,是三个不同判断。IPv6 也有不同作用范围,不能因为没有传统 IPv4 NAT 就直接判定某个服务一定从公网可访问。
lo 是回环接口,数据不必发到物理网卡。IPv4 的 127.0.0.1 和 IPv6 的 ::1 都指向本机,但属于不同地址族。fe80::/10 范围的 IPv6 链路本地地址只在对应链路内有效;当机器有多个接口时,访问这类地址还需要用 %接口名 指明作用域。IPv6 的地址类型和作用域见 Linux ipv6(7)。
路由表回答的是“去这个目标时,下一步交给谁”。同网段的目标通常经直连路由到达;其他目标可能经网关转发。default 是没有更具体匹配时使用的路由,相当于 IPv4 的 0.0.0.0/0 或 IPv6 的 ::/0。它不等于“所有请求无条件都从这里走”:普通目标地址路由首先选择更长的匹配前缀;多出口和策略路由还会涉及额外规则。
与其猜测接口,不如询问内核对一个具体目标的选择。例如,先取得 Ubuntu 软件源的 IPv4 地址,再从实际结果中选择一个:
getent ahostsv4 archive.ubuntu.com
read -r -p '输入查询结果中的一个 IPv4 地址:' LAB_ROUTE_IP
ip -4 route get "$LAB_ROUTE_IP"route get 的结果中,dev 是出口接口,via 在需要网关时给出下一跳,src 是本机为这次路由选择的源地址。这只是内核的路由计算,不会证明沿途设备允许连接,也不会验证远端应用。没有 DNS 结果时,可以对虚拟网络配置中已知的网关或第二台练习机地址执行相同查询,不必先改 DNS。地址和路由子命令分别见 ip-address(8) 与 ip-route(8)。
临时网络状态和持久配置是两件事
ip 修改的是当前内核中的网络状态。网络管理器重新配置接口、连接重新建立或机器重启后,这些改动可能消失。Ubuntu Server 使用 Netplan 描述持久网络配置,再交给 systemd-networkd 等后端执行;桌面或其他安装方式也可能采用 NetworkManager。先认清正在使用的配置来源:
ls -l /etc/netplan
sudo netplan get
systemctl is-active systemd-networkd NetworkManager某个管理服务返回 inactive 并不说明网络坏了;重点是正在使用哪一个,以及 Netplan 为当前接口定义了什么。由 cloud-init 生成的文件可能注明会被重新生成,不能忽略其配置所有者直接修改。
下面的地址修改只适用于额外建立的隔离实验网卡:假设接口确实叫 enp0s8,已经启用并接入 192.168.56.0/24 虚拟网络,而且管理者已把 192.168.56.21 留给本机,确认它不在 DHCP 分配池内且未被其他机器使用。保留原有 NAT 管理网卡,不拿当前 SSH 所依赖的接口练习。
sudo ip address add 192.168.56.21/24 dev enp0s8
ip -4 address show dev enp0s8
ip -4 route show dev enp0s8添加接口地址时,内核通常也建立相应的直连前缀路由,所以没有配置默认网关,机器仍可能访问这个隔离网段内的其他地址。这不表示它能通过该网卡访问互联网。观察完后,删除刚刚添加的这一个地址:
sudo ip address del 192.168.56.21/24 dev enp0s8
ip -4 address show dev enp0s8如果添加时得到 File exists,先检查地址是否本来就存在,不要继续执行对应的删除命令。这里不使用 ip address flush 或清空路由表;它们可能把原有连接一起移走。
把同一个隔离接口改成持久配置时,应先通过虚拟机控制台操作,并备份将要编辑的原文件。现有接口应修改它的现有定义,不要在第二份 YAML 中另写一套矛盾配置。只有确认这个额外接口尚未被任何 Netplan 文件管理、下列目标文件也不存在时,才可新建 /etc/netplan/60-linuxlab.yaml:
network:
version: 2
ethernets:
enp0s8:
renderer: networkd
dhcp4: false
addresses:
- 192.168.56.21/24YAML 用空格缩进,不用 Tab。这个例子刻意没有默认路由和 DNS:额外网卡只负责隔离实验网络,原有 NAT 网卡继续负责外网访问。真正需要静态默认路由时,接口定义中使用 routes 下的 to: default、via: 实际网关;DNS 服务器则在 nameservers.addresses 中配置。二者解决的问题不同,不能因为“上不了网”就把网关地址随意当作 DNS 地址。完整字段见 Netplan YAML 配置。
编辑结束后,在控制台中校验并试用:
sudo chmod 600 /etc/netplan/60-linuxlab.yaml
sudo netplan generate
sudo netplan try --timeout 120generate 检查并生成后端配置,但不等于已经完成联网验证。try 会临时应用候选配置并等待确认;在确认前,用另一个终端查看接口、路由,并测试原来的管理连接和新网段内的连通性。只有这些检查符合预期,才确认保留。默认路由、DNS 也被修改时,还应分别验证它们,不能只看 SSH 没断。
超时或取消后要重新查看实际地址和路由,不能仅凭提示认定回滚已经完整完成。Netplan 文档明确提醒某些情形存在回滚问题。候选 YAML 已经写在磁盘上,失败后还应恢复原文件;若是本例新建的文件,则在控制台确认路径和内容后只删除该文件,再重新生成、试用配置。未核对磁盘配置前不要靠重启“恢复网络”。try 的确认与回滚边界见 netplan try,Ubuntu 的配置流程见 Configuring networks。
主机名、NSS 与 DNS 查询
机器的主机名是本机身份设置的一部分,可以用 hostnamectl status 查看;设置了主机名,并不等于其他机器的 DNS 就会自动知道它。/etc/hosts 中的名称映射也只影响使用该文件的本机查询,不会向整个局域网广播。
应用拿到域名后,通常通过系统提供的名称解析接口查地址。Linux 的 NSS 决定查询哪些来源、按什么顺序查询;/etc/hosts、DNS 或 systemd-resolved 都可能处于这条路径上。
grep '^hosts:' /etc/nsswitch.conf
getent ahosts archive.ubuntu.com
resolvectl status
resolvectl query archive.ubuntu.com
ls -l /etc/resolv.confnsswitch.conf 中的 hosts: 行是名称查询顺序,不是网卡配置。方括号内的动作也会影响后续来源是否继续查询,所以不能只看见 dns 就认定其他来源无关。getent 通过 NSS 查询,适合接近普通应用的系统解析路径;具体应用还可能使用自己的缓存或解析器。nsswitch.conf(5) 说明了来源顺序与返回动作。
resolvectl status 显示 systemd-resolved 当前获得的全局、各链路 DNS 配置;这些信息可能来自 DHCP、Netplan 或连接管理器。resolvectl query 则直接请这个解析服务查询。多接口系统中,不同域名可能被送往不同链路的 DNS,因此“系统 DNS”未必只是一个全局服务器列表。若该机并未使用 systemd-resolved,这个命令的失败不代表所有解析方式都失效,应回到实际的 NSS 与解析器配置。resolvectl(1) 解释了这些查询入口。
Ubuntu 的 /etc/resolv.conf 常是符号链接,内容可能出现 127.0.0.53。这个地址通常是本地解析服务的入口,不是运营商提供的上游 DNS;上游应从 resolvectl status 等实际管理工具中查看。直接覆盖这个文件,容易破坏连接管理器维护的关系,也未必能持久生效。
需要进一步区分 DNS 返回的记录时,先检查 dig 是否可用;Ubuntu 中可通过 dnsutils 软件包安装它:
command -v dig
dig -r archive.ubuntu.com A
dig -r archive.ubuntu.com AAAAA 查询 IPv4 地址,AAAA 查询 IPv6 地址。-r 不读取个人 .digrc,减少本机习惯设置的干扰。dig 是 DNS 查询工具,不按 NSS 的全部来源顺序查询;不指定 @服务器 时,它通常读取 /etc/resolv.conf 中的服务器。如果这里指向本地解析服务,返回结果仍可能受该服务的合成答案或本地配置影响。因此,“dig 与 getent 不同”不是立即修改 /etc/hosts 的理由,要先确定两者实际询问了谁。只有在网络策略允许时,才用 dig -r @已确认的DNS地址 域名 A 比较一个明确上游的答案。BIND dig 手册 给出了服务器选择和查询选项。
从监听地址走到第二个客户端
先让 linuxlab.service 保持 bind=127.0.0.1、port=18080。在服务器虚拟机中检查监听,再发一次有明确超时限制的请求:
sudo ss -lntp 'sport = :18080'
curl -q --noproxy '*' --fail-with-body \
--connect-timeout 3 --max-time 5 \
http://127.0.0.1:18080/helloss 中的 -l 只看监听,-n 保留数字地址和端口,-t 选择 TCP,-p 查看进程。需要同时读本地地址与端口:127.0.0.1:18080 只接收发往该回环地址的连接;0.0.0.0:18080 表示所有本机 IPv4 地址,会扩大服务暴露面。[::]:18080 是 IPv6 通配监听,能否顺带接收 IPv4 还取决于套接字和程序设置,不能仅凭这一行下结论;必要时用 ss -4、ss -6 与对应客户端请求分别验证。ss(8) 说明了地址族与套接字筛选方式。
JDK 使用双栈套接字时,ss 也可能把指定的 IPv4 地址显示成 [::ffff:127.0.0.1]:18080 或 [::ffff:实际IPv4]:18080。这是 IPv4 映射表示,仍需按其中的具体地址判断,与 [::]:18080 的通配监听不同。不要只因输出带方括号就把地址替换成任意 IPv6 地址测试。
这里的 -q 放在 curl 的第一个选项位置,以免读取个人 curl 配置;--noproxy '*' 让这次已获准的实验请求直连,不改变系统代理设置;--fail-with-body 在 HTTP 错误状态下保留响应体并返回非零状态。两个超时分别限制建立连接和整次请求的等待,不需要为一次练习在终端里无限等下去。这些选项的含义见 curl 手册。
这个服务的 GET /hello 与 GET / 都应返回配置中的 message 并换行;其他 GET 路径返回 404。请求一次不存在的路径,同时查看 HTTP 状态和命令退出状态:
curl -q --noproxy '*' --fail-with-body \
--connect-timeout 3 --max-time 5 \
--write-out '\nhttp_status=%{http_code}\n' \
http://127.0.0.1:18080/missing
printf 'curl_exit=%s\n' "$?"预期响应体为 request rejected、HTTP 状态为 404、curl 退出状态为 22。这组结果表明请求已到达 HTTP 服务,只是路径不被支持。只有非零退出状态还不能作出相同判断:例如状态为 000,应先处理连接等传输问题。后续继续使用已知的 /hello 端点,便于比较网络条件改变前后的结果。
然后查看虚拟机的非回环 IPv4 地址。在当前服务器终端中输入已经分配给本机、准备供实验客户端访问的地址:
ip -4 -brief address
read -r -p '输入本机用于实验的 IPv4 地址:' LAB_SERVER_IP
curl -q --noproxy '*' --fail-with-body \
--connect-timeout 3 --max-time 5 \
"http://${LAB_SERVER_IP}:18080/hello"只监听回环时,这次访问非回环地址通常会失败;具体表现还可能受本机防火墙影响。先前对 127.0.0.1 的成功,不会自动变成对其他地址的成功。
为第二个客户端开放时,使用虚拟机控制台编辑 /etc/linux-lab/app.properties,只把 bind 改为刚确认的本机实验地址,保留 port=18080 与原来的 message。不要填客户端地址,也不要为图省事改成通配地址。修改后重启并先在服务器内复核:
sudoedit /etc/linux-lab/app.properties
sudo systemctl restart linuxlab.service
sudo ss -lntp 'sport = :18080'
curl -q --noproxy '*' --fail-with-body \
--connect-timeout 3 --max-time 5 \
"http://${LAB_SERVER_IP}:18080/hello"监听结果应变成指定的非回环地址。这个配置并不同时监听 127.0.0.1,因此不能再把旧的回环请求当作此时必须通过的检查。若服务启动失败,先看 systemctl status 和本单元日志;填了并不属于本机的地址,是应优先核对的配置错误之一。
第二客户端必须真的处于能到达这个接口的网络中,例如同一隔离虚拟交换网络中的另一台练习虚拟机。许多虚拟化软件的默认 NAT 连接不允许宿主机或另一台虚拟机直接主动访问来宾;这时先选择合适的隔离网络拓扑,不要顺手把端口转发暴露到公网。在第二客户端的终端中重新设置变量并发请求:
read -r -p '输入服务器的实验 IPv4 地址:' LAB_SERVER_IP
ip -4 route get "$LAB_SERVER_IP"
curl -q --noproxy '*' --fail-with-body \
--connect-timeout 3 --max-time 5 \
"http://${LAB_SERVER_IP}:18080/hello"成功时,应读到与服务器内相同的 message。这次结果才包含客户端出口、虚拟网络、服务端入口与应用响应;服务器自己请求自己的网卡地址,不等于已经检验了这段路径。客户端得到域名而非裸 IP 时,还须检查它自己的解析结果;服务器上的 DNS 正常不能替它作证。IPv6 HTTP URL 则必须给地址加方括号,例如 http://[::1]:18080/hello,前提仍是服务确实监听了该地址族。
区分解析、连接和 HTTP 失败
客户端直连失败后,先保留错误与服务端监听结果,再检查入口规则,不要先停止防火墙。Ubuntu 已安装 UFW 时可先运行只读检查:
sudo ufw status verbose如果 UFW 未安装或没有启用,继续确认是否由其他组件管理 nftables 规则;已安装 nft 时可用 sudo nft list ruleset 阅读现状。虚拟交换网络、云安全组和云网络 ACL 又处于不同位置,应分别核对实际客户端源地址、协议、目标端口与返回路径。UFW 显示 inactive 并不证明链路上没有任何过滤规则。规则语法与状态查询见 ufw(8)。
只在确认某条既有策略挡住了这次获准实验后,才按策略增加“指定客户端来源到指定服务器地址的 TCP 18080”这一条最小许可,并记下准确的删除方式。不要用全来源放行、清空规则或关闭防火墙代替排查;若这台机器由他人管理,规则变更也不能仅凭教程自行决定。
几种表面相近的失败实际停在不同位置:
- 名称解析失败时,先比较客户端的
getent、解析器状态和 DNS 答案,尚未进入目标端口连接。 Connection refused常见于目标可达但没有对应监听,也可能由主动拒绝规则产生;要把客户端错误与服务器的ss、规则一起看。- 连接超时只能说明在时限内没完成连接,可能涉及错误地址、丢包、过滤或返回路由,不能直接定性为防火墙。
- 已返回 404 时,请求已经得到 HTTP 响应;继续核对路径、实际命中的进程和应用日志,而不是反复改路由。
ping 只测试相应 ICMP 路径,可能被单独限制;它成功也不证明 TCP 18080 可访问。相反,ICMP 不响应也不能推翻已经成功的 HTTP 请求。
实验结束后,把配置中的 bind 恢复为 127.0.0.1,重启服务,再验证 ss 的监听地址及回环 /hello。第二客户端的原请求应不再成功;若还得到同样响应,应核对是否命中了另一个进程、另一台机器或残留转发规则。撤销本次确实新增的最小放行规则,不改动原有策略。
时间、日志与计划任务
服务日志记录“什么时候发生”,定时任务决定“什么时候开始”,而超时和耗时统计关心“过去了多久”。这三类问题看起来都在读时间,实际上不应依赖完全相同的时钟语义。
时钟的读数、时区的显示与时间同步
先只读观察练习虚拟机,不修改实际系统时间:
date --iso-8601=seconds
date -u --iso-8601=seconds
TZ=Asia/Shanghai date --iso-8601=seconds
timedatectl status
timedatectl show -p Timezone -p NTP -p NTPSynchronized前两条把同一套系统时钟的读数按本地时区和 UTC 显示,连续执行本身也会经过少量时间。第三条只给这一次 date 命令指定时区,不修改系统时区,也不把机器的时钟往前拨八小时。时区负责把时间点转换成当地日历表示;修改时区与校准时钟不是一回事。
timedatectl 报告的时区、NTP 服务状态和是否已同步也要分别阅读。同步服务已启用,不表示此刻已经联系上可用时间源;虚拟机刚启动、网络暂时不可用或时间源被拦截时,两者可能不一致。若本机使用 systemd-timesyncd,可继续看 timedatectl timesync-status;若实际使用 chrony,则查看它自己的 chronyc tracking 与 chronyc sources -v。不要为了让某个查询命令成功,同时启动两套时间同步服务。timedatectl(1) 区分了这些状态与查询方式。
程序通常从墙上时钟取得可换算为日期的时间点;Linux 中典型接口是 CLOCK_REALTIME。它会受手工设时或同步校正影响,不能假设两次读数之差永远适合作为耗时。CLOCK_MONOTONIC 则适合衡量同一次开机中的时间间隔:不会跟随墙上时钟的突然跳变倒退,但仍可能受到时间同步的速率调整,而且通常不把系统挂起期间计入。需要把挂起时间也算进去时,还有 CLOCK_BOOTTIME 这一类时钟。clock_gettime(2) 给出了各时钟的边界。
这一区别会直接影响排障。跨机器拼接日志需要可信的墙上时钟及明确的时区;测量一次请求耗时,需要合适的单调时钟;不能用不同机器的单调时间直接相减。遇到日志时间先后异常,应核对同步与启动边界,而不是立刻推断程序“穿越”执行。练习不需要通过 date -s 或关闭 NTP 制造故障,那会同时影响日志、证书校验和计划任务。
从一次启动中找到与服务有关的日志
linuxlab.service 的标准输出、标准错误由 systemd 接到 journal。日志中的单元、进程、启动标识和时间等元数据,使查询不必仅靠在一个大文本文件里搜索服务名。
sudo journalctl -u linuxlab.service -b -n 50 --no-pager
sudo journalctl -u linuxlab.service --since '-10 min' --no-pager
sudo journalctl -u linuxlab.service -b -p warning --no-pager
sudo journalctl -u linuxlab.service -f-u 限定单元,-b 限定本次开机,-n 取末尾记录;--since 按时间缩小范围;-f 等待新日志,按 Ctrl+C 只停止跟随显示,不停止服务。-p warning 包括 warning 及更严重的级别,但它依赖记录的实际优先级:普通应用把“ERROR”写进标准输出,并不保证 journal 自动把它识别成 error 级别。因此先看完整的近期单元日志,再用级别筛选,不要把“筛选后没有记录”当成“没有错误”。
开机历史、内核日志和应用日志回答不同问题:
sudo journalctl --list-boots
sudo journalctl -u linuxlab.service -b -1 --no-pager
sudo journalctl -k -b -n 50 --no-pager
sudo journalctl -u linuxlab.service -b -o short-monotonic --no-pager-b -1 选择前一次开机,但只有那次日志仍被保留时才有内容。-k 查看内核记录,适合核对设备、文件系统或内核报告的故障,不是普通 Java 应用日志的另一份副本。short-monotonic 用开机内的相对时间显示,便于观察同次启动中的先后与间隔;比较前后两次开机时必须同时保留启动边界。查询选项详见 journalctl(1)。
本例有两条明确的记录路径:程序把启动信息、请求方法、路径和响应状态写到标准输出,处理中的 I/O 异常写到标准错误,它们经 systemd 进入 journal;正常 GET /hello 处理还会向工作目录中的 /var/lib/linuxlab/requests.log 追加一条请求记录。可以在请求后分别运行 sudo journalctl -u linuxlab.service -b -n 20 --no-pager 与 sudo tail -n 10 /var/lib/linuxlab/requests.log 比较内容。
这不是 journal 对文件做了自动采集,而是程序主动写了两个不同的输出位置。文件只记录代码中安排的请求,journal 还包含启动、退出和错误等消息。日志没有出现时,要继续区分“事件没有产生”“查错位置或启动范围”“权限看不到”和“已经被清理”,而不是先认定程序没有执行。
日志能否跨重启保留,取决于存储方式和容量
journald 的临时日志放在 /run/log/journal,重启后不保留;持久日志通常位于 /var/log/journal。Storage=auto 是否走持久存储与相应目录是否存在有关,不能只凭发行版名称假定。先查看配置来源和实际占用:
systemd-analyze cat-config systemd/journald.conf
sudo journalctl --disk-usage
sudo ls -ld /run/log/journal /var/log/journal某个目录不存在不一定是错误,正好说明不能把它视为已经建立的存储路径。cat-config 展示主配置与 drop-in,应结合后出现的有效设置阅读;--disk-usage 统计当前可见的活动及归档 journal 文件。
如果希望在这台独立练习 VM 中观察持久日志,可以增加一个专用 drop-in。先检查 /etc/systemd/journald.conf.d/60-linuxlab.conf 不存在;若已存在,先查看其用途,不覆盖它。使用 sudo install -d -m 0755 /etc/systemd/journald.conf.d 准备目录,再用 sudoedit /etc/systemd/journald.conf.d/60-linuxlab.conf 写入:
[Journal]
Storage=persistent
SystemMaxUse=200M
RuntimeMaxUse=50M这组数值只是小型练习 VM 的容量示例,而且限制的是整台机器的 journal,不是 linuxlab.service 单独的配额。正式系统还要根据磁盘空间、日志增长量和保留要求确定容量与 SystemKeepFree 等约束。应用配置后再观察:
sudo systemctl restart systemd-journald.service
sudo journalctl --flush
sudo journalctl --disk-usage
sudo ls -ld /var/log/journal--flush 在持久存储可用时把运行期日志转入持久存储;启用 persistent 后,启动早期或磁盘不可写时仍可能先使用运行期存储。需要观察跨重启保留时,先确认专用练习 VM 没有其他未完成任务、保存随机目录的实际路径,再在 VM 控制台执行 sudo reboot。重新登录后,用 sudo journalctl --list-boots 和 sudo journalctl -u linuxlab.service -b -1 --no-pager 查找前次启动记录。存储模式与容量含义见 journald.conf(5)。
撤销这个实验设置时,只移除刚创建的 60-linuxlab.conf 并重启 journald;已有持久日志不会因此消失,而且目录仍存在时,默认 auto 仍可能继续持久存储。回退配置与删除历史日志是两项不同操作,不应为了“清理干净”顺手删除 /var/log/journal。若实验前已经建立 VM 快照,需要恢复初始环境时可在保留所需记录后使用该快照。
日志轮转是把当前写入文件切换为归档,再开始新文件;清理则决定哪些归档可以删除。journal 自己管理二进制文件,不应交给 logrotate 直接切割。journalctl --rotate 可请求轮转;--vacuum-size 只清理归档文件,活动文件仍计入总占用,因此不能理解为“无条件把整个目录压到指定大小”。清理会丢失历史证据,应先确认保留需求;日常实验只读查看占用即可。
传统文本日志的轮转还涉及应用持有的文件描述符:把日志重命名后,长时间持有该文件的进程仍可能写向原来的文件对象,需要按程序支持的方式重新打开日志。本例的 Files.writeString 每次追加都会打开、写入并关闭文件,不持有跨请求的日志文件描述符;其他应用的日志库未必如此。
copytruncate 避开了重新打开的要求,但复制与截断之间存在可能丢日志的窗口,不能视为无损替代。若已安装 logrotate,可用只读调试模式 sudo logrotate --debug /etc/logrotate.conf 阅读它将如何处理现有规则;不要对这个 Java 练习服务随意发送一个未经支持的信号。logrotate(8) 说明了这些轮转方式。
定时执行首先需要一个能独立运行的任务
计划任务不继承交互终端里已经切换的目录、临时设置的变量和输入密码的机会。先把任务做成身份明确、使用绝对路径、退出状态可检查的一次性程序,再给它增加时钟。
下面的任务只打印执行身份、工作目录与内核的运行时间,不发送网络请求,不删除数据。继续使用已有的 linuxlab-svc 用户、/opt/linux-lab/app 程序目录与 /var/lib/linuxlab 数据目录。先确认目标不存在,避免覆盖前面建立的文件:
sudo test ! -e /opt/linux-lab/app/show-runtime
echo $?结果为 0 才继续;否则查看已有文件并停止这个新建步骤。用 sudoedit /opt/linux-lab/app/show-runtime 写入完整内容:
#!/bin/sh
set -eu
/usr/bin/printf 'user=%s directory=%s\n' "$(/usr/bin/id -un)" "$PWD"
/usr/bin/printf 'uptime: '
/usr/bin/cat /proc/uptime随后设置为 root 持有、服务用户可执行,并在缩小的环境中手动运行:
sudo chown root:root /opt/linux-lab/app/show-runtime
sudo chmod 0755 /opt/linux-lab/app/show-runtime
sudo -u linuxlab-svc -- /usr/bin/env -i PATH=/usr/bin:/bin \
/bin/sh -c 'cd /var/lib/linuxlab && /opt/linux-lab/app/show-runtime'
echo $?输出应包含 user=linuxlab-svc directory=/var/lib/linuxlab,最后一行包含 /proc/uptime 的两个数值;它们会随机器运行变化,不应照着固定数字核对。退出状态应为 0。env -i 是对“没有当前交互环境帮助时能否执行”的一次检查,并非宣称 cron 与 systemd 的真实环境都完全为空。手动执行失败时先解决身份、目录或文件权限,不要把它丢给定时器等待奇迹。
cron 的时间字段不包含执行环境
cron 适合按日历字段触发简短任务,但每条规则都需要说清谁执行、从哪里找命令、输出去哪里。Ubuntu 24.04 的默认 cron 软件包与某些系统上的 cronie 不是同一个软件包,查手册前可用 dpkg-query -W cron 确认已安装实现;没有安装时,下面先作为语法阅读,实际定时练习使用随后已有 systemd 提供的 timer。包信息见 Ubuntu noble cron。
用户通过 crontab -e 编辑自己的任务,管理员可以通过 sudo crontab -u linuxlab-svc -e 编辑服务用户的任务。下面的例子需要写入 /var/lib/linuxlab,因此若实际安装,应选择后者,由 linuxlab-svc 执行。用户 crontab 的每个任务行是“五个时间字段加命令”,执行用户就是这份 crontab 的所有者:
SHELL=/bin/sh
PATH=/usr/bin:/bin
*/5 * * * * cd /var/lib/linuxlab && /usr/bin/flock -n /var/lib/linuxlab/show-runtime.lock /opt/linux-lab/app/show-runtime >> /var/lib/linuxlab/show-runtime.log 2>&1五个字段依次是分钟、小时、月中的日期、月份、星期。这里的 */5 表示匹配每小时中分钟数为 0、5、10 等时刻,不是“上一次结束五分钟以后”。用户 crontab 中不能在时间字段后再插入 linuxlab-svc,否则它会被当成命令的一部分。
/etc/crontab 与 /etc/cron.d/ 中的系统格式则多一个用户名字段,例如下面这一行的 linuxlab-svc 才是执行身份:
*/5 * * * * linuxlab-svc cd /var/lib/linuxlab && /usr/bin/flock -n /var/lib/linuxlab/show-runtime.lock /opt/linux-lab/app/show-runtime >> /var/lib/linuxlab/show-runtime.log 2>&1系统文件也要写自己的 SHELL、PATH 等环境设置,不能假设 /etc/cron.d/ 中的文件继承 /etc/crontab 的变量。这两种格式二选一,后面的 timer 又是另一种选择,不要把同一个任务同时安装三份。Ubuntu 使用的 Debian cron 系列语法可对照 crontab(5) 与 cron(8)。
示例先显式进入数据目录,再使用绝对程序路径。flock -n 在锁已被同机另一次任务持有时立即失败,从而跳过这次执行;任务退出后锁由内核释放,锁文件本身仍存在是正常现象。锁保护的是所有执行者共同遵守的本机锁约定,不是跨机器的分布式锁。若希望“未执行”也可观测,需要由任务包装逻辑记录这个状态,而不是把它当成成功。其锁与退出状态语义见 flock(1)。
>> ... 2>&1 明确保存输出和错误,不依赖本机已经配置可投递邮件;这个文本文件之后也需要轮转。cron 不会自动装载交互 Bash 的初始化文件,行内未转义的 % 还有特殊含义,所以包含日期格式、复杂引用或多步处理时,应放进脚本,不继续拉长 cron 命令行。若实际安装了本例的服务用户任务,以 sudo crontab -u linuxlab-svc -l 核对;停止时用 sudo crontab -u linuxlab-svc -e 只删除本次新增规则,不使用 crontab -r 清空整份任务表。
用 oneshot 和 timer 分别表达任务与触发
systemd 把“运行什么”放进 service 单元,把“何时请求运行”放进 timer 单元。同一个任务因此能先手动启动、查看日志与退出结果,再按相同身份和环境进入调度。
先分别检查以下两个单元文件都不存在;若有同名文件,停止新建并核对来源:
sudo test ! -e /etc/systemd/system/linuxlab-sample.service
echo $?
sudo test ! -e /etc/systemd/system/linuxlab-sample.timer
echo $?两个结果均为 0 时,用 sudoedit /etc/systemd/system/linuxlab-sample.service 写入:
[Unit]
Description=Show Linux lab runtime information
[Service]
Type=oneshot
User=linuxlab-svc
Group=linuxlab-svc
WorkingDirectory=/var/lib/linuxlab
ExecStart=/opt/linux-lab/app/show-runtime
StandardOutput=journal
StandardError=journal
TimeoutStartSec=10s
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=trueType=oneshot 表示这是一段会结束的工作,不是要常驻的服务器。这里不设置 RemainAfterExit=yes:成功执行完后单元回到 inactive,下一次才能再次运行。ProtectSystem=strict 让这个单元看到的文件系统按相应保护规则只读;本任务只读 /proc/uptime 并把输出交给 journal,不需要额外开放数据写权限。它不意味着所有应用都能原样使用这份限制。
再用 sudoedit /etc/systemd/system/linuxlab-sample.timer 写入:
[Unit]
Description=Run Linux lab runtime sample periodically
[Timer]
OnActiveSec=10s
OnUnitInactiveSec=1min
AccuracySec=1s
Unit=linuxlab-sample.service
[Install]
WantedBy=timers.target先只加载、检查并手动运行任务,不启动 timer:
sudo systemd-analyze verify \
/etc/systemd/system/linuxlab-sample.service \
/etc/systemd/system/linuxlab-sample.timer
sudo systemctl daemon-reload
sudo systemctl start linuxlab-sample.service
systemctl show linuxlab-sample.service \
-p ActiveState -p Result -p ExecMainStatus
sudo journalctl -u linuxlab-sample.service -b -n 20 --no-pager手动运行的预期是 Result=success、ExecMainStatus=0,日志中的身份和目录与刚才相同。完成后 ActiveState=inactive 是正常结果,不能要求一个已经结束的 oneshot 永远显示 running。若启动失败,先看此单元的状态和 journal,修复后重新手动启动;只有手动结果正确再继续。
然后启用定时触发:
sudo systemctl enable --now linuxlab-sample.timer
systemctl list-timers --all linuxlab-sample.timer
sudo journalctl -u linuxlab-sample.service -fOnActiveSec=10s 提供 timer 本次启动后的首次触发点;OnUnitInactiveSec=1min 让任务结束后约一分钟再次触发。两条规则可以共同触发,不是先后执行的 Shell 语句。刚才手动运行形成的任务时间状态也可能参与下一次触发计算,所以以 list-timers 的实际 NEXT/LAST 和日志为准,不把秒表上的整十秒当成唯一核对标准。观察至少两次新的执行记录后,按 Ctrl+C 退出跟随查看。
AccuracySec=1s 缩小调度合并窗口,不是硬实时保证;系统繁忙时任务仍可能晚些获得运行机会。timer 请求启动的仍是同一个 service 单元,若它还在执行,systemd 不会为同一个单元再并行拉起一份。这里又使用“从上次结束计时”,天然适合避免同一任务重叠;如果另一个脚本绕开单元直接运行,仍需另行考虑共用锁与业务幂等性。
日历触发、补触发与失败重试不能混为一谈
前面的 timer 按相对时间工作,不依赖时区。每天某个当地时刻执行则属于日历任务,可以把 [Timer] 中的两个相对时间选项替换为:
OnCalendar=*-*-* 03:00:00
Persistent=true这是另一种调度方案,不是要求在当前短周期实验中再叠加一套时间规则。不写时区时使用系统本地时区;可先用 systemd-analyze calendar '*-*-* 03:00:00' 检查解析后的表达式和下次时刻。日历调度前还应确认系统时钟与同步状态可信。
Persistent=true 只对 OnCalendar 生效:systemd 保存上次触发的时间,timer 重新进入活动状态时,如果停用或关机期间原本应触发过,便补请求一次运行;并非把错过的每个周期排队重放,也不是给失败任务自动重试。实际业务若要求逐日补齐数据,需要任务自己记录和处理缺失区间。启用随机延迟时,补触发也会受其影响。相对时间、日历时间、精度与持久补触发的完整定义见 systemd v255 timer 手册源码。
普通 cron 不能被当成持久化作业队列;停机期间遗漏的任务通常不由该条 crontab 自行逐条补跑。anacron、发行版安排的周期任务与 cron 对较小时钟跳变的处理又有各自语义,不能把某一种实现的补偿行为套到所有计划任务上。
要检查短周期实验确实停止,先停 timer,再停可能仍在运行的任务:
sudo systemctl disable --now linuxlab-sample.timer
sudo systemctl stop linuxlab-sample.service
systemctl is-enabled linuxlab-sample.timer
systemctl is-active linuxlab-sample.timer
systemctl list-timers --all linuxlab-sample.timer此时预期是 disabled、inactive,前两个状态查询返回非零不代表停止失败。再查看任务日志,并观察超过原来的一分钟间隔,确认没有新触发。已加载但不活动的 timer 可能仍可被查询,不能只凭列表里还看到名称就认定它在运行。
若曾切换到 Persistent=true 的日历方案,卸载前在停止 timer 后执行 sudo systemctl clean --what=state linuxlab-sample.timer 清理本单元记录的触发时间。然后只删除本节创建的三个已确认文件,保留 HTTP 服务和它的数据目录:
sudo rm -i /etc/systemd/system/linuxlab-sample.timer \
/etc/systemd/system/linuxlab-sample.service \
/opt/linux-lab/app/show-runtime
sudo systemctl daemon-reload
systemctl list-timers --all linuxlab-sample.timerrm -i 会逐个确认路径;不要改成删除整个 /etc/systemd/system 或 /opt/linux-lab。若另外试用了 cron,应先移除那条规则并确认它不再触发,再决定是否保留或删除本次产生的 show-runtime.log、show-runtime.lock。保留旧 journal 记录不会让已卸载的任务复活;调度来源、运行实例和历史证据本来就是三种不同的对象。
常见故障的定位与后续学习
排查从失败的具体操作开始。记录执行了什么命令、使用哪个身份、访问哪个路径或地址,以及原始错误;再选择能解释这个错误的系统对象。一次失败可能经过多个层次,但首轮检查不需要把所有工具都运行一遍。
| 首先观察到的现象 | 应先区分的原因 | 回到对应操作 |
|---|---|---|
| 命令找不到,或文件存在却无法执行 | Shell 解析、PATH、执行权限、解释器与加载器 | 软件包、命令与运行环境 |
| 普通用户可读,服务用户失败 | UID/GID、路径每层搜索权限、ACL 与服务隔离设置 | 用户身份与访问权限 |
| systemctl 启动失败 | unit 准备阶段错误、程序自己的启动异常、启动限速 | 复现一次准备阶段失败并恢复 |
| 服务 active,请求仍失败 | 程序是否已监听、绑定地址、请求目标及实际返回 | 网络配置与服务访问 |
| 写文件失败或空间统计不一致 | 挂载对象、字节容量、inode、配额、已打开的删除文件 | 文件系统与存储空间 |
| 请求变慢或进程被结束 | CPU 时间、等待、可用内存、交换、IO 和当前限制 | CPU、内存与运行限制 |
| 定时任务没有预期结果 | 触发器状态、运行身份和环境、程序退出、日志保存 | 时间、日志与计划任务 |
修复后重复最初失败的操作,并保持相同身份和目标。例如修复 linuxlab 的目录权限后仍用服务身份读写,修复启动配置后仍请求 /hello;临时切成 root 或换到另一地址获得的成功,尚未回答原来的问题。
结束服务练习
需要保留练习机时,可以保留源码和服务,方便重复观察。准备清理时,先按计划任务与资源章节撤销本轮 timer、脚本和额外 drop-in,再停用 HTTP 服务:
sudo systemctl disable --now linuxlab.service
systemctl is-active linuxlab.service
sudo ss -lntp 'sport = :18080'正常停止后 is-active 显示 inactive 并返回非零状态,本次监听消失。若仍有监听,先确认进程来源;它可能是先前未关闭的手动进程,不按端口号批量终止程序。
执行 sudo find /opt/linux-lab /etc/linux-lab /var/lib/linuxlab /etc/systemd/system/linuxlab.service.d -maxdepth 2 -type f,核对这些确实是练习创建的对象。不存在的 drop-in 目录可以忽略;仍有其他文件时,先辨认内容和保留需要,再继续。下面只删除已明确创建的文件:
sudo rm -i /etc/systemd/system/linuxlab.service
sudo rm -i /opt/linux-lab/app/LinuxHello.class /etc/linux-lab/app.properties /var/lib/linuxlab/requests.log
sudo rmdir /opt/linux-lab/app /opt/linux-lab /etc/linux-lab /var/lib/linuxlab
sudo systemctl daemon-reload空的 linuxlab.service.d 可单独使用 rmdir 移除;非空目录保留检查。源码目录位于最初打印出的 linux-build.随机后缀,重启后原变量已经丢失,应按保存的实际路径重新找到它,不用通配符批量清理。服务账号暂留不会启动进程;确认它只属于本练习、已无运行进程及其他文件用途后,才考虑使用 userdel 移除账号。
将基础操作用于更具体的任务
需要从另一台机器长期登录时,继续处理 SSH 的身份认证、主机密钥和文件传输;需要把一组命令变成可重复任务时,再使用 Shell 自动化处理参数、错误和并发。
生产服务器的账号基线、真实磁盘布局和时间源配置见服务器初始化。公网访问涉及路由、防火墙或云平台规则时,使用网络排障;域名委派与缓存变更则查域名与 DNS。这里的文件权限、进程身份和监听地址,是继续判断这些问题时仍需保持的基础。
一个应用由多台主机运行以后,日志收集和整套服务恢复会形成新的任务,可继续查日志体系与备份和恢复。Java 应用需要制品发布、配置切换和正式运行维护时,接到 Spring Boot 部署。
权威资料与规范地址
系统安装与介质
- Ubuntu 发布周期:https://ubuntu.com/about/release-cycle
- Ubuntu 24.04 x86-64 介质:https://releases.ubuntu.com/24.04/
- Ubuntu 24.04 其他架构介质:https://cdimage.ubuntu.com/ubuntu/releases/24.04/release/
- Ubuntu 安装镜像真实性校验:https://documentation.ubuntu.com/security/software-integrity/image-verification/
- PowerShell 文件摘要:https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-filehash?view=powershell-5.1
- Hyper-V 创建虚拟机:https://learn.microsoft.com/en-us/windows-server/virtualization/hyper-v/get-started/create-a-virtual-machine-in-hyper-v
- Ubuntu 在 Hyper-V 上的安装说明:https://ubuntu.com/server/docs/how-to/virtualisation/ubuntu-on-hyper-v/
- Subiquity 安装器逐屏说明:https://canonical-subiquity.readthedocs-hosted.com/en/latest/tutorial/screen-by-screen.html
- Subiquity 存储配置:https://canonical-subiquity.readthedocs-hosted.com/en/latest/howto/configure-storage.html
目录、Shell 与文件工具
- 文件系统目录层次规范:https://refspecs.linuxfoundation.org/FHS_3.0/fhs/index.html
- Ubuntu 24.04 Bash 手册:https://manpages.ubuntu.com/manpages/noble/man1/bash.1.html
- 临时文件与目录:https://manpages.ubuntu.com/manpages/noble/man1/mktemp.1.html
- nano 编辑器:https://www.nano-editor.org/dist/latest/nano.html
- grep 搜索与退出状态:https://manpages.ubuntu.com/manpages/noble/man1/grep.1.html
- GNU tar 归档:https://www.gnu.org/software/tar/manual/html_node/index.html
软件获取时的网络检查
- ip 地址与路由查询:https://manpages.ubuntu.com/manpages/noble/man8/ip.8.html
- getent 系统名称查询:https://manpages.ubuntu.com/manpages/noble/man1/getent.1.html
用户身份、文件权限与安全策略
- 进程用户与组标识:https://manpages.ubuntu.com/manpages/noble/man7/credentials.7.html
- sudo 身份与授权:https://manpages.ubuntu.com/manpages/noble/man8/sudo.8.html
- 路径逐级解析:https://manpages.ubuntu.com/manpages/noble/man7/path_resolution.7.html
- 文件名删除条件:https://manpages.ubuntu.com/manpages/noble/man2/unlink.2.html
- useradd 账号创建:https://manpages.ubuntu.com/manpages/noble/man8/useradd.8.html
- nologin 登录 Shell:https://man7.org/linux/man-pages/man8/nologin.8.html
- namei 路径展开:https://manpages.ubuntu.com/manpages/noble/man1/namei.1.html
- ACL 访问检查:https://man7.org/linux/man-pages/man5/acl.5.html
- setfacl 与 mask:https://manpages.ubuntu.com/manpages/noble/man1/setfacl.1.html
- umask 与默认 ACL:https://manpages.ubuntu.com/manpages/noble/man2/umask.2.html
- Ubuntu AppArmor 管理:https://ubuntu.com/server/docs/how-to/security/apparmor/
- AppArmor 运行模式:https://manpages.ubuntu.com/manpages/noble/man7/apparmor.7.html
- Rocky SELinux:https://docs.rockylinux.org/guides/security/learning_selinux/
软件包与程序执行
- APT 软件源格式:https://manpages.ubuntu.com/manpages/noble/man5/sources.list.5.html
- APT 仓库信任与验证:https://manpages.ubuntu.com/manpages/noble/man8/apt-secure.8.html
- apt-get 刷新、模拟与安装:https://manpages.ubuntu.com/manpages/noble/man8/apt-get.8.html
- apt-cache 候选版本查询:https://manpages.ubuntu.com/manpages/noble/man8/apt-cache.8.html
- dpkg-query 安装状态与文件归属:https://manpages.ubuntu.com/manpages/noble/man1/dpkg-query.1.html
- 程序执行与解释器错误:https://man7.org/linux/man-pages/man2/execve.2.html
- apt 交互管理:https://manpages.ubuntu.com/manpages/noble/man8/apt.8.html
- Rocky 软件包管理:https://docs.rockylinux.org/books/admin_guide/13-softwares/
进程与服务管理
- ps 状态与字段:https://manpages.ubuntu.com/manpages/noble/man1/ps.1.html
- 信号与默认动作:https://manpages.ubuntu.com/manpages/noble/man7/signal.7.html
- systemd 启动关系:https://manpages.ubuntu.com/manpages/noble/man7/bootup.7.html
- Ubuntu OpenJDK 21 包:https://packages.ubuntu.com/noble/openjdk-21-jdk-headless
- JDK HTTP Server API:https://docs.oracle.com/en/java/javase/21/docs/api/jdk.httpserver/com/sun/net/httpserver/HttpServer.html
- 安装文件与权限:https://manpages.ubuntu.com/manpages/noble/man1/install.1.html
- systemd 服务类型与生命周期:https://manpages.ubuntu.com/manpages/noble/man5/systemd.service.5.html
- systemd 执行环境与错误:https://manpages.ubuntu.com/manpages/noble/man5/systemd.exec.5.html
- systemctl 管理操作:https://manpages.ubuntu.com/manpages/noble/man1/systemctl.1.html
文件系统、挂载与链接
- 块设备及字段:https://manpages.ubuntu.com/manpages/noble/man8/lsblk.8.html
- ext4 文件系统日志:https://docs.kernel.org/filesystems/ext4/journal.html
- XFS 文件系统日志:https://docs.kernel.org/filesystems/xfs/xfs-delayed-logging-design.html
- 挂载查询与配置检查:https://manpages.ubuntu.com/manpages/noble/man8/findmnt.8.html
- 持久挂载字段:https://manpages.ubuntu.com/manpages/noble/man5/fstab.5.html
- 服务挂载依赖:https://manpages.ubuntu.com/manpages/noble/man5/systemd.unit.5.html
- 挂载选项:https://manpages.ubuntu.com/manpages/noble/man8/mount.8.html
- tmpfs 容量与生命周期:https://docs.kernel.org/filesystems/tmpfs.html
- inode 元数据:https://manpages.ubuntu.com/manpages/noble/man7/inode.7.html
- 硬链接与符号链接:https://manpages.ubuntu.com/manpages/noble/man1/ln.1.html
- 文件状态查询:https://manpages.ubuntu.com/manpages/noble/man1/stat.1.html
- 文件内存映射生命周期:https://manpages.ubuntu.com/manpages/noble/man2/mmap.2.html
- 文件系统容量:https://manpages.ubuntu.com/manpages/noble/man1/df.1.html
- 文件占用统计:https://manpages.ubuntu.com/manpages/noble/man1/du.1.html
- 打开文件的查询与筛选:https://manpages.ubuntu.com/manpages/noble/man8/lsof.8.html
指标、限制与内存不足
- 可用处理单元:https://manpages.ubuntu.com/manpages/noble/man1/nproc.1.html
- 负载平均值:https://manpages.ubuntu.com/manpages/noble/man1/uptime.1.html
- vmstat 采样字段:https://manpages.ubuntu.com/manpages/noble/man8/vmstat.8.html
- top 显示和 CPU 口径:https://manpages.ubuntu.com/manpages/noble/man1/top.1.html
- 进程及内存内核接口:https://docs.kernel.org/filesystems/proc.html
- free 内存字段:https://manpages.ubuntu.com/manpages/noble/man1/free.1.html
- 设备 I/O 指标:https://manpages.ubuntu.com/manpages/noble/man1/iostat.1.html
- 进程采样指标:https://manpages.ubuntu.com/manpages/noble/man1/pidstat.1.html
- 资源压力统计:https://docs.kernel.org/accounting/psi.html
- 进程资源软硬限制:https://manpages.ubuntu.com/manpages/noble/man2/getrlimit.2.html
- systemd 服务组资源控制:https://manpages.ubuntu.com/manpages/noble/man5/systemd.resource-control.5.html
- cgroup v2 计费与事件:https://docs.kernel.org/admin-guide/cgroup-v2.html
- 用户空间 OOM 管理:https://manpages.ubuntu.com/manpages/noble/man8/systemd-oomd.service.8.html
网络地址与名称查询
- Ubuntu 网络配置:https://ubuntu.com/server/docs/explanation/networking/configuring-networks/
- Netplan 配置与试用回滚:https://netplan.readthedocs.io/en/stable/netplan-yaml/、https://netplan.readthedocs.io/en/stable/netplan-try/
- 地址、路由与监听:https://manpages.ubuntu.com/manpages/noble/man8/ip-address.8.html、https://manpages.ubuntu.com/manpages/noble/man8/ip-route.8.html、https://manpages.ubuntu.com/manpages/noble/man8/ss.8.html、https://man7.org/linux/man-pages/man7/ipv6.7.html
- NSS、系统解析服务与 DNS 查询:https://manpages.ubuntu.com/manpages/noble/man5/nsswitch.conf.5.html、https://manpages.ubuntu.com/manpages/noble/man1/resolvectl.1.html、https://bind9.readthedocs.io/en/v9.18.39/manpages.html#dig-dns-lookup-utility
- 受控请求与防火墙检查:https://curl.se/docs/manpage.html、https://manpages.ubuntu.com/manpages/noble/man8/ufw.8.html
时钟与日志保存
- 时钟与同步状态:https://manpages.ubuntu.com/manpages/noble/man2/clock_gettime.2.html、https://manpages.ubuntu.com/manpages/noble/man1/timedatectl.1.html
- journal 与文本日志轮转:https://manpages.ubuntu.com/manpages/noble/man1/journalctl.1.html、https://manpages.ubuntu.com/manpages/noble/man5/journald.conf.5.html、https://manpages.ubuntu.com/manpages/noble/man8/logrotate.8.html
计划任务与触发
- cron 包与对应语法:https://packages.ubuntu.com/noble/cron、https://manpages.debian.org/bookworm/cron/crontab.5.en.html、https://manpages.debian.org/bookworm/cron/cron.8.en.html
- 本机任务互斥与 timer 语义:https://manpages.ubuntu.com/manpages/noble/man1/flock.1.html、https://raw.githubusercontent.com/systemd/systemd/v255/man/systemd.timer.xml
