<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>hechen0 (hechen0)</title>
    <link>https://ruby-china.org/hechen0</link>
    <description></description>
    <language>en-us</language>
    <item>
      <title>从 gRPC 的重试策略说起</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;本文首发在 &lt;a href="hechen0.com" title=""&gt;技术成长之道&lt;/a&gt; 博客，访问 &lt;a href="hechen0.com" title=""&gt;hechen0.com&lt;/a&gt; 查看更多，或者微信搜索「技术成长之道」关注我的公众号，或者扫描下方二维码关注
公众号获得第一时间更新通知！&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src="http://s.hechen0.com/weixin1.png" title="" alt="微信"&gt;&lt;/p&gt;
&lt;h2 id="本文让你了解"&gt;本文让你了解&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;重试解决什么问题&lt;/li&gt;
&lt;li&gt;短时故障的产生原因&lt;/li&gt;
&lt;li&gt;处理短时故障的挑战&lt;/li&gt;
&lt;li&gt;重试分为几步&lt;/li&gt;
&lt;li&gt;gRPC 是如何进行重试的&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="1. 重试解决什么问题"&gt;1. 重试解决什么问题&lt;/h2&gt;
&lt;p&gt;如今的互联网服务早已不是单体应用，而是由若干个模块组成的微服务，每个模块可以进行单独的扩容、缩容，独立上线部署等等；模块与模块之间通过网络进行联通。我们的应用必须对网络错误进行妥善的处理。从发生时长上而言，网络错误可以分为两类：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;长时间不可用，如光纤被挖断，机房被炸等&lt;/li&gt;
&lt;li&gt;短时间不可用，比如网络出现抖动，正在通信的对端机器正好重新上线等&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;而重试是应对短时故障利器，简单却异常有效。&lt;/p&gt;
&lt;h2 id="2. 短时间故障的产生原因"&gt;2. 短时间故障的产生原因&lt;/h2&gt;
&lt;p&gt;在任何环节下应用都会有可能产生短时故障。即使是在没有网络参与的应用里，软件 bug 或硬件故障或一次意外断电都会造成短时故障。短时故障是常态，想做到高可用不是靠避免这些故障的发生，而是去思考短时故障发生之后的应对策略。&lt;/p&gt;

&lt;p&gt;就互联网公司的服务而言，通过冗余，各种切换等已经极大提高了整体应用的可用性，但其内部的短时故障却是连绵不断，原因有这么几个：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;应用所使用的资源是共享的，比如 docker、虚拟机、物理机混布等，如果多个虚拟单位 (docker 镜像、虚拟机、进程等) 之间的资源隔离没有做好，就可能产生一个虚拟单位侵占过多资源导致其它共享的虚拟单元出现错误。这些错误可能是短时的，也有可能是长时间的。&lt;/li&gt;
&lt;li&gt;现在服务器都是用比较便宜的硬件，即使是最重要的数据库，互联网公司的通常做法也是通过冗余去保证高可用。贵和便宜的硬件之间有个很重要的指标差异就是故障率，便宜的机器更容易发生硬件故障，虽然故障率很低，但如果把这个故障率乘以互联网公司数以万计、十万计的机器，每天都会有机器故障自然是家常便饭。这里有个 [硬盘故障率统计][hd failure] 很有意思可以看看。&lt;/li&gt;
&lt;li&gt;除掉本身的问题外，现今的互联网架构所需要的硬件组件也更多了，比如路由和负载均衡等等，更多的组件，意味着通信链路上更多的节点，意味着增加了更多的不可靠。&lt;/li&gt;
&lt;li&gt;应用之间的网络通信问题，在架构设计时，对网络的基本假设就是不可靠，我们需要通过额外的机制弥补这种不可靠，有人问了，我的应用就是一个纯内网应用，网络都是内网，也不可靠么？嗯是的，不可靠。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="3. 处理短时故障的挑战"&gt;3. 处理短时故障的挑战&lt;/h2&gt;
&lt;p&gt;短时故障处理以下两点挑战&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;感知。应用需要能够区分不同类型的错误，不同类型的错误对应的错误处理方式是不同的，没有哪种应对手段可以处理所有的错误。比如网络抖动我们简单重试即可，如果网络不可用，对于一个可靠的存储系统，可能就需要经历选主，副本切换等复杂操作才能保证数据的正确性。&lt;/li&gt;
&lt;li&gt;处理。如何选择一个合适的处理策略对于快速恢复故障、缩短响应时间以及减少对对端的冲击是非常重要的。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="4. 重试分为几步"&gt;4. 重试分为几步&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;感知错误。通常我们使用错误码识别不同类型的错误。比如在 REST 风格的应用里面，HTTP 的 status code 可以用来识别不同类型的错误。&lt;/li&gt;
&lt;li&gt;决策是否应该重试。不是所有错误都应该被重试，比如 HTTP 的 4xx 的错误，通常 4xx 表示的是客户端的错误，这时候客户端不应该进行重试操作。什么错误可以重试需要具体情况具体分析，对于网络类的错误，我们也不是一股脑都进行重试，比如 zookeeper 这种强一致的存储系统，发生了 network partition 之后，需要经过一系列复杂操作，简单的重试根本不管用。&lt;/li&gt;
&lt;li&gt;选择重试策略。选择一个合适的重试次数和重试间隔非常的重要。如果次数不够，可能并不能有效的覆盖这个短时间故障的时间段，如果重试次数过多，或者重试间隔太小，又可能造成大量的资源 (CPU、内存、线程、网络) 浪费。合适的次数和间隔取决于重试的上下文。举例：如果是用户操作失败导致的重试，比如在网页上点了一个按钮失败的重试，间隔就应该尽量短，确保用户等待时间较短；如果请求失败成本很高，比如整个流程很长，一旦中间环节出错需要重头开始，典型的如转账交易，这种情况就需要适当增加重试次数和最长等待时间以尽可能保证短时间的故障能被处理而无需重头来过。&lt;/li&gt;
&lt;li&gt;失败处理与自动恢复。短时故障如果短时间没有恢复就变成了长时间的故障，这个时候我们就不应该再进行重试了，但是等故障修复之后我们也需要有一种机制能自动恢复。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="4.1 常见的重试时间间隔策略"&gt;4.1 常见的重试时间间隔策略&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;指数避退。重试间隔时间按照指数增长，如等 3s 9s 27s 后重试。指数避退能有效防止对对端造成不必要的冲击，因为随着时间的增加，一个故障从短时故障变成长时间的故障的可能性是逐步增加的，对于一个长时间的故障，重试基本无效。&lt;/li&gt;
&lt;li&gt;重试间隔线性增加。重试间隔的间隔按照线性增长，而非指数级增长，如等 3s 7s 13s 后重试。间隔增长能避免长时间等待，缩短故障响应时间。&lt;/li&gt;
&lt;li&gt;固定间隔。重试间隔是一个固定值，如每 3s 后进行重试。&lt;/li&gt;
&lt;li&gt;立即重试。有时候短时故障是因为网络抖动造成的，可能是因为网络包冲突或者硬件有问题等，这时候我们立即重试通常能解决这类问题。但是立即重试不应该超过一次，如果立即重试一次失败之后，应该转换为指数避退或者其它策略进行，因为大量的立即重试会给对端造成流量上的尖峰，对网络也是一个冲击。&lt;/li&gt;
&lt;li&gt;随机间隔。当服务有多台实例时，我们应该加入随机的变量，比如 A 服务请求 B 服务，B 服务发生短时间不可用，A 服务的实例应该避免在同一时刻进行重试，这时候我们对间隔加入随机因子会很好的在时间上平摊开所有的重试请求。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="5. gRPC是如何进行重试的"&gt;5. gRPC 是如何进行重试的&lt;/h2&gt;&lt;h2 id="5.1 如何感知错误"&gt;5.1 如何感知错误&lt;/h2&gt;
&lt;p&gt;gRPC 有自己一套类似 HTTP status code 的错误码，每个错误码都是个字符串，如 INTERNAL、ABORTED、UNAVAILABLE。&lt;/p&gt;
&lt;h2 id="5.2 如何决策"&gt;5.2 如何决策&lt;/h2&gt;
&lt;p&gt;对于哪些错误可以重试是可配置的。通常而言，只有那些明确标识对端没有接收处理请求的错误才需要被重试，比如对端返回一个 UNAVAILABLE 错误，这代表对端的服务目前处于不可用状态。但也可以配置一个更加激进的重试策略，但关键是需要保证这些被重试的 gRPC 请求是幂等的，这个需要服务使用者和提供者共同协商出一个可以被重试的错误集合。&lt;/p&gt;
&lt;h2 id="5.3 重试策略"&gt;5.3 重试策略&lt;/h2&gt;
&lt;p&gt;gRPC 的重试策略分为两类&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;重试策略，失败后进行重试。&lt;/li&gt;
&lt;li&gt;对冲策略，一次请求会给对端发出多个相同请求，只要有一个成功就认为成功。&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;先说下重试策略&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src="http://s.hechen0.com/too_many_attempts.png" title="" alt="重试策略"&gt;&lt;/p&gt;
&lt;h3 id="重试之时间策略"&gt;重试之时间策略&lt;/h3&gt;
&lt;p&gt;gPRC 用了上面我们提到的 &lt;strong&gt;指数避退 + 随机间隔&lt;/strong&gt; 组合起来的方式进行重试，[详见这里][grpc backoff]&lt;/p&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* 伪码 */&lt;/span&gt;

&lt;span class="n"&gt;ConnectWithBackoff&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="n"&gt;current_backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;INITIAL_BACKOFF&lt;/span&gt;
  &lt;span class="n"&gt;current_deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;INITIAL_BACKOFF&lt;/span&gt;
  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TryConnect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_deadline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;MIN_CONNECT_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
         &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;SleepUntil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;current_backoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_backoff&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;MULTIPLIER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MAX_BACKOFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;current_deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;current_backoff&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="n"&gt;UniformRandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;JITTER&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;current_backoff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JITTER&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;current_backoff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的算法里有这么几个关键的参数&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;INITIAL_BACKOFF：第一次重试等待的间隔&lt;/li&gt;
&lt;li&gt;MULTIPLIER：每次间隔的指数因子&lt;/li&gt;
&lt;li&gt;JITTER：控制随机的因子&lt;/li&gt;
&lt;li&gt;MAX_BACKOFF：等待的最大时长，随着重试次数的增加，我们不希望第 N 次重试等待的时间变成 30 分钟这样不切实际的值&lt;/li&gt;
&lt;li&gt;MIN_CONNECT_TIMEOUT：一次成功的请求所需要的时间，因为即使是正常的请求也需要有响应时间，比如 200ms，我们的重试时间间隔显然要大于这个响应时间才不会出现请求明明已经成功，但却进行重试的操作。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;通过指数的增加每次重试间隔，gRPC 在考虑对端服务和快速故障处理中间找到了一个平衡点。&lt;/p&gt;
&lt;h3 id="重试之次数策略"&gt;重试之次数策略&lt;/h3&gt;
&lt;p&gt;上面的算法里面没有关于次数的限制，gRPC 中的最大重试次数是可配置的，硬限制的最大值为 [5 次][grpc retry]，设置这个硬限制的目的我想主要还是出于对对端服务的保护，避免一些人为的错误。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;再说下对冲策略&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src="http://s.hechen0.com/basic_hedge.png" title="" alt="对冲策略"&gt;&lt;/p&gt;
&lt;h3 id="对冲之时间策略"&gt;对冲之时间策略&lt;/h3&gt;
&lt;p&gt;对冲策略里面，请求是按照如下逻辑发出的：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;第一次正常的请求正常发出&lt;/li&gt;
&lt;li&gt;在等待固定时间间隔后，没有收到正确的响应，第二个对冲请求会被发出&lt;/li&gt;
&lt;li&gt;再等待固定时间间隔后，没有收到任何前面两个请求的正确响应，第三个会被发出&lt;/li&gt;
&lt;li&gt;一直重复以上流程直到发出的对冲请求数量达到配置的最大次数&lt;/li&gt;
&lt;li&gt;一旦收到正确响应，所有对冲请求都会被取消，响应会被返回给应用层&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="对冲之次数策略"&gt;对冲之次数策略&lt;/h3&gt;
&lt;p&gt;次数和上面重试是一样的限制，都是 5 次。&lt;/p&gt;
&lt;h3 id="其它需要注意的问题"&gt;其它需要注意的问题&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;不同的对冲请求应该被对端不同的实例处理&lt;/li&gt;
&lt;li&gt;对冲策略应该只用于幂等的操作，因为不同的对冲的请求通常是由不同的对端实例处理的&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="5.4 重试失败"&gt;5.4 重试失败&lt;/h2&gt;
&lt;p&gt;当然不能一直重试，对于重试失败，gRPC 有以下的策略以顾全大局，对于每个 server，客户端都可配置一个针对该 server 的限制策略如下：&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"retryThrottling"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"maxTokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tokenRatio"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于每个 server，gRPC 的客户端都维护了一个 token_count 变量，变量初始值为配置的 maxTokens 值，每次 RPC 请求都会影响这个 token_count 变量值：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;每次失败的 RPC 请求都会对 token_count 减 1&lt;/li&gt;
&lt;li&gt;每次成功的 RPC 请求都会对 token_count 增加 tokenRation 值&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;如果 token_count &amp;lt;= (maxTokens / 2)，那么后续发出的请求即使失败也不会进行重试了，但是正常的请求还是会发出去，直到这个 token_count &amp;gt; (maxTokens / 2) 才又恢复对失败请求的重试。这种策略可以有效的处理长时间故障。&lt;/p&gt;

&lt;p&gt;当然重试失败还能更进一步，比如 Netflix 出品的&lt;a href="https://github.com/Netflix/Hystrix" rel="nofollow" target="_blank" title=""&gt;hytrix&lt;/a&gt;能对故障进行熔断&amp;amp;降级处理，感兴趣的读者可以进一步了解。&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;本文从问题出发，介绍『重试』这种简单而又有效的故障处理手段，希望能对大家有所帮助，有任何问题欢迎在评论区留言交流，或扫描二维码/微信搜索『技术成长之道』关注公众号后留言私信。&lt;/p&gt;

&lt;p&gt;[transient faults]: &lt;a href="https://docs.microsoft.com/en-us/azure/architecture/best-practices/transient-faults" rel="nofollow" target="_blank"&gt;https://docs.microsoft.com/en-us/azure/architecture/best-practices/transient-faults&lt;/a&gt;
[hd failure]: &lt;a href="https://www.backblaze.com/blog/hard-drive-stats-q2-2019/" rel="nofollow" target="_blank"&gt;https://www.backblaze.com/blog/hard-drive-stats-q2-2019/&lt;/a&gt;
[grpc backoff]: &lt;a href="https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md" rel="nofollow" target="_blank"&gt;https://github.com/grpc/grpc/blob/master/doc/connection-backoff.md&lt;/a&gt;
[grpc retry]: &lt;a href="https://github.com/grpc/proposal/blob/master/A6-client-retries.md" rel="nofollow" target="_blank"&gt;https://github.com/grpc/proposal/blob/master/A6-client-retries.md&lt;/a&gt;&lt;/p&gt;</description>
      <author>hechen0</author>
      <pubDate>Sat, 28 Mar 2020 16:03:04 +0800</pubDate>
      <link>https://ruby-china.org/topics/39679</link>
      <guid>https://ruby-china.org/topics/39679</guid>
    </item>
    <item>
      <title>一文看懂 IO 多路复用</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;本文首发在 &lt;a href="hechen0.com" title=""&gt;技术成长之道&lt;/a&gt; 博客，访问 &lt;a href="hechen0.com" title=""&gt;hechen0.com&lt;/a&gt; 查看更多，或者微信搜索「技术成长之道」关注我的公众号，或者扫描下方二维码关注公众号获得第一时间更新通知！&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src="http://s.hechen0.com/weixin1.png" title="" alt="微信"&gt;&lt;/p&gt;
&lt;h2 id="本文让你理解"&gt;本文让你理解&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;什么是 IO 多路复用&lt;/li&gt;
&lt;li&gt;IO 多路复用解决什么问题&lt;/li&gt;
&lt;li&gt;目前有哪些 IO 多路复用的方案&lt;/li&gt;
&lt;li&gt;具体怎么用&lt;/li&gt;
&lt;li&gt;不同 IO 多路复用方案优缺点&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="1. 什么是IO多路复用"&gt;1. 什么是 IO 多路复用&lt;/h2&gt;
&lt;p&gt;一句话解释：单线程或单进程同时监测若干个文件描述符是否可以执行 IO 操作的能力。&lt;/p&gt;
&lt;h2 id="2. 解决什么问题"&gt;2. 解决什么问题&lt;/h2&gt;&lt;h2 id="说在前头"&gt;说在前头&lt;/h2&gt;
&lt;p&gt;应用程序通常需要处理来自多条事件流中的事件，比如我现在用的电脑，需要同时处理键盘鼠标的输入、中断信号等等事件，再比如 web 服务器如 nginx，需要同时处理来来自 N 个客户端的事件。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;逻辑控制流在时间上的重叠叫做 &lt;strong&gt;并发&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;而 CPU 单核在同一时刻只能做一件事情，一种解决办法是对 CPU 进行时分复用 (多个事件流将 CPU 切割成多个时间片，不同事件流的时间片交替进行)。在计算机系统中，我们用线程或者进程来表示一条执行流，通过不同的线程或进程在操作系统内部的调度，来做到对 CPU 处理的时分复用。这样多个事件流就可以并发进行，不需要一个等待另一个太久，在用户看起来他们似乎就是并行在做一样。&lt;/p&gt;

&lt;p&gt;但凡事都是有成本的。线程/进程也一样，有这么几个方面：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;线程/进程创建成本&lt;/li&gt;
&lt;li&gt;CPU 切换不同线程/进程成本 [Context Switch][context switch]&lt;/li&gt;
&lt;li&gt;多线程的资源竞争&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;有没有一种可以在单线程/进程中处理多个事件流的方法呢？一种答案就是 IO 多路复用。&lt;/p&gt;

&lt;p&gt;因此 IO 多路复用解决的本质问题是在&lt;strong&gt;用更少的资源完成更多的事&lt;/strong&gt;。&lt;/p&gt;

&lt;p&gt;为了更全面的理解，先介绍下在 Linux 系统下所有 IO 模型。&lt;/p&gt;
&lt;h2 id="I/O模型"&gt;I/O 模型&lt;/h2&gt;
&lt;p&gt;目前 Linux 系统中提供了 5 种 IO 处理模型&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;阻塞 IO&lt;/li&gt;
&lt;li&gt;非阻塞 IO&lt;/li&gt;
&lt;li&gt;IO 多路复用&lt;/li&gt;
&lt;li&gt;信号驱动 IO&lt;/li&gt;
&lt;li&gt;异步 IO&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="阻塞IO"&gt;阻塞 IO&lt;/h3&gt;
&lt;p&gt;这是最常用的简单的 IO 模型。阻塞 IO 意味着当我们发起一次 IO 操作后一直等待成功或失败之后才返回，在这期间程序不能做其它的事情。阻塞 IO 操作只能对单个文件描述符进行操作，详见 [read][read] 或 [write][write]。&lt;/p&gt;
&lt;h3 id="非阻塞IO"&gt;非阻塞 IO&lt;/h3&gt;
&lt;p&gt;我们在发起 IO 时，通过对文件描述符设置 O_NONBLOCK flag 来指定该文件描述符的 IO 操作为非阻塞。非阻塞 IO 通常发生在一个 for 循环当中，因为每次进行 IO 操作时要么 IO 操作成功，要么当 IO 操作会阻塞时返回错误 EWOULDBLOCK/EAGAIN，然后再根据需要进行下一次的 for 循环操作，这种类似轮询的方式会浪费很多不必要的 CPU 资源，是一种糟糕的设计。和阻塞 IO 一样，非阻塞 IO 也是通过调用 [read][read] 或 write[write] 来进行操作的，也只能对单个描述符进行操作。&lt;/p&gt;
&lt;h3 id="IO多路复用"&gt;IO 多路复用&lt;/h3&gt;
&lt;p&gt;IO 多路复用在 Linux 下包括了三种，[select][select]、[poll][poll]、[epoll][epoll]，抽象来看，他们功能是类似的，但具体细节各有不同：首先都会对一组文件描述符进行相关事件的注册，然后阻塞等待某些事件的发生或等待超时。更多细节详见下面的 "具体怎么用"。IO 多路复用都可以关注多个文件描述符，但对于这三种机制而言，不同数量级文件描述符对性能的影响是不同的，下面会详细介绍。&lt;/p&gt;
&lt;h3 id="信号驱动IO"&gt;信号驱动 IO&lt;/h3&gt;
&lt;p&gt;[信号驱动 IO][signal] 是利用信号机制，让内核告知应用程序文件描述符的相关事件。这里有一个信号驱动 IO 相关的 [例子][sigio]。&lt;/p&gt;

&lt;p&gt;但信号驱动 IO 在网络编程的时候通常很少用到，因为在网络环境中，和 socket 相关的读写事件太多了，比如下面的事件都会导致 SIGIO 信号的产生：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;TCP 连接建立&lt;/li&gt;
&lt;li&gt;一方断开 TCP 连接请求&lt;/li&gt;
&lt;li&gt;断开 TCP 连接请求完成&lt;/li&gt;
&lt;li&gt;TCP 连接半关闭&lt;/li&gt;
&lt;li&gt;数据到达 TCP socket&lt;/li&gt;
&lt;li&gt;数据已经发送出去 (如：写 buffer 有空余空间)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;上面所有的这些都会产生 SIGIO 信号，但我们没办法在 SIGIO 对应的信号处理函数中区分上述不同的事件，SIGIO 只应该在 IO 事件单一情况下使用，比如说用来监听端口的 socket，因为只有客户端发起新连接的时候才会产生 SIGIO 信号。&lt;/p&gt;
&lt;h3 id="异步IO"&gt;异步 IO&lt;/h3&gt;
&lt;p&gt;异步 IO 和信号驱动 IO 差不多，但它比信号驱动 IO 可以多做一步：相比信号驱动 IO 需要在程序中完成数据从用户态到内核态 (或反方向) 的拷贝，异步 IO 可以把拷贝这一步也帮我们完成之后才通知应用程序。我们使用 [aio_read][aio_read] 来读，[aio_write][aio_write] 写。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;同步 IO vs 异步 IO&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;同步 IO 指的是程序会一直阻塞到 IO 操作如 read、write 完成&lt;/li&gt;
&lt;li&gt;异步 IO 指的是 IO 操作不会阻塞当前程序的继续执行&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;所以根据这个定义，上面阻塞 IO 当然算是同步的 IO，非阻塞 IO 也是同步 IO，因为当文件操作符可用时我们还是需要阻塞的读或写，同理 IO 多路复用和信号驱动 IO 也是同步 IO，只有异步 IO 是完全完成了数据的拷贝之后才通知程序进行处理，没有阻塞的数据读写过程。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="3. 目前有哪些IO多路复用的方案"&gt;3. 目前有哪些 IO 多路复用的方案&lt;/h2&gt;&lt;h2 id="解决方案总览"&gt;解决方案总览&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;os&lt;/th&gt;
&lt;th&gt;解决方案&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux&lt;/td&gt;
&lt;td&gt;select、poll、epoll&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MacOS/FreeBSD&lt;/td&gt;
&lt;td&gt;kqueue&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows/Solaris&lt;/td&gt;
&lt;td&gt;[IOCP][IOCP]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;h2 id="常见软件的IO多路复用方案"&gt;常见软件的 IO 多路复用方案&lt;/h2&gt;&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;软件&lt;/th&gt;
&lt;th&gt;解决方案&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;redis&lt;/td&gt;
&lt;td&gt;Linux 下 epoll(level-triggered)，没有 epoll 用 select&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;nginx&lt;/td&gt;
&lt;td&gt;Linux 下 epoll(edge-triggered)，没有 epoll 用 select&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;&lt;h2 id="4. 具体怎么用"&gt;4. 具体怎么用&lt;/h2&gt;
&lt;p&gt;我在工作中接触的都是 Linux 系统的服务器，所以在这里只介绍 Linux 系统的解决方案&lt;/p&gt;
&lt;h3 id="select"&gt;select&lt;/h3&gt;
&lt;p&gt;相关函数定义如下&lt;/p&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* According to POSIX.1-2001, POSIX.1-2008 */&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;sys/select.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="cm"&gt;/* According to earlier standards */&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;sys/time.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;sys/types.h&amp;gt;&lt;/span&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;
&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;select&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;nfds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fd_set&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;readfds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fd_set&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;writefds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;fd_set&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;exceptfds&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;timeval&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;pselect&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;nfds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fd_set&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;readfds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fd_set&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;writefds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;fd_set&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;exceptfds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;timespec&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;sigset_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sigmask&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;FD_CLR&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="n"&gt;fd_set&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt;  &lt;span class="nf"&gt;FD_ISSET&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="n"&gt;fd_set&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;FD_SET&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="n"&gt;fd_set&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;FD_ZERO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd_set&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;select 的调用会阻塞到有文件描述符可以进行 IO 操作或被信号打断或者超时才会返回。&lt;/p&gt;

&lt;p&gt;select 将监听的文件描述符分为三组，每一组监听不同的需要进行的 IO 操作。readfds 是需要进行读操作的文件描述符，writefds 是需要进行写操作的文件描述符，exceptfds 是需要进行 [异常事件][poll] 处理的文件描述符。这三个参数可以用 NULL 来表示对应的事件不需要监听。&lt;/p&gt;

&lt;p&gt;当 select 返回时，每组文件描述符会被 select 过滤，只留下可以进行对应 IO 操作的文件描述符。&lt;/p&gt;

&lt;p&gt;FD_xx 系列的函数是用来操作文件描述符组和文件描述符的关系。&lt;/p&gt;

&lt;p&gt;FD_ZERO 用来清空文件描述符组。每次调用 select 前都需要清空一次。&lt;/p&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;fd_set&lt;/span&gt; &lt;span class="n"&gt;writefds&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;FD_ZERO&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;writefds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FD_SET 添加一个文件描述符到组中，FD_CLR 对应将一个文件描述符移出组中&lt;/p&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;FD_SET&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;writefds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;FD_CLR&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;writefds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;FD_ISSET 检测一个文件描述符是否在组中，我们用这个来检测一次 select 调用之后有哪些文件描述符可以进行 IO 操作&lt;/p&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FD_ISSET&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;readfds&lt;/span&gt;&lt;span class="p"&gt;)){&lt;/span&gt;
    &lt;span class="cm"&gt;/* fd可读 */&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;select 可同时监听的文件描述符数量是通过 FS_SETSIZE 来限制的，在 Linux 系统中，该值为 1024，当然我们可以增大这个值，但随着监听的文件描述符数量增加，select 的效率会降低，我们会在『不同 IO 多路复用方案优缺点』一节中展开。&lt;/p&gt;

&lt;p&gt;pselect 和 select 大体上是一样的，但有一些细节上的 [区别][select]。&lt;/p&gt;

&lt;p&gt;[打开链接查看完整的使用 select 的例子][select example]&lt;/p&gt;
&lt;h3 id="poll"&gt;poll&lt;/h3&gt;
&lt;p&gt;相关函数定义&lt;/p&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;poll.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;poll&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;pollfd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nfds_t&lt;/span&gt; &lt;span class="n"&gt;nfds&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;

&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;signal.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;poll.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;ppoll&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;pollfd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;fds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nfds_t&lt;/span&gt; &lt;span class="n"&gt;nfds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;timespec&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tmo_p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;sigset_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sigmask&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;pollfd&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="cm"&gt;/* file descriptor */&lt;/span&gt;
    &lt;span class="kt"&gt;short&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* requested events to watch */&lt;/span&gt;
    &lt;span class="kt"&gt;short&lt;/span&gt; &lt;span class="n"&gt;revents&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="cm"&gt;/* returned events witnessed */&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;和 select 用三组文件描述符不同的是，poll 只有一个 pollfd 数组，数组中的每个元素都表示一个需要监听 IO 操作事件的文件描述符。events 参数是我们需要关心的事件，revents 是所有内核监测到的事件。合法的事件可以参考 [这里][poll]。&lt;/p&gt;

&lt;p&gt;[打开链接查看完整的使用 poll 的例子][poll example]&lt;/p&gt;
&lt;h3 id="[epoll][epoll]"&gt;[epoll][epoll]&lt;/h3&gt;
&lt;p&gt;相关函数定义如下&lt;/p&gt;
&lt;pre class="highlight c"&gt;&lt;code&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;
&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;epoll_create&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;size&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&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;

&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;

&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;
            &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;
&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;epoll_pwait&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;
            &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;
            &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;sigset_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sigmask&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;[epoll_create][epoll_create]&amp;amp;[epoll_create1][epoll_create] 用于创建一个 epoll 实例，而 [epoll_ctl][epoll_ctl] 用于往 epoll 实例中增删改要监测的文件描述符，[epoll_wait][epoll_wait] 则用于阻塞的等待可以执行 IO 操作的文件描述符直到超时。&lt;/p&gt;

&lt;p&gt;[打开链接查看完整的使用 epoll 的例子][epoll example]&lt;/p&gt;
&lt;h3 id="level-triggered and edge-triggered"&gt;level-triggered and edge-triggered&lt;/h3&gt;
&lt;p&gt;这两种底层的事件通知机制通常被称为水平触发和边沿触发，真是翻译的词不达意，如果我来翻译，我会翻译成：状态持续通知和状态变化通知。&lt;/p&gt;

&lt;p&gt;这两个概念来自电路，triggered 代表电路激活，也就是有事件通知给程序，level-triggered 表示只要有 IO 操作可以进行比如某个文件描述符有数据可读，每次调用 epoll_wait 都会返回以通知程序可以进行 IO 操作，edge-triggered 表示只有在文件描述符状态发生变化时，调用 epoll_wait 才会返回，如果第一次没有全部读完该文件描述符的数据而且没有新数据写入，再次调用 epoll_wait 都不会有通知给到程序，因为文件描述符的状态没有变化。&lt;/p&gt;

&lt;p&gt;select 和 poll 都是状态持续通知的机制，且不可改变，只要文件描述符中有 IO 操作可以进行，那么 select 和 poll 都会返回以通知程序。而 epoll 两种通知机制可选。&lt;/p&gt;
&lt;h3 id="状态变化通知(edge-triggered)模式下的epoll"&gt;状态变化通知 (edge-triggered) 模式下的 epoll&lt;/h3&gt;
&lt;p&gt;在 epoll 状态变化通知机制下，有一些的特殊的地方需要注意。考虑下面这个例子&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;服务端文件描述符 rfd 代表要执行 read 操作的 TCP socket，rfd 已被注册到一个 epoll 实例中&lt;/li&gt;
&lt;li&gt;客户端向 rfd 写了 2kb 数据&lt;/li&gt;
&lt;li&gt;服务端调用 epoll_wait 返回，rfd 可执行 read 操作&lt;/li&gt;
&lt;li&gt;服务端从 rfd 中读取了 1kb 数据&lt;/li&gt;
&lt;li&gt;服务端又调用了一次 epoll_wait&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;在第 5 步的 epoll_wait 调用不会返回，而对应的客户端会因为服务端没有返回对应的 response 而超时重试，原因就是我上面所说的，epoll_wait 只会在状态变化时才会通知程序进行处理。第 3 步 epoll_wait 会返回，是因为客户端写了数据，导致 rfd 状态被改变了，第 3 步的 epoll_wait 已经消费了这个事件，所以第 5 步的 epoll_wait 不会返回。&lt;/p&gt;

&lt;p&gt;我们需要配合非阻塞 IO 来解决上面的问题：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;对需要监听的文件描述符加上非阻塞 IO 标识&lt;/li&gt;
&lt;li&gt;只在 read 或者 write 返回 EAGAIN 或 EWOULDBLOCK 错误时，才调用 epoll_wait 等待下次状态改变发生&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;通过上述方式，我们可以确保每次 epoll_wait 返回之后，我们的文件描述符中没有读到一半或写到一半的数据。&lt;/p&gt;
&lt;h2 id="5. 不同IO多路复用方案优缺点"&gt;5. 不同 IO 多路复用方案优缺点&lt;/h2&gt;&lt;h2 id="poll vs select"&gt;poll vs select&lt;/h2&gt;
&lt;p&gt;poll 和 select 基本上是一样的，poll 相比 select 好在如下几点：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;poll 传参对用户更友好。比如不需要和 select 一样计算很多奇怪的参数比如 nfds(值最大的文件描述符 +1)，再比如不需要分开三组传入参数。&lt;/li&gt;
&lt;li&gt;poll 会比 select 性能稍好些，因为 select 是每个 bit 位都检测，假设有个值为 1000 的文件描述符，select 会从第一位开始检测一直到第 1000 个 bit 位。但 poll 检测的是一个数组。&lt;/li&gt;
&lt;li&gt;select 的时间参数在返回的时候各个系统的处理方式不统一，如果希望程序可移植性更好，需要每次调用 select 都初始化时间参数。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;而 select 比 poll 好在下面几点&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;支持 select 的系统更多，兼容更强大，有一些 unix 系统不支持 poll&lt;/li&gt;
&lt;li&gt;select 提供精度更高 (到 microsecond) 的超时时间，而 poll 只提供到毫秒的精度。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;但总体而言 select 和 poll 基本一致。&lt;/p&gt;
&lt;h2 id="epoll vs poll&amp;amp;select"&gt;epoll vs poll&amp;amp;select&lt;/h2&gt;
&lt;p&gt;epoll 优于 select&amp;amp;poll 在下面几点：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;在需要同时监听的文件描述符数量增加时，select&amp;amp;poll 是 O(N) 的复杂度，epoll 是 O(1)，在 N 很小的情况下，差距不会特别大，但如果 N 很大的前提下，一次 O(N) 的循环可要比 O(1) 慢很多，所以高性能的网络服务器都会选择 epoll 进行 IO 多路复用。&lt;/li&gt;
&lt;li&gt;epoll 内部用一个文件描述符挂载需要监听的文件描述符，这个 epoll 的文件描述符可以在多个线程/进程共享，所以 epoll 的使用场景要比 select&amp;amp;poll 要多。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;本文从使用者的角度，从问题出发，介绍了多种 IO 多路复用方案，有任何问题欢迎在下方留言交流，或扫描二维码/微信搜索『技术成长之道』关注公众号后留言私信。&lt;/p&gt;

&lt;p&gt;PS：代码永远是最正确的，man 文档其次，更多细节可以多看代码和文档。&lt;/p&gt;

&lt;p&gt;参考&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://book.douban.com/subject/25828773/" rel="nofollow" target="_blank" title=""&gt;Linux 系统编程（第二版）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://book.douban.com/subject/4859464/" rel="nofollow" target="_blank" title=""&gt;UNIX 网络编程 : 第 1 卷：套接口 API(第 3 版)&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;[select example]: &lt;a href="https://gist.github.com/hechen0/c134d2722fe8861288e060dd11e0f9c4" rel="nofollow" target="_blank"&gt;https://gist.github.com/hechen0/c134d2722fe8861288e060dd11e0f9c4&lt;/a&gt;
[context switch]: &lt;a href="https://en.wikipedia.org/wiki/Context_switch" rel="nofollow" target="_blank"&gt;https://en.wikipedia.org/wiki/Context_switch&lt;/a&gt; "Context Switch"
[read]: &lt;a href="http://man7.org/linux/man-pages/man2/read.2.html" rel="nofollow" target="_blank"&gt;http://man7.org/linux/man-pages/man2/read.2.html&lt;/a&gt; "man read"
[write]: &lt;a href="http://man7.org/linux/man-pages/man2/write.2.html" rel="nofollow" target="_blank"&gt;http://man7.org/linux/man-pages/man2/write.2.html&lt;/a&gt; "man write"
[select]: &lt;a href="http://man7.org/linux/man-pages/man2/select.2.html" rel="nofollow" target="_blank"&gt;http://man7.org/linux/man-pages/man2/select.2.html&lt;/a&gt; "select"
[poll]: &lt;a href="http://man7.org/linux/man-pages/man2/poll.2.html" rel="nofollow" target="_blank"&gt;http://man7.org/linux/man-pages/man2/poll.2.html&lt;/a&gt; "poll"
[epoll]: &lt;a href="http://man7.org/linux/man-pages/man7/epoll.7.html" rel="nofollow" target="_blank"&gt;http://man7.org/linux/man-pages/man7/epoll.7.html&lt;/a&gt; "epoll"
[signal]: &lt;a href="http://man7.org/linux/man-pages/man7/signal.7.html" rel="nofollow" target="_blank"&gt;http://man7.org/linux/man-pages/man7/signal.7.html&lt;/a&gt;
[sigio]: &lt;a href="https://github.com/troydhanson/network/blob/master/tcp/server/sigio-server.c" rel="nofollow" target="_blank"&gt;https://github.com/troydhanson/network/blob/master/tcp/server/sigio-server.c&lt;/a&gt;
[epoll_create]: &lt;a href="http://man7.org/linux/man-pages/man2/epoll_create.2.html" rel="nofollow" target="_blank"&gt;http://man7.org/linux/man-pages/man2/epoll_create.2.html&lt;/a&gt;
[epoll_ctl]: &lt;a href="http://man7.org/linux/man-pages/man2/epoll_ctl.2.html" rel="nofollow" target="_blank"&gt;http://man7.org/linux/man-pages/man2/epoll_ctl.2.html&lt;/a&gt;
[epoll_wait]: &lt;a href="http://man7.org/linux/man-pages/man2/epoll_wait.2.html" rel="nofollow" target="_blank"&gt;http://man7.org/linux/man-pages/man2/epoll_wait.2.html&lt;/a&gt;
[epoll example]: &lt;a href="https://github.com/millken/c-example/blob/master/epoll-example.c" rel="nofollow" target="_blank"&gt;https://github.com/millken/c-example/blob/master/epoll-example.c&lt;/a&gt;
[aio_read]: &lt;a href="http://man7.org/linux/man-pages/man3/aio_read.3.html" rel="nofollow" target="_blank"&gt;http://man7.org/linux/man-pages/man3/aio_read.3.html&lt;/a&gt; "aio_read"
[aio_write]: &lt;a href="http://man7.org/linux/man-pages/man3/aio_write.3.html" rel="nofollow" target="_blank"&gt;http://man7.org/linux/man-pages/man3/aio_write.3.html&lt;/a&gt; "aio_write"
[poll example]: &lt;a href="https://gist.github.com/hechen0/1bbc107793ec6cc3b00cbe7d3a54dd29" rel="nofollow" target="_blank"&gt;https://gist.github.com/hechen0/1bbc107793ec6cc3b00cbe7d3a54dd29&lt;/a&gt;
[IOCP]: &lt;a href="https://en.wikipedia.org/wiki/Input/output_completion_port" rel="nofollow" target="_blank"&gt;https://en.wikipedia.org/wiki/Input/output_completion_port&lt;/a&gt;&lt;/p&gt;</description>
      <author>hechen0</author>
      <pubDate>Sun, 22 Mar 2020 14:59:34 +0800</pubDate>
      <link>https://ruby-china.org/topics/39644</link>
      <guid>https://ruby-china.org/topics/39644</guid>
    </item>
  </channel>
</rss>
