信号概念:
是进程
间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。
我们可以通过命令kill -l来查看所有的信号

哈哈哈哈乍一看会以为是64个信号,但是仔细看没有31和32信号,所以其实只有62个信号啦
产生信号的方式
-
用户在终端下按下某些键的时候,就会给正在运行的进程发送一个信号,例如 当我们按下ctrl+c的时候,就会产生一个SIGINT信号,然后按下一个ctrl+z会产生一个SIHTSTP,ctrl+\会产生SIGQUIT信号
-
硬件异常产生信号:当我们运行一个除以0的程序的时候,CPU的运算器会产生异常,内核将这个异常解释为SIGFPE信号发送给进程,再比如当我们访问非法内存时,MMU会产生异常,内核将此异常解释为一个SIGSEGV信号发送给进程
-
用系统函数kill 可以给一个进程发送信号
调用系统函数给进程发送信号
kill(给一个指定的进程发送信号) -信号的标号或者信号 进程的pid,raise(给当前进程发送信号),就不需要指定pid了直接发送信号
例:kill -9 24344
由软件条件产生信号
我们了解一下SIGALRM信号,alarm函数就可以产生SIGALRM信号,通过alarm设置一个闹钟就是告诉内核多长时间之后会给进程发送SIGALRM信号,该信号的默认动作是终止进程
然后我们可以写一个代码来测试一下
#include<stdio.h>
#include<unistd.h>
int main()
{
size_t count = 0;
//让程序执行三秒之后发送一个SIGALRM信号,让程序返回
alarm(3);
while(1)
{
count += 1;
printf("%lu ",count);
}
return 0;
}
我们会发现程序运行3秒后直接退出,就是进程在3秒后收到了SIGALRM信号使进程退出
阻塞信号
信号的其他相关概念
-
实际执行信号的处理动作叫做信号递达
-
信号从产生到递达状态称为信号未决
-
进程可以选择阻塞哪些信号
-
被阻塞的信号会一直处于未决状态,直到进程解决对信号的阻塞状态,信号才会执行递达的动作

void MyHandler(int sig)
{
printf("sig = %d",sig);
}
void PrintSigset(sigset_t *set)
{
int i = 1;
for(; i<=31 ;i++)
{
if(sigismember(set,i))
{
printf("1");
}
else{
printf("0");
}
}
printf("\n");
}
int main()
{
//捕捉SIGINT信号
signal(SIGINT,MyHandler);
//吧SIGINT信号屏蔽掉
sigset_t set;
sigset_t oset;
sigemptyset(&set);
sigaddset(&set,SIGINT);
sigprocmask(SIG_BLOCK,&set,&oset);
//循环读取,未决信号集
while(1)
{
sigset_t pending_set;
sigpending(&pending_set);
PrintSigset(&set);
sleep(1);
}
return 0;
}
我们在这定义了一个未决信号集,然后循环的读取此位图,当我们收到了一个SIGINT信号之后在循环读取我们就会发现第二位的位图变成了1
因为当我们一旦将2号信号屏蔽了之后我们的阻塞信号集的第二位的位图就会被置1,所以一旦我们在给此进程发送2号信号的话那么此进程的未决信号集的第二位的位图就也会变成1
在这里我们重点的了解一下这个信号的捕捉!
我们画个图了解一下
举个栗子,
我们知道当我们解引用一个NULL指针的时候会发生段错误,那其实是,操作系统在执行代码的时候,硬件设备MMU发现了错误后进行入内核,进行处理给进程发送一个11号信号,告诉进程发生了段错误,进程执行相应的处理函数一般是默认退出,所以程序直接崩溃退出
可重入函数
一个函数被不同的控制流执行,有可能在一个控制流还没有结束的时候,另一个控制流就再次进入了该进程,这函数就是可冲入函数
如果一个函数满足如下条件之一就成为不可重入函数
-
调用了malloc/free,malloc用全局链表来管理堆的
-
调用了标准I/O库函数
volatile限定符
保证内存的可见性
先看下面的代码
#include<stdio.h>
#include<signal.h>
//保证内存的可见性
volatile int g_val = 1;
void MyHandler(int sig)
{
(void) sig;
g_val = 0;
}
int main()
{
//信号处理函数
signal(SIGINT,MyHandler);
while(g_val);
return 0;
}
当我们捕捉到SIGINT信号的时候,进程退出,而当我们将编译器的优化级别开到最大O3的时候,我们会发现即使我们收到了SIGINT信号进程也不会退出,那这是为什么呢?
其实当我们将编译器的优化级别设到最高的时候,编译器一次将g_val的值读入内存的时候,他就不会再去内存读取这个值,因为一次一次的内存访问效率特别低,而且如果程序是单一的执行流的话,这个值根本是不会在改变的,那么编译器没有必要再一次一次的去内存在读取这个值,而且还会提高程序运行的效率,那么,为了解决这个问题,
c语言就有了一个限定符volatile,保证内存的可见性,那么即使制定了优化级别,也不会优化掉对此变量的读写
SIGCHLD信号
如果我们一下创建多个子进程,然后父进程wait子进程的时候,如果同时有多个子进程完成,那么父进程只会处理一个子进程其他的都会变成僵尸进程,那其实我们呢就可以用waitpid函数来一个一个非阻塞式的等待,然后当许多子进程同时完成时,调用waitpid函数一个一个的回收,然后继续等待下一次SIGCHLD信号的到达,然后继续调用信号处理函数处理子进程
void MyHandler(int sig)
{
(void) sig;
//int ret = wait(NULL);
//循环处理当前一起结束的进程
while(1)
{
int ret = waitpid(-1,NULL,WNOHANG);
if(ret > 0)
{
printf("waitpid %d\n",ret);
continue;
}
//还有其他的未结束的进程,直接返回,因为接下来的还有其他的进程会触发此处理函数
else if(ret == 0)
{
break;
}
//子进程都结束了
else{
break;
}
}
}
int main()
{
//忽略掉SINCHID信号,自动销毁子进程
signal(SIGCHLD,MyHandler);
int i = 0;
for(;i<20;i++)
{
pid_t ret = fork();
if(ret < 0)
{
perror("fork error\n");
return 1;
}
//子进程打印pid
if(ret == 0)
{
printf("child = %d\n",getpid());
sleep(3);
exit(0);
}
//父进程直接执行下一次循环
}
while(1)
{
printf("father work!\n");
sleep(1);
}
return 0;
}
但是其实这种方法还不是很优雅,我们可以用一种更优雅的方式,
我们可知直接把SIGCHLD信号忽略掉,然后进程就会自动的将这些子进程销毁掉
int main()
{
//忽略掉SINCHID信号,自动销毁子进程
signal(SIGCHLD,SIG_IGN);
int i = 0;
for(;i<20;i++)
{
pid_t ret = fork();
if(ret < 0)
{
perror("fork error\n");
return 1;
}
//子进程打印pid
if(ret == 0)
{
printf("child = %d\n",getpid());
sleep(3);
exit(0);
}
//父进程直接执行下一次循环
}
while(1)
{
printf("father work!\n");
sleep(1);
}
}