【Linux&操作系统】11. 信号

71 阅读25分钟

11 信号

11.1 什么是信号

11.1.1 信号的概念
  • OS作为一个承载着非常多软件运行的平台,软件会占用计算机资源,这就意味着OS不能过多地分配资源给软件,一旦软件干了一些出格的事情,OS有权下场解决问题,就像是红绿灯,用于规范车辆占用的空间资源和工作方向,而OS也有类似于红绿灯的东西,一旦OS使用这个东西下场,那么进程必须停下手头的活转而执行OS使用的这个东西,这个东西就是信号,换句话说,就是赛场上的红黄牌,该干掉你就毫不留情

  • 其实OS中的信号也没有什么神秘的,我们之前就已经接触过类似于信号的东西,还记得我们利用命名管道和shm完成的fifo吗?其中命名管道就承担了传输信号的责任,服务端一旦接收到客户端"开始传输"的信号,就会等待客户端传输完毕,一旦服务端接收到了客户端"传输完毕"的信号,服务端就被允许开始处理信息,这与OS中的信号没啥区别,只不过OS中的信号的发出者是shell,接收者是进程罢了,但这里还是要澄清一下,我这里的意思是这俩实现的行为逻辑是类似的,但用这俩的目的却是不一样的

  • 这里我们就要扯一下两个机制:"同步通信机制"和"异步通信机制"

    1. 同步通信机制: 简单来说就是阻塞等待式的通信,保证信号接收端一接收到信号就开始工作,没信号的时候不工作,发送端必须等待接收端完成任务才能开始自己的工作(换句话说,可以理解成变种的函数?函数不也是等待当前函数调用完之后,才回到上一层再执行逻辑吗)
    2. 异步通信机制: 简单来说就是非阻塞等待式的通信,换句话说就是信号发出端发出信号前,接收端可能依然在工作,接收端可能会在工作中途接收到信号,然后执行信号的任务,同时信号发送端发送完信号后不需要等待接收端完成任务,而是可以自己干自己的事情
  • 这也就意味着,如果我们将fifo改成轮询+非阻塞的逻辑,就可以实现类似于异步通信的效果,不过这依然不是最完善的异步通信,这其实还只能算是模拟

  • 那么异步通信最牛逼的例子就是OS的信号机制

  • 当然此时咱一定还有个疑问没解决,进程怎么知道shell发出了信号?进程接收到信号之后执行的代码逻辑我们根本就没看到过啊?!

  • 答案其实也是显而易见的,记得我们谈论ELF文件的小节吗,其中提到过,程序的起始函数起始并不是main(),而是一个预设好的,叫做_start()的函数,这意味着我们写好的代码在编译成可执行文件的过程中,编译器肯定是做过手脚的,既然编译器可以做手脚,难道就不能私自添加关于信号的执行逻辑吗,或者说难道就没有别的东西可能会给程序做手脚吗,这也是咱为什么看不到关于信号逻辑的原因(事实是编译器本身并没有注入相关代码,而是在程序启动的时候由glibc和内核协作自动设置的,当然事实上我们也有办法手动设置信号逻辑),因为这个东西就不是给用户(开发者)看的,而是给用户(开发者)用的!

  • 当然,既然要给用户使用,势必会有方案允许用户自己定义信号的执行逻辑,即signal()

  • 我们可以来玩一下signal()

  • 先看看signal()怎么定义的

#include <signal.h>
       sighandler_t signal(int signum, sighandler_t handler);
  • 使用方法是,传入你要修改执行逻辑的signum,比方说如果要修改ctrl + c代表信号SIGINT的执行逻辑,这里可以填2,handler是一个函数指针,表示你要修改成的逻辑,意味着如果该进程如果接受到了SIGINT信号,就会执行handler里的逻辑而不是被杀死(signal()本身并不会执行handler的内容,仅仅只是设置默认调用逻辑,如果没有使用过signal(),那么进程将会使用默认的逻辑)
#include <iostream>
#include <signal.h>

void func(int signumber)
{
        std::cout << "接收到信号: " << signumber << std::endl;
}


int main()
{
        signal(2, func);

        while(true)
        {
                std::cout << "正在等待" << std::endl;
                sleep(1);
        }

        return 0;
}
oldking@iZwz9b2bj2gor4d8h3rlx0Z:~/code/code_25_6_8_signal_01$ ./test
正在等待
正在等待
正在等待
^C接收到信号: 2
正在等待
^C接收到信号: 2
正在等待
^C接收到信号: 2
正在等待
  • 当然,要杀死他也很简单,开另一个窗口用kill -9 [pid]就行,SIGKILL可是最后的杀手锏,该进程收到该信号后的执行逻辑不能被修改
11.1.2 信号机制究竟做了什么?

###### 11.1.2.1 硬中断和软中断

  • 硬中断和软中断并非某种具体的信号,而是大的概念,和"加锁"一样,硬中断和软中断是结果而非过程,具体过程的实现可以有很多种,需要看具体情况来对策性使用

  • 我们简单概述一下概念:

    1. 软中断: 指由软件触发的中断行为,一般情况下,软中断在CPU占用极高的时候不会被立刻执行,而是会做等待,软中断会在某个合适的时刻被触发,意味着软中断存在"中断时机"这么一说
    2. 硬中断: 指由硬件触发的中断行为
  • 更详细地说,比方说进程地址越界访问被内核检测到了,内核会触发软中断,内核此时有两个选择,其一,内核会找一个"软中断子系统"的玩意,属于内核旗下,收到内核管理,专门用于处理软中断,软中断收到内核的通知之后下方中断信号到进程,然后进程就被中断了,其二,有时候内核也会自己下方中断,比方说利用之前在进程调度有提到过的workqueue的机制,也可以实现中断(这里其实理解起来还有些模糊,后面还会聊到的)

  • 或者说,电脑中有某个硬件向内核发送了一个错误报告,此时就是硬中断,内核收到错误报告会执行软中断的步骤

  • 更简单的说,区分软中断和硬中断的方式,就是看中断的根源究竟是由谁发出的

  • 而"信号"这个东西,也算是属于软中断,但和由内核发出的软中断不太一样,这中类型的中断的发出者理论上是另一个进程,例如进程调用kill()系统调用,此时信号代码会被传递到内核,内核会进行有效性分析,权限检测等等,确认这个命令合法之后,才会开始执行软中断


  • 信号机制实际上有两种:

    1. 可处理信号: 用户可注册函数的信号,意味着这种信号是用户态的
    2. 不可处理信号: 完全由内核接管,用户无权注册函数的信号,这种信号是内核态的
  • 关于可处理信号,之前我们聊到过,因为启动一个可执行文件时,glibc和内核会注入一段内容在进程中,这部分内容就包含着可处理信号的逻辑,换句话说,可处理信号被内核或者说"软中断子系统"发送到进程后,由进程自行调用自己的代码,完成对于该信号的处理,并且因为代码是归进程管的,所以用户对于这部分代码是可以"注册"(也就是接口signal()来自定义处理逻辑)的,这里的实现方式其实有点像是之前的进程池的沟通方式,都是设置接收信号后的执行逻辑,然后由接收者自行执行(当然这里只指过程,而非达成进程池这种同步机制,信号可是异步的),意思是可处理信号才更像是"信号",由接收方自行处理接收信号后的逻辑

  • 而关于不可处理信号,这个可就暴躁很多了,我们知道"信号"中不乏有一些极为暴力的条目,比方说SIGKILL这种,像这样的"后背隐藏能源"信号要是留给用户乱搞乱自定义,到时候进程都杀不死可就好玩了,所以一般不可处理信号都是用来江湖救急的,用法也十分暴力,内核一旦发出这种信号,进程就可以一边凉快去了,内核会自己通过进程调度等等方式杀死你,根本不需要进程自己自杀,然后最后传递给进程的,就一个退出码而已,其他的什么也没留下,所以可以说,不可处理信号其实反倒是非常不像"信号",因为这就是无情的大手轻易撕碎进程,进程连收到信号之后执行的机会都没有

  • 同时,因为可处理信号需要留给用户自定义的机会,所以一般这种信号的执行机制是有一定接口规范的,一旦用户通过规范的方式(也就是signal())注册了自己的执行逻辑,那么执行的时候就会通过一个跳板机制跳转到用户的函数而非默认函数

  • 而不可处理信号因为处理的方式太过于底层,所用的技术和机制也可能存在很大差异,所以一般没啥接口规范(毕竟连接口都没有),完成目的就行

  • 我们称"进程能不能自己定义怎么处理"为"信号处置方式"

  • 而真正处理一个信号的时机,我们称作"信号的delivery"

  • 事实上我把信号机制的工作原理和中断机制的工作原理混杂了,实际上信号机制的工作原理和中断机制的工作原理并没有很强的关联性,不过从最终实现的效果上看,信号机制确实可以称得上是软中断,但从"软中断"这个术语含义严谨的考究看,其实实际关联不大,只是信号机制最终的效果类似于"软中断"而已,二者都实现了一种"非同步打断 + 延迟执行特定逻辑"的语义效果


###### 11.1.2.3 宏观来看

  • 其实这里的中断机制也很好地反映了操作系统内核"向下沟通硬件,向上支持软件"的神经中枢式的操作系统哲学
  • 中断机制保证了用户可以有限制地控制进程,也尽可能保证了操作系统运行的稳定性,以及调度的合理性

11.2 常见信号及其默认处理动作

  • 我们可以使用以下命令查找所有信号的的默认处理动作(以下是普通信号,普通信号会等待一个合适的时机再进行处理,事实上操作系统中还会存在一种叫做实时信号的东西,实时信号往往是立即处理的,但实时信号我们不讨论)
man 7 signal
SignalStandardActionComment
SIGABRTP1990CoreAbort signal from abort(3)
SIGALRMP1990TermTimer signal from alarm(2)
SIGBUSP2001CoreBus error (bad memory access)
SIGCHLDP1990IgnChild stopped or terminated
SIGCLD-IgnA synonym for SIGCHLD
SIGCONTP1990ContContinue if stopped
SIGEMT-TermEmulator trap
SIGFPEP1990CoreFloating-point exception
SIGHUPP1990TermHangup detected on controlling terminal or death of controlling process
SIGILLP1990CoreIllegal Instruction
SIGINFO-A synonym for SIGPWR
SIGINTP1990TermInterrupt from keyboard
SIGIO-TermI/O now possible (4.2BSD)
SIGIOT-CoreIOT trap. A synonym for SIGABRT
SIGKILLP1990TermKill signal
SIGLOST-TermFile lock lost (unused)
SIGPIPEP1990TermBroken pipe: write to pipe with no readers; see pipe(7)
SIGPOLLP2001TermPollable event (Sys V); synonym for SIGIO
SIGPROFP2001TermProfiling timer expired
SIGPWR-TermPower failure (System V)
SIGQUITP1990CoreQuit from keyboard
SIGSEGVP1990CoreInvalid memory reference
SIGSTKFLT-TermStack fault on coprocessor (unused)
SIGSTOPP1990StopStop process
SIGTSTPP1990StopStop typed at terminal
SIGSYSP2001CoreBad system call (SVr4); see also seccomp(2)
SIGTERMP1990TermTermination signal
SIGTRAPP2001CoreTrace/breakpoint trap
SIGTTINP1990StopTerminal input for background process
SIGTTOUP1990StopTerminal output for background process
SIGUNUSED-CoreSynonymous with SIGSYS
SIGURGP2001IgnUrgent condition on socket (4.2BSD)
SIGUSR1P1990TermUser-defined signal 1
SIGUSR2P1990TermUser-defined signal 2
SIGVTALRMP2001TermVirtual alarm clock (4.2BSD)
SIGXCPUP2001CoreCPU time limit exceeded (4.2BSD); see setrlimit(2)
SIGXFSZP2001CoreFile size limit exceeded (4.2BSD); see setrlimit(2)
SIGWINCH-IgnWindow resize signal (4.3BSD, Sun)
  • 其中:

    1. Standard表示来源标准(没有标准的话可能是某个平台特有的信号)
    2. Action表示默认行为:
      1. Term表示终止进程
      2. Core表示终止进程并生成core dump文件
      3. Ign表示忽略信号
      4. Stop表示暂停进程(可恢复)
      5. Cont表示继续运行被停止的进程
    3. Comment则是解释说明
  • 以及所有信号以及在不同架构的CPU下对应的信号编号

Signalx86/ARMAlpha/MIPSPARISCNotes
most othersSPARC
SIGHUP1111
SIGINT2222
SIGQUIT3333
SIGILL4444
SIGTRAP5555
SIGABRT6666
SIGIOT6666
SIGBUS7101010
SIGEMT-77-
SIGFPE8888
SIGKILL9999
SIGUSR110301616
SIGSEGV11111111
SIGUSR212311717
SIGPIPE13131313
SIGALRM14141414
SIGTERM15151515
SIGSTKFLT16--7
SIGCHLD17201818
SIGCLD--18-
SIGCONT18192526
SIGSTOP19172324
SIGTSTP20182425
SIGTTIN21212627
SIGTTOU22222728
SIGURG23162129
SIGXCPU24243012
SIGXFSZ25253130
SIGVTALRM26262820
SIGPROF27272921
SIGWINCH28282023
SIGIO29232222
SIGPOLLSame as SIGIO
SIGPWR3029/-1919
SIGINFO-29/---
SIGLOST--/29--
SIGSYS31121231
SIGUNUSED31--31
  • 当然,比较快的方式还可以使用
kill -l

11.3 可处理信号的信号处理

  • 准确来说,这个小节谈论的信号处理是我们可以以什么方式处理一个可处理信号,意味着一个可处理信号可以以不同形式处理,这点我们已经聊过了,你可以自定义可处理信号的处理方式

  • 本小节会谈论三种信号处理的方式(准确来说只有一种没谈过了,谈三种其实就只是强迫症犯了):

    1. 默认处理方式: 简单来说就是不做任何自定义修改时,处理信号时默认完成的动作,已经谈过了,就不赘述了
    2. 自定义修改: 简单来说是使用接口signal()来重定向信号处理的方法函数,也已经谈过了,也不赘述了
    3. 忽略处理: 简单来说就是不做处理,效果和自定义修改时,重定向函数里啥也不写的效果是一样的
  • 关于忽略处理,一样是使用signal(),只不过传入的是宏定义SIG_IGN("ignore signal"),效果就是,一旦将某个信号的处理方法忽略了,传这个信号给进程就不会有任何处理动作

  • 当然,有没有办法将一个信号的忽略处理改回默认处理方式呢?当然是有的,用宏定义SIG_DFL("default action")就行了

  • 我们来看个例子吧

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


int main()
{
        signal(2, SIG_IGN);

        while(true)
        {
                std::cout << "正在等待" << std::endl;
                sleep(1);
        }

        return 0;
}
oldking@iZwz9b2bj2gor4d8h3rlx0Z:~/code/code_25_6_8_signal_01$ ./test2
正在等待
正在等待
正在等待
^C正在等待
^C^C^C^C^C正在等待
^C^C^C^C^C正在等待
^C^C^C^C^C正在等待
^C^C^C^C正在等待
正在等待
正在等待
  • 你会发现,ctrl + c根本结束该进程,因为该进程忽略了处理该信号的方法

  • 好,那么问题来了,明明signal()的第二个参数必须传入一个类型为void (*) (int)的函数指针,为什么可以传一个宏定义进去,这个宏定义也是函数指针吗?

  • 答案是:是的,这个两个宏定义也是函数指针,但是,这两个函数指针的值分别是0(SIG_DFL)和1(SIG_IGN),准确来说是这个宏定义强转了int类型为void (*) (int)

  • 值得注意的是,虽然忽略处理的效果和重定向函数里啥也不写的效果是一样的,但底层的逻辑却有差别:

    1. 忽略处理: 我们知道一个信号肯定要经过内核,如果是忽略处理该信号的话,内核根本就不会发送该可处理信号给该进程
    2. 重定向函数里啥也不写: 这种情况的话,内核依旧会发送该可处理信号给该进程,只不过进程不做任何处理而已

11.4 信号传递的生命周期

  • 我们可以将信号传递的生命周期分为四个:
    1. 信号的产生(generation): 意味着信号因为某种条件而诞生
    2. 信号的排队与保存(queuing&pending): 我们知道,信号类似于软中断(如果你看了那个删去的小节的话,你就知道软中断发出的类似于"指令"的东西,并不会立刻被进程执行,而是说会在合适的时机传递给进程,那么信号也是类似的,不过信号不等于软中断!),所以一定的,信号可能会在内核中停留一段时间,那么肯定需要有可能是队列结构什么的来保存这些信号
    3. 信号的递达(delivery): 一个进程是怎样接收到一个信号的方式
    4. 信号的处理(handing): 这个我们已经谈论过了,就不在讨论了
11.4.1 信号的产生(generation)
11.4.1.1 int kill(pid_t pid, int sig)
  • 系统调用

  • 简单来说,就和在shell中使用kill命令差不多,你可以使用该系统调用给对应pid的进程发送信号,当然,这个操作肯定要经过内核,内核肯定会做一些可行性分析和权限检测之类的

  • 示例代码

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

int main()
{
        pid_t pid;
        std::cin >> pid;
        kill(pid, SIGKILL);

        return 0;
}
  • 那么我跑起来一个无限循环的可执行之后,就可以用这个带kill的可执行杀死那个死循环进程
oldking@iZwz9b2bj2gor4d8h3rlx0Z:~/code/code_25_6_8_signal_01$ ./test_SIG_IGN_exe 
正在等待
正在等待
正在等待
...
oldking@iZwz9b2bj2gor4d8h3rlx0Z:~/code/code_25_6_8_signal_01$ ./test_kill_exe 
337749
...
正在等待
正在等待
正在等待
Killed
  • 模拟实现kill命令
// mykill.cpp
#include <signal.h>
#include <string>
#include <algorithm>
#include <cmath>

int main(int argc, char* argv[])
{
        if(argc == 3)
        {
                int signal = 0;
                std::string s_signal = argv[1];

                int pid = 0;
                std::string s_pid = argv[2];

                // 字符处理
                s_signal = s_signal.substr(1);

                std::reverse(s_signal.begin(), s_signal.end());
                for(int i = 0; i < (int)s_signal.length(); i++)
                        signal += (s_signal[i] - '0') * std::pow(10, i);

                std::reverse(s_pid.begin(), s_pid.end());
                for(int i = 0; i < (int)s_pid.length(); i++)
                        pid += (s_pid[i] - '0') * std::pow(10, i);

                // 发送信号
                kill(pid, signal);
        }
        else 
        {
                perror("argv err");
        }

        return 0;
}
oldking@iZwz9b2bj2gor4d8h3rlx0Z:~/code/code_25_6_8_signal_01$ ./test_SIG_IGN_exe 
正在等待
正在等待
正在等待
...
oldking@iZwz9b2bj2gor4d8h3rlx0Z:~/code/code_25_6_8_signal_01$ ./mykill_exe -9 340793
...
正在等待
正在等待
正在等待
Killed
11.4.1.2 int raise(int sig)
  • 注意了,这个可不是系统调用!包含在signal.h

  • 这个接口用于给自己发送信号,类似于

int raise(int sig)
{
    kill(getpid(), sig);
}
  • 所以实际上哪怕给自己发信号,也是要经过内核的,因为可能要进行信号的存储/排队以及可行性检查等等操作,这些都需要更高一级的内核帮忙搞定
#include <iostream>
#include <signal.h>

void func(int signalnum)
{
        std::cout << "收到了信号: " << signalnum << std::endl;
}

int main()
{
        signal(2, func);

        std::cout << "sleep: 3" << std::endl;
        sleep(3);

        while(true)
        {
                raise(2);
                std::cout << "sleep: 2" << std::endl;
                sleep(2);
        }

        return 0;
}
oldking@iZwz9b2bj2gor4d8h3rlx0Z:~/code/code_25_6_8_signal_01$ ./test_raise_exe 
sleep: 3
收到了信号: 2
sleep: 2
收到了信号: 2
sleep: 2
收到了信号: 2
sleep: 2
11.4.1.3 void abort(void)
  • 注意了,这个也不是系统调用,包含在stdlib.h

  • 这个接口的效果是,直接杀死自己,没有参数,没有返回值,或者说,向自己发送一个叫做SIGABRT的信号,然后杀死自己

  • 如果自定义了处理方式,那么有意思的是,他会先执行自定义的处理方式,然后才杀死自己

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

void func(int signalnum)
{
        std::cout << "收到了信号: " << signalnum << std::endl;
}

int main()
{
        signal(SIGABRT, func);

        std::cout << "sleep: 3" << std::endl;

        abort();

        return 0;
}
oldking@iZwz9b2bj2gor4d8h3rlx0Z:~/code/code_25_6_8_signal_01$ ./test_abort_exe 
sleep: 3
收到了信号: 6
Aborted (core dumped)
11.4.1.4 由软件条件产生的信号
  • 简单来说,就是内核判断到了某些逻辑,然后触发了发送信号的机制,发送某些特定信号给某个进程

  • 我们简单举个例子哈,类似于:

    1. SIGPIPE: 当管道写端关闭时,内核会检测该管道还有没有写端,如果管道没有写端了,进程就会发送SIGPIPE给读端,然后读端进程就会被杀死
    2. SIGALRM: 当调用了alarm()函数后,每隔用户设定好的时间后,内核会发送SIGALRM信号给该进程,如果该进程没有自定义对于SIGALRM的处理方式,就会被执行默认处理方式,即被杀死
  • 模拟实现一个简易闹钟

#include <iostream>
#include <signal.h>
#include <unistd.h> //alarm()

void func(int signumber)
{
        std::cout << "接收到信号: " << signumber << std::endl;
        std::cout << "响铃!!!!" << std::endl;
}


int main()
{
        signal(SIGALRM, func);

        int second = 0;
        std::cout << "到定多久时? " << std::endl;
        std::cin >> second;

        alarm(second);

        for(int i = 0; i < 8; i++)
        {
                std::cout << "正在等待" << std::endl;
                sleep(1);
        }

        return 0;
}
oldking@iZwz9b2bj2gor4d8h3rlx0Z:~/code/code_25_6_8_signal_01$ ./myalarm_exe 
到定多久时? 
4
正在等待
正在等待
正在等待
正在等待
接收到信号: 14
响铃!!!!
正在等待
正在等待
正在等待
正在等待
11.4.1.5 由硬件异常产生信号
  • 区别于由软件产生的信号,由硬件产生的信号的发出者是硬件,硬件通过某种方式(其实是异常或者中断机制)通知给内核,然后内核分析后转发一个信号给进程

  • 模拟野指针

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

void func(int signumber)
{
        std::cout << "接收到信号: " << signumber << std::endl;
}

int main()
{
        //signal(SIGSEGV, func);

        int* p = NULL;
        *p = 10;

        while(true)
        {
                std::cout << "正在等待" << std::endl;
                sleep(1);
        }

        return 0;
}
oldking@iZwz9b2bj2gor4d8h3rlx0Z:~/code/code_25_6_8_signal_01$ ./test_SIGSEGV_exe 
Segmentation fault (core dumped)
  • 当然,这个信号的处理方式是可以自定义的
#include <iostream>
#include <signal.h>

void func(int signumber)
{
        std::cout << "接收到信号: " << signumber << std::endl;
        sleep(1);
}


int main()
{
        signal(SIGSEGV, func);

        int* p = NULL;
        *p = 10;

        while(true)
        {
                std::cout << "正在等待" << std::endl;
                sleep(1);
        }

        return 0;
}
oldking@iZwz9b2bj2gor4d8h3rlx0Z:~/code/code_25_6_8_signal_01$ ./test_SIGSEGV_exe 
接收到信号: 11
接收到信号: 11
接收到信号: 11
...
  • 有意思的问题来了,为什么内核会一直给进程发信号呢?
  • 我们简单点说,即陷入了"检查代码"->"发现错误"->"发送信号"->"开始继续运行"->"检查代码"->"发现错误"->"发送信号"的死循环
  • 从底层视角看,CPU寄存器会记录进程运行的上下文,同时,我们也知道,一个进程不可能会一直占用CPU资源,所以寄存器还担负着恢复执行位置的职责(准确来说是从保存运行上下文的栈中拷贝当前执行位置到寄存器),于是就发生了很神奇的一幕
  • CPU从记录位置准备开始执行,执行之前需要做一些检查,比如说这里是检查内存是否访问合法,但此时检查发现问题了,于是报了个硬件异常给了内核,CPU对当前进程的运行位置做好保存工作转而让内核去处理报出的硬件异常(此时切换到内核态,或者说让内核占用CPU资源以执行发送信号的代码),然后内核诊断为段错误,于是此时还要去解决发送信号的事情,信号完成发送之后,恢复当时执行的代码,但因为检查出错误的代码根本就没执行,所以依旧从错误代码处准备执行,然后做执行前检查,然后接着发现错误并报出硬件异常给CPU...至此,死循环达成
11.4.1.6 Core Dump
  • 老实说咱现阶段着实用不到这个东西,不过还是得见一见,免得以后程序跑出问题都不知道咋解决

  • 解释一下Core Dump: Core Dump是一个可执行文件,简单来说这就是一个定格瞬间,它保存了一个程序出错时的状态,包括堆栈,运行位置,错误原因,变量值等等,我们可以用gdb运行这个Core Dump,以排查错误

  • 默认情况下Core Dump是不会生成的,需要用一些手段,ulimit -c 1024可以允许生成Core Dump文件并设置文件最大大小为1024kb

oldking@iZwz9b2bj2gor4d8h3rlx0Z:~/code/code_25_6_8_signal_01$ ulimit -c 1024
oldking@iZwz9b2bj2gor4d8h3rlx0Z:~/code/code_25_6_8_signal_01$ ulimit -a
real-time non-blocking time  (microseconds, -R) unlimited
core file size              (blocks, -c) 1024
data seg size               (kbytes, -d) unlimited
scheduling priority                 (-e) 0
file size                   (blocks, -f) unlimited
pending signals                     (-i) 6469
max locked memory           (kbytes, -l) 214672
max memory size             (kbytes, -m) unlimited
open files                          (-n) 65535
pipe size                (512 bytes, -p) 8
POSIX message queues         (bytes, -q) 819200
real-time priority                  (-r) 0
stack size                  (kbytes, -s) 8192
cpu time                   (seconds, -t) unlimited
max user processes                  (-u) 6469
virtual memory              (kbytes, -v) unlimited
file locks                          (-x) unlimited
  • 默认不生成Core Dump文件的原因是因为可能会包含用户的密码等各种敏感信息,但如果产品还没有上线,这个玩意还是值得一用的(猜的)

  • 关于coredump的使用(以下内容是在写线程池的时候补充的,你猜猜为什么补充这个doge),总之就是,如果不会这个,那么复现线程池崩溃且调试线程池将会是一个异常恶心的事情,毕竟这不是IDE,Linux上调试这种还是有点麻烦的

  • 首先,我这里已经默认你启用了coredump并设置了其文件最大大小

  • 那么好,怎么获取一个coredump文件??

  • 首先先检查一下你的工作路径,coredump有可能会直接生成在工作路径里

  • 不过如果你是比较新的系统,估计coredump多半也不会在工作路径,此时使用以下命令查看配置

cat /proc/sys/kernel/core_pattern
  • 我这里返回了这个
|/usr/share/apport/apport -p%p -s%s -c%c -d%d -P%P -u%u -g%g -F%F -- %E
  • 证明该coredump其实不是没有生成,而是被一个叫做apport的服务接管了,这里用了管道接管了coredump的生成

  • 按理讲,apport接管了coredump生成之后,会统一存储路径,但如果你是将一个云服务器当作生产环境用(就像我一样),那么其实云服务器厂商可能设置了apport不允许生成coredump,而是直接禁用了其功能,相当于coredump直接被丢垃圾桶了

  • 而找到一个coredump实际上挺繁琐的,这里直接教各位一个一劳永逸的办法

sudo prlimit --pid $$ --core=unlimited
ulimit -c
  • 此时应该返回unlimited,然后
echo core.%e.%p | sudo tee /proc/sys/kernel/core_pattern
  • 这样就临时修改coredump的存储路径为工作路径了

  • 使用以下指令以使用coredump

gdb [exe file name] [coredump file name]
  • 如何观察?

  • 首先,一旦使用gdb打开execoredump,就能看到以下这一串,包含杀死该进程的信号,原因,哪个文件的哪个函数,甚至说哪一行

Program terminated with signal SIGSEGV, Segmentation fault.
#0  0x0000571d026973a0 in main () at test.cpp:62
62              *pa = 1;
  • 使用thread命令可以查看当前所处的线程
(gdb) thread
[Current thread is 1 (Thread 0x77f3aaf33500 (LWP 1113959))]
  • 使用info threads可以查看现在有那些线程存在
(gdb) info threads
  Id   Target Id                           Frame 
* 1    Thread 0x77f3aaf33500 (LWP 1113959) 0x000055ca32faa30e in main () at test.cpp:62
  2    Thread 0x77f3aa6006c0 (LWP 1113960) 0x000077f3aa8a0038 in lll_mutex_lock_optimized (mutex=0x55ca32fad040 <mutex>)
    at ./nptl/pthread_mutex_lock.c:45
  3    Thread 0x77f3a9c006c0 (LWP 1113961) __GI___lll_lock_wake (futex=futex@entry=0x55ca32fad040 <mutex>, private=<optimized out>)
    at ./nptl/lowlevellock.c:64
  • 最左边这个idgdb内部定义的线程id,并非操作系统中的LWP无关,不过你也可以看到其也能返回线程的LWP

  • 使用thread [thread id]可以切换所处线程

(gdb) thread 2
[Switching to thread 2 (Thread 0x77f3aa6006c0 (LWP 1113960))]
#0  0x000077f3aa8a0038 in lll_mutex_lock_optimized (mutex=0x55ca32fad040 <mutex>) at ./nptl/pthread_mutex_lock.c:45
warning: 45     ./nptl/pthread_mutex_lock.c: No such file or directory
(gdb) info threads
  Id   Target Id                           Frame 
  1    Thread 0x77f3aaf33500 (LWP 1113959) 0x000055ca32faa30e in main () at test.cpp:62
* 2    Thread 0x77f3aa6006c0 (LWP 1113960) 0x000077f3aa8a0038 in lll_mutex_lock_optimized (mutex=0x55ca32fad040 <mutex>)
    at ./nptl/pthread_mutex_lock.c:45
  3    Thread 0x77f3a9c006c0 (LWP 1113961) __GI___lll_lock_wake (futex=futex@entry=0x55ca32fad040 <mutex>, private=<optimized out>)
    at ./nptl/lowlevellock.c:64
  • 使用bt命令,将会获取当前所处线程栈帧中所有入栈的函数及其参数信息
(gdb) bt
#0  0x000077f3aa8a0038 in lll_mutex_lock_optimized (mutex=0x55ca32fad040 <mutex>) at ./nptl/pthread_mutex_lock.c:45
#1  ___pthread_mutex_lock (mutex=0x55ca32fad040 <mutex>) at ./nptl/pthread_mutex_lock.c:93
#2  0x000055ca32faa235 in func1 (args=0x0) at test.cpp:17
#3  0x000077f3aa89caa4 in start_thread (arg=<optimized out>) at ./nptl/pthread_create.c:447
#4  0x000077f3aa929c3c in clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:78
  • 使用thread apply all bt可以一次性所有线程的栈帧的所有内容
(gdb) thread apply all bt

Thread 3 (Thread 0x77f3a9c006c0 (LWP 1113961)):
#0  __GI___lll_lock_wake (futex=futex@entry=0x55ca32fad040 <mutex>, private=<optimized out>) at ./nptl/lowlevellock.c:64
#1  0x000077f3aa8a1ad2 in lll_mutex_unlock_optimized (mutex=0x55ca32fad040 <mutex>) at ./nptl/pthread_mutex_unlock.c:43
#2  __GI___pthread_mutex_unlock_usercnt (decr=1, mutex=0x55ca32fad040 <mutex>) at ./nptl/pthread_mutex_unlock.c:68
#3  ___pthread_mutex_unlock (mutex=0x55ca32fad040 <mutex>) at ./nptl/pthread_mutex_unlock.c:368
#4  0x000055ca32faa253 in func1 (args=0x0) at test.cpp:20
#5  0x000077f3aa89caa4 in start_thread (arg=<optimized out>) at ./nptl/pthread_create.c:447
#6  0x000077f3aa929c3c in clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:78

Thread 2 (Thread 0x77f3aa6006c0 (LWP 1113960)):
#0  0x000077f3aa8a0038 in lll_mutex_lock_optimized (mutex=0x55ca32fad040 <mutex>) at ./nptl/pthread_mutex_lock.c:45
#1  ___pthread_mutex_lock (mutex=0x55ca32fad040 <mutex>) at ./nptl/pthread_mutex_lock.c:93
#2  0x000055ca32faa235 in func1 (args=0x0) at test.cpp:17
#3  0x000077f3aa89caa4 in start_thread (arg=<optimized out>) at ./nptl/pthread_create.c:447
#4  0x000077f3aa929c3c in clone3 () at ../sysdeps/unix/sysv/linux/x86_64/clone3.S:78

Thread 1 (Thread 0x77f3aaf33500 (LWP 1113959)):
#0  0x000055ca32faa30e in main () at test.cpp:62
  • 自此,我们可以通过栈帧大致分析究竟是哪个接口或者成员出了问题,此时足矣,至于多线程调试,我们挖个坑,后面谈
11.4.2 信号的保存(pending)
  • 我们暂时不聊信号的排队,因为其不是传统信号

  • 说实话,我们之前聊到过的,内核发送一个信号给进程的概念其实是非常模糊的,难道内核和进程之间还需要用某种管道一类的东西通信???

  • 并非并非,内核可是管理进程的啊

  • 在了解什么是"发送信号"这个模糊概念之前,我们首先得了解一下相关的内核数据结构

  • 我们知道的,管理一个进程使用的是task_struct,也就是PCB

  • 其中两个bitmap,一个叫做blocked(阻塞),另一个叫作sigpending(挂起),这两个bitmap用于描述现在这个信号的状态,不过现在说他俩的概念会非常难说,等下就会提到

  • 那么发送信号其实就是一件非常简单的事情,即CPU在内核态的时候,将这个进程PCB的这两个bitmap做修改就行了

  • 现在来谈谈这两个bitmap的使用

  • 首先,每一个bit都对应着一个信号,例如,signalnumber1的信号对应着bitmap0号位

  • 一旦该位为1,证明该信号处于该状态,比方说blocked0号位为1,则代表signalnumber1的信号正在被阻塞(暂时不用理解什么是"阻塞")

  • 那么好的,现在我们需要着手准备解释这两个状态的意义和处理时机了,老实说我自己学这里的时候,还是比较煎熬的,做好准备即刻发车

11.4.2.1 信号阻塞的意义
  • 信号的阻塞代表着该信号暂时不可被处理

  • 什么情况下不可被处理?

    1. 进程在处理临界区时
    2. 设置某个信号的handler(用户自定义的信号处理方式)时
    3. 其他......
  • 因为处理临界区时,是不允许其他任何内容干扰的,所以包括对于信号的处理,一律得滚一边去,要是干扰到了公共资源,那遭殃的可能就不仅仅是当前进程了

  • 而设置某个信号的handler也要防止其他信号干扰

11.4.2.2 信号挂起的意义
  • 要知道,信号会因为时间片分配等问题,并不会被立刻执行,所以存在这么一种挂起的状态

  • 信号执行需要满足几个条件:

    1. 已经运行到该进程的时间片了
    2. 信号没有被屏蔽
    3. 信号没有被阻塞
    4. 信号被挂起了
  • 一般来说,内核需要让进程响应某个信号时,pending一定是优先被设置为1的,一旦进程满足了其他条件,那么信号处理就会被立刻执行

  • 或者我们可以这么理解,内核让进程去干了,所以pending标记为1,但进程不一定马上得干,要符合"安全地干","规范地干","稳定地干"的条件,那么pending就是用来记录还不符合干的条件的信号的!

11.4.2.3 信号忽略
  • task_struct中,有一个叫做sighand_struct的结构专门用于描述一个进程忽略的信号
struct sighand_struct {
	atomic_t		count;
	struct k_sigaction	action[_NSIG];
	spinlock_t		siglock;
};
  • 你能看到其中有一个叫做action的数组,每一个元素里都维护着对应信号的sa_handler,也就是一个信号的处理方式

  • 于是,如果你使用signal()这个接口忽略了一个信号,那么其中的sa_handler就会被设置为SIG_IGN

  • 在进程修改一个信号的pendingblocked之前,会先检查这个action数组,如果对应信号的sa_handler被设置为了SIG_IGN,那么将不被允许修改pendingblocked

  • 如果对应信号的sa_handler被设置为了SIG_IGN,信号的pendingblocked依然可能被正确设置,只不过在deliver的时候会什么都不做(也有一些情况是,内核压根不会pending该信号)

11.4.2.4 阻塞和挂起的时机
  • 情景1: 进程正在临界区,此时内核需要进程响应一个信号时

    1. 对应blocked会在进入临界区的时候会被系统调用设置为1,现在进程在临界区,意味着blocked已经被设置为1
    2. 检测对应信号的sa_handler,发现其设置并非SIG_IGN
    3. 对应pending会被设置为1,因为内核需要进程响应该信号
    4. 从内核态恢复进程上下文后,CPU切换为用户态并处理该进程的代码使其脱离临界区
    5. 因为进程调用了系统调用等,CPU进入内核态,内核发现该进程脱离了临界区,于是设置对应blocked0
    6. CPU切换为用户态并处理该进程的代码时,发现有pending的信号且所有条件均符合
    7. 立刻执行对应信号的处理方式
  • 情景2: 进程忽略了某个信号,但此时内核需要进程响应此信号时

    1. 检测对应信号的sa_handler,发现其设置为SIG_IGN
    2. 内核什么都不做,并恢复进程上下文,然后回到用户态
    3. 用户或者进程自己做了一些操作,使得该信号不被忽略了
    4. 因为一些其他原因,内核再次需要进程响应此信号
    5. 检测对应信号的sa_handler,发现其设置并非SIG_IGN
    6. 内核将该信号的pending都设置为1
    7. CPU切换为用户态并处理该进程的代码时,发现了该符合条件且在pending的信号
    8. 立刻执行对应信号的处理方式
  • 情景3: 当一个进程正在正常运行时,内核试图让进程响应多个不同的信号

    1. 检测对应信号的sa_handler,发现其设置并非SIG_IGN
    2. 将所有需响应信号的pending都设置为1
    3. CPU切换为用户态并处理该进程的代码时,发现了多个符合条件且在pending的信号
    4. 立刻执行对应信号的处理方式(进程将会按照优先级处理信号,即signalnumber越小,优先级越高)
  • 情景4: 当一个进程正在正常运行时,内核试图让进程响应多个不同的信号,但该时间片处理不完所有的信号

    1. 检测对应信号的sa_handler,发现其设置并非SIG_IGN
    2. 将所有需响应信号的pending都设置为1
    3. CPU切换为用户态并处理该进程的代码时,发现了多个符合条件且在pending的信号
    4. 立刻执行对应信号的处理方式(进程将会按照优先级处理信号,即signalnumber越小,优先级越高)
    5. CPU脱离当前进程的时间片转而处理其他进程或者进入内核态
    6. CPU再次切换为用户态并处理该进程的代码时,会继续执行没执行完的信号处理方式,同时接着执行没处理的pending信号的处理方式
  • 值得注意的是,更改sa_handler仅可以修改一部分信号是否可被忽略,而像是SIG_KILL这种信号是无法被忽略的,需要注意

11.4.2.5 信号保存相关的系统调用
  • 首先了解几个概念名称:

    1. 我们谈到的叫做blockedbitmap,我们一般称它为阻塞信号集
    2. 我们谈到的叫做pendingbitmap,我们一般称它为未决信号集
  • 后面我们需要接触一个新的对象类型,叫做sigset_t我们粗略的将sigset_t这个类型看作是一个bitmap就行了

  • 接下来我们学习的几个系统调用就是用来调整和查找信号集类型的对象的:

    1. int sigemptyset(sigset_t *set): 将set信号集清空
    2. int sigfillset(sigset_t *set): 将set信号集填满所有已知信号
    3. int sigaddset(sigset_t *set, int signo): 向set信号集中添加一个signalnumbersigno的信号
    4. int sigdelset(sigset_t *set, int signo): 向set信号集中删除一个signalnumbersigno的信号
    5. int sigismember(const sigset_t *set, int signo): 在set信号集中判断是否存在signalnumbersigno的信号
  • 值得注意的是,这几个接口都不是用于调整PCB中的信号集的,而是用来辅助其他接口用的,意味着此时这里的这个set就是一个临时对象而已

  • 打个比方,就好像是你点外卖一样,这几个接口是用来做菜的,真正要完成订单,还需要另一个外卖员(即"其他接口")来帮忙运送

  • int sigprocmask(int how, const sigset_t *set, sigset_t *oset): 用于读取或者更改阻塞信号集

    1. how: 信号屏蔽字,其实就是一个宏定义罢了,传入不同的信号屏蔽字以让该接口实现不同的效果
    2. set: 对PCB中的阻塞信号集进行修改的依据
    3. oset: 对PCB中的阻塞信号集进行输出的返回型参数
    4. 返回值: 成功为0,失败为-1并设置errno
  • 使用方面

    1. 如果oset不是空指针,那么将当前阻塞信号集中的内容会被传进oset
    2. 如果set不是空指针,则会依据set的内容和how这个"修改方式"对PCB的阻塞信号集进行修改
    3. 如果osetset都不是空指针,则那么将修改前的阻塞信号集中的内容传进oset中,然后依据set的内容和howPCB的阻塞信号集进行修改
  • 常用的信号屏蔽字:

    1. SIG_BLOCK: 将set中的信号添加进阻塞信号集
    2. SIG_UNBLOCK: 在阻塞信号集中,移除set中存在的信号
    3. SIG_SETMASK: 直接将阻塞信号集的信号替换为set的信号
  • int sigpending(sigset_t *set): 用于读取当前进程的未决信号集

    1. set: 返回型参数,接收读取的未决信号集的内容
    2. 返回值: 成功为0,失败为-1并设置errno
  • 写一个小程序,可以通过手动输入的方式阻塞若干信号

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

int main()
{
        sigset_t set;

        std::set<int> siglist;
        while(true)
        {
                int tmp = 0;
                std::cout << "输入你想要阻塞的信号:> ";
                std::cin >> tmp;
                if(tmp == 0)
                        break;
                siglist.insert(tmp);
        }

        for(auto it : siglist)
        {
                sigaddset(&set, it);
        }

        sigprocmask(SIG_BLOCK, &set, NULL);

        for(auto it : siglist)
        {
                raise(it);
        }

        sigset_t oset;
        sigpending(&oset);

        for(int i = 1; sigismember(&oset, i) != -1; i++)
        {
                if(sigismember(&oset, i) == 1)
                {
                        std::cout << i << "号信号已被阻塞" << std::endl;
                }
                else 
                {
                        std::cout << i << "号信号未被阻塞" << std::endl;
                }
        }


        for(int i = 5; i >= 0; i--)
        {
                std::cout << "进程将在" << i << "秒后被杀死" << std::endl;
                sleep(1);
        }

        raise(9);

        return 0;
}
oldking@iZwz9b2bj2gor4d8h3rlx0Z:~/code/code_25_6_13_signal_02$ ./test_sigset_t_EXE 
输入你想要阻塞的信号:> 1
输入你想要阻塞的信号:> 2
输入你想要阻塞的信号:> 4
输入你想要阻塞的信号:> 7
输入你想要阻塞的信号:> 10
输入你想要阻塞的信号:> 15
输入你想要阻塞的信号:> 7
输入你想要阻塞的信号:> 12
输入你想要阻塞的信号:> 0
1号信号已被阻塞
2号信号已被阻塞
3号信号未被阻塞
4号信号已被阻塞
5号信号未被阻塞
6号信号未被阻塞
7号信号已被阻塞
8号信号未被阻塞
9号信号未被阻塞
10号信号已被阻塞
11号信号未被阻塞
12号信号已被阻塞
13号信号未被阻塞
14号信号未被阻塞
15号信号已被阻塞
16号信号未被阻塞
17号信号未被阻塞
18号信号未被阻塞
19号信号未被阻塞
20号信号未被阻塞
...
61号信号未被阻塞
62号信号未被阻塞
63号信号未被阻塞
64号信号未被阻塞
进程将在5秒后被杀死
进程将在4秒后被杀死
进程将在3秒后被杀死
进程将在2秒后被杀死
进程将在1秒后被杀死
进程将在0秒后被杀死
Killed
11.4.3 信号的递达(delivery)
11.4.3.1 信号的递达的机制
  1. 当进程因为CPU调度,或者是其他原因中断,抑或是调用了系统调用后,CPU此时会进入内核态
  2. 进入内核态后,CPU可能会先干一些其他无关操作,例如处理异常等等,然后将当前需要发送的信号通过task_struct的几个结构发送给进程
  3. 最后在内核态调用do_signal(),如果用户自定义了处理方式,那么将会以某个函数为跳板回到用户态
  4. 回到用户态后,进程会强制被执行自定义的处理方式,执行完后会通过sys_sigreturn()回到内核态
  5. 回到内核态后,此时就开始处理中断恢复流程,恢复进程的上下文,然后转为用户态
  6. 进程接着往下执行如此往复

image-29.png

  • 值得一提的是,以上是信号已经被用户自定义注册过且没有被阻塞也没有被忽略的情况

  • 如果信号被忽略了,那么在执行do_signal()的时候会跳过该进程,执行结束后恢复进程上下文并回到用户态

  • 如果信号被阻塞了,那么在执行do_signal()的时候会设置pending1,然后跳过执行处理动作,执行结束后恢复进程上下文并回到用户态

  • 如果信号没有用户自定义注册的处理动作,那么将根据处理动作来调整是否处于用户态和内核态,有以下情况

    1. 执行结束后恢复进程上下文并回到用户态(Ign)
    2. 保持内核态并杀死进程(Term/Core)(可能伴随生成core文件)
    3. 保持内核态并挂起进程(Stop)
    4. 解除进程阻塞状态,执行结束后恢复进程上下文并回到用户态(Cont)
  • 总结一下

信号状态结果是否进入用户态是否执行 handler
被阻塞留在 pending,忽略
被忽略清除,不触发处理
默认:Term/Core内核中终止
默认:Stop内核中挂起
默认:Ign不处理
默认:Cont不处理
用户处理器构造 trap frame 执行
11.4.3.2 查询/修改信号的对应处理方式
  • int sigaction(int signo, const struct sigaction *act, struct sigaction *oact): 这个接口用于查询,修改一个信号的处理方式

    1. signo: 代表需要调整的signal
    2. act: 如果此参数不为NULL,则修改对应信号的处理方式
    3. oact: 如果此参数不为NULL,则输出原来的信号的处理方式
  • 关于结构sigaction

struct sigaction {
	__sighandler_t	sa_handler;
	unsigned long	sa_flags;
	sigset_t	sa_mask;	/* mask last for extensibility */
};
  • 该结构用于描述一个信号的处理方式,sa_handler就是这个处理方式的函数指针

  • sa_mask用于描述处理该信号时需要阻塞哪些其他信号

  • sa_flags是控制信号行为的标志位

  • 你能看到,sigaction()这个接口也可以实现修改信号的处理方式,同时还支持导出信号处理方式,总体来说比signal()这个接口更加高级一些,同时也会更安全一些

  • 关于设计该接口的意义

  • 设计该接口主要的主要功能和signal()这个接口基本一致,但设计该接口还有一个用途,即防止信号处理方法被递归式调用

  • 换句话说,就是假设这个信号处理方法本身就有问题导致内核发送了相同的信号给进程,然后进程又重新执行一边该处理方法,然后又出现问题,又发送相同信号给进程...如此往复,就是一个死递归了,最终栈帧的空间会被耗尽(其实实际上会默认屏蔽自身信号,所以一般情况下这个问题是看不到的,反而会发生的其实是下面这条)

  • 同时,他还能避免一个类似的问题,就是不同处理方法之间的互相递归,方法a触发了信号b,然后执行信号b,信号b又触发了方法a,又是如此往复的问题

  • 换句话说,这也是sa_mask存在的意义,即防止发生死递归问题,抑或是逻辑及其混乱的其他递归调用问题

  • 这里我们可以写一个小玩意验证一下

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


int count = 0;

void func(int signum)
{
        count++;
        std::cout << "收到信号:" << signum << " 递归:" << count << "次" << std::endl;
        int* pa = nullptr;
        *pa = 11;
}

int main()
{
        struct sigaction in_sa;
        sigemptyset(&in_sa.sa_mask); // 清空sa_mask是没有用的,因为默认屏蔽自身信号,所以这条其实是无效操作
        in_sa.sa_handler = func;
        in_sa.sa_flags = SA_NODEFER; // 这里才是设置不屏蔽自身信号

        struct sigaction out_sa;

        sigaction(11, &in_sa, &out_sa);

        int* pa = nullptr;
        *pa = 11;

        return 0;
}
  • 然后效果就很搞笑了,最后被杀死了
收到信号:11 递归:1次
收到信号:11 递归:2次
...
收到信号:11 递归:2516次
收到信号:11 递归:2517次
收到信号:11 递归:2518次
Segmentation fault (core dumped)
11.4.4 关于信号机制的思考
  • 其实,如果咱们细看了"信号的递达"这一小节的话,你就可以发现,信号机制中内核和进程的异步通信通信策略,是一种非周期检测的异步通信策略,因为接收端接收并响应处理方式的时机,完全取决于内核是怎样安排的
  • 区别于进程间异步通信,进程间的异步通信一般是周期性的
  • 那为什么进程间很难实现这种非周期性的异步通信策略呢?
  • 主要是因为内核拥有对于进程的完全控制权,换句话说,虽然我们说是进程"接收"信号,但实际上进程是被动响应的,再换句话说,完全是内核要求进程响应的
  • 而进程和进程之间,几乎不存在这种完全控制权的关系,毕竟进程之间还是有独立性的
  • 而这种完全控制权,其实一定程度上可以归结为代码设计时,有非常逆天的耦合度,使得进程和内核之间的独立性毫无存在可言

  • 如有问题或者有想分享的见解欢迎留言讨论