【Linux】进程信号

🌈前言

这篇文章给大家带来进程信号的学习!!!


🌸1、信号

🍡1.1、生活角度的信号

  • 在生活中:红绿灯、下课铃声、闹钟都是会发送信号的东西

我是进程,快递员是操作系统,快递是信号

这些东西我们是怎么认识的呢?
  • 因为我们“有人教”(能够认识不同场景下的信号以及所表达的含义)-- 识别信号
  • 我们在网上买了很多件商品,等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递,也就是你能“识别快递
  • 我们在过马路时,遇到红绿灯,我们知道红灯要等,绿灯走的含义 – “识别红绿灯

特定信号产生时,我们该做什么?
  • 我们早就知道,信号产生之后,要做什么,即便信号没有产生,我们就已经提前知道这个信号的处理方法处理信号的能力
  • 当快递员到家门口时,你也收到快递到来的信息,但是你正在做重要的事情,需要过几分钟才能取快递,这几分钟里你并没有去取快递,但是知道快递来了,也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取
  • 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取
  • 当你要拿快递时,一般方式有三种:1、执行默认动作(准时取快递)2、执行自定义动作(等几分钟再拿快递)3、忽略快递(无视快递,但知道已经来了)
  • 快递到来的整个过程,对你来说是异步的,你不能准确断定快递员什么时候给你打电话

🍢1.2、技术角度的信号

  • 进程收到信号就是快递员发信息叫拿快递,进程处理信号就是用什么方式拿快递

  • 对于进程来说,即便是信号没有产生,进程已经具有识别和处理信号的能力了

  • 进程可能不需要立即处理这个信号(异步性,不知道什么时收到OS发送的信号),但是不代表这个信号不会被处理,且必须记住这个信号已经来了

进程是如何记住这个信号的呢?
  • 在进程控制块(PCB)中保存着这个信号
  • 保存信号的变量是一个位图,它是内核的数据结构
谁能修改PCB的信号呢?
  • 只有OS能修改PCB中保存信号的位图数据结构,因为它是内核级的
  • OS是进程的管理者,进程所有的属性读取和设置,都只能通过OS来做
  • 位图中是用0,1标识信号有没有产生的,由比特位的位置来标识什么信号
struct task_struct
{
	// ....
	uint32_t sig; // 位图:0000 0000
};

🍧1.3、技术应用的信号(指令)

我们之前指令部分学的热键中ctrl + c就是将前台进程给终止掉

前台和后台进程
  • 前台进程:在bash进程跑的子进程就是前台进程,它会一直占用bash终端
  • 后台进程:在后台跑的进程,它不会占用bash终端,可以进行其他指令操作

ctrl + c:可以将前台进程给杀掉,并且会回到bash进程中

  • 用户按下Ctrl + C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程

  • 前台进程因为收到OS发送的信号,进而引起进程退出

[lyh@localhost lesson5(进程信号)]$ ls
makefile  signal  signal.cc
[lyh@localhost lesson5(进程信号)]$ cat signal.cc
#include <iostream>
#include <unistd.h>

int main()
{
    while (true)
    {
        std::cout << "我是一个进程, 我的pid: " << getpid() << std::endl;
        sleep(2);
    }
    return 0;
}

[lyh@localhost lesson5(进程信号)]$ ./signal 
我是一个进程, 我的pid: 2872
我是一个进程, 我的pid: 2872
我是一个进程, 我的pid: 2872
我是一个进程, 我的pid: 2872
^C
[lyh@localhost lesson5(进程信号)]$ 
后台进程相关指令
  • ctrl + z:暂停前台运行的进程,并把它放到后台,回到bash进程
  • Jobs:查看OS中的后台进程及编号
  • ./可执行程序 &:创建一个后台进程
  • fg N:将后台进程编号为N的进程放到前台执行
  • bg N:将后台进程编号为N的进程放到后台执行(后台进程可能是停止运行的)
[lyh@localhost lesson5(进程信号)]$ ls
makefile  signal  signal.cc
[lyh@localhost lesson5(进程信号)]$ cat signal.cc
#include <iostream>
#include <unistd.h>

int main()
{
    while (true)
    {
        std::cout << "我是一个进程, 我的pid: " << getpid() << std::endl;
        sleep(2);
    }
    return 0;
}

在这里插入图片描述

注意:
  • bash进程可以同时运行一个前台进程和多个后台进程
  • 前台进程在运行过程中用户随时可能按下 Ctrl + C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止进程,所以信号相对于进程的控制流程来说是异步(Asynchronous)的

🌸2、信号概念(kill -l)

查看信号列表
  • 使用 kill -l 指令查看系统定义的信号列表
  • 信号列表中1到32是普通信号,34到64是实时信号,系统一共定义了62个信号
  • 普通信号是异步通知的,而实时信号是立即执行的(实时OS)
  • 左边数字是信号编号(位图第几个位置),右边是宏定义的信号名
[lyh@localhost lesson5(进程信号)]$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX
[lyh@localhost lesson5(进程信号)]$
信号的常见处理方式
  • 忽略:忽略此信号
  • 默认:执行该信号的默认处理动作
  • 自定义信号:提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号

🌺3、产生信号

🍡3.1、通过系统调用发送信号

后台执行一个死循环程序,我们向该程序发送编号为9的SIGKILL信号,杀掉该进程

[lyh@localhost lesson5(进程信号)]$ ls
makefile  signal  signal.cc
[lyh@localhost lesson5(进程信号)]$ cat signal.cc
#include <iostream>
#include <unistd.h>

int main()
{
    while (true)
    {
        sleep(1);
    }
    return 0;
}

[lyh@localhost lesson5(进程信号)]$ ./signal &
[1] 8852
[lyh@localhost lesson5(进程信号)]$ kill -9 8852
[1]+  Killed                  ./signal
[lyh@localhost lesson5(进程信号)]$ jobs
[lyh@localhost lesson5(进程信号)]$ 
  • 8852是进程pid,指定发送某种信号的kill命令可以有多种写法,上面的命令还可以写成 kill -SIGKILL 8852 或 kill -9 8852,9是信号SIGKILL的编号

  • kill命令是调用kill函数实现的,kill函数可以给一个指定的进程发送指定的信号

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

pid参数:进程pid
sig参数:系统定义的信号编号
返回值:成功返回0,错误返回-1

模拟实现一个kill指令

[lyh@localhost lesson5(进程信号)]$ ls
Kill  Kill.cc  makefile  signal  signal.cc
[lyh@localhost lesson5(进程信号)]$ cat Kill.cc
#include <iostream>
#include <cstring>
#include <sys/types.h>
#include <signal.h>

int main(int argc, char* argv[])
{
    // Kill -sig pid
    if (argc < 3)
    {
        std::cout << "Kill -sig pid" << std::endl;
        return 1;
    }
    if (kill(atoi(argv[2]), argv[1][1]) == -1)
    {
        std::cout << strerror(errno) << std::endl;
        return 1;
    }
    return 0;
}

[lyh@localhost lesson5(进程信号)]$ ./signal &
[1] 11564
[lyh@localhost lesson5(进程信号)]$ ./Kill -9 11564
[1]+  Real-time signal 23     ./signal
[lyh@localhost lesson5(进程信号)]$ jobs
[lyh@localhost lesson5(进程信号)]$ 
  • raise函数可以给当前进程发送指定的信号(自己给自己发信号)
#include <sys/types.h>
#include <signal.h>
int raise(int signo);

signo参数:系统定义的信号编号
返回值:成功返回0,错误返回-1
#include <iostream>
#include <sys/types.h>
#include <signal.h>

int main()
{
    // raise: 给当前进程发送指定信号
    raise(2);
    return 0;
}
  • abort函数使当前进程接收到信号而异常终止、

  • abort函数使当前进程收到 SIGABRT(编号:6) 信号,进而异常终止程序

  • C++程序出现异常,如果没捕获,自动调用abort函数,可以自己测试一下除0错误

#include <stdlib.h>
void abort(void);
[lyh@192 other_function]$ ls
Abort  Abort.cc  Kill  Kill.cc  makefile  Raise  Raise.cc
[lyh@192 other_function]$ cat Abort.cc 
#include <iostream>
#include <unistd.h>
#include <cstdlib>

int main()
{
    // abort()函数对当前进程发送SIGABRT信号,使进程异常终止
    // C++程序出现异常,如果没捕获,自动调用abort
    int cnt = 0;
    while (true)
    {
        if (cnt == 5)
            abort();
        std::cout << "I am a process, my pid is " << getpid() << std::endl;
        ++cnt;
        sleep(1);
    }
    return 0;
}
[lyh@192 other_function]$ ./Abort 
I am a process, my pid is 7036
I am a process, my pid is 7036
I am a process, my pid is 7036
I am a process, my pid is 7036
I am a process, my pid is 7036
Aborted (core dumped)
[lyh@192 other_function]$ 

core dump:

我们之前学进程等待(waitpid的status参数)中,还有一个core dump标记位没有学

  • core dump:它能把进程在运行中,对应的异常上下文数据,core dump到磁盘上,方便调试

  • 如果进程出现异常,并且core dump被打开了,则status中的core dump标记位会被设置为1

在这里插入图片描述

  • core dump一般是关闭的,因为很占磁盘的内存,可以用ulimit -a查看是否打开

在这里插入图片描述
core dump的使用:

  • 使用core文件之前,需要打开core file,gcc/g++要带-g选项,生成可调试可执行文件

  • 运行可执行程序后,程序发生异常,会生成core.进程pid文件(用于gdb调试)

  • 启动gdb调试后,写入core-file core文件,就能显示异常出现在第几行异常信号的编号

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

int main()
{
	std::cout << "I am a process, my pid is " << getpid() << std::endl;
    int a = 1, b = a / 0;
    return 0;
}

在这里插入图片描述

使用waitpid的status查看core dump右0置1

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

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // Child Process -- 除零错误
        int a = 1, b = a / 0;
    }
    else if (id > 0)
    {
        // Parent Process
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if (ret > 0)
        {
            std::cout << "等待成功, 退出码: " << WEXITSTATUS(status) << ", 退出信号: " << WTERMSIG(status)
                << ", core dump: " << ((status >> 7) & 1) << std::endl;
        }
        else
        {
            std::cout << "waitpid error: " << strerror(errno) << std::endl;
        }
    }
    return 0;
}

在这里插入图片描述


🍢3.2、由软件条件产生信号(软中断)

软中断:
  • 信号是进程之间事件异步通知的一种方式,属于软中断
  • 软中断:利用硬件中断的概念,用软件方式进行模拟,实现宏观上的异步执行效果
  • 软中断无法屏蔽信号,而硬中断可以屏蔽信号(阻塞信号
  • 异步通知:进程在被CPU调度运行中时,随时可能产生信号
  • 软中断是由当前正在运行的进程所产生的,硬中断是在CPU上运行的,当异常产生时,会中断CPU的运行,然后对异常进行处理
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
函数解析
  • alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号(编号:12), 该信号的默认处理动作是终止当前进程
  • SIGALRM是:在定时器终止时发送给进程的信号
  • 返回值:0 或者 以前设定的闹钟时间还余下的秒数
  • 打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟

创建一个计数器,记录一秒内的IO次数,1秒到了后,被SIGALRM信号终止程序

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

int main()
{
    int cnt = 0;
    // 设置一个闹钟,该闹钟会在1秒后发送信号给当前进程
    alarm(1);
    // 记录一秒内cnt的IO次数
    while (true)
    {
        ++cnt;
        printf("cnt: %d\n", cnt);
    }
    return 0;
}

🍧3.3、硬件异常产生信号(硬中断)

信号捕捉(signal函数):

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);
函数解析
  • signum:需要捕捉的信号编号
  • handler:设置一个回调函数,如果捕捉到信号,则会调用这个函数单独处理信号
  • 返回值:返回信号处理程序的上一个值,或出错时返回SIG_ERR,并且设置errno
  • 该函数是用来自定义处理信号的,如果信号被捕捉到,则调用handler回调函数进行处理

捕获2号SIGHUP信号,并且定义一个回调函数单独处理

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

void Sighandler(int signum)
{
    std::cout << "捕获成功,编号: " << signum << ",已经对其进行处理" << std::endl;
}

int main()
{
    // 这里不是调用Sighandler方法,这里只是设置了一个回调,让SIGINT(2)产生的时候,该方法才会被调用
    // 如果不产生SIGINT(2),该方法不会被调用!
    signal(2, Sighandler);
    std::cout << "设置自定义信号成功!!!" << std::endl;
    while (true)
    {
        // std::cout << "我是一个进程, 我的pid: " << getpid() << std::endl;
        std::cout << "I am a process, my pid is " << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述

如果把全部信号屏蔽(全部自定义,但是不退出),那么是不是这个进程就刀枪不入了!!!

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

void Sighandler(int signum)
{
    std::cout << "捕获成功,编号: " << signum << ",已经对其进行处理" << std::endl;
}

int main()
{
    //  将前31个信号全部屏蔽
    for (int i = 1; i < 32; ++i)
    {
        signal(i, Sighandler);
    }
    std::cout << "设置自定义信号成功!!!" << std::endl;
    while (true)
    {
        // std::cout << "我是一个进程, 我的pid: " << getpid() << std::endl;
        std::cout << "I am a process, my pid is " << getpid() << std::endl;
        sleep(5);
    }
    return 0;
}

在这里插入图片描述

  • 通过测试发现,虽然把全部信号屏蔽了,但是 9和19号 信号完全不受影响

  • 9号和19号信号是管理信号,它虽然被设置了,但是还是会执行原来的操作


硬件异常产生信号概念:
  • 硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核解析该异常,最后向指定的进程发送解析后的信号
  • 例如:当前进程执行了除以0的指令CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程
  • 再比如:当前进程访问了非法内存地址MMU(硬件:内存管理单元)会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程

在C/C++中如何理解程序异常后就崩溃了呢?******

  • 程序崩溃本质是进程崩溃了,进程崩溃的本质是:该进程收到了异常信号!!!

  • 硬件发生异常,进而导致OS向目标进程发送异常信号,最后导致进程终止的现象

如何理解硬件发生异常?********

  • 除零错误:CPU内部有状态寄存器(硬件)运算发生错误,状态寄存器的状态信号会被设置(浮点数越界),OS管理软硬件,识别到CPU内部有报错,向目标进程发送信号,进程在合适(异步性)的时候,会处理该异常信号,最后终止程序(默认行为)

  • 越界&&野指针:语言层面使用的地址是虚拟地址,需要映射到物理地址,映射过程中,需读取代码和数据,映射工作是由(MMU(硬件)+页表(软件))完成,映射过程中出现问题,表现在MMU上,OS发现硬件出现异常,会构建信号,向目标进程发送信号,进程在合适(异步性)的时候,会处理该异常信号,最后终止程序(默认行为)

除零错误

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

void Sighandler(int signum)
{
    std::cout << "捕获成功,编号: " << signum << ",已经对其进行处理" << std::endl;
    abort();
}

int main()
{
    // 自定义处理除零错误
    signal(SIGFPE, Sighandler);
    std::cout << "设置自定义信号成功!!!" << std::endl;
    int a = 1, b = a / 0;
    return 0;
}

越界错误

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

void Sighandler(int signum)
{
    std::cout << "捕获成功,编号: " << signum << ",已经对其进行处理" << std::endl;
    abort();
}

int main()
{
    // 自定义处理越界和野指针错误
    signal(SIGPIPE, Sighandler);
    std::cout << "设置自定义信号成功!!!" << std::endl;
    int *p = nullptr;
    *p = 1;
    return 0;
}

总结:

  • 所有信号的发送,最终只能由OS来执行,因为OS是管理者

  • 进程收到信号时,不会立即被处理,进程可能有比处理信号更重要的事情要做

  • 信号如果不被立即处理,那么信号会暂时被进程的某个位图结构置为1(后面讲)

  • 当一个进程没有收到信号时,已经拥有捕获和处理信号的能力,这是在设计OS时完成的


🍁4、阻塞信号

🍡4.1、信号递达与未决概念

  • 实际执行信号的处理动作,称为信号递达(Delivery)

  • 信号从产生到递达之间的状态,称为信号未决(Pending)

  • 进程可以选择阻塞 (Block )某个信号

  • 被阻塞的信号产生时,将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作

  • 注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作


🍡4.2、在内核中信号阻塞、未决与递达的原理

在这里插入图片描述

原理
  • 每个信号都有两个标志位分别表示信号阻塞(block)未决(pending),还有一个函数指针数组(handler表)表示处理特定信号的动作
  • 信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志
  • 处理信号的过程,应该先看信号未决,然后再看信号阻塞,最后再看信号递达
  • 我们后面学习的信号系统调用都是围绕这三张表展开的!!!
第一个例子:
  • 在上图的例子中,SIGHUP信号未阻塞(block:0)未产生信号(pending:0),当它递达(handler)时执行默认处理动作
第二个例子:
  • SIGINT信号产生了,但正在被阻塞,所以暂时不能递达
  • 虽然它的处理动作是忽略(SIG_IGN),但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞
第三个例子:
  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler(使用signal函数设置)
  • 如果在进程解除对某信号的阻塞之前,这种信号产生过多次,将如何处理? POSIX.1允许系统递送该信号一次或多次
  • Linux是这样实现的,常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里

🍧4.3、sigset_t(信号集)

在这里插入图片描述

信号集
  • 从上图来看,每个信号只有一个bit的未决标志非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的(常规信号,实时信号除外)
  • 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t 称为:信号集
  • 这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态
  • 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为信号阻塞而不是信号递达的忽略

Linux中信号集的类型

typedef __sigset_t sigset_t;
#define _SIGSET_NWORDS	(1024 / (8 * sizeof (unsigned long int)))

typedef struct
{
    unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;

🍁5、信号集操作函数

🍡5.1、初始化、添加、删除、判断是否存在

  • sitgset_t类型内部如何存储这些bit则依赖于系统实现

  • 从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量

  • 而不应该对它的内部数据(位图)做任何解释,比如用printf直接打印sigset_t变量是没有意义的

#include <signal.h>

初始化信号集:

// 初始化set所指向的信号集,将全部标记位置0
int sigemptyset(sigset_t *set);
// 初始化set所指向的信号集,将全部标记位置1,并且屏蔽所有信号(block表也置1)
int sigfillset(sigset_t *set);

添加或删除信号集:

// 向set所指向的信号集(创建/删除)一个signo(传信号编号或信号名)有效信号
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);

判断信号是否存在信号集:

// 判断signo信号是否在set所指向的信号集中
int sigismember (const sigset_t *set, int signo);
函数解析
  • sigemptyset函数初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号
  • sigfillset()用来将参数set信号集初始化,然后把所有的信号加入到此信号集里即将所有的信号标志位置为1屏蔽所有的信号(block表中全部标记位置1)
  • 注意:在使用sigset_ t类型的变量之前,一定要调用 sigemptyset 或 sigfillset 做初始化,使信号集处于确定的状态
  • 初始化sigset_t变量之后,就可以在调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信,sigo可以是信号编号或信号名
  • 这四个函数都是成功 返回0,出错 返回-1
  • sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1不包含则返回0,出错返回-1

函数的使用

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

void SigsetPrint(sigset_t* set)
{
    // 逐个判断普通信号是否存在
    for (int i = 1; i < 32; ++i)
    {
        if (sigismember(set, i))
            std::cout << 1;
        else
            std::cout << 0;
    }
    std::cout << std::endl;
}

int main()
{
    // 定义一个信号集
    sigset_t set;
    // 1. 初始化信号集 -- 全部bit置为0
    sigemptyset(&set);
    // 2. 添加2和3号信号到set指向的信号集中
    sigaddset(&set, 2);
    sigaddset(&set, 3);
    SigsetPrint(&set);
    // 3. 删除set指向的信号集中的2号信号
    sigdelset(&set, 2);
    SigsetPrint(&set);
    return 0;
}

[lyh@192 Sigset_t(3)]$ ./Sigset_t 
0110000000000000000000000000000
0010000000000000000000000000000

🍢5.2、sigprocmask(控制信号屏蔽字)

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
函数解析:
  • 作用:调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
  • set参数是需要修改的信号集(新信号集 – 输入型参数),oset是保存更改之前的信号集(旧信号集 – 输出型参数,由OS填充)
  • 返回值:若成功则为0,若出错则为-1
set 和 oset的三种情况
  • 如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出 - 获取信号屏蔽字
  • 如果set是非空指针,则更改进程的信号屏蔽字参数how指示如何更改
  • 如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到 oset 里,然后根据 set 和 how 参数更改信号屏蔽字

下图为how参数的可选值:假设当前的信号屏蔽字为mask,set为信号集

在这里插入图片描述

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

void SigsetPrint(sigset_t *set)
{
    // 逐个判断信号集是否被置位 -- 是,则打印1,反之0
    for (int i = 1; i < 32; ++i)
    {
        if (sigismember(set, i))
            std::cout << 1;
        else
            std::cout << 0;
    }
    std::cout << std::endl;
}

int main()
{
    // 定义二个信号集
    sigset_t set, oset;

    // 1. 初始化信号集 -- 全部bit置为0
    sigemptyset(&set);
    sigemptyset(&oset);

    // 2. 添加2号信号到set指向的信号集中
    sigaddset(&set, 2);
    std::cout << "信号集: ";
    SigsetPrint(&set);

    // 3、设置2号信号屏蔽字 -- set和oset非空,set屏蔽字会被拷贝到oset
    sigprocmask(SIG_SETMASK, &set, &oset);

    // 打印信号屏蔽字 -- 读取当前信号屏蔽字填充到oset中
    sigprocmask(0, nullptr, &oset);
    std::cout << "信号屏蔽字: ";
    SigsetPrint(&oset);

	sleep(5);

    // 4. 取消信号屏蔽字
    sigprocmask(SIG_UNBLOCK, &set, nullptr);
    sigprocmask(0, nullptr, &oset);
    std::cout << "取消信号屏蔽字: ";
    SigsetPrint(&oset);
    return 0;
}

[lyh@192 Sigset_t(3)]$ ./Sigset_t 
信号集: 0100000000000000000000000000000
信号屏蔽字: 0100000000000000000000000000000
取消信号屏蔽字: 0000000000000000000000000000000

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

[lyh@192 Sigset_t(3)]$ ./Sigset_t 
信号集: 0100000000000000000000000000000
信号屏蔽字: 0100000000000000000000000000000
^C^C^C^C^C^C^C^C^C
[lyh@192 Sigset_t(3)]$ 

🍧5.3、sigpending(获取未决信号表)

#include <signal.h>
int sigpending(sigset_t *set)
函数解析
  • 作用:返回在送往进程的时候被阻塞挂起的信号集合(pending(信号未决)表),这个信号集合通过参数set返回
  • 返回值:若成功则为0,若出错则为-1
#include <iostream>
#include <unistd.h>
#include <signal.h>

void SigsetPrint(sigset_t *set)
{
    // 逐个判断信号集是否被置位 -- 是,则打印1,反之0
    for (int i = 1; i < 32; ++i)
    {
        if (sigismember(set, i))
            std::cout << 1;
        else
            std::cout << 0;
    }
    std::cout << std::endl;
}

int main()
{
    // 定义二个信号集
    sigset_t set, oset, Pending;

    // 1. 初始化信号集 -- 全部bit置为0
    sigemptyset(&set);
    sigemptyset(&oset);
    sigemptyset(&Pending);

    // 2. 添加2号信号到set指向的信号集中
    sigaddset(&set, 2);
    std::cout << "信号集: ";
    SigsetPrint(&set);

    // 3、设置2号信号屏蔽字 -- set和oset非空,set屏蔽字会被拷贝到oset
    sigprocmask(SIG_SETMASK, &set, &oset);

    // 打印信号屏蔽字 -- 读取当前信号屏蔽字填充到oset中
    sigprocmask(0, nullptr, &oset);
    std::cout << "信号屏蔽字: ";
    SigsetPrint(&oset);

    // 打印pending表
    sigpending(&Pending);
    std::cout << "信号未决表: ";
    SigsetPrint(&oset);
    return 0;
}

[lyh@192 Sigset_t(3)]$ ./Sigset_t 
信号集: 0100000000000000000000000000000
信号屏蔽字: 0100000000000000000000000000000
信号未决表: 0100000000000000000000000000000
[lyh@192 Sigset_t(3)]$ 

🍁6、信号捕捉

🍡6.1、用户态与内核态

当信号产生时,进程不是立即处理它,会在合适的时候去处理,合适是什么时候呢?

  • 当进程从内核态切换用户态的时候,进行信号的检测与处理!!!

内核态与用户态是什么?有什么用?它们的区别是什么?

  • 内核态:进程地址空间可以访问到全部的代码和数据,包括内核空间的代码和数据

  • 用户态:进程地址空间只能访问用户空间(0-3G)的代码和数据,不能访问内核空间


注意:内核代码也会被加载到内存当中,最后被映射到物理内存当中

在这里插入图片描述

内核级页表与用户级页表
  • 内核级页表:它是所有进程共享的一份代码,前提是进程有权限访问
  • 用户及页表:所有进程都有自己的一份用户级页表,因为它们的代码和数据都不一样,映射到物理内存时,位置也会有所不同
  • 无论进程怎么进行用户态和内核态的切换,只要进程有足够的权限,就能访问到地址空间的内核代码和数据

进程怎么知道自己是用户还是内核态呢?
  • CPU里面有对应的状态寄存器CR3,有比特位标识当前进程的状态0是内核态3是用户态,CR3一共有四种状态【0,3】,数值越小,权限越大

什么时候进程切换内核态呢?
  • 调用系统调用
  • 进程时间片到了,进行进程间切换
  • 凡是要执行内核的代码时,进程就会切换到内核态

总结
  • 内核态:可以访问进程地址空间的所有代码和数据,具有最高权限
  • 用户态:只能访问自己所写的代码
  • 进程在合适的时候处理信号 – 从内核态切换到用户态的时候 – 检测pending表和block表,最后找到handler表中对应的函数进行信号处理

🍢6.2、信号捕捉的原理

内核如何实现捕捉的呢?

  • 如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号

  • 比如:用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时程序发生中断或异常,用户态切换到内核态

  • 中断处理完毕后,要返回用户态的main函数之前,检查到有信号SIGQUIT递达(信号处理)

  • 内核决定返回用户态后,不是恢复main函数的上下文继续执行,而是执行sighandler函数sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程

  • sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了

在这里插入图片描述

注意:

  • 在进行内核和用户态的切换时,CR3状态寄存器标记进程状态的bit会从0变成3

  • 在进行用户态和内核的切换时,CR3状态寄存器标记进程状态的bit会从3变成0

自定义处理信号快速记忆方法

在这里插入图片描述


🍧6.3、sigaction

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);

struct sigaction
{
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
};
函数解析
  • 作用:sigaction函数可以读取和修改与指定信号相关联的处理动作
  • signo:指定信号的编号,可以指定SIGKILL和SIGSTOP以外的所有信号,这二个信号管理信号,修改了也是不变的
  • 返回值:调用成功则返回0,出错则返回 -1

act 和 oact二种情况
  • 若act指针非空,则根据act修改该信号的处理动作
  • 若oact指针非空,则通过oact传出该信号原来的处理动作

struct sigaction结构体解析
  • sa_handler:该成员包含一个信号捕捉函数的地址,它与signal()的参数handler相同,赋值为SIG_IGN表示忽略信号SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号
  • sa_sigaction:它是实时信号的处理函数,实时信号编号为34 - 64
  • sa_mask:该成员说明需要额外屏蔽的信号(指:信号集sigset_t),当信号处理函数返回时,自动恢复原来的信号屏蔽字,在处理该信号时暂时将sa_mask 指定的信号集搁置
  • sa_flags:该成员包含一些选项,一般设置为0,用来设置信号处理的其他相关操作
  • sa_restorer:该成员是一个替代的信号处理程序,当设置SA_SIGINFO时才会用他
sa_flags选项作用
SA_INTERRUPT如果信号中断了进程的某个系统调用,则系统不会自动启动该系统调用
SA_RESTART如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_RESETHAND当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_SIGINFO提供附加信息,一个指向siginfo结构的指针以及一个指向进程上下文标识符的指针
SA_NODEFER一般情况下,当信号处理函数运行时,内核将阻塞(sigaction函数注册时的信号)。但是如果设置了SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号

自定义设置一个2号信号的捕捉,使用sigaction函数,而不是signal函数

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
using namespace std;

// |-----------------------------------------------------------|
// |int sigaction(int signum, const struct sigaction *act,     |
// |                                struct sigaction *oldact); |
// |-----------------------------------------------------------|

// 打印信号集中的block表或pending表的1 - 31个bit --没有0号信号
void Print_Sigset(sigset_t* sig)
{
    for (int i = 1; i <= 31; ++i)
    {
        if (sigismember(sig, i))
        {
            printf("1");
        }
        else
        {
            printf("0");
        }
    }
    printf("\n");
}

// 信号捕捉 -- 自定义处理信号
void SigHandler(int signo)
{
    printf("信号编号: signo, 已经处理完成! ! !\n");
    exit(0);
}

int main()
{
    // 0、定义信号集,并且初始化全部bit为0
    sigset_t set, oset, pending;
    sigemptyset(&set);
    sigemptyset(&oset);
    sigemptyset(&pending);

    // --------------------------------------------------

    // 1、添加1 - 4号信号到信号集中
    for (int i = 1; i <= 4; ++i)
    {
        sigaddset(&set, i);
    }
    Print_Sigset(&set);

    // --------------------------------------------------

    // 2. 删除信号集中的1,3,4号信号(由1置0)
    sigdelset(&set, 1);
    sigdelset(&set, 3);
    sigdelset(&set, 4);
    Print_Sigset(&set);

    // --------------------------------------------------

    // 3、添加2号信号(ctrl c)的捕捉
    // 3.1、定义sigaction结构体
    struct sigaction act, oact;
    // 3.2、设置2号信号指定的信号处理函数
    act.sa_handler = SigHandler;
    act.sa_flags = 0;
    sigaction(2, &act, &oact);

    while (true)
    {
        printf("主进程正在运行, 进程pid: %d\n", getpid());
        sleep(1);
    }
    return 0;
}

运行结果:
[lyh@192 SIgaction(4)]$ make
g++ -o sigaction Sigaction.cpp -std=c++11
[lyh@192 SIgaction(4)]$ ./sigaction 
1111000000000000000000000000000
0100000000000000000000000000000
主进程正在运行, 进程pid: 11565
主进程正在运行, 进程pid: 11565
^C信号编号: signo, 已经处理完成! ! !

🍁7、可重入函数(多线程预热)

可重入函数是多线程的内容,这里先预热一下:

在这里插入图片描述

上图解析:

  • main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理

  • 于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态

  • 再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步

  • 结果是,main函数和sighandler先后向链表中插入两个节点,而只有一个节点真正插入链表中了,最后导致了该程序的内存泄漏,指向node2的头节点永远找不到了


可重入函数概念:

  • 重入:insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数

  • 不可重入函数:insert函数访问一个全局链表,有可能因为重入而造成错乱

  • 可重入函数:如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数

如果一个函数符合以下条件之一则是不可重入的:

  • 调用了C语言的malloc或free,因为malloc也是用全局链表管理堆的

  • 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构


🍂8、volatile

volatile是C/C++中的关键字,是个很冷门的关键字

makefile

Volatile:Volatile.c
	gcc -o $@ $^ -std=c99 -O3 #O3选项是开启gcc编译器最高优化,默认为O

.PHONY:clean
clean:
	rm -rf Volatile
Volatile.c

#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

bool flag = 0;

void handler(int signo)
{
    flag = 1;
    printf("信号捕获成功, 已经对其进行处理, flag: %d\n", flag);
}

int main()
{
    signal(2, handler);
    int cnt = 5;
    while (!flag)
        ;
    printf("flag: %d\n", flag);
    return 0;
}
代码解析:
  • 标准情况下(没有加-O2选项),键盘按下ctrl c时 ,2号信号被捕捉,执行自定义动作,修改 flag=1,while 条件不满足,退出循环,进程退出
  • 优化情况下,键盘按下ctrl c时 ,2号信号被捕捉,执行自定义动作,修改 flag=1,但是很明显flag肯定已经被修改了,但是为何循环依旧执行呢?
  • 很明显, while 循环检查的flag,并不是内存中最新的flag,这就存在了数据二异性的问题,flag其实已经因为优化,被放在了CPU寄存器当中
解决方法:
  • 这时候就要volatile关键字来修饰该变量了,就能让CPU到内存中读取变量
  • volatile作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作
Volatile.c

#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

volatile bool flag = 0;

void handler(int signo)
{
    flag = 1;
    printf("信号捕获成功, 已经对其进行处理, flag: %d\n", flag);
}

int main()
{
    signal(2, handler);
    int cnt = 5;
    while (!flag)
        ;
    printf("flag: %d\n", flag);
    return 0;
}

🍃9、SIGCHLD信号

概念:

  • SIGCHLD信号:子进程退出时,自动给父进程发送该信号,默认处理动作为忽略

  • 前面进程控制章节已经讲了进程等待的处理方法,使用 wait 和waitpid 函数清理僵尸进程

  • 父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理,也就是轮询检测


信号角度处理子进程退出:

  • 其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数

  • 这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用waitpid清理子进程即可

没有等待,也没设置信号捕捉,默认处理信号的方法是”忽略“

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
using namespace std;

// SIGCHLD信号:子进程退出时,会给父进程发送该信号,默认处理动作是"忽略"

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // Chile Process
        int cnt = 3;
        while (cnt)
        {
            cout << "我是子进程, 我还有" << cnt << "秒后退出!!!" << endl;
            --cnt;
            sleep(1);
        }
        cout << "子进程执行完毕,已退出!!!" << endl;
        exit(0);
    }
    else if (id > 0)
    {
        // Parent Process
        while (true)
        {
            cout << "我是父进程, 我的pid: " << getpid() << endl;
            sleep(1);
        }
    }
    return 0;
}

在这里插入图片描述


使用自定义信号处理函数,处理函数里面使用非阻塞等待方式等待子进程

处理信号中使用非阻塞的原因:

  • 父进程可能创建2个以上的子进程,如果用阻塞的方式等待,会有个别子进程会一直处于Z状态

  • 因为在处理第一个SIGCHLD信号时,会将该信号阻塞,如果这时其他子进程也退出了,一直发送SIGCHLD信号,就会被屏蔽掉,一直处于僵尸状态

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <wait.h>
#include <signal.h>
using namespace std;

// SIGCHLD信号:子进程退出时,会给父进程发送该信号,默认处理动作是"忽略"
void handler(int signo)
{
    // 使用非阻塞等待方式,因为父进程可能创建了一堆子进程
    while (true)
    {
    	// -1: 等待所有子进程
        pid_t id = waitpid(-1, nullptr, WNOHANG);
        if (id > 0)
        {
            cout << "等待子进程成功, 子进程pid: " << id << endl;
            break;
        }
        else if (id == 0)
        {
            // 还有子进程,但是现在没有退出
            cout << "还有子进程,但是现在没有退出, 父进程要去忙自己的事情了" << endl;
            break;
        }
        else
        {
            cout << "子进程全部等待完成, 立即退出!!!" << endl;
            break;
        }
    }
}

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // Chile Process
        int cnt = 3;
        while (cnt)
        {
            cout << "我是子进程, 我还有" << cnt << "秒后退出!!!" << endl;
            --cnt;
            sleep(1);
        }
        cout << "子进程执行完毕,已退出!!!" << endl;
        exit(0);
    }
    else if (id > 0)
    {
        // Parent Process
        signal(SIGCHLD, handler);
        while (true)
        {
            cout << "我是父进程,我正在运行: " << getpid() << endl;
            sleep(1);
        }
    }
    return 0;
}

使用signal函数设置"忽略"信号,只有在Linux下可行,其他OS不行

  • 由于UNIX的历史原因,要想不产生僵尸进程还有另外一种办法,父进程调用sigaction或signal将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉

  • 也不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction或signal函数自定义的忽略,通常是没有区别的,但这是一个特例

  • 此方法对于Linux可用,但不保证在其它UNIX系统上都可用

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <wait.h>
#include <signal.h>
using namespace std;

// SIGCHLD信号:子进程退出时,会给父进程发送该信号,默认处理动作是"忽略"

int main()
{
    // SIG_IGN手动设置,让子进程退出,不要给父进程发送信号了,并且自动释放
    signal(SIGCHLD, SIG_IGN);
    pid_t id = fork();
    if (id == 0)
    {
        // Chile Process
        int cnt = 3;
        while (cnt)
        {
            cout << "我是子进程, 我还有" << cnt << "秒后退出!!!" << endl;
            --cnt;
            sleep(1);
        }
        cout << "子进程执行完毕,已退出!!!" << endl;
        exit(0);
    }
    else if (id > 0)
    {
        // Parent Process
        while (true)
        {
            cout << "我是父进程, 我的pid: " << getpid() << endl;
            sleep(1);
        }
    }
    return 0;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值