ls 都没有的话,那些概念明天就忘了。我们会用 5 组 LAB 把观察工具(top/htop/ps)→ 性能分析(perf/time)→ 系统追踪(strace)→ 调度控制(taskset/nice)→ 进程编程(fork/exec)串起来。所有命令在 mac 上能直接跑(Linux 上更全),输出预期我都标在框里——和你看到的对比一下。
top、ps、strace 等命令在 mac 和 Linux 上有差异。本文档以 mac 命令为主,括号里标 Linux 等价命令。强烈建议你之后在一个 Linux VM / Docker 里把 LAB 重做一遍——LLM 服务实际跑在 Linux 上,差异不能不知道。
top 是看进程状态的入门工具,你一定见过。但 99% 的人只看 CPU% 和 MEM%。今天把所有字段搞清楚。
top -o cpu(mac)或 top(Linux),观察头部和列。# mac 上
top -o cpu
# Linux 上更好用的是 htop(要先 brew install htop / apt install htop)
htop
q 退出。在 top 顶部找到类似 Load Avg: 1.84, 1.42, 1.36 三个数。这是过去 1 分钟、5 分钟、15 分钟的平均 load。Day 4 第 9 节讲过:
5.0, 1.5, 1.0,说明刚刚突发了高负载,但 15 分钟前正常1.5, 1.5, 1.5,说明稳定状态用 sysctl -n hw.ncpu(mac)或 nproc(Linux)看你机器核数。load / 核数 > 1 才算真过载。
| 列 | 含义 |
|---|---|
| PID | 进程 ID |
| COMMAND | 命令名 |
| %CPU | 占用 CPU 百分比(单核满 = 100%,多核满 = 100% × 核数) |
| TIME | 累计 CPU 时间 |
| #TH | 线程数 |
| STATE | R(running) / S(sleeping) / I(idle) / U(uninterruptible) |
| MEM | 占用内存(mac 显示 RSS 真实物理内存) |
| VPRVT | 私有虚拟内存 |
# mac top 交互键
o cpu # 按 CPU 排序
o mem # 按内存排序
e # 列出某 PID 的详细
q # 退出
# htop(Linux/mac brew 装)的快捷键更丰富
F2 设置
F3 搜索
F4 过滤
F9 发送信号(kill 一个进程)
F10 退出
ps 比 top 更适合"我要一次性看清楚某个状态"。今天玩几个进阶用法。
ps -p $$ -o pid,ppid,user,stat,nice,pri,vsz,rss,command
# $$ 是当前 shell 的 PID
pri 是优先级数字(越小越高,CFS 下 nice=0 通常 pri=20)。vsz(虚拟大小) 通常远大于 rss(实际物理内存)——因为虚拟地址空间预订了一堆没真用的区域。ps aux | sort -k 6 -rn | head -5
# 第 6 列是 RSS(实际物理内存 KB)
# 等价的 Linux 写法(mac 也能用):
ps -eo pid,user,rss,command --sort=-rss | head -5
# 所有非睡眠的进程
ps aux | awk '$8 !~ /S/ {print}'
# 找 Z(zombie) 进程
ps aux | awk '$8 ~ /Z/ {print}' # 通常 0 个,但如果你 Python 多进程编程有 bug 就会有
# Linux:
pstree -p $$
# mac 没自带 pstree,用 ps 模拟
ps -ef | grep $$
# 或者 brew install pstree
Day 1/4 都讲了上下文切换有开销,但到底多少?用 time 实测一下。
cat > /tmp/fork_bench.py <<'EOF'
import os, sys
N = 10000
for _ in range(N):
pid = os.fork()
if pid == 0:
os._exit(0)
else:
os.waitpid(pid, 0)
EOF
time python3 /tmp/fork_bench.py
cat > /tmp/thread_bench.py <<'EOF'
import threading, time
N = 100000
e = threading.Event()
count = [0]
def worker():
for _ in range(N):
count[0] += 1
# 主动让出 CPU(不一定真切换,但提示调度器)
time.sleep(0)
threads = [threading.Thread(target=worker) for _ in range(4)]
t0 = time.perf_counter()
for t in threads: t.start()
for t in threads: t.join()
print(f"took {time.perf_counter()-t0:.2f}s")
EOF
python3 /tmp/thread_bench.py
time.sleep(0) 触发的调度切换累加起来,可能比你少做的工作还多。这就是为什么"线程不是免费的"。# Linux 上
cat /proc/$$/status | grep -E "voluntary|nonvoluntary"
# voluntary_ctxt_switches: 自己主动让出(等 IO、调 sleep)
# nonvoluntary_ctxt_switches: 被时间片打断强制切走
# mac 上没有 /proc,但可以用:
ps -o pid,minflt,majflt -p $$
# minflt/majflt 不是切换次数,是 page fault 次数,但能感受调度器的"动作"
strace(Linux)/ dtruss(mac)能拦截一个进程的所有系统调用。这是调试 / 安全审计的瑞士军刀。
dtruss 默认需要关闭 SIP(系统完整性保护)或在特殊用户下运行。如果你不想动 SIP,跳过本节实操,改在 Linux Docker 里做(命令在最后给)。
print('hi') 到底跑了多少 syscall。# 简单跟踪
strace -e openat,read,write python3 -c "print('hi')" 2>&1 | head -30
# 统计每种 syscall 的次数
strace -c python3 -c "print('hi')" 2>&1 | tail -20
strace -c 的输出会告诉你不同 syscall 的调用次数 + 累计时间。一个最简单的 print('hi') 在 Python 3 上会触发 几百次 syscall——绝大多数是加载解释器、import 模块。这就是为什么Python 启动慢。strace -e openat -f python3 -c "
import json
with open('/tmp/test.json', 'w') as f:
json.dump({'a': 1}, f)
" 2>&1 | grep -E "openat|tmp" | head -20
这种"看进程到底碰了哪些文件"在安全审计里非常关键——比如你怀疑某个 Python 工具偷偷读了你的 SSH key,strace 一跑直接看清楚。
# mac 上看一个进程打开的文件(不是 syscall 级,但有用)
lsof -p $$ | head -20
# fs_usage 看文件系统级活动(需要 sudo)
sudo fs_usage -w -f filesys python3
Day 4 讲了 CPU 亲和性和 nice。今天动手看效果。
docker run --rm -it ubuntu:22.04 bash,里面 apt install util-linux 后就有 taskset。
taskset -p $$
# 输出: pid 12345's current affinity mask: ff
# ff = 11111111 二进制,说明能在 8 个核全部上跑
taskset -c 0 python3 -c "
while True: pass" &
PID=$!
# 看它真的只在核 0 上
ps -o pid,psr,command -p $PID
# psr 列就是当前在哪个核
# 用 top 按 1 键展开每个核的 CPU%,看是不是只核 0 在 100%
top
# 在 top 里按 1,能看到 CPU0..CPUN 分别的利用率
# 清理
kill $PID
# 起两个一样的任务,一个 nice=0,一个 nice=15
python3 -c "while True: pass" &
NORMAL=$!
nice -n 15 python3 -c "while True: pass" &
LOWPRI=$!
# 把它们绑到同一个核(这样才能看到调度竞争)
taskset -cp 0 $NORMAL
taskset -cp 0 $LOWPRI
sleep 5
# 看两个进程的 CPU%
ps -o pid,ni,pcpu,command -p $NORMAL,$LOWPRI
# 清理
kill $NORMAL $LOWPRI
把 Day 1-2 讲的进程创建链全部串起来。
cat > /tmp/mini_shell.py <<'EOF'
"""一个最小化的 shell 实现:fork + exec + wait"""
import os, sys
def run_one(cmd):
pid = os.fork()
if pid == 0:
# ===== 子进程 =====
print(f" [子] my pid is {os.getpid()}, my parent is {os.getppid()}")
# 用 exec 替换地址空间
try:
os.execvp(cmd[0], cmd)
except FileNotFoundError:
print(f" [子] command not found: {cmd[0]}")
os._exit(127)
else:
# ===== 父进程 =====
print(f"[父] forked child pid={pid}")
finished_pid, status = os.waitpid(pid, 0)
exit_code = os.WEXITSTATUS(status)
print(f"[父] child {finished_pid} exited with code {exit_code}")
if __name__ == "__main__":
while True:
line = input("minish> ").strip()
if line in ("exit", "quit", ""): break
run_one(line.split())
EOF
python3 /tmp/mini_shell.py
minish> date
minish> ls /
minish> echo hello
minish> false # 这个会以 exit code 1 退出
minish> xyz123 # 找不到的命令,child 报错
minish> exit
fork → exec → wait。多出来的功能(管道、重定向、变量、补全)都是在这个核心上加的料。Day 1 讲的 hello world 旅程,最后一步"shell 调 fork + execve"——你现在亲手实现了。cat > /tmp/orphan.py <<'EOF'
import os, time, sys
pid = os.fork()
if pid == 0:
# 子进程
print(f" child pid={os.getpid()} parent={os.getppid()}")
time.sleep(3)
print(f" child pid={os.getpid()} now parent={os.getppid()} ← 应该是 init/launchd!")
sys.exit(0)
else:
# 父进程立即退出,不 wait
print(f"父 pid={os.getpid()} 立即退出,留下子进程")
sys.exit(0)
EOF
python3 /tmp/orphan.py
sleep 4 # 等子进程打印
parent=<父PID>,第二行打印 parent=1(mac 是 launchd, Linux 是 systemd / init)。这就是孤儿进程被 init 收养的现象——你今天的实验里亲眼看到了。1. strace 是 LLM 沙箱的"录音机"。你的 Agent 安全产品要监控 LLM 生成代码的行为,最可靠的层级就是 syscall。strace 能让你看到一个不可信脚本真正碰了什么。在生产环境用 strace 实时拦截不现实(性能开销 20–50%),所以会换成 eBPF——Week 2 周六会讲。但今天用 strace 直接观察一遍,建立"syscall 视角"的感觉非常重要。
2. nice + cgroups 是不可信代码的两道闸。LAB 5 你看到了 nice 让低优先级进程拿不到 CPU。但 nice 只能"压低",不能"封死"——一个 nice=19 的进程在系统空闲时仍能占满 CPU。要真正限制(比如"这个沙箱最多用 0.5 个核"),需要 cgroups,Week 3 会讲。今天先建立"系统提供多种隔离强度"的认识:nice (软) → ulimit (中) → cgroups (硬) → 独立进程 (强) → 独立容器 (更强) → 独立 VM (最强)。每往下一级,隔离更强、开销更大。
3. fork-exec 是攻击落地点。LAB 6 你实现了 fork+exec 的最小逻辑。攻击者通过 prompt injection 让 Agent 执行恶意 Python,绝大多数后续动作都要走 fork-exec:起一个 nc 反弹 shell、起一个挖矿程序、起一个 ssh 客户端连外网……所以监控 execve syscall 是最有效的拦截点。这正是 Falco / Tetragon 这类工具的核心规则之一。
4. top/ps 是上线后的第一道诊断工具。你的 LLM 服务出现 P99 抖动,先 ssh 到机器跑 top 看:CPU 是不是被某个不该跑的进程占了?看 STATE 列有没有大量 D 状态(IO 卡了)?看 load average 三个数的趋势?这些 30 秒的观察能筛掉一半"看起来很神秘"的问题。
5. 调度抖动是延迟敏感服务的隐形杀手。一个不绑核、不调 nice 的 LLM 推理服务,跑在与其他工作负载共享的机器上,P99 延迟可能是平均的 5-10 倍。LAB 5 教你的 taskset + nice,加上明天 Week 2 要讲的 cgroups 内存限制,是生产环境的标配三件套。
你今天 hands-on 接触的工具及其用途速查表(建议存下来,未来 24 周经常翻):
| 工具 | 用途 | 第一次出现日 |
|---|---|---|
top / htop | 实时进程状态总览 | Day 5 |
ps | 进程快照查询 | Day 5 |
time | 测程序耗时 | Day 5 |
strace / dtruss | 系统调用追踪 | Day 5 |
lsof | 看进程打开的文件/网络 | Day 5 |
taskset (Linux) | CPU 亲和性控制 | Day 5 |
nice / renice | 调整调度优先级 | Day 5 |
os.fork / os.exec / os.wait (Python) | 进程编程接口 | Day 5 |