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

news2024/11/19 5:35:59
本节重点:
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)的。

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

 任务管理。

4. 信号概念

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

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

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

6. 信号处理常见方式

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

man signal

 

#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;
}

 

#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信号。

#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

#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);
    }
}

 

  • 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。
abort函数使当前进程接收到信号而异常终止。

#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值。
#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
#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信号终止。

#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++);
    }
}

 

#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向进程发送信号?能否描述一下完整的发送处理过程?

 进程等待部分回顾:

 

#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);
}

 

 ulimit -c 可以设置。

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

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

三、阻塞信号

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

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

2. 在内核中的表示

信号在内核中的表示示意图

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

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

sigpending

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

程序运行时 , 每秒钟把各信号的未决状态打印一遍 , 由于我们阻塞了 SIGINT 信号 , Ctrl-C 将会 使 SIGINT 信号处于未决状态, Ctrl-\ 仍然可以终止程序,因为SIGQUIT信号没有阻塞。
#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);
    }
}

 

 

#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);
    }
}

 解除信号屏蔽:

 

#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);
        }
    }
}

四、捕捉信号

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

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

 

 

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 是实时信号的处理函数 , 本章不详细解释这两个字段,有兴趣的同学可以在了解一下。 

 

 

 

 

#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. 可重入函数

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

 

#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;
}

 

 

#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;
}

 

 

#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 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作

#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

#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计算的时候,需要从内存中获取数据!

这就是保持内存的可见性!

 

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;
}

 

#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);
    }
}

 

 

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

                                                                           ——By 作者:新晓·故知

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/106973.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

面试官:Docker 有几种网络模式?5 年工作经验都表示答不上来。。

docker容器网络 Docker在安装后自动提供3种网络&#xff0c;可以使用docker network ls命令查看 [rootlocalhost ~]# docker network ls NETWORK ID NAME DRIVER SCOPE cd97bb997b84 bridge bridge l…

第1章 概述

第一章 概述 考试范围&#xff1a; 1.1-1.10 考试内容&#xff1a; 章节后的Review Terms&#xff08;名词基本都在课文中&#xff09; 考试题型&#xff1a; 综合题 Review Terms Database-management system (DBMS) &#xff1a;A collection of interrelated data and a …

信息检索 Information Retrieval

信息检索主要是查找与用户查询相关的文档。 给定&#xff1a;大型静态文档集合 和信息需求&#xff08;基于关键字的查询&#xff09; 任务&#xff1a;查找所有且仅与查询相关的文档 典型的 IR 系统&#xff1a; • 搜索一组摘要 • 搜索报纸文章 • 图书馆搜索 • 搜索网络 …

毕业后,我已经离开机械行业转行码农一年多了......

背景 鄙人本科毕业两年有余&#xff0c;机械工程专业&#xff0c;我已经离开机械行业转行码农一年多了。 如果有正在学习的&#xff0c;退学还是千万不要&#xff0c;不过能换专业就换专业&#xff0c;不能换就往机电一体化靠&#xff0c;加上自学编程&#xff0c;以后做嵌入…

计算机毕设Python+Vue野生动物保护资讯管理系统(程序+LW+部署)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

STM32的ST-link调试下载

调试原理 STM32F-10X使用M3内核&#xff0c;该内核支持复杂的同i傲视操作&#xff0c;硬件调试模块允许在取指令&#xff08;指令单步运行&#xff09;或访问数据&#xff08;数据断电时&#xff09;使得内核停止。在内核停止时&#xff0c;内核状态都可被查询&#xff0c;完成…

范登堡(van den berg)CPT使用记录

前段时间的CPT外业所使用的设备是范登堡的井下式或者说交互式的静力触探仪&#xff08;CPT&#xff09;&#xff0c;型号是WISON-APB&#xff0c;下面是官网提供的照片。根据官网的介绍&#xff0c;它的探测工具分为三种&#xff0c;分别50KN&#xff08;3m&#xff09;、100KN…

KVM部署操作-尚文网络xUP楠哥

~~全文共1250字&#xff0c;阅读需约5分钟。 进Q群11372462&#xff0c;领取专属报名福利! # 安装KVM先决条件 KVM 需要有 CPU 的支持&#xff08;Intel VT 或 AMD SVM&#xff09;&#xff0c;在安装 KVM 之前检查一下 CPU 是否提供了虚拟技术的支持。 基于 Intel 处理器的…

Qt之使用CQU库快速开发统一风格界面

在使用Qt开发时&#xff0c;肯定是想让开发的项目界面统一风格&#xff1b;不希望每个界面都要程序员用代码去修饰美化以及进行事件处理等等&#xff0c;这样非常繁琐&#xff0c;容易出错而且没有格调&#xff1b;所以我就开发一个动态链接库&#xff0c;封装统一的风格界面、…

尚医通-前端Vue学习(九)

&#xff08;1&#xff09;vscode的安装及使用 &#xff08;2&#xff09;前端知识-ES6语法知识点 &#xff08;3&#xff09;Vue-入门 &#xff08;4&#xff09; Vue的生命周期 &#xff08;5&#xff09;Vue-Axios的使用 &#xff08;6&#xff09;ElemmentUI介绍 &…

Java项目:SpringBoot课程在线学习系统

作者主页&#xff1a;源码空间站2022 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文末获取源码 项目介绍 采用SpringBootSpringMybatisThyeleaf实现的在线学习系统&#xff0c;一共2个角色&#xff1a;管理员与学生。 管理员角色功能&#xff1a; 登录…

Unreal Engine中的Actor:理解、生成与消亡

目录 谈谈Actor 生成&#xff08;实例化&#xff09;Acotr StaticClass/UClass&#xff1a;类模板/类的快照 Actor的生命周期 Actor的消亡 Destroy&#xff1a;标记删除 SetLifeSpan&#xff1a;设置存活时间 Destroyed&#xff1a;消亡Actor的通知 EndPlay&#xff1…

chatGPT对接微信

代码地址: https://github.com/jeffcail/go-wecaht-bot 喜欢的帅锅美女可以点个star哦&#x1f60a; 背景 玩一玩chatGPT。只因GPT你太美&#xff5e;&#xff5e;&#xff5e; wechat SDK go get github.com/eatmoreapple/openwechat实现功能 自动通过好友申请用户私聊回…

我的转行之路

我一直想为自己没有继续从事编程&#xff0c;而转到铁路行业找一个合理的理由&#xff0c;来掩饰我的愚蠢。我日思夜想终于找到了更合理更有说服力的理由&#xff0c;十个字&#xff0c;目光的短浅&#xff0c;认知的缺乏。 那个时候&#xff0c;只被一毕业就能拿到不错的工资…

Python还有发展前景吗?现在该怎么去学习?

从2008年开始Python就突然火了起来&#xff0c;腾讯最近组织了一个大型Python技术交流峰会&#xff0c;由此可见Python现在已经到了我们不能想象的境界&#xff0c;一切的步伐都要跟着大公司走&#xff0c;腾讯这样级别的公司都这样重视Python技术&#xff0c;可见Python的发展…

什么样的人适合当黑客?

别说我没资格说这些话或是我的话是废话(对你来说是,对别人呢?),我想任何人都有资格尽自己的能力帮助别人. 首先,看到这篇文章,觉得好长,好烦,懒的看,走人… OK,我觉的你不适合做黑客&#xff01; 07年我考上大学&#xff0c;选择了机械制造及其自动化&#xff08;数控编程&a…

【linux kernel】linux内核裁剪随想

为什么需要裁剪&#xff1f; 首先&#xff0c;裁剪并不一定是缩小内核镜像的大小&#xff0c;而是移植和适配。内核支持的特性很多&#xff0c;站在项目的角度&#xff0c;不一定全部需要这些特性和功能。这时候就需要将不需要的特性和功能剔除&#xff0c;随着这个过程的进行…

LabVIEW在两台计算机之间传输数据

LabVIEW在两台计算机之间传输数据 有几种网络协议可用于完成此任务。使用正确的网络协议白皮书将完成为应用选择正确协议的任务。它涵盖了控制和监视应用中最常用的通信模型&#xff0c;并根据配置、性能、易用性等推荐最适合每种情况的网络协议。 参考的白皮书重点介绍了三种…

【MAX7800与ESP8266mcu通讯关键字控制】

【MAX7800与ESP8266mcu通讯关键字控制】 1. 前言2. 实验条件2.1 硬件条件2.2 软件条件3. 程序编写3.1 ESP8266程序解剖3.2 MAX7800程序解剖4. 实验效果4.1 esp8266打印如下4.2 max7800打印如下5. 小结1. 前言 前期搭好MAX7800 的eclipse和ESP82666的Arduino开发环境,现在开始…

98.第十九章 MySQL数据库 -- MySQL数据库架构、存储引擎、服务器配置和状态(八)

4.MySQL架构和性能优化 MySQL是C/S 架构的,connectors是连接器;可供Native C API、JDBC、ODBC、NET、PHP、Perl、 Python、Ruby、Cobol等连接mysql;ODBC叫开放数据库(系统)互联,open database connection; JDBC是主要用于java语言利用较为底层的驱动连接数据库;以上这…