对于网络I/O模型的学习,在操作系统中是非常重要的一环,因为I/O也同样是我们系统设计中至关重要的一个方面和要考虑的因素,因此想利用一篇文章来解析一下,就目前而言,业界对五种网络I/O模型的分类,主要分为以下:
- 阻塞IO模型
- 非阻塞IO模型
- 多路复用IO模型
- 信号驱动IO模型
- 异步IO模型
根据自身学习和一些资料的参考,作出如下图示内容:
1 概念简述
根据图示的这些分类,我们可以对比一下概念:
(1)同步和异步的概念描述的是用户线程与内核的交互方式:
- 同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;
- 异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程,或者调用用户线程注册的回调函数。
(2)阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式:
- 阻塞是指IO操作需要彻底完成后才返回到用户空间;
- 非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。
(3)多路复用I/O
- I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
(4)事件驱动I/O
- 事件驱动I/O本质上就是将I/O操作转化为程序需要处理的事件,减少了等待机制,而是利用事件触发的机制,增加了效率。
2 socket-同步阻塞I/O
socket 是进程间通信里是可以跨主机间通信。对于Socket的编程过程我们也比较熟悉:
(1)服务端调用 socket() 函数
(2)然后服务端调用 bind() 函数,给 Socket 绑定一个 IP 地址和端口
(3)绑定完 IP 地址和端口后,调用 listen() 函数监听
(4)服务端进入了监听状态后,通过调用 accept() 函数,来从内核获取客户端的连接,如果没有客户端连接,则会阻塞等待客户端连接的到来
(5)客户端在创建好 Socket 后,调用 connect() 函数发起连接,该函数的参数要指明服务端的 IP 地址和端口号
(6)然后 TCP 三次握手
(7)连接建立后,客户端和服务端就开始相互传输数据了,双方都可以通过 read() 和 write() 函数来读写数据
多进程模型:
多线程引入:
综上,以上的Socket不管是单进程模式,还是多进程或多线程模式,他们的原理都是阻塞的,也就是下一个客户端进程的处理都需要等待当然客户端请求被处理完成,同理,服务端也会一直处于监听的状态,直到接受请求时才会去处理。
3 select-同步阻塞&多路复用I/O
select 实现多路复用的方式是:将已连接的 Socket 都放到一个文件描述符集合,然后调用 select 函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,检查的方式很粗暴,就是通过遍历文件描述符集合的方式,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后再对其处理。
select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,默认最大值为 1024,只能监听 0~1023 的文件描述符。
如图,对于 select 这种方式,需要进行 2 次遍历文件描述符集合,一次是在内核态里,一个次是在用户态里 ,而且还会发生 2 次拷贝文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中。
4 poll-同步阻塞&加强版多路复用I/O
poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。
但是 poll 和 select 并没有太大的本质区别,都是使用线性结构存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,这种方式随着并发数上来,性能的损耗会呈指数级增长。
5 epoll-同步阻塞&加强版多路复用&事件驱动I/O
epoll 相比 select/poll增强了这两个方面:
- 第一点,epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过 epoll_ctl() 函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删查一般时间复杂度是 O(logn),通过对这棵黑红树进行操作,这样就不需要像 select/poll 每次操作时都传入整个 socket 集合,只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。
- 第二点, epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。
epoll 的方式即使监听的 Socket 数量越多的时候,效率不会大幅度降低,能够同时监听的 Socket 的数目也非常的多了,上限就为系统定义的进程打开的最大文件描述符个数。因而,epoll 被称为解决 C10K 问题的利器。
Tips:
epoll 是解决 C10K 问题的利器,通过两个方面解决了 select/poll 的问题:
(1)epoll 在内核里使用「红黑树」来关注进程所有待检测的 Socket,红黑树是个高效的数据结构,增删查一般时间复杂度是 O(logn),通过对这棵黑红树的管理,不需要像 select/poll 在每次操作时都传入整个 Socket 集合,减少了内核和用户空间大量的数据拷贝和内存分配。
(2)epoll 使用事件驱动的机制,内核里维护了一个「链表」来记录就绪事件,只将有事件发生的 Socket 集合传递给应用程序,不需要像 select/poll 那样轮询扫描整个集合(包含有和无事件的 Socket ),大大提高了检测的效率。
6 异步I/O
异步I/O目前仅有概念的存在。
异步IO的概念和同步IO相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。在一个CPU密集型的应用中,有一些需要处理的数据可能放在磁盘上。预先知道这些数 据的位置,所以预先发起异步IO读请求。等到真正需要用到这些数据的时候,再等待异步IO完成。使用了异步IO,在发起IO请求到实际使用数据这段时间内,程序还可以继续做其他事情
7 总结
通过对网络I/O模型的学习,从阻塞到非阻塞,从同步到异步再到事件驱动,可以说效率是大大提高的,并且从网络延伸到放放面面,比如Spring框架的Reactor机制,Kafka消息队列等等。
今天的文章就到这里,bye~
参考文章:
https://baike.baidu.com/item/%E5%BC%82%E6%AD%A5IO/6018433?fr=aladdin