Unix I/O 模型 与 select, poll, epoll

版权声明:欢迎转载 https://blog.csdn.net/antony1776/article/details/84345556

参考文件
https://blog.csdn.net/darmao/article/details/78306200
https://www.zhihu.com/question/32163005/answer/55772739
http://www.knowsky.com/1043584.html

IO 委托

在执行 IO 操作时,进程与系统之间是委托与被委托的关系!
在这里插入图片描述

用户进程没有直接访问 IO 设备的权限,所有的 IO 操作必须通过系统调用的形式委托给内核,内核会检测 IO 设备的状态,并负责数据拷贝。

因此,整个流程可以简单的概况为——A 委托 B 去做某事!

在 B 工作期间,A 的行为模式有多种选择:

  • 同步阻塞:在 B 工作期间,阻塞等待,直到 B 完成并返回结果;
  • 轮询:在 B 工作期间,A 离开去做其他事,但是每过一段时间就查看一下 B 的工作状态;
  • 通知:B 在工作期间,一旦有新情况(状态)就通知 A;

阻塞IO的模型,显然,这是最低效的方式。
在这里插入图片描述

轮询,就是在同一个线程中,使用非阻塞 IO 读写数据。比如,先将两个输入描述符设置为非阻塞,对第一个描述符调用 read,如果该输入上有数据,则读数据并处理它,如果没有数据,则 read 立即返回,处理后面的文件描述符。

轮询的问题是无法确定轮询的频率,因为大多数时间实际上是无数据可读的,大量的无效的轮询会造成 CPU 的浪费。
在这里插入图片描述

异步 IO(Asynchronous io),基本思想是告诉内核,当 io 可用时通知我。信号机制提供了一种以异步形式通知某种事件已经发生的方法,由 BSD(SIGIO) 和 SystemV(SIGPOLL) 派生的所有系统提供了使用信号的异步 IO 方法,该信号通知进程某个描述符已经发生了所关心的某个事件。

这种技术有两个问题,第一,并非所有系统都支持该机制;第二,基于信号的通知方式,并不能告诉线程到底是哪个描述符可用,仍然需要轮询一遍。
在这里插入图片描述

select

select, poll, epoll 都是I/O多路复用的具体的实现,之所以有这三个鬼存在,其实是他们出现是有先后顺序的。
在这里插入图片描述
I/O多路复用这个概念被提出来以后, select是第一个实现 (1983 左右在BSD里面实现的)。select 被实现以后,很快就暴露出了很多问题。

  1. select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的。
  2. select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但是并不会告诉你是那个sock上有数据,于是你只能自己一个一个的找,10几个sock可能还好,要是几万的sock每次都找一遍,这个无谓的开销就颇有海天盛筵的豪气了。
  3. select 只能监视1024个链接,这个跟草榴没啥关系哦,linux 定义在头文件中的,参见FD_SETSIZE。
  4. select 不是线程安全的,如果你把一个sock加入到select, 然后突然另外一个线程发现,尼玛,这个sock不用,要收回。对不起,这个select 不支持的,如果你丧心病狂的竟然关掉这个sock, select的标准行为是。。呃。。不可预测的, 这个可是写在文档中的哦. “If a file descriptor being monitored by select() is closed in another thread, the result is unspecified” 霸不霸气。

于是14年以后(1997年)一帮人又实现了poll, poll 修复了select的很多问题,比如poll 去掉了1024个链接的限制,于是要多少链接呢, 主人你开心就好。 poll 从设计上来说,不再修改传入数组,不过这个要看你的平台了,所以行走江湖,还是小心为妙。其实拖14年那么久也不是效率问题, 而是那个时代的硬件实在太弱,一台服务器处理1千多个链接简直就是神一样的存在了,select很长段时间已经满足需求。但是poll仍然不是线程安全的, 这就意味着,不管服务器有多强悍,你也只能在一个线程里面处理一组I/O流。你当然可以那多进程来配合了,不过然后你就有了多进程的各种问题。于是5年以后, 在2002, 大神 Davide Libenzi 实现了epoll.epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如:epoll 现在是线程安全的。 epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。

/*
*  maxfd1: 文件描述符的最大值 + 1
*  readfds, writefds, exceptfds: 指向描述符集合的指针
*  time:1 一直等待,2 不等待,3 等待指定的时间
*/
int select( 
        int maxfd1, 
        fd_set *readfds, 
        fd_set *writefds, 
        fd_set *exceptfds, 
        struct timeval *tvptr
    );

使用过程

fd_set set;
FD_ZERO(&set);  
FD_SET(5, &set);    
FD_SET(0, &set);
FD_SET(1, &set);
//阻塞等待 
ret = select((5+1),&set,0,0,0); 
// 如果 fds 发生可读事件则 select 返回一个正值

if(ret < 0)
    printf("select");
else if(ret == 0)
    printf("timeout");
else
    printf("data ready");

select 的返回值

  • 返回值 -1 表示出错,比如捕获到一个信号
  • 返回值 0 表示没有描述符准备好,且指定时间已到
  • 返回值 > 0 表示已经准备好的描述符数目

在这里插入图片描述

select 的几个缺点:

  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符数量太小了,默认是1024

poll

int poll(struct pollef fdarray[],unsigned long nfds,int timeout);

struct pollfd
{
    int fd;
    short events;
    shrot revents;
}

和 select 不同的是,poll 不是为每一个条件构造一个描述符集,而是构造一个 pollfd 结构数组,每个数组元素制定一个描述符编号以及对其关心的条件,并且 poll 没有限制可以监视的最大的fd的个数。

events 告诉内核我们对该 fd 所关心的事件,返回时,内核设置 revents,说明对于该描述符已经发生了什么事件。events 是 immutable 的,与 select 不同。

描述符是否阻塞,都不影响 select 和 poll 是否阻塞。

select 和 poll 可以实现异步形式的通知,关于描述符的状态,内核并不惠东告诉我们任何信息,需要进行查询(调用 select 或 poll)。

epoll

epoll 既然是对 select 和 poll 的改进,就应该能避免上述的三个缺点。那 epoll 都是怎么解决的呢?在此之前,我们先看一下epoll 和 select 和 poll 的调用接口上的不同。

epoll提供了三个函数,epoll_create, epoll_ctl 和 epoll_wait,epoll_create 是创建一个 epoll 句柄;epoll_ctl 是注册要监听的事件类型;epoll_wait 则是等待事件的产生。

  1. 对于第一个缺点(内存拷贝),epoll的解决方案在 epoll_ctl 函数中。每次注册新的事件到 epoll 句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的 fd 拷贝进内核,而不是在 epoll_wait 的时候重复拷贝。epoll 保证了每个 fd 在整个过程中只会拷贝一次。
  2. 对于第二个缺点(遍历),epoll 的解决方案不像 select 或 poll 一样每次都把 current 轮流加入 fd 对应的设备等待队列中,而只在 epoll_ctl 时把 current 挂一遍(这一遍必不可少)并为每个 fd 指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表。epoll_wait 的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout() 实现睡一会,判断一会的效果,和 select 实现中的第7步是类似的)。
  3. 对于第三个缺点,epoll 没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于 2048,举个例子, 在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

总结

  1. select,poll 实现需要自己不断轮询所有 fd 集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而 epoll 其实也需要调用 epoll_wait 不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在 epoll_wait 中进入睡眠的进程。虽然都要睡眠和交替,但是 select 和 poll 在“醒着”的时候要遍历整个fd集合,而 epoll 在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
  2. select,poll 每次调用都要把 fd 集合从用户态往内核态拷贝一次,并且要把 current 往设备等待队列中挂一次,而 epoll 只要一次拷贝,而且把 current 往等待队列上挂也只挂一次(在 epoll_wait 的开始,注意这里的等待队列并不是设备等待队列,只是一个 epoll 内部定义的等待队列)。这也能节省不少的开销。

猜你喜欢

转载自blog.csdn.net/antony1776/article/details/84345556