Linux C/C++ как обрабатывать ошибки системных вызовов

Есть определенная программа которая использует системные вызовы. Всё понятно, когда, например, вызов ошибочный и в errno что-то типа: ENOENT, EEXIST, EAGAIN, EACCESS, EINTR. Но, список этих ошибок довольно таки большой и некоторые из этих ошибок я не понимаю как обрабатывать.

Что программа должна делать, если в errno, например, будет: EIO, ENOMEM или EFAULT. Есть, конечно, предположение, что пол системы ляжет и там уже неважно что делать, ведь если у одной программы такой результат после системного вызова, то и других такая же ситуация :)

EIO: "что-то неладное происходит с диском на низком уровне" (я знаю, там и другие варианты есть (связаны с терминалом), но возьмем файл), и что делать? Сломалось ядро или устройство? Если проверять устройство кодом в той же программы, то добавится немало кода: оправдано ли это и много ли кто на практике таким занимается?

ENOMEM: "недостаточно памяти ядра" (есть и другие описания). Мы ведь не знаем когда появится и появится ли вообще, возможно ли эту ошибку избежать при повторном вызове или после перезапуска программы?

EFAULT: "Указатель за пределами допустимых значений". Это, конечно, скорее всего для отладки и поиска багов для устранения на месте, вряд ли ошибка возникнет позже, если всё хорошо работает (если все пути программы где это может возникнуть протестированы). Или может?

Во многих источниках по обучению программированию примеры что-то вроде таких:

int fd = open(...);
if (fd < 0) {
        if (errno == ENOENT)
                ...;
        else {
                сообщение;
                exit(...);
        }
}

Если программа должна отработать тут и сейчас, то это еще допустимо, но если программа должна работать долго без участия/контроля человека, то такое ведь явно не годится.

Вопрос расплывчатый и зависит от среды: десктоп/сервер/удаленный контроллер и есть ли система инициализации которая перезапустит программу в случае чего - например, systemd. Может кто-то сталкивался с такими ситуациям и поделится опытом: в какой среде и как решали? Очень интересно. Спасибо.

UPD: вижу запросы на закрытие из-за того, что вопрос неточный. Логичный вопрос:

какой ответ пойдет под галочку, что ожидает автор?

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

Если быть точнее, то вопрос придется разбивать на 3-5 минимум (на каждую ошибку вопрос, например), а это будет лишним.

UPD 2: сужаю область: обособленный объект без связи с внешним миром (контроллер с GNU/Linux). Что делать программе которая столкнулась с ENOMEM/EIO в такой системе. Как-то действительно это можно решить программно: выяснить, что дальше некуда, или выяснить, что нужно пытаться вновь, сталкивались ли с таким? И по поводу c++: никаких std/exception (Почему метка c++ автоматически предписывает эти штуки? - Вопрос риторический).


Ответы (1 шт):

Автор решения: Swift - Friday Pie

В POSIX-совместимой системе для каждого вызова функции API своя семантика и свой набор возможных ошибок, часть предопределена стандартом. Причем в ряде случаев набор и значение ошибок зависит от аргументов переданных этому вызову или режима работы объекта, с котором он работает. Например, флаги доступа, с которыми открывался файл или сокет. Этот набор обычно указан в документации (man). Для некоторых функций расширенный набор может варьироваться от версии к версии ядра.

Возьму из приведенных примеров ENOMEM. В большинстве случаев не появляется. Только в редких случаях это нехватка памяти. Это может быть нехватка буферов ввода-вывода. При вызове malloc из glibc - это отсутствие непрерывного блока заданного размера, с другими реализациями errno вообще не устанавливается и стандартом это поведение не описано. Например, захотели выделить 512 Мб памяти, а столько подряд и нет, glibc установит ENOMEM, в остальных случаях просто возвращается NULL. Это не значит, что у всех остальных процессов то же самое. При использовании Си++ и new на этот вообще не следует обращать внимание, библиотека времени исполнения уже захватывает возвращенный errno внутри. В Си++ new в таком случае вызывает исключение, и, скорее всего, это - фатальная ситуация.

В результате универсальный обработчик или стратегию описать невозможно, скорее нужна какая-то обертка, запоминающая состояние. В Си++ чаще всего для этого заводят объекты-абстракции, "адаптеры" или "менеджеры". Обработка ошибок делается в них же, на основе сохраненных инвариантов. Учитывая "возраст" языка Си++, практически для каждого вида задач есть какая-нибудь библиотека с такой оберткой.

PS. Нужно заметить, что функции API как правило не являются системными вызовами, последние являются более низкоуровневыми. Системный вызов - это обращение к прерыванию для передачи управления ядру, c помощью инструкций int или syscall. То что вы вызываете - функции-обертки вокруг системных вызовов, поэтому иногда допустимые аргументы или возвращаемые значения могут варьироваться в зависимости от версии библиотеки времени исполнения. Самый яркий пример - функция setsockopt.

→ Ссылка