Linux系统的IO模型与select/poll/epoll的工作原理
3、Linux内核中的select/poll/epoll工作原理
1、Unix中的IO模型
1.1、进程中的IO调用步骤
大致可以分为以下四步:
(1)进程向操作系统请求数据;
(2)操作系统把外部数据加载到内核缓冲区;
(3)操作系统把内核缓冲区数据拷贝到进程缓冲区;
(4)进程获得数据完成自己的功能。
也可以精简为两个过程:
(1)数据准备阶段;
(2)内核空间复制回用户进程缓冲区空间。
1.2、IO模型
IO过程主要分两个阶段:
- 数据准备阶段
- 内核空间复制回用户进程缓冲区空间
无论阻塞式 IO 还是非阻塞式 IO ,都是同步 IO 模型,区别就在与第一步是否完成后才返回,但第二步都需要当前进程去完成。
异步 IO 就是从第一步开始就返回,直到第二步完成后才会返回一个消息,也就是说,非阻塞能够让你在第一步时去做其它的事情,而真正的异步 IO 能让你第二步的过程也能去做其它事情。
以 epoll 为例,在 epoll 开发的服务器模型中,epoll_wait()
这个函数会阻塞等待就绪的 fd ,将就绪的 fd 拷贝到 epoll_events 集合这个过程中也不能做其它事(虽然这段时间很短,所以 epoll 配合非阻塞 IO 是很高效也是很普遍的服务器开发模式--同步非阻塞IO模型)。有人把 epoll 这种方式叫做同步非阻塞(NIO),因为用户线程需要不停地轮询,自己读取数据,看上去好像只有一个线程在做事情。也有人把这种方式叫做异步非阻塞(AIO),因为毕竟是内核线程负责扫描 fd 列表,并填充事件链表。
2、Unix中的I/O分类
2.1、阻塞I/O
阻塞IO的调用模型如下图所示:
使用revfrom产生系统调用,在等到数据返回前不执行其他操作。阻塞 I/O 是 socket 的默认设置,程序调用 recvfrom 产生一个系统调用,kernel 收到该调用请求后有两个步骤,第一是等待数据准备好,第二是将数据从内核空间拷贝到用户空间然后返回 OK ,用户空间收到系统调用返回后才会继续程序流的执行。
2.2、非阻塞I/O
非阻塞I/O的调用模型如下图所示:
调用后立即返回,设置描述符为非阻塞,进程自己一直检查是否可读。Socket 使用非阻塞 IO 模型需要对 Socket 进行另行设置。内核收到系统调用后,若数据未准备好立即返回 error ,用户进程收到 error 会继续产生系统调用,直到数据准备好了并被拷贝到用户空间。
2.3、I/O复用
IO复用调用模型如下图所示:
相比于非阻塞 I/O ,具有更多的描述符,有一定异步的感觉,但是检查是否可读时需要阻塞。select/poll/epoll
对应的是 IO 复用模型,优势是能够监听多个 socket 。用户进程调用 select() 产生系统调用,kernel 会监听所有 select 负责的 socket ,一旦有一个 socket 数据准备好了,kernel 即返回,用户再去 recvfrom 产生系统调用将数据从内核空间读到用户空间。
2.4、信号驱动
信号驱动型的IO调用模型如下图所示:
信号驱动IO模型采用信号机制等待,不用监视描述符,而且不用阻塞着等待数据到来,被动等待信号通知。用户程序注册一个信号 handler ,然后继续做其他事情。当内核数据准备好了会发送一个信号,程序调用 recvfrom 进行系统调用将数据从内核空间拷贝到用户空间。
2.5、异步I/O
异步I/O模型如下图所示:
异步I/O模型是完全异步的。aio_read 产生系统调用,kernel 在数据准备好后将数据从内核空间拷贝到用户空间后返回一个信号告知 read 数据成功,整个过程程序调用 aio_read 后就继续执行其他部分直到收到信号,调用 handler 处理。
2.6 小结
上述5中I/O调用模型的对比如下图所示:
严格意义上的异步,没有任何阻塞。而前四种I/O,都有不同程度上的阻塞,而且都有一个共同的阻塞:内核拷贝数据到进程空间时需要等待。
3、Linux内核中的select/poll/epoll工作原理
3.1、综述
select/poll/epoll
都是 I/O 多路复用的机制。I/O 多路复用可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select/poll/epoll
本质上都是同步 I/O ,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责把数据从内核拷贝到用户空间。
3.2、select
首先创建事件的描述符集合。对于一个描述符,可以关注其上的读事件、写事件以及异常事件,所以要创建三类事件的描述符集合,分别用来收集读事件描述符、写事件描述符以及异常事件描述符。select 调用时,首先将时间描述符集合 fd_set 从用户空间拷贝到内核空间;注册回调函数并遍历所有 fd ,调用其 poll 方法, poll 方法返回时会返回一个描述读写操作是否就绪的 mask 掩码,根据这个掩码给 fd 赋值,如果遍历完所有 fd 后依旧没有一个可以读写就绪的 mask 掩码,则会使进程睡眠;如果已过超时时间还是未被唤醒,则调用 select 的进程会被唤醒并获得 CPU ,重新遍历 fd 判断是否有就绪的fd;最后将 fd_set从内核空间拷贝回用户空间。
select缺点:
- 每次调用 select ,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大
- 同时每次调用 select 都需要在内核遍历传递进来的所有 fd ,这个开销在 fd 很多时也很大
- select支持的文件描述符数量较小,默认是1024
3.3、poll
poll 是 select 的优化版。poll 使用 pollfd 结构而不是 select 的 fd_set 结构。select 需要为读事件、写事件和异常事件分别创建一个描述符集合,轮询时需要分别轮询这三个集合。而 poll 库只需要创建一个集合,在每个描述符对应的结构上分别设置读事件、写事件或者异常事件,最后轮询时可同时检查这三类事件是否发生。
3.4、epoll
select 与 poll 中,都创建一个待处理事件列表。然后把这个列表发送给内核,返回的时候再去轮询这个列表,以判断事件是否发生。在描述符比较多的时候,效率极低。epoll 将文件描述符列表的管理交给内核负责,每次注册新的事件时,将 fd 拷贝进内核,epoll 保证 fd 在整个过程中仅被拷贝一次,避免了反复拷贝重复 fd 的巨大开销。此外,一旦某个事件发生时,内核就把发生事件的描述符列表通知进程,避免对所有描述符列表进行轮询。最后, epoll 没有文件描述符的限制,fd 上限是系统可以打开的最大文件数量,通常远远大于2048 。
3.5、select/poll/epoll的对比
select,poll实现需要自己不断轮询所有 fd 集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而 epoll 其实也需要调用 epoll_wait 不断轮询就绪链表,期间也可能多次睡眠和唤醒交替。但是它在设备就绪时,调用回调函数,把就绪 fd 放入就绪链表中,并唤醒在 epoll_wait 中进入睡眠的进程。虽然都要睡眠和交替,但是 select 和 poll 在醒着的时候要遍历整个 fd 集合,而 epoll 在醒着的时候只要判断一下就绪链表是否为空就行了,这节省了大量的 CPU 时间。
3.6、select/poll/epoll各自的应用场景
(1)select
timeout 参数精度为 1ns,而 poll 和 epoll 为 1ms,因此 select 更加适用于实时性要求比较高的场景,比如核反应堆的控制。select 可移植性更好,几乎被所有主流平台所支持。
(2)poll
poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。
(3)epoll
只运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接;需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势;需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且 epoll 的描述符存储在内核,不容易调试。
声明:本文部分内容整理来源于网络,仅做个人学习使用!侵删~
本文部分内容参考链接:
参考链接:https://blog.nowcoder.net/n/91bd1ed23c474db497de9246f14509a2