【Linux】进程信号

devtools/2025/1/23 4:41:53/
🔥 个人主页:大耳朵土土垚
🔥 所属专栏:Linux系统编程

这里将会不定期更新有关Linux的内容,欢迎大家点赞,收藏,评论🥳🥳🎉🎉🎉

文章目录

  • 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. 结语

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)
    信号处理器是一个函数,它在进程接收到指定信号时被调用。你可以为每个信号设置一个自定义的处理器,函数如下:
#include <signal.h>void (*signal(int signum, void (*handler)(int)))(int);

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

  • 忽略信号
    可以将信号处理器设置为 SIG_IGN 来忽略某些信号。但是,不能忽略像 SIGKILL 和 SIGSTOP 这样的不可捕获信号。代码如下:
#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没有终止程序。

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

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

#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或直接跟信号编号来指定发送的信号类型。
    kill -9 <pid>  # 发送SIGKILL信号,强制终止进程
    kill -15 <pid> # 发送SIGTERM信号,请求进程正常终止
    

3) kill函数及系统调用

  • kill()函数: 可以直接从程序内部发送信号给其他进程。
#include <sys/types.h>
#include <signal.h>int kill(pid_t pid, int sig);

参数说明:

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

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

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

#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函数:用于向调用它的进程自身发送信号。
int raise(int sig);

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

  • abort函数:abort函数用于异常终止程序。当调用该函数时,程序会立即退出,并生成SIGABRT信号。
void abort(void);

4) 软件条件

  • 当特定的软件条件发生时,比如子进程结束(SIGCHLD),文件描述符准备就绪(SIGIO),可能会触发信号的产生。
  • 例如alarm定时器:用于设置一个定时器,可以在指定的时间后产生一个SIGALRM信号。
#include <unistd.h>unsigned int alarm(unsigned int seconds);
  • 参数seconds指定了定时器的时间间隔,单位为秒。
  • 该函数返回当前的闹钟定时器的剩余秒数。如果没有闹钟定时器正在运行,则返回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信号处理器来优化,代码如下:

#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。测试代码如下:
#include<iostream>
#include<unistd.h>
#include<signal.h>int main()
{int n = ::alarm(4);//在此之前没有闹钟在运行,返回0std::cout<<n<<std::endl;sleep(1);int m = ::alarm(0);//0:表示取消闹钟,m表示剩余秒数std::cout<<m<<std::endl;return 0;
}

结果如下:
在这里插入图片描述

  • 闹钟原理:

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

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

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

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

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

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

#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异常:
#include <stdio.h>
#include <signal.h>void handler(int sig)
{printf("catch a sig : %d\n", sig);
}int main()
{signal(SIGFPE, handler); // 8) SIGFPEsleep(1);int a = 10;a /= 0;while (1);return 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信号集函数

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)

#include <signal.h>
int sigpending(sigset_t *set);
  • 读取当前进程的未决信号集(pending),通过set参数传出。
  • 调用成功则返回0,出错则返回-1

3) 读取或更改阻塞信号集函数(block)

#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_BLOCKset包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set
SIG_UNBLOCKset包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set
SIG_SETMASK设置当前信号屏蔽字为set所指向的值,相当于mask=set

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

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

#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信号返回1std::cout << 1;elsestd::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设为0sigemptyset(&old_set);//将自定义old_set设为0sigaddset(&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函数

#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 结构体:

struct sigaction {void     (*sa_handler)(int); // 指定信号处理函数或特殊值 SIG_IGN 或 SIG_DFLvoid     (*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函数调用,而是被系统所调用。

代码示例如下:

#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进程信号有关的内容啦~ 完结撒花~ 🥳🎉🎉


http://www.ppmy.cn/devtools/152466.html

相关文章

MySQL数据表操作

目录 常用数据类型 数值类型 整型 浮点型 字符串类型 日期类型 数据表的操作 查看表结构 创建表 约束 删除表 修改表 添加列 删除列 修改列的定义 重命名列 重命名表 总结 在学习了数据库操作之后&#xff0c;我们接着来看数据表的相关操作 我们首先来学习 …

【2024 年度总结】从小白慢慢成长

【2024 年度总结】从小白慢慢成长 1. 加入 CSDN 的契机2. 学习过程2.1 万事开头难2.2 下定决心开始学习2.3 融入技术圈2.4 完成万粉的目标 3. 经验分享3.1 工具的选择3.2 如何提升文章质量3.3 学会善用 AI 工具 4. 保持初心&#xff0c;继续前行 1. 加入 CSDN 的契机 首次接触…

【深度学习】利用Java DL4J 训练金融投资组合模型

🧑 博主简介:CSDN博客专家,历代文学网(PC端可以访问:https://literature.sinhy.com/#/literature?__c=1000,移动端可微信小程序搜索“历代文学”)总架构师,15年工作经验,精通Java编程,高并发设计,Springboot和微服务,熟悉Linux,ESXI虚拟化以及云原生Docker和K8s…

人工智能核心知识:AI Agent的四种关键设计模式

导读&#xff1a;AI Agent是指能够在特定环境中自主执行任务的人工智能系统&#xff0c;不仅接收任务&#xff0c;还自主制定和执行工作计划&#xff0c;并在过程中不断自我评估和调整&#xff0c;类似于人类在创造性任务中的思考和修正过程。AI Agent的四种关键设计模式是实现…

oneplus3t-lineage-14编译-android7

lineageOS-14.1-oneplus3t-build.md lineageOS-14(android7)的开发者模式/usb调试(adb)有root功能, 而lineageOS-16(android9)无 oneplus3t-lineage-14编译-android7 1 清华linageos镜像 x lineage-14.1-20180223-nightly-oneplus3-signed.zip ntfs分区挂载为普通用户目录…

【mybatis】基本操作:详解Spring通过注解和XML的方式来操作mybatis

mybatis 的常用配置 配置数据库连接 #驱动类名称 spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver #数据库连接的url spring.datasource.urljdbc:mysql://127.0.0.1:3306/mybatis_test? characterEncodingutf8&useSSLfalse #连接数据库的⽤⼾名 spring.dat…

游戏引擎学习第81天

仓库:https://gitee.com/mrxiao_com/2d_game_2 或许我们应该尝试在地面上添加一些绘图 在这段时间的工作中&#xff0c;讨论了如何改进地面渲染的问题。虽然之前并没有专注于渲染部分&#xff0c;因为当时主要的工作重心不在这里&#xff0c;但在实现过程中&#xff0c;发现地…

工业视觉5-工业视觉选型

工业视觉5-工业视觉选型 任务分析三、知识准备问答四、相机选型五、总结 任务分析 重点明确任务要求 例子&#xff1a; 检测任务类型 外观检测&#xff1a;检查产品表面是否有划痕、污渍、缺陷等。例如&#xff0c;在电子元件生产中&#xff0c;需要检测芯片表面的瑕疵&…