文章目录
- 信号是什么
- 信号的产生
- 信号的系统调用接口
- 软件条件产生信号
- 硬件异常产生信号
- 阻塞信号
- 信号处理
一、信号是什么
1.生活中的信号
- 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时, 你该怎么处理快递。也就是你能“识别快递”
- 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。
- 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”
- 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:
- 1. 执行默认动作(幸福的打开快递,使用商品)
- 2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)
- 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
这里所说的信号与我们进程通信中的信号量时没有关系的,是两套不同的体系。信号是属于通信范畴,而信号量属于用于互斥和同步通信体系。
当我们知道了一些信号之后,需要记住这些对应场景下的信号和后序对应的执行动作。这样我们就可以识别出这些信号。
注意:
- 1.在我们的大脑中能够识别这个信号的。
- 2.如果特定的信号没有产生,但是我们依旧知道我们应该如何处理这个信号。
- 3.我们在接受这个信号的时候,可能不会立即处理这个信号。
- 4.信号本身在我们无法立即被处理的时候,也一定要先被临时地记住。
2.什么是Linux信号
本质上是一种通知机制。用户或者操作系统通过发送一定的信号,通知进程,某些事件已经发生了,可以在后序进行处理。
结合进程,信号结论:
- 1.进程要处理信号,必须具备信号“识别”的能力
- 1.要看到这个信号
- 2.要处理这个信号
- 2.进程内部提前规定了这个信号应该如何被处理才能够让进程识别这个信号。
- 3.利用kill -9来杀死一个进程,本质上就是对进程发送9号信号来杀死进程。
- 4.信号是随机产生的,进程可能正在忙自己的业务,所以信号的处理可能并不是被立即处理的。
- 5.信号会临时地记录一下对应的信号,一般在合适的时候会进行处理。
- 6.一般而言,对于进程而言信号的产生是异步的。
测试给进程发送信号代码:
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("hello world\n");
sleep(1);
}
return 0;
}
这里我们可以发现,利用组合间ctrl+c可以发现进程退出。本质上就是向该进程发送2号信号,将其终止。
3.前台进程和后台进程
- 前台进程:当前正在使用的程序
- 后台进程:在当前没有使用的但是也在运行的进程,包括那些系统隐蔽或者是没有打印的程序。后台进程运行时,可以运行其他前台进程。
注意:在一个bash终端里面只能有一个前台进程,而ctrl+c这种组合键产生的信号只能发送给前台进程,一个进程如果在后台运行,该进程收不到该信号。运行程序时最后加上一个&,让其在后台进行。
这里发现输入ctrl+c进程仍然进行,说明此时进程为后台进程。
4.信号处理的常见方式
- 1.默认的处理方式(每一个信号都有默认的处理动作,进程自带的,是程序要写好的逻辑)。
- 2.忽略(将计算机中记住的信号忘掉)
- 3.自定义动作,利用signal系统调用,提供一个信号处理函数,要求在内核处理该信号时切换到用户态执行这个函数,这种方式被称为捕捉一个信号。(signal函数是修改了当前进程对信号的处理方式,等收到改变的信号时,直接实行自定义的函数)
5.Linux当中的信号
查看Linux中对应的所有信号
kill -l
这里我们可以发现,总共有62种信号。1~31是普通信号,34~64是带有RT的实时信号。
分时操作系统和实时操作系统
分时操作系统:使一台计算机同时为几个、几十个甚至几百个用户服务的一种操作系统。把计算机与许多终端用户连接起来,分时操作系统将系统处理机时间与内存空间按一定的时间间隔,轮流地切换给各终端用户的程序使用(时间片的概念)。由于时间间隔很短,每个用户的感觉就像他独占计算机一样。
交互性:用户与系统进行人机对话。
多路性:多用户同时在各自终端上使用同一CPU。
独立性:用户可彼此独立操作,互不干扰,互不混淆。
及时性:用户在短时间内可得到系统的及时回答。
实时操作系统:实时操作系统(RTOS)是指当外界事件或数据产生时,能够接受并以足够快的速度予以处理,其处理的结果又能在规定的时间之内来控制生产过程或对处理系统作出快速响应,并控制所有实时任务协调一致运行的操作系统。其特点是及时响应和高可靠性。实时系统又分为硬实时系统和软实时系统,硬实时系统要求在规定的时间内必须完成操作,这是在操作系统设计时保证的;软实时则只要按照任务的优先级,尽可能快地完成操作即可。
多任务:由于真实世界的事件的异步性,能够运行许多并发进程或任务是很重要的。多任务提供了一个较好的对真实世界的匹配,因为它允许对应于许多外部事件的多线程执行。系统内核分配 CPU 给这些任务来获得并发性。
抢占调度:真实世界的事件具有继承的优先级,在分配CPU的时候要注意到这些优先级。基于优先级的抢占调度,任务都被指定了优先级,在能够执行的任务(没有被挂起或正在等待资源)中,优先级最高的任务被分配CPU资源。换句话说,当一个高优先级的任务变为可执行态,它会立即抢占当前正在运行的较低优先级的任务。
任务间的通讯与同步:在一个实时系统中,可能有许多任务作为一个应用的一部分执行。系统必须提供这些任务间的快速且功能强大的通信机制。内核也要提供为了有效地共享不可抢占的资源或临界区所需的同步机制。任务与中断之间的通信:尽管真实世界的事件通常作为中断方式到来,但为了提供有效的排队、优先化和减少中断延时,我们通常希望在任务级处理相应的工作。所以需要在任务级和中断级之间存在通信。
查看每一个信号对应的具体的意义:
man 7 signal
如何理解信号被进程保存?信号发送的本质?
进程必须具有保存信号的相关数据结构(位图结构,unsigned int 用第几个比特位的位置表示第几个信号,0000 0100表示这个位图的第3个信号被接受了。
进程的PCB结构体内部会保存信号位图字段。信号位图是在task_struct->task_struct是属于内核数据结构->只有操作系统才有资格去修改操作系统内部的数据结构。所以所有信号的本质都是操作系统发送的,只有操作系统才有权限去修改PCB内部的相关字段。
信号发送的本质:OS向目标进程写信号,OS直接修改对应的PCB当中的指定的位图结构,完成信号发送过程。
那么通过组合键是如何变成信号呢?
由于键盘是采用中断的方式进行工作的。当键盘识别了组合键之后,OS解释组合键,OS查找相应的进程列表,找到前台运行的进程,操作系统写入对应的信号到我们进程内部的位图结构中。(所以kill命令底层调用了系统调用接口)
二、信号的产生
1.signal函数
signal函数的作用是对特定的信号进行捕捉
- signum:要登记的信号值
- handler:信号处理函数指针,可以是自定义的信号处理函数,或者SIG_IGN(忽略信号),或者SIG_DFL(采用系统默认方式处理信号)。
测试代码:
#include <iostream>
#include <unistd.h>
#include <signal.h>
void catchSig(int signum)
{
std::cout<<"进程收到了一个信号,正在处理中:"<<signum<<"Pid:"<<getpid()<<std::endl;
}
int main()
{
//系统中信号对应的名称
//只要我们收到了2号信号,就将这个信号传送给catchSig这个回调函数中去
signal(SIGINT,catchSig);//signal(2,catchSig);
while(true){
std::cout<<"我是一个进程,我正在运行...........,PID:"<<getpid()<<std::endl;
sleep(1);
}
return 0;
}
这里我们可以发现,当进程运行起来,我们利用ctrl+c这种组合键的方式给进程发送2号信号,进程并没有退出,而是处理回调函数。但是我们可以利用ctrl+\这种组合键可以使我们进程退出,其实就是向进程发送3号信号。由于我们没有对3号信号进行相应的处理动作,而是使进程退出。
注意:
- 特定的信号的处理动作一般只有一个。
- signal函数仅仅是修改进程对特定信号的后续处理动作,不是直接调用对应的处理动作。(如果没有发送这个信号,该信号的回调函数也就不会被调用)
- signal函数一般都是写在前面。
测试代码:
处理3号信号。
#include <iostream>
#include <unistd.h>
#include <signal.h>
void catchSig(int signum)
{
std::cout<<"进程收到了一个信号,正在处理中:"<<signum<<"Pid:"<<getpid()<<std::endl;
}
int main()
{
//系统中信号对应的名称
//只要我们收到了2号信号,就将这个信号传送给catchSig这个回调函数中去
signal(SIGINT,catchSig);//signal(2,catchSig);
signal(SIGQUIT,catchSig);//signal(3,catchSig);
while(true){
std::cout<<"我是一个进程,我正在运行...........,PID:"<<getpid()<<std::endl;
sleep(1);
}
return 0;
}
我们这里发现,对于向该进程发送2号信号和3号信号该进程都无法退出,而是调用该进程的回调函数。
2.核心转储
利用 man 7 signal来查看
这里我们可以发现信号相应的动作分别为 Term、Core、Ign、Cont、Stop
Term:终止当前进程
Core:终止当前进程并产生Core文件(用于调试)
Ign:忽略该信号
Cont:继续执行先前暂停的进程
Stop:暂停当前进程
这里的Core就是core dump标志位,代表的是是否发生了核心转储。
核心转储:在Linux中,指在进程发生异常终止或崩溃时,将进程的内存空间中的数据和状态信息保存到一个特殊的文件中。(将当前进程在内存中的核心数据转储到磁盘中,也就是生成了core文件)
注意:对于我们的云服务(生产环境)的核心转储功能是关闭的。
查看操作系统对于我们进程的限制:
利用 ulimit -a命令来显示当前所有资源的限制
利用ulimit -c 1024来设置core文件的最大值。单位为blocks,默认是0
测试代码:
这里我们采用ctrl+\的这种方式像该进程发送3号信号,此时会生成一个core文件。
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{
while(true){
std::cout<<"我是一个进程,我正在运行.......,PID:"<<getpid()<<std::endl;
}
return 0;
}
利用du命令来查看当前文件占多少空间
测试代码:
利用生成的core文件来进行调试
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{
sleep(1);
int a=100;
a/=0;
std::cout<<"hello world"<<std::endl;
return 0;
}
利用gdb进行调试:在gdb中将此时的core文件进行打开。
3.core dump标志位
验证dore dump标志位代码:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
int main()
{
pid_t id=fork();
if(id==0){
sleep(1);
int a=100;
a/=0;
exit(0);
}
int status=0;
waitpid(id,&status,0);
std::cout<<"父进程:"<<getpid()<<"子进程:"<<id<<"exit sig:"<<(status&0x7F)<<"core is :"<<((status>>7)&1)<<std::endl;
return 0;
}
这里core dump标记为的意思是当子进程退出时,是否是用core dump标记位的形式推出的。如果我们从系统层面将core dump给关闭的话,那么此时core就是0。
为什么生产环境中需要关闭core dump?
由于生产环境中如果打开了core dump,那么此时就会产生大量的core文件,所以我们的磁盘非常容易慢,然后导致我们的系统崩溃。
三、信号的系统调用接口
1.kill函数
利用系统调用接口向指定进程发送指定信号测试代码:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <string>
static void Usage(std::string pro)
{
std::cout<<"Usage:\r\n\t"<<pro<<"signumber processid"<<std::endl;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(1);
}
int signalnumber=atoi(argv[1]);
int proceid=atoi(argv[2]);
kill(proceid,signalnumber);
return 0;
}
这里我们可以发现利用kill的系统调用对sleep进程发送2号信号使其从阻塞状态退出。
2.raise函数
raise函数的作用是自己给自己发送信号
测试代码:
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main(int argc,char* argv[])
{
std::cout<<"我开始运行了"<<std::endl;
sleep(1);
raise(8);
return 0;
}
3.abort函数
#include <stdlib.h>
void abort(void);
//就像exit函数一样,abort函数总是会成功的,所以没有返回值。
测试代码:
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <signal.h>
int main(int argc,char* argv[])
{
std::cout<<"我开始运行了"<<std::endl;
sleep(1);
abort();
return 0;
}
如何理解系统调用接口发送信号?
首先用户调用系统接口->执行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>
int main()
{
//1.创建管道
int pipefd[2]={0};//pipefd[0]表示读端,pipefd[1]表示写端
int n=pipe(pipefd);
assert(n!=-1);
(void)n;
#ifdef DEBUG
std::cout<<"pipefd[0]"<<pipefd[0]<<std::endl;
std::cout<<"pipefd[1]"<<pipefd[1]<<std::endl;
#endif
//2.创建子进程
pid_t id=fork();
assert(id!=-1);
if(id==0)
{
close(pipefd[0]);
std::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];
size_t s=read(pipefd[0],buffer,sizeof(buffer)-1);
if(s>0)
{
buffer[s]=0;
std::cout<<"father get a message["<<getpid()<<"] child#"<<buffer;
}
sleep(10);
close(pipefd[0]);
int status;
pid_t res=waitpid(id,&status,0);
std::cout<<"退出码为:"<<status<<std::endl;
assert(res<0);
(void)res;
return 0;
}
我们发现pipe在一端被关闭后,就没有了通信功能了,这种称为我们的软件条件不满足,于是我们的子进程就被终止了,也就是发送了13号信号。
1.alarm函数
- #include <unistd.h>
- unsigned int alarm(unsigned int seconds);
- 调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
测试代码:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <string>
#include <stdlib.h>
int main(int argc,char*argv[])
{
//设置一个1s的闹钟
//一秒之后发送13号信号
//也就是验证1s,我们能够计算多少次count++
alarm(1);
int count=0;
while(true)
{
std::cout<<"count:"<<count++<<std::endl;
}
return 0;
}
为什么1s中只能计算出这么多次的count?
由于需要进行打印操作,也就是需要通过大量的IO和长距离的传输,而且是在云服务器下,需要通过网络发送。
如何计算CPU的计算力呢?
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <string>
#include <stdlib.h>
typedef uint64_t i64;
i64 count=0;
void catchSig(int signum)
{
std::cout<<"final cout:"<<count<<std::endl;
}
int main(int argc,char*argv[])
{
//设置一个1s的闹钟
//一秒之后发送13号信号
//也就是验证1s,我们能够计算多少次count++
alarm(1);
signal(SIGALRM,catchSig);
while(true) count++;
return 0;
}
这里我们发现我们设置了1s的闹钟,然后进程收到了该信号,然后执行了回调函数,此时进程并没有退出,而是进行执行循环语句。所以我们可以设置周期性统计CPU的计算力。
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <string>
#include <stdlib.h>
typedef uint64_t i64;
i64 count=0;
void catchSig(int signum)
{
std::cout<<"final cout:"<<count<<std::endl;
alarm(1);
}
int main(int argc,char*argv[])
{
//设置一个1s的闹钟
//一秒之后发送13号信号
//也就是验证1s,我们能够计算多少次count++
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>
void handler(int signum)
{
sleep(1);
std::cout<<"获得了一个信号:"<<signum<<std::endl;
}
int main()
{
signal(SIGFPE,handler);
int a=100;
a/=0;
while(true) sleep(1);
return 0;
}
我们可以发现此时发生了除零错误发送了8号信号,但是为什么我们的进程循环打印8号信号呢?
如何理解这里的除零呢?
首先进行计算的是计算机里面的CPU,是一种硬件,其次CPU中有许多的寄存器,其中一个是叫状态寄存器(不进行数值保存),用来保存本次计算的计算状态(有没有出现进位,有没有出现溢出),状态寄存器里面有对应的状态标记位,溢出标记位,操作系统会进行计算完毕之后的检测。如果溢出标记位是1,操作系统里面识别到有溢出问题,立即只要找到当前谁在运行提取pid,操作系统完成信号发送的过程,进程会在合适的时候进行处理。
为什么会发生死循环呢?
由于我们的异常的退出变成了打印错误信号,进程不退出!但是寄存器中的异常一直没有被解决!那我们的操作系统会对其进行调度,那么所以会一直给我们打印错误信号,除非我们将进程退出。
注意:
如果出现了硬件异常,那么此时的进程不一定会退出,因为硬件异常的默认行为是退出,但是如果你捕捉了这个异常就不会退出了。但是溢出标记位是由CPU维护的,所以即便我们不退出,我们也做不了什么,只能打印这个错误,然后进行退出。
2.野指针
测试代码:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <stdlib.h>
#include <functional>
#include <sys/wait.h>
int main()
{
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>
void handler(int signum)
{
sleep(1);
std::cout<<"获得了一个信号:"<<signum<<std::endl;
exit(1);
}
int main()
{
signal(SIGSEGV,handler);
int *p=nullptr;
*p=100;
while(true) sleep(1);
return 0;
}
这里我们可以发现段错误本质上是给进程发送11号信号。
如何理解野指针或者越界问题?
- 1.首先都必须通过地址找到目标位置
- 2.我们在语言上面的地址全部都是虚拟地址
- 3.将虚拟地址转成物理地址
- 4.页表+MMU(Memory Manager Unit)(内存管理单元,这是一个硬件!)
- 5.野指针,或者是越界->非法地址->在MMU转化的时候一定会报错!
- 6.操作系统将这个报错进行捕获。
所有的信号,都有它的来源,但是最终都是被OS识别,解释并释发送的!
六、阻塞信号
1. 信号其他相关常见概念
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)。
- 进程可以选择阻塞 (Block )某个信号。
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
- 注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
2. 在内核中的表示
- 每个信号都有两个标志位分别表示阻塞(block)和未决(p产生时,内核在进程控制块中设置该信号的未决标志之中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认动作。
- SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。不能忽略这个信号,因为进程仍有机会改变处理动作。
- SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞, 如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
在进程PCB中存在如上三张表,分别为:
- pending表:表中是无符号整数。其中为1的代表收到了该信号,为0代表没有收到该信号,上述表结构也就是位图结构。
- handler表:表中填充的全部都是函数的地址,当我们的进程收到了一个信号,我们只要按照这个信号的编号就能够在handler表中找到我们信号对应的处理方法。
- block表:block表也是位图结构,这个结构和pending表示一样的,里面都是无符号整数。但是位图中的内容代表的含义是对应的信号是否被阻塞。
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);
return 0;
}
- 操作系统会先识别我们的信号编号signal
- handler[signal]
- 进行强制类型转换
- (int)handler[signal]==0//执行默认动作(SIG_DFL)
- (int)handler[signal]==1//执行忽略动作(SIG_IGN)
- 如果上面两个都没有匹配上的话,就执行我们自定义的处理方法,调用我们的函数。handler[signal]();
操作系统给我们的pending位图发送给信号->处理信号->查看pending位图中哪些位置为1->然后查看相应的block是否为1,如果为1说明该信号发生了屏蔽,不进行处理,如果没有被屏蔽,我们需要去相应的pending表的对应位置查找对应的信号的处理方法,也就是处理信号的发生逻辑为:
pending->block->handler
3.sigset_t信号集
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
基本上,在语言上会给我们提供.h,.hpp和语言的自定义类型(语言类的类型可能会包含系统提供的类型)。同时OS也会给我们提供.h和OS自定义的类型sigset_t:这是一种位图结构 ,也是操作系统给我们提供的一种类型,不允许用户自己进行位操作。操作系统给我们提供了对应的操作位图的方法。
- sigset_t:用户是可以直接使用该类型的,和用内置类型和自定义类型没有任何差别。一定需要对应的系统接口来完成对应的功能,其中系统调用接口需要的参数,可能就包含了sigset_t定义的变量或者对象。
4.信号集操作函数
#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在该信号集中添加或删除某种有效信号。
- 这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1。
1.sigpending
- #include <signal.h>
- sigpending :读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。 下面用刚学的几个函数做个实验。
- 程序如下:
#include <iostream>
#include <signal.h>
#include <unistd.h>
void printsigset(sigset_t *set)
{
for(int i=0;i<32;i++){
if(sigismember(set,i)) std::cout<<"1";
else std::cout<<"0";
}
std::cout<<std::endl;
}
int main()
{
//定义信号集对象并清空初始化
sigset_t s,p;
sigemptyset(&s);
//添加2号信号
sigaddset(&s,SIGINT);
//设置阻塞信号集,阻塞SIGINT信号
sigprocmask(SIG_BLOCK,&s,NULL);
while(true){
//获取未决信号集
sigpending(&p);
printsigset(&p);
sleep(1);
}
return 0;
}
2.sigprocmask
- #include <signal.h>
- int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
- 返回值:若成功则为0,若出错则为-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);
return 0;
}
这里我们发现我们的9号信号依旧能够杀死我们的进程,由于9号信号是属于管理员信号,是不能够被捕捉的。
测试代码二:
如果我们将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()
{
//1.方便调试,捕捉2号信号,不需要退出
signal(2,handler);
//1.定义两个信号集对象(在栈区开辟了空间)
sigset_t bset,obset;
sigset_t pending;
//2.初始化
sigemptyset(&bset);
sigemptyset(&obset);
sigemptyset(&pending);
//3.添加要进行屏蔽的信号
sigaddset(&bset,2);
//4.设置到对应的进程内部(默认进程不会对任何信号进程block)
int n=sigprocmask(SIG_BLOCK,&bset,&obset);
assert(n==0);
(void)n;
std::cout<<"block 2号信号成功.... pid:"<<getpid()<<std::endl;
//5.重复打印当前进程的pending信号集
int count=0;
while(true)
{
//1.获取当前进程的pending信号集
sigpending(&pending);
//2.显示pending信号集中没有被递达的信号
showPending(pending);
sleep(1);
count++;
if(count==20){
//默认情况下,恢复对于2号信号的block的时候,确实会进行递达
//但是2号信号的默认处理动作是终止进程
//需要对2号信号进行捕捉
std::cout<<"解除对于2号信号的block"<<std::endl;
//需要对2号信号进行捕捉
int n=sigprocmask(SIG_SETMASK,&obset,nullptr);
assert(n==0);
(void)n;
}
}
return 0;
}
注意:没有一个接口用来设置pending位图(所有的信号发送方式,都是修改pending位图的位置的过程),我们是可以获取的sigpending。
测试代码三:
如果我们对所有的信号都进行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脚本来跳过9号信号和19号信号,来查看一下其他信号会发生什么情况?
#! /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号进程进行处理,进程不会中断,而对于20号信号在pending中不会变成1。
七、信号处理
信号产生之后,信号可能无法立即被处理,而是在合适的时候进行处理!
什么是合适的时候?
- 信号相关的数据字段都是在进程PCB内部,属于内核范畴,只有在内核态,从内核态返回用户态时候,才进行信号检测和处理。因为在返回的时候,也就是说我们需要做的事情已经处理完成了。
为什么需要进入内核态?
- 由于需要进行相应的系统调用,缺陷陷阱异常等。
- 在汇编语言上有一个中断编号int80,内置在我们的系统调用函数中。
- 用户态:用户态时一个受管控的状态,受访问权限的约束,访问资源的限制。
- 内核态:内核态是OS执行自己代码的一个状态,内核态具备非常高的优先级基本不受任何资源和权限的约数。
如果正文代码中存在open调用,由于是系统调用,我们只需要在内核级页表中查找对应的方法。
- 注意:
- 内核也是在所有进程的地址空间上下文中跑的。
- OS直接在进程地址空间中找到对应的进程,将其数据保存,然后切换上我们想要执行的代码,这样我们就可以执行进程切换的代码了。
为什么可以有权利执行OS的代码呢?
- 由于我们处于内核态还是用户态。
- CPU内的寄存器分为两类,一类是可见的,另一类是不可见的(自用的)
- CPU中存在一个CR3寄存器,其中有若干个比特位表示当前CPU的执行权限,比如说用1表示内核,0表示用户态。所以说我们调用上面寄存器指令int80的时候,我们就会修改这个寄存器中的这个标志位。然后我们就可以访问内核级页表,然后进行内核级操作了。
用户态->内核态,为什么?
由于有时候一些功能在用户态时没有办法执行的。用户不能绕过操作系统直接去访问底层的硬件资源。所以必须要切换成内核态。通过相应的系统调用接口来解决。
1.信号捕捉
注意:
- 执行对应的信号捕捉处理方法,此时我们处于内核态,也就是我们在进行信号检测之后,还在内核态的时候,继续执行对应的处理方法。
- 如果我当前是内核态,是可以执行user handler方法的,我可以直接访问用户的0-3G空间的。可以在用户级页表中找到对应的方法的。OS能做到帮用户执行对应的handler方法,但是OS不愿意,也不想,如果我们以OS的身份去执行handler方法,那如果我们的方法是非法操作呢?OS不相信任何人,这个handler是用户写的,所以不能帮用户执行handler(不能用内核态执行用户的代码)。
2.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_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函 数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
#include <iostream>
#include <signal.h>
#include <unistd.h>
void handler(int signum)
{
std::cout<<"获取了一个信号: "<< signum<<std::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);
std::cout<<"default action:"<<(int)(oact.sa_handler)<<std::endl;
while(true) sleep(1);
return 0;
}
#include <iostream>
#include <signal.h>
#include <unistd.h>
void handler(int signum)
{
std::cout<<"获取了一个信号: "<< signum<<std::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);
std::cout<<"default action:"<<(int)(oact.sa_handler)<<std::endl;
while(true) sleep(1);
return 0;
}
处理信号的时候,执行自定义动作,如果在处理信号期间,又来了同样的信号,OS如何处理?
Linux在任何时候只能处理一层信号。
为什么要有block(信号屏蔽)?
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么 它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。sa_flags字段包含一些选项
#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;
}
注意:
正在处理2号信号的时候,如果不断地传入2号信号是不会被处理的。
3.可重入函数
- main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函 数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
- 像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
重入的概念:在同一时间,被多个执行流重复进入
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
- 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
注意:信号捕捉,并没有创建新的进程或者线程。
4.volatile关键字
#include <iostream>
#include <signal.h>
#include <unistd.h>
int flag=0;
void changeFlag(int signum)
{
(void)signum;
std::cout<<"change flag"<<flag;
flag=1;
std::cout<<"->"<<flag<<std::endl;
}
int main()
{
signal(2,changeFlag);
while(!flag);
std::cout<<"进程正常退出:"<<flag<<std::endl;
return 0;
}
标准情况下,键入 ctrl+c ,2号信号被捕捉,执行自定义动作,修改 flag=1 , while 条件不满足,退出循环,进程退出。
由于编译器有时候会自动给我们代码进行优化,当我们再次编译时,采用更高的优化等级时:
g++ -o signal signal.cpp -std=c++11 -g
如果没有进行优化,我们的edx就会正常地进行修改flag,但是进行优化后,我们将此时的flag优化到edx中,但是后序的更改都是内存中的flag,所以我们的优化导致了CPU无法看到内存中的情况了。
#include <iostream>
#include <signal.h>
#include <unistd.h>
volatile int flag=0;
void changeFlag(int signum)
{
(void)signum;
std::cout<<"change flag"<<flag;
flag=1;
std::cout<<"->"<<flag<<std::endl;
}
int main()
{
signal(2,changeFlag);
while(!flag);
std::cout<<"进程正常退出:"<<flag<<std::endl;
return 0;
}
注意:编译器优化是在编译的时候进行优化的。程序执行的时候,只是按照编译的情况进行执行。
5.SIGCHLD信号
#include <iostream>
#include <signal.h>
#include <unistd.h>
void handler(int signum)
{
std::cout<<"子进程退出:"<<signum<<std::endl;
}
int main()
{
signal(SIGCHLD,handler);
if(fork()==0){
sleep(1);
exit(0);
}
while(true) sleep(1);
}
这里我们发现子进程退出时向父进程发送了退出信号17,那么接下来我们来进行测试父进程是否接受了该信号!
#include <iostream>
#include <signal.h>
#include <unistd.h>
void handler(int signum)
{
std::cout<<"子进程退出:"<<signum<<"father:"<<getpid()<<std::endl;
}
int main()
{
signal(SIGCHLD,handler);
if(fork()==0){
std::cout<<"child:"<<getpid()<<std::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>
int main()
{
if(fork()==0)
{
std::cout<<"child:"<<getpid()<<std::endl;
sleep(5);
exit(0);
}
while(true)
{
std::cout<<"parent:"<<getpid()<<"执行我自己的任务!"<<std::endl;
sleep(1);
}
return 0;
}
这里我们发现我们的子进程退出时,由于资源没有被释放,所以子进程变成了僵尸进程。
不等待子进程,子进程退出后,自动释放僵尸进程。
解决方案一:
循环等待我们的子进程退出
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
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号信号进行忽略
#include <iostream>
#include <signal.h>
#include <unistd.h>
int main()
{
signal(SIGCHLD,SIG_IGN);//手动设置对子进程进行忽略
if(fork()==0)
{
std::cout<<"child:"<<getpid()<<std::endl;
sleep(5);
exit(0);
}
while(true)
{
std::cout<<"parent:"<<getpid()<<"执行我自己的任务!"<<std::endl;
sleep(1);
}
return 0;
}
这里我们知道17号信号本身默认就是的行为就是忽略,为什么还需要手动设置呢 ?
由于OS默认的也是忽略,由于这个忽略和我们手动忽略属于两个不同的级别。我们OS级别的忽略就是默认的动作,该变成僵尸进程就变成僵尸进程,OS并不知道你真的是不是要将其回收,所以就子进程搁在那里,变成僵尸进程了。如果我们自己设置了忽略,就是用户告诉OS系统将其忽略,也就是直接告诉OS我忽略了,运行完毕直接回收吧。