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 通知:
- 注册异步通知结构:在驱动中定义
struct fasync_struct指针。 - 实现
fasync()方法:调用fasync_helper()管理异步通知队列。 - 触发信号发送:在数据到达时调用
kill_fasync()发送SIGIO。
关键点: 用户态必须通过
fcntl(fd, F_SETOWN, getpid()) 设置文件描述符的属主进程,否则内核不知道将信号发送给谁。
2.4 信号处理流程全景图
下图展示了从硬件中断产生到用户态信号处理函数执行的完整路径:
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()发送。 - 信号处理流程是异步的:驱动在中断上下文发送信号,进程在用户态上下文处理信号,两者解耦。
- 信号处理函数执行时,进程处于用户态,可以安全地调用大部分库函数(需注意异步信号安全限制)。