Главная » Ядро Linux » Завершение процесса

0

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

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

цесс  не может  обработать или  проигнорировать. Независимо от того, каким образом процесс завершается, основную массу работы  выполняет функция doexe c (), а именно указанные далее операции.

• Устанавливается флаг  PF_EXITING в поле  flag s структуры  tas k  struct .

• Вызывается функция  del_timer_syn c () , чтобы  удалить  все  таймеры ядра.

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

• Если  включена возможность учета системных ресурсов, занятых  процессами (BSD  process  accounting), то вызывается функция acct_proces s ()  для записи информации об учете ресурсов, которые использовались процессом.

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

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

• Вызываются функции __exit_file s (), __exit_f s () , exit_namespace ()   и exit_signal s ()  для  уменьшения счетчика ссылок на  объекты, которые  отвечают  файловым дескрипторам, данным по файловой системе, пространству имен  и обработчикам сигналов соответственно. Если  счетчик ссылок какоголибо  объекта  достигает  значения,  равного нулю, то  соответствующий объект больше  не используется никаким процессом и удаляется.

• Устанавливается код завершения задания, который хранится в поле exitcod e структуры  tas k  struct .  Значение этого  кода  передается как  аргумент  функции  exi t () или  задается  тем механизмом ядра, из-за  которого  процесс завершается.

• Вызывается функция exitnoti f у (), которая отправляет сигналы  родительскому процессу завершающегося задания и назначает новый родительский процесс  (reparent) для  всех порожденных завершающимся  заданием процессов, этим  процессом становится или  какой-либо один  поток из группы  потоков завершающегося процесса, или  процесс init . Состояние завершающегося процесса устанавливается в значение TASK_ZOMBIE.

• Вызывается функция schedule  ()  для переключения на новый  процесс (см. главу 4, "Планирование выполнения процессов"). Поскольку процесс в состоянии TASK_ZOMBIE никогда не планируется на выполнение, этот код является последним, который выполняется завершающимся процессом.

Исходный код функции do_exit  ()  описан в файле  kernel/exit.с .

К этому  моменту  освобождены все объекты, занятые задачей  (если  они используются  только  этой  задачей). Задача  больше  не может  выполняться (действительно, у нее больше  нет адресного пространства, в котором она может  выполняться), а кроме того, состояние задачи — TASK_ZOMBIE. Единственные области  памяти, которые теперь занимает процесс, — это стек режима  ядра и слябовый объект, соответственно  содержащие структуры  thread_in f о и task_struct .

Задание завершено настолько, насколько остается  возможность передать  необходимую  информацию родительскому процессу.

Удаление дескриптора процесса

После  возврата  из функции do_exi t ()  дескриптор завершенного процесса все еще существует  в системе, но процесс находится в состоянии TASK_ZOMBIE и не может выполняться. Как  уже рассказывалось выше, это  позволяет системе  получить информацию о порожденном процессе после его завершения. Следовательно, завершение  процесса и удаление  его дескриптора происходят в разные  моменты времени. После  того как родительский процесс получил  информацию о завершенном порожденном процессе, структура  task_struc t  порожденного процесса освобождается.

Семейство функций wai t ()  реализовано через  единственный (и  достаточно сложный) системный вызов  wait4 (). Стандартное поведение этой  функции — приостановить выполнение вызывающей задачи  до тех пор, пока  один  из ее порожденных процессов не завершится. При  этом возвращается идентификатор PID  завершенного порожденного процесса. В дополнение к этому, в данную  функцию передается указатель  на область  памяти, которая после  возврата  из  функции будет содержать код завершения завершившегося порожденного процесса.

Когда  приходит время  окончательно освободить дескриптор процесса, вызывается функция release_tas k (), которая выполняет указанные ниже  операции.

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

• Вызывается функция  unhash_process ()  для удаления процесса из хеш-таблицы идентификаторов процессов pidhash  и удаления задачи  из списка задач.

• Если  задача  была в состоянии трассировки (ptrace), то родительским для нее снова  назначается первоначальный родительский процесс и задача удаляется  из списка задач, которые находятся в состоянии трассировки  (pirate)  данным процессом.

• В конце  концов вызывается функция put_task_struc t ()  для освобождения страниц памяти, содержащих стек  ядра  процесса и структуру thread_infо , a также  освобождается слябовый кэш,  содержащий структуру  task_struct .

На данном этапе  дескриптор процесса, а также  все ресурсы, которые принадлежали только  этому процессу, освобождены.

Дилемма "беспризорного" процесса

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

какой-либо один  поток  из  группы   потоков завершившегося  родительского процес са, или  процесс init . При  выполнении функции do_exi t ()   вызывается функция notify_paren t () ,  которая в  свою  очередь   вызывает  forget_origina l   paren t () для осуществления переназначения родительского процесса (reparent), как  показано ниже.

struct task_struct *р, *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;

Этот  программный код  присваивает переменной  reape r указатель на другое  зада ние  в группе  потоков данного процесса. Если  в этой  группе  потоков нет  другого  задания, то  переменной reape r  присваивается  значение  переменной  child_reaper , которая содержит указатель на  процесс init . Теперь, когда  найден подходящий родительский процесс, нужно  найти  все  порожденные процессы и установить для  них полученное значение родительского процесса,  как  показано ниже.

list_for_each(list,&father->children){

р = list_entry(list, struct task_struct, sibling);

reparent_thread(p, reaper, child_reaper);

}

list_for_each (list, sfather->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).  Когда  завершается истинный  родительский  процесс для  такого задания, то для  такой  дочерней задачи  также  нужно   осуществить переназначение родительского процесса. В ядрах  более  ранних версий это  приводило к необходимости  организации цикла  по всем заданиям системы для поиска порожденных процессов. Решение проблемы, как  было  указано выше, — это  поддержка отдельного списка для порожденных процессов, которые находятся в состоянии трассировки, что уменышает  число  операций поиска:  происходит переход  от  поиска порожденных процессов по  всему  списку задач  к  поиску только   по  двум  спискам с достаточно малым   числом элементов.

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

Процесс Ini t  периодически вызывает  функцию wai t ()  для  всех своих  порожденных процессов и, соответственно, удаляет все зомби-процессы, назначенные ему.

Резюме

В этой  главе рассмотрена важная  абстракция операционной системы — процесс. Здесь  описаны общие  свойства  процессов, их назначение, а также  представлено сравнение процессов и потоков. Кроме  того,  описывается, как  операционная система  Linux  хранит  и  представляет информацию,  которая   относится  к  процессам  (структуры   task_struc t  и  thread_infо) ,  как  создаются   процессы  (вызовы clone ()  и fork ()),  каким  образом  новые  исполняемые образы  загружаются в адресное пространство (семейство вызовов  exec ()),  иерархия процессов, каким  образом родительский процесс  собирает  информацию о своих потомках  (семейство функций wait ()) и как  в конце  концов процесс  завершается (непроизвольно или с помощью вызова  exi t ()).

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

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

Источник: Лав,  Роберт. Разработка ядра  Linux, 2-е  издание. : Пер.  с англ.  — М.  : ООО  «И.Д.  Вильяме» 2006. — 448 с. : ил. — Парал. тит. англ.

По теме:

  • Комментарии