不可靠信号指的是信号可能会丢失,不支持信号阻塞,不能控制是否重启中断的系统调用等等。

下面是不可靠信号可能会出现的问题:

信号丢失

不可靠指的是信号可能会丢失:一个信号发生了,但进程却可能一直不知道。

不支持阻塞信号

内核不支持阻塞信号的功能:用户希望内核阻塞某个信号,但不要忽略,再进程准备好了再通知它。

重置信号动作为默认值

进程再每次接收到信号对其进行处理时,会将该信号动作重置为默认值

使用早期信号的示例:

int sig_int();
signal(SIGINT, sig_int);

sig_int()
{
    // 存在时间窗口,此时产生第二次SIG_INT信号会执行默认动作,终止进程
    signal(SIGINT, sig_int);
}

不能关闭信号

当进程不希望某种信号发生时,它不能关闭该信号。

所以这种情况下关闭信号的方式就是捕捉该信号,然后不做任何处理。

下面以SIGINT信号作为示例(发生SIGINT信号时会进行标记):

int sig_int();
int sig_int_flag;
main()
{
    signal(SIGINT, sig_int);
    while(sig_int_flag)
        // 这里存在时间窗口,此时发生信号会导致进程已经从信号处理程序返回了,但pause()还没准备好
        pause(); // 在进程执行完任一信号处理程序并从其中返回时,该函数才会返回。
}

sig_int()
{
    signal(SIGINT, sig_int);
    sig_int_flag = 1;
}

存在问题:while检测条件与pause()之间存在时间窗口,在时间窗口中发生SIGINT信号且仅发生这一次,那么pause()函数就会导致进程永久阻塞。

这个例子也能够表明信号可能会丢失。

中断系统调用

进程在执行低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行。该系统调用返回出错(内核中执行的系统调用而非函数),errno设置为EINTR

低速系统调用是可能会使进程永远阻塞的一类系统调用

  1. 如果某些类型文件(如读管道、终端设备和网络设备)的数据不存在,则读操作可能会使调用者永远阻塞。
  2. 如果这些数据不被相同的类型文件立即接受,那么写操作可能会使调用者永远阻塞(例如写满管道之后,再继续写就会阻塞,直到读管道数据以释放空间)。
  3. 在某种条件发生之前打开某些类型文件,可能会发生阻塞(例如要打开一个终端设备,要先等与之连接的调制解调器应答)。
  4. pause函数(该函数使调用进程休眠直至捕捉到一个信号)和wait函数。
  5. 某些ioctl操作。
  6. 某些进程间通信函数。

在这种情况下,必须显式地处理出错返回。下面的例子是进行一个读操作,然后被中断,再重新启动该读操作:

again:
    // 早期的信号处理中,中断系统调用返回出错,read()返回-1
    // POSIX中read返回已读取的字节数
    if ((n = read(fd, buf, BUFFSIZE)) < 0) {
        if(errno == EINTR)
            goto again;
    }

为了帮助应用程序不必处理被中断的系统调用,之后的信号处理支持基于每个信号来配置是否自动重启动系统调用

Linux中read系统调用默认会被自动重启动,下面的例子验证了这一点:

// 信号中断读操作
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

void sig_int(int signo)
{
    printf("%d\n", signo);
}

int main()
{
    signal(SIGINT, sig_int);
    char buf[1024] = {0};
    // read阻塞期间,向进程发送SIGINT信号,会使进程中断read调用,转而执行信号处理程序,之后read操作又会恢复
    read(STDIN_FILENO, buf, sizeof(buf));
    printf("%s\n", buf);
}

执行结果如下:

$ ./interrupt_sys_read # 回车之后阻塞在read系统调用
^C2 # 中断字符Ctrl+C发送SIGINT信号,执行信号处理函数,输出信号编号2
test # read系统调用恢复,并输入数据
test

可重入函数

进程捕捉到信号并对其进行处理时,进程正在执行的正常指令序列就被信号处理程序临时中断,它首先执行该信号处理程序中的指令。

如果从信号处理程序返回,则继续执行在捕捉到信号时进程正在执行的指令序列。

可重入函数也被称为异步信号安全函数,这些函数在信号处理操作期间,会阻塞任何会引起不一致的信号发送。

大多数函数不可重入的原因有以下3点(即使是线程安全的):

  1. 使用静态数据结构。
  2. 调用malloc或free。
  3. 标准I/O函数(使用全局数据结构)。

因此存在问题,在不支持阻塞信号的情况下,调用不可重入函数就有可能导致错误。

例如,如果进程正在调用malloc,在堆中分配另外的存储空间,而此时捕捉到信号而插入执行该信号处理程序,其中又调用malloc,就有可能破坏malloc维护的存储区链表。

又例如,getpwnam函数会将返回值存放在全局静态指针中,如果getpwnam函数获取到值并且存在到指针中之后,还未来得及返回,此时捕捉到信号而插入执行信号处理程序,其中又调用getpwnam,则有可能改变全局指针静态指针的内容,进而导致getpwnam函数返回出错。

同理,每个线程仅有一个errno变量,由于不支持阻塞信号,进程来不及做处理信号的准备,所以信号处理程序就可能会修改其原先值。 这个问题无论是否调用可重入函数都会存在,但可以在信号处理程序中调用函数前备份,调用后再恢复。

重置信号动作导致捕捉SIGCLD信号出错

早期的System V对SIGCLD的语义有两种处理方式:

  1. 如果进程明确地将该信号的配置设置为SIG_IGN(默认SIG_IGN不会),则调用进程的子进程不会产生僵死进程。 子进程在终止时,会将其状态丢弃。如果调用进程随后调用wait函数则会阻塞到所有子进程终止,并出错返回,errno设置为ECHILD
  2. 如果进程捕捉该信号,则内核会立即检查是否有子进程准备好被等待,如果有,则立即调用该信号的处理程序。

第2中处理方式会引发问题,问题是早期信号处理会重置信号动作为默认值,因此要在信号处理程序中立即重新设置(减少时间窗口),但这会导致重新调用信号处理程序,进而陷入死循环。

#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
static void sig_cld(int signo)
{
    pid_t pid;
    int status;
    printf("SIGCLD, received\n");
    // 立即重新设置信号处理程序,减少时间窗口,防止信号丢失
    // 但设置完之后,内核检测到有需要等待的子进程,然后又调用信号处理程序,在wait之后调用该函数才会解决这个问题,但是这中间会丢失信号
    if (signal(SIGCLD, sig_cld) == SIG_ERR)
        perror("signal error");
    if ((pid = wait(&status)) < 0)
        perror("wait error");

    printf("pid=%d\n", pid);
}
int main()
{
    pid_t pid;
    if (signal(SIGCLD, sig_cld) == SIG_ERR)
        perror("signal error");
    if ((pid = fork()) < 0)
        perror("fork error");
    else if (pid == 0)
    {
        sleep(2);
        _exit(0);
    }
    pause();  // 父进程阻塞,直到信号处理程序执行完
    exit(0);
}

POSIX.1 没有明确要求SIGCHLD信号被忽略时的行为,因此Linux对SIGCHLD信号被忽略的行为与1中相同,即不会产生僵死进程。如下例子验证了这一点

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

int main()
{
    signal(SIGCHLD, SIG_IGN);
    pid_t pid;
    if ((pid = fork()) < 0)
        perror("fork error: ");
    else if (pid > 0)
    {
        printf("pid=%d\n", pid);
        while (1)
        {
            sleep(3);
        }
    }
    else
    {
        exit(0);
    }
}

执行结果如下:

$ ./child_proc_zombie &
$ ps aux | grep child* | grep -v grep
blduan   1916538  0.0  0.0   2616  1408 pts/1    S    11:13   0:00 ./child_proc_zombie # S表明不是僵死进程

对于2中引发的问题,POSIX.1不会在信号发生时将信号处理程序设置为默认值,同时Linux不会为已经终止的子进程产生SIGCHLD信号,可以避免上述问题。