Skip to content

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["核对请求延迟与错误是否同步变化"]
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
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
waCPU 统计中的 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           0

available 大于 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"]
放大图示

原始尺寸下可横向或纵向滚动;按 Escape 关闭。

正在绘制图示…

查看图源代码
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/comm

inspect_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.target

diagnostic-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"

保留各书来源与许可 · 示例运行条件以正文为准