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

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

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

  • Системный вызов ppoll()

  • Сравнение poll() и select()

  • Внутренняя организация ядра

  • Виртуальная файловая система

  • Страничный кэш

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


    Скачать 0.65 Mb.
    НазваниеLinux. Системное программирование. Вступление
    АнкорСистемное программирование Линукс
    Дата23.01.2023
    Размер0.65 Mb.
    Формат файлаpdf
    Имя файлаLinuxSystemProgramming.pdf
    ТипКраткое содержание
    #900372
    страница12 из 14
    1   ...   6   7   8   9   10   11   12   13   14
    Системный вызов poll()
    Вызов poll()
    является в System V как раз тем решением, которое обеспечивает мультиплексный ввод­вывод. Он компенсируют некоторые недостатки, имеющие­
    ся у select()
    , хотя select()
    по­прежнему используется очень часто (как по привыч­
    ке, так и для обеспечения оптимальной переносимости):
    Глава 2. Файловый ввод-вывод

    89
    #include int poll (struct pollfd *fds, nfds_t nfds, int timeout);
    В отличие от вызова select()
    , применяющего неэффективный метод с тремя группами дескрипторов на основе битовых масок, poll()
    работает с единым масси­
    вом структур nfds pollfd
    , на которые указывают файловые дескрипторы. Такая структура определяется следующим образом:
    #include struct pollfd {
    int fd; /* файловый дескриптор */
    short events; /* запрашиваемые события для отслеживания */
    short revents; /* зафиксированные возвращаемые события */
    };
    В каждой структуре pollfd указывается один файловый дескриптор, который будет отслеживаться. Можно передавать сразу несколько структур, указав poll()
    отслеживать несколько файловых дескрипторов. Поле events каждой структуры представляет собой битовую маску событий, которые мы собираемся отслеживать на данном файловом дескрипторе. Ядро устанавливает это поле после возврата значения. Все события, возвращенные в поле events
    , могут быть возвращены в поле revents
    . Допустимы следующие события:
    
    POLLIN
    — имеются данные для считывания;
    
    POLLRDNORM
    — имеются обычные данные для считывания;
    
    POLLRDBAND
    — имеются приоритетные данные для считывания;
    
    POLLPRI
    — имеются срочные данные для считывания;
    
    POLLOUT
    — запись блокироваться не будет;
    
    POLLWRNORM
    — запись обычных данных блокироваться не будет;
    
    POLLWRBAND
    — запись приоритетных данных блокироваться не будет;
    
    POLLMSG
    — доступно сообщение
    SIGPOLL
    Кроме того, в поле revents могут быть возвращены следующие события:
    
    POLLER
    возникла ошибка на указанном файловом дескрипторе;
    
    POLLHUP
    — событие зависания на указанном файловом дескрипторе;
    
    POLLNVAL
    — указанный файловый дескриптор не является допустимым.
    Эти события не имеют никакого значения в поле events
    , и их не следует пере­
    давать в данное поле, поскольку при необходимости система всегда их возвращает.
    При использовании poll()
    , чего не скажешь о select()
    , не требуется явно задавать необходимость отчета об исключениях.
    Сочетание
    POLLIN | POLLPRI
    эквивалентно событию считывания в вызове select()
    , а событие
    POLLOUT | POLLWRBAND
    идентично событию записи в вызове select()
    . Значение
    POLLIN
    эквивалентно
    POLLRDNORM |POLLRDBAND
    , а
    POLLOUT
    эквива­
    лентно
    POLLWRNORM
    Мультиплексный ввод-вывод

    90
    Например, чтобы одновременно отслеживать на файловом дескрипторе воз­
    можность считывания и возможность записи, следует задать для параметра events значение
    POLLIN | POLLOUT
    . Получив возвращаемое значение, мы проверим поле revents на наличие этих флагов в структуре, соответствующей интересующему нас файловому дескриптору. Если бы флаг
    POLLOUT
    был установлен, то файловый дес­
    криптор был бы доступен для записи без блокирования. Эти флаги не являются взаимоисключающими: можно установить сразу оба, обозначив таким образом, что возможен возврат и при считывании, и при записи. Блокирования при считывании и записи на этом файловом дескрипторе не будет.
    Параметр timeout указывает задержку (длительность ожидания) в миллисекундах перед возвратом независимо от наличия или отсутствия готового ввода­вывода. От­
    рицательное значение соответствует неопределенно долгой задержке. Значение
    0
    предписывает вызову вернуться незамедлительно, перечислив все файловые дес­
    крипторы, на которых имеется ожидающий обработки ввод­вывод, но не дожидаясь каких­либо дальнейших событий. Таким образом, poll()
    оправдывает свое название: он совершает один акт опроса и немедленно возвращается.
    Возвращаемые значения и коды ошибок
    В случае успеха poll()
    возвращает количество файловых дескрипторов, чьи струк­
    туры содержат ненулевые поля revents
    . В случае возникновения задержки до каких­
    либо событий этот вызов возвращает
    0
    . При ошибке возвращается
    –1
    , а errno уста­
    навливается в одно из следующих значений:
    
    EBADF
    — для одной или нескольких структур были заданы недопустимые фай­
    ловые дескрипторы;
    
    EFAULT
    — значение указателя на файловые дескрипторы находится за пределами адресного пространства процесса;
    
    EINTR
    — до того как произошло какое­либо из запрошенных событий, был выдан сигнал; вызов можно повторить;
    
    EINVAL
    — параметр nfds превысил значение
    RLIMIT_NOFILE
    ;
    
    ENOMEM
    — для выполнения запроса оказалось недостаточно памяти.
    Пример использования poll()
    Рассмотрим пример программы, использующей вызов poll()
    для одновременной проверки двух условий: не возникнет ли блокировка при считывании с stdin и за­
    писи в stdout
    :
    #include
    #include
    #include
    #define TIMEOUT 5 /* задержка poll, значение в секундах */
    int main (void)
    {
    struct pollfd fds[2];
    Глава 2. Файловый ввод-вывод

    91
    int ret;
    /* отслеживаем ввод на stdin */
    fds[0].fd = STDIN_FILENO;
    fds[0].events= POLLIN;
    /* отслеживаем возможность записи на stdout (практически всегда true) */
    fds[1].fd = STDOUT_FILENO;
    fds[1].events = POLLOUT;
    /* Все установлено, блокируем! */
    ret= poll(fds, 2, TIMEOUT* 1000);
    if (ret == –1) {
    perror ("poll");
    return 1;
    }
    if (!ret) {
    printf ("%d seconds elapsed.\n", TIMEOUT);
    return 0;
    }
    if (fds[0].revents &POLLIN)
    printf ("stdin is readable\n");
    if (fds[1].revents &POLLOUT)
    printf ("stdout is writable\n");
    return 0;
    }
    Запустив этот код, мы получим следующий результат, как и ожидалось:
    $ ./poll stdout is writable
    Запустим его еще раз, но теперь перенаправим файл в стандартный ввод, и мы увидим оба события:
    $ ./pollЕсли бы мы использовали poll()
    в реальном приложении, то нам не требовалось бы реконструировать структуры pollfd при каждом вызове. Одну и ту же структу­
    ру можно передавать многократно; при необходимости ядро будет заполнять поле revents нулями.
    Системный вызов ppoll()
    Linux предоставляет вызов ppoll()
    , напоминающий poll()
    . Сходство между ними примерно такое же, как между select()
    и pselect()
    . Однако в отличие от pselect()
    вызов ppoll()
    является специфичным для Linux интерфейсом.
    Мультиплексный ввод-вывод

    92
    #define _GNU_SOURCE
    #include int ppoll (struct pollfd *fds,
    nfds_t nfds,
    const struct timespec *timeout,
    const sigset_t *sigmask);
    Как и в случае с pselect()
    , параметр timeout задает значение задержки в секундах и наносекундах. Параметр sigmask содержит набор сигналов, которых следует ожи­
    дать.
    Сравнение poll() и select()
    Системные вызовы poll()
    и select()
    выполняют примерно одну и ту же задачу, однако poll()
    удобнее, чем select()
    , по нескольким причинам.
    
    Вызов poll()
    не требует от пользователя вычислять и передавать в качестве па­
    раметра значение «максимальный номер файлового дескриптора плюс один».
    
    Вызов poll()
    более эффективно оперирует файловыми дескрипторами, име­
    ющими крупные номера. Предположим, мы отслеживаем с помощью select()
    всего один файловый дескриптор со значением
    900
    . В этом случае ядру пришлось бы проверять каждый бит в каждой из переданных групп вплоть до 900­го.
    
    Размер групп файловых дескрипторов, используемых с select()
    , задается ста­
    тически, что вынуждает нас идти на компромисс: либо делать их небольшими и, следовательно, ограничивать максимальное количество файловых дескрип­
    торов, которые может отслеживать select()
    , либо делать их большими, но не­
    эффективными. Операции с крупными масками битов неэффективны, особен­
    но если заранее не известно, применялось ли в них разреженное заполнение
    1
    С помощью poll()
    мы можем создать массив именно того размера, который нам требуется. Будем наблюдать только за одним элементом? Хорошо, передаем всего одну структуру.
    
    При работе с select()
    группы файловых дескрипторов реконструируются уже после возврата значения, поэтому каждый последующий вызов должен по­
    вторно их инициализировать. Системный вызов poll()
    разграничивает ввод
    (поле events
    ) и вывод (
    поле revents
    ), позволяя переиспользовать массив без изменений.
    
    Параметр timeout вызова select()
    на момент возврата значения имеет неопре­
    деленное значение. Переносимый код должен заново инициализировать его.
    При работе с вызовом pselect()
    такая проблема отсутствует.
    1
    Если битовая маска после заполнения получилась разреженной, то каждое слово, содер­
    жащееся в ней, можно проверить, сравнив его с нулем. Только если эта операция возвра­
    тит «ложно», потребуется отдельно проверять каждый бит. Тем не менее, если маска не разреженная, то это совершенно напрасная работа.
    Глава 2. Файловый ввод-вывод

    93
    Однако у системного вызова select()
    есть определенные преимущества:
    
    для select()
    характерна значительная переносимость, а вот poll()
    в некоторых системах UNIX не поддерживается;
    
    вызов select()
    обеспечивает более высокое разрешение задержки — до микро­
    секунд, тогда как poll()
    гарантирует разрешение лишь с миллисекундной точ­
    ностью; и ppoll()
    , и pselect()
    теоретически должны обеспечивать разрешение с точностью до наносекунд, но на практике ни один из этих вызовов не может предоставлять разрешение даже на уровне микросекунд.
    Оба — и poll()
    , и select()
    — уступают по качеству интерфейсу epoll
    . Это спе­
    цифичное для Linux решение, предназначенное для мультиплексного ввода­выво­
    да. Подробнее мы поговорим о нем в гл. 4.
    Внутренняя организация ядра
    В этом разделе мы рассмотрим, как ядро Linux реализует ввод­вывод. Нас в данном случае интересуют три основные подсистемы ядра: виртуальная файловая систе-
    ма (VFS), страничный кэш и страничная отложенная запись. Взаимодействуя, эти подсистемы обеспечивают гладкий, эффективный и оптимальный ввод­вывод.
    ПРИМЕЧАНИЕ
    В гл. 4 мы рассмотрим и четвертую подсистему — планировщик ввода-вывода.
    Виртуальная файловая система
    Виртуальная файловая система, которую иногда также называют виртуальным
    файловым коммутатором, — это механизм абстракции, позволяющий ядру Linux вызывать функции файловой системы и оперировать данными файловой системы, не зная — и даже не пытаясь узнать, — какой именно тип файловой системы при этом используется.
    VFS обеспечивает такую абстракцию, предоставляя общую модель файлов — ос­
    нову всех используемых в Linux файловых систем. С помощью указателей функций, а также с использованием различных объектно­ориентированных приемов
    1
    общая файловая модель образует структуру, которой должны соответствовать файловые системы в ядре Linux. Таким образом, виртуальная файловая система может делать обобщенные запросы в фактически применяемой файловой системе. В данном фреймворке предоставляются привязки для поддержки считывания, создания ссылок, синхронизации и т. д. Затем каждая файловая система регистрирует функ­
    ции для обработки операций, которые в ней обычно приходится выполнять.
    Такой подход требует определенной схожести между файловыми системами.
    Так, VFS работает в контексте индексных дескрипторов, суперблоков и записей
    1
    Да, на языке C.
    Внутренняя организация ядра

    94
    каталогов. Если приходится иметь дело с файловой системой, не относящейся к се­
    мейству UNIX, то в ней могут отсутствовать некоторые важные концепции UNIX, например индексные дескрипторы, и необходимо как­то с этим справляться. Пока это удается. Например, Linux может без проблем взаимодействовать с файловыми системами FAT и NTFS.
    Преимуществ использования VFS множество. Единственного системного вызо­
    ва достаточно для считывания информации из любой файловой системы, с любого носителя. Отдельно взятая утилита может копировать информацию из любой фай­
    ловой системы в какую угодно другую. Все файловые системы поддерживают одни и те же концепции, интерфейсы и системные вызовы. Все просто работает — и ра­
    ботает хорошо.
    Если определенное приложение выдает системный вызов read()
    , то путь этого вызова получается довольно интересным. Библиотека C содержит определения системного вызова, которые во время компиляции преобразуются в соответству­
    ющие операторы ловушки. Как только ядро поймает процесс из пользовательского пространства, переданный обработчиком системного вызова и «врученный» сис­
    темному вызову read()
    , ядро определяет, какой объект лежит в основе конкрет­
    ного файлового дескриптора. Затем ядро вызывает функцию считывания, ассо­
    циированную с этим базовым объектом. Данная функция входит в состав кода файловой системы. Затем функция выполняет свою задачу — например, физи­
    чески считывает данные из файловой системы — и возвращает полученные данные вызову read()
    , относящемуся к пользовательскому пространству. После этого read()
    возвращает информацию обработчику вызова, который, в свою очередь, копирует эти данные обратно в пользовательское пространство, где происходит возврат системного вызова read()
    и выполнение процесса продол­
    жается.
    Системный программист должен разбираться и в разновидностях файловой системы VFS. Обычно ему не приходится беспокоиться о типе файловой системы или носителя, на котором находится файл. Универсальные системные вызовы — read()
    , write()
    и т. д. — могут оперировать файлами в любой поддерживаемой файловой системе и на каком угодно поддерживаемом носителе.
    Страничный кэш
    Страничный кэш — это находящееся в памяти хранилище данных, которые взяты из файловой системы, расположенной на диске, и к которым недавно выполнялись обращения. Доступ к диску удручающе медленный, особенно по сравнению со скоростями современных процессоров. Благодаря хранению востребованных дан­
    ных в памяти ядро может выполнять последующие запросы к тем же данным, уже не обращаясь к диску.
    В страничном кэше задействуется концепция временной локальности. Это разно­
    видность локальности ссылок, согласно которой ресурс, запрошенный в определенный момент времени, с большой вероятностью будет снова запрошен в ближайшем бу­
    дущем. Таким образом, компенсируется память, затрачиваемая на кэширование
    Глава 2. Файловый ввод-вывод

    95
    данных после первого обращения: мы избавляемся от необходимости последующих затратных обращений к диску.
    Если ядру требуется найти данные о файловой системе, то оно первым делом обращается именно в страничный кэш. Ядро приказывает подсистеме памяти по­
    лучить данные с диска, только если они не будут обнаружены в страничном кэше.
    Таким образом, при первом считывании любого элемента данных этот элемент переносится с диска в системный кэш и возвращается к приложению уже из кэша.
    Все операции прозрачно выполняются через страничный кэш. Так гарантируется актуальность и правильность используемых данных.
    Размер страничного кэша Linux является динамическим. В результате операций ввода­вывода все больше информации с диска оседает в памяти, страничный кэш растет, пока наконец не израсходует всю свободную память. Если места для роста страничного кэша не осталось, но система снова совершает операцию выделения, требующую дополнительной памяти, страничный кэш урезается, высвобождая страницы, которые используются реже всего, и «оптимизируя» таким образом использование памяти. Такое урезание происходит гладко и автоматически. Раз­
    меры кэша задаются динамически, поэтому Linux может пользоваться всей памятью в системе и кэшировать столько данных, сколько возможно.
    Однако иногда бывает более целесообразно подкачивать на диск редко исполь­
    зуемую страницу процессной памяти, а не обрезать востребованные части странич­
    ного кэша, которые вполне могут быть заново перенесены в память уже при сле­
    дующем запросе на считывание (благодаря возможности подкачки ядро может хранить сравнительно большой объем данных на диске, таким образом выделяя больший объем памяти, чем общий объем RAM на данной машине). Ядро Linux использует эвристику, чтобы добиться баланса между подкачкой данных и уреза­
    нием страничного кэша (а также других резервных областей памяти). В рамках такой эвристики может быть решено выгрузить часть данных из памяти на диск ради урезания страничного кэша, особенно если выгружаемые таким образом дан­
    ные сейчас не используются.
    Баланс подкачки и кэширования можно настроить в
    /proc/sys/vm/swappiness
    Этот виртуальный файл может иметь значение в диапазоне от
    0
    до
    100
    . По умолча­
    нию оно равно
    60
    . Чем выше значение, тем выше приоритет урезания страничного кэша относительно подкачки.
    Еще одна разновидность локальности ссылок — это сосредоточенность после-
    довательности. В соответствии с данным принципом ссылка часто делается на ряд данных, следующих друг за другом. Чтобы воспользоваться преимуществом дан­
    ного принципа, ядро также реализует опережающее считывание страничного кэша.
    Опережающее считывание — это акт чтения с диска дополнительных данных с пе­
    ремещением их в страничный кэш. Опережающее считывание сопутствует каждо­
    му запросу и обеспечивает постоянное наличие в памяти некоторого избытка данных. Когда ядро читает с диска фрагмент данных, оно также считывает и один­
    два последующих фрагмента. Одновременное считывание сравнительно большой последовательности данных оказывается эффективным, так как обычно обходится без позиционирования (подвода головок). Кроме того, ядро может выполнить запрос
    Внутренняя организация ядра

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


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