Linux操作系统进程信号

Posted Ricky_0528

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux操作系统进程信号相关的知识,希望对你有一定的参考价值。

文章目录

1. 信号介绍

信号产生之后,是发给进程的,进程要在合适的时候执行对应的动作

进程在没有收到信号的时候,就已经知道了哪种信号对应哪一个动作,进程具有识别信号并处理信号的能力,远远早于信号的产生

进程收到某种信号的时候,并不一定是立即处理的,而是在合适的时候,因为信号随时都有能产生(异步的),但进程当前可能在执行更重要的事

因此信号不能被立即处理的时候,收到的信号需要被保存起来,保存在struct task_struct中,信号的本质也是数据,所以信号的发送就变成了往进程的task_struct中写入信号数据,这个操作由操作系统来完成,因而无论我们的信号是如何发送的,本质都是底层在通过操作系统发送的

信号产生前:信号产生的各种方式 -> 信号产生中:信号的保存方式 -> 信号产生后:信号处理的方式

各种信号(64个)

ctrl + c发出的就是2号信号


typedef void (*sighandler_t)(int):为一个函数指针

signum:信号对应的整型值

handler:修改进程对信号的默认处理动作

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void handler(int signum)

    printf("\\nrecieve a signal, signal num: %d, process pid: %d\\n", signum, getpid());


int main()

    // 通过signal注册对2号信号的处理动作,改成我们自己自定义的动作
    signal(2, handler);

    while (1) 
        printf("hello, my pid: %d\\n", getpid());
        sleep(1);
    

    return 0;

注意:注册函数的时候,不会调用这个函数,只有当信号到来的时候,这个函数才会被调用

使用ctrl + c向该进程发出2号信号

使用kill命令同样可以向该进程发出2号信号

2号信号无法退出,可以使用ctrl + \\退出进程

2. 信号的产生

2.1 键盘产生

信号产生的方式其中一种就是通过键盘产生,注意键盘产生的信号只能用来终止前台进程,加上&运行的后台进程不行

一般而言,进程收到信号的处理方式有三种

  • 默认动作——如终止自己、暂停等
  • 忽略动作——是一种信号处理的方式,只不过动作就是什么也不干
  • 自定义动作(信号的捕捉)——如signal方法,就是在修改信号的处理动作由默认动作 -> 自定义动作
#include <stdio.h>
#include <unistd.h>
#include <signal.h>

void handler(int signum)

    switch(signum) 
        case 2:
            printf("hello, this is No.2, pid: %d\\n", getpid());
            break;
        case 3:
            printf("hello, this is No.3, pid: %d\\n", getpid());
            break;
        case 9:
            printf("hello, this is No.9, pid: %d\\n", getpid());
            break;
        default:
            printf("hello, this is No.%d, pid: %d\\n", signum, getpid());
            break;
    


int main()

    int sig = 1;
    // 注册1~31号信号
    for (; sig <= 31; sig++) 
        signal(sig, handler);
    

    while (1) 
        printf("hello, my pid: %d\\n", getpid());
        sleep(1);
    

    return 0;

发现9号信号不可以被捕捉,即无法自定义动作

2.2 程序中存在异常

程序中存在异常问题,会导致进程收到信号退出

#include <stdio.h>

int main()

    while (1) 
        int *p = NULL;
        *p = 100;
        printf("hello, my pid: %d\\n", getpid());
        sleep(1);
    

    return 0;

程序会发生段错误:Segmentation fault,即进程的崩溃,这是因为进程收到了11号信号

在Windows或Linux下,进程崩溃的本质就是收到了对应的信号,然后进程执行信号的默认处理动作

当进程崩溃时,我们需要知道崩溃的原因,waitpid()status会携带进程异常退出的原因

进程如果异常的时候,core-dump该位置会被设置为1

我们还需要知道程序是在哪一行崩溃

  • 在Linux中,当一个进程推出的时候,它的退出码和推出信号都会被设置(正常情况)
  • 当一个进程异常的时候,进程的终止信号会被设置,表明当前进程退出的原因
  • 如果必要,操作系统会设置退出信息中的core dump标志位,并将进程在内存中的数据转储到磁盘当中,方便后期调试

云服务器上core file默认是关闭的,这种情况下程序崩溃会发生:

core file打开之后

8398为异常进程的pid

如何查看异常情况(事后调式)

  • 编译时加上选项-g
  • 运行让程序产生异常,生成core文件
  • 再用gdb调试该程序
  • 在gdb中输入core-file [core文件名],即可查看异常的详细信息


验证打开core file之后异常退出后core dump被设置为1

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

int main()

    if (fork() == 0) 
        while (1) 
            printf("I am child process...\\n");
            int a = 10;
            a /= 0;
        
    

    int status = 0;
    waitpid(-1, &status, 0);

    printf("exit code: %d, exit sig: %d, core dump: %d\\n", (status >> 8) & 0xff, status & 0x7f, (status >> 7) & 1);

    return 0;

2.3 系统调用

① kill

模拟实现一个kill命令

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

static void Usage(const char *proc)

    printf("Usage: \\n\\t %s signal who\\n", proc);


int main(int argc, char *argv[])

    if (argc != 3) 
        Usage(argv[0]);
        return 1;
    

    int signo = atoi(argv[1]);
    int who = atoi(argv[2]);

    kill(who, signo);

    printf("signal: %d, who: %d\\n", signo, who);

    return 1;

② raise

可以给当前进程发送指定的信号,即自己给自己发信号

#include <stdio.h>
#include <signal.h>

int main()

    raise(11);

    return 0;

成功返回0,错误返回-1

③ abort

使当前进程接收到信号而异常终止

#include <stdio.h>
#include <stdlib.h>

int main()

    abort();

    return 0;

就像exit函数一样,abort函数总是会成功的,所以没有返回值

2.4 软件条件

通过某种软件(OS)来触发信号的发送,系统层面设置了定时器,或者某种操作而导致条件不就绪等这样的场景下,触发信号的发送

举例:进程间通信时,当读端不仅不读,而且还关闭了读fd,写端一直在写,最终进程会收到SIGPIPE(13),就是一种典型的软件条件触发的信号发送

① alarm

调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程

这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数

打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数

#include <stdio.h>
#include <unistd.h>

int main()

    int ret = alarm(20);

    while (1) 
        printf("I am a process, ret = %d\\n", ret);
        sleep(5);

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

    return 0;

利用alarm来比较有IO与无IO的情况下,CPU执行效率

  • 有IO

    #include <stdio.h>
    #include <unistd.h>
    
    int count = 0;
    
    int main()
    
        alarm(1); // 没有设置alarm的捕捉动作,就执行默认动作,终止进程
    
        while(1) 
            printf("hello, %d\\n",count++);
        
    
        return 0;
    
    

  • 无IO

    #include <stdio.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <signal.h>
    
    int count = 0;
    
    void handlerAlarm(int signum) 
        printf("hello, %d\\n", count);
    
        exit(1);
    
    
    int main()
    
        signal(SIGALRM, handlerAlarm);
    
        alarm(1); // 没有设置alarm的捕捉动作,就执行默认动作,终止进程
    
        while(1) 
            count++;
        
    
        return 0;
    
    

无IO的情况下会快很多

2.5 OS给进程发送信号

信号产生的方式种类虽然非常多,但是无论产生信号的方式千差万别,但是最终一定都是通过OS向目标进程发送信号

产生信号的方式,其实都是OS发送信号数据给task_struct

struct task_struct中有进程的各种属性,那么其中也一定有对应的数据变量,来保存是否收到了对应的信号,而信号的编号也是有规律的1~31

进程中采用uint32_t sigs;——位图结构来标识该进程是否收到信号

第31个<--------------------------------------------第1个

0000 0000 0000 0000 0000 0000 0000 0000

比特位的位置(第几个)代表的就是哪一个信号,比特位的内容(0或1),代表的就是是否收到了信号

故本质是OS向指定进程的task_struct中的信号位图写入比特1,即完成信号的发送,也可以说是信号的写入

以上是关于Linux操作系统进程信号的主要内容,如果未能解决你的问题,请参考以下文章

Linux中的信号

Linux信号及工作原理

linux进程间通信--信号通信

Linux-信号的本质

linux信号-------初涉

Python模块之信号学习(signal)