Сборка мусора для `fopen()`?
Boehm gc занимается только выделением памяти. Но если кто-то хочет использовать сборщик мусора, чтобы иметь дело с fopen()
чтобы fclose()
больше не нужен. Есть ли способ сделать это в C?
PS Например, PyPy использует подход сборки мусора для открытия файлов.
Наиболее очевидный эффект этого заключается в том, что файлы (и сокеты, и т. Д.) Не закрываются сразу после выхода из области видимости. Для файлов, которые открыты для записи, данные могут некоторое время оставаться в своих выходных буферах, что делает файл на диске пустым или усеченным.
2 ответа
В случае, если это не очевидно, Boehm GC не делает ничего возможного в C. Вся библиотека представляет собой огромную кучу неопределенного поведения, которое вроде бы работает в некоторых (многих?) Реальных реализациях. Чем более продвинуты, особенно в области безопасности, реализации C, тем меньше вероятность того, что какая-либо из них продолжит работать.
С учетом вышесказанного, я не вижу причин, по которым тот же принцип не может быть распространен на FILE*
ручки. Проблема, однако, заключается в том, что из-за того, что он обязательно является консервативным GC, ложные срабатывания для оставшихся ссылок будут препятствовать закрытию файла, что имеет видимые последствия для состояния процесса и файловой системы. Если вы явно fflush
в правильных местах это может быть приемлемо только наполовину сломанным, все же.
С другой стороны, нет абсолютно никакого осмысленного способа сделать это с помощью файловых дескрипторов, поскольку они представляют собой небольшие целые числа. По сути, у вас всегда будут ложные срабатывания для оставшихся ссылок.
TL;DR: Да, но. Больше, чем да.
Обо всем по порядку. Поскольку стандартная библиотека C должна сама автоматически собирать мусор, открывайте дескрипторы файлов в exit()
функция (см. стандартные кавычки ниже), нет необходимости вызывать fclose
пока:
Вы абсолютно уверены, что ваша программа в конце концов прекратит работу, вернувшись из
main()
или по телефонуexit()
,Вам не важно, сколько времени проходит до закрытия файла (делая данные, записанные в файл, доступными для других процессов).
Вам не нужно получать информацию, если операция закрытия не удалась (возможно, из-за сбоя диска).
Ваш процесс не будет открыт больше, чем
FOPEN_MAX
файлы, и не будет пытаться открыть один и тот же файл дважды. (FOPEN_MAX
должно быть не менее восьми, но это включает три стандартных потока.)
Конечно, кроме очень простых игрушечных приложений, эти гарантии довольно ограничительны, особенно для файлов, открытых для записи. Для начала, как вы собираетесь гарантировать, что хост не выйдет из строя или не отключится (условие отмены 1)? Поэтому большинство программистов считают очень плохим стилем не закрывать все открытые файлы.
Тем не менее, можно представить себе приложение, которое только открывает файлы для чтения. В этом случае самая серьезная проблема с никогда не звонит fclose
будет последним, лимит одновременных открытых файлов. Пять - это довольно небольшое число, и хотя большинство систем имеют гораздо более высокие ограничения, они почти все имеют ограничения; если приложение работает достаточно долго, оно неизбежно откроет слишком много файлов. (Условие 3 также может быть проблемой, хотя не все операционные системы накладывают это ограничение, и лишь немногие системы накладывают ограничение на файлы, открытые только для чтения.)
Как оказалось, именно эти проблемы, теоретически, может помочь сборщик мусора. Немного поработав, можно получить сборщик мусора, который поможет управлять количеством одновременно открытых файлов. Но... как уже упоминалось, есть ряд Но с. Вот несколько из них:
Стандартная библиотека не обязана динамически распределять
FILE
объекты, использующиеmalloc
или даже для их динамического выделения. (Библиотека, которая допускает только восемь открытых файлов, может иметь внутренний статически распределенный массив из восьмиFILE
структуры, например.) Таким образом, сборщик мусора может никогда не увидеть распределение памяти. Для того, чтобы вовлекать сборщик мусора в удалениеFILE
объекты, каждыйFILE*
должен быть заключен в динамически распределенный прокси ("дескриптор"), и каждый интерфейс, который принимает или возвращаетFILE*
указатели должны быть обернуты тем, который создает прокси. Это не слишком много работы, но есть много интерфейсов, которые нужно обернуть, и использование оберток в основном зависит от модификации исходного кода; вам может быть трудно представитьFILE*
прокси, если некоторые файлы открыты внешними библиотечными функциями.Хотя сборщику мусора можно сказать, что делать, прежде чем он удаляет определенные объекты (см. Ниже), большинство библиотек сборщика мусора не имеют интерфейса, который предусматривает ограничение на создание объектов, кроме наличия памяти. Сборщик мусора может решить проблему "слишком много открытых файлов" только в том случае, если он знает, сколько файлов разрешено открывать одновременно, но не знает, и у него нет способа сообщить вам об этом. Таким образом, вы должны договориться о том, чтобы сборщик мусора вызывался вручную, когда этот предел вот-вот будет нарушен. Конечно, так как вы уже завершили все вызовы
fopen
согласно пункту 1, вы можете добавить эту логику в свою оболочку, либо отслеживая количество открытых файлов, либо реагируя на индикацию ошибки изfopen()
, (Стандарт C не определяет переносимый механизм для обнаружения этой конкретной ошибки, но Posix говорит, чтоfopen
должен потерпеть неудачу и установитьerrno
вEMFILE
если у процесса слишком много открытых файлов. Posix также определяетENFILE
значение ошибки для случая, когда слишком много файлов открыто во всех процессах; Вероятно, стоит рассмотреть оба этих случая.)Кроме того, сборщик мусора не имеет механизма, ограничивающего сборку мусора одним типом ресурса. (Было бы очень трудно реализовать это в сборщике мусора с разметкой меток, таком как сборщик BDW, потому что вся используемая память должна быть отсканирована для поиска живых указателей.) Таким образом, запуск сборки мусора при каждом использовании всех слотов файловых дескрипторов может быть использован оказываются довольно дорогими.
Наконец, сборщик мусора не гарантирует своевременного сбора мусора. Если ресурсов не будет, сборщик мусора может оставаться в неактивном состоянии в течение длительного времени, и если вы используете сборщик мусора для закрытия файлов, это означает, что файлы могут оставаться открытыми в течение неограниченного времени, даже если они больше не используется. Итак, первые два условия в первоначальном списке требований для исключения
fclose()
продолжать действовать даже с сборщиком мусора.
Так. Да, но, но, но, но. Вот что рекомендует документация Boehm GC (сокращенно):
- Действия, которые должны быть выполнены быстро... должны обрабатываться явными вызовами в коде.
- Скудные системные ресурсы должны управляться явно, когда это удобно. Используйте [сборщик мусора] только в качестве механизма резервного копирования для случаев, которые трудно обработать явно.
- Если ограниченные ресурсы управляются с помощью [сборщика мусора], подпрограмма выделения для этого ресурса (например, дескрипторы открытых файлов) должна форсировать сборку мусора (два, если этого недостаточно), если ей не хватает ресурса.
- Если управление чрезвычайно дефицитными ресурсами (например, файловые дескрипторы в системах с ограничением в 20 открытых файлов), может потребоваться ввести схему кэширования дескрипторов, чтобы скрыть ограничение ресурсов.
Теперь предположим, что вы прочитали все это, и вы все еще хотите это сделать. Это на самом деле довольно просто. Как упоминалось выше, вам нужно определить прокси-объект или дескриптор, который содержит FILE*
, (Если вы используете интерфейсы Posix, такие как open()
которые используют файловые дескрипторы - маленькие целые числа - вместо FILE
структуры, то ручка держит FD. Очевидно, это другой тип объекта, но механизм идентичен.)
В вашей обертке для fopen()
(или же open()
или любые другие вызовы, которые возвращаются открытыми FILE*
или файлы), вы динамически выделяете дескриптор, а затем (в случае GC Boehm) вызываете GC_register_finalizer
сообщить сборщику мусора, какую функцию вызывать, когда ресурс будет удален. Почти все библиотеки GC имеют такие возможности; ищи finalizer
в их документации. Вот документация для сборщика Boehm, из которой я извлек список предупреждений выше.
Будьте осторожны, чтобы избежать условий гонки, когда вы заканчиваете открытый колл. Рекомендуемая практика заключается в следующем:
- Динамически распределить ручку.
- Инициализируйте его содержимое для значения часового значения (например, -1 или NULL), которое указывает, что дескриптор еще не был назначен открытому файлу.
- Зарегистрируйте финализатор для ручки. Функция финализатора должна проверять значение часового, прежде чем пытаться вызвать
fclose()
, поэтому регистрация дескриптора на этом этапе - это нормально. - Откройте файл (или другой такой ресурс).
- Если открытие успешно, сбросьте дескриптор, чтобы использовать возвращенный из открытия. Если ошибка связана с исчерпанием ресурсов, запустите сборку мусора вручную и повторите при необходимости. (Будьте осторожны, чтобы ограничить количество раз, которое вы делаете это для одной открытой оболочки. Иногда вам нужно сделать это дважды, но три последовательных сбоя, вероятно, указывают на какую-то другую проблему.)
- Если открытие в конце концов завершилось успешно, верните ручку. В противном случае, необязательно отмените регистрацию финализатора (если это позволяет ваша библиотека GC) и верните сообщение об ошибке.
Обязательные C стандартные кавычки
Возвращаясь из
main()
так же, как звонитьexit()
§5.1.2.2.3 (Завершение программы): (Применяется только к размещенным реализациям)
- Если тип возвращаемого значения
main
функция совместима с типомint
, возврат от первоначального вызова кmain
Функция эквивалентна вызову функции выхода со значением, возвращаемымmain
функция в качестве аргумента; достигнув}
что завершаетmain
Функция возвращает значение 0.
- Если тип возвращаемого значения
призвание
exit()
очищает все файловые буферы и закрывает все открытые файлы§7.22.4.4 (функция выхода):
- Затем все открытые потоки с неписанными буферизованными данными сбрасываются, все открытые потоки закрываются и все файлы, созданные
tmpfile
функция снята…
- Затем все открытые потоки с неписанными буферизованными данными сбрасываются, все открытые потоки закрываются и все файлы, созданные