ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

进程:计算机世界的执行单元——概念、生命周期与实战排查

进程:计算机世界的执行单元——概念、生命周期与实战排查 每次有人问我“进程到底是什么”我就想起那次同事的求助电话说 F 盘根目录下一个临时文件夹怎么也删不掉系统提示“另一个程序正在使用此文件夹”但任务管理器翻了三遍也看不出是谁占用的。他在电话里嘀咕“这跟电脑中毒了似的。”我说这不是中毒这就是进程的基本操作——你的文件被某条执行路径锁住了只不过你没找到那条路径。这个场景很常见也特别能说明事情的本质进程不是一个能看得见摸得着的东西但它实实在在管理着运行时的文件句柄、CPU时间、内存空间以及你感受到的卡顿、假死、杀不掉的程序、莫名其妙的内存报错。把“进程”这个概念搞明白了Windows 的任务管理器、Liunx 的 ps 命令、Java 服务的 OOM 日志、SQLServer 的异常终止大部分问题都能自己分析出个七七八八。这篇文章就是围绕“进程计算机世界的执行单元”这个主题把概念、生命周期、通信、常见疑难杂症和排查方法摊开来聊。不管你是刚学操作系统原理的在校生还是整天跟服务器报错打交道的开发运维都能从中找到直接能用的思路。1. 先把“进程”和“程序”的关系掰扯清楚1.1 程序是静止的进程是活着的很多人第一个卡住的地方就是程序Program和进程Process到底差在哪我用一句大白话说程序是硬盘上的一段静态文件进程是这段文件被加载到内存并交由 CPU 执行后形成的整体运行环境。举一个生活化的例子。你想做一道菜菜谱就是程序它写在纸上不占灶台、不用锅铲但你真正开始洗菜切菜时灶台上多了一个奶油色的“做菜现场”——食材被摊开、火候在变化、调料瓶摆了一排这个现场就是进程。菜谱不会因为你手上被溅到油就改变但进程的状态会它随时随地都在变。所以进程不仅是“运行起来的代码”它还携带了运行现场寄存器状态、栈帧里的局部变量、堆上的动态数据、打开的文件描述符、网络连接句柄、环境变量、工作目录。程序文件可以同时启动很多份每一份启动后的现场是完全独立的——这就是为什么你打开三个浏览器窗口它们互不干扰因为它们对应着三个进程各自维护各自的标签页和内存空间。1.2 进程内部装了什么PCB、地址空间、句柄表要给一个进程做“解剖”重点看三样东西。第一是进程控制块PCBProcess Control Block。这是操作系统内部为该进程建立的一个数据结构记录着进程编号、状态、优先级、CPU 寄存器的保存现场、调度信息等。你想成是医院里的病历档案病人进程在病房里活动档案跟着走护士操作系统一看档案就知道病人现在病情如何、下一步该安排什么检查。第二是地址空间。一个进程在内存里看到的是一段从低到高的“虚拟连续地址”它包含了代码段、数据段、堆、栈等区域。不同进程之间的地址空间是隔离的进程 A 完全访问不到进程 B 的堆这就是“进程隔离”。具体隔离到什么程度即使两个进程运行的是同一个可执行文件它们各自的内存页表也不同写操作谁也不会影响谁。第三是句柄/文件描述符表。进程打开的文件、建立的网络连接操作系统都会分配给这个进程一个唯一的整数编号集中记录在一张表里。前面提到的“F盘被另一个进程锁定”本质就是这个表中有人持有了该文件句柄且没有释放删掉之前文件系统不允许标记为可删除状态于是系统就给出“另一个进程正在使用此文件夹”的提示。这三样东西是排查问题的钥匙。遇到“进程删不掉”“进程退出了但端口还占着”“文件锁死”这类现象十有八九得从句柄表入手。后面第 4 节会展开讲。2. 进程的一生从创建、等待、换醒到异常终止2.1 五态模型里最容易被忽视的“等待”教科书中通常把进程生命周期描述为三态模型就绪Ready、运行Running、阻塞Blocked。更完整一点是五态模型新建New、就绪、运行、阻塞、终止Terminated个别教材再加上挂起状态。我实际处理问题时发现大多数人最容易忽略“阻塞”这个状态也就是热搜词里的“进程等待 wait”。阻塞到底怎么发生的当一个进程执行到read()等待磁盘数据返回或者等待键盘输入、等待网络数据包到来时它没有办法继续计算操作系统就把它的状态标记为阻塞让出 CPU调度器转去执行其他就绪进程。等 I/O 完成、数据到了操作系统把该进程的阻塞状态唤醒重新放入就绪队列。整个过程里进程自己没有“感觉”到时间流逝但任务管理器里你能看到它 CPU 占用率掉得很低。wait 这个系统调用在花括号之外也很常见。在 Linux 下父进程调用wait()或waitpid()的目的有两个一是等待子进程结束二是回收子进程退出后留下的返回值和资源占用。如果父进程一直不调用 wait子进程虽然逻辑上结束了但它的进程控制块还留在系统里内核线程会看到它处于“僵尸状态”也就是热搜里“进程等待 wait”真正对应的底层问题。2.2 僵尸进程与 wait 的意义僵尸进程是新手排查服务器问题时的经典盲区。它不是一个还在运行的进程它的代码段和内存早就释放了只剩下一个 PCB 尸骸里面保存着退出码。为什么内核不直接把这个 PCB 也删掉因为父进程可能还想知道子进程是正常退出还是异常退出、退出码是多少内核必须留一份记录等着父进程来“领走”。那么父进程要怎么领就是调用 wait 或 waitpid。子进程退出后父进程收到 SIGCHLD 信号然后在信号处理函数或主流程里调用 wait 回收。如果父进程不写这段逻辑僵尸进程就会一直累积又因为它们几乎不消耗 CPU只看top很难发现。我之前维护的老服务就出过一次积累了几千个僵尸进程的情况表象是机器明明很闲但新进程创建却报“资源不足”因为系统的进程表默认有上限PCB 满了。排查方法也简单ps -ef | grep defunct能直接列出僵尸进程。根治办法是修代码次选方案是在启动脚本里把子进程交给 PID 1 托管比如用 subreaper 机制不推荐用 kill -9 去杀僵尸——僵尸进程本来就不是被杀死的它是等被回收的。2.3 收到 SQL2000 服务进程异常终止后我做了什么热搜里有一条很具体的老问题“sql2000服务进程异常终止无法继续提供服务。”这个问题放在今天看有很强的教材价值因为关键在于“异常终止”四个字代表了进程最不常见的结局。进程正常终止一般是主动退出比如用户点了退出程序完成函数正常返回 main 并调用 exit异常终止则包括未捕获的异常、非法内存访问、磁盘满导致日志写入失败、系统强制终止等。SQL Server 2000 那类服务进程如果一天到晚被异常终止绝大多数情况下不是操作系统随机干的事而是某个组件写入了非法内存或某个线程栈溢出或 SQL 执行计划计算出问题导致进程自爆。我当时的排查思路是先看 Windows 事件查看器里的应用程序日志找到 faulting module 和异常偏移地址对应 SQL Server 的版本和补丁线然后检查数据库日志文件是否已满服务进程是否因为日志不能写入而被强制停止最后如果是老掉牙的 SQL2000 跑在 xp 架构的旧机器上还要考虑到内存条损坏这种硬件层面问题用内存诊断工具跑一整夜。一句话总结异常终止的排查永远是先找“为什么死”再谈“怎么重启”靠着重启脚本苟延残喘解决不了根因。3. 进程间通信IPC与多进程协作场景3.1 为什么隔离之后还得通信前面说进程之间有地址空间隔离这是保护机制,同时也带来一个问题多个进程如果要协作怎么交换数据于是进程间通信IPCInter-Process Communication就成了操作系统设计的核心内容。热搜关键词里“进程通信ipc”直接命中这个话题。你可能想问现在大家不是都写多线程吗为什么还要搞多进程通信因为进程隔离带来安全性一个服务崩溃不会拖垮另一个另外某些业务天然需要拆隔离层比如浏览器主进程和渲染进程之间渲染进程崩了浏览器还能把整个标签页关掉重开而不是整体卡死。这就是 chrome 系浏览器敢一个标签页承载一个进程的原因。多进程架构绕不开的一件事就是通信链路。常见 IPC 方式包括管道Pipe、命名管道FIFO、消息队列、共享内存、信号量、信号Signal、Socket以及现代框架里更抽象的消息中间件如 Redis、Kafka。操作系统管不了“应用间通信”它只管到 Socket 这一层剩下的交给应用。3.2 不同 IPC 方式的取舍管道、消息队列、共享内存、Socket我做过一个数据采集网关多进程之间既要传递大量采集数据又要保证某一路采集进程挂了时其他路不受影响选 IPC 时花了很大力气。这里把几种方式的适用情景说清楚。管道是最简单也最常用的一种。它本质上是一个内核缓冲区一端写、一端读写端不在时读端会得到 EOF。适用于父子进程之间因为子进程在fork时自动继承了父进程的文件描述符不需要额外配置。缺点也明显默认匿名管道只能用于有血缘关系的进程要跨两个不相关的进程得用命名管道。消息队列比管道更进一步它以“消息”为单位每条消息有类型和长度多个进程可以通过队列 key 访问同一个队列。应用场景是模块间传递结构化短消息比如多个工作进程把处理状态上报给管理进程。缺点是消息大小有限制且内核空间拷贝的代价在超大消息量时很高。共享内存是速度最快的 IPC因为进程直接把某段物理内存映射到各自的虚拟地址空间数据不需要在内核和用户态之间来回拷贝。但它有个致命问题需要配合信号量或锁来做同步否则两个进程同时写同一块区域数据就残了。我通常的做法是“共享内存承载大数据 POSIX 信号量控制读写顺序”性能能到每秒百万级消息比管道强一个量级。Socket 则把视角拉高。同一个机器上的进程可以用 Unix Domain Socket网络上的进程走 TCP/UDP Socket跨语言、跨机器都支持。业务系统里最常见的进程间通信其实就是通过 HTTP 接口或 gRPC 调用这本质上是建立在 Socket 之上的应用层通信。如果两个进程不在同一台机器那系统本身提供的 IPC 基本用不上只能靠网络协议把负载封装好。3.3 进程池、守护进程和前台进程监控不同形态的进程工程进程不只是一个操作系统概念在工程实践里演化出了很多常见形态。热搜词里“进程池”“守护进程与会话”“监控前台进程”就是这些形态的关键词。进程池是一组预创建的、可复用的工作进程。比如 Java 的 Tomcat 连接池、Python 的 multiprocessing.Pool、Go 的 worker pool 都是典型实现。为什么不用“来一个任务就新建一个进程”因为进程创建需要拷贝页表、分配 PID、初始化资源开销远大于线程甚至远大于某些网络操作。预创建一批进程挂在那里等待任务能显著减少任务启动延迟。在使用进程池时池的大小设置是有讲究的。池太小任务排队池太大CPU 上下文切换开销反而拖垮性能。经验值是 CPU 核数加一或两倍核数具体看任务是 CPU 密集还是 IO 密集。守护进程Daemon是后台常驻进程的代名词。它的特点是脱离了用户终端的会话Session进程组领导被置为一定规则后不接收终端控制信号也没有标准输入输出。Nginx、MySQL、Redis 这些服务安装后默认就以守护进程模式运行。Linux 下用setsid()系统调用创建新会话实现“守护”效果。而 Windows 里的服务程序Service与守护进程类似它由 SCM服务控制管理器管理启动、停止、失败重启都由系统帮你监督。这就是为什么 SQLServer 服务进程异常终止后如果你配置了“失败时自动重启”系统会拉起新进程。“监控前台进程”这个热搜背后是移动端和桌面端的一个共同需求知道当前用户正在使用哪个 App或者服务端需要知道某个关键进程是不是还活着。Windows 下可以遍历进程列表拿窗口句柄Android 上可以通过UsageStatsManager或前台 Activity 监听桌面 Linux 一般看焦点窗口的 PID。实际做客户端监控时要注意权限Windows 的某些用户进程被 Session 0 隔离后普通权限是看不到完整信息的这个坑在写监控工具时最容易踩。4. 高频热搜进程问题的排查链路4.1 Windows CPU 占用率 100%System 进程到底在忙什么“windows cpu占用率一直100syatem进程”这一条热搜几乎可以入选 Windows 贴吧年度月经帖。System 进程在任务管理器里看起来挺神秘System Idle Process 的 CPU 占用高说明 CPU 空闲但 System 进程本身占用高往往意味着内核线程很忙。System 进程里跑的是各种系统线程比如内存管理、断点处理、驱动回调、文件系统过滤等。如果 System 进程 CPU 长期 100%最常见三大原因一是某驱动有 bug频繁陷入内核态做错误处理。这种只能靠卸载最近安装的驱动、进入安全模式验证来定位。第二种是 Windows 的磁盘/内存压缩机制在极端情况下反复压缩和解压缩多见于机械硬盘小内存旧机器。第三种是第三方杀毒软件或备份工具往内核里塞了太多过滤驱动每个文件操作都要经过多层回调CPU 就上去了。排查时不要只看任务管理器那一列。我习惯用 Windows Performance AnalyzerWPA采集 30 秒的 CPU 采样数据按进程栈拆解看 System 进程下面到底哪些函数调用消耗最大。如果是 ntoskrnl.exe 里 fltmgr 相关的栈频繁出现那大概率是文件系统过滤驱动的问题如果是 storport、disk.sys 相关栈很长则重点查磁盘性能。简单粗暴地“结束 System 进程”没有意义系统不允许也解决不了内核线程的问题正确方向是定位驱动和硬件层。4.2 F 盘被另一个进程锁定找出元凶的完整步骤回到开头那个“F盘被另一个进程锁定”的问题。排查锁定进程我习惯按下面这条链路走基本不落空。第一步如果只是某个文件或文件夹占用先把能手动关的窗口都关掉资源管理器窗口、编辑器、命令行终端。大多数人会漏掉“命令行里的当前目录正好在 F 盘”这个细节cmd 的当前工作目录会使盘符句柄被进程持有。第二步使用系统自带的openfiles命令。在管理员权限的 cmd 下执行openfiles /query /v可以看到系统记录的所有打开的文件和锁定它们的进程。不过注意 openfiles 要启动全局标志才生效没开的话会提示“信息不足”这时需要先运行openfiles /local on并重启。第三步用微软官方工具 Sysinternals 套件里的 Handle。执行handle.exe F:\目录 /accepteula输出里会列出锁定句柄的进程名和 PID。拿到 PID 后到任务管理器定位或者是命令行执行taskkill /PID xxx /F尝试结束。第四步如果 Handle 显示进程是 explorer.exe 而你又关闭了所有相关窗口还锁着那通常是资源管理器本身的缩略图缓存或预览窗格在搞鬼重启 explorer 就能解决。这一步一步看似乎很简单但实际排查最耗时的不是找工具而是分辨“文件是被正常业务持有的”还是“异常泄漏导致的持有”。如果是自己写的程序抓进程快照后一定要看句柄是否被逐步泄露——模块里打开了文件却忘了 CloseHandle这种 bug 在服务端反复运行几天之后会把整个磁盘的文件都锁死那时候再叫 Handle 工具也难定位到几百个重复句柄。4.3 杀不死的小强com.vortex.helper 和 ThunderPlatform 的自我保护机制另一个高频卡点是“一直出现 com.vortex.helper 这个进程杀了一个 PID又换了一个”以及“thunderplatform占用进程”。这两个现象在原理上如出一辙进程本身有父子进程守护和快速重启机制同时服务组件会在后台重新拉起被杀的模块。你看到杀死 PID 后马上出现新 PID说明你杀掉的只是“工作者进程”真正的控制器进程还活着。控制器进程监控到 worker 退出立刻创建一个新的 worker。如果没有复杂的权限保护解决思路很简单找到父进程链条把父进程一起杀掉。在命令行下用wmic process where (namecom.vortex.helper) get ParentProcessId或者 PowerShell 的Get-Process -Name com.vortex.helper | Select Id, Parent找出父 PID然后再列出父进程名称查一下父进程属于哪个安装包。很多捆绑软件把守护进程伪装成某个系统服务或自启动计划任务所以你真正要做的是去服务列表里停掉对应服务、到“启动”和“计划任务”里禁用自启项。为什么杀一个还换一个有些程序还用了“看门狗”机制守护进程定期检查重要任务是否存活不存活就重新拉起。还有些恶意组件连守护进程都不需要直接靠 Windows 计划的触发器schtasks /query /fo TABLE一查就能看到定时任务里有重启命令。所以答案不是“它杀不死”而是“你没有切断它被拉起的源头”。切断源头的方法通常在三个地方任务计划程序、系统服务services.msc和注册表 Run 项。禁用后再杀 PID大概率能终止。说到 ThunderPlatform这是迅雷相关的后台服务组件常见表现是开机后占用 CPU 和 IO 居高不下。它的进程结构同样是多模块互相守护卸载软件时如果没有把内核网络过滤驱动一并卸载残留的服务进程就会反复启动。处理方式还是回到服务和控制面板卸载程序把跟产品相关的所有项清干净个别残留驱动要到设备管理器“查看-显示隐藏的设备”里找网络适配器下面的旧驱动右键卸载。4.4 找不到 baidunetdiskhost 进程也许它真的不叫这个名字“未找到baidunetdiskhost进程”这条热搜看起来有点冷门但背后涉及一个常见误解“我明明安装了这个软件为什么进程中找不到名字完全匹配的条目”其实很多软件的多进程架构会在不同场景下使用不同名称的子进程。百度网盘的后台进程可能叫baidunetdisk也可能叫BaiduNetdiskHostPlugin还会在下载、上传、资源监视等功能模块里有各自的独立进程。你用“baidunetdiskhost”全名去找找不到很正常以 host 结尾的后台进程在文件名里经常被缩写或后缀变体覆盖。另一个原因是权限隔离。你看到的是普通权限的进程列表而某些以提升权限运行的服务进程默认不会显示在普通用户的任务管理器里或者显示为系统进程。解决办法是勾选任务管理器左下角的“显示所有用户的进程”或者用管理员身份打开任务管理器。开发者在自己的监控工具里也要注意这个问题如果用低权限的 API 枚举进程是拿不到高权限进程的 PID 的必须提权后使用CreateToolhelp32Snapshot或者调用 WMI 的特权查询。搜索引擎上这类“未找到某进程”的问题有九成是用户想确认“软件是不是没在运行”。与其盯着进程名不如看软件本身的运行状态日志文件有没有更新、端口有没有监听、CPU 占用有没有波动。进程名不是唯一身份标识PID 也只是瞬时呈现组合起来看才准确。5. Java 进程的堆内存与 OutOfMemoryError 实战5.1 一个 Java 进程的堆是什么为什么独占把“进程”这个概念映射到 Java 世界会有一个非常具体的产品JVM。Java 字节码运行时JVM 本身就是一个独立的操作系统进程。JVM 内部把内存划分为堆Heap、虚拟机栈、方法区、程序计数器等区域其中堆是绝大部分对象存放的地方。热搜里那条“idea编译时进程堆大小调整为8000还是报错java: java.lang.OutOfMemoryError: G”一看就是经典问题。先解释“堆大小调整为8000”指的是什么。常见做法是在 IDEA 的 vmoptions 文件里设-Xmx8000m也就是把最大堆内存设为 8000 MB。很多人误以为只要堆设大了所有内存问题都解决其实 OutOfMemoryError 至少分两类堆内存溢出Java heap space和“直接内存/栈/元空间”溢出后面这一类和-Xmx没有直接关系。如果是编译时报java.lang.OutOfMemoryError: GC overhead limit exceeded那说明 GC 花了大量时间回收内存但回收效果极差对象几乎都被存活引用链绑定着此时堆越大填满垃圾的时间越长报错只是迟早的事。还有一类是OutOfMemoryError: Metaspace元空间膨胀通常跟动态生成类有关调-XX:MaxMetaspaceSize才是正解。5.2 “堆调到 8000 还报 OOM”的几种常见解释以 IDEA 编译一个较大的工程为例我遇到过的根因有五种大家可以根据自身报错信息和日志逐个排查。一物理内存不够。Windows 上 32 位 JVM 最大只能分配约 1.5 GB 到 2 GB 的进程地址空间。如果用的是 32 位 JDK-Xmx8000m根本不会被接受启动时就报“Could not reserve enough space for object heap”。当然现在 64 位 JDK 很常见但如果你在 IDEA 里同时开了多个项目多个 JVM 进程叠加起来轻轻松松超过 16GB机器物理内存不够操作系统就会拒绝分配。二堆内存虽然是 8000MB但如果代码有大量对象循环引用没释放堆再大也会溢出而且溢出时间跟数据规模有关不是内存设置能背的锅。遇到这种应该先 dump 堆转储文件分析。三编译期模块把大量中间语法树放在堆里。比如 Gradle 或 Maven 并行编译多个子模块每个模块都会建独立的编译器上下文中间树和符号表占的内存可能翻好几倍此时单进程加大堆不如关闭并行编译、扩大元空间。四进程里不仅堆在占内存JVM 的 Direct Memory、线程栈、JIT 编译缓存都在占。-Xmx8000m只是承诺堆的最大上限整个 JVM 进程的内存占用可能达到堆的 1.3 到 1.5 倍。如果你给容器或 IDE 配置的内存上限是 8GB-Xmx 8g就非常容易触发操作系统级别的内存压力甚至被 OOM Killer 杀掉。五OutOfMemoryError出现的位置很关键。若错误栈指向Java_java_lang_String相关的本地方法多半是本地方法或 DirectBuffer 在申请直接内存时失败这类需要调-XX:MaxDirectMemorySize若指向 GC 线程则属于上面说的 GC overhead。所以无论热搜怎么描述遇到 OOM 的第一原则永远是别急着调大堆先把错误帧完整截下来用 jcmd 或 VisualVM 导 HPROF 文件用 MAT 分析哪些类型对象占据了最多堆空间谁持有它们的引用链。OOM 是结果不是原因把堆调大只是延长了结果发生的时间。5.3 从 CPU 100% 到内存泄漏监控前台进程的正确姿势热搜里“监控前台进程”单独出现但在 Java 服务端语境下它实际上指的是怎么持续监控一个关键进程的 CPU、内存、线程数保证它在合理范围内运行而不是等用户报告“系统卡了”再来查。Linux 上我常用的命令组合ps -o pid,ppid,%cpu,%mem,rss,vsz,cmd -p PID看进程实时状态top -H -p PID看进程内线程的 CPU 占用jstat -gcutil PID 1000每秒打印 GC 信息连续看几十秒GC 频率和 Full GC 次数能直观反映堆是否健康。Windows 平台上用 PerfMon 或任务管理器性能监视器设置计数进程和线程对象。如果既要监控又要自动处理可以用 systemd 的Restarton-failure也可以写个守护脚本每 30 秒检查一次进程是否存在不存在则启动新进程并在发现 CPU 持续 100% 且持续超过 10 分钟时先抓jstack线程栈再重启。注意顺序很重要——一定要先留下诊断数据再重启否则问题现场就没了。这个“先留证据再处理”的原则适用于前面所有进程问题杀进程不是目的定位 root cause 才是。6. 进程资源图的可化简性与死锁判定6.1 资源分配图比文字更直观的死锁模型最后一个热搜词“进程资源图可化简”来自操作系统教材里的死锁章节。很多人在学的时候觉得抽象其实它完全可以对应到真实的多进程多资源场景进程 A 持有打印机、请求扫描仪进程 B 持有扫描仪、请求打印机两者都不释放自己手里的资源都在等对方释放于是系统僵住了这就是死锁。进程资源图Resource Allocation Graph就是用图表示“谁持有了什么资源、谁在请求什么资源”的模型圆形节点是进程方形节点是资源类型带圆点的方形节点是每类资源的实例。进程到资源节点的边代表“请求资源”资源节点到进程的边代表“资源已分配”。如果图中出现了环路且环路上的资源每一类都只有一个实例那么这个环路就是死锁的必要条件。我对“可化简”的理解是这样把图看成一条流水线若一个进程当前请求的资源能被系统满足该资源还有空闲实例或者持有者在不依赖该进程的情况下能被释放那么这个进程可以执行完毕并释放它所占的所有资源。删除它的所有边分配边和请求边后图被“化简”一步。重复这个过程一直到所有边都能删掉说明系统不会死锁如果化简到某一步还剩下一环进程无法化简那这一环上的进程就是死锁的。6.2 化简的实操方法与一个例子举一个非常小的例子系统有资源 R1 两个实例、R2 一个实例进程 P1 持有一个 R1请求一个 R2进程 P2 持有一个 R1也已拿到 R2没有额外请求。画出来就是P1 有来自 R1 的分配边有一条指向 R2 的请求边P2 有来自 R1 和 R2 的分配边没有请求边。化简时先看 P2P2 不需要等待任何资源可直接完成并释放一个 R1 和 R2释放后 R2 多出一个实例P1 的请求边可以被满足于是 P1 完成所有边删除系统无死锁。如果换一个场景R1 和 R2 都只有一个实例P1 占 R1 请求 R2P2 占 R2 请求 R1。这时 P1 依赖 P2 释放 R2P2 依赖 P1 释放 R1两个进程都无法被化简环没法打开死锁成立。这时候处理手段就三种外部干预杀掉其中一个进程或者强制剥夺它持有的资源或者回滚事务重来。我在真实项目中见过最典型的一个死锁是数据库连接池和业务线程之间两个线程各自持有一个连接池连接然后相互等着对方释放另一条连接处理一个嵌套事务。表现出来就是服务端两个线程 CPU 接近 0却一直互相等待等待超时后触发连接池重试又引发更多等待。后来通过调整连接池配置加锁顺序规范从源头把“请求边”变成单向环就造不起来了。这也是为什么说进程资源图不只是考试题目它就是死锁问题的建模语言。7. 收尾前再分享几条实战经验前面从进程的概念一路聊到了 OOM、死锁、进程锁定等具体问题最后我不做总结只分享几个真实操作后才有的感受。第一进程管理类问题有个万能开局先看清进程树再动手杀。在 Windows 用 PowerShell 按 ParentProcessId 递归列父子关系在 Linux 用pstree -p PID三分钟内能分清谁是主进程、谁是子进程、谁是被拉起的守护。多数“杀不死”的进程都是因为你没按住源头。第二处理 OOM 时先怀疑代码再怀疑参数。把堆从 4G 调到 8G 很容易但治标不治本。我见过太多线上事故重启一次就好了是因为重启前没留下堆转储下次数据量再涨一点照样挂。建议标准动作是在启动参数加-XX:HeapDumpOnOutOfMemoryError -XX:HeapDumpPath/data/dump/这样一旦 OOM现场会自动留在磁盘方便事后分析。第三排查“文件被锁定”时不要急着结束所有进程先打handle.exe或者 Windows 资源监视器的“CPU/磁盘”标签逐个盘查。很多时候锁你的不是某个高大上的服务而是资源管理器预览窗格、杀软扫描线程、甚至是 Windows Search 的索引进程。无差别杀进程不仅粗鲁而且可能把其他重要服务的状态搞乱。第四学习进程知识最有效的方式不是背概念而是装一个任务管理器级的工具把每个进程的父子关系翻一遍Nginx 为什么有两个进程、Tomcat 为什么有这么多子线程、你的 Python 脚本为什么开了一堆 worker。把这些日常现象和书上的 PCB、状态转换、IPC 一一对应起来对进程的理解会扎实得多。进程是计算机世界当之无愧的执行单元你遇到的大部分软件问题最后都能回溯到某个进程的创建、阻塞、通信或终止上。希望你读完这篇以后再碰到任务管理器里的陌生进程或者 OOM 日志时第一反应不是百度搜索而是打开进程树看两分钟。
返回列表