Главная » Хабрахабр » Linux API. Управление буферизацией файлового ввода-вывода, осуществляемой в ядре

Linux API. Управление буферизацией файлового ввода-вывода, осуществляемой в ядре

image Привет, Хаброжители! Мы уже писали о книге Майкла Керриска «Linux API. Исчерпывающее руководство». Сейчас решили опубликовать отрывок из книги «Управление буферизацией файлового ввода-вывода, осуществляемой в ядре»

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

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

В SUSv3 понятие синхронизированного завершения ввода-вывода означает «операцию ввода-вывода, которая либо привела к успешному переносу данных [на диск], либо была диагностирована как неудачная».

Различие между типами касается метаданных («данных о данных»), описывающих файл. В SUSv3 определяются два различных типа завершений синхронизированного ввода-вывода. Подробности метаданных файла будут рассмотрены в разделе 14. Ядро хранит их вместе с данными самого файла. Пока же будет достаточно отметить, что файловые метаданные включают такую информацию, как сведения о владельце файла и его группе, полномочия доступа к файлу, размер файла, количество жестких ссылок на файл, метки времени, показывающие время последнего обращения к файлу, время его последнего изменения и время последнего изменения метаданных, а также указатели на блоки данных. 4 при изучении индексных дескрипторов файлов.

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

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

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

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

Системные вызовы для управления буферизацией, проводимой в ядре при файловом вводе-выводе

Вызов fsync() приводит файл в состояние целостности (файла) после завершения синхронного ввода-вывода. Системный вызов fsync() приводит к сбросу всех буферизованных данных и всех метаданных, которые связаны с открытым файлом, имеющим дескриптор fd.

Вызов fsync() возвращает управление только после завершения переноса данных на дисковое устройство (или по крайней мере в его кэш-память).

#include <unistd.h>
int fsync(int fd);

Возвращает при успешном завершении 0 или –1 при ошибке

Системный вызов fdatasync() работает точно так же, как и fsync(), но приводит файл в состояние целостности (данных) после после завершения синхронного ввода-вывода.

#include <unistd.h>
int fdatasync(int fd);

Возвращает при успешном завершении 0 или –1 при ошибке

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

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

2 и более ранних версиях fdatasync() реализован в виде вызова fsync(), поэтому не дает никакого прироста производительности. В Linux 2.

6. Начиная с ядра версии 2. Он позволяет более точно управлять процессом сброса данных файла на диск, чем fdatasync(). 17, в Linux предоставляется нестандартный системный вызов sync_file_range(). Дополнительные подробности вы найдете на странице руководства sync_file_range(2). При вызове можно указать сбрасываемую область файла и задать флаги, устанавливающие условия блокировки данного вызова.

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

#include <unistd.h>
void sync(void);

В реализации Linux функция sync() возвращает управление только после того, как все данные будут перенесены на дисковое устройство (или как минимум в его кэш-память). Но в SUSv3 разрешается, чтобы sync() просто вносила в план перенос данных для операции ввода-вывода и возвращала управление до завершения этого переноса.

Это делается для того, чтобы не допустить рассинхронизации данных буферов с соответствующим дисковым файлом на длительные периоды времени (и не подвергнуть их риску утраты при отказе системы). Постоянно выполняемый поток ядра обеспечивает сброс измененных буферов ядра на диск, если они не были явным образом синхронизированы в течение 30 секунд. 6 эта задача выполняется потоком ядра pdflush. В Linux 2. 4 она выполнялась потоком ядра kupdated.) (В Linux 2.

Дополнительные файлы в том же самом каталоге управляют другими особенностями операции, выполняемой потоком pdflush.
Срок (в сотых долях секунды), через который измененный буфер должен быть сброшен на диск кодом потока pdflush, определяется в файле /proc/sys/vm/dirty_expire_centisecs.

Включение режима синхронизации для всех записей: O_SYNC

Указание флага O_SYNC при вызове open() приводит к тому, что все последующие операции вывода выполняются в синхронном режиме:

fd = open(pathname, O_WRONLY | O_SYNC);

После этого вызова open() каждая проводимая с файлом операция write() автоматически сбрасывает данные и метаданные файла на диск (то есть записи выполняются как синхронизированные операции записи с целостностью файла).

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

Влияние флага O_SYNC на производительность

В табл. Использование флага O_SYNC (или же частые вызовы fsync(), fdatasync() или sync()) может сильно повлиять на производительность. 3 показано время, требуемое для записи 1 миллиона байт в только что созданный файл (в файловой системе ext2) при различных размерах буфера с выставленным и со сброшенным флагом O_SYNC. 13. 6. Результаты были получены (с помощью программы filebuff/write_bytes.c, предоставляемой в исходном коде для книги) с использованием «ванильного» ядра версии 2. В каждой строке приводится усредненное значение, полученное после 20 запусков для заданного размера буфера. 30 и файловой системы ext2 с размером блока 4096 байт.

3. Таблица 13. Влияние флага O_SYNC на скорость записи 1 миллиона байт

image

Как видно, указание флага O_SYNC приводит к чудовищному увеличению затрачиваемого времени при использовании буфера размером 1 байт более чем в 1000 раз. Обратите также внимание на большую разницу, возникающую при выполнении записей с флагом O_SYNC, между затраченным временем и временем задействования ЦП. Она является последствием блокирования выполнения программы при фактическом сбросе содержимого каждого буфера на диск.

13. В результатах, показанных в табл. Современные дисковые накопители обладают внутренней кэш-памятью большого объема, и по умолчанию установка флага O_SYNC просто приводит к переносу данных в эту кэш-память. 3, не учтен еще один фактор, влияющий на производительность при использовании O_SYNC. При размере буфера 1 байт затраченное время возрастет с 1030 секунд до приблизительно 16 000 секунд. Если отключить кэширование на диске (воспользовавшись командой hdparm –W0), влияние O_SYNC на производительность станет еще более существенным. В итоге, если нужно выполнить принудительный сброс на диск буферов ядра, следует рассмотреть, можно ли спроектировать приложение с использованием бóльших по объему буферов для write() или же подумать об использовании вместо флага O_SYNC периодических вызовов fsync() или fdatasync(). При размере буфера 4096 байт затраченное время возрастет с 0,34 секунды до 4 секунд.

Флаги O_DSYNC и O_RSYNC

В SUSv3 определены два дополнительных флага состояния открытого файла, имеющих отношение к синхронизированному вводу-выводу: O_DSYNC и O_RSYNC.

Эффект от его работы отличается от эффекта, вызываемого флагом O_SYNC, использование которого приводит к выполнению в последующем синхронизированных операций записи с целостностью файла (подобно fsync()). Флаг O_DSYNC приводит к выполнению в последующем синхронизированных операций записи с целостностью данных завершаемого ввода-вывода (подобно использованию fdatasync()).

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

6. До выхода версии ядра 2. (В случае с O_RSYNC это было неверно, поскольку O_SYNC не влияет на какие-либо функциональные особенности операций чтения.) 33 флаги O_DSYNC и O_RSYNC в Linux не были реализованы и в заголовочных файлах glibc эти константы определялись как выставление флага O_SYNC.

6. Начиная с ядра версии 2. 33, в Linux реализуется флаг O_DSYNC, а реализация флага O_RSYNC, скорее всего, будет добавлена в будущие выпуски ядра.

6. До выхода ядра 2. Вместо этого флаг O_SYNC был реализован как O_DSYNC. 33 в Linux отсутствовала полная реализация семантики O_SYNC. 6. В приложениях, скомпонованных со старыми версиями GNU библиотеки C для старых ядер, в версиях Linux 2. Это сделано для сохранения привычного поведения таких программ. 33 и выше флаг O_SYNC по прежнему ведет себя как O_DSYNC. 6. (Для сохранения обратной бинарной совместимости в ядре 2. Это позволяет приложениям, скомпилированным с новыми заголовочными файлами, получать в ядрах, вышедших до версии 2. 33 флагу O_DSYNC было присвоено старое значение флага O_SYNC, а новое значение O_SYNC включает в себя флаг O_DSYNC (на одной из машин это 04010000 и 010000 соответственно). 33, по меньшей мере семантику O_DSYNC.) 6.

13.4. Обзор буферизации ввода-вывода

На рис. 13.1 приведена схема буферизации, используемой (для файлов вывода) библиотекой stdio и ядром, а также показаны механизмы для управления каждым типом буферизации. Если пройтись по схеме вниз до ее середины, станет виден перенос пользовательских данных функциями библиотеки stdio в буфер stdio, который работает в пользовательском пространстве памяти. Когда этот буфер заполнен, библиотека stdio прибегает к системному вызову write(), переносящему данные в буферную кэш-память ядра (находящуюся в памяти ядра). В результате ядро инициирует дисковую операцию для переноса данных на диск.

13. В левой части схемы на рис. В правой части показаны вызовы, которые могут применяться для автоматического выполнения сброса либо за счет выключения буферизации в библиотеке stdio, либо включением для системных вызовов файлового вывода синхронного режима выполнения, чтобы при каждом вызове write() происходил немедленный сброс на диск. 1 показаны вызовы, которые могут использоваться в любое время для явного принудительного сброса любого из буферов.

image

13.5. Уведомление ядра о схемах ввода-вывода

Системный вызов posix_fadvise() позволяет процессу информировать ядро о предпочитаемой им схеме обращения к данным файла.

На семантику программы вызов posix_fadvise() не влияет. Ядро может (но не обязано) использовать информацию, предоставляемую системным вызовом posix_fadvise() для оптимизации задействования им буферной кэш-памяти, повышая тем самым производительность ввода-вывода для процесса и для системы в целом.

#define _XOPEN_SOURCE 600
#include <fcntl.h>
int posix_fadvise(int fd, off_t offset, off_t len, int advice);

Возвращает при успешном завершении 0 или положительный номер ошибки при ее возникновении

Аргументы offset и len идентифицируют область файла, к которой относится уведомление: offset указывает на начальное смещение области, а len — на ее размер в байтах. Аргумент fd является дескриптором файла, идентифицирующим тот файл, о схеме обращения к которому нужно проинформировать ядро. (В версиях ядра до 2. Присвоение для len значения 0 говорит о том, что имеются в виду все байты, начиная с offset и заканчивая концом файла. 6 значение 0 для len интерпретировалось буквально, как 0 байт.) 6.

Он определяется с одним из следующих значений. Аргумент advice показывает предполагаемый характер обращения процесса к файлу.

Это поведение по умолчанию, если для файла не дается никаких уведомлений. POSIX_FADV_NORMAL — у процесса нет особого уведомления, касающегося схем обращения. В Linux эта операция устанавливает для окна упреждающего считывания данных из файла его исходный размер (128 Кбайт).

В Linux эта операция устанавливает для окна упреждающего считывания данных из файла его удвоенное исходное значение. POSIX_FADV_SEQUENTIAL — процесс предполагает последовательное считывание данных от меньших смещений к бóльшим.

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

Ядро выполняет упреждающее считывание данных для заполнения буферной кэш-памяти данными файла в диапазоне, заданном аргументами offset и len. POSIX_FADV_WILLNEED — процесс предполагает обращение к указанной области файла в ближайшее время. Ядро не дает никаких гарантий насчет продолжительности нахождения извлекаемых из файла данных в буферной кэш-памяти. Последующие вызовы read() в отношении файла не блокируют дисковый ввод-вывод, а просто извлекают данные из буферной кэш-памяти. Иными словами, если память остро востребована, нам нужно гарантировать небольшой разрыв по времени между вызовом posix_fadvise() и последующим вызовом (или вызовами) read(). Если при работе другого процесса или ядра возникнет особая потребность в памяти, то страница в конечном итоге будет повторно использована. (Функциональные возможности, эквивалентные операции POSIX_FADV_WILLNEED, предоставляет характерный для Linux системный вызов readahead().)

Тем самым ядро уведомляется, что оно может высвободить соответствующие страницы кэш-памяти (если таковые имеются). POSIX_FADV_DONTNEED — процесс не предполагает в ближайшем будущем обращений к указанной области файла. Сначала, если очередь записи на базовом устройстве не переполнена серией запросов, ядро сбрасывает любые измененные страницы кэш-памяти в указанной области. В Linux эта операция выполняется в два этапа. Для измененных страниц в данной области второй этап завершится успешно, только если они были записаны на базовое устройство в ходе первого этапа, то есть очередь записи на устройстве не переполнена. Затем ядро предпринимает попытку высвободить все страницы кэш-памяти из указанной области. Так как приложение не может проверить состояние очереди на устройстве, гарантировать освобождение страниц кэша можно, вызвав fsync() или fdatasync() в отношении дескриптора fd перед применением POSIX_FADV_DONTNEED.

Тем самым ядро уведомляется о том, что оно может высвободить страницы после однократного обращения к ним. POSIX_FADV_NOREUSE — процесс предполагает однократное обращение к данным в указанной области файла, без ее повторного использования. В Linux эта операция в настоящее время остается без внимания.

В Linux вызов posix_fadvise() предоставляется, начиная с версии ядра 2. Спецификация posix_fadvise() появилась только в SUSv3, и этот интерфейс поддерживается не всеми реализациями UNIX. 6.

13.6. Обход буферной кэш-памяти: непосредственный ввод-вывод

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

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

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

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

4. Флаг O_DIRECT работает, начиная с версии ядра 2. Использование этого флага поддерживается не всеми файловыми системами и версиями ядра Linux. 10. Можно проверить поддержку этой возможности, протестировав выбранную файловую систему (если файловая система не поддерживает O_DIRECT, вызов open() даст сбой с выдачей ошибки EINVAL) или исследовав на этот предмет исходный код ядра. Большинство базовых файловых систем поддерживают флаг O_DIRECT, но многие файловые системы, не относящиеся к UNIX (например, VFAT), — нет.

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

Сведения об устаревшем (ныне нерекомендуемом) методе получения необрабатываемого (raw) доступа к дисковому устройству можно найти на странице руководства raw(8).

Ограничения по выравниванию для непосредственного ввода-вывода

Поскольку непосредственный ввод-вывод (как на дисковых устройствах, так и в отношении файлов) предполагает непосредственное обращение к диску, при выполнении ввода-вывода следует соблюдать некоторые ограничения.

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

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

— длина переносимых данных должна быть кратной размеру блока.

В показанном выше перечне под размером блока подразумевается размер физическего блока устройства (обычно это 512 байт). Несоблюдение любого из этих ограничений влечет за собой возникновение ошибки EINVAL.

4 накладывается больше ограничений, чем в Linux 2. При выполнении непосредственного ввода-вывода в Linux 2. (Обычно размеры логических блоков в файловой системе равны 1024, 2048 или 4096 байт.)
6: выравнивание, длина и смещение должны быть кратны размеру логического блока используемой файловой системы.

Пример программы

1 предоставляется простой пример использования O_DIRECT при открытии файла для чтения. В листинге 13. Последние два аргумента опциональны и по умолчанию настроены соответственно на значения нулевого смещения и 4096 байт. Эта программа воспринимает до четырех аргументов командной строки, указывающих (в порядке следования) файл, из которого будут считываться данные, количество считываемых из файла байтов, смещение, к которому программа должна перейти, прежде чем начать считывание данных из файла, и выравнивание буфера данных, передаваемое read().

Рассмотрим примеры того, что будет показано при запуске программы:

$ ./direct_read /test/x 512 Считывание 512 байт со смещения 0
Read 512 bytes Успешно
$ ./direct_read /test/x 256
ERROR [EINVAL Invalid argument] read Длина не кратна 512
$ ./direct_read /test/x 512 1
ERROR [EINVAL Invalid argument] read Смещение не кратно 512
$ ./direct_read /test/x 4096 8192 512
Read 4096 bytes Успешно
$ ./direct_read /test/x 4096 512 256
ERROR [EINVAL Invalid argument] read Выравнивание не кратно 512

1 выделяет блок памяти, который выровнен по адресу, кратному ее первому аргументу, и для этого использует функцию memalign(). Программа в листинге 13. 1. Функция memalign() рассматривалась в подразделе 7. 4.

#define _GNU_SOURCE /* Получение определения O_DIRECT из <fcntl.h> */ #include <fcntl.h>
#include <malloc.h>
#include "tlpi_hdr.h" int
main(int argc, char *argv[])
{ int fd; ssize_t numRead; size_t length, alignment; off_t offset; void *buf; if (argc < 3 || strcmp(argv[1], "–help") == 0) usageErr("%s file length [offset [alignment]]\n", argv[0]); length = getLong(argv[2], GN_ANY_BASE, "length"); offset = (argc > 3) ? getLong(argv[3], GN_ANY_BASE, "offset") : 0; alignment = (argc > 4) ? getLong(argv[4], GN_ANY_BASE, "alignment") : 4096; fd = open(argv[1], O_RDONLY | O_DIRECT); if (fd == -1) errExit("open"); /* Функция memalign() выделяет блок памяти, выровненный по адресу, кратному ее первому аргументу. Следующее выражение обеспечивает выравнивание 'buf' по границе, кратной 'alignment', но не являющейся степенью двойки. Это делается для того, чтобы в случае, к примеру, запроса буфера с выравниванием, кратным 256 байтам, не происходило случайного получения буфера, выровненного также и по 512-байтовой границе. Приведение к типу '(char *)' необходимо для проведения с указателем арифметических операций (что невозможно сделать с типом 'void *', который возвращает memalign(). */ buf = (char *) memalign(alignment * 2, length + alignment) + alignment; if (buf == NULL) errExit("memalign"); if (lseek(fd, offset, SEEK_SET) == -1) errExit("lseek"); numRead = read(fd, buf, length); if (numRead == -1) errExit("read"); printf("Read %ld bytes\n", (long) numRead); exit(EXIT_SUCCESS);
}
_______________________________________________________________filebuff/direct_read.c

» Более подробно с книгой можно ознакомиться на сайте издательства
» Оглавление
» Отрывок

Для Хаброжителей скидка 20% по купону — Linux


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

Ложные срабатывания в PVS-Studio: как глубока кроличья нора

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

Онлайн контест по решению задачи из теории игр

Привет, Хабр! На факультативе по теории игр мы решаем различные интересные задачи, и я хотел бы поделиться с вами одной из таких. Меня зовут Миша, и я студент. Описание игры «Я люблю вархаммер, поэтому решил адаптировать условие» Играют двое. 1. ...