文章目录
信号机制
- 信号 Linux 进程间通信、异步事件通知和进程控制的一种基本机制
- 它本质上是一个异步中断,用于通知进程发生了某个事件(如:SIGINT 用户中断,SIGSEGV 段错误)
latex
+-------------------+ 产生 (raise/kill) +-------------------+
| 信号产生源 | ------------------------> | 目标进程 |
| (内核/其他进程/自身)| | |
+-------------------+ +-------------------+
|
| 内核检查进程信号掩码
V
+-----------------------------+
| 信号是否在阻塞掩码中? |
+-----------------------------+
| 否 | 是
V V
+----------------+ +----------------+
| 立即递送 | | 标记为未决 |
+----------------+ | (pending set) |
| +----------------+
V |
+-------------------+ | (解除阻塞后)
| 信号处理决策 | <------------------+
+-------------------+
|
V
+-----------------------------------------+
| 1. 自定义处理函数 (如果已用 sigaction 注册) |
| 2. 忽略 (如果设置为 SIG_IGN) |
| 3. 执行默认行为 (Term, Ign, Core, Stop) |
+-----------------------------------------+
信号生命周期
- 产生:由内核、其他进程或自身(如调用 kill)产生
- 注册/未决:
- 如果进程阻塞了该信号,信号加入未决信号集,等待解除阻塞,即pending状态
- 如果进程未阻塞该信号,进入下一步
- 递送:内核将信号传递给目标进程
- 处理:进程收到信号后,按以下优先级执行:
- a. 自定义信号处理函数:如果进程使用 signal() 或 sigaction() 为该信号安装了处理函数
- b. 默认行为:如果未安装自定义处理函数,执行系统默认行为(Term, Ign, Core, Stop, Cont)
- c. 忽略:如果将信号处理设置为 SIG_IGN,则直接丢弃
信号的处置
- 默认行为
- 恢复进程
- 停止进程
- 产生核心转储文件,同时进程终止
- 终止(杀死)进程
- 忽略信号
- 忽略信号
- 执行信号处理器程序
信号处理器程序
- 信号处理器程序是++由程序员编写的函数++,用于为响应传递来的信号而执行适当任务
- 通知内核应当去调用某一处理器程序的行为,通常称之为安装 或者建立信号处理器程序
- 调用信号处理器程序以响应传递来的信号,则称之为信号已处理(handled) ,或者已捕获(caught)
信号特点
- 异步性:信号可以在进程执行的任何时刻到达
- 简单性:只传递信号编号(一个整数),不携带复杂数据(SIGRTMIN 以上的实时信号除外)
- 针对每个信号,都定义了一个唯一的(小)整数,从1开始顺序展开
- 优先级:每个信号有默认行为(终止、忽略、暂停、继续等)
信号分类
- 不可靠信号 (1 ~ 31):早期 UNIX 信号,不支持排队,可能丢失
- 可靠信号/实时信号 (SIGRTMIN ~ SIGRTMAX):支持排队,不会丢失
<signal.h>以SIGxxxx形式的符号名对这些整数做了定义Linux中标准信号的编号范围为 1 ~ 31- 常用信号示例:
SIGINT (2):终端中断符 (Ctrl+C),默认终止进程,终端驱动程序将发送该信号给前台进程组该信号的默认行为是终止进程SIGQUIT (3):终端退出符 (Ctrl+),该信号将发往前台进程组,默认终止并产生调试的核心转储文件( core dump)SIGKILL (9):强制终止,不可捕获、忽略或阻塞。此信号为"必杀(sure kill)"信号,处理器程序无法将其阻塞、忽略或者捕获,故而"一击必杀",总能终止进程。SIGTERM (15):优雅终止,可被捕获处理SIGSTOP (19):暂停进程,不可捕获、忽略或阻塞SIGCONT (18):继续执行已停止的进程。将该信号发送给已停止的进程,进程将会恢复运行(即在之后某个时间点重新获得调度);当接收信号的进程当前不处于停止状态时,默认情况下将忽略该信号。进程可以捕获该信号, 以便在恢复运行时可以执行某些操作SIGCHLD (17):当父进程的某一子进程终止时,(内核) 将向父进程发送该信号;当父进程的某一子进程因收到信号而停止或恢复时,也可能会向父进程发送该信号。即子进程状态改变(退出或停止),默认忽略SIGHUP(1):当终端断开(挂机)时,将发送该信号给终端控制进程,(如关闭终端窗口、SSH断开)SIGALRM(14):经调用alarm()或setitimer()而设置的实时定时器一旦到期,内核将产生该信号;实时定时器是根据挂钟时间进行计时的(即人类对逝去时间的概念)SIGSEGV(11):段错误信号,当应用程序对内存的引用无效时,就会产生该信号SIGUSR1(10)和SIGUSR2(12):- 内核绝不会为进程产生这些信号
- 进程可以使用这些信号来相互通知事件的发生,或是彼此同步
- 在早期的 UNIX 实现中,这是可供应用随意使用的仅有的两个信号
- 现代UNIX实现则提供了很多实时信号,也可用于程序员自定义的目的
信号集
- 多个信号可使用一个称之为信号集的数据结构来表示,用于表示一组信号,其系统数据类型为
sigset_t - 通常用于"批量操作"信号,比如设置进程的阻塞掩码
核心操作函数
c
#include <signal.h>
// 1. 初始化信号集为空集
int sigemptyset(sigset_t *set);
// 2. 初始化信号集为包含所有信号
int sigfillset(sigset_t *set);
// 3. 将指定信号 signo 添加到信号集
int sigaddset(sigset_t *set, int signo);
// 4. 从信号集中删除指定信号 signo
int sigdelset(sigset_t *set, int signo);
// 5. 判断信号 signo 是否在信号集中
int sigismember(const sigset_t *set, int signo);
- 示例
c
sigset_t my_set;
sigemptyset(&my_set);
sigaddset(&my_set, SIGINT);
sigaddset(&my_set, SIGTERM);
信号掩码
核心概念
- 每个进程都有一个信号掩码(Signal Mask),它是一个信号集,定义了当前被阻塞(Blocked) 的信号
- 阻塞:如果一个信号被加入进程的阻塞掩码,那么当该信号发生时,它不会被递送(即进程不会感知到它)而是保持未决(Pending)状态,直到进程将其从掩码中移除。
关键函数sigprocmask
- 用于查看或修改进程当前的信号掩码
函数原型
c
#include <signal.h>
/* Prototype for the glibc wrapper function */
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数
- how:指定操作方式。
SIG_BLOCK:将 set 中的信号加入当前掩码(即额外阻塞这些信号)SIG_UNBLOCK:将 set 中的信号从当前掩码中移除(即解除阻塞)SIG_SETMASK:直接用 set 替换当前信号掩码
- set:指向新信号集的指针
- oldset:如果不为 NULL,则旧的信号掩码会保存于此
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
int main(int argc, const char *argv[])
{
sigset_t blockSet, prevMask;
sigemptyset(&blockSet);
sigaddset(&blockSet, SIGINT);
if ( sigprocmask(SIG_BLOCK, &blockSet, &prevMask) == -1) {
perror("sigprocmask");
exit(0);
}
sleep(5);
return 0;
}
未决信号集(pending)
- 与阻塞掩码对应,系统内核为每个进程维护一个 "未决信号集",记录那些已经产生但还未被递送(因为被阻塞)的信号
- 可以通过 sigpending 函数查看
c
int sigpending(sigset_t *set); // 获取当前未决信号集
- 重要关系
latex
信号产生
|
V
[ 是否在进程的阻塞掩码中? ]
|否 是
V V
递送给进程处理 加入未决信号集
(当解除阻塞时再递送)
捕获信号
sigaction
- 推荐,功能强大且行为明确
函数原型
c
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {
void (*sa_handler)(int); // 指向信号处理器函数的指针
void (*sa_sigaction)(int, siginfo_t *, void *); // 可获取更多信息的处理函数,处理标准信号时一般不使用
sigset_t sa_mask; // 信号掩码,在执行处理函数期间,要额外阻塞的信号
int sa_flags; // 修改信号行为的标志
void (*sa_restorer)(void); // 仅供glibc库内部使用,已废弃
};
参数
- signum:要检查或更改的信号
- act:为非NULL,则从act安装新的信号处理器程序
- sa_handler:信号处理程序函数指针
- 如果设置为
SIG_DFL修改信号的处置方式为默认处置方式 - 如果设置为
SIG_IGN修改信号的处置方式为忽略此信号
- 如果设置为
- sa_mask:指定在信号处理程序执行期间应阻止的信号掩码。当信号处理函数正在执行时,内核会自动将当前信号加入阻塞掩码(防止重入),通过 sa_mask 可以指定还需要阻塞哪些其他信号。
- sa_flags:
- SA_RESTART:被该信号中断的系统调用自动重启
- SA_NOCLDSTOP:仅对 SIGCHLD 有效,子进程停止时不产生此信号
- SA_SIGINFO:使用 sa_sigaction 而非 sa_handler 作为处理函数,可以获得信号的发送者、PID 等额外信息
- 默认情况下设置为0
- sa_handler:信号处理程序函数指针
- oldact:为非NULL,则前一个信号处理器程序保存在oldact中
在某些架构中,可能是以联合体的形式实现的,所以不要同时设置给
sa_handler和sa_sigaction
示例1:基本信号处理
c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
// 信号处理函数
void sighandler(int sig) {
printf("捕获信号:%s\n", strsignal(sig) );
// 可以在这里进行一些清理工作
_exit(0);
}
int main() {
struct sigaction sa;
// 清空 sigaction 结构体
sigemptyset(&sa.sa_mask);
// 设置信号处理函数
sa.sa_handler = sighandler;
// 不使用信号处理函数的扩展功能
sa.sa_flags = 0;
// 使用 sigaction 注册信号处理函数
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
//SIGINT会终止nanosleep()系统调用,所以这里如果去掉while(1),会直接导致进程终止
while (1) sleep(1);
return 0;
}
signal
- 简单但可移植性差
函数原型
c
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void timeout_handler(int signum) {
fprintf(stderr, "触发定时器\n");
alarm(2); // 2 秒后触发 SIGALRM
}
int main() {
signal(SIGALRM, timeout_handler);
alarm(2); // 2 秒后触发 SIGALRM
while(1) sleep(1);
return 0;
}