I/O多路转接(多路复用)又被称为“事件驱动”,是操作系统提供的一个功能,当你关心的文件(如socket)可读、可写时(称为事件就绪)采用某种方式通知你,只有收到通知时你才去执行read/write操作,这样在每次读或写时就不会阻塞,即I/O操作中等的部分交给操作系统内核去完成,而read/write之类的操作只需要在事件就绪时完成数据拷贝。等的过程由select/poll/epoll等系统调用触发,这些函数可同时监视多个描述符上的事件是否就绪,因此可以在一个线程内不发生阻塞的交替完成多个文件的I/O操作。复用是指复用同一个线程。
1.I/O多路转接之select
函数声明:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数解释:
- nfds 需要关注的最大文件描述符编号加1;通知指定所关注的最大描述符,内核就只需监视此范围内文件描述符。
- readfs/writefds/exceptfds分别是关注的可读文件描述符集合、可写文件描述符集合、处于异常条件的描述符集合。由参数位置确定关住的事件类型。
- timeout结构用来设置select的等待时间。timeout=NULL则一直等下去,直到收到信号或有事件就绪才返回;为0时,根本不等待,检测一遍描述符状态,立即返回;特定的时间值,如果在特定的时间内关注的描述符之一已准备好则立即返回,并且timeout将被更新为剩余时间,若指定时间内没有事件发生,select将超时返回。
函数返回值:
- 返回值为正值:已经事件就绪的文件描述符数,如果一个文件同时读写就绪,则会被计两次数;
- 0值:没有事件就绪,没有一个事件就绪,等待时间就到了。此时3个描述符集都会被置0;
- -1:出错,例如在一个事件都没就绪时捕捉到一个信号。此时不会对文件描述符集做修改。
文件描述符集fd_set结构本质上是一个位图,其中比特位值为1/0代表对于该文件描述符的某种事件关注/不关注,对fd_set结构的操作有:
void FD_CLR(int fd, fd_set *set); //清除fd_set中相关的fd位 int FD_ISSET(int fd, fd_set *set);//检测fd位是否为真,事件是否就绪 void FD_SET(int fd, fd_set *set);//将fd添加到fd_set中 void FD_ZERO(fd_set *set);//将fd_set清0
注意:函数中文件描述符集参数既是输入型参数也是输出型参数,因此需要在每次调用select之前被重新设定。因此也就需要用户维护一个容器保存关注的所有文件描述符。
select的特点:
typedef struct fd_set { fd_mask fds_bits[howmany(FD_SETSIZE, NFDBITS)]; } fd_set;
- 由于fd_set结构大小固定,即位图大小固定,因此select可监控的文件描述符个数有上限,取决于sizeof(fd_set),上限为sizeof(fd_set)*8;
- 需要维护一个容器来保存所有关注的文件描述符,(1)在select调用前先使用FD_ZERO将fd_set清0,再遍历该容器设置fd_set,并获得文件描述符最大值,用来设置select第一个参数;(2)在select返回时,遍历其使用FD_ISSET检测是否事件就绪。
select缺点:
- 每次调用前需要手动设置fd_set,使用非常不便;
- 每次调用时,会将fd_set从用户态拷贝至内核态,当关注的文件描述符很多时,是很大的开销;
- 在select返回时需要遍历查询那个文件描述符的那个事件就绪了,这也是很大的开销;
- select可监视的文件描述符个数有上限。
2.I/O多路转接之poll
函数声明:
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ };
函数参数:
- fds为一个结构体数组,每个元素为一个pollfd结构体,fd为文件描述符,events为用户关注的事件集合(输入型),由用户设置,revents为已就绪的事件集合(输出型),由内核设置;对于events/revents类型short,每一种类型的事件关心与否可以一个比特位0/1表示,因此事件类型不会超过16种,够用。
- nfds为fds数组的有效元素个数;
- timeout不同于select中,此处单位为毫秒,整形值(0、-1、>0)。
返回值:
小于0表示出错,等于0表示超时,大于0,有几个事件就绪。
poll的优点:
- 不同于select,poll的参数只需维护一个pollfd结构体数组;
- poll没有最大数量限制,因为pollfd数组可由用户扩容。但poll返回时需要遍历数组检测就绪事件,因此也不易过大。
poll的缺点:
- poll返回时,需要遍历pollfd来获取就绪事件及描述符;
- 每次调用poll都需要将pollfd结构从用户态拷贝至内核态;
- 同时关注的众多描述符可能只有很少处于就绪状态,而每次返回都有挨个遍历也会使其效率下降。
以上缺点在监听的文件描述符数目增多时都会影响效率。
用例:poll_server
3.I/O多路转接之epoll
epoll是为了处理大批量句柄而做了优化的poll(man手册),几乎修正了select和poll的所有缺点。
相关系统调用:
#include <sys/epoll.h> int epoll_create(int size); //Since Linux 2.6.8, the size argument is ignored, but must be greater than zero;
创建一个epoll的句柄,返回一个描述符指向这个epoll句柄。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll事件注册函数,epoll_ctl通过(EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL)三个操作来分散对需要监控的fds集合的修改,做到了有变化才变更,将select或poll高频、大块内存拷贝(集中处理)变成epoll_ctl的低频、小块内存的拷贝(分散处理),避免了大量的内存拷贝。
struct epoll_event结构如下:
typedef union epoll_data { void *ptr; int fd; //关注的文件描述符 uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
epoll_wait
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
参数events是由用户提前分配好的一个满足需求足够大小的结构体数组,maxevents为该结构体数组的容量,timeout同poll,当epoll_wait返回时内核将所有已就绪事件epoll_event拷贝至events数组内(epoll_event结构体内容与用户设置时相同),返回值为存在就绪事件描述符个数。
epoll工作原理
当调用epoll_create时,内核会创建一个eventpoll结构体,该结构体中有两个重要的成员:
- 一个红黑树的根结点,这棵树中存储着所有通过epoll_ctl注册到epoll模型中的事件,每个节点代表一个文件描述符。利用红黑树可以在调用epoll_ctl时高效的进行增删查改,存在重复的事件也可高效的识别出来。
- 一个双向链表,用来存储已经就绪的事件。
当执行epoll_ctl时,除了将事件增加到红黑树中,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个事件就绪了,就将该事件添加到就绪链表中。因此在调用epoll_wait时,只需检查就绪链表内是否有数据,有数据立即返回,没有数据,就在timeout时间内阻塞。epoll_wait不同于select/epoll的是,不需要在被调用时遍历自己所关注的事件,查看是否有就绪的,因为提前注册事件,这些工作都被内核中回调函数与就绪链表替代,epoll_wait只需检测链表是否为空,等待就好。
epoll使用过程:
- epoll_create:创建一个epoll对象;
- epoll_ctl:将需要监控的文件描述符及事件注册到epoll对象;
- epoll_wait:等待事件就绪。
epoll优点:
- 接口使用方便:不需要像select每次调用都要设置关注的描述符及事件;
- 数据轻量拷贝:只需要在关注的事件有变化时,调用epoll_ctl进行增删改,拷贝epoll_event结构至内核,而poll/select都是每次调用都要拷贝所有关注的事件结构至内核。
- 事件回调机制:避免使用遍历,使用回调函数将就绪的描述符结构加入就绪队列中,epoll_wait只需直接访问就绪队列。效率不随着文件描述符的增多而下降。
- 没有数量限制。
epoll工作方式:以socket为例
水平触发LT
当读事件就绪,此时接受缓冲区内有数据,若不讲接受缓冲区内数据处理完,则会一直触发读事件就绪;
当写事件就绪,此时发送缓冲区中有空间,若未将发送缓冲区打满,则会一直触发写事件就绪。
边缘触发ET
当接收缓冲区为满或有新的数据到来,会触发一次读事件就绪,如果此后再也没有数据到来,就再也不会触发读事件就绪,即使缓冲区内依然有之前收到的数据未处理完;
当发送缓冲区为空或剩余空间变化时,会触发一次写事件就绪,如果发送缓冲区剩余空间一直不变,就一直不触发,即使还有剩余空间。
可见在ET模式下,在读事件就绪时,需要一次将所有数据全部读取,因为如果再无读事件就绪,就再也无法读到未读完的数据。所以需要循环式的读取缓冲区中内容,直到实际读取大小小于期待读取大小或读到空为止,因此在ET模式下,描述符必须设为非阻塞,如果读到空,可避免阻塞。对于单线程而言,倘若阻塞,一直没有数据到来,就挂了。
例:客户端向服务器发送10k数据(触发服务器一次读事件就绪),服务器处于ET模式下,服务器由于某种原因一次只读取了1k数据,剩余9k处于仍接受缓冲区内,只有下次客户端再向服务器发送数据(再触发读事件就绪),服务器才能读到那9k数据。由于服务器没能读到完整信息,无法给客户端响应,客户端没收到响应,也不会发送下一个请求,该连接将处于僵持状态。为避免上述状况(一次read未能将数据读完),可以采用非阻塞轮询的方式来读取缓冲区,确保一定能将数据全部读出。
select/poll工作在LT模式下,epoll默认工作方式为LT,也支持ET。
epoll的使用场景:
对于多连接的,且只有一部分连接比较活跃时,比较适合用epoll。
多路复用,事件驱动本质上还是IO事件,适宜于IO密集型工作,一个工作进程就可完成。但对于计算密集型事务,事件驱动并不合适,一个计算需要CPU耗时2秒,这两秒对于整个进程是完全阻塞的,即使有事件就绪也只能等待。这是多进程多线程就体现出优势,每个线程各做各的事情互补干扰。因此事件驱动适宜IO密集型业务,多进程多线程适宜计算密集型业务。
原文地址:https://www.cnblogs.com/dabai56/p/11279835.html