不可靠信号的相关问题
不可靠信号指的是信号可能会丢失,不支持信号阻塞,不能控制是否重启中断的系统调用等等。
下面是不可靠信号可能会出现的问题:
信号丢失
不可靠指的是信号可能会丢失:一个信号发生了,但进程却可能一直不知道。
不支持阻塞信号
内核不支持阻塞信号的功能:用户希望内核阻塞某个信号,但不要忽略,再进程准备好了再通知它。
重置信号动作为默认值
进程再每次接收到信号对其进行处理时,会将该信号动作重置为默认值。
使用早期信号的示例:
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
。
低速系统调用是可能会使进程永远阻塞的一类系统调用:
- 如果某些类型文件(如读管道、终端设备和网络设备)的数据不存在,则读操作可能会使调用者永远阻塞。
- 如果这些数据不被相同的类型文件立即接受,那么写操作可能会使调用者永远阻塞(例如写满管道之后,再继续写就会阻塞,直到读管道数据以释放空间)。
- 在某种条件发生之前打开某些类型文件,可能会发生阻塞(例如要打开一个终端设备,要先等与之连接的调制解调器应答)。
- pause函数(该函数使调用进程休眠直至捕捉到一个信号)和wait函数。
- 某些ioctl操作。
- 某些进程间通信函数。
在这种情况下,必须显式地处理出错返回。下面的例子是进行一个读操作,然后被中断,再重新启动该读操作:
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点(即使是线程安全的):
- 使用静态数据结构。
- 调用malloc或free。
- 标准I/O函数(使用全局数据结构)。
因此存在问题,在不支持阻塞信号的情况下,调用不可重入函数就有可能导致错误。
例如,如果进程正在调用malloc
,在堆中分配另外的存储空间,而此时捕捉到信号而插入执行该信号处理程序,其中又调用malloc
,就有可能破坏malloc
维护的存储区链表。
又例如,getpwnam
函数会将返回值存放在全局静态指针中,如果getpwnam
函数获取到值并且存在到指针中之后,还未来得及返回,此时捕捉到信号而插入执行信号处理程序,其中又调用getpwnam
,则有可能改变全局指针静态指针的内容,进而导致getpwnam
函数返回出错。
同理,每个线程仅有一个errno
变量,由于不支持阻塞信号,进程来不及做处理信号的准备,所以信号处理程序就可能会修改其原先值。 这个问题无论是否调用可重入函数都会存在,但可以在信号处理程序中调用函数前备份,调用后再恢复。
重置信号动作导致捕捉SIGCLD信号出错
早期的System V对SIGCLD的语义有两种处理方式:
- 如果进程明确地将该信号的配置设置为
SIG_IGN
(默认SIG_IGN
不会),则调用进程的子进程不会产生僵死进程。 子进程在终止时,会将其状态丢弃。如果调用进程随后调用wait
函数则会阻塞到所有子进程终止,并出错返回,errno
设置为ECHILD
。 - 如果进程捕捉该信号,则内核会立即检查是否有子进程准备好被等待,如果有,则立即调用该信号的处理程序。
第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
信号,可以避免上述问题。
- 原文作者:生如夏花
- 原文链接:https://blduan.top/post/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/apue/%E4%B8%8D%E5%8F%AF%E9%9D%A0%E4%BF%A1%E5%8F%B7%E7%9A%84%E7%9B%B8%E5%85%B3%E9%97%AE%E9%A2%98/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。