I/O 模型
02 Nov 2016Overview
I/O 模型是 操作系统 抽象出来的概念, 方便操作系统统一的管理. 各种各样的模型之间到底有什么区别呢? 阻塞 (blocking) 和 同步 (synchronous) 是什么关系?
I/O 过程
简单来讲, I/O 的过程分为两个部分 (以读取为例):
- 等待数据就绪
- 把数据从内核拷贝到用户进程空间
用 socket 来举例, 第一个步骤就是等待网卡接收到数据, 当接收到数据, 处理器把数据拷贝到内核的缓冲区. 第二个步骤就是把数据从内核的缓冲区拷贝到用户程序的缓冲区.
第一个步骤还是很直观的, 但是为什么要有第二个步骤呢? 数据拷来拷去不是很没效率么? 这牵扯到处理器的特权级别了. 现代的处理器在运行时是分为不同的级别的. 低级别程序会受到一定的限制, 比如只能访问一部分内存, 只能执行特定的指令. 所以需要将内核的数据拷贝到用户进程能访问的空间去.
五种 I/O 模型
下面的讨论主要基于 UNIX 操作系统.
Blocking I/O
最常见的 I/O 模型就是阻塞型的. 以 socket 来举例, 当我们调用 recv()
, send()
, connect()
, accept()
这四个方法时,
线程都会阻塞住. 也就是说从等待数据就绪到数据拷贝到用户进程都是阻塞的.
Nonblocking I/O
当我们把 socket 设置成 nonblocking 的, 这时候调用 recv()
, send()
, connect()
, accept()
, 如果没有数据会返回 EWOULDBLOCK
错误而不是阻塞住, 如果有数据那么就会把数据从内核拷贝到用户进程.
虽然叫 nonblocking 但是在把数据从内核拷贝到用户进程 (写数据是从用户进程到内核) 时还是会阻塞住.
I/O multiplexing
在这种模型下, 我们一个线程能 同时 处理多个 I/O. 还是以 socket 为例,
我们将一个 socket 的集合传给 select()
函数, 调用 select()
后线程会阻塞住,
当传入的集合中有一个 socket 有数据了, select()
就会返回, 然后通过相应的方法将数据读取, 同样的,
在读取数据时线程会被阻塞住. 传给 select()
的 socket 可以是 blocking 的.
因为阻塞与否影响的只是 recv()
, send()
, connect()
, accept()
这四个函数. 但是最好是 nonblocking 的,
因为即使 select()
返回了, socket 也可能在调用那四个函数时被阻塞.
(见 select with blocking and non-blocking socket)
select, poll, epoll
这三个函数是 I/O 多路复用模型常用的函数, 各自实现的功能差不多.
select 传入的文件描述符的集合的大小是有限制的, 通常是 1024. poll 和 epoll 没有限制 (主要的限制是你的硬件资源了). select 和 poll 都会随着传入的文件描述符的数量增多而效率变低, epoll 的效率只和活跃的文件描述符数量有关. epoll 使用消息驱动来通知用户进程数据就绪.
epoll 怎么实现的我还蛮好奇的, 为什么 epoll 通常情况下效率要高呢? epoll 比 select 高效的原因
Signal-Driven I/O
在这个模型里, 我们向操作系统注册 handler (通过 sigaction
系统调用), 当数据就绪时,
handler 会被操作系统调用, 这时候我们再去调用相应的方法获取数据 (会阻塞住).
Asynchronous I/O
和 Signal-Driven 类似, 我们需要向操作系统注册一个 handler. 这个模型和 Signal-Driven 最大的不同是, 当操作系统调用我们的 handler 时, 数据已经从内核态拷贝到用户进程了 (显然我们在注册 handler 的时候需要让操作系统知道把数据拷贝到哪里).
Synchronous I/O versus Asynchronous I/O
POSIX (Portable Operating System Interface) 其实只定义了两种类型的 I/O:
- 同步的: 进行 I/O 操作时, 之前提到的两个过程中只要有一个会阻塞就算同步的
- 异步的: 进行 I/O 操作时, 之前提到的两个过程都不会阻塞就是异步的
所以, 上面提到的五种 I/O 模型, 只有 Asynchronous I/O 是属于异步的, 其他的都属于同步的, 因为他们在把数据从内核拷贝到用户进程时都会阻塞住.
举个例子
找资料过程中发现用钓鱼来类比上面的几个模型是很合适的. 因为钓鱼成两个步骤, 等鱼儿上钩 (数据就绪), 把鱼儿放进桶里 (把数据从内核拷贝到用户进程)
- Blocking: 下饵之后就一直等着直到鱼儿上钩, 再把鱼儿放到桶里.
- Nonblocking: 下饵之后隔一会就来检查下有没有鱼儿上钩, 没有就做其他事情, 有就把鱼儿放进桶里.
- I/O multiplexing: 同时用很多鱼竿, 然后在旁边看 (不能干其他事情), 哪个鱼竿有鱼了就捞上来放进桶里.
- Signal-Driven: 用的是比较高级的鱼竿, 当有鱼上钩时会发出声音提醒 (可以专心做其它事情, 不用像 nonblocking 一样亲自来检查), 听到声音提醒之后把鱼儿捞起来放进桶里.
- Asynchronous: 直接雇了个人 (操作系统), 当鱼上钩后捞起来放进桶里, 然后被雇佣的人会过来告诉你鱼钓到了.