Linux的五种网络IO模型

阻塞IO(Blocking IO)

在linux中,默认情况下所有的socket都是blocking的,一个read操作大致的流程如下:
在这里插入图片描述

阻塞IO读操作流程图

  1. 当用户进程调用了recvfrom这个系统调用方法,kernel就开始了IO的第一个阶段:准备数据

  2. 此时Kernel会开始接收数据报文,此时整个用户进程处于阻塞状态

  3. 当Kernel接收到足够的数据报文后,就会将数据从Kernel中拷贝到用户进程的内存空间中,这个拷贝的过程用户进程依旧是阻塞状态

  4. 直到拷贝过程结束,Kernel会给用户进程返回结果,用户进程才会解除阻塞状态

阻塞IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据)都被阻塞了

由于所有Socket网络编程都是从listen()、send()、recv()等方法开始的,这些接口也都是阻塞型的,使用这些方法实现一个C/S模型非常容易,如下图所示

在这里插入图片描述

所有IO方法包括Socket方法都是阻塞型的,因此通常的改进方式都是采用多线程,多线程的目的是让每个连接都拥有独立的线程,这样任何一个连接的阻塞都不会影响其他的连接,也可以使用多进程(这种方式很少)

什么情况下使用多进程?什么情况下使用多线程?

  • 如果需要同时为较多客户端提供服务,则使用多线程,因为创建/销毁进程的开销要远大于创建/销毁线程

  • 如果单个服务执行体需要消耗较多的CPU资源,譬如需要进行大规模或长时间的数据运算或文件访问,则使用进程比较安全

在实际开发中,更多会选择线程池的方式来实现多线程IO,使用线程池的技术可以降低创建/销毁线程的频率,使其维持一定合理数量的线程,但是线程池是有上限的,一旦请求超过上限,也会带来各种问题。
在这里插入图片描述

非阻塞IO(Non-Blocking IO)

在Linux中,可以通过设置Socket使其变为Non-Blocking。对一个Non-Blocking Socket执行读操作时的执行流程如下:

非阻塞IO读操作流程图

  1. 当用户进程发出read操作时,如果kernel中的数据还没有获取完成,那么它并不会block用户进程,而是立即返回一个error,提示用户进程Kernel当前操作正在阻塞中

  2. 当用户进程收到返回的结果就会进行判断,如果是error就再次发送read操作。如果不是error就表示数据已经拷贝到了用户内存了

  3. 如果Kernel中的数据获取完成,并且再次收到用户进程的系统调用,那么它就会将数据拷贝到用户内存,然后返回操作成功

  4. 用户收到了操作成功的结果就会对数据报进行处理

非阻塞IO的特点就是用户进程需要不断的主动询问kernel数据准备的情况

非阻塞IO底层的实现逻辑:

使用fcntl(fd,F_SETFL,O_NONBLOCK);可以将某个句柄fd设为非阻塞状态

在这里插入图片描述

使用非阻塞的接收数据模型

recv()返回值大于0,表示接收数据完毕,返回值即接收到的字节数;

recv()返回值为0,表示连接已经正常断开;

recv()返回值为-1,且error等于EWOULDBLOCK,表示recv操作还没有执行完成

recv()返回值为-1,且error不等于EWOULDBLOCK,表示recv操作遇到系统错误

客户端线程不断发送系统调用的同时,kernel线程通过循环调用recv()确认是否接收数据成功,可以在单个线程内实现所有连接的数据接收工作

但是这个模型不推荐!!!!!

循环调用recv()将大幅度提高CPU占用率;

recv()只是用于检测操作是否完成,实际操作系统提供了更为高效的检测“操作是否完成”的方法,那就是select()多路复用模式,可以一次检测多个连接是否活跃

多路复用IO(IO multiplexing)

多路复用IO在有些地方也被称为事件驱动IO(event driven IO),了解多路复用IO,就必须要了解select/poll/epoll

select:是一种传统的多路复用IO机制,在多个文件描述符上进行事件的轮询检测。这个过程select会使用一个位图来表示文件描述符的状态,通过调用select函数来阻塞等待,当任何一个文件描述符的状态发生变化时,select函数返回,并告诉应用程序哪些文件描述符有事件发生。然后应用程序就会遍历文件描述符,进行相应的读写操作。

poll:poll是对select的改进,它允许程序监视一组文件描述符,当其中任何一个文件描述符准备好进行IO操作时,poll函数就会返回。相比于select,poll使用链表来保存文件描述符,性能上有所提升,但是仍然存在效率问题

epoll:是Linux特有的一种多路复用IO机制,相较于select具有更高的性能和扩展性。epoll提供了三个函数用于操作事件集合:epoll_create、epoll_ctl和epoll_wait。使用epoll首先要创建一个epoll实例,然后将需要监听的文件描述符注册到该实例中。通过epoll_wait函数来等待事件的发生,并返回就绪的文件描述符列表。与select不同,epoll使用回调的方式通知应用程序哪些文件描述符有事件发生,避免了遍历文件描述符的开销。

因此多路复用IO的实现原理就是:

用户进程发起系统调用select方法,此时select会不断轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程,然后使用recvfrom触发数据复制,当数据复制完成之后返回给用户进程操作成功
在这里插入图片描述

多路复用IO执行读操作流程

  1. 当用户进程调用了select,那么整个进程会被阻塞

  2. kernel会监视所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回给用户进程

  3. 用户进程再调用read操作,将数据从kernel拷贝到用户进程

这个流程和阻塞IO的图其实并没有太大的不同,事实上还更差一些

因为这里需要使用两个系统调用(select和recvfrom),而阻塞IO只调用了一个系统调用(recvfrom)。

但是使用select的优势在于它可以同时处理多个connection

因此我们得出一个结论:

如果处理的连接数不是很高的话,使用多路复用IO不一定比使用多线程+阻塞IO的性能更好,可能延迟还更大

多路复用IO的优势就是在于能处理更多的连接

另外提出一个疑问:从操作流程上明明用户进程一直被阻塞,为什么多路复用IO是非阻塞的?

在多路复用模型中,对于每一个socket,一般都是设置成为Non-Blocking,但是用户进程会被select函数block,而不是socket IO的阻塞,因此我们所描述的是非阻塞IO

选择select()作为事件驱动模型的探讨

事件驱动模型:每一个执行周期都会探测一次或一组事件,一个特定的事件会触发某个特定的响应,这种模型统一归类为事件驱动模型

相比其他模型,使用select()的事件驱动模型只用单线程执行,占用资源少,不消耗太多CPU,同时能为多客户端提供服务

缺点:

  1. select并不是实现事件驱动的最好的选择,因为当需要探测的Socket连接较多,所需要获取的文件描述符自然也会增加,那么就会增加文件描述符的遍历的时间复杂度,消耗大量的时间

很多操作系统都提供了更为高效的接口,比如Linux提供的epoll、BSD提供的kqueue、Solaris提供的dev/poll等,如果需要实现更高效的服务器程序,epoll会更加好用,但实际上

  1. 不同的操作系统提供的epoll接口有很大差异,跨平台能力较差。

  2. 其次该模型将事件探测和事件响应夹杂在一起,一旦事件响应的执行体庞大,对整个模型是灾难性的
    庞大的执行体对使用select()的事件驱动模型的影响
    庞大的执行体对使用select()的事件驱动模型的影响
    为了解决这种灾难性问题,很多高效的事件驱动库(libevent库、libev库)会根据操作系统的特点选择最合适的事件探测接口(select<->epoll),并加入了信号(signal)等技术来支持异步响应,这就是异步IO

信号驱动IO(signal driven IO)

首先我们允许Socket进行信号驱动IO,并安装一个信号处理函数,进程继续运行并不阻塞,当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用IO操作处理数据
在这里插入图片描述

信号驱动IO模型读操作流程

  1. 用户进程使用sigaction函数设置一个信号处理函数,该信号处理函数将在数据就绪时被调用

  2. 当数据报文准备完成,内核会调用信号处理函数,将信号SIGIO传递给用户进程

  3. 此时用户进程获取到SIGIO之后,系统调用recvfrom,开始数据的复制工作

  4. 复制成功后返回操作成功

信号驱动IO通过使用信号机制实现非阻塞IO操作,使得应用程序能够在数据就绪时立即得到通知,而不需要主动轮询或阻塞等待


为什么上述四种模型都是同步IO?

数据传输过程主要分为两个阶段:第一次发送select请求,询问数据状态是否准备好,第二次发送recvfrom请求读取数据

  • 在IO模型里面如果请求方从发起请求到数据最后完成的这一段过程中都需要自己参与,那么就是同步

  • 如果应用发送完指令后就不再参与过程,秩序等待最终完成结果的通知,那么就是异步

因此很明显以上的模型都需要recvfrom的介入,不管是通过recvfrom轮询,还是通过select轮询,亦或是使用信号值检测,都只是解决了阻塞的问题,因此非阻塞IO、多路复用IO、信号驱动IO都属于非阻塞IO

但是我们也发现,以上四种模型在第一阶段结束后,都需要用户进行拿到数据是否就绪的状态进行第二步的操作recvfrom并产生阻塞,因此以上四种都是同步IO。


异步IO(Asynchronous IO)

用户进程发起一个Read操作请求,通知内核后会立即返回,内核收到请求后会建立一个信号联系,当数据准备就绪,内核就会主动把数据从内核复制到用户空间,等待所有操作都完成之后,内核会发起一个通知给用户进程

在这里插入图片描述

一个Read操作的IO流程(网络层面)

用户进程与内核之间的实际交互流程如下:
在这里插入图片描述

  1. 用户进程发起read操作之后,kernel会立即创建一个信号值,并返回

  2. kernel等待数据准备完成,然后将数据拷贝到用户内存

  3. 拷贝完成后,kernel会将信号值signal发送给用户进程,告诉用户进程read操作完成了

五种模型的流程总结

在这里插入图片描述

  • 15
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Carl·杰尼龟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值