I/O多路复用select、poll、epoll的区别使用

采用单线程的方式处理高并发

在单线程模型中,可以采用I/O复用来提高单线程处理多个请求的能力,然后再采用事件驱动模型,基于异步回调来处理事件来。

I/O 多路复用技术是为了解决进程或线程阻塞到某个 I/O 系统调用而出现的技术,使进程不阻塞于某个特定的 I/O 系统调用。

select(),poll(),epoll()都是I/O多路复用的机制。I/O多路复用通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪,就是这个文件描述符进行读写操作之前),能够通知程序进行相应的读写操作
他们本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

与多线程和多进程相比,I/O 多路复用的最大优势是系统开销小,系统不需要建立新的进程或者线程,也不必维护这些线程和进程。

select():

监视并等待多个文件描述符的属性变化(可读、可写或错误异常)。
select()函数监视的文件描述符分 3 类,分别是writefds、readfds、和 exceptfds。
调用后 select() 函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有错误异常),或者超时( timeout 指定等待时间),函数才返回。
当 select()函数返回后,可以通过遍历 fdset,来找到就绪的描述符。

#include 
#include 
#include 
#include 

//成功:就绪描述符的数目,超时返回 0,
//出错:-1
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
//nfds: 要监视的文件描述符的范围,一般取监视的描述符数的最大值+1,如这里写 10, 这样的话,描述符 
//0,1, 2 …… 9 都会被监视,在 Linux 上最大值一般为1024。
//readfd: 监视的可读描述符集合,只要有文件描述符即将进行读操作,这个文件描述符就存储到这。
//writefds: 监视的可写描述符集合。
//exceptfds: 监视的错误异常描述符集合

缺点:

  • 每次调用 select(),都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大,同时每次调用 select() 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大。
  • 单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。
poll():

select() 和 poll() 系统调用的本质一样,管理多个描述符也是进行轮询,根据描述符的状态进行处理。
但是 poll() 没有最大文件描述符数量的限制(但是数量过大后性能也是会下降)。

缺点与select()一样:包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。

#include 

int poll(struct pollfd *fds, nfds_t nfds, int timeout);//监视并等待多个文件描述符的属性变化。
//fds: 不同与 select() 使用三个位图来表示三个 fdset 的方式,poll() 使用一个 pollfd 的指针实现。
//一个 pollfd 结构体数组,其中包括了你想测试的文件描述符和事件, 事件由结构中事件域 events 来确定,
//调用后实际发生的时间将被填写在结构体的 revents 域。

//nfds: 用来指定第一个参数数组元素个数。

//timeout: 指定等待的毫秒数,无论 I/O 是否准备好,poll() 都会返回。

struct pollfd{
    int fd;         /* 文件描述符 */
    short events;   /* 等待的事件 */
    short revents;  /* 实际发生了的事件 */
}; 

poll() 的实现和 select() 非常相似,只是描述 fd 集合的方式不同,poll() 使用 pollfd 结构而不是 select() 的 fd_set 结构,其他的都差不多。

epoll():

epoll 使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。

#include 

//成功:epoll 专用的文件描述符
//失败:-1
int epoll_create(int size);//生成一个 epoll 专用的文件描述符(创建一个 epoll 的句柄)。会占用一个 fd 值。
//size:参数 size 并不是限制了 epoll 所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。
//linux 2.6.8 之后,size 参数是被忽略的,也就是说可以填只有大于 0 的任意值。


//成功:0
//失败:-1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//功能:epoll 的事件注册函数,它不同于 select() 是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
//epfd: epoll 专用的文件描述符,epoll_create()的返回值
//op: 表示动作,用三个宏来表示:EPOLL_CTL_ADD(注册新的 fd 到 epfd 中)、EPOLL_CTL_MOD(修改fd监听事件)、EPOLL_CTL_DEL(删除fd)
//fd: 需要监听的文件描述符
//event: 告诉内核要监听什么事件,也是几个宏的集合


//成功:返回需要处理的事件数目,如返回 0 表示已超时。
//失败:-1
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//功能:等待事件的产生,收集在 epoll 监控的事件中已经发送的事件,类似于 select() 调用。
//epfd: epoll 专用的文件描述符,epoll_create()的返回值
//events: 分配好的 epoll_event 结构体数组,epoll 将会把发生的事件赋值到events 数组中
//(events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存)。
//maxevents: maxevents 告之内核这个 events 有多大 。
//timeout: 超时时间,单位为毫秒,为 -1 时,函数为阻塞

epoll 对文件描述符的操作有两种模式:LT(level trigger)和 ET(edge trigger)。
LT 模式:
.socket接收缓冲区不为空 有数据可读 读事件一直触发
.socket发送缓冲区不满 可以继续写入数据 写事件一直触发
符合思维习惯,epoll_wait返回的事件就是socket的状态
只要可读,就一直触发读事件,只要可写,就一直触发写事件。事件循环处理比较简单,无需关注应用层是否有缓冲或缓冲区是否满,只管上报事件。缺点是:可能经常上报,可能影响性能。
ET 模式:
.socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
.socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件
仅在状态变化时触发事件。
从不可读变为可读,从可读变为不可读,从不可写变为可写,从可写变为不可写,都只触发一次。

LT的编程与poll/select接近,符合一直以来的习惯,不易出错
ET的编程可以做到更加简洁,某些场景下更加高效,但另一方面容易遗漏事件,容易产生bug

优点:

  • 监视的描述符数量不受限制。
    它所支持的 FD 上限是最大可以打开文件的数目,这个数字一般远大于 2048,举个例子,在 1GB 内存的机器上大约是 10 万左右。虽然也可以选择多进程的解决方案( Apache 就是这样实现的),不过虽然 Linux 上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。
  • I/O 的效率不会随着监视 fd 的数量的增长而下降。
    轮询就绪期间可能要睡眠和唤醒多次交替。但是 select() 和 poll() 在“醒着”的时候要遍历整个 fd 集合,而 epoll 在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的 CPU 时间。这就是回调机制带来的性能提升。
  • 节省拷贝开销。
    select(),poll() 每次调用都要把 fd 集合从用户态往内核态拷贝一次,而 epoll 只要一次拷贝,这也能节省不少的开销。

epoll怎么实现的

Linux epoll机制是通过红黑树和双向链表实现的。 首先通过epoll_create()系统调用在内核中创建一个eventpoll类型的句柄,其中包括红黑树根节点和双向链表头节点。然后通过epoll_ctl()系统调用,向epoll对象的红黑树结构中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。最后通过epoll_wait()系统调用判断双向链表是否为空,如果为空则阻塞。当文件描述符状态改变,fd上的回调函数被调用,该函数将fd加入到双向链表中,此时epoll_wait函数被唤醒,返回就绪好的事件。

具体步骤:

利用sys_epoll_create()创建内核事件表,在sys_epoll_creat()里面创建了struct eventpoll结构体,其中包括两个成员:

  • 就绪双端队列struct list_head rdlist,用来存放有就绪事件的描述符;
  • 红黑树struct rb_root rbr,作为内核事件表,用来收集描述符;

每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会通过ep_instert挂载到红黑树上,这样重复添加的事件就可以通过红黑树而高效的识别出来;

而所有添加到epoll中的事件都会与驱动程序建立回调关系,当相应的事件发生时,会调用ep_poll_callback这个回调方法,它会将发生的事件添加到rdlist双端队列中;

在epoll中,对于每一个事件,都会建立一个epitem结构体,它里面包括:

  1. 红黑树节点
  2. Rdlist节点
  3. 事件句柄信息
  4. 一个指向其所属的eventpoll对象的指针
  5. 期待发生的事件类型

当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的双端队列rdlist中是否有epitem元素即可。如果rdlist不为空,则把事件复制到用户态,同时将事件数量返回给用户;如果为空,就等待直到超时。

你可能感兴趣的:(I/O多路复用select、poll、epoll的区别使用)