Linux I/O多路复用

多路复用(I/O multiplexing)是实现多路I/O操作的重要技术,在块I/O和网络I/O中有着重要的应用。Linux中主要有select/poll/epoll三种系统调用函数用于支持I/O多路复用技术。

I/O并发访问

在一般的应用中只需要访问一个文件描述符,阻塞I/O模型就能很好地实现客户需要。但是在网络应用等许多场合,应用中往往需要并发地访问两个甚至多个文件描述符,这时普通的阻塞I/O模型已难已满足需求。此时,根据前面的I/O模型有如下思路可解决并发问题:

  1. 基于阻塞I/O模型的多进程/线程技术:每一个进程/线程控制一个文件描述符的阻塞访问,从而实现多路并发访问。
  2. 基于非阻塞I/O模型的轮询技术:用轮询的方式非阻塞地访问所有文件描述符
  3. 异步I/O技术:利用异步I/O模型在收到信号后非阻塞地访问所有文件描述符(无法确认准备好的描述符,故需非阻塞访问所有)
  4. I/O多路复用技术:利用poll/select等I/O多路利用函数监听所有文件描述符状态,并在描述符准备好后访问相应描述符
多进程/线程技术需要处理进程间的通信或是线程同步,轮询方式浪费大量的CPU时间用于描述符状态查询,异步I/O信号中无法包含描述符状态信息需要非阻塞访问所有描述符,都很难达到I/O多路利用技术在处理I/O并发问题在系统开销和程序复杂度方面的效果。

select

select通过系统调用监视三个文件描述符集合(可读/可写/异常),监听事件发生或时间到select返回,就绪的文件描述符会保留在集合中而未就绪的将会被从集合中清除,进程可以从集合中获得就绪的文件描述符并继续I/O操作。
#include  
#include  
int select(int maxfd, fd_set *rdset, fd_set *wrset, fd_set *exset, struct timeval *timeout);    
maxfd - 被监听的最大fd+1
rdset - 读操作 被监听fd集合
wrset - 操作被监听fd集合
exset - 异常 被监听fd集合
timeout - 监听超时时间
return返回监听到的就绪fd数目(就绪fd被置位于对应的fd_set中)
另外,Linux提供以下宏作为select的辅助:
FD_SET(int fd, fd_set *set)-集合中添加fd
FD_CLR(int fd, fd_set *set) - 集合中删除fd
FD_ISSET(int fd, fd_set *set) - fd是否在集合中
FD_ZERO(fd_set *set) - 清空集合

poll

poll通过系统调用监听一个事件pollfd集合(监听的事件在pollfd中),监听事件发生或时间到poll返回,集合中的每个pollfd中中存有监听到的就绪事件,进程可轮询集合中的所有pollfd从中获得就绪文件描述符及相关事件并继续I/O操作。
int poll(struct pollfd *ufds, unsigned int nfds, int timeout)    

struct pollfd
{
int fd;           /* file descriptor */
short events;     /* requested events */
short revents;    /* returned events */
};

相对于select,poll有两个方面的改进:
  1. poll不再有描述符个数限制
  2. poll将文件描述符、监听事件和就绪事件分开保存,近回后不会丢失文件描述符和监听的事件信息
  3. poll调用时会清除所有pollfd中的就绪事件信息,避免多次poll操作时每次都要重新配置数据

epoll

epoll是Linux 2.5.44为处理大批量文件描述符而引入的的多路复用机制,在处理大批量监听任务时比select/poll更高效。不同于select/poll,epoll会专门维护一个队列来保存就绪的事件信息,这样只需查询就绪队列即可而无需查询所有文件描述符。

#include
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);      

struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};
epoll的实现有三个系统函数:epoll_create用于创建epoll文件保存文件描述符相关信息;epoll_ctrl用于配置文件描述符事件监听信息;epoll_wait用于阻塞查询就绪队列。在epoll机制中,只要有文件描述符通过epoll_ctrl添加到系统中即开始工作,监听事件信息并维护就绪队列。

不同于select/poll,epoll有两种工作模式:
  • LT模式(Level Trigger):默认工作方式,支持block和non-block操作。LT模式下,epoll_wait过程会把就绪事件拷贝到用户进程同时仍保存LT就绪事件。
  • ET模式(Edge Trigger):高速工作方式,只支持non-block操作。ET模式下,epoll_wait过程会把就绪事件拷贝到用户进程同时删除ET就绪事件。
注:epoll_wait并不是直接把就绪队列中的事件拷贝到用户进程,在拷贝之前会重新扫描各就绪事件文件描述符(调用设备poll),然后将就绪队列中的所有内容转移到用户进程中。在向用户进程转移过程中会丢弃状态已发生变化的事件(由就绪变为等待),同时就绪队列中会保留一份LT模式的就绪事件备份。这样,epoll_wait返回后,就绪队列中将只保留LT模式的用户事件和新产生的用户事件。

内核实现

所有的I/O多路复用函数(select/poll/epoll)都是通过调用设备的poll函数来实现多路访问的控制,其差别只是在等待和事件处理的策略上有所不同。在Linux内核中,select/poll的调用过程大致为:
  1. 执行select/poll系统调用
  2. 将函数参数拷贝到内核空间
  3. 扫描所有文件描述符(调用设备poll函数),若当前fd监听事件未就绪,将当前进程加入到设备等待队列(在其中注册唤醒函数)并记录扫描状态;否则直接记录扫描状态
  4. 所有fd扫描一遍后,若未扫描到就绪事件且定时器时间未到,当前进程进入休眠;否则从内核返回
  5. 若定时器时间到或休眠中进程被设备被唤醒,重新扫描所有fd 并从内核返回
与select/poll在实现上有一些差别,epoll的调用过程为:
  1. 执行epoll_create系统调用,在系统中创建一个epoll文件(保存监听事件、就绪队列等信息)
  2. 执行epoll_ctrl系统调用,添加/删除/修改一个epoll事件,添加事件会将当前进程添加到对应设备的等待队列(删除反之),若监听事件发生将会被添加到epoll就绪队列中,并唤醒epoll进程(如果在睡眠中)
    注:监听多个事件需多次调用epoll_ctrl
  3. 执行epoll_wait系统调用,扫描epoll就绪队列更新事件状态并将队列中事件转移到用户空间,如果其中无事件且定时时间未到,当前进程进入休眠;否则从内核返回
  4. 若定时间到或休眠中进程被设备唤醒,重新扫描epoll就绪队列并从内核返回

epoll优点

  1. epoll采用回调机制检测就绪事件不需要检测所有的文件描述符,其性能只与当前活跃的文件描述符有关,而select/poll性能受所监听文件描述符数量影响
  2. epoll只会在epoll_ctrl时进行一次文件描述符从用户空间到内核的拷贝和相关等待队列的添加,epoll_wait不需要做这些工作;而select/poll每次调用都要进行文件描述符的拷贝和相关等待队列的添加








你可能感兴趣的:(Linux)