Пытаясь воспроизвести полностью несвязанное предполагаемое ложное срабатывание предупреждения о переполнении буфера стека из asan, я заметил кое-что странное. Когда я впоследствии прошу два сигнала тревоги (), второй, очевидно, никогда не срабатывает. Это почему?

Вот MWE:

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

static jmp_buf jump_buffer;

void f()
{
    while(true) {};
}

void handle_timeout(int)
{
    longjmp(jump_buffer, 1);
}

void test()
{
    if (setjmp(jump_buffer) == 0)
    {
            f();
    }
}

int main()
{
    signal (SIGALRM, handle_timeout);
    alarm(2);
    test();
    signal (SIGALRM, handle_timeout);
    alarm(2);
    test();
    return 0;
}

Если вы раскомментируете второй вызов test, программа завершится, как и предполагалось, через 2 секунды, но при этом будет работать вечно.

Мне хорошо известно, что "сигнал автоматически блокируется [...] во время работы обработчика" согласно gnu.org, но разве это время не закончилось с longjump()?

1
choeger 2 Янв 2018 в 16:05

2 ответа

Лучший ответ

Как я отмечал в комментариях, в целом лучше использовать sigaction() а не signal(), потому что это дает вам более точный контроль над тем, как обработка сигнала выполнена. Также лучше использовать pause() (по крайней мере, в непоточных приложениях) ждать, пока не поступит какой-либо сигнал, чем заставить процессор крутиться в тесном бесконечном цикле.

Как отметил какой-то чувак-программист в comment, лучше использовать sigsetjmp() и siglongjmp(), чем использовать setjmp() и longjmp().

Однако, к моему большому удивлению, я смог воспроизвести проблему на macOS High Sierra 10.13.2. Моя инструментальная версия кода использует pause() вместо цикла вращения и использует sigsetjmp() (с аргументом savemask для 0) и siglongjmp(), и он восстанавливается после первого сигнала тревоги и никогда не получает второй. На Mac alarm() задокументирован в разделе 3. (функции), а не раздел 2 (системные вызовы). Это не должно иметь значения, но на странице руководства указано, что {{X6} } используется под прикрытием.

Когда я удалил вызовы sigsetjmp() / siglongjmp(), был доставлен второй сигнал тревоги (он сработал) - так что похоже, что нелокальные переходы имеют эффект. Я использовал sigsetjmp() с 0 в качестве последнего параметра. Когда я изменил его на 1, тогда код работал с кодом sigsetjmp() / siglongjmp(). Итак, я думаю, что это некоторая комбинация нелокальных масок goto и сигналов, которые создают проблемы.

Вот вариант вашего кода с довольно обширным инструментарием. Он использует мои предпочтительные функции отчетов об ошибках, которые доступно на GitHub в моем SOQ (стек Overflow Questions) в виде файлов stderr.c и stderr.h в src / libsoq подкаталог. Они позволяют легко сообщать о времени появления сообщений и т. Д., Что полезно.

#include <assert.h>
#include <setjmp.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <unistd.h>
#include "stderr.h"

static bool use_jmpbuf = false;
static int  save_mask  = 1;
static sigjmp_buf jump_buffer;

static void handle_timeout(int signum)
{
    assert(signum == SIGALRM);
    if (use_jmpbuf)
        siglongjmp(jump_buffer, 1);
}

static void handle_sigint(int signum)
{
    err_error("Got signal %d (SIGINT)\n", signum);
    /*NOTREACHED*/
}

static void test(void)
{
    err_remark("Entering %s()\n", __func__);
    if (use_jmpbuf)
    {
        if (sigsetjmp(jump_buffer, save_mask) == 0)
        {
            err_remark("Pausing in %s()\n", __func__);
            pause();
        }
    }
    else
    {
        err_remark("Pausing in %s()\n", __func__);
        pause();
    }
    err_remark("Leaving %s()\n", __func__);
}

static void set_sigalrm(void)
{
    void (*handler)(int) = signal(SIGALRM, handle_timeout);
    if (handler == SIG_ERR)
        err_syserr("signal failed: ");
    if (handler == SIG_IGN)
        err_remark("SIGALRM was ignored\n");
    else if (handler == SIG_DFL)
        err_remark("SIGALRM was defaulted\n");
    else
        err_remark("SIGALRM was being handled\n");
}

static const char optstr[] = "hjm";
static const char usestr[] = "[-hjm]";
static const char hlpstr[] =
    "  -h  Print this help information and exit\n"
    "  -j  Use sigsetjmp()\n"
    "  -m  Do not save signal mask when using sigsetjmp\n"
    ;

int main(int argc, char **argv)
{
    err_setarg0(argv[0]);
    int opt;
    while ((opt = getopt(argc, argv, optstr)) != -1)
    {
        switch (opt)
        {
        case 'h':
            err_help(usestr, hlpstr);
            /*NOTREACHED*/
        case 'j':
            use_jmpbuf = true;
            break;
        case 'm':
            use_jmpbuf = true;
            save_mask = 0;
            break;
        default:
            err_usage(usestr);
            /*NOTREACHED*/
        }
    }
    if (optind != argc)
        err_usage(usestr);

    signal(SIGINT, handle_sigint);
    err_setlogopts(ERR_MILLI);
    err_stderr(stdout);

    if (use_jmpbuf)
        err_remark("Config: using sigsetjmp() %s saving signal mask\n", save_mask ? "with" : "without");
    else
        err_remark("Config: no use of sigsetjmp\n");
    set_sigalrm();
    unsigned left;

    left = alarm(2);
    err_remark("Left over from previous alarm: %u\n", left);
    test();
    err_remark("In %s() once more\n", __func__);
    set_sigalrm();
    left = alarm(2);
    err_remark("Left over from previous alarm: %u\n", left);
    test();
    err_remark("Exiting %s() once more\n", __func__);
    return 0;
}

Примеры запусков (название программы alrm61):

$ alrm61 -h
Usage: alrm61 [-hjm]
  -h  Print this help information and exit
  -j  Use sigsetjmp()
  -m  Do not save signal mask when using sigsetjmp
$ alrm61
alrm61: 2018-01-02 21:34:01.893 - Config: no use of sigsetjmp
alrm61: 2018-01-02 21:34:01.894 - SIGALRM was defaulted
alrm61: 2018-01-02 21:34:01.894 - Left over from previous alarm: 0
alrm61: 2018-01-02 21:34:01.894 - Entering test()
alrm61: 2018-01-02 21:34:01.894 - Pausing in test()
alrm61: 2018-01-02 21:34:03.898 - Leaving test()
alrm61: 2018-01-02 21:34:03.898 - In main() once more
alrm61: 2018-01-02 21:34:03.898 - SIGALRM was being handled
alrm61: 2018-01-02 21:34:03.898 - Left over from previous alarm: 0
alrm61: 2018-01-02 21:34:03.898 - Entering test()
alrm61: 2018-01-02 21:34:03.898 - Pausing in test()
alrm61: 2018-01-02 21:34:05.902 - Leaving test()
alrm61: 2018-01-02 21:34:05.902 - Exiting main() once more
$ alrm61 -j
alrm61: 2018-01-02 21:34:23.103 - Config: using sigsetjmp() with saving signal mask
alrm61: 2018-01-02 21:34:23.104 - SIGALRM was defaulted
alrm61: 2018-01-02 21:34:23.104 - Left over from previous alarm: 0
alrm61: 2018-01-02 21:34:23.104 - Entering test()
alrm61: 2018-01-02 21:34:23.104 - Pausing in test()
alrm61: 2018-01-02 21:34:25.108 - Leaving test()
alrm61: 2018-01-02 21:34:25.108 - In main() once more
alrm61: 2018-01-02 21:34:25.108 - SIGALRM was being handled
alrm61: 2018-01-02 21:34:25.108 - Left over from previous alarm: 0
alrm61: 2018-01-02 21:34:25.109 - Entering test()
alrm61: 2018-01-02 21:34:25.109 - Pausing in test()
alrm61: 2018-01-02 21:34:27.112 - Leaving test()
alrm61: 2018-01-02 21:34:27.112 - Exiting main() once more
$ alrm61 -m
alrm61: 2018-01-02 21:34:37.578 - Config: using sigsetjmp() without saving signal mask
alrm61: 2018-01-02 21:34:37.578 - SIGALRM was defaulted
alrm61: 2018-01-02 21:34:37.578 - Left over from previous alarm: 0
alrm61: 2018-01-02 21:34:37.578 - Entering test()
alrm61: 2018-01-02 21:34:37.578 - Pausing in test()
alrm61: 2018-01-02 21:34:39.584 - Leaving test()
alrm61: 2018-01-02 21:34:39.584 - In main() once more
alrm61: 2018-01-02 21:34:39.584 - SIGALRM was being handled
alrm61: 2018-01-02 21:34:39.584 - Left over from previous alarm: 0
alrm61: 2018-01-02 21:34:39.584 - Entering test()
alrm61: 2018-01-02 21:34:39.584 - Pausing in test()
^Calrm61: 2018-01-02 21:35:00.638 - Got signal 2 (SIGINT)
$ 
2
Jonathan Leffler 3 Янв 2018 в 05:38

Перепишите код с sigset* и неактивным ожиданием (pause), как предлагает Джонатан Леффлер:

static sigjmp_buf jump_buffer;

void f() {
  pause();
}

void handle_timeout(int sig) {
  siglongjmp(jump_buffer, 1);
}

void test() {
  if (sigsetjmp(jump_buffer,0) == 0) // SAVE or NOT...
    {
      f();
    }
}

int main() {
  printf("1\n");
  signal (SIGALRM, handle_timeout);
  alarm(2);
  test();
  sigset_t m;
  sigprocmask(0,NULL,&m);
  printf("%d\n",m);
  printf("2\n");
  signal (SIGALRM, handle_timeout);
  alarm(2);
  test();
  return 0;
}

Затем выполнение второй части блокируется, потому что выход из обработчика с jmp не восстанавливает маску, а поскольку signal блокирует текущий доставленный сигнал, то после первого вызова test() сигнала маска содержит SIGALRM, который затем блокируется, см. выполнение:

$ ./test
1
8192 #SIGALRM
2    <-blocked

Теперь, если изменить значение 0 на 1 (строка прокомментирована как СОХРАНИТЬ или НЕ), как говорится в документации по sigsetjmp:

Пары функций sigsetjmp () / siglongjmp () сохраняют и восстанавливают маску сигнала, если аргумент savemask не равен нулю; в противном случае сохраняются только набор регистров и стек.

Маска сигнала после первого вызова test восстанавливается, см. выполнение:

$ ./test
1
0
2
$
2
Jean-Baptiste Yunès 4 Янв 2018 в 09:52