写在前面
今天学两块内容——Shell编程基础和进程管理。前者是"怎么让Linux帮我干活",后者是"Linux里面的事都是怎么跑的"。看起来不相关,但其实是一条线:你写Shell脚本让系统干活,系统靠进程来执行这些活。那就从最简单的表达式开始说。
第一部分:Shell编程 — 给Linux定个规矩
1.1 表达式 — Shell的"语法课"
Shell表达式说白了就三件事:算数、判断、比较。
运算表达式 — 小学算数
$((3 + 5)) # 输出8 $((10 % 3)) # 输出1(取余数)
没啥好说的,跟计算器一样。
测试表达式 — 问问题
Shell里问问题,答案不是"对"就是"错":
[ 条件 ] 写成真(0) 写成假(非0)
坑来了:Shell里执行成功返回0,失败返回非0。跟绝大多数编程语言正好相反!C语言里0是假,1是真。Shell里0是真,1是假——别记反了。
整数比较 — 谁的数更大
| 运算符 | 含义 | 记忆法 |
|---|---|---|
-eq | 等于 (equal) | 首字母e |
-ne | 不等于 (not equal) | 首字母n |
-gt | 大于 (greater than) | g=greater |
-ge | 大于等于 | g+equal |
-lt | 小于 (less than) | l=less |
-le | 小于等于 | l+equal |
[ $age -gt 18 ] && echo "成年了"
1.2 单括号 vs 双括号 — 两个"判官"的区别
Shell里判断条件有两种写法:[ ](老祖宗)和[[ ]](新一代)。跟iPhone一样,新的比旧的功能多:
| 特性 | [ ] | [[ ]] |
|---|---|---|
| 逻辑与 | -a | &&✅ |
| 逻辑或 | -o | \|\|✅ |
| 正则匹配 | ❌ | =~✅ |
| 通配符匹配 | ❌ | 支持 ✅ |
| 可读性 | 一般(字母命名的运算符) | 更好 |
结论:能用[[ ]]就用[[ ]],除非你的脚本要在上古 Shell 上跑。
# 判断是不是纯数字 [[ $var =~ ^[0-9]+$ ]] && echo "是纯数字" # 判断文件是不是jpg [[ $file == *.jpg ]] && echo "是图片"
1.3 流程控制 — 让Shell有脑子
if 语句 — 做决策
if [[ $score -ge 60 ]]; then echo "及格了" elif [[ $score -ge 30 ]]; then echo "还有救" else echo "回家种地" fi
注意
if后面的; then——分号表示"一句话里换行"。也可以写成:if [[ $score -ge 60 ]] then echo "及格了" fi
case 语句 — 做选择题
适合那种"值就是这几个之一"的场景:
case $1 in start) echo "启动服务" ;; stop) echo "停止服务" ;; restart) echo "重启服务" ;; *) echo "用法: $0 {start|stop|restart}" ;; esac
case比if-elif-elif-elif清爽太多,看到;;就相当于"这个case结束"。最后的*)是默认分支,类似 switch 里的 default。
第二部分:进程管理 — Linux的"人口管理"
2.1 进程到底是什么?
程序是菜谱,进程是在做菜的过程。
程序(菜谱)放在磁盘上,是一堆静态的指令。当你双击(或执行)它,操作系统把它加载到内存里,分配资源、调度执行——这时候它才叫进程。
进程由三部分组成:
┌─────────────────────────────────┐ │ 进程 = 代码 + 数据 + PCB │ └─────────────────────────────────┘
拿做饭来类比:
代码(Text) = 菜谱上的步骤(怎么切菜、什么时候放盐)
数据(Data/Heap/Stack) = 锅里的食材(变量、状态)
PCB(Process Control Block) = 你脑袋里记着的"做到哪一步了"
PCB是最关键的部分——它是进程的身份证。操作系统就是靠PCB来管理所有进程的。PCB里记录的东西包括:PID(身份证号)、状态(在排队还是在干活)、优先级(你是VIP还是普通号)、CPU寄存器值(刚才做到哪一行了)。
2.2 进程的五种状态 — 一个人的五种"状态"
在Linux里,一个进程从生到死会经历这些状态:
创建 → 就绪 ↔ 运行 ↔ 阻塞 ↓ 终止
拿医院挂号看病来理解:
🟢创建(New)
你到医院前台,挂号了,护士在电脑里录入你的信息(创建PCB),给你分配病历本(分配资源)。
🟡就绪(Ready)
你拿号坐着等,啥也没干,就等大屏幕叫号(等待CPU分配)。你已经准备好了一切,就差CPU了。
🔴运行(Running)
大屏幕叫到你,你进了诊室见医生(获得CPU,正在执行)。单核CPU同一时间只能有一个进程在跑,就像只有一个医生一次只能看一个病人。
🔵阻塞(Blocked/Waiting)
医生说"你先去拍个CT,片子出来再来找我"。你出去等片子(等待I/O完成),这时候医生可以看下一个病人(CPU被调度走了)。注意:你已经拿到CPU了,但因为要等某个事件主动放弃了。
⚫终止(Terminated)
看完了,拿药走人,护士注销你的信息(回收资源)。
就绪 vs 阻塞的区别(面试高频题):
就绪:万事俱备,只差CPU(你坐在诊室门口像等号,门一开就能进)
阻塞:CPU来了你也干不了(你去拍CT了,就算医生喊你你也没法看病)
2.3 进程是怎么生出来的?— fork 和 clone
这是这块最重要的知识点,面试必问。
fork() — 影分身之术
pid_t pid = fork(); if (pid == 0) { // 这是子进程 printf("我是儿子,我的PID是%d\n", getpid()); } else if (pid > 0) { // 这是父进程 printf("我是爸爸,我儿子的PID是%d\n", pid); }fork()会创建一个几乎完全一样的子进程:
子进程复制了父进程的代码段、数据段、堆、栈
父进程收到子进程的PID,子进程收到0
如果fork失败,返回 -1
但如果每次都完全复制,太浪费内存了——父子进程大部分数据是一样的,干嘛复制两份?
这就引出了写时复制(COW, Copy-On-Write):
刚开始父子进程共享同一块物理内存,只是各自的虚拟地址映射到同一块物理内存。只有当其中一方试图修改数据时,内核才真正复制一份给修改方。这就像:你和你爸住一起,客厅的电视共用。只有当你和你爸同时想看不同频道的时候,才会再买一台电视。
clone() — 轻量级分身
fork()是复制所有,而clone()可以选择性共享:
共享内存空间 → 线程
不共享内存空间 → 普通进程
共享文件描述符 → 同一组打开文件
本质是:在Linux内核里,线程和进程底层都是用 task_struct 表示的,线程只是"共享了大部分资源的进程"。
你现在常用的pthread_create底层就是调用的clone()。
2.4 进程管理命令 — Linux的"人口普查工具"
① 看谁在跑 — ps(静态快照)
ps -ef # 标准格式,所有进程 ps aux # 详细格式,含CPU/内存占用 ps aux | grep nginx # 只找nginx相关
-ef和aux是两种流派,用哪个都行,习惯哪个用哪个。
② 谁生了谁 — pstree(家族树)
pstree # 树状展示父子关系,非常直观 pstree -p # 带上PID
你会发现
systemd是所有进程的老祖宗(PID为1),所有进程追根溯源都是它生出来的。
③ 实时监控 — top(动态直播)
top # 跑起来像Windows的任务管理器
进去之后按:
P— 按CPU排序M— 按内存排序k— 输入PID杀进程q— 退出
④ 查内存 — free
free -h # 友好格式,显示GB/MB
⑤ 看端口 — lsof(谁占了我的端口?)
lsof -i :80 # 看谁占用了80端口 lsof -Pti :80 # 只输出PID,适合写脚本用
这是排查端口冲突的经典三板斧:
lsof -i :端口→ 拿到PID →kill
⑥ 系统整体性能 — vmstat
vmstat 1 5 # 每隔1秒刷新一次,总共5次
输出里重点关注:
r(run queue):排队的进程数,持续大于CPU核数说明CPU不够用b(blocked):被阻塞的进程数,过高说明I/O瓶颈si/so:swap换入换出,非0说明内存紧张
2.5 进程控制 — 生杀大权
温柔的杀 vs 暴力的杀
kill 1234 # 发SIGTERM(15),请求进程自己退出 ← 温柔版 kill -9 1234 # 发SIGKILL(9),强制杀死 ← 暴力版
能用
kill 1234就别用kill -9。SIGTERM 相当于跟进程说"请收拾一下下班",SIGKILL 相当于直接断电拔网线。很多程序收到 SIGTERM 会做清理工作(保存数据、关闭文件等),收到 SIGKILL 啥都来不及做。
按名字杀
killall nginx # 杀掉所有nginx进程 killall -i nginx # 杀之前问问你,防止误杀 pkill -U test # 杀掉用户test的所有进程 pkill -t pts/1 # 杀掉某个终端的所有进程
killall和pkill的区别:前者按进程名匹配,后者更灵活(可以按用户、终端等条件)。
抢CPU — nice/renice
Linux是"多任务"系统,但CPU只有一个(单核)。那谁先谁后?靠优先级:
优先级范围:-20(最高优先级) ~ 19(最低优先级) 默认值:0 数字越小,越优先
# 启动时指定优先级 nice -n 10 ./slow_script.sh # 低优先级跑,让出CPU给重要任务 nice -n -5 ./urgent_task.sh # 高优先级跑(需要root权限) # 运行时调整优先级 renice -n 5 -p 1234 # 把PID 1234的优先级改成5 renice -n 10 -u test # 把用户test的所有进程优先级改成10
类比:超市结账
nice值-20 ≈ VIP通道,优先结账
nice值19 ≈ 你拿了一车东西但只有几件,让后面拿一瓶水的先结
默认0 ≈ 普通排队
2.6 前后台任务 — 一个人当两个人用
想象你在终端前:
前台:你眼前正在运行的程序,占着你的终端
后台:在幕后默默运行的程序,不占用终端操作
# 启动后放到后台 tar -czvf backup.tar.gz /data & # 查看所有后台任务 jobs jobs -l # 带PID # 调回前台 fg %1 # 把1号后台任务调到前台 # 暂停的前台任务在后台继续跑 bg %1 # 恢复暂停的1号后台任务 # 杀后台任务 kill %1 # 通过任务编号杀
典型场景:
tar打了大包,不想干等,按Ctrl+Z暂停
bg %1把它放后台继续跑继续敲其他命令
想查进度
fg %1调回前台查看
要点速查卡
Shell编程
[ 条件 ] — 单括号(传统) [[ 条件 ]] — 双括号(推荐,支持正则 && ||) 整数比较:-eq -ne -gt -ge -lt -le 逻辑运算符:&&(与) ||(或) !(非) case语法: case $var in pattern1) cmd1 ;; pattern2) cmd2 ;; *) default ;; esac
进程管理
进程三要素:代码 + 数据 + PCB(最重要的身份证) 五种状态:创建 → 就绪 ↔ 运行 ↔ 阻塞 → 终止 创建方式:fork()(完全复制+COW)| clone()(选择性共享=线程) 常用命令: ps -ef / ps aux # 看进程 pstree -p # 看父子关系 top # 动态监控 lsof -i :端口 # 查端口占用 free -h # 看内存 vmstat 1 5 # 看系统性能 kill -9 PID # 强制杀 nice -n N cmd # 设置优先级启动 jobs / fg / bg # 前后台管理
最后说几句
今天学的东西有个很清晰的脉络:
Shell编程→ 是用代码跟Linux对话的方式表达式和流程控制→ 是语法规则进程→ 是Linux执行这些代码的方式fork/clone→ 是进程如何"生孩子"(面试必考)ps/top/kill→ 是你在运维中每天都要用的"人口管理工具"
把进程的五种状态流转和fork()的原理理解透,这两个是面试最爱问的。其他的命令多用几次就记住了,不用硬背。