1.理解信号
1.信号:能够识别并做出行为的一种指令
2.信号来临的时候不一定能够立即对信号做出处理,但是并不影响信号的产生。
3.信号来临------>-------时间窗口------>------>被处理,结果是要处理的但是需要时间。
4.对待信号的处理方法:1.默认2.自定义3.忽略
5.因为对于来的信号可能不能立即做出处理所以信号要保存
2.信号的产生
查看系统定义的信号列表 kill -l
1. 通过键盘产生信号
程序正在运行时我们可以通过键盘向对应的进程发送信号
Ctrl + c //向进程发送二号信号,默认动作是结束该进程
Ctrl + \ //向进程发送三号信号 ,默认动作是结束进程
2.系统调用产生信号
1.kill 向任意进程发送信号
这里通过mysignal进程向test进程发送终止信号
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<string.h>
#include<sys/types.h>
#include<cstdio>
static void Usage(const std::string& proc)
{
std::cout<<"\nUsage: "<<proc<<"pid signo"<<std::endl;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(1);
}
pid_t pid=atoi(argv[1]);
int signo=atoi(argv[2]);
int n =kill(pid,signo);
if(n != 0)
{
perror("kill");
}
return 0;
}
test.cpp
#include<iostream>
#include<unistd.h>
#include<sys/types.h>
int main()
{
while(true)
{
std::cout<<"我是进程,我的pid是:"<<getpid()<<std::endl;
sleep(1);
}
return 0;
}
2.raise() 函数
该函数是自己的进程向自己的进程发送信号
#include<unistd.h>
#include<cstdio>
#include<signal.h>
int main()
{
//raise()给自己发送任意信号
int cnt = 0;
while(cnt<=10)
{
printf("cnt:%d\n",cnt++);
sleep(1);
if(cnt>=5)
{
raise(3); //kill(getpid(),signo);其实就是kill函数的封装
}
}
return 0;
}
3.abort()函数
函数会给自己发送六号信号
#include<cstdio>
#include<unistd.h>
#include<stdlib.h>
int main()
{
int cnt = 0;
while(cnt<=10)
{
printf("cnt:%d,pid:%d\n",cnt++,getpid());
sleep(1);
if(cnt>=5)
{
abort();//kill(getpid(),SIGABRT)
}
}
}
3.硬件异常产生信号
信号的产生不一定要我们给进程发送,程序运行中出现导致硬件异常的时候OS会进程发送信号
int main()
{
while(true)
{
cout<<"我在运行中..."<<endl;
sleep(1);
int a = 10;
a/=0;
}
return 0;
}
OS给进程发送了信号并且终止了进程,这个终止进程的行为时OS 默认的,下面用自定义函数的方法处理信号:
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
void catchSig(int signo)
{
//自定义处理信号的方法。。。。
cout<<"捕捉到一个信号,信号编号是:"<<signo<<endl;
}
int main()
{
signal(SIGFPE,catchSig);
int a = 10;
a/=0;
while(true)
{
cout<<"我在运行中..."<<endl;
sleep(1);
}
}
运行时该进程一直收到OS发送的8号信号,这种现象该如何理解?
CPU内有很多寄存器eax,edx等,执行int a=10,a/=0;CPU内除了数据保存,还得保证运算有没有问题,所以还有状态寄存器,状态寄存器衡量这次的运算结果,10/0.相当于10乘以无穷大,结果无穷大,引起状态寄存器溢出标记位由0变成1,CPU发生了运算异常,OS得知CPU发生运算异常,就要识别异常:状态寄存器的标记位置为1,由当前进程导致的,在向目标进程发送信号,最后就终止进程了。但是收到信号的处理方法是自定义处理的,所以说进程收到信号不一定会引起进程退出。进程没有退出,则还有可能还会被调度,CPU内部的寄存器只有一份,但是寄存器中的内容属于当前进程的上下文,一旦出现异常OS没有能力去修正这个问题,所以当进程被切换的时候,就有无数次状态寄存器被保存和恢复的过程,所以每一次恢复的时候就让OS识别到了CPU内部的状态寄存器中的溢出标志位是1。所以OS就会给进程发送信号,但是进程对待信号的处理动作是我们自定义完成的,自定义代码中并没有设置结束进程,OS再次调度时看到出错的进程还没有退出,所以又发送信号,周而复始,就能看到进程一直能捕捉到8号信号。
4.软件条件产生信号
1.在之前的管道学习中两个进程通过管道进行通信的时候,如果读端不继续在管道中读取内容时,而写端还在一直往管道中写数据时,写端就会收到OS发送的13号信号,终止写端进程。
2.定时器
int main()
{
alarm(10);
int cnt=1;
while(true)
{
cout<<"cnt:"<<cnt++<<endl;
sleep(1);
}
return 0;
}
在这里给程序设定了一个计时器,即倒数10秒钟,时间到了之后,OS会给该进程发送14号信号,导致进程终止了。计时器的本质是软件实现的所以这里的信号的产生是由软件条件产生的。
总结:
1.上面我一直都在说OS检测到了进程xxx,所以OS给进程发送了几号信号导致进程终止了。为什么都是OS去做这些??因为OS是进程的管理者
3.如果不能及时处理信号保存到哪里? PCB中
4.如何理解OS给进程发送信号?OS直接修改进程PCB中的关于信号的位图结构
3.核心转储
man -7 signal //详细的查看各种信号的相关信息
Term终止表正常终止,操作系统向进程发送信号后,该进程正常终止,不会作其他工作。而Core终止会做其他工作——核心转储,目的是进程崩溃后使用gdb调式时快速定位。由core类型的信号导致的进程终止运行时,将此时进程地址空间的内容以及有关进程状态的其他信息写出的一个磁盘文件。这种信息往往用于调试(事后调试)。文件名时core.12334(pid)
我使用的时云服务器,五哦一要打开core file选项
ulimit -a //查看各种选项的最大数量
ulimit -c 1024 //设置文件的大小
编译文件时加 -g选项代表可调式,若程序运行到某一步被OS发送信号终止了,此时在当前目录下就会出现core.12334(pid)文件,然后在命令行输入gdb a.out(可执行程序的文件名),接着输入 core-file core.12334 就可以快速的定位到导致程序挂了的地方。--------事后调试。
4.信号的保存
相关概念
实际执行信号的处理动作称为信号递达
信号从产生到递达之间的状态,称为信号未决
进程可以选择阻塞某个信号
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
在进程内部要保存信号的信息,有3种数据结构与之相关的,第一个是pending表,pending表就是位图结构。进程可能在任何时候收到OS给它发送的信号,该信号可能暂时不被处理,所以需要暂时被保存,进程为了保存信号采用位图来保存,这个位图结构就是pending位图,在没有收到信号时对应的位置就是0,当进程收到信号而现在不具备处理条件时就会先将信号保存起来即在对应的位置置1.所以OS向进程发信号就是向目标进程的peding位图设置比特位,从0到1就是当前进程收到该信号,所以发信号应该是写信号,PCB属于OS内核结构,只有OS有权力修改pending位图,所以发送信号的载体只能是OS。第二个是block位图:block位图比特位的位置代表信号标号,比特位的内容代表是否阻塞了该信号。还有一个是handler_t handler[32]={0},这个就是函数指针数组,这个数组在内核中有指针指向它,这个数组称为当前进程所匹配的信号递达的所有方法,数组是有下标的,数组的位置(下标)代表信号的编号,数组下标对应的内容表示对应信号的处理方法、
收到了信号但是还没有递达,就会保存到pending表中,如果信号在block表中被阻塞了,那这个信号即使在进程可以处理信号时也不会被递达。直到该信号不被阻塞时,进程才能够对信号做出处理。一个信号还没有产生但是并不影响它先被阻塞。
信号集
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。 阻塞信号集也叫做当 前进程的信号屏蔽字,这里的“屏蔽”应该理解为阻塞而不是忽略。
信号集操作函数
sigset_t 类型时让用户更好的设置pending表和block表
#include <signal.h>
int sigemptyset(sigset_t *set); //把信号集都设置为0
int sigfillset(sigset_t *set); //把信号集都设置位1
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 sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//返回值:若成功则为0,若出错则为-1
sigpending :
#include <signal.h>
sigpending
//读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
现在练习使用这些信号集函数实现用户自定义对某些信号的屏蔽并且查看屏蔽前后信号集的变化
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<vector>
using namespace std;
//#define BLOCK_SIGNAL 2
#define MAX_SIZE 31
// static vector<int> sigarr={2,3};
static vector<int> sigarr={2}; //添加要屏蔽的信号
static void show_pending(const sigset_t& pending) //打印pending表
{
for(int signo=MAX_SIZE;signo>=1;signo--)
{
if(sigismember(&pending,signo))
{
cout<<"1";
}
else
{
cout<<"0";
}
}
cout<<endl;
}
static void myhandler(int signo) //自定义处理信号的处理方法
{
cout<<"捕捉到一个信号,编号是:"<<signo<<endl;
}
int main()
{
for(const auto &sig : sigarr)
{
signal(sig,myhandler); //对信号的自定义捕捉
}
sigset_t pending,block,oblock; //pending表 ,block表,oblock表是返回的设置完block表返回设置之前的表
// 初始化
sigemptyset(&pending);
sigemptyset(&block);
sigemptyset(&oblock);
//添加要屏蔽的信号 屏蔽了信号无法被递达,被保存在pending表格中
// sigaddset(&block,BLOCK_SIGNAL);
//方法二: 多个信号屏蔽
for(const auto &sig : sigarr)
{
sigaddset(&block,sig);
}
//开始屏蔽并且设置到进程
sigprocmask(SIG_SETMASK,&block,&oblock); //选择屏蔽的方法,返回新的屏蔽信号集合,返回旧的信号屏蔽集
int cnt =10;
while(true)
{
//初始化s
sigemptyset(&pending);
//获取
sigpending(&pending);
//打印
show_pending(pending);
sleep(1);
if(cnt-- == 0)
{
cout<<"恢复了信号的屏蔽动作,现在开始不屏蔽任何信号11"<<endl; // 放在之前等打印出来,因为此时还没有对信号做出恢复
sigprocmask(SIG_SETMASK,&oblock,&block);
}
}
return 0;
}
结果:
程序开始运行时,我们将从键盘接受的Ctrl + c 信号即2号信号进行屏蔽,这里的屏蔽值得时将对应的信号添加到block表的位图结构中,这也再次验证了一个信号还没有被接收但是并不影响在接收之前对该信号的屏蔽。程序运行时循环打印block表,此时进程没有对任何信号进行屏蔽,在键盘上对进程发送了二号信号后,二号信号的位图结构由0置为1,但是该信号并没有立即进行递达,因为我们设定好了对于该信号屏蔽的动作,等屏蔽的时间一到,进程解除了对各种信号的屏蔽动作,此时,pending表中的二号信号的位图结构是1,而block表中二号信号的位图结构是0,所以该进程就要递达二号信号,而二号信号的处理动作时我们自定义的,所以就打印出了自定义函数中的内容。
5.信号的捕捉
前面说过,信号产生的时候,信号可能不会立即处理,会在合适的时候处理。合适的时候就是从内核态返回用户态的时候进程处理,这也说明了曾经一定是先进入了内核态,最典型的就是系统调用与进程切换
1.内核态与用户态
用户代码和内核代码,平时我们自己写的代码是属于用户态的代码,但是用户态难免会访问OS的资源(getpid,waitpid…),硬件资源(比如printf,write,red…),用户自己写的代码为了访问资源必须直接或间接访问OS提供的接口,必须通过系统调用来完成访问。系统调用是OS提供的接口,而普通用户不能以用户态的身份执行系统调用,必须让自己的身份变成内核态。身份切换成内核态才能切入到内核中访问内核的资源。实际执行系统调用是进程,但是身份其实是内核。从用户态调内核态需要身份的切换,还要调OS内部的代码,所以一般系统调用比较费时间一些。我们应该尽量避免频繁调用系统调用。
进程地址空间0-3G是用户级页表,通过用户级页表映射到不同的物理空间处,进程地址空间的3-4G是内核空间,OS为了维护从虚拟到物理之间的OS级别的代码所构成的内核级映射表,开机时OS加载到内存中,OS在物理内存中只会存在一份,因此OS的代码和数据在内存中只有独一份,所以内核级页表只有一份就够了。当进程需要访问内核的数据时通过内核级页表映射到物理内存中的OS的相关代码即可。内核空间是被映射到了每一个进程的3-4G空间,每一个进程都可以通过页表映射到OS,而且每个进程看到的OS都是一样的,所以进程要访问OS的接口,其实只需要在自己的地址空间上进行跳转就可以了。
2.信号的捕捉过程
通过系统调用,陷入内核,从用户态进入内核态,在内核态访问了内核的相关代码和数据后并不是直接返回用户态,陷入内核比较费时间,切入内核之后OS会做其他工作:信号的检测和信号的处理
进程的身份在转变为内核态后还要进行信号的检测,如果此时进程PCB中收到信号且没有阻塞该信号,那就会对该信号递达,此时的身份是内核态,内核态的身份比较高,如果用户自定义处理信号做出恶意操作那可能会导致系统的崩溃。所以为了安全要将身份切回到用户态再去执行对信号的处理动作,处理完成后身份再次切换到内核态,再次进行信号的检测和信号的处理,如果没有那就返回到用户态,继续代码的执行。
3.sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo
是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传
出该信号原来的处理动作。act和oact指向sigaction结构体:
这个函数的特点:
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来 的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。 如果 在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需 要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
函数的第二个参数是一个结构体,该结构体的名称和函数的名称是相同的,使用的时候设置好相关的内容即可:
练习使用该函数:
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void Count(int cnt)
{
while(cnt)
{
printf("cnt:%2d\r",cnt);
fflush(stdout);
cnt--;
sleep(1);
}
cout<<endl;
}
void handler(int n)
{
cout<<"get a signo:"<<n<<"正在处理中----"<<endl;
Count(20);
}
int main()
{
struct sigaction act,oact;
act.sa_handler=handler;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
//在处理二号信号期间 也可以选择屏蔽其他的信号
sigaddset(&act.sa_mask,3); //处理信号期间同时屏蔽三号信号
sigaction(SIGINT,&act,&oact); //二号信号的英文
while(1) sleep(1);
return 0;
}
连续向该进程发送两次二号信号和一次三号信号,通过结果可以发现程序最后的结果是执行两次自定义函数和一次三号信号的默认处理方法。进程第一次接收到二号信号时就去执行我们设定的自定义处理动作。若再次发送二号信号此时sigaction函数会导致进程屏蔽再次执行二号信号,即在该进程PCB中的pending表和block表中的二号信号的位图结构置为1,当第一次二号信号的自定义动作结束后会自动解除对二号信号的屏蔽,然后又去执行一次二号信号,此时三号信号的位图结构都为1,等二号信号处理完后又开始执行三号信号。如果信号的发送顺序是 2 3 2处理信号的顺序依旧是两次二号信号完了之后才执行三号信号。