6

我正在使用以下代码尝试从用户读取输入并超时并在超过 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?

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

4

7 回答 7

8

从 longjmp 的手册页:

POSIX 没有指定是否 longjmp()会恢复信号上下文。如果要保存和恢复信号掩码,请使用siglongjmp()

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

于 2009-11-11T14:24:02.897 回答
2

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

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

在信号处理程序中调用 longjmp 也可以(通常,但在您的示例中可能不会)引入安全漏洞

于 2009-11-11T15:34:23.233 回答
2

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

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

And in the signal handler:

close(0);

Presumably this works even on ancient systems without sigaction and with BSD signal semantics.

于 2010-07-26T12:56:46.620 回答
1

我认为您不需要使用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);
}
于 2009-11-11T14:24:53.153 回答
0

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

于 2009-11-11T14:29:45.330 回答
0

如果警报在 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);
}
于 2009-11-11T14:35:53.760 回答
0

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

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

于 2009-11-12T04:14:43.623 回答