赞
踩
信号和我们上一章节中刚说的信号量是没有任何关系的,食欲两套不同的体系。
信号属于通信范畴,信号量属于用于互斥和同步通信体系的。
在生活中有哪些跟信号相关的场景:
红绿灯、请求集合信号、短信的提示音、狼烟、
因为你记住了这些对应场景下的信号+后序是有动作需要你执行的。
(闹钟响了,你就知道你需要起床了)
这样我们就能够识别这些信号
本质是一种通知机制。用户或者操作系统通过发送一定的信号,通知进程,某些事件已经发生,你可以在后序进行处理。
结合进程,信号结论
我们不妨编写一个死循环然后我们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号信号,将其终止(进程退出了)。
查看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命令底层一定调用了系统接口)
对特定的信号进行捕捉
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,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
浮点数错误
所以我们这个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文件,所以我们的磁盘非常容易被占满,然后导致我们的系统崩溃。
编写一个程序向指定的进程发送指定的信号
#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进行关闭
自己给自己发送信号
#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;
}
给自己发送确定的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号信号。
#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构建信号,发送给指定的进程。
#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; }
#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是进程的管理者
信号的处理是否是立即处理的?在合适的时候
进程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
基本上,语言会给我们提供.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在该信号集中添加或删除某种有效信号
检查pending信号,也就是获取当前调用进程的pending信号集
返回值成功就是0,失败就是-1
检查并且更改我们的阻塞信号集,能够对我们的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的时候,我们就会修改这个寄存器中的这个标志位。
然后我们就可以访问内核级页表,然后进行内核级操作了。
用户->内核,为什么?
有时候有一些功能在用户态是没办法执行的。
用户不能绕过操作系统直接去访问底层的硬件等资源。
所以必须要切换到内核态。
用户如何切换到内核态
用系统调用接口
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
执行对应的信号捕捉处理方法,我当前是什么状态
我们当前是kernel状态。也就是我们再进行信号检测之后,
还在内核态的时候,继续执行对应的处理方法
我当前的状态能不能执行user handler方法
我当前是内核态,是可以执行user handler方法的,我可以直接访问用户的0-3G空间的。
可以再用户级页表中找到对应的方法的。
OS能做到帮用户执行对应的handler方法,但是OS不愿意,也不想。
如果我们以OS的身份去执行handler方法,那如果我们的方法中有非法的操作呢?
操作系统不相信任何人,这个handler是用户写的,所以不能帮用户执行handler
(不能用内核态执行用户的代码)
执行用户态代码->陷入内核->处理信号->信号检测->处理handler方法->
再次进入内核,进行信号的一些收尾性工作(修改pending位图等等)->返回用户态,继续向后执行
这是我们之前使用的
signal(int,void*(int*));
sigaction也是一个信号捕捉的方法
这里的参数中的sigaction是一个结构体。
*act是一个输入型参数,用来传入新的action
*oldact是一个输出型参数,用来保存之前旧的action
sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。
signo是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。
act和oact指向sigaction结构体:
将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,
赋值为常数SIG_DFL表示执行系统默认动作,
赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,
该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,
这样就可以用同一个函数处理多种信号。
显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
struct sigaction {
void (*sa_handler)(int);//回调函数
void (*sa_sigaction)(int, siginfo_t *, void *);//暂时不考虑
sigset_t sa_mask;
int sa_flags;//暂时不考虑
void (*sa_restorer)(void);//暂时不考虑
};
基本的用法
#include<iostream> #include<signal.h> #include<unistd.h> using namespace std; void handler(int signum) { cout<<"获取了一个信号: "<< signum<<endl; } int main() { //内核数据类型,用户栈结构上定义的。也就是属于0-3G的空间中 struct sigaction act,oact; act.sa_flags=0; //初始化为空 sigemptyset (&act.sa_mask); act.sa_handler=handler; //设置进当前进程调用的pcb中 sigaction(2,&act,&oact); cout<<"default action: "<<(int)(oact.sa_handler)<<endl; while(true) sleep(1); return 0; }
#include<iostream> #include<signal.h> #include<unistd.h> using namespace std; void handler(int signum) { cout<<"获取了一个信号: "<< signum<<endl; } int main() { //这里我们将2号信号的处理动作变成SIG_IGN signal(2,SIG_IGN); //内核数据类型,用户栈结构上定义的。也就是属于0-3G的空间中 struct sigaction act,oact; act.sa_flags=0; //初始化为空 sigemptyset (&act.sa_mask); act.sa_handler=handler; //设置进当前进程调用的pcb中 sigaction(2,&act,&oact); cout<<"default action: "<<(int)(oact.sa_handler)<<endl; while(true) sleep(1); return 0; }
处理信号的时候,执行自定义动作,如果在处理信号期间,又来了同样的信号,OS如何处理?
Linux在任何时候只能处理一层信号。
本质:为什么要有block(信号屏蔽)?
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,
当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,
如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。
如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,
则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
sa_flags字段包含一些选项
也就是说正在处理2号信号的时候,再不断地传2号信号,是不会被处理的
#include<iostream> #include<signal.h> #include<unistd.h> using namespace std; void showPending(sigset_t *pending) { for(int sig=1;sig <=31;sig++) { if(sigismember(pending,sig)) cout<<"1"; else cout<<"0"; } cout<<endl; } void handler(int signum) { cout<<"获取了一个信号: "<< signum<<endl; sigset_t pending; int c=10; while(true) { sigpending(&pending); showPending(&pending); c--; if(!c) break; sleep(1); } } int main() { signal(2,SIG_DFL); //内核数据类型,用户栈结构上定义的。也就是属于0-3G的空间中 struct sigaction act,oact; act.sa_flags=0; //初始化为空 sigemptyset (&act.sa_mask); act.sa_handler=handler; //设置进当前进程调用的pcb中 sigaction(2,&act,&oact); cout<<"default action: "<<(int)(oact.sa_handler)<<endl; while(true) sleep(1); return 0; }
如何在处理一个信号的同时屏蔽别的信号
#include<iostream> #include<signal.h> #include<unistd.h> using namespace std; void showPending(sigset_t *pending) { for(int sig=1;sig <=31;sig++) { if(sigismember(pending,sig)) cout<<"1"; else cout<<"0"; } cout<<endl; } void handler(int signum) { cout<<"获取了一个信号: "<< signum<<endl; sigset_t pending; int c=20; while(true) { sigpending(&pending); showPending(&pending); c--; if(!c) break; sleep(1); } } int main() { cout<<"pid: "<<getpid()<<endl; signal(2,SIG_DFL); //内核数据类型,用户栈结构上定义的。也就是属于0-3G的空间中 struct sigaction act,oact; act.sa_flags=0; //初始化为空 sigemptyset (&act.sa_mask); act.sa_handler=handler; sigaddset(&act.sa_mask,3); sigaddset(&act.sa_mask,4); sigaddset(&act.sa_mask,5); sigaddset(&act.sa_mask,6); sigaddset(&act.sa_mask,7); //设置进当前进程调用的pcb中 sigaction(2,&act,&oact); cout<<"default action: "<<(int)(oact.sa_handler)<<endl; while(true) sleep(1); return 0; }
信号捕捉,并没有创建新的进程或者线程
这里我们假设我们的链表如下,是有头结点的
下面我们将我们的链表进行头插
main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
重入:在同一个时间,被多个执行流重复进入
可重入函数 vs 不可重入函数
是一种函数的特征,在不同的场景下有不同的应用。
目前我们使用的函数一般都是不可重入的函数。
如果一个函数符合以下条件之一则是不可重入的:
调用了malloc或free,因为malloc也是用全局链表来管理堆的。
调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
#include<iostream> #include<signal.h> #include<unistd.h> using namespace std; int flag=0; void changeFlag(int signum) { (void)signum; cout<<"change flag"<<flag; flag=1; cout<<"->"<<flag<<endl; } int main() { signal(2,changeFlag); while(!flag); cout<<"进程正常退出 : "<<flag<<endl; }
编译器有时候会自动地给我们进行代码优化
这里我们再编译的时候,指定更高的优化等级
g++ -o signal signal.cc -O3 -std=c++11
然后我们就会观察到下面的情况
如果我们不做优化,我们的edx就会正常地修改flag
但是如果我们进行了优化,我们就将我们的flag优化到了寄存器当中,但是我们后序的更改的都是内存中的flag,所以我们的优化导致了cpu无法看到内存中的情况了,所以就出现了我们上面的情况。
我们为了保证内存的可见性,我们就引入了volatile关键字,然后就能够解决我们这里的问题。
#include<iostream> #include<signal.h> #include<unistd.h> using namespace std; volatile int flag=0; void changeFlag(int signum) { (void)signum; cout<<"change flag"<<flag; flag=1; cout<<"->"<<flag<<endl; } int main() { signal(2,changeFlag); while(!flag); cout<<"进程正常退出 : "<<flag<<endl; }
这个编译器的优化发生在什么时候?
是在编译的时候还是在程序执行的过程中?
是在编译的时候进行优化的。程序执行的时候,只是按照编译的情况进行执行。
子进程退出会主动向父进程发送第17号信号,也就是我们的SIGCHLD信号
父进程在处理第17号信号的时候,默认是忽略
所以我们可以在子进程推出的时候,利用这个17号信号,来通知我们的父进程对子进程进行回收。
#include<iostream> #include<signal.h> #include<unistd.h> using namespace std; void handler(int signum) { cout<<"子进程退出: "<<signum<<endl; } int main() { signal(SIGCHLD,handler); if(fork()==0) { sleep(1); exit(0); } while(true) sleep(1); }
所以我们这里证明了子进程退出会向父进程发送退出信号。
验证是不是父进程接受了信号
#include<iostream> #include<signal.h> #include<unistd.h> using namespace std; void handler(int signum) { cout<<"子进程退出: "<<signum<<" father:"<<getpid()<<endl; } int main() { signal(SIGCHLD,handler); if(fork()==0) { cout<<"child pid :"<<getpid()<<endl; sleep(1); exit(0); } while(true) sleep(1); }
那如果我们有10个子进程想要回收呢?
我同一时刻收到了10个SIGCHLD信号,
但是我的pending位图里面只有一个比特位表示是否收到了这个信号。
如果同时传过来了,我们的pending位图只会收到一次这个信号
所以如果有10个进程,我根本就不知道是哪一个退出了!
所以我们采用了一个while循环,依次等待每一个进程,退出了就回收,没退出就等待(阻塞式地等待)
或者我们直接用waitpid(-1,NULL,WNOHANG),也就是等待任意一个进程退出,(非阻塞式地等待)
我们不关心是哪一个退出了。
如果我们不想等待子进程,并且我们还想让我们的子进程退出之后,自动释放僵尸进程
#include<iostream> #include<signal.h> #include<unistd.h> using namespace std; int main() { if(fork()==0) { cout<<"chils: "<<getpid()<<endl; sleep(5); exit(0); } while(true) { cout<<"parent: "<<getpid()<<" 执行我自己的任务!"<<endl; sleep(1); } }
由于我们的子进程退出的时候,资源没有被回收,我们的子进程就变成了僵尸进程。
方案一:编写循环等待我们的子进程退出
#include <stdio.h> #include <stdlib.h> #include <signal.h> #include <iostream> #include <unistd.h> #include<sys/types.h> #include<sys/wait.h> using namespace std; 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; }
方案二:在父进程中将17号信号进行忽略
编写脚本捕捉我们的进程
while :;do ps -axj|head -1 &&ps axj|grep signal ;sleep 1;done
#include<iostream> #include<signal.h> #include<unistd.h> using namespace std; int main() { signal(SIGCHLD,SIG_IGN);//手动设置对子进程进行忽略 if(fork()==0) { cout<<"chils: "<<getpid()<<endl; sleep(5); exit(0); } while(true) { cout<<"parent: "<<getpid()<<" 执行我自己的任务!"<<endl; sleep(1); } }
这样我们就不会有僵尸进程了
但是我们默认的第17号信号不也是将其忽略嘛?
但是操作系统默认也是忽略呀?
这个忽略跟我们手动进行忽略属于两个不同的级别。
我们操作系统级别的忽略就是默认的动作,该变成僵尸进程就变成僵尸进程,
操作系统并不知道你真的是不是要将其回收,所以就将子进程搁那里了,变成僵尸进程了。
如果我们自己设置忽略,就是用户告诉操作系统将其忽略,也就是直接告诉操作系统,
我忽略了,运行完毕你就直接回收吧。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。