欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Linux信号-信号概念/信号的产生及处理方式

程序员文章站 2022-07-12 10:29:27
...

信号

一.信号的基本概念

        在生活中,我们会听到上下课铃,你会对应的知道该到上课或者下课的时间了;在早上,闹钟响了,你知道是时候起床了;在马路上,看见红灯你知道不能过马路等等。其实这些都是信号,你可以根据你接收到的信号不同而做出不同的反应。
        同样的,在操作系统中,也有相应的信号机制。比如在Linux下,我们有很多不同的信号,我们可以用kill -l查看系统定义的信号列表:
Linux信号-信号概念/信号的产生及处理方式
        可以看到,一共有62种信号。其中,1-31号为普通信号,34-64为实时信号。
        我们也可以看见,每个信号都有一个编号和一个宏定义名称。这些宏定义都可以在头文件signal.h中找到。

二.信号的产生方式

1.键盘产生信号-----前台进程
        用户在终端按下某些键时,终端驱动程序会发送信号给前台进程。比如ctrl+c会产生2号SIGINT信号;ctrl+\会产生3号SIGQUIT信号;ctrl+z会产生20号SIGTSTP信号(该信号会使前台进程停止)。

这里简单介绍一下前台进程和后台进程的区分:

Linux信号-信号概念/信号的产生及处理方式
Linux信号-信号概念/信号的产生及处理方式
        同时可以看到,上图是由键盘用ctrl+c向进程发送了信号让其终止。我们编写一个程序检测一下ctrl+\是否发送的是3号SIGQUIT信号,测试代码如下:
#include <stdio.h>

int main()
{
    printf("This is YoungMay\n");
    while(1);                                                                   
    return 0;
}

        将程序编译并运行,在它死循环时,我们从键盘输入ctrl+\,可以看到进程退出且core dump。
Linux信号-信号概念/信号的产生及处理方式

Core Dump

        当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump,也叫做核心转储。
        进程异常终止通常是因为有bug,比如非法访问内存导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。
        一个进程能产生多大的core文件取决于进程的Resourse Limit(这个信息保存在PCB中)。默认是不允许产生core文件的,因为core文件中可能包括用户密码等敏感信息,不安全,尤其是针对线上服务器。但在开发调试阶段可以用ulimit命令改变这个限制,让其允许产生core文件。
        首先我们用ulimit命令改变shell进程的Resourse Limit,允许产生core文件最大位1024K
Linux信号-信号概念/信号的产生及处理方式
        然后运行可执行程序,从而产生core文件:
Linux信号-信号概念/信号的产生及处理方式
        接着我们用core文件调试查看程序core dump原因:
Linux信号-信号概念/信号的产生及处理方式
        可以看到程序时因为收到了3号信号从而core dump了。
2. 硬件异常产生信号

        硬件异常产生信号,这些条件由硬件检测到并通知内核,然后由内核向当前进程发送适当的信号。例如,当前进程执行了除以0的指令,CPU运算单元产生了异常,内核将这个异常解释为将8号SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为将11号SIGSEGV信号发送为进程。
(1)除0异常:
#include <stdio.h>
 
int main()
{ 
    int a = 5 ; 
    int b = 0 ; 
    printf("a/b = %d\n",a/b); 
    return 0;
}

        程序运行结果如下:
Linux信号-信号概念/信号的产生及处理方式

        可以看到进程依旧core dump了,我们可以用上面同样的方法查看进程core dump原因。由理论知识可知,进程是收到了8号信号。(这里不再演示)
(2)访问非法内存
#include <stdio.h>

int main()
{
    int *p;
    *p = 4;                                                                     
    return 0;
}
        程序运行结果:
Linux信号-信号概念/信号的产生及处理方式
        它core dump的原因应该就是进程收到了11号信号。

3. 命令/系统调用接口产生信号

(1)通过命令向进程发送信号:
        一个终端下:
Linux信号-信号概念/信号的产生及处理方式
        两个终端:
Linux信号-信号概念/信号的产生及处理方式
        可以看到,我们向该进程发送9号信号,该信号默认的处理动作是终止进程。所以我们可以看到该进程被kill而退出。

(2)通过系统调用接口产生信号
    

        1)kill函数

        我们可以调用Kill函数向进程发送信号,kill函数原型如下:
Linux信号-信号概念/信号的产生及处理方式
        参数:pid为进程的pid,你要向哪个进程发送信号,就写哪个进程的pid;sig就是你要发送的信号的编号。
        返回值:成功返回0,失败返回-1。
编写测试代码如下:

首先编写一个程序test1.c,让它打印出自己的pid之后,就死循环:
#include <stdio.h>                                                              
#include <unistd.h>
#include <sys/types.h>

int main()
{
    printf("pid: %d\n",getpid());
    while(1);
    return 0;
}
再写一个程序test.c,实现利用命令行参数向另一个进程发送信号:
#include <stdio.h>                                                              
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
#include <stdlib.h>

int main(int argc, char* argv[])//利用命令行参数向另一进程发送信号
{
    if(argc != 3)//命令格式输入错误
    {
        printf("可执行文件+pid+signo\n");
        return -1;
    }
    int ret = kill(atoi(argv[1]),atoi(argv[2]));//调用kill向进程发信号
    if(ret == -1)
    {
        perror("kill");
        return -2;
    }
    else
    {
        printf("send %d to %d succeed\n",atoi(argv[2]),atoi(argv[1]));
    }
    return 0;
}        
我们编译两个程序并运行:

Linux信号-信号概念/信号的产生及处理方式
        可以看到,我们在一个终端运行test1,test1打印出它的pid后就一直死循环;我们在另一个终端下运行test,并利用命令行参数输入test1的pid以及要发送的9号信号。最后看到test1进程被killed。

程序中用到了atoi函数:
        函数原型:
Linux信号-信号概念/信号的产生及处理方式
        函数功能:用于将一个字符串转换为一个int型
        返回值:无法转换时返回0;成功时返回转换的int整数
        说明:如果传入的参数字符串的第一个字符就不能识别为int型时,函数停止读入该字符串

        2)raise函数
函数原型:
Linux信号-信号概念/信号的产生及处理方式
函数功能:向调用它的进程发送信号
返回值:成功返回0,失败返回非0

测试代码如下:
#include <stdio.h>
#include <signal.h>//raise
#include <stdlib.h>//atoi
#include <unistd.h>//sleep

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        printf("可执行程序+signo\n");
        return -1;
    }
    int i = 10;
    while(i--)
    {
        printf("This is YoungMay\n");
        sleep(1);
        if(i == 5)
        {
            int ret = raise(atoi(argv[1]));
            if(ret != 0)//调用成功返回0,否则返回非0
            {
                perror("raise");
                return -2;
            }
            else
            {
                printf("send %d succeed\n",atoi(argv[1]));
            }
        }
    }
    return 0;
}                      

运行结果如下:
Linux信号-信号概念/信号的产生及处理方式
        其中,程序为打印发送信号成功,是因为9号信号直接终止了进程,未让进程进行到该句。

        3)abort函数
abort函数原型为:

Linux信号-信号概念/信号的产生及处理方式
函数功能:用于给自己发送6号SIGABRT信号
返回值:无返回值。因为就像exit函数一样,abort函数总是会成功,所以无返回值。

编写测试代码如下:
#include <stdio.h>                                                              
#include <stdlib.h>

int main()
{
    printf("I am YoungMay\n");
    abort();//给自己发信号
    printf("He is fafa\n");
    return 0;
}       
运行结果如下:

Linux信号-信号概念/信号的产生及处理方式

4. 软件条件产生信号
        之前在进程间通信的管道中有提过:当写端一直在写,但是读端不读还关闭。此时操作系统就会向管道发送SIGPIPE信号终止它。这个信号就是由软件条件产生的信号。在这里,我们在介绍一个由alarm函数触发的信号SIGALRM。
(1)alarm函数
        1)函数原型:
Linux信号-信号概念/信号的产生及处理方式
        2)函数功能:设定一个闹钟,告诉内核在seconds秒后向当前进程发送SIGALRM信号,该信号的默认处理动作是终止当前进程。
        3)参数:为0 表示取消以前设定的闹钟,其他无符号整型数表示多少秒之后闹钟“响”
        4)返回值:0或是以前设定的闹钟时间剩下的秒数
(2)编写代码测试闹钟向进程发送信号:
//alarm函数会给进程发送SIGALRM信号                                              
#include <stdio.h>
#include <unistd.h>

int main()
{
    unsigned int ret = alarm(2);//两秒之后响的闹钟
    printf("This is YoungMay\n");
    sleep(3);
    printf("That is fafa\n");
    return 0;
}

运行结果:
Linux信号-信号概念/信号的产生及处理方式

        我们可以看到,进程打印完一条消息后,在等待3秒时,被2秒后“响”的闹钟终止了进程。

(3)编写代码测试取消闹钟:
//alarm函数会给进程发送SIGALRM信号                                              
#include <stdio.h>
#include <unistd.h>

int main()
{
    unsigned int ret = alarm(2);//两秒之后响的闹钟
    printf("This is YoungMay\n");
    sleep(1);
    ret = alarm(0);
    printf("ret :%d\n",ret);//取消闹钟
    printf("That is fafa\n");
    return 0;
}

运行结果:
Linux信号-信号概念/信号的产生及处理方式
        我们可以看到,取消闹钟之后,alarm函数的返回值是剩余的时间。



三. 信号的常见处理方式

1. 忽略此信号
2. 执行该信号的默认处理动作,一般默认处理动作为终止进程
3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号。

上一篇: Java高级特性——反射

下一篇: dfs