type
Post
status
Published
date
Apr 3, 2023
slug
summary
阻塞型、非阻塞型、I/O复用和信号驱动都是同步 I/O 模型。异步I/O是唯一的异步型
tags
Linux
category
学习思考
icon
password
1.Linux中的五种 I/O 模型
1.1 Blocking I/O(阻塞)
blocking I/O发起system call
recvfrom()
时,用户进程将一直阻塞等待另一端Socket的数据到来。在这种I/O模型下,我们不得不为每一个Socket都分配一个线程,这会造成很大的资源浪费。Blocking I/O优缺点都非常明显。
优点:简单易用,对于本地I/O而言性能很高。
缺点:处理网络I/O时,造成进程阻塞空等,浪费资源。

1.2 Non-Blocking I/O(非阻塞)
相对于阻塞I/O在那傻傻的等待,非阻塞I/O隔一段时间就发起 system call 看数据是否就绪(ready)。
如果数据就绪,就从kernel space 复制到 user space,操作数据;如果还没就绪,它并不会 block 用户进程,kernel 会立即返回
EWOULDBLOCK
这个错误,从用户进程角度讲,它发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。
优点:用户进程可以继续处理其他请求,不会因为数据还没就绪而阻塞
缺点:大量的 system call 使得cpu消耗增加,同时I/O任务完成的响应延迟增加了
1.3 I/O Multiplexing(I/O多路复用)
优化了非阻塞I/O大量发起system call的问题。
上面介绍的I/O模型都是直接发起I/O操作,而 I/O Multiplexing首先向 kernel 发起 system call,传入file descriptor和感兴趣的事件(readable、writable等)让kernel监测,当其中一个或多个fd数据就绪,就会返回结果。程序再发起真正的I/O操作
recvfrom
读取数据。I/O多路复用在select、epoll等系统调用和实际的 I/O 操作 (recvfrom)会阻塞进程!!!
优点:可以处理更多的 I/O 任务

在linux中,有3种system call可以让内核监测file descriptors,分别是select、poll、epoll。
select
select函数
int poll(struct pollfd *fds, nfds_t nfds, int timeout); //pollfd结构体如下 struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ };
优点:
- 几乎所有系统都支持select
- 一次让kernel监测多个fd
缺点:
- 每个fd_set的大小,受限于FD_SETSIZE(默认的1024)
- kernel返回后,需要轮询所有fd找出就绪的fd,随着fd数量增加,性能会逐渐下降
poll
poll本质上和select并没有太大不同
poll函数
int poll(struct pollfd *fds, nfds_t nfds, int timeout); //pollfd结构体如下 struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ };
优点:不受限FD_SETSIZE的值
缺点:
- 只有linux支持poll
- kernel返回后,需要轮询所有fd找出就绪的fd,随着fd数量增加,性能会逐渐下降
epoll
select和poll都是等待内核返回后轮询集合寻找就绪的fd,而 epoll 的机制,是当某个fd就绪,kernel直接返回就绪的fd。
epoll是一种性能很高的同步I/O方案。现在linux中的高性能网络框架(tomcat、netty等)都有epoll的实现。
缺点是只有linux支持epoll,BSD内核的kqueue类似于epoll。
1.4 Signal-Driven I/O(信号驱动型I/O)
从图片可以看到,进程注册一个 SIGIO 的处理函数,然后发起system call,不会阻塞进程,因此进程可以继续处理其他请求。当数据就绪时,kernel 会发送一个 SIGIO 信号给进程,此时进程发起真正的IO操作,可以在信号处理函数中调用 I/O 操作函数处理数据。

信号驱动IO在实际中并不常用。
1.5 Asynchronous I/O(异步I/O)
用户进程进行 aio_read 系统调用之后,无论 kernel 数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去处理别的请求,等到 kernel 数据准备就绪,此时不同于同步I/O,程序不需要再次发起system call,kernel 直接拷贝数据给进程,然后 kernel 向进程发送通知,告诉它 read 操作完成。
I/O的两个阶段,进程都是非阻塞的。

异步IO的两个阶段:
第一阶段:当在异步 I/O 模型下时,用户进程如果想进行 I/O 操作,只需进行系统调用,告知内核要进行 I/O 操作,此时内核会马上返回, 用户进程 就可以去处理其他的逻辑了 。
第二阶段:当内核完成所有的 I/O 操作和数据拷贝后,内核将通知我们的程序,此时数据已经在用户空间了,可以对数据进行处理了。
1.6 总结
1.6.1 非阻塞I/O和异步I/O的区别
在non-blocking I/O中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,进行system call,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。
而 Asynchronous I/O则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。
1.6.2 I/O方式总结
前四种I/O模型都是同步I/O操作,他们的区别在于第一阶段,而他们的第二阶段是一样的。在数据从内核复制到应用缓冲区期间(用户态),进程阻塞于系统调用。

1.6.3 I/O模型分类
阻塞型、非阻塞型、I/O复用和信号驱动都是同步 I/O 模型。异步I/O是唯一的异步型。

参考文献:
- 作者:GreekLee
- 链接:https://www.rusellwest.top/article/c37667a7-52b0-4e8b-a5f9-aedad1da1922
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。