Day 05 · LAB 进程与调度动手 🔧

Week 01 · 周五(序号)· 2026-05-15 · 主线:操作系统核心 · 预计 30min 阅读 + 1.5h 上机
今日导读:前面 4 天讲了进程、线程、地址空间、调度——全是概念。今天全部上机。一行 ls 都没有的话,那些概念明天就忘了。我们会用 5 组 LAB 把观察工具(top/htop/ps)→ 性能分析(perf/time)→ 系统追踪(strace)→ 调度控制(taskset/nice)→ 进程编程(fork/exec)串起来。所有命令在 mac 上能直接跑(Linux 上更全),输出预期我都标在框里——和你看到的对比一下。
今日实验
  1. LAB 1:top / htop 字段全解读
  2. LAB 2:ps 高级查询
  3. LAB 3:用 time 测一次上下文切换的代价
  4. LAB 4:strace 跟踪 Python 的全部 syscall
  5. LAB 5:taskset + nice 实战
  6. LAB 6:Python 实现完整 fork-exec-wait
  7. 与 AI / Agent 安全的连结
平台说明:你在 mac 上。toppsstrace 等命令在 mac 和 Linux 上有差异。本文档以 mac 命令为主,括号里标 Linux 等价命令。强烈建议你之后在一个 Linux VM / Docker 里把 LAB 重做一遍——LLM 服务实际跑在 Linux 上,差异不能不知道。

1. LAB 1:top / htop 字段全解读

top 是看进程状态的入门工具,你一定见过。但 99% 的人只看 CPU% 和 MEM%。今天把所有字段搞清楚。

步骤

A在终端运行 top -o cpu(mac)或 top(Linux),观察头部和列。
# mac 上
top -o cpu

# Linux 上更好用的是 htop(要先 brew install htop / apt install htop)
htop
预期:你会看到顶部几行汇总(系统负载、内存使用),下面是进程列表,按 CPU 占用排序。按 q 退出。
B把头部那行 "load avg" 拆开看。

在 top 顶部找到类似 Load Avg: 1.84, 1.42, 1.36 三个数。这是过去 1 分钟、5 分钟、15 分钟的平均 load。Day 4 第 9 节讲过:

sysctl -n hw.ncpu(mac)或 nproc(Linux)看你机器核数。load / 核数 > 1 才算真过载。

C解读进程行的关键列(mac 版)。
含义
PID进程 ID
COMMAND命令名
%CPU占用 CPU 百分比(单核满 = 100%,多核满 = 100% × 核数)
TIME累计 CPU 时间
#TH线程数
STATER(running) / S(sleeping) / I(idle) / U(uninterruptible)
MEM占用内存(mac 显示 RSS 真实物理内存)
VPRVT私有虚拟内存
D在 top 里按交互键。
# mac top 交互键
o cpu     # 按 CPU 排序
o mem     # 按内存排序
e    # 列出某 PID 的详细
q         # 退出

# htop(Linux/mac brew 装)的快捷键更丰富
F2  设置
F3  搜索
F4  过滤
F9  发送信号(kill 一个进程)
F10 退出

2. LAB 2:ps 高级查询

pstop 更适合"我要一次性看清楚某个状态"。今天玩几个进阶用法。

步骤

A看你自己 shell 的完整信息。
ps -p $$ -o pid,ppid,user,stat,nice,pri,vsz,rss,command
# $$ 是当前 shell 的 PID
解读pri 是优先级数字(越小越高,CFS 下 nice=0 通常 pri=20)。vsz(虚拟大小) 通常远大于 rss(实际物理内存)——因为虚拟地址空间预订了一堆没真用的区域。
B找出系统里最耗内存的 5 个进程。
ps aux | sort -k 6 -rn | head -5
# 第 6 列是 RSS(实际物理内存 KB)

# 等价的 Linux 写法(mac 也能用):
ps -eo pid,user,rss,command --sort=-rss | head -5
C找出某个特定状态的进程(mac 上看 R 和 D 较少见,因为是桌面系统)。
# 所有非睡眠的进程
ps aux | awk '$8 !~ /S/ {print}'

# 找 Z(zombie) 进程
ps aux | awk '$8 ~ /Z/ {print}'  # 通常 0 个,但如果你 Python 多进程编程有 bug 就会有
D看进程树。
# Linux:
pstree -p $$

# mac 没自带 pstree,用 ps 模拟
ps -ef | grep $$
# 或者 brew install pstree
思考:你的 shell 的 PPID(父进程)是什么?它的 PPID 又是什么?一路追到 PID=1(launchd 在 mac / systemd 在 Linux)——所有进程的最终祖先。

3. LAB 3:用 time 测一次上下文切换的代价

Day 1/4 都讲了上下文切换有开销,但到底多少?用 time 实测一下。

步骤

A跑一个只做 fork 的小程序。
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
预期:在现代 mac 上 ~1-3 秒。换算下来每次 fork+exit+wait 约 100-300μs。考虑这里有两次进程创建/销毁 + 多次上下文切换
B对比"不 fork,只切换的":用线程 yield。
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
解读:你会发现这不一定比纯 CPU 计算快——time.sleep(0) 触发的调度切换累加起来,可能比你少做的工作还多。这就是为什么"线程不是免费的"
C看一个进程总共被切换了多少次。
# 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 次数,但能感受调度器的"动作"

4. LAB 4:strace 跟踪 Python 的全部 syscall

strace(Linux)/ dtruss(mac)能拦截一个进程的所有系统调用。这是调试 / 安全审计的瑞士军刀

mac 注意dtruss 默认需要关闭 SIP(系统完整性保护)或在特殊用户下运行。如果你不想动 SIP,跳过本节实操,改在 Linux Docker 里做(命令在最后给)。

步骤(Linux / Linux Docker)

A看一个 Python 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 启动慢
B看你的 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 一跑直接看清楚。

Cmac 上的近似替代方案。
# mac 上看一个进程打开的文件(不是 syscall 级,但有用)
lsof -p $$ | head -20

# fs_usage 看文件系统级活动(需要 sudo)
sudo fs_usage -w -f filesys python3

5. LAB 5:taskset + nice 实战

Day 4 讲了 CPU 亲和性和 nice。今天动手看效果。

mac 注意:mac 内核默认不允许用户空间程序绑核(taskset 没有等价命令)。本节用 Linux 命令演示,你可以在一个 Linux VM 或 Docker 里跑。
临时方案docker run --rm -it ubuntu:22.04 bash,里面 apt install util-linux 后就有 taskset。

步骤(Linux)

A看一个进程当前的亲和性。
taskset -p $$
# 输出: pid 12345's current affinity mask: ff
# ff = 11111111 二进制,说明能在 8 个核全部上跑
B启动一个 CPU 密集程序绑到核 0。
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
Cnice 的实际效果。
# 起两个一样的任务,一个 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
预期:两个进程绑到同一个核,CPU 总和应该 = 100%。nice=0 的拿 ~76%,nice=15 的拿 ~24%。这就是 Day 4 第 6 节讲的"nice 每差 1,约差 25% CPU 时间"——但要注意 nice 差 15 不是简单乘法,CFS 内部是 1.25^15 ≈ 28× 的权重比。

6. LAB 6:Python 实现完整 fork-exec-wait

把 Day 1-2 讲的进程创建链全部串起来。

步骤

A写一个完整的 shell 雏形(30 行 Python)。
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
B在 mini_shell 里跑几个命令。
minish> date
minish> ls /
minish> echo hello
minish> false   # 这个会以 exit code 1 退出
minish> xyz123  # 找不到的命令,child 报错
minish> exit
解读:你刚刚手写了一个 shell!bash、zsh、fish 内部干的事情核心就是这个三步:fork → exec → wait。多出来的功能(管道、重定向、变量、补全)都是在这个核心上加的料。Day 1 讲的 hello world 旅程,最后一步"shell 调 fork + execve"——你现在亲手实现了。
C挑战:实现孤儿进程和僵尸进程。
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 收养的现象——你今天的实验里亲眼看到了。

🔗 与 AI / Agent 安全的连结

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