Системное программирование Линукс. Linux. Системное программирование. Вступление
![]()
|
Значения ошибок. В случае успеха оба вызова возвращают количество байтов, которые соответственно были прочитаны или записаны. Возвращаемое значение 0 , полученное от pread() , означает конец файла; возвращаемое значение 0 от pwrite() указывает, что вызов ничего не записал. При ошибке оба вызова возвращают –1 и устанавливают errno соответствующее значение. В случае pread() это может быть любое допустимое значение errno для read() или lseek() . В случае pwrite() это мо жет быть любое допустимое значение errno для write() или lseek() Усечение файлов В Linux предоставляется два системных вызова для усечения длины файла. Оба они определены и обязательны (в той или иной степени) согласно различным стандартам POSIX. Вот эти вызовы: #include #include int ftruncate (int fd, off_t len); и #include #include int truncate (const char *path, off_t len); Оба системных вызова выполняют усечение заданного файла до длины, указан ной в len . Системный вызов ftruncate() оперирует файловым дескриптором fd , который должен быть открыт для записи. Системный вызов truncate() оперирует именем файла, указанным в path , причем этот файл должен быть пригоден для за писи. Оба вызова при успешном выполнении возвращают 0 . При ошибке оба вызо ва возвращают –1 и присваивают errno соответствующее значение. Как правило, эти системные вызовы используются для усечения файла до дли ны, меньшей чем текущая. При успешном возврате вызова длина файла равна len Глава 2. Файловый ввод-вывод 81 Все данные, прежде находившиеся между len и неусеченным показателем длины, удаляются и становятся недоступны для запросов на считывание. Эти функции могут также выполнять «усечение» файла с увеличением его размера, как при комбинации позиционирования и записи, описанной выше (см. разд. «Позиционирование с выходом за пределы файла» данной главы). Дополнительные байты заполняются нулями. Ни при одной из этих операций файловая позиция не обновляется. Рассмотрим, например, файл pirate.txt длиной 74 байт со следующим содер жимым: Edward Teach was a notorious English pirate. He was nicknamed Blackbeard Не выходя из каталога с этим файлом, запустим следующую программу: #include #include int main() { int ret; ret = truncate ("./pirate.txt", 45); if (ret == –1) { perror ("truncate"); return –1; } return 0; } В результате получаем файл следующего содержания длиной 45 байт: Edward Teach was a notorious English pirate. Мультиплексный ввод-вывод Зачастую приложениям приходится блокироваться на нескольких файловых дес крипторах, перемежая вводвывод от клавиатуры (stdin), межпроцессное взаимодей ствие и оперируя при этом несколькими файлами. Однако современные приложения с событийно управляемыми графическими пользовательскими интерфейсами (GUI) могут справляться без малого с сотнями событий, ожидающими обработки, так как в этих интерфейсах используется основной цикл 1 1 Основные циклы должны быть знакомы каждому, кто когдалибо писал приложения с графическими интерфейсами. Например, в приложениях системы GNOME использу ется основной цикл, предоставляемый glib — базовой библиотекой GNOME. Основной цикл позволяет отслеживать множество событий и реагировать на них из одной и той же точки блокирования. Мультиплексный ввод-вывод 82 Не прибегая к потокам — в сущности, обслуживая каждый файловый дескрип тор отдельно, — одиночный процесс, разумеется, может фиксироваться только на одном дескрипторе в каждый момент времени. Работать с множественными фай ловыми дескрипторами удобно, если они всегда готовы к считыванию или записи. Однако если программа встретит файловый дескриптор, который еще не готов к взаимодействию (допустим, мы выполнили системный вызов read() , а данные для считывания пока отсутствуют), то процесс блокируется и не сможет заняться работой с какимилибо другими файловыми дескрипторами. Он может блокиро ваться даже на несколько секунд, изза чего приложение станет неэффективным и будет только раздражать пользователя. Более того, если нужные для файлового дескриптора данные так и не появятся, то блокировка может длиться вечно. Опе рации вводавывода, связанные с различными файловыми дескрипторами, зачастую взаимосвязаны (вспомните, например, работу с конвейерами), поэтому один из файловых дескрипторов вполне может оставаться не готовым к работе, пока не будет обслужен другой. В частности, при работе с сетевыми приложениями, в ко торых одновременно бывает открыто большое количество сокетов, эта проблема может стать весьма серьезной. Допустим, произошла блокировка на файловом дескрипторе, относящемся к межпроцессному взаимодействию. В то же время в режиме ожидания остаются данные, введенные с клавиатуры (stdin). Пока блокированный файловый дескрип тор, отвечающий за межпроцессное взаимодействие, не вернет данные, приложение так и не узнает, что еще остаются необработанные данные с клавиатуры. Однако что делать, если возврата от блокированной операции так и не произойдет? Ранее в данной главе мы обсуждали неблокирующий вводвывод в качестве воз можного решения этой проблемы. Приложения, работающие в режиме неблокиру ющего вводавывода, способны выдавать запросы на вводвывод, которые в случае подвисания не блокируют всю работу, а возвращают особое условие ошибки. Это ре шение неэффективно по двум причинам. Вопервых, процессу приходится постоянно осуществлять операции вводавывода в какомто произвольном порядке, дожидаясь, пока один из открытых файловых дескрипторов не будет готов выполнить операцию вводавывода. Это некачественная конструкция программы. Вовторых, программа работала бы эффективнее, если бы могла ненадолго засыпать, высвобождая процессор для решения других задач. Просыпаться программа должна, только когда один фай ловый дескриптор или более будут готовы к обработке вводавывода. Пора познакомиться с мультиплексным вводом-выводом. Мультиплексный ввод вывод позволяет приложениям параллельно блокировать несколько файловых дескрипторов и получать уведомления, как только любой из них будет готов к чте нию или записи без блокировки, поэтому мультиплексный вводвывод оказывает ся настоящим стержнем приложения, выстраиваемым примерно по следующему принципу. 1. Мультиплексный вводвывод: сообщите мне, когда любой из этих файловых дескрипторов будет готов к операции вводавывода. 2. Ни один не готов? Перехожу в спящий режим до готовности одного или не скольких дескрипторов. Глава 2. Файловый ввод-вывод 83 3. Проснулся! Где готовый дескриптор? 4. Обрабатываю без блокировки все файловые дескрипторы, готовые к вводувы воду. 5. Возвращаюсь к шагу 1. В Linux предоставляется три сущности для различных вариантов мультиплекс ного вводавывода. Это интерфейсы для выбора ( select ), опроса ( poll ) и расши ренного опроса ( epoll ). Здесь мы рассмотрим первые два решения. Последний вариант — продвинутый, специфичный для Linux. Его мы обсудим в гл. 4. select() Системный вызов select() обеспечивает механизм для реализации синхронного мультиплексного вводавывода: #include int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); FD_CLR(int fd, fd_set *set); FD_ISSET(int fd, fd_set *set); FD_SET(int fd, fd_set *set); FD_ZERO(fd_set *set); Вызов к select() блокируется, пока указанные файловые дескрипторы не будут готовы к выполнению вводавывода либо пока не истечет необязательный интервал задержки. Отслеживаемые файловые дескрипторы делятся на три группы. Дескрипторы из каждой группы дожидаются событий определенного типа. Файловые дескрип торы, перечисленные в readfds , отслеживают, не появились ли данные, доступные для чтения, то есть они проверяют, удастся ли совершить операцию считывания без блокировки. Файловые дескрипторы, перечисленные в группе writefds , анало гичным образом дожидаются возможности совершить неблокирующую операцию записи. Наконец, файловые дескрипторы из группы exceptfds следят, не было ли исключения либо не появились ли в доступе внеполосные данные (в таком состоя нии могут находиться только сокеты). Одна из групп может иметь значение NULL ; это означает, что select() не отслеживает события данного вида. При успешном возврате каждая группа изменяется таким образом, что в ней остаются только дескрипторы, готовые к вводувыводу определенного типа, соот ветствующего конкретной группе. Предположим, у нас есть два файловых дес криптора со значениями 7 и 9 , которые относятся к группе readfds . Возвращается вызов. Если к этому моменту дескриптор 7 не покинул эту группу, то, следователь но, он готов к неблокирующему считыванию. Если 9 уже не находится в этой Мультиплексный ввод-вывод 84 группе, то он, вероятно, не сможет выполнить считывание без блокировки. Под «вероятно» здесь подразумевается, что данные для считывания могли стать до ступны уже после того, как произошел возврат вызова. В таком случае при после дующем вызове select() этот файловый дескриптор будет расцениваться как го товый для считывания 1 Первый параметр, n , равен наивысшему значению файлового дескриптора, при сутствующему во всех группах, плюс 1. Следовательно, сторона, вызывающая select() , должна проверить, какой из заданных файловых дескрипторов имеет наивысшее значение, а затем передать сумму (это значение плюс 1) первому параметру. Параметр timeout является указателем на структуру timeval , определяемую следующим образом: #include struct timeval { long tv_sec; /* секунды */ long tv_usec; /* микросекунды */ }; Если этот параметр не равен NULL , то вызов select() вернется через tv_sec секунд и tv_usec микросекунд, даже если ни один из файловых дескрипторов не будет готов к вводувыводу. После возврата состояние этой структуры в различных UNIXподобных системах не определено, поэтому должно инициализироваться заново (вместе с группами файловых дескрипторов) перед каждой активацией. На самом деле современные версии Linux автоматически изменяют этот параметр, устанавливая значения в оставшееся количество времени. Таким образом, если величина задержки была установлена в 5 секунд и истекло 3 секунды с момента, как файловый дескриптор перешел в состояние готовности, tv.tv_sec после воз врата вызова будет иметь значение 2 Если оба значения задержки равны нулю, то вызов вернется немедленно, сооб щив обо всех событиях, которые находились в режиме ожидания на момент вызо ва. Однако этот вызов не будет дожидаться никаких последующих событий. Манипуляции с файловыми дескрипторами осуществляются не напрямую, а по средством вспомогательных макрокоманд. Благодаря этому системы UNIX могут реализовывать группы дескрипторов так, как считается целесообразным. В боль шинстве систем, однако, эти группы реализованы как простые битовые массивы. FD_ZERO удаляет все файловые дескрипторы из указанной группы. Эта команда должна вызываться перед каждой активизацией select() : fd_set writefds; FD_ZERO(&writefds); 1 Дело в том, что вызовы select() и poll() являются обрабатываемыми по уровню, а не по фронту. Вызов epoll(), о котором мы поговорим в гл. 4, может работать в любом из этих режимов. Операции, обрабатываемые по фронту, проще, но если пользоваться ими не аккуратно, то некоторые события могут быть пропущены. Глава 2. Файловый ввод-вывод 85 FD_SET добавляет файловый дескриптор в указанную группу, а FD_CLR удаляет дескриптор из указанной группы: FD_SET(fd, &writefds); /* добавляем 'fd' к группе */ FD_CLR(fd, &writefds); /* ой, удаляем 'fd' из группы */ В качественном коде практически не должно встречаться случаев, в которых приходится воспользоваться FD_CLR , поэтому данная команда действительно ис пользуется очень редко. FD_ISSET проверяет, принадлежит ли определенный файловый дескриптор к кон кретной группе. Если дескриптор относится к группе, то эта команда возвращает ненулевое целое число, а если не относится, возвращает 0 FD_ISSET используется после возврата вызова от select() . С его помощью мы проверяем, готов ли опреде ленный файловый дескриптор к действию: if (FD_ISSET(fd, &readfds)) /* 'fd' доступен для неблокирующего считывания! */ Группы файловых дескрипторов создаются в статическом режиме, поэтому устанавливается лимит на максимальное количество дескрипторов, которые могут находиться в группах. Кроме того, задается максимальное значение, которое может иметь какойлибо из этих дескрипторов. Оба значения определяются командой FD_SETSIZE . В Linux данное значение равно 1024 . Далее в этой главе мы рассмотрим случаи отклонения от данного максимального значения. Возвращаемые значения и коды ошибок В случае успеха select() возвращает количество файловых дескрипторов, готовых для вводавывода, во всех трех группах. Если была задана задержка, то возвраща емое значение может быть равно нулю. При ошибке вызов возвращает значение –1 , а errno устанавливается в одно из следующих значений: EBADF — в одной из трех групп оказался недопустимый файловый дескрип тор; EINVAL — сигнал был получен в период ожидания, и вызов можно повторить; ENOMEM — запрос не был выполнен, так как не был доступен достаточный объем памяти. Пример использования select() Рассмотрим пример тривиальной, но полностью функциональной программы. На нем вы увидите использование вызова select() . Эта программа блокируется, дожидаясь поступления ввода на stdin , блокировка может продолжаться вплоть до 5 секунд. Эта программа отслеживает лишь один файловый дескриптор, поэтому здесь отсутствует мультиплексный вводвывод как таковой. Однако данный пример должен прояснить использование этого системного вызова: #include #include Мультиплексный ввод-вывод 86 #include #include #define TIMEOUT 5 /* установка тайм-аута в секундах */ #define BUF_LEN 1024 /* длина буфера считывания в байтах */ int main (void) { struct timeval tv; fd_set readfds; int ret; /* Дожидаемся ввода на stdin. */ FD_ZERO(&readfds); FD_SET(STDIN_FILENO, &readfds); /* Ожидаем не дольше 5 секунд. */ tv.tv_sec = TIMEOUT; tv.tv_usec = 0; /* Хорошо, а теперь блокировка! */ ret = select (STDIN_FILENO + 1, &readfds, NULL, NULL, &tv); if (ret == –1) { perror ("select"); return 1; } else if (!ret) { printf ("%d seconds elapsed.\n", TIMEOUT); return 0; } /* * Готов ли наш файловый дескриптор к считыванию? * (Должен быть готов, так как это был единственный fd, * предоставленный нами, а вызов вернулся ненулевым, * но мы же тут просто развлекаемся.) */ if (FD_ISSET(STDIN_FILENO, &readfds)) { char buf[BUF_LEN+1]; int len; /* блокировка гарантированно отсутствует */ len = read (STDIN_FILENO, buf, BUF_LEN); if (len == –1) { perror ("read"); return 1; } if (len) { Глава 2. Файловый ввод-вывод 87 buf[len] = '\0'; printf ("read: %s\n", buf); } return 0; } fprintf(stderr, "Этого быть не должно!\n"); return 1; } Использование select() для краткого засыпания Исторически на различных UNIXподобных системах вызов select() был более распространен, чем механизм засыпания с разрешающей способностью менее се кунды, поэтому данный вызов часто используется как механизм для кратковремен ного засыпания. Чтобы использовать select() в таком качестве, достаточно указать ненулевую задержку, но задать NULL для всех трех групп: struct timeval tv; tv.tv_sec = 0; tv.tv_usec = 500; /* засыпаем на 500 микросекунд */ select (0, NULL, NULL, NULL, &tv); В Linux предоставляются интерфейсы для засыпания с высоким разрешением. О них мы подробно поговорим в гл. 11. Вызов pselect() Системный вызов select() , впервые появившийся в 4.2BSD, достаточно популярен, но в POSIX есть и собственный вариант решения — вызов pselect() . Он был описан сначала в POSIX 1003.1g2000, а затем в POSIX 1003.12001: #define _XOPEN_SOURCE 600 #include int pselect (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, const struct timespec *timeout, const sigset_t *sigmask); /* эти же значения используются и сselect() */ FD_CLR(int fd, fd_set *set); FD_ISSET(int fd, fd_set *set); FD_SET(int fd, fd_set *set); FD_ZERO(fd_set *set); Между системными вызовами pselect() и select() есть три различия. 1. Вызов pselect() использует для своего параметра timeout структуру timespec , а не timeval . Структура timespec может иметь значения в секундах и наносекундах, Мультиплексный ввод-вывод 88 а не в секундах и микросекундах. Теоретически timespec должна создавать за держки с более высоким разрешением, но на практике ни одна из этих структур не может надежно обеспечивать даже разрешение в микросекундах. 2. При вызове pselect() параметр timeout не изменяется. Следовательно, не требу ется заново инициализировать его при последующих вызовах. 3. Системный вызов select() не имеет параметра sigmask . Если при работе с сиг налами установить этот параметр в значение NULL , то pselect() станет функцио нально аналогичен select() Структура timespec определяется следующим образом: #include struct timespec { long tv_sec; /* секунды */ long tv_nsec; /* наносекунды */ }; Основная причина, по которой вызов pselect() был добавлен в инструментарий UNIX, связана с появлением параметра sigmask . Этот параметр призван справлять ся с условиями гонки, которые могут возникать при ожидании файловых дескрип торов и сигналов. Подробнее о сигналах мы поговорим в гл. 10. Предположим, что обработчик сигнала устанавливает глобальный флаг (большинство из них именно так и делают), а процесс проверяет этот флаг перед вызовом select() . Далее пред положим, что сигнал приходит в период после проверки, но до вызова. Приложение может оказаться заблокированным на неопределенный срок и так и не отреагиро вать на установленный флаг. Вызов pselect() позволяет решить эту проблему: приложение может вызвать pselect() , предоставив набор сигналов для блокирова ния. Заблокированные сигналы не обрабатываются, пока не будут разблокированы. Как только pselect() вернется, ядро восстановит старую маску сигнала. До версии ядра Linux 2.6.16 pselect() был реализован в этой операционной системе не как системный вызов, а как обычная обертка для вызова select() , пре доставляемая glibc . Такая обертка сводила к минимуму риск возникновения усло вий гонки, но не исключала его полностью. Когда pselect() стал системным вызо вом, проблема с условиями гонки была решена. Несмотря на (относительно незначительные) улучшения, характерные для pselect() , в большинстве приложений продолжает использоваться вызов select() Это может делаться как по привычке, так и для обеспечения оптимальной перено симости. |