信号(Linux)

前言

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:要发送信号目标进程的PID
		pid=0,表示给调用进程同一个组的所有进程发送信号
		pid为负数,表示给某个进程组的所有进程发送信号
	2. sig:表示要发送的信号编号,可以是宏

返回值:
	1. 成功,返回0
	2. 失败,返回-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. 成功,返回0
	2. 失败,返回非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)
    {
        // child
        while (true)
        {
            cout << "I am a child process, pid:" << getpid() << endl;
            sleep(1);
        }
        exit(0);
    }
    // father
    int 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); //向信号集合中添加指定的信号signum
    int sigdelset(sigset_t *set, int signum); //向信号集合中删除指定的信号signum
    int sigismember(const sigset_t *set, int signum); //判断signum信号是否在信号集合中

参数:
	1. set:被操作的sigset_t类型对象
	2. signum:向set对象中添加、删除几号信号,或者判断相应的信号是否在set指向的信号集中

返回值:
	1. 前四个函数成功返回0,出错返回-1
	2. 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|set
		SIG_UNBLOCK:从进程的信号屏蔽字中移除set中的信号,这些信号不再被阻塞。相当于mask=mask&~set
		SIG_SETMASK:将进程的信号屏蔽字设置为set中的值。相当于mask=set
	2. 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...00000000010
    for(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. 打印当前进程pending
    sigset_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_struct
    sigprocmask(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)
    {
        //child
        while(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)
        {
            // child
            while (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)
        {
            // child
            while (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这个信号属于特例。
特例

  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

kpl_20

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值