【Linux】基础:进程信号

一、概述

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. 欢迎各位点赞、评论、收藏与关注大家的支持是我更新的动力我会继续不断地分享更多的知识
阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux

“【Linux】基础:进程信号” 的相关文章