在操作系统编程中,我们了解了进程间通信的内容。本文将深入探讨 Linux 中的信号机制。信号是操作系统用于进程间通信的一种机制,通常用于通知进程发生了某种事件。
信号分为三部分:信号产生 -> 信号保存 -> 信号执行。
这如何理解呢?我们可以用生活中的例子类比。例如点外卖时,电话铃声响起代表信号产生(外卖到了),你暂时继续玩游戏代表信号被保存(未立即处理),游戏结束后下楼取餐代表信号执行。生活中还有许多类似例子,如上下课铃声、红绿灯等。
在开始了解信号之前,我们先思考几个问题:
- 我们是如何认识这些信号的?关键在于将规则记忆在系统中,这是认识信号的关键。
- 即使信号没有产生,一旦产生,我们知道该做什么。
- 信号产生后,可能无法立即处理,会推迟到合适的时候。
认识信号
站在进程的角度,进程如何认识信号?
- 能够识别信号。
- 知道该信号的处理方式。
在信号产生到处理的间隔内,必须记住信号的到来,即信号保存。
结论:
- 进程必须具备识别和处理信号的能力,这是进程的内置功能。操作系统需明确进程可识别哪些信号及默认动作。
- 收到信号后,进程可能不会立即处理,而是在合适时机处理。
- 进程具有临时保存信号的能力。
#include <iostream>
#include <unistd.h>
int main() {
while (1) {
std::cout << "this is a process" << std::endl;
sleep(1);
}
return 0;
}
运行上述代码,可以通过 Ctrl+C 终止进程。这是因为 Ctrl+C 被解释为收到了 2 号信号(SIGINT)。Linux 中的信号包含编号和宏定义名称,可在 signal.h 中找到,例如 #define SIGINT 2。编号 34 以上的是实时信号,本文主要讨论编号 34 以下的普通信号。
进程收到信号后的处理方式有三种:
- 默认动作行为(SIG_DFL)。
- 忽略信号(SIG_IGN)。
- 自定义动作。
对于 2 号信号,默认行为是终止进程。
验证信号处理
系统调用 signal 用于设置信号处理函数。
void (*signal(int signum, void (*handler)(int)))(int);
参数说明:
signum:信号编号。handler:信号处理函数(系统默认动作、忽略、自定义动作)。
系统默认动作
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main() {
while (1) {
signal(SIGINT, SIG_DFL);
std::cout << "this is a process" << std::endl;
sleep(1);
}
return 0;
}
默认情况下,进程会被终止。
忽略信号
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main() {
while (1) {
signal(SIGINT, SIG_IGN);
std::cout << "this is a process" << std::endl;
sleep(1);
}
return 0;
}
按下 Ctrl+C 后,进程忽略该信号,不终止。
自定义动作
#include <iostream>
#include <unistd.h>
#include <signal.h>
void myhandler(int signum) {
std::cout << "这是一个 2 号信号" << std::endl;
}
int main() {
while (1) {
signal(SIGINT, myhandler);
std::cout << "this is a process" << std::endl;
sleep(1);
}
return 0;
}
收到 2 号信号后,执行自定义打印语句,不终止。
键盘数据与中断机制
键盘输入数据给内核的过程涉及 I/O 中断机制。当 CPU 等待用户输入时,进程阻塞,其他进程运行。用户输入后,数据放入内核缓冲区。I/O 控制器发送中断请求,内核根据中断向量表查找处理地址,将数据放入缓冲区供进程使用。
Ctrl+C 被操作系统识别为特定组合键,映射为 2 号信号。
前 31 号信号中,只有 9 号(SIGKILL)和 19 号(SIGSTOP)不可被自定义,其余均可自定义。
产生信号的系统调用
kill
kill 用于向指定进程或进程组发送信号。
#include <signal.h>
int kill(pid_t pid, int sig);
示例简易 kill 命令实现:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
void Usage(std::string str) {
std::cout << str << " : " << "Signum Pid" << std::endl;
}
int main(int argc, char *argv[]) {
if (argc != 3) {
Usage(argv[0]);
exit(1);
}
kill(atoi(argv[2]), atoi(argv[1]));
return 0;
}
raise
raise 用于向当前进程自身发送信号。
#include <signal.h>
int raise(int sig);
示例:
#include <iostream>
#include <unistd.h>
#include <signal.h>
void myhandler(int signum) {
std::cout << "这是一个" << signum << "号信号" << std::endl;
}
int main() {
signal(2, myhandler);
int count = 1;
while (1) {
std::cout << "this is a process , pid : " << getpid() << std::endl;
if (count % 2 == 0) {
raise(2);
}
count++;
sleep(1);
}
return 0;
}
abort
abort() 用于使当前进程异常终止,生成 SIGABRT。
#include <stdlib.h>
void abort(void);
示例:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>
void myhandler(int signum) {
std::cout << "这是一个" << signum << "号信号" << std::endl;
}
int main() {
signal(SIGABRT, myhandler);
printf("before abort\n");
abort();
printf("after abort\n"); // 不会执行
return 0;
}
由异常产生信号
整数除零
整数除零会触发硬件异常,对应 SIGFPE(8 号信号)。
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main() {
int a = 10;
std::cout << "div before" << std::endl;
a = a / 0;
std::cout << "div after" << std::endl;
return 0;
}
验证代码:
#include <iostream>
#include <unistd.h>
#include <signal.h>
void myhandler(int signum) {
std::cout << "这是一个" << signum << "号信号" << std::endl;
}
int main() {
signal(SIGFPE, myhandler);
int a = 10;
std::cout << "div before" << std::endl;
a = a / 0;
std::cout << "div after" << std::endl;
return 0;
}
若自定义了处理行为,程序会持续打印,因为每次调度都会检测到 PSW 寄存器中的溢出标记位为 1,再次发送信号。
野指针异常
野指针访问非法内存会触发页故障,对应 SIGSEGV(11 号信号)。
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main() {
std::cout << "point error before" << std::endl;
int *p = nullptr;
*p = 10;
std::cout << "point error after" << std::endl;
return 0;
}
验证:
#include <iostream>
#include <unistd.h>
#include <signal.h>
void myhandler(int signum) {
std::cout << "这是一个" << signum << "号信号" << std::endl;
}
int main() {
signal(SIGSEGV, myhandler);
std::cout << "point error before" << std::endl;
int *p = nullptr;
*p = 10;
std::cout << "point error after" << std::endl;
return 0;
}
通过 gdb 调试程序可以定位错误位置。
SIGPIPE 信号
当写进程向管道写入数据,而读进程已关闭时,会触发 SIGPIPE 信号,导致写进程终止。
进程定时器机制
alarm 函数用于设置定时器,到达时间后发送 SIGALRM。
unsigned int alarm(unsigned int seconds);
特点:一个进程同一时间只能有一个 alarm,新的会覆盖旧的。
正确用法示例:
#include <iostream>
#include <unistd.h>
#include <signal.h>
void myhandler(int signum) {
std::cout << "这是一个" << signum << "号信号" << std::endl;
}
int main() {
signal(SIGALRM, myhandler);
alarm(5);
while (1) {
std::cout << "wait ......." << std::endl;
sleep(1);
}
return 0;
}
Core Dump
Core Dump 是进程异常终止时将内存信息转储到文件(core.pid),用于调试。
测试代码:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
pid_t id = fork();
if (id == 0) {
while (1) {
std::cout << "this is child process , pid : " << getpid() << std::endl;
sleep(1);
}
exit(1);
}
int status;
int rid = waitpid(id, &status, 0);
if (rid == id) {
printf("child process quit, rid : %d, exit code : %d, exit signal : %d, core dump:%d\n", rid, (status >> 8) & 0xFF, status & 0x7F, (status >> 7) & 0x1);
}
return 0;
}
若发送信号的行为是 Core,当前目录会出现 core.pid 文件。结合 gdb 调试工具可快速定位错误行数和内容。
以上就是对 Linux 信号机制的概述。

