【Linux】基础:进程信号
摘要:本文将会从生活实际出发,由此掌握进程信号的学习过程,分别为信号的产生、信号的传输、信号的保存和信号的处理,最后再补充学习信号后方便理解的其他概念。
文章目录
- 【Linux】基础:进程信号
- 一、概述
- 1.1 信号理解
- 1.2 预备知识
- 1.2.1 系统定义的信号列表
- 1.2.2 signal系统调用
- 1.2.3 信号相关常见概念
- 二、信号的产生
- 2.1 通过终端按键产生信号
- 2.2 进程发生异常产生信号
- 2.2.1 程序崩溃的本质
- 2.2.2 程序崩溃信号发送的过程
- 2.2.3 补充:获取崩溃的原因
- 2.3 通过系统调用产生信号
- 2.3.1 kill
- 2.3.2 raise
- 2.3.3 abort
- 2.4 通过软件条件产生信号
- 三、信号的传输
- 四、信号的保存
- 4.1 概述
- 4.2 系统调用
- 4.2.1 sigset_t
- 4.2.2 信号集操作函数
- 4.2.3 sigprocmask
- 4.2.4 sigpending
- 五、信号的处理方式
- 5.1 信号处理时机
- 5.2 信号处理方式
- 5.3 sigaction
- 六、总结
- 七、知识补充
- 7.1 可重入函数
- 7.2 volatile
- 7.3 sigchld
一、概述
1.1 信号理解
信号是进程之间事件异步通知的一种方式,属于软中断。对于这句话的理解是较为抽象的,为此可以从实际生活出发了解生活中的概念,从而获取学习进程信号的方法。
比如在实际生活中的烽火戏诸侯、谈虎色变、闻鸡起舞等,对于这些信号来说,首先并非是天生就知道的 ,而是需要被教育,需要了解信号的产生与发送过程。其次对于这些信号的处理动作,也是需要后天教育,而对于信号处理发生的时机,也是需要在特定的场景,合适的时候才可以触发,比如望梅才会止渴。可是在生活中,信号不一定被立即处理,信号随时都可能产生(异步),此时可能有更复杂的事情选哟处理,当时机不合适时,还需要在我们大脑中保存下信号。
为此推导出进程间的通信内容,需要掌握的是进程间信号的产生,信号是如何发送给进程的,进程是如何被识别的,信号需要在哪些适合的时候去执行哪些对应的信号处理动作。
而这些信号的内容,本质上是属于进程的数据,对于信号的发送,则是先PCB中写入信号数据,而PCB是一个内核数据结构,为此信号的本质是底层操作系统发送的数据。
1.2 预备知识
1.2.1 系统定义的信号列表
通过指令kill -l
可以查看系统定义的信号列表,示例如下:
[root@VM-12-7-centos Blog_Signal]# kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
- 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义
#define SIGINT 2
- 编号34以上的是实时信号,编号34以下的是普通信号,本章只讨论编号34以下的信号,不讨论实时信号。
- 普通信号的产生条件,默认动作等信息在
man 7 signal
中有详细说明- 在以往常用的信号有
Ctrl + c
为2号信号,Ctrl + /
为3号信号,Ctrl + z
为19号信号。
1.2.2 signal系统调用
作用:进程用该系统调用设定某个信号的处理方法
头文件:
#include <signal.h>
定义:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
void (*signal(int signum, void (*handler)(int)))(int);
参数:
signum:表示需要设定的某个信号的序号
handler:信号处理动作,该函数的为注册函数,注册函数时不调用该函数,只有当信号到来时,这个函数才会调用。除自定义信号处理动作,还可以是以下是相关宏定义:
SIG_IGN:忽略参数signum所指的信号。
SIG_DFL:恢复参数signum所指信号的处理方法为默认值。
返回值:返回值为sighandler_t
,实际上为 void (*sighandler_t)(int)
,该数据类型为返回值为void
,参数为int
的函数指针。
示例:通过自定义2号信号,进行自定义信号处理,当通过键盘将二号信号发送后,会打印信号发送的信号和进程的pid。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int signo){
printf("get a signal: NO %d,pid: %d\n",signo,getpid());
}
int main(){
signal(2,handler);
while(1){
printf("hello world,pid :%d\n",getpid*());
sleep(1);
}
}
[root@VM-12-7-centos Blog_Signal]# ./test_signal
hello world,pid :3365344
hello world,pid :3365344
^Cget a signal: NO 2,pid: 3365344
hello world,pid :3365344
^Cget a signal: NO 2,pid: 3365344
hello world,pid :3365344
补充:第9号信号是不可以被捕获的,示例如下:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void handler(int signo){
printf("get a signal: NO.%d\n",signo);
}
int main(){
int sig = 1;
for(;sig <= 31; sig++){
signal(sig,handler);
}
while(1){
printf("hello world,pid :%d\n",getpid());
sleep(1);
}
}
[root@VM-12-7-centos Blog_Signal]# ./test_signal
hello world,pid :3369302
^Cget a signal: NO.2
......
hello world,pid :3369302
^_hello world,pid :3369302
......
hello world,pid :3369302
^Zget a signal: NO.20
hello world,pid :3369302
......
Killed
[root@VM-12-7-centos Blog_Signal]# kill -9 3369302
1.2.3 信号相关常见概念
- 信号递达(Delivery):实际执行信号处理的动作,包括默认、忽略和自定义捕捉
- 信号未决(Pending):信号从产生到递达之间的状态,本质是这个信号被暂存到进程PCB的信号位图中
- 阻塞(Block):进程可以阻塞某个信号,本质是操作系统允许进程展示评比指定的信号,而且该型号依旧是未决的,且信号是不会被递达,直到解除阻塞方可递达。
递达的忽略和阻塞的区别:忽略是递达的一种方式,阻塞是没有被递达,是一种独立状态。
二、信号的产生
信号的产生主要有四种方式,分别为:
- 通过终端按键产生信号
- 由于进程发生异常而产生信号
- 通过系统调用产生信号
- 通过软件条件产生信号
信号产生的本质:操作系统向目标进程发送信号
2.1 通过终端按键产生信号
在以往运行可执行程序时,常会在以键盘来发送信号,常用的信号有Ctrl + c
为2号信号,Ctrl + /
为3号信号,Ctrl + z
为19号信号。其中,SIGINT
的默认处理动作是终止进程,SIGQUIT
的默认处理动作是终止进程,简单的示例如下:
[root@VM-12-7-centos Blog_Signal]# ./test_signal
hello world,pid :3374471
hello world,pid :3374471
^Cget a signal: NO.2
hello world,pid :3374471
^Zget a signal: NO.20
2.2 进程发生异常产生信号
2.2.1 程序崩溃的本质
当进程发生异常时,进程会发生崩溃,发生崩溃的本质原因就是获得了信号,然后进程执行信号的默认行为。通过以下示例,先空指针NULL
中写入数据,发生程序异常,并获取对应信号。示例如下:
#include <stdio.h>
#include <unistd.h>
int main(){
int sig = 1;
while(1){
int *p = NULL;
*p = 100;
printf("hello world\n");
sleep(1);
}
}
[root@VM-12-7-centos Blog_Signal]# ./test_signal
Segmentation fault (core dumped)
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void handler(int signo){
printf("get a signal: NO.%d\n",signo);
exit(1);
}
int main(){
int sig = 1;
for(;sig <= 31; sig++){
signal(sig,handler);
}
while(1){
int *p = NULL;
*p = 100;
printf("hello world\n");
sleep(1);
}
}
[root@VM-12-7-centos Blog_Signal]# ./test_signal
get a signal: NO.11
2.2.2 程序崩溃信号发送的过程
对于软件上的异常错误来说,一般会体现在硬件或者其他软件上。因此当程序发生异常时,CPU或者内存等硬件将会将错误体现出来,而操作系统是硬件的管理者,对于硬件的健康进行负责的是操作系统,因此操作系统将会发送信号给进程,让程序崩溃。示意图如下:
2.2.3 补充:获取崩溃的原因
进程等待
当程序崩溃时,需要获取程序崩溃的原因,崩溃时收到的时哪一个信号,在哪一行程序代码发生了异常。在进程控制一文中的进程等待内容中,介绍过waitpid()
和status
进行记录。在Linux中,当一个进程正常退出时,他的退出码和退出信号都会被设置。当一个进程异常退出时,进程的退出信号会被设置,表明当前进程退出的原因,在必要时,操作系统可以设置退出信息中的core dump的标志位,并在进程在内存中的数据转储到磁盘中,方便后期进行调试。
设置core-dump
通过指令ulimit -a
可以查看core dump的设置情况,通过ulimit -c
可以设置core dump
,其中core file size
即生成的core文件字符的大小。示例如下:
[root@VM-12-7-centos Blog_Signal]# ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 0
......
[root@VM-12-7-centos Blog_Signal]# ulimit -c 10240
[root@VM-12-7-centos Blog_Signal]# ulimit -a
core file size (blocks, -c) 10240
......
再设置core dump标志位后,程序崩溃后会提示(core dump)字符,生成对应的core-file文件
并可以使用gdb进行事后调试,示例如下:
int main(){
while(1){
int a = 10;
a /= 0;
printf("hello world\n");
sleep(1);
}
}
[root@VM-12-7-centos Blog_Signal]# ls core*
core-test_signal-3443746
验证core dump设置
而对于进程等待的输出型参数status
,在进程异常退出时,core dump
将会设置为1,否则将会设置为0,以下通过实验进行证明:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main(){
if(fork() == 0){
while(1){
printf("I am a child!\n");
int a = 1;
a /= 0;
}
}
int status = 0;
waitpid(-1,&status,0);
printf("exit code:%d\nexit signal:%d\ncore dump flag:%d\n",
(status>>8)&0xFF,status&0x7F,(status>>7)&1);
}
[root@VM-12-7-centos Blog_Signal]# ./test_signal
I am a child!
exit code:0
exit signal:8
core dump flag:1
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main(){
if(fork() == 0){
printf("I am a child!\n");
int a = 1;
exit(1);
}
int status = 0;
waitpid(-1,&status,0);
printf("exit code:%d\nexit signal:%d\ncore dump flag:%d\n",
(status>>8)&0xFF,status&0x7F,(status>>7)&1);
}
[root@VM-12-7-centos Blog_Signal]# ./test_signal
I am a child!
exit code:1
exit signal:0
core dump flag:0
2.3 通过系统调用产生信号
在此主要介绍三种方式来完成系统调用的信号产生,分别为:
- 通过kill系统调用发送相应信号
- 通过raise完成信号发送
- 通过abort完成信号发送给
2.3.1 kill
头文件:
#include <sys/types.h>
#include <signal.h>
定义:int kill(pid_t pid, int sig);
作用:给进程发送信号
参数:
- pid:进程描述符,当pid>0,将此信号发送给进程ID为pid的进程;pid=0,将此信号发送给进程组ID和该进程相同的进程;pid<0,将此信号发送给进程组内进程ID为pid的进程;pid==-1,将此信号发送给系统所有的进程。
- sig:表示要发送的信号的编号,假如其值为0则没有任何信号送出,但是系统会执行错误检查,通常会利用sig值为0来检验某个进程是否仍在执行。
返回值:成功执行时,返回0;失败返回-1。errno被设为以下的某个值EINVAL:指定的信号码无效(参数sig不合法);EPERM:权限不够无法传送信号给指定进程;ESRCH:参数pid所指定进程或进程组不存在。
说明:可以通过kill -l
指令进行查看信号种类
[root@VM-12-7-centos Blog_Signal]# kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
示例:通过命令行参数获取信号和进程控制块pid,通过kill系统调用完成信号传递,示例如下:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
static void Usage(const char* proc){
printf("Usage:\n\t %s signo Pid\n",proc);
}
int main(int argc,char *argv[]){
if(argc != 3){
Usage(argv[0]);
return 1;
}
int signo = atoi(argv[1]);
int Pid = atoi(argv[2]);
printf("signo: %d ---> Pid :%d\n",signo,Pid);
kill(Pid, signo);
return 0;
}
[root@VM-12-7-centos Blog_Signal]# ./test_kill
hello world ---> PID = 3428088
Killed
[root@VM-12-7-centos Blog_Signal]# ./test_signal 9 3428088
signo: 9 ---> Pid :3428088
2.3.2 raise
头文件:#include <signal.h>
定义:int raise(int sig);
作用:给当前进程发送指定信号(自己给自己发信号),raise(signo)
相当于kill(getpid(),signo)
参数:表示要发送的信号的编号
返回值:成功返回0;失败返回非0值
示例:向自身进程发送3号信号
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main(){
while(1){
printf("hello world\n");
sleep(5);
raise(3);
}
}
[root@VM-12-7-centos Blog_Signal]# ./test_signal
hello world
Quit (core dumped)
2.3.3 abort
头文件:#include <stdlib.h>
定义:void abort(void);
作用:使当前进程接收到信号而异常终止
返回值:就像exit函数一样,abort函数总是会成功的,所以没有返回值
示例:调用abort函数终止自身进程
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
void handler(int signo){
printf("get a signal: NO.%d\n",signo);
exit(1);
}
int main(){
int sig = 1;
for(;sig <= 31; sig++){
signal(sig,handler);
}
int cnt = 3;
while(1){
printf("hello world:Pid ---> %d\n",getpid());
if(cnt == 0){
abort();
}
cnt--;
sleep(1);
}
}
[root@VM-12-7-centos Blog_Signal]# ./test_signal
hello world:Pid ---> 3431841
hello world:Pid ---> 3431841
hello world:Pid ---> 3431841
hello world:Pid ---> 3431841
get a signal: NO.6
2.4 通过软件条件产生信号
通过某种软件(操作系统),来触发信号的发送,如系统层面设置定时器,或者某种操作而导致条件不就绪等这样的场景将会触发信号的发送。最为常见的例子是进程间的通信,当读端关闭了fd时,写端一直在写,最终会收到13号信号(sigpipe),就是一种典型的软件条件触发信号的产生。
在此介绍系统调用alarm
来完成该方法的信号产生,具体内容如下:
alarm
头文件:#include <unistd.h>
定义:unsigned int alarm(unsigned int seconds);
作用:设置一个定时器来传输信号,信号为14号信号
参数:设置的秒数
返回值:0或者是以前设定的闹钟时间还余下的秒数
示例:完成信号捕获操作,并且对信号的返回值不断地获取,通过alarm
的两次调用提前终止预设秒数。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
void handler(int signo){
printf("get a signal: NO.%d\n",signo);
exit(1);
}
int main(){
signal(SIGALRM,handler);
int ret = alarm(10);
while(1){
printf("hello world:PID = %d,ret = %d\n",getpid(),ret);
sleep(2);
int res = alarm(1);
printf("res = %d\n",res);
}
}
[root@VM-12-7-centos Blog_Signal]# ./test_signal
hello world:PID = 3435462,ret = 0
res = 8
hello world:PID = 3435462,ret = 0
get a signal: NO.14
三、信号的传输
了解信号产生的方式后,那么操作系统是如何给进程发送信号的呢,从进程的管理和组织方式而言,就是操作系统发送信号给进程数据控制块。因此,对于进程控制块而言,除了相应的进程属性外,还会有记录保存是否收到信号的对应数据结构。而在以往的观察可以发现,通过kill -l
指令查看信号,信号是有编号的,对于普通信号而言,正是32位信号。可以推测,其实对应的数据结构又是采用了位图结构,所谓比特位的位置标示第几个信号,比特位的内容,而位置的内容表示是否收到信号。
因此对于操作系统发送信号给进程的本质就是操作系统向指定的信号位图写入比特位,即完成信号的发送。
四、信号的保存
4.1 概述
在了解本段内容前需要再次复习1.2.3的信号相关常见概念,了解各个信号的阻塞状态、处理动作和递达状态。
信号在内核中是通过三张表完成保存的,这三张表分别为:pending表、block表和handler表,以下是具体介绍及其图解:
- pending表:位图数据结构,表示确认进程是否收到信号。通过无符号32位整数定义,比特位的位置表示哪一个信号,比特位的内容,表示是否收到信号。对应了信号的未决状态。
- handler表:函数指针数组为数据结构,函数指针传参有三种形式,分别为宏定义
SIG_DFL
表示默认定义的、宏定义SIG_IGN
表示忽略,函数void sighandler(int signo)
表示自定义捕捉,系统调用signal就是修改了该表的内容,对应了常见概念中的信号的递达操作。- block表:也称信号屏蔽字,为位图数据结构,同样采用32位无符号整数定义,比特位的位置表示信号的编号,比特位的内容代表信号是否被阻塞。
进程内置了“识别”信号的方式,对于该三个表的查看进程的信号是通过**横向查看的,首先查看信号是否被block,如果block表置1,则不会查看是否收到信号。如果block未被阻塞,将会查看信号是否收到,如果收到将会在handler表中调用相应的递达方法。**在此使用伪代码进行说明,代码如下:
int isHandler(int signo){
if(block & signo){//信号是否被阻塞
//不会查看信号
}
else{//如果信号未被阻塞
if(signo & pending){ // 该型号未被阻塞且收到信号
handler_array[signo](signo);
return 0;
}
}
return 1;
}
4.2 系统调用
4.2.1 sigset_t
对于系统调用而言,不单单只有接口才算是系统调用,操作系统给予用户提供的不仅有借口,还有各种数据类型,配合系统调用来完成。在4.1介绍的pending表和block表中,可以发现每个信号只有一个bit的表决标志,不记录该信号的发生次数,因此可以采用位图的数据结构,一般情况下,是使用32位无符号整数来表明的,当对于内核来说,不可以直接给予用户使用无符号数来对此表示,为此内核提供了内核数据结构sigset_t
,这个数据集称为信号集。
sigset_t
类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
4.2.2 信号集操作函数
虽然sigset_t
是一个位图结构,但是不同操作系统实现是不一样的,这个类型内部如何存储这些数据则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用信号集操作函数来操作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:初始化sigset_t变量之后调用sigaddset和sigdelset在该信号集中添加某种有效信
- sigdelset:初始化sigset_t变量之后调用sigaddset和sigdelset在该信号集中删除某种有效信
- sigismember:是一个布尔函数,用于判断一个信号集的有效信息中是否包含某种信号,不包含返回0,出错返回-1
- 返回值:前四个函数成功返回0,出现错误返回-1
4.2.3 sigprocmask
头文件: #include <signal.h>
定义:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
作用:调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
参数:
how:表示用于指定信号修改的方式,主要有三种可选值:
可选值 作用 SIG_BLOCK set为希望添加的信号屏蔽字的信号,相当于mask = mask | set SIG_UNBLOCK set为希望解除的信号屏蔽字的信号,相当于mask = mask & ~set SIG_SETMASK 设置当前信号的屏蔽字为set所指向的值,相当于mask = set set:输入型参数,传入新的信号屏蔽字
oldset:输出型参数,返回旧的信号屏蔽字
返回值:若成功则为0,若出错则为-1
实例:完成对于二号信号的屏蔽
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
int main(){
sigset_t in_set , old_set;
sigemptyset(&in_set);
sigemptyset(&old_set);
sigaddset(&in_set,2);
sigprocmask(SIG_SETMASK,&in_set,&old_set);
while(1){
printf("hello world\n");
sleep(1);
}
return 0;
}
[root@VM-12-7-centos Blog_Signal]# ./test_sigprocmask
hello world
hello world
hello world
^C^C^Chello world
^Z
[2]+ Stopped ./test_sigprocmask
4.2.4 sigpending
头文件:#include <signal.h>#include <signal.h>
定义:int sigpending(sigset_t *set);
作用:不对pending位图做修改,而只是单纯的获取进程的pending位图
参数:为输出型参数,读取当前进程的未决信号集,通过set传出
返回值:成功返回0,出错则返回-1
实例:屏蔽2号信号,不断地获取当前进程的pending位图,并打印现实,再通过手动发送2号信号,因为2号信号不会被传达,所以会打印在pending表中打印出来。当一段时间后,解除屏蔽,并再次打印pending表
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void printSigpending(sigset_t *set){
printf("Process %d pending:",getpid());
for(int i = 1;i<=31;i++){
if(sigismember(set,i)){
printf("1");
}
else{
printf("0");
}
}
printf("\n");
}
void handler(int signo){
printf("give signal :NO.%d ----> Finished\n",signo);
}
int main(){
// 信号递达:自定义捕获
signal(2,handler);
sigset_t iset,oset;
sigemptyset(&iset);
sigemptyset(&oset);
sigaddset(&iset,2);
// 信号屏蔽
sigprocmask(SIG_SETMASK,&iset,&oset);
sigset_t pending;
int cnt = 0;
// pending打印
while(1){
sigemptyset(&pending);
sigpending(&pending);
printSigpending(&pending);
cnt++;
// 解除屏蔽
if(cnt == 5){
sigprocmask(SIG_SETMASK,&oset,NULL);
printf("signal NO.2 recovery\n");
}
sleep(1);
}
}
[root@VM-12-7-centos Blog_Signal]# ./test_sigpending
Process 3617412 pending:0000000000000000000000000000000
Process 3617412 pending:0000000000000000000000000000000
Process 3617412 pending:0000000000000000000000000000000
^CProcess 3617412 pending:0100000000000000000000000000000
Process 3617412 pending:0100000000000000000000000000000
give signal :NO.2 ----> Finished
signal NO.2 recovery
Process 3617412 pending:0000000000000000000000000000000
^Z
[6]+ Stopped ./test_sigpending
五、信号的处理方式
5.1 信号处理时机
对于信号来说,信号的产生是异步的,当前进程可能正在处理优先级更高的事,信号需要进行延迟处理。对于信号处理的时机,由于信号是被保存在进程的PCB中的pending位图中,当进程从内核态返回到用户态时,进行检测并完成处理工作。
而所谓内核态和用户态的切换是在于用户调用系统函数时,除了进入函数,身份也会发生变化,用户身份编程内核身份。内核态为执行操作系统的代码和数据,计算机所处的状态称为内核态,操作系统代码的执行全都是内核态。用户态是用户代码和数据被访问或者执行时,所处的状态,用户的代码就是在用户态执行的。而二者的区别就是在于权限的区别。
在进程地址空间部分,学习过在进程的地址空间中,如果为32位4GB的地址空间,将会有1G的内核空间,以及3G的用户空间。实际上用户的数据代码和操作系统的数据和代码都是被加载到内存中的,而是通过CPU内有寄存器CR3
保存了相应进程的状态,对于用户使用的是用户级页表,只能访问用户的数据和代码,为用户空间。对于内核,使用的是内核级页表,只能访问内核级的数据和代码,为内核空间。而系统调用就是进程的身份切换到内核,执行内核页表的系统函数,也可以看出无论进程如何切换都是保证了一个操作系统,其实就是每个进程是有3~4G的地址空间,使用了的是同一张内核页表。示意图如下:
5.2 信号处理方式
信号处理的方式为当用户态执行系统调用时,身份转换为内核身份,其中将会进行信号的检测和处理,但进行信号的处理过程中,可能需要自定义捕捉信号,为此还需要进入用户态,去执行信号捕捉方法,最后还需要回到内核态,才执行sys_sigreturn()
函数,返回用户态。图示如下:
当然可能存在疑问,为何第三步为何需要在用户态处理信号捕捉方法呢?是因为操作系统的身份特殊,可能会因为权限过高进行误操作,因此需要回到用户态执行用户态的代码。
5.3 sigaction
头文件:#include <signal.h>
定义:int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
作用:检测与改变信号行为,修改handler函数指针数组
参数:
signum:需要修改的信号序号
act:输入型参数,传入结构体
struct sigaction
,其中包含了关于信号捕获方法的内容,而一般常用的是void (*sa_handler)(int);
定义自定义捕获执行函数和sigset_t sa_mask
用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置。struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };
对于其他成员,本文将不再介绍,有兴趣可以自己查阅资料
oldact:输出型参数,用来传回旧的信号捕获方法
返回值:成功返回0,失败返回-1
实例:捕获2号信号
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
void handler(int signo){
printf("get a signal:NO.%d\n",signo);
}
int main(){
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = handler;
sigaction(2, &act ,NULL);
while(1){
printf("hello world\n");
sleep(1);
}
return 0;
}
[root@VM-12-7-centos Blog_Signal]# ./test_sigaction
hello world
^Cget a signal:NO.2
hello world
^Z
[8]+ Stopped ./test_sigaction
补充:当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。
如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
实例如下:对sa_mask设置三号信号,在二号信号处理时,三号信号将会被屏蔽
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
void handler(int signo){
while(1){
printf("get a signal:NO.%d\n",signo);
sleep(2);
}
}
int main(){
struct sigaction act;
memset(&act, 0, sizeof(act));
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,3);
act.sa_handler = handler;
sigaction(2, &act ,NULL);
while(1){
printf("hello world:PID = %d\n",getpid());
sleep(1);
}
return 0;
}
[root@VM-12-7-centos Blog_Signal]# ./test_sigaction
hello world:PID = 3654808
hello world:PID = 3654808
get a signal:NO.2
Killed
[root@VM-12-7-centos Blog_Signal]# kill -2 3654808
[root@VM-12-7-centos Blog_Signal]# kill -3 3654808
[root@VM-12-7-centos Blog_Signal]# kill -9 3654808
六、总结
在本节中,结合生活中对信号的理解,分别从信号前中后三过程发出,介绍了信号的产生、信号的传输、信号的保存和处理方式。在信号的产生一节,主要介绍了各种方式及其信号发送的本质,同样还拓展了程序崩溃的原因及如何获取程序崩溃的错误。再由信号传输进行过度,说明操作系统如何向进程发送信号。再通过信号保存一节,介绍了相应的组织管理数据结构,完成了对于该数据结构的系统调用介绍。最后介绍了关于信号发送后,需要处理的信号发送时机以及信号处理方式。
七、知识补充
7.1 可重入函数
可重入函数指在多执行流中,如果函数一旦重入,不会发生问题,则称为可重入函数,否则称为不可重入函数。该概述较为抽象,以下通过信号进行讲解。
在下图中,main
函数调用insert
函数向一个链表head
中插入节点node1
,插入操作分为两步,刚做完第一步够,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler
函数,sighandler
也调用insert
函数向同一个链表head
中插入节点node2
,插入操作的两步都做完之后从 sighandler
返回内核态,再次回到用户态就从main
函数调用的insert
函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
函数符合不可重入的条件如下:
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
7.2 volatile
volatile
为C语言的关键字,作用为保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作,读取必须贯穿式读取,不要读取中间的缓冲区中的数据。
实验示例:通过获取信号的方式来修改全局变量(唯一修改方法),结束主函数循环,当不使用volatile
,若优化编译过程,会导致编译器认为主函数中不会对flag进行修改了,因此直接优化到寄存器中,而不经过冗余的寻址,到内存中读取。而信号修改的是内存的flag,将会导致屏蔽了内存数据。而增加volatile则不会发生该情况。
代码如下:
// 不带volatile
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int signo){
flag = 1;
printf("get signal:NO.%d , flag: 0--->1 \n",signo);
}
int main(){
signal(2,handler);
while(!flag);
printf("process exit!\n");
return 0;
}
[root@VM-12-7-centos Blog_Signal]# gcc -o test_volatile test_volatile.c -O3
[root@VM-12-7-centos Blog_Signal]# ./test_volatile
^Cget signal:NO.2 , flag: 0--->1
^Cget signal:NO.2 , flag: 0--->1
^Cget signal:NO.2 , flag: 0--->1
^Cget signal:NO.2 , flag: 0--->1
^Z
[9]+ Stopped ./test_volatile
// 带volatile
#include <stdio.h>
#include <signal.h>
volatile int flag = 0;
void handler(int signo){
flag = 1;
printf("get signal:NO.%d , flag: 0--->1 \n",signo);
}
int main(){
signal(2,handler);
while(!flag);
printf("process exit!\n");
return 0;
}
[root@VM-12-7-centos Blog_Signal]# gcc -o test_volatile test_volatile.c -O3
[root@VM-12-7-centos Blog_Signal]# ./test_volatile
^Cget signal:NO.2 , flag: 0--->1
process exit!
7.3 sigchld
子进程在终止时,会给父进程发送SIGCHLD
信号,该信号的默认动作是忽略。父进程可以自定义SIGCHLD
信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait
清理子进程即可。
事实上,由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction
将SIGCHLD
的处理动作置为SIG_IGN
,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用sigaction
函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
实例如下:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main(){
signal(SIGCHLD,SIG_IGN);
pid_t pid = fork();
if(pid == 0){
int cnt = 3;
while(cnt){
printf("I am a child\n");
sleep(1);
cnt--;
}
exit(0);
}
sleep(10);
return 0;
}
[root@VM-12-7-centos Blog_Signal]# while :; do ps axj | head -1 && ps axj | grep -v "grep" | grep test_sigchld; sleep 1; echo "===============================================================";done
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
===============================================================
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
===============================================================
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
===============================================================
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
===============================================================
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
3784860 3786590 3786590 3784860 pts/4 3786590 S+ 0 0:00 ./test_sigchld
3786590 3786591 3786590 3784860 pts/4 3786590 S+ 0 0:00 ./test_sigchld
===============================================================
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
3784860 3786590 3786590 3784860 pts/4 3786590 S+ 0 0:00 ./test_sigchld
3786590 3786591 3786590 3784860 pts/4 3786590 S+ 0 0:00 ./test_sigchld
===============================================================
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
3784860 3786590 3786590 3784860 pts/4 3786590 S+ 0 0:00 ./test_sigchld
3786590 3786591 3786590 3784860 pts/4 3786590 S+ 0 0:00 ./test_sigchld
===============================================================
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
3784860 3786590 3786590 3784860 pts/4 3786590 S+ 0 0:00 ./test_sigchld
===============================================================
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
3784860 3786590 3786590 3784860 pts/4 3786590 S+ 0 0:00 ./test_sigchld
===============================================================
补充:
- 代码将会放到: https://gitee.com/liu-hongtao-1/c–c–review.git ,欢迎查看!
- 欢迎各位点赞、评论、收藏与关注,大家的支持是我更新的动力,我会继续不断地分享更多的知识!