I/O复用是Linux中的I/O模型之一。所谓I/O复用,指的是进程预先告诉内核,使得内核一旦发现进程指定的一个或多个I/O条件就绪,就通知进程进行处理,从而不会在单个I/O上导致阻塞。
在Linux中,提供了select、poll、epoll三类接口来实现I/O复用。
select函数接口
select中主要就是一个select函数,用于监听指定事件的发生,原型如下:
1 |
#include<sys/select.h> |
其中各参数的含义如下:
maxfd:最大文件描述符加1,比它小的从0开始的描述符都将被监视,它的值不能超过系统中定义的FD_SETSIZE(通常是1024)。
rset,wset,eset:分别表示监视的读、写、错误的描述符位数组,通常是一个整数数组,每一个整数可以表示32个描述符是否被监视。需要注意的是这几个参数都是值-结果参数,在调用select后这几个参数将表示哪些描述符就绪了。通过以下几个宏可以很方便的操作fset数组:
1 |
void FD_ZERO(fd_set *fdset); |
timeout:超时时间,即select最长等待多久就返回,为NULL时表示等到有操作符准备就绪后才返回。该时间可以精确到微秒,其结构如下:
1 |
struct timeval{ |
描述符就绪条件
对于普通数据的读写,描述符就绪显而易见,但仍有一些特殊情况时描述符会读写就绪,UNP中对描述符的读写就绪条件进行了说明。1)满足以下4个条件时,描述符准备好读
a)套接字接收缓冲区中的数据字节数大于套接字接收缓冲区低水位标记的当前大小(默认为1),读将会返回大于0的数。
b)该连接的读半部关闭,读将会返回0。
c)套接字上有一个错误待处理,读将返回-1。
d)该套接字是一个监听套接字并且已完成连接数不为0。2)满足以下4个条件时,描述符准备好写
a)套接字发送缓冲区中的可用空间字节数大于等于套接字发送缓冲区低水位标记的当前大小(默认2048),写将会返回大于0的数。
b)该连接的写半部关闭,写将会返回EPIPE。
c)套接字上有一个错误待处理,写将返回-1。
d)使用非阻塞式connect的套接字建立有结果返回。
poll函数接口
poll中的主要函数也只有一个poll,与select作用类似,但参数有所不同,函数原型如下:
1 |
#include<poll.h> |
其中各参数的含义如下:
fdarray:是一个指向pollfd结构数组的指针,维护着描述符以及事件信息,该结构体是poll里比较核心的结构体,结构如下:
1 |
struct pollfd{ |
该结构体通过两个变量区分关注的事件和发生的事件,从而避免了使用值-结果参数。events和revents可选的标志位如下:
1 |
POLLIN //普通或优先级带数据可读 |
nfds:指定结构体数组中元素的个数。
timeout:每次调用poll最大等待的毫秒数,负值代表等待到直到有事件触发。
epoll函数接口
epoll主要有三个函数,函数原型如下:
1 |
#include <sys/epoll.h> |
epoll_create(int size)
size:能监听多少个描述符,返回一个epoll描述符。注意使用完epoll后要关闭该描述符。
epoll_ctl(int efd, int op, int fd, struct epoll_event *event)
efd:epoll_create返回的epoll描述符
op:表示动作,可以在以下三个宏里选择一个
1 |
EPOLL_CTL_ADD //注册新的fd到epoll中 |
fd:要监听的fd
event:告诉内核要监听什么事件,其结构如下:
1 |
typedef union epoll_data { |
其中events表示epoll事件,可选的标志位如下:
1 |
EPOLLIN //描述符可以读 |
而epoll_data_t使用了union来存储数据,用户可以使用data来存放一些关于该fd的额外内容。
标志位中比较特殊的是EPOLLET这个选项,这个选项将EPOLL设置为边缘触发模式,EPOLL有EPOLLET和EPOLLLT两种工作模式。
EPOLLLT(Level Triggered,水平触发模式):默认工作模式,支持block和no-block socket,内核通知你描述符事件后,如果不进行操作,会一直通知。
EPOLLET(Edge Triggered,边缘触发模式):高速工作模式,只支持no-block socket,只会在描述符状态由未就绪转为就绪时会通知一次,使用该模式时,如果程序编写的不够健全,是很容易出现问题的。
epoll_wait(int efd, struct epoll_event *events, int maxevents, int timeout);
该函数与select和poll函数的功能类似,监视指定事件的发生并返回给用户。
efd:epoll_create返回的opoll描述符。
events:用来从内核得到事件的集合。
maxevents:用来告知内核events数组的大小。
timeout:超时时间,-1将阻塞直到有事件发生,否则表示最多等待多少毫秒后函数就返回。
select,poll,epoll比较
select
- select能监控的描述符个数由内核中的FD_SETSIZE限制,仅为1024,这也是select最大的缺点,因为现在的服务器并发量远远不止1024。即使能重新编译内核改变FD_SETSIZE的值,但这并不能提高select的性能。
- 每次调用select都会线性扫描所有描述符的状态,在select结束后,用户也要线性扫描fd_set数组才知道哪些描述符准备就绪,等于说每次调用复杂度都是O(n)的,在并发量大的情况下,每次扫描都是相当耗时的,很有可能有未处理的连接等待超时。
- 每次调用select都要在用户空间和内核空间里进行内存复制fd描述符等信息。
poll
- poll使用pollfd结构来存储fd,突破了select中描述符数目的限制。
- 与select的后两点类似,poll仍然需要将pollfd数组拷贝到内核空间,之后依次扫描fd的状态,整体复杂度依然是O(n)的,在并发量大的情况下服务器性能会快速下降。
epoll
- epoll维护的描述符数目不受到限制,而且性能不会随着描述符数目的增加而下降。
- 服务器的特点是经常维护着大量连接,但其中某一时刻读写的操作符数量却不多。epoll先通过epoll_ctl注册一个描述符到内核中,并一直维护着而不像poll每次操作都将所有要监控的描述符传递给内核;在描述符读写就绪时,通过回掉函数将自己加入就绪队列中,之后epoll_wait返回该就绪队列。也就是说,epoll基本不做无用的操作,时间复杂度仅与活跃的客户端数有关,而不会随着描述符数目的增加而下降。
- epoll在传递内核与用户空间的消息时使用了内存共享,而不是内存拷贝,这也使得epoll的效率比poll和select更高。
程序示例
分别使用select,poll和epoll实现了简单的回显服务器程序,客户端使用select来实现。其中select和poll程序主要参考unp的实现,只是Demo程序,对一些异常情况没有进行处理。
客户端程序
使用select来监听终端输入和连接服务器的流输入,这样可以保证客户端不在某一个输入流上死等。
1 |
#include <sys/socket.h> |
select服务器
1 |
#include <sys/socket.h> |
poll服务器
1 |
#include <sys/socket.h> |
epoll服务器
回显服务器使用了ET高速模式。在该模式下,最好所有的操作都是非阻塞的,程序中套接字都设置为了non-socket,并且使用了缓冲区,在读到数据时先将数据存到缓冲区中,下次可写时才将数据从缓冲区写回客户端。
另外,在ET模式下,accept、read、write时都要使用循环直到读到EAGAIN才能说明没有数据了。
1 |
#include <sys/epoll.h> |
本文标题:Linux中的IO复用接口简介
文章作者:Vimer Su
发布时间:2013年11月19日 - 19时56分
最后更新:2016年06月04日 - 22时57分
原始链接:http://vimersu.win/blog/2013/11/19/linux-io-reuse-interface/
许可协议: "署名-非商用-相同方式共享 3.0" 转载请保留原文链接及作者。