Системное программирование Линукс. Linux. Системное программирование. Вступление
Скачать 0.65 Mb.
|
Запись с помощью 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. Файловый ввод-вывод |