目标
- 用最少的术语,把 Shell 编程和进程管理讲清楚
- 每个概念都先说"是什么 / 用来干嘛",再给命令
- 覆盖 SRE 日常 80% 用法
知识地图
shell 编程 + 进程管理
│
├── 1. 表达式("判断题")
│ ├── 文件测试:这个东西存不存在?是不是文件?
│ ├── 字符串测试:这个变量是不是空?两个字符串等不等?
│ ├── 数字测试:10 大于 5 吗?
│ └── 逻辑组合:两个条件同时满足 / 满足一个就行
│
├── 2. 流程控制("把判断连起来")
│ ├── if:如果...就...
│ └── case:这个值是 A 还是 B 还是 C...
│
└── 3. 进程管理("看运行中的程序")
├── 进程是什么:跑起来的程序
├── 进程状态:等 CPU / 跑着 / 睡着了 / 死了
└── 进程命令:怎么看、怎么杀、怎么丢后台
学习顺序建议:先吃透文件测试 + if + ps/kill/jobs,这三块占日常 80% 用法。其他的有印象就行。
Shell 表达式
一句话理解
表达式 = 一个会返回"是 / 不是"的句子。在[ ]或[[ ]]里写。
比如 [ -f /etc/hosts ] 的意思就是:"请问 /etc/hosts 是不是一个普通文件?"
文件测试(最常用,先学这个)
| 表达式 | 翻译成人话 |
|---|---|
[ -e 路径 ] | 存在吗(不管是文件还是目录) |
[ -f 路径 ] | 是普通文件吗 |
[ -d 路径 ] | 是目录吗 |
[ -r 路径 ] | 我能读吗 |
[ -w 路径 ] | 我能写吗 |
[ -x 路径 ] | 我能执行吗 |
[ -L 路径 ] | 是软链接吗 |
[ -s 路径 ] | 存在且不为空(size > 0) |
步骤 1:演示文件测试
# 准备测试文件
mkdir -p /tmp/shell_test
touch /tmp/shell_test/a.txt
echo "hello" > /tmp/shell_test/b.txt
ln -s /tmp/shell_test/a.txt /tmp/shell_test/link.txt
# 逐个测试
[ -e /tmp/shell_test/a.txt ] && echo "a 存在" # 输出: a 存在
[ -f /tmp/shell_test/a.txt ] && echo "a 是文件" # 输出: a 是文件
[ -d /tmp/shell_test ] && echo "是目录" # 输出: 是目录
[ -s /tmp/shell_test/b.txt ] && echo "b 不为空" # 输出: b 不为空
[ -L /tmp/shell_test/link.txt ] && echo "是软链" # 输出: 是软链
&& 的意思:左边成立(真),就执行右边。
步骤 2:字符串测试
先搞清一件事:shell 里变量要用双引号包起来,不然空变量会报错。
name=""
# 没加双引号:会报语法错误
[ -n $name ] && echo "非空" # 报错
# 加双引号:正常
[ -n "$name" ] && echo "非空" # 安静地不执行(因为 name 是空)
| 表达式 | 翻译 |
|---|---|
[ -z "$str" ] | str 是空字符串吗(zero length) |
[ -n "$str" ] | str 不是空的吗(nonzero length) |
[ "$a" = "$b" ] | a 和 b 相等吗(注意是单个等号) |
[ "$a" != "$b" ] | a 和 b 不相等吗 |
[ -z "$USER" ] && echo "未登录" || echo "当前用户: $USER"
# 输出: 当前用户: <你的用户名>
步骤 3:数字测试(关键区别)
a=10
b=5
[ "$a" = "$b" ] && echo "等" # = 是字符串相等,10 和 5 不等,没输出
[ "$a" -eq "$b" ] && echo "等" # -eq 是数字相等,也没输出
[ "$a" -gt "$b" ] && echo "a 大" # -gt = greater than,输出: a 大
[ "$a" -lt "$b" ] && echo "a 小" # -lt = less than,没输出
| 表达式 | 含义 |
|---|---|
-eq | 等于(equal) |
-ne | 不等于(not equal) |
-gt | 大于(greater than) |
-lt | 小于(less than) |
-ge | 大于等于 |
-le | 小于等于 |
常见坑:用=比数字,结果不可靠。"10" = "9"字符串意义上也不等,但用-eq才是真的在比数字。
步骤 4:逻辑组合
两种写法都能用,新手记住 [[ ]] 用 && ||,[ ] 用 -a -o:
| 场景 | 写法 1([ ]) | 写法 2([[ ]],推荐) |
|---|---|---|
| 两个都要成立 | [ $a -gt 0 -a $b -gt 0 ] | [[ $a -gt 0 && $b -gt 0 ]] |
| 满足一个就行 | [ $a -gt 0 -o $b -gt 0 ] | [[ $a -gt 0 || $b -gt 0 ]] |
| 取反 | [ ! -f file ] | [[ ! -f file ]] |
# 示例:CPU 和内存都高才报警
cpu=85
mem=90
if [[ $cpu -gt 80 && $mem -gt 80 ]]; then
echo "资源紧张"
else
echo "正常"
fi
# 输出: 资源紧张
步骤 5:模式匹配和正则
file="app.log.20240101"
# 模式匹配(glob):文件名以 .log 结尾吗?
[[ $file == *.log* ]] && echo "是日志" # 输出: 是日志
# 正则匹配
[[ $file =~ \.log\.[0-9]+$ ]] && echo "匹配" # 输出: 匹配
[ ]没有这两个能力,涉及模式匹配就用[[ ]]。
步骤 6:算术运算
# 最常用:$(( ))
echo $(( 1 + 2 )) # 3
echo $(( 10 % 3 )) # 1(取余)
((count++)) # 自增(不用写 $,这是赋值)
((count += 5)) # 自增 5
# 浮点运算:用 bc
echo "scale=2; 10/3" | bc # 3.33
流程控制
if:最基础的判断
骨架(背下来):
if [ 条件 ]; then
命令
elif [ 条件2 ]; then
命令
else
命令
fi
步骤 7:单 if
score=85
if [ $score -ge 60 ]; then
echo "及格"
fi
# 输出: 及格
步骤 8:if-else
if [ $score -ge 60 ]; then
echo "及格"
else
echo "不及格"
fi
步骤 9:if-elif-else(多分支)
if [ $score -ge 90 ]; then
echo "优秀"
elif [ $score -ge 80 ]; then
echo "良好"
elif [ $score -ge 60 ]; then
echo "及格"
else
echo "不及格"
fi
# 输出: 良好
条件后面加 ;,命令后面加 ;,可以压成一行(用在 crontab 或一行命令里):
if [ -f /tmp/lock ]; then echo "locked"; exit 1; fi
case:值匹配
典型场景:写服务启停脚本。
# 写一个迷你 nginx 控制脚本
action=$1 # 第一个参数
case "$action" in
start)
echo "启动 nginx"
;;
stop)
echo "停止 nginx"
;;
restart)
echo "重启 nginx"
;;
status|state) # 两个值匹配同一段
echo "查看状态"
;;
*)
echo "用法: $0 {start|stop|restart|status}"
exit 1
;;
esac
bash myscript.sh start
# 输出: 启动 nginx
bash myscript.sh hello
# 输出: 用法: myscript.sh {start|stop|restart|status}
*) 是兜底分支(类似 if 的 else),任何没匹配上的都走这里。
循环(先有个印象)
# for 遍历列表
for host in web1 web2 web3; do
echo "检查 $host"
done
# for 计数
for i in 1 2 3 4 5; do
echo "第 $i 次"
done
# while 条件循环
count=3
while [ $count -gt 0 ]; do
echo "倒计时: $count"
((count--))
done
# 输出: 3 / 2 / 1
进程管理
什么是进程
程序 = 硬盘上的一个文件(比如/usr/bin/ls)
进程 = 程序被加载到内存里、正在跑(或者等跑)的状态
打比方:菜谱 vs 做饭。一份菜谱可以同时做好几桌饭,每一桌都是一个"进程"。
每个进程都有一个唯一编号:PID(Process ID)。比如 PID 1234。
进程的三个组成部分
| 部分 | 是什么 | 类比 |
|---|---|---|
| 程序代码 | 那个可执行文件的内容 | 菜谱步骤 |
| 运行数据 | 变量、堆、栈、打开的文件 | 当前做到第几步、用了哪些食材 |
| PCB | 内核为进程建的"档案" | 餐桌订单(记着谁点的、PID、状态) |
进程状态(最核心的 4 个)
┌────── 创建(new)
│
▼
┌─── 就绪(R)──┐ ← 在排队等 CPU
│ │ │
调度▼ │ │
│ ┌── 运行(R)─┐ ← 正在 CPU 上跑
│ │ │ │
│ │ ▼ │
└───┤ 阻塞(S/D) ← 睡着了(在等 IO 完成)
│ │
│ ▼
└─── 终止(结束)
| 状态 | 英文 | 你看到的现象 |
|---|---|---|
| 就绪 / 运行 | R(Runnable/Running) | 正在跑,或者马上要被 CPU 调度 |
| 阻塞(可中断) | S(Sleeping) | 在等 IO(网络、键盘、磁盘) |
| 阻塞(不可中断) | D(Disk sleep) | 也在等 IO,但杀不掉(一般磁盘 IO 紧张) |
| 僵尸 | Z(Zombie) | 已经死了但没人收尸(父进程没回收) |
举例:
- 你打开一个文件,编辑器在等你按键 → S 状态
- 你下载一个大文件,浏览器在等网络数据 → S 状态
- 服务器磁盘 IO 打满,NFS 卡住 → 可能出现一堆 D
- 一个进程死掉了但父进程没 wait → 出现一个 Z
进程命令
ps:看一眼有哪些进程(静态)
ps aux
输出长这样(看列名):
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.1 12345 6789 ? Ss Jan01 0:01 /sbin/init
root 1234 0.5 1.2 45678 12345 ? Sl Jan01 1:23 nginx: master
nginx 1235 0.0 0.3 45678 3456 ? S Jan01 0:05 nginx: worker
| 列 | 含义 |
|---|---|
| USER | 谁起的 |
| PID | 进程号 |
| %CPU / %MEM | 占 CPU / 内存百分比 |
| STAT | 状态(R / S / D / Z 等) |
| COMMAND | 启动命令 |
常用过滤:
# 找 nginx 的进程
ps aux | grep nginx
# 找特定用户的进程
ps -u root
# 自定义显示哪些列
ps -eo pid,ppid,stat,cmd
top:动态版 ps(实时刷新)
top
常用快捷键(在 top 运行中按):
P:按 CPU 排序M:按内存排序1:展开每个 CPU 核心q:退出
kill:给进程发信号
关键认知:kill 不是只能杀进程,它是给进程发"信号"(信号 = 一个整数代号,代表某种事件)。
| 信号 | 编号 | 含义 | 什么时候用 |
|---|---|---|---|
SIGTERM | 15 | 礼貌地说"请退出" | 默认信号,给进程清理机会 |
SIGKILL | 9 | 强杀,不给面子 | 进程卡死了才用 |
SIGHUP | 1 | 挂起 | 很多 daemon 收到这个会重读配置 |
# 1. 礼貌终止(默认发 SIGTERM)
kill 1234
# 2. 明确指定信号
kill -15 1234 # 礼貌
kill -9 1234 # 强杀(杀不掉再考虑 D 状态或内核态死锁)
# 3. 按名字杀
killall nginx
# 4. 按模式杀
pkill -f "python.*app.py"
杀进程的标准动作:
kill <pid> # 先礼貌
sleep 5 # 等 5 秒
kill -0 <pid> 2>/dev/null && kill -9 <pid> # 还活着就强杀
任务控制:后台 / 前台 / 暂停
这是初学者最容易混的,分三步学。
第一步:用 & 启动后台任务
sleep 30 &
# 输出: [1] 12345
# [job号] PID
方括号里的 1 是这个 shell 里的"任务编号"(job 号),跟 PID 不是一回事。
第二步:用 jobs 看当前 shell 的后台任务
sleep 30 &
sleep 60 &
jobs -l
# 输出:
# [1]+ 12345 Running sleep 30 &
# [2]- 12346 Running sleep 60 &
第三步:Ctrl+Z 暂停,fg 拉到前台,bg 丢回后台
# 假设前台有个命令在跑
# 按 Ctrl+Z → 暂停,显示 [1]+ Stopped
# 输入 bg → 继续在后台跑
# 输入 fg %1 → 拉到前台
# 输入 kill %1 → 杀掉这个任务(按 job 号)
完整演示:
$ sleep 100 # 前台跑,会卡住终端
^Z # 按 Ctrl+Z
[1]+ Stopped sleep 100
$ jobs # 看任务
[1]+ Stopped sleep 100
$ bg %1 # 放后台继续跑
[1]+ sleep 100 &
$ jobs # 状态变了
[1]+ Running sleep 100 &
$ kill %1 # 按 job 号杀掉
[1]+ Terminated sleep 100
其他命令(知道干嘛用就行)
| 命令 | 一句话用途 |
|---|---|
pstree | 进程树,看父子关系 |
free -h | 看内存还剩多少 |
nice -n 10 ./task | 启动时设优先级(值越大越不"急") |
renice -n 5 -p <pid> | 调整运行中进程的优先级 |
lsof -i :80 | 谁在用 80 端口 |
vmstat 1 | CPU/内存/IO 综合情况,每秒刷一次 |
实战脚本
检查 nginx 进程在不在,不在就启动
#!/bin/bash
if pgrep nginx > /dev/null; then
echo "nginx 正在运行"
else
echo "nginx 挂了,正在启动..."
systemctl start nginx
fi
批量杀掉所有 python 脚本(开发环境慎用)
pkill -f "python.*my_script"
# 跑完后 ps aux | grep python 确认一下
CPU 占用前 5 的进程
ps aux --sort=-%cpu | head -6
验证
- 在 WSL 里把上面每段代码都敲一遍
- 故意改错(比如把
-eq写成=)看报什么错,加深印象 - 起一个
sleep 1000当靶子,练kill和jobs的全套操作
本节知识回顾表
| 你应该掌握 | 关键命令 / 语法 | 熟练度目标 |
|---|---|---|
| 文件测试 | [ -f ] [ -d ] [ -e ] | 能直接写 |
| 字符串测试 | [ -z ] [ -n ] "$var" | 能直接写 |
| 数字测试 | -eq -gt -lt | 知道 ≠ = |
| 逻辑组合 | [[ A && B ]] | 默认就用这个 |
| if 多分支 | if / elif / else / fi | 背熟骨架 |
| case | case $x in ... esac | 写服务脚本用 |
| 进程状态 | R / S / D / Z | 能看懂 ps 输出 |
| ps | ps aux ps -eo | 必会 |
| kill | kill -15 kill -9 killall | 必会 |
| 任务控制 | & Ctrl+Z bg fg jobs kill %n | 必会 |