文章目录
- 1.什么是信号
- 2.信号列表
- 3.信号处理常见方式
- 4.信号的存储
- 5.信号产生前-中-后
- 1.信号产生前
- 2.信号产生中
- 6产生信号
- 1.signal
- 2.kill
- 3.raise
- 4.abort
- 5.alarm
- 6.硬件异常
- 7.core dump
- 8.信号产生中
- 1. sigset_t(数据类型)
- 2.信号集操作函数
- 1.sigprocmask
- 2.sigpending
- 3.sigaction
- 9.内核态和用户态
1.什么是信号
生活中有哪些信号?
红绿灯,铃声,闹钟…
我们是如何得知这些东西?
有人教(能够认识这些场景下的信号以及所表示的含义)——识别信号
我们找到对应的信号产生时,要做什么?
我们早就知道了,信号产生之后,要做什么,即便当前信号还没有产生。——知道信号的处理方法
只有具备以上两种能力才具有——处理信号的能力
信号是给进程发送的,进程要具备处理信号的能力
1.该能力一定是预先已经早就有了的
2.进程能够识别对应的信号
3.进程能够处理对应的信号
对于进程来讲,即便是信号还没有产生,我们进程已经具有识别和处理这个信号的能力了
2.信号列表
用kill -l 命令可以查看系统定义的信号列表
信号被分成两批,1~31是普通信号,34 ~64是实时信号。我们要学习就是1 ~ 31。
3.信号处理常见方式
- 忽略此信号
- 执行该信号的默认处理动作
- 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这个方法称为捕捉(Catch)一个信号。
4.信号的存储
进程是如何记住信号的,在哪里保存信号的?
1.有没有产生 —— 比特位的内容1/0
2.什么信号产生——比特位的位置
在进程的PCB中信号的位图中保存
struct task_struct{
uint32_t sig; //位图, 0000 0010 ——号2号信号
}
PCB是不是内核数据结构?
是的,所以只有OS有这个权利,能直接修改这个task_struct内的数据位图。
5.信号产生前-中-后
信号其他相关常见概念
实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞 (Block )某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
1.信号产生前
用户层产生信号的方式
1.键盘产生——谁给进程发送的信号?OS把进程的PCB里的sig第几位置为1,就完成信号发送。
2.通过系统接口完成对进程发送信号的过程
3.软件条件
4.硬件产生信号
2.信号产生中
信号在内核中的表示
6产生信号
1.signal
作用:设置对信号的处理
原型: sighandler_t signal(int signum, sighandler_t handler);
参数:
signum: 要捕捉的信号
handler:函数指针,当捕捉到信号,就调用该函数
返回值:
样例:对SIGINT(2)(按ctrl+c就是向进程发送2号信号)设置
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void handler(int signo)
{
cout<<"我是一个进程,刚刚获取了一个信号:"<<signo<<endl;
}
int main()
{
signal(SIGINT,handler);
sleep(3);
cout<<"进程已经设置完了"<<endl;
sleep(3);
while(true)
{
cout<<"我是一个正在运行的进程:"<<getpid()<<endl;
sleep(1);
}
return 0;
}
如果不向进程发送2号信号,就不会调用自定义函数。
ctrl+c :本质就是给前台进程产生了2号信号发送给目标进程,目标进程默认对2号信号的处理是终止自己,刚刚代码更改了对2号信号的处理,设置了用户自定义处理方法。
注:9号信号不能被设置。
2.kill
作用:向指定进程发送指定信号
原型: int kill(pid_t pid, int sig);
参数:
pid :进程id
sig:要发送的信号
返回值:
成功返回0
失败返回-1
样例:写一个程序,对进程发送信号
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>
using namespace std;
int main(int argc,char* argv[])
{
if(argc!=3)
{
cout<<argv[0]<<" -信号 进程pid"<<endl;
return 1;
}
pid_t pid = atoi(argv[2]);
int sig = atoi((argv[1]+1));
if(kill (pid,sig)==-1)
{
cerr<<"kill error"<<endl;
}
return 0;
}
3.raise
作用:给自己发信号
原型: int raise(int sig);
参数:
sig:信号
返回值:
成功返回0
失败返回非零
发送2号信号
raise(2);
4.abort
作用:向自己发送SIGABRT(6),终止进程
原型:void abort(void);
abort();
注:6号信号可以被捕捉,但依旧要被终止进程。
5.alarm
作用:x秒后向进程发送SIGALRM(14)信号
原型: unsigned alarm(unsigned seconds);
参数:
seconds : 设定多少秒
alarm(1);
6.硬件异常
崩溃的本质是什么?
进程崩溃的本质,是该进程收到了异常信号!
为什么会崩溃?
因为硬件异常,而导致OS向目标进程发送信号,进而导致进程终止的现象!
除零错误:CPU内部有状态寄存器,当我们除0的时候,CPU内的状态寄存器会被设置成为有报错:浮点数越界
CPU的内部寄存器(硬件),OS就会识别到CPU内有报错:
1.谁干的
2.什么报错(OS->构建信号)->目标进程发送信号->目标进程在合适的时候->处理信号->终止信号
越界&&野指针:我们在语言层面使用的地址(指针),虚拟地址->物理地址->物理内存->读取对应的数据
如果虚拟地址有问题,地址转化的工作是由(MMU(硬件)+页表(软件)),转化过程就会引起问题,表现在硬件MMU上,OS发现硬件出现问题
1.谁干的
2.什么报错(OS->构建信号)->目标进程发送信号->目标进程在合适的时候->处理信号->终止信号
7.core dump
之前在进程等待提到,进程被有些信号所杀,会将core dump设为1,并产生核心转储。
注:如果没有被置1且没有生产核心转储,ulimit -a查看core文件大小,并设置core的大小
上图就是core文件为0,进程异常不会生产核心转储。
ulimit -c 100000
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdio.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id==0)
{
int a =10;
a/=0;
exit(1);
}
int status = 0;
waitpid(id,&status,0);
printf("exitcode:%d ,signo:%d, core dump flag: %d\n",(status>>8)&0xFF,status&0x7F,(status>>7)&0x1);
return 0;
}
被信号所杀,core dump 被设置1后,会把进程在运行中,对应的异常上下文数据,core dump到磁盘上,方便调试(要debug版本编译带上-g)。
下面是调试的过程:
8.信号产生中
pending:表示是否收到信号
block:是否阻塞信号的抵达
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号
1. sigset_t(数据类型)
未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集。
2.信号集操作函数
#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.sigprocmask
作用:可以读取或更改进程的信号屏蔽字(阻塞信号集)
原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
参数:
how :做什么操作
set:要被设置的信号集
oset:老的信号集的返回
返回值:成功为0,失败为-1
2.sigpending
作用:获取当前进程的pengind信号集。
原型: int sigpending(sigset_t *set);
参数:
set:字符集
返回值:成功为0,失败为-1
#include<iostream>
#include <signal.h>
#include<unistd.h>
using namespace std;
static void showPending(sigset_t* pendings)
{
for(int sig=1;sig<=31;sig++)
{
if(sigismember(pendings,sig))
{
cout<<"1";
}
else
{
cout<<"0";
}
}
cout<<endl;
}
void handler(int signo)
{
cout<<"我是一个进程,刚刚获取了一个信号:"<<signo<<endl;
}
int main()
{
//屏蔽2号信号
sigset_t bsig,osig;
sigisemptyset(&bsig);
sigisemptyset(&osig);
sigaddset(&bsig,2);
sigprocmask(SIG_SETMASK,&bsig,&osig);
signal(2,handler);
//不断的获取当前进程的pending信号集
sigset_t pendings;
while(true)
{
//清空信号集
sigemptyset(&pendings);
//获取当前进程的pending信号集
if(sigpending(&pendings)==0)
{
//打印当前进程的pending信号集
showPending(&pendings);
}
sleep(1);
}
return 0;
}
3.sigaction
#include<iostream>
#include <signal.h>
#include<unistd.h>
using namespace std;
void handler(int signo)
{
cout<<"我是一个进程,刚刚获取了一个信号:"<<signo<<endl;
}
int main()
{
struct sigaction act,oact;
act.sa_handler =handler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaction(2,&act,&oact);
while(true)
{
sleep(1);
}
return 0;
}
9.内核态和用户态
进程处理信号,不是立即处理的,而是合适的时候?
合适的时候是指当进程从内核态,切换回用户态的时候,进行信号的检查和处理
每个进程都有进程地址空间,用户空间有着对应的页表是用户级的,而且大家用户级页表都不一样,地址空间里还有1G为内核空间,这个空间也有着对应的页表是内核级的,而且所有进程共享一份。
无论进程怎么切换,我们都可以找到内核的代码和数据,前提是你只要有能够有权利访问。
当前进程如何具备权利,访问这个内核页表,乃至访问内核数据?
要进行,身份切换。
进程如果是用户态的——只能访问用户级页表
进程如果是内核态——访问内核级和用户级页表
如何知道我是用户态还是内核态?
CPU内部有对应的状态寄存器CR3,有比特位标识当前进程状态
0:内核态
3:用户态
内核态VS用户态
内核态可以访问所有代码和数据——具备更高权限
用户态只能访问自己的
我们的程序会无数次直接或者间接的访问系统级软硬件资源(管理者是OS),本质上,你并没有自己去操作这些硬件资源,而是必须通过OS->无数次陷入内核(1.切换身份2.切换页表)->调用内核的代码->完成访问的动作->结果返回给用户(1.切换身份2.切换页表)->得到结果
信号的捕捉过程: