Linux-C 编程 | 3 分钟快速了解信号驱动式 IO

系统
系统调用可能因为无法立即完成而被操作系统挂起,直到等待的事件发生为止。系统调用则总是立即返回,而不管事件是否已经发生。

[[384784]]

本文转载自微信公众号「嵌入式Hacker」,作者嵌入式Hacker。转载本文请联系嵌入式Hacker公众号。

一、Linux 的 5 种 IO 模型

二、如何使用信号驱动式 I/O?

三、内核何时会发送 "IO 就绪" 信号?

四、最简单的示例

五、扩展知识

一、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() 来完成:

  1. int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); 

默认情况下,这个通知信号为 SIGIO。

2> 为文件描述符的设置属主。

通过 fcntl() 的 F_SETOWN 操作来完成:

  1. fcntl(fd, F_SETOWN, pid) 

属主是当文件描述符上可执行 I/O 时,会接收到通知信号的进程或进程组。

pid 为正整数时,代表了进程 ID 号。

pid 为负整数时,它的绝对值就代表了进程组 ID 号。

3> 使能非阻塞 I/O。

通过 fcntl() 的 F_SETFL 操作来完成:

  1. flags = fcntl(fd, F_GETFL); 
  2. fcntl(fd, F_SETFL, flags | O_NONBLOCK); 

4> 使能信号驱动 I/O。

通过 fcntl() 的 F_SETFL 操作来完成:

  1. flags = fcntl(fd, F_GETFL); 
  2. 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 近乎无用。

太多情况都会产生信号,而我们又无法得知事件类型,因此这里就不再列举其产生信号的情况。

四、最简单的示例

信号处理函数:

  1. static volatile sig_atomic_t gotSigio = 0; 
  2.  
  3. static void handler(int sig) 
  4.     gotSigio = 1; 

主程序:

  1. int main(int argc, char *argv[]) 
  2.     int flags, j, cnt; 
  3.     struct termios origTermios; 
  4.     char ch; 
  5.     struct sigaction sa; 
  6.     int done; 
  7.  
  8.     /* Establish handler */ 
  9.     sigemptyset(&sa.sa_mask); 
  10.     sa.sa_flags = SA_RESTART; 
  11.     sa.sa_handler = handler; 
  12.     if (sigaction(SIGIO, &sa, NULL) == -1) { 
  13.         perror("sigaction()\n"); 
  14.         exit(1); 
  15.     } 
  16.  
  17.     /* Set owner process */ 
  18.     if (fcntl(STDIN_FILENO, F_SETOWN, getpid()) == -1) { 
  19.         perror("fcntl() / F_SETOWN\n"); 
  20.         exit(1); 
  21.     } 
  22.  
  23.     /* Enable "I/O possible" signaling and make I/O nonblocking */ 
  24.     flags = fcntl(STDIN_FILENO, F_GETFL); 
  25.     if (fcntl(STDIN_FILENO, F_SETFL, flags | O_ASYNC | O_NONBLOCK) == -1) { 
  26.         perror("fcntl() / F_SETFL\n"); 
  27.         exit(1); 
  28.     } 
  29.  
  30.     for (done = 0, cnt = 0; !done ; cnt++) { 
  31.         sleep(1); 
  32.  
  33.         if (gotSigio) { 
  34.             gotSigio = 0; 
  35.  
  36.             /* Read all available input until error (probably EAGAIN) 
  37.                or EOF */ 
  38.             while (read(STDIN_FILENO, &ch, 1) > 0 && !done) { 
  39.                 printf("cnt=%d; read %c\n", cnt, ch); 
  40.                 done = ch == '#'
  41.             } 
  42.         } 
  43.     } 
  44.     exit(0); 

运行效果:

  1. ./build/sigio  
  2. cnt=0; read a 
  3. cnt=0; read  
  4.  
  5. abc 
  6. cnt=4; read a 
  7. cnt=4; read b 
  8. cnt=4; read c 
  9. cnt=4; read  
  10.  
  11. 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模型

 

责任编辑:武晓燕 来源: 嵌入式Hacker
相关推荐

2021-03-31 09:10:25

UI设计经验

2016-12-30 13:43:35

异步编程RxJava

2023-11-28 09:17:05

Linux编程

2016-08-11 10:11:07

JavaScript函数编程

2016-08-11 10:34:37

Javascript函数编程

2024-01-09 18:09:43

模型方式DMA

2009-11-12 10:05:09

Visual C++

2020-10-18 07:13:44

Linux系统编程信号捕捉

2018-05-06 16:26:03

关联规则数据分析关联规则推荐

2020-07-17 07:44:25

云计算边缘计算IT

2021-01-27 18:15:01

Docker底层宿主机

2020-03-23 15:18:26

知识图谱Web

2020-09-25 07:34:40

Linux系统编程信号量

2017-03-01 13:06:39

Linux驱动技术DMA编程

2018-07-02 15:18:39

Linux文件系统

2017-10-26 08:53:38

前端JavaScript函数式编程

2022-09-08 11:12:09

ES6JavaScript

2024-11-07 16:09:53

2023-02-13 09:01:29

Linux驱动实例

2021-01-06 05:23:15

ServiceMesh网络阿帕网
点赞
收藏

51CTO技术栈公众号