生活中的信号——给人看的!
计算机中的信号——给进程看的!
①进程具有识别并处理信号的能力(这个能力远远早于信号的产生),且进程知道该如何识别信号,且进程提前约定好当接收到信号时要执行了哪些操作,即约定信号的处理方式和安装信号处理函数。
②进程收到某种信号的时候,并不是立即处理,而是在合适的时候。(信号随时都可能产生——异步性,但是当前进程可能会做更加重要的事情)
③进程收到信号后,需要先将信号保存起来,以供在合适的时候处理
信号的本质:数据
保存在哪里呢?
信号发送—>是往task_struct内写入信号数据的!
但是task_struct是一个内核数据结构,内核并不相信任何人,只相信自己,所以是操作系统向task_struct中写入信号数据的。
所以:无论我们的任何信号在任何时间发送,本质都是在底层通过操作系统发送的!
用户输入命名,在shell中启动一个前台程序,ctrl+c就能在键盘输入产生一个硬件中断,被操作系统获取,解释称为信号,发送给目标前台进程,前台进程因为收到信号,进而退出
#include
#include
int main()
{
while(1){
printf("hello world! pid:%d\n",getpid());
sleep(1);
}
return 0;
}
函数名称 | signal |
---|---|
函数功能 | 信号处理函数 |
头文件 | #include |
函数原型 | sighandler_t signal(int signum, sighandler_t handler); |
参数 | signum:要处理的信号 handler:信号处理函数 |
返回值 | ≠-1:成功(该信号以前的处理函数) SIG_ERR:出错 |
typedef void (*sighandler_t)(int);// 函数指针
sighandler_t signal(int signum, sighandler_t handler); // 修改进程对于信号的默认处理动作
在signal
函数声明中,出现了一个新的类型,sighandler_t
,该类型是被重命名的,是一种函数指针,函数指针指向的这个函数返回值是void
,有一个int
类型的参数。在signal
函数安装时需要此类型的函数指针作为参数,并且当进程调用signal
函数捕捉信号成功后,将会返回signo
信号之前的处理函数;如果进程在运行过程中收到信号signo
时,将会转去指向handler
函数(回调函数)。
#include
#include
#include
void handler(int signo)
{
printf("you can not stop this process by ctrl+c! signo:%d pid:%d\n",signo,getpid());
}
int main()
{
//通过signal注册对2号信号的处理动作,改成我们的自定义动作
signal(2,handler);
while(1){
printf("hello world! pid:%d\n",getpid());
sleep(1);
}
return 0;
}
注册函数的时候,不是调用这个函数,只有当信号到来的时候,这个函数才会去调用我们的自定义动作(回调函数)
结果展示:
总结:
一般而言,进程收到信号的处理方案有3种情况
1.默认动作——部分是终止自己,暂停等
2.忽略动作——是一种信号处理的方式,只不过动作就是什么也不干
3.自定义动作——我们刚刚用signal方法,就是在修改信号的处理动作由:默认->自定义动作
我们可以捕捉各种各样的信号:
void handler(int signo)
{
printf("signo:%d pid:%d\n",signo,getpid());
exit(123);
}
int main()
{
int sig=1;
for(;sig<31;sig++)
{
signal(sig,handler);
}
while(1){
printf("hello world! pid:%d\n",getpid());
sleep(1);
}
return 0;
}
我们可以知道,信号的一种产生方式,通过键盘终端产生。
键盘产生的信号,只能终止前台进程,加个&可以让进程后台运行
我们还可以发现9号信号是不能够被捕捉的(不可被自定义)!
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE
信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV
信号发送给进程。
int main()
{
while (1)
{
int * p=NULL;
p=(int*)100;
*p=100; //error 空指针解引用
printf("hello world! pid:%d\n", getpid());
sleep(1);
}
return 0;
}
结果展示:
段错误!!!VS下叫做崩溃
就是程序中有越界或者野指针的情况!
为什么会崩溃?
我们再来看一下这个代码:
void handler(int signo)
{
switch (signo)
{
case 2:
printf("get a signo:%d\n", signo);
break;
case 3:
printf("get a signo:%d\n", signo);
break;
case 9:
printf("get a signo:%d\n", signo);
break;
default:
printf("get a signo:%d pid:%d\n", signo,getpid());
break;
}
// exit(123);
}
int main()
{
int sig = 1;
for (; sig < 31; sig++)
{
signal(sig, handler);
}
while (1)
{
int *p = NULL;
p = (int *)100;
*p = 100; // error 空指针解引用
printf("hello world! pid:%d\n", getpid());
sleep(1);
}
return 0;
}
同样我们有着其他错误,比如除0时会收到8号信号。
同样我们不捕捉该信号,又会发生什么呢?
如下发生浮点数错误:
崩溃的原理:
总结:
当进程崩溃的时候,你最想知道什么? 崩溃的原因是什么,可以通过waitpid();+ status参数获取到
崩溃的原因–>崩溃时,收到的是哪一个信号?
你还想知道什么? ?在哪一行崩溃了! !
core dump
标志位,并将进程在内存中的数据转储到磁盘当中,方便我们后期调试core dump
。默认情况下,在Linux云服务器上,core dump这项技术是被关闭的,我们要手动打开。
查看系统资源指令:ulimit -a
手动设置该文件的大小:ulimit -c 10240
现在再次运行我们之前的程序:
现在观察,发现多了一个core
文件,这个文件可以让我们用gbd
调试的
使用gdb调试:
验证core dump
标志位是否被设置的:
//验证core dump标志位是否被设置的
#include
int main()
{
if (fork() == 0)
{
while (1)
{
printf("I am child...!\n");
int a = 10;
a/=0;
}
}
int status = 0;
waitpid(-1, &status, 0);
printf("exit code:%d\n", (status >> 8) & 0xFF);
printf("exit signal:%d\n", status & 0x7F);
printf("exit dump flag:%d\n", (status >> 7) & 1);
return 0;
}
产生信号的另一种方法是调用kill
函数,是由某个进程调用kill发送信号给指定进程或者进程组
函数名称 | kill |
---|---|
函数功能 | 向进程发送信号 |
头文件 | #include |
函数原型 | int kill(pid_t pid, int sig); |
参数 | pid:目标进程PID sig:要发送的信号 |
返回值 | 0:成功 -1:失败 |
采用系统调用向目标进程发送信号
#include
static void Usage(const char* proc)
{
printf("Usage:\n\t %s signo who\n",proc);
}
// ./mytest signo who
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
return 1;
}
int signo=atoi(argv[1]); // 强转成为整数
int who=atoi(argv[2]);
printf("signo:%d,who:%d\n",signo,who);
kill(who,signo);
return 0;
}
结果展示:
raise函数也可以向进程发送信号,与kill函数不同的是,raise函数发送信号的目标进程是确定的,是向调用raise函数的进程自己发送一个信号,因此使用raise函数时,通常说进程自举了一个信号。
函数名称 | raise |
---|---|
函数功能 | 自举一个信号 |
头文件 | #include |
函数原型 | int raise(int signo) |
参数 | signo:要发送的信号 |
返回值 | 0:成功 -1:失败 |
// raise函数
int main()
{
while (1)
{
printf("I am process!\n");
sleep(3);
raise(8);
}
return 0;
}
函数名称 | abort |
---|---|
函数功能 | 使当前进程接收到信号而异常终止,意思就是给自己发送6号信号 |
头文件 | #include |
函数原型 | void abort(void) |
参数 | 无 |
返回值 | 无 |
//abort函数
void handler(int signo)
{
printf("get a signo:%d pid:%d\n",signo,getpid());
exit(123);
}
int main()
{
int sig=1;
for(;sig<31;sig++)
{
signal(sig,handler);
}
while(1){
printf("hello world! pid:%d\n",getpid());
sleep(1);
abort();//给自己发送6号信号
}
return 0;
}
通过某种软件(OS),来触发信号的发送,系统层面设置定时器,或者某种操作而导致条件不就绪等这样的场景下,而触发的信号发送
进程间通信:当读端不光不读, 而且还关闭了读fd,写端一直在写,最终写进程会受到sigpipe (13) ,就是一种典型的软件条件触发的信号发送
alarm函数
调用alarm函数可以设置一个闹钟,也就是告诉内核在seconds秒之后给当前进程发送SIGALRM(14号)信号,该信号的默认处理动作是终止当前进程
函数名称 | alarm |
---|---|
函数功能 | 设置计时器 |
头文件 | #include |
函数原型 | int alarm(int seconds); |
参数 | seconds:定时器设置的秒数 |
返回值 | 0:之前未调用过alarm >0:上一次调用alarm时设置的秒数余留的时间 |
void handler(int signo)
{
switch (signo)
{
case 2:
printf("get a signo:%d\n", signo);
break;
case 3:
printf("get a signo:%d\n", signo);
break;
case 9:
printf("get a signo:%d\n", signo);
break;
default:
printf("get a signo:%d pid:%d\n", signo,getpid());
break;
}
exit(123);
}
int main()
{
int sig=1;
for(;sig<31;sig++)
{
signal(sig,handler);
}
alarm(3); // 3秒后给当前进程发送14号信号
while(1){
printf("hello world! pid:%d\n",getpid());
sleep(1);
}
return 0;
}
结果展示:
返回值
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就 是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数
//alarm 函数
void handler(int signo)
{
switch (signo)
{
case 2:
printf("get a signo:%d\n", signo);
break;
case 3:
printf("get a signo:%d\n", signo);
break;
case 9:
printf("get a signo:%d\n", signo);
break;
default:
printf("get a signo:%d pid:%d\n", signo,getpid());
break;
}
exit(123);
}
int main()
{
int sig=1;
for(;sig<31;sig++)
{
signal(sig,handler);
}
int ret=alarm(30); // 30秒后给当前进程发送14号信号
while(1){
printf("hello world! ret=%d\n",ret);
sleep(3);
int res=alarm(0);// 取消闹钟
printf("res=%d\n",res);
}
return 0;
}
统计一秒钟我们的服务器对一个整型值能递增多少
//统计一下一秒钟对于整型值能递增多少
int count=0;
int main()
{
alarm(1);// 没有设置alarm信号捕捉(自定义),而是执行默认动作——终止进程
while(1)
{
printf("count=%d\n",count);
count++;
}
return 0;
}
//统计一下一秒钟对于整型值能递增多少
int count=0;
void handlerAlarm(int signo)
{
printf("count=%d\n",count);
exit(123);
}
int main()
{
signal(SIGALRM,handlerAlarm);
alarm(1);// 没有设置alarm信号捕捉(自定义),而是执行默认动作——终止进程
while(1)
{
//现在不进行打印操作
count++;
}
return 0;
}
总结:
信号产生的方式种类虽然非常多,但是无论产生信号的方式千差万别,但是最终,一定都是通过OS向目标进程发送的信号! ! !
进程中采用位图结构来标识进程是否收到了某信号!
众多的信号一定是发给进程的,而且是发给进程的task_struct的,那么就会用到一个数据变量(unisgned int)来标识是否收到了某信号。——位图结构
所以:
比特位的位置,代表的是哪一个信号
比特位的内容,代表的就是是否收到了信号
如何理解OS给进程发送信号?
OS发送信号数据给task_ struct–>本质是OS向指定进程的task_struct 中的信号位图写入比特位1,即完成能信号的发送,信号的发送–>信号的写入。
递达:实际执行信号的处理动作
- 递达又分为三种 1. 默认 2. 忽略 3. 自定义捕捉
未决:信号从产生到递达之间
- 本质是这个信号被暂存在进程的task_struct信号位图中
阻塞:进程可以选择阻塞某个信号
- 本质是OS允许进程暂时屏蔽指定的信号
1.信号依旧是未决的
2.该信号不会被递达,直到解除阻塞,方可递达
忽略和阻塞的区别
忽略是信号递达之后的处理动作
阻塞是未递达
block
和pending
表都是位图结构
阻塞位图也叫做信号屏蔽字
比特位的位置,代表的是哪一个信号
比特位的内容,代表的就是是否收到了信号
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表handler。
block
:状态位图,表示哪些信号不应该被递达,直到解除阻塞
pending
:保存的是已经收到,但是还没有被递达的信号
handler
:处理信号的动作
信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志
OS发送信号的本质:修改目标进程中的pending位图。所以进程内置了“识别”信号的方式!
例如:SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
不要认为有接口才是系统调用,OS也会给用户提供数据类型,配合系统调用完成
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。
因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略
//set是一个变量,该变量在什么地方保存?
//和我们之前用到的int, double, 没有任何差别,都是在用户栈上
sigset_t set;
set|=1;//error 不合法
虽然sigset_t
是一个位图结构,但是不同的操作系统实现是不一样的,不能让用户直接修改该变量,需要使用特定的函数。
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。
#include
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置位,表示 该信号集的有效信号包括系统支持的所有信号。sigset_ t
类型的变量之前,一定要调用sigemptyset
或sigfillset
做初始化,使信号集处于确定的状态。初始化sigset_t
变量之后就可以在调用sigaddset
和sigdelset
在该信号集中添加或删除某种有效信号这四个函数都是成功返回0,出错返回-1。sigismember
是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
如果进程需要阻塞某些信号,可以使用函数sigprocmask来设置屏蔽信号集。
函数名称 | sigprocmask |
---|---|
函数功能 | 查看或者修改当前的屏蔽信号集 |
头文件 | #include |
函数原型 | int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); |
参数 | how:修改屏蔽信号集的方式 set:本次要按照how的方式操作的信号集合指针 oset:修改前的屏蔽信号集指针 |
返回值 | 0:成功 -1:失败 |
说明:如果oset为非空,那么进程当前的屏蔽信号集就会通过oset返回。指针set为空时,不修改进程当前的屏蔽信号集;set非空时,how的取值才有意义,表示如何修改当前的屏蔽信号集,可以选择如下三种方式。
how | 说明 |
---|---|
SIG_BLOCK | set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask按位或上set |
SIG_UNBLOCK | set包含了我们希望从当前信号屏蔽字中解除阻塞信号,相当于mask=mask&~set |
SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值,相当于mask=set |
#include
#include
#include
int main()
{
// sigset_t set;
// //set|=1; error
sigset_t iset,oset;// in out
sigemptyset(&iset);
sigemptyset(&oset);
//对2号进行信号屏蔽
sigaddset(&iset,2);
//1. 设置当前进程的屏蔽字
//2. 获取当前进程老的屏蔽字
sigprocmask(SIG_SETMASK,&iset,&oset);
while(1)
{
printf("hello world!\n");
sleep(1);
}
return 0;
}
#include
#include
#include
int main()
{
// sigset_t set;
// //set|=1; error
sigset_t iset,oset;// in out
sigemptyset(&iset);
sigemptyset(&oset);
//对2 9 号进行信号屏蔽
sigaddset(&iset,2);
sigaddset(&iset,9);
//1. 设置当前进程的屏蔽字
//2. 获取当前进程老的屏蔽字
sigprocmask(SIG_SETMASK,&iset,&oset);
while(1)
{
printf("hello world!\n");
sleep(1);
}
return 0;
}
不修改pending位图,只是单纯的获取进程的pending位图
#include
int sigpending(sigset_t *set);
void show_pending(sigset_t *set)
{
printf("current process pending:");
int i = 1;
for (i = 1; i < 31; i++)
{
if (sigismember(set, i))
{
printf("1");
}
else
{
printf("0");
}
}
printf("\n");
}
// 获取pending位图
int main()
{
sigset_t iset, oset;
sigemptyset(&iset);
sigemptyset(&oset);
//对2号进行信号屏蔽
sigaddset(&iset, 2);
// 1. 设置当前进程的屏蔽字
// 2. 获取当前进程老的屏蔽字
sigprocmask(SIG_SETMASK, &iset, &oset);
sigset_t pending;
while (1)
{
sigemptyset(&pending);
sigpending(&pending);
show_pending(&pending);
sleep(1);
}
return 0;
}
void show_pending(sigset_t *set)
{
printf("current process pending:");
int i = 1;
for (i = 1; i <= 31; i++)
{
if (sigismember(set, i))
{
printf("1");
}
else
{
printf("0");
}
}
printf("\n");
}
void handler(int signo)
{
printf("%d号信号被递达了,已经处理完成!\n",signo);
}
// 获取pending位图
int main()
{
signal(2,handler);
sigset_t iset, oset;
sigemptyset(&iset);
sigemptyset(&oset);
//对2号进行信号屏蔽
sigaddset(&iset, 2);
// 1. 设置当前进程的屏蔽字
// 2. 获取当前进程老的屏蔽字
sigprocmask(SIG_SETMASK, &iset, &oset);
int count=0;
sigset_t pending;
while (1)
{
sigemptyset(&pending);
sigpending(&pending);
show_pending(&pending);
sleep(1);
count++;
if(count==10)
{
sigprocmask(SIG_SETMASK,&oset,NULL);
// 2号信号的动作是默认终止进程
printf("恢复2号信号,可以被递达了");
}
}
return 0;
}
信号什么时候被处理?
因为信号是被保存在进程的PCB中的pending
位图里面的,处理包括检测和递达(三个动作:默认、忽略、自定义)。
处理是发生在进程从内核态返回用户态时进行处理工作!
用户态:就是用户代码和数据被访问或者执行的时候所处的状态。我们自己写的代码全部都是在用户态执行的,它是一种受监管的状态!
内核态:执行OS的代码和数据时,计算机所处的状态就叫做内核态。OS的代码的执行全部都是在内核态,它的权限很高!
主要区别:在于权限!
用户调用系统函数后,除了进入函数,身份也会发生变化,会从用户身份变成内核身份
从底层学习用户态和内核态的切换
用户的代码和数据一定要被加载到内存中,OS的代码和数据也会被加载进入内存中,我们此处假设计算机中只有一个CPU,那么内核页表是被所有进程所共享的!!!这样设计的话,我们的进程A能够看到自己的代码和数据,同时也能够看到操作系统的代码和数据!进程具有了地址空间是能够看到用户和内核的所有内容的,但是不一定能够访问。
CPU内有寄存器保存了当前进程的状态
用户态使用的是用户级页表,只能访问用户数据和代码
内核态使用的是内核级页表,只能访问内核级的数据和代码
总结:
1、进程之间无论如何切换,我们能够保证我们一定能够找到同一个OS,因为我们每个进程都有3~4G的地址空间,使用同一张内核页表
2、所谓的系统调用:就是进程的身份转化成为内核,然后根据内核页表找到系统函数,执行就行了
3、在大部分情况下,实际上我们的操作系统都是可以在进程的上下文中直接运行的
为了方便理解和记忆信号的处理过程,我们可以把模型简化成为无穷大的样子:
为什么一定要切换回用户态才能执行信号的捕捉?
理论上操作系统是能够直接执行用户的代码的!但是OS因为身份特殊,不能直接执行用户的代码。
与signal函数差不多,它的主要功能是修改handler函数指针数组
函数名称 | sigaction |
---|---|
函数功能 | 信号处理 |
头文件 | #include |
函数原型 | int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact); |
参数 | signum:要处理的信号 act:信号处理函数 oldact:之前的信号处理函数 |
返回值 | 0:成功 -1:出错 |
看一看结构体内容:
struct sigaction {
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t*, void*);//实时信号,我们不关心
sigset_t sa_mask;
int sa_flags;// 不用关心此选项,设置为0
void (*sa_restorer)(void);// 实时信号相关,也不用关心
};
在以上的类型定义中,
sa_handler
或者sa_sigaction
用于接收信号处理函数,两者使用其一即可,当信号处理函数需要接收附加信息时,必须给sa_sigaction
赋予信号处理函数指针,同时还要将sa_flags
置为SA_SIGINFO
,如果程序只需要接收信号,而不需要接收额外信息时,那将函数指针赋值给sa_handler即可。
将sa_handler
赋值为常数SIG_IGN
传给sigaction表示忽略信号,赋值为常数SIG_DFL
表示执行系统默认动作,赋值为一个函数指针表示用自定义函数(handler
)捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
void handler(int signo)
{
printf("get a signal:%d\n", signo);
}
// sigaction函数的使用
int main()
{
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = handler; // 自定义捕捉(去执行handler方法)
// act.sa_handler=SIG_DFL; // 默认动作(终止进程)
// act.sa_handler=SIG_IGN; // 忽略动作(什么都不做)
// 本质是修改当前进程的handler函数指针数组的特定的内容
sigaction(2, &act, NULL);
while (1)
{
printf("hello world!\n");
sleep(1);
}
return 0;
}
核心功能:sa_mask
表示在响应信号,进入信号处理函后,进程需要阻塞对于哪些信号的响应,可以将该信号添加到sa_mask
信号集中去。
成员sa_mask
用来定义在执行信号处理函数时要阻塞的信号集合,sa_mask
成员类型为sigset_t
,即信号集类型,需要使用信号操作集函数来操作它,它的每一位代表一种信号。
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用
sa_mask
字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。sa_flags
字段包含一些选项,本篇的代码都把sa_flags
设为0,sa_sigaction
是实时信号的处理函数,我们不用关心。
void handler(int signo)
{
while(1){
printf("get a signal:%d\n", signo);
sleep(1);
}
}
int main()
{
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
// 捕捉2号信号时,将3号信号屏蔽掉
// 向sa_mask信号集中添加3号信号
sigaddset(&act.sa_mask,3);
// 本质是修改当前进程的handler函数指针数组的特定的内容
sigaction(2, &act, NULL);
while (1)
{
printf("hello world!\n");
sleep(1);
}
return 0;
}
1、main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数。
2、sighandler也调用insert函数向同一个链表head中插入节点node2
3、插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
insert函数被不同的控制流调用,像这样的有可能在第一次调用还没返回时就再次进入该函数,我们将这种现象称之为重入函数。
不可被重入函数: insert函数一旦重入,有可能出现问题
可重入函数:insert函数一旦重入,不会出现问题
我们所学到的大部分函数,STL, boost库中的函数,大部分都是不可重入的!(这是一个中性词)
volatile
的作用:保持内存的可见性!告诉编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中操作,读取必须贯穿式的读取内存,不要读取中间缓冲区中寄存器区中的数据!
#include
#include
int flag=0;
void handler(int signo)
{
flag=1;
printf("change flag 0 to 1\n");
}
int main()
{
signal(2,handler);
while(!flag);
printf("this process quit normal!\n");
return 0;
}
标准情况下,键入ctrl+c
,2号信号被捕捉,执行自定义动作,修改flag=1,while循环条件不满足,退出循环,进程结束。
但是我们的编译器是可以有各种优化的:
我们这里使用-O3
优化一下
test_volatile:test_volatile.c
gcc -o $@ $^ -O3 //优化
.PHONY:clean
clean:
rm -f test_volatile
优化情况下,键入ctrl+c
,2号信号被捕捉,执行自定义动作,修改flag=1,但是while循环条件依旧满足,进程继续运行!但是很显然flag已经被修改了,这是什么原因呢?
编译器优化是不能够甄别代码中的多执行流的情况的,handler函数的执行流编译器是识别不到的,所以它只能识别在main执行流中,while循环只对flag做检测,它永远发现flag=0,编译器编译的时候就会将flag优化到CPU里面去,flag是一个变量,进程运行起来就要把flag加载进入内存,因为需要 !flag作逻辑运算,就需要CPU中的运算器,它检测到flag=0,循环永远继续。
我们优化完之后,就不会进行内存级别的访问了,而是直接访问CPU。
很显然,while循环检测的flag并不是内存中最新的flag,这就存在数据二义性的问题。(while检测的flag其实已经因为优化,被放在了CPU寄存器中),解决这个问题就需要我们的volatile
关键字
volatile int flag=0;
进程那一章学过用wait
和waitpid
函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一下,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD
信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD
信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程
#include
#include
#include
#include
void GetChild(int signo)
{
printf("get a signal:%d,pid:%d\n",signo,getpid());
}
int main()
{
signal(SIGCHLD,GetChild);
pid_t id=fork();
if(id==0)
{
//child
int cnt=5;
while(cnt--)
{
printf("I am child process:%d\n",getpid());
sleep(1);
}
exit(0);
}
//father
while(1);
return 0;
}
但是有时我们不想获取子进程的退出信息,父进程照常做自己的事情,我们可以用signal
将SIGCHLD
信号的处理动作设置为SIG_IGN
,这样fork
出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用signal函数自定义的忽略通常没有区别。但这是一个特例,此方法只在Linux下可用,但不保证在其他平台下可行。
void GetChild(int signo)
{
//waitpid();//可以在这里处理
printf("get a signal:%d,pid:%d\n",signo,getpid());
}
int main()
{
//signal(SIGCHLD,GetChild);
signal(SIGCHLD,SIG_IGN);// 显示的忽略17号信号,当进程退出后,自动释放僵尸进程
pid_t id=fork();
if(id==0)
{
//child
int cnt=5;
while(cnt--)
{
printf("I am child process:%d\n",getpid());
sleep(1);
}
exit(0);
}
//father
while(1);
return 0;
}