I/O复用:select、poll、epoll

目录

1.I/O复用:

2.select:

(1)select的API:

(2)处理带外数据:

3.poll:

(1)poll的API:

4.epoll:

(1)epoll的不同:

(2)epoll_ctl():

(3)epoll_wait():

(4)LT和ET:

5.三种I/O复用方式的比较:

(1)从事件集看:

(2)从实现方式看:

(3)总结:

6.写在最后:


1.I/O复用:

I/O复用使得程序能同时监听多个文件描术符,这对提高程序的性能至关重要。需要指出的是,I/O复用虽然能同时监听多个文件描述符,但它本身是阻塞的。并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能按顺序依次处理其中的每一个文件描述符,这使得服务器程序看起来像是串行工作的。如果要实现并发,只能使用多进程或多线程等编程手段。

2.select:

  select系统调用的用途是﹔在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件。

(1)select的API:

#include<sys/select.h>
int select(int nfds,fd_set* readfds,fd_set*writefds,fe_set*exceptfds,struct timeval*timeout);

(1)nfds参数指定被监听的文件描述符的总数。

(2)readfds、writefds 和 exceptfds参数分别指向可读、可写和异常等事件对应的文件描述符集合。应用程序调用select函数时,通过这3个参数传入自己感兴趣的文件描述符。

fd_set类型的结构体:fd_set结构体仅包含一个整型数组,该数组的每个元素的每一位(bit)标记一个文件描述符。fd_set能容纳的文件描述符数量由FD_SETSIZE指定,这就限制了sclect 能同时处理的文件描述符的总量。

(3)timeout参数用来设置select函数的超时时间。

网络程序中,select能处理的异常情况只有一种: socket 上接收到带外数据(简单来说,就是一种优先级比普通数据高的数据)

(2)处理带外数据:

socket上接收到普通数据和带外数据都将使select返回,但socket处于不同的就绪状态:前者处于可读状态,后者处于异常状态。

对于普通数据是直接调用普通recv函数读取数据,带外数据是采用带MSG_OOB标志的recv函数读取带外数据

代码如下:

//对于可读事件,调用普通recv函数读取数据
if(FD_ISSET(connfd,&read_fds))
{
    ret=recv(connfd,buf,sizeof(buf)-1,0);
    if(ret<=0)
    {
      break;
    }
}
//对于异常事件,采用带MSG_OOB标志的recv函数读取带外数据
else if(FD_ISSET(connfd,&exception_fds))
{
    ret=recv(connfd,buf,sizeof(buf)-1,MSG_OOB);
    if(ret<=0)
    {
      break;
    }
}

3.poll:

poll系统调用和select类似,也是在指定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者。

(1)poll的API:

#include<poll.h>
int poll(struct pollfd* fds,nfds_t nfds,int timeout);

(1)fds参数是一个pollfd结构类型的数组,它指定所有我们感兴趣的文件描述符上发生的可读、可写和异常等事件。

      pollfd结构体的定义如下:

struct pollfd
{
  int fd; //文件描述符
  short events; //注册的事件
  short revents;//实际发生的事件,由内核填充
}
  • fd成员指定文件描述符;
  • events成员告诉poll监听f上的哪些事件,它是一系列事件的按位或;
  • revents成员则由内核修改,以通知应用程序fd上实际发生了哪些事件。

(2)nfds参数指定被监听事件集合fds 的大小。

(3)timeout参数指定poll的超时值,单位是毫秒。

4.epoll:

(1)epoll的不同:

epoll是Linux特有的IO复用函数。它在实现和使用上与select、poll有很大差异。首先,epoll使用一组函数来完成任务,而不是单个函数。其次,epoll把用户关心的文件描术符上的事件放在内核里的一个事件表中,从而无须像select和 poll那样每次调用都要重复传入文件描述符集或事件集。但epoll需要使用一个额外的文件描述符,来唯标识内核中这个事件表。

  • epoll的一组函数:epoll_create()用于创建内核时间表;
  •                                     epoll_ctl()用于操作内核事件表;
  •                                     epoll_wait()用于在一段超时时间内等待一组文件描述符上的事件;
  • (2)epoll_ctl():

  • #include<sys/epoll.h>
    int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);

    (1)fd参数是要操作的文件描述符,

  • (2)op参数则指定操作类型:

    •       EPOLL_CTL_ADD,往事件表中注册fd上的事件。
    •       EPOLL_CTL_MOD,修改fd上的注册事件。
    •       EPOLL_CTL_DEL,删除fd上的注册事件。
    • (3)event参数指定事件,它是epoll_event结构指针类型。
struct epoll_event
{
   _unint32_t events;//epoll事件
   epoll_data_t data;//用户数据
}

 events成员描述事件类型。
 data成员用于存储用户数据,

(3)epoll_wait():

epoll系列系统调用的主要接口是epoll_wait函数。它在一段超时时间内等待一组文件描述符上的事件。

epoll_wait函数如果检测到事件,就将所有就绪的事件从内核事件表(由epfd参数指定)中复制到它的第二个参数events 指向的数组中。这个数组只用于输出epoll_wait检测到的就绪事件,而不像select和 poll的数组参数那样既用于传入用户注册的事件,又用于输出内核检测到的就绪事件。这就极大地提高了应用程序索引就绪文件描述符的效率。

poll和epoll在使用上的差别:poll必须遍历所有已注册文件描述符并找到其中的就绪者,epoll仅遍历就绪的文件描述符,所以时间复杂度上poll为O(n),epoll为O(1)

(4)LT和ET:

epoll对文件描述符的操作有两种模式: LT (Level Trigger,电平触发)模式和ET(EdgeTrigger,边沿触发〉模式。LT模式是默认的工作模式,这种模式下epoll相当于一个效率较高的poll。

(1)对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件,直到该事件被处理。

(2)而对于采用ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率要比LT模式高。

5.三种I/O复用方式的比较:

(1)从事件集看:

(1)select的参数类型fd_set没有将文件描述符和事件绑定,它仅仅是一个文件描述符集合,因此select需要提供3个这种类型的参数来分别传入和输出可读、可写及异常等事件。这一方面使得select不能处理更多类型的事件,另一方面由于内核对fd_set集合的在线修改,应用程序下次调用select前不得不重置这3个fd_set集合。并且应用程序索引就绪文件描述符的时间复杂度为O(n)。

(2)poll 的参数类型pollfd则多少“聪明”一些。它把文件描述符和事件都定义其中,任何事件都被统一处理,从而使得编程接口简洁得多。并且内核每次修改的是pollfd结构体的revents 成员,而events成员保持不变,因此下次调用poll时应用程序无须重置pollfd 型的事件集参数。由于每次select和 poll调用都返回整个用户注册的事件集合(其中包括就绪的和未就绪的),所以应用程序索引就绪文件描述符的时间复杂度为O(n)。

(3)epoll则采月与select和 poll完全不同的方式来管理用户注册的事件。它在内核中维护一个事件表,并技供了一个独立的系统调用epoll_ctl来控制往其中添加、删除、修改事件。这样,每次 epollwait调用都直接从该内核事件表中取得用户注册的事件,而无须反复从用户空间读入这些享件。epoll_wait系统调用的events参数仅用来返回就绪的事件,这使得应用程序索引就绪文件描述符的时间复杂度达到O (1)。

(2)从实现方式看:

(1)select和 poll采用的都是轮询的方式,即每次调用都要扫描整个注册文件描述符集合,并将其中就绪的文件描述符返回给用户程序,因此它们检测就绪事件的算法的时间复杂度是О (n)。(2)epoll_wait则不同,它采用的是回调的方式。内核检测到就绪的文件描述符时,将触发回调函数,回调函数就将该文件描述符上对应的事件插人内核就绪事件队列。内核最后在适当的时机将该就绪事件队列中的内容拷贝到用户空间。因此epoll_wait无须轮询整个文件描述符集合来检测哪些事件已经就绪,其算法时间复杂度是O (1)。但是,当活动连接比较多的时候,epoll_wait的效率未必比select和 poll高,因为此时回调函数被触发得过于频繁。所以epoll_wait适用于连接数量多,但活动连接较少的情况。

(3)总结:

6.写在最后:

通常,大部分网络编程都会用到I/O复用,对提高程序的性能至关重要。大家可以去看看《linux高性能服务器编程》,里边有几个I/O复用的高级应用,如:非阻塞的connect、聊天程序、同时处理TCP和UDP服务,可以让你对I/O复用有更深刻的了解。

猜你喜欢

转载自blog.csdn.net/weixin_57133901/article/details/124902102