<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Fu, Wei</title>
        <link>https://fuweid.com/</link>
        <copyright>2026 Wei Fu</copyright>
        <lastBuildDate>Sun, 01 Mar 2026 10:20:45 -0500</lastBuildDate>
        <atom:link href="/" rel="self" type="application/rss+xml" />

        
        <item>
          <title>Debug Log #3: Stale reads caused by process pausing</title>
          <link>/post/2026-debug-03-etcd-stale-readindex/</link>
          <pubDate>Sun, 01 Mar 2026 10:20:45 -0500</pubDate>
          <guid>https://fuweid.com/post/2026-debug-03-etcd-stale-readindex/</guid>
          <description>&lt;p&gt;今天分享的问题是 &lt;a href=&#34;https://github.com/etcd-io/etcd/issues/20418&#34;&gt;Stale reads caused by process pausing&lt;/a&gt;，它由 SIG-ETCD Leader &lt;a href=&#34;https://github.com/serathius&#34;&gt;Marek Siarkowicz&lt;/a&gt; 完成定位并修复。&lt;/p&gt;
&lt;h2 id=&#34;问题介绍&#34;&gt;问题介绍&lt;/h2&gt;
&lt;p&gt;社区每天都会运行 &lt;a href=&#34;https://antithesis.com/&#34;&gt;antithesis&lt;/a&gt; 的故障注入测试。整体流程是：先启动一个三节点的 etcd 集群，再启动多个客户端并发执行读写；在读写过程中，antithesis 平台会随机注入短暂故障。待客户端操作结束后，测试会对结果进行数据一致性校验，其中一项关键检查就是 Linearizability - 线性一致性。线性一致性校验方面，社区使用 &lt;a href=&#34;https://github.com/anishathalye/porcupine&#34;&gt;porcupine&lt;/a&gt; 的可视化方法：客户端会记录每一次请求的返回结果，以及请求发出与响应返回的时序，呈现出来的效果如下。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2026-debug-03-etcd-stale-readindex/porcupine-1.png&#34; alt=&#34;porcupine-1&#34;&gt;&lt;/p&gt;
&lt;p&gt;测试会随机暂停某个 etcd 成员进程 2-3 秒（例如通过 cgroup freeze，或对进程发送 SIGSTOP）。问题在于：当原 Leader 暂停恢复后、但尚未意识到已选出新 Leader 之前，它仍可继续处理线性一致性读请求。如上图所示，某客户端已经成功将值更新为 &lt;code&gt;key6=4&lt;/code&gt;，并观察到集群的 Revision 为 &lt;code&gt;4&lt;/code&gt;；但随后另一个客户端发起读请求时，却读到了 Revision 为 &lt;code&gt;3&lt;/code&gt; 的旧数据。这就出现了数据不一致的问题。这种进程停顿在实际场景中还挺常见的，比如底层物理机出现问题需要迁移虚拟机，这种迁移可能会导致短暂的停顿。&lt;/p&gt;
&lt;h2 id=&#34;readindex&#34;&gt;ReadIndex&lt;/h2&gt;
&lt;p&gt;为了实现线性一致性的读操作，etcd 采用 ReadIndex 机制 ( Raft - 6.4 Processing read-only queries more efficiently )：当 Leader 收到线性一致读请求时，它不会直接用本地状态返回，而是先通过一次广播心跳来确认自己仍然是 Leader，从而避免在发生 Leader 变更或网络故障时读到过期数据。&lt;/p&gt;
&lt;p&gt;Leader 发起一轮 ReadIndex 流程，向其他节点发送心跳（本质是空的 AppendEntries）。这些心跳会携带一个 Request-ID 作为上下文，用来标识这次 ReadIndex 请求。Follower 不需要解析这个上下文，只需按 Raft 协议正常响应心跳即可。Leader 收到多数派的确认后，就能确定一个安全的 readIndex（对应当时已经被多数派确认的提交进度）。&lt;/p&gt;
&lt;p&gt;etcd Leader 在管理 ReadIndex 这件事时，思路其实很直接: 用 Request-ID 作为这次请求的唯一标识，然后按照请求进入流程的先后顺序排队，也就是入队 &lt;a href=&#34;https://github.com/etcd-io/raft/blob/4f13735b20be5421ee0ff7dde7cb8b8330880576/read_only.go#L52C1-L63C2&#34;&gt;readIndexQueue&lt;/a&gt;。等收到 Follower 回来的心跳响应之后，如果心跳上下文里的 Request-ID 已经拿到了多数派确认，那有意思的地方就来了: etcd Leader 并不是「哪个请求确认了，就只处理哪个请求」。它 &lt;a href=&#34;https://github.com/etcd-io/raft/blob/4f13735b20be5421ee0ff7dde7cb8b8330880576/read_only.go#L81&#34;&gt;advance&lt;/a&gt; 函数会从队头开始扫描 readIndexQueue，一直处理到这个 Request-ID 为止。也就是说，&lt;strong&gt;一旦某个请求得到确认，那排在它前面的那些请求，它都会可以顺带确认了&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;每个 etcd 成员都有一个名为 &lt;a href=&#34;https://github.com/etcd-io/etcd/blob/95e304269f9bfa466930fbd96c16cf8f04ea5e41/server/etcdserver/v3_server.go#L805&#34;&gt;linearizableReadLoop&lt;/a&gt; 的守护协程，用来发起 ReadIndex 请求，从而确认当前最新、已达成共识的 CommitIndex。每次请求的超时大约是 7 秒。除非等待超时，或节点检测到 Leader 发生变化，否则它不会放弃这次请求，而是每隔 500 毫秒使用 &lt;strong&gt;同一个 request ID&lt;/strong&gt; 继续重试。&lt;/p&gt;
&lt;h2 id=&#34;迟到的双心跳反馈&#34;&gt;迟到的双心跳反馈&lt;/h2&gt;
&lt;p&gt;假设三个成员分别是 etcd0、etcd1、etcd2，当前 Leader 是 &lt;code&gt;etcd0(term=2)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;在停止 etcd0 进程之前，etcd1 收到来自 client1 的线性读请求，于是向 Leader etcd0 发送 &lt;code&gt;ReadIndex(reqID=10)&lt;/code&gt;。同时，etcd0 对外发送心跳 &lt;code&gt;Heartbeat(term=2, context=[reqID=10])&lt;/code&gt;，紧接着 etcd0 被暂停。&lt;/p&gt;
&lt;p&gt;etcd1/etcd2 收到心跳后，分别返回 &lt;code&gt;HeartbeatResp(term=2, context=[reqID=10])&lt;/code&gt;。但 etcd1 一直没有等到 etcd0 返回的 &lt;code&gt;MsgReadIndexResp&lt;/code&gt;，因此它会继续重试：不断发送同一个 &lt;code&gt;ReadIndex(reqID=10)&lt;/code&gt;，直到它发现集群已经出现新 Leader &lt;code&gt;etcd2(term=3)&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;由于数据量不大、机器也没死，&lt;strong&gt;暂停期间内核仍在收包&lt;/strong&gt;；再加上暂停时间并不长，etcd0 恢复后仍能收到暂停期间积压的消息：&lt;code&gt;HeartbeatResp(term=2, context=[reqID=10], from=etcd1)&lt;/code&gt;、后续重试的 &lt;code&gt;ReadIndex(reqID=10)&lt;/code&gt;，以及 &lt;code&gt;HeartbeatResp(term=2, context=[reqID=10], from=etcd2)&lt;/code&gt;。可以确定的是：etcd1 的重试 &lt;code&gt;ReadIndex(reqID=10)&lt;/code&gt; 一定发生在它发送 &lt;code&gt;HeartbeatResp(term=2, context=[reqID=10], from=etcd1)&lt;/code&gt; 之后；但 etcd0 恢复后，协程调度时间不确定，所以不确定 &lt;code&gt;HeartbeatResp(term=2, context=[reqID=10], from=etcd2)&lt;/code&gt; 和 &lt;code&gt;ReadIndex(reqID=10)&lt;/code&gt; 的顺序。&lt;/p&gt;
&lt;p&gt;假设 etcd0 恢复后，又收到了 client2 一个直接发给它自己的线性读请求，那么它会产生新的 &lt;code&gt;ReadIndex(reqID=20)&lt;/code&gt;，并向其它两个成员发起新一轮心跳来等待多数派的确认。由于 etcd0 和其它成员之间的连接并没有断开，新一轮心跳的回包在「发生时间」上肯定晚于上一轮 term=2 的回包。如果此时 readIndexQueue 的顺序刚好变成 &lt;code&gt;[reqID=20, reqID=10]&lt;/code&gt;，那么当 etcd0 处理到 &lt;code&gt;HeartbeatResp(term=2, context=[reqID=10], from=etcd2)&lt;/code&gt; 时，就可能错误地确认队头的 &lt;code&gt;reqID=20&lt;/code&gt;。于是 client2 会直接在 etcd0 本地完成线性读；一旦新 Leader &lt;code&gt;etcd2(term=3)&lt;/code&gt; 产生新的数据变化，那么 client2 就会读到旧 Revision，从而形成 stale read。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;时间&lt;/th&gt;
&lt;th&gt;发送方 → 接收方&lt;/th&gt;
&lt;th&gt;消息 / 动作&lt;/th&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;备注&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;T0&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;etcd0 是 Leader&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;初始状态&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T1&lt;/td&gt;
&lt;td&gt;client1 → etcd1&lt;/td&gt;
&lt;td&gt;线性读触发&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T2&lt;/td&gt;
&lt;td&gt;etcd1 → etcd0&lt;/td&gt;
&lt;td&gt;ReadIndex(reqID=10)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T3&lt;/td&gt;
&lt;td&gt;etcd0 → etcd1/etcd2&lt;/td&gt;
&lt;td&gt;Heartbeat(context=[reqID=10])&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T4&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;etcd0 被暂停 (SIGSTOP / freezer)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;用户态不处理消息，但内核仍在收包&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T5&lt;/td&gt;
&lt;td&gt;etcd1/etcd2 → etcd0&lt;/td&gt;
&lt;td&gt;HeartbeatResp(context=[reqID=10])&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T6&lt;/td&gt;
&lt;td&gt;etcd1 → etcd0&lt;/td&gt;
&lt;td&gt;ReadIndex(reqID=10)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;重试&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T7&lt;/td&gt;
&lt;td&gt;etcd2 成为新 Leader&lt;/td&gt;
&lt;td&gt;Leader 切换&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T8&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;etcd0 恢复&lt;/td&gt;
&lt;td&gt;2/3&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T9&lt;/td&gt;
&lt;td&gt;client2 → etcd0&lt;/td&gt;
&lt;td&gt;线性读触发&lt;/td&gt;
&lt;td&gt;2/3&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T10&lt;/td&gt;
&lt;td&gt;etcd0 → etcd1/etcd2&lt;/td&gt;
&lt;td&gt;Heartbeat(context=[reqID=20])&lt;/td&gt;
&lt;td&gt;2/3&lt;/td&gt;
&lt;td&gt;为 reqID=20 等待多数派确认&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T11&lt;/td&gt;
&lt;td&gt;etcd0&lt;/td&gt;
&lt;td&gt;处理 Heartbeat(context=[reqID=10],from=etcd1)&lt;/td&gt;
&lt;td&gt;2/3&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T12&lt;/td&gt;
&lt;td&gt;etcd0&lt;/td&gt;
&lt;td&gt;处理重复的 ReadIndex(reqID=10,from=etcd1)&lt;/td&gt;
&lt;td&gt;2/3&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T13&lt;/td&gt;
&lt;td&gt;etcd0&lt;/td&gt;
&lt;td&gt;处理迟到的 Heartbeat(context=[reqID=10],from=etcd2)&lt;/td&gt;
&lt;td&gt;2/3&lt;/td&gt;
&lt;td&gt;确认 T10 - ReadIndex(reqID=20) 的请求&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T14&lt;/td&gt;
&lt;td&gt;etcd0&lt;/td&gt;
&lt;td&gt;client2 读本地状态机&lt;/td&gt;
&lt;td&gt;2/3&lt;/td&gt;
&lt;td&gt;若 etcd2(term3) 有新的数据变化 → stale read&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;解决的方案就是避免在 linearizableReadLoop 守护协程里使用重复的 Request-ID。&lt;/p&gt;
&lt;h2 id=&#34;最后&#34;&gt;最后&lt;/h2&gt;
&lt;p&gt;为了避免先入为主，在查看修复代码之前，我会先过一遍测试日志和关键路径源码，先把可能原因收敛到一个大致范围，再回头对照修复方案。&lt;/p&gt;
&lt;p&gt;这次排查给我带来灵感的是十年前的一条审阅评论——&lt;a href=&#34;https://github.com/etcd-io/etcd/pull/6275#discussion_r78322406&#34;&gt;raft: support safe readonly request&lt;/a&gt;。虽然和这次问题不算直接相关，但这确实是调试的乐趣。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2026-debug-03-etcd-stale-readindex/review-comment.png&#34; alt=&#34;review-comment&#34;&gt;&lt;/p&gt;
&lt;p&gt;最后我在本地用 SIGSTOP 跑了几个小时就把问题复现出来了。这次没有用 AI，不过后续可以整理一个 SKILL，用来更系统地分析这类测试日志。&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>弗吉尼亚, 2025</title>
          <link>/post/2026-my-2025-in-va/</link>
          <pubDate>Sun, 15 Feb 2026 23:28:45 -0500</pubDate>
          <guid>https://fuweid.com/post/2026-my-2025-in-va/</guid>
          <description>&lt;p&gt;弗吉尼亚州北部的小城市待了一年。来这前，我对这个州没有什么概念，唯一的印象就是新闻里，某云服务商又双叒叕在北弗吉尼亚地区出故障了。&lt;/p&gt;
&lt;p&gt;北弗吉尼亚的气候和北京差不多，但「妖风」少了些，冬天雨水充足，反倒不干燥。虽然中餐馆屈指可数，但还是有不错的中超，蔬菜调料基本能满足，照着某书的菜谱动手，还是能满足下自己的中国胃。当地超市冰柜里的预制菜不少，仅有限的观察来看，年轻人是消费主力 - 买完回家，「叮」一下就开吃，然后遛狗。这和北京一样的美食荒漠，所以 In-N-Out 什么时候开到这边啊？&lt;/p&gt;
&lt;p&gt;在国内，几乎所有事情都能在手机端解决。但在这边，你还是得拿起电话去争取自己的权益，还要接受接通前长达一小时的音乐 - Patience is the key。&lt;/p&gt;
&lt;p&gt;美国快递物流没有想象中的差，亚马逊的 Prime 会员几乎能做到「次日达」。但和国内最大的反差是，各个系统的消息推送却是靠实体信件。个人信息泄露比国内更离谱，经常是「你没注意的条款已经替你同意了」，广告保险推销会充斥你的信件箱。即便如此，下楼开信箱依然是我每天的乐趣。&lt;/p&gt;
&lt;p&gt;我在远程工作，所以除了去超市买菜之外，平时见到的活人都是在山里 - 好山好水好「无聊」。相较于北京，拿到驾照就可以买车上路了。没有电动车和行人频繁乱穿，对新手司机相对友好。两三小时车程内的徒步路线我大多都走过了。最喜欢的两条线路: 秋天的马里兰州&lt;strong&gt;安纳波利斯岩&lt;/strong&gt; 以及春天前的&lt;strong&gt;仙纳度河州立公园&lt;/strong&gt;。没有西部丰富地貌的震撼，全靠四季交替带来的视觉冲击。对，我说的是北京的百望山公园。&lt;/p&gt;
&lt;p&gt;除去文化环境冲击外，AI 的冲击更是无法避免。这段时间，我通过强制让 gpt-5.2-codex 做本地代码检索再做分析，它的代码审阅结果很具有指导价值。再结合最小复现模型去验证思路，很多想法几分钟就能跑通。这种效率，确实会让人短暂地产生「10x 工程师」的错觉。但它和接口报错不同：接口报错通常是显性的，而它的错误往往是「看起来好像挺合理」。它的门槛不在如何调用，而是在判断。&lt;/p&gt;
&lt;p&gt;最近看到开源社区里关于「是否接受 AI 代码」的讨论越来越多。但本质上，社区协作仍然是人与人之间的交互。考虑到代码版权与责任边界，即便开发过程用了 AI 辅助，最终提交、签字画押的还是人。其实这个问题很简单，什么时候 AI 能背锅了，什么时候这个话题就停止了。&lt;/p&gt;
&lt;p&gt;我还是挺享受在下雨天和朋友一起打暗黑的。&lt;/p&gt;
&lt;p&gt;100 天&lt;/p&gt;
&lt;p&gt;2025 -&amp;gt; 2026。&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>Debug Log #2: Using AI to Fix a Flaky Containerd ENXIO Test Case</title>
          <link>/post/2025-debug-02-ai-loopback-enxio-flaky-testcase/</link>
          <pubDate>Tue, 25 Nov 2025 21:20:45 -0500</pubDate>
          <guid>https://fuweid.com/post/2025-debug-02-ai-loopback-enxio-flaky-testcase/</guid>
          <description>&lt;h2 id=&#34;enxio-no-such-device-or-address&#34;&gt;ENXIO: no such device or address&lt;/h2&gt;
&lt;p&gt;在 containerd v2.2 发布前期，我们多次遇到与 &lt;a href=&#34;https://man7.org/linux/man-pages/man4/loop.4.html&#34;&gt;loop(4)&lt;/a&gt; 块设备相关的测试用例 TestLoopbackMount 出现不稳定的问题，但在本地环境却始终无法复现。我在审阅代码时碰到这种失败的频率并不高，重试一下通常就能通过，所以当时并没有太在意。直到最近我连续点了好几次重试之后，我真的不想再当 S(oftware) R(etry) E(ngineer) 了。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;o&#34;&gt;===&lt;/span&gt; FAIL: core/mount/manager TestLoopbackMount &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;0.05s&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;
    log_hook.go:47: &lt;span class=&#34;nv&#34;&gt;time&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;2025-10-23T21:49:22.532811960Z&amp;#34;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;level&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;debug &lt;span class=&#34;nv&#34;&gt;msg&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;activating mount&amp;#34;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;manager.(*mountManager).Activate&amp;#34;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;file&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;/home/runner/work/containerd/containerd/core/mount/manager/manager.go:134&amp;#34;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;mounts&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;[{loop /tmp/TestLoopbackMount989607109/001/fs-1621892597  []} {format/ext4 {{ mount 0 }}  []}]&amp;#34;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;id1 &lt;span class=&#34;nv&#34;&gt;testcase&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;TestLoopbackMount
    helpers.go:100: unmount /tmp/TestLoopbackMount989607109/001/test-mount-3030342351
    manager_linux_test.go:80:
        	Error Trace:	/home/runner/work/containerd/containerd/core/mount/manager/manager_linux_test.go:80
        	            				/home/runner/work/containerd/containerd/core/mount/manager/manager_linux_test.go:105
        	Error:      	Received unexpected error:
        	            	failed to get loop device info: no such device or address
        	Test:       	TestLoopbackMount
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;testloopbackmount&#34;&gt;TestLoopbackMount&lt;/h2&gt;
&lt;p&gt;在真实的生产环境里，基本不会有人用 loop 设备当存储后端。不过在测试环境就不一样了，总不能为了跑几条测试就给 CI runner 挂一块 NVMe 盘吧。containerd 在这里其实只负责 snapshot 的生命周期管理，真正的差异主要来自不同文件系统的实现。所以，用 loop 设备来跑测试已经足够覆盖这部分逻辑，也是最简单直接的方案。&lt;/p&gt;
&lt;p&gt;containerd v2.2 将 loop 设备挂载抽象成 mount manager 的插件，挂载流程相对简单：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;步骤&lt;/th&gt;
&lt;th&gt;操作&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;&lt;a href=&#34;https://man7.org/linux/man-pages/man2/ioctl.2.html&#34;&gt;ioctl(2)&lt;/a&gt; + &lt;code&gt;LOOP_CTL_GET_FREE&lt;/code&gt; 获取 &lt;code&gt;/dev/loopX&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;找到一个可用的 loop 设备&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;&lt;code&gt;ioctl&lt;/code&gt; + &lt;code&gt;LOOP_CONFIGURE(后端文件 + LO_FLAGS_AUTOCLEAR)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;绑定文件并设置自动清理标志&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;创建 &lt;code&gt;/dev/loopX&lt;/code&gt; 的软链接&lt;/td&gt;
&lt;td&gt;方便管理，重启后仍能通过软链接找到设备&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;取消 &lt;code&gt;LO_FLAGS_AUTOCLEAR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;允许 containerd 重启或其他进程重新挂载该设备&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;挂载&lt;/td&gt;
&lt;td&gt;将 loop 设备挂载到目标路径&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;取消挂载&lt;/td&gt;
&lt;td&gt;卸载 loop 设备&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;打开 &lt;code&gt;/dev/loopX&lt;/code&gt; 并重新设置 &lt;code&gt;LO_FLAGS_AUTOCLEAR&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;确保设备在关闭句柄后自动释放&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;关闭 &lt;code&gt;/dev/loopX&lt;/code&gt; 句柄&lt;/td&gt;
&lt;td&gt;完成释放流程&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;第 3、4 步主要用于简化管理，确保 containerd 重启后，loop 设备的后端文件不会被意外清空，同时通过软链接方便找到设备号。&lt;/p&gt;
&lt;p&gt;即使多个进程同时通过 &lt;code&gt;LOOP_CTL_GET_FREE&lt;/code&gt; 获取相同的可用设备号，内核在绑定后端文件时会加锁，确保只有一个进程成功绑定。其他进程会收到 &lt;code&gt;EBUSY&lt;/code&gt; 错误，它们只需重新获取设备号即可。重试逻辑和现有的 &lt;a href=&#34;https://man7.org/linux/man-pages/man8/losetup.8.html&#34;&gt;losetup(8)&lt;/a&gt; 保持一致。&lt;/p&gt;
&lt;p&gt;但是在第 7 步，TestLoopbackMount 出现了错误。当它尝试获取设备配置信息时，内核返回了 ENXIO。这个错误表示该设备没有绑定任何后端，因此无法获取状态。&lt;/p&gt;
&lt;p&gt;在确认内核锁能够保证不会出现并发问题后，可以断定，错误原因是其他进程调用了 ioctl(LOOP_CLR_FD) 来清理了「错误」设备号。&lt;/p&gt;
&lt;h2 id=&#34;using-ai&#34;&gt;Using AI?&lt;/h2&gt;
&lt;p&gt;最直接的方法就是启动一个 Vagrant Box，运行和 CI 环境一模一样的内核版本，然后不停地跑测试；同时写一个小的 eBPF 程序来监控所有的 ioctl 系统调用。只要能复现问题，就可以通过 eBPF 的输出追踪到到底是谁在操作设备。不过准备环境需要做的事情不少：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;下载 Ubuntu 22.04 镜像，安装对应的 Azure Ubuntu 版本，以及 llvm、libbpf 等依赖工具，光这些就至少要 20 分钟；&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;让 AI 写一个监控 ioctl 的 eBPF 程序，简单调试保证没有问题，也至少需要 5-10 分钟。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;算下来，整个过程至少也得半小时以上。上周日我在家看 PGL Wallachia S6 - Dota 2 决赛，MOUZ 对雪碧战队，比赛挺精彩的，不想错过 BP 时间，所以就放弃了这个方案。于是，在中间休息的时候，我问了问 AI，看看是不是内部还有其他测试用例在干扰。&lt;/p&gt;
&lt;h2 id=&#34;好像对了&#34;&gt;好像对了？&lt;/h2&gt;
&lt;p&gt;根据我过往的经验，如果当甩手掌柜，直接让 AI 去搞定环境跑这种级别的测试，大概率它会迷失在各种奇葩的错误里。加上我也不确定是不是有其他的测试干扰了，我就只是让 Claude Code 去根据我的问题来生成 Plan。&lt;/p&gt;
&lt;p&gt;没有意外，第一次的答复永远都是让我改现有逻辑来容忍这个错误。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2025-debug-02-AI-loopback-enxio-flaky-testcase/suggestion-1.png&#34; alt=&#34;suggestion&#34;&gt;&lt;/p&gt;
&lt;p&gt;我通过两次纠正，并把 loop(4) 的内核文档链接发给它后，Claude Code 居然帮我找到了那个干扰项 —— TestAutoclearTrueLoop。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2025-debug-02-AI-loopback-enxio-flaky-testcase/race-condition.png&#34; alt=&#34;race-condition&#34;&gt;&lt;/p&gt;
&lt;p&gt;在 TestAutoclearTrueLoop 测试里，只要把设备句柄关闭，内核就会自动解除设备背后的后端文件，不需要手动调用 LOOP_CLR_FD。由于多个进程同时申请设备，假设当 TestAutoclearTrueLoop 通过关闭句柄释放了设备 X 后，其他进程就可以获取到这个设备 X。如果此时 TestAutoclearTrueLoop 再调用 LOOP_CLR_FD 去释放这个设备，就可能会影响到其他进程的操作。&lt;/p&gt;
&lt;p&gt;不过，Claude Code 的分析有点离谱。我倒是挺好奇，它到底是以什么概率「选出」这段既胡扯又好像有点道理的分析的。&lt;/p&gt;
&lt;p&gt;最终花费了我 5 分钟 - 3.4k tokens，还不错，没有影响看比赛。&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>Previewing new mount manager in containerd v2.2.0</title>
          <link>/post/2025-containerd-220-mount-manager/</link>
          <pubDate>Sun, 02 Nov 2025 10:20:45 -0500</pubDate>
          <guid>https://fuweid.com/post/2025-containerd-220-mount-manager/</guid>
          <description>&lt;p&gt;延续上一篇&lt;a href=&#34;https://fuweid.com/post/2025-containerd-220-rebase-snapshot/&#34;&gt;文章&lt;/a&gt;，在 v2.2.0 版本中最关键的更新是 Mount Manager 的引入。该功能主要用于简化现有 snapshotter 的管理方式，并为后续用户自定义的存储方案提供更灵活的集成能力。在深入介绍之前，我们先回顾一下 snapshotter 在演进过程中存在的问题。&lt;/p&gt;
&lt;h2 id=&#34;mount-by-snapshotter-&#34;&gt;Mount by Snapshotter ?&lt;/h2&gt;
&lt;p&gt;最初，snapshotter 的接口设计仅负责管理 snapshot 的生命周期；至于 snapshot 的挂载（mount）动作，则由上层组件自行处理，例如镜像解压插件（diff）或 containerd-shim。该设计的前提是假设 snapshotter 的存储行为足够简单，挂载逻辑完全可以依赖底层文件系统的能力，例如 OverlayFS 的 union mounting 特性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2025-containerd-220-mount-manager/2017-snapshot-api.png&#34; alt=&#34;2017-snapshot-api&#34;&gt;&lt;/p&gt;
&lt;center&gt;From &lt;a href=&#34;https://youtu.be/UUDDCetB7_A?si=RmpE2Gy_PEW76JKJ&#34; target=&#34;_blank&#34;&gt;2017: containerd deep dive presentation at the containerd summit&lt;/a&gt;&lt;/center&gt;
&lt;p&gt;然而，这一设计基于的前提过于理想化，忽略了更复杂的使用场景，例如文件系统镜像的初始化、底层块设备的动态添加或移除，以及多阶段的挂载流程。真正暴露设计局限的是延迟加载（lazy loading）技术的引入，其次是安全容器（VM-like container）对挂载的需求。&lt;/p&gt;
&lt;h3 id=&#34;v140-skip-the-prepare-phase-with-a-two-stage-commit2&#34;&gt;&lt;a href=&#34;https://github.com/containerd/containerd/pull/3793&#34;&gt;[v1.4.0] Skip the Prepare phase with a two-stage commit&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;延迟加载镜像不需要在本地下载完整的镜像层数据，通常只依赖索引信息即可，例如 &lt;a href=&#34;https://github.com/dragonflyoss/nydus/blob/master/docs/nydus-image.md&#34;&gt;nydus&lt;/a&gt;、&lt;a href=&#34;https://github.com/containerd/overlaybd&#34;&gt;overlayBD&lt;/a&gt; 或 &lt;a href=&#34;https://github.com/containerd/stargz-snapshotter&#34;&gt;stargz&lt;/a&gt;。为了让 containerd 在延迟加载场景中避免下载原始镜像层数据，snapshotter 采用了&lt;a href=&#34;https://github.com/containerd/containerd/blob/main/docs/remote-snapshotter.md#snapshotter-apis-for-querying-remote-snapshots&#34;&gt;两项约定&lt;/a&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;所有以 &lt;code&gt;containerd.io/snapshot/&lt;/code&gt; 开头的用户自定义标签会被透传到 &lt;code&gt;snapshotter.Prepare&lt;/code&gt; 接口。snapshotter 可借助这些标签获取镜像相关信息（具体取决于各自的存储方案）。这些信息可以在镜像构建阶段就写入 OCI 镜像的元数据中，containerd 会自动识别并传递。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在 Prepare 阶段，containerd 会传递标签 &lt;code&gt;containerd.io/snapshot.ref=chainID&lt;/code&gt;。如果自定义 snapshotter 使用延迟加载技术，它可以利用这些标签获取镜像索引数据并完成初始化。此时，snapshotter 应在 Prepare 阶段直接将 snapshot 提交为 Committed 状态，并返回 &lt;code&gt;ErrAlreadyExists&lt;/code&gt;，让 containerd 认为该层已准备就绪，然后继续处理下一个镜像层。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;在第二个约定中，自定义 snapshotter 可能会发起 HTTP 或 RPC 请求，从而导致 Prepare 调用耗时较长。而 containerd 底层的元数据存储（metadata plugin）依赖 &lt;a href=&#34;https://github.com/etcd-io/bbolt&#34;&gt;boltdb&lt;/a&gt;，这是一种仅支持单写操作的嵌入式数据库。由于 snapshot 元数据同时由元数据存储和 snapshotter 的双重 boltdb 管理，为避免单个大锁阻塞其他操作，containerd 将 Prepare 阶段的数据提交设计为两步式流程。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NOTE: 使用双 boltdb 管理的一个好处是，GC 扫描可以以异步方式清理 snapshot，从而避免阻塞其他操作。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;与 OverlayFS snapshotter 不同，延迟加载类存储必须在 Prepare 阶段提前完成容器根目录挂载所需的初始化。例如，stargz-snapshotter 会在这一阶段启动 FUSE 挂载，而 overlayBD 则通过 &lt;a href=&#34;https://www.kernel.org/doc/Documentation/target/tcmu-design.txt&#34;&gt;TCMU&lt;/a&gt; 初始化 iSCSI 块设备。由于 containerd 在容器启动前只会调用一次 Prepare 来创建可写层，snapshotter 只能利用这个唯一窗口完成初始化 —— 这实际上形成了文档未明确说明的 &lt;strong&gt;「隐含的第三个约定」&lt;/strong&gt; 。&lt;/p&gt;
&lt;p&gt;但随之出现了新的问题：这些初始化过程中创建的挂载点或设备，应在何时安全清理？&lt;/p&gt;
&lt;p&gt;容器根目录的最终挂载和清理都由 containerd-shim 完成，而 snapshotter 仅会在 snapshot 被删除时才执行清理。这意味着挂载点或设备可能在容器退出后仍被保留，从而导致清理滞后，甚至引起额外的系统资源占用。&lt;/p&gt;
&lt;h3 id=&#34;2022-add-new-extraoptions-field&#34;&gt;[2022] Add new ExtraOptions field?&lt;/h3&gt;
&lt;p&gt;同样，安全容器在启动前也需要执行额外的初始化步骤，仅依靠现有的 snapshotter 接口无法优雅地实现优化。曾经，Kata Containers 社区提出通过 &lt;a href=&#34;https://github.com/containerd/containerd/pull/6746&#34;&gt;ExtraOptions&lt;/a&gt; 将配置信息传递给 Kata-container-shim，但最终被否决。原因很明确：在容器启动前，不仅 containerd-shim 会挂载容器根目录，CRI 插件也需要挂载它以获取进程的 UID/GID。一旦 containerd 无法识别或理解 ExtraOptions，这套方案就无法成立。此外，ExtraOptions 本身也无法表达复杂的初始化逻辑，因此无法满足需求。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NOTE: ExtraOptions 需求来源于机密计算 - &lt;a href=&#34;https://github.com/confidential-containers/confidential-containers/issues/137&#34;&gt;https://github.com/confidential-containers/confidential-containers/issues/137&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;2023-mounthelper-binary&#34;&gt;[2023] mount.helper binary?&lt;/h3&gt;
&lt;p&gt;在 Darwin 支持开发阶段，Akihiro Suda 曾提出利用 &lt;a href=&#34;https://github.com/containerd/containerd/pull/8789#issuecomment-1687818124&#34;&gt;mount.helper in &amp;ndash; Add support for bind-mounts on Darwin (a.k.a. &amp;ldquo;make native snapshotter work&amp;rdquo;)&lt;/a&gt; 扩展挂载方式，类似 &lt;a href=&#34;https://github.com/containerd/containerd/pull/3765&#34;&gt;mount.fuse&lt;/a&gt;，用于满足第三方挂载需求。虽然该方案解决了「如何执行挂载」的问题，但 mount 本身只是一个代码库而非插件服务，并且没有处理挂载点生命周期管理，因此 snapshotter 的核心问题仍然没有解决。&lt;/p&gt;
&lt;h3 id=&#34;v210-erofs-snapshotter&#34;&gt;[v2.1.0] EROFS snapshotter&lt;/h3&gt;
&lt;p&gt;直到上一个版本，新的 &lt;a href=&#34;https://docs.kernel.org/filesystems/erofs.html&#34;&gt;EROFS&lt;/a&gt; 存储支持还是在 Prepare 阶段挂载 loopback 设备。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2025-containerd-220-mount-manager/v2.1.0-erofs.png&#34; alt=&#34;v2.1.4-erofs&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;mount-manager13&#34;&gt;&lt;a href=&#34;https://github.com/containerd/containerd/issues/11303&#34;&gt;Mount Manager&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Derek 在今年年初提出了 Mount Manager 插件。其核心理念是将挂载初始化操作从 snapshotter 中解耦。通过这一插件机制，挂载点的初始化由 &lt;code&gt;Activate&lt;/code&gt; 操作负责完成，而不再依赖 snapshotter 的 &lt;code&gt;Prepare&lt;/code&gt; 阶段。同时，Mount Manager 可以将挂载点的生命周期与其他资源建立引用关系。例如，将挂载点与容器生命周期绑定：当容器退出并被删除时，containerd 的 GC 模块会沿着引用关系自动清理不再需要的挂载点。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Manager&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;interface&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
	&lt;span class=&#34;nf&#34;&gt;Activate&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Mount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ActivateOpt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ActivationInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
	&lt;span class=&#34;nf&#34;&gt;Deactivate&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;error&lt;/span&gt;
	&lt;span class=&#34;nf&#34;&gt;Info&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ActivationInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
	&lt;span class=&#34;nf&#34;&gt;Update&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;ActivationInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ActivationInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
	&lt;span class=&#34;nf&#34;&gt;List&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;([]&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ActivationInfo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;error&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;NOTE: containerd GC 设计可查看 - &lt;a href=&#34;https://fuweid.com/post/2023-containerd-17-gc/&#34;&gt;https://fuweid.com/post/2023-containerd-17-gc/&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;Activate&lt;/code&gt; 会将 snapshotter 管理的挂载信息转换为可以直接用于系统调用的挂载描述。为了支持更灵活的扩展能力，Mount Manager 引入了 Mount Handler 和 Transformer 插件机制。原则上，自定义存储方案只需定义不同的 mount.Type，便可实现独立的挂载初始化逻辑。&lt;/p&gt;
&lt;p&gt;举个例子，如下图所示。 &lt;code&gt;Activate&lt;/code&gt; 会完成以下三步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;初始化一个 500 MiB XFS 镜像。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;创建一个 loopback 设备，将其绑定到第一步创建的镜像上，并完成挂载。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在第二步挂载的基础上，创建 upper 和 worker 目录，并返回一个 OverlayFS 挂载信息。&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2025-containerd-220-mount-manager/mount-example.png&#34; alt=&#34;mount-example&#34;&gt;&lt;/p&gt;
&lt;p&gt;内置的 Mount Handler 和 Transformer 通过 Go Template 实现链式初始化过程中信息的共享。引入的概念较多，有兴趣的朋友可以参考文档：&lt;a href=&#34;https://github.com/containerd/containerd/blob/v2.2.0-rc.0/docs/mounts.md#mount-management&#34;&gt;mounts.md&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id=&#34;new-intergration--v230&#34;&gt;New intergration &amp;gt;= v2.3.0&lt;/h2&gt;
&lt;p&gt;v2.2.0 版本仅引入了 Mount Manager，目前只有 EROFS snapshotter 集成了这一方案（它现在不再在 snapshotter 内部处理任何挂载操作）。此外，Mount Manager 还未开放 Mount Handler 插件 (proxy) 接口，第三方组件的调用方式计划在下一版本中引入。&lt;/p&gt;
&lt;p&gt;可以预见，未来 snapshotter 将更多聚焦于元数据管理，逐步回归其最初的设计定位。而 Mount Manager 则可能成为存储管理的新趋势——它将更加贴近容器运行时，尤其在安全容器的设计中，其作用会愈发明显。&lt;/p&gt;
&lt;h2 id=&#34;note&#34;&gt;NOTE&lt;/h2&gt;
&lt;p&gt;Mount Manager 的运行同样依赖 boltdb 来管理状态，但要求该数据库运行在 tmpfs 上。如果有人将 containerd 的 state 目录配置在持久化存储中，containerd 初始化时将会失败。通常，社区建议将 state 目录（临时数据）放置在 &lt;code&gt;/run&lt;/code&gt; 等 tmpfs 路径下。&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>Previewing Rebase Snapshots in containerd v2.2.0</title>
          <link>/post/2025-containerd-220-rebase-snapshot/</link>
          <pubDate>Fri, 31 Oct 2025 19:20:45 -0500</pubDate>
          <guid>https://fuweid.com/post/2025-containerd-220-rebase-snapshot/</guid>
          <description>&lt;p&gt;containerd 社区近期发布了 v2.2.0-rc.0 版本，预计将在亚特兰大 KubeCon 前正式 GA。在正式发布之前，我想先分享其中的一个特性 &amp;ndash; Rebase Snapshot，它可以显著提升大型镜像的下载效率。&lt;/p&gt;
&lt;h1 id=&#34;snapshot-model&#34;&gt;Snapshot Model&lt;/h1&gt;
&lt;p&gt;在 Open Container Initiative (OCI) 镜像标准中，每次对容器镜像的修改都会生成一个镜像层（Image Layer）。每层只包含新增、删除或修改的文件，层与层之间互相依赖，就像 Git 的提交记录一样，后一层是基于前一层构建的。&lt;/p&gt;
&lt;p&gt;容器运行时（消费端）会按照依赖顺序依次应用（或叠加）这些镜像层的数据，最终组合形成容器进程的根文件系统。至于数据如何叠加和存储，则取决于所采用的底层存储。此外，容器进程对其根文件系统产生的变更，可以被「提交」并封装为一个新的镜像层，这是常见的镜像构建方式。&lt;/p&gt;
&lt;p&gt;为了统一管理镜像层和容器文件系统的数据，containerd 引入了 snapshot的概念。它将这些文件系统抽象为可管理的 snapshot，并使用统一的 snapshotter 接口来对接和表达不同底层存储介质（如 OverlayFS, EROFS, OverlayBD, DevMapper 等）的实现方式。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2025-containerd-220-rebase-snapshot/snapshot-model.png&#34; alt=&#34;snapshot-model&#34;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NOTE: From &lt;a href=&#34;https://youtu.be/UUDDCetB7_A?si=RmpE2Gy_PEW76JKJ&#34;&gt;2017: containerd deep dive presentation at the containerd summit&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Snapshot 包含两个关键状态：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* Active：在此状态下，允许对文件内容进行添加、删除和修改等操作。

* Committed：一旦 Active Snapshot 被提交（Commit），它将转换为 Committed 状态。
            原则上，处于 Committed 状态的文件内容是不可变更的。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在解压镜像层数据时，containerd 会基于上一层 snapshot 来创建新的 active snapshot。上一层 snapshot 可以为空，「通常」表达当前 active snapshot 对应着第一层。解压数据完毕后，再将其提交成 committed 状态；重复此流程直到所有镜像层都处理完毕。Snapshot 之间的依赖关系能帮助 containerd 的 GC 模块识别哪些 snapshot 已经不再被引用（orphan snapshot），从而将其安全删除。&lt;strong&gt;但这也意味着 containerd 必须按照顺序解压镜像&lt;/strong&gt;。&lt;/p&gt;
&lt;h1 id=&#34;existing-enhancements-on-pulling&#34;&gt;Existing Enhancements on pulling&lt;/h1&gt;
&lt;h3 id=&#34;v100-initial-version&#34;&gt;[v1.0.0] Initial Version&lt;/h3&gt;
&lt;p&gt;在 containerd 的最初版本（2017 年）中，镜像处理流程被设计为两个阶段：首先并发下载所有镜像层，然后按照层级关系依次解压。解压某一层时，containerd 需要先将其所有祖先 snapshot 按顺序进行 union mount，形成完整的基础视图，然后才能在此基础上应用当前层的数据。&lt;/p&gt;
&lt;h3 id=&#34;v130-direct-unpack-support-for-overlayfshttpsgithubcomcontainerdcontainerdpull3528&#34;&gt;&lt;a href=&#34;https://github.com/containerd/containerd/pull/3528&#34;&gt;[v1.3.0] Direct unpack support for overlayFS&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Union mount 本身是一项开销较高的操作，特别是在执行卸载（umount）时更为明显。在 Linux kernel v5.10（&lt;a href=&#34;https://www.phoronix.com/news/OverlayFS-Linux-5.10&#34;&gt;支持 volatile 选项&lt;/a&gt;）之前，卸载 OverlayFS 挂载点会触发强制刷盘，写放大进一步增大了开销。&lt;strong&gt;而实际上，在解压 OCI 镜像层数据时，并不需要访问上一层已展开的文件内容；containerd 也无需先构建完整的文件系统视图再进行解压。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;因此，从 v1.3 开始，containerd 为 overlayFS snapshotter 引入了一种优化方式：直接将镜像层解压到对应的 snapshot 目录，避免为了解压而执行 union mount。&lt;/p&gt;
&lt;h3 id=&#34;v130-add-simultaneous-unpack-supporthttpsgithubcomcontainerdcontainerdpull2918files&#34;&gt;&lt;a href=&#34;https://github.com/containerd/containerd/pull/2918/files&#34;&gt;[v1.3.0] Add simultaneous unpack support&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;此外，v1.3 版本实施了另一项优化：下载与解压的并行处理。containerd 现在无需等待所有镜像层下载完毕，而是在任何一层下载完成后就立即启动解压任务，以提升镜像处理的整体速度。然而，这项优化存在一个关键的局限性：镜像层的解压必须严格按照其依赖的顺序进行。这意味着即使后面的层（例如第 3 层）比前面的层（例如第 2 层）先下载完成，它也必须等待所有前置层（第 1 层、第 2 层等）下载并解压完成后才能开始处理。由于在大多数镜像结构中，较小或变更较少的层通常位于镜像的末尾，因此顺序依赖使得「并行解压」收益有限。&lt;/p&gt;
&lt;h3 id=&#34;v210-multipart-layer-fetchhttpsgithubcomcontainerdcontainerdpull10177&#34;&gt;&lt;a href=&#34;https://github.com/containerd/containerd/pull/10177&#34;&gt;[v2.1.0] Multipart layer fetch&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;在上一个版本中，containerd 引入了同一镜像层的并发下载能力。利用 OCI Distribution 协议中支持可断点续传 - &lt;a href=&#34;https://github.com/opencontainers/distribution-spec/blob/main/spec.md#resumable-pull&#34;&gt;resumable pull&lt;/a&gt; - 的特性，containerd 会将大型镜像层切分为多个区段，多路并发下载。各区段数据在内存中按顺序重新组合，并最终写回本地磁盘，形成完整的镜像层。这一优化提升了大镜像层的下载速度，但也带来了两个限制：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* 会额外占用内存，因为数据区段需要在内存中进行拼接；

* 效果依赖区段大小配置，划分不合理会影响性能收益。
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;目前，大多数由 Go 编写的服务（包括容器镜像仓库）默认使用 HTTP/2 协议。然而，HTTP/2 在传输大体积数据时效率并不高，带宽利用率受限，服务端往往需要等待客户端的窗口更新（WINDOW_UPDATE），而非持续传输数据。containerd 通过多连接并发下载来缓解这一问题，本质上是在规避 HTTP/2 流控带来的性能限制。在网络质量可控的情况下，使用单连接的 HTTP/1.1 反而可能实现更高的吞吐性能。&lt;/p&gt;
&lt;h1 id=&#34;rebase&#34;&gt;Rebase&lt;/h1&gt;
&lt;p&gt;尽管过去的优化提升了下载效率，但一个事实没有改变：镜像层的解压仍然是按顺序进行的。为了解决这一瓶颈，Derek 在问题 &lt;a href=&#34;https://github.com/containerd/containerd/issues/8881#issuecomment-1654612733&#34;&gt;Parallel Container Layer Unpacking&lt;/a&gt; 中提出了 Rebase Snapshot 来加速处理。&lt;/p&gt;
&lt;p&gt;经常使用「变基」的朋友对这个词应该不陌生：在 Git 中，&lt;code&gt;git rebase upstream/master&lt;/code&gt; 可以用来更新当前分支。它的本质是将当前分支上的提交，在最新的 master 分支基础上重新回放一遍。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2025-containerd-220-rebase-snapshot/basic-rebase-git.png&#34; alt=&#34;base-rebase-git&#34;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NOTE: From &lt;a href=&#34;https://git-scm.com/book/ms/v2/Git-Branching-Rebasing&#34;&gt;https://git-scm.com/book/ms/v2/Git-Branching-Rebasing&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;同样地，支持 rebase 的 snapshotter 允许在提交时，将一个没有 parent 的 active snapshot 绑定到指定的 committed snapshot 上，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2025-containerd-220-rebase-snapshot/rebase-model.png&#34; alt=&#34;rebase-model&#34;&gt;&lt;/p&gt;
&lt;p&gt;在下载镜像时，containerd 会同步解压所有镜像层，此时这些 Active snapshot 尚未指定父 snapshot。解压完成后，它们会按顺序提交，并关联到指定的父 snapshot，从而建立层之间的依赖关系。这个过程类似于 Git 的 rebase：在管理层面回放各层提交，实现了真正意义上的「同步解压」。&lt;/p&gt;
&lt;p&gt;我在本地开发机器上测试了镜像 &lt;strong&gt;huggingface/transformers-pytorch-gpu:4.41.2&lt;/strong&gt;（解压后约 17.7 GiB）的下载与解压效果，并与 containerd v2.1.4 版本进行了对比。&lt;/p&gt;
&lt;p&gt;本地机器配置如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2025-containerd-220-rebase-snapshot/neofetch.png&#34; alt=&#34;neofetch&#34;&gt;&lt;/p&gt;
&lt;p&gt;为了避免网络带宽的影响，我把镜像下载到本地，并传到本地运行的 registry 服务里。&lt;strong&gt;优化效果还是挺明显，一倍左右&lt;/strong&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;v2.1.4  需要 - &lt;strong&gt;72s&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关闭 multipart layer fetch 优化&lt;/li&gt;
&lt;li&gt;max_concurrent_download = 3&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2025-containerd-220-rebase-snapshot/v214-72.png&#34; alt=&#34;v214-72&#34;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;v2.2.0-rc.0 需要 - &lt;strong&gt;34.7s&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;关闭 multipart layer fetch 优化&lt;/li&gt;
&lt;li&gt;max_concurrent_download = 3&lt;/li&gt;
&lt;li&gt;Max_concurrent_unpacks = 3&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2025-containerd-220-rebase-snapshot/v2.2.0-rc.0-34.png&#34; alt=&#34;v220-34&#34;&gt;&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>Debug Log #1: From Containerd -EBUSY to ETCD bbolt Corruption</title>
          <link>/post/2025-debug-01-ebusy-and-corruption/</link>
          <pubDate>Sun, 21 Sep 2025 09:20:45 -0500</pubDate>
          <guid>https://fuweid.com/post/2025-debug-01-ebusy-and-corruption/</guid>
          <description>&lt;p&gt;我打算开一个 Debug Log 系列，把平时在开源项目或生产环境里遇到的有趣问题记录下来。作为系列的第一篇，我想分享近期的两次排查经历：一次是 Containerd 临时挂载点残留，另一次是 ETCD 数据库损坏。&lt;/p&gt;
&lt;h2 id=&#34;-ebusy-导致临时挂载点残留&#34;&gt;-EBUSY 导致临时挂载点残留&lt;/h2&gt;
&lt;p&gt;Netflix 工程师 halaney 在 Containerd 社区提交了一个问题 &lt;a href=&#34;https://github.com/containerd/containerd/issues/12139&#34;&gt;#12139&lt;/a&gt;：他们在测试环境中遇到了大量的 &lt;a href=&#34;https://www.kernel.org/doc/html/next/filesystems/idmappings.html&#34;&gt;Idmappings&lt;/a&gt; 临时挂载点残留问题，几乎在并发创建容器时必现。据了解，Netflix 计划升级到 Containerd v2.0.x，这个问题成为了升级的障碍。他们内部已经通过使用 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/umount.2.html#:~:text=MNT_DETACH%20(since%20Linux%202.4.11)&#34;&gt;MNT_DETACH&lt;/a&gt; - lazy umount 的方式来规避。不过，我个人并不喜欢这种做法，因为它并没有真正解决问题本身。&lt;/p&gt;
&lt;p&gt;前几年，我曾遇到一个「黑科技」日志采集方案——它会扫描 &lt;code&gt;/run/containerd&lt;/code&gt; 目录下的挂载点，也就是容器的根目录，并长期持有容器根目录下的日志文件句柄，导致出现 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/umount.2.html#:~:text=EBUSY%20%20target%20could%20not%20be%20unmounted%20because%20it%20is%20busy.&#34;&gt;-EBUSY&lt;/a&gt; 错误而无法清理容器挂载点，从而出现残留。业务为重，只能妥协，加上 MNT_DETACH :)。过往社区也出现过类似问题，但最终发现都是安全软件间歇性扫描导致的。因此一开始排查这个残留问题时，我会下意识地认为这可能是「环境」问题。&lt;/p&gt;
&lt;p&gt;我当时怀疑的对象是 &lt;a href=&#34;https://www.freedesktop.org/software/systemd/man/latest/systemd.mount.html&#34;&gt;systemd.mount&lt;/a&gt;。每当 host mount_namespace 出现新的挂载点时，systemd 就会生成一个 mount.unit。比如当你运行 &lt;code&gt;docker run -d busybox sleep 1d&lt;/code&gt; 时，你会发现会有一个这样奇葩的 mount.unit。老实说，我并不知道这东西的实际作用 :)&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;$ sudo systemctl list-units &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; grep var-lib-docker
  var-lib-docker-overlay2-9643eb3cca86f8de7ac881124eb01ce10d3af44872f246e196a1cfe9e0ec6753-merged.mount loaded active     mounted      /var/lib/docker/overlay2/9643eb3cca86f8de7ac881124eb01ce10d3af44872f246e196a1cfe9e0ec6753/merged

$ sudo systemctl status var-lib-docker-overlay2-9643eb3cca86f8de7ac881124eb01ce10d3af44872f246e196a1cfe9e0ec6753-merged.mount

● var-lib-docker-overlay2-9643eb3cca86f8de7ac881124eb01ce10d3af44872f246e196a1cfe9e0ec6753-merged.mount - /var/lib/docker/overlay2/9643eb3cca86f8de7ac881124eb01ce10d3af44872f246e196a1cfe9e0ec6753/merged
     Loaded: loaded &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;/proc/self/mountinfo&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;
     Active: active &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;mounted&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; since Sat 2025-09-20 22:51:06 EDT&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; 6min ago
      Where: /var/lib/docker/overlay2/9643eb3cca86f8de7ac881124eb01ce10d3af44872f246e196a1cfe9e0ec6753/merged
       What: overlay
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;通过 &lt;a href=&#34;https://manpages.debian.org/unstable/bpfcc-tools/opensnoop-bpfcc.8.en.html&#34;&gt;opensnoop-bpfcc&lt;/a&gt; 和 &lt;a href=&#34;https://github.com/bpftrace/bpftrace&#34;&gt;bpftrace&lt;/a&gt; 等工具观察各种可能的系统调用，但 &lt;a href=&#34;https://github.com/systemd/systemd/blob/49e5c6462e6fe82e607b7e395bd01cd8a54133a3/src/core/mount.c#L2185&#34;&gt;systemd.mount_process_proc_self_mountinfo&lt;/a&gt; 只是读取了 &lt;code&gt;/proc/self/mountinfo&lt;/code&gt;，并不是问题的根源。后来我又花了不少时间研究 &lt;a href=&#34;https://github.com/opencontainers/runc&#34;&gt;runc&lt;/a&gt; 相关逻辑，发现 runc 做了很多保护性处理，确保挂载点传播不会引发问题。最后只能回过头来看 Containerd 的代码了。&lt;/p&gt;
&lt;p&gt;从 &lt;a href=&#34;https://github.com/anakryiko/retsnoop&#34;&gt;retsnoop&lt;/a&gt; 的结果来看，&lt;a href=&#34;https://elixir.bootlin.com/linux/v6.14/source/fs/pnode.c#L420&#34;&gt;mnt_get_count&lt;/a&gt; 在 &lt;a href=&#34;https://elixir.bootlin.com/linux/v6.14/source/fs/pnode.c#L407&#34;&gt;propagate_mount_busy&lt;/a&gt; 函数中返回了多个引用（如下图所示），这很可能是因为多个文件句柄同时指向了同一个挂载点。结合此前的观察，可以排除外部进程扫描的可能性，因此这些额外引用几乎只能来源于子进程创建过程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2025-debug-01-ebusy-and-corruption/retsnoop-result.png&#34; alt=&#34;retsnoop-result&#34;&gt;&lt;/p&gt;
&lt;p&gt;IDmapping 是在挂载点上建立一个 UID/GID 映射层，使同一份数据在不同目录下呈现不同的所有者身份，具体细节可以查阅 &lt;a href=&#34;https://fuweid.com/post/2023-containerd-17-userns/&#34;&gt;containerd 1.7: UserNamespace Stateless Pod&lt;/a&gt;。IDmapping 的挂载流程如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Step 1：通过 &lt;a href=&#34;https://lwn.net/Articles/829496/&#34;&gt;open_tree(2)&lt;/a&gt; 复制一份源目录的挂载子树，并获得一个指向该子树的文件句柄。&lt;/li&gt;
&lt;li&gt;Step 2：通过 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/mount_setattr.2.html&#34;&gt;mount_setattr(2)&lt;/a&gt; 系统调用修改挂载子树的属性。主要利用 user_namespace 引用来调整 UID/GID 映射。
&lt;ul&gt;
&lt;li&gt;例如，源目录所有者是 root，我们可以映射成用户 1000。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Step 3：通过 move_mount(2) 系统调用将挂载子树移动到目标目录，这时挂载点形成。&lt;/li&gt;
&lt;li&gt;Step 4：关闭文件句柄。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在第三步成功返回后，该文件句柄会指向目标目录（即挂载点）。因此，在关闭前，它可能被复制到子进程中，例如通过 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/clone.2.html&#34;&gt;clone(2)&lt;/a&gt; 系统调用。即使 Containerd 在第一步中使用了 &lt;code&gt;OPEN_TREE_CLOEXEC&lt;/code&gt;，在执行 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/execve.2.html&#34;&gt;execve(2)&lt;/a&gt; 之前，子进程仍会持有挂载点的句柄。如果这时调用 umount，就会触发 EBUSY 错误。&lt;/p&gt;
&lt;p&gt;此外，第二步需要 Containerd 通过一次 &lt;a href=&#34;https://fuweid.com/post/2024-ptrace-hallofshame/&#34;&gt;ptrace re-exec&lt;/a&gt; 获取 user_namespace 引用。这意味着在并发创建容器时，目标挂载点的文件句柄很容易被复制到 Containerd 的子进程中。当然，调用二进制 CNI 以及启动 contained-shim 同样也会触发复制行为。因此，&lt;a href=&#34;https://elixir.bootlin.com/linux/v6.14/source/fs/pnode.c#L420&#34;&gt;mnt_get_count&lt;/a&gt; 返回高引用数也就可以理解了。由于 &lt;a href=&#34;https://lwn.net/Articles/829496/&#34;&gt;open_tree(2)&lt;/a&gt; 在挂载时必须生成文件句柄，这一问题基本无法避免。最终的解决方案只能依靠多次重试，毕竟 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/clone.2.html&#34;&gt;clone(2)&lt;/a&gt; 与 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/execve.2.html&#34;&gt;execve(2)&lt;/a&gt; 之间的时间窗口相对较小。后续可以通过 mount plugin 统一管理所有挂载点，并借助 GC 机制从根本上解决残留问题。&lt;/p&gt;
&lt;p&gt;在这里，我强烈推荐使用 &lt;a href=&#34;https://github.com/anakryiko/retsnoop&#34;&gt;retsnoop&lt;/a&gt; 来排查 Linux 内核中的系统调用错误。即便不是内核开发者，也可以通过全局检索 Linux 源代码，并结合 retsnoop 提供的调用栈信息，高效地进行定位与分析。&lt;/p&gt;
&lt;h2 id=&#34;panic-assertion-failed-page-expected-to-be-5&#34;&gt;panic: assertion failed: Page expected to be: 5&lt;/h2&gt;
&lt;p&gt;ETCD 社区最近将故障注入测试迁移到了 &lt;a href=&#34;https://antithesis.com/&#34;&gt;antithesis&lt;/a&gt; 产品上。该产品会在测试运行过程中随机注入网络丢包、网络连通性中断以及 CPU 限流等故障。&lt;a href=&#34;https://antithesis.com/docs/environment/fault_injection/&#34;&gt;文档&lt;/a&gt; 中并未说明具体实现方式，但由于 ETCD 测试基于 docker-compose，最直观的 CPU 限流手段就是通过 cgroup 来完成。&lt;/p&gt;
&lt;p&gt;在迁移过程中发现，ETCD Member 会接收到损坏的 snapshot（见 &lt;a href=&#34;https://github.com/etcd-io/etcd/issues/20271&#34;&gt;#20271&lt;/a&gt;），如下输出所示。这表明 bbolt 存储结构中出现了错误的引用。ETCD 社区过去也曾遇到过许多类似的数据库损坏问题，其中大多数由断电引起。由于对 antithesis 环境缺乏深入了解，起初很容易将其误认为是网络故障导致的错误。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;panic: assertion failed: Page expected to be: 5, but self identifies as &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;
goroutine &lt;span class=&#34;m&#34;&gt;8052&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;running&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;:
go.etcd.io/bbolt/internal/common.Assert&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;...&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;
	go.etcd.io/bbolt@v1.4.2/internal/common/verify.go:65
go.etcd.io/bbolt/internal/common.&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;*Page&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;.FastCheck&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;0x7f16ce802000, 0x5&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;
	go.etcd.io/bbolt@v1.4.2/internal/common/page.go:83 +0x1d9
go.etcd.io/bbolt.&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;*Tx&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;.page&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;0xc0010a31d0?, 0xc0002992e8?&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;
	go.etcd.io/bbolt@v1.4.2/tx.go:598 +0x7b
go.etcd.io/bbolt.&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;*Tx&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;.forEachPageInternal&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;0xc000c71340, &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;0xc0010a31d0, 0x1, 0xa&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;, 0xc0002993a0&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;该测试过程主要通过注入网络故障和 CPU 限流来触发 Follower 节点落后于 Leader 日志进度的情况。随后，Leader 会发送完整数据库快照（snapshot）以完成同步。除此之外，测试还会以高频率对所有 Member 进行 &lt;a href=&#34;https://etcd.io/docs/v3.6/op-guide/maintenance/#:~:text=every%2030%2Dminute.-,Defragmentation,-After%20compacting%20the&#34;&gt;defragment&lt;/a&gt; 操作。&lt;/p&gt;
&lt;p&gt;这个问题已经放置一段时间，没有太大进展，因为大家普遍认为是环境问题。后来抱着试一试的心态去查看 ETCD Member 日志。只能说这种方法比较「原始」，一旦日志记录不够详尽，没有关键的信息，就几乎是在浪费时间。只能说运气还不错，大部分 debug 信息都输出了。&lt;/p&gt;
&lt;p&gt;Follower 收到的数据库快照显示 Index 为 74，并且确认已落盘。然而，当该 Follower 从该快照恢复时，却显示 Index 为 67。由此可以确认，这不是环境问题，而是 ETCD 内部存在 Race condition。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2025-debug-01-ebusy-and-corruption/etcd-snapshot.png&#34; alt=&#34;etcd-snapshot&#34;&gt;&lt;/p&gt;
&lt;p&gt;这个 race condition 是由 defragment 操作触发的。在 ETCD 使用接收的快照之前，它会通过 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/rename.2.html&#34;&gt;rename(2)&lt;/a&gt; 覆盖当前正在使用的数据库文件。事件时间线如下：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2025-debug-01-ebusy-and-corruption/timeline.png&#34; alt=&#34;etcd-timeline&#34;&gt;&lt;/p&gt;
&lt;p&gt;需要解释的是：如果一个进程保持着一个文件句柄，即使这个文件被删除或覆盖，该进程依然可以继续通过这个句柄操作文件。一个最直观的场景是：业务进程写了大量日志，而磁盘空间几乎耗尽，此时需要清理日志文件，但业务进程不能重启。如果直接删除日志文件，文件并不会真正释放空间，因为业务进程仍然持有文件句柄，可以继续写入数据。正确的做法是使用 &lt;a href=&#34;https://man7.org/linux/man-pages/man1/truncate.1.html&#34;&gt;truncate(1)&lt;/a&gt; - &lt;code&gt;truncate -s 0 log&lt;/code&gt; 或者是 &lt;code&gt;cat /dev/null &amp;gt; log&lt;/code&gt;，保留原文件的 inode，将文件大小强制清零，从而释放磁盘空间，同时保证业务进程继续写入日志而不受影响。&lt;/p&gt;
&lt;p&gt;同样地，在 &lt;code&gt;T10&lt;/code&gt; 之后，ETCD Member 会将后续的变化写入到一个已经被覆盖的文件中。到这一步，其实问题不大——重启后的 ETCD Member 仍然落后于其他 Member，仍可以继续接收新的快照。然而，从 &lt;code&gt;T10&lt;/code&gt; 之后，它开始成为 Leader。在测试的后期，它需要向其他 Member 发送快照，这时问题就暴露出来了。&lt;/p&gt;
&lt;p&gt;在发送快照时，ETCD 利用了多版本并行控制 (MVCC) 特性，只会发送当前 bbolt 只读事务对应的版本。bbolt 数据库的前两页保存着整棵 B+Tree 的根节点引用，因此在发送快照前，ETCD 会先发送当前事务版本的根节点引用，然后再读取数据库的其余部分。即使在此过程中有新的事务提交，也不会影响当前版本的数据，从而保证快照的一致性，如下图所示。这里有一个前提，文件不能被覆盖。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2025-debug-01-ebusy-and-corruption/bbolt-writeTo.png&#34; alt=&#34;bbolt-writeTo&#34;&gt;&lt;/p&gt;
&lt;p&gt;在测试过程中，ETCD 的 snap.db 文件已经被 &lt;code&gt;T8&lt;/code&gt; 时刻的操作覆盖，其内容与正在使用的、已被删除的文件完全不同。值得注意的是，&lt;a href=&#34;https://github.com/etcd-io/bbolt/blob/v1.4.1/tx.go#L389&#34;&gt;bbolt.WriteTo&lt;/a&gt; 并没有使用原有的文件句柄，而是重新打开了一个新的句柄。由于前两页存储的根节点引用与实际存储内容不一致，最终会导致 bbolt 初始化失败并触发 panic。&lt;/p&gt;
&lt;p&gt;为解决该问题，ETCD 需要避免 Race Condition（见 &lt;a href=&#34;https://github.com/etcd-io/etcd/pull/20553&#34;&gt;#20553&lt;/a&gt;），而 bbolt 则必须确保传输的文件内容保持一致（见 &lt;a href=&#34;https://github.com/etcd-io/bbolt/pull/1057&#34;&gt;#1057&lt;/a&gt;）。不过，对于 &lt;a href=&#34;https://github.com/etcd-io/bbolt/pull/1057&#34;&gt;#1057&lt;/a&gt;，实际上也可以考虑重新打开一份已被删除文件的句柄，因为 io.Copy 可以利用 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/splice.2.html&#34;&gt;splice(2)&lt;/a&gt; 实现 Zero-Copy。当然，在 ETCD 发送快照时，中间存在一层 Copy，此时无法使用 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/splice.2.html&#34;&gt;splice(2)&lt;/a&gt;，因此使用 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/pwrite.2.html&#34;&gt;pread(2)&lt;/a&gt; 也还行吧。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span class=&#34;gh&#34;&gt;diff --git a/tx.go b/tx.go
&lt;/span&gt;&lt;span class=&#34;gh&#34;&gt;index f32a209..d6c2e85 100644
&lt;/span&gt;&lt;span class=&#34;gh&#34;&gt;&lt;/span&gt;&lt;span class=&#34;gd&#34;&gt;--- a/tx.go
&lt;/span&gt;&lt;span class=&#34;gd&#34;&gt;&lt;/span&gt;&lt;span class=&#34;gi&#34;&gt;+++ b/tx.go
&lt;/span&gt;&lt;span class=&#34;gi&#34;&gt;&lt;/span&gt;&lt;span class=&#34;gu&#34;&gt;@@ -5,8 +5,10 @@ import (
&lt;/span&gt;&lt;span class=&#34;gu&#34;&gt;&lt;/span&gt;        &amp;#34;fmt&amp;#34;
        &amp;#34;io&amp;#34;
        &amp;#34;os&amp;#34;
&lt;span class=&#34;gi&#34;&gt;+       &amp;#34;path/filepath&amp;#34;
&lt;/span&gt;&lt;span class=&#34;gi&#34;&gt;&lt;/span&gt;        &amp;#34;runtime&amp;#34;
        &amp;#34;sort&amp;#34;
&lt;span class=&#34;gi&#34;&gt;+       &amp;#34;strconv&amp;#34;
&lt;/span&gt;&lt;span class=&#34;gi&#34;&gt;&lt;/span&gt;        &amp;#34;strings&amp;#34;
        &amp;#34;sync/atomic&amp;#34;
        &amp;#34;time&amp;#34;
&lt;span class=&#34;gu&#34;&gt;@@ -390,7 +392,7 @@ func (tx *Tx) Copy(w io.Writer) error {
&lt;/span&gt;&lt;span class=&#34;gu&#34;&gt;&lt;/span&gt; // If err == nil then exactly tx.Size() bytes will be written into the writer.
 func (tx *Tx) WriteTo(w io.Writer) (n int64, err error) {
        // Attempt to open reader with WriteFlag
&lt;span class=&#34;gd&#34;&gt;-       f, err := tx.db.openFile(tx.db.path, os.O_RDONLY|tx.WriteFlag, 0)
&lt;/span&gt;&lt;span class=&#34;gd&#34;&gt;&lt;/span&gt;&lt;span class=&#34;gi&#34;&gt;+       f, err := tx.db.openFile(filepath.Join(&amp;#34;/proc&amp;#34;, strconv.Itoa(os.Getpid()), &amp;#34;fd&amp;#34;, strconv.Itoa(int(tx.db.file.Fd()))), os.O_RDONLY|tx.WriteFlag, 0)
&lt;/span&gt;&lt;span class=&#34;gi&#34;&gt;&lt;/span&gt;        if err != nil {
                return 0, err
        }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;最后&#34;&gt;最后&lt;/h2&gt;
&lt;p&gt;我尝试用 AI 来排查这两个问题，但可能是使用方式不对。即使输入了大量信息，把我的分析步骤都写了出来，依然得不到理想的结果。最接近的一次是，它认为我没有关闭 &lt;a href=&#34;https://lwn.net/Articles/829496/&#34;&gt;open_tree(2)&lt;/a&gt; 返回的句柄，于是它在代码里加了一行关闭操作。结果却导致对同一个句柄进行了两次关闭，最后它又开始聚焦在为什么会发生新的错误&amp;hellip;&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>Streaming JSON/Protobuf in Kubernetes</title>
          <link>/post/2025-streaming-jsonpb-in-k8s/</link>
          <pubDate>Mon, 03 Feb 2025 10:20:45 -0500</pubDate>
          <guid>https://fuweid.com/post/2025-streaming-jsonpb-in-k8s/</guid>
          <description>&lt;h3 id=&#34;oom-kill&#34;&gt;OOM-KILL&lt;/h3&gt;
&lt;p&gt;当 kube-apiserver 处理 LIST 请求时，它会一次性将数据序列化为 JSON 或 Protobuf 格式，然后交由底层的 Go/http 处理。根据标准的 &lt;a href=&#34;https://github.com/golang/go/blob/b07b20fbb591ac77158e1089299ce5acad71ffde/src/encoding/json/stream.go#L202-L235&#34;&gt;encoding/json&lt;/a&gt; 库实现，kube-apiserver 需要分配一大块内存来存放完整的序列化结果。更严重的是，这块内存要等到数据的最后一个字节被传输完毕后才会释放，容易导致高峰时的内存占用激增。&lt;/p&gt;
&lt;p&gt;在 Go/http2 实现中，每个 http2/stream 都有一个传输队列，队列中的成员存储需要发送的数据。多个 http2/stream 共享同一个 TCP 连接，为了保证公平性，Go 按顺序选择 http2/stream 并限制每次发送的数据量。由于每个 http2/stream 只有有限的发送窗口，数据会像“挤牙膏”一样逐步推送。然而，在 kube-apiserver 的实现中，每个 http2/stream 的传输队列通常只有一个成员指向序列化的数据，该成员会一直持有完整的序列化数据，直到传输完毕才释放。如果短时间内有多个大请求，kube-apiserver 会迅速消耗大量内存，甚至可能导致 OOM 崩溃。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2025-streaming-jsonpb-in-k8s/http2-stream-writedata.png&#34; alt=&#34;http2-stream-writedata&#34;&gt;&lt;/p&gt;
&lt;p&gt;更揪心的是 encoding/json 的实现。虽然它内部使用了 sync.Pool 来提高内存复用率，但这也带来了一个隐患：如果先处理了少量的大请求，而后续主要是大量的小请求，那么 sync.Pool 可能会长期持有之前分配的大块内存，而这些内存无法及时释放。在这种情况下，kube-apiserver 的内存占用无法真实反映当前的请求负载，即使大请求已经结束，进程仍可能维持较高的内存使用率。&amp;ndash; &lt;a href=&#34;https://github.com/kubernetes/kubernetes/issues/114276&#34;&gt;Kubernetes#114276&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&#34;kep-5116&#34;&gt;KEP-5116&lt;/h3&gt;
&lt;p&gt;解决这个问题的关键在于引入 &lt;strong&gt;流式处理&lt;/strong&gt; 来序列化数据。&lt;a href=&#34;https://github.com/kubernetes/enhancements/issues/5116&#34;&gt;KEP-5116&lt;/a&gt; 根据 LIST 响应的结构特点，可以依次序列化 TypeMeta、ListMeta，然后逐项序列化 Items，避免一次性分配和持有大块内存，从而降低内存占用。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;XYZList&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
        &lt;span class=&#34;nx&#34;&gt;metav1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;TypeMeta&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;`json:&amp;#34;,inline&amp;#34;`&lt;/span&gt;
        &lt;span class=&#34;c1&#34;&gt;// Standard list metadata.
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;        &lt;span class=&#34;c1&#34;&gt;// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;        &lt;span class=&#34;c1&#34;&gt;// +optional
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;        &lt;span class=&#34;nx&#34;&gt;metav1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ListMeta&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;`json:&amp;#34;metadata,omitempty&amp;#34; protobuf:&amp;#34;bytes,1,opt,name=metadata&amp;#34;`&lt;/span&gt;

        &lt;span class=&#34;c1&#34;&gt;// List of XYZ.
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;        &lt;span class=&#34;c1&#34;&gt;// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;        &lt;span class=&#34;nx&#34;&gt;Items&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;XYZ&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;`json:&amp;#34;items&amp;#34; protobuf:&amp;#34;bytes,2,rep,name=items&amp;#34;`&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这样不仅解决了 encoding/json 大内存回收的问题，还解决了 Go/http2 的内存管理问题。
在流式处理的模式下，http2 stream 的队列成员数量会增加，但每个成员最多只会引用 1-2 MiB 的内存，并在发送完毕后及时释放。
此外，根据 Go/http2 的实现，当网络较为繁忙时，kube-apiserver 无法无限制地向 http2/stream 队列中添加待发送的数据成员，从而在一定程度上抑制了内存的过快增长，进一步提升了系统的稳定性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2025-streaming-jsonpb-in-k8s/http2-stream-writedata2.png&#34; alt=&#34;http2-stream-writedata-2&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;benchmark&#34;&gt;Benchmark&lt;/h3&gt;
&lt;p&gt;实验准备:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1024 个 configmap，每个 configmap 大小为 1 MiB&lt;/li&gt;
&lt;li&gt;每个请求数据量为 100 MiB, 数据格式为 JSON，请求不经过 ETCD&lt;/li&gt;
&lt;li&gt;每秒最多发出 10 个请求，客户端最多并发处理 100 个请求&lt;/li&gt;
&lt;li&gt;一共发出 6000 个请求&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;下图左侧的为 kube-apiserver v1.32.0 版本，它最高消耗了将近 29 GiB; 而采用了 &lt;a href=&#34;https://github.com/kubernetes/kubernetes/pull/129334&#34;&gt;Kubernetes#129334&lt;/a&gt; 的版本仅仅需要 2-3 GiB，内存占用大幅降低。
这个优化方案比 &lt;a href=&#34;https://github.com/kubernetes/enhancements/blob/master/keps/sig-api-machinery/3157-watch-list/README.md&#34;&gt;WatchList&lt;/a&gt; 实用性更强，客户端不需要做任何改造。可惜这么好的优化，上游不愿意 backport 到老版本上。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2025-streaming-jsonpb-in-k8s/benchmark.png&#34; alt=&#34;benchmark-result&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;65k-nodes&#34;&gt;65K Nodes?&lt;/h3&gt;
&lt;p&gt;Kubernetes 在近几个版本的优化都挺给力的，比如 &lt;a href=&#34;https://github.com/kubernetes/enhancements/blob/master/keps/sig-api-machinery/2340-Consistent-reads-from-cache/README.md&#34;&gt;v1.31 ConsistentListFromCache&lt;/a&gt; 可以很大程度避免击穿背后的 ETCD，再配合上基于缓存的分页能力，ETCD 仅需要做好数据推送的任务就好了。最后再配合上 KEP-5116，65K 节点不是梦啊 :)&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NOTE: 这个问题同样存在于 gRPC-go 的 UnaryCall 方式中。除非服务端一开始就采用 gRPC-go StreamCall 进行流式传输，否则在处理大规模数据时，仍然可能触发 OOM-KILL。ETCD 也面临类似的挑战，但 Kubernetes 目前的优化策略更倾向于让 ETCD 专注于数据推送，而由 kube-apiserver 承担数据处理的压力。从这个角度来看，Kubernetes 也算是间接解决了这个问题。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
        </item>
        
        <item>
          <title>退群「Hall of shame」</title>
          <link>/post/2024-ptrace-hallofshame/</link>
          <pubDate>Tue, 03 Sep 2024 07:47:45 +0800</pubDate>
          <guid>https://fuweid.com/post/2024-ptrace-hallofshame/</guid>
          <description>&lt;p&gt;在 GO 1.23 版本中，&lt;a href=&#34;https://github.com/golang/go/issues/67401&#34;&gt;GO Linker 将来会限制 &lt;code&gt;//go:linkname&lt;/code&gt; 直接引用标准库中不可访问的对象&lt;/a&gt;。
看到这个消息后，我立刻用 GO 1.23.0 编译了 containerd，发现即使不加 &lt;code&gt;-checklinkname=0&lt;/code&gt; 也能正常工作。
之前为了支持 ID-mapped mount, 我在 containerd 项目里使用了 GO runtime 的隐藏功能，算是语言层面的灵活运用。
现在可倒好，GO 团队直接在代码里把 containerd 列为 &lt;a href=&#34;https://github.com/golang/go/blob/a9e6a96ac092cc5191d759488ee761e9a403ab8f/src/runtime/proc.go#L4848&#34;&gt;Hall of shame&lt;/a&gt; 成员。
这种强行拉入群的形式实在无力吐槽，只能想想如何退群了。&lt;/p&gt;
&lt;h3 id=&#34;为什么要使用-golinkname&#34;&gt;为什么要使用 go:linkname&lt;/h3&gt;
&lt;p&gt;Kubernetes 社区之所以能落地 User Namespace 特性，是因为 Linux 社区在 2022 年引入了 &lt;a href=&#34;https://lwn.net/Articles/896255/&#34;&gt;ID-mapped mount&lt;/a&gt; 的概念。
原先，文件属性中的 UID 和 GID 是固定的，只有特定的用户或用户组才能访问相应的文件。
容器镜像中的文件大多是由 root 用户创建的，如果要让非特权用户访问镜像中的所有内容，过去唯一的办法是使用 &lt;code&gt;chown -R&lt;/code&gt; 来物理改变所有文件的 UID 和 GID。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Inodes&lt;/th&gt;
&lt;th&gt;overlayfs w/ metacopy&lt;/th&gt;
&lt;th&gt;overlayfs w/o metacopy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;tensorflow/tensorflow:latest&lt;/td&gt;
&lt;td&gt;1489MiB&lt;/td&gt;
&lt;td&gt;32596&lt;/td&gt;
&lt;td&gt;1.29s&lt;/td&gt;
&lt;td&gt;54.80s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;library/node:latest&lt;/td&gt;
&lt;td&gt;1425MiB&lt;/td&gt;
&lt;td&gt;33385&lt;/td&gt;
&lt;td&gt;1.18s&lt;/td&gt;
&lt;td&gt;52.86s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;library/ubuntu:22.04&lt;/td&gt;
&lt;td&gt;83.4MiB&lt;/td&gt;
&lt;td&gt;3517&lt;/td&gt;
&lt;td&gt;0.15s&lt;/td&gt;
&lt;td&gt;5.32s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;现在有了 ID-mapped mount 的功能后，原本只有 UID=0/GID=0 用户可以访问的目录，只需几次系统调用就可以让 UID=1001/GID=1001 的用户访问。
ID-mapped mount 在内核层面实现 ID 映射，比传统的 &lt;code&gt;chown -R&lt;/code&gt; 方法在性能和效率上都有显著提升。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-userns/mount-idmapped-bind.png&#34; alt=&#34;mount-idmapped-bind&#34;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NOTE: 具体细节可以查看之前的文章 - &lt;a href=&#34;https://fuweid.com/post/2023-containerd-17-userns/&#34;&gt;containerd 1.7: UserNamespace Stateless Pod&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不过，ID-mapped mount 需要提供 &lt;a href=&#34;https://man7.org/linux/man-pages/man7/user_namespaces.7.html&#34;&gt;user_namespace(7)&lt;/a&gt; 的句柄信息，而这种信息只能通过创建新进程（使用 CLONE_USER）来获取（/proc/$PID/ns/user 句柄）。
由于 GO 对多进程编程的支持有限，标准库中只有 &lt;a href=&#34;https://pkg.go.dev/os#StartProcess&#34;&gt;os.StartProcess&lt;/a&gt; 接口，而它采用了 fork 和 execve 组合的方式。
换句话说，多进程的 官方 方法只能通过调用命令行来实现，这要求开发者为每个进程逻辑准备一个独立的二进制文件。
为了简化运维，常见的做法是将每个独立逻辑封装成子命令，并通过调用自身的 &lt;code&gt;/proc/self/exe&lt;/code&gt; 来完成，这种方法被称为 &lt;code&gt;re-exec&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;然而，这种方法的成本太高。相当于每个引用了 ID-mapped mount 功能的程序都需要一个专门的命令行来创建新的 user_namespace。
因此，在 containerd 的 &lt;a href=&#34;https://github.com/containerd/containerd/blob/27de5fea738a38345aa1ac7569032261a6b1e562/pkg/sys/userns_unsafe_linux.go#L38&#34;&gt;GetUsernsFD&lt;/a&gt; 中，模拟了 os.StartProcess 的 fork 流程，并将最后的 execve 替换为 &lt;code&gt;syscall.RawSyscall(syscall.SYS_PPOLL, 0, 0, 0)&lt;/code&gt;，使子进程进入无限期的睡眠状态。
这样，父进程 containerd 在 fork 返回时，子进程已经运行在新的 user_namespace 中，父进程可以安全地获取子进程的 user_namespace 句柄，而无需担心进程间的保活问题。&lt;/p&gt;
&lt;p&gt;在整个 fork 过程中，子进程处于单线程模式，不具有完整的 GO runtime 逻辑。因此，需要使用 GO runtime 中的三个隐藏功能来屏蔽信号处理，并防止父子进程间发生栈大小的变化。这些隐藏功能只能通过 &lt;code&gt;//go:linkname&lt;/code&gt; 指令来激活。
在我看来，这本属于灵活使用，本身 GO 标准库都是这么玩的 :)。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;c1&#34;&gt;//go:linkname beforeFork syscall.runtime_BeforeFork
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;beforeFork&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;

&lt;span class=&#34;c1&#34;&gt;//go:linkname afterFork syscall.runtime_AfterFork
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;afterFork&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;

&lt;span class=&#34;c1&#34;&gt;//go:linkname afterForkInChild syscall.runtime_AfterForkInChild
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;afterForkInChild&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote&gt;
&lt;p&gt;当然，子进程也只能使用 RawSyscall 来完成初始化工作。这是为了避免进入不应该进入的流程，例如释放当前的执行线程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;替换方案-ptrace&#34;&gt;替换方案 ptrace&lt;/h3&gt;
&lt;p&gt;我并不想为了 fork 而引入一个子命令，所以给的选择也就只有 ptrace，如下面的代码所示。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;nx&#34;&gt;os&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;StartProcess&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;/proc/self/exe&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;containerd[getUsernsFD]&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;os&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ProcAttr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
	&lt;span class=&#34;nx&#34;&gt;Sys&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;syscall&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;SysProcAttr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
		&lt;span class=&#34;nx&#34;&gt;Cloneflags&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;  &lt;span class=&#34;nx&#34;&gt;unix&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;CLONE_NEWUSER&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
		&lt;span class=&#34;nx&#34;&gt;UidMappings&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;uidMaps&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
		&lt;span class=&#34;nx&#34;&gt;GidMappings&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;gidMaps&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
		&lt;span class=&#34;c1&#34;&gt;// NOTE: It&amp;#39;s reexec but it&amp;#39;s not heavy because subprocess
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;c1&#34;&gt;// be in PTRACE_TRACEME mode before performing execve.
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;nx&#34;&gt;Ptrace&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;    &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
		&lt;span class=&#34;nx&#34;&gt;Pdeathsig&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;syscall&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;SIGKILL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
	&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;根据 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/ptrace.2.html&#34;&gt;ptrace(2)&lt;/a&gt; 的文档说明，在替换完程序入口指令后，子进程在返回用户态前会进入停止状态，即 &lt;code&gt;tracing stop&lt;/code&gt;。
以下是命令 &lt;code&gt;retsnoop -e &#39;*_send_signal_locked*&#39; --comm exe -S&lt;/code&gt; 的输出示例。
在执行 &lt;a href=&#34;https://elixir.bootlin.com/linux/v6.5/source/fs/exec.c#L1803&#34;&gt;exec_binrpm&lt;/a&gt; 时，会触发 PTRACE_EVENT_EXEC 事件，然后在 &lt;code&gt;exit_to_user_mode_loop&lt;/code&gt; 中处理掉该信号，使得进程主动进入 &lt;code&gt;ptrace_stop&lt;/code&gt; 状态。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;07:42:51.340953 -&amp;gt; 07:42:51.340956 TID/PID 2854060/2854060 &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;exe/exe&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;:

              entry_SYSCALL_64_after_hwframe+0x73
              do_syscall_64+0x56
              x64_sys_call+0x680
              __x64_sys_execve+0x37
              do_execveat_common.isra.0+0x198
              bprm_execve+0x5e
              bprm_execve.part.0+0x17b
              exec_binprm+0x15a
              send_sig+0x29
              do_send_sig_info+0x63
              send_signal_locked+0xdd
!    2us &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;0&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;  __send_signal_locked


07:42:51.341038 -&amp;gt; 07:42:51.341043 TID/PID 2854060/2854060 &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;exe/exe&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;:

              entry_SYSCALL_64_after_hwframe+0x73
              do_syscall_64+0x63
              syscall_exit_to_user_mode+0x17
              exit_to_user_mode_prepare+0xf4
              exit_to_user_mode_loop+0xaa
              arch_do_signal_or_restart+0x2a
              get_signal+0x5c1
              ptrace_signal+0x4e
              ptrace_stop.part.0+0x246
              do_notify_parent_cldstop+0x17d
              send_signal_locked+0xdd
!    4us &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;0&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;  __send_signal_locked
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这与之前使用 poll 进入睡眠状态的本质是一样的，因为子进程并不需要执行具体的任务。
有了这种能力后，可以直接使用 re-exec，利用现成的二进制文件。所以在 os.StartProcess 中，第一个参数直接指定为 &lt;code&gt;/proc/self/exe&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;os.StartProcess 通过配置一个带有 CLOEXEC 标志的匿名管道的写端来与子进程通信，该函数会在子进程进入 execve 系统调用的 &lt;a href=&#34;https://elixir.bootlin.com/linux/v6.5/source/fs/file.c#L818&#34;&gt;do_close_on_exec&lt;/a&gt; 阶段后才返回。
也就是说，当 os.StartProcess 返回时，子进程已经运行在了新的 user_namespace 中。&lt;/p&gt;
&lt;p&gt;相比之前的方法，os.StartProcess 需要执行 execve 系统调用，这意味着它会读取程序的内容并解析入口指令的位置。虽然这种方法在成本上略高，但依然在可以接受的范围内。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;goos: linux
goarch: amd64
pkg: github.com/containerd/containerd/v2/core/mount
cpu: AMD Ryzen &lt;span class=&#34;m&#34;&gt;7&lt;/span&gt; 5800H with Radeon Graphics
                                    │ go122-golinkname │             go122-ptrace              │             go123-ptrace              │
                                    │      sec/op      │    sec/op     vs base                 │    sec/op     vs base                 │
BatchRunGetUsernsFD_Concurrent1-16        533.3µ ± ∞ ¹   739.6µ ± ∞ ¹        ~ &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1.000 &lt;span class=&#34;nv&#34;&gt;n&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; ²   723.3µ ± ∞ ¹        ~ &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1.000 &lt;span class=&#34;nv&#34;&gt;n&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; ²
BatchRunGetUsernsFD_Concurrent10-16       3.662m ± ∞ ¹   4.024m ± ∞ ¹        ~ &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1.000 &lt;span class=&#34;nv&#34;&gt;n&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; ²   3.957m ± ∞ ¹        ~ &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1.000 &lt;span class=&#34;nv&#34;&gt;n&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; ²
geomean                                   1.397m         1.725m        +23.45%                   1.692m        +21.06%
¹ need &amp;gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;6&lt;/span&gt; samples &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; confidence interval at level 0.95
² need &amp;gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;4&lt;/span&gt; samples to detect a difference at alpha level 0.05

                                    │ go122-golinkname │              go122-ptrace               │              go123-ptrace               │
                                    │       B/op       │     B/op       vs base                  │     B/op       vs base                  │
BatchRunGetUsernsFD_Concurrent1-16       1.118Ki ± ∞ ¹   3.855Ki ± ∞ ¹         ~ &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1.000 &lt;span class=&#34;nv&#34;&gt;n&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; ²   4.121Ki ± ∞ ¹         ~ &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1.000 &lt;span class=&#34;nv&#34;&gt;n&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; ²
BatchRunGetUsernsFD_Concurrent10-16      11.29Ki ± ∞ ¹   38.67Ki ± ∞ ¹         ~ &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1.000 &lt;span class=&#34;nv&#34;&gt;n&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; ²   41.36Ki ± ∞ ¹         ~ &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1.000 &lt;span class=&#34;nv&#34;&gt;n&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; ²
geomean                                  3.553Ki         12.21Ki        +243.65%                   13.06Ki        +267.43%
¹ need &amp;gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;6&lt;/span&gt; samples &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; confidence interval at level 0.95
² need &amp;gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;4&lt;/span&gt; samples to detect a difference at alpha level 0.05

                                    │ go122-golinkname │             go122-ptrace             │             go123-ptrace             │
                                    │    allocs/op     │  allocs/op   vs base                 │  allocs/op   vs base                 │
BatchRunGetUsernsFD_Concurrent1-16         43.00 ± ∞ ¹   68.00 ± ∞ ¹        ~ &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1.000 &lt;span class=&#34;nv&#34;&gt;n&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; ²   69.00 ± ∞ ¹        ~ &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1.000 &lt;span class=&#34;nv&#34;&gt;n&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; ²
BatchRunGetUsernsFD_Concurrent10-16        421.0 ± ∞ ¹   671.0 ± ∞ ¹        ~ &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1.000 &lt;span class=&#34;nv&#34;&gt;n&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; ²   682.0 ± ∞ ¹        ~ &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1.000 &lt;span class=&#34;nv&#34;&gt;n&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;1&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; ²
geomean                                    134.5         213.6        +58.76%                   216.9        +61.23%
¹ need &amp;gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;6&lt;/span&gt; samples &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; confidence interval at level 0.95
² need &amp;gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;4&lt;/span&gt; samples to detect a difference at alpha level 0.05
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;最后&#34;&gt;最后&lt;/h3&gt;
&lt;p&gt;在 &lt;a href=&#34;https://github.com/containerd/containerd/pull/10611&#34;&gt;core/mount: use ptrace instead of go:linkname&lt;/a&gt; 合并后，我第一时间给 GO 社区发起了退群请求 - &lt;a href=&#34;https://go-review.googlesource.com/c/go/+/609996&#34;&gt;609996: runtime: update comment for golinkname&lt;/a&gt;.&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>[CONT] Non-Preemptible RCU soft lockup: zap_pid_ns_processes</title>
          <link>/post/2024-rcu-soft-lockup-zap_pid_ns_processes-2/</link>
          <pubDate>Mon, 10 Jun 2024 14:00:45 +0800</pubDate>
          <guid>https://fuweid.com/post/2024-rcu-soft-lockup-zap_pid_ns_processes-2/</guid>
          <description>&lt;p&gt;接着上一篇 &lt;a href=&#34;https://fuweid.com/post/2024-rcu-soft-lockup-zap_pid_ns_processes-1/&#34;&gt;Non-Preemptible RCU soft lockup: zap_pid_ns_processes&lt;/a&gt;。
根据内核维护者的回复和修复补丁，已经确定是和 &lt;a href=&#34;https://man7.org/linux/man-pages/man7/io_uring.7.html&#34;&gt;io_uring(7)&lt;/a&gt; worker 唤醒机制有关。&lt;/p&gt;
&lt;h2 id=&#34;io_uring-io-wq-worker-pool&#34;&gt;io_uring io-wq worker pool&lt;/h2&gt;
&lt;p&gt;根据 &lt;a href=&#34;https://lwn.net/Articles/803070/&#34;&gt;Redesigned workqueues for io_uring&lt;/a&gt; 文章的描述，io_uring 使用 &lt;a href=&#34;https://docs.kernel.org/core-api/workqueue.html&#34;&gt;workqueue&lt;/a&gt; 处理异步 IO 请求有局限性，
因此在 &lt;a href=&#34;https://lore.kernel.org/linux-block/20191024134439.28498-1-axboe@kernel.dk/T/#m22034ebbbc1a932ddb412af354e2a3bb25da0e08&#34;&gt;io-wq: small threadpool implementation for io_uring&lt;/a&gt; 补丁里开始用线程池来替代 workqueue。&lt;/p&gt;
&lt;p&gt;当用户态进程通过 &lt;a href=&#34;https://www.man7.org/linux/man-pages/man2/io_uring_enter.2.html&#34;&gt;io_uring_enter(2)&lt;/a&gt; 系统调用提交 io 请求，io_uring 模块会将 io 请求放入到队列里，
并按需创建内核线程 &lt;a href=&#34;https://elixir.bootlin.com/linux/v5.15.160/source/io_uring/io-wq.c#L619&#34;&gt;io_wqe_worker&lt;/a&gt; 来处理请求，如下图的上半部分。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2024-rcu-soft-lockup-zap_pid_ns_processes/missing-manuals-io_uring-worker-pool-1.png&#34; alt=&#34;missing-manuals-io_uring-worker-pool-1&#34;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NOTE: 该图来源于 &lt;a href=&#34;https://blog.cloudflare.com/missing-manuals-io_uring-worker-pool&#34;&gt;https://blog.cloudflare.com/missing-manuals-io_uring-worker-pool&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;以该 &lt;a href=&#34;https://github.com/rlmenge/rcu-soft-lock-issue-repro/blob/662b8e414ff15d75419e2286b8121b7c2049a37c/npm/package.json#L4&#34;&gt;package.json&lt;/a&gt; 为例，nodejs-20.11.0 默认开启 io_uring，在 &lt;code&gt;npm run done&lt;/code&gt; 前，nodejs 会利用 &lt;a href=&#34;https://github.com/libuv/libuv/blob/eb5af8e3c0ea19a6b0196d5db3212dae1785739b/src/unix/linux.c#L1050&#34;&gt;uv__iou_fs_statx&lt;/a&gt; 函数来获取当前文件系统的信息。
这个 io_uring statx io 请求会触发内核创建 io_wqe_worker 线程。
那么我们看下 &lt;code&gt;npm run zombie&lt;/code&gt; 长时间运行的程序都有哪些线程。
如下图的下半部分为例，&lt;code&gt;1147997&lt;/code&gt; 进程有两个 &lt;code&gt;iou-wrk-1148006&lt;/code&gt; 的线程。
io_wqe_worker 线程会以调用 io_uring_enter 的线程 ID 来命名 &lt;code&gt;iou-wrk-%d&lt;/code&gt;，
因此 &lt;code&gt;iou-wrk-1148006&lt;/code&gt; 内核线程是由 &lt;code&gt;1148006&lt;/code&gt; 线程创建的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2024-rcu-soft-lockup-zap_pid_ns_processes/strace-k-npm-run-done.png&#34; alt=&#34;strace-k-npm-run-done&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2024-rcu-soft-lockup-zap_pid_ns_processes/npm-run-zombie-1.png&#34; alt=&#34;npm-run-zombie-1&#34;&gt;&lt;/p&gt;
&lt;p&gt;当创建 io_wqe_worker 的用户线程退出时，那么内核会在 &lt;a href=&#34;https://elixir.bootlin.com/linux/v5.15.160/source/include/linux/io_uring.h#L13&#34;&gt;do_exit.io_uring_files_cancel&lt;/a&gt; 函数里唤醒 worker 并告知它应该结束工作。&lt;/p&gt;
&lt;h2 id=&#34;tif_notify_signal&#34;&gt;TIF_NOTIFY_SIGNAL&lt;/h2&gt;
&lt;p&gt;就目前了解到细节来看，线程的信号处理更像是调度处理（以打标记为主）。
负责发送信号的线程除了添加信号信息外，它还需要把目标线程标记上 &lt;code&gt;TIF_SIGPENDING&lt;/code&gt;；
当目标线程准备从系统调用返回用户态时，目标线程会在 &lt;a href=&#34;https://elixir.bootlin.com/linux/v5.15.160/source/kernel/entry/common.c#L157&#34;&gt;exit_to_user_mode_loop&lt;/a&gt; 检查 &lt;code&gt;TIF_SIGPENDING&lt;/code&gt; 是否存在；
如有信号到来，那么线程将调用 &lt;a href=&#34;https://elixir.bootlin.com/linux/v5.15.160/source/kernel/entry/common.c#L149&#34;&gt;handle_signal_work&lt;/a&gt; 处理信号，它有可能会回到用户态，比如用户自定义的 SIGUSR1 信号处理。
同样，io_uring 也是通过添加 &lt;code&gt;TIF_SIGPENDING&lt;/code&gt; 标记的方式来唤醒 io_wqe_worker。&lt;/p&gt;
&lt;p&gt;但根据 &lt;a href=&#34;https://lore.kernel.org/lkml/20201008152752.218889-5-axboe@kernel.dk/&#34;&gt;task_work: use TIF_NOTIFY_SIGNAL if available&lt;/a&gt; 补丁来看，
早期 io_uring 通过 &lt;code&gt;jobctl&lt;/code&gt; 来进行信号标记，但这个方式需要对线程的 &lt;code&gt;task-&amp;gt;sighand&lt;/code&gt; 属性进行加锁；
当进程是多线程程序时，锁对性能的影响比较大。因此 io_uring 引入了新的标记 &lt;code&gt;TIF_NOTIFY_SIGNAL&lt;/code&gt; 避免锁的影响。&lt;/p&gt;
&lt;p&gt;当创建 io_wqe_worker 的线程退出后，那么该线程会把 &lt;code&gt;IO_WQ_BIT_EXIT&lt;/code&gt; 标记成 &lt;code&gt;1&lt;/code&gt;,
并将所有相关的 io_wqe_worker 线程都标记上 &lt;code&gt;TIF_NOTIFY_SIGNAL&lt;/code&gt; 并唤醒让其退出。&lt;/p&gt;
&lt;p&gt;但我们看看 io_wqe_worker 关键代码，整个过程里，该线程只有在 &lt;a href=&#34;https://elixir.bootlin.com/linux/v5.15.160/source/io_uring/io-wq.c#L524&#34;&gt;io_flush_signals&lt;/a&gt; 里有机会消除这个 &lt;code&gt;TIF_NOTIFY_SIGNAL&lt;/code&gt; 标记，
那么 io_wqe_worker 线程是有可能带着这个标记进入到 zap_pid_ns_processes 阶段，从而导致 CPU 无法运行其他线程。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// v5.15.160
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;io_wqe_worker&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;void&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
	&lt;span class=&#34;c1&#34;&gt;// ...
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;	&lt;span class=&#34;c1&#34;&gt;// IO_WQ_BIT_EXIT=1 表示 worker 应该结束工作
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;	&lt;span class=&#34;k&#34;&gt;while&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;test_bit&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;IO_WQ_BIT_EXIT&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;wq&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
		&lt;span class=&#34;c1&#34;&gt;// ...
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;
		&lt;span class=&#34;c1&#34;&gt;// 清除 TIF_NOTIFY_SIGNAL
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;io_flush_signals&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;())&lt;/span&gt;
			&lt;span class=&#34;k&#34;&gt;continue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;

		&lt;span class=&#34;c1&#34;&gt;// 没任务时，将进入睡眠态
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;n&#34;&gt;ret&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;schedule_timeout&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;WORKER_IDLE_TIMEOUT&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;

		&lt;span class=&#34;c1&#34;&gt;// 检查当前线程是否有以下两个标记
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;c1&#34;&gt;//   TIF_SIGPENDING
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;c1&#34;&gt;//   TIF_NOTIFY_SIGNAL
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;signal_pending&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;current&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
			&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ksignal&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ksig&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;

			&lt;span class=&#34;c1&#34;&gt;// 取出信号
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;get_signal&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ksig&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
				&lt;span class=&#34;k&#34;&gt;continue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
			&lt;span class=&#34;k&#34;&gt;break&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
		&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
		&lt;span class=&#34;c1&#34;&gt;// ...
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;	&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
	&lt;span class=&#34;c1&#34;&gt;// ...
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;	&lt;span class=&#34;c1&#34;&gt;// 调用 do_exit(0);
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;	&lt;span class=&#34;n&#34;&gt;io_worker_exit&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;worker&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt; 
&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;io_wqe_worker-thread-is-last-one-in-init&#34;&gt;io_wqe_worker thread is last one in init&lt;/h2&gt;
&lt;p&gt;现在有一个进程 X，它运行在新的 &lt;a href=&#34;https://man7.org/linux/man-pages/man7/pid_namespaces.7.html&#34;&gt;pid_namespace(7)&lt;/a&gt; 以及 &lt;a href=&#34;https://man7.org/linux/man-pages/man7/mount_namespaces.7.html&#34;&gt;mount_namespace(7)&lt;/a&gt; 里。
它只有两个线程，它们分别是 X-1 以及 X-iou-wrk-1。除此之外，X 还有一个孩子进程 Y。
进程 Y 和 X 运行在相同的 &lt;a href=&#34;https://man7.org/linux/man-pages/man7/namespaces.7.html&#34;&gt;namespace&lt;/a&gt; 下。&lt;/p&gt;
&lt;p&gt;进程 X 作为它所在 pid_namespace 里是 &lt;strong&gt;一号进程&lt;/strong&gt;。
当进程 X 主线程 X-1 退出时，它会给 X-iou-wrk-1 线程发送 SIGKILL 信号并唤醒它。
如果 X-iou-wrk-1 线程还没完全从 io_wqe_worker 上下文里结束，线程 X-1 有机会给 X-iou-wrk-1 标记上 &lt;code&gt;TIF_NOTIFY_SIGNAL&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;如果 X-1 提前结束, X-iou-wrk-1 将会是一号进程里唯一非 zombie 线程，
它将负责清理在这个 pid_namespace 下的所有进程，比如这里的进程 Y。
线程 X-iou-wrk-1 将进入 zap_pid_ns_processes 来清理进程 Y。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// v5.15.160
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;void&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;zap_pid_ns_processes&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pid_namespace&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pid_ns&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
	&lt;span class=&#34;c1&#34;&gt;// ...
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;
	&lt;span class=&#34;c1&#34;&gt;// 给当前 pid_namespace 下的所有线程发送 SIGKILL
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;	&lt;span class=&#34;c1&#34;&gt;// 根据前面的假设，这里只有进程 Y。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;	&lt;span class=&#34;n&#34;&gt;rcu_read_lock&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;
	&lt;span class=&#34;n&#34;&gt;read_lock&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tasklist_lock&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
	&lt;span class=&#34;n&#34;&gt;nr&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
	&lt;span class=&#34;n&#34;&gt;idr_for_each_entry_continue&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pid_ns&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;idr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pid&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;nr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
		&lt;span class=&#34;n&#34;&gt;task&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pid_task&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pid&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PIDTYPE_PID&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
		&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;task&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;__fatal_signal_pending&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;task&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
			&lt;span class=&#34;n&#34;&gt;group_send_sig_info&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;SIGKILL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SEND_SIG_PRIV&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;task&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PIDTYPE_MAX&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
	&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
	&lt;span class=&#34;n&#34;&gt;read_unlock&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;tasklist_lock&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
	&lt;span class=&#34;n&#34;&gt;rcu_read_unlock&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;

	&lt;span class=&#34;c1&#34;&gt;// 给进程 Y 发送 SIGKILL 后，还需要回收 Y
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;	&lt;span class=&#34;k&#34;&gt;do&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
		&lt;span class=&#34;c1&#34;&gt;// 清理掉 TIF_SIGPENDING
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;c1&#34;&gt;// 如果有新来的 TIF_SIGPENDING 信号
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;c1&#34;&gt;// 那么 kernel_wait4 就有机会释放 CPU
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;n&#34;&gt;clear_thread_flag&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;TIF_SIGPENDING&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
		&lt;span class=&#34;n&#34;&gt;rc&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;kernel_wait4&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;__WALL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;NULL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
	&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;while&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rc&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ECHILD&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;

	&lt;span class=&#34;c1&#34;&gt;// ...
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;a href=&#34;https://elixir.bootlin.com/linux/v5.15.160/source/kernel/exit.c#L1554&#34;&gt;kernel_wait4.do_wait&lt;/a&gt; 遇到没有可以回收的线程时，它会判断是否需要处理信号。
如果没有信号需要处理，那么它将调用 &lt;a href=&#34;https://elixir.bootlin.com/linux/v5.15.160/source/kernel/sched/core.c#L6452&#34;&gt;schedule&lt;/a&gt; 来释放当前 CPU。
在没有开启抢占模式的内核下，进入内核态的线程只能主动释放 CPU，比如结束系统调用或者主动调用 schedule。
不然它将长期霸占所在的 CPU。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// v5.15.160
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;long&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;do_wait&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;wait_opts&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;wo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
	
	&lt;span class=&#34;c1&#34;&gt;// ...
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;
&lt;span class=&#34;nl&#34;&gt;notask&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
	&lt;span class=&#34;n&#34;&gt;retval&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;wo&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;notask_error&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
	&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;retval&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;wo&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;wo_flags&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;WNOHANG&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
		&lt;span class=&#34;n&#34;&gt;retval&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ERESTARTSYS&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
		&lt;span class=&#34;c1&#34;&gt;// 没有可以回收线程，比如 D 状态的线程
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;c1&#34;&gt;// 假设没有信号需要处理，那么线程主动释放 CPU
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;signal_pending&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;current&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
			&lt;span class=&#34;n&#34;&gt;schedule&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;
			&lt;span class=&#34;k&#34;&gt;goto&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;repeat&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
		&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
	&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
 
	&lt;span class=&#34;c1&#34;&gt;// ...
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然而线程 X-iou-wrk-1 身上一直有 &lt;code&gt;TIF_NOTIFY_SIGNAL&lt;/code&gt; 标记，导致 &lt;a href=&#34;https://elixir.bootlin.com/linux/v5.15.160/source/include/linux/sched/signal.h#L361&#34;&gt;signal_pending&lt;/a&gt; 一直返回 &lt;code&gt;True&lt;/code&gt;, 它永远都走不到 &lt;code&gt;schedule&lt;/code&gt; 这个函数。
而进程 Y 收到信号后退出，它是最后一个引用 mount_namespace 的进程，
它将负责回收 mount_namespace 相关的资源，并进入到 &lt;code&gt;synchronize_rcu_expedited&lt;/code&gt; 函数。
而 &lt;code&gt;synchronize_rcu_expedited&lt;/code&gt; 要求所有 CPU 都要上报静止态，也就是至少发生过一次上下文切换。
但线程 X-iou-wrk-1 并不会释放 CPU，所以进程 Y 无法从 &lt;code&gt;synchronize_rcu_expedited&lt;/code&gt; 中离开从而长期处于 D 状态，形成了一个死锁。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# 进程 Y&lt;/span&gt;
$ sudo cat /proc/2522605/task/2522645/stack
&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;&amp;lt;0&amp;gt;&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; synchronize_rcu_expedited+0x177/0x1f0
&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;&amp;lt;0&amp;gt;&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; namespace_unlock+0xd6/0x1b0
&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;&amp;lt;0&amp;gt;&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; put_mnt_ns+0x73/0xa0
&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;&amp;lt;0&amp;gt;&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; free_nsproxy+0x1c/0x1b0
&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;&amp;lt;0&amp;gt;&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; switch_task_namespaces+0x5d/0x70
&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;&amp;lt;0&amp;gt;&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; exit_task_namespaces+0x10/0x20
&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;&amp;lt;0&amp;gt;&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; do_exit+0x2ce/0x500
&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;&amp;lt;0&amp;gt;&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; io_sq_thread+0x48e/0x5a0
&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;&amp;lt;0&amp;gt;&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; ret_from_fork+0x3c/0x60
&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;&amp;lt;0&amp;gt;&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; ret_from_fork_asm+0x1b/0x30

$ sudo cat /proc/2522605/task/2522645/status
Name: iou-sqp-2522605
State: D &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;disk sleep&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;c1&#34;&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;后续任何回收 mount_namespace 的操作都将卡死，只有重启系统才可以缓解。
这个问题的修复也比较简单，就是在进入 kernel_wait4 之前清理掉 &lt;code&gt;TIF_NOTIFY_SIGNAL&lt;/code&gt; 标记即可。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span class=&#34;gh&#34;&gt;diff --git a/kernel/pid_namespace.c b/kernel/pid_namespace.c
&lt;/span&gt;&lt;span class=&#34;gh&#34;&gt;index dc48fecfa1dc..25f3cf679b35 100644
&lt;/span&gt;&lt;span class=&#34;gh&#34;&gt;&lt;/span&gt;&lt;span class=&#34;gd&#34;&gt;--- a/kernel/pid_namespace.c
&lt;/span&gt;&lt;span class=&#34;gd&#34;&gt;&lt;/span&gt;&lt;span class=&#34;gi&#34;&gt;+++ b/kernel/pid_namespace.c
&lt;/span&gt;&lt;span class=&#34;gi&#34;&gt;&lt;/span&gt;&lt;span class=&#34;gu&#34;&gt;@@ -218,6 +218,7 @@ void zap_pid_ns_processes(struct pid_namespace *pid_ns)
&lt;/span&gt;&lt;span class=&#34;gu&#34;&gt;&lt;/span&gt; 	 */
 	do {
 		clear_thread_flag(TIF_SIGPENDING);
&lt;span class=&#34;gi&#34;&gt;+		clear_thread_flag(TIF_NOTIFY_SIGNAL);
&lt;/span&gt;&lt;span class=&#34;gi&#34;&gt;&lt;/span&gt; 		rc = kernel_wait4(-1, NULL, __WALL, NULL);
 	} while (rc != -ECHILD);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;summary&#34;&gt;Summary&lt;/h2&gt;
&lt;p&gt;如果你是从事容器开发的朋友，应该对函数 zap_pid_ns_processes 不陌生，遇到就只能献上重启大法 QQ。
本次分享的问题只有使用 io_uring 的容器应用才会遇到。对于 nodejs 而言，如果 io_uring 不是刚需，可以考虑用 &lt;a href=&#34;https://nodejs.org/api/cli.html#uv_use_io_uringvalue&#34;&gt;UV_USE_IO_URING=0&lt;/a&gt; 关闭掉。&lt;/p&gt;
&lt;p&gt;最后，根据内核大佬的说法，zap_pid_ns_processes 这个函数已经遇到过很多问题，不排除还有其他坑 T.T。&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>Non-Preemptible RCU soft lockup: zap_pid_ns_processes</title>
          <link>/post/2024-rcu-soft-lockup-zap_pid_ns_processes-1/</link>
          <pubDate>Thu, 06 Jun 2024 07:47:45 +0800</pubDate>
          <guid>https://fuweid.com/post/2024-rcu-soft-lockup-zap_pid_ns_processes-1/</guid>
          <description>&lt;p&gt;分享一个最近遇到的容器问题吧，虽然还不知道该怎么解决，但已经发信给内核社区寻求帮助了。&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://lore.kernel.org/linux-kernel/1386cd49-36d0-4a5c-85e9-bc42056a5a38@linux.microsoft.com/T/#u&#34;&gt;[RCU] zombie task hung in synchronize_rcu_expedited&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;这是一个关于 &lt;code&gt;zap_pid_ns_processes.kernel_wait4&lt;/code&gt; 死循环的问题，因为没有开启内核的 &lt;strong&gt;抢占模式&lt;/strong&gt;，
似乎处于 &lt;code&gt;zap_pid_ns_processes.kernel_wait4&lt;/code&gt; 调用栈的线程没法进行上下文切换，导致 &lt;code&gt;synchronize_rcu_expedited&lt;/code&gt; 卡死，
容器无法正常释放 mount_namespace。目前我可以在 &lt;code&gt;v5.15/v6.1/v6.5/v6.9/v6.10-rc2&lt;/code&gt; 内核版本上复现该问题，
而且在 v5.15 内核上尤其容易复现。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注: 编译条件可以查看文章的最后。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;non-preemptible-rcu-synchronize_rcu_expedited&#34;&gt;Non-Preemptible RCU synchronize_rcu_expedited&lt;/h2&gt;
&lt;p&gt;RCU 是一个极其复杂的话题，本节关于 RCU 的描述会存在不准确或者不正确的可能性。
本人仅在复现 soft lockup 问题上表达自己对不可抢占 RCU 的一些理解，对于更官方或者更准确的表达，应该查阅 &lt;a href=&#34;https://docs.kernel.org/RCU/index.html&#34;&gt;RCU 的官方文档&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;Read-Copy-Update 是内核里对并发读比较友好的同步机制。与传统的互斥锁不同，RCU 利用 CPU 上下文切换来反馈进程是否离开了临界区。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;n&#34;&gt;rcu_read_lock&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;span class=&#34;c1&#34;&gt;// RCU read-side critical section
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rcu_read_unlock&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;RCU 临界区内不可自行调用 &lt;code&gt;schedule&lt;/code&gt; 来进行 CPU 线程的上下文切换。
因此非抢占模式下的 CPU 线程发生上下文切换时，比如内核态返回用户态或者执行其他进程时，那么当前 CPU 一定离开了 RCU 临界区。
内核没有内存垃圾回收机制，因此当受 RCU 临界区保护的数据需要更新时，更新者要负责回收正在被 RCU 临界区访问的老数据。
因此 RCU 的更新操作分两个阶段：&lt;strong&gt;删除&lt;/strong&gt; 和 &lt;strong&gt;回收&lt;/strong&gt;。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// https://lwn.net/Articles/262464/#Publish-Subscribe%20Mechanism
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;
&lt;span class=&#34;n&#34;&gt;q&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;kmalloc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;sizeof&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;GFP_KERNEL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;q&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;span class=&#34;n&#34;&gt;q&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;b&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;span class=&#34;n&#34;&gt;q&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;c&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;span class=&#34;c1&#34;&gt;// Removal
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;list_replace_rcu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;list&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;q&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;list&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;span class=&#34;n&#34;&gt;synchronize_rcu&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;
&lt;span class=&#34;c1&#34;&gt;// Reclamation
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;kfree&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;p&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;删除操作本质上是赋值，表示当前的数据已指向新的内存地址。
但&lt;strong&gt;更新前的数据内存&lt;/strong&gt;可能正在被其他 CPU 线程访问，更新者无法直接释放这块内存，它需要等待所有 CPU 线程都离开了临界区。&lt;/p&gt;
&lt;p&gt;在具体实现细节里，每一个 CPU 线程都有 &lt;code&gt;Quiescent State&lt;/code&gt; 标志。
当 CPU 线程正在访问 RCU 临界区时，我们称之为 &lt;strong&gt;活跃态&lt;/strong&gt;；当 CPU 离开临界区后并发生上下文切换时，那么我们称之为 &lt;strong&gt;静止态&lt;/strong&gt;。
&lt;strong&gt;RCU 数据更新者&lt;/strong&gt; 在更新完数据后，它需要调用 &lt;code&gt;synchronize_rcu&lt;/code&gt; 函数等待所有 CPU 线程上报静止态，这段等待时长称之为 &lt;code&gt;Grace Period&lt;/code&gt; &lt;strong&gt;宽限期&lt;/strong&gt;。
如下图所示，CPU 1 和 CPU 2 分别离开 Reader 1-1 和 Reader 2-1 临界区后上报静止态；
而 CPU 3 启发宽限期时，CPU 0 已经属于空闲状态，因此后续 Reader 0-2 临界区访问的是新的数据内存，不存在释放内存的风险。
因此宽限期在 CPU 2 上报静止态后结束。 宽限期后并没有代码会继续访问老的数据内存，CPU 3 可以放心释放这段内存空间。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2024-rcu-soft-lockup-zap_pid_ns_processes/Grace-Period-v1.svg&#34; alt=&#34;grace-period&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;synchronize_rcu&lt;/code&gt; 主动等待其他 CPU 线程上报静止态，可能存在长延迟的情况。
内核提供了 &lt;code&gt;synchronize_rcu_expedited&lt;/code&gt; 函数来加速其他 CPU 线程上报静止态的效率。
&lt;code&gt;synchronize_rcu_expedited&lt;/code&gt; 会主动给 &lt;code&gt;non-idle no-hz&lt;/code&gt; CPU 线程发送 IPI 中断。
以 v5.15 内核代码为例，&lt;a href=&#34;https://elixir.bootlin.com/linux/v5.15.160/source/kernel/rcu/tree_exp.h#L821&#34;&gt;synchronize_rcu_expedited&lt;/a&gt; 会启动 kernel worker 去运行 &lt;a href=&#34;https://elixir.bootlin.com/linux/v5.15.160/source/kernel/rcu/tree_exp.h#L630&#34;&gt;wait_rcu_exp_gp&lt;/a&gt;, 然后等待所有的 CPU 线程上报静止态。
&lt;code&gt;wait_rcu_exp_gp&lt;/code&gt; 会调用 &lt;code&gt;smp_call_function_single&lt;/code&gt; 给 CPU 线程发送 IPI 中断，随后对应的 CPU 线程会进入到 &lt;code&gt;rcu_exp_handler&lt;/code&gt;。
在 &lt;a href=&#34;https://elixir.bootlin.com/linux/v5.15.160/source/kernel/rcu/tree_exp.h#L742&#34;&gt;rcu_exp_handler&lt;/a&gt; 的逻辑里，如果发现 CPU 线程原本属于空闲状态，那么它将立刻上报静止态。
如果不是，那么它将当前进程标记成可抢占，一旦该进程满足被抢占条件后，该 CPU 线程就会出现上下文切换并上报静止态。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// v5.15.160
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;
&lt;span class=&#34;cm&#34;&gt;/* Request an expedited quiescent state. */&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;void&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;rcu_exp_need_qs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;void&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
	&lt;span class=&#34;n&#34;&gt;__this_cpu_write&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rcu_data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cpu_no_qs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;exp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
	&lt;span class=&#34;cm&#34;&gt;/* Store .exp before .rcu_urgent_qs. */&lt;/span&gt;
	&lt;span class=&#34;n&#34;&gt;smp_store_release&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;this_cpu_ptr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rcu_data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rcu_urgent_qs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
	&lt;span class=&#34;n&#34;&gt;set_tsk_need_resched&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;current&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
	&lt;span class=&#34;n&#34;&gt;set_preempt_need_resched&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;

&lt;span class=&#34;cm&#34;&gt;/* Invoked on each online non-idle CPU for expedited quiescent state. */&lt;/span&gt;
&lt;span class=&#34;k&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;void&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;rcu_exp_handler&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;void&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;unused&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
	&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;rcu_data&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rdp&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;this_cpu_ptr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rcu_data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
	&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;rcu_node&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rnp&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;rdp&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;mynode&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;

	&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;READ_ONCE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rnp&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;expmask&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;rdp&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;grpmask&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt;
	    &lt;span class=&#34;n&#34;&gt;__this_cpu_read&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rcu_data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cpu_no_qs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;exp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
		&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
	&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rcu_is_cpu_rrupt_from_idle&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;())&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
		&lt;span class=&#34;n&#34;&gt;rcu_report_exp_rdp&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;this_cpu_ptr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;rcu_data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;
		&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
	&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
	&lt;span class=&#34;n&#34;&gt;rcu_exp_need_qs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;();&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;但如果目标 CPU 线程进入了内核态，而且内核关闭了可抢占的功能，那么 &lt;code&gt;synchronize_rcu_expedited&lt;/code&gt; 函数只能等待目标 CPU 线程自己切换上下文了。&lt;/p&gt;
&lt;h2 id=&#34;put_mnt_nssynchronize_rcu_expedited-stuck&#34;&gt;put_mnt_ns.synchronize_rcu_expedited stuck&lt;/h2&gt;
&lt;p&gt;现在我们使用的容器一般都会配置 &lt;a href=&#34;https://www.man7.org/linux/man-pages/man7/pid_namespaces.7.html&#34;&gt;pid_namespace(7)&lt;/a&gt; 和 &lt;a href=&#34;https://www.man7.org/linux/man-pages/man7/mount_namespaces.7.html&#34;&gt;mount_namespace(7)&lt;/a&gt; 分别做进程视图隔离以及挂载点隔离。
在新的 &lt;code&gt;pid_namespace PA&lt;/code&gt; 里，一旦一号进程退出，一号进程会在 &lt;a href=&#34;https://elixir.bootlin.com/linux/v5.15.160/source/kernel/exit.c#L708&#34;&gt;do_exit.exit_notify&lt;/a&gt; 里调用 &lt;a href=&#34;https://elixir.bootlin.com/linux/v5.15.160/source/kernel/pid_namespace.c#L166&#34;&gt;zap_pid_ns_processes&lt;/a&gt; 给 &lt;code&gt;PA&lt;/code&gt; 下的所有进程发送 SIGKILL 信号，
并主动回收僵尸进程。假设只有这个 &lt;code&gt;PA&lt;/code&gt; 下的进程使用了 &lt;code&gt;mount_namespace MA&lt;/code&gt;，那么当最后一个线程进入到 &lt;a href=&#34;https://elixir.bootlin.com/linux/v5.15.160/source/kernel/exit.c#L871&#34;&gt;do_exit.exit_task_namespaces&lt;/a&gt; 阶段时，
它回收 &lt;code&gt;mount_namespace MA&lt;/code&gt; 资源。&lt;a href=&#34;https://elixir.bootlin.com/linux/v5.15.160/source/fs/namespace.c#L1935&#34;&gt;put_mnt_ns.drop_collected_mounts&lt;/a&gt; 释放完资源后，它会调用 &lt;code&gt;synchronize_rcu_expedited&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;现在遇到的问题是，没有开启抢占模式的情况下，因为某种原因导致某个 CPU 线程卡在 &lt;code&gt;zap_pid_ns_processes.kernel_wait4&lt;/code&gt; 阶段。
那么这将导致调用 &lt;code&gt;synchronize_rcu_expedited&lt;/code&gt; 的线程也将进入 &lt;code&gt;Disk Sleep&lt;/code&gt; 状态。因为内核并不允许并发调用 &lt;code&gt;synchronize_rcu_expedited&lt;/code&gt;，其他容器在退出时，那么必定会有线程进入到 &lt;a href=&#34;https://elixir.bootlin.com/linux/v5.15.160/source/kernel/rcu/tree_exp.h#L280&#34;&gt;exp_funnel_lock&lt;/a&gt; 等锁状态。&lt;/p&gt;
&lt;p&gt;为了方便复现这个卡顿问题，我们使用 GO 程序来 re-exec 自身来构造出以下进程树。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;unshare(CLONE_NEWPID|CLONE_NEWNS)     [Start]
		 |
		 |
		 v
rcudeadlock task &amp;amp;&amp;amp; rcudeadlock start [Entrypoint]
		 |
		 |
		 v
rcudeadlock start                     [PID 1 and exit]
   |___ rcudeadlock zombie
         |__ bash -c &amp;quot;while true; echo zombie; sleep 1; done&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;以下使用 Ubuntu v5.15.0-1064-azure 内核复现的日志，可以看到 &lt;code&gt;rcudeadlock start (pid 2928)&lt;/code&gt; 进入 &lt;code&gt;kernel_wait4&lt;/code&gt; 状态，并长时间霸占 &lt;code&gt;CPU #2&lt;/code&gt; 线程上；而它的孩子进程 &lt;code&gt;sleep (pid 3141)&lt;/code&gt; 等待在了 &lt;code&gt;synchronize_rcu_expedited&lt;/code&gt;。如果后续还有新的进程需要回收 mount_namespace，那么这些进程都将卡在 &lt;code&gt;synchronize_rcu_expedited&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2024-rcu-soft-lockup-zap_pid_ns_processes/deadlock-dmesg-log.png&#34; alt=&#34;deadlock-dmesg-log&#34;&gt;&lt;/p&gt;
&lt;p&gt;目前还不知道具体原因，但比较有意思的是，一旦在 &lt;code&gt;rcudeadlock&lt;/code&gt; 进程中引入 &lt;a href=&#34;https://man7.org/linux/man-pages/man7/io_uring.7.html&#34;&gt;io_uring(7)&lt;/a&gt; 空闲线程后，这个问题就特别容易复现。&lt;/p&gt;
&lt;h2 id=&#34;how-to-build-kernel-to-reproduce-this-issue&#34;&gt;How to build kernel to reproduce this issue?&lt;/h2&gt;
&lt;p&gt;编译内核需要关闭 &lt;code&gt;CONFIG_PREEMPT&lt;/code&gt; 以及 &lt;code&gt;CONFIG_PREEMPT_RCU&lt;/code&gt;，大致如下图所示。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;$ cat /boot/config-5.15.0-1064-azure &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; grep _RCU
&lt;span class=&#34;nv&#34;&gt;CONFIG_TREE_RCU&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;y
&lt;span class=&#34;c1&#34;&gt;# CONFIG_RCU_EXPERT is not set&lt;/span&gt;
&lt;span class=&#34;nv&#34;&gt;CONFIG_TASKS_RCU_GENERIC&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;y
&lt;span class=&#34;nv&#34;&gt;CONFIG_TASKS_RUDE_RCU&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;y
&lt;span class=&#34;nv&#34;&gt;CONFIG_TASKS_TRACE_RCU&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;y
&lt;span class=&#34;nv&#34;&gt;CONFIG_RCU_STALL_COMMON&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;y
&lt;span class=&#34;nv&#34;&gt;CONFIG_RCU_NEED_SEGCBLIST&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;y
&lt;span class=&#34;nv&#34;&gt;CONFIG_RCU_NOCB_CPU&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;y
&lt;span class=&#34;nv&#34;&gt;CONFIG_MMU_GATHER_RCU_TABLE_FREE&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;y
&lt;span class=&#34;c1&#34;&gt;# CONFIG_RCU_SCALE_TEST is not set&lt;/span&gt;
&lt;span class=&#34;c1&#34;&gt;# CONFIG_RCU_TORTURE_TEST is not set&lt;/span&gt;
&lt;span class=&#34;c1&#34;&gt;# CONFIG_RCU_REF_SCALE_TEST is not set&lt;/span&gt;
&lt;span class=&#34;nv&#34;&gt;CONFIG_RCU_CPU_STALL_TIMEOUT&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;60&lt;/span&gt;
&lt;span class=&#34;c1&#34;&gt;# CONFIG_RCU_TRACE is not set&lt;/span&gt;
&lt;span class=&#34;c1&#34;&gt;# CONFIG_RCU_EQS_DEBUG is not set&lt;/span&gt;

$ cat /boot/config-5.15.0-1064-azure &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; grep _PREEMPT
&lt;span class=&#34;c1&#34;&gt;# CONFIG_PREEMPT_NONE is not set&lt;/span&gt;
&lt;span class=&#34;nv&#34;&gt;CONFIG_PREEMPT_VOLUNTARY&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;y
&lt;span class=&#34;c1&#34;&gt;# CONFIG_PREEMPT is not set&lt;/span&gt;
&lt;span class=&#34;nv&#34;&gt;CONFIG_HAVE_PREEMPT_DYNAMIC&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;y
&lt;span class=&#34;nv&#34;&gt;CONFIG_PREEMPT_NOTIFIERS&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;y
&lt;span class=&#34;nv&#34;&gt;CONFIG_DRM_I915_PREEMPT_TIMEOUT&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;640&lt;/span&gt;
&lt;span class=&#34;c1&#34;&gt;# CONFIG_PREEMPTIRQ_DELAY_TEST is not set&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;目前在 8 vcores 环境下，运行 &lt;a href=&#34;https://github.com/rlmenge/rcu-soft-lock-issue-repro/blob/main/rcudeadlock.go&#34;&gt;rcudeadlock&lt;/a&gt; 容易复现。在 v5.15 内核环境下，大概就几分钟的时间就可以复现。而在 &lt;strong&gt;&amp;gt;= v6.5&lt;/strong&gt; 环境下复现时间不固定，有时候需要花几个小时, v6.10-rc2 内核依旧可以复现。&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>脏页大小 - 18446744073709551614 ?!</title>
          <link>/post/2024-dirtypage-2/</link>
          <pubDate>Thu, 11 Apr 2024 20:48:45 +0800</pubDate>
          <guid>https://fuweid.com/post/2024-dirtypage-2/</guid>
          <description>&lt;p&gt;分享一个 &lt;a href=&#34;https://github.com/etcd-io/etcd/issues/17615&#34;&gt;etcd-io/etcd@17615&lt;/a&gt; 问题：用户发现 etcd 进程写入 WAL 日志有抖动，但是磁盘压力并不大，而且 fsync 系统调用都很正常；
后来通过 Tracing 工具定位到了内核 memory-cgroup 脏页数据统计有问题，不过该问题仅会出现在 v5.15.0-63 小版本。
&lt;a href=&#34;https://github.com/etcd-io/etcd/issues/17615#issuecomment-2017512594&#34;&gt;17615&lt;/a&gt; 问题单里有详尽的定位过程，推荐看看。
因为这个问题，我翻阅了相关的内核 PATCH 邮件，做了以下的脏页分配限流 (简单) 总结。&lt;/p&gt;
&lt;h2 id=&#34;脏页分配限流---balance_dirty_pages&#34;&gt;脏页分配限流 - balance_dirty_pages&lt;/h2&gt;
&lt;p&gt;回写 (Writeback) 是指内核负责将脏页 (DirtyPage) 刷入存储设备上，它涉及到脏页分配控制。&lt;/p&gt;
&lt;p&gt;在 v4.2 版本之前，脏页分配更多是由 memory-cgroup 来控制；而脏页流控核心 - &lt;a href=&#34;https://elixir.bootlin.com/linux/v5.15/source/mm/page-writeback.c#L1560&#34;&gt;balance_dirty_pages&lt;/a&gt; - 根据全局的脏页水位情况来决定是否需要控速。
在 &lt;a href=&#34;https://lwn.net/Articles/648292/&#34;&gt;LinuxCon Japan 2015 会议&lt;/a&gt;上，内核开发者 Tejun Heo 认为，在没有感知到全局脏页水位的情况，memory-cgroup 无法对脏页进行有效的控制。
核心 balance_dirty_pages 函数通过 &lt;code&gt;vm.dirty[_background]_{ratio, bytes}&lt;/code&gt; 参数来计算全局脏页的水位上限。
Tejun Heo 认为普通的 memory-cgroup 也应采用同样的方式来计算组内的脏页水位上限，而根组 memory-cgroup 只不过是退化到全局模式，当然全局脏页的水位优先级更高。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2024-dirtypage--2/stack.png&#34; alt=&#34;stack&#34;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NOTE: &lt;code&gt;balance_dirty_pages_ratelimited&lt;/code&gt; 会调用 &lt;code&gt;balance_dirty_pages&lt;/code&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 &lt;a href=&#34;https://lore.kernel.org/lkml/1420579582-8516-9-git-send-email-tj@kernel.org/&#34;&gt;writeback: cgroup writeback support&lt;/a&gt; PATCH 合并到 v4.2 之后，每个 memory-cgroup 都有独立的设备回写控制器 (bdi_writeback)，它用于执行回写操作。
回写控制器维护写入带宽 - &lt;a href=&#34;https://elixir.bootlin.com/linux/v5.15/source/include/linux/backing-dev-defs.h#L136&#34;&gt;dirty_ratelimit&lt;/a&gt; - ，初始状态下为 100 MiB/s。假设当前脏页大小为 dirty 和同时有 N 个线程在写入，该线程需要等待的时长大约为&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pause = dirty / (dirty_ratelimit / N)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在 &lt;a href=&#34;https://lore.kernel.org/all/20110806094526.878435971@intel.com/&#34;&gt;writeback: dirty rate control&lt;/a&gt; PATCH 里，内核开发者 Wu Fengguang 给出了 &lt;code&gt;N = roundup_pow_of_two(1 + HZ / 8)&lt;/code&gt; 的计算方法；
自 2011 年以来，内核一直在使用该方法计算 N。
让线程停顿是为了防止脏页增长过快。但如果整体可用内存水位状态良好，内核应该尽量避免不必要的停顿，因此脏页水位有三个状态 freerun, setpoint 和 limit。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;threshold = vm.dirty_ratio * total_available_memory 
background_threshold = vm.dirty_background_ratio * total_available_memory

* freerun = (threshold + background_threshold) / 2
* limit = threshold
* setpoint = (freerun + limit) / 2
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当脏页水位线低于 freerun 时，那么线程并不需要停顿，内存允许脏页快速增长；当脏页水位高于 limit 时，内核将通过 sleep(pause) 来禁止线程写入新的脏页，内核需要通过回写来降低水位线到安全的位置。
为了更好地计算停顿时长，内核引入了线程级别的带宽概念 &lt;code&gt;task_ratelimit&lt;/code&gt;: 利用水位状态 &lt;code&gt;pos_ratio&lt;/code&gt; 来调节写入带宽。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* pos_ratio(dirty) = 1.0 + ((setpoint - dirty) / (limit - setpoint) ) ^3 
* task_ratelimit(dirty) = dirty_ratelimit * pos_ratio(dirty)

* pause = dirty / (task_ratelimit / N)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2024-dirtypage--2/pos_ratio.png&#34; alt=&#34;pos_ratio&#34;&gt;&lt;/p&gt;
&lt;p&gt;内核还会根据 IO 回写控制器的情况来微调 &lt;code&gt;pos_ratio&lt;/code&gt;，尤其是在内存较大的机器上能充分利用内存优势，
通过维持较高的写入带宽来迅速降低脏页水位线，细节可以查询 &lt;a href=&#34;https://lore.kernel.org/all/20110806094526.733282037@intel.com/&#34;&gt;writeback: dirty position control&lt;/a&gt; PATCH。&lt;/p&gt;
&lt;h2 id=&#34;memory-cgroup-脏页状态更新---rstat&#34;&gt;memory-cgroup 脏页状态更新 - rstat&lt;/h2&gt;
&lt;p&gt;当进程跑在非根组的 memory-cgroup 时，balance_dirty_pages 需要从对应 memory-cgroup 获取当前的脏页大小。&lt;/p&gt;
&lt;p&gt;2019 年 &lt;a href=&#34;https://lore.kernel.org/lkml/20190412151507.2769-4-hannes@cmpxchg.org/&#34;&gt;mm: memcontrol: make cgroup stats and events query API explicitly local&lt;/a&gt; PATCH 让内核在更新侧 (内存的分配和释放) 做 per-cpu 的批量数据聚合，减少读取侧因遍历 memory-cgroup 树形结构测带来的 CPU 消耗。
假设每次更新 32 个页的数据，在 32 CPU 下运行着 32 个 memory-cgroup，那么最大误差会在 128 MiB 左右。
为了解决误差和读取效率问题，内核开发者 Johannes Weiner 在 &lt;a href=&#34;https://lore.kernel.org/linux-mm/20210202184746.119084-7-hannes@cmpxchg.org/&#34;&gt;mm: memcontrol: switch to rstat&lt;/a&gt; PATCH 里引入了 rstat 框架:
更新侧仅需为祖辈们维护 pending-update cgroup 队列，而读取侧仅选择性地做数据聚合，减少了不必要的遍历。&lt;/p&gt;
&lt;p&gt;假设当前 memory-cgroup 结构为 &lt;code&gt;root -&amp;gt; A -&amp;gt; B -&amp;gt; C&lt;/code&gt;，当线程在 C 内分配了内存，那么内核在更新 C 的同时，它也会标记 B, A, root 为待更新状态；
当有线程需要读取 C 状态时，那么读取侧需要将 B, A, root 也更新了，才能保证 &lt;code&gt;root -&amp;gt; A -&amp;gt; B -&amp;gt; C&lt;/code&gt; 链路上的数据是一致的。
如果新增了 &lt;code&gt;root -&amp;gt; A -&amp;gt; D -&amp;gt; E&lt;/code&gt; memory cgroup, 但 D, E 没有数据的变化，那么在读取 C 的数据时，内核并不会遍历 D, E。
rstat 架构的选择性聚合优化了读取效率和准确性。关于 rstat 的更多细节可以查看 &lt;a href=&#34;https://lpc.events/event/16/contributions/1240/&#34;&gt;Linux Plumbers Conferences 2022 - cgroup rstat&amp;rsquo;s advanced adoption&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id=&#34;脏页大小---18446744073709551614-&#34;&gt;脏页大小 - 18446744073709551614 ？&lt;/h2&gt;
&lt;p&gt;回到 ETCD 遇到的这个问题上。
在更新 memory-cgroup 字段时，线程仅更新当前 CPU 的 &lt;code&gt;memcg-&amp;gt;vm_stats_percpu-&amp;gt;state&lt;/code&gt; 上，由读取侧调用 &lt;a href=&#34;https://elixir.bootlin.com/linux/v5.15/source/kernel/cgroup/rstat.c#L148&#34;&gt;cgroup_rstat_flush_locked&lt;/a&gt; 函数来做 CPU 级别的数据聚合。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2024-dirtypage--2/cgroup_rstat_flush_locked.png&#34; alt=&#34;cgroup_rstat_flush_locked&#34;&gt;&lt;/p&gt;
&lt;p&gt;那么问题来了，假设有 32 个 CPU，当 &lt;code&gt;cgroup_rstat_flush_locked&lt;/code&gt; 读取第三个 CPU 上的数据时，第一个 CPU 产生了新的脏页，然后被第 30 个 CPU 刷盘了。
错过了第一个 CPU 产生的增量，导致在那个时刻的统计结果里脏页是负数。虽然状态最终是正确的，但负数被转化成 unsigned long 将变成非常大。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;k&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;kr&#34;&gt;inline&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;unsigned&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;long&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;memcg_page_state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;mem_cgroup&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;memcg&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;idx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
        &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;READ_ONCE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;memcg&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;vmstats&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;idx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]);&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;根据前面提到的停顿时长计算公式，线程相当于无限期停服了。
好在内核允许的最大停顿时间为 200 ms，下一轮检查大概率就恢复正常了。
&lt;a href=&#34;https://lore.kernel.org/all/20220817172139.3141101-1-shakeelb@google.com/&#34;&gt;Revert &amp;ldquo;memcg: cleanup racy sum avoidance code&lt;/a&gt; PATCH 修复也非常简单，就是回滚之前的一个 PATCH。&lt;/p&gt;
&lt;p&gt;我在一个 CPU 32 vcores - Memory 64 GiB 虚拟机上复现了这个问题：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;* 单实例 ETCD 运行在 /sys/fs/cgroup/testing1 里，并使用 ETCD benchmark 疯狂发写请求
* 反复在 /sys/fs/cgroup/testing2 里运行 dd 来产生大量的脏页，迫使 ETCD 进程进入 balance_dirty_pages
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;通过 Tracing Event 日志发现，脏页大小为 18446744073709551614 (-2)。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2024-dirtypage--2/dirtypage=-2.png&#34; alt=&#34;dirtypage=-2&#34;&gt;&lt;/p&gt;
&lt;p&gt;核数越多，越容易遇到这个问题。&lt;/p&gt;
&lt;h2 id=&#34;最后&#34;&gt;最后&lt;/h2&gt;
&lt;p&gt;博客停更好久了，有时间就多写写吧。&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>同步近期 containerd 的高频问题</title>
          <link>/post/2023-08-sync-containerd-issue/</link>
          <pubDate>Sun, 13 Aug 2023 23:48:45 +0800</pubDate>
          <guid>https://fuweid.com/post/2023-08-sync-containerd-issue/</guid>
          <description>&lt;p&gt;最近 &lt;a href=&#34;https://github.com/containerd/containerd/issues/8698&#34;&gt;Issue 8698&lt;/a&gt; 有用户说容器启动和清理都偏慢，尤其多个 Pod 同时启动时现象特别明显。之前有过类似的问题: containerd 启动容器前，它需要临时挂载 rootfs 来读取 uid/gid 信息。因为挂载的是可写属性的 overlayfs，卸载时内核会强制刷盘。当系统大量的脏页数据需要回写时，这个刷盘动作容易造成系统卡顿。 &lt;a href=&#34;https://github.com/containerd/containerd/pull/6478&#34;&gt;oci: use readonly mount to read user/group info&lt;/a&gt; 已经解决读取 uid/gid 的性能问题了，但这一次是 Pod Init-container 带来的 。&lt;/p&gt;
&lt;p&gt;Init-container rootfs 大部分都是可写模式的 overlayfs，如果 Init-container 是做数据预下载的话，那么 containerd 在删除 Init-container 时，内核一定会刷盘。在大部分场景下，同一个节点上的 Pod 共享同一块数据盘，这种不预期的刷盘很容易把系统打崩。还有 &lt;a href=&#34;https://github.com/containerd/containerd/issues/8647&#34;&gt;Issue 8647&lt;/a&gt; 用户说，他的系统一开始还好好的，跑几天就不稳定了。后来查看他提供的日志，发现有几个 Pod 一直启动失败，相当于每隔几秒都要去刷盘，导致整个系统不稳定。&lt;/p&gt;
&lt;p&gt;这个问题的最佳解决方案应该是做好 Pod 的存储隔离，但显然这成本确要高不少。然而 Kubernetes 场景下的容器并不会重启，即使在「失败后无限重启」的策略下，kubelet 依然是删除重建，这也意味着容器 rootfs 并不需要持久化。个人觉得，成本最低的解决方案应该是使用 &lt;a href=&#34;https://docs.kernel.org/filesystems/overlayfs.html#volatile-mount&#34;&gt;overlayfs-volatile-mount&lt;/a&gt;，它需要 Linux Kernel ≥ 5.10。以下是个人目前了解到的情况，大部分云厂家都支持了 overlayfs-volatile。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Azure Ubuntu 22.04 LTS - Kernel 5.15 (GA)&lt;/li&gt;
&lt;li&gt;AWS Kubernetes ≥ 1.24 使用 Kernel 5.10&lt;/li&gt;
&lt;li&gt;阿里云 Alibaba Cloud Linux 2 使用 Kernel 4.19，但它支持 volatile&lt;/li&gt;
&lt;li&gt;Google 支持的发行版比较多，包含了 Ubuntu 22.04 TLS - Kernel 5.15&lt;/li&gt;
&lt;li&gt;华为云 Huawei Cloud EulerOS 2.0 使用 Kernel 5.10&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;除此之外，还有 &lt;a href=&#34;https://github.com/containerd/containerd/issues/7496&#34;&gt;Issue 7496&lt;/a&gt; &lt;a href=&#34;https://github.com/containerd/containerd/issues/8931&#34;&gt;Issue 8931&lt;/a&gt; 用户报告说 umount 刷盘耗时太长导致 containerd-shim 泄露。其实这个问题的根因并不在于 umount，而是 containerd 清理 shim 的流程忽略了一些关键错误，导致上层调用者 (比如 CRI 插件) 没有重试机会，进而出现了 shim 泄露问题。&lt;a href=&#34;https://github.com/containerd/containerd/pull/8954&#34;&gt;PR 8954&lt;/a&gt; 仅修复 umount 超时带来的泄露，但可能会出现因 shim.Shutdown 超时带来的泄露。要完全修复 shim 泄露问题，containerd 应该让上层调用者发起删除，而不是通过异步来做清理。这里涉及到 containerd event 的可靠性以及上游 moby/moby 使用的调整，估计要讨论上一段时间，所以 PR 8954 也仅是降低泄露风险。&lt;/p&gt;
&lt;p&gt;如果有遇到该问题的朋友，可以关注下  &lt;a href=&#34;https://github.com/containerd/containerd/pull/8961&#34;&gt;Cherry-pick: [overlay] add configurable mount options to overlay snapshotter&lt;/a&gt; ，尽量使用 volatile 来避免刷盘带来的影响。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;containerd v1.7 系列文章的最后一篇被我鸽了，考虑到最近华为云提议的 &lt;a href=&#34;https://github.com/containerd/containerd/pull/8268&#34;&gt;Sandbox: make sandbox controller plugin&lt;/a&gt; 特性还在讨论阶段，等到 v2.0 在补一篇好了。下次一定！&lt;/p&gt;
&lt;/blockquote&gt;
</description>
        </item>
        
        <item>
          <title>containerd 1.7: 垃圾回收的拓展</title>
          <link>/post/2023-containerd-17-gc/</link>
          <pubDate>Sun, 19 Mar 2023 19:48:45 +0800</pubDate>
          <guid>https://fuweid.com/post/2023-containerd-17-gc/</guid>
          <description>&lt;p&gt;之前和苦总/Ace 维护 pouch-container/containerd 的时候，我们遇到比较多的问题是资源泄漏，比如节点负载太高以至于无法 umount 容器根目录(容器 bundle 目录残留），内核 pidns 死锁问题导致容器进程僵尸态( cgroup 残留)，还有进程重启导致 CNI 网络资源泄漏等等。对于内核死锁等问题，重启可能是唯一的解决方案；而那些因为短期高负载或者进程重启导致的资源泄漏，它们需要从系统层面解决，至少应该让资源的创建者有机会去清理。&lt;/p&gt;
&lt;p&gt;containerd 它本身就具备垃圾回收能力，但它只关注内部资源的清理，比如镜像数据以及容器可写层。而 CNI 网络资源以及 containerd-shim 等外部资源并不在垃圾回收的管理范围内，这部分资源容易出现泄漏的情况，比如 GKE 平台的用户就多次遇到了 &lt;a href=&#34;https://github.com/containerd/containerd/issues/5768&#34;&gt;Containerd IP leakage&lt;/a&gt; : 直到 pause 容器创建之前，containerd 仅在内存里保持对网络资源的引用；在将网络资源绑定给 pod 以前，containerd 一旦被强制重启就会发生泄漏。当时 containerd 社区的处理方式是提前创建 pod 记录，以此来提前关联网络资源，并利用 kubelet 的垃圾回收机制来触发资源清理。问题倒是解决了，但该方案仅适用于 kubernetes 场景，最合理的方案应该是创建者应具备周期性的清理流程。&lt;/p&gt;
&lt;p&gt;考虑我们还有 shim 资源泄漏的问题，containerd 社区提了 &lt;a href=&#34;https://github.com/containerd/containerd/pull/6804&#34;&gt;Add collectible resources to metadata gc&lt;/a&gt; 方案来管理外部资源清理。在介绍这个方案之前，我想先介绍下 containerd 的垃圾清理机制。&lt;/p&gt;
&lt;h2 id=&#34;基于标签系统和-lease-构建的垃圾回收&#34;&gt;基于标签系统和 lease 构建的垃圾回收&lt;/h2&gt;
&lt;p&gt;containerd 核心插件 metadata 管理着容器和镜像数据，如下图所示，其中虚线方块代表着子模块，比如 Images 代表着 image service，它仅用来管理镜像名字和镜像 manifest 的映射关系，而镜像的 blob 数据和解压后的文件内容分别由 content service 和 snapshot service 管理；需要说明的是，container service 仅用来保存用于启动容器的配置信息，它并不负责管理容器进程的生命周期。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-gc/metadata-overview.png&#34; alt=&#34;metadata-overview&#34;&gt;&lt;/p&gt;
&lt;p&gt;上图的方向键代表着数据之间的关联性，但除了 image/container 可以直接指向其相关联的数据外，content 数据间的关系以及 content 和 snapshot 数据间的关系都将通过标签系统来维系。&lt;/p&gt;
&lt;p&gt;根据 OCI 镜像标准的定义，镜像 manifest 的数据内容会关联其配置信息 config 以及 layer 的数据。在 containerd content service 里，这些数据的关系将由标签 &lt;strong&gt;containerd.io/gc.ref.content.&lt;/strong&gt; 来呈现，如下图的所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-gc/content-label.png&#34; alt=&#34;content-label&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;busybox:1.25&lt;/strong&gt; 镜像名字关联着一个哈希值为 &lt;strong&gt;sha256:29f5d56d1&amp;hellip;&lt;/strong&gt; 的 manifest，而这个 manifest 身上有两个重要的标签，如下表所示，它们分别指向了 config 和 layer 哈希值。由于每一层 layer 数据都对应着一个 snapshot，而这些 snapshot ID 是来自 config 里的 &lt;a href=&#34;https://github.com/opencontainers/image-spec/blob/v1.0/config.md#layer-diffid&#34;&gt;diffID&lt;/a&gt; 所生成的 &lt;a href=&#34;https://github.com/opencontainers/image-spec/blob/v1.0/config.md#layer-chainid&#34;&gt;chainID&lt;/a&gt;。为了管理方便，containerd 在 config 上添加标签 &lt;strong&gt;containerd.io/gc.ref.snapshot.X&lt;/strong&gt; 来关联最后一层 snapshot，其中 &lt;strong&gt;X&lt;/strong&gt; 表示具体的 snapshotter 插件，比如 linux 平台常用的 overlayfs。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Ref&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;containerd.io/gc.ref.content.config&lt;/td&gt;
&lt;td&gt;sha256:e02e811dd&amp;hellip;&lt;/td&gt;
&lt;td&gt;config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;containerd.io/gc.ref.content.l.0&lt;/td&gt;
&lt;td&gt;sha256:56bec22e3&amp;hellip;&lt;/td&gt;
&lt;td&gt;layer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;containerd.io/gc.ref.content.l.N&lt;/strong&gt; 用来指向具体的镜像层，&lt;strong&gt;N&lt;/strong&gt; 代表的是第几层。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;现在我们回头看第一张图，container/image 的元数据就可以作为垃圾回收的 GC-Root，containerd 配合标签系统进行全局扫描，并通过 &lt;a href=&#34;https://en.wikipedia.org/wiki/Tracing_garbage_collection#Tri-color_marking&#34;&gt;Tricolor&lt;/a&gt; 来获得正在使用的数据，那些没有被关联到的数据就可以被删除。&lt;/p&gt;
&lt;p&gt;比如我们将 redis:latest 镜像删除之后，redis:latest 镜像关联的 manifest &lt;strong&gt;sha256:78CF547&amp;hellip;&lt;/strong&gt; 没有可达的路径；同样地，redis:latest 镜像的 config &lt;strong&gt;sha256:B5CC793&amp;hellip;&lt;/strong&gt; 也没有可达路径。但图中的 redis:latest 镜像是基于 alpine:latest 构建，所以 redis:latest 镜像的第一层 &lt;strong&gt;sha256:3B87DCE&amp;hellip;&lt;/strong&gt; 将被保留；加上 &lt;strong&gt;Redis container&lt;/strong&gt; 容器关联的可写层 &lt;strong&gt;Contaner Root&lt;/strong&gt; 是基于 redis:latest 构建，因此垃圾回收仅会清理没有可达路径的 content 数据。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-gc/gc-demo-delete-redis.png&#34; alt=&#34;gc-demo-delete-redis&#34;&gt;
&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-gc/gc-demo-delete-redis-content.png&#34; alt=&#34;gc-demo-delete-redis-content&#34;&gt;&lt;/p&gt;
&lt;p&gt;如果在此基础上删除 Redis container，那么 &lt;strong&gt;Redis + Alpine&lt;/strong&gt; 和 &lt;strong&gt;Container Root&lt;/strong&gt; snapshot 将没有可达路径，它们也将会被垃圾回收清理。在这里也侧面解释了 &lt;strong&gt;为什么 containerd 允许用户删除正在被容器使用的镜像&lt;/strong&gt;: 垃圾回收并不会真正去清理容器根目录所依赖的 snapshot，只有等到没有容器或者镜像引用时，containerd 才会真正去删除镜像相关的数据。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-gc/gc-demo-delete-container.png&#34; alt=&#34;gc-demo-delete-container&#34;&gt;&lt;/p&gt;
&lt;p&gt;前面提到了标签系统如何关联数据，那么你可能会问，containerd 垃圾回收流程会清理正在下载的镜像数据吗？毕竟调用 image service 关联 mainifest 是下载流程的最后步骤。如果没有 &lt;a href=&#34;https://en.wikipedia.org/wiki/Lease_(computer_science)&#34;&gt;lease&lt;/a&gt; 关联，containerd 会清理正在下载的镜像数据。&lt;/p&gt;
&lt;p&gt;下载镜像是一个不可控的事务，它的耗时由镜像大小、网络带宽以及磁盘读写能力来决定。为了避免未关联的临时数据被删除，containerd 引入了 lease 数据作为新的 GC-Root。它和 image/container 数据平级，它所关联的 content/snapshot 不会被垃圾回收清理。因此下镜像的第一步就是去申请 lease。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-gc/ctr-lease-demo.png&#34; alt=&#34;ctr-lease-demo&#34;&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，我中途取消了 golang:1.20.1 镜像的下载，其中一个 layer &lt;strong&gt;sha256:543368fb&amp;hellip;&lt;/strong&gt; 没有被清理掉，因为它已经被 lease &lt;strong&gt;650284033-Pgbh&lt;/strong&gt; 关联上了。一般情况下，lease 数据上都会有标签 &lt;strong&gt;containerd.io/gc.expire&lt;/strong&gt; 来表明过期时间。过期之后，lease 和它关联的数据都会被 containerd 清理。如果我没有删除 lease，而是选择重新下载 golang:1.20.1 镜像，containerd 可以恢复取消前的状态，它具备 &lt;strong&gt;断点续传&lt;/strong&gt; 的能力。&lt;/p&gt;
&lt;h2 id=&#34;新版本的拓展能力&#34;&gt;新版本的拓展能力&lt;/h2&gt;
&lt;p&gt;前面提到的垃圾回收仅针对容器和镜像数据，并没有涉及其他资源。但标签系统足以将 metadata 定义的 GC-Root 资源关联到外部组件定义的资源信息，现在仅需要 containerd 提供垃圾回收的标记机会即可。
为此， containerd 社区为 metadata 插件引入了 GC Collector 插件接口，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-gc/collection-context.png&#34; alt=&#34;collection-context&#34;&gt;&lt;/p&gt;
&lt;p&gt;在标记 GC-Root 阶段，containerd 给予 GC Collector 三种方式标记:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;第一种是在扫描 lease 资源的阶段。在扫描某一个 namespace 下的 lease 时，containerd 将通过 &lt;strong&gt;CollectionContext.Leased()&lt;/strong&gt; 方法来告知插件: 请把当前 &lt;strong&gt;namespace&lt;/strong&gt; 下所有跟这个 &lt;strong&gt;lease&lt;/strong&gt; 有关的数据都通过 &lt;strong&gt;fn&lt;/strong&gt; 回传回来。目前 streaming service 主要采用这种方式标记。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第二种是扫描 container/image 数据标签的阶段。&lt;strong&gt;Collector.ReferenceLabel()&lt;/strong&gt; 告知了 containerd 它感兴趣的标签，这个标签以 &lt;strong&gt;containerd.io/gc.ref.&lt;/strong&gt; 开头。比如 CNICollector 插件用来管理容器的网络资源，它维护容器和 netns 路径间的关联关系。当 container 数据创建时，它的 netns 路径为 &lt;strong&gt;/run/netns/demo&lt;/strong&gt;，那么开发者可以为 container 数据添加一条标签 &lt;strong&gt;containerd.io/gc.ref.cni=/run/netns/demo&lt;/strong&gt;。当 containerd 在扫描 container 数据时，它会将 &lt;strong&gt;/run/netns/demo&lt;/strong&gt; 作为可达对象。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;第三种是扫描完某一个 namespace 的路径发起点之后，containerd 会调用 &lt;strong&gt;CollectionContext.Active()&lt;/strong&gt; 来告知插件：请把当前 &lt;strong&gt;namespace&lt;/strong&gt; 下所有正在使用的数据通过 &lt;strong&gt;fn&lt;/strong&gt; 回传回来。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当 containerd 标记完所有 GC-Root 后，它会扫描所有数据，并删除没有被关联到的数据；之后它将调用 &lt;strong&gt;CollectionContext.All()&lt;/strong&gt; 来遍历插件维护的所有数据，并调用 &lt;strong&gt;CollectionContext.Remove()&lt;/strong&gt; 来清理没被关联上的数据；最后调用 &lt;strong&gt;CollectionContext.Finish()&lt;/strong&gt; 来结束本次清理。&lt;/p&gt;
&lt;p&gt;有了这个拓展包之后，理论上所有和容器相关的资源都可以被监控起来，比如开头提到的 CNI 网络资源、containerd-shim 进程，甚至是 containerd-shim 创建出来的 socket 文件。&lt;/p&gt;
&lt;p&gt;但需要明确的是，这个拓展包针对的是 containerd 开发者，并不是终端用户。&lt;/p&gt;
&lt;h2 id=&#34;containerd-v20-计划&#34;&gt;containerd v2.0 计划&lt;/h2&gt;
&lt;p&gt;目前比较明确的是，由 AWS 开发者主导的 &lt;a href=&#34;https://github.com/containerd/containerd/issues/7751&#34;&gt;[Proposal] Add Networks Service Plugin&lt;/a&gt; 会重新改变现有 CRI 的网络管理方式。而像 &lt;a href=&#34;https://github.com/containerd/containerd/issues/7496&#34;&gt;shim process leaked when the host having high disk i/o usage&lt;/a&gt; 这些泄漏问题，可能会涉及到 &lt;a href=&#34;https://github.com/containerd/containerd/issues/4131&#34;&gt;Sandbox API&lt;/a&gt; 的改造，进展应该不会太快吧。&lt;/p&gt;
&lt;h2 id=&#34;最后&#34;&gt;最后&lt;/h2&gt;
&lt;p&gt;这次没有展开解释 containerd 如何清理 snapshotter 数据，个人觉得这部分设计还是不错的，感兴趣的开发者可以去看看这部分实现。这次就到这吧，下一篇会是 containerd 1.7 特性系列介绍的最后一篇。&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>containerd 1.7: Image Transfer Service</title>
          <link>/post/2023-containerd-17-transfer-service/</link>
          <pubDate>Mon, 13 Mar 2023 01:20:45 +0800</pubDate>
          <guid>https://fuweid.com/post/2023-containerd-17-transfer-service/</guid>
          <description>&lt;p&gt;从 HELM Chart 成功对接容器镜像仓库开始，到近两年 OCI 社区 &lt;a href=&#34;https://github.com/opencontainers/artifacts&#34;&gt;artifacts&lt;/a&gt; 标准化的落地推广，现在 OCI Registry as Storage 已经是事实标准，不同业务场景的数据存储都可以对接 OCI registry 的分发协议。同时，这几年容器镜像的安全供应链需求徒增，以及各大云厂商对容器镜像 lazy-loading 探索力度在加大，OCI 社区在 artifacts 标准的基础上为容器镜像引入 &lt;a href=&#34;https://github.com/opencontainers/image-spec/pull/934&#34;&gt;referrers&lt;/a&gt; 定义：在保证前后兼容的情况下，每一个容器镜像都可以通过关联的 artifacts 来拓展自身的属性，如下图所示，用户可以通过 referrers 关联特性为 net-monitor:v1 镜像提供了镜像签名和 SBOM 信息。当然，容器镜像 lazy-loading 属性也可以与之相关联，比如阿里云的 &lt;a href=&#34;https://github.com/containerd/overlaybd&#34;&gt;overlaybd&lt;/a&gt;，蚂蚁金服的 &lt;a href=&#34;https://github.com/dragonflyoss/image-service&#34;&gt;nydus&lt;/a&gt; 以及亚马逊的 &lt;a href=&#34;https://github.com/awslabs/soci-snapshotter&#34;&gt;soci&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-transfer-service/oci-image-referrers-example.png&#34; alt=&#34;from-oras-referrers-demo&#34;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NOTE: 图片取自 Feynman Zhou 发表的 &lt;a href=&#34;https://techcommunity.microsoft.com/t5/apps-on-azure-blog/azure-container-registry-the-first-cloud-registry-to-support-the/ba-p/3708998&#34;&gt;Azure Container Registry: the first cloud registry to support the OCI Specifications 1.1&lt;/a&gt; 文章。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;可预见的是，各大云厂商的镜像仓库都会跟进这一标准，安全供应链以及容器镜像加速的增值服务也会因此得到大面积推广。而作为容器镜像的消费端，为了支持这一特性，containerd 社区的想法是引入组合型插件 Image Transfer Service，将镜像分发和打包处理都抽象成数据流的转发，并统一收编在服务端处理。&lt;/p&gt;
&lt;h2 id=&#34;pull-需要新的抽象&#34;&gt;Pull 需要新的抽象&lt;/h2&gt;
&lt;p&gt;在 containerd 1.7 版本之前，镜像数据处理的绝大部分操作都集中在客户端。containerd 设计偏向于把服务端做薄，服务端仅做底层资源的 CRUD；而客户端则采用类似 Backends For Frontend 理念来为复杂流程封装接口，如下图所示 &lt;a href=&#34;https://pkg.go.dev/github.com/containerd/containerd@v1.7.0#Client.Pull&#34;&gt;Pull&lt;/a&gt; 镜像的函数接口。下载镜像涉及到镜像地址解析、密钥管理、并发下载以及解压处理，但该流程仅适用于下载标准格式的镜像，任何调整都容易产生不适。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-transfer-service/smart-client-pull.png&#34; alt=&#34;smart-client-pull&#34;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NOTE: containerd CRI 插件属于 containerd 进程的一部分，因此 CRI 插件在使用 Pull 函数接口时，镜像数据在 content/diff/snapshotter service 之间传递不走 grpc 协议，仅是内部函数调用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;让我们来简单回顾下当初 containerd 是如何支持容器镜像 lazy-loading 特性。&lt;/p&gt;
&lt;p&gt;由于容器镜像打包采用了不可检索的 tar 格式，因此目前市面上开源的镜像 lazy-loading 方案都涉及到镜像格式的改造，比如阿里云基于块设备的 overlaybd、蚂蚁金服基于文件系统的 nydus，以及 containerd 社区基于可检索 gzip 构建的 &lt;a href=&#34;https://github.com/containerd/stargz-snapshotter&#34;&gt;stargz&lt;/a&gt;。而格式的变化意味着 tar 解压流程不适合这类镜像(上图里的 &lt;strong&gt;apply diff&lt;/strong&gt; 阶段)，为了适配这一特性，containerd 在 GA 之后首次修改了 snapshotter 元数据的管理逻辑 &lt;a href=&#34;https://github.com/containerd/containerd/pull/3793&#34;&gt;Support target snapshot references on prepare&lt;/a&gt;：即在解压镜像时，它会告知 snapshotter 插件这是为解压镜像而准备的可写层。&lt;/p&gt;
&lt;p&gt;容器镜像的制作过程大多都是将容器的可写层提交成镜像的只读层，因此 containerd snapshotter 实现里并不会区分可写层是为谁准备的。而上述特性可以让 lazy-loading snapshotter 感知到这是在下镜像阶段，它可以通过返回 &lt;strong&gt;已存在&lt;/strong&gt; 错误来告知客户端：&lt;strong&gt;该镜像层已经下载并解压过了，请处理下一层吧&lt;/strong&gt;。在不大改流程的情况下，这也算是支持 lazy-loading 特性。&lt;/p&gt;
&lt;p&gt;但这种方案仅支持匿名用户访问的镜像。基于安全的考虑，下镜像过程并不会将用户密钥信息传递给 snapshotter；而 lazy-loading 需要时刻保证数据可访问，否则业务容器在读取 rootfs 时将会出现 IO 阻塞。直到目前为止，containerd 社区依旧在讨论如何管理用户密钥信息: &lt;a href=&#34;https://github.com/containerd/containerd/issues/5105&#34;&gt;Enable remote snapshotter to receive creds from containerd CRI plugin&lt;/a&gt;。除此之外，容器镜像 lazy-loading 特性还要求用户手动替换或者集群管理员通过 webhook 来修改 pod 里的镜像信息。从产品侧的角度看，这种端到端的集成体验极差。&lt;/p&gt;
&lt;p&gt;安全供应链相关的特性也面临相同问题：Datadog 开发者提出 &lt;a href=&#34;https://github.com/containerd/containerd/issues/6691&#34;&gt;Plugin to CRI for image digest verification&lt;/a&gt;，他们希望 containerd 能在下载镜像阶段进行镜像校验，而不是在集群的 webhook 里。但目前的 Pull 流程是无法感知镜像的辅助信息，更谈不上校验。考虑到镜像辅助信息的多样性，Pull 函数接口的 Optional 配置方法无法满足插件化需求。&lt;/p&gt;
&lt;h2 id=&#34;image-transfer-service&#34;&gt;Image Transfer Service&lt;/h2&gt;
&lt;p&gt;镜像数据的来源可以是镜像仓库、本地镜像 tar.gz 或者是本地解压后的存储等。不同的来源之间可以相互流动，比如下载流程可以理解成 &lt;strong&gt;镜像仓库 -&amp;gt; 本地对象存储 (containerd content service) -&amp;gt; 本地解压后的存储 (containerd snapshot service)&lt;/strong&gt;，相反的流转将变成上传镜像；甚至还可以通过简单的数据转发，实现镜像在不同仓库之间迁移。containerd 通过引入 &lt;a href=&#34;https://github.com/containerd/containerd/issues/7592&#34;&gt;Image Transfer service&lt;/a&gt; 来将 Pull/Push 抽象成数据迁移流程，而 transfer service 本身是插件化的，在相同数据流向的情况下，containerd 支持定制化开发。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Transferer&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;interface&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
	&lt;span class=&#34;nf&#34;&gt;Transfer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;ctx&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Context&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; 
	    &lt;span class=&#34;nx&#34;&gt;source&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;interface&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{},&lt;/span&gt;
		&lt;span class=&#34;nx&#34;&gt;destination&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;interface&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{},&lt;/span&gt;
		&lt;span class=&#34;nx&#34;&gt;opts&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Opt&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;，&lt;/span&gt;
	&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;error&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-transfer-service/overview-transfer-service.png&#34; alt=&#34;overview-transfer-service&#34;&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，Transfer objects 代表着数据源，数据流向是 Registry 到 Image Store，目前已在 1.7 版本里支持的迁移能力如下表所示。其中数据转发过程可能会和客户端交互，比如推送当前的传输状态，或者客户端提供密钥信息做鉴权等等。这些交互过程都通过 &lt;strong&gt;streaming service&lt;/strong&gt; 来实现: 客户端在发起数据传输时，它会向 &lt;strong&gt;streaming service&lt;/strong&gt; 申请数据交互通道，这些通道将会共享给 Transfer object，并由具体的 Transfer object 实现来决定如何交互。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Destination&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Registry&lt;/td&gt;
&lt;td&gt;Image Store&lt;/td&gt;
&lt;td&gt;&amp;ldquo;pull&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image Store&lt;/td&gt;
&lt;td&gt;Registry&lt;/td&gt;
&lt;td&gt;&amp;ldquo;push&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Object stream (Archive)&lt;/td&gt;
&lt;td&gt;Image Store&lt;/td&gt;
&lt;td&gt;&amp;ldquo;import&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image Store&lt;/td&gt;
&lt;td&gt;Object stream (Archive)&lt;/td&gt;
&lt;td&gt;&amp;ldquo;export&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image Store&lt;/td&gt;
&lt;td&gt;Image Store&lt;/td&gt;
&lt;td&gt;&amp;ldquo;tag&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;对于 lazy-loading 和安全供应链场景，镜像的数据流向应该如下表所示。在后续的实现里，&lt;strong&gt;fetch&lt;/strong&gt; 操作将会负责拉取镜像相关的 referrers，并在目标数据源中做相应的处理，比如安全供应链的数据源会校验数据的合法性，而 lazy-loading 场景下的关注点将会是 &lt;strong&gt;如何通过 referrers 来获得真正的镜像格式&lt;/strong&gt;，后续的进展可以关注 &lt;a href=&#34;https://github.com/containerd/containerd/issues/7654&#34;&gt;OCI Referrers API support&lt;/a&gt;。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Destination&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Registry&lt;/td&gt;
&lt;td&gt;Content Store(Object Stream)&lt;/td&gt;
&lt;td&gt;&amp;ldquo;fetch&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content Store(Object Stream)&lt;/td&gt;
&lt;td&gt;Mount/Snapshot&lt;/td&gt;
&lt;td&gt;&amp;ldquo;unpack&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content Store(Object Stream)&lt;/td&gt;
&lt;td&gt;Image Store&lt;/td&gt;
&lt;td&gt;&amp;ldquo;tag&amp;rdquo;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;除此之外，镜像的密钥管理也将会插件化，只是现在如何同 lazy-loading snapshotter 交互还未确定，后续的进展可以关注 &lt;a href=&#34;https://github.com/containerd/containerd/issues/8228&#34;&gt;[transfer] credential manager plugin&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id=&#34;统一镜像处理逻辑&#34;&gt;统一镜像处理逻辑&lt;/h2&gt;
&lt;p&gt;虽然 containerd CRI 插件使用了 &lt;strong&gt;Pull&lt;/strong&gt; 函数方法来处理镜像下载，但是它有不少优化特性，比如可根据流量来自动取消下载 &lt;a href=&#34;https://github.com/containerd/containerd/pull/6150&#34;&gt;feature: support image pull progress timeout&lt;/a&gt;，通过共享机制来避免重复下载 &lt;a href=&#34;https://github.com/containerd/containerd/pull/6702&#34;&gt;CRI: improve image pulling performance&lt;/a&gt;，或者是优化 lazy-loading snapshotter 集成体验 &lt;a href=&#34;https://github.com/containerd/containerd/pull/8036&#34;&gt;Export remote snapshotter label handler&lt;/a&gt; 等。这些处理在 nerdctl 也有类似的实现，transfer service 的出现可以很好的统一类似的处理逻辑。&lt;/p&gt;
&lt;p&gt;需要说明的是，transfer service 出现并不意味着放弃客户端下载的支持，类似 buildkit 这样项目有自身镜像管理需求，transfer service 不一定适合它们，这些项目依旧会使用 containerd 提供的 &lt;strong&gt;Pull&lt;/strong&gt; 函数接口。&lt;/p&gt;
&lt;h2 id=&#34;把镜像下载到虚拟机里&#34;&gt;把镜像下载到虚拟机里？？&lt;/h2&gt;
&lt;p&gt;有部分厂家会使用机密计算和普通安全容器来提供多住户的 Kubernetes 产品，为了保证容器数据安全，这些场景有在 Guest OS 内下镜像的需求。为了支持这一特性，containerd ttrpc 支持了 streaming 能力 &lt;a href=&#34;https://github.com/containerd/ttrpc/pull/107&#34;&gt;Introduce streaming&lt;/a&gt;。配合上 transfer service，containerd 2.0 大概率会把下载请求转发给 Guest OS 内的控制器，如下图所示。这部分转发逻辑也还在设计阶段，预计会在 Sandbox API 定稿后出提供。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-transfer-service/pull-image-in-vm.png&#34; alt=&#34;pull-image-in-vm&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;最后&#34;&gt;最后&lt;/h2&gt;
&lt;p&gt;个人一直很喜欢 containerd 插件机制，所以整体下来会比较期待 transfer service 在插件管理上的演进。&lt;/p&gt;
&lt;p&gt;这一篇就先这样吧，有问题欢迎私信。&lt;/p&gt;
&lt;p&gt;下一篇打算聊一下 containerd 垃圾回收机制。&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>containerd 1.7: UserNamespace Stateless Pod</title>
          <link>/post/2023-containerd-17-userns/</link>
          <pubDate>Sat, 04 Mar 2023 15:20:45 +0800</pubDate>
          <guid>https://fuweid.com/post/2023-containerd-17-userns/</guid>
          <description>&lt;p&gt;containerd 1.7 版本有比较多的&lt;a href=&#34;https://github.com/containerd/containerd/blob/1e0e909dcc4676b2afafe0851a609992482cc006/RELEASES.md?plain=1#L385&#34;&gt;实验特性&lt;/a&gt;。在这里，我会介绍 containerd 对 &lt;a href=&#34;https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/127-user-namespaces#phase-1-pods-without-volumes&#34;&gt;UserNamespace Stateless Pod&lt;/a&gt; 支持的情况，算是个人对 containerd 1.7 版本特性介绍系列的开篇。&lt;/p&gt;
&lt;h2 id=&#34;1-usernamespace-安全特性&#34;&gt;1. UserNamespace 安全特性&lt;/h2&gt;
&lt;p&gt;Linux 内核是基于进程的 &lt;a href=&#34;https://man7.org/linux/man-pages/man7/credentials.7.html&#34;&gt;credentials(7)&lt;/a&gt; 凭证来做访问控制，比如进程的拥有者标识 UID/GID 和用于系统资源访问控制判定的 Effective UID/GID 等凭证。而 &lt;a href=&#34;https://man7.org/linux/man-pages/man7/user_namespaces.7.html&#34;&gt;user_namespace(7)&lt;/a&gt; 提供了安全隔离特性。在不同的 user namespace(userns) 下，同一个进程不仅有不同的 UID/GID，同时还具备了不同的 &lt;a href=&#34;https://man7.org/linux/man-pages/man7/capabilities.7.html&#34;&gt;capabilities(7)&lt;/a&gt; 权限。比如 u1001 用户进程 bash 通过 &lt;code&gt;unshare -r bash&lt;/code&gt; 进入到了新的 userns，并将自己映射成了 root 用户，还具备了所有的 capabilities。但这个进程真的就变成了特权进程？这取决于该用户在系统资源所属的 userns 里是否拥有访问权限。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-userns/userns-root-mount-failed.png&#34; alt=&#34;userns-root-mount-failed&#34;&gt;&lt;/p&gt;
&lt;p&gt;在介绍具体的判定规则前，先花点时间了解下内核是如何管理 UID/GID 的映射关系。&lt;/p&gt;
&lt;p&gt;每个进程都必须归属于某一个 userns。在初始状态下，进程都属于 &lt;a href=&#34;https://elixir.bootlin.com/linux/v6.1.15/source/kernel/user.c#L27&#34;&gt;initial userns&lt;/a&gt;。Initial userns 比较特殊，它没有任何映射关系；Linux 支持嵌套的 userns，所有正在使用的 userns 组成的关系图将会是以 initial userns 为根的树状图。在 parent userns 里的进程，只要具备 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/setuid.2.html&#34;&gt;CAP_SET{UID,GID}&lt;/a&gt; 能力，这些进程就可以通过 &lt;code&gt;/proc/[pid]/{uid,gid}_map&lt;/code&gt; 文件接口，以 &lt;code&gt;ID-inside-ns ID-outside-ns Range-length&lt;/code&gt; 的格式，给刚进入 child userns 的进程设置 UID/GID 映射关系，而创建该 userns 进程的 Effective UID(EUID) 将成为 userns 的所有者，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-userns/userns-idmapping-userspace-kernel.svg&#34; alt=&#34;userns-idmapping-between-userspace-kernel&#34;&gt;&lt;/p&gt;
&lt;p&gt;在上图中，有两层 userns 映射关系：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;uid_map: 0 1001 10&lt;/code&gt; 将 initial userns 的 UID &lt;code&gt;[1001, 1010]&lt;/code&gt; 映射到 userns X 里的 UID &lt;code&gt;[0, 9]&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;uid_map: 10 0 5&lt;/code&gt; 将 userns X 中的 UID &lt;code&gt;[0, 4]&lt;/code&gt; 映射成 userns Y 里的 UID &lt;code&gt;[10, 14]&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但这些都是用户态的映射关系，&lt;code&gt;uid_map&lt;/code&gt; 文件接口会将其转化成 &lt;a href=&#34;https://elixir.bootlin.com/linux/v6.1.15/source/kernel/user_namespace.c#L1060&#34;&gt;内核态的映射关系 &lt;code&gt;userspace-id:kernel-id:range(u:k:r)&lt;/code&gt;&lt;/a&gt;，比如 &lt;code&gt;u1000:k0:r100&lt;/code&gt; 映射关系可以把 &lt;code&gt;k10&lt;/code&gt; 映射成 &lt;code&gt;u1010 = k10 - k0 + u1000&lt;/code&gt;；同样的，&lt;code&gt;u1005&lt;/code&gt; 可以转化为 &lt;code&gt;k5 = u1005 - u1000 + k0&lt;/code&gt;，其中 &lt;code&gt;r100&lt;/code&gt; 是用来判断映射是否超出范围。Initial userns 的 ID 映射关系是 &lt;code&gt;u0:k0:r4294967295&lt;/code&gt;，相当于 &lt;code&gt;u&lt;/code&gt; 等于 &lt;code&gt;k&lt;/code&gt;。Userns X 的 parent userns 是 initial userns，结合 uid_map 的配置，内核态的映射关系应为 &lt;code&gt;u0:k1001:r10&lt;/code&gt;；同理 Userns Y 的内核态映射关系是 &lt;code&gt;u10:k1001:r5&lt;/code&gt;。在进行权限判断时，内核最终都以 kernel-id 为凭据。&lt;/p&gt;
&lt;p&gt;回到最初的话题，为什么具备了所有 capabilities 还是不能进行 bind-mount？&lt;/p&gt;
&lt;p&gt;Linux 的系统资源都归属于某一个 userns，比如 &lt;a href=&#34;https://man7.org/linux/man-pages/man7/mount_namespaces.7.html&#34;&gt;mount_namespace(7)&lt;/a&gt; 下的挂载资源，&lt;a href=&#34;https://man7.org/linux/man-pages/man7/network_namespaces.7.html&#34;&gt;network_namespace(7)&lt;/a&gt; 下的设备资源等等。当进程想要访问系统资源时，内核会采用下面的策略来决定进程是否有权 (为了方便解释，被访问的系统资源属于 userns X，而访问该系统资源的进程属于 userns Y)：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在 initial userns 为根的树状关系里，当 userns X 是 userns Y 的祖先或者是兄弟分支，进程无权访问；&lt;/li&gt;
&lt;li&gt;当 userns X 与 userns Y 相等或者 userns Y 是 userns X 的祖先时，只要进程具备资源要求的 capabilities 即可访问；&lt;/li&gt;
&lt;li&gt;当 userns Y 是 userns X 的父节点，且当前进程的 EUID 为 userns X 的创建者，那么该进程可以访问 userns Y 下的所有系统资源。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;判断进程的 EUID 是否为 userns X 的所有者时，内核比较的是 kernel-id。上诉的判断来自 &lt;a href=&#34;https://elixir.bootlin.com/linux/v6.1.15/source/security/commoncap.c#L51&#34;&gt;cap_capable&lt;/a&gt; 函数。大部分的权限判断都包着它来做，比如挂载权限的判断逻辑在 &lt;a href=&#34;https://elixir.bootlin.com/linux/v6.1.15/source/fs/namespace.c#L1763&#34;&gt;may_mount&lt;/a&gt; 等，感兴趣的可以去看看。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-userns/userns-mountns-root-mount.png&#34; alt=&#34;userns-mountns-root-mount&#34;&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，虽然 unshare 切换了 userns，但 mount_namespace(mountns) 依然属于 parent userns。根据访问策略来看，当前进程无权访问祖先 userns 的资源，因此挂载失败。如果想要在不提权的情况下进行挂载，最好的方式就是 unshare 切换新的 mountns。全新的 mountns 将属于当前的 userns；当前进程具有 SYS_ADMIN 权限，所以它能进行挂载。&lt;/p&gt;
&lt;p&gt;现在的容器默认会配置独立的 &lt;a href=&#34;https://man7.org/linux/man-pages/man7/namespaces.7.html&#34;&gt;mount/network/pid/ipc/uts_namespace(7)&lt;/a&gt; 资源，没有的独立的 userns，这些系统资源依旧隶属于 initial userns。而大部分容器进程都以 root 用户身份启动，为了防止容器进程逃逸后对系统造成破坏，业界的普遍做法是限制容器进程的 capabilities，同时采用 SELinux/Apparmor 安全规则控制对系统资源的访问。但这毕竟都是后验形式，集群管理员需要对容器进程做大量的行为审计，他们才能得到有效的安全规则。但就目前的结果来看，现在依然有不少 initial userns 的权限泄漏所引发的安全问题。&lt;/p&gt;
&lt;p&gt;既然全新的 userns 可以完全禁止进程访问隶属于 initial userns 的系统资源，那么为什么迟迟不落地呢？&lt;/p&gt;
&lt;h2 id=&#34;2-fsuid-映射问题&#34;&gt;2. FSUID 映射问题&lt;/h2&gt;
&lt;p&gt;在 Linux 系统里，一个进程有两个关键的身份凭证：一个是前面提到的 EUID，它用于做系统资源的权限判断；而另外一个是 Filesystem UID(FSUID)，它用来判断是否有权限访问文件目录。一般情况下只要不调用 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/setfsuid.2.html&#34;&gt;setfsuid&lt;/a&gt;，进程的 FSUID 都和 EUID 保持一致。&lt;/p&gt;
&lt;p&gt;当进程在创建文件目录时，文件系统会将当前进程 FSUID 的映射结果写入到存储设备。当然，文件系统并不是持久化当前 userns 的映射结果。当进程挂载文件系统时，内核会将该进程所在的 userns &lt;code&gt;caller u:k:r&lt;/code&gt; 映射关系作为对应文件系统的 &lt;code&gt;fs u:k:r&lt;/code&gt; 映射关系。文件系统会将创建文件进程的 FSUID kernel-id 转化成 &lt;code&gt;fs u:k:r&lt;/code&gt; 下的 userspace-id，这个 id 才是最终存储的结果。有了这个概念后，我们看下查看文件拥有者标识的过程。&lt;/p&gt;
&lt;p&gt;假设用户 u1001 HOME 目录所在的文件系统采用了 &lt;code&gt;fs u0:k0:r4294967295&lt;/code&gt; 映射，以及当前 stat 进程的 userns 也采用了同样的映射 &lt;code&gt;caller u0:k0:r4294967295&lt;/code&gt;。文件系统先将存储设备里的 &lt;code&gt;u1001&lt;/code&gt; 转化成 &lt;code&gt;k1001&lt;/code&gt;，内核再通过当前进程的映射关系转化，最终得到我们所看到的 &lt;code&gt;u1001&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-userns/initns-stat.png&#34; alt=&#34;initns-stat&#34;&gt;&lt;/p&gt;
&lt;p&gt;让我们再看看把 u1001 映射成 root 用户后的显示效果，如下图所示。因为进程已经切换到了新的 userns，但 &lt;code&gt;.bashrc&lt;/code&gt; 文件的存储没有发生变化，经过两次映射，我们最终看到的是 &lt;code&gt;u0&lt;/code&gt;。原先 &lt;code&gt;/var/lib/containerd&lt;/code&gt; 在存储设备里的 FSUID 是 &lt;code&gt;u0&lt;/code&gt;，经过 &lt;code&gt;fs u0:k0:r4294967295&lt;/code&gt; 映射后得到 &lt;code&gt;k0&lt;/code&gt;，但 &lt;code&gt;k0&lt;/code&gt; 并不在 &lt;code&gt;caller u0:k1001:r1&lt;/code&gt; 的范围内。对于这些不在映射范围内的 UID，内核统一显示成 nobody(65534)。这也意味着，当前 root 用户依然无法访问 &lt;code&gt;/var/lib/containerd&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-userns/userns-stat.png&#34; alt=&#34;userns-stat&#34;&gt;&lt;/p&gt;
&lt;p&gt;回到容器的使用场景。首先，绝大多数的容器镜像是以 root 用户构建，这就要求容器进程的 FSUID 经过映射后要和存储对应的 FSUID 一致。为了方便解释，这里假设容器镜像的底层文件系统采用 initial userns 映射方式。如果容器进程采用 &lt;code&gt;caller r0:k1001:r1&lt;/code&gt; 映射，那么它是无权访问镜像内容，它将无法启动。解决这个问题的方式，就是在启动容器前，我们通过 &lt;code&gt;chown -R&lt;/code&gt; 把容器的根录都修改成 u1001。但这也意味着效率低下。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;th&gt;Inodes&lt;/th&gt;
&lt;th&gt;overlayfs w/ metacopy&lt;/th&gt;
&lt;th&gt;overlayfs w/o metacopy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;tensorflow/tensorflow:latest&lt;/td&gt;
&lt;td&gt;1489MiB&lt;/td&gt;
&lt;td&gt;32596&lt;/td&gt;
&lt;td&gt;1.29s&lt;/td&gt;
&lt;td&gt;54.80s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;library/node:latest&lt;/td&gt;
&lt;td&gt;1425MiB&lt;/td&gt;
&lt;td&gt;33385&lt;/td&gt;
&lt;td&gt;1.18s&lt;/td&gt;
&lt;td&gt;52.86s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;library/ubuntu:22.04&lt;/td&gt;
&lt;td&gt;83.4MiB&lt;/td&gt;
&lt;td&gt;3517&lt;/td&gt;
&lt;td&gt;0.15s&lt;/td&gt;
&lt;td&gt;5.32s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;当我们在修改容器根目录的文件属性时，如果没有 metacopy=on 的属性支持，那么 &lt;code&gt;chown -R&lt;/code&gt; 在修改文件属性前，overlayfs 文件系统会将这个文件从只读层拷贝到可写层。如果文件越大，整个修改过程就越慢，同时还产生磁盘压力；带上 metacopy=on 之后，至少 overlayfs 仅拷贝文件的 metadata，效率高不少。但面对小文件特别多的场景，&lt;code&gt;chown -R&lt;/code&gt; 操作依旧很耗时。与此同时， &lt;code&gt;chown&lt;/code&gt; 还是永久性修改文件属性。&lt;/p&gt;
&lt;p&gt;然而不同容器之间可能会共享同一个数据卷。常见的场景有：容器 A 以 &lt;code&gt;caller r0:k1001:r1&lt;/code&gt; 映射关系产生数据，然后容器 B 再进行分析消费。这就要求容器 B 也必须使用 &lt;code&gt;r0:k1001:r1&lt;/code&gt; 映射关系，否则只能再次使用 &lt;code&gt;chown&lt;/code&gt; 来切换。&lt;/p&gt;
&lt;p&gt;在全民白嫖镜像仓库的形势下，镜像仓库已经变成 &lt;code&gt;免费数据&lt;/code&gt; 仓库。个人见过上百 GiB 的容器镜像，在这种容器镜像面前使用 &lt;code&gt;chown&lt;/code&gt;，这简直是劝退。&lt;/p&gt;
&lt;h2 id=&#34;3-mount-idmapped-特性--kernel-v512&#34;&gt;3. Mount idmapped 特性 &amp;gt;= kernel v5.12&lt;/h2&gt;
&lt;p&gt;为了解决这个映射的难题，&lt;a href=&#34;https://github.com/torvalds/linux/commit/7d6beb71da3cc033649d641e1e608713b8220290&#34;&gt;Linux v5.12 版本&lt;/a&gt; 引入了挂载点的映射关系 &lt;code&gt;mount u:k:r&lt;/code&gt;，用户可以通过它来绕过文件系统原先的 &lt;code&gt;fs u:k:r&lt;/code&gt; 映射，其中该特性由 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/mount_setattr.2.html&#34;&gt;mount_setattr(2)&lt;/a&gt; 系统调用提供。我们直接看例子解释吧，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-userns/mount-idmapped-bind.png&#34; alt=&#34;mount-idmapped-bind&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/brauner/mount-idmapped&#34;&gt;mount-idmapped&lt;/a&gt; 工具把 &lt;code&gt;/var/lib/containerd&lt;/code&gt; 挂载到 &lt;code&gt;/mnt&lt;/code&gt; 下，并通过 &lt;code&gt;mount u0:k1001:r1&lt;/code&gt; 映射关系将其 &lt;code&gt;授权&lt;/code&gt; 给 u1001 用户进程访问，u1001 用户无需提权就可以在 &lt;code&gt;/mnt&lt;/code&gt; 下访问 &lt;code&gt;/var/lib/containerd&lt;/code&gt; 的数据。挂载点 &lt;code&gt;/mnt&lt;/code&gt; 上的 &lt;code&gt;mount u0:k1001:r1&lt;/code&gt; 映射关系是替代了原先的 &lt;code&gt;fs u0:k0:r4294967295&lt;/code&gt;。只是底层文件系统 inode 保存的是 &lt;code&gt;fs u:k:r&lt;/code&gt; 映射后的 kernel-id；而 &lt;code&gt;mount u:k:r&lt;/code&gt; 由 Virtual filesystem(vfs) 模块管理，vfs 拿不到存储设备上保存的 userspace-id，所以 vfs 需要利用 &lt;code&gt;fs u:k:r&lt;/code&gt; 转化回来。这也就是上图里，&lt;code&gt;u0 -&amp;gt; k0 -&amp;gt; u0&lt;/code&gt; 转化看似无效，但它其实是真实的处理流程。&lt;/p&gt;
&lt;p&gt;如果 u1001 用户进程在 &lt;code&gt;/mnt&lt;/code&gt; 目录里进行写操作，通过上图的映射关系，新文件的 FSUID 最终将以 &lt;code&gt;u0&lt;/code&gt; 落盘。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-userns/mount-idmapped-bind-write.png&#34; alt=&#34;mount-idmapped-bind-write&#34;&gt;&lt;/p&gt;
&lt;p&gt;无论多大的文件目录，该内核特性都可以很快完成 FSUID 映射，还可以同时支持多个 &lt;code&gt;mount u:k:r&lt;/code&gt; 映射，也解决了数据卷共享问题。我想这就是为什么社区开始推进 userns 落地的原因吧。&lt;/p&gt;
&lt;h2 id=&#34;4-containerd-目前的集成情况&#34;&gt;4. containerd 目前的集成情况&lt;/h2&gt;
&lt;p&gt;为了支持 userns，containerd 对 CRI RunPodSandbox 的流程有比较大的改动，但这仅针对 userns 场景的 Pod，其他场景和之前保持一致。&lt;/p&gt;
&lt;h3 id=&#34;41-依旧使用-chown-修改容器根目录&#34;&gt;4.1 依旧使用 chown 修改容器根目录&lt;/h3&gt;
&lt;p&gt;containerd 并没有使用 mount_setattr 特性，目前还是使用 chown 的形式。为了减少不必要的 chown 操作，containerd 将首次 chown 之后的容器根目录做成只读 snapshot，而对于后续使用相同镜像以及相同 uid_map 的容器而言，containerd 可以直接用现成的 snapshot 做根目录。也算是一个优化吧。&lt;/p&gt;
&lt;p&gt;没有集成 mount idmapped 的主要原因在于，它对 containerd 现有的 mount 挂载流程影响比较大。&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://lore.kernel.org/all/20220407112157.1775081-1-brauner@kernel.org/&#34;&gt;Linux v5.19&lt;/a&gt; overlayfs 文件系统开始支持 mount idmapped，但它是针对只读层设计，用户无法对一个已挂载好的 overlayfs 文件系统进行 mount idmapped，毕竟 overlayfs 并没有像 ext4/xfs/btrfs 那样拥有 &lt;code&gt;FS_ALLOW_IDMAP&lt;/code&gt;。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// https://elixir.bootlin.com/linux/v6.1.15/source/fs/namespace.c#L3979
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;static&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;can_idmap_mount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;const&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;mount_kattr&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;kattr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;mount&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;mnt&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
	&lt;span class=&#34;p&#34;&gt;...&lt;/span&gt;

	&lt;span class=&#34;cm&#34;&gt;/* The underlying filesystem doesn&amp;#39;t support idmapped mounts yet. */&lt;/span&gt;
	&lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;mnt_sb&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s_type&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fs_flags&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;FS_ALLOW_IDMAP&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
		&lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;EINVAL&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
    
    &lt;span class=&#34;p&#34;&gt;...&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;为此，我们需要将容器镜像的只读层挨个进行 mount idmapped，最后再挂载成 overlayfs。或者是将容器镜像直接解压到已经 mount idmapped 好的挂载点上。但后者对现有 containerd snapshot 管理有比较大的冲击，前者看起来会更容易落地些。&lt;/p&gt;
&lt;p&gt;值得一提的是，新版本内核已经演进出了新的 mount API: fsopen(2)/fsconfig(2)/fsmount(2) 将文件系统挂载拆成多次系统调用，而且 fsmount(2) 仅是生成 &lt;code&gt;anonymous mount&lt;/code&gt; 记录，需要 move_mount(2) 将其转化成可见的挂载点。而普通的 bind mount 将由 open_tree(2) 来产生 &lt;code&gt;anonymouns mount&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;mount_setattr 有一个要求就是必须是 anonymouns mount。但除了 fsopen，fsconfig/fsmount/open_tree 都没进入 &lt;code&gt;golang.org/x/sys/unix&lt;/code&gt;。整体来看，mount_setattr 在 containerd 上落下来可能还需要几个月的时间。&lt;/p&gt;
&lt;h3 id=&#34;42-延后网络初始化&#34;&gt;4.2 延后网络初始化&lt;/h3&gt;
&lt;p&gt;早期 dockerd 的容器网络初始化是在 runC 进入 init 阶段后，runC 通过 &lt;a href=&#34;https://github.com/opencontainers/runtime-spec/blob/58ec43f9fc39e0db229b653ae98295bfde74aeab/config.md?plain=1#L478&#34;&gt;prestart_hook&lt;/a&gt; 回调通知 dockerd， dockerd 再通过 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/setns.2.html&#34;&gt;setns(2)&lt;/a&gt; 进入到容器 network namespace(netns) 进行配置。&lt;/p&gt;
&lt;p&gt;同样，kubelet 在处理 dockershim 的网络时，它也是等待 pause 容器启动后，并通过 &lt;code&gt;/proc/[pause-pid]/ns/net&lt;/code&gt; 来进行 CNI 调用。但这里有一个很大的问题就是，一旦 pause 容器进程被意外杀死，同时原先 pause 容器进程 pid 被复用，那么 CNI 将会在错误的 netns 里进行操作。运气不好的话，CNI 可能会操作到 initial userns 下的 netns，造成整机的网络中断。&lt;/p&gt;
&lt;p&gt;而 containerd CRI 当前的设计可以避免 pid 复用所引发的问题。在启动 pause 容器前，containerd 会提前准备好 netns，并将其持久化到 &lt;code&gt;/run/netns&lt;/code&gt; 之下。这个行为和 &lt;code&gt;ip netns add&lt;/code&gt; 行为一致，如下图所示。CNI 初始化完毕后，containerd 才将持久化后的 netns 地址交给 runC。这完全避免了 pid 复用的问题。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-userns/ip-netns-add.png&#34; alt=&#34;ip-netns-add&#34;&gt;&lt;/p&gt;
&lt;p&gt;但这个不适用于 userns 场景，因为 containerd 创建的出来 netns 属于 initial userns，而且它还挂载在属于 initial userns 的 mountns 里。新的 userns 无法操作 netns 下的资源，同时因为 netns 归属问题，&lt;a href=&#34;https://github.com/torvalds/linux/commit/7dc5dbc879bd0779924b5132a48b731a0bc04a1e#diff-4839664cd0c8eab716e064323c7cd71fR1164&#34;&gt;还导致 runC 无法将 &lt;code&gt;/sys/&lt;/code&gt; 挂载容器根目录&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;解决这个问题的唯一途径就是让 runC 创建 netns。为了防止出现 pid 复用问题，在 runC init 阶段结束后，containerd 将 &lt;code&gt;/proc/[pause-pid]/ns/net&lt;/code&gt; 持久化到 &lt;code&gt;/run/netns&lt;/code&gt; 之下，然后再判断 runC init 进程是否还处于运行状态，避免出现 datarace 的情况。&lt;/p&gt;
&lt;p&gt;为了避免对现有容器运行时集成的影响，这个处理逻辑仅针对使用 userns 的容器，原先支持的场景不受影响。&lt;/p&gt;
&lt;h2 id=&#34;5-最后&#34;&gt;5. 最后&lt;/h2&gt;
&lt;p&gt;目前该特性距离上生产还有很长的路要走，下图是 Kubernetes SIG-Node 之前讨论过&lt;a href=&#34;https://docs.google.com/presentation/d/1z4oiZ7v4DjWpZQI2kbFbI8Q6botFaA07KJYaKA-vZpg/edit#slide=id.gfd10976c8b_1_41&#34;&gt;规划&lt;/a&gt;。除此之外，还得看用户侧是否实时跟进新内核，以及各大 Linux 发行版商是否会及时 backport 新版本特性。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2023-containerd-17-userns/kep-roadmap.png&#34; alt=&#34;kep-roadmap&#34;&gt;&lt;/p&gt;
&lt;p&gt;个人倒是很喜欢这个特性，因为 userns 几乎可以让容器进程更好地享受隔离特性，减少 seccomp 这些作用不大但影响性能的配置干扰。说到这，我想起前同事做过的镜像构建项目，因为安全问题，管理员迟迟不愿意开放 mount 挂载权限。如果这个特性能推广，应该能减少很多不必要的会了吧。。。&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>使用 unshare(CLONE_FS) 来优化 OverlayFS 挂载</title>
          <link>/post/2022-go-unshare-clonefs/</link>
          <pubDate>Sat, 15 Oct 2022 18:20:45 +0800</pubDate>
          <guid>https://fuweid.com/post/2022-go-unshare-clonefs/</guid>
          <description>&lt;h3 id=&#34;背景&#34;&gt;背景&lt;/h3&gt;
&lt;p&gt;在 Linux 平台上，大部分情况下会使用 &lt;a href=&#34;https://docs.kernel.org/filesystems/overlayfs.html&#34;&gt;OverlayFS&lt;/a&gt; 文件系统来管理容器镜像存储，而 OverlayFS 文件的特点也比较符合容器场景使用：它不仅可以将多个目录合并成统一的访问视图，还能做到读写分离。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;mount -t overlay overlay &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  -olowerdir&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;/lower1:/lower2:/lower3,upperdir&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;/upper,workdir&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;/work &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  /merged
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如上面的挂载命令所示， lowerdir 代表着容器镜像层解压后的目录。从 OCI Image 标准 定义来看，容器镜像的层数并没有限制。但 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/mount.2.html&#34;&gt;mount(2)&lt;/a&gt; 系统调用的参数被严格限制在 4KiB，所以实际使用的容器镜像层级有限制的。&lt;/p&gt;
&lt;p&gt;为了解决这个层级的问题，Docker 采用压缩 lowerdir 参数来尽可能地支持更多层级的容器镜像。Docker 存储插件使用 &lt;code&gt;l/${random-id(len=26)}&lt;/code&gt; 软链接指向实际的存储目录，然后跳到 &lt;code&gt;/var/lib/docker/overlay2&lt;/code&gt; 目录下进行挂载，这样就不需要在 lowerdir 参数里重复填写 &lt;code&gt;/var/lib/docker/overlay2/&lt;/code&gt; 这 25 个字符。按照 Docker 代码里的注释，Overlay 镜像存储最大可支持到 128 层。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;/var/lib/docker/overlay2/l/63WSQBTYICXV2O7SOZXAXYLAY2 
  -&amp;gt; ../f98d68377b05c44bacc062397f7ebaaf066b070fce15fbcfe824698d15f2eaa8/diff
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;但在当时，Go 并没有提供太多的线程操作，所有被 Go-Runtime 管理的线程都使用了 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/clone.2.html&#34;&gt;CLONE_FS&lt;/a&gt;。一旦某个 Goroutine 通过 Chdir 修改了当前工作目录，这会污染到整个进程，Docker 无法基于这样的方式来并发处理 OverlayFS 挂载请求，所以在当时只能选择 Fork-Exec 子进程来处理。考虑到维护多个二进制的成本过高，Docker 采用了 Re-exec 的方式。&lt;/p&gt;
&lt;p&gt;不管怎么样，Fork-Exec 处理挂载成本很高，而且这样挂载逻辑没法独立成一个 Go Package，它要求使用者在 Go-Main-Init 函数里添加启动的预处理逻辑。所以在 containerd 项目里，我们采用了 Clone-Thread 的形式。&lt;/p&gt;
&lt;p&gt;这个 Patch 是 Derek 带着我做的，也算是我给 containerd 提交的第一个有意思的修改。当时我们模拟了 Go-Exec-Fork 进程的步骤来创建线程。但毕竟这个线程的状态并不能用于其他 Goroutine，所以我们会锁住这个线程，这个线程在处理完 Chdir 和 mount 之后就主动退出，避免对整个进程的影响。当然这种模拟 Go-Exec-Fork 进程的行为不只有我们这么做，gVisor 也这么玩 ：）&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;c1&#34;&gt;//go:linkname beforeFork syscall.runtime_BeforeFork
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;beforeFork&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;

&lt;span class=&#34;c1&#34;&gt;//go:linkname afterFork syscall.runtime_AfterFork
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;afterFork&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;

&lt;span class=&#34;c1&#34;&gt;//go:linkname afterForkInChild syscall.runtime_AfterForkInChild
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;afterForkInChild&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;模拟 Go-Exec-Fork 来管理线程在 containerd 平稳运行了近 5 年，但最近社区的 Brian Goff 和 Cory Snider 发现还有更好的处理方式。&lt;/p&gt;
&lt;h3 id=&#34;unshareclone_fs&#34;&gt;Unshare(CLONE_FS)&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;https://man7.org/linux/man-pages/man2/unshare.2.html

CLONE_FS
              Reverse the effect of the clone(2) CLONE_FS flag.  Unshare
              filesystem attributes, so that the calling process no
              longer shares its root directory (chroot(2)), current
              directory (chdir(2)), or umask (umask(2)) attributes with
              any other process.
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;根据 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/unshare.2.html&#34;&gt;unshare(2)&lt;/a&gt; 的文档来看，CLONE_FS 只提到了进程。但通过实践来看，它是可以作用在单个线程上。如下面的代码所示，syscall.Unshare(CLONE_FS) 之后修改当前工作目录并不会对其他线程造成影响。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;➜  /tmp cat main.go
package main

import &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;
        &lt;span class=&#34;s2&#34;&gt;&amp;#34;fmt&amp;#34;&lt;/span&gt;
        &lt;span class=&#34;s2&#34;&gt;&amp;#34;os&amp;#34;&lt;/span&gt;
        &lt;span class=&#34;s2&#34;&gt;&amp;#34;runtime&amp;#34;&lt;/span&gt;
        &lt;span class=&#34;s2&#34;&gt;&amp;#34;syscall&amp;#34;&lt;/span&gt;
&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;

func main&lt;span class=&#34;o&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
        ch :&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; make&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;chan struct&lt;span class=&#34;o&#34;&gt;{})&lt;/span&gt;
        go func&lt;span class=&#34;o&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
                runtime.LockOSThread&lt;span class=&#34;o&#34;&gt;()&lt;/span&gt;
                defer close&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;ch&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;

                syscall.Unshare&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;syscall.CLONE_FS&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;
                syscall.Chdir&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;/etc&amp;#34;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;

                fmt.Println&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;os.Getwd&lt;span class=&#34;o&#34;&gt;())&lt;/span&gt;
        &lt;span class=&#34;o&#34;&gt;}()&lt;/span&gt;
        &amp;lt;-ch
        fmt.Println&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;os.Getwd&lt;span class=&#34;o&#34;&gt;())&lt;/span&gt;
&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;

➜  /tmp go run main.go
/etc &amp;lt;nil&amp;gt;
/tmp &amp;lt;nil&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;unshare 提供了对 Go 管理线程改造的能力，再配合上 &lt;code&gt;runtime.LockOSThread&lt;/code&gt; 锁线程的能力，基本上就可以 Go 来做一些更底层的操作了。理论上来说，&lt;a href=&#34;https://github.com/containerd/containerd/pull/7513&#34;&gt;Replace mount fork hack with CLONE_FS&lt;/a&gt; 是比前面模拟 Go-Fork-Exec 逻辑更优秀的解法。而且除此之外，unshare(CLONE_FS) 还支持 chroot, umask 等系统调用，这无疑是给容器相关的编程带来了很大便利。&lt;/p&gt;
&lt;p&gt;但目前这个优化并没有在 containerd 社区合并，原因是我们发现 Go-Runtime 自身的问题。&lt;/p&gt;
&lt;h3 id=&#34;后续跟进&#34;&gt;后续跟进&lt;/h3&gt;
&lt;p&gt;首先，Go-Runtime 的 LockOSThread 文档没有提及 Main-Thread 的特殊性。在 Linux Kernel 里，Main Thread 其实就是我们平时提到的进程；当它被 LockOSThread 了但没有被 Unlock，按照官方文档说明，这个线程会主动退出。但实际上 Main-Thread 一旦推出，整个进程就变成僵尸状态，也就是退出了，所以 Go-Runtime 并不会退出这个线程，只是将其变成不可调度状态。&lt;/p&gt;
&lt;p&gt;其实，在 Github Action Pipeline 里，我们发现 Go-Runtime 自身处理锁的时候有问题，也不知道是不是和 Main-Thread 没有退出有关导致。比较麻烦的是，每次出现的错误都不一样。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;https://github.com/fuweid/containerd-pr-7513/actions/runs/3255360436/jobs/5345228547

runtime: newstack at runtime.checkdead+0x2f5 &lt;span class=&#34;nv&#34;&gt;sp&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;0x7fb781e8ae38 &lt;span class=&#34;nv&#34;&gt;stack&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=[&lt;/span&gt;0xc00004c800, 0xc00004d000&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;
	&lt;span class=&#34;nv&#34;&gt;morebuf&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;={&lt;/span&gt;pc:0x4745df sp:0x7fb781e8ae40 lr:0x0&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
	&lt;span class=&#34;nv&#34;&gt;sched&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;={&lt;/span&gt;pc:0x47c975 sp:0x7fb781e8ae38 lr:0x0 ctxt:0x0&lt;span class=&#34;o&#34;&gt;}&lt;/span&gt;
runtime.mexit&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;0x1&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;
	/opt/hostedtoolcache/go/1.19.2/x64/src/runtime/proc.go:1545 +0x17f &lt;span class=&#34;nv&#34;&gt;fp&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;0x7fb781e8ae70 &lt;span class=&#34;nv&#34;&gt;sp&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;0x7fb781e8ae40 &lt;span class=&#34;nv&#34;&gt;pc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;0x4&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;74&lt;span class=&#34;o&#34;&gt;](&lt;/span&gt;https://github.com/fuweid/containerd-pr-7513/actions/runs/3255360436/jobs/5345228547#step:5:75&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;5df
runtime.mstart0&lt;span class=&#34;o&#34;&gt;()&lt;/span&gt;
	/opt/hostedtoolcache/go/1.19.2/x64/src/runtime/proc.go:1391 +0x89 &lt;span class=&#34;nv&#34;&gt;fp&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;0x7fb781e8aea0 &lt;span class=&#34;nv&#34;&gt;sp&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;0x7fb781e8ae70 &lt;span class=&#34;nv&#34;&gt;pc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;0x474289
runtime.mstart&lt;span class=&#34;o&#34;&gt;()&lt;/span&gt;
	/opt/hostedtoolcache/go/1.19.2/x64/src/runtime/asm_amd64.s:390 +0x5 &lt;span class=&#34;nv&#34;&gt;fp&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;0x7fb781e8aea8 &lt;span class=&#34;nv&#34;&gt;sp&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;0x7fb781e8aea0 &lt;span class=&#34;nv&#34;&gt;pc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;0x4a2725
created by github.com/fuweid/containerd-pr-&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;75&lt;span class=&#34;o&#34;&gt;](&lt;/span&gt;https://github.com/fuweid/containerd-pr-7513/actions/runs/3255360436/jobs/5345228547#step:5:76&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;13.mountAt
	/home/runner/work/containerd-pr-7513/containerd-pr-7513/mount.go:126 +0x2ac
fatal error: runtime: stack split at bad &lt;span class=&#34;nb&#34;&gt;time&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;具体问题描述都在 &lt;a href=&#34;https://github.com/golang/go/issues/56243&#34;&gt;runtime: &amp;ldquo;runtime·lock: lock count&amp;rdquo; fatal error when cgo is enabled&lt;/a&gt;，感兴趣的朋友可以关注下这个问题。&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>eBPF 动态观测之指令跳板</title>
          <link>/post/2022-bpf-kprobe-fentry-poke/</link>
          <pubDate>Sun, 12 Jun 2022 12:00:53 +0800</pubDate>
          <guid>https://fuweid.com/post/2022-bpf-kprobe-fentry-poke/</guid>
          <description>&lt;p&gt;在 containerd 自定义插件 embedshim 项目里，我借助了 Linux 内核里的 trace_sched_process_exit 观测能力，并利用 eBPF Map 记录和持久化容器进程退出事件。
这类观测能力依赖内核在关键代码路径上提前定义好钩子，它属于静态观测技术，任何变化都需要重新编译 Linux 内核。
如果我们想观测内核中的某一个关键函数或者某一行关键代码时，我们可以选择 kprobe 或者 ftrace 这类动态观测技术。&lt;/p&gt;
&lt;h2 id=&#34;kprobe---single-step&#34;&gt;kprobe - single-step&lt;/h2&gt;
&lt;p&gt;Kernel Probe(kprobe) 是一个轻量级内核指令观测的技术，用户可以指定观测内核的某一个函数，甚至可以观测函数内的某一条指令，除了 kprobe 框架自身的代码以及异常处理函数外，用户几乎可以观测内核运行的每一条指令。&lt;/p&gt;
&lt;p&gt;当 CPU 执行到被观测指令时，也就是产生了一次 &lt;strong&gt;观测事件&lt;/strong&gt;，那么 kprobe 会把当前 CPU 的寄存器信息作为输入去执行用户注册的观测程序。
然而被观测的指令由用户随机指定，考虑到性能问题，kprobe 无法在编译内核时为每一条指令预留埋点，同时我们很难在编译好的程序里动态插入指令。
基于性能和稳定性考虑，kprobe 选择了 &lt;strong&gt;单步调试&lt;/strong&gt; 的通用方案。&lt;/p&gt;
&lt;p&gt;在介绍 kprobe 方案之前，我们先简单回顾下 gdb 调试过程。为了调试某一行代码，我们先通过 &lt;code&gt;breakpoint&lt;/code&gt; 给该行打上断点，当程序运行到该行代码时就会停下来，等待我们的下一步交互。
这个时候我们就可以通过 &lt;code&gt;p&lt;/code&gt; 或者 &lt;code&gt;info&lt;/code&gt; 等命令来查看当前程序的状态，甚至我们还可以通过 &lt;strong&gt;单步调试&lt;/strong&gt; 来观察程序每条指令带来的变化。
我们利用断点和单步调试产生的 &lt;strong&gt;停顿&lt;/strong&gt; 来观测程序，这本质上也是一种埋点，内核也正是通过这种方式来实现 kprobe，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-ebpf-kprobe-fentry-poke/kprobe-single-step.svg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;x86_64 CPU 架构下的断点指令为 INT3，它是一个单字节指令 &lt;code&gt;0xcc&lt;/code&gt; 。我们可以用 INT3 来替换任何指令的 opcode，被替换的指令（以及后续指令）都将被中断所短路掉，而 CPU 将进入 do_int3 [&lt;a href=&#34;https://elixir.bootlin.com/linux/v4.19/source/arch/x86/kernel/traps.c#L581&#34;&gt;1&lt;/a&gt;] 中断处理逻辑。&lt;/p&gt;
&lt;p&gt;如上图所示，kprobe 观测的是 &lt;code&gt;Near CALL wq_worker_running&lt;/code&gt; 指令。在观测之前，kprobe 申请新的空间 &lt;code&gt;copy-ip-addr&lt;/code&gt; 来存储被观测指令的内容 &lt;code&gt;e8 fc 57 77 ff&lt;/code&gt;，然后调用 &lt;code&gt;arch_arm_kprobe/text_poke&lt;/code&gt; [&lt;a href=&#34;https://elixir.bootlin.com/linux/v4.19/source/arch/x86/kernel/kprobes/core.c#L500&#34;&gt;2&lt;/a&gt;] 将 &lt;code&gt;Near CALL 0xe8&lt;/code&gt; 替换成 &lt;code&gt;INT3 0xcc&lt;/code&gt;。打完断点后，一旦有 CPU 执行到这条指令上，那 CPU 必然会进入到 INT3 中断处理逻辑里的 &lt;code&gt;do_int3/kprobe_int3_handler&lt;/code&gt; [&lt;a href=&#34;https://elixir.bootlin.com/linux/v4.19/source/arch/x86/kernel/kprobes/core.c#L655&#34;&gt;3&lt;/a&gt;]。&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;kprobe_int3_handler&lt;/code&gt; 中断逻辑里，kprobe 首先会标记当前 CPU 为 &lt;code&gt;KPROBE_HIT_ACTIVE&lt;/code&gt; 状态，表明该 CPU 正在处理 kprobe 的观测事件。
而通过内核模块或者 eBPF 系统调用注册的观测程序，它们都被聚合到 &lt;code&gt;pre_handler&lt;/code&gt; 钩子函数内，&lt;code&gt;kprobe_int3_handler&lt;/code&gt; 会先执行这些观测程序，再去运行观测的指令。&lt;/p&gt;
&lt;p&gt;在执行被观测的指令时，内核并不会将 &lt;code&gt;INT3&lt;/code&gt; 还原成 &lt;code&gt;Near CALL&lt;/code&gt;，毕竟还会有其他 CPU 可能会触发该指令的观测事件，因此该 CPU 需要跳到 &lt;code&gt;copy-ip-addr&lt;/code&gt; 指令地址上运行原先的指令。
&lt;code&gt;copy-ip-addr&lt;/code&gt; 仅保存一条指令，&lt;code&gt;Near CALL&lt;/code&gt; 返回的指令地址依然在原先指令 &lt;code&gt;orig-ip-addr&lt;/code&gt; 地址之后；加上 &lt;code&gt;Near CALL&lt;/code&gt; 返回指令地址以及下一条指令地址的计算都是基于当前指令地址计算出来的，该 CPU 需要一个修正指令地址和返回指令地址的机会，而这个机会由 &lt;strong&gt;单步调试&lt;/strong&gt; 所产生的中断来提供。&lt;/p&gt;
&lt;p&gt;首先 kprobe 会将 CPU 状态切换成 &lt;code&gt;KPROBE_HIT_SS&lt;/code&gt;，表明 CPU 正在准备单步调试状态。内核会将寄存器中的 EIP 指向 &lt;code&gt;copy-ip-addr&lt;/code&gt;，并配置 &lt;code&gt;X86_EFLAGS_TF&lt;/code&gt; 标记，这个标记会让 CPU 执行完一条指令后产生调试中断，保证 CPU 会进入 &lt;code&gt;do_debug/kprobe_debug_handler&lt;/code&gt; [&lt;a href=&#34;https://elixir.bootlin.com/linux/v4.19/source/arch/x86/kernel/kprobes/core.c#L936&#34;&gt;4&lt;/a&gt;] 中断函数。当进入 &lt;code&gt;kprobe_debug_handler&lt;/code&gt; 中断处理后，CPU 的下一条执行指令地址变成了 &lt;code&gt;copy-ip-addr + 5 + fc 57 77 ff&lt;/code&gt;，其中 &lt;code&gt;copy-ip-addr + 5&lt;/code&gt; 表示 &lt;code&gt;copy-ip-addr&lt;/code&gt; 的下一条指令地址，因为这个 Near CALL 是五字节指令，而 &lt;code&gt;fc 57 77 ff&lt;/code&gt; 表示指令之间的距离。但 &lt;code&gt;fc 57 77 ff&lt;/code&gt; 是由 &lt;code&gt;orig-ip-addr + 5&lt;/code&gt; 和 &lt;code&gt;wq_worker_running&lt;/code&gt; 计算出来的，因此我们需要将其修正成正确的 &lt;code&gt;wq_worker_running&lt;/code&gt; 的位置。同理，函数栈里的返回地址也需要更正: &lt;code&gt;return-eip&lt;/code&gt; - &lt;code&gt;copy-ip-addr&lt;/code&gt; 可获取被观测指令的长度，仅需要将指令长度加到 &lt;code&gt;orig-ip-addr&lt;/code&gt; 就可以拿到真正的返回指令地址了。
之后 kprobe 将移除 &lt;code&gt;X86_EFFLAGS_TF&lt;/code&gt; 标记，取消单步调试状态，并更成 CPU 成 &lt;code&gt;KPROBE_HIT_SSDONE&lt;/code&gt; 状态，表明单步调试结束。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;NOTE: KPROBE_HIT_XYZ 状态是用来记录当前 kprobe 运行状态。用户自定义程序可能会调用到一个被 kprobe 观测的指令上了，因此在执行 &lt;code&gt;pre/post_handler&lt;/code&gt; 时会再次触发 INT3 中断。而 per-CPU 的 kprobe 状态记录可以用来判断当前 INT3 触发是第一次触发 KPROBE_HIT_ACTIVE 还是再次触发 KPROBE_HIT_REENTER。目前 kprobe 仅允许发生一次 KPROBE_HIT_REENTER。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;虽然单步执行模式可以让用户观测任何指令，但每条指令都要产生两次中断；如果观测在关键的代码路径上，这种模式势必会影响到内核的性能。
kprobe 针对两次中断共有两个优化方案，我们先来看看 2021 年初的一个优化方案。&lt;/p&gt;
&lt;h2 id=&#34;kprobe---x86-insn-emulation&#34;&gt;kprobe - x86 Insn Emulation&lt;/h2&gt;
&lt;p&gt;2021 年优化提交名叫 &lt;code&gt;x86/kprobes: Use int3 instead of debug trap for single-step&lt;/code&gt; [&lt;a href=&#34;https://lore.kernel.org/all/161469874601.49483.11985325887166921076.stgit@devnote2/T/#mbb8fd3431b354681310a12741adfd57fad0e7d95&#34;&gt;5&lt;/a&gt;]，它是通过离线模拟被观测指令来去除单步调试中断，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-ebpf-kprobe-fentry-poke/kprobe-emulate-execute-out-of-line.svg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;我们还是使用 &lt;code&gt;Near CALL&lt;/code&gt; 指令的例子来解释。根据 x86_64 IA-32 架构开发文档对 &lt;code&gt;Near CALL&lt;/code&gt; 的描述，我们可以将用两条指令来模拟它：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;将 &lt;code&gt;Near CALL&lt;/code&gt; 的下一条指令 &lt;code&gt;next-ip-addr&lt;/code&gt; 地址压入栈，作为函数返回的 &lt;code&gt;return-eip&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;通过 &lt;code&gt;next-ip-addr&lt;/code&gt; 计算出来的指令地址可以直接用 &lt;code&gt;Near JMP&lt;/code&gt; 指令跳过去&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;换言之，&lt;code&gt;Near CALL&lt;/code&gt; 指令产生的结果完全可以在 &lt;code&gt;do_int3/kprobe_int3_handler&lt;/code&gt; 中断处理中离线模拟出来。
在上文提到的单步调试里，&lt;code&gt;do_debug/kprobe_debug_handler&lt;/code&gt; 调试中断处理需要通过 &lt;code&gt;resume_execution&lt;/code&gt; [&lt;a href=&#34;https://elixir.bootlin.com/linux/v4.19/source/arch/x86/kernel/kprobes/core.c#L869&#34;&gt;6&lt;/a&gt;] 里面修正 CPU 的寄存器信息，在我看来，这本质上和模拟没有区别。
所以这个方案通过模拟指令的方式来移除了不必要的单步调试中断，这对性能有极大的提升。不过需要说明的是，该方案并非支持所有指令的模拟，根据邮件里的讨论来看，&lt;code&gt;prepare_emulation&lt;/code&gt; [&lt;a href=&#34;https://elixir.bootlin.com/linux/v5.18/source/arch/x86/kernel/kprobes/core.c#L584&#34;&gt;7&lt;/a&gt;] 模拟的指令足以覆盖大部分场景。&lt;/p&gt;
&lt;p&gt;指令模拟优化掉了单步调试中断，但每次观测指令达到时，kprobe 还是会触发 INT3 中断，对关键代码执行路径还是存在影响。&lt;/p&gt;
&lt;h2 id=&#34;kprobe---detour-buffer&#34;&gt;kprobe - detour buffer&lt;/h2&gt;
&lt;p&gt;Linux 内核社区在 2009 年提出了 kprobe jump optimization [&lt;a href=&#34;https://lwn.net/Articles/365833/&#34;&gt;8&lt;/a&gt;] 方案，该方案的思路是通过 &lt;code&gt;Near JMP&lt;/code&gt; 指令来模拟 &lt;code&gt;do_int3/kprobe_int3_handler&lt;/code&gt; 中断处理。
该方案在各大发行版里目前默认开启，在当时的优化结果比单步调试快了近 10 倍。虽然它存在一定的局限性，但不妨碍我们了解它，这个方案和后面提到的 ftrace 设计理念一致。&lt;/p&gt;
&lt;p&gt;该优化方案通过修改被观测指令成 &lt;code&gt;Near JMP&lt;/code&gt; 指令，一旦产生观测事件，CPU 将跳到预定义好的一个代码片段上模拟 &lt;code&gt;kprobe_int3_handler&lt;/code&gt; 处理逻辑。
但 &lt;code&gt;Near JMP (Rel32)&lt;/code&gt; 是一个 &lt;strong&gt;五字节&lt;/strong&gt; 的指令，如果在指令更新过程中，中间结果被其他 CPU 读取执行了，那么将产生不可预知的行为，这甚至会造成内核崩溃。
为了保证能安全更新被观测指令，kprobe 还是需要依赖 INT3 中断来协助处理指令更新。&lt;/p&gt;
&lt;p&gt;假设我们已经有了模拟 kprobe_int3_handler 的代码片段，为了方便解释，我们将其简称为 &lt;strong&gt;跳板&lt;/strong&gt; （虽然方案和文档都称之为 detour buffer，但我觉得 跳板 更容易理解些）。
在这里，我们继续拿之前 &lt;code&gt;Near CALL&lt;/code&gt; 的观测指令来举例子。被观测指令 &lt;code&gt;e8 fc 57 77 ff&lt;/code&gt; 被 kprobe 更新成 &lt;code&gt;INT3[cc] fc 57 77 ff&lt;/code&gt;; 那么任何时刻 CPU 执行这条指令时，它们都会触发 INT3 中断，即使 &lt;code&gt;fc 57 77 ff&lt;/code&gt; 这个值被改成非法的值，CPU 也不会使用这个值，INT3 给我们形成了天然的屏障，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-ebpf-kprobe-fentry-poke/kprobe-poke-update-with-int3.svg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;kprobe jump 优化方案是一个异步的操作，该方案会触发一个 kworker [&lt;a href=&#34;https://elixir.bootlin.com/linux/v5.18/source/kernel/kprobes.c#L515&#34;&gt;9&lt;/a&gt;] 来执行被观测指令的更新。那么在指令更新前，CPU 还是可以会触发观测事件，这个时候的处理链路还是会走到 &lt;code&gt;kprobe_int3_handler/setup_singlestep&lt;/code&gt; [&lt;a href=&#34;https://elixir.bootlin.com/linux/v5.18/source/arch/x86/kernel/kprobes/core.c#L833&#34;&gt;10&lt;/a&gt;]。但它并非使用前面提到的指令模拟，而是直接通过 &lt;code&gt;setup_detour_execution&lt;/code&gt; [&lt;a href=&#34;https://elixir.bootlin.com/linux/v5.18/source/arch/x86/kernel/kprobes/opt.c#L560&#34;&gt;11&lt;/a&gt;] 将寄存器的 EIP 转化成 &lt;strong&gt;跳板&lt;/strong&gt; 代码指令地址上，相当于模拟了一次更新后的 &lt;code&gt;Near JMP&lt;/code&gt; 指令。如果 kworker 开始调用 &lt;code&gt;text_poke_bp&lt;/code&gt; [&lt;a href=&#34;https://elixir.bootlin.com/linux/v5.18/source/arch/x86/kernel/alternative.c#L1578&#34;&gt;12&lt;/a&gt;] ，那么内核会告知所有 CPU，有一个 CPU 当前正在处理指令升级。如果其他 CPU 触发了该指令的 INT3 中断，那么 CPU 将会进入到 &lt;code&gt;poke_int3_handler&lt;/code&gt; [&lt;a href=&#34;https://elixir.bootlin.com/linux/v5.18/source/arch/x86/kernel/alternative.c#L1240&#34;&gt;13&lt;/a&gt;] 中断处理逻辑，同样的它会根据指令来调用不同的模拟逻辑：在 kprobe 指令优化场景下，&lt;code&gt;poke_int3_handler&lt;/code&gt; 将会调用 &lt;code&gt;int3_emulate_jmp&lt;/code&gt; 模拟逻辑，效果和前面提到的 &lt;code&gt;setup_detour_execution&lt;/code&gt; 一致。那么有了 INT3 中断这一屏障后，&lt;code&gt;text_poke_bp&lt;/code&gt; 就可以放心更新指令了，其中 &lt;code&gt;text_poke_bp&lt;/code&gt; 更新有三步：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;指令首地址 opcode 更新成 INT3，确保 &lt;code&gt;poke_int3_handler&lt;/code&gt; 能模拟预期的行为；&lt;/li&gt;
&lt;li&gt;将指令后半部分更新成预期的值;&lt;/li&gt;
&lt;li&gt;将指令的首地址 opcode 更新成预期的值。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;code&gt;text_poke_bp&lt;/code&gt; 每一步更新都会同步给所有 CPU，确保它们读到的都是最新的指令内容。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-ebpf-kprobe-fentry-poke/kprobe-jmp-detour-buffer.svg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;在 kprobe 场景下，最终被观测指令将会变成跳到 &lt;strong&gt;跳板&lt;/strong&gt; 的指令，而这跳板里的指令内容如上图所示。
跳板上的指令由 &lt;code&gt;arch_prepare_optimized_kprobe&lt;/code&gt; [&lt;a href=&#34;https://elixir.bootlin.com/linux/v5.18/source/arch/x86/kernel/kprobes/opt.c#L411&#34;&gt;14&lt;/a&gt;] 代码生成，其中 &lt;code&gt;optimized_callback&lt;/code&gt; [&lt;a href=&#34;https://elixir.bootlin.com/linux/v5.18/source/arch/x86/kernel/kprobes/opt.c#L176&#34;&gt;15&lt;/a&gt;] 用来执行 kprobe 的 &lt;code&gt;pre_handler&lt;/code&gt;。
跳板指令最后为被观测指令的 &lt;strong&gt;副本&lt;/strong&gt; 以及跳回到被观测指令的下一指令，其中这个副本并非直接拷贝原来的指令。
对于 &lt;code&gt;Near CALL&lt;/code&gt; 或者 &lt;code&gt;Near JMP&lt;/code&gt; 等具有相对位置的指令，我们需要根据当前跳板和被观测指令之间的差值来更新指令，这样才可以确保被观测指令可以离线运行，原理和单步调试里的 &lt;code&gt;resume_execution&lt;/code&gt; 类似。&lt;/p&gt;
&lt;p&gt;这个优化效果十分显著，但它对被观测指令有一定的要求。在前面我举的例子里，被观测指令的长度和 &lt;code&gt;Near JMP&lt;/code&gt; 指令长度一致，所以被修改的指令仅一条。
但在 x86_64 架构里，指令长度是变长编码，常用的指令编码需要的字节少，而不常用的字节多。如果被观测的指令短于五字节，那么指令修改必定涉及到多条。
而 &lt;code&gt;text_poke_bp&lt;/code&gt; 更新的指令如果跨越了多个指令，那么 INT3 中断将无法保证中间修改的状态不被访问。假设某一条指令可以跳过 INT3 指令访问正在修改的值，
那么 CPU 执行时必然会出现不可预知的情况。因此 kprobe 会通过 &lt;code&gt;can_optimize&lt;/code&gt; [&lt;a href=&#34;https://elixir.bootlin.com/linux/v5.18/source/arch/x86/kernel/kprobes/opt.c#L296&#34;&gt;16&lt;/a&gt;] 来扫描被观测指令所在函数的每一条指令，以确保可使用跳板模式来优化。&lt;/p&gt;
&lt;p&gt;总的来说，这个优化方案要求被观测指令不能涉及到异常处理、不能出现跳跃到被修改指令的中间位置以及被观测指令是可以脱离原上下文离线运行的。
有一定的局限性，但如果我们想要观测某一个函数入口时，我们还是可以使用上这个优化。&lt;/p&gt;
&lt;h2 id=&#34;kretprobe--kprobe--rethook&#34;&gt;kretprobe = kprobe + rethook&lt;/h2&gt;
&lt;p&gt;kretprobe 提供了观测函数返回时刻 CPU 上下文的能力。&lt;/p&gt;
&lt;p&gt;以常用的 &lt;code&gt;Near CALL&lt;/code&gt; 为例，我们先回顾下 x86_64 下的函数调用和返回的指令：在函数调用时，CPU 会将返回的指令地址压入栈顶，再执行函数入口处的指令; 当函数返回 (&lt;code&gt;RET&lt;/code&gt; 指令） 时，CPU 会将栈顶的返回指令地址更新到 EIP 寄存器上，这样 CPU 就可以按照原来的分支继续执行。从出栈入栈的角度看，个人能想到观测函数返回的上下文总有两种方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在函数入口处修改返回指令地址，当函数返回时，CPU 就能到我们指定的指令地址上运行，从而完成观测事件的处理；&lt;/li&gt;
&lt;li&gt;使用 kprobe 观测函数的返回指令。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;第二个方案有一个明显的弊端，注册 kretprobe 的时候需要扫描整个函数的指令。一般函数入口的第一条指令都是可以优化成 &lt;code&gt;Near JMP&lt;/code&gt; 指令，无需 INT3 中断。基于这样的假设，kretprobe 选择第一种方式会比较合适，如下图所示（为了方便解释，我使用 x86 kprobe 离线模拟指令的方式来说明）。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-ebpf-kprobe-fentry-poke/kretprobe-with-emulate-kprobe.svg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;首先 kretprobe 会注册一个 kprobe 到函数入口处。这个 kprobe 用来记录当前线程 &lt;code&gt;current&lt;/code&gt; 的栈帧 EBP 以及真实返回指令地址 Return-EIP，并修改返回指令地址成 &lt;code&gt;arch_rethook_trampoline&lt;/code&gt; 函数。被观测的函数可能没有返回值，也可能递归调用多次，kretprobe 并不会无限制地调用申请 &lt;code&gt;rethook node&lt;/code&gt; 来存储上下文，因此用户需要通过配置 &lt;code&gt;maxactive&lt;/code&gt; 来决定可以同时处理返回。&lt;/p&gt;
&lt;p&gt;在保存 &lt;code&gt;rethook node&lt;/code&gt; 信息时，kretprobe 采用入栈的形式保存数据。举一个例子，假设 &lt;code&gt;schedule&lt;/code&gt; 函数调用了 &lt;code&gt;wq_worker_running&lt;/code&gt; 函数，同时它两都属于 kretprobe 的观测对象。当一个线程调用了 &lt;code&gt;schedule&lt;/code&gt; 函数，也走到了 &lt;code&gt;schedule&lt;/code&gt; 函数内的 &lt;code&gt;wq_worker_running&lt;/code&gt; 函数，那么 kretprobe 在处理返回事件时，第一个处理的应该是 &lt;code&gt;wq_worker_running&lt;/code&gt; 的返回时。kretprobe 采用了栈的形式保存 &lt;code&gt;rethook node&lt;/code&gt; 信息，有利于 &lt;code&gt;arch_rethook_trampoline&lt;/code&gt; 快速地找到正确的 &lt;code&gt;rethook node&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;arch_rethook_trampoline&lt;/code&gt; [&lt;a href=&#34;https://elixir.bootlin.com/linux/v5.18/source/arch/x86/kernel/rethook.c#L62&#34;&gt;17&lt;/a&gt;] 是由一段汇编代码拼成，它主要是用来处理用户注册的观测程序，处理完毕后释放掉 &lt;code&gt;rethook node&lt;/code&gt;，并还原成正确的 Return-EIP。&lt;/p&gt;
&lt;p&gt;rethook 的概念是近期 &lt;code&gt;x86,rethook,kprobes: Replace kretprobe with rethook on x86&lt;/code&gt; [&lt;a href=&#34;https://lore.kernel.org/bpf/164826163692.2455864.13745421016848209527.stgit@devnote2/&#34;&gt;18&lt;/a&gt;] 引入， 和早期的 &lt;code&gt;kretprobe-instance&lt;/code&gt; 概念作用类似。对于我而言，rethook 更容易理解 kretprobe，毕竟 kretprobe 是通过 kprobe 加入的一种回调钩子，等价于 kprobe + rethook。&lt;/p&gt;
&lt;h2 id=&#34;fentryfexit---bpf_trampoline&#34;&gt;fentry/fexit - bpf_trampoline&lt;/h2&gt;
&lt;p&gt;fentry/fexit 和 kprobe/kretprobe 功能类似，其中 &lt;code&gt;f&lt;/code&gt; 表示的是函数，fentry/fexit 分别用来观测函数入口和函数返回的事件。相比于 kprobe，它具有静态观测技术的特点。在高版本的 GCC 里，GCC 提供了 &lt;code&gt;-mentry&lt;/code&gt; 选项来为每一个函数入口生成一个埋点函数。为了实现 ftrace 技术，在编译内核会带上这个选项。由于可观测的函数很多，关键路径上频繁调用埋点函数会造成 &lt;strong&gt;13%&lt;/strong&gt; 的性能损失，因此内核在 Link 阶段会将这条 &lt;code&gt;Near CALL&lt;/code&gt; 指令替换成 &lt;code&gt;NOP5 指令&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-ebpf-kprobe-fentry-poke/gcc-mentry.svg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NOP5&lt;/code&gt; 指令长度为五个字节，所以对于那些可观测的函数来说，kprobe jump 优化方案肯定是适用的。
但 fentry/fexit 采用的并不是 &lt;code&gt;Near JMP&lt;/code&gt;，而是 &lt;code&gt;Near CALL&lt;/code&gt;。下图展示的是 fentry eBPF 观测程序关联到 &lt;code&gt;do_unlinkat&lt;/code&gt; 函数入口处的过程。&lt;/p&gt;
&lt;p&gt;首先 &lt;code&gt;bpf_tramp_image_alloc&lt;/code&gt; [&lt;a href=&#34;https://elixir.bootlin.com/linux/v5.18/source/kernel/bpf/trampoline.c#L290&#34;&gt;19&lt;/a&gt;] 函数会申请一个页大小的指令内存空间，而这个指令内存空间的地址将有一个符号标记，符号的命名规则为 &lt;code&gt;bpf_trampoline_$key_$index&lt;/code&gt;。 &lt;code&gt;key&lt;/code&gt; 是通过 BTF ID 来生成，只要关联的函数固定，这个 &lt;code&gt;key&lt;/code&gt; 值是固定的; 而 &lt;code&gt;index&lt;/code&gt; 是一个自增的 ID，每一次调用 &lt;code&gt;bpf_tracing_prog_attach&lt;/code&gt; 函数来关联 eBPF 程序时，它会都 &lt;code&gt;+1&lt;/code&gt;。eBPF fentry 并没有 kprobe 那样的 &lt;code&gt;pre_handler&lt;/code&gt; 聚合函数，每次关联观测程序都需要重新生成汇编指令，而重新生成的 &lt;code&gt;bpf_tramp_image&lt;/code&gt; 将通过新的 &lt;code&gt;index&lt;/code&gt; 来做区分。我们可以通过 &lt;code&gt;/proc/kallsyms&lt;/code&gt; 来查看这个符号，当然也可以通过符号后的 &lt;code&gt;index&lt;/code&gt; 来查看被观测函数的关联次数。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-ebpf-kprobe-fentry-poke/fentry-fexit-bpf_trampoline_link.svg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;fentry 会通过 &lt;code&gt;arch_prepare_bpf_trampoline&lt;/code&gt; [&lt;a href=&#34;https://elixir.bootlin.com/linux/v5.18/source/arch/x86/net/bpf_jit_comp.c#L1981&#34;&gt;20&lt;/a&gt;] 函数在新申请的 &lt;code&gt;bpf_tramp_image&lt;/code&gt; 上添加必要的指令：将 &lt;code&gt;do_unlinkat&lt;/code&gt; 的函数参数压入栈，然后生成调用已关联的 eBPF 观测程序指令，当然中间还会记录 eBPF 程序调用的耗时，最后会调用 &lt;code&gt;RET&lt;/code&gt; 来结束该过程。最后这个 &lt;code&gt;bpf_tramp_image&lt;/code&gt; 跳板函数会替换掉 &lt;code&gt;do_unlinkat&lt;/code&gt; 函数入口处 &lt;code&gt;NOP5&lt;/code&gt; 的指令。替换过程和 kprobe jump 优化一样，同样是通过 &lt;code&gt;text_poke_bp&lt;/code&gt; 三步更新模式来替换，最后这个 &lt;code&gt;NOP5&lt;/code&gt; 会变成 &lt;code&gt;Near CALL &amp;lt;bpf_tramopline_$key_1&amp;gt;&lt;/code&gt;。当然，如果再次添加一个新的 &lt;code&gt;fentry/do_unlinkat&lt;/code&gt; 观测程序，那么该指令将从 &lt;code&gt;Near CALL &amp;lt;bpf_trampoline_$key_1&amp;gt;&lt;/code&gt; 变成 &lt;code&gt;Near CALL &amp;lt;bpf_tramopline_$key_2&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;对于 fexit 而言，&lt;code&gt;arch_prepare_bpf_trampoline&lt;/code&gt; 生成的指令稍微复杂些，但也不难理解，其中唯一的区别在于， &lt;code&gt;do_unlinkat&lt;/code&gt; 的调用是发生在 &lt;code&gt;bpf_trampoline_$key_$index&lt;/code&gt; 函数体内，而 &lt;code&gt;bpf_trampoline_$key_$index&lt;/code&gt; 函数返回后将直接返回到调用 &lt;code&gt;do_unlinkat&lt;/code&gt; 的地方。在这里，就不再贴图说明啦，更多细节可以查看 arch_prepare_bpf_trampoline 函数体。&lt;/p&gt;
&lt;h2 id=&#34;summary&#34;&gt;Summary&lt;/h2&gt;
&lt;p&gt;kprobe 和 fentry/fexit 使用的动态观测技术大同小异，都是通过跳板形式来完成对指令或者函数的观测,都是在开着飞机换引擎。就目前了解的情况来看，fentry eBPF 跳板模式的代码结构和内存使用更简单，没有额外的 kprobe perf_event 数据结构介入。如果内核支持 fentry 的话，我倾向使用 fentry :)。&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>embedshim: 内核是我的边车</title>
          <link>/post/2022-embedshim-kernel-is-my-sidecar/</link>
          <pubDate>Mon, 02 May 2022 18:20:45 +0800</pubDate>
          <guid>https://fuweid.com/post/2022-embedshim-kernel-is-my-sidecar/</guid>
          <description>&lt;p&gt;在 2019 年的时候，当时所在的团队正在开始大规模使用 containerD，我们初期遇到较多 containerd-shim 的死锁等稳定性问题，我们不得不去思考去除 containerd-shim 进程的可能性。由于当时技术选型上的限制，containerd-shim 必须作为容器 subreaper 而存在。直到去年才留意到 &lt;a href=&#34;https://lwn.net/Articles/794707/&#34;&gt;pidfd pollable&lt;/a&gt;，我才发现 containerd-shim 管控面其实是可以被移除。我顺着这个思路作出了 &lt;a href=&#34;https://github.com/fuweid/embedshim&#34;&gt;embedshim&lt;/a&gt; 这个 containerD 第三方插件。在介绍这个插件之前，我们先简单回顾下 containerd-shim 的发展历程。&lt;/p&gt;
&lt;h2 id=&#34;1-从-docker-的原地升级到-containerd-shim&#34;&gt;1. 从 docker 的原地升级到 containerd-shim&lt;/h2&gt;
&lt;h3 id=&#34;11-原地升级的需求&#34;&gt;1.1 原地升级的需求&lt;/h3&gt;
&lt;p&gt;最初 dockerd 的容器进程管理是非常简单粗暴的，它采用了 Fork-and-Wait 模式来监控容器状态，并通过无名管道接管容器的标准输入输出。如果 dockerd 进程重启，那么它将无法重新监控容器的状态变化，而这些已运行的容器都将变成 &lt;code&gt;孤儿&lt;/code&gt;。为了防止资源残留，dockerd 重启后的第一件事就是停掉正在运行的容器。然而节点组件的重启和周期性升级都属于正常操作，dockerd 停服重启应保证正在运行的容器不受影响。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-embedshim-kernel-is-my-sidecar/docker-issue-2658.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;这 &lt;a href=&#34;https://github.com/moby/moby/issues/2658&#34;&gt;docker#2658&lt;/a&gt; 帖子记录了当时 dockerd 原地升级的细节讨论；当然除了方案讨论外，用户对该需求落地呼声评论是更强烈些的。组件进程重启涉及到的细节比较多，但可以归类为状态恢复以及临时（残留）数据的清理，比如有讨论清理未完成的网络初始化资源，有讨论如何恢复接管容器的标准输出，还有讨论如何做镜像下载的断点续下等等。而对于本文的主题 - 如何重新接管存量容器进程的场景而言，个人认为仅需要考虑下面两个问题即可：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;如何保证容器退出事件不丢失？&lt;/li&gt;
&lt;li&gt;如何重新接管容器的标准输入输出？&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首先，我们来看第一个问题。进程退出码能正确反映一个进程是以什么状态结束的，有正常退出的，有收到 SIGTERM 信号优雅停服的，还有因无法分配新的内存而被内核 SIGKILL 的。开发者和运维人员可以根据进程退出码以及关键日志信息来做 &lt;code&gt;非预期退出事件&lt;/code&gt; 的诊断，所以对于进程管理方案而言，进程退出码必须要能被正确捕获，而当时最稳妥的方式是有一个常驻进程来做容器进程的 subreaper。&lt;/p&gt;
&lt;p&gt;相比于第一个问题，第二个问题处理起来要简单些。经历过早期节点运维的朋友都知道，在容器化之前呢，大部分业务进程的管理是通过 systemd-service 来实现。业务进程直接被一号进程所监管，同时它们采用了 Headless 无界面无交互的方式运行。它们的标准输出通常以 UDS 流的形式传递给 &lt;a href=&#34;https://www.freedesktop.org/software/systemd/man/systemd-journald.service.html#Stream%20logging&#34;&gt;systemd-journald&lt;/a&gt; 服务，由 systemd-journald 来做日志持久化和轮动存储。&lt;/p&gt;
&lt;p&gt;容器化后的节点运维比 systemd 模式要稍微复杂些。容器化产生了根路经和资源视图隔离，容器管理面需要封装 nsenter 和 chroot/pivot 等系统调用来提供便捷的运维通道。dockerd 进程提供了 execCreate/execStart/execAttach HTTP 接口来进入到容器隔离视图，这种具有交互能力的运维通道必定会感知 dockerd 停服。但个人认为这种感知是可接受的，只要能保证容器标准输出不因 dockerd 停服而丢失即可。在标准输入输出的接管上，dockerd 并没有采用 UDS 流模式，而是采用有名管道的方式。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-embedshim-kernel-is-my-sidecar/sec1-systemd-journald.svg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;一般情况下，systemd-journald 服务会备份 UDS 通信的文件句柄在一号进程那，否则 systemd-journald 重启将自动关闭 UDS 通信管道，业务进程将收到 SIGPIPE 错误，该通信管道将无法接受数据，只能通过重启业务进程来解决问题。但 dockerd 进程没有这样的福利，它只能选择通信管道可文件实体化的有名管道。只要数据产生者以 &lt;code&gt;读写模式&lt;/code&gt; 开启有名管道，即使作为接收端的 dockerd 停服了也不会产生 SIGPIPE 错误，最差也就是停服时间长导致业务进程阻塞（systemd-journald 同样有该问题）。&lt;/p&gt;
&lt;p&gt;所以回到最初的问题上，dockerd 需要一个常驻进程来做容器进程的 subreaper。&lt;/p&gt;
&lt;h3 id=&#34;12-live-restore-containerd-shim-的雏形&#34;&gt;1.2 live-restore： containerd-shim 的雏形&lt;/h3&gt;
&lt;p&gt;从 2013 年到 2016 年期间，docker 社区先后将 libcontainer(runC) 实现捐赠给 OCI 以及 libcontainerd 组件进程化，这些里程碑的出现推动了 docker 原地升级的落地，并在 2016 年的 v1.12 大版本里推出了 &lt;code&gt;live-restore: true&lt;/code&gt; 原地升级的能力。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-embedshim-kernel-is-my-sidecar/docker-containerd-shim.svg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;如上图所示，docker-containerd-shim(后称 shim) 将作为容器进程的父进程，它仅用于转发容器的标准输入输出和监控容器状态变化。由于 shim 功能单一，它编译后仅有 2 MB 左右。如果容器数目不多，而且没有过多的数据转发给 dockerd，节点上的 shim 进程消耗资源属于可控的状态。至于这里为什么会多一个 docker-containerd 进程，个人猜测这和后续捐赠动作有关，docker-containerd 后续将会成为独立项目 containerD 并捐赠给 CNCF 基金会;而 containerD 的定位是 &lt;strong&gt;an industry-standard container runtime&lt;/strong&gt;，开发者可以根据自己的需要来定制容器管控面，比如早期的 containerd-CRI 组件，buildkitd 镜像构建服务以及阿里巴巴的 PouchContainer 引擎等等，所以 v1.12 大版本把 dockerd 拆成三层管控，个人理解这是为了方便后续的集成。&lt;/p&gt;
&lt;p&gt;docker v1.12 版本是一个重要的里程碑，它的出现基本上预示着容器技术将会在生产环境的大规模使用。当时 docker 社区的版本里称之为 Deamonless Container，即不需要常驻进程来管理容器。其实阿，这里的 Deamonless 指的是 dockerd。&lt;/p&gt;
&lt;h3 id=&#34;13-cncf-版本的-containerd-shim&#34;&gt;1.3 CNCF 版本的 containerd-shim&lt;/h3&gt;
&lt;p&gt;到了 2017 年，docker 将 docker-containerd 组件&lt;a href=&#34;https://www.cncf.io/announcements/2017/03/29/containerd-joins-cloud-native-computing-foundation/&#34;&gt;捐赠&lt;/a&gt;给了 CNCF 基金会，并以 containerD(Con-tay-ner-D) 全新的项目出现。从个人角度来看，containerD 是 OCI 镜像格式标准、镜像分发标准以及容器运行时管控的最佳实践。containerD 目标是成为一个工业级别的容器引擎，可用它来对接任何自定义的管控需求：向上可以对接 Kubernetes Container Runtime Interface(CRI) 和构建镜像的 buildkitd；对下可以管理 Kata-Container, gVisor, Windows-Container 等不同的容器运行时。&lt;/p&gt;
&lt;p&gt;但在 containerD 项目初期，容器生命周期管控逻辑集中在 containerD 进程里。为了对接不同的容器运行时，containerD 将管控逻辑下沉到 shim 实现上，如下图所示。容器运行时的作者仅需要实现 shim 接口就可以和 containerD 做第三方插件的集成。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-embedshim-kernel-is-my-sidecar/cncf-containerd-shim-API.svg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;除了插件化增强外，CNCF 版本的 shim 针对普通容器管理做了两个优化：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Exec 管理将由容器所属的 shim 管理&lt;/li&gt;
&lt;li&gt;不同容器之间可以共享同一个 shim 进程&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/containerd/containerd/pull/3004&#34;&gt;不同容器共享同一个 shim 进程的方案&lt;/a&gt;是用来优化 kubernetes 场景下的内存资源问题。一个具有 RPC 能力的 shim 活跃内存就有 3 MB (计算逻辑为 memory.usage_in_bytes - memory.stat.inactive_file), 而一个 Pod 默认就有两个容器；而在云原生场景下，业务容器配置个日志采集、Service Mesh 等边车型容器是非常常见的。如果一个容器就需要 3 MB 常驻内存，kubernetes 场景下节点一般都有 10-20+ Pod, 啥都没做就轻松消耗上百兆资源，放大到整个基建以及数据中心都是比较客观的数字。共享模式可优化多容器的 Pod 资源，边车型的容器越多时，效果越明显。&lt;/p&gt;
&lt;h3 id=&#34;14-有可能去除-runc-shim-吗&#34;&gt;1.4 有可能去除 runC shim 吗?&lt;/h3&gt;
&lt;p&gt;在 containerD 架构设计里，shim 是一个很关键的抽象概念，它做到了 Out-of-Tree 模式对接各式各样的容器运行时，尤其是对 Kata-Container Host 管控面的优化上发挥了重要作用。但对于我们常用的 runC 运行时而言，它并不需要常驻的 QEMU 进程，也不需要常驻的 Application Kernel，容器进程和普通进程一样，进程间共享同一个内核。那么回顾下最初 dockerd 的管控逻辑，如果我们能保证进程退出码不会丢失，那么 runC shim 是否可以被优化掉？&lt;/p&gt;
&lt;p&gt;答案是可以！&lt;/p&gt;
&lt;p&gt;我个人开源了 embedshim 项目，它是一个 containerD 第三方容器管控插件，它不仅保证容器退出码不会丢失，还能确保 containerD 重启后依然能感知到容器进程的退出事件。使用它之后，runC shim 将被移除，缩短整个容器管控链路，节省不必要的资源开销。&lt;/p&gt;
&lt;h2 id=&#34;2-内核是我们的边车&#34;&gt;2. 内核是我们的边车！&lt;/h2&gt;
&lt;p&gt;embedshim 将监控进程退出码的工作几乎都交给了内核。&lt;/p&gt;
&lt;p&gt;首先内核支持使用 &lt;code&gt;sched_process_exit&lt;/code&gt; 来追踪处理进程退出的事件。在当前讨论的场景下，我们不仅需要能感知到容器进程退出事件，还需要将容器进程退出码持久化，以防止 containerD 进程重启过程中丢失。在这里，eBPF 是我们的最佳选择。临时文件系统 BPF-FS 可以持久化 &lt;code&gt;关联后&lt;/code&gt; 的 eBPF 程序和 Map 存储。即使注入 eBPF 程序的进程退出了，内核依然可以调用这些持久化后 eBPF 程序来响应事件，比如 &lt;code&gt;sched_process_exit&lt;/code&gt; 事件。&lt;/p&gt;
&lt;p&gt;除此之外，更重要的是 v5.4 内核还提供了 pidfd pollable 能力。pidfd 是和某一个正在运行的进程相关联的文件句柄，这个句柄可用来给关联进程定向发送信号、复制被关联进程的文件句柄以及感知被关联进程的退出事件。&lt;code&gt;感知被关联进程的退出事件&lt;/code&gt; 便是我们提到的 pidfd pollable。它打破了 subreaper 必须是父进程的限制，即使是非父进程也可以感知到类 SIGCHLD 信号。这个信号通知发生在 &lt;code&gt;sched_process_exit&lt;/code&gt; 事件处理之后，换句话说，pidfd 文件句柄可读则意味着我们可以从 eBPF Map 中读取关联进程的退出码了。&lt;/p&gt;
&lt;p&gt;有了 &lt;code&gt;sched_process_exit&lt;/code&gt; 事件处理程序以及 pidfd pollable 这两大神器，containerD 将不再需要 subreaper，容器进程可以放心交给一号进程管理。在过去 shim 是我们的边车，而现在内核是我们的边车，而且车将开的更稳！&lt;/p&gt;
&lt;h2 id=&#34;3-embedshim-进程管理&#34;&gt;3. embedshim 进程管理&lt;/h2&gt;
&lt;p&gt;下图为 embedshim 设计概况，除了借助内核能力外，embedshim 还依赖 OCI runC 容器创建流程设计。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-embedshim-kernel-is-my-sidecar/sec2-overview-p1.svg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;runC 把创建容器分为两阶段: create 和 start。早期我并没有参与过定制 OCI Runtime 标准的讨论，但从个人角度来看，两阶段的设计其实是有利于做进程管理的。首先 runC-create 会 fork 出 runC-init 进程，然后它通过 UDS 和 runC-init 进程进行交互，比如 cgroup 参数写入、数据卷的挂载、capability 权限配置、当然还有一些 Hook 的调用等等。之后 runC-create 进程将退出，而 runC-init 进程通过 exec.fifo 有名管道等待 runC-start 的指令来完成最后的 exec 系统调用（切换成容器进程）。runC-create 和 runC-start 两命令行之间所产生的停端间隔可以让集成方做一些能力拓展，尤其是那些需要在 exec 系统调用前完成的事情。&lt;/p&gt;
&lt;p&gt;在 embedshim 这个场景下，我用它来追踪容器进程的状态变化。如果 runC 创建容器不是两阶段，那么遇到 &lt;code&gt;短命&lt;/code&gt; 的容器进程，比如 flannel CNI initContainer，我们很可能还没开始注册追踪任务，容器进程就退出了，而两阶段创建可确保不出现这样的问题。所以在 runC-create 成功执行后， embedshim 会给该容器分配一个独一无二的 TraceID 来标识容器进程 PID，它将追踪任务注册到 &lt;code&gt;tracking_tasks&lt;/code&gt; eBPF Map 存储里，并使用 pidfd_open 来监控进程退出事件。一旦监控准备工作完毕， containerD 就会开始调用 runC-start 来启动容器进程。只要容器进程一退出，embedshim 注册的 eBPF 追踪程序便会将其退出码持久化到 &lt;code&gt;exited_events&lt;/code&gt; eBPF Map 存储里，并去除追踪任务，其中 &lt;code&gt;exited_events&lt;/code&gt; 使用 TraceID 做容器退出码的索引。因为 PID 有复用的风险，利用 &lt;code&gt;exited_events&lt;/code&gt; 存储可确保退出码信息准确。内核调用追踪程序后，它会通过 pidfd 来通知 embedshim 有容器退出了，而 embedshim 会拿着容器退出码来更新容器状态。&lt;/p&gt;
&lt;p&gt;当然除了容器进程外，我们还需要支持容器的运维进程。runC-exec 运维命令并非两阶段创建模型，它直接采用 execve 模型来创建运维进程。这些运维进程一般都比较短命，尤其是 Pod Probe 类型的探针性命令。为了保证能正确捕捉到 exec 进程的退出事件，embedshim 通过 runcext 进程以 subreaper 的身份调用 runC-exec。runcext 进程调用完毕 runC-exec 后，它会通过 UDS 将 exec 进程状态反馈给 embedshim。即使 exec 进程真的短命，runcext 也能通过 waitpid 系统调用感知它的退出码。针对这种场景，embedshim 可直接将状态更新到 &lt;code&gt;exited_events&lt;/code&gt; 存储里，无需走 &lt;code&gt;sched_process_exit&lt;/code&gt; 事件处理模式。&lt;/p&gt;
&lt;p&gt;runcext 一定程度上模拟了两阶段创建，并不原生化。为了解决这个问题，我在 runC 社区提出了 &lt;a href=&#34;https://github.com/opencontainers/runc/issues/3453&#34;&gt;Feature Request: Support Two Phases to Start Exec Process like Init&lt;/a&gt; 请求，目前已两个 runC Maintainer 同意了该提案，后续我会把这部分优化提交到社区。&lt;/p&gt;
&lt;p&gt;这就是 embedshim 管理容器进程的整体思路。embedshim 监控一个容器进程的退出码仅需要 32B 的内存空间，即使有上千个容器进程也仅需 KB 级别的内存，
它不仅去除了 shim subreaper 缩短了调用链路，还减少了不必要的内存开销，这可能就是内核边车的价值所在。&lt;/p&gt;
&lt;h2 id=&#34;4-简化容器-stdio-重定向可支持-99-场景&#34;&gt;4. 简化容器 stdio 重定向：可支持 99% 场景&lt;/h2&gt;
&lt;p&gt;shim 边车并非直接将有名管道作为容器进程的标准输入输出，而是中间加了一层转发，如下图所示。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-embedshim-kernel-is-my-sidecar/sec4-shim-fifo.svg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;shim 边车将无名管道的一端作为容器的标准输出，另外一端用来将数据同步到有名管道上，由接收方 dockerd 或者 containerD 消费。因为有名管道开启了读写模式，所以即使接收端退出了也不会出现 SIGPIPE 错误，接收端重启后还是可以接管容器输出。同样地，标准输入的处理和标准输出一样，只是数据方向相反。&lt;/p&gt;
&lt;p&gt;单纯从容器标准输出来看，shim 边车的确不应该做这一层转化，直接将有名管道交给容器即可。但交互模式下的标准输入是必要的。*nux 系统提供了管道来将多个命令行串联在一起，比如 &lt;code&gt;echo hello | cat&lt;/code&gt;，echo 传递 hello 之后并发送 EOF，这样 cat 打印 hello 完毕后会认为已经没有输入，它便自动会退出。这种模式在容器场景下也十分常见。但如果我们直接将 &lt;code&gt;读写模式&lt;/code&gt; 的有名管道作为容器进程的标准输入，那么我们将无法给容器进程发送 EOF，它将永远等待用户的输入，所以 shim 边车只能利用无名管道的模式来做 EOF 信号通知。可能是为了方便管理吧，shim 边车也将这种中转模式应用到了标准输出。&lt;/p&gt;
&lt;p&gt;embedshim 同样也采用中转的方式来处理标准输入，但它直接将读写模式的有名管道交给了容器的标准输出，减少标准输出的拷贝。embedshim 插件属于 containerD 进程的一部分，一旦 containerD 重启，那么容器进程的 &lt;code&gt;输入端&lt;/code&gt; 将收到 SIGPIPE 错误。对于这种情况，个人觉得是可以接受的。在交互模式下，用户会感知到容器引擎的停服。而线上环境的大部分场景都是采用 Headless 无交互模式，容器进程的输入端都是 /dev/null，而标准输出的状态由有名管道做持久化，不会因为 containerD 停服而出现 &lt;code&gt;容器输出端&lt;/code&gt; 的 SIGPIPE 错误。&lt;/p&gt;
&lt;p&gt;所以从个人使用经验来看，只要不强求恢复停服前的交互输入，embedshim 基本上能满足大部分人的需求，至少 Kubernetes 场景下的容器都可以满足。&lt;/p&gt;
&lt;h2 id=&#34;5-v010-版本发布&#34;&gt;5. v0.1.0 版本发布&lt;/h2&gt;
&lt;p&gt;最后，我刚发布了 embedshim v0.1.0 版本，它目前经过 CRI-TEST 测试套件的验证，也欢迎大家使用来提提意见。&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>Towards truly portable eBPF</title>
          <link>/post/2022-ebpf-portable-with-btfhub/</link>
          <pubDate>Thu, 10 Mar 2022 00:00:45 +0800</pubDate>
          <guid>https://fuweid.com/post/2022-ebpf-portable-with-btfhub/</guid>
          <description>&lt;p&gt;在上一篇 &lt;a href=&#34;https://fuweid.com/post/2022-ebpf-loader/&#34;&gt;eBPF Loader&lt;/a&gt; 中介绍了 eBPF 加载器的工作原理。Compile-Once Run-Everywhere (CO-RE) 是目前社区的发力方向，但它要求内核版本支持 &lt;code&gt;CONFIG_DEBUG_INFO_BTF=y&lt;/code&gt; 特性。除了 REHL 等商业公司会回合高版本特性到当前支持的商业版本之外，大部分社区免费版本都要求 &lt;strong&gt;&amp;gt;= 5.5&lt;/strong&gt; 版本的内核。为了能在当前主流的 &lt;strong&gt;4.14, 4.15, 4.18, 4.19&lt;/strong&gt; 内核版本上支持 eBPF CO-RE 特性， &lt;a href=&#34;https://www.aquasec.com/&#34;&gt;Aqua Security&lt;/a&gt; 的工程师在 2021 Linux Plumbers Conference - &lt;a href=&#34;https://lpc.events/event/11/contributions/948/&#34;&gt;Towards truly portable eBPF&lt;/a&gt; 议题上展示了它们的想法: BTF-Hub + Embedded BTF。&lt;/p&gt;
&lt;h2 id=&#34;1-bpf-portable&#34;&gt;1. BPF Portable&lt;/h2&gt;
&lt;p&gt;作为用户提供的程序片段，eBPF bytecode 可直接注入到 linux 内核里直接读取和操作内核运行态的内存数据，它的可观测性和对内核模块的可拓展性受到系统开发者的青睐。这是它强大的优势，但同时也是一大痛点：只有获得了目标内核版本的头文件才能保证 eBPF 程序能正确地访问内存数据。&lt;/p&gt;
&lt;p&gt;早期在使用 &lt;a href=&#34;https://github.com/iovisor/bcc&#34;&gt;bcc&lt;/a&gt; 诊断工具时，我们需要 llvm/clang 做 eBPF 实时编译；如果开发 - 测试 - 线上环境没有做到版本的强一致，那么即使逃过了 BPF Verifier 的审判，程序也会因为没有正确地读取内存 (偏移地址不正确) 而出现非预期的行为。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-ebpf-portable-with-btfhub/p1-shall-not-pass.svg&#34; alt=&#34;shall-not-pass&#34;&gt;&lt;/p&gt;
&lt;p&gt;虽然 eBPF 程序的非预期行为不会导致内核崩溃，但研发体验和内核模块开发差别不大：eBPF 开发者还是需要针对不同的内核版本编译出不同的版本，线上内核版本越多，测试上线的成本就越高。下图为 &lt;a href=&#34;https://falco.org/&#34;&gt;Falco&lt;/a&gt; 为不同版本提供的内核模块；如果 eBPF 没有移植能力的话，它的发布模式其实和普通的内核模块差别不大。&lt;strong&gt;一次构建，到处运行&lt;/strong&gt; 的能力直到 &lt;a href=&#34;https://www.kernel.org/doc/html/latest/bpf/btf.html&#34;&gt;BPF Type Format (BTF)&lt;/a&gt; 的出现才有了质的飞跃。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-ebpf-portable-with-btfhub/p2-falco-download-driver-ko-screenshoot.png&#34; alt=&#34;falco-driver-ko&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;11-btf&#34;&gt;1.1 BTF&lt;/h3&gt;
&lt;p&gt;BTF 描述了程序所需要的数据结构信息，包括结构体大小名字，字段类型和偏移量等等。它一般通过 &lt;a href=&#34;https://linux.die.net/man/1/pahole&#34;&gt;pahole&lt;/a&gt; 将 DWARF 调试信息转化得到，其压缩比非常高，一个常用内核的 BTF 仅需要 1-5 MB。正是因为它压缩比高、易携带，社区通过 &lt;code&gt;CONFIG_DEBUG_INFO_BTF=y&lt;/code&gt; 编译选项来决定将其注入到 vmlinux 上，携带该信息的内核将通过 &lt;code&gt;/sys/kernel/btf/vmlinux&lt;/code&gt; 路径作为外部获取的接口。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-ebpf-portable-with-btfhub/p3-dwarf-btf.svg&#34; alt=&#34;dwarf-vs-btf&#34;&gt;&lt;/p&gt;
&lt;p&gt;BTF 同 DWARF 一样，我们可以根据 BTF 信息来生成了头文件。对于开发者而言，BTF 改善体验的第一件事就是仅需要一个头文件就可以拿到内核所使用的所有结构体描述。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;$ bpftool btf dump file /sys/kernel/btf/vmlinux format c &amp;gt; vmlinux.h
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;但仅靠 BTF 还是无法保证 eBPF 程序能在目标内核上正确地获取内存地址，只能通过修正、校对现有指令才行。&lt;/p&gt;
&lt;h3 id=&#34;12-preserve_access_index&#34;&gt;1.2 preserve_access_index&lt;/h3&gt;
&lt;p&gt;以 Tracing 为例，eBPF 程序会经常使用进程元数据 &lt;code&gt;task_struct&lt;/code&gt;，内核提供了 &lt;code&gt;bpf_get_current_task&lt;/code&gt; 接口来获取当前上下文的进程信息，这个接口的返回值将直接指向内核运行态地址。从安全角度考虑，eBPF 访问内核空间的地址必须要走 &lt;code&gt;bpf_probe_read_kernel&lt;/code&gt; 调用来完成。比如，我们想获取当前上下文的父进程 &lt;code&gt;pid&lt;/code&gt;, 那么我们是无法直接通过 &lt;code&gt;task-&amp;gt;real_parent-&amp;gt;pid&lt;/code&gt; 来读取；当前 &lt;code&gt;libbpf&lt;/code&gt; 库为我们提供了 &lt;code&gt;BPF_PROBE_READ&lt;/code&gt; 宏来读取父进程的 &lt;code&gt;pid&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-ebpf-portable-with-btfhub/p4-probe-read-bytecode.svg&#34; alt=&#34;probe-read-bytecode&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;BPF_PROBE_READ&lt;/code&gt; 宏展开后的编译结果显示两次 &lt;code&gt;task-&amp;gt;real_parent&lt;/code&gt;, &lt;code&gt;real_parent-&amp;gt;pid&lt;/code&gt; 读取，每次都是基于 &lt;code&gt;vmlinux_508.h&lt;/code&gt; 头文件 (内核 v5.8) 的偏移量来读取。这和我本地的目标内核 (v5.13) 偏移量存在差异；即使程序不报错，它也不是按照预期来进行。为了解决这个问题，社区在编译器上做了优化。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;libbpf&lt;/code&gt; 提供了新的宏 &lt;code&gt;BPF_CORE_READ&lt;/code&gt;，它使用 &lt;code&gt;__builtin_preserve_access_index&lt;/code&gt; 包住被读取的内核空间地址，比如 &lt;code&gt;task-&amp;gt;real_parent&lt;/code&gt;, &lt;code&gt;real_parent-&amp;gt;pid&lt;/code&gt;，那么在编译阶段会将访问路径 &lt;code&gt;0:57&lt;/code&gt;，&lt;code&gt;0:54&lt;/code&gt; 作为 relocation 的符号保存在 &lt;code&gt;.BTF.ext&lt;/code&gt; section 里，其中 &lt;code&gt;57&lt;/code&gt; 和 &lt;code&gt;54&lt;/code&gt; 分别是 &lt;code&gt;real_parent&lt;/code&gt; 和 &lt;code&gt;pid&lt;/code&gt; 在 v5.8 内核 &lt;code&gt;task_struct&lt;/code&gt; 结构体里的第 57 和 54 个字段。加载器会根据访问路径来比较源和当前内核对应数据结构的差异。当找到匹配对象后，那么将会以当前内核的偏移地址来修改原先的指令，保证程序可以正确运行；否则将会加载失败。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-ebpf-portable-with-btfhub/p5-relo-access-str.svg&#34; alt=&#34;relo-access-bytecode&#34;&gt;&lt;/p&gt;
&lt;p&gt;早期 preserve_access_index 是配合 &lt;code&gt;BPF_CORE_READ&lt;/code&gt; 来使用，现在直接将这个拓展放到每一个内核结构上，保证编译期间能注入 relocation 信息。当然这里仅仅列列出了 &lt;code&gt;field offset relocation&lt;/code&gt;，还有 ENUM, BITFIELD 等模式，这些模式仅在结构匹配算法上存在差异，整体还都是先匹配再修改指令，在此就不再赘述。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;$ cat vmlinux.h
&lt;span class=&#34;c1&#34;&gt;#ifndef __VMLINUX_H__&lt;/span&gt;
&lt;span class=&#34;c1&#34;&gt;#define __VMLINUX_H__&lt;/span&gt;

&lt;span class=&#34;c1&#34;&gt;#ifndef BPF_NO_PRESERVE_ACCESS_INDEX&lt;/span&gt;
&lt;span class=&#34;c1&#34;&gt;#pragma clang attribute push (__attribute__((preserve_access_index)), apply_to = record)&lt;/span&gt;
&lt;span class=&#34;c1&#34;&gt;#endif&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;eBPF 加载器在做指令修改时，它需要当前内核的 BTF 信息。一般来说，这个特性仅在 &amp;gt;= v5.5 内核才支持，还有很多用户在使用 4.x 的内核。因可观测性的需求强行升级内核并不现实，我们需要寻求其他方式来获得 BTF 信息。&lt;/p&gt;
&lt;h2 id=&#34;2-btf-hub-and-embedded-btf&#34;&gt;2. BTF-Hub and embedded BTF&lt;/h2&gt;
&lt;p&gt;内核在编译时，它是通过 &lt;code&gt;pahole&lt;/code&gt; 将 DWARD 信息转化成 BTF，并在 link-vmlinux 阶段将数据放到 BTF section 中。所以，即使是低内核版本，只要能拿到对应内核的编译调试信息，我们就可以独立转化并得到 BTF 信息，它和 &lt;code&gt;/sys/kernel/btf/vmlinux&lt;/code&gt; 读出来的结果一致。eBPF 加载器是在用户态做指令改写，只要 eBPF 加载器能识别到指定的 BTF 信息即可。&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://lpc.events/event/11/contributions/948/&#34;&gt;Towards truly portable eBPF&lt;/a&gt; 议题提出了 &lt;a href=&#34;https://github.com/aquasecurity/btfhub&#34;&gt;BTF-Hub&lt;/a&gt;：Aqua Security 工程师将 ubuntu/centos/federo/debian 对外开放的 DWARF 信息转化成对应的 BTF 信息，并配合加载器使用就可以完成 CO-RE 能力了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-ebpf-portable-with-btfhub/p6-customize-btf-path.svg&#34; alt=&#34;customize-btf-path&#34;&gt;&lt;/p&gt;
&lt;p&gt;这个方案解决了低内核版本不支持导出 BTF 的问题。虽然每一个内核版本 BTF 信息不大，但也抗不住小版本的迭代，就目前 BTF-Hub 提供 ubuntu 发行版的可用 BTF 信息就有 200 MB。在演讲者看来它和 falco 驱动的多内核版本构建差别不大，会场主持人也开玩笑说：&amp;ldquo;如果 github 挂了，我们就获取不到 BTF 数据了&amp;rdquo;。演讲者提出了比较有意思的想法: Embedded BTF。&lt;/p&gt;
&lt;p&gt;Embeded BTF 核心设计就是去除无用的 BTF 信息。举个例子， bcc 提供了 libbpf-tools 38 个 eBPF 程序，但这些程序使用的内核数据结构只是内核 BTF 信息的子集，只要能获取出这个子集就能降低携带 BTF 信息成本，这个提取的方案叫做 &lt;a href=&#34;https://lore.kernel.org/bpf/20220215225856.671072-1-mauricio@kinvolk.io/&#34;&gt;BTFGen&lt;/a&gt;，已经被 Linux bpf-next 所接受了。下面可以看下我本地使用的效果：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2022-ebpf-portable-with-btfhub/p7-min-core-gen.svg&#34; alt=&#34;min-core-gen&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;4.9MB&lt;/code&gt; 里只有 &lt;code&gt;3.3KB&lt;/code&gt; 数据有用，对于一类 eBPF 程序而言，即使把尽可能多内核版本的 BTF 随身携带也不会带来存储空间压力；当线上内核版本较为统一时，开发者甚至还可以将 BTF 信息放入到容器镜像或者应用程序的二进制里。&lt;/p&gt;
&lt;p&gt;对我而言，这个方案的确是让低版本的内核享受到 CO-RE 的好处，&lt;a href=&#34;https://github.com/iovisor/bcc/pull/3889&#34;&gt;甚至会改变 bcc 工具包的发布方式&lt;/a&gt;。&lt;/p&gt;
&lt;h2 id=&#34;3-summary&#34;&gt;3. Summary&lt;/h2&gt;
&lt;p&gt;除了 BTF-Hub 和 Embedded BTF 之外，演讲者还介绍了很多他们遇到的 CO-RE 挑战，实战编程中还是有一定的参考意义，值得一看。&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>eBPF Loader</title>
          <link>/post/2022-ebpf-loader/</link>
          <pubDate>Sun, 27 Feb 2022 23:00:53 +0800</pubDate>
          <guid>https://fuweid.com/post/2022-ebpf-loader/</guid>
          <description>&lt;h2 id=&#34;0-what-is-ebpf&#34;&gt;0. What is eBPF?&lt;/h2&gt;
&lt;p&gt;Extended Berkeley Packet Filter (eBPF) 是由 Linux 提供的内核技术，它是以安全沙盒 (Virtual Machine) 的形式运行用户定义的 ByteCode 来观测内核运行状态以及拓展内核的能力，开发者无需定制内核模块就可以高效地完成对现有模块的拓展。eBPF 安全沙盒是嵌入到 Linux 内核运行态的关键路径上，通过事件订阅的形式来触发 eBPF 程序，其运用场景有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/cilium/cilium&#34;&gt;cilium&lt;/a&gt; 在 L3/L4 提供高效的网络转发能力&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/iovisor/bcc&#34;&gt;bcc&lt;/a&gt; 提供常用的观测组件来定位业务遇到的性能问题&lt;/li&gt;
&lt;li&gt;Google 内核调度拓展 &lt;a href=&#34;https://github.com/google/ghost-userspace&#34;&gt;ghOSt&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我过去主要使用 eBPF 在观测和排查一些节点的性能问题。由于底层基础设施能力以及业务运行模型存在差异，这将导致节点组件出现不在预期内的行为，而在本地又难以复现，大大增加了沟通和排查成本。而 eBPF 可以捕捉到程序在内核里的状态，甚至是短命的程序调用 (比如容器领域的 runC 命令) 都可以捕捉, 它可以最大程度地呈现程序的运行状态来提升问题的排查效率。&lt;/p&gt;
&lt;p&gt;比如前段时间遇到的 containerD CRI 组件创建容器超时的问题，我通过 &lt;a href=&#34;https://github.com/iovisor/bcc&#34;&gt;bcc&lt;/a&gt; stackcount 捕抓到 &lt;a href=&#34;https://github.com/containerd/containerd/pull/6478&#34;&gt;根因&lt;/a&gt;: umount 可写的文件系统时会调用底层文件系统的刷盘动作，它用来保证数据能及时落盘；但这同时也给磁盘带来压力，IOPS 弱的数据盘将会拖慢 umount 调用。对于一个不熟悉内核代码的开发者来说，eBPF 观测类工具暴露出的关键函数路径就和日志的错误信息一样，全局搜索相应的内核代码段，然后顺藤摸瓜，总会找到些蛛丝马迹。&lt;/p&gt;
&lt;p&gt;eBPF 目前发展的比较快，其中一个重要的支线是 Compile-Once Run-Everwhere (CO-RE) ，这有点像 &lt;a href=&#34;https://github.com/moby/moby&#34;&gt;docker&lt;/a&gt; 镜像分发的 Build-Once Run-Anywhere, 下面将主要围绕兼容性去介绍如何加载 eBPF 程序。&lt;/p&gt;
&lt;h3 id=&#34;1-type-metadata&#34;&gt;1. Type Metadata&lt;/h3&gt;
&lt;p&gt;eBPF 允许读取和修改内核运行态的数据，常见数据结构有线程的 &lt;code&gt;task_struct&lt;/code&gt; 和网络子系统的 &lt;code&gt;__sk_buff&lt;/code&gt;。这些常用的结构体在不同内核版本之间存在差异， 比如某一个字段位于 &lt;code&gt;task_struct&lt;/code&gt; 结构体的第 16 字节处，然而升级到某一个内核版本后，这个字段被移到第 24 字节处，那么原先编译好的 eBPF 将无法正常工作。早期的 &lt;a href=&#34;https://github.com/iovisor/bcc&#34;&gt;bcc&lt;/a&gt; 组件在使用过程中都是在目标节点上利用现有的内核相关头文件来编译。但即使如此，bcc 也没法解决字段名被更换的问题，字段名变化容易出现编译不通过。因此早期大部分情况下，开发者还是会选择根据不同内核版本出不同的二进制，这给测试验证带来了极大的成本。&lt;/p&gt;
&lt;p&gt;为了解决类型匹配问题，eBPF 需要额外的类型系统的描述数据。eBPF 程序所使用的数据类型和函数接口都通过常用的调试信息 &lt;a href=&#34;https://dwarfstd.org/doc/Debugging%20using%20DWARF-2012.pdf&#34;&gt;DWARF&lt;/a&gt; 描述, 如下图所示 &lt;a href=&#34;https://github.com/libbpf/libbpf-bootstrap/blob/master/examples/c/bootstrap.bpf.c&#34;&gt;bootstrap.bpf.c&lt;/a&gt; 编译完毕后的 &lt;code&gt;task_struct&lt;/code&gt; 部分类型信息， 其中 &lt;code&gt;thread_info&lt;/code&gt; 和 &lt;code&gt;state&lt;/code&gt; 是 &lt;code&gt;task_struct&lt;/code&gt;的字段。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;// use llvm-dwarfdump
...

0x00005f27:   DW_TAG_structure_type
                DW_AT_name      (&amp;#34;task_struct&amp;#34;)
                DW_AT_byte_size (0x1ac0)
                DW_AT_decl_file (&amp;#34;xxx&amp;#34;)
                DW_AT_decl_line (883)

0x00005f2f:     DW_TAG_member
                  DW_AT_name    (&amp;#34;thread_info&amp;#34;)
                  DW_AT_type    (0x00006775 &amp;#34;thread_info&amp;#34;)
                  DW_AT_decl_file       (&amp;#34;xxx&amp;#34;)
                  DW_AT_decl_line       (884)
                  DW_AT_data_member_location    (0x00)

0x00005f3a:     DW_TAG_member
                  DW_AT_name    (&amp;#34;state&amp;#34;)
                  DW_AT_type    (0x00006793 &amp;#34;volatile long&amp;#34;)
                  DW_AT_decl_file       (&amp;#34;xxx&amp;#34;)
                  DW_AT_decl_line       (885)
                  DW_AT_data_member_location    (0x10)

...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;DWARF 对类型描述十分详细，包含类型字段的大小、字段的偏移量等等。当 eBPF 加载时，只需要在当前 Linux 内核的 DWARF 调试信息找到最匹配 eBPF 程序中的类型即可，即使字段的偏移量发生改变也没关系，只需 Relocation 成对应的偏移量即可。但 DWARF 调试信息为纯文本格式，内核的调试信息大概有 100+ MB，让内核启动带上这些信息成本太高了，这里需要可压缩的类型格式。&lt;/p&gt;
&lt;p&gt;内核社区提出了 &lt;a href=&#34;https://www.kernel.org/doc/html/v5.13/bpf/btf.html&#34;&gt;BPF Type Format (BTF)&lt;/a&gt;, 它可以将 100+ MB DWARF 信息压缩到 1.5 MB，eBPF 以及 Linux 内核都方便携带，具体的压缩算法可以阅读 &lt;a href=&#34;https://nakryiko.com/posts/btf-dedup/&#34;&gt;BTF deduplication and Linux kernel BTF&lt;/a&gt;, 在此就仅仅展示 BTF dump 信息。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;// use pahole -JV
...

[624] STRUCT task_struct size=6848
        thread_info type_id=625 bits_offset=0
        state type_id=626 bits_offset=128
        stack type_id=29 bits_offset=192
        usage type_id=308 bits_offset=256
        flags type_id=37 bits_offset=288
        ptrace type_id=37 bits_offset=320
        on_cpu type_id=10 bits_offset=352
        wake_entry type_id=628 bits_offset=384
        cpu type_id=37 bits_offset=512
...

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当每个字段的偏移量以及类型信息可通过极低的成本携带时，eBPF 加载器可以使用类型匹配策略来将使用体验提升到新的高度。下面将介绍加载器的核心工作 - 重定向。&lt;/p&gt;
&lt;h3 id=&#34;2-relocation&#34;&gt;2. Relocation&lt;/h3&gt;
&lt;p&gt;eBPF 一般来说是由 clang/llvm 编译 C 文件得到的 ELF 二进制文件， 其中它采用十个通用的寄存器、只读的 Frame Pointer 寄存器以及使用长度为 64 bits 的 &lt;a href=&#34;https://www.kernel.org/doc/html/latest/bpf/instruction-set.html&#34;&gt;指令集&lt;/a&gt;。加载器需要解决重定向后才能进行 BPF Verifier，而重定向内容主要涉及到三部分 Map，CO-RE 以及函数调用。&lt;/p&gt;
&lt;h4 id=&#34;21-map&#34;&gt;2.1 Map&lt;/h4&gt;
&lt;p&gt;eBPF 程序和程序之间以及同用户态之间的交互都是通过 Map 来实现，而 Map 增删改查是通过文件句柄的形式来操作，而这依赖 &lt;a href=&#34;https://man7.org/linux/man-pages/man2/bpf.2.html&#34;&gt;BPF_MAP_CREATE&lt;/a&gt; bpf 系统调用。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// https://man7.org/linux/man-pages/man2/bpf.2.html
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;
&lt;span class=&#34;n&#34;&gt;bpf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;BPF_MAP_CREATE&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bpf_attr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;sizeof&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bpf_attr&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;));&lt;/span&gt;

&lt;span class=&#34;k&#34;&gt;union&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;bpf_attr&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
  &lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt; &lt;span class=&#34;cm&#34;&gt;/* anonymous struct used by BPF_MAP_CREATE command */&lt;/span&gt;
      &lt;span class=&#34;n&#34;&gt;__u32&lt;/span&gt;   &lt;span class=&#34;n&#34;&gt;map_type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;       &lt;span class=&#34;cm&#34;&gt;/* one of enum bpf_map_type */&lt;/span&gt;
      &lt;span class=&#34;n&#34;&gt;__u32&lt;/span&gt;   &lt;span class=&#34;n&#34;&gt;key_size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;       &lt;span class=&#34;cm&#34;&gt;/* size of key in bytes */&lt;/span&gt;
      &lt;span class=&#34;n&#34;&gt;__u32&lt;/span&gt;   &lt;span class=&#34;n&#34;&gt;value_size&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;     &lt;span class=&#34;cm&#34;&gt;/* size of value in bytes */&lt;/span&gt;
      &lt;span class=&#34;n&#34;&gt;__u32&lt;/span&gt;   &lt;span class=&#34;n&#34;&gt;max_entries&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;    &lt;span class=&#34;cm&#34;&gt;/* max number of entries in a map */&lt;/span&gt;
      &lt;span class=&#34;n&#34;&gt;__u32&lt;/span&gt;   &lt;span class=&#34;n&#34;&gt;map_flags&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;      &lt;span class=&#34;cm&#34;&gt;/* prealloc or not */&lt;/span&gt;
  &lt;span class=&#34;p&#34;&gt;};&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;bpf_attr&lt;/code&gt; 定义可以从 ELF 二进制的 section &lt;code&gt;.maps&lt;/code&gt; 或者 &lt;code&gt;maps&lt;/code&gt; 中获取， 如下面的代码片段所示。&lt;code&gt;bpf_map_def&lt;/code&gt; 定义数据模式已经被弃用了，如下图所示，加载器需要严格按照固定的偏移量来读取 &lt;code&gt;bpf_attr&lt;/code&gt;，相比 BTF 类型系统而言，编码和使用体验都差很多。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// Use BTF
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
	&lt;span class=&#34;n&#34;&gt;__uint&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;BPF_MAP_TYPE_HASH&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
	&lt;span class=&#34;n&#34;&gt;__uint&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;max_entries&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;8192&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
	&lt;span class=&#34;n&#34;&gt;__type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;key&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;pid_t&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
	&lt;span class=&#34;n&#34;&gt;__type&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;value&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;u64&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;exec_start_btf&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SEC&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;.maps&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;

&lt;span class=&#34;c1&#34;&gt;// Use symbol but it has been deprecated
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;bpf_map_def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;SEC&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;maps&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;exec_start_symbol&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
	&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;type&lt;/span&gt;        &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;BPF_MAP_TYPE_HASH&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
	&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;key_size&lt;/span&gt;    &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;sizeof&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;pid_t&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;
	&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;value_size&lt;/span&gt;  &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;sizeof&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;u64&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;),&lt;/span&gt;
	&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;max_entries&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;8192&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;};&lt;/span&gt;

&lt;span class=&#34;c1&#34;&gt;// https://github.com/fuweid/demos/tree/master/ebpf
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;➜&lt;/span&gt;  &lt;span class=&#34;n&#34;&gt;llvm&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;readelf&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;x&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;maps&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;output&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;example&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;map&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;relo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;bpf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;o&lt;/span&gt; 

&lt;span class=&#34;n&#34;&gt;Symbol&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;table&lt;/span&gt; &lt;span class=&#34;err&#34;&gt;&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;symtab&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;&amp;#39;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;contains&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;6&lt;/span&gt; &lt;span class=&#34;nl&#34;&gt;entries&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
   &lt;span class=&#34;nl&#34;&gt;Num&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;    &lt;span class=&#34;n&#34;&gt;Value&lt;/span&gt;          &lt;span class=&#34;n&#34;&gt;Size&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Type&lt;/span&gt;    &lt;span class=&#34;n&#34;&gt;Bind&lt;/span&gt;   &lt;span class=&#34;n&#34;&gt;Vis&lt;/span&gt;       &lt;span class=&#34;n&#34;&gt;Ndx&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Name&lt;/span&gt;
     &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mo&#34;&gt;0000000000000000&lt;/span&gt;     &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;NOTYPE&lt;/span&gt;  &lt;span class=&#34;n&#34;&gt;LOCAL&lt;/span&gt;  &lt;span class=&#34;n&#34;&gt;DEFAULT&lt;/span&gt;   &lt;span class=&#34;n&#34;&gt;UND&lt;/span&gt; 
     &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mo&#34;&gt;0000000000000000&lt;/span&gt;     &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;SECTION&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;LOCAL&lt;/span&gt;  &lt;span class=&#34;n&#34;&gt;DEFAULT&lt;/span&gt;     &lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;tp&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sched&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sched_process_exec&lt;/span&gt;
     &lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mo&#34;&gt;0000000000000000&lt;/span&gt;   &lt;span class=&#34;mi&#34;&gt;184&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;FUNC&lt;/span&gt;    &lt;span class=&#34;n&#34;&gt;GLOBAL&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DEFAULT&lt;/span&gt;     &lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;handle_exec&lt;/span&gt;
     &lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mo&#34;&gt;0000000000000000&lt;/span&gt;    &lt;span class=&#34;mi&#34;&gt;32&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;OBJECT&lt;/span&gt;  &lt;span class=&#34;n&#34;&gt;GLOBAL&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DEFAULT&lt;/span&gt;     &lt;span class=&#34;mi&#34;&gt;5&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;exec_start_btf&lt;/span&gt;
     &lt;span class=&#34;mi&#34;&gt;4&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mo&#34;&gt;0000000000000000&lt;/span&gt;    &lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;OBJECT&lt;/span&gt;  &lt;span class=&#34;n&#34;&gt;GLOBAL&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DEFAULT&lt;/span&gt;     &lt;span class=&#34;mi&#34;&gt;4&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;exec_start_symbol&lt;/span&gt;
     &lt;span class=&#34;mi&#34;&gt;5&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mo&#34;&gt;0000000000000000&lt;/span&gt;    &lt;span class=&#34;mi&#34;&gt;13&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;OBJECT&lt;/span&gt;  &lt;span class=&#34;n&#34;&gt;GLOBAL&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;DEFAULT&lt;/span&gt;     &lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;LICENSE&lt;/span&gt;
&lt;span class=&#34;n&#34;&gt;Hex&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;dump&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;of&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;section&lt;/span&gt; &lt;span class=&#34;err&#34;&gt;&amp;#39;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;maps&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;&amp;#39;&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;:&lt;/span&gt;
          &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;     &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;key_size&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;4&lt;/span&gt;   &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;value_size&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;8&lt;/span&gt;   &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;max_entries&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;8192&lt;/span&gt;
&lt;span class=&#34;mh&#34;&gt;0x00000000&lt;/span&gt; &lt;span class=&#34;mo&#34;&gt;01000000&lt;/span&gt;   &lt;span class=&#34;mo&#34;&gt;04000000&lt;/span&gt;      &lt;span class=&#34;mi&#34;&gt;08000000&lt;/span&gt;        &lt;span class=&#34;mo&#34;&gt;00200000&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;.............&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;..&lt;/span&gt;
          &lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;flags&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;
&lt;span class=&#34;mh&#34;&gt;0x00000010&lt;/span&gt; &lt;span class=&#34;mo&#34;&gt;00000000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当加载器读取 eBPF ELF 时， 代码段 section, 比如 &lt;code&gt;tp/sched/sched_process_exec&lt;/code&gt;, 其对应的重定向符号的偏移量会在 &lt;code&gt;.reltp/sched/sched_process_exec&lt;/code&gt; 描述，如下面的结果所示，其中 &lt;code&gt;18 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00&lt;/code&gt; DWORD 指令将是我们要替换的指令。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;➜  llvm-objdump -dr ./.output/example-map-relo-btf.bpf.o

./.output/example-map-relo-btf.bpf.o:	file format elf64-bpf


Disassembly of section tp/sched/sched_process_exec:

&lt;span class=&#34;m&#34;&gt;0000000000000000&lt;/span&gt; &amp;lt;handle_exec&amp;gt;:
       0:	&lt;span class=&#34;m&#34;&gt;85&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 0e &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	call &lt;span class=&#34;m&#34;&gt;14&lt;/span&gt;
       1:	&lt;span class=&#34;m&#34;&gt;77&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;20&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	r0 &amp;gt;&amp;gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;32&lt;/span&gt;
       2:	&lt;span class=&#34;m&#34;&gt;63&lt;/span&gt; 0a &lt;span class=&#34;nb&#34;&gt;fc&lt;/span&gt; ff &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	*&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;u32 *&lt;span class=&#34;o&#34;&gt;)(&lt;/span&gt;r10 - 4&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; r0
       3:	&lt;span class=&#34;m&#34;&gt;85&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;05&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	call &lt;span class=&#34;m&#34;&gt;5&lt;/span&gt;
       4:	7b 0a f0 ff &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	*&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;u64 *&lt;span class=&#34;o&#34;&gt;)(&lt;/span&gt;r10 - 16&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; r0
       5:	bf a2 &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nv&#34;&gt;r2&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; r10
       6:	&lt;span class=&#34;m&#34;&gt;07&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;02&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;fc&lt;/span&gt; ff ff ff	&lt;span class=&#34;nv&#34;&gt;r2&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; -4
       7:	bf a3 &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nv&#34;&gt;r3&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; r10
       8:	&lt;span class=&#34;m&#34;&gt;07&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;03&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; f0 ff ff ff	&lt;span class=&#34;nv&#34;&gt;r3&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; -16
       9:	&lt;span class=&#34;m&#34;&gt;18&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;01&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nv&#34;&gt;r1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; ll
		0000000000000048:  R_BPF_64_64	exec_start_btf
      11:	b7 &lt;span class=&#34;m&#34;&gt;04&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nv&#34;&gt;r4&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;0&lt;/span&gt;
      12:	&lt;span class=&#34;m&#34;&gt;85&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;02&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	call &lt;span class=&#34;m&#34;&gt;2&lt;/span&gt;
      13:	&lt;span class=&#34;m&#34;&gt;95&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nb&#34;&gt;exit&lt;/span&gt;

➜  llvm-readelf -r ./.output/example-map-relo-btf.bpf.o

Relocation section &lt;span class=&#34;s1&#34;&gt;&amp;#39;.reltp/sched/sched_process_exec&amp;#39;&lt;/span&gt; at offset 0x6d8 contains &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt; entries:
    Offset             Info             Type               Symbol&lt;span class=&#34;s1&#34;&gt;&amp;#39;s Value  Symbol&amp;#39;&lt;/span&gt;s Name
&lt;span class=&#34;m&#34;&gt;0000000000000048&lt;/span&gt;  &lt;span class=&#34;m&#34;&gt;0000000300000001&lt;/span&gt; R_BPF_64_64            &lt;span class=&#34;m&#34;&gt;0000000000000000&lt;/span&gt; exec_start_btf
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当前阅读最新的内核文档 (v5.17.0-rc5) 没有说明如何替换 eBPF Map 符号指令，反倒是 &lt;a href=&#34;https://github.com/libbpf/libbpf&#34;&gt;libbpf&lt;/a&gt; 加载器中有说明：当指令类型为 LD_IMM64 且原寄存器编号不为 0 时，那么该指令可以被重写成 MAP 相关的操作，重写规则如下面的代码所示。回顾上头要被替换指令，其开头 &lt;code&gt;18&lt;/code&gt; 为 LD_IMM64 且 &lt;code&gt;01&lt;/code&gt; 是 &lt;code&gt;r1&lt;/code&gt; 寄存器， 替换的 Map 符号符合该模式；因此通过 BPF 系统调用申请 Map 文件句柄之后，加载器可以把代码段中第 52 个 Byte 替换成对应的文件句柄即可。&lt;/p&gt;
&lt;p&gt;需要说明的是，替换的并不是 &lt;code&gt;.reltp/sched/sched_process_exec&lt;/code&gt; 中提到的第 48 Byte。BPF 并不采用 &lt;a href=&#34;https://llvm.org/doxygen/structllvm_1_1ELF_1_1Elf64__Rela.html&#34;&gt;Elf64_Rela&lt;/a&gt; 来携带 Addend， 而是&lt;a href=&#34;https://www.kernel.org/doc/html/latest/bpf/llvm_reloc.html#different-relocation-types&#34;&gt;针对不同的重定向类型作了特殊的约定&lt;/a&gt;， 比如 &lt;code&gt;R_BPF_64_64&lt;/code&gt; 类型的替换地址为 &lt;code&gt;重定向声明的偏移量 + 4 = 48 + 4 = 52&lt;/code&gt;。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// https://github.com/libbpf/libbpf/blob/v0.7.0/include/uapi/linux/bpf.h#L1121
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;
&lt;span class=&#34;cm&#34;&gt;/* When BPF ldimm64&amp;#39;s insn[0].src_reg != 0 then this can have
&lt;/span&gt;&lt;span class=&#34;cm&#34;&gt; * the following extensions:
&lt;/span&gt;&lt;span class=&#34;cm&#34;&gt; *
&lt;/span&gt;&lt;span class=&#34;cm&#34;&gt; * insn[0].src_reg:  BPF_PSEUDO_MAP_[FD|IDX]
&lt;/span&gt;&lt;span class=&#34;cm&#34;&gt; * insn[0].imm:      map fd or fd_idx
&lt;/span&gt;&lt;span class=&#34;cm&#34;&gt; * insn[1].imm:      0
&lt;/span&gt;&lt;span class=&#34;cm&#34;&gt; * insn[0].off:      0
&lt;/span&gt;&lt;span class=&#34;cm&#34;&gt; * insn[1].off:      0
&lt;/span&gt;&lt;span class=&#34;cm&#34;&gt; * ldimm64 rewrite:  address of map
&lt;/span&gt;&lt;span class=&#34;cm&#34;&gt; * verifier type:    CONST_PTR_TO_MAP
&lt;/span&gt;&lt;span class=&#34;cm&#34;&gt; */&lt;/span&gt;
&lt;span class=&#34;cp&#34;&gt;#define BPF_PSEUDO_MAP_FD	1
&lt;/span&gt;&lt;span class=&#34;cp&#34;&gt;#define BPF_PSEUDO_MAP_IDX	5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;为了方便用户态修改程序中的只读常量，clang/llvm 会将 &lt;code&gt;const volatile&lt;/code&gt; 声明的全局变量合并成一个结构体，并将为其声明成只有 &lt;strong&gt;[一个元素的 Map]&lt;/strong&gt;，数据按照顺序存储在第一个 Value 里。因为不同变量在结构体的偏移量不同，那么指令改写逻辑有点差异，如下面的代码所示。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// https://github.com/libbpf/libbpf/blob/v0.7.0/include/uapi/linux/bpf.h#L1135
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;
&lt;span class=&#34;cm&#34;&gt;/* insn[0].src_reg:  BPF_PSEUDO_MAP_[IDX_]VALUE
&lt;/span&gt;&lt;span class=&#34;cm&#34;&gt; * insn[0].imm:      map fd or fd_idx
&lt;/span&gt;&lt;span class=&#34;cm&#34;&gt; * insn[1].imm:      offset into value
&lt;/span&gt;&lt;span class=&#34;cm&#34;&gt; * insn[0].off:      0
&lt;/span&gt;&lt;span class=&#34;cm&#34;&gt; * insn[1].off:      0
&lt;/span&gt;&lt;span class=&#34;cm&#34;&gt; * ldimm64 rewrite:  address of map[0]+offset
&lt;/span&gt;&lt;span class=&#34;cm&#34;&gt; * verifier type:    PTR_TO_MAP_VALUE
&lt;/span&gt;&lt;span class=&#34;cm&#34;&gt; */&lt;/span&gt;
&lt;span class=&#34;cp&#34;&gt;#define BPF_PSEUDO_MAP_VALUE		2
&lt;/span&gt;&lt;span class=&#34;cp&#34;&gt;#define BPF_PSEUDO_MAP_IDX_VALUE	6
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;不过使用的时候一定要注意用户态的类似是不是和 eBPF 程序中的一致，感兴趣的可以查看 &lt;a href=&#34;https://github.com/iovisor/bcc/pull/3777&#34;&gt;iovisor/bcc#3777&lt;/a&gt;。&lt;/p&gt;
&lt;h4 id=&#34;22-field-offset-rewrite&#34;&gt;2.2 Field Offset Rewrite&lt;/h4&gt;
&lt;p&gt;前面提到了 Linux 内核社区设计可压缩的 BTF 格式，我们通过工具转化 DWARF 数据成 BTF，那么内核里所有的数据结构都可以通过 BTF 反推成 C 头文件。当我们有了这个 All-in-One 头文件，那我们就不再需要在安装 kernel-headers 了！没人喜欢安装一堆依赖，不是吗？！&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;➜  bpftool btf dump file /sys/kernel/btf/vmlinux format c &amp;gt; vmlinux.h
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;我们来看一个具体的例子吧， &lt;a href=&#34;https://github.com/fuweid/demos/blob/master/ebpf/example-field-offset-rewrite.bpf.c&#34;&gt;example-field-offset-rewrite&lt;/a&gt; 读取当前 exec 进程 &lt;code&gt;task_struct&lt;/code&gt; 结构体，并返回它的 &lt;code&gt;real_parent&lt;/code&gt; 的 pid。如下图所示，我们可以看到没有加载前的访问指令是符合 vmlinux.c 描述的偏移量的：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;real_parent&lt;/code&gt; 在 &lt;code&gt;task&lt;/code&gt; 变量的 &lt;code&gt;9472 bits = 1184 bytes&lt;/code&gt; 的位置；&lt;/li&gt;
&lt;li&gt;同时 &lt;code&gt;real_parent&lt;/code&gt; 本身也是一个 &lt;code&gt;task_struct&lt;/code&gt; 类型的变量，它的 &lt;code&gt;pid&lt;/code&gt; 字段将在 &lt;code&gt;9344 bits = 1168 bytes&lt;/code&gt; 位置读取。&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;➜  llvm-objdump -dr ./.output/example-field-offset-rewrite.bpf.o

./.output/example-field-offset-rewrite.bpf.o:	file format elf64-bpf


Disassembly of section tp/sched/sched_process_exec:

&lt;span class=&#34;m&#34;&gt;0000000000000000&lt;/span&gt; &amp;lt;handle_exec&amp;gt;:
       0:	&lt;span class=&#34;m&#34;&gt;85&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;23&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	call &lt;span class=&#34;m&#34;&gt;35&lt;/span&gt;
       // 使用 Ubuntu v5.8 内核 BTF dump 出来的 vmlinux.c 
       // &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;596&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; STRUCT task_struct &lt;span class=&#34;nv&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;6848&lt;/span&gt;
       //   ...
       //   real_parent &lt;span class=&#34;nv&#34;&gt;type_id&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;594&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;bitfield_size&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;bits_offset&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;9472&lt;/span&gt;
       1:	b7 &lt;span class=&#34;m&#34;&gt;01&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; a0 &lt;span class=&#34;m&#34;&gt;04&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nv&#34;&gt;r1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;1184&lt;/span&gt;  &amp;lt;&lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;1184&lt;/span&gt; Bytes
       2:	0f &lt;span class=&#34;m&#34;&gt;10&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nv&#34;&gt;r0&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; r1
       3:	bf a1 &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nv&#34;&gt;r1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; r10
       4:	&lt;span class=&#34;m&#34;&gt;07&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;01&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; f0 ff ff ff	&lt;span class=&#34;nv&#34;&gt;r1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; -16
       5:	b7 &lt;span class=&#34;m&#34;&gt;02&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;08&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nv&#34;&gt;r2&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;8&lt;/span&gt;
       6:	bf &lt;span class=&#34;m&#34;&gt;03&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nv&#34;&gt;r3&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; r0

      // bpf_probe_read 调用读出 real_parent &lt;span class=&#34;nv&#34;&gt;task_struct&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; task-&amp;gt;real_parent
       7:	&lt;span class=&#34;m&#34;&gt;85&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;71&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	call &lt;span class=&#34;m&#34;&gt;113&lt;/span&gt;

      // &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;596&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; STRUCT task_struct &lt;span class=&#34;nv&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;6848&lt;/span&gt;
      //   ...
      // pid &lt;span class=&#34;nv&#34;&gt;type_id&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;1800&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;bitfield_size&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;bits_offset&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;9344&lt;/span&gt;
       8:	b7 &lt;span class=&#34;m&#34;&gt;01&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;90&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;04&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nv&#34;&gt;r1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;1168&lt;/span&gt;
       9:	&lt;span class=&#34;m&#34;&gt;79&lt;/span&gt; a3 f0 ff &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nv&#34;&gt;r3&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; *&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;u64 *&lt;span class=&#34;o&#34;&gt;)(&lt;/span&gt;r10 - 16&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;
      10:	0f &lt;span class=&#34;m&#34;&gt;13&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nv&#34;&gt;r3&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; r1
      11:	bf a1 &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nv&#34;&gt;r1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; r10
      12:	&lt;span class=&#34;m&#34;&gt;07&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;01&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;fc&lt;/span&gt; ff ff ff	&lt;span class=&#34;nv&#34;&gt;r1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; -4
      13:	b7 &lt;span class=&#34;m&#34;&gt;02&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;04&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nv&#34;&gt;r2&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;4&lt;/span&gt;
      14:	&lt;span class=&#34;m&#34;&gt;85&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;71&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	call &lt;span class=&#34;m&#34;&gt;113&lt;/span&gt;
      15:	&lt;span class=&#34;m&#34;&gt;61&lt;/span&gt; a0 &lt;span class=&#34;nb&#34;&gt;fc&lt;/span&gt; ff &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nv&#34;&gt;r0&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; *&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;u32 *&lt;span class=&#34;o&#34;&gt;)(&lt;/span&gt;r10 - 4&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;
      16:	&lt;span class=&#34;m&#34;&gt;95&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nb&#34;&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;但上面的 eBPF 指令访问 &lt;code&gt;task_struct&lt;/code&gt; 内部字段是基于 v5.8 内核 BTF 构建的，和我当前的 v5.13.0 内核版本存在较大的差异， &lt;code&gt;real_parent&lt;/code&gt; 和 &lt;code&gt;pid&lt;/code&gt; 偏移量都变化较多。而且 &lt;code&gt;llvm-objdump -dr&lt;/code&gt; 并没有显示哪些 [读取字段的指令] 需要替换。由于这部分指令修改策略还没有更新到当前最新的 &lt;code&gt;v5.17.0-rc5&lt;/code&gt; 版本上，只能通过阅读代码来获取。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;➜  uname -a
Linux chaofan 5.13.0-30-generic &lt;span class=&#34;c1&#34;&gt;#33~20.04.1-Ubuntu SMP Mon Feb 7 14:25:10 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux&lt;/span&gt;

➜  bpftool btf dump file /sys/kernel/btf/vmlinux &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; less
...
&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;174&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt; STRUCT &lt;span class=&#34;s1&#34;&gt;&amp;#39;task_struct&amp;#39;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;size&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;9472&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;vlen&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;232&lt;/span&gt;
...
  &lt;span class=&#34;s1&#34;&gt;&amp;#39;real_parent&amp;#39;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;type_id&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;175&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;bits_offset&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;18816&lt;/span&gt;
  &lt;span class=&#34;s1&#34;&gt;&amp;#39;pid&amp;#39;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;type_id&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;60&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;bits_offset&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;m&#34;&gt;18688&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;通过查看代码，我发现主要由三个 Patch 来提供替换的编码信息：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://reviews.llvm.org/rG048493f882faf19d80a1343d877d69cbb19c5091&#34;&gt;[BPF] Preserve debuginfo array/union/struct type/access index&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://lore.kernel.org/bpf/20190801064803.2519675-9-andriin@fb.com/t/&#34;&gt;CO-RE offset relocations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://lore.kernel.org/bpf/20211124060209.493-1-alexei.starovoitov@gmail.com/&#34;&gt;bpf: CO-RE support in the kernel&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中 llvm 的 Patch 做到了在 BTF 中保留对内核数据结构的访问路径, 如下面的代码片段所示， 目前 libbpf 加载器一次的访问路径的最大深度为 9。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// https://github.com/cilium/ebpf/blob/v0.8.1/internal/btf/core.go#L435
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;sample&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
  &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
  &lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
    &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;];&lt;/span&gt;
  &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;};&lt;/span&gt;

&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;sample&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;s&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;...;&lt;/span&gt;                                                                                         
&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;x&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;a&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;     &lt;span class=&#34;c1&#34;&gt;// encoded as &amp;#34;0:0&amp;#34; (a is field #0)
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;y&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;5&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;];&lt;/span&gt;  &lt;span class=&#34;c1&#34;&gt;// encoded as &amp;#34;0:1:0:5&amp;#34; (anon struct is field #1,
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                   &lt;span class=&#34;c1&#34;&gt;// b is field #0 inside anon struct, accessing elem #5)
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;z&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;b&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// encoded as &amp;#34;10:1&amp;#34; (ptr is used as an array)
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;
&lt;span class=&#34;c1&#34;&gt;// More info： https://llvm.org/docs/LangRef.html#getelementptr-instruction
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;而 &lt;code&gt;0:1:0:5&lt;/code&gt; 这样的信息作为字符串保存在 &lt;code&gt;.BTF&lt;/code&gt; section 中，而重写指令的偏移量则保存在 &lt;code&gt;.BTF.ext&lt;/code&gt; 的 sub-section 中，具体的数据结构如下：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-c&#34; data-lang=&#34;c&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// https://www.kernel.org/doc/html/latest/bpf/btf.html
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;btf_ext_info_sec&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
   &lt;span class=&#34;n&#34;&gt;__u32&lt;/span&gt;   &lt;span class=&#34;n&#34;&gt;sec_name_off&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;cm&#34;&gt;/* offset to section name */&lt;/span&gt;
   &lt;span class=&#34;n&#34;&gt;__u32&lt;/span&gt;   &lt;span class=&#34;n&#34;&gt;num_info&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
   &lt;span class=&#34;cm&#34;&gt;/* Followed by num_info * record_size number of bytes */&lt;/span&gt;
   &lt;span class=&#34;n&#34;&gt;__u8&lt;/span&gt;    &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;];&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;};&lt;/span&gt;

&lt;span class=&#34;c1&#34;&gt;// https://github.com/libbpf/libbpf/blob/v0.7.0/include/uapi/linux/bpf.h#L6560
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;k&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;bpf_core_relo&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
  &lt;span class=&#34;n&#34;&gt;__u32&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;insn_off&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;              &lt;span class=&#34;c1&#34;&gt;// 一开始是 section 偏移量，读出来一般都会修改成 [基于所在函数的偏移量]，
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                               &lt;span class=&#34;c1&#34;&gt;// 方便后续不同 section 函数指令合并后的指令重写。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;
  &lt;span class=&#34;n&#34;&gt;__u32&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;type_id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;               &lt;span class=&#34;c1&#34;&gt;// 以上面 struct sample，那么 type_id 为 struct sample 的 BTF ID                                            
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;n&#34;&gt;__u32&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;access_str_off&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;        &lt;span class=&#34;c1&#34;&gt;// 0:1:0:5 
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;  &lt;span class=&#34;k&#34;&gt;enum&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;bpf_core_relo_kind&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;kind&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;};&lt;/span&gt;

&lt;span class=&#34;c1&#34;&gt;// sub-section layout about bpf_core_relo
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;btf_ext_info_sec&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;section&lt;/span&gt; &lt;span class=&#34;err&#34;&gt;#&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;cm&#34;&gt;/* bpf_core_relo for section #1 */&lt;/span&gt;
&lt;span class=&#34;n&#34;&gt;btf_ext_info_sec&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;section&lt;/span&gt; &lt;span class=&#34;err&#34;&gt;#&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt; &lt;span class=&#34;cm&#34;&gt;/* bpf_core_relo for section #2 */&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;...&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;有了这样访问路径信息后，加载器会读出本地 BTF 信息按照访问路径进行相应的结构体匹配，当然这里是 [尽力去找最匹配的结果]，比如结构体的名字要一致，相应的结构体大小要一致等等。如果存在匹配结果，那么 &lt;code&gt;bpf_core_relo.insn_off&lt;/code&gt; 对应的指令将会被修改； 否则，加载将会返回失败。&lt;/p&gt;
&lt;p&gt;[一次编译，到处运行] 将 eBPF 使用体验都提升了好几个档次，想想看，OCI Artifacts 是不是可以 eBPF 程序作分发标准化呢？!&lt;/p&gt;
&lt;h4 id=&#34;23-call-insn-on-pc-relative&#34;&gt;2.3 CALL INSN on pc-relative?&lt;/h4&gt;
&lt;p&gt;eBPF 是将每一个函数挂到相应的事件触发器上。一般来说，调用自定义的函数都会被 inlined 掉，即使代码段不在同一个 section 里。如下面的代码所示，这样程序是不需要重写指令的。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# https://github.com/fuweid/demos/blob/master/ebpf/example-func-inline.bpf.c&lt;/span&gt;
➜  llvm-objdump -dr ./.output/example-func-inline.bpf.o

./.output/example-func-inline.bpf.o:	file format elf64-bpf


Disassembly of section .text:

&lt;span class=&#34;m&#34;&gt;0000000000000000&lt;/span&gt; &amp;lt;double_ts_in_text&amp;gt;:
       0:	bf &lt;span class=&#34;m&#34;&gt;10&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nv&#34;&gt;r0&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; r1
       1:	&lt;span class=&#34;m&#34;&gt;67&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;01&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	r0 &amp;lt;&amp;lt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;
       2:	&lt;span class=&#34;m&#34;&gt;95&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nb&#34;&gt;exit&lt;/span&gt;

Disassembly of section tp/sched/sched_process_exec:

&lt;span class=&#34;c1&#34;&gt;# 全部 inlined 到 handle_exec， 无需跳转&lt;/span&gt;
&lt;span class=&#34;m&#34;&gt;0000000000000000&lt;/span&gt; &amp;lt;handle_exec&amp;gt;:
       0:	&lt;span class=&#34;m&#34;&gt;85&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;05&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	call &lt;span class=&#34;m&#34;&gt;5&lt;/span&gt;
       1:	&lt;span class=&#34;m&#34;&gt;67&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;01&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	r0 &amp;lt;&amp;lt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;
       2:	&lt;span class=&#34;m&#34;&gt;95&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nb&#34;&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果 &lt;code&gt;double_ts&lt;/code&gt; 函数不 inlined，那么编译后的结果如下。可以看到函数代码段在不同的 Section 中，加载器需要把所有相关的代码片段都 [追加] 到 &lt;code&gt;handle_exec&lt;/code&gt; 的末尾，然后根据重定向的偏移量来修改指令。&lt;/p&gt;
&lt;p&gt;需要注意的是，内核文档在描述 eBPF CALL 指令时并没有说 Immediate 是不是相对值。我是通过查看 libbpf 加载器中 &lt;a href=&#34;https://github.com/libbpf/libbpf/blob/v0.7.0/include/uapi/linux/bpf.h#L1166&#34;&gt;BPF_PSEUDO_CALL&lt;/a&gt; 描述才知道这是一个相对值，偏移量的计算也是 &lt;a href=&#34;https://www.kernel.org/doc/html/latest/bpf/llvm_reloc.html#different-relocation-types&#34;&gt;约定值&lt;/a&gt;。函数调用重写相比 MAP/Field Offset 重写要简单些，一般这部分的指令修改都是放到最后做。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# https://github.com/fuweid/demos/blob/master/ebpf/example-func-noinline.bpf.c&lt;/span&gt;

➜  llvm-objdump -dr ./.output/example-func-noinline.bpf.o

./.output/example-func-noinline.bpf.o:	file format elf64-bpf


Disassembly of section .text:

&lt;span class=&#34;m&#34;&gt;0000000000000000&lt;/span&gt; &amp;lt;double_ts&amp;gt;:
       0:	bf &lt;span class=&#34;m&#34;&gt;10&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nv&#34;&gt;r0&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; r1
       1:	&lt;span class=&#34;m&#34;&gt;67&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;01&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	r0 &amp;lt;&amp;lt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;
       2:	&lt;span class=&#34;m&#34;&gt;95&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nb&#34;&gt;exit&lt;/span&gt;

Disassembly of section tp/sched/sched_process_exec:

&lt;span class=&#34;m&#34;&gt;0000000000000000&lt;/span&gt; &amp;lt;handle_exec&amp;gt;:
       0:	&lt;span class=&#34;m&#34;&gt;85&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;05&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	call &lt;span class=&#34;m&#34;&gt;5&lt;/span&gt;
       1:	bf &lt;span class=&#34;m&#34;&gt;01&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nv&#34;&gt;r1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; r0
       2:	&lt;span class=&#34;m&#34;&gt;85&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;10&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; ff ff ff ff	call -1
		0000000000000010:  R_BPF_64_32	.text
       3:	&lt;span class=&#34;m&#34;&gt;95&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;00&lt;/span&gt; 00	&lt;span class=&#34;nb&#34;&gt;exit&lt;/span&gt;

&lt;span class=&#34;c1&#34;&gt;# 修改后的结果 dump from kernel&lt;/span&gt;
int handle_exec&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;struct trace_event_raw_sched_process_exec * ctx&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;:
&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;ts&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; bpf_ktime_get_ns&lt;span class=&#34;o&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
   0: &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;85&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; call bpf_ktime_get_ns#135136
&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;ts&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; double_ts&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;ts&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
   1: &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;bf&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;r1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; r0
   2: &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;85&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; call pc+1#bpf_prog_6aadb6445c8badae_F
&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; ts&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
   3: &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;95&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;exit&lt;/span&gt;
u64 double_ts&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;u64 ts&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;:
&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; u64 double_ts&lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;u64 ts&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;{&lt;/span&gt;
   4: &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;bf&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;r0&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; r1
&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; ts + ts&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
   5: &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;67&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; r0 &amp;lt;&amp;lt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; ts + ts&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
   6: &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;95&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;30-conclusion&#34;&gt;3.0 Conclusion&lt;/h3&gt;
&lt;p&gt;本文只是简单介绍了 eBPF 加载器是怎么修改指令的，虽然还有很多细节没有提到，比如 &lt;code&gt;CALL bpf_ktime_get_ns&lt;/code&gt; 指令是如何工作，以及 eBPF 尾调用如何拼接在一起等等，但大体的工作方式差不多，建议读一读 libbpf 加载器，或者 &lt;a href=&#34;https://github.com/cilium/ebpf&#34;&gt;cilium/ebpf go 代码&lt;/a&gt;，还是比较有意思的。&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>go sync.Mutex 源码阅读</title>
          <link>/post/2020-go-sync-mutex-insight/</link>
          <pubDate>Sun, 21 Jun 2020 18:00:53 +0800</pubDate>
          <guid>https://fuweid.com/post/2020-go-sync-mutex-insight/</guid>
          <description>&lt;p&gt;Linux Kernel 提供 Semaphore/Mutex 来实现线程间的同步机制，可保证在同一个时间段
只有少量的线程可以访问同一块资源（也称为进入临界区域）。
线程之间要通过竞争来获得访问权限，一旦竞争失败，线程会进入到阻塞状态；
而阻塞的线程只能等待离开临界区域被内核唤醒。&lt;/p&gt;
&lt;p&gt;go runtime 提供的 sync.Mutex 并不是采用内核级别的同步机制。
作为执行单元的线程一旦阻塞，意味该线程将不再受到 go runtime 控制，
go runtime 需要创建新的线程来执行其他 runnable goroutine ，
线程的数目会和竞争资源的请求成正比，容易造成资源浪费。
而 go 优势是 goroutine 轻量级调度，因此 sync.Mutex 选择在用户态来实现同步机制。&lt;/p&gt;
&lt;p&gt;和线程阻塞类似，在无法进入临界区的情况下，goroutine 会主动释放当前的
执行单元 - 线程，进入到阻塞状态；在 sync.Mutex 持有者离开临界区之前，
阻塞状态的 goroutine 将不会出现在调度队列里。
这样被释放的线程会去执行其他 runnable goroutine，提升线程的利用率。&lt;/p&gt;
&lt;h3 id=&#34;syncmutex-结构设计分析&#34;&gt;sync.Mutex 结构设计分析&lt;/h3&gt;
&lt;p&gt;Mutex 也被称之为锁。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// sync/mutex.go
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;
&lt;span class=&#34;c1&#34;&gt;// A Mutex is a mutual exclusion lock.
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// The zero value for a Mutex is an unlocked mutex.
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;//
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// A Mutex must not be copied after first use.
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;Mutex&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
        &lt;span class=&#34;nx&#34;&gt;state&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int32&lt;/span&gt;
        &lt;span class=&#34;nx&#34;&gt;sema&lt;/span&gt;  &lt;span class=&#34;kt&#34;&gt;uint32&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;每一个 Mutex 实例都有虚拟全局唯一的地址，go runtime 通过 Mutex.sema 地址来维护
阻塞的 goroutine 队列。当 goroutine 无法获得锁的情况下，goroutine 主动调用
runtime_Semacquire ，将自己加入锁对应的阻塞队列中；而锁的持有者在释放锁之后，
根据当前阻塞情况来调用 runtime_Semrelease 方法，唤醒阻塞队列头部的 goroutine 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// runtime/sema.go

//go:linkname sync_runtime_Semrelease sync.runtime_Semrelease
func sync_runtime_Semrelease(addr *uint32, handoff bool, skipframes int) {
        semrelease1(addr, handoff, skipframes)
}

//go:linkname sync_runtime_SemacquireMutex sync.runtime_SemacquireMutex
func sync_runtime_SemacquireMutex(addr *uint32, lifo bool, skipframes int) {
        semacquire1(addr, lifo, semaBlockProfile|semaMutexProfile, skipframes)
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;而 Mutex 更多的细节是在 state 字段上。Mutex.state 将 32 bit 划分成四块区域。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2020-go-sync-mutex-insight/mutex-state.png&#34; alt=&#34;mutex-state&#34;&gt;&lt;/p&gt;
&lt;p&gt;高位 3-31 bits 表示当前进入阻塞状态的 goroutine 数目，它直接反应出调用
runtime_SemacquireMutex 的次数。runtime_SemacquireMutex 采用单链表管理队列。
正常情况下，阻塞的 goroutine 是通过尾插法的方式加入队列；释放锁的时候会唤醒队列
头部的 goroutine，即先入先出，保证了公平特性。&lt;/p&gt;
&lt;p&gt;被唤醒的 goroutine 会和新来的 goroutine 竞争加锁，
被唤醒的 goroutine 可能会因拿不到锁而重新回到阻塞队列。
在处理并发请求时，最先发起的请求会因为竞争关系可能一直拿不到锁，
导致个别请求耗时非常长；并发请求越多，这样的问题就越严重。&lt;/p&gt;
&lt;p&gt;为了保证公平性，Mutex 引入了 Starving 模式。经历了长时间阻塞，如果被唤醒的
goroutine 还是拿不到锁，它就主动加上 Starving 标志位，该标志位用来告诉新来的
goroutine 要照顾下「阻塞了长时间-刚被唤醒-还拿不到锁的同志」: &lt;strong&gt;不要加锁啦，
直接把自己加入到阻塞队列里吧&lt;/strong&gt;。这样新到达的 goroutine 会被加入到阻塞队列的尾部，
之前就在阻塞队列里的 goroutine 就可以优先被唤醒了，降低长尾带来的问题。&lt;/p&gt;
&lt;p&gt;那些被唤醒的 goroutine 再次回到阻塞队列时，它们不再重新排队，通过设置
Last In, First Out(LIFO) 来强行插队，保证它是下一个被唤醒的 goroutine。&lt;/p&gt;
&lt;p&gt;除了保护公平性之外，Starving 模式还减少了 goroutine 之间的竞争关系。
因为运气不好的情况下，新来的 goroutine 会一直拿到锁，导致唤醒的动作白费了，
系统线程还不如执行其他 runnable goroutine。&lt;/p&gt;
&lt;p&gt;Woken 比特位是用来告知持有锁的调用者：现在有一个活跃状态 goroutine 在尝试拿锁，
如果不是处于 Starving 状态，请不要在释放锁的时候做唤醒，尽量让这个活跃的
goroutine 去竞争拿锁，减少不必要的唤醒竞争。&lt;/p&gt;
&lt;p&gt;以上是 sync.Mutex 设计介绍，下面我们通过查看代码注释来了解细节。&lt;/p&gt;
&lt;h3 id=&#34;unlock-逻辑&#34;&gt;Unlock 逻辑&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;
&lt;span class=&#34;c1&#34;&gt;// sync/mutex.go
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;
&lt;span class=&#34;c1&#34;&gt;// Unlock unlocks m.
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Mutex&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;Unlock&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;race&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Enabled&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                &lt;span class=&#34;nx&#34;&gt;_&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;state&lt;/span&gt;
                &lt;span class=&#34;nx&#34;&gt;race&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Release&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;unsafe&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Pointer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;

        &lt;span class=&#34;c1&#34;&gt;// Fast path: drop lock bit.
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;        &lt;span class=&#34;c1&#34;&gt;// 通过减一来完成解锁。如果 m.state 没有其他标记位，那么解锁结束。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;        &lt;span class=&#34;c1&#34;&gt;// 否则将进入到 slow path，判断是否要唤醒其他阻塞的 goroutine。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;        &lt;span class=&#34;nx&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;atomic&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;AddInt32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexLocked&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                &lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;unlockSlow&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;

&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Mutex&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;unlockSlow&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
        &lt;span class=&#34;c1&#34;&gt;// 为了防止出现 Unlock 非锁定状态的 Mutex，需要检查下 mutexLocked 标记位。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;+&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexLocked&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexLocked&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                &lt;span class=&#34;nf&#34;&gt;throw&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;sync: unlock of unlocked mutex&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
        &lt;span class=&#34;c1&#34;&gt;// 正常模式，还未出现 Starving
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexStarving&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;new&lt;/span&gt;
                &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
			&lt;span class=&#34;c1&#34;&gt;// 这里有两大类场景，出现了直接结束掉 slow path:
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;// 没有阻塞状态的 goroutine (old &amp;gt;&amp;gt; mutexWaiterShift == 0)
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;// 还存在阻塞状态的 goroutine(s)
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;// 1. 当前有活跃状态的 goroutine (old&amp;amp;mutexWoken != 0)
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//    选择让当前活跃状态的 goroutine 去竞争锁，减少不必要的唤醒
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;// 2. 当前锁已经被其他 goroutine 获取了 (old&amp;amp;mutexLocked != 0)
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//    需要等待释放锁的时候再做唤醒，应直接退出，
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//    交给下一次 Unlock 调用在处理。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;// 3. 当前是一个 Starving 状态 (old&amp;amp;(mutexStarving) != 0)
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//    进入循环前是「非 Starving」状态，而现在确是 Starving 模式。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//    说明这段时间里出现了 (Lock/Unlock)../Lock 连续调用，
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//    导致「被其他 Unlock 调用唤醒的 goroutine」 拿不到锁，
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//    进入到 Starving 模式。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//    这种情况下应该直接退出，交给下一次 Unlock 调用在处理了。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexWaiterShift&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexLocked&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexWoken&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexStarving&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                                &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;
                        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;

			&lt;span class=&#34;c1&#34;&gt;// 这个时候 mutexLocked|Starving|Woken 标记位为空，尝试将阻塞数目减一。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;// 只要 CAS 原子操作成功，就可以唤醒阻塞队列头部的 goroutine。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                        &lt;span class=&#34;nx&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexWaiterShift&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;mutexWoken&lt;/span&gt;
                        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;atomic&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;CompareAndSwapInt32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                                &lt;span class=&#34;nf&#34;&gt;runtime_Semrelease&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;sema&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
                                &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;
                        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
                        &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;state&lt;/span&gt;
                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
		&lt;span class=&#34;c1&#34;&gt;// 因为出现 Starving 状态，说明阻塞时间足够长了，Unlock 调用者会将
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;c1&#34;&gt;// runtime_Semrelease 函数第二个参数设置成 true，表示会主动释放
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;c1&#34;&gt;// 当前执行线程，而当前执行线程会直接执行阻塞队列头部的 goroutine。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;
		&lt;span class=&#34;c1&#34;&gt;// 被唤醒的 goroutine 相当于获得锁的状态了，因为在 Starving 状态下，
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;c1&#34;&gt;// 新到达的 goroutine 不会竞争锁，它们会直接进入阻塞队列。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                &lt;span class=&#34;nf&#34;&gt;runtime_Semrelease&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;sema&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;lock-细节&#34;&gt;Lock 细节&lt;/h3&gt;
&lt;p&gt;如果一开始 Mutex.state 是一个空值状态，那么 CAS 更新 mutexLocked 标志位会直接成功，相当于上锁了。
那么其他 goroutine 想要上锁就要走 slow path 了。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// sync/mutex.go
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;
&lt;span class=&#34;c1&#34;&gt;// Lock locks m.
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// If the lock is already in use, the calling goroutine
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// blocks until the mutex is available.
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Mutex&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;Lock&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
        &lt;span class=&#34;c1&#34;&gt;// Fast path: grab unlocked mutex.
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;atomic&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;CompareAndSwapInt32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;mutexLocked&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;race&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Enabled&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                        &lt;span class=&#34;nx&#34;&gt;race&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Acquire&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;unsafe&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Pointer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
                &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt;
        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
        &lt;span class=&#34;c1&#34;&gt;// Slow path (outlined so that the fast path can be inlined)
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;        &lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;lockSlow&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里代码细节比较多，我们直接查看中文注释~&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// sync/mutex.go
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;
&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Mutex&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;lockSlow&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
        &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;waitStartTime&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int64&lt;/span&gt;
	&lt;span class=&#34;c1&#34;&gt;// 所有刚进入 slow path 的 goroutine 都会以正常模式运行
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;	&lt;span class=&#34;c1&#34;&gt;// 只有出现阻塞了超过 1ms 的情况，才会将 starving = true
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;        &lt;span class=&#34;nx&#34;&gt;starving&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt;
        &lt;span class=&#34;nx&#34;&gt;awoke&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;false&lt;/span&gt;
        &lt;span class=&#34;nx&#34;&gt;iter&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;
        &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;state&lt;/span&gt;
        &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
		&lt;span class=&#34;c1&#34;&gt;// 正常模式下（非 Starving） 的时候，新到达的 goroutine 会尝试
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;c1&#34;&gt;// 空转 4 次左右。如果还是 Locked 状态 或者 出现了 Starving 状态，
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;c1&#34;&gt;// goroutine 会尝试释放执行单元，进入阻塞状态。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexLocked&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexStarving&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;mutexLocked&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;runtime_canSpin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;iter&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
			&lt;span class=&#34;c1&#34;&gt;// 如果阻塞队列非空，那么应该尝试设置上 Woken 状态。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;// 用来通知 Unlock 不要做唤醒动作，让当前的 goroutine 去竞争锁。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;awoke&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexWoken&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexWaiterShift&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt;
                                &lt;span class=&#34;nx&#34;&gt;atomic&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;CompareAndSwapInt32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexWoken&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                                &lt;span class=&#34;nx&#34;&gt;awoke&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;
                        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
                        &lt;span class=&#34;nf&#34;&gt;runtime_doSpin&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
                        &lt;span class=&#34;nx&#34;&gt;iter&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;++&lt;/span&gt;
                        &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;state&lt;/span&gt;
                        &lt;span class=&#34;k&#34;&gt;continue&lt;/span&gt;
                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
                &lt;span class=&#34;nx&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;
                &lt;span class=&#34;c1&#34;&gt;// 如果已经处于 Starving 状态了，那么新到达的 goroutine 就不要
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;c1&#34;&gt;// 去竞争锁了。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexStarving&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                        &lt;span class=&#34;nx&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;|=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;mutexLocked&lt;/span&gt;
                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
		&lt;span class=&#34;c1&#34;&gt;// 如果当前(已经上锁|处于 Starving) 状态，那么(新到达|被唤醒) 
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;c1&#34;&gt;// goroutine 应该变成阻塞状态。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexLocked&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexStarving&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                        &lt;span class=&#34;nx&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;+=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;mutexWaiterShift&lt;/span&gt;
                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
		&lt;span class=&#34;c1&#34;&gt;//「长时间阻塞 - 被唤醒了还拿不到锁」goroutine 会设置上 Starving。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;c1&#34;&gt;// 希望在释放锁的时候，优先唤醒自己。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;starving&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexLocked&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                        &lt;span class=&#34;nx&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;|=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;mutexStarving&lt;/span&gt;
                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
		&lt;span class=&#34;c1&#34;&gt;// 如果是当前 goroutine 设置上了 woken 状态，那么在尝试获得锁的时候，
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;		&lt;span class=&#34;c1&#34;&gt;// 应该去掉该标记位。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;awoke&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexWoken&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                                &lt;span class=&#34;nf&#34;&gt;throw&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;sync: inconsistent mutex state&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
                        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
                        &lt;span class=&#34;nx&#34;&gt;new&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;amp;^=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;mutexWoken&lt;/span&gt;
                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
                &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;atomic&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;CompareAndSwapInt32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;new&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
			&lt;span class=&#34;c1&#34;&gt;// 1. old&amp;amp;(mutexLocked|mutexStarving) = 10B
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//    锁已经释放，但正在唤醒「设置 Starving」 goroutine，
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//    当前 goroutine 拿不到锁；
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;// 2. old&amp;amp;(mutexLocked|mutexStarving) = 01B
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//    锁还没被释放，当前 goroutine 拿不到锁；
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;// 3. old&amp;amp;(mutexLocked|mutexStarving) = 11B
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//    被唤醒的 goroutine 刚更新成 Starving 状态，
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//    当前 goroutine 拿不到锁；
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;// 4. old&amp;amp;(mutexLocked|mutexStarving) = 0
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//    正好遇到释放锁，运气不错，new 值拿到锁，退出。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexLocked&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexStarving&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                                &lt;span class=&#34;k&#34;&gt;break&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// locked the mutex with CAS
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
                        &lt;span class=&#34;nx&#34;&gt;queueLifo&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;waitStartTime&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;
                        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;waitStartTime&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                                &lt;span class=&#34;nx&#34;&gt;waitStartTime&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;runtime_nanotime&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
                        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
			&lt;span class=&#34;c1&#34;&gt;// 「被唤醒过 - 但竞争失败」的 goroutine 都采用 LIFO 
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;// 头插法入队，即插队。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;//
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;// runtime_SemacuireMutex 将当前 goroutine 设置成阻塞态
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                        &lt;span class=&#34;nf&#34;&gt;runtime_SemacquireMutex&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;sema&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;queueLifo&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;

                        &lt;span class=&#34;c1&#34;&gt;// 被唤醒之后继续执行 
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                        &lt;span class=&#34;nx&#34;&gt;starving&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;starving&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;runtime_nanotime&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;waitStartTime&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;starvationThresholdNs&lt;/span&gt;
                        &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;state&lt;/span&gt;

			&lt;span class=&#34;c1&#34;&gt;// old&amp;amp;mutexStarving != 0 说明当前 goroutine 已经拿到锁。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;// 但这个时候 Mutex.state 相应标记位还没更新。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexStarving&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
				&lt;span class=&#34;c1&#34;&gt;// 在 Starving 状态下，Unlock 只负责唤醒，并不
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;				&lt;span class=&#34;c1&#34;&gt;// 会更新 Mutex.state 状态。如果状态被修改成
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;				&lt;span class=&#34;c1&#34;&gt;// mutexLocked，导致不一致，应该 panic。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                                &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexLocked&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexWoken&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;!=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexWaiterShift&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                                        &lt;span class=&#34;nf&#34;&gt;throw&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;sync: inconsistent mutex state&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
                                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
                                &lt;span class=&#34;c1&#34;&gt;// 更新 mutexLocked 以及对阻塞数目减一。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                                &lt;span class=&#34;nx&#34;&gt;delta&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;int32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexLocked&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexWaiterShift&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
				&lt;span class=&#34;c1&#34;&gt;// 如果 Starving 状态不清理，那么每次 Unlock
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;				&lt;span class=&#34;c1&#34;&gt;// 都会直接唤醒阻塞队列里的。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;				&lt;span class=&#34;c1&#34;&gt;// 
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;				&lt;span class=&#34;c1&#34;&gt;// 毕竟 Starving 会让新到达的 goroutine 直接放
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;				&lt;span class=&#34;c1&#34;&gt;// 弃竞争，解决某些「阻塞太久 goroutine」
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;				&lt;span class=&#34;c1&#34;&gt;// 获得锁的问题，但也浪费了新到达的 goroutine
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;				&lt;span class=&#34;c1&#34;&gt;// 的执行时间。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;				&lt;span class=&#34;c1&#34;&gt;//
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;				&lt;span class=&#34;c1&#34;&gt;// 如果发现阻塞队列里的 goroutine 并没有达到
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;				&lt;span class=&#34;c1&#34;&gt;// Starving 设置阈值，那么 应该清理掉 Starving
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;				&lt;span class=&#34;c1&#34;&gt;// 标记位。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                                &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;!&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;starving&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;||&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;mutexWaiterShift&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;==&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                                        &lt;span class=&#34;nx&#34;&gt;delta&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;mutexStarving&lt;/span&gt;
                                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
                                &lt;span class=&#34;nx&#34;&gt;atomic&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;AddInt32&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;state&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;delta&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
                                &lt;span class=&#34;k&#34;&gt;break&lt;/span&gt;
                        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
			&lt;span class=&#34;c1&#34;&gt;// 属于正常唤醒，Unlock 已经帮忙设置上 mutexWoken 标记
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;			&lt;span class=&#34;c1&#34;&gt;// 和 对阻塞数目减一。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                        &lt;span class=&#34;nx&#34;&gt;awoke&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;
                        &lt;span class=&#34;nx&#34;&gt;iter&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;
                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;else&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                        &lt;span class=&#34;nx&#34;&gt;old&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;state&lt;/span&gt;
                &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;

        &lt;span class=&#34;k&#34;&gt;if&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;race&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Enabled&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                &lt;span class=&#34;nx&#34;&gt;race&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Acquire&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;unsafe&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Pointer&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;小结&#34;&gt;小结&lt;/h3&gt;
&lt;p&gt;sync.Mutex 整体代码量不多，其中很多细节都被 runtime.sync_runtime_SemacquireMutex 和
runtime.sync_runtime_Semrelease 函数屏蔽了，后面有时间会更新这部分的代码分析。&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>可以同时对一个 go string 进行读写操作吗？</title>
          <link>/post/2020-go-string-data-race/</link>
          <pubDate>Sat, 30 May 2020 00:00:00 -0400</pubDate>
          <guid>https://fuweid.com/post/2020-go-string-data-race/</guid>
          <description>&lt;p&gt;写过 Go 代码的同学都知道，在程序内启动多个 goroutine 处理任务是很常见的事情，
启动一个 goroutine 要比启动一个线程简单的多。当多个 goroutine 同时处理同一份数据时，
我们应该在代码中加入同步机制，保证多个 goroutine 按照一定顺序来访问数据，
不然就会出现 data race。
最常见的例子如下，同时写操作 map 数据会导致程序 panic，即使操作的是不同 key：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// example 1
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;
&lt;span class=&#34;kn&#34;&gt;package&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;main&lt;/span&gt;

&lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;main&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
        &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                &lt;span class=&#34;nx&#34;&gt;c&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;make&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;chan&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;bool&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
                &lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;:=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;make&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;kd&#34;&gt;map&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
                &lt;span class=&#34;k&#34;&gt;go&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
                        &lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;1&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;a&amp;#34;&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// First conflicting access.
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                        &lt;span class=&#34;nx&#34;&gt;c&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;lt;-&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;true&lt;/span&gt;
                &lt;span class=&#34;p&#34;&gt;}()&lt;/span&gt;
                &lt;span class=&#34;nx&#34;&gt;m&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s&#34;&gt;&amp;#34;2&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;b&amp;#34;&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;// Second conflicting access.
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;                &lt;span class=&#34;o&#34;&gt;&amp;lt;-&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;c&lt;/span&gt;
        &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;那么下面的代码也会 panic 吗？&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// example 2
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;//
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;  &lt;span class=&#34;kn&#34;&gt;package&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;main&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt;  &lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;sync&amp;#34;&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;4&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;5&lt;/span&gt;  &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;main&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;6&lt;/span&gt;          &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;wg&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;sync&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;WaitGroup&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;7&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;8&lt;/span&gt;          &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;9&lt;/span&gt;                  &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;s&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;                  &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;r&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[]&lt;/span&gt;&lt;span class=&#34;kt&#34;&gt;byte&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;11&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;12&lt;/span&gt;                  &lt;span class=&#34;nx&#34;&gt;wg&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Add&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;13&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;14&lt;/span&gt;                  &lt;span class=&#34;c1&#34;&gt;// goroutine 1: update string s
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;15&lt;/span&gt;                  &lt;span class=&#34;k&#34;&gt;go&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;16&lt;/span&gt;                          &lt;span class=&#34;k&#34;&gt;defer&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;wg&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Done&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;17&lt;/span&gt;                          &lt;span class=&#34;nx&#34;&gt;s&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;panic?&amp;#34;&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;18&lt;/span&gt;                  &lt;span class=&#34;p&#34;&gt;}()&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;19&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;20&lt;/span&gt;                  &lt;span class=&#34;c1&#34;&gt;// goroutine 2: read string s
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;21&lt;/span&gt;                  &lt;span class=&#34;k&#34;&gt;go&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;22&lt;/span&gt;                          &lt;span class=&#34;k&#34;&gt;defer&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;wg&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Done&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;23&lt;/span&gt;                          &lt;span class=&#34;nx&#34;&gt;r&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;append&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;r&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;...&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;24&lt;/span&gt;                  &lt;span class=&#34;p&#34;&gt;}()&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;25&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;26&lt;/span&gt;                  &lt;span class=&#34;nx&#34;&gt;wg&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nf&#34;&gt;Wait&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;27&lt;/span&gt;          &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;28&lt;/span&gt;  &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;答案稍后揭晓，现在先看下 Go Runtime 下是如何描述 string 数据: 一个 string 有两个
字段，str 用来存储长度为 len 的字符串。那么 string 的赋值会是原子操作吗？&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span class=&#34;c1&#34;&gt;// https://github.com/golang/go/tree/release-branch.go1.13/src/runtime/string.go
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;
&lt;span class=&#34;kd&#34;&gt;type&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;stringStruct&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;struct&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
        &lt;span class=&#34;nx&#34;&gt;str&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;unsafe&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;Pointer&lt;/span&gt;
        &lt;span class=&#34;nx&#34;&gt;len&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;int&lt;/span&gt;
&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;

&lt;span class=&#34;c1&#34;&gt;// 为了防止编译器优化带来影响，需要在下面的代码里引入 print 和额外的 goroutine，
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// 保证在汇编结果里就可以看到实际的字符串赋值语句了。
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;//
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;// cat -n main.go
&lt;/span&gt;&lt;span class=&#34;c1&#34;&gt;&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;  &lt;span class=&#34;kn&#34;&gt;package&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;main&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt;  &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;main&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;4&lt;/span&gt;          &lt;span class=&#34;kd&#34;&gt;var&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;s&lt;/span&gt; &lt;span class=&#34;kt&#34;&gt;string&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;5&lt;/span&gt;          &lt;span class=&#34;k&#34;&gt;go&lt;/span&gt; &lt;span class=&#34;kd&#34;&gt;func&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;6&lt;/span&gt;                  &lt;span class=&#34;nx&#34;&gt;s&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;&amp;#34;I am string&amp;#34;&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;7&lt;/span&gt;          &lt;span class=&#34;p&#34;&gt;}()&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;8&lt;/span&gt;          &lt;span class=&#34;nb&#34;&gt;print&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nx&#34;&gt;s&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;span class=&#34;mi&#34;&gt;9&lt;/span&gt;  &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;为了查看具体 string 赋值代码，这里需要使用 &lt;code&gt;go tool compile -S ./main.go&lt;/code&gt; 来获
取汇编结果。在下面的输出结果中，&lt;code&gt;s = &amp;quot;I am string&amp;quot;&lt;/code&gt; 赋值语句会被拆成两部分: 先
更新字符串的长度 len 字段, 再更新具体的字符串内容到 str 字段。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&#34;language-assemble&#34; data-lang=&#34;assemble&#34;&gt;&amp;quot;&amp;quot;.main.func1 STEXT size=89 args=0x8 locals=0x8
	0x0000 00000 (./main.go:5)      TEXT    &amp;quot;&amp;quot;.main.func1(SB), ABIInternal, $8-8
	0x0000 00000 (./main.go:5)      MOVQ    (TLS), CX
	0x0009 00009 (./main.go:5)      CMPQ    SP, 16(CX)
	0x000d 00013 (./main.go:5)      JLS     82
	0x000f 00015 (./main.go:5)      SUBQ    $8, SP
	0x0013 00019 (./main.go:5)      MOVQ    BP, (SP)
	0x0017 00023 (./main.go:5)      LEAQ    (SP), BP
	0x001b 00027 (./main.go:5)      FUNCDATA        $0, gclocals·1a65e721a2ccc325b382662e7ffee780(SB)
	0x001b 00027 (./main.go:5)      FUNCDATA        $1, gclocals·69c1753bd5f81501d95132d08af04464(SB)
	0x001b 00027 (./main.go:5)      FUNCDATA        $2, gclocals·39825eea4be6e41a70480a53a624f97b(SB)
	0x001b 00027 (./main.go:6)      PCDATA  $0, $1
	0x001b 00027 (./main.go:6)      PCDATA  $1, $1

	  0x001b 00027 (./main.go:6)      MOVQ    &amp;quot;&amp;quot;.&amp;amp;s+16(SP), DI
先更新长度  0x0020 00032 (./main.go:6)      MOVQ    $11, 8(DI)

	0x0028 00040 (./main.go:6)      PCDATA  $0, $-2
	0x0028 00040 (./main.go:6)      PCDATA  $1, $-2
	0x0028 00040 (./main.go:6)      CMPL    runtime.writeBarrier(SB), $0
	0x002f 00047 (./main.go:6)      JNE     68

再赋值内容  0x0031 00049 (./main.go:6)      LEAQ    go.string.&amp;quot;I am string&amp;quot;(SB), AX
	  0x0038 00056 (./main.go:6)      MOVQ    AX, (DI)

	0x003b 00059 (./main.go:7)      MOVQ    (SP), BP
	0x003f 00063 (./main.go:7)      ADDQ    $8, SP
	0x0043 00067 (./main.go:7)      RET
	0x0044 00068 (./main.go:6)      LEAQ    go.string.&amp;quot;I am string&amp;quot;(SB), AX
	0x004b 00075 (./main.go:6)      CALL    runtime.gcWriteBarrier(SB)
	0x0050 00080 (./main.go:6)      JMP     59
	0x0052 00082 (./main.go:6)      NOP
	0x0052 00082 (./main.go:5)      PCDATA  $1, $-1
	0x0052 00082 (./main.go:5)      PCDATA  $0, $-1
	0x0052 00082 (./main.go:5)      CALL    runtime.morestack_noctxt(SB)
	0x0057 00087 (./main.go:5)      JMP     0
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;NOTE: &lt;code&gt;runtime.xxxBarrier&lt;/code&gt; 是 Go 编译器为垃圾回收生成的代码，可以忽略。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;回到一开始的问题 example 2 代码片段，&lt;code&gt;r = append(r, s...)&lt;/code&gt; 采用 &lt;a href=&#34;https://github.com/golang/go/blob/release-branch.go1.13/src/runtime/stubs.go#L88&#34;&gt;memmove&lt;/a&gt;
方法从字符串 s 拷贝 len(s) 个字节到 r 里。由于 &lt;code&gt;s = &amp;quot;panic?&amp;quot;&lt;/code&gt; 赋值和 &lt;code&gt;append&lt;/code&gt; 读
操作是同时进行：假设 s.len 已经被更新成 6 ，但是 s.str 还是 nil 状态，这个时候
正好执行了 append 的操作，直接读取空指针必定会 panic。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;// 其中一种可能的执行顺序

goruntine 1: set s.len = len(&amp;#34;panic?&amp;#34;) # 6 字节
goruntine 2: r = append(r, s...)       # 将从 s.str 中拷贝 6 字节，但 s.str = nil
goroutine 1: set s.str = &amp;#34;panic?&amp;#34;

// part of example 2
//
(...)

14                  // goroutine 1: update string s
15                  go func() {
16                          defer wg.Done()
17                          s = &amp;#34;panic?&amp;#34;
18                  }()
19
20                  // goroutine 2: read string s
21                  go func() {
22                          defer wg.Done()
23                          r = append(r, s...)
24                  }()

(...)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;除了 append 这种场景以外，字符串的比较同样需要 len 和 str 一致。如果在执行读操作
时，str 实际存储的数据长度比 len 短，程序就会 panic。所以避免 data race 最好方式
就是采用合适的同步机制，这来自 Go 团队给出的最佳实践：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Programs that modify data being simultaneously accessed by multiple goroutines must serialize such access.&lt;/p&gt;
&lt;p&gt;from &lt;a href=&#34;https://golang.org/ref/mem&#34;&gt;https://golang.org/ref/mem&lt;/a&gt; Advice section&lt;/p&gt;
&lt;/blockquote&gt;
</description>
        </item>
        
        <item>
          <title>在线看 O&#39;Reilly 动物新书指南</title>
          <link>/post/2020-digital-book-from-safari/</link>
          <pubDate>Sat, 21 Mar 2020 00:00:00 -0400</pubDate>
          <guid>https://fuweid.com/post/2020-digital-book-from-safari/</guid>
          <description>&lt;p&gt;想当初，为了看 &lt;a href=&#34;http://pages.cs.wisc.edu/~remzi/OSTEP/#book-chapters&#34;&gt;Operating Systems: Three Easy Pieces&lt;/a&gt; 和 &lt;a href=&#34;https://book.douban.com/subject/30218046/&#34;&gt;A Philosophy of Software Design&lt;/a&gt; 原版技术书，还特别麻烦了朋友从国外人肉带回来，成本极高。
但如果等国内出版社引进，就会出现时间跨度太大没法尝鲜；加上翻译水平参差不齐，等待中文版的路子基本上行不通。
为了解决这个尴尬问题，最近找到了一个比较实惠看国外原版书籍的方式：&lt;strong&gt;ACM Professional Membership&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;ACM Professional Membership &lt;a href=&#34;https://www.acm.org/membership/membership-benefits&#34;&gt;会员权益&lt;/a&gt; 有很多，其中有一项是：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Learning Center with resources for lifelong learning, including online courses
targeted toward essential IT skills and popular certifications;
online books &amp;amp; videos from Skillsoft®, &lt;strong&gt;online books from O&amp;rsquo;Reilly®&lt;/strong&gt;,
Morgan Kaufmann and Syngress; videos and webinars on hot topics, presented by today&amp;rsquo;s innovators&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;会员可以享受学习平台，其中包含了 O&amp;rsquo;Reilly online books。
平时在网上买技术书籍，基本上都能看到 O&amp;rsquo;Reilly 动物封面书籍，比如 &lt;a href=&#34;https://book.douban.com/subject/26675256/&#34;&gt;Site Reliability Engineering&lt;/a&gt;。
这个出版社覆盖的书籍比较多，基本上能满足我大部分阅读需求。
本着怀疑的态度看这个权益，没想到尝试之后，真香。&lt;/p&gt;
&lt;p&gt;ACM Professional Membership 会员直接注册的话，需要 99 美金一年。但是 ACM 还提供了 &lt;a href=&#34;https://services.acm.org/public/qj/proflevel/countryListing.cfm?promo=PWEBTOP&amp;amp;form_type=Professional&#34;&gt;Special rates for Professionals in economically developing countries&lt;/a&gt; 优惠。在页面上你会看见 China 字样，点击之后会发现一年基本包才 170 RMB，真的优惠不少。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&#34;https://www.acm.org/&#34;&gt;ACM&lt;/a&gt; 主页有 &lt;a href=&#34;https://www.acm.org/membership/join-acm&#34;&gt;Join&lt;/a&gt; 链接，点击就可以看到 &lt;strong&gt;Special rates for Professionals in economically developing countries&lt;/strong&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2020-digital-book-from-safari/special-rate-for-China.png&#34; alt=&#34;special-rate-for-China&#34;&gt;&lt;/p&gt;
&lt;p&gt;只要有本科学士学位或者同等教育，或者两年 IT 领域的工作经历就可以申请会员了。
我在注册过程中，没有遇到需要提供相关证明的要求，基本上填完信息、付款完毕就申请成功了。
但是付款页面做的实在太简陋了，用信用卡看着就不安全，再加上邮寄付款方式不方便，所以最后选择了 Paypal 支付。
注册完毕之后，申请 ACM Web Account 就大功告成了，如何登陆 O&amp;rsquo;Reilly 可以查看&lt;a href=&#34;https://learning.acm.org/faq/oreilly-faqs&#34;&gt;学习平台使用指南&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;在家使用了一个月，在电脑和手机端都可以登陆，除了页面搜索响应速度不快以外，基本上体验还是很不错。
最重要的是新书基本都可以看到，就像最新上架的 &lt;a href=&#34;https://book.douban.com/subject/34875994/&#34;&gt;Software Engineering at Google&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2020-digital-book-from-safari/preview-for-se-at-google.png&#34; alt=&#34;preview-for-se-at-google&#34;&gt;&lt;/p&gt;
&lt;p&gt;言而总之，一言以蔽之，这付费订阅体验真的很好！&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>Hola Barcelona</title>
          <link>/post/2019-hola-barcelona/</link>
          <pubDate>Sun, 02 Jun 2019 00:00:00 +0000</pubDate>
          <guid>https://fuweid.com/post/2019-hola-barcelona/</guid>
          <description>&lt;p&gt;前段时间因为 KubeCon 演讲去了趟西班牙-巴塞罗那，忙里偷闲，感受了下西方文化。&lt;/p&gt;
&lt;h3 id=&#34;不再是白本&#34;&gt;不再是「白本」&lt;/h3&gt;
&lt;p&gt;5.18 号从杭州出发，途径香港转机到巴塞罗那。飞机上的娱乐设施还算丰富，「海王」、「绿皮书」等新片都可以观看到。十几个小时的飞机总不能一直看电影，还得兼顾倒时差的任务。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2019-hola-barcelona/airport-departure.jpeg&#34; alt=&#34;airport-departure&#34;&gt;&lt;/p&gt;
&lt;p&gt;之前没有调整时差的经验，加上平时作息比较规律，十三个小时的飞行过程里相对属于清醒状态。网络要收费，印象中比较贵，基本上干不了别的事情，除了看电影就是睡觉。如果经济允许，可以考虑升舱，坐经济舱飞十几小时简直了。到达 巴塞罗那 是当地时间早上8点，天气还算不错，就是有点过于「凉快」，完全没有夏天的感觉。上了摆渡车，直奔海关。&lt;/p&gt;
&lt;p&gt;当地的海关工作人员整体都不严肃，有些还带着耳机工作，有点不可思议。轮到我的时候，那位海关小哥看了我半天，感觉不像，盖章的时候特别犹豫，而且盖完章之后他应该是后悔了，还用类似验钞机的东西反复扫描我的签注，最后才说「Wei Fu, Welcome」。&lt;/p&gt;
&lt;p&gt;说句实在话，我当时的反应是这签证不会特么是假的吧，因为从广州签证处提交申请到拿到签证只花了「三天」，申请港澳通行证都没有那么快。还好，有惊无险。&lt;/p&gt;
&lt;h3 id=&#34;骑行友好的街道&#34;&gt;骑行友好的街道&lt;/h3&gt;
&lt;p&gt;虽然没有游玩整个 巴塞罗那 ，但是可以感觉到城市的大部分街道都是单行道。我在早高峰的时候打过车，车多但不算堵。&lt;/p&gt;
&lt;p&gt;说到打车，这里的打车算是一件高消费的服务了。我没有用 Uber/MyTaxi 软件打车，大部分都是通过酒店来约车，而这种约车是需要收调度费，大概2-3 欧左右吧。接近4公里左右的路程要 15 欧，贵！&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2019-hola-barcelona/street-near-chaozhoulao.jpeg&#34; alt=&#34;street-near-chaozhoulao&#34;&gt;&lt;/p&gt;
&lt;p&gt;可能是养一辆车的费用比较高吧，这座城市的摩托车特别多，几乎随处可见。比较有意思的是摩托车车锁，他们的车锁是锁车把和车身，基本上不需要下蹲去锁车。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2019-hola-barcelona/lock-for-motobike.jpeg&#34; alt=&#34;lock-for-motobike&#34;&gt;&lt;/p&gt;
&lt;p&gt;虽然北京也有自行车道，但是大部分都被私家车占用了，对骑行的人来说极度不友好。这边的街道基本是两车道配一个自行车道，而汽车道和自行车道基本上严格分开，有些地方还有隔离带，对爱骑车的人来说真是太幸福了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2019-hola-barcelona/random-street-1.jpeg&#34; alt=&#34;random-street-1&#34;&gt;&lt;/p&gt;
&lt;p&gt;如果你打开 Mobike，估计还会有惊喜哦。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2019-hola-barcelona/mobike-near-kubecon.png&#34; alt=&#34;mobike-near-kubecon&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;地铁还不算太破&#34;&gt;地铁，还不算太破&lt;/h3&gt;
&lt;p&gt;我住的酒店离地铁站不远，走几个街头就可以到达地铁站。在长达 15 个小时日照时间里，在拓展区里步行还算安全，也比较舒服。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2019-hola-barcelona/random-street-2.jpeg&#34; alt=&#34;random-street-2&#34;&gt;&lt;/p&gt;
&lt;p&gt;随处可见的遛狗人士，转角处的饮酒闲谈，还有无法欣赏的涂鸦。建筑都相对破旧，估计这十几年的变化也就是街上跑的私家车了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2019-hola-barcelona/random-street-3.jpeg&#34; alt=&#34;random-street-3&#34;&gt;&lt;/p&gt;
&lt;p&gt;不管是去日本，还是来到 巴塞罗那，这边的地铁和火车一样，发车和到达时间点都是严格规定好的。这有个好处就是，每天都可以踩点出发。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2019-hola-barcelona/random-subway-1.jpeg&#34; alt=&#34;random-subway-1&#34;&gt;&lt;/p&gt;
&lt;p&gt;整个城市的地铁覆盖范围还是比较大，就是不同的区域由不同的公司运营，换乘基本上要出站重新购票了。如果不买套票，单程票就要 2.2 欧，贵的一匹。刷票进站后，给人感觉就是简陋，但不算太破。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2019-hola-barcelona/random-subway-2.jpeg&#34; alt=&#34;random-subway-2&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;语言no-english&#34;&gt;语言？No English&lt;/h3&gt;
&lt;p&gt;巴塞罗那 选择的时区和德国好像是一样的，都是东一区。在夏天，他们日照时间有 15 小时，到晚上 9 点天还是亮的。&lt;/p&gt;
&lt;p&gt;因为长时间日照的原因，他们物产是比较丰富的。但是想不明白的是，他们这边的特产是「西班牙火腿」。这火腿是腌制好几年而成，吃的时候切的越薄口感越好。主要这是生肉。。。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2019-hola-barcelona/meat.jpeg&#34; alt=&#34;meat&#34;&gt;&lt;/p&gt;
&lt;p&gt;和当地的「潮州佬」店主聊，他们当地喜欢吃「生」，日料店相对中餐而言要受欢迎些，估计是好吃「生」的食材吧。尝过 Tapa，也看过所谓的海鲜饭，还是觉得国内的菜还吃。&lt;/p&gt;
&lt;p&gt;出去吃饭更要命的是，点菜的时候没有图片，加上有些还没有英文注释，基本就瞎了。看不懂可以问吧，但是这边的人不太会说英语，也不愿意说。你可以想象下，机场里的工作人员会直接对你「No English」。这体验真的比去日本还糟糕。&lt;/p&gt;
&lt;h3 id=&#34;一直在修&#34;&gt;一直在「修」&lt;/h3&gt;
&lt;p&gt;城市比较小，到达的第一天就小转了一圈。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2019-hola-barcelona/random-street-4.jpeg&#34; alt=&#34;random-street-4&#34;&gt;&lt;/p&gt;
&lt;p&gt;因为加泰罗尼亚要闹独立，涂鸦就是这边的独特的风景线。从酒店走到港口，街道上的店铺都没开门。问了下当地国人，说这边的人比较懒，周末基本都享受去了，开便利店的基本都是非本地人。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2019-hola-barcelona/random-street-5.jpeg&#34; alt=&#34;random-street-5&#34;&gt;&lt;/p&gt;
&lt;p&gt;一路上都有海鸥到处飞，海鸥也不怕人，基本给啥吃啥，爆米花都吃！&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2019-hola-barcelona/random-street-6.jpeg&#34; alt=&#34;random-street-6&#34;&gt;&lt;/p&gt;
&lt;p&gt;在港口溜达一会，就去看世界上最著名的「烂尾楼」 - 「圣家族大教堂」。这座大教堂修了 100 年，因为修的时间是在太长了，不知道是不是中间换了几个设计师，教堂每个角度的风格都不太一样。不过这也算是巴塞罗那著名景点了，只可惜当天去的时候没有门票了，没能进去。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://fuweid.com/img/2019-hola-barcelona/random-street-7.jpeg&#34; alt=&#34;random-street-7&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;最后&#34;&gt;最后&lt;/h3&gt;
&lt;p&gt;这次出国没准备攻略，基本上是很佛系地逛了下，剩下大部分时间都在准备演讲内容。不过怎样，出远门才觉得国内是真TM方便。&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>工作三年</title>
          <link>/post/2018-3years-career/</link>
          <pubDate>Fri, 13 Apr 2018 00:00:00 +0000</pubDate>
          <guid>https://fuweid.com/post/2018-3years-career/</guid>
          <description>&lt;p&gt;不知不觉就过了三年，但是我还能很清楚地记得当时签卖身契的场景，只能说毕业之后时间过的飞起。这三年没写过什么年度总结，今天打算矫情一把，记下流水账。&lt;/p&gt;
&lt;h3 id=&#34;vimer&#34;&gt;Vimer&lt;/h3&gt;
&lt;p&gt;有一次参加罗老师开发环境的分享会后，我就开始迷上 vim，并结束了 IDE/Sublime 之间的摇摆。从实用角度看，IDE 有着开箱即用的特点，这的确让人无法抗拒。但在平时的工作里，不同语言之间切换是常有的事，而且经常远程调试、常年沉浸在 Terminal 的我，vim 作为编辑器是一个不错的选择。加上韩国小哥 junegunn 开源神器 fzf ，解决了 vim 全文检索巨卡的痛点，这让我毫不犹豫地坚持使用 vim。没试过 fzf 的朋友不妨试试！&lt;/p&gt;
&lt;p&gt;我在习惯 HJKL 的同时，也在尝试回馈社区。还记得刚用 fzf.vim 的时候，当时的全文检索没有预览功能，相比于 Sublime，几乎没法用。所以在参加完公司第二个 Hackathon 之后的那个周末提了人生的第一个 PR。在这期间和韩国小哥 junegunn 来来回回讨论了快一个月，虽然最后失败了，但是我很享受这期间的沟通过程，毕竟能把自己的想法表达清楚是一件难得的事，因为非实时的沟通一旦出现理解偏差，时间成本将会急速上升。&lt;/p&gt;
&lt;p&gt;在后来使用了 vim-delve 插件，并帮助作者修复了几个 bug，也算是回馈社区了~&lt;/p&gt;
&lt;h3 id=&#34;mit-6828&#34;&gt;MIT 6.828&lt;/h3&gt;
&lt;p&gt;在 16 年年初，非计算机专业的我选择了恶补操作系统：MIT 6.828。&lt;/p&gt;
&lt;p&gt;前前后后花了三个周末完成所有基本要求。课程设计者虽然尽可能地避免了琐碎的硬件操作，但是这三个周末还是非常的虐，毕竟 x86 架构有很多历史包袱，需要阅读 Intel x86 的开发文档。。。这三个周末完成的玩具内核让我重新认识了操作系统， 对我后续的工作帮助极大。&lt;/p&gt;
&lt;p&gt;不过你们会相信恶补的原因只是想知道 fork 怎么做到两个返回值 吗？&lt;/p&gt;
&lt;h3 id=&#34;gopher&#34;&gt;Gopher&lt;/h3&gt;
&lt;p&gt;语言切换算是这三年里最大的变化吧。为了看 Docker 的代码而接触 Golang，但是在16年年底的时候公司并没有项目让我去实践，当时的我只能自己啃代码，等到真正实践的时候也差不多到17年年中了。&lt;/p&gt;
&lt;p&gt;去年七八月我偶然发现 PingCAP 有赠马克杯的 TiDB 重构活动，很幸运的是提的三个 PR 都被接受了，这也是我第一次在 Github 上贡献代码。再后来就是 Pouch，因为对容器的喜爱吧，几乎大部分的空闲时间都参与到 Pouch 上来了。&lt;/p&gt;
&lt;p&gt;除了代码以外，还因为 Golang 开源项目结识了一些朋友，不得不说真是名副其实的 G**hub。&lt;/p&gt;
&lt;h3 id=&#34;我的-2018&#34;&gt;我的 2018&lt;/h3&gt;
&lt;p&gt;以上是过去三年来影响最大的三件事，不管怎样，希望还能像过去三年那样保持好奇的心吧。&lt;/p&gt;
</description>
        </item>
        
        <item>
          <title>Goroutine Scheduler Overview</title>
          <link>/post/2018-goroutine-scheduler-overview/</link>
          <pubDate>Tue, 06 Feb 2018 00:00:00 -0500</pubDate>
          <guid>https://fuweid.com/post/2018-goroutine-scheduler-overview/</guid>
          <description>&lt;p&gt;Goroutine 是 Golang 世界里的 &lt;code&gt;Lightweight Thread&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;Golang 在语言层面支持多线程，代码可以通过 &lt;code&gt;go&lt;/code&gt; 关键字来启动 Goroutine ，调用者不需要关心调用栈的大小，函数上下文等等信息就可以完成并发或者并行操作，加快了我们的开发速度。
分析 Goroutine 调度有利于了解和分析 go binary 的工作状况，所以接下来的内容将分析 &lt;code&gt;runtime&lt;/code&gt; 中关于 Goroutine 调度的逻辑。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;以下内容涉及到的代码是基于 &lt;a href=&#34;https://github.com/golang/go/tree/048c9cfaac&#34;&gt;go1.9rc2&lt;/a&gt; 版本。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;1-scheduler-structure&#34;&gt;1. Scheduler Structure&lt;/h3&gt;
&lt;p&gt;整个调度模型由 Goroutine/Processor/Machine 以及全局调度信息 sched 组成。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;                   Global Runnable Queue

                          runqueue
                ----------------------------
                 | G_10 | G_11 | G_12 | ...
                ----------------------------

                                        P_0 Local Runnable Queue
                +-----+      +-----+       ---------------
                | M_3 | ---- | P_0 |  &amp;lt;===  | G_8 | G_9 |
                +-----+      +-----+       ---------------
                                |
                             +-----+
                             | G_3 |  Running
                             +-----+

                                        P_1 Local Runnable Queue
                +-----+      +-----+       ---------------
                | M_4 | ---- | P_1 |  &amp;lt;===  | G_6 | G_7 |
                +-----+      +-----+       ---------------
                                |
                             +-----+
                             | G_5 |  Running
                             +-----+
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&#34;11-goroutine&#34;&gt;1.1 Goroutine&lt;/h4&gt;
&lt;p&gt;Goroutine 是 Golang 世界里的 &lt;code&gt;线程&lt;/code&gt; ，同样也是可调度的单元。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/runtime2.go
type g struct {
        ....
        m       *m
        sched gobuf
        goid   int64
        ....
}

type gobuf struct {
        sp   uintptr
        pc   uintptr
        ....
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;runtime&lt;/code&gt; 为 Goroutine 引入了类似 PID 的属性 &lt;code&gt;goid&lt;/code&gt; ，使得每一个 Goroutine 都有全局唯一的 &lt;code&gt;goid&lt;/code&gt; 标识。
不过官方并没有提供接口能 &lt;strong&gt;直接&lt;/strong&gt; 访问当前 Goroutine 的 &lt;code&gt;goid&lt;/code&gt;，在这种情况下我们可以通过 &lt;a href=&#34;https://github.com/0x04C2/gid/blob/master/gid_amd64.s#L5&#34;&gt;汇编&lt;/a&gt; 或者 &lt;a href=&#34;https://github.com/0x04C2/gid/blob/master/gid_test.go#L21&#34;&gt;取巧&lt;/a&gt; 的方式得到 &lt;code&gt;goid&lt;/code&gt;，有些第三方 package 会利用 &lt;code&gt;goid&lt;/code&gt; 做一些有趣的事情，比如 &lt;a href=&#34;https://github.com/jtolds/gls&#34;&gt;Goroutine local storage&lt;/a&gt; ，后面会介绍 &lt;code&gt;runtime&lt;/code&gt; 是如何生成唯一的 &lt;code&gt;goid&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;在调度过程中，&lt;code&gt;runtime&lt;/code&gt; 需要 Goroutine 释放当前的计算资源，为了保证下次能恢复现场，执行的上下文现场（指令地址 和 Stack Pointer 等）将会存储在 &lt;code&gt;gobuf&lt;/code&gt; 这个数据结构中。&lt;/p&gt;
&lt;p&gt;整体来说，Goroutine 仅代表任务的内容以及上下文，并不是具体的执行单元。&lt;/p&gt;
&lt;h4 id=&#34;12-machine&#34;&gt;1.2 Machine&lt;/h4&gt;
&lt;p&gt;Machine 是 OS Thread，它负责执行 Goroutine。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/runtime2.go

type m struct {
        ....
        g0      *g     // goroutine with scheduling stack
        curg    *g     // current running goroutine

        tls     [6]uintptr // thread-local storage (for x86 extern register)
        p       puintptr // attached p for executing go code (nil if not executing go code)
        ....
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;runtime&lt;/code&gt; 在做调度工作或者和当前 Goroutine 无关的任务时，Golang 会切换调用栈来进行相关的任务，就好像 Linux 的进程进入系统调用时会切换到内核态的调用栈一样，这么做也是为了避免影响到调度以及垃圾回收的扫描。&lt;/p&gt;
&lt;p&gt;Machine 一般会调用 &lt;a href=&#34;https://github.com/golang/go/blob/048c9cfaacb6fe7ac342b0acd8ca8322b6c49508/src/runtime/stubs.go#L39&#34;&gt;systemstack 函数&lt;/a&gt; 来切换调用栈。
从名字可以看出，Golang 对外部 go code 的调用栈称之为 &lt;code&gt;user stack&lt;/code&gt; ，而将运行核心 &lt;code&gt;runtime&lt;/code&gt; 部分代码的调用栈称之为 &lt;code&gt;system stack&lt;/code&gt;。
Machine 需要维护这两个调用栈的上下文，所以 &lt;code&gt;m&lt;/code&gt; 中 &lt;code&gt;g0&lt;/code&gt; 用来代表 &lt;code&gt;runtime&lt;/code&gt; 内部逻辑，而 &lt;code&gt;curg&lt;/code&gt; 则是我们平时写的代码，更多详情可以关注 &lt;a href=&#34;https://github.com/golang/go/blob/048c9cfaacb6fe7ac342b0acd8ca8322b6c49508/src/runtime/HACKING.md#user-stacks-and-system-stacks&#34;&gt;src/runtime/HACKING.md&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;因为调用栈可以来回地切换，Machine 需要知道当前运行的调用栈信息，所以 Golang 会利用 Thread Local Storage 或者指定寄存器来存储当前运行的 &lt;code&gt;g&lt;/code&gt;。
&lt;code&gt;settls&lt;/code&gt; 汇编代码会将 &lt;code&gt;g&lt;/code&gt; 的地址放到 &lt;code&gt;m.tls&lt;/code&gt; 中，这样 Machine 就可以通过 &lt;code&gt;getg&lt;/code&gt; 取出当前运行的 Goroutine。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不同平台 &lt;code&gt;settls&lt;/code&gt; 的行为有一定差别。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/sys_linux_amd64.s

// set tls base to DI
TEXT runtime·settls(SB),NOSPLIT,$32
#ifdef GOOS_android
        // Same as in sys_darwin_386.s:/ugliness, different constant.
        // DI currently holds m-&amp;gt;tls, which must be fs:0x1d0.
        // See cgo/gcc_android_amd64.c for the derivation of the constant.
        SUBQ    $0x1d0, DI  // In android, the tls base·
#else
        ADDQ    $8, DI  // ELF wants to use -8(FS)
#endif
        MOVQ    DI, SI
        MOVQ    $0x1002, DI     // ARCH_SET_FS
        MOVQ    $158, AX        // arch_prctl
        SYSCALL
        CMPQ    AX, $0xfffffffffffff001
        JLS     2(PC)
        MOVL    $0xf1, 0xf1  // crash
        RET

// src/runtime/stubs.go

// getg returns the pointer to the current g.
// The compiler rewrites calls to this function into instructions
// that fetch the g directly (from TLS or from the dedicated register).
func getg() *g

// src/runtime/go_tls.h 

#ifdef GOARCH_amd64
#define get_tls(r)      MOVQ TLS, r
#define g(r)    0(r)(TLS*1)
#endif
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;但是 Machine 想要执行一个 Goroutine，必须要绑定 Processor。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;runtime&lt;/code&gt; 内部有些函数执行时会直接绑定 Machine，并不需要 Processor，比如 &lt;a href=&#34;https://github.com/golang/go/blob/048c9cfaacb6fe7ac342b0acd8ca8322b6c49508/src/runtime/proc.go#L3810&#34;&gt;sysmon&lt;/a&gt; 。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&#34;13-processor&#34;&gt;1.3 Processor&lt;/h4&gt;
&lt;p&gt;Processor 可以理解成处理器，它会维护着本地 Goroutine 队列 &lt;code&gt;runq&lt;/code&gt; ，并在新的 Goroutine 入队列时分配唯一的 &lt;code&gt;goid&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type p struct {
        ...
        m           muintptr   // back-link to associated m (nil if idle)

        // Cache of goroutine ids, amortizes accesses to runtime·sched.goidgen.
        goidcache    uint64
        goidcacheend uint64

        // Queue of runnable goroutines. Accessed without lock.
        runqhead uint32
        runqtail uint32
        runq     [256]guintptr
        ...
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Processor 的数目代表着 &lt;code&gt;runtime&lt;/code&gt; 能同时处理 Goroutine 的数目，&lt;code&gt;GOMAXPROCS&lt;/code&gt; 环境变量是用来指定 Processor 的数目，默认状态会是 CPU 的个数。&lt;/p&gt;
&lt;p&gt;也正是因为 Processor 的存在，&lt;code&gt;runtime&lt;/code&gt; 并不需要做一个集中式的 Goroutine 调度，每一个 Machine 都会在 Processor 本地队列、Global Runnable Queue 或者其他 Processor 队列中找 Goroutine 执行，减少全局锁对性能的影响，后面会对此展开说明。&lt;/p&gt;
&lt;h4 id=&#34;14-全局调度信息-sched&#34;&gt;1.4 全局调度信息 sched&lt;/h4&gt;
&lt;p&gt;全局调度信息 &lt;code&gt;sched&lt;/code&gt; 会记录当前 Global Runnable Queue、当前空闲的 Machine 和空闲 Processor 的数目等等。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;后面说明这 &lt;code&gt;goidgen&lt;/code&gt; 和 &lt;code&gt;nmspinning&lt;/code&gt; 两个字段的作用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/runtime2.go

var (
        ...
        sched      schedt
        ...
)

type schedt struct {
        // accessed atomically. keep at top to ensure alignment on 32-bit systems.
        goidgen  uint64

        lock mutex

        midle        muintptr // idle m&#39;s waiting for work
        nmidle       int32    // number of idle m&#39;s waiting for work
        maxmcount    int32    // maximum number of m&#39;s allowed (or die)

        pidle      puintptr // idle p&#39;s
        npidle     uint32
        nmspinning uint32 // See &amp;quot;Worker thread parking/unparking&amp;quot; comment in proc.go.

        // Global runnable queue.
        runqhead guintptr
        runqtail guintptr
        runqsize int32
        ....
}
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;2-create-a-goroutine&#34;&gt;2. Create a Goroutine&lt;/h3&gt;
&lt;p&gt;下面那段代码非常简单，在 &lt;code&gt;main&lt;/code&gt; 函数中产生 Goroutine 去执行 &lt;code&gt;do()&lt;/code&gt; 这个函数。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  main cat -n main.go
     1  package main
     2
     3  func do() {
     4          // nothing
     5  }
     6
     7  func main() {
     8          go do()
     9  }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;我们编译上述代码并反汇编看看 &lt;code&gt;go&lt;/code&gt; 关键字都做了什么。
可以看到源代码的第 8 行 &lt;code&gt;go do()&lt;/code&gt; 编译完之后会变成 &lt;code&gt;runtime.newproc&lt;/code&gt; 方法，下面我们来看看 &lt;code&gt;runtime.newproc&lt;/code&gt; 都做了些什么。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  main uname -m -s
Linux x86_64
➜  main go build
➜  main go tool objdump -s &amp;quot;main.main&amp;quot; main
TEXT main.main(SB) /root/workspace/main/main.go
  main.go:7             0x450a60                64488b0c25f8ffffff      MOVQ FS:0xfffffff8, CX
  main.go:7             0x450a69                483b6110                CMPQ 0x10(CX), SP
  main.go:7             0x450a6d                7630                    JBE 0x450a9f
  main.go:7             0x450a6f                4883ec18                SUBQ $0x18, SP
  main.go:7             0x450a73                48896c2410              MOVQ BP, 0x10(SP)
  main.go:7             0x450a78                488d6c2410              LEAQ 0x10(SP), BP
  main.go:8             0x450a7d                c7042400000000          MOVL $0x0, 0(SP)
  main.go:8             0x450a84                488d05e5190200          LEAQ 0x219e5(IP), AX
  main.go:8             0x450a8b                4889442408              MOVQ AX, 0x8(SP)
  main.go:8             0x450a90                e88bb4fdff              CALL runtime.newproc(SB)  &amp;lt;==== I&#39;m here.
  main.go:9             0x450a95                488b6c2410              MOVQ 0x10(SP), BP
  main.go:9             0x450a9a                4883c418                ADDQ $0x18, SP
  main.go:9             0x450a9e                c3                      RET
  main.go:7             0x450a9f                e88c7dffff              CALL runtime.morestack_noctxt(SB)
  main.go:7             0x450aa4                ebba                    JMP main.main(SB)
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&#34;21-创建-do-的执行上下文&#34;&gt;2.1 创建 do() 的执行上下文&lt;/h4&gt;
&lt;p&gt;平时写代码的时候会发现，Goroutine 执行完毕之后便消失了。那么 &lt;code&gt;do()&lt;/code&gt; 这个函数执行完毕之后返回到哪了呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  main go tool objdump -s &amp;quot;main.do&amp;quot; main
TEXT main.do(SB) /root/workspace/main/main.go
  main.go:5             0x450a50                c3                      RET
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;根据 Intel 64 IA 32 开发指南上 &lt;code&gt;Chaptor 6.3 CALLING PROCEDURES USING CALL AND RET&lt;/code&gt; 的说明，&lt;code&gt;RET&lt;/code&gt; 会将栈顶的指令地址弹出到 &lt;code&gt;IP&lt;/code&gt; 寄存器上，然后继续执行 &lt;code&gt;IP&lt;/code&gt; 寄存器上的指令。
为了保证 Machine 执行完 Goroutine 之后，能够正常地完成一些清理工作，我们需要在构建 Goroutine 的执行上下文时指定 &lt;code&gt;RET&lt;/code&gt; 的具体地址。&lt;/p&gt;
&lt;p&gt;下面的代码段会将准备好的调用栈内存保存到 &lt;code&gt;newg.sched&lt;/code&gt; 中，其中 &lt;code&gt;gostartcallfn&lt;/code&gt; 函数会把 &lt;code&gt;do()&lt;/code&gt; 函数添加到 &lt;code&gt;newg.sched.pc&lt;/code&gt; ，并将 &lt;code&gt;goexit&lt;/code&gt; 函数地址推入栈顶 &lt;code&gt;newg.sched.sp&lt;/code&gt;。
所以 Goroutine 执行完毕之后，Machine 会跳到 &lt;code&gt;goexit&lt;/code&gt; 函数中做一些清理工作。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/proc.go @ func newproc1

if narg &amp;gt; 0 {
        memmove(unsafe.Pointer(spArg), unsafe.Pointer(argp), uintptr(narg)
        ....
}

newg.sched.sp = sp
newg.sched.pc = funcPC(goexit) + sys.PCQuantum // +PCQuantum so that previous instruction is in same function
newg.sched.g = guintptr(unsafe.Pointer(newg))
gostartcallfn(&amp;amp;newg.sched, fn)
newg.gopc = callerpc
newg.startpc = fn.fn
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;想了解 Intel 指令的更多细节，请查看 &lt;a href=&#34;https://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-vol-1-manual.html&#34;&gt;Intel® 64 and IA-32 Architectures Developer&amp;rsquo;s Manual: Vol. 1&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&#34;22-全局唯一的-goid&#34;&gt;2.2 全局唯一的 goid&lt;/h4&gt;
&lt;p&gt;除了创建执行上下文以外，&lt;code&gt;runtime&lt;/code&gt; 还会为 Goroutine 指定一个全局唯一的 id。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/proc.go

const (
        // Number of goroutine ids to grab from sched.goidgen to local per-P cache at once.
        // 16 seems to provide enough amortization, but other than that it&#39;s mostly arbitrary number.
        _GoidCacheBatch = 16
)

// src/runtime/proc.go @ func newproc1

if _p_.goidcache == _p_.goidcacheend {
        // Sched.goidgen is the last allocated id,
        // this batch must be [sched.goidgen+1, sched.goidgen+GoidCacheBatch].
        // At startup sched.goidgen=0, so main goroutine receives goid=1.
        _p_.goidcache = atomic.Xadd64(&amp;amp;sched.goidgen, _GoidCacheBatch)
        _p_.goidcache -= _GoidCacheBatch - 1
        _p_.goidcacheend = _p_.goidcache + _GoidCacheBatch
}
newg.goid = int64(_p_.goidcache)
_p_.goidcache++
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;全局调度信息 &lt;code&gt;sched.goidgen&lt;/code&gt; 是专门用来做发号器，Processor 每次可以从发号器那拿走 &lt;code&gt;_GoidCacheBatch&lt;/code&gt; 个号，然后内部采用自增的方式来发号，这样就保证了每一个 Goroutine 都可以拥有全局唯一的 &lt;code&gt;goid&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;从全局调度信息那里取号的时候用原子操作来保证并发操作的正确性，而内部发号时却采用非原子操作，这是因为一个 Processor 只能被一个 Machine 绑定上，所以这里 &lt;code&gt;_p_.goidcache&lt;/code&gt; 自增不需要要原子操作也能保证它的正确性。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&#34;23-local-vs-global-runnable-queue&#34;&gt;2.3 Local vs Global Runnable Queue&lt;/h4&gt;
&lt;p&gt;当 Goroutine 创建完毕之后，它是放在当前 Processor 的 Local Runnable Queue 还是全局队列里？&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/golang/go/blob/048c9cfaacb6fe7ac342b0acd8ca8322b6c49508/src/runtime/proc.go#L4289&#34;&gt;runqput&lt;/a&gt; 这个函数会尝试把 &lt;code&gt;newg&lt;/code&gt; 放到本地队列上，如果本地队列满了，它会将本地队列的前半部分和 &lt;code&gt;newg&lt;/code&gt; 迁移到全局队列中。剩下的事情就等待 Machine 自己去拿任务了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/proc.go @ func newproc1

runqput(_p_, newg, true)
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&#34;24-小结&#34;&gt;2.4 小结&lt;/h4&gt;
&lt;p&gt;看到这里，一般都会有以下几个疑问：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;main 函数是不是也是一个 Goroutine ？&lt;/li&gt;
&lt;li&gt;Machine 怎么去取 Goroutine 来执行?&lt;/li&gt;
&lt;li&gt;&lt;code&gt;goexit&lt;/code&gt; 做完清理工作之后就让 Machine 退出吗？还是继续使用这个 Machine?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;那么就继续往下读吧~&lt;/p&gt;
&lt;h3 id=&#34;3-main-is-a-goroutine&#34;&gt;3. main is a Goroutine&lt;/h3&gt;
&lt;p&gt;我们写的 &lt;code&gt;main&lt;/code&gt; 函数在程序启动时，同样会以 Goroutine 身份被 Machine 执行，下面会查看 go binary 启动时都做了什么。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  main uname -m -s
Linux x86_64
➜  main go build --gcflags &amp;quot;-N -l&amp;quot;
➜  main gdb main
(gdb) info file
Symbols from &amp;quot;/root/workspace/main/main&amp;quot;.
Local exec file:
        `/root/workspace/main/main&#39;, file type elf64-x86-64.
        Entry point: 0x44bb80
        0x0000000000401000 - 0x0000000000450b13 is .text
        0x0000000000451000 - 0x000000000047a6bc is .rodata
        0x000000000047a7e0 - 0x000000000047afd4 is .typelink
        0x000000000047afd8 - 0x000000000047afe0 is .itablink
        0x000000000047afe0 - 0x000000000047afe0 is .gosymtab
        0x000000000047afe0 - 0x00000000004a96c8 is .gopclntab
        0x00000000004aa000 - 0x00000000004aaa38 is .noptrdata
        0x00000000004aaa40 - 0x00000000004ab5b8 is .data
        0x00000000004ab5c0 - 0x00000000004c97e8 is .bss
        0x00000000004c9800 - 0x00000000004cbe18 is .noptrbss
        0x0000000000400fc8 - 0x0000000000401000 is .note.go.buildid
(gdb) info symbol 0x44bb80
_rt0_amd64_linux in section .text
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;入口函数是 &lt;code&gt;_rt0_amd64_linux&lt;/code&gt;，需要说明的是，不同平台的入口函数名称会有所不同，全局搜索该方法之后，发现该方法会调用 &lt;code&gt;runtime.rt0_go&lt;/code&gt; 汇编。&lt;/p&gt;
&lt;p&gt;省去了大量和硬件相关的细节后，&lt;code&gt;rt0_go&lt;/code&gt; 做了大量的初始化工作，&lt;code&gt;runtime.args&lt;/code&gt; 读取命令行参数、&lt;code&gt;runtime.osinit&lt;/code&gt; 读取 CPU 数目，&lt;code&gt;runtime.schedinit&lt;/code&gt; 初始化 Processor 数目，最大的 Machine 数目等等。&lt;/p&gt;
&lt;p&gt;除此之外，我们还看到了两个奇怪的 &lt;code&gt;g0&lt;/code&gt; 和 &lt;code&gt;m0&lt;/code&gt; 变量。&lt;code&gt;m0&lt;/code&gt; Machine 代表着当前初始化线程，而 &lt;code&gt;g0&lt;/code&gt; 代表着初始化线程 &lt;code&gt;m0&lt;/code&gt; 的 &lt;code&gt;system stack&lt;/code&gt;，似乎还缺一个 &lt;code&gt;p0&lt;/code&gt; ？
实际上所有的 Processor 都会放到 &lt;a href=&#34;https://github.com/golang/go/blob/048c9cfaacb6fe7ac342b0acd8ca8322b6c49508/src/runtime/runtime2.go#L722&#34;&gt;allp&lt;/a&gt; 里。&lt;code&gt;runtime.schedinit&lt;/code&gt; 会在调用 &lt;a href=&#34;https://github.com/golang/go/blob/048c9cfaacb6fe7ac342b0acd8ca8322b6c49508/src/runtime/proc.go#L3507&#34;&gt;procresize&lt;/a&gt; 时为 &lt;code&gt;m0&lt;/code&gt; 分配上 &lt;code&gt;allp[0]&lt;/code&gt; 。所以到目前为止，初始化线程运行模式是符合上文提到的 G/P/M 模型的。&lt;/p&gt;
&lt;p&gt;大量的初始化工作做完之后，会调用 &lt;code&gt;runtime.newproc&lt;/code&gt; 为 &lt;code&gt;mainPC&lt;/code&gt; 方法生成一个 Goroutine。
虽然 &lt;a href=&#34;https://github.com/golang/go/blob/048c9cfaacb6fe7ac342b0acd8ca8322b6c49508/src/runtime/proc.go#L106&#34;&gt;mainPC&lt;/a&gt; 并不是我们平时写的那个 main 函数，但是它会调用我们写的 main 函数，所以 main 函数是会以 Goroutine 的形式运行。&lt;/p&gt;
&lt;p&gt;有了 Goroutine 之后，那么 Machine 怎么执行呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/asm_amd64.s

TEXT runtime·rt0_go(SB),NOSPLIT,$0
         ...

// set the per-goroutine and per-mach &amp;quot;registers&amp;quot;
        // save m-&amp;gt;g0 = g0
        MOVQ    CX, m_g0(AX)
        // save m0 to g0-&amp;gt;m
        MOVQ    AX, g_m(CX)

        ...
        CALL    runtime·args(SB)
        CALL    runtime·osinit(SB)
        CALL    runtime·schedinit(SB)

        // create a new goroutine to start program
        MOVQ    $runtime·mainPC(SB), AX        // entry
        PUSHQ   AX
        PUSHQ   $0      // arg size
        CALL    runtime·newproc(SB)

        ...
        // start this M
        CALL    runtime·mstart(SB)  &amp;lt;=== I&#39;m here!

        MOVL    $0xf1, 0xf1  // crash
        RET
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;4-machine-----work-stealing&#34;&gt;4. Machine &amp;mdash; Work Stealing&lt;/h3&gt;
&lt;p&gt;在上一节查看 &lt;code&gt;rt0_go&lt;/code&gt; 汇编代码的时候，发现最后一段代码 &lt;code&gt;CALL runtime.mstart(SB)&lt;/code&gt; 是用来启动 Machine。&lt;/p&gt;
&lt;p&gt;因为在 Golang 的世界里，任务的执行需要 Machine 本身自己去获取。
每个 Machine 运行前都会绑定一个 Processor，Machine 会逐步消耗完当前 Processor 队列。
为了防止某些 Machine 没有事情可做，某些 Machine 忙死，所以 &lt;code&gt;runtime&lt;/code&gt; 会做了两件事：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;当前 Processor 队列已满，Machine 会将本地队列的部分 Goroutine 迁移到 Global Runnable Queue 中;&lt;/li&gt;
&lt;li&gt;Machine 绑定的 Processor 没有可执行的 Goroutine 时，它会去 Global Runnable Queue、Net Network 和其他 Processor 的队列中抢任务。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这种调度模式叫做 &lt;a href=&#34;https://en.wikipedia.org/wiki/Work_stealing&#34;&gt;Work Stealing&lt;/a&gt;。&lt;/p&gt;
&lt;h4 id=&#34;41-如何执行-goroutine&#34;&gt;4.1 如何执行 Goroutine？&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/proc.go

func mstart() {
        ...
        } else if _g_.m != &amp;amp;m0 {
                acquirep(_g_.m.nextp.ptr()) // 绑定 Processor
                _g_.m.nextp = 0
        }
        schedule()
}

mstart() =&amp;gt; schedule() =&amp;gt; execute() =&amp;gt; xxx() =&amp;gt; goexit()
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;runtime.mstart&lt;/code&gt; 函数会调用 &lt;a href=&#34;https://github.com/golang/go/blob/048c9cfaacb6fe7ac342b0acd8ca8322b6c49508/src/runtime/proc.go#L2195&#34;&gt;schedule&lt;/a&gt; 函数去寻找可执行的 Goroutine，查找顺序大致是:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Local Runnable Queue&lt;/li&gt;
&lt;li&gt;Global Runnable Queue&lt;/li&gt;
&lt;li&gt;Net Network&lt;/li&gt;
&lt;li&gt;Other Processor&amp;rsquo;s Runnable Queue&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;需找可执行的 Goroutine 的逻辑都在 &lt;a href=&#34;https://github.com/golang/go/blob/048c9cfaacb6fe7ac342b0acd8ca8322b6c49508/src/runtime/proc.go#L1919&#34;&gt;findrunnable&lt;/a&gt; 里。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;找到任何一个可执行的 Goroutine 之后，会调用 &lt;a href=&#34;https://github.com/golang/go/blob/048c9cfaacb6fe7ac342b0acd8ca8322b6c49508/src/runtime/proc.go#L1886&#34;&gt;execute&lt;/a&gt; 去切换到 &lt;code&gt;g.sched&lt;/code&gt; 相应的调用栈，这样 Machine 就会执行我们代码里创建 Goroutine。&lt;/p&gt;
&lt;p&gt;执行完毕之后会 &lt;code&gt;RET&lt;/code&gt; 到 &lt;code&gt;goexit&lt;/code&gt;, &lt;code&gt;goexit&lt;/code&gt; 会调用 &lt;a href=&#34;https://github.com/golang/go/blob/048c9cfaacb6fe7ac342b0acd8ca8322b6c49508/src/runtime/proc.go#L2366&#34;&gt;goexit0&lt;/a&gt; 进行清理工作，
然后再进入 &lt;code&gt;schedule&lt;/code&gt; 模式。如果这个时候释放了当前 Machine，那么每次执行 Goroutine 都要创建新的 OS-Thread，这样的代价略大。
所以 Machine 会不断地拿任务执行，直到没有任务。
当 Machine 没有可执行的任务时，它会在 &lt;code&gt;findrunnable&lt;/code&gt; 中调用 &lt;a href=&#34;https://github.com/golang/go/blob/048c9cfaacb6fe7ac342b0acd8ca8322b6c49508/src/runtime/proc.go#L1653&#34;&gt;stopm&lt;/a&gt; 进入休眠状态。&lt;/p&gt;
&lt;p&gt;那么谁来激活这些休眠状态的 Machine ？&lt;/p&gt;
&lt;h4 id=&#34;42-wake-up&#34;&gt;4.2 Wake Up&lt;/h4&gt;
&lt;p&gt;常见的激活时机就是新的 Goroutine 创建出来的时候。我们回头看看 &lt;code&gt;runtime.newproc&lt;/code&gt; 返回前都做了什么。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/proc.go @ func newproc1

if atomic.Load(&amp;amp;sched.npidle) != 0 &amp;amp;&amp;amp; atomic.Load(&amp;amp;sched.nmspinning) == 0 &amp;amp;&amp;amp; runtimeInitTime != 0 {
        wakep()
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当 Machine 找不到可执行的 Goroutine 时，但是还在努力地寻找可执行的 Goroutine，这段时间它属于 &lt;code&gt;spinning&lt;/code&gt; 的状态。
它实在是找不到了，它才回释放当前 Processor 进入休眠状态。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;atomic.Load(&amp;amp;sched.npidle) != 0 &amp;amp;&amp;amp; atomic.Load(&amp;amp;sched.nmspinning) == 0&lt;/code&gt; 指的是有空闲的 Processor 而没有 &lt;code&gt;spinning&lt;/code&gt; 状态的 Machine。
这个时候可能是有休眠状态的 Machine，可能是程序刚启动的时候并没有足够的 Machine。当遇到这种情况，当前 Machine 会执行 &lt;code&gt;wakep&lt;/code&gt;，让程序能快速地消化 Goroutine。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在初始化过程中，为 &lt;code&gt;runtime.main&lt;/code&gt; 函数创建的第一个 Goroutine 并不需要调用 &lt;code&gt;wakep&lt;/code&gt;，所以在该判断条件里 &lt;code&gt;runtimeInitTime != 0&lt;/code&gt; 会失败。
&lt;a href=&#34;https://github.com/golang/go/blob/048c9cfaacb6fe7ac342b0acd8ca8322b6c49508/src/runtime/proc.go#L123&#34;&gt;runtimeInitTime&lt;/a&gt; 会在 &lt;code&gt;runtime.main&lt;/code&gt; 函数中被赋值，表明正式开始执行任务啦。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;wakep&lt;/code&gt; 首先会查看有没有空闲的 Machine，如果找到而且状态合理，那么就会激活它。如果没有找到，那么会创建一个新的 &lt;code&gt;spinning&lt;/code&gt; Machine。&lt;/p&gt;
&lt;p&gt;在 Golang 世界里，新创建的 Machine 可以认为它属于 &lt;code&gt;spinning&lt;/code&gt;，因为创建 OS-Thread 有一定代价，一旦创建出来了它就要去干活。
除此之外，Golang 创建新的线程并不会直接交付任务给它，而是让它调用 &lt;code&gt;runtime.mstart&lt;/code&gt; 方法自己去找活做。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/proc.go

func wakep() {
        // be conservative about spinning threads
        if !atomic.Cas(&amp;amp;sched.nmspinning, 0, 1) {
                return
        }
        startm(nil, true)
}

func mspinning() {
        // startm&#39;s caller incremented nmspinning. Set the new M&#39;s spinning.
        getg().m.spinning = true
}

func startm(_p_ *p, spinning bool) {
        lock(&amp;amp;sched.lock)
        if _p_ == nil {
                _p_ = pidleget()
                if _p_ == nil {
                        unlock(&amp;amp;sched.lock)
                        if spinning {
                                // The caller incremented nmspinning, but there are no idle Ps,
                                // so it&#39;s okay to just undo the increment and give up.
                                if int32(atomic.Xadd(&amp;amp;sched.nmspinning, -1)) &amp;lt; 0 {
                                        throw(&amp;quot;startm: negative nmspinning&amp;quot;)
                                }
                        }
                        return
                }
        }
        mp := mget()
        unlock(&amp;amp;sched.lock)
        if mp == nil {
                var fn func()
                if spinning {
                        // The caller incremented nmspinning, so set m.spinning in the new M.
                        fn = mspinning
                }
                newm(fn, _p_)
                return
        }
        ...
        mp.spinning = spinning
        mp.nextp.set(_p_)
        notewakeup(&amp;amp;mp.park)
}
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;在 Linux 平台上，&lt;a href=&#34;https://github.com/golang/go/blob/048c9cfaacb6fe7ac342b0acd8ca8322b6c49508/src/runtime/proc.go#L1626&#34;&gt;newm&lt;/a&gt; 会调用 &lt;a href=&#34;https://github.com/golang/go/blob/048c9cfaacb6fe7ac342b0acd8ca8322b6c49508/src/runtime/os_linux.go#L144&#34;&gt;newosproc&lt;/a&gt; 来产生新的 OS-Thread。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;5-preemptive&#34;&gt;5. Preemptive&lt;/h3&gt;
&lt;p&gt;Machine 会在全局范围内查找 Goroutine 来执行，似乎还缺少角色去通知 Machine 释放当前 Goroutine，总不能执行完毕再切换吧。
我们知道操作系统会根据时钟周期性地触发系统中断来进行调度，Golang 是用户态的线程调度，那它怎么通知 Machine 呢？&lt;/p&gt;
&lt;p&gt;回忆上文, 提到了有些 Machine 执行任务前它并不需要绑定 Processor，它们都做什么任务呢？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/proc.go

func main() {
        ...
        systemstack(func() {
                newm(sysmon, nil)
        })
        ...
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在 &lt;code&gt;runtime.main&lt;/code&gt; 函数中会启动新的 OS-Thread 去执行 &lt;a href=&#34;https://github.com/golang/go/blob/048c9cfaacb6fe7ac342b0acd8ca8322b6c49508/src/runtime/proc.go#L3813&#34;&gt;sysmon&lt;/a&gt; 函数。
该函数会以一个上帝视角去查看 Goroutine/Machine/Processor 的运行情况，并会调用 &lt;a href=&#34;https://github.com/golang/go/blob/048c9cfaacb6fe7ac342b0acd8ca8322b6c49508/src/runtime/proc.go#L3940&#34;&gt;retake&lt;/a&gt; 去让 Machine 释放正在运行的 Goroutine。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/proc.go

// forcePreemptNS is the time slice given to a G before it is
// preempted.
const forcePreemptNS = 10 * 1000 * 1000 // 10ms

func retake(now int64) uint32 {
        for i := int32(0); i &amp;lt; gomaxprocs; i++ {
                _p_ := allp[i]
                if _p_ == nil {
                        continue
                }
                pd := &amp;amp;_p_.sysmontick
                s := _p_.status

                ...
                } else if s == _Prunning {
                        // Preempt G if it&#39;s running for too long.
                        t := int64(_p_.schedtick)
                        if int64(pd.schedtick) != t {
                                pd.schedtick = uint32(t)
                                pd.schedwhen = now
                                continue
                        }
                        if pd.schedwhen+forcePreemptNS &amp;gt; now {
                                continue
                        }
                        preemptone(_p_)
                }
        }
        ...
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Processor 在 Machine 上执行时间超过 10ms，Machine 会给调用 &lt;a href=&#34;https://github.com/golang/go/blob/048c9cfaacb6fe7ac342b0acd8ca8322b6c49508/src/runtime/proc.go#L4024&#34;&gt;preemptone&lt;/a&gt;
给当前 Goroutine 加上标记：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/proc.go

func preemptone(_p_ *p) bool {
        ...
        gp.preempt = true

        // Every call in a go routine checks for stack overflow by
        // comparing the current stack pointer to gp-&amp;gt;stackguard0.
        // Setting gp-&amp;gt;stackguard0 to StackPreempt folds
        // preemption into the normal stack overflow check.
        gp.stackguard0 = stackPreempt
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;可以看到它并不是直接发信号给 Machine 让它立即释放，而是让 Goroutine 自己释放，那它什么时候会释放？&lt;/p&gt;
&lt;p&gt;Golang 创建新的 Goroutine 时，都会分配有限的调用栈空间，按需进行拓展或者收缩。
所以在执行下一个函数时，它会检查调用栈是否溢出。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  main go tool objdump -s &amp;quot;main.main&amp;quot; main
TEXT main.main(SB) /root/workspace/main/main.go
  main.go:7             0x450a60                64488b0c25f8ffffff      MOVQ FS:0xfffffff8, CX
  main.go:7             0x450a69                483b6110                CMPQ 0x10(CX), SP
  main.go:7             0x450a6d                7630                    JBE 0x450a9f    &amp;lt;= I&#39;m here!!
  main.go:7             0x450a6f                4883ec18                SUBQ $0x18, SP
  main.go:7             0x450a73                48896c2410              MOVQ BP, 0x10(SP)
  main.go:7             0x450a78                488d6c2410              LEAQ 0x10(SP), BP
  main.go:8             0x450a7d                c7042400000000          MOVL $0x0, 0(SP)
  main.go:8             0x450a84                488d05e5190200          LEAQ 0x219e5(IP), AX
  main.go:8             0x450a8b                4889442408              MOVQ AX, 0x8(SP)
  main.go:8             0x450a90                e88bb4fdff              CALL runtime.newproc(SB)
  main.go:9             0x450a95                488b6c2410              MOVQ 0x10(SP), BP
  main.go:9             0x450a9a                4883c418                ADDQ $0x18, SP
  main.go:9             0x450a9e                c3                      RET
  main.go:7             0x450a9f                e88c7dffff              CALL runtime.morestack_noctxt(SB)
  main.go:7             0x450aa4                ebba                    JMP main.main(SB)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;gp.stackguard0 = stackPreempt&lt;/code&gt; 设置会让检查失败，进入 &lt;code&gt;runtime.morestack_noctxt&lt;/code&gt; 函数。
它发现是因为 &lt;code&gt;runtime.retake&lt;/code&gt; 造成，Machine 会保存当前 Goroutine 的执行上下文，重新进入 &lt;code&gt;runtime.schedule&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;你可能会问，如果这个 Goroutine 里面没有函数调用怎么办？请查看这个 &lt;a href=&#34;https://github.com/golang/go/issues/11462&#34;&gt;issues/11462&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;一般情况下，这样的函数不是死循环，就是很快就退出了，实际开发中这种的类型函数不会太多。&lt;/p&gt;
&lt;h3 id=&#34;6-关于线程数目&#34;&gt;6. 关于线程数目&lt;/h3&gt;
&lt;p&gt;Processor 的数目决定 go binary 能同时处理多少 Goroutine 的能力，感觉 Machine 的数目应该不会太多。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  scheduler cat -n main.go
     1  package main
     2
     3  import (
     4          &amp;quot;log&amp;quot;
     5          &amp;quot;net/http&amp;quot;
     6          &amp;quot;syscall&amp;quot;
     7  )
     8
     9  func main() {
    10          http.HandleFunc(&amp;quot;/sleep&amp;quot;, func(w http.ResponseWriter, r *http.Request) {
    11                  tspec := syscall.NsecToTimespec(1000 * 1000 * 1000)
    12                  if err := syscall.Nanosleep(&amp;amp;tspec, &amp;amp;tspec); err != nil {
    13                          panic(err)
    14                  }
    15          })
    16
    17          http.HandleFunc(&amp;quot;/echo&amp;quot;, func(w http.ResponseWriter, r *http.Request) {
    18                  w.Write([]byte(&amp;quot;hello&amp;quot;))
    19          })
    20
    21          log.Fatal(http.ListenAndServe(&amp;quot;:8080&amp;quot;, nil))
    22  }
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Golang 提供了 &lt;code&gt;GODEBUG&lt;/code&gt; 环境变量来观察当前 Goroutine/Processor/Machine 的状态。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  scheduler go build
➜  scheduler GODEBUG=schedtrace=2000 ./scheduler
SCHED 0ms: gomaxprocs=4 idleprocs=1 threads=6 spinningthreads=1 idlethreads=0 runqueue=0 [0 0 0 0]
SCHED 2008ms: gomaxprocs=4 idleprocs=4 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
SCHED 4016ms: gomaxprocs=4 idleprocs=4 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;GODEBUG=schedtrace=2000&lt;/code&gt; 会开启 &lt;code&gt;schedtrace&lt;/code&gt; 模式，它会让 &lt;code&gt;sysmon&lt;/code&gt; 中调用 &lt;a href=&#34;https://github.com/golang/go/blob/048c9cfaacb6fe7ac342b0acd8ca8322b6c49508/src/runtime/proc.go#L4046&#34;&gt;schedtrace&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/proc.go

func schedtrace(detailed bool) {
        ...
        print(&amp;quot;SCHED &amp;quot;, (now-starttime)/1e6, &amp;quot;ms: gomaxprocs=&amp;quot;, gomaxprocs, &amp;quot; idleprocs=&amp;quot;, sched.npidle, &amp;quot; threads=&amp;quot;, sched.mcount, &amp;quot; spinningthreads=&amp;quot;, sched.nmspinning, &amp;quot; idlethreads=&amp;quot;, sched.nmidle, &amp;quot; runqueue=&amp;quot;, sched.runqsize)
        ...
}

gomaxprocs:      当前 Processor 的数目
idleprocs:       空闲 Processor 的数目
threads:         共创建了多少个 Machine
spinningthreads: spinning 状态的 Machine
nmidle:          休眠状态的 Machine 数目
runqueue:        Global Runnable Queue 队列长度
[x, y, z..]:     每个 Processor 的 Local Runnable Queue 队列长度
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;下面我们会通过 &lt;a href=&#34;https://github.com/wg/wrk&#34;&gt;wrk&lt;/a&gt; 对 sleep 和 echo 这两个 endpoint 进行压力测试，并关注 Machine 的数目变化。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  scheduler GODEBUG=schedtrace=2000 ./scheduler &amp;gt; echo_result 2&amp;gt;&amp;amp;1 &amp;amp;
[1] 6015
➜  scheduler wrk -t12 -c400 -d30s http://localhost:8080/echo
Running 30s test @ http://localhost:8080/echo
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency    51.15ms  104.96ms   1.31s    89.35%
    Req/Sec     4.97k     4.48k   20.53k    74.84%
  1780311 requests in 30.08s, 205.44MB read
Requests/sec:  59178.76
Transfer/sec:      6.83MB
➜  scheduler head -n 20 echo_result
SCHED 0ms: gomaxprocs=4 idleprocs=1 threads=6 spinningthreads=2 idlethreads=0 runqueue=0 [0 0 0 0]
SCHED 2000ms: gomaxprocs=4 idleprocs=4 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
SCHED 4005ms: gomaxprocs=4 idleprocs=4 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
SCHED 6008ms: gomaxprocs=4 idleprocs=4 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
SCHED 8014ms: gomaxprocs=4 idleprocs=0 threads=12 spinningthreads=0 idlethreads=6 runqueue=195 [20 53 6 32]
SCHED 10018ms: gomaxprocs=4 idleprocs=0 threads=12 spinningthreads=0 idlethreads=6 runqueue=272 [65 16 5 37]
SCHED 12021ms: gomaxprocs=4 idleprocs=0 threads=13 spinningthreads=0 idlethreads=7 runqueue=218 [97 5 52 7]
SCHED 14028ms: gomaxprocs=4 idleprocs=0 threads=13 spinningthreads=0 idlethreads=7 runqueue=41 [2 1 25 3]
SCHED 16029ms: gomaxprocs=4 idleprocs=0 threads=13 spinningthreads=0 idlethreads=7 runqueue=178 [10 31 45 38]
SCHED 18033ms: gomaxprocs=4 idleprocs=0 threads=13 spinningthreads=0 idlethreads=7 runqueue=144 [15 92 47 0]
SCHED 20034ms: gomaxprocs=4 idleprocs=0 threads=13 spinningthreads=0 idlethreads=7 runqueue=195 [1 7 4 41]
SCHED 22035ms: gomaxprocs=4 idleprocs=0 threads=13 spinningthreads=0 idlethreads=7 runqueue=159 [88 14 41 5]
SCHED 24038ms: gomaxprocs=4 idleprocs=0 threads=13 spinningthreads=0 idlethreads=7 runqueue=231 [47 19 53 41]
SCHED 26046ms: gomaxprocs=4 idleprocs=0 threads=13 spinningthreads=0 idlethreads=7 runqueue=6 [1 0 1 10]
SCHED 28049ms: gomaxprocs=4 idleprocs=0 threads=13 spinningthreads=0 idlethreads=7 runqueue=133 [61 13 97 53]
SCHED 30049ms: gomaxprocs=4 idleprocs=0 threads=13 spinningthreads=0 idlethreads=7 runqueue=220 [13 49 29 28]
SCHED 32058ms: gomaxprocs=4 idleprocs=0 threads=13 spinningthreads=0 idlethreads=7 runqueue=138 [40 93 63 50]
SCHED 34062ms: gomaxprocs=4 idleprocs=0 threads=13 spinningthreads=0 idlethreads=7 runqueue=266 [51 9 38 31]
SCHED 36068ms: gomaxprocs=4 idleprocs=0 threads=13 spinningthreads=0 idlethreads=7 runqueue=189 [1 3 46 14]
SCHED 38084ms: gomaxprocs=4 idleprocs=4 threads=13 spinningthreads=0 idlethreads=10 runqueue=0 [0 0 0 0]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;测试 &lt;code&gt;localhost:8080/echo&lt;/code&gt; 30s 之后，发现当前线程数目为 13。接下来再看看 &lt;code&gt;localhost:8080/sleep&lt;/code&gt; 的情况。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;➜  scheduler GODEBUG=schedtrace=1000 ./scheduler &amp;gt; sleep_result 2&amp;gt;&amp;amp;1 &amp;amp;
[1] 8284
➜  scheduler wrk -t12 -c400 -d30s http://localhost:8080/sleep
Running 30s test @ http://localhost:8080/sleep
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.01s    13.52ms   1.20s    86.57%
    Req/Sec    83.06     89.44   320.00     79.12%
  11370 requests in 30.10s, 1.26MB read
Requests/sec:    377.71
Transfer/sec:     42.79KB
➜  scheduler cat sleep_result
SCHED 0ms: gomaxprocs=4 idleprocs=1 threads=6 spinningthreads=2 idlethreads=0 runqueue=0 [0 0 0 0]
SCHED 1000ms: gomaxprocs=4 idleprocs=4 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
SCHED 2011ms: gomaxprocs=4 idleprocs=4 threads=6 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
SCHED 3013ms: gomaxprocs=4 idleprocs=4 threads=282 spinningthreads=0 idlethreads=1 runqueue=0 [0 0 0 0]
SCHED 4020ms: gomaxprocs=4 idleprocs=4 threads=400 spinningthreads=0 idlethreads=1 runqueue=0 [0 0 0 0]
SCHED 5028ms: gomaxprocs=4 idleprocs=4 threads=401 spinningthreads=0 idlethreads=2 runqueue=0 [0 0 0 0]
SCHED 6037ms: gomaxprocs=4 idleprocs=4 threads=401 spinningthreads=0 idlethreads=2 runqueue=0 [0 0 0 0]
SCHED 7038ms: gomaxprocs=4 idleprocs=4 threads=402 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
SCHED 8039ms: gomaxprocs=4 idleprocs=4 threads=402 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
SCHED 9046ms: gomaxprocs=4 idleprocs=4 threads=402 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
SCHED 10049ms: gomaxprocs=4 idleprocs=4 threads=402 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
SCHED 11056ms: gomaxprocs=4 idleprocs=4 threads=402 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
SCHED 12058ms: gomaxprocs=4 idleprocs=4 threads=402 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0]
SCHED 13058ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=4 runqueue=0 [0 0 0 0]
SCHED 14062ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=4 runqueue=0 [0 0 0 0]
SCHED 15064ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=4 runqueue=0 [0 0 0 0]
SCHED 16066ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=4 runqueue=0 [0 0 0 0]
SCHED 17068ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=4 runqueue=0 [0 0 0 0]
SCHED 18072ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=4 runqueue=0 [0 0 0 0]
SCHED 19083ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=4 runqueue=0 [0 0 0 0]
SCHED 20084ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=4 runqueue=0 [0 0 0 0]
SCHED 21086ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=4 runqueue=0 [0 0 0 0]
SCHED 22088ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=4 runqueue=0 [0 0 0 0]
SCHED 23096ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=4 runqueue=0 [0 0 0 0]
SCHED 24100ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=4 runqueue=0 [0 0 0 0]
SCHED 25100ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=4 runqueue=0 [0 0 0 0]
SCHED 26100ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=4 runqueue=0 [0 0 0 0]
SCHED 27103ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=4 runqueue=0 [0 0 0 0]
SCHED 28110ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=4 runqueue=0 [0 0 0 0]
SCHED 33131ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=396 runqueue=0 [0 0 0 0]
SCHED 34137ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=400 runqueue=0 [0 0 0 0]
SCHED 35140ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=400 runqueue=0 [0 0 0 0]
SCHED 36150ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=400 runqueue=0 [0 0 0 0]
SCHED 37155ms: gomaxprocs=4 idleprocs=4 threads=403 spinningthreads=0 idlethreads=400 runqueue=0 [0 0 0 0]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;压力测试完毕之后，创建的线程明显比 &lt;code&gt;localhost:8080/echo&lt;/code&gt; 多不少。在压测过程中采用 &lt;code&gt;gdb attach&lt;/code&gt; + &lt;code&gt;thread apply all bt&lt;/code&gt; 查看这些线程都在做什么:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;...
Thread 152 (Thread 0x7f4744fb1700 (LWP 27863)):
#0  syscall.Syscall () at /usr/local/go/src/syscall/asm_linux_amd64.s:27
#1  0x000000000047151f in syscall.Nanosleep (time=0xc42119ac90,
#2  0x000000000060f042 in main.main.func1 (w=..., r=0xc4218d8900)
#3  0x00000000005e8974 in net/http.HandlerFunc.ServeHTTP (f=
#4  0x00000000005ea020 in net/http.(*ServeMux).ServeHTTP (
#5  0x00000000005eafa4 in net/http.serverHandler.ServeHTTP (sh=..., rw=...,
#6  0x00000000005e7a5d in net/http.(*conn).serve (c=0xc420263360, ctx=...)
#7  0x0000000000458e31 in runtime.goexit ()
#8  0x000000c420263360 in ?? ()
#9  0x00000000007cf100 in crypto/elliptic.p224ZeroModP63 ()
#10 0x000000c421180ec0 in ?? ()
#11 0x0000000000000000 in ?? ()
Thread 151 (Thread 0x7f47457b2700 (LWP 27862)):
#0  syscall.Syscall () at /usr/local/go/src/syscall/asm_linux_amd64.s:27
#1  0x000000000047151f in syscall.Nanosleep (time=0xc4206bcc90,
#2  0x000000000060f042 in main.main.func1 (w=..., r=0xc4218cd300)
#3  0x00000000005e8974 in net/http.HandlerFunc.ServeHTTP (f=
#4  0x00000000005ea020 in net/http.(*ServeMux).ServeHTTP (
#5  0x00000000005eafa4 in net/http.serverHandler.ServeHTTP (sh=..., rw=...,
#6  0x00000000005e7a5d in net/http.(*conn).serve (c=0xc42048afa0, ctx=...)
#7  0x0000000000458e31 in runtime.goexit ()
#8  0x000000c42048afa0 in ?? ()
#9  0x00000000007cf100 in crypto/elliptic.p224ZeroModP63 ()
#10 0x000000c4204fd080 in ?? ()
#11 0x0000000000000000 in ?? ()
...
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;Red Hat 系列的机器可以直接使用 &lt;code&gt;pstack&lt;/code&gt; 去 Dump 当前主进程内部的调用栈情况，可惜 Ubuntu 64 Bit 没有这样的包，只能自己写一个脚本去调用 &lt;code&gt;gdb&lt;/code&gt; 来 Dump。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;截取两个线程的调用栈信息，发现它们都在休眠状态，几乎都卡在 &lt;code&gt;/usr/local/go/src/syscall/asm_linux_amd64.s&lt;/code&gt; 上。如果都阻塞了，那么它是怎么处理新来的请求？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/syscall/asm_linux_amd64.s

TEXT    ·Syscall(SB),NOSPLIT,$0-56
        CALL    runtime·entersyscall(SB)
        MOVQ    a1+8(FP), DI
        MOVQ    a2+16(FP), SI
        MOVQ    a3+24(FP), DX
        MOVQ    $0, R10
        MOVQ    $0, R8
        MOVQ    $0, R9
        MOVQ    trap+0(FP), AX	// syscall entry
        SYSCALL
        CMPQ    AX, $0xfffffffffffff001
        JLS     ok
        MOVQ    $-1, r1+32(FP)
        MOVQ    $0, r2+40(FP)
        NEGQ    AX
        MOVQ    AX, err+48(FP)
        CALL    runtime·exitsyscall(SB)
        RET
ok:
        MOVQ    AX, r1+32(FP)
        MOVQ    DX, r2+40(FP)
        MOVQ    $0, err+48(FP)
        CALL    runtime·exitsyscall(SB)
        RET
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;Syscall&lt;/code&gt; 会调用 &lt;code&gt;runtime.entersyscall&lt;/code&gt; 会将当前 Processor 的状态设置为 &lt;code&gt;_Psyscall&lt;/code&gt;。
当进入系统调用时间过长时，&lt;code&gt;retake&lt;/code&gt; 函数在这些 &lt;code&gt;_Psyscall&lt;/code&gt; Processor 的状态改为 &lt;code&gt;_Pidle&lt;/code&gt;，防止长时间地占用 Processor 导致整体不工作。&lt;/p&gt;
&lt;p&gt;进入空闲状态的 Processor 可能会被 &lt;code&gt;wakep&lt;/code&gt; 函数创建出来的新进程绑定上，然而新的 Goroutine 可能还会陷入长时间的系统调用，这一来就进入恶性循环，导致 go binary 创建出大量的线程。&lt;/p&gt;
&lt;p&gt;当然，Golang 会限制这个线程数目。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/proc.go

func checkmcount() {
        // sched lock is held
        if sched.mcount &amp;gt; sched.maxmcount {
                print(&amp;quot;runtime: program exceeds &amp;quot;, sched.maxmcount, &amp;quot;-thread limit\n&amp;quot;)
                throw(&amp;quot;thread exhaustion&amp;quot;)
        }
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当 Machine 从内核态回来之后，会进入 &lt;code&gt;runtime.exitsyscall&lt;/code&gt;。
如果执行时间很短，它会尝试地夺回之前的 Processor ；或者是尝试绑定空闲的 Processor，一旦绑定上了 Processor ，它便会继续运行当前的 Goroutine。
如果都失败了，Machine 因为没有可绑定的 Processor 而将当前的 Goroutine 放回到全局队列中，将自己进入休眠状态，等待其他 Machine 来唤醒。&lt;/p&gt;
&lt;p&gt;一般情况下，go binary 不会创建特别多的线程，但是上线的代码还是需要做一下压测，了解一下代码的实际情况。
一旦真的创建大量的线程了，Golang 目前的版本是不会回收这些空闲的线程。
不过好在 Go10/Go11 会改进这一缺点，详情请查看 &lt;a href=&#34;https://github.com/golang/go/issues/14592&#34;&gt;issues/14592&lt;/a&gt;。&lt;/p&gt;
&lt;h3 id=&#34;7-总结&#34;&gt;7. 总结&lt;/h3&gt;
&lt;p&gt;本文粗粒度地介绍了 Golang Goroutine Scheduler 的工作流程，并没有涉及到垃圾回收，Netpoll 以及 Channel Send/Receive 对调度的影响，希望能让读者有个大体的认识。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;runtime.mstart&lt;/code&gt; 内部的细节很多，而且很多并发操作都建立在无锁的基础上，这样能减少锁对性能的影响，感兴趣的朋友可以根据上文提到的函数一步一步地查看，应该会有不少的收获。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;8-reference&#34;&gt;8. Reference&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://talks.golang.org/2012/waza.slide&#34;&gt;Rob Pike&amp;rsquo;s 2012 Concurrency is not Parallelism&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://golang.org/doc/asm&#34;&gt;A Quick Guide to Go&amp;rsquo;s Assembler&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://docs.google.com/document/d/1TTj4T2JO42uD5ID9e89oa0sLKhJYD0Y_kqxDv3I3XMw/edit#heading=h.mmq8lm48qfcw&#34;&gt;Scalable Go Scheduler Design Doc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://software.intel.com/en-us/blogs/2014/05/10/debugging-performance-issues-in-go-programs&#34;&gt;Debugging performance issues in Go programs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        </item>
        
        <item>
          <title>Protobuf 3.0 编码</title>
          <link>/post/2017-protobuf-3-encoding/</link>
          <pubDate>Wed, 15 Nov 2017 00:00:00 +0000</pubDate>
          <guid>https://fuweid.com/post/2017-protobuf-3-encoding/</guid>
          <description>&lt;p&gt;Protobuf 是 G 厂开源的序列化数据的方法，可用来通信或者存储数据。它采用 IDL 描述数据接口，使得不同语言编写的程序可以根据同一接口通信。不同编程语言也可以根据 IDL 的描述来生成对应数据结构，该数据结构用来编解码。为此，G 厂为主流开发语言都提供代码生成器（即 protoc ）。&lt;/p&gt;
&lt;p&gt;为了更好地了解一些细节，本文将主要描述 Protobuf 3.0 的编码规则。
Protobuf 里面主要采用 Varint 和 Zig-Zag 的方式来对整型数字进行编码。在理解 Protobuf 之前，需要先了解这两种编码方式。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Protobuf 采用是 Little Endian 的方式编码。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;1-varints&#34;&gt;1. Varints&lt;/h3&gt;
&lt;p&gt;int64, int32, uint64, uint32 都有固定的二进制位数。&lt;/p&gt;
&lt;p&gt;如果将这些数字序列化成二进制流的时候，需要额外空间告知接收方数据的长度。对于采用 int64, uint64 这两种类型的数据而言，如果大部分时间都只是使用较小的数值，那么会极大地浪费传输带宽和存储空间。针对这两个问题，Protobuf 采用 Varints 的编码方式。&lt;/p&gt;
&lt;p&gt;Varints 将源数据按照 &lt;strong&gt;7 bit&lt;/strong&gt; 分组，每 &lt;strong&gt;7 bit&lt;/strong&gt; 加 &lt;strong&gt;MSB (Most Significant Bit)&lt;/strong&gt; 标识位来组成一个字节，其中 MSB 标识位用来判断是否存在后序分组。如果出现多组的情况，那么低有效位比特组优先。&lt;/p&gt;
&lt;p&gt;64 有效位为 7 bit，不需要额外的字节，所以 MSB 比特位为 0。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;64 = 0100 0000
  =&amp;gt; 0100 0000
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;16657 有效位为 15 bit，需要分成三组字节，前两组字节为了提示还存在后续字节，所以前两组字节的 MSB 比特位为 1。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;16657 = 0100 0001 0001 0001
  =&amp;gt;  001 0001 ++ 000 0010 ++ 000 0001 (reverse the groups of 7 bits)
  =&amp;gt;  1001 0001 1000 0010 0000 0001
  =&amp;gt;  0x91 0x82 0x01
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;由于负数的最高有效位为 1，int32 类型的负数固定需要 5 字节，而 int64 的负数需要 10 字节，基本上告别了空间效益。所以 Varints 在编码负数时，需要引入 Zig-Zag 编码解决压缩问题。&lt;/p&gt;
&lt;h3 id=&#34;2-zig-zag&#34;&gt;2. Zig-Zag&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&#34;text-align:center&#34;&gt;Signed Original&lt;/th&gt;
&lt;th style=&#34;text-align:center&#34;&gt;Encoded As&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:center&#34;&gt;0&lt;/td&gt;
&lt;td style=&#34;text-align:center&#34;&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:center&#34;&gt;-1&lt;/td&gt;
&lt;td style=&#34;text-align:center&#34;&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:center&#34;&gt;1&lt;/td&gt;
&lt;td style=&#34;text-align:center&#34;&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:center&#34;&gt;-2&lt;/td&gt;
&lt;td style=&#34;text-align:center&#34;&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:center&#34;&gt;2&lt;/td&gt;
&lt;td style=&#34;text-align:center&#34;&gt;4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:center&#34;&gt;2147483647&lt;/td&gt;
&lt;td style=&#34;text-align:center&#34;&gt;4294967294&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:center&#34;&gt;-2147483648&lt;/td&gt;
&lt;td style=&#34;text-align:center&#34;&gt;4294967295&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Zig-Zag 编码可以将负数转化成正数，如上表所示。根据上述表格可以很快地得出结论，负数 &lt;code&gt;n&lt;/code&gt; 编码后的值为 &lt;code&gt;2 * abs(n) - 1&lt;/code&gt;，而正数，编码后为 &lt;code&gt;2 * n&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;实际上，Zig-Zag 会采用以下方式来进行编解码。为了简单起见，接下来将使用 &lt;strong&gt;int8&lt;/strong&gt; 类型分析 Zig-Zag 编解码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;encode(n):
  int64 =&amp;gt; (n &amp;lt;&amp;lt; 1) ^ (n &amp;gt;&amp;gt; 63)
  int32 =&amp;gt; (n &amp;lt;&amp;lt; 1) ^ (n &amp;gt;&amp;gt; 31)
  int64 =&amp;gt; (n &amp;lt;&amp;lt; 1) ^ (n &amp;gt;&amp;gt; 15)
  int8  =&amp;gt; (n &amp;lt;&amp;lt; 1) ^ (n &amp;gt;&amp;gt; 7)

decode(n): 
  (n &amp;gt;&amp;gt;&amp;gt; 1) ^ - (n &amp;amp; 1)

NOTE: 
  &amp;lt;&amp;lt;, &amp;gt;&amp;gt; Arithmetic Shift
  &amp;gt;&amp;gt;&amp;gt;       Logical Shift
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&#34;21-encode&#34;&gt;2.1 Encode&lt;/h4&gt;
&lt;p&gt;Zig-Zag 会将最高符号位算数位移到 &lt;strong&gt;LSB（Least significant bit）&lt;/strong&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;positive: (n &amp;gt;&amp;gt; 7) =&amp;gt; 0x00
negative: (n &amp;gt;&amp;gt; 7) =&amp;gt; 0xFF
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;任何数值与 &lt;code&gt;0x00&lt;/code&gt; 异或都等到本身，而与 &lt;code&gt;0xFF&lt;/code&gt; 异或会现成按位取反。
根据补码互补的原理，一个数 &lt;code&gt;A&lt;/code&gt; 与 &lt;code&gt;0xFF&lt;/code&gt; 异或就变成 &lt;code&gt;-A - 1&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;A ^ 0xFF = ~A = -A - 1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因此，负数经过运算之后变成 &lt;code&gt;- 2 * n - 1&lt;/code&gt;。而正数经过运算只是简单扩大两倍而已，将会 &lt;code&gt;2 * n&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;-2 =&amp;gt; 3

  1111 1100 (1111 1110 &amp;lt;&amp;lt; 1)
^ 1111 1111 (1111 1110 &amp;gt;&amp;gt; 7)
-----------------
  0000 0011 (-2 &amp;lt;&amp;lt; 1) ^ (-2 &amp;gt;&amp;gt; 7)
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&#34;22-decode&#34;&gt;2.2 Decode&lt;/h4&gt;
&lt;p&gt;Zig-Zag 编码的时候将最高符号位移位到了 LSB，解码的时候需要还原到 MSB。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;positive: - (n &amp;amp; 1) = 0  =&amp;gt; 0x00
negative: - (n &amp;amp; 1) = -1 =&amp;gt; 0xFF
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;n &amp;gt;&amp;gt;&amp;gt; 1&lt;/code&gt; 逻辑右移的过程相当于做了除以 2 的操作，所有奇数的逻辑右移都可以得到 &lt;code&gt;n / 2 = (n - 1)/2&lt;/code&gt;，根据解码的表达式可以得到以下推断。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;n &amp;amp; 1 == 0:
  (n &amp;gt;&amp;gt;&amp;gt; 1) ^ -(n &amp;amp; 1) = (n &amp;gt;&amp;gt;&amp;gt; 1) = n / 2

n &amp;amp; 1 == 1:
  (n &amp;gt;&amp;gt;&amp;gt; 1) ^ -(n &amp;amp; 1) = ~(n &amp;gt;&amp;gt;&amp;gt; 1) = - (n &amp;gt;&amp;gt;&amp;gt; 1) - 1 = - (n + 1) / 2
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;255&lt;/code&gt; 解码之后的结果为 &lt;code&gt;-128&lt;/code&gt;。如果解码过程是通过先加后除的方式，将会出现溢出错误。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;255 =&amp;gt; -128

  0111 1111 (1111 1111 &amp;gt;&amp;gt;&amp;gt; 1)
^ 1111 1111 (-(1111 1111 &amp;amp; 1))
-----------------
  1000 0000 (255 &amp;gt;&amp;gt;&amp;gt; 1) ^ -(255 &amp;amp; 1)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Protobuf 在编码负数的时候，它提供了 Zig-Zag 编码的可能，可在此基础上在使用 Varints 来达到压缩效果。&lt;/p&gt;
&lt;h3 id=&#34;3-message&#34;&gt;3. Message&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;message Simple {
  //
  //     _ declared type
  //    /      _ field name
  //   /      /     _ field number, alias tag
  //  /      /     /
  int64 o_int64 = 16;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Protobuf Message 是一系列的 Key-Value 二进制数据流。在编码过程中，仅仅使用 &lt;strong&gt;field number&lt;/strong&gt; 和 &lt;strong&gt;wire type&lt;/strong&gt; 为 Key，而 &lt;strong&gt;declared type&lt;/strong&gt; 和 &lt;strong&gt;field name&lt;/strong&gt; 会辅助解码来判断数据的具体类型，其中 wire type 有以下几种类型。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style=&#34;text-align:center&#34;&gt;Wire Type&lt;/th&gt;
&lt;th style=&#34;text-align:center&#34;&gt;Meaning&lt;/th&gt;
&lt;th style=&#34;text-align:center&#34;&gt;Used For&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:center&#34;&gt;0&lt;/td&gt;
&lt;td style=&#34;text-align:center&#34;&gt;Varint&lt;/td&gt;
&lt;td style=&#34;text-align:center&#34;&gt;int32, int64, uint32, uint64, sint32, sint64, bool, enum&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:center&#34;&gt;1&lt;/td&gt;
&lt;td style=&#34;text-align:center&#34;&gt;64-bit&lt;/td&gt;
&lt;td style=&#34;text-align:center&#34;&gt;fixed64, sfixed64, double&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:center&#34;&gt;2&lt;/td&gt;
&lt;td style=&#34;text-align:center&#34;&gt;Length-delimited&lt;/td&gt;
&lt;td style=&#34;text-align:center&#34;&gt;string, bytes, embedded messages, packed repeated fields&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:center&#34;&gt;3&lt;/td&gt;
&lt;td style=&#34;text-align:center&#34;&gt;Start Group&lt;/td&gt;
&lt;td style=&#34;text-align:center&#34;&gt;groups (deprecated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:center&#34;&gt;4&lt;/td&gt;
&lt;td style=&#34;text-align:center&#34;&gt;End Group&lt;/td&gt;
&lt;td style=&#34;text-align:center&#34;&gt;groups (deprecated)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style=&#34;text-align:center&#34;&gt;5&lt;/td&gt;
&lt;td style=&#34;text-align:center&#34;&gt;32-bit&lt;/td&gt;
&lt;td style=&#34;text-align:center&#34;&gt;fixed32, sfixed64, float&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;每一个 Key 都是 &lt;code&gt;(field number &amp;lt;&amp;lt; 3 | wire type)&lt;/code&gt; 的 Varint 编码值。&lt;/p&gt;
&lt;p&gt;现在按照 Simple 的约定发送来以下数据。接下来，我们将作为人工解码器来分析这份数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;80 01 96 01
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;首先 Protobuf Message 编码之后是一系列的 Key-Value，因此首字节属于 Key 的一部分。Key 首字节 &lt;code&gt;80&lt;/code&gt; 的 MSB 标志位为 1，说明 Key 除了 &lt;code&gt;80&lt;/code&gt; 外还有后序字节。根据上文 Varints 的介绍，可以得到 Key 中 field number(&lt;code&gt;16&lt;/code&gt;) 和 wire type(&lt;code&gt;0&lt;/code&gt;)。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;80 01

1000 0000 0000 0001
   =&amp;gt; 000 0000 ++ 000 0001  (drop the msb)
   =&amp;gt; 1000 0000             (reverse the groups of 7 bits)
   =&amp;gt; (0001 0000 &amp;lt;&amp;lt; 3) | 0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;按照同样方式，Value 数据为 &lt;code&gt;96 01&lt;/code&gt;。经过 Varints 解码后为 150，所以 &lt;code&gt;80 01 96 01&lt;/code&gt; 代表着 &lt;code&gt;Simple.o_int64 = 150&lt;/code&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;96 01

1001 0110 0000 0001
  =&amp;gt; 001 0110 ++ 000 0001    (drop the msb)
  =&amp;gt; 1001 0110               (reverse the groups of 7 bits)
  =&amp;gt; 128 + 16 + 4 + 2 = 150
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;4-wire-type&#34;&gt;4. Wire Type&lt;/h3&gt;
&lt;h4 id=&#34;41-varint---sint32sint64&#34;&gt;4.1 Varint - sint32/sint64&lt;/h4&gt;
&lt;p&gt;对于负数而言，前序比特 1 不能带来压缩上效益，所以 Protobuf 提供 &lt;code&gt;sint32&lt;/code&gt;，&lt;code&gt;sint64&lt;/code&gt; 类型来使用 Zig-Zag 提高压缩率。&lt;/p&gt;
&lt;h4 id=&#34;42-32-bit--64-bit&#34;&gt;4.2 32-bit / 64-bit&lt;/h4&gt;
&lt;p&gt;这两部分 wire type 会使用固定长度去传输数据，其中 &lt;code&gt;64-bit&lt;/code&gt; 采用 8 字节传输，而 &lt;code&gt;32-bit&lt;/code&gt; 采用 4 字节传输。&lt;/p&gt;
&lt;h4 id=&#34;43-length-delimited&#34;&gt;4.3 Length-delimited&lt;/h4&gt;
&lt;p&gt;Length-delimited 会引入 &lt;strong&gt;payload size&lt;/strong&gt; 来辅助说明后序字节数，其中 &lt;strong&gt;payload size&lt;/strong&gt; 的编码采用 Varints 。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;strings/bytes&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;message SimpleString {
  string o_string = 1;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;将 &lt;code&gt;o_string&lt;/code&gt; 设置成 &lt;code&gt;Hello, world!&lt;/code&gt;，会得到以下数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0A 0D 48 65 6C 6C 6F 2C 20 77 6F 72 6C 64 21
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Key &lt;code&gt;0A&lt;/code&gt; 可以推断出 field number(&lt;code&gt;1&lt;/code&gt;) 和 wire type(&lt;code&gt;2&lt;/code&gt;)。payload size(&lt;code&gt;0D&lt;/code&gt;) 解码之后为 13 ，后序 13 个字节将代表 &lt;code&gt;o_string&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;embedded messages&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;message SimpleEmbedded {
  Simple o_embedded = 1; 
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;将 &lt;code&gt;o_embedded.o_int64&lt;/code&gt; 设置成 150，会得到以下数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0A 04 80 01 96 01
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Key &lt;code&gt;0A&lt;/code&gt; 可以推断出 field number(&lt;code&gt;1&lt;/code&gt;) 和 wire type(&lt;code&gt;2&lt;/code&gt;)。payload size(&lt;code&gt;04&lt;/code&gt;)  解码之后为 4 ，后序 4 个字节将代表 &lt;code&gt;o_embedded&lt;/code&gt;。整个过程基本和 SimpleString 一致，只不过 &lt;code&gt;o_embedded&lt;/code&gt; 还需要进一步的解码。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;packed repeated fields&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Protobuf 3.0 对于 repeated field 默认都采用了 packed 的形式。不过在介绍 packed 特性前，有必要说明一下 unpacked 的编码结构。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;message SimpleInt64 {
  int64 o_int64 = 1;
}

message SimpleUnpacked {
  repeated int64 o_ids = 1 [packed = false];
}

message SimplePacked {
  repeated int64 o_ids = 1;
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;将 &lt;code&gt;SimpleUnpacked.o_ids&lt;/code&gt; 设置成 &lt;code&gt;1,2&lt;/code&gt; 数组，会得到以下数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;08 01 08 02

08 // field number = 1, wire type = 0
01 // value = 1 
08 // field number = 1, wire type = 0
02 // value = 2
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Protobuf 编码 unpacked repeated fields 时，并不会将 repeated fields 看成是一个整体，而是单独编码每一个元素。所以在解码 unpacked repeated fileds 时，需要将相同 field number 的数据合并到一起。&lt;/p&gt;
&lt;p&gt;从另外一个角度看，Protobuf 允许将相同 Key 的数据合并到一起。&lt;code&gt;08 01 08 02&lt;/code&gt; 数据可以看成是 &lt;code&gt;SimpleInt64.o_int64 = 1&lt;/code&gt; 和 &lt;code&gt;SimpleInt64.o_int64 = 2&lt;/code&gt; 编码合并的结果。&lt;/p&gt;
&lt;p&gt;让我们来看看 packed repeated fields 编码结果。同样将 &lt;code&gt;SimplePacked.o_ids&lt;/code&gt; 设置成 &lt;code&gt;1,2&lt;/code&gt; 数组，却得到不同的数据，因为 Protobuf 编码时将 &lt;code&gt;o_ids&lt;/code&gt; 看成是一个整体。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;0A 02 01 02

0A // field number = 1, wire type = 2
02 // payload size = 2
01 // first elem = 1
02 // second elem = 2
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Protobuf 3.0 packed 的行为仅仅支持基础数据类型，即 &lt;code&gt;Varint/64-bit/32-bit&lt;/code&gt; 三种 wire type。&lt;/p&gt;
&lt;p&gt;packed 和 unpacked 编码面对长度为 0 的数据时，它并不会输出任何二进制数据。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;个人认为基础数据类型所占用字节数少，整体字节数相对可控，引入 payload size 能带来压缩效益。一旦使用 embedded message 之后，每一个元素的大小将不可控，可能只有少量元素，但是整体字节数将会很大，payload size 需要大量的字节表示。面对这种场景，unpacked repeated fields 单独编码的方式会带来压缩效益，即使包含了重复的 Key 信息。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;5-start-groupend-group&#34;&gt;5. Start Group/End Group&lt;/h3&gt;
&lt;p&gt;由于 Protobuf 放弃使用 &lt;code&gt;Start Group&lt;/code&gt; 和 &lt;code&gt;End Group&lt;/code&gt;，在此也不再介绍。&lt;/p&gt;
&lt;h3 id=&#34;6-reference&#34;&gt;6. Reference&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://developers.google.com/protocol-buffers/docs/encoding&#34;&gt;Protocol Buffers Encoding&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        </item>
        
        <item>
          <title>Go Interface &amp; Duck Typing</title>
          <link>/post/2017-go-interface-duck-typing/</link>
          <pubDate>Mon, 05 Jun 2017 00:00:00 +0000</pubDate>
          <guid>https://fuweid.com/post/2017-go-interface-duck-typing/</guid>
          <description>&lt;p&gt;Go 不需要像 Java 那样显式地使用 &lt;strong&gt;implement&lt;/strong&gt; 说明某一数据类型实现了 interface，只要某一数据类型实现了 interface 所定义的方法名签，那么就称该数据类型实现了 interface。interface 的语言特性可以容易地做到接口定义和具体实现解耦分离，并将注意力转移到如何使用 interface ，而不是方法的具体实现，我们也称这种程序设计为 Duck Typing。文本将描述 Go 是如何通过 interface 来实现 Duck Typing。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;本文提供的源代码都是基于 go1.7rc6 版本。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;1-duck-typing&#34;&gt;1. Duck Typing&lt;/h3&gt;
&lt;p&gt;了解实现原理之前，我们可以简单过一下 Go 的 Duck Typing 示例。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

type Ducker interface { Quack() }

type Duck struct {}
func (_ Duck) Quack() { println(&amp;quot;Quaaaaaack!&amp;quot;) }

type Person struct {} 
func (_ Person) Quack() { println(&amp;quot;Aha?!&amp;quot;) }

func inTheForest(d Ducker) { d.Quack() }

func main() {
	inTheForest(Duck{})
	inTheForest(Person{})
}

// result:
// Quaaaaaack!
// Aha?!
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在示例中，&lt;code&gt;inTheForest&lt;/code&gt; 函数使用了 &lt;code&gt;Ducker&lt;/code&gt; 的 &lt;code&gt;Quack()&lt;/code&gt; 方法，而 &lt;code&gt;Quack()&lt;/code&gt; 方法的具体实现由实参所决定。根据 Go interface 的定义，&lt;code&gt;Duck&lt;/code&gt; 和 &lt;code&gt;Person&lt;/code&gt; 两种数据类型都有 &lt;code&gt;Quack()&lt;/code&gt; 方法，说明这两种数据类型都实现了 &lt;code&gt;Ducker&lt;/code&gt; 。当实参分别为这两种类型的数据时，&lt;code&gt;inTheForest&lt;/code&gt; 函数表现出『多态』。&lt;/p&gt;
&lt;p&gt;在这没有继承关系的情况下，Go 可以通过 interface 的 Duck Typing 特性来实现『多态』。作为一个静态语言，Go 是如何实现 Duck Typing 这一特性？&lt;/p&gt;
&lt;h3 id=&#34;2-interface-data-structure&#34;&gt;2. interface data structure&lt;/h3&gt;
&lt;p&gt;interface 是 Go 数据类型系统中的一员。在分析运行机制之前，有必要先了解 interface 的数据结构。&lt;/p&gt;
&lt;h4 id=&#34;21-empty-interface&#34;&gt;2.1 empty interface&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/runtime2.go
type eface struct {
    _type *_type
    data  unsafe.Pointer
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当一个 interface 没有定义方法签名时，那么我们称之为 empty interface。它由 &lt;code&gt;_type&lt;/code&gt; 和 &lt;code&gt;data&lt;/code&gt; 组成，其中 &lt;code&gt;data&lt;/code&gt; 表示 interface 具体实现的数据，而 &lt;code&gt;_type&lt;/code&gt; 是 &lt;code&gt;data&lt;/code&gt; 对应数据的类型元数据。因为没有定义方法签名，所以任何类型都『实现』empty interface。换句话来说，empty interface 可以接纳任何类型的数据。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

func main() {
    i := 1
    var eface interface{} = i
    
    println(eface)
}

// gdb info
// (gdb) i locals
// i = 1
// eface = {
//   _type = 0x55ec0 &amp;lt;type.*+36000&amp;gt;,
//   data = 0xc420045f18
// }
// (gdb) x/x eface.data
// 0xc420045f18:   0x00000001
// (gdb) x/x &amp;amp;i
// 0xc420045f10:   0x00000001
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在使用 gdb 来查看 &lt;code&gt;eface&lt;/code&gt; 数据结构的过程中，我们会发现比较特别的一点：&lt;code&gt;eface.data&lt;/code&gt; 和 &lt;code&gt;i&lt;/code&gt; 的地址不同。一般情况下，将一个数据赋值给 interface 时，程序会为数据生成一份副本，并将副本的地址赋给 &lt;code&gt;data&lt;/code&gt; 。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/cmd/compile/internal/gc/subr.go
// Can this type be stored directly in an interface word?
// Yes, if the representation is a single pointer.
func isdirectiface(t *Type) bool {
    switch t.Etype {
    case TPTR32,
        TPTR64,
        TCHAN,
        TMAP,
        TFUNC,
        TUNSAFEPTR:
        return true

    case TARRAY:
        // Array of 1 direct iface type can be direct.
        return t.NumElem() == 1 &amp;amp;&amp;amp; isdirectiface(t.Elem())

    case TSTRUCT:
        // Struct with 1 field of direct iface type can be direct.
        return t.NumFields() == 1 &amp;amp;&amp;amp; isdirectiface(t.Field(0).Type)
    }

    return false
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当一个数据的类型符合 &lt;code&gt;isdirectiface&lt;/code&gt; 的判定时，那么程序不会生成副本，而是直接将实际地址赋给 &lt;code&gt;data&lt;/code&gt; 。由于这部分内存分配优化和 &lt;strong&gt;reflect&lt;/strong&gt; 实现有关，在此就不做展开描述了。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;reflect 要想在运行时解析数据的方法和属性，它就需要知道数据以及类型元数据。而 empty interface 正好能满足这一需求，这也正是 reflect 的核心方法 &lt;code&gt;ValueOf&lt;/code&gt; 和 &lt;code&gt;TypeOf&lt;/code&gt; 的形参是 empty interface 的原因。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;在 Duck Typing 的使用上，empty interface 使用频率比较高的场景是 Type Switch, Type Assertion，接下来会介绍这些使用场景。&lt;/p&gt;
&lt;h4 id=&#34;22-non-empty-interface&#34;&gt;2.2 non-empty interface&lt;/h4&gt;
&lt;p&gt;相对于 empty interface 而言，有方法签名的 interface 的数据结构要复杂一些。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/runtime2.go
type iface struct {
    tab  *itab
    data unsafe.Pointer
}

type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    bad    int32
    unused int32
    fun    [1]uintptr // variable sized
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;iface&lt;/code&gt; 包含两个字段 &lt;code&gt;tab&lt;/code&gt; 和 &lt;code&gt;data&lt;/code&gt;。和 empty interface 一样，&lt;code&gt;data&lt;/code&gt; 表示具体实现的数据。&lt;code&gt;tab&lt;/code&gt; 不再是简单的 &lt;code&gt;_type&lt;/code&gt;，不仅维护了（&lt;code&gt;interfacetype&lt;/code&gt;，&lt;code&gt;_type&lt;/code&gt;）匹配的信息，还维护了具体方法实现的列表入口 &lt;code&gt;fun&lt;/code&gt;。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;其中 &lt;code&gt;interfacetype&lt;/code&gt; 是相应 interface 类型的元数据。
而 &lt;code&gt;fun&lt;/code&gt; 字段是一个变长数组的 header ，它代表着具体方法数组的头指针，程序通过&lt;code&gt;fun&lt;/code&gt;去定位具体某一方法实现。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;来看看下面这一段程序。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

type Ducker interface {
        Quack()
        Feathers()
}

type Duck struct{ x int }

func (_ Duck) Quack() { println(&amp;quot;Quaaaaaack!&amp;quot;) }

func (_ Duck) Feathers() { println(&amp;quot;The duck has white and gray feathers.&amp;quot;) }

func inTheForest(d Ducker) {
        d.Quack()
        d.Feathers()
}

func main() {
        inTheForest(Duck{x: 1})
}

// gdb info at func inTheForest
(gdb) p d
$2 = {
  tab = 0x97100 &amp;lt;Duck,main.Ducker&amp;gt;,
  data = 0xc42000a118
}
(gdb) x/2xg d.tab.fun
0x97120 &amp;lt;go.itab.main.Duck,main.Ducker+32&amp;gt;:     0x00000000000022f0      0x0000000000002230
(gdb) i symbol 0x00000000000022f0
main.(*Duck).Feathers in section .text
(gdb) i symbol 0x0000000000002230
main.(*Duck).Quack in section .text
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在 &lt;code&gt;inTheForest&lt;/code&gt; 函数里，&lt;code&gt;d.tab.fun&lt;/code&gt; 数组包含了 &lt;code&gt;Duck&lt;/code&gt; 的 &lt;code&gt;Quack&lt;/code&gt; 以及 &lt;code&gt;Feathers&lt;/code&gt; 的方法地址，因此在 &lt;code&gt;d.Quack()&lt;/code&gt; 和 &lt;code&gt;d.Feathers()&lt;/code&gt; 分别使用了 &lt;code&gt;Duck&lt;/code&gt; 的 &lt;code&gt;Quack&lt;/code&gt; 和 &lt;code&gt;Feathers&lt;/code&gt; 方法的具体实现。假如这个时候，传入的不是 &lt;code&gt;Duck&lt;/code&gt; ，而是其他实现了 &lt;code&gt;Ducker&lt;/code&gt; 的数据类型，那么 &lt;code&gt;d.tab.fun&lt;/code&gt; 将会包含相应类型的具体方法实现。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;d.tab.fun 不会包含 interface 定义以外的方法地址。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不难发现，&lt;code&gt;itab.fun&lt;/code&gt; 包含了具体方法的实现，程序在运行时通过 &lt;code&gt;itab.fun&lt;/code&gt; 来决议具体方法的调用，这也是实现 Duck Typing 的核心逻辑。那么问题来了，&lt;code&gt;itab&lt;/code&gt; 是什么时候生成的？&lt;/p&gt;
&lt;h3 id=&#34;3-itab&#34;&gt;3. itab&lt;/h3&gt;
&lt;p&gt;当数据类型 &lt;code&gt;Duck&lt;/code&gt; 实现了 &lt;code&gt;Ducker&lt;/code&gt; 中的所有方法时，编译器才会生成 &lt;code&gt;itab&lt;/code&gt;，并将 &lt;code&gt;Duck&lt;/code&gt; 对 &lt;code&gt;Ducker&lt;/code&gt; 的具体实现绑定到 &lt;code&gt;itab.fun&lt;/code&gt; 上，否则编译不通过。&lt;code&gt;itab.fun&lt;/code&gt; 很像 C++ 中的虚函数表。而 Go 没有继承关系，一个 interface 就可能会对应 N 种可能的具体实现，这种 M:N 的情况太多，没有必要去为所有可能的结果生成 &lt;code&gt;itab&lt;/code&gt;。因此，编译器只会生成部分 &lt;code&gt;itab&lt;/code&gt;，剩下的将会在运行时生成。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;C++ 通过继承关系，在编译期间就生成类的虚函数表。在运行状态下，通过指针来查看虚函数表来定位具体方法实现。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当一个数据类型实现 interface 中所声明的所有方法签名，那么 &lt;code&gt;iface&lt;/code&gt; 就可以携带该数据类型对 interface 的具体实现，否则将会 panic 。这部分判定需要 &lt;code&gt;_type&lt;/code&gt; 和 &lt;code&gt;interfacetype&lt;/code&gt; 元数据，而这部分数据在编译器已经为运行时准备好了，那么判定和生成 &lt;code&gt;itab&lt;/code&gt; 就只要照搬编译器里那一套逻辑即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/iface.go
var (
    ifaceLock mutex // lock for accessing hash
    hash      [hashSize]*itab
)

func itabhash(inter *interfacetype, typ *_type) uint32 {...}
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {...}
func additab(m *itab, locked, canfail bool) {...}

// src/runtime/runtime2.go
type itab struct {
    inter  *interfacetype
    _type  *_type
    link   *itab
    bad    int32
    unused int32
    fun    [1]uintptr // variable sized
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;为了保证运行效率，程序会在运行时会维护全局的 &lt;code&gt;itab&lt;/code&gt; hash 表，&lt;code&gt;getitab&lt;/code&gt; 会在全局 hash 表中查找相应的 &lt;code&gt;itab&lt;/code&gt;。当 &lt;code&gt;getitab&lt;/code&gt; 发现没有相应的 &lt;code&gt;itab&lt;/code&gt; 时，它会调用 &lt;code&gt;additab&lt;/code&gt; 来添加新的 &lt;code&gt;itab&lt;/code&gt;。在插入新的 &lt;code&gt;itab&lt;/code&gt; 之前，&lt;code&gt;additab&lt;/code&gt; 会验证 &lt;code&gt;_type&lt;/code&gt; 对应的类型是否都实现了 &lt;code&gt;interfacetype&lt;/code&gt; 声明的方法集合。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;运行时通过 &lt;code&gt;itabhash&lt;/code&gt; 负责生成 hash 值，并使用单链表来解决冲突问题，其中 &lt;code&gt;itab.link&lt;/code&gt; 可用来实现链表。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;那么问题又来了，&lt;code&gt;_type&lt;/code&gt; 有 N 个方法，&lt;code&gt;interfacetype&lt;/code&gt; 有 M 个方法签名，验证匹配的最坏可能性就是需要 N * M 次遍历。除此之外，&lt;code&gt;additab&lt;/code&gt; 在写之前需要加锁，这两方面都会影响性能。&lt;/p&gt;
&lt;h4 id=&#34;31-additab-的效率问题&#34;&gt;3.1 additab 的效率问题&lt;/h4&gt;
&lt;p&gt;为了减少验证的时间，编译期间会对方法名进行排序，这样最坏的可能也就需要 N + M 次遍历即可。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;细心的朋友可能会发现，在上一个例子中 &lt;code&gt;d.tab.fun&lt;/code&gt; 中的方法是按照字符串大小排序的。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/iface.go
func additab(m *itab, locked, canfail bool) {
    inter := m.inter
    typ := m._type
    x := typ.uncommon()

    // both inter and typ have method sorted by name,
    // and interface names are unique,
    // so can iterate over both in lock step;
    // the loop is O(ni+nt) not O(ni*nt).
    ...
}
&lt;/code&gt;&lt;/pre&gt;&lt;h4 id=&#34;32-锁的效率问题&#34;&gt;3.2 锁的效率问题&lt;/h4&gt;
&lt;p&gt;关于锁的问题，在实现 &lt;code&gt;getitab&lt;/code&gt; 的时候，引入了两轮查询的策略。因为 &lt;code&gt;itab&lt;/code&gt; 数据比较稳定，引入两轮查询可以减少锁带来的影响。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/iface.go
func getitab(inter *interfacetype, typ *_type, canfail bool) *itab {
    ....
    // look twice - once without lock, once with.
    // common case will be no lock contention.
    var m *itab
    var locked int
    for locked = 0; locked &amp;lt; 2; locked++ {
        if locked != 0 {
            lock(&amp;amp;ifaceLock)
        }
        ...
     }
     ...
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;itab&lt;/code&gt; 的生成和查询或多或少带有运行时的开销。然而 &lt;code&gt;itab&lt;/code&gt; 不仅提供了静态语言的类型检查，还提供了动态语言的灵活特性。只要不滥用 interface，&lt;code&gt;itab&lt;/code&gt; 还是可以提供不错的编程体验。&lt;/p&gt;
&lt;h3 id=&#34;4-type-switch--type-assertion&#34;&gt;4. Type Switch &amp;amp; Type Assertion&lt;/h3&gt;
&lt;p&gt;开发者会使用 interface 的 Type Switch 和 Type Assertion 来进行『类型转化』。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

type Ducker interface { Feathers() }

type Personer interface { Feathers() }

type Duck struct{}

func (_ Duck) Feathers() { /* do nothing */ }

func example(e interface{}) {
	if _, ok := e.(Personer); ok {
		println(&amp;quot;I&#39;m Personer&amp;quot;)
	}
	
	if _, ok := e.(Ducker); ok {
		println(&amp;quot;I&#39;m Ducker&amp;quot;)
	}
}

func main() {
     var d Ducker = Duck{}
     example(d)
}

// result:
// I&#39;m Personer
// I&#39;m Ducker
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;根据之前对 &lt;code&gt;itab&lt;/code&gt; 的分析，其实 &lt;code&gt;e.(Personer)&lt;/code&gt; 和 &lt;code&gt;e.(Ducker)&lt;/code&gt; 这两个断言做的就是切换 &lt;code&gt;itab.inter&lt;/code&gt; 和 &lt;code&gt;itab.fun&lt;/code&gt; ，并不是动态语言里的『类型转化』。那么断言的函数入口在哪？&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// go tool objdump -s &#39;main.example&#39; ./main
        main.go:15      0x2050  65488b0c25a0080000      GS MOVQ GS:0x8a0, CX
        main.go:15      0x2059  483b6110                CMPQ 0x10(CX), SP
        main.go:15      0x205d  0f86eb000000            JBE 0x214e
        main.go:15      0x2063  4883ec38                SUBQ $0x38, SP
        main.go:15      0x2067  48896c2430              MOVQ BP, 0x30(SP)
        main.go:15      0x206c  488d6c2430              LEAQ 0x30(SP), BP
        main.go:16      0x2071  488d05c8840500          LEAQ 0x584c8(IP), AX
        main.go:16      0x2078  48890424                MOVQ AX, 0(SP)
        main.go:16      0x207c  488b442448              MOVQ 0x48(SP), AX
        main.go:16      0x2081  488b4c2440              MOVQ 0x40(SP), CX
        main.go:16      0x2086  48894c2408              MOVQ CX, 0x8(SP)
        main.go:16      0x208b  4889442410              MOVQ AX, 0x10(SP)
        main.go:16      0x2090  48c744241800000000      MOVQ $0x0, 0x18(SP)
     =&amp;gt; main.go:16      0x2099  e892840000              CALL runtime.assertE2I2(SB)
        main.go:16      0x209e  0fb6442420              MOVZX 0x20(SP), AX
        main.go:16      0x20a3  8844242f                MOVB AL, 0x2f(SP)
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;通过 &lt;code&gt;objdump&lt;/code&gt; 发现一个很特别的方法：&lt;code&gt;runtime.assertE2I2&lt;/code&gt;。&lt;code&gt;assertE2I2&lt;/code&gt; 是一个断言函数，它负责判断一个 empty interface 里的数据能否转化成一个 non-empty interface，名字最后那个 &lt;code&gt;2&lt;/code&gt; 代表着有两个返回值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第一参数是转化后的结果&lt;/li&gt;
&lt;li&gt;第二参数是断言结果&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;接下来看看 &lt;code&gt;assertE2I2&lt;/code&gt; 的源码。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// src/runtime/iface.go
func assertE2I2(inter *interfacetype, e eface, r *iface) bool {
    if testingAssertE2I2GC {
        GC()
    }
    t := e._type
    if t == nil {
        if r != nil {
            *r = iface{}
        }
        return false
    }
    tab := getitab(inter, t, true)
    if tab == nil {
        if r != nil {
            *r = iface{}
        }
        return false
    }
    if r != nil {
        r.tab = tab
        r.data = e.data
    }
    return true
}
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;该函数会拿出 empty interface 中的 &lt;code&gt;_type&lt;/code&gt; 和 &lt;code&gt;interfacetype&lt;/code&gt; 在 &lt;code&gt;getitab&lt;/code&gt; 中做查询和匹配验证。如果验证通过，&lt;code&gt;r&lt;/code&gt; 会携带转化后的结果，并返回 &lt;code&gt;true&lt;/code&gt;。否则返回 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;src/runtime/iface.go&lt;/code&gt; 中还有很多类似 &lt;code&gt;assertE2I2&lt;/code&gt; 的函数，在这里就不一一阐述了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// T: 具体的数据类型_type
// E: empty interface
// I:  non-empty interface

// src/runtime/iface.go
func assertE2I(inter *interfacetype, e eface, r *iface) {...}
func assertI2I2(inter *interfacetype, i iface, r *iface) bool {..}
func assertI2E(inter *interfacetype, i iface, r *eface) {...}
func assertI2E2(inter *interfacetype, i iface, r *eface) bool {...}
func assertE2T2(t *_type, e eface, r unsafe.Pointer) bool {..}
func assertE2T(t *_type, e eface, r unsafe.Pointer) {..}
func assertI2T2(t *_type, i iface, r unsafe.Pointer) bool {...}
func assertI2T(t *_type, i iface, r unsafe.Pointer) {...}

func convI2I(inter *interfacetype, i iface) (r iface) {...}
func convI2E(i iface) (r eface) {...}
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;5-最后&#34;&gt;5. 最后&lt;/h3&gt;
&lt;p&gt;interface 的 Duck Typing 可以用来实现『多态』、代码的模块化。但是这毕竟有运行时的开销，interface 的滥用和声明大量的方法签名还是会影响到性能。&lt;/p&gt;
&lt;h3 id=&#34;6-reference&#34;&gt;6. Reference&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Duck_typing&#34;&gt;Duke Typing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;http://coolshell.cn/articles/12165.html&#34;&gt;C++ 虚函数表解析&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://research.swtch.com/interfaces&#34;&gt;Go Data Structures: Interfaces&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        </item>
        
        <item>
          <title>让你的 shell 脚本变得可控</title>
          <link>/post/2017-control-your-shell-script/</link>
          <pubDate>Mon, 20 Mar 2017 00:00:00 -0400</pubDate>
          <guid>https://fuweid.com/post/2017-control-your-shell-script/</guid>
          <description>&lt;p&gt;刚开始接触 shell 脚本的时候，最痛苦的地方在于出了问题，却不容易定位问题。&lt;/p&gt;
&lt;p&gt;shell 脚本遇到错误，“大部分” 情况下都会继续执行剩下的命令，最后返回 Zero &lt;a href=&#34;https://en.wikipedia.org/wiki/Exit_status&#34;&gt;Exit Code&lt;/a&gt;  并不代表着结果正确。&lt;/p&gt;
&lt;p&gt;这让人很难发现问题，它不像其他脚本语言，遇到 &lt;code&gt;语法错误&lt;/code&gt; 和 &lt;code&gt;typo&lt;/code&gt; 等错误时便会立即退出。&lt;/p&gt;
&lt;p&gt;如果想要写出容易维护、容易 debug 的 shell 脚本，我们就需要让 shell 脚本变得可控。&lt;/p&gt;
&lt;h3 id=&#34;set--e&#34;&gt;set -e&lt;/h3&gt;
&lt;p&gt;默认情况下，shell 脚本遇到错误并不会立即退出，它还是会继续执行剩下的命令。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# cat example
#!/usr/bin/env bash
# set -e

sayhi # this command is not available.
echo &amp;quot;sayhi&amp;quot;
[root@localhost ~]# ./example
./example: line 4: sayhi: command not found
sayhi
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;我们知道 Linux/Unix 用户等于系统的时候，内核会加载 &lt;code&gt;.bashrc&lt;/code&gt; 或者 &lt;code&gt;.bash_profile&lt;/code&gt; 里的配置。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不同 shell 版本会使用不同的 rc/profile 文件，比如 zsh 版本的 rc 文件名是 .zshrc。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;简单设想下，假如 shell 脚本遇到错误就退出，那么只要这些文件里有 typo 等错误，该用户就永远登陆不了系统。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;在此，并没有考究默认行为的设计缘由，只是想表达 shell 脚本默认行为会让脚本变得不可控。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;code&gt;set -e&lt;/code&gt; 能会让 shell 脚本遇到 Non-Zero Exit Code 时，会立即停止执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# cat example
#!/usr/bin/env bash
set -e

sayhi # this command is not available.
echo &amp;quot;sayhi&amp;quot;
[root@localhost ~]# ./example
./example: line 4: sayhi: command not found
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;set--u&#34;&gt;set -u&lt;/h3&gt;
&lt;p&gt;初始化后再使用变量，这是好的编程习惯。&lt;/p&gt;
&lt;p&gt;但在默认情况下，shell 脚本使用未初始化的变量并不会报错。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# cat example
#!/usr/bin/env bash
# set -u

echo &amp;quot;Hi, ${1}&amp;quot;
[root@localhost ~]# ./example
Hi,
[root@localhost ~]# echo $?
0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;若脚本设置 &lt;code&gt;set -u&lt;/code&gt; ，一旦使用没有初始化的变量或者 &lt;code&gt;positional parameter&lt;/code&gt; 时，脚本将立即返回 1 Exit Code。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# cat example
#!/usr/bin/env bash
set -u

echo &amp;quot;Hi, ${1}&amp;quot;
[root@localhost ~]# ./example
./example: line 4: 1: unbound variable
[root@localhost ~]# echo $?
1
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;需要说明的是，对于预定义的 &lt;code&gt;$@&lt;/code&gt;, &lt;code&gt;$*&lt;/code&gt; 等这些变量，是可以正常使用。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;为了避免使用未初始化的变量，常使用 &lt;code&gt;${VAR:-DEFAULT}&lt;/code&gt; 来设置默认值。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#!/usr/bin/env bash
set -u

# ${VAR:-DEFAULT} evals to DEFAULT if VAR undefined.
foo=${nonexisting:-ping}

echo &amp;quot;${foo}&amp;quot; # =&amp;gt; ping

bar=&amp;quot;pong&amp;quot;

foo=${bar:-ping}

echo &amp;quot;${foo}&amp;quot; # =&amp;gt; pong

# DEFAULT can be empty
empty=${nonexisting:-}

echo &amp;quot;${empty}&amp;quot; # =&amp;gt; &#39;&#39;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;set--o-pipefail&#34;&gt;set -o pipefail&lt;/h3&gt;
&lt;p&gt;在默认情况下，&lt;code&gt;pipeline&lt;/code&gt; 会采用最后一个命令的 Exit Code 作为最终返回的 Exit Code。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# cat example
#!/usr/bin/env bash
# set -o pipefail

grep string /non-existing-file | sort
[root@localhost ~]# ./example
grep: /non-existing-file: No such file or directory
[root@localhost ~]# echo $?
0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;明明报错了，为什么还会返回 Zero Exit Code?&lt;/p&gt;
&lt;p&gt;&lt;code&gt;grep&lt;/code&gt; 一个并不存在的文件会返回 2 Exit Code。&lt;code&gt;grep&lt;/code&gt; 不仅会输出错误信息到 &lt;code&gt;STDERR&lt;/code&gt; 上，还会输出空的字符串到 &lt;code&gt;STDOUT&lt;/code&gt;。对于 &lt;code&gt;sort&lt;/code&gt; 命令而言，空字符串是合法的输入，所以最后命令返回 Zero Exit Code。&lt;/p&gt;
&lt;p&gt;这样错误信息并不能很好地帮助我们改善脚本，返回的 Exit Code 应该要尽可能地反映错误现场。&lt;/p&gt;
&lt;p&gt;和前面两个设置一样，&lt;code&gt;set -o pipefail&lt;/code&gt; 会让 shell 脚本在 &lt;code&gt;pipeline&lt;/code&gt; 过程遇到错误便立即返回相应错误的 Exit Code。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# cat example
#!/usr/bin/env bash
set -o pipefail

grep string /non-existing-file | sort
[root@localhost ~]# ./example
grep: /non-existing-file: No such file or directory
[root@localhost ~]# echo $?
2
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;non-zero-exit-code-is-expected&#34;&gt;non-zero exit code is expected&lt;/h3&gt;
&lt;p&gt;这三个配置太过于苛刻，某些情况下还需要放宽这些限制：当程序可以接受 non-zero exit code 时。&lt;/p&gt;
&lt;p&gt;这里有两种常用的方式去放宽限制：&lt;/p&gt;
&lt;h4 id=&#34;set-&#34;&gt;set +&lt;/h4&gt;
&lt;p&gt;这里有一个脚本是用来产生长度为 64 的随机字符串：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@localhost ~]# cat example
#!/usr/bin/env bash
set -euo pipefail

str=$(cat /dev/urandom | tr -dc &#39;0-9A-Za-z&#39; | head -c 64)

echo &amp;quot;${str}&amp;quot;

[root@localhost ~]# ./example
[root@localhost ~]# echo $?
141
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;该脚本有一个问题，就是 &lt;code&gt;head&lt;/code&gt; 命令在获取到第 64 个字节之后，会关闭 &lt;code&gt;STDIN&lt;/code&gt;，但是 &lt;code&gt;pipe&lt;/code&gt; 还在不断地输出，导致内核不得不抛出 &lt;code&gt;SIGPIPE&lt;/code&gt; 来终止命令。&lt;/p&gt;
&lt;p&gt;因为设置 &lt;code&gt;set -o pipefail&lt;/code&gt; 了 ，整个脚本因为 &lt;code&gt;SIGPIPE&lt;/code&gt; 会退出。&lt;/p&gt;
&lt;p&gt;假设该脚本剩下命令还很多，不能整体去掉 &lt;code&gt;pipefail&lt;/code&gt; ，那么我们就局部放弃这个限制好了。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# cat example
#!/usr/bin/env bash
# exit immediately if non-zero exit code/unset variable/pipe error
set -euo pipefail

# loosen up
set +o pipefail
str=$(cat /dev/urandom | tr -dc &#39;0-9A-Za-z&#39; | head -c 64)
set -o pipefail

echo &amp;quot;${str}&amp;quot;

[root@localhost ~]# ./example
pvScFHDZrdjlI091rQbruyEPM9e6iTN59IyzaKcCJwiCxYmiSNRmkFOfp0YuXi1C
[root@localhost ~]# echo $?
0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;同理，只要设置上 &lt;code&gt;set +e&lt;/code&gt; 或者 &lt;code&gt;set +u&lt;/code&gt; 时，就会放宽相应的限制。&lt;/p&gt;
&lt;p&gt;记得 &lt;strong&gt;有借有还，再借不难&lt;/strong&gt; 就好了。&lt;/p&gt;
&lt;h4 id=&#34;短路运算&#34;&gt;短路运算&lt;/h4&gt;
&lt;p&gt;现在有一个脚本，该脚本用来统计文件 &lt;code&gt;file&lt;/code&gt; 中有多少行是包含了 &lt;code&gt;string&lt;/code&gt; 这个字符串。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;root@localhost ~]# cat example
#!/usr/bin/env bash
set -euo pipefail

count=$(grep -c string ./file)

echo &amp;quot;${count}&amp;quot;

[root@localhost ~]# ./example
[root@localhost ~]# echo $?
1
[root@localhost ~]# cat ./file
example
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因为文件 &lt;code&gt;file&lt;/code&gt; 中并不包含 &lt;code&gt;string&lt;/code&gt; 这一字符串，所以 &lt;code&gt;grep&lt;/code&gt; 返回 1 Exit Code。&lt;/p&gt;
&lt;p&gt;假设遇到没有匹配上的文件，该脚本应该显示零，而不是错误。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;$(cmd || true)&lt;/code&gt; 短路运算会让该命令永远都正常执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# cat example
#!/usr/bin/env bash
set -euo pipefail

count=$(grep -c string ./file || true)

echo &amp;quot;${count}&amp;quot;

[root@localhost ~]# ./example
0
[root@localhost ~]# echo $?
0
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;思考&#34;&gt;思考&lt;/h3&gt;
&lt;p&gt;shell 脚本能帮助我们轻松地完成自动化的任务，这是它的优势。&lt;/p&gt;
&lt;p&gt;但是劣势也比较明显，就是 shell 脚本的返回值。我们来看看下面的一个例子。&lt;/p&gt;
&lt;p&gt;相对于 &lt;code&gt;if/else&lt;/code&gt;, 短路运算可以让代码变得简洁。&lt;/p&gt;
&lt;p&gt;但是一旦最终的判断结果为否，那么该短路运算将会返回 Non-Zero Exit Code。&lt;/p&gt;
&lt;p&gt;假如有一个脚本的最后一条命令是短路运算。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# cat ./echo_filename
#!/usr/bin/env bash
set -euo pipefail

file=${1:-}

[[ -f &amp;quot;${file}&amp;quot; ]] &amp;amp;&amp;amp; echo &amp;quot;File: ${file}&amp;quot;
[root@localhost ~]# ./echo_filename
[root@localhost ~]# echo $?
1
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果没有传参数，那么短路运算将会返回 1 Exit Code，这个结果也将作为整个脚本的返回结果。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;需要说明的是，虽然短路运算返回的 Non-Zero Exit Code，但 &lt;code&gt;set -e&lt;/code&gt; 不会因为它而退出。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;然后我们再看看使用 &lt;code&gt;if/else&lt;/code&gt; 的结果。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[root@localhost ~]# cat echo_filename
#!/usr/bin/env bash
set -euo pipefail

file=${1:-}

if [[ -f &amp;quot;${file}&amp;quot; ]]; then
    echo &amp;quot;File: ${file}&amp;quot;
fi
[root@localhost ~]# ./echo_filename
[root@localhost ~]# echo $?
0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;从逻辑上来分析，即使不传参数，呈现的应该是空字符串，并返回 Zero Exit Code。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;if/else&lt;/code&gt; 语句相对于短路运算要合理。&lt;/p&gt;
&lt;p&gt;我们别小看这一区别，如果这里有脚本调用 &lt;code&gt;echo_filename&lt;/code&gt;，那么使用短路运算将会导致调用该脚本的脚本停止工作。&lt;/p&gt;
&lt;p&gt;归根结底，是因为 shell 脚本并不像其他语言那样支持返回多种数据类型，它只能返回数字的 Exit Code。&lt;/p&gt;
&lt;p&gt;这就代表着脚本的程序设计必须要考虑返回正确的 Exit Code，这样 &lt;code&gt;set -euo pipefail&lt;/code&gt; 才能让脚本变得更加可控。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;关于 &lt;code&gt;set&lt;/code&gt; 更多的内容，请前往 &lt;a href=&#34;https://www.gnu.org/software/bash/manual/bashref.html#The-Set-Builtin&#34;&gt;Link&lt;/a&gt; 。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
        </item>
        
        <item>
          <title>shebang - #!</title>
          <link>/post/2017-shebang-compatibility-version/</link>
          <pubDate>Sun, 19 Mar 2017 00:00:00 -0400</pubDate>
          <guid>https://fuweid.com/post/2017-shebang-compatibility-version/</guid>
          <description>&lt;p&gt;写脚本的时候通常会在脚本的开头加上 &lt;a href=&#34;https://en.wikipedia.org/wiki/Shebang_(Unix)&#34;&gt;shebang&lt;/a&gt;, 系统会将这段内容作为解释器指令，比如 bash shell 脚本。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$&amp;gt; cat example
#!/usr/bin/bash
echo &amp;quot;HaHa&amp;quot;

$&amp;gt; chmod +x ./example

$&amp;gt; ./example
HaHa
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;只要为脚本添加了可执行的属性，那么内核在执行脚本的时候，会调用 shebang 描述的解释器来执行脚本。
&lt;code&gt;./exmaple&lt;/code&gt; 其实等价于 &lt;code&gt;/usr/bin/bash ./example&lt;/code&gt;。shebang 描述的解释器需要写其绝对路径或者相对路径，因为内核并不会在用户设置的 &lt;code&gt;PATH&lt;/code&gt; 里找解释器。关于 shebang，讨论最多的应该是 &lt;strong&gt;兼容性&lt;/strong&gt; 和 &lt;strong&gt;版本控制&lt;/strong&gt; 问题。&lt;/p&gt;
&lt;h3 id=&#34;兼容性&#34;&gt;兼容性&lt;/h3&gt;
&lt;p&gt;Linux 和 Unix 在存放解释器的具体路径不太一致，比如 Linux 会放到 &lt;code&gt;/usr/bin/&lt;/code&gt; 中，而 openBSD 会放到 &lt;code&gt;/usr/local/bin/&lt;/code&gt; 中。不同包管理器在安装解释器的时候，存放的位置也不尽相同。
当你在 Mac  上写了 shell  脚本，测试并提交到代码库。
结果等到部署的那一天，执行脚本的时候发现找不到解释器了。
为了解决这个问题，可以通过 &lt;code&gt;env&lt;/code&gt; 来解决，因为它在 Linux 和 Unix 存放的位置相同。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;$&amp;gt; cat exmaple
&lt;span class=&#34;c1&#34;&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class=&#34;nb&#34;&gt;echo&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;HaHa&amp;#34;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;env&lt;/code&gt; 会在用户设置的 &lt;code&gt;PATH&lt;/code&gt; 中查找解释器第一次出现的具体路径。
虽然办法比较 tricky，但是这种方式能解决脚本解释器的兼容性问题。&lt;/p&gt;
&lt;h3 id=&#34;版本控制&#34;&gt;版本控制&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;env&lt;/code&gt; 会在用户配置的 &lt;code&gt;PATH&lt;/code&gt; 中查找解释器第一次出现的具体路径。
这个机制就说明这存在两个问题：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不同用户配置的 &lt;code&gt;PATH&lt;/code&gt; 内容不同，导致找到的解释器版本会出现不一致&lt;/li&gt;
&lt;li&gt;很难通过 &lt;code&gt;env&lt;/code&gt; 的方式来做到版本控制&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;所以有些人坚持不用 &lt;code&gt;/usr/bin/env cmd&lt;/code&gt; 这种方式。&lt;/p&gt;
&lt;h3 id=&#34;思考&#34;&gt;思考&lt;/h3&gt;
&lt;p&gt;从部署的角度看，线上机器的环境都是一致的，而且都是通过自动化脚本去安装各种依赖。
版本控制较细，这种情况下，不太建议采用 &lt;code&gt;env&lt;/code&gt; 这种方式。如果从开发者的角度看，
还是希望脚本能做到兼容，毕竟开发者的环境千差万别，&lt;code&gt;env&lt;/code&gt;  基本上能解决这一大痛点。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;不用 &lt;code&gt;env&lt;/code&gt; 这种方式，就得确保测试环境和线上机器是一致的，通常 Docker 和 虚拟机都能解决这样的问题。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
        </item>
        
        <item>
          <title>Netfilter 初探</title>
          <link>/post/2017-netfilter-beginning/</link>
          <pubDate>Fri, 17 Mar 2017 00:00:00 -0400</pubDate>
          <guid>https://fuweid.com/post/2017-netfilter-beginning/</guid>
          <description>&lt;p&gt;Linux 内核在 2.4.x 版本中正式引入 &lt;a href=&#34;http://www.netfilter.org/&#34;&gt;Netfilter&lt;/a&gt; 模块，该模块负责网络数据包过滤和 &lt;a href=&#34;https://en.wikipedia.org/wiki/Network_address_translation&#34;&gt;Network Address Translation&lt;/a&gt;。
Netfilter 代表着一系列的 Hook ，被内核嵌入到 TCP/IP 协议栈中，数据包在穿梭协议栈时，Hook 会检查数据包，从而达到访问控制的作用。&lt;/p&gt;
&lt;h3 id=&#34;规则链&#34;&gt;规则链&lt;/h3&gt;
&lt;p&gt;Netfilter 模块默认定义了五种类型的 Hook：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;PREROUTING&lt;/li&gt;
&lt;li&gt;INPUT&lt;/li&gt;
&lt;li&gt;FORWARD&lt;/li&gt;
&lt;li&gt;OUTPUT&lt;/li&gt;
&lt;li&gt;POSTROUTING&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;在 Netfilter 里，Hook 也称为 Chain，规则链&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我们可以从数据包的来源和走向入手来进行分析这条五条规则链的设计。
首先，数据包按照来源可以分成 Incoming 和 Outgoing 这两种类型。
Incoming 数据包是指其他网卡发来的数据包。这类数据包可能直接奔向用户态的程序，
也有可能被内核转发到其他机器或者其他网卡上，这需要内核做路由判定。&lt;/p&gt;
&lt;p&gt;而 Outgoing 数据包是用户态程序准备要发送的数据包。
数据包到达内核之后，内核会为它选择合适的网卡和端口，在此之后便会一层层地穿过协议栈，内核在此过程之中会做出路由判定。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一般情况下，客户端所使用的高端口号。在 Linux 下，我们可以通过 &lt;code&gt;cat /proc/sys/net/ipv4/ip_local_port_range&lt;/code&gt; 查看系统会随机使用的端口号范围。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;需要注意的是，如果这是内网和外网之间的通信，内核会使用到 NAT 技术来对地址进行转化。
对于 Incoming 数据包而言，内核路由前需要对数据包进行 Destination NAT 转化。
同理，数据包在路由之后也需要做 Source NAT 转化。&lt;/p&gt;
&lt;p&gt;根据上面的分析，可以得到以下结论：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Incoming 数据包的目的地就在本地：PREROUTING -&amp;gt; INPUT&lt;/li&gt;
&lt;li&gt;Incoming 数据包需要转发：PREROUTING -&amp;gt; FORWARD -&amp;gt; POSTROUTING&lt;/li&gt;
&lt;li&gt;Outgoing 数据包：OUTPUT -&amp;gt; POSTROUTING&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不同走向的数据包都必定会通过以上五个环节中的部分环节，只要系统管理员在五个环节中设置关卡，就可以做到系统的访问控制。&lt;/p&gt;
&lt;h3 id=&#34;功能表&#34;&gt;功能表&lt;/h3&gt;
&lt;p&gt;为了更好地管理访问控制规则，Netfilter 制定 &lt;strong&gt;功能表&lt;/strong&gt; 来定义和区分不同功能的规则。&lt;/p&gt;
&lt;p&gt;Netfilter 一共有五种功能表：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;raw: 当内核启动 &lt;code&gt;ip_conntrack&lt;/code&gt; 模块以后，所有信息都会被追踪，raw 却是用来设置不追踪某些数据包&lt;/li&gt;
&lt;li&gt;mangle: 用来设置或者修改数据包的 IP 头信息&lt;/li&gt;
&lt;li&gt;nat: 用来设置主机的 NAT 规则，用来修改数据包的源地址和目的地址&lt;/li&gt;
&lt;li&gt;filter: 通常情况下，用来制定接收、转发、丢弃和拒绝数据包的规则&lt;/li&gt;
&lt;li&gt;security: 安全相关&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;由于 filter 和 nat 基本能满足大部分的访问控制需求，加上篇幅的原因，接下来只会介绍 filter 和 nat 这两张功能表。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不同的功能表有内置的规则链。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;filter: INPUT／OUTPUT ／FORWARD&lt;/li&gt;
&lt;li&gt;nat: PREROUTING ／INPUT／OUTPUT／POSTROUTING&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Linux 内核 2.6.34 开始给 nat 功能表引入了 INPUT 规则链，具体详情请查看 &lt;a href=&#34;http://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=c68cd6cc21eb329c47ff020ff7412bf58176984e&#34;&gt;Commit&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不同来源的数据包的走向不同，触发的规则链也不同。&lt;/p&gt;
&lt;h4 id=&#34;incoming-数据包&#34;&gt;Incoming 数据包&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;+----------------------+                               +-------------------------------------+
| chain: PREROUTING    |                               | chain: INPUT                        |
|                      |      +==================+     |                                     |      +===============+
| table:               | --&amp;gt;  + Routing Decision + --&amp;gt; | table:                              | --&amp;gt;  + Local Process +
| raw -&amp;gt; mangle -&amp;gt; nat |      +=========+========+     | mangle -&amp;gt; filter -&amp;gt; security -&amp;gt; nat |      +===============+
+----------------------+                |              +-------------------------------------+
                                        |
                                        v
                        +------------------------------+     +---------------------+
                        | chain: FORWARD               |     | chain: POSTROUTING  |
                        |                              |     |                     |     +=========+
                        | table:                       | --&amp;gt; | table:              | --&amp;gt; + Network +
                        | mangle -&amp;gt; filter -&amp;gt; security |     | mangle -&amp;gt; nat       |     +=========+
                        +------------------------------+     +---------------------+
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;为了能使 FORWARD 生效，请确保 &lt;code&gt;cat /proc/sys/net/ipv4/ip_forward&lt;/code&gt; 为1。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&#34;outgoing-数据包&#34;&gt;Outgoing 数据包&lt;/h4&gt;
&lt;pre&gt;&lt;code&gt;+===============+     +==================+     +----------------------+     +==================+     +--------------------+      +----------------------+     +=========+
+ Local Process + --&amp;gt; + Routing Decision + --&amp;gt; | chain: OUTPUT        | --&amp;gt; + Routing Decision + --&amp;gt; | chain: OUTPUT      | --&amp;gt;  | chain: POSTROUTING   | --&amp;gt; + Network +
+===============+     +=========+========+     |                      |     +=========+========+     |                    |      |                      |     +=========+
                                               | table:               |                              | table:             |      | table:               |
                                               | raw -&amp;gt; mangle -&amp;gt; nat |                              | filter -&amp;gt; security |      | mangle -&amp;gt; nat        |
                                               +----------------------+                              +--------------------+      +----------------------+
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;第一个路由判定主要是收集数据发送前的必要信息，比如所要使用的网卡、IP 地址以及端口号。
当数据包触发了协议栈中的规则链时，内核将会遍历不同的功能表（顺序如上图所示），比如 Incoming 数据包触发 PREROUTING 规则链时，内核会先执行 raw 功能表中的 PREROUTING 规则链，其次 mangle 功能表，最后才是 nat 功能表。
Netfilter 还允许系统管理员创建自己的规则链，这样可以在内置的规则链中进一步划分规则。&lt;/p&gt;
&lt;h3 id=&#34;规则&#34;&gt;规则&lt;/h3&gt;
&lt;p&gt;功能表包含了多条规则链，而每一条规则链包含多条规则。&lt;/p&gt;
&lt;p&gt;规则包含了 &lt;strong&gt;匹配标准&lt;/strong&gt; 和 &lt;strong&gt;具体动作&lt;/strong&gt;。内核会依次遍历规则链中的规则。&lt;/p&gt;
&lt;p&gt;当数据包满足某一条规则的匹配标准时，内核将会执行规则所制定的具体动作。&lt;/p&gt;
&lt;p&gt;比如系统管理员设置了“来自a.b.c.d的连接可以接收”这样的一条规则，其中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;来自a.b.c.d的连接&lt;/strong&gt; 指的是 匹配标准&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接收&lt;/strong&gt; 是 具体动作&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;当具体动作只是用来 &lt;strong&gt;记录日志&lt;/strong&gt; 或者 &lt;strong&gt;标记数据包&lt;/strong&gt; 时，表明该动作不具有 &lt;strong&gt;终结&lt;/strong&gt; 特性，内核还是会继续遍历规则链中剩下的规则。否则，当内核匹配上了具有终结特性动作的规则时，内核执行完具体动作之后，将停止遍历剩下的规则。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;具体动作分为 终结 和 非终结 两种类型，其中非终结类型使用较多的一种是跳到自定义的规则链上遍历规则。&lt;/p&gt;
&lt;p&gt;所谓终结特性是指不会影响到数据包的命运。所以条件越苛刻的规则应该放越前面。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;当规则链中没有规则，或者是数据包没有满足任何一条终结特性的规则时，内核将会采用规则链的 &lt;strong&gt;策略&lt;/strong&gt; 来决定是否接收该数据包。&lt;/p&gt;
&lt;p&gt;策略一共有两种：接收和丢弃。从另外一个角度看，规则链的策略体现出访问控制策略的设计：&lt;strong&gt;通&lt;/strong&gt; 和 &lt;strong&gt;堵&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;对于通策略而言，整个系统的大门是关闭着的，只有系统管理员赋予你权限才能访问。而堵则是整个系统的大门都是敞开着的，而规则将用来限制一些用户的访问。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;自定义的规则链并不存在策略，当出现没有规则匹配或者数据包不满足规则时，将会跳回上一级，类似于函数调用栈。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;iptables&#34;&gt;iptables&lt;/h3&gt;
&lt;p&gt;iptables 是 Netfilter 模块提供的命令接口，系统管理员可以通过它来配置各种访问控制规则。在定义规则时，可以参考以下模版。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# list rules in one table
# iptables -t table -nvL

# 查看 nat 功能表下规则
# iptables -t nat -nvL

# append new rule
# iptables [-t table] -A chain matchCretira -j action

# 系统管理员要限制来自 a.b.c.d IP 地址的访问
iptables -t filter -A INPUT -s a.b.c.d/n -j REJECT

# delete rule
# iptables [-t table] -D chain ruleNum

# 需要删除 filter/OUTPUT 的第二条规则
iptables -t filter -D OUTPUT 2
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;常用的具体动作有：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;filter 功能表：ACCEPT／DROP／REJECT／RETURN／LOG&lt;/li&gt;
&lt;li&gt;nat 功能表：SNAT／DNAT／REJECT／MASQUERADE／LOG&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;man iptables&lt;/code&gt; 能提供很多信息。但说明文档始终没有更新 nat 功能表添加了 INPUT 规则链。。。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;初体验&#34;&gt;初体验&lt;/h3&gt;
&lt;p&gt;为了保护本地环境以及模拟多节点环境，以下实验过程都在虚拟机上运行，并在虚拟机上利用Docker 来模拟多节点的环境。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# docker version | grep &#39;Version:&#39; -B 1
Client:
 Version:         1.12.5
--
Server:
 Version:         1.12.5
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;整个网络模型如下图所示：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;+---------------------------------------+
| +----------------+ +----------------+ |
| | Container #1   | | Container #2   | |
| | IP: 172.17.0.2 | | IP: 172.17.0.3 | |
| +-------------+--+ +--+-------------+ |
|               |       |               |
|               v       v               |
|           +---+-------+----+          |
|           | Bridge docker0 |          |
|           | IP: 172.17.0.1 |          |
|           +-------++-------+          |
| The               ||                  |
| Box     +--------------------+        |
+---------| Host-Only  Adapter |--------+
          | IP: 192.168.33.100 |
          +--------------------+
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;整个网络将虚拟机作为防火墙，初始状态下，不开放任何端口，将 filter 的三条内置链的策略为 DROP。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# iptables -t filter -P INPUT DROP
# iptables -t filter -P OUTPUT DROP
# iptables -t filter -P FOPWARD DROP
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;设置完以后，你会发现你连 ping 都 ping 不通了。。。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ wfu at wfu-mac in ~/workspace/docs
$ ping -c 3 192.168.33.100
PING 192.168.33.100 (192.168.33.100): 56 data bytes
Request timeout for icmp_seq 0
Request timeout for icmp_seq 1

--- 192.168.33.100 ping statistics ---
3 packets transmitted, 0 packets received, 100.0% packet loss
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;接下来的任务是能在 Mac 本地访问虚拟机里的 Docker Container，其中 Container 的启动方式如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# docker run -it -d ubuntu-nw python -m SimpleHTTPServer 8000
3301547d70b356223688fd9e38a1925ba90028084a44775bf79422be624c486b

# docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
3301547d70b3        ubuntu-nw           &amp;quot;python -m SimpleHTTP&amp;quot;   9 seconds ago       Up 8 seconds                            boring_borg
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id=&#34;开放22端口&#34;&gt;开放22端口&lt;/h3&gt;
&lt;p&gt;这台虚拟机默认是没有开启桌面。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# stty size
25 80
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在25 * 80这样的窗口里操作系统实在是太痛苦了。
在不启动桌面的情况，有必要通过远程登陆来改善下体验。&lt;/p&gt;
&lt;p&gt;虚拟机上已经预先装好了ssh server，只需要开放22端口即可。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# iptables -t filter -A INPUT -d 192.168.33.100 -p tcp --dport 22 -j ACCEPT
# iptables -t filter -A OUTPUT -s 192.168.33.100 -p tcp --sport 22 -j ACCEPT
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;为什么需要两条命令？&lt;/p&gt;
&lt;p&gt;回顾之前的内容，数据包穿过 INPUT 规则链后会被本地程序所消费。该数据包的生命周期就结束了，访问者接收到的数据包是本地程序所产生，这两者需要区分开。
第一条命令是系统管理员发给访问者的数据包的通行证，数据包到达 ssh server 之后就不复存在了，通行证也就不存在了。
ssh server 产生的数据包系统并不认识，它在穿过 OUTPUT 规则链时，如果没有通行证的话，就会被内核“吃掉”，永远都回不到访问者。
这需要两边都打通才能形成一个回路。&lt;/p&gt;
&lt;p&gt;好了，马上登陆虚拟机。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ wfu at wfu-mac in ~/workspace/docs
$ ssh root@192.168.33.100
root@192.168.33.100&#39;s password:
Last login: Fri Mar  3 18:04:55 2017 from 192.168.33.1
[root@localhost ~]# stty size
72 278
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;可以在必要的时候记录日志。&lt;/p&gt;
&lt;p&gt;比如 &lt;code&gt;iptables -t filter -I INPUT 1 -p icmp -j LOG --log-prefix &#39;filter-input:&#39;&lt;/code&gt;，只要 &lt;a href=&#34;https://en.wikipedia.org/wiki/Internet_Control_Message_Protocol&#34;&gt;ICMP&lt;/a&gt; 数据包触发了 filter 功能表中的 INPUT 规则链，那么系统将会记录下该数据包的基本信息。&lt;/p&gt;
&lt;p&gt;然后通过 &lt;code&gt;tail -f /var/log/messages&lt;/code&gt; 来查看日志。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;如何访问容器&#34;&gt;如何访问容器&lt;/h3&gt;
&lt;p&gt;在创建 Container 的时候，如果不制定 Network 类型，那么 Daemon 会自动将 Container 挂到 docker0 下面，并形成了一个 &lt;code&gt;172.17.0.0/16&lt;/code&gt; 子网。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# docker network ls
NETWORK ID          NAME                DRIVER              SCOPE
6786e7d4c36a        bridge              bridge              local
f16e611d5715        host                host                local
912ae96541e9        none                null                local

# docker network inspect bridge
[
    {
        &amp;quot;Name&amp;quot;: &amp;quot;bridge&amp;quot;,
        &amp;quot;Id&amp;quot;: &amp;quot;6786e7d4c36acbd9d359289f90bd737bfcb21e74a5e467769e45fa9f732954f2&amp;quot;,
        &amp;quot;Scope&amp;quot;: &amp;quot;local&amp;quot;,
        &amp;quot;Driver&amp;quot;: &amp;quot;bridge&amp;quot;,
        &amp;quot;EnableIPv6&amp;quot;: false,
        &amp;quot;IPAM&amp;quot;: {
            &amp;quot;Driver&amp;quot;: &amp;quot;default&amp;quot;,
            &amp;quot;Options&amp;quot;: null,
            &amp;quot;Config&amp;quot;: [
                {
                    &amp;quot;Subnet&amp;quot;: &amp;quot;172.17.0.0/16&amp;quot;,
                    &amp;quot;Gateway&amp;quot;: &amp;quot;172.17.0.1&amp;quot;
                }
            ]
        },
        &amp;quot;Internal&amp;quot;: false,
        &amp;quot;Containers&amp;quot;: {},
        &amp;quot;Options&amp;quot;: {
            &amp;quot;com.docker.network.bridge.default_bridge&amp;quot;: &amp;quot;true&amp;quot;,
            &amp;quot;com.docker.network.bridge.enable_icc&amp;quot;: &amp;quot;true&amp;quot;,
            &amp;quot;com.docker.network.bridge.enable_ip_masquerade&amp;quot;: &amp;quot;true&amp;quot;,
            &amp;quot;com.docker.network.bridge.host_binding_ipv4&amp;quot;: &amp;quot;0.0.0.0&amp;quot;,
            &amp;quot;com.docker.network.bridge.name&amp;quot;: &amp;quot;docker0&amp;quot;,
            &amp;quot;com.docker.network.driver.mtu&amp;quot;: &amp;quot;1500&amp;quot;
        },
        &amp;quot;Labels&amp;quot;: {}
    }
]
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;而 Mac 和 虚拟机在 &lt;code&gt;192.168.33.0/24&lt;/code&gt; 子网内，想要在 Mac 访问虚拟机上的 Container，需要用 Destination NAT 转发请求，所以只需要关注 &lt;code&gt;PREROUTING&lt;/code&gt;／&lt;code&gt;FORWARD&lt;/code&gt; 这两条规则链即可。&lt;/p&gt;
&lt;p&gt;Docker Daemon 启动以后会自动在 Netfilter 添加访问控制规则。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# iptables -t nat -nvL
Chain PREROUTING (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 DOCKER     all  --  *      *       0.0.0.0/0            0.0.0.0/0            ADDRTYPE match dst-type LOCAL

Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain OUTPUT (policy ACCEPT 1 packets, 128 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 DOCKER     all  --  *      *       0.0.0.0/0           !127.0.0.0/8          ADDRTYPE match dst-type LOCAL

Chain POSTROUTING (policy ACCEPT 1 packets, 128 bytes)
 pkts bytes target     prot opt in     out     source               destination
    0     0 MASQUERADE  all  --  *      !docker0  172.17.0.0/16        0.0.0.0/0

Chain DOCKER (2 references)
 pkts bytes target     prot opt in     out     source               destination
    0     0 RETURN     all  --  docker0 *       0.0.0.0/0            0.0.0.0/0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;PREROUTING 规则链只有一条规则，只要目的地址是本地地址，就跳到 DOCKER 这条自定义规则链中。
DOCKER 规则链中，只要数据包到达 docker0 网卡，就直接返回到上一层。&lt;/p&gt;
&lt;p&gt;Mac 发来的数据包不会直接到达 docker0 网卡，而是 enp0s8 网卡。所以需要在 DOCKER 规则链中添加对来自 &lt;code&gt;192.168.33.0/24&lt;/code&gt; 的 Destination NAT 规则。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;nat 功能表中 OUTPUT 规则链是用来对本地数据进行 DNAT／REDIRECT 操作，因为系统内部的通信一般情况不会穿过 PREROUTING。而在 DOCKER 规则链中添加对外部地址的 DNAT 规则并不会在 OUTPUT 规则链中被匹配。&lt;/p&gt;
&lt;p&gt;一旦通信双方通过 nat 功能表建立连接，内核将不会使用 nat 功能表上的规则过滤该连接上的数据包。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre&gt;&lt;code&gt;# ip addr
1: lo: &amp;lt;LOOPBACK,UP,LOWER_UP&amp;gt; mtu 65536 qdisc noqueue state UNKNOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp0s8: &amp;lt;BROADCAST,MULTICAST,UP,LOWER_UP&amp;gt; mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 08:00:27:20:9f:cb brd ff:ff:ff:ff:ff:ff
    inet 192.168.33.100/24 brd 192.168.33.255 scope global enp0s8
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:fe20:9fcb/64 scope link
       valid_lft forever preferred_lft forever
3: docker0: &amp;lt;NO-CARRIER,BROADCAST,MULTICAST,UP&amp;gt; mtu 1500 qdisc noqueue state DOWN
    link/ether 02:42:c3:e1:87:ab brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 scope global docker0
       valid_lft forever preferred_lft forever
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;数据包经过 PREROUTING 规则链之后，被路由到了 FORWARD 规则链。&lt;/p&gt;
&lt;p&gt;数据包在 enp0s3 网卡与 docker0 网卡的转发，会匹配到 FORWARD 规则链的第3和第4条规则，这里不需要额外的配置。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# iptables -t filter -xnvL
Chain INPUT (policy ACCEPT 9 packets, 548 bytes)
    pkts      bytes target     prot opt in     out     source               destination

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
    pkts      bytes target     prot opt in     out     source               destination
     108    13856 DOCKER-ISOLATION  all  --  *      *       0.0.0.0/0            0.0.0.0/0
      58     3736 DOCKER     all  --  *      docker0  0.0.0.0/0            0.0.0.0/0
       0        0 ACCEPT     all  --  *      docker0  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
      50    10120 ACCEPT     all  --  docker0 !docker0  0.0.0.0/0            0.0.0.0/0
       0        0 ACCEPT     all  --  docker0 docker0  0.0.0.0/0            0.0.0.0/0

Chain OUTPUT (policy ACCEPT 7 packets, 744 bytes)
    pkts      bytes target     prot opt in     out     source               destination

Chain DOCKER (1 references)
    pkts      bytes target     prot opt in     out     source               destination

Chain DOCKER-ISOLATION (1 references)
    pkts      bytes target     prot opt in     out     source               destination
     108    13856 RETURN     all  --  *      *       0.0.0.0/0            0.0.0.0/0
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;根据上面的分析，我们只需要添加下面一条规则，便可访问该 Container。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# iptables -t nat -A DOCKER -p tcp -i enp0s8 -d 192.168.33.100 --dport 80 -j DNAT --to-destination 172.17.0.2:8000
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;访问结果如下。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ wfu at wfu-mac in ~/workspace/docs
$ curl 192.168.33.100
&amp;lt;!DOCTYPE html PUBLIC &amp;quot;-//W3C//DTD HTML 3.2 Final//EN&amp;quot;&amp;gt;&amp;lt;html&amp;gt;
&amp;lt;title&amp;gt;Directory listing for /&amp;lt;/title&amp;gt;
&amp;lt;body&amp;gt;
&amp;lt;h2&amp;gt;Directory listing for /&amp;lt;/h2&amp;gt;
&amp;lt;hr&amp;gt;
&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;.dockerenv&amp;quot;&amp;gt;.dockerenv&amp;lt;/a&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;bin/&amp;quot;&amp;gt;bin/&amp;lt;/a&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;boot/&amp;quot;&amp;gt;boot/&amp;lt;/a&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;dev/&amp;quot;&amp;gt;dev/&amp;lt;/a&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;etc/&amp;quot;&amp;gt;etc/&amp;lt;/a&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;home/&amp;quot;&amp;gt;home/&amp;lt;/a&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;lib/&amp;quot;&amp;gt;lib/&amp;lt;/a&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;lib64/&amp;quot;&amp;gt;lib64/&amp;lt;/a&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;media/&amp;quot;&amp;gt;media/&amp;lt;/a&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;mnt/&amp;quot;&amp;gt;mnt/&amp;lt;/a&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;opt/&amp;quot;&amp;gt;opt/&amp;lt;/a&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;proc/&amp;quot;&amp;gt;proc/&amp;lt;/a&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;root/&amp;quot;&amp;gt;root/&amp;lt;/a&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;run/&amp;quot;&amp;gt;run/&amp;lt;/a&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;sbin/&amp;quot;&amp;gt;sbin/&amp;lt;/a&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;srv/&amp;quot;&amp;gt;srv/&amp;lt;/a&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;sys/&amp;quot;&amp;gt;sys/&amp;lt;/a&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;tmp/&amp;quot;&amp;gt;tmp/&amp;lt;/a&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;usr/&amp;quot;&amp;gt;usr/&amp;lt;/a&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&amp;quot;var/&amp;quot;&amp;gt;var/&amp;lt;/a&amp;gt;
&amp;lt;/ul&amp;gt;
&amp;lt;hr&amp;gt;
&amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;docker run -it -d ubuntu-nw -p 80:8000 python -m SimpleHTTPServer 8000&lt;/code&gt; 能帮我们完成这次访问，可以观察 Docker Daemon 都为我们做了什么。&lt;/p&gt;
&lt;/blockquote&gt;
</description>
        </item>
        
    </channel>
</rss>
