前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【Linux】进程信号

【Linux】进程信号

作者头像
大耳朵土土垚
发布2024-12-24 08:14:18
发布2024-12-24 08:14:18
19000
代码可运行
举报
文章被收录于专栏:c/c++c/c++
运行总次数:0
代码可运行

Linux进程信号是一种进程间通信的机制,它允许一个进程通知另一个进程某个事件已经发生。以下是关于Linux进程信号的详细介绍:

1. 信号的概念

  信号就像是一个突然的电话铃声,它会打断正在进行的程序并引起其注意。在Linux系统中,信号是一种软件中断,它通常是异步发生的,用来通知进程某个事件已经发生。每个信号都有一个唯一的编号和一个宏定义名称,这些宏定义可以在signal.h中找到。

  • 使用kill -l命令查看信号编号:
  • 查看信号宏定义:

2. 信号的分类

  在Linux中,信号被分为标准信号(也称为传统或不可靠信号)和实时信号。它们的主要区别在于编号范围、处理方式以及特性。

1) 标准信号 (Traditional/Standard Signals)

这些信号是早期Unix系统定义的,编号通常从1到31(尽管某些系统可能会有所不同)。以下是一些常见的标准信号:

  • SIGHUP (1): 终端挂起或控制进程结束。

1号信号,当用户退出终端时,由该终端开启的所有进程都会接收到这个信号,默认动作为终止进程。但也可以捕获这个信号,比如wget能捕获SIGHUP信号并忽略它,以便在退出登录后继续下载。

  • SIGINT (2): 中断信号,通常是Ctrl+C产生的。

2号信号,程序终止(interrupt)信号,在用户键入INTR字符(通常是Ctrl+C)时发出,用于通知前台进程组终止进程。

  • SIGQUIT (3): 退出信号,产生核心转储。

3号信号,和SIGINT类似,但由QUIT字符(通常是Ctrl+\)来控制。进程在因收到SIGQUIT退出时会产生core文件,在这个意义上类似于一个程序错误信号。

  • SIGILL (4): 非法指令。
  • SIGTRAP (5): 跟踪陷阱(由调试器使用)。
  • SIGABRT (6): 调用abort()函数生成的信号。
  • SIGBUS (7): 总线错误。
  • SIGFPE (8): 浮点异常。
  • SIGKILL (9): 强制终止信号(不可被捕获、阻塞或忽略)。

9号信号,用来立即结束程序的运行。本信号不能被阻塞、处理和忽略。

  • SIGSEGV (11): 段违例。
  • SIGPIPE (13): 管道破裂。
  • SIGALRM (14): 定时器到期。
  • SIGTERM (15): 终止请求。

15号信号,程序结束(terminate)信号,与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,允许进程做一些必要的清理工作后退出。

2) 实时信号 (Real-time Signals)

实时信号是在POSIX.1b标准中引入的,用于提供更可靠的信号机制。它们的编号范围从SIGRTMINSIGRTMAX,具体数值取决于操作系统实现。一般情况下,这个范围是从34开始直到系统的最大信号数。例如,在许多Linux系统上,SIGRTMIN对应的是34,而SIGRTMAX可以达到64或者更高。

实时信号的特点包括但不限于:

  • 不会丢失:如果多个相同的实时信号发送给同一个进程,所有信号都会被接收。
  • 支持排队:每个类型的实时信号可以有一个队列来存储未处理的信号实例。
  • 有序性:实时信号按照发送顺序处理。
  • 可携带数据:可以通过sigqueue()发送附加的数据(一个整数或指针)。

请注意,实际的信号编号可能根据不同的系统架构和版本有所变化。此外,对于实时信号,应当使用SIGRTMIN + nSIGRTMAX - n这样的形式来引用,而不是直接使用具体的数字值,以确保兼容性和正确性。

本章只讨论编号31以下的信号,不讨论实时信号。

3. 信号的处理

在Linux中,信号处理是进程对特定事件响应的一种机制。信号处理有三种方式:

✨方式一:执⾏该信号的默认处理动作

  • 使用命令man 7 signal查看信号在什么条件下产⽣,默认的处理动作是什么:

✨方式二:忽略此信号

可以通过设置信号处理器(也就是信号处理函数来实现):

  • 信号处理器(Signal Handler) 信号处理器是一个函数,它在进程接收到指定信号时被调用。你可以为每个信号设置一个自定义的处理器,函数如下:
代码语言:javascript
代码运行次数:0
运行
复制
#include <signal.h>

void (*signal(int signum, void (*handler)(int)))(int);

参数signum表示要设置的信号编号,参数handler表示要设置的信号处理函数。signal函数会返回上一个信号处理函数的指针,如果出错则返回SIG_ERR。

  • 忽略信号 可以将信号处理器设置为 SIG_IGN 来忽略某些信号。但是,不能忽略像 SIGKILL 和 SIGSTOP 这样的不可捕获信号。代码如下:
代码语言:javascript
代码运行次数:0
运行
复制
#include<iostream>
#include<unistd.h>
#include<signal.h>
int main()
{
    signal(SIGINT,SIG_IGN);//将2号信号忽略
    while(true)
    {
        std::cout<<"PID:"<<getpid()<<"   I am waiting a signal."<<std::endl;
        sleep(1);
    }
    return 0;
}

结果如下:

2号信号的默认处理动作是程序终止(interrupt),但是由于我们使用信号处理器忽略了该信号,所以输入Ctrl+c没有终止程序。

✨情况三:设置自定义处理方式

和忽略信号相同我们也是使用信号处理器来实现,实例代码如下:

代码语言:javascript
代码运行次数:0
运行
复制
#include<iostream>
#include<unistd.h>
#include<signal.h>
void Handler(int signo)
{
    std::cout<<"PID:"<<getpid()<<"  Get a signal:"<<signo<<std::endl;//当接受到SIGINT也就是2号信号会执行打印动作,而非终止进程
}
int main()
{
    signal(SIGINT,Handler);//对2号信号设置自定义处理动作
    while(true)
    {
        std::cout<<"PID:"<<getpid()<<"   I am waiting a signal."<<std::endl;
        sleep(1);
    }
    return 0;
}

结果如下:

对于信号默认处理动作,我们也可以使用信号处理器来实现:signal(SIGINT/*2*/, SIG_DFL);使用SIG_DFL即可选择默认处理动作,结果如下:

注意:signal方法只需要设置一次,那么在这个进程中,只要接受到被设置的信号就会执行Handler方法;此外如果没有产生被设置的信号,Handler方法就永远不会被执行。也就是说signal方法类似的设置了一种机制,当有对应的信号产生时就触发,没有就不触发。

4. 理解信号处理

  对于信号的处理我们可以分别通过软件和硬件这两个视角来理解,由于硬件比较麻烦,设计操作系统运行原理、时钟中断、死循环等,所以这里不做解释。

  • 软件

我们以发送2号信号——从键盘输入Ctrl+c为例,当键盘将输入的Ctrl+c信息交给操作系统后,操作系统就会将信号发送给对应的进程,然后进程就会在合适的时候处理信号。

注意,进程处理信号不是立即处理,而是在合适的时机,因为当前进程可能在处理自己的事。

当进程接收到信号时,是如何记录是哪个信号从而执行相应的信号处理任务呢?

  • 我们发现本次学习的信号是1 ~ 31,连续的数字,那么只需选择位图即可使用最小的空间记录下完整的1 ~ 31个数字;
  • 只需在进程PCB中定义一个无符号整型,使用32个比特位记录31个信号,比特位的位置代表信号的编号,比特位设为1代表收到该位置的信号,为0代表没有。

也就是说操作系统给进程发送信号就是将进程PCB中记录信号的位图对应位置的信号比特位由0置1,然后进程在合适的时候发现自己收到了信号,执行对应处理动作。

接下来为了保证条理,将采⽤如下思路来进⾏阐述:

5. 信号产生

   在Linux系统中,信号(signal)是一种异步通知机制,用于通知进程发生了某些事件。进程可以接收到多种类型的信号,并且可以对这些信号进行处理或者忽略。以下是几种产生信号的常见方法:

1) 键盘输入

  • Ctrl+C:发送SIGINT给前台进程,通常用于终止一个进程。
  • Ctrl+\:发送SIGQUIT给前台进程,类似于SIGINT但会生成核心转储文件。
  • Ctrl+Z:发送SIGTSTP给前台进程,暂停进程的执行。

2) kill命令

使用kill命令可以向指定的进程发送信号,默认情况下是发送SIGTERM信号请求进程正常终止。可以通过-l选项列出所有可用的信号,通过-s或直接跟信号编号来指定发送的信号类型。

代码语言:javascript
代码运行次数:0
运行
复制
kill -9 <pid>  # 发送SIGKILL信号,强制终止进程
kill -15 <pid> # 发送SIGTERM信号,请求进程正常终止

3) kill函数及系统调用

  • kill()函数: 可以直接从程序内部发送信号给其他进程。
代码语言:javascript
代码运行次数:0
运行
复制
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

参数说明:

  • pid:要发送信号的进程ID
  • sig:要发送的信号编号

该函数的返回值为0表示成功,返回-1表示失败。

所以我们可以根据kill系统调用来封装一个自己的kill命令,代码如下:

代码语言:javascript
代码运行次数:0
运行
复制
#include<iostream>
#include <sys/types.h>
#include <signal.h>

int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        std::cout<<"you should print: ./mykill -signo -pid"<<std::endl;//提示输入格式
        return 1;
    }
    int signo = std::stoi(argv[1]);//字符串转整型
    pid_t pid = std::stoi(argv[2]);

    int n = ::kill(pid,signo);
    if(n < 0)
    {
        perror("kill");
        return 2;
    }
    return 0;
}

结果如下:

所以其实kill命令本质也是通过系统调用来实现的。

  • raise函数:用于向调用它的进程自身发送信号。
代码语言:javascript
代码运行次数:0
运行
复制
int raise(int sig);

参数sig为要生成的信号编号。常见的信号编号包括SIGINT(中断信号,通常由终端键盘输入产生)、SIGABRT(终止信号,由abort函数产生)等。

  • abort函数:abort函数用于异常终止程序。当调用该函数时,程序会立即退出,并生成SIGABRT信号。
代码语言:javascript
代码运行次数:0
运行
复制
void abort(void);

4) 软件条件

  • 当特定的软件条件发生时,比如子进程结束(SIGCHLD),文件描述符准备就绪(SIGIO),可能会触发信号的产生。
  • 例如alarm定时器:用于设置一个定时器,可以在指定的时间后产生一个SIGALRM信号。
代码语言:javascript
代码运行次数:0
运行
复制
#include <unistd.h>

unsigned int alarm(unsigned int seconds);

  • 参数seconds指定了定时器的时间间隔,单位为秒。
  • 该函数返回当前的闹钟定时器的剩余秒数。如果没有闹钟定时器正在运行,则返回0。

使用代码如下:

代码语言:javascript
代码运行次数:0
运行
复制
#include<iostream>
#include<unistd.h>
#include<signal.h>
int main()
{
    ::alarm(1);//设置1s的定时器
    
    int count = 0;
    while(true)
    {
        count++;//统计服务器1s可以将计数器累加到多少
        printf("count:%d\n",count);
    }
    return 0;
}

结果如下:

我们发现服务器1s也就计算4万多次,有点慢,这其实是因为打印count数值时,进行了IO交互,会影响速度;所以我们可以通过signal信号处理器来优化,代码如下:

代码语言:javascript
代码运行次数:0
运行
复制
#include<iostream>
#include<unistd.h>
#include<signal.h>

int gcount = 0;

void Handler(int signo)
{
    printf("count:%d\n",gcount);//只在1s后打印总的计算次数
    exit(1);//直接退出
}
int main()
{
    signal(SIGALRM,Handler);
    ::alarm(1);
    while(true)
    {
        gcount++;
    }
    return 0;
}

结果如下:

在这里插入图片描述
在这里插入图片描述

可以看到服务器1s大概计算了6亿多次

  • 闹钟返回值:返回当前的闹钟定时器的剩余秒数。如果没有闹钟定时器正在运行,则返回0。测试代码如下:
代码语言:javascript
代码运行次数:0
运行
复制
#include<iostream>
#include<unistd.h>
#include<signal.h>

int main()
{
    int n = ::alarm(4);//在此之前没有闹钟在运行,返回0
    std::cout<<n<<std::endl;
    sleep(1);
    int m = ::alarm(0);//0:表示取消闹钟,m表示剩余秒数
    std::cout<<m<<std::endl;

    return 0;
}

结果如下:

  • 闹钟原理:

  其实本质是OS必须自身具有定时功能,并能让用户设置这种定时功能,才可能实现闹钟这样的技术。操作系统需要对定时器进行管理,使用结构体对它先描述再组织,其结构体大概包含:

代码语言:javascript
代码运行次数:0
运行
复制
struct timer
{
	int who;
	task_struct *t;//表示哪个进程设置的闹钟
	uint64_t expired;//过期时间
	struct timer *next;//定时器可以有多个,通过链表连接起来
	func_t f;//到期执行的函数
	//...
}

  当定时器到期后,操作系统就会根据它的func_t f函数,执行对应的方法,比如给目标进程发送SIGALRM信号。此外我们可以理解定时器在操作系统中连接的数据结构为小堆,每次都是堆顶元素先到期,这样操作系统就不需要每次都遍历定时器查找是否有过期的定时器。

也就是说定时器到期时可以理解为软件条件就绪,操作系统就会给对应进程发送信号,所以软件条件当作信号产生的方式之一。

  • 使用闹钟完成定时器功能:

  根据闹钟定时作用,我们可以使用信号处理器使得闹钟响起时进程自动去完成某些任务,代码如下:

代码语言:javascript
代码运行次数:0
运行
复制
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
#include<vector>
#include<functional>

using func_t = std::function<void()>;
int gcount = 0;
std::vector<func_t> gfuncs;//使用vector管理相关任务

void Handler(int signo)
{
    for(auto& f : gfuncs)//遍历任务执行
        f();
    std::cout<<"gcount:"<<++gcount<<std::endl;//记录执行次数
    alarm(1);
}
int main()
{
    gfuncs.push_back([](){std::cout<<"我是一个内核刷新操作..."<<std::endl;});
    gfuncs.push_back([](){std::cout<<"我是一个检测进程时间片的操作..."<<std::endl;});
    gfuncs.push_back([](){std::cout<<"我是一个检测进程时间片的操作..."<<std::endl;});

    signal(SIGALRM,Handler);//信号处理器自定义设置闹钟响起时执行的任务

    ::alarm(1);//设置1s后执行任务

    while(true)//防止进程退出
    {
        pause();//暂停等待信号到来
    }
    return 0;
}

结果如下:

注意闹钟设置是一次性的,所以在Handler执行方法中要再设置闹钟,不然就会暂停在那。

5) 硬件异常

  硬件异常被硬件以某种⽅式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执⾏了除以0的指令, CPU的运算单元会产⽣异常, 内核将这个异常解释为SIGFPE信号发送给进程。再⽐如当前进程访问了⾮法内存地址, MMU会产⽣异常,内核将这个异常解释为SIGSEGV信号发送给进程。

  • 除0异常:
代码语言:javascript
代码运行次数:0
运行
复制
#include <stdio.h>
#include <signal.h>

void handler(int sig)
{
    printf("catch a sig : %d\n", sig);
}

int main()
{
    signal(SIGFPE, handler); // 8) SIGFPE
    sleep(1);
    int a = 10;
    a /= 0;
    while (1)
    ;
    return 0;
}
  • 野指针异常:
代码语言:javascript
代码运行次数:0
运行
复制
#include <stdio.h>
#include <signal.h>

void handler(int sig)
{
    printf("catch a sig : %d\n", sig);
}
int main()
{
    signal(SIGSEGV, handler);
    sleep(1);
    int *p = NULL;
    *p = 100;
    while (1)
        ;

由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层⾯上,是被当成信号处理的。

但是我们发现⼀直有8/11号信号产⽣被我们捕获,这是为什么呢?上⾯我们只提到CPU运算异常后,如何处理后续的流程,实际上 OS 会检查应⽤程序的异常情况,其实在CPU中有⼀些控制和状态寄存器,主要⽤于控制处理器的操作,通常由操作系统代码使用。状态寄存器可以简单理解为⼀个位图,对应着⼀些状态标记位、溢出标记位。OS 会检测是否存在异常状态,有异常存在就会调⽤对应的异常处理⽅法。除零异常后,我们并没有清理内存,关闭进程打开的⽂件,切换进程等操作,所以CPU中还保留上下⽂数据以及寄存器内容,除零异常会⼀直存在,就有了我们看到的⼀直发出异常信号的现象。访问⾮法内存其实也是如此,⼤家可以⾃⾏实验。

  以上就是信号产生的五种方法,包括键盘输入、命令行产生、kill函数及系统调用、软件条件及硬件异常;但是无论产生信号是何种方式,发送信号的永远是操作系统!!!这是因为操作系统是进程的管理者。

6. 信号保存

6.1 信号其他相关常见概念

  • 信号递达(Delivery):实际执⾏信号的处理动作称为信号递达
  • 信号未决(Pending):信号从产⽣到递达之间的状态,称为信号未决
  • 阻塞:进程可以选择阻塞 (Block )某个信号。被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作

注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,⽽忽略是在递达之后可选的⼀种处理动作。

6.2 信号在内核中的表示

  每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有⼀个函数指针表示处理动作。如图所示:

  信号产⽣时,内核在进程控制块中设置该信号的未决标志,表示接收到信号,直到信号递达(也就是说信号处理完成)才清除该标志。   在上图的例⼦中,SIGHUP信号未产生(pendingSIGHUP信号标志位为0),也未阻塞(blockSIGHUP信号标志位为0)。SIGINT信号产⽣过,但正在被阻塞,所以暂时不能递达。SIGQUIT信号未产⽣过,⼀旦产⽣SIGQUIT信号将被阻塞,无法执行它的处理动作(也就是无法递达)。   如果SIGHUP信号产生时,操作系统会将SIGHUP信号发送给进程,进程pendingSIGHUP标志位就会由0置为1,进程在将自己的事情处理完后查看信号表,发现收到了SIGHUP信号而且没被阻塞,进而执行SIGHUPhandler方法——SIG_DEF表示默认处理方法。   如果SIGINT解除阻塞,那么进程在查看信号时就会执行SIGINT的处理方法——SIG_IGN(忽略)。

如果在进程解除对某信号的阻塞之前这种信号产⽣过多次,将如何处理?

  • POSIX.1允许系统递送该信号⼀次或多次。
  • Linux是这样实现的:常规信号在递达之前产⽣多次只计⼀次,⽽实时信号在递达之前产⽣多次可以依次放在⼀个队列⾥。本章不讨论实时信号。

6.3 sigset_t

  从上图来看,pending表中每个信号只有⼀个bit的未决标志, ⾮0即1, 不记录该信号产⽣了多少次;阻塞标志(block表)也是这样表示的。因此, 未决和阻塞标志可以⽤相同的数据类型sigset_t来存储, sigset_t称为信号集,这个类型可以表⽰每个信号的“有效”或“⽆效”状态。   在阻塞信号集中“有效”和“⽆效”的含义是该信号是否被阻塞, ⽽在未决信号集中“有 效”和“⽆效”的含义是该信号是否处于未决状态。

sigset_t的底层实现是一个整数类型,使用位操作来设置和获取各个信号的状态。阻塞信号集也叫做当前进程的信号屏蔽字这⾥的“屏蔽”应该理解为阻塞⽽不是忽略。

6.4 信号集操作函数

  有了信号的操作类型——信号集sigset_t之后,我们就可以通过它对pending表和block表进行操作,函数如下:

1) 初始化sigset_t信号集函数
代码语言:javascript
代码运行次数:0
运行
复制
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
  • 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置为1,表示该信号集的有效信号包括系 统⽀持的所有信号。
  • 注意,在使⽤sigset_ t类型的变量之前,⼀定要调⽤sigemptysetsigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调⽤sigaddsetsigdelset在该信号集中添加或删除某种有效信号。
  • 这四个函数都是成功返回0,出错返回-1。
  • sigismember是⼀个布尔函数,⽤于判断⼀个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

有了上述函数,我们就可以定义完一个信号集之后进行添加或删除等初始化操作。

2) 读取或更改未决信号集(pending)
代码语言:javascript
代码运行次数:0
运行
复制
#include <signal.h>
int sigpending(sigset_t *set);
  • 读取当前进程的未决信号集(pending),通过set参数传出。
  • 调用成功则返回0,出错则返回-1
3) 读取或更改阻塞信号集函数(block)
代码语言:javascript
代码运行次数:0
运行
复制
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
  • 返回值:若成功则为0,若出错则为-1
  • 如果oset是非空指针,则读取进程的当前信号屏蔽字(阻塞信号集)通过oset参数传出
  • 如果set是非空指针,则 更改进程的信号屏蔽字
  • 参数how指⽰如何更改。
  • 如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
  • 假设当前的信号屏蔽字为mask,下表说明了how可选参数及其含义:

how参数

参数使用说明

SIG_BLOCK

set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set

SIG_UNBLOCK

set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set

SIG_SETMASK

设置当前信号屏蔽字为set所指向的值,相当于mask=set

如果调⽤sigprocmask解除了对当前若⼲个未决信号的阻塞,则在sigprocmask返回前,⾄少将其中⼀ 个信号递达。

下⾯是使用上述函数的示例代码:

代码语言:javascript
代码运行次数:0
运行
复制
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <sys/types.h>
#include <sys/wait.h>
//打印pending表
void PrintPending(sigset_t &pending)
{
    std::cout << "cur process[" << getpid() << "]pending: ";
    for (int signo = 31; signo >= 1; signo--)
    {
        if (sigismember(&pending, signo))//如果pending表中有signo信号返回1
            std::cout << 1;
        else
            std::cout << 0;
    }
    std::cout << "\n";
}


void handler(int signo)
{
    std::cout << signo << " 号信号被递达!!!" << std::endl;
    std::cout << "-------------------------------" << std::endl;
    sigset_t pending;
    sigpending(&pending);//获取当前pending表
    PrintPending(pending);//打印pending表
    std::cout << "-------------------------------" << std::endl;
}
int main()
{                        
    signal(2, handler); // 0.⾃定义捕捉2号信号
   
    //  1. 屏蔽2号信号
    sigset_t block_set, old_set;
    sigemptyset(&block_set);//将自定义block_set设为0
    sigemptyset(&old_set);//将自定义old_set设为0
    sigaddset(&block_set, SIGINT); // 我们有没有修改当前进⾏的内核block表呢???1 0
    // 1.1 设置进⼊进程的Block表中
    sigprocmask(SIG_BLOCK, &block_set, &old_set); //SIG_BLOCK->block_set = mask|oldset 真正的修改当前进⾏的内核block表,完成了对2号信号的屏蔽!
    int cnt = 10;
    while (true)
    {
        // 2. 获取当前进程的pending信号集
        sigset_t pending;
        sigpending(&pending);
        // 3. 打印pending信号集
        PrintPending(pending);
        cnt--;
        // 4. 解除对2号信号的屏蔽
        if (cnt == 0)
        {
            std::cout << "解除对2号信号的屏蔽!!!" << std::endl;
            sigprocmask(SIG_SETMASK, &old_set, &block_set);
        }
        sleep(1);
    }
}

结果如下:

没有看懂的同学,可以回顾一下信号未决与阻塞的概念以及相关操作函数。

7. 信号捕捉

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。

7.1 信号捕捉流程

由于信号处理函数的代码是在用户空间的,处理过程⽐较复杂,举例如下:

  • 用户程序注册了 SIGQUIT 信号的处理函数 sighandler
  • 当前正在执行main 函数,这时发生中断或异常切换到内核态。
  • 在中断处理完毕后要返回用户态的 main 函数之前检查到有信号 SIGQUIT 递达。
  • 内核决定返回用户态后不是恢复 main 函数的上下⽂继续执行,而是执行 sighandler 函数, sighandlermain 函数使用不同的堆栈空间,它们之间不存在调⽤和被调⽤的关系,是两个独⽴的控制流程。
  • sighandler 函数返回后自动执行特殊的系统调用 sigreturn 再次进⼊内核态。
  • 如果没有新的信号要递达,这次再返回用户态就是恢复 main 函数的上下文继续执行了。

我们之前说过信号处理不是立即的,而是选择合适的时间,所以进程在进行信号处理的时间段如下图:

也就是说当接收到信号会被存储在pending表中,此时不会立即处理,等到进程从用户态与内核态切换时再检查pending表看是否有信号要处理。

我们可以简单理解:

  • 用户态:执行我自己写的代码
  • 内核态:执行操作系统的代码

7.2 sigaction函数

代码语言:javascript
代码运行次数:0
运行
复制
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
  • sigaction函数可以读取和修改与指定信号相关联的处理动作。调⽤成功则返回0,出错则返回- 1。
  • signo:指定要操作的信号编号,例如 SIGINT、SIGTERM 等。
  • act:指向 sigaction 结构体的指针,根据act修改该信号的处理动作。如果不需要改变当前的信号处理方式,则可以设置为 NULL。
  • oact:指向 sigaction 结构体的指针,通过oact传出该信号原来的处理动作。如果不需要保存当前的信号处理方式,则可以设置为 NULL。
  • sigaction 结构体:
代码语言:javascript
代码运行次数:0
运行
复制
struct sigaction {
    void     (*sa_handler)(int); // 指定信号处理函数或特殊值 SIG_IGN 或 SIG_DFL
    void     (*sa_sigaction)(int, siginfo_t *, void *); // 用于替代 sa_handler 的扩展信号处理器
    sigset_t   sa_mask; // 额外的信号屏蔽字,在执行信号处理器期间阻止其他信号
    int        sa_flags; // 特殊标志,影响信号传递行为
    void     (*sa_restorer)(void); // 不再使用,应设为 NULL
};
  • sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号;
  • 赋值为常数SIG_DFL表⽰执⾏系统默认动作;
  • 赋值为⼀个函数指针表示用自定义函数捕捉信号,或者说向内核注册了⼀个信号处理函数,该函数返回值为void,可以带⼀个int参数——通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是⼀个回调函数,不是被main函数调用,而是被系统所调用。

代码示例如下:

代码语言:javascript
代码运行次数:0
运行
复制
#include<iostream>
#include<signal.h>
void handler(int signo)//自定义处理动作
{
    std::cout<<"get a signal:"<<signo<<std::endl;
}
int main()
{
    struct sigaction act,oact;
    act.sa_handler=handler;//将sa_handler赋值为一个函数指针,自定义2号信号处理动作
    ::sigaction(2,&act,&oact);//oact保存的是2号信号原来的处理动作
    while(true)
    {
        ::pause();//等待
    }
    return 0;
}

结果如下:

其实和信号处理器功能一样,只是sigaction函数提供了对信号处理更精确的控制,相比于 signal 函数来说更为安全和灵活。

8. 结语

  我们从信号定义、分类、处理谈到信号产生、信号保存最后到信号捕捉,关键在于信号处理的理解、相关的信号处理函数、信号保存的三张表——pending表、block表和handler表以及信号捕捉的理解与运用。总之,Linux进程信号是一种强大且灵活的进程间通信机制。通过合理地使用信号,可以实现进程间的异步通知、同步和通信等功能。以上就是Linux进程信号有关的内容啦~ 完结撒花~ 🥳🎉🎉

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-12-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 信号的概念
  • 2. 信号的分类
    • 1) 标准信号 (Traditional/Standard Signals)
    • 2) 实时信号 (Real-time Signals)
  • 3. 信号的处理
  • 4. 理解信号处理
  • 5. 信号产生
    • 1) 键盘输入
    • 2) kill命令
    • 3) kill函数及系统调用
    • 4) 软件条件
    • 5) 硬件异常
  • 6. 信号保存
    • 6.1 信号其他相关常见概念
    • 6.2 信号在内核中的表示
    • 6.3 sigset_t
    • 6.4 信号集操作函数
      • 1) 初始化sigset_t信号集函数
      • 2) 读取或更改未决信号集(pending)
      • 3) 读取或更改阻塞信号集函数(block)
  • 7. 信号捕捉
    • 7.1 信号捕捉流程
    • 7.2 sigaction函数
  • 8. 结语
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档