select、poll和epoll

介绍:

在说select、poll和epoll之前,先说下IO多路复用机制,简单来说,就是服务端通过一个线程(或者进程)来维护多个Socket连接,多个连接共用一个阻塞对象,线程只需要在这个阻塞对象上等待,无需再轮询多个连接。当任何一个连接上有数据可以处理时,操作系统就会通知进程,进程就阻塞的状态返回,开始进行业务处理。

select、poll和epoll都是IO多路复用机制的常见方式,通过这种方式,可以监控多个文件描述符,一旦某个描述符就绪,操作系统就能够通知进程进行相应的读写操作。
select、poll和epoll都是同步IO。

select:

select函数允许内核挂起当前进程,当一个或多个IO事件发生后,再唤醒该进程。
select的定义如下:

int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);

参数说明:
maxfd指待测试的文件描述符个数,它的值是待测试的最大文件描述符加 1。(因为文件描述符是从0开始的)
readset是读描述符集合,代表内核可以在这些描述符上检测数据是否可读。
writeset是写描述符集合,代表内核可以在这些描述符上检测数据是否可写。
exceptset是异常描述符集合,代表内核可以在这些描述符上检测数据是否有异常发生。
timeout指的是等待描述符上数据就绪的超时时间,是一个结构体,,定义如下:

struct timeval {
  long   tv_sec; /* seconds */
  long   tv_usec; /* microseconds */
};

如果把该参数设置为null,则表示一直等待,直到有数据就绪。如果把tv_sec和 tv_usec都设置为0,则表示不等待,还可以设置非0的值,表示等待的时间。

该方法的返回值:
如果有就绪的文件描述符,则返回就绪的文件描述符的数目,若超时则为0,若出错则为-1。

select的缺点:
每次调用select,都需要把fd的集合从用户态拷贝到内核态。而且select能够支持的文件描述符数量太小,默认只有1024个。

poll:

poll和select的功能类似,两个接口描述fd的方式不同,poll使用的是pollfd,而select使用的是fd_set。poll和select只是编程接口上的区别,内核的本质实现其实是差不多的。但是克服了select的文件描述符限制的缺陷,比select使用的更广。
poll的定义如下:

int poll(struct pollfd *fds, unsigned long nfds, int timeout); 

函数需要三个入参,第一个参数是pollfd 的数组。pollfd的定义如下:

struct pollfd {
    int    fd;       /* file descriptor */
    short  events;   /* events to look for */
    short  revents;  /* events returned */
 };

fd代表文件描述符,events代表描述符上待检测的文件类型,可以表示多个不同的事件,revents存放每次检测完的结果。
nfds描述的是数组 fds 的大小,简单说,就是向 poll 申请的事件检测的个数。
timeout也是代表的等待的超时时间,如果大于0,表示等待指定的毫秒数后返回,等于0表示立即返回,小于0表示永远等待。

函数返回值:
如果有就绪的文件描述符,则返回就绪的文件描述符的数目,若超时则为0,若出错则为-1。

epoll:

epoll是对select和poll的改进,在面对大量文件描述符时,性能表现非常出色。
epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait。
epoll_create:

int epoll_create(int size);
int epoll_create1(int flags);

epoll_create方法创建一个epoll实例,epoll实例用来调用epoll_ctl和epoll_wait。
返回值: 若创建成功则返回一个大于0的值,表示epoll实例;若返回-1表示出错
epoll_ctl:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

调用epoll_ctl方法可以往epoll实例中增加或删除监控的事件,入参有四个,epfd是一个epoll实例描述字,就是通过epoll_create创建的epoll实例。op表示增加还是删除一个监控事件,fd是注册的事件的文件描述符。event表示的是注册的事件类型。
返回值: 若成功返回0;若返回-1表示出错。
epoll_wait:

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epoll_wait和select、poll功能类似,等待事件的就绪,调用后进程会被挂起。
入参有四个,第一个参数epfd是 epoll 实例描述字,events代表返回给用户空间需要处理的 I/O 事件,maxevents表示 epoll_wait 可以返回的最大事件值,timeout也是代表超时时间,如果为-1表示不超时,如果为0则是立即返回。
返回值: 成功返回的是一个大于0的数,表示事件的个数;返回0表示的是超时时间到;若出错返回-1。

select和poll提供的是水平触发(level-triggered)机制,而epoll除了提供水平触发机制外,还提供了性能更强的边缘触发(edge-triggered)机制。
水平触发:当检测到某描述符事件就绪,服务器端需要不断的从epoll_wait中苏醒,直到内核缓冲区的数据被读取完成。水平触发是epoll默认的方式。
边缘触发:当检测到某描述符事件就绪,服务器端只需要从epoll_wait中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;

如果用的是水平触发方式,当内核通知文件描述符可以读写时,接下来还可以继续检测它的状态,判断是否可以继续读写。
但如果使用的是边缘触发方式,内核只会通知一次可以读写,而且还不知道能读写多少内容,所以收到通知后一般是去循环读取,以免失去读写机会。
一般来说,边缘触发方式能够减少epoll_wait的系统调用,所以效率比水平触发要高。

epoll是Linux内核为处理大批量文件描述符而对poll进行的改进,算是select和poll的增强版本。在大量并发并且少量连接活跃的场景中能显著提高CPU的利用率,被称为解决C10K的利器。

上一篇:IO复用总结


下一篇:Appuim源码剖析(Bootstrap)