操作系统核心知识
这是人工智能训练师系统学习系列的第四章。按照知识体系的重点标记,操作系统属于核心内容,定位是”讲清核心概念”——不像数据结构算法那样要讲透每个细节,但进程、内存、文件、I/O 这几块的核心概念你必须能讲明白、能理清思路。这一章是计算机基础的”大管家”,管资源、管进程,概念密集但套路清晰,跟着下面的串讲走一遍,遗忘的内容就能捡回来。
操作系统:管资源、管进程的大管家
操作系统是核心重点,核心功能就四件大事:进程管理、内存管理、文件管理、设备(I/O)管理。说白了,它就是计算机里的大管家,CPU、内存、磁盘、外设这些资源全归它调度分配,谁先用、谁后用、谁占多少,它说了算。下面按这四块依次展开。
进程与线程
先记住一句话:进程是资源分配的基本单位,线程是 CPU 调度的基本单位。这句话是重点知识,别记反。
打个比方,进程像”独立的车间”,每个车间有自己的厂房(地址空间)、仓库(打开的文件)、账本(资源清单),相互之间天然隔离,隔壁车间着火了也不烧到你这边;线程像”车间里的工人”,多个工人共用同一个车间的厂房和仓库,但各自手里有自己的一份活儿(程序计数器、寄存器、栈)。
把这个类比再讲细一点,帮你把”资源分配”和”通信开销”这两件事分清。资源分配上,进程是分配单位——操作系统给进程分配独立的地址空间、文件描述符表、内存限额,这些都是”车间级”的配给,线程不单独分配这些,而是共享所属进程的全部资源。通信开销上,进程间要传货得走”物流”——管道、消息队列、共享内存、信号量这些都属于跨车间的运输方式,开销大、要内核介入;而线程间都在同一个车间里,要协作直接喊话就行——读写同一进程内的共享变量,开销小,但要排队(加锁)避免两个人抢同一份货。
进程和线程的关键区别还有两个重点知识:创建销毁进程比线程贵得多,因为要建/拆整个地址空间;一个进程崩了通常不影响别的进程(车间隔离),但一个线程崩了很可能把整个进程拖垮(一个工人操作失误炸掉整个车间)。
进程有三个基本状态:就绪(万事俱备只差 CPU)、运行(正在 CPU 上跑)、阻塞(等 I/O 或事件)。注意阻塞不能直接到运行,必须先回到就绪。
进程状态转换实例
光记三个状态不够,得跟着一个具体场景走一遍转换过程。假设进程 P 要做一件简单的事:读磁盘上的一个文件,然后处理它。
- 第一步:进程 P 被创建出来,进入就绪队列,等 CPU。这是”就绪”状态。
- 第二步:调度器选中 P,分配 CPU 给它,P 开始执行代码。这是”就绪→运行”。
- 第三步:P 执行到 read() 系统调用,要读磁盘。磁盘 I/O 是慢活,CPU 不能干等,于是操作系统把 P 从 CPU 上拿下来,让它去等磁盘数据。这是”运行→阻塞”。
- 第四步:磁盘控制器把数据读好,向 CPU 发一个中断,操作系统在中断处理里把 P 唤醒,让它重新进入就绪队列。这是”阻塞→就绪”。注意 P 不能直接跳到运行,因为此刻 CPU 可能正在跑别的进程,它得排队等下一次被选中。
- 第五步:调度器再次选中 P,P 继续从 read() 之后那行代码往下跑,处理数据。这是”就绪→运行”。
你看,整个转换链条是:就绪 → 运行 → 阻塞 → 就绪 → 运行。”阻塞→运行”这条边不存在,这是最容易混淆的判断点。要是看到”阻塞直接到运行”这种说法,直接判错。
调度算法
调度算法记几个经典的就行,重点理解”什么场景用什么、会不会饿死”。
先来先服务(FCFS):谁先来谁先跑,简单公平,但短任务会被长任务拖死。短作业优先(SJF):优先跑估计时间最短的,平均等待时间最短,但长任务可能永远轮不上(饿死)。优先级调度:按优先级高低跑,同样可能饿死,解决办法是”老化”(aging),等得越久优先级慢慢抬高。时间片轮转(RR):每个进程给一个时间片,用完就被踢到就绪队列尾部,公平响应快,适合分时系统。多级反馈队列(MLFQ):集大成者,多个队列不同优先级,新进程进高优先级队列,用完时间片降级,是经典的”既能响应快又能兼顾吞吐”的算法。
容易混淆的点:SJF 是非抢占的,SRTF(最短剩余时间优先)才是抢占版本;时间片轮转是抢占式的。
三种调度算法算一遍
光说概念不够,给你一个具体的进程列表,咱们把 FCFS、SJF、时间片轮转各跑一遍,看看等待时间怎么算。假设有四个进程:
- P1:到达时间 0,执行时间 10
- P2:到达时间 1,执行时间 4
- P3:到达时间 2,执行时间 3
- P4:到达时间 3,执行时间 1
先看 FCFS(先来先服务)。谁先到谁先跑,所以顺序就是 P1、P2、P3、P4。
- P1 在 0 时刻开始,10 时刻结束。等待时间 = 0 - 0 = 0。
- P2 在 10 时刻开始(P1 跑完才轮到它),14 时刻结束。等待时间 = 10 - 1 = 9。
- P3 在 14 时刻开始,17 时刻结束。等待时间 = 14 - 2 = 12。
- P4 在 17 时刻开始,18 时刻结束。等待时间 = 17 - 3 = 14。
- 平均等待时间 = (0 + 9 + 12 + 14) / 4 = 35 / 4 = 8.75。
FCFS 的问题很明显:P4 其实只需要 1 个单位就能跑完,却等了 14 个单位,被 P1 这个长任务拖死了。
再看 SJF(短作业优先,非抢占)。0 时刻只有 P1 到了,所以 P1 先跑(0-10)。10 时刻 P2、P3、P4 都到了,挑最短的 P4(执行 1)跑。11 时刻剩 P2(4)和 P3(3),挑 P3。14 时刻剩 P2,跑完到 18。
- P1 等待 = 0
- P4 等待 = 10 - 3 = 7
- P3 等待 = 11 - 2 = 9
- P2 等待 = 14 - 1 = 13
- 平均等待时间 = (0 + 7 + 9 + 13) / 4 = 29 / 4 = 7.25。
SJF 比 FCFS 平均少了 1.5 个单位,但要注意 P2 这种”中等长度”的任务反而等得更久,因为它排在 P4 和 P3 后面。如果不断有短任务到来,P2 可能永远轮不上——这就是饿死。
最后看时间片轮转(RR),时间片大小取 4。所有进程按到达时间排队,每个进程用完一个时间片就让给下一个。
- 0 时刻:P1 开始跑,用完 4 个单位(4 时刻),P1 还剩 6,回到就绪队列尾部。
- 4 时刻:P2 到了(1 时刻到达)已经在队列里,P2 跑 4 个单位(4-8),P2 完成。
- 8 时刻:P3(2 时刻到达)跑 3 个单位(8-11),P3 完成。
- 11 时刻:P4(3 时刻到达)跑 1 个单位(11-12),P4 完成。
- 12 时刻:P1 接着跑 4 个单位(12-16),P1 还剩 2。
- 16 时刻:P1 跑 2 个单位(16-18),P1 完成。
等待时间 = 完成时间 - 到达时间 - 执行时间:
- P1 等待 = 18 - 0 - 10 = 8
- P2 等待 = 8 - 1 - 4 = 3
- P3 等待 = 11 - 2 - 3 = 6
- P4 等待 = 12 - 3 - 1 = 8
- 平均等待时间 = (8 + 3 + 6 + 8) / 4 = 25 / 4 = 6.25。
时间片轮转的平均等待时间最低(6.25),原因是它”切得细”,让短任务尽早得到执行机会。代价是上下文切换开销大——你看 P1 被切了三次。所以时间片太小,切换开销吃掉 CPU 时间;时间片太大,退化成 FCFS。一般经验值取几十到一百毫秒【需网络查询确认:典型分时系统时间片大小经验值】。
内存管理:分页、分段与虚拟内存
分页(Paging):把物理内存切成固定大小的页框,把逻辑地址切成同样大小的页,通过页表做映射。优点是没有外部碎片,缺点是有内部碎片(最后一页填不满)。地址结构是”页号 + 页内偏移”。
分段(Segmentation):按程序的逻辑结构划分(代码段、数据段、栈段),段大小不固定。优点是共享和保护方便,缺点是有外部碎片。段页式:先分段再分页,结合两者优点。
分页有内部碎片无外部碎片,分段有外部碎片无内部碎片,这个对比要记牢。
虚拟内存:让程序以为自己有很大内存,实际只有一部分在物理内存里,其余在磁盘的交换区。靠”请求调页”实现——访问的页不在内存时触发缺页中断,操作系统把它从磁盘调入,可能还要换出一页(页面置换算法)。
页面置换算法必记。最佳置换(OPT):换掉将来最长时间不被用的页,理论最优但无法实现(因为没法预知未来)。先进先出(FIFO):换掉最早进来的,简单,但有 Belady 异常——分配的页框变多反而缺页率上升(经典知识)。最近最久未使用(LRU):换掉最久没被访问的,效果好但实现开销大。时钟算法(CLOCK / 二次机会):LRU 的近似实现。注意 FIFO 有 Belady 异常,LRU 和 OPT 不会,这是重点知识。
分页地址转换实例
光说”页号 + 页内偏移”不够具体,咱们走一个真实的地址转换。假设:
- 页面大小 = 4KB = 4096 字节(注意:页框大小和页大小一样)。
- 页内偏移占 12 位(因为 2^12 = 4096)。
- 给定一个逻辑地址 8452,要把这个地址转成物理地址。
第一步,拆分逻辑地址。页号 = 8452 整除 4096 = 2,页内偏移 = 8452 取模 4096 = 8452 - 8192 = 260。也就是说,8452 这个地址落在第 2 号页里,偏移到页内第 260 个字节。
第二步,查页表。页表里记录”逻辑页号 → 物理页框号”的映射,假设页表内容是:
- 页号 0 → 物理页框 5
- 页号 1 → 物理页框 8
- 页号 2 → 物理页框 3
查到页号 2 对应物理页框 3。
第三步,拼物理地址。物理地址 = 物理页框号 × 页大小 + 页内偏移 = 3 × 4096 + 260 = 12288 + 260 = 12548。
整个过程用伪代码写就是:
1 | PAGE_SIZE = 4096 |
注意一个重点:页大小是 2 的幂(比如 4KB),就是为了用”高位当页号、低位当偏移”快速拆分,不用真的做除法,硬件直接截位就行。
页面置换算法实例(FIFO、LRU、OPT 各跑一遍)
这是最容易混淆的部分,咱们用一个经典的访问序列,把三种算法都算一遍,看看缺页率差多少。假设:
- 页面访问序列:7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1
- 分配 3 个页框。
- 一开始页框全空。
先看 FIFO(先进先出)。规则:换掉最早进入内存的页,命中时不调整顺序。
- 访问 7:缺页,装入 7,页框 = [7]。
- 访问 0:缺页,装入 0,页框 = [7, 0]。
- 访问 1:缺页,装入 1,页框 = [7, 0, 1]。
- 访问 2:缺页,换掉最早进的 7,页框 = [0, 1, 2]。
- 访问 0:命中(0 在页框里),不调整顺序,页框 = [0, 1, 2]。
- 访问 3:缺页,换掉最早进的 0,页框 = [1, 2, 3]。
- 访问 0:缺页,换掉最早进的 1,页框 = [2, 3, 0]。
- 访问 4:缺页,换掉最早进的 2,页框 = [3, 0, 4]。
- 访问 2:缺页,换掉最早进的 3,页框 = [0, 4, 2]。
- 访问 3:缺页,换掉最早进的 0,页框 = [4, 2, 3]。
- 访问 0:缺页,换掉最早进的 4,页框 = [2, 3, 0]。
- 访问 3:命中。
- 访问 2:命中。
- 访问 1:缺页,换掉最早进的 2,页框 = [3, 0, 1]。
- 访问 2:缺页,换掉最早进的 3,页框 = [0, 1, 2]。
- 访问 0:命中。
- 访问 1:命中。
- 访问 7:缺页,换掉最早进的 0,页框 = [1, 2, 7]。
- 访问 0:缺页,换掉最早进的 1,页框 = [2, 7, 0]。
- 访问 1:缺页,换掉最早进的 2,页框 = [7, 0, 1]。
数一下缺页次数:第 1、2、3、4、6、7、8、9、10、11、14、15、18、19、20 次访问缺页,共 15 次。访问总次数 20,缺页率 = 15 / 20 = 75%。
再看 LRU(最近最久未使用)。规则:换掉最久没被访问的页,命中时把访问的页”提到最新”。
- 访问 7:缺页,[7]。
- 访问 0:缺页,[7, 0](0 最新)。
- 访问 1:缺页,[7, 0, 1](1 最新,7 最旧)。
- 访问 2:缺页,换掉最旧的 7,[0, 1, 2]。
- 访问 0:命中,0 提到最新,[1, 2, 0]。
- 访问 3:缺页,换掉最旧的 1,[2, 0, 3]。
- 访问 0:命中,0 提到最新,[2, 3, 0]。
- 访问 4:缺页,换掉最旧的 2,[3, 0, 4]。
- 访问 2:缺页,换掉最旧的 3,[0, 4, 2]。
- 访问 3:缺页,换掉最旧的 0,[4, 2, 3]。
- 访问 0:缺页,换掉最旧的 4,[2, 3, 0]。
- 访问 3:命中,3 提到最新,[2, 0, 3]。
- 访问 2:命中,2 提到最新,[0, 3, 2]。
- 访问 1:缺页,换掉最旧的 0,[3, 2, 1]。
- 访问 2:命中,2 提到最新,[3, 1, 2]。
- 访问 0:缺页,换掉最旧的 3,[1, 2, 0]。
- 访问 1:命中,1 提到最新,[2, 0, 1]。
- 访问 7:缺页,换掉最旧的 2,[0, 1, 7]。
- 访问 0:命中,0 提到最新,[1, 7, 0]。
- 访问 1:命中,1 提到最新,[7, 0, 1]。
数一下缺页次数:第 1、2、3、4、6、8、9、10、11、14、16、18 次访问缺页,共 12 次。缺页率 = 12 / 20 = 60%。
最后看 OPT(最佳置换)。规则:换掉将来最长时间不被访问的页。这需要”预知未来”,所以实际无法实现,但它是理论最优,用来给其他算法做下界。
- 访问 7:缺页,[7]。
- 访问 0:缺页,[7, 0]。
- 访问 1:缺页,[7, 0, 1]。
- 访问 2:缺页,看未来:7 下次出现在第 17 位,0 下次在第 4 位,1 下次在第 13 位。换掉最远的 7,[2, 0, 1]。
- 访问 0:命中。
- 访问 3:缺页,未来:2 在第 8 位,0 在第 6 位,1 在第 13 位。换掉最远的 1,[2, 0, 3]。
- 访问 0:命中。
- 访问 4:缺页,未来:2 在第 8 位,0 在第 10 位,3 在第 9 位。换掉最远的 0,[2, 4, 3]。
- 访问 2:命中。
- 访问 3:命中。
- 访问 0:缺页,未来:2 在第 12 位,4 不再出现,3 在第 11 位。换掉不再出现的 4,[2, 0, 3]。
- 访问 3:命中。
- 访问 2:命中。
- 访问 1:缺页,未来:2 在第 14 位,0 在第 15 位,3 不再出现。换掉不再出现的 3,[2, 0, 1]。
- 访问 2:命中。
- 访问 0:命中。
- 访问 1:命中。
- 访问 7:缺页,未来:2 不再出现,0 在第 18 位,1 在第 19 位。换掉不再出现的 2,[7, 0, 1]。
- 访问 0:命中。
- 访问 1:命中。
数一下缺页次数:第 1、2、3、4、6、8、11、14、18 次访问缺页,共 9 次。缺页率 = 9 / 20 = 45%。
三种算法对比:FIFO 缺页率 75%、LRU 缺页率 60%、OPT 缺页率 45%。规律是 OPT 最好(理论下界)、LRU 次之、FIFO 最差。学习时一般算 LRU 和 FIFO,OPT 偶尔涉及。算这类题的关键是每一步都要在草稿纸上把当前页框内容写出来,别凭脑子记。
Belady 异常实例
FIFO 算法有个反直觉的现象:分配的页框变多,缺页率反而上升,这就是 Belady 异常。这是重点知识,必须能用一个具体例子证明。
用这个访问序列:1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5。
先看分配 3 个页框时的 FIFO:
- 1:缺页,[1]。
- 2:缺页,[1, 2]。
- 3:缺页,[1, 2, 3]。
- 4:缺页,换 1,[2, 3, 4]。
- 1:缺页,换 2,[3, 4, 1]。
- 2:缺页,换 3,[4, 1, 2]。
- 5:缺页,换 4,[1, 2, 5]。
- 1:命中。
- 2:命中。
- 3:缺页,换 1,[2, 5, 3]。
- 4:缺页,换 2,[5, 3, 4]。
- 5:命中。
缺页次数:第 1、2、3、4、5、6、7、10、11 次,共 9 次。缺页率 = 9 / 12 = 75%。
再看分配 4 个页框时的 FIFO:
- 1:缺页,[1]。
- 2:缺页,[1, 2]。
- 3:缺页,[1, 2, 3]。
- 4:缺页,[1, 2, 3, 4]。
- 1:命中。
- 2:命中。
- 5:缺页,换 1,[2, 3, 4, 5]。
- 1:缺页,换 2,[3, 4, 5, 1]。
- 2:缺页,换 3,[4, 5, 1, 2]。
- 3:缺页,换 4,[5, 1, 2, 3]。
- 4:缺页,换 5,[1, 2, 3, 4]。
- 5:缺页,换 1,[2, 3, 4, 5]。
缺页次数:第 1、2、3、4、7、8、9、10、11、12 次,共 10 次。缺页率 = 10 / 12 ≈ 83.3%。
看到了吗?页框从 3 个增加到 4 个,缺页次数从 9 次涨到 10 次,缺页率从 75% 涨到 83.3%——给更多内存反而更差,这就是 Belady 异常。原因在于 FIFO 替换的是”最早进入”的页,而不是”最不常用”的页,有时候换出去的恰好是马上要再用的页。
同样这个序列,LRU 和 OPT 不会出现 Belady 异常——这是它们对 FIFO 的关键优势,也是”哪些算法有 Belady 异常”的标准答案(只有 FIFO 有,LRU 和 OPT 没有)。这一点务必记牢。
分页 vs 分段:内部碎片 vs 外部碎片
分页和分段的核心区别是”内部碎片”和”外部碎片”,这两个词最容易混。用一个具体的例子分清。
假设内存有 100KB 空闲,要放两个程序。
分页情况,页大小 4KB。程序 A 需要 10KB,得分 3 页(3 × 4 = 12KB),多出来的 2KB 是程序用不上的,这 2KB 叫”内部碎片”——它已经被分给程序 A 了,但 A 用不到,别人也用不了。程序 B 需要 5KB,得分 2 页(8KB),多出 3KB 内部碎片。这种碎片在”页内”,没法被外部利用。
分段情况,段大小不固定,按程序逻辑划分。程序 A 的代码段 6KB、数据段 4KB,刚好 10KB,没有内部碎片。程序 B 的代码段 3KB、数据段 2KB,刚好 5KB,也没有内部碎片。但问题是:内存里可能因为反复分配回收,留下”零碎”的空闲区——比如有 3KB 的空闲、2KB 的空闲、4KB 的空闲,加起来 9KB,但程序 C 要一个连续的 8KB 段却放不下(因为不连续)。这种”零碎的、加起来够大但单独不够用”的空闲区叫”外部碎片”。
一句话总结:分页固定大小,页内填不满产生内部碎片;分段按需分配,段间留缝隙产生外部碎片。解决外部碎片的办法是”紧凑”(把零碎空闲区合并成大块),但开销大;解决内部碎片的办法是减小页大小,但页表会变大。这个权衡是常涉及的知识点。
文件系统与I/O
文件物理存放三种方式:连续分配、链接分配、索引分配。
文件分配方式实例
光说”连续、链接、索引”容易记混,咱们给三种方式一个具体的存储示例。假设磁盘有 20 个块(编号 0-19),三个文件 A、B、C 分别需要 3、2、4 个块。
连续分配,每个文件占一片连续的磁盘块。
- 文件 A(3 块):占据块 5、6、7。目录里记录”文件 A → 起始块 5,长度 3”。
- 文件 B(2 块):占据块 8、9。目录记录”文件 B → 起始块 8,长度 2”。
- 文件 C(4 块):占据块 15、16、17、18。目录记录”文件 C → 起始块 15,长度 4”。
要读文件 A 的第 2 块(从 0 开始数),直接算 5 + 2 = 7,访问块 7,一次 IO 搞定,最快。问题是:如果删了文件 B,块 8、9 空出来了,但下一个新文件 D 需要 3 块,连续分配放不进去(只有 2 块连续空闲),就产生了外部碎片。
链接分配,每个文件用链表串起磁盘块,每块末尾存下一块的指针。
- 文件 A:块 5(指针→9)→ 块 9(指针→3)→ 块 3(指针→NULL)。
- 文件 B:块 8(指针→12)→ 块 12(指针→NULL)。
- 文件 C:块 15(指针→1)→ 块 1(指针→7)→ 块 7(指针→11)→ 块 11(指针→NULL)。
- 目录记录”文件 A → 起始块 5,结束块 3”。
要读文件 A 的第 2 块,得从头遍历:先读块 5 拿到指针 9,再读块 9 拿到指针 3,才能访问到第 2 块(块 3)。要读 2 次,不支持随机访问,这是链接分配最大的缺点。优点是没有外部碎片,磁盘哪里有空块都能用。
索引分配,给每个文件建一个索引表,索引表里存这个文件所有数据块的块号。索引表本身存在一个”索引块”里。
- 文件 A 的索引块是块 10,块 10 里存的是 [5, 9, 3](即数据块号列表)。
- 文件 B 的索引块是块 13,块 13 里存的是 [8, 12]。
- 文件 C 的索引块是块 14,块 14 里存的是 [15, 1, 7, 11]。
- 目录记录”文件 A → 索引块 10”。
要读文件 A 的第 2 块,先读索引块 10 拿到列表 [5, 9, 3],第 2 项是 9,直接访问块 9。读 2 次(一次读索引块,一次读数据块),但支持随机访问——想读第几块直接查表。i 节点(inode)就是这种思路,Linux/Unix 文件系统用的是这种方式。索引分配的缺点是索引表本身占空间,文件很大时索引表也很长,要分级索引(直接索引、一级间接索引、二级间接索引)。
I/O控制方式
I/O 管理记住三种控制方式:程序查询、中断驱动、DMA。SPOOLing(假脱机)是把独占设备虚拟成共享设备,典型例子是打印机排队。
三种 I/O 控制方式工作流程对比
程序查询方式,CPU 全程盯着设备看。
- 第一步:CPU 发出 I/O 命令,让设备开始工作(比如让磁盘读一个块)。
- 第二步:CPU 进入循环,不断读设备的状态寄存器,看”好了没、好了没”。
- 第三步:设备完成,CPU 从设备数据寄存器读取数据,存到内存。
整个过程 CPU 一直在等,干不了别的事,效率最低。适合 CPU 闲着没事、设备很快的简单场景。
中断驱动方式,CPU 发完命令就去做别的事,设备完成了再来叫它。
- 第一步:CPU 发出 I/O 命令,然后立刻去执行别的进程(CPU 不等)。
- 第二步:设备独立完成数据准备工作。
- 第三步:设备完成后向 CPU 发中断信号。
- 第四步:CPU 收到中断,暂停当前工作,跳到中断处理程序,从设备数据寄存器读一个字到内存,然后恢复原进程。
CPU 只在”开始发命令”和”结束中断处理”时介入,效率高很多。但问题是:每传一个字(或一小块)就要中断一次 CPU,数据量大时 CPU 还是被中断淹没。这是当前主流方式。
DMA(直接内存访问)方式,让 DMA 控制器接管数据搬运,CPU 几乎不参与。
- 第一步:CPU 向 DMA 控制器发送命令,包括读/写方向、设备号、内存起始地址、要传输的数据量。
- 第二步:CPU 去做别的事。DMA 控制器直接控制设备和内存之间的数据传输,整个块的数据(比如 4KB)由 DMA 一次性搬完,CPU 不参与。
- 第三步:DMA 传完所有数据后,向 CPU 发一个中断。
- 第四步:CPU 收到中断,知道这次大块传输完成了。
CPU 只在”开始”和”结束”各介入一次,中间全程由 DMA 干活,适合磁盘等高速设备的大批量数据传输。注意 DMA 不是”绕过 CPU 直接访问内存给设备用”——它还是受 CPU 指挥开始的,只是搬运过程 CPU 不插手。
三种方式按效率从低到高排:程序查询 < 中断驱动 < DMA。按 CPU 占用程度从高到低排:程序查询 > 中断驱动 > DMA。重点理解”哪种方式 CPU 利用率最高””哪种适合磁盘大文件传输”,答案都是 DMA。
SPOOLing 简述
SPOOLing(假脱机)是把独占设备虚拟成共享设备。典型例子是打印机:打印机本身是独占设备,一次只能打印一个任务,但通过 SPOOLing,所有用户的打印请求先写到磁盘上的”打印队列”里,打印机一台一台从队列里取任务打印。对用户来说,好像大家都在同时用打印机,实际是被 SPOOLing 虚拟成了共享设备。常见的判断点是”SPOOLing 的核心是利用磁盘做缓冲”。
死锁
死锁不是操作系统的四大核心功能,但是进程管理里重要的延伸点,得知道。
死锁的四个必要条件
死锁要发生,必须同时满足四个条件,缺一不可,这叫”死锁的四个必要条件”:
- 互斥条件:资源一次只能被一个进程使用。比如打印机,A 在打印时 B 就用不了。
- 占有并等待条件:进程已经持有至少一个资源,同时还在等待获取别的资源。比如 A 拿着资源 R1,又去申请资源 R2。
- 不剥夺条件:已经分配给进程的资源不能被强制剥夺,只能由进程自己用完释放。比如 R1 给了 A,操作系统不能硬抢回来给 B。
- 循环等待条件:存在一个进程-资源的循环等待链。比如 A 等 B 持有的资源,B 等 C 持有的资源,C 又等 A 持有的资源,绕一圈谁也动不了。
四个条件必须同时成立才会死锁,所以预防死锁的思路就是”破坏其中一个”:
- 破坏互斥:让资源可共享(比如只读文件可以让多个进程同时读),但很多资源天生不可共享(打印机),这条不好破。
- 破坏占有并等待:要求进程一次性申请所有需要的资源,要么全拿到要么一个都不拿。代价是资源利用率低。
- 破坏不剥夺:允许抢占资源。可能造成”活锁”——进程被抢了又申请、申请了又被抢,一直忙但没进展。
- 破坏循环等待:给所有资源编号,进程只能按序号递增的顺序申请资源。这能保证不会成环,是常用的预防策略。
这四个条件和四种破坏策略要能对上号。
银行家算法思路
银行家算法是死锁避免的经典方法,思路跟银行贷款很像。假设银行家(操作系统)有一笔资金(资源),要借给若干客户(进程),每个客户事先声明自己最多要借多少(最大需求),客户分批来借(每次请求一部分),银行家要在每次放贷前先算一笔账:如果这笔钱借出去,剩下的钱还能让所有客户都顺利完成生意吗?能,就借;不能,就让客户等。
具体步骤:
- 每个进程在开始前声明自己对每类资源的最大需求量(Max)。
- 进程实际申请资源时,系统先”假装”分配给它,然后跑一个安全性检查:能不能找到一个执行顺序,让所有进程都能拿到所需资源完成、再释放?这个顺序叫”安全序列”。
- 如果能找到安全序列,说明这次分配是安全的,真正分配;如果找不到,说明分配后系统可能进入不安全状态(可能死锁),就拒绝这次请求,让进程等待。
打个比方,系统有 10 个同类资源,进程 A 声明最多要 9 个、进程 B 最多要 5 个、进程 C 最多要 3 个。当前 A 已拿 3、B 已拿 2、C 已拿 2,剩下 3 个空闲。A 又来申请 1 个(拿 4 个)。系统先假装分给它,剩 2 个空闲。然后看:B 还要 3 个,剩 2 个不够,先跳过;C 还要 1 个,剩 2 个够,给 C,C 完成后释放 3 个,剩 5 个;B 还要 3 个,5 够,给 B,B 完成后释放 5 个,剩 7 个;A 还要 5 个,7 够,给 A,A 完成。找到一个安全序列 [C, B, A],所以这次分配安全,可以批准。这就是银行家算法的核心:每次分配前先模拟,安全才真正给。
银行家算法的代价是保守——为了绝对不死锁,会拒绝一些其实安全的请求,资源利用率低。所以实际系统里很少直接用,更多是理论价值。
操作系统重点回顾
进程是资源分配单位、线程是 CPU 调度单位,这句话别记反。进程三个状态”就绪、运行、阻塞”的转换里,”阻塞→运行”这条边不存在,必须先”阻塞→就绪”再”就绪→运行”,这是最容易混淆的点。线程崩了会拖垮整个进程,进程崩了一般不影响别的进程。SJF 是非抢占的、SRTF 才是抢占版本,时间片轮转是抢占式的,这个非抢占/抢占的对应关系别记混。SJF 平均等待时间最短但会饿死长任务,时间片轮转公平响应快但切换开销大,多级反馈队列是”既要又要”的集大成者。算调度题时把甘特图画出来,每个进程的”开始时间 - 到达时间”就是等待时间。
分页有内部碎片无外部碎片、分段有外部碎片无内部碎片,这个对比是重点知识。内部碎片在页内(最后一页填不满),外部碎片在段间(零碎空闲区加起来够大但单独不够用)。分页地址转换三步走:拆逻辑地址成”页号 + 偏移”、查页表得到物理页框号、拼成物理地址 = 页框号 × 页大小 + 偏移。页面置换算法里只有 FIFO 有 Belady 异常,LRU 和 OPT 不会,这是重点;OPT 理论最优但无法实现,LRU 效果好但实现开销大,CLOCK 是 LRU 的近似。算置换题时每一步都要在草稿纸上写下当前页框内容,别凭脑子记。
文件分配三种方式记特点:连续分配快但有外部碎片、链接分配没外部碎片但不支持随机访问、索引分配支持随机访问且 inode 就是这种思路。I/O 控制方式效率从低到高是程序查询、中断驱动、DMA,CPU 占用从高到低也是这个顺序,DMA 适合磁盘大文件传输。SPOOLing 的核心是利用磁盘做缓冲、把独占设备虚拟成共享设备,打印机排队是经典例子。死锁四个必要条件:互斥、占有并等待、不剥夺、循环等待,缺一不可,破坏任何一个都能预防死锁;银行家算法是死锁避免(不是预防),核心是”分配前先模拟找安全序列、找到才真正分配”。