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

Linux 进程信号

目录

一、了解信号

1、概念

2、前台进程和后台进程

3、信号的处理

4、硬件层面

5、信号与我们的代码是异步的

 二、信号的产生

1、产生的方式

2、键盘组合键

3、kill命令

4、系统调用

man kill

 man raise

man abort

 5、异常软件条件

(1)异常产生信号

(2)软条件——闹钟

(3)core dump

三、信号保存

1、信号其他的相关概念

2、在内核中表示

3、sigset_t

4、信号集操作函数

(1)sigprocmask

(2)sigpending


我们进程通信讲的信号量和信号没有任何关系

一、了解信号

1、概念

我们举个例子了解一下信号

在我们的生活中处处可见信号,就比如我们网购了一件商品,这个快递在运输的过程,我们已经能够知道这个商品或被用来做什么,因为我们能识别快递

当快递送到了,而我们正在打游戏还有五分钟结束游戏,我们此时不会立即去取快递,而是打完这把游戏再去拿快递,也就是说拿快递这个过程不是立即执行的,我们可以理解成在合适的时间去取

在我们收到快递到了这个信息和拿到这个快递,中间会存在时间窗口。当我们收到这个快递,我们会有三种执行方式:1、默认动作(拆开快递直接使用),2、忽略动作(取到快递直接放一边) 3、自定义动作(我们把这个快递送给别人)。

快递运输到取快递这个过程对我们来说是异步的。

异步的理解:

异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。
异步就相当于当客户端发送给服务端请求时,在等待服务端响应的时候,客户端可以做其他的事情,这样节约了时间,提高了效率。

 在我们的生活中也处处存在信号:红绿灯,下课铃声等等....

我们怎么认识这些信号呢?  从小有人教过我们。 我们不仅要识别信号,还要直到信号的处理方法,比如红灯停绿灯行。但信号产生了,我们可能并不立即处理这个信号,在合适的时候,因为我们可能正在做更重要的事情。 — 所以,信号产生后一直到信号处理时,中间一定有一个时间窗口。在这个时间窗口内,我们必须记住信号到来!

OS 中也会有很多信号围绕着信号去展开,所以进程要能够识别非常多的信号。这里只想说明进程能够认识信号,以及信号不管到没到来进程都知道该怎么做。

结论:

  1. 进程必须能识别并能够处理信号,信号没有产生,也要具备处理信号的能力。
  2. 进程收到一个具体信号的时候,进程可能并不会立即处理这个信号。
  3. 一个进程从信号产生到信号被处理,一定有时间窗口,进程会在合适的时候处理信号。

2、前台进程和后台进程

如下图 这就是前台进程,我们使用CTRL + C可以杀掉前台进程(该进程运行时,shell不会接收其他命令了)

我们在可执行程序后面加一个& 表示这个程序放在后台运行,这样 Shell 不必等待进程,结束就可以接受新的命令,启动新的进程,在他运行的时候还可以运行其他指令,并且Ctrl+c已经无法杀掉该进程。

我们要终止这个进程,就要用kill -9信号杀死这个进程。

Crtl+c为什么可以终止前台进程呢?

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

在Linux中,一次登录中,一个终端,一般会配上一个bash,只允许一个进程为前台进程,运行多个进程是后台进程。

那么既然一开始bash是前台进程,那么为什么使用CTRL+C时候,bash不退出呢?

这当然是因为bash在里面对这个信号做了特殊处理

Ctrl+c本质上被进程解释成收到信号,是2号信号,这个后续给大家验证。我们可以用kill -l来看到所有信号。

在我们系统中总共有62个信号(没有0、32、33信号)

1-31号信号被称为普通信号,34-64被称为实时信号。

不会被立即处理的是普通信号,要立即处理的是实时信号。

  • 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在 signal.h 中找到,例如其中有定义 #define SIGINT 2

由此我们可以知道,进程就是你,操作系统就是快递员,信号就是快递

3、信号的处理

信号有三种处理方式,我们在上面也提到过

1、默认动作    2、忽略   3、自定义动作(提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数)

我们在上面写的Ctrl+c实际是给该进程发2号信号。我们接下来写个代码测试一下。

我们先认识一个接口。

man signal

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

 这个函数的作用是设置对于signum信号的处理方法,处理方法为handler。handler是一个函数指针。

#include <iostream>
#include <unistd.h>
#include <signal.h>

using namespace std;

void handler(int sign)
{
    cout<<"process get a signo:"<<sign<<endl;

}
int main()
{
    signal(2,handler);
    while(1)
    {
        cout<<"i am a process:"<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

 运行结果如下:

这里我们Ctrl+c并不能终止该进程,因为Ctrl+c实际上是给进程发2号信号,默认执行方式是终止该进程,但我们用signal修改了该进程对信号的处理动作。

对于这个signal函数,只需要设置一次即可,往后都有效

只有收到了对应的信号,才会调用这个方法

注意:不是所有的信号都可以自定义的。有些信号不能自定义

4、硬件层面

我们会有几个问题思考:1、键盘数据是如何输入给内核的呢? 2、ctrl+c又是如何变成信号的呢?

首先键盘输出数据肯定操作系统先知道,那操作系统是怎么知道键盘上有数据的呢?

在冯诺依曼体系结构中,Linux下一切皆文件,键盘也有自己对应的文件,往键盘输入数据本质就是把输入的数据拷贝到缓冲区上。所以操作系统就知道了所以我们就可以用read,write通过文件的方式把数据读到进程当中。

操作系统怎么知道键盘有数据了?

其实在CPU上有很多的引脚,我们的CPU是直接插到主板上的。而键盘是可以间接的和CPU直接物理上连接到的。虽然CPU不从键盘读数据。但是键盘可以给CPU发送一个硬件中断。一旦键盘写完了数据就会给CPU发送硬件中断给CPU,通知CPU数据就绪,从而让操作系统去完成文件的拷贝。显示器和网卡与键盘的原理是一样的,我们用中断号来区分这些硬件, 假如键盘文件的中断号是10,把10存放到CPU寄存器中,告诉CPU这是键盘,让CPU去执行相应的硬件驱动程序。

在软件层面上,操作系统一启动,就会形成一张,中断向量表。里面放的是方法的地址。这些方法是直接访问外设的方法—主要是磁盘,显示器,键盘 然后最后这个读取键盘的方法,才是将键盘的数据放到缓冲区的方法

一句话总结一下这个过程:

当键盘输入数据,会通过硬件中断发送给CPU,CPU会利用这个中断号,让操作系统直接去通过中断向量表找到键盘的读取办法,通过这个办法让键盘上文件数据拷贝到操作系统的缓冲区

所以键盘这个外设是通过中断来工作的。这个就是硬件中断

而我们前面所说的信号,也是通过一堆数字来进行控制。这两者其实比较相似,但是没有关系。一个是软硬件结合的,一个是纯软件行为。

我们所用的信号,就是用软件的方式,对进程模拟的硬件中断

那ctrl+c又是如何变成信号的呢?

实际上把键盘文件数据拷贝到操作系统缓冲区的时候,操作系统会判断是数据还是控制,如果是控制,比如CTRL+ C会把这个转化为2号信号发送给进程。而不是放到缓冲区中。所以进程就收到了2号信号

5、信号与我们的代码是异步的

信号的产生的和我们自己的代码的运行是异步的

同步就是发生一件事后等这件事发生完了才继续做我们的事情

异步就是这件事发生后我们不管这个事情,继续做我们的事情

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

 二、信号的产生

1、产生的方式

  1. 键盘组合键
  2. kill命令
  3. 系统调用
  4. 异常软件条件

产生的信号都是由操作系统给进程发送,因为操作系统是进程的管理者。

2、键盘组合键

我们刚刚讲了Ctrl+c是发送2号信号,再给大家讲两个组合键产生的信号

使用CTRL + \即可捕捉3号信号

#include <iostream>
#include <unistd.h>
#include <signal.h>

using namespace std;

void handler(int sign)
{
    cout<<"process get a signo:"<<sign<<endl;

}
int main()
{
    signal(3,handler);
    while(1)
    {
        cout<<"i am a process:"<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

CTRL + Z是19号信号

#include <iostream>
#include <unistd.h>
#include <signal.h>

using namespace std;

void handler(int sign)
{
    cout<<"process get a signo:"<<sign<<endl;

}
int main()
{
    signal(19,handler);
    while(1)
    {
        cout<<"i am a process:"<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

 

如下所示,我们似乎会发现,我们上面似乎并没有将19号信号用自定义的方法进行处理

其实这是因为不是所有的信号,都是可以被signal捕捉的,比如19,9号信号

我们可以用下面的代码进行测试。

我们能看到9号信号是无法被捕捉的,这里就不给大家全部演示了。

3、kill命令

我们在上面的实验也证明了kill命令也是可以给进程发信号的。

4、系统调用

man kill

 int kill(pid_t pid, int sig);

它的两个参数分别是pid和信号的编号。与命令行中的kill是很相似的

如果成功返回0,失败返回-1

我们可以简单的利用这个系统调用接口实现一个kill命令

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.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]);
        return 1;
    }
    int signum=stoi(argv[1]);
    pid_t pid=stoi(argv[2]);
    int n = kill(pid,signum);
    if(n < 0)
    {
        perror("kill");
        return 2;
    }

    return 0;
}

运行结果如下: 

 man raise

int raise(int sig);

 

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.h>


using namespace std;

void myhandler(int signo)
{
    cout << "process get a signal: " << signo << endl;
    exit(1);
}
int main()
{
    signal(2, myhandler);
    int cnt = 5;
    while(true)
    {
        cout << "I am a process, pid: " << getpid() << endl;
        sleep(1);
        cnt--;
        if(cnt == 0) raise(2);
    }
    return 0;
}

运行结果如下:

这个raise相当于

kill(getpid(), 2);

man abort

void abort(void);

它的作用是引起一个正常的进程直接终止

它相当于给自己发送一个6号信号

我们先用下面代码进行测试

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.h>


using namespace std;

void myhandler(int signo)
{
    cout << "process get a signal: " << signo << endl;
    exit(1);
}
int main()
{
    signal(6,myhandler);
    while(true)
    {
        cout << "I am a process, pid: " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

 5、异常软件条件

(1)异常产生信号

我们先来看一段代码

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.h>


using namespace std;

int main()
{
    cout<<"div before"<<endl;
    sleep(3);
    int a=10;
    a/=0;
    cout<<"div after"<<endl;
    return 0;
}

运行结果如下:

我们看到这个Floating point exception实际上就是信号,我们kill -l查看到是8号信号 我们也可以查看7号手册加以验证

 我们用signal捕捉8号信号看运行是什么情况

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.h>


using namespace std;

void myhandler(int signo)
{
    cout << "process get a signal: " << signo << endl;
}
int main()
{
    signal(8,myhandler);
    cout<<"div before"<<endl;
    sleep(3);
    int a=10;
    a/=0;
    cout<<"div after"<<endl;
    return 0;
}

运行结果如下:

这个我们发现是除0错误导致的,我们在看看野指针造成什么情况。

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.h>


using namespace std;

void myhandler(int signo)
{
    cout << "process get a signal: " << signo << endl;
}
int main()
{
    cout<<"point error before"<<endl;
    int *p=nullptr;
    *p=10;
    cout<<"point error after"<<endl;
}

运行结果如下:

我们查看这是11号信号

 我们捕捉一下11号信号看看

void myhandler(int signo)
{
    cout << "process get a signal: " << signo << endl;
}
int main()
{
    signal(11,myhandler);
    cout<<"point error before"<<endl;
    int *p=nullptr;
    *p=10;
    cout<<"point error after"<<endl;
}

 运行结果显示也是一直打印

这里为什么除0和野指针问题会导致进程崩溃呢?

这里我们曾经讲过在CPU上有一个eip/pc寄存器来记录当前执行的是哪一行代码

这里我们再讲一个,其实在CPU内部还一个状态寄存器,在它的签名有一个溢出标志位,当我们的代码除0,可以理解成在除一个极限小的数字,所以溢出了,在CPU状态寄存器的溢出标志位设置为1,意味着CPU在执行当前进程出异常了,又因为操作系统是硬件的管理者,得知CPU在执行当前进程出异常,所以操作系统给当前进程发送信号,但这个信号被我们捕捉了,它本来默认是终止,但这个异常没有解决,操作系统就一直发信号,所以才会出现一直打印的情况。 这里这个行为并不影响其他进程,我们之前讲过在CPU里的寄存器是进程的上下文,修改CPU内部的状态只影响了自己。这也更好的验证了进程的独立性。

野指针的问题

如下图所示,在CPU里面有一个内存管理单元,因为直接查页表太慢了,所以有一个MMU硬件来进行查表。一旦异常,也就是地址转化失败了。虚拟到物理转化失败了。在CPU内还有一个寄存器,一旦转化失败了。它会把转化失败的虚拟地址放在这里,CPU异常操作系统就知道了,给进程发信号终止该进程。后面的原理是一样的。

这里是对CPU的硬件不同的报错,操作系统能检测并分辨出是除0错误还是野指针问题。

那么异常只会由硬件产生吗?

比如我们之前的管道,如果一开始读写端都打开,但是我们突然关闭了读端。那么写端进程就会被杀掉。会收到一个SIGPIPE(13)号信号。这就是一种软件异常。

也有的异常,操作系统只是会返回值出错的形式进行处理

(2)软条件——闹钟

man alarm

unsigned int alarm(unsigned int seconds);

alarm 系统调用用于设置一个定时器,当定时器计时器达到指定的时间时,内核会发送一个 SIGALRM 信号(14号信号)给调用进程。这可以用于实现定时器功能,例如在一定时间间隔内执行某个特定的操作或执行定时任务

seconds 参数表示定时器的秒数。如果 seconds 参数为非零值,表示设置定时器,在指定秒数后会发送 SIGALRM 信号给进程。如果 seconds 参数为零,则表示取消之前设置的定时器。

返回值是剩余的未完成的定时器秒数。如果之前有一个定时器已经设置,调用 alarm 会取消之前的定时器,并返回剩余的秒数。如果没有之前的定时器,或者之前的定时器已经到期,返回值为 0。

我们来看这段代码

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.h>


using namespace std;

int main()
{
    int n=alarm(5);
    while(1)
    {
        cout<<"proc is running"<<endl;
        sleep(1);
    }
    return 0;
}

运行结果如下:

 我们尝试捕捉一下14号信号

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.h>


using namespace std;

void myhandler(int signo)
{
    cout << "process get a signal: " << signo << endl;
}
int main()
{
    signal(14,myhandler);
    int n=alarm(5);
    while(1)
    {
        cout<<"proc is running"<<endl;
        sleep(1);
    }
    return 0;
}

运行结果如下

因为闹钟只会响一次,所以我们只捕捉了一次。

如果我们想让它每隔3秒响一次

我们再来看代码

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.h>


using namespace std;

void myhandler(int signo)
{
    cout << "process get a signal: " << signo << endl;
    int n=alarm(3);
}
int main()
{
    signal(14,myhandler);
    int n=alarm(3);
    while(1)
    {
        cout<<"proc is running"<<endl;
        sleep(1);
    }
    return 0;
}

关于它的返回值,我们做一下试验

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <signal.h>


using namespace std;

void myhandler(int signo)
{
    cout << "process get a signal: " << signo << endl;
    int n=alarm(3);
    cout<<"剩余时间:"<<n<<endl;
}
int main()
{
    signal(14,myhandler);
    int n=alarm(50);
    while(1)
    {
        cout<<"proc is running,pid:"<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

返回值返回的是上一个闹钟的剩余时间。

(3)core dump

先解释一下什么是 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

我们可以先用ulimit -a查看

我们用上面提到的命令使其产生core文件。大小为1024

设置 core file size,kill -8/11 后,发现报错信息中多了一个(core dumped),且 ll 还发现多了一个 core 文件

ulimit 命令改变了 Shell 进程的 Resource Limit,test 进程的 PCB 由 Shell 进程复制而来,所以也具有和 Shell 进程相同的 Resource Limit 值,这样就可以产生 Core Dump 了。 
前面讲进程等待的时候说过一个概念,父进程中 waitpid 可以获取子进程的退出信息,其中 status 中,低 7 位表示进程退出时的终止信号,次低 8 位表示进程退出时的退出码,而低 8 位中的最后 1 位还没有讲
它表示进程是否 core dump,core dump 是一个标志位。

当一个进程被异常退出时,退出码没有意义,我们不仅想知道它的退出信号,更想知道的是它在代码的哪一行触发的信号。因为云服务器默认看不到现象,如果是虚拟机的话就可以看到。所以为了让云服务器能够看到,我们就需要设置一下,ulimit -a 查看系统资源,其中 ulimit -c 1024 就设置好了 core file size。

在上面运行报错后,有一个(core dumped),它叫做核心转储。当一个进程崩溃时,OS 会将进程运行时的核心数据 dump 到磁盘上,方便用户进行调试,一旦发生核心转储,core dump 标志位就会被设置 1,否则就是 0。

一般而言,线上环境的核心转储是被关闭的。因为程序每崩溃一次就会 dump 一次,而这一个 core 文件有 56 万多个字节,还不说这个文件不大。如果线上环境的核心转储是打开的,那么在公司项目中有几千台机器,那肯定是自动运行的,此时如果存在大量错误,一运行就 dump,一 dump 就运行,那么过了一晚,服务器肯定都登不上了,原因就是磁盘已经被大量的 core 文件占用了。

三、信号保存

1、信号其他的相关概念

在操作系统给进程发送信号的时候,实际是给进程的PCB发送。

在task_struct中维护一个int signal,我们不把它当整数,我们可以理解成一个32位的数组,或者是位图,把他当成二进制来看,用0,1来描述信号,用位图来管理信号

所谓的发信号,本质上是操作系统去修改task_struct的信号位图对应的比特位。

那为什么是操作系统发送信号呢?

因为操作系统是进程的管理者,只有操作系统有资格去修改tast_struct内部的属性。

为什么要信号保存呢?

进程收到信号之后,可能不会立即处理这个信号,会有一个不被处理的时间窗口,所以我们要把信号保存起来。

  • 实际执行信号的处理动作(忽略、默认、自定义捕捉)称为信号递达(Delivery)。
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞(Block)某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

注意 :阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。我们可以把阻塞理解成已读不回,忽略理解成未读。

2、在内核中表示

实际在 Linux kernel 的 task_struct 中还包含了一些信号相关字段,如下面这个信号在内核中的示意图:这个图应该横着来看:

SIGHUP(1),没有收到 pending,也没有收到 block,所以默认处理是 SIG_DFL。

SIG_INT(2),收到 pending,因为也收到了 block,所以不会处理 SIG_IGN。

SIGQUIT(3),没有收到 pending,收到了 block,如果没有收到对应的信号,照样可以阻塞信号,所以阻塞更准备的理解是它是一种状态;

信号的自定义捕捉方法是用户提供的,是在用户权限下对应的方法。下面学习信号的操作都是围绕着这三个表来展开。

  1. pending(未决):它是一个无符号整型的位图,比特位的位置代表信号的编号,比特位的内容 0 1 代表是否收到信号,OS 发送信号本质是修改 task_struct ➡ pending 位图的内容。
  2. handler(递达):它是一个函数指针数组,它是用信号的编号,作为 handler 数组的索引,找到该信号编号对应的信号处理方式,然后执行对应的方法。
  3. block(阻塞):它是一个无符号整型的位图,比特位的位置代表信号的编号,比特位的内容 0 1 代表是否阻塞该信号。
  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP 信号未阻塞也未产生过,当它递达时执行默认处理动作。
  • SIGINT 信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  • SIGQUIT 信号未产生过,一旦产生 SIGQUIT 信号将被阻塞,它的处理动作是用户自定义函数 sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?。Linux是这样实现的:常规信号在递达之前产生多次只计一次,如果存在一个信号多次的情况,在递达的时候再把block对应的信号设置为1,也就是阻塞该信号,当递达结束后,再解除该信号的阻塞。而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号

3、sigset_t

可以理解为了能让我们更好的对上面的三张表操作,OS 给我们提供了一种系统级别 sigset_t  类型这个类型 OS 内部的当然也有定义,我们可以使用这个数据类型在用户空间和内核交互,此时就一定需要系统接口。

未决和阻塞标志可以用相同的数据类型 sigset_t 来存储,sigset_t 称为信号集

sigset_t 定义的变量 set 当然是在栈上开辟空间,那么这个栈就是用户栈,实际上我们在进程地址空间中谈的代码段、数据段、堆区、内存映射段、栈区、命令行参数、环境变量都是在用户空间,而将来要把用户空间中的进程信号属性设置到内核,所以除了 sigset_t,一定还需要系统接口。

4、信号集操作函数

当然光有 sigset_t 这个类型还不够,这个类型本身就是一个位图。实际我们不支持或者不建议直接操作 sigset_t,因为不同平台,甚至不同位数的 OS,sigset_t 位图的底层组织结构实现可能是不一样的,所以 OS 提供了一些专门针对 sigset_t 的系统接口,这些接口会先在用户层把信号相关的位图数据处理好。

这些函数是以位图为单位,将位图全部清理或者全部置1等。

​
#include <signal.h>
int sigemptyset(sigset_t* set);//全部置0
int sigfillset(sigset_t* set);//全部置1
int sigaddset(sigset_t* set, int signo);//指定位置置为1 信号集添加一个信号   
int sigdelset(sigset_t* set, int signo);//指定位置置为0 信号集指定信号删除
int sigismember(const sigset_t* set, int signo);//判断特定信号是否已经被设置

​

  • 函数 sigemptyset 初始化 set 所指向的信号集,使其中所有信号的对应 bit 清零,表示该信号集不包含任何有效信号。
  • 函数 sigfillset 初始化 set 所指向的信号集,使其中所有信号的对应 bit 置位,表示该信号集的有效信号包括系统支持的所有信号。

注意:在使用 sigset_ t 类型的变量之前,一定要调用 sigemptyset 或 sigfillset 做初始化,使信号集处于确定的状态。初始化 sigset_t 变量之后就可以在调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信号。

这四个函数都是成功返回  0, 出错返回  -1 。 sigismember  是一个布尔函数, 用于判断一个信号集的有效信号中是否包含某种信号, 若包含则返回  1, 不包含则返回  0, 出错返回  -1 。

(1)sigprocmask

调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)。传入一个 set 信号集,设置进程的 block 位图,一般把用户空间定义的信号集变量或对象设置成进程 block 位图,这样的信号集叫做信号屏蔽字(Signal Mask),阻塞信号集也叫做当前进程的信号屏蔽字,这里的屏蔽应该理解为阻塞而不是忽略。

set:输入型参数,由用户层把信号屏蔽字拷贝到内核。
oset:输出型参数,把老的信号屏蔽字返回,方便恢复,不想保存可设置 NULL。

如果 oset 是非空指针,则读取进程的当前信号屏蔽字通过 oset 参数传出。如果 set 是非空指针,则更改进程的信号屏蔽字,参数 how 指示如何更改。如果 oset 和 set 都是非空指针,则先将原来的信号屏蔽字备份到 oset 里,然后根据 set 和 how 参数更改信号屏蔽字。假设当前的信号屏蔽字为 mask,下表说明了 how 参数的可选值。

如果调用 sigprocmask 解除了对当前若干个未决信号的阻塞,则在 sigprocmask 返回前,至少将其中一个信号递达。 

返回值

成功返回0,失败返回-1

(2)sigpending

获取当前调用进程的 pending 信号集, 通过  set  参数传出。调用成功则返回  0, 出错则返回  -1 。

set为输出型参数 

我们写一下代码来熟悉一下这些接口

#include <iostream>
#include <signal.h>
#include <unistd.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<<endl;
}
int main()
{
    //定义信号集变量
    sigset_t bset,oset;
    sigset_t pending;
    //初始化
    sigemptyset(&bset);
    sigemptyset(&oset);
    sigemptyset(&pending);
    //添加想要的屏蔽的信号
    sigaddset(&bset,2);

    //将该屏蔽的信号设置到block中
    sigprocmask(SIG_BLOCK,&bset,&oset);
    //重复打印pending信号集
    while(1)
    {
        int n=sigpending(&pending);
        if(n<0) continue;
        PrintPending(pending);
        sleep(1);
    }

}

运行结果如下:我们屏蔽了2号信号,当我们按下Ctrl+c,发现pending位图接收到了2号信号,由0->1。 

我们想让这个信号过10秒解除,并能递达 

我们能看到我们发送2号信号的时候,pending位图上显示,接收到了2号信号,可此时2号信号被屏蔽了,一共循环了10次 解除了屏蔽,2信号递达了并执行默认动作。

当我们捕捉2号信号看看,使其执行自定义动作

#include <iostream>
#include <signal.h>
#include <unistd.h>

using namespace std;

void handler(int signo)
{
    cout<<"hello linux"<<endl;
}

void PrintPending(sigset_t &pending)
{
    for(int signo=31;signo>=1;signo--)
    {
        if(sigismember(&pending,signo))
        {
            cout<<"1";
        }
        else
        {
            cout<<"0";
        }
    }
    cout<<endl;
}
int main()
{
    signal(2,handler);
    //定义信号集变量
    sigset_t bset,oset;
    sigset_t pending;
    //初始化
    sigemptyset(&bset);
    sigemptyset(&oset);
    sigemptyset(&pending);
    //添加想要的屏蔽的信号
    for(int i=1;i<31;i++)
    {
        sigaddset(&bset,i);
    }

    //将该屏蔽的信号设置到block中
    sigprocmask(SIG_BLOCK,&bset,&oset);
    int count=0;
    //重复打印pending信号集
    while(1)
    {
        int n=sigpending(&pending);
        if(n<0) continue;
        PrintPending(pending);
        cout<<"proc id:"<<getpid()<<endl;
        if(count++==10)
        {
            //解除2号信号的屏蔽
            sigprocmask(SIG_SETMASK,&oset,nullptr);
            cout<<"2号信号解除"<<endl;
        }
        sleep(1);
    }

}

运行结果如下:

信号捕捉执行了自定义动作。

相关文章:

  • 文件包含漏洞第一关
  • llvm数据流分析
  • 【数据结构】2算法及分析
  • Android 粘包与丢包处理工具类:支持多种粘包策略的 Helper 实现
  • 灰度发布和方法灰度实践探索
  • 【一起学Rust | Tauri2.0框架】基于 Rust 与 Tauri 2.0 框架实现软件开机自启
  • 方案精读:IBM方法论-IT规划方法论
  • centos linux安装mysql8 重置密码 远程连接
  • ctf-web: Gopher伪协议利用 -- GHCTF Goph3rrr
  • python---pickle库
  • 关于sqlalchemy的ORM的使用
  • 物联网商业模式
  • Java算术运算符与算术表达式
  • 第一章:大模型的起源与发展
  • 二、重学C++—C语言核心
  • JavaWeb——Mybatis、JDBC、数据库连接池、lombok
  • 【Linux系统编程】操作文件和目录的函数
  • 03_NLP常用的文本数据分析处理方法
  • elasticsearch 8.17.3部署文档
  • 『VUE』vue 引入Font Awesome图标库(详细图文注释)
  • 一季度全国30强城市出炉:谁能更进一步?谁掉队了?
  • 上海模速空间“北斗七星”与“群星”生态布局,正在做些什么?
  • 工人日报关注跟着演出去旅游:票根经济新模式兴起,让过路客变过夜客
  • 新闻1+1丨多地政府食堂开放“舌尖上的服务”,反映出怎样的理念转变?
  • 五一车市消费观察:政策赋能、企业发力,汽车消费火热
  • 准80后遵义市自然资源局局长陈清松任怀仁市委副书记、代市长