С помощью setvbuf можно заставить fprintf немедленно синхронизировать вывод с файлом
Р абота с текстовым файлом похожа работу с консолью: с помощью функций форматированного ввода мы сохраняем данные в файл, с помощью функций форматированного вывода считываем данные из файла. Есть множество нюансов, которые мы позже рассмотрим. Основные операции, которые необходимо проделать, это
- 1. Открыть файл, для того, чтобы к нему можно было обращаться. Соответственно, открывать можно для чтения, записи, чтения и записи, переписывания или записи в конец файла и т.п. Когда вы открываете файл, может также произойти куча ошибок – файла может не существовать, это может быть файл не того типа, у вас может не быть прав на работу с файлом и т.д. Всё это необходимо учитывать.
- 2. Непосредственно работа с файлом - запись и чтение. Здесь также нужно помнить, что мы работаем не с памятью с произвольным доступом, а с буферизированным потоком, что добавляет свою специфику.
- 3. Закрыть файл. Так как файл является внешним по отношению к программе ресурсом, то если его не закрыть, то он продолжит висеть в памяти, возможно, даже после закрытия программы (например, нельзя будет удалить открытый файл или внести изменения и т.п.). Кроме того, иногда необходимо не закрывать, а "переоткрывать" файл для того, чтобы, например, изменить режим доступа.
Кроме того, существует ряд задач, когда нам не нужно обращаться к содержимому файла: переименование, перемещение, копирование и т.д. К сожалению, в стандарте си нет описания функций для этих нужд. Они, безусловно, имеются для каждой из реализаций компилятора. Считывание содержимого каталога (папки, директории) – это тоже обращение к файлу, потому что папка сама по себе является файлом с метаинформацией.
Иногда необходимо выполнять некоторые вспомогательные операции: переместиться в нужное место файла, запомнить текущее положение, определить длину файла и т.д.
Для работы с файлом необходим объект FILE. Этот объект хранит идентификатор файлового потока и информацию, которая нужна, чтобы им управлять, включая указатель на его буфер, индикатор позиции в файле и индикаторы состояния.
Объект FILE сам по себе является структурой, но к его полям не должно быть доступа. Переносимая программа должна работать с файлом как с абстрактным объектом, позволяющим получить доступ до файлового потока.
Создание и выделение памяти под объект типа FILE осуществляется с помощью функции fopen или tmpfile (есть и другие, но мы остановимся только на этих).
Функция fopen открывает файл. Она получает два аргумента – строку с адресом файла и строку с режимом доступа к файлу. Имя файла может быть как абсолютным, так и относительным. fopen возвращает указатель на объект FILE, с помощью которого далее можно осуществлять доступ к файлу.
Например, откроем файл и запишем в него Hello World
Функция fopen сама выделяет память под объект, очистка проводится функцией fclose. Закрывать файл обязательно, самостоятельно он не закроется.
Функция fopen может открывать файл в текстовом или бинарном режиме. По умолчанию используется текстовый. Режим доступа может быть следующим
Тип | Описание |
---|---|
r | Чтение. Файл должен существовать. |
w | Запись нового файла. Если файл с таким именем уже существует, то его содержимое будет потеряно. |
a | Запись в конец файла. Операции позиционирования (fseek, fsetpos, frewind) игнорируются. Файл создаётся, если не существовал. |
r+ | Чтение и обновление. Можно как читать, так и писать. Файл должен существовать. |
w+ | Запись и обновление. Создаётся новый файл. Если файл с таким именем уже существует, то его содержимое будет потеряно. Можно как писать, так и читать. |
a+ | Запись в конец и обновление. Операции позиционирования работают только для чтения, для записи игнорируются. Если файл не существовал, то будет создан новый. |
Если необходимо открыть файл в бинарном режиме, то в конец строки добавляется буква b, например “rb”, “wb”, “ab”, или, для смешанного режима “ab+”, “wb+”, “ab+”. Вместо b можно добавлять букву t, тогда файл будет открываться в текстовом режиме. Это зависит от реализации. В новом стандарте си (2011) буква x означает, что функция fopen должна завершиться с ошибкой, если файл уже существует. Дополним нашу старую программу: заново откроем файл и считаем, что мы туда записали.
Вместо функции fgets можно было использовать fscanf, но нужно помнить, что она может считать строку только до первого пробела.
fscanf(file, "%127s", buffer);
Также, вместо того, чтобы открывать и закрывать файл можно воспользоваться функцией freopen, которая «переоткрывает» файл с новыми правами доступа.
Функции fprintf и fscanf отличаются от printf и scanf только тем, что принимают в качестве первого аргумента указатель на FILE, в который они будут выводить или из которого они будут читать данные. Здесь стоит сразу же добавить, что функции printf и scanf могут быть без проблем заменены функциями fprintf и fscanf. В ОС (мы рассматриваем самые распространённые и адекватные операционные системы) существует три стандартных потока: стандартный поток вывода stdout, стандартный поток ввода stdin и стандартный поток вывода ошибок stderr. Они автоматически открываются во время запуска приложения и связаны с консолью. Пример
Ошибка открытия файла
Если вызов функции fopen прошёл неудачно, то она возвратит NULL. Ошибки во время работы с файлами встречаются достаточно часто, поэтому каждый раз, когда мы окрываем файл, необходимо проверять результат работы
Проблему вызывает случай, когда открывается сразу несколько файлов: если один из них нельзя открыть, то остальные также должны быть закрыты
В простых случаях можно действовать влоб, как в предыдущем куске кода. В более сложных случаях используются методы, подменяющиее RAII из С++: обёртки, или особенности компилятора (cleanup в GCC) и т.п.
Буферизация данных
- 1) Если он заполнен
- 2) Если поток закрывается
- 3) Если мы явно указываем, что необходимо очистить буфер (здесь тоже есть исключения:)).
- 4) Также очищается, если программа завершилась удачно. Вместе с этим закрываются и все файлы. В случае ошибки выполнения этого может не произойти.
Форсировать выгрузку буфера можно с помощью вызова функции fflush(File *). Рассмотрим два примера – с очисткой и без.
Раскомментируйте вызов fflush. Во время выполнения откройте текстовый файл и посмотрите на поведение.
Буфер файла можно назначить самостоятельно, задав свой размер. Делается это при помощи функции
которая принимает уже открытый FILE и указатель на новый буфер. Размер нового буфера должен быть не меньше чем BUFSIZ (к примеру, на текущей рабочей станции BUFSIZ равен 512 байт). Если передать в качестве буфера NULL, то поток станет небуферизированным. Можно также воспользоваться функцией
- _IOFBF - полная буферизация. Данные записываются в файл, когда он заполняется. На считывание, буфер считается заполненным, когда запрашивается операция ввода и буфер пуст.
- _IOLBF - линейная буферизация. Данные записываются в файл когда он заполняется, либо когда встречается символ новой строки. На считывание, буфер заполняется до символа новой строки, когда запрашивается операция ввода и буфер пуст.
- _IONBF – без буферизации. В этом случае параметры size и buffer игнорируются.
Пример: зададим свой буфер и посмотрим, как осуществляется чтение из файла. Пусть файл короткий (что-нибудь, типа Hello, World!), и считываем мы его посимвольно
Видно, что данные уже находятся в буфере. Считывание посимвольно производится уже из буфера.
Функция int feof (FILE * stream); возвращает истину, если конец файла достигнут. Функцию удобно использовать, когда необходимо пройти весь файл от начала до конца. Пусть есть файл с текстовым содержимым text.txt. Считаем посимвольно файл и выведем на экран.
Всё бы ничего, только функция feof работает неправильно. Это связано с тем, что понятие "конец файла" не определено. При использовании feof часто возникает ошибка, когда последние считанные данные выводятся два раза. Это связано с тем, что данные записывается в буфер ввода, последнее считывание происходит с ошибкой и функция возвращает старое считанное значение.
Этот пример сработает с ошибкой (скорее всего) и выведет последний символ файла два раза.
Решение – не использовать feof. Например, хранить общее количество записей или использовать тот факт, что функции fscanf и пр. обычно возвращают число верно считанных и сопоставленных значений.
Примеры
1. В одном файле записаны два числа - размерности массива. Заполним второй файл массивом случайных чисел.
2. Пользователь копирует файл, при этом сначала выбирает режим работы: файл может выводиться как на консоль, так и копироваться в новый файл.
3. Пользователь вводит данные с консоли и они записываются в файл до тех пор, пока не будет нажата клавиша esc. Проверьте программу и посмотрите. как она себя ведёт в случае, если вы вводите backspace: что выводится в файл и что выводится на консоль.
4. В файле записаны целые числа. Найти максимальное из них. Воспользуемся тем, что функция fscanf возвращает число верно прочитанных и сопоставленных объектов. Каждый раз должно возвращаться число 1.
Другое решение считывать числа, пока не дойдём до конца файла.
5. В файле записаны слова: русское слово, табуляция, английское слово, в несколько рядов. Пользователь вводит английское слово, необходимо вывести русское.
Файл с переводом выглядит примерно так
солнце sun
карандаш pen
шариковая ручка pencil
дверь door
окно windows
стул chair
кресло armchair
и сохранён в кодировке cp866 (OEM 866). При этом важно: последняя пара cлов также заканчивается переводом строки.
Алгоритм следующий - считываем строку из файла, находим в строке знак табуляции, подменяем знак табуляции нулём, копируем русское слово из буфера, копируем английское слово из буфера, проверяем на равенство.
6. Подсчитать количество строк в файле. Будем считывать файл посимвольно, считая количество символов '\n' до тех пор, пока не встретим символ EOF. EOF – это спецсимвол, который указывает на то, что ввод закончен и больше нет данных для чтения. Функция возвращает отрицательное значение в случае ошибки.
ЗАМЕЧАНИЕ: EOF имеет тип int, поэтому нужно использовать int для считывания символов. Кроме того, значение EOF не определено стандартом.
void setbuf(FILE * stream , char * buf );
void setbuffer(FILE * stream , char * buf , size_t size );
void setlinebuf(FILE * stream );
int setvbuf(FILE * stream , char * buf , int mode , size_t size );
ОПИСАНИЕ
Существует три типа буферизации: нулевая буферизация (ее отсутствие), буферизация блока и буферизация строки. Когда выводимые данные не буферизируются, то информация незамедлительно оказывается на терминале (или в файле назначения); когда производится буферизация блока, то сохраняется большое количество символов (их блок), при строчной буферизации все символы сохраняются в буфере до перевода строки. Функция fflush (3) принуждает закончить буферизацию блока раньше (см. fclose (3)). Обычно все файлы буферизуются блоком. При операции I/O над файлом производится вызов malloc (3), затем принимается буфер. Если поток ссылается на терминал (как это делает stdout ), то первый буферизируется построчно. Стандартный поток ошибок stderr по умолчанию никогда не буферизуется.Функция setvbuf может быть использована над любым открытым потоком для изменения типа буферизации. Параметр mode должен быть одним из трех следующих макросов: _IONBF (отключить буферизацию); _IOLBF (строчная буферизация); _IOFBF (блочная буферизация).
Аргумент buf должен указывать на буфер (кроме случаев, когда буферизация отключается) размером, как минимум, равным size байтам; и этот буфер будет использоваться вместо текущего. Если аргумент buf равен NULL , то это отразится только на буферизации; при следующей операции считывания или записи будет создан новый буфер. Функция setvbuf может быть использована после открытия потока и до выполнения над ним любых операций.
Следующие три вызова идентичны по сути вызову setvbuf . Функция setbuf в точности соответствует вызову
setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ);
Функция setbuffer также аналогична ей, но размер буфера в данном случае определяется вызовом, а не размером по умолчанию BUFSIZ . Функция setlinebuf полностью идентична вызову
setbuf, setbuffer, setlinebuf
ОБЗОР
Требования макроса тестирования свойств для glibc (см. feature_test_macros(7)):
setbuffer(), setlinebuf(): _BSD_SOURCE
ОПИСАНИЕ
Существует три типа буферизации: отсутствие буферизации, блочная буферизация и строковая буферизация. Когда выходной поток не буферизируется, то информация незамедлительно оказывается на терминале или в файле назначения; при блочной буферизации сначала сохраняется большое количество символов, а затем они записываются за один раз; при строковой буферизации все символы сохраняются в буфере до появления символа перевода строки при выводе или при вводе из любого потока, подключённого к устройству терминала (обычно, stdin). Функцию fflush(3) можно использовать для досрочного окончания блочной буферизации (смотрите fclose(3)).Обычно, для файлов включена блочная буферизация. Если поток ссылается на терминал (обычно для stdout), то он буферизируется построчно. Стандартный поток ошибок stderr по умолчанию никогда не буферизируется.
Функция setvbuf() может быть использована для изменения типа буферизации любого открытого потока. Параметр mode должен быть одним из трёх следующих макросов:
_IONBF отключить буферизацию _IOLBF строковая буферизация _IOFBF полная буферизация
За исключением небуферизованных файлов аргумент buf должен указывать на буфер размером, как минимум, size байт; этот буфер будет использоваться вместо текущего. Если аргумент buf равен NULL, то это отразится только на типе буферизации; при следующей операции чтения или записи будет создан новый буфер. Функция setvbuf() может быть использована только после открытия потока и до выполнения над ним любых операций.
Остальные три вызова, фактически, являются псевдонимами вызова setvbuf(). Функция setbuf() в точности соответствует вызову
setvbuf(stream, buf, buf ? _IOFBF : _IONBF, BUFSIZ);
Функция setbuffer() также аналогична ей, но размер буфера в данном случае определяется вызывающим, а не размером по умолчанию BUFSIZ. Функция setlinebuf() полностью идентична вызову
setvbuf(stream, NULL, _IOLBF, 0);
ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ
Функция setvbuf() возвращает 0 при нормальном завершении работы. При ошибках она возвращает ненулевое значение (некорректное значение mode или запрос невозможно выполнить). При ошибках может быть соответственно изменено значение errno.Другие функции не возвращают никаких значений.
АТРИБУТЫ
Описание терминов данного раздела смотрите в attributes(7).Интерфейс | Атрибут | Значение |
setbuf(), setbuffer(), setlinebuf(), setvbuf() | безвредность в нитях | безвредно (MT-Safe) |
СООТВЕТСТВИЕ СТАНДАРТАМ
Функции setbuf() и setvbuf() соответствуют стандарту C89 и C99.ДЕФЕКТЫ
Вы должны удостовериться, что пространство, на которое указывает buf, всё ещё существует, в то время как stream закрыт, что обычно случается при закрытии программы. Например, следующее будет неправильным:
В данном коде fwrite (в моём конкретном тестовом случае: построение патча между близкими 300 Мб файлами, входные данные полностью в памяти) вызывается миллионы раз с буфером малого размера. Очевидно, что штука данная будет тормозить, и потому хотелось бы как-то повлиять на это безобразие. Внедрять разного рода источники данных, асинхронный ввод-вывод пока нет желания, хотелось найти решение проще. Первое, что пришло в голову — увеличить размер буфера
но существенного улучшения результата я не получил (теперь на fwrite приходилось около 37% времени) — значит дело всё же не в частой записи данных на диск. Заглянув «под капот» fwrite можно увидеть, что внутри происходит lock/unlock FILE структуры примерно так (псевдокод, весь анализ проводился под Visual Studio 2017):
fwrite, _fwrite_nolock, setvbuf
Давайте абстрагируемся от оригинального проекта и займёмся тестированием конкретного случая: записи большого файла (512 Мб) предельно малыми порциями — в 1 байт. Тестовая система: AMD Ryzen 7 1700, 16 Гб ОЗУ, HDD 3.5" 7200 rpm 64 Мб кэша, Windows 10 1809, бинарь строился 32-х битный, оптимизации включены, библиотека статически прилинкована.
Сэмпл для проведения эксперимента:
В качестве переменных будут выступать TEST_BUFFER_SIZE, а также для пары случаев заменим fwrite_unlocked на fwrite. Начнём со случая fwrite без явной установки размера буфера (закомментируем setvbuf и связанный код): время 27048906 мкс, скорость записи — 18.93 Мб/с. Теперь установим размер буфера в 64 Кб: время — 25037111 мкс, скорость — 20.44 Мб/с. Теперь протестируем работу _fwrite_nolock без вызова setvbuf: 7262221 мкс, скорость — 70.5 Мб/с!
Дальше поэкспериментируем с размером буфера (setvbuf):
Данные получены усреднением 5 экспериментов, погрешности считать я поленился. Как по мне, 93 Мб/с при записи по 1 байту на обычный HDD — это очень неплохой результат, всего-то надо выбрать оптимальный размер буфера (в моём случае 256 Кб — в самый раз) и заменить fwrite на _fwrite_nolock/fwrite_unlocked (в случае, если не нужна потокобезопасность, разумеется).
Аналогично с fread в подобных условиях. Теперь посмотрим, как обстоят дела на Linux, тестовая конфигурация такая: AMD Ryzen 7 1700X, 16 Гб ОЗУ, HDD 3.5" 7200 rpm 64 Мб кэша, ОС OpenSUSE 15, GCC 8.3.1, тестировать будем x86-64 бинарь, файловая система на тестовом разделе ext4. Результат fwrite без явной установки размера буфера в данном тесте 67.6 Мб/с, при установке буфера в 256 Кб скорость увеличилась до 69.7 Мб/c. Теперь проведём аналогичные замеры для fwrite_unlocked — результаты тут 93.5 и 94.6 Мб/с соответственно. Варьирование размера буфера от 1 Кб до 8 Мб привело меня к следующим выводам: увеличение буфера увеличивает скорость записи, но разница в моём случае составила всего 3 Мб/с, различий в скорости между буфером в 64 Кб и 8 Мб не заметил вовсе. Из полученных на данной Linux машине данных можно сделать следующие выводы:
- fwrite_unlocked работает быстрее, чем fwrite, но разница в скорости записи не столь велика, как на Windows
- Размер буфера на Linux не оказывает столь существенного влияния на скорость записи через fwrite/fwrite_unlocked, как на Windows
Итого предложенный метод эффективен как на Windows, но и на Linux (хоть и в существенно меньшей мере).
Читайте также: