Unix/Linux编程:信号类型和默认行为

本文详细介绍了Linux中的信号机制,包括硬件异常产生的信号如SIGBUS、SIGFPE、SIGILL和SIGSEGV,以及如何处理这些信号。讨论了忽略或阻塞这些信号的后果,并提供了一个示例程序演示了处理SIGFPE的不当方式。此外,还提到了其他一些常见信号,如SIGALRM、SIGCHLD和SIGKILL,以及它们的用途和默认行为。文章强调了信号作为进程间通信的限制,并给出了父子进程使用SIGUSR1和SIGUSR2进行通信的例子。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

这里指出,Linux对标准信号的编号为1~31。然而,Linux 于 signal(7)手册页中列出的信号名称却超出了 31 个。名称超出的原因有多种。有些名称只是其他名称的同义词,之所以定义是为了与其他 UNIX 实现保持源码兼容。其他名称虽然有定义,但却并未使用。以下列表介绍了各种信号

Linux的可用信号都定义在bits\signum.h中,其中包括标准信号和POSIX实时信号

在这里插入图片描述
在这里插入图片描述

信号

硬件产生的信号

硬件异常可以产生 SIGBUS、SIGFPE、SIGILL,和 SIGSEGV 信号,调用 kill()函数来发送此类信号是另一种途径,但较为少见。SUSv3 规定,在硬件异常的情况下,如果进程从此类信号的处理器函数中返回,亦或是进程忽略或者阻塞了此类信号,那么进程的行为未定义。原因如下:

  • 从信号处理器中返回:假设机器语言指令产生了上述信号之一,并因此而调用了信号处理器函数。当从处理器函数正常返回后,程序会尝试从其中断处恢复执行。可当初引发信号产生的恰恰正是这条指令,所以信号会再次“光临”。故事的结局通常是,程序进入无限循环,重复调用信号处理器函数
  • 忽略信号:忽略因硬件而产生的信号于情理不合,试想算术异常之后,程序应当如何
    继续执行呢?无法明确。当由于硬件异常而产生上述信号之一时,Linux 会强制传递信号,即使程序已经请求忽略此类信号。
  • 阻塞信号。与上一种情况一样,阻塞因硬件而产生的信号也不合情理:不清楚程序随
    后应当如何继续执行

正确处理硬件产生信号的方法有二:要么接受信号的默认行为(进程终止);要么为其编
写不会正常返回的处理器函数。除了正常返回之外,终结处理器执行的手段还包括调用_exit()以终止进程,或者调用 siglongjmp(),确保将控制传递回程序中(产生信号的指令
位置之外)的某一位置

下面程序展示了忽略或者阻塞 SIGFPE 信号的后果,或者可正常返回的处理器将其捕获的结果。

#include <memory.h>
#include <stdlib.h>
#include <stdio.h>
#include <zconf.h>
#define _GNU_SOURCE             /* Get strsignal() declaration from <string.h> */
#include <string.h>
#include <signal.h>

static void
sigfpeCatcher(int sig)
{
    printf("Caught signal %d (%s)\n", sig, strsignal(sig));
    /* UNSAFE (see Section 21.1.2) */
    sleep(1);                   /* Slow down execution of handler */
}
int
main(int argc, char *argv[])
{
    /* If no command-line arguments specified, catch SIGFPE, else ignore it */

    if (argc > 1 && strchr(argv[1], 'i') != NULL) {
        printf("Ignoring SIGFPE\n");
        if (signal(SIGFPE, SIG_IGN) == SIG_ERR)   {
            printf("signal");
            exit(EXIT_FAILURE);
        }
    } else {
        printf("Catching SIGFPE\n");

        struct sigaction sa;
        sigemptyset(&sa.sa_mask);
        sa.sa_flags = SA_RESTART;
        sa.sa_handler = sigfpeCatcher;
        if (sigaction(SIGFPE, &sa, NULL) == -1) {
            printf("sigaction");
            exit(EXIT_FAILURE);
        }
    }

    bool blocking = argc > 1 && strchr(argv[1], 'b') != NULL;
    sigset_t prevMask;
    if (blocking) {
        printf("Blocking SIGFPE\n");

        sigset_t blockSet;
        sigemptyset(&blockSet);
        sigaddset(&blockSet, SIGFPE);
        if (sigprocmask(SIG_BLOCK, &blockSet, &prevMask) == -1){
            printf("sigprocmask");
            exit(EXIT_FAILURE);
        }
    }

    printf("About to generate SIGFPE\n");
    int x, y;
    y = 0;
    x = 1 / y;
    y = x;      /* Avoid complaints from "gcc -Wunused-but-set-variable" */

    if (blocking) {
        printf("Sleeping before unblocking\n");
        sleep(2);
        printf("Unblocking SIGFPE\n");
        if (sigprocmask(SIG_SETMASK, &prevMask, NULL) == -1){
            printf("sigprocmask");
            exit(EXIT_FAILURE);
        }
    }

    printf("Shouldn't get here!\n");
    exit(EXIT_FAILURE);
}

在这里插入图片描述

SIGBUS

  • 产生该信号(总线错误,bus error)即表示发生了某种内存访问错误。
  • 当使用由 mmap()所创建的内存映射时,如果试图访问的地址超出了底层内存映射文件的结尾,那么将产生该错误

SIGFPE

  • 该信号因特定类型的算术错误而产生,比如除以0。
  • 后缀 FPE 是浮点异常的缩写,不过整型算术错误也能产生该信号。
  • 该信号于何时产生的精确细节取决于硬件架构和对 CPU 控制寄存器的设置

SIGILL

  • 如果进程试图执行非法(即格式不正确)的机器语言指令,系统将向进程发送该信号。

SIGABRT

  • 当进程调用abort()函数时,系统向进程发送该信号。默认情况下,该信号会终止进程,并产生核心转储文件。
  • 产生核心转储文件用于调试

SIGALRM

  • 当alarm函数设置的定时器超时时,产生此信号
  • setitimer函数设置的间隔时间已经超时时,产生此信号
#include <signal.h>
#include<stdio.h>
#include <unistd.h>

void  handler();

int main()
{
    int i;

    signal(SIGALRM,handler);
    alarm(5);

    for(i=1;i<8;i++){
        printf("sleep is -----%d\n",i);
        sleep(1);
    }
    return 0;
}

void  handler()
{
    printf("hello\n");
}


在这里插入图片描述
分析:alarm当时间(5s)超时时,会发送 SIGALRM 信号,从而调用 signal注册的函数handler,handler的动作是打印一个hello

SIGCHLD

  • 当父进程的某一子进程终止(或者因为调用了 exit(),或者因为被信号杀死)时,(内核)将向父进程发送该信号。系统默认,忽略此信号。
  • 如果父进程希望被告知其子进程的这种状态改变,应该捕捉这个信号
  • 信号捕捉函数中通常序表调用wait函数以取得子进程ID和其终止状态
  • 当父进程的某一子进程因收到信号而停止或恢复时,也可能会向父进程发送该信号。

SIGCLD

与 SIGCHLD 信号同义。

SIGEMT

  • Unix系统通常用该信号来标识一个依赖于实现的硬件错误。
  • Linux 系统仅在 Sun SPARC实现中使用了该信号。后缀 EMT 源自仿真器陷阱(emulator trap),Digital PDP-11 的汇编程序助记符之一

SIGHUP

  • 当终端断开(挂机)时,将发送该信号给终端控制进程。
  • SIGHUP 信号还可用于守护进程(比如,init、httpd 和 inetd)。许多守护进程会在收到 SIGHUP 信号时重新进行初始化并重读配置文件
  • 借助于显式执行 kill命令或者运行同等功效的程序或脚本,系统管理员可向守护进程手工发送 SIGHUP 信号来触发这些行为

SIGINFO

在 Linux 中,该信号名与 SIGPWR 信号名同义。在 BSD 系统中,键入 Control-T 可产生SIGINFO 信号,用于获取前台进程组的状态信息

SIGINT

  • 当用户键入终端中断字符(通常为 Control-C)时,终端驱动程序将发送该信号给前台进程组。该信号的默认行为是终止进程

SIGIO

  • 利用 fcntl()系统调用,即可于特定类型(诸如终端和套接字)的打开文件描述符发生 I/O事件时产生该信号

SIGIOT

SIGKILL 和 SIGSTOP

  • SIGKILL 信号的默认行为是终止一个进程,SIGSTOP 信号的默认行为是停止一个进程,二者的默认行为均无法改变。
  • 当试图用 signal()和 sigaction()来改变对这些信号的处置时,将总是返回错误。同样,也不能将这两个信号阻塞
  • 这是一个深思熟虑的设计决定。不允许修改这些信号的默认行为,这也意味着总是可以利用这些信号来杀死或者停止一个失控进程。

SIGCONT

  • 将该信号发送给已停止的进程,进程将会恢复运行(即在之后某个时间点重新获得调度)。
  • 当接收信号的进程当前不处于停止状态时,默认情况下将忽略该信号。
  • 进程可以捕获该信号,以便在恢复运行时可以执行某些操作。

可使用 SIGCONT 信号来使某些(因接收 SIGSTOP、SIGTSTP、SIGTTIN 和SIGTTOU 信号而)处于停止状态的进程得以继续运行。由于这些停止信号具有独特目的,所以在某些情况下内核对它们的处理方式将有别于其他信号:

  • 如果一个进程处于停止状态,那么一个 SIGCONT 信号的到来总是会促使其恢复运行,即使该进程正在阻塞或者忽略 SIGCONT 信号。该特性之所以必要,是因为如果要恢复这些处于停止状态的进程,舍此之外别无他法。(如果处于停止状态的进程正在阻塞 SIGCONT 信号,并且已经为 SIGCONT 信号建立了处理器函数,那么在进程恢复运行后,只有当取消了对 SIGCONT的阻塞时,进程才会去调用相应的处理器函数。
  • 每当进程收到 SIGCONT 信号时,会将处于等待状态的停止信号丢弃(即进程根本不知道这些信号)。相反,如果任何停止信号传递给了进程,那么进程将自动丢弃任何处于等待状态的 SIGCONT 信号。之所以采取这些步骤,意在防止之前发送的一个停止信号会在随后撤销SIGCONT 信号的行为,反之亦然

如果有任一其他信号发送给了一个已经停止的进程,那么在进程收到 SIGCONT 信号而恢复运行之前,信号实际上并未传递。SIGKILL 信号则属于例外,因为该信号总是会杀死进程,即使进程目前处于停止状态。

可中断和不可中断的进程睡眠状态

SIGKILL和SIGSTOP信号对进程的作用是立竿见影的。对于这一论断,需要加入一个前提。内核经常需要令进程进入休眠。而休眠状态分为两种

  • TASK_INTERRUPTIBLE:
    • 进程正在等待某一事件。比如:
      • 正在等待终端输入
      • 等待数据写入当前的空管道
      • 等待 System V 信号量值的增加
    • 进程在该状态下所耗费的时间可长可短。
    • 如果为这种状态下的进程产生一个信号,那么操作将中断,而传递来的信号将唤醒进程。
    • ps(1)命令在显示处于 TASK_INTERRUPTIBLE 状态的进程时,会将其 STAT(进程状态)字段标记为字母 S
  • TASK_UNINTERRUPTIBLE:
    • 进程正在等待某些特定类型的事件,比如:
      • 磁盘 I/O 的完成。
    • 如果为这种状态下的进程产生一个信号,那么在进程摆脱这种状态之前,系统将不会把信号传递给进程。
    • ps(1)命令在显示处于 TASK_UNINTERRUPTIBLE 状态的进程时,会将其 STAT 字段标记为字母 D

因为进程处于 TASK_UNINTERRUPTIBLE 状态的时间通常转瞬即逝,所以系统在进程脱离该状态时传递信号的现象也不易于被发现。然而,在极少数情况下,进程可能会因硬件故障、NFS 问题或者内核缺陷而在该状态下保持挂起。这时,SIGKILL 将不会终止挂起进程。如果问题诱因无法得到解决,那么就只能通过重启系统来消灭该进程。

大多数UNIX系统实现都支持TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE状态。从内核 2.6.25 开始,Linux 加入第三种状态来解决上述挂起进程的问题。

  • TASK_KILLABLE:
    • 该状态类似于 TASK_UNINTERRUPTIBLE,但是会在进程收到一个致命信号(即一个杀死进程的信号)时将其唤醒。
    • 在对内核代码的相关部分进行改造后,就可使用该状态来避免各种因进程挂起而重启系统的情况。这时,向进程发送一个致命信号就能杀死进程。
    • 为使用 TASK_KILLABLE 而进行代码改造的首个内核模块是 NFS

利用信号进行进程间通信

从某种角度,可将信号视为进程间通信(IPC)的方式之一。然而,信号作为一种 IPC 机制却也饱受限制:

  • 信号的异步本质就意味着需要面对各种问题,包括可重入性需求、竞态条件及在信号处理器中正确处理全局变量。(如果用 sigwaitinfo()或者 signalfd()来同步获取信号,这些问题中的大部分都不会遇到。)
  • 没有对标准信号进行排队处理。即使是对于实时信号,也存在对信号排队数量的限制。这意味着,为了避免丢失信息,接收信号的进程必须想方设法通知发送者,自己为接受另一个信号做好了准备。要做到这一点,最显而易见的方法是由接收者向发送者发送信号

还有一个更深层次的问题,信号所携带的信息量有限:信号编号以及实时信号情况下一字之长的附加数据(一个整数或者一枚指针值)。与诸如管道之类的其他 IPC 方法相比,过低的带宽使得信号传输极为缓慢。

由于上述种种限制,很少将信号用于 IPC。

父子进程使用SIGUSR1和SIGUSR2进行通信

SIGUSR1 用户自定义信号 默认处理:进程终止
SIGUSR2 用户自定义信号 默认处理:进程终止

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

void handler(int signo)
{
    switch(signo) {
        case SIGUSR1: //处理信号 SIGUSR1
            printf("Parent : catch SIGUSR1\n");
            break;
        case SIGUSR2: //处理信号 SIGUSR2
            printf("Child : catch SIGUSR2\n");
            break;
        default:      //本例不支持
            printf("Should not be here\n");
            break;
    }
}

int main(void)
{
    pid_t ppid, cpid;
    //为两个信号设置信号处理函数
    if(signal(SIGUSR1, handler) == SIG_ERR)
    { //设置出错
        perror("Can't set handler for SIGUSR1\n");
        exit(1);
    }

    if(signal(SIGUSR2, handler) == SIG_ERR)
    { //设置出错
        perror("Can't set handler for SIGUSR2\n");
        exit(1);
    }

    ppid = getpid();//得到父进程ID

    if((cpid = fork()) < 0)
    {
        perror("fail to fork\n");
        exit(1);
    }
    else if(cpid == 0)
    {
        // 子进程内向父进程发送信号SIGUSER1
        if(kill(ppid, SIGUSR1) == -1)
        {
            perror("fail to send signal\n");
            exit(1);
        }

        while(1);//死循环,等待父进程的信号
    }
    else
    {
        sleep(1);//休眠,保证子进程先运行,并且发送SIGUSR1信号
        // 父进程向子进程发送SIGUSER2信号
        if(kill(cpid, SIGUSR2) == -1)
        {
            perror("fail to send signal\n");
            exit(1);
        }

        // 必须sleep一下,否则子进程捕获不到SIGUSER2信号
        sleep(1);

        printf("will kill child\n");//输出提示
        if(kill(cpid, SIGKILL) == -1)
        { //发送SIGKILL信号,杀死子进程
            perror("fail to send signal\n");
            exit(1);
        }

        if(wait(NULL) ==-1)
        { //回收子进程状态,避免僵尸进程
            perror("fail to wait\n");
            exit(1);
        }
        printf("child has been killed.\n");
    }
    return 0;
}
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
 
static void sig_usr(int);
int main(void)
{
        if(signal(SIGUSR1, sig_usr) == SIG_ERR)
            printf("can not catch SIGUSR1\n");
        if(signal(SIGUSR2, sig_usr) == SIG_ERR)
            printf("can not catch SIGUSR2\n");
        for(;;)
                pause();
}
 
static void sig_usr(int signo)
{
        if(signo == SIGUSR1)
            printf("received SIGUSR1\n");
        else if(signo == SIGUSR2)
            printf("received SIGUSR2\n");
        else
            printf("received signal %d\n", signo);
}
[chinsung@thinkpad apue]$ ./a.out &
[1] 2581
[chinsung@thinkpad apue]$ kill -USR1 2581
received SIGUSR1
[chinsung@thinkpad apue]$ kill -USR2 2581
received SIGUSR2
[chinsung@thinkpad apue]$ kill 2581
[1]+ Terminated              ./a.out
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值