基础知识
Unix的内存空间可划分为:
- 用户空间(User Space):用户空间是用户进程(即除了内核的其他东东)运行所在的内存空间。内核的任务之一就是管理用户空间中的独立的用户进程,防止它们互相干扰。
- 内核空间(Kernel Space):内核空间是内核(即操作系统的核心)运行和提供服务的地方。只有通过用户进程的系统调用(system calls)才能访问内核空间。系统调用即类Unix系统中活动进程发起的对内核服务的请求(如输入输出、进程创建等)。
概览
Unix有五种I/O模型:
- 阻塞 I/O
- 非阻塞 I/O
- I/O 多路复用 (select 和 poll)
- 信号驱动 I/O (SIGIO)
- 异步 I/O (POSIX中的aio_前缀的函数)
一般来说,一个输入操作包含了以下步骤:
- 等待数据准备就绪
- 从操作系统内核拷贝数据到进程中
对于socket的输入操作,第一步通常是等待网络中的数据到达。当数据包到达后,它被拷贝到内核的缓冲区中。第二步是将数据从内核缓冲区中拷贝到应用程序缓冲区中。
阻塞I/O
最流行的I/O模型,默认情况下所有的socket都是阻塞的。以下是一个数据报socket(datagram socket)的示例。
应用进程调用recvfrom,直到数据报到达并且被拷贝到进程缓冲区或发生错误后,该次调用才返回。换言之,进程从它调用recvfrom开始一直处于被阻塞状态。当recvfrom成功返回,应用进程就会处理该数据报。
非阻塞I/O
当应用将socket设置为非阻塞时,是在告诉内核“当我请求一个I/O操作,在不挂起使当前进程的情况下,若无法完成,直接返回错误”。
前三次调用的recvfrom并没有数据返回,所以内核立刻返回了EWOULDBLOCK的错误。当第四次调用recvfrom时,数据报准备好了,被拷贝到应用缓冲区中,并且返回成功。之后应用进程处理数据。
当应用程序像这样针对一个非阻塞的文件描述符进行recvfrom调用时,我们称之为轮询。应用程序持续地向内核发起轮询去查看操作是否准备就绪。这通常会造成CPU时间的浪费,但是这种模型只会偶尔遇到,一般出现在单一功能的系统中。
I/O多路复用
通过I/O多路复用,应用调用select 或 poll,阻塞在两者之一的系统调用而不是实际的I/O系统调用。
select允许应用进程监控多个文件描述符,应用进程阻塞在select调用处直到一个或多个文件描述符变为I/O操作就绪状态。当一个文件描述符可以执行相应的I/O操作而无需阻塞即可认为其为就绪状态。当select返回socket可读时,调用recvfrom拷贝数据报到应用缓冲区。
相比较阻塞I/O的图例,I/O多路复用看起来并不具有任何优势,实际上,它还存在一个微弱的劣势,因为除了recvfrom系统调用,它还额外需要select调用,而阻塞I/O只需要一个recvfrom系统调用。但是在等待多个文件描述符时,使用select的优势就体现出来了。
信号驱动I/O
使用信号驱动模型,可以使内核在文件描述符准备就绪时发送SIGIO信号通知应用。
应用首先启用信号驱动I/O模式的socket,并且使用sigaction系统调用设置一个信号处理器。这次系统调用立马返回并且应用进程继续运行而不会被阻塞。当数据报准备就绪后,应用进程会收到SIGIO信号。一种处理方式是通过从信号处理器中调用recvfrom读取数据报,并且通知主程序(main loop)数据已经准备好被处理;另外一种方式是直接通知主程序让其读取数据报。
不管信号如何处理,该模型的好处是当等待数据报到达时应用进程并没有被阻塞。主程序可以继续执行,信号处理器会通知它数据准备就绪或数据报准备好被读取。
异步I/O
异步I/O由POSIX标准定义,出现在各个标准中的这些函数的诸多差异在POSIX中获得了调和。总体来说,这些函数告诉内核开始操作并且当整个操作(包括从内核拷贝数据到应用缓冲区中)完成后才通知进程。这个模型与信号驱动I/O模型的主要区别是:信号驱动I/O模型中,内核告诉我们什么时候I/O操作初始化完成,而异步I/O模型中,内核告诉我们什么时候整个I/O操作完成。如下图所示。
应用调用 aio_read(POSIX标准中异步I/O函数以aio_ 或 lio_开头),并且传递文件描述符,缓冲指针,缓冲大小(这三个参数同read函数),文件偏移(同lseek相似)和当整个操作完成后的通知方式。我们假定该例中当操作完成后是内核生成信号的方式。信号直到数据被拷贝到应用缓冲区后才会生成,这是与信号驱动I/O模型不同之处。
I/O模型对比
下面是这五种I/O模型的对比。可以看出,前四种模型的主要区别是第一阶段,他们的第二阶段是相同的:当数据从内核拷贝到调用者的缓冲区时,进程阻塞在recvfrom调用处。然而,异步I/O在两个阶段的处理方式上同前四个不同。
作个类比
某次网购,
- 阻塞 I/O:下完单你在家屁事没有干等快递小哥送货上门。
- 非阻塞 I/O:你每隔几个小时就打电话问快递小哥是否送达,一段时间后,询问得知东西已放到菜鸟驿站,然后你下楼屁颠屁颠地将它取回家。
- I/O 多路复用 :你和家人剁手买了好多东西,都由菜鸟驿站代收,你到那里坐等你们的快递送达,一旦快递送到你就拿回家。(对比每个人都去菜鸟驿站等各自的快递)
- 信号驱动 I/O :下完单你就去吃鸡去了,一段时间后,快递小哥打电话说东西已放到菜鸟驿站,然后你下楼屁颠屁颠地将它取回家。
- 异步 I/O:下完单你就吃鸡去了,一段时间后,快递小哥打电话让你开门拿下快递。
同步I/O VS 异步I/O
POSIX定义了以下两种术语:
- 同步I/O操作会导致请求进程被阻塞到I/O操作完成
- 异步I/O操作不会导致请求进程被阻塞
按照上述定义,前四个I/O模型(阻塞I/O、非阻塞I/O、I/O多路复用和信号驱动I/O)都属于同步的,因为实际的I/O操作(recvfrom)阻塞了进程。只有异步I/O模型符合异步I/O定义。
参考
本文主要翻译整理至《Unix Network Programming, Vol. 1: The Sockets Networking API, Third Edition》6.2 节I/O Models