详解Linux的信号

341 阅读5分钟

信号发送前

信号产生的方式

  1. 可以通过键盘产生信号,比如ctrl+c给进程发送信号,使前台信号停止
  2. 程序异常同样可以产生信号,比如我的进程发生异常从而引起硬件的异常,操作系统会向这个程序发送中断的信号
  3. 系统调用也会产生信号,比如使用kill系统调用发送信号

小结一下

信号产生的方式有很多种,但是无一例外都是OS发送的信号

core dump

当进程发生错误的时候,进程的错误信息里面有一位叫做coredump会被设为1,我们通过向中断输入ulimit -c 10240,就可以查看coe dump的状态

#include<signal.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
// void hand(int sig)
// {
//     printf("sigal id %d\n",sig);
//     exit(1);
// }
int main()
{
    // signal(2,hand);
    // while(1)
    // {
    //     printf("hello www\n");
    //     sleep(1);
    // }
    // return 0;
    if(fork()==0)
    {
        int a=0;
        int c=2;
        int b=c/a;
        exit(1);
    }
    int status=0;
    waitpid(-1,&status,0);
    printf("exit code:%d exit sig:%d code dump:%d\n",(status>>8)&0xff,status&0x7f,(status>>7)&1);
    return 0;
}

信号发送后并不是被立即处理的而是要在合适的时候来处理信号,那么进程就要有具有保存信号的能力, 信号发送时

进程保存信号

实际上在进程的PCB里面是有可以保存信号的地方的

image.png 我们可以看到一段空间中储存了两段段位图结构和一段函数指针,分别是block,pending,和handler block是决定信号是否被阻塞的,pending是决定信号是否被抵达,handler就是一个函数指针数组, 这三个东西管理着信号。而且这里数组下标实际上就是多少号信号的意思,0,对应着SIG_DFL信号,我们也可以自定义信号处理方式,也就是让函数指针指向我们自定义的处理函数。

信号集操作函数

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);
注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfifillset做初始化,使信号集处于确定的

状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号
  1. 对于信号集都置为0,相当于初始化
  2. 将信号集的信号处理方式都初始化为系统的64种信号处理方式
  3. 在set指向的信号集中加入signum信号
  4. 删除signum信号
  5. 看看signo是不是信号集里面的信号

sigprocmask

调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)

int sigprocmask(int how, const [sigset_t] *restrict set, sigset_t *restrict oldset);

mask表示口罩,可以有阻挡的意思,就是阻塞信号集

how表示如何阻挡信号 参数how的取值不同,带来的操作行为也不同,该参数可选值如下:

1.SIG_BLOCK: 该值代表的功能是将newset所指向的信号集中所包含的信号加到当前的信号掩码中,作为新的信号屏蔽字。

2.SIG_UNBLOCK:将参数newset所指向的信号集中的信号从当前的信号掩码中移除。

3.SIG_SETMASK:设置当前信号掩码为参数newset所指向的信号集中所包含的信号。

如果这里的newset和oldset都是空指针,那么会把原来的信号储存到old色图里面,新的信号在newset,如果oldset是空指针,那么就更改信号的屏蔽字通过newset来更改

sigpending

获取未决信号集也就是pending位图,

int sigpending(sigset_t *set)

比如我们让一个信号集被调用,通过这个函数,可以获取位图

信号发送后

信号处理

在信号发送后进程需要对信号进行一个处理,代码是处在用户状态的,要处理信号的话,要找到相应的系统函数所以要陷入内核进行调用,如果对于信号处理是默认的或者忽略的,调用返回时就进入用户状态,执行用户代码的下一行。

自定义捕捉

信号的捕捉函数是用户写的,也就是位于用户级,所以当进程处理函数的时候就要先陷入内核找到信号集,执行自定义捕捉方法的时候再回到用户区,在用户区执行完毕后是不能回到先前的用户态的,所以又要陷入内核执行sigret()最后再回到用户态

image.png

所以当执行自定义捕捉的时候,实际上完成了四次用户和内核状态的切换,从内核到用户切换时执行自定义的信号处理函数

image.png

为什么要进行状态的切换呢?

实际上操作系统一般不直接执行用户的代码的,比如操作系统执行了rm -rf/

用户的身份是以进程为代表的用户态使用用户级页表只能访问用户数据和代码,内核态使用的是内核级页表只能访问内核级的代码和数据,所以要正确的进行信号处理要进行状态的切换。

实际上进程拥有一段4GB的虚拟内存,能够看到用户和内核的所有内容,但是却不一定能够访问,CPU通过CR3寄存器来控制进程访问内核级代码还是用户级代码,这个寄存器保存了进程的状态,如果是用户级那就通过用户级页表映射到实际内存中,如果是系统级那就是通过系统中唯一的一个系统级页表映射到实际内存中,所以进程无论怎么切换,都可以访问同一段OS中的系统代码。所谓的系统调用,就是将进程的身份转化到内核然后根据内核页表找到系统函数

image.png