open, close и обработка ошибок через iostat
Грамотная работа с файлами начинается с правильного open и заканчивается дисциплинированной обработкой ошибок через iostat — без этого программа падает на первой же проблеме с диском.
iostat — целочисленная переменная, в которую операторы I/O записывают код результата: 0 при успехе, отрицательное значение при конце файла/записи, положительное при ошибке. Она превращает фатальный сбой в управляемую ситуацию.
Жизненный цикл файла
Работа с внешним файлом в Fortran устроена вокруг понятия юнита (unit) — целочисленного дескриптора, через который происходит весь ввод-вывод. Цикл всегда один: open связывает файл с юнитом и задаёт режим, затем серия read/write по этому юниту, в конце — close, разрывающий связь и сбрасывающий буферы на диск. Пропустить close опасно: данные могут остаться в буфере и не записаться, а число одновременно открытых юнитов ограничено операционной системой. Хорошая программа закрывает каждый файл, который открыла, — желательно сразу после того, как он перестал быть нужен.
open и его спецификаторы
Оператор open принимает набор именованных спецификаторов. Самые важные перечислены ниже; современный стиль настоятельно рекомендует newunit= вместо ручного выбора номера юнита.
| Спецификатор | Назначение | Значения |
newunit=u | безопасно выделить свободный номер юнита | возвращает отрицательное число в u |
file= | имя файла | строка |
status= | как обойтись с существованием файла | old/new/replace/scratch/unknown |
action= | разрешённые операции | read/write/readwrite |
form= | форматный или двоичный | formatted/unformatted |
access= | модель доступа | sequential/direct/stream |
iostat= | код результата открытия | целое |
Особо о newunit=: исторически программисты сами выбирали номера юнитов (open(10, ...)), рискуя случайно занять чужой или зарезервированный системой (обычно 0, 5, 6 заняты под стандартные потоки). newunit=u поручает выбор компилятору — он вернёт гарантированно свободный (отрицательный) номер. В современном коде всегда используют newunit: это устраняет целый класс коллизий юнитов.
Зачем нужен iostat
По умолчанию ошибка ввода-вывода аварийно завершает программу: файл не найден, нет прав, диск переполнен — и расчёт, считавшийся часами, обрывается с невнятным сообщением. Это недопустимо для надёжного кода. Спецификатор iostat=var перехватывает результат: при успехе var=0, иначе — ненулевой код, а программа продолжает выполнение, передав вам управление. Теперь вы сами решаете, что делать: повторить, сообщить пользователю, переключиться на запасной путь. Это разница между программой, которая «падает», и программой, которая «обрабатывает».
program safe_open
implicit none
integer :: u, ios
character(len=256) :: msg
open(newunit=u, file="input.dat", status="old", action="read", &
iostat=ios, iomsg=msg)
if (ios /= 0) then
print *, "Не удалось открыть файл, код:", ios
print *, "Сообщение: ", trim(msg)
stop 1
end if
! ... работа с файлом ...
close(u)
end program safe_open
Вместе с iostat идёт iomsg=msg — строка, куда runtime кладёт человекочитаемое описание ошибки («No such file or directory» и т.п.). Связка iostat + iomsg даёт и код для логики, и текст для диагностики. Это канонический шаблон надёжного открытия файла в современном Fortran.
Чтение до конца файла
Самая частая задача — прочитать файл целиком, не зная заранее числа строк. Здесь iostat играет вторую роль: при достижении конца файла он получает отрицательное значение (стандарт даёт именованную константу iostat_end из модуля iso_fortran_env). Цикл чтения опирается на это.
program read_all
use iso_fortran_env, only: iostat_end
implicit none
integer :: u, ios, count
real :: x
open(newunit=u, file="numbers.txt", status="old", action="read")
count = 0
do
read(u, *, iostat=ios) x
if (ios == iostat_end) exit ! нормальный конец файла
if (ios /= 0) then
print *, "Ошибка чтения, код:", ios
exit
end if
count = count + 1
end do
close(u)
print *, "Прочитано чисел:", count
end program read_all
Логика разделяет три исхода: ios == 0 — значение прочитано, работаем дальше; ios == iostat_end — файл кончился штатно, выходим без ошибки; ios > 0 — настоящая ошибка (битые данные, сбой диска), реагируем особо. Различать «конец файла» и «ошибку» важно: первое — нормальное завершение, второе — проблема. Использование именованной константы iostat_end вместо «магического» отрицательного числа делает код переносимым и читаемым.
Альтернатива: спецификаторы end= и err=
Исторически тот же эффект достигался метками перехода: end= указывает метку, на которую прыгнуть при конце файла, err= — при ошибке. Этот стиль восходит к Fortran 77 и встречается в легаси-коде.
read(u, *, end=200, err=900) x
! ... обработка значения ...
200 continue ! сюда при конце файла
! ...
900 continue ! сюда при ошибке
Современный стиль предпочитает iostat переходам end=/err=: переходы по меткам ломают структуру кода и плохо сочетаются с конструкциями do/if, тогда как iostat естественно вписывается в обычный поток управления. Знать end=/err= нужно для чтения старого кода, но в новом выбирайте iostat.
Как работает под капотом
Под каждым оператором I/O лежит вызов runtime-библиотеки, которая обращается к файловой системе ОС. Эти системные вызовы могут вернуть код ошибки (нет файла, нет прав, нет места). Без iostat runtime, получив ошибку, по умолчанию вызывает аварийное завершение. Спецификатор iostat меняет политику: runtime вместо завершения записывает код в вашу переменную и возвращает управление. Значения кодов: ноль — успех; для конца файла/записи стандарт гарантирует именованные константы iostat_end и iostat_eor (конец записи при чтении с advance='no'); положительные коды — ошибки, чьи конкретные номера зависят от компилятора, поэтому на их числовые значения опираться не стоит — сравнивайте с нулём и именованными константами, а текст берите из iomsg. Буферизация объясняет важность close: записанные данные сперва оседают в буфере runtime/ОС и физически попадают на диск при сбросе буфера, который гарантированно происходит на close (или на flush).
Запрос состояния файла: оператор inquire
Помимо open, read, write и close, у Fortran есть пятый оператор ввода-вывода, о котором часто забывают, — inquire. Он спрашивает о свойствах файла или юнита, не открывая и не читая его. Это незаменимо для надёжного кода: прежде чем открыть файл, можно проверить, существует ли он; прежде чем писать, убедиться, что юнит не занят; узнать размер файла, его формат, позицию. inquire работает в двух режимах — по имени файла (file=) или по номеру юнита (unit=) — и возвращает запрошенные сведения в переменные.
logical :: exists, is_open
integer :: fsize, unit_num
character(len=20) :: rw_status
! проверить существование файла ДО открытия:
inquire(file="data.dat", exist=exists, size=fsize)
if (.not. exists) then
print *, "Файл не существует, создаю новый"
else
print *, "Файл есть, размер байт:", fsize
end if
! узнать, открыт ли уже юнит, и в каком режиме:
inquire(unit=10, opened=is_open, action=rw_status)
print *, "Юнит 10 открыт:", is_open, " режим:", trim(rw_status)
Спецификатор exist= — самый употребительный: он позволяет элегантно обрабатывать сценарии «открыть существующий или создать новый», «использовать конфиг, если он есть, иначе значения по умолчанию», не полагаясь на код ошибки open. Другие полезные запросы: size= (размер файла в байтах), opened= (открыт ли), number= (какой юнит связан с файлом), pos= (текущая позиция в stream-файле), form=/access= (как файл открыт). inquire делает работу с файлами более «осознанной»: вместо того чтобы пытаться открыть и реагировать на ошибку, программа сначала узнаёт обстановку и действует обдуманно. Это особенно важно в долгоживущих расчётах, которые должны корректно вести себя при разных состояниях файловой системы.
Стандартные потоки и философия обработки ошибок
Три юнита в Fortran зарезервированы и доступны без open: стандартный ввод, стандартный вывод и стандартный поток ошибок. Их номера исторически 5, 6 и (часто) 0, но опираться на эти числа не стоит — современный стандарт даёт именованные константы в модуле iso_fortran_env: input_unit, output_unit, error_unit. Запись print * и read * используют стандартные вывод и ввод. Важная практика — направлять диагностику и ошибки не в output_unit, а в error_unit: тогда сообщения об ошибках не смешиваются с полезным выводом и видны, даже когда основной поток перенаправлен в файл.
use iso_fortran_env, only: error_unit, output_unit
! полезный результат — в стандартный вывод:
write(output_unit, *) "Результат:", answer
! диагностика — в поток ошибок:
write(error_unit, *) "Предупреждение: достигнут предел итераций"
За техникой стоит более широкая философия обработки ошибок ввода-вывода, которую стоит усвоить как принцип. Ввод-вывод — это граница программы с внешним, ненадёжным миром: диски переполняются, файлы исчезают, права меняются, сеть рвётся. В отличие от чисто вычислительного кода, где при корректной логике ошибок быть не должно, I/O-операция может законно завершиться неудачей по причинам вне власти программы. Поэтому зрелый код относится к каждой I/O-операции как к потенциально сбойной: проверяет iostat, различает конец данных и реальную ошибку, выдаёт внятную диагностику в error_unit и принимает осмысленное решение — повторить, использовать запасной путь, аккуратно завершиться с ненулевым кодом. Программа, которая молча падает с непонятным сообщением при первой же проблеме с файлом, непригодна для серьёзного использования, где расчёты идут часами и должны переживать неурядицы окружения. Дисциплина iostat/iomsg/inquire/error_unit — это и есть граница между учебным примером и надёжным инженерным кодом, способным работать без присмотра в реальных условиях вычислительного кластера.
Частые ошибки
- Игнорировать
iostatпри открытии. Без него отсутствующий файл или нехватка прав уронят программу. Всегда проверяйте код послеopenдля файлов, чьё существование не гарантировано. - Сравнивать
iostatс конкретным положительным числом. Числовые коды ошибок зависят от компилятора; проверяйте/= 0для ошибки и== iostat_endдля конца файла, текст берите изiomsg. - Путать конец файла и ошибку. Конец файла — отрицательный
iostat(iostat_end), это нормальный исход; положительный — настоящая ошибка. Обрабатывайте их по-разному. - Ручной выбор номера юнита.
open(6, ...)может конфликтовать со стандартным выводом. Используйтеnewunit=. - Забыть
close. Несброшенный буфер — потеря данных; неосвобождённые юниты — утечка дескрипторов. Закрывайте файлы.
Итоги
- Цикл файла:
open(связать юнит и задать режим) →read/write→close(сбросить буфер, освободить юнит). newunit=безопасно выделяет свободный номер юнита — предпочитайте его ручным номерам.iostat=перехватывает результат и не даёт программе аварийно упасть;iomsg=даёт текст ошибки.- Конец файла — отрицательный
iostat(константаiostat_end); положительный — настоящая ошибка; различайте их. iostatпредпочтительнее метокend=/err=: он не ломает структуру кода.