0
点赞
收藏
分享

微信扫一扫

Linux 进程信号

       

前几天一直在搞算法,没来得及更新博客。今天就来谈谈进程信号。主要分信号发送前、信号发送中、信号发送后三个阶段来讲。这个知识还蛮重要的~

目录

信号背景知识

信号发送前

键盘产生信号

signal

一个小疑问

总结

进程异常产生信号

演示一

演示二

硬件上理解进程崩溃的本质 

core dump

core dump 事后调试

编码打印查看core dump

总结

通过系统调用产生信号

kill

raise

 abort

 软件条件产生信号

alarm

alarm补充

总结

信号发送中

如何理解OS给进程发送信号?

那如何向信号位图中写入? 

信号相关的概念

信号递达

未决

阻塞

信号保存三张表

 pending

handler 

block 

总结 

代码验证->函数接口

sigset_t

信号操作集函数

sigprocmask

sigpending

总结

信号发送后

内核态&用户态

内核级页表

信号处理

默认处理动作

 忽略处理动作

自定义处理动作 

精炼化理解

总结

sigaction

补充


信号背景知识

信号的发送是谁去做呢?

信号发送前

信号在发送前,需要产生信号,那么信号产生的方式有哪些呢?具体又做了些什么呢?

键盘产生信号

在做试验前,,我们先认识一个系统调用接口:

signal

代码练习+实验:

void handler(int signo)
{
    printf("get a signal: %d, pid: %d\n", signo, getpid());
}
int main()
{
    //通过signal注册对2号信号的默认处理动作,改为我们的自定义动作
    signal(2, handler);//handler和&handler一样都是表示地址

    while(1)
    {
        printf("hello wmm!, pid: %d\n", getpid());
        sleep(1);
    }
    return 0;
}

我们使用signal完成对2号信号的自定义处理过程,我们通过键盘ctrl+C看是否可以向进程发送信号

演示如下:

一个小疑问

我们来演示一下:

void handler(int signo)
{
    switch (signo)
    {
    case 2:
        printf("hello wmm, get a signal: %d\n", signo);
        break;
    case 3:
        printf("hello wmm, get a signal: %d\n", signo);
        break;
    case 9:
        printf("hello wmm, get a signal: %d\n", signo);
        break;
    default:
        break;
    }
}
int main()
{
    for (int sig = 1; sig <= 31; sig++)
    {
        signal(sig, handler);
    }

    while (1)
    {
        printf("hello wmm!, pid: %d\n", getpid());
        sleep(1);
    }
    return 0;
}

演示结果:

总结

进程异常产生信号

演示一

我们用代码演示一下:

int main()
{
    int* ptr = NULL;
    *ptr = 100; //err
    return 0;
}

运行一下这个野指针错误问题的代码:

我们发现出现了错误,这时候我们用signal捕捉一下,看看是进程收到了哪个信号:

void handler(int signo)
{
    printf("hello wmm, get a signal: %d\n", signo);
}
int main()
{
    for(int sig = 1; sig <= 31; sig++)
    {
        signal(sig, handler);
    }
    int* ptr = NULL;
    *ptr = 100; //err
    return 0;
}

 运行结果:

演示二

我们再来一个错误代码演示一下:

void handler(int signo)
{
    printf("hello wmm, get a signal: %d\n", signo);
}
int main()
{
    for(int sig = 1; sig <= 31; sig++)
    {
        signal(sig, handler);
    }
    int a = 10;
    a /= 0;
    return 0;
}

我们使用整数除以0的浮点数错误的代码,这时候我们看看进程会收到什么信号:

硬件上理解进程崩溃的本质 

为什么进程崩溃?因为进程收到了信号。为什么收到信号?因为进程崩溃了。这样理解的话我们发现我们一直陷入到逻辑怪圈里,这时候我们需要在硬件上去理解为什么会收到信号:

core dump

core dump 事后调试

在云服务器上,core dump默认是关闭的:

[cyq@VM-0-7-centos 进程信号]$ ulimit -a

这时候我们主动去设置一下,core dump就可以使用了: 

[cyq@VM-0-7-centos 进程信号]$ ulimit -c 1024

我们使用之前编译的浮点数错误的代码:

void handler(int signo)
{
    printf("hello wmm, get a signal: %d\n", signo);
}
int main()
{
    for(int sig = 1; sig <= 31; sig++)
    {
        signal(sig, handler);
    }
    int a = 10;
    a /= 0;
    return 0;
}

我们使用debug方式编译:

[cyq@VM-0-7-centos 进程信号]$ gcc test.c -o test -std=c99 -g

运行后我们发现是这样的结果:

我们然后使用gdb和core-file来查找进程崩溃的地方:

[cyq@VM-0-7-centos 进程信号]$ gdb test
(gdb) core-file core.460 

 以上就是在Linux下时候调试的步骤。

编码打印查看core dump

int main()
{
    if(fork() == 0)
    {
        while(1)
        {
            printf("i am child...\n");
            int a = 10;
            a /= 0;
        }
    }
    int status = 0;
    waitpid(-1, &status, 0);
    printf("core dump: %d\n", (status>>7)&1);
    return 0;
}

运行结果:

总结

通过系统调用产生信号

kill

代码练习:

void Usage(const char* proc)
{
    printf("Usage: \n\t %s who signo\n", proc);
}
//   ./test PID signo
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }
    int who = atoi(argv[1]);
    int signo = atoi(argv[2]);

    kill(who, signo);
    printf("who: %d signo: %d\n", who, signo);
    return 0;
}

我们提前写一个死循环的程序,a.out。

运行结果如下:

[cyq@VM-0-7-centos 进程信号]$ ./可执行程序 要发送信号的进程的PID 发送几号信号

raise

举个栗子:

void handler(int signo)
{
    printf("get a signo: %d\n", signo);
}
int main()
{
    signal(8, handler);
    int cnt = 3;
    while(cnt--)
    {
        printf("hello wmm\n");
        sleep(1);
    }
    raise(8); //自己个自己发送信号
    return 0;
}

我们提前对8号信号进行捕捉,然后再主程序3s后执行raise语句,给自己发送8号信号:

运行结果:

 abort

举个栗子:

void handler(int signo)
{
    printf("get a signo: %d\n", signo);
}
int main()
{
    for(int sig = 1; sig <= 31; sig++)
    {
        signal(sig, handler);
    }
    int cnt = 3;
    while(cnt--)
    {
        printf("hello wmm\n");
        sleep(1);
    }
    abort(); //自己个自己发送信号
    return 0;
}

运行结果:

 软件条件产生信号

alarm

举个栗子:

int main()
{
    int ret = alarm(30);
    printf("hello wmm: ret %d\n",ret);
    sleep(5);

    int res = alarm(0);//取消闹钟
    printf("hello wmm: res %d\n",res);

    return 0;
}

我们来看alarm的返回值:

我们用代码捕捉alarm发送的信号: 

void handler(int signo)
{
    printf("get a signo: %d\n", signo);
}

int main()
{
    signal(SIGALRM, handler);

    alarm(3);
    while(1)
    {
        printf("hello wmm\n");
        sleep(1);
    }
    return 0;
}

运行结果:

alarm补充

我们对比两组代码。

涉及IO时:

int main()
{
    alarm(1);
    int count = 0;
    while(1)
    {
        count++;
        printf("hello wmm: %d\n",count);
    }
    return 0;
}

运行结果:

纯CPU计算:

int count = 0;
void handler(int signo)
{
    printf("hello wmm: %d\n", count);
    exit(1); //不能使用return
}
int main()
{
    signal(SIGALRM, handler);
    alarm(1);
    while(1)
    {
        count++;
    }
    return 0;
}

测试结果:

总结

信号发送中

如何理解OS给进程发送信号?

那如何向信号位图中写入? 

信号相关的概念

信号递达

未决

阻塞

信号保存三张表

     

 pending

比如在图中表示收到了2号信号,因为在2号信号位置比特位为1。

handler 

所以当pending表和handler表结合起来并横着看:

block 

总结 

我们把三张表结合看,实际上这三张表是横着看的:

我们写一个伪代码来体验一下:

               

代码验证->函数接口

sigset_t

信号操作集函数

sigprocmask

 代码测试练习:

int main()
{
    //虽然sigset_t是一个位图结构,但是不同的OS实现是不一样的,
    //不能让用户直接修改该变量!需要使用特定的函数

    sigset_t iset;
    sigset_t oset;
    
    //所有bit为清0
    sigemptyset(&iset);
    sigemptyset(&oset);

    //添加某种特定的信号
    sigaddset(&iset, 2);
    sigprocmask(SIG_SETMASK, &iset, &oset);
    while(1)
    {
        printf("hello wmm\n");
        sleep(1);
    }
    return 0;
}

我们完成对2号信号的屏蔽,演示结果:

sigpending

代码测试练习:

void show_pending(sigset_t* pending)
{
    printf("sur process pending: ");
    fflush(stdout);
    int i = 0;
    for(i = 1; i <=31; i++)
    {
        if(sigismember(pending, i)) //检测i信号是否在pending位图里面
        {
            printf("1");
        }
        else
        {
            printf("0");
        }
    }
    printf("\n");
}
int main()
{
    sigset_t iset;
    sigset_t oset;

    //bit位清0
    sigemptyset(&iset);
    sigemptyset(&oset);

    //添加某种有效信号
    sigaddset(&iset, 2);

    //设置当前进程的信号屏蔽字
    sigprocmask(SIG_SETMASK, &iset, &oset);

    int count = 0;
    sigset_t pending;
    while(1)
    {
        sigemptyset(&pending);
        sigpending(&pending);//获取当前进程的位图
        show_pending(&pending);

        sleep(1);
        count++;
        if(count == 10) //10s后
        {
            //把老的信号恢复过去
            sigprocmask(SIG_SETMASK, &oset, NULL);//不需要接收输出
            printf("恢复2号信号,可以被递达\n");
        }
    }
    return 0;
}

show_pending负责打印位图,我们先屏蔽2号信号,10s后,解除2号信号的屏蔽,我们来看一下这个执行过程:

至于handler表的函数接口已经介绍过了,就是signal,可以捕捉信号,并指定处理动作。

总结

信号发送后

我们知道,信号到来时,不一定是被立即处理的,而是在合适的时候。什么是合适的时候呢?我们讲之前,先补充一些知识:

内核态&用户态

内核级页表

信号处理

"合适"的时候,就是指当进程从内核态转换为用户态的时候,在这之前要进行信号检测!

信号处理动作有三种方式,在这里主要介绍自定义捕捉。

默认处理动作

 忽略处理动作

自定义处理动作 

精炼化理解

上面信号检测的过程是不是比较复杂?别急,我们仔细看那个过程,是不是就是数学中无穷大的符号?

为何一定要切换成为用户态,才能执行信号捕捉方法?

总结

          

sigaction

至于const struct sigaction结构体:

代码练习:

void handler(int signo)
{
    printf("get a signo: %d\n", signo);
}
int main()
{
    struct sigaction act;
    memset(&act, 0, sizeof(act)); //对这个结构体变量初始化
    act.sa_handler = handler;

    //本质就是修改当前进程的handler函数指针数组里面特定的内容
    sigaction(2, &act, NULL);

    while(1)
    {
        printf("hello wmm\n");
        sleep(1);
    }
    return 0;
}

测试结果:

补充

1、简单就是说,如果一个信号的处理动作正在执行时,该信号会被阻塞,直到该信号动作处理完之后,该信号就解除阻塞。(就算该信号传递再多次,pending表中只记录1次)

2、在处理自定义捕捉动作期间,除了屏蔽当前正在处理的信号,还想屏蔽其它信号,使用sa_mask可以屏蔽多个信号。

代码测试练习:

void handler(int signo)
{
    printf("get a signo: %d\n", signo);
    //在这里,7s内,2号信号被阻塞,即使收到2号信号也不会重新执行
    sleep(7);
}
int main()
{
    struct sigaction act;
    memset(&act, 0, sizeof(act)); //对这个结构体变量初始化
    act.sa_handler = handler;

    sigemptyset(&act.sa_mask); //所有bit位置0
    sigaddset(&act.sa_mask, 3); //在处理2号信号期间屏蔽3号信号---可以同时屏蔽多个
    //本质就是修改当前进程的handler函数指针数组里面特定的内容
    sigaction(2, &act, NULL); //对2号信号自定义


    while(1)
    {
        printf("hello wmm\n");
        sleep(1);
    }
    return 0;
}

运行结果:

看到这里,给博主点个赞吧~

                      

举报

相关推荐

0 条评论