Linux:进程信号

信号基础
  • 概念
    信号是一种操作系统与进程之间某些事件发生的通知机制,具有随机性(异步),理解为信号是一个软件中断,通知进程发生了某个事件,打断进程当前操作,去处理这个事件,信号就代表着事件;
    当获取一个信号时,并不一定会立即做这件事情,而是记住这个信号。
    进程后续必定要认识信号,知道信号怎么处理,操作系统向进程发信号(操作系统是进程的管理者,可以向进程发信号),就是操作系统找到目标进程PCB,然后修改PCB中位图中的比特位,由0置1,因此这种说法不正确,而应该叫做操作系统向进程写信号(但是一般还是采用前面那种叫法)。
    例如:实现代码
#include <stdio.h>
#include <unistd.h>
int main()
{
	while(1)
	{
		printf("I am a process...\n");
		sleep(1);
	}
	return 0;
}

运行起来就是一个循环,每隔一秒打印内容,我们可以在虚拟机中通过ctrl+c来终止,这个是一种键盘行为,就是通过键盘向前台进程发送2号信号,那么怎么证明这是2号信号,有一个函数叫做signal,查看它的使用手册,例如:

 #include <signal.h>
 typedef void (*sighandler_t)(int);//函数指针
 sighandler_t signal(int signum, sighandler_t handler);

这个函数的作用是进行捕捉信号的,第一个参数表示对谁捕捉,第二个参数是回调函数,表示修改信号的行为,例如刚刚的代码改为:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{
	printf("catch signal,signo:%d\n",signo);
}
int main()
{
	signal(2,handler);//修改信号行为
	while(1)
	{
		printf("I am a process...\n");
		sleep(1);
	}
	return 0;
}

这时运行起来然后ctrl+c,结果是:

[Daisy@localhost myprocess]$ ./myprocess 
I am a process...
I am a process...
I am a process...
I am a process...
I am a process...
^Ccatch signal,signo:2
I am a process...
^Ccatch signal,signo:2
I am a process...
^Ccatch signal,signo:2
I am a process...
^Ccatch signal,signo:2
I am a process...
I am a process...
^\退出(吐核)

这时就不能终止,打印出来的signo是2,表示捕捉到的信号是2,这时我们使用.\就可以退出。

  • 信号的种类
    通过kill -l命令查看信号种类,共有62种信号,分为非可靠信号可靠信号,1-31是非可靠信号,有可能会造成事件丢失,34-64是可靠信号,不会造成事件丢失;
    1-31号信号叫作普通信号,34-64叫做实时信号,它的特点是当前实时信号一旦产生,实时信号立马退出。
  • 信号的生命周期
    信号的产生、在进程中注册、信号的销毁、信号的处理、信号的阻塞
信号的产生
  • 硬件产生
    在终端下按键ctrl+c(2号信号SIGINT,表示中断信号)、ctrl+\(3号信号SIGQUIT,表示让进程退出并且Core Dump)ctrl+z(20号信号SIGTSTP,表示当前进程停止运行,放到后台)
    例如实现代码:
#include <stdio.h>
int main()
{
	while(1)
	{
		printf("-------\n");
	}
	return 0;
}

这是一个死循环,运行起来使用ctrl+\结果是

-------
-------
-------退出(吐核)

表示退出进程
使用ctrl+z结果是:

-------
-------^Z
[2]+  已停止               ./test

表示停止进程,放到后台
使用kill -9 进程id可以强制杀死指定进程,9号信号表示强制杀死

  • 软件产生
    1、kill -signum(信号值) pid表示给指定进程发送指定信号,例如kill-9 pid表示给指定进程发送强制杀死信号
    2、调用系统函数向进程发信号
    (1) kill,函数原型是:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);

表示发送任意一个信号给进程,pid表示进程id,sig表示几号信号
例如:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
int main()
{
	kill(getpid(),3);//发送3号信号,也可以使用这个信号的宏名称
	while(1)
	{
		printf("-------\n");
	}
	return 0;
}

这时就不会进行死循环,而是运行起来发生

[Daisy@localhost 2019_10_31]$ ./test
退出(吐核)

此时就表示kill的确发送了3号信号(SIGQUIT)。
(2)raise
表示向调用的进程发送信号,也就是给当前进程发送信号
它的函数原型:

#include <signal.h>
 int raise(int sig);

sig就是要发送几号信号,例如:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
int main()
{
   raise(SIGQUIT);
   while(1)
   {
   	printf("-------\n");
   }
   return 0;
}

此时结果就是:

[Daisy@localhost 2019_10_31]$ ./test
退出(吐核)

表示给当前进程发送3号停止进程信号。
(3)abort
表示引发不正常进程的终止
它的函数原型是:

 #include <stdlib.h>
 void abort(void);

这个函数没有参数,表示给当前进程发送SIGABRT信号,让当前进程以异常终止的方式结束
例如:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
   abort();
   while(1)
   {
   	printf("-------\n");
   }
   return 0;
}

此时运行得到:

[Daisy@localhost 2019_10_31]$ ./test
已放弃(吐核)

(4) alarm
一个定时器,它的函数原型是:

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

表示调用这个函数之后,等到seconds秒之后给调用进程发送SIGALRM信号,例如:

#include <stdio.h>
#include <unistd.h>
int main()
{
	alarm(2);
	while(1)
	{
		printf("-------\n");
	}
	return 0;
}

它的运行结果就是2秒后发送SIGALRM信号,

-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------
-------闹钟
Core Dump(核心转储)

表示程序异常退出时保存程序的运行信息,方便事后调试,他默认是关闭的,使用ulimit -a命令查看进程中的一些限制信息,使用ulimit -c 1024n命令表示指定核心转储最大不超过1024n大小,使用ulimit -c 0恢复即可,例如:

[Daisy@localhost 2019_10_31]$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 7168
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
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) 4096
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

此时ulimit -c 1024,结果是:

[Daisy@localhost 2019_10_31]$ ulimit -c 1024
[Daisy@localhost 2019_10_31]$ ulimit -a
core file size          (blocks, -c) 1024
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 7168
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
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) 4096
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

发现核心转储大小的确变成了1024
此时重新实现abort函数,运行之后ls发现

[Daisy@localhost 2019_10_31]$ ls
core.3691  Makefile  test  test.c

生成了一个文件core.3691,只要运行一次,就生出core.文件,会占用核心转储的空间
之后进行事后调试,例如:

可以看出此时因为6号信号进程停止,bt查看函数调用栈,发现是因为abort函数而进程停止。最后ulimit -c 0就可以恢复,再次运行程序不会生出core.文件,这就是核心转储的原理。

信号捕捉

例如:

#include <stdio.h>
#include <signal.h>
void handler(int signo)
{
	printf("catch a sig:%d\n",signo);
}
int main()
{
	signal(2,handler);
	while(1);
	return 0;
}

这里的signal函数在前面已经提过,它用来进行函数捕捉,这个代码就是捕捉2号SIGINT信号。
模拟野指针异常
例如此时实现:

int main()
{
	sleep(1);
	int *p=NULL;
	*p=100;
	return 0;
}

p是个野指针,此时运行程序结果是:

[Daisy@localhost 2019_10_31]$ ./test
段错误(吐核)

发现发送了一个信号,这个是11号信号SIGSEGV
此时来捕捉信号

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{
	printf("catch a sig:%d\n",signo);
	exit(1);
}
int main()
{
	int i=1;
	while(i<=31)
	{
		signal(i++,handler);
	}
	int *p=NULL;
	*p=100;
	return 0;
}

这时运行结果是:

[Daisy@localhost 2019_10_31]$ ./test
catch a sig:11

捕捉到的的确是11号信号,kill -l查看到11号信号是SIGSEGV。
总结
(1)信号产生后,由OS进行执行,因为OS是进程的管理者
(2)信号处理时不是立即处理的,信号暂时被进程记录下来,记录在PCB中,使用位图记录
(3)一个进程没有收到信号的时候,知道自己应该怎么处理合法信号
(4)OS向进程发信号就是OS向进程写信号

阻塞信号
  • 信号相关概念
    (1)实际执行信号的处理动作称为信号的递达,信号的处理方式有3种(默认(SIG_DFL)(系统已经定义好的默认函数)、忽略(SIG_IGN)(处理的动作就是什么都不做,依然可以信号注册,只是处理动作什么都不做而已)、自定义(也叫捕捉)(用户自己定义信号回调函数,然后使用这个函数的地址替换原有信号动作数组中的函数地址,也就是替换了信号处理动作中的回调函数)),也就是递达的三种方式
    (2)信号已经产生但是还没有被处理的状态叫做未决状态(Pending)
    (3)进程可以选择阻塞(Block)某个信号,被阻塞信号产生时,将一直保持未决状态,直到进程解除对信号的阻塞,信号执行递达的动作
    注意:阻塞与忽略不同,信号被阻塞就不会递达,而忽略是在递达之后选择的一种处理动作,执行忽略处理。
  • 在内核中的表示
    如图:
    在这里插入图片描述
    每个信号中都有两个标志位(位图)阻塞(block)和未决(pending),一个函数指针数组(handler)表示处理的动作(默认、忽略、自定义(也叫捕捉)),信号值相当于数组下标,信号产生时,内核在PCB中设置该信号的未决标志,直到信号递达才消除该标志
    分析上图:
    (1)SIGHUP信号没有阻塞,也没有达到未决状态,说明没有产生过,当它递达时就执行默认(SIG_DFL,也就是0)处理动作;
    (2)SIGINT信号产生过,正在被阻塞,所以暂时不能递达,虽然它的处理动作是忽略(SIG_IGN也就是1),但是没有解除阻塞之前,不能忽略处理这个信号,因为进程仍旧有机会改变处理动作之后再解除阻塞;
    (3)SIGQUIT信号未产生过,一旦产生这个信号将被阻塞,它的处理动作是用户自定义函数sighandler。
  • sigset_t
    每个信号都只有一个bit的未决标志,是0或1,不记录该信号产生多少次,阻塞也是。
    因此,未决和阻塞标志都可以用sigset_t来存储,sigset_t称为信号集,这个类型表示每个信号的“有效”和“无效”状态,阻塞信号集也叫作当前进程的信号屏蔽字(signal mask)
  • 信号集操作函数

#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和sigdelset表示在信号集中添加或者删除某种有效信号。
函数sigismember函数表示判断当前信号是否在pending集信号中
注意:在使用sigset_t类型变量之前,调用sigemptyset或者sigfillset初始化,使信号集处于确定的状态
sigset_t函数对于每一种信号用一个bit表示“有效”或者“无效”状态,这个类型内部如何存储这些bit依赖于系统的实现,使用者不必关心,不对它的内部数据进行任何打印(例如printf打印sigset_t变量没有意义)

  • sigprocmask
    表示读取或者更改进程的信号屏蔽字(阻塞信号集)
    函数原型是:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

其中参数oldset表示每当block集合要发生改变时,都会将原block集合中的数据拷贝到oldset中返回给用户,便于后期还原;参数set表示信号集合(位图);参数how表示即将要对pcb中block集合所做的操作,设当前信号屏蔽字为block,则how参数的可选值为:SIG_BLOCK:将第二个参数set集合中信号添加到block集合中/将set集合中信号加入阻塞,block=block|set;SIG_UNBLOCK:将第二个参数set集合中信号从block集合中移除/将set集合中信号解除阻塞,block=block&~set;SIG_SETMASK:使用第二个参数set集合中信号替换block集合中的数据,block=set

  • sigpending
    函数原型是:
 #include <signal.h>
 int sigpending(sigset_t *set);

表示读取当前进程的未决信号集,通过set参数传出,调用成功返回0,失败返回-1。
例如:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void show(sigset_t *pending)//遍历位图,一旦产生2号信号打印1
{
	int i=1;
	for(;i<=31;++i)
	{
		if(sigismember(pending,i))
		{
			printf("1");
		}
		else
			printf("0");
	}
	printf("\n");
}
int main()
{
	//屏蔽2号信号,获取当前pending信号,pending为000000...,然后解除阻塞时会变成010000....
	sigset_t set,oset;
	sigemptyset(&set);
	sigemptyset(&oset);

	sigaddset(&set,2);//将2号信号添加到set中
	sigprocmask(SIG_SETMASK,&set,&oset);//更改信号屏蔽字为set所指向的2号信号,就是屏蔽了2号信号

	while(1)
	{
		sigset_t pending;//未决信号集
		sigemptyset(&pending);//初始化未决信号集

		sigpending(&pending);//读取当前未决信号集,通过pending传出
		show(&pending);
		sleep(1);
	}
	return 0;
}

此程序先将2号信号屏蔽,使用sigaddset函数将3号信号添加到set信号集中,然后更改进程的信号屏蔽字为set所指向的值,oset放旧的信号屏蔽字,然后循环,先初始化pending信号集,然后读取当前未决信号集,打印出来(未决信号集有1-31对应bit(对应几号信号),然后进行循环,使用sigismember函数来判断pending信号集中的有效信号一旦包含2号信号,就打印1,其余的打印0),最终的结果就是:

[Daisy@localhost test_2019_10_31_2]$ ./sigpending 
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
^C0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000

可以看出一开始没有产生2号信号(ctrl+c)时,打印的是pending信号集是32个0,之后输入ctrl+c产生信号,此时达到未决状态,第2个bit变成1,当然可以让其他的信号添加到set中。
当然也可以恢复,例如:

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void show(sigset_t *pending)
{
	int i=1;
	for(;i<=31;++i)
	{
		if(sigismember(pending,i))
		{
			printf("1");
		}
		else
			printf("0");
	}
	printf("\n");
}
int main()
{
	//屏蔽2号信号,获取当前pending信号,pending为000000...,然后解除阻塞时会变成010000....
	sigset_t set,oset;
	sigemptyset(&set);
	sigemptyset(&oset);

	sigaddset(&set,2);//将2号信号添加到set中
	sigaddset(&set,3);//将3号信号添加到set中
	sigprocmask(SIG_SETMASK,&set,&oset);
	int count=10;
	while(1)
	{
		sigset_t pending;
		sigemptyset(&pending);

		sigpending(&pending);
		show(&pending);
		sleep(1);
		//恢复回去
		//10秒后恢复信号,然后递达
		if(count--<=0)
		{
			printf("恢复signal!\n");
			sigprocmask(SIG_SETMASK,&oset,NULL);
		}
	}
	return 0;
}

此时运行起来然后执行2号信号ctrl+c,然后10秒钟就出现:

[Daisy@localhost test_2019_10_31_2]$ ./sigpending 
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
^C0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
恢复signal!

此时2号信号就到达递达状态,然后此时可以捕捉一下2号信号,例如:

[Daisy@localhost test_2019_10_31_2]$ ./sigpending 
0000000000000000000000000000000
0000000000000000000000000000000
0000000000000000000000000000000
^C0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
0100000000000000000000000000000
恢复signal!
catch signal:2
0000000000000000000000000000000
恢复signal!
0000000000000000000000000000000
恢复signal!
0000000000000000000000000000000
恢复signal!
0000000000000000000000000000000
恢复signal!
0000000000000000000000000000000
恢复signal!
0000000000000000000000000000000
恢复signal!
0000000000000000000000000000000
恢复signal!
0000000000000000000000000000000
恢复signal!
0000000000000000000000000000000

此时发现2号bit位变回了0,2号信号处于递达状态

信号的捕捉
  • 内核实现信号的捕捉
    首先我们要明白,操作系统不相信任何人,从内核(操作系统)返回用户时进行信号检测,信号处理,进行信号捕捉
    若信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,称为捕捉信号
    由于信号处理函数的代码是在用户空间的,自定义处理过程例如:
    (1)程序运行在用户态主控流程,在中断/异常/系统调用的情况下,进程切换到内核去运行;
    (2)完成内核功能之后,在即将返回用户态之前调用do_signal接口去处理信号;
    (3)其中默认/忽略处理都是在内核中完成,但是自定义接口是用户自己定义的函数运行的用户态;
    (4)因此进程从内核态切换到用户态运行的是信号自定义回调函数,去处理信号函数;
    (5)在信号处理函数运行完毕后,调用sigreturn返回内核态;
    (6)当没有信号待处理,则调用sys_sigreturn返回用户态主控流程从之前运行的地方开始运行;
    如图为内核中实现信号捕捉的过程:
    在这里插入图片描述
  • sigaction
    这个函数的功能是读取和修改与指定信号相关联的处理动作
    它的函数原型是:
 #include <signal.h>
 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

函数调用成功则返回0,失败返回-1,其中参数signum表示指定信号的编号;act表示若act指针非空,则根据act修改该信号的处理动作;oldact表示若他非空,则通过它传出该信号原来的处理动作,其中act和oldact都指向sigaction结构体
例如:

#include <iostream>
#include <signal.h>
#include <sys/types.h>
void handler(int sig)
{
   std::cout <<"catch sig"<< sig << std::endl;
}
int main()
{
   struct sigaction act, oact;
   act.sa_handler=handler;
   sigemptyset(&act.sa_mask);
   act.sa_flags=0;
   sigaction(2,&act,&oact);
   while(1);

   return 0;
}

此时运行,输入2号信号,得到:

[Daisy@localhost test_2019_10_31_3]$ ./sigaction 
^Ccatch sig2
^Ccatch sig2
^Ccatch sig2
^Ccatch sig2
^Ccatch sig2
^Ccatch sig2
^Ccatch sig2

发现的确可以进行信号处理,捕捉信号。
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样保证了在处理某个信号时,如果这种信号再次产生,那么他会阻塞到当前处理结束为止。
如果在调用信号处理函数时,除了当前信号被自动屏蔽以外,还要屏蔽另外一些信号。则用sa_mask字段说明需要额外屏蔽的信号,当信号处理函数返回时自动回复原来的信号屏蔽字,sa_sigaction函数是实时信号的处理函数。(注意sa是sigaction的简写)
信号一共有62种,前1-31叫做普通信号(不可靠信号),34-64叫做实时信号(可靠信号)
实时信号:在队列中并按顺序交付,同一类型的实时信号按照顺序交付给进程
它可以携带额外的信息,进程也可以通过专门的函数更快的恢复信号,并且当定时器到期。空消息队列有消息到达、有异步IO完成时,信号能够及时交付给进程。

可重入函数

函数的重入:在多个执行流中,重复进入同一个函数执行代码
不可重入:如果一个函数重入后有可能造成数据二义或程序的逻辑混乱,叫做不可重入函数
可重入:函数重入之后不会出现任何影响
例如实现一个不带头结点的头插insert操作,main函数调用insert函数向一个链表head中插入结点node1,插入的操作分为两步:第一步是node1->next=head,刚做完第一步,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号要处理,切换到sighandler函数,sighandler函数也调用insert函数向同一个链表head中插入结点node2,insert函数的两步完成后,从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续执行,之前做完第一步被打断,现在开始第二步,结果就是main函数和sighandler先后向链表中插入两个结点,最后只有一个结点真正插入到了链表之中。insert函数被不同的控制流程调用,可能在第一次调用还没有返回就再次进入该函数,这就是重入。
可重入与不可重入的关键点
在这个函数中对全局数据进行了非原子安全操作或者调用了标准I/O库函数的函数就是不可重入的。因此当自己设计函数或者使用别人的函数的时候,根据使用场景考虑函数的重入状况。
竞态条件:在多个执行流中,对同一段代码进行竞争执行。

volatile

它的作用是保存变量的内存可见性,修饰一个变量,防止编译器过度优化
-O1、-O2、-O3选项分别表示一级、二级、三级优化,例如这段代码:

#include <iostream>
#include <signal.h>
bool quit =false;//全局变量
void handler(int sig)
{
   switch(sig)
   {
   	case 2:
   		quit=true;
   		std::cout<<"catch signal"<<sig<<std::endl;
   		break;
   	default:
   		break;
   }
}
int main()
{
   signal(2,handler);
   while(!quit);
   std::cout<<"while quit!"<<std::endl;
   return 0;
}

如果Makefile中是0级优化,那么当产生2号信号时机会退出,那么如果进行1级、2级或者3级优化呢,例如Makefile中实现:

volatile:volatile.cpp
	g++ -O2 -o $@ $^
.PHONY:clean
clean:
	rm -f volatile

此时进行二级优化,这时运行结果就是:

[Daisy@localhost test_2019_11_1]$ ./volatile 
^Ccatch signal2
^Ccatch signal2
^Ccatch signal2
^Ccatch signal2

产生2号信号并没有退出,继续循环,因为while循环检查出的quit已经因为优化,被放在了寄存器中,这时就需要volatile关键字
此时在代码中定义quit全局变量时,前面加上volatile关键字,例如:

#include <iostream>
#include <signal.h>
volatile bool quit =false;//全局变量
void handler(int sig)
{
	switch(sig)
	{
		case 2:
			quit=true;
			std::cout<<"catch signal"<<sig<<std::endl;
			break;
		default:
			break;
	}
}
int main()
{
	signal(2,handler);
	while(!quit);
	std::cout<<"while quit!"<<std::endl;
	return 0;
}

此时运行产生2号信号进程就直接退出了

[Daisy@localhost test_2019_11_1]$ ./volatile 
^Ccatch signal2
while quit!

因此可以得出,在没有使用volatile关键字之前,由于过度优化,全局变量quit放在了CPU的寄存器中,内存中并不是最新的quit(quit已经变成了true,可是内存中仍然是false),可以得出volatile关键字的作用就是保存内存的可见性,防止变量被过度优化,每次都从内存中重新获取变量的数据,修饰的这个变量必须在真实的内存中进行操作。

SIGCHLD信号

在进程等待中提到使用wait函数和waitpid函数来清理僵尸进程,父进程要么阻塞等待子进程结束,要么非阻塞轮询是否有子进程结束等待清理,这两种实现比较复杂,其实,子进程在终止时会向父进程发送SIGCHLD信号,这个信号的默认处理动作是忽略,导致父进程无法及时处理,因此只有使用进程等待才可以(类似于儿子车祸去世了,父亲不知道,必须等待才能收到通知),父进程知道什么时候信号到来了,然后再去调用wait或waitpid接口,则可以不需要继续等待;
父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需要处理自己的工作,不必关心子进程,子进程在退出时会通知父进程,父进程在信号处理函数中调用wait或者waitpid清理子进程即可。
我们之前说了1-31号信号是不可靠信号,如果通过父进程创建了多个子进程,多个子进程同时退出,因为SIGCHLD是一个非可靠信号,因此可能会早造成信号丢失(非可靠信号在已经注册的情况下,就不再注册了),多个子进程同时退出,只注册了一次信号,表示只有一次事件,也就只会调用一次回调函数,只能处理一个子进程,剩下的就会成为僵尸进程,因此最好在一次回调函数中就能将所有的僵尸进程都处理掉,即循环调用waitpid接口,直到没有子进程才退出信号回调函数
例如:

#include<iostream>
using namespace std;
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
void handler(int signo)
{
    cout<<"pid is"<<getpid()<<endl;
    cout<<" signo is"<<signo<<endl;
}
int main()
{
    signal(SIGCHLD,handler);
    pid_t id=fork();
    if(id==0)//child
    {
        cout<<"child id"<<getpid()<<endl;
        sleep(5);
        exit(1);
    }
    else
    {
        cout<<"father id"<<getpid()<<endl;
        while(1);
    }
    return 0;
}

使用signal捕捉SIGCHLD信号,handler函数中打印的pid应该是父进程的id(因为是子进程发送SIGCHLD信号到父进程,在父进程中捕捉信号)运行结果就是:

[Daisy@localhost test_2019_11_1_2]$ ./SIGCHLD 
father id5801
child id5802
pid is5801
 signo is17
^C

可以看出捕捉到的信号编号是17,kill -l发现17号信号的确是SIGCHLD信号
在这里插入图片描述
编写程序:

#include<iostream>
using namespace std;
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
void handler(int signo)
{
    pid_t id;
    while((id=waitpid(-1,NULL,WNOHANG))>0)
    {
        cout<<"wait child sucess!"<<id<<endl;
    }
    cout<<"child is quit!"<<getpid()<<endl;
}
int main()
{
    signal(SIGCHLD,handler);
    pid_t id=fork();
    if(id==0)//child
    {
        cout<<"child id"<<getpid()<<endl;
        sleep(3);
        exit(1);
    }
    while(1)
    {
         cout<<"father is doing some thing"<<endl;
         sleep(1);
    }
    return 0;
}

运行结果是:

[Daisy@localhost test_2019_11_1_2]$ ./SIGCHLD 
father is doing some thing
child id6462
father is doing some thing
father is doing some thing
wait child sucess!6462
child is quit!6461
father is doing some thing
father is doing some thing
father is doing some thing
father is doing some thing
^C

先运行父进程,打印,沉睡一秒过程中运行子进程,打印,子进程沉睡3秒过程中父进程轮询,3秒后,父进程收到SIGCHLD信号,捕捉这个信号,实现的函数handler中采用非阻塞的方法(因此waitpid的第一个参数是-1表示非阻塞),因为如果采用阻塞的方法,例如有10个子进程,退出了5个,第6个要是不退出,父进程一直阻塞,产生问题,因此采用非阻塞,使用循环是因为如果10个子进程同时退出,使用循环。
源代码(github):
信号产生与捕捉:https://github.com/wangbiy/Linux1/tree/master/2019_10_31_1
未决信号集的操作:https://github.com/wangbiy/Linux1/tree/master/test_2019_10_31_2
sigaction用法:https://github.com/wangbiy/Linux2/tree/master/test_2019_10_31_3
volatile关键字的用法:https://github.com/wangbiy/Linux2/tree/master/test_2019_11_1
SIGCHLD信号:https://github.com/wangbiy/Linux2/tree/master/test_2019_11_1_2

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值