尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

进程是什么?从原理到故障排查的完整指南

进程是什么?从原理到故障排查的完整指南 我先把这个话题说清楚进程Process是操作系统的核心抽象之一是“正在运行的程序”的载体。无论是学习操作系统原理、排查线上服务故障还是写多进程/多线程代码你对进程的理解深度直接决定了你能不能在关键时刻快速定位问题。这篇文章我打算从三个层面来写先讲清楚进程到底是什么、为什么操作系统非要引入“进程”这个概念再拆解进程的内部结构、生命周期和状态流转让知其然也知其所以然最后落到实操把进程查看、进程通信、进程管理、常见故障排查这些日常高频场景全部过一遍并且给出具体的命令、工具和避坑经验。适合正在学操作系统的学生、写后端服务的开发者以及天天跟服务器打交道的运维朋友。1. 核心思路拆解为什么“进程”不是“程序”1.1 程序只是菜谱进程才是烹饪过程很多人刚接触这个概念时最容易卡在“进程和程序有什么区别”上。我的理解方式是用一个厨房来类比程序是磁盘上一份静态的菜谱记录了一堆步骤和食材清单而进程是厨师按照菜谱实际开火烹饪的过程它有自己的灶台、锅铲、调料以及“现在做到第几步了”的现场状态。程序放在那里一万年也不会变它只是一堆二进制指令。可一旦你双击运行它、在命令行敲下启动命令操作系统就得为它分配内存、创建执行上下文、给它安排CPU时间片这一刻程序就“活”过来了变成了进程。所以进程一定包含“动态”的特征它在执行、在推进、在消费资源。这个区别是所有后续讨论的基石。1.2 为什么操作系统非要引入进程没有进程这门“学科”之前计算机的运行方式很简单一次只跑一个程序跑完再跑下一个。但很快就撑不住了。第一CPU的速度远快于外设程序在等磁盘、等网络的时候CPU在空转浪费得肉疼第二多个程序同时装入内存后直接裸奔访问物理内存会互相踩踏程序A把程序B的数据改了系统立刻崩给你看第三一个程序死循环了整个机器就彻底卡死没有任何挽留余地。所以进程被设计出来本质是为了做好两件事隔离和复用。隔离是指每个进程拥有独立的地址空间、独立的资源记录谁也不能随便碰谁的内存一个进程崩溃不会拖垮整个系统复用是指操作系统可以通过进程切换让所有进程公平地分享CPU让CPU在I/O等待期间去跑别的进程不浪费一分一秒的处理能力。1.3 设计思路的连锁反应为了支撑“隔离和复用”操作系统必须给每个进程建一套完整的管理档案这就是后面要讲的进程控制块PCB为了在多个进程之间切换必须设计一套状态机和调度算法为了让进程之间能协作而不是老死不相往来又有了IPC进程间通信机制。可以说进程这个概念是整个操作系统的骨架后面学的线程、锁、信号量、管道、套接字全是围绕“把进程管理好、让进程协同好”这两个目标延伸出来的。这也是为什么我在排障时遇到任何诡异现象第一步永远是先看进程进程没了是启动失败进程还在但没响应是卡死在某个资源上进程变多了可能是请求被错误地分发或者代码里疯狂fork。理解了进程这个抽象你看到的就不再是“系统坏了”而是“系统里某个进程处于什么状态、卡在什么环节”。2. 进程的内部结构从控制块到虚拟地址空间2.1 进程控制块进程的“身份证病历本”每个进程在操作系统内核里都对应一个数据结构叫进程控制块Process Control BlockPCB有的教材也叫任务控制块。它就是进程存在的唯一凭证内核没有为某个程序创建PCB那它就不算一个进程。PCB里记录的信息非常杂但可以分四组来看进程标识信息进程IDPID、父进程IDPPID、用户IDUID。PID是内核分配的唯一号码类似身份证号所有查进程、杀进程的操作都是围绕PID展开。处理器状态信息程序计数器PC、通用寄存器、状态字等。这些是进程被切换出去时“现场保护”的内容等进程重新获得CPU时内核原封不动地把寄存器值写回去进程接着往下跑完全感觉不到自己被暂停过。进程调度信息进程状态运行/就绪/阻塞、优先级、等待事件等。调度器就靠这些信息决定下一个该谁上CPU。内存管理信息代码段、数据段、堆、栈的地址范围页表指针等。虚拟地址空间映射到物理内存的所有信息都在这里。资源使用信息打开的文件描述符表、消息队列、信号处理函数、I/O设备列表等。一句话总结PCB就是进程在操作系统里的“病历本”记录了一切关于它的动态信息。每次进程切换本质就是“保存当前进程的PCB、加载下一个进程的PCB”的过程这个过程叫上下文切换Context Switch。2.2 虚拟地址空间每个进程都以为独占整个内存这是很多初学者最容易迷惑的点既然内存只有4GB为什么每个进程都感觉自己有独立的4GB因为操作系统给每个进程做了一个虚拟地址空间。CPU拿到的地址不是真实的物理地址而是一个虚拟地址需要经过页表翻译才能访问物理内存。这个设计带来的好处非常明显。进程A和进程B都访问地址0x1000翻译之后可能指向完全不同的物理内存彼此无感一个进程访问了没有映射的地址CPU立刻触发缺页异常或段错误不会把别人的数据踩了。一个典型的虚拟地址空间从低地址到高地址大致分布为代码段存放可执行指令、已初始化数据段、未初始化数据段BSS、堆动态申请内存向高地址增长、内存映射区共享库、mmap、栈局部变量、函数调用栈向低地址增长。我补充一个实操场景排查Java进程内存占用高时经常看到占用的虚拟内存VSZ特别大、但物理内存RSS正常这通常就是因为JVM的堆和元空间把虚拟地址空间预留得很大并没有全部真实使用。所以看内存占用不要只看VSZ要结合RSS和实际堆使用情况综合判断。2.3 进程状态与状态转换别被“卡死”迷惑了眼进程不是生下来就一直跑它会在多个状态之间反复横跳。经典的三态模型是运行态Running、就绪态Ready、阻塞态Blocked也叫等待态。运行态表示进程正在CPU上执行就绪态表示进程万事俱备、只欠CPU排队等待调度阻塞态表示进程在等待某个事件比如磁盘I/O完成、用户输入、锁释放暂时无法执行。状态转换逻辑要特别注意就绪 → 运行调度器选中该进程把CPU分配给它。运行 → 就绪时间片用完或者被更高优先级进程抢占它只能回到就绪队列排队。运行 → 阻塞进程主动发起I/O请求或等待某事件暂时不需要CPU了。阻塞 → 就绪等待的事件完成比如I/O返回进程再次具备运行条件但要排队等CPU。理解这个模型的最大价值在于排障。当你在ps里看到一个进程处于D状态不可中断睡眠时它其实相当于“深度阻塞”通常是在等待内核层面的磁盘I/O这种状态连kill -9都杀不掉要等I/O返回或系统重启才能恢复。当看到大量进程处于S可中断睡眠状态时说明它们在等资源这时候要看是不是锁竞争、连接池耗尽或者网络超时。3. 进程与线程一对剪不断理还乱的兄弟3.1 为什么又要引入线程进程已经能隔离资源了但它的代价有点大每次创建进程都要分配独立的地址空间和资源上下文切换至少要切换页表、刷新TLB成本高而且进程之间的数据共享极不方便后面会讲IPC有多麻烦。可现实中很多场景需要多个执行流并发地访问同一份数据比如一个Web服务器要同时处理成千上万个请求它们都操作同一个内存缓存。所以线程被设计成“进程内部的执行单元”。一个进程可以包含多个线程这些线程共享进程的地址空间、全局变量、打开的文件等资源但每个线程有自己独立的栈和寄存器上下文有自己的调度状态。用一句话概括进程是资源分配的最小单位线程是CPU调度的最小单位。进程是“老板”线程是“员工”老板花钱租办公室资源员工们共用办公室干活。3.2 进程与线程的对比表对比维度进程线程资源拥有者独立拥有地址空间、文件、信号等共享所属进程的资源系统开销创建/切换开销大页表切换、TLB刷新创建/切换开销小仅切换线程上下文通信方式需要IPC机制成本高直接共享全局变量成本低崩溃影响一个进程崩溃不影响其他进程一个线程崩溃如段错误可能导致整个进程崩溃可靠性高隔离性好低需要小心处理同步与异常这也是为什么多进程程序比多线程程序更“抗造”但代价是吃内存、通信麻烦多线程程序性能高、方便共享但容易踩并发坑数据竞争、死锁、竞态条件。我见过太多同学上线前自信满满结果一压测就发现临界区没加锁统计数字全乱了。3.3 线程模型1:1、N:1、M:N实际说到线程还要分用户级线程和内核级线程。用户级线程比如早期绿色线程、协程完全在用户态调度内核只看到一个进程切换极快但一个线程阻塞会阻塞整个进程内核级线程由操作系统内核管理每个线程都是调度单位一个线程阻塞不会影响其他线程但切换需要陷入内核开销大。主流操作系统Linux、Windows用的都是1:1模型一个用户线程对应一个内核线程协程则是把M:N模型的思路用到了极致。了解这个区别你就明白为什么高并发场景下协程能扛住那么多连接、还要配合异步I/O了。4. 进程通信IPC隔离了但不隔绝4.1 为什么不能让进程直接共享数据很多初学并发编程的人第一反应是进程A把变量x改了进程B直接用不就完了吗问题是虚拟地址空间隔离是操作系统最底层的安全保证进程A的地址和进程B的地址根本不在同一个物理内存上强行共享等于废掉隔离机制。所以进程间要交换数据必须经过内核提供的专门通道这就是IPC。4.2 常见IPC方式对比IPC方式核心思想优点缺点典型场景管道Pipe一个进程写、一个进程读的字节流实现简单使用方便半双工、无结构字节流、容量有限shell中的|连接命令命名管道FIFO有路径名的管道不限于父子进程支持任意进程通信仍需文件系统效率一般两个独立进程间的简单通信消息队列内核维护的消息链按类型读取有结构、支持多类型、解耦容量有限、需要拷贝请求/响应模式的轻量异步通信共享内存映射同一块物理内存到各自地址空间速度最快零拷贝访问需要自己处理同步高频大数据量传输数据库缓冲池信号Signal异步通知机制非常轻量用于控制只能传信号值信息量少kill -9、CtrlC、超时通知套接字Socket网络抽象跨主机通信跨网络、通用协议栈开销大HTTP服务、RPC、分布式系统4.3 为什么共享内存最快但最危险共享内存的原理很简单内核把一块物理内存同时映射到多个进程的虚拟地址空间进程A往这块内存写数据进程B立刻能看到整个过程不需要进入内核做数据拷贝所以性能是所有IPC方式里最好的。但它也是唯一一个“操作系统不管同步”的方式——多个进程同时写就会脏读、覆盖必须配合信号量或锁来自行保证互斥。我在实际项目中遇到过真实案例一个C后台系统用共享内存做数据分发高峰期出现数据错乱查了半天才发现是发送端和接收端对同步标志位的读写顺序错了。要记住共享内存只解决了“数据能共享”没解决“数据不会乱”后者永远是你的责任。4.4 消息格式与同步方式IPC调优的本质搜索热词里提到“消息的格式与进程的同步方式”这两点其实是IPC设计的核心矛盾。消息格式决定了通信内容的组织方式是二进制结构体高效、需约定字节序和对齐、JSON/XML易读、有解析开销还是Protobuf/Thrift结构清晰、序列化性能好。同步方式决定了消息从发送方到接收方的“节奏”是阻塞同步发送后等待结果、异步回调、还是发布订阅推拉模式。选型建议我给三条第一数据量大、对延迟敏感优先共享内存信号量第二数据量小、需要跨机器直接用消息队列或套接字别为了省事把共享内存搞成分布式方案第三任何跨语言的IPC或网络通信强烈建议用Protobuf这类自带版本管理能力的序列化方案否则字段一变双方编译出来对不上排查起来会疯掉。5. 进程管理实操从查到杀的全流程5.1 Linux下查看进程ps、top、pstree的组合拳在Linux上排查问题最常用的就是ps命令。给你一个我日常最常用的配置ps -ef # UID PID PPID C STIME TTY TIME CMD # root 1 0 0 08:00 ? 00:00:05 /sbin/init-e表示显示所有进程-f表示全格式输出。需要关注的有几列UID进程运行用户、PID进程号、PPID父进程号、CCPU占用率百分比、TIME累计占用CPU时间、CMD启动命令。如果只想知道某个关键字对应的进程用pgrep更快pgrep -af java # 17420 java -Xmx2g -jar app.jar查看父子进程树我用pstreepstree -p # systemd(1)─┬─nginx(1005)─┬─nginx(1006) # │ └─nginx(1007) # ├─java(17420)─┬─{java}(17421) # │ └─{java}(17422)这个视图特别有用能一眼看出进程之间的从属关系。热词里提到的“windows查看进程父进程”在Linux上对应的就是ps -ef里的PPID列配合top -H还可以看到每个线程的PID。5.2 怎么查看某个路径下运行的进程有时候你想知道某个目录下的程序到底有没有在运行、被谁运行了光靠ps -ef不够因为命令名可能看不出路径。我的做法是两步走which java # /usr/lib/jvm/java-8/bin/java lsof /usr/lib/jvm/java-8/bin/javalsoflist open files可以列出正在使用某个文件的所有进程。这个命令太强了排查“端口被谁占了”“文件被谁锁了”都靠它lsof -i :8080 # java 17420 root 456u IPv6 123456 TCP *:http-alt (LISTEN) lsof D /var/log/myapp/5.3 Windows下查进程与杀进程任务管理器之外的方法Windows排查进程图形界面用任务管理器但服务端或脚本场景下命令行才是效率之王。打开PowerShell或CMD输入tasklist # 映像名称 PID 会话名 会话# 内存使用 # java.exe 17420 Console 1 78,456 K按名字找PID可以配合findstr过滤tasklist | findstr java杀进程用taskkill两种方式都支持taskkill /PID 17420 /F taskkill /IM java.exe /F/F表示强制终止。你可能遇到“拒绝访问”的报错这种情况多半是进程以管理员权限运行而你当前CMD不是管理员需要用管理员身份打开终端再操作。还有一点要注意taskkill /IM java.exe会杀掉所有同名进程慎用尤其是服务器上可能同时跑着多个Java服务别一键全给清了。5.4 任务管理器进程空白怎么排查热词里有个很现实的问题“任务管理器进程空白”。我碰到过几种原因一种是系统资源耗尽任务管理器本身加载不出来这时候按CtrlAltDel进到安全选项界面先看能不能操作另一种是某些安全软件或显卡驱动导致Explorer异常explorer.exe挂掉后桌面图标和任务栏都会消失。解决思路是先按WinR输入taskmgr强开任务管理器如果任务管理器白屏用CtrlShiftEsc试试再不行就重启explorer.exe进程。核心逻辑是任务管理器本身也是进程它出问题不代表其他进程出问题先恢复它再诊断。5.5 kill -9为什么杀不死进程这是运维圈永不过时的高频问题。先说结论kill -9杀不掉的原因是进程处于两种特殊状态之一。第一种是D状态即不可中断睡眠Uninterruptible Sleep常见于进程正在等待磁盘I/O如NFS挂载卷卡住、硬件故障这种状态下的进程在内核态无法响应信号kill -9没用只能等I/O恢复或者重启机器。第二种是僵尸进程Zombie特征是状态为Z它其实已经死了只是父进程没有调用wait()回收它的PCB。僵尸进程不占CPU内存但占PID号数量多了会耗尽PID。kill -9对僵尸进程无效因为你要杀的其实是一个已经死掉的进程正确做法是杀掉它的父进程让initPID 1接管回收。还有第三种情况容易被忽略进程本身收到信号后卡在无响应的系统调用里比如内核模块死锁。这种只能靠内核栈分析或者重启。热词“终端进程已终止,退出代码: -1”其实也跟这个相关VS Code等领域开发工具报这个错通常是子进程被外部强制杀掉比如内存不足触发OOM Killer或者启动器权限不对排查方向放在系统日志和可用内存上。5.6 进程守护与进程池让服务更稳定生产环境里的服务进程不可能全靠人工盯着这就用到了进程守护。常用的有supervisor、systemd、宝塔自带的进程守护管理器。核心机制都是守护进程监控目标进程状态发现退出就自动拉起重启。我特别强调一点守护进程也需要配置重启次数限制和退避时间如3秒内连续重启5次就放弃否则进程因为配置错误每秒崩溃一次守护进程也跟着每秒拉起一次形成“重启风暴”比不守护更可怕。进程池则是为了解决“频繁创建销毁进程开销大”的问题预先创建一批工作进程任务来了分发下去跑完归还。Python的concurrent.futures.ProcessPoolExecutor、Node的cluster、Java的ForkJoinPool都是这个思路。但注意进程池里的进程数不是越多越好CPU密集型任务的进程数最好等于或略少于CPU核心数nprocI/O密集型可以适当多开但也要防止内存撑爆。6. 进程相关高频故障排查实录6.1 Java进程排查jps看不到进程怎么办热词“arthas启动无法获取jps进程”是我见过很多次的坑。用Java的人都熟悉jpsJava Virtual Machine Process Status Tool用来列JVM进程。但环境里Java应用是别人用其他用户启动的时候你执行jps可能什么都看不到这不是Java没跑而是权限受限jps只能看到当前用户且有权限访问/tmp下的hsperfdata目录的JVM进程。解决办法# 1. 用ps直接搜java进程 ps -ef | grep java # 2. 切换到启动用户再执行jps sudo -u appuser jps -l # 3. 使用jcmd或jattach直接指定PID jcmd 17420 GC.heap_info还有个配套问题很多同学用Arthas诊断时提示“没有找到java进程”也是同一个原因直接用ps -ef | grep java查出PID再用./as.sh PID绑定即可。6.2 进程CPU飙高从进程到线程再到代码排查Java进程CPU飙高的完整链路是# 1. 先找到CPU占用最高的进程PID top -c # 2. 再按线程查看找到占用最高的线程TID注意top里显示的线程号是十进制 top -H -p 17420 # 3. 把TID转十六进制 printf %x\n 17421 # 4. 用jstack导出线程栈搜索十六进制线程号 jstack 17420 | grep -A 30 0x441d这个链路的关键在于jstack输出的线程号是十六进制而top -H给的是十进制不转一下你就永远找不到对应代码。整个过程十分钟内能定位到具体方法比瞎猜高效得多。6.3 nginx worker进程以root运行的安全隐患热词“nginx worker进程运行用户为:root,这个漏洞怎么处理”也是真实生产问题。Nginx的master进程需要绑定80端口通常以root启动之后会降权运行worker进程。如果worker还以root身份跑一旦Web应用有漏洞被上传了恶意脚本攻击者就直接获得root权限风险极大。安全配置很简单在nginx.conf里显式指定user nginx; worker_processes auto;然后确保nginx用户对日志目录、Web目录有读取权限动态代理场景下可能还要写权限。改完nginx -t验证然后重启。这个看似不起眼的配置很多人故意忽略只为图省事但线上被人拿到root后果不是你一句“我不知道”能兜得住的。6.4 进程启动失败posix_spawnp与VS Code报错VS Code的终端如果报“进程启动失败: 无法在启动时发生本地异常posix_spawnp failed”通常是shell路径配置错误或权限问题。排查步骤是先确认默认shell配置是否正确在设置里检查terminal.integrated.profiles.linux再看/bin/bash是否存在、是否可执行最后检查安装目录是否有写权限。这里的底层逻辑就是VS Code的集成终端本质上是启动一个子进程shell如果启动参数、环境变量或权限不对子进程创建失败终端就崩。6.5 进程数量过多windows powershell进程过多“windows powershell 进程过多”的现象我在客户机器上见过不止一次。常见原因是计划任务或启动项里反复调用PowerShell脚本每次调用都启动一个新的PowerShell进程脚本执行完却没退出或者脚本里执行了比较重的cmdlet导致进程长期驻留。排查命令# 统计PowerShell进程数量 Get-Process powershell | Measure-Object # 看每个进程的命令行需要管理员 Get-CimInstance Win32_Process -Filter namepowershell.exe | Select ProcessId, CommandLine解决办法是找到启动源在任务计划程序、注册表Run键、启动文件夹里查临时应急就批量结束Get-Process powershell | Stop-Process -Force7. 我的实操体会与一个压箱底的建议我在这行干了这些年最大的体会是进程这个概念看起来基础实际上是把操作系统、编程语言运行时、运维故障串起来的一条主线。很多看似莫名其妙的线上问题往深了挖最后都是进程状态、进程权限、进程通信方式的问题。程序启动失败去查进程有没有起来服务变慢去看进程是CPU跑满还是等I/O数据对不上去想进程之间的共享和同步有没有设对。说实话90%的故障排查都逃不出“进程视角”这四个字。最后分享一个小技巧是我压箱底的经验排查任何进程相关问题先把系统时间、用户权限、文件句柄数、内存几个基础项看一遍往往比直接扎进代码里更快。比如用ulimit -n看看进程的文件描述符上限很多“进程假死”其实就是句柄泄漏耗尽用free -h看看内存很多“进程被杀死”就是内存不足OOM。把这些基础项当成例行检查比什么都管用。
返回列表