← 返回首页

Shell 编程基础 + 进程管理

目标

  • 用最少的术语,把 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 不是只能杀进程,它是给进程发"信号"(信号 = 一个整数代号,代表某种事件)。
信号编号含义什么时候用
SIGTERM15礼貌地说"请退出"默认信号,给进程清理机会
SIGKILL9强杀,不给面子进程卡死了才用
SIGHUP1挂起很多 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 1CPU/内存/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 当靶子,练 killjobs 的全套操作

本节知识回顾表

你应该掌握关键命令 / 语法熟练度目标
文件测试[ -f ] [ -d ] [ -e ]能直接写
字符串测试[ -z ] [ -n ] "$var"能直接写
数字测试-eq -gt -lt知道 ≠ =
逻辑组合[[ A && B ]]默认就用这个
if 多分支if / elif / else / fi背熟骨架
casecase $x in ... esac写服务脚本用
进程状态R / S / D / Z能看懂 ps 输出
psps aux ps -eo必会
killkill -15 kill -9 killall必会
任务控制& Ctrl+Z bg fg jobs kill %n必会