linux系统编程-5种I/O模型

本文记录我对linux5种I/O模型的学习。参考了如下的链接,作者的原文写的非常赞,建议直接看作者的原文。

作者:陶邦仁
链接:http://www.jianshu.com/p/486b0965c296
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

概述

1 . 网络I/O的本质是socket读取,socket在linux系统被抽象为流,IO可以理解为对流的操作

2 . 对于一次IO访问(以read举例),会经过如下的两个过程:

第一阶段:等待数据准备 (Waiting for the data to be ready)。
第二阶段:将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)。
既然强调了I/O模型的两个过程,所以下面对于每一种模型的介绍,也主要是围绕这两个过程去说。

3 . 五种I/O模型如下:

  • 同步模型(synchronous IO)
    • 阻塞I/O(bloking IO)
    • 非阻塞(non-blocking IO)
    • 多路复用(multiplexing IO)
    • 信号驱动(signal-driven IO)
  • 异步模型(asynchronous IO)

同步阻塞 IO(blocking IO)

在这个IO模型中,

  • 用户空间的应用程序执行一个系统调用(recvform),这会导致应用程序阻塞。
  • 应用程序阻塞什么也不干,直到内核将数据准备好,并且将数据从内核复制到用户进程,最后进程再处理数据。

在等待数据到处理数据的两个阶段,整个进程都被阻塞。不能处理别的网络IO。
用户程序发出阻塞调用之后,释放cpu资源,等待数据准备完毕被唤醒,进行处理。所以,在进行等待的时候并没有占用cpu的资源。这是一种比较有效的方式。

这里写图片描述

详细分析:
当用户进程调用了recv()/recvfrom()这个系统调用,kernel就开始了IO的第一个阶段:准备数据。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。
第二个阶段:当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

优点:能够及时返回数据,无延迟;并且等待的时候其实是释放cpu的。
缺点:对用户来说处于等待就要付出性能的代价了;

非阻塞I/O(nonblocking IO)

同步非阻塞就是 “每隔一会儿瞄一眼进度条” 的轮询(polling)方式。

在这种模型中,设备是以非阻塞的形式打开的。这意味着 IO 操作不会立即完成,read 操作可能会返回一个错误代码,说明这个命令不能立即满足(EAGAIN 或 EWOULDBLOCK)。

  • 非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。
    轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。
while( EWOULDBLOCK == recvfrom(socket, ...) ){ // wait for the data to be ready
/* code to do other things */
}
// handle the socket
/* code to handle the socket */

这里写图片描述

详细分析:
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以先去做点其他的事情,然后再次发送read操作。

一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。

所以,nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。

优点:能够在等待任务完成的时间里干其他活了
缺点::任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。poling这种方式本生就会占用大量的cpu资源。做无用功。

分析nonblocking I/O的两个阶段,发现第一个阶段用户程序是不阻塞的,但是第二个阶段任然是阻塞的。所以,从这个细节中我们也可以看出阻塞与非阻塞的区别暂时是在第一个阶段。

I/O多路复用( IO multiplexing)

由于同步非阻塞方式需要不断主动轮询,轮询占据了很大一部分过程,轮询会消耗大量的CPU时间。并且对于一个process而言,只能轮询一个socket,因为从上面的代码我们看出它是while去轮询的,所以没法轮询多个。所以,对于同步阻塞而言,

  • 用户进程要编写轮询的代码,即轮询的任务在用户态。
  • 并且没法进行多任务处理。
    鉴于以上原因,有了I/O multiplexing.

I/O多路复用用三个特别的系统调用select, poll, epoll.以select为例进行说明。

首先,select调用是内核级别的调用。
其次,select轮询相对非阻塞的轮询的区别在于—前者可以等待多个socket,能实现同时对多个IO端口进行监听,当其中任何一个socket的数据准好了,就能返回进行可读。然后进程再进行recvform系统调用,将数据由内核拷贝到用户进程。

从上面的分析我们知道,对比同步非阻塞,多路复用是把轮询的这件事交给内核了,让内核去干。并且内核提供了对于过个socket的轮询,实现就是循环查询多个任务的完成状态,只要有一个任务完成,就去处理它。所以,多路复用也被成为event-driven驱动模型。事件就相当于某个socket数据准备完毕,就触发这个socket的读事件。代表这个socket数据可读。

I/O复用模型会用到select、poll、epoll函数,这几个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作。而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时(注意不是全部数据可读或可写),才真正调用I/O操作函数。

既然select函数也会让用户程序阻塞,那么它为什么是同步非阻塞模型呢?

这是个很重要的问题,要注意区别!!!我们说了,多路复用本质上还是进行轮询,它和同步非阻塞的区别在于,前者是内核去轮询,并且可以轮询多个。如果它要是同步阻塞模型,还有轮询什么事?这个问题的本质在于,I/O是非阻塞的,select是阻塞的我们看I/O的第一个阶段,正是因为非阻塞,内核才可以进行轮询,判断socket是否可读可写。所以,I/O是非阻塞的。但是,select是阻塞。是select把用户程序挂起来了,不是I/O操作。这点特别强调一下。
这里写图片描述

select调用的过程:

  • 当用户进程调用了select,那么整个进程会被block。
  • 而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。
  • 这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。

select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。

在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。所以IO多路复用是阻塞在select,epoll这样的系统调用之上,而没有阻塞在真正的I/O系统调用如recvfrom之上。

在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。(复用的由来)

从整个IO过程来看,多路复用模型他们都是顺序执行的,因此可以归为同步模型(synchronous)。都是进程主动等待且向内核检查状态。同步是需要主动等待消息通知,而异步则是被动接收消息通知,通过回调、通知、状态等方式来被动获取消息。IO多路复用在阻塞到select阶段时,用户进程是主动等待并调用select函数获取数据就绪状态消息,并且其进程状态为阻塞。所以,把IO多路复用归为同步阻塞模式。


那么问题来了,既然现在认为select是同步非阻塞模型,那么socket此时的阻塞非阻塞又有什么影响?
先说我之前的理解,我认为是同步非阻塞,所以,socket必须设置成non-blocking.但是,真相并不是这样。还是没有仔细分析上面的图。

参考了[select与阻塞和非阻塞]有新的收货。

多路复用的时候,由于程序是阻塞在select调用上,此时由内核负责轮询fd。(此处,我期初认为,如果你不把fd设成non-blocking,内核怎么轮询,不设置内核一轮询不就阻塞了,还谈什么轮询?但是,现在看这里理解是不对的!不管你的fd如何设置,内核在此处对于fd都看做非阻塞,从而进行轮询,因为内核的轮询并不是I/O,fd属性的设置只有对于I/O才有效。)

假设监听的是读事件,select调用结束之后,此时读事件触发。假设你代码写的是read(fd, buf, 10)。即要读取10个字节。但是,读事件触发之后,只有8个字节。
那么,如果fd被设置为non-blocking,read调用会返回。显然,还需要下次再调用才能读取完毕。
但是,如果是blocking模式,此时会阻塞,直到读取完所有数据。相当于执行read的两个io阶段,select只是起了部分缓冲的作用
注意,对于non-blocking,一定要再次select才能获取完所有数据。

上面的理解存在一定问题,但是基本是这个意思,正解如下
参考了两个链接:
[在使用Multiplexed I/O的情况下,还有必要使用Non Blocking I/O么 ?]
[为什么 IO 多路复用要搭配非阻塞 IO?]

原因有两个:

  • select在man 手册中提到:There may be other circumstances in which a file descriptor is spuriously reported as ready. Thus it may be safer to use O_NONBLOCK on sockets that should not block.举的例子是,数据收到之后会报告ready,但是,随后检验和发现数据错误,丢弃。那么此时read,会导致进程挂起
  • 上面读的例子不太合理,因为read(STDIN_FILENO, buf, 128),即使只有hello可以读取,read也会返回,哪怕在阻塞的情况下。所以,用读举例子不合适,考虑写,缓冲区有32 byte,但是有64byte发送。select之后,肯定返回,因为有写的空间。问题是,如果阻塞io,写了32byte之后,无法继续写,会阻塞进程。从而,应该使用非阻塞io.

为什么多路复用使用阻塞io就不好呢?因为,既然是多路复用,监听的不只是你一个fd,这次你只能写一部分就阻塞了,但是如果其他的fd读就绪呢,哪呢因为你不能写,就把其他人的读给阻塞了。其他人可以正常的读。所以,为了不影响其他人。考虑使用非阻塞io。

IO多路复用的情形下,建议使用Non-blocking I/O

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值