Как закрыть файл?

Я чувствовал себя в мире с Posix после многолетнего опыта.

Затем я прочитал это сообщение от Линуса Торвальдса, около 2002 года:

int ret;
do {
    ret = close(fd);
} while(ret == -1 && errno != EBADF);

НЕТ.

Вышеуказанное

(a) не переносится

     

(b) не существующая практика

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

Не только цикл, пока EBADF не переносится, но любой цикл из-за условия гонки, который я, вероятно, заметил бы, если бы я не "сделал мир", приняв такие вещи как должное.

Однако в реализации стандартной библиотеки GCC С++ basic_file_stdio.cc мы имеем

    do
      __err = fclose(_M_cfile);
    while (__err && errno == EINTR);

Основной целью этой библиотеки является Linux, но, похоже, она не воспринимает Linus.

Насколько я понял, EINTR происходит только после блокировки системных вызовов, что подразумевает, что ядро ​​получило запрос на освобождение дескриптора до начала любой работы, прерванной. Поэтому нет необходимости в цикле. Действительно, поведение сигнала SA_RESTART не относится к close и генерирует такой цикл по умолчанию, именно потому, что он небезопасен.

Это ошибка стандартной библиотеки, верно? В каждом файле, когда-либо закрытом приложением С++.

РЕДАКТИРОВАТЬ: Чтобы избежать чрезмерной тревоги, прежде чем какой-то гуру приходит с ответом, я должен отметить, что close только кажется позволять блокироваться при определенных обстоятельствах, возможно, ни один из которых никогда не применяется к обычным файлам. Я не понимаю всех деталей, но вы не должны видеть EINTR от close, не выбрав что-либо fcntl или setsockopt. Тем не менее эта возможность делает общий код библиотеки более опасным.

Ответ 1

Что касается POSIX, R.. ответ на соответствующий вопрос, то это очень ясно и красно: close() является не перезапускаемым частным случаем, и цикл не должен использоваться.

Это было удивительно для меня, поэтому я решил описать свои выводы, после чего мои выводы и выбранное решение было завершено.

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


POSIX.1-2001 и POSIX.1-2008 описывают три возможных значения errno, которые могут иметь место: EBADF, EINTR и EIO. Состояние дескриптора после EINTR и EIO является "неуказанным", что означает, что оно может быть закрыто или, возможно, не было закрыто. EBADF указывает, что fd не является допустимым дескриптором. Другими словами, POSIX.1 явно рекомендует использовать

    if (close(fd) == -1) {
        /* An error occurred, see 'errno'. */
    }

без повторной петли, чтобы закрыть дескрипторы файлов.

(Даже группа Austin Group дефект # 519, упомянутый R.. не помогает с восстановлением от ошибок close(): он оставляет это не указано, возможен ли какой-либо ввод-вывод после ошибки EINTR, даже если сам дескриптор оставлен открытым.)


Для Linux, syscall close() определяется в fs/open.c, с __do_close() в fs/file.c управление блокировкой таблицы дескрипторов и filp_close() назад в fs/open.c заботясь о деталях.

Таким образом, запись дескриптора сначала удаляется из таблицы, а затем сбрасывается файловой системой (f_op->flush()), за ней следует уведомление (dnotify/fsnotify hook) и, наконец, путем удаления любых блокировок записей или файлов. (Большинство локальных файловых систем, таких как ext2, ext3, ext4, xfs, bfs, tmpfs и т.д., Не имеют ->flush(), поэтому с учетом действительного дескриптора close() не может завершиться. Только ecryptfs, exofs, fuse, cifs и Насколько я знаю, nfs имеют обработчики ->flush() в Linux-3.13.6.)

Это означает, что в Linux при возникновении ошибки записи в обработчике ->flush(), специфичном для файловой системы, во время close() невозможно повторить попытку; дескриптор файла всегда закрыт, как сказал Торвальдс.

В справочной странице FreeBSD close() описывается то же самое поведение.

Ни OpenBSD, ни Mac OS X close() описать, закрыт ли дескриптор в случае ошибок, но я считаю, что они разделяют поведение FreeBSD.


Мне кажется очевидным, что для закрытия файлового дескриптора не требуется или требуется цикл. Однако close() может по-прежнему возвращать ошибку.

errno == EBADF указывает, что дескриптор файла уже закрыт. Если мой код встречается неожиданно, для меня это указывает на существенную ошибку в логике кода, и процесс должен изящно выйти; Я предпочел бы, чтобы мои процессы умирали, а не мусор.

Любые другие значения errno указывают на ошибку при завершении состояния файла. В Linux это определенно ошибка, связанная с очисткой любых оставшихся данных до фактического хранилища. В частности, я могу представить ENOMEM, если нет места для буферизации данных, EIO, если данные не могут быть отправлены или записаны на фактическое устройство или носитель, EPIPE, если соединение с хранилищем было потеряно, ENOSPC, если хранилище уже заполнено без резервирования нераспакованных данных и т.д. Если файл является файлом журнала, у меня будет отчет о сбое процесса и изящно выйти. Если содержимое файла все еще доступно в памяти, я удаляю (отсоединяю) весь файл и повторю попытку. В противном случае я сообщаю об ошибке пользователю.

(Помните, что в Linux и FreeBSD вы не "просачиваете" дескрипторы файлов в случае ошибки, они гарантированно закрываются, даже если возникает ошибка. Я предполагаю, что все другие операционные системы, которые я могу использовать, ведут себя одинаково.)

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

#include <unistd.h>
#include <errno.h>

/**
 * closefd - close file descriptor and return error (errno) code
 *
 * @descriptor: file descriptor to close
 *
 * Actual errno will stay unmodified.
*/
static int closefd(const int descriptor)
{
    int saved_errno, result;

    if (descriptor == -1)
        return EBADF;

    saved_errno = errno;

    result = close(descriptor);
    if (result == -1)
        result = errno;

    errno = saved_errno;
    return result;
}

Я знаю, что это безопасно для Linux и FreeBSD, и я предполагаю, что он безопасен для всех других систем POSIX-y. Если я столкнулся с тем, что нет, я могу просто заменить выше с помощью специальной версии, завернув ее в подходящую #ifdef для этой ОС. Причина, по которой это поддерживает errno неизменной, - это просто причуда моего стиля кодирования; он сокращает короткие пути ошибок (менее повторяющийся код).

Если я закрываю файл, содержащий важную информацию пользователя, я сделаю fsync() или fdatasync() на нем до закрытие. Это гарантирует, что данные попадают в хранилище, но также вызывает задержку по сравнению с обычной работой; поэтому я не буду делать это для обычных файлов данных.

Если я не буду unlink() ing закрытым файлом, я проверю возвращаемое значение closefd() и действую соответственно. Если я смогу легко повторить попытку, я сделаю это, но не более одного или двух раз. Для файлов журналов и сгенерированных/потоковых файлов я предупреждаю пользователя.

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