主题
Linux:从一个故障问题出发,选命令、读指标、写可控脚本
“CPU 高”“内存满”“端口不通”都还不是诊断。真正有用的下一句话是:哪个进程、哪段时间、哪种资源、看到了什么证据。Linux 命令很多,先记住每个工具能回答的问题,再记参数,比背一串命令更容易用对。
本文按排查顺序介绍 top、ps、ss / netstat、lsof、strace、tcpdump、df、free、vmstat、iostat,再讲权限、systemd 与 Shell 脚本。实际输出来自当前 Linux/amd64 环境,只查看自身测试进程和临时工作目录;没有附着生产进程、抓取业务流量或修改系统服务。工具缺失、权限不足也按真实结果列出。
先选问题,再选工具
| 你现在要知道什么 | 第一个入口 | 继续追问 |
|---|---|---|
| 哪个进程在消耗 CPU 或内存 | top、ps | 是持续负载,还是瞬时采样? |
| 端口是否监听、连接处于什么状态 | ss,旧环境可能用 netstat | 监听在哪个地址、属于哪个网络命名空间? |
| 哪个进程持有文件或套接字 | lsof | 是路径还在,还是已删除但仍打开? |
| 程序卡在哪个系统调用 | strace | 跟踪权限、开销和敏感输出能否接受? |
| 某类包是否真的到达接口 | tcpdump | 抓包范围和保留方式是否足够小? |
| 某路径所在文件系统还有多少空间 | df | 是字节不足,还是 inode 不足? |
| 系统内存能否再满足分配 | free | 看 available,还是误看了 free? |
| CPU、换页、运行队列的整体变化 | vmstat | 是否连续出现,而非单个样本? |
| 块设备延迟和队列是否异常 | iostat | 是否与实际服务使用的设备对应? |
这些入口分别依据下文各命令的官方手册。它们是一组互补视角,没有一个工具能独立证明应用健康。
正在绘制图示…
查看图源代码
flowchart TB
P["现象:请求变慢"] --> S["限定时间窗口与目标进程"]
S --> A["top / ps:谁在忙、谁在等"]
S --> B["free / vmstat / df:是否资源压力"]
S --> C["ss / lsof:连接与文件归谁"]
A --> H["形成具体假设"]
B --> H
C --> H
H --> V["小范围验证:调用栈、strace、iostat 或 tcpdump"]
V --> R["核对请求延迟与错误是否同步变化"]
图中的先后是一种保守排查建议:先做低干扰观察,再使用可能有开销或暴露数据的工具。不要一上来就全机抓包或重启服务,那会改变现场。
CPU 与进程:top 看变化,ps 留快照
top:确认是谁忙,以及忙在哪一类 CPU 时间
top 提供动态的系统与进程视图。常用操作是按 CPU 或内存排序、切换线程显示;脚本采样用批处理 -b,用 -n 限定次数,用 -p 缩小到指定 PID。top(1) COMMAND-LINE、FIELDS
| 字段 | 读法 | 常见误解 |
|---|---|---|
%CPU | 在工具的计量模式与采样范围内消耗的 CPU 时间 | 多线程进程超过 100% 不一定有错 |
VIRT | 虚拟地址空间规模 | 不等于实际独占的物理内存 |
RES | 当前驻留内存 | 不等于只归本进程所有的页面 |
us / sy | 用户态 / 内核态 CPU 时间 | 内核态高不自动说明系统调用有 bug |
wa | CPU 统计中的 I/O 等待时间口径 | 不是“某进程等待 I/O 的百分比” |
| load average | 一段时间内的系统负载概览 | 不是 CPU 使用率百分比 |
top 的 CPU 百分比还受 Irix/Solaris 模式等设置影响,比较不同截图前先确认口径;load 涉及可运行和不可中断等待任务,不能单凭负载数值决定加 CPU。top(1) SUMMARY、%CPU、LOAD Averages
本次对一个刚创建的 Python 测试进程运行 top -b -n 1 -p 33,其行中 %CPU 为 0.0,状态为 S。只能说这个采样时刻它主要在等待;单个样本不足以证明它从未占用 CPU。要看变化,应取固定间隔的多个样本并与请求慢的时间对齐。
ps:一次性确认身份、父子关系和状态
ps 更适合稳定地选出特定字段。ps -p PID -o pid,ppid,stat,pcpu,pmem,rss,vsz,comm 查看进程快照;ps -L -p PID 展开线程。选择 comm 可以只看可执行命令名,避免默认把带令牌的完整命令行复制进日志。ps(1) OUTPUT FORMAT CONTROL、STANDARD FORMAT SPECIFIERS
本次输出:
text
PID PPID STAT %CPU %MEM RSS VSZ COMMAND
33 9 S 100 0.0 9304 36128 python3这里 ps 的 %CPU=100,稍后的 top 却是 0。不是两个工具矛盾:ps 的 CPU 利用率按进程已消耗 CPU 时间与存活时间计算,刚启动的进程和短时间窗口尤其容易出现这种差别;top 常用刷新间隔口径。RSS、VSZ 在这份输出中以 KiB 表示。ps(1) NOTES、%cpu、rss、vsz
常见状态包括 R(运行或可运行)、S(可中断睡眠)、D(不可中断睡眠)、Z(僵尸)。S 很常见,例如服务在等请求,不应当作异常;Z 则提示退出状态尚未被父进程回收。D 也需要结合具体等待点,不能见到它就认定磁盘损坏。ps(1) PROCESS STATE CODES
套接字与文件:先确认对象真的存在
ss 与 netstat:连接状态不是业务状态
ss -ltn 查看 TCP 监听端口:-l 只看监听,-t 选 TCP,-n 用数字表示地址端口。ss -tan 看 TCP 各状态,-p 请求显示进程信息,-i 请求内部 TCP 信息。知道目标端口时,使用过滤表达式缩小范围。ss(8) OPTIONS、FILTER
| 命令入口 | 要回答的问题 | 边界 |
|---|---|---|
ss -ltn | 哪些 TCP 地址正在监听 | 不等于防火墙放行或应用已就绪 |
ss -tan | 哪些连接在哪种 TCP 状态 | ESTABLISHED 不证明业务能完成 |
ss -tin | 重传、窗口等 TCP 信息线索 | 字段依内核与权限,单次值要有上下文 |
netstat -lnt | 旧工具查看 TCP 监听 | 常来自 net-tools,未必已安装 |
netstat 手册明确建议套接字场景使用 ss;不能因熟悉旧命令就假设所有发行版都预装它。两者输出与实现不同,迁移脚本时应重写解析逻辑,别只替换命令名。netstat(8) NOTES
本机确实创建了 127.0.0.1 的随机端口监听。ss -ltn 'sport = :48043' 返回:
text
Cannot open netlink socket: Operation not permitted
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 0 127.0.0.1:48043 0.0.0.0:*这个例子重要的部分是警告。虽然命令退出状态为 0,而且列出了监听行,netlink 信息获取仍被限制;不能把这里的队列数或缺少字段当成完整内核诊断。工具“能运行”和“所有观察能力都可用”是两回事。
正常权限下,队列列也要按连接状态解释:已建立 TCP 连接的收发队列与监听套接字的队列含义不同。先读该版本文档和表头,再讨论积压;不要把监听行的 Send-Q 直接当成尚未发出的业务字节。ss(8)、netstat(8) OUTPUT
lsof:把进程与资源连起来
Linux 里的“打开文件”不只有磁盘文件,还包括管道和套接字。lsof -p PID 以进程为中心查看打开对象;lsof -nP -a -p PID -iTCP 把 PID 条件和 TCP 条件取交集,-nP 避免名字和服务名解析。lsof(8) 的 -a、-i、-n、-P
本次同一个 Python 进程的结果:
text
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
python3 33 agent 3u IPv4 399577 0t0 TCP 127.0.0.1:48043 (LISTEN)这把前面看到的端口与 PID 33 的 fd 3 关联起来,u 表示以读写方式打开。它证明这个进程持有监听套接字,仍不证明处理请求的业务函数正常。
排查“删了文件空间没回来”时,可以用 lsof +L1 找链接数小于 1、仍打开的文件;应再按 PID 或路径缩小,而不是直接关闭未知进程。大范围 lsof 可能读取很多进程信息、受权限限制,结果不是无条件完整。lsof(8) 的 +L、SECURITY
进一步取证:strace 与 tcpdump 都要限制范围
strace:看系统调用,不是 Go 函数调用树
strace 记录进程的系统调用、参数、返回值和信号。-f 跟随新建的子进程等,-e trace=... 筛选调用,-T 显示调用耗时,-o 写日志;-p PID 则附着已有进程,需要相应权限。strace(1) DESCRIPTION、OPTIONS
适合的问题是“程序在等网络 read,还是反复打开不存在的文件”“系统调用返回了什么错误”。如果瓶颈在用户态计算,系统调用少,strace 可能看不出原因;如果高频调用很多,跟踪本身又会显著扰动时序。调用耗时包含该调用期间的等待,不能直接等同内核 CPU 用时。
本次只尝试跟踪自建的操作系统实验程序,仍得到:
text
strace: ptrace(PTRACE_TRACEME, ...): Operation not permitted因此没有系统调用跟踪结果,也没有为完成实验去放宽环境安全策略。查看错误后,继续用程序输出和官方接口文档验证可验证的部分。线上需要使用时,应先确认权限和开销,限定进程、调用类别、时长和日志大小;参数、路径、读写内容都可能包含敏感数据。strace(1) DIAGNOSTICS、BUGS
tcpdump:包到了哪张网卡,不等于应用处理了什么
tcpdump 使用捕获过滤表达式筛选包。常用入口为 -i 指定接口,-nn 不解析名称,-c 限定包数,-s 限定每包捕获长度,-w 保存原始捕获文件,-r 读取已有捕获文件。tcpdump(8) OPTIONS
仅在已获授权的本机实验里,一个小范围入口可以是:
sh
# 示范命令,本文没有执行;PORT 必须是自己的测试端口
tcpdump -i lo -nn -c 20 -s 96 "tcp port $PORT"它有接口、端口、数量和截取长度约束,但 96 字节仍可能包含部分负载,不能声称这样就绝无敏感数据;没有包时 -c 也不会自动按时间结束。正式诊断还要设置外层时长限制、确定保留位置和删除计划。
抓包适合检查握手是否往返、重传是否出现、哪一段路径有回应。TLS 加密后的数据包通常不能直接显示业务正文,抓包也看不到服务端数据库是否提交。抓取位置、网卡卸载和捕获丢包还可能改变观察结果,尤其不能把工具丢包统计等同于业务网络丢包。tcpdump(8) DESCRIPTION、OUTPUT FORMAT
本环境未安装 tcpdump,本文没有生成或伪造 pcap。
容量与压力:df、free、vmstat、iostat 各看一层
df:查文件系统,不是目录里所有文件的简单相加
df -h PATH 看该路径所在文件系统的容量,-h 使用易读单位;df -i PATH 看 inode 使用量。inode 用于记录文件等对象的元数据,可能出现容量还有很多,但可用 inode 已用尽的情况。GNU df(1)、inode(7)
本次 df -h .:
text
Filesystem Size Used Avail Use% Mounted on
overlay 32G 2.4G 28G 8% /workspace/shared这里看到的是承载工作目录的 overlay 文件系统视图,不是物理机器所有磁盘。另一个目录可能位于另一挂载点,排查写入失败时应给出实际目标路径。du 统计目录可见文件占用,与 df 的文件系统口径不同;已删除但仍打开的文件、保留空间等都可能造成差异。前述 lsof 可以帮助追查其中一种原因。df(1)、unlink(2)
free:低 free 不等于内存耗尽
free 汇总内存使用情况。free 列是当前未使用的内存,available 则是估计在不发生交换的情况下还能供新应用使用多少,考虑了可回收部分。Linux 使用空闲内存做缓存很正常,不能因缓存多就先清缓存。free(1) DESCRIPTION
本次 free -m 的 MiB 口径快照:
text
total used free shared buff/cache available
Mem: 9967 1468 5973 46 2907 8498
Swap: 0 0 0available 大于 free,是因为部分缓存等内存可被回收。这里 Swap 为 0 只说明这个观察视图没有可用交换空间,不代表其他机器也应关闭 swap。
在容器中,还要检查 cgroup 限额和压力。系统总体有空闲,不代表这个容器还能继续申请;memory.max、memory.current、memory.events 等可以解释容器级限制与 OOM 事件。Linux cgroup v2 Memory。本文没有修改 cgroup,也未做 OOM 实验。
vmstat:把资源活动放到连续时间里
vmstat 1 2 输出两次报告,间隔一秒。第一份多数速率统计是启动以来的平均,之后才是采样间隔内的统计;进程和内存字段则是相应时点状态。不要拿第一行就说“刚才一秒发生了这些事”。vmstat(8) DESCRIPTION
| 字段组 | 要看什么 | 怎么继续判断 |
|---|---|---|
r / b | 可运行任务、阻塞等待 I/O 的任务 | 与可用 CPU、请求量、等待点一起看 |
si / so | 换入、换出速率 | 持续交换和延迟恶化同时出现才更值得追查 |
bi / bo | 块设备读写流量统计 | 不是应用 read/write 调用次数 |
in / cs | 中断、上下文切换速率 | 高值本身不等于性能故障 |
us / sy / id / wa / st | 用户、内核、空闲、I/O 等待、被虚拟机环境占用的时间口径 | 结合多次样本和具体资源 |
字段含义及单位见 vmstat(8) FIELD DESCRIPTION。不同版本可能新增列,本次还出现 gu,所以脚本不应按永久固定列数盲目切分。
本次第二个样本中 r=0、b=0、si=0、so=0、cs=356、id=100。它只说明这一秒的系统视图基本空闲,没有证明服务在另一个时间窗口也没有问题。这里没有运行压力测试。
iostat:先看延迟和工作量,再解释利用率
iostat -xz 1 是常见入口:-x 输出扩展指标,-z 省略空闲设备,间隔 1 秒;-y 可省略启动以来的第一份报告。工具通常来自 sysstat,当前环境未安装,下面只解释手册字段,不给虚构输出。iostat(1)
| 指标 | 主要含义 | 不能直接推出的结论 |
|---|---|---|
r/s、w/s | 每秒完成的读写请求数 | 请求大小相同 |
| 读写 KiB/s 等 | 吞吐量 | 吞吐低必然没有压力 |
await、r_await、w_await | 请求等待加服务时间的平均量级 | 所有请求延迟都等于平均值 |
aqu-sz | 平均队列长度 | 队列越长一定越坏,仍要看并行能力和时延 |
%util | 设备有 I/O 活动的时间比例 | 现代并行设备达到 100% 就一定完全饱和 |
sysstat 手册明确提醒,并行处理请求的 RAID 和现代 SSD 等设备,%util 不能像简单串行设备那样直接解释为性能极限。iostat(1) 的 %util。应用文件经过容器、文件系统、逻辑卷后映射到哪个设备,也必须查清,否则可能看错盘。
文件权限:目录的 x,和文件的 x 不是同一个动作
传统 Unix 权限把访问者区分为拥有者、组和其他用户,用 r、w、x 表示权限。chmod 640 file 表示拥有者可读写、组只读、其他无权限;chown 改变所有者或组,改变的对象与 chmod 不同。inode(7) 的 file mode
| 权限 | 普通文件 | 目录 |
|---|---|---|
| r | 读取内容 | 列出目录项名称 |
| w | 修改内容 | 修改目录项,例如创建、删除名字,还受搜索权限等约束 |
| x | 执行文件,仍需满足格式和环境要求 | 搜索、穿过目录,访问其中已知名称 |
访问 /a/b/c 时,中间目录也要有相应搜索权限。只有最终文件可读,但上级目录不可穿过,仍会失败。删除文件主要受所在目录权限及 sticky bit 等规则控制,不是简单查看文件自身有没有 w。path_resolution(7)、unlink(2) ERRORS
新文件权限还受 umask 影响,它按位屏蔽请求的权限,并不是普通十进制减法。没有默认 ACL 等额外因素时,文件请求 0666、umask 为 0027,结果为 0640。umask(2)
本次只在临时目录实验:
sh
umask 027
printf 'hello\n' > "$work/a file.txt"
stat -c 'new file mode=%a' -- "$work/a file.txt"
chmod 600 -- "$work/a file.txt"实际结果:
text
new file mode=640
changed file mode=600
directory mode=700遇到权限错误,先查实际运行用户、目录链、传统权限、ACL、挂载和安全策略等条件。chmod -R 777 会扩大可写面,却可能仍解决不了只读挂载、SELinux 或容器约束;不应把它当通用修复。扩展访问控制和能力机制会影响传统模式位之外的结果,本文不通过修改这些策略来完成测试。
systemd:配置服务生命周期,不是把命令放到后台
systemd 使用 unit 描述服务、目标、套接字、定时器等对象。service unit 决定程序如何启动、何时算启动完成、如何重启和停止;unit 的依赖与顺序另行定义。systemd v257 systemd.service、systemd.unit
正在绘制图示…
查看图源代码
flowchart LR
U["service unit 配置"] --> M["systemd 管理器"]
M -->|ExecStart| P["主进程"]
P -->|退出状态| M
M -->|Restart 条件与频率限制| P
M -->|停止请求与 TimeoutStopSec| P
P --> J["标准输出与错误,可进入 journal"]
| 常用字段 | 做什么 | 边界 |
|---|---|---|
Type=exec | 执行程序成功后再认为启动阶段达到相应条件 | 不是业务健康检查,也不代表端口已就绪 |
ExecStart= | 启动命令 | 不默认经过 Shell,不能照抄管道、重定向 |
User= | 指定运行身份 | 用户及目录权限必须真实存在并匹配 |
WorkingDirectory= | 工作目录 | 相对路径所依赖的目录必须清楚 |
Restart=on-failure | 失败时自动重启 | 还受启动频率限制,不能修复坏配置 |
TimeoutStopSec= | 停止流程等待边界 | 超时后可能进入强制终止行为 |
After= | 同时被安排时的启动顺序 | 不单独把另一个 unit 拉起 |
Wants= / Requires= | 不同强度的依赖关系 | 依赖关系不自动等于先后顺序 |
WantedBy= | enable 时建立安装关系的目标 | 写了不等于当前已经启用 |
字段语义依据 service v257 与 unit v257。例如 After=network.target 不是“互联网已经能访问”的保证;应用仍应处理依赖尚未就绪和网络失败。
为了让配置可读,附录用 /usr/bin/sleep infinity 作为占位程序,明确不把它当 Go 服务部署。NoNewPrivileges=true、PrivateTmp=true 展示的是限制权限扩张和隔离临时目录的入口;真实服务还需要按目录写入、网络、文件等需求评估,不能复制几个选项就宣称完成安全加固。systemd v257 systemd.exec
| 管理入口 | 含义 | 是否改变运行状态 |
|---|---|---|
systemctl status NAME | 查看状态和部分日志 | 主要是读取 |
journalctl -u NAME --since '10 minutes ago' | 按服务与时间读 journal | 读取,可能受权限和保留策略限制 |
systemctl daemon-reload | 管理器重新读取 unit 配置 | 不等于重启业务进程 |
systemctl start NAME | 现在启动 | 是 |
systemctl enable NAME | 建立后续启动关系 | 不等于现在启动;--now 才兼做启动 |
systemctl restart NAME | 停止再启动 | 是,可能中断请求 |
命令行为见 systemctl v257、journalctl v257。这些是认识管理动作的入口,本次没有执行状态变更命令。
环境实际安装了 systemd 257 工具,但 PID 1 是 codex。对附录文件尝试 systemd-analyze verify --man=no 又失败于:
text
Failed to create directory '/run/systemd/': Read-only file system所以本文既没有运行该 service,也没有宣称静态校验通过。配置经过官方字段文档核对;在真正使用 systemd 的目标环境,还需先验证配置、确认运行用户和目录,再由有权限的人进行部署。
Shell 脚本:把失败、参数边界和清理写出来
先确定使用 Bash,还是可移植 sh
本文使用 Bash 5.2.37,脚本首行是 #!/usr/bin/env bash。[[ ... ]]、数组、pipefail 等用法不能无条件假定在所有 /bin/sh 实现上可用。用 bash -n script.sh 可以做语法检查,却不会证明运行命令存在、权限足够或逻辑正确。bash(1) INVOCATION、OPTIONS
管道的默认成功,可能掩盖上游失败
Shell 管道把前一个命令的标准输出交给后一个命令。默认情况下,Bash 管道的退出状态取最后一个命令;开启 pipefail 后,才会把管道中右侧最后一个非零状态作为失败结果。bash(1) Pipelines
sh
if (set +o pipefail; false | true); then
echo 'without pipefail: success'
fi
if (set -o pipefail; false | true); then
echo 'with pipefail: success'
else
echo 'with pipefail: failure'
fi实际输出:
text
without pipefail: success
with pipefail: failure假设前面是生成备份、后面是压缩,最后一个命令成功并不能证明备份内容完整。pipefail 能暴露一部分这种问题,但也要理解预期的短路管道,例如下游提早退出可能让上游收到 SIGPIPE;不能把任何非零状态都不加区分地解释为同一种业务失败。
set -euo pipefail 常作为起点:-e 在特定失败条件下退出,-u 对未设置变量的使用报错,pipefail 调整管道状态。但 -e 有明确例外,例如 if 条件、部分 && / || 结构等,因此重要动作仍应显式检查错误。本实验用 if 故意接住失败,验证代码可以继续。bash(1) set builtin;本机 help set
引号决定参数边界,不是可有可无的装饰
"$file" 把变量扩展结果保留为一个参数;不加引号可能发生单词拆分和通配符展开。文件名含空格时尤为容易出错。调用支持该约定的命令时加 --,可避免以减号开头的路径被误认成选项。bash(1) QUOTING、EXPANSION
附录故意创建 a file.txt,所有路径都加引号。输出文本用 printf '%s\n' "$value" 这一类固定格式,不把外部输入当作格式串。不要用 eval 重新解释不可信数据,也不要用 for x in $(ls ...) 解析文件名。
读取简单逐行数据时,while IFS= read -r line 可以避免去掉首尾空白和把反斜杠当转义。示例为了演示字段拆分,改用 IFS=, 读取自己生成、没有引号和内嵌逗号的两列表格:
text
Alice=10
Bob=20这不是完整 CSV 解析器。真实 CSV 允许引号和字段内换行时,应使用相应格式解析库,不要把这个循环扩张成通用导入方案。read 行为按本机 help read 与 Bash 手册核对。
重定向顺序和退出清理要可预测
标准输入、输出、错误通常是 fd 0、1、2。>file 2>&1 先把标准输出指向文件,再让标准错误复制该目标;2>&1 >file 的先后不同,标准错误保留的是调整前的标准输出目标。重定向从左到右处理,不能随意调换。bash(1) REDIRECTION
本实验先用 mktemp -d 创建专用目录,再注册退出清理:
sh
work=$(mktemp -d './linux-lab.XXXXXX')
trap 'rm -rf -- "$work"' EXIT清理范围仅限脚本自己新建的目录。trap 能处理正常退出与可捕获情形,却不能在 SIGKILL 或机器断电后运行,因此重要持久数据还需要另外的恢复设计。bash(1) trap builtin、signal(7)
后台 command & 只说明任务启动了,父脚本要用 wait 收集退出状态;收到终止信号时,还要按实际子进程关系安排清理。Shell 适合串联命令和小规模控制流。状态、并发、数据结构越来越复杂时,改用 Go 或 Python 往往更容易测试,这是维护性判断,不是 Shell 做不到。
本次到底验证了什么
| 项目 | 实际结果 |
|---|---|
| Bash 语法、权限、带空格路径、pipefail、简单 read 循环 | 执行成功,输出如正文 |
| top、ps、lsof | 对自建进程执行成功 |
| ss | 有输出,但 netlink 权限受限,结果范围有限 |
| df、free、vmstat | 成功取得短时快照,没有负载实验 |
| strace | 工具存在,ptrace 被拒绝,没有成功跟踪 |
| netstat、tcpdump、iostat | 未安装,没有执行 |
| systemd | 工具存在;PID 1 不是 systemd,verify 又因只读 /run 失败;未部署 |
没有因工具存在就声称实验完成,也没有为获取完整输出扩大权限。真正排查时也应保留这些限制:一个带警告的快照,往往只能支持比你原先设想更窄的结论。
依据
- procps-ng 官方手册:
top(1)、ps(1)、free(1)、vmstat(8)。 - iproute2、net-tools、lsof、strace、tcpdump、sysstat 各项目手册:正文相应命令的链接,重点为选项、字段解释和限制。
- GNU Coreutils
df(1);Bash 官方手册bash(1)的 Pipelines、Quoting、Redirection、set、read、trap、wait;本机 Bash 5.2.37 的help输出。 - Linux man-pages:
inode(7)、path_resolution(7)、umask(2)、unlink(2)、signal(7);Linux 内核 cgroup v2 Memory 文档。 - systemd 固定 v257 官方源码中的手册:
systemd.service.xml、systemd.unit.xml、systemd.exec.xml、systemctl.xml、journalctl.xml。语义引用均在相应段落。
全部代码、配置与复现命令
下面的 lab.sh 和 inspect_self.py 是实际运行文件,teaching-demo.service 是未安装、未运行、静态验证受阻的教学配置。把文件放在一个可写工作目录,运行:
sh
bash --version
bash -n lab.sh
bash lab.sh
python3 inspect_self.py
# 仅静态验证,不安装、不启动;本次此命令失败
systemd-analyze verify --man=no ./teaching-demo.service进一步诊断的命令入口集中保留在最后的 diagnostic-entrypoints.sh,只是阅读和手动选择的模板;不会自动执行附着、抓包或服务变更操作。不要把占位 PID、端口、服务名直接用于真实机器。
lab.sh
bash
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
work=$(mktemp -d './linux-lab.XXXXXX')
trap 'rm -rf -- "$work"' EXIT
umask 027
printf 'hello\n' > "$work/a file.txt"
stat -c 'new file mode=%a' -- "$work/a file.txt"
chmod 600 -- "$work/a file.txt"
stat -c 'changed file mode=%a' -- "$work/a file.txt"
mkdir -- "$work/private"
chmod 700 -- "$work/private"
stat -c 'directory mode=%a' -- "$work/private"
if (set +o pipefail; false | true); then echo 'without pipefail: success'; fi
if (set -o pipefail; false | true); then echo 'with pipefail: success'; else echo 'with pipefail: failure'; fi
printf 'name,score\nAlice,10\nBob,20\n' > "$work/scores.csv"
while IFS=, read -r name score; do
[[ "$name" == name ]] && continue
printf '%s=%s\n' "$name" "$score"
done < "$work/scores.csv"
printf '\nTool availability\n'
for cmd in top ps netstat ss lsof strace tcpdump df free vmstat iostat systemd-analyze; do
if command -v "$cmd" >/dev/null 2>&1; then printf '%s=available\n' "$cmd"; else printf '%s=missing\n' "$cmd"; fi
done
printf '\nFilesystem containing workspace\n'
df -h .
printf '\nMemory snapshot\n'
free -m
printf '\nOne-second vmstat sample (first line is since boot)\n'
vmstat 1 2
printf '\nPID 1 command\n'
cat /proc/1/comminspect_self.py
python
import os
import shutil
import socket
import subprocess
s = socket.socket()
s.bind(("127.0.0.1", 0))
s.listen(1)
pid = os.getpid()
port = s.getsockname()[1]
commands = [
["ps", "-p", str(pid), "-o", "pid,ppid,stat,pcpu,pmem,rss,vsz,comm"],
["top", "-b", "-n", "1", "-p", str(pid)],
["ss", "-ltn", f"sport = :{port}"],
["lsof", "-nP", "-a", "-p", str(pid), "-iTCP"],
]
try:
for cmd in commands:
print("$ " + " ".join(cmd), flush=True)
if shutil.which(cmd[0]) is None:
print("unavailable", flush=True)
continue
result = subprocess.run(cmd, text=True, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, timeout=10)
print(result.stdout, end="", flush=True)
print(f"exit={result.returncode}", flush=True)
finally:
s.close()teaching-demo.service
ini
[Unit]
Description=Static verification example, not an installed service
After=network.target
[Service]
Type=exec
ExecStart=/usr/bin/sleep infinity
Restart=on-failure
RestartSec=3s
TimeoutStopSec=10s
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.targetdiagnostic-entrypoints.sh
bash
#!/usr/bin/env bash
# Read-only reference list, not an automatically executed diagnostic script.
# Pick commands manually only after verifying target, permissions, and data scope.
exit 0
top -b -n 3 -d 1 -p "$PID"
ps -p "$PID" -o pid,ppid,stat,pcpu,pmem,rss,vsz,comm
ps -L -p "$PID"
ss -ltn
ss -tan
ss -tin
ss -ltn "sport = :$PORT"
netstat -lnt
lsof -nP -a -p "$PID" -iTCP
lsof -a -p "$PID" +L1
strace -f -T -e trace=read,write,openat,connect -o trace.log ./your-test-program
tcpdump -i lo -nn -c 20 -s 96 "tcp port $PORT"
df -h "$TARGET_PATH"
df -i "$TARGET_PATH"
free -m
vmstat 1 5
iostat -xyz 1 5
systemctl status "$SERVICE"
journalctl -u "$SERVICE" --since '10 minutes ago'
# State-changing commands below were NOT executed for this article.
systemctl daemon-reload
systemctl start "$SERVICE"
systemctl enable "$SERVICE"
systemctl restart "$SERVICE"