Семейство функций wait()
реализовано через единственный (и достаточно сложный) системный вызов wait4()
. Стандартное поведение этой функции — приостановить выполнение вызывающей задачи до тех пор, пока один из ее порожденных процессов не завершится. При этом возвращается идентификатор PID
завершенного порожденного процесса. В дополнение к этому, в данную функцию передается указатель на область памяти, которая после возврата из функции будет содержать код завершения завершившегося порожденного процесса.
Когда приходит время окончательно освободить дескриптор процесса, вызывается функция release_task()
, которая выполняет указанные ниже операции.
• Вызывается функция free_uid()
для декремента счетчика ссылок на информацию о пользователе процесса. В системе Linux поддерживается кэш с информацией о каждом пользователе, в частности сколько процессов и открытых файлов имеет пользователь. Если счетчик ссылок достигает значения нуль, то пользователь больше не имеет запущенных процессов и открытых файлов, в результате кэш уничтожается.
• Вызывается функция unhash_process()
для удаления процесса из хеш-таблицы идентификаторов процессов pidhash
и удаления задачи из списка задач.
• Если задача была в состоянии трассировки ( ptrace ), то родительским для нее снова назначается первоначальный родительский процесс и задача удаляется из списка задач, которые находятся в состоянии трассировки ( ptrace ) данным процессом.
• В конце концов вызывается функция put_task_struct()
для освобождения страниц памяти, содержащих стек ядра процесса и структуру thread_info
, a также освобождается слябовый кэш, содержащий структуру task_struct
.
На данном этапе дескриптор процесса, а также все ресурсы, которые принадлежали только этому процессу, освобождены.
Дилемма "беспризорного" процесса
Если родительский процесс завершается до того, как завершаются вес его потомки, то должен существовать какой-нибудь механизм назначения нового родительского процесса для порожденных, иначе процессы, у которых нет родительского, навсегда останутся в состоянии зомби, что будет зря расходовать системную память. Решение этой проблемы было указано выше: новым родительским процессом становится или какой-либо один поток из группы потоков завершившегося родительского процесса, или процесс init
. При выполнении функции do_exit()
вызывается функция notify_parent()
, которая в свою очередь вызывает forget_original_parent()
для осуществления переназначения родительского процесса (reparent), как показано ниже.
struct task_struct *p, *reaper = father;
struct list_head *list;
if (father->exit_signal != -1)
reaper = prev_thread(reaper);
else
reaper = child_reaper;
if (reaper == father)
reaper = child_reaper;
Этот программный код присваивает переменной reaper указатель на другое задание в группе потоков данного процесса. Если в этой группе потоков нет другого задания, то переменной reaper
присваивается значение переменной child_reaper,
которая содержит указатель на процесс init
. Теперь, когда найден подходящий родительский процесс, нужно найти все порожденные процессы и установить для них полученное значение родительского процесса, как показано ниже.
list_for_each(list, &father->children) {
p = list_entry(list, struct task_struct, sibling);
reparent_thread(p, reaper, child_reaper);
}
list_for_each(list, &father->ptrace_children) {
p = list_entry(list, struct task_struct, ptrace_list);
reparent_thread(p, reaper, child_reaper);
}
В этом программном коде организован цикл по двум спискам: по списку порожденных процессов child list и по списку порожденных процессов, находящихся в состоянии трассировки другими процессами ptraced child list . Основная причина, по которой используется именно два списка, достаточно интересна (эта новая особенность появилась в ядрах серии 2.6). Когда задача находится в состоянии ptrace , для нее временно назначается родительским тот процесс, который осуществляет отладку (debugging). Когда завершается истинный родительский процесс для такого задания, то для такой дочерней задачи также нужно осуществить переназначение родительского процесса. В ядрах более ранних версий это приводило к необходимости организации цикла по всем заданиям системы для поиска порожденных процессов. Решение проблемы, как было указано выше, — это поддержка отдельного списка для порожденных процессов, которые находятся в состоянии трассировки, что уменьшает число операций поиска: происходит переход от поиска порожденных процессов по всему списку задач к поиску только по двум спискам с достаточно малым числом элементов.
Читать дальше