UNIX下的5种IO模型


套接字的IO操作,如recvfrom,分为两个阶段:



(1)等待内核中的接收缓冲区中有数据可读。



(2)将接收缓冲区中的数据复制进应用缓冲区。



1,阻塞式IO



文件描述符open时,如果没有指定flags为O_NONBLOCK,或者open后,没有使用fcntl设置O_NONBLOCK,默认文件描述符为阻塞模式。



阻塞式IO在等待接收缓冲区数据到来时,会阻塞;



数据到来,进行数据复制时,也会阻塞。



2,非阻塞式IO



如上所述,可以通过open或者fcntl设置文件描述符为非阻塞模式。



非阻塞式IO,当内核缓冲区没有数据时,不会阻塞,会立即返回一个错误——EAGAIN或者EWOULDBLOCK。EAGAIN表示需要再次调用recvfrom,以判断数据是否准备好,这也是非阻塞式IO的用法,不断的调用recvfrom,以判断是否可以读。EWOULDBLOCK是虚拟语气,表示“本应该阻塞”,其实没有阻塞。由于并不确定返回EAGAIN还是EWOULDBLOCK,因此需要对这两个值都进行判断。



从接收缓冲区向应用缓冲区复制数据阶段,调用进程阻塞。



3,IO复用



在阻塞式IO中,如果接收缓冲区没有数据,调用进程阻塞于recvfrom操作。使用select或者poll,可以在此情况下使进程阻塞于select或者poll操作(因此要把文件描述符设置为非阻塞式),而且可以同时检测多个文件描述符是否可读,即检测这些描述符对应的内核中的接收缓冲区是否有数据。



从接收缓冲区向应用缓冲区复制数据阶段,调用进程阻塞。



4,信号驱动式IO



信号驱动式IO与上述三个IO模型相比,即不像阻塞式IO那样阻塞于recvfrom操作,也不像非阻塞式IO那样需要多次调用甚至轮询recvfrom才能得知是否有数据,也不像IO复用那样阻塞于select或者poll,而是当内核接收缓冲区有数据时向调用进程发送一个信号。



从接收缓冲区向应用缓冲区复制数据阶段,调用进程阻塞。



5,异步IO



上述4种IO模型,其不同点在于当接收缓冲区没有数据时,如何判断数据已经到来:阻塞式IO中recvfrom会阻塞直到接收缓冲区有数据;非阻塞式IO通过轮询recvfrom以判断接收缓冲区是否有数据;IO复用中使用select或者poll以判断接收缓冲区是否有数据;信号驱动IO通过信号通知接收缓冲区是否有数据。



其相同点在于,IO操作的第二个阶段,即从内核接收缓冲区向应用缓冲区复制数据时,调用recvfrom的进程会阻塞。



可见,上述4种IO模型都会使进程阻塞,直到IO操作的两个阶段都完成才能执行其他操作,因此称为同步IO。



异步IO模型中,IO操作的两个阶段都不阻塞,因此称为异步IO。



阻塞IO
这是我们熟悉的IO模型,一个进程在作IO操作时,非要等到数据从内核空间拷贝到用户进程空间,才会返回。这个模型的优点就是简单,而且在阻塞的时候,CPU还可以进行调度,去执行别的进程。
非阻塞IO
一开始我看是非阻塞IO,觉得应该要比阻塞IO模型先进,可是当我一看使用方法的时候,就知道这个模型是不会被实际使用的,仅仅只能作为理论上存在的IO模型。这个模型的观点是:进行IO操作的时候,不阻塞,如果没有数据准备好,就直接返回错误码(或者是别的代码)。因此,使用者就只能不断进行轮询来调用IO函数。这样的后果就是,不仅在宏观上形成了与阻塞IO一共的“阻塞”效果,而且在微观上,CPU一直被用来轮询,造成了CPU的浪费。所以,这个模型还不如阻塞IO模型实用。
IO复用
对于IO复用,我的理解有三点:
在一次系统调用中,实现了询问多个描述符的IO准备情况 —— 根据事件通知
为了实现第一点,就需要把阻塞的地方进行转移。把一次系统调用,分为两次系统调用。第一次系统调用可以询问多个描述符的IO准备情况,在这个地方进行阻塞;而第二次系统调用,是针对已经准备好IO的描述符进行调用,此时,理论上(按照我的理解),也是会发生阻塞的,只不过是此时内核已经把数据准备好了,阻塞的时间可以忽略不计罢了。
本质上,还是阻塞的。
信号IO
我们都知道,信号是UNIX提供了进程间进行通信的一种方式。我们常用的 kill -9 命令(kill是向进程传递信号量,9只是众多信号中的一个代号),或者是 Ctrl + C 的时候,就是向某个进程发出终止的信号,这样进程就退出了。
而对于信号IO的模型,我是这么理解的:进程在发起IO操作,系统调用之后,直接访问,内核会在IO数据准备好之后,以某个信号通知发起IO操作的进程,从而使得该进程的信号处理函数可以读取IO数据的操作。
本质上,这也是阻塞的IO模型,因为在信号处理函数中,同样也是要进行阻塞的,只是在在这个时候发起系统系统,内核已经把数据准备好了。
异步IO
这是真正的异步IO了。实现的机制是:用户在发起异步IO的系统调用时,会把相应的数据处理函数作为回调函数,等到IO数据准备好,内核会主动调用此回调函数。可以看出,用户进程在这种模型下,只调用了一次系统调用,而且是立即返回的,因此,就不会出现让进程阻塞的情况,也就符合了POSIX中异步IO的定义。
其实我理解起来,思路是和信号IO差不多的,唯一不同的地方,对于IO数据的操作,异步IO是由内核主动发起的,而信号IO是由用户进程发起的。



进程切换
为了控制进程的执行,内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说,任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。



从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:



  1. 保存处理机上下文,包括程序计数器和其他寄存器。

  2. 更新PCB信息。

  3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。

  4. 选择另一个进程执行,并更新其PCB。

  5. 更新内存管理的数据结构。

  6. 恢复处理机上下文。


Category linux