IO模式和IO多路复用

原文连接:http://www.javashuo.com/article/p-pnwzixrq-n.htmlhtml

http://www.javashuo.com/article/p-xaxjhpru-z.htmllinux

  网络编程里常听到阻塞IO、非阻塞IO、同步IO、异步IO等概念,总听别人装13不如本身下来钻研一下。不过,搞清楚这些概念以前,还得先回顾一些基础的概念。web

1 基础知识回顾

注意:我们下面说的都是Linux环境下,跟Windows不同哈~~~编程

1.1 用户空间和内核空间

  如今操做系统都采用虚拟寻址,处理器先产生一个虚拟地址,经过地址翻译成物理地址(内存的地址),再经过总线的传递,最后处理器拿到某个物理地址返回的字节。数组

  对32位操做系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操做系统的核心是内核,独立于普通的应用程序,能够访问受保护的内存空间,也有访问底层硬件设备的全部权限。为了保证用户进程不能直接操做内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操做系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。缓存

补充:地址空间就是一个非负整数地址的有序集合。如{0,1,2...}。安全

1.2 进程上下文切换(进程切换)

  为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复之前挂起的某个进程的执行。这种行为被称为进程切换(也叫调度)。所以能够说,任何进程都是在操做系统内核的支持下运行的,是与内核紧密相关的。网络

  从一个进程的运行转到另外一个进程上运行,这个过程当中通过下面这些变化
  1. 保存当前进程A的上下文数据结构

  上下文就是内核再次唤醒当前进程时所须要的状态,由一些对象(程序计数器、状态寄存器、用户栈等各类内核数据结构)的值组成。多线程

  这些值包括描绘地址空间的页表、包含进程相关信息的进程表、文件表等。
  2. 切换页全局目录以安装一个新的地址空间

    ...
  3. 恢复进程B的上下文

  能够理解成一个比较耗资源的过程。

1.3 进程的阻塞

  正在执行的进程,因为期待的某些事件未发生,如请求系统资源失败、等待某种操做的完成、新数据还没有到达或无新工做作等,则由系统自动执行阻塞原语(Block),使本身由运行状态变为阻塞状态。可见,进程的阻塞是进程自身的一种主动行为,也所以只有处于运行态的进程(得到CPU),才可能将其转为阻塞状态。当进程进入阻塞状态,是不占用CPU资源的

1.4 文件描述符

  文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。

  文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者建立一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些涉及底层的程序编写每每会围绕着文件描述符展开。可是文件描述符这一律念每每只适用于UNIX、Linux这样的操做系统。

1.5 直接I/O和缓存I/O

  缓存 I/O 又被称做标准 I/O,大多数文件系统的默认 I/O 操做都是缓存 I/O。在 Linux 的缓存 I/O 机制中,以write为例,数据会先被拷贝进程缓冲区,在拷贝到操做系统内核的缓冲区中,而后才会写到存储设备中。

缓存I/O的write:

直接I/O的write:(少了拷贝到进程缓冲区这一步)

 

write过程当中会有不少次拷贝,知道数据所有写到磁盘。好了,准备知识概略复习了一下,开始探讨IO模式。

 

2 I/O模式

  对于一次IO访问(这回以read举例),数据会先被拷贝到操做系统内核的缓冲区中,而后才会从操做系统内核的缓冲区拷贝到应用程序的缓冲区,最后交给进程。因此说,当一个read操做发生时,它会经历两个阶段:
  1. 等待数据准备 (Waiting for the data to be ready)
  2. 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)

正式由于这两个阶段,linux系统产生了下面五种网络模式的方案:
  -- 阻塞 I/O(blocking IO)
  -- 非阻塞 I/O(nonblocking IO)
  -- I/O 多路复用( IO multiplexing)
  -- 信号驱动 I/O( signal driven IO)
  -- 异步 I/O(asynchronous IO)

  注:因为signal driven IO在实际中并不经常使用,因此我这只说起剩下的四种IO 模型。

2.1 block I/O模型(阻塞I/O)

阻塞I/O模型示意图:

read为例:

(1)进程发起read,进行recvfrom系统调用;

(2)内核开始第一阶段,准备数据(从磁盘拷贝到缓冲区),进程请求的数据并非一下就能准备好;准备数据是要消耗时间的;

(3)与此同时,进程阻塞(进程是本身选择阻塞与否),等待数据ing;

(4)直到数据从内核拷贝到了用户空间,内核返回结果,进程解除阻塞。

也就是说,内核准备数据数据从内核拷贝到进程内存地址这两个过程都是阻塞的。

 

2.2 non-block(非阻塞I/O模型)

能够经过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操做时,流程是这个样子:

 

  (1)当用户进程发出read操做时,若是kernel中的数据尚未准备好;

  (2)那么它并不会block用户进程,而是马上返回一个error,从用户进程角度讲 ,它发起一个read操做后,并不须要等待,而是立刻就获得了一个结果;

  (3)用户进程判断结果是一个error时,它就知道数据尚未准备好,因而它能够再次发送read操做。一旦kernel中的数据准备好了,而且又再次收到了用户进程的system call;

  (4)那么它立刻就将数据拷贝到了用户内存,而后返回。

  因此,nonblocking IO的特色是用户进程内核准备数据的阶段须要不断的主动询问数据好了没有

 

2.3 I/O多路复用(JAVA NIO就是采用此模式)

    I/O多路复用实际上就是用select, poll, epoll监听多个io对象,当io对象有变化(有数据)的时候就通知用户进程。好处就是单个进程能够处理多个socket。固然具体区别咱们后面再讨论,如今先来看下I/O多路复用的流程:

  (1)当用户进程调用了select,那么整个进程会被block;

      (2)而同时,kernel会“监视”全部select负责的socket;

  (3)当任何一个socket中的数据准备好了,select就会返回;

  (4)这个时候用户进程再调用read操做,将数据从kernel拷贝到用户进程。

  因此,I/O 多路复用的特色是经过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就能够返回

  这个图和blocking IO的图其实并无太大的不一样,事实上,还更差一些。由于这里须要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。可是,用select的优点在于它能够同时处理多个connection。

  因此,若是处理的链接数不是很高的话,使用select/epoll的web server不必定比使用多线程 + 阻塞 IO的web server性能更好,可能延迟还更大。

  select/epoll的优点并非对于单个链接能处理得更快,而是在于能处理更多的链接。)

  在IO multiplexing Model中,实际中,对于每个socket,通常都设置成为non-blocking,可是,如上图所示,整个用户的process实际上是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。

 

 2.4 asynchronous I/O(异步 I/O)

  真正的异步I/O很牛逼,流程大概以下:

(1)用户进程发起read操做以后,马上就能够开始去作其它的事。

(2)而另外一方面,从kernel的角度,当它受到一个asynchronous read以后,首先它会马上返回,因此不会对用户进程产生任何block。

(3)而后,kernel会等待数据准备完成,而后将数据拷贝到用户内存,当这一切都完成以后,kernel会给用户进程发送一个signal,告诉它read操做完成了。

 

2.5 小结

(1)blocking和non-blocking的区别

  调用blocking IO会一直block住对应的进程直到操做完成,而non-blocking IO在kernel还准备数据的状况下会马上返回。

(2)synchronous IO和asynchronous IO的区别

  在说明synchronous IO和asynchronous IO的区别以前,须要先给出二者的定义。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;

  二者的区别就在于synchronous IO作”IO operation”的时候会将process阻塞。按照这个定义,以前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。

  有人会说,non-blocking IO并无被block啊。这里有个很是“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操做,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,若是kernel的数据没有准备好,这时候不会block进程。可是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。

  而asynchronous IO则不同,当进程发起IO 操做以后,就直接返回不再理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程当中,进程彻底没有被block。

(3)non-blocking IO和asynchronous IO的区别

  能够发现non-blocking IO和asynchronous IO的区别仍是很明显的。

  --在non-blocking IO中,虽然进程大部分时间都不会被block,可是它仍然要求进程去主动的check,而且当数据准备完成之后,也须要进程主动的再次调用recvfrom来将数据拷贝到用户内存

  --而asynchronous IO则彻底不一样。它就像是用户进程将整个IO操做交给了他人(kernel)完成,而后他人作完后发信号通知。在此期间,用户进程不须要去检查IO操做的状态,也不须要主动的去拷贝数据。

sellect、poll、epoll三者的区别  

select 
select最先于1983年出如今4.2BSD中,它经过一个select()系统调用来监视多个文件描述符的数组,当select()返回后,该数组中就绪的文件描述符便会被内核修改标志位,使得进程能够得到这些文件描述符从而进行后续的读写操做。

select目前几乎在全部的平台上支持,其良好跨平台支持也是它的一个优势,事实上从如今看来,这也是它所剩很少的优势之一。

select的一个缺点在于单个进程可以监视的文件描述符的数量存在最大限制,在Linux上通常为1024,不过能够经过修改宏定义甚至从新编译内核的方式提高这一限制。

另外,select()所维护的存储大量文件描述符的数据结构,随着文件描述符数量的增大,其复制的开销也线性增加。同时,因为网络响应时间的延迟使得大量TCP链接处于非活跃状态,但调用select()会对全部socket进行一次线性扫描,因此这也浪费了必定的开销。

poll 
poll在1986年诞生于System V Release 3,它和select在本质上没有多大差异,可是poll没有最大文件描述符数量的限制。

poll和select一样存在一个缺点就是,包含大量文件描述符的数组被总体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增长而线性增大。另外,select()和poll()将就绪的文件描述符告诉进程后,若是进程没有对其进行IO操做,那么下次调用select()和poll()的时候将再次报告这些文件描述符,因此它们通常不会丢失就绪的消息,这种方式称为水平触发(Level Triggered)

epoll 
直到Linux2.6才出现了由内核直接支持的实现方法,那就是epoll,它几乎具有了以前所说的一切优势,被公认为Linux2.6下性能最好的多路I/O就绪通知方法。

epoll能够同时支持水平触发和边缘触发(Edge Triggered,只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,若是咱们没有采起行动,那么它将不会再次告知,这种方式称为边缘触发),理论上边缘触发的性能要更高一些,可是代码实现至关复杂。

epoll一样只告知那些就绪的文件描述符,并且当咱们调用epoll_wait()得到就绪文件描述符时,返回的不是实际的描述符,而是一个表明就绪描述符数量的值,你只须要去epoll指定的一个数组中依次取得相应数量的文件描述符便可,这里也使用了内存映射(mmap)技术,这样便完全省掉了这些文件描述符在系统调用时复制的开销。

另外一个本质的改进在于epoll采用于事件的就绪通知方式。在select/poll中,进程只有在调用必定的方法后,内核才对全部监视的文件描述符进行扫描,而epoll事先经过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用相似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便获得通知。

epoll是Linux内核为处理大批量文件描述符而做了改进的poll,是Linux下多路复用IO接口select/poll的加强版本,它能显著提升程序在大量并发链接中只有少许活跃的状况下的系统CPU利用率。缘由就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就好了。
相关文章
相关标签/搜索