进程这个词,凡是学过操作系统的同学肯定都不陌生。但“进程的算法”这个说法,范围其实比我当年在课本上看的要大得多——它不只是调度算法那一章,还包含了进程生命周期管理、同步互斥、通信协作、守护进程、资源回收等一系列设计。今天就跟大家聊聊这些年我在实际项目和复习过程中,对进程相关算法的理解,重点讲清楚背后的“为什么”,也把我踩过的坑一并交代了。
这篇文章适合三类人看:一是正在准备操作系统期末考试或考研的同学,二是面试时总被问“进程与线程区别”但答不出深度的求职者,三是日常开发中经常跟多进程、多线程打交道,想搞清楚Linux/Windows下进程到底是咋运转的工程师。我会尽量用大白话拆解核心概念,穿插可复现的代码和命令,保证你读完能直接上手用。
1. 先搞懂:进程的算法到底在说什么
1.1 进程是什么:从程序到进程的一次升级
很多教材开篇就说“进程是程序的一次执行过程”,这话没错,但太抽象了。我更喜欢把它理解成“程序从磁盘上的静态文件,变成CPU里正在跑的动态实体”的那一刻。程序只是一堆指令和数据放在硬盘上,它本身不占CPU、不占内存资源;进程则是程序被加载到内存后,操作系统为它分配的一切资源的总和——代码段、数据段、堆、栈、寄存器值、打开的文件描述符、信号处理状态等等。
这里有一个很关键的点:同一个程序可以同时产生多个进程。比如你开了三个浏览器窗口,虽然顶层窗口看起来是同一个浏览器程序,但底层可能是三个独立进程,各自有独立的内存地址空间,互不干扰。这也是为什么某个标签页崩溃了,其他页面还能活得好好的——因为它们的进程是隔离开的。
理解“进程 = 资源容器”,后面很多东西就顺了:进程调度是在给这些容器分配CPU时间,进程同步是在协调多个容器访问共享资源,进程通信是在让这些容器之间搬运数据。所以标题里的“进程的算法”,本质上是围绕这个资源容器展开的一整套策略算法。
1.2 进程的一生:状态机里的每一次切换
进程从被创建到销毁,不是一直处于“运行”状态的。教科书上最经典的是五状态模型,而我实际写代码时更常用的是一种带“挂起”的视角:
- 新建态:进程正在被创建,资源还没完全分配好。此时进程还没进入就绪队列。
- 就绪态:万事俱备,只欠CPU。进程在排队等调度器发号施令。
- 运行态:拿到了CPU,正在执行指令。
- 阻塞态:因为等待某个事件(IO完成、信号到达、锁释放)而暂停,哪怕CPU有空也不会执行它。
- 终止态:执行完毕或被强制杀掉,等待操作系统回收资源。
状态切换的触发点特别容易考,也特别容易混:就绪 → 运行是调度器选中的结果;运行 → 就绪是因为时间片用完了被强占;运行 → 阻塞是进程自己主动请求等待(比如调用read()读磁盘);阻塞 → 就绪是等待的事件发生了。我当年总记混“运行→阻塞”和“运行→就绪”的区别,其实一句话就能记住:阻塞是进程自己等东西,就绪是CPU被别人拿走了。
1.3 进程、线程与守护进程的边界
说到进程就绕不开线程。简单来说,线程是进程内的执行单元,同一个进程的线程共享内存地址空间和文件资源,但各自有独立的栈和寄存器上下文。所以线程切换比进程切换轻量得多——进程切换要换地址空间(TLB、页表全都得重新折腾),线程切换只是在同一个地址空间里换个执行轨迹。
那守护进程(daemon)又是什么?它就是一种生存周期很长的后台进程,脱离了终端会话的控制。常见的如Linux的sshd、Nginx的worker进程、Windows服务等。后面我会专门讲守护进程与会话的关系,因为这里藏着一个很多人不知道的细节:为什么终端关掉了,守护进程还能活着。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 进程调度算法:CPU资源怎么分才公平
2.1 经典调度算法的横向对比
调度算法的核心目标,是在“吞吐率最大化”、“响应时间最小化”、“公平性”和“开销可控”之间取得平衡。光说这些太虚,直接上一张我整理过的对比表吧:
| 算法 | 核心思想 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| FCFS(先来先服务) | 按到达顺序排队 | 实现简单,公平 | 平均等待时间长,护航效应明显 | 批处理系统 |
| SJF(短作业优先) | 选预计运行时间最短的 | 平均等待时间最优 | 需要预知运行时间,长任务可能饿死 | 批处理系统(理论最优) |
| SRTF(最短剩余时间优先) | SJF的抢占版 | 响应更及时 | 同样依赖预估,切换开销大 | 交互式轻负载场景 |
| 时间片轮转(RR) | 轮流分配固定时间片 | 响应快,避免饿死 | 时间片大小敏感,切换开销大 | 分时系统 |
| 优先级调度 | 按优先级选任务 | 灵活处理紧急任务 | 低优先级可能饿死 | 实时系统、交互系统 |
| 多级反馈队列 | 多队列+动态优先级+时间片递增 | 综合性能好,兼顾前后台 | 参数调优复杂 | 现代通用操作系统 |
这表里的“护航效应”值得单独说一下。FCFS下如果排在最前面的是一个耗时极长的大任务,后面所有短任务都得干等着,就像一条车道上前面是辆慢悠悠的拖拉机,后面堵了一排小轿车。这个问题我在Windows上跑批处理深有体会:一个占满CPU的扫描任务能把其他所有进程的响应拖到卡顿。
2.2 时间片轮转与多级反馈队列:操作系统里的“红绿灯”
时间片轮转(RR)比较好理解,就是把CPU时间切成一个个等长的小块,按队列顺序一人一块,用完了就排到队尾。真正有意思的是多级反馈队列(MLFQ),它解决了两个问题:
第一,任务长度不同,就不能用同一个时间片。短任务应该尽快跑完,长任务应该慢慢跑但保证不饿死。MLFQ的做法是分级:第一级队列时间片最短(比如10ms),第二级加长(比如50ms),第三级更长(比如100ms)。新进程先进第一级,用完了没执行完,就降到下一级。
第二,交互型任务不能让他等太久。如果有一个进程频繁因为IO阻塞,它每次醒来其实只需要一点点CPU就能继续干活,这种情况下它应该保持在高层级队列,享受更短的响应时间。而CPU密集型任务因为每次时间片都用满,会逐渐沉到低层级队列,用长的时间片换取更少的切换开销。
我常说MLFQ就是操作系统的“红绿灯系统”:绿灯短的路口先放行短途车,长途车走绿波带但每个路口停留时间更长,整体通行效率反而最高。现代Linux的CFS调度器虽然实现方式不同(用红黑树+虚拟运行时间),但设计哲学跟MLFQ是一脉相承的——照顾短任务、防饿死、保交互响应。
2.3 调度器怎么落地:从Linux到Windows的真实现状
理论归理论,现实操作系统的调度器都做了大量工程优化。Linux的CFS用“虚拟运行时间”来决策:每个进程有一个vruntime,每次调度都选vruntime最小的进程。新进程会获得一定的“优惠”(即vruntime倒扣),保证它优先跑。I/O密集型进程因为经常睡眠,vruntime增长慢,所以会排在大多数CPU密集型进程前面。
Windows则使用基于优先级的抢占式多任务调度,优先级分32级,实时优先级16-31,可变优先级1-15。前台进程会获得一定的优先级提升,这就是为什么你在Windows上切个窗口,那个程序的反应会变快。我实测过同一个计算任务在Linux和Windows上吃CPU的表现:Linux下更平稳,Windows下更容易出现某个进程独占CPU导致其他程序卡顿的情况,这就是调度策略差异的直观体现。
提示:面试或考试时,如果被问“现代操作系统用什么调度算法”,别直接背MLFQ。要说明“传统教材讲MLFQ,Linux实际用CFS,Windows用多级优先级抢占调度”,这才是加分项。
3. wait、同步与互斥:多进程协作的关键算法
3.1 进程为什么要等待:wait与阻塞的本质
标题热词里有“进程等待wait”,这确实是进程算法里非常核心的一环。进程执行过程中经常需要等待:等子进程结束、等磁盘IO完成、等另一个进程释放资源。操作系统不傻,不会让一个等待中的进程白白占着CPU自旋,而是把它挂到对应事件的等待队列里,CPU调度器直接跳过它。
Linux下最常见的父子进程同步就是wait()和waitpid()。父进程调用wait()后会阻塞,直到某个子进程终止。为什么不直接让父进程做完事再检查子进程结果?因为如果父进程和子进程各自跑,父进程可能在子进程还没完成时就尝试读取结果,读到的就是脏数据。用wait把父进程挂起来,本质是建立了一种“先完成、后读取”的执行顺序约束。
我在实战中经常遇到另一个问题:进程虽然退出了,但它变成了“僵尸进程”(Zombie)。原因是子进程退出时,操作系统只释放了它的代码和数据内存,但进程控制块(PCB)里还留着退出码等信息,必须等父进程调用wait/waitpid来“收尸”。如果父进程一直不调用,子进程就成了僵尸。
c复制#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 子进程
printf("子进程开始工作,PID=%d\n", getpid());
sleep(2);
printf("子进程工作完成,退出\n");
exit(0);
}
// 父进程:等待子进程结束并回收
int status;
waitpid(pid, &status, 0);
printf("父进程:子进程已回收,退出码=%d\n", WEXITSTATUS(status));
return 0;
}
这段代码里waitpid的第三个参数0表示阻塞等待,如果你不想阻塞,可以用WNOHANG,返回值0表示子进程还在跑,等于pid表示回收成功。这个技巧在写守护进程或任务管理器时特别有用。
3.2 互斥与同步:临界区、锁与信号量
如果说调度算法是操作系统在做“资源分配”,那同步互斥算法就是进程之间在“竞争资源”时的裁判规则。先厘清两个概念:互斥是保证同一时间只有一个进程进入临界区,同步是保证多个进程按某种顺序执行。它们解决的场景不同:互斥解决的是“别同时改”,同步解决的是“必须等前一个做完”。
以生产者消费者问题为例,生产者往缓冲区里放数据,消费者从缓冲区取数据。如果没有互斥,两个进程同时操作缓冲区就可能导致数据错乱;如果没有同步,生产者可能往满的缓冲区写(溢出),消费者可能从空的缓冲区取(下溢)。经典的解法是用三个信号量:
mutex:控制缓冲区访问的互斥锁,初始值为1;empty:空槽位数量,初始为缓冲区大小;full:已填充槽位数量,初始为0。
伪代码里最核心的是P(wait)和V(signal)操作的顺序,必须先申请资源信号量,再申请互斥锁。这个顺序一旦反了,就可能出现死锁:生产者拿到了mutex但发现缓冲区满了,在等empty的时候别的进程也拿不到mutex,大家一起僵住。
我在实际编码中踩过一次这个坑。当时写一个多线程日志收集器,线程A写日志,线程B转发日志,缓冲队列满时线程B持有队列锁等待清空信号,而生产者线程拿不到锁却还在往队列里塞数据,最后整个服务卡死。后来把所有“先信号量、后锁”的顺序改成标准协议,问题立刻消失。这个教训在面试时讲出来,比单纯背PV操作要生动得多。
3.3 经典同步问题的三种解法:生产者消费者、哲学家进餐、读者写者
除生产者消费者外,操作系统课程的三大经典同步问题还有哲学家进餐和读者写者问题。
哲学家进餐讲的是5个哲学家围一圈,每两人之间有一根筷子,哲学家需要同时拿到左右两根筷子才能吃饭。如果每个人都先拿起左边筷子,再等右边筷子,就会死锁。常见的解法有几种:最多允许4个人同时拿筷子;要求哲学家必须同时拿起两根筷子(用互斥锁保护拿筷操作);或者给筷子编号,每个人先拿小号筷子再拿大号筷子——这样至少保证一个人能吃到饭。这个问题的核心思想,其实是死锁的四个必要条件:互斥、持有并等待、不可抢占、循环等待。破坏任何一个,死锁就不会发生。
读者写者问题则凸显了“读读不互斥、写写互斥、读写互斥”的原则。简单说就是读者可以随便读,但写者必须独占。我在做缓存服务时用过读写锁(RW Lock),并发读性能提升非常明显。不过要注意写者优先还是读者优先,如果读者一直来,写者可能被饿死。工程上一般用“写者优先”策略,避免写请求长期得不到执行。
这三个问题的价值不在题目本身,而在于它们提炼了所有并发程序都会遇到的三种冲突形态:容量竞争(缓冲区满/空)、资源抢购(筷子)、权限独占(读写)。你理解了它们的通用解法,就能迁移到实际系统的锁设计上。
4. 进程通信与进程池:数据怎么流动、任务怎么复用
4.1 IPC的本质与常见实现
进程之间是隔离的,那么它们怎么交换数据?这就是进程通信(IPC,Inter-Process Communication)要解决的问题。分类上常见的有管道、消息队列、共享内存、信号、Socket等。我给它们排了个“性价比”对比:
| 通信方式 | 原理 | 优缺点 | 典型场景 |
|---|---|---|---|
| 管道 | 内核缓冲区,一端写一端读 | 简单可靠,但单向且容量有限 | 命令行|、父子进程 |
| 消息队列 | 内核维护的消息链表 | 支持多对多,有消息类型,但拷贝开销大 | 跨进程业务解耦 |
| 共享内存 | 映射同一块物理内存 | 速度最快,但需要额外同步 | 大数据量高频传输 |
| 信号 | 异步通知事件 | 轻量,但不能携带复杂数据 | 通知退出、挂起 |
| Socket | 网络协议栈通信 | 可跨机器,最通用 | 分布式系统、浏览器与服务器 |
很多人一提“最快”,我就推荐共享内存。共享内存只在建立映射时做一次地址映射,之后读写都是直接访问物理内存,不经过内核拷贝。代价是没有任何自动同步机制,你必须配合信号量或锁使用,这就是上一节讲的同步算法在此处的用武之地。
我记得有次做一个股票行情分发系统,消息队列的方式每秒只能扛2万条,换成共享内存+双缓冲后直接到20万条,性能差了一个数量级。但代价是代码复杂了数倍——要设计双缓冲区切换、处理读写竞争、考虑进程崩溃后锁残留怎么办。所以IPC选型没有绝对的“最好”,只有“最合适”。
4.2 守护进程与会话:后台服务的生存之道
前面说到守护进程,它为什么能脱离终端独立存活?这跟“会话”概念密切相关。一个登录终端会创建一个会话,会话里有一个控制终端,所有在这个终端启动的进程都在这个会话里。如果终端关闭,内核会向这个会话的所有前台进程发送SIGHUP信号,默认动作是终止进程。
守护进程的创建过程,本质上就是让自己脱离这个会话。标准步骤是:
- 调用
fork(),父进程退出,让子进程成为孤儿进程被init收养; - 子进程调用
setsid()创建新会话,成为新会话首进程,从而脱离原终端的控制; - 改变工作目录到
/,避免占用已卸载的目录; - 重定向标准输入输出到
/dev/null或日志文件; - 关闭不需要的文件描述符。
我这里给一段Python实现守护进程的简化版代码,方便你理解框架:
python复制import os
import sys
import atexit
def daemonize():
# 第一次 fork,让子进程脱离父进程的会话和控制终端
pid = os.fork()
if pid > 0:
sys.exit(0)
# 创建新会话
os.setsid()
# 第二次 fork,防止重新获取控制终端
pid = os.fork()
if pid > 0:
sys.exit(0)
# 重定向标准输入输出
sys.stdout.flush()
sys.stderr.flush()
with open('/dev/null', 'r') as f:
os.dup2(f.fileno(), sys.stdin.fileno())
with open('/dev/null', 'a') as f:
os.dup2(f.fileno(), sys.stdout.fileno())
os.dup2(f.fileno(), sys.stderr.fileno())
atexit.register(lambda: os.remove('/tmp/daemon.pid'))
两次fork的细节很讲究。第一次fork是为了让子进程不是会话首进程,这样setsid()才有意义;第二次fork是为了防止子进程后续重新打开控制终端时成为会话首进程而自动获得终端。很多网上的教程省略了第二次fork,写出来的守护进程在某些环境下不够“顽固”,我建议直接把双fork当标准姿势记住。
4.3 进程池与协程:为什么有时候别创建太多进程
“进程池”这个词在热词里出现了,它不仅是一个底层概念,更是工程实践里非常普遍的手段。进程池的核心思想是:预先创建一组工作进程,任务来了就分配一个空闲进程去执行,执行完不销毁,而是回到池中等待下一个任务。
为什么要用池?因为进程的创建和销毁开销极大——要分配虚拟内存、建立地址空间、初始化文件描述符表,一次fork大概要几毫秒到几十毫秒。如果任务本身只需要5毫秒,你却花20毫秒创建进程,那效率肯定崩。进程池相当于把“创建进程”的成本前置摊薄,让高频小任务直接复用已有的进程资源。
我在Python里经常用concurrent.futures.ProcessPoolExecutor,在上线Web服务时踩过一个大坑:每来一个请求就spawn一个进程,结果并发一高,机器直接内存耗尽,整个服务被打挂。换成进程池(固定8个进程)以后,内存占用稳定在300MB左右,吞吐量反而翻了一倍。这里有个经验值:进程池大小一般设为CPU核心数的1-2倍,太多会导致大量上下文切换,太少会浪费CPU。
至于协程,它跟进程池是两种哲学。协程是在单线程内通过用户态的切换实现高并发,没有内核调度开销,但无法利用多核。我的习惯是:IO密集型任务用协程,CPU密集型任务用进程池,混合型任务用“协程+进程池”,各取所长。
5. 实操:在Linux和Windows上观察与排查进程
5.1 Linux下的进程观测三板斧
先说最基础的三条命令,它们几乎是进程排查的必修课:
ps aux:查看当前所有进程的快照,主要看PID、CPU、内存、状态、启动命令;top或htop:实时监控CPU/内存占用,能看到进程排序和负载情况;kill -9 PID:强制杀掉进程,但注意-9是SIGKILL,进程没有机会做清理,慎用。
光会这三个还不够,我工作中还常用这几个冷门但极其实用的技巧:
第一,看进程的启动路径。有时候你看到一个诡异进程名,比如“python”,但不知道它启动脚本在哪儿,可以用readlink /proc/PID/exe或ls -l /proc/PID/cwd来查看它的实际可执行文件和当前工作目录。这个在排查恶意进程或者“这个进程是哪来的”时特别有用。
第二,看进程打开的文件。lsof -p PID会列出该进程所有打开的文件描述符。如果磁盘分区无法卸载,多半是有进程占用了该目录下的文件,lsof能帮你揪出元凶。这也是热词里“F盘被另一个进程锁定”的Linux版本解法。
第三,不阻塞地等待子进程。写脚本时用wait命令等子进程,但如果你要同时监控多个后台任务,可以考虑wait -n(等待任意一个子进程结束),这在bash 4.3及以上版本支持,能显著简化并行任务的收尾逻辑。
5.2 Windows下那些让人迷惑的进程问题
Windows的进程体系跟Linux差异很大。首先是进程名字都有“exe”后缀,而且有些进程很容易让人误会,比如热词里提到的“msedgewebview2.exe”——这是Edge浏览器用的WebView2运行时进程,不是你电脑被装了垃圾软件。每次看到它占内存,别急着杀,先看看是不是某些应用在后台用WebView渲染内容。
再比如“thunderplatform占用进程”,这是迅雷下载平台的服务进程,通常在开机自启里挂着。如果平时用不到迅雷,可以在“任务管理器-启动”里禁用它的自启动项,比手动杀进程更彻底。
还有些朋友问“客户机操作系统已禁用CPU,请关闭或重置虚拟机”,这多半是VMware等虚拟机软件里客户机设置出错,点“重置虚拟机”基本能解决,跟实体机CPU本身没什么关系。遇到Windows进程问题时我的建议是:先看PID对应的可执行文件路径(右键进程,在“打开文件位置”里确认),再做决定。很多老手造谣“这进程是病毒”,一查路径全是正经的C:\Windows\System32目录下的系统进程。
热词里还有个“刚买的二手联想笔记本,需要跟卖家要Windows密钥吗”,顺带说一句我的实操经验:先看系统激活状态,如果显示“已激活”且是数字许可证,那说明跟硬件绑定了,不需要额外的密钥。如果是未激活状态,建议直接跟卖家确认绑定账号,避免后续系统无法更新。这个问题不在进程算法范畴,但既然是热词里提到的,也算帮大家避个坑。
5.3 Java进程OOM与堆大小的真实教训
热词里那个“idea编译时进程堆大小调整为8000,还是报错java.lang.OutOfMemoryError”的问题,我太有共鸣了。很多人第一反应就是把-Xmx调大,但往往治标不治本。
先分清两个堆:一个是JVM堆(Java应用内存),一个是编译器的构建进程堆(比如Gradle或Maven的daemon进程)。如果你改的是IDEA的-Xmx,但Gradle守护进程还是默认的1GB,一样会OOM。正确做法是分开调:
bash复制# Gradle 构建守护进程内存调整,在 gradle.properties 中
org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m
其次,单纯的提高堆内存有时候反而加剧问题。我遇到过一段代码里存在内存泄漏,-Xmx从2GB调到8GB,结果只是把“崩溃”变成“越来越慢”而已。JVM GC在做Full GC时如果堆太大,停顿时间会非常感人,整个服务就像卡死一样。这个时候应该做堆转储分析(jmap -dump),用jvisualvm或MAT工具找到谁占满了堆,而不是盲目调参。
关于“java进程”相关的排查,我还有一个实用建议:jstack PID > thread.txt可以打印Java进程的线程栈,如果某个线程卡死,你能清晰地看到它停在哪个调用栈上。我排查过很多次“接口没响应”,用top -H -p PID找到CPU最高的线程号,再转成十六进制去jstack -l PID里找对应的线程栈,基本一抓一个准。
6. 常见问题速查与复习参考
6.1 经典问题速查表
结合我遇到过的各类问题,整理成一张速查表,方便你直接翻阅:
| 现象 | 可能原因 | 排查/解决思路 |
|---|---|---|
| 进程卡死不响应 | 死锁、无限循环、IO阻塞 | jstack看Java线程;Linux下strace -p PID看系统调用 |
| 进程没了但端口还被占用 | 进程处于TIME_WAIT或被僵尸进程占着 | Linux ss -tlnp查端口归属;Windows netstat -ano配合任务管理器 |
| 内存不断上涨 | 内存泄漏、缓存无上限 | 定期dump堆,分析GC日志 |
| CPU持续100% | 死循环、GC频繁、计算密集 | top定位进程,perf top或jstack找代码位置 |
| 子进程退出但残留僵尸进程 | 父进程没调用wait | 修复父进程回收逻辑,或临时kill父进程让init收养 |
| Windows删除文件提示“被另一个进程锁定” | 有进程占用该文件句柄 | 用handle.exe或“资源监视器”搜索句柄,关闭对应进程 |
| 多个进程竞争同一日志文件乱序 | 缺少互斥锁 | 加文件锁或改用独立进程内的队列写入 |
6.2 期末和面试怎么答进程算法
如果是期末复习,重点应该放在调度算法的计算题和PV操作的代码题上。FCFS的平均等待时间、SJF的最优性证明、时间片轮转的周转时间计算,这些必须动手算一遍,不能光看。PV操作要熟练到能写出生产者消费者、读者写者的完整代码框架。
如果是面试,我的建议是准备三个层次的答案。底层把“进程生命周期状态转换”讲顺;中层能对比FCFS、RR、MLFQ,并说出现代Linux CFS的vruntime机制;高层要能结合项目谈你实际遇到过的调度、同步或通信问题,比如“我们当时用进程池就是因为进程创建开销太大”。面试官最想听的其实是你对“为什么”的理解,而不是背答案。
我还想特别提醒一点:别把“进程的算法”局限在调度算法上。面试官如果问“你讲讲进程的算法”,你回答一大篇调度,大概率只能算及格。真正的高分是宏观梳理:进程生命周期管理→调度→同步→通信→资源回收,把整条链路串起来,说明每个环节的算法都在解决什么问题,这样才能体现出系统思维。
最后再分享一点个人体会
操作系统这门课,很多人觉得它离业务开发很远,但我做了几年后台和桌面端开发之后发现,进程相关的算法和机制几乎每天都在影响你的系统表现——为什么服务会卡、为什么内存涨了、为什么杀掉进程端口还被占、为什么多线程比多进程快。这些问题的底层答案,全都藏在进程的设计和算法里。
我自己最受益的一次经历是排查线上服务的“神秘变慢”:进程没崩、CPU不高、内存也正常,但接口响应时间翻了几倍。最后strace跟踪发现是大量子进程频繁fork、wait,系统调度开销把CPU时间全耗在了上下文切换上。换成进程池之后,问题当场消失。那次之后我才真正理解教科书里“进程创建开销大”这句话的分量。
如果你正在学这块内容,别急着背名词,先拿你电脑上的真实进程开刀:用ps看一看有多少进程在跑,用wait感受一下子进程回收,用进程池重写一个并行下载脚本。跑起来、报错、排查、解决,走上这一轮,你对进程算法的理解会比别人深一个层次。
