Системное программирование Линукс. Linux. Системное программирование. Вступление
![]()
|
73 Согласно действующим стандартам от sync() не требуется дожидаться, пока все буферы будут сброшены на диск, и только потом возвращаться. Требуется лишь следующее: вызов должен инициировать процесс отправки на диск содержимого всех буферов, поэтому часто рекомендуется делать вызов sync() неоднократно, чтобы гарантировать надежную доставку всех данных на диск. Однако как раз Linux действительно дожидается, пока информация из всех буферов отправится на диск, поэтому в данной операционной системе достаточно будет и одного вызова sync() Единственный практически важный пример использования sync() — реализация утилиты sync. Приложения, в свою очередь, должны применять fsync() и fdatasync() для отправки на диск только данных, которые обладают требуемыми файловыми дескрипторами. Обратите внимание: в активно эксплуатируемой системе на завер шение sync() может потребоваться несколько минут или даже больше времени. Флаг O_SYNC Флаг O_SYNC может быть передан вызову open() . Этот флаг означает, что все операции вводавывода, осуществляемые с этим файлом, должны быть синхронизированы: int fd; fd = open (file, O_WRONLY | O_SYNC); if (fd == –1) { perror ("open"); return –1; } Запросы на считывание всегда синхронизированы. Если бы такая синхрониза ция отсутствовала, то мы не могли бы судить о допустимости данных, считанных из предоставленного буфера. Тем не менее, как уже упоминалось выше, вызовы write() , как правило, не синхронизируются. Нет никакой связи между возвратом вызова и отправкой данных на диск. Флаг O_SYNC принудительно устанавливает такую связь, гарантируя, что вызовы write() будут выполнять синхронизированный вводвывод. Флаг O_SYNC можно рассмотреть в следующем ключе: он принудительно выпол няет неявный вызов fsync() после каждой операции write() перед возвратом вызо ва. Этот флаг обеспечивает именно такую семантику, хотя ядро реализует вызов O_SYNC немного эффективнее. При использовании O_SYNC несколько ухудшаются два показателя операций записи: время, затрачиваемое ядром, и пользовательское время. Это соответствен но периоды, затраченные на работу в пространстве ядра и в пользовательском пространстве. Более того, в зависимости от размера записываемого файла O_SYNC общее истекшее время также может увеличиваться на одиндва порядка, посколь ку все время ожидания при вводе-выводе (время, необходимое для завершения операций вводавывода) суммируется со временем, затрачиваемым на работу про цесса. Налицо огромное увеличение издержек, поэтому синхронизированный вводвывод следует использовать только при отсутствии альтернатив. Синхронизированный ввод-вывод 74 Как правило, если приложению требуется гарантировать, что информация, за писанная с помощью write() , попала на диск, обычно используются вызовы fsync() или fdatasync() . С ними, как правило, связано меньше издержек, чем с O_SYNC , так как их требуется вызывать не столь часто (то есть только после завершения определен ных критически важных операций). Флаги O_DSYNC и O_RSYNC Стандарт POSIX определяет еще два флага для вызова open() , связанных с синхро низированным вводомвыводом, — O_DSYNC и O_RSYNC . В Linux эти флаги определя ются как синонимичные O_SYNC , они предоставляют аналогичное поведение. Флаг O_DSYNC указывает, что после каждой операции должны синхронизировать ся только обычные данные, но не метаданные. Ситуацию можно сравнить с неявным вызовом fdatasync() после каждого запроса на запись. O_SYNC предоставляет более надежные гарантии, поэтому совмещение O_DSYNC с ним не влечет за собой никако го функционального ухудшения. Возможно лишь потенциальное снижение произ водительности, связанное с тем, что O_SYNC предъявляет к системе более строгие требования. Флаг O_RSYNC требует синхронизации запросов как на считывание, так и на запись. Его нужно использовать вместе с O_SYNC или O_DSYNC . Как было сказано выше, опе рации считывания синхронизируются изначально — если уж на то пошло, они не возвращаются, пока получат какуюлибо полезную информацию, которую можно будет предоставить пользователю. Флаг O_RSYNC регламентирует, что все побочные эффекты операции считывания также должны быть синхронизированы. Это озна чает, что обновления метаданных, происходящие в результате считывания, должны быть записаны на диск прежде, чем вернется вызов. На практике данное требование обычно всего лишь означает, что до возврата вызова read() должно быть обновлено время доступа к файлу, фиксируемое в копии индексного дескриптора, находящей ся на диске. В Linux флаг O_RSYNC определяется как аналогичный O_SYNC , пусть это и кажется нецелесообразным (ведь O_RSYNC не являются подмножеством O_SYNC , в от личие от O_DSYNC , которые таким подмножеством являются). В настоящее время в Linux отсутствует способ, позволяющий обеспечить функциональность O_RSYNC Максимум, что может сделать разработчик, — инициировать fdatasync() после каждого вызова read() . Правда, такое поведение требуется редко. Непосредственный ввод-вывод Ядро Linux, как и ядро любых других современных операционных систем, реализу ет между устройствами и приложениями сложный уровень архитектуры, отвеча ющий за кэширование, буферизацию и управление вводомвыводом (см. разд. «Внут ренняя организация ядра» данной главы). Высокопроизводительным приложениям, возможно, потребуется обходить этот сложный уровень и применять собственную систему управления вводомвыводом. Правда, обычно эксплуатация такой системы Глава 2. Файловый ввод-вывод 75 не оправдывает затрачиваемых на нее усилий. Вероятно, инструменты, которые уже доступны вам на уровне операционной системы, позволят обеспечить значительно более высокую производительность, чем подобные им существующие на уровне приложений. Тем не менее в системах баз данных обычно предпочтительнее исполь зовать собственный механизм кэширования и свести к минимуму участие операци онной системы в рабочих процессах, насколько это возможно. Когда мы снабжаем вызов open() флагом O_DIRECT , мы предписываем ядру свести к минимуму активность управления вводомвыводом. При наличии этого флага операции вводавывода будут инициироваться непосредственно из буферов поль зовательского пространства на устройство, минуя страничный кэш. Все операции вводавывода станут синхронными, вызовы не будут возвращаться до завершения этих действий. При выполнении непосредственного вводавывода длина запроса, выравнивание буфера и смещения файлов должны представлять собой целочисленные значения, кратные размеру сектора на базовом устройстве. Как правило, размер сектора со ставляет 512 байт. До выхода версии ядра Linux 2.6 это требование было еще стро же. Так, в версии 2.4 все эти значения должны были быть кратны размеру логиче ского блока файловой системы (обычно 4 Кбайт). Для обеспечения совместимости приложения должны соответствовать более крупной (и потенциально менее удоб ной) величине — размеру логического блока. Закрытие файлов После того как программа завершит работу с дескриптором файла, она может разорвать связь, существующую между дескриптором и файлом, который с ним ассоциирован. Это делается с помощью системного вызова close() : #include int close (int fd); Вызов close() отменяет отображение открытого файлового дескриптора fd и разрывает связь между файлом и процессом. Данный дескриптор файла больше не является допустимым, и ядро свободно может переиспользовать его как возвра щаемое значение для последующих вызовов open() или creat() . При успешном выполнении вызов close() возвращает 0 . При ошибке он возвращает –1 и устанав ливает errno в соответствующее значение. Пример использования прост: if (close (fd) == –1) perror ("close"); Обратите внимание: закрытие файла никак не связано с актом сбрасывания файла на диск. Чтобы перед закрытием файла убедиться, что он уже присутствует на диске, в приложении необходимо задействовать одну из возможностей синхро низации, рассмотренных выше (см. разд. «Синхронизированный вводвывод» данной главы). Закрытие файлов 76 Правда, с закрытием файла связаны некоторые побочные эффекты. Когда за крывается последний из открытых файловых дескрипторов, ссылавшийся на данный файл, в ядре высвобождается структура данных, с помощью которой обес печивалось представление файла. Когда эта структура высвобождается, она «рас цепляется» с хранимой в памяти копией индексного дескриптора, ассоциирован ного с файлом. Если индексный дескриптор ни с чем больше не связан, он также может быть высвобожден из памяти (конечно, этот дескриптор может остаться доступным, так как ядро кэширует индексные дескрипторы из соображений про изводительности, но это не гарантируется). В некоторых случаях разрывается связь между файлом и диском, но файл остается открытым вплоть до этого раз рыва. В таком случае физического удаления данного файла с диска не происходит, пока файл не будет закрыт, а его индексный дескриптор удален из памяти, поэто му вызов close() также может привести к тому, что ни с чем не связанный файл окажется физически удаленным с диска. Значения ошибок Распространена порочная практика — не проверять возвращаемое значение close() В результате можно упустить критическое условие, приводящее к ошибке, так как подобные ошибки, связанные с отложенными операциями, могут не проявиться вплоть до момента, как о них сообщит close() При таком отказе вы можете встретить несколько возможных значений errno Кроме EBADF (заданный дескриптор файла оказался недопустимым), наиболее важ ным значением ошибки является EIO . Оно соответствует низкоуровневой ошибке вводавывода, которая может быть никак не связана с самим актом закрытия. Если файловый дескриптор допустим, то при выдаче сообщения об ошибке он всегда закрывается, независимо от того, какая именно ошибка произошла. Ассоциирован ные с ним структуры данных высвобождаются. Вызов close() никогда не возвращает EINTR , хотя POSIX это допускает. Разра ботчики ядра Linux знают, что делают. Позиционирование с помощью Iseek() Как правило, операции вводавывода происходят в файле линейно и все пози ционирование сводится к неявным обновлениям файловой позиции, происхо дящим в результате операций чтения и записи. Однако некоторые приложения перемещаются по файлу скачками, выполняя произвольный, а не линейный доступ к данным. Системный вызов lseek() предназначен для установки в за данное значение файловой позиции конкретного файлового дескриптора. Этот вызов не осуществляет никаких других операций, кроме обновления файловой позиции, в частности не инициирует какихлибо действий, связанных с вводом выводом. Глава 2. Файловый ввод-вывод 77 #include #include off_t lseek (int fd, off_t pos, int origin); Поведение вызова lseek() зависит от аргумента origin , который может иметь одно из следующих значений. SEEK_CUR — текущая файловая позиция дескриптора fd установлена в его текущее значение плюс pos . Последний может иметь отрицательное, положительное или нулевое значение. Если pos равен нулю, то возвращается текущее значение файловой позиции. SEEK_END — текущая файловая позиция дескриптора fd установлена в текущее значение длины файла плюс pos , который может иметь отрицательное, положи тельное или нулевое значение. Если pos равен нулю, то смещение устанавлива ется в конец файла. SEEK_SET — текущая файловая позиция дескриптора fd установлена в pos . Если pos равен нулю, то смещение устанавливается в начало файла. В случае успеха этот вызов возвращает новую файловую позицию. При ошибке он возвращает –1 и присваивает errno соответствующее значение. В следующем примере файловая позиция дескриптора fd получает значение 1825 : off_t ret; ret = lseek (fd, (off_t) 1825, SEEK_SET); if (ret == (off_t) –1) /* ошибка */ В качестве альтернативы можно установить файловую позицию дескриптора fd в конец файла: off_t ret; ret = lseek (fd, 0, SEEK_END); if (ret == (off_t) –1) /* ошибка */ Вызов lseek() возвращает обновленную файловую позицию, поэтому его мож но использовать для поиска текущей файловой позиции. Нужно установить зна чение SEEK_CUR в нуль: int pos; pos = lseek (fd, 0, SEEK_CUR); if (pos == (off_t) –1) /* ошибка */ else /* 'pos' — это текущая позиция fd */ Позиционирование с помощью Iseek() 78 По состоянию на настоящий момент lseek() чаще всего применяется для поис ка относительно начала файла, конца файла или для определения текущей позиции файлового дескриптора. Поиск с выходом за пределы файла Можно указать lseek() переставить указатель файловой позиции за пределы фай ла (дальше его конечной точки). Например, следующий код устанавливает позицию на 1688 байт после конца файла, на который отображается дескриптор fd : int ret; ret = lseek (fd, (off_t) 1688, SEEK_END); if (ret == (off_t) –1) /* ошибка */ Само по себе позиционирование с выходом за пределы файла не дает результа та — запрос на считывание такой новой файловой позиции вернет значение EOF (конец файла). Однако если затем сделать запрос на запись, указав такую конеч ную позицию, то между старым и новым значениями длины файла будет создано дополнительное пространство, которое программа заполнит нулями. Такое заполнение нулями называется дырой. В UNIXподобных файловых сис темах дыры не занимают на диске никакого пространства. Таким образом, общий размер всех файлов, содержащихся в файловой системе, может превышать физиче ский размер диска. Файлы, содержащие дыры, называются разреженными. При ис пользовании разреженных файлов можно экономить значительное пространство на диске, а также оптимизировать производительность, ведь при манипулировании дырами не происходит никакого физического вводавывода. Запрос на считывание фрагмента файла, полностью находящегося в пределах дыры, вернет соответствующее количество нулей. Значения ошибок. При ошибке lseek() возвращает –1 и присваивает errno одно из следующих значений. EBADF — указанное значение дескриптора не ссылается на открытый файловый дескриптор. EINVAL — значение аргумента origin не является SEEK_SETSEEK_CUR или SEEK_END либо результирующая файловая позиция получится отрицательной. Факт, что EINVAL может соответствовать обеим подобным ошибкам, конечно, неудобен. В первом случае мы наверняка имеем дело с ошибкой времени компиляции, а во втором, возможно, наличествует более серьезная ошибка в логике исполнения. EOVERFLOW — результирующее файловое смещение не может быть представлено как off_t . Такая ситуация может возникнуть лишь в 32битных архитектурах. В момент получения такой ошибки файловая позиция уже обновлена; данная ошибка означает лишь, что новую позицию файла невозможно вернуть. ESPIPE — указанный дескриптор файла ассоциирован с объектом, который не поддерживает позиционирования, например с конвейером, FIFO или сокетом. Глава 2. Файловый ввод-вывод 79 Ограничения Максимальные значения файловых позиций ограничиваются типом off_t . В боль шинстве машинных архитектур он определяется как тип long языка C. В Linux размер этого вида обычно равен размеру машинного слова. Как правило, под разме ром машинного слова понимается размер универсальных аппаратных регистров в конкретной архитектуре. Однако на внутрисистемном уровне ядро хранит фай ловые смещения в типах long языка C. На машинах с 64битной архитектурой это не представляет никаких проблем, но такая ситуация означает, что на 32битных машинах ошибка EOVERFLOW может возникать при выполнении операций относи тельного поиска. Позиционное чтение и запись Linux позволяет использовать вместо lseek() два варианта системных вызовов — read() и write() . Оба эти вызова получают файловую позицию, с которой требует ся начинать чтение или запись. По завершении работы эти вызовы не обновляют позицию файла. Данная форма считывания называется pread() : #define _XOPEN_SOURCE 500 #include ssize_t pread (int fd, void *buf, size_t count, off_t pos); Этот вызов считывает до count байт в buf , начиная от файлового дескриптора fd на файловой позиции pos Данная форма записи называется pwrite() : #define _XOPEN_SOURCE 500 #include ssize_t pwrite (int fd, const void *buf, size_t count, off_t pos); Этот вызов записывает до count байт в buf , начиная от файлового дескриптора fd на файловой позиции pos Функционально эти вызовы практически идентичны своим собратьям без бук вы p , за исключением того, что они игнорируют текущую позицию файла. Вместо использования ее они прибегают к значению, указанному в pos . Кроме того, выпол нив свою работу, они не обновляют позицию файла. Таким образом, если смешивать позиционные вызовы с обычными read() и write() , последние могут полностью испортить всю работу, выполненную позиционными вызовами. Оба позиционных вызова применимы только с файловыми дескрипторами, кото рые поддерживают поиск, в частности с обычными файлами. С семантической точки зрения их можно сравнить с вызовами read() или write() , которым предшествует Позиционное чтение и запись 80 вызов lseek() , но с тремя оговорками. Вопервых, позиционные вызовы проще в ис пользовании, особенно если нас интересует какаялибо хитрая манипуляция, напри мер перемещение по файлу в обратном направлении или произвольном порядке. Вовторых, завершив работу, они не обновляют указатель на файл. Наконец, самое важное — они исключают возможность условий гонки, которые могут возникнуть при использовании lseek() Потоки совместно используют файловые таблицы, и текущая файловая позиция хранится в такой разделяемой таблице, поэтому один поток программы может обно вить файловую позицию уже после вызова lseek() , поступившего к файлу от друго го потока, но прежде, чем закончится выполнение операции считывания или записи. Налицо потенциальные условия гонки, если в вашей программе присутствуют два и более потока, оперирующие одним и тем же файловым дескриптором. Таких усло вий гонки можно избежать, работая с системными вызовами pread() и pwrite() |