Операционная система MS-DOS© Александр Фролов, Григорий ФроловТом 1, книга 3, М.: Диалог-МИФИ, 1992. 3.8. Буферизация ввода/выводаВвод/вывод для дисков в операционной системе MS-DOS буферизован. Это означает, что данные не сразу записываются на диск, а накапливаются в специальном массиве (буфере). По мере заполнения буфер сбрасывается на диск. При чтении информация заполняет весь входной буфер, независимо от количества байтов, которые программа читает из файла. В дальнейшем, если программе потребуются данные, которые уже были считаны с диска и записаны во входной буфер, она получит данные непосредственно из этого буфера. Обращения к диску при этом не будет. Буферизация сокращает затраты времени на ввод/вывод, особенно в тех случаях, когда программе периодически требуется одни и те же участки файлов. При копировании файлов буферизация сокращает время на перемещение головок от исходного файла к результирующему и обратно, причем эффект получается тем больше, чем больше размер используемого буфера. Операционная система MS-DOS имеет несколько буферов. Их количество зависит от оператора BUFFERS, находящегося в файле CONFIG.SYS. Этот оператор позволяет определить от 2 до 99 буферов. Если файл CONFIG.SYS не содержит оператора BUFFERS, по умолчанию используются два буфера. При увеличении количества буферов увеличивается вероятность того, что нужная часть файла уже считана и находится в оперативной памяти. Однако необходимо учитывать, что для хранения буферов расходуется основная оперативная память. Кроме того, с ростом количества буферов увеличивается время, необходимое операционной системе на анализ состояния буферов, что может привести к снижению производительности. Значительное снижение скорости работы наступает при количестве буферов порядка 50. Обычно для машин класса AT с диском размером 20-40 мегабайтов рекомендуется использовать 32 буфера, однако для каждого конкретного случая может потребоваться подбор этого параметра для оптимизации производительности системы. Если ваша программа интенсивно использует обращение к каталогам файловой системы, вы можете использовать утилиту MS-DOS FASTOPEN, которая запоминает в оперативной памяти расположение на диске файлов и каталогов, уменьшая интенсивность обращения к диску. Например, при использовании следующей команды в оперативной памяти будет храниться информация о расположении максимально о 100 файлах и каталогах: FASTOPEN c:=100 В операционной системе MS-DOS версии 4.0 вы можете указать для утилиты FASTOPEN опцию /X. Эта опция вызывает размещение информации о файлах и каталогах в дополнительной (expanded) памяти. Для этой версии операционной системы вызов утилиты FASTOPEN лучше всего выполнять через оператор INSTALL файла CONFIG.SYS: INSTALL=C:\DOS\FASTOPEN d: =(n,m) ... [/X] В приведенной выше строке используются
следующие обозначения:
Еще один способ организовать буферизацию ввода/вывода для жестких дисков - использовать драйвер SMARTDRV.SYS. Этот драйвер позволяет создать для диска кэш-память в расширенной или дополнительной памяти. Например, следующая строка в файле CONFIG.SYS определяет дисковый кэш размером 530 килобайтов, размещенный в расширенной памяти: device=c:\dos\smartdrv.sys 530 Кэш-память особенно эффективна при работе с базами данных, когда вам периодически требуется одна и та же информация. Если создать кэш-память достаточно большого размера, можно значительно сократить количество обращений к диску. Если вы используете кэш-память для диска, не следует задавать оператор BUFFERS в файле CONFIG.SYS или пользоваться утилитой FASTOPEN, так как это приведет к многократной буферизации и вызовет излишние пересылки данных в оперативной памяти. Буферизация данных имеет и свои недостатки. Если в результате аварии в питающей сети или по какой-то другой причине компьютер выключился, то информация, хранящаяся в буферах и не записанная на диск, будет потеряна. При закрытии файла все буфера, связанные с ним,
сбрасываются на диск. Если вам надо сбросить
буфера, не закрывая файл, это можно сделать с
помощью функции 68h прерывания INT 21h:
Дополнительно обновляется дескриптор файла в каталоге, а именно поля времени, даты и размера файла. Обратите также внимание на функцию расширенного открытия файлов 6Ch, входящую в состав MS-DOS версии 4.0. Эта функция позволяет при открытии файла отменить буферизацию. Стандартные библиотеки трансляторов Microsoft QC 2.5 и C 6.0 содержат многочисленные функции, использующие собственный механизм буферизации при работе с файлами. Их часто называют функциями потокового ввода/вывода. Такую буферизацию не следует путать с буферизацией, выполняемой операционной системой. Имена всех этих функций начинаются на f - fopen(), fclose(), fprintf() и т.д. Функции потокового ввода/вывода хорошо описаны во многих учебных пособиях по языку программирования Си, поэтому мы приведем лишь краткий обзор этих функций, делая акцент на особенностях их применения. При использовании функций потокового ввода/вывода файлы открываются функцией fopen(), закрываются функцией fclose(). Эти функции не только открывают и закрывают файлы (получают и освобождают их файловый индекс), но и, соответственно, создают и уничтожают структуру типа FILE, описанную в файле stdio.h и связанную с данным файлом: extern FILE { char *_ptr; // положение текущего символа int _cnt; // количество оставшихся байтов char *_base; // адрес буфера char _flag; // флаг управления доступом char _file; // файловый индекс } _NEAR _CDECL _iob[]; Для организации потокового ввода/вывода вначале необходимо при помощи функции fopen() открыть файл. Функция fopen() имеет следующий прототип: FILE *fopen(char *filename, char *mode); Первый параметр указывает на строку,
содержащую путь открываемого файла, второй - на
строку режима открытия файла. Возможны следующие
режимы:
К буквам r, w, a справа могут добавляться буквы t и b. Буква t означает, что файл будет открыт в текстовом режиме, b - в двоичном. Для двоичного режима не производится обработка таких символов, как конец строки, конец файла и т.д. Строка режима открытия файла может дополнительно содержать символ '+'. Этот символ означает, что для файла разрешены операции чтения и записи одновременно. Для закрытия файлов, открытых для ввода/вывода потоком, должна использоваться функция fclose(): int fclose(FILE *stream); При закрытии файла освобождаются и сбрасываются на диск все буфера, распределенные этому файлу. Если вы открыли файл с помощью низкоуровневой функции open(), вы можете создать поток для этого файла, используя функцию fdopen(): FILE *fdopen(int handle, char *mode); В качестве первого параметра используется файловый индекс, полученный от функции open(), второй параметр аналогичен парметру mode для функции fopen(). Для того чтобы закрыть поток, созданный функцией fdopen(), необходимо использовать функцию fclose(), а не close(). Для открытого потока вы можете узнать файловый индекс с помощью функции fileno(): int fileno(FILE *stream); Функция возвращает файловый индекс, связанный с данным потоком. Существуют потоки, соответствующие
стандартным устройствам ввода, вывода, вывода
сообщений об ошибках, стандартное устройство
последовательного ввода/вывода и стандартное
устройство печати. Для использования этих
потоков не требуются процедуры открытия и
закрытия. Для работы с потоками,
соответствующими стандартным устройствам
ввода/вывода, в функциях необходимо указывать:
Для работы со стандартными устройствами ввода/вывода в библиотеках трансляторов для языка программирования Си имеется соответствующий набор функций, которые должны быть вам хорошо известны - printf(), scanf(), putchar() и т.д.. Мы не будем их описывать для экономии места. Приведем обзор функций, предназначенных для потокового ввода/вывода. Для записи данных в поток предназначена функция fwrite(): size_t fwrite(void *buffer, size_t size, size_t count, FILE *stream); Эта функция записывает в файл stream блоки информации, каждый из которых имеет длину size байтов. Количество блоков - count. Данные для записи расположены по адресу buffer. Если файл открыт в текстовом режиме, каждый символ возврата каретки CR заменяется на два символа - возврата каретки CR и перевода строки LF. Функция возвращает количество действительно записанных блоков информации, без учета замены символа CR в текстовом режиме. Чтение данных потоком можно выполнить с помощью функции fread(): size_t fread(void *buffer, size_t size, size_t count, FILE *stream); Эта функция используется аналогично предыдущей. Для распознавания конца файла и обнаружения ошибок после вызова этой функции необходимо использовать функции feof() и ferror(). Если при использовании функции fread() вы задали значения параметров size или count, равные нулю, функция fread() не изменяет содержимое буфера buffer. Для позиционирования внутри файла, открытого потоком с помощью функции fopen(), предназначена функция fseek(): int fseek(FILE *stream, long offset, int origin); В этой функции параметр offset задает новое
содержимое указателя текущей позиции в файле stream,
а параметр origin - определяет способ задания
новой позиции. Этот оператор может иметь
значения, аналогичные используемым в функции
lseek():
При открытии файла указатель текущей позиции устанавливается на начало файла. Операции ввода/вывода вызывают увеличение значения этого указателя на количество прочитанных/записанных байтов. Функция fseek() позволяет вам установить указатель за конец файла, однако при попытке установаит указатель до начала файла функция возвратит признак ошибки - ненулевое значение. При использовании функции fseek() для позиционирования внутри файлов, открытых в текстовом режиме, необходимо учитывать особенность обработки текстовых файлов - автоматическую замену символа возврата каретки CR на пару символов: возврат каретки CR и перевод строки LF. Для текстовых файлов функция fseek() будет правильно работать только в следующих двух случаях:
Функция ftell() возвращает текущее значение указателя позиции для файла, или -1 при ошибке: long ftell(FILE *stream); Пара функций ftell() - fseek() позволит вам правильно организовать позиционирование для файлов, открытых в текстовом режиме. Есть еще одна возможность организовать позиционирование внутри файлов, открытых потоком - использовать пару функций fgetpos() - fsetpos(): int fgetpos(FILE *stream, fpos_t *pos); int fsetpos(FILE *stream, fpos_t *pos); Эти две функции используют для запоминания и установки позиции переменную с типом fpos_t, определенным в файле stdio.h. Функция fgetpos() записывает в эту переменную текущую позицию в потоке stream. Содержимое переменной затем может быть использовано для установки позиции в потоке с помощью функции fsetpos(). Обе эти функции возвращают нулевое значение в случае успешного завершения работы, или ненулевое - при ошибке. Среди функций потокового ввода/вывода можно выделить группу функций форматного ввода/вывода. Это такие функции, как fputc(), fgetc(), fputs(), fgets(), fprintf(), fscanf(). Функции форматного ввода/вывода сильно облегчают запись/чтение таких элементов данных, как отдельные байты, текстовые строки, числа в различных форматах. Для записи в поток отдельных байтов используется функция fputc(): int fputc(int c, FILE *stream); Байт c записывается в файл stream начиная с текущей позиции. После записи текущая позиция увеличивается на единицу. Функция возвращает записываемый байт или значение EOF, которое служит признаком ошибки. Для побайтового чтения содержимого файла, открытого потоком, удобно использовать функцию fgetc(): int fgetc(FILE *stream); Эта функция возвращает считанный из файла и преобразованный к типу int байт из потока stream. После чтения байта текущая позиция в файле увеличивается на единицу. При достижении конца файла или в случае ошибок функция fgetc() возвращает значение EOF. Однако для проверки на ошибку или конец файла лучше пользоваться специальными функциями ferror() и feof(). Если вы открыли файл в двоичном режиме, единственный способ определить момент достижения конца файла - использовать функцию feof(), так как значение константы EOF может находиться в любом месте двоичного файла. Для работы со строками предназначены функции fputs() и fgets(). Функция fputs() предназначена для вывода строки в файл, открытый потоком: int fputs(char *string, FILE *stream); Первый параметр определяет выводимую строку, второй - файл, в который эта строка выводится. Двоичный ноль, закрывающий строку, в выходной файл не копируется. После вывода строки содержимое текущего указателя позиции увеличивается на количество записанных байтов. Для ввода строк из текстового файла удобна функция fgets(): int fgets(char *string, int n, FILE *stream); Функция читает байты из потока stream и записывает их в буфер string до тех пор, пока не произойдет одно из двух событий - будет прочитан символ новой строки '\n' или количество прочитанных символов не будет равно n-1. После того, как байты будут прочитаны в буфер, в конец образованной из этих байтов строки функция записывает двоичный ноль. Если был прочитан символ новой строки '\n', он тоже записывается в буфер. Для анализа достижения конца файла или ошибок необходимо использовать функции feof() и ferror(). Для форматного вывода в файл содержимого переменных удобно использовать функцию fprintf(): int fprintf(FILE *stream, char *format [,arg]...); Эта функция аналогична хорошо известной вам функции форматного вывода на экран printf(), с которой обычно начинают изучение языка программирования Си. Вспомните такую программу: #include <stdio.h> main() { printf("Hello, world!"); } Функция fprintf() имеет дополнительно один параметр - stream, который определяет выходной файл. После завершения работы функция возвращает количество записанных байтов или отрицательную величину, если при записи произошла ошибка. Для форматного ввода информации из файла можно использовать функцию fscanf(), аналогичную известной вам функции scanf(): int fscanf(FILE *stream, char *format [,arg]...); Эта функция читает данные, начиная с текущей позиции в потоке stream, в переменные, определенные аргументами arg. Каждый аргумент должен являться указателем на переменную, соответствующую типу, определенному в строке формата format. Функция fscanf() возвращает количество успешно считанных и преобразованных в указанный формат полей. Те поля, которые были считаны, но не преобразовывались, в возвращаемом функцией значении не учитываются. При достижении конца файла функция возвращает значение EOF. Если функция возвратила нулевое значение, это означает, что преобразование полей не производилось. Рассмотрим теперь функции, управляющие буферизацией для потокового ввода/вывода. Функция setbuf() позволяет вам заменить системный буфер на свой собственный: void setbuf(FILE *stream, char *buffer); Параметр buffer должен указывать на подготовленный пользователем массив, имеющий размер BUFSIZ байтов. Константа BUFSIZ описана в файле stdio.h. Функция setvbuf() позволяет программе не только указать свой буфер, но и задать его размер: int setvbuf(FILE *stream, char *buffer, int mode, size_t size); Параметр stream должен указывать на открытый поток, причем для этого потока до вызова функции setvbuf() нельзя выполнять операции чтения/записи. Параметр buffer должен указыват на подготовленный программой буфер размером size байтов. Этот буфер будет использоваться для работы с потоком stream. Параметр mode может иметь значения _IOFBF, _IOLBF, _IONBF. Если mode равно _IOFBF или _IOLBF, параметр size указывает размер буфера. Если параметр mode равен _IONBF, буферизация не используется, парметры buffer и size игнорируются. Параметры _IOFBF и _IOLBF эквивалентны. Если в качестве адреса буфера buffer задать значение NULL, функция автоматически закажет буфер размером size. Функция setvbuf() возвращает ноль при успешном завершении и ненулевую величину, если указан неправильный парметр mode или неправильный размер буфера size. Для чего может понадобиться изменение размера буфера? Главным образом - для сокращения времени, необходимого для позиционирования магнитных головок при выполнении операций одновременно над несколькими файлами, например, при копировании файлов, слиянии нескольких файлов в один и т.д. При закрытии потока функцией fclose() содержимое буфера записывается на диск. Если программе необходимо выполнить запись содержимого буфера на диск без закрытия файла, она может воспользоваться функцией fflush(): int fflush(FILE *stream); Эта функция возвращает ноль при успешной записи буфера на диск, а так же в тех случаях, когда поток либо совсем не имеет буферов, либо открыт только для чтения. При ошибке возвращается значение EOF. Если поток открыт только для чтения, функция fflush() очищает содержимое буфера, связанного с этим потоком. В качестве примера приведем текст программы, копирующей содержимое текстового файла. Программа копирует этот файл три раза. В первый раз одна использует буфер стандартного размера, затем увеличивает размер буфера в десять раз, и, наконец, копирует файл без использования механизма буферизации. Каждый раз программа измеряет продолжительность копирования файла с помощью функции clock(), входящей в состав стандартных библиотек трансляторов Microsoft QC 2.5 и C 6.0. #include <stdio.h> #include <stdlib.h> #include <time.h> void filecpy(FILE *stream_from, FILE *stream_to); // Буфера для файлов char buf1[BUFSIZ * 10]; char buf2[BUFSIZ * 10]; void main(int argc, char *argv[]) { time_t start, end; FILE *stream_from, *stream_to; if(argc < 3) { printf("Задайте имена файлов!\n"); exit(-1); } // Открываем файлы и используем для копирования // буфер стандартного размера if((stream_from = fopen(argv[1], "rt")) == NULL) { printf("Задайте имя входного файла!\n"); exit(-1); } stream_to = fopen(argv[2], "wt+"); // Определяем время начала копирования start = clock(); // Выполняем копирование файла filecpy(stream_from,stream_to); // Определяем время завершения копирования end = clock(); // Выводим время копирования при использовании // буферов стандартного размера printf("Время копирования: %5.1f Размер буфера, байтов: %d\n", ((float)end - start) / CLK_TCK, BUFSIZ); // Задаем свой буфер большего размера if((stream_from = fopen(argv[1], "rt")) == NULL) exit(-1); stream_to = fopen(argv[2], "wt+"); // Устанавливаем буфера как для входного, // так и для выходного файлов setvbuf(stream_from, buf1, _IOFBF, sizeof(buf1)); setvbuf(stream_to, buf2, _IOFBF, sizeof(buf2)); // Копируем файл и измеряем продолжительность // копирования start = clock(); filecpy(stream_from,stream_to); end = clock(); printf("Время копирования: %5.1f Размер буфера: %d\n", ((float)end - start) / CLK_TCK, BUFSIZ * 10); // Копируем без использования буферизации if((stream_from = fopen(argv[1], "rt")) == NULL) exit(-1); stream_to = fopen(argv[2], "wt+"); setvbuf(stream_from, NULL, _IONBF, 0); setvbuf(stream_to, NULL, _IONBF, 0); start = clock(); filecpy(stream_from,stream_to); end = clock(); printf("Время копирования: %5.1f Буферизация не используется\n", ((float)end - start) / CLK_TCK); exit(0); } // Функция для копирования файлов void filecpy(FILE *stream_from, FILE *stream_to) { char linebuf[256]; // Цикл копирования. Условие выхода из цикла - // конец входного файла while(!feof(stream_from)) { // Читаем в буфер linebuf одну строку if(fgets(linebuf, 255, stream_from) == NULL) break; // Записываем содержимое буфера linebuf // в выходной файл if(fputs(linebuf, stream_to) != 0) break; } // Закрываем входной и выходной файлы fclose(stream_from); fclose(stream_to); } |