文章目录
- 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.切换页表)->得到结果
信号的捕捉过程:
 






![记一次Cannot deploy POJO class [xxx$$EnhancerBySpringCGLIB$$xxx]的错误](https://img-blog.csdnimg.cn/direct/2e7e0f35f8de4935bef769394b98251a.png)












