Comments 256
Они выбивают этот номер, а затем используют его позже.
В камне выбивают? Кто "они"?
По-моему, это автора глюкануло. А fork() может ошибку вернуть.
Да, именно так (я смиренно полагал себя больше похожим на человека, который не будет спорить с оригиналом в статье-переводе, но имею что имею).
Впрочем, был разочарован и тем, и другим. Заголовок со словом «глючить» был кликбейтом. По нему я ожидал увидеть какое-нибудь месиво, когда «мы форкаем 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.
Происходит то, что происходит, если не читать документацию и не обрабатывать ошибки.
Это только начало. Настоящая боль начинается позже
Но ведь не форк в этом виноват?
Я понял. Надо писать софт без багов, а софт с багами писать не надо. Всё просто, да?
Читать-то много чего надо, но простое чтение документации само по себе ошибку не исправит, надо ещё и возвращаемое значение на -1 проверить же.
Многие API устроены более "прощающим" образом: значение, сигнализирующее об ошибке, непригодно к использованию и вызывает другую ошибку при попытке использования. Например, malloc
при ошибке возвращает нулевой указатель, обращение по которому в большинстве систем вызывают SIGSEGV. Это неприятно — но дальше процесса проблема не распространяется.
Информация о том, что пара fork+kill лени в обработке ошибок не прощает, важна.
Читать-то много чего надо, но простое чтение документации само по себе ошибку не исправит, надо ещё и возвращаемое значение на -1 проверить же.
Ну, справедливости ради, конкретно в этом данном случае чтение документации в процессе разработки ошибку исправит. Точнее, не даст допустить…
Неа, если программист уже настроился что пишет "наколеночную поделку" и решил на всякие редкие случаи сознательно забить — ничто не подскажет ему, что -1 является очень опасным значением для pid_t
.
Неа, если программист уже настроился что пишет «наколеночную поделку» и решил на всякие редкие случаи сознательно забить
Это как раз тот случай, когда программист доку не прочитал. Собственно, ограничить диапазон pid_t положительными числами много ума не надо. При этом в качестве возвращаемых значений практически во всех юниксовых апи используется ровно 2 модели: «0 — ок, !0-код ошибки» (которая, очевидно, неприменима для конкретно взятого кейса) и ">0 — искомое значение, <0 — код ошибки * (-1)" (которая, очевидно, наш кейс).
Никакого «срыва покровов» не происходит. Если так «низкоуровнево» не хочется — юзаем высокоуровневые обертки, где таковой проблемы нет. А если хочется «низкоуровнево» — ну, страдаем и читаем документацию.
То есть в целом, в одноразовом решении автор может забить на проверку ошибок и в редких ошибочных ситуациях программа просто не выполнит свою задачу, в худшем случае упадёт (как в случае с ошибкой в malloc). Возможно, повредит данные, с которыми работала (но программист знает с какими данными работает и учитывает риски их повреждения, принимая решение написать что-то быстро).
А fork+kill выделяется и большого количества других API, потому что невалидный результат одного является валидным результатом для другого. Да ещё и насколько валидным — рассылка сигнала всем процессам в системе. Эффект от сбоя выходит далеко за пределы программы и даже явным образом обрабатываемых ею данными. Это грабли, на которые можно наступить (в реальном мире все люди иногда ошибаются, просто кто-то это делает реже, кто-то чаще).
Большинство 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 возвращает. Что не так?
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?
Ну, справедливости ради, конкретно в этом данном случае чтение документации в процессе разработки ошибку исправит. Точнее, не даст допустить…Не даст допустить язык с алгебраическими типами данных, в котором значение перед использованием обязаны проверить, вместе с пожизненным запретом писать на языках без него некоторым людям >_<
Если взять любой низкоуровневый код в продакшне, то там будет дофига ассертов, что является хорошим стилем, но плохо выглядит в коде и в примерах использования часто ассерты опускают
Предполагаю, однако, что те самые «некоторые люди», склонные допускать ошибку из статьи, также вряд ли способны и освоить язык с алгебраическими типами данных.
А программисты-то нынешнему народному хозяйству нужны, и много. И терять таких вот «некоторых людей», которые уже чему-то обучены — это не выгодно экономически.
Куда проще IMHO этим «некоторым людям» запретить пользоваться низкоуровневыми языками, дозволяющими прямой доступ к потенциально опасным системным вызовам. Тем более, что эти «некоторые люди» обычно и так не рвутся использовать низкоуровневые языки, как слишком недружественные.
Предполагаю, однако, что те самые «некоторые люди», склонные допускать ошибку из статьи, также вряд ли способны и освоить язык с алгебраическими типами данных.АДТ это очень просто, и если захотеть, это можно воткнуть хоть в бейсик.
А программисты-то нынешнему народному хозяйству нужны, и много. И терять таких вот «некоторых людей», которые уже чему-то обучены — это не выгодно экономически.Эту концепцию при желании можно уместить в один урок, это если говорить в терминах c будет enum с union в одном флаконе, а следит за этим компилятор. Другие решения, вроде того, что приняты в c(возврат особого значения), go(возврат двух значений) проще будут разве что на бумаге, либо в момент реализации компилятора.
Например, malloc при ошибке возвращает нулевой указатель
У меня для вас новость, и она вам не понравится.
В реальности malloc примерно никогда не возвращает nullptr, а сигнал прилетает в момент обращения к памяти. И, возможно, не вам.
То, что вместо nullptr в большинстве случаев прилетает сигнал — это ещё лучше, ведь это означает что никаких последствий кроме вылета точно не будет. То, что сигнал прилетает "возможно, не вам" — иногда хорошо, иногда плохо, но в любом случае никак на код не влияет.
Ничего хорошего в швырянии сигналами ОС нет. Например, открытые на запись файлы окажутся дважды неоткрываемыми после перезапуска программы: сначала потому, что они были эксклюзивно заблокированными на запись подорвавшимся на мине процессом (и нет, terminate сам ничего не закрывает), а после перезагрузки ОС — окажутся просто битыми, потому что запись тупо не успела отработать.
Написание своего обработчика сигнала, особенно того, который всё равно закончится terminate, требует черного пояса по системному программированию и знания подкапотной магии конкретного компилятора. Потому что сигналы прилетают действительно асинхронно, например, посредине входа в какую-нибудь функцию, когда стековый фрейм содержит мусор, у вас есть неполностью сконструированные объекты, да и указатели-мины продолжают «действовать». И да, вы должны писать реентерабельный код, который может быть вызван несколько раз подряд параллельно.
Всё это нисколько не отменяет того факта, что возвращаемое значение malloc можно не проверять если нет задачи писать надёжный и переносимый код.
В отличии от возвращаемого значения fork.
«Нет задачи писать надёжный код» это как? Даже стиль разработки «фигак-фигак и в продакшн» предполагает исправление багов в будущем.
Вообще-то результат fork нужно проверять, чтобы понять, оказались ли мы в потомке или в родителе: у них очевидно будет разная логика.
Ага, например вот так:
if (pid = fork()) {
// ...
} else {
// ...
}
Это пример из книжки по операционным системам! :-)
В реальности 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.
Или таки «некорректная интерпретация и игнорирование ошибок — зло»?
Так это, получается, в kill() ошибка тогда?
Нет.
Ошибка в дизайне системного API. Не стоило жалеть отдельного имени для killall или там killpg. Да, это можно назвать ошибкой в kill.
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 — там всё просто и очевидно ;)
С фига ли оно плюсовое?
С одной стороны в винде больше информации и возможностей управления, с другой стороны запустить и остановить 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_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 сунул. Ну, мне кажется, защитное программирование тут не поможет…
Вообще-то именно от таких тупых ошибок оно и поможет.
Откуда 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 небесплатный.
Вообще-то тут две проблемы:
- Что возвращаемое значение функции только одно (а эмуляция в виде структуры в случае сложнее чем complex<> будет требовать скрытого указателя). Посмотреть любое классическое соглашение о вызове — там 4-6-10 параметров функции, но только одно возвращаемое значение. Во всяких Go это переламывают, но медленно.
А если бы возвращаемых значений было несколько — вот этот костыль с -1 плюс errno был бы тупо не нужен, подобная системная функция возвращала бы уже пару значений — типа <pid,error>, <fd,error> и так далее.
Более того, такие функции изначально есть! pipe() костылирует три значения через явно переданный массив для дескрипторов.
- Запись в 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 я пропустил. Но с точки зрения реализации оно таки мапится на:
- Конкретный pid — делается лукап по нему (где-то есть мапа всех процессов в данном пространстве pidʼов; дерево или хэш-таблица — зависит от свойства местности).
- -1 — Пробегается явным образом список всех процессов (может быть та же мапа).
- 0 или <-1 — находится группа процессов и итерирование по ней, если она явно как-то выделена (например, подсписок); может быть опять же по всем видимым процессам с их фильтрацией.
Совмещение случаев 2 и 3 возможно, а вот 1 наверняка нет, потому что быстрый поиск (быстрее линейного от количества процессов) по pid — обязателен для нормальной работы системы, и если он есть, то незачем и тут делать перебор всех процессов.
При этом они мапятся тупо в 2:
Угу.
Но это не причина объединять интерфейсы. Внутри много вещей реализовано одинаково (например, read фактически враппер вокруг readv), но зачем такие опасные комбинации?
но ядро же на glibc на C же пишут. Поэтому имеем то, что имеем, и без коренных изменений языка ничего не поменяется.
Увы. Ну лет через 20 и это сдвинется.
pid_t fork(void);
а вы можете найти ту древнюю запись этой функции когда она именно создавалась, а не современное объявление?
Тут косяк опять же не в функции, а в том, кто её в современном виде обозвал pid_t. Попробую дома поискать книгу по Linux старенькую, не помню что там был pid_t
также как и read возвращает -1 и -1 как возвращаемое значение почти всегда говорит об ошибке. А вот тому кто его обозвал pid_t нужно сказать «спасибо»
Просто тот, кто потом доку читает, почему-то уверен, что pid_t — это значение, возвращаемое fork'ом. Классическая невнимательность. А это — тупо тип.
Ну, очевидно, это произошло где-то в момент переползания на 64-битную архитектуру.
"Очевидность" вас подвела. pid_t родился, когда 32767 процессов стало недостаточно, и перевели с 16- на 32-битные значения. Было это ещё в самом начале 1990-х, синхронно как минимум у BSD и Linux. У SysV, возможно, даже раньше.
Просто тот, кто потом доку читает, почему-то уверен, что pid_t — это значение, возвращаемое fork'ом. Классическая невнимательность. А это — тупо тип.
Я не знаю, кто этот "тот" — я никогда так не читал и не слышал такого заблуждения ни от кого из коллег.
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'а процесса в 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.
Было бы, кстати, неплохо так и сделать. По крайней мере, код на Си без плюсов — в первую же очередь. Жаль только, что эта мечта нереализуема...
Если коротко, то переопределив getC и putС в C на своей платформе, то можно будет компилить практически любой исходный код написанный на C.
Мне кажется что народ не понимает что такое C и код ядра
Чисто для самопроверки: мне кажется, что C — это человекочитаемый относительно-платформонезависимый ассемблер. Я прав/нет?
А Rust, в моем далеком от расто-движухи понимании, какая-то достаточно странная (не отрицаю, возможно, «опередившая время», «прорывная» и прочее) мало кому нужная штука с неочевидными преимуществами, неустоявшимся апи и непонятной сферой применения. Больше всего меня смущает такая штука забавная, практически уже закон интернета: в любом обсуждении нюансов любого языка всенепременно возникает человек, демонстрирущий якобы-очевидные сферические преимущества Rust'а над обсуждаемым языком, всеми прочими языками, причем во всех проявлениях. Печально в этом то, что реальных проектов, демонстрирующих «успехи» не прилагается…
Печально в этом то, что реальных проектов, демонстрирующих «успехи» не прилагается
Я прав/нет?
ага
А 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() статьи не случилось бы ;)
А данное API подкладывает грабли, делающие сбой в программе совершенно нелокальным и влияющим на абсолютно несвязанные с программой процессы.
Форк может глюкануть. Так же, как и malloc
Глючит программа погромиста, а вышеописанные функции действуют согласно описанию. Возврат ошибки это нормальное поведение, а не «глюк».
Большинство багов — это человек "не подумал".
В статье человек описывает последствия "не подумал" в этом месте — у него не просто "не работает", но "не работает с катастрофическими последствиями".
(это очень интересная черта русскоязычного интернета — тут не прощают ошибок, и любой рассказ человека про свои ошибки встречают комментариями на тему "какой он идиот"/"некачественный программист" и т.д.).
В то же самое время, в англоязычной среде, честный рассказ про свои ошибки воспринимается с благодарностью — человек рассказал и поделился критическим опытом, на котором можно учиться.
И конечно виноват в этом будет видимо кто? биос?
Будет баг. Кому-то надо будет пойти и написать тест на этот случай, а потом пофиксить этот баг.
Я не понимаю вашей мысли. Моя мысль: баги — плохо; баги — неизбежно. Учиться на чужом опыте — хорошо.
да мало ли причин.
Например, у вас Mac OS, не установлена environment variable OBJC_DISABLE_INITIALIZE_FORK_SAFETY и немного не повезло.
ffff… Спасибо, перечитал man kill. Буду знать.
Вот я с линуксами 15 лет, а про "интересное" поведение kill с отрицательными значениями узнал только сегодня. Буду знать. Автору спасибо.
А вы, видимо, читали все доки к тому, чем пользуетесь, да?
Даже касательно того же 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 же.
Terminate the process group with PGID 117:
kill — -117
А, да. Но прикол в том, что все побежали проверять man kill, хотя в статье прямым текстом описывается поведение системных вызовов fork() и kill(). И прямым текстом в статье посылают прочитать man 2 fork и man 2 kill
Когда-то давно я запускал "ловилку паролей", которая после попытки поймать пароль делала kill -9 -1, фактически выгружая мой сеанс. Потом за неё атата получил, само собой, но опыт остался.
Меня сейчас, скорее всего запинают, ну да ладно.
Я не проверяю результат fork, clone и malloc на возможность ошибки. Потому что я считаю, что это не моя проблема.
Я не пишу код ядра, не пишу драйвера, я делаю вещи попроще и повыше. И я считаю, что если такие базовые вещи сломались и вернули что-то не то, то у моего приложения всё равно нет шансов выжить. Если malloc или fork вернули ошибку, то значит всё плохо на уровне системы. Как мне это обработать? Что мне делать? Упасть? Ну, приложение и так упадет, если попытаюсь что-то сделать не проверив ошибку.
Естественно, я так делаю не всегда. Как-то я допиливал одной утилите возможность работать в режиме демона. Там были подобные проверки, чтобы администратор, подключившись к машине, мог отследить логи и исправить это дело. Но, повторюсь, основное, чем я занимаюсь, работает под контролем пользователя и, я думаю, что что-то не так пользователь заметит на уровне всей системы, потому что ломаться будет всё, а не только моё приложение.
У вас есть шанс меня переубедить, но пока что моя вера в причины, по которым я этого не проверяю, тверда.
Проблема-то в том, что без проверки на -1 программа сама собой не упадёт. Я согласен, что для большинства программ "упасть" — лучший вариант, но в случае fork чтобы правильно упасть — нужно добавить проверку.
Справедливости ради, когда я делаю fork, я чаще всего буду общаться с форком через пайпы или файлы. В первом случае я словлю либо EOF, либо SIGPIPE. Во втором, либо файл не будет существовать, либо он создастся и дочерний процесс закончит работу.
Когда не нужно общение между процессами, то дочерний процесс тоже отработает, либо упадет где-то ещё опять же из-за проблем в системе. То есть мне опять же нечего тут делать. Это слишком глобальная проблема. Это всё равно что не забыть выключить утюг убегая из дома, потому что в нём пожар.
А делать kill для дочернего процесса… Ну как-то такое себе. Я всё таки жду, что он отработает правильно :)
У меня одна грубая ошибка во втором абзаце, извините. Конечно же про дочерний процесс речи не идет. Но если нет общения между процессами, то мы отправили делаться задачу, которая просто не выполнится. Раз общения нет, то мы не ждём результата и просто двигаемся дальше и всё равно падаем ещё где-то из-за проблем в системе.
Что мне делать? Упасть?Можно рухнуть камнем вниз, а можно аккуратно, ничего не сломав лечь.
Если malloc или fork вернули ошибку, то значит всё плохо на уровне системы.
Malloc в некоторых реализациях libc может вернуть ошибку в случае, если поломан механизм переаллоцирования. Например, если был вызван free() на уже удалённый адрес. Т.е. плохо не в системе, а в софте.
fork() может вернуть -1, если pid-ы кончились, как вариант.
Зависит, конечно, от критичности приложения и того, какие гарантии оно дает. Если это, БД, например и в этот момент идет запись данных/индекса, хорошо ли падать и оставлять это в неконсистентном виде, так, что оно потом не поднимется совсем без ручного вмешательства?
Я не пишу код ядра, не пишу драйвера, я делаю вещи попроще и повыше.
"Зочем писать грамотный код? Я выше этого!"
Исправил!
if (fork() == 0) {
...
} else if (fork() == -1) {
...
} else {
...
}
Шутка
Какова подача сего факта в статье: может глюкануть — прям поражает. Я не понимаю, за что так заплюсовали статью? За описанное поведение?
На счёт документации — её не обязательно читать всю на свете. Нужно читать и изучать как минимум то, что используешь здесь и сейчас.
Я часто читаю код ядра. Это очень, очень тяжело. Как-то я пытался понять, с чем едят SIG_DFL. (Это такой интересный псевдосигнал, который простые смертные в userspace не видят). Но за всю свою жизнь я пристально прочитал, ну дай бог, тысяч пять строк ядра.
А их там… Я понимаю, что раньше были любители "читать код ядра". Сейчас скорость MR в ядро такая, что никто (даже Линус!) не может прочитать всё.
Но, возможно, ваши легендарные читатели на это способны, да.
Вот как раз в подобных книжках я и видел примеры кода, который не проверял возвращаемое значение на -1.
Кажется, гораздо более естественный способ реализации многопоточности — с помощью функции создания потока, которой в качестве аргумента передается указатель на функцию потока. pthread_create или что-то подобное.
Когда вы создаёте процесс, вам надо настроить ему 100500 параметров. Stdin, stdout, лимиты, окружение, стек, рабочий каталог, etc, etc. Ещё нужно предусмотреть как ему передать произвольное число fd (включая сетевые сокеты).
Этих параметров очень много. Либо вы делаете развесистое API (как сейчас пишут новое API для создания процесса с использованием proc_fd), либо вы просто говорите "скопировать с меня". Второе явно проще. Процесс подготавливает всё, что нужно, и "форкается".
А потом ловит баги с того, что ресурсы, которые раньше находились в его единоличном владении, теперь расшарены с потомком. Нет уж, спасибо.
И еще память получается общая до первого изменения. Удобно для трюков с prefork серверами.
Android со своим zygote — живой пример.
Правда, там проблемы с ASLR отсюда, но кого и когда это волновало…
А вот запустить заранее и потом fork()-ать — пожалуйста.
Я Вас дополню — Gradle + Junit используют fork() для JVM тестов. А потому собирать Java/Kotlin лучше на Linux машинах, так как быстрее процесс клонируется.
Неизвестно, как различные ресурсы переживают fork.
Сокеты, поидее, переживают плохо, если продолжать использовать их одновременно в двух процессах.
Еще есть различные буферы, кеши.
Если не очистить перед fork(), можно получить двойную запись.
Вобщем, надо следить, какие ресурсы уходят в fork().
Это может быть сложно, если есть много библиотек. Все не проконтролируешь.
А тут еще и GC может вмешаться и добавить неопределенного поведения, в зависимости от того, успел он сработать до fork() или не успел.
Короче, fork() — это лютая утечка абстракций.
вопрос, что же делать при отказе и как его обнаружить
Есть стиль программирования, где принято ничего не делать и надеяться что оно само где-то там упадет, или выдаст exception.
Есть стиль программирования, где принято ничего не делать и надеяться что оно само где-то там упадет, или выдаст exception.
Ничего не имею против такого подхода, он может быть даже вполне оправдан, если язык программирования и окружение позволяют. Можно было бы понять, если автор возмущался поведением fork() в таком языке программирования или библиотеке, декларирующей подобный стиль.
Но потом почитал комментарии.
А потом подумал, что давным-давно, когда вышла Ява, я очень злился на жёсткое требование ловить или декларировать exceptions, а потом понял, что это — единственный способ заставить среднего программера не забивать на обработку ошибок.
А потом подумал, что это всё равно не работает — средние программеры давно объявили Яву вселенским злом и соскочили с неё туда, где опять всё можно.
А потом подумал, и налил себе ликёру. Люблю я его.
И не только у fork(). Это вообще в принципе хорошая идея.
А вы пробовали php?
У нас функция, например, strpos может вернуть 0, а может вернуть false. Гы
Когда нет нормальной системы типов, очевидно.
На питоне сложно стрелять себе в ногу со сравнимой хотя бы по порядку производительностью =)
А так… можно читать из сокета не установив таймаут, например.
Не каждый. К примеру, возврата -1 из os.fork
вы на Питоне никогда не увидите — а потому и в качестве параметра для os.kill
эта -1 случайно никогда не окажется.
Ну, на самом деле, учитывая смысл возвращаемых значений — вряд ли проблема прямо уж настолько актуальна. Проверять значение в любом случае надо (хотя бы на ноль/не ноль, чтоб понять ребёнок это или родитель). И если родитель сохраняет pid после форка для каких-то целей (что тоже не всегда, достаточно часто "форкнулся и забыл") — то там элементарное if (pid>0) сразу отсеет и детей, и ошибки.
pid_t fork(void);
int kill(pid_t pid, int sig);
И там и там pid_t. По идее fork должен возвращать не pid_t а что-то вроде pid_or_invalid_t, так чтобы невалидное значение не могло быть преобразованно к pid_t без сигнала ошибки.
То что man читать надо, это и так понятно.
-1 и так не является валидным pid. Проблема в том, что на входе kill может быть и специальное значение -1. Вот такое вот API.
невалидное значение 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. А 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);
-1 для posix как возвращаемое значение почти всегда ошибка (например read). В моей памяти там жестко зашит int как возвращаемое значение, а уже после ты его обрабатываешь и принимаешь решение.
1. 0 — все ок, !0 — код ошибки.
2. > 0 — какой-то нужный нам дескриптор, <0 — ошибка.
Собственно, к этому привыкаешь, и ничего особенно страшного я тут не вижу.
Собственно, к этому привыкаешь
По привычке считаешь, что -1 — ошибочное значение для всех функций, что работают с pid, а тут вдруг очень не так.
kill() принимает собственно pid_t pid — окей. По привычке мы считаем, что -1 — ошибочное значение, и, как хорошие мальчики, заведомо ошибочное значение в kill() не пихаем — так ведь, да? Мы же хорошие мальчики?
И вот если мы захотели по какой-то причине убить не один процесс а хренову-тучу-сколько-найдем, мы заглянем в доку kill(), просветимся, и засунем то, что ранее мы считали ошибочным значением.
Заодно придем к «просветлению» вида "возвращаемый отрицательный pid_t свидетельствует об ошибке, а входные аргументы мы контролируем сами".
Просто вот эта «проблема fork+kill» — она вылазит исключительно в тех случаях, когда мы ведем себя, как плохие мальчики — так ведь? А плохим мальчикам, не имеющим привычки проверять возвращаемые fork'ом значения и совать коды возврата вместо pid'ов туда, куда не следует, надо по рукам бить указкой, а не бросаться исправлять «трагические ошибки в проектировании» в угоду кривым рученькам. Так ведь?)))
Мне в связи с этим стало интересно. То, что AT_FDCWD определили как минимум в Linux и FreeBSD равным -100 (а не банальному -1), вызвано этими же соображениями?
Проблема вызвана тем, что ошибочный результат возврата не типизирован, просто для него отведено специальное значение в рамках того же типа, что имеет и положительный результат.
Интересно, что в программе на 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"),
}
язык Си очень сильно помогает (помогал, если быть точнее) человеку.
То что он морально устарел тут никто не опровергает.
Посикс создавался во времена, когда не было понимания ни о паттернах, ни о нормальной типизации. Тут тупо обсуждение легаси кода. Естественно Си работает с легаси кодом. С чем вы не согласны со мной в последнем предложении?
Я не согласен с обоснованием «проблема в том, что программист невнимательно прочитал описание $functionname»
Заметьте, я такого не говорил :)
Я пишу что технология старая и не меняется потому что была сделана, когда не было новых технологий, вот и докатились до легаси. А от легаси требовать то, что есть 21 веке как то глупо, это как деда заставить вальс танцевать
Никогда такого не было и вот опять!) Причем здесь "если бы". Есть так, как есть. Когда я немного погружался в С и немного изучал *nix, меня учили: внимательно читайте man'ы. А Вы говорите: "латинский язык устарел, он мертвый, неудобный, учите и говорите на английском, он популярен и удобнее". Это если иносказательно.
Если бы результат fork был описан как data ForkResult = ForkSuccess Pid | ForkFail ErrorMessage, а аргумент kill как data KillTarget = KillPid Pid | KillGroup Gid | KillAll
То это не был бы C. Пока что суть претензии только в том, что glibc на C написан, а в 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.
fork() может потерпеть неудачу: это важно