外观
lock:按 CPU 分配内存,并实现写者优先读写锁
版本:lock,提交 af12b48372ebf7a5d9f441524ebbbc4e255dfe72。2025 后半任务为读写自旋锁;官网开头残留的 block cache 描述不能替代后文具体要求与骨架。先读并发。
第一部分:让常见路径各走各的锁
原分配器只有 kmem.lock,一次 kalloc/kfree 即使很短,也会让多个 CPU 争用同一 cache line。改为 NCPU 个 freelist,每个带自己的锁。kinit 初始化全部结构,freerange 可先把内存放到启动 CPU;其他 CPU 按需偷取。
每次先关闭本 CPU 中断,再取得并使用 cpuid,保证选定本地表期间不迁移;完成后恢复嵌套状态。本地有页则直接取走。本地为空时,释放本地锁再逐个看远端,摘下一批页后释放远端锁,最后把剩余部分放入本地。这样容易做到一次只持一把 allocator 锁,先避免死锁,再讨论批量大小。
批量偷取是摊销优化:每次只偷一页,CPU 连续分配会频繁访问远端;每次偷走全部,可能让另一 CPU 立刻变空。可以先用固定小批次,记录 steal 次数与测试时间,再调整策略。没有一个对所有负载都最优的批量。
核心不变量:全局所有 freelist 的并集等于未分配页集合,各列表互不重叠。验证时特别看链表切断:摘下 k 个节点后,尾节点 next 应正确分离,否则两条表会共享一段链,最终同页重复分配。
第二部分:把互斥锁扩展成读写协议
kernel/spinlock.c 已有 read_acquire_inner/read_release_inner/write_acquire_inner/write_release_inner 骨架,最初用一把普通锁退化实现。外层 read/write acquire/release 已负责 push_off/pop_off。修改 inner 时保留这套外层契约,避免漏恢复或错误地提早开中断。
一种便于证明的设计,用短暂的元数据自旋锁保护三类状态:readers、writer_active、writers_waiting。它只保护状态更新,不在整个读临界区中持有,否则读者仍然串行。
text
读者尝试:
取得元数据锁
只有无活跃写者且无等待写者时,readers++ 并成功
否则释放元数据锁,重试
写者尝试:
登记 writers_waiting++(只登记一次)
等待 readers==0 且无活跃写者
原子地改为 writer_active=true,并撤销等待登记
释放:
在元数据锁下减少 readers,或清 writer_active真正使用这一思路时,必须保证状态锁的 acquire/release 带内存顺序,并且等待时不握着状态锁。若读者在 while 中持锁等待 writer_active 清零,写者连释放状态都无法更新,系统会卡死。
写者优先保证后来的读者不无限插队,但它并不自动保证写者之间 FIFO,也不保证读者在连续写入压力下无饥饿。报告应陈述自己实际实现的公平性,而不是把“有等待计数”描述成所有线程公平。
观察测试,而不是仅追逐分数
kalloctest 检查 allocator,usertests sbrkmuch 检查可用内存没有丢失,rwlktest 检查读写协议,最后执行 usertests -q 与 make grade。该固定分支的读写锁测试采用四个 CPU 屏障,Makefile 的 LAB_LOCK 设置也对应四核;不要拿 CPUS=1 的调试习惯直接运行 rwlktest,否则可能只是屏障等不到参与者。
可用 make clean 后 make KCSAN=1 qemu 做额外竞争检查。KCSAN 会扰动性能,因此先看有无 race,再用普通构建比较竞争次数。宿主机繁忙时,计数和耗时都可能波动;记录样本而不只选最漂亮的一次。
自测:为什么 writers_waiting 不能在每一轮重试时都加一?
它表示等待写者的数量,不是失败尝试的数量。同一个写者重复增加会导致成功后只减一次,计数永远大于零,所有后续读者被永久阻塞。
来源:2025 lock 官方任务,对应 kalloc.c/spinlock.c/spinlock.h/Makefile。