IO多路复用技术本质就是: 通过一种机制,使得单个进程可以监视多个文件描述符,一旦某个描述符就绪(读就绪或者写就绪),就能够通知进程进行相应读写操作。
文件描述符:
linux下一切都被看做文件,包含普通文件,目录文件,设备文件,套接字等等。抽象为文件,提供统一接口,方便调用。
为了将文件与进程对应上,对于进程打开的每个文件,内核会返回一个文件描述符。本质上其实就是一个非负整数。
每个进程有一个文件描述符表,每个表的存储一个指向文件的指针,指向系统打开文件的表,然后打开文件表内存储指向inode的指针。
Unix网络编程中的五种IO模型
网络IO涉及两个交互1. 等待数据准备 2. 将数据从内核拷贝到用户内存中
缓存IO: 缓存IO机制中,OS会将IO数据先拷贝到操作系统内核缓冲区中,然后才拷贝到用户进程地址空间
阻塞IO 用户进程会阻塞等待数据,进程被挂起,陷入等待状态,等数据准备好执行系统调用将数据从内核拷贝到用户内存,然后内核返回结构。进程解除block状态,重新运行。
非阻塞IO 用户进程请求数据之后会立即返回结果。如果数据没准备好,每隔一段时间再发送一次,一旦准备好,执行系统调用将数据拷贝到用户内存,然后返回。
IO多路复用 看起来与阻塞IO相似,等待数据、拷贝数据都阻塞,区别在于 等待多个数据就绪,即可以处理多个连接。进程调用select之后就被slect阻塞,会监听多个文件描述符直到有数据就绪。
一般会设置为非阻塞IO,select通过轮训机制判断每个IO数据是否就绪。
信号驱动IO 进程请求之后,内核立即返回。等待数据就绪之后,发送信号给进程,信号处理程序中将数据从内核空间拷贝到用户内存
异步IO 不阻塞进程,即使从内核空间缓冲区将数据拷贝到进程中也不阻塞,而是通过一个回调函数通知进程拷贝完成。
select、poll、 epoll
select
函数原型:
int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);
参数一:最大的监听的文件描述值+1,从0开始
参数二三四: 指定要监听的文件文件描述符的集合
参数五: 超时时间 指定内核等待就绪文件描述符的时间。两个特殊case: NULL, 0
返回值: 就绪文件描述符数目,如果是超时返回是0, 出错返回-1.
工作流程:
- 将FD_SET从用户空间拷贝到内核空间
- 内核轮询遍历FD_SET中的文件描述符,调用文件描述符对应的设备驱动的poll函数,返回当前文件的状态,设置就绪回调函数,并将调用进程放入文件描述符对应的等待序列中。
- 遍历完成,如果没有文件描述符就绪, 将进程睡眠,等待timeout时间内有文件描述符就绪。这段时间文件就绪后,会触发回调函数,唤醒等待队列上的当前进程。再次轮询一遍,FD_SET统计所有就绪的文件描述符。进入步骤四
- 遍历完成之后,如果有文件描述符就绪,退出循环,将进程从全部文件描述符等待序列中删除。并返回就绪数量。
特点:
- select/poll 每次都需要将FD_SET从用户空间传递到内核空间
- fd事件回调函数是 pollwake,只是唤醒调用进程,调用进程需要重新遍历fd。
- 当有事件发生时,返回的FD_SET保存所有FD,尽管其中只有很少fd就绪
- select/poll 返回时,会将该进程从全部监听的FD等待队列中删除,这样就需要每次都需要重新传入全部监听fd,然后重新挂载。
- 单个进程能够监听的最大文件描述符的数量为1024(内核定义 FD_SETSIZE)。
POLL
函数原型:
poll机制与select类似,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 重点是没有 没有文件描述符数量限制。
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
typedef struct pollfd {
int fd; // 需要被检测或选择的文件描述符
short events; // 对文件描述符fd上感兴趣的事件
short revents; // 文件描述符fd上当前实际发生的事件
} pollfd_t;
参数一: dfs是一个pollfd类型数组,用于存放监听的描述符,并且调用之后不会被清空。
参数二: 描述符数量
最后一个参数以及返回值与select一致。
poll相比select, 文件描述符集合的表示方式不同,poll使用pollfd结构, 是链式的,没有最大连接数量限制。
EPOLL
函数原型:
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);
函数一创建一个epoll句柄,size表示要监听的描述符的数量
函数二,将被监听的文件描述符添加到epoll句柄或者是修改、删除epoll句柄中的文件描述符
函数三,等待事件触发,参数二表示从内核中得到的事件集合,参数三表示这个事件集合size最大值(不能大于函数一的size)
工作流程:
-
在内核空间中建立监听列表(红黑树实现),通过epoll_ctl 添加fd
-
调用fd对应的驱动程序的poll函数,返回fd的状态,并将事件就绪回调函数加入文件描述符等待队列。回调函数将就绪fd加入就绪队列中,同时唤醒本进程,
-
调用epoll_wait会将本进程添加到等待队列,然后判断就绪队列是否为空,为空的话进程睡眠一段时间,然后重新判断。不为空,将就绪事件拷贝到用户空间中。
与Select/Poll机制相同点:
- 主要监测流程一致,都需要当前进程挂载到fd上,fd就绪事件发生后,回调函数基本功能是唤醒本进程
- 都需要监测是否有就绪事件,有就返回,没有就睡眠本进程,不同的是select、poll监测每个fd,epoll监测就绪队列。
优点:
- 没有最大监听文件描述符数量限制
- 将监听的文件描述符放在内核空间,减少了用户空间与内核空间数据拷贝次数
- IO不会随着FD数目增加而线性下降
- 使用mmap(内存映射)技术传递FD消息,内核空间与用户空间使用同一块内存实现。
EPOLL支持两种事件类型: ET以及LT
ET模式只支持非阻塞SOCKET, LT都支持。
LT模式下,内核通知一个文件描述符就绪,如果不做操作时,内核还是会就绪通知。select、poll都是该模式
ET模式下,只有当描述符从未就绪变为就绪时,内核才会通知。只支持非阻塞IO,避免由于一个文件描述符的阻塞读、阻塞写将整个进程饿死。因此需要循环读、写数据,直到产生一个EAGIN错误才认为此时事件处理完成。
EPOLLONESHOT: 设置了ONESHOT的文件描述符,操作系统最多触发可读、可写或者异常事件中的一种,而且只触发一次。
保证任意时刻同一socket连接只能被一个线程处理
设置了ONESHOT之后,epoll等待注册在epoll句柄上文件描述符的事件发生,如果发生,则将文件描述符以及事件类型记录在events中,然后将注册在句柄上的文件描述符的该事件类型给清除,如果希望继续关注,需要使用epoll_ctl(epoll_fd, EPOLL_CTL_MOD, listen_fd, &events)重新设置事件类型。
设置了ONESHOT事件,在事件的数据处理完成之后,应该立刻重置ONESHOT,保证其他线程能处理该socket上的后续事件。
多线程编程中,如果使用LT模式,只要缓冲区还有未处理的数据,就会一直触发,很可能被多个线程处理。如果此时使用ONESHOT, 每个线程只能取到很小一部分数据,可能会导致错误。
因此,多线程编程中的LT模式,设置EPOLLONESHOT之后,也需要循环读。