文章目录
- 一、信号是什么
- 1.生活中的信号
- 2.什么是Linux信号
- 3.信号处理的常见方式
- 4.Linux当中的信号
- 二、信号的产生
- 1.signal函数
- 2.核心转储
- 3.验证进程等待中的core dump标记位
- 三、信号的系统调用接口
- 1.kill
- 2.raise
- 3.abort
- 四、由软件条件产生信号
- alarm
- 五、硬件异常产生信号
- 1.除零异常
- 2.野指针(段错误11号信号)
- 六、阻塞信号
- sigset_t信号集
- 信号集的处理函数
- 1.sigpending
- 2.sigprocmask
- 七、信号处理
一、信号是什么
1.生活中的信号
信号和我们上一章节中刚说的信号量是没有任何关系的,食欲两套不同的体系。
信号属于通信范畴,信号量属于用于互斥和同步通信体系的。
在生活中有哪些跟信号相关的场景:
红绿灯、请求集合信号、短信的提示音、狼烟、
- 你为什么会知道这些信号呢?
因为你记住了这些对应场景下的信号+后序是有动作需要你执行的。
(闹钟响了,你就知道你需要起床了)
这样我们就能够识别这些信号
- 我们再我们的大脑中,能识别这个信号的。
- 如果特定的信号没有产生,但是我们依旧知道我们应该如何处理这个信号。
- 我在收到这个信号的时候,可能不会立即处理这个信号。
(外卖到了,单手我手头的活还没做完,我们就没有办法立即处理这个信号,我们还需要等一等) - 信号本身在我们无法立即被处理的时候,也一定要先被临时地记住
(我们需要记着我们的外卖已经送到哪里了,也就是先记住这个信号)
2.什么是Linux信号
本质是一种通知机制。用户或者操作系统通过发送一定的信号,通知进程,某些事件已经发生,你可以在后序进行处理。
结合进程,信号结论
- 进程要处理信号,必须具备信号“识别”的能力(a.看到这个信号b.处理这个信号)
- 凭什么进程能够“识别”这个信号呢?
一定是在进程内部提前规定了这个信号应该如何被处理。 - 曾经我们使用过kill -9来杀死一个进程,本质就是对进程发送了9号新号来杀死进程,这里的9就是一个信号
- 信号是随机产生的,进程可能正在忙自己的事情。所以新号的处理可能不是立即处理的
- 信号会临时地记录一下对应的信号,方便后序的处理。
- 在什么时候处理呢?合适的时候
- 一般而言,信号的产生相对于进程而言是异步的
我们不妨编写一个死循环然后我们ctrl c一下,终止这个进程
#include<iostream>
#include<unistd.h>
using namespace std;
int main()
{
while(1)
{
cout<<"hello world"<<endl;
sleep(1);
}
return 0;
}
ctrl+c:本质就是向我们的进程发送2号信号,将其终止(进程退出了)。
3.信号处理的常见方式
- 默认的处理方式(每一种信号都有默认的处理动作,进程自带的,是程序要写好的逻辑)
- 忽略(闹钟响了,但是你还是不想醒来。)(将计算机中记住的信号忘掉)
- 自定义动作(捕捉信号)(闹钟响了,别人默认是起床,但是你想要跳一套广播体操,那这个就是自定义动作)
4.Linux当中的信号
查看Linux中对应的所有的信号
kill -l
一共是62个信号,没有32,33,没有0号信号
[1:31]号新号被称为普通信号
[34:64]信号中带有RT的称为实时信号
分时操作系统和实时操作系统
实时操作系统有严格的时序,需要立马严格地处理完成。
比方说汽车中的车载操作系统,有些操作系统是Linux,也会参与我们的汽车的操作,比方说刹车。
如果是分时操作系统,那么如果我们刹车的这个进程没有获得处理机的处理,那么久不会立马被刹车。
如果是实时操作系统的话,那么我们的刹车就会被立即执行。
查看每一个信号对应的具体的意义
man 7 signal
如何理解信号被进程保存呢?如何理解信号发送的本质?
a.什么信号
b.是否产生
进程必须具有保存信号的相关数据结构(位图结构,unisgned int用第几个比特位的位置表示第几个信号,0000 0010,比方说这个位图的从右往左第二个位置为1,表示2号新信号被接收到了)。
(信号本质都是给进程发送的)
位图在哪里保存呢?
进程的PCB内部会保存信号位图字段。
信号位图是在task_struct->task_struct属于内核数据结构->只有操作系统才有资格去修改操作系统内部的数据结构
所以所有的信号本质就是操作系统发送的,只有操作系统才有权限去修改PCB内部的相关字段。
信号发送的本质:OS向目标进程写信号,OS直接修改对应的PCB当中的指定的位图结构,完成发送信号的过程。
我们上面的组合键ctrl c就是发送了一个2号信号,那么我们如何理解组合键变成信号呢?
键盘的工作方式是通过:中断方式进行的
键盘可以识别abcd的字符,当然也能够是被组合键
当键盘识别到了ctrl c之后,OS解释组合键
OS查找进程列表,找到前台运行的进程
操作系统写入对应的信号到我们进程内部的位图结构当中。
(所以kill命令底层一定调用了系统接口)
二、信号的产生
1.signal函数
对特定的信号进行捕捉
signal
第一个参数:你要对哪一个信号进行捕捉
第二个参数:函数指针。
想一个函数传入另外一个函数的函数指针,就是回调函数。通过回调的方式,修改赌赢的信号捕捉方法。然后我们这个回调函数的参数为int,返回值类型为void。
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
void catchSig(int signum)
{
cout<<"进程捕捉到了一个信号,正在处理中"<<signum<<"Pid:"<<getpid()<<endl;
}
int main()
{
//系统中信号对应的名称
//只要我们收到了2号信号,就将这个信号传递给catchSig函数
signal(SIGINT,catchSig);
//系统中信号对应的编号
// signal(2,catchSig);
while(true)
{
cout<<"我是一个进程,我正在运行……,Pid:"<<getpid()<<endl;
sleep(1);
}
return 0;
}
我们发现我们ctrl+c之后,我们的程序并没有停止。
因为以前对于2号信号的处理动作就是终止这个进程,现在我们改成了执行对应的函数
所以我们的进程就不退出了。
特定信号的处理动作,一般只有一个。
signal函数仅仅是修改进程对特定信号的后序处理动作,不是直接调用对应的处理动作。
(比方说我们上面的signal函数是写在最前面的,但并不是运行到这一行就直接调用我们的catchSig函数,而是只有捕捉到2号信号的时候才会调用我们的catchSig函数)
(如果我们没有发送这个信号,我们的这个catchSig函数就不会调用)
signal一般都是写在前面,就好比是我们先注册了一个方法。
向进程发送三号信号,同样也能让我们的进程退出
ctrl \
我们同样可以处理我们的3号信号
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
void catchSig(int signum)
{
cout<<"进程捕捉到了一个信号,正在处理中: "<<signum<<"Pid:"<<getpid()<<endl;
}
int main()
{
//系统中信号对应的名称
//只要我们收到了2号信号,就将这个信号传递给catchSig函数
signal(SIGINT,catchSig);
//ctrl \,处理三号信号
signal(SIGQUIT,catchSig);
while(true)
{
cout<<"我是一个进程,我正在运行……,Pid:"<<getpid()<<endl;
sleep(1);
}
return 0;
}
2.核心转储
我们刚刚观察到我们的2,3号信号都能让我们的进程停止,那么我们用
man 7 signal
查看到的这里信号的动作分别是term和core,这个有什么区别吗?
(ign是忽略,cont是继续)
这个core就是我们core dump中的一个标志位的不同,代表是否发生和核心转储
可以查看这篇博文waitpid获取子进程退出结果的部分
进程
一般而言,我们云服务器(生产环境)的核心转储功能是被关闭的
查看操作系统对于我们进程的限制。
ulimit -a
ulimit -c 1024
然后我们编写下面的程序
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
int main()
{
while(true)
{
cout<<"我是一个进程,我正在运行……,Pid:"<<getpid()<<endl;
sleep(1);
}
return 0;
}
将我们的程序运行起来之后,我们执行ctrl+\,也就是发送一个三号信号
会生成一个core文件
du -k core.5991
核心转储的意思就是当进程出现某种异常的时候,是否由OS将当前进程在内存中的相关核心数据,转存到磁盘中,也就是生成我们的core文件。
为什么要转储呢?
主要是为了调试。
如何进行调试定位到出错的位置
首先编译我们的程序,生成我们的core文件
程序如下
#include<iostream>
#include<unistd.h>
#include<signal.h>
using namespace std;
int main()
{
sleep(1);
int a=100;
a/=0;
cout<<"hello world"<<endl;
return 0;
}
然后使用gdb打开我们的程序
gdb signal
然后在我们的gdb中打开我们的core文件
core core.6828
浮点数错误
3.验证进程等待中的core dump标记位
所以我们这个core dump的标记位就是是否发生核心转储。
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
using namespace std;
int main()
{
pid_t id=fork();
//子进程制造一个除零错误(8号信号)
if(id==0)
{
sleep(1);
int a=100;
a/=0;
exit(0);
}
int status=0;
//以阻塞的方式进行等待
waitpid(id,&status,0);
cout<<"父进程:"<<getpid()<<"子进程:"<<id<<"exit sig: "<<(status&0x7F)<<"is core: "<< ((status>>7)&1)<<endl;
return 0;
}
也就是说,这个coredump标记位的意思就是当你子进程退出的时候,是否是用coredump的形式推出的。
那如果我们系统层面将我们的core dump给关闭的话,我们这里读取到的core就是0
ulimit -c 0
为什么生产环境一般要关闭core dump?
因为如果我们的生产环境中打开了core dump,那么可能就会生成大量的core文件,所以我们的磁盘非常容易被占满,然后导致我们的系统崩溃。
三、信号的系统调用接口
1.kill
编写一个程序向指定的进程发送指定的信号
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<string>
using namespace std;
// ./signal 2 pid
static void Usage(string proc)
{
cout<<"Usage:\r\n\t"<<proc<<" signumber processid"<<endl;
}
int main(int argc,char *argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(1);
}
//我们的信号
int signumber=atoi(argv[1]);
//我们的进程的pid
int procid=atoi(argv[2]);
kill(procid,signumber);
return 0;
}
首先我们让一个进程进行睡眠
然后我们查看这个进程的pid
然后我们调用我们刚刚的程序将我们的这个sleep进行关闭
2.raise
自己给自己发送信号
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<string>
using namespace std;
int main(int argc,char *argv[])
{
cout<<"我开始运行了"<<endl;
sleep(1);
raise(8);
return 0;
}
3.abort
给自己发送确定的abort信号,也就是自己终止自己,我们的6号信号就是这个功能。
可以理解成自己给自己发送了6号信号
raise(6)
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<stdlib.h>
using namespace std;
int main(int argc,char *argv[])
{
cout<<"我开始运行了"<<endl;
abort();
return 0;
}
我们的abort通常用来终止进程。
如何理解系统调用接口发送信号?
用户调用系统接口-> 执行OS对应的系统调用代码-> OS提取参数,或者设置特定的数值->
OS先目标进程写信号-> 修改对应进程的信号标记位-> 进程后续会处理信号->
执行对应的处理动作
四、由软件条件产生信号
管道中,读端不光不读,而且还关闭了,此时写端一直在写会发生什么问题?
写没有意义!OS会自动终止对应的写端进程,通过发送信号的方式,SIGPIPE
也就是我们的13号信号
1.创建匿名管道
2.让父进程进行读取,子进程进行写入
3.让我们的父进程关闭读端,并且waitpid()等待子进程,子进程只要一直写入就行
4.子进程退出,父进程waitpid拿到子进程的退出status
5.提取出退出信号
#include<iostream>
#include<unistd.h>
#include<assert.h>
#include<string>
#include<cstdio>
#include<cstring>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
int main()
{
//1.创建管道
int pipefd[2]={0};//pipefd[0]:读端,pipefd[1]:写端
int n=pipe(pipefd);
//在debug模式下assert是有效的,但是release版本下是会无效的
assert(n!=-1);
//所以我们这里需要写下面的代码,证明n被使用过
(void)n;
//如果是DEBUG模式下就不打印了,相当于就是注释掉了
#ifdef DEBUG
cout<<"pipefd[0]"<<pipefd[0]<<endl;
cout<<"pipefd[1]"<<pipefd[1]<<endl;
#endif
//2.创建子进程
pid_t id=fork();
assert(id!=-1);
if(id==0)
{
close(pipefd[0]);
string message="我是子进程,我正在给你发消息";
int count=0;
char send_buffer[1024];
while(true)
{
snprintf(send_buffer,sizeof(send_buffer),"%s[%d]:%d",message.c_str(),getpid(),count++);
write(pipefd[1],send_buffer,strlen(send_buffer));
sleep(1);
}
}
//父进程
close(pipefd[1]);
char buffer[1024];
//从0号文件描述符中读取,读取到缓冲区buffer中
size_t s=read(pipefd[0],buffer,sizeof(buffer)-1);
if(s>0)
{
//添加\0
buffer[s]=0;
cout<<"father get a message["<<getpid()<<"] Child#"<<buffer<<endl;
}
sleep(10);
close(pipefd[0]);
int status;
pid_t ret=waitpid(id,&status,0);
cout<<"退出码为:"<<status<<endl;
assert(ret<0);
(void)ret;
//子进程中的pipefd[0]关闭可写可不写,因为进程退出了,进程中的文件描述符也会被关掉
return 0;
}
我们的pipe在一端被关闭后,就没有通信功能了。
这就称为我们的软件条件不满足,于是我们的子进程就被终止了,也就是被发送了13号信号。
alarm
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<string>
#include<stdlib.h>
using namespace std;
int main(int argc,char *argv[])
{
//设置一个1秒的闹钟
//一秒之后给我们发送13号信号
//也就是验证1秒只能,我们一共会计算多少次count++
alarm(1);
int count=0;
while(true)
{
cout<<"count: "<<count++<<endl;
}
return 0;
}
为什么我们只运行了10w+次左右?
1.因为cout,要打印出来 2(云服务器)网络发送
也就是说要通过大量的IO和长距离传输
所以就会非常慢。
那我们如果想单纯地计算算力呢?
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<string>
#include<stdlib.h>
using namespace std;
uint64_t count=0;
void catchSig(int signum)
{
cout<<"final cout: "<<count<<endl;
}
int main(int argc,char *argv[])
{
//设置一个1秒的闹钟
//一秒之后给我们发送13号信号
alarm(1);//我们设定了一个闹钟,这个闹钟一旦触发,就自动移除了
signal(SIGALRM,catchSig);
while(true)
{count++;}
return 0;
}
我们设定了一个闹钟,这个闹钟一旦触发,就自动移除了
我们可以周期性地定闹钟
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<string>
#include<stdlib.h>
using namespace std;
uint64_t count=0;
void catchSig(int signum)
{
cout<<"final cout: "<<count<<endl;
alarm(1);
}
int main(int argc,char *argv[])
{
//设置一个1秒的闹钟
//一秒之后给我们发送13号信号
alarm(1);//我们设定了一个闹钟,这个闹钟一旦触发,就自动移除了
signal(SIGALRM,catchSig);
while(true)
{count++;}
return 0;
}
或者我们可以设置一个任务列表,周期性地执行任务
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<string>
#include<stdlib.h>
#include<vector>
#include<functional>
#include<sys/wait.h>
using namespace std;
typedef function<void ()> func;
vector<func> callbacks;
uint64_t count=0;
void showCount()
{
cout<<"final cout: "<<count<<endl;
}
void showLog()
{
cout<<"这个是日志功能"<<endl;
}
void loguser()
{
if(fork()==0)
{
execl("/usr/bin/who","who","-a",nullptr);
exit(1);
}
wait(nullptr);
}
void catchSig(int signum)
{
for(auto&f:callbacks)
{
f();
}
alarm(1);
}
int main(int argc,char *argv[])
{
//设置一个1秒的闹钟
//一秒之后给我们发送13号信号
alarm(1);//我们设定了一个闹钟,这个闹钟一旦触发,就自动移除了
signal(SIGALRM,catchSig);
callbacks.push_back(showCount);
callbacks.push_back(showLog);
callbacks.push_back(loguser);
while(true){count++;}
return 0;
}
如何理解软件条件给进程发送信号
a.OS先识别到某种软件条件触发或者不满足
b.OS构建信号,发送给指定的进程。
五、硬件异常产生信号
1.除零异常
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<stdlib.h>
#include<functional>
#include<sys/wait.h>
using namespace std;
void handler(int signum)
{
sleep(1);
cout<<"获得了一个信号:"<<signum<<endl;
//
}
int main(int argc,char *argv[])
{
signal(SIGFPE,handler);
int a=100;
a/=0;
while(true) sleep(1);
return 0;
}
除零错误给我们发送了8号信号,但是为什么我们的进程循环打印8号信号呢?
如何理解这里的除0呢?
为什么只有除0会被我们的操作系统发现,除2除3不会吗?
1.进行计算的是CPU,这个是硬件
2.CPU内部是有寄存器的,状态寄存器(不进行数值保存),用来保存本次计算的计算状态
(有没有出现进位,有没有出现溢出)
3.状态寄存器里面有对应的状态标记位,溢出标记位,操作系统会进行计算完毕之后的检测。
如果溢出标记位是1,操作系统里面识别到有溢出问题,立即只要找到当前谁在运行提取pid
操作系统完成信号发送的过程,进程会在合适的时候,进行处理。
一旦出现硬件异常,进程一定会退出码?
不一定。
因为硬件异常的默认行为是退出,但是如果你捕捉了这个异常就不会退出了。
但是溢出标记位是由CPU维护的
所以即便我们不退出,我们也做不了什么。
我们只能打印这个错误,然后进行退出。
那为什么会出现死循环呢?
我们的异常的退出变成了打印错误信号,但是不退出。
但是寄存器中的异常一直没有被解决!那我们的操作系统同会对其进行调度
那么所以会一直给打我们打印错误信号,除非我们将我们的进程退出。
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<stdlib.h>
#include<functional>
#include<sys/wait.h>
using namespace std;
void handler(int signum)
{
sleep(1);
cout<<"获得了一个信号:"<<signum<<endl;
exit(1);
}
int main(int argc,char *argv[])
{
signal(SIGFPE,handler);
int a=100;
a/=0;
while(true) sleep(1);
return 0;
}
2.野指针(段错误11号信号)
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<stdlib.h>
#include<functional>
#include<sys/wait.h>
using namespace std;
int main(int argc,char *argv[])
{
int *p=nullptr;
*p=100;
while(true) sleep(1);
return 0;
}
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
#include<stdlib.h>
#include<functional>
#include<sys/wait.h>
using namespace std;
void handler(int signum)
{
sleep(1);
cout<<"获得了一个信号:"<<signum<<endl;
exit(1);
}
int main(int argc,char *argv[])
{
signal(SIGSEGV,handler);
int *p=nullptr;
*p=100;
while(true) sleep(1);
return 0;
}
如何理解野指针或者越界问题?
1.都必须通过地址,找到目标位置
2.我们语言上面的地址,全部都是虚拟地址
3.将虚拟地址转成物理地址
4.页表+MMU(Memory Manager Unit)(内存管理单元,这是一个硬件!)
5.野指针,或者是越界->非法地址->在MMU转化的时候,一定会报错!
6.操作系统将这个报错进行捕获。
所以说:所有的信号,都有他的来源,但最终全部都是被OS识别,解释并发送的!
上面所说的所有信号产生,最终都要有OS来进行执行,为什么?OS是进程的管理者
信号的处理是否是立即处理的?在合适的时候
六、阻塞信号
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)。(信号存在但是没有被处理)
- 进程可以选择阻塞 (Block )某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
进程PCB内部就有三张表
pending表:表中是无符号整数。其中为1的代表收到了该信号,信号为0代表没有收到该信号,也就是我们上面所说的位图结构
handler表:表中填充的全部都是函数的地址,当我们的进程收到了一个信号,我们只要按照这个信号的编号就能够在handler表中找到我们信号的对应的处理方法(在上面我们的代码中有实验。)
typedef(*hander_t)(int);
handler_t handler[32]; //函数指针数组,数组的下标就是信号的编号。
signal(signum,handler);//这个就是将我们的handler函数的指针填到我们这个数组的下标为signum的位置。
但是这样仅仅是满足了信号的自定义呀,那信号的忽略和默认应该怎么解决呢?
#include<iostream>
#include<signal.h>
int main()
{
//信号默认0
signal(2,SIG_DFL);
//信号忽略1
signal(2,SIG_IGN);
}
所以操作系统会先识别你的信号编号sigal
handler[signal]
进行强制类型转换
(int)handler[signal]==0;//执行默认动作,done结束(我们上面的SIG_DFL就是0)
(int)handler[signal]==1;//执行忽略动作,done结束(我们上面的SIG_IGN就是1)
如果上面两个都没有匹配上的话,就执行我们自定义的处理方法,调用我们的函数。
handler[signal]();
block表:block表中也是位图,这个结构跟我们的pending表的结构一模一样,里面也全部都是无符号整数。但是位图中的内容代表的含义是对应的信号是否被阻塞。
操作系统给我们的pending位图发送信号
->处理信号-> 查看pending位图中哪些位置为1
->看看对应的block是否为1,如果为1就是被屏蔽了,不处理
->如果没有被屏蔽,那么我们再去handler表的对应位置查找对应的信号的处理方法
也就是说:pending->block->handler
sigset_t信号集
基本上,语言会给我们提供.h,.hpp和语言的自定义类型(语言类的类型可能会包含系统提供的类型)
同时OS也会给我们提供.h和OS自定义的类型
sigset_t :是一种位图结构,也是操作系统给我们提供的一种类型,不允许用户自己进行位操作。操作系统给我们提供了对应的操作位图的方法
sigset_t :user是可以直接使用该类型的,和用内置类型和自定义类型没有任何差别
sigset_t :一定需要对应的系统接口来完成对应的功能,其中系统调用接口需要的参数,可能就包含了sigset_t定义的变量或者对象。
信号集的处理函数
#include <signal.h>
int sigemptyset(sigset_t *set);
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);
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。
初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号
1.sigpending
检查pending信号,也就是获取当前调用进程的pending信号集
返回值成功就是0,失败就是-1
2.sigprocmask
检查并且更改我们的阻塞信号集,能够对我们的block的信号集进行获取和更改
这个how参数有下面几个选择:
sigset_t *oldset是一个输出型参数,比方说你想对2,5,8信号进行屏蔽,但当你屏蔽完成,之后想要恢复的时候,就可以用到这个。
它会返回旧的信号屏蔽字
1.如果我们队所有的信号都进行了自定义捕捉–我们是不是就写了一个不会被异常或者用户杀掉的进程?可以吗?
#include<iostream>
#include<unistd.h>
#include<signal.h>
void catchSig(int signum)
{
std::cout<<"获得了一个信号:"<<signum<<std::endl;
}
int main()
{
for(int i=1;i<=31;i++) signal(i,catchSig);
while(true) sleep(1);
}
我们发现我们的9号信号依旧能够杀死我们的进程
9号信号属于管理员信号,是不能够被捕捉的!
2.如果我们将2号信号block,并且不断获取并且打印当前进程的pending信号集,如果我们突然发送一个2号信号,我们就应该看到pending信号集中,有一个比特位0->1(该信号一直被阻塞,得不到处理,所以一直是没有被处理的状态,也就是1)
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<assert.h>
static void showPending(sigset_t &pending)
{
for(int sig=1;sig<=31;sig++)
{
if(sigismember(&pending,sig)) std::cout<<"1";
else std::cout<<"0";
}
std::cout<<std::endl;
}
static void handler(int signum)
{
std::cout<<"捕捉信号:"<<signum<<std::endl;
}
int main()
{
//0.方便测试,捕捉2号信号,不要退出
signal(2,handler);
//1.定义两个信号集对象(在栈区开辟了空间)
sigset_t bset,obset;
sigset_t pending;
//2.初始化
sigemptyset(&bset);
sigemptyset(&obset);
sigemptyset(&pending);
//3.添加要进行屏蔽的信号
sigaddset(&bset,2 /*SIGINT*/);
//4.设置到对应的进程内部(默认进程不会对任何信号进行block)
int n=sigprocmask(SIG_BLOCK,&bset,&obset);
//assert是一个宏,在release版本是无效的,所以要定义一个void(n);
assert(n==0);
(void)n;
std::cout<<"block 2号信号成功…… pid:"<<getpid()<<std::endl;
//5.重复打印当前进程的pending信号集
int count=0;
while(true)
{
//5.1获取当前进程的pending信号集
sigpending(&pending);
//5.2显示pending信号集中没有被递达的信号
showPending(pending);
sleep(1);
count++;
if(count==20)
{
//默认情况下,恢复对于2号信号的block的时候,确实会进行递达
//但是2号信号的默认处理动作是终止进程!
//需要对2号信号进行捕捉
std::cout<<"解除对于2号信号的block"<<std::endl;
int n=sigprocmask(SIG_SETMASK,&obset,nullptr);
//assert是一个宏,在release版本是无效的,所以要定义一个(void)n;
assert(n==0);
(void)n;
}
}
return 0;
}
貌似没有一个接口用来设置pending位图(所有的信号发送方式,都是修改pending位图的过程),我们是可以获取的sigpending
3.如果我们对所有的信号都进行block,那么我们是不是就写了一个不会被异常或者用户杀掉的进程?可以吗
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<assert.h>
static void showPending(sigset_t &pending)
{
for(int sig=1;sig<=31;sig++)
{
if(sigismember(&pending,sig)) std::cout<<"1";
else std::cout<<"0";
}
std::cout<<std::endl;
}
static void handler(int signum)
{
std::cout<<"捕捉信号:"<<signum<<std::endl;
}
//对指定信号进行屏蔽
static void blockSig(int sig)
{
sigset_t bset;
sigaddset(&bset,sig);
int n=sigprocmask(SIG_BLOCK,&bset,nullptr);
assert(n==0);
(void)n;
}
int main()
{
for(int sig=1;sig<=31;sig++)
{
blockSig(sig);
}
sigset_t pending;
sigemptyset(&pending);
while(true)
{
sigpending(&pending);
showPending(pending);
sleep(1);
}
return 0;
}
编写并且运行上面的程序之后,我们输入下面的bash命令,向我们的进程发送1-31号信号,看看会发生什么情况
i=1;id=$(pidof signal); while [ $i -le 31 ]; do kill -$i $id ; echo "send signal $i" ;let i++; sleep 1;done
这里我们不妨跳过9和19信号,来查看一下我们别的信号会发生什么情况
编写下面的bash脚本,运行。
#! /bin/bash
i=1
id=$(pidof signal)
while [ $i -le 31 ]
do
if [ $i -eq 9 ];then
let i++
continue
fi
if [ $i -eq 19 ];then
let i++
continue
fi
kill -$i $id
echo "kill - $i $id"
let i++
sleep 1
done
我们发现9,19信号会杀死我们的进程,或者让我们的进程stop
我们的20号信号在pending中不会变成1
七、信号处理
信号产生之后,信号可能无法立即被处理,在合适的时候(是什么?)
1.在合适的时候(是什么?)
信号相关的数据字段都是在进程PCB内部,属于内核的范畴。
内核态 vs 用户态
只有在内核态,从内核态返回用户态的时候,才进行信号检测和处理。
我为什么会进入内核态呢?
进行系统调用,缺陷陷阱异常等。
在汇编语言上有一个中断编号int 80,内置在我们的系统调用函数中。
如果存在一个open的系统调用,我们只需要在内核级页表中查找到对应的方法就可以了
内核也是在所有进程的地址空间上下文中跑的
那么我们可以执行进程切换的代码吗?
当然可以。
操作系统直接在进程地址空间中找到对应的进程,将其数据保存,然后切换上我们想要执行的代码。
我凭什么有权利执行OS的代码呢?
凭的就是我们处于内核态还是用户态。
cpu内的寄存器分为两类,一套是可见的,一套是cpu不可见的(自用的)
cpu中有一个CR3寄存器,其中有若干个比特位表示当前CPU的执行权限,
比方说用1表示内核,3表示用户态
所以我们调我们上面介绍的寄存器指令int 80的时候,我们就会修改这个寄存器中的这个标志位。
然后我们就可以访问内核级页表,然后进行内核级操作了。