Подтвердить что ты не робот

Вилка и каркас с резьбой

Подобные точки, относящиеся к этому вопросу, были подняты до здесь и здесь, m знает библиотеку coredump Google (которую я оценил и нашел недостаточно, хотя я мог бы попробовать и поработать над этим, если лучше пойму проблему).

Я хочу получить базовый дамп запущенного процесса Linux без прерывания процесса. Естественный подход состоит в том, чтобы сказать:

if (!fork()) { abort(); }

Поскольку разветвленный процесс получает фиксированную копию моментального снимка исходной памяти процесса, я должен получить полный дамп ядра, и поскольку копия использует copy-on-write, она обычно должна быть дешевой. Однако критический недостаток этого подхода состоит в том, что fork() только разветвляет текущий поток, а все остальные потоки исходного процесса не будут существовать в разветвленной копии.

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

  • Является ли память, содержащая все стеки потоков, доступными и доступными в разветвленном процессе?

  • Можно ли (быстро) перечислить все текущие потоки в исходном процессе и сохранить адреса оснований их стеков? Насколько я понимаю, база стека потоков в Linux содержит указатель на данные учета потока ядра, поэтому...

  • с сохраненными базовыми адресами потоков, можете ли вы прочитать соответствующие данные для каждого из исходных потоков в разветвленном процессе?

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

4b9b3361

Ответ 1

Вы знакомы с процессом checkpoint-restart? В частности, CRIU? Мне кажется, это может обеспечить вам легкий вариант.

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

Забудьте о том, чтобы не прерывать процесс. Если вы думаете об этом, дамп ядра должен прервать процесс на протяжении всего дампа; поэтому ваша истинная цель должна заключаться в том, чтобы свести к минимуму продолжительность этого прерывания. Ваша первоначальная идея использования fork() прервала процесс, он просто делает это в течение очень короткого времени.

  • Является ли память, содержащая все стеки потоков, доступными и доступными в разветвленном процессе?

Нет. fork() сохраняет только поток, который выполняет фактический вызов, и стеки для остальных потоков теряются.

Вот процедура, которую я буду использовать, если CRIU не подходит:

  • Создайте родительский процесс, который создает основной дамп дочернего процесса всякий раз, когда ребенок остановлен. (Обратите внимание, что может быть сгенерировано более одного последовательного события остановки, только первое, пока не будет выполнено действие следующего события продолжения.)

    Вы можете обнаружить события остановки/продолжения, используя waitpid(child,,WUNTRACED|WCONTINUED).

  • Дополнительно: используйте sched_setaffinity(), чтобы ограничить процесс одним процессором, и sched_setscheduler() (и, возможно, sched_setparam()), чтобы отменить приоритет процесса IDLE.

    Вы можете сделать это из родительского процесса, для которого требуется только функция CAP_SYS_NICE (которую вы можете передать с помощью setcap 'cap_sys_nice=pe' parent-binary в родительский двоичный файл, если у вас есть возможности файловой системы, как и большинство современных дистрибутивов Linux), в как эффективные, так и разрешенные наборы.

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

    Лично я не беспокоюсь. На моей четырехъядерной машине только один SIGSTOP дает аналогичные задержки между потоками в качестве мьютекса или семафора, поэтому я не вижу необходимости стремиться к еще лучшей синхронизации.

  • Когда поток в дочернем процессе решает, что он хочет сделать снимок самого себя, он отправляет SIGSTOP самому себе (через kill(getpid(), SIGSTOP)). Это останавливает все потоки в процессе.

    Родительский процесс получит уведомление о том, что ребенок был остановлен. Сначала будет рассмотрен /proc/PID/task/, чтобы получить TID для каждого потока дочернего процесса (и, возможно, /proc/PID/task/TID/ псевдофайлов для другой информации), затем присоединяется к каждому TID, используя ptrace(PTRACE_ATTACH, TID). Очевидно, что ptrace(PTRACE_GETREGS, TID, ...) будет получать состояния регистров для каждого потока, которые могут использоваться совместно с /proc/PID/task/TID/smaps и /proc/PID/task/TID/mem для получения трассировки стека в потоке, и любую другую интересующую вас информацию. Например, вы можете создать файл с поддержкой отладчика для каждого потока.)

    Когда родительский процесс выполняется захватом дампа, он позволяет продолжить процесс child. Я считаю, вам нужно отправить отдельный сигнал SIGCONT, чтобы весь дочерний процесс продолжался, а не просто полагался на ptrace(PTRACE_CONT, TID), но я не проверял это; подтвердите это, пожалуйста.

Я верю, что приведенное выше приведет к минимальной задержке времени настенных часов между потоками в процессе остановки. Быстрые тесты на AMD Athlon II X4 640 на Xubuntu и ядре 3.8.0-29-generic показывают, что жесткие циклы, увеличивающие изменчивую переменную в других потоках, только продвигают счетчики на несколько тысяч, в зависимости от количества потоков (там слишком много шум в нескольких тестах, которые я сделал, чтобы сказать что-то более конкретное).

Ограничение процесса одним процессором и даже приоритетом IDLE значительно сократит эту задержку. CAP_SYS_NICE позволяет родителям не только уменьшать приоритет дочернего процесса, но и поднимать приоритет до исходных уровней; Возможности файловой системы означают, что родительский процесс даже не должен быть установлен, поскольку достаточно CAP_SYS_NICE. (Я думаю, что это было бы достаточно безопасно - с некоторыми хорошими проверками в родительской программе - для установки, например, в университетских компьютерах, где студенты довольно активно ищут интересные способы использования установленных программ.)

Можно создать патч (или модуль) ядра, который обеспечивает повышенный kill(getpid(), SIGSTOP), который также пытается оттолкнуть другие потоки от запущенных процессоров и, таким образом, попытаться сделать задержку между остановками потоков еще меньше. Лично я бы не стал беспокоиться. Даже без обработки CPU/приоритета я получаю достаточную синхронизацию (достаточно небольшие задержки между моментами прекращения потоков).

Вам нужен пример кода для иллюстрации моих идей выше?

Ответ 2

Если вы намерены получить основной файл в неспецифическом месте и просто получите основной образ процесса, работающего без убийства, вы можете использовать gcore.

Если вы намерены получить основной файл в определенном месте (состоянии) и все еще продолжаете выполнение процесса - грубый подход заключается в выполнении gcore программно из этого местоположения.

Более классический, чистый подход состоял бы в том, чтобы проверить API, который использует gcore и встроил его в ваше приложение, но это будет слишком много усилий по сравнению с потребностью большую часть времени.

НТН!

Ответ 3

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

Вы можете легко проверить это. Создайте многопоточную программу и запустите:

pid_t parent_pid = getpid();

if (!fork()) {
    kill(parent_pid, SIGSTOP);

    char buffer[0x1000];

    pid_t child_pid = getpid();
    sprintf(buffer, "diff /proc/%d/maps /proc/%d/maps", parent_pid, child_pid);

    system(buffer);

    kill(parent_pid, SIGTERM);

    return 0;
} else for (;;);

Итак, вся ваша память есть, и когда вы создаете дамп ядра, он будет содержать все остальные стеки потоков (при условии, что ваш максимальный размер файла ядра позволяет это). Единственными частями, которые будут отсутствовать, являются их регистры. Если вам это нужно, вам придется ptrace ваш родитель, чтобы получить их.

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

Чтобы ответить на другие ваши вопросы:

Вы можете перечислить потоки, пройдя через /proc/[pid]/tasks, но вы не можете идентифицировать их базы стека, пока не будете ptrace их.

Да, у вас есть полный доступ к другим потокам, который хранит моментальные снимки (см. выше) из разветвленного процесса. Определить их нетривиально, но они попадают в дамп ядра, если это позволяет размер основного файла. Лучше всего сохранить их в какой-либо общедоступной структуре, если сможете при создании.

Ответ 4

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

Я не знаю ни одной системы, которая предоставляет такой полный дамп ядра процесса. Грубыми очертаниями процесса были бы следующие:

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

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

Это довольно тяжелый вариант, так как его основная функциональность требует приостановки всех процессоров на значительное количество времени (синхронизация, клонирование, прохождение всех потоков). Однако это должно позволить вам точно фиксировать статус всех потоков, а также определять, какие потоки выполнялись (и на каких ЦП), когда контрольная точка была достигнута. Я бы предположил, что некоторые рамки для этого процесса существуют (например, в CRIU). Конечно, возобновление процесса приведет к шторму выделения страниц, так как копия на механизме записи защищает состояние контрольной точки.