信号(Linux)

信号

  • 前言
    • 1. 引入
    • 2. 概念
    • 3. 初步认识ctrl+c信号
    • 4. 硬件中断
  • 一、信号的产生
    • 1. 键盘组合键
    • 2. kill命令
    • 3. 系统调用
      • ①kill
      • ②raise
      • ③ abort
    • 4. 异常
      • ①异常产生信号
      • ②原理
    • 5. 软件条件
    • 6. 小结
  • 二、信号的保存
    • 1. 引入
    • 2. 原理
    • 3. 接口
      • ①信号集——sigset_t
      • ②sigprocmask
      • ③sigpending
      • ④使用接口
  • 三、信号的处理
    • 1. 信号什么时候处理和怎么处理
    • 2. 接口——sigaction
  • 四、拓展
    • 1. 可重入函数
    • 2. volatile
    • 3. SIGCHLD——17

前言

1. 引入

  • 在生活中处处有信号的身影。eg:红绿灯,闹钟,外卖信息提示等…。即便现在没有信号产生,但是我们知道信号产生之后,我们应该怎么做。—— 我们知道信号产生后的对应动作
  • 信号产生了,我们可能不立即处理这个信号对应的动作,例如外卖信息提示了,但是现在正在和父母打电话,很明显和父母打电话更重要,等打完电话再去拿外卖。—— 而这期间就有个时间窗口

所以:—— 进程(用户)

  1. 信号处理功能在进程中内置: 进程必须识别并且可以处理信号,哪怕没有产生信号,也有具备相关信号处理的能力
  2. 时间窗口: 进程可能不会第一时间收到信号就去处理,可能他要先处理更重要的事情。—— 所以进程就要有保存信号的能力
  3. 信号的处理方式:例如,绿灯亮了默认就是过马路,也有可能我们没有过马路的需求,所以就是忽略这个绿灯,还有就是旁边有个年迈的老奶奶,我们扶老奶奶过马路。
    • 默认动作
    • 忽略
    • 自定义动作

2. 概念

是一种异步的通知机制,用来提醒进程一个事件已经发生,属于软中断(等下介绍硬件中断,类似)。
异步: 接收者不知道发送者什么时候发送信息,也不需要知道,只需要跑自己的流程,在后续的某个时间点产生交集

Linux中的信号:
信号

  1. [1 - 31]:这个范围的信号为普通信号,需要掌握
  2. [34 - 64]:这个范围的信号为实时信号,了解即可。

实时信号常用的是车载系统之类的,这边都要撞车了,那边还要听歌肯定不行,所以要及时处理。而我们学的Linux是基于时间片轮转的操作系统,是相对公平的。

3. 初步认识ctrl+c信号

代码:

#include <iostream>
#include <unistd.h>
using namespace std;int main()
{while(true){cout << "I am a process, pid:" << getpid() << endl;sleep(1);}return 0;
}

运行结果:
运行结果

  1. ./mykill的方式执行代码,使用ctrl+c,能终止该进程。本质是被进程解释成收到了信号——2号信号(可以在上面找一下2) SIGINT(interrupt —— 中断))。在讲信号产生时会进行验证
  2. ./mykill &的方式执行的代码不能被ctrl+c,原因是因为ctrl+c只能被前台进程接收,而./mykill &的方式是以后台进程的方式启动该程序。所以实际上ctrl+c被发送给了bash进程
  3. bash既然又是进程又是前台进程为什么不受到ctrl+c的影响呢,很明显bash内部进行了设置,可能就是该信号接收的处理方式改为自定义动作
  4. 在Linux中,一次登录,一个终端一般会配上一个bash,每一次登陆,只允许一个前台进程,但是可以允许多个后台进程。(只有前台进程能获取键盘输入)

注: ll在显示屏上,错乱显示,为什么都可以执行?
在一切皆文件部分,我们也说了外设也被映射成为文件。
简化图解:
简化图解

认识:

  1. pidof proc | xargs kill -9
    查看proc(指定进程)的PID,然后杀死该进程。

    • pidof proc:pidof命令可以用来查找指定进程的PID。proc是要查找的进程名。pidof会返回一个或多个PID。如果使用proc生成多个后台进程,可以返回多个PID
    • xargs: 传递命令行参数的工具。在这个命令中,xargs会将上一个命令(pidof)的输出作为参数传递给后面的命令(kill)
    • kill -9:杀死指定进程
      示例:
      示例
  2. ps -axj | grep proc | awk 'print $2' | xargs kill -9
    找到包含"proc"关键字的所有进程,并强制杀死这些进程。

    • ps -axj | grep proc:过滤proc的进程信息
    • awk ‘print $2’:使用awk命令获取第二列(即PID)的信息
  3. awk
    文本处理工具。可以逐行读取文本文件、提取数据、对数据进行处理和格式化输出。

    • awk 'pattern { action }' file
      pattern表示匹配条件,若省略则适用于所有行;action表示对匹配行执行的操作。常见的操作包括打印特定列、计算求和、使用条件语句等
    • 打印列:awk '{ print $1, $3 }' file.txt

4. 硬件中断

键盘的数据时如何输入给内核的,ctrl+c又是如何变成信号的?
OS怎么知道键盘缓冲区有数据了,难道是一次次遍历?——肯定不是,毕竟硬件那么多。
图解:
图解
硬件发送的信号是高低电平,即高电平代表1,低电平代表0。这种信号在中断控制器中会被解释成二进制信号,然后发送给CPU。

我们学习的信号,就是模拟的硬件中断,所以称为软中断

注: 中断的处理是串性的,不会多个硬件同时发生中断

一、信号的产生

无论信号如何产生,最终都是OS发送给进程,OS是进程的管理者

先认识一个系统调用:方便做测试
signal:设置一个函数处理对应的信号

头文件#include <signal.h>函数声明:sighandler_t signal(int signum, sighandler_t handler);参数:signum:要捕获信号的编号,是SIGINT这类也可以handler:表示信号处理函数的指针。参数类型:typedef void (*sighandler_t)(int)。可以设置成:SIG_IGN:忽略该信号SIG_DFL:使用系统默认处理方式自定义:用户自定义信号处理函数返回值:1. 成功,返回一个函数指针,指向先前的signum对应的信号处理函数2. 错误,返回SIG_ERR

后续实验

1. 键盘组合键

  1. 测试:Ctrl + c是2号信号——中断进程
    使用signal测试
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;void handler(int signo)
{cout << " process get a signal:" << signo << endl;
}int main()
{//对2号信号捕获,自定义函数处理该信号signal(SIGINT, handler);while(true){cout << "I am a process, pid: " << getpid() << endl;sleep(1);}return 0;
}

运行结果:
运行结果

  1. Ctrl+\是3号信号——退出进程
    这就不测试了,和测试2号信号同样的方式

  2. Ctrl+Z是19号信号——暂停进程

测试一下是所有进程都可以被捕获吗?

#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;void handler(int signo)
{//signo收到的信号cout << " process get a signal:" << signo << endl;
}int main()
{//普通信号就31个for(int i = 0; i <= 31; i++){//那个信号被触发,都会执行我们自定义的处理方式signal(i, handler);}while(true){cout << "I am a process, pid: " << getpid() << endl;sleep(1);}return 0;
}

测试结果:(测试方法:使用kill命令一次次的给我们测试的进程发送信号)9号和19号进程没有被捕获 其余的都被捕获了。

2. kill命令

方式:kill -signo [PID]
在上面测试信号能不能被全部捕获时,就是通过另一个终端使用kill命令,向测试进程发送信号。
示例:
示例

3. 系统调用

①kill

向指定进程发送信号

头文件:#include <sys/types.h>#include <signal.h>函数声明:int kill(pid_t pid, int sig);参数:1. pid:要发送信号目标进程的PIDpid=0,表示给调用进程同一个组的所有进程发送信号pid为负数,表示给某个进程组的所有进程发送信号2. sig:表示要发送的信号编号,可以是宏返回值:1. 成功,返回02. 失败,返回-1,错误码被设置
  1. 测试代码1:使用kill命令终止另一个启动的进程
#include <iostream>
#include <string>
#include <cstdio>
#include <cerrno>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;void Usage(string proc)
{cout << "Usage:\n\t" << proc << " signum pid\n\n";
}int main(int argc, char *argv[])
{if(argc != 3){Usage(argv[0]);exit(errno);}int signum = stoi(argv[1]);pid_t pid = stoi(argv[2]);int n = kill(pid, signum);if(n == -1){perror("kill");exit(errno);}return 0;
}

被终止的进程:

#include <iostream>
#include <unistd.h>
using namespace std;int main()
{while(true){cout << "I am a pro, pid:" << getpid() << endl;sleep(1);}return 0;
}

运行结果:终止成功
运行结果

  1. 测试代码2:使用kill命令终止自己
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>using namespace std;int main()
{int cnt = 10;while(cnt--){cout << "I am a proc, pid:" << getpid() << endl;sleep(1);if(cnt == 5){kill(getpid(), 2);}}return 0;
}

运行结果:
运行结果

②raise

向调用者进程发送信号

头文件:#include <signal.h>函数声明:int raise(int sig);参数:sig:要发送的信号,可以是宏返回值:1. 成功,返回02. 失败,返回非0

测试代码:
代码描述:五秒之后给自己发送2号终止信号

#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;int main()
{int cnt = 10;while(cnt--){cout << "I am a proc, pid:" << getpid() << endl;sleep(1);if(cnt == 5){raise(2);}}return 0;
}

运行结果:
运行结果
注:也可以使用signal捕获一下看看是不是收到了2号信号

③ abort

使进程异常终止。实际就是给调用进程发送6号信号

头文件:#include <stdlib.h>函数声明:void abort(void);
无参无返回值
  1. 测试代码1:正常调用
    代码描述:5秒后给自己发送6号信号
#include <iostream>
#include <unistd.h>
#include <cstdlib>using namespace std;int main()
{int cnt = 10;while(cnt--){cout << "I am a proc, pid:" << getpid() << endl;sleep(1);if(cnt == 5){abort();}}return 0;
}

运行结果:
运行结果

  1. 测试代码2:捕获6号信号
#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>using namespace std;void handler(int signo)
{//signo收到的信号cout << " process get a signal:" << signo << endl;
}int main()
{//两种方式都可以// signal(6, handler);signal(SIGABRT, handler);int cnt = 5;while(cnt--){cout << "I am a proc, pid:" << getpid() << endl;sleep(1);if(cnt == 2){abort();}}return 0;
}

运行结果:
运行结果
发现:6号信号被我们捕获后,我们在自定义处理方法中并没有终止进程,但是调用abort还是进行了终止

注:使用kill -6 [PID]可以执行正常的自定义

4. 异常

①异常产生信号

1. 除0异常——SIGFPE

  • 测试代码1:
#include <iostream>
using namespace std;int main()
{cout << "div zero before!" << endl;int a = 10;a /= 0;cout << "div zero after!" << endl;return 0;
}

运行结果:打印出浮点数异常就退出了
运行结果
解释结果:除0异常,其实就是向进程发送8号信号——8) SIGFPE

  • 测试代码2:看看除0异常是不是进程收到了8号信号
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;int main()
{while(true){cout << "I am a process, pid: " << getpid() << endl;sleep(1);}return 0;
}

运行结果:
运行结果
解释结果:所以浮点数异常确实是因为收到了8号信号

  • 测试代码3:如果对8号信号捕获会发送什么?
#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;void handler(int signo)
{cout << " process get a signal:" << signo << endl;sleep(1);
}int main()
{// 对8号捕获,自定义函数处理该信号signal(SIGFPE, handler);cout << "div zero before!" << endl;int a = 10;a /= 0;cout << "div zero after!" << endl;return 0;
}

运行结果:
运行结果
结果:一直执行这个自定义的8号信号处理方法。(下面原理讲原因)

2. 野指针异常——SIGSEGV

  • 测试代码1:
#include <iostream>using namespace std;int main()
{cout << "wild pointers before!" << endl;int* ptr = nullptr;*ptr = 10;cout << "wild pointers after!" << endl;return 0;
}

运行结果:段错误
运行结果
解释结果:野指针本质就是向进程发送了11号信号——11) SIGSEGV

  • 测试代码2:直接对11号信号捕获
#include <iostream>
#include <unistd.h>
#include <signal.h>using namespace std;void handler(int signo)
{cout << " process get a signal:" << signo << endl;sleep(1);
}int main()
{signal(SIGSEGV, handler);cout << "wild pointers before!" << endl;int* ptr = nullptr;*ptr = 10;cout << "wild pointers after!" << endl;return 0;
}

运行结果:
运行结果

结果:一直执行这个自定义的11号信号处理方法。(下面原理讲原因)

②原理

  1. 除0错误的原理:
    一般理解:除0错误会让进程崩溃 ——> 因为OS给进程发送信号了——>进程出问题了,OS检测到了,所以要给进程发信号
    问题:OS怎么知道进程出现异常了
    简化的图解:主要是CPU,其它弱化
    简化图解
  1. 野指针错误的原理:
    简化的图解:
    简化图解

注:我们没有资格修改寄存器的值,所以不要妄想修改寄存器的值,从而让进程继续跑,没有意义(毕竟已经出错了)

5. 软件条件

  • 上述讲的除0和野指针这类异常时基于硬件的,然后触发相应处理机制。
  • 在之前学习管道时,有一种情况读端关闭,写端继续写,进程会收到13号信号——SIGPIPE。OS认为这浪费资源的操作,所以直接终止正在写入的进程,这就是基于软件的异常。本节内容主要介绍的是alarm接口和SIGALRM——基于软件条件产生的信号

alarm接口和SIGALRM信号
认识alarm:
作用:用于设置一个定时器,在指定时间后发送SIGALRM信号给当前进程,该信号的默认处理动作时终止当前进程

头文件:#include <unistd.h>函数声明:unsigned int alarm(unsigned int seconds);参数:要设置闹钟的秒数返回值:1. 上一个闹钟剩下的秒数2. 没有前一个闹钟返回0
  1. 测试代码1:
#include <iostream>
#include <unistd.h>
using namespace std;int main()
{//设置一个两秒的闹钟alarm(2);while(true){cout << "I am a proc, pid: " << getpid() << endl;sleep(1);}return 0;
}

运行结果:
运行结果

  1. 测试代码2:
    代码描述:在开始定义一个15秒的闹钟,然后我在代码开始运行5秒后,使用kill命令直接向该进程发送14号信号,该信号的处理方法被我们自定义了,所以会执行我们自定义的信号处理方法。执行完之后直接退出,此时获取了上一个设置闹钟时间还余下的秒数。
    注:sleep影响闹钟的触发,所以自定义方法中的闹钟没有被触发
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;void handler(int signo)
{cout << " process get a signal:" << signo << endl;sleep(1);int val = alarm(2);cout << "return val:" << val << endl;sleep(5);exit(0);
}int main()
{signal(14, handler);alarm(15);while(true){cout << "I am a proc, pid: " << getpid() << endl;sleep(1);}return 0;
}

运行结果:和预期结果相符
运行结果

注:alarm——先描述再组织

  1. 先描述:系统中不会就一个alarm闹钟,所以必然会被结构体描述起来,等alarm时间到达之后,再发送信号
  2. 再组织:将alarm对象用优先级队列维护,堆顶为最近闹钟触发时间,所以OS只需要判断堆顶元素即可

6. 小结

Core Dump:
在讲到进程控制的进程等待时status输出型参数,低16位中有一个core dump——核心转储标志位。
图解core dump:
图解

测试core dump,代码:

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>using namespace std;int main()
{pid_t pid = fork();if (pid == 0){// childwhile (true){cout << "I am a child process, pid:" << getpid() << endl;sleep(1);}exit(0);}// fatherint status = 0;pid_t rid = waitpid(pid, &status, 0);if(rid == pid){cout << "wait child proc success!, rid:" << rid << endl;cout << "exit code:" << ((status>>8)&0xFF) << " exit signal: "<< (status&0x7F) << endl;cout << "core dump: " << ((status>>7)&1) << endl;}return 0;
}

运行结果+分析:
运行结果
core文件分析:

  1. 打开系统的core dump功能,一旦进程出现异常,OS会讲进程在内存中的运行信息,给我dump(转储)到进程的当前目录(磁盘),形成core.pid文件——核心转储(core dump)
  2. 运行时错误,可以直接定位到出错行,先运行然后core-file调试。
    示例:
    示例

终端信号和内核信号:
在Linux系统中,信号可以分为两类:终端信号(Terminal Signal)和内核信号(Kernel Signal)

  1. 终端信号: 由进程之间交互或控制终端发出的信号,由用户或其他进程直接发送给目标进程。终端信号通常用来通知目标进程某种事件的发生
  2. 内核信号:由内核直接向进程发送的信号,表示某种系统事件的发生或错误的发生。由内核管理和发送的,用于处理系统级事件或错误情况。

二、信号的保存

1. 引入

信号保存的相关问题:

  1. 信号为什么要保存?
    因为进程可能不会第一时间收到信号就去处理,可能他要先处理更重要的事情—— 所以进程就要有保存信号的能力
  2. 怎么保存?
    普通信号使用位图保存,比特位的内容是两态的,所以可以决定是否收到相对应的信号
  3. OS在这里扮演什么作用?(信号的发送)
    OS对进程发送信号,本质就是OS修改task_struct属性中信号位图对应的比特位。因为OS是进程的管理者,只有OS才有资格修改进程PCB的属性信息

为介绍下面内容,引入一些信号的常见概念:

  1. 信号递达(Delivery):实际执行信号的处理动作
  2. 信号未决(Pending):信号从产生到递达中间的状态
  3. 阻塞信号:进程可以选择指定信号阻塞,就是让该信号不能递达
    被阻塞的信号会保持在未决状态,直到进程解除阻塞,才会执行递达。

注:有些内容在信号处理中讲,这里主要是介绍三张表

在上面我们也说了,信号肯定是保存在位图中,实际上也确实这样(哈哈哈哈哈),但是(突如其来的转折——要注意了)内核里有三张相关的表。block、pending和handler,前两张都是位图,handler是函数指针数组。
画图了画图了,图里介绍。

2. 原理

图解:
图解

文字描述:

  1. 对于每个信号=block和pending表都有一个标志位+handler表中的一个函数指针(表示处理动作)。
  2. 信号产生时,如果没有阻塞,内核会设置该信号的未决标志,直到该信号递达才清除(执行处理信号函数前,对应的未决标志就会被置0了,同时对应的阻塞信号也被置1,信号处理时再实验),递达时如果判断handler表中是0就执行默认动作,是1就执行忽略动作,此外是自定义动作
  3. 如果指定的信号被阻塞,暂时不能被递达,就算对应的处理动作是忽略,但是没有解除阻塞前不能忽略该信号,因为进程在这个过程中有机会改变处理动作然后解除阻塞。
  4. handler表中有一个SIG_IGN(默认处理动作是忽略),block表是阻塞,前者是处理动作,后者是对应阻塞的信号不能递达。
  5. 某个信号被阻塞了,也可以向该进程发送信号,也会pending只不过不会递达。
    注:
    问题:进程解除对某信号的阻塞之前,该信号产生多次的处理方法?
    答:普通信号在递达前产生多次只记一次,实时信号在递达前产生多次可以一次放在队列中维护,后续依次执行

3. 接口

①信号集——sigset_t

sigset_t也称为信号集,该类型是给用户提供的。包含在头文件#include <signal.h>

提供该类型的原因:
block和pending两张表都是用相同的数据类型sigset_t来存储,虽然也可以使用long这种类型,但是考虑到扩展性等方面,提供了sigset_t类型,本质就是位图,每个位代表一个信息,有效和无效。

注:阻塞信号集也叫做当前进程的信号屏蔽字(signal mask)

信号集操作函数:
与该类型一起出现的还有相关的位图操作的接口,用来操作sigset_t类型的变量

头文件:#include <signal.h>相关操作函数声明:int sigemptyset(sigset_t *set);  //用于清空信号集合int sigfillset(sigset_t *set);   //用于填满信号集合int sigaddset(sigset_t *set, int signum); //向信号集合中添加指定的信号signumint sigdelset(sigset_t *set, int signum); //向信号集合中删除指定的信号signumint sigismember(const sigset_t *set, int signum); //判断signum信号是否在信号集合中参数:1. set:被操作的sigset_t类型对象2. signum:向set对象中添加、删除几号信号,或者判断相应的信号是否在set指向的信号集中返回值:1. 前四个函数成功返回0,出错返回-12. sigismember函数的返回值,判断一个信号集中的有效信号是否包含指定的信号,包含返回1、不包含返回0,出错返回-1

注:
使用sigset_t类型的变量之前,要先调用sigemptyset或sigfillset做初始化,使信号集和处于确定的状态。

②sigprocmask

sigprocmask:用于修改当前进程信号屏蔽字(signal mask)的系统调用。可以在程序中动态地控制进程对信号的阻塞和解除阻塞

头文件:#include <signal.h>函数声明:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);参数:1. how:指定信号屏蔽字的操作方式,取值有以下几种:SIG_BLOCK:将set中的信号添加到进程的信号屏蔽字中,这些信号将被阻塞。相当于mask=mask|setSIG_UNBLOCK:从进程的信号屏蔽字中移除set中的信号,这些信号不再被阻塞。相当于mask=mask&~setSIG_SETMASK:将进程的信号屏蔽字设置为set中的值。相当于mask=set2. set(输入型参数):表示要设置的信号集合。3. oldset(输出型参数):用于存储之前的信号屏蔽字。如果不为NULL,则会把之前的信号屏蔽字存放在oldset中。返回值:成功返回0,出错返回-1,错误码被设置

③sigpending

sigpending:读取当前进程未决信号集。可以用来查询有哪些信号被阻塞而尚未处理,以便进一步处理。

头文件:#include <signal.h>函数声明:int sigpending(sigset_t *set);参数:set:输出型参数,把pending表带出来返回值:成功返回0,出错返回-1,错误码被设置

④使用接口

  1. 测试代码1:
    代码描述:对2号信号捕获,阻塞2号信号,然后10秒后解除阻塞,在这十秒钟向进程发送2号信号。结果可以发现2号信号处于未决状态
#include <iostream>
#include <signal.h>
#include <unistd.h>using namespace std;void PrintPending(const sigset_t &pending)
{//发送2号信号之后,我们想要看到的结果是000000...00000000010for(int signo = 31; signo > 0; signo--){if(sigismember(&pending, signo)){cout << "1";}else{cout << "0";}}cout << endl;
}void handler(int signo)
{cout << "catch a signo: " << signo << endl;
}int main()
{// 1. 对2号信号做自定义捕获signal(2, handler);// 2. 先对2号信号进行屏蔽sigset_t set, oset;sigemptyset(&set); // 初始化操作sigemptyset(&oset);sigaddset(&set, 2); // 对2号信号屏蔽// 3. 系统调用,把set的数据设置到内核sigprocmask(SIG_SETMASK, &set, &oset); // 屏蔽了2号信号// 4. 打印当前进程pendingsigset_t pending;int cnt = 0;while (true){// 获取pending表int n = sigpending(&pending);if (n < 0)continue;PrintPending(pending);sleep(1);cnt++;if (cnt == 10){cout << "unblock 2 signo" << endl;sigprocmask(SIG_SETMASK, &oset, nullptr);}}return 0;
}

运行结果:
运行结果

  1. 测试代码2:
    代码描述:试试把所有的信号都屏蔽
#include <iostream>
#include <signal.h>
#include <unistd.h>using namespace std;void PrintBlock(const sigset_t &block)
{for (int signo = 31; signo > 0; signo--){if (sigismember(&block, signo)){cout << "1";}else{cout << "0";}}cout << endl;
}int main()
{sigset_t set, oset;sigfillset(&set); // 初始化操作sigemptyset(&oset);cout << "set before:";PrintBlock(set);// 设置进task_structsigprocmask(SIG_SETMASK, &set, nullptr);// 看一看有哪些信号被屏蔽了sigprocmask(0, nullptr, &oset);cout << "set after:";PrintBlock(oset);return 0;
}

运行结果:
运行结果
ok了牢底,9和19号信号不能被阻塞,之前也说过9和19号信号不能被捕获

三、信号的处理

信号递达:实际执行信号的处理动作。
在文章的开头也说过,信号的处理方式有三种:

  1. 默认动作:即系统对相应的信号默认的处理动作
  2. 忽略:处理动作就是忽略该信号
  3. 自定义动作:即用户自己实现的信号处理动作

1. 信号什么时候处理和怎么处理

在谈这个之前,就要再谈进程地址空间
进程地址空间

再分析信号是什么时候处理的和怎么处理的?
答:当我们的进程从内核态返回用户态的时候,进行信号检测和处理

  • 用户态:允许访问用户自己的代码和数据
  • 内核态:允许访问OS的代码和数据
    图解:
    图解
    注:
  1. 从③返回到用户态执行自定义信号处理方法,其实内核态也可以执行,但是群众里有坏人,使用内核执行内核的代码,用户执行用户的代码。
  2. sighandler和main函数使用不同的堆栈空间
  3. 所有的系统调用也是在一张表中,是函数指针数组,调用系统调用本质是把其对应的系统调用号(数组下标)写到寄存器中
  4. 用户层看是sigreturn,在内核看sys_sigreturn

2. 接口——sigaction

捕捉信号:如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数。
捕捉信号在上面我们已经学习并使用了一个接口signal,在上面测试时,我们也发现9和19号信号是不能被捕捉的。

学习另一个捕捉方法:sigaction——用于设置信号处理方法

头文件:#include <signal.h>函数声明:int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);参数:1. signum:要设置信号处理函数的信号2. act(输入型参数):一个结构体指针。包含了信号的处理方法和标志位3. oldact(输出型参数):传出该信号原来的处理动作struct sigaction {void     (*sa_handler)(int);   //向内核注册信号处理函数sigset_t   sa_mask;            };sa_mask:当调用信号处理函数时,除了当前信号被自动屏蔽之外,屏蔽别的信号用sa_mask,信号处理函数返回时自动恢复原来的屏蔽字返回值:成功返回0,失败返回-1,错误码被设置

sigaction使用:

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cstring>using namespace std;void PrintPending()
{sigset_t set;sigpending(&set);  //把pending位图带出来for(int signo = 1; signo <= 31; signo++){if(sigismember(&set, signo)){cout << "1";}else{cout << "0";}}cout << endl;
}void handler(int signo)
{cout << "catch a signal, signal num:" << 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;     //自定义// act.sa_handler = SIG_IGN;  //忽略// act.sa_handler = SIG_DFL;  //默认//阻塞指定信号sigemptyset(&act.sa_mask);  //初始化工作sigaddset(&act.sa_mask, 3); //阻塞3号信号sigaddset(&act.sa_mask, 4); //阻塞4号信号sigaddset(&act.sa_mask, 5); //阻塞5号信号//捕获2号信号sigaction(2, &act, &oact);while(true){cout << "I am a process! pid:" << getpid() << endl;sleep(1);}return 0;
}

测试结果:
测试结果

结论:

  1. pending位图由1置0的时间是:执行信号捕捉方法前,先清0,再调用
  2. 信号被处理时,对应的信号会被自动添加到block表中,防止嵌套调用
    eg:正在处理自定义信号处理函数时,完全有可能在自定义函数中再次陷入内核,这时再收到一样的信号,就嵌套了。
  3. 也证明了前面所说,普通信号在递达前产生多次只记一次

四、拓展

1. 可重入函数

1. 引入问题

不同执行流访问一个链表的插入函数:
图解:
图解

信号可以理解为假执行流,可以影响到可重入函数问题:
图解

  1. main函数调用insert插入节点,插入操作分为两步,刚执行完第一步,因为硬件中断,切换到内核,完成相应的处理之后,返回用户态
  2. 返回用户态要检测信号有没有可以递达的,有执行信号处理函数
  3. 信号处理函数也调用了同一个链表,执行了插入节点操作
  4. 执行完信号处理函数,返回从主控流程上次被中断的地方继续执行
  5. 执行插入操作第二步
    结果:只有一个节点插入到链表,另一个节点丢失

2. 概念
上述问题中,insert函数被main和handler(假执行流)执行流重复进入,导致节点丢失,内存泄漏。

不可重入函数:如果一个函数被重复进入,导致可能出错。eg:insert
反之就是可重入函数

3. 局部变量
上述的insert函数访问的是一个全局的链表,有可能因为重入造成错乱,如果一个函数只访问自己的局部变量或参数就不会错乱。原因是:每个控制流程调用函数时都会创建一个新的栈帧,它们实际上操作的是各自栈帧中的不同副本,不会相互影响,从而避免了数据错乱。

2. volatile

volatile:保持内存的可见性。告诉编译器该关键字修饰的变量可以在程序的其他部分改变,不应该对其进行优化处理。每次访问“volatile”变量时,都应该从内存读取数据,而不是从寄存器中获取。

验证代码:
代码描述:在main函数中写个死循环,当收到2号信号时程序结束

#include <iostream>
#include <signal.h>using namespace std;int flag = 0;void handler(int signo)
{cout << "catch a signal, signal num:" << signo << endl;flag = 1;
}int main()
{signal(2, handler);while(!flag);cout << "process quit normal" << endl;return 0;
}

运行结果:

  1. 不进行优化编译:g++ -o $@ $^ -std=c++11
    编译结果
    符合我们的预期结果

  2. 进行优化编译: g++ -o $@ $^ -O3 -std=c++11
    编译结果
    进程不终止了

解释优化编译的结果:
代码:在代码中定义的flag变量,并被设置为循环条件。
优化:flag符合下面两个条件,所以被编译器优化到CPU寄存器中。

  1. flag在代码中没有被更改(如果信号不被触发,那将会一直不被修改)
  2. !flag是一种计算(逻辑计算)。(计算中有两种计算,一种算术计算,一种逻辑计算)

解决办法:

volatile int flag = 0;   //在定义变量flag时,使用volatile关键字修饰

运行结果:
运行结果

3. SIGCHLD——17

子进程在退出的时候,不是直接就退出了,而是会主动向父进程发送17号信号——SIGCHLD

测试代码1: 看看是不是子进程退出会给父进程发送17号信号
代码描述:父进程死循环,子进程执行五秒自动退出,对17号信号捕捉

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstdlib>using namespace std;void handler(int signo)
{cout << "catch a signal, signum:" << signo << endl;
}int main()
{signal(17, handler);pid_t id = fork();if(id == 0){//childwhile(true){cout << "I am child proc, pid: "<< getpid() << " ppid: " << getppid() << endl;sleep(5);break;}exit(0);}while(true){cout << "I am father proc, pid:" << getpid() << endl;sleep(1);}return 0;
}

运行结果:符合预期,结果也是17号信号
运行结果
所以,我们也就可以在信号处理方法哪里进行等待。

测试代码2: 多个子进程,分批退出,或者一起退出的情况

问题1:同时退出可能会出现多个信号同时发送给父进程,只执行一次
解决方式:采用循环的形式,直到这一批子进程全部退出
问题2:一部分一部分的退出
解决方式:非阻塞轮询的方式

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>#include <cstdlib>using namespace std;void handler(int signo)
{cout << "catch a signal, signum:" << signo << endl;int id = -1;while ((id = waitpid(-1, nullptr, WNOHANG)) > 0)   //非阻塞轮询的方式{cout << "I am proc, pid: " << getpid() << "  "<< "wait success! pid: " << id << endl;}
}int main()
{signal(17, handler);for (int i = 0; i < 10; i++){pid_t id = fork();if (id == 0){// childwhile (true){cout << "I am child proc, pid: " << getpid() << " ppid: " << getppid() << endl;sleep(5);break;}exit(0);}//创建的慢一点sleep(1);}while (true){cout << "I am father proc, pid:" << getpid() << endl;sleep(1);}return 0;
}

测试结果:所有子进程无论一起退出还是分批退出,都被正常等待成功。

测试代码3: 父进程必须要等待吗?不是

int main()
{signal(17, SIG_IGN);for (int i = 0; i < 10; i++){pid_t id = fork();if (id == 0){// childwhile (true){cout << "I am child proc, pid: " << getpid() << " ppid: " << getppid() << endl;sleep(5);break;}exit(0);}//创建的慢一点//sleep(1);}while (true){cout << "I am father proc, pid:" << getpid() << endl;sleep(1);}return 0;
}

运行结果:
运行结果

结论:讲SIGCHLD信号设置为SIG_IGN(忽略),这样fork出来的子进程,在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程

注:

signal(17, SIG_DFL);

更改这一句代码的运行结果:信号默认处理动作还是会产生僵尸进程
运行结果

系统默认的做法:
系统默认的忽略动作和用户用sigaction函数自定义的忽略一般没有区别,SIGCHLD这个信号属于特例。
特例

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/751254.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

spring注解驱动系列--AOP探究一

一、AOP--动态代理 指在程序运行期间动态的将某段代码切入到指定方法指定位置进行运行的编程方式 二、使用栗子 一、导入aop模块 <dependency><groupId>org.springframework</groupId><artifactId>spring-aspects</artifactId><version>4…

虚拟机开机字体变大,进入系统后字体模糊

问题 虚拟机开机字体变大&#xff0c;进入系统后字体模糊。 原因 虚拟机配置问题。 解决办法 修改配置为如下:

资深老鸟经验,性能测试-性能指标分析总结,一篇策底概全...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 性能测试指标 1、…

leetcode代码记录(不同路径

目录 1. 题目&#xff1a;2. 我的代码&#xff1a;小结&#xff1a; 1. 题目&#xff1a; 一个机器人位于一个 m x n 网格的左上角 &#xff08;起始点在下图中标记为 “Start” &#xff09;。 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角&#xff08;在…

Python实现24点游戏

24点游戏是一种数学益智游戏&#xff0c;它的目标是通过使用加法、减法、乘法和除法这四种基本算术运算&#xff0c;使得四个数字的结果等于24。这个游戏不仅能锻炼玩家的数学计算能力&#xff0c;还能提高逻辑思维和快速反应能力。 游戏规则非常简单&#xff1a; 游戏通常使…

如何使用ArcGIS Pro生成带计曲线等高线

等高线作为常见的地图要素经常会被使用到&#xff0c;一般情况下生成的等高线是不带计曲线的&#xff0c;在某些情况下我们需要带计曲线的等高线&#xff0c;这里为大家介绍一下ArcGIS Pro生成带计曲线等高线的方法&#xff0c;希望能对你有所帮助。 数据来源 教程所使用的数…

Node.js 文件夹遍历技巧:实现高效的文件管理

在 Node.js 开发中&#xff0c;经常需要对文件系统进行操作&#xff0c;包括遍历文件夹以获取文件列表。本文将讨论使用 Node.js 遍历文件夹的几种常用方法&#xff0c;并通过一个实际案例来演示如何实现。 基本概念 在开始之前&#xff0c;让我们了解一些基本的概念&#xff…

关 于 重 燃 学 习 的 热 情

3月1日是我回学校的第一天。经历了长达8个月在家的昏暗时刻&#xff0c;我这10天的感觉和在家的感觉发生了翻天覆地的变化&#xff0c;最明显的莫过于学习状态的改变。 倒不是说在家学的不好&#xff0c;而是说在学校&#xff0c;我对学习的整体感觉&#xff0c;以及专注程度&…

微信小程序Skyline模式自定义tab组件胶囊与原生胶囊平齐,安卓和ios均自适应

进入下面小程序可以体验效果&#xff1a; 至于原理的话&#xff0c;解释起来毕竟麻烦&#xff0c;各位可以看源码自己分析。其实很简单&#xff0c;就算计算布局。很多网上公布的布局&#xff0c;都不能正常自适应。在下这个是完美可以的 1、WXML <view class"weui…

Kubernetes operator系列:kubebuilder 实战演练 之 开发多版本CronJob

云原生学习路线导航页&#xff08;持续更新中&#xff09; 本文是 Kubernetes operator学习 系列文章&#xff0c;本节会在上一篇开发的Cronjob基础上&#xff0c;进行 多版本Operator 开发的实战 本文的所有代码&#xff0c;都存储于github代码库&#xff1a;https://github.c…

【全面了解自然语言处理三大特征提取器】RNN(LSTM)、transformer(注意力机制)、CNN

目录 一 、RNN1.RNN单个cell的结构2.RNN工作原理3.RNN优缺点 二、LSTM1.LSTM单个cell的结构2. LSTM工作原理 三、transformer1 Encoder&#xff08;1&#xff09;position encoding&#xff08;2&#xff09;multi-head-attention&#xff08;3&#xff09;add&norm 残差链…

【C语言】指针基础知识(一)

计算机上CPU&#xff08;中央处理器&#xff09;在处理数据的时候&#xff0c;需要的数据是在内存中读取的&#xff0c;处理后的数据也会放回内存中。 一,内存和地址 内存被分为一个个单元&#xff0c;一个内存单元的大小是一个字节。 内存单元的编号&#xff08;可以理解为门…

2024年值得创作者关注的十大AI动画创新平台

别提找大型工作室制作动画了。如今,AI平台让我们就可以轻松制作动画。从简单的文本生动画功能到复杂的角色动作,这些平台为各种类型的创作者提供了不同的功能。 AI已经有了长足的发展,现在它可以理解复杂的人类动作和艺术意图,将简单的输入转化成丰富而详细的动画。 下面…

【前端Vue】Vue3+Pinia小兔鲜电商项目第1篇:认识Vue3,1. Vue3组合式API体验【附代码文档】

全套笔记资料代码移步&#xff1a; 前往gitee仓库查看 感兴趣的小伙伴可以自取哦&#xff0c;欢迎大家点赞转发~ 全套教程部分目录&#xff1a; 部分文件图片&#xff1a; 认识Vue3 1. Vue3组合式API体验 通过 Counter 案例 体验Vue3新引入的组合式API vue <script> ex…

AJAX学习(四)

版权声明 本文章来源于B站上的某马课程&#xff0c;由本人整理&#xff0c;仅供学习交流使用。如涉及侵权问题&#xff0c;请立即与本人联系&#xff0c;本人将积极配合删除相关内容。感谢理解和支持&#xff0c;本人致力于维护原创作品的权益&#xff0c;共同营造一个尊重知识…

C++内存分布与动态内存管理

文章目录 :dizzy: C/C内存分布:dizzy:C语言中动态内存管理方式  :sparkles:malloc   :sparkles:calloc  :sparkles:reallocfree :dizzy:C语言中动态内存管理方式  :sparkles:new和delete操作内置类型  :sparkles:new和delete操作自定义类型 :dizzy:operator new与ope…

数星星 刷题笔记 (树状数组)

依题意 要求每个点 x, y 的左下方有多少个星星 又因为 是按照y从小到大 给出的 所以 我们在计算个数的时候是按照y一层层变大来遍历的 因此我们在处理每一个点的时候 只需要看一下 当前的点有多少个点的x值比当前点小即可 树状数组的 操作模板 P3374 【模板】树…

动态规划题目集一(代码 注解)

目录 介绍&#xff1a; 题目一: 题目二&#xff1a; 题目三&#xff1a; 题目四&#xff1a; 题目五&#xff1a; 题目六&#xff1a; 题目七&#xff1a; 题目八&#xff1a; 题目九&#xff1a; 介绍&#xff1a; 动态规划是一种算法设计技术&#xff0c;用于解决具有重叠…

水泵房远程监控物联网系统

随着物联网技术的快速发展&#xff0c;越来越多的行业开始利用物联网技术实现设备的远程监控与管理。水泵房作为城市供水系统的重要组成部分&#xff0c;其运行状态的监控与管理至关重要。HiWoo Cloud作为专业的物联网云服务平台&#xff0c;为水泵房远程监控提供了高效、稳定、…

Java访问数据库(重点:SpringBoot整合Mybatis)

目录 一、通过JDBC访问数据库1、思路2、示例3、思考 二、通过ORM框架访问数据库&#xff08;主要是Mybatis&#xff09;1、示例1.1 配置1.2 SQL写在xxxMapper.xml中&#xff1a;mapper/UserMapper.xml1.3 xxxMapper.xml对应的xxxMapper接口&#xff08;Application通过该接口访…