信号
什么是信号
用户或者操作系统通过发送一定的信号,通知进程,让进程做出相应的处理,这就是信号
进程要处理信号,必须要具有识别他的能力
信号产生之后,进程可以找个时间进行处理,不需要立即进行处理——那么此时我们就要记录下来这个信号——记录这个信号我们可以用位图结构
常见的信号:
1到31为普通信号
34到64为实时信号
每个信号其实就是一个宏,它有自己对应的值
这里的Core
为核心转储
信号如何产生
键盘产生
核心转储
我们在学习进程等待的时候,当一个进程被杀死的时候,第8位为core dump标志,为是发生核心转储。
一般而言,云服务器(生产环境)的核心转储功能是被关闭的
用命令ulimit -a
进行查看,可以用ulimit -c 数字
进行修改
它会产生一个文件,文件名为core.进程pid
主要是为了调试
核心转储演示:
int main()
{
pid_t pid=fork();
if(pid==0)
{
int i=0;
while(true)
{
cout<<"我是子进程:"<<getpid()<<endl;
sleep(1);
if(i==10)
{
int a=1;
a/=0;
}
++i;
}
}
int stat;
waitpid(pid,&stat,0);
//提取code dump
cout<<"我是父进程:"<<getpid()<<"是否发生核心转储:"<<((stat>>7)&1)<<endl;
return 0;
}
就会发现有这个,除0,发送的8号信号,浮点错误,行为会发送核心转储。
调用系统函数向进程发信号
当我们在命令行上输入kill命令的时候,其实是调用的kill函数实现的,kill函数可以向指定进程发送信号。
成功返回0,失败返回-1。
模拟一个像命令一样的程序
int main(int argc, char *argv[])
{
if(argc!=3)
{
cout<<"命令输入有误"<<endl;
exit(1);
}
kill(atoi(argv[2]),atoi(argv[1]));
return 0;
}
还有raise
函数
它的作用就是给当前进程发送信号
还有一个abort
函数
就像exit
函数一样,abort
函数其实发送的是6号信号。
如何理解系统调用接口向进程发送信号:
由软件条件产生信号
在管道中,如果读端不读而且关闭,写端一直写,那么就会被os会自动终止写端进程,发送的是SIGPIPE信号
下面就进行验证:
int main()
{
int fd[2];
int ret=pipe(fd);
if(ret!=0)
exit(-1);
pid_t pid=fork();
//child
if (pid == 0)
{
close(fd[0]);
for (int i = 0; i < 100000; i++)
write(fd[1], "h", 1);
exit(0);
}
close(fd[0]);
close(fd[1]);
int stat;
waitpid(pid,&stat,0);
cout<<"退出信号:"<< (stat&0x0000007f)<<endl;
return 0;
}
看运行的结果:
[lighthouse@VM-4-8-centos 信号]$ ./signal退出信号:13
为什么是父进程读取呢?如果是父进程写,子进程读。因为子进程把读端关闭,父进程写没有意义,就会把父进程终止,那么我们无法拿到信号,而反过来就可以的。
还有一种由软件异常产生的信号,就是时钟信号,当时间到了,系统发送时钟信号SIGALRM
14号信号。
该函数
这个是设置的是秒级别的定时器。
如何理解软件条件给进程发送信号?
- os先识别某种软件条件触发或者不满足
- os构建信号,发送给指定的进程
硬件异常产生信号
除0发生的异常以及野指针、越界问题导致的硬件异常。
除0发送的是8号信号,越界、野指针发送的是11号信号
在cpu中有一个标志寄存器,当发生除0错误的时候,在寄存器中会进行标记,os会自动识别检测。当识别出有问题的时候,os会向当前进程发送信号,进程在合适的时候,进行处理
如何理解野指针、越界我问题导致的硬件异常?
- 都必须通过地址,找出目标位置
- 语言上面的地址,全部都是虚拟地址
- 将虚拟地址转换成物理地址,需要用到页表+MMU(这是一个硬件,分页内存管理单元)
- 转换的时候,MMU会发送异常,导致报错。
所有的信号,有他的来源,但最终全部被OS识别,解释,并发送的!
信号的处理
信号处理的常见方式:
- 默认的,进程自带的,也就是程序员写好的逻辑
- 忽略
- 自定义动作(捕捉信号)
对于自定义动作的演示
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handler(int signum)
{
cout<<"获得一个信号:"<<signum<<endl;
}
int main()
{
for(int i=1;i<32;i++)
{
signal(i,handler);
}
while(true)
{
cout<<"hello world "<<getpid()<<endl;
sleep(1);
}
return 0;
}
看似是把所有的信号都自定义捕捉了,但是当我们发生9号信号的,依然可以终止进程,os是不可能让一个进程无法终止的。
阻塞信号
一些常见的概念:
信号递达:执行信号的处理动作
信号未决(Pending):信号产生到递达之间的状态
阻塞:阻塞就是进销存阻塞该信号——阻塞信号集也叫做信号屏蔽字
当一个信号被阻塞的时候,当产生这个信号的时候,那么这个信号一直处于未决状态,直到进程解除对此信号的阻塞,才能执行递达的动作。阻塞和忽略是不同的,阻塞之后信号就不会被递达,而忽略是递达之后的一种处理动作
信号在内核中的样子:
在block中,0表示没有阻塞,1表示阻塞
在pending中,0表示没有接收到信号,1表示接受到信号
hander表示处理动作——忽略,默认,自定义
sigset_t类型
该类型不允许用户自己进行位的操作,os会给我们提供对应的操作位图的方法sigset_t一定需要对应的系统接口,来完成对应发功能。
像block,pending这样的信号我们用sigset_t来存储,sigset_t称为信号集,他的本质也就是一个位图结构。
对于这个类型的结构我们要学会怎么使用它,我们不需要关注它内部是怎么实现的。
下面是操作它的几个函数:
#include <signal.h>
int sigemptyset(sigset_t *set);//初始化,都清0
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);//检测信号集中是否包含该信号
上面这些函数虽然操作信号了,但是并没有在系统的角度设计信号,下面介绍几个系统接口来设置信号。sigprocmask
#include <signal.h>
int sigpromask(int how,const sigset_t* set,sigset_t* oset);
how的参数选择:
SIG_BLOCK
,把set里面的信号写入到系统中进行屏蔽SIG_UNBLOCK
,把set里面的信号从系统中解除屏蔽SIG_SETMASK
,把系统的屏蔽字设置成set指向的set为我们设置的信号屏蔽字,oset为我们旧的信号屏蔽字。对于返回值,成功为0,失败为-1。
sigpending
#include <signal.h>
int sigpending(sigset_t *set);
函数 sigpending 用于获取当前被阻塞且未决的信号集。这个函数可以用来查询那些在当前进程中被阻塞但尚未处理的信号。
返回值:成功返回0,失败返回-1。
信号的保存
脚本发送信号
信号发送的本质就是:OS向目标进程写信号,OS直接修改pcb中的指定位图结构,完成信号发送的过程
捕捉信号
信号产生之后,信号可能无法被立即处理,在合适的时候会被处理。
- 在合适的时候(是什么时候?)
- 信号处理的整个流程
下面对这两个问题进行回答:
- 与信号相关的数据字段都是在进程的PCB内部的,当接收到一个信号的时候,会从用户态切换到内核态,进行修改内核中有关信号的数据结构,之后会从内核态再次切换到用户态,在切换的时候(内核->用户),就会对信号进行检测和处理!因为此时已经处理好系统的各种逻辑了。
- 在CPU中也有2套,1套是可见的,一套是不可见的,用来自用的,这个自用的里面,其中有有关CR3的寄存器表示当前cpu的执行权限——1为内核,3为用户
内核是如何实现信号捕捉的?
- 捕捉信号
如果信号的处理动作是用户自己定义的,那么在信号递达的时候调用这个函数,这就称为捕捉信号。
信号捕捉的一个流程:
- 进程在执行的时候,由于中断、异常或者系统调用等原因进入内核开始执行代码
- 内核开始执行自己的代码,当处理完成的时候,准备返回用户态的时候
- 在返回用户态的时候,就会进行信号的处理,检查pending位图中,是否存在信号,如果不存在信号,之间返回用户态;如果存在,在去看它的block位图是否存在被阻塞,如果阻塞,那么也直接返回用户态;如果不阻塞,就进行信号的处理,信号处理有3种——忽略,默认,自定义捕捉。如果为忽略,把pending位图置成0,然后直接进行返回到用户层;如果为默认,把pending位图置成0,执行它的默认动作,如果有核心转储,就进行核心转储,然后杀掉进程;如果是自定义①,
①:因为自定义的代码在用户态,那么内核态可以执行用户态的代码吗?——是可以的,但是我们不能去执行,因为用户写的代码可以存在非法的情况,必须要切换到用户态去执行自定义捕捉的代码。在执行自定义代码的时候,在返回的时候会执行特殊的系统调用,然后再次进入内核。最后再从内核返回用户,执行用户的代码,在返回的时候还要在做检查,如果有其他信号,就按照上面的方式继续进行信号的处理;如果没有可以返回用户态了。
上面的信号处理的逻辑可以用下面的简图来解释:
sigaction函数
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
sigaction函数可以读取和修改与指定信号相关的处理动作。调用成功返回0,失败返回-1。
signo是指定的信号编号。 struct sigaction是一个结构体类型,用于定义信号处理程序的属性和行为 下面是这个结构体的内容
struct sigaction {
void (*sa_handler)(int);//信号处理动作
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;//信号屏蔽字
int sa_flags;
void (*sa_restorer)(void);
};
在这里,我们只需要了解
void (*sa_handler)(int);
和sigset_t sa_mas
即可,act为需要修改的信号动作,oact为旧的,可以联想sigpromask
处理信号的时候,执行自定义动作,如果在处理信号期间,又来了同样的信号,os将怎么办呢?
当某个信号的处理函数被调用时,内核自动将当前信号加入到进程的信号屏蔽字,处理完成后解除。这样就可以保证在处理该信号的时候,再次来相同的信号,就会被阻塞到当前处理结束为止。
验证一下:
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
void printsig(sigset_t &set)
{
for(int i=1;i<=31;i++)
{
if(sigismember(&set,i))
cout<<1;
else
cout<<0;
}
cout<<endl;
}
void handler(int sig)
{
cout<<"这是"<<sig<<"号信号"<<endl;
sigset_t pending;
int c=0;
while(true)
{
sigpending(&pending);
printsig(pending);
c++;
if(c==10)
break;
sleep(1);
}
}
int main()
{
struct sigaction act,oact;
act.sa_handler=handler;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,2);
sigaddset(&act.sa_mask,3);
sigaddset(&act.sa_mask,4);
sigaddset(&act.sa_mask,5);
//把2号设置到当前进程的pcb中,其中2号进程为自定义捕捉动作;3,4,5为默认的动作
sigaction(2,&act,&oact);
cout<<"进程pid:"<<getpid()<<endl;
while(true)
{
;
}
return 0;
}
可重入函数
什么叫做重入函数:同一个函数被多个执行流进入,那么这个函数就是重入函数
可以让多个执行流进入的重入函数叫做可重入函数;不可以让多个执行流进入的,叫做不可重入函数。
为什么不可以让多个执行流进入,就是因为不出现不好的结果。
可重入、不可重入是函数的一种特征。
比如一个函数实现链表的头插,那么这个函数就是不可重入函数
volatile
c语言中的关键字volatile
,它的作用就是保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在内存级别进行操作。
下面进行验证一下:
int flag=0;
void hendler(int sig)
{
cout<<"flag的值进行进行修改"<<flag;
flag=1;
cout<<"->"<<flag<<endl;
}
int main()
{
signal(2,hendler);
while(!flag);
cout<<"flag的最终值:"<<flag<<endl;
return 0;
}
当我们对上面的代码进行普通的编译g++ -o $@ $^ -std=c++11
它的运行结果如下:
[ml@VM-4-8-centos 信号]$ ./mysignal
^Cflag的值进行进行修改0->1
flag的最终值:1
当我们加入优化选项的时候g++ -o $@ $^ -std=c++11 -O3
它的运行结果如下:
[ml@VM-4-8-centos 信号]$ ./mysignal
^Cflag的值进行进行修改0->1
^Cflag的值进行进行修改1->1
^Cflag的值进行进行修改1->1
接收2号信号的时候,会陷入死循环
为什么会出现这种情况呢?
当加入优化选项之后,在循环中
flag
的值会存放在寄存器中,而改变flag值的时候,直接改的是内存中flag的值,寄存器中的值是木有改变的。所以会一直进入循环。下面我们对flag加上关键字volatile
的时候,对变量使用会从内存中进行找。下面是代码改变的部分volatile int flag=0;
,还是按照刚才的优化进行编译。结果如下:
[ml@VM-4-8-centos 信号]$ ./mysignal
^Cflag的值进行进行修改0->1
flag的最终值:1
SIGCHLD信号
当子进程退出或者被终止的时候,会给父进程发送SIGCHLD信号,该信号的动作是忽略——让子进程陷入僵尸状态。父进程可以自己定义SIGCHLD信号的处理函数,如果自己设置忽略,就会让子进程退出,和系统的忽略还是不一样的。
标签:函数,sigset,int,发送,信号,进程 From: https://blog.51cto.com/u_15869810/7922500