2. 信号机制基础

信号(Signal)是 Linux 内核向进程发送的异步事件通知机制。对于驱动开发者而言,信号是连接硬件中断与用户态应用程序的关键桥梁。本章将深入剖析信号的核心概念、驱动开发中最常用的 SIGIO 信号,以及信号从内核产生到用户态处理的完整生命周期。

2.1 Linux 信号概念与分类

信号是一种软件中断,用于通知进程某个事件已经发生。每个信号都有一个唯一的编号(1~31 为标准信号,34~64 为实时信号)。信号的处理方式分为三类:

  • 默认处理:由内核执行预定义动作(终止、忽略、暂停等)。
  • 忽略:进程主动告知内核不处理该信号。
  • 捕获:进程注册自定义处理函数,信号到达时执行该函数。
驱动开发核心认知: 信号是内核向用户态“推”事件的主要手段。驱动通过 kill_fasync() 发送 SIGIO,用户态通过 fcntl() 和 signal() 接收,整个过程无需用户态轮询。

2.2 常见信号速查表

以下表格列出了驱动开发中频繁出现的信号及其含义:

信号编号 信号名称 默认动作 驱动场景
1 SIGHUP 终止 终端断开,驱动可监听以清理资源
2 SIGINT 终止 Ctrl+C,驱动可捕获以优雅退出
9 SIGKILL 终止 不可捕获,强制杀死进程
10 SIGUSR1 终止 用户自定义,驱动可用于私有通知
12 SIGUSR2 终止 用户自定义,驱动可用于私有通知
29 SIGIO 终止 异步 I/O 通知,驱动核心信号
14 SIGALRM 终止 定时器超时,驱动可配合使用
避坑提示: SIGIO 在部分架构上编号可能不同(如 MIPS 上为 29,ARM 上为 29,x86 上为 29)。始终使用宏 SIGIO 而非硬编码数字,以保证跨平台兼容性。

2.3 SIGIO:异步 I/O 通知的基石

SIGIO(也称作 SIGPOLL)是 Linux 专为异步 I/O 设计的信号。当设备文件描述符上有 I/O 事件(数据可读、可写、异常)发生时,内核可以向拥有该文件描述符的进程发送 SIGIO。驱动开发者通过以下三个步骤实现 SIGIO 通知:

  1. 注册异步通知结构:在驱动中定义 struct fasync_struct 指针。
  2. 实现 fasync() 方法:调用 fasync_helper() 管理异步通知队列。
  3. 触发信号发送:在数据到达时调用 kill_fasync() 发送 SIGIO。
关键点: 用户态必须通过 fcntl(fd, F_SETOWN, getpid()) 设置文件描述符的属主进程,否则内核不知道将信号发送给谁。

2.4 信号处理流程全景图

下图展示了从硬件中断产生到用户态信号处理函数执行的完整路径:

信号处理流程全景图 信号处理流程全景图 硬件中断 内核中断处理 驱动 kill_fasync() 信号挂起队列 进程调度点 用户态处理函数 流程说明: 1. 硬件中断触发 → 内核中断服务程序执行 2. 驱动在中断上下文中调用 kill_fasync() 发送 SIGIO 3. 信号被挂起到目标进程的信号队列中 4. 进程从内核态返回用户态时,在调度点检查并递送信号 5. 用户态注册的信号处理函数被执行

2.5 信号处理流程关键步骤详解

信号从产生到处理,经历以下四个阶段:

阶段 发生位置 关键操作 驱动开发者关注点
1. 信号产生 内核空间 kill_fasync() 或 send_sig() 确保在正确的上下文(中断/进程上下文)调用
2. 信号挂起 内核空间 将信号加入 struct sigpending 链表 无需干预,内核自动管理
3. 信号递送 内核→用户过渡 在 do_signal() 中检查挂起信号 进程必须处于运行状态才能递送
4. 信号处理 用户空间 执行 sigaction() 注册的处理函数 处理函数中只能调用异步信号安全函数

2.6 用户态信号注册代码示例

以下代码展示了用户态如何注册 SIGIO 处理函数,并与驱动建立异步通知关系:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>

/* SIGIO 信号处理函数 */
static void sigio_handler(int signo, siginfo_t *info, void *context) {
    if (signo == SIGIO) {
        printf("[用户态] 收到 SIGIO 信号,驱动有数据到达\n");
        /* 注意:此处只能调用异步信号安全函数 */
    }
}

int main(int argc, char *argv[]) {
    int fd;
    struct sigaction act;

    /* 1. 打开设备文件 */
    fd = open("/dev/mydevice", O_RDWR);
    if (fd < 0) {
        perror("open");
        exit(EXIT_FAILURE);
    }

    /* 2. 注册 SIGIO 处理函数 */
    memset(&act, 0, sizeof(act));
    act.sa_sigaction = sigio_handler;
    act.sa_flags = SA_SIGINFO;  /* 使用三参数处理函数 */
    sigaction(SIGIO, &act, NULL);

    /* 3. 设置文件描述符属主为当前进程 */
    fcntl(fd, F_SETOWN, getpid());

    /* 4. 启用异步通知模式 */
    fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_ASYNC);

    printf("[用户态] 异步通知已启用,等待驱动发送 SIGIO...\n");

    /* 5. 进入等待状态 */
    while (1) {
        pause();  /* 等待信号 */
    }

    close(fd);
    return 0;
}
代码要点:
  • SA_SIGINFO 标志使处理函数能接收 siginfo_t 结构,其中包含驱动传递的额外信息。
  • F_SETOWN 必须在使用 O_ASYNC 之前调用,否则内核可能找不到信号接收者。
  • pause() 使进程休眠直到收到信号,避免 CPU 空转。

2.7 信号处理中的常见陷阱

  • 信号丢失:标准信号不排队,如果同一信号多次到达但尚未处理,只会递送一次。实时信号(34~64)可排队,但驱动通常使用标准信号 SIGIO。
  • 重入问题:信号处理函数可能中断主程序的任意位置。处理函数中只能调用异步信号安全函数(如 write()、read() 不安全,write() 到 STDERR_FILENO 是安全的)。
  • 竞态条件:在 fcntl(F_SETOWN) 和 fcntl(F_SETFL, O_ASYNC) 之间,驱动可能发送信号但进程尚未准备好接收。建议先设置属主,再启用异步模式。
驱动开发者警示: 在中断上下文中调用 kill_fasync() 时,不要持有任何可能导致睡眠的锁。信号发送本身是原子的,但锁的竞争可能导致死锁。

2.8 信号机制与异步通知的关系

信号机制是异步通知的底层基础设施。驱动开发者需要理解:

  • 信号是内核向进程发送事件的标准方式,不仅限于 I/O 事件。
  • SIGIO 是专门为 I/O 异步通知设计的信号,驱动通过 kill_fasync() 发送。
  • 信号处理流程是异步的:驱动在中断上下文发送信号,进程在用户态上下文处理信号,两者解耦。
  • 信号处理函数执行时,进程处于用户态,可以安全地调用大部分库函数(需注意异步信号安全限制)。