第 11 篇:空闲空间管理——碎片是怎么攒出来的
只要允许任意大小的块,外部碎片就消不掉。这一篇先用一个实测把它拍在桌上:分配 20 万块 512 字节吃掉 106 MB,全部释放后 RSS 掉回 3 MB;改成隔一个释放一个——同样释放了一半内存,RSS 一个字节都没降,连 malloc_trim 都救不了。然后讲分配器手里的全部牌:空闲链表、切分与合并、三种适配策略、伙伴系统、slab,以及为什么现代分配器全都放弃了「找最好的那块」。
只要允许任意大小的块,外部碎片就消不掉。这一篇先用一个实测把它拍在桌上:分配 20 万块 512 字节吃掉 106 MB,全部释放后 RSS 掉回 3 MB;改成隔一个释放一个——同样释放了一半内存,RSS 一个字节都没降,连 malloc_trim 都救不了。然后讲分配器手里的全部牌:空闲链表、切分与合并、三种适配策略、伙伴系统、slab,以及为什么现代分配器全都放弃了「找最好的那块」。
分段的死穴是块的大小任意,于是外部碎片消不掉。分页的破法粗暴而彻底:所有块一律 4096 字节。一样大之后,任何空闲块都能装下任何请求,外部碎片从定义上就不存在了。这一篇讲页表怎么把虚拟页号翻译成物理帧号,然后用实测证明「按需分页」——申请 1 GB 地址空间只占 8 MB 物理内存,碰哪一页才给哪一页。最后算两笔账:一次访问变两次,以及页表本身有多大。
分页让每次内存访问都要先查一遍页表,访存次数翻倍。TLB 是给这本账本配的一张便签,命中时翻译几乎不花时间。这一篇用一个实测把 TLB 的边界量出来:同样是读一个字节,工作集在 2048 页以内时每次 1.75 纳秒,超过之后跳到 3.42 纳秒——同一条指令、同样的数据量,只因为涉及的页变多了就慢了一倍。然后讲上下文切换为什么会让 TLB 全部作废,以及 ASID 怎么补救。
朴素的页表,48 位地址空间要 512 GB,每个进程一份。但实测里 VmPTE 只有几十 KB——差了六个数量级。这一篇讲这六个数量级是怎么省下来的:把页表本身也分页,空的那一大片就整块不建。然后用一个实验把这个机制的边界拍出来——同样是碰 1000 个页、同样 4 MB 数据,挨着碰页表只涨 8 KB,散着碰涨 4008 KB。五百倍的差距,只因为数据摆放的位置不同。
地址空间可以比物理内存大,靠的是把暂时用不到的页挪到磁盘上。这一篇讲那个机制,以及它真正难的部分——挑谁走。最优算法要预知未来(第二次遇到这种情况),LRU 要在每次访问时记录,也做不起,于是硬件只给了一个访问位,时钟算法用它凑合。最后用实测演示 LRU 的经典最坏情况:同一个 800 MB 的文件反复顺序扫,缓存 2 GB 时零淘汰,缓存 256 MB 时每一遍都要淘汰 800 MB——命中率归零,而访问模式一个字没改。
前两部分是操作系统骗你,这一部分是你自己骗自己:你以为一行代码是一步做完的。这一篇用两个线程各加一百万次证明它不是——期望两百万,实测一百万出头,而且每次跑的数字都不一样。然后把 counter++ 反汇编出来,看到 ldr / add / str 三条指令,中间任何一处被打断都会丢结果。最后说明线程和进程的真正区别只有一条:共享地址空间——而并发的全部麻烦都从这一条来。
你不能用普通变量拼出一把锁——这一篇先跑一遍证明它:拿一个标志位当锁,结果还是错的,因为「检查」和「置位」之间那道缝跟原来一样宽。真正的解法必须来自硬件:一条把「读」和「写」焊死在一起的指令。然后是四种做法的实测对比,其中最反直觉的一条是——自旋锁比互斥量慢了九倍。
给数据结构加锁很容易:函数进来上锁,出去解锁,正确性立刻有了。问题是快不快。这一篇用一个实测把最坏情况拍出来——一把大锁保护的计数器,一个线程每秒 2.7 亿次,两个线程掉到 3300 万次:加了一个线程,慢了八倍。然后给出三级解法:拆锁、换数据结构、以及最激进的一招——放弃「随时都准确」这个要求,让近似计数器做到近乎线性的扩展。中间插一节读写锁,并让 glibc 自己演示它的默认配置怎么把写者饿到 3 秒里只写进去 3 次。
锁解决的是「别人在用,我不能进」。这一篇解决另一个问题:「条件还不成立,我得等」。用忙等来做这件事有多贵?实测把它拍在桌上——在一个核上搬运十万件货物,条件变量用 0.17 秒,忙等用 75 秒,慢了四百四十倍。然后讲三条必须记住的规矩,其中最重要的一条是:等待必须写在 while 里,绝不能写在 if 里——而这条规矩的原因,比「防止虚假唤醒」深得多。
锁管互斥,条件变量管等待。信号量把两件事塞进了一个原语:一个整数加上「减到负数就睡、加回去就叫醒一个」的规则。初值设成 1 它是锁,设成 0 它是等待,设成 N 它是限流器——实测二十个线程同时冲,许可数 3 的信号量把同时在场的人数死死压在 3。这一篇也说清楚它为什么不是万能的:它的状态是一个数,而一个数表达不了「谁在等什么」。