Недавно я мимоходом отметил, что errno
был, в целом, хорошим интерфейсом в Unix-системах до появления в них многопоточности. Кого-то подобное высказывание может удивить, поэтому сегодня предлагаю поговорить о сильных и слабых сторонах errno
в традиционных Unix-окружениях, таких, как V7 Unix.
Сильной стороной errno
является тот факт, что этот интерфейс представляет собой простейший механизм, способный возвращать несколько значений из системных вызовов C, в которых нет непосредственной поддержки возврата нескольких значений (особенно — в ранних вариантах C). Использование глобальной переменной для «возврата» второго значения — это практически идеал того, что можно сделать в обычном C, если только не планировать передачу из C-библиотеки указателя на каждый системный вызов и функцию, которые собираются возвращать значение errno
(при таком подходе придётся, например, интенсивно пользоваться stdio
). Постоянная передача подобного указателя приводит не только к ухудшению внешнего вида кода. Такой подход увеличивает объём кода, и, из-за использования дополнительного параметра, приводит к повышению нагрузки на стек (или на регистры).
(Современный C способен на такие фокусы, как возврат двухэлементной структуры в паре регистров, но этого нельзя сказать о более старых и более простых версиях C, используемых, как минимум, в Research Linux V7.)
Некоторые системные вызовы C-библиотек Unix в V7 могли возвращать сведения об ошибке в виде специального значения, и, вероятно, нельзя говорить о том, что все они поддерживали подобную возможность (в V7 действовали ограничения на количество файлов, да и адресное пространство на PDP-11 тоже было достаточно ограниченным). Даже если бы это поддерживали все вызовы, это привело бы к необходимости писать больше кода в случаях, когда нужно было проверять возвращаемые значения команд вроде open()
или sbrk(). В C-коде пришлось бы проверять то, в каком диапазоне значений находится возвращаемое значение, или другие характеристики этого значения.
(Реальные системные вызовы в V7 Unix и до неё использовали метод оповещения об ошибках, спроектированный для ассемблера, когда ядро было настроено на возврат в регистр r0
либо результата системного вызова, либо номера ошибки, и на выполнение установок, зависящих от того, что именно было возвращено. Почитать об этом можно в справке по dup для V4, которая написана в те времена, когда к Unix ещё готовили серьёзную ассемблерную документацию. C-библиотека V7 сделана так, что при возникновении ошибки делается запись в errno
и возвращается -1
. Почитайте, например, libc/sys/dup.s вместе с libc/crt/cerror.s.)
Слабая сторона errno
заключается в том, что это — самостоятельное глобальное значение. То есть — оно может быть случайно перезаписано в том случае, если между моментом, когда в него, интересующим нас системным вызовом, были записаны сведения об ошибке, и моментом, когда мы решили воспользоваться errno
, что-то ещё записало в него сведения о собственной ошибке. Подобное легко может произойти тогда, когда, после сбоя, выполняется прямое или непрямое обращение из обычного кода к какому-нибудь системному вызову, который тоже даёт сбой. Классической ошибкой такого рода была попытка сделать проверку того, является ли стандартный вывод (или стандартный вывод ошибки) терминалом. Делается это путём выполнения на нём TTY-вызова ioctl()
. Когда вызов ioctl()
завершится с ошибкой, исходное значение errno
будет перезаписано значением ENOTTY
, и причина ошибки, из-за которой завершился вызов open()
или какой-то другой вызов, будет описана таинственным сообщением not a typewriter
(cf).
Даже если вы избежали этой ловушки — у вас могут возникнуть проблемы с сигналами, так как сигналы могут прерывать выполнение программ в любых местах, в том числе — сразу после возврата из системных вызовов и до того, как было проанализировано значение, хранящееся в errno
. В наши дни обычно не ожидается, что в обработчиках сигналов будут выполнять какие-то действия, но в давние времена в них могли делать очень много всего. Особенно, например, в обработчике сигнала SIGCHLD
, где, чтобы узнать о статусе выхода дочернего процесса, вызывали wait()
до тех пор, пока он не завершался с ошибкой и с записью чего-то в errno
, что, если это было сделано в неудачное время, привело бы к перезаписи исходного значения errno
. Обработчик сигнала может быть рассчитан на работу в таких условиях в том случае, если программист помнит об этой проблеме, но о ней вполне можно и забыть. Программисты часто упускают из виду особенности работы программ, связанные со временем, способные вызывать «состояние гонок», особенно тогда, когда речь идёт о маленьких промежутках времени, и в случаях, когда проблемы, связанные со временем, возникают нечасто.
(В V7 не было сигнала SIGCHLD
, но он был в BSD. Это так из-за того, что в BSD появилась система управления заданиями, что и привело к необходимости наличия подобного сигнала. Но это — уже совсем другая история.)
В целом же я полагаю, что errno
был хорошим интерфейсом, учитывая ограничения традиционной Unix, когда не было многопоточности или нормальных способов возврата нескольких значений из вызовов C-функций. Хотя у него есть и минусы, и слабые стороны, их обычно можно было обойти, и обычно они не слишком часто давали о себе знать. API errno
стал выглядеть весьма нескладно только тогда, когда в Unix появилась многопоточность, и в одном адресном пространстве могло присутствовать несколько сущностей, одновременно выполняющих системные вызовы. Как и большая часть того, что имеется в Unix (в особенности — в эру Research Unix V7), это — не идеальное, хотя и вполне приемлемое решение.
Сталкивались ли вы с проблемами errno?
Автор: ru_vds