在Linux系统编程中,进程信号是最基础也是最重要的进程间通信机制之一。作为一名系统程序员,理解信号的本质和工作原理至关重要。信号本质上是一种软件中断,它提供了一种异步事件通知机制,允许操作系统和进程之间、进程与进程之间进行简单的信息传递。
信号的异步性体现在两个方面:首先,信号的产生是异步的,可能在任何时间点由各种事件触发;其次,信号的处理时机也是异步的,进程通常不会立即响应信号,而是在从内核态返回到用户态时才会检查并处理待处理的信号。
这种异步机制带来了几个关键特性:
一个完整的信号生命周期包含三个阶段:
值得注意的是,Linux内核使用位图来管理信号的未决(pending)和阻塞(block)状态。每个信号对应两个比特位:一个表示是否处于未决状态(已产生但未处理),另一个表示是否被阻塞(暂时不递达)。
Linux系统定义了31个标准信号(编号1-31),每个信号都有特定的用途和默认行为。以下是几个关键信号及其特性:
| 信号编号 | 信号名称 | 触发方式 | 默认动作 | 可否捕获 |
|---|---|---|---|---|
| 2 | SIGINT | Ctrl+C | Term | 是 |
| 3 | SIGQUIT | Ctrl+\ | Core | 是 |
| 9 | SIGKILL | kill -9 | Term | 否 |
| 11 | SIGSEGV | 非法内存访问 | Core | 是 |
| 14 | SIGALRM | alarm()到期 | Term | 是 |
| 17 | SIGCHLD | 子进程状态改变 | Ign | 是 |
| 19 | SIGSTOP | Ctrl+Z | Stop | 否 |
Linux为每个信号提供了三种基本处理方式:
默认处理(SIG_DFL):系统预定义的行为,通常是终止进程(Term)、终止并生成core文件(Core)、忽略(Ign)或暂停进程(Stop)
忽略信号(SIG_IGN):完全丢弃该信号,不做任何处理。但需要注意,SIGKILL和SIGSTOP这两个信号不能被忽略或捕获。
自定义处理:通过signal()或sigaction()注册用户定义的处理函数。处理函数原型为:
c复制void handler(int signo);
虽然Term和Core都会终止进程,但它们有一个关键区别:
要生成core文件,需要满足两个条件:
在终端环境中,特定的键盘组合会产生信号:
| 按键组合 | 产生信号 | 典型用途 |
|---|---|---|
| Ctrl+C | SIGINT | 中断前台进程 |
| Ctrl+\ | SIGQUIT | 强制退出并生成core |
| Ctrl+Z | SIGTSTP | 暂停前台进程 |
需要注意的是,这些键盘信号仅对前台进程有效。当进程在后台运行时,终端输入会直接交给shell处理。
Linux提供了多个系统调用用于发送和处理信号:
c复制// 向指定进程发送信号
int kill(pid_t pid, int sig);
// 向当前进程发送信号
int raise(int sig);
// 设置定时器,到期发送SIGALRM
unsigned int alarm(unsigned int seconds);
// 暂停进程直到收到信号
int pause(void);
kill()系统调用功能强大,pid参数的不同取值可以实现不同的发送策略:
CPU执行指令时检测到的异常会被Linux转换为信号发送给进程:
SIGFPE(浮点异常):
SIGSEGV(段错误):
SIGILL(非法指令):
这些信号的产生流程如下:
code复制CPU检测到异常 → 触发中断 → 内核处理中断 → 转换为信号 → 发送给进程
某些系统状态变化也会触发信号:
alarm()函数的典型用法是设置一个单次定时器,常用于实现超时机制:
c复制alarm(5); // 5秒后发送SIGALRM
Linux内核为每个进程维护三个关键数据结构来管理信号:
当信号产生时,内核会:
Linux提供了一组函数来操作信号集(sigset_t):
c复制// 初始化空信号集
int sigemptyset(sigset_t *set);
// 初始化包含所有信号的信号集
int sigfillset(sigset_t *set);
// 添加信号到信号集
int sigaddset(sigset_t *set, int signum);
// 从信号集中删除信号
int sigdelset(sigset_t *set, int signum);
// 测试信号是否在信号集中
int sigismember(const sigset_t *set, int signum);
sigprocmask()函数用于修改进程的信号屏蔽字(blocked位图):
c复制int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how参数指定了修改方式:
一个典型的使用场景是临界区保护:
c复制sigset_t newset, oldset;
sigemptyset(&newset);
sigaddset(&newset, SIGINT);
// 进入临界区前阻塞SIGINT
sigprocmask(SIG_BLOCK, &newset, &oldset);
/* 临界区代码 */
// 恢复原来的信号屏蔽字
sigprocmask(SIG_SETMASK, &oldset, NULL);
Linux信号分为两个历史阶段:
不可靠信号(1-31):
实时信号(34-64):
编写信号处理函数时需要特别注意:
c复制void handler(int sig) {
// 处理事件
}
int main() {
signal(SIGALRM, handler);
alarm(1);
while(1) {
pause(); // 等待信号
}
}
c复制void handler(int sig) {
if (sig == SIGINT) {
// 处理SIGINT
} else if (sig == SIGTERM) {
// 处理SIGTERM
}
}
信号丢失问题:
标准信号不支持排队,如果在处理一个信号期间,同种信号再次产生,可能只会被记录一次。解决方案是使用实时信号,或者在处理函数中及时处理所有待处理事件。
全局变量访问:
信号处理函数中访问的全局变量应该声明为volatile sig_atomic_t类型,防止编译器优化导致的问题。
系统调用中断:
慢速系统调用(如read、write)在被信号中断时会返回EINTR错误。正确处理方式是重试调用:
c复制while ((n = read(fd, buf, size)) == -1 && errno == EINTR)
continue;
在多线程环境中,信号的处理更加复杂:
当信号行为不符合预期时,可以使用以下调试技巧:
c复制void sigchld_handler(int sig) {
while (waitpid(-1, NULL, WNOHANG) > 0)
;
}
结合信号和定时器可以实现多种功能:
一个使用setitimer的精确定时器示例:
c复制struct itimerval timer = {
.it_interval = { .tv_sec = 1, .tv_usec = 0 }, // 间隔时间
.it_value = { .tv_sec = 1, .tv_usec = 0 } // 首次到期时间
};
setitimer(ITIMER_REAL, &timer, NULL);