[操作系统] IO模式、select、poll、epoll
参考博客:
https://segmentfault.com/a/1190000003063859
https://zhuanlan.zhihu.com/p/95872805
当一次IO操作执行时,会产生数据等待时间。通常这个时间不短,因此在Linux中产生了多种网络模式IO复用方案。
阻塞/非阻塞IO、IO多路复用、信号驱动IO、异步IO。
注意:IO多路复用并非多线程/进程模型,而是由一个进程/线程管理多个IO事件(网络编程中是负责多个业务处理,应对多个套接字),而使用线程池、任务池进行IO复用实际上是多线程\进程进行阻塞IO操作。
1.阻塞IO
没啥好说的,等待IO时间处理完毕。
2.非阻塞IO
用户进程发出读写操作后,若数据读取没有完成,则立即返回一个错误。如此程序可以不用一直等待。
这种模型的特点是:用户进程需要不断地主动询问kernel数据是否准备好。
3.IO多路复用
也称为Event Driven IO,其优势在于一个线程/进程可以处理多个IO事件。
其机制为让一个简称监视多个文件描述符,当某个描述符就绪,进程就对它进行相应的操作。
一般有select、poll、epoll三种模式。
3.1 select
select监视三类文件描述符,并在形参中传入其集合(类型为fd_set)的指针:readfds、writefds、exceptfds。
调用select函数后进程会阻塞,随后可以通过遍历fdset来找到就绪的描述符。select有timeout参数,当超时时会停止阻塞(参数为-1时永远不会停止)。
其运行机制为将fd_set从用户空间拷贝到内核空间,然后注册回调函数,在内核中进行数据是否准备完成的判断。若有就绪则select返回(显然也会将fd_set拷贝回去以供判断)
缺点:
每次调用时都需要将fd_set拷贝到内核态;
内核中需要对fd_set进行遍历,开销较大;
select支持的fd数量有限(因为FD_SETSIZE的限制),一般为1024。
注:传入的fd_set本质上是一个文件描述符结构体数组,数组当然是固定长度且有上限的。
3.2 poll
改进版的select,使用结构体pollfd来指定需要监听的描述符,pollfd包括fd(文件描述符)、events(监听事件)和revents(调用返回事件)。
poll的文件描述符数量不需要限制为最大1024。
缺点:
每次调用需要将pollfd数组拷贝到内核态;
每次调用,内核就需要遍历所有传入的fd;
Q:poll和select的区别是什么?
A:poll使用了pollfd作为传入内核的文件描述符结构,相对方便,并且没有最大限制。
3.3 epoll
将文件描述符事件保存在内核的一个事件表中,通过内存映射使其可在用户空间中访问,省去了拷贝开销。
其使用步骤为:
int ep = epoll_create(size); //创建epoll实例 int op = ...; struct epoll_event * events = ...; int ok = epoll_ctl(ep, op, events); //向epoll注册事件 if(ok == -1) {...} //失败处理 struct epoll_event * done_events = ...; //用以保存就绪的事件 int maxevents = ...; //希望返回的最大事件数量,一般为done_events的大小 int timeout = ...; //超时时间,-1则永不超时 int event_num = epoll_wait(ep, done_events, maxevents, timeout); //等待返回就绪描述符事件,event_num是就绪数量
运行机制:
1. epoll的fd(文件描述符)在内核态和用户态之间共享,实现方式为内存映射方法mmap。
2. epoll_ctl调用时,会为传入的每个fd指定一个回调函数,当fd就绪时,会由回调函数将fd加入就绪链表,最后epoll_wait只需要检查就绪链表即可。
3. 由于2,epoll只需要关心就绪的fd,在连接较多(需要处理的fd较多)的情况下,效率更高。
工作模式:
1. 水平触发 LT(level trigger)
默认模式,当fd就绪并通知程序后,若程序不立即处理,那么事件会被放回就绪链表。
2. 边缘触发 ET(edge trigger)
fd就绪后程序需要立即处理,否则不会放回就绪链表(意味着下一次调用wait不会返回这个fd)
Q:为什么有ET?
当系统中充满大量可能不关心的文件事件时(例如大量不需要进行读写的文件),可以采用ET。这样可以避免每次都得到太多就绪事件的返回。
优点:
1. 使用内存映射,减少数据拷贝消耗;
2. 通过给每个fd注册回调函数进行实现,不需要每次遍历所有fd从而导致性能下降;
3. 文件描述符数量不受限制;
另外还有类似于Epoll的Kqueue;
4.异步IO
用户进程发起IO请求后,继续其他处理任务,由系统负责数据读取,当读取完毕后系统向进程发送信号通知对方操作完成可以接收数据。
Java中的future包可以完成这种操作。
5.比较
1)最大连接数
Select 由FD_SETSIZE宏定义,最大为2^32-1,当然具体的还是根据操作系统内核的配置决定。
Poll 与Select没有本质区别,但它的最大连接数基于链表存储。
Epoll FD上限为系统支持的可以打开文件的数量。
2)FD剧增带来的IO效率问题
Select 由于每次调用都会对连接线性遍历,因此FD增加会造成速度的显著下降。
Poll 与Select相同。
Epoll 由于实现是活跃的FD调用回调函数,因此活跃FD较少时不会产生线性的速度下降问题,但是大量FD活跃时依然有性能问题。
3)消息传递方式
Select、Poll 内核到用户空间的消息传递都需要拷贝。
Epoll 内核和用户空间共享一块内存来实现。
4)什么时候选择select,什么时候选择epoll?
当连接数量少并且活跃时,select和poll性能可能更佳,因为其轮询机制带来的线性性能下降影响不明显,反而是epoll因为回调函数的存在会导致一些性能消耗。