【Linux】Linux进程信号(上)
一、信号快速认识
1.1 生活角度的信号
- 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”。
- 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
- 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”,当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)。
- 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话。
基本结论:
- 你怎么能识别信号呢?识别信号是内置的,进程识别信号,是内核程序员写的内置特性。
- 信号产生后,你知道怎么处理吗?知道。如果信号没有产生,你知道怎么处理吗?知道。所以,信号的处理方法,在信号产生之前,就已经准备好了。
- 处理信号,立即处理吗?可能在做优先级更高的的事情,不会处理。什么时候处理?合适的时候。
- 信号到来、信号保存、信号处理
- 怎么进行信号处理呢?1. 默认;2. 忽略;3. 自定义,后续都叫做信号捕捉。
1.2 技术角度的信号
1.2.1 一个样例
#include <iostream>
#include <unistd.h>int main()
{while(true) {std::cout << "I am a process, I am waiting a signal!" << std::endl;sleep(1);}return 0;
}
- 用户输入命令,在Shell下执行一个前台进程。
- 用户按下ctrl + c,这个命令让键盘产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程。
- 前台进程因为收到信号,进而引起进程退出。
1.2.2 一个系统函数
NAME
signal - ANSI C signal handling
SYNOPSIS
#include <signal.h>typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数说明:
- signum:信号编号(后面解释,知道是数字即可)。
- handler:函数指针,表示更改信号的处理动作,当收到对应的信号,就回调执行handler方法。
其实,ctrl + c本质是向前台进程发送SIGINT即2信号,我们验证一下,这里要引用上面介绍的系统函数。
#include <iostream>
#include <unistd.h>
#include <signal.h>void handler(int number) {std::cout << "I am " << getpid() << ", I get a signal: " << number << std::endl;
}int main()
{std::cout << "I am " << getpid() << std::endl;signal(SIGINT, handler);while(true) {std::cout << "I am a process, I am waiting a signal!" << std::endl;sleep(1);}return 0;
}
注意:
- 要注意的是,signal函数仅仅只是设置了特定信号的捕捉行为处理方式,并不是直接调用处理动作。如果后续特定信号没有产生,设置的捕捉函数永远也不会被调用。
- ctrl + c 的信号只能发给前台进程。一个命令后面加个&可以放到后台进行,这样Shell不用等待进程结束就可以接受新的命令,启动新进程。
- Shell可以同时运行一个前台进程和多个后台进程,只有前台进程才能得到像ctrl + c这种控制键产生的信号。
- 前台进程在运行过程中用户随时可能按下ctrl + c产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步的。
1.3 信号概念
信号是进程间事件异步通知的一种方式,属于软中断。
1.3.1 查看信号
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在 signal.h 中找到,例如其中有定义#define SIGINT 2
编号34以上的是实时信号,这里只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中有详细说明:man 7 signal
1.3.2 信号处理
可选的处理动作有以下三种:
- 忽略此信号
#include <iostream>
#include <unistd.h>
#include <signal.h>void handler(int number) {std::cout << "I am " << getpid() << ", I get a signal: " << number << std::endl;
}int main()
{std::cout << "I am " << getpid() << std::endl;signal(SIGINT, SIG_IGN); // 设置忽略信号的宏while(true) {std::cout << "I am a process, I am waiting a signal!" << std::endl;sleep(1);}return 0;
}
- 执行该信号的默认处理动作
#include <iostream>
#include <unistd.h>
#include <signal.h>void handler(int number) {std::cout << "I am " << getpid() << ", I get a signal: " << number << std::endl;
}int main()
{std::cout << "I am " << getpid() << std::endl;signal(SIGINT, SIG_DFL); // 执行信号默认处理动作while(true) {std::cout << "I am a process, I am waiting a signal!" << std::endl;sleep(1);}return 0;
}
- 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这个方式称为自定义捕捉一个信号
#include <iostream>
#include <unistd.h>
#include <signal.h>void handler(int number) {std::cout << "I am " << getpid() << ", I get a signal: " << number << std::endl;
}int main()
{std::cout << "I am " << getpid() << std::endl;signal(SIGINT, handler);while(true) {std::cout << "I am a process, I am waiting a signal!" << std::endl;sleep(1);}return 0;
}
二、产生信号
2.1 通过终端按键产生信号
2.1.1 基本操作
- ctrl + c(SIGINT) 上面已经验证过了,这里不再重复
- ctrl + \(SIGQUIT) 可以发送终止信号并生成core dump文件,用于事后调试(后面详谈)
#include <iostream>
#include <unistd.h>
#include <signal.h>void handler(int number) {std::cout << "I am " << getpid() << ", I get a signal: " << number << std::endl;
}int main()
{std::cout << "I am " << getpid() << std::endl;signal(SIGQUIT, handler);while(true) {std::cout << "I am a process, I am waiting a signal!" << std::endl;sleep(1);}return 0;
}
- ctrl + z (SIGTSTP) 可以发送停止信号,将当前前台进程挂起到后台等。
#include <iostream>
#include <unistd.h>
#include <signal.h>void handler(int number) {std::cout << "I am " << getpid() << ", I get a signal: " << number << std::endl;
}int main()
{std::cout << "I am " << getpid() << std::endl;signal(SIGTSTP, handler); // 20while(true) {std::cout << "I am a process, I am waiting a signal!" << std::endl;sleep(1);}return 0;
}
2.1.2 理解OS如何得知键盘有数据
2.1.3 初步理解信号起源
注意:
- 信号其实是从纯软件角度,模拟硬件中断的行为。
- 只不过硬件中断是发给CPU,而信号是发给进程。
- 两者有相似性,但是层级不同。
2.2 调用系统命令向进程发信号
#include <iostream>
#include <unistd.h>
#include <signal.h>int main()
{while(true) {sleep(1);}return 0;
}
首先在后台执行死循环的程序,然后用 kill 命令给它发送SIGSEGV信号
- 7243 是 test 进程的 pid。之所以要再次回车才显示 Segmentation fault,是因为在 7243 进程终止掉之前已经回到了Shell提示符等待用户输入下一条命令,Shell不希望用户 Segmentation fault 信息和用户的输入交错在一起,所以等到用户输入之后才显示。
- 指定发送某种信号的 kill 命令可以有多种写法,上面的命令还可以写成 kill -11 7243,11 是 SIGSEGV 的编号。以往遇到的段错误都是由非法内存访问而产生的,而这个程序本身没错,给它发送 SIGSEGV 信号也能产生段错误。
2.3 使用函数产生信号
2.3.1 kill
kill 命令是调用 kill 函数实现的。kill 函数可以给一个指定的进程发送一个指定的信号。
NAME
kill - send signal to a process
SYNOPSIS
#include <signal.h>int kill(pid_t pid, int sig);
RETURAN VALUE
On success (at least one signal was sent), zero is returned. On error, -1 is returned, and errno is set to indicate the error.
参数说明:
- pid:进程号
- sig:信号的编号
实现自己的 kill 命令:
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>// ./mykill -sig pid
int main(int argc, char* argv[])
{if(argc != 3) {std::cout << "Usage: " << argv[0] << "-signumber pid" << std::endl;return 1;}int sig = std::stoi(argv[1]+1); // 去掉 -int pid = std::stoi(argv[2]);int n = kill(pid, sig);if(n < 0) {perror("kill");return 2;}return 0;
}
2.3.2 raise
NAME
raise - send a signal to the caller
SYNOPSIS
#include <signal.h>int raise(int sig);
RETURN VALUE
raise() returns 0 on success, and nonzero for failure.
#include <iostream>
#include <signal.h>
#include <unistd.h>void handler(int signumber)
{// 整个代码就只有这一处打印std::cout << "获取了⼀个信号: " << signumber << std::endl;
}// mykill -signumber pid
int main()
{signal(2, handler); // 先对2号信号进行捕捉// 每隔1S,自己给自己发送2号信号while (true){sleep(1);raise(2);}return 0;
}
2.3.3 abort
NAME
abort - cause abnormal process terminationSYNOPSIS
#include <stdlib.h>[[noreturn]] void abort(void);
RETURN VALUE
The abort() function never returns.// 就像 exit() 函数一样,abort函数总是成功的,所以没有返回值
#include <iostream>
#include <signal.h>
#include <unistd.h>void handler(int signumber)
{// 整个代码就只有这一处打印std::cout << "获取了⼀个信号: " << signumber << std::endl;
}// mykill -signumber pid
int main()
{signal(SIGABRT, handler); // 每隔1S,自己给自己发送 abort 信号while (true){sleep(1);abort();}return 0;
}
我们能看到,abort给自己返送固定的6号信号,虽然捕捉了,但还是退出了。
2.4 由软件条件产生信号
SIGPIPE 是一种由软件条件产生的信号,在 “管道” 中已经介绍过了。这里主要介绍 alarm 函数和 SIGALRM 信号。
NAME
alarm - set an alarm clock for delivery of a signalSYNOPSIS
#include <unistd.h>unsigned int alarm(unsigned int seconds);
RETURN VALUE
alarm() returns the number of seconds remaining until any previously scheduled alarm was due to be delivered, or zero if there was no previously scheduled alarm.
- 调用 alarm 函数可以设置一个闹钟,也就是告诉内核在 seconds 秒后向当前进程发起 SIGALRM 信号,该信号的默认处理动作是终止进程。
- 这个函数的返回值是0或者是以前设定的闹钟时间还剩的秒数。打个比方,某人要睡一会,设了一个30分钟的闹钟,20分钟时被人吵醒了,还行多睡会,于是重新设定闹钟为15分钟后响,“以前设定的闹钟还余下的时间”就是10分钟。如果seconds为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟还余下的秒数。
2.4.1 基本alarm验证 - 体会IO效率问题
- 程序的作用是一秒钟之内不断地数数,1秒钟到了就被SIGALRM信号终止。
- 必要的时候对SIGALRM信号进行捕捉。
#include <iostream>
#include <unistd.h>// IO多
int main()
{int count = 0;alarm(1);while(true) {std::cout << "count: " << count << std::endl;count++;}return 0;
}
#include <iostream>
#include <unistd.h>
#include <signal.h>int count = 0;void handler(int number) {std::cout << "count: " << count << std::endl;exit(number);
}int main()
{signal(SIGALRM, handler);alarm(1);while(true) {count++;}return 0;
}
结论:
- 脑中会响一次,默认终止进程。
- 有IO效率低。
2.4.2 设置重复闹钟
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <functional>
#include <vector>using func_t = std::function<void()>;std::vector<func_t> func;
int count = 0;// 把 信号 更换成 硬件中断
void handler(int number) {for(auto& f : func) {f();}std::cout << "count: " << count << std::endl;int n = alarm(1);std::cout << "剩余时间: " << n << std::endl;
}int main() {func.push_back([](){std::cout << "I am " << getpid() << ", ";});func.push_back([](){std::cout << "hello world!" << std::endl;});signal(SIGALRM, handler);alarm(1); // 一次性闹钟,超时会自动被取消while(1) {pause(); // 等待一个信号std::cout << "我醒来了..." << std::endl;count++;}return 0;
}
结论:
- 闹钟设置一次,起效一次
- 可以重复设置闹钟
- 感兴趣可以尝试使用alarm(0),取消以前的闹钟
2.4.3 如何理解软件条件
在操作系统中,信号的软件条件指的是软件内部状态或者特定软件操作触发的信号产生机制。这些条件包括但不限定于计时器超时(如alarm函数设定的时间到达)、软件异常(如向已关闭的管道写数据产生的SIGPIPE信号)等。当这些软件条件满足时,操作系统会向相关进程发送相应的信号,已通知进程进行相应的处理。简而言之,软件条件是因操作系统内部或外部软件操作而触发的信号产生。
2.4.4 如何简单快速理解系统闹钟
系统闹钟,其实本质是OS本身必须具有计时功能,并能让用户设置这种定时功能,才可能实现闹钟这样的技术。
现代Linux是提供了定时功能的,定时器也要被管理:先描述,再组织。内核中的定时器数据结构是:
struct timer_list
{struct list_head entry;unsigned long expires;void (*function)(unsigned long);unsigned long data;struct tvec_t_base_s *base;
};
我们可以看到:定时器超时时间 expires 和处理方法 function。
操作系统管理计时器,采用的是时间轮的做法,但是我们为了简单理解,可以再组织成为“堆结构”。
2.5 硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU运算单位会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
2.5.1 模拟除0
#include <iostream>
#include <signal.h>void handler(int number) {std::cout << "catch a sig: " << number << std::endl;
}int main()
{signal(SIGFPE, handler);int a = 9;a /= 0;return 0;
}
2.5.2 模拟野指针
#include <iostream>
#include <signal.h>void handler(int number) {std::cout << "catch a sig: " << number << std::endl;
}int main()
{signal(SIGSEGV, handler);int* p = nullptr;*p = 10;return 0;
}
由此可以确认,我们在C/C++当中除以0,内存越界等情况,在系统层面上,是被当成信号处理的。
注意:
通过上面的实验,我们可以发现:
发现一直有8号信号被我们捕获,这是为什么呢?上面我们只提到CPU运算异常后,如何处理后续的流程,实际上OS会检查应用程序的异常情况,其实在CPU中有一些控制和状态寄存器,主要用于控制处理器的操作,通常由操作系统代码使用。状态寄存器可以简单理解为一个位图,对应着一些状态标记位、溢出标记位。OS会检测是否存在异常状态,有异常存在就会调用对应的异常处理方法。
除零异常后,我们并没有清理内存,关闭进程打开的文件,切换进程等操作,所以CPU中还保留上下文数据以及寄存器内容,除零异常会一直存在,就有了我们看到的一直发送异常信号的现象。访问非法内存其实也是如此。
2.5.3 子进程退出core dump
#include <iostream>
#include <string>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/wait.h>int main()
{if (fork() == 0){sleep(1);int a = 10;a /= 0;exit(0);}int status = 0;waitpid(-1, &status, 0);printf("exit signal: %d, core dump: %d\n", status & 0x7F, (status >> 7) & 1);return 0;
}
# man 7 signal
Signal Standard Action Comment
────────────────────────────────────────────────────────────────────────
SIGABRT P1990 Core Abort signal from abort(3)
SIGALRM P1990 Term Timer signal from alarm(2)
SIGBUS P2001 Core Bus error (bad memory access)
SIGCHLD P1990 Ign Child stopped or terminated
SIGCLD - Ign A synonym for SIGCHLD
SIGCONT P1990 Cont Continue if stopped
SIGEMT - Term Emulator trap
SIGFPE P1990 Core Floating-point exception
SIGHUP P1990 Term Hangup detected on controlling terminal
or death of controlling process
SIGILL P1990 Core Illegal Instruction
SIGINFO - A synonym for SIGPWR
SIGINT P1990 Term Interrupt from keyboard
SIGIO - Term I/O now possible (4.2BSD)
SIGIOT - Core IOT trap. A synonym for SIGABRT
SIGKILL P1990 Term Kill signal
SIGLOST - Term File lock lost (unused)
SIGPIPE P1990 Term Broken pipe: write to pipe with no
readers; see pipe(7)
SIGPOLL P2001 Term Pollable event (Sys V);
synonym for SIGIO
# ulimit -a
real-time non-blocking time (microseconds, -R) unlimited
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
file size (blocks, -f) unlimited
pending signals (-i) 6455
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) 6455
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
2.5.4 Core Dump
- SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且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:
然后写一个死循环程序:
#include <iostream>
#include <unistd.h>int main()
{std::cout << "I am " << getpid() << std::endl;while(1);return 0;
}
ulimit命令改变了Shell进程的 Resource Limit,test进程的PCB由Shell进程复制而来,所以也具有和Shell进程相同的 Resource Limit,这样就可以产生Core Dump了。
三、保存信号
3.1 信号其他相关常见概念
- 实际执行信号的处理动作称为信号递达(Delivery)。
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞(Block)某个信号。
- 被阻塞的进程产生时将保持未决状态,知道进程解除对此信号的阻塞,才执行递达动作。
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是递达之后一种可选的处理动作。
3.2 在内核中的表示
信号在内核中表示示意图:
- 每个信号都有两个标志位,分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,知道信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞,也未产生过,当它递达时执行默认处理动作。
- SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有接触阻塞之前,不能忽略这个信号,因为进程仍有机会在改变处理动作之后解除阻塞。
- SIGQUIT信号未产生过,一旦产生SIGQUIT信号将会被阻塞,它的处理动作是用户自定义函数sighandler。
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以一次放在一个队列里,这里不了解实时信号。
// 内核结构 2.6.18
struct task_struct
{.../* signal handlers */struct sighand_struct *sighand;sigset_t blocked;struct sigpending pending;...
};struct sighand_struct
{atomic_t count;struct k_sigaction action[_NSIG]; // #define _NSIG 64spinlock_t siglock;
};struct __new_sigaction
{__sighandler_t sa_handler;unsigned long sa_flags;void (*sa_restorer)(void); /* Not used by Linux/SPARC */__new_sigset_t sa_mask;
};struct k_sigaction
{struct __new_sigaction sa;void __user *ka_restorer;
};/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);struct sigpending
{struct list_head list;sigset_t signal;
};
3.3 sigset_t
从上图来看,每个信号只有一个 bit 的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以使用相同的数据类型 sigset_t 来存储,sigset_t 称为信号集,这个类型可以表示每个信号的 “有效” 或 “无效” 状态,在阻塞信号集中 “有效” 和 “无效” 的含义是该信号是否被阻塞,而在未决信号集中 “有效” 和 “无效” 的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的屏蔽应该理解为阻塞而不是忽略。
类似权限里的umask。
3.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清零,表示该信号集不包含任何有效信号。
- 函数 sigfillset 初始化set所指向的信号集,使其中所有信号对应的bit置位,表示该信号集的有效信号包括系统支持所有信号。
- 函数 sigaddset 在set所指向的信号集中添加signo信号。
- 函数 sigdelset 在set所指向的信号集中删除signo信号。
- 函数 sigismember 判断 signo 信号是否在 set 信号集中被标记。
- 注意:在使用 sigset_t 类型变量之前,一定要调用 sigemptyset 或 sigfillset 函数进行初始化,使信号集处于确定状态。初始化 sigset_t 变量之后就可以调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种信号。
3.4.1 sigprocmask
调用函数 sigprocmask 可以读取或更改信号屏蔽字(阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
参数说明:
- how:指定信号屏蔽字的修改方式,取以下值之一:
SIG_BLOCK 将 set 中的信号添加到当前信号屏蔽字中(阻塞这些信号) SIG_UNBLOCK 从当前信号屏蔽字中移除 set 中的信号(解除阻塞) SIG_SETMASK 直接将当前信号屏蔽字设置为 set(覆盖原有屏蔽字)
- set:指向 sigset_t 类型的指针,表示需要操作的信号集合。如果为NULL,则忽略此参数(仅通过 how 和 oset 获取或修改屏蔽字)。
- oset:指向 sigset_t 类型的指针,用于保存修改前的信号屏蔽字。如果为NULL,则不保存旧值。
返回值:成功返回0,失败返回-1并设置errno。
如果调用了 sigprocmask 解除了对若干个未决信号的阻塞状态,则在 sigprocmask 返回前,至少将其中一个信号递达。
3.4.2 sigpending
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。
调⽤成功则返回0,出错则返回-1
我们使用上面新学的几个函数做个小实验:
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>void PrintPending(sigset_t& pending) {std::cout << "current process[" << getpid() << "]pending: ";for(int signo = 31; signo >= 1; signo--) {if(sigismember(&pending, signo)) std::cout << "1";else std::cout << "0";}std::cout << std::endl;
}void handler(int number) {std::cout << "-----------------------------------------" << std::endl;sigset_t pending;sigpending(&pending);PrintPending(pending);std::cout << "-----------------------------------------" << std::endl;
}int main()
{// 捕捉2号信号,自定义捕捉signal(2, handler);// 屏蔽2号信号sigset_t block_set, old_set;sigemptyset(&block_set);sigemptyset(&old_set);sigaddset(&block_set, 2); // 目前还没有修改当前进行的内核block表// 设置进进程的block表中sigprocmask(SIG_BLOCK, &block_set, &old_set); // 真正修改当前进行的内核block表,完成了对2号信号的屏蔽int cnt = 9;while(cnt--) {// 获取当前进程的pending集sigset_t pending;sigpending(&pending);// 打印pending信号集PrintPending(pending);// 解除对2号信号的屏蔽if(cnt == 3) {std::cout << "解除2号信号的屏蔽!!!" << std::endl;sigprocmask(SIG_SETMASK, &old_set, &block_set);}sleep(1);}return 0;
}