LINUX.ORG.RU

fork() is evil;

 


0

2

So here goes.

Long ago, I, like many Unix fans, thought that fork(2) and the fork-exec process spawning model were the greatest thing, and the Windows sucked for only having exec*() and _spawn*(), the last being a Windows-ism.

After many years of experience, I learned that fork(2) is in fact evil.

https://gist.github.com/nicowilliams/a8a07b0fc75df05f684c23c18d7db234

★★★★★

Медленно до них доходит. fork() изначально defective by design, он ломает логику программы и взаимодействие с внешними ресурсами. Для того чтобы это работало нормально часто требуется костыль в виде обработчика afterFork. Также механизм COW используемый в fork() опасен в плане неконтролируемого роста памяти и в результате нехватки памяти. Например если куча отображена с помощью COW, то при изменении небольшого участка будет скопирована вся страница и в результате в скором времени с большой вероятностью будет скопирована вся куча.

X512 ★★ ()

В статье автор замечает, что CreateProcess и posix_spawn сложны, потому что в один вызов функции засовываются все на свете опции запуска процесса. Я должен заметить, что в форточках на нативном уровне создание процесса и создание главного потока процесса — это две разные задачи, которые выполняются по очереди. То есть, создается процесс, в него грузится выполняемый код, создается главный поток, которому скармливается загруженный код на выполнение — и всё это безо всяких там форков. vfork() делает примерно то же, но наоборот — родительский процесс как бы вселяется в новосозданный дочерний и производит там инициализацию. При этом родительский поток/процесс на время инициализации висит.

А что мешает сделать интерфейсы, которые позволили бы родительскому процессу произвести инициализацию дочернего как это удобно родительскому, без остановки асинхронной обработки других запросов? Или даже передать инициализацию дочернего процесса системной службе. Кривые POSIX интерфейсы, которые испокон веков однопоточны и синхронны?

byko3y ★★★ ()
Последнее исправление: byko3y (всего исправлений: 1)
Ответ на: комментарий от byko3y

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

В Haiku также. Она вообще во многих чертах на Windows NT похожа. В Линуксе вообще потоков нет, это процессы с общим адресным пространством, что задаётся флагом системного вызова clone.

А что мешает сделать интерфейсы, которые позволили бы родительскому процессу произвести инициализацию дочернего как это удобно родительскому, без остановки асинхронной обработки других запросов?

Это как? Создать недоинициализированный процесс и потом с помощью API его настраивать и инициализировать?

X512 ★★ ()
Ответ на: комментарий от X512

В Линуксе вообще потоков нет, это процессы с общим адресным пространством

Это бессмысленное жонглирование словами с непонятной целью исказить общепринятые формулировки. Единицы учёта шедулера ОС в общем адресном пространстве = «потоки».

LamerOk ★★★★★ ()
Ответ на: комментарий от X512

Да неужели.

#include <future>
#include <boost/process/environment.hpp>

int main() {
  return !(boost::this_process::get_id() == std::async(
      std::launch::async, []() noexcept {
        return boost::this_process::get_id();
      }
    ).get());
}
$ c++ -std=c++17 -otest -pthread test.cc -lboost_system && ./test && echo 'X512 некомпетентен'
X512 некомпетентен
anonymous ()
Ответ на: комментарий от anonymous

boost::this_process::get_id();

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

X512 ★★ ()
Последнее исправление: X512 (всего исправлений: 2)
Ответ на: комментарий от X512
#include <future>
#include <thread>

int main() {
  return !(std::this_thread::get_id() != std::async(
      std::launch::async, []() noexcept {
        return std::this_thread::get_id();
      }
    ).get());
}
$ c++ -std=c++17 -otest -pthread test.cc && ./test && echo 'X512 не отличает PID и TID'
X512 не отличает PID и TID

Ололо.

anonymous ()

Мне после чтения того толстого sysdev мануала по 386 всегда непонятно было, чо все так носятся с созданием процесса. Ну выделил страниц, скопировал/расшарил pic-дллов туда с диска или уже готовых, стек сегмент указал и погнали. Кроме неминуемых затрат цпу особо ничего делать не надо. И если надо процессу передать дохрена всего, можно в родителе выделить страниц, записать туда данные, а потом отчуждить их в пользу чилда, а там уж он решит их судьбу. А то придумали херни, засунь все через дырку exec(argv), то ли код дескриптора туда, то ли имя файла. Самые стремные интерфейсы это когда надо что-то сериализовывать через замочную скважину.

Мне кажется эти все проблемы и стенания (в линуксе форк хармфул, в винде createprocess soooo slow) от изначально кривой архитектуры. Процесс это tss, stack и немного структур в «ядре» (да, я в курсе, что хардваре тсс сейчас не юзают, но суть одна). Это вообще одна из самых простых вещей, по сравнению с сокетами или девайсами.

В чем я ошибаюсь?

anonymous ()
Ответ на: комментарий от anonymous

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

anonymous ()
Ответ на: комментарий от X512

Создать недоинициализированный процесс и потом с помощью API его настраивать и инициализировать?

Ну да. Ты же можешь с помощью API, например, приостановить процесс, трассировать системные вызовы, или вообще убить его — почему нельзя запустить его на выполнение?

byko3y ★★★ ()
Ответ на: комментарий от anonymous

Все знают, что треды в линуксе прилеплены сбоку на основе процессов

Это дошколятские поверья. Там кто-то написал подобное, имея ввиду полиморфную обработку в ядре, тот же clone. Но дошколята увидели, нихрена не поняли и давай нести шаблонную херню про «нет потоков в линуксе». Уровень.

Там в толксах твоя сосед по парте бегает, meliafaro называется. Вот у того трепла по ходу «биты в линуксе прилеплены сбоку на основе байтов».

anonymous ()
Ответ на: комментарий от WitcherGeralt

Где есть?! Как это они, вообще, смогли реализовать?

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

Смысл форка в том, чтобы в дочернем процессе настроить каналы, а потом запустить нужный образ в рамках дочернего процесса.

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

Поэтому не сочиняй, товарищ! Ты просто не в курсе дела.

anonymous ()

с неделю максимум, через YC попадалось прекрасное - fork() наоборот :-) то есть объединение пространств двух процессов в один.

Практической пользы не увидел, поэтому подиагонали только и посмотел. И не поставил закладку и теперь фик найти…

если кто ЭТО встретит - киньте тут ссылкой

MKuznetsov ★★★★★ ()
Ответ на: комментарий от WitcherGeralt

Там же куча предостережений в описании! Если уповать на то, что после форка нигде в дочернем процессе не сработает ни одна блокировка, а пользователь будет достаточно грамотен, чтобы во время загрузить в дочерний процесс новый образ, то может и прокатит. Видишь, как много «если», сколько неуверенности!

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

anonymous ()
Ответ на: комментарий от anonymous

Двойной форк безупречно работает, никаких проблем за лет десять у меня не было. Это я так по классике демона пулял. Там нужно было небольшое количество логики, а шелл я тогда не знал.

WitcherGeralt ★★ ()
Последнее исправление: WitcherGeralt (всего исправлений: 1)
Ответ на: комментарий от WitcherGeralt

А я видел, как постоянно падал под нагрузкой в корку дочерний процесс, пока не убрали оттуда printfn, вызываемый до загрузки другого образа после форка (printfn использует блокировки). Падал непредсказуемо. Примерно в одном случае из тысяч (!!) запусков, но стабильно каждый день. Нагрузка на многоядерный процессор была примерно 97-98%.

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

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

А так, вот что выдает команда man fork на макоси:

[quote] CAVEATS There are limits to what you can do in the child process. To be totally safe you should restrict yourself to only executing async-signal safe operations until such time as one of the exec functions is called. All APIs, including global data symbols, in any framework or library should be assumed to be unsafe after a fork() unless explicitly documented to be safe or async-signal safe. If you need to use these frameworks in the child process, you must exec. In this situation it is reasonable to exec yourself. [/quote]

Короче, тебе просто пока везет.

anonymous ()
Ответ на: комментарий от LamerOk

Почему нельзя? Можно: DWORD ResumeThread(HANDLE hThread);

Это же оффтоп. Отправка сигналов потокам есть только в лине, и то не для всех сигналов. Мы как бы снова возвращаемся к тому, что в никса понятия «поток» изначально вообще не существовало, поток и процесс были одним и тем же, потому понятия «создать поток», «остановить поток» в принципе отсутствовали.

byko3y ★★★ ()
Ответ на: комментарий от byko3y

Это же оффтоп.

Ничего это не «оффтоп», а вполне легальный способ запустить поток на исполнение в другом процессе.

Отправка сигналов потокам есть только в лине, и то не для всех сигналов.

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

в никса понятия «поток» изначально вообще не существовало

Всё верно, и для это есть отдельная библиотека pthreads, в рамках которой есть все эти pthread_create, pthread_cancel, pthread_kill.

LamerOk ★★★★★ ()
Ответ на: комментарий от LamerOk

Ты определись, про что ты, - про межпроцессное взаимодействие с потоками или про юниксовые сигналы (которые тут вообще не в тему)
Всё верно, и для это есть отдельная библиотека pthreads, в рамках которой есть все эти pthread_create, pthread_cancel, pthread_kill

А как по-твоему реализованы pthread_cancel/pthread_kill? Через D-Bus?

byko3y ★★★ ()
Ответ на: комментарий от pathfinder

Негативные последствия получаются, когда делаешь форк из потока многопоточного процесса. Так как в момент форка в оригинальном процессе исполняется несколько потоков, щелкающих мьютексами, в копии этого процесса эти мьютексы оказываются в непредсказуемом состоянии. Поэтому в чайлде после форка нельзя вызывать библиотечные функции, они непредсказуемо виснут на мьютексах. Например, malloc нельзя, localtime нельзя, stdio нельзя, и т.д. Переинициализировать все библиотечные мьютексы тоже невозможно, они не публичны. Все что можно, по сути, это системными вызовами setgid, setuid, dup, и т.п. подготовить среду исполнения и загрузить в процесс новый образ execve.

iliyap ★★★★★ ()
Ответ на: комментарий от X512

Линуксе вообще потоков нет, это процессы с общим адресным пространством, что задаётся флагом системного вызова clone.

Вообще-то в линуксе нет процесов, а как раз есть только треды с разными атрибутами, которые позволяют обозвать некоторые треды «процессом»...

Jetty ★★★★★ ()