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

信号

一.信号的基本概念

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

二.信号的产生方式

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

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

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

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

        将程序编译并运行,在它死循环时,我们从键盘输入ctrl+\,可以看到进程退出且core dump。

Core Dump

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

        程序运行结果如下:

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

int main()
{
    int *p;
    *p = 4;                                                                     
    return 0;
}
        程序运行结果:
        它core dump的原因应该就是进程收到了11号信号。

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

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

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

        1)kill函数

        我们可以调用Kill函数向进程发送信号,kill函数原型如下:
        参数: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;
}        
我们编译两个程序并运行:

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

程序中用到了 atoi函数:
        函数原型:
        函数功能:用于将一个字符串转换为一个int型
        返回值:无法转换时返回0;成功时返回转换的int整数
        说明:如果传入的参数字符串的第一个字符就不能识别为int型时,函数停止读入该字符串

        2)raise函数
函数原型:
函数功能:向调用它的进程发送信号
返回值:成功返回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;
}                      

运行结果如下:
        其中,程序为打印发送信号成功,是因为9号信号直接终止了进程,未让进程进行到该句。

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

函数功能: 用于给自己发送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;
}       
运行结果如下:

4. 软件条件产生信号
        之前在进程间通信的管道中有提过:当写端一直在写,但是读端不读还关闭。此时操作系统就会向管道发送SIGPIPE信号终止它。这个信号就是由软件条件产生的信号。在这里,我们在介绍一个由alarm函数触发的信号SIGALRM。
(1)alarm函数
        1)函数原型:
        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;
}

运行结果:

        我们可以看到,进程打印完一条消息后,在等待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;
}

运行结果:
        我们可以看到,取消闹钟之后,alarm函数的返回值是剩余的时间。



三. 信号的常见处理方式

1. 忽略此信号
2. 执行该信号的默认处理动作,一般默认处理动作为终止进程
3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值