进程间信号
- 信号的认识
- 信号的产生
- 进程对信号的处理机制
- 普通信号的处理机制
- 实时信号的处理机制
- 信号集操作函数
- 信号的捕捉
信号的认识
信号的概念:
信号是一种软件中断,它用于通知进程一个异步事件的发生。
这些事件可能来自系统内部(如硬件异常、定时器到期等),也可能来自其他进程或用户操作(如通过键盘输入Ctrl+C发送中断信号)。
每个信号都有一个特定的编号和名称,例如SIGINT(中断信号)和SIGTERM(终止请求信号)。
信号总数:
标准信号(非实时信号):编号范围为 1 到 31(如 SIGINT、SIGTERM)。
实时信号(POSIX 标准扩展):编号从 SIGRTMIN(通常 34)到 SIGRTMAX(通常 64)。
总计约 64 个信号。
进程对信号的响应:
当一个进程接收到信号时,它可以采取以下三种行动之一:
执行默认动作:大多数信号都有默认的动作,可能是忽略信号、暂停进程、终止进程或者产生核心转储文件。
忽略信号:进程可以选择忽略某些信号,除了SIGKILL(9号信号)和SIGSTOP(19号信号),这两个信号不能被忽略。
捕获并处理信号:进程可以定义自己的信号处理函数来捕获信号,并在接收到信号时执行自定义的行为。
系统调用signal(): 允许进程指定一个信号处理函数,当进程接收到特定类型的信号时,操作系统将调用这个函数而不是执行该信号的默认行为。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int sig, sighandler_t handler);
参数:
sig:需处理的信号编号(如 SIGINT、SIGTERM)。
handler:信号处理方式,可为:
1.用户自定义函数:信号触发时执行;
2.SIG_IGN:忽略该信号;
3.SIG_DFL:恢复系统默认行为。
返回值:成功时返回旧的信号处理函数指针,失败返回 SIG_ERR。
进程的整个生命周期里只需要设置一次,往后都有效。
不是所有的信号都能被自定义捕捉, 9号(杀死进程)和19号(暂停进程)信号不能被捕捉。(只看1~31号普通信号)
示例:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
// 信号处理函数
void handle_sigint(int sig) {
printf("Caught signal %d (SIGINT), exiting...\n", sig);
exit(0); // 优雅地退出程序
}
int main() {
// 设置 SIGINT 信号的处理函数
if (signal(SIGINT, handle_sigint) == SIG_ERR) {
perror("signal");
exit(EXIT_FAILURE);
}
// 无限循环,等待信号
while (1) {
// 通常,这里会执行一些有用的工作
}
return 0;
}
信号的产生
1. 键盘组合键(终端驱动触发)
用户通过终端输入特定组合键时,终端驱动程序会检测并生成信号,发送给当前前台进程组。
Ctrl + C
:- 产生
SIGINT
(2号信号),默认行为是终止进程。 - 用于用户主动中断进程(如停止卡死的程序)。
- 产生
Ctrl + \
:- 产生
SIGQUIT
(3号信号),默认行为是终止进程并生成核心转储(core dump)。 - 用于强制终止进程并调试(如定位程序崩溃点)。
- 产生
2. kill 命令(用户主动发送)
通过命令行工具 kill
显式向指定进程发送信号,本质是调用 kill()
系统调用的封装。
- 语法:
kill -信号编号 PID
- 示例:
kill -9 1234 # 发送 SIGKILL(9号信号)强制终止 PID=1234 的进程 kill -TERM 5678 # 发送 SIGTERM(15号信号)请求优雅终止进程
- 示例:
- 关键信号:
SIGTERM
(15号):默认终止进程,允许进程清理资源(友好终止)。SIGKILL
(9号):强制立即终止进程,不可被捕获或忽略(终极手段)。
3. 库函数(程序内部触发)
通过编程调用库函数或系统调用,由进程主动生成信号。
(1) kill()
系统调用
- 功能:向指定进程(或进程组)发送信号。
- 原型:
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig); // 成功返回 0,失败返回 -1
- 示例:
kill(1234, SIGTERM); // 向 PID=1234 发送终止信号 kill(-1, SIGKILL); // 向所有进程广播 SIGKILL(需 root 权限)
(2) raise()
系统调用
- 功能:向当前进程自身发送信号(等价于
kill(getpid(), sig)
)。 - 原型:
int raise(int sig); // 成功返回 0,失败返回非 0
- 示例:
raise(SIGABRT); // 主动触发异常终止(生成 core dump)
(3) abort()
库函数
- 功能:触发
SIGABRT
(6号信号),强制终止当前进程并生成核心转储。 - 特点:
- 即使进程捕获
SIGABRT
并定义处理函数,处理完毕后仍会终止进程(除非处理函数内调用longjmp
等跳转函数)。 - 用于断言失败(如
assert()
宏底层调用abort()
)。
- 即使进程捕获
- 示例:
if (error) abort(); // 检测到致命错误时终止程序
4. 硬件异常或软件条件(操作系统触发)
由硬件错误或系统内部条件触发的信号,由内核直接生成并发送给进程。
(1) 硬件异常
- 场景:
- 除零错误:CPU 检测到除零操作 → 内核发送
SIGFPE
(8号信号)。 - 非法内存访问:MMU 检测到野指针访问 → 内核发送
SIGSEGV
(11号信号)。 - 总线错误:未对齐内存访问 → 内核发送
SIGBUS
(7号信号)。
- 除零错误:CPU 检测到除零操作 → 内核发送
(2) 软件条件
- 场景:
- 定时器到期:
alarm(seconds)
设置定时器 → 到期后触发SIGALRM
(14号信号)。 - 子进程终止:子进程退出时,内核向父进程发送
SIGCHLD
(17号信号)。 - 管道破裂:向已关闭的管道写数据 → 触发
SIGPIPE
(13号信号)。
- 定时器到期:
- 示例:
alarm(5); // 5秒后触发 SIGALRM
信号产生的本质
所有信号最终由操作系统内核生成并传递给目标进程:
- 异步事件:信号可能在任何时间点中断进程的执行流程。
- 统一管理:无论信号来源(用户、硬件、程序自身),均由内核统一调度和处理。
信号产生方式 | 典型场景 | 核心机制 |
---|---|---|
键盘组合键 | 用户中断前台进程 | 终端驱动调用 kill() |
kill 命令 | 管理进程生命周期 | 封装 kill() 系统调用 |
库函数/系统调用 | 程序主动触发异常或终止 | 调用 kill() /raise() |
硬件异常或软件条件 | CPU/MMU 异常或系统事件 | 内核直接生成信号 |
进程对信号的处理机制
普通信号的处理机制
进程收到信号后,不一定会立即处理,而是等待特定时机(如从内核态返回用户态时),这种延迟形成一个时间窗口,信号三表来统筹完成这个过程。
信号传递过程相关名词:
信号递达:实际执行信号的处理动作。
信号未决:信号从产生到递达之间的状态。
进程可以选择阻塞 (Block )某个信号,被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
信号三表:
blocked位图: 用来标记当前哪些信号被该进程所屏蔽(阻塞),即使这些信号已经到达也不会立即触发处理函数,而是保持在未决状态直到解除屏蔽。
(位值为 1 表示信号被阻塞,被阻塞的信号会停留在 pending 中,直到解除阻塞。)
pending位图: 用于记录所有已经到达但是还没有被递达给进程的信号。
(位值为 1 表示信号已到达但未处理,信号递送时,内核清除对应位)
handler表: 实际上是一个函数指针数组,用于定义每种信号的具体处理动作。当信号递达时,根据信号编号找到对应的处理函数指针,并执行相应的处理逻辑。如果信号没有特定的处理函数,则按照默认行为处理或者忽略该信号。
【注】位图中的不同位代表不同的信号是否被接收到(不同类型的信号会分别占据pending位图中的不同比特位)。
普通信号不支持排队,多个相同类型的信号在它们被处理之前到达,只会有一个实例被记录下来。
实时信号的处理机制
(1) 可靠排队传递
队列化存储:当多个相同的实时信号到达时,内核会将其按接收顺序加入等待队列,确保每次触发均被处理。
(2) 优先级与顺序控制
按编号区分优先级:实时信号的编号越大,优先级越高。
同种信号按到达顺序处理:若多次发送同一实时信号(如多次 SIGRTMIN+5),则按 先进先出(FIFO) 顺序处理。
(3) 支持携带数据
通过 sigqueue() 发送实时信号时,可附加用户自定义数据(整数或指针),由内核传递给信号处理函数。
信号集操作函数
#include <signal.h>
int sigemptyset(sigset_t *set);
//初始化信号集 set,清空所有信号(所有位设为0)
int sigfillset(sigset_t *set);
//初始化信号集 set,包含所有信号(所有位设为1)
int sigaddset (sigset_t *set, int signo);
//将信号 signo 添加到信号集 set 中
int sigdelset(sigset_t *set, int signo);
//从信号集 set 中删除信号 signo
int sigismember (const sigset_t *set, int signo);
//检查信号 signo 是否在信号集 set 中
信号屏蔽控制函数:
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
//修改或获取进程的信号屏蔽字(blocked信号集)
int sigpending(sigset_t *set)
//获取当前进程的未决信号集(pending信号集,(已到达但被阻塞的信号))
信号的捕捉
信号捕捉在进程从内核态返回用户态时触发,如果自定义信号处理函数会返回用户态执行函数,函数返回时执行特殊系统调用再次进入内核态,内核态执行返回系统调用,返回用户态中断的地方继续向下执行。
系统调用sigaction(): 更灵活更安全的signal()。
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
参数说明:
signum:目标信号的编号(如 SIGINT、SIGTERM)。
act:指向 struct sigaction 的指针,用于设置新行为。
oldact:若不为 NULL,则保存之前的信号处理配置。
返回值:成功返回 0,失败返回 -1 并设置 errno。
struct sigaction 结构体
struct sigaction {
void (*sa_handler)(int); // 信号处理函数(和signal()类似的处理函数,无法携带附加信息)
void (*sa_sigaction)(int, siginfo_t *, void *); // 信号处理函数(可带附加信息)
//两个信号处理函数选择其中一个填写使用
sigset_t sa_mask; // 信号掩码,在处理期间临时屏蔽的信号集
int sa_flags; // 信号处理标志
};
示例:
//无法携带附加信息的信号处理函数版本
#include <signal.h>
#include <stdio.h>
void handler(int sig) {
printf("Caught signal %d\n", sig);
}
int main() {
struct sigaction act;
sigemptyset(&act.sa_mask); // 清空信号掩码
act.sa_flags = 0;
act.sa_handler = handler; // 设置处理函数
if (sigaction(SIGINT, &act, NULL) == -1) {
perror("sigaction");
return 1;
}
while(1) pause(); // 等待信号
return 0;
}
//使用携带附加信息处理函数的版本
#include <signal.h>
#include <stdio.h>
void handler(int sig, siginfo_t *info, void *context) {
printf("Caught signal %d from pid %d\n", sig, info->si_pid);
}
int main() {
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_flags = SA_SIGINFO; // 启用 sa_sigaction
act.sa_sigaction = handler; // 设置带附加信息的处理函数
if (sigaction(SIGUSR1, &act, NULL) == -1) {
perror("sigaction");
return 1;
}
while(1) pause();
return 0;
}
信号处理时的机制:
接收到信号后,pending位图上的对应位置由0->1;信号在递达时,位图对应位置上的1会在执行信号捕捉方法之前被清0。
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止,不允许嵌套捕捉。
(sa_mask屏蔽的信号,在信号处理函数执行时也会被屏蔽,当信号处理函数返回时自动恢复原来的信号屏蔽字。)