Как убедиться, что обработчик сигнала никогда не уступает потоку в одной и той же группе процессов?

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

Подвох в том, что после сбоя мое приложение запускается llvm-symbolizer который занимает некоторое время (условно говоря) и вызывает доходность (либо из-за clone + execve или превышая кванты времени для потока, я видел, как это происходило, когда я сам выполнял символизацию в процессе, используя libLLVM). Причиной всего этого является получение трассировки стека с разбитыми символами и информацией о строках / файлах (хранится в отдельном файле DWP). По понятным причинам я не хочу, чтобы доходность происходила через мой SIGSEGV обработчик, так как я намерен завершить приложение (группу потоков) после его выполнения и никогда не возвращаться из обработчика сигнала.

Я не очень хорошо знаком с обработкой сигналов в Linux и с тем, что оболочки glibc творит магию вокруг них, хотя я знаю основные хитрости, но не так много информации о специфике обработки сигналов, например о том, получают ли обработчики синхронных сигналов какой-либо особый приоритет с точки зрения планирования.

Мозговой штурм, у меня было несколько идей и минусов к ним:

  • pthread_kill(<every other thread>, SIGSTOP) - Громоздкий с большим количеством потоков, взаимодействует с обработчиками сигналов, что может привести к непреднамеренным побочным эффектам. Также требует перехвата создания потока из других библиотек для отслеживания списка потоков и увеличения вероятности упреждения при каждом системном вызове. Возможно, даже изменить их контексты, как только они остановились, чтобы указать на системный вызов exit заглушка или плоское использование SIGKILL,
  • Глобальный флаг, служащий точками отмены для всей цепочки (вроде как pthread_cancel/pthread_testcancel). Безопаснее, но требует большого обслуживания, и на большой кодовой базе это может быть адским, в дополнение к небольшому снижению производительности. Глобальный флаг также может вызвать каскадную ошибку, поскольку программа уже находится в непредсказуемом состоянии, поэтому запуск любого другого потока там уже невелик.
  • "Злоупотребление" планировщиком, который является моим текущим выбором, с моей реализацией в качестве одного из ответов. Переключение на FIFO поэтому планирование политики и повышение приоритета становятся единственными работающими потоками в этой группе.
  • Основные дампы не вариант, так как цель здесь состояла в том, чтобы избежать их в первую очередь. Я бы предпочел, чтобы не требовалась вспомогательная программа, кроме символизатора.

Окружающая среда является типичным glibc на основе Linux (4.4) NPTL,

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

Редактировать: Кажется, что я на самом деле не рассматривал MP в этом уравнении (согласно комментариям) и тот факт, что другие потоки все еще работоспособны в ситуации MP и могут счастливо продолжать работать вместе с FIFO нить на другом процессоре. Однако я могу изменить сродство процесса, чтобы он выполнялся только на том же ядре, что и аварийный поток, что в основном эффективно замораживает все другие потоки на границах расписания. Тем не менее, это по-прежнему оставляет сценарий "Поток FIFO уступающий из-за блокирования ввода-вывода" открытым.

Кажется, что FIFO + SIGSTOP Опция лучшая, хотя мне интересно, есть ли другие хитрости, которые могут сделать поток неконтролируемым за исключением использования SIGSTOP, Из документа видно, что невозможно установить сродство ЦП потока к нулю (оставив его в состоянии неопределенности, когда он технически работоспособен, за исключением того, что нет доступных процессоров для его запуска).

2 ответа

Решение

Это лучшее решение, которое я мог придумать (части для краткости опущены, но оно показывает принцип), с моим основным предположением, что в этой ситуации процесс запускается с правами root. Такой подход может привести к нехватке ресурсов в случае, если дела идут очень плохо и требуются привилегии (если я понимаю man(7) sched страница правильно) Я запускаю ту часть обработчика сигнала, которая вызывает вытеснения под OSSplHigh Охраняй и выходи из прицела, как только смогу. Это не строго связано с C++, поскольку то же самое можно сделать на C или любом другом родном языке.

void spl_get(spl_t& O)
{
    os_assert(syscall(__NR_sched_getattr,
        0, &O, sizeof(spl_t), 0) == 0);
}

void spl_set(spl_t& N)
{
    os_assert(syscall(__NR_sched_setattr,
        0, &N, 0) == 0);
}

void splx(uint32_t PRI, spl_t& O) {
    spl_t PL = {0};

    PL.size = sizeof(PL);
    PL.sched_policy = SCHED_FIFO;
    PL.sched_priority = PRI;

    spl_set(PL, O);
}

class OSSplHigh {
    os::spl_t OldPrioLevel;

public:
    OSSplHigh() {
        os::splx(2, OldPrioLevel);
    }

    ~OSSplHigh() {
        os::spl_set(OldPrioLevel);
    }
};

Сам обработчик довольно тривиально sigaltstack а также sigaction хотя я не блокирую SIGSEGV в любой теме. Также, как ни странно, системные вызовы sched_setattr и sched_getattr или определение структуры не были представлены через glibc вопреки документации.


Позднее редактирование: лучшее решение связано с отправкой SIGSTOP ко всем потокам (перехватывая pthread_create через линкер --wrap опция) вести учет всех запущенных тем, спасибо за предложение в комментариях.

после сбоя мое приложение запускает llvm-symbolizer

Это может вызвать тупики. Я не могу найти никаких утверждений о том, что llvm-symbolizer безопасен для async-signal. Это скорее всего позвонит mallocи, если это так, безусловно, будет тупик, если сбой также происходит внутри malloc (например, из-за повреждения кучи в другом месте).

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

Я считаю, что вы ошибаетесь: SCHED_FIFO поток будет работать до тех пор, пока он работает (т.е. не выполняет никаких системных блокирующих вызовов). Если поток выполняет такой вызов (который он должен: например, open отдельный .dwp файл), он заблокирует и другие потоки станут работоспособными.

TL; DR: нет простого способа достичь того, чего вы хотите, и все равно кажется ненужным: какое вам дело до того, что другие потоки продолжат работать, пока завершающий бизнес завершает свою работу?

Другие вопросы по тегам