IO多路复用(select/poll/epol),以及golangIO里使用goroutine结合IO多路复用实现的“异步”IO模型

转自:https://zhuanlan.zhihu.com/p/344581947

仅做个人备份,浏览请看原文

目录

io模型

阻塞式IO

非阻塞式IO

IO多路复用(包含:select/poll/epoll)

select/poll/epoll三者的区别

epoll介绍

异步IO

Golang异步IO实现思路


使用Golang可以轻松地为每一个TCP连接创建一个协程去服务而不用担心性能问题,这是因为Go内部使用goroutine结合IO多路复用实现了一个“异步”的IO模型,这使得开发者不用过多的关注底层,而只需要按照需求编写上层业务逻辑。这种异步的IO是如何实现的呢?下面我会针对Linux系统进行分析。

在Unix/Linux系统下,一切皆文件,每条TCP连接对应了一个socket句柄,这个句柄也可以看做是一个文件,在socket上收发数据,相当于对一个文件进行读写,所以一个socket句柄,通常也用表示文件描述符fd来表示。可以进入/proc/PID/fd/查看进程占用的fd。

系统内核会为每个socket句柄分配一个读(接收)缓冲区和一个写(发送)缓冲区,发送数据就是在这个fd对应的写缓冲区上写数据,而接收数据就是在读缓冲区上读数据,当程序调用write或者send时,并不代表数据发送出去,仅仅是把数据拷贝到了写缓冲区,在时机恰当时候(积累到一定数量),会将数据发送到目的端。

Golang runtime还是需要频繁去检查是否有fd就绪的,严格说并不算真正的异步,算是一种非阻塞IO复用。

io模型

阻塞式IO

程序想在缓冲区读数据时,缓冲区并不一定会有数据,这会造成陷入系统调用,只能等待数据可以读取,没有数据读取时则会阻塞住进程,这就是阻塞式IO。当需要为多个客户端提供服务时,可以使用线程方式,每个socket句柄使用一个线程来服务,这样阻塞住的则是某个线程。虽然如此可以解决进程阻塞,但是还是会有相当一部分CPU资源浪费在了等待数据上,同时,使用线程来服务fd有些浪费资源,因为如果要处理的fd较多,则又是一笔资源开销。

非阻塞式IO

与之对应的是非阻塞IO,当程序想要读取数据时,如果缓冲区不存在,则直接返回给用户程序,但是需要用户程序去频繁检查,直到有数据准备好。这同样也会造成空耗CPU。

IO多路复用(包含:select/poll/epoll)

包含:select/poll/epoll

而IO多路复用则不同,他会使用一个线程去管理多个fd,可以将多个fd加入IO多路复用函数中,每次调用该函数,传入要检查的fd,如果有就绪的fd,直接返回就绪的fd,再启动线程处理或者顺序处理就绪的fd。这达到了一个线程管理多个fd任务,相对来说较为高效。常见的IO多路复用函数有select,poll,epoll。select与poll的最大缺点是每次调用时都需要传入所有要监听的fd集合,内核再遍历这个传入的fd集合,当并发量大时候,用户态与内核态之间的数据拷贝以及内核轮询fd又要浪费一波系统资源(关于select与poll这里不展开)。

select/poll/epoll三者的区别

I/O多路复用就是通过一种机制,一个进程可以监视多个文件描述符,一旦某个描述符就绪(读就绪或写就绪),能够通知程序进行相应的读写操作 。但是,select,poll,epoll本质还是同步I/O(I/O多路复用本身就是同步IO)的范畴,因为它们都需要在读写事件就绪后线程自己进行读写,读写的过程阻塞的。而异步I/O的实现是系统会把负责把数据从内核空间拷贝到用户空间,无需线程自己再进行阻塞的读写,内核已经准备完成。

(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。

(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。

epoll介绍

接下来介绍一下epoll系统调用

epoll相比于select与poll相比要灵活且高效,他提供给用户三个系统调用函数。Golang底层就是通过这三个系统调用结合goroutine完成的“异步”IO。

//用于创建并返回一个epfd句柄,后续关于fd的添加删除等操作都依据这个句柄。
int epoll_create(int size);
//用于向epfd添加,删除,修改要监听的fd。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
//传入创建返回的epfd句柄,以及超时时间,返回就绪的fd句柄。
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
  • 调用epoll_create会在内核创建一个eventpoll对象,这个对象会维护一个epitem集合,可简单理解为fd集合。
  • 调用epoll_ctl函数用于将fd封装成epitem加入这个eventpoll对象,并给这个epitem加了一个回调函数注册到内核,会在这个fd状态改变时候触发,使得该epitem加入eventpoll的就绪列表rdlist。
  • 当相应数据到来,触发中断响应程序,将数据拷贝到fd的socket缓冲区,fd缓冲区状态发生变化,回调函数将fd对应的epitem加入rdlist就绪队列中。
  • 调用epoll_wait时无需遍历,只是返回了这个就绪的rdlist队列,如果rdlist队列为空,则阻塞等待或等待超时时间的到来。

大致工作原理如图

异步IO

当用户程序想要读取fd数据时,系统调用直接通知到内核并返回处理其他的事情,内核将数据准备好之后,通知用户程序,用户程序再处理这个fd上的事件。

Golang异步IO实现思路

我们都知道,协程的资源占有量很小,而且协程也拥有多种状态如阻塞,就绪,运行等,可以使用一个协程服务一个fd不用担心资源问题。将监听fd的事件交由runtime来管理,实现协程调度与依赖fd的事件。当要协程读取fd数据但是没有数据时,park住该协程(改为Gwaiting),调度其他协程执行。

在执行协程调度时候,去检查fd是否就绪,如果就绪时,调度器再通知该park住的协程fd可以处理了(改为Grunnable并加入执行队列),该协程处理fd数据,这样既减少了CPU的空耗,也实现了消息的通知,用户层面上看实现了一个异步的IO模型。

Golang netpoll的大致思想就是这样

猜你喜欢

转载自blog.csdn.net/chushoufengli/article/details/115231156