系列文章目录
前言
学习进程的信号是因为信号是 Linux 中 进程和内核之间异步通信的重要机制。它可以用来控制进程(如 Ctrl+C 发送 SIGINT 终止程序)、实现进程间事件通知(如子进程退出触发 SIGCHLD)、支持定时器和调试(如 alarm()、gdb 的断点),并帮助程序优雅处理错误和安全退出(如捕获 SIGSEGV、SIGTERM)。掌握信号能让我们理解操作系统如何管理进程,写出健壮的系统级程序,并为并发与网络编程打下基础。
一,进程信号是什么?
信号(signal)是 软件层面的中断。它让操作系统或一个进程,可以 异步 地通知另一个进程“发生了某个事件”。
信号会打断进程的正常执行,转去执行 信号处理函数,类似于硬件中断打断 CPU 执行程序。
- 简单的信号
int main()
{
while(true)
{
cout<<"I am a process"<<endl;
sleep(1);
}
}
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ ./exe
I am a process
I am a process
I am a process
I am a process
I am a process
^C
这里我们设置了一个死循环函数,每隔一秒打印一次信息,但是当我们按住键盘Ctrl+c后进程的循环就断开了,这就是所谓的信号。我们用键盘给进程发送了一个Ctrl+c的信号,而该信号对应的执行函数是让这个进程的执行中断。
- 配置信号的系统函数signal
函数原型:
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
signal是函数名- 它的第一个参数:
int signum→ 信号编号,比如 SIGINT (2)、SIGTERM (15)
- 它的第二个参数:
void (*handler)(int)→ 一个函数指针,指向信号处理函数- 这个函数必须接受一个
int参数(信号编号),返回 void
- 返回值:
signal返回一个 函数指针,指向原来处理该信号的函数- 这样你就可以在需要时恢复旧的处理函数
这样可能不太好理解,尤其是对一些同学来讲,连什么是函数指针都不懂换一种写法,将返回值和函数原型拆开来看
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
这个sighandler_t就是一个函数指针指向了一个返回值为void,函数参数只有一个int类型参数的自定义类型
当我们在Ctrl+c给进程发送信号后,进程接收到了这个信号然后执行了这个信号对应的函数,这个函数是一个能够中止进程的执行的函数,那我们设想一下,如果用signal重新定义Ctrl+c会发生什么
void reaction(int signals)
{
cout<<"I am a process and I received a signal: "<<signals<<endl;
}
int main()
{
while(true)
{
signal(SIGINT,reaction);
cout<<"I am a process"<<endl;
sleep(1);
}
}
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ ./exe
I am a process
I am a process
^CI am a process and I received a signal: 2
I am a process
I am a process
^CI am a process and I received a signal: 2
I am a process
这里的SIGINT是一个宏,对应的数值是2,当我们按Ctrl+c时其实就是向进程发送一个2的信号,然后进程再执行对应的函数,但是这里我们将函数改成我们自己编写的函数,所以当我们Ctrl+c后并不会中止进程,而是执行我们自己的函数reaction,这里的reaction的执行并不会影响到主函数的执行,所以我们的信号的执行对于进程来讲是异步的
- 前台进程和后台进程
我们使用键盘向进程发送信号是发送给前台进程的信号,我们再执行进程时,默认是前端进程,再执行命令后面加&则成为后台进程
- 前台进程
- 指 占据终端,直接与用户交互的进程
- 用户在终端输入命令,输出会直接显示在终端
- 前台进程会 独占当前终端,直到执行完毕或被中断
./a.out
此时 a.out 就是前台进程,它会运行并在屏幕上输出内容。如果它不结束,你就不能在这个终端继续输入新命令。
- 后台进程
- 指 不占用终端输入 的进程,可以在后台运行
- 通过在命令后加
&启动 - 后台进程仍然可以输出到终端,但不会阻塞你在终端输入新命令
./a.out &
此时 a.out 在后台运行,你还能继续在终端输入别的命令
- 前后台切换
在 Linux 里,可以通过 作业控制 (job control) 来切换:- 暂停前台进程:
Ctrl+Z→ 把进程挂起(T 状态) - 转后台继续运行:
bg %job_number - 转前台运行:
fg %job_number
- 暂停前台进程:
./a.out # 启动前台
^Z # Ctrl+Z 暂停
bg %1 # 把作业 1 放到后台运行
fg %1 # 把作业 1 拉回前台
- 信号的概念
通过kill -l命令查看所有信号类型
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
- 标准信号
- 范围:
1–31 - 特点:不排队,同一种信号在未处理前,只会记录“来过一次”,不会计数,响应速度快,但可能丢失信号
- 范围:
- 实时信号
- 范围:
34-64 - 特点:可排队,多个相同信号不会丢失,按顺序进入队列,这篇文章我们并不讲解实时信号。
- 范围:
Linux 没有 32 和 33 号信号,是因为它们被 glibc 保留给线程库使用,不对用户进程开放。
二,信号的产生
2-1,普通信号
- 通过键盘发送信号
void reaction(int signals)
{
cout<<"I am a process and I received a signal: "<<signals<<endl;
}
int main()
{
while(true)
{
signal(SIGINT,reaction);
cout<<"I am a process"<<endl;
sleep(1);
}
}
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ ./exe
I am a process
I am a process
^CI am a process and I received a signal: 2
I am a process
I am a process
^CI am a process and I received a signal: 2
I am a process
当我们按Ctrl+c就是向前台进程发送一个SIGINT信号
- 调用系统命令向进程发信号
kill -信号类型 进程PID
int main()
{
while(true)
{
cout << "Running..." << endl;
sleep(1);
}
}
root@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2# ps axj | head -1 && ps axj | grep exe
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
2149 2418 2417 2149 pts/2 2417 S+ 0 0:00 grep --color=auto exe
root@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2# kill -SIGSEGV 2418
运行进程后,查找进程的PID并且用kill发送信号中止进程
- 使用函数产生信号
使用函数产生信号一般要用到三个函数kill ,raise,abort
- kill
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
pid:指定要发送信号的目标进程或进程组
sig:要发送的信号编号
演示代码:
int main(int argc,char* argv[])
{
if(argc!=3)
{
cout<<"argc need ==3";
return 1;
}
int num=stoi(argv[1]+1);
pid_t pd=stoi(argv[2]);
kill(pd,num);
return 0;
}
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ g++ -o exe code4.cc
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ ./exe -2 PID
这里我们仿写了一个kill指令,主要用到kill函数指定PID和传递信号,和stoi将字符串转化为int类型数据
- raise
raise 用来 给调用它的进程自己发送一个信号。
函数原型
#include <signal.h>
int raise(int sig);
sig:要发送的信号编号
演示代码
void message(int sig)
{
cout<<"receive signal "<<sig<<endl;
}
int main()
{
signal(3,message);
while(true)
{
raise(3);
sleep(2);
}
return 0;
}
演示结果
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ ./exe
receive signal 3
receive signal 3
...
这里raise每隔两秒会给自己进程发送一个3信号,每次进程捕捉到了3信号都会执行message函数
- abort
函数原型
#include <stdlib.h>
void abort(void);
abort() 用于 异常终止当前进程,它会向自己发送 SIGABRT 信号,这个信号默认会让进程终止,并生成 core dump 文件,abort() 没有返回值,调用它后进程一定会结束
演示代码
void handler(int sig)
{
cout<<"receive abort signal "<<sig<<endl;
}
int main()
{
signal(SIGABRT,handler);
while(true)
{
abort();
sleep(2);
}
}
演示结果
root@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2# ./exe
receive abort signal 6
Aborted (core dumped)
这里就是注意一下abort向进程发送的信号是6(SIGABRT)即可
- alarm
alarm用于定时给自己的进程发送一个SIGALRM信号
函数原型
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
seconds:用于设置多少秒发送信号
演示代码:
int count=0;
void handler(int sig)
{
cout<<"I am "<<sig<<endl;
exit(0);
}
int main()
{
signal(SIGALRM,handler);
alarm(1);
while(true)
{
count++;
cout<<"count:"<<count<<endl;
}
return 0;
}
演示结果:
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ ./exe
...
count:42447
count:42448
count:42449
I am 14
alarm发送信号后,进程接收到信号,如果不定义信号,会默认终止我们的进程,演示代码:
int main()
{
int count=0;
alarm(1);
while(true)
{
count++;
cout<<"count:"<<count<<endl;
}
return 0;
}
演示结果:
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ ./exe
...
count:136821
count:136822
count:136823
Alarm clock
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$
这里我们设置了一个alarm,隔一秒发送一个SIGALRM信号给自己进程,进程收到信号后中止。
2-2 硬件异常产生信号
当程序运行时,如果硬件发现了某些错误,就会通过特殊的机制把这个问题告诉操作系统。操作系统接收到后,会把它翻译成一个信号,发给当前正在运行的进程。
如果程序里做了“除以 0”的操作,CPU 的运算单元会立刻报错,操作系统就会把这个错误转换成一个 SIGFPE 信号发给进程。
再比如,程序访问了一个不属于它的内存地址,内存管理单元(MMU)会触发异常,操作系统就会把这个错误变成一个 SIGSEGV 信号发给进程
模拟除以0
演示代码:
void handler(int sig)
{
cout<<"I am "<<sig<<endl;
exit(0);
}
int main()
{
signal(SIGFPE,handler);
int a=1,b=0;
cout<<a/b<<endl;
return 0;
}
演示结果:
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ ./exe
I am 8
模拟野指针
演示代码:
void handler(int sig)
{
cout<<"I am "<<sig<<endl;
exit(0);
}
int main()
{
signal(SIGFPE,handler);
int* ptr=nullptr;
*ptr=10;
while(1)
{
}
return 0;
}
演示结果:
gch@hcss-ecs-f59a:/gch/code/HaoHao/learn2/day2$ ./exe
Segmentation fault (core dumped)
2-3 core dump
core dump(中文常叫“核心转储”)是操作系统在进程 异常终止 时,把该进程的内存内容、寄存器状态、调用栈等调试信息保存到一个文件里的过程
一般产生core dump的场景有除零错误(SIGFPE),访问非法内存(SIGSEGV),调用 abort()(SIGABRT),进程因为 未处理的致命信号 崩溃时。
主要用于 程序调试和问题定位,通过 gdb 打开 core 文件,可以查看崩溃时进程在哪个函数,哪一行代码出错,变量、内存的值是多少,调用栈的情况。
生成core dump
Linux 默认可能限制 core dump 文件大小,需要先打开:
ulimit -c unlimited
运行一个会崩溃的程序
using namespace std;
#include<iostream>
#include<unistd.h>
#include<signal.h>
int main()
{
int a=1,b=0;
cout<<a/b<<endl;
return 0;
}
总结
Linux/系统中产生信号的方式主要有 三种:
-
硬件产生的信号(
硬件异常):CPU 或硬件检测到进程执行了非法操作。由硬件异常触发,内核捕获后发送给进程- 异常类型:除以 0(
SIGFPE),指针/非法内存访问(SIGSEGV),非对齐访问/总线错误 (SIGBUS),非法指令 (SIGILL)
- 异常类型:除以 0(
-
软件产生的信号(
程序/系统调用触发):程序调用库函数或系统调用来发送信号- 常见例子:
raise,kill,abort,alarm
- 常见例子:
-
用户交互产生的信号:用户在终端操作触发信号。
- 常见例子:
Ctrl+C,Ctrl+\,Ctrl+Z
- 常见例子:

被折叠的 条评论
为什么被折叠?



