[ Страница назад | Страница вперед | Содержание | Индекс | Библиотека | Юридическая информация | Поиск ]

Программирование: Разработка и отладка программ


Работа с файлами

Во всех операциях ввода-вывода используется смещение указателя в файле, которое хранится в таблице открытых файлов (см. Таблица открытых файлов и таблица дескрипторов файлов). Это смещение измеряется в байтах и отслеживается для каждого открытого файла. Смещение указателя в файле иногда называют текущим смещением ввода-вывода, поскольку оно определяет позицию в файле, начиная с которой будут считываться или записываться данные. Функция open помещает указатель в начало файла. Изменить положение указателя можно с помощью функции lseek.

Дополнительные сведения о работе с файлами приведены в следующих разделах:

Изменение положения указателя в файле

Данные считываются из файла и записываются в файл последовательно. Это связано с тем, что после выполнения каждой операции ввода-вывода смещение указателя в файле сохраняется. Оно хранится в таблице открытых файлов.

В файлах с прямым доступом, например, в обычных и особых файлах, положение указателя в файле можно изменить с помощью функции lseek.

lseek Эта функция позволяет поместить указатель в заданную позицию файла. Позиция указателя в файле определяется переменной Смещение. Значение Offset можно задавать относительно следующих трех позиций в файле (текущий способ указывается в переменной Точка отсчета):

Абсолютное смещение
Смещение относительно начала файла

Относительное смещение
Смещение относительно прежнего положения указателя

Конец файла
Смещение относительно конца файла

Функция lseek возвращает текущее положение указателя в файле. Например:

cur_off= lseek(fd, 0, SEEK_CUR);

Результат выполнения lseek сохраняется в таблице открытых файлов. Все последующие операции чтения и записи будут выполняться с учетом нового положения указателя в файле.

Примечание: для каналов и сокетов положение указателя изменять нельзя.

fclear Эта функция создает в файле пустое пространство. Она обнуляет область файла, размер которой задается переменной Число байт, начиная с текущей позиции указателя в файле. Если при открытии файла был установлен флаг O_DEFER, то функция fclear будет недоступна.

Чтение файла


read Эта функция копирует указанное число байт из открытого файла в заданный буфер. Копирование начинается с текущей позиции указателя в файле. Число байт и буфер указываются в параметрах число-байт и буфер соответственно.

Процедура read выполняет следующие действия:

  1. Проверяет правильность дескриптора, указанного в параметре дескриптор-файла, и наличие у процесса прав на чтение. После этого она обращается к записи таблицы открытых файлов, соответствующей данному дескриптору файла.
  2. Устанавливает в файле флаг, указывающий, что выполняется операция чтения. Это гарантирует, что другие процессы не будут обращаться к файлу во время чтения.
  3. Преобразует значение указателя в файле и значение параметра Число_байт в адрес блока.
  4. Копирует содержимое блока в буфер.
  5. Копирует содержимое буфера в область памяти, на которую указывает переменная Буфер.
  6. Сдвигает указатель в файле на число прочитанных байт. Это гарантирует последовательное считывание данных.
  7. Вычитает количество прочитанных байт из значения переменной Число_байт.
  8. Повторяет те же действия до тех пор, пока не будут прочитаны все данные.
  9. Возвращает общее количество прочитанных байт.

Операция завершается, если файл пустой, прочитаны все запрошенные данные или произошла ошибка.

Ошибки могут возникнуть при чтении файла с диска или при копировании данных в файловое пространство системы.

Рекомендуется начинать чтение с начала блока данных и считывать целое число блоков. В этом случае операция чтения не будет выполнять лишние действия. Если процесс считывает несколько последовательных блоков, то операционная система предполагает, что дальше он также будет считывать данные по порядку.

I-узел блокируется на время операции чтения. Это означает, что во время чтения файла другие процессы не могут его изменять. Сразу после завершения чтения блокировка снимается. Если другой процесс изменит файл между двумя операциями чтения, то результат чтения будет разным, однако целостность данных будет сохранена.

В следующем примере показано, как с помощью функции read можно подсчитать число нулевых байтов в файле foo:

#include <fcntl.h>
#include <sys/param.h>
 
main()
{
        int      fd;
        int nbytes;
        int nbytes;
        int nnulls;
        int i;
        char buf[PAGESIZE];      /*Рекомендуемый размер буфера*/
        nnulls=0;
        if ((fd = open("foo",O_RDONLY)) < 0)
                exit();
        while ((nbytes = read(fd,buf,sizeof(buf))) > 0)
                for (i = 0; i < nbytes; i++)
                        if (buf[i] == '\0';
                                  nnulls++;
        printf("Пустых символов: %d\n", nnulls);
}

Запись данных в файл


write Эта функция считывает данные, объем которых задается переменной число-байт, из области памяти, на которую указывает переменная буфер, и добавляет их в файл с дескриптором дескриптор-файла. При этом выполняется примерно та же последовательность действий, что и для операции read. Текущее значение указателя в файле считывается из таблицы открытых файлов.

При записи данных в файл может возникнуть ситуация, когда в файле отсутствует блок, соответствующий текущему значению указателя. В такой ситуации функция write добавляет в файл еще один блок. Информация о нем заносится в описание i-узла, связанного с файлом. Если при добавлении нового блока был создан косвенный блок (i_rindirect), то функция добавляет сразу несколько блоков.

I-узел блокируется на время записи. Это означает, что во время записи другие процессы не могут изменять файл. Сразу после завершения записи блокировка снимается. Если другой процесс изменит файл между двумя операциями записи, то результат записи будет различным, однако целостность данных будет сохранена.

Функция write выполняет тот же цикл операций, что и функция read. За один цикл она записывает на диск один блок данных. Иногда требуется записать только часть блока. В этом случае функция write считывает блок с диска, чтобы сохранить данные, которые хранились в нем ранее. Если нужно записать целый блок данных, то старое содержимое блока не сохраняется, а заменяется целиком. Блоки последовательно записываются на диск до тех пор, пока объем записанной информации не станет равен указанному числу-байт.

Отложенная запись

Для включения режима отложенной записи нужно указать флаг O_DEFER. В этом случае данные записываются на диск как временный файл. Процедура отложенной записи сохраняет данные в кэше, что позволяет быстрее обрабатывать последующие обращения к этим же данным. Механизм отложенной записи сокращает число обращений к диску. Многие программы, например, программы работы с электронной почтой и текстовые редакторы, создают временные файлы в каталоге tmp и удаляют их через некоторое время.

Если при открытии файла был указан флаг отложенной записи (O_DEFER), то данные не записываются в постоянную память до тех пор, пока не будет вызвана функция fsync или операция синхронной записи в файл (write) (в случае, если он был открыт с флагом O_SYNC flag). Функция fsync сохраняет на диске все изменения, внесенные в файл. Более подробная информация о флагах O_DEFER и O_SYNC приведена в описании функции open.

Усечение файлов

Функции truncate и ftruncate позволяют изменять длину обычных файлов. Для их выполнения необходимы права на запись в файл. Новый размер файла задается параметром Длина. При этом указанное число байт отсчитывается от начала файла, а не от текущей позиции указателя в файле. Если новая длина меньше текущей длины файла, то усеченные данные удаляются. Если новая длина больше текущей, то дополнительное пространство заполняется нулями. Функция возвращает новое число блоков в файле и обновляет информацию о размере файла.

Использование в программах прямых операций ввода-вывода

Начиная с AIX версии 4.3, приложение может непосредственно считывать и записывать данные файлов JFS и JFS2. В этом разделе рассматриваются сложности, которые могут возникать при использовании этой функции.

Сравнение прямого ввода-вывода и ввода-вывода с использованием кэша

Обычно JFS и JFS2 записывают страницы файла в кэш, расположенный в памяти ядра. При получении запроса на чтение файла JFS и JFS2 считывают данные с диска в кэш (если их там еще нет), а затем копируют из кэша в пользовательский буфер. При получении запроса на запись данные копируются из пользовательского буфера в кэш. На диск данные записываются позже.

Такая стратегия кэширования может быть довольно эффективной при большом числе попаданий в кэш. Кроме того, она поддерживает алгоритмы упреждающего чтения и отложенной записи. Запись в файл выполняется в асинхронном режиме, что дает возможность приложению сразу продолжать работу, а не ждать выполнения запроса на ввод-вывод.

При выполнении операций прямого ввода-вывода пользовательский буфер обменивается данными напрямую с диском, минуя кэш. Операции прямого ввода-вывода для файлов аналогичны операциям прямого ввода-вывода для устройств.

Преимущества прямого ввода-вывода

Основное преимущество прямого ввода-вывода состоит в том, что он позволяет снизить нагрузку на CPU за счет исключения промежуточных операций копирования из кэша в пользовательский буфер. Такой механизм может применяться при небольшом числе попаданий в кэш, когда системе все равно приходится обращаться к диску. Кроме того, прямой ввод-вывод рекомендуется использовать в приложениях с синхронной записью, так как данные будут записываться непосредственно на диск. В обоих случаях нагрузка на CPU будет снижена за счет исключения операций копирования в кэш.

Второе преимущество прямого ввода-вывода заключается в том, что он повышает эффективность кэширования остальных файлов. Каждый запрос на чтение и запись файла конкурирует в кэше с другими запросами. В результате данные других файлов могут быть вытеснены из кэша. Если к данным, добавленным в кэш, обращаются редко, то эффективность работы кэша снижается. Прямой ввод-вывод может применяться для тех файлов, кэширование которых не дает существенных преимуществ. При этом в кэше будут размещаться те файлы, для которых действительно эффективно применять стратегию кэширования.

Влияние прямого ввода-вывода на производительность

Хотя прямой ввод-вывод снижает нагрузку на CPU, средняя продолжительность выполнения операций возрастает, особенно при выполнении небольших запросов. Такие издержки связаны с существенным различием между прямым вводом-выводом и операциями с участием кэша.

Чтение напрямую с диска

Чтение напрямую с диска выполняется синхронно, в отличие от обычной стратегии кэширования, когда данные копируются на диск из кэша. Такой способ чтения оказывается крайне неэффективным в тех случаях, когда данные находятся в оперативной памяти в соответствии со стратегией кэширования.

Прямой ввод-вывод не поддерживает алгоритм упреждающего чтения JFS и JFS2. Этот алгоритм позволяет значительно повысить производительность последовательных операций чтения файла за счет увеличения объема считываемых данных и выполнения чтения параллельно с работой приложения.

Для того чтобы компенсировать отсутствие алгоритма упреждающего чтения, рекомендуется запрашивать чтение большого объема данных. Для того чтобы производительность чтения напрямую с диска была сравнима с производительностью алгоритма упреждающего чтения, объем одновременно считываемых данных должен быть не меньше 128 Кб.

Кроме того, алгоритм упреждающего чтения можно реализовать в самом приложении путем отправки асинхронных запросов на чтение данных с запасом напрямую с диска. Это можно сделать путем использования нескольких нитей, либо с помощью функции aio_read.

Запись напрямую на диск

Запись напрямую на диск выполняется синхронно, в отличие от обычной стратегии кэширования, когда данные копируются в кэш и лишь затем записываются на диск. Это отличие может привести к значительному снижению скорости работы приложений, в которых обычные операции ввода-вывода были заменены на прямой ввод-вывод.

Конфликт режимов доступа к файлу

Для того чтобы избежать конфликтов между программами, применяющими прямой ввод-вывод и ввод-вывод с использованием кэша, операции прямого ввода-вывод всегда выполняются только в исключительном режиме. Если файл открыт несколькими процессами, причем некоторые из них выполняют прямой ввод-вывод, а некоторые используют кэш, то сохраняется обычный режим чтения-записи с помощью кэша. Данные будут напрямую считываться и записываться на диск только в том случае, если все процессы выполняют операции прямого ввод-вывода.

Аналогично, файл останется в режиме ввода-вывода с помощью кэша, если он размещен в виртуальной памяти с помощью вызова shmat или mmap.

В случае конфликта JFS и JFS2 пытаются перевести файл в режим прямого ввода-вывода. Ввод-вывод с помощью кэша при этом запрещается (с помощью close, munmap или shmdt). Переход в режим прямого ввода-вывода связан с дополнительными операциями, поскольку все страницы файла должны быть удалены из оперативной памяти, а измененные страницы - записаны на диск.

Включение режима прямого ввода-вывода

Для того чтобы включить режим прямого ввода-вывода для файла, нужно передать в файл fcntl.h флаг O_DIRECT. Информация об этом флаге приведена в описании функции open. Для просмотра определения O_DIRECT нужно откомпилировать приложение с опцией _ALL_SOURCE.

Требования к смещению, размеру и адресу целевого буфера

Для того чтобы запросы на прямой ввод-вывод обрабатывались эффективно, они должны удовлетворять некоторым условиям. Приложения могут получить информацию о требованиях к смещению, размеру и адресу с помощью функций finfo и ffinfo. При работе с командой FI_DIOCAP, функции finfo и ffinfo возвращают структуру diocapbuf, описанную в файле sys/finfo.h. Эта структура содержит следующие поля:

dio_offset Указывает рекомендуемую границу для выравнивания смещения в случае прямой записи в файл
dio_max Указывает рекомендуемый максимальный объем данных, записываемых в файл напрямую
dio_min Указывает рекомендуемый минимальный объем данных, записываемых в файл напрямую
dio_align Указывает рекомендуемую границу для выравнивания буфера в случае прямой записи в файл

Отклонение от рекомендуемых значений может привести к тому, что чтение и запись будут выполняться в обычном режиме с кэшированием. В разных файловых системах к этим значениям предъявляются различные требования.

Формат файловой системы dio_offset dio_max dio_min dio_align
фиксированные блоки по 4 Кб 4 Кб 2 Мб 4 Кб 4 Кб
фрагментированная 4 Кб 2 Мб 4 Кб 4 Кб
сжатая н/д н/д н/д н/д
с поддержкой больших файлов 128 Кб 2 Мб 128 Кб 4 Кб

Недостатки прямого ввода-вывода

Прямой ввод-вывод не поддерживается для файлов, расположенных в сжатых файловых системах. Если при открытии файла будет задан флаг O_DIRECT, то он будет проигнорирован. Файл будет открыт в обычном режиме ввода-вывода с кэшированием.

Прямой ввод-вывод и целостность данных

Несмотря на то, что запись напрямую на диск выполняется синхронно, она не обеспечивают целостность данных в той мере, которая требуется в стандарте POSIX. Если в приложении предъявляются строгие требования к целостности данных, при открытии файла вместе с флагом O_DIRECT нужно указать флаг O_DSYNC. O_DSYNC гарантирует, что в постоянную память будут записаны все данные, а также достаточный объем метаданных (т.е. косвенных блоков). Это означает, что в случае сбоя системы все данные можно будет восстановить. При указании флага O_DIRECT без флага O_DSYNC записываются только обычные данные, но не метаданные.

Работа с каналами

Канал - это неименованный объект, позволяющий процессам обмениваться данными. При этом один из процессов записывает данные в канал, а другой их считывает. Такой тип файла называется также файлом FIFO. В файле FIFO блоки данных образуют очередь, в которой есть указатели чтения и записи (указывающие на голову и на хвост очереди), которые позволяют сохранять порядок элементов очереди. Максимальный размер элемента очереди в байтах задается системной переменной PIPE_BUF, которая определена в файле limits.h.

В оболочке неименованные каналы применяются для создания конвейеров команд. Большинство неименованных каналов создается именно оболочкой. Канал между процессами обозначается символом | (вертикальная черта). Например:

ls | pr

печатает вывод команды ls на экране.

По возможности все каналы рассматриваются как обычные файлы. Обычно положение указателя в файле хранится в таблице открытых файлов. Однако канал совместно используется двумя процессами, которые должны работать с одним и тем же файлом, но не с одним и тем же указателем. С другой стороны, при выполнении операции open в таблице открытых файлов создается запись, уникальная для процесса, а не для файла. Для решения этой проблемы в таблице открытых файлов создаются записи, общие для нескольких процессов.

Функции работы с каналами

Функция pipe создает канал между двумя процессами и возвращает два дескриптора файла. Дескриптор 0 открывается для чтения. Дескриптор 1 открывается для записи. Операция чтения получает данные в порядке их записи. Описанные дескрипторы применяются в функциях read, write и close.

В следующем примере дочерний процесс создает канал для связи с родительским процессом и отправляет по нему свой ИД:

#include <sys/types.h>
main()
{
        int p[2];
        char buf[80];
        pid_t pid;
 
        if (pipe(p))
        {
                  perror("ошибка канала");
                exit(1)'
        }
        if ((pid=fork()) == 0)
        {
                                       /* дочерний процесс */
                close(p[0]);           /*закрывает ненужный*/
                                       *дескриптор чтения*/
                sprintf(buf,"%d",getpid()); 
                                       /* создание данных */
                                       /*для отправки*/
                write(p[1],buf,strlen(buf)+1);
                        /*запись данных с учетом
                        /*байта null*/
                exit(0);
        }
                                        /*родительский процесс*/
        close(p[1]);                    /*закрыть ненужную сторону канала  */
        read(p[0],buf,sizeof(buf));     /*чтение данных из канала*/
        printf("Сообщение дочернего процесса: %s/n", buf);
                                       /*вывод результата*/
        exit(0);
}

При попытке чтения из пустого канала операция будет отложена до появления данных в канале. При попытке записи в переполненный канал (PIPE_BUF) операция будет отложена до освобождения необходимого пространства в канале. Если к моменту чтения данных из канала дескриптор записи будет уже закрыт, то операция чтения вернет признак конца файла.

Существуют еще две функции, предназначенные для работы с каналом: popen и pclose.

popen Создает канал (с помощью функции pipe) и копию процесса, из которого она была вызвана. Дочерний процесс закрывает ненужный дескриптор канала (в зависимости от того, будет ли он записывать или считывать данные) и вызывает оболочку для запуска требуемого процесса с помощью функции execl.

Родительский процесс закрывает ненужный дескриптор канала. Все ненужные дескрипторы должны быть закрыты для обеспечения правильной передачи признака конца файла. Например, если дочерний процесс, читающий данные из файла, не закроет дескриптор записи, то он никогда не получит признак конца файла, так как один процесс записи всегда будет потенциально активным.

Ниже описан удобный способ связывания дескриптора файла канала со стандартным вводом процесса:

close(p[1]);
close(0);
dup(p[0]);
close(p[0]);

Функция close освобождает дескриптор 0 - стандартный ввод. Функция dup возвращает копию ранее открытого дескриптора файла. Дескрипторы файлов выделяются в порядке возрастания их номеров, причем всегда возвращается первый свободный дескриптор. Следовательно, функция dup выделит для дескриптора чтения канала дескриптор 0. Таким образом, стандартный ввод будет связан с дескриптором чтения канала. Старый дескриптор чтения закрывается. Аналогичное действие должно выполняться дочерним процессом, который записывает данные в канал связи с родительским процессом.

pclose Закрывает канал между программой, отправившей запрос, и командой оболочки, которая должна быть выполнена. Функция pclose позволяет закрыть все потоки, открытые с помощью popen.

Функция pclose дожидается завершения процесса, закрывает канал и возвращает код завершения команды. Эта функция удобнее функции close, так как она позволяет дочернему процессу закончить работу, и только после этого закрывает канал. Кроме того, в процессе может быть только ограниченное число незавершенных дочерних процессов, даже с учетом тех процессов, которые фактически уже выполнили свою задачу. Функция pclose всегда дожидается завершения процесса, поэтому данное ограничение никогда не будет превышено.

Синхронный ввод-вывод

По умолчанию данные записываются в файлы JFS и JFS2 асинхронно. Тем не менее, JFS поддерживает три типа синхронного ввода-вывода. Первому типу соответствует флаг O_DSYNC. Если он будет указан при открытии файла, системный вызов write () будет возвращать управление программе только после записи в постоянную память всех данных и метаданных, необходимых для восстановления этих данных.

Другому типу синхронного ввода-вывода соответствует флаг O_SYNC. При указании этого флага функция write() будет выполнять те же действия, что и для флага O_DSYNC. Кроме того, перед передачей управления программе она запишет в постоянную память все атрибуты файла, связанные с вводом-выводом, даже если они не нужны для восстановления файла.

Перед появлением флага O_DSYNC его функции в AIX выполнял флаг O_SYNC. Для обеспечения двоичной совместимости назначение этого флага менять нельзя. Для того чтобы воспользоваться фактическими функциями флага O_SYNC, при открытии файла нужно указать оба флага O_DSYNC и O_SYNC. Флаг O_SYNC также будет выполнять свои функции при наличии переменной среды XPG_SUS_ENV=ON.

Третьему типу асинхронного ввода-вывода соответствует флаг O_RSYNC, который выполняет функции флагов O_SYNC и _DSYNC для операций чтения. Для файлов JFS флаг O_RSYNC имеет смысл указывать только в комбинации с флагом O_SYNC. В этом случае системный вызов read будет возвращать управление программе только после записи в постоянную память времени обращения к файлу.

Связанная информация

Глава 5, Файловые системы и каталоги

Работа с i-узлами JFS

Распределение памяти в JFS

Работа с дескрипторами файлов

Команды ls, pr

Функции close, exec, execl, execv, execle, execve, execlp, execvp, exect, fclear, fsync, lseek , open, openx, creat, read, readx, readv, readvx, truncate, ftruncate, write, writex, writev и writevx.


[ Страница назад | Страница вперед | Содержание | Индекс | Библиотека | Юридическая информация | Поиск ]