Главная страница
Навигация по странице:

  • Флаги O_DSYNC и O_RSYNC

  • Непосредственный ввод-вывод

  • Закрытие файлов

  • Значения ошибок

  • Позиционирование с помощью Iseek()

  • Поиск с выходом за пределы файла

  • Значения ошибок.

  • Позиционное чтение и запись

  • Системное программирование Линукс. Linux. Системное программирование. Вступление


    Скачать 0.65 Mb.
    НазваниеLinux. Системное программирование. Вступление
    АнкорСистемное программирование Линукс
    Дата23.01.2023
    Размер0.65 Mb.
    Формат файлаpdf
    Имя файлаLinuxSystemProgramming.pdf
    ТипКраткое содержание
    #900372
    страница10 из 14
    1   ...   6   7   8   9   10   11   12   13   14
    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()
    1   ...   6   7   8   9   10   11   12   13   14


    написать администратору сайта