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

  • Случаи частичной записи

  • Неблокирующая запись

  • Ограничения размера при использовании write()

  • Поведение write()

  • Синхронизированный ввод-вывод

  • Возвращаемые значения и коды ошибок.

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


    Скачать 0.65 Mb.
    НазваниеLinux. Системное программирование. Вступление
    АнкорСистемное программирование Линукс
    Дата23.01.2023
    Размер0.65 Mb.
    Формат файлаpdf
    Имя файлаLinuxSystemProgramming.pdf
    ТипКраткое содержание
    #900372
    страница9 из 14
    1   ...   6   7   8   9   10   11   12   13   14
    Запись с помощью write()
    Самый простой и распространенный системный вызов для записи информации называется write()
    Э
    то парный вызов для read()
    , он также определен в POSIX.1:
    #include
    ssize_t write (int fd, const void *buf, size_t count);
    При вызове write()
    записывается некоторое количество байтов, меньшее или равное тому, что указано в count
    . Запись начинается с buf
    , установленного в теку­
    щую файловую позицию. Ссылка на нужный файл определяется по файловому дескриптору fd
    . Если в основе файла лежит объект, не поддерживающий позицио­
    нирования (так, в частности, выглядит ситуация с символьными устройствами), запись всегда начинается с текущей позиции «курсора».
    При успешном выполнении возвращается количество записанных байтов, а фай­
    ловая позиция обновляется соответственно. При ошибке возвращается
    -1
    и уста­
    навливается соответствующее значение errno
    . Вызов write()
    может вернуть
    0
    , но
    Запись с помощью write()

    66
    это возвращаемое значение не имеет никакой специальной трактовки, а всего лишь означает, что было записано 0 байт.
    Как и в случае с read()
    , простейший пример использования тривиален:
    const char *buf = "My ship is solid!";
    ssize_t nr;
    /* строка, находящаяся в 'buf', записывается в 'fd' */
    nr = write (fd, buf, strlen (buf));
    if (nr == –1)
    /* ошибка */
    Однако, как и в случае с read()
    , вышеуказанный код написан не совсем грамот­
    но. Вызывающая сторона также должна проверять возможное наличие частичной записи:
    unsigned long word = 1720;
    size_t count;
    ssize_t nr;
    count = sizeof (word);
    nr = write (fd, &word, count);
    if (nr == –1)
    /* ошибка, проверить errno */
    else if (nr != count)
    /* возможна ошибка, но значение 'errno' не установлено */
    Случаи частичной записи
    Системный вызов write()
    выполняет частичную запись не так часто, как системный вызов read()
    — частичное считывание. Кроме того, в случае с write()
    отсутствует условие EOF. В случае с обычными файлами write()
    гарантированно выполняет всю запрошенную запись, если только не возникает ошибка.
    Следовательно, при записи информации в обычные файлы не требуется исполь­
    зовать цикл. Однако при работе с файлами других типов, например сокетами, цикл может быть необходим. С его помощью можно гарантировать, что вы действитель-
    но записали все требуемые байты. Еще одно достоинство использования цикла заключается в том, что второй вызов write()
    может вернуть ошибку, проясняющую, по какой причине при первом вызове удалось осуществить лишь частичную запись
    (правда, вновь следует оговориться, что такая ситуация не слишком распростра­
    нена). Вот пример:
    ssize_t ret, nr;
    while (len != 0 && (ret = write (fd, buf, len)) != 0) {
    if (ret == –1) {
    if (errno == EINTR)
    continue;
    perror ("write");
    Глава 2. Файловый ввод-вывод

    67
    break;
    }
    len -= ret;
    buf += ret;
    }
    Режим дозаписи
    Когда дескриптор fd открывается в режиме дозаписи (с флагом
    O_APPEND
    ), запись начинается не с текущей позиции дескриптора файла, а с точки, в которой в данный момент находится конец файла.
    Предположим, два процесса пытаются записать информацию в конец одного и того же файла. Такая ситуация распространена: например, она может возникать в журнале событий, разделяемом многими процессами. Перед началом работы файловые позиции установлены правильно, каждая из них соответствует концу файла. Первый процесс записывает информацию в конец файла. Если режим до­
    записи не используется, то второй процесс, попытавшись сделать то же самое, начнет записывать свои данные уже не в конце файла, а в точке с тем смещением, где конец файла находился до операции записи, выполненной первым процессом.
    Таким образом, множественные процессы не могут дозаписывать информацию в ко­
    нец одного и того же файла без явной синхронизации между ними, поскольку при ее отсутствии наступают условия гонки.
    Режим дозаписи избавляет нас от таких неудобств. Он гарантирует, что фай­
    ловая позиция всегда устанавливается в его конец, поэтому все добавляемые информационные фрагменты всегда дозаписываются правильно, даже если они поступают от нескольких записывающих процессов. Эту ситуацию можно срав­
    нить с атомарным обновлением файловой позиции, предшествующим каждому запросу на запись информации. Затем файловая позиция обновляется и устанав­
    ливается в точку, соответствующую окончанию последних записанных данных.
    Это совершенно не мешает работе следующего вызова write()
    , поскольку он об­
    новляет файловую позицию автоматически, но может быть критичным, если по какой­то причине далее последует вызов read()
    , а не write()
    При решении определенных задач режим дозаписи целесообразен, например, при упомянутой выше операции записи файлов журналов, но в большинстве слу­
    чаев он не находит применения.
    Неблокирующая запись
    Когда дескриптор fd открывается в неблокирующем режиме (с флагом
    O_NONBLOCK
    ), а запись в том виде, в котором она выполнена, в нормальных условиях должна быть заблокирована, системный вызов write()
    возвращает
    –1
    и устанавливает errno в зна­
    чение
    EAGAIN
    . Запрос следует повторить позже. С обычными файлами этого, как правило, не происходит.
    Запись с помощью write()

    68
    Другие коды ошибок
    Следует отдельно упомянуть следующие значения errno
    
    EBADF
    — указанный дескриптор файла недопустим или не открыт для записи.
    
    EFAULT
    — указатель, содержащийся в buf
    , ссылается на данные, расположенные вне адресного пространства процесса.
    
    EFBIG
    — в результате записи файл превысил бы максимум, допустимый для одного процесса по правилам, действующим в системе или во внутренней реа­
    лизации.
    
    EINVAL
    — указанный дескриптор файла отображается на объект, не подходящий для записи.
    
    EIO
    — произошла ошибка низкоуровневого ввода­вывода.
    
    ENOSPC
    — файловая система, к которой относится указанный дескриптор файла, не обладает достаточным количеством свободного пространства.
    
    EPIPE
    — указанный дескриптор файла ассоциирован с конвейером или сокетом, чей считывающий конец закрыт. Этот процесс также получит сигнал
    SIGPIPE
    Стандартное действие, выполняемое при получении сигнала
    SIGPIPE
    , — завер­
    шение процесса­получателя. Следовательно, такие процессы получают данное значение errno
    , только когда они явно запрашивают, как поступить с этим сиг­
    налом — игнорировать, блокировать или обработать его.
    Ограничения размера при использовании write()
    Если значение count превышает
    SSIZE_MAX
    , то результат вызова write()
    не определен.
    При вызове write()
    со значением count
    , равным нулю, вызов возвращается не­
    медленно и при этом имеет значение
    0
    Поведение write()
    При возврате вызова, отправленного к write()
    , ядро уже располагает данными, скопированными из предоставленного буфера в буфер ядра, но нет гарантии, что рассматриваемые данные были записаны именно туда, где им следовало оказаться.
    Действительно, вызовы write возвращаются слишком быстро и о записи в нужное место, скорее всего, не может быть и речи. Производительность современных про­
    цессоров и жестких дисков несравнима, поэтому на практике данное поведение было бы не только ощутимым, но и весьма неприятным.
    На самом деле, после того как приложение из пользовательского пространства осуществляет системный вызов write()
    , ядро Linux выполняет несколько проверок, а потом просто копирует данные в свой буфер. Позже в фоновом режиме ядро со­
    бирает все данные из грязных буферов — так именуются буферы, содержащие более актуальные данные, чем записанные на диске. После этого ядро оптимальным образом сортирует информацию, добытую из грязных буферов, и записывает их
    Глава 2. Файловый ввод-вывод

    69
    содержимое на диск (этот процесс называется отложенной записью). Таким обра­
    зом, вызовы write работают быстро и возвращаются практически без задержки.
    Кроме того, ядро может откладывать такие операции записи на сравнительно не­
    активные периоды и объединять в «пакеты» несколько отложенных записей.
    Подобная запись с отсрочкой не меняет семантики POSIX. Например, если выполняется вызов для считывания только что записанных данных, находящихся в буфере, но отсутствующих на диске, в ответ на запрос поступает именно инфор­
    мация из буфера, а не «устаревшие» данные с диска. Такое поведение, в свою оче­
    редь, повышает производительность, поскольку в ответ на запрос о считывании поступают данные из хранимого в памяти кэша, а диск вообще не участвует в опе­
    рации. Запросы о чтении и записи чередуются верно, а мы получаем ожидаемый результат — конечно, если система не откажет прежде, чем данные окажутся на диске! При аварийном завершении системы наша информация на диск так и не попадет, пусть приложение и будет считать, что запись прошла успешно.
    Еще одна проблема, связанная с отложенной записью, заключается в невозмож­
    ности принудительного упорядочения записи. Конечно, приложению может требо­
    ваться, чтобы запросы записи обрабатывались именно в том порядке, в котором они попадают на диск. Однако ядро может переупорядочивать запросы записи так, как считает целесообразным в первую очередь для оптимизации производительности.
    Как правило, это несоответствие приводит к проблемам лишь в случае аварийного завершения работы системы, поскольку в нормальном рабочем процессе содержимое всех буферов рано или поздно попадает в конечную версию файла, содержащуюся на диске, — отложенная запись срабатывает правильно. Абсолютное большинство при­
    ложений никак не регулируют порядок записи. На практике упорядочение записи применяется редко, наиболее распространенные примеры подобного рода связаны с базами данных. В базах данных важно обеспечить порядок операций записи, при котором база данных гарантированно не окажется в несогласованном состоянии.
    Последнее неудобство, связанное с использованием отложенной записи, — это сообщения системы о тех или иных ошибках ввода­вывода. Любая ошибка ввода­
    вывода, возникающая при отложенной записи, — допустим, отказ физического диска — не может быть сообщена процессу, который сделал вызов о записи. На са­
    мом деле грязные буферы, расположенные в ядре, вообще никак не ассоциированы с процессами. Данные, находящиеся в конкретном грязном буфере, могли быть доставлены туда несколькими процессами, причем выход процесса может произой­
    ти как раз в промежуток времени, когда данные уже записаны в буфер, но еще не попали на диск. Кроме того, как в принципе можно сообщить процессу о неуспеш­
    ной записи уже постфактум?
    Учитывая все потенциальные проблемы, которые могут возникать при отло­
    женной записи, ядро стремится минимизировать связанные с ней риски. Чтобы гарантировать, что все данные будут своевременно записаны на диск, ядро задает
    максимальный возраст буфера и переписывает все данные из грязных буферов до того, как их срок жизни превысит это значение. Пользователь может сконфигури­
    ровать данное значение в
    /proc/sys/vm/dirty_expire_centisecs
    . Значение указывается в сантисекундах (сотых долях секунды).
    Запись с помощью write()

    70
    Кроме того, можно принудительно выполнить отложенную запись конкретного файлового буфера и даже синхронизировать все операции записи. Эти вопросы будут рассмотрены в следующем разделе («Синхронизированный ввод­вывод») данной главы.
    Далее в этой главе, в разд. «Внутренняя организация ядра», подробно описана подсистема буферов ядра Linux, используемая при отложенной записи.
    Синхронизированный ввод-вывод
    Конечно, синхронизация ввода­вывода — это важная тема, однако не следует пре­
    увеличивать проблемы, связанные с отложенной записью. Буферизация записи обеспечивает значительное повышение производительности. Следовательно, любая операционная система, хотя бы претендующая на «современность», реализует от­
    ложенную запись именно с применением буферов. Тем не менее в определенных случаях приложению нужно контролировать, когда именно записанные данные попадают на диск. Для таких случаев Linux предоставляет возможности, позволя­
    ющие пожертвовать производительностью в пользу синхронизации операций.
    fsync() и fdatasync()
    Простейший метод, позволяющий гарантировать, что данные окажутся на диске, связан с использованием системного вызова fsync()
    . Этот вызов стандартизирован в POSIX.1b:
    #include
    int fsync (int fd);
    Вызов fsync()
    гарантирует, что все грязные данные, ассоциированные с кон­
    кретным файлом, на который отображается дескриптор fd
    , будут записаны на диск.
    Файловый дескриптор fd должен быть открыт для записи. В ходе отложенной за­
    писи вызов заносит на диск как данные, так и метаданные. К метаданным относят­
    ся, в частности, цифровые отметки о времени создания файла и другие атрибуты, содержащиеся в индексном дескрипторе. Вызов fsync()
    не вернется, пока жесткий диск не сообщит, что все данные и метаданные оказались на диске.
    В настоящее время существуют жесткие диски с кэшами (обратной) записи, поэтому вызов fsync()
    не может однозначно определить, оказались ли данные на диске физически к определенному моменту. Жесткий диск может сообщить, что данные были записаны на устройство, но на самом деле они еще могут находиться в кэше записи этого диска. К счастью, все данные, которые оказываются в этом кэше, должны отправляться на диск в срочном порядке.
    В Linux присутствует и системный вызов fdatasync()
    :
    #include
    int fdatasync (int fd);
    Глава 2. Файловый ввод-вывод

    71
    Этот системный вызов функционально идентичен fsync()
    , с оговоркой, что он лишь сбрасывает на диск данные и метаданные, которые потребуются для корректного доступа к файлу в будущем. Например, после вызова fdatasync()
    на диске окажется информация о размере файла, так как она необходима для вер­
    ного считывания файла. Этот вызов не гарантирует, что несущественные мета­
    данные будут синхронизированы с диском, поэтому потенциально он быстрее, чем fsync()
    . В большинстве практических случаев метаданные (например, от­
    метка о последнем изменении файла) не считаются существенной частью транз­
    акции, поэтому бывает достаточно применить fdatasync()
    и получить выигрыш в скорости.
    ПРИМЕЧАНИЕ
    При вызове fsync() всегда выполняется как минимум две операции ввода-вывода: в ходе одной из них осуществляется отложенная запись измененных данных, а в ходе другой обновляется времен- ная метка изменения индексного дескриптора. Данные из индексного дескриптора и данные, отно- сящиеся к файлу, могут находиться в несмежных областях диска, поэтому может потребоваться затратная операция позиционирования. Однако в большинстве случаев, когда основная задача сводится к верной передаче транзакции, можно не включать в эту транзакцию метаданные, несу- щественные для правильного доступа к файлу в будущем. Примером таких метаданных является отметка о последнем изменении файла. По этой причине в большинстве случаев вызов fdatasync() является допустимым, а также обеспечивает выигрыш в скорости.
    Обе функции используются одинаковым простым способом:
    int ret;
    ret = fsync (fd);
    if (ret == –1)
    /* ошибка */
    Вот пример с использованием fdatasync()
    :
    int ret;
    /* аналогично fsync, но на диск не сбрасываются несущественные метаданные */
    ret = fdatasync (fd);
    if (ret == –1)
    /* ошибка */
    Ни одна из этих функций не гарантирует, что все обновившиеся записи ката­
    логов, в которых содержится файл, будут синхронизированы на диске. Имеется в виду, что если ссылка на файл недавно была обновлена, то информация из дан­
    ного файла может успешно попасть на диск, но еще не отразиться в ассоциирован­
    ной с файлом записи из того или иного каталога. В таком случае файл окажется недоступен. Чтобы гарантировать, что на диске окажутся и все обновления, каса­
    ющиеся записей в каталогах, fsync()
    нужно вызвать и к дескриптору файла, откры­
    тому для каталога, содержащего интересующий нас файл.
    Возвращаемые значения и коды ошибок. В случае успеха оба вызова возвра­
    щают
    0
    . В противном случае оба вызова возвращают
    –1
    и устанавливают errno в одно из следующих трех значений:
    Синхронизированный ввод-вывод

    72
    
    EBADF
    — указанный дескриптор файла не является допустимым дескриптором, открытым для записи;
    
    EINVAL
    — указанный дескриптор файла отображается на объект, не поддержи­
    вающий синхронизацию;
    
    EIO
    — при синхронизации произошла низкоуровневая ошибка ввода­вывода; здесь мы имеем дело с реальной ошибкой ввода­вывода, более того — именно тут обычно отлавливаются подобные ошибки.
    В некоторых версиях Linux вызов fsync()
    может оказаться неуспешным потому, что вызов fsync()
    не реализован в базовой файловой системе этой версии, даже если fdatasync()
    реализован. Некоторые параноидальные приложения пытаются сделать вызов fdatasync()
    , если fsync()
    вернул
    EINVAL
    , например:
    if (fsync (fd) == –1) {
    /*
    * Предпочтителен вариант сfsync(), но мы пытаемся сделать и fdatasync(),
    * если fsync() окажется неуспешным — так, на всякий случай.
    */
    if (errno == EINVAL) {
    if (fdatasync (fd) == –1)
    perror ("fdatasync");
    } else perror ("fsync");
    }
    POSIX требует использовать fsync()
    , а fdatasync()
    расценивает как необязатель­
    ный, поэтому системный вызов fsync()
    непременно должен быть реализован для работы с обычными файлами во всех распространенных файловых системах Linux.
    Файлы необычных типов (например, в которых отсутствуют метаданные, требу­
    ющие синхронизации) или малораспространенные файловые системы могут, конеч­
    но, реализовывать только fdatasync()
    sync()
    Дедовский системный вызов sync()
    не оптимален для решения описываемых задач, зато гораздо более универсален. Этот вызов обеспечивает синхронизацию всех буферов, имеющихся на диске:
    #include
    void sync (void);
    Эта функция не имеет ни параметров, ни возвращаемого значения. Она всегда завершается успешно, и после ее возврата все буферы — содержащие как данные, так и метаданные — гарантированно оказываются на диске
    1 1
    Здесь мы сталкиваемся с теми же подводными камнями, что и раньше: жесткий диск может солгать и сообщить ядру, что содержимое буферов записано на диске, тогда как на самом деле эта информация еще может оставаться в кэше диска.
    Глава 2. Файловый ввод-вывод

    1   ...   6   7   8   9   10   11   12   13   14


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