【问题标题】:longjmp() from signal handler来自信号处理程序的 longjmp()
【发布时间】:2009-11-11 14:13:58
【问题描述】:

我正在使用以下代码尝试从用户读取输入并超时并在超过 5 秒后退出。这是通过 setjmp/longjmp 和 SIGALRM 信号的组合来实现的。

代码如下:

#include <stdio.h>
#include <setjmp.h>
#include <unistd.h>
#include <string.h>
#include <sys/signal.h>

jmp_buf buffer;

// this will cause t_gets() to return -2
void timeout() {
    longjmp(buffer, 1);
}

int t_gets(char* s, int t)
{
    char* ret;
    signal(SIGALRM, timeout);
    if (setjmp(buffer) != 0)
        return -2; // <--- timeout() will jump here
    alarm(t);
    // if fgets() does not return in t seconds, SIGALARM handler timeout()
    // will be called, causing t_gets() to return -2
    ret = fgets(s, 100, stdin);
    alarm(0);
    if (ret == NULL ) return -1;
    return strlen(s);
}

int main()
{
    char s[100];
    int z=t_gets(s, 5);
    printf("%d\n", z); 
}

现在,我的问题是这个功能是否有什么问题。我读过从信号处理程序调用 longjmp() 可能有未定义的行为,它到底指的是什么?

另外,如果警报在 fgets() 返回之后,但在调用 alarm(0) 之前触发怎么办?即使用户确实输入了一些内容,它是否会导致函数返回 -2?

稍后编辑: 我对改进代码的方法不感兴趣。我只是想知道它是如何失败的。

【问题讨论】:

  • ret 没有初始化,所以调用fgets 会导致内存损坏。
  • @outis: ret 被分配了fgets() 函数调用的结果。
  • fgets() 在成功的情况下返回第一个参数,在任何错误的情况下返回 NULL。我看不出这会如何导致任何内存损坏。内容保存在s中。
  • 没关系 - 快速浏览。误读“fgets(s, ...)”。

标签: c unix signals setjmp


【解决方案1】:

来自 longjmp 的手册页:

POSIX 没有指定是否 longjmp() 将恢复信号 语境。如果你想保存和 恢复信号掩码,使用siglongjmp()

您的第二个问题:是的,该函数将返回 -2,因为 longjmp() 将导致它转到 setjmp(buffer) 部分,但时间必须非常精确。

【讨论】:

  • 信号上下文是否恢复无关紧要,因为调用longjmp()将导致函数返回,并且不再需要警报信号。
  • 注意,常规信号(即 SIGUSR*)在排列时会被内核组合。 RT 信号在它们到来时被传递。这意味着,您使用 longjmp() 破坏了一个原始但有效的消息传递方案,尤其是当线程处于某种 I/O 睡眠(自愿或 D 睡眠)时。
  • 这在您的特定程序中是正确的,因为该函数直接返回到退出的main。函数具有破坏信号掩码的不良副作用这一事实通常是一个问题。
【解决方案2】:

当行为未定义时可能会出现什么问题时,您唯一可以依靠的答案是“任何事情,包括什么都没有”。也许没有什么问题,也许你得到了一个段错误,也许你得到了鼻守护进程。

更具体的答案取决于您使用的系统和版本。例如,在Linux 发行版上(至少从 2000 年开始),内核在信号处理程序返回后执行一些任务。如果您使用 longjmp,您可能会在内核堆栈上留下垃圾,这可能会导致以后出现问题,例如在捕获信号时错误地返回程序正在执行的代码(在示例中调用 'fgets')。或者不。

在信号处理程序中调用 longjmp 也可以(通常,但在您的示例中可能不是)引入 security hole

【讨论】:

  • CERT 文章,就像他们的大多数“安全编码”文章一样,略有错误。从信号处理程序调用longjmp 是安全的当且仅当您可以确保信号处理程序不会中断异步信号不安全函数。不幸的是fgets 是异步信号不安全的,所以 OP 的代码是假的。
【解决方案3】:

另一种使fgets 纾困的好方法(或丑陋,取决于您的观点)是:

int tmp = dup(0);
ret = fgets(s, 100, stdin);
if (!ret && errno == EBADF) clearerr(stdin);
dup2(tmp, 0);
close(tmp);

在信号处理程序中:

close(0);

据推测,即使在没有 sigaction 和 BSD signal 语义的古老系统上,这也适用。

【讨论】:

    【解决方案4】:

    我认为您不需要使用setjmp/longjmpfgets 应该被信号中断(errno 设置为 EINTR),尽管您可能需要使用 sigaction(...) 而不是 signal(...) 来确保 SA_RESTART 是明确的。

    void timeout(int) {
       // doesn't actually need to do anything
    }
    int t_gets(char* s, int t)
    {
        char* ret;
        struct sigaction action = {0};
        action.sa_handler = timeout;
        sigaction(SIGALRM, &action, NULL);
        alarm(t);
        // if fgets() does not return in t seconds, SIGALARM handler timeout()
        // will be called, interrupting fgets and causing t_gets() to return -2
        ret = fgets(s, 100, stdin);
        // even if the alarm is called after fgets returns, it won't erroneously cause
        // t_gets to return -2
        int err = errno;
        alarm(0);
        if (ret == NULL) { 
            switch (err) {
            case EINTR:
                return -2;
            // add other cases as warranted
            default:
                return -1;
            }
        }
        return strlen(s);
    }
    

    【讨论】:

    • fgets() 应该在t 秒后失败,无论是否发送了任何信号。
    • 我不明白fgets 在不发送信号的情况下如何失败。你不是在使用报警信号导致故障吗?
    • 确实,使用sigaction 确实会导致fgets 失败。感谢您的回答。但是(如果您还阅读了我后来的编辑),我对我发布的方法可能会失败的方式更感兴趣。
    【解决方案5】:

    关于您的第二个问题,您可以添加一个锁,当主线程通过 fgets 调用时阻止返回 -2。

    【讨论】:

      【解决方案6】:

      如果警报在之后触发怎么办 fgets() 返回,但在 alarm(0) 之前 叫什么?

      您可以初始化 ret(可能为 NULL)并在 if(setjmp()) 语句的主体中检查:

      /* NOT TESTED */
      int t_gets(char* s, int t)
      {
          char* ret = NULL;
          signal(SIGALRM, timeout);
          if (setjmp(buffer) != 0) {
              // timeout() will jump here
              if (ret == NULL) {
                  return -2;
              } else {
                  goto end_of_function;
              }
          }
          alarm(t);
          // if fgets() does not return in t seconds, SIGALARM handler timeout()
          // will be called, causing t_gets() to return -2
          ret = fgets(s, 100, stdin);
      end_of_function:
          alarm(0);
          if (ret == NULL ) return -1;
          return strlen(s);
      }
      

      【讨论】:

        【解决方案7】:

        您可以将 longjmp/setjmp 替换为 siglongjmp/sigsetjump,这样就不会出现 jmp 后未定义信号上下文的问题。您可能并不真正关心这里,因为您没有明确更改掩码。我忘记了掩码是否被信号调用本身改变了。

        也许更大的问题是确保您的代码是信号安全的。例如, fgets() 是否获取任何互斥体(可能隐含地作为 malloc 调用的一部分)?如果是这样,并且您的计时器在该互斥锁被持有时关闭,那么下次您尝试进行堆分配时,您的程序就会烤面包。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-06-19
          • 1970-01-01
          • 2018-09-04
          • 2017-11-30
          相关资源
          最近更新 更多