<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Epoll on JasperSao的博客</title><link>https://jasperstonnne.github.io/MyBlog/tags/epoll/</link><description>Recent content in Epoll on JasperSao的博客</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Sun, 30 Aug 2026 08:12:59 +0000</lastBuildDate><atom:link href="https://jasperstonnne.github.io/MyBlog/tags/epoll/index.xml" rel="self" type="application/rss+xml"/><item><title>io_uring（二）：QPS 实测，真的比 epoll 强吗？</title><link>https://jasperstonnne.github.io/MyBlog/p/io-uring-qps-vs-epoll/</link><pubDate>Thu, 20 Aug 2026 00:00:00 +0800</pubDate><guid>https://jasperstonnne.github.io/MyBlog/p/io-uring-qps-vs-epoll/</guid><description>&lt;img src="https://jasperstonnne.github.io/MyBlog/p/io-uring-qps-vs-epoll/cover.svg" alt="Featured image of post io_uring（二）：QPS 实测，真的比 epoll 强吗？" /&gt;&lt;h2 id="一学习目标"&gt;一、学习目标&#10;&lt;/h2&gt;&lt;p&gt;这部分学习主要围绕两个目标展开：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;编写一个 TCP 客户端测试工具，对比 epoll 和 io_uring 服务端的性能。&lt;/li&gt;&#10;&lt;li&gt;整理网络相关面试题，重点理解 TCP、UDP、建链、断链、并发及分包粘包问题。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="二qps-测试工具需求"&gt;二、QPS 测试工具需求&#10;&lt;/h2&gt;&lt;p&gt;客户端需要支持以下参数：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;-n request_num 请求总数&#10;-t thread_num 线程数量&#10;-c connection_num 连接数量&#10;-s server_ip 服务端 IP&#10;-p server_port 服务端端口&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;使用示例：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;./test_qps_tcpclient \&#10; -s 172.16.145.129 \&#10; -p 2048 \&#10; -t 50 \&#10; -c 100 \&#10; -n 10000&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;测试采用 Echo 模型：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;客户端向服务端发送一段数据。&lt;/li&gt;&#10;&lt;li&gt;服务端将数据原样返回。&lt;/li&gt;&#10;&lt;li&gt;客户端比较发送和接收的数据。&lt;/li&gt;&#10;&lt;li&gt;数据一致则请求成功，否则失败。&lt;/li&gt;&#10;&lt;li&gt;最后统计总耗时和 QPS。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;QPS 的计算方式为：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;QPS = 完成的请求数 ÷ 耗时（秒）&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;代码中使用毫秒计时，因此写成：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;qps = request_num * 1000 / time_used_ms;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;&#10;&lt;h2 id="三测试数据的构造"&gt;三、测试数据的构造&#10;&lt;/h2&gt;&lt;p&gt;测试字符串为：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;#define TEST_MESSAGE \&#10;&amp;#34;ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz\r\n&amp;#34;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这段字符串的长度正好是 64 字节：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;26 个大写字母&#10;+ 10 个数字&#10;+ 26 个小写字母&#10;+ \r\n 两个字符&#10;= 64 字节&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;通过重复复制，可以构造不同大小的请求：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;for (i = 0; i &amp;lt; 2; i++) {&#10; strcpy(&#10; wbuffer + i * strlen(TEST_MESSAGE),&#10; TEST_MESSAGE&#10; );&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;重复次数与数据长度的关系如下：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;重复次数&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;数据长度&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;64 字节&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;128 字节&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;256 字节&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;8&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;512 字节&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;16&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1024 字节&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;测试大包时，需要关闭服务端和客户端的数据打印，否则终端输出本身会严重影响测试结果。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="四64-bytes-测试"&gt;四、64 bytes 测试&#10;&lt;/h2&gt;&lt;p&gt;测试 100 万次、每次发送 64 bytes 时，结果如下：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;数据长度&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;服务端模型&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;成功&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;失败&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;耗时&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;QPS&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;64 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;epoll&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5132 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;194855&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;64 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;io_uring&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4400 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;227272&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;在 64 bytes 测试中：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;epoll QPS：194855&#10;io_uring QPS：227272&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;io_uring 的 QPS 比 epoll 高约 16.6%。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="五不同数据长度下的测试"&gt;五、不同数据长度下的测试&#10;&lt;/h2&gt;&lt;h3 id="1-io_uring-服务端"&gt;1. io_uring 服务端&#10;&lt;/h3&gt;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;数据长度&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;成功&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;失败&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;耗时&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;QPS&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;128 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4337 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;230574&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;256 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4268 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;234301&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;512 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4307 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;232180&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1024 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;4390 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;227790&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;从结果来看，在 128～1024 bytes 范围内，io_uring 的 QPS 比较稳定，基本维持在 22 万～23 万。&lt;/p&gt;&#10;&lt;p&gt;1024 bytes 测试时，笔记中怀疑可能出现“粘包”。&lt;/p&gt;&#10;&lt;p&gt;更准确地说，TCP 本身是字节流，没有一条 &lt;code&gt;send()&lt;/code&gt; 必须对应一条 &lt;code&gt;recv()&lt;/code&gt; 的保证。一次发送的 1024 bytes，可能需要多次 &lt;code&gt;recv()&lt;/code&gt; 才能完整读取，因此客户端必须实现循环接收。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="2-epoll-服务端阻塞-io"&gt;2. epoll 服务端——阻塞 I/O&#10;&lt;/h3&gt;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;数据长度&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;成功&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;失败&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;耗时&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;QPS&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;128 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;74935 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;13344&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;256 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;147749 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;6768&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;512 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;296207 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;3376&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1024 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;721163 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1386&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;这组结果明显异常：数据长度每增加一倍，耗时几乎也增加一倍，QPS 则接近减半。&lt;/p&gt;&#10;&lt;p&gt;问题在于 epoll 服务端使用了阻塞 socket。&lt;/p&gt;&#10;&lt;p&gt;epoll 的作用是通知程序某个 fd 当前可读或者可写。但即使 fd 已经可读，也不代表接下来所有的 &lt;code&gt;recv()&lt;/code&gt; 都不会阻塞。&lt;/p&gt;&#10;&lt;p&gt;尤其是在循环读取时，如果已经把当前到达的数据读完，又调用了一次阻塞式 &lt;code&gt;recv()&lt;/code&gt;，整个事件循环就可能停在某一个连接上，导致其他连接无法及时处理。&lt;/p&gt;&#10;&lt;p&gt;所以 epoll 一般需要与非阻塞 socket 配合：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;epoll 通知 fd 可读&#10; ↓&#10;循环调用 recv&#10; ↓&#10;读取当前已经到达的数据&#10; ↓&#10;recv 返回 EAGAIN&#10; ↓&#10;处理下一个就绪事件&#10;&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;&#10;&lt;h3 id="3-epoll-服务端非阻塞-io"&gt;3. epoll 服务端——非阻塞 I/O&#10;&lt;/h3&gt;&lt;p&gt;将 socket 设置成非阻塞后重新测试：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;数据长度&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;成功&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;失败&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;耗时&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;QPS&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;128 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5198 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;192381&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;256 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5144 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;194401&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;512 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5141 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;194514&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1024 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1000000&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;0&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5216 ms&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;191717&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;修改后，epoll 的 QPS 恢复到约 19 万，并且不再随数据长度增加而大幅下降。&lt;/p&gt;&#10;&lt;p&gt;这说明前一组数据反映的主要不是 epoll 本身的性能，而是“epoll 配合阻塞 I/O”造成的异常。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="六epoll-与-io_uring-的完整对比"&gt;六、epoll 与 io_uring 的完整对比&#10;&lt;/h2&gt;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;数据长度&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;io_uring QPS&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;epoll QPS&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;64 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;227272&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;194855&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;128 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;230574&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;192381&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;256 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;234301&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;194401&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;512 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;232180&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;194514&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;1024 bytes&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;227790&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;191717&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;从测试结果来看：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;64 bytes 时，io_uring 比 epoll 高约 16.6%；&lt;/li&gt;&#10;&lt;li&gt;128～1024 bytes 时，io_uring 大约比 epoll 高 19%～21%；&lt;/li&gt;&#10;&lt;li&gt;所有测试包大小下，io_uring 的 QPS 都高于 epoll；&lt;/li&gt;&#10;&lt;li&gt;两种模型在改正 epoll 的阻塞 I/O 问题后，QPS 都没有随着数据大小增加而发生明显下降。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;二者的处理思路不同。&lt;/p&gt;&#10;&lt;h3 id="epoll"&gt;epoll&#10;&lt;/h3&gt;&lt;p&gt;epoll 是就绪通知模型：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;先等待 fd 可读或可写&#10;→ 得到就绪事件&#10;→ 应用程序再调用 recv/send&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;也就是说，epoll 负责告诉应用程序“现在可以尝试进行 I/O”，真正的数据读写仍然由应用程序发起。&lt;/p&gt;&#10;&lt;h3 id="io_uring"&gt;io_uring&#10;&lt;/h3&gt;&lt;p&gt;io_uring 更接近完成通知模型：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;应用程序先提交 recv/send 请求&#10;→ 内核执行 I/O&#10;→ I/O 完成后通知应用程序&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;应用程序可以提前向内核提交 I/O 请求，然后通过完成队列取得结果。&lt;/p&gt;&#10;&lt;p&gt;根据本次测试，可以得到以下结论：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;在当前服务端实现、当前客户端模型和当前测试环境下，io_uring 的 QPS 全面高于非阻塞 epoll，整体高约 17%～21%。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;这个结论应当限定在本次测试条件内，不能仅凭这一轮测试认为所有业务中 io_uring 都一定优于 epoll。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="七业务中的数据包大小"&gt;七、业务中的数据包大小&#10;&lt;/h2&gt;&lt;p&gt;实际业务中可能同时存在小包和大包。&lt;/p&gt;&#10;&lt;h3 id="1-心跳包"&gt;1. 心跳包&#10;&lt;/h3&gt;&lt;p&gt;客户端和服务端之间通常会存在心跳包。&lt;/p&gt;&#10;&lt;p&gt;心跳包只用于检测连接是否仍然存活，因此数据量通常很小，可能只有几个字节。&lt;/p&gt;&#10;&lt;p&gt;这类场景更关注：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;大量连接的管理能力；&lt;/li&gt;&#10;&lt;li&gt;小包的处理效率；&lt;/li&gt;&#10;&lt;li&gt;I/O 事件通知和调度开销；&lt;/li&gt;&#10;&lt;li&gt;服务端是否能够及时发现断开的连接。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="2-视频和图片传输"&gt;2. 视频和图片传输&#10;&lt;/h3&gt;&lt;p&gt;视频或者较大的图片需要被拆分成多个数据块。&lt;/p&gt;&#10;&lt;p&gt;应用层可以将每一个业务包组织成约 1 KB，再通过协议头标识数据长度、类型和序号。&lt;/p&gt;&#10;&lt;p&gt;这类场景除了 QPS，还需要关注：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;吞吐量；&lt;/li&gt;&#10;&lt;li&gt;单次发送的数据大小；&lt;/li&gt;&#10;&lt;li&gt;分包和组包；&lt;/li&gt;&#10;&lt;li&gt;内存复制；&lt;/li&gt;&#10;&lt;li&gt;慢连接对事件循环的影响。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;因此，也可以结合不同数据大小下的测试结果，理解 epoll 和 io_uring 的差别。&lt;/p&gt;&#10;&lt;h3 id="3-百万并发测试"&gt;3. 百万并发测试&#10;&lt;/h3&gt;&lt;p&gt;后续还可以使用 io_uring 和 epoll 测试百万并发，包括：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;建立相同数量连接所需要的时间；&lt;/li&gt;&#10;&lt;li&gt;相同并发连接数下的 QPS；&lt;/li&gt;&#10;&lt;li&gt;百万连接下的内存占用；&lt;/li&gt;&#10;&lt;li&gt;建链事件的处理能力；&lt;/li&gt;&#10;&lt;li&gt;断链事件的处理能力。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;断开连接涉及 TCP 协议栈中的状态转换，但应用层仍然需要正确执行 &lt;code&gt;close()&lt;/code&gt;、移除连接状态，并释放自己管理的连接资源。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="八qps-客户端代码详解"&gt;八、QPS 客户端代码详解&#10;&lt;/h2&gt;&lt;h3 id="1-测试上下文"&gt;1. 测试上下文&#10;&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;struct test_context_s {&#10; char serverip[16];&#10; int port;&#10; int threadnum;&#10; int connection;&#10; int requestion;&#10;&#10;#if 1&#10; int failed;&#10;#endif&#10;};&#10;&#10;typedef struct test_context_s test_context_t;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个结构保存测试参数：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;serverip&lt;/code&gt;：服务端 IPv4 地址；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;port&lt;/code&gt;：服务端端口；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;threadnum&lt;/code&gt;：测试线程数；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;connection&lt;/code&gt;：期望建立的连接数量；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;requestion&lt;/code&gt;：请求总数；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;failed&lt;/code&gt;：失败请求数。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;code&gt;requestion&lt;/code&gt; 想表达的应该是请求数量，也就是 &lt;code&gt;request_count&lt;/code&gt; 或者 &lt;code&gt;requests&lt;/code&gt;，只是命名不太准确，不影响程序运行。&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;serverip&lt;/code&gt; 的长度是 16，刚好可以保存最长的 IPv4 字符串和字符串结尾的 &lt;code&gt;\0&lt;/code&gt;：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;255.255.255.255\0&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;不过，代码使用的是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;strcpy(ctx.serverip, optarg);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果传入的参数超过数组长度，就会发生越界写入。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="2-建立-tcp-连接"&gt;2. 建立 TCP 连接&#10;&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;int connect_tcpserver(&#10; const char *ip,&#10; unsigned short port&#10;)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;函数首先创建 TCP socket：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;int connfd = socket(AF_INET, SOCK_STREAM, 0);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;AF_INET&lt;/code&gt; 表示使用 IPv4；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;SOCK_STREAM&lt;/code&gt; 表示使用 TCP；&lt;/li&gt;&#10;&lt;li&gt;返回值 &lt;code&gt;connfd&lt;/code&gt; 是连接对应的文件描述符。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;然后构造服务端地址：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;struct sockaddr_in tcpserver_addr;&#10;&#10;memset(&#10; &amp;amp;tcpserver_addr,&#10; 0,&#10; sizeof(struct sockaddr_in)&#10;);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;清零后设置地址类型：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;tcpserver_addr.sin_family = AF_INET;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;设置服务端 IP：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;tcpserver_addr.sin_addr.s_addr = inet_addr(ip);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;inet_addr()&lt;/code&gt; 将类似下面的 IPv4 字符串：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;172.16.145.129&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;转换成网络地址。&lt;/p&gt;&#10;&lt;p&gt;设置端口：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;tcpserver_addr.sin_port = htons(port);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;htons()&lt;/code&gt; 将端口从主机字节序转换成网络字节序。&lt;/p&gt;&#10;&lt;p&gt;最后调用：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;int ret = connect(&#10; connfd,&#10; (struct sockaddr *)&amp;amp;tcpserver_addr,&#10; sizeof(struct sockaddr_in)&#10;);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果连接成功，返回 &lt;code&gt;connfd&lt;/code&gt;：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;return connfd;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果连接失败，返回 &lt;code&gt;-1&lt;/code&gt;：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;if (ret) {&#10; perror(&amp;#34;connect\n&amp;#34;);&#10; return -1;&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当前代码没有检查 &lt;code&gt;socket()&lt;/code&gt; 是否创建失败。&lt;/p&gt;&#10;&lt;p&gt;另外，如果 &lt;code&gt;connect()&lt;/code&gt; 失败，已经创建的 &lt;code&gt;connfd&lt;/code&gt; 没有关闭，会造成文件描述符泄漏。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="3-时间计算"&gt;3. 时间计算&#10;&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;#define TIME_SUB_MS(tv1, tv2) \&#10; ((tv1.tv_sec - tv2.tv_sec) * 1000 + \&#10; (tv1.tv_usec - tv2.tv_usec) / 1000)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个宏用来计算两个 &lt;code&gt;timeval&lt;/code&gt; 之间相差的毫秒数：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;秒差 × 1000 + 微秒差 ÷ 1000&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;主函数在线程创建前记录开始时间：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;struct timeval tv_begin;&#10;gettimeofday(&amp;amp;tv_begin, NULL);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;所有线程结束后记录结束时间：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;struct timeval tv_end;&#10;gettimeofday(&amp;amp;tv_end, NULL);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;然后计算：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;int time_used = TIME_SUB_MS(tv_end, tv_begin);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因此，当前统计结果包含：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;创建线程的时间；&lt;/li&gt;&#10;&lt;li&gt;建立 TCP 连接的时间；&lt;/li&gt;&#10;&lt;li&gt;发送和接收请求的时间；&lt;/li&gt;&#10;&lt;li&gt;等待线程退出的时间。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;所以当前 QPS 是一轮完整压测的整体 QPS，并不是纯粹的数据收发 QPS。&lt;/p&gt;&#10;&lt;p&gt;如果后续需要分别测试建链性能和请求处理性能，就需要把建立连接和发送请求分开计时。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="4-测试消息"&gt;4. 测试消息&#10;&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;#define TEST_MESSAGE \&#10;&amp;#34;ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnopqrstuvwxyz\r\n&amp;#34;&#10;&#10;#define RBUFFER_LENGTH 2048&#10;#define WBUFFER_LENGTH 2048&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;TEST_MESSAGE&lt;/code&gt; 是 64 bytes。&lt;/p&gt;&#10;&lt;p&gt;读缓冲区和写缓冲区都设置为 2048 bytes。&lt;/p&gt;&#10;&lt;p&gt;在当前代码中，&lt;code&gt;WBUFFER_LENGTH&lt;/code&gt; 宏并没有被使用，因为写缓冲区直接写成了：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;char wbuffer[2048] = {0};&#10;&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;&#10;&lt;h3 id="5-单次请求过程"&gt;5. 单次请求过程&#10;&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;int send_recv_tcppkt(int fd)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这个函数完成一次请求和响应。&lt;/p&gt;&#10;&lt;h4 id="构造发送数据"&gt;构造发送数据&#10;&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;char wbuffer[2048] = {0};&#10;int i = 0;&#10;&#10;for (i = 0; i &amp;lt; 2; i++) {&#10; strcpy(&#10; wbuffer + i * strlen(TEST_MESSAGE),&#10; TEST_MESSAGE&#10; );&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;由于 &lt;code&gt;TEST_MESSAGE&lt;/code&gt; 为 64 bytes，循环两次后，&lt;code&gt;wbuffer&lt;/code&gt; 中保存了 128 bytes 数据。&lt;/p&gt;&#10;&lt;p&gt;第一次复制的位置是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;wbuffer + 0&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;第二次复制的位置是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;wbuffer + 64&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因此，两段 64 bytes 的数据首尾相连，组成 128 bytes 的请求。&lt;/p&gt;&#10;&lt;h4 id="发送数据"&gt;发送数据&#10;&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;int res = send(&#10; fd,&#10; wbuffer,&#10; strlen(wbuffer),&#10; 0&#10;);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果发送失败：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;if (res &amp;lt; 0) {&#10; exit(1);&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这里的主要问题是，一次 &lt;code&gt;send()&lt;/code&gt; 不保证发送完全部数据。&lt;/p&gt;&#10;&lt;p&gt;例如：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;send(fd, buffer, 1024, 0);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;返回值可能是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;1024：全部发送完成&#10;512：只发送了 512 bytes&#10;-1：发送失败&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;所以不能只判断 &lt;code&gt;res &amp;lt; 0&lt;/code&gt;，还需要判断返回值是否等于预期发送长度。&lt;/p&gt;&#10;&lt;p&gt;正确的逻辑应当是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;记录总共需要发送的长度&#10; ↓&#10;循环调用 send&#10; ↓&#10;每次根据返回值移动缓冲区指针&#10; ↓&#10;直到所有字节发送完成&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h4 id="接收数据"&gt;接收数据&#10;&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;char rbuffer[RBUFFER_LENGTH] = {0};&#10;&#10;res = recv(&#10; fd,&#10; rbuffer,&#10; RBUFFER_LENGTH,&#10; 0&#10;);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果返回值小于或者等于 0：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;if (res &amp;lt;= 0) {&#10; exit(1);&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;recv()&lt;/code&gt; 的返回值含义是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;大于 0：本次实际读取的字节数&#10;等于 0：对端正常关闭连接&#10;小于 0：读取失败&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;和 &lt;code&gt;send()&lt;/code&gt; 一样，一次 &lt;code&gt;recv()&lt;/code&gt; 也不保证读取到完整的响应。&lt;/p&gt;&#10;&lt;p&gt;服务端即使完整返回了 1024 bytes，客户端也可能出现：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;第一次 recv：读取 300 bytes&#10;第二次 recv：读取 500 bytes&#10;第三次 recv：读取 224 bytes&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因此，需要循环接收，直到读取到预期长度。&lt;/p&gt;&#10;&lt;p&gt;这正是笔记中“很有必要重新封装一个 &lt;code&gt;recv&lt;/code&gt; 和 &lt;code&gt;send&lt;/code&gt;”的原因。&lt;/p&gt;&#10;&lt;h4 id="比较数据"&gt;比较数据&#10;&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;if (strcmp(rbuffer, wbuffer) != 0) {&#10; printf(&#10; &amp;#34;failed: &amp;#39;%s&amp;#39;!=&amp;#39;%s&amp;#39;\n&amp;#34;,&#10; rbuffer,&#10; wbuffer&#10; );&#10; return -1;&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这里通过 &lt;code&gt;strcmp()&lt;/code&gt; 比较接收数据和发送数据。&lt;/p&gt;&#10;&lt;p&gt;但网络数据本质上是一段带长度的二进制数据，使用 &lt;code&gt;strcmp()&lt;/code&gt; 的前提是两个缓冲区都以 &lt;code&gt;\0&lt;/code&gt; 结尾。&lt;/p&gt;&#10;&lt;p&gt;更稳定的比较方式是根据实际数据长度使用：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;memcmp(rbuffer, wbuffer, expected_length);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果一次 &lt;code&gt;recv()&lt;/code&gt; 只读到部分数据，那么当前 &lt;code&gt;strcmp()&lt;/code&gt; 会直接认为请求失败，即使剩余数据稍后还能继续收到。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="6-工作线程"&gt;6. 工作线程&#10;&lt;/h3&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;static void *test_qps_entry(void *arg)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;线程首先取得测试上下文：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;test_context_t *pctx =&#10; (test_context_t *)arg;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;原始笔记中出现了：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;(test\_context\_t *)arg&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果实际代码中确实包含反斜杠，需要将反斜杠删除。它也可能只是复制代码时产生的 Markdown 转义。&lt;/p&gt;&#10;&lt;h4 id="计算每个线程的连接数"&gt;计算每个线程的连接数&#10;&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;int conn_num =&#10; pctx-&amp;gt;connection / pctx-&amp;gt;threadnum;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这里理论上表示每个线程需要管理多少个连接。&lt;/p&gt;&#10;&lt;p&gt;例如：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;连接数：100&#10;线程数：50&#10;每个线程应当管理：2 个连接&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;但是，&lt;code&gt;conn_num&lt;/code&gt; 后面没有被使用。&lt;/p&gt;&#10;&lt;p&gt;实际代码只建立了一次连接：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;int connfd = connect_tcpserver(&#10; pctx-&amp;gt;serverip,&#10; pctx-&amp;gt;port&#10;);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因此，每个线程只建立一个连接。&lt;/p&gt;&#10;&lt;p&gt;这意味着：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;-t 50 -c 100&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;实际建立的不是 100 个连接，而是 50 个连接。&lt;/p&gt;&#10;&lt;p&gt;当前代码中的 &lt;code&gt;-c&lt;/code&gt; 参数实际上没有生效。&lt;/p&gt;&#10;&lt;h4 id="计算每个线程的请求数"&gt;计算每个线程的请求数&#10;&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;int count =&#10; pctx-&amp;gt;requestion / pctx-&amp;gt;threadnum;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;例如：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;请求总数：10000&#10;线程数：50&#10;每个线程：200 次请求&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;每个线程执行：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;while (i++ &amp;lt; count) {&#10; res = send_recv_tcppkt(connfd);&#10;&#10; if (res != 0) {&#10; printf(&amp;#34;send_recv_tcppkt failed\n&amp;#34;);&#10; pctx-&amp;gt;failed++;&#10; continue;&#10; }&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;线程中的请求模型为：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;发送一次请求&#10; ↓&#10;阻塞等待响应&#10; ↓&#10;收到完整响应&#10; ↓&#10;再发送下一次请求&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;所以每个连接同一时间最多只有一个未完成请求。&lt;/p&gt;&#10;&lt;p&gt;整个客户端能够同时处理的请求数量，实际上接近线程数量，而不是 &lt;code&gt;-c&lt;/code&gt; 指定的连接数量。&lt;/p&gt;&#10;&lt;h4 id="请求数量不能整除线程数"&gt;请求数量不能整除线程数&#10;&lt;/h4&gt;&lt;p&gt;如果：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;-n 10003&#10;-t 50&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;则：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;count = 10003 / 50;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;整数除法结果为：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;200&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;实际发送请求数为：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;200 × 50 = 10000&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;剩余 3 次请求不会发送。&lt;/p&gt;&#10;&lt;p&gt;但是，程序最后仍然按照 10003 计算成功数量和 QPS，所以统计结果会不准确。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="7-失败请求统计"&gt;7. 失败请求统计&#10;&lt;/h3&gt;&lt;p&gt;失败时执行：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;pctx-&amp;gt;failed++;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;所有线程共享同一个 &lt;code&gt;failed&lt;/code&gt;，但这里没有加锁，也没有使用原子变量。&lt;/p&gt;&#10;&lt;p&gt;多个线程可能同时执行：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;pctx-&amp;gt;failed++;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;例如，两个线程同时读到：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;failed = 10&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;两个线程都计算得到 11，再分别写回，最终结果可能仍然是 11，而不是正确的 12。&lt;/p&gt;&#10;&lt;p&gt;因此，当前失败数具有以下特点：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;可以粗略判断是否出现了失败；&lt;/li&gt;&#10;&lt;li&gt;不能保证失败数量准确；&lt;/li&gt;&#10;&lt;li&gt;多线程同时修改时存在数据竞争。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;笔记中提到“数据没有那么重要，只需要知道有失败的可能性”，这种处理可以用于临时测试，但最终版本如果需要准确统计失败数，仍然需要使用锁或者原子操作。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="8-命令行参数解析"&gt;8. 命令行参数解析&#10;&lt;/h3&gt;&lt;p&gt;主函数通过 &lt;code&gt;getopt()&lt;/code&gt; 解析参数：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;while (&#10; (opt = getopt(&#10; argc,&#10; argv,&#10; &amp;#34;s:p:t:c:n:?&amp;#34;&#10; )) != -1&#10;)&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;参数含义如下：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;-s：服务端 IP&#10;-p：服务端端口&#10;-t：线程数量&#10;-c：连接数量&#10;-n：请求数量&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;例如：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;case &amp;#39;s&amp;#39;:&#10; strcpy(ctx.serverip, optarg);&#10; break;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;读取 IP。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;case &amp;#39;p&amp;#39;:&#10; ctx.port = atoi(optarg);&#10; break;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;读取端口。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;case &amp;#39;t&amp;#39;:&#10; ctx.threadnum = atoi(optarg);&#10; break;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;读取线程数量。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;case &amp;#39;c&amp;#39;:&#10; ctx.connection = atoi(optarg);&#10; break;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;读取连接数量。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;case &amp;#39;n&amp;#39;:&#10; ctx.requestion = atoi(optarg);&#10; break;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;读取请求总数。&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;getopt()&lt;/code&gt; 使用了一些全局状态，因此笔记中记录它不是线程安全函数。不过当前代码只在主线程、创建工作线程之前调用，所以不会产生并发调用问题。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="9-创建工作线程"&gt;9. 创建工作线程&#10;&lt;/h3&gt;&lt;p&gt;程序首先根据线程数量申请线程 ID 数组：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;pthread_t *ptid =&#10; malloc(&#10; ctx.threadnum * sizeof(pthread_t)&#10; );&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;然后记录开始时间：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;struct timeval tv_begin;&#10;gettimeofday(&amp;amp;tv_begin, NULL);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;创建工作线程：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;for (i = 0; i &amp;lt; ctx.threadnum; i++) {&#10; pthread_create(&#10; &amp;amp;ptid[i],&#10; NULL,&#10; test_qps_entry,&#10; &amp;amp;ctx&#10; );&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;所有线程都接收同一个 &lt;code&gt;ctx&lt;/code&gt; 地址：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;&amp;amp;ctx&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;其中：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;IP、端口、线程数、连接数和请求数只读；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;failed&lt;/code&gt; 会被所有线程共同修改。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;创建完成后，主线程等待所有工作线程退出：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;for (i = 0; i &amp;lt; ctx.threadnum; i++) {&#10; pthread_join(ptid[i], NULL);&#10;}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;只有全部线程执行完毕，主线程才会继续计算总耗时。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="10-统计-qps"&gt;10. 统计 QPS&#10;&lt;/h3&gt;&lt;p&gt;所有线程结束后，记录结束时间：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;struct timeval tv_end;&#10;gettimeofday(&amp;amp;tv_end, NULL);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;计算总耗时：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;int time_used =&#10; TIME_SUB_MS(tv_end, tv_begin);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;最后输出：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;printf(&#10; &amp;#34;success: %d,failed:%d,&amp;#34;&#10; &amp;#34;time_used:%d,qps:%d\n&amp;#34;,&#10; ctx.requestion - ctx.failed,&#10; ctx.failed,&#10; time_used,&#10; ctx.requestion * 1000 / time_used&#10;);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;输出内容包括：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;成功请求数；&lt;/li&gt;&#10;&lt;li&gt;失败请求数；&lt;/li&gt;&#10;&lt;li&gt;总耗时；&lt;/li&gt;&#10;&lt;li&gt;QPS。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;当前 QPS 使用的是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;ctx.requestion * 1000 / time_used&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;这里使用了配置的请求数，而不是实际完成的请求数。&lt;/p&gt;&#10;&lt;p&gt;如果请求数不能整除线程数，或者中间出现失败，QPS 就不能准确表示真正成功完成的请求数量。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="11-资源释放"&gt;11. 资源释放&#10;&lt;/h3&gt;&lt;p&gt;主函数结束前释放线程数组：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;free(ptid);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;但工作线程建立的 TCP 连接没有关闭：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;close(connfd);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;在线程退出前，应当关闭自己创建的连接。&lt;/p&gt;&#10;&lt;p&gt;虽然进程退出时，操作系统最终会回收文件描述符，但如果后续让一个进程连续执行多轮测试，不关闭连接就会逐渐产生资源泄漏。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="九当前客户端的实际测试模型"&gt;九、当前客户端的实际测试模型&#10;&lt;/h2&gt;&lt;p&gt;虽然命令行参数中包含 &lt;code&gt;-c&lt;/code&gt;，但当前代码的实际模型是：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;主线程&#10; ├─ 工作线程 1&#10; │ └─ 一个 TCP 连接&#10; │ └─ 串行发送请求&#10; │&#10; ├─ 工作线程 2&#10; │ └─ 一个 TCP 连接&#10; │ └─ 串行发送请求&#10; │&#10; ├─ 工作线程 3&#10; │ └─ 一个 TCP 连接&#10; │ └─ 串行发送请求&#10; │&#10; └─ 其他工作线程&#10; └─ 每个线程一个 TCP 连接&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因此：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;实际连接数 = 线程数&#10;每个连接同时只有一个请求&#10;-c 参数没有生效&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;当前工具比较的是：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;多线程、每个线程使用一个阻塞 TCP 连接、请求与响应严格交替时，epoll 和 io_uring 服务端的处理能力。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;它还不能测试固定线程数管理大量连接的能力，也不能直接用于百万并发测试。&lt;/p&gt;&#10;&lt;p&gt;如果需要让 &lt;code&gt;-c&lt;/code&gt; 生效，每个线程就要创建：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;connection / threadnum&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;个连接，并在这些连接之间分配请求。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十当前代码需要优先处理的问题"&gt;十、当前代码需要优先处理的问题&#10;&lt;/h2&gt;&lt;p&gt;按照对测试准确性的影响排序：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;code&gt;-c&lt;/code&gt; 参数没有生效，每个线程实际上只建立一个连接。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;send()&lt;/code&gt; 没有循环处理短写。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;recv()&lt;/code&gt; 没有循环处理短读。&lt;/li&gt;&#10;&lt;li&gt;请求数不能整除线程数时，实际请求数量少于 &lt;code&gt;-n&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;failed++&lt;/code&gt; 存在多线程数据竞争。&lt;/li&gt;&#10;&lt;li&gt;工作线程结束后没有执行 &lt;code&gt;close(connfd)&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;QPS 使用配置的请求数计算，而不是实际完成数。&lt;/li&gt;&#10;&lt;li&gt;没有检查 &lt;code&gt;socket()&lt;/code&gt;、&lt;code&gt;malloc()&lt;/code&gt; 和 &lt;code&gt;pthread_create()&lt;/code&gt; 的返回值。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;connect()&lt;/code&gt; 失败后没有关闭已经创建的 socket。&lt;/li&gt;&#10;&lt;li&gt;工作线程中的 &lt;code&gt;exit(1)&lt;/code&gt; 会让一个线程的网络错误终止整个压测进程。&lt;/li&gt;&#10;&lt;li&gt;如果源代码确实包含 &lt;code&gt;test\_context\_t&lt;/code&gt;，需要删除反斜杠。&lt;/li&gt;&#10;&lt;li&gt;如果源代码第一行确实是 &lt;code&gt;include&amp;lt;stdio.h&amp;gt;&lt;/code&gt;，需要补上 &lt;code&gt;#&lt;/code&gt;。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十一网络面试问题整理"&gt;十一、网络面试问题整理&#10;&lt;/h2&gt;&lt;p&gt;原始笔记目前记录了四个核心问题，没有足够内容整理成十个，因此这里只整理已有内容，不额外补题。&lt;/p&gt;&#10;&lt;h3 id="1-tcp-三次握手与四次挥手"&gt;1. TCP 三次握手与四次挥手&#10;&lt;/h3&gt;&lt;h4 id="三次握手"&gt;三次握手&#10;&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;客户端 服务端&#10;&#10;SYN&#10; ------------------------&amp;gt;&#10;&#10; SYN + ACK&#10; &amp;lt;------------------------&#10;&#10;ACK&#10; ------------------------&amp;gt;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;三次握手需要确认：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;客户端能够发送；&lt;/li&gt;&#10;&lt;li&gt;服务端能够接收；&lt;/li&gt;&#10;&lt;li&gt;服务端能够发送；&lt;/li&gt;&#10;&lt;li&gt;客户端能够接收；&lt;/li&gt;&#10;&lt;li&gt;双方同步初始序列号。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;之所以不能只使用两次握手，是因为服务端发送 &lt;code&gt;SYN + ACK&lt;/code&gt; 后，还需要知道客户端是否真正收到了自己的响应。&lt;/p&gt;&#10;&lt;p&gt;第三次 ACK 让服务端确认：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;客户端已经收到服务端的 SYN&#10;客户端也能够正常接收数据&#10;&lt;/code&gt;&lt;/pre&gt;&lt;hr&gt;&#10;&lt;h3 id="2-为什么建链是三次断链通常是四次"&gt;2. 为什么建链是三次，断链通常是四次&#10;&lt;/h3&gt;&lt;p&gt;建立连接时，服务端可以把两个操作合并：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;确认客户端的 SYN&#10;+&#10;发送自己的 SYN&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;因此，服务端可以通过一个 &lt;code&gt;SYN + ACK&lt;/code&gt; 同时完成这两个操作，最终形成三次握手。&lt;/p&gt;&#10;&lt;p&gt;断开连接时：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;收到 FIN 后必须先回复 ACK；&lt;/li&gt;&#10;&lt;li&gt;但是本端可能还有数据没有发送完；&lt;/li&gt;&#10;&lt;li&gt;等剩余数据发送完成后，才能发送自己的 FIN。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;因此，ACK 和 FIN 通常不能立即合并，形成四次挥手。&lt;/p&gt;&#10;&lt;h4 id="四次挥手"&gt;四次挥手&#10;&lt;/h4&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;主动关闭方 被动关闭方&#10;&#10;FIN&#10; ------------------------&amp;gt;&#10;&#10; ACK&#10; &amp;lt;------------------------&#10;&#10; FIN&#10; &amp;lt;------------------------&#10;&#10;ACK&#10; ------------------------&amp;gt;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;TCP 是全双工连接，两个方向需要分别关闭。&lt;/p&gt;&#10;&lt;p&gt;一方发送 FIN，只表示自己不再发送数据，并不表示对方也已经没有数据可发。&lt;/p&gt;&#10;&lt;p&gt;如果对方收到 FIN 时，刚好也没有剩余数据需要发送，那么 ACK 和 FIN 也可能合并。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="3-udp-的并发如何实现"&gt;3. UDP 的并发如何实现&#10;&lt;/h3&gt;&lt;p&gt;UDP 没有 TCP 中的：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;listen&#10;accept&#10;connection fd&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;客户端通过：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;sendto()&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;向服务端指定的 IP 和端口发送数据。&lt;/p&gt;&#10;&lt;p&gt;服务端通过：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;recvfrom()&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;接收数据，同时得到客户端的 IP 和端口。&lt;/p&gt;&#10;&lt;p&gt;假设有 100 万个客户端同时向同一个 UDP 端口发送数据，内核会按照数据报把数据放入 socket 的接收队列。&lt;/p&gt;&#10;&lt;p&gt;服务端每次调用 &lt;code&gt;recvfrom()&lt;/code&gt; 时，可以获得：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;一条 UDP 数据报；&lt;/li&gt;&#10;&lt;li&gt;发送方的 IP；&lt;/li&gt;&#10;&lt;li&gt;发送方的端口。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;因此，不会因为多个客户端共用一个服务端端口，就直接把不同客户端的数据拼接成一条“脏数据”。&lt;/p&gt;&#10;&lt;p&gt;应用层可以使用：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;客户端 IP + 客户端端口&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;标识一个逻辑会话。&lt;/p&gt;&#10;&lt;p&gt;笔记中提出了一种模拟 TCP 的管理方式：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;客户端第一次向服务端发送握手包；&lt;/li&gt;&#10;&lt;li&gt;服务端得到客户端的 IP 和端口；&lt;/li&gt;&#10;&lt;li&gt;服务端为这个客户端建立逻辑会话；&lt;/li&gt;&#10;&lt;li&gt;后续通过会话信息管理双方的数据；&lt;/li&gt;&#10;&lt;li&gt;如果需要分散压力，可以使用多个服务端端口或者多个 UDP socket。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;例如：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;2000 个端口&#10;每个端口管理约 1000 个逻辑连接&#10;总计约 200 万个逻辑连接&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;但是，不一定需要为每个 UDP 客户端单独分配一个端口。&lt;/p&gt;&#10;&lt;p&gt;UDP 的并发数量也不是直接受 65535 个端口限制。多个客户端可以通过不同的来源 IP 和来源端口访问同一个服务端端口。&lt;/p&gt;&#10;&lt;p&gt;这一部分需要继续整理成一篇独立的技术笔记：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;UDP 的并发实现与应用层会话管理。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="4-tcp-和-udp-有哪些区别"&gt;4. TCP 和 UDP 有哪些区别&#10;&lt;/h3&gt;&lt;h4 id="连接方式"&gt;连接方式&#10;&lt;/h4&gt;&lt;p&gt;TCP 是面向连接的协议。&lt;/p&gt;&#10;&lt;p&gt;通信前需要建立连接：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;客户端连接 fd ←→ 服务端连接 fd&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;服务端通过 &lt;code&gt;accept()&lt;/code&gt; 为每个客户端创建连接 fd，因此每条 TCP 连接都有独立的连接状态。&lt;/p&gt;&#10;&lt;p&gt;UDP 是无连接协议。&lt;/p&gt;&#10;&lt;p&gt;客户端不需要先建立 TCP 意义上的连接，可以直接通过 &lt;code&gt;sendto()&lt;/code&gt; 发包，服务端通过 &lt;code&gt;recvfrom()&lt;/code&gt; 接收。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h4 id="数据形式"&gt;数据形式&#10;&lt;/h4&gt;&lt;p&gt;TCP 是字节流协议。&lt;/p&gt;&#10;&lt;p&gt;它传输的是连续字节流，本身不保留应用层的消息边界。&lt;/p&gt;&#10;&lt;p&gt;UDP 是数据报协议。&lt;/p&gt;&#10;&lt;p&gt;每次 &lt;code&gt;sendto()&lt;/code&gt; 发送一条 UDP 数据报，接收端通过 &lt;code&gt;recvfrom()&lt;/code&gt; 接收一条数据报。UDP 会保留数据报边界，但如果接收缓冲区太小，超出的部分可能被截断。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h4 id="可靠性和顺序"&gt;可靠性和顺序&#10;&lt;/h4&gt;&lt;p&gt;TCP 提供：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;有序传输；&lt;/li&gt;&#10;&lt;li&gt;丢包重传；&lt;/li&gt;&#10;&lt;li&gt;流量控制；&lt;/li&gt;&#10;&lt;li&gt;拥塞控制。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;先发送的数据，在连接正常的情况下，会先交付给接收方应用程序。&lt;/p&gt;&#10;&lt;p&gt;UDP 本身不保证：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;数据一定到达；&lt;/li&gt;&#10;&lt;li&gt;数据按照发送顺序到达；&lt;/li&gt;&#10;&lt;li&gt;数据不会重复；&lt;/li&gt;&#10;&lt;li&gt;丢失的数据会自动重传。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;如果业务需要这些能力，就需要在 UDP 的应用层加入：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;包 ID；&lt;/li&gt;&#10;&lt;li&gt;数据序号；&lt;/li&gt;&#10;&lt;li&gt;ACK 确认；&lt;/li&gt;&#10;&lt;li&gt;超时重传；&lt;/li&gt;&#10;&lt;li&gt;去重；&lt;/li&gt;&#10;&lt;li&gt;乱序重排。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;例如，把一份大数据拆成多个 UDP 包时，可以在用户空间定义：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;消息 ID&#10;分片序号&#10;总分片数量&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;接收端根据 ID 和序号判断当前收到的是第几个包，并将多个数据包重新组装。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="5-tcp-的分包与粘包"&gt;5. TCP 的分包与粘包&#10;&lt;/h3&gt;&lt;p&gt;TCP 是字节流。&lt;/p&gt;&#10;&lt;p&gt;应用层调用一次：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;send(fd, buffer, 1024, 0);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;并不代表接收端一定通过一次：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;recv(fd, buffer, 1024, 0);&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;得到完整数据。&lt;/p&gt;&#10;&lt;p&gt;可能出现：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;一次 send → 多次 recv&#10;多次 send → 一次 recv&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;所以所谓的 TCP 分包和粘包，本质上是应用层没有明确消息边界。&lt;/p&gt;&#10;&lt;p&gt;常见解决方法有两种。&lt;/p&gt;&#10;&lt;h4 id="方法一长度字段"&gt;方法一：长度字段&#10;&lt;/h4&gt;&lt;p&gt;在应用数据的头部记录消息体长度：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;| 固定长度的消息头 | 消息体 |&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;例如使用前两个或者前四个字节表示长度：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;| 2 bytes 长度 | 实际数据 |&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;接收端先读取固定长度的消息头：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;先循环读取 2 bytes&#10; ↓&#10;解析出消息体长度&#10; ↓&#10;再循环 recv&#10; ↓&#10;读取完整消息体&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;由于 TCP 保证字节流有序，因此接收端可以按照这个顺序解析数据。&lt;/p&gt;&#10;&lt;h4 id="方法二分隔符"&gt;方法二：分隔符&#10;&lt;/h4&gt;&lt;p&gt;每条消息后面增加一个特殊分隔符，例如：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;\r\n\r\n&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;接收端不断读取数据并放入缓冲区，然后查找分隔符：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;收到数据&#10; ↓&#10;加入接收缓冲区&#10; ↓&#10;查找 \r\n\r\n&#10; ↓&#10;找到后切出一条完整消息&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;如果消息正文中也可能出现相同分隔符，就需要进行转义，或者使用长度字段。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="6-tcp-和-udp-的并发区别"&gt;6. TCP 和 UDP 的并发区别&#10;&lt;/h3&gt;&lt;p&gt;TCP 服务端通常采用：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;listen socket&#10; ↓&#10;accept&#10; ↓&#10;每个客户端对应一个连接 fd&#10; ↓&#10;使用 epoll 管理大量连接 fd&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;TCP 协议栈已经维护了每条连接的状态，因此应用层可以通过不同 fd 区分客户端。&lt;/p&gt;&#10;&lt;p&gt;UDP 没有为每个客户端创建独立的连接 fd。&lt;/p&gt;&#10;&lt;p&gt;服务端一般根据：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;来源 IP&#10;来源端口&#10;应用层会话 ID&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;管理客户端状态。&lt;/p&gt;&#10;&lt;p&gt;因此，不是说 UDP 不能做高并发，而是 UDP 需要在应用层建立自己的会话管理方式。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="7-udp-的使用场景"&gt;7. UDP 的使用场景&#10;&lt;/h3&gt;&lt;p&gt;UDP 更适合以下场景：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;实时音视频；&lt;/li&gt;&#10;&lt;li&gt;对实时性要求较高的游戏数据；&lt;/li&gt;&#10;&lt;li&gt;DNS 等一次请求、一次响应的场景；&lt;/li&gt;&#10;&lt;li&gt;能够容忍少量丢包的场景；&lt;/li&gt;&#10;&lt;li&gt;由应用层自己控制可靠性的场景。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;例如在游戏团战中，玩家位置、技能释放和实时状态对延迟比较敏感。&lt;/p&gt;&#10;&lt;p&gt;如果一个旧的位置数据由于重传很晚才到达，这个数据可能已经失去意义。因此，这类实时状态通常更偏向使用 UDP。&lt;/p&gt;&#10;&lt;p&gt;但是，“对战游戏没有使用 TCP”这个说法过于绝对。&lt;/p&gt;&#10;&lt;p&gt;更合适的理解是：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;游戏中强调实时性的位置、动作等消息更倾向使用 UDP；登录、充值、聊天等要求可靠的数据仍然可能使用 TCP。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="8-tcp-的使用场景"&gt;8. TCP 的使用场景&#10;&lt;/h3&gt;&lt;p&gt;TCP 更适合：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;文件传输；&lt;/li&gt;&#10;&lt;li&gt;需要完整交付的数据；&lt;/li&gt;&#10;&lt;li&gt;需要严格有序的数据；&lt;/li&gt;&#10;&lt;li&gt;HTTP/1.1、HTTP/2 等基于 TCP 的协议；&lt;/li&gt;&#10;&lt;li&gt;长连接通信。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;文件下载需要保证最终文件完整，因此通常需要可靠传输。&lt;/p&gt;&#10;&lt;p&gt;UDP 本身没有拥塞控制和可靠重传。如果使用 UDP 传输文件，应用层就需要自己实现：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;丢包检测；&lt;/li&gt;&#10;&lt;li&gt;重传；&lt;/li&gt;&#10;&lt;li&gt;顺序恢复；&lt;/li&gt;&#10;&lt;li&gt;流量控制；&lt;/li&gt;&#10;&lt;li&gt;拥塞控制。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;TCP 已经提供了这些能力，因此使用 TCP 开发更加直接。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h3 id="9-短通信与长连接"&gt;9. 短通信与长连接&#10;&lt;/h3&gt;&lt;p&gt;DNS 是典型的短请求、短响应场景：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;发送一次查询&#10; ↓&#10;服务端返回查询结果&#10; ↓&#10;本次通信结束&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;UDP 不需要建立连接，适合这种简单的请求响应。&lt;/p&gt;&#10;&lt;p&gt;“UDP 做短连接”更准确的说法是：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;UDP 没有 TCP 意义上的连接，适合短暂的一次性请求响应。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;有些业务也会选择 TCP 完成短请求，因为：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;TCP 相关框架成熟；&lt;/li&gt;&#10;&lt;li&gt;开发更加方便；&lt;/li&gt;&#10;&lt;li&gt;不需要自己实现可靠性；&lt;/li&gt;&#10;&lt;li&gt;业务本身更重视开发成本和正确性。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;TCP 则天然适合需要长期保持通信状态的长连接，但 TCP 并不是只能做长连接。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十二阶段总结"&gt;十二、阶段总结&#10;&lt;/h2&gt;&lt;p&gt;从传统阻塞 I/O、epoll 再到 io_uring，容易产生一种混沌感。&lt;/p&gt;&#10;&lt;p&gt;当前最需要捋清的是三个层次之间的关系：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;TCP&#10;提供字节流、连接、可靠传输等协议能力&#10; ↓&#10;epoll / io_uring&#10;解决应用程序如何高效处理 I/O&#10; ↓&#10;业务协议&#10;解决消息边界、心跳、会话和错误处理&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;本轮实验可以得到三个明确认识：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;epoll 应当与非阻塞 I/O 配合，否则单个连接可能阻塞整个事件循环。&lt;/li&gt;&#10;&lt;li&gt;当前测试中，io_uring 的 QPS 全面高于非阻塞 epoll，整体优势约为 17%～21%。&lt;/li&gt;&#10;&lt;li&gt;客户端的连接模型、收发方式和计数准确性，会直接影响测试结果。当前 &lt;code&gt;-c&lt;/code&gt; 参数没有生效，循环收发也尚未实现。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十三面试复盘"&gt;十三、面试复盘&#10;&lt;/h2&gt;&lt;p&gt;面试需要不断复盘。&lt;/p&gt;&#10;&lt;p&gt;简历中能够被问到的内容是有限的，每次面试后都应该记录问题，并重新整理问题背后的知识。&lt;/p&gt;&#10;&lt;p&gt;原笔记中的经验数据为：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;面试时间&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;个人估计的通过概率&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;30 分钟&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;5%&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;45 分钟&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;30%&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;60 分钟&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;80%&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;求职过程可以通过两个比例进行观察：&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;邀面数量 / 投递数量&#10;→ 反映简历的匹配度和成功度&#10;&lt;/code&gt;&lt;/pre&gt;&lt;pre tabindex="0"&gt;&lt;code&gt;Offer 数量 / 邀面数量&#10;→ 反映技术能力和面试表现&#10;&lt;/code&gt;&lt;/pre&gt;</description></item><item><title>io_uring（一）：与 epoll 一较高下</title><link>https://jasperstonnne.github.io/MyBlog/p/io-uring-vs-epoll/</link><pubDate>Wed, 19 Aug 2026 00:00:00 +0800</pubDate><guid>https://jasperstonnne.github.io/MyBlog/p/io-uring-vs-epoll/</guid><description>&lt;img src="https://jasperstonnne.github.io/MyBlog/p/io-uring-vs-epoll/cover.svg" alt="Featured image of post io_uring（一）：与 epoll 一较高下" /&gt;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;核心目标：能从代码上说清楚 &lt;code&gt;epoll&lt;/code&gt; 和 &lt;code&gt;io_uring&lt;/code&gt; 的区别，而不是只记 API。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;h2 id="1-今天最重要的结论"&gt;1. 今天最重要的结论&#10;&lt;/h2&gt;&lt;p&gt;一句话区分：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Reactor / epoll：内核通知“现在可以做 I/O 了”，真正的 &lt;code&gt;accept/recv/send&lt;/code&gt; 由应用执行。&lt;/strong&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Proactor / io_uring：应用先提交 I/O 操作，内核完成后通知“这个操作已经做完了”。&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;最值得记住的一组对照：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;epoll 的 EPOLLIN = 可以读了，但数据还没被应用读出&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;io_uring 的 READ CQE = 读操作已结束，结果在 cqe-&amp;gt;res，数据已进入指定 buffer&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;epoll 的 EPOLLOUT = 现在可以写，但应用还要调用 send/write&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;io_uring 的 WRITE CQE = 写操作已结束，cqe-&amp;gt;res 表示实际写出的字节数&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;因此，二者虽然都有“注册/提交 → 等待 → 处理事件”的循环，但事件的含义完全不同：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;epoll 返回的是&lt;strong&gt;就绪事件&lt;/strong&gt;。&lt;/li&gt;&#10;&lt;li&gt;io_uring 返回的是&lt;strong&gt;完成事件&lt;/strong&gt;。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="2-异步与异步-io不是一回事"&gt;2. “异步”与“异步 I/O”不是一回事&#10;&lt;/h2&gt;&lt;h3 id="21-一般意义上的异步"&gt;2.1 一般意义上的异步&#10;&lt;/h3&gt;&lt;p&gt;发起任务后，不原地等待它结束，当前执行流可以继续做别的事；任务完成后再通过回调、消息、事件或协程恢复等方式取得结果。&lt;/p&gt;&#10;&lt;p&gt;例如：把日志写盘任务放入线程池。业务线程只投递任务，工作线程实际调用同步 &lt;code&gt;write()&lt;/code&gt;。从业务流程看它是异步的，但底层 &lt;code&gt;write()&lt;/code&gt; 本身仍可能是同步 I/O。&lt;/p&gt;&#10;&lt;h3 id="22-同步-io"&gt;2.2 同步 I/O&#10;&lt;/h3&gt;&lt;p&gt;普通的：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;一次函数调用既发起操作，也返回该次操作的结果。即使 fd 被设为非阻塞，调用者仍然要亲自执行 &lt;code&gt;recv/send&lt;/code&gt;；“非阻塞”不自动等于“异步 I/O”。&lt;/p&gt;&#10;&lt;h3 id="23-epoll-做到的是什么"&gt;2.3 epoll 做到的是什么&#10;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;epoll_wait()&lt;/code&gt; 可以高效等待大量 fd 的&lt;strong&gt;就绪状态&lt;/strong&gt;，避免逐个 fd 轮询。它通常与非阻塞 socket 配合，构成 Reactor。&lt;/p&gt;&#10;&lt;p&gt;但 &lt;code&gt;epoll&lt;/code&gt; 不替应用搬运数据：收到 &lt;code&gt;EPOLLIN&lt;/code&gt; 后，代码仍要调用 &lt;code&gt;recv()&lt;/code&gt;；收到 &lt;code&gt;EPOLLOUT&lt;/code&gt; 后，仍要调用 &lt;code&gt;send()&lt;/code&gt;。&lt;/p&gt;&#10;&lt;h3 id="24-io_uring-做到的是什么"&gt;2.4 io_uring 做到的是什么&#10;&lt;/h3&gt;&lt;p&gt;应用把“请替我执行 accept/read/write……”描述成 SQE，提交给内核；操作完成后，内核写入 CQE。应用处理 CQE 时，看到的是操作结果，而不只是“可以开始操作”。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="3-io_uring-的两个环"&gt;3. io_uring 的两个环&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;应用准备 SQE → SQ（Submission Queue）→ 内核执行 I/O&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;应用处理结果 ← CQ（Completion Queue）← 内核生成 CQE&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="sq--sqe"&gt;SQ / SQE&#10;&lt;/h3&gt;&lt;ul&gt;&#10;&lt;li&gt;SQ：提交队列。&lt;/li&gt;&#10;&lt;li&gt;SQE：一次待执行 I/O 的描述，例如操作类型、fd、buffer、长度和用户数据。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;io_uring_get_sqe()&lt;/code&gt;：取得一个可填写的 SQE。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;io_uring_prep_accept/read/write(...)&lt;/code&gt;：只是在填写 SQE，&lt;strong&gt;不会立刻执行 I/O&lt;/strong&gt;。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;io_uring_submit()&lt;/code&gt;：把准备好的请求提交给内核。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="cq--cqe"&gt;CQ / CQE&#10;&lt;/h3&gt;&lt;ul&gt;&#10;&lt;li&gt;CQ：完成队列。&lt;/li&gt;&#10;&lt;li&gt;CQE：一个已经完成的 I/O 结果。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;cqe-&amp;gt;res&lt;/code&gt;：操作结果。成功时通常是新 fd 或字节数；失败时是负的 errno 值。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;cqe-&amp;gt;user_data&lt;/code&gt;：应用提交时附带的标识，用来判断这是哪个连接、哪类操作。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;io_uring_cq_advance()&lt;/code&gt;：告诉 ring，这批 CQE 已处理，可以回收。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="为什么使用环形队列和-mmap"&gt;为什么使用环形队列和 mmap&#10;&lt;/h3&gt;&lt;p&gt;环形队列可以复用固定槽位，减少频繁分配；SQ/CQ 通过 &lt;code&gt;mmap&lt;/code&gt; 在用户态和内核态之间共享队列元数据，减少提交、取结果时的额外系统调用和元数据复制。&lt;/p&gt;&#10;&lt;p&gt;但要注意：这不代表所有 I/O 数据都天然“零拷贝”。例如普通 socket 接收的数据通常仍需进入应用提供的 buffer。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="4-io_uring-三个底层系统调用"&gt;4. io_uring 三个底层系统调用&#10;&lt;/h2&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;io_uring_setup&lt;/code&gt;：创建并配置 ring，建立 SQ/CQ 所需资源。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;io_uring_enter&lt;/code&gt;：提交请求和/或等待完成事件。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;io_uring_register&lt;/code&gt;：预注册文件、buffer、事件等资源，减少热路径开销；它不是每个普通请求都必须调用。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;课堂代码使用的是 &lt;code&gt;liburing&lt;/code&gt; 封装：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;io_uring_queue_init_params&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ENTRIES_LENGTH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ring&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;io_uring_get_sqe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ring&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;io_uring_prep_accept&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;io_uring_submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ring&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;io_uring_wait_cqe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ring&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cqe&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可近似理解为：初始化 ring → 取得请求槽位 → 描述操作 → 提交 → 等结果。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="5-你的-io_uring-echo-server-怎么走"&gt;5. 你的 io_uring Echo Server 怎么走&#10;&lt;/h2&gt;&lt;p&gt;关键入口：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;set_event_accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ring&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;sockfd&lt;/span&gt;&lt;span class="p"&gt;,(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;sockaddr&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;clientaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;set_event_accept()&lt;/code&gt; 做两件事：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;用 &lt;code&gt;io_uring_get_sqe()&lt;/code&gt; 取得 SQE。&lt;/li&gt;&#10;&lt;li&gt;用 &lt;code&gt;io_uring_prep_accept()&lt;/code&gt; 填写 accept 请求，并在 &lt;code&gt;user_data&lt;/code&gt; 中记录 &lt;code&gt;fd + EVENT_ACCEPT&lt;/code&gt;。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;此时只是&lt;strong&gt;准备请求&lt;/strong&gt;，并没有等待客户端。&lt;/p&gt;&#10;&lt;p&gt;主循环：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;io_uring_submit&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ 提交当前准备好的 accept/recv/send 请求&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;io_uring_wait_cqe&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ 没有完成结果时在这里等待&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;io_uring_peek_batch_cqe&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ 批量取得已完成操作&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;根据 user_data 判断 EVENT_ACCEPT&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;再次 set_event_accept，同时为新 clientfd 提交 recv&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;io_uring_cq_advance&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;你测试时发现程序阻塞在 &lt;code&gt;io_uring_wait_cqe()&lt;/code&gt;，这是合理的：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;io_uring_prep_accept()&lt;/code&gt; 只准备请求，很快返回。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;io_uring_submit()&lt;/code&gt; 提交请求。&lt;/li&gt;&#10;&lt;li&gt;在客户端真正连接前，accept 尚未完成，CQ 中没有结果，所以 &lt;code&gt;wait_cqe()&lt;/code&gt; 等待。&lt;/li&gt;&#10;&lt;li&gt;客户端连接后，CQE 到达，&lt;code&gt;cqe-&amp;gt;res&lt;/code&gt; 应是新连接的 client fd（失败则为负数）。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="51-三种事件不是就绪事件而是操作身份"&gt;5.1 三种事件不是“就绪事件”，而是操作身份&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define EVENT_ACCEPT 0&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define EVENT_READ 1&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define EVENT_WRITE 2&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里的 &lt;code&gt;EVENT_*&lt;/code&gt; 是应用自己定义的标签。准备 SQE 时，代码把 &lt;code&gt;fd + event&lt;/code&gt; 写入 &lt;code&gt;sqe-&amp;gt;user_data&lt;/code&gt;；操作完成后，再从 CQE 的 &lt;code&gt;user_data&lt;/code&gt; 还原标签，据此判断完成的是 accept、recv 还是 send。&lt;/p&gt;&#10;&lt;p&gt;三种 SQE 的含义：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;函数&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;提交给内核的操作&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;CQE 到达时 &lt;code&gt;entries-&amp;gt;res&lt;/code&gt; 的含义&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;set_event_accept&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;接收一个新连接&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;新连接 fd；负数表示失败&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;set_event_recv&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;把数据读进指定 buffer&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;实际读取字节数；0 表示对端关闭；负数表示失败&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;set_event_send&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;发送指定 buffer 中的数据&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;实际发送字节数；负数表示失败&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="52-完整状态循环"&gt;5.2 完整状态循环&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;启动：准备 ACCEPT SQE&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ submit&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ACCEPT 完成&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─ entries-&amp;gt;res 得到 connfd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─ 再准备一个 ACCEPT，继续接收其他客户端&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─ 为 connfd 准备 RECV&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ submit&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; READ 完成&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├─ res == 0：对端断开，close(fd)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └─ res &amp;gt; 0：buffer 中已有数据，准备 SEND&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ submit&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; WRITE 完成&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 再为该 fd 准备 RECV&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对单个连接而言，核心循环就是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RECV 完成 → SEND 完成 → 再次 RECV&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这份代码没有额外解析数据，而是把收到的 &lt;code&gt;buffer&lt;/code&gt; 原样发送回去，因此它是一个 io_uring Echo Server。&lt;/p&gt;&#10;&lt;h3 id="53-为什么循环开头的-submit-能提交刚刚准备的操作"&gt;5.3 为什么循环开头的 submit 能提交刚刚准备的操作&#10;&lt;/h3&gt;&lt;p&gt;在处理 CQE 时调用的 &lt;code&gt;set_event_accept/recv/send()&lt;/code&gt; 都只是取得并填写新的 SQE。它们不会立刻执行；本轮处理结束并 &lt;code&gt;cq_advance&lt;/code&gt; 后，程序回到 &lt;code&gt;while&lt;/code&gt; 开头，由下一次 &lt;code&gt;io_uring_submit()&lt;/code&gt; 统一提交。&lt;/p&gt;&#10;&lt;p&gt;因此时间顺序是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;本轮处理 CQE → 准备下一批 SQE → advance → 下一轮 submit&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这种批量准备、批量提交正是 io_uring 的重要思路。&lt;/p&gt;&#10;&lt;h3 id="为什么处理后要再次设置-accept"&gt;为什么处理后要再次设置 accept&#10;&lt;/h3&gt;&lt;p&gt;普通的 &lt;code&gt;io_uring_prep_accept()&lt;/code&gt; 描述的是&lt;strong&gt;一次 accept 操作&lt;/strong&gt;。完成一次就消费掉一次请求；若想继续接收连接，需要再准备并提交新的 accept。&lt;/p&gt;&#10;&lt;p&gt;这与 epoll 的常规 LT 模式不同：监听 fd 注册 &lt;code&gt;EPOLLIN&lt;/code&gt; 后，只要仍处于可读状态，epoll 可以继续报告就绪，无需每完成一次 accept 就重新 &lt;code&gt;EPOLL_CTL_ADD&lt;/code&gt;。&lt;/p&gt;&#10;&lt;h3 id="为什么必须-advance"&gt;为什么必须 advance&#10;&lt;/h3&gt;&lt;p&gt;CQE 被读取不等于被消费：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;io_uring_cq_advance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ring&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;nready&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这一步更新 CQ 的消费位置。漏掉它，旧 CQE 会一直被当成尚未处理，表现为循环反复看到同一结果。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="6-你的-reactor-代码怎么走"&gt;6. 你的 Reactor 代码怎么走&#10;&lt;/h2&gt;&lt;h3 id="61-初始化"&gt;6.1 初始化&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;epoll_create&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;创建 20 个监听 socket（端口 2000～2019）&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;把监听 fd 以 EPOLLIN 加入 epoll&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;进入 epoll_wait 主循环&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;监听 fd 的回调被设置为 &lt;code&gt;accept_cb&lt;/code&gt;：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sockfd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;r_action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recv_callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accept_cb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="62-建立连接"&gt;6.2 建立连接&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;监听 fd 出现 EPOLLIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;主循环调用 accept_cb&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;accept_cb 主动执行 accept()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;event_register(clientfd, EPOLLIN)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;关键点：&lt;code&gt;EPOLLIN&lt;/code&gt; 只表示监听 socket 可以执行 accept；真正建立并取出 client fd 的动作仍由 &lt;code&gt;accept()&lt;/code&gt; 完成。&lt;/p&gt;&#10;&lt;h3 id="63-接收数据"&gt;6.3 接收数据&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;clientfd 出现 EPOLLIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;调用 recv_cb&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;recv() 把数据读进 rbuffer&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ws_request() 处理请求&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;把关注事件修改为 EPOLLOUT&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果 &lt;code&gt;recv()&lt;/code&gt; 返回 0，说明对端正常断开；返回负数则表示错误。当前代码随后关闭 fd 并从 epoll 删除。&lt;/p&gt;&#10;&lt;h3 id="64-发送数据"&gt;6.4 发送数据&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;clientfd 出现 EPOLLOUT&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;调用 send_cb&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ws_response() 生成响应&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;send() 主动发送 wbuffer&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;把关注事件改回 EPOLLIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;整个连接状态近似为：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;等待读 EPOLLIN → recv + 处理请求 → 等待写 EPOLLOUT&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↑ ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └──────── send + 改回读 ──────────┘&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="7-逐段对照reactor-与-io_uring"&gt;7. 逐段对照：Reactor 与 io_uring&#10;&lt;/h2&gt;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;阶段&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;epoll / Reactor&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;io_uring / Proactor 思路&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;接收连接&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;注册监听 fd 的 &lt;code&gt;EPOLLIN&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;提交一个 accept SQE&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;通知到达&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;“监听 fd 可以 accept”&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;“accept 已完成”&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;新连接 fd&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;应用调用 &lt;code&gt;accept()&lt;/code&gt; 得到&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;从 accept CQE 的 &lt;code&gt;res&lt;/code&gt; 得到&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;接收数据&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;EPOLLIN&lt;/code&gt; 后应用调用 &lt;code&gt;recv()&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;预先提交 recv/read SQE，完成后 buffer 已有数据&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;发送数据&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;EPOLLOUT&lt;/code&gt; 后应用调用 &lt;code&gt;send()&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;预先提交 send/write SQE，完成后得到实际写入量&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;事件身份&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;events[i].data.fd&lt;/code&gt; + 回调表&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;cqe-&amp;gt;user_data&lt;/code&gt; 标识 fd/事件/上下文&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;一次操作结束&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;修改下一次关注的就绪事件&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;消费 CQE，再准备下一次 I/O SQE&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;批量处理&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;epoll_wait(..., events, 1024, ...)&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;io_uring_peek_batch_cqe(..., cqes, 128)&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;代码上的一一对应：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;epoll_ctl ADD/MOD ↔ io_uring_get_sqe + prep_xxx&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;epoll_wait ↔ io_uring_wait_cqe / peek_batch_cqe&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;events[i].data.fd ↔ cqe-&amp;gt;user_data&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;EPOLLIN 后 recv ↔ READ CQE 到达时数据已读完&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;EPOLLOUT 后 send ↔ WRITE CQE 到达时写操作已完成&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;下一轮 set_event ↔ 下一轮重新准备 SQE&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↔ io_uring_cq_advance 消费完成项&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="8-对学习笔记中三个说法的校正"&gt;8. 对学习笔记中三个说法的校正&#10;&lt;/h2&gt;&lt;h3 id="-sqe-与-cqe-是同一个节点共用一块内存不准确"&gt;① “SQE 与 CQE 是同一个节点，共用一块内存”——不准确&#10;&lt;/h3&gt;&lt;p&gt;SQE 和 CQE 是不同队列中的不同结构：SQE 描述请求，CQE 描述结果。它们通过 &lt;code&gt;user_data&lt;/code&gt; 关联。共享内存指的是这些 ring 能被用户态与内核态共同访问，不是 SQE 原地变成 CQE。&lt;/p&gt;&#10;&lt;h3 id="-io_uring_prep_accept-底层调用-register不准确"&gt;② “io_uring_prep_accept 底层调用 register”——不准确&#10;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;io_uring_prep_accept()&lt;/code&gt; 主要是填写 SQE。&lt;code&gt;io_uring_register&lt;/code&gt; 是独立的资源注册系统调用，普通 accept 请求不要求每次 register。&lt;/p&gt;&#10;&lt;h3 id="-mmap-后就没有-copy范围过大"&gt;③ “mmap 后就没有 copy”——范围过大&#10;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;mmap&lt;/code&gt; 主要减少 SQ/CQ 控制信息在用户态和内核态之间的复制及系统调用开销；普通网络 I/O 的 payload 是否复制，是另一层问题。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="9-最小记忆框架"&gt;9. 最小记忆框架&#10;&lt;/h2&gt;&lt;p&gt;只背下面四句话：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;code&gt;epoll&lt;/code&gt; 给我的是&lt;strong&gt;就绪&lt;/strong&gt;，我还要自己 &lt;code&gt;accept/recv/send&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;io_uring&lt;/code&gt; 给我的是&lt;strong&gt;完成&lt;/strong&gt;，结果看 &lt;code&gt;cqe-&amp;gt;res&lt;/code&gt;，身份看 &lt;code&gt;cqe-&amp;gt;user_data&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;io_uring 的路径是：&lt;strong&gt;取 SQE → prep → submit → 等 CQE → 处理 → advance&lt;/strong&gt;。&lt;/li&gt;&#10;&lt;li&gt;普通 io_uring 请求通常是一次性的：&lt;strong&gt;完成一次，消费一次，再提交下一次&lt;/strong&gt;。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;</description></item><item><title>DPDK 用户态 TCP 协议栈（四）：并发与自实现 epoll</title><link>https://jasperstonnne.github.io/MyBlog/p/dpdk-userspace-tcp-concurrency-epoll/</link><pubDate>Tue, 18 Aug 2026 00:00:00 +0800</pubDate><guid>https://jasperstonnne.github.io/MyBlog/p/dpdk-userspace-tcp-concurrency-epoll/</guid><description>&lt;img src="https://jasperstonnne.github.io/MyBlog/p/dpdk-userspace-tcp-concurrency-epoll/cover.svg" alt="Featured image of post DPDK 用户态 TCP 协议栈（四）：并发与自实现 epoll" /&gt;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;本篇只围绕一个问题展开：&lt;strong&gt;用户态 TCP 协议栈怎样让一个应用线程并发管理多条 TCP 连接？&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;前三篇已经反复讲过 DPDK 收发、mbuf、Ethernet/IPv4/TCP 解析、三次握手、SEQ/ACK、TCP 状态机和 POSIX Socket。本篇只在它们影响 epoll 逻辑时简要回顾，不再重新逐层讲包头和握手细节。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="一先给出整篇的核心答案"&gt;一、先给出整篇的核心答案&#10;&lt;/h2&gt;&lt;p&gt;TCP 并发 epoll 的实现可以浓缩成一个闭环：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1. 每条 TCP 连接拥有独立的 stream/TCB 和用户态 fd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2. 应用通过 epoll_ctl 注册“我关心哪些 fd 的哪些事件”&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3. 协议栈处理报文时发现某个 fd 已经可读或可写&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4. 协议栈调用 epoll 回调，把该 fd 放入就绪队列&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;5. 回调唤醒阻塞在 epoll_wait 的应用线程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;6. epoll_wait 返回就绪 fd，应用执行 accept/recv/send/close&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;7. 应用重新进入 epoll_wait，继续管理所有连接&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;epoll 没有让 TCP 报文并行到达，也没有替协议栈处理握手。它解决的是：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;当许多连接都可能产生数据时，应用怎样只处理当前已经就绪的连接，而不是为每条连接创建一个长期阻塞的线程。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;理解本篇时始终抓住三种对象：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;对象&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;解决的问题&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;ng_tcp_stream&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;这是谁的连接，当前 TCP 状态和数据是什么&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;epitem&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;应用是否关注这个 fd，它现在是否就绪&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;eventpoll&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;一个 epoll 实例管理的整集、就绪集和等待线程&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="二只保留必要的旧知识数据最终从哪里来到哪里去"&gt;二、只保留必要的旧知识：数据最终从哪里来到哪里去&#10;&lt;/h2&gt;&lt;p&gt;完整程序有三个长期运行的执行流：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;main/lcore：网卡 RX/TX&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↕ 全局 in/out ring&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pkt_process/lcore：Ethernet/IP/TCP 解析、TCP 输出封包&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↕ 每连接 rcvbuf/sndbuf&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;tcp_server_entry/lcore：accept、recv、send、epoll_wait&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;两层 ring 不要混淆：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;全局 &lt;code&gt;ring-&amp;gt;in/out&lt;/code&gt; 传递 &lt;code&gt;rte_mbuf *&lt;/code&gt;，用于网卡线程与协议处理线程之间传包。&lt;/li&gt;&#10;&lt;li&gt;每个 &lt;code&gt;ng_tcp_stream&lt;/code&gt; 的 &lt;code&gt;rcvbuf/sndbuf&lt;/code&gt; 传递 TCP fragment，用于协议栈与应用之间传数据。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;一段客户端数据的最短路径是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;网卡 RX&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 全局 in ring&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ ng_tcp_process()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 按四元组找到 stream&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ payload 放入 stream-&amp;gt;rcvbuf&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 触发 EPOLLIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ nepoll_wait() 返回 connfd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 应用 nrecv(connfd)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;应用响应的路径相反：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nsend(connfd)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ fragment 放入 stream-&amp;gt;sndbuf&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ ng_tcp_out() 取出并封装 TCP 报文&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 全局 out ring&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 网卡 TX&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这些收发与报文构造细节在前几篇已有完整说明。本篇真正关心的是中间这一步：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stream 的状态发生变化&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 怎样让正在等待的应用知道？&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="三支持并发的第一前提连接必须彼此独立"&gt;三、支持并发的第一前提：连接必须彼此独立&#10;&lt;/h2&gt;&lt;h3 id="1-一条连接对应一个-ng_tcp_stream"&gt;1. 一条连接对应一个 &lt;code&gt;ng_tcp_stream&lt;/code&gt;&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;ng_tcp_stream&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;sip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dip&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint16_t&lt;/span&gt; &lt;span class="n"&gt;sport&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dport&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;snd_nxt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;rcv_nxt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;NG_TCP_STATUS&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;rte_ring&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sndbuf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;rte_ring&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rcvbuf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;pthread_cond_t&lt;/span&gt; &lt;span class="n"&gt;cond&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;pthread_mutex_t&lt;/span&gt; &lt;span class="n"&gt;mutex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它是本代码中的简化 TCB。每个客户端都必须拥有独立的：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;地址与端口；&lt;/li&gt;&#10;&lt;li&gt;TCP 状态；&lt;/li&gt;&#10;&lt;li&gt;SEQ/ACK；&lt;/li&gt;&#10;&lt;li&gt;接收和发送缓冲区；&lt;/li&gt;&#10;&lt;li&gt;用户态 fd；&lt;/li&gt;&#10;&lt;li&gt;阻塞与唤醒状态。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;因此，TCP 并发的根基不是 epoll，而是&lt;strong&gt;连接级状态隔离&lt;/strong&gt;。如果两条连接共用序列号或接收缓冲区，换成 epoll 也不能让协议栈正确并发。&lt;/p&gt;&#10;&lt;h3 id="2-报文怎样找到自己的-stream"&gt;2. 报文怎样找到自己的 stream&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sip&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;sip&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;dip&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;dip&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sport&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;sport&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;dport&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;dport&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;一般网络流用五元组标识；这里已经确定是 TCP，所以搜索时比较四元组。两个客户端连接同一个服务端地址和端口时，客户端 IP 或源端口不同，因此能找到不同的 stream。&lt;/p&gt;&#10;&lt;h3 id="3-监听-stream-和连接-stream"&gt;3. 监听 stream 和连接 stream&#10;&lt;/h3&gt;&lt;p&gt;服务端只有一个监听 stream，但可以有许多已连接 stream：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;listenfd / LISTEN stream&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── connfd A / ESTABLISHED stream A&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── connfd B / ESTABLISHED stream B&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── connfd C / ESTABLISHED stream C&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;收到 SYN 时，协议栈保留监听 stream，并创建一个新的连接 stream 处理该客户端：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;ng_tcp_stream&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;syn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ng_tcp_stream_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;iphdr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;src_addr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iphdr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;dst_addr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tcphdr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;src_port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tcphdr&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;dst_port&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;LL_ADD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;syn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;tcb_set&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;syn&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NG_TCP_STATUS_SYN_RCVD&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;监听对象负责“还能不能接新连接”，连接对象负责“这个客户端的数据和状态”。epoll 后面会分别监听这两类 fd。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="四一连接一线程为什么能工作又为什么需要-epoll"&gt;四、一连接一线程为什么能工作，又为什么需要 epoll&#10;&lt;/h2&gt;&lt;h3 id="1-一连接一线程"&gt;1. 一连接一线程&#10;&lt;/h3&gt;&lt;p&gt;最直观的并发方式是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;主线程：不断 naccept()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;连接 A：线程 A 阻塞在 nrecv(A)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;连接 B：线程 B 阻塞在 nrecv(B)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;连接 C：线程 C 阻塞在 nrecv(C)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它适合验证连接表、stream 隔离和收发缓冲区是否支持多个客户端。问题是大部分连接经常没有数据，线程却仍占用栈空间和调度资源。连接数增长时，线程数也同步增长。&lt;/p&gt;&#10;&lt;h3 id="2-epoll-的改变"&gt;2. epoll 的改变&#10;&lt;/h3&gt;&lt;p&gt;epoll 模型下，应用线程不阻塞在某一个 &lt;code&gt;nrecv(connfd)&lt;/code&gt; 上，而是阻塞在“所有已注册 fd 的就绪集合”上：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;应用线程：nepoll_wait(A, B, C, ...)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;只有 B 有数据&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ nepoll_wait 只返回 B&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 应用处理 B&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 再次等待所有连接&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;所以两种模型的区别不是 TCP 连接数，而是等待位置：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;一连接一线程：每个线程等待一个 fd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;epoll：一个线程等待一组 fd&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="五为什么不能直接调用-linux-原生-epoll"&gt;五、为什么不能直接调用 Linux 原生 epoll&#10;&lt;/h2&gt;&lt;p&gt;代码中的 &lt;code&gt;listenfd&lt;/code&gt;、&lt;code&gt;connfd&lt;/code&gt; 和 &lt;code&gt;epfd&lt;/code&gt; 由自己的位图分配：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;get_fd_frombitmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DEFAULT_FD_NUM&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MAX_FD_COUNT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd_table&lt;/span&gt; &lt;span class="err"&gt;中该位空闲&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="err"&gt;标记为已用&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这些整数只在用户态协议栈内部有意义：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;用户态 fd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ get_hostinfo_fromfd(fd)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ ng_tcp_stream / localhost / eventpoll&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Linux 内核没有创建对应的 socket 对象，也不知道 &lt;code&gt;stream-&amp;gt;rcvbuf&lt;/code&gt; 是否有数据。因此把这种 &lt;code&gt;connfd&lt;/code&gt; 传给内核 &lt;code&gt;epoll_ctl()&lt;/code&gt;，内核无法监听它。&lt;/p&gt;&#10;&lt;p&gt;既然 Socket API 是用户态自己实现的，事件机制也要自己补齐：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nsocket / nbind / nlisten / naccept / nrecv / nsend&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; +&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nepoll_create / nepoll_ctl / nepoll_wait&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="六理解-epoll-的关键就绪是一种状态"&gt;六、理解 epoll 的关键：就绪是一种状态&#10;&lt;/h2&gt;&lt;p&gt;应用关注的不是“是否来过一个包”，而是“现在执行某个操作会不会阻塞”。&lt;/p&gt;&#10;&lt;h3 id="1-监听-fd-可读"&gt;1. 监听 fd 可读&#10;&lt;/h3&gt;&lt;p&gt;监听 fd 的 &lt;code&gt;EPOLLIN&lt;/code&gt; 表示：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;至少有一条已完成握手的连接可以被 naccept() 取出&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它不是说监听 socket 收到了应用 payload，而是说 &lt;code&gt;accept&lt;/code&gt; 现在不会阻塞。&lt;/p&gt;&#10;&lt;h3 id="2-连接-fd-可读"&gt;2. 连接 fd 可读&#10;&lt;/h3&gt;&lt;p&gt;连接 fd 的 &lt;code&gt;EPOLLIN&lt;/code&gt; 表示：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;nrecv() 现在可以取得 payload，或者取得 EOF/关闭信息&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;因此，收到 FIN 也可以触发 &lt;code&gt;EPOLLIN&lt;/code&gt;。应用随后调用 &lt;code&gt;nrecv()&lt;/code&gt; 得到 0，知道对端已关闭。&lt;/p&gt;&#10;&lt;h3 id="3-连接-fd-可写"&gt;3. 连接 fd 可写&#10;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;EPOLLOUT&lt;/code&gt; 的一般含义是：发送缓冲区有空间，&lt;code&gt;send&lt;/code&gt; 不会因为缓冲区已满而阻塞。它不等于“一个数据包已经从网卡发完”。&lt;/p&gt;&#10;&lt;p&gt;本节代码重点实现的是 &lt;code&gt;EPOLLIN&lt;/code&gt; 链路；要完整支持 &lt;code&gt;EPOLLOUT&lt;/code&gt;，还需要定义 &lt;code&gt;sndbuf&lt;/code&gt; 从满变为可用时怎样产生通知。&lt;/p&gt;&#10;&lt;p&gt;这一点非常重要：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;epoll 事件是 Socket 操作条件的变化，不是对每个网络包做一次简单转发。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="七epoll-的两个集合"&gt;七、epoll 的两个集合&#10;&lt;/h2&gt;&lt;p&gt;一个 epoll 实例内部有两份不同用途的集合。&lt;/p&gt;&#10;&lt;h3 id="1-整集我关注谁"&gt;1. 整集：我关注谁&#10;&lt;/h3&gt;&lt;p&gt;整集保存所有通过 &lt;code&gt;EPOLL_CTL_ADD&lt;/code&gt; 注册的 fd。代码用红黑树组织，以 &lt;code&gt;sockfd&lt;/code&gt; 为 key：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;整集 = {listenfd, connfd_A, connfd_B, connfd_C, ...}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它回答：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;这个 fd 是否被注册？&lt;/li&gt;&#10;&lt;li&gt;应用对它关心 &lt;code&gt;EPOLLIN&lt;/code&gt; 还是 &lt;code&gt;EPOLLOUT&lt;/code&gt;？&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;ADD/DEL/MOD&lt;/code&gt; 应该操作哪个节点？&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;红黑树提供稳定的 &lt;code&gt;O(log n)&lt;/code&gt; 查找、插入和删除，并能随注册数量按节点增长。哈希的平均查找更快，但要额外处理桶容量、冲突和扩容。这里选择红黑树的重点是动态集合与稳定性能，并不是红黑树在所有场景都绝对更快。&lt;/p&gt;&#10;&lt;h3 id="2-就绪集现在谁能处理"&gt;2. 就绪集：现在谁能处理&#10;&lt;/h3&gt;&lt;p&gt;就绪集是链表，只保存当前已经产生事件的节点：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;整集：{listenfd, A, B, C, D}&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;就绪集：{B, D}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;nepoll_wait()&lt;/code&gt; 无须扫描整棵红黑树，只需从就绪链表取出 B、D。&lt;/p&gt;&#10;&lt;h3 id="3-同一个节点同时属于两份集合"&gt;3. 同一个节点同时属于两份集合&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;epitem&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;RB_ENTRY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epitem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;rbn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;LIST_ENTRY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epitem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;rdlink&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;rdy&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;sockfd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;epoll_event&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;一个 &lt;code&gt;epitem&lt;/code&gt; 中同时有红黑树节点和链表节点：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rbn：长期挂在整集，表示注册关系&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rdlink：就绪时临时挂在就绪链表&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rdy：防止同一节点被重复插入就绪链表&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;节点进入就绪链表后仍保留在红黑树里。应用消费一次就绪事件，不等于取消对这个 fd 的关注；只有 &lt;code&gt;EPOLL_CTL_DEL&lt;/code&gt; 才删除注册关系。&lt;/p&gt;&#10;&lt;h3 id="4-eventpoll-管理整个-epoll-实例"&gt;4. &lt;code&gt;eventpoll&lt;/code&gt; 管理整个 epoll 实例&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;eventpoll&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ep_rb_tree&lt;/span&gt; &lt;span class="n"&gt;rbr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;rbcnt&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;LIST_HEAD&lt;/span&gt;&lt;span class="p"&gt;(,&lt;/span&gt; &lt;span class="n"&gt;epitem&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;rdlist&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;rdnum&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;pthread_mutex_t&lt;/span&gt; &lt;span class="n"&gt;mtx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;pthread_spinlock_t&lt;/span&gt; &lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;pthread_cond_t&lt;/span&gt; &lt;span class="n"&gt;cond&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;pthread_mutex_t&lt;/span&gt; &lt;span class="n"&gt;cdmtx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;关系可以画成：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;epfd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;└── eventpoll&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── rbr：全部已注册 epitem&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── rdlist：当前就绪 epitem&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── cond：没有就绪事件时，让 nepoll_wait 睡眠&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="八三个外部接口怎样组成应用侧逻辑"&gt;八、三个外部接口怎样组成应用侧逻辑&#10;&lt;/h2&gt;&lt;h3 id="1-nepoll_create创建事件管理器"&gt;1. &lt;code&gt;nepoll_create()&lt;/code&gt;：创建事件管理器&#10;&lt;/h3&gt;&lt;p&gt;它做的事情不是创建 TCP 连接，而是创建一个 &lt;code&gt;eventpoll&lt;/code&gt;：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;分配 epfd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 分配 eventpoll&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 初始化红黑树&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 初始化就绪链表&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 初始化锁与条件变量&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 建立 epfd 到 eventpoll 的映射&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;应用以后通过 &lt;code&gt;epfd&lt;/code&gt; 找到这一整套事件管理状态。&lt;/p&gt;&#10;&lt;h3 id="2-nepoll_ctl维护整集"&gt;2. &lt;code&gt;nepoll_ctl()&lt;/code&gt;：维护整集&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ADD：树中不存在 → 分配 epitem → 插入红黑树&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DEL：树中存在 → 从红黑树移除 → 释放 epitem&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;MOD：树中存在 → 修改关注的事件&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;因此，&lt;code&gt;nepoll_ctl()&lt;/code&gt; 只是在表达应用的兴趣：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;“如果这个 fd 以后出现我关心的状态，请通知我。”&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;注册本身不会凭空制造事件。&lt;/p&gt;&#10;&lt;h3 id="3-nepoll_wait没有事件就睡有事件就返回"&gt;3. &lt;code&gt;nepoll_wait()&lt;/code&gt;：没有事件就睡，有事件就返回&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rdnum&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;pthread_cond_timedwait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cond&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cdmtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nf"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;pthread_cond_wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cond&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cdmtx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;超时语义：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;timeout &amp;lt; 0&lt;/code&gt;：一直等待；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;timeout == 0&lt;/code&gt;：立即检查并返回；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;timeout &amp;gt; 0&lt;/code&gt;：最多等待指定时长。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;醒来后，它从就绪链表取出最多 &lt;code&gt;maxevents&lt;/code&gt; 个节点，把其中的 &lt;code&gt;epoll_event&lt;/code&gt; 复制给应用：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rdlist 中的 epitem&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ events[0...n-1]&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 返回 nready&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;至此，epoll 只告诉应用“哪些 fd 可以处理”。真正的 &lt;code&gt;accept/recv/send&lt;/code&gt; 仍由应用自己调用。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="九最关键的内部接口协议栈怎样触发事件"&gt;九、最关键的内部接口：协议栈怎样触发事件&#10;&lt;/h2&gt;&lt;p&gt;三个 &lt;code&gt;nepoll_*&lt;/code&gt; 接口面向应用；&lt;code&gt;epoll_event_callback()&lt;/code&gt; 面向协议栈：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;epoll_event_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;eventpoll&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;sockid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;epitem&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;epi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;RB_FIND&lt;/span&gt;&lt;span class="p"&gt;(...,&lt;/span&gt; &lt;span class="n"&gt;sockid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;epi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epi&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rdy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;epi&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;pthread_spin_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;epi&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rdy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;LIST_INSERT_HEAD&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rdlist&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;epi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rdlink&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rdnum&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;pthread_spin_unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;lock&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;pthread_mutex_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cdmtx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;pthread_cond_signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cond&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;pthread_mutex_unlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;cdmtx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它完成四步：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;按 sockid 查整集&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 把事件记录到对应 epitem&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 把 epitem 加入就绪链表&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ signal 唤醒 nepoll_wait&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果节点已经在就绪链表中，就不重复插入，只合并事件位：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;epi&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;|=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这避免同一个 fd 连续收到多个通知时，在 &lt;code&gt;rdlist&lt;/code&gt; 中出现多个相同节点。&lt;/p&gt;&#10;&lt;h3 id="epoll-模块在整体架构中的位置"&gt;epoll 模块在整体架构中的位置&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;协议栈 epoll 应用&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;握手完成 ─┐&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;收到数据 ─┼→ event_callback → rdlist → cond_signal → nepoll_wait&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;收到 FIN ─┘ ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; accept/recv/close&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;所以 epoll 不是协议栈之外的独立轮询器。它必须由协议栈在状态真正改变的地方主动通知。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十用三条时间线彻底串起实现"&gt;十、用三条时间线彻底串起实现&#10;&lt;/h2&gt;&lt;h3 id="时间线一新客户端完成连接"&gt;时间线一：新客户端完成连接&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;① 客户端发送 SYN&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;② ng_tcp_process 找到 LISTEN stream&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;③ 为客户端创建新的 stream，进入 SYN_RCVD&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;④ 协议栈发送 SYN+ACK&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;⑤ 收到客户端 ACK，新 stream 进入 ESTABLISHED&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;⑥ 协议栈触发 listenfd 的 EPOLLIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;⑦ nepoll_wait 返回 listenfd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;⑧ 应用调用 naccept(listenfd)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;⑨ naccept 取出已建立 stream，为它分配 connfd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;⑩ 应用用 EPOLL_CTL_ADD 把 connfd 加入 epoll&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对应代码的关键通知：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;NG_TCP_STATUS_ESTABLISHED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;ng_tcp_stream&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;listener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;ng_tcp_stream_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;dport&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;epoll_event_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EPOLLIN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;为什么通知 &lt;code&gt;listener-&amp;gt;fd&lt;/code&gt;，而不是新连接的 fd？&lt;/p&gt;&#10;&lt;p&gt;因为此时应用还没有执行 &lt;code&gt;naccept()&lt;/code&gt;，新 stream 还没有分配给应用使用的 &lt;code&gt;connfd&lt;/code&gt;。对应用来说，当前可执行的动作是 &lt;code&gt;accept(listenfd)&lt;/code&gt;，所以就绪的是监听 fd。&lt;/p&gt;&#10;&lt;h3 id="时间线二已连接客户端发送-payload"&gt;时间线二：已连接客户端发送 payload&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;① 报文按四元组找到 established stream&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;② 协议栈复制 payload，构造 fragment&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;③ fragment 进入 stream-&amp;gt;rcvbuf&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;④ 协议栈触发 stream-&amp;gt;fd 的 EPOLLIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;⑤ callback 找到 connfd 对应的 epitem&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;⑥ epitem 进入 rdlist，唤醒 nepoll_wait&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;⑦ 应用得到 connfd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;⑧ 应用调用 nrecv(connfd)，从该 stream 的 rcvbuf 取数据&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;关键顺序必须是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;先让数据真正可读&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 再发布 EPOLLIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;否则应用被唤醒后可能发现 &lt;code&gt;rcvbuf&lt;/code&gt; 仍为空。&lt;/p&gt;&#10;&lt;h3 id="时间线三客户端发送-fin"&gt;时间线三：客户端发送 FIN&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;① 协议栈收到 FIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;② stream 进入 CLOSE_WAIT&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;③ rcvbuf 中放入长度为 0 的 fragment，表示 EOF&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;④ 触发 connfd 的 EPOLLIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;⑤ nepoll_wait 返回 connfd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;⑥ nrecv(connfd) 返回 0&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;⑦ 应用 EPOLL_CTL_DEL，然后 nclose(connfd)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这条时间线解释了为什么“关闭”也能通过可读事件交给应用：读取操作已经不会阻塞，只是结果为 EOF。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十一epoll-服务器循环现在应该怎样读"&gt;十一、epoll 服务器循环现在应该怎样读&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;epfd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;nepoll_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EPOLLIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;listenfd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;nepoll_ctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EPOLL_CTL_ADD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;listenfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;nready&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;nepoll_wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;nready&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;listenfd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;connfd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;naccept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;listenfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EPOLLIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;connfd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;nepoll_ctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EPOLL_CTL_ADD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;connfd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;nrecv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFER_SIZE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;nsend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;nepoll_ctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EPOLL_CTL_DEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;connfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;nclose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connfd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;不要只把它看成一个 API 调用顺序。这个循环其实在处理两类状态机入口：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;listenfd 就绪&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 连接管理路径&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ accept 新连接并将 connfd 注册进 epoll&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;connfd 就绪&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 数据路径或关闭路径&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ recv 数据，或者识别 EOF 后移除连接&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;多个客户端的并发体现在：所有 &lt;code&gt;connfd&lt;/code&gt; 都在同一棵红黑树中，但某一时刻只有真正就绪的连接进入 &lt;code&gt;rdlist&lt;/code&gt;。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十二薄弱但非常重要ltet-与消费事件"&gt;十二、薄弱但非常重要：LT、ET 与“消费事件”&#10;&lt;/h2&gt;&lt;p&gt;理解整集和就绪集后，还必须区分两种事件语义。&lt;/p&gt;&#10;&lt;h3 id="1-ltlevel-triggered水平触发"&gt;1. LT：Level Triggered，水平触发&#10;&lt;/h3&gt;&lt;p&gt;只要可读条件仍然成立，就应该继续报告：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rcvbuf 里还有数据&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ fd 仍然可读&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 下一次 epoll_wait 仍应返回它&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;应用可以一次只读一部分，剩余数据会让 fd 继续保持就绪。&lt;/p&gt;&#10;&lt;h3 id="2-etedge-triggered边沿触发"&gt;2. ET：Edge Triggered，边沿触发&#10;&lt;/h3&gt;&lt;p&gt;只在状态从“不可读”变为“可读”的边沿通知一次：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;rcvbuf：空 → 非空&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 触发一次 EPOLLIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果应用没有把数据一直读到 &lt;code&gt;EAGAIN&lt;/code&gt;，而 &lt;code&gt;rcvbuf&lt;/code&gt; 始终保持非空，就可能没有新的空→非空边沿，因此也不会再次收到通知。&lt;/p&gt;&#10;&lt;h3 id="3-当前学习代码的核心行为"&gt;3. 当前学习代码的核心行为&#10;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;epoll_event_callback()&lt;/code&gt; 把节点加入 &lt;code&gt;rdlist&lt;/code&gt;；&lt;code&gt;nepoll_wait()&lt;/code&gt; 返回后将其移出并清除 &lt;code&gt;rdy&lt;/code&gt;。以后再次发生协议栈回调时，节点才能重新加入。&lt;/p&gt;&#10;&lt;p&gt;这更接近“协议栈事件通知队列”的实现骨架。要形成严格的 LT 或 ET，还需要把&lt;strong&gt;就绪谓词&lt;/strong&gt;定义清楚：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;监听 fd 可读：accept 队列非空&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;连接 fd 可读：rcvbuf 非空，或存在 EOF/错误&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;连接 fd 可写：sndbuf 有可用空间&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然后选择策略：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;LT：&lt;code&gt;epoll_wait&lt;/code&gt; 返回事件后，如果谓词仍为真，应保留或重新加入就绪集。&lt;/li&gt;&#10;&lt;li&gt;ET：只有谓词从假变真时才入就绪集，应用必须把资源处理到再次变为假。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;这部分是自实现 epoll 最容易遗漏的核心：&lt;strong&gt;就绪链表只是结果，真正决定事件语义的是状态谓词和重新入队规则。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十三线程同步为什么需要三种同步手段"&gt;十三、线程同步：为什么需要三种同步手段&#10;&lt;/h2&gt;&lt;p&gt;代码中不同锁保护不同对象：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;同步对象&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;主要保护内容&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;ep-&amp;gt;mtx&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;红黑树的 ADD/DEL/MOD&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;ep-&amp;gt;lock&lt;/code&gt; 自旋锁&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;短时间修改 &lt;code&gt;rdlist&lt;/code&gt;、&lt;code&gt;rdnum&lt;/code&gt;、&lt;code&gt;rdy&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;ep-&amp;gt;cdmtx + cond&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;nepoll_wait&lt;/code&gt; 睡眠与协议栈唤醒&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;h3 id="1-为什么等待必须检查条件"&gt;1. 为什么等待必须检查条件&#10;&lt;/h3&gt;&lt;p&gt;正确思路不是“收到 signal 就相信一定有事件”，而是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rdnum&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;pthread_cond_wait&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;条件变量可能虚假唤醒，也可能多个等待者竞争同一批事件。醒来后必须重新检查 &lt;code&gt;rdnum&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;同理，&lt;code&gt;nrecv()&lt;/code&gt; 等待数据时也使用循环检查 &lt;code&gt;rcvbuf&lt;/code&gt;，而不是单次 &lt;code&gt;if&lt;/code&gt;。&lt;/p&gt;&#10;&lt;h3 id="2-为什么先入就绪集再-signal"&gt;2. 为什么先入就绪集，再 signal&#10;&lt;/h3&gt;&lt;p&gt;发布顺序是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;修改受保护状态：epitem 进入 rdlist，rdnum++&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;→ 再 signal&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;signal 的作用只是提醒等待线程重新检查条件；真正可信的是受锁保护的 &lt;code&gt;rdlist/rdnum&lt;/code&gt;。&lt;/p&gt;&#10;&lt;h3 id="3-del-与回调之间的关系"&gt;3. &lt;code&gt;DEL&lt;/code&gt; 与回调之间的关系&#10;&lt;/h3&gt;&lt;p&gt;应用关闭连接时会删除 &lt;code&gt;epitem&lt;/code&gt;，协议栈线程则可能正在为同一 fd 触发回调。完整实现必须让红黑树查找、节点删除和就绪链表访问遵守一致的生命周期规则，避免回调继续访问已经释放的节点。&lt;/p&gt;&#10;&lt;p&gt;这里不展开代码审查，只需记住：并发数据结构除了“查得快”，还要保证节点从注册、就绪到删除期间始终有效。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十四单-epoll-与多个-epoll"&gt;十四、单 epoll 与多个 epoll&#10;&lt;/h2&gt;&lt;p&gt;当前代码在创建时执行：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;ng_tcp_table&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;tcpInstance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;协议栈触发事件时也直接使用：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;epoll_event_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EPOLLIN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这意味着当前教学实现默认整个 TCP table 只有一个 epoll。它足以展示完整通知链路，但还不能表达：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;同一进程创建多个 epoll 实例；&lt;/li&gt;&#10;&lt;li&gt;不同 epoll 注册不同连接；&lt;/li&gt;&#10;&lt;li&gt;同一个 fd 同时被多个 epoll 关注。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;扩展为多个 epoll 时，不能只保留一个 &lt;code&gt;table-&amp;gt;ep&lt;/code&gt;。需要建立：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;epfd → eventpoll&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;以及&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sockfd → 所有注册了该 sockfd 的 epitem/eventpoll&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;协议栈产生事件后，要找到所有相关订阅者并分别更新它们的就绪集。这实际上把单指针通知扩展成一对多的观察者关系。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十五多连接-core-dump-在本篇中的位置"&gt;十五、多连接 core dump 在本篇中的位置&#10;&lt;/h2&gt;&lt;p&gt;多连接时暴露 core dump，说明“单连接路径成立”并不等于“所有连接级资源都已正确隔离”。本节笔记直接涉及的一点是：每条 stream 都会创建自己的 DPDK ring，而 DPDK 命名对象需要可区分的名称。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sbufname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;sndbuf%x%d&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sport&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;sndbuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rte_ring_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sbufname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rbufname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;bufname%x%d&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sport&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rcvbuf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;rte_ring_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rbufname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里用客户端 IP 和源端口区分连接 ring。要记住三层不同的身份：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;四/五元组：让协议栈把报文分给正确 stream&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DPDK ring 名称：让每条 stream 获得独立缓冲对象&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;用户态 fd：让应用和 epoll 找到正确 stream&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;本篇不继续展开具体崩溃排查，因为重点是 epoll 的事件闭环。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十六把全部实现压缩成一张图"&gt;十六、把全部实现压缩成一张图&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 应用线程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; nepoll_ctl ADD │ 注册兴趣&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ┌───────────────┐&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ eventpoll │&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ │&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ 红黑树 rbr │ ← 全部注册 fd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ 就绪表 rdlist │ ← 当前就绪 fd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ cond │ ← 没事件时睡眠&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └───────┬───────┘&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ nepoll_wait 返回&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▼&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; accept / recv / send / close&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ▲&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ 用户态 fd → stream&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ┌───────┴────────┐&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ ng_tcp_stream │&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ 状态/SEQ/ACK │&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │ rcvbuf/sndbuf │&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └───────▲────────┘&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ng_tcp_process 按四元组定位&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 握手完成 / payload 到达 / FIN 到达&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; epoll_event_callback&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; │&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 加入 rdlist + signal&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;从这张图可以看到，TCP 并发 epoll 不是单独一个数据结构，而是四套映射协作：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;报文四元组 → stream&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;用户态 fd → stream&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;epfd → eventpoll&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;eventpoll 中 sockfd → epitem&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;缺少任何一层，事件都无法从报文准确传递到应用。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十七最终应掌握的实现逻辑"&gt;十七、最终应掌握的实现逻辑&#10;&lt;/h2&gt;&lt;ol&gt;&#10;&lt;li&gt;多连接首先要求每条连接拥有独立 TCB、状态、序列号和收发缓冲区。&lt;/li&gt;&#10;&lt;li&gt;报文通过四元组找到 stream，应用通过用户态 fd 找到同一个 stream。&lt;/li&gt;&#10;&lt;li&gt;一连接一线程让每个线程阻塞等一个 fd；epoll 让一个线程等待一组 fd。&lt;/li&gt;&#10;&lt;li&gt;用户态 fd 不属于内核，因此必须自实现 epoll，不能直接交给 Linux epoll。&lt;/li&gt;&#10;&lt;li&gt;红黑树保存“关注谁”，就绪链表保存“现在谁能处理”。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;nepoll_ctl()&lt;/code&gt; 建立兴趣关系，&lt;code&gt;epoll_event_callback()&lt;/code&gt; 发布事件，&lt;code&gt;nepoll_wait()&lt;/code&gt; 消费事件。&lt;/li&gt;&#10;&lt;li&gt;握手完成触发监听 fd 的 &lt;code&gt;EPOLLIN&lt;/code&gt;，因为此时 &lt;code&gt;accept&lt;/code&gt; 不再阻塞。&lt;/li&gt;&#10;&lt;li&gt;payload 或 FIN 到达触发连接 fd 的 &lt;code&gt;EPOLLIN&lt;/code&gt;，因为此时 &lt;code&gt;recv&lt;/code&gt; 能返回数据或 EOF。&lt;/li&gt;&#10;&lt;li&gt;必须先改变真实就绪状态，再把事件加入就绪集并唤醒等待线程。&lt;/li&gt;&#10;&lt;li&gt;LT/ET 的本质不在链表名字，而在就绪谓词以及事件消费后的重新入队规则。&lt;/li&gt;&#10;&lt;li&gt;单 epoll 可以用 &lt;code&gt;tcp_table-&amp;gt;ep&lt;/code&gt;；多个 epoll 需要 fd 到多个订阅者的映射。&lt;/li&gt;&#10;&lt;li&gt;epoll 解决应用调度问题，不替代 TCP 状态机，也不能弥补连接状态未隔离的问题。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h2 id="十八反刍题"&gt;十八、反刍题&#10;&lt;/h2&gt;&lt;ol&gt;&#10;&lt;li&gt;为什么三次握手完成时触发 &lt;code&gt;listenfd&lt;/code&gt;，而不是新连接 fd？&lt;/li&gt;&#10;&lt;li&gt;为什么 FIN 可以通过 &lt;code&gt;EPOLLIN&lt;/code&gt; 通知？&lt;/li&gt;&#10;&lt;li&gt;红黑树和就绪链表各自回答什么问题？&lt;/li&gt;&#10;&lt;li&gt;为什么一个 &lt;code&gt;epitem&lt;/code&gt; 要同时包含 &lt;code&gt;RB_ENTRY&lt;/code&gt; 和 &lt;code&gt;LIST_ENTRY&lt;/code&gt;？&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;nepoll_ctl()&lt;/code&gt; 注册了一个 fd 后，为什么它不一定立刻出现在就绪链表？&lt;/li&gt;&#10;&lt;li&gt;从 payload 到达到 &lt;code&gt;nepoll_wait()&lt;/code&gt; 返回，中间依次发生了什么？&lt;/li&gt;&#10;&lt;li&gt;如果 &lt;code&gt;rcvbuf&lt;/code&gt; 仍有未读数据，LT 和 ET 下一步分别应该怎样表现？&lt;/li&gt;&#10;&lt;li&gt;为什么条件变量的等待必须放在 &lt;code&gt;while&lt;/code&gt; 中？&lt;/li&gt;&#10;&lt;li&gt;为什么发布事件时要先更新 &lt;code&gt;rdlist/rdnum&lt;/code&gt;，再调用 &lt;code&gt;signal&lt;/code&gt;？&lt;/li&gt;&#10;&lt;li&gt;当前的 &lt;code&gt;tcp_table-&amp;gt;ep&lt;/code&gt; 为什么只能自然表达单 epoll？&lt;/li&gt;&#10;&lt;li&gt;如果一个 connfd 被两个 epoll 实例注册，协议栈该怎样分发一次 &lt;code&gt;EPOLLIN&lt;/code&gt;？&lt;/li&gt;&#10;&lt;li&gt;epoll 已经实现正确，但不同连接仍共用 &lt;code&gt;rcvbuf&lt;/code&gt;，并发为什么仍然会失败？&lt;/li&gt;&#10;&lt;/ol&gt;&#10;</description></item><item><title>协程运行时：IO Hook、调度器与汇编上下文切换</title><link>https://jasperstonnne.github.io/MyBlog/p/coroutine-runtime-io-hook-scheduler-context-switch/</link><pubDate>Tue, 11 Aug 2026 00:00:00 +0800</pubDate><guid>https://jasperstonnne.github.io/MyBlog/p/coroutine-runtime-io-hook-scheduler-context-switch/</guid><description>&lt;img src="https://jasperstonnne.github.io/MyBlog/p/coroutine-runtime-io-hook-scheduler-context-switch/cover.svg" alt="Featured image of post 协程运行时：IO Hook、调度器与汇编上下文切换" /&gt;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;本篇承接 yield、resume、ucontext 和简单轮询调度，重点理解阻塞式 I/O 的 Hook、协程状态管理、调度器与 epoll 的协作，以及 x86-64 汇编上下文切换。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="一从上下文切换走向完整协程运行时"&gt;一、从上下文切换走向完整协程运行时&#10;&lt;/h2&gt;&lt;p&gt;已经掌握的两个方向：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;main → ctx ：resume&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ctx → main ：yield&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它们底层都依赖一次 context switch：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;保存当前执行现场&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;恢复目标执行现场&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;目标协程从上次暂停位置继续&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;但只有 switch、resume 和 yield 还不够。真正处理网络 I/O 还需要回答：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1. 普通read/recv没有数据时，怎样自动yield？&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2. fd就绪以后，怎样找到并resume对应协程？&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3. 十万个协程如何保存和组织？&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4. READY、WAITING、SLEEPING、EXITED如何管理？&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;5. 定时器和I/O超时如何处理？&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;6. 上下文切换怎样用汇编实现？&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;7. 多核机器上怎样运行多个调度器？&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;最终需要形成：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;POSIX I/O调用&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Hook层&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;非阻塞I/O + epoll&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;yield / resume&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;协程状态和调度器&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ucontext或汇编context switch&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="二为什么需要-hook-io"&gt;二、为什么需要 Hook I/O&#10;&lt;/h2&gt;&lt;p&gt;一个协程中的业务代码希望保持顺序结构：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;阅读时仍然是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;接收数据&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;解析数据&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;发送响应&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;但如果这里直接调用阻塞式 recv()，没有数据时会阻塞整个线程，线程内其他协程也无法运行。&lt;/p&gt;&#10;&lt;p&gt;协程库希望在不大改业务流程的情况下，把调用替换为自己的实现：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;业务调用recv/read&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;进入协程库Hook函数&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;先尝试非阻塞I/O&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── 已就绪：调用真实系统函数并返回&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── 未就绪：注册epoll事件并yield&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; fd就绪&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; resume协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 再调用真实系统函数&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这就是：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;上层保持同步代码结构，底层通过 epoll 和协程实现异步等待。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="三hook-的概念流程"&gt;三、Hook 的概念流程&#10;&lt;/h2&gt;&lt;p&gt;笔记中的伪代码：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;recv&lt;/span&gt;&lt;span class="err"&gt;（&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="err"&gt;）&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;pollfd&lt;/span&gt; &lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)){&lt;/span&gt;&lt;span class="c1"&gt;//io可读&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;recv_f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;epoll_ctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;EPOLL_CTL_ADD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;swapcontext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它表达了四步。&lt;/p&gt;&#10;&lt;h3 id="31-检查-fd-是否就绪"&gt;3.1 检查 fd 是否就绪&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;timeout 为 0，表示立即检查，不在 poll() 中等待。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;返回值 &amp;gt; 0 → 当前fd有事件&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;返回值 = 0 → 当前没有事件&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;返回值 &amp;lt; 0 → 调用发生错误&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="32-已就绪时调用真实函数"&gt;3.2 已就绪时调用真实函数&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;recv_f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;recv_f 指向真正的系统 recv，而不是 Hook 后的同名函数。&lt;/p&gt;&#10;&lt;h3 id="33-未就绪时注册-epoll"&gt;3.3 未就绪时注册 epoll&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;epoll_ctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;EPOLL_CTL_ADD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;同时记录：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;哪个fd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;等待什么事件&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;哪个协程正在等待它&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;是否存在超时时间&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="34-当前协程-yield"&gt;3.4 当前协程 yield&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;swapcontext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里代表概念上的 yield。实际实现需要明确传入当前协程上下文和调度器上下文。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;当前协程WAITING&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;yield回调度器&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;调度器运行其他READY协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="四使用-dlsym-拦截-read-和-write"&gt;四、使用 dlsym 拦截 read 和 write&#10;&lt;/h2&gt;&lt;p&gt;以下是提供的 Hook 代码：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define _GNU_SOURCE&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;dlfcn.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;ucontext.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;string.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;unistd.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;fcntl.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;sys/socket.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;errno.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;netinet/in.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;pthread.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;sys/poll.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;sys/epoll.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="41-为什么需要-_gnu_source"&gt;4.1 为什么需要 _GNU_SOURCE&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#define _GNU_SOURCE&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;它在包含系统头文件前启用 GNU 扩展声明，使 dlsym()、RTLD_NEXT 等接口在相应环境中可见。&lt;/p&gt;&#10;&lt;h3 id="42-函数指针类型"&gt;4.2 函数指针类型&#10;&lt;/h3&gt;&lt;p&gt;原代码：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="nf"&gt;ssize_t&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kt"&gt;read_t&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;read_t&lt;/span&gt; &lt;span class="n"&gt;read_f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="nf"&gt;ssize_t&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="kt"&gt;write_t&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;write_t&lt;/span&gt; &lt;span class="n"&gt;write_f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;read_t 描述真实 read() 的函数类型：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;参数1：fd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;参数2：接收缓冲区&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;参数3：最大读取长度&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;返回值：实际读取长度或错误&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;write_t 对应真实 write()。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;read_f → 保存原始read地址&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;write_f → 保存原始write地址&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="五init_hook-如何取得真实函数"&gt;五、init_hook() 如何取得真实函数&#10;&lt;/h2&gt;&lt;p&gt;原代码：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;init_hook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;read_f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;&lt;span class="n"&gt;read_f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dlsym&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RTLD_NEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;read&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;write_f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;&lt;span class="n"&gt;write_f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dlsym&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RTLD_NEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;write&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;dlsym() 根据符号名查找函数地址：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;dlsym&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RTLD_NEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;&amp;#34;read&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;RTLD_NEXT 的含义可以理解为：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;从当前 Hook 定义之后继续寻找下一个名为 read 的实现。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;因此调用关系是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;业务代码调用read()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;进入当前文件定义的Hook read()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Hook内部调用read_f()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;进入真正的系统read()&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果 Hook 内部再次直接调用 read()：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Hook read()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;又进入Hook read()&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;无限递归&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;所以必须使用 read_f 调用真实函数。&lt;/p&gt;&#10;&lt;p&gt;使用 dlsym() 时通常需要链接动态加载库：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;gcc ... -ldl&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="六read-hook-的执行过程"&gt;六、read() Hook 的执行过程&#10;&lt;/h2&gt;&lt;p&gt;原代码：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;ssize_t&lt;/span&gt; &lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;pollfd&lt;/span&gt; &lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;POLLIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;//不可读&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;&lt;span class="c1"&gt;// fd --&amp;gt; epoll_ctl();&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;&lt;span class="nf"&gt;swapcontext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// fd --&amp;gt; ctx fd与context一对一&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="c1"&gt;// io&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="kt"&gt;ssize_t&lt;/span&gt; &lt;span class="n"&gt;ret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;read_f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="nf"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;read: %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ret&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="61-构造-pollfd"&gt;6.1 构造 pollfd&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;pollfd&lt;/span&gt; &lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;POLLIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;表示只检查一个 fd 的可读事件。&lt;/p&gt;&#10;&lt;h3 id="62-立即检查"&gt;6.2 立即检查&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;第三个参数为 0：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;不等待&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;立即返回当前状态&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="63-不可读时暂停协程"&gt;6.3 不可读时暂停协程&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;res&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// fd --&amp;gt; epoll_ctl();&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;swapcontext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;完整运行时还需要在 yield 前完成：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;将fd设置为非阻塞&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;将fd注册到epoll&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;记录fd等待POLLIN/EPOLLIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;记录当前等待协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;设置可选超时时间&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;当前协程状态改为WAITING&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;yield到scheduler&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;fd 就绪后：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;epoll_wait返回fd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;找到等待它的协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;协程进入READY队列&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;scheduler resume协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;从swapcontext()之后继续&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;read_f()真正读取数据&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="64-调用真实-read"&gt;6.4 调用真实 read&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;ssize_t&lt;/span&gt; &lt;span class="n"&gt;ret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;read_f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这一步才真正从内核读取数据。&lt;/p&gt;&#10;&lt;p&gt;poll 告诉程序“当前可能可读”，read_f 才完成实际读取。就绪状态可能在检查和读取之间变化，因此 fd 仍然应该是非阻塞的，并正确处理 EAGAIN。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="七write-hook"&gt;七、write() Hook&#10;&lt;/h2&gt;&lt;p&gt;原代码：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;ssize_t&lt;/span&gt; &lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="nf"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;write: %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;write_f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当前实现主要展示：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;业务调用write&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;进入Hook write&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;调用write_f进入真实write&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;完整协程版 write() 也需要处理：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;真实write成功一部分&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 记录已写偏移，继续写剩余数据&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;真实write返回EAGAIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 注册EPOLLOUT&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 当前协程yield&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → fd可写后resume&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Hook write() 内部使用 printf() 记录日志时要特别小心，因为 printf() 最终也可能调用 write()，从而再次进入 Hook。实际实现通常使用不会再次触发该 Hook 的日志路径，或者直接调用保存下来的真实 write_f。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="八hook-示例怎样接入普通服务端"&gt;八、Hook 示例怎样接入普通服务端&#10;&lt;/h2&gt;&lt;p&gt;原代码：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="nf"&gt;init_hook&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;sockfd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AF_INET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SOCK_STREAM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;sockaddr_in&lt;/span&gt; &lt;span class="n"&gt;serveraddr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="nf"&gt;memset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;serveraddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;sockaddr_in&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="n"&gt;serveraddr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sin_family&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AF_INET&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="n"&gt;serveraddr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sin_addr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s_addr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;htonl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INADDR_ANY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="n"&gt;serveraddr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sin_port&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;htons&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sockfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;sockaddr&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;serveraddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;sockaddr&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;&lt;span class="nf"&gt;perror&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;bind&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sockfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;sockaddr_in&lt;/span&gt; &lt;span class="n"&gt;clientaddr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="kt"&gt;socklen_t&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clientaddr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;clientfd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sockfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;sockaddr&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;clientaddr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="nf"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;accept&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clientfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;&#9;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clientfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&#9;&lt;span class="nf"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;sockfd: %d, clientfd: %d, count: %d, buffer: %s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sockfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clientfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;业务代码仍然调用：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clientfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clientfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;但由于程序中定义了同名 read() 和 write()，符号解析会先进入 Hook。&lt;/p&gt;&#10;&lt;p&gt;这正是 Hook 的价值：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;业务代码结构保持不变&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;read/write调用被协程库接管&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;等待I/O时自动yield&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;就绪后自动resume&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当前示例只接收一个 clientfd，重点是展示 Hook 链路。与真正协程服务器结合时，还需要 accept Hook、多连接协程、epoll mainloop 和 scheduler。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="九为什么第三方库也可以受益"&gt;九、为什么第三方库也可以受益&#10;&lt;/h2&gt;&lt;p&gt;例如本地程序使用 hiredis 连接 Redis。hiredis 内部会调用 read/write 或 send/recv 完成网络操作。&lt;/p&gt;&#10;&lt;p&gt;不修改 hiredis 源码时：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;业务调用hiredis&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;hiredis内部调用read/write&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;进入协程库Hook&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;没有数据时yield&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Redis响应到达后resume&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;因此原本按照阻塞式 POSIX API 编写的库，有机会直接运行在协程调度环境中。&lt;/p&gt;&#10;&lt;p&gt;前提是 Hook 覆盖了它实际调用的 API，并正确处理：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;阻塞标志&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;超时&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;EINTR&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;EAGAIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;部分读写&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;close和fd复用&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;线程安全&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="十如何定义一个协程"&gt;十、如何定义一个协程&#10;&lt;/h2&gt;&lt;p&gt;笔记中的初始设计：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;coroutinue&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;ucontext_t&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="c1"&gt;//stack ，stack_size,func包含在里面&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;一个协程需要保存两类信息。&lt;/p&gt;&#10;&lt;h3 id="101-执行现场"&gt;10.1 执行现场&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ctx → 寄存器、栈指针、执行位置等上下文&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stack → 协程自己的栈&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stack_size → 栈大小&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;entry → 协程入口函数&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;arg → 入口参数&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="102-调度状态"&gt;10.2 调度状态&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;id → 协程标识&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;state → READY、RUNNING、WAITING等&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fd → 当前等待的I/O对象&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;events → 等待EPOLLIN或EPOLLOUT&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;expire_at → 超时时间或唤醒时间&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;scheduler → 所属调度器&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;概念上的完整结构：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;struct coroutine&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── id&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── state&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── context&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── stack / stack_size&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── entry / arg&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── waiting_fd / events&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── expire_at&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── ready_node&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── wait_node&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── sleep_node&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── scheduler&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;本篇沿用原笔记中的 coroutinue 拼写来对应代码，实际项目中可以统一为 coroutine。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十一协程入口与创建"&gt;十一、协程入口与创建&#10;&lt;/h2&gt;&lt;p&gt;原始设计：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;coroutinue_entry&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;create_coroutinue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;co_id&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;coroutinue_entry&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;coroutinue&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;co&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;malloc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;coroutinue&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;co&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ss_sp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;makecontext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;co&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;创建协程需要完成：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;1. 分配struct coroutine&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;2. 分配或绑定独立栈&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;3. 保存entry和arg&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;4. 初始化context&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;5. 设置入口包装函数&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;6. 设置协程退出后的返回目标&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;7. state设为READY&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;8. 加入ready_queue&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;为什么通常需要一个入口包装函数：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;scheduler resume新协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;进入wrapper&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wrapper调用entry(arg)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;entry返回&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wrapper把协程状态设为EXITED&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;yield回scheduler&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;scheduler回收资源&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果直接让入口函数随意 return，而没有统一退出路径，调度器很难正确修改状态和释放栈。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十二十万个协程如何组织"&gt;十二、十万个协程如何组织&#10;&lt;/h2&gt;&lt;p&gt;假设采用“一连接一协程”：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;10万个fd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;10万个coroutine对象&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;但它们不会全部同时运行。&lt;/p&gt;&#10;&lt;p&gt;例如：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;10万个协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── 2万个READY&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── 5万个WAITING&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── 2万个SLEEPING&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── 1万个已结束或待回收&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;调度器必须按状态组织它们，不能每轮扫描全部十万个对象。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十三ready-为什么使用队列"&gt;十三、READY 为什么使用队列&#10;&lt;/h2&gt;&lt;p&gt;笔记中的设计：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;queue_node&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;corountine&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;&lt;span class="n"&gt;ready_queue&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;READY 协程表示现在就可以运行。&lt;/p&gt;&#10;&lt;p&gt;常见操作：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;协程变为READY → 从队尾加入&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;scheduler取任务 → 从队头取出&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;协程yield但仍可运行 → 再放回队尾&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;因此队列适合：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;O(1)入队&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;O(1)出队&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;天然支持轮询公平调度&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果有 2 万个 READY 协程：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ready_head&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → co1 → co2 → co3 → ... → co20000&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;scheduler 每次取队头协程 resume。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十四waiting-与-sleeping-为什么需要有序结构"&gt;十四、WAITING 与 SLEEPING 为什么需要有序结构&#10;&lt;/h2&gt;&lt;p&gt;笔记中的设计：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;rbtree_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coroutinue&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;&lt;span class="n"&gt;wait_rb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;rbtree_node&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coroutinue&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;&lt;span class="n"&gt;sleep_rb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;等待 I/O 的协程可能还设置超时：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;协程A等待fd=4，100ms后超时&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;协程B等待fd=5，500ms后超时&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;协程C等待fd=6，50ms后超时&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;睡眠协程也有不同唤醒时间：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;协程D睡眠到10:00:00.100&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;协程E睡眠到10:00:00.800&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果以 expire_at 为 key 放入有序结构，调度器可以快速找到最早到期节点：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;最小expire_at&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;与当前时间比较&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;已经到期就移出&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;状态改为READY&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;加入ready_queue&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;红黑树、最小堆、跳表、时间轮都可以实现定时管理，具体选择取决于插入、删除、精度和规模要求。&lt;/p&gt;&#10;&lt;p&gt;I/O fd 到协程的查找，还可以另外使用：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;epoll_event.data.ptr&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;哈希表 fd → coroutine&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;数组 fd → coroutine&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;时间树解决“谁先超时”，fd 映射解决“哪个事件唤醒谁”，它们是两个不同查找维度。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十五一个协程为什么可以同时拥有多个节点"&gt;十五、一个协程为什么可以同时拥有多个节点&#10;&lt;/h2&gt;&lt;p&gt;笔记中的思路是把数据结构节点直接放进 coroutine：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ready_node&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wait_node&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sleep_node&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这种方式称为侵入式数据结构：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;coroutine对象本身包含链表节点或树节点&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;不需要额外分配包装节点&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;从节点可以找回所属coroutine&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;但协程状态必须保持一致：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;READY → 位于ready_queue&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;WAITING → 位于I/O等待集合，可能同时位于超时树&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SLEEPING → 位于sleep定时结构&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RUNNING → 当前正在执行，通常不在ready_queue&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;EXITED → 位于回收集合或等待立即回收&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;协程从一个状态切到另一个状态时，需要从旧集合移除，再加入新集合，避免同一个节点被重复挂载。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十六scheduler-如何定义"&gt;十六、scheduler 如何定义&#10;&lt;/h2&gt;&lt;p&gt;笔记中的设计：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;scheduler&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;epfd&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;epoll_event&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;queue_node&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;corountine&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;&lt;span class="n"&gt;ready_head&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;rbtree_root&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coroutinue&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;&lt;span class="n"&gt;wait_rb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;rbtree_root&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coroutinue&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt;&lt;span class="n"&gt;sleep_rb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;各字段含义：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;epfd → epoll实例&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;events[] → epoll_wait返回的就绪事件&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ready_head → 当前可以运行的协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wait_rb → 等待I/O并带超时的协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sleep_rb → 主动睡眠的协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;完整调度器还会需要：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;main_context → scheduler自己的上下文&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;current → 当前正在运行的协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;all_coroutines → 所有协程的所有权集合&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;exited_queue → 等待回收的协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;stop → 退出标志&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;thread_id → 所属线程&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="十七哪些操作由协程执行哪些由调度器执行"&gt;十七、哪些操作由协程执行，哪些由调度器执行&#10;&lt;/h2&gt;&lt;h3 id="171-协程主动执行"&gt;17.1 协程主动执行&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;调用read/recv/send/write&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;发现需要等待后yield&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;主动sleep&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;业务处理&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;入口函数返回并exit&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="172-调度器执行"&gt;17.2 调度器执行&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;维护ready_queue&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;计算最近超时时间&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;调用epoll_wait&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;处理fd就绪事件&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;处理wait/sleep超时&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;把协程转为READY&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;选择下一个协程resume&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;回收EXITED协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;核心边界：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;协程表达“我要等待什么”，调度器决定“等待期间运行谁，以及何时唤醒我”。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十八调度循环的完整流程"&gt;十八、调度循环的完整流程&#10;&lt;/h2&gt;&lt;p&gt;一个合理的 scheduler mainloop 可以概括为：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;while没有停止：&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 1. 处理已经到期的sleep节点&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 2. 处理已经超时的I/O等待节点&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 3. 处理ready_queue中的协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 4. 根据最近定时器计算epoll_wait超时&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 5. 调用epoll_wait&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 6. 根据events找到等待协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 7. 将就绪协程加入ready_queue&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; 8. 回收已经退出的协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果 ready_queue 非空：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;可以先继续运行READY协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;epoll_wait使用timeout=0避免阻塞&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果 ready_queue 为空：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;根据最近timer计算等待时间&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;epoll_wait可以阻塞到I/O到达或定时器到期&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这样线程不会忙等。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="十九协程状态怎样流转"&gt;十九、协程状态怎样流转&#10;&lt;/h2&gt;&lt;p&gt;典型状态：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;NEW&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ create&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;READY&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ scheduler resume&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RUNNING&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── 等待I/O → WAITING&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── sleep → SLEEPING&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── 主动yield且仍可运行 → READY&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── entry返回 → EXITED&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;WAITING&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ├── fd就绪 → READY&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── 超时 → READY，并携带timeout结果&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;SLEEPING&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; └── 时间到 → READY&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;EXITED&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓ scheduler回收&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;DESTROYED&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;状态与容器必须同步：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;state = READY&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↔ 在ready_queue中&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;state = WAITING&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↔ 在I/O等待映射中&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;state = SLEEPING&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↔ 在timer结构中&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="二十setjmpucontext-和汇编怎样选择"&gt;二十、setjmp、ucontext 和汇编怎样选择&#10;&lt;/h2&gt;&lt;p&gt;三种方案：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;setjmp / longjmp&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ucontext&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;architecture-specific assembly&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="201-setjmp--longjmp"&gt;20.1 setjmp / longjmp&#10;&lt;/h3&gt;&lt;p&gt;优点：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;标准C接口&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;可移植性相对好&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;适合理解保存点和恢复点&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;限制：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;不直接提供独立协程栈管理&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;构造完整栈式协程较复杂&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="202-ucontext"&gt;20.2 ucontext&#10;&lt;/h3&gt;&lt;p&gt;优点：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;getcontext/makecontext/swapcontext语义清晰&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;可以直接指定独立栈和入口函数&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;教学和原型实现简单&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;限制：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;已经不属于现代POSIX标准&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;不同平台支持情况不一致&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="203-汇编"&gt;20.3 汇编&#10;&lt;/h3&gt;&lt;p&gt;优点：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;直接控制保存和恢复的寄存器&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;切换路径短&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;适合性能敏感的运行时&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;限制：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;与CPU架构、ABI和调用约定绑定&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;x86-64、ARM64等需要不同实现&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;测试和维护成本高&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;性能不能只靠固定排名判断，需要在相同编译器、架构和切换模型下基准测试。汇编通常能做得更精简，但正确性和可移植性同样重要。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="二十一x86-64-汇编上下文切换原代码"&gt;二十一、x86-64 汇编上下文切换原代码&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#elif defined(__x86_64__)&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;__asm__&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; .text &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; .p2align 4,,15 &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34;.globl _switch &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34;.globl __switch &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34;_switch: &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34;__switch: &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; movq %rsp, 0(%rsi) # save stack_pointer &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; movq %rbp, 8(%rsi) # save frame_pointer &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; movq (%rsp), %rax # save insn_pointer &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; movq %rax, 16(%rsi) &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; movq %rbx, 24(%rsi) # save rbx,r12-r15 &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; movq %r12, 32(%rsi) &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; movq %r13, 40(%rsi) &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; movq %r14, 48(%rsi) &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; movq %r15, 56(%rsi) &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; movq 56(%rdi), %r15 &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; movq 48(%rdi), %r14 &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; movq 40(%rdi), %r13 # restore rbx,r12-r15 &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; movq 32(%rdi), %r12 &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; movq 24(%rdi), %rbx &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; movq 8(%rdi), %rbp # restore frame_pointer &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; movq 0(%rdi), %rsp # restore stack_pointer &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; movq 16(%rdi), %rax # restore insn_pointer &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; movq %rax, (%rsp) &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s"&gt;&amp;#34; ret &lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="cp"&gt;#endif &#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;对应的上下文结构：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;_nty_cpu_ctx&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;esp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;//&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ebp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;eip&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;edi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;esi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ebx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;r1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;r2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;r3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;r4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;r5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;nty_cpu_ctx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;_switch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nty_cpu_ctx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;new_ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nty_cpu_ctx&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;cur_ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;函数语义：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;new_ctx → 即将恢复的目标上下文&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cur_ctx → 当前需要保存的上下文&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在 System V AMD64 ABI 中，前两个整数或指针参数通常放在：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;第一个参数new_ctx → RDI&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;第二个参数cur_ctx → RSI&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;因此：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RSI作为基地址 → 保存当前上下文&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RDI作为基地址 → 加载目标上下文&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="二十二汇编第一部分保存当前上下文"&gt;二十二、汇编第一部分：保存当前上下文&#10;&lt;/h2&gt;&lt;h3 id="221-保存栈指针"&gt;22.1 保存栈指针&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-asm" data-lang="asm"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;movq&lt;/span&gt; &lt;span class="nv"&gt;%rsp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;%rsi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;含义：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;把当前RSP保存到cur_ctx偏移0的位置&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;RSP 指向当前线程正在使用的栈顶。恢复 RSP 才能回到该协程自己的栈。&lt;/p&gt;&#10;&lt;h3 id="222-保存帧指针"&gt;22.2 保存帧指针&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-asm" data-lang="asm"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;movq&lt;/span&gt; &lt;span class="nv"&gt;%rbp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;%rsi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;一个指针占 8 字节，所以第二个字段位于偏移 8。&lt;/p&gt;&#10;&lt;h3 id="223-保存返回地址"&gt;22.3 保存返回地址&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-asm" data-lang="asm"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;movq&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;%rsp&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;%rax&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;movq&lt;/span&gt; &lt;span class="nv"&gt;%rax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;%rsi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;调用 _switch 时，返回地址位于当前栈顶。先读到 RAX，再保存到 cur_ctx 偏移 16。&lt;/p&gt;&#10;&lt;p&gt;这个地址就是以后恢复协程时需要继续执行的位置。&lt;/p&gt;&#10;&lt;h3 id="224-保存被调用者保存寄存器"&gt;22.4 保存被调用者保存寄存器&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-asm" data-lang="asm"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;movq&lt;/span&gt; &lt;span class="nv"&gt;%rbx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;%rsi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;movq&lt;/span&gt; &lt;span class="nv"&gt;%r12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;%rsi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;movq&lt;/span&gt; &lt;span class="nv"&gt;%r13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;%rsi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;movq&lt;/span&gt; &lt;span class="nv"&gt;%r14&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;%rsi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;movq&lt;/span&gt; &lt;span class="nv"&gt;%r15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;%rsi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这些寄存器按照 x86-64 System V ABI 属于 callee-saved registers。函数返回后调用方有权期待它们保持不变，因此 context switch 必须保存和恢复。&lt;/p&gt;&#10;&lt;p&gt;偏移每次增加 8：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;0 → rsp&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;8 → rbp&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;16 → rip/返回地址&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;24 → rbx&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;32 → r12&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;40 → r13&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;48 → r14&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;56 → r15&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;原结构中的 esp、ebp、eip 是 32 位风格名称；在这段 x86-64 汇编中实际保存的是 rsp、rbp 和返回地址。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="二十三汇编第二部分恢复目标上下文"&gt;二十三、汇编第二部分：恢复目标上下文&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-asm" data-lang="asm"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;movq&lt;/span&gt; &lt;span class="mi"&gt;56&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;%rdi&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;%r15&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;movq&lt;/span&gt; &lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;%rdi&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;%r14&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;movq&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;%rdi&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;%r13&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;movq&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;%rdi&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;%r12&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;movq&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;%rdi&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;%rbx&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;movq&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;%rdi&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;%rbp&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;movq&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;%rdi&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;%rsp&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;movq&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;%rdi&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;%rax&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;movq&lt;/span&gt; &lt;span class="nv"&gt;%rax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;%rsp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;ret&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这部分按相反方向加载 new_ctx：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;恢复r15、r14、r13、r12、rbx&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;恢复rbp&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;恢复目标协程rsp&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;把目标返回地址放到目标栈顶&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ret弹出返回地址&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;从目标协程保存位置继续执行&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;最关键的一步：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-asm" data-lang="asm"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;movq&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;%rdi&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nv"&gt;%rsp&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;RSP 一旦切换，当前使用的栈就从旧协程栈变成了目标协程栈。&lt;/p&gt;&#10;&lt;p&gt;最后：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-asm" data-lang="asm"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;ret&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;从目标栈弹出保存的指令地址，于是代码看起来像目标协程之前调用的 _switch() 正常返回。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="二十四为什么不是保存所有寄存器"&gt;二十四、为什么不是保存所有寄存器&#10;&lt;/h2&gt;&lt;p&gt;x86-64 有更多通用寄存器，但按照函数调用约定分为：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;caller-saved → 调用方负责在需要时保存&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;callee-saved → 被调用函数必须保证返回后不变&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;_switch() 以普通函数调用形式进入，编译器会按照 ABI 处理 caller-saved 寄存器中的临时值，因此最小切换实现主要保存 callee-saved 寄存器、栈指针和返回位置。&lt;/p&gt;&#10;&lt;p&gt;这依赖：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;明确的ABI&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;正确的函数声明&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;编译器遵循调用约定&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;汇编与结构体布局完全一致&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;System V AMD64 中，前六个整数或指针参数通常依次使用：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RDI、RSI、RDX、RCX、R8、R9&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;函数并不是不能超过六个参数。超过六个的参数会按照 ABI 放到栈上，只是调用方式不再全部使用参数寄存器。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="二十五从-hook-read-到调度器唤醒的完整链路"&gt;二十五、从 Hook read 到调度器唤醒的完整链路&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;协程A调用read(fd)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;进入Hook read&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;poll或真实read发现暂时不可读&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;fd注册EPOLLIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;记录fd → 协程A&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;协程A状态RUNNING → WAITING&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;_switch(scheduler_ctx, coA_ctx)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;scheduler运行其他READY协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;epoll_wait返回fd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;根据event找到协程A&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;从等待集合删除协程A&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;协程A状态WAITING → READY&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;加入ready_queue&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;scheduler选择协程A&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;_switch(coA_ctx, scheduler_ctx)&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;协程A从Hook read中的yield位置继续&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;调用read_f真正读取&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;返回业务代码&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这一整条链路把四个模块连接起来：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Hook层&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;epoll事件层&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;scheduler调度层&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;context switch底层&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="二十六多线程与多进程模式"&gt;二十六、多线程与多进程模式&#10;&lt;/h2&gt;&lt;h3 id="261-多线程模式"&gt;26.1 多线程模式&#10;&lt;/h3&gt;&lt;p&gt;一种设计是多个线程各自运行调度器：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;线程1 → scheduler1 → epoll1 → 一组协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;线程2 → scheduler2 → epoll2 → 一组协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;线程3 → scheduler3 → epoll3 → 一组协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这通常比多个线程共同操作一个 scheduler 更容易控制，因为每个协程和上下文固定属于一个线程。&lt;/p&gt;&#10;&lt;p&gt;如果多个线程共享：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ready_queue&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;wait tree&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sleep tree&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;coroutine对象&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;就需要考虑锁、并发状态迁移、跨线程唤醒和缓存竞争。&lt;/p&gt;&#10;&lt;p&gt;CPU affinity 可以让调度线程尽量固定在某个 CPU 核，减少迁移和缓存失效，但是否绑定需要通过测试决定。&lt;/p&gt;&#10;&lt;h3 id="262-多进程模式"&gt;26.2 多进程模式&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;进程1 → scheduler1 → 一组连接&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;进程2 → scheduler2 → 一组连接&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;进程3 → scheduler3 → 一组连接&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;每个进程拥有独立地址空间和调度器，协程数据天然隔离，减少共享锁。&lt;/p&gt;&#10;&lt;p&gt;需要额外考虑：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;监听端口如何共享&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;连接如何负载均衡&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;进程间通信&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;共享状态&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;进程崩溃恢复&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;多线程和多进程都能利用多核。区别主要在共享内存、隔离性和通信成本。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="二十七怎样把协程能力用到实际项目"&gt;二十七、怎样把协程能力用到实际项目&#10;&lt;/h2&gt;&lt;h3 id="271-网络框架"&gt;27.1 网络框架&#10;&lt;/h3&gt;&lt;p&gt;以 ntyco 为基础理解：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;coroutine&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;scheduler&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;epoll&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Hook&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;timer&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;assembly switch&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;重点不是只复现 switch，而是打通：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;I/O等待 → yield → epoll唤醒 → ready → resume&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="272-webserver"&gt;27.2 WebServer&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;一个连接或请求由一个协程处理&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;协程recv HTTP请求&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;解析协议&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;查询Redis/MySQL&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;发送HTTP响应&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;每个等待点都由 Hook 和 scheduler 自动让出。&lt;/p&gt;&#10;&lt;h3 id="273-kv-存储"&gt;27.3 KV 存储&#10;&lt;/h3&gt;&lt;p&gt;例如 dkvstore：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;连接协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;接收命令&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;解析SET/GET&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;访问存储结构&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;返回结果&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="274-图床"&gt;27.4 图床&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;上传连接&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;接收文件数据&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;写磁盘或对象存储&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;写数据库&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;返回图片URL&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这些项目能够体现的不是“使用了协程”一句话，而是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;怎样Hook阻塞API&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;怎样管理十万连接状态&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;怎样设计ready/wait/sleep结构&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;怎样处理超时和部分读写&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;怎样在多核上部署调度器&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;怎样测试切换和网络性能&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="二十八核心总结"&gt;二十八、核心总结&#10;&lt;/h2&gt;&lt;h3 id="重点一hook-让同步代码接入异步调度"&gt;重点一：Hook 让同步代码接入异步调度&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;业务仍然调用read/write&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Hook判断I/O状态&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;不可用时注册epoll并yield&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;就绪后resume&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="重点二协程对象既保存执行现场也保存调度状态"&gt;重点二：协程对象既保存执行现场，也保存调度状态&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;context/stack → 回来后从哪里继续&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;state/fd/time → 什么时候可以回来&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;data nodes → 当前属于哪个调度集合&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="重点三不同状态需要不同数据结构"&gt;重点三：不同状态需要不同数据结构&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;READY → queue&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;I/O映射 → epoll data、数组或哈希&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;超时/睡眠 → 红黑树、最小堆或时间轮&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;EXITED → 回收队列&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="重点四scheduler-是整个运行时的中心"&gt;重点四：scheduler 是整个运行时的中心&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;处理epoll事件&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;处理定时器&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;维护协程状态&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;选择READY协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;执行resume&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;接收yield&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;回收退出协程&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="重点五汇编-switch-只做保存和恢复"&gt;重点五：汇编 switch 只做保存和恢复&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RSI → 保存当前上下文&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RDI → 恢复目标上下文&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;RSP → 切换协程栈&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;ret → 跳回目标协程保存位置&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;最终可以把协程运行时理解成：&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;Hook 决定何时等待，epoll 决定何时就绪，scheduler 决定运行谁，context switch 决定怎样切过去。&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;</description></item><item><title>Reactor 与 epoll：百万并发网络服务器</title><link>https://jasperstonnne.github.io/MyBlog/p/reactor-epoll-million-connections/</link><pubDate>Sat, 08 Aug 2026 00:00:00 +0800</pubDate><guid>https://jasperstonnne.github.io/MyBlog/p/reactor-epoll-million-connections/</guid><description>&lt;img src="https://jasperstonnne.github.io/MyBlog/p/reactor-epoll-million-connections/cover.svg" alt="Featured image of post Reactor 与 epoll：百万并发网络服务器" /&gt;&lt;h2 id="1-本节课要解决什么问题"&gt;1. 本节课要解决什么问题&#10;&lt;/h2&gt;&lt;p&gt;本节课不是直接实现完整 WebServer，而是先搭建一个能够承载大量网络连接的底层 I/O 框架。在这个框架之上，后续才能继续实现 HTTP 解析、路由、业务处理等功能。&lt;/p&gt;&#10;&lt;p&gt;核心目标有两个：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;使用 Linux &lt;code&gt;epoll&lt;/code&gt; 管理大量 socket。&lt;/li&gt;&#10;&lt;li&gt;使用 Reactor 思想，把不同 I/O 事件分发给不同回调函数。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;当前代码实现的是一个简单的多端口 Echo Server：客户端发来什么，服务端就回写什么。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="2-reactor-的核心思想"&gt;2. Reactor 的核心思想&#10;&lt;/h2&gt;&lt;p&gt;Reactor 可以概括为：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;I/O 对象（fd） → 发生事件（event） → 执行回调（callback）&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;本例中的映射关系如下：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;I/O 对象&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;事件&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;含义&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;回调函数&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;listenfd&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;EPOLLIN&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;有新连接到达&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;accept_cb()&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;clientfd&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;EPOLLIN&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;客户端数据可读&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;recv_cb()&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;clientfd&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;EPOLLOUT&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;socket 当前可写&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;code&gt;send_cb()&lt;/code&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;因此，主循环不需要判断“这个 fd 到底要做什么业务”，而只需要：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;调用 &lt;code&gt;epoll_wait()&lt;/code&gt; 等待事件。&lt;/li&gt;&#10;&lt;li&gt;从事件中取得 fd。&lt;/li&gt;&#10;&lt;li&gt;根据事件类型，调用该 fd 已经注册好的回调。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;这就是“事件驱动”：没有事件时线程阻塞等待，有事件时才处理对应连接。&lt;/p&gt;&#10;&lt;h3 id="21-reactor-的两个重点"&gt;2.1 Reactor 的两个重点&#10;&lt;/h3&gt;&lt;ol&gt;&#10;&lt;li&gt;建立 &lt;code&gt;event&lt;/code&gt; 与 &lt;code&gt;callback&lt;/code&gt; 的对应关系。&lt;/li&gt;&#10;&lt;li&gt;为每一个 I/O 保存独立的连接状态。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;第二点非常重要。多个连接的数据不能共用一组读写缓冲区，否则连接之间会互相覆盖。因此每个 fd 都需要自己的：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;读缓冲区 &lt;code&gt;rbuffer&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;当前读取长度 &lt;code&gt;rlength&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;写缓冲区 &lt;code&gt;wbuffer&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;待发送长度 &lt;code&gt;wlength&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;读事件回调&lt;/li&gt;&#10;&lt;li&gt;写事件回调&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="3-epoll-在-reactor-中的职责"&gt;3. &lt;code&gt;epoll&lt;/code&gt; 在 Reactor 中的职责&#10;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;epoll&lt;/code&gt; 是 Linux 提供的 I/O 多路复用机制。一个线程可以通过一个 &lt;code&gt;epoll&lt;/code&gt; 实例监听大量 fd，而不必给每条 TCP 连接创建一个线程。&lt;/p&gt;&#10;&lt;p&gt;三个最重要的接口是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;epoll_create1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;epoll_ctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;epoll_event&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;epoll_wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;epoll_event&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;maxevents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;其作用分别为：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;epoll_create1()&lt;/code&gt;：创建一个 epoll 实例。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;epoll_ctl()&lt;/code&gt;：添加、修改或删除所关注的 fd 和事件。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;epoll_wait()&lt;/code&gt;：等待已经注册的 fd 产生事件。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;当前代码使用的是默认的 LT（Level Triggered，水平触发）模式，因为没有设置 &lt;code&gt;EPOLLET&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;LT 模式下，只要 fd 仍处于可读或可写状态，&lt;code&gt;epoll_wait()&lt;/code&gt; 就会继续报告它。若以后改成 ET（边沿触发），则 socket 必须设置成非阻塞，并且 &lt;code&gt;accept()&lt;/code&gt;、&lt;code&gt;recv()&lt;/code&gt; 等操作通常要循环执行，直到返回 &lt;code&gt;EAGAIN&lt;/code&gt;。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="4-代码中的连接对象"&gt;4. 代码中的连接对象&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;rbuffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BUFFER_LENGTH&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;rlength&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;wbuffer&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;BUFFER_LENGTH&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;wlength&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;RCALLBACK&lt;/span&gt; &lt;span class="n"&gt;send_callback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;union&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;RCALLBACK&lt;/span&gt; &lt;span class="n"&gt;recv_callback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;RCALLBACK&lt;/span&gt; &lt;span class="n"&gt;accept_callback&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;r_action&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="41-为什么需要-struct-conn"&gt;4.1 为什么需要 &lt;code&gt;struct conn&lt;/code&gt;&#10;&lt;/h3&gt;&lt;p&gt;TCP 是字节流协议，一次 &lt;code&gt;recv()&lt;/code&gt; 不保证收到一个完整业务消息：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;一个完整消息可能被拆成多次接收。&lt;/li&gt;&#10;&lt;li&gt;多个消息也可能在一次 &lt;code&gt;recv()&lt;/code&gt; 中一起到达。&lt;/li&gt;&#10;&lt;li&gt;一次 &lt;code&gt;send()&lt;/code&gt; 也不保证发送全部待发送数据。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;所以 Reactor 不仅要保存回调，还必须保存每个连接的处理进度。严格来说，后续还应增加：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;已读取但尚未解析的数据偏移量；&lt;/li&gt;&#10;&lt;li&gt;待发送数据的起始偏移量；&lt;/li&gt;&#10;&lt;li&gt;协议解析状态；&lt;/li&gt;&#10;&lt;li&gt;连接状态和超时信息；&lt;/li&gt;&#10;&lt;li&gt;动态扩容或链式缓冲区。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="42-为什么-accept_callback-与-recv_callback-使用-union"&gt;4.2 为什么 &lt;code&gt;accept_callback&lt;/code&gt; 与 &lt;code&gt;recv_callback&lt;/code&gt; 使用 &lt;code&gt;union&lt;/code&gt;&#10;&lt;/h3&gt;&lt;p&gt;监听 fd 的读事件表示“有连接可接受”，其回调是 &lt;code&gt;accept_cb()&lt;/code&gt;；客户端 fd 的读事件表示“有数据可接收”，其回调是 &lt;code&gt;recv_cb()&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;同一个 fd 在这里不会同时既是监听 fd 又是客户端 fd，因此两个函数指针可以共用同一块存储空间。&lt;code&gt;union&lt;/code&gt; 表达了二者“二选一”的关系。&lt;/p&gt;&#10;&lt;p&gt;不过，代码在赋值时使用了：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sockfd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;r_action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;recv_callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accept_cb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;语义上更清晰的写法是：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sockfd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;r_action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;accept_callback&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accept_cb&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;调用时也应根据 fd 的角色使用对应成员，或者直接将二者合并成一个名称更通用的 &lt;code&gt;read_callback&lt;/code&gt;。&lt;/p&gt;&#10;&lt;h3 id="43-为什么可以用-fd-作为数组下标"&gt;4.3 为什么可以用 fd 作为数组下标&#10;&lt;/h3&gt;&lt;p&gt;当前代码使用：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CONNECTION_SIZE&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;并直接通过 &lt;code&gt;conn_list[fd]&lt;/code&gt; 找到连接。这种方法查找复杂度为 &lt;code&gt;O(1)&lt;/code&gt;，非常直接。&lt;/p&gt;&#10;&lt;p&gt;但它有两个条件：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;必须保证 &lt;code&gt;0 &amp;lt;= fd &amp;lt; CONNECTION_SIZE&lt;/code&gt;，否则会越界。&lt;/li&gt;&#10;&lt;li&gt;数组内存非常大。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;每个连接对象包含两个 1024 字节缓冲区，再加上长度、fd 和函数指针。100 多万个元素大约需要 2 GiB 以上的地址空间和可观的实际内存。因此“支持 100 万个 fd”不等于“采用这种结构后机器就一定能稳定承载 100 万连接”。生产实现通常会根据实际连接动态分配，或者按块分配连接对象与缓冲区。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="5-程序完整执行流程"&gt;5. 程序完整执行流程&#10;&lt;/h2&gt;&lt;h3 id="51-初始化-epoll"&gt;5.1 初始化 epoll&#10;&lt;/h3&gt;&lt;p&gt;代码中：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;epfd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;epoll_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;参数在现代 Linux 中只要大于 0 即可，并不表示只能监听一个 fd。更推荐：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;epfd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;epoll_create1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EPOLL_CLOEXEC&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;创建失败时必须检查返回值。&lt;/p&gt;&#10;&lt;h3 id="52-创建-20-个监听-socket"&gt;5.2 创建 20 个监听 socket&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;MAX_PORTS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;sockfd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;init_server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;port&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;程序监听 &lt;code&gt;2000~2019&lt;/code&gt; 共 20 个端口。每个监听 fd 都注册 &lt;code&gt;EPOLLIN&lt;/code&gt;，并对应 &lt;code&gt;accept_cb()&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;监听 fd 上的 &lt;code&gt;EPOLLIN&lt;/code&gt; 不是普通业务数据可读，而是表示全连接队列中存在已经完成握手、可以由 &lt;code&gt;accept()&lt;/code&gt; 取出的连接。&lt;/p&gt;&#10;&lt;h3 id="53-接受新连接"&gt;5.3 接受新连接&#10;&lt;/h3&gt;&lt;p&gt;&lt;code&gt;accept_cb()&lt;/code&gt; 的逻辑是：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;调用 &lt;code&gt;accept()&lt;/code&gt;，获得 &lt;code&gt;clientfd&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;检查 &lt;code&gt;clientfd &amp;lt; 0&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;初始化 &lt;code&gt;conn_list[clientfd]&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;给客户端 fd 注册 &lt;code&gt;EPOLLIN&lt;/code&gt;。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;这时事件映射从：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;listenfd + EPOLLIN → accept_cb&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;变成新连接上的：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;clientfd + EPOLLIN → recv_cb&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="54-接收数据"&gt;5.4 接收数据&#10;&lt;/h3&gt;&lt;p&gt;当客户端 fd 可读时执行 &lt;code&gt;recv_cb()&lt;/code&gt;：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;recv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;rbuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BUFFER_LENGTH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当前 Echo 逻辑把收到的数据复制到写缓冲区：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;wlength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;rlength&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;memcpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;wbuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;rbuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;wlength&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;随后把关注事件修改为 &lt;code&gt;EPOLLOUT&lt;/code&gt;：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;clientfd + EPOLLOUT → send_cb&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="55-发送数据"&gt;5.5 发送数据&#10;&lt;/h3&gt;&lt;p&gt;当 fd 可写时执行 &lt;code&gt;send_cb()&lt;/code&gt;。数据发送结束后，再把关注事件切换回 &lt;code&gt;EPOLLIN&lt;/code&gt;，等待客户端下一次发送。&lt;/p&gt;&#10;&lt;p&gt;完整状态流转如下：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;监听 EPOLLIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;accept_cb：建立 clientfd&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;客户端 EPOLLIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;recv_cb：接收并准备响应&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;客户端 EPOLLOUT&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;send_cb：发送响应&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ↓&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;重新监听客户端 EPOLLIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;hr&gt;&#10;&lt;h2 id="6-set_event-与事件注册"&gt;6. &lt;code&gt;set_event()&lt;/code&gt; 与事件注册&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;set_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当前约定：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;flag == 1&lt;/code&gt;：使用 &lt;code&gt;EPOLL_CTL_ADD&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;flag == 0&lt;/code&gt;：使用 &lt;code&gt;EPOLL_CTL_MOD&lt;/code&gt;。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;功能上可行，但 &lt;code&gt;flag&lt;/code&gt; 的含义不直观。更清晰的设计是直接传入 &lt;code&gt;EPOLL_CTL_ADD&lt;/code&gt;、&lt;code&gt;EPOLL_CTL_MOD&lt;/code&gt;，或者分别封装成 &lt;code&gt;add_event()&lt;/code&gt; 和 &lt;code&gt;mod_event()&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;同时必须检查 &lt;code&gt;epoll_ctl()&lt;/code&gt; 的返回值。例如：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;epoll_ctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nf"&gt;perror&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;#34;epoll_ctl&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;set_event()&lt;/code&gt; 和 &lt;code&gt;event_rigister()&lt;/code&gt; 声明为返回 &lt;code&gt;int&lt;/code&gt;，但成功路径没有 &lt;code&gt;return&lt;/code&gt;，这属于未定义行为。应补充 &lt;code&gt;return 0&lt;/code&gt;。另外，&lt;code&gt;rigister&lt;/code&gt; 应改为 &lt;code&gt;register&lt;/code&gt;。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="7-主事件循环"&gt;7. 主事件循环&#10;&lt;/h2&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;nready&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;epoll_wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;epfd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;nready&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;connfd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;EPOLLIN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;connfd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;r_action&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recv_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connfd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;EPOLLOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;connfd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;send_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;connfd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;timeout == -1&lt;/code&gt; 表示没有事件时一直阻塞，避免空转消耗 CPU。&lt;/p&gt;&#10;&lt;h3 id="两个独立的-if-还是-if--else-if"&gt;两个独立的 &lt;code&gt;if&lt;/code&gt; 还是 &lt;code&gt;if ... else if&lt;/code&gt;？&#10;&lt;/h3&gt;&lt;p&gt;两个独立 &lt;code&gt;if&lt;/code&gt; 理论上允许同一次返回同时处理读、写事件；&lt;code&gt;else if&lt;/code&gt; 每轮只处理其中一种。&lt;/p&gt;&#10;&lt;p&gt;但不能简单认为两个 &lt;code&gt;if&lt;/code&gt; 一定更好。若读回调发现连接已经断开并关闭了 fd，随后继续执行写回调就可能访问已经失效或被复用的 fd。稳妥做法是：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;先处理 &lt;code&gt;EPOLLERR&lt;/code&gt;、&lt;code&gt;EPOLLHUP&lt;/code&gt;、&lt;code&gt;EPOLLRDHUP&lt;/code&gt;。&lt;/li&gt;&#10;&lt;li&gt;回调返回关闭状态后，不再处理该 fd 的其他事件。&lt;/li&gt;&#10;&lt;li&gt;再依据连接状态处理读写事件。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="8-课堂代码补充"&gt;8. 课堂代码补充&#10;&lt;/h2&gt;&lt;p&gt;这份代码主要是为了演示 Reactor 的整体流程，所以有些细节课堂上只是带了一下。自己再写一遍时，可以顺手补上下面这些地方。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;关于 &lt;code&gt;send()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;课上代码里有一句：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conn_list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;wbuffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;这里最后的 &lt;code&gt;count&lt;/code&gt; 还没有赋值，应该写成 &lt;code&gt;conn_list[fd].wlength&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;另外，一次 &lt;code&gt;send()&lt;/code&gt; 不一定能把 &lt;code&gt;wlength&lt;/code&gt; 个字节全部发完。可以在连接对象里再记一个 &lt;code&gt;woffset&lt;/code&gt;，每次发送后往后移动。数据还没发完就继续等 &lt;code&gt;EPOLLOUT&lt;/code&gt;，全部发完后再切回 &lt;code&gt;EPOLLIN&lt;/code&gt;。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;关于 &lt;code&gt;recv()&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;记住三个返回值：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;大于 0：本次收到的字节数；&lt;/li&gt;&#10;&lt;li&gt;等于 0：对端关闭连接；&lt;/li&gt;&#10;&lt;li&gt;小于 0：要结合 &lt;code&gt;errno&lt;/code&gt; 判断，非阻塞情况下可能只是暂时没有数据。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;不能让 &lt;code&gt;-1&lt;/code&gt; 继续参与后面的 &lt;code&gt;memcpy()&lt;/code&gt;，否则长度转换后可能越界。&lt;code&gt;EINTR&lt;/code&gt; 可以重试，&lt;code&gt;EAGAIN/EWOULDBLOCK&lt;/code&gt; 表示这次先读到这里，其他情况再按真正的错误处理。&lt;/p&gt;&#10;&lt;p&gt;还有一点：&lt;code&gt;recv()&lt;/code&gt; 收到的是一段字节，不会自动在末尾补 &lt;code&gt;\0&lt;/code&gt;。要用 &lt;code&gt;%s&lt;/code&gt; 打印时需要自己留位置并补结束符；如果处理二进制数据，就一直按长度来处理。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;关于非阻塞&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;Reactor 一般要和非阻塞 socket 配合。监听 fd 和客户端 fd 都可以设置成 &lt;code&gt;O_NONBLOCK&lt;/code&gt;，避免某一次 &lt;code&gt;accept()&lt;/code&gt;、&lt;code&gt;recv()&lt;/code&gt; 或 &lt;code&gt;send()&lt;/code&gt; 卡住整个事件循环。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;fcntl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F_SETFL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;fcntl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;F_GETFL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;O_NONBLOCK&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果以后从 LT 改成 ET，&lt;code&gt;accept()&lt;/code&gt; 和 &lt;code&gt;recv()&lt;/code&gt; 通常都要放在循环里，一直处理到返回 &lt;code&gt;EAGAIN/EWOULDBLOCK&lt;/code&gt;。当前是 LT，一次只 &lt;code&gt;accept()&lt;/code&gt; 一个连接也能继续触发，不过连接集中到来时，循环取完会更快一些。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;关于 fd 和连接状态&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;当前写法直接用 fd 作为 &lt;code&gt;conn_list&lt;/code&gt; 的下标，使用前要判断：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-c" data-lang="c"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;CONNECTION_SIZE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;连接关闭时也不只是 &lt;code&gt;close(fd)&lt;/code&gt;，对应的读写长度、偏移量和回调最好一起清空。因为 fd 的数字会被复用，下一个新连接有可能再次拿到同一个编号。&lt;/p&gt;&#10;&lt;p&gt;fd 也不能当作累计连接数。想每建立 1000 个连接打印一次，应该另外维护一个 &lt;code&gt;accepted_count&lt;/code&gt;，不能用 &lt;code&gt;clientfd % 1000&lt;/code&gt; 来判断。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;其他随手记下的点&lt;/strong&gt;&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;socket()&lt;/code&gt;、&lt;code&gt;bind()&lt;/code&gt;、&lt;code&gt;listen()&lt;/code&gt;、&lt;code&gt;epoll_ctl()&lt;/code&gt;、&lt;code&gt;epoll_wait()&lt;/code&gt; 等系统调用都要看返回值；&lt;/li&gt;&#10;&lt;li&gt;监听 socket 通常会设置 &lt;code&gt;SO_REUSEADDR&lt;/code&gt;；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;listen(sockfd, 10)&lt;/code&gt; 中的 &lt;code&gt;10&lt;/code&gt; 是 backlog，不是服务器最多只能保持 10 条连接；&lt;/li&gt;&#10;&lt;li&gt;主循环除了 &lt;code&gt;EPOLLIN&lt;/code&gt;、&lt;code&gt;EPOLLOUT&lt;/code&gt;，还要留意 &lt;code&gt;EPOLLERR&lt;/code&gt;、&lt;code&gt;EPOLLHUP&lt;/code&gt; 和 &lt;code&gt;EPOLLRDHUP&lt;/code&gt;；&lt;/li&gt;&#10;&lt;li&gt;对端关闭后继续发送可能触发 &lt;code&gt;SIGPIPE&lt;/code&gt;，可以忽略该信号，或在 &lt;code&gt;send()&lt;/code&gt; 时使用 &lt;code&gt;MSG_NOSIGNAL&lt;/code&gt;；&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;TIME_SUB_MS(current, begin)&lt;/code&gt; 当前记录的是两次打印之间的间隔，不是从程序启动开始的总耗时。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="9-大包拆包与粘包"&gt;9. 大包、拆包与粘包&#10;&lt;/h2&gt;&lt;p&gt;代码中的读缓冲区只有 1024 字节。若客户端发送 2048 字节，一次 &lt;code&gt;recv()&lt;/code&gt; 可能只收到其中一部分；即使缓冲区足够大，TCP 也不保证一次接收对应一次发送。&lt;/p&gt;&#10;&lt;p&gt;因此不能把：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;一次 send = 一个业务消息 = 一次 recv&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;当成可靠规律。&lt;/p&gt;&#10;&lt;p&gt;业务协议需要定义消息边界，常见方法有：&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;固定长度消息。&lt;/li&gt;&#10;&lt;li&gt;特殊分隔符，例如文本协议中的换行。&lt;/li&gt;&#10;&lt;li&gt;消息头携带正文长度，例如“4 字节长度 + 消息体”。&lt;/li&gt;&#10;&lt;li&gt;HTTP 中使用请求行、首部、&lt;code&gt;Content-Length&lt;/code&gt; 或分块传输规则。&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Reactor 收到数据后应先追加到该连接自己的缓冲区，再交给协议解析器：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;recv → 追加到连接缓冲区 → 判断是否形成完整消息&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 未完整：保留并等待下次 EPOLLIN&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; → 已完整：取出消息并处理，剩余数据继续保留&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如果固定数组空间不足，可以使用动态缓冲区、环形缓冲区或链式缓冲区。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="10-为什么会出现-too-many-open-files"&gt;10. 为什么会出现 &lt;code&gt;Too many open files&lt;/code&gt;&#10;&lt;/h2&gt;&lt;p&gt;报错对应 &lt;code&gt;errno == EMFILE&lt;/code&gt;（常见值为 24），表示当前进程已经达到可打开 fd 的上限。&lt;/p&gt;&#10;&lt;p&gt;一条 TCP 连接在客户端和服务端各自都占用一个 fd。百万连接测试至少要同时考虑：&lt;/p&gt;&#10;&lt;h3 id="101-进程级限制"&gt;10.1 进程级限制&#10;&lt;/h3&gt;&lt;p&gt;查看当前 shell 的限制：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;ulimit&lt;/span&gt; -n&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;ulimit&lt;/span&gt; -a&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;临时提高当前 shell 的 soft limit：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;ulimit&lt;/span&gt; -n &lt;span class="m"&gt;1048576&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;程序必须从修改后的 shell 中启动，才能继承该限制。还要确保 hard limit 足够高。&lt;/p&gt;&#10;&lt;h3 id="102-系统级限制"&gt;10.2 系统级限制&#10;&lt;/h3&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /proc/sys/fs/file-max&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /proc/sys/fs/file-nr&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;fs.file-max&lt;/code&gt; 是系统范围的文件句柄限制，&lt;code&gt;ulimit -n&lt;/code&gt; 是单进程/会话限制，两者不是同一个概念。持久化设置还取决于系统的 PAM、systemd unit 和发行版配置，不能只改一个参数就假定生效。&lt;/p&gt;&#10;&lt;h3 id="103-程序自身限制"&gt;10.3 程序自身限制&#10;&lt;/h3&gt;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;CONNECTION_SIZE&lt;/code&gt; 是否覆盖可能出现的 fd 值；&lt;/li&gt;&#10;&lt;li&gt;进程虚拟内存和实际物理内存是否足够；&lt;/li&gt;&#10;&lt;li&gt;每连接缓冲区是否过大；&lt;/li&gt;&#10;&lt;li&gt;是否存在 fd 泄漏；&lt;/li&gt;&#10;&lt;li&gt;epoll 实例和监听 socket 自己也会占 fd。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="11-为什么单个目标端口大约只能建立数万连接"&gt;11. 为什么单个目标端口大约只能建立数万连接&#10;&lt;/h2&gt;&lt;p&gt;一条 TCP 连接由五元组唯一标识：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;源 IP、源端口、目的 IP、目的端口、传输层协议&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在单台压测客户端连接单台服务器的单个端口时：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;源 IP 固定；&lt;/li&gt;&#10;&lt;li&gt;目的 IP 固定；&lt;/li&gt;&#10;&lt;li&gt;目的端口固定；&lt;/li&gt;&#10;&lt;li&gt;协议固定为 TCP；&lt;/li&gt;&#10;&lt;li&gt;主要只能变化源端口。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;当可用临时源端口耗尽时，客户端可能报：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Cannot assign requested address&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="111-一个需要校正的细节"&gt;11.1 一个需要校正的细节&#10;&lt;/h3&gt;&lt;p&gt;客户端临时端口范围并非固定为 &lt;code&gt;1024~65535&lt;/code&gt;。Linux 实际自动分配范围应通过下面的命令查看：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sysctl net.ipv4.ip_local_port_range&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;不同系统、不同配置的范围可能不同，部分端口还可能被占用、保留，或暂时处于 &lt;code&gt;TIME_WAIT&lt;/code&gt; 等状态。因此可建立数量通常小于理论上的 65535。&lt;/p&gt;&#10;&lt;h3 id="112-为什么增加到-20-个服务端端口有效"&gt;11.2 为什么增加到 20 个服务端端口有效&#10;&lt;/h3&gt;&lt;p&gt;将目标端口从一个增加到 &lt;code&gt;2000~2019&lt;/code&gt;，改变了五元组中的“目的端口”。只要客户端把连接分散到这些端口，同一个源端口就可以与不同目的端口组成不同连接，理论连接空间随之扩大。&lt;/p&gt;&#10;&lt;p&gt;其他扩展方式还包括：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;使用多个客户端源 IP；&lt;/li&gt;&#10;&lt;li&gt;使用多台压测机；&lt;/li&gt;&#10;&lt;li&gt;使用多个服务端 IP；&lt;/li&gt;&#10;&lt;li&gt;使用多个网络命名空间，但必须正确配置各自地址和路由。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;不能仅用单台客户端的结果直接代表服务端的最大承载能力，因为压测端本身可能先耗尽端口、fd、CPU、内存或 conntrack 容量。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="12-nf_conntrack-应该怎样理解"&gt;12. &lt;code&gt;nf_conntrack&lt;/code&gt; 应该怎样理解&#10;&lt;/h2&gt;&lt;p&gt;&lt;code&gt;nf_conntrack&lt;/code&gt; 是 Linux Netfilter 的连接跟踪机制，防火墙、NAT 等功能会用到它。它不是 &lt;code&gt;epoll&lt;/code&gt; 工作所必需的组成部分。&lt;/p&gt;&#10;&lt;p&gt;如果测试路径启用了连接跟踪，并且连接数达到表容量，才需要观察：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sysctl net.netfilter.nf_conntrack_max&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;cat /proc/sys/net/netfilter/nf_conntrack_count&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;旧资料中的 &lt;code&gt;ip_conntrack&lt;/code&gt; 是较早的名称；现代内核通常使用 &lt;code&gt;nf_conntrack&lt;/code&gt;。只有确实需要且模块尚未加载时，才考虑：&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;sudo modprobe nf_conntrack&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;不应看到连接问题就盲目加载模块或增大参数。应先确认机器是否启用了防火墙/NAT、模块是否存在、表是否真的已满。在服务端还是客户端调整，取决于哪台机器实际承担连接跟踪；中间的 NAT/防火墙设备也可能成为瓶颈。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="13-百万连接断开时为什么-cpu-会突然升高"&gt;13. 百万连接断开时为什么 CPU 会突然升高&#10;&lt;/h2&gt;&lt;p&gt;如果一次性终止百万连接的一端，另一端会在短时间内集中处理大量 TCP 状态变化与关闭事件，例如 FIN/RST、epoll 唤醒、连接对象清理、定时器更新等。&lt;/p&gt;&#10;&lt;p&gt;这会形成“惊群式的业务处理洪峰”或资源回收洪峰，表现为 CPU 短时暴增。它不一定表示程序死循环，而可能是系统在集中处理积压事件。&lt;/p&gt;&#10;&lt;p&gt;优化方向包括：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;分批建立和分批关闭连接；&lt;/li&gt;&#10;&lt;li&gt;减少关闭路径上的日志和同步操作；&lt;/li&gt;&#10;&lt;li&gt;将耗时清理异步化；&lt;/li&gt;&#10;&lt;li&gt;使用高效定时器结构，如时间轮；&lt;/li&gt;&#10;&lt;li&gt;避免每连接大量内存分配和释放；&lt;/li&gt;&#10;&lt;li&gt;做好背压和过载保护；&lt;/li&gt;&#10;&lt;li&gt;使用多 Reactor、多线程或多进程分摊事件处理；&lt;/li&gt;&#10;&lt;li&gt;用火焰图、&lt;code&gt;perf&lt;/code&gt;、系统指标确认 CPU 真正消耗在哪里。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;优化目标不是保证 CPU 永不升高，而是让峰值可控、持续时间缩短，并保证服务仍能继续响应。&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="14-百万并发不等于百万-qps"&gt;14. “百万并发”不等于“百万 QPS”&#10;&lt;/h2&gt;&lt;p&gt;性能测试至少要区分四个维度：&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;指标&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;含义&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;并发连接数&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;同一时刻保持的连接数量&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;QPS/吞吐量&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;单位时间完成的请求或消息数量&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;延迟&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;一次请求从发出到收到响应所需时间，通常关注 P50/P95/P99&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;测试用例&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;报文大小、收发频率、连接是否长连、业务是否计算或访问存储&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;保持 100 万条空闲 TCP 连接，和让 100 万条连接持续高速收发数据，是完全不同的负载。前者更考验 fd、内存和内核连接管理；后者还会迅速消耗 CPU、网络带宽、内存带宽以及业务依赖资源。&lt;/p&gt;&#10;&lt;p&gt;因此，一份可信的测试报告至少应说明：&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;客户端和服务端硬件配置；&lt;/li&gt;&#10;&lt;li&gt;内核和发行版版本；&lt;/li&gt;&#10;&lt;li&gt;所有相关内核参数与 fd 限制；&lt;/li&gt;&#10;&lt;li&gt;连接建立速率；&lt;/li&gt;&#10;&lt;li&gt;活跃连接与空闲连接比例；&lt;/li&gt;&#10;&lt;li&gt;请求/响应大小和频率；&lt;/li&gt;&#10;&lt;li&gt;QPS、吞吐量、错误率和延迟分位数；&lt;/li&gt;&#10;&lt;li&gt;CPU、内存、网络、fd、conntrack 使用率；&lt;/li&gt;&#10;&lt;li&gt;测试持续时间和连接关闭方式。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="15-本节课一句话总结"&gt;15. 本节课一句话总结&#10;&lt;/h2&gt;&lt;p&gt;Reactor 的本质不是“用了 epoll 就很快”，而是把每个 fd 的状态独立保存起来，由事件循环将 &lt;code&gt;I/O + event&lt;/code&gt; 精确分发给相应 callback；百万连接则是在这个正确模型之上，再共同解决 fd、内存、端口空间、内核网络栈和压测方法等系统性限制。&lt;/p&gt;&#10;&lt;hr&gt;&#10;</description></item></channel></rss>