后台开发核心技术(10):网络IO模型

前言

IO有很多种,磁盘IO、外设IO、网络IO等,都有着同步和异步两种操作,今天主要说一下网络IO模型。看到有关网络IO、同步异步的很多博客,各有不同的理解,今天我说一下自己的理解,如有不对,请大佬们多多指点!
首先看一下网络IO种的四个概念:阻塞非阻塞同步异步

我认为阻塞与同步的概念不是一个概念,不意味着非阻塞就是同步,阻塞就是异步。整个网络IO发生过程中会经历两个阶段,数据准备数据拷贝,弄清楚其中的过程,理解TCP是如何将数据送来的就可能有很大帮助。

数据准备:
TCP将数据(拿TCP作为例子)送到接收方套接字的缓冲区,这是一个数据准备的过程,这个过程经常在我们进行套接字编程的时候用函数recv() 表现出来,recv去调用系统调用recvfrom() 等待发送方的数据到来或者等待一个完整的数据到来,这时候recv() 就是阻塞的,这也是数据准备的过程。

数据拷贝:
数据到达套接字接收缓冲区后,我们的应用程序要去去读这个数据,并不能直接读取缓冲区中的数据,而是从缓冲区中经过系统调用拷贝到应用程序的内存空间中,这一阶段叫做数据拷贝阶段。

总结:
阻塞发生在第一阶段:阻塞和非阻塞的概念是用户线程调用内核IO的操作方式:阻塞是指IO操作彻底完成后才返回到用户空间;非阻塞是指IO操作被调用后立刻返回给用户一个状态值,不需要等待;
同步异步是指第二阶段:线程是否是同步,取决于完成从数据拷贝是否由用户线程去完成,比如我的应用A去自己拷贝缓冲区中的数据,那么我花费的是A程序自己的时间,这就是同步的,如果我只是将我的请求和发生请求以后的通知方式告诉系统调用,然后就可以去做其他事情了,等到事件发生的时候,系统调用会按照约定的通知去通知A,这就是异步。

异步的实现:
muduo作者陈硕——在处理IO的时候,阻塞和非阻塞都是同步IO。只有在使用了特殊的API的时候才是异步IO。
是否支持异步,取决于操作系统提供的API,比如linux内核从2.6版本开始,引入了支持异步响应的IO操作,如aio_read、aio_write,就是异步接口。(具体实现是进程A向异步接口提供一个参数,异步接口直接将缓存区的数据存储到进程A的内存中,不用A花费时间去拷贝)。

阻塞IO模型

在Linux中,默认情况下的所有套接字都是阻塞的,一个典型的读写操作如下图:

在这里插入图片描述
阻塞IO模型的特点是在IO执行的两个阶段全都被阻塞了!
问题来了:
大部分的IO接口都是阻塞的,这就给网络编程带来了很大的问题,如在调用send()的同时,线程处于阻塞状态,在此期间,线程将无法执行任何运算或响应任何网络请求。
解决方法1:
使用多线程(或者多进程),让每个连接拥有独立的线程,但是如果要连接成千上百的服务请求,无论是多线程还是多进程都会严重占据系统资源,线程和进程本身也容易进入假死状态。
解决方法2:
线程池”、“连接池”。连接池是指维持连接的缓存池,尽量重用已有的连接,降低创建和关闭连接的频率;线程池旨在降低创建和销毁线程的频率,使其维持一定合理数量的线程,并让空闲的线程重新承担起新的任务。但是,线程池也只是在一定程度上缓解了频繁调用IO接口带来的资源占用,或许可以缓解上千或者上万次的客户端请求,但是终究有瓶颈,可以使用非阻塞解决这个问题,下面请看:

非阻塞IO模型

在linux下可以通过设置socket使IO模型变成非阻塞的状态。当对一个非阻塞IO执行read时,流程如下图:
在这里插入图片描述
可以看到,当用户进程发起read操作时,如果内核中的数据还没有准备好,会立刻返回一个错误而不会block用户进程。从用户角度讲,它发起一个read操作后,立刻得到一个结果。当结果是错误时,它就知道数据还没有准备好,于是再次发起read操作,一旦内核数据准备好了,又再次收到用户的系统调用,那么它就会将数据马上复制到内存中,然后返回正确值。
但是由于循环调用recv将大幅度占用CPU使用率,所以上面模型基本上不会被单独使用,但是并不代表这个模型一无是处,一个非常好的模型就需要用到——多路IO复用,就是配合非阻塞才能实现高性能的监听很多套接字!

多路复用IO

多路复用IO,有时也叫做事件驱动IO。它的基本原理就是有个函数(select)会不断地轮询所负责的socket,当某个socket有数据到达了,就会通知用户进程,多路复用IO的模型如下:
在这里插入图片描述
当用户调用了select,整个进程会被阻塞,同时内核会监视所有select所负责的socket,当任何一个socket数据准备好了,select就会返回,这个时候用户进程再去调用read操作将数据从内核拷贝进用户进程中。
这个模型与阻塞IO本质上一样 ,甚至更差,因为涉及到了两个系统调用,一个是select,一个是recvfrom,但是这个优势就在于能同时处理多个连接!
注意: 前面有提到,在多路复用的IO模型中,对于其中的每个socket,一般设置为非阻塞的!否则如果轮询到其中一个socket,被这个socket的内部阻塞住了,那么select怎么去轮询其他socket。

具体看一下实现:

//函数原型
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval* timeout);

使用select的关键地方是如何维护select()的三个参数,readfd、writefds和exceptfds。作为参数,readfds应该标记所有的需要检测的“可读事件”的句柄,同时,writefds和exceptfds应该标记所有要检测的“可写事件”和“错误事件”的句柄。(这里注册的事件类型比较固定,而epool对这种做了改进,随意注册自己想监听的事件,下一个文章重点总结一下epool这种多路复用模型)。

事件循环的模型:
在这里插入图片描述

异步IO模型

能否异步,取决于操作系统提供的支持异步的操作,比如Linux内核从2.6版开始引入了支持异步的IO操作,例如aio_read、aio_write,就是异步IO接口。
什么是异步?当用户进程发起一个read操作之后,立刻就开始去做其他事情;而另一方面,从内核的角度,当它收到一个异步的read请求操作之后,首先会立刻返回,所以不会对用户进程产生任何阻塞,然后内核会等待数据准备完成,然后内核将数据拷贝到用户内存中,当这一切都完成后,内核会按照约定好的方式、信号去通知用户进程,返回read操作已完成的信息。——所以事情用户进程不用花费时间去完成。
在这里插入图片描述

总结

之前所述的阻塞IO、非阻塞IO、以及多路复用IO都是同步IO,因为数据准备好以后,用户进程自己去拷贝,这时候对于用户进程来说是阻塞的,就是网络IO的第二阶段——数据拷贝阶段。举个真实的例子,recvfrom系统调用:非阻塞IO在执行recvfrom这个系统调用的时候,如果内核的数据没有准备好,这时候不会阻塞进程。但是当内核中的数据准备好了以后,recvfrom会将数据从内核拷贝到用户内存中,这时候进程被阻塞!而异步则不同,当发起了IO操作之后,就直接返回了,直到内核发送一个信号,告诉进程IO已完成,在整个过程中没有被阻塞。

其实还有一个小的点:不是说好异步与网络IO第一阶段数据准备阶段没有关系吗?为什么还要求第一阶段是非阻塞的,整个过程是非阻塞的呢?其实不要求第一阶段非阻塞,可以存在异步阻塞的I网络IO模型,但是现实中没人这么做,都是异步的IO了,还非阻塞等待着数据到来?这是纯属浪费资源了,线程完全可以做其他事情。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值