使用 SIGALRM 中断 open()

Interrupting open() with SIGALRM

我们有一个遗留的嵌入式系统,它使用 SDL 从 NFS 共享中读取图像和字体。

如果出现网络问题,TTF_OpenFont() 和 IMG_Load() 基本上永远挂起。测试应用程序显示 open() 的行为方式相同。

我们想到一个快速修复方法是在调用打开 NFS 共享上的文件之前调用 alarm()。手册页并不完全清楚 open() 在被 SIGALRM 中断时是否会因 EINTR 而失败,因此我们整理了一个测试应用程序来验证这种方法。我们设置了一个信号处理程序,将 sigaction::sa_flags 设置为零,以确保未设置 SA_RESTART。

已调用信号处理程序,但未中断 open()。 (我们观察到 SIGINT 和 SIGTERM 的相同行为。)

我想即使在 NFS 等 "slow" 基础设施上,系统也将 open() 视为 "fast" 操作。

有什么方法可以改变这种行为并允许 open() 被信号中断吗?

The man pages weren't entirely clear whether open() would fail with EINTR when interrupted by SIGALRM, so we put together a test app to verify this approach.

open(2) 是一个慢速系统调用(慢速系统调用是那些可以永远休眠,并且可以在同时捕获到信号时唤醒的系统调用)仅适用于某些文件类型。通常,在某些情况发生之前阻塞调用者的打开通常是可中断的。已知示例包括打开 FIFO(命名管道),或(在过去)打开物理终端设备(它会休眠直到调制解调器被拨号)。

NFS 安装的文件系统可能不会导致 open(2) 进入可中断状态。毕竟你打开的多半是普通文件,那样的话open(2)就不会被中断了。

Is there any way to change this behaviour and allow open() to be interrupted by a signal?

我不这么认为,除非对内核进行一些(重要的)更改。

我会探讨使用 setjmp(3) / longjmp(3) 的可能性(如果您不熟悉,请参阅联机帮助页;它基本上是非本地 goto)。您可以在调用 open(2) 之前初始化环境缓冲区,并在信号处理程序中发出 longjmp(3)。这是一个例子:

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

static jmp_buf jmp_env;

void sighandler(int signo) {
    longjmp(jmp_env, 1);
}

int main(void) {
    struct sigaction sigact;
    sigact.sa_handler = sighandler;
    sigact.sa_flags = 0;
    sigemptyset(&sigact.sa_mask);

    if (sigaction(SIGALRM, &sigact, NULL) < 0) {
        perror("sigaction(2) error");
        exit(EXIT_FAILURE);
    }

    if (setjmp(jmp_env) == 0) {
        /* First time through
         * This is where we would open the file
         */

        alarm(5);

        /* Simulate a blocked open() */
        while (1)
            ; /* Intentionally left blank */

        /* If open(2) is successful here, don't forget to unset
         * the alarm
         */

        alarm(0);
    } else {
        /* SIGALRM caught, open(2) canceled */
        printf("open(2) timed out\n");
    }
    return 0;
}

它的工作原理是在调用 open(2) 之前借助 setjmp(3) 保存上下文环境。 setjmp(3) returns 0 第一次通过,returns 否则传递给 longjmp(3) 的任何值。

请注意,此解决方案并不完美。请记住以下几点:

  • 在调用 alarm(2) 和调用 open(2) 之间有 window 的时间(此处用 while (1) { ... } 模拟),进程可能会被抢占很长一段时间,所以在我们实际尝试打开文件之前,警报可能会过期。当然,如果超时时间较长(例如 2 或 3 秒),这很可能不会发生,但这仍然是一个竞争条件。
  • 同样,在成功打开文件和取消警报之间有一个 window 的时间,同样,进程可能会被抢占很长时间,并且警报可能会在我们有机会之前过期取消它。这有点糟糕,因为我们已经打开了文件,所以我们将 "leak" 文件描述符。同样,在实践中,如果超时时间长,这可能永远不会发生,但它仍然是一个竞争条件。
  • 如果代码捕捉到其他信号,当捕捉到 SIGALRM 时,可能有另一个信号处理程序正在执行中。在信号处理程序中使用 longjmp(3) 将破坏这些其他信号处理程序的执行上下文,并且根据它们的操作,可能会发生非常讨厌的事情(如果信号处理程序正在操作程序中的其他数据结构,则状态不一致, ETC。)。就好像它开始执行,然后突然在中间某处崩溃了。您可以通过以下方式修复它:a) 仔细设置所有信号处理程序,以便 SIGALRM 在它们被调用之前被阻塞(这确保 SIGALRM 处理程序在其他处理程序完成之前不会开始执行)和 b ) 在捕获 SIGALRM 之前阻止这些其他信号。这两个操作都可以通过使用必要的掩码设置 struct sigactionsa_mask 字段来完成(操作系统在开始执行处理程序之前自动将进程的信号掩码设置为该值,并在从处理程序)。 OTOH,如果其余代码没有捕捉到信号,那么这不是问题。
  • sleep(3)可以和alarm(2)实现,alarm(2)setitimer(2)共用一个定时器;如果代码中的其他部分使用这些函数中的任何一个,它们将会干扰并且结果将是一团糟。

在盲目使用这种方法之前,请务必权衡这些缺点。通常不鼓励使用 setjmp(3) / longjmp(3),这会使程序更难阅读、理解和维护。这并不优雅,但目前我认为您别无选择,除非您愿意在项目中进行一些核心重构。

如果您最终使用 setjmp(3),那么至少要记录这些限制。

也许有一种策略是使用单独的线程来执行打开操作,这样主线程的等待时间不会超过预期。