一、linux 的 5 种 io 模型
阻塞式 i/o:
系统调用可能因为无法立即完成而被操作系统挂起,直到等待的事件发生为止。
非阻塞式 i/o (o_nonblock):
系统调用则总是立即返回,而不管事件是否已经发生。
i/o 复用 (select、poll、epoll):
通过 i/o 复用函数向内核注册一组事件,内核通过 i/o 复用函数把其中就绪的事件通知给应用程序。
信号驱动式 i/o (sigio):
为一个目标文件描述符指定宿主进程,当文件描述符上有事件发生时,sigio 的信号处理函数将被触发,然后便可对目标文件描述符执行 i/o 操作。
异步 i/o (posix 的 aio_ 系列函数):
异步 i/o 的读写操作总是立即返回,而不论 i/o 是否是阻塞的,真正的读写操作由内核接管。
思考一下,什么时候应该选择何种 i/o 模型?为何要这么选择?
下面重点关注信号驱动式 i/o 这一模型,其他模型可查阅文末参考书籍。
二、如何使用信号驱动式 i/o?
一般通过如下 6 个步骤来使用信号驱动式 i/o 模型。
1> 为通知信号安装处理函数。
通过 sigaction() 来完成:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
默认情况下,这个通知信号为 sigio。
2> 为文件描述符的设置属主。
通过 fcntl() 的 f_setown 操作来完成:
fcntl(fd, f_setown, pid)
属主是当文件描述符上可执行 i/o 时,会接收到通知信号的进程或进程组。
pid 为正整数时,代表了进程 id 号。
pid 为负整数时,它的绝对值就代表了进程组 id 号。
3> 使能非阻塞 i/o。
通过 fcntl() 的 f_setfl 操作来完成:
flags = fcntl(fd, f_getfl); fcntl(fd, f_setfl, flags | o_nonblock);
4> 使能信号驱动 i/o。
通过 fcntl() 的 f_setfl 操作来完成:
flags = fcntl(fd, f_getfl); fcntl(fd, f_setfl, flags | o_async);
5> 进程等待 io 就绪 信号的到来。
当 i/o 操作就绪时,内核会给进程发送一个信号,然后调用在第 1 步中安装好的信号处理函数。
6> 进程尽可能多地执行 i/o 操作。
循环执行 i/o 系统调用直到失败为止,此时错误码为 eagain 或 ewouldblock。
原因:
信号驱动 i/o 提供的是边缘触发通知,即只有当 i/o 事件发生时我们才会收到通知,
且当文件描述符收到 i/o 事件通知时,并不知道要处理多少 i/o 数据。
三、内核何时会发送 io 就绪 信号?
对于不同类型的文件描述符,情况不一样。
1> 终端
对于终端,当有新的输入时会会产生信号。
2> 管道和 fifo
对于读端,下列情况会产生信号:
数据写入到管道中;
管道的写端关闭;
对于写端,下列情况会产生信号:
对管道的读操作增加了管道中的空余空间大小。
管道的读端关闭;
3> 套接字
对于 udp 套接字,下列情况会产生信号:
数据报到达套接字;
套接字上发生异步错误;
对于 tcp 套接字,信号驱动式 i/o 近乎无用。
太多情况都会产生信号,而我们又无法得知事件类型,因此这里就不再列举其产生信号的情况。
四、最简单的示例
信号处理函数:
static volatile sig_atomic_t gotsigio = 0; static void handler(int sig) { gotsigio = 1; }
主程序:
int main(int argc, char *argv[]) { int flags, j, cnt; struct termios origtermios; char ch; struct sigaction sa; int done; /* establish handler */ sigemptyset(&sa.sa_mask); sa.sa_flags = sa_restart; sa.sa_handler = handler; if (sigaction(sigio, &sa, null) == -1) { perror(sigaction() ); exit(1); } /* set owner process */ if (fcntl(stdin_fileno, f_setown, getpid()) == -1) { perror(fcntl() / f_setown ); exit(1); } /* enable i/o possible signaling and make i/o nonblocking */ flags = fcntl(stdin_fileno, f_getfl); if (fcntl(stdin_fileno, f_setfl, flags | o_async | o_nonblock) == -1) { perror(fcntl() / f_setfl ); exit(1); } for (done = 0, cnt = 0; !done ; cnt++) { sleep(1); if (gotsigio) { gotsigio = 0; /* read all available input until error (probably eagain) or eof */ while (read(stdin_fileno, &ch, 1) > 0 && !done) { printf(cnt=%d; read %c , cnt, ch); done = ch == '#'; } } } exit(0); }
运行效果:
./build/sigio a cnt=0; read a cnt=0; read abc cnt=4; read a cnt=4; read b cnt=4; read c cnt=4; read # cnt=7; read #
该程序会先使能信号驱动 io,然后循环执行计数操作。
当有 io 就绪信号到来时,会去终端读取数据并打印出来,然后继续执行计数操作。
五、扩展知识
i/o 多路复用 、信号驱动 i/o 以及 epoll 机制可用于监视多个文件描述符。
它们并不实际执行 i/o 操作,当某个文件描述符处于就绪态,仍需采用传统的 i/o 系统调用来完成 i/o 操作。
相比 i/o 多路复用,当监视大量的文件描述符时信号驱动 i/o 有着显著的性能优势,原因是内核能够帮进程记录了正在监视的文件描述符列表。
信号驱动 i/o 的缺点:
信号的处理流程较为复杂;
无法指定需要监控的事件类型。
linux 特有的 epoll 是一个更好的选择。
六、相关参考
unix 网络编程卷1
6.2 i/o模型
25 信号驱动式i/o
linux-unix 系统编程手册
63 其他备选的i/o模型
linux 高性能服务器编程
8.3 i/o 模型
linux 多线程服务端编程_使用muduo c++网络库
7.4.1 muduo的io模型
御芯微入选2022年重庆市物联网产业协会优秀会员单位
2019年第一季度,歌尔股份的业绩预计有所好转
dfrobot最小的micro:bit多功能I/O扩展板简介
国产芯片真金质!基本半导体挑战极限高温测试“火”力全开
B628移动电源升压IC-PL2628
只需要3分钟就能让你快速了解信号驱动式IO 快来看看吧
南京市政府举办集成电路产业地标发展沙龙
全闪存存储的数据库加速场景应用
什么是电子管?电子管的简介
成为一名区块链开发人员需要具备哪些条件
华为进军美国市场遇挫,疑接受难以赢得美国这场政治战争的事实
LC滤波器的设计
物联网的范围扩大对芯片有没有什么要求
新日本无线独创环绕“eala”新产品--立体声功率放大器NUU7085
Arm推出全球首款集成功能安全的自动驾驶级处理器
工信部表态今年7月1日前取消流量“漫游费”
路由器单臂路由配置实验报告与配置方法
两轮车新规实施,电池安全再上“紧箍咒”
7款业界主流企业级SSD测评分析
cd4027应用电路图汇总(继电器/音频信号/CD4047/CD4060)