I/O Models
概念
Input/Output
- 在硬件层面,I/O是字节在硬盘、网卡、键盘等设备到内存之间流动的过程。
- 在应用软件的角度上,Input是应用软件通过直接或间接地调用操作系统(kernel)提供的IO接口访问应用进程外部数据的过程。这个过程通常包括两个阶段:第一阶段是在向内核提交需求(调用kernel接口);第二阶段是内核将这部分数据从内核缓冲区复制到应用缓冲区域即成功获得数据(完成任务)。Output是应用调用内核IO接口向应用进程外输出数据的过程。同样地通常涉及两个阶段:第一阶段是直接或间接调用内核IO接口请求输出数据;第二阶段内核将数据从应用缓冲区复制到目的地。从第一阶段进入第二阶段需要满足一定条件。对于Input,条件通常包括应用需要的数据在内核缓冲区准备好并可以复制到应用缓冲区中。对于Output,条件包括目标文件有足够空间存放数据。
- 对于应用程序来说,向网络(Network)输出输入数据与读写文件本质上都是I/O:前者是将数据写到网卡,最终发到目的主机,而后者是将数据写到硬盘。在Linux操作系统中,所有外设都被映射成文件,即对外设的操作都被映射成对文件的操作。TCP协议中,通信链接的两端是Socket(套接字,由IP地址和端口组成)。服务端应用进程与客户端进程的通信就是通过读写Socket文件:服务端进程想要接收客户端发送的数据,就要从本机上与该客户端对应的Socket文件输入流中读取数据,如果想发送数据,就要向该Socket文件输出流写入数据。所以应用程序间的网络通信本质上只是I/O的一种。
中断
中断包括程序中断、时间中断、I/O中断和硬件失效中断。当程序向内核发出I/O请求,内核执行I/O程序,I/O设备通过中断的方式通知内核:”已发现需要的数据“,内核执行中断程序回应硬件设备:”已获悉“。
中断机制在每个OS中都可能有不一样的实现,下面简述Linux系统中的一般中断机制,具体实现仍取决于开发者。
上半部分:内核通过中断处理程序及时回应发出中断请求的硬件(尽快返回被中断的工作中)。当I/O事件完成,I/O设备发送中断信号到中断控制器芯片,如果此时处理器允许中断,中断控制器向处理器发送一个中断请求的电信号,处理器在指令周期中的检测中断阶段检测到I/O中断请求,立刻停止当前处理的程序并屏蔽中断,跳到预先由内核设置好的内存地址读取指令并执行,这个地址就是中断处理函数的入口,内核在这个函数内计算出中断号,根据中断号运行对应的处理程序(该中断处理程序来自于外部设备的驱动),完成中断处理程序后即对硬件的中断请求作出了回应。在这个阶段有时还会做另外一些同样对时间很敏感的工作,例如从硬件拷贝数据到系统内存缓冲区,如当网卡接收到的数据,之后可以返回到被中断的程序中继续执行指令。
下半部分:完成对时间不是非常敏感的工作,这部分工作可以往后推迟。例如处理系统内存中从硬件接收到的数据,如果此时完成了一个I/O请求的条件,可以将阻塞在此I/O请求上的进程设置为就绪态。‘
系统调用处理程序
用户空间的程序无法直接执行内核代码,因为内核的函数存放在受保护的系统空间内。应用程序通过软中断的方式,通知系统自己需要执行一个系统调用:通过引发一个异常促使系统从用户态切换到系统态,执行一个异常处理函数,这个函数就是系统调用处理程序。
阻塞(Blocking)与 非阻塞(Non-Blocking)
阻塞态是进程的一种执行态,此时进程不能被内核调度继续执行。阻塞的原因有很多种,可以归纳为等待的事件仍未发生。在这里我们讨论的阻塞与非阻塞就是进程在执行I/O调用之后的状态。
当进程执行一个系统调用以请求I/O资源,但内核检测到I/O资源被占用或者数据未准备好,所以不能立刻响应线程为其服务,进程的状态被内核调整为阻塞态,直到该I/O资源准备好并返回系统调用的结果,内核将进程执行态设置为就绪态。相反,如果进程执行一个系统调用,该系统调用无论I/O资源准备好与否都立即返回某个结果,进程就不需要进入阻塞态且可以继续执行后续指令。两种系统调用的区别在于:当操作系统没有准备好程序申请的I/O资源时,进程是否会被内核设置为阻塞态。
同步(synchronous )与 异步(Asynchronous)
同步与异步是对请求方与被请求方之间的通信方式。
同步(synchronous ):当请求方发送请求后,只能做获取被请求方的响应的事情,可以是什么都不做,等待被请求方回应结果(阻塞),也可以是不断地询问被请求方是否有结果(非阻塞、轮询-polling),但就是不会做与这次请求无关的其他任务。当进程执行I/O请求后,被阻塞等待内核通知I/O事件完成,或者非阻塞地通过轮询的方式询问内核I/O事件是否完成,而不会去执行其他不相干的代码。
异步(Asynchronous):当请求方发送请求后,不关心结果,直接执行其他任务的操作(非阻塞),等到被请求方处理完请求后再通知请求方,请求方在合适的时候再处理这个结果,或者在请求的时候就告诉被请求方当处理完请求后帮其干点什么。当程序执行I/O系统调用,该系统调用是非阻塞的,即会立即返回一个结果,程序可以继续执行后面的指令,这些指令不会涉及到这个结果,当内核通知其I/O事件完成后才会考虑处理结果。或者设置回调函数,当I/O事件完成时执行回调函数。
I/O Models
阻塞I/O(Blocking I/O:BIO)Model
BIO模型是同步阻塞的。
阻塞:进程执行系统调用请求获取数据,被阻塞直到数据复制到系统缓冲区,并从系统缓冲区复制到应用缓冲区。
同步:由于在内核准备数据这段时间,程序没有做其他工作,直到获取到内核的结果。
非阻塞I/O(Non-Blocking I/O:NIO)Model
NIO模型是同步非阻塞的。
非阻塞于:进程执行系统调用请求数据,当系统调用检测系统缓冲区没有准备好数据时,并没有让程序等待直到数据准备好,而是直接返回一个负数代表数据没有准备好,程序可以继续执行后面的指令。
同步于:虽然程序可以继续执行后面的指令,但是想先收到数据再做其他工作,通过轮询的方式不断询问内核数据是否准备好。
I/O 多路复用(I/O Multiplexing)Model
在多路复用模型中,复用的是线程,即用一个线程监听多个I/O流上可读可写的事件。
在之前的BIO模型与NIO模型中,服务端在一个线程里只等待一个客户端Socket文件I/O流上可读或可写的事件发生,这意味着一个线程只服务一个客户端,当客户端Socket文件的输入流不可读,线程同步地等待直到该输入流可读,然后读取数据并处理。如果服务端进程要同时处理多个客户端即监听多个Socket文件上的I/O流可读写事件,需要增加线程数目。值得注意的是,除了根据客户端数目1:1地增加线程数目来服务多个客户端,也可以通过设置线程池的方式,用若干个线程服务多个客户端。但本质上,这两种I/O模型中每个线程都只能同时监听一个Socket文件上的I/O流事件。
在I/O多路复用模型中,服务端在一个线程里可以同时监听多个客户端Socket文件上的I/O流。线程等待多个I/O流上的可读可写事件,当一个或以上的I/O流可读/可写事件发生,内核返回被包装好的I/O流对象,线程串行地根据事件的类型对这些I/O流进行读或者写。
I/O多路复用是同步阻塞的。
阻塞于:当没有一个I/O上有可读或可写事件发生,线程是被阻塞的,select()函数不能马上返回(select函数选择一个或以上的可读写I/O流)。值得注意的是,有的文章会根据:即使有部分I/O流不满足读/写条件也不会阻塞整个线程妨碍其他I/O流读写这一观点,认为I/O复用是非阻塞的,这是对的,只是大家角度不同。
同步于:因为在发送I/O请求(select函数)后如果没有可读或可写事件发生,线程是阻塞的,线程没办法执行其他工作,所以是同步的。
异步I/O模型(Asynchronous I/O Model)
异步I/O模型是异步非阻塞的。在该模型中,线程为I/O请求执行的系统调用不会阻塞线程而是马上返回,此次请求仅仅是通知内核将需求数据复制到应用缓冲区,之后线程可以执行后面的指令,与NIO不同的是,AIO不会以轮询地方式等待I/O操作完成,而是去执行其他工作,直到内核将数据复制到应用缓冲区再通知线程,因此AIO是非阻塞的,异步的。
参考
- 《操作系统——精髓与设计原理》
- 《 UNIX Network Programming, Volume 1 》
- 《Linux内核设计与实现》
- 《Computer Systems - A Programmer's Perspective》



