五种 IO 模型
一共有五种 IO 模型
- 阻塞 IO
- 非阻塞 IO
- 多路复用 IO
- 信号驱动 IO
- 异步 IO
其中,前面4种是同步IO,最后一个是异步IO
IO 简述
IO (Input/Output,输入/输出)即数据的读取(接收)或写入(发送)操作,通常用户进程中的一个完整IO分为两阶段:用户进程空间<-->内核空间、内核空间<-->设备空间(磁盘、网络等)。IO有内存IO、网络IO和磁盘IO三种,通常我们说的IO指的是后两者。
LINUX中进程无法直接操作I/O设备,其必须通过系统调用请求kernel来协助完成I/O动作;内核会为每个I/O设备维护一个缓冲区。
对于一个输入(读取到内存)操作来说,进程IO系统调用后,内核会先看缓冲区中有没有相应的缓存数据,没有的话再到设备中读取,因为设备IO一般速度较慢,需要等待;内核缓冲区有数据则直接复制到进程空间。
所以,对于一个网络输入(读取网络资源)的操作,通常包括两个部分
- 等待网络数据到达网卡 --> 读取到
内核空间
(这个过程就是准备数据) - 从内核缓冲区复制数据到
进程空间
- 然后具体的用户函数,把结果返回。
IO 操作发生时一般涉及两个对象,一个是调用这个IO的 process (or thread) ,另一个就是系统内核 (kernel)。
IO 模型介绍
我们在介绍IO模型的时候,是根据IO操作的两个阶段,是否被锁了来分类的。
下面我们使用获取一个网络资源来举例(读取数据),假设是 read 方法。
阻塞 IO
- 准备数据阶段(用户进程阻塞)
- 复制数据阶段(用户进程阻塞)
当用户进程在读取一个网络资源的时候,内核就开始了IO的第一个阶段:准备数据。
对于网络IO来说,数据一开始还没有准备好(比如还没有收到完整的UDP包),这个时候内核就要等待足够数据到来。
当数据没有准备好,在用户进程这边,用户的进程就会被阻塞,一直等待数据准备好。
当数据准备好了(数据在内核空间中
),然后就需要把数据复制到用户进程空间
,在复制数据的这个过程中,用户进程仍然是阻塞的。
总结:阻塞IO,就是指当调用读取数据的函数(比如 read),这个函数不立马返回结果,而是阻塞当前进程,直到数据被复制到用户进程空间
或者是超时出错才解除阻塞,并返回结果。
缺点:实际上,除非特别指定,几乎所有的IO接口 ( 包括socket接口 ) 都是阻塞型的。这给网络编程带来了一个很大的问题,如在调用 recv(1024) 的同时,线程将被阻塞,在此期间,线程将无法执行任何运算或响应任何的网络请求。
一个简单的解决方案
使用多线程或者多进程,多线程(或多进程)的目的是让每个连接都拥有独立的线程(或进程),这样任何一个连接的阻塞都不会影响其他的连接。或者是使用线程池,进程池。
非阻塞 IO
- 准备数据阶段(用户进程非阻塞)
- 复制数据阶段(用户进程阻塞)
如果采用非阻塞的方式读取数据,当用户调用 read 方法的时候。
如果数据还没有准备好,内核就立马返回一个结果给用户进程(error),用户进程不会阻塞,用户知道数据还没有准备好,那么就可以过一段时间再调用 read 方法进行获取数据,而且在两次 read 的时间间隔中,用户进程还可以做其他操作。
一旦数据准备好了(在内核空间中
),而且恰好用户调用了 read 方法,那么数据就会被复制到用户进程空间
并返回给用户,复制到用户进程空间
这个过程也是阻塞的。
总结:在非阻塞 IO 中,用户在调用 read 方法后,进程没有被阻塞。内核会立马返回数据给进程,这个数据可能是error提示,也可能是最终的结果。如果要得到最终的结果,就需要用户进程主动的多次调用 read 方法。
重复调用 read 方法的过程,称为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,==拷贝数据整个过程,进程仍然是属于阻塞的状态==。
缺点:轮询调用 read 是很费CPU资源的,所以一般我们会在代码中加入 time.sleep(2)。但是加入了 sleep 以后,任务的执行时间长了,因为我们不确定,任务多久执行完成。
多路复用 IO
暂时不考虑
信号驱动 IO
当调用 read 函数的时候,准备数据的过程中用户线程不阻塞,用户线程可以去做其他事情。等到数据准备完了,用户进程会收到一个SIGIO信号,然后可以在信号处理函数中处理数据。
复制数据的阶段,仍然是阻塞的。
异步 IO
相对于同步IO,异步IO不是顺序执行的。用户调用 read 之后,准备数据阶段和复制数据阶段,都是非阻塞的。
数据准备好了后,内核直接复制数据给用户进程空间,然后从内核向进程发送通知,然后用户进程处理数据。