欢迎光临
我们一直在努力

《UNIX 网络编程-卷1》阅读笔记23: 信号驱动 I/O

作者: andylin02
学习章节: 第二十二章 信号驱动I/O
关键词: 信号驱动I/O, SIGIO, O_ASYNC, F_SETOWN, fcntl, UDP回射服务器, 异步通知, 信号处理函数


一、章节概述

1.1 本章焦点

第二十二章介绍的是五种I/O模型中的信号驱动式I/O(Signal-Driven I/O)。在前面的章节中,我们讨论了阻塞式I/O、非阻塞式I/O和I/O复用模型。它们虽然各有特点,但都有一个共同点——无论是主动阻塞等待数据,还是通过select轮询,都需要应用程序主动“询问”内核I/O的状态。

信号驱动I/O改变了这种模式:由内核主动通知进程——当描述符就绪时,内核发送SIGIO信号给进程。进程不再需要主动轮询或阻塞等待,只需注册信号处理函数,就可以在I/O事件发生时被动接收通知。这使得进程可以去做其他事情,实现真正的异步通知。

💡 本章核心价值:读完第二十二章,你将能够——

  • 深刻理解信号驱动I/O的工作原理及其在五种I/O模型中的定位
  • 掌握将套接字设置为信号驱动模式的三个关键步骤
  • 使用SIGIO信号和信号处理函数编写UDP回射服务器
  • 理解UDP与TCP在信号驱动I/O上的行为差异
  • 了解信号驱动I/O在实际工程中的局限性

1.2 本章内容结构

节号标题核心内容
22.1 概述 信号驱动I/O在五种I/O模型中的位置
22.2 套接口上的信号驱动I/O 设置套接字为信号驱动模式的三个步骤
22.3 使用SIGIO的UDP回射服务器程序 完整示例:SIGIO驱动的UDP回射服务器
22.4 小结 信号驱动I/O的优缺点与适用场景
22.5 习题 课后思考题

二、五种I/O模型回顾

2.1 信号驱动I/O在五种模型中的位置

在《UNIX网络编程》第6章中,Stevens总结了五种I/O模型。下表回顾了这五种模型的本质区别:

I/O模型第一阶段(等待数据)第二阶段(拷贝数据)通知方式
阻塞式I/O 进程阻塞等待 进程阻塞拷贝 操作完成才返回
非阻塞式I/O 立即返回(轮询) 进程阻塞拷贝 轮询检查
I/O复用(select/poll) 阻塞于select 进程阻塞拷贝 select返回通知
信号驱动I/O 不阻塞,内核就绪时发信号 进程阻塞拷贝 SIGIO信号通知
异步I/O 不阻塞 不阻塞(内核完成拷贝后通知) 完成时通知

2.2 信号驱动I/O vs 异步I/O的核心区别

信号驱动I/O常被误认为是异步I/O,但两者有本质区别:

对比维度信号驱动I/O异步I/O
内核通知的时机 数据已就绪,可以启动I/O操作 I/O操作已完成(数据已拷贝到用户空间)
应用程序仍需调用I/O函数 ✅ 需要(收到SIGIO后调用recvfrom) ❌ 不需要(数据已在用户缓冲区)
数据拷贝阶段是否阻塞 ✅ 阻塞(recvfrom调用) ❌ 不阻塞(内核完成拷贝后通知)

💡 关键理解:信号驱动I/O的通知内容是“数据准备好了,你可以来读了”;异步I/O的通知内容是“数据已经帮你读好了,你可以直接用了”。正因如此,信号驱动I/O本质上仍属于同步I/O范畴。

2.3 信号驱动I/O的独特优势

与I/O复用(select和poll)相比,信号驱动I/O的优势在于:免去了select的阻塞与轮询,当有活跃套接字时,由注册的handler直接处理。在需要处理大量描述符但活跃连接较少的场景中,信号驱动I/O可以避免每次都要遍历所有描述符的开销。

三、套接口上的信号驱动I/O

3.1 三个基本步骤

为使一个套接字能够使用信号驱动I/O,需要执行以下三个步骤:

步骤操作函数/命令说明
建立SIGIO信号处理程序 signal() 或 sigaction() 注册处理SIGIO信号的函数
设置套接字属主 fcntl 的 F_SETOWN 命令 指定接收SIGIO信号的进程或进程组
激活信号驱动I/O fcntl 的 F_SETFL 命令 设置 O_ASYNC 文件状态标志

3.2 设置套接字属主

套接字属主指定了当套接字上有事件发生时,内核应该将信号发送给哪个进程或进程组。

// 将当前进程设置为套接字的属主
fcntl(sockfd, F_SETOWN, getpid());

3.3 激活信号驱动I/O

设置 O_ASYNC 标志后,内核会在套接字就绪时发送SIGIO信号。通常需要与 O_NONBLOCK 标志一起设置,以便在信号处理函数中可以非阻塞地循环读取数据。

int flags;

flags = fcntl(sockfd, F_GETFL, 0);
flags |= O_NONBLOCK | O_ASYNC;
fcntl(sockfd, F_SETFL, flags);

3.4 SIGIO信号的产生条件

UDP套接字:在UDP上,SIGIO信号会在以下两个事件时产生:

  • 数据报到达套接字
  • 套接字上发生异步错误
  • TCP套接字:SIGIO信号在TCP上的产生条件较为复杂,因此信号驱动I/O模型在实际应用中主要用在UDP套接字上,在TCP套接字上几乎没有什么使用。

    四、完整源代码示例

    4.1 使用SIGIO的UDP回射服务器

    以下是一个使用SIGIO信号驱动的UDP回射服务器程序。程序启动后,主循环进入睡眠状态(或执行其他任务),当数据报到达时,内核发送SIGIO信号,信号处理函数负责接收数据并回射。

    #include "unp.h"
    #include <sys/ioctl.h>

    static int sockfd;

    // 环形缓冲区数据结构
    #define MAXDATAGRAM 8192
    #define MAXDGRAMQUEUE 64

    struct dg_packet {
    struct sockaddr_storage sa;
    socklen_t salen;
    char buf[MAXDATAGRAM];
    };

    static struct dg_packet queue[MAXDGRAMQUEUE];
    static volatile int quelen = 0; // 当前队列中的数据报数量
    static volatile int qput = 0;
    static volatile int qget = 0;
    static volatile int queuedatagrams = 0;

    // 信号处理函数
    void sig_io(int signo)
    {
    int n;
    struct sockaddr_storage cliaddr;
    socklen_t clilen;
    char buf[MAXDATAGRAM];

    for ( ; ; ) {
    clilen = sizeof(cliaddr);
    // 非阻塞读取,避免信号不排队导致的数据丢失
    n = recvfrom(sockfd, buf, MAXDATAGRAM, MSG_DONTWAIT,
    (SA *)&cliaddr, &clilen);
    if (n < 0) {
    if (errno == EWOULDBLOCK)
    break; // 没有更多数据可读
    else
    err_sys("recvfrom error");
    }

    // 将数据报存入环形缓冲区
    if (queuedatagrams >= MAXDGRAMQUEUE)
    err_quit("queue overflow");
    queue[qput].salen = clilen;
    memcpy(&queue[qput].sa, &cliaddr, clilen);
    memcpy(queue[qput].buf, buf, n);
    queue[qput].buf[n] = 0;
    qput = (qput + 1) % MAXDGRAMQUEUE;
    queuedatagrams++;
    }
    }

    int main(int argc, char **argv)
    {
    struct sockaddr_in servaddr;
    int on = 1;

    // 1. 创建UDP套接字
    sockfd = Socket(AF_INET, SOCK_DGRAM, 0);

    // 2. 绑定服务器地址
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERV_PORT);
    Bind(sockfd, (SA *)&servaddr, sizeof(servaddr));

    // 3. 设置非阻塞模式(防止信号不排队时阻塞)
    int flags = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

    // 4. 建立SIGIO信号处理程序
    Signal(SIGIO, sig_io);

    // 5. 设置套接字属主为当前进程
    Fcntl(sockfd, F_SETOWN, getpid());

    // 6. 激活信号驱动I/O(设置O_ASYNC标志)
    flags = fcntl(sockfd, F_GETFL, 0);
    Fcntl(sockfd, F_SETFL, flags | O_ASYNC);

    // 7. 主循环:处理队列中的数据报
    for ( ; ; ) {
    while (queuedatagrams == 0)
    pause(); // 无数据时进入睡眠等待SIGIO信号

    // 处理队列中的一个数据报
    Sendto(sockfd, queue[qget].buf, strlen(queue[qget].buf), 0,
    (SA *)&queue[qget].sa, queue[qget].salen);
    qget = (qget + 1) % MAXDGRAMQUEUE;
    queuedatagrams;
    }
    }

    4.2 代码说明

    设计要点说明
    非阻塞recvfrom 信号处理函数中使用MSG_DONTWAIT标志,防止信号不排队时阻塞
    环形缓冲区 使用队列存储多个数据报,避免信号处理函数处理时间过长
    主循环睡眠等待 无数据时pause(),收到SIGIO信号后唤醒处理队列中的数据
    原子操作volatile 队列指针和计数使用volatile修饰,确保编译器不会优化掉对内存的访问

    4.3 简化的UDP时间服务器示例

    以下是一个使用SIGIO信号驱动I/O实现的简化版UDP时间服务器,完整演示了三个关键步骤:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <signal.h>
    #include <time.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <fcntl.h>

    #define PORT 9999
    #define MAXLINE 1024

    static int sockfd;

    void sig_io_handler(int signo)
    {
    char buf[MAXLINE];
    struct sockaddr_in cliaddr;
    socklen_t clilen = sizeof(cliaddr);
    time_t ticks;

    // 接收客户端请求
    if (recvfrom(sockfd, buf, MAXLINE, 0, (struct sockaddr *)&cliaddr, &clilen) > 0) {
    ticks = time(NULL);
    snprintf(buf, sizeof(buf), "%.24s\\r\\n", ctime(&ticks));
    sendto(sockfd, buf, strlen(buf), 0, (struct sockaddr *)&cliaddr, clilen);
    printf("Served client: %s:%d\\n",
    inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
    }
    }

    int main()
    {
    struct sockaddr_in servaddr;

    // ① 创建UDP套接字并绑定
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(PORT);
    bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    // ② 建立SIGIO信号处理程序
    signal(SIGIO, sig_io_handler);

    // ③ 设置套接字属主
    fcntl(sockfd, F_SETOWN, getpid());

    // ④ 激活信号驱动I/O
    int flags = fcntl(sockfd, F_GETFL, 0);
    fcntl(sockfd, F_SETFL, flags | O_ASYNC);

    printf("UDP time server started on port %d\\n", PORT);

    // ⑤ 主循环:只需等待信号
    for ( ; ; )
    pause();

    return 0;
    }

    五、关键图表

    5.1 信号驱动I/O工作流程图

    ┌─────────────────────────────────────────────────────────────────────────────┐
    │ 信号驱动I/O(Signal-Driven I/O)工作流程 │
    ├─────────────────────────────────────────────────────────────────────────────┤
    │ │
    │ 进程 内核 │
    │ │ │ │
    │ │ ① 安装SIGIO信号处理程序 │ │
    │ │ ② fcntl(F_SETOWN) 设置套接字属主 │ │
    │ │ ③ fcntl(F_SETFL | O_ASYNC) 激活信号驱动I/O │ │
    │ │────────────────────────────────────────────────────────→│ │
    │ │ │ │
    │ │ │ 数据尚未到达 │
    │ │ │ │
    │ │ ④ 进程继续执行其他任务 │ │
    │ │ │ │
    │ │ │ ⑤ 数据到达 │
    │ │ │ │
    │ │ ⑥ SIGIO信号(就绪通知) │ │
    │ │←────────────────────────────────────────────────────────│ │
    │ │ │ │
    │ │ ⑦ 信号处理函数中调用recvfrom │ │
    │ │────────────────────────────────────────────────────────→│ │
    │ │ │ │
    │ │ │ ⑧ 拷贝数据 │
    │ │ │ │
    │ │ ⑨ recvfrom返回 │ │
    │ │←────────────────────────────────────────────────────────│ │
    │ │ │ │
    │ │ ⑩ 信号处理函数返回,进程继续执行 │ │
    │ │ │ │
    │ │ 进程不被阻塞 │
    │ │
    │ 💡 内核在数据就绪时主动通知进程,而非进程主动询问 │
    │ │
    └─────────────────────────────────────────────────────────────────────────────┘

    5.2 UDP信号驱动I/O时序图

    ┌─────────────────────────────────────────────────────────────────────────────┐
    │ UDP信号驱动I/O服务器 – 完整时序图 │
    ├─────────────────────────────────────────────────────────────────────────────┤
    │ │
    │ 主进程 信号处理函数 内核 │
    │ │ │ │ │
    │ │ socket() │ │ │
    │ │───────────────────────────┼───────────────────────────→│ │
    │ │ │ │ │
    │ │ bind() │ │ │
    │ │───────────────────────────┼───────────────────────────→│ │
    │ │ │ │ │
    │ │ signal(SIGIO, sig_io) │ │ │
    │ │───────────────────────────→│ │ │
    │ │ │ (安装信号处理函数) │ │
    │ │ │ │ │
    │ │ fcntl(F_SETOWN, pid) │ │ │
    │ │───────────────────────────┼───────────────────────────→│ │
    │ │ │ │ │
    │ │ fcntl(F_SETFL, O_ASYNC) │ │ │
    │ │───────────────────────────┼───────────────────────────→│ │
    │ │ │ │ │
    │ │ pause() / 主循环 │ │ │
    │ │ │ │ │
    │ │ │ │ 数据报到达 │
    │ │ │ │ │
    │ │ │ SIGIO信号 ───────→│ │
    │ │ (进程被唤醒) │ │ │
    │ │←──────────────────────────│ │ │
    │ │ │ │ │
    │ │ (在信号处理上下文中执行) │ │ │
    │ │ │ │ │
    │ │ recvfrom() ─────────────────→│ │
    │ │ │ │ │
    │ │ ←───────────────────────── 数据│ │
    │ │ │ │ │
    │ │ sendto() ───────────────────→│ │
    │ │ │ │ │
    │ │ (信号处理函数返回) │ │ │
    │ │ │ │ │
    │ │ 继续循环或再次pause() │ │ │
    │ │
    └─────────────────────────────────────────────────────────────────────────────┘

    六、UDP vs TCP:信号驱动I/O的差异

    套接字类型SIGIO产生条件实际应用
    UDP套接字 数据报到达;异步错误发生 ✅ 常用,简单清晰
    TCP套接字(监听) 新连接到达(accept就绪) ⚠️ 可用但较少用
    TCP套接字(已连接) 多种复杂情况(数据到达、连接断开、带外数据等) ❌ 几乎不用,条件复杂

    💡 为什么TCP上很少使用信号驱动I/O?

    信号驱动I/O在TCP上的应用存在诸多限制:首先,TCP是字节流协议,数据到达的边界不明确;其次,SIGIO信号在TCP上可能由多种原因触发(普通数据到达、带外数据到达、连接断开、错误发生等),信号处理函数需要处理多种情况,增加了复杂度;再者,Linux信号不排队,多个事件可能合并为一个信号,容易导致数据丢失。因此在实际工程中,TCP上的高并发通常使用I/O复用(select/poll/epoll)而非信号驱动I/O。

    七、常见问题与注意事项

    7.1 常见错误速查表

    问题原因解决方案
    收不到SIGIO信号 未设置套接字属主 调用fcntl(F_SETOWN, getpid())
    SIGIO信号只触发一次 未持续设置O_ASYNC标志 确认fcntl设置了O_ASYNC
    数据报丢失 Linux信号不排队 在信号处理函数中使用非阻塞循环读取,直到EWOULDBLOCK
    程序收到SIGIO后崩溃 信号处理函数中调用了非异步安全函数 只调用异步信号安全函数,或将数据放入队列由主循环处理
    TCP上SIGIO混乱 触发条件过多,难以区分 改用I/O复用方案

    7.2 编程注意事项

    注意事项说明
    信号不排队 Linux默认不将SIGIO信号排队,多个事件可能合并为一个信号。必须在信号处理函数中循环读取所有待处理数据
    异步信号安全 信号处理函数中只能调用异步信号安全的函数(如recvfrom、sendto、write),不可调用printf等非安全函数
    数据队列 最佳实践是在信号处理函数中将数据放入队列,由主循环处理
    TCP场景谨慎 信号驱动I/O在TCP上几乎没有什么使用,如需在TCP上使用,需谨慎处理
    端口绑定时间 设置O_ASYNC之前,必须先设置套接字属主F_SETOWN

    7.3 与其他模型的对比

    I/O模型编程复杂度适用场景
    阻塞式I/O 简单程序、低并发
    I/O复用(select/epoll) 高并发TCP服务器
    信号驱动I/O UDP服务器、对响应延迟敏感的场景
    异步I/O 高性能文件I/O、特殊网络场景

    💡 实际应用现状:信号驱动I/O模型在实际中并不常用。其主要原因包括:信号不排队的限制、在TCP上行为复杂、以及epoll/kqueue等更高效的事件驱动机制的普及。但对于UDP服务器,信号驱动I/O仍是一种简洁有效的实现方式。

    八、本章小结

    8.1 核心知识点回顾

    知识点关键要点
    信号驱动I/O本质 内核在描述符就绪时发送SIGIO信号,主动通知进程
    三个关键步骤 建立信号处理程序 → 设置套接字属主(F_SETOWN)→ 激活信号驱动I/O(O_ASYNC)
    UDP vs TCP UDP上SIGIO产生条件简单清晰,是主要应用场景;TCP上产生条件复杂,几乎不使用
    信号不排队 Linux中SIGIO信号不排队,必须在信号处理函数中循环读取所有数据
    信号处理函数限制 只能调用异步信号安全的函数,推荐将数据放入队列由主循环处理
    vs异步I/O 信号驱动I/O通知“数据就绪”(仍需应用读取);异步I/O通知“操作完成”(无需应用读取)

    8.2 本章思维导图

    第二十二章 信号驱动I/O
    ├── I/O模型定位
    │ ├── 五种I/O模型中的第四种
    │ ├── 数据就绪时由内核主动发送SIGIO信号
    │ └── vs 异步I/O:通知时机不同(就绪 vs 完成)
    ├── 三个关键步骤
    │ ├── ① 建立SIGIO信号处理程序(signal/sigaction)
    │ ├── ② 设置套接字属主(fcntl F_SETOWN)
    │ └── ③ 激活信号驱动I/O(fcntl F_SETFL | O_ASYNC)
    ├── UDP套接字使用
    │ ├── 数据报到达时产生SIGIO
    │ ├── 异步错误时产生SIGIO
    │ └── 主要应用场景
    ├── TCP套接字使用
    │ ├── 监听套接字:新连接到达时产生
    │ ├── 已连接套接字:多种复杂情况
    │ └── 实际中几乎不使用
    ├── 编程注意事项
    │ ├── 信号不排队 → 循环非阻塞读取
    │ ├── 异步信号安全 → 避免printf等非安全函数
    │ ├── 数据队列设计 → 信号处理函数只入队,主循环出队处理
    │ └── UDP服务器中设置O_NONBLOCK
    └── 实践示例
    ├── UDP回射服务器(SIGIO驱动)
    └── UDP时间服务器(简化版)

    九、下一章预告

    📌 下一篇:《UNIX网络编程》读书笔记(二十三):第二十三章 线程

    第二十三章将详细讲解:

  • 线程概述:为什么需要线程?线程与进程的对比(资源共享、创建开销、调度效率)
  • 基本线程函数:
    • pthread_create:创建新线程
    • pthread_join:等待线程终止
    • pthread_self:获取自身线程ID
    • pthread_detach:分离线程
    • pthread_exit:线程终止
  • 线程同步机制:
    • 互斥锁(Mutex):保护临界区
    • 条件变量(Condition Variable):线程间事件通知
    • 读写锁(RWLock):读共享、写独占
  • 线程安全的网络编程:
    • 多线程TCP回射服务器
    • 线程池模型的设计与实现
    • 线程与I/O复用的结合
  • 线程与信号的交互:信号掩码、异步信号安全
  • 学习目标:学完第二十三章后,你将能够——

    • 使用POSIX线程API编写多线程网络程序
    • 使用互斥锁和条件变量解决线程间同步问题
    • 实现线程池模型,提高服务器的并发处理能力
    • 编写线程安全的TCP并发服务器

    敬请期待!

    参考资料

  • W. Richard Stevens, Bill Fenner, Andrew M. Rudoff. 《UNIX网络编程 卷1:套接字联网API(第3版)》. 北京:人民邮电出版社
  • UNPv1第二十二章:信号驱动IO,腾讯云开发者社区,https://cloud.tencent.com.cn/developer/article/1393692
  • UNP Chapter 22 – 信号驱动I/O,CSDN,https://blog.csdn.net/jianwen_li/article/details/50442078
  • 《UNIX网络编程 卷1》 笔记: 信号驱动式I/O,CSDN,https://blog.csdn.net/weixin_41923658/article/details/108222200
  • 信号驱动IO模型 — Linux IO模型漫谈,阿里云开发者社区,https://developer.aliyun.com/article/414074
  • 25.3 UDP Echo Server Using SIGIO,UNP在线版,https://books.gigatux.nl/mirror/unixnetworkprogramming/0131411551_ch25lev1sec3.html
  • Linux下基于UDP的信号式驱动IO编程,CSDN,https://blog.csdn.net/chexlong/article/details/105700060

  • 本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
    👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!

    赞(0)
    未经允许不得转载:171主机测评 » 《UNIX 网络编程-卷1》阅读笔记23: 信号驱动 I/O
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址