以下所有 IO 模型都是 Linux 系统下的 网络 IO。
- 阻塞 IO (blocking IO)
- 非阻塞 IO (nonblocking IO)
- 异步 IO (asynchronous IO)
- (事件驱动) IO 复用 (IO multiplexing)
- 信号驱动 IO 复用 (signal driven IO)
以上,除异步 I/O 外,其余模型均为同步模型。
当一个 read 操作发生时,需要
- 等待描述符集合中的描述符准备好读(iff 一个从该描述符读取一个字节的请求不会阻塞时,该描述符准备好读);
- 将数据从内核拷贝到进程中;
什么是 Socket
Linux Socket 是一个接口,是 TCP/IP 网络的 API,定义了网络编程的函数和例程。
在形式上,Socket 为一个文件,在一个进程中对应一个文件描述符。
Socket 数据传输是一种特殊的 IO。
blocking IO
Linux 中,默认情况下所有 socket 都是 阻塞的。
当用户进程调用 recvfrom 系统调用时,系统内核开始准备数据。在内核等待数据的过程中,调用进程被阻塞,直到数据准备好后,内核将数据拷贝到进程的用户内存,返回结果并恢复运行。
缺点:调用进程在读写时无法进行任何其他操作,如接受和响应新的连接请求。
改进:使用多进程或多线程,为每个连接请求创建一个新进程或线程,这样每个连接的进程或线程被阻塞时,都不会影响主线程或主进程响应其他连接请求。
缺点:连接数量大时,严重占用系统资源,降低响应效率。
改进:使用线程池或连接池。线程池维护一定数量的线程,减少创建和销毁线程的频率从而节省开销,尽量重用空闲的线程。而连接池维持连接的缓存池,尽量重用已有连接,减少创建和关闭连接的频率。
缺点:当请求数量远远大于池的容量时,池无法发挥作用。
解决:面对大规模的服务请求,可以使用非阻塞接口。
nonblocking IO
用户进程调用 read 时,系统调用立即返回结果,表示内核是否准备好数据。立即返回的结果就是调用进程可以继续运行,但是需要轮询内核的状态,也就是不断重复调用 read。
该模型绝对不被推荐使用,因为循环调用大幅度提高了 CPU 占用率。而实际上,操作系统提供了更高效的接口,例如 select 多路复用模式,一次检测多个连接是否活跃。
IO multiplexing
多路复用也叫事件驱动IO (event driven IO)。epoll 是 Linux 操作系统提供的接口,使得单个进程可以同时处理多个网络连接的IO。原理是 select/epoll 会轮询它负责的所有 socket,当某个 socket 有数据到达时通知用户进程。
与 blocking IO 相似,用户进程在调用 select/epoll 时会被阻塞,直到描述符集合中有一个准备好读,此时,函数调用返回,之后,用户进程再调用 read 将数据拷贝到用户内存。
多路复用IO 模型相较于 blocking IO 的优势在于,它可以同时处理多个连接。因此,当连接数较大时,多路复用才有优势。
缺点:该模型将事件探测与事件响应夹杂在一起。如果事件响应的执行体庞大,那么就会降低事件探测的效率。
改进:加入信号,使用异步响应的 IO 操作。
signal driven IO
asynchronous IO
当用户进程调用 read 时,调用立即返回,不会阻塞用户进程。此后,内核等待数据准备好,然后将数据拷贝到用户内存,拷贝完成后,内核向用户进程发送信号,表示 read 完成。
区别
阻塞与非阻塞:进程调用系统 IO 接口时,如果数据没准备好,进程是否会被阻塞;
同步与异步:两者区别在于内核将数据拷贝到用户内存时,用户进程是否被阻塞;