如果你是个写过几年代码的人,肯定遇到过这样的场景:程序一启动就跑起来了,但它到底算"程序"还是"进程"?任务管理器里密密麻麻那一排名字,哪些能杀哪些不能碰?两个程序之间怎么互相传数据?这些问题追踪到根子上,全都能落到"进程"这个词上。
我之前带新人做项目时,发现很多人对进程的理解停留在"进程就是运行中的程序"这个口诀层面。口诀没错,但光靠这个口诀,你连kill -9为什么有时候杀不死进程都解释不了。所以这次我打算把进程这个话题从头捋一遍,从操作系统的设计动机讲起,到进程控制块里每一块字段的用途,再到进程状态流转、进程和线程的边界、进程间通信的几大方式,最后用Linux和Windows下的实操命令收尾。这篇是第一篇,先把地基打牢,后面再聊进程调度、同步互斥这些进阶内容。
1. 为什么必须引出"进程":程序自己搞不定的三件事
先别急着背定义,先想想一个原始问题:如果没有进程这个概念,只让程序直接在CPU上跑,会出什么乱子?
早期计算机确实是这么干的——单道程序系统,一个程序独占CPU和内存,跑完再跑下一个。但很快三个痛点就暴露了。
第一个痛点是CPU利用率太低。程序在等待I/O(比如读磁盘、等键盘输入)的时候,CPU是闲着的。而I/O速度比CPU慢好几个数量级,这就意味着CPU大部分时间在空转。当时人们的想法很朴素:既然一个程序在等I/O,为什么不趁机让另一个程序用CPU?但问题来了——多个程序同时放在内存里,谁来管理它们各自的运行现场?如果一个程序算到一半被切走,等会儿切回来,它记住自己算到哪一行了吗?
第二个痛点是内存隔离。如果多个程序直接摊在内存里,A程序不小心写坏了B程序的内存区域,或者A程序直接读走了B程序的数据,这就乱套了。操作系统需要一套机制,让每个程序都觉得自己独占了内存,同时又不能让它们越界。
第三个痛点是资源分配的公平性。A程序是个死循环,B程序是个需要快速响应的服务,如果CPU一直让A占着,B根本没有出头的机会。操作系统需要能按策略把CPU、内存、I/O设备分配给不同程序,而且能随时收回来再分配出去。
看到这里你应该明白了:程序本身是静态的,它只是一堆指令和数据的集合,像一张乐谱;而演奏过程是动态的,指挥得盯着每个乐手到哪一节了、乐器用完了没、该换谁上场——这个"动态演奏过程"加上"指挥手里的控制信息",其实就是进程的雏形。
所以教科书上那句"进程是程序的一次执行过程",听起来抽象,落到实际就是:进程 = 程序代码 + 数据 + 运行现场 + 操作系统为了管理它而记录的一组信息。一个程序可以被多个进程跑(比如你开了三个浏览器窗口,底层可能是同一个浏览器进程模板,但每个窗口一个进程实例),一个进程也可以在一生中执行多段不同的代码(比如进程先加载配置文件再执行业务逻辑)。
从这层意义上说,引出进程,本质上是操作系统从"程序为主体"过渡到"执行为主体"的一次抽象升级。程序回答"做什么",进程回答"现在做到哪了、占着什么资源、该不该被切走"。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 进程的"身份证":进程控制块(PCB)里到底存了什么
操作系统的内存里住着几十个进程,内核是怎么分清谁是谁的?靠的就是每个进程都有一张专属登记表——进程控制块(Process Control Block,PCB)。在Linux里,这个结构体叫task_struct,Windows里叫EPROCESS,叫法不同,干的活完全一样。
PCB可以说是进程在操作系统里的"身份证"加"档案袋"。它记录了进程从出生到消亡的所有关键信息,拆开来看大致有这几类:
- 进程标识信息:PID(进程号)、PPID(父进程号)、UID(属于哪个用户)。PID是进程身份证号,一个系统里同时存在的PID不会重复;PPID用来追溯这个进程是谁拉起来的,排查"任务管理器里莫名其妙多出来的进程"时,先看PPID往往能顺藤摸瓜找到元凶。
- 处理机状态信息:程序计数器(PC)指向下一条要执行的指令地址,寄存器组保存当前运算的中间结果,状态字保存标志位。这组信息是进程切换的命根子——进程被切走时,这些值原样存回PCB;下次切回来时,再从PCB里恢复,进程自己完全感觉不到自己被"冻结"过。
- 进程调度信息:进程状态、优先级、已等待时间、已占用CPU时间等。调度器每时每刻都在翻这些字段,决定"下一个轮到谁上CPU"。
- 内存管理信息:代码段、数据段、堆栈段的地址边界,页表指针。记住一个原则:PCB记录的是进程的地址空间边界,但实际内存数据本身在内存管理单元(MMU)手里管着。
- 资源清单:打开的文件描述符表、占用的I/O设备、锁信息等。这也是为什么查一个进程"泄漏了文件句柄"时要翻PCB里的资源表。
- 进程间通信信息:信号、管道、消息队列等通信机制需要的标识和指针。
这里要特别强调一点:PCB是内核数据结构,运行在内核态,用户态的普通程序根本碰不到。你写应用代码时用到的getpid(),其实是系统调用让内核从PCB里取出PID再返回给你。PCB本身也不在进程自己的地址空间里,而是存在内核专门划出的区域中。每次进程切换,本质就是"把当前进程的PCB填好并保存,再把下一个进程的PCB取出来恢复现场"。
我见过很多新手去网上找"修改进程优先级"的教程,试图直接改一个文件里的字段来实现,结果全以失败告终。原因就在这:PCB是内核私有数据,你只能通过系统调用(比如setpriority())或命令工具(比如systemd-run --nice=)间接修改内核替你改PCB里的值。
3. 进程一生都在五个状态里倒腾:创建、就绪、运行、阻塞、终止
有了PCB,进程像是一个人有了户口本,接下来就要经历完整的一生。教科书上经典的进程五状态模型,在不同操作系统里细节有差异,但骨架是通用的。
五状态分别是:新建态(New)、就绪态(Ready)、运行态(Running)、阻塞态(Blocked/Waiting)、终止态(Terminated)。很多人觉得这五个状态很枯燥,但我用做饭来比喻,你一下就能记住。
- 新建态:你把材料买好放厨房了(PCB已创建),但还没开火,也没上案板。此时进程的各种资源正在分配中。
- 就绪态:所有材料都备齐了(资源就绪),菜谱也打开了,就等大厨伸铲子。就绪队列里的进程已经拿到除CPU之外的一切条件,随时可以上CPU执行。
- 运行态:大厨正在颠勺,CPU正在执行该进程的指令。单核CPU上同一时刻只能有一个进程处于运行态。
- 阻塞态:菜炖上了,得等15分钟,大厨不用守在锅边。进程在等待某个事件(比如I/O完成、信号量释放、读网络数据返回),即便给它CPU也干不了活,所以被挂起。
- 终止态:菜出锅倒掉,厨房收拾干净,进程的PCB被回收,只留一个"僵尸"记录供父进程收尸(这个后面细说)。
状态之间的转换要特别注意几个方向:
- 就绪→运行:被调度器选中,分配CPU。这是最常见也最频繁的一次切换。
- 运行→就绪:时间片用完,被剥夺CPU,比如你开着一个死循环程序,操作系统每分钟切他上千次,让它反复在"运行"和"就绪"之间横跳。
- 运行→阻塞:主动请求I/O或等待某事件,比如程序执行到
read()文件时,如果数据还没就绪,进程就从运行态掉到阻塞态。 - 阻塞→就绪:等待的事件完成了,比如磁盘数据读回来了,进程被唤醒,重新排进就绪队列。
这里面最容易被新手绕晕的是"阻塞→就绪"不是"阻塞→运行"。你应该能猜到原因:事件完成时,CPU可能正被别的进程占着,你不能直接把CPU抢过来,只能先排到就绪队列里等调度。这跟收银台前排队一样,轮到你办业务了,结果你银行流水没带全(缺资源),只能先去旁边等材料补齐,材料补齐后又得重新排队。
Linux里实际状态比五个要多一些,ps命令里你能看到R、S、D、T、Z、I等状态码,解释一下:
R(running/runnable):正在运行或在就绪队列中等待调度。S(interruptible sleep):可中断睡眠,对应阻塞态,可以接收信号唤醒。D(uninterruptible sleep):不可中断睡眠,通常在等待磁盘I/O,这种状态下进程连信号都不响应,kill -9都杀不动(这就是"kill -9杀不死进程"最常见的根因之一)。T(stopped/traced):暂停或被调试器接管。Z(zombie):僵尸态,进程已经终止但PCB未被父进程回收。
看到Z状态时别慌,它说明进程已经结束了运行,只是"户口本"(PCB)还没销掉。正常流程是:子进程先退出,向父进程发送SIGCHLD信号,父进程调用wait()回收子进程的PCB和退出码。如果父进程一直不调用wait(),子进程的PCB就留在内核里,变成僵尸进程。恶意代码常常故意不回收子进程,制造大量僵尸进程挤占内核内存,这个坑如果你以后做服务端开发,迟早会遇到。
4. 进程 vs 线程 vs 协程:别再被"进程就是线程的容器"骗了
进程讲到这,避不开的就是"过程 vs 线程"这个热门搜索词。很多人觉得线程就是"轻量级进程",然后止步于此,其实不够。要看清它们的本质,得回到资源分配和调度的维度。
最直观的对比表格如下:
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 资源所有权 | 拥有独立的地址空间、文件描述符、堆栈 | 线程共享所属进程的地址空间和大多资源 |
| 系统开销 | 创建/切换开销大(要切换地址空间、刷新TLB) | 创建/切换开销小(只切换栈和寄存器等上下文) |
| 相互隔离 | 一个进程崩溃通常不影响其他进程 | 一个线程崩溃(如段错误)会拖垮整个进程 |
| 通信方式 | 需要IPC机制(管道、共享内存、消息队列等) | 线程间可直接读写共享变量,但也需要同步 |
| 调度实体 | 操作系统以线程为基本调度单位 | 线程是CPU调度的基本单位 |
教科书里有个经典比喻:进程是"车间",线程是"生产线"。车间有自己的电力、照明、物料仓库(独立资源);一个车间里可以有几条生产线,生产线之间共用车间里的照明和仓库(共享大部分资源),但每条生产线有自己的组长和工位状态(独立栈和寄存器)。
但这里我要多说一层:现代操作系统里,真正的调度单位其实是线程,不是进程。Linux中clone()系统调用可以创建不同共享级别的任务:完全独立地址空间就是进程,完全共享地址空间就是线程,中途还可以控制共享哪些部分。所以严格来说,进程是"资源分配单位",线程是"CPU调度单位"。这也是为什么Java里Thread会被映射为操作系统原生线程直接参与调度,而网上有人问"为什么进程也叫任务(task)",因为内核眼里它把每个可调度实体都叫任务。
协程(比如Go的Goroutine、Python的asyncio)则在用户态自己管理调度,不走内核。协程切换不涉及系统调用,开销比线程又小一个量级,但协程得靠程序主动让出CPU(yield),而且一个阻塞的系统调用会把同一线程上的其他协程一起卡住。三者关系可以这样记:进程是最小资源单位,线程是最小调度单位,协程是最小"可控挂起"单位。
实操里怎么选?大量计算密集任务要充分利用多核CPU,优先用线程池;要稳定隔离、防止某个模块崩溃拖垮全局,用多进程;要处理超高并发的I/O密集型任务(比如Web请求转发),协程往往是最舒服的方案。记住"IO密集用协程、CPU密集用线程、稳定隔离用进程"这个口诀,面试和真写代码都能用上。
5. 进程间通信(IPC):管道、消息队列、共享内存、信号和Socket
进程之间互相独立,那它们怎么协作?你也许会说"网络请求啊",但同一台机器上两个进程之间也要通信,这是进程间通信(IPC)要解决的问题。网上搜"进程通信(IPC)"相关关键词热度一直很高,这里我把主流方案一次讲透,顺便给出选型建议。
5.1 管道:最简单的"内存水管"
管道(Pipe)是最早的IPC方式之一,Shell里一条ls | grep xxx就是管道:ls进程的输出作为grep进程的输入。它本质是内核里一块环形缓冲区,一个进程往里写,另一个进程往外读,数据按先进先出流通。
管道分两类:匿名管道(pipe())只用于父子进程之间,因为子进程能继承父进程的文件描述符;命名管道(mkfifo)则可以在任意两个进程之间用,它在文件系统里有一个路径名,打开它就能读写。
管道的特点是简单、双向数据流(匿名管道只能单向,需要两个管道才能双向通信),但数据只能流式读写,不能随机访问,而且读出即销毁。适合做"流水线"式的单向数据传输,不适合需要频繁定位读写的交互场景。
5.2 消息队列:带边界的"信箱"
消息队列(Message Queue)比管道先进一点,数据以消息为单位,消息有类型有长度,读进程可以按类型读取,不用"读完就没了"那么死板。用msgget()创建队列,msgsnd()发送消息,msgrcv()接收消息。内核负责维护队列,进程退出消息还在(除非显式删除),这是管道做不到的。
但消息队列也有短板:每条消息有大小上限(典型系统里一般是8KB),存取都要在内核空间和用户空间之间拷来拷去,性能比共享内存差不少。适合"小数据量、有结构、需要持久排队"的场景,比如日志收集、任务分发。
5.3 共享内存:性能之王但需要"管纪律"
共享内存(Shared Memory)是让多个进程把同一块物理内存映射到自己的地址空间里,进程直接读写这段内存,不需要内核参与数据拷贝,速度在所有IPC里几乎是最快的。
代价也很明确:多个进程同时读写同一块区域会数据竞争,必须配合信号量或锁来同步。共享内存的生命周期是显式的,创建后要记得清理,否则那块内存会一直占用。典型用法是配合shmget()/shmat()再加上信号量做"生产者-消费者"模型。性能敏感的场景,比如消息中间件Broker内部的缓存、高性能日志缓冲,基本都是共享内存方案。
5.4 信号:控制指令比数据更划算
信号(Signal)严格来说不是传输数据的,而是操作系统用来通知进程"发生了某件事"的异步消息,比如SIGINT(Ctrl+C)、SIGKILL、SIGTERM。它能传递的信息量很小(只是一个编号),但特点是快、支持异步。进程可以给信号注册自定义的处理函数,处理完再恢复。
需要注意SIGKILL(9)和SIGSTOP(19)这两个信号不能捕获、不能屏蔽、不能忽略——这就是为什么kill -9通常能终结进程,也是它杀不死D状态进程时显得无比无力的原因。
5.5 Socket:跨机器也通用的"万能管道"
Socket不在同一台机器也能通信,这是它比前面几种优越的地方。本机进程之间可以用Unix Domain Socket(本地套接字),网络间的进程可以用TCP/UDP Socket。它的接口统一、编程模型清楚,因此成为分布式系统里最主流的通信方式。
一句话总结选型:要快,用共享内存;要可靠排队,用消息队列;要简单流式,用管道;要信号通知,用信号;要跨机通信,用Socket。
6. 实操:Linux和Windows下查进程、杀进程的完整姿势
光懂理论不行,一线排查问题必须会操练。网上搜"linux查看进程"、"windows cmd杀进程"、"任务管理器结束进程弹出拒绝访问"的朋友特别多,我把常用的命令和坑一次整理清楚。
6.1 Linux下查进程
最常用的三件套:ps、top/htop、pgrep。
ps -ef看到的是全量进程快照,ps -aux在BSD风格下也能看到类似信息,关键看以下几列:
PID:进程号PPID:父进程号(排查多余进程时,先看PPID)STAT:进程状态(前面提的R、S、D、T、Z)%CPU/%MEM:CPU和内存占用率COMMAND:实际启动的命令行
只看到固定格式还不够,多数时候要找"哪个进程在占用8000端口",配合netstat -tlnp | grep 8000或ss -tlnp | grep 8000。如果是找"哪个路径下的程序在跑",lsof +D /path或lsof +f -- /path能列出打开该目录文件的进程。
动态监控CPU/内存用top,按P键按CPU排序,按M键按内存排序;更友好的是htop,支持树状视图(按F5),能直观看到父子进程关系。写脚本时用pgrep -f "关键字"拿PID比ps | grep快得多。
6.2 Linux下杀进程
顺序要记住:先温和后激烈。杀普通进程用kill <PID>,它发送SIGTERM信号,给进程留出清理文件、释放资源的善后时间。如果进程拒不退出,再用kill -9 <PID>发送SIGKILL强制终结。
但有两个坑:
第一,kill -9杀不死不可中断睡眠(D状态)的进程。这种情况多半是进程在等待磁盘I/O或NFS网络存储,内核层卡住了。解法通常不是硬杀,而是修复底层I/O(比如网络存储恢复),进程才有机会退出。你可以在ps -eo pid,stat,cmd里看到D状态进程,先确认它卡在哪个资源上。
第二,杀父进程不代表子进程会被连带杀掉。子进程被init(PID 1,系统里所有进程的祖先进程)收养,继续活着。要连带杀掉整棵进程树,可以用pkill -P <父PID>先杀子进程,再杀父进程;或者用kill -- -<进程组ID>按进程组杀。
6.3 Windows下查进程和杀进程
Windows下很多人只在任务管理器里点点点,用命令行其实更快。tasklist列出所有进程及其PID和内存占用,wmic process list full能看到更详细的信息包括可执行路径和命令行参数(新版PowerShell更推荐Get-Process / Get-CimInstance Win32_Process)。
按端口查进程是另一个高频需求,比如本机8080端口被占用,用netstat -ano | findstr :8080,最后一列是PID,然后tasklist /FI "PID eq <PID>"看是哪个进程。
杀进程用taskkill /PID <PID> /F。如果提示"拒绝访问",通常原因有三个:进程以管理员权限运行,命令提示符没以管理员身份运行;进程是系统关键进程(比如wininit.exe、csrss.exe等),受保护,强制杀会触发系统保护机制;还有一类就是前面说的进程处于不可中断内核等待中。遇到这种情况,右键"以管理员身份运行"命令提示符再执行。如果还杀不掉,优先用wmic process where processid=<PID> call terminate试试,再不行就去确认是不是Windows Defender或安全软件在保护进程。
6.4 任务管理器进程空白、进程过多这类问题的排查思路
很多人遇到"任务管理器进程空白"会以为是系统出大事了。其实多半是权限不足——任务管理器没以管理员权限打开,看不见其他用户的进程。还有一种情况是explorer.exe崩了,任务管理器UI刷新失效。用命令行tasklist能看到进程,说明系统层面没问题,果断重启资源管理器基本能解。
"进程过多"要先分清楚是正常系统机制还是挖矿木马。Windows本来就有几十个系统进程,加上浏览器多进程架构、Java进程、PowerShell子进程,数量上百很正常。判断异常的标准是:CPU/内存占用异常高的位置是否与已知程序对应,启动路径是否在可疑目录,PPID是否指向奇怪的对象。看到不认识的名字(比如nhdoohgu这种乱码进程名),第一反应应该是在Win+R输入msinfo32,或者用wmic process get name,executablepath,processid,parentprocessid查出可执行文件路径,去路径目录和签名里找线索。一句话:不认识的进程别急着杀,先查路径、签名和父进程。
7. 几个高频进程相关疑难杂症的排查链路
最后把网上最常被搜的几类"进程疑难杂症"单独拿出来分析,因为这些案例背后往往映射着某种机制,理解了能举一反三。
7.1 kill -9为什么杀不死Java进程
Java进程用kill -9杀不死,常见三种情况:
- 进程处于
D状态,内核I/O等待;此时任何kill信号都排队进不去。 - 你杀的是父进程(比如脚本启动的Java进程),但Java实际跑在子进程里;
kill的PID可能是Shell脚本的PID。 - Java程序里有JNI调用或native code卡在不可中断的系统调用里;
kill -9无法中止这类调用,只能重启机器。
建议排查链路是:ps -eo pid,ppid,stat,cmd | grep java先定位真正的Java进程PID和状态;用jstack <PID>看线程栈有没有卡死的线程(如果jstack连不上,说明JVM已经挂死或处于极不正常状态);然后用kill -3 <PID>优雅打印线程转储,再考虑kill -9。生产环境不要动不动kill -9,脚本写kill -9前至少要打日志、做线程转储,否则故障现场全丢了。
7.2 客户端与Linux断开后,Nohup程序还在运行吗
这是个经典问题。SSH会话断开时,终端Shell会给前台进程发送SIGHUP信号,默认行为是终止进程。但如果你用了nohup启动命令,它会让进程忽略SIGHUP,再配上&放到后台,退出终端后进程照样活。
自己写脚本时,更稳妥的做法是用setsid启动新会话,完全脱离控制终端;或者用systemd管理服务单元,让服务具备自动重启能力。别只依赖nohup,它只管忽略挂断信号,不解决进程被别的原因杀掉的问题。最稳的是把关键服务交给systemd,写个简单的unit文件,崩溃自动拉起,这才是正经做法。
7.3 Windows下WPS进程无法关闭和任务管理器结束进程弹出拒绝访问
这类问题其实分两层。第一层是WPS等办公软件会常驻后台,打开很多文档后会有多个进程留守,直接关会提示"正在使用"。先用tasklist | findstr /i wps列出所有WPS进程,再逐个taskkill,不要只杀一个。第二层是杀进程时"拒绝访问",按前面说的权限判断顺序走,管理员身份运行CMD,禁用杀软的进程保护,再试wmic process where "name like '%wps%'" call terminate。
更本质的办法是找到WPS设置里"关闭后保留后台进程"的选项,从源头上减少僵尸常驻。很多国产软件都有类似的后台驻留策略,学一次,后面遇到同类的心里就有数了。
7.4 开机后看到kswapd0进程CPU高
kswapd0是Linux内核的交换守护进程,负责内存回收。它CPU高,多半是系统物理内存不足,内核在不停地把页面换入换出,或回收文件缓存页来应急。你应该做的是查内存:free -h看available是否见底,top按内存排序找出吃内存的大户。临时性解决是杀掉或重启那个占用大户;根本解决要么加内存,要么优化业务内存使用。千万别拿kill -9 kswapd0,它是内核线程,杀掉会导致系统不稳定甚至崩溃。
7.5 Java的jps报了"增量注解进程已禁用"
这个问题在开发环境很常见,JAVA_TOOL_OPTIONS或编译插件引入了增量注解处理开关,导致IDEA编译时提示"jps增量注解进程已禁用"并伴随"使用构建进程"提示。它不一定是错误,只是说明增量编译没有启动,重新编译可能变慢,但不影响功能。如果非要消除,建议在编译器配置里关闭"Incremental compilation"或用-proc:full显式指定注解处理模式。这个问题的价值在于:提醒你别把jps输出为空和"系统里Java进程不存在"画等号,jps只能看到它自己有权限访问的JVM。生产环境用ps -ef | grep java更可靠。
8. 一点自查和排障的"土办法"
聊了这么多,最后分享一个我自己排查进程问题时的土办法,不花哨但非常顶用。
遇到"什么进程不对劲",先别急着杀,按这个顺序来:
- 先确认是什么进程:
ps -eo pid,ppid,stat,%cpu,%mem,cmd(或Windows的wmic process get),把PID、PPID、启动命令行这三样抄下来。 - 确认谁拉起的:看PPID。如果父进程是
1(init/systemd),说明它是孤儿进程(原父进程退出了,被收养),不一定是坏事,但值得留意。 - 确认程序路径:
ls -l /proc/<PID>/exe(Linux下能直接看到可执行文件的真实路径),Windows下用wmic process where processid=<PID> get executablepath。 - 确认网络连接:
ss -tnp看这个进程是否有不寻常的外联网络连接,尤其连向未知IP的。 - 确认资源占用:
top -p <PID>盯一会,看CPU和内存波动规律。 - 最后才是杀:先
kill <PID>(SIGTERM),观察最多几秒钟,不行再kill -9。
这套流程能筛掉九成"莫名其妙"的问题。很多所谓"神秘进程"其实只是浏览器预加载、系统更新服务、扫描软件的后台任务,看了路径和父进程后一目了然。
进程这个话题第一篇就先写到这。基础的抽象、PCB、状态、线程边界、IPC方式、常用命令和常见坑都过了一遍,后续第二篇我打算深入进程调度的具体算法(CFS/时间片轮转/多级队列),再把同步互斥和死锁一起盘点掉。真用到的时候回来翻这篇,应该能帮你省不少排查时间。
