本文首发在 技术成长之道 博客,访问 hechen0.com 查看更多,或者微信搜索「技术成长之道」关注个人公众号,或者扫描下方二维码关注公众号得到第一时间更新通知!html
一句话解释:单线程或单进程同时监测若干个文件描述符是否能够执行IO操做的能力。linux
应用程序一般须要处理来自多条事件流中的事件,好比我如今用的电脑,须要同时处理键盘鼠标的输入、中断信号等等事件,再好比web服务器如nginx,须要同时处理来来自N个客户端的事件。nginx
逻辑控制流在时间上的重叠叫作 并发git
而CPU单核在同一时刻只能作一件事情,一种解决办法是对CPU进行时分复用(多个事件流将CPU切割成多个时间片,不一样事件流的时间片交替进行)。在计算机系统中,咱们用线程或者进程来表示一条执行流,经过不一样的线程或进程在操做系统内部的调度,来作到对CPU处理的时分复用。这样多个事件流就能够并发进行,不须要一个等待另外一个过久,在用户看起来他们彷佛就是并行在作同样。github
但凡事都是有成本的。线程/进程也同样,有这么几个方面:web
有没有一种能够在单线程/进程中处理多个事件流的方法呢?一种答案就是IO多路复用。redis
所以IO多路复用解决的本质问题是在用更少的资源完成更多的事。编程
为了更全面的理解,先介绍下在Linux系统下全部IO模型。数组
目前Linux系统中提供了5种IO处理模型服务器
这是最经常使用的简单的IO模型。阻塞IO意味着当咱们发起一次IO操做后一直等待成功或失败以后才返回,在这期间程序不能作其它的事情。阻塞IO操做只能对单个文件描述符进行操做,详见read或write。
咱们在发起IO时,经过对文件描述符设置O_NONBLOCK flag来指定该文件描述符的IO操做为非阻塞。非阻塞IO一般发生在一个for循环当中,由于每次进行IO操做时要么IO操做成功,要么当IO操做会阻塞时返回错误EWOULDBLOCK/EAGAIN,而后再根据须要进行下一次的for循环操做,这种相似轮询的方式会浪费不少没必要要的CPU资源,是一种糟糕的设计。和阻塞IO同样,非阻塞IO也是经过调用read或writewrite来进行操做的,也只能对单个描述符进行操做。
IO多路复用在Linux下包括了三种,select、poll、epoll,抽象来看,他们功能是相似的,但具体细节各有不一样:首先都会对一组文件描述符进行相关事件的注册,而后阻塞等待某些事件的发生或等待超时。更多细节详见下面的 "具体怎么用"。IO多路复用均可以关注多个文件描述符,但对于这三种机制而言,不一样数量级文件描述符对性能的影响是不一样的,下面会详细介绍。
信号驱动IO是利用信号机制,让内核告知应用程序文件描述符的相关事件。这里有一个信号驱动IO相关的例子。
但信号驱动IO在网络编程的时候一般不多用到,由于在网络环境中,和socket相关的读写事件太多了,好比下面的事件都会致使SIGIO信号的产生:
上面全部的这些都会产生SIGIO信号,但咱们没办法在SIGIO对应的信号处理函数中区分上述不一样的事件,SIGIO只应该在IO事件单一状况下使用,好比说用来监听端口的socket,由于只有客户端发起新链接的时候才会产生SIGIO信号。
异步IO和信号驱动IO差很少,但它比信号驱动IO能够多作一步:相比信号驱动IO须要在程序中完成数据从用户态到内核态(或反方向)的拷贝,异步IO能够把拷贝这一步也帮咱们完成以后才通知应用程序。咱们使用 aio_read 来读,aio_write 写。
同步IO vs 异步IO
- 同步IO指的是程序会一直阻塞到IO操做如read、write完成
- 异步IO指的是IO操做不会阻塞当前程序的继续执行
因此根据这个定义,上面阻塞IO固然算是同步的IO,非阻塞IO也是同步IO,由于当文件操做符可用时咱们仍是须要阻塞的读或写,同理IO多路复用和信号驱动IO也是同步IO,只有异步IO是彻底完成了数据的拷贝以后才通知程序进行处理,没有阻塞的数据读写过程。
os | 解决方案 |
---|---|
Linux | select、poll、epoll |
MacOS/FreeBSD | kqueue |
Windows/Solaris | IOCP |
软件 | 解决方案 |
---|---|
redis | Linux下 epoll(level-triggered),没有epoll用select |
nginx | Linux下 epoll(edge-triggered),没有epoll用select |
我在工做中接触的都是Linux系统的服务器,因此在这里只介绍Linux系统的解决方案
相关函数定义以下
/* According to POSIX.1-2001, POSIX.1-2008 */
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
int pselect(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
复制代码
select的调用会阻塞到有文件描述符能够进行IO操做或被信号打断或者超时才会返回。
select将监听的文件描述符分为三组,每一组监听不一样的须要进行的IO操做。readfds是须要进行读操做的文件描述符,writefds是须要进行写操做的文件描述符,exceptfds是须要进行异常事件处理的文件描述符。这三个参数能够用NULL来表示对应的事件不须要监听。
当select返回时,每组文件描述符会被select过滤,只留下能够进行对应IO操做的文件描述符。
FD_xx系列的函数是用来操做文件描述符组和文件描述符的关系。
FD_ZERO用来清空文件描述符组。每次调用select前都须要清空一次。
fd_set writefds;
FD_ZERO(&writefds)
复制代码
FD_SET添加一个文件描述符到组中,FD_CLR对应将一个文件描述符移出组中
FD_SET(fd, &writefds);
FD_CLR(fd, &writefds);
复制代码
FD_ISSET检测一个文件描述符是否在组中,咱们用这个来检测一次select调用以后有哪些文件描述符能够进行IO操做
if (FD_ISSET(fd, &readfds)){
/* fd可读 */
}
复制代码
select可同时监听的文件描述符数量是经过FS_SETSIZE来限制的,在Linux系统中,该值为1024,固然咱们能够增大这个值,但随着监听的文件描述符数量增长,select的效率会下降,咱们会在『不一样IO多路复用方案优缺点』一节中展开。
pselect和select大致上是同样的,但有一些细节上的区别。
相关函数定义
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
#include <signal.h>
#include <poll.h>
int ppoll(struct pollfd *fds, nfds_t nfds, const struct timespec *tmo_p, const sigset_t *sigmask);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
复制代码
和select用三组文件描述符不一样的是,poll只有一个pollfd数组,数组中的每一个元素都表示一个须要监听IO操做事件的文件描述符。events参数是咱们须要关心的事件,revents是全部内核监测到的事件。合法的事件能够参考这里。
相关函数定义以下
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_create1(int flags);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask);
复制代码
epoll_create&epoll_create1用于建立一个epoll实例,而epoll_ctl用于往epoll实例中增删改要监测的文件描述符,epoll_wait则用于阻塞的等待能够执行IO操做的文件描述符直到超时。
这两种底层的事件通知机制一般被称为水平触发和边沿触发,真是翻译的词不达意,若是我来翻译,我会翻译成:状态持续通知和状态变化通知。
这两个概念来自电路,triggered表明电路激活,也就是有事件通知给程序,level-triggered表示只要有IO操做能够进行好比某个文件描述符有数据可读,每次调用epoll_wait都会返回以通知程序能够进行IO操做,edge-triggered表示只有在文件描述符状态发生变化时,调用epoll_wait才会返回,若是第一次没有所有读完该文件描述符的数据并且没有新数据写入,再次调用epoll_wait都不会有通知给到程序,由于文件描述符的状态没有变化。
select和poll都是状态持续通知的机制,且不可改变,只要文件描述符中有IO操做能够进行,那么select和poll都会返回以通知程序。而epoll两种通知机制可选。
在epoll状态变化通知机制下,有一些的特殊的地方须要注意。考虑下面这个例子
在第5步的epoll_wait调用不会返回,而对应的客户端会由于服务端没有返回对应的response而超时重试,缘由就是我上面所说的,epoll_wait只会在状态变化时才会通知程序进行处理。第3步epoll_wait会返回,是由于客户端写了数据,致使rfd状态被改变了,第3步的epoll_wait已经消费了这个事件,因此第5步的epoll_wait不会返回。
咱们须要配合非阻塞IO来解决上面的问题:
经过上述方式,咱们能够确保每次epoll_wait返回以后,咱们的文件描述符中没有读到一半或写到一半的数据。
poll和select基本上是同样的,poll相比select好在以下几点:
而select比poll好在下面几点
但整体而言 select和poll基本一致。
epoll优于select&poll在下面几点:
本文从使用者的角度,从问题出发,介绍了多种IO多路复用方案,有任何问题欢迎在下方留言交流,或扫描二维码/微信搜索『技术成长之道』关注公众号后留言私信。
PS:代码永远是最正确的,man文档其次,更多细节能够多看代码和文档。
参考