<Linux进程信号>——《Linux》

时间:2022-12-22 11:32:26
本节重点:
1. 掌握 Linux 信号的基本概念
2. 掌握信号产生的一般方式
3. 理解信号递达和阻塞的概念,原理。
4. 掌握信号捕捉的一般方式。
5. 重新了解可重入函数的概念。
6. 了解竞态条件的情景和处理方式
7. 了解 SIGCHLD 信号, 重新编写信号处理函数的一般处理机制

目录

一、信号入门

1. 生活角度的信号

2. 技术应用角度的信号

3. 信号区分与说明

4. 信号概念

5. 用kill -l命令可以察看系统定义的信号列表

6. 信号处理常见方式

二、产生信号

1. 通过终端按键产生信号

2. 调用系统函数向进程发信号

3. 由软件条件产生信号

4. 硬件异常产生信号

三、阻塞信号

1. 信号其他相关常见概念

2. 在内核中的表示

3. sigset_t

4. 信号集操作函数

sigprocmask

sigpending

四、捕捉信号

1. 内核如何实现信号的捕捉

2. 信号捕捉函数signal

 3. 可重入函数​

后记:●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!

                                                                           ——By 作者:新晓·故知


一、信号入门

1. 生活角度的信号

在生活中,我们经常会通过一些信息去做相应的事情。这些信息其实就是一种信号。

2. 技术应用角度的信号

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

3. 信号区分与说明

(1)信号是给进程发送的,进程要具备处理信号的能力!

进程能够识别对应的信号,能够处理对应的信号。对于进程而言,即便是信号还没有产生,进程也已经具备识别和处理这个信号的能力。

(2)信号的产生是异步的,当信号产生的时候,对应的进程可能正在做更重要的事情,进程可以暂时不处理这个信号。

(3)进程是如何记住信号的?

进程对信号的处理有三种方式:

默认动作、忽略、自定义动作。

在进程的PCB的task_struct{}中,有位图这个结构,通过比特位的内容(1 or 0),标记信号。而task_struct{}是内核结构,只有OS能修改!OS是进程的管理者,进程的所有的属性的获取和设置,只能由OS进行。无论信号怎样产生,最终都是OS进行信号设置!

  • Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
  •  Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
  • 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。

<Linux进程信号>——《Linux》

<Linux进程信号>——《Linux》

以上发现在前后台混打时,指令顺序打乱,这不影响。根据冯诺依曼体系,输入输出分别在不同的空间,且OS可以回显也可以不回显。

<Linux进程信号>——《Linux》

 任务管理。

4. 信号概念

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

 5. kill -l命令可以察看系统定义的信号列表

<Linux进程信号>——《Linux》

<Linux进程信号>——《Linux》

  • 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define  SIGINT 2
  • 编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,signal(7)中都有详细说明: man 7 signal

6. 信号处理常见方式

(sigaction 函数稍后详细介绍 ), 可选的处理动作有以下三种 :
  •  忽略此信号。
  •  执行该信号的默认处理动作。
  • 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。

man signal

<Linux进程信号>——《Linux》

 <Linux进程信号>——《Linux》

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

using namespace std;

void handler(int signo)
{
    cout << "这是一个进程,刚刚获取了一个信号:" << signo << endl;
}
int main()
{
    cout << "信号回调函数测试:" << endl;
    signal(SIGINT, handler);
    
    sleep(3);
    cout << "进程已经设置完成!" << endl;
    sleep(3);
    while (true)
    {
        cout << "这是一个正在运行中的进程:" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

 <Linux进程信号>——《Linux》

<Linux进程信号>——《Linux》

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

using namespace std;

void handler(int signo)
{
    cout << "这是一个进程,刚刚获取了一个信号:" << signo << endl;
}
int main()
{
    cout << "信号回调函数测试:" << endl;
    signal(SIGINT, handler);
    // 这里不是调用handler方法,只是设置了一个回调,让SIGINT(2)产生的时候,该方法才会被调用
    // 如果不产生SIGINT(2),该方法不会被调用!
    // Ctrl + c:本质是给前台进程发送2号信号给目标进程,目标进程默认对2号信号的处理,是终止自己的进程

    signal(3, handler); // 更改了对2号信号的处理,设置了用户自定义处理方法

    sleep(3);
    cout << "进程已经设置完成!" << endl;
    sleep(3);
    while (true)
    {
        cout << "这是一个正在运行中的进程:" << getpid() << endl;
        sleep(1);
    }
    return 0;
}

 一般而言,一个进程的异常都与信号有关。

9号信号,是管理员信号,不能像3、4、5...等被设置为自定义信号。9号信号一般能杀掉大部分进程(D状态信号除外)。

二、产生信号

1. 通过终端按键产生信号

SIGINT 的默认处理动作是终止进程 ,SIGQUIT 的默认处理动作是终止进程并且 Core Dump, 现在我们来验证一下。
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命令改变了Shell进程的Resource Limit,test进程的PCBShell进程复制而来,所以也具 有和Shell进程相同的Resource Limit,这样就可以产生Core Dump了。 使用core文件:

core dump会把进程在运行中对应的异常上下文数据,core dump到磁盘上,方便进行调试。但一般会被关掉,因为若程序出现大量异常,那么将会小号很大存储空间。

2. 调用系统函数向进程发信号

首先在后台执行死循环程序 , 然后用 kill 命令给它发 SIGSEGV信号。 <Linux进程信号>——《Linux》
<Linux进程信号>——《Linux》
<Linux进程信号>——《Linux》

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

using namespace std;
//自己实现一个kill命令
//mykill  9 1234
static void Usage(const std::string &proc)
{
    cerr << "Usage: \n\t"<<proc<<"signo pid"<<endl;
}
//test3
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    if(kill(static_cast<pid_t>(atoi(argv[2])),atoi(argv[1])) == -1)
    {
        cerr << "kill: " <<strerror(errno)<<endl;
        exit(2);
    }
}

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

 int main()
 {
    while(1)
    {
        sleep(1);
        cout<<"我是一个进程: "<<getpid()<<endl;
    }
 }
 
.PHONY:all
all:mykill myproc
mykill:mykill.cc
	g++ -o $@ $^ -std=c++11
myproc:myproc.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f myproc mykill

<Linux进程信号>——《Linux》

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

using namespace std;

void handler(int signo)
{
    cout << "这是一个进程,刚刚获取了一个信号:" << signo << endl;
}
int main(int argc, char *argv[])
{
    signal(2,handler);    //没有调用对一个的handler方法,仅仅是注册
    while(1)
    {
        sleep(1);
        raise(2);
    }
}

 <Linux进程信号>——《Linux》

  • 4568test进程的id。之所以要再次回车才显示 Segmentation fault ,是因为在4568进程终止掉 之前已经回到了Shell提示符等待用户输入下一条命令,Shell不希望Segmentation fault信息和用 户的输入交错在一起,所以等用户输入命令之后才显示。
  • 指定发送某种信号的kill命令可以有多种写法,上面的命令还可以写成 kill -SIGSEGV 4568 kill -11 4568 , 11是信号SIGSEGV的编号。以往遇 到的段错误都是由非法内存访问产生的,而这个程序本身没错, 给它发SIGSEGV也能产生段错误
kill 命令是调用 kill 函数实现的。 kill 函数可以给一个指定的进程发送指定的信号。 raise 函数可以给当前进程发送指定 的信号 ( 自己给自己发信号 )
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
这两个函数都是成功返回0,错误返回-1。
<Linux进程信号>——《Linux》abort函数使当前进程接收到信号而异常终止。

#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。
<Linux进程信号>——《Linux》
<Linux进程信号>——《Linux》
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

using namespace std;

void handler(int signo)
{
    cout << "这是一个进程,刚刚获取了一个信号:" << signo << endl;
}
//test3
int main(int argc, char *argv[])
{
    signal(2,handler);    //没有调用对一个的handler方法,仅仅是注册
    signal(SIGABRT,handler);    //没有调用对一个的handler方法,仅仅是注册.  SIGABRT是6号信号,对其进行捕捉
    while(1)
    {
        sleep(1);
        //raise(2);
        abort();
    }
}

 是谁在推动操作系统做一系列的动作呢?

是硬件,时钟硬件,给OS发送时钟中断。

3. 由软件条件产生信号

SIGPIPE 是一种由软件条件产生的信号 , 管道 中已经介绍过了。本节主要介绍 alarm 函数 和 SIGALRM 信号。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
这个函数的返回值是 0 或者是以前设定的闹钟时间还余下的秒数。打个比方 , 某人要小睡一觉 , 设定闹钟为 30 分钟之后响,20 分钟后被人吵醒了 , 还想多睡一会儿 , 于是重新设定闹钟为 15 分钟之后响 ,“ 以前设定的闹钟时间还余下的时间 就是10 分钟。如果 seconds 值为 0, 表示取消以前设定的闹钟 , 函数的返回值仍然是以前设定的闹钟时间还余下的秒数
例 alarm <Linux进程信号>——《Linux》
<Linux进程信号>——《Linux》
#include <stdio.h>
#include <unistd.h>
int main()
{
    int count =14;
    alarm(1);
    for(;1;count++)
    {
        printf("count = %d\n",count);
    }
    return 8;
}

这个程序的作用是1秒钟之内不停地数数,1秒钟到了就被SIGALRM信号终止。<Linux进程信号>——《Linux》

#include <stdio.h>
#include <unistd.h>

using namespace std;
int cnt = 0;
int main()
{
    int sum = 0;
    // 统计进程1S内 cnt++多少次
    alarm(1);
    while (1)
    {
        printf("hello: %d\n", cnt++);
    }
}

 <Linux进程信号>——《Linux》

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

int cnt = 0;
void handler(int signo)
{
    cout << "这是一个进程,刚刚获取了一个信号:" << signo << " cnt: " << cnt << endl;
    exit(1);
}

int main()
{
    // 统计进程1S内 cnt++多少次
    signal(SIGALRM, handler);
    alarm(1);
    while (1)
    {
        cnt++;
        // printf("hello: %d\n", cnt++);
    }
}

那么崩溃的本质是什么呢?

在Linux环境下,其实是进程崩溃。其本质是该进程收到了异常信号!

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

比如:

(1)除零报错

在CPU内部进行计算,有状态寄存器,当进行除0操作时,CPU内部的状态寄存器会被设置成为有报错:浮点数越界。CPU内部的寄存器(硬件),OS就会识别到有报错。通过OS构建信号—>目标进程发送信号—>目标进程在合适的时候处理信号—>然后终止进程。

(2)越界&&野指针报错

我们在语言层面使用的地址(指针),其实都是虚拟地址—>物理地址—>物理内存—>读取对应的数据和代码。

如果虚拟地址有问题,地址转化的工作是由(MMU(硬件)+页表(软件)),转化过程就会引起问题—>表现在MMU上—>OS发现硬件出现问题。

同样,,OS就会识别到有报错。通过OS构建信号—>目标进程发送信号—>目标进程在合适的时候处理信号—>然后终止进程。

崩溃了不一定会导致进程终止!

4. 硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核 , 然后内核向当前进程发送适当的信号。例如当前进程执行了除以0 的指令 ,CPU 的运算单元会产生异常 , 内核将这个异常解释 为 SIGFPE 信号发送给进程。再比如当前进程访问了非法内存地址,,MMU 会产生异常 , 内核将这个异常解释为 SIGSEGV 信号发送给进程。
信号捕捉初识
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
   printf("catch a sig : %d\n", sig);
}
int main()
{
   signal(2, handler); //前文提到过,信号是可以被自定义捕捉的,siganl函数就是来进行信号捕捉的提前了解一下
   while(1);
   return 0;
}
[hb@localhost code_test]$ ./sig 
^Ccatch a sig : 2
^Ccatch a sig : 2
^Ccatch a sig : 2
^Ccatch a sig : 2
^\Quit (core dumped)
[hb@localhost code_test]$
模拟野指针
//默认行为
[hb@localhost code_test]$ cat sig.c 
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
   printf("catch a sig : %d\n", sig);
}
int main()
{
   //signal(SIGSEGV, handler);
   sleep(1);
   int *p = NULL;
   *p = 100;
    while(1);
   return 0;
}
[hb@localhost code_test]$ ./sig 
Segmentation fault (core dumped)
[hb@localhost code_test]$ 
//捕捉行为
[hb@localhost code_test]$ cat sig.c 
#include <stdio.h>
#include <signal.h>
void handler(int sig)
{
   printf("catch a sig : %d\n", sig);
}
int main()
{
   //signal(SIGSEGV, handler);
   sleep(1);
   int *p = NULL;
   *p = 100;
   while(1);
   return 0;
}
[hb@localhost code_test]$ ./sig 
[hb@localhost code_test]$ ./sig 
catch a sig : 11
catch a sig : 11
catch a sig : 11
由此可以确认,我们在 C/C++ 当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。
总结思考一下
  • 上面所说的所有信号产生,最终都要有OS来进行执行,为什么?OS是进程的管理者
  • 信号的处理是否是立即处理的?在合适的时候

信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?

  • 一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
  • 如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?

 进程等待部分回顾:

<Linux进程信号>——《Linux》

 <Linux进程信号>——《Linux》

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

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程
        int *p = nullptr;
        *p = 1000; // 野指针问题
        exit(1);
    }
    // 父进程
    int status = 0;
    waitpid(id, &status, 0);
    printf("exitcode: %d, signo: %d, core dump flag: %d\n",
           (status >> 8) & 0xFF, status & 0x7F, (status >> 7) & 0x1);
}

 <Linux进程信号>——《Linux》

<Linux进程信号>——《Linux》

<Linux进程信号>——《Linux》 ulimit -c 可以设置。

进程处理信号,不是立即处理的!而是当前进程从内核态切换至用户态会进行信号的检测预处理!

一般会有block、pending、递达、信号集(信号屏蔽等)等方式。

三、阻塞信号

1. 信号其他相关常见概念

  • 实际执行信号的处理动作称为信号递达(Delivery)
  • 信号从产生到递达之间的状态,称为信号未决(Pending)
  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

2. 在内核中的表示

信号在内核中的表示示意图 <Linux进程信号>——《Linux》

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

<Linux进程信号>——《Linux》

 

 

 

 

3. sigset_t

从上图来看 , 每个信号只有一个 bit 的未决标志 , 0 1, 不记录该信号产生了多少次 , 阻塞标志也是这样表示的。因此, 未决和阻塞标志可以用相同的数据类型 sigset_t 来存储 ,sigset_t 称为信号集 , 这个类型可以表示每个信号的“ 有效 无效 状态 , 在阻塞信号集中 有效 无效 的含义是该信号是否被阻塞 , 而在未决信号集中 有效” 无效 的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask), 这里的 屏蔽 应该理解为阻塞而不是忽略。

4. 信号集操作函数

sigset_t 类型对于每种信号用一个 bit 表示 有效 无效 状态 , 至于这个类型内部如何存储这些 bit 则依赖于系统实现, 从使用者的角度是不必关心的 , 使用者只能调用以下函数来操作 sigset_ t 变量 , 而不应该对它的内部数据做 任何解释 , 比如用 printf 直接打印 sigset_t 变量是没有意义的
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
  • 函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
  • 函数sigfifillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
  • 注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptysetsigfifillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddsetsigdelset在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回 0, 出错返回 -1 sigismember 是一个布尔函数 , 用于判断一个信号集的有效信号中是否包含某种 信号, 若包含则返回 1, 不包含则返回 0, 出错返回 -1

sigprocmask

调用函数 sigprocmask 可以读取或更改进程的信号屏蔽字 ( 阻塞信号集 )。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1
如果 oset 是非空指针 , 则读取进程的当前信号屏蔽字通过 oset 参数传出。如果 set 是非空指针 , 则 更改进程的信号屏蔽字, 参数 how 指示如何更改。如果 oset set 都是非空指针 , 则先将原来的信号 屏蔽字备份到 oset , 然后根据set how 参数更改信号屏蔽字。假设当前的信号屏蔽字为 mask, 下表说明了 how 参数的可选值。
  <Linux进程信号>——《Linux》

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

sigpending

<Linux进程信号>——《Linux》

#include <signal.h>
sigpending
读取当前进程的未决信号集 , 通过 set 参数传出。调用成功则返回 0, 出错则返回 -1 。 下面用刚学的几个函数做个实验。程
序如下 :
  <Linux进程信号>——《Linux》

程序运行时 , 每秒钟把各信号的未决状态打印一遍 , 由于我们阻塞了 SIGINT 信号 , Ctrl-C 将会 使 SIGINT 信号处于未决状态, Ctrl-\ 仍然可以终止程序,因为SIGQUIT信号没有阻塞。
<Linux进程信号>——《Linux》
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

static void showPending(sigset_t *pendings)
{
    for (int sig = 1; sig <= 31; sig++)
    {
        if (sigismember(pendings, sig))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}
// test
int main()
{
    // 1.不断获取当前进程的pending信号集
    sigset_t pendings;
    while (true)
    {
        // 清空信号集
        sigemptyset(&pendings);
        // 获取当前进程(谁调用,获取谁)的pending 信号集
        if (sigpending(&pendings) == 0)
        {
            // 打印一下当前进程的pending 信号集
            showPending(&pendings);
        }
        sleep(1);
    }
}

 <Linux进程信号>——《Linux》

 <Linux进程信号>——《Linux》

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

void handler(int signo)
{
    cout << "这是一个进程,刚刚获取了一个信号:" << signo << endl;
    // exit(1);
}

static void showPending(sigset_t *pendings)
{
    for (int sig = 1; sig <= 31; sig++)
    {
        if (sigismember(pendings, sig))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}
// test
int main()
{
    sigset_t bsig, obsig;
    sigemptyset(&bsig);
    sigemptyset(&obsig);
    // sigfillset();
    for (int sig = 1; sig <= 31; sig++)
    {
        // 添加2号信号到信号屏蔽字中
        sigaddset(&bsig, sig);
    }
    // 设置用户记得信号屏蔽字到内核中,让当前进程屏蔽2号信号
    sigprocmask(SIG_SETMASK, &bsig, &obsig);
    signal(2, handler);

    // 1.不断获取当前进程的pending信号集
    sigset_t pendings;
    while (true)
    {
        // 清空信号集
        sigemptyset(&pendings);
        // 获取当前进程(谁调用,获取谁)的pending 信号集
        if (sigpending(&pendings) == 0)
        {
            // 打印一下当前进程的pending 信号集
            showPending(&pendings);
        }
        sleep(1);
    }
}

 解除信号屏蔽:

<Linux进程信号>——《Linux》

 <Linux进程信号>——《Linux》

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

void handler(int signo)
{
    cout << "这是一个进程,刚刚获取了一个信号:" << signo << endl;
    // exit(1);
}

static void showPending(sigset_t *pendings)
{
    for (int sig = 1; sig <= 31; sig++)
    {
        if (sigismember(pendings, sig))
        {
            cout << "1";
        }
        else
        {
            cout << "0";
        }
    }
    cout << endl;
}
// test
int main()
{
    cout<<"pid: "<<getpid()<<endl;
    sigset_t bsig, obsig;
    sigemptyset(&bsig);
    sigemptyset(&obsig);
    // sigfillset();
    for (int sig = 1; sig <= 31; sig++)
    {
        // 添加2号信号到信号屏蔽字中
        sigaddset(&bsig, sig);
        signal(sig, handler);
    }
    // 设置用户记得信号屏蔽字到内核中,让当前进程屏蔽2号信号
    sigprocmask(SIG_SETMASK, &bsig, &obsig);

    // 1.不断获取当前进程的pending信号集
    sigset_t pendings;
    int cnt = 0;
    while (true)
    {
        // 清空信号集
        sigemptyset(&pendings);
        // 获取当前进程(谁调用,获取谁)的pending 信号集
        if (sigpending(&pendings) == 0)
        {
            // 打印一下当前进程的pending 信号集
            showPending(&pendings);
        }
        sleep(2);
        cnt++;
        if (cnt == 20)
        {
            cout << "解除对所有信号的block..." << endl;
            sigprocmask(SIG_SETMASK, &obsig, nullptr);
        }
    }
}

四、捕捉信号

<Linux进程信号>——《Linux》

1. 内核如何实现信号的捕捉

如果信号的处理动作是用户自定义函数 , 在信号递达时就调用这个函数 , 这称为捕捉信号。由于信号处理函数的代码是在用户空间的, 处理过程比较复杂 , 举例如下 : 用户程序注册了 SIGQUIT 信号的处理函数 sighandler 。 当前正在执行main函数 , 这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的 main 函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复 main 函数的上下文继续执行 , 而是执行 sighandler 函 数 ,sighandler 和main 函数使用不同的堆栈空间 , 它们之间不存在调用和被调用的关系 , 是 两个独立的控制流程。 sighandler 函数返回后自动执行特殊的系统调用sigreturn 再次进入内核态。 如果没有新的信号要递达 , 这次再返回用户态就是恢复main函数的上下文继续执行了。
 
 内核态与用户态: <Linux进程信号>——《Linux》
自定义捕捉信号的处理过程:
<Linux进程信号>——《Linux》

 

 

2. 信号捕捉函数signal

sigaction

 
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
  • sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact出该信号原来的处理动作。actoact指向sigaction结构体:
  • 将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
当某个信号的处理函数被调用时 , 内核自动将当前信号加入进程的信号屏蔽字 , 当信号处理函数返回时自动恢复原来的信号屏蔽字, 这样就保证了在处理某个信号时 , 如果这种信号再次产生 , 那么 它会被阻塞到当前处理结束为止。 如果
在调用信号处理函数时 , 除了当前信号被自动屏蔽之外 , 还希望自动屏蔽另外一些信号 , 则用 sa_mask 字段说明这些需要额外屏蔽的信号, 当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flflags 字段包含一些选项 , 本章的代码都把sa_flflags 设为 0,sa_sigaction 是实时信号的处理函数 , 本章不详细解释这两个字段,有兴趣的同学可以在了解一下。 
<Linux进程信号>——《Linux》

 

 

<Linux进程信号>——《Linux》

 <Linux进程信号>——《Linux》

 <Linux进程信号>——《Linux》

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

using namespace std;

void handler(int signo)
{
    cout<<"获取到一个信号,信号的编号是: "<<signo<<endl;
}
int main()
{
    struct sigaction act,oact;
    act.sa_handler = handler;       //自定义方法
    //act.sa_handler = SIG_IGN;     //忽略信号
    //act.sa_handler = SIG_DFL;     //默认信号
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaction(2,&act,&oact);
    while(true)
    {
        sleep(1);
    }
    return 0;
}

 3. 可重入函数<Linux进程信号>——《Linux》

  • main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
  • 像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之, 如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。想一下,为什么两个不同的控制流程调用同一个函数,访问它的同一个局部变量或参数就不会造成错乱?
如果一个函数符合以下条件之一则是不可重入的 :
调用了 malloc free, 因为 malloc 也是用全局链表来管理堆的。
调用了标准 I/O 库函数。标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构。
<Linux进程信号>——《Linux》

<Linux进程信号>——《Linux》 

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

void handler(int signo)
{
    cout << "获取到一个信号,信号的编号是: " << signo << endl;
    // 增加handler信号的时间
    // sleep(20);
    // 或者
    sigset_t pending;
    while (true)
    {
        cout << "*" << endl;
        for (int i = 1; i <= 31; i++)
        {
            if (sigismember(&pending, i))

                cout << "1";
            else
                cout << "0";
        }
        cout << endl;
        sleep(1);
    }
}
int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler; // 自定义方法
    // act.sa_handler = SIG_IGN;     //忽略信号
    // act.sa_handler = SIG_DFL;     //默认信号
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaction(2, &act, &oact);
    while (true)
    {
        cout << "main running" << endl;
        sleep(1);
    }
    return 0;
}

 <Linux进程信号>——《Linux》

<Linux进程信号>——《Linux》 

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

void handler(int signo)
{
    cout << "获取到一个信号,信号的编号是: " << signo << endl;
    // 增加handler信号的时间
    // sleep(20);
    // 或者
    sigset_t pending;
    while (true)
    {
        //模拟永远处理2号信号
        cout<<"pid: "<<getpid()<<endl;
        cout << "*" << endl;
        for (int i = 1; i <= 31; i++)
        {
            if (sigismember(&pending, i))

                cout << "1";
            else
                cout << "0";
        }
        cout << endl;
        sleep(1);
    }
}
int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler; // 自定义方法
    // act.sa_handler = SIG_IGN;     //忽略信号
    // act.sa_handler = SIG_DFL;     //默认信号
    act.sa_flags = 0;
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask,3);   //在拦住2号信号的同时,也拦住3号信号,这就是设置sa_mask的意义
    sigaction(2, &act, &oact);
    while (true)
    {
        cout << "main running" << endl;
        sleep(1);
    }
    return 0;
}

 <Linux进程信号>——《Linux》

<Linux进程信号>——《Linux》 <Linux进程信号>——《Linux》

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

// 自定义实现信号处理
void Handler2()
{
    cout << "Test Signal 2" << endl;
}
void Handler3()
{
    cout << "Test Signal 3" << endl;
}
void Handler4()
{
    cout << "Test Signal 4" << endl;
}
void Handler5()
{
    cout << "Test Signal 5" << endl;
}
void Handler(int signo)
{
    cout<<"pid: "<<getpid()<<endl;
    switch (signo)
    {
    case 2:
        Handler2();
        break;
    case 3:
        Handler3();
        break;
    case 4:
        Handler4();
        break;
    case 5:
        Handler5();
        break;
    default:
        break;
    }
}

int main()
{
    signal(2, Handler);
    signal(3, Handler);
    signal(4, Handler);
    signal(5, Handler);
    
    while (1)
    {
        sleep(1);
    }
    
    return 0;
}

 volatile

该关键字在 C 当中我们已经有所涉猎,今天我们站在信号的角度重新理解一下 
[hb@localhost code_test]$ cat sig.c 
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{
 printf("chage flag 0 to 1\n");
 flag = 1;
}
int main()
{
 signal(2, handler);
 while(!flag);
 printf("process quit normal\n");
 return 0;
}
[hb@localhost code_test]$ cat Makefile 
sig:sig.c
 gcc -o sig sig.c #-O2
.PHONY:clean
clean:
 rm -f sig
[hb@localhost code_test]$ ./sig 
^Cchage flag 0 to 1
process quit normal
标准情况下,键入 CTRL - C ,2 号信号被捕捉,执行自定义动作,修改 flag 1 while 条件不满足 , 退出循 环,进程退出
[hb@localhost code_test]$ cat sig.c 
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{
 printf("chage flag 0 to 1\n");
 flag = 1;
}
int main()
{
 signal(2, handler);
 while(!flag);
 printf("process quit normal\n");
 return 0;
}
[hb@localhost code_test]$ cat Makefile 
sig:sig.c
 gcc -o sig sig.c -O2
.PHONY:clean
clean:
 rm -f sig
[hb@localhost code_test]$ ./sig 
^Cchage flag 0 to 1
^Cchage flag 0 to 1
^Cchage flag 0 to 1
优化情况下,键入 CTRL - C ,2 号信号被捕捉,执行自定义动作,修改 flag 1 ,但是 while 条件依旧满足 , 进程继续运行!但是很明显flflag 肯定已经被修改了,但是为何循环依旧执行?很明显, while 循环检查的 flflag ,并不是内存中最新的flflag ,这就存在了数据二异性的问题。 while 检测的 flflag 其实已经因为优化,被放在了CPU寄存器当中。如何解决呢?很明显需要 volatile
[hb@localhost code_test]$ cat sig.c 
#include <stdio.h>
#include <signal.h>
volatile int flag = 0;
void handler(int sig)
{
 printf("chage flag 0 to 1\n");
 flag = 1;
}
int main()
{
 signal(2, handler);
 while(!flag);
 printf("process quit normal\n");
 return 0;
}
[hb@localhost code_test]$ cat Makefile 
sig:sig.c
 gcc -o sig sig.c -O2
.PHONY:clean
clean:
 rm -f sig
 
[hb@localhost code_test]$ ./sig
^Cchage flag 0 to 1
process quit normal
  • volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作

<Linux进程信号>——《Linux》

#include <stdio.h>
#include <signal.h>

int flags = 0;
void handler(int signo)
{
    flags = 1;
    printf("更改flags:0——>1\n");
}
int main()
{
    signal(2, handler);
    while(!flags);
    printf("进程是正常退出的!\n");

    return 0;
}

编译器会进行优化。

对于while (!flags),当在没有修改时,进行检测,那么编译器就会进行优化。flags是全局变量,本来存储在内存,而while是逻辑运算(在CPU),编译器会优化,将flags的值优化到CPU的寄存器中,再次进行while循环检测时,就会在寄存器中读取,一旦有信号要求修改flags的值,那修改的是内存中的值,但编译器不一定知道,OS程序有多执行流,编译器只能检测语法,不能检测逻辑。所以,flags最终的值检测和程序逻辑造成不一样的结果。

这里,我们更改优化级别:—O2

<Linux进程信号>——《Linux》

#include <stdio.h>
#include <signal.h>

int flags = 0;
void handler(int signo)
{
    flags = 1;
    printf("更改flags:0——>1\n");
}
int main()
{
    signal(2, handler);
    while(!flags);
    printf("进程是正常退出的!\n");

    return 0;
}

如何解决?

告诉编译器,不准对flags做任何优化,每次CPU计算的时候,需要从内存中获取数据!

这就是保持内存的可见性!<Linux进程信号>——《Linux》

 

SIGCHLD 信号  
进程一章讲过用 wait waitpid 函数清理僵尸进程 , 父进程可以阻塞等待子进程结束 , 也可以非阻 塞地查询是否有子进程结束等待清理( 也就是轮询的方式 ) 。采用第一种方式 , 父进程阻塞了就不 能处理自己的工作了 ; 采用第二种方式 , 父进程在处理自己的工作的同时还要记得时不时地轮询一 下, 程序实现复杂。
其实 , 子进程在终止时会给父进程发 SIGCHLD 信号 , 该信号的默认处理动作是忽略 , 父进程可以自 定义 SIGCHLD 信号的处理函数, 这样父进程只需专心处理自己的工作 , 不必关心子进程了 , 子进程 终止时会通知父进程 , 父进程在信号处理函数中调用wait 清理子进程即可。
请编写一个程序完成以下功能 : 父进程 fork 出子进程 , 子进程调用 exit(2) 终止 , 父进程自定 义 SIGCHLD 信号的处理函数 , 在其中调用wait 获得子进程的退出状态并打印。
事实上 , 由于 UNIX 的历史原因 , 要想不产生僵尸进程还有另外一种办法 : 父进程调 用 sigaction SIGCHLD 的处理动作置为SIG_IGN, 这样 fork 出来的子进程在终止时会自动清理掉 , 不 会产生僵尸进程 , 也不会通知父进程。系统默认的忽略动作和用户用sigaction 函数自定义的忽略 通常是没有区别的 , 但这是一个特例。此方法对于 Linux 可用 , 但不保证在其它UNIX 系统上都可 用。请编写程序验证这样做不会产生僵尸进程。
测试代码:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
 pid_t id;
 while( (id = waitpid(-1, NULL, WNOHANG)) > 0){
 printf("wait child success: %d\n", id);
 }
 printf("child is quit! %d\n", getpid());
}
int main()
{
 signal(SIGCHLD, handler);
 pid_t cid;
 if((cid = fork()) == 0){//child
 printf("child : %d\n", getpid());
 sleep(3);
 exit(1);
 }
 while(1){
 printf("father proc is doing some thing!\n");
 sleep(1);
}
 return 0;
}

<Linux进程信号>——《Linux》

<Linux进程信号>——《Linux》 

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

void handler(int signo)
{
    cout << "子进程退出了,父进程收到退出信号:" << signo << " 我是:" << getpid() << endl;
}
int main()
{
    signal(SIGCHLD, handler);
    pid_t id = fork();
    if (id == 0)
    {
        while (true)
        {
            cout << "我是子进程: " << getpid() << endl;
            sleep(1);
        }
        exit(0);
    }
    // 父进程
    while (true)
    {
        cout << "我是父进程: " << getpid() << endl;
        sleep(1);
    }
}

 <Linux进程信号>——《Linux》

 

后记:
●由于作者水平有限,文章难免存在谬误之处,敬请读者斧正,俚语成篇,恳望指教!

                                                                           ——By 作者:新晓·故知