Linux - 信号概念 & 信号产生
- 信号概念
- 信号产生
- 软件信号
- kill
- raise
- abort
- alarm
- 硬件信号
- 键盘产生信号
- 硬件中断
信号概念
信号是进程之间事件异步通知的一种方式
在Linux命令行中,我们可以通过ctrl + c
来终止一个前台运行的进程,其实这就是一个发送信号的行为。我们按下ctrl + c
是在shell
进程中,而被终止的进程,是在前台运行的另外一个进程。因此信号是一种进程之间的通知方式。
可以通过指令kill -l
来查询信号:
以上就是Linux中的全部信号,它们分为两个区间:[1, 31]
和[34, 64]
,也就是说没有0,32,33
这三个信号,虽然信号的最大编号为64
,但实际上只有62
个信号。
[1, 31]
:这些信号称为非实时信号
,当进程收到这些信号后,可以自己选择合适的时候处理[34, 64]
:这些信号称为实时信号
,当进程收到这些信号后,必须立马处理
由于现在的操作系统基本都是分时操作系统,因此实时信号其实是不符合设计理念的,几乎用不到实时信号,本博客只讲解非实时信号
。
上图中,所有信号都是大写的单词,在C/C++
中,一般来说宏
就是大写的,其实信号名就是宏。
那么进程收到信号后要怎么处理呢?
进程有三种处理信号的方式:
- 忽略此信号
- 执行信号的默认处理函数
- 执行信号的自定义处理函数,这种方式也成为
信号捕捉
可以通过man 7 signal
来查看信号的默认处理行为:
在开头,可以看到如下页面:
其中Term
,Ign
,Core
,Stop
,Cont
就是信号处理的默认行为,我简单翻译一下它们的作用:
Term
:默认操作是终止进程Ign
:默认操作是忽略信号Core
:默认操作是终止进程并转储核心Stop
:默认操作是暂停进程Cont
:默认操作是,如果该进程当前已暂停,则继续该进程
以上五种
其中Term
和Core
都是终止进程,不过Core
会额外进行一个核心转储
,这个会在博客后续讲解。
再往下翻阅,就可以看到每个信号的描述:
各列的含义如下:
Signal
:信号的名称Standard
:该信号在哪一个标准中提出Action
:进程收到该信号后的默认处理行为Comment
:对信号的简单描述
对于刚才说的三种处理方式:
- 忽略此信号 -> 即
Action
为Ign
- 执行信号的默认处理函数 -> 即
Action
不为Ign
- 执行信号的自定义处理函数,这种方式也成为`信号捕捉
那么我们又要如何自己定义信号处理函数呢?
signal
函数,包含在头文件<signal.h>
中,可以自定义信号的处理方式,函数原型如下:
sighandler_t signal(int signum, sighandler_t handler);
其中这个sighandler_t
类型,本质是一个void (*)(int)
类型的函数指针,也就是说自定义的信号处理函数必须是void (int)
的格式。其中这个处理函数的第一个参数int
,就是用来接收信号的编号的。
而返回值也是sighandler_t
,其返回原先该信号处理的函数的函数指针。
示例:
void handler(int sig)
{cout << "get sig: " << sig << endl;
}int main()
{signal(2, handler);while(true){cout << "hello world!" << endl;sleep(1);}return 0;
}
以上代码中,我们通过signal(2, handler);
把2
号信号的处理方式变成了执行函数handler
,此后进程收到2
号信号时,就会执行cout << "get sig: " << sig << endl;
了。
而2
号信号SIGINT
就是ctrl + C
发送的信号,因此我们可以直接在shell
中通过ctrl + C
来发送2
号信号,从而验证效果。
输出结果:
可以看到,我在hello world!
期间按下了两次ctrl + C
,本来ctrl + C
发送2
号信号时会直接终止进程,但是现在却是输出get sig 2
了,因为我们将2
号信号的行为改变了。
信号产生
简单讲解了一下信号的三种处理方式后,再来看看信号是如何产生的,在Linux中,信号主要有两种产生方式:软件条件
,硬件异常
。
软件信号
在 Linux 中,“软件条件” 发出的信号指的是由 进程自身或其他进程 产生的信号,而不是由硬件中断或其他外部事件触发的信号。
Linux中有多种系统调用可以发送信号,在此我讲解kill
,raise
,abort
,alarm
四种接口,其中abort
并不是一个系统调用,而是一个用户操作接口。
kill
kill
函数用于给指定pid
的进程发送指定信号,需要头文件<sys/types.h>
和<signal.h>
,函数原型如下:
int kill(pid_t pid, int sig);
参数:
pid
:收到该信号的进程的pid
sig
:发送哪一个信号
返回值:
- 返回
0
:发送信号成功 - 返回
-1
:发送信号失败
示例:
void handler (int sig)
{cout << "get sig: " << sig << endl;exit(1);
}int main()
{pid_t id = fork();if (id == 0) // 子进程{signal(2, handler);while (true){cout << "I am child process" << endl;sleep(1);}}sleep(5);kill(id, 2);return 0;
}
以上示例中,父进程通过fork
创建子进程,sleep
五秒后通过kill(id, 2);
给子进程发送(2) SIGINT
信号。子进程通过signal(2, handler);
修改了信号处理方式,随后每秒钟输出一次I am child process
。
在handler
中,会先输出get sig: 2
,表示自己收到了信号,然后exit
退出进程。
输出结果:
可以看到,我们确实通过kill
函数给进程发送信号了。
另外的,也可以通过kill
指令发送信号,格式为:
kill -sig pid
其中sig
为要发送的信号,pid
为收到信号的进程pid
。
示例:
现在进程执行如下代码:
void handler(int sig)
{cout << "get sig: " << sig << endl;exit(1);
}int main()
{signal(2, handler);while (true){cout << "pid = " << getpid() << endl;sleep(1);}return 0;
}
进程先修改信号2
的处理方式为handler
,然后死循环输出自己的pid
。随后我们在另外一个窗口通过kill -2 pid
来发送(2) SIGINT
信号。
输出结果:
左侧窗口中,进程输出自己的pid = 130988
,在右侧窗口通过kill -2 130988
向进程发送信号,输出一句get sig 2
后退出。
其实kill
指令底层就是调用kill
接口,依然属于系统调用的范围。
raise
raise
函数用于给自己发送信号,需要头文件<signal.h>
,函数原型如下:
int raise(int sig);
参数:
sig
:发送哪一个信号
返回值:
- 返回
0
:发送信号成功 - 返回
-1
:发送信号失败
示例:
void handler(int sig)
{cout << "get sig: " << sig << endl;exit(1);
}int main()
{signal(2, handler);int cnt = 5;while (cnt){cout << "I am a process cnt = " << cnt-- << endl;sleep(1);}raise(2);return 0;
}
先通过signal(2, handler);
修改信号的处理函数,随后循环五次,输出"I am a process cnt = "
,最后通过raise(2);
给自己发送(2) SIGINT
信号。
输出结果:
abort
abort
函数用于给自己发送(6) SIGABRT
信号,需要头文件<stdlib.h>
,属于用户操作接口
,函数原型如下:
void abort(void);
这个函数功能十分简单,就是给自己发送(6) SIGABRT
信号,示例:
int main()
{abort();return 0;
}
我们就在进程内部直接调用abort();
,输出结果:
最后进程输出了Aborted
表示自己收到了(6) SIGABRT
信号。
alarm
alarm
函数用于设定一个闹钟,在一定之间后给当前进程发送信号(14) SIGALRM
,需要头文件<unistd.h>
,函数原型如下:
unsigned int alarm(unsigned int seconds);
参数:
seconds
:在seconds
秒后发送信号
返回值:
- 如果之前有还没响的闹钟:取消上一次的闹钟,并返回上一次闹钟的剩余秒数
- 如果之前没有闹钟了:返回
0
示例:
void handler(int sig)
{cout << "get sig: " << sig << endl;exit(1);
}int main()
{signal(SIGALRM, handler);alarm(1);int i = 0;while(true){cout << i++ << endl;}return 0;
}
以上代码中,先通过signal(SIGALRM, handler);
自定义信号(14) SIGALRM
的处理方式。然后通过alarm(1);
设定一秒钟的闹钟,在一秒内,程序会不断执行while
循环让i++
,我们可以看看一秒内计算机可以执行多少次i++
。
输出结果:
可以看到,计算到76451
时,就收到了SIGALRM
,终止进程了。
硬件信号
硬件信号指的是由 硬件事件 触发的信号,而不是由软件代码逻辑控制的。
具体来说,以下情况属于硬件条件发出的信号:
- 中断: 硬件设备,例如键盘、鼠标、网络接口等,在发生事件时会向 CPU 发送中断信号,例如键盘按键按下、网络数据包到达等。
- 异常: CPU 在执行指令过程中,如果遇到错误情况,例如除以零、内存访问错误等,会产生异常信号。
- 时钟中断: 系统定时器会定期向 CPU 发送时钟中断信号,用于调度进程和执行定时任务。
键盘产生信号
通过键盘发送信号是最简单的信号发送方式,最常用的有ctrl + C
和ctrl + \
。
ctrl + C
:向前台进程发送(2) SIGINT
信号,效果为直接终止进程ctrl + \
:向前台进程发送(3) SIGQUIT
信号,效果为直接终止进程
示例:
该进程每隔一秒输出Hello world
,第一次执行进程,我按下ctrl + C
后直接终止了进程,第二次按下ctrl + \
输出了一个Quit
后终止进程。
硬件中断
硬件给进程发送信号的本质,其实是通过硬件中断
。
硬件中断是指硬件设备向 CPU 发送的信号,通知 CPU 有事件发生需要处理。
想象一下,你的电脑就像一个忙碌的办公室,CPU 就像办公室的经理,负责处理各种任务。而键盘、鼠标、网卡等硬件设备就像办公室的员工,需要向经理汇报工作进度或请求帮助。
当一个员工需要向经理汇报工作进度时,他会敲响经理办公室的门,发出一个“中断”信号。经理收到这个信号后,会暂停当前的工作,转而去处理员工的请求。
硬件中断也是类似的机制。当一个硬件设备需要向 CPU 汇报事件发生时,它会向 CPU 发送一个中断信号。CPU 接收这个信号后,会暂停当前执行的任务,转而去处理硬件设备的请求。
看到以下代码:
int main()
{int a = 5 / 0;return 0;
}
这是一个很经典的除零错误,如果我们强行运行这个进程,会报出如下错误:
发送了错误Floating point exception
,本质上是收到了信号(8) SIGFPE
,那么为什么除零会发送这个信号呢?其实本质上是发生了硬件中断。
如下图:
在以上过程中,各个部分发挥作用如下:
CPU
:CPU
是一个硬件,其要处理计算,而5/0
这个计算过程就是在CPU
中完成的,当CPU
检测到这个计算中0
做了除数,于是对自己发起硬件中断
操作系统
:操作系统检测到硬件中断
后,跳转到中断处理程序
(中断处理程序
是Linux
内核的一部分),然后检测该硬件中断是什么原因,发现是因为除零错误
,于是给进程发送(8) SIGPFE
信号进程
:进程收到操作系统发来的(8) SIGPFE
信号后,执行相应的处理措施
这就是硬件中断发送信号的过程,其实发送信号本质还是操作系统来发送,而硬件发现异常后,只是通知操作系统去发送信号。
再比如说我们之前的ctrl + C
按键发送(2) SIGINT
信号,本质也是硬件中断,当我们从键盘输入了数据后,键盘向CPU
发出硬件中断,随后CPU
去执行操作系统
中的硬件中断程序
,发现是用户按下了ctrl + C
,于是操作系统向进程发送(2) SIGINT
信号。
再看到这样的代码:
void handler(int sig)
{cout << "get sig: " << sig << endl;sleep(1);
}int main()
{signal(8, handler);int a = 5 / 0;return 0;
}
我们已经知道,除0错误是(8) SIGFPE
信号,现在我们把其对应的处理方式改为输出一条语句,我们看看会发生什么:
可以看到,进程现在在不断的输出get sig: 8
,可是我们只发生了一次int a = 5 / 0
,为什么会产生这么多信号?
现在先想一想,为什么我们进程会收到(8) SIGFPE
信号,这是因为CPU
检测到5 / 0
的错误,发生了硬件中断。请问我们调用了handler
函数之后,CPU
内部的5 / 0
被处理了吗?答案是没有的!
也就是说,CPU
会被一直卡在5 / 0
,CPU
不知道怎么计算这个表达式,于是一直硬件中断,操作系统就一直发送(8) SIGFPE
信号,所以进程就一直执行handler
函数了。
与软件条件信号相比,硬件条件信号具有以下特点:
- 由硬件触发: 硬件条件信号的产生是由硬件事件触发的,而不是由代码逻辑决定的
- 不可控性: 硬件条件信号的产生通常无法被进程控制,例如硬件设备的故障、网络连接中断等。
- 不可预测性: 硬件条件信号的产生通常是不可预测的,因为它们是由硬件事件触发的。