1. 进程模型与子进程
一句话总结: 每条外部命令都是当前 Shell fork 出的子进程;只有内建命令和变量赋值在本进程执行,理解这条边界才能解释脚本行为。
Shell 执行命令时,外部命令会 fork 出子进程去跑,内建命令(cd、echo、export 等)在本进程内直接执行。
# 查看父子关系:$$ 是当前 Shell 的 PID
echo "shell pid=$$"
sleep 1 & # 后台跑一条外部命令
echo "child pid=$!" # 子进程 PID
# subshell:() 会在子 Shell 里执行,变量改动不回传
var=old
(var=new; echo "子Shell: $var") # new
echo "父Shell: $var" # old,没变
1.1 内建 vs 外部命令
| 类别 | 例子 | 执行位置 |
|---|---|---|
| 内建命令 | cd export local shift | 当前 Shell 进程 |
| 外部命令 | grep awk sleep | fork 出的子进程 |
| 关键字 | if for while case | 语法结构,不产生进程 |
| 管道线 | cmd1 | cmd2 | 每个命令一个子进程 |
一句话总结: 想让"改变"生效于当前 Shell,必须用内建命令;外部命令的一切状态变化都留在子进程里,这是 Shell 进程模型的根基。
2. 后台任务与作业控制
一句话总结:
&把命令放后台,jobs/bg/fg管理作业,wait等全部结束,disown脱离终端。
# 后台执行
long_task.sh &
echo "PID=$!"
# 查看作业列表
jobs -l # 带 PID 的作业列表
# 前台/后台切换
# Ctrl+Z 暂停当前前台任务
# fg %1 把作业 1 调回前台
# bg %1 让作业 1 在后台继续
fg %1
bg %1
# 等待所有后台任务结束
wait
2.1 作业控制速查
| 操作 | 含义 |
|---|---|
cmd & | 后台运行 |
Ctrl+Z | 暂停前台任务(SIGTSTP) |
jobs | 列出作业 |
bg %n | 后台继续第 n 个作业 |
fg %n | 调到前台 |
wait [pid] | 等待指定/全部作业 |
disown %n | 让作业脱离 Shell 管理 |
# 并行下载后再统一处理
download.sh a.txt &
download.sh b.txt &
wait
echo "两个下载都完成了"
# 退出 Shell 时不想被挂起,disown 丢给 init
nohup server.sh > server.log 2>&1 &
disown
3. 管道与进程间通信
一句话总结: 管道是匿名 IPC,上游写入、下游读取,中间没有缓冲区之外的存储,是 Shell 组合命令的血管。
# 管道串联:上一个 stdout 接下一个 stdin
ps aux | grep nginx | grep -v grep | awk '{print $2}'
# 退出码只能看到最后一个命令
false | true
echo "管道退出码=$?" # 0,是 true 的退出码
# 变量在管道两端是子 Shell,改动不外泄
echo "hi" | read x
echo "x=$x" # 空!read 在子 Shell 里执行
3.1 管道数据流特性
| 特性 | 说明 |
|---|---|
| 方向 | 单向,stdout → stdin |
| 缓冲 | 内核缓冲区,约 64KB 阈值 |
| 阻塞 | 读端未开,写端阻塞 |
| 退出码 | 默认取最右命令 |
| 子 Shell | 两端都在子 Shell 中运行 |
# Bash 4.4+ 用 PIPESTATUS 拿全段退出码
false | true
echo "${PIPESTATUS[@]}" # 1 0
# shopt -s lastpipe 让末段在当前 Shell 跑(仅脚本内生效)
set +m; shopt -s lastpipe
echo "hi" | read x
echo "x=$x" # 这次有值了
一句话总结: 管道好用但有三个坑:退出码只看最后一个、变量改动不外泄、两侧在子 Shell。需要保留状态时改用进程替换或临时文件。
4. exec 与进程替换
一句话总结:
exec用新程序替换当前 Shell 进程,不会 fork 新子进程;进程替换<( )把命令输出变成"文件"喂给别的程序。
# exec:替换当前进程(PID 不变)
exec java -jar app.jar # 脚本进程变成 Java,后续代码不执行
# 无命令的 exec:只重定向当前 Shell
exec 3> /tmp/out.log # 打开文件描述符 3
echo "写入 fd3" >&3
exec 3>&- # 关闭 fd3
# 进程替换:<( ) 提供只读文件,>( ) 提供写入口
diff <(sort a.txt) <(sort b.txt)
grep error <(tail -f app.log)
4.1 exec 常见场景
| 场景 | 写法 |
|---|---|
| 脚本转前台程序 | exec "$@" |
| 重定向整段 | exec >log 2>&1 后所有输出进文件 |
| 打开命名 fd | exec {fd}>file |
| 关闭 fd | exec {fd}>&- |
# 整个脚本输出重定向
exec > /var/log/deploy.log 2>&1
echo "这条也进日志" # 后续全部进日志
一句话总结: 用
exec "$@"启动常驻程序,能省掉一个多余的父进程层级,PID 保持稳定,systemd/supervisor 看到的进程树更干净。
5. 信号基础
一句话总结: 信号是内核发给进程的通知,
kill按号码发信号,trap在 Shell 里捕获处理,SIGTERM/SIGINT/SIGKILL 是最常打交道的一组。
# 查看全部信号
kill -l
# 按号码/名字发信号
kill -15 1234 # SIGTERM,请求终止,可被捕获
kill -9 1234 # SIGKILL,强制杀死,不可捕获
kill -INT 1234 # 等同 Ctrl+C
# 常用信号表
# 1 SIGHUP 终端挂断/重载配置
# 2 SIGINT 键盘中断 Ctrl+C
# 9 SIGKILL 强杀
# 15 SIGTERM 正常终止请求
# 17 SIGCHLD 子进程结束通知
5.1 trap 基本用法
# 捕获信号执行清理
trap 'echo "收到中断,清理退出"; exit 1' INT TERM
# 捕获 EXIT:脚本无论怎么退出都会执行(最实用)
trap 'rm -f "$tmpfile"; echo "清理完成"' EXIT
# 忽略信号
trap '' HUP
| 信号 | 默认行为 | 可否 trap |
|---|---|---|
| SIGINT | 终止 | 可捕获 |
| SIGTERM | 终止 | 可捕获 |
| SIGKILL | 终止 | 不可捕获 |
| SIGHUP | 终止 | 可捕获 |
| SIGCHLD | 忽略 | 可捕获 |
6. 信号处理实战
一句话总结: 优雅退出的套路是:trap 捕获 TERM → 停止接收新任务 → 等正在处理的任务跑完 → 清理临时文件 → 退出。
#!/usr/bin/env bash
# 优雅退出的最小实现
running=1
cleanup() {
echo "收到退出信号,清理中..."
running=0
rm -f /tmp/app.pid
}
trap cleanup TERM INT
echo $$ > /tmp/app.pid
while (( running )); do
# 干活
sleep 1
done
echo "已优雅退出"
6.1 常见陷阱与对策
| 场景 | 问题 | 对策 |
|---|---|---|
| 子进程收到信号 | 主进程没等它 | wait 配合 trap |
| 二次信号 | 清理被中断 | 第二次直接退出 |
| Ctrl+C 后残留临时文件 | 清理没跑 | EXIT trap 兜底 |
| 后台任务被信号打断 | 状态未知 | 用退出码判断重跑 |
# 二次信号直接强退的写法
trap 'echo "强退"; exit 130' INT
trap 'echo "清理中"; rm -f "$tmp"; trap - EXIT' EXIT
一句话总结: 只要是会创建临时文件、锁文件、后台任务的脚本,就该挂 EXIT trap 做收尾。这是脚本从"能跑"走向"可运维"的关键一步。
7. FIFO 与命名管道
一句话总结: FIFO 是文件系统里的管道,mkfifo 创建后两个进程可像读写文件一样通信,适合解耦生产者与消费者。
匿名管道只存在于命令之间,跨脚本、跨 Shell 会话的通信要用命名管道 FIFO。
# 创建 FIFO
mkfifo /tmp/my_fifo
# 终端 A:写入
echo "hello" > /tmp/my_fifo
# 终端 B:读取
cat /tmp/my_fifo # hello
# 用后删除
rm -f /tmp/my_fifo
7.1 FIFO 特性与注意事项
| 特性 | 说明 |
|---|---|
| 阻塞 | 无读者时写者阻塞,无写者时读者阻塞 |
| 单向 | 只支持单向数据流 |
| 持久 | 存在于文件系统,进程退出后仍在 |
| 权限 | 受文件权限控制 |
| 坑 | 读写端必须同时打开,否则互相死等 |
# 生产消费示例:一个进程喂数据,一个进程消费
(
for i in {1..5}; do echo "data$i" > /tmp/fifo; done
) &
while read -r line; do
echo "消费: $line"
done < /tmp/fifo
一句话总结: FIFO 的价值在于解耦:生产者的写入频率不必匹配消费者的读取频率。但务必同时打开读写两端,否则阻塞会互相等待,这也是调试难题的常客。
8. 总结
| 环节 | 要点 |
|---|---|
| 进程模型 | 外部命令是子进程,内建命令在本进程 |
| 作业控制 | & 后台、jobs/bg/fg 管理、wait 汇总 |
| 管道 | 单向流、末命令退出码、子 Shell 边界 |
| exec | 替换当前进程,PID 不变 |
| 信号 | TERM 可捕获、KILL 不可捕获、trap 拦截 |
| 优雅退出 | trap 清理 + wait 收拢子进程 + 二次信号兜底 |
| FIFO | 命名管道解耦生产消费,读写端须同时打开 |
进程与信号是脚本走向"服务化"的必修课。记住三点:管道两端在子 Shell、外部命令是子进程、EXIT trap 做收尾,就掌握了进程世界的主要地形。参数的解析与错误处理,则是脚本面对真实世界输入的第一道关口。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。