I/O复用及它们之间的区别

来源:互联网 发布:js代码反混淆工具 编辑:程序博客网 时间:2024/05/21 09:37

1.select

用途:在指定的时间内,监听用户感兴趣的读事件、写事件和异常事件。

1.1 select API
int select( int nfds , fd_set readfds, fd_set writefds, fd_set *exceptfds , struct timeval *timeout);
参数介绍:
nfds :指定被监听的文件描述符的总数,通常是最大数加1(文件描述符都是从0开始计数的)
readfds、writrfds、exceptfds表示拥护感兴趣的事件,可读、可写、异常事件,三个参数的类型都是fd_set结构体的,fd_set能容纳的最大文件描述符由FD_SETSIZE决定。由于未操作过于繁琐,所以用一些函数来操作

FD_ZERO( fd_set *fdset); //清除fdset所有的位
FD_SET(int fd, fd_set *fdset );//设置fdset的位fd
FD_CLP( int fd, fd_set *fdset );//清楚fdset的位fd
FD_ISSET(int fd, fd_set *fdset);测试fdset的位fd是否被设置;
timeout :用来设置select函数的超时时间。如果传0,则select 将立即返回,如果穿NULL,则select一直阻塞。

1.2 文件描述符就绪条件

下列条件下认为是可读的;

  • socket内核接收缓冲区中的字节大于或者等于低水位标记SO_RECVLOWAT.此时我们就可以无阻塞的读该socket,并且返回的读操作字节数大于0;
  • socket通信的对方关闭连接,则此时select返回0;
  • 监听socket上有新的客户端连接请求;
  • socket上面有未处理的请求错误;

下面条件是可写的:

  • socket内核发送缓冲区中的字节数大于或者等于其低水位的标记SO_SNDLOWAT;此时我们可以无阻塞的写改socket,并且返回的字节数大于0;
  • socket写操作被关闭,
  • socket使用非阻塞的connect连接成功之后;
  • socket上有未处理的错误

2 .poll

用途:在一定时间内,轮询一定数量的文件描述符,测试是否有就绪事件发生;
原型:int poll ( struct pollfd *fds, nfds_t nfds,int timeout );
fds :是一个pollfd类型的结构体数组,他将制定我们感兴趣的文件描述符上发生的事件;

struct pollfd{    int fd;         //文件描述符    short event;    //注册事件    short revent;    //实际发生的事件,有内核填充};

fd指定文件描述符;events 告诉poll监听fd上哪些事件;revents内核修改,以通知应用程序上发生那些事件;
这里写图片描述
nfds :指定被监听事件集合fds的大小;
timeout :-1表示永远阻塞,知道某个事件发生; 0表示立即返回;

3.epoll

3.1 内核事件表
epoll是一个特有的io复用函数,他的实现和使用和select 、poll的差距很大。
epoll的底层是一组函数来完成的。epoll把关心的事件放在一个事件表中,无须像select、poll每次调用都要重复传入文件描述符集和事件集。但是epoll只需要使用一个额外的文件描述符,来唯一的标识内核中的这个事件表,这个文件描述符使用如下epoll_create函数来创建。
int epoll_create( int size);这个参数size并不起作用,而是给内核一个提示,告诉它事件表需要多大。
下面的函数用来操作epoll内核表;
int epoll_ctl( int epfd, int op , int fd, struct epoll_event *event);
fd :要操作的文件描述符;op指定操作类型;
EPOLL_CTL_ADD :往事件表中注册fd上的事件;
EPOLL_CTL_MOD :修改fd上的事件;
EPOLL_CTL_DEL :删除fd上的注册事件;
event :指定事件,

3.2 epoll_wait函数

epoll函数调用的主要接口是epoll_wait, 原型:
int epoll_wait( int epfd , struct epoll_event * events, int maxevents,int timeout);
成功返回就绪事件描述符的个数,失败返回-1;
maxevents :指定监听最多事件个数,必须大于0;

epoll_wait函数如果检测到事件,就将所有的就绪事件从内核时间表中复制到它的第二个参数events指向的数组中,这个数组只用于输出epoll_wait检测到的就绪事件,而不像select、poll的数组存放的既有用户注册的事件,又有输出内核检测到的就绪事件。这样子就提高了应用程序的搜索文件描述符的效率。

3.3 LT和ET模式

LT是默认的工作模式。这种模式下,相当于一个效率较高的poll,当epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll将以ET模式操作该文件描述符。ET模式是epoll的效率工作模式。

LT模式:当检测到事件发生时,如果没有处理该事件,下一次调用epool_wait时,还会通知应用程序处理该事件,直到该事件被处理。
ET模式:当有事件发生时,应用程序必须立即处理该事件,后续该事件不会再被处理。
ET模式很大程度上降低了同一个epoll事件被重复触发的次数,因此效率比LT模式高;

3.4 EPOLLONESHOT事件

即使在ET模式下,一个socket上的某个事件还是有可能被触发多次的。比如一个线程得到socket上的一个新的数据,开始处理这个事件,可是与此同时,我们有另一个线程来看socket,于是出现了两个线程处理一个socket的局面,所以我们出现了竞态条件。我们为了处理一个socket只被一个线程处理,我们注册一个EPOLLONESHOT事件;

当被处理成EPOLLONESHOT事件,我们在一个线程处理完socket之后,该线程必须立即重置这个socket的EPOLLONESHOT事件,以确保下一次可读;

三个I/O复用函数的比较

select缺点:
【1】每次调用select都需要把fd从用户态拷贝到内核态,开销比较大
【2】每次都需要在内核遍历传入的fd
【3】select支持文件数量比较小,默认是1024

poll的实现和select非常相似,只是描述fd集合的方式不同,poll使用pollfd结构而不是select的fd_set结构,就不赘述了。

epoll的优点:
select/poll只提供了一个函数,但是epoll一下子就提供了3个函数,真是人多力量大,难怪这么强,如下3个函数:
epoll_create,epoll_ctl和epoll_wait,
epoll_create是创建一个epoll句 柄;
epoll_ctl是注册要监听的事件类型;e
poll_wait则是等待事件的产生。

优点:
【1】每次注册新事件到epoll句柄都会把所有的fd拷贝进来,而不是在epoll_wait中重复拷贝,这样确保fd只会被拷贝一次
【2】epoll不是想select/poll那样每次都把fd加入等待队列中,epoll把每个fd指定一个回调函数,当设备就绪时,唤醒等待队列的等待者就会调用其的回调函数,这个回调函数会把就绪的fd放入一个就绪链表。epoll_wait就是在这个就绪链表中查看有没有就绪fd
【3】epoll没有fd数目限制

总结:
(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内 部定义的等待队列)。这也能节省不少的开销.

这里写图片描述