улавливание сигналов в многопоточной среде

у меня есть большая программа, которая должна быть максимально устойчивой и имеет большое количество потоков. Мне нужно поймать все сигналы SIGBUS SIGSEGV и при необходимости повторно инициализировать проблемный поток или отключить поток для продолжения с уменьшенной функциональностью.

моя первая мысль-сделать setjump, а затем установите обработчики сигналов, которые могут регистрировать проблему, а затем сделать longjump вернуться к точке восстановления в потоке. Существует проблема, что обработчик сигнала нужно было бы определить, из какого потока пришел сигнал, использовать соответствующий буфер перехода, так как прыжок назад к неправильному потоку был бы бесполезен.

кто-нибудь знает, как определить оскорбительный поток в обработчике сигнала?

3 ответов


используя syscall(SYS_gettid) работает для меня на моем Linux box:gcc pt.c -lpthread -Wall -Wextra

//pt.c
#define _GNU_SOURCE
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <setjmp.h>
#include <signal.h>
#include <string.h>
#include <ucontext.h>
#include <stdlib.h>

static sigjmp_buf jmpbuf[65536];

static void handler(int sig, siginfo_t *siginfo, void *context)
{
    //ucontext_t *ucontext = context;
    pid_t tid = syscall(SYS_gettid);

    printf("Thread %d in handler, signal %d\n", tid, sig);
    siglongjmp(jmpbuf[tid], 1);
}

static void *threadfunc(void *data)
{
    int index, segvindex = *(int *)data;
    pid_t tid = syscall(SYS_gettid);

    for(index = 0; index < 500; index++) {
        if (sigsetjmp(jmpbuf[tid], 1) == 1) {
            printf("Recovery of thread %d\n", tid); 
            continue;
        }
        printf("Thread %d, index %d\n", tid, index);
        if (index % 5 == segvindex) {
            printf("%zu\n", strlen((char *)2)); // SIGSEGV
        }
        pthread_yield();
    }
    return NULL;
}

int main(void)
{
    pthread_t thread1, thread2, thread3;
    int segvindex1 = rand() % 5;
    int segvindex2 = rand() % 5;
    int segvindex3 = rand() % 5;
    struct sigaction sact;

    memset(&sact, 0, sizeof sact);
    sact.sa_sigaction = handler;
    sact.sa_flags = SA_SIGINFO;
    if (sigaction(SIGSEGV, &sact, NULL) < 0) {
        perror("sigaction");
        return 1;
    }
    pthread_create(&thread1, NULL, &threadfunc, (void *) &segvindex1);
    pthread_create(&thread2, NULL, &threadfunc, (void *) &segvindex2);
    pthread_create(&thread3, NULL, &threadfunc, (void *) &segvindex3);
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    pthread_join(thread3, NULL);
    return 0;
}

чтобы быть более портативным pthread_self можно использовать. Это асинхронно-сигнал-безопасный.

но нить, которая получила SIGSEGV должен запустить новый поток асинхронно-сигнальными средствами и не должен делать siglongjmp поскольку это может привести к вызову не-асинхронно-сигнальных функций.


Я собираюсь предположить, что вы уже продумали это и имеете очень веские основания полагать, что ваша программа будет больше resilient, пытаясь повторить попытку после SIGSEGV-имея в виду segfaults выделите проблемы с болтающимися указателями и другими злоупотреблениями, которые также могут повредить непредсказуемые местоположения в адресном пространстве процесса без segfaulting.

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

обработчик SIGSEGV в Linux выполняется в потоке неудачной инструкции (сигнал man 7). Мы не можем вызвать pthread_self (), поскольку это не безопасный асинхронный сигнал, но интернет широко кажется, согласен, что syscall (man 2 syscall) безопасен, поэтому мы можем получить идентификатор потока через syscall SYS_gettid. Поэтому мы будем поддерживать отображение pthread_t (pthread_self) в pid (gettid ()). Поскольку write () также безопасен, мы можем поймать SEGV, записать текущий идентификатор потока в канал, а затем приостановить, пока pthread_cancel не завершит нас.

нам также нужен поток монитора, чтобы следить за тем, когда вещи идут грушевидной формы. Поток монитора контролирует прочитанный конец трубы для информации на прерванный поток и может перезапустить его.

потому что я думаю, что притворяться, чтобы справиться с SIGSEGV, глупо, я собираюсь назвать структуры здесь, которые делают это daft_thread_t и т. д. someone_please_fix_me представляет ваш сломанный код. Поток монитора-main (). Когда поток segfaults, он захвачен обработчиком сигнала, записывает свой идентификатор вниз по трубе; монитор считывает канал, отменяет поток с помощью pthread_cancel и pthread_join и перезапускает его.

#include <assert.h>
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <sys/syscall.h>

#define MAX_DAFT_THREADS (1024) // arbitrary

#define CHECK_OSCALL(call, onfail) { \
    if ((call) == -1) { \
        char buf[512]; \
        strerror_r(errno, buf, sizeof(buf)); \
        fprintf(stderr, "%s@%d failed: %s\n", __FILE__, __LINE__, buf); \
        onfail; \
    } \
}

/*********************** daft thread accounting *****************/
typedef void* (*threadproc_t)(void* arg);

struct daft_thread_t {
    threadproc_t start_routine;
    void* start_routine_arg;
    pthread_t pthread;
    pid_t tid;
};

struct daft_thread_accounting_info_t {
    int monitor_pipe[2];
    pthread_mutex_t info_lock;
    size_t daft_thread_count;
    struct daft_thread_t daft_threads[MAX_DAFT_THREADS];
};

static struct daft_thread_accounting_info_t g_thread_accounting;

void daft_thread_accounting_info_init(struct daft_thread_accounting_info_t* inf)
{
    memset(inf, 0, sizeof(*inf));
    pthread_mutex_init(&inf->info_lock, NULL);
    CHECK_OSCALL(pipe(inf->monitor_pipe), abort());
}

struct daft_thread_wrapper_data_t {
    struct daft_thread_t* thread_info;
};

static void* daft_thread_wrapper(void* arg)
{
    struct daft_thread_t* wrapper = arg;
    wrapper->tid = gettid();
    return (*wrapper->start_routine)(wrapper->start_routine_arg);
}

static void start_daft_thread(threadproc_t proc, void* arg)
{
    struct daft_thread_t*  info;
    pthread_mutex_lock(&g_thread_accounting.info_lock);
    assert (g_thread_accounting.daft_thread_count < MAX_DAFT_THREADS);
    info = &g_thread_accounting.daft_threads[g_thread_accounting.daft_thread_count++];
    pthread_mutex_unlock(&g_thread_accounting.info_lock);
    info->start_routine = proc;
    info->start_routine_arg = arg;
    CHECK_OSCALL(pthread_create(&info->pthread, NULL, daft_thread_wrapper, info), abort());
}

static struct daft_thread_t* find_thread_by_tid(pid_t thread_id)
{
    int k;
    struct daft_thread_t* info = NULL;
    pthread_mutex_lock(&g_thread_accounting.info_lock);
    for (k = 0; k < g_thread_accounting.daft_thread_count; ++k) {
        if (g_thread_accounting.daft_threads[k].tid == thread_id) {
            info = &g_thread_accounting.daft_threads[k];
            break;
        }
    }
    pthread_mutex_unlock(&g_thread_accounting.info_lock);
    return info;
}

static void restart_daft_thread(struct daft_thread_t* info)
{
    void* unused;
    CHECK_OSCALL(pthread_cancel(info->pthread), abort());
    CHECK_OSCALL(pthread_join(info->pthread, &unused), abort());
    info->tid = 0;
    CHECK_OSCALL(pthread_create(&info->pthread, NULL, daft_thread_wrapper, info), abort());
}

/************* signal handling stuff **************/
struct sigdeath_notify_info {
    int signum;
    pid_t tid;
};

static void sigdeath_handler(int signum, siginfo_t* info, void* ctx)
{
    int z;
    struct sigdeath_notify_info inf = {
        .signum = signum,
        .tid = gettid()
    };
    z = write(g_thread_accounting.monitor_pipe[1], &inf, sizeof(inf));
    assert (z == sizeof(inf)); // or else SIGABRT. Are we handling that too? Hope     not.
    pause(); // returning doesn't do us any good.
}

static void register_signal_handlers()
{
    struct sigaction sa = {};
    sa.sa_sigaction = sigdeath_handler;
    sa.sa_flags = SA_SIGINFO;
    CHECK_OSCALL(sigaction(SIGSEGV, &sa, NULL), abort());
    CHECK_OSCALL(sigaction(SIGBUS, &sa, NULL), abort());
}

pid_t gettid() { return (pid_t) syscall(SYS_gettid); }

/** This is the code that segfaults randomly. Kwality with a 'k'. */
static void* someone_please_fix_me(void* arg)
{
    char* i_think_this_address_looks_nice = (char*) 42;
    sleep(1 + rand() % 200);
    i_think_this_address_looks_nice[0] = 'q'; // ugh
    return NULL;
}

// main() will serve as the monitor thread here
int main()
{
    int k;
    struct sigdeath_notify_info death;
    daft_thread_accounting_info_init(&g_thread_accounting);
    register_signal_handlers();
    for (k = 0; k < 200; ++k) {
        start_daft_thread(someone_please_fix_me, (void*) k);
    }
    while (read(g_thread_accounting.monitor_pipe[0], &death, sizeof(death)) == sizeof(death)) {
        struct daft_thread_t* info = find_thread_by_tid(death.tid);
        if (info == NULL) {
            fprintf(stderr, "*** thread_id %u not found\n", death.tid);
            continue;
        }
        fprintf(stderr, "Thread %u (%d) died of %d, restarting.\n",
            death.tid, (int) info->start_routine_arg, death.signum);
        restart_daft_thread(info);
    }
    fprintf(stderr, "Shouldn't get here.\n");
    return 0;
}

Если вы не подумайте об этом: попытка оправиться от SIGSEGV чрезвычайно рискованна - я настоятельно рекомендую этого не делать. Потоки совместно используют адресное пространство. Поток, который segfaulted может также повредить другие данные потока или глобальные учетные данные, такие как учет malloc (). Гораздо более безопасный подход-предполагая, что неисправный код непоправимо нарушен, но должен использоваться - заключается в карантине неисправного кода за границей процесса, например, fork()ING перед вызовом сломанного кода. Тогда вы должны ловушка SIGCLD и справиться с процессом сбоя или завершения нормально, наряду с рядом других ловушек, но, по крайней мере, вам не придется беспокоиться о случайной коррупции. Конечно, лучший вариант-исправить кровавый код, чтобы вы не наблюдали за segfaults.


по моему опыту, когда потоковая программа получает синхронный сигнал-то есть тот, который генерируется чем - то, что программа сделала, например, разыменовав плохой указатель-поток, который вызвал проблему, получает сигнал.

Я использовал одну систему, которая явно гарантировала это поведение, но я не знаю, является ли она общей. Также, конечно, если оскорбительный поток заблокировал сигнал, как в парадигме, где один поток обрабатывает все сигналы, предположительно он перейдет к поток обработки сигналов.