信号的捕捉
该图为基于信号处理为用户自定义动作的图解;
-
信号的捕捉
当一个信号被递达时,如果该信号的处理动作是用户自定义的函数(如
int sighandler(int)
)时就会调用这个函数,该步骤被称为捕捉信号; -
用户程序注册新号处理函数
用户程序中注册了
SIGQUIT
信号的处理函数sighandler
; -
切换到内核态
当前正在执行
main
函数时发生了中断或是异常,这导致程序从用户态切换到内核态(如图中标注的1
和2
); -
内核态处理信号
内核在处理完中断或异常之后,在返回用户态的
main
函数前检测到有信号SIGQUIT
递达(图中标注2
); -
独立控制流程
sighandler
和main
函数使用不同的堆栈空间,之间没有调用和被调用的关系,是两个独立的控制流程(图中标注4
); -
返回用户态
sighandler
函数返回后通过执行的特殊的系统调用sys_sigreturn
再次进入内核(图中标注4
); -
恢复主流程
如果没有新的信号被注册,这次返回用户态时将恢复
main
函数中断前的上下文继续执行(图中标注5
);
sigaction函数
sigaction()
函数与signal()
函数相同,功能都为捕捉信号;
NAME
sigaction - examine and change a signal action
SYNOPSIS
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
sigaction(): _POSIX_C_SOURCE >= 1 || _XOPEN_SOURCE || _POSIX_SOURCE
siginfo_t: _POSIX_C_SOURCE >= 199309L
RETURN VALUE
sigaction() returns 0 on success; on error, -1 is returned, and errno is set to indicate the error.
该函数的具体功能用于检查和改变指定型号的处理方式;
它允许程序定义或更改信号处理函数,也可以获取当前信号处理函数的信息;
函数调用成功时返回0
,调用失败时返回-1
并设置errno
;
参数如下:
-
int signum
表示传入一个
int
类型的参数,该参数表明需要传入的信号编号; -
const struct sigaction *act
传入一个
struct sigaction *
参数,其中加了const
作修饰表示是一个传入型参数;该参数用于指定新的信号处理动作,其结构体包含以下成员:
struct sigaction { void (*sa_handler)(int); // 信号处理函数指针 void (*sa_sigaction)(int, siginfo_t *, void *); // 另一个信号处理函数指针,用于接收额外信息 sigset_t sa_mask; // 在信号处理期间需要阻塞的信号集 int sa_flags; // 影响信号处理行为的标志位 void (*sa_restorer)(void); // 不常用的字段,一般设置为NULL };
在处理普通信号时只需要关注
void (*sa_handler)(int)
成员与sigset_t sa_masksigset_t sa_mask
成员即可;-
void (*sa_handler)(int)
该成员为一个函数指针类型,用于指向信号处理函数,如
SIG_IGN
,SIG_DFL
或是用户自定义函数void userhandler(int)
; -
sigset_t sa_mask
该成员为一个
sigset_t
类型的数据,其中sigset_t
类型为操作系统封装的一个位图结构用于依靠该结构对信号集进行操作;当一个进程在处理一个信号的时候会将对应的信号添加到其信号屏蔽字(阻塞信号集)中以避免信号方法重复调用;
设置该成员可以使得进程在处理一个信号时同时将多个信号添加至信号屏蔽字中;
其余成员可默认设为
0
; -
-
struct sigaction *oldact
该参数类型与
act
类型相同,不被const
修饰为一个输出型参数;该参数用于保存之前的信号处理动作,如果不需要保存旧的信号处理则传递
nullptr
;
void sighandler(int signo) {
// 自定义处理动作
printf("sighandler get a signal: %d\n", signo);
}
int main() {
struct sigaction act, oldact;
memset(&act, 0, sizeof(act)); // 利用memset()初始化结构体
memset(&act, 0, sizeof(oldact));
act.sa_handler = sighandler; // 为成员赋值 设置自定义处理动作
int n = sigaction(SIGINT, &act, &oldact); // 函数调用
while (!n) { // 循环打印
cout << "I am a Process ,id : " << getpid() << endl;
sleep(1);
}
return 0;
}
该例子未使用可以同时阻塞多个信号的特性;
运行结果为:
$ ./mysignal
I am a Process ,id : 29791
^Csighandler get a signal: 2
I am a Process ,id : 29791
^Csighandler get a signal: 2
I am a Process ,id : 29791
^\Quit
当使用Ctrl + C
对进程发送SIGINT
信号时被捕捉而后执行自定义动作;
未决信号集的置零时机
当信号被处理后未决信号集对应位置应置零从而表示该信号已经被处理完毕为递达状态;
实际上未决信号集的置零时机为执行信号处理前,即先将未决信号集对应位置置零再执行处理信号的函数;
以上文代码为基础进行修改:
void PrintPending() {
sigset_t pending;
sigemptyset(&pending);
sigpending(&pending);
for (int i = 31; i > 0; --i) {
if (sigismember(&pending, i))
cout << "1";
else
cout << "0";
}
cout << endl;
}
void sighandler(int signo) {
// 自定义处理动作
PrintPending();
printf("sighandler get a signal: %d\n", signo);
}
int main() {
struct sigaction act, oldact;
memset(&act, 0, sizeof(act)); // 利用memset()初始化结构体
memset(&act, 0, sizeof(oldact));
act.sa_handler = sighandler; // 为成员赋值 设置自定义处理动作
int n = sigaction(SIGINT, &act, &oldact); // 函数调用
while (!n) { // 循环打印
cout << "I am a Process ,id : " << getpid() << endl;
sleep(1);
}
return 0;
}
添加了PrintPending()
函数用于打印整张Pending
位图;
运行结果为:
$ ./mysignal
I am a Process ,id : 29858
^C0000000000000000000000000000000
sighandler get a signal: 2
I am a Process ,id : 29858
^C0000000000000000000000000000000
sighandler get a signal: 2
I am a Process ,id : 29858
^C0000000000000000000000000000000
当进行信号处理并对Pending
位图进行打印时并未出现SIGINT
位置变为1
的情况;
这意味着实际上在进行信号处理函数前对应的Pending
位图已经被置零;
信号处理过程的阻塞
当一个信号在被进行处理时将会把正在处理的信号添加进信号屏蔽字以阻塞下一个相同的信号;
本质上是防止同一个信号处理函数被重复调用;
void PrintPending() {
sigset_t pending;
sigemptyset(&pending);
sigpending(&pending);
for (int i = 31; i > 0; --i) {
if (sigismember(&pending, i))
cout << "1";
else
cout << "0";
}
cout << endl;
}
void sighandler(int signo) {
// 自定义处理动作
// PrintPending();
printf("sighandler get a signal: %d\n", signo);
while (1) {
PrintPending();
cout << endl;
sleep(1);
}
}
int main() {
struct sigaction act, oldact;
memset(&act, 0, sizeof(act)); // 利用memset()初始化结构体
memset(&act, 0, sizeof(oldact));
act.sa_handler = sighandler; // 为成员赋值 设置自定义处理动作
int n = sigaction(SIGINT, &act, &oldact); // 函数调用
while (!n) { // 循环打印
cout << "I am a Process ,id : " << getpid() << endl;
sleep(1);
}
return 0;
}
该程序中当进程捕获到一个信号时将会调用用户自定义动作进行信号处理;
用户自定义动作为打印对应捕捉到的信号而后无限循环打印pending
表以验证相同信号在第二次注册时是否会被阻塞保留其未决状态;
运行并在另一个窗口中使用kill -signo <pid>
命令向进程发送SIGINT
信号,结果为:
$ ./mysignal
I am a Process ,id : 29921
sighandler get a signal: 2
0000000000000000000000000000000
0000000000000000000000000000010
0000000000000000000000000000010
0000000000000000000000000000010
^\Quit
当第一次注册SIGINT
信号时将执行用户自定义动作,即先打印sighandler get a signal: 2
再循环打印pending
表;
一开始的pending
表SIGINT
信号处已经置零表示正在对该信号进行处理;
当再次注册SIGINT
信号时由于上一个相同信号未被处理完成,处于未递达状态,相同的信号被阻塞,停留在未决信号集中;
可调用sigaction()
函数并设置struct sigaction *act
成员中的sigset_t sa_mask
位图以能够在进行一个信号的处理时阻塞多个信号;
void PrintPending() {
sigset_t pending;
sigemptyset(&pending);
sigpending(&pending);
for (int i = 31; i > 0; --i) {
if (sigismember(&pending, i))
cout << "1";
else
cout << "0";
}
cout << endl;
}
void sighandler(int signo) {
// 自定义处理动作
// PrintPending();
printf("sighandler get a signal: %d\n", signo);
while (1) {
PrintPending();
cout << endl;
sleep(1);
}
}
int main() {
struct sigaction act, oldact;
memset(&act, 0, sizeof(act)); // 利用memset()初始化结构体
memset(&act, 0, sizeof(oldact));
sigset_t mask;
sigemptyset(&mask);
// 使用sigaddset()设置信号集
sigaddset(&mask, 3);
sigaddset(&mask, 4);
sigaddset(&mask, 1);
sigaddset(&mask, 5);
act.sa_mask = mask; // 将设置好的信号集进行赋值从而设置处理时阻塞
act.sa_handler = sighandler; // 为成员赋值 设置自定义处理动作
int n = sigaction(SIGINT, &act, &oldact); // 函数调用
while (!n) { // 循环打印
cout << "I am a Process ,id : " << getpid() << endl;
sleep(1);
}
return 0;
}
该函数中定义了一个sigset_t
类型的mask
,并用sigaddset()
将信号1
,3
,4
,5
分别加入了信号集,并将该sigset_t
类型赋值给sa_mask
成员,使其阻塞多个信号;
运行程序并在另一个终端中使用kill
命令分别以2
,1
,2
,3
,4
,5
的顺序依次注册信号并观察结果;
对应的结果为:
$ ./mysignal
I am a Process ,id : 30083
I am a Process ,id : 30083
sighandler get a signal: 2
0000000000000000000000000000010
0000000000000000000000000000011
0000000000000000000000000000111
0000000000000000000000000001111
0000000000000000000000000011111
结果为首先处理2
号信号SIGINT
并进入死循环打印pending
表;
向进程再次发送1
-5
号信号其都被阻塞至未决信号集中;
可重入函数
当一个进程在调用一个函数,在函数执行过程中接收到了一个信号发生了中断;
而信号的处理中又需要调用一次该函数,若是出现了数据错误或是数据不一致等错误问题则称该函数为不可重入函数,反之则称为可重入函数;
以该图为例;
全局环境中存在一个链表并对其进行一次头插操作,其头插操作的核心代码为:
/* 伪代码 */
insert(...){
// ...
NodeA->next = head;
head = &NodeA;
// ...
}
即将新头插的节点的next
指针指向head
所指向的节点;
head
重新指向新插入的头结点;
若是在插入过程中,即执行完了NodeA -> next = head;
后进程接收到了一个信号并对信号进行处理;
而在信号的处理中需要调用insert()
函数进行头插而再次执行该函数时这个过程被称为 “重入” ,即相同函数重复进入;
当执行主控制流时执行insert()
函数,在调用insert()
时接收信号并处理型号,在处理信号中再次调用了一次insert()
;
流程图如下:
结果为main()
函数和sighandler()
先后向链表中插入两个头结点;
而最后只有一个节点被真正插入链表中,使得另一个节点出现 节点丢失 的内存泄漏问题;
这意味着该insert()
函数为一个不可重入函数;
可重入函数 , 不可重入函数 都只为一个函数的特点;
如果一个函数符合以下条件之一的则是不可重入函数:
-
调用了
malloc
或free
malloc
也是用全局链表来管理堆的; -
调用了标准
I/O
库函数标准
I/O
库的很多实现都以不可重入的方式使用全局数据堆;
volatile 关键字
编译器在编译时可以将代码进行优化;
对应的使用不同优化级别使编译时对代码进行优化,常见的有-O0
,-O1
,-O2
,-O3
等选项,其中-O0
为默认优化,即表示不进行优化;
volatile
关键字是用来修饰一个变量以防止编译器过度优化;
int flag = 1;
void sighandler(int signo) {
printf("sighandler get a signal: %d\n", signo);
flag = 0;
printf("falg : %d\n", flag);
}
int main() {
signal(SIGINT, sighandler);
while (flag);
cout << "process quit sucess" << endl;
}
在这段代码中定义了一个全局变量,且设置了对SIGINT
信号的捕获;
在主控制流main
函数中使用while
以flag
为条件进行循环,当条件为真时循环,条件为假时跳出循环;
当捕捉到SIGINT
信号时对该信号进行处理,处理方案为使用自定义动作,为将全局变量flag
设为0
,即条件为假并对当前flag
进行一次打印;
使用g++
带-O3
选项进行编译并进行编译优化;
g++ -o mysignal mysignal.cc -g -O3 -Wall -std=c++11
运行该程序并向该程序发送2
号信号SIGINT
;
$ ./mysignal
^Csighandler get a signal: 2
falg : 0
^Csighandler get a signal: 2
falg : 0
^Csighandler get a signal: 2
falg : 0
从结果看出,即使注册了SIGINT
信号且执行了自定义动作将全局变量flag
设为了0
但进程仍不退出;
本质原因是由于进行了优化后,其flag
参数将被存放至寄存器当中,而main()
函数中的循环条件始终以寄存器中的flag
进行条件判断,修改的flag
确实内存中的flag
;
两者出现了隔离,寄存器不会向内存再去读取flag
变量而是由于while
循环不停判断寄存器中的变量从而使得进程无法正常退出;
该行为即为编译器的一种编译过度优化;
可通过使用volatile
关键字修饰来放置过度优化行为;
将int flag
修改为volatile int flag
;
重新使用-O3
选项编译并运行,并使用Ctrl + C
向进程发送一个2
号信号SIGINT
信号;
$ ./mysignal
^Csighandler get a signal: 2
falg : 0
process quit sucess
$
结果为使用Ctrl + C
发送2
号信号SIGINT
时进程被终止;
SIGCHLD 信号
当一个子进程退出时将会为其父进程发送一个信号,该信号为17
号信号SIGCHLD
;
该信号默认行为SIG_DFL
为忽略;
可在父进程中调用signal()
接口捕捉SIGCHLD
函数进行验证;
void sighandler(int signo) {
sleep(2);
printf("parent process catch a signal: %d\n", signo);
cout << endl;
waitpid(-1, nullptr, WNOHANG); // 不考虑获取进程退出信息 参数2设置为nullptr
}
int main() {
signal(SIGCHLD, sighandler);
pid_t id = fork();
if (id == 0) {
// child
int cnt = 2;
while (cnt--) {
printf("I am child process,the PID is %d\n", getpid());
cout << endl;
sleep(1);
}
cout << "child process quit...." << endl<<endl;;
exit(-1);
}
// parent
while (1) {
printf("I am parent process,the PID is %d\n", getpid());
cout << endl;
sleep(1);
}
return 0;
}
在父进程中使用signal()
设置捕获17
号信号SIGCHLD
信号,并设置自定义动作为调用waitpid(-1,nullptr,WNOHANG)
以非阻式来等待子进程退出;
fork()
创建子进程,子进程在2s
后退出进程并向父进程发送17
号信号SIGCHLD
;
当父进程获取到子进程所发的信号时将该信号进行捕获,而后调用自定义动作对已经僵尸的子进程进行等待处理;
运行程序并在另一个窗口使用shell
脚本:
$ while :; do ps axj | head -1 && ps axj | grep mysignal | grep -v grep ; echo "----------------------------" ; sleep 1 ; done
观察父子进程的状态;
运行结果为:
$ ./mysignal
I am parent process,the PID is 32131
I am child process,the PID is 32132
I am parent process,the PID is 32131
I am child process,the PID is 32132
I am parent process,the PID is 32131
child process quit....
parent process catch a signal: 17
I am parent process,the PID is 32131
I am parent process,the PID is 32131
2s
过后子进程退出,父进程捕获到17
号信号并对子进程进行waitpid()
清理;
另一个会话显示的结果为:
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
29111 32131 32131 29111 pts/0 32131 S+ 1001 0:00 ./mysignal
32131 32132 32131 29111 pts/0 32131 S+ 1001 0:00 ./mysignal
----------------------------
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
29111 32131 32131 29111 pts/0 32131 S+ 1001 0:00 ./mysignal
32131 32132 32131 29111 pts/0 32131 S+ 1001 0:00 ./mysignal
----------------------------
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
29111 32131 32131 29111 pts/0 32131 S+ 1001 0:00 ./mysignal
32131 32132 32131 29111 pts/0 32131 Z+ 1001 0:00 [mysignal] <defunct>
----------------------------
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
29111 32131 32131 29111 pts/0 32131 S+ 1001 0:00 ./mysignal
32131 32132 32131 29111 pts/0 32131 Z+ 1001 0:00 [mysignal] <defunct>
----------------------------
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
29111 32131 32131 29111 pts/0 32131 S+ 1001 0:00 ./mysignal
----------------------------
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
29111 32131 32131 29111 pts/0 32131 S+ 1001 0:00 ./mysignal
----------------------------
^C
子进程僵尸了2s
后被父进程回收;
若是不需要子进程的退出信息时父进程可对子进程发出的信号使用signal()
进行忽略从而避免子进程停留在僵尸;
以上述代码为基础进行修改:
int main() {
// signal(SIGCHLD, sighandler);
signal(SIGCHLD, SIG_IGN);
pid_t id = fork();
if (id == 0) {
// child
int cnt = 2;
while (cnt--) {
printf("I am child process,the PID is %d\n", getpid());
cout << endl;
sleep(1);
}
cout << "child process quit...." << endl<<endl;;
exit(-1);
}
// parent
while (1) {
printf("I am parent process,the PID is %d\n", getpid());
cout << endl;
sleep(1);
}
return 0;
}
对应结果为:
# 程序所在会话
$ ./mysignal
I am parent process,the PID is 32213
I am child process,the PID is 32214
I am parent process,the PID is 32213
I am child process,the PID is 32214
I am parent process,the PID is 32213
child process quit....
I am parent process,the PID is 32213
I am parent process,the PID is 32213
^C
# 脚本所在会话
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
29111 32213 32213 29111 pts/0 32213 S+ 1001 0:00 ./mysignal
32213 32214 32213 29111 pts/0 32213 S+ 1001 0:00 ./mysignal
----------------------------
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
29111 32213 32213 29111 pts/0 32213 S+ 1001 0:00 ./mysignal
32213 32214 32213 29111 pts/0 32213 S+ 1001 0:00 ./mysignal
----------------------------
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
29111 32213 32213 29111 pts/0 32213 S+ 1001 0:00 ./mysignal
----------------------------
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
29111 32213 32213 29111 pts/0 32213 S+ 1001 0:00 ./mysignal
----------------------------
结果表示子进程退出时直接退出,其父进程并未接收到子进程的SIGCHLD
而是直接将其进行忽略处理;
shell
脚本所在会话显示子进程并未在僵尸状态下进行停留而是直接退出;
-
SIGCHLD
信号的忽略行为SIGCHLD
的默认动作是忽略与对SIGCHLD
信号进行忽略是两种概念;第一种为默认动作实际上是调用
signal(SIGCHLD,SIG_DFL)
,而其对应的行为为忽略;