当前位置: 首页 > news >正文

掌握 Linux 信号机制的技巧与策略

在这里插入图片描述

目录

  • 一.信号的产生
      • 1.信号的产生(预备)
      • 2.异常
        • (1).硬件异常
        • (2).core dump
        • (3).软件条件产生信号
    • 二.信号的保存
      • 1.信号的发送
      • 2.block.pending.handler(保存)
        • (1).sigset_t类型
      • 三.信号的捕捉处理
      • 1.什么时候捕捉
      • 2.三顾进程地址空间
      • 3.如何处理信号
      • 4.附加知识
      • 5.volatile
      • 6.可重入函数

一.信号的产生

1.信号的产生(预备)

在了解信号产生之前我们先行观察一个现象:
在这里插入图片描述
我们先编写一个循环打印的代码,接着运行起来:
在这里插入图片描述
他就会像这样每隔一秒在屏幕上打印一行信息,这时候如果我们想要这个进程别打印了给我停止退出,我们常会用到ctrl+c操作:
在这里插入图片描述
这样这个进程就如我所愿退出了。
我们再来看一个现象:
在这里插入图片描述

我们在运行程序时,在指令后面加上&,这次跑起来的程序就无法被ctrl+c退出了。
其实啊:Linux中,一次登录中,一个终端,一般会配上一个bash,每一个登陆,只允许一个进程是前台进程(谁来获取键盘输入,谁就是前台进程),可以运行多个进程是后台进程。
在正常运行程序时其可以被ctrl+c退出的,因为其是前台进程,当我们带上&将其在后台运行,这时允许你在启动一个程序的同时继续使用终端会话进行其他操作
那我们又想知道了,为什么ctrl+c能够杀掉我们的前台进程呢:本质其实是ctrl+c被进程解释为了收到了信号。(2号信号
我们可以用kill -l指令查看信号:
在这里插入图片描述
共有62个信号,其中1到31称为普通信号,34到64称为实时信号
接下来我们来学习一个信号捕捉的函数:
在这里插入图片描述

  • signum代表的用户想要捕捉几号信号
  • handler是个函数指针,代表捕捉到的信号要执行的方法(用户自定义的)

下面我们写一个演示代码捕捉二号信号并且执行我们自定义的方法:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdio>
#include <cstdlib>
using namespace std;void myhandler(int signo)
{cout << "process get a signal: " << signo <<" pid:"<<getpid()<<endl;
}int main()
{signal(2,myhandler); while(true){cout << "I am a  process : " << getpid() << endl;sleep(1);}return 0 ; 
}

需要注意的是signal函数只需要设置一次即可,代表进程再收到2号信号时执行myhandler方法
运行起来后:
在这里插入图片描述
我们可以看到ctrl+c并不能再让进程退出了,而是执行了我们自定义的方法:打印收到的信号和当前的pid。
那又想到了,我们是否可以把所有普通信号都捕捉了呢?届时又会发生什么呢:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdio>
#include <cstdlib>
using namespace std;void myhandler(int signo)
{cout << "process get a signal: " << signo <<" pid:"<<getpid()<<endl;
}int main()
{//signal(2,myhandler); for(int i = 1;i<=31;i++){signal(i,myhandler);}while(true){cout << "I am a  process : " << getpid() << endl;sleep(1);}return 0 ; 
}

在这里插入图片描述
可以看到给这个进程再发送信号时就会执行用户自定义的方法了:那么难道说真的所有的信号都能被signal捕捉并让用户设定自定义的方法吗?
在这里插入图片描述
在这里插入图片描述
其实不是的9号和19号信号无法被捕捉的,这是OS为了安全设置(不能什么信号都能让用户乱搞的)
继续学习几个函数:
在这里插入图片描述
参数表示对pid进程发送sig号信号

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdio>
#include <cstdlib>
using namespace std;int main()
{int cnt =0;while(true){cout << "I am a  process : " << getpid() << endl;sleep(1);cnt++;if(cnt==5){kill(getpid(),2);}} return 0;
} 

在这里插入图片描述
如我们所料在打印五次后,kill函数向当前进程发送二号信号终止了进程。
在这里插入图片描述
raise函数即是向自己的进程发送sig信号:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <cstdio>
#include <cstdlib>
using namespace std;int main()
{int cnt =0;while(true){cout << "I am a  process : " << getpid() << endl;sleep(1);cnt++;if(cnt==5){//kill(getpid(),2);raise(2);}} return 0;
} 

和kill(getpid(),2)的运行效果一样。
在这里插入图片描述
abort 函数是 C 标准库中的一个函数,用于异常终止程序。调用 abort 会导致程序立即终止,且不执行任何清理操作。这里就不演示了
根据以上我们需要知道:无论信号如何产生,一定是OS发送给进程的,因为OS是进程的管理者。

2.异常

(1).硬件异常

这个我们先写一个除零错误,程序运行起来:

#include <iostream>
using namespace std;int main()
{int a =1;a/=0;return 0 ; 
}

在这里插入图片描述
出现八号信号错误SIGFPE(浮点异常)

#include <iostream>using namespace std;int main()
{int *p =nullptr;*p=20;return 0 ; 
} 

再编写一个野指针错误,运行程序:
在这里插入图片描述
出现11号信号段错误SIGSEGV,是我们最常见的错误。
OS之所以向我们进程发送错误信号(抛异常),是为了我们完成后续总结工作,并不是让我们解决的。
那硬件层面上的异常是如何产生的呢:
在这里插入图片描述
当OS根据cpu上的状态寄存器的溢出标志位,检测出代码出现异常时 溢出标志位0变1,就会向进程发送异常信号。
在这里插入图片描述
野指针问题即是MMU与cpu分析虚拟地址转换成物理地址发生错误
上面我们要知道被进程包裹的异常只会影响自己不会波及OS,任何异常都只会影响进程本身。

(2).core dump

在这里插入图片描述
在进程等待中我们讲过子进程退出码的8到15位代表着退出状态,0到7位是终止信号,第八位是core dump标志,他标识着收到的终止信号是否为core(核心转储)。

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>using namespace std;int main()
{pid_t id = fork();if(id == 0){int cnt = 500;while(cnt){cout << "i am a child process, pid: " << getpid() << " cnt: " << cnt << endl;sleep(1);cnt--;}exit(0);}int status = 0;pid_t rid = waitpid(id, &status, 0);if(rid == id){cout << "child quit info, rid: " << rid << " exit code: " << ((status>>8)&0xFF) << " exit signal: " << (status&0x7F) <<" core dump: " << ((status>>7)&1) << endl;   }
}

以上是一个演示代码:

在这里插入图片描述
这样我们能观察到:
在这里插入图片描述
产生了core.pid形式的临时文件,当我们打开系统的core dump功能后,一旦进程出现异常,OS会将进程再内存中的运行信息,转储到当前目录下(磁盘),成为核心转储。通常,这个文件会包含程序的内存、寄存器状态和栈信息,帮助开发人员了解程序崩溃时的状态。
在这里插入图片描述
可以利用其进行事后调试

在Linux系统中,SIGTERM是请求程序正常终止的信号,而CORE是程序崩溃时生成的转储文件。这两者都是处理和调试程序的重要工具,但它们的作用和用途是不同的。
在这里插入图片描述

(3).软件条件产生信号

在这里插入图片描述在 Linux 下,alarm 函数是一个用于设定定时器的系统调用,通常用于在指定的时间后发送 SIGALRM 信号(14号信号)。这个函数可以帮助我们在程序中实现定时操作或超时机制,但在使用时需要注意以下几点:

  • seconds 是设定定时器的时间(秒)
  • 返回以前设置的定时器时间(如果有的话),或者 0(如果之前没有设置定时器)

一个简单的使用代码:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>using namespace std;void settime(int signum)
{cout<<"signum is :"<<signum<<" pid is: "<<getpid()<<endl;alarm(2);
}int main()
{signal(SIGALRM,settime);alarm(2);while(1){sleep(1);cout<<"my pid is :"<<getpid()<<endl;}return 0;
}

在这里插入图片描述

二.信号的保存

1.信号的发送

对于普通信号而言,对于进程而言,收到哪一个信号自己有还是没有,是给进程的PCB发。

1.比特位的内容是0还是1表明是否收到
2.比特位的位置(第几个),表示信号的编号
3.所谓的“发信号”本质就是OS去修改task struct的信号位图对应的比特位。

  • 普通信号:有限、固定优先级、可能丢失、处理方式预定义。
  • 实时信号:数量更多、支持优先级和顺序、信号队列、可以携带附加数据。

2.block.pending.handler(保存)

首先先了解些信号的概念:

实际执行信号的处理动作称为信号递达 信号从产生到递达之间的状态,称为信号未决。 进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

信号在内核中的表示图
在这里插入图片描述

  • block表也是位图 比特位代表是否屏蔽(阻塞)
  • pending位图表收到哪个信号。即那些已经发送给进程但尚未处理的信号
  • handler表存放在接收到信号时执行的函数
(1).sigset_t类型

sigset_t 是一种信号集类型 ,方便对三个表进行操作。
在这里插入图片描述
上图为其常用的函数:

#include <signal.h>
int sigemptyset(sigset_t *set);

sigemptyset 是一个用于操作 sigset_t 类型的函数,它用于初始化一个信号集合为空集

#include <signal.h>
int sigaddset(sigset_t *set, int signum);

sigaddset 将指定的信号 signum 添加到信号集合 set 中。如果该信号已经在集合中,则集合保持不变。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

sigprocmask 是一个用于操作进程信号掩码的函数。信号掩码用于指定当前被阻塞的信号集。通过操纵信号掩码,可以控制哪些信号被阻塞,哪些信号可以被传递到进程。

  • how:用于指定如何修改信号掩码,取值可以是以下三种:
    SIG_BLOCK:将 set 中的信号添加到当前的信号掩码中,即阻塞这些信号。
    SIG_UNBLOCK:从当前的信号掩码中移除 set 中的信号,即解除对这些信号的阻塞。
    SIG_SETMASK:将当前的信号掩码设置为 set 中的信号集,即完全替换当前的信号掩码
  • set:指向一个 sigset_t 类型的信号集合,表示要修改的信号集。可以为 NULL,表示不修改当前的信号掩码
  • oldset:指向一个 sigset_t 类型的变量,用于存储调用函数前的旧信号掩码。如果不需要保存旧信号掩码,可以设置为 NULL。
#include <signal.h>
int sigpending(sigset_t *set);

sigpending 将当前被阻塞但尚未处理的信号存储到 set 中。这样可以检查哪些信号已经被阻塞,并且尚未处理。即pending表里面的内容
下面我们用一段代码演示下,信号的保存:

#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;void PrintPending(sigset_t &pending)
{for (int signo = 31; signo >= 1; signo--){if (sigismember(&pending, signo)){cout << "1";}else{cout << "0";}}cout << "\n\n";
}int main()
{先对2号信号进行屏蔽 --- 数据预备sigset_t bset, oset;   //在哪里开辟的空间???用户栈上的,属于用户区sigemptyset(&bset);sigemptyset(&oset);sigaddset(&bset, 2);       //1.2 调用系统调用,将数据设置进内核sigprocmask(SIG_SETMASK, &bset, &oset);   //我们已经把2好信号屏蔽了吗?oksigset_t pending;int cnt = 0;while (true){int n = sigpending(&pending);if (n < 0)continue;// 打印pending表PrintPending(pending);sleep(1);cnt++;//2解除阻塞if(cnt == 10){cout << "unblock 2 signo" <<" pid: "<<getpid()<< endl;sigprocmask(SIG_SETMASK, &oset, nullptr); }}return 0;
}

在这里插入图片描述
结果与我们预想的一想,刚开是的pending表没有被阻塞但尚未处理的信号,当我们ctrl+c发送二号信号后,因为事先二号信号已经被添加到block表(即被阻塞了),所以这是pending表上第二位比特位置1,即收到2号信号但是被阻塞尚未处理的信号。五秒后再次通过sigprocmask函数接触对2号信号的阻塞。这时将处理2号信号即终止进程。
还有一点需要明确的在之前我们了解到signal捕捉不了9号和19号信号因为OS考虑到安全问题,所以这里一样也不能阻塞9号和19号信号
还有一点需要注意的:如果一个信号没有被阻塞,我们多次给进程发送这个信号时,进程在处理此信号时会暂时将此信号的block置1即阻塞

三.信号的捕捉处理

1.什么时候捕捉

当我们进程从内核态允许访问OS的代码和数据返回到用户态只允许访问用户自己的代码和数据)的时候,进行信号的检测和处理,如果有信号,就进行处理
在这里插入图片描述

2.三顾进程地址空间

在这里插入图片描述

用户页表有几个进程,就有几份用户级页表,而内核页表只有1份每一个进程看到的3~4GB的东西都是一样的
进程视角:我们调用系统中的方法,就是在我自己的地址空间中进行执行的。
操作系统的本质:基于时钟中断的一个死循环!计算机硬件中,有一个时钟芯片,每个很短的时间,向计算机发送时钟中断。

3.如何处理信号

#include <signal.h>int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

sigaction 是一个用于设置信号处理程序的函数,相较于早期的 signal 函数,它提供了更细粒度的控制,并避免了一些 signal 函数中的缺陷。在 Linux 和其他 Unix 系统中,sigaction 被广泛使用来替代 signal

  • signum:要捕捉或处理的信号编号。
  • act:一个指向 struct sigaction 结构的指针,用于指定新的信号处理行为。如果这个指针是 NULL,则不会修改当前的信号处理行为。
  • oldact:一个指向 struct sigaction 结构的指针,用于存储之前的信号处理行为。如果这个指针是 NULL,则不保存之前的信号处理行为

struct sigaction结构

struct sigaction {void (*sa_handler)(int);     // 信号处理函数或以下三个宏之一: SIG_DFL, SIG_IGN, SIG_ERRvoid (*sa_sigaction)(int, siginfo_t *, void *); // 使用 SA_SIGINFO 标志时的信号处理函数sigset_t sa_mask;            // 在处理该信号时需要阻塞的信号集int sa_flags;                // 修改信号行为的标志void (*sa_restorer)(void);   // 已废弃,不应使用
};

目前我们只需要了解sa_handlersa_flags即可。
sa_handler指向处理方法的函数,sa_flags信号处理程序执行期间需要被阻塞的信号集,可添加多个信号。
下面我们来用段代码演示功能:

#include <iostream>
#include <cstring>
#include <ctime>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>using namespace std;void PrintPending(){sigset_t set;sigpending(&set);for (int signo = 31; signo>= 1; signo--){if (sigismember(&set, signo))cout << "1";elsecout << "0";}cout << "\n";}void handler(int signo){cout << "catch a signal, signal number : " << signo << endl;while (true){PrintPending();sleep(1);}}int main(){struct sigaction act, oact;memset(&act, 0, sizeof(act));memset(&oact, 0, sizeof(oact))act.sa_handler = handler; sigaction(2, &act, &oact);while (true){cout << "I am a process: " << getpid() << endl;sleep(1);}return 0;}

在这里插入图片描述
在这里插入图片描述
由上图我们可以总结:

  1. pending位图,什么时候从1->0. 是在执行信号捕捉方法之前,先清0,在调用
  2. 信号被处理的时候,对应的信号也会被添加到block表中,防止信号捕捉被嵌套调用(处理一个信号时会将block置1 禁止重复调用

4.附加知识

子进程在退出时会向父进程发送17号SIGCHLD信号

#include <sys/types.h>
#include <sys/wait.h>pid_t waitpid(pid_t pid, int *status, int options);

options:控制 waitpid 行为的选项。常见选项包括: WNOHANG:如果没有子进程结束,则立即返回,不阻塞。
WUNTRACED:也返回已停止的子进程的状态。 WCONTINUED:也返回如果子进程继续执行的状态(适用于已停止的子进程)。

#include <iostream>
#include <cstring>
#include <ctime>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>using namespace std;void handler(int signo){sleep(5);pid_t rid;while ((rid = waitpid(-1, nullptr, WNOHANG)) > 0){cout << "I am proccess: " << getpid() << " catch a signo: " << signo << " child process quit: " << rid << endl;}}int main()
{srand(time(nullptr));//signal(17, SIG_IGN); // SIG_DFL -> action -> IGNsignal(17, handler);for (int i = 0; i < 10; i++){pid_t id = fork();if (id == 0){while (true){cout << "I am child process: " << getpid() << ", ppid: " << getppid() << endl;sleep(5);break;}cout << "child quit!!!" << endl;exit(0);}sleep(rand()%5+3);sleep(1);}//fatherwhile (true){cout << "I am father process: " << getpid() << endl;sleep(1);}return 0;
}

在这里插入图片描述

所以在等待子进程退出时,我们可以基于信号的方式进行等待:17号信号的默认处理动作时SIG_DFL缺省—>什么都不做

5.volatile

防止编译器过度优化,保持内存可见性:

#include <iostream>
#include <cstring>
#include <ctime>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>using namespace std;int flag = 0;void handler(int signo){cout << "catch a signal: " << signo << endl;flag = 1;}int main(){signal(2, handler);//在优化条件下, flag变量可能被直接优化到CPU内的寄存器中while(!flag);  //flag 0, !falg 真cout << "process quit normal" << endl;return 0;}

在这里插入图片描述
在默认的编译器优化程度下是正常运行的:

mysignal2:mysignal2.ccg++ -o $@ $^  -O3 -g  -std=c++11

但是我们将编译器的优化程度拉满到O3时候:
在这里插入图片描述
就会出现这样的情况,其原因是在编译器优化条件下, flag变量可能被直接优化到CPU内的寄存器中,代码数据中更改flag的值自然就影响不到寄存器中的flag了。
这时候就需要关键字volatile登场了:

#include <iostream>
#include <cstring>
#include <ctime>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>using namespace std;volatile int flag = 0;void handler(int signo){cout << "catch a signal: " << signo << endl;flag = 1;}int main(){signal(2, handler);//在优化条件下, flag变量可能被直接优化到CPU内的寄存器中while(!flag);  //flag 0, !falg 真cout << "process quit normal" << endl;return 0;}


这时候即使在最高程度的编译器优化下,也可以正常完成代码逻辑了。

6.可重入函数

如果一个函数,被重复进入的情况下,出错了,或者可能出错,不可入函数!否则,叫做可重入函数。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Java同城达人交友系统源码
  • (五)activiti-modeler 编辑器初步优化
  • Qt WebEngine播放DRM音视频
  • 马尔科夫决策过程
  • Windows编译Boost需要注意的问题
  • C++面试基础算法的简要介绍
  • Java ExecutorService:你真的了解它吗?
  • 小程序支付(前端)
  • 自注意力和位置编码
  • 西部菱斑响尾蛇教你基础IO
  • 拉削基础知识——拉床的类型及特点
  • 自己履行很多的话语,依旧按照这个方式进行生活
  • 近期关于云服务器window server 2012屏蔽游戏加速器/IP加速器模拟IP连接限制策略
  • 基于python旅游推荐系统(源码+论文+部署讲解等)
  • OpenAI not returning a result?
  • (十五)java多线程之并发集合ArrayBlockingQueue
  • 《剑指offer》分解让复杂问题更简单
  • Babel配置的不完全指南
  • GraphQL学习过程应该是这样的
  • js面向对象
  • macOS 中 shell 创建文件夹及文件并 VS Code 打开
  • miniui datagrid 的客户端分页解决方案 - CS结合
  • Mithril.js 入门介绍
  • nginx(二):进阶配置介绍--rewrite用法,压缩,https虚拟主机等
  • React中的“虫洞”——Context
  • Spring Boot MyBatis配置多种数据库
  • SpringCloud(第 039 篇)链接Mysql数据库,通过JpaRepository编写数据库访问
  • Spring声明式事务管理之一:五大属性分析
  • Terraform入门 - 1. 安装Terraform
  • vue的全局变量和全局拦截请求器
  • 不用申请服务号就可以开发微信支付/支付宝/QQ钱包支付!附:直接可用的代码+demo...
  • 从0搭建SpringBoot的HelloWorld -- Java版本
  • 复杂数据处理
  • 解决jsp引用其他项目时出现的 cannot be resolved to a type错误
  • 今年的LC3大会没了?
  • 聊聊sentinel的DegradeSlot
  • 如何编写一个可升级的智能合约
  • 如何优雅的使用vue+Dcloud(Hbuild)开发混合app
  • 设计模式(12)迭代器模式(讲解+应用)
  • 腾讯优测优分享 | Android碎片化问题小结——关于闪光灯的那些事儿
  • 详解NodeJs流之一
  • # Swust 12th acm 邀请赛# [ K ] 三角形判定 [题解]
  • ###C语言程序设计-----C语言学习(3)#
  • #70结构体案例1(导师,学生,成绩)
  • #if 1...#endif
  • #LLM入门|Prompt#1.7_文本拓展_Expanding
  • #设计模式#4.6 Flyweight(享元) 对象结构型模式
  • #我与Java虚拟机的故事#连载03:面试过的百度,滴滴,快手都问了这些问题
  • #我与Java虚拟机的故事#连载18:JAVA成长之路
  • $NOIp2018$劝退记
  • ()、[]、{}、(())、[[]]等各种括号的使用
  • (152)时序收敛--->(02)时序收敛二
  • (M)unity2D敌人的创建、人物属性设置,遇敌掉血
  • (MIT博士)林达华老师-概率模型与计算机视觉”
  • (办公)springboot配置aop处理请求.