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

news2024/9/23 23:35:58

目录

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.cc
	g++ -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; // 4

    pid_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)) //在信号集里,输出1
            cout << "1";
        else
            cout << "0";
    }
    cout << endl;
}

int main(int argc, char *argv[])
{
    // 0. 方便测试,捕捉2号信号,不要退出
    signal(2, catchSig);
    // 1. 定义信号集对象
    sigset_t bset, obset; // b是block,o是old
    sigset_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)) //在信号集里,输出1
            cout << "1";
        else
            cout << "0";
    }
    cout << endl;
}

int main(int argc, char *argv[])
{
    // 0. 方便测试,捕捉2号信号,不要退出
    signal(2, catchSig);
    // 1. 定义信号集对象
    sigset_t bset, obset; // b是block,o是old
    sigset_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.coloradmin.cn/o/1110853.html

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

相关文章

day08_面向对象_封装_继承_this_super_访问修饰符

今日内容 1.作业 2.封装 3.继承 4.this和super 5.访问修饰符 零、复习 成员变量和局部变量(画表格) this的作用 this是当前对象,当前方法的调用者this可以调用属性和方法this.属性, this.方法名(),this() 构造方法的作用和语法特征 作用: 创建对象,属性初始化特征: 没有返回值,…

数据结构和算法(13):优先级队列

概述 按照事先约定的优先级&#xff0c;可以始终高效查找并访问优先级最高数据项的数据结构&#xff0c;也统称作优先级队列 优先级队列将操作对象限定于当前的全局极值者。 根据数据对象之间相对优先级对其进行访问的方式&#xff0c;与此前的访问方式有着本质区别&#xf…

PostgreSQL与MySQL数据库对比:适用场景和选择指南

数据库是现代应用程序的基石之一&#xff0c;而在选择合适的数据库管理系统&#xff08;DBMS&#xff09;时&#xff0c;开发者常常会面临着许多选择。在这方面&#xff0c;PostgreSQL和MySQL是两个备受瞩目的选项。本文将深入研究这两者之间的异同&#xff0c;并为您提供适用场…

疯狂星期四的营销策略是什么?如何复制?

你知道疯狂星期四吗&#xff1f;它的策略是什么&#xff1f;如何对标它写一个类似的方案呢&#xff1f; 1、消费者心理 KFC疯狂星期四的核心目标消费者是对价格敏感的年轻人和家庭消费者。他们寻求物有所值的美食体验&#xff0c;希望在合理的价格范围内享受到美味的食物。通过…

Unity之ShaderGraph如何实现UV抖动

前言 今天我们通过噪波图来实现一个UV抖动的效果。 如下图所示&#xff1a; 关键节点 Time&#xff1a;提供对着色器中各种时间参数的访问 UV&#xff1a;提供对网格顶点或片段的UV坐标的访问。可以使用通道下拉参数选择输出值的坐标通道。 SimpleNoise&#xff1a;根据…

论文导读 | 支持事务与图分析的图存储系统

事务系统保证了系统的数据一致性&#xff0c;确保事务更新的原子性或是不同事务之间的数据隔离性等在多线程并发环境下所必不可少的ACID特性。而在今天快速变化的商业环境下&#xff0c;诸如物流和供应链&#xff0c;金融风控和欺诈检测等场景都需要图分析系统提供对数据动态更…

Node.js在Python中的应用实例解析

随着互联网的发展&#xff0c;数据爬取成为了获取信息的重要手段。本文将以豆瓣网为案例&#xff0c;通过技术问答的方式&#xff0c;介绍如何使用Node.js在Python中实现数据爬取&#xff0c;并提供详细的实现代码过程。 Node.js是一个基于Chrome V8引擎的JavaScript运行时环境…

SEAL:RLWE-BFV 开源算法库

参考文献&#xff1a; GitHub - microsoft/SEAL: Microsoft SEAL is an easy-to-use and powerful homomorphic encryption library.[HS13] Halevi S, Shoup V. Design and implementation of a homomorphic-encryption library[J]. IBM Research (Manuscript), 2013, 6(12-15…

UML类图关系(泛化 、继承、实现、依赖、关联、聚合、组合)

在UML类图中&#xff0c;常见的有以下几种关系: 泛化&#xff08;Generalization&#xff09;, 实现&#xff08;Realization&#xff09;&#xff0c;关联&#xff08;Association)&#xff0c;聚合&#xff08;Aggregation&#xff09;&#xff0c;组合(Composition)&#x…

【python零基础入门学习】python进阶篇之OOP - 面向对象的程序设计

本站以分享各种运维经验和运维所需要的技能为主 《python零基础入门》&#xff1a;python零基础入门学习 《python运维脚本》&#xff1a; python运维脚本实践 《shell》&#xff1a;shell学习 《terraform》持续更新中&#xff1a;terraform_Aws学习零基础入门到最佳实战 《k8…

MS5248数模转换器可pin对pin兼容AD5648

MS5228/5248/5268 是一款 12/14/16bit 八通道输出的电压型 DAC&#xff0c;内部集成上电复位电路、可选内部基准、接口采用四线串口模式&#xff0c;最高工作频率可以到 40MHz&#xff0c;可以兼容 SPI、QSPI、DSP 接口和 Microwire 串口。可pin对pin兼容AD5648。输出接到一个 …

潮玩IP助力环境保护,泡泡玛特发布行业首款碳中和产品

在今年的2023上海PTS国际潮流玩具展上&#xff0c;泡泡玛特正式发布了首款“碳中和”潮玩产品DIMOO X蒙新河狸手办&#xff08;下简称DIMOO河狸&#xff09;&#xff0c;通过环保主题与流行文化的联合&#xff0c;让年轻人知道野生动物保护有多种方式&#xff0c;同时以创新的设…

Crypto(3)NewStarCTF 2023 公开赛道 WEEK2|Crypto-不止一个pi

题目代码 from flag import flag from Crypto.Util.number import * import gmpy2 p getPrime(1024) #这行生成一个大约1024位长度的随机素数&#xff0c;并将其赋给变量p。 q getPrime(1024) #类似地&#xff0c;这行生成另一个大约1024位长度的随机素数&#xff0c;并将其…

Fortinet详解如何量化网安价值,把握网安态势

网络安全价值量化是企业准确把握自身网络安全态势的前提&#xff0c;也是网络安全管理人员持续推动工作的有力支撑。网络安全行业发展迅速&#xff0c;其价值量化也在不断地演进&#xff0c;如何进行量化则一直都是企业和网络安全管理人员的挑战。近期&#xff0c;Fortinet 委托…

【视觉算法系列2】在自定义数据集上训练 YOLO NAS(上篇)

提示&#xff1a;免费获取本文涉及的完整代码与数据集&#xff0c;请添加微信peaeci122 YOLO-NAS是目前最新的YOLO目标检测模型&#xff0c;它在准确性方面击败了所有其他 YOLO 模型。与之前的 YOLO 模型相比&#xff0c;预训练的 YOLO-NAS 模型能够以更高的准确度检测更多目标…

【三】kubernetes kuboard部署分布式系统

#服务器 #部署 #云原生 #k8s 目录 一、前言二、搭建docker私有仓库三、系统搭建1、NFS部署1)部署nfs server &#xff08;192.168.16.200&#xff09;2)部署nfs client &#xff08;全部节点&#xff09;3)在Kuboard中创建 NFS 存储类 2、创建命名空间3、添加docker密文4、创建…

Nginx 配置文件解读

一.配置文件解读 nginx配置文件主要分为四个部分&#xff1a; main{ #&#xff08;全局设置&#xff09;http{ #服务器配置upstream{} #&#xff08;负载均衡服务器设置&#xff09;server{ #&#xff08;主机设置&#xff1a;主要用于指定主机和端口&#xff09;location{} …

Flink学习之旅:(三)Flink源算子(数据源)

1.Flink数据源 Flink可以从各种数据源获取数据&#xff0c;然后构建DataStream 进行处理转换。source就是整个数据处理程序的输入端。 数据集合数据文件Socket数据kafka数据自定义Source 2.案例 2.1.从集合中获取数据 创建 FlinkSource_List 类&#xff0c;再创建个 Student 类…

5256C 5G终端综合测试仪

01 5256C 5G终端综合测试仪 产品综述&#xff1a; 5256C 5G终端综合测试仪主要用于5G终端、基带芯片的研发、生产、校准、检测、认证和教学等领域。该仪表具备5G信号发送功能、5G信号功率特性、解调特性和频谱特性分析功能&#xff0c;支持5G终端的产线高速校准及终端发射机…

Dev-C++ 软件安装教程

Dev-C 软件安装包https://download.csdn.net/download/W_Fe5/88446511&#xff08;软件包下载后&#xff0c;右键解压&#xff09; 一、打开文件夹&#xff0c;双击“Dev-C” 二、软件安装&#xff0c;点击“OK” 三、点击“I Agree” 四、点击“Next” 五、更改安装目录&…