资讯详情

资讯详情

操作系统导论第1章:内核如何用中断与双模式管理资源

1. 第1章不是过场它是整门课的骨架图第一次读操作系统导论那一章我估计大多数人跟我当年一样翻得飞快。什么是操作系统、计算机系统组成、中断、双模式、多道程序设计、分时、实时——这些词看着都懂合上书一个也说不上来考试画个“计算机系统四个组成部分”的框图就能拿分于是理所当然地把它当成全书最水的一章。真正被打脸是在几年后写一个字符设备驱动的时候代码里出现了copy_from_user我盯着这个函数名琢磨了半天——凭什么它要单独存在为什么我不能直接把用户传进来的指针memcpy到内核缓冲区为什么我一执行特权指令整个机器就崩了这些问题的答案全部躺在第1章那几页里。所以这篇东西我想换个讲法。不复述教材的目录结构而是站在一个实际动手写过内核模块、调过中断、被双重释放坑过的人的角度把导论这一章重新拆一遍操作系统到底在解决什么问题、内核凭什么能管住整台机器、几种多道方案各自在什么场景下才成立、以及那几个看起来纯理论的内核数据结构为什么真的会影响你写的驱动能不能跑稳。操作系统、导论、内核、资源管理、中断、双模式、多道程序设计这些词会反复出现因为这一章的价值就在于它们是互相咬合的拆开单独背没有意义。不管你是在准备课程考核还是刚开始接触底层开发或者只是想知道自己敲下回车之后机器里发生了什么下面这些内容都能直接对应到你手上正在做的事。我尽量把抽象概念落到能复现的操作和能跑通的代码上读到某一段你觉得“原来是这个意思”的时候那大概就是第1章真正开始起作用的时刻。2. 操作系统到底在管什么从一张资源清单倒推设计2.1 计算机系统的四层堆叠以及每层在替谁挡刀教材里那张“硬件—操作系统—系统程序—应用程序—用户”的层叠图很多人当装饰看过去了。我建议你换个方式理解这张图讲的不是分层而是“谁在替谁承担复杂度”。最底下的硬件只会响应电信号和寄存器操作比如你让它把硬盘某个扇区读进内存它只知道“起始扇区号、扇区数量、目标物理地址”这几个数字至于这个物理地址属于哪个进程、这个进程有没有权限读这个扇区、读完要不要通知谁硬件一概不管。这些“一概不管”的部分全部被上面一层吃掉了。于是每一层往上都在做同一件事把下层的一堆细节封装成一个更抽象、更好用的接口。硬件把电平抽象成寄存器和指令内核把这些指令抽象成“文件”“进程”“内存空间”这些概念系统程序再把内核的系统调用封装成你能直接用的命令和库函数到了应用层你只需要open()一个路径就能读文件完全不用关心磁头在哪。这条链条一旦断掉麻烦立刻显形你在应用里传了一个非法指针给内核如果内核老老实实按地址去解引用整个系统瞬间趴下——这就是分层失效的典型后果也是后面要讲的“双模式”存在的最直接理由。我个人的经验是理解这一层叠结构最有效的方法不是背图而是沿着一次真实操作往上追溯。比如你在终端敲cat demo.txtshell 解析命令行、调用fork和execve系统调用层、内核创建进程并加载可执行文件、文件系统根据路径找到 inode、块设备驱动把逻辑块号翻译成物理扇区、磁盘控制器通过 DMA 把数据搬进内存、中断通知 CPU 数据就绪、内核唤醒等待的进程、数据被拷贝回用户缓冲区、write 到标准输出……整条路径上每一层都只干自己那一小段。你能把这条链路讲清楚第1章基本就吃透了。2.2 资源管理器、扩展机器、保护者内核的三个身份导论里给操作系统下的定义通常是一句话“操作系统是控制程序执行的软件是计算机硬件与用户之间的接口。”这句话太密拆开看其实是三个完全不同的身份而且这三个身份经常互相打架。第一个身份是资源分配器。CPU 时间、内存页、磁盘块、网络带宽、文件描述符、设备句柄全都是有限资源而进程数量可以是几百上千个。内核的核心工作之一就是在这堆互相竞争的请求之间做裁决谁先拿 CPU、谁的内存被换出去、谁的文件句柄要回收。这里所有的“公平”都是有权衡的——追求吞吐量就会牺牲响应延迟追求确定性就会牺牲利用率没有免费的午餐。你在导论里看到的批处理、分时、实时这几种方案本质上是同一套资源分配逻辑在不同目标函数下的三个解后面第4节会展开。第二个身份是扩展机器。裸硬件太难用了内核把它包装成一台“虚拟机器”交给上层。这个视角特别重要你写程序时面对的不是寄存器和中断控制器而是“进程”“虚拟地址空间”“文件”这些虚构出来的东西。教材里有一句常被忽略的话——操作系统是“扩展机器”它隐藏了硬件的真实细节呈现出一台更易编程的抽象机器。理解这句话你就明白为什么系统调用表要设计成现在这个样子read、write、open这些接口之所以长得像文件操作就是因为“一切皆文件”这个抽象能把五花八门的设备统一成一套接口。第三个身份是保护者。这一条最容易被低估。多个用户、多个进程共享同一台机器如果没有强制隔离一个写错指针的程序就能把别人的数据改成乱码。所以内核必须掌握“强制力”它需要一种机制让用户代码想干坏事也干不了而不是“约定不要干坏事”。这个强制力的技术底座就是下一节要说的中断、陷阱与双模式。这三样东西一旦理解了导论里剩下的内容基本都是它们的推论。提示读这一章时凡是看到“操作系统负责 XX”的表述都追问一句“它凭什么能做到”。答案几乎总能落到“它有特权、它能被硬件中断、它掌握着地址翻译”这三件事上。养成这个习惯后面内存管理、进程调度、文件系统会一路顺下去。2.3 一个常被跳过但很关键的前提操作系统是事件驱动的教材导论部分有一小节专门讲“操作系统的操作”讲中断驱动、I/O 结构、DMA篇幅不大很多人扫一眼就过。但这一小节其实是全书节奏的定调内核不是一个从头跑到尾的主程序它是一个被事件反复打断、处理完再回去等下一个事件的被动体。没活儿干的时候内核执行的是空闲循环或者干脆执行一条让 CPU 进入低功耗状态的指令一旦有键盘敲击、网络包到达、磁盘读完、定时器到期、进程发起系统调用硬件或软件就产生一个事件把控制权抢回内核。这个模型决定了内核代码的写法它必须是可重入的必须考虑“我正在改这个链表中断来了中断处理程序也要改这个链表”这种竞态。我见过太多刚上手的人写的驱动在中断处理函数里直接睡眠等待结果机器卡死——根源就在于没建立“内核是事件驱动、中断上下文不能阻塞”这个基本认知而这个认知的第一块砖就埋在第1章。3. 中断、陷阱与双模式内核掌权的三条硬通道3.1 中断CPU 唯一愿意主动停下来的理由先说清楚一件事CPU 在执行指令流的时候是个彻底的“死脑筋”它不会自己检查“该不该停下看看有没有新情况”。如果没有外部机制一段死循环能跑到天荒地老。让 CPU 愿意交出控制权的就是中断。中断从来源上分两类。一类是硬件中断也叫外部中断由设备控制器通过中断请求线发给 CPU比如网卡收到包、磁盘完成一次传输、键盘按下某个键。另一类是软件中断/异常由指令执行本身触发比如除零、访问非法地址、执行了非法指令以及有意为之的陷阱trap——系统调用就是通过陷阱指令主动进入内核的。这里有个关键设计值得停下来想一想为什么系统调用要用“陷阱”这种方式而不是普通函数调用因为普通函数调用只是在同一个特权级里跳转用户代码依然可以随便读写任何它想读写的内存。要让内核真正掌握控制权必须有一个硬件级别的、必然会切换特权级的入口。陷阱指令就是这么个东西CPU 执行到它会自动保存现场、切换到内核态、跳到预先登记好的处理程序地址。这个“预先登记”的动作在系统启动时就完成了——内核把各种异常和中断的处理入口写进一张表里中断向量表CPU 靠中断号去查表。这张表本身受到保护用户态改不了保护链条才闭合。我在实际排查问题时最常用的一个切入点就是看中断计数。在类 Unix 系统里/proc/interrupts这个文件会把每个 CPU 核心上每个中断号被触发的次数列出来第一列是中断号后面几列是各核计数最后是设备名。这个文件几乎是我的第一诊断工具# 观察中断分布连按两次看哪个设备在涨 cat /proc/interrupts | head -20 sleep 2 cat /proc/interrupts | head -20如果某个网卡的中断数只集中在 CPU0 上疯狂增长而其他核闲着那基本可以判断中断亲和性没配好后面会遇到单核打满、整体吞吐上不去的问题。这个现象在第1章里对应的知识点叫做“中断处理的开销”教材只是一笔带过但在真实系统里它直接决定性能上限。3.2 陷阱与系统调用用户态进入内核的合法入口把系统调用单独拎出来讲是因为它是用户程序唯一合法的越权通道。你在用户态想读文件、想申请内存、想创建进程都不能自己动手必须“请求”内核代劳。这个请求的动作在汇编层面通常是一条特定指令不同架构指令不同比如某些架构上是通过专门的陷入指令另一些是通过特定的软中断号执行后 CPU 切到内核态根据事先约定好的编号去系统调用表里找对应的处理函数。这条路径上有两个细节理解了能省很多调试时间。第一参数传递是有讲究的。系统调用的参数不能像普通函数那样放在栈上随便传因为特权级切换时栈也换了。实际做法通常是把参数放在寄存器里或者放一个指向参数块的指针在寄存器里。这就解释了一个常见困惑为什么有些系统调用出错时返回 -1 并设置 errno而不是直接抛异常因为内核必须用一种用户态能理解的方式把错误传回去errno 就是这么个约定俗成的“回传通道”。第二系统调用的开销远大于函数调用。一次切换要保存现场、切栈、做参数校验、可能还要拷贝数据回来再恢复现场。所以高性能场景下大家都在想办法减少系统调用次数批量读写、内存映射、零拷贝本质都是在“把多次小切换合并成一次大切换”。很多人写 IO 密集型程序性能上不去profile 一看全耗在系统调用上根因就在这里。/* 一个刻意放大的对比示例逐字节写 vs 批量写 */ #include unistd.h #include string.h void slow_write(int fd, const char *buf, size_t n) { for (size_t i 0; i n; i) { write(fd, buf i, 1); /* n 次用户态/内核态切换 */ } } void fast_write(int fd, const char *buf, size_t n) { size_t off 0; while (off n) { ssize_t w write(fd, buf off, n - off); /* 通常 1 次就够 */ if (w 0) break; off (size_t)w; } }上面两段代码在同样数据量下的耗时差距在小文件上可能只有几倍但在大数据量、高并发场景下能拉开一个数量级。这不是什么高级优化技巧纯粹是对“系统调用很贵”这件事有没有概念。3.3 双模式与特权指令没有它保护就是一句空话硬件通常提供至少两种执行模式用户模式和内核模式有的架构叫法不同还有的架构支持更多层级的保护环。用一个状态位来标记当前处于哪种模式这个位只有在内核态才能改。模式切换的规则很简单但极其重要用户态只能通过陷阱或中断进入内核态内核态通过一条专门的返回指令回到用户态。硬件在执行每条指令前会检查这条指令是不是特权指令当前模式够不够格不够格就直接触发异常把进程干掉。所谓特权指令包括直接操作 I/O 设备、修改页表基址寄存器、关闭中断、加载中断向量表、切换模式位等等。这些操作一旦放开给用户态隔离就彻底失效了。我建议你用一个小实验来建立直觉前提是你有一台可以随便折腾的机器别在重要环境上试# 观察当前进程所处的保护级别部分架构上可读读不到也正常 cat /proc/self/status | grep -i -E seccomp|cap # 查看当前进程的能力集理解特权在工程上如何被细粒度切分 capsh --print 2/dev/null | head -20现代系统在“双模式”这个大框架上又加了一层细粒度控制把内核特权拆成几十种能力capability进程可以只保留自己需要的那几项。这是一个很务实的演进——宁可让一个服务只有绑定低端口的能力、没有加载内核模块的能力也不要让它要么全有要么全无。你在导论里读到的“双模式”是骨架实际系统里长出来的是这套能力机制两者是一条线上的东西。还有一个常被忽略的保护装置定时器。如果没有它一个用户态死循环就能永久霸占 CPU因为用户态没有任何指令能主动把控制权交回去。定时器由硬件周期性触发中断中断一来内核就有机会重新评估“是不是该把这个进程换下去”。所以“抢占式多任务”这件事的技术前提其实是“硬件定时器 中断 调度器”三件套缺一不可。教材导论里讲定时器往往只有一句话但它是整个分时系统能成立的物理基础值得单独记一笔。3.4 I/O 结构与 DMA为什么 CPU 不该亲自搬数据导论里讲 I/O 的部分通常会给出三种方式轮询、中断驱动、DMA。很多人的理解停留在“DMA 更高级”这个层面其实关键在谁在消耗 CPU 时间。轮询方式下CPU 反复读设备状态寄存器问“好了没”这段时间 CPU 完全被浪费在等待上。中断驱动方式下CPU 发起 I/O 后可以去干别的设备完成后发中断通知效率明显提升但数据传输本身还是要 CPU 一个字节一个字节地搬。DMA直接内存访问则把“搬数据”这件事交给了专门的控制器CPU 只需要告诉 DMA 控制器“从设备读到内存哪个地址、读多少”剩下的事情 DMA 自己干搬完再发一次中断。整块数据的搬运过程中CPU 完全解放。这个演进脉络告诉你一件很重要的事操作系统设计的核心矛盾之一就是如何让最贵的资源CPU尽量不去做最廉价的活搬字节。你在后面学缓冲、缓存、零拷贝、异步 I/O 的时候会发现全都在延续这条思路。理解了这一点导论里那段干巴巴的 I/O 结构描述就有了主线。4. 多道、分时、实时、虚拟化四种调度哲学的取舍4.1 多道程序设计把 CPU 的空等时间卖出去最早的单道批处理系统有个巨大浪费程序一旦发起 I/OCPU 就闲着等而 I/O 往往是毫秒级、CPU 计算是微秒级等一次的工夫够算几千条指令。多道程序设计的核心思想特别朴素——既然一个程序在等 I/O那就再塞一个程序进去算让 CPU 一直有活干。这件事听起来简单实现起来牵扯出一大堆基础设施内存里要同时装下多个作业就得有内存保护和地址翻译作业之间要能切换就得保存和恢复上下文一个作业在等 I/O 时要能被挂起就得有进程状态和等待队列。所以你会在导论里看到“多道程序设计需要硬件支持内存保护”这种看似突兀的话它其实是在说软件上的并发前提是硬件上能隔离。我特别想强调一个数字感受多道程序设计的收益不是线性的。假设一个作业的计算时间占 20%、I/O 等待占 80%单道时 CPU 利用率大概就是 20%塞两个作业进去CPU 同时都在等的概率是 0.8×0.80.64也就是利用率能到 36%塞五个作业全都在等的概率降到 0.8⁵≈0.33利用率接近 67%。这个简单估算说明了多道程度和利用率的关系也说明了为什么它会有上限——作业再多内存装不下、切换开销上来了收益就摊薄了。4.2 分时系统把延迟藏到人感知不到的地方批处理追求的是吞吐量分时的目标完全不同让每个交互用户都觉得自己独占一台机器。技术手段是给每个用户/进程分配一个极短的时间片轮流执行。人眼对交互延迟的容忍阈值大概在百毫秒量级只要时间片足够短、轮转足够快用户就感知不到别人也在用。这里有个取舍必须说清楚时间片越短响应越及时但上下文切换开销占比越高。假设一次上下文切换要几微秒时间片设成 1 毫秒那切换开销可能占到千分之几设成 100 微秒开销比例立刻上升到百分之几CPU 有相当一部分时间在做“换人”这件事而不是干活。时间片长度交互响应感受上下文切换开销占比适用场景极短百微秒级非常跟手偏高可能到百分之几强交互、低负载桌面中等毫秒级基本无感可控通用服务器、桌面较长十毫秒级偶尔可感知很低吞吐优先的批处理动态调整兼顾自动平衡现代通用内核的常见做法实际系统很少用固定时间片而是根据进程行为和系统负载动态调整交互型进程给短时间片保证响应计算型进程给长时间片减少切换。你在导论里看到的“分时”是一个理念落到工程上是一整套调度策略这部分内容会在后面的调度章节展开但理念的种子就在第1章。4.3 实时系统确定性优先吞吐量靠边站实时系统要解决的不是“快”而是“按时”。一个控制指令晚到 10 毫秒可能导致机械臂撞上工件那这套系统就是不合格的哪怕它平均响应只有 1 毫秒——因为平均值在实时语境下毫无意义最坏情况才是唯一有意义的指标。这就引出实时系统的两条基本路线。硬实时要求任务必须在截止时间前完成超时就是系统失效设计上往往要放弃通用操作系统的很多东西不做虚拟内存分页避免缺页导致的不可预测延迟、不做复杂的公平调度改成优先级抢占或时间触发调度、关闭或严格限制中断延迟。软实时则允许偶尔超时只是希望超时尽量少比如音视频播放偶尔丢一帧用户几乎察觉不到。我在实际项目里见过一个很典型的踩坑有人用通用内核跑一个采样周期 1 毫秒的控制任务逻辑写得没问题但偶尔会出现几十毫秒的抖动。查下来是缺页和后台 I/O 造成的不可预测延迟。解决方案不是“优化代码”而是换实时内核、把关键内存锁定在物理页、把中断亲和性绑到专用核上。这类问题的根源就是没有区分“平均快”和“最坏可控”——而这两者的差别导论在介绍实时系统时其实已经点明了。4.4 虚拟化与云计算把内核本身当成资源导论的新版本里通常会补一节虚拟化。这一节的视角转换很有意思前面讲的都是“操作系统管硬件”虚拟化讲的是“把操作系统本身也变成被管理的资源”。做法是在硬件和客户操作系统之间插一层虚拟机监控器由它来分配 CPU、内存、设备。实现上有两条路线。一条是监控器直接跑在硬件上客户系统运行在受控的虚拟环境中特权指令会被截获并由监控器模拟——这条路性能好、隔离强但客户系统需要做适配。另一条是监控器作为宿主系统上的一个进程运行客户系统的特权操作全部由宿主内核代劳——这条路部署方便但隔了一层开销相对高一些。这个演进对你理解导论里的概念有个额外好处它把“特权级”这件事从两个层级变成了多个层级。原本是用户态和内核态两层现在是“应用—客户内核—监控器—硬件”多层每一层都在用同一套思路拦截特权操作、模拟、再返回。你看懂了双模式就看得懂虚拟化的基本机制。同样的思路还延伸到了两个方向。一个是容器不做硬件虚拟化而是利用内核本身的隔离机制命名空间、控制组把进程隔开启动快、开销小但共享同一个内核。另一个是嵌入式/移动系统资源极度受限所以内核被裁剪得很小调度策略更偏向省电和响应I/O 路径高度定制。这两个方向说明一件事——导论里列出的那些“操作系统类型”不是历史陈列而是同一套原理在不同约束下的不同取值。5. 内核数据结构写驱动时才想起的那几个轮子5.1 链表与队列内核里的“随处插入”和“先进先出”导论最后那部分讲内核数据结构很多人觉得是凑数。但只要你开始读内核源码或者写驱动就会发现几乎每个子系统都在用同一批数据结构而且用得极其讲究。先说链表。内核里的链表跟你在数据结构课上写的单向链表有个关键差别它通常把“链接节点”和“数据”分离。数据结构课上链表节点里直接放数据内核里则是把next/prev指针做成一个独立的小结构然后嵌到你的数据结构里。这么做的原因很实在同一个对象可能同时挂在多个链表上比如一个进程既在就绪队列里又在某个等待队列里如果把next/prev写死在数据里就只能挂一个链表。分离之后一个对象想挂几个链表就嵌几个节点。/* 简化的入侵式链表节点理解思想即可 */ struct list_node { struct list_node *next; struct list_node *prev; }; /* 你的业务结构体把节点嵌进来而不是把数据塞进节点 */ struct task { int pid; int state; struct list_node run_list; /* 挂在就绪队列上 */ struct list_node wait_list; /* 同时挂在某个等待队列上 */ };这个设计带来的一个“陷阱”是由节点反查宿主结构体需要做地址计算。因为节点嵌在结构体中间你知道节点的地址要算出结构体首地址得减去节点在结构体中的偏移量。内核里通常用两个宏封装这个计算——一个算偏移一个做反查。这套东西第一次看很晕但想明白之后你会发现它极其优雅不用为每种类型写一套链表操作一套通用链表就能服务所有子系统。再说队列。队列在内核里的典型应用是就绪队列、各种等待队列、消息队列。它的语义就是先进先出但内核实现里往往要额外考虑并发多个 CPU 同时入队出队怎么办所以你会看到队列操作常常配有自旋锁或者在设计上做成每个 CPU 一个本地队列以减少争用。这个“为了并发而改造基础数据结构”的思路会贯穿整个内核。5.2 树、哈希表与位图三种“查找”的取舍选数据结构的本质是选查找方式而选查找方式要先回答一个问题你的数据规模有多大、查询有多频繁、需不需要有序。结构查找复杂度是否需要有序内核中的典型用途取舍要点链表O(n)插入有序则有序短小的等待队列、设备列表简单规模大就慢平衡树O(log n)天然有序按地址索引的内存区域、定时器有序查询强节点开销大哈希表期望 O(1)无序按 ID/名字快速定位对象极快但要处理冲突和扩容位图O(1) 定位天然带序号语义资源分配标记、页状态极省空间只适合二值状态平衡树那一条值得多说两句。内核里有一类非常常见的需求是“给一个地址找到它属于哪个区间”比如判断某个虚拟地址落在哪个已映射的内存区域里。这种需求用哈希表就不好办因为哈希是按精确键查找的而这里要查的是“范围”。平衡树尤其是那种每个节点存一段区间的变体天生适合这种查询从根往下比较能在对数时间内定位到包含目标地址的区间。位图则是一个被严重低估的结构。它的本质是“用一位表示一个对象的状态”空间效率高得离谱管理 100 万个页框用位图只需约 122KB用链表每个节点至少十几个字节那就是十几 MB。所以物理页分配器普遍用位图或类似结构来记录“哪些页空闲”。代价是它只能表达二值状态而且分配时通常要顺序扫描找空闲位所以实现上会配合“记录下一个空闲位置”之类的优化来避免每次从头扫。/* 位图的两个核心操作看代码比看描述直观 */ #define BITS_PER_WORD (sizeof(unsigned long) * 8) static inline void bitmap_set(unsigned long *map, unsigned int nr) { map[nr / BITS_PER_WORD] | (1UL (nr % BITS_PER_WORD)); } static inline void bitmap_clear(unsigned long *map, unsigned int nr) { map[nr / BITS_PER_WORD] ~(1UL (nr % BITS_PER_WORD)); } static inline int bitmap_test(const unsigned long *map, unsigned int nr) { return (map[nr / BITS_PER_WORD] (nr % BITS_PER_WORD)) 1UL; }注意上面这段代码只在单线程语境下成立。多核环境里对同一个字的“读—改—写”必须用原子操作否则两个核同时置位会互相覆盖。这个坑我在早期写的一个简易分配器里踩过症状是偶发的“资源泄漏”查了很久才发现是竞态。选型上我自己的判断顺序是先问规模再问查询模式最后才看实现复杂度。规模小几十个元素用链表最省事不要为了“优雅”上树查询是精确键、量又大就上哈希查询是范围或需要有序遍历就上树状态是二值且对象数量巨大就上位图。这个顺序不是教条但它能避免绝大多数“为了用而用”的错误。6. 常见问题与排查实录6.1 一张速查表那些看起来像“玄学”的现象导论这一章的很多概念在出问题的时候会以非常具体的症状出现。我把这些年遇到过、也被别人问过很多次的情况整理成一张表方便你对着症状往回找原理。现象最可能的原理层面原因排查切入点程序死循环后整机无响应定时器中断被屏蔽或调度失效检查是否在临界区长时间关中断驱动里sleep导致系统挂起中断上下文不可阻塞确认代码是否在中断处理路径上高并发下数据偶发错乱缺少原子操作或锁存在竞态检查共享数据的读改写是否原子IO 密集型程序 CPU 占用极高系统调用次数过多统计调用次数考虑批量化服务偶发几十毫秒抖动缺页、后台 IO 带来的不可预测延迟锁内存页、隔离 IO 路径多核机器单核打满中断亲和性或锁争用集中观察各核中断计数与负载分布修改内核配置后启动失败特权级/启动流程被破坏用安全模式回退检查早期初始化6.2 三个真实的排查过程以及它们指向的第1章知识点第一个案例关于“关中断的代价”。有个模块需要保证一段操作不被中断打断作者图省事在整段逻辑外面包了一个关中断这段逻辑里还包含一次等设备的循环。表现是系统偶尔卡顿几百毫秒。根因很清楚关中断期间定时器中断也进不来抢占被彻底禁用其他进程只能干等。正确的做法是把临界区压缩到只保护共享数据的几行代码绝不把等待、拷贝、日志这类耗时的活放进关中断区间。这条经验对应的正是导论里“双模式与中断”的部分——关中断是一种极强的特权操作代价是全局性的。第二个案例关于“用户态指针”。一个刚上手的人写字符设备直接把用户传进来的缓冲区指针存下来在中断处理函数里往那个地址写数据。结果时好时坏偶尔整个系统崩。原因有两层一是用户态指针在当前进程上下文之外可能根本无效换了个进程地址空间就变了二是往用户地址写数据必须经过专门的拷贝接口由内核做地址合法性校验不能直接解引用。这件事的技术背景就是导论里讲的“操作系统作为保护者”和“双模式”——用户空间和内核空间是两套地址语义跨越边界必须走受控通道。第三个案例关于“位图分配的竞态”。前面代码块里提过一个简易资源分配器用位图记录空闲块单线程测试一切正常多线程压测偶发泄漏。原因就是两个线程同时对同一个字做“读—改—写”后写的覆盖了先写的。修法很简单把置位和清位换成原子操作。但这个 bug 背后的思维方式值得记住导论里介绍数据结构时完全不提并发而真实的操作系统里任何一个共享结构都必须回答“多核同时访问怎么办”。这个落差是很多人从课本走向工程时最大的不适应。6.3 几条我认为最有价值的实操心得第一条别急着背概念先建立“一次操作的全链路视图”。挑一个你天天用的动作比如打开文件、发一个网络请求从应用层一路追到硬件层把每一层做了什么、边界在哪写下来。写完你会发现导论里那些零散的名词自动就串起来了而且记得特别牢。这个方法我用过很多次比反复读书有效得多。第二条动手观察不要只靠想象。中断分布、内存映射、进程状态、系统调用次数这些在现代系统上都有现成的观察入口。我习惯在遇到性能或稳定性问题时先把这几类信息抓一遍往往五分钟就能锁定方向。导论里讲的那些机制不是纸上的它们全都在实时运行只是你没去看。第三条警惕“平均值陷阱”。导论里介绍分时和实时的时候其实已经暗示了这一点交互体验看的是尾部延迟实时控制看的是最坏情况只有吞吐量才看平均值。很多优化做不下去就是因为一直在优化平均值而用户感知的是那 1% 的慢请求。第四条把“特权”当成一条主线来理解整个系统。从双模式到系统调用从虚拟化到容器本质都是在回答同一个问题谁能执行特权操作、特权如何被委托、委托的边界在哪。你把这条线捋顺了后面读内存管理、文件系统、设备驱动会顺得多因为它们全都在不同层面上重复这个模式。最后分享一个我自己养成的习惯每学完一个抽象概念就问自己“如果这个机制不存在系统会以什么方式坏掉”。双模式不存在任何程序都能改别人的内存定时器不存在一个死循环就能霸占整机中断不存在CPU 只能靠轮询效率低到无法接受多道不存在CPU 大部分时间在等 IO。这个反问法特别适合第1章因为这一章里几乎每个概念都是“为了解决某个具体问题才被发明出来的”把问题想清楚答案自然就留住了。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →