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

进程信号的产生

程序员文章站 2022-03-19 13:21:25
...

信号是信息的载体,发送某种信号即是发送传递某种信息。这里的进程信号是指软件中断,它提供了一种处理异步事件的方法。
在Linux中,我们可以用kill -l 命令查看有多少信号:
进程信号的产生
可以看到有62种信号,其中1~31号信号是普通信号,34~64属于实时信号,这里仅讨论普通信号。
不同的信号有不同的产生方式,可分为这么几类:
1、用户在终端按下某些按键时,终端驱动程序会发送信号给前台进程,如按Ctrl-C产⽣生SIGINT信号,Ctrl-\产⽣生SIGQUIT信号,Ctrl-Z产⽣生SIGTSTP信号(可使前台进程停止)。
2、硬件异常产生信号,比如除0、内存非法访问等,当硬件检测到这些异常时就会通知内核,然后内核就会向当前进程发送适当的信号(SIGFPE、SIGSEGV等)。
3、调用系统函数发送信号,如终端的kill命令就是调用系统的kill()函数向指定进程发送指定信号。
4、软件条件产生信号,当满足某个软件条件时会产生信号,比如管道的读端关闭,一个进程写此管道时便会产生信号SIGPIPE。
下面来分别叙述这些信号的产生方式:
1、先来看看用终端按键产生的信号。
先介绍一下Core Dump。我们知道,当子进程退出时,如果父进程要获取子进程的退出状态会定义一个输出型参数status,当用wait取得子进程的退出信息后,可以根据status上的信息来进行相应的操作。而status仅有低16位保存子进程的退出状态,高16位并未使用,其低十六位的意义如下:
进程信号的产生
当我们发送信号杀死一个进程时,其信号就记录在低八位中其中低七位记录信号值,第八位记录是否Core Dump(当进程要异常终止时可以选择把进程的用户空间内存中的所有数据保存在磁盘上,生成的文件名通常是core,这个过程叫Core Dump),是则为1,否则为0。
常用的终端按键产生的信号有SIGINT(2号信号,ctrl+c产生)、SIGQUIT(3号信号,ctrl+\产生)和SIGTSTP(20号信号,ctrl+z产生),其中SIGQUIT信号会Core Dump,SIGINT和SIGTSTP信号不会。给出这样一段代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(void)
{
    pid_t pid = fork();
    if(pid < 0)
    {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    if(pid == 0)
    {
        while(1)
        {
            printf("process id:>%d\n", getpid());
            sleep(1);
        }
    }
    else
    {
        int status;
        wait(&status);
        printf("%d\n", status);
    }

    return 0;
}

这段代码运行会会产生两个进程,子进程循环打印字符,而父进程调用wait函数阻塞等待。当我们用某个信号杀死子进程后,父进程就会获得子进程的退出信息,通过打印出status的值,我们可以知道信号值以及是否Core Dump。
(PS:因在终端直接按键会同时结束两个进程而不能打印出status值,因此我们用kill命令向进程发送相应的信号(即2、3、20号信号)来模拟按键产生信号。)
先用2号信号(即SIGINT信号)结束进程:
进程信号的产生
进程信号的产生
可以看见,status的值为2,说明子进程被2号信号(即SIGINT信号)杀掉,并且没有
Core Dump。3号信号:
进程信号的产生
进程信号的产生
status的值为131,因此第八位中的二进制数为:
进程信号的产生
Core Dump标志位为1,因此进程被3号信号(SIGQUIT信号)杀死时Core Dump了,可以看到生成了一个core文件:
进程信号的产生
要注意的是这个core文件原本大小为0,这时core文件不可见,只有我们用ulimit指令改变core文件的大小后才会显示出来。因此如果要某个进程退出时产生core文件,就要先用ulimit指令改变core文件的大小(通常用ulimit -c 1024),然后再运行程序。
20号信号:
进程信号的产生
进程信号的产生
可以看到,在子进程接收到20号信号(SIGTSTP信号)后立即停止,但是没有退出。
现在来看看真正用终端按键产生信号结束进程:
进程信号的产生

2、硬件异常产生信号
常见的硬件异常信号有两种,即除0和内存非法访问。
先来看除0异常信号,先给出这样一段代码:

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


int main(void)
{
    pid_t pid = fork();
    if(pid < 0)
    {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    if(pid == 0)
    {
        int num = 3/0;
        printf("child process finish\n");
    }
    else
    {
        int status;
        wait(&status);
        printf("%d\n", status);
    }

    return 0;
}

代码中,子进程存在除0错误,如果产生除0异常信号SIGFPE信号(8号信号),就会立即结束子进程,并且父进程打印出status,不产生SIGFPE信号则子进程打印出字符串,父进程打印status值,看看结果:
进程信号的产生
可以看到产生了除0异常信号,而status的值为136,说明进程退出时Core Dump了
进程信号的产生
内存非法访问异常信号:
当发生内存的非法访问时,会产生SIGSEGV信号(即11号信号)。现在将上面代码中的子进程代码改为:

int  *p;
*p = 2;

编译运行:
进程信号的产生
输出139,很明显产生了11号信号(即SIGSEGV信号)并Core Dump了:
进程信号的产生

3、调用系统函数向进程发信号
前面我们用kill 指令向进程发送指定信号就是调用系统函数kill()完成的,其原型为:

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid,  int sig);
返回值:成功返回0,失败返回-1;
参数:
pid:目的进程。
sig:要发送的信号。

给出代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
int main(void)
{
    pid_t pid = fork();
    if(pid < 0)
    {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    if(pid == 0)
    {
        while(1)
        {
            printf("child\n");
            sleep(1);
        }
    }
    else
    {
        int status;
        sleep(3);
        kill(pid, 9);
        wait(&status);
        printf("%d\n", status);
    }

    return 0;
}

代码中子进程每隔一秒输出一次字符串,父进程三秒后调用kill()函数发送9号信号结束掉子进程,并输出信号:
进程信号的产生

进程可以给其他进程发送信号,当然也可以给自己发送信号,这就要用到raise函数,其原型为:

#include <signal.h>
int raise(int sig);
返回值:成功返回0,失败返回-1;
参数:要发送的信号。

给出这样一段代码:

#include <stdio.h>
#include <signal.h>
int main(void)
{
    int i = 0;
    while(1)
    {
        printf("time:>%d\n", ++i);
        sleep(1);
        if(i == 5)
            raise(9);
    }
    return 0;
}

代码在运行五秒后向自己发送9号信号杀死自己,运行效果:
进程信号的产生
当然raise函数也可以发送其他信号执行其他功能。
还有一个函数:

#include <stdlib.h>
void abort(void); 
因为abort函数总是会成功的,所以没有返回值。

abort函数功能是使当前进程接收到信号而异常终止。
执行程序:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
int main(void)
{
    pid_t pid = fork();
    if(pid < 0)
    {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    if(pid == 0)
    {
        int i = 0;
        while(1)
        {
            printf("child:> %d\n", ++i);
            if(i == 5)
                abort();
            sleep(1);
        }
    }
    else
    {
        int status;
        wait(&status);
        printf("%d\n", status);
    }

    return 0;
}

子程序每隔一秒打印一次,在第五次打印后会收到信号结束自己,父进程用status保存信号,并打印出来:
进程信号的产生
可以看到status值为134,说明子进程收到6号信号(SIGABRT信号)而退出,并Core Dump。

4、由软件条件产生信号
两个进程在用管道进行通信时,如果读端关闭,而写端继续写,此时就会产生SIGPIPE信号。在进程中我们也可以用alarm函数设定一个闹钟,当时间到后系统向进程发送SIGALRM信号,这也是由软件条件产生信号。

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调⽤alarm函数可以设定⼀个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 返回值是0或者是以前设定的闹钟时间还余下的秒数。如果seconds值为0,表⽰取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。该信号的默认处理动作是终⽌止当前进程。

给出这样一段代码:

int main(void)
{
    alarm(10);
    sleep(2);
    int t = alarm(2);
    printf("remaining time :> %d\n", t);

    return 0;
}

先设定一个10秒的闹钟,过2秒后再设定一个2秒的闹钟,此时应该返回8,再过两秒系统发送SIGALRM信号给进程,进程被唤醒结束进程,因此运行程序后打印出字符串和8:
进程信号的产生
这就是一种软件条件产生的信号。