前言
花两天把lab3B/C写了一下,有了A的基础,简单了不少。gitee地址放在末尾。
一、3B/3C 前的整体认知
1.1 3B 的目标
Leader 接收
Start(command)→ 追加到rf.logs→ 复制到多数派 → 推进commitIndex→ 通过applyChan交给状态机。
1.2 3C 的目标
把term / votedFor / logs做到“随时可重启且不违背承诺”(同一任期不可能投两次票、日志不可能倒退)。
二、Part B:日志复制(Log Replication,3B)
2.1 整体思路:把 Raft 看成一条闭环流水线
主要流程:Start 写入 → AppendEntries 复制 → reply 反馈 → Leader 推进 commit → 心跳携带 LeaderCommit → 双端 applier 应用。
客户端 → Leader:Start
Leader → Followers:AppendEntries(entries, LeaderCommit)
Followers → Leader:AppendEntriesReply(Success/Term)
Leader:更新 nextIndex/matchIndex → updateCommitIndex → 唤醒 applier
Followers:更新 commitIndex → 唤醒 applier
2.2 关键数据结构
持久化状态(3C 才真正持久化):
rf.term、rf.votedFor、rf.logs易失状态(所有节点):
rf.commitIndex、rf.lastApplied易失状态(Leader 专用):
rf.nextIndex[]、rf.matchIndex[]
日志索引约定:
rf.logs[0]为占位条目(term=0, command=nil) 不仅能够对日志进行更方便的访问,在3D中这里放快照的最后一个日志信息第一条真实日志在 index=1
最后一条 index =
len(rf.logs)-1
2.3 Start(command):
写这一段时建议按(接口语义 + 并发点 + 返回值)组织:
语义:只有
rf.state==Leader才能接收命令;否则立即返回(-1, rf.term, false)写入:追加
LogEntry{Term: rf.term, Command: command}到rf.logs返回:
(index, term, true)触发复制:两种常见方式
- 触发一次广播(推模式),或
- 等下一次心跳(拉模式,简单但可能慢)
func (rf *Raft) Start(command interface{}) (int, int, bool) { // Your code here (3B). rf.mu.Lock() if rf.Killed() { rf.mu.Unlock() return -1, -1, false } if rf.state != Leader { rf.mu.Unlock() return -1, -1, false } term := rf.currentTerm index := rf.getLastIndex() + 1 rf.logs = append(rf.logs, LogEntry{Term: term, Command: command}) rf.persist() rf.mu.Unlock() //Leader 刚追加了一条日志,立刻再推一轮 RPC,不用干等下面 ticker 的 50ms 心跳周期,复制会快一点 go rf.broadcastHeartbeat()//广播心跳 return index, term, true }2.4 AppendEntries(Follower 侧):日志一致性检查与截断追加
这一节建议用“检查顺序”写,跟 Figure 2/论文一致,最好对着一步步走:
term 检查:
args.Term < rf.term直接拒绝,回reply.Term = rf.term退位与重置计时器:收到合法 Leader 的 RPC →
lastHeartBeatTime = now,必要时退回 Follower一致性检查:用
PrevLogIndex/PrevLogTerm验证日志前缀一致
- 本地太短(PrevLogIndex 超界)→Success=false
- term 不匹配 →Success=false
截断并追加:从
PrevLogIndex+1开始,删除本地冲突后缀,再追加Entries推进 commitIndex:
commitIndex = min(args.LeaderCommit, lastNewIndex)
func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) { rf.mu.Lock() defer rf.mu.Unlock() DPrintf("[%d] AppendEntries from L%d term=%d, my term=%d state=%d", rf.me, args.LeaderId, args.Term, rf.currentTerm, rf.state) reply.Success = false reply.Term = rf.currentTerm // 发现任期比自己小 if args.Term < rf.currentTerm { DPrintf("[%d] AppendEntries: 拒绝 term 更小 leader=%d argsTerm=%d myTerm=%d", rf.me, args.LeaderId, args.Term, rf.currentTerm) return } // 发现更高任期或来自合法 Leader 的心跳 rf.lastHeartBeatTime = time.Now() if args.Term > rf.currentTerm { rf.currentTerm = args.Term rf.votedFor = -1 rf.persist() DPrintf("[%d] step down to Follower due to AE term=%d", rf.me, rf.currentTerm) } rf.state = Follower //一致性检查 //日志长度不够 Follower 若还没有 PrevLogIndex 这一条,才拒绝 if rf.getLastIndex() < args.PrevLogIndex { // 3C reply.ConflictTerm = -1 // 3C reply.ConflictIndex = rf.getLastIndex() + 1 // 3C return // 3C } //一致性检查:PrevLogIndex 处的 Term 是否匹配 // 特别注意:如果 PrevLogIndex 恰好在快照边界,要用 LastIncludedIndex if args.PrevLogIndex < rf.LastIncludedIndex { // 3C reply.ConflictTerm = -1 // 3C reply.ConflictIndex = rf.LastIncludedIndex + 1 // 3C return // 3C } //PrevLogIndex处任期不匹配 if rf.getTermByIndex(args.PrevLogIndex) != args.PrevLogTerm { // 3C reply.ConflictTerm = rf.getTermByIndex(args.PrevLogIndex) // 3C idx := args.PrevLogIndex // 3C //从最新的日志位置开始向前找,找到冲突任期的下标 //告诉leader,这个下标是冲突任期的下标,下一步继续找冲突位置,若没有则进行同步 for idx > rf.LastIncludedIndex && rf.getTermByIndex(idx) == reply.ConflictTerm { // 3C idx-- // 3C } reply.ConflictIndex = idx + 1 // 3C return // 3C } //追加日志 isChange := false // 3D for i, entry := range args.Entries { // 3D logicIdx := i + args.PrevLogIndex + 1 // 3D // 如果 logicIdx 已经落入快照范围,跳过(或者报错,理论上不该发生) if logicIdx <= rf.LastIncludedIndex { // 3D continue // 3D } phyIdx := rf.getPhysicIdx(logicIdx) // 3D if phyIdx < len(rf.logs) { // 3D //如果索引范围内已经有日志了,检查任期 if rf.logs[phyIdx].Term != entry.Term { // 3D //如果追加日志的位置的任期和leader日志的位置的任期不相等 //将idx下标前面的日志进行切片保留 rf.logs = rf.logs[:phyIdx] // 3D rf.logs = append(rf.logs, entry) // 3D isChange = true // 3D } //如果任期一样,说明这一段已经同步过了,下一条 } else { // 3D //超出本地的日志长度,直接追加 rf.logs = append(rf.logs, entry) // 3D isChange = true // 3D } } if isChange { // 3D rf.persist() // 3D } // 更新 CommitIndex:须用「当前日志最后一条」与 LeaderCommit 取 min。 // 心跳时 len(Entries)==0,若仍用 PrevLogIndex+0 会小于 getLastIndex(),导致 commit 永远追不上 Leader。 if args.LeaderCommit > rf.commitIndex { rf.commitIndex = min(args.LeaderCommit, rf.getLastIndex()) rf.applyCond.Broadcast() } reply.Success = true }2.5 Leader 侧复制:nextIndex / matchIndex 维护
这一部分主要分为两个小点:
- 发送时构造 args(按 peer 单独算)
-prevIdx := nextIndex[i]-1
-entries := logs[nextIndex[i]:] 将日志切片
- 心跳就是entries为空,但仍携带LeaderCommit
- 收到回复时更新
- 新增关键成员ConflictTerm (冲突任期)/ ConflictIndex(冲突任期索引)
-Success=true:更新matchIndex[i]、nextIndex[i]
-Success=false:回退nextIndex[i]并重试
-优化(性能关键):ConflictTerm / ConflictIndex,让回退“整块跳过”
func (rf *Raft) sendAppendEntries(server int, args *AppendEntriesArgs, reply *AppendEntriesReply) bool { ok := rf.peers[server].Call("Raft.AppendEntries", args, reply) if ok { rf.mu.Lock() defer rf.mu.Unlock() // 处理回复(任期更新、调整 nextIndex 等) if reply.Term > rf.currentTerm { rf.currentTerm = reply.Term rf.state = Follower rf.votedFor = -1 rf.persist() return ok } // 2. 状态检查 if rf.state != Leader || rf.currentTerm != args.Term { return ok } // 3B 以后你会在这里处理日志同步的 reply.Success 为 false 的情况 // 如果追加日志成功 —— 对应论文 Leader 规则:If successful, update matchIndex and nextIndex if reply.Success { newMathIdx := args.PrevLogIndex + len(args.Entries) if newMathIdx > rf.matchIndex[server] { rf.matchIndex[server] = newMathIdx } rf.nextIndex[server] = rf.matchIndex[server] + 1 // 更新提交的日志 rf.updateCommitIndex() } else { // 如果失败,根据 reply.ConflictIndex 实现快速跳转 —— 对应论文:If AppendEntries fails because of log inconsistency, decrement nextIndex and retry if reply.ConflictTerm == -1 { // 日志过短 rf.nextIndex[server] = reply.ConflictIndex } else { flag := false for i := args.PrevLogIndex; i >= rf.LastIncludedIndex; i-- { if rf.getTermByIndex(i) == reply.ConflictTerm { flag = true rf.nextIndex[server] = i + 1 break } } if !flag { rf.nextIndex[server] = reply.ConflictIndex } } } } return ok }2.6 updateCommitIndex:Leader 如何合法推进提交
这节写清楚 Figure 8 约束(非常重要):
- Leader 推进到某个 index
N的条件:
- 多数派matchIndex[i] >= N
- 且logs[N].Term == rf.term(只用当前任期的日志推进提交,间接提交旧任期日志)
推荐这样写:
从后往前找最大的 N(更快)
注意边界:
len(logs)-1和len(logs)// 日志提交应用-leader专属(matchIndex/commitIndex 均为逻辑索引) func (rf *Raft) updateCommitIndex() { if rf.state != Leader { return } for i := rf.getLastIndex(); i > rf.commitIndex; i-- { if rf.getTermByIndex(i) == rf.currentTerm { cnt := 1 for j := range rf.peers { if j != rf.me && rf.matchIndex[j] >= i { cnt++ } } if cnt >= len(rf.peers)/2+1 { rf.commitIndex = i rf.applyCond.Broadcast() break } } else if rf.getTermByIndex(i) < rf.currentTerm { break } } }
2.7 applier:把已提交日志送到 applyChan(不要持锁发 chan)
在这部分中需要特别注意的是:
不要用 sleep 轮询:延迟大、测试反馈慢
用 sync.Cond:commitIndex 增加时
Broadcast唤醒 applier锁内拷贝、锁外发送:避免
applyChan阻塞时卡死整个 Raft
来看这个简短伪代码流程:
lock → 等待
commitIndex > lastApplied(cond.Wait)计算要 apply 的区间,拷贝 entries
unlock → 逐条向
applyChan发送lock → 更新
lastApplied→ unlock
2.8 3B 典型 Bug 记录
我按表格整理了一下常见的坑,我基本都是踩过的:
| 问题分类 | 具体描述 |
|---|---|
| Agreement Failed(commitIndex 没推进 / applier 没被唤醒) | 就是集群明明已经达成一致了,但commitIndex死活不涨,或者涨了却没人去 apply。要么是 Leader 忘了给 Follower 发心跳,要么是 Follower 收到了心跳却没更新自己的commitIndex,再要么是applyLoop没被唤醒,卡在 channel 上没人通知。 |
| too many RPCs(心跳过频,缺少节流) | 每个日志条目都单独发一次 RPC,或者心跳发得太快,把网络打满。正确做法是用lastBroadcastTime做个节流,比如 10ms 内只发一次心跳,没必要每条日志都立刻广播。 |
| 回退太慢(未实现 conflict 优化) | 日志冲突之后,如果只靠nextIndex--一格格往回退,遇到恶意日志会退几百次,导致同步极慢。用论文里的conflict term 和 conflict index做快速回退,一次性跳到冲突点,性能好很多。 |
| 死锁(持锁发 chan / 持锁做重活) | 最常见的是持有锁然后向 applyCh 发消息,如果对端处理慢或者也来拿同一把锁,就死锁了。还有就是持有锁做耗时的磁盘写入,虽然不死锁,但会卡住整个 Raft 状态机。解决方法是先拷贝需要的数据,解锁,再发 chan 或写盘。 |
这些 Bug 在 3B 阶段几乎每个人都会遇到,提前有个印象能省不少调试时间。
三、Part C:持久化(Persistence,3C)
3.1 3C 的核心:持久化“承诺”,不是持久化“结果”
一句话:只要重启可能导致你做出与重启前矛盾的行为,就必须在返回 RPC 之前把状态写盘,其实很简单,只要将需要持久化的内容每次改动的时候进行持久化就可以了。
3.2 persist/readPersist:持久化内容与编码格式
持久化内容(Figure 2):
rf.termrf.votedForrf.logs
编码顺序必须和解码顺序一致,否则 readPersist (会失忆/乱序),这里也是一个坑,但是我这里为了方便怎么存的怎么取,所以把坑跳过去了。
func (rf *Raft) persist() { // Your code here (3C). // Example: // w := new(bytes.Buffer) // e := labgob.NewEncoder(w) // e.Encode(rf.xxx) // e.Encode(rf.yyy) // raftstate := w.Bytes() // rf.persister.Save(raftstate, nil) w := new(bytes.Buffer) e := labgob.NewEncoder(w) e.Encode(rf.currentTerm) e.Encode(rf.votedFor) e.Encode(rf.logs) e.Encode(rf.LastIncludedIndex) e.Encode(rf.LastIncludedTerm) raftstate := w.Bytes() snap := rf.persister.ReadSnapshot()//先读取快照 rf.persister.Save(raftstate, snap)//再持久化状态和快照 } // restore previously persisted state. func (rf *Raft) readPersist(data []byte) { if data == nil || len(data) < 1 { // bootstrap without any state? return } // Your code here (3C). // Example: r := bytes.NewBuffer(data) d := labgob.NewDecoder(r) var term int var vorfor int var logs []LogEntry var lastincludedindex int var lastincludedterm int if d.Decode(&term) != nil || d.Decode(&vorfor) != nil || d.Decode(&logs) != nil || d.Decode(&lastincludedindex) != nil || d.Decode(&lastincludedterm) != nil { DPrintf("readPersist err") } else { rf.currentTerm = term rf.votedFor = vorfor rf.logs = logs rf.LastIncludedIndex = lastincludedindex rf.LastIncludedTerm = lastincludedterm } }3.3 何时调用 persist:时机与原子性
可以按谁改了谁负责 persist写成清单:
startElection:
term++、votedFor=me之后RequestVote:更新
term/votedFor之后(尤其是投票成功路径)AppendEntries:看到更大 term 退位时;日志截断/追加时
Start:追加日志后
关键点:一定要在 handler 返回 reply 之前 persist(否则崩溃重启后可能同任期重复投票,造成脑裂等一系列问题)。
3.4 为什么 3B 正常、3C 就崩
3B 都过了,3C 断电测试出现 Log Inconsistency
根因通常是:readPersist 没写 / 解码顺序错 / logs 没恢复占位条目
commitIndex/lastApplied/nextIndex/matchIndex不需要持久化,它们会在重启后通过 Leader 的 LeaderCommit、以及日志复制流程动态恢复。
3.5 性能与正确性的取舍
persist 频率高会拖慢系统(尤其心跳很频繁时)
心跳频率要“优雅”:足够快维持领导权,但不能淹没网络(too many RPCs)
正确性优先:该 persist 的地方不能省;优化要在正确性上做节流/批量/减少无效 RPC
四、测试和遇到的问题
cd 6.5840/src make RUN="-run 3B" raft1 make RUN="-run 3C" raft1
3B 阶段主要折腾的是 Raft 里两个搬运者:谁在什么时候推进提交、谁把已提交日志交给状态机。我对这两块做了比较彻底的重构。
updateCommitIndex(Leader 侧)
- 问题:一开始有数组越界(
len和len-1混用)、以及用「日志下标」去当matchIndex的下标这类逻辑错误。- 做法:改成从后往前扫合法的提交点 NN,并严格按 Figure 8:Leader 只能用「当前任期里复制到多数派的那条日志」去推进
commitIndex,旧任期的条目只能被顺带提交。- Figure 2 流水线(自测时心里要有这条链)
Start写日志 → Leader 发AppendEntries→ Follower 确认并追加 → Leader 用matchIndex推进commitIndex并唤醒 applier → 后续心跳带上LeaderCommit,Follower 跟进commitIndex并同样唤醒 applier → 双方经applyChan按序应用。
实现中需要注意的点:
- 尽量不要在持锁时写 channel。
- 等 commit 用
sync.Cond,少用忙等 / 固定Sleep。- 心跳:够快以维持领导权,但不要打到 RPC 风暴。
readPersist- 这是 3C 里很隐蔽的一类问题:没实现、或 Decode 顺序和 Encode 对不上,节点等于「失忆」,往往在断电测试里表现为大量 Log Inconsistency,而纯 3B 有时还能蒙混过关。
- 性能和正确性
HeartBeatTimeout、RPC 频率和persist次数会互相拉扯;变了就存最安全,但心跳极密时无脑persist会把测试拖慢。可以在保证该存必存的前提下,适当拉长心跳间隔、减少无意义刷盘(具体以你当时通过测试的配置为准)。- 哪些不用存
commitIndex等易失状态不必持久化;重启后靠 Leader 的 LeaderCommit 和日志一致性检查再对齐即可。其它 3B 测试里碰到的
问题 现象 原因与处理 Agreement 失败
one(100)等闭环缺一环:Leader 收到成功回复没更新提交;或 Follower 更新了
commitIndex却没唤醒 applier。RPC 过多
too many RPCs心跳发得太勤(例如 ticker 里固定短间隔全员 RPC)。需要按间隔节流(如配合选举/心跳超时)。
冲突回退慢
性能测试超时
nextIndex一格一格退。应实现 ConflictTerm / ConflictIndex,让 Leader 整块对齐。死锁
测试 hang
持锁路径里 RPC 回调或 apply 过重。缩小锁粒度,禁止持锁写 channel。
3C:持久化(测试拉长后才会暴露)
何时
persist、保的是什么
我一开始在Start里就落了盘,其它 RPC 路径也零零散散加了一些persist,跑 3C 才发现还有路径没覆盖全。
要点:持久化保的不是业务结果,而是 承诺——凡是崩溃重启后可能做出和崩溃前矛盾的事(例如同一任期投两次票),就要在 对外承诺已经形成之后、RPC 返回之前 把该存的状态写盘。applier(应用侧)
- 问题:早期用
Sleep轮询commitIndex,测试反馈慢;还有在持锁时往applyChan里送消息,容易把整个节点卡死。- 做法:用
sync.Cond,在commitIndex前进时Broadcast;发送 apply 消息时坚持 锁里只拷贝、锁外再写 channel,避免 channel 阻塞拖死持锁路径。
这是最终测试通过的输出
3B
lcz@iv-yef3xahqtc5i3z5jzmr5:~/mit6.5840/6.5840/src$ make RUN="-run 3B" raft1
go build -race -o main/raft1d main/raft1d.go cd raft1 && go test -v -race -run 3B
=== RUN TestBasicAgree3B Test (3B): basic agreement (reliable network)
... ... Passed -- time 0.4s #peers 3 #RPCs 14 #Ops 3
--- PASS: TestBasicAgree3B (0.71s)
=== RUN TestRPCBytes3B Test (3B): RPC byte count (reliable network)
... ... Passed -- time 1.8s #peers 3 #RPCs 58 #Ops 11
--- PASS: TestRPCBytes3B (2.14s)
=== RUN TestFollowerFailure3B Test (3B): test progressive failure of followers (reliable network)...
... Passed -- time 4.3s #peers 3 #RPCs 188 #Ops 3
--- PASS: TestFollowerFailure3B (4.67s)
=== RUN TestLeaderFailure3B Test (3B): test failure of leaders (reliable network)...
... Passed -- time 4.7s #peers 3 #RPCs 294 #Ops 3
--- PASS: TestLeaderFailure3B (5.03s)
=== RUN TestFailAgree3B Test (3B): agreement after follower reconnects (reliable network)...
... Passed -- time 3.9s #peers 3 #RPCs 134 #Ops 7
--- PASS: TestFailAgree3B (4.37s)
=== RUN TestFailNoAgree3B Test (3B): no agreement if too many followers disconnect (reliable network)...
... Passed -- time 3.3s #peers 5 #RPCs 316 #Ops 2
--- PASS: TestFailNoAgree3B (3.81s)
=== RUN TestConcurrentStarts3B Test (3B): concurrent Start()s (reliable network)... ... Passed -- time 0.6s #peers 3 #RPCs 24 #Ops 0
--- PASS: TestConcurrentStarts3B (1.07s)
=== RUN TestRejoin3B Test (3B): rejoin of partitioned leader (reliable network)...
... Passed -- time 5.7s #peers 3 #RPCs 282 #Ops 4
--- PASS: TestRejoin3B (6.05s)
=== RUN TestBackup3B Test (3B): leader backs up quickly over incorrect follower logs (reliable network)...
... Passed -- time 19.1s #peers 5 #RPCs 2568 #Ops 102
--- PASS: TestBackup3B (19.68s)
=== RUN TestCount3B Test (3B): RPC counts aren't too high (reliable network)...
... Passed -- time 2.2s #peers 3 #RPCs 72 #Ops 0
--- PASS: TestCount3B (2.71s) PASS ok 6.5840/raft1 51.273s
3C
lcz@iv-yef3xahqtc5i3z5jzmr5:~/mit6.5840/6.5840/src$ make RUN="-run 3C" raft1
go build -race -o main/raft1d main/raft1d.go
cd raft1 && go test -v -race -run 3C
=== RUN TestPersist13C
Test (3C): basic persistence (reliable network)...
... Passed -- time 3.3s #peers 3 #RPCs 98 #Ops 6
--- PASS: TestPersist13C (3.75s)
=== RUN TestPersist23C
Test (3C): more persistence (reliable network)...
... Passed -- time 13.7s #peers 5 #RPCs 568 #Ops 16
--- PASS: TestPersist23C (14.36s)
=== RUN TestPersist33C
Test (3C): partitioned leader and one follower crash, leader restarts (reliable network)...
... Passed -- time 1.5s #peers 3 #RPCs 48 #Ops 4
--- PASS: TestPersist33C (1.84s)
=== RUN TestFigure83C
Test (3C): Figure 8 (reliable network)...
2026/03/18 22:15:05 6PCdkEFTs2eiMT_RlFB1: dmxsrv.reader: clnt ACu3swj2_8gbs1Wd6nbn ReadCall err read unix /tmp/6.5840-6PCdkEFTs2eiMT_RlFB1->@: read: connection reset by peer
2026/03/18 22:15:47 6PCdkEFTs2eiMT_RlFB1: dmxsrv.reader: clnt 5VPxjteaE_i4P1o9h71T ReadCall err read unix /tmp/6.5840-6PCdkEFTs2eiMT_RlFB1->@: read: connection reset by peer
... Passed -- time 51.8s #peers 5 #RPCs 2369 #Ops 2
--- PASS: TestFigure83C (52.30s)
=== RUN TestUnreliableAgree3C
Test (3C): unreliable agreement (unreliable network)...
... Passed -- time 3.4s #peers 5 #RPCs 220 #Ops 246
--- PASS: TestUnreliableAgree3C (4.02s)
=== RUN TestFigure8Unreliable3C
Test (3C): Figure 8 (unreliable) (unreliable network)...
... Passed -- time 48.0s #peers 5 #RPCs 7496 #Ops 2
2026/03/18 22:16:44 T5AgHTtizWPRzjsYwAQ6: dmxsrv.reader: clnt UkD_e2Q-b5OG3f1PBPSa ReadCall err read unix /tmp/6.5840-T5AgHTtizWPRzjsYwAQ6->@: read: connection reset by peer
--- PASS: TestFigure8Unreliable3C (48.77s)
=== RUN TestReliableChurn3C
Test (3C): churn (reliable network)...
... Passed -- time 16.6s #peers 5 #RPCs 1084 #Ops 1
--- PASS: TestReliableChurn3C (17.17s)
=== RUN TestUnreliableChurn3C
Test (3C): unreliable churn (unreliable network)...
... Passed -- time 16.8s #peers 5 #RPCs 1028 #Ops 1
--- PASS: TestUnreliableChurn3C (17.46s)
PASS
ok 6.5840/raft1 160.685s
五、收获
在看完论文理解之后lab3还是较简单的,但是对于test中其实并不能test出你代码的漏洞,就像我之前ticker设置的检索间隔一样。照样是通过了所有test。但是在B中却因为RPC调用太频繁超时了。我们在代码实现中要使用尽量多的调试信息打印,以便后续调试,希望我的思路可以带给你们思路,然后按照自己的思路实现出属于自己的lab1。最后希望有错误的地方多指正指正~
- 完整实现:mit6.5840: 用来记录mit6.5840(原6.824)的实现历程
- 觉得有收获的可以帮忙点个star~