云实验室案例-Linux signal机制
本文档涉及案例包括:
• Linux信号机制核Pthread多线程基本介绍
• Linux信号的简单操作和实验
相关知识:
想要了解应用程序如何响应用户键盘中断并实现安全退出,首先就需要了解Linux中的信号(signal)机制,知道信号如何产生并执行对应的处理行为。这些知识的简单介绍如下。
Linux 信号机制
信号在Linux中属于一种采用异步通信方式的软中断,这种机制是进程之间通信的一种方式。
可靠信号和不可靠信号
信号大体上分为可靠信号和不可靠信号两种;其中不可靠信号也被叫做非实时信号,它不支持排队,信号可能丢失,如果发送多次信号,进程只会收到一次;可靠信号也被叫做实时信号,它支持排队,信号不会丢失,多少次发送对应多少次接收。
Linux中一共定义了64种信号量,其中前32种为不可靠信号,后32种为可靠信号。
对于绝大多数信号而言,默认的处理都是终止或停止进程、dump内核映像转储。这里简单罗列了所有不可靠信号,如下表所示:
取值 | 名称 | 解释 | 默认动作 |
1 | SIGHUP | 挂起 | |
2 | SIGINT | 中断 | |
3 | SIGQUIT | 退出 | |
4 | SIGILL | 非法指令 | |
5 | SIGTRAP | 断点或陷阱指令 | |
6 | SIGABRT | abort发出的信号 | |
7 | SIGBUS | 非法内存访问 | |
8 | SIGFPE | 浮点异常 | |
9 | SIGKILL | kill信号 | 不能被忽略、处理和阻塞 |
10 | SIGUSR1 | 用户信号1 | |
11 | SIGSEGV | 无效内存访问 | |
12 | SIGUSR2 | 用户信号2 | |
13 | SIGPIPE | 管道破损,没有读端的管道写数据 | |
14 | SIGALRM | alarm发出的信号 | |
15 | SIGTERM | 终止信号 | |
16 | SIGSTKFLT | 栈溢出 | |
17 | SIGCHLD | 子进程退出 | 默认忽略 |
18 | SIGCONT | 进程继续 | |
19 | SIGSTOP | 进程停止 | 不能被忽略、处理和阻塞 |
20 | SIGTSTP | 进程停止 | |
21 | SIGTTIN | 进程停止,后台进程从终端读数据时 | |
22 | SIGTTOU | 进程停止,后台进程想终端写数据时 | |
23 | SIGURG | I/O有紧急数据到达当前进程 | 默认忽略 |
24 | SIGXCPU | 进程的CPU时间片到期 | |
25 | SIGXFSZ | 文件大小的超出上限 | |
26 | SIGVTALRM | 虚拟时钟超时 | |
27 | SIGPROF | profile时钟超时 | |
28 | SIGWINCH | 窗口大小改变 | 默认忽略 |
29 | SIGIO | I/O相关 | |
30 | SIGPWR | 关机 | 默认忽略 |
31 | SIGSYS | 系统调用异常 |
常用的信号及其处理方式
信号的产生
信号从产生来源的方式上,可以被分为软件和硬件两种方式。
硬件方式有两种,一种最常见的就是用户的输入,通过在终端中按下组合键“CTRL+C”,产生SIGINT信号,这也是本文将要使用的中断方式;另一种是由硬件异常产生,CPU在检测到内存的非法访问等异常后,通知Linux内核并产生相应的信号,进而传递给对应事件的进程。
软件方式一般是通过系统调用产生,常见的函数包括kill(), raise(), sigqueue(), alarm()等。
信号的注册注销
需要特别注意的是,信号的“注销”与用户直观的理解可能不同,它并非是为当前进程“注册”该信号,使得该信号可以被响应。相反,“一个进程收到了一个信号”,这种行为被称为信号的注册。与之类似,当程序处理了信号后,就会将该信号注销。
进程中会有一个unsigned long类型的信号注册位图,用于记录当前需要处理的信号,其中共64个bit位,分别用于记录64个信号的情况,1代表该信号被注册,需要处理;0意味着该信号已被注销。例如,数组的第三个bit位为1,则意味着3号信号被注册。
信号相关函数
1)信号集操作函数
sigemptyset(sigset_t *set):信号集全部清0;
sigfillset(sigset_t *set): 信号集全部置1,则信号集包含linux支持的64种信号;
sigaddset(sigset_t *set, int signum):向信号集中加入signum信号;
sigdelset(sigset_t *set, int signum):向信号集中删除signum信号;
sigismember(const sigset_t *set, int signum):判定信号signum是否存在信号集中。
2)信号阻塞函数
sigprocmask(int how, const sigset_t *set, sigset_t *oldset));
不同how参数,实现不同功能:
SIG_BLOCK:将set指向信号集中的信号,添加到进程阻塞信号集;
SIG_UNBLOCK:将set指向信号集中的信号,从进程阻塞信号集删除;
SIG_SETMASK:将set指向信号集中的信号,设置成进程阻塞信号集;
sigpending(sigset_t *set)):获取已发送到进程,却被阻塞的所有信号;
sigsuspend(const sigset_t *mask)):用mask代替进程的原有掩码,并暂停进程执行,直到收到信号再恢复原有掩码并继续执行进程。
信号安装
在实际的使用过程中,用户也可以选择重写信号的处理函数,这种行为被叫做信号安装。
常见的信号安装函数
signal():不支持信号传递信息,主要用于非实时信号安装;
sigaction():支持信号传递信息,可用于所有信号安装;
signal函数原型为:
sighandler_t signal(int signum, sighandler_t handler);
signum:信号值
handler:更改为哪一个函数处理,接受一个函数地址,函数指针(回调函数)。
typedef void (*sighandler_t)(int);
sigaction函数原型为:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
signum:要操作的signal信号。
act:设置对signal信号的新处理方式。
oldact:原来对信号的处理方式。
执行步骤:
1 预定板子
2 案例执行
2.1 键盘中断退出函数的简单实验
我们首先创建一个简单的测试程序,在打印“hello world”后,陷入while循环:
// SPDX-License-Identifier: GPL-2.0+
/* *
* Copyright 2024 NXP
*/
#include <stdio.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
printf("main task finished!\n\r");
while(1);
return 0;
}
将该文件命名为hello_world.c后,使用gcc命令编译并执行
可以看到,终端此时被hello_world程序占用。输入组合键“CTRL+C”,程序退出,终端可以继续输入命令。
2.2 Kill命令发送信号
在Linux中,如果你想要让一个命令在后台执行,可以在命令的末尾添加一个&符号。这样,命令将在后台执行,而你可以继续在终端中执行其他命令。此时,在终端中输入“CTRL+C”等命令将不再能够强制退出应用程序。
首先现在后台运行hello_world测试程序,并在终端中输入“ps”命令可以查看当前状态下后台运行的进程:
用户可以借助kill命令,通过kill -l查看所有支持的信号量,列出的信号量和上文描述的64个信号量相对应。
用户可以借助ps命令得到的PID号,直接kill杀死进程。如hello_world程序的PID为858,kill命令后hello_world程序终止:
也可以给进程发送特定信号,命令格式为
kill -[num] [pid]
num为数字信号,如20;pid为进程的pid号,经由ps命令得到。这里给hello_world发送一个20号信号:
可以看到,程序接收到了SIGTSTP信号,进程停止。此时使用ps命令查看进程,可以发现该进程只是停止,并没有被终止,后续可以通过18号信号使其继续运行:
2.3 kill()函数和raise()函数
除了在终端种使用kill命令或者组合键“CTRL+C”等,给线程发送命令,用户也可以在程序中通过调用命令,如kill()和raise()等,去给当前进程发送信号。
1)kill函数
kill函数的原型为:
int kill(pid_t pid, int sig);
pid_t:要给哪个进程发,就填哪个进程的pid
sig:要给进程发送的信号
比如我们可以在上面的程序中,添加代码如下
// SPDX-License-Identifier: GPL-2.0+
/* *
* Copyright 2024 NXP
*/
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
kill(getpid(), 3);
printf("main task finished!\n\r");
while(1);
return 0;
}
可以发现程序刚刚运行,就收到了SIGQUIT信号,进程直接终止了
2)raise函数
raise函数的作用是给当前进程发送一个信号,其函数原型为
int raise(int sig);
sig: 给当前进程发送的信号值
比如我们可以给当前进程发送一个6号信号,如下所示:
// SPDX-License-Identifier: GPL-2.0+
/* *
* Copyright 2024 NXP
*/
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main(int argc, const char *argv[])
{
raise(6);
printf("main task finished!\n\r");
while(1);
return 0;
}
同样可以发现,程序刚刚进入就退出了,log中的sig=6表明收到了6号信号。
2.4 自定义信号的处理函数
以signal函数为例,在调用signal函数的时候,我们给函数的第二个参数传递一个回调函数的地址,当我们收到第一个参数所定义的信号值时,就会调用回调函数,执行回调函数的功能。
signal函数原型为:
sighandler_t signal(int signum, sighandler_t handler);
signum:信号值
handler:更改为哪一个函数处理,接受一个函数地址,函数指针(回调函数)。
typedef void (*sighandler_t)(int);
这里针对2号和9号信号进行测试,程序如下
// SPDX-License-Identifier: GPL-2.0+
/* *
* Copyright 2024 NXP
*/
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void callback(int sig)
{
printf("receive the signal %d\n\r", sig);
}
int main(int argc, const char *argv[])
{
signal(2, callback);
signal(9, callback);
printf("main task finished!\n\r");
while(1);
return 0;
}
执行并通过kill命令向进程发送信号2(CTRL+C)和信号9(CTRL+Z),得到结果如下:
可以发现,2号信号可以接收到,并执行自定义的处理函数打印log;但9号信号并不会执行回调函数,而是直接将进程强杀了,其原因是9号信号是无法被用户更改处理方式的。