信号介绍
生活中处处有信号,比如红绿灯,铃声,还有古代的狼烟等等,信号一出现,就提醒我们开始去做一些事情了,在计算机中也同样的是这个道理,信号出现,需要我们的一些进程去一些特定的事情
信号的产生
从键盘产生
下面写一个程序
cpp
#include <iostream>
int main()
{
while(true)
{
std::cout<<"hello world"<<std::endl;
}
return 0;
}
运行结果

从上面的结果来看,不断循环打印hello world,但是当我们按下ctrl c的时候,进程就终止了,ctrl c
就是一个来自于键盘的信号,但是我们有一点需要补充,它只能杀死前台进程,把这个进程变成后台进程之后,ctrl c就不起作用了,变成后台进程的命令
cpp
./可执行文件 &
现在运行这个命令

现在按下ctrl+c就杀不死这个后天进程了,现在想要杀这个进程,需要通过kill -9 pid的方式
重新打开一个终端,通过下列的命令查询进程pid
cpp
ps ajx|grep 进程名字
分别是任务编号和进程pid,现在我们可以将后台进程放到前台来,然后ctrl c就可以杀死进程了
信号的捕获
cpp
#include <signal.h>
typedef void (*sighandler_t)(int); //函数指针
sighandler_t signal(int signum, sighandler_t handler); //signum信号值
这个函数的作用是将信号捕获之后去执行对应的函数,不再执行默认的了,比如ctrl+c是终止一个前台进行,但是如果有了这个,可以自己定义接受信号之后行为
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
void die(int signal)
{
std::cout<<"I accept a signal:"<<signal<<std::endl;
}
int main()
{
signal(SIGINT,die);
while(true)
{
std::cout<<"hello world"<<std::endl;
sleep(1);
}
return 0;
}
运行结果

上面的实现了一个捕捉了信号之后的自定义行为,现在按下ctrl+c不再是终止进程了,而是去执行对应的函数,其中ctrl+c和ctrl+\对应信号分别为2号和3号信号,默认都是终止进程,还有就是整个进程中信号的捕获只需要设置一次就可以了

理解信号处理
当按下键盘的时候,引发中断,这个时候操作系统就会读取我们按下的值,然后发给对应的进程,在进程的PCB里面,有一个32位的位图,发送信号本质上就是给对应位置的比特位置1,在上面的图片里,最常用的信号就是前32个,正好对应了32个比特位
kill
cpp
#include <signal.h>
int kill(pid_t pid, int sig);
//成功返回0,失败返回-1
这个函数的作用是给指定的进程发送信号,下面实现一个函数
cpp
#include <iostream>
#include <signal.h>
int main(int argc,int *argv[])
{
if(argc!=3)
{
std::cout<<"usage: mykill signalnum process"<<std::endl;
return;
}
int signalnum=std::stoi(argv[1]);
int pid=std::stoi(argv[2]);
int n=::kill(pid,signalnum);
if(n<0)
{
perror("kill");
return;
}
return 0;
}
这个函数实现了一个自己的kill命令
alarm
cpp
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
这是一个闹钟函数,等待seconds就发送一个 SIGALRM信号,这个信号默认也是终止进程
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
int main()
{
alarm(2);
while(true)
{
std::cout<<"hello world"<<std::endl;
sleep(1);
}
return 0;
}
运行结果

异常
有时候我们只要出现野指针了对野指针进行访问就会出现崩溃的现象,以及整数除以0也会出现这样一个现象
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
int main()
{
int *pInt=nullptr;
*pInt=10;
return 0;
}
运行结果

这是因为当系统检测到了这样一个现象就会发出一个SIGSEVG的信号,可以查看一下这个信号对应的默认处理动作是什么,命令是man 7 signal

现在我们来捕获一下这个信号
cpp
#include <iostream>
#include <signal.h>
#include <unistd.h>
void die(int signalnum)
{
std::cout<<"I accept a signal:"<<signalnum<<std::endl;
sleep(1);
}
int main()
{
signal(SIGSEGV,die);
int *pInt=nullptr;
*pInt=10;
return 0;
}
运行结果

不应该语句只打印一次吗,为啥会出现死循环嘞,这是因为地址在进行页表转换的时候,内部有一个mmu寄存器,一旦出现野指针,里面的某一个标志位就会置成true,然后会被一直保存在这个硬件里面,我们调用了signal返回但是进程并没有退出,所以一直触发这个信号造成死循环
core Vs Term

有时候程序崩溃了会产生如上的信息,这是针对有些信号的

有些信号会产生core,有些信号会产生term,两个的区别就是core会在本地路径下会产生一个core文件,term不会,但是即便信号是core的有时候也不会产生core文件,这个时候需要我们把用户限制打开

有时候这里为的core文件大小为0,我们需要设置它的大小
cpp
ulimit -c 文件大小(比如1024,2048,有大小就可以)
运行程序之后我们就可以在当前路径下看见一个文件

有了这个文件,以后程序奔溃了,我们调试的时候就可以快速定位到这个崩溃点了
但是编译选项里面要加-g选项

然后调试就可以定位到崩溃信息了

信号其他相关常见概念
1.实际执行信号的处理动作称为信号抵达(Delivery)
2.信号从产生到递达之间的状态成为信号未决(Pending)
3.进程可以选择阻塞(Bloc)某个信号
4.被阻塞的信号产生的时候会将保持在未决状态,除非我们解除这个状态
在每一个进程当中,都有这样的一个表

其中前两个表都是位图,当block中对应位置的信号值为1的时候,表示这个信号被阻塞了,即便这个时候pending的信号值为1,信号都发不出去,然后就是第三个表handler,这个本质上是一个函数指针数组,当我们没有自定义信号处理的函数的时候,该表项中指向的是默认处理函数,一般都是终止进程,一旦我们自定义处理函数,里面的表项指向的就是我们自己定义的函数
sigset_t
从上图来看,每个信号只有⼀个bit的未决标志,⾮0即1,不记录该信号产⽣了多少次,阻塞标志也是这样表⽰的。因此,未决和阻塞标志可以⽤相同的数据类型sigset_t来存储,这个类型可以表⽰每个信号的"有效"或"⽆效"状态,在阻塞信号集中"有效"和"⽆效"的含义是该信号是否被阻塞,⽽在未决信号集中"有效"和"⽆效"的含义是该信号是否处于未决状态。
下面介绍几个函数
cpp
int sigemptyset(sigset_t *set); //将所有信号对应的比特位置为0
int sigfillset(sigset_t *set); //将所有信号对应的比特位置为1
int sigaddset(sigset_t *set, int signo); //将信号signo加入到信号集set中
int sigdelset(sigset_t *set, int signo); //删除set中的信号signo
int sigismember(const sigset_t *set, int signo); //判断信号signo是否在信号集里面
注意,在使⽤sigset_t类型的变量之前,⼀定要调⽤sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调⽤sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
前四个函数都是成功返回0,出错返回-1。sigismember是⼀个布尔函数,⽤于判断⼀个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
下面再介绍一个函数
cpp
#include <signal.h>
int sigprocmask(int how, const sigset_t *_Nullable restrict set,
sigset_t *_Nullable restrict oldset);
how的取值一共有三种
how - 指定操作类型:
SIG_BLOCK:将 set 中的信号添加到当前信号掩码,相当于mask=mask|set
SIG_UNBLOCK:从当前信号掩码中移除 set 中的信号,相当于mask=mask|(~set)
SIG_SETMASK:用 set 直接替换当前信号掩码,相当于mask=set
set - 要操作的信号集(可以为 NULL,此时 how 被忽略)
oldset - 用于保存修改前的信号掩码(可以为 NULL)
下面实现一个函数来阻塞二号信号
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void PrintPending(const sigset_t& pending)
{
for(int signo = 31;signo > 0;signo--)
{
if(sigismember(&pending,signo))
{
std::cout << 1;
}
else
{
std::cout << 0;
}
}
std::cout<<std::endl;
}
int main()
{
sigset_t block,oblock;
sigemptyset(&block);
sigemptyset(&oblock);
sigaddset(&block,2);
sigprocmask(SIG_SETMASK,&block,&oblock);
int cnt = 0;
while(true)
{
sigset_t pending;
sigpending(&pending);
cnt++;
//打印pending表
PrintPending(pending);
sleep(1);
//取消对2号信号的屏蔽
if(10 == cnt)
{
std::cout << "取消对2号信号的阻塞,cnt:" << cnt << std::endl;
sigprocmask(SIG_SETMASK,&oblock,nullptr);
}
}
return 0;
}
运行结果

操作系统信号处理流程

sigaction
cpp
#include <signal.h>
int sigaction(int signum,const struct sigaction *_Nullable restrict act,
struct sigaction *_Nullable restrict oldact);
中用于设置信号处理函数 的更强大、更可靠的函数,是 signal() 的替代品
其中,sigaction是一个结构体
cpp
struct sigaction
{
void (*sa_handler)(int); //自定义的处理函数
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask; //想要屏蔽的信号
int sa_flags;
void (*sa_restorer)(void);
};
其中,我们现在只需要关注的是上买按带注释的两个参数
cpp
#include <iostream>
#include <unistd.h>
#include <signal.h>
void hander(int signum)
{
std::cout << "我收到了" << signum << "信号" << std::endl;
}
int main()
{
struct sigaction actioon,oldaction;
actioon.sa_handler = hander;
sigemptyset(&actioon.sa_mask);
sigaddset(&actioon.sa_mask, 3); //在执行自定义函数时屏蔽一下三号信号,使用之前必须清空
sigaction(2, &actioon, &oldaction);
while(true)
{
std::cout << "hello world" << std::endl;
sleep(1);
}
return 0;
}
可重入函数

main函数调⽤insert函数向⼀个链表head中插⼊节点node1,插⼊操作分为两步,刚做完第⼀步的时候,因为硬件中断使进程切换到内核,再次回⽤⼾态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调⽤insert函数向同⼀个链表head中插⼊节点node2,插⼊操作的两步都做完之后从sighandler返回内核态,再次回到⽤⼾态就从main函数调⽤的insert函数中继续往下执⾏,先前做第⼀步之后被打断,现在继续做完第⼆步。结果是,main函数和sighandler先后向链表中插⼊两个节点,⽽最后只有⼀个节点真正插⼊链表中了。
像上例这样,insert函数被不同的控制流程调⽤,有可能在第⼀次调⽤还没返回时就再次进⼊该函
数,这称为重⼊,insert函数访问⼀个全局链表,有可能因为重⼊⽽造成错乱,像这样的函数称为不可
重⼊函数,反之,如果⼀个函数只访问⾃⼰的局部变量或参数,则称为可重⼊(Reentrant)函数。
volatile
cpp
#include <stdio.h>
#include <signal.h>
int flag = 0;
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
printf("process quit normal\n");
return 0;
}
在上面的函数当中,当我们按下ctrl+c的时候,flag变成1,while不满足条件,直接退出,但是当我们对程序编译的时候进行优化,就会出现不一样的现象

发现对程序编译的时候进行O1优化之后按下ctrl+c不再能退出程序了,原因如下:
当我们没有对程序进行优化的时候,CPU每次都是从内存当中取数据的,取完数据之后放到自己的寄存器中,但是当对程序进行优化的之后,CPU为了方便不再从从内存中取数据,直接从寄存器中取,但是寄存去中的一直是0,所以循环条件一直满足,为了解决这个问题,我们在前面加上一个
volatile关键字
cpp
#include <stdio.h>
#include <signal.h>
volatile int flag = 0; //这是一个易变变量
void handler(int sig)
{
printf("chage flag 0 to 1\n");
flag = 1;
}
int main()
{
signal(2, handler);
while(!flag);
printf("process quit normal\n");
return 0;
}
现在运行按下ctrl+c就能够直接退出了

SIGCHLD信号
进程⼀章讲过⽤wait和waitpid函数清理僵⼫进程,⽗进程可以阻塞等待⼦进程结束,也可以⾮阻塞地查询是否有⼦进程结束等待清理(也就是轮询的⽅式)。采⽤第⼀种⽅式,⽗进程阻塞了就不?能处理⾃⼰的⼯作了;采⽤第⼆种⽅式,⽗进程在处理⾃⼰的⼯作的同时还要记得时不时地轮询⼀?下,程序实现复杂。其实,⼦进程在终⽌时会给⽗进程发SIGCHLD信号,该信号的默认处理动作是忽略,⽗进程可以⾃定义SIGCHLD信号的处理函数,这样⽗进程只需专⼼处理⾃⼰的⼯作,不必关⼼⼦进程了,⼦进程终⽌时会通知⽗进程,⽗进程在信号处理函数中调⽤wait清理⼦进程即可。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{
pid_t id;
while( (id = waitpid(-1, NULL, WNOHANG)) > 0)
{
printf("wait child success: %d\n", id);
}
printf("child is quit! %d\n", getpid());
}
int main()
{
signal(SIGCHLD, handler);
pid_t cid;
if((cid = fork()) == 0)
{ //child
printf("child : %d\n", getpid());
sleep(3);
exit(1);
}
while(1)
{
printf("father proc is doing some thing!\n");
sleep(1);
}
return 0;
}
运行结果
