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

  • Усечение файлов

  • Мультиплексный ввод-вывод

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

  • Пример использования select()

  • Использование select() для краткого засыпания

  • Вызов pselect()

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


    Скачать 0.65 Mb.
    НазваниеLinux. Системное программирование. Вступление
    АнкорСистемное программирование Линукс
    Дата23.01.2023
    Размер0.65 Mb.
    Формат файлаpdf
    Имя файлаLinuxSystemProgramming.pdf
    ТипКраткое содержание
    #900372
    страница11 из 14
    1   ...   6   7   8   9   10   11   12   13   14
    Значения ошибок. В случае успеха оба вызова возвращают количество байтов, которые соответственно были прочитаны или записаны. Возвращаемое значение
    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.1g­2000, а затем в POSIX 1003.1­2001:
    #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()
    Это может делаться как по привычке, так и для обеспечения оптимальной перено­
    симости.
    1   ...   6   7   8   9   10   11   12   13   14


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