Что такое форк linux

Что такое форк linux

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

pid_t является примитивным типом данных, который определяет идентификатор процесса или группы процессов. При вызове fork() порождается новый процесс (процесс-потомок), который почти идентичен порождающему процессу-родителю. Процесс-потомок наследует следующие признаки родителя:

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

Потомок не наследует от родителя следующих признаков:

  • идентификатора процесса (PID, PPID);
  • израсходованного времени ЦП (оно обнуляется);
  • сигналов процесса-родителя, требующих ответа;
  • блокированных файлов (record locking).

При вызове fork() возникают два полностью идентичных процесса. Весь код после fork() выполняется дважды, как в процессе-потомке, так и в процессе-родителе.

Процесс-потомок и процесс-родитель получают разные коды возврата после вызова fork() . Процесс-родитель получает идентификатор (PID) потомка. Если это значение будет отрицательным, следовательно при порождении процесса произошла ошибка. Процесс-потомок получает в качестве кода возврата значение 0, если вызов fork() оказался успешным.

Таким образом, можно проверить, был ли создан новый процесс:

Пример порождения процесса через fork() приведен ниже:

Когда потомок вызывает exit() , код возврата передается родителю, который ожидает его, вызывая wait() . WEXITSTATUS() представляет собой макрос, который получает фактический код возврата потомка из вызова wait() .

Функция wait() ждет завершения первого из всех возможных потомков родительского процесса. Иногда необходимо точно определить, какой из потомков должен завершиться. Для этого используется вызов waitpid() с соответствующим PID потомка в качестве аргумента. Еще один момент, на который следует обратить внимание при анализе примера, это то, что и родитель, и потомок используют переменную rv . Это не означает, что переменная разделена между процессами. Каждый процесс содержит собственные копии всех переменных.

Рассмотрим следующий пример:

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

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

Источник

Что такое форк linux

Дочерний и родительский процессы находятся в отдельных пространствах памяти. Сразу после fork() эти пространства имеют одинаковое содержимое. Запись в память, отображение файлов (mmap(2)) и снятие отображения (munmap(2)), выполненных в одном процессе, ничего не изменяет в другом.

Дочерний процесс является точной копией родительского процесса за исключением следующих моментов:

* Потомок имеет свой уникальный идентификатор процесса, и этот PID (идентификатор процесса) не совпадает ни с одним существующим идентификатором группы процессов (setpgid(2)). * Идентификатор родительского процесса у потомка равен идентификатору родительского процесса. * Потомок не наследует блокировки памяти родителя (mlock(2), mlockall(2)). * Счётчики использования ресурсов (getrusage(2)) и времени ЦП у потомка сброшены в 0. * Набор ожидающих сигналов потомка изначально пуст (sigpending(2)). * Потомок не наследует значения семафоров родителя (semop(2)). * Потомок не наследует связанные с процессом блокировки родителя (fcntl(2)) (с другой стороны, он наследует блокировки файловых описаний fcntl(2) и блокировки flock(2)). * Потомок не наследует таймеры родителя (setitimer(2), alarm(2), timer_create(2)). * Потомок не наследует ожидающие выполнения операции асинхронного ввода-вывода (aio_read(3), aio_write(3)) и контексты асинхронного ввода-вывода родителя (см. io_setup(2)).

Все перечисленные атрибуты указаны в POSIX.1. Родитель и потомок также отличаются по следующим атрибутам процесса, которые есть только в Linux:

* Потомок не наследует уведомления об изменении каталога (dnotify) родителя (смотрите описание F_NOTIFY в fcntl(2)). * Настройка PR_SET_PDEATHSIG у prctl(2) сбрасывается, и поэтому потомок не принимает сигнал о завершении работы родителя. * Резервное значение по умолчанию устанавливается равным родительскому текущему резервному значению таймера. Смотрите описание PR_SET_TIMERSLACK в prctl(2). * Отображение памяти, помеченное с помощью флага MADV_DONTFORK через madvise(2), при fork() не наследуется. * Сигнал завершения работы потомка всегда SIGCHLD (см. clone(2)). * Биты прав доступа к порту, установленные с помощью ioperm(2), не наследуются потомком; потомок должен установить все нужные ему биты с помощью ioperm(2).

Также стоит учитывать следующее:

* Процесс потомка создаётся с одиночной нитью — той, которая вызвала fork(). Всё виртуальное адресное пространство родителя копируется в потомок, включая состояние мьютексов, условных переменных и других объектов pthreads; в случае проблем с этим может помочь pthread_atfork(3). * В многонитивой программе после fork(2) потомок может безопасно вызывать только безопасные-асинхронные-сигнальные функции (смотрите signal(7)) до тех пор, пока не вызовет execve(2). * Потомок наследует копии набора открытых файловых дескрипторов родителя. Каждый файловый дескриптор в потомке ссылается на то же описание файла что и родитель (смотрите open(2)). Это означает, что два файловых дескриптора совместно используют флаги состояния открытого файла, текущее смещение файла и атрибуты ввода-вывода, управляемые сигналами (смотрите описание F_SETOWN и F_SETSIG в fcntl(2)). * Потомок наследует копии набора файловых дескрипторов открытых очередей сообщений родителя (смотрите mq_overview(7)). Каждый файловый дескриптор в потомке ссылается на то же описание открытой очереди сообщений что и родитель. Это означает, что два файловых дескриптора совместно используют флаги (mq_flags). * Потомок наследует копии набора потоков открытых каталогов родителя (смотрите opendir(3)). В POSIX.1 сказано, что соответствующие потоки каталогов в родителе и потомке могут совместно использовать позицию в потоке каталога; в Linux/glibc они не могут этого делать.

ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ

ОШИБКИ

Возникло системного ограничение на количество нитей. Есть несколько ограничений, которые могут вызвать эту ошибку: был достигнут мягкий ограничитель RLIMIT_NPROC (задаётся с помощью setrlimit(2)), который ограничивает количество процессов и ните для реального ID пользователя; был достигнут ядерный системный ограничитель на количество процессов и нитей, /proc/sys/kernel/threads-max (смотрите proc(5)); был достигнуто максимальное количество PID, /proc/sys/kernel/pid_max (смотрите proc(5)).

EAGAIN Вызывающий работает по алгоритму планирования SCHED_DEADLINE и у него не установлен флаг сброса-при-fork (reset-on-fork). Смотрите sched(7). ENOMEM Вызов fork() завершился с ошибкой из-за невозможности разместить необходимые структуры ядра, потому что слишком мало памяти. ENOSYS Вызов fork() не поддерживается на этой платформе (например, из-за того, что аппаратное обеспечение не содержит блока управления памятью (MMU)).

СООТВЕТСТВИЕ СТАНДАРТАМ

ЗАМЕЧАНИЯ

В Linux, fork() реализован с помощью «копирования страниц при записи» (copy-on-write, COW), поэтому расходы на вызов состоят из времени и памяти, требуемой на копирование страничных таблиц родителя и создания уникальной структуры, описывающей задачу.

Источник

fork() — зло; vfork() — добро; afork() — лучше; clone () — глупо

Недавно я наткнулся на реализацию popen() (та же идея, другой API) с использованием clone(2), где я открыл issue с запросом использования vfork(2) или posix_spawn() в целях лучшей переносимости на другие платформы. Оказывается, для Linux есть одно очень важное преимущество в использовании clone(2). И вот я думаю, что мне следует раскрыть тему, которую я там затронул, где-нибудь еще: в гисте, в блоге, да где угодно.

Давным-давно я, как и многие фанаты Unix, думал, что fork(2) и модель порождения процессов fork-exec были лучшим решением в мире, а Windows нервно курил в сторонке, имея только exec*() и _spawn*(), последний из которых был чистым виндоусизмом.

После многих лет практики я пришел к выводу, что fork(2) на самом деле является злом. А vfork(2) , долгое время считавшийся злом, на самом деле является добром. Изящная вариация vfork(2) , которая избегает необходимости блокировать родителя, была бы даже лучше (об этом чуть позже).

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

Я не буду утруждать себя объяснением, что такое fork(2) — если вы это читаете, я полагаю, вы уже знаете. Но я расскажу про vfork(2) и почему он считается опасным. vfork(2) очень похож на fork(2) , но новый процесс, который он создает, запускается в том же адресном пространстве, что и родительский, как если бы он был потоком. Он даже разделяет тот же стек с потоком, который вызвал vfork(2) ! Два потока не могут совместно использовать стек, поэтому родительский процесс останавливается до момента, когда дочерний выполнит свою задачу: либо exec*(2) , либо _exit(2) .

3BSD добавила vfork(2) , а несколько лет спустя 4.4BSD удалила его, так как к тому времени он стал считаться небезопасным. По крайне мере так говорят на страницах нескольких последующих руководств. Но производные 4.4BSD восстановили его и не называют небезопасным. Для этого есть причина: vfork(2) гораздо дешевле, чем fork(2) — намного, намного дешевле. Это потому, что fork(2) должен либо скопировать адресное пространство родителя, либо организовать копирование при записи (что должно быть оптимизацией, чтобы избежать ненужных копий). Но даже копирование при записи очень дорого обходится, потому что требует изменения маппинга памяти, дорогостоящего устранения ошибок страниц и т. д. Современные ядра, как правило, seed’ят дочерний элемент копией резидентного набора родительского объекта, но если родительский элемент имеет большой объем потребляемой памяти (например, JVM), то RSS будет огромным. Таким образом, fork(2) неизбежно дорог, за исключением небольших программ с небольшими объемами потребляемой памяти (например, shell).

Итак, вы начинаете понимать, почему fork(2) — зло. И я еще не дошел до рисков, связанных с fork-safety! Соображения fork-safety во многом схожи с thread-safety (потокобезопасностью), но сделать библиотеки fork-safe сложнее, чем thread-safe. Я не буду здесь вдаваться в тонкости fork-safety: в этом нет необходимости.

(Прежде чем продолжить, я должен признаться в лицемерии: я пишу код, который использует fork(2) , зачастую для многопроцессных демонов, в отличие от многопоточности, хотя я часто использую и последнее. Но форки там происходят очень рано, когда ничего fork-unsafe еще не произошло, а адресное пространство еще мало, что позволяет избежать большинства зол fork(2) . vfork(2) не может использоваться в этих целях. В Windows нужно было бы прибегать к CreateProcess() или _spawn() для реализации многопроцессных демонов, что является большой головной болью.)

Почему я тогда думал, что fork(2) элегантен? По той же причине, что и все остальные: CreateProcess*() , _spawn() и posix_spawn() , и такие функции чрезвычайно сложны, какими они и должны быть, потому что существует огромное количество вещей, которые можно сделать между fork() и exec() , скажем, shell. Но с fork() и exec() не нужен язык или API, которые могут выразить все эти вещи: язык хоста подойдет! fork(2) дал создателям Unix возможность перенести всю эту сложность из kernel-land в user-land, где гораздо проще разрабатывать программное обеспечение — это сделало их более продуктивными, возможно, намного более эффективными. Цена, которую создатели Unix заплатили за эту элегантность, заключалась в необходимости копирования адресных пространств. Поскольку в то время программы и процессы были небольшими, их неэлегантность было легко не заметить. Но теперь процессы имеют тенденцию быть огромными, и это делает копирование даже только резидентного набора родительского объекта и возню с таблицей страниц для всего остального чрезвычайно дорогостоящим занятием.

Но у vfork() есть вся эта элегантность и нет недостатков fork() !

У vfork() есть один недостаток: родительский процесс (в частности, поток в родительском процессе, который вызывает vfork() ) и дочерний процесс совместно используют стек, что требует остановки родительского (потока) до тех пор, пока дочерний процесс не выполнит exec() или _exit() . (Это можно простить из-за того, что vfork(2) давно предшествовал потокам — когда потоки появились, необходимость в отдельном стеке для каждого нового потока стала совершенно очевидной и неизбежной. Исправление для потоковой передачи заключалось в использовании нового стека для нового потока и использовании коллбек-функции и аргумента как main()-подобия для этого нового стека.) Но блокировка — это плохо, потому что синхронное поведение плохо, особенно когда это единственный вариант, но все могло бы быть лучше. Асинхронная версия vfork() должна будет запускать дочерний процесс в новом/альтернативном стеке. Назовем ее afork() или avfork() . afork() должен быть очень похож на pthread_create() : он должен принимать функцию для вызова в новом стеке, а также аргумент для передачи этой функции.

Я должен упомянуть, что все справочные страницы vfork() , которые я видел, говорят, что родительский процесс останавливается до тех пор, пока дочерний не совершит exit/exec, но это предшествует потокам. Linux, например, останавливает только один поток в родительском процессе — тот который вызвал vfork() , а не все потоки. Я считаю, что это правильно, но другие IIRC ОС останавливают все потоки в родительском процессе (что является ошибкой, IMO).

afork() позволил бы API по типу popen() очень быстро возвращать с соответствующими пайпами для I/O с детьми. Если что-то пойдет не так на стороне дочернего процесса, тогда дочерний процесс выйдет, и их выходной пайп (если есть) продемонстрирует EOF, и/или записи на дочерний вход получат EPIPE и/или вызовут SIGPIPE, после чего вызывающий popen() сможет проверить наличие ошибок.

С таким же успехом можно было одолжить флаги forkx()/vforkx() Illumos и сделать так, чтобы afork() выглядел как-то так:

Можно также реализовать что-то вроде afork () (без Illumos forkx() флагов) в системах POSIX, используя pthread_create() для запуска потока, который будет блокироваться в vfork() , пока вызвавший afork() процесс продолжает свои дела. Добавьте taskq, чтобы заранее создать столько рабочих потоков, сколько необходимо, и у вас будет быстрый afork() . Однако afork() , реализованный таким образом, не сможет вернуть PID, если только потоки не в taskq pre-vfork (хорошая идея!), вместо этого потребуется колбек по завершению, что-то вроде этого:

Если потоки pre-vfork, то может быть реализован возвращающий PID afork() , хотя передача задачи pre-vfork потоку может быть сложной задачей: pthread_cond_wait() может не работать в дочернем процессе, поэтому придется использовать пайп, в который записывается указатель на отправленный запрос. (Пайпы безопасны для использования на дочерней стороне vfork() . То есть, read() и write() на пайпе безопасны в дочернем процессе vfork() ) Вот как это будет работать:

В заголовке также говорится, что clone(2) — это глупо. Позвольте мне объяснить это. clone (2) изначально был добавлен как альтернатива полноценным потокам POSIX, которые можно было использовать для реализации потоков POSIX. Идея заключалась в том, что было бы неплохо иметь множество вариаций fork() , и, как мы видим здесь, это действительно так, что касается avfork() ! Однако avfork() не был изначальной мотивацией. На пути к NPTL было сделано много ошибок.

В Linux должен был быть системный вызов создания потока — тогда он избавил бы себя от боли, связанной с первой реализацией pthread для Linux. Linux следовало извлечь уроки из Solaris/SVR4, где эмуляция сокетов BSD через libsocket поверх STREAMS оказалась очень долгой и дорогостоящей ошибкой. Эмулировать один API из другого API с несоответствием импеданса в лучшем случае сложно.

С тех пор clone() превратился в швейцарский армейский нож — он эволюционировал, чтобы иметь функции входа в зоны/jail’ы, но только своего рода: в Linux нет полноценных зон/jail’ов, вместо этого добавляются новые пространства имен и новые флаги clone(2) , которые идут с ними. И поскольку новые связанные с контейнером флаги clone(2) добавляются, старый код может захотеть их использовать. только придется изменить и перестроить рутину вызовов clone(2) , что явно не элегантно.

В Linux должны быть первоклассные системные вызовы fork() , vfork() , avfork() , thread_create() и container_create() . Семейство форков могло бы быть одним системным вызовом с параметрами, но потоки не являются процессами и не являются контейнерами (хотя контейнеры могут иметь процессы и могут иметь процесс minder/init). Объединение всего этого в один системный вызов кажется немного сложным, хотя даже это было бы нормально, если бы был только один флаг для записи контейнера/запуска/форка/любой метафоры, применяемой к контейнерам. Но дизайн clone(2) или его разработчики поощряют распространение флагов, что означает, что нужно постоянно обращать внимание на возможную необходимость добавления новых флагов в существующие места вызовов.

Мои друзья часто говорят мне, и я много где это встречаю, что «нет, контейнеры — это не зоны/jail’ы, они не предназначены для такого использования», но меня не волнуют эти аргументы. Миру нужны зоны/jail’ы, а контейнеры Linux действительно хотят быть зонами/jail’и. Да, хотят. Зоны/jail’ы должны начинать жизнь максимально изолированными, а совместное использование нужно добавлять явно с хоста. Делать это наоборот — плохо, потому что каждый раз, когда изоляция увеличивается, приходится патчить clone(2) вызовы. Это не лучший подход к безопасности для ОС, которая не интегрирована по принципу «сверху вниз» (в Linux у всего есть разные мейнтейнеры и сообщества: ядро, библиотеки C, каждая важная системная библиотека, shell’ы, система инициализации, все пользовательские программы — у всего). В таком мире контейнеры нужно начинать с максимальной изоляции.

Я мог бы продолжать. Я мог бы поговорить о безопасности форка. Я мог бы обсудить все функции, которые обычно или в определенных случаях безопасны для вызова в fork() , в сравнении с дочерним процессом vfork() , в сравнении с дочерним процессом afork() (если он у нас есть) или дочерним процессом вызова clone() (но мне пришлось бы рассмотреть довольно много комбинаций флагов!). Я мог бы рассказать, почему 4.4BSD удалил vfork() (хотя мне пришлось бы немного углубиться в тонкости). Я думаю, что длина этой статьи, вероятно, уже достигла оптимума, поэтому здесь я и остановлюсь.

Источник

Читайте также:  Thoosje windows logon editor
Оцените статью