ARTICLE DETAIL

资讯详情

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

操作系统进程管理:从PCB、状态转换到线程与IPC

操作系统进程管理:从PCB、状态转换到线程与IPC 1. 进程这张网到底捞的是什么鱼很多人学《计算机操作系统》第二章之前觉得操作系统就是管理内存、管理文件、管理设备学完之后才发现进程才是整个系统真正的主角。甚至可以说进程这章没学透后面的内存管理、文件系统、设备管理全都像是在听天书。我带过的学生里没有一个卡在第一章引论上的但百分之八十的人会在进程这儿第一次掉队——原因很简单前面讲的是操作系统有什么这章开始讲操作系统怎么活着。而活着的核心就是进程。这一章的原书名《进程的描述与控制》其实非常精准。描述解决的是进程长什么样、系统怎么认识它控制解决的是进程怎么生、怎么死、怎么切换。但很多教材写得过于抽象一上来就抛出PCB、原语、状态转换图让人不知道这些东西在真实的Linux、Windows里到底对应什么。这篇文章我打算换个讲法先搞清楚进程这章在整门课里的位置再把每个抽象概念砸到真实系统里最后用命令行和常见的故障排查把它串起来。适合刚学完引论正在被进程状态转换图折磨的学生也适合工作几年但对操作系统底层认知一直靠经验直觉的开发者——你会发现很多工作中玄学的bug根子其实都在这一章。我不打算把教材抄一遍而是把为什么教材要这么写讲明白配合实操中真正用得到的命令和排查思路。这样你再看第二章就不再是背概念而是搭骨架。2. 为什么非要有进程这个词程序的局限与操作系统的解药2.1 程序顺序执行的美好时代以及一去不返的原因早期的计算机是纯粹的串行世界。程序员把程序交给计算机计算机从第一条指令老老实实执行到最后一条执行完了再处理下一个任务。这个阶段程序执行的三大特性大家应该很熟悉顺序性、封闭性、可再现性。说白了输入相同、环境相同跑出来的结果一定相同。你不需要考虑另一个程序会不会偷改我的变量这种荒唐问题。问题出在计算机越来越快之后。处理器速度上来了但输入输出设备慢啊——一台打印机每秒只能打几个字符CPU却可以在微秒级别做几万次运算。如果程序A在等打印机的时候CPU只能干等着那这台机器的有效利用率低得令人发指。于是人们想到了多道程序设计内存里同时放若干道程序当程序A因为等待I/O让出CPU时马上把CPU切换给程序B让CPU永远在干活。这个思路本身不复杂但它像一个多米诺骨牌——一旦程序之间开始交替执行、共享资源就立刻打破了刚才那三个美好特性。程序A执行到一半程序B可能把某个共享变量改了程序A的结果也不再可再现这次跑是这个结果下次跑可能另一个结果。所以进程这个概念的诞生本质上不是为了追新潮而是为了给并发执行的世界立规矩。教材里的标准定义——进程是程序在一个数据集合上的一次执行过程——其实说得还不够透。我更愿意这么理解进程是操作系统为了管理并发执行而抽象出来的一个执行实体容器它把程序代码、数据、运行时状态、资源占用全部装进一个独立的盒子里。操作系统不直接管理程序因为程序是静态的、一堆躺在磁盘上的指令操作系统管理的是进程因为进程是动态的、正在发生的执行。2.2 进程和程序的区别菜谱和做饭的区别很多教材用进程是动态的程序是静态的一句话带过但初学者其实很难体会。我上课经常用一个例子菜谱和做饭。菜谱放在书架上一个字都不会变它是程序你照着菜谱在厨房里忙碌切菜、热油、颠勺状态一直在变灶上的火候、锅里的菜色每一个瞬间都不同这就是进程。同一个菜谱你今天做一次、明天做一次是同一个程序的两个不同进程。菜谱可以复制无数份你甚至可以同一道菜同时开三个灶头来做——这就对应同一个程序被多次执行产生多个进程。从这个类比里能自然推出进程和程序的几个关键差异进程是动态的有生命周期会创建、运行、消亡程序是静态的一直躺在那儿。进程是并发的多个进程可以同时推进程序本身不具备并发性。最要命的是进程和程序之间不是一一对应的——同一个程序可以被多个进程执行同一个进程在运行过程中也可能执行多个程序比如通过exec系统调用加载新的程序映像。这些差异教科书上列得很清楚但如果你脑子里建立不起动态执行的画面背下来也会忘。进程这章最核心的心智模型就是把一切动态的东西往进程身上挂CPU的使用、内存的空间、打开的文件、屏蔽的中断……一个进程一个包互不干扰又彼此协作。2.3 这章在整本书里的地位它是后面所有章节的地基我为什么前面强调这章是分水岭因为操作系统四大资源——CPU、内存、文件、设备——的管理的具体载体都是进程。第三章讲调度调度的对象是进程第四章讲内存管理每个进程都有自己的地址空间文件系统里每个打开的文件描述符归某个进程所有设备驱动里的I/O请求也挂在进程身上。你想理解虚拟内存得先知道进程的地址空间是怎么划分的你想理解页面置换得先理解进程访问内存的局部性模型你想理解信号量得先理解进程之间为什么要同步。进程则像一根竹签把这些资源全串起来。这门课越往后学你会发现遇到的所有问题最后都会追溯到进程头上。3. 进程怎么被描述PCB操作系统认识进程的唯一凭据3.1 没有PCB进程就是一团没有身份证的乱码假设你是操作系统内存里同时驻留了十个程序CPU在它们之间来回切换。你现在面临一个最朴素的困境你怎么知道哪个程序在运行、哪个暂停了、哪个在等打印机你凭什么保证切出去的程序下次切回来还能从原来的位置继续执行答案就是进程控制块Progress Control Block简称PCB。PCB是一张表操作系统为每一个进程创建一张里面记录了操作系统管理这个进程所需的全部信息。教材会把这些信息分门别类进程标识符PID、处理机状态通用寄存器、程序计数器、程序状态字、进程调度信息优先级、状态、等待原因、进程控制信息程序和数据地址、进程同步和通信机制、资源清单。翻译成大白话就是操作系统需要知道你是谁PID、你当前状态怎么样运行/就绪/阻塞、你攒了多少家底寄存器、打开的文件、内存资源。你可以把PCB想象成医院里每个病人床头挂的病历卡。医生查房的时候不会把病人整个翻一遍而是先看那张卡片——姓名、年龄、体温、血压、诊断结果。进程也一样操作系统在调度、切换、管理进程时大部分操作都是在读写PCB。所以教材里有一句话很关键PCB是进程存在的唯一标志。什么意思进程的程序代码和数据可以换exec、可以不在内存里被换出到磁盘但只要PCB还在这个进程就还活着反过来进程结束时PCB会被回收这个进程才算彻底消失。3.2 队列操作系统的候诊室管理体系面试常考的隐藏考点是PCB不只是单张表它们是被组织成队列来管理的。操作系统一般会维护几条核心队列就绪队列所有已就绪、只等CPU的进程排队、阻塞队列正在等待某个事件的进程通常按等待原因分成多条、运行队列正在CPU上执行的进程单核系统里只有一个。还有一些教科书里不常见但实际存在的队列比如等待I/O完成的设备队列、等待分配内存的进程队列。为什么要强调队列因为进程调度的本质就是从就绪队列里挑选一个进程把它变成运行态。状态转换图里的箭头落到实际系统里全部是从一个队列进出另一个队列的操作。进程被创建后放进就绪队列调度器选中后移出就绪队列进入运行时间片用完又回到就绪队列队尾——这就是所谓的轮转调度的物理实现。在Linux里这套队列做得非常精巧。每个CPU都有一个运行队列队列里的任务按照优先级分成了若干组高优先级的实时任务插到最前面普通任务按照时间片和nice值动态调整优先级这已经是第三章调度算法的内容了。但如果你在学这一章时就能把PCB组织成队列这个模型刻在脑子里后面学多级反馈队列、CFS调度器时就会顺很多。3.3 进程状态转换从三态到七态每一态都有讲究第二章最经典的图就是进程状态转换图。教材通常从三态模型讲起运行态Running进程此刻在CPU上执行、就绪态Ready万事俱备只欠CPU、阻塞态Blocked正在等待某个事件比如等待I/O完成、等待信号量这三个状态之间的转换逻辑是就绪 - 运行调度器选中了你把CPU分配给你。这是调度器主动的行为。运行 - 就绪时间片用完或者来了一个优先级更高的进程把你挤下来。你不是出错了只是暂时让位。运行 - 阻塞进程主动请求等待某个事件比如发起一个read系统调用在数据没回来之前你就是阻塞态。阻塞 - 就绪你等待的事件发生了比如数据到了、锁被释放了。注意这一步不是直接进运行态而是回到就绪态重新排队因为CPU可能正被别人占着。很多初学者会问为什么阻塞的进程醒来之后不能直接运行答案是CPU可能没空而且即便有空按照公平原则也不能插队。这就像银行排队办业务你去填单子等事件的时候不在队伍里填完了回来得重新排队不能冲到柜台前面跟正在办业务的人说我填完了让我先来。五态模型在基础上多了创建态和终止态。创建态是指进程正在被创建的过程中——PCB刚分配好但还没有进入就绪队列终止态是进程已经执行完或被杀死系统正在回收它的资源。七态模型又加入了挂起就绪和挂起阻塞。挂起是什么意思简单说就是把进程换出内存放到磁盘上的交换区去这是为了缓解内存压力。挂起的进程不参与调度只有被换回内存后才能恢复原状。这个知识点在Linux里对应的是swap机制但在现代操作系统的实现中这个挂起更多地被虚拟内存的按需调页给取代了——不过考试还是得会。3.4 在Linux里观察进程状态教科书落地的样子课本上说的运行态、阻塞态、就绪态在Linux的ps命令里都有对应的字母。我用ps aux看到的STAT列取值就有一堆RRunning表示正在运行或处于就绪队列——在Linux里只要在运行队列里就算R不管它是不是真的在CPU上执行SSleeping可中断睡眠对应阻塞态进程在等待某个事件可以被信号唤醒DDisk Sleep不可中断睡眠也是阻塞态但此时进程连信号都不响应多发生在等待磁盘I/O时如果D状态进程长期不消失基本说明磁盘子系统出问题了ZZombie僵尸态进程已经结束但PCB还没被父进程回收TStopped进程被暂停了通常是收到了SIGSTOP信号。我见过很多初学者在学状态图时对阻塞和就绪的区分很模糊但在终端里跑几条命令就理解了。你敲一个ps aux | grep sleep看到的S状态就是进程在sleep系统调用里等闹钟你写一个死循环while(1);跑起来看到的R状态就是进程在就绪队列里抢CPU一个进程调了waitpid等子进程退出它就是S状态子进程先结束变成僵尸父进程还没回收你copy会看到Z状态。概念从纸上落到命令行里一下就通了。4. 进程怎么被控制从被迫诞生到体面退场4.1 进程的创建fork和exec一次造物主的操作进程不是石头缝里蹦出来的。教材里说除了0号进程是在系统启动时手工创建的其他所有进程都是通过创建原语产生的。所谓原语就是由若干条机器指令组成的、不可被中断的原子操作序列。为什么进程创建必须原子因为如果创建到一半被切走就会出现一个半成品进程混在系统里PCB缺一半、资源分配了一半整个系统的数据结构就乱了。操作系统在管理进程这类全局资源时必须保证要么不建要么建完整。经典的创建流程是分配一个PCB - 为新进程分配资源内存空间 - 初始化PCB填PID、父进程号、寄存器初值等 - 把新进程插入就绪队列。这个流程在Linux里对应的是fork() exec()的组合。fork()做的事情是把当前进程父进程的内存和资源复制一份生成一个几乎一模一样的新进程子进程子进程和父进程唯一的显著区别是getpid()不同、fork()的返回值不同。exec()则是在子进程中加载一个新的程序映像把fork复制过来的旧程序覆盖掉。为什么要拆成两步因为这样设计足够灵活——你可以在fork之后、exec之前做一些中间操作比如重定向标准输入输出这在shell实现管道命令时至关重要。我上课经常让学生做个实验写一个fork()的程序什么都不干就fork一次然后用pstree看看进程树的变化。你会发现fork出来的子进程也带着一份父进程的完整地址空间拷贝包括打开的文件描述符、环境变量、信号处理函数。这个一份镜像复制看起来很笨重所以现代Linux实现用的是写时复制技术——fork()时不真正复制物理内存而是让父子进程共享同一份物理页谁写了才真正复制。这也是操作系统课程里时间换空间、空间换时间思想的经典案例。4.2 进程的终止与回收僵尸进程是怎么诞生的进程终止的原因分正常和异常两种。正常退出是进程执行完最后一条指令或者显式调用exit()异常退出包括收到终止信号、越界访问内存、除零错误等。终止原语做的事情和创建原语正好相反回收分配给进程的资源、撤销它的PCB、把进程从队列里移除。但这里藏着一个容易忽略的重要环节——终止之后的善后。Linux的进程生命周期有个让无数人丈二和尚摸不着头脑的阶段僵尸态。子进程终止后它并不会立刻从系统中消失而是要留下一个PCB里面保存着它的退出状态和资源统计信息等着父进程来收取。父进程通过wait()或waitpid()系统调用读取子进程的退出状态读到之后这个僵尸进程的PCB才会被系统回收。如果父进程一直不调wait()子进程就一直是僵尸。如果父进程自己先退了子进程会被1号进程init收养由init进程负责回收。很多学生在面试的时候都能背出僵尸进程是已经终止但未被父进程回收的进程但实际排查的时候抓瞎。我教过一个非常典型的排查方法敲ps aux看到STAT列是Z的进程然后用ps -o ppid -p PID查它的父进程PID再用pstree看父进程是谁。如果是你写的程序十有八九是忘掉了wait()调用如果父进程是1号init还出现僵尸那通常说明系统本身或者驱动层有问题。还有一个坑不能直接kill一个僵尸进程因为它的资源已经释放了kill它没有任何效果你得去解决它的父进程——让父进程调用wait或者把父进程杀掉让init来收养。这个知识点每年都有学生在实际工作里踩坑。4.3 进程的阻塞与唤醒sleep和wakeup的底层逻辑进程在运行过程中如果它需要等待某个事件比如等键盘输入、等锁、等网络包就会主动调用阻塞原语。阻塞的对照面是唤醒当进程等待的事件发生时由事件产生者调用唤醒原语把进程从阻塞队列移到就绪队列。注意阻塞是进程主动做的唤醒却往往是被别人唤醒——这是初学者很容易忽略的一点。一个进程不能自己把自己唤醒因为在它处于阻塞态的时候根本没有机会执行任何代码必须靠另一个进程通常是负责那个事件的进程来叫醒它。这里就牵扯出一个非常重要的设计问题阻塞和唤醒这两个原语必须配对使用、且必须保证原子性。如果进程在检查条件到执行阻塞之间被插入了其他操作就可能出现条件已经不满足了进程还在傻傻地等这种死锁或者逻辑错误。教材里讲的同步机制的缘起就在这儿——后面讲信号量时P操作和V操作就是成对的原子操作本质上干的就是阻塞和唤醒这件事只不过加上了计数器来保证多个进程在共享资源上不会互相打架。4.4 进程切换一次上下文切换的气球理论进程切换是这一章的重头戏也是面试常客。所谓切换就是把CPU从一个进程让给另一个进程。切换的第一步是保存当前进程的上下文CPU寄存器的值、程序计数器、栈指针、程序状态字等加载新进程的上下文。这个过程叫上下文切换教材里会强调上下文切换的开销是系统开销。但是你们注意切换本身并不执行任何用户计算它是在内核态做的一件纯浪费的事情——那为什么还不得不做因为并发执行是刚需你只能尽量降低切换的频率和单次切换的成本。我习惯用气球来类比上下文切换进程在CPU上吹气球吹到一半你把它拿走把另一个气球塞给CPU。你拿走气球的时候必须记住它吹到多大了——是吹了五口气还是八口气、气嘴拧到哪个位置——否则下次还给它的时候你没法继续往里面吹气。寄存器和程序计数器就是那个吹到哪里的位置记录。上下文切换的成本一是保存和恢复数据的开销二是切换期间CPU被完全占用的开销。如果一个系统切换得过于频繁比如时间片设得太短CPU的大部分时间都在做切换而不是在跑用户程序系统的有效吞吐率会暴跌。这也是为什么时间片一般设在毫秒级别而不是微秒级别。5. 进程同步与通信多进程世界的交通规则5.1 为什么进程通信必须走官方通道多道程序设计带来一个两难问题进程之间需要协作凡事相互独立就不需要进程这个概念了但协作又意味着共享数据和互相等待。文件编辑进程要跟磁盘I/O进程沟通求值进程要把结果交给打印进程。那它们怎么通信教材给了一个总体上的策略进程之间必须通过操作系统提供的通信机制来交换信息禁止进程之间直接互相访问彼此的地址空间。为什么禁止直接访问因为一旦允许系统的安全隔离就彻底崩了。一个进程可以随便改另一个进程内存里的数据那银行系统的余额字段还不随随便便就被别的进程改了。更别说这种直接访问几乎没有同步可言——你正在写一个变量的时候对方也来写数据没等你写完就被读走了。所以操作系统提供的通信方式按教材分类是三大类共享存储系统默契共同访问某个区域但同步借助信号量等机制、消息传递系统消息队列、邮箱、管道等、管道通信系统用一个连接两个进程的共享文件进行读写。这些在Linux里都有对应的具体API比如共享内存信号量、消息队列、匿名管道。网络热词里高频出现进程通信IPC这个词说明这是大家在工作里躲不开的坎。很多开发者写多进程程序遇到卡顿、死锁、数据不一致多半是IPC用得不对。我见过的典型坑是共享内存不加锁——两个进程同时写共享区数据互相覆盖查起来又像玄学。本质上就是第二章讲的没有同步机制的共享等同于裸奔这句话在现实中的翻版。5.2 信号量与P、V操作计数器、等待队列和一套必须成对的操作讲到同步绕不开信号量。信号量的本质是一个计数器它记录可用资源的数量配合两个原子操作Pwait申请资源和Vsignal释放资源来管理多个进程对共享资源的访问。只要资源数大于零P操作就可以顺利通过并把计数器减一如果计数器已经为零P操作会让当前进程阻塞并挂到信号量的等待队列上V操作做相反的事把计数器加一并从等待队列里唤醒一个进程。很多学生不理解的一点是为什么信号量能让检查资源和占用资源这两步变得原子原因是P、V操作是在操作系统内核里实现的在执行过程中不会被时钟中断打断也不会被其他进程抢占。这就是教材反复强调的原语性。你可以脑补一下如果不是原子的两个进程同时检测到资源可用同时去申请资源资源只有一份那就直接混乱了。信号量的P、V原语保证了这个检测-占用的临界区不被切开。经典的同步问题有生产者-消费者问题用两个信号量分别表示缓冲区空位和产品数量、读者-写者问题、哲学家进餐问题。我学生在面试时被问到最多的是生产者-消费者问题而且几乎每次都会追问如果两个P操作的顺序写反了会怎样答案是可能死锁。比如本来应该先P(空位)再P(互斥)你写成先P(互斥)再P(空位)一旦缓冲区是空的生产者就会抱着互斥锁去等空位消费者也进不了临界区去取产品——死锁了。这个例子我每次上课都讲顺序太重要了同步操作的精髓就在于操作顺序和对原语的理解。5.3 四种通信方式什么时候用哪种共享存储速度最快适合大数据量传输但需要额外的同步手段来保护共享区实现复杂度高。消息传递操作系统承担了复制数据的任务安全性好但每次通信都要经过内核拷贝性能比共享内存差一个量级适合小数据量、以控制信号为主的消息。管道最简单的进程间通信方式本质是一个内核缓冲区一方写入、一方读取。匿名管道只能用于有亲缘关系的进程之间命名管道FIFO允许无亲缘关系的进程通信在文件系统里有路径名。消息队列类似邮箱可以把消息按类型发给特定接收者而且消息不需要双方同时在线发完就能走接收方以后再取。共享内存则是把一块物理内存映射到多个进程的地址空间里配合信号量使用是吞吐率最高的方式。画个重点工作在Linux下做高并发多进程设计我一般推荐的原则是——控制信号走消息队列或信号大数据传输走共享内存简单亲缘进程之间的流式数据走匿名管道无亲缘关系但要持续交换结构化数据走命名管道或消息队列。每种方式都有擅长的场景别指望一把锤子敲遍所有钉子。6. 从进程到线程并发粒度的一场变革6.1 进程切换开销大于是线程出场了进程解决了并发和隔离的问题但它的解药也是它的副作用。进程切在切换时要保存/恢复一大堆上下文而且进程之间地址空间互相独立通信必须经过内核每次通信和切换都要陷入内核态。如果某个应用需要同时干好几件事——比如一个文本编辑器既要在后台检查拼写又要不停刷新界面——用多进程来做效率和响应速度都不理想。于是线程出现了。线程是进程内的一个执行流是CPU调度的基本单位。同一个进程内的多个线程共享进程的地址空间、打开的文件、全局变量等资源但每个线程有自己的程序计数器、寄存器和栈。因为共享地址空间线程之间不必通过内核通信直接就能读写共享变量通信效率极高线程切换时不需要切换地址空间开销比进程切换小得多。用类比说进程是公司线程是公司里的员工。公司有独立的办公场地、财务账目、注册资质员工共享这些基础设施但每个人有自己的工位、电脑和工作清单。你要给公司配备全套设备很贵但招聘两个员工共用一套设备就便宜得多。教材里的线程模型分了三种用户级线程线程的创建、切换、销毁全部在用户空间完成内核不知道线程的存在同一进程内的用户线程不会被内核真正并行调度、内核级线程线程的创建、切换由内核负责多核CPU可以真正并行执行多个内核线程、组合方式用户线程映射到多个内核线程上兼得灵活性和并行性。这三种模型对应Linux的线程实现其实也很有意思——在Linux里线程和进程的统一实现方式是复制进程的资源但共享同一个地址空间线程本质上是一种轻量级进程。所以你在Linux里用ps -L或者top -H能看到同一进程的多个线程它们的PIDTID不同但进程组和地址空间是共享的。6.2 线程与进程的区别一张表收干净直接用表格来对照会清晰些这也是面试里最常考的送分题维度进程线程资源拥有者进程是资源分配的独立单位拥有独立地址空间、文件、信号等线程共享进程的资源本身几乎不独立拥有资源调度单位早期操作系统以进程为调度单位现代系统以线程为调度单位切换开销切换进程上下文需要切换地址空间开销大同一进程内线程切换只切换栈和寄存器开销小通信方式需要IPC管道、消息队列、共享内存直接读写共享变量线程间通信零拷贝健壮性一个进程崩溃不影响其他进程一个线程崩溃可能导致整个进程崩溃系统开销每个进程有独立的PCB和资源开销大线程的TCB相对轻量创建撤销开销小表格好背但你要能从中解读出设计哲学。进程的核心价值是隔离——独立地址空间、独立资源、出问题互不拖累线程的核心价值是协作——共享资源、高效通信。所以现代大型系统往往是多进程多线程的组合用多进程做模块之间的隔离用多线程做模块内部的并行。比如Chrome浏览器每个标签页都是一个进程但每个标签页内部的渲染、JS执行、网络请求是多线程协作完成的。这样即使某个页面崩溃也只会拖垮一个进程其他页面不受影响。6.3 Java、Python里的进程与线程和操作系统课本差多少热词里频繁出现的java进程进程和线程的区别说明大家在语言层面接触的概念和操作系统层面的概念是有落差的。Java的Thread对象对应操作系统的一个线程JVM跑起来本身就是一个操作系统进程Java的ProcessBuilder对应的是操作系统进程的创建。但有一个常见的坑Java的线程创建并不直接对应操作系统内核线程的创建——JVM通常在用户态实现了线程的调度和栈管理然后按需映射到内核线程上具体取决于JVM实现和操作系统。所以你在Linux上开了1000个Java线程用top -H看到的可能不是1000个内核线程而是JVM统一管理的绿色线程。这个差异在写高并发服务时特别重要因为操作系统线程的调度开销和栈内存占用默认8MB远大于JVM线程。Python的GIL全局解释器锁则是另一个经典误区。很多开发者以为Python多线程是并行的但实际上因为GIL的存在同一时刻只有一个线程在执行Python字节码。所以CPU密集型任务用Python多线程基本没用得用多进程multiprocessing模块才能真正利用多核。这个语言的线程模型和操作系统线程模型不一致的落差本质上是第二章里用户级线程和内核级线程的概念在真实世界的投影。学的时候觉得抽象工作里遇到性能瓶颈回头翻课本会发现课本早就告诉你答案了。7. 学完这章去命令行里把概念抓出来7.1 ps、top、pstree给进程拍X光理论学完不落到实践上等于纸上谈兵。我建议每个人都应该在Linux里亲手玩一遍这几个命令这是对第二章最好的复习ps aux查看所有进程的详细信息重点看STAT列和%CPU、%MEM。STAT里的R、S、D、Z、T对应我们前面说的运行态、阻塞态、僵尸态等。top动态刷新进程状态按P按内存排序。ShiftH能切换到线程视图看每个线程的CPU占用。排查哪个线程烧光了CPU时必须用这个。pstree以树形显示进程间的父子关系。进程创建的那一章说子进程由父进程fork出来pstree就是这句话的直观证明从init进程PID 1一路展开能看到整个系统的家族谱。ps -o pid,ppid,stat,cmd -p PID查某个进程的父进程PID和状态排查僵尸进程时第一反应就用它。pgrep或pidof按名字找PID。注意pgrep -f可以用完整命令行匹配比如pgrep -f python script.py比单纯按进程名匹配准得多。我让学生做的一个小练习是开两个终端一个终端运行一个死循环程序另一个终端用top -H查看它的CPU占用然后给这个程序发一个SIGSTOP信号kill -19再看一次状态变成T接着发SIGCONTkill -18恢复运行。整个过程就是在终端里亲手操作状态转换图上的运行-暂停-恢复比背十遍状态转换图都有用。7.2 进程名的那些坑comm字段的15字符限制与修改方法很多人在写运维脚本时会突然发现一个奇怪的现象Linux里进程名最多只能显示15个字符。原因在于进程名存放在task_struct里的comm字段这个字段的数组长度是TASK_COMM_LEN16字节包含结尾的\0所以最多只能存15个字符。这也是热词里linux 修改进程名称 大于15个字符的由来——你确实没法在comm里存一个超过15字符的名字。那超过15字符的需求怎么实现有几种办法。一是通过修改内核重新编译把TASK_COMM_LEN改大一劳永逸但不灵活。二是利用prctl的PR_SET_NAME参数修改进程名但它也是受comm字段限制的。三是修改/proc/self/comm文件效果相同。四是更取巧的办法由于ps展示的进程名实际上是从/proc/PID/comm读的字节串就行但同样受内核字段限制。所以如果你需要让ps里显示一个完整的长名称实际的工程手段往往是把可执行文件做成一个symlink用带路径的长名字cmdline来展示。这里有个细节值得注意ps aux展示的是从/proc/PID/cmdline读到的完整命令行不受15字符限制而ps -e -o comm展示的是进程名受15字符限制。很多脚本里用grep进程名时踩的坑根源就在这两者的差异。我在写服务矩阵监控脚本时曾经被这个坑过一个Java服务的进程名默认是java两个不同的Java服务在ps里都叫java然后用pgrep java一抓抓出一堆进程根本分不清谁是谁。后来改成了启动脚本里通过exec -a自定义argv[0]再用ps -o cmd去匹配完整命令行才解决了问题。这也是工作里真实常见的进程描述问题——你作为系统管理员怎么精确描述出你想管理的进程本质上就是在做第二章进程标识这件事。7.3 排查进程杀不掉CPU打满页面无响应的思路热词里有几个很典型的应用层问题wps进程无法关闭、任务管理器里的进程怎么卸载、chatgpt有进程没画面、msedgewebview2.exe进程如何关闭、频繁进程管理崩溃。这些问题看起来五花八门但如果用操作系统的视角来归类无非是几类僵尸进程进程杀不掉、进程挂起页面无响应的根因、某类进程CPU打满占用资源异常、图形会话层的进程异常比如WebView2进程残留。先说杀不掉。在Linux里kill -9杀不掉的进程要么处于D状态不可中断睡眠要么在内核态做不可中断的操作。D状态的进程你再怎么kill也没用只能等它的内核操作完成或者排查底层磁盘I/O问题。在Windows里任务管理器结束进程按钮是灰的多半是权限不够——很多系统进程必须管理员权限才能结束或者进程受保护机制比如防病毒软件、系统关键进程钳制。msmpeng.exe是Windows Defender的恶意软件扫描进程CPU长期高占用是个老问题解决办法不是手动杀——你杀了系统又会重启它——而是优化扫描计划、添加目录排除或者干脆禁用实时保护不推荐。杀不掉还有一层含义你有多个同名进程根本杀不完。比如wps进程无法关闭经常是WPS的崩溃守护进程在捣鬼——你杀掉主进程守护进程又会拉起一个新的。这种进程守护机制在服务端软件里非常常见超级进程supervisor、systemd也会不断重启崩溃的服务。所以排查杀不掉的问题时第一步不是急着kill而是先看进程的父子关系找到真正的守护者是谁把守护者先停掉再处理目标进程。用pstree -p PID耙一遍进程树往往一分钟就能找到元凶。7.4 系统崩溃时的进程排查从黑屏到无响应热词里还有一条误删了一个进程任务电脑黑屏怎么怎么办——这多半是桌面环境里误杀了关键进程。Windows下误杀explorer.exe资源管理器会出现桌面图标消失、任务栏不见但系统还在运行的半死状态Linux下误杀GNOME Shell、kwin等桌面进程画面也会黑掉但系统没死。这种现象其实恰好说明了一个第二章的核心概念进程是系统功能的具体载体。桌面UI不是系统自带不可分割的一部分它只是一个进程进程被杀功能就没了。解决办法也很简单重新启动那个进程。Windows按CtrlShiftEsc打开任务管理器如果任务管理器还活着文件-运行新任务-输入explorer.exe桌面就回来了Linux里切到TTYCtrlAltF2登录后重启桌面进程也是同理。黑屏怎么怎么办这类问题对很多电脑用户来说是灾难级故障但你要是理解了进程模型就会觉得它不过是个普通的业务重启。操作系统不会因为你杀掉一个UI进程就整个崩溃它的底层内核和进程管理器还在正常运转。这种从恐慌到淡定的转变正是学操作系统带给一个人的最大红利——你不会再把自己的电脑当黑箱而是能拆出哪些进程负责哪些功能的清晰认知。8. 几个现场翻车的经验教训这一章的知识点面试要考、考试要考但真正让一个人从背概念的考生变成懂系统的工程师的是那些踩过的坑和悟出来的经验。我把自己带学生和工作中积累的几个翻车现场分享出来每一条都能在现实工作里遇到。第一fork之后父子进程的缓冲区重复输出问题。很多初学者会在fork之前先调用printf然后fork发现输出内容打印了两次甚至多次。原因是标准库的printf是先缓冲再写入文件描述符的fork的时候缓冲区里的内容也被复制了一份于是父子进程各自把缓冲区刷出来一次。解决办法是fork之前fflush或者直接用write系统调用。这个坑很隐蔽但理解了fork复制了进程的整个地址空间包括用户态缓冲区其实很简单你不是打印了两次你是有两份一模一样的进程各自打了一份。第二waitpid的返回值是个陷阱。我在公司排查过一个子进程退出后父进程一直在轮询的bug代码里用waitpid(pid, status, WNOHANG)去轮询子进程是否退出但返回值等于0表示子进程还在运行等于pid表示子进程已经退出返回了它的pid。那位同事把判断写反了导致程序永远认为子进程没退出。这本质上就是进程状态转换里阻塞-就绪的判断逻辑错误——你在错误地判断进程状态而不是进程本身出了问题。第三共享内存不加锁的翻车现场最常见。有一次排查一个性能压测工具的诡异行为压测结果忽高忽低有时候数据全对有时候乱成一团。最后发现是压测工具用共享内存做数据采集但没配信号量多个进程同时写内存互相覆盖。数据大部分时候正确只是在并发写入的瞬间被半覆盖了这种bug特别难盯但本质上就是第二章的临界区没有保护在现实世界的翻版。你在教科书里学的那句话——对临界资源的访问必须在临界区内进行——绝对不是空话是真的会咬人的。第四说到D状态进程堆积的排查。我经历过一次生产环境的诡异卡顿机器负载不高但所有新进程无法启动top一看满屏的D状态进程全部卡在磁盘I/O上。查到最后是某个存储节点故障导致所有读写该存储的进程都处于不可中断睡眠状态堆积成山。D状态进程不响应信号导致的严重后果是连kill -9都杀不掉因为内核不允许你用用户态信号去打断一个内核I/O操作。这种情况下唯一的出路是恢复底层I/O而不是跟进程较劲。这些都是进程讲的是理论但它的脊梁骨就是现实系统里的每一次调度切换的明证。学习第二章时别只盯着教材里的状态转换图背要把每个状态、每个原语、每种通信方式都映射到一个真实的命令或真实的故障上。你如果能做到这一点你对操作系统的理解就已经超过大多数人的平均水平了。
返回列表