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/writeclose (сбросить буфер, освободить юнит).
  • newunit= безопасно выделяет свободный номер юнита — предпочитайте его ручным номерам.
  • iostat= перехватывает результат и не даёт программе аварийно упасть; iomsg= даёт текст ошибки.
  • Конец файла — отрицательный iostat (константа iostat_end); положительный — настоящая ошибка; различайте их.
  • iostat предпочтительнее меток end=/err=: он не ломает структуру кода.
Проверьте себя
1. Что произойдёт по умолчанию, если open не находит файл и спецификатор iostat не указан?
AПрограмма создаст пустой файл
BПрограмма аварийно завершится с ошибкой ввода-вывода
Copen вернёт ноль и продолжит
DФайл откроется в режиме записи
2. Как в цикле чтения отличить нормальный конец файла от ошибки?
AКонец файла даёт iostat = 0, ошибка — любое ненулевое
BКонец файла — отрицательный iostat (iostat_end), ошибка — положительный
CКонец файла и ошибка неразличимы
DОшибка даёт iostat = 0, конец файла — отрицательное