Как стать автором
Обновить

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

Они выбивают этот номер, а затем используют его позже.

В камне выбивают? Кто "они"?

А в оригинале там и вовсе "sock it away" — откладывают

Типа на чёрный день. До лучших времён. Сохраняют. Да. "Выбивают в камне" подходит.
И да, в оригинале и вовсе "fork can fail", а мы тут имеем что имеем: "форк может глюкануть". Перевод кэн глюкануть.

По-моему, это автора глюкануло. А fork() может ошибку вернуть.

это автора перевода глюкануло. В оригинале fork can fail.

Да, именно так (я смиренно полагал себя больше похожим на человека, который не будет спорить с оригиналом в статье-переводе, но имею что имею).


Впрочем, был разочарован и тем, и другим. Заголовок со словом «глючить» был кликбейтом. По нему я ожидал увидеть какое-нибудь месиво, когда «мы форкаем 200000 раз в секунду и вот на 200001-м начинается неведомое, мы ловили баг в ядре месяц всем отделом», а тут -1, блин. Во-вторых, оригинал тоже написан довольно удивительным языком (на мой вкус, разговорным) и его можно было в два-три твита втолкать.


При этом периодически ликбез проводить нужно, и опять люди могут принести довод «форма или содержание», но вот конкретно тут можно было несложно и содержание хорошее сделать, и форму. А не переводить дословно многоточия. В результате 180 комментариев про «документацию никто не читает». Так её ещё и не пишут иногда. Горе горькое, в общем.

Форк может глюкануть. Понимаете? В самое деле, понимаете?

А в чем с этим проблема?
Может не хватить памяти, может не хватить прав, может не хватить места, да мало ли причин.
Почему именно глюк виноват?

Похоже, всем известно, что fork возвращает дочернему процессу 0, а родителю некоторое положительное число — pid ребенка.

Или -1, если не получилось:
RETURN VALUE
Upon successful completion, fork() returns 0 to the child process and returns the process ID of the child process to the parent process. Otherwise, -1 is returned to the parent process, no child process is created, and errno is set to indicate the error.

Угадайте, что происходит, когда вы не проверяете ответ на ошибку? Да, вы возьмёте "-1" (глюк форка) как pid.

Происходит то, что происходит, если не читать документацию и не обрабатывать ошибки.

Это только начало. Настоящая боль начинается позже

Но ведь не форк в этом виноват?

Я понял. Надо писать софт без багов, а софт с багами писать не надо. Всё просто, да?

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

Читать-то много чего надо, но простое чтение документации само по себе ошибку не исправит, надо ещё и возвращаемое значение на -1 проверить же.


Многие API устроены более "прощающим" образом: значение, сигнализирующее об ошибке, непригодно к использованию и вызывает другую ошибку при попытке использования. Например, malloc при ошибке возвращает нулевой указатель, обращение по которому в большинстве систем вызывают SIGSEGV. Это неприятно — но дальше процесса проблема не распространяется.


Информация о том, что пара fork+kill лени в обработке ошибок не прощает, важна.

Читать-то много чего надо, но простое чтение документации само по себе ошибку не исправит, надо ещё и возвращаемое значение на -1 проверить же.


Ну, справедливости ради, конкретно в этом данном случае чтение документации в процессе разработки ошибку исправит. Точнее, не даст допустить…

Неа, если программист уже настроился что пишет "наколеночную поделку" и решил на всякие редкие случаи сознательно забить — ничто не подскажет ему, что -1 является очень опасным значением для pid_t.

Неа, если программист уже настроился что пишет «наколеночную поделку» и решил на всякие редкие случаи сознательно забить


Это как раз тот случай, когда программист доку не прочитал. Собственно, ограничить диапазон pid_t положительными числами много ума не надо. При этом в качестве возвращаемых значений практически во всех юниксовых апи используется ровно 2 модели: «0 — ок, !0-код ошибки» (которая, очевидно, неприменима для конкретно взятого кейса) и ">0 — искомое значение, <0 — код ошибки * (-1)" (которая, очевидно, наш кейс).

Никакого «срыва покровов» не происходит. Если так «низкоуровнево» не хочется — юзаем высокоуровневые обертки, где таковой проблемы нет. А если хочется «низкоуровнево» — ну, страдаем и читаем документацию.
Большинство API при ошибке возвращают непригодное значение. Например, если мы сделаем open-read-write-close и случится ошибка на первом этапе (файл не найден или нет прав), то каскадно выдадут ошибку все последующие системные вызовы и ничего страшного не случится. Аналогично с сокетами и т. д.

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

А fork+kill выделяется и большого количества других API, потому что невалидный результат одного является валидным результатом для другого. Да ещё и насколько валидным — рассылка сигнала всем процессам в системе. Эффект от сбоя выходит далеко за пределы программы и даже явным образом обрабатываемых ею данными. Это грабли, на которые можно наступить (в реальном мире все люди иногда ошибаются, просто кто-то это делает реже, кто-то чаще).
ну для работы функций в С часто используется unsigned, а возврат часто signed и -1 при ошибке, так повелось со времен ассемблера, так как какой ни будь 0xFFFF не является валидным значением, но в «высших» языках это -1
Большинство API при ошибке возвращают непригодное значение.


Ну вот fork(), при ошибке возвращает -1, что явным образом является непригодным в качестве pid значением. Все верно же? Все нормально, получается, с fork'ом?

Возможно, повредит данные, с которыми работала (но программист знает с какими данными работает и учитывает риски их повреждения, принимая решение написать что-то быстро).


Ну вот kill с -1 — просто убьет все процессы пользователя, от которого запущена. Одноразовое наколеночное решение с отношением «насрать даже, если какие-то данные повредит» — вполне нормально, почему нет. Если не хотите завершения процессов — таки прочтите доку на kill.

А fork+kill выделяется и большого количества других API


Ну вот нет такого API fork+kill. Есть API fork и есть API kill. Что то вроде (осторожно, псевдокод) `int result = process.fork().doSomething().kill()` у вас не получится. fork() возвращает int, т.е. скаляр, обычное целое число. Корректно интерпретировать это целое число — ваша работа. kill() принимает на вход целое число и возвращает другое целое число — передать корректное значение и корректно интерпретировать число на выходе — ваша обязанность.

Все unix-API написаны достаточно давно, и возвращают целые числа. Да, их надо интерпретировать. Да, вам, возможно, это неудобно. Но причина такому поведению есть — производительность. В вашем случае «одноразовое наколеночное решение» эта штука (производительность) не так уж чтобы прям настолько важна. Поэтому для вас предпочтительным решением будет таки не использовать напрямую fork() и kill(). Вот прям никогда не использовать. Над ними вагон и маленькая тележка более-менее удобных оберток, «представляющих удобную абстракцию процесса».

потому что невалидный результат одного является валидным результатом для другого


Ну, во первых, для второго это таки не валидный результат, а валидный параметр. При этом откуда взялось понятие «невалидный результат». Нормальный результат fork() -1, означающий, что процесс не удалось форкнуть. Его можно интерпретировать, его можно обрабатывать. Единстенное, что нужно понимать — fork() не возвращает процесс. Форк возращает числовой идентификатор процесса, он возвращает число. Если это число отрицательное, значит форк не удался, и с этим что-то надо делать. Отрицательное значение вы обязаны проверить либо на этапе собственно форка, либо на входе в kill().

В общем и целом, имхо, тут как всегда. Либо крестик снимите, либо рясу в трусы не заправляйте. Либо вы пишете действительно низкоуровневые вещи, и вам нужен «в прямую» fork() и kill(), и тогда непонятны стенания по поводу «ой, там документацию читать надо». Низкоуровневые вещи — это всегда trade-in между удобством API и производительностью. Либо, если вы пишете пользовательский софт — почему не пользуетесь сейфовыми API-обертками, в которых «доку читать не надо»?
Ну вот fork(), при ошибке возвращает -1, что явным образом является непригодным в качестве pid значением. Все верно же? Все нормально, получается, с fork'ом?

Неа, не является. Если можно передать в kill, и kill от этого не вернёт ошибки — какое же оно непригодное?


Если не хотите завершения процессов — таки прочтите доку на kill.

Так доки на kill недостаточно, чтобы понять где проблема — нужно читать обе доки, и на kill, и на fork.


Поэтому для вас предпочтительным решением будет таки не использовать напрямую fork() и kill(). Вот прям никогда не использовать.

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

Неа, не является.


Кхм, с чего это -1 не является некорректным значением pid? Давайте попробуем расшифровать: знакомьтесь, pid — process identificator, идентификатор процесса. Вы, видимо, сможете мне показать процесс, имеющий идентификатор -1?

Если можно передать в kill, и kill от этого не вернёт ошибки — какое же оно непригодное?


Если число можно передать в kill, это еще совершенно не говорит о том, что это число — pid.

Так доки на kill недостаточно, чтобы понять где проблема — нужно читать обе доки, и на kill, и на fork.


Не, любой одной из — достаточно. Из доки на fork вы узнаете, что возвращаться могут не только целые положительные числа (собственно, тут и доки на fork не надо, оно в целом в api так устроено, любой вызов потенциально может вернуть код ошибки). Более того, при форке вы один хрен проверяете код возврата, т.к. есть еще значение «0». Если вы не проверяете его, то вы делаете что-то странное. Например, в случае ошибки вы посылаете kill туда, куда прав хватает, а в случае корректного fork'а — посылаете из дочернего процесса kill всем процессам в группе, которой принадлежит процесс.

Еще раз: если у вас возникла проблема с -1, вы изначально еще на этапе fork'а делаете что-то странное.

В случае прочтения доки на kill вы видите, что на вход принимается либо явный id процесса, либо что-то из специальных значений, поведение которых вы явно не хотите лицезреть. Тогда вы вставляете проверку перед вызовом kill.

Если вы этого не делаете, вы, видимо, не читали доку ни на fork, ни на kill.

Более того, так лучше делать даже там, где производительность важна.


Ну, пока именно обертки над fork/kill не станут ботлнеком всей высоконагруженной системы (спойлер: для прикладной системы — никогда).

Осталось понять как другой программист может прийти к тому же самому выводу не читая постов подобных обсуждаемому.


Ну, пост таки про то, что «glibc — ужас-ужас», а не про специфику. Тут вопрос в том, скольких потенциально хороших специалистов оно отпугнет.
Тут вопрос в том, скольких потенциально хороших специалистов оно отпугнет.

Тот случай, когда лучше отпугнуть.

Все unix-API написаны достаточно давно, и возвращают целые числа. Да, их надо интерпретировать. Да, вам, возможно, это неудобно. Но причина такому поведению есть — производительность.

Если это так, то какого хрена у нас есть kill, работающий совершенно по другому при параметре -1, вместо раздельных kill и killall? Обработка особого значения параметра требует отдельного if внутри kill, так что это не максимальная производительность.

Если это так, то какого хрена у нас есть kill, работающий совершенно по другому


Не понял, в чем по другому? kill(), ровно как и все остальные, возвращает целое число — можете проверить спеку. Причем kill прямо в явную int возвращает. Что не так?
Вопрос был не про то, что он возвращает, а про то, что он принимает. А принимает он значение -1, которое для вызова, порождающего процессы, является «сообщение» об ошибке. И да, напрашивается именно отдельный вызов для рассылки сигнала по площадям, а не с помощью специфического значения, которое в той же «предметной области» — управлении процессами — имеет совсем другой смысл, при том, что вызову kill зачастую отдаётся именно то, что создаёт fork(), а не результат последующих преобразований. А жёсткая необходимость проверки результата fork() — это как раз bug by design, костыль. Потому что, как было написано выше, в других случаях передача значения, являющегося индикатором ошибки у функций из той же «предметной области», приводит к сбою, а не к успешному выполнению с фатальными для системы последствиями. Например, попытка записи по тому дескриптору файла, который вернул неудачный вызов open(), приведёт к сбою и завершению программы, а не к мочаливому затиранию, к примеру, корневой директории.

P.S. Кстати, если уж надо проверять, а проверять надо, то почему тогда на впилить проверку именно в код kill()? Проверка всё равно быть должна, так что пусть будет в одном месте. Я понимаю, что если экономить байты и такты, причём, именно байты и так ты в штуках, это не лучший вариант, но всё же.
Вопрос был не про то, что он возвращает, а про то, что он принимает.


Вопрос был именно про то, что возвращает. Исходный посыл был следущий: «Все unix-API написаны достаточно давно, и возвращают целые числа.» Ну, они действительно так написаны, и действительно возращают коды возврата в виде целых чисел. На это возникло возражение: «какого хрена у нас есть kill, работающий совершенно по другому». kill() ровно так же возвращает целое число в качестве кода возврата, несоответствия не вижу. Принимать только положительные целые числа никто не обещал, список входных параметров — индивидуален для каждого метода.

А принимает он значение -1, которое для вызова, порождающего процессы, является «сообщение» об ошибке.


Воот, мне кажется, вы начинаете догадываться, что происходит. Для примера, есть число 2. Это просто число. Есть 2 однополых друга, есть 2 партнера в сексе. Вы же не возмущаетесь, что «и там, и там — 2, в чем разница, почему 2 друзьям нельзя», вы же понимаете, что это принципиально разные 2. Вот и здесь fork() возвращает код возврата -1, а kill() на входе просит идентификатор процесса или (в конкретном случае -1) специпальное wildcard-значение. Зачем вы во входной параметр kill суете код возврата fork? Это разные вещи, а чисел — одно единственное бесконечное множество.

в той же «предметной области» — управлении процессами


В предметной области unix-api (которые таки да, древние, и т.д. и т.п.) принято явным образом понимать, что внутри что у кода возврата, что у дескриптора открытого файла, что у pid процесса — совершенно одинаковые числа. То, что возвращает вам любой вызов unix-api — это просто число. Даже процесс (любой) при завершении возвращает число (0 — удачно, все остальное — код ошибки). Ни один вызов unix-api не возвращает в явную что-то отличное от кода возврата. Там прямо так и написано в fork() — код возврата, интерпретировать так и так. Нигде не написано, что это pid процесса.

В C достаточно бедная система типов, поэтому принято явно разделять входные параметры и выходные. Пихать выходной параметр одной функции в другую без любых проверок — явный моветон. Это примерно как в той же Java не обрабатывать исключения или в Go тупо underscor'ить ошибки.

оторое в той же «предметной области» — управлении процессами — имеет совсем другой смысл


Смысл кодов возврата внутри указанной предметной области строго идентичен. Коды возврата что у fork(), что у kill() ведут себя одинаково.

вызову kill зачастую отдаётся именно то, что создаёт fork(), а не результат последующих преобразований


Ну, допустим, зря и говнокод.

А жёсткая необходимость проверки результата fork() — это как раз bug by design, костыль.


Погодите-погодите… Вы же мне подскажете язык/среду в которой в явную считается хорошим тоном игнорировать ошибки? Или таки везде принято проверять возвращаемое значение, не только в C?

передача значения, являющегося индикатором ошибки


… во всех без исключения случаях не является разумным подходом, например.

с фатальными для системы последствиями


Хм, прям фатальными-фатальными? От завершения сеанса кто-то умер уже?

Кстати, если уж надо проверять, а проверять надо, то почему тогда на впилить проверку именно в код kill()


Проверку чего вы собираетесь в код kill впилить? Вот есть у нас числа 3857, 0, -1, -100 — какое из них некорректное входное значение для kill?

Есть 2 однополых друга, есть 2 партнера в сексе. Вы же не возмущаетесь, что «и там, и там — 2, в чем разница, почему 2 друзьям нельзя», вы же понимаете, что это принципиально разные 2.

А почему, собственно говоря, нельзя?

Ну, справедливости ради, конкретно в этом данном случае чтение документации в процессе разработки ошибку исправит. Точнее, не даст допустить…
Не даст допустить язык с алгебраическими типами данных, в котором значение перед использованием обязаны проверить, вместе с пожизненным запретом писать на языках без него некоторым людям >_<
ну любой стиль программирования с ассертами на валидное избавит от этой ошибки
Если взять любой низкоуровневый код в продакшне, то там будет дофига ассертов, что является хорошим стилем, но плохо выглядит в коде и в примерах использования часто ассерты опускают
НЛО прилетело и опубликовало эту надпись здесь

Не избавит. Ситуация редкая и на отладочной сборке имеет все шансы никогда не случиться.
А на релизной ассерты превращаются "в тыкву".

Конечно, запретить было бы хорошо, но…
Предполагаю, однако, что те самые «некоторые люди», склонные допускать ошибку из статьи, также вряд ли способны и освоить язык с алгебраическими типами данных.
А программисты-то нынешнему народному хозяйству нужны, и много. И терять таких вот «некоторых людей», которые уже чему-то обучены — это не выгодно экономически.
Куда проще IMHO этим «некоторым людям» запретить пользоваться низкоуровневыми языками, дозволяющими прямой доступ к потенциально опасным системным вызовам. Тем более, что эти «некоторые люди» обычно и так не рвутся использовать низкоуровневые языки, как слишком недружественные.
Предполагаю, однако, что те самые «некоторые люди», склонные допускать ошибку из статьи, также вряд ли способны и освоить язык с алгебраическими типами данных.
АДТ это очень просто, и если захотеть, это можно воткнуть хоть в бейсик.
А программисты-то нынешнему народному хозяйству нужны, и много. И терять таких вот «некоторых людей», которые уже чему-то обучены — это не выгодно экономически.
Эту концепцию при желании можно уместить в один урок, это если говорить в терминах c будет enum с union в одном флаконе, а следит за этим компилятор. Другие решения, вроде того, что приняты в c(возврат особого значения), go(возврат двух значений) проще будут разве что на бумаге, либо в момент реализации компилятора.
Например, malloc при ошибке возвращает нулевой указатель

У меня для вас новость, и она вам не понравится.
В реальности malloc примерно никогда не возвращает nullptr, а сигнал прилетает в момент обращения к памяти. И, возможно, не вам.

То, что вместо nullptr в большинстве случаев прилетает сигнал — это ещё лучше, ведь это означает что никаких последствий кроме вылета точно не будет. То, что сигнал прилетает "возможно, не вам" — иногда хорошо, иногда плохо, но в любом случае никак на код не влияет.

Вместо nullptr прилетает не сигнал, а указатель-мина. Тронешь-взорвётся.
Ничего хорошего в швырянии сигналами ОС нет. Например, открытые на запись файлы окажутся дважды неоткрываемыми после перезапуска программы: сначала потому, что они были эксклюзивно заблокированными на запись подорвавшимся на мине процессом (и нет, terminate сам ничего не закрывает), а после перезагрузки ОС — окажутся просто битыми, потому что запись тупо не успела отработать.
Написание своего обработчика сигнала, особенно того, который всё равно закончится terminate, требует черного пояса по системному программированию и знания подкапотной магии конкретного компилятора. Потому что сигналы прилетают действительно асинхронно, например, посредине входа в какую-нибудь функцию, когда стековый фрейм содержит мусор, у вас есть неполностью сконструированные объекты, да и указатели-мины продолжают «действовать». И да, вы должны писать реентерабельный код, который может быть вызван несколько раз подряд параллельно.

Всё это нисколько не отменяет того факта, что возвращаемое значение malloc можно не проверять если нет задачи писать надёжный и переносимый код.


В отличии от возвращаемого значения fork.

Вообще-то результат fork нужно проверять, чтобы понять, оказались ли мы в потомке или в родителе: у них очевидно будет разная логика.
«Нет задачи писать надёжный код» это как? Даже стиль разработки «фигак-фигак и в продакшн» предполагает исправление багов в будущем.
Вообще-то результат fork нужно проверять, чтобы понять, оказались ли мы в потомке или в родителе: у них очевидно будет разная логика.

Ага, например вот так:


if (pid = fork()) {
    // ...
} else {
    // ...
}

Это пример из книжки по операционным системам! :-)

ну и неправильный пример кстати. Должно быть:
if (pid = fork() > 0) {
    // parent
} else if (!pid) {
    // child
} else {
    // oops
}

Ну разумеется он неправильный. Писали бы всюду правильные примеры — не было бы обсуждаемого поста.


PS Тогда уж if ((pid = fork()) > 0) { в первой строчке должно быть.

Ну тогда уж if( (int pid = fork()) > 0 ) :)
Ну вот ещё чего захотели, чтобы примеры — и компилировались!
В реальности malloc примерно никогда не возвращает nullptr

Такое постоянно происходит, когда размер виртуальной памяти процесса таки ограничен через rlimits. Если вы это не используете, то очень зря.
Вы правы в том, что надо постоянно иметь в виду, что отказ в памяти может происходить неожиданно и асинхронно (при использовании, а не запросе), но не надо таки обобщать это на 100% случаев.

/usr/share/man на моей машине содержит 5060529 строк. И это только man-страницы. А рядом стандарты, rfc, документация к языку программирования, маленький комплект документов по xml/html/css/js, что у нас нам ещё? А, у каждой библиотеке тоже пачка документации.


Читаем. Но будем реалистами, вы не прочитали и 1% документации от технологий, которыми пользуетесь. Вот, например. Всё прочитали? http://refspecs.linux-foundation.org/IA64-softdevman-vol1.pdf Закончите, там есть шкаф документации по UEFI.

Если я использую какую-то функцию, которую я до этого не использовал, я беру и читаю мануал по ней, т.к. я пишу на C и частенько даже не на уровне ОС, а в этом случае у меня слишком мало ног, за день закончатся. Ну а если не проверять возвращаемые значения, тем более на C, то пардоньте, ССЗБ в полной своей красоте.
Простите, но я никак не понимаю, каким образом возврат -1 в форке является багов, если это документированная функция. Это не креш функции, не внезапный выход по эксепшену. Это задокументированный функционал, что если форк не смог создать процесс, он возвращает -1
Речь не о возврате, а о дальнейшем некорректном использовании возвращаемого значения.
Так это, получается, в kill() ошибка тогда?

Или таки «некорректная интерпретация и игнорирование ошибок — зло»?
Так это, получается, в kill() ошибка тогда?

Нет.

Ошибка в дизайне системного API. Не стоило жалеть отдельного имени для killall или там killpg. Да, это можно назвать ошибкой в kill.

Мне кажется, проблема несколько глубже. Она скорее в неверном понимании системного API. Системное API — штука низкоуровневая, экономная к ресурсам, и этим, видимо, объясняется тот факт, что оно практически полностью основывается на принципе возврата цифровых кодов результата выполнения операций. То, что возвращает fork() — это код возврата выполнения метода. Далее он может быть интерпретирован как PID процесса в случае положительного числа или как признак ошибки во всех остальных случаях. Ключевое тут — он может (и должен) быть интерпретирован.

kill() принимает на вход PID процесса, который нужно «убить» (или специальное значение -1 для того, чтобы «убить всех»). Косяк именно в понимании API. kill() хочет PID, а ему суют код возврата fork().

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

Еще раз: fork() отдает код возврата, kill() на вход просит pid или -1. Почему кто-то отдает на вход kill() код возврата, а потом жалуется, что ему прилетело граблей по лбу — непонятно.
Еще раз: fork() отдает код возврата

pid_t fork(void);

kill() принимает на вход PID процесса, который нужно «убить» (или специальное значение -1 для того, чтобы «убить всех»

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


В общем и целом, не совсем понятно, с какой целью прикладники ручками лезут в fork()/kill()

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


и жалуются при этом, что системное API концептуально отличается от привычного им прикладного

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


Ладно бы это было API системных вызовов, где надо номера вызовов экономить (кстати, а зачем?) — так ведь это API на языке высокого уровня, где нет никаких причин "лепить" несколько режимов работы на одну функцию.

pid_t fork(void);


Этим вы что показать хотели? То, что fork() возвращает значение типа pid_t? Я еще вот такую штуку могу показать:

#define __PID_T_TYPE __S32_TYPE


pid_t — это просто 32-битный знаковый int. Это не pid процесса, это целое число в диапазоне -2147483648...2147483647. Заметьте, не pid, а pid_t, т.е. тип, предназначенный для работы с pid'ами.

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


К сожалению, в системных API это не так. Уход от этих ужасных реалий имеет стоимость, что-то придется приносить в жертву. Например, в жертву ради ухода от «магических чисел» в системном API, вероятнее всего, будет принесена производительность оных API. Ну, такое себе, да и зачем.

Ну и плюс не всегда есть нормальные обёртки без подводных камней.


Над управлением процессами — есть. Миллион штук.

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


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

Любое изменение в пользу бОльшего удобства, вероятнее всего, ударит по производительности. 2% производительности в системных вызовах в легкую положат производительность вашего прикладного решения в разы.

Ладно бы это было API системных вызовов, где надо номера вызовов экономить (кстати, а зачем?)


Ну, например, чтобы оно на микроволновке работало…

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


Это системное API.
Любое изменение в пользу бОльшего удобства, вероятнее всего, ударит по производительности. 2% производительности в системных вызовах в легкую положат производительность вашего прикладного решения в разы.

Не верю. Скорее поверю, что изменение этого API никак не повлияет на производительность. Всё-таки что fork, что kill кучу раз в секунду не вызываются.


Это системное API.

А дальше? Почему в системном API нужно лепить на одну функцию несколько режимов работы?

Не верю. Скорее поверю, что изменение этого API никак не повлияет на производительность.


Зря не веришь. Собственно, есть `pid_t fork(void)`, например. А также тысячи других методов, все как один возвращающие тот или иной вариант int'а. На что будем менять, при условии, что апи все еще на C и поломать юзерспейс нельзя?

Вы же согласны, надеюсь, что АПИ в целом должен быть как можно более однообразен? Т.е. менять таки надо все методы АПИ? В связи с этим предлагайте, какой тип в пределах C можно использовать для более явного определения фейл/не фейл? В структуру паковать? Boxing/Unboxing небесплатный. Возвращать указатель на int? Вы уверены, что идея унести все это дело в кучу — хорошая, и никак не скажется на производительности? Из более-менее быстрого — разве что битовые маски возвращать? Но вы уверены, что битовые маски будут более очевидны?

В общем и целом альтернативы-то особо не видно? Или у вас таки есть решение?

Почему в системном API нужно лепить на одну функцию несколько режимов работы?


Я вот не уверен, что pid > 0 и pid = -1 — это разные режимы работы kill. Надо, очевидно, в код смотреть.
В общем и целом альтернативы-то особо не видно?


Смотрим на Windows API (CreateProcessEx vs fork, например), возвращаемся кушать кактус POSIX — там всё просто и очевидно ;)
Погодите, но Windows API, разве, не плюсовое?

С фига ли оно плюсовое?

В нутре — самое обычное C, структуры по 20 полей и zero terminated strings.
Процессы в Windows по дефолту гораздо тяжелее, чем в Линукс. Количество аттрибутов и диспетчер очереди также сложнее.
С одной стороны в винде больше информации и возможностей управления, с другой стороны запустить и остановить 1000 процессов в Линуксе — в разы быстрее.
НЛО прилетело и опубликовало эту надпись здесь
разве что битовые маски возвращать?

Причем, оно уже возвращается как битовая маска, совмещенная с реальным значением: signed же.

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

Очевидно, что проблему в дизайне системного вызова kill надо решать меняя kill, а не fork. Как конкретно — вам уже написали ниже.


Я вот не уверен, что pid > 0 и pid = -1 — это разные режимы работы kill. Надо, очевидно, в код смотреть.

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


Но если вам интересен код — то вот он: https://github.com/torvalds/linux/blob/df561f6688fef775baa341a0f5d960becd248b11/kernel/signal.c#L1560-L1561

А вы точно уверены, что прямо всенепременно надо? Какбэ, kill() подразумевает, что мы как-то там процессами порулить пытаемся, можно и доку разок прочитать, она небольшая.

А вот что будет при изменении сигнатуры kill() для прикладного уровня — это сложнопредсказуемая вещь. Едиснтвенное, что можно сейчас прямо сделать — запилить kill_single(pid, signal) и kill_all(signal) в виде оберток над kill. Только кто ими будет пользоваться?

Если бы kill не было изначально — то kill_single использовали бы все.


Если начать менять API сейчас — то пользоваться kill_single будут те, кто пишет программу для гипотетического нового стандарта POSIX и умеет читать предупреждения компилятора об устаревших функциях.

Ну вот, опять мы мануалы не читаем.

The kill() system call can be used to send any signal to any process group or process.


Переведу: kill() — системный вызов, который может быть использован для отправки сигнала любой группе процессов или прицессу.

И вот этот системный вызов kill() делает именно то, что декларирует. Положительный pid — посылает конкретному процессу. 0 — каждому процессу, принадлежащему группе вызывающего процесса. -1 — каждому процессу, которому вызывающий процесс имеет право слать сигналы. Любое отрицательное число — айдишник группы, которой слать.

kill() — это не «убивалка процесса». Это инструмент отправки сигналов. И этот самый инструмент делает именно то, что декларирует, депрекейтить его никто не будет.

Поэтому kill_single — ересь, а не сигнатура фукнции. kill_pid, kill_group, kill_current, kill_owned — это набор методов, на который вы предлагаете распилить один единственный kill. При этом внутри они все будут делать примерно одно и то же. Ну и зачем, спрашивается? Просто заради «порадовать внутреннего перфекциониста» распилить один отлично работающий метод на 4 дублирующих функционал?
И вот этот системный вызов kill() делает именно то, что декларирует. Положительный pid — посылает конкретному процессу. 0 — каждому процессу, принадлежащему группе вызывающего процесса. -1 — каждому процессу, которому вызывающий процесс имеет право слать сигналы. Любое отрицательное число — айдишник группы, которой слать.

С этим я не спорю, но вот зачем под одним именем объединять 3 разные вещи — совершенно не понятно.


Я бы с вами согласился если бы айдишники групп процессов были отрицательными числами. Но это положительные числа, которым надо сменить знак перед передачей в kill. А потом kill в отдельной ветке сменит знак обратно, чтобы получить айдишник группы. Ну и нахера эти сложности на пустом месте?


Поэтому kill_single — ересь, а не сигнатура фукнции. kill_pid, kill_group, kill_current, kill_owned — это набор методов, на который вы предлагаете распилить один единственный kill. При этом внутри они все будут делать примерно одно и то же. Ну и зачем, спрашивается?

Во-первых, для упрощения ментальной модели.


Во-вторых, для снижения вероятности ошибки за счёт защитного программирования.


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

С этим я не спорю, но вот зачем под одним именем объединять 3 разные вещи — совершенно не понятно.


4 же!)))

А с точки зрения реализации, видимо, тупо 2.

Положительные pid — конкретный pid процесса. Неположительные — умножаем на -1 и считаем gid'ом. 0, видимо, броадкаст в пределах текущей группы, 1 — традиционно init, который отсекает то, на что прав нет, остальные значения — конкретные айдишники групп.

Т.е. на уровне glibc метода 2, сценария 4. Видимо, пока срались, каждый на белом коне, как пилить, решили оставить 1.

Во-первых, для упрощения ментальной модели.


Ну, тут ментальная модель разойдется с реальной. И это таки +3 метода, которые, при всей их примитивности, таки придется поддерживать.

Во-вторых, для снижения вероятности ошибки за счёт защитного программирования.


Тут у нас случай был — человек не проверил возвращаемое значение от fork и как есть в kill сунул. Ну, мне кажется, защитное программирование тут не поможет…

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


Самое частое использование, видимо, таки kill с pid > 0. Да и не так часто оно нужно. А на фоне вороха поддерживаемых архитектур перегружать таки придется, вроде как. Ну, видимо, экономия на спичках будет.

В общем и целом, API не самый удобный, но не настолько катастрофический, как тут в статье пытаются описать, имхо. Поэтому и «исправлять катастрофическую ситуацию» никто не будет, даже если найдется энтузиаст, готовый за это взяться. Некоторое количество крупных контор могут не понять, заради чего им переписывать код, работающий поверх стопроцентно валидного и на тыщу раз известного метода, в угоду внутреннему перфекционизму…
Тут у нас случай был — человек не проверил возвращаемое значение от fork и как есть в kill сунул. Ну, мне кажется, защитное программирование тут не поможет…

Вообще-то именно от таких тупых ошибок оно и поможет.

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

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

А что, механизма устаревания апи там нет? В чём проблема пометить функцию устаревшей и когда-нибудь через 20 лет её удалить?
Механика устаревания есть. Вопрос в том, зачем депрекейтить один хорошо работающий метод в пользу 4 других, работающих не так хорошо и дублирующих друг друга?

Откуда 4? И почему они будут дублировать, если код в разных ветках разный? И почему они будут работать не так хорошо? Сплошные вопросы.

Откуда 4?


The kill() system call can be used to send any signal to any process group or process.
If pid is positive, then signal sig is sent to the process with the ID specified by pid.
If pid equals 0, then sig is sent to every process in the process group of the calling process.
If pid equals -1, then sig is sent to every process for which the calling process has permission to send signals, except for process 1 (init), but see below.
If pid is less than -1, then sig is sent to every process in the process group whose ID is -pid.


Сценария таки 4: конкретный процесс, группа вызывающего процесса, все процессы, на который у вызывающего хватает прав, конкретная группа процессов.

И почему они будут дублировать, если код в разных ветках разный?


Ну, собственно, глубоко не копал, но ветки таки в коде всего 2. Отдельно конкретный pid убить и вторая if (pid <= 0). code.woboq.org/userspace/glibc/signal/kill.c.html#34

Т.е. у вас будет 4 метода на 2 ветки кода. Т.е. либо оборачиваем, либо копи-пастим.

И почему они будут работать не так хорошо?


Потому что kill уже сейчас уже работает, а ваши 4 еще не написаны?

Зачем депрекейтить уже существующий абсолютно валидный метод? Зачем пилить этот метод на 4 других, реализация которых либо будет тупо оберткой над тем, который мы попилили, либо будет дублировать себя? В каких сценариях некорректная обработка ошибок и передача чего ни попадя в системные API становится проблемой именно системного API? Почему не написать враппер для особенно страждущих? Кто этим займется? Сколько это будет в итоге стоить? Сплошные вопросы…
В структуру паковать? Boxing/Unboxing небесплатный.

Вообще-то тут две проблемы:


  1. Что возвращаемое значение функции только одно (а эмуляция в виде структуры в случае сложнее чем complex<> будет требовать скрытого указателя). Посмотреть любое классическое соглашение о вызове — там 4-6-10 параметров функции, но только одно возвращаемое значение. Во всяких Go это переламывают, но медленно.

А если бы возвращаемых значений было несколько — вот этот костыль с -1 плюс errno был бы тупо не нужен, подобная системная функция возвращала бы уже пару значений — типа <pid,error>, <fd,error> и так далее.


Более того, такие функции изначально есть! pipe() костылирует три значения через явно переданный массив для дескрипторов.


  1. Запись в errno может быть дорогой. Во-первых, ядро его напрямую не отделяет: для Linux, например, значения -4096...-1 транслируются в код ошибки, а для FreeBSD/x86, например, флаг CF означает, что значение — код ошибки, а не результат. А потом его ещё и надо записать в память, причём сейчас практически всегда разрезолвить ещё thread-local адрес конкретной errno. Да, на двух регистрах было бы всяко дешевле — и понятнее нормальному компилятору, что тут делать.

Так что тут косвенный unboxing на двух регистрах давно уже дешевле. И ответ: да, производительность только улучшилась бы.


Я вот не уверен, что pid > 0 и pid = -1 — это разные режимы работы kill. Надо, очевидно, в код смотреть.

Очевидно разные. Там даже три режима:
pid>0 — конкретный процесс
pid==-1 — все кого можем
pid<-1 — -pid задаёт pgid

А если бы возвращаемых значений было несколько — вот этот костыль с -1 плюс errno был бы тупо не нужен, подобная системная функция возвращала бы уже пару значений — типа <pid,error>, <fd,error> и так далее.


Это да, но ядро же на glibc на C же пишут. Поэтому имеем то, что имеем, и без коренных изменений языка ничего не поменяется.

Так что тут косвенный unboxing на двух регистрах давно уже дешевле.


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

Очевидно разные. Там даже три режима:
pid>0 — конкретный процесс
pid==-1 — все кого можем
pid<-1 — -pid задаёт pgid


Их либо 2 (с точки зрения имплементации), либо 4 (с точки зрения ментальной модели).
4 такие:
> 0 — конкретный процесс по PID
0 — все в группе процесса
-1 — все, на какие права есть
< -1 — все в указанной группе
При этом они мапятся тупо в 2:
>1 — по PID
<1 — по GID, просто 0 — wildcard для текущего, 1 — gid init'а, остальное — другие группы.
Тут смысл в том, что в текущей ситуации при корректной успешной работе функции анбоксинга нет

Он есть. Реализации линуксовых сисколлов в glibc по сути работают так:


ssize_t ret = syscall(...);
if (ret < 0 && ret >= -4096) {
  *errno_ptr() = -ret;
  return -1;
} else {
  return ret;
}

Вот эта проверка с возможной заменой и есть анбоксинг, только манера специфическая.


Их либо 2 (с точки зрения имплементации), либо 4 (с точки зрения ментальной модели).

Ну да, про 0 я пропустил. Но с точки зрения реализации оно таки мапится на:


  1. Конкретный pid — делается лукап по нему (где-то есть мапа всех процессов в данном пространстве pidʼов; дерево или хэш-таблица — зависит от свойства местности).
  2. -1 — Пробегается явным образом список всех процессов (может быть та же мапа).
  3. 0 или <-1 — находится группа процессов и итерирование по ней, если она явно как-то выделена (например, подсписок); может быть опять же по всем видимым процессам с их фильтрацией.

Совмещение случаев 2 и 3 возможно, а вот 1 наверняка нет, потому что быстрый поиск (быстрее линейного от количества процессов) по pid — обязателен для нормальной работы системы, и если он есть, то незачем и тут делать перебор всех процессов.


При этом они мапятся тупо в 2:

Угу.


Но это не причина объединять интерфейсы. Внутри много вещей реализовано одинаково (например, read фактически враппер вокруг readv), но зачем такие опасные комбинации?


но ядро же на glibc на C же пишут. Поэтому имеем то, что имеем, и без коренных изменений языка ничего не поменяется.

Увы. Ну лет через 20 и это сдвинется.

pid_t fork(void);


а вы можете найти ту древнюю запись этой функции когда она именно создавалась, а не современное объявление?

Тут косяк опять же не в функции, а в том, кто её в современном виде обозвал pid_t. Попробую дома поискать книгу по Linux старенькую, не помню что там был pid_t
Функцию же никто не обзывал. pid_t — это просто тип возвращаемого значения.
раньше он было просто int и считался именно возвращаемым значением
также как и read возвращает -1 и -1 как возвращаемое значение почти всегда говорит об ошибке. А вот тому кто его обозвал pid_t нужно сказать «спасибо»
Ну, очевидно, это произошло где-то в момент переползания на 64-битную архитектуру. Тупо тайпдефом определили для кроссплатформенности и возможности разом поменять. Название, возможно, не самое удачное, но тут уж так мир устроен.

Просто тот, кто потом доку читает, почему-то уверен, что pid_t — это значение, возвращаемое fork'ом. Классическая невнимательность. А это — тупо тип.
Ну, очевидно, это произошло где-то в момент переползания на 64-битную архитектуру.

"Очевидность" вас подвела. pid_t родился, когда 32767 процессов стало недостаточно, и перевели с 16- на 32-битные значения. Было это ещё в самом начале 1990-х, синхронно как минимум у BSD и Linux. У SysV, возможно, даже раньше.


Просто тот, кто потом доку читает, почему-то уверен, что pid_t — это значение, возвращаемое fork'ом. Классическая невнимательность. А это — тупо тип.

Я не знаю, кто этот "тот" — я никогда так не читал и не слышал такого заблуждения ни от кого из коллег.

не знаю, кто этот «тот» — я никогда так не читал и не слышал такого заблуждения ни от кого из коллег.


Прочтите чуть выше ветвь, там как раз всплывает заблуждение на тему «ну вот смотрите на сигнатуру, pid_t fork() — это он pid же возвращает»
На самом деле, видимо, косяк тут в сигнатуре kill.

int kill(pid_t pid, int sig);


pid_t — фиг с ним. А вот то, что аргумент обозван pid (что явно трактуется как process id) — это уже не совсем корректно, т.к. там и gid*(-1) может встретиться, и пара спец-значений. Переименовать в receiver какой-нибудь, и уже лучше будет, наверное.
Если бы в древности не занимались экономией на спичках…
int kill(pid_t pid, int sig, int kill_flags)

или
int killall(int sig)

Чем плохо?
Вся история начинается таки с pid_t fork(void). И с искренней уверенности, что pid_t — это всенепременно pid процесса.

Весь сыр-бор оттого, что вместо pid'а процесса в kill() отдают код возврата fork'а. При любом валидном pid имеющийся kill делает ровно то, что от него ожидают, поэтому переписывание kill — ну, такое себе извращение, да и вам никто не мешает тупо напилить обертку над kill вида kill_one + kill_all, которые заставят вашЧудесныйКилл вести себя именно так, как вы ожидаете.

В целом, описанное поведения kill к фатальным последствиям не приводит, ФС не крашит, блок питания не сжигает, фондовые биржи не обрушивает. Без рутовых прав оно просто убьет ваш текущий сеанс, причем в зависимости от сигнала может еще и аккуратно завершить. Ну, такая себе паника. Те несуразности, которые могут к серьезным последствиям привести — их правят, а непонимание того, что kill — достаточно серьезный инструмент, могущий в некоторые последствия… Ну, кто тут кому злобный буратино?
да и вам никто не мешает тупо напилить обертку над kill


Мне никто не мешает и код возврата от fork проверить ;) Раз уж в POSIX так принято — приходится использовать существующее API, с его magick numbers.
Мне никто не мешает и код возврата от fork проверить ;)


Ну вот, главное же — понимать, что fork код возврата отдает. И понимать, что такое собственно форк. Сфейлиться ведь может что угодно, поэтому первое, что принято делать (правило вежливости, что ли) — выяснять, как определить, сфейлилось/нет.

Идея «пусть софтина работает дальше, пусть оно как-то само поломается» — ну, такое себе… И это нормальное отношение не только при работе с системными апи, но и с прикладными. Я прикладник, поэтому не проверяю ошибки — говно позиция.

А magic numbers — оно историческое же, да и не так оно плохо, как его малюют. Бывает и хуже, а «более лучших системных апи» я не особо наблюдаю, справедливости ради.

Более лучших системных API вы в языке Си не наблюдаете из соображений обратной совместимости. Это не означает, что если бы изначально предложили другое API — оно бы обязательно оказалось хуже.


А вот в других языках они есть, тут в комментариях уже приводили варианты fork на Питоне и на Rust.

тут в комментариях уже приводили варианты fork на Питоне и на Rust


И то, и другое — обертки над C-шным методом. Если нужен «более другой АПИ» — всегда есть обертки, пользуйтесь ими. А «переписать системное АПИ на python» — в голос, что называется…

Более лучших системных API вы в языке Си не наблюдаете из соображений обратной совместимости. Это не означает, что если бы изначально предложили другое API — оно бы обязательно оказалось хуже.


Я их несколько видел. Существенно лучше — нет. Вся «прелесть ситуации» в том, что C-API — это штука, которую можно заюзать откуда угодно, все остальные решения будут местечковыми обертками, нужными 3,5 пользователей.
И то, и другое — обертки над C-шным методом.

А вот Go или freepascal (если верно вспомнил, как этот паскаль зовётся) делают собственные врапперы сисколлов. И могут при этом применять другие варианты, чем glibc (например, последняя всегда мапит open() в openat(), что не обязательно повторяется).


C в этом плане хорош только тем, что синхронность реализации в glibc контролируется командой ядра.


Я их несколько видел. Существенно лучше — нет.

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

Баг в том, что это значение не обработано.

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

А, ну да. И все-все-все срочно переписать на Rust…

Было бы, кстати, неплохо так и сделать. По крайней мере, код на Си без плюсов — в первую же очередь. Жаль только, что эта мечта нереализуема...

Ну, идея переписать ядро на Rust — утопия
Мне кажется что народ не понимает что такое C и код ядра
Если коротко, то переопределив getC и putС в C на своей платформе, то можно будет компилить практически любой исходный код написанный на C.
Мне кажется что народ не понимает что такое C и код ядра


Чисто для самопроверки: мне кажется, что C — это человекочитаемый относительно-платформонезависимый ассемблер. Я прав/нет?

А Rust, в моем далеком от расто-движухи понимании, какая-то достаточно странная (не отрицаю, возможно, «опередившая время», «прорывная» и прочее) мало кому нужная штука с неочевидными преимуществами, неустоявшимся апи и непонятной сферой применения. Больше всего меня смущает такая штука забавная, практически уже закон интернета: в любом обсуждении нюансов любого языка всенепременно возникает человек, демонстрирущий якобы-очевидные сферические преимущества Rust'а над обсуждаемым языком, всеми прочими языками, причем во всех проявлениях. Печально в этом то, что реальных проектов, демонстрирующих «успехи» не прилагается…
Печально в этом то, что реальных проектов, демонстрирующих «успехи» не прилагается

Вы это сейчас серьёзно?

В основном переписывание инфраструктурных боттлнек-сервисов. Флагманского продукта не наблюдается. Не подумайте, я не злорадствую, исключительно успехов желаю, даже rust-by-example уже до 3 страницы дочитал.
Я прав/нет?

ага

А Rust, в моем далеком от расто-движухи понимании, какая-то достаточно странная мало кому нужная штука

а это уже C++ без возможности выстрелить себе в колено с «современным» синтаксисом.

непонятной сферой применения

Сфера понятная, те кто пишет на kotlin/swift подобных языках сможет делать байткод нормальный без вникания в сами байтики и память.
Сфера понятная, те кто пишет на kotlin/swift подобных языках сможет делать байткод нормальный без вникания в сами байтики и память.


Может быть, в том и проблема? В смысле, в «революционном подходе»? Зачем Kotlin тот же нужен — примерно понятно. Это просто чуть-чуть другая Java. И относительный взлет Котлина связан с тем, что в бюджетных андроидах java древняя, а kotlin под древнюю JVM позволяет писать на чуть-чуть другой современной Java. Дальше, конечно, ребят понесло… И нативный, и вебный и все-все-все. Но площадка для старта понятна, и трудозатраты на миграцию не слишком высокие.

А Rust — революционный все-по-новому язык, где еще все шишки собрать не успели, а выгоды неочевидны. Причем его несут-то в массы с помпой «идеальная замена языку X (вставьте любой вообще язык)». При этом, в моём понимании, например, тому же C он замена очень и очень слабая (по крайней мере в тех областях, где прямо C и прямо оправдан). Замена Go? В областях, где действительно обоснованно применяется Go (круды крудить да ресты рестить), Rust — из пушки по воробьям без видимых причин. Доходит до того, что Wordpress на Rust переписать предлагают…

А из реально мне, как человеку со стороны, видимых ниш — ну, Rust — он C++ конкурент в определенных сферах применения.
При этом, в моём понимании, например, тому же C он замена очень и очень слабая (по крайней мере в тех областях, где прямо C и прямо оправдан).

Можете раскрыть мысль? Мне на ум приходят только платформы, которые rustc пока не поддерживает.

Это просто чуть-чуть другая Java

ну вот нет
А Scala тогда что по вашему? :)
Rust — революционный все-по-новому язык

И эта «революция» похожа на котлин/свифт
А Rust, в моем далеком от расто-движухи понимании, какая-то [...] мало кому нужная штука с неочевидными преимуществами, неустоявшимся апи и непонятной сферой применения.

Простите, а что считается "устоявшимся API"?

больше всего меня смущает такая штука забавная, практически уже закон интернета: в любом обсуждении нюансов любого языка всенепременно возникает человек, демонстрирущий якобы-очевидные сферические преимущества Rust'а над обсуждаемым языком, всеми прочими языками, причем во всех проявлениях.
Вы сейчас описали парадокс Блаба
Не проверять ошибки плохо, пнятненько? ;)

Да, понятненько. Читать за пределами выделенной памяти — плохо. Конкурентно менять структуру неатомарным образом — плохо. Не валидировать входые данные — плохо.


… Почему же это "плохо" происходит снова и снова? Наверное, потому что люди упорно отказываются писать софт без багов, хотя им стопятьсот раз говорили, что баги — это плохо.


А вы пишите софт без багов?

Почему же это «плохо» происходит снова и снова?


Потому, что сделать хорошо сложнее, чем как попало. А люди они ленивые…

А вы пишите софт без багов?


Кто без греха, пусть первым бросит камень? ;) Каюсь, зело прегрешен бываю. Но не упорствую в своих прегрешениях, епитимью, тестировщиками наложенную, исполняю со скрежетом зубовным, но без роптаний :D

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

Из теоретической невозможности делать предсказания о поведении произвольно взятого кода разве следует, что в любой программе не нужно делать никаких проверок?

Нет, конечно. Но глядя на код вы не можете сказать, сделаны там все проверки или нет. В ряде языков пошли на экстремальные меры для того, чтобы обеспечить exhaustive pattern matching, и вот эту конкретную ошибку условный Rust бы просто не пропустил; но глядя на код вы не можете сказать, все ли случаи покрыты.


Это называется баги (и они бывают, в т.ч. и по невнимательности). Например, я бы на -1 проверил, но вот что -1 у kill имеет специальный семантический смысл, я без этой статьи никогда бы не узнал.

я бы на -1 проверил, но вот что -1 у kill имеет специальный семантический смысл, я без этой статьи никогда бы не узнал


Потому что при выполненной проверке на -1 != fork() статьи не случилось бы ;)

Нельзя так проверять, иначе 0 и >0 в одной ветке окажутся. А второй раз вызвать форк — 4 процесса вместо двух. Это баг (ц)!

Обычно баг в программе проявляется локально — то есть программа падает, максимум портит данные, с которыми работала. Есть огромное количество случаев, когда потеря данных/необъяснимое падение приложения в некоторых ситуациях не имеет серьёзных последствий. В таких случаях программист, поджимаемый ленью или сроками может проигнорировать часть ошибок и документации.

А данное API подкладывает грабли, делающие сбой в программе совершенно нелокальным и влияющим на абсолютно несвязанные с программой процессы.
Причём тут баги? В статье человек ответственность переносит на других.
Форк может глюкануть. Так же, как и malloc

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

Большинство багов — это человек "не подумал".


В статье человек описывает последствия "не подумал" в этом месте — у него не просто "не работает", но "не работает с катастрофическими последствиями".


(это очень интересная черта русскоязычного интернета — тут не прощают ошибок, и любой рассказ человека про свои ошибки встречают комментариями на тему "какой он идиот"/"некачественный программист" и т.д.).


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

Простите за баян...


Американский форум- задал вопрос, тебе на него обстоятельно и вежливо ответят.
Еврейский форум- задал вопрос, тебе зададут встречный вопрос.
Русский форум-задал вопрос, тебе ещё 2 часа будут объяснять какой ты мудак!
Это переводчик мгимо финишд, в оригинале fork() can failed, что не означает что это что-то ненормальное, и никакого глюканула тут нет.
Ну например вы можете написать fdisk, который при создании раздела не проверяет доступное место и может затереть существующие, и выпустить его в обновлении релиза дистрибутива, где его будут запускать от рута на продакшене.

И конечно виноват в этом будет видимо кто? биос?
Ну вот в OpenBSD такой fdisk(во всяком случае, был в последний раз, когда видел). Какие вы ему границы раздела вводите — с такими и создает. И виноват, кхм, тот, кто вводит.

Будет баг. Кому-то надо будет пойти и написать тест на этот случай, а потом пофиксить этот баг.


Я не понимаю вашей мысли. Моя мысль: баги — плохо; баги — неизбежно. Учиться на чужом опыте — хорошо.

да мало ли причин.

Например, у вас Mac OS, не установлена environment variable OBJC_DISABLE_INITIALIZE_FORK_SAFETY и немного не повезло.

ffff… Спасибо, перечитал man kill. Буду знать.

Не читаем доку, отстреливаем себе ногу и пишем сразу на хабр — это новый тренд хабра?

Вот я с линуксами 15 лет, а про "интересное" поведение kill с отрицательными значениями узнал только сегодня. Буду знать. Автору спасибо.


А вы, видимо, читали все доки к тому, чем пользуетесь, да?

Кода я дергал форк в Python, то да — прочитал сначала как он работает, потом написал и обработка pid = -1 у меня была сделана сразу.

Э-э-э, но ведь как раз на Питоне вызов os.fork не может вернуть -1! Зачем вы этот случай обрабатываете?

Там оно через ексепшен «приходит».
Но сути не меняет как получить ошибку главное не идти дальше если форк не удался.
И уж точно не вызывать kill после провала форка.
А «Examples» в доках тоже обязательно читать или достаточно основной части man?

Даже касательно того же kill, например, в man kill FreeBSD поведение PGID затронуто исключительно в части примеров, без описания оного в параметрах программы.

Ничего не путаете? По вашей ссылке


The following PIDs  have special meanings:

     -1      If superuser, broadcast the signal to all processes; otherwise
         broadcast to all processes belonging to the user.

находится в разделе Description не самого длинного на свете мана.

Он не на -1 жаловался, а на PGID же.

Именно так, "-1" в основном разделе, а остальные «отрицательные случаи» только в примерах
Terminate the process group with PGID 117:

kill — -117

А, да. Но прикол в том, что все побежали проверять man kill, хотя в статье прямым текстом описывается поведение системных вызовов fork() и kill(). И прямым текстом в статье посылают прочитать man 2 fork и man 2 kill

Пишу программы под Unix с 1994 года (в том числе демоны). Не знал. Спасибо!

Когда-то давно я запускал "ловилку паролей", которая после попытки поймать пароль делала kill -9 -1, фактически выгружая мой сеанс. Потом за неё атата получил, само собой, но опыт остался.

что за ловилка паролей? Первый раз такое слышу.

Я так понял, визуально похожая на экран логина утилита, запущенная от юзера. Когда приходит следующий юзер, то вбивает свои данные, его пароль "ловится" и кладётся в файл, а дальше происходит вышеописанный разлогин уже в настоящий экран входа.

Меня сейчас, скорее всего запинают, ну да ладно.
Я не проверяю результат fork, clone и malloc на возможность ошибки. Потому что я считаю, что это не моя проблема.
Я не пишу код ядра, не пишу драйвера, я делаю вещи попроще и повыше. И я считаю, что если такие базовые вещи сломались и вернули что-то не то, то у моего приложения всё равно нет шансов выжить. Если malloc или fork вернули ошибку, то значит всё плохо на уровне системы. Как мне это обработать? Что мне делать? Упасть? Ну, приложение и так упадет, если попытаюсь что-то сделать не проверив ошибку.


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


У вас есть шанс меня переубедить, но пока что моя вера в причины, по которым я этого не проверяю, тверда.

Проблема-то в том, что без проверки на -1 программа сама собой не упадёт. Я согласен, что для большинства программ "упасть" — лучший вариант, но в случае fork чтобы правильно упасть — нужно добавить проверку.

Справедливости ради, когда я делаю fork, я чаще всего буду общаться с форком через пайпы или файлы. В первом случае я словлю либо EOF, либо SIGPIPE. Во втором, либо файл не будет существовать, либо он создастся и дочерний процесс закончит работу.


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


А делать kill для дочернего процесса… Ну как-то такое себе. Я всё таки жду, что он отработает правильно :)

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

А потом вы в какой-то момент ловите несколько "сирот", активно потребляющих ресурсы при упавшем родителе, и решаете на всякий случай посылать им SIGTERM при падении. После чего оказывается, что SIGPIPE больше не спасает вас от описанной в посте проблемы, а порождает её...

Статья как раз о том, что код ошибки одной функции может оказаться валидным (но неожиданным для программиста) параметром для другой функции. Ошибки можно и не обрабатывать, ага. Но результат работы программы может не понравиться. ;)
Что мне делать? Упасть?
Можно рухнуть камнем вниз, а можно аккуратно, ничего не сломав лечь.
Ага, рухнуть кому-нибудь на голову.
Если malloc или fork вернули ошибку, то значит всё плохо на уровне системы.


Malloc в некоторых реализациях libc может вернуть ошибку в случае, если поломан механизм переаллоцирования. Например, если был вызван free() на уже удалённый адрес. Т.е. плохо не в системе, а в софте.
Ну так делай проверку и abort. Это же абсолютно несложно.
Ну там может файлы нужно закрыть, fsync дернуть, например, перед смертью или чего в лог напечатать?
fork() может вернуть -1, если pid-ы кончились, как вариант.
Зависит, конечно, от критичности приложения и того, какие гарантии оно дает. Если это, БД, например и в этот момент идет запись данных/индекса, хорошо ли падать и оставлять это в неконсистентном виде, так, что оно потом не поднимется совсем без ручного вмешательства?
Я не пишу код ядра, не пишу драйвера, я делаю вещи попроще и повыше.

"Зочем писать грамотный код? Я выше этого!"

Исправил!


if (fork() == 0) {
  ...
} else if (fork() == -1) {
  ...
} else {
  ...
}

Спойлер

Шутка

Бомба!
В блоке else нужно не забыть добавить
pid = fork();
После чтения комментов я понял, что читать документацию не нужно, т.к. все равно всю документацию не прочитаешь, и код возврата проверять не нужно, т.к. все равно все пишут с багами.
НЛО прилетело и опубликовало эту надпись здесь
зря что ли тестеры свой хлеб едят? :)
НЛО прилетело и опубликовало эту надпись здесь

Вот именно!

Эх, да. Так и просится завернуть в энум (хотя бы из раста, не говоря уже о) и использование без проверки становится невозможно.

Какова подача сего факта в статье: может глюкануть — прям поражает. Я не понимаю, за что так заплюсовали статью? За описанное поведение?


На счёт документации — её не обязательно читать всю на свете. Нужно читать и изучать как минимум то, что используешь здесь и сейчас.

НЛО прилетело и опубликовало эту надпись здесь

Я часто читаю код ядра. Это очень, очень тяжело. Как-то я пытался понять, с чем едят SIG_DFL. (Это такой интересный псевдосигнал, который простые смертные в userspace не видят). Но за всю свою жизнь я пристально прочитал, ну дай бог, тысяч пять строк ядра.


А их там… Я понимаю, что раньше были любители "читать код ядра". Сейчас скорость MR в ядро такая, что никто (даже Линус!) не может прочитать всё.


Но, возможно, ваши легендарные читатели на это способны, да.

Вот как раз в подобных книжках я и видел примеры кода, который не проверял возвращаемое значение на -1.

ye хз, для fork всегда был код вида
int pid = fork();
if(pid ==0){
// current
} else if(pid == -1)
// error
else {
// created
}
Давно интересовало — а почему fork? Откуда он взялся исторически, и почему такая странная (ИМХ) форма?
Кажется, гораздо более естественный способ реализации многопоточности — с помощью функции создания потока, которой в качестве аргумента передается указатель на функцию потока. pthread_create или что-то подобное.

Когда вы создаёте процесс, вам надо настроить ему 100500 параметров. Stdin, stdout, лимиты, окружение, стек, рабочий каталог, etc, etc. Ещё нужно предусмотреть как ему передать произвольное число fd (включая сетевые сокеты).


Этих параметров очень много. Либо вы делаете развесистое API (как сейчас пишут новое API для создания процесса с использованием proc_fd), либо вы просто говорите "скопировать с меня". Второе явно проще. Процесс подготавливает всё, что нужно, и "форкается".

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

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

И еще память получается общая до первого изменения. Удобно для трюков с prefork серверами.

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

Не так. Сейчас обычно есть общий вызов (rfork, clone), флагами которого задаётся желаемый объём независимости потомка.
(Не является ли это ещё одним примером обсуждаемого антипаттерна?)

Например, потому, что клонирование адресного пространства намного дешевле, чем создание его с нуля.
Android со своим zygote — живой пример.
Правда, там проблемы с ASLR отсюда, но кого и когда это волновало…
Java-машину быстро запустить с нуля так вообще сложно.
А вот запустить заранее и потом fork()-ать — пожалуйста.
Вообще, конечно, немного стремно fork()-ать процесс с Java-машиной.
Неизвестно, как различные ресурсы переживают fork.

Сокеты, поидее, переживают плохо, если продолжать использовать их одновременно в двух процессах.
Еще есть различные буферы, кеши.
Если не очистить перед fork(), можно получить двойную запись.

Вобщем, надо следить, какие ресурсы уходят в fork().
Это может быть сложно, если есть много библиотек. Все не проконтролируешь.
А тут еще и GC может вмешаться и добавить неопределенного поведения, в зависимости от того, успел он сработать до fork() или не успел.

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


Есть стиль программирования, где принято ничего не делать и надеяться что оно само где-то там упадет, или выдаст exception.
НЛО прилетело и опубликовало эту надпись здесь
оптимистичное же
Есть стиль программирования, где принято ничего не делать и надеяться что оно само где-то там упадет, или выдаст exception.

Ничего не имею против такого подхода, он может быть даже вполне оправдан, если язык программирования и окружение позволяют. Можно было бы понять, если автор возмущался поведением fork() в таком языке программирования или библиотеке, декларирующей подобный стиль.
Хотел написать «Офигеть статья, нам рассказали, что надо проверять коды возврата. Дальше будет статья про то, что надо вытирать задницу.»

Но потом почитал комментарии.

А потом подумал, что давным-давно, когда вышла Ява, я очень злился на жёсткое требование ловить или декларировать exceptions, а потом понял, что это — единственный способ заставить среднего программера не забивать на обработку ошибок.

А потом подумал, что это всё равно не работает — средние программеры давно объявили Яву вселенским злом и соскочили с неё туда, где опять всё можно.

А потом подумал, и налил себе ликёру. Люблю я его.
ну когда то была создана целая группа из разработчиков для обсуждения как избавиться от NullPointerException, где серьезно обсуждали эту проблему
Ну думаю нужно просто проверять код возврата.
И не только у fork(). Это вообще в принципе хорошая идея.

А вы пробовали php?
У нас функция, например, strpos может вернуть 0, а может вернуть false. Гы

еще в php есть волшебная (не путать с волшебными функциями языка) функция empty, сколько багов в мире было создано с помощью этой функции…
Согласен.
Недавно узнал что строка «0» на самом деле empty!
"" (an empty string)
0 (0 as an integer)
0.0 (0 as a float)
«0» (0 as a string)
NULL
FALSE
array() (an empty array)

ну так в доках написано что есть эмпти :)
НЛО прилетело и опубликовало эту надпись здесь
О… до сих пор помню(больше 15 лет уже прошло) лабу в универе с гадким преподавателем не захотевшим отпустить пораньше всех, кто всё сделал. Делать было нечего и сделал так:
for(;;)
{
    fork();
}
А поскольку всё выполнялось на сервере, а все студенты сидели за терминалами повисло всё и у всех :D
Препод на ехидные заявления "Вот вам ваш хвалёный надёжный Linux" начал оправдываться тем, что "Ну тут максимальное число процессов не задано..."
НЛО прилетело и опубликовало эту надпись здесь

Когда нет нормальной системы типов, очевидно.

Система типов вряд ли избавит от необходимости писать/читать документацию. Любая система типов.
Но выстрелить в ногу из python сложнее чем из C ;)

На питоне сложно стрелять себе в ногу со сравнимой хотя бы по порядку производительностью =)
А так… можно читать из сокета не установив таймаут, например.

Каждый выстрел в ногу из Python неизбежно в процессе выполнения превращается в выстрел в ногу из C. Ну, это из того, что я видел)

Не каждый. К примеру, возврата -1 из os.fork вы на Питоне никогда не увидите — а потому и в качестве параметра для os.kill эта -1 случайно никогда не окажется.

Я про то, что каждый раз, когда вы из питона, например, тот же форк вызываете, сам питон его звать не умеет. Он вызывает сишный форк.

В смысле, когда вы таки умудряетесь в питоне себе в ногу выстрелить, это обычно всего лишь отдача от сишного выстрела)
НЛО прилетело и опубликовало эту надпись здесь

Ну, на самом деле, учитывая смысл возвращаемых значений — вряд ли проблема прямо уж настолько актуальна. Проверять значение в любом случае надо (хотя бы на ноль/не ноль, чтоб понять ребёнок это или родитель). И если родитель сохраняет pid после форка для каких-то целей (что тоже не всегда, достаточно часто "форкнулся и забыл") — то там элементарное if (pid>0) сразу отсеет и детей, и ошибки.

В комментах разразилась куча споров о том что надо читать документацию, надо проверять ошибки. Но как будто никого не смущает что это очевидная ошибка дизайна — невалидное значение pid_t, возвращаемое из fork, является вполне себе валидным для kill:
pid_t fork(void);
int kill(pid_t pid, int sig);
И там и там pid_t. По идее fork должен возвращать не pid_t а что-то вроде pid_or_invalid_t, так чтобы невалидное значение не могло быть преобразованно к pid_t без сигнала ошибки.
То что man читать надо, это и так понятно.
Тогда уже какой-нибудь Some pid_d | None

-1 и так не является валидным pid. Проблема в том, что на входе kill может быть и специальное значение -1. Вот такое вот API.

Ну да, в этом и проблема. Magic Number, выполняющее роль кода ошибки исполняет совсем другую роль как аргумент kill().
Но! Проверка значения на валидность и обработка ошибок должна происходить раньше, при получении результата вызова fork().


А то так его и в sqrt() передать можно.

невалидное значение pid_t, возвращаемое из fork, является вполне себе валидным для kill:
pid_t fork(void);
int kill(pid_t pid, int sig);


Вот тут вы уже невнимательно читаете. Давайте попробуем разобрать по порядку:
pid_t — это тип возвращаемого значения. Не назначение, не высший сакральный смысл, это просто тип возвращаемого значения. `#define __PID_T_TYPE __S32_TYPE` — это просто 32-битный знаковый int, не более и не менее. Нигде, ни в каком месте не сказано, что это именно PID процесса, это просто тип, предназначенный для того, чтобы эти PIDы туда складировать, просто структура данных для возможности «в случае чего» сделать PID'ы 64-битными, например, или еще что-то вроде, не задевая остальные части ядра.
Еще раз повторюсь, pid_t — это просто тип возвращаемого значения, 32-битный знаковый int. Почему вы считаете, что -1 — невалидное значение для знакового инта — для меня загадка.
Дальше читаем вот такую штуку:
«On success, the PID of the child process is returned in the parent,
and 0 is returned in the child. On failure, -1 is returned in the
parent, no child process is created, and errno is set appropriately.»
Очевидно, что в момент вызова fork() возвращаемое значение — это код возврата, который можно и нужно интерпретировать.
kill() принимает на вход pid_t (знаковый 32-битный int), в котором должен быть pid процесса или специальное значение -1. Не код возврата fork'а, а pid процесса. В этом вся прелесть.

И там и там pid_t.


Перефразируем: и там и там int32. В чем проблема?

По идее fork должен возвращать не pid_t а что-то вроде pid_or_invalid_t, так чтобы невалидное значение не могло быть преобразованно к pid_t без сигнала ошибки.


Это системное API, в том и проблема. На прикладном уровне такое, безусловно, зло. На системном — это нормально.

Если pid_t — это всего лишь тип возвращаемого значения, то какого фига kill принимает его параметром?

Если int — тип возвращаемого main'ом значения, какого фига его используют где-то еще? Ну, круто, чо…

Так int нигде не определяется как значение, возвращаемое main. А pid_t вы сами определили как значение, возвращаемое fork.

А pid_t вы сами определили как значение, возвращаемое fork.


Это где я такое «определил». Я же ясно, вроде, написал: pid_t — это тип значения возвращаемого fork(). «Определил» это не я, а документация fork и даже, более того, сигнатура метода fork. Вот она прямо так и выглядит: pid_t fork(void).

А сигнатура метода main выглядит так, например: int main(int argc, char *argv[]). Ровно так же и на тех же основаниях определено.
Согласен, имхо в таких случаях лучше использовать конструкции типа
err = fork(&ret_pid);
как минимум, лишний расход памяти, имхо…
надо просто найти когда появился pid_t и кто его туда поставил
-1 для posix как возвращаемое значение почти всегда ошибка (например read). В моей памяти там жестко зашит int как возвращаемое значение, а уже после ты его обрабатываешь и принимаешь решение.
ну, в posix я натыкался на 2 варианта (честно скажу, глубоко не копал):

1. 0 — все ок, !0 — код ошибки.
2. > 0 — какой-то нужный нам дескриптор, <0 — ошибка.

Собственно, к этому привыкаешь, и ничего особенно страшного я тут не вижу.
Собственно, к этому привыкаешь


По привычке считаешь, что -1 — ошибочное значение для всех функций, что работают с pid, а тут вдруг очень не так.
Ээээ, не, тут ты меня не проведешь!)))

kill() принимает собственно pid_t pid — окей. По привычке мы считаем, что -1 — ошибочное значение, и, как хорошие мальчики, заведомо ошибочное значение в kill() не пихаем — так ведь, да? Мы же хорошие мальчики?

И вот если мы захотели по какой-то причине убить не один процесс а хренову-тучу-сколько-найдем, мы заглянем в доку kill(), просветимся, и засунем то, что ранее мы считали ошибочным значением.

Заодно придем к «просветлению» вида "возвращаемый отрицательный pid_t свидетельствует об ошибке, а входные аргументы мы контролируем сами".

Просто вот эта «проблема fork+kill» — она вылазит исключительно в тех случаях, когда мы ведем себя, как плохие мальчики — так ведь? А плохим мальчикам, не имеющим привычки проверять возвращаемые fork'ом значения и совать коды возврата вместо pid'ов туда, куда не следует, надо по рукам бить указкой, а не бросаться исправлять «трагические ошибки в проектировании» в угоду кривым рученькам. Так ведь?)))
все в этой теме забывают про модуль предсказания ветвлений и работу команд jz, jnz, je, jne процессора
Не знаю, стоит ли отнести kill именно к тем самым узким местам в API, которые стоит прям настолько оптимизировать…

Мне в связи с этим стало интересно. То, что AT_FDCWD определили как минимум в Linux и FreeBSD равным -100 (а не банальному -1), вызвано этими же соображениями?

Господи, это надо же, кошмар какой! Системные вызовы могут возвращать ошибки! Давайте отдельные статьи напишем про «закрытие файлов может возвращать ошибки, которые надо обрабатывать», «выделение памяти может возвращать ошибки, которые надо обрабатывать», «запись в stdout/stderr может возвращать ошибки, которые надо обрабатывать». Каждый из перечисленных случаев способен достаточно округлить глаза у программистов, которые с этим не сталкивались, и вызвать спектр проблем — от стабильности до безопасности.

Проблема в том, что компилятор явно не требует отличать ошибку от корректного значения возврата. А значит рано или поздно кам-то тут будет допущена ошибка.

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


Интересно, что в программе на Rust такой ошибки не возникнет:


pub fn fork() -> Result<ForkResult, Error>

use nix::unistd::{fork, ForkResult};

match fork() {
   Ok(ForkResult::Parent { child, .. }) => println!(
        "Continuing execution in parent process, new child has pid: {}",
        child,
   ),
   Ok(ForkResult::Child) => println!("I'm a new child process"),
   Err(_) => println!("Fork failed"),
}
Вы невнимательно прочитали описание fork и kill
НЛО прилетело и опубликовало эту надпись здесь
Казалось бы, при чём здесь Си?
НЛО прилетело и опубликовало эту надпись здесь
Предлагаю вам попробовать пописать на ассемблере с годик, а после на Си, ну и после этого повторить фразу про помощь человеку :)
язык Си очень сильно помогает (помогал, если быть точнее) человеку.
НЛО прилетело и опубликовало эту надпись здесь
а смысл, если Си нужен был для замены ассемблера?
То что он морально устарел тут никто не опровергает.
НЛО прилетело и опубликовало эту надпись здесь
Не совсем понял что вас стриггерило и с чем вы воюете
Посикс создавался во времена, когда не было понимания ни о паттернах, ни о нормальной типизации. Тут тупо обсуждение легаси кода. Естественно Си работает с легаси кодом. С чем вы не согласны со мной в последнем предложении?
НЛО прилетело и опубликовало эту надпись здесь
Я не согласен с обоснованием «проблема в том, что программист невнимательно прочитал описание $functionname»

Заметьте, я такого не говорил :)
Я пишу что технология старая и не меняется потому что была сделана, когда не было новых технологий, вот и докатились до легаси. А от легаси требовать то, что есть 21 веке как то глупо, это как деда заставить вальс танцевать
НЛО прилетело и опубликовало эту надпись здесь
язык Си очень сильно помогает (помогал, если быть точнее) человеку.
Язык си оправдан в той сфере, в которой он возник — в семидесятых годах. А как минимум с 2006 года уже было решение многих проблем.
Ну ну. И что даст компилятор наткнувшись на код, где есть описанное сочетание? Я вообще не понимаю, к чему этот перевод вопроса к теме «С устарел, он не нужен и т.д.». Речь о вполне конкретной вещи: если вы в коде на С используете сочетание fork() и kill(), чтобы избежать проблем, читайте внимательнее описание!
НЛО прилетело и опубликовало эту надпись здесь

Никогда такого не было и вот опять!) Причем здесь "если бы". Есть так, как есть. Когда я немного погружался в С и немного изучал *nix, меня учили: внимательно читайте man'ы. А Вы говорите: "латинский язык устарел, он мертвый, неудобный, учите и говорите на английском, он популярен и удобнее". Это если иносказательно.

НЛО прилетело и опубликовало эту надпись здесь

Абсолютно согласен. Однако используя потенциально опасный функционал все же следует быть внимательнее

Если бы все опасные места в C были подписаны… ;)
НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь
Если бы результат fork был описан как data ForkResult = ForkSuccess Pid | ForkFail ErrorMessage, а аргумент kill как data KillTarget = KillPid Pid | KillGroup Gid | KillAll


То это не был бы C. Пока что суть претензии только в том, что glibc на C написан, а в C почему-то все не так, как в <ВашЛюбимыйЯзык>
Можно, кто же вам (и нам) запретит. Я просто про то, что в дискуссии в явном виде обсуждаются тонкости реализации fork из glibc — таки C-шной библиотеки. Ну вот тут позиции может быть две:
1. Предлагать более удачную форму API в C-семантики (пока ничего внятного не предложено).
2. Предлагать переписать ВСЮ библиотеку на более другом языке. (делать это, конечно, никто не будет, ввиду явной утопичности идеи).

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

В дискуссии обсуждаются разные вопросы, которые не ограничиваются реализацией fork из glibc. Особенно странно продолжать сводить всё к реализации fork из glibc в ветке, которая началась с примера кода на Rust.

2. Предлагать переписать ВСЮ библиотеку на более другом языке. (делать это, конечно, никто не будет, ввиду явной утопичности идеи).
Для того, чтобы случаев из статьи не происходило, нужно чтобы все люди, у которых нет желания/возможности вчитываться в документацию не писали на си, а это вполне осуществимо, и это третий вариант, который можно посоветовать автору данной статьи.

Только не автору статьи. Мне кажется, именно это автор статьи и сам бы посоветовал.

Кстати, к слову об оптимизации. Я добыл ассемблерный вывод для этого кода и увидел там вот это:


Листинг
_ZN2rs4main17h3697ea83859154ecE:
    .cfi_startproc
    push    rbx
    .cfi_def_cfa_offset 16
    sub rsp, 80
    .cfi_def_cfa_offset 96
    .cfi_offset rbx, -16
    call    qword ptr [rip + fork@GOTPCREL]
    mov ebx, eax
    call    qword ptr [rip + _ZN49_$LT$i32$u20$as$u20$nix..errno..ErrnoSentinel$GT$8sentinel17h16928b7dc7987017E@GOTPCREL]
    cmp eax, ebx
    jne .LBB4_5
    call    qword ptr [rip + _ZN3nix5errno43_$LT$impl$u20$nix..errno..consts..Errno$GT$4last17h2fc252ad5e6597edE@GOTPCREL]
    lea rax, [rip + .L__unnamed_2]
    jmp .LBB4_3
.LBB4_5:
    test    ebx, ebx
    je  .LBB4_2
    mov dword ptr [rsp + 12], ebx
    lea rax, [rsp + 12]
    mov qword ptr [rsp + 64], rax
    mov rax, qword ptr [rip + _ZN55_$LT$nix..unistd..Pid$u20$as$u20$core..fmt..Display$GT$3fmt17hd1c4231197a4bba9E@GOTPCREL]
    mov qword ptr [rsp + 72], rax
    lea rax, [rip + .L__unnamed_3]
    mov qword ptr [rsp + 16], rax
    mov qword ptr [rsp + 24], 2
    mov qword ptr [rsp + 32], 0
    lea rax, [rsp + 64]
    mov qword ptr [rsp + 48], rax
    mov qword ptr [rsp + 56], 1
    jmp .LBB4_4
.LBB4_2:
    lea rax, [rip + .L__unnamed_4]
.LBB4_3:
    mov qword ptr [rsp + 16], rax
    mov qword ptr [rsp + 24], 1
    mov qword ptr [rsp + 32], 0
    mov qword ptr [rsp + 48], 8
    mov qword ptr [rsp + 56], 0
.LBB4_4:
    lea rdi, [rsp + 16]
    call    qword ptr [rip + _ZN3std2io5stdio6_print17hb3b1d4f2add6c097E@GOTPCREL]
    add rsp, 80
    .cfi_def_cfa_offset 16
    pop rbx
    .cfi_def_cfa_offset 8
    ret

Если внимательно приглядеться к этому коду — то видно, что результат fork сравнивается сначала -1, а потом с 0, после чего сразу же идёт формирование строки и printf. То есть во время выполнения никаких структур ForkResult и Result нет!


Единственная проблема тут — получение константы -1 через вызов функции. Но она достаточно просто исправляется добавлением #[inline] в нужное место, тут не язык виноват.


Это к комментарию qrKot о том, что Boxing/Unboxing не бесплатный. И к другому комментарию о неочевидных преимуществах Rust.


Так вот — одним из преимуществ Rust является как раз бесплатный Boxing/Unboxing.

Эмоционально! Прям ощущается, сколько переживаний испытал автор!)
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации