硬件软件系统方案应用范例智能车大赛技术支持学习园地关于我们恩智浦官网

Linux signal机制

云实验室案例-Linux signal机制

本文档涉及案例包括:

•        Linux信号机制核Pthread多线程基本介绍

•        Linux信号的简单操作和实验

相关知识:

想要了解应用程序如何响应用户键盘中断并实现安全退出,首先就需要了解Linux中的信号(signal)机制,知道信号如何产生并执行对应的处理行为。这些知识的简单介绍如下。

Linux 信号机制

信号在Linux中属于一种采用异步通信方式的软中断,这种机制是进程之间通信的一种方式。

可靠信号和不可靠信号

信号大体上分为可靠信号和不可靠信号两种;其中不可靠信号也被叫做非实时信号,它不支持排队,信号可能丢失,如果发送多次信号,进程只会收到一次;可靠信号也被叫做实时信号,它支持排队,信号不会丢失,多少次发送对应多少次接收。

Linux中一共定义了64种信号量,其中前32种为不可靠信号,后32种为可靠信号。

对于绝大多数信号而言,默认的处理都是终止或停止进程、dump内核映像转储。这里简单罗列了所有不可靠信号,如下表所示:

取值名称解释默认动作
1SIGHUP挂起
2SIGINT中断
3SIGQUIT退出
4SIGILL非法指令
5SIGTRAP断点或陷阱指令
6SIGABRTabort发出的信号
7SIGBUS非法内存访问
8SIGFPE浮点异常
9SIGKILLkill信号不能被忽略、处理和阻塞
10SIGUSR1用户信号1
11SIGSEGV无效内存访问
12SIGUSR2用户信号2
13SIGPIPE管道破损,没有读端的管道写数据
14SIGALRMalarm发出的信号
15SIGTERM终止信号
16SIGSTKFLT栈溢出
17SIGCHLD子进程退出默认忽略
18SIGCONT进程继续
19SIGSTOP进程停止不能被忽略、处理和阻塞
20SIGTSTP进程停止
21SIGTTIN进程停止,后台进程从终端读数据时
22SIGTTOU进程停止,后台进程想终端写数据时
23SIGURGI/O有紧急数据到达当前进程默认忽略
24SIGXCPU进程的CPU时间片到期
25SIGXFSZ文件大小的超出上限
26SIGVTALRM虚拟时钟超时
27SIGPROFprofile时钟超时
28SIGWINCH窗口大小改变默认忽略
29SIGIOI/O相关
30SIGPWR关机默认忽略
31SIGSYS系统调用异常

常用的信号及其处理方式

  1. 2号信号SIGINT,触发条件为“CTRL+C”,该信号会终止当前进程
  2. 3号信号SIGQUIT,触发条件为“CTRL+|”,该信号会终止当前进程
  3. 20号信号SIGINT,触发条件为“CTRL+Z”,该信号会使当前进程进入暂停状态

信号的产生

信号从产生来源的方式上,可以被分为软件和硬件两种方式。

硬件方式有两种,一种最常见的就是用户的输入,通过在终端中按下组合键“CTRL+C”,产生SIGINT信号,这也是本文将要使用的中断方式;另一种是由硬件异常产生,CPU在检测到内存的非法访问等异常后,通知Linux内核并产生相应的信号,进而传递给对应事件的进程。

软件方式一般是通过系统调用产生,常见的函数包括kill(), raise(), sigqueue(), alarm()等。

信号的注册注销

需要特别注意的是,信号的“注销”与用户直观的理解可能不同,它并非是为当前进程“注册”该信号,使得该信号可以被响应。相反,“一个进程收到了一个信号”,这种行为被称为信号的注册。与之类似,当程序处理了信号后,就会将该信号注销。

进程中会有一个unsigned long类型的信号注册位图,用于记录当前需要处理的信号,其中共64bit位,分别用于记录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程序的PID858kill命令后hello_world程序终止:

 也可以给进程发送特定信号,命令格式为

kill -[num] [pid] 

num为数字信号,如20pid为进程的pid号,经由ps命令得到。这里给hello_world发送一个20号信号:

 

可以看到,程序接收到了SIGTSTP信号,进程停止。此时使用ps命令查看进程,可以发现该进程只是停止,并没有被终止,后续可以通过18号信号使其继续运行:

 

2.3       kill()函数和raise()函数

除了在终端种使用kill命令或者组合键“CTRL+C”等,给线程发送命令,用户也可以在程序中通过调用命令,如kill()raise()等,去给当前进程发送信号。

1kill函数

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信号,进程直接终止了

 

2raise函数

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号信号是无法被用户更改处理方式的。