【Linux】信号的产生
1、信号的产生
- 键盘产生信号:Ctrl+C → 2号信号(SIGINT) → 终止进程Ctrl+\ → 3号信号(SIGQUIT) → 终止进程(可生成核心转储文件),Ctrl+Z → 19号信号(SIGSTOP) → 暂停进程,转入后台
- 硬件异常产生信号:CPU运算异常(除0错误),MMU内存转换异常(野指针),硬件报错触发中断,OS向current进程发送信号
- 命令行指令产生信号:kill命令
- 系统调用/库函数产生信号:kill、raise、abort,用户代码主动调用API触发信号
- 软件条件产生信号:内核检测软件逻辑异常,管道通信中,写端正常,读端关闭,操作系统会发送13号信号。alarm闹钟
无论哪种产生方式,最后发送信号时操作系统修改目标进程PCB内部信号位图完成
2、硬件异常产生信号
代码遇到除0、野指针访问,程序崩溃
除0错误:CPU 运算单元报错 → 收到 SIGFPE 8 号信号
野指针非法访问内存:MMU 内存管理硬件报错 → 收到 SIGSEGV 11 号信号
发送信号本质:操作系统修改目标进程task_structPCB 内部的信号位图。只有操作系统能修改 PCB,所以无论信号来源是键盘、硬件异常,最终写位图一定是操作系统完成,因此发送信号必须依赖系统调用
问题1:除0为什么会让CPU硬件报错?
- 用户代码的变量存放在内存,计算时CPU必须把数据从内存读到CPU内部的通用寄存器,如eax、ebx等
- CPU内部有ALU算逻运算单元,负责算术运算、逻辑运算
- x86 CPU有标志寄存器EFLAGS,寄存器每一个bit代表CPU运算后的硬件状态
- CPU硬件异常后,触发中断,通知操作系统,当前CPU运行出错
问题2:内核如何知道是哪个进程出现的异常?
内核全局指针current,类型为struct task_struct* ,永远指向当前CPU正在运行的进程,操作系统通过全局指针current识别当前正在运行的进程,并向该进程的PCB中的信号位图写入对应的信号
问题3:为社么不调用exit退出,程序就会死循环打印信号?
由于进程上下文(包括错误状态)在调度时被保存和恢复,若信号处理函数未终止进程,进程会反复触发相同异常,导致"死循环"现象
问题3:野指针为什么硬件报错?
CPU内部控制寄存器CR3,保存当前进程页表物理地址,MMU硬件单元集成在 CPU 内部:负责虚拟地址→物理地址转换,依靠页表,野指针是非法虚拟地址,MMU查页表时硬件报错,触发中断通知操作系统;OS 识别是内存访问异常,给当前进程发送 11 号 SIGSEGV 段错误信号
3、库/系统调用产生信号
3.1 kill系统调用
头文件<signal.h>
int kill(pid_t pid, int sig);
功能:给指定pid的进程发送sig信号
返回:成功返回0,失败返回-1
模拟实现kill:
#include <iostream>
#include <cstdlib>
#include <signal.h>
#include <sys/types.h>
void Usage(const std::string &cmd)
{
std::cout << "Usage: " << cmd << " signumber pid" << std::endl;
std::cout << "example: ./mykill 2 12345 send SIGINT to 12345\n";
}
int main()
{
if(argc != 3)
{
Usage(argv[0]);
exit(1);
}
int signo = std::stoi(argv[1]);
pid_t pid = std::stoi(argv[2]);
// 核心系统调用:给pid发送signo信号
int ret = kill(pid, signo);
if(ret == -1)
{
perror("kill"); // 打印错误,比如pid不存在、权限不足
exit(2);
}
return 0;
}
用户调用kill只是发起请求,真正发送信号、修改PCB位图是操作系统
3.2 raise库函数
int raise(int sig);

raise(sig)等价于kill(getpid(), sig)功能:给自己当前进程发送信号,自己给自己发
演示:
#include <iostream>
#include <signal.h>
#include <unistd.h> // getpid sleep
#include <string>
int cnt = 1;
void Usage(const std::string &cmd)
{
std::cout << "Usage: " << cmd << " signumber who" << std::endl;
}
void handler(int sig)
{
std::cout << "进程捕捉到信号: " << sig << " pid: "
<< getpid() << " cnt: " << cnt << std::endl;
cnt++;
}
int main()
{
// 注册捕捉2号信号 SIGINT
signal(SIGINT, handler);
std::cout << "进程pid = " << getpid() << std::endl;
while (true)
{
sleep(2);
raise(2); // raise:给自己进程发送 SIGINT(2)
}
return 0;
}

3.3 abort库函数
void abort(void);

底层等价kill(getpid(), SIGABRT); 6 号信号
4、软件条件产生信号
不是硬件报错,是内核检测软件层面异常条件,主动发送信号
例子1:管道读端关闭,子进程继续往管道里写,内核检测这个软件异常,发送SIGPIPE杀死写端进程,这就是软件条件产生信号
例子2:alarm闹钟系统调用
unsigned int alarm(unsigned int seconds);

功能:给调用进程设置闹钟,seconds秒之后,操作系统给本进程发送SIGALRM14号闹钟信号
alarm是一次性闹钟,触发后就失效,不会重复发信号
默认SIGALRM处理动作TERM终止进程
返回值:上一个未触发闹钟的剩余秒数,没有旧闹钟返回0
如果想实现周期性定时,需要在 SIGALRM 信号处理函数内部,再次调用 alarm () 重新设置闹钟
设置闹钟:
#include <iostream>
#include <signal.h>
#include <unistd.h> // getpid sleep alarm
int cnt = 1;
int main()
{
int n = alarm(3); // 设置3秒闹钟
std::cout << "进程正在运行: " << cnt << " pid: " << getpid()
<< " n: " << n << std::endl;
while (true)
{
std::cout << "进程正在运行: " << cnt << " pid: " << getpid() << " n: " << n << std::endl;
sleep(1);
}
return 0;
}

捕捉闹钟信号:
#include <iostream>
#include <signal.h>
#include <unistd.h> // getpid sleep alarm
int cnt = 1;
void handler(int sig)
{
std::cout << "进程捕捉到信号: " << sig << " pid: "
<< getpid() << " cnt: " << cnt << std::endl;
int n = alarm(2);
std::cout << "上一个闹钟剩余的时间: " << n << std::endl;
cnt++;
}
int main()
{
// 注册捕捉闹钟信号 SIGALRM(14)
signal(SIGALRM, handler);
alarm(3); // 设置3秒后发送SIGALRM
while (true)
{
std::cout << "进程正在运行: " << cnt << " pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
}

alarm返回值n:上一次闹钟还剩下多少秒,没有旧闹钟返回0
注意:不要在信号处理函数中写死循环,信号处理函数依然属于进程上下文
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)