<div class="._5ce-wx-style" style="font-size:16px;">
<div class="rich_media_content" id="js_content">
<p><img alt="08b992e9908e32a9be9712850ff30d8f.png" src="https://beijingoptbbs.oss-cn-beijing.aliyuncs.com/cs/5606289-61947a8d0e92f078fb0f4d1b262598f5.png"></p>
<p> 使用Golang可以轻松地为每一个TCP连接创建一个协程去服务而不用担心性能问题,这是因为Go内部使用goroutine结合IO多路复用实现了一个“异步”的IO模型,这使得开发者不用过多的关注底层,而只需要按照需求编写上层业务逻辑。这种异步的IO是如何实现的呢?下面我会针对Linux系统进行分析。</p>
<p> 在Unix/Linux系统下,一切皆文件,每条TCP连接对应了一个socket句柄,这个句柄也可以看做是一个文件,在socket上收发数据,相当于对一个文件进行读写,所以一个socket句柄,通常也用表示文件描述符fd来表示。可以进入/proc/PID/fd/查看进程占用的fd。</p>
<p> 系统内核会为每个socket句柄分配一个读(接收)缓冲区和一个写(发送)缓冲区,发送数据就是在这个fd对应的写缓冲区上写数据,而接收数据就是在读缓冲区上读数据,当程序调用write或者send时,并不代表数据发送出去,仅仅是把数据拷贝到了写缓冲区,在时机恰当时候(积累到一定数量),会将数据发送到目的端。</p>
<blockquote>
<p>Golang runtime还是需要频繁去检查是否有fd就绪的,严格说并不算真正的异步,算是一种非阻塞IO复用。</p>
</blockquote>
<h3>IO模型</h3>
<p> 借用教科书中几张图</p>
<h4>阻塞式IO</h4>
<p> 程序想在缓冲区读数据时,缓冲区并不一定会有数据,这会造成陷入系统调用,只能等待数据可以读取,没有数据读取时则会阻塞住进程,这就是阻塞式IO。当需要为多个客户端提供服务时,可以使用线程方式,每个socket句柄使用一个线程来服务,这样阻塞住的则是某个线程。虽然如此可以解决进程阻塞,但是还是会有相当一部分CPU资源浪费在了等待数据上,同时,使用线程来服务fd有些浪费资源,因为如果要处理的fd较多,则又是一笔资源开销。</p>
<p><img alt="c49236b6e9af1e63dda7262dd65c9be3.png" src="https://beijingoptbbs.oss-cn-beijing.aliyuncs.com/cs/5606289-6ff6ccd0a59540b659aa8bf898142e0b.png"></p>
<h4>非阻塞式IO</h4>
<p> 与之对应的是非阻塞IO,当程序想要读取数据时,如果缓冲区不存在,则直接返回给用户程序,但是需要用户程序去频繁检查,直到有数据准备好。这同样也会造成空耗CPU。</p>
<p><img alt="b76f2ce420c4cc400b7481d717019709.png" src="https://beijingoptbbs.oss-cn-beijing.aliyuncs.com/cs/5606289-c9ca3b62e98bfb6f92e10d523ff73cf6.png"></p>
<h4>IO多路复用</h4>
<p> 而IO多路复用则不同,他会使用一个线程去管理多个fd,可以将多个fd加入IO多路复用函数中,每次调用该函数,传入要检查的fd,如果有就绪的fd,直接返回就绪的fd,再启动线程处理或者顺序处理就绪的fd。这达到了一个线程管理多个fd任务,相对来说较为高效。常见的IO多路复用函数有select,poll,epoll。select与poll的最大缺点是每次调用时都需要传入所有要监听的fd集合,内核再遍历这个传入的fd集合,当并发量大时候,用户态与内核态之间的数据拷贝以及内核轮询fd又要浪费一波系统资源(关于select与poll这里不展开)。</p>
<p><img alt="8aeb86e6f41abb9507e1d36b4bc01657.png" src="https://beijingoptbbs.oss-cn-beijing.aliyuncs.com/cs/5606289-4e628288ef56abed784f7e350dd4e129.png"></p>
<h5>epoll介绍</h5>
<p> 接下来介绍一下epoll系统调用</p>
<p> epoll相比于select与poll相比要灵活且高效,他提供给用户三个系统调用函数。Golang底层就是通过这三个系统调用结合goroutine完成的“异步”IO。</p>
<pre class="blockcode"><code>//用于创建并返回一个epfd句柄,后续关于fd的添加删除等操作都依据这个句柄。<br>int epoll_create(int size);<br>//用于向epfd添加,删除,修改要监听的fd。<br>int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);<br>//传入创建返回的epfd句柄,以及超时时间,返回就绪的fd句柄。<br>int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);</code></pre>
<ul><li><p>调用epoll_create会在内核创建一个eventpoll对象,这个对象会维护一个epitem集合,可简单理解为fd集合。</p></li><li><p>调用epoll_ctl函数用于将fd封装成epitem加入这个eventpoll对象,并给这个epitem加了一个回调函数注册到内核,会在这个fd状态改变时候触发,使得该epitem加入eventpoll的就绪列表rdlist。</p></li><li><p>当相应数据到来,触发中断响应程序 |
|