目录
(5)硬件异常产生信号硬件异常产生信号)
信号的基本概念
信号:Linux系统提供的一种,向指定进程发送特定事件的方式,能做识别和处理;
信号是异步产生的;
信号是进程之间事件异步通知的一种方式,属于软中断;
用kill -l命令可以察看系统定义的信号列表:
每个信号都有一个编号和一个宏定义名称 ,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2
一个信号要先产生,产生后不一定会立即执行这个信号,有时候会先等待,等到时机合适的时候才会执行,所以对于产生的信号我们还要进行保存,时机合适的时候我们再对信号进程处理;
所以接下来我们将对信号进行三个主要方面的解释:信号的产生,信号的保存,信号的处理;
信号的产生(5种方法)
(1)kill命令,向指定进程发送指定的信号
我们写一个while(true)循环,一直打印;运行后,再另外一个窗口可以对这个进程用kill直接发送信号;
kill 信号 进程的pid
(2)键盘产生信号
我们平时终止一个进程时经常会使用ctrl+c,这其实也是向进程发送一个信号,进程收到这个信号,就会终止;
SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump;
(3)系统调用
kill函数:可以给任意进程发送任意的信号;
其实kill命令是调用kill函数实现的
代码:
#include<iostream>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
using namespace std;
//./mykill 2 1234
int main(int argc ,char *argv[])
{
if(argc!=3)
{
cout<<"Usage: "<<argv[0]<<"signum pid"<<endl;
return 1;
}
pid_t pid=stoi(argv[2]);
int sig =stoi(argv[1]);
kill(pid,sig);
return 0;
}
运行结果:
rasie函数:给当前进程发送指定的信号(自己给自己发信号)
while(true)
{
cout<<"hello world,pid:"<<getpid()<<endl;
raise(2);
sleep(1);
}
运行:运行到raise函数时,会向进程发送2号信号;
abort函数:abort函数使当前进程接收到信号而异常终止;
就像exit函数一样,abort函数总是会成功的,所以没有返回值
while(true)
{
cout<<"hello world,pid:"<<getpid()<<endl;
abort();
sleep(1);
}
运行:
(4)软件条件产生信号
alarm函数:调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
返回值:函数的返回值是0或者是以前设定的闹钟时间还余下的秒数 ;
int main()
{
alarm(1);
while(true)
{
cout<<"get a pid:"<<getpid()<<endl;
}
return 0;
}
运行:
(5)硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。
默认终止进程有两种:core,term;
term:异常终止;
core:异常终止,但会帮我们形成debug文件,进程退出时镜像数据(核心转储);
云服务器默认是关闭核心转储的,因为不断的核心转储,不断的dump,会使磁盘打满;
可以用ulimit -a来查询core文件是否开启核心转储;
没有的话可以用ulimit -c 10240来开启;
为什么要有core?
可以协助我们进行debug调试
我们在编译代码时:g++ ...... -g;
加上-g的话可以允许程序被调试:gdb;
core -file core文件名----->用gdb可以直接定位出错
这时我们就要解决一下历史遗留问题了;
进程有运行完正常终止,也有异常状态,在被信号所杀的时候,会有core dump标志;
总结
信号的有OS发出,上面的5种产生情况,都是操作心态发出信号的触发条件;
(1)上面所有信号的产生都会需要操作心态的参与,为什么?
因为操作系统是进程的管理者,只要操作系统能管理进程。
(2)OS是怎么向进程发送信号的?
普通信号有31个,剩余的是实时信号,进程PCB中有数据结构,位图(只需要一个整个即可),来记录当前进程是否收到对应位置的信号,OS向进程发送信号时,将对应位置置1即可;
信号的保存
- 执行信号的处理动作:信号递达;
- 产生到递达之间的状态:信号未决;
- 进程可选择阻塞某个信号;阻塞一个信号,对应的信号一旦产生,永不递达,一直未决,直到主动接触阻塞;
在内核中信号在内存中的表示:示意图
关于block和pending位图:普通信号是1~31,只要一个整数(32位)就可以记录所有普通信号;
block位图:位图的位置表示信号的编号,位图的内容表示信号是否阻塞;
pending位图:位图的位置表示信号的编号,位图的内容表示信号是否收到;
handle:函数指针数组,是指定信号递达的方式(信号处理);信号编号就是数组下标,可采用信号编号;
怎么让系统快速对block和pending位图进行操作呢?
用sigset_t(信号集);--->Linux给用户提供的一个用户级的数据类型(禁止用户直接设置);
既然禁用户直接设置,那如何操作修改位图呢?
可以调用信号集操作函数:
#include <signal.h>
- int sigemptyset(sigset_t *set);初始化,使其中所有信号的对应bit清零
- int sigfillset(sigset_t *set);初始化,其中所有信号的对应bit置位;
- int sigaddset (sigset_t *set, int signo);在该信号集中添加某种有效信号
- int sigdelset(sigset_t *set, int signo);在该信号集中删除某种有效信号
- int sigismember(const sigset_t *set, int signo);布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
sigprocmask
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)
返回值:若成功则为0,若出错则为-1
how参数:
set:输入型
oldset:输出型
sigpending
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
总结
#include<iostream>
#include<signal.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
using namespace std;
void PrintPending(sigset_t * pending)
{
for(int signo=31;signo>=1;signo--)
{
if(sigismember(pending,signo))
{
cout<<"1";
}
else
{
cout<<"0";
}
}
cout<<endl;
}
void handler(int sig)
{
cout<<"get a pid:"<<getpid()<<endl;
}
int main()
{
signal(2,handler);
//1、屏蔽2号信号
sigset_t block_set,old_set;
sigemptyset(&block_set);//初始化
sigemptyset(&old_set);//初始化
sigaddset(&block_set,2);//添加2号信号屏蔽
sigprocmask(SIG_BLOCK,&block_set,&old_set);//完成真正的修改
int cnt=10;
cout<<"get a pid:"<<getpid()<<endl;
while(true)
{
sleep(1);
//2、获取当前进程的pending进程
sigset_t pending;
sigpending(&pending);
//3、打印pending信号集
PrintPending(&pending);
}
return 0;
}
上述代码,我们将2号信号进行屏蔽后,在去发送2号信号,2号信号不会被执行,观察pending位图,在发送完2号信号后,2号信号处于了未决状态;
注意:阻塞和忽略信号不同,阻塞信号,信号处于未决状态,并不是达到信号,忽略是在递达信号;
解除2号信号的屏蔽:
int main()
{
signal(2, handler);
// 1、屏蔽2号信号
sigset_t block_set, old_set;
sigemptyset(&block_set); // 初始化
sigemptyset(&old_set); // 初始化
sigaddset(&block_set, 2); // 添加2号信号屏蔽
sigprocmask(SIG_BLOCK, &block_set, &old_set); // 完成真正的修改
int cnt = 10;
cout << "get a pid:" << getpid() << endl;
while (true)
{
sleep(1);
// 2、获取当前进程的pending进程
sigset_t pending;
sigpending(&pending);
// 3、打印pending信号集
PrintPending(&pending);
cnt--;
// 4、解除对2号信号的屏蔽
if (cnt == 0)
{
cout << "2号信号解除屏蔽" << endl;
sigprocmask(SIG_SETMASK, &old_set, &block_set);
}
}
return 0;
}
通过运行结果我们会发现,解除屏蔽后,一般会立即处理当前被解除的信号;
pending位图对应的信号被清零是在递达之前;
信号的处理
信号处理动作有三种:
- 忽略动作(SIG_IGN)
- 默认动作(SIG_DFL)
- 自定义动作 (handler)
信号的捕捉(自定义动作)
信号可能不会被立即处理,而是在合适的时候处理;这个合适的时候就是进程从内核态返回到用户态的时候,检测并处理信号;
注意: 9号信号是不会被捕捉,不会被屏蔽的;
signal
void handler(int sig)
{
cout << "get a pid:" << getpid() << endl;
}
int main()
{
signal(2, handler);
......
}
sigaction
act:输入型
oldact:输出型
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
using namespace std;
void PrintPending(sigset_t *pending)
{
for (int signo = 31; signo >= 1; signo--)
{
if (sigismember(pending, signo))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl;
}
void handle(int sig)
{
cout<<"get a pid"<<getpid()<<endl;
}
int main()
{
struct sigaction act;
struct sigaction oldact;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
act.sa_handler=handle;
sigaction(2,&act,&oldact);//自定义2号信号
sigset_t pending;
sigset_t block_set;
sigset_t old_set;
sigemptyset(&block_set);
sigemptyset(&old_set);
sigaddset(&block_set,2);
sigprocmask(SIG_SETMASK,&block_set,&old_set);//将2号信号阻塞
int cnt=10;
while(true)
{
sleep(1);
sigemptyset(&pending);
sigpending(&pending);
PrintPending(&pending);
cnt--;
if(cnt==0)
{
sigprocmask(SIG_SETMASK,&old_set,&block_set);
cout<<"2号信号阻塞清除"<<endl;
}
}
return 0;
}
运行结果:
用户态VS内核态
计算机在运行程序时,会有两种状态,用户态和内核态。
当程序运行的是用户自己编写的代码,并没有涉及中断,异常会在系统调用时,计算机会处于用户态。
当程序运行到中断,异常或者系统调用时,计算机会处于内核态。内核态就相当于是操作系统。
但一个程序在运行时,可能在不断进行内核态和用户态的切换。
计算机中怎么能实现用户态和内核态的互相切换?
因为在虚拟地址空间有两个区域,一个是用户区,一个是内核区。其中,用户区映射的是当计算机处于用户态时,要执行的代码和数据。内核区映射的是计算机处于内核态时,要执行的代码和数据。
当计算机处于用户态时,在虚拟地址空间的用户区,通过用户级页表,找到代码和数据执行;
当计算机处于内核态时,在虚拟地址空间的内核区,通过内核级页表,找到代码和数据执行;
怎么知道计算机现在处于用户态还是内核态??
CPU中有一个及存储CR0,里面有标志位记录了计算机处于内核态还是用户态;
注意:OS不相信任何人,所有用户访问【3,4】GB的地址空间时,受到一定的约束----->只能通过系统调用来访问;
可重入函数
可重入函数就是当一个执行流进入函数,会发生信号的捕捉处理,而处理的还是执行这个函数,如果不会出现错误就是可重入函数;如果发生错误就是不可重入函数;
比如链表的插入:
此时node2就找不到了,造成了内存泄漏,所以是不可重入函数;
SIGCHLD信号
父进程创建子进程需要以调用wait或者waitpid来等待子进程退出,不然子进程会变成僵尸进程。
子进程退出时,并不是静悄悄的退出,而是会给父进程发送信号 ------>SIGCHLD
怎么证明呢?------>通过捕捉信号
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
void handle(int sig)
{
cout<<"SIGCHLD : pid: "<<getpid()<<endl;
}
int main()
{
signal(SIGCHLD,handle);
pid_t id = fork();
if (id == 0)
{
// child
int cnt = 10;
while (true)
{
sleep(1);
cout << "get a child process,pid:" << getpid() << endl;
cnt--;
if (cnt == 0)
{
sleep(10);
exit(1);
}
}
}
// father
sleep(200);
waitpid(-1, nullptr, 0);
return 0;
}
父进程对SIGCHLD处理方式如果为SIG_ING忽略,子进程不会进入僵尸状态,会自动清理子进程的空间和数据结构
signal(SIGCHLD,SIG_IGN);
以上就是信号的全部内容,希望有所帮助!!!