操作系统:进程调度,创建和终止
进程的调度
调度的概念
进程调度是操作系统在就绪队列中,按照某种算法(策略)动态地选择一个进程,并将CPU的控制权分配给它,使其执行的过程。
调度的类型
特性维度 | 长程调度 (Long-Term Scheduling) | 中程调度 (Medium-Term Scheduling) | 短程调度 (Short-Term Scheduling) |
---|---|---|---|
中文别名 | 作业调度、高级调度 | 交换调度、中级调度 | CPU调度、低级调度 |
核心职责 | 控制入口:决定哪些程序可被调入内存,成为就绪进程 | 控制负载:将进程在内存和磁盘间换出/换入,缓解内存压力 | 分配CPU:从就绪队列中选择下一个要执行的进程 |
调度对象 | 磁盘上的程序(作业) | 内存中的进程 | 内存中的就绪进程 |
发生频率 | 低(秒、分钟级) | 中等 | 极高(毫秒级) |
决策速度 | 慢,可进行复杂权衡 | 中等 | 极快,开销必须最小化 |
主要目标 | 控制多道程序度 平衡I/O型和CPU型进程的混合比例(控制多道程序的度) | 提高内存利用率 改善系统总体吞吐量和响应速度 | 最大化CPU利用率 保证公平性、高吞吐量、低响应时间 |
关键操作 | 接纳(Admit) 或创建(Create) | 换出(Swap Out) / 挂起(Suspend) 换入(Swap In) / 激活(Resume) | 分发(Dispatch) (分配CPU) |
影响状态 | 无 → 就绪/挂起就绪 | 就绪/阻塞 ↔ 挂起就绪/挂起阻塞 | 就绪 → 运行 |
类比理解调度的类型:
1. 长程调度 (Long-Term Scheduling) - “招聘经理”
中文别名:作业调度、高级调度
核心职责:决定是否允许一个新的程序进入内存,成为一个有资格竞争CPU的进程。它控制着系统的多道程序度(Degree of Multiprogramming),即内存中同时存在的进程数量。
好比:HR招聘经理决定从大量简历中录用哪些人成为公司正式员工。他需要控制公司总人数,并考虑员工的技能组合(IO型还是计算型),以保证公司整体效率。
特点:
执行频率极低:几分钟甚至更久才执行一次。
决策速度可以慢:因为不频繁,可以做复杂的权衡。
平衡负载混合:会混合接纳I/O密集型和CPU密集型进程,确保系统资源(CPU和I/O设备)都能得到充分利用。
2. 中程调度 (Medium-Term Scheduling) - “部门经理”
中文别名:交换调度、中级调度
核心职责:当系统内存紧张时,负责将暂时不运行的进程从内存换出(Swap out) 到磁盘挂起,以释放内存空间。当内存有空闲时,再将其换入(Swap in),重新激活。
好比:部门经理发现项目太多,资源紧张,决定让一些暂时不紧急的项目(员工)暂时休假(换出),等资源充足时再叫他们回来继续工作(换入)。这并没有解雇他们,只是暂时挂起。
特点:
引入了“挂起”状态:这是中程调度存在的标志。
目的是平衡系统负载:通过换出换入来缓解内存压力,提高系统整体吞吐量和响应速度。
3. 短程调度 (Short-Term Scheduling) - “项目经理”
中文别名:CPU调度、低级调度
核心职责:当CPU空闲时,立刻从内存的就绪队列中选择一个进程,并将CPU分配给它执行。这是最频繁、最核心的调度。
好比:项目经理手下有多个已就绪的员工(进程),每当一个员工完成一项任务或时间到了,经理立刻决定下一个任务派给谁。这个决策需要非常快。
特点:
执行频率极高:几十毫秒(ms)就会发生一次。
决策必须极快:因为频繁,调度算法本身不能太复杂,否则开销太大。
是通常所说的“进程调度”:我们讨论的各种调度算法(如先来先服务、短作业优先、时间片轮转、优先级调度)都是指短程调度。
也就是说:
长程调度管 “进不进” 内存(控制数量和质量)。
中程调度管 “待不待” 在内存(灵活调整内存负载)。
短程调度管 “谁先跑” CPU(微观上实现并发执行)。
进程的两种类型
1. I/O密集型进程 (I/O-Bound Process)
特点:这类进程的大多数时间都花在等待输入/输出(I/O)操作的完成上,而不是进行大量的计算。
影响:
CPU underutilized
(导致CPU利用率不足)。因为进程大部分时间在等待,如果把CPU全给它,CPU就闲着了。行为模式:通常会发起一个I/O请求(如读取文件、等待网络数据、响应鼠标点击),然后因为需要等待慢速的I/O设备而主动放弃CPU,进入阻塞状态。当I/O操作完成后,它被唤醒,进行短暂的计算处理,然后很快又发起下一个I/O请求。
典型例子:
大多数交互式程序:文本编辑器、浏览器、Office软件(用户思考或打字时就是在“等待I/O”)
需要频繁读写磁盘或网络的应用
对调度的需求:
响应速度:当I/O操作完成(如用户按下一个键),进程需要被快速调度到CPU上运行,以便给用户即时反馈。否则系统会显得“卡顿”。
不需要很大的CPU时间片,因为每次它只做一点计算就去等待I/O了。
2. CPU密集型进程 (CPU-Bound Process)
特点:这类进程的大多数时间都花在执行计算上,会长时间占用CPU而不被打断。
影响:
I/O underutilized
(导致I/O设备利用率不足)。因为进程一直霸占CPU进行计算,很少去请求I/O,磁盘、网络等设备就闲着了。行为模式:进程包含大量需要CPU进行处理的指令(如循环计算、数学运算、图像渲染)。它们可以连续运行很多个时间片而几乎不产生I/O请求。
典型例子:
科学计算、数值模拟
视频编码/解码、图形渲染
编译大型项目(如编译Linux内核)
复杂数据分析
对调度的需求:
吞吐量:更关注在单位时间内完成的计算总量。
需要长时间、连续的CPU时间片。如果给它的时间片太短,会导致频繁的上下文切换,反而降低整体效率(因为大部分时间都花在切换进程上了,而不是真正计算)。
理想状态:CPU和I/O都保持“忙碌”的工作状态。当CPU密集型进程在疯狂计算时,I/O密集型进程可以排队进行I/O操作;当I/O密集型进程等待I/O时,CPU可以立刻切换到CPU密集型进程上工作。
这就是“正确的组合”:系统中同时存在这两种类型的进程,可以相互填补对方造成的资源空闲期,像一个配合默契的团队,使得系统的总体效率达到最高。
对比总结
特征 | I/O密集型进程 (I/O-Bound) | CPU密集型进程 (CPU-Bound) |
---|---|---|
主要时间花费 | 等待I/O操作完成(磁盘、网络、用户输入) | 执行CPU计算 |
CPU使用率 | 低 | 高(甚至渴望100%占用) |
行为特点 | 频繁放弃CPU,进入阻塞 | 长期占用CPU,不主动放弃 |
核心需求 | 低响应延迟(快速被调度) | 高吞吐量(大块CPU时间) |
典型例子 | 浏览器、文本编辑器、Web服务器 | 视频编码、科学计算、编译器 |
进程的创建
进程的创建是依靠原语(Primitive)来实现的。
原语是一种特殊的程序,它的执行具有原子性,也就是说这段程序的执行必须一气呵成,不可中断。(一个原语操作要么全部执行完成,要么一点都不执行,在执行过程中不允许被任何中断(包括时钟中断))
为什么需要原语?
想象一下,如果没有原语的原子性保证,在创建进程做到一半(比如刚分配了PID,但还没建好页表)时,突然发生时钟中断,CPU转去执行另一个进程了。这会导致系统处于一个极其混乱和不一致的状态,可能引发致命的错误。原语通过“关中断执行-开中断”等方式,确保了这些关键操作的完整性。
进程创建的过程:
申请并初始化PCB:为新进程分配一个唯一的进程标识符(PID),并在内核中创建一个空的进程控制块(PCB)。
分配资源:为新进程分配必要的资源,最重要的是建立地址空间。这包括:
创建页表:建立虚拟内存到物理内存的映射关系。
继承或设置资源:处理父进程资源的继承问题(如打开的文件描述符)。
初始化PCB:将新进程的详细信息填入PCB,包括:
程序计数器(PC)指向程序的入口点(对于
fork()
则是继承父进程的断点)。分配系统堆栈。
设置进程状态为“就绪”或“就绪/挂起”。
放入就绪队列:将初始化好的新进程的PCB插入到系统的就绪队列中,等待调度器在合适的时机分配CPU给它运行。
这一切操作,作为一个整体,要么全部成功,要么全部失败(回滚),对外界来说是不可见的、瞬间完成的原子操作。 这就是原语的威力。
进程的终止
触发终止
首先,需要一个事件来触发进程的终止。通常有四种情况:
正常退出(自愿):进程完成了它的工作,主动调用退出系统调用(如
exit()
)。这是最常见的情况。错误退出(自愿):进程发现了错误并决定自行终止(例如,Python 脚本遇到
SyntaxError
)。致命错误(非自愿):进程在执行过程中遇到了不可恢复的错误,如执行了非法指令、访问了非法内存地址(段错误)、除以零等。通常由硬件触发异常,再由操作系统处理并终止该进程。
被其他进程杀死(非自愿):另一个进程(通常拥有权限,如用户通过
kill
命令或父进程)通过系统调用(如kill()
)强制终止该进程。
核心操作:执行终止原语
进程终止同样是依靠原语(Primitive)来实现的,例如 exit()
系统调用。这个原语会原子性地完成一系列清理工作,确保系统资源被完整回收,系统状态保持一致。
操作系统内核会执行终止原语,其主要步骤包括:
传递退出状态:获取进程的退出状态码(例如在
exit(0)
中的0
),并将其保存在该进程的PCB中。父进程可以通过wait()
等系统调用来获取这个状态码,以了解子进程是如何终止的。资源回收与归还:这是终止过程最核心的工作,操作系统会遍历该进程拥有的所有资源,并逐一释放:
释放内存空间:释放该进程占用的所有物理内存页和页表、代码段、数据段、堆栈段等。
关闭打开文件:遍历进程的打开文件表,关闭所有已打开的文件描述符。这一步至关重要,因为它确保了文件数据被正确写回磁盘,并释放了文件锁,避免资源泄漏。
释放其他内核资源:释放该进程占用的所有其他内核对象,如管道、信号量、共享内存段、网络连接等。
管理进程关系:
处理子进程:将该进程的所有子进程移交给了另一个进程(在许多现代系统中,如Linux,会移交给
init
或systemd
进程),防止子进程成为“孤儿进程”。通知父进程:向该进程的父进程发送一个
SIGCHLD
信号,通知它有一个子进程已经终止。
销毁进程控制块(PCB):在确保所有资源都被释放、所有后续事宜都已安排妥当后,操作系统最终会销毁该进程的PCB,并回收其PID。从此,这个进程在系统中就彻底消失了。
需要注意的是,进程的终止并不是单单一个进程的结束,更会引发一系列父子进程间的通信和状态同步行为。其核心在于 “父进程需要知道子进程的死活和结局”。
我们来简单说一下这一过程:
场景一:子进程终止 (Child Process Terminates)
当子进程通过 exit()
系统调用(或其它方式)终止时,会发生以下一系列原子性的操作:
子进程变为“僵尸”(Zombie State):
子进程的执行停止了,资源(内存、CPU等)也被操作系统回收了。
但是,它的进程控制块(PCB)并没有被立即销毁。这个保留的PCB就像一个“墓碑”,里面记录了该进程的进程ID(PID) 和 退出状态(Exit Status)(比如是正常退出还是因为某个信号被杀掉,以及返回值是什么)。
处于这种状态的进程被称为 “僵尸进程”(Zombie or Defunct Process)。
子进程向父进程发送信号(SIGCHLD):
操作系统会向该子进程的父进程发送一个
SIGCHLD
信号,异步地通知它:“你的一个子进程已经终止了”。
父进程的职责:等待(Wait):
父进程在收到
SIGCHLD
信号后,应该调用wait()
或waitpid()
系统调用来回收(reap) 那个子进程。wait()
的作用是:从僵尸子进程中获取退出状态信息,并彻底销毁它的PCB,让这个子进程真正从系统里消失。如果父进程一直不调用
wait()
,那么子进程就会一直保持僵尸状态。
场景二:父进程终止 (Parent Process Terminates)
当父进程先于子进程终止时,情况则有所不同。操作系统不会允许子进程变成“孤儿”无人照管,因此有特殊机制:
子进程变为“孤儿”(Orphaned Process):
父进程终止后,它的子进程就失去了父进程。
操作系统介入:重新指定父进程(Reparenting):
为了解决孤儿问题,操作系统内核(具体是
init
进程,在现代系统中是systemd
)会接管所有这些孤儿进程。init
/systemd
进程(PID = 1)会成为这些孤儿进程的新父进程。这是一个原子性的操作。
新父进程的行为:
init
/systemd
进程被设计为一个简单的守护进程,它会持续地调用wait()
来回收其任何终止的子进程。因此,被接管后的孤儿进程如果终止,会很快被它的新父进程(
init
)回收,不会长时间保持僵尸状态。
总结与关键点
行为 | 子进程先终止 | 父进程先终止 |
---|---|---|
触发事件 | 子调用 exit() | 父调用 exit() |
子进程状态 | 僵尸 (Zombie) | 孤儿 (Orphan) |
操作系统动作 | 向父发送 SIGCHLD 信号 | reparenting :将子进程过继给 init |
父进程职责 | 调用 wait() 回收子进程 | 无(父进程已死) |
最终结果 | 父进程回收后,子进程彻底消失 | init 会负责回收子进程,子进程彻底消失 |
关键结论:
僵尸进程是正常的中间状态:一个进程终止后,到其父进程调用
wait()
读取其状态之前的这段时间,它必然处于僵尸状态。这是设计使然,目的是为了让父进程有机会获取子进程的最终状态。僵尸进程的危害:如果父进程永远不调用
wait()
( due to a bug),僵尸进程就会一直存在。虽然它不消耗内存和CPU,但会占用宝贵的进程ID(PID)。如果系统中僵尸进程过多,将无法创建新进程。孤儿进程无害:孤儿进程本身仍在正常运行,只是父进程变了。它们最终会被
init
进程妥善处理,不会造成资源泄漏。这是一个重要的安全机制。
总结:进程终止的本质
进程终止的本质是:一个进程的执行序列结束,操作系统通过原子性的终止原语,回收其占用的所有资源,并将其从系统的进程列表中彻底移除,从而确保系统资源不会泄漏,并为后续进程的运行腾出空间。