Комментарии 60
Можно поподробнее про то, что именно получает дочерний процесс (fork и/или exec): открытые файлы, активные соединения, ждущие входящих запросов соединения, итд.
Википедия в этом плане чуть более развёрнуто объясняет:
Между процессом-потомком и процессом-родителем существуют различия:
— PID процесса-потомка отличен от PID процесса-родителя;
значению PPID процесса-потомка присваивается значение PID процесса-родителя;
— Процесс-потомок получает собственную таблицу файловых дескрипторов, являющуюся копией таблицы процесса-родителя на момент вызова fork(). Это означает, что открытые файлы наследуются, но если процесс-потомок, например, закроет какой-либо файл, то это не повлияет на таблицу дескрипторов процесса-родителя.
— для процесса-потомка очищаются все ожидающие доставки сигналы;
— временная статистика выполнения процесса-потомка в таблицах ОС обнуляется;
— блокировки памяти и записи, установленные в процессе-родителе, не наследуются.
Всё остальное наследуется. Если открыт сокет, то после fork'а он будет открыт и у родителя, и у ребёнка. Так как сокет остаётся один, то и очередь у них будет общая.
В man clone это всё описано. Собственно в Linux fork(2) — это обёртка над clone(2) (правда внутри ядра, не внутри системной библиотеки).
enivron?
Мне непонятно, зачем ребёнку наследовать открытые сокеты, файлы и т. д.
Раз потомок получает копию адресного пространства родителя, значит, заодно получает и все открытые файлы (это же тоже структура в памяти).Это структура в другой памяти. И её нужно копировать, да.
Делать иначе — очевидно, дороже (ядро, вместо Copy-On-Write должно будет сделать полную копию памяти и освободить некоторые ресурсы, пользовательская программа — должна проверять валидность некоторых указателей..)Какой ужас. У вас вообще представление есть о том, как файловые дескрипторы работают? Даю подсказку — это ни разу не указатель. Это число. Маленькое. От 0 до 1023 в далёком прошлом, сейчас верхний предел динамический, но принцип тот же. Дальше — рассказывать или сами догадаетесь?
Копировать файловые дескрипторы сложнее и дороже, но удобнее. Hint: Unix, в ранних версиях, сокетов не имел — но зато имел pipes. Дальше рассказывать или сами догадаетесь?
Я программист ненастоящий, man в репозитории нашел :) Если есть маленькое число, идентифицирующее файловый дескриптор, должна существовать структура, указывающая на имя файла и его атрибуты.
Если есть маленькое число, идентифицирующее файловый дескриптор, должна существовать структура, указывающая на имя файла и его атрибуты.Угу. Вот только самое важное вы пропустили — если число маленькое, то это, скорее всего, индекс в массиве. И массив копируется, когда вы делаете fork. И количество ссылок пересчитывается.
В общем совершается довольно много работы, которую можно было бы и не делать, если бы у «копии» процесса все файлы бы закрывались. Но так — этим было бы сложнее пользоваться…
http://turnoff.us/geek/dont-sigkill/
Вы мне глаза решили сломать?
Отсюда есть проблема — т.к. fork делает дублирование текущего процесса, происходит проверка есть ли свободная памяти под этот форк (в 100% размера от текщего процесса), в итоге процесс который использует много памяти (>60%) не сможет запустить другое приложение (тот же /bin/cat) по причине не хватки памяти под форк.
Хм. У меня появилось желание опытным путём проверить истинность этого высказывания. Как только доберусь до ПК, проверю и отпишусь.
Вот мне кажется, что решает.
Просто из вредности я проверил это на 4.15 (поставив vm.overcommit_memory=1), без проблем всё работает (как и ожидалось). Ядро 4.15 (Ubuntu 18.04), реальной памяти 2G, свопа нет, процесс запрашивает 1G памяти через calloc() и потом делает fork() 16 раз — без проблем.
Если посмотреть на процессы, то видно 17 процессов и каждый имеет резидентных чуть более 1G — но реально этот 1G является shared memory. Если же дети начнут писать в эту память — вот тогда начнутся проблемы.
Просто из вредности я проверил это на 4.15 (поставив vm.overcommit_memory=1), без проблем всё работаетДа, это опционально, и vm.overcommit_memory=1 может быть не безопасно, но в дефолтном состоянии будет валится.
Так что не плохо упомянуть «особый» способ запуска новых процессов в статье про процессы.
Обьясните пожалуйста, Вы или кто-то другой, более развернуто, что такое ядро? То есть обозначьте его рамки что-ли. Его везде описывают как некую абстракцию, а хочется чуть больше конкретики, и простым языком.
…
И ещё один вопрос:
void child_sm_kill() { wait(NULL); }
void SendMessage(char *chat_id, char *send_text, int cod)
{
pid_t smpid;
signal(SIGCHLD, child_sm_kill);
smpid = fork();
if(smpid == 0)
{
char json_str[LENJSONSEND] = {0,};
char str[BREADSIZE] = {0,};
if(cod == 0) // strat
{
...
Функция SendMessage вызывается время от времени, выполняет свою работу и завершается.
Скажите, правильно ли я убиваю зомби?
signal(SIGCHLD, child_sm_kill); ⇨ void child_sm_kill() { wait(NULL); }
Заранее спасибо.
Функция SendMessage вызывается время от времени, выполняет свою работу и завершается.
Во-первых, использование signal(2) не рекомендуется, даже ман-страница об этом говорит:
The behavior of signal() varies across UNIX versions, and has also varied historically across different versions of Linux. Avoid its use: use sigaction(2) instead.
Во-вторых, мне кажется несколько избыточным устанавливать обработчик сигналов КАЖДЫЙ раз, это достаточно сделать один раз при инициализации.
К самому обработчику вопросов нет, я бы так же сделал.
В моём понимании, ядро — это просто большая программа и ничего более.Собственно про это и хотел спросить. Мне однажды задали вопрос — «что такое ядро»? — и я не смог дать какого-либо ответа, кроме похожего на Ваш. Углубление в режимы (привилегированный и непривилегированный) приводит к ещё большему размыванию понятия. )))
Просто хотел услышать Ваше мнение и мнение других людей в виде тезиса.
Во-первых, использование signal(2) не рекомендуется, даже ман-страница об этом говорит:Приму к сведению.
Во-вторых, мне кажется несколько избыточным устанавливать обработчик сигналов КАЖДЫЙ раз, это достаточно сделать один раз при инициализации.Достаточно будет поместить его в майн()?
Достаточно будет поместить его в майн()?
Зависит от того, как вы написали программу. Если инициализация программы происходит в main() — то да, самое место. Если где-то ещё — то лучше вставить его туда, где логически ему самое место.
void child_sm_kill() { wait(NULL); }
void child_ui_kill() { wait(NULL); }
void SendMessage(char *chat_id, char *send_text, int cod)
{
pid_t smpid;
signal(SIGCHLD, child_sm_kill);
smpid = fork();
if(smpid == 0)
{
...
void update_instag(char *chat_id)
{
pid_t geekfork;
signal(SIGCHLD, child_ui_kill);
geekfork = fork();
if(geekfork == 0)
{
...
int main()
{
...
… то как быть? Сделать в main() один вызов сигнала для обоих функций?
А процесс может завершаться сколько угодно времени (например если NFS-сервер «выйдет погулять»).
Так что если у вас несколько обработчиков, то это всё равно надёжно работать не будет, а если один — то почему бы его в main и не проинициализировать?
Если же обработчики будут разные — значит, нужно сохранять значения возвращаемые fork в какой-нибудь структуре данных, а в обработчике сигнала проверять что вам вернула wait и принимать на основе этого нужное решение.
Если обработчик будет один, будет ли он правильно работать (подчищать зомби) для обоих функций, или нужно обязательно писать два разных обработчика?
signal(2)
(или, лучше, sigaction(2)
) хоть 100 раз — но работать-то будет только обрабочик, поставленный последним!Потому обработчик должен быть один… а дальше уже всё, что писал mayorovp. Да, конечно, «чистый»
wait
— для «зачистки зомби» достаточен, а если вам нужно что-то большее, то писать два обработчика всё равно бессмысленно, так как использовать-то можно безопасно только один! Нужно как-то в рамках вот этого одного всё разруливать…Мне нужно только убийство зомби. Если я оставлю один вызов сигнала в main(), и один обработчик, то этого будет достаточно для убийства зомби обоих функций?
До этого момента я думал, что понимаю работу signal и wait, оказывается нет.
До этого момента я думал, что понимаю работу signal и wait, оказывается нет.У меня есть ощущение, что вы всей картины не видите.
То, что вы изобразили — это то, что называется code smell — то есть код, который не то, что на 100% неверен, но, скорее, код, который скорее всего неверен — потому что вы не смотрите на всю картину с достаточно большой высоты.
То есть начнём сначала: зачем вы вообще вешаете обработчик на SIGCHLD? Если вам нужно просто запустить ребёнка и дождаться пока он отработает — то никакие сигналы вам не нужны! Просто вызываете
waitpid(2)
и ждёте, пока процесс завершится.Если же вы устраиваете возню с сигналами — то это значит, что вы хотите, чтобы программа работала параллельно со своим ребёнком.
А тогда как вы обеспечите, что ребёнок умрёт и будет «подметён» обрабочиком SIGCHLD, который вы установили до вызова
fork(2)
, до того, как другая функция с другим обработчиком вызовется? Даже если ребёнок сообщает родителю о том, что он завершил работу — я вам сейчас 2-3 сценария могу нарисовать, когда от завершения функции main
до завершения процесса будет полчаса проходить при корректно написанном коде!Да, можно навернуть какие-то локи, семафоры и как-то «разрулить» эту ситуацию… но зачем? В 99% программ проще иметь один обработчик SIGCHLD, который будет «обслуживать» все форки. Ибо, как я уже сказал — этот обрабочик это глобальный ресурс (когда-то давно в Линуксе можно было повесить этот обработчик на поток, но потом кто-то огрел Линуса талмудом с распечаткой POSIX-стандарта по башке и это стало невозможно)!
У меня есть ощущение, что вы всей картины не видите.
Вы правы.
это значит, что вы хотите, чтобы программа работала параллельно со своим ребёнком.
Да, нужно чтоб программа работала независимо от «детей». Функция маин() крутится в цикле и время от времени вызывает функции с форками.
А тогда как вы обеспечите, что ребёнок умрёт и будет «подметён» обрабочиком SIGCHLD, который вы установили до вызова fork(2), до того, как другая функция с другим обработчиком вызовется?
Мыслил исходя из того, что каждый форк делает копию всей программы.
Сделал так:
void ckill_all_childl() { wait(NULL); }
void SendMessage(char *chat_id, char *send_text, int cod)
{
pid_t smpid;
smpid = fork();
if(smpid == 0)
{
...
void update_instag(char *chat_id)
{
pid_t geekfork;
geekfork = fork();
if(geekfork == 0)
{
...
int main()
{
signal(SIGCHLD, kill_all_child);
...
Сигналы — вообще страшная штука. Практически, как прерывания. Могут прийти тогда, когда вы их совершенно не ожидаете (и когда не ожидает рантайм), например во время работы malloc()
или во время pthread_mutex_lock()
. Поэтому и не рекомендуют использовать signal (2)
, а в особо сложных случаях — рекомендуют обрабатывать сигналы синхронно, через sigwait (3)
/sigwaitinfo(2)
Мне однажды задали вопрос — «что такое ядро»? — и я не смог дать какого-либо ответа, кроме похожего на Ваш.
Зависит от того что хочет услышать вопрошающий. Еще круче спросить «что такое ОС»?
Особенно, принимая во внимание существование всяких baremetal OS (типа freertos или minios), микроядер и попытки запихнуть веб-сервер прямо в linux kernel.
Вообще наиболее полный ответ, который я могу дать звучит приблизительно так: «ОС — понятие довольно размытое. В большинстве случаев под этим словом подразумевают минимальных набор системных программ, который позволит пользователю запускать их прикладные приложения и таким образом получать какую-то пользу от использования компьютера». Можно еще ввернуть что-то про управление и разделение ресурсов (процессор, память, сеть, диск, батарея и другая периферия) и абстрагирование от железа, но все равно совершенно точное определение дать не получится потому что практически всегда можно будет привести контрпример.
и попытки запихнуть веб-сервер прямо в linux kernel.
В другой ОС эта попытка даже оказалась успешной...
signal(SIGCHLD, SIG_IGN);
и забываете про зомби как страшный сон — их просто не будет. Для этого простейшего случая использование signal() вместо sigaction() вполне адекватно.
wait() нужно только если вам важен код завершения процесса или если нужно дождаться завершения оного.
Функция signal() вызванная единожды из main() висит где-то в памяти на протяжении всего времени жизни приложения и ожидает сигналов. То есть функция signal() это что-то вроде какого-то отдельного процесса? Или это что-то похожее на обработчик прерываний в микроконтроллерах?
И с параметрами поясните пожалуйста. Первый параметр — это ожидаемый сигнал, второй параметр — это то, что нужно сделать при поступлении сигнала.
То есть, вот это — signal(SIGCHLD, SIG_IGN) нужно понимать как — signal(при получении сигнала от ребёнка, игнорировать сигнал), что значит игнорировать? Типа пошёл ты к чёрту, плевать я на тебя хотел. ))
Но!
Как только приложение становится многопоточным и один из его тредов застревает в этом состоянии, то ситуация становится creepy.
Во-первых, все потоки работают нормально (кроме залипшего). Во-вторых, попытка сделать SIG_STOP приводит к тому, что ядро не возвращает управление из обработчика сигнала (и SIG_CONT не работает). В третьих, выход из треда переводит процесс в состояние (которое я не могу толком описать).
Короче, всё хорошо, пока не случается TASK_UNINTERRUPTIBLE (D+ в ps'е).
Вся информация о завершении процесса влезает в тип данных int.
На самом деле это не совсем так — можно получить несколько больше информации о завершении процесса, если использовать waitid() и потом изучить данные из siginfo_t. Это тоже далеко не всё (есть ещё taskstats), хотя в большинстве случаев это мало кому нужно.
Хорошая статья. Но, не раскрыта тема потоков и примитивов синхронизации.
Например — что будет если ждать в дочернем процессе на cv созданной в родительском? Какие потоки наследуются дочерним процессом? Как с этим жить, какие коллбэки для этого предусмотренны в posix?
Изучаем процессы в Linux