IO漫谈之IO模型(一)

几个概念

User space && Kernel space

A modern computer operating system usually segregates virtual memory into kernel space and user space.Primarily, this separation serves to provide memory protection and hardware protection from malicious or errant software behaviour.

Kernel space is strictly reserved for running a privileged operating system kernel, kernel extensions, and most device drivers. In contrast, user space is the memory area where application software and some drivers execute.

以上是来自维基的解释,简单点说就是kernel space是操作系统的运行空间,user space是用户程序的运行空间。kernel space可以执行任何命令,调用系统的一切资源;user space只能执行简单的运算,不能调用系统资源,必须通过系统接口,才能向内核发出执行指令。

File descriptor

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

以上是来自维基百科的解释

IO 模型

对于Linux系统而言,所有设备都是文件,其中包括磁盘、内存、网卡、键盘等等,对所有这些文件的访问都属于IO。针对所有的IO对象,可以将IO大致分为三类:网络IO、磁盘IO和内存IO,而通常我们说的是前两种。

关于标准的IO,也即缓存IO,我们可以看下面这张图:

我们可以看到读过程分为两个阶段

等待数据准备,也就是数据从磁盘拷贝到内核空间

将数据从内核拷贝到进程中,也就是将数据从内核空间拷贝到用户空间

相应的写过程也分为两阶段,可以参看上图。这两个阶段很重要,因为接下来我们要讨论的IO模型的区别就是在这两阶段上各有不同。

在Unix/Linux系统中,一共比较了5种IO模型:

  • 阻塞式IO
  • 非阻塞式IO
  • IO复用
  • 信号驱动IO
  • 异步IO
阻塞式IO

Linux IO模型 - 阻塞式IO

当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据,如数据从磁盘拷贝到操作系统内核空间。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户空间,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。

阻塞式IO的特点就是在IO执行的两个阶段都被block了

非阻塞式IO

Linux IO模型 - 非阻塞式IO

当用户进程调用了recvfrom这个系统调用,如果kernel数据还没有准备好,那么recvfrom会立刻返回一个EWOULDBLOCK错误。用户进程收到EWOULDBLOCK之后,不会进入阻塞状态,而是循环发起recvfrom操作直到数据就绪。当数据就绪,用户进程此时进入阻塞状态,等待系统将数据从kernel中复制到用户空间,复制完成后kernel返回结果,用户进程解除block的状态,重新运行起来。

非阻塞式IO的特点就是用户进程需要不断的主动询问

IO多路复用

Linux IO模型 - IO复用

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

上面的同步阻塞、同步非阻塞,每次只能处理单个连接IO。当同时要处理很多个耗时短的IO,多路复用IO就擅长这个,我们所说的select,poll,epoll都属于多路复用IO。多路复用IO主要改进在单个进程就可以同时处理多个IO,上图以select为例。

I/O 多路复用的特点是通过一种机制让一个进程能同时等待多个文件描述符,而这些文件描述符其中的任意一个进入读就绪状态,select()函数就可以返回。

信号驱动IO

Linux IO模型 - 信号驱动式IO

信号驱动IO相当于同步非阻塞的升级版。用户进程调用read命令,系统在准备数据时,用户进程不会被阻塞,可以同时做其他任务,不需要循环去问系统数据是否准备就绪。当数据准备好之后,系统通知用户进程。但在数据从内核空间复制到用户空间这阶段,此时用户进程处于阻塞状态。

异步IO

Linux IO模型 - 异步IO

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

异步IO的特点就是IO执行的两个阶段都不会block

总结

阻塞IO和非阻塞IO的区别在哪里?

阻塞IO会一直block住对应的进程直到read操作完成,而非阻塞IO在kernel还未把数据准备好的情况下就返回了。

阻塞和非阻塞就是上面所说的第一阶段,即在数据被拷贝到内核空间前,用户进程是否等待。如果用户进程是等待的,就是阻塞,如果用户进程是立即返回,就是非阻塞。

同步IO和异步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;

两者的区别就在于同步IO做IO operation的时候会将process阻塞。按照这个定义,之前所述的阻塞 IO,非阻塞IO,IO多路复用都属于同步IO。

有人会说,非阻塞IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的IO operation是指真实的IO操作,就是例子中的recvfrom这个系统调用。非阻塞IO在执行recvfrom这个系统调用的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。

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

同步和异步说就是上面所说的第二阶段,即数据从内核空间复制到用户进程空间,这时用户进程是否处于等待状态,如果是用户进程需要等待,即为同步,否则为异步。

各个IO模型的比较如下图所示:

IO模型比较

通过上面的图片,可以发现非阻塞IO和异步 IO的区别还是很明显的。在非阻塞IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而异步IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。

参考资料

Linux IO解读
【Java】NIO的原理与浅析
Linux IO解读
聊聊同步、异步、阻塞与非阻塞
聊聊Linux 五种IO模型
IO - 同步,异步,阻塞,非阻塞 (亡羊补牢篇)