<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>操作系统 on 我在栀南镇的日子</title>
    <link>https://blog.ifcalm.org/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/</link>
    <description>Recent content in 操作系统 on 我在栀南镇的日子</description>
    <generator>Hugo -- 0.147.7</generator>
    <language>zh-cn</language>
    <lastBuildDate>Thu, 03 Sep 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://blog.ifcalm.org/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>第 1 篇：操作系统在做什么——三件事，和一个反复出现的手法</title>
      <link>https://blog.ifcalm.org/posts/os/01-what-an-os-does/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/01-what-an-os-does/</guid>
      <description>「管理硬件资源」是一句听不出内容的话。这一篇换个说法：操作系统在对你撒三个谎——CPU 是你的、内存是你的、你已经写进硬盘了。三个谎各配一段能跑的代码，让真实的 Linux 内核自己把谎戳破：20 个进程分 10 个核，每个都慢了 4 倍；两个进程在同一个内存地址上存着不同的值；write() 说写完了 300 MB，而内核同时报告这 300 MB 一个字节都没落盘。最后指出三个谎其实是同一招，以及这一招要付的两笔钱。</description>
    </item>
    <item>
      <title>第 2 篇：进程——把一个 CPU 变成很多个</title>
      <link>https://blog.ifcalm.org/posts/os/02-process/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/02-process/</guid>
      <description>程序是磁盘上一坨死的字节，进程是这坨字节正在被执行的一次。这一篇讲清一个进程到底由什么构成（地址空间、寄存器、打开的文件），以及它为什么必须有「状态」这个概念——用 /proc 让内核自己报告：一个在算的进程是 R，一个在睡的进程是 S。最后指出「阻塞」才是整个虚拟化能成立的原因：如果没有人会停下来等，多任务根本不划算。</description>
    </item>
    <item>
      <title>第 3 篇：进程 API——为什么创建进程要 fork 和 exec 两步</title>
      <link>https://blog.ifcalm.org/posts/os/03-process-api/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/03-process-api/</guid>
      <description>创建一个进程，最直觉的设计是「给我一个程序名，我给你一个新进程」。UNIX 偏偏拆成了两步：fork 复制一份自己，exec 把自己换成别的程序。这一篇先让你看 fork 一次调用两次返回，再证明这个看起来别扭的设计好在哪——shell 的重定向和管道，全都发生在 fork 和 exec 之间那道缝里。最后用 /proc 抓一个真正的僵尸进程，说明 wait 不是礼貌，是义务；并补上这套接口唯一一条从外面进来的路——信号，包括那个几乎人人都不知道的性质：连发十个信号，处理函数只会跑一次。</description>
    </item>
    <item>
      <title>第 4 篇：受限直接执行——内核怎么把控制权拿回来</title>
      <link>https://blog.ifcalm.org/posts/os/04-limited-direct-execution/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/04-limited-direct-execution/</guid>
      <description>让程序直接在 CPU 上跑，速度最快，但会带来两个问题：它想干什么就干什么，而且它可以永远不还回来。这一篇给出两个答案——特权级和时钟中断，并且都让硬件自己作证：一段 C 程序执行特权指令，被 CPU 直接判 SIGILL；/proc/stat 显示你什么都没做的两秒里，硬件打断了 CPU 476 次、内核换了 738 次进程。最后算一笔上下文切换的账。</description>
    </item>
    <item>
      <title>第 5 篇：调度——周转时间和响应时间不能同时最优</title>
      <link>https://blog.ifcalm.org/posts/os/05-scheduling/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/05-scheduling/</guid>
      <description>有了抢占的能力，接下来的问题是：下一个给谁？这一篇用真实测量证明三件事——同样三个作业，只换执行顺序，平均周转时间从 0.89 秒降到 0.45 秒；最优的那个策略需要预知未来，所以没法用；而让所有人轮流跑能把响应时间从 1.04 秒压到 0.00 秒，代价是平均周转反而变差。周转和响应是一对不可兼得的指标，这就是调度这件事永远没有标准答案的原因。</description>
    </item>
    <item>
      <title>第 6 篇：多级反馈队列——不问你是谁，只看你干过什么</title>
      <link>https://blog.ifcalm.org/posts/os/06-mlfq/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/06-mlfq/</guid>
      <description>上一篇卡在一个死结上：最优策略要求预知作业要跑多久，而你不可能知道。MLFQ 的破法是不问未来问过去——用完整个时间片的降级，没用完的留在原地。这一篇给出它的五条规则，说明每一条都是被一个具体的失败逼出来的，然后用真实的 Linux 作证：同一个核上 20 个进程死命抢，交互型进程的唤醒延迟中位数是 0.05 毫秒，计算型进程一停就是 60 毫秒——差了一千二百倍，而调度器从没问过它们是谁。</description>
    </item>
    <item>
      <title>第 7 篇：多核与公平份额——CFS 到底「公平」在哪</title>
      <link>https://blog.ifcalm.org/posts/os/07-multicore-cfs/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/07-multicore-cfs/</guid>
      <description>MLFQ 只管排序，不管分配比例。当一台机器要同时跑三个客户的服务时，你需要的是「这个容器拿 20%」这种硬承诺。这一篇讲 Linux 的答案：CFS 不排队，它记账——每个进程一个虚拟时间，谁的最小谁先跑。我们把 /proc/&lt;!-- raw HTML omitted --&gt;/sched 里的 vruntime 直接读出来，看到两个进程真实 CPU 用量差了 67 倍，虚拟时间却几乎一样，比值精确等于 1024/15 = 68.3。然后拆多核为什么必须每核一个队列，以及 cpu.max 这种硬上限为什么会制造延迟尖刺。</description>
    </item>
    <item>
      <title>第 8 篇：地址空间——每个进程都以为自己独占内存</title>
      <link>https://blog.ifcalm.org/posts/os/08-address-space/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/08-address-space/</guid>
      <description>第 1 篇证明了两个进程能用同一个地址存不同的值。这一篇把那个地址空间整个摊开看：读 /proc/self/maps，看到代码段、堆、栈各在哪；用 C 证明堆往上长、栈往下长；关掉地址随机化让同一个程序三次跑出一模一样的地址，再打开它看三次全变。最后指出一件反直觉的事——申请 64 MB 之后，[heap] 一个字节都没涨。</description>
    </item>
    <item>
      <title>第 9 篇：内存 API——malloc/free 和那些经典错法</title>
      <link>https://blog.ifcalm.org/posts/os/09-memory-api/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/09-memory-api/</guid>
      <description>这一篇落到 C，因为只有在这里才看得见内存管理的真相。五个经典错法逐个跑一遍，由 glibc 自己当裁判：漏掉两万次分配，RSS 从 1 MB 涨到 80 MB；用完之后接着用，读到的是后来别人写进去的数据；释放两次和释放一个栈地址，glibc 当场 abort。最危险的是第四个——往 16 字节的地里写 64 字节，程序一声不吭地跑完了，退出码 0，而 AddressSanitizer 一开就精确指出了那一行。</description>
    </item>
    <item>
      <title>第 10 篇：地址转换——从基址界限到分段</title>
      <link>https://blog.ifcalm.org/posts/os/10-address-translation/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/10-address-translation/</guid>
      <description>前面一直说「地址会被翻译」，这一篇讲第一代翻译机制怎么做的。从最简单的基址加界限讲起：一个加法一个比较，硬件就实现了重定位和保护。然后是它的问题——栈和堆之间那片空地也被算成了你的内存。分段把一对寄存器变成三对，问题解决了，但引来一个更麻烦的：外部碎片。中间用一个真实的 SIGSEGV 演示，让硬件报出精确到字节的越权地址。</description>
    </item>
    <item>
      <title>第 11 篇：空闲空间管理——碎片是怎么攒出来的</title>
      <link>https://blog.ifcalm.org/posts/os/11-free-space/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/11-free-space/</guid>
      <description>只要允许任意大小的块，外部碎片就消不掉。这一篇先用一个实测把它拍在桌上：分配 20 万块 512 字节吃掉 106 MB，全部释放后 RSS 掉回 3 MB；改成隔一个释放一个——同样释放了一半内存，RSS 一个字节都没降，连 malloc_trim 都救不了。然后讲分配器手里的全部牌：空闲链表、切分与合并、三种适配策略、伙伴系统、slab，以及为什么现代分配器全都放弃了「找最好的那块」。</description>
    </item>
    <item>
      <title>第 12 篇：分页——把「随便切」改成「切成一样大」</title>
      <link>https://blog.ifcalm.org/posts/os/12-paging/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/12-paging/</guid>
      <description>分段的死穴是块的大小任意，于是外部碎片消不掉。分页的破法粗暴而彻底：所有块一律 4096 字节。一样大之后，任何空闲块都能装下任何请求，外部碎片从定义上就不存在了。这一篇讲页表怎么把虚拟页号翻译成物理帧号，然后用实测证明「按需分页」——申请 1 GB 地址空间只占 8 MB 物理内存，碰哪一页才给哪一页。最后算两笔账：一次访问变两次，以及页表本身有多大。</description>
    </item>
    <item>
      <title>第 13 篇：TLB——页表的代价，和缓存怎么救它</title>
      <link>https://blog.ifcalm.org/posts/os/13-tlb/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/13-tlb/</guid>
      <description>分页让每次内存访问都要先查一遍页表，访存次数翻倍。TLB 是给这本账本配的一张便签，命中时翻译几乎不花时间。这一篇用一个实测把 TLB 的边界量出来：同样是读一个字节，工作集在 2048 页以内时每次 1.75 纳秒，超过之后跳到 3.42 纳秒——同一条指令、同样的数据量，只因为涉及的页变多了就慢了一倍。然后讲上下文切换为什么会让 TLB 全部作废，以及 ASID 怎么补救。</description>
    </item>
    <item>
      <title>第 14 篇：更小的页表——多级页表</title>
      <link>https://blog.ifcalm.org/posts/os/14-multilevel-page-tables/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/14-multilevel-page-tables/</guid>
      <description>朴素的页表，48 位地址空间要 512 GB，每个进程一份。但实测里 VmPTE 只有几十 KB——差了六个数量级。这一篇讲这六个数量级是怎么省下来的：把页表本身也分页，空的那一大片就整块不建。然后用一个实验把这个机制的边界拍出来——同样是碰 1000 个页、同样 4 MB 数据，挨着碰页表只涨 8 KB，散着碰涨 4008 KB。五百倍的差距，只因为数据摆放的位置不同。</description>
    </item>
    <item>
      <title>第 15 篇：交换与页面置换——内存不够时先扔谁</title>
      <link>https://blog.ifcalm.org/posts/os/15-swapping/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/15-swapping/</guid>
      <description>地址空间可以比物理内存大，靠的是把暂时用不到的页挪到磁盘上。这一篇讲那个机制，以及它真正难的部分——挑谁走。最优算法要预知未来（第二次遇到这种情况），LRU 要在每次访问时记录，也做不起，于是硬件只给了一个访问位，时钟算法用它凑合。最后用实测演示 LRU 的经典最坏情况：同一个 800 MB 的文件反复顺序扫，缓存 2 GB 时零淘汰，缓存 256 MB 时每一遍都要淘汰 800 MB——命中率归零，而访问模式一个字没改。</description>
    </item>
    <item>
      <title>第 16 篇：线程——共享地址空间的代价</title>
      <link>https://blog.ifcalm.org/posts/os/16-threads/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/16-threads/</guid>
      <description>前两部分是操作系统骗你，这一部分是你自己骗自己：你以为一行代码是一步做完的。这一篇用两个线程各加一百万次证明它不是——期望两百万，实测一百万出头，而且每次跑的数字都不一样。然后把 counter++ 反汇编出来，看到 ldr / add / str 三条指令，中间任何一处被打断都会丢结果。最后说明线程和进程的真正区别只有一条：共享地址空间——而并发的全部麻烦都从这一条来。</description>
    </item>
    <item>
      <title>第 17 篇：锁——从一条原子指令建起来</title>
      <link>https://blog.ifcalm.org/posts/os/17-locks/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/17-locks/</guid>
      <description>你不能用普通变量拼出一把锁——这一篇先跑一遍证明它：拿一个标志位当锁，结果还是错的，因为「检查」和「置位」之间那道缝跟原来一样宽。真正的解法必须来自硬件：一条把「读」和「写」焊死在一起的指令。然后是四种做法的实测对比，其中最反直觉的一条是——自旋锁比互斥量慢了九倍。</description>
    </item>
    <item>
      <title>第 18 篇：并发数据结构——加锁容易，加对锁难</title>
      <link>https://blog.ifcalm.org/posts/os/18-concurrent-data-structures/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/18-concurrent-data-structures/</guid>
      <description>给数据结构加锁很容易：函数进来上锁，出去解锁，正确性立刻有了。问题是快不快。这一篇用一个实测把最坏情况拍出来——一把大锁保护的计数器，一个线程每秒 2.7 亿次，两个线程掉到 3300 万次：加了一个线程，慢了八倍。然后给出三级解法：拆锁、换数据结构、以及最激进的一招——放弃「随时都准确」这个要求，让近似计数器做到近乎线性的扩展。中间插一节读写锁，并让 glibc 自己演示它的默认配置怎么把写者饿到 3 秒里只写进去 3 次。</description>
    </item>
    <item>
      <title>第 19 篇：条件变量——等待一件事发生</title>
      <link>https://blog.ifcalm.org/posts/os/19-condition-variables/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/19-condition-variables/</guid>
      <description>锁解决的是「别人在用，我不能进」。这一篇解决另一个问题：「条件还不成立，我得等」。用忙等来做这件事有多贵？实测把它拍在桌上——在一个核上搬运十万件货物，条件变量用 0.17 秒，忙等用 75 秒，慢了四百四十倍。然后讲三条必须记住的规矩，其中最重要的一条是：等待必须写在 while 里，绝不能写在 if 里——而这条规矩的原因，比「防止虚假唤醒」深得多。</description>
    </item>
    <item>
      <title>第 20 篇：信号量——一个原语顶两个</title>
      <link>https://blog.ifcalm.org/posts/os/20-semaphores/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/20-semaphores/</guid>
      <description>锁管互斥，条件变量管等待。信号量把两件事塞进了一个原语：一个整数加上「减到负数就睡、加回去就叫醒一个」的规则。初值设成 1 它是锁，设成 0 它是等待，设成 N 它是限流器——实测二十个线程同时冲，许可数 3 的信号量把同时在场的人数死死压在 3。这一篇也说清楚它为什么不是万能的：它的状态是一个数，而一个数表达不了「谁在等什么」。</description>
    </item>
    <item>
      <title>第 21 篇：死锁与并发 bug——不死锁的那些 bug 更常见</title>
      <link>https://blog.ifcalm.org/posts/os/21-deadlock/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/21-deadlock/</guid>
      <description>死锁很出名，但研究真实项目的并发 bug 会发现：死锁只占三分之一，另外三分之二是原子性违反和顺序违反——它们不挂死，只是偶尔算错，因此更难发现。这一篇先跑一个真死锁（一个线程 A→B，一个 B→A，两个都卡死；统一顺序后立刻正常），拆开它成立的四个必要条件，然后讲那三分之二，最后是 1997 年火星探路者号的优先级反转——它在火星上反复重启，原因是第 6 篇讲过的调度策略和这一篇的锁撞在了一起。</description>
    </item>
    <item>
      <title>第 22 篇：事件驱动与异步——从 epoll 到协程到 io_uring</title>
      <link>https://blog.ifcalm.org/posts/os/22-event-driven/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/22-event-driven/</guid>
      <description>你在网络课上写的那个「一个连接一个线程」的服务器，为什么撑不住一万个连接？这一篇把账算清楚：五万个线程要 414 MB 内存和 412 GB 地址空间，而 epoll 盯着同样五万个连接只用 256 KB——差了一千六百倍。然后讲这条路的代价（回调地狱、一处阻塞全线卡死），协程怎么把它还原成看起来同步的代码，以及 io_uring 为什么要把系统调用本身也批量化。</description>
    </item>
    <item>
      <title>第 23 篇：I/O 设备——轮询、中断、DMA</title>
      <link>https://blog.ifcalm.org/posts/os/23-io-devices/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/23-io-devices/</guid>
      <description>CPU 比外设快几个数量级，所以「怎么等」这件事本身就是设计。这一篇讲三次减负：中断让 CPU 不用一直问、DMA 让 CPU 不用亲自搬数据、而当设备快到中断反而成为负担时，又转回了轮询。中间用一个实测把「每次操作的固定开销」拍出来——同一个 64 MB 的文件，一次读 1 字节要 17.23 秒，一次读 1 MB 只要 0.00 秒，而每次系统调用的成本都是 260 纳秒左右。</description>
    </item>
    <item>
      <title>第 24 篇：磁盘与 SSD——寻道、FTL、写放大</title>
      <link>https://blog.ifcalm.org/posts/os/24-disks-ssds/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/24-disks-ssds/</guid>
      <description>机械硬盘的随机读比顺序读慢几百倍，这个数字塑造了此后三十年的文件系统设计。SSD 拿掉了机械臂，却带来一条新的怪规则——能按页读写，但只能按块擦除，于是有了 FTL、垃圾回收和写放大。这一篇也如实交代一件事：这个容器跑在虚拟磁盘上，我测出来顺序 2.2 微秒、随机 2.7 微秒，这个数字不是磁盘的性质，是虚拟化的性质。所以物理部分用可验算的算术讲，不拿测不准的数字充数。</description>
    </item>
    <item>
      <title>第 25 篇：文件与目录——inode、文件描述符、link 到底是什么</title>
      <link>https://blog.ifcalm.org/posts/os/25-files-and-directories/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/25-files-and-directories/</guid>
      <description>文件名不在文件里。这一篇用几个实验把这句话拆开：两个名字指向同一个 inode，删掉其中一个，内容完好无损；而 rm 一个正在被打开的 200 MB 文件，ls 里立刻消失、链接数变成 0，但它仍然占着 200 MB 磁盘、fd 还能照读——直到最后一个 fd 关闭。搞清楚「名字、inode、打开的文件」这三层，rm 为什么不释放空间、日志切割为什么要 reopen，这类问题就都不用背了。</description>
    </item>
    <item>
      <title>第 26 篇：文件系统实现——在一块裸盘上摆出结构</title>
      <link>https://blog.ifcalm.org/posts/os/26-filesystem-implementation/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/26-filesystem-implementation/</guid>
      <description>一块盘就是一长串编号的块，文件系统要在上面摆出目录树。这一篇把最小的那套结构讲清楚——超级块、位图、inode 表、数据块——然后用实测证明两件事：一个 1 字节的文件占掉 4096 字节（分配以块为单位），而一个「大小 1 GB」的文件可以只占 4096 字节（它是一张从偏移到块的映射表，不是一段连续的地）。再加上目录本身随文件数增长的大小，你能直接看见目录就是一个装着表的文件。</description>
    </item>
    <item>
      <title>第 27 篇：崩溃一致性——断电之后，怎么还能是对的</title>
      <link>https://blog.ifcalm.org/posts/os/27-crash-consistency/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/27-crash-consistency/</guid>
      <description>往文件里追加一个块要改三个地方：位图、inode、数据块。中间断电，这三个的任意子集可能落盘了——有些结果只是浪费空间，有些是文件里出现别人的旧数据。这一篇讲两种解法：事后扫描修复（fsck，慢到不可用）和事前写日志（journaling，今天的答案），以及日志为什么必须先写日志再写数据、为什么要有提交记录。最后用实测给出你自己写代码时唯一该记住的那条：原地覆盖有 92% 的概率被读到半成品，写临时文件再 rename 是 0%。</description>
    </item>
    <item>
      <title>第 28 篇：现代文件系统——日志结构、写时复制、校验和</title>
      <link>https://blog.ifcalm.org/posts/os/28-modern-filesystems/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/28-modern-filesystems/</guid>
      <description>第 27 篇的日志把「改到一半断电」堵上了，但还有两件事没解决：随机小写依然很贵，而且没有人在检查读回来的数据是不是当初写下去的。这一篇讲三个答案——日志结构文件系统把所有写变成顺序追加（这个 1991 年为机械硬盘设计的思想，今天活在每一个 SSD 和每一个 LSM-tree 里）、写时复制把提交收缩成一次指针替换、校验和让「静默损坏」不再静默。开头先用一个实验说明第三件事有多必要：改掉一个字节，read() 照常返回成功。</description>
    </item>
    <item>
      <title>第 29 篇：虚拟机与容器——namespace、cgroup、镜像分层</title>
      <link>https://blog.ifcalm.org/posts/os/29-vm-containers/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/29-vm-containers/</guid>
      <description>容器不是轻量级虚拟机，它根本不是虚拟机——它就是宿主机上一个普通进程，只是被内核换了几副眼镜。这一篇让内核自己证明：同一个进程，NSpid 那一行同时写着外面的 8 和里面的 1；同一个路径，两个进程 cat 出完全不同的内容；新的网络 namespace 里 eth0 直接消失。然后讲三根支柱各自管什么——namespace 管「你看得见什么」，cgroup 管「你能用多少」，联合文件系统管「你的根目录从哪来」。</description>
    </item>
    <item>
      <title>第 30 篇：隔离的边界——特权提升、沙箱逃逸、侧信道</title>
      <link>https://blog.ifcalm.org/posts/os/30-isolation-boundaries/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/30-isolation-boundaries/</guid>
      <description>这门课的最后一篇，回到第 1 篇那句话：谎言总会在某个地方漏出来。这里看的是有人专门盯着漏点的时候会发生什么。用一个不需要任何漏洞的实验收尾——攻击者只靠一个普通的 mincore 系统调用，就精确还原出受害者读了文件的哪 20 页，一页不差，而且全程没有任何权限被突破。顺便说清楚 Linux 后来给它打的那个补丁为什么没有真正修好任何东西。然后讲清楚三层边界各自靠什么撑着、Meltdown 为什么是「架构层没漏、时间上漏了」，以及为什么这一层的防御永远只能说「目前还没被打穿」。</description>
    </item>
    <item>
      <title>实验一：写一个 shell</title>
      <link>https://blog.ifcalm.org/posts/os/70-lab1-shell/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/70-lab1-shell/</guid>
      <description>用 fork / exec / wait / pipe / dup2 写一个能真正用的 shell。验收标准不由我给——你的 shell 跑出来的东西要和 bash 跑出来的一模一样，diff 说了算。从执行一条命令开始，逐步加上重定向、管道、后台任务和信号处理。</description>
    </item>
    <item>
      <title>实验二：调度器——自己实现，再和真实的 CFS 对照</title>
      <link>https://blog.ifcalm.org/posts/os/71-lab2-scheduler/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/71-lab2-scheduler/</guid>
      <description>先写一个能跑的调度器模拟器，实现 FIFO、SJF、RR 和 CFS 的 vruntime，算出周转和响应时间。然后做这个实验真正的部分：用同样的负载去问真实的 Linux，把你的模型和内核的实际行为并排放。模型对不上的地方，才是你真正学到东西的地方。</description>
    </item>
    <item>
      <title>实验三：内存分配器——自己写一个 malloc</title>
      <link>https://blog.ifcalm.org/posts/os/72-lab3-allocator/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/72-lab3-allocator/</guid>
      <description>用 mmap 向内核要地，自己在上面切。实现空闲链表、切分、合并，然后用真实的程序压它。验收不靠自测——把你的分配器用 LD_PRELOAD 挂到真实命令上去，让 ls、grep、python 跑在你的 malloc 上。它们跑通了，才算你写对了。</description>
    </item>
    <item>
      <title>实验四：并发——写一个线程安全的哈希表</title>
      <link>https://blog.ifcalm.org/posts/os/73-lab4-concurrency/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/73-lab4-concurrency/</guid>
      <description>从一个故意写错的并发哈希表开始，用 ThreadSanitizer 让它自己招供——它会精确指出是哪一行、哪两个线程。然后一步步修：一把大锁、每桶一把锁、无锁读。每一步都要测扩展性，而且每一步都必须在 TSan 下干干净净。裁判是 TSan，不是「跑了几次没崩」。</description>
    </item>
    <item>
      <title>实验五：迷你文件系统——在一个普通文件上做出 inode 和目录</title>
      <link>https://blog.ifcalm.org/posts/os/74-lab5-filesystem/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/74-lab5-filesystem/</guid>
      <description>拿一个 16 MB 的普通文件当「磁盘」，在上面摆出超级块、位图、inode 表和数据块，实现 mkfs、create、write、read、mkdir、ls、unlink。验收方式很硬：把你的文件系统挂到 FUSE 上，然后用真正的 cp、tar、diff 去操作它——这些工具会用你没想到的方式调用你的接口，它们通过了才算数。</description>
    </item>
    <item>
      <title>实验六：崩溃一致性——模拟断电，验证日志</title>
      <link>https://blog.ifcalm.org/posts/os/75-lab6-crash-consistency/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/75-lab6-crash-consistency/</guid>
      <description>给实验五那个文件系统加一层可以随时「断电」的块设备：写到第 N 次就直接扔掉后面所有的写。然后穷举所有的断点，看每一次崩溃之后文件系统还对不对。你会发现没日志的版本有大量崩点会留下损坏，加了日志之后应该一个都没有。验收不是「试了几次没事」，是穷举。</description>
    </item>
    <item>
      <title>习题一：虚拟化（CPU 与内存）</title>
      <link>https://blog.ifcalm.org/posts/os/80-problem-set-1/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/80-problem-set-1/</guid>
      <description>覆盖第 2 到 15 篇。十二道题，一半是要动笔算的：调度指标、页表大小、TLB 覆盖范围、缺页代价。每题都附完整解答和推导过程，算错了能看出错在哪一步。</description>
    </item>
    <item>
      <title>习题二：并发</title>
      <link>https://blog.ifcalm.org/posts/os/81-problem-set-2/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/81-problem-set-2/</guid>
      <description>覆盖第 16 到 22 篇。有找 bug 的（给一段代码，指出它在什么交错下会错）、有算账的（阿姆达尔定律、锁竞争、C10K 的资源）、也有设计题（怎么破坏死锁的四个条件）。每题附完整解答。</description>
    </item>
    <item>
      <title>习题三：持久化与隔离</title>
      <link>https://blog.ifcalm.org/posts/os/82-problem-set-3/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/82-problem-set-3/</guid>
      <description>覆盖第 23 到 30 篇。磁盘 IOPS 的算术、写放大的估算、崩溃点的枚举、inode 与链接数的推理，以及容器隔离的边界在哪。最后两道是综合题，把整门课的线索串起来。每题附完整解答。</description>
    </item>
    <item>
      <title>术语表：中英文对照</title>
      <link>https://blog.ifcalm.org/posts/os/95-glossary/</link>
      <pubDate>Thu, 03 Sep 2026 00:00:00 +0000</pubDate>
      <guid>https://blog.ifcalm.org/posts/os/95-glossary/</guid>
      <description>操作系统里的中英文对应特别容易翻车：「页」到底是 page 还是 frame、「阻塞」是 blocked 还是 blocking、「中断」和「陷入」中文常常混着用。这一页把这些坑单独标出来，并注明每个词在哪一篇里第一次出现。</description>
    </item>
  </channel>
</rss>
