IO常见模型-详解io多路复用

常见io模型介绍

  • blocking IO
  • nonblocking IO
  • IO multiplexing
  • signal driven IO
  • asynchronous IO

IO发生时涉及的对象和步骤

  • 对于一个network IO(这里我们以read举例),它会涉及到两个系统对象:

    • 一个是调用这个IO的process(or thread)
    • 一个就是系统内核(kernel)
  • 当一个read操作发生时,它会经历两个阶段:

    • 等待数据准备,比如accept(),recv()等待操作 (Waiting for the data to be ready)
    • 将数据从内核拷贝到进程中,比如accept()接受到请求,recv()接受连接的数据后需要复制到内核,再从内核复制到进程的用户空间 (Copying the data from the kernel to the process)
  • 对于socket流而言,数据的流向经历两个阶段:

    • 第一步通常涉及到等待网络上的数据分组到达,然后被复制到内核的某个缓冲区
    • 第二步把数据从内核缓冲区复制到应用进程缓冲区
      记住这两点很重要,因为这些IO模型的区别就是在这两个阶段各有不同的情况

阻塞I/O (blocking IO)


在linux中,默认情况下所有的socket都是blocking, 一个典型的读流程大概是这样的:
avatar
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段: 准备数据(对于网络IO来说,很多时候数据在一开始还没有到达.比如,还没有收到一个完整的UDP包.这个时候kernel就要等待足够的数据到来). 这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区是需要一个过程的.
而用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞).当kernel一直等到数据准备好了,它就会将数据从kernel拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来.

所有,blockingIO的特点就是在IO执行的两个阶段都被block了.

非阻塞I/O(nonblocking IO)


linux下,可以通过设置socket使其变为non-blocking. 当对一个non-blocking socket执行读操作时候,流程是这个样子:
avatar
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立即返回一个error.从用户进程角度来讲,它发起一个read操作后,并不需要等待,而是马上就得到一个结果.用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作.一旦kernel中的数据准备好了,并且又再次收到用户进程的system call, 那么它马上就将数据拷贝到用户内容, 然后返回.

所以,nonblockingIO的特点是用户进程需要不断的主动询问kernel数据好了没有
值得注意的是,此时的非阻塞IO只是应用到等待数据上,当真正有数据到达执行的时候,还是同步阻塞IO来的,从途中的 copy data from kernel to user 可以看出

I/O多路复用(IO multiplexing)


IOmultiplexing就是我们说的select, poll, epoll, 有些地方这种IO方式为event driven IO.
select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO. 它的基本原理就是select,pollepoll这个function会不断的轮训所负责的所有socket,当某个socket有数据到达了,就通知用户进程.
avatar
这个图和blockingIO的图其实并没有太大的不同,事实上,还更差一些.因为这里需要使用两个system call(select和recvfrom),而blockingIO只调用了一个system call(recvfrom). 但是,用select的优势在于它可以同时处理多个connection.

所有,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大.
select/epoll的优势并不是对单个连接能处理的更快,而是在于能处理更多的连接

在IOmultiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,因为只有设置成non-blocking才能使用单个线程/进程不被则色(或者说锁住),可以继续处理其它socket>如上图所示,整个用户的process其实是一直被block的.只不过process是被select这个函数block,而不是被socketIO给block.

当用户进程调用了select,那么整个进程会被block,而同时,所有进来的连接socket,都会加入到select监视列表里面,由kernel会’监视’所有select负责的socket,而之后select(poll, epoll等)函数会不断的轮训所负责的所有socket,这些socket都是非阻塞的存在与select的监视列表,select使用某种监视机制检查某个socket是否有数据到达了,当任何一个socket中的数据准备好了,select就会返回.这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程.

点评:
I/O多路服用的特点是通过一种机制一个进程能同事等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入就读就绪状态,select()函数就可以返回.
所有,IO多路服用,本质上不会有并发的功能,因为任何时候还是只有一个进程或者线程在进行工作,它之所以能提高效率是因为select/epoll把进来的socket放到它们’监视’的列表里面,当任何socket有可读可写数据立马处理.select/epoll手里同事检测着很多socket,一有动静马上返回给进程处理,总比一个一个socket过来,阻塞等待,处理的效率高
当然也可以多线程/多进程方式,一个连接过来开一个进程/线程处理,这样消耗的内存和进程切换会耗掉更多的系统资源.
所以,我们可以结合IO多路复用和多进程/多线程来提高性能并发,IO服用负责提高接受socket的通知效率,收到请求后,交给进程池/线程池来处理逻辑


异步IO(asynchronous IO)

linux下的asynchronousIO其实用的很少. 先看一下它的流程:
avatar
用户进程发起read操作后,立即就可以开始去做其它的事. 而另一方面,从kernel的角度,当它收到一个asynchronous read之后,首先它会立即返回,所以不会对用户进程产生任何block. 然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发一个signal,告诉它read操作完成了.

阻塞IO,非阻塞IO与同步IO,异步IO的区别和联系

阻塞IO VS 非阻塞IO:
概念:
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程.

例子: 你打电话问书店老板有没有<<深入理解计算机操作系统>>这本书, 如果你是阻塞式调用,你会一直把自己’挂起’,直到得到这本书有还是没有的这个结果, 如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边玩去了,当然你也要偶尔过几分钟check一下老板有没有返回结果. 在这里阻塞与非阻塞与是否同步异步无关.跟老板通过什么方式回答你结果无关.
分析:
阻塞IO会一直block住对应的进程直到操作完成,而非阻塞在kernel还在准备数据的情况下会立刻返回


同步IO VS 异步IO:
概念:
同步和异步关注的是**消息通信机制(synchronous communication/asynchronous communication)**所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回.但是一旦调用返回,就得到返回值了.换句话,就是由调用者主动等待这个调用的结果. 而异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果.换句话,当一个异步过程调用发出后,调用者不会立刻得到结果.而是在调用发出来后,被调用者通过状态,通知等来通知调用者,或通过回调函数处理这个调用.

典型的异步变成模型,比如Node.js.
举个通俗的例子: 你打电话问书店老板有没有<<深入理解计算机操作系统>>这本书,如果是同步通讯机制,书店老板会说,你稍等一下,‘我查一下’,然后开始查呀查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果).而异步通讯机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果).然后查好了,他会主动打电话给你. 这里老板通过’回电’这种方式来回调.

分析:
在说明同步IO和异步IO的区别之前,需要先给出两者的定义.Stevens给出的定义(其实是POSIX的定义)是这样子的:

A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;

两者的区别就在于同步IO做’IO operation’的时候会将process阻塞.按照这个定义,之前所述的阻塞IO,非阻塞IO,IO复用都属于同步IO.
有人可能会说,非阻塞IO并没有被block啊.这里有个非常’狡猾’的地方,定义中所指的’IO operation’是指真实的IO操作,就是例子中的recvfrom这个system call. 非阻塞IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候会不block进程.但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的.

而异步IO则不一样,当进程发起IO操作之后,就直接返回再也不理睬了,知道kernel发送一个信号,告诉进程说IO已经完成. 在这个过程中,进程完全没有被block.

IO模型的形象举例

最后,再举几个不是很恰当的例子来说明这四个IOmodel:
有A, B, C, D,四个人在钓鱼:
A用户是最老式的鱼竿,所以呢,得一直守着,等鱼上钩了再拉杆;
B的鱼竿有个功能,能显示是否有鱼上钩,所以呢,B就和旁边的MM聊天,个会再次看看有没有鱼上钩,有的话就迅速拉杆;
C用的鱼竿和B差不多,但他想到了一个好办法,就是同时放好几根鱼竿,让后找一个小孩来帮他看着,一旦有显示说鱼上钩了,小孩就告诉它哪个鱼竿上钩了,C就将对应的鱼竿拉起来;
D是个有钱人,干脆雇了一个人帮他钓鱼,一旦那个人把鱼钓上来了,就给D发短信.

注意,除了异步IO,其它的都是进程自己copy data from kernel to user. 异步IO是kernel将准备好的数据拷贝到用户的进程中,然后通知用户

Select/Poll/Epoll轮训机制

select, poll, epoll本质都是同步IO(自己阻塞这,将准备好的数据,从kernel拷贝到自己的进程中),因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞.
Select/Poll/Epoll都是IO复用的实现方式, 上面说了使用IO复用,会把socket设置成non-blocking,然后放进Select/Poll/Epoll各自的监视列表里面,那么,他们的对socket是否有数据到达的监视机制又分别是怎么样的?效率又如何?我们应该使用那种方式实现IO复用比较好?下面列出他们各自的实现方式,效率,优缺点:

  1. select, poll实现需要自己不断轮询所有fd集合,知道设备就绪,期间可能要睡眠和唤醒多次交替. 而epoll其实也需要调用epoll_wait不断轮训就续链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放到就绪链表中,并唤醒在epoll_wait总会给你进入睡眠的进程.虽然都要睡眠和交替,但是select和poll在’醒着’的时候都要遍历整个fd集合,而epll’醒着’的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间. 这就是回调机制带来的性能提升.
  2. select, poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列只是一个epoll内部定义的等待队列).这也能节省不少的开销.

再次举例清楚五中IO模型

  1. 阻塞I/O模型
    老李去火车站买票,排队三天买到一张退票。
    耗费:在车站吃喝拉撒睡 3天,其他事一件没干。

  2. 非阻塞I/O模型
    老李去火车站买票,隔12小时去火车站问有没有退票,三天后买到一张票。耗费:往返车站6次,路上6小时,其他时间做了好多事。

  3. I/O复用模型

    1. select/poll
      老李去火车站买票,委托黄牛,然后每隔6小时电话黄牛询问,黄牛三天内买到票,然后老李去火车站交钱领票。
      耗费:往返车站2次,路上2小时,黄牛手续费100元,打电话17次
    2. epoll
      老李去火车站买票,委托黄牛,黄牛买到后即通知老李去领,然后老李去火车站交钱领票。
      耗费:往返车站2次,路上2小时,黄牛手续费100元,无需打电话
  4. 信号驱动I/O模型
    老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李,然后老李去火车站交钱领票。
    耗费:往返车站2次,路上2小时,免黄牛费100元,无需打电话

  5. 异步I/O模型
    老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李并快递送票上门。
    耗费:往返车站1次,路上1小时,免黄牛费100元,无需打电话

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值