longjmp () из обработчика сигнала - PullRequest
5 голосов
/ 11 ноября 2009

Я использую следующий код, чтобы попытаться прочитать ввод от пользователя и тайм-аут и выйти, если прошло более 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, даже если пользователь что-то ввел?

ПОСЛЕДНЕЕ РЕДАКТИРОВАНИЕ: Меня не интересуют способы улучшения кода. Я просто хочу знать, как это может потерпеть неудачу.

Ответы [ 7 ]

7 голосов
/ 11 ноября 2009

Со страницы руководства для longjmp:

POSIX не указывает, longjmp() восстановит сигнал контекст. Если вы хотите сохранить и восстановить маски сигналов, используйте siglongjmp()

Ваш второй вопрос: Да, функция вернет -2, потому что longjmp() заставит ее перейти к части setjmp(buffer), но время будет очень точным.

2 голосов
/ 26 июля 2010

Другой хороший (или уродливый в зависимости от вашей точки зрения) способ заставить 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.

2 голосов
/ 11 ноября 2009

Когда речь идет о том, что может пойти не так, когда поведение не определено, единственный ответ, на который вы можете рассчитывать, это «все, включая ничто». Возможно, все идет не так, как надо, может быть, у вас есть ошибка, может быть, вы получаете носовые демоны.

Более конкретные ответы зависят от того, с какой системой и выпуском вы работаете. Например, в дистрибутивах Linux (по крайней мере, все с 2000 года) ядро ​​выполняет некоторые задачи после возврата обработчика сигнала. Если вы используете longjmp, вы, вероятно, оставите мусор в стеке ядра, который впоследствии может вызвать проблемы, такие как ошибочный возврат к коду, который выполняла ваша программа, когда был пойман сигнал (в этом примере был вызван вызов 'fgets'). Или нет.

Вызов longjmp в обработчике сигналов также может (в общем, но, возможно, не в вашем примере) создать дыру в безопасности .

1 голос
/ 11 ноября 2009

Не думаю, что вам нужно использовать setjmp / longjmp. fgets должно прерываться сигналами (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);
}
0 голосов
/ 12 ноября 2009

Вы можете заменить longjmp / setjmp на siglongjmp / sigsetjump, и тогда не возникнет проблема неопределенности контекста сигнала после jmp. Возможно, вам здесь все равно, поскольку вы явно не меняете маску. Я забыл, изменилась ли маска самим сигнальным вызовом.

Возможно, самая большая проблема заключается в обеспечении безопасности вашего кода. Например, получает ли fgets () какой-либо мьютекс (возможно, неявно как часть вызова malloc)? Если это так, и ваш таймер выключается, пока удерживается этот мьютекс, ваша программа запускается в следующий раз, когда вы попытаетесь выделить кучу.

0 голосов
/ 11 ноября 2009

что если тревога сработает сразу после fgets () возвращается, но до тревоги (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);
}
0 голосов
/ 11 ноября 2009

По вашему второму вопросу вы можете добавить блокировку, которая блокирует возврат -2, когда основной поток пройдет вызов fgets.

...