零基础Linux_19(进程信号)产生信号+Core_Dump+保存信号

目录

1. 信号前期知识

1.1 生活中的信号

1.2 Linux中的信号

1.3 信号概念

1.4 信号处理方法的注册

2. 产生信号

2.1 通过终端按键产生信号

2.2 调用系统调用向进程发信号

2.3 软件条件产生信号

2.4 硬件异常产生信号

3. 核心转储Core Dump

4. 保存信号

4.1 信号在内核中的表示

4.2 信号集操作

4.2.1 信号集sigset_t:

4.2.2 信号集操作函数

4.3 代码使用实验

5. 所有测试代码

本篇完。


1. 信号前期知识

1.1 生活中的信号

从生活中入手,例如闹钟,红绿灯,狼烟,LOL游戏信号等等,这些都是信号。信号必须都是动态的,像路标就不能称之为信号。

以红绿灯为例,一看到红绿灯我们就知道红灯行,绿灯停,我们不仅能认识它是一个红绿灯,而且还知道应该产生什么样的行为,这样才算是能够识别红绿灯。识别 = 认识 + 行为产生。

对于红绿等这个信号,我们需要有如下几个共识:

  • 我们之所以能识别红绿灯,是因为我们受到过教育(手段),让我们在大脑中记住了不同颜色对应的行为(属性)。
  • 当绿灯亮了以后,不一定要立刻过马路,比如有其他的车闯红灯,需要进行避让,所以说我们不一定要立刻产生相应的行为。
  • 红灯亮了以后,正好来了一个电话,在接电话这个期间我们会记住此时是红灯,不会将这个状态忘记。
  • 红绿灯默认的行为是红灯行,绿灯停,但是也可以产生其他行为,还可以忽略。

现在将生活中红绿灯的例子迁移到进程中:信号是发给进程的。

进程之所以能够识别信号,是因为程序员将对应的信号种类和逻辑已经写好了的。

当信号发给进程后,进程不一定要立刻去处理,可能有更加紧急的任务,会在合适的时候去处理

进程收到信号到处理信号之前会有一个窗口期,这个期间要将收到的信号进行保存。

处理信号的方式有三种:默认动作,自定义动作,忽略。

再举个例子:

你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”
当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”
当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:

1.执行默认动作(幸福的打开快递,使用商品)

2.执行自定义动作(快递是零食,你要开始吃了)

3.忽略快递(快递拿上来之后,扔到床头,继续开一把游戏)
快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话

我们学习信号是学习它的整个生命周期,分为产生信号,保存信号,处理信号。但是在这之前先需要学习一些预备知识。

1.2 Linux中的信号

用户输入命令, 在Shell下启动一个前台进程。
用户按下Ctrl C, 这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程
前台进程因为收到信号,进而引起进程退出:

这里进程就是你,操作系统就是快递员,信号就是快递。

注意:

① Ctrl C 产生的信号只能发给前台进程。一个命令后面加个 & 可以放到后台运行, 这样Shell不必等待进程结束就可以接受新的命令, 启动新的进程。
② Shell可以同时运行一个前台进程和任意多个后台进程, 只有前台进程才能接到像 Ctrl - C 这种控制键产生的信号。
③ 前台进程在运行过程中用户随时可能按下 Ctrl - C 而产生一个信号, 也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止, 所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

再看什么是Linux信号?

本质是一种通知机制,用户 or 操作系统通过发送一定的信号,通知进程,某些事件已经发生,你可以在后续进行处理。

结合进程得出的信号结论
① 进程要处理信号,必须具备信号“识别”的能力 (看到 + 处理动作作)
② 凭什么进程能够“识别”信号呢? 程序员
③ 信号产生是随机的,进程可能正在忙自己的事情,信号的后续处理,可能不是立即处理的

④ 信号会临时的记录下对应的信号,方便后续进行处理
⑤ 在什么时候处理呢? 合适的时候
⑥ 一般而言,信号的产生相对于进程而言是异步的

1.3 信号概念

信号是进程之间事件异步通知的一种方式,属于软中断。

用kill - l命令可以察看系统定义的信号列表:

白色区域的是普通信号,编号从1-31。

其它区域的是实时信号,编号从34-64。

这其中没有32号和33号信号,所以一共有62个信号。而且这里我们只学习普通信号,对实时信号暂不做研究。

在使用这些信号时,可以用信号名,也可以用信号编号,它是一样的,都是宏定义后的结果。

每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,

例如其中有定义 #define SIGINT 2

这些信号各自在什么条件下 产生,默认的处理动作是什么,在signal(7)中都有详细说明:

man 7 signal然后下滑:

根据我们对Linux的了解,信号存放在哪里呢?既然信号是给进程的,而进程是通过内核数据结构来管理的,所以我们可以推断出,信号放在进程的task_struct结构体中。

既然它是在PCB中,而且数量是31个,task_struct中必定不会设置31个变量来存放信号,数组还有可能,但是信号的状态只分为有和没有两种,所以再次推断,31个信号放在一个32位的整形变量中,每个比特位代表一个信号。写一段伪代码来示意一下:

struct task_struct
{// 进程属性unsigned int signal;// .......
}

就像在学习基础IO和进程间通信的时候,那些flags标志中的不同的比特位代表着不同的意义,这31个信号量也是这种方式:

 

问题来了,内核数据结构的修改,这个工作是由谁来完成的?毫无疑问是操作系统,因为task_struct就是它维护的,而且是存在于内存中的,只有操作系统才有权力去修改它,用户是无法直接操作的,因为操作系统不相信任何人。

所以说,无论哪个信号,最后的本质都是由操作系统发生给进程的,这里的发送本质就是在修改task_struct中存放信号哪个变量的比特位。

信号发送的本质就是在修改PCB中的信号位图。

无论未来我们学习了多少中发送信号的方式,本质都是通过操作系统向目标进程发送信号。

所以操作系统一定会提供相关的系统调用,比如我们之前使用过的各自信号:

kill -9 pid值 	//停止某个进程
kill -19 pid值	//暂停某个进程

它们的底层一定是在调用相关的系统调用,来让操作系统修改PCB中的信号位图。

信号处理常见方式:

① 忽略此信号。

② 执行该信号的默认处理动作。

③ 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉 (Catch)一个信号。

1.4 信号处理方法的注册

所谓的注册,就是告诉操作系统,当某个进程接收到某个信号后的处理方式。

既然是告诉操作系统,那么肯定会用到系统调用,该系统调用的名字是signal,man 2 signal:

  • int signal:要注册的信号编号
  • sighandler_t handler:自定义的函数指针

可以将信号的处理方式写成一个函数,然后将函数名传递个signal,此时当进程接收到signum指定的信号编号时,就会执行我们定义的函数。

2. 产生信号

有了上面的知识以后,就可以正式来研究信号了,先来看看产生信号的几种方式。

2.1 通过终端按键产生信号

也就是在键盘上按一些热键,来给进程发送相应的信号,比如上面讲的ctrl c,它产生的是2号信号SIGINT,还有常用按键ctrl \,它产生的是3号信号SIGQUIT。怎么验证呢?

写个正经点的代码了:

Makefile

mykill:mykill.ccg++ -o $@ $^ -std=c++11 -g
.PHONY:clean
clean:rm -f mykill

mykill.cc

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;void catchSig(int signum)
{cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl;
}int main()
{signal(SIGINT, catchSig); // 特定信号的处理动作,一般只有一个signal(SIGQUIT, catchSig);// signal函数,仅仅是修改进程对特定信号的后续处理动作,不是直接调用对应的处理动作// 如果后续没有任何SIGINT信号产生,catchSig永远也不会被调用while(1){cout << "I am a process,my pid: " << getpid()<< endl;sleep(1);}return 0;
}

如图,得证,也演示了还可以用其它信号终止进程。

2.2 调用系统调用向进程发信号

和命令一样名字的系统调用kill(),man 2 kill: 

  • pid_t pid:要给发信号的pid
  • int sig:要发送的信号编号
  • 返回值:发送成功返回0,失败返回-1

该系统调用是一个进程给另一个进程发送指定信号,可以向任意进程发送任意信号。

mykill.cc

#include <iostream>
#include <cstring>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;// void catchSig(int signum)
// {
//     cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl;
// }static void Usage(string proc)
{cout << "Usage:\r\n\t" << proc << " signumber processid" << endl;
}int main(int argc, char *argv[])
{if(argc != 3) // ./mykill 9 pid{Usage(argv[0]);exit(1);}int signumber = atoi(argv[1]);int procid = atoi(argv[2]); // 获取两个命令行参数并转化int ret = kill(procid, signumber);if(ret != 0){cerr << errno << ": " << strerror(errno) << endl;}return 0;
}

首先创建了一个睡眠进程,然后用自己写的mykill给其发了9号信号。

给自己发信号的系统调用raise(),man raise:

 

编译运行:

可以发现并没有执行raise后面的代码

给自己发送6号信号的系统调用abort(),man abort:

编译运行:

虽然有3个系统调用来产生信号,但是归根到底都是在使用kill系统调用。

  • kill()可以给任意进程发送任意信号。
  • raise()可以给自己发送任意信号。
  • abort()可以给自己发送6号SIGABRT信号。

2.3 软件条件产生信号

验证一下管道当读端关闭的时候,写端所在进程就会收到编号为13的SIGPIPE信号结束进程:

pipe.cc

#include <iostream>
#include <cerrno> // C++包C语言头文件常用的方法,和.h效果一样
#include <cstring>
#include <cassert>
#include <unistd.h> // pipe + close + read + write
#include <sys/types.h> // waitpid两个头文件
#include <sys/wait.h>
#include <signal.h>using namespace std;void catchSig(int signum)
{cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl;
}int main()
{int pipefd[2];int ret = pipe(pipefd); // 一.创建管道if(ret < 0){cerr << errno << ": " << strerror(errno) << endl;}// cout << "pipefd[0]: " << pipefd[0] << endl; // 3// cout << "pipefd[1]: " << pipefd[1] << endl; // 4pid_t id = fork(); // 二.创建子进程assert(id != -1);if (id == 0) // 子进程,读,关闭写{close(pipefd[1]);// 三. 子进程读while (true){int cnt = 5;char buffer[1024 * 8];ssize_t s = read(pipefd[0], buffer, sizeof(buffer) - 1); // read读cout << "我是子进程,我的pid: " << getpid()<< endl;if(cnt--) // 5秒后关闭读端{close(pipefd[0]);exit(0);}sleep(1);}}signal(SIGPIPE, catchSig); // 特定信号的处理动作,一般只有一个close(pipefd[0]); // 父进程,写,关闭读// 四. 父进程写char send_buffer[1024 * 8];while (true){cout << "我是父进程,我的pid: " << getpid()<< endl;ssize_t s = write(pipefd[1], send_buffer, strlen(send_buffer));sleep(1); // 父进程一直写,不关闭写端}return 0;
}

编译运行;

在读端关闭以后,写端的自定义处理方式中就接收到了系统发给的SIGPIPE信号,编号为13。

(管道,读端不光不读,而且还关闭了,写端一直在写,会发生什么问题?写没有意义,OS会自动终止对应的写端进程,通过发送13信号SIGPIPE的方式,这就是软件条件产生信号)

  •  读端是否关闭是软件中的条件。
  •  当条件达成以后,产生信号。

下面介绍一下alarm函数和SIGALRM信号

闹钟触发的信号:

闹钟就是系统中的定时器,使用的时候同样需要通过系统调用实现:

  • 参数:要定的时长。
  • 返回值:距离定的时间还差多少。

验证1s之内,一共会进行多少次count++:定时1秒钟,在循环中进行疯狂加1,设置自定义处理方式,打印定时到后收到的信号编号,并且统计这一秒中内进行了多少次加1操作。

编译运行:

有点延迟的打印了四万多次,这是包括IO的,如果单纯向计算算力呢?:

五亿多,这就发现我们计算机还是计数得很快的。

上面代码也写了闹钟自动触发就移除了,我们在任务里重设一个闹钟,就实现了定时器的功能:

编译运行:

成功实现,你也可以把int cnt改成无符号的。

如何理解软件条件给进程发送信号?:

OS先识别到某种软件条件触发或者不满足,然后构建信号,发送给指定进程。

2.4 硬件异常产生信号

除0操作导致的硬件异常:

编译运行:

  •  在运行的时候,直接出错,没有再执行下去,是因为接收到了信号。
  •  接收到的信号是SIGFPE信号,编号为8号。

这其实就是一种硬件异常产生的信号。

CPU中有很多的寄存器,例如eax,ebx,eip等等。CPU会从内存中将代码中的变量拿到寄存器中进行运算,如果有必要,还会将运算的结果放回到内存中。

还有一个状态寄存器,如果CPU在运算的时候发现了除0操作,就会将状态寄存器的溢出标志位置一。此时就意味着硬件产生了异常。而操作系统是一个进行软硬件资源管理的软件,CPU的中状态寄存器的溢出标志位置一后,操作系统可以第一时间拿到。除0导致硬件异常以后,操作系统会给对应的进程发送SIGFPE信号。
当进程接收到SIGFPE信号以后,默认的处理方式就是结束进程。

现在我们对这个SIGFPE信号注册一个自定义处理方式:

编译运行:

怎么这个信号被操作系统不停的发送给这个进程?

进程收到信号后进程不退出,随着CPU时间片的轮转就会再次被调到。
CPU中只有一份寄存器,但是寄存器中的内容属于当前进程的上下文。
当进程被切换的时候,就有无数次的状态寄存器被保存和恢复的过程。
而除0操作导致的溢出标志位置一的数据还会被恢复到CPU中。
所以每一次恢复的时候,操作系统就会识别到,并且给对应进程发送SIGFPE信号。


所以就会导致上面不停调用自定义处理函数,不停打印接收到的信号编号。

如何理解除0呢?:

① 进行计算的是CPU,这个硬件。

② CPU内部是有寄存器的,状态寄存器(位图),有对应的状态标记位, 溢出标记位, OS会自动进行计算完毕之后的检测,如果溢出标记位是1, OS里面识别到有溢出问题,立即只要找到当前谁在运行提取PID,OS完成信号发送的过程,进程会在合适的时候,进行处理。

③ 一旦出现硬件异常,进程一定会退出吗?

不一定,一般默认是退出,但是我们即便不退出,我们也做不了什么

④ 为什么会死循环?寄存器中的异常一直没有被解决。

解引用空指针导致的硬件异常:

编译运行:

上面代码中存在对空指针的解引用操作,空指针的本质是(void*)0,而0地址处是不允许我们用户进行访问的,这部分属于内核空间。

  • 运行的时候直接出错,没有再运行下去,也是因为接收到了信号。
  •  接收到的信号是SIGSEGV,编号是11。

这同样是一种硬件异常产生的信号。

  • 我们之前一直谈论的页表实际上是页表+MMU,而MMU是在CPU中的,为了简便,我们就只说页表。
  •  进程地址空间和物理内存之间的映射关系实际上是有MMU去完成映射的。
  • 当对空指针解引用的时候,MMU会拒绝这种操作,从而产生异常标志。
  • 操作系统拿到MMU产生的异常以后就会给对应的进程发送SIGSEGV信号。

当进程接收到编号为11的SIGSEGV信号以后,默认的处理动作就是结束进程。

将这个信号注册自定义处理方式,同样打印接收到的信号编号,但是不结束进程,可以看到,和除0操作一样,不停的打印。

如何理解野指针或者越界问题?:

① 都必须通过地址,找到目标位置。

② 我们语言上面的地址,全部都是虚拟地址。

③ 将虚拟地址转成物理地址。

④ 页表+ MMU((Memory Manager Unit是硬件)

⑤ 野指针,越界, 使用非法地址,MMU转化的时候,一定会报错。

  • 硬件异常所产生的信号,如果不结束这个进程,我们是没有能力去处理这个进程的。
  • 随着时间片的轮转,这个导致硬件异常的进程还会不停的调到,所以操作系统会不停的向进程发送信号。

 硬件异常产生的信号并不会显示发送,而是由操作系统自动发送的。

产生信号总结思考:

上面所说的所有信号产生,最终都要有OS来进行执行,为什么?

OS是进程的管理者。

信号的处理是否是立即处理的?

在合适的时候才处理。

信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?

是的,记录在PCB对应的信号位图当中。

一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?

能知道,因为这是程序员帮我们写好的。

如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?

OS去修改位图,根据信号编号修改特定比特位,由比特位由0置1。

如何理解系统调用接口?

系统调用接口执行OS对应的系统调用代码,然后OS提取参数,或者设置特定的数值,再然后OS向目标进程写信号,修改对应进程的信号标记位,进程后续会处理信号,最后执行对应的处理动作。

3. 核心转储Core Dump

以前在Linux_10_进程等待讲的:

学了上面信号的知识,是否有一个疑问,31个信号的默认处理方式都是结束进程,并且还可以自定义处理方式,那么为什么要这么多信号呢?一个信号不就行了吗?

  • 重要的不是产生信号的结果而是产生信号的原因
  • 所有出现异常的进程,必然是收到了某一个信号。

man 7 signal 介绍了信号的名称,对应的编号,默认处理方式,以及产生该信号的原因:

我们可以根据这个表找到不同信号产生所对应的不同原因。

以信号2和3为例,他们的默认处理方式一个是Term,一个是Core。

  •  Term和Core的结果都是结束进程。

那么这两个方式的区别在哪里呢?Term方式仅仅是结束进程,结束了以后就什么都不干了。但是Core不仅结束进程,而且还会保存一些信息。

比如刚才使用了野指针收到的11号信号的默认处理方式就是Core,退出了,但会保存一些信息。

在云服务器上,默认情况下是看不到Core退出的现象的,这是因为云服务器默认关闭了core file选项,ulimit -a:

看到第一行,core file size的大小是0,意味着这个选项是关闭的。

  • 从这里还可以看到别的关于这个云服务器的信息,比如能够打开的最多文件个数,管道个数,以及栈的大小等等信息。

为了能够看到Core方式的明显现象,我们需要将core file选项打开,ulimit -c 1024:

此时该选项就打开了,表示的意思就是核心转储文件的大小是1024个数据块。

再运行使用野指针的程序,但是不捕捉信号了:

同样会收到11号信号停止。但是在当前目录下会多出一个文件,如下图。

  • core.7607:被叫做核心转储文件,其中后缀7607是接收到该信号进程的pid值。

对于一个奔溃的程序,我们最关心的是它为什么崩溃,在哪里崩溃?

当进程出现异常的时候,将进程在对应的时刻,

在内存中的有效数据转储到磁盘中:核心转储

核心转储的文件我们可以拿着它进行调试,快速定位到出现异常而崩溃的位置。

  • 使用gdb调试我们的可执行程序。
  • 调试开始后,输入core-file core.pid值,表明调试核心转储文件。
  • 此时gdb就会直接定位到产生异常的位置。

这就是核心转储的重要意义,它相比Term方式,能够让我们快速定位出现异常的位置。

再看Core Dump:当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误, 事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许 产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K:$ ulimit -c 1024

4. 保存信号

首先介绍几个新的概念:

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

注意: 阻塞和忽略是不同的,只要信号被阻塞就不会被递达,但是忽略是在递达之后进行的一种处理动作。

4.1 信号在内核中的表示

我们知道,信号是保存在内核数据结构中的,下面来看它具体的储存模型:

  • pending表:用来存放接收到的信号,操作系统向进程发送信号时,都会修改pending表中对应编号处的比特位。
  • block表:用来存放被阻塞的信号,当指定信号需要被阻塞时,操作系统会修改block表中对应编号处的比特位。
  • handler表:这是是一个数组,用来存放不同信号的处理方法,保存的是函数指针。
     

当我们使用signal注册一个自定义处理方式时,

操作系统会将我们定义的函数指针放在handler表中,在信号递达后调用。

如果是默认处理方式,会调用handler默认的初始函数指针所对应的函数。

  • 信号产生后,操作系统就会修改pending位图,使信号处于未决状态。

操作系统会按照一定的顺序来检查block表和pending表,然后去调用相应信号编号的处理方式来完成信号递达。大概逻辑(伪代码):

if(1<<(signo - 1) & pcb->block)
{//signo信号被阻塞,不会被递达
}
else
{if(1<<(signo - 1) & pcb->pending){//信号递达,处理该信号handler[signo - 1];}
}

操作系统在对信号进行检测的时候,先检测的是信号的block位图,如果对应信号的比特位被置一,说明该信号被阻塞,就不再去检测pending位图。如果没有被阻塞,才会去检测pending位图,如果pending位图相应的位被置一,再去调用handler表中的处理函数。

所以如果一个信号没有产生,但是并不妨碍它被阻塞。被阻塞的信号,在产生之后就会一直处于未决状态,不会被递达,只有当阻塞被解除后才会被递达。

  • 默认情况下,所有信号都是不被阻塞的,所有信号都没有产生,也就是block位图和pending位图都是0。

4.2 信号集操作

pending图,block图以及handler表是存放在内核数据结构中的,所以只能由操作系统来修改,我们用户如果要修改也能通过操作系统来实现,所以操作系统同样给我们提供了系统调用。

handler表中的函数指针可以通过系统调用signal来设置。

4.2.1 信号集sigset_t:

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来表示,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下面将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

用户在设置pending位图和block位图的时候,并不能直接让系统调用将内核中对于的比特位置1或清0,而是需要预先在一个变量中表达出我们的意愿,然后将这个变量通过系统调用给到操作系统,再由操作系统去修改内核数据结构。

操作系统给我们提供了一个sigset_t的变量类型,用户只需要对这个变量进行预设置,然后再交给操作系统。


系统提供的信号集操作函数操作的也是也是这个域先处理的变量,之所以也用系统调用来处理这个变量,是因为这个变量不单单是一个32位的整形变量,它的结构和内核是对应的,所以操作也要按照相应的规则。

从使用者的角度不必关心具体是如何操作的,只需要使用信号集操作函数来操作sigset_t变量即可。sigset_t变量用其他方式是无法操作的,比如用printf去打印,这是没有意义的。

4.2.2 信号集操作函数

对于block位图和pending位图的修改,操作系统提供了一族系统调用,称为信号集操作函数

man sigemptyset:

  • sigset_t set:信号集变量。
  • int signum:信号编号。
  • 返回值:成功返回0,失败返回-1。

sigemptyset:使所有信号对应的bit清零,表示该信号集不包含任何有效信号。
sigfillset:使所有信号对应的bit置位,表示该信号集的有效信号包括系统支持的所有信号。
sigaddset:使指定信号所对应的bit置位,表示该信号集中对应信号有效。
sigdetset:使指定信号所对应的bit清零,表示该信号集中对应信号无效。
sigismember:判断指定信号所对应的bit是否有效,返回类型是bool类型。

在使用sigset_t类型的变量之前,一定要调用sigemptyset进行初始化,使信号集处于确定状态。

此时我们已经对sigset_t变量预处理好了,下一步就是把这个变量交给操作系统了,操作系统同样提供了对应的系统调用。

sigprocmask()

该系统调用是专门用来修改内核数据结构中的block位图的。man sigprocmask:

  • int how:修改方式,有三个选项:
  • SIG_BLOCK:block在block原有位图基础上添加sigset_t变量中设置的比特位。
  • SIG_UNBLICK:unblock在bolck原有位图解除上删除sigset_t变量中设置的比特位。
  • SIG_SETMASK:setmask用sigset_t变量覆盖原有的block位图。一般使用这个。
  • set:我们设置好的sigset_t变量。
  • oldeset:这是一个输出型参数,将原本block位图输出到这个sigset_t变量中。
  • 返回值:设置成功返回0,失败返回-1。

sigpending()

这是专门用来获取内核数据结构中的pending位图的。man sigpending:

  • set:这是一个输出型参数,用来返回从内核中获取的pending位图情况。
  • 返回值:成功返回0,失败返回-1。

4.3 代码使用实验

前面的:man 7 signal 介绍了信号的名称,对应的编号,默认处理方式,以及产生该信号的原因:

(注意到表格下面的一句话,SIGKILL and SIGSTOP不能被捕捉,阻塞,忽略,这里9号信号就是管理员信号,就是防止你把所有信号都设定自定义动作,导致进程不能退出的情况,可以自己做一个实验验证)

我们还可以利用上面的系统调用做一个小的实验,来验证某个信号被阻塞后,它的pengding位图会被置一,但是不会被递达。mykill.cc:

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;void catchSig(int signum)
{cout << "进程捕捉到了一个信号: " << signum << " Pid: " << getpid() << endl;
}static void showPending(sigset_t &pending)
{for (int sig = 1; sig <= 31; sig++){if (sigismember(&pending, sig)) //在信号集里,输出1cout << "1";elsecout << "0";}cout << endl;
}int main(int argc, char *argv[])
{// 0. 方便测试,捕捉2号信号,不要退出signal(2, catchSig);// 1. 定义信号集对象sigset_t bset, obset; // b是block,o是oldsigset_t pending;// 2. 初始化sigemptyset(&bset);sigemptyset(&obset);sigemptyset(&pending);// 3. 添加要进行屏蔽的信号sigaddset(&bset, 2 /*SIGINT*/);// 4. 设置set到内核中对应的进程内部[默认情况进程不会对任何信号进行block]int n = sigprocmask(SIG_BLOCK, &bset, &obset); // sigset_t变量和老的sigset_t变量assert(n == 0); // sigprocmask成功了返回0(void)n; // 强转一下,防止relese下出现变量未被使用的警告cout << "block 2 号信号成功..., pid: " << getpid() << endl;// 5. 重复打印当前进程的pending信号集int count = 0;while (true){// 5.1 获取当前进程的pending信号集sigpending(&pending);// 5.2 显示pending信号集中的没有被递达的信号showPending(pending);sleep(1);count++;if (count == 20) // 20秒后恢复2号信号的block,1->0{// 默认情况下,恢复对于2号信号的block的时候,确实会进行递达// 但是2号信号的默认处理动作是终止进程,需要对2号信号进行捕捉->第0步cout << "开始解除对于2号信号的block" << endl;int n = sigprocmask(SIG_SETMASK, &obset, nullptr); // 用老的set恢复assert(n == 0);(void)n;cout << "解除对于2号信号的block成功" << endl;}}return 0;
}

5. 所有测试代码

这里放一些前面的所有测试代码,很多注释起来了:

#include <iostream>
#include <cstdlib>
#include <cstring>
#include <cassert>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
using namespace std;// int cnt = 0;
void catchSig(int signum)
{cout << "进程捕捉到了一个信号,正在处理中: " << signum << " Pid: " << getpid() << endl;// cout << "final cnt: " << cnt << " 信号: " << signum << " Pid: " << getpid() << endl;// alarm(1); // 重设闹钟 -> 定时器 -> 你可以实现任意任务
}// static void Usage(string proc)
// {
//     cout << "Usage:\r\n\t" << proc << " signumber processid" << endl;
// }static void showPending(sigset_t &pending)
{for (int sig = 1; sig <= 31; sig++){if (sigismember(&pending, sig)) //在信号集里,输出1cout << "1";elsecout << "0";}cout << endl;
}int main(int argc, char *argv[])
{// 0. 方便测试,捕捉2号信号,不要退出signal(2, catchSig);// 1. 定义信号集对象sigset_t bset, obset; // b是block,o是oldsigset_t pending;// 2. 初始化sigemptyset(&bset);sigemptyset(&obset);sigemptyset(&pending);// 3. 添加要进行屏蔽的信号sigaddset(&bset, 2 /*SIGINT*/);// 4. 设置set到内核中对应的进程内部[默认情况进程不会对任何信号进行block]int n = sigprocmask(SIG_BLOCK, &bset, &obset); // sigset_t变量和老的sigset_t变量assert(n == 0); // sigprocmask成功了返回0(void)n; // 强转一下,防止relese下出现变量未被使用的警告cout << "block 2 号信号成功..., pid: " << getpid() << endl;// 5. 重复打印当前进程的pending信号集int count = 0;while (true){// 5.1 获取当前进程的pending信号集sigpending(&pending);// 5.2 显示pending信号集中的没有被递达的信号showPending(pending);sleep(1);count++;if (count == 20) // 20秒后恢复2号信号的block,1->0{// 默认情况下,恢复对于2号信号的block的时候,确实会进行递达// 但是2号信号的默认处理动作是终止进程,需要对2号信号进行捕捉->第0步cout << "开始解除对于2号信号的block" << endl;int n = sigprocmask(SIG_SETMASK, &obset, nullptr); // 用老的set恢复assert(n == 0);(void)n;cout << "解除对于2号信号的block成功" << endl;}}// cout << "my pid: " << getpid() << endl;// for (int sig = 1; sig <= 31; sig++)// {//     signal(sig, catchSig);// }// while (true)// {//     sleep(1);// }// signal(SIGSEGV, catchSig);// cout << "my pid: " << getpid() << endl;// int *p = nullptr;// *p = 100;// while (true)// {//     sleep(1);// }// signal(SIGFPE,catchSig);// int cnt = 0;// while(true)// {//     cout << "正在运行的进程" << cnt++ << endl;//     int result = 7;//     result /= 0;//     sleep(1);// }// signal(SIGALRM,catchSig);// alarm(1); // 先设定了一个闹钟,这个闹钟一旦触发,就自动移除了// while(true)// {//     ++cnt;// }// cout << "我开始运行咯" << endl;// sleep(1);// abort(); // 通常用来进行终止进程。等于raise(6) 等于kill(getpid(), 6)// // raise(9); // 等于kill(getpid(), 8)// cout << "运行结束咯" << endl;// if(argc != 3) // ./mykill 9 pid// {//     Usage(argv[0]);//     exit(1);// }// int signumber = atoi(argv[1]);// int procid = atoi(argv[2]); // 获取两个命令行参数并转化// int ret = kill(procid, signumber);// if(ret != 0)// {//     cerr << errno << ": " << strerror(errno) << endl;// }// signal(SIGINT, catchSig); // 特定信号的处理动作,一般只有一个// signal(SIGQUIT, catchSig);// // signal函数,仅仅是修改进程对特定信号的后续处理动作,不是直接调用对应的处理动作// // 如果后续没有任何SIGINT信号产生,catchSig永远也不会被调用// while(1)// {//     cout << "I am a process,my pid: " << getpid()<< endl;//     sleep(1);// }return 0;
}

本篇完。

下一篇继续进程信号的内容:处理信号部分和一些信号的笔试面试题,再就是多线程的内容了。

下一篇:零基础Linux_20(进程信号)内核态和用户态+处理信号+不可重入函数+volatile。

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

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

相关文章

尚硅谷kafka3.0.0

目录 &#x1f483;概述 ⛹定义 ​编辑⛹消息队列 &#x1f938;‍♂️消息队列应用场景 ​编辑&#x1f938;‍♂️两种模式&#xff1a;点对点、发布订阅 ​编辑⛹基本概念 &#x1f483;Kafka安装 ⛹ zookeeper安装 ⛹集群规划 ​编辑⛹流程 ⛹原神启动 &#x1f938;‍♂️…

Windows网络监视工具

对于任何规模的企业来说&#xff0c;网络管理在信息技术中都起着至关重要的作用。管理、监控和密切关注网络基础设施对任何组织都至关重要。在Windows网络中&#xff0c;桌面&#xff0c;服务器&#xff0c;虚拟服务器和虚拟机&#xff08;如Hyper-V&#xff09;在Windows操作系…

C算法:写一个用于找出数组的最大值和最小值的函数

需求&#xff1a; 写一个用于找出数组的最大值和最小值的函数。 示例&#xff1a;int array[9] {5, 9, 3, 1, 2, 8, 4, 7, 6}; 该数组最大值的下标为1&#xff0c;最小值的小标为3。 代码实现&#xff1a; #include <stdio.h>int getNum(int *array,int len,int (*…

【C++面向对象】6. 指向类的指针

文章目录 【 1. 基本原理 】【 2. 实例 】 【 1. 基本原理 】 一个指向 C 类的指针与指向结构体的指针类似&#xff0c;访问指向类的指针的成员&#xff0c;需要使用 成员访问运算符 ->&#xff0c;就像访问指向结构的指针一样。 【 2. 实例 】 // 使用指向类的指针&…

PX4-Autopilot下载与编译

文章目录 1 Git clone 代码2 下载子模块3 编译4 可能遇到的问题参考 1 Git clone 代码 Github Repository 链接&#xff1a;PX4-Autopilot 查看现有版本&#xff1a; 在终端用命令下载&#xff0c;-b表示branch git clone -b v1.14.0 https://github.com/PX4/PX4-Autopilot.…

win10下u2net tensorrt模型部署

TensorRT系列之 Win10下yolov8 tensorrt模型加速部署 TensorRT系列之 Linux下 yolov8 tensorrt模型加速部署 TensorRT系列之 Linux下 yolov7 tensorrt模型加速部署 TensorRT系列之 Linux下 yolov6 tensorrt模型加速部署 TensorRT系列之 Linux下 yolov5 tensorrt模型加速部署…

Google Chrome的新“IP保护”功能将隐藏用户的IP地址

导语&#xff1a;在保护用户隐私方面&#xff0c;Google Chrome正在测试一项名为“IP保护”的新功能。通过使用代理服务器掩盖用户的IP地址&#xff0c;这项功能能够增强用户的隐私保护。在意识到IP地址可能被用于秘密追踪后&#xff0c;Google希望在确保用户隐私的同时&#x…

云原生微服务实战 Spring Cloud Alibaba 之 Nacos

系列文章目录 第一章 Java线程池技术应用 第二章 CountDownLatch和Semaphone的应用 第三章 Spring Cloud 简介 第四章 Spring Cloud Netflix 之 Eureka 第五章 Spring Cloud Netflix 之 Ribbon 第六章 Spring Cloud 之 OpenFeign 第七章 Spring Cloud 之 GateWay 第八章 Sprin…

【proteus】8086仿真/汇编:创建项目并添加汇编代码文件

1.创建好新项目 2.点击source code 弹出VSM 3. 4.注意两个都不勾选 可以看到schematic有原理图出现 5. 再次点击source code 6.project/project settings&#xff0c;取消勾选embed 7. add 8.输入文件名保存后&#xff1a; 注意&#xff1a;proteus不用写dos的相关语句 。

UA硬件安装环境

v2301硬件安装环境 Opcenter Execution Foundation 计算机至少应具有以下特征&#xff1a; 操作系统 RAM &#xff1a;最小 12 GB &#xff0c;建议 16 GB CPU &#xff1a;最小 2 vCPU &#xff08;建议频率 > 2.5 GHz &#xff09; HDD &#xff1a;高达 120 GB 的…

Node学习笔记之MySQL基本使用

使用 SQL 管理数据库 其实写接口简单来说就是操作数据库数据&#xff0c;所以我们需要学会数据库的增、删、查、改等基本操作 1. 什么是 SQL SQL&#xff08;英文全称&#xff1a;Structured Query Language&#xff09;是结构化查询语言&#xff0c;专门用来访问和处理数据…

Linux系列讲解 —— VIM配置与美化

目录 1. Vim基本配置1.1 配置文件1.2 基本配置 2. 插件管理器Vundle2.1 下载Vundle2.2 在vimrc中添加Vundle的配置 3. Vundle的使用3.1 安装插件3.2 卸载插件 1. Vim基本配置 1.1 配置文件 vim的配置文件有两处&#xff0c;请根据实际情况选择修改哪个。 (1) 全局配置文件&am…

实验数据旋转角度处理过程中的常见问题

问题 做实验过程中使用 EM tracker 测量自己机器人末端旋转时的角度。 尾部 设置EMTracker 1&#xff0c;作为固定基准&#xff0c;其轴线与机器人中心轴线近似重合&#xff0c;EM Tracker 2 固定在机器人活动关节上&#xff0c;两者轴线夹角近似为机器人旋转角度。论文尚未发…

Python----range方法(函数)

range 英 /reɪndʒ/ n. &#xff08;变动或浮动的&#xff09;范围&#xff0c;界限&#xff1b;视觉&#xff08;或听觉&#xff09;范围&#xff1b;v. &#xff08;在一定的范围内&#xff09;变化&#xff0c;变动&#xff1b;&#xff08;按一定位置或顺序&#x…

LeetCode算法心得——元素和最小的山形三元组 II(预处理和简单动规)

大家好&#xff0c;我是晴天学长&#xff0c;枚举&#xff0b;简单的动态规划思想&#xff0c;和前段时间的周赛题的写法可以说一模一样&#xff0c;像这种类似3元的题&#xff0c;要控制时间复杂度的话&#xff0c;只能枚举一个变量&#xff0c;所以要前缀和或者动规等待。需要…

FPGA时序分析与约束(6)——综合的基础知识

在使用时序约束的设计过程中&#xff0c;综合&#xff08;synthesis&#xff09;是第一步。 一、综合的解释 在电子设计中&#xff0c;综合是指完成特定功能的门级网表的实现。除了特定功能&#xff0c;综合的过程可能还要满足某种其他要求&#xff0c;如功率、操作频率等。 有…

深度学习中的不确定性综述

领域学者&#xff1a; http://www.gatsby.ucl.ac.uk/~balaji/ 论文标题&#xff1a; A Survey of Uncertainty in Deep Neural Networks 论文链接&#xff1a; https://arxiv.org/pdf/2107.03342.pdf 概要 在过去的十年中&#xff0c;神经网络几乎遍及所有科学领域&#x…

mysqld: File ‘./binlog.index‘ not found (OS errno 13 - Permission denied) 问题解决

问题背景 Centos7 安装Mysql 8后启动时遇到的问题&#xff0c;看了好几个博客方案无效&#xff0c;搞了半小时才找到正解&#xff0c;在此次进行记录。 在此假设你已经修改了对应目录的权限&#xff0c;比如配置的mysql data目录初始化后已经执行了chown -R mysql:mysql /XXX/…

leetcode:面试题 17.04. 消失的数字(找单身狗/排序/公式)

一、题目&#xff1a; 函数原型&#xff1a;int missingNumber(int* nums, int numsSize) 二、思路&#xff1a; 思路1 利用“找单身狗”的思路&#xff08;n^n0&#xff1b;0^nn&#xff09;&#xff0c;数组中有0-n的数字&#xff0c;但缺失了一个数字x。将这些数字按位异或0…

02-2、PyCharm中文乱码的三处解决方法

PyCharm中文乱码 修改处1&#xff1a; 修改处2&#xff1a;这个也没用 在Pycharm中可以创建一个模版&#xff0c;每次新建python文件时Pycharm会默认在前两行生成utf-8 #!/user/bin/env python3 # -- coding: utf-8 -- 还是乱码 再在这里设置以下 添加 &#xff1a; -Dfi…