【Linux】基础:进程信号

news2024/11/26 0:46:05

【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的标志位,并在进程在内存中的数据转储到磁盘中,方便后期进行调试

img

设置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

image-20230114225803453

验证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_BLOCKset为希望添加的信号屏蔽字的信号,相当于mask = mask | set
    SIG_UNBLOCKset为希望解除的信号屏蔽字的信号,相当于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 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigactionSIGCHLD的处理动作置为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
===============================================================

补充:

  1. 代码将会放到: https://gitee.com/liu-hongtao-1/c–c–review.git ,欢迎查看!
  2. 欢迎各位点赞、评论、收藏与关注,大家的支持是我更新的动力,我会继续不断地分享更多的知识!

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

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

相关文章

echarts柱状图值为0时不显示以及柱状图百分比展示

echarts柱状图值为0时不显示以及柱状图百分比展示 1.效果展示 2.代码 <template><div id"container"><div id"main"></div></div> </template> <script>import * as echarts from echarts import * as lodash…

(JVM)浅堆深堆与内存泄露

​浅堆深堆与内存泄露 1. 浅堆&#xff08;Shallow Heap&#xff09; 浅堆是指一个对象所消耗的内存。在 32 位系统中&#xff0c;一个对象引用会占据 4 个字节&#xff0c;一个 int 类型会占据 4 个字节&#xff0c;long 型变量会占据 8 个字节&#xff0c;每个对象头需要占用…

01.【Vue】Vue2基础操作

一、Vue Vue (读音 /vjuː/&#xff0c;类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是&#xff0c;Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层&#xff0c;不仅易于上手&#xff0c;还便于与第三方库或既有项目整合。另一方面&…

十五天学会Autodesk Inventor,看完这一系列就够了(七),工程图纸

众所周知&#xff0c;Autocad是一款用于二维绘图、详细绘制、设计文档和基本三维设计&#xff0c;现已经成为国际上广为流行的绘图工具。Autodesk Inventor软件也是美国AutoDesk公司推出的三维可视化实体模拟软件。因为很多人都熟悉Autocad&#xff0c;所以再学习Inventor&…

自动化测试 | 这些常用测试平台,你们公司在用的是哪些呢?

本文节选自霍格沃兹测试学院内部教材 测试管理平台是贯穿测试整个生命周期的工具集合&#xff0c;它主要解决的是测试过程中团队协作的问题。在整个测试过程中&#xff0c;需要对测试用例、Bug、代码、持续集成等等进行管理。下面分别从这四个方面介绍现在比较流行的管理平台。…

Spring入门-SpringAOP详解

文章目录SpringAOP详解1&#xff0c;AOP简介1.1 什么是AOP?1.2 AOP作用1.3 AOP核心概念2&#xff0c;AOP入门案例2.1 需求分析2.2 思路分析2.3 环境准备2.4 AOP实现步骤步骤1:添加依赖步骤2:定义接口与实现类步骤3:定义通知类和通知步骤4:定义切入点步骤5:制作切面步骤6:将通知…

Anaconda+VSCode配置tensorflow

主要参考https://blog.csdn.net/qq_42754919/article/details/106121979vscode的安装以及Anaconda的安装网上有很多教程&#xff0c;大家可以自行百度就行。在安装Anaconda的时候忘记勾选自动添加path&#xff0c;需要手动添加环境变量path下面介绍tensorflow安装教程:1.打开An…

getRequestDispatcher()转发和sendRedirect()重定向介绍与比较

文章目录1. request.getRequestDispatcher()1.1请求转发和请求包含的区别1.2request域2.response.sendRedirect()3.请求转发与重定向的区别比较测试1. request.getRequestDispatcher() getRequestDispatcher()包含两个重要方法&#xff0c;分别是请求转发和请求包含。一个请求…

系分 - 案例分析 - 系统设计

个人总结&#xff0c;仅供参考&#xff0c;欢迎加好友一起讨论 文章目录系分 - 案例分析 - 系统设计结构化设计SD内聚偶然内聚逻辑内聚时间&#xff08;瞬时&#xff09;内聚过程内聚通信内聚顺序内聚功能内聚耦合内容耦合公共耦合外部耦合控制耦合标记耦合数据耦合非直接耦合补…

DTO 与 PO的相互转换

目录 常见Bean映射框架 Dozer Orika MapStruct ModelMapper JMapper 测试模型 转化器 OrikaConverter DozerConverter MapperStructConvert JMapperConvert ModelMapperConverter 测试 平均时间 吞吐量 SingleShotTime 采集时间 DTO&#xff08;Data Transfer …

Android项目Gadle统一依赖管理

一.Gradle管理依赖版本 在中大型Android项目中&#xff0c;都会有多个Module进行协同配合。这些module中可能会依赖同一个库的不同版本&#xff0c;这将导致一些问题&#xff0c;要么是代码冲突&#xff0c;要么是APK包体积增大&#xff0c;亦或是项目构建的时间变长&#xff…

在Revit里如何将普通墙与曲面墙的内壁连接

在Revit里如何将普通墙与曲面墙的内壁连接&#xff1f;创建异形建筑时&#xff0c;为了达到如图1所示的效果&#xff0c;该如何操作&#xff1b; 我们可以使用体量建模的方式来创建该类建筑&#xff0c;要点在于如将幕墙与曲面墙的内壁连接。具体方法如下&#xff1a; 一、创建…

ASP.NET Core+Element+SQL Server开发校园图书管理系统(一)

随着技术的进步&#xff0c;跨平台开发已经成为了标配&#xff0c;在此大背景下&#xff0c;ASP.NET Core也应运而生。本文主要基于ASP.NET CoreElementSql Server开发一个校园图书管理系统为例&#xff0c;简述基于MVC三层架构开发的常见知识点&#xff0c;仅供学习分享使用&a…

ubuntu20.04下出现protoc与gazebo版本问题

ubuntu20protocgazebo问题描述问题定位解决方案问题描述 今天在搞路径规划算法时&#xff0c;从git上拉下来一个算法&#xff0c;ros环境那些都有&#xff0c;但是在编译的时候出现了如下图所示的一下问题&#xff1a;&#xff08;为了方便搜索关键词&#xff09; In file incl…

锂离子电池热失控预警资料整理(三)

此前 个人搜集了一些锂电池热失控预警相关期刊、文献&#xff0c;并整理了一些个人认为重要的逻辑、知识点&#xff0c;希望通过此分享让有需要的人了解一些内容&#xff0c;如有问题欢迎同我探讨~ 锂离子电池热失控预警资料整理&#xff08;三&#xff09;九、基于数据分析的锂…

C语言基于FOC控制算法和STM32主控芯片的双路直流无刷电机控制器源码

【FOCSTM32】双路直流无刷电机矢量控制器-使用文档 &#x1f4d5; 介绍 控制器主控芯片采用STM32F405RGT6&#xff0c;控制器底层基于HAL库和FreeRTOS实时操作系统&#xff0c;预留CAN、USART、SWD、USB接口各一&#xff0c;便于通信和控制的工程应用。该控制器提供双路无刷电…

2022年艺术品和古董投资策略研究报告

第一章 行业概况 艺术品是艺术家智力劳动成果的结晶。作为一种特殊商品流通于艺术市场&#xff0c;与其他商品相同的是&#xff0c;它也具备普通商品的基本属性&#xff1a;使用价值和价值。不同的是&#xff0c;艺术品的使用价值体现在精神层面而不是物质层面上&#xff0c;它…

RabbitMQ消息队列实战(1)—— RabbitMQ的体系

RabbitMQ是一个开源的消息代理和队列服务器&#xff0c;用来在不同的应用之间共享数据。1983年&#xff0c;被认为是RabbitMQ的雏形的Teknekron创建&#xff0c;首次提出了消息总线的概念。中间经历过数个阶段的发展&#xff0c;一直到2004年&#xff0c;AMQP&#xff08;Advan…

02.指针的进阶

1.字符指针 在指针的类型中我们知道有一种指针类型为字符指针 char* ; 一般使用: int main() {char ch w;char *pc &ch;*pc w;return 0; } char * p---const char * p(因为常量字符串不能被修改) #include<stdio.h> int main() {const char * pstr "hel…

从汇编的角度了解C++原理——虚函数

文章目录1、虚函数1.1、虚函数储存结构1.2、子类重写虚函数1.3、在栈上调用虚函数1.4、在堆上调用虚函数(通过指针调用&#xff0c;多态)本文用到的反汇编工具是objconv&#xff0c;使用方法可以看我另一篇文章https://blog.csdn.net/weixin_45001971/article/details/12866064…