对UNIX下的五种常见IO模型分析,帮助理解
IO操作的两个阶段
以读数据操作为例:
- 等待内核数据准备(数据拷贝到内核缓冲区)
- 将数据从内核拷贝到用户空间
IO模型
UNIX下共有五种常见的IO模型:
下面以recvfrom
接口举例
阻塞IO
默认情况下,所有的套接字都是阻塞的
调用recvfrom
接口,进程在IO操作的两个阶段都会阻塞,直到最终数据拷贝到用户空间或者过程中出现错误才会返回,进程在阻塞状态下是不占用CPU资源的
最常见的错误是发生系统中断,此时需要重读,可参考这里
非阻塞IO
可以通过fcntl(sockfd,F_SETFL,O_NONBLOCK)
将套接字设置成非阻塞
调用recvfrom
接口,无论内核缓冲区是否有可用数据,进程都会立即返回,所以在IO操作的第一阶段是非阻塞的; 若无数据可用,内核将errno
设置为为EWOULDBLOCK
或者EAGAIN
,进程可以使用轮询的方法,保证内核在数据准备好时,能立即拷贝到用户空间; 若有则立即将数据拷贝到用户空间,进程在数据拷贝到用户空间即IO操作的第二阶段是阻塞的;
非阻塞IO过于消耗CPU时间,将大部分时间用于轮询
多路复用IO
多路复用系统调用:select
,poll
和epoll
,其中windows平台不支持poll
和epoll
,使用方法可以参考I/O 多路复用之select、poll、epoll详解和Linux select/poll和epoll实现机制对比
调用select
,等待内核数据准备,所以IO操作的第一个阶段,进程是阻塞的,不过是阻塞在多路复用系统调用上,而不是IO系统调用上; 当select
返回套接字可读条件时,再调用recvfrom
将数据从内核拷贝到用户空间,IO操作的第二阶段,进程是阻塞的
多路复用IO和阻塞IO,在IO操作的两个阶段都是阻塞的,不过多路复用IO使用了两个系统调用,而阻塞IO只使用了一个,所以在连接数不是很多的情况下,阻塞IO可能性能更佳; 多路复用IO的优势在于可以同时监控多个用于IO的文件描述符。
多线程中的阻塞IO,与多路复用IO极为相似
信号驱动IO
调用sigaction
等系统调用安装信号处理函数,并立即返回,所以IO操作的第一阶段,进程是非阻塞的; 当内核数据准备好时,内核会产生一个信号,通知进程将数据从内核拷贝到用户空间,IO操作的第二阶段,进程是阻塞的
使用方法:IO的多路复用和信号驱动
异步IO
异步IO有一组以aio
开头的系统调用,使用方法可参考Linux AIO机制
调用异步IO系统调用,给内核传递描述字、缓冲区指针、缓冲区大小(与read
相同的三个参数)、文件偏移(与lseek
类似),告诉内核当整个操作完成时如何通知我们,并立即返回,在IO操作的两个阶段,进程都不阻塞
总结
- 同步IO和异步IO的主要区别是将数据从内核拷贝到用户空间是否阻塞,前者会在将数据从内核拷贝到用户空间时即IO操作的第二个阶段发生阻塞,而后者则在系统调用后直接返回,直到内核发送信号通知IO操作完成,在IO操作的两个阶段都没有阻塞
- 阻塞IO和非阻塞IO的主要区别是系统调用是否立即返回(默认将数据从内核拷贝到用户空间即IO操作的第二个阶段是立即返回的),前者会在IO操作的两个阶段完成前一直阻塞,后者在内核没有准备好数据的情况下立即返回,即只会在IO操作的第二个阶段阻塞
- 信号驱动IO和异步IO的主要区别在于前者由内核通知我们何时启动一个IO操作,在将数据从内核拷贝到用户空间过程中即IO操作的第一个阶段依旧是阻塞的,而后者是由内核通知我们IO操作何时完成,在IO操作的两个阶段都没有阻塞
知乎上有一个比较生动的例子可以说明这几种模型之间的关系。