前面文章中说了一些关于IO多路复用的知识,今天专门总结一下select、poll、epoll的用法和区别;
IO复用有时也称为事件驱动,基本原理就是:有一个函数会不断轮询所负责的socket,当某个socket有数据到达了,就通知用户进程。所以称之为事件驱动!
这里的函数就是用select、poll、epoll来实现轮询的功能。
select
select在socket编程中还是相当重要的,可是很多初学者并不爱用select写程序,习惯直接用connect、accept、recv或recvfrom这样的阻塞程序。使用select就可以完成非阻塞方式工作的程序,它能够监视需要被监视的文件描述符的变化情况——读、写或异常。
函数原型
int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval*timeout);
这里用到了两个结构体,fd_set
和timeval
。
1、结构体fd_set可以理解为一个集合,这个集合中存放的是文件描述符,即文件句柄,这可以认为是常说的普通意义文件,socket就是一个文件(在linux中),所以socket句柄就是一个文件描述符。
2、结构体timeval是一个常用得结构,用来代表时间值,有两个成员,一个是秒数一个是毫秒数。
3、maxfdp
是一个整数值,是指集合中所有文件描述符得范围,即最大得文件描述符加1。
4、readfds
是指向fd_set结构的指针,这个集合中应该包括文件描述符。因为要监视文件描述符是变化的,即关心是否可以从这些文件中读取数据,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读。如果没有可读的文件,则根据timeout参数再判断是否超时:若超出timeout的时间,select返回0;若发生错误则返回负数;
5、writefds
是指向fd_set结构的指针,这个集合中应该包括文件描述符。因为要监视文件描述符是可写变化的,同样与readfds一样,返回值代表含义:大于0——有可写,等于0——超时,小于0——错误。
6、errorfds
同上面两个参数的意图,用来监视文件错误异常。
7、timeout
是select的超时时间,这个参数至关重要,它可以使select处于三种状态:第一种是若将NULL以形参传入,则不传入时间结构,就是将select置于阻塞状态,一定要等待监视文件秒描述符集合中某个文件描述符发生变化为止。第二种是将时间设置为0,就变成了一个纯粹的非阻塞函数,不管描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正数。第三种,timeout大于0,就是等待的超时时间,即select在timeout时间内阻塞,超时时间内有时间到来就返回了,否则在超时后不管怎么样都返回,返回值同上。
poll
和select一样,poll也可以用于执行多路复用IO。函数原型:
int poll(struct pollfd * fds,unsigned int nfds,int timeout);
pollfd
结构体的定义如下:
struct pollfd
{
int fd; // 文件描述符
short events; // 等待的事件
short revents; // 实际发生了的事件
};
每一个结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。每个events
域是监视该文件描述符的事件掩码,由用户来设置这个域的属性,revents
域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。
详细看一下revents事件的事件分类:
事件分类 | 事件代码 | 意义 |
---|---|---|
合法事件 | POLLIN | 有数据可读 |
合法事件 | POLLRDNORM | 有普通数据可读 |
合法事件 | POLLRDBAND | 有优先数据可读 |
合法事件 | POLLPRI | 有紧迫数据可读 |
合法事件 | POLLOUT | 写数据不会导致阻塞 |
合法事件 | POLLWRNORM | 写普通数据不会导致阻塞 |
合法事件 | POLLWRBAND | 写优先数据不会导致阻塞 |
合法事件 | POLLMSGSIGPOLL | 消息可用 |
非法事件 | POLLER | 指定的文件描述符发生错误 |
非法事件 | POLLHUP | 指定的文件描述符挂起事件 |
非法事件 | POLLNVAL | 指定的文件描述符非法 |
timeout参数和select一样,返回值也一样,但是这里当返回值为负数时(timeout为大于0时),只返回-1,并设置errno为下列值之一:
EBADF | 一个或多个结构体中指定的文件描述符无效 |
---|---|
EFAULTfds | 指针指向的地址超出进程的地址空间 |
EINTR | 请求的事件之前产生一个信号,调用可以重新发起 |
EINVALnfds | 参数超出PLIMIT_NOFILE |
ENOMEM | 可用内存不足,无法完成请求 |
epoll
epoll是在Linux2.6内核中提出的,是之前select和poll的增强版本,相对于select和poll来说,epoll更加灵活,没有描述符限制,epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间之间的数据拷贝只需要一次。
epoll接口
首先使用epoll必须包含 #include <sys/epoll.h>
epoll操作过程需要3个接口,分别如下:
int epoll_create(int size);
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_create(int size);
创建一个epoll句柄,size用来告诉内核要监听的数目,这个参数不同于select中的第一个参数,是最大监听的fd+1的值。当创建好了epoll句柄后,就会占用一个fd的值,所以在使用完epoll后,一定要close()关闭,否则可能导致fd被耗尽。
int epoll_ctl(int epfd,int op,int fd, struct epoll_event *event);
epoll的事件注册函数, 它不同于select()在监听事件时告诉内核要监听什么类型的事件,而是先注册要监听的事件类型,
第一个参数是epoll_create()的返回值;
第二个参数表示动作,用三个宏来表示:EPOLL_CTL_ADD
,注册新的fd到epfd中;EPOLL_CTL_MOD
:修改已经注册的fd的监听事件;EPOLL_CTL_DEL
:从epfd中删除一个fd。
第三个参数是需要监听的fd;
第四个参数是告诉内核要监听什么事,strcut epoll_event结构如下:
strcut epoll_event
{
_uint32_t events; // epoll监听的事件
epoll_data_t data; // 用户数据可变
};
events可以是以下几个宏的集合:EPOLLIN
,表示对应的文件描述符可以读;EPOLLOUT
,表示对应的文件描述符可以写;EPOLLPRI
,表示对应的文件描述符有紧急数据可读;EPOLLERR
,表示对应的文件描述符发生错误;EPOLLHUP
,表示对应的文件描述符被挂断;
int epoll_wait(int epfd,struct epoll_event * events, int maxevents, int timeout);
等待事件的发生,类似于select()调用,参数events用来从内核得到事件的集合,maxevents告诉内核这个events有多大,且maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间。
总结
select、poll、epoll的异同:
同:
select、poll、epoll都是多路IO复用的机制,多路IO复用就通过一种机制,可以实现多个描述符,一旦某个描述符就绪(一般是读写),能够通知程序进行相应的读写操作。但是select、poll、epoll本质上都是同步IO,因为它们都需要在读写事件就绪后自己负责读写,即是阻塞的,而异步IO则无需自己负责读写,异步IO的实现会负责把数据从内核空间拷贝到用户空间!
异:
1、首先看一下select和poll:
1、poll()在应付大数目的文件描述符的时候更快,因为对于select来说内核
需要检查大量描述符对应的fd_set中的每一位比特,比较费时。
2、select监控的数量是固定的,相对来说较少(1024或2048),如果需要监听
数值比较大的描述符效率很低,而poll可以创建特定大小的数组来保存监控的描述
符,而不受文件描述符值的大小限制,而且poll的监控文件数量远大于select。
3、select的超时参数在返回时是未定义的,考虑到可移植性,每次在超时之后再
下一次进入到select之前都需要重新设置超时参数。
4、select的可移植性更好,在某些UNIX系统上不支持poll。select对于超时值提供了更好的精度。
下面对epoll单独介绍一下它的强大之处!
第一:支持一个进程打开最大数目的socket描述符;
select最大的缺点就是一个进程打开的fd有限制,由FD_SETSIZE的默认值1024/2048。对于那些需要支持上万链接数量的服务器来说显然不够。这时候可以选择修改这个宏然后重新编译内核。不过epoll没有这个限制,它所支持的FD上限是最大可以打开的文件的数目,这个数目一般远大于2048,例如,1G内存空间中这个数字一般是10w左右。
第二:IO效率不随FD数目增加而线性增加!
传统的select/poll另一个致命的弱点就是当你拥有一个很大的socket集合,不过由于网络延迟,任一时间只有部分的socket是活跃的,但是select/poll每次调用都会线性扫描全部集合,导致效率呈线性下降。但是epoll不存在这个问题,它只会对活跃的socket进行操作——这个是因为在内核中实现epoll是根据每个fd上面的callback函数实现的。那么只有活跃的socket才会主动去调用callback函数,在这一点上,epoll实现了一个伪AIO,因为这时候推动力由Linux内核提供。
第三:使用mmap加速内核与用户空间的消息传递。
这点实际上涉及epoll的具体实现,无论是select、poll还是epoll都需要内核把fd消息通知给用户空间,如何避免不必要的内存拷贝就非常重要了,在这里epoll是通过内核与用户空间mmap处于同一块内存实现的。