Главная » Ядро Linux » Создание нового процесса

0

В операционной системе Unix  создание процессов происходит уникальным образом.   В  большинстве операционных систем  для  создания процессов используется метод  порождения  процессов  (spawn).  При  этом   создается новый  процесс в  новом адресном пространстве,  в которое считывается исполняемый файл,  и  после  этого начинается исполнение процесса. В ОС  Unix  используется другой  подход, а именно разбиение указанных выше  операций на две  функции:  for k ()  и exe c () 8 .

8Под  exec( ) будем понимать любую функцию из семейства exec* () . В ядре реализован системный вызов   execve () ,  н а основе   которого реализованы  библиотечны е  функции   e xeclp() ,  execle(), execv( )      и    execvp().

В начале  с  помощью функции  fork( )  создается порожденный процесс,  который является копией  текущего задания. Порожденный процесс отличается от родительского  только  значением идентификатора PID   (который является уникальным в системе), значением параметра PPID  (идентификатор PID  родительского процесса, который устанавливается в значение PID  порождающего процесса), некоторыми ресурсами, такими как  ожидающие на обработку сигналы (которые не  наследуются), а также  статистикой использования ресурсов Вторая  функция — exe c ()  — загружает исполняемый файл  в  адресное пространство процесса и  начинает исполнять его. Комбинация функций for k ()   и  exe c ()   аналогична той  одной  функции создания процесса,  которую предоставляет большинство операционных систем.

Копирование при записи

Традиционно при  выполнении функции  fork( )  делался дубликат всех  ресурсов родительского  процесса  и  передавался порожденному.  Такой  подход  достаточно наивный и  неэффективный. В операционной системе Linux  вызов  for k ()  реализован  с использованием механизма копирования  при записи   (copy-on-write)  страниц памяти. Технология копирования  при  записи (copy-on-write,  COW)  позволяет отложить или вообще предотвратить копирование  данных. Вместо  создания  дубликата адресного пространства процесса родительский и порожденный процессы могут  совместно использовать одну  и  ту же  копию адресного пространства.  Однако при  этом  данные помечаются особым образом,  и  если  вдруг  один  из  процессов начинает изменять данные, то  создается дубликат данных,  и  каждый процесс получает  уникальную копию  данных.  Следовательно,  дубликаты ресурсов создаются только  тогда,  когда  в эти  ресурсы осуществляется запись,  а до  того  момента они  используются совместно в  режиме только  для  чтения  (read-only).  Такая  техника позволяет задержать копирование каждой страницы памяти до того  момента, пока  в  эту  страницу памяти не будет осуществляться запись. В случае, если  в страницы памяти никогда не делается запись, как,  например, при  вызове  функции exe c ()   сразу  после  вызова   for k () , то эти  страницы никогда и не  копируются. Единственные накладные расходы, которые вносит вызов  функции for k () , — это  копирование таблиц  страниц родительского процесса и создание дескриптора порожденного процесса. Данная оптимизация предотвращает ненужное копирование  большого количества данных (размер адресного пространства часто  может  быть  более  10 Мбайт), так  как  процесс после  разветвления  в большинстве случаев  сразу же начинает выполнять новый исполняемый образ. Эта  оптимизация очень  важна, потому  чти  идеология операционной  системы Unix предусматривает быстрое выполнение  процессов.

Функция for k ()

В  операционной  системе Linux  функция  for k ()   реализована через  системный вызов   clon e ()  .  Этот  системный  вызов   может  принимать в  качестве аргументов набор   флагов,  определяющих,  какие  ресурсы должны быть  общими (если  вообще должны) у родительского и  порожденного процессов. Далее  в разделе  "Реализация потоков в ядре  Linux"  об  этих  флагах  рассказано более  подробно.  Библиотечные вызовы    fork(),vfork( )   и       clone d     вызывают системную функцию  clon e ()   с соответствующими флагами.  В  свою  очередь   системный вызов   clon e ()   вызывает функцию ядра  do_for k () .

Основную массу работы  по разветвлению процесса выполняет функция do_f ork (), которая определена в файле  kernel/fork.с . Эта функция, в свою  очередь, вызывает функцию copy_pracess () и запускает новый  процесс на выполнение. Ниже  описана  та интересная работа, которую  выполняет функция copy_process ().

• Вызывается функция dup_task_struc t () , которая создает  стек  ядра,  структуры  thread_inf o и task_struc t для нового  процесса, причем все  значения указанных структур  данных  идентичны для  порождающего и  порожденного процессов. На  этом  этапе  дескрипторы родительского и порожденного процессов  идентичны.

• Проверяется, не произойдет ли при создании нового  процесса переполнение лимита  на количество процессов для данного пользователя.

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

• Далее  состояние порожденного процесса устанавливается в значение  TASK_ UNINTERRUPTIBLE, чтобы  гарантировать, что порожденный процесс не будет выполняться.

• Из функции copy_process ()  вызывается функция copy_f lag s (), которая обновляет значение поля  flag s структуры  tas k   struct . При  этом сбрасывается флаг  PF_SUPERPRIV,  который определяет, имеет  ли процесс права суперпользователя. Флаг PF_FORKNOEXEC,  который указывает на то, что процесс не вызвал функцию exec (), — устанавливается.

• Вызывается функция get_pi d () , которая назначает новое  значение  идентификатора PID  для новой  задачи.

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

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

• Наконец, происходит окончательная зачистка структур данных  и возвращается указатель  на новый  порожденный процесс.

Далее  происходит возврат  в функцию do_for k () .  Если  возврат  из  функции copy_process ()  происходит успешно, то новый  порожденный процесс возобновляет выполнение. Порожденный процесс намеренно запускается на выполнение раньше родительского9.

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

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

Функция vfor k ()

Системный вызов  vfork ()  позволяет получить  тот же эффект, что и системный вызов  fork (), за исключением того, что записи таблиц  страниц родительского процесса  не копируются. Вместо  этого  порожденный процесс запускается как  отдельный  поток  в адресном пространстве родительского процесса и родительский процесс блокируется до того момента, пока  порожденный процесс не вызовет  функцию exec ()  или  не завершится. Порожденному процессу запрещена запись  в адресное пространство. Такая  оптимизация была  желанной в старые  времена 3BSD, когда реализация системного вызова  for k ()  не  базировалась на технике  копирования страниц памяти при  записи. Сегодня, при  использовании техники копирования страниц памяти при  записи и запуске  порожденного процесса перед  родительским, единственное преимущество вызова  vfork ()  — это отсутствие  копирования таблиц страниц родительского процесса. Если  когда-нибудь в операционной системе  Linux будет реализовано копирование полей  таблиц  страниц при записи10, то вообще  не останется никаких преимуществ. Поскольку семантика функции vfork ()  достаточно ненадежна (что, например, будет, если  вызов  exec ()  завершится неудачно?), то было  бы здорово, если  бы системный вызов  vfork () умер  медленной и мучительной  смертью.  Вполне  можно  реализовать системный вызов  vfork () через обычный вызов  fork (), что действительно имело  место  в ядрах  Linux до версии  2.2.

Сейчас  системный вызов  vfork () реализован через специальный флаг  в системном  вызове  clone  (), как показано ниже.

• При  выполнении функции copy_proces s ()  поле  vfork_don e  структуры task_struc t устанавливается в значение NULL.

• При  выполнении функции do_fvork () , если  соответствующий флаг установ лен, поле  vfork_done устанавливается в ненулевое значение (начинает указывать на определенный адрес).

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

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

• При  возврате  в функцию do_fork() родительский процесс возобновляет вы полнение и выходит  из этой функции.

10 В действительности уже сейчас  есть заплаты  для добавления такой  функции в ОС Linux. Хотя,  скорее всего,  возможность совместного использования таблиц  страниц в ядрах серии  2.6 реализована не будет,  такая  возможность может  появиться в будущих  версиях.

Если  все прошло так, как  запланировано, то теперь  порожденный процесс выполняется в новом  адресном пространстве, а родительский процесс — в первоначальном адресном пространстве. Накладные  расходы  меньше,  но  реализация  не  очень  привлекательна.

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

По теме:

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