在生活中有很多的信号在我们身边围绕,例如红绿灯,发令枪,上课铃等等
在接受到信号,我们可以做出三种动作 1.立马去做对应信号的事情 2.等一会再做,有自己的事情或者更重要的事情 3.直接忽视,不做
信号是给进程发送的 eg: kill -9 pid
进程本身是程序员编写的属性和逻辑的集合:所以进程有识别信号,认知到信号的功能
对于信号,进程必须要有保存信号的能力,就好比人记在脑子里一样
由此可以引出对于信号,可以做出三种反应:
1.默认动作 2.忽略动作 3.自定义动作
如下图,可以发现信号有 1-31号(普通信号) ,34-64号(实时信号)
对于进程,收到一个信号时候,只有两种可能 : 是否收到信号,所以可以用0和1来表示是否收到信号
仔细观察则会发现信号有64个,那么我们可以用位图的形式用一个保存
进程收到的信号保存在pcb的一个变量里,用比特位来表示第几个信号
struct task_struct{
......
unsigned int singal;
.......
}
给进程发送信号一定要经过系统调用,所以kill命令等都是OS提供的接口。
通过键盘发送
在运行进程的时候,终止进程一般用Ctrl+c
来终止当前进程,其实这个热键是OS进行对这个操作进行识别,然后再对对应的进程发送2号信号(SIGINT)
,从而终止当前进程
int kill(pid_t pid, int sig) ;
头文件
#include <sys/types.h> #include <signal.h>
功能: 对目标进程pid,发送sig号信号
为此,我们可以实现一个自己的kill命令,eg:
#include<iostream>
#include<cstdio>
#include<string>
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>
void UsPage(const std::string& pro){
//用户手册
std::cout<<"\nplease input: "<<pro<<" pid signal"<<std::endl;
}
// 输入格式为 ./mysignal pid signal
int main(int argc,char* argv[]){
if(argc!=3){
UsPage(argv[0]);
exit(0);
}
int id=atoi(argv[1]); //id为要杀死的进程的pid即为传的命令行的第二个参数
int sign=atoi(argv[2]); //对进程发送的信号sign
int res=kill(id,sign);
if(res==-1){
perror("kill: ");//打印错误信息
exit(-1);
}
return 0;
}
(1) 除0操作
当在代码中有除0操作的时侯,底层实际上是硬件计算溢出,CPU会异常,此时会给进程发送异常的信号,发送
发生除0操作系统会抛以下异常:Floating point exception
man 7 signal 查看信号时候可以发现为 : SIGFPE 8 Core Floating point exception,为8号信号
(2) 野指针异常
如果发生对于一个无效指针的访问,实际是进程地址空间发生对野指针的访问异常,进程地址空间是由页表映射到物理内存的,其中包括了一个MMU(内存管理单元),如果异常,那么会给进程发送信号
野指针等越界错误会发生 Segmentation fault
man 7 signal 查看:SIGSEGV 11 Core Invalid memory reference
..........例子不止这两个,所以硬件也可以导致信号的产生
**(1)管道 **
在之前的管道学习中,假如有一个管道 : 关闭管道的读端,但是写端没有关,那么此时也会产生信号 ,因为操作系统不允许任何的资源浪费,所以这种关闭读,写不关就会造成信号的产生,从而终止程序(大部分信号都会终止)
(2) 闹钟
int alarm(unsigned int num) ;//num秒后关闭进程
其实这个闹钟底层就是在num秒后给进程发送一个14号信号,然后num秒后终止
SIGALRM 14 Term Timer signal from alarm(2)
..........例子不止这两个
signal()函数:
sighandler_t signal(int signum, sighandler_t handler);
其中第二个参数是一个函数指针类型,类型为 (*func)(int)
typedef void (*func)(int); //其中这个函数的参数为int型
设置处理信号的功能
指定使用sig指定的信号编号处理信号的方法。 参数func指定程序可以处理信号的三种方式之一:
这个就是自定义动作,我们自己定义改进程在遇到signnum
的信号的时候做什么操作
eg:
void catchSign(int signal){
std::cout<<"catach signal:"<<signal<<std::endl;
sleep(1);
}
int main(){
signal(11,catchSign);
int arr[100];
arr[10000000]=666;
return 0;
}
signal只是声明,本来遇到第num号信号应该是按系统指定的操作
signal函数之后就把遇到该信号的时候,做出自己定义的操作了,即上边的catchSign函数
所以当遇到改变操作的函数的信号的时候,才会触发自定义操作,并不是声明signal就会触发,如果没有遇到该信号甚至不会触发自己的自定义操作。
以上 31个普通信号,Core
和Term
类型的都会造成进程终止,但是不同的是,Core
会产生核心转储
在云服务器上如果进程是Core退出的
,没有明显的现象可以观察到,这是因为云服务器关闭了Core file的文件大小
如果要更改有效文件大小可以使用命令:ulimit -c 1024
,1024是设置的文件大小,可以设置为其他,-c是corefile 其他的也可以用后边那个-字母
设置
将core file设置大小后,再次遇到造成core退出的进程,会在进程目录下产生一个名为core.xxx
的文件
用gdb下输入: core-file core.xxx会出现以下内容 要生成debug的可执行程序才可以
则gdb会显示该进程异常的原因,该原因是野指针问题
首先要理解信号的保存要引入以下概念:
- 实际执行信号的处理过程称为递达
- 信号从产生到递达之间的状态称为未决
- 信号可以被阻塞
- 被阻塞的信号将处于未决状态,直到解除阻塞,才会执行递达的动作
所以信号是可以被阻塞的 , 也可以说某个信号可以被屏蔽
其实在pcb中信号采用位图的思想保存,这些所有的信号被称为信号集
在pcb
中信号的保存分为三个部分:
- 未决信号集
- 信号屏蔽集
- 信号集操作函数表
当屏蔽信号存在时,这个信号将不会被递达
pending : 00000000000000000000000
block : 00000000000000000000010 不会被递达
当屏蔽信号存在时,未决信号也存在,这个信号也不会被递达
pending : 00000000000000000000010
block : 00000000000000000000010 不会被递达
只有当未决信号存在,并且没有被屏蔽,才能被递达
pending : 00000000000000000000010
block : 00000000000000000000000 递达
首先要理解信号的捕捉流程需要理解以下两个内容:
用户态和内核态
在操作系统中,内存以及硬件等资源是由操作系统所维护的,正所谓操作系统不相信任何人,当我们进行申请系统资源的时候,其实进程会切换到内核态去进行一系列操作,从而申请资源
实际执行系统调用的人是”进程“,但是身份是内核 , 系统调用往往比较费时间所以尽量少的进行系统调用
那么切换状态的过程是什么呢?
在之前的进程地址空间中,了解到了用户有0-3G的地址空间,进程地址空间通过页表映射到物理内存中,从而实现cpu与进程交互
操作系统在开机时候也会加载到内存中,管理着整个电脑,整个问题毋庸置疑,所以操作系统也在内存中
而对于每个进程,剩余的3-4G地址空间,其实就是内核态的进程地址空间,称为内核空间,整个进程地址空间映射到物理内存中的操作系统的物理内存区。
由于每个进程固定的3-4G都是内核空间,而且操作系统只有一个,所以对于每个进程都会通过内核级页表映射到一个操作系统的物理内存处,内核级页表一个就够了。
当进程通过系统调用时候,系统调用会发出一个陷入指令,让cpu的CR3寄存器(该寄存器记录当前进程的运行级别0代表内核态,3代表用户态)标记为内核态,从而变为内核态,所以在用户态进行系统调用的时候,切换身份并且跳转在内核空间中执行有关操作,执行完后会发送指令再将CR3寄存器标记为用户态
信号的捕捉处理是在内核态返回用户态的时候进行的
首先我一定是进入了内核态,进入内核态的原因有很多 : 中断,系统调用,进程切换等。
当要返回到用户态时,既然进入内核态那么不容易,所以索性去检查一下进程是否收到了信号,因为是在内核态,所以pcb访问轻轻松松,然后检查是否有需要递达的信号
对于信号的处理有三种方式 : 1.默认动作 2.忽略动作 3.自定义动作
对于不同的处理方式也会有不同的流程
pending和bolck都是由一个信号集组成的,也就是位图的思想类型为sigset_t
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t;
所以定义一个信号集,可以 sigset block;
下边介绍对信号集的有关函数:
#include <signal.h>
int sigemptyset(sigset_t *set);
//初始化信号集,全置为0
int sigfillset(sigset_t *set);
//初始化信号集,使所有的信号都有,即为全1
int sigaddset (sigset_t *set, int signo);
//将信号集中添加signo号信号,即将set信号集中signo信号的位置置为1
int sigdelset(sigset_t *set, int signo);
//将set信号集中的signo号信号由1置为0,即去掉
int sigismember(const sigset_t *set, int signo);
//判断signo信号是否在set中出现,如果没有返回0,有的话返回1
sigprocmask函数 :
调用sigpromask函数可以读取或者更改当前进程中屏蔽信号集
#include<signal.h>
int sigprocmask(int how , const sigset_t *set , sigset_t *oset);
参数:
SIG_BLOCK : 希望添加的屏蔽字的信号,即 mask = mask|set
SIG_UNBLOCK : 希望解除屏蔽字的信号,即可mask = mask | ~set
SIG_SETMASK : 将当前进程的屏蔽字设置为set所指向的值
sigpending函数:读取当前进程的未决信号集,通过set参数传出
#include <signal.h>
int sigpending(sigset_t *set);
//读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
下面用刚学的几个函数做个实验。程序如下:
#include<iostream>
#include<vector>
#include<string>
#include<unistd.h>
#include<signal.h>
using namespace std;
vector<int> Blocks={2};//想屏蔽多少,假如数组中即可
string show_pending(sigset_t& pending){
string s;
int cnt=1;
for(int i=31;i>=1;i--){
//打印31个信号集
if(sigismember(&pending,i)){
//sigismember比较传参的信号集是否有第i个信号,有返回1,无返回0
s+='1';
}
else s+='0';
if(cnt++%4==0) s+=' ';
}
return s;
}
void hander(int sign){
//自定义捕捉函数
cout<<"捕捉到信号"<<sign<<endl;
}
int main(){
//1.初始化信号集
sigset_t block,oblock,pending;
sigemptyset(&block);
sigemptyset(&oblock);
sigemptyset(&pending);
//2.阻塞信号
for(const auto& e:Blocks) sigaddset(&block,e);
//3.设置屏蔽的信号
sigprocmask(SIG_SETMASK,&block,&oblock);
int num=1;
for(const auto&e:Blocks) signal(e,hander);
while(true){
//获取当前进程的pending表
sigpending(&pending);
//打印当前进程表
cout<<show_pending(pending)<<endl;
sleep(1);
if(num++==5){
//假设5秒后取消阻塞
sigprocmask(SIG_SETMASK,&oblock,&block);
//oblock就是保存的原来的,代表取消屏蔽,为了安全,将原来的block备份一下
}
}
return 0;
}