目录
- 阻塞 I/O
- 非阻塞 I/O
- I/O复用
- 信号驱动 I/O
- 异步 I/O
- 总结
I/O 其实就是 input 和 output(输入输出)
在计算机操作系统中对应数据流的输入与输出,在 Linux 中,既有文件的 I/O,也有网络 I/O
无论是文件 I/O 还是网络 I/O,其传输过程都是类似的
今天我们以文件 I/O 为例,来深入浅出一下 Linux 的 I/O 模型
我们的程序想要打开一个文件,去查看里面的内容,那么就需要将文件内容加载到内存里面,这个过程是一个比较复杂的过程
首先需要通过内核将数据从硬盘中读取出来,然后放到操作系统的内核缓冲区中,然后再将数据拷贝到程序缓冲区,这时候程序才能获取到相应的数据,打开相应的文件
简单来说,无论什么 I/O 模型,其读取过程都会经历下面两个阶段:
- 将数据放到内核缓冲区(等待数据过程)
- 将内核缓冲区的数据拷贝(copy)到程序缓冲区(拷贝数据过程)
接下来我们来看一下五种 Linux I/O 模型,不然晚上就得睡沙发了捏
阻塞 I/O
阻塞 I/O 称为 Blocking I/O,简称 BIO
在阻塞 I/O 模型里面,当进程发起一个读取文件的请求时(recvfrom 系统调用),如果内核缓冲区没有缓存对应的数据,那么内核就会去读取磁盘,将对应数据放到内核缓冲区,当数据读取完毕后,返回给进程
这便是第一个阶段,在第一阶段中,进程是阻塞的,它干不了任何事,只能乖乖地等内核将数据读取到内核缓冲区
当进程收到内核的响应之后,把内核缓冲区中的数据 copy 一份到程序缓冲区,最后完成读取文件的操作
这便是第二阶段,在第二阶段中,进程也是堵塞的,它只能乖乖地将数据从内核缓冲区 copy 到程序缓冲区
总结:在阻塞 I/O 模型里面,从硬件到内核,从内核到程序空间,都是阻塞的
非阻塞 I/O
在非阻塞 I/O 模型中,当进程发起一个读取文件的请求时(recvfrom 系统调用),如果内核缓冲区没有缓存对应的数据,那么内核就会去读取磁盘,将对应数据放到内核缓冲区
但此时进程的请求不是堵塞的,内核在读取磁盘数据的时候会先返回一个错误信息给进程——数据暂时还没准备好,你待会再来试试
于是进程就会不断地向内核发起重试,一直问:数据准备好了没数据准备好了没
当内核读取数据完毕后,就会通知进程:数据已经准备好了,你可以来拿了
于是第一阶段就此结束,在第一阶段里面进程不会在这里干等,而是不断的轮询重试,但也是阻塞的
当进程收到内核的响应之后,把内核缓冲区中的数据 copy 一份到程序缓冲区,最后完成读取文件的操作
于是第二阶段就此结束,在第二阶段里面进程是堵塞的,它只能乖乖地将数据从内核缓冲区 copy 到程序缓冲区
总结:在非阻塞 I/O 模型里面,从硬件到内核,从内核到程序空间,都是阻塞的。但比阻塞 I/O 有进步了一点,并不是站在那里干等,而是时不时跑去问一下内核。即使是无用功,效率多少还是提高了一点的
I/O复用
之所以叫复用,是因为在这个模型中进程可以同时操作多个数据流,而不像前面所讲的阻塞 I/O、非阻塞 I/O 同一时间只能操作一个数据流
在 I/O 复用模型中,进程监听多个数据流,然后去不断的轮询,当其中一个数据流有数据之后,内核便会通知进程
UNIX/Linux 下的 select、poll、epoll 就是干这个的(epoll 比 poll、select 效率高,做的事情是一样的)
于是第一阶段就此结束,在第一阶段里面进程会不断地对多个数据流进行轮询重试,但依旧是阻塞的
当收到内核的通知之后,进程就会将数据从内核缓冲区 copy 到 程序缓冲区,最后完成读取文件的操作
于是第二阶段就此结束,在第二阶段里面进程是堵塞的,它只能乖乖地将数据从内核缓冲区 copy 到程序缓冲区
I/O 多路复用中有 select、poll、epoll 函数,select 调用是内核级别的,select轮询相对非阻塞的轮询的区别在于——select 轮询可以监听多个数据流,当其中任何一个数据流的数据准备好了,就能返回成功
总结:
- I/O 复用模型的第二阶段与阻塞 I/O、非阻塞 I/O 的第二阶段是一致的,但是在第一阶段中,进程能够同时轮询多个数据流,监听多个数据流,其效率有巨大的提升
- I/O 多路复用是阻塞在select,epoll 这样的系统调用之上,而没有阻塞在真正的 I/O 系统调用如 recvfrom 之上
- I/O 多路复用在阻塞到 select 阶段时,用户进程是主动等待并调用 select 函数获取数据就绪状态消息,并且其进程状态为阻塞。所以,把IO多路复用归为同步阻塞模式
信号驱动 I/O
信号驱动 I/O 可以说是 I/O 模型上的一个里程碑
它与前面三个 I/O 模型的区别在于信号这个词
信号驱动 I/O 在第一阶段,即等待内核将数据从磁盘读取到内核缓冲区这个阶段,进程是不阻塞的,而是设置了一个信号回调,当数据到达内核缓冲区之后,内核调用程序的回调
在第一阶段里面进程是非阻塞的,它可以去干其他事情
当数据到达内核缓存区,进程收到内核的信号之后,进程再将数据从内核缓冲区 copy 到程序缓冲区,最后完成读取文件的操作
第二阶段就此结束,在第二阶段里面进程是堵塞的,它只能乖乖地将数据从内核缓冲区 copy 到程序缓冲区
总结:
-
虽然说信号驱动 I/O 模型的第二阶段跟前面三个 I/O 模型一样,即进程是阻塞的。但在第一阶段做到了真正的异步
-
信号驱动 IO 在第一阶段,进程去请求内核读取数据,这时候其不会阻塞,也不会去轮询,而是设置一个信号回调。 当数据完全拷贝到系统内核时,系统发出 SIGIO 信号,通知进程去进行第二阶段,将数据拷贝到程序缓冲区
异步 I/O
异步I/O 相比前面四个 I/O 模型,无论是第一阶段还是第二阶段都实现了非阻塞
与信号驱动 I/O 类似,异步 I/O 模型在第一阶段通过信号回调的方式实现了进程的非阻塞
即用户进程进行 aio_read 系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情
而在第二阶段,进程将数据从内核缓冲区 copy 到程序缓冲区的时候并非阻塞,而是同样设置一个信号回调,当 copy 完成后,进程收到通知,再去执行相应的操作
总结:异步 IO 不仅仅是在第一阶段实现了信号回调,其也在第二阶段实现了信号回调,两个过程都是非阻塞的,从而完全实现了异步 IO 操作
总结
我们根据这五种 I/O 模型在第一第二阶段的表现来总结一下:
- 阻塞 I/O
- 第一阶段(硬件到系统内核),阻塞
- 第二阶段(系统内核到程序空间),阻塞
- 非阻塞 I/O
- 第一阶段(硬件到系统内核),轮询阻塞
- 第二阶段(系统内核到程序空间),阻塞
- I/O 复用
- 第一阶段(硬件到系统内核),多路轮询阻塞
- 第二阶段(系统内核到程序空间),阻塞
- 信号驱动 I/O
- 第一阶段(硬件到系统内核),信号驱动不阻塞
- 第二阶段(系统内核到程序空间),阻塞
- 异步 I/O
- 第一阶段(硬件到系统内核),信号驱动不阻塞
- 第二阶段(系统内核到程序空间),信号驱动不阻塞
从上面的 5 种 I/O 模型,我们可以看出,真正实现异步非阻塞的只有异步 IO 这种模型,而其他四种都是同步 I/O
因为在第二阶段:从内核缓冲区复制到程序缓冲区的时候,不可能干其他事情
最后再来简单说说阻塞与非阻塞、同步与异步的区别
阻塞与非阻塞:
- 阻塞 I/O 会一直 block 对应的进程,直到 I/O 操作完成
- 非阻塞 I/O 在内核还在准备数据的情况下会立刻返回给进程
同步与异步:
在说明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;
两者区别就在于——同步 I/O 做 I/O 操作(I/O operation)时会将进程阻塞掉,按照这个定义,之前所述的阻塞I/O、非阻塞 I/O,I/O 多路复用都属于同步 I/O
可能这里有人会想:非阻塞 I/O 并没有被 block 啊,请注意上面说的定义,定义中所指的 I/O 操作是指真实的 I/O 操作,就是上面例子中的 recvfrom 这个系统调用
非阻塞 I/O 在第一阶段,也就是执行 recvfrom 这个系统调用的时候,如果内核数据还没有准备好,是不会 block 进程的,但是在第二阶段,如果内核的数据准备好并要 copy 到用户缓冲区中,这个时候进程就会被 block
而异步 I/O 则不一样,它实现了两个阶段都是非阻塞的,当进程发起 I/O 操作之后,就直接返回再也不理睬了,直到内核发送一个信号,告诉进程说 I/O 完成。在这整个过程中,进程完全没有被block