linux下的 I/O 模型,同步/异步,阻塞/非阻塞介绍

同步/异步,阻塞/非阻塞

一提到网络编程中的 I/O 模型,总会涉及到这几个概念,但是这几个名词又容易混淆,于是我想总结一下。

我们先看一下在《UNIX网络编程:卷一》中讲到的5中 UNIX 下的 I/O 模型,分别是

  • 阻塞式 I/O
  • 非阻塞式 I/O
  • I/O 复用
  • 信号驱动式 I/O
  • 异步 I/O

五种 I/O 模型

阻塞式 I/O 模型

默认情况下,所有的套接字都是阻塞的。用户进程执行系统调用 recvfrom 时,会导致应用进程阻塞,什么也不干,直到内核把数据准备好,并且将数据从内核复制到用户进程,最后用户进程再处理数据。在等待数据到处理数据的两个阶段,整个进程都被阻塞,不能做别的事情,此进程不再拥有 cpu 时间片,只是简单的等待响应的状态,因此从处理的角度来看,这是非常有效的。
这里写图片描述
当用户进程调用 recvfrom/recv 时,内核就开始了 IO 的第一个阶段,准备数据(对于网络IO 来说,很多时候数据在一开始还没有到达。比如还没有收到一个完整的 UDP 包,这时内核就需要等待数据到来)。数据被拷贝到内核的缓冲区是需要一个过程的。在用户进程这里,进程会被阻塞。第二个阶段,当内核将数据准备好了,会将数据从内核拷贝到用户进程然后内核返回结果,用户进程解除阻塞状态。即阻塞式 IO 在 IO 执行阶段的两个过程中都被阻塞了。

优点:能够及时返回数据,无延时。
缺点:需要用户进程一直处于等待状态。

非阻塞式 IO

进程把一个套接字设置为非阻塞时,通知内核当所请求的 IO 操作非得把本进程投入睡眠状态才能完成时,不要把进程投入睡眠,而是返回一个错误。也就是说非阻塞的 recvfrom 被调用后,进程不会被阻塞,内核马上返回给进程,如果数据还没有准备好,会返回一个error。进程在返回之后,可以做一些其他事情,然后再调用 recvfrom 。重复上述过程,循环的调用 recvfrom。这个过程通常被称为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。这里需要注意一点,在拷贝数据的过程中,进程仍然处于阻塞状态。以下为非阻塞套接字调用 recvfrom 时的操作
这里写图片描述
优点:可以在等待任务完成的时间内做其它事了。
缺点:任务完成的响应延迟增大了,因为没过一段时间才会去检查一次,而任务可能在两次查询之间的时刻内完成。

IO 复用

非阻塞式 IO 需要不断主动轮询,轮询占据了很大一部分过程,会消耗大量 cpu 资源,如果内核可以帮我们循环查询多个任务的完成状态,有要有任何一个任务完成,会告诉进程就好了。这就是 IO 多路复用,对应的有 select,poll,epoll 这些函数。select 轮询相对于非阻塞式的轮询的区别在于,select 可以等待多个 socket,能同时对多个 IO 端口进行监听,当其中任何一个 socket 的数据准备好了,都可以知道,然后进程再进行 recvfrom 调用,将内核数据拷贝到用户进程,当然这个过程是阻塞的。

IO 复用的好处就在于单个进程/线程就可以同时处理多个网络连接的 IO。它的基本原理就是不断的轮询所负责的所有 socket,当某个 socket 有数据到达时,就通知用户进程。当用户进程调用了 select,那么整个进程会被阻塞,同时内核会监视所有 select 负责的socket,select 会在指定的时间内(由参数设定)返回准备好的 socket 的个数,这个时候用户进程再调用 read 操作,数据从内核拷贝到用户进程。
这里写图片描述
上图和阻塞式 IO 的图其实并没有太大的不同,事实上,要差一些。因为这里需要使用两个系统调用,而阻塞式 IO 只使用了一个,select 的优势在于可以同时处理多个连接。

在 IO 多路复用中,对于每一个 socket,一般设置为非阻塞的,但是,如上图所示,用户进程其实是一直被阻塞的,只不过是阻塞在 select 上,而不是被 socket IO 给阻塞。

信号驱动式 IO

先注册一个信号处理函数,进程继续运行并不阻塞,当数据准备好时,进程会收到一个SIGIO 信号,可以在信号处理函数中调用 IO 操作函数处理数据。

这里写图片描述

异步 IO

调用类似于 aio_read 等函数之后,无论内核是否将数据准备好,此类函数都会直接返回。然后用户态进程可以去做其他事情。等到 soecket 数据准备好了,内核直接复制数据给进程。然后从内核向进程发送通知。
这里写图片描述
当用户进程调用 aio_read 之后,立刻就可以去做其他事情,从内核来看,当它收到一个异步调用之后,它会立刻返回,所以不会阻塞用户进程。然后,内核会等待数据准备完成,然后将数据拷贝到用户进程。然后内核会给用户进程发送一个信号,或执行一个回调函数来完成这次的 IO 处理过程,告诉用户进程操作完成。

同步与异步

同步和异步这两个概念关注的消息的通知机制。

  • 同步,就是请求一个‘调用’时,在没有得到结果之前,该调用不返回,但是该调用一旦返回,就可以得到调用结果。即‘调用者’主动等待‘调用’结果。
  • 异步,是在发出一个‘调用’后,这个调用就直接返回了,没有返回结果。即一个异步过程调用发出后,调用者不会立刻得到结果,而是在‘调用’发出后’,由‘被调用者’通过状态,通知来通知调用者,或者通过回调函数来处理这个调用。

举一个例子,我打电话给书店老板问他有没有 《c++ primer》这本书,我需要购买一本,老板可能会说(同步方式),不好意思麻烦你等稍等一会,我查一下,等他查好了(可能是几分钟,也可能几个小时)告诉你结果(返回结果)。而老板也有可能会说(异步方式),不好意思,我需要查一下,查完了我打会电话通知你,然后老板挂掉了电话(不返回结果)。等老板查好后,他会给你打电话通知你结果。

阻塞和非阻塞

阻塞和非阻塞关注的是进程在等待调用结果时的状态。

  • 阻塞,是指在调用结果返回之前,当前进程被挂起,调用进程只有在得到结果后才会返回。
  • 非阻塞,是指在不能立即得到调用结果时,该调用不会阻塞当前进程。

继续上面的例子,当我打电话询问书店老板有没有 《c++ primer》这本书时,我可能会一直在电话前等待(阻塞),直到获取书店里到底有没有这本书的结果,我才去做其他事情。我也可能会不管老板有没有给我回复,就去一边玩去了,当然我还惦记着买书这件事,时不时检查一下老板有没有给我结果。
需要注意的是这里阻塞与非阻塞与是否同步,异步无关,跟老板以何种方式通知我无关。

总结

在了解了五种 IO 模型,同步,异步,阻塞,非阻塞的概念之后,可以得知,同步和异步是站在‘被调用者’的角度出发的,描述的是被调用者的通知方式,是等待我完成一件事后才返回给发起调用者结果(同步),还是先告诉发起调用者我完成后会用哪种方式通知你(异步)。而阻塞是在发起调用者的角度出发的,描述的是发起调用者在等待结果时的状态。是一直等待被调用事件的完成,还是在等待期间可以去做其它事情。以上就是我在学习这些知识的过程中的理解,如果有误,还请大家指出。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值