poll и select

Предыдущая  Содержание  Следующая V*D*V

Приложения, использующие неблокирующий ввод/вывод, часто также используют системные вызовы poll, select и epoll. poll, select и epoll в сущности имеют одинаковую функциональность: каждый позволяет процессу определить, может ли он читать или записывать данные в один или более открытых файлов без блокировки. Эти вызовы могут также блокировать процесс, пока любой из заданного набора файловых дескрипторов не станет доступным для чтения или записи. Поэтому они часто используются приложениями, которые должны использовать много входных и выходных потоков, не застревая на каком-то одном из них. Такая же функциональность предлагается множеством функций, потому что две были реализованы в Unix почти в одно и то же время двумя разными группами: select (выбор) был введён в BSD Unix, тогда как poll (опрос) был решением System V. Вызов epoll (* На самом деле epoll представляет собой набор из трёх вызовов, которые вместе могут быть использованы для достижения функциональности последовательного опроса. Однако, для наших целей мы можем думать о нём как об одном вызове.) был добавлен в версии 2.5.45 в качестве одного из способов сделать функцию опроса масштабируемой к тысячам файловых дескрипторов.

 

Поддержка любого из этих вызовов требует поддержки со стороны драйвера устройства. Эта поддержка (для всех трёх вызовов) обеспечивается с помощью метода драйвера poll (опрос). Этот метод имеет следующий прототип:

 

unsigned int (*poll) (struct file *filp, poll_table *wait);

 

Метод драйвера вызывается всякий раз, когда программа пользовательского пространства выполняет системный вызов poll, select или epoll с участием дескриптора файла, связанного с драйвером. Метод устройства отвечает за эти два действия:

 

1.Вызвать poll_wait для одной или более очередей ожидания, что может свидетельствовать об изменении в статусе опроса. Если нет файловых дескрипторов, доступных в настоящее время для ввода/вывода, ядро заставит процесс ждать в очереди ожидания для всех файловых дескрипторов, переданных системному вызову.

2.Возврат битовой маски, описывающей операции (если таковые имеются), которые могут быть немедленно выполнены без блокировки.

 

Обе эти операции, как правило, просты и очень похожи от одного драйвера к другому. Предполагается, однако, что только драйвер может предоставить эту информацию и, следовательно, они должны быть реализованы индивидуально каждым драйвером.

 

Структура poll_table, второй аргумент метода poll, используется в ядре для реализации вызовов poll, select и epoll; она объявлена в <linux/poll.h>, которая должна быть подключена в исходник драйвера. Авторам драйверов не требуется знать о её внутренностях и они должны использовать её как непрозрачный объект; она передаётся в метод драйвера так, чтобы драйвер мог загружать её для каждой очереди ожидания, которая могла бы пробудить этот процесс и изменить статус операции poll. Драйвер добавляет очередь ожидания к структуре poll_table вызывая функцию poll_wait:

 

void poll_wait (struct file *filp, wait_queue_head_t *wait_queue, poll_table *wait);

 

Вторая задача выполняется методом poll возвращением битовой маски описания, какие операции могут быть завершены немедленно; это тоже просто. Например, если в устройстве имеются данные, чтение выполнилось бы без сна; методу poll следует показать такое положение дел. Для обозначения возможных операций используются несколько флагов (определяемых через <linux/poll.h>):

 

POLLIN

Этот бит должен быть установлен, если устройство может быть прочитано без блокировки.

 

POLLRDNORM

Этот бит должен быть установлен, если "нормальные" данные доступны для чтения. Читаемое устройство возвращает (POLLIN | POLLRDNORM).

 

POLLRDBAND

Этот бит показывает, что для чтения из устройства доступны данные вне логического канала связи (из дополнительного вспомогательного канала). В настоящее время используется только в одном месте в ядре Linux (код DECnet) и, как правило, не применим к драйверам устройств.

 

POLLPRI

Высокоприоритетные данные (вспомогательного канала) могут быть прочитаны без блокировки. Этот бит заставляет select сообщить, что на файле произошло условие исключения, потому что select сообщает о дополнительных данных как о состоянии исключения.

 

POLLHUP

Когда процесс, читающий это устройство, видит конец файла, драйвер должен установить POLLHUP (зависание). Процесс, вызывающий select говорит, что устройство это читаемо, как это диктуется функциональностью select.

 

POLLERR

В устройстве произошла ошибка. При вызове poll об устройстве сообщается как о читаемом и записываемом, а read и write возвращают код ошибки без блокирования.

 

POLLOUT

Этот бит устанавливается в возвращаемом значении, если в это устройство можно записать без блокировки.

 

POLLWRNORM

Этот бит имеет такое же значение, как POLLOUT и иногда он действительно является тем же номером. Записываемое устройство возвращает (POLLOUT | POLLWRNORM).

 

POLLWRBAND

Как и POLLRDBAND, этот бит означает, что данные с ненулевым приоритетом могут быть записаны в устройство. Только реализация poll для датаграммы (пакет данных + адресная информация) использует этот бит, так как датаграммы могут передавать данные в дополнительном канале.

 

Стоит повторить, что POLLRDBAND и POLLWRBAND имеют смысл только для файловых дескрипторов, связанных с сокетами: драйверы устройств обычно не будут использовать эти флаги.

 

Описание poll занимает много места для того, что является относительно простым для использования на практике. Рассмотрим реализацию метода poll в scullpipe:

 

static unsigned int scull_p_poll(struct file *filp, poll_table *wait)

{

    struct scull_pipe *dev = filp->private_data;

    unsigned int mask = 0;

 

    /*

     * Этот буфер является круговым; он считается полным

     * если "wp" находится прямо позади "rp" и пустым, если

     * они равны.

     */

    down(&dev->sem);

    poll_wait(filp, &dev->inq, wait);

    poll_wait(filp, &dev->outq, wait);

    if (dev->rp != dev->wp)

        mask |= POLLIN | POLLRDNORM; /* читаемо */

    if (spacefree(dev))

        mask |= POLLOUT | POLLWRNORM; /* записываемо */

    up(&dev->sem);

    return mask;

}

 

Этот код просто добавляет две очереди ожидания scullpipe к poll_table, затем устанавливает соответствующие битовые маски в зависимости от того, могут ли данные быть считаны или записаны.

 

Показанный код poll опускает поддержку "конца файла", потому что scullpipe не поддерживает условие "конец файла". Для большинства реальных устройств метод poll должен вернуть POLLHUP, если данных больше нет (или не будет). Если вызывающий использовал системный вызов select, об этом файле сообщается как о читаемом. Независимо от того, использовался poll или select, приложение знает, что оно может вызвать чтение не ожидая вечно и метод read вернётся, просигнализировав 0 о конце файла.

 

С реальными FIFO, например, читатель видит конец файла, когда все писатели закрывают этот файл, тогда как читатель scullpipe никогда не видит конца файла. Поведение отличается, потому что FIFO предназначен, чтобы быть каналом связи между двумя процессами, а scullpipe является мусорной корзиной, куда каждый может поместить данные, пока есть хотя бы один читатель. Кроме того, нет смысла заново реализовывать то, что уже доступно в ядре, так что мы выбрали в нашем примере другое поведение для реализации.

 

Реализация "конца файла" так же, как в FIFO, означало бы сделать проверку dev->nwriters в read и в poll и сообщать о "конце файла" (как описано выше), если нет процесса, имеющего устройство открытым для записи. Однако, к сожалению, при такой реализации, если читатель открыл устройство scullpipe перед писателем, он будет видеть "конец файла", не имея шанса дождаться данных. Лучшим способом решить эту проблему будет осуществлять блокировку в open, как это делают реальные FIFO; эта задача остаётся как упражнение для читателя.

Взаимодействие с read и write

Целью вызовов poll и select является определить заранее, будет ли операция ввода/вывода блокирована. В этом отношении они дополняют read и write. Что более важно, poll и select полезны, потому что они позволяют приложению одновременно ожидать несколько потоков данных, хотя мы и не используем эту функцию в примерах scull. Чтобы сделать работу корректной, необходима правильная реализация трёх вызовов: хотя о следующих правилах уже более или менее говорилось, мы просуммировали их здесь.

Чтение данных из устройства

Если есть данные во входном буфере, вызов read должен вернуться немедленно, без каких-либо заметных задержек, даже если доступно меньше данных, чем запрошено приложением, и драйвер уверен, что оставшиеся данные прибудут в ближайшее время. Вы всегда можете вернуть меньше данных, чем запрошено, по крайней мере один байт, если это удобно по любой причине (мы делали это в scull). В этом случае poll должен вернуть POLLIN | POLLRDNORM.

В случае отсутствия данных во входном буфере, по умолчанию read должен блокироваться, пока нет по крайней мере одного байта. С другой стороны, если установлен O_NONBLOCK, read сразу же возвращается со значением -EAGAIN (хотя в некоторых старых версиях System V в данном случае возвращается 0). В этих случаях poll должен сообщить, что устройство не доступно для чтения, пока не будет получен по крайней мере один байт. Как только в буфере оказываются некоторые данные, мы откатываемся к предыдущему случаю.

Если мы в конце файла, read должна немедленно вернуться с возвращаемым значением 0, независимо от O_NONBLOCK. poll в этом случае должен сообщить POLLHUP.

Запись в устройство

Если есть место в выходном буфере, write должен вернуться без задержки. Он может принять меньше данных, чем запросил вызов, но он должен принять как минимум один байт. В этом случае poll сообщает, что устройство доступно для записи, возвращая POLLOUT | POLLWRNORM.

Если выходной буфер заполнен, по умолчанию write блокируется, пока не освобождается некоторое пространство. Если установлен O_NONBLOCK, write немедленно возвращается со значением -EAGAIN (старые System V возвращали 0). В этих случаях poll должен сообщить, что файл не доступен для записи. Если, с другой стороны, устройство не может принять какие-либо дополнительные данные, write возвращает -ENOSPC (“No space left on device”, "Нет места на устройстве") независимо от установки O_NONBLOCK.

Никогда не делайте ожидания в вызове write для ожидания передачи данных, прежде чем вернуться, даже если O_NONBLOCK очищен. Многие приложения используют select, чтобы узнать, будет ли блокирован write. Если об устройстве сообщается как о доступном для записи, вызов не должен блокироваться. Если программа, использующая устройство, стремится к тому, что данные из очереди в выходном буфере передавались на самом деле, такой драйвер должен предоставить метод fsync. Например, съёмное устройство должно иметь точку входа fsync.

 

Хотя это хороший набор общих правил, следует также признать, что каждое устройство является уникальным, и что иногда правила должны быть слегка изменены. Например, устройства, ориентированные на запись (такие как ленточные накопители), не могут выполнять частичные записи.

Сброс на диск в процессе вывода

Мы видели, как метод write сам по себе не учитывает всех потребностей передачи данных. Функция fsync, вызываемая системным вызовом с тем же именем, заполняет этот пробел. Прототипом метода является

 

int (*fsync) (struct file *file, struct dentry *dentry, int datasync);

 

Если какое-либо приложение всегда должно быть уверено, что данные были отправлены в устройство, метод fsync должен выполняться независимо от того, установлен ли O_NONBLOCK. Вызов fsync должен вернуться только тогда, когда данные в устройство были полностью  переданы (то есть, выходной буфер пуст), даже если это занимает некоторое время. Аргумент datasync используется, чтобы различать системные вызовы fsync и fdatasync; как таковой, он представляет интерес только для кода файловой системы и может быть проигнорирован драйверами.

 

Метод fsync не имеет необычных особенностей. Вызов не критичен по времени, так что в каждом драйвере устройства можно реализовать его на вкус автора. В большинстве случаев символьные драйвера просто имеют указатель NULL в их fops. Блочные устройства, с другой стороны, всегда реализуют метод общего назначения block_fsync, который, в свою очередь, сбрасывает все блоки устройства, ожидая завершения ввода/вывода.

Нижележащая структура данных

Фактическая реализация системных вызовов poll и select является достаточно простой для тех, кто заинтересовался, как это работает; epoll является немного более сложным, но построен на том же механизме. Всякий раз, когда пользовательское приложение вызывает poll, select или epoll_ctl (* Это функция, которая создаёт внутреннюю структуру данных для будущих вызовов epoll_wait.), ядро вызывает метод poll всех файлов, на которые ссылается системный вызов, передавая каждому из них ту же poll_table. Структура poll_table является только обёрткой вокруг функции, которая создаёт реальную структуру данных. Для poll и select эта структура является связным списком страниц памяти, содержащих структуры poll_table_entry. Каждая poll_table_entry содержит структуру file и указатели wait_queue_head_t передаются в poll_wait наряду со связанным объектом очереди ожидания. Вызов poll_wait иногда также добавляет этот процесс к данной очереди ожидания. В целом вся структура должна обслуживаться ядром так, чтобы этот процесс мог быть удалён из всех этих очередей до возврата poll или select.

 

Если ни один из опрошенных драйверов не показывает, что ввод/вывод может происходить без блокировки, вызов poll просто спит, пока одна из (может быть многих) очередей ожидания его не разбудит.

 

Интересным в реализации poll является то, что метод драйвера poll может быть вызван с указателем NULL в качестве аргумента poll_table. Эта ситуация может произойти по нескольким причинам. Если приложение, вызывающее poll, передало значение времени ожидания 0 (что свидетельствует, что не должно быть ожидания), нет никаких причин, чтобы собирать очередь ожидания, и система просто ничего не делает. Указатель poll_table также сразу устанавливается в NULL после любого опроса драйвера, показывающего, что ввод/вывод возможен. Так как с этих пор ядро знает, что ожидания не будет, оно не строит список очередей ожидания.

 

После завершения вызова poll структура poll_table освобождается и все объекты очереди ожидания, ранее добавленные к таблице опроса (если таковая имеется), будут удалены из таблицы и их очередей ожидания.

 

Мы постарались показать на Рисунке 6-1 структуры данных, участвующие в опросе; этот рисунок является упрощённым представлением реальной структуры данных, поскольку он игнорирует многостраничный характер таблицы опроса и игнорирует файловый указатель, который является частью каждой poll_table_entry. Читателю, заинтересованному в фактической реализации, настоятельно рекомендуется заглянуть в <linux/poll.h> и fs/select.c.

 

Рисунок 6-1. Структура данных, скрывающаяся за poll

Рисунок 6-1. Структура данных, скрывающаяся за poll

 

На данный момент можно понять мотив для нового системного вызова epoll. В типичном случае вызов poll или select включает в себя лишь небольшое количество файловых дескрипторов, так что расходы на создание структуры данных невелики. Однако, существуют приложения, которые работают с тысячами файловых дескрипторов. При этом создание и удаление этой структуры данных между каждой операцией ввода/вывода становится непомерно дорогим. Семейство системного вызова epoll позволяет приложениям такого вида создать внутреннюю структуру данных ядра только один раз и использовать её много раз.

Предыдущая  Содержание  Следующая