Что такое дескрипторы на самом деле?

Основным строительным блоком всех операций ввода-вывода в Unix является последовательность байтов. Большинство программ работают с еще более простой абстракцией - потоком байтов или потоком ввода-вывода.

Процесс ссылается на потоки ввода-вывода с помощью дескрипторов, также известных как файловые дескрипторы . Каналы, файлы, FIFO, IPC POSIX (очереди сообщений, семафоры, разделяемая память), очереди событий - все это примеры потоков ввода-вывода, на которые ссылается дескриптор .

Создание и выпуск дескрипторов

Дескрипторы либо создаются явно системными вызовами, такими как open, pipe, socket и т. Д., Либо наследуются от родительский процесс.

Дескрипторы освобождаются, когда:

- процесс завершается,
- вызывая системный вызов close
- неявно после exec , когда дескриптор помечен как закрыть при выполнении.

Close-on-exec

Когда процесс разветвляется, все дескрипторы «дублируются» в дочернем процессе. Если какой-либо из дескрипторов помечен как закрыть при выполнении, то после родительского разветвления, но перед дочерним execs, дескрипторы в дочернем элементе помечены как «Close-on-exec» закрываются и больше не будут доступны для дочернего процесса.

Передача данных происходит с помощью системного вызова read или write для дескриптора.

Вход в файл

Каждый дескриптор указывает на структуру данных, называемую записью файла в ядре. запись файла поддерживает для каждого дескриптора смещение файла в байтах от начала объекта записи файла. Системный вызов open создает новую запись файла.

Форк / дублирование и файловые записи

Системный вызов fork приводит к тому, что дескрипторы совместно используются родительским и дочерним объектами с совместным использованием по ссылке семантика. И родительский, и дочерний используют один и тот же дескриптор и ссылаются на одинаковое смещение в записи файла. Та же семантика применяется к системному вызову dup / dup2, используемому для дублирования дескриптора файла.

#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
int main(char *argv[]) {
    int fd = open("abc.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
    fork();
    write(fd, "xyz", 3);
    printf("%ld\n", lseek(fd, 0, SEEK_CUR));
    close(fd);
    return 0;
}

который печатает:

3
6

Более интересно то, что делает флаг close-on-exec, если дескрипторы только общие. Я предполагаю, что установка флага удаляет дескриптор из таблицы дескрипторов дочернего элемента, так что родитель может продолжать использовать дескриптор, но дочерний элемент не сможет использовать его, когда у него будет exec -ед.

Смещение на дескриптор

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

Анатомия файловой записи

Каждая запись файла содержит:

- тип
- массив указателей на функции. Этот массив указателей на функции переводит общие операции с файловыми дескрипторами в реализации конкретных типов файлов.

Немного устраняя неоднозначность, все файловые дескрипторы предоставляют общий общий API, который указывает операции (такие как чтение, запись, изменение режима дескриптора, усечение дескриптора, ioctl операции, опрос и т. д.), которые могут выполняться с дескриптором.

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

Большая часть сетей выполняется с использованием сокетов. На сокет ссылается дескриптор, и он действует как конечная точка для связи. Два процесса могут создать по два сокета каждый и установить надежный поток байтов, соединив эти две конечные точки. Как только соединение установлено, дескрипторы могут быть прочитаны или записаны с использованием описанных выше смещений файлов. Ядро может перенаправлять вывод одного процесса на ввод другого на другой машине. Одни и те же системные вызовы read и write используются для соединений типа байтового потока, но разные системные вызовы обрабатывают адресованные сообщения, такие как сетевые дейтаграммы.

Неблокирующие дескрипторы

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

Любой дескриптор (каналы, FIFO, сокеты, терминалы, псевдотерминалы и некоторые другие типы устройств) можно перевести в неблокирующий режим. Когда дескриптор установлен в неблокирующий режим, системный вызов ввода-вывода для этого дескриптора будет немедленно возвращен, даже если этот запрос не может быть выполнен немедленно (и будет следовательно, в противном случае процесс будет заблокирован). Возвращаемое значение может быть одним из следующих:

- ошибка: когда операция не может быть завершена вообще
- частичный счет: когда операция ввода или вывода может быть частично завершена
- весь результат: когда операция ввода-вывода может быть полностью завершена

Дескриптор переводится в неблокирующий режим путем установки флага no-delay O_NONBLOCK. Это также называется флагом статуса открытого файла (в glibc флаги открытого файла - это флаги, определяющие поведение системного вызова open. Обычно эти параметры не применяются после открытия файла, но O_NONBLOCK является исключением, поскольку также рабочий режим ввода-вывода).

Готовность дескрипторов

Дескриптор считается готовым, если процесс может выполнить операцию ввода-вывода для дескриптора без блокировки. Чтобы дескриптор считался «готовым», не имеет значения, операция действительно передает какие-либо данные - важно только то, что операция ввода-вывода может быть выполняется без блокировки.

Дескриптор переходит в состояние готов, когда происходит событие ввода-вывода, такое как поступление нового ввода или завершение подключения к сокету, или когда пространство доступно на ранее заполненный буфер отправки сокета после того, как TCP передает данные из очереди одноранговому сокету.

Есть два способа узнать о состоянии готовности дескриптора - срабатывание по фронту и запуск по уровню.

Уровень срабатывает

Я рассматриваю это как модель «тяги» или модель «опроса». Чтобы определить, готов ли дескриптор, процесс пытается выполнить неблокирующую операцию ввода-вывода. Процесс может выполнять такие операции любое количество раз. Это обеспечивает большую гибкость в отношении обработки любых последующих операций ввода-вывода - например, если дескриптор готов, процесс может выбрать либо читать все доступные данные, либо не выполнять никаких Ввод-вывод вообще или выберите не читать все входные данные, доступные в буфере. Давайте посмотрим, как это работает, на примере.

В момент времени t0 процесс может попытаться выполнить операцию ввода-вывода на неблокирующем дескрипторе. Если операция ввода-вывода блокируется, системный вызов возвращает ошибку.

Затем в момент t1 процесс может снова попытаться выполнить ввод-вывод для дескриптора. Допустим, вызов снова блокируется и возвращается ошибка.

Затем в момент t2 процесс снова пытается выполнить ввод-вывод для дескриптора. Предположим, вызов снова блокируется и возвращается ошибка.

Допустим, в момент времени t3 процесс опрашивает состояние дескриптора, и дескриптор готов. Затем процесс может выбрать фактическое выполнение всей операции ввода-вывода (например, прочитать все данные, доступные в сокете).

Предположим, что в момент времени t4 процесс опрашивает состояние дескриптора, но дескриптор не готов. Вызов снова блокируется, и операция ввода-вывода возвращает ошибку.

Предположим, что в момент времени t5 процесс опрашивает состояние дескриптора, и дескриптор готов. Впоследствии процесс может выбрать выполнение только частичной операции ввода-вывода (например, чтение только половины всех доступных данных).

Предположим, что в момент t6 процесс опрашивает состояние дескриптора, и дескриптор готов. На этот раз процесс может вообще не выполнять последующий ввод-вывод.

Edge Triggered

Процесс получает уведомление только тогда, когда файловый дескриптор «готов» (обычно, когда есть какие-либо новые действия в файловом дескрипторе с момента последнего наблюдения). Я рассматриваю это как модель «проталкивания», в которой процессу отправляется уведомление о готовности дескриптора файла. Кроме того, с моделью push процесс уведомляется только о том, что дескриптор готов к вводу-выводу, но не предоставляет дополнительную информацию, например, сколько байтов поступило в буфер сокета.

Таким образом, процесс вооружается только неполными данными, поскольку он пытается выполнить любую последующую операцию ввода-вывода. Чтобы обойти это, процесс может попытаться выполнить максимальное количество операций ввода-вывода, которое он, возможно, может каждый раз, когда получит уведомление о готовности дескриптора, поскольку в противном случае процессу придется ждать до прибытия следующего уведомления, даже если ввод-вывод возможен для дескриптора до прибытия следующего уведомления.

Давайте посмотрим, как это работает, на следующем примере.

В момент времени t2 процесс получает уведомление о готовности дескриптора.

Поток байтов, доступный для ввода-вывода, сохраняется в буфере. Предположим, что 1024 байта доступны для чтения, когда процесс получает уведомление в момент времени t2.

Предположим, процесс считывает только 500 из 1024 байтов.

Это означает, что временами t3, t4 и t5 в буфере все еще есть 524 байта, которые процесс может читать без блокировки. Но поскольку процесс может выполнять ввод-вывод только после получения следующего уведомления, эти 524 байта остаются в буфере в течение этого времени.

Предположим, процесс получает следующее уведомление в момент времени t6, когда в буфер поступило 1024 дополнительных байта. Общий объем данных, доступных в буфере, теперь составляет 1548 байтов - 524 байта, которые ранее не считывались, и 1024 байта, которые были получены вновь.

Предположим, процесс теперь читает 1024 байта.

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

Хотя может показаться заманчивым выполнить все операции ввода-вывода сразу после получения уведомления, это имеет последствия. Большая операция ввода-вывода для одного дескриптора может привести к нехватке ресурсов для других дескрипторов. Кроме того, даже в случае уведомлений, инициируемых уровнем, очень большой вызов write или send может быть заблокирован.

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

В предыдущем разделе мы только описали, как процесс обрабатывает ввод-вывод для единственного дескриптора. Часто процессу может потребоваться обработать ввод-вывод более чем для одного дескриптора. Чрезвычайно распространенный вариант использования - это когда программе необходимо выполнять вход в stdout и stderr, а также принимать соединения через сокет и выполнять исходящие соединения RPC с другими службами.

Есть несколько способов мультиплексирования ввода-вывода на дескрипторах:

- неблокирующий ввод-вывод (сам дескриптор помечен как неблокирующий, операции могут завершаться частично)
- ввод-вывод, управляемый сигналом (процесс, владеющий дескриптором, уведомляется, когда Состояние ввода-вывода дескриптора изменяется)
- опрос ввода-вывода (с помощью select или poll системные вызовы, оба из которых предоставляют уведомления срабатывания уровня о готовности дескрипторов)
- опрос событий ядра BSD (с помощью kevent системный вызов).

Мультиплексирование ввода-вывода с неблокирующим вводом-выводом

Что происходит с дескрипторами?

Когда у нас есть несколько файловых дескрипторов, мы можем перевести их все в неблокирующий режим.

Что происходит в процессе?

Процесс может попытаться выполнить операцию ввода-вывода для дескрипторов, чтобы проверить, не приводит ли какая-либо из операций ввода-вывода к ошибке.

Что происходит в ядре?

Ядро выполняет операцию ввода-вывода для дескриптора и возвращает ошибку, частичный вывод или результат операции ввода-вывода, если она завершается успешно.

Какие минусы?

Частые проверки. Если процесс пытается выполнять операции ввода-вывода очень часто, он должен постоянно повторять операции, которые вернули ошибку, чтобы проверить, готовы ли какие-либо дескрипторы. Такое ожидание занятости в замкнутом цикле может привести к сжиганию циклов ЦП.

Нечастые проверки. Если такие операции выполняются нечасто, то процессу может потребоваться неприемлемо много времени, чтобы отреагировать на доступное событие ввода-вывода.

Когда может иметь смысл использовать этот подход?

Операции с дескрипторами вывода (например, записью) обычно не блокируются. В таких случаях может помочь попытаться сначала выполнить операцию ввода-вывода и вернуться к опросу, когда операция вернет ошибку. Также имеет смысл использовать этот подход с уведомлениями с запуском по фронту, где дескрипторы могут быть переведены в неблокирующий режим, и как только процесс получает уведомление о событии ввода-вывода, он может повторно попробовать I Операции / O, пока системные вызовы не заблокируются с помощью EAGAIN или EWOULDBLOCK.

Мультиплексирование ввода-вывода через управляемый сигналом ввод-вывод

Что происходит с дескрипторами?

Ядру дается указание посылать процессу сигнал, когда ввод-вывод может быть выполнен для любого из дескрипторов.

Что происходит в процессе?

Процесс будет ждать доставки сигналов, когда любой дескриптор будет готов к операции ввода-вывода.

Что происходит в ядре?

Отслеживает список дескрипторов и отправляет процессу сигнал каждый раз, когда любой из дескрипторов становится готовым для ввода-вывода.

Какие минусы такого подхода?

Улавливание сигналов связано с большими затратами, что делает ввод-вывод, управляемый сигналом, непрактичным в случаях, когда выполняется большой объем операций ввода-вывода.

Когда может иметь смысл использовать этот подход?

Обычно он используется для «исключительных условий», когда стоимость обработки сигнала ниже, чем стоимость постоянного опроса с помощью select / poll / epoll или kevent . Примером «исключительного случая» является поступление внеполосных данных в сокет или когда изменение состояния происходит на псевдотерминальном вторичном сервере, подключенном к ведущему в пакетном режиме.

Мультиплексирование ввода-вывода через ввод-вывод с опросом

Что происходит с дескрипторами?

Дескрипторы переводятся в неблокирующий режим.

Что происходит в процессе?

Процесс использует механизм срабатывания уровня, чтобы запросить ядро ​​с помощью системного вызова (select или poll), какие дескрипторы способны выполнять I / O. Ниже описывается реализация как select, так и poll.

Выбирать

Подпись select на Дарвине:

int
select(
    int nfds,
    fd_set *restrict readfds,
    fd_set *restrict writefds,
    fd_set *restrict errorfds,
    struct timeval *restrict timeout
);

в то время как в Linux это:

int
select(
   int nfds,
   fd_set *readfds,
   fd_set *writefds,
   fd_set *exceptfds,
   struct timeval *timeout
);

Select отслеживает три независимых набора дескрипторов:

- дескрипторы readfds отслеживаются, чтобы увидеть, не будет ли чтение заблокировано (когда байты становятся доступными для чтения или при обнаружении EOF)
- дескрипторы writefds отслеживаются, когда запись не блокируется.
- дескрипторы exceptfds отслеживаются на предмет исключительных условий.

Когда select возвращается, дескрипторы изменяются на месте, чтобы указать, какие дескрипторы файлов фактически изменили статус. Любой из трех наборов файловых дескрипторов может иметь значение NULL, если мы не хотим отслеживать эту конкретную категорию событий.

Последний аргумент - это значение тайм-аут, которое указывает, как долго система select звонок заблокируется:

- когда для тайм-аута установлено значение 0, select не блокируется, а возвращается сразу после опроса файловых дескрипторов < br /> - если для тайм-аута установлено значение NULL, select будет блокироваться «навсегда». Когда select блокируется, ядро ​​может перевести процесс в спящий режим до тех пор, пока select не вернется. Select будет блокироваться до тех пор, пока 1) один или несколько дескрипторов, указанных в трех описанных выше наборах, не будут готовы или 2) вызов будет прерван обработчиком сигнала
- когда Для тайм-аута задано определенное значение, тогда select будет блокироваться до тех пор, пока 1) один или несколько дескрипторов, указанных в три набора, описанные выше, готовы или 2) вызов прерван обработчиком сигнала или 3) время, указанное в тайм-ауте, истекло

Возвращаемые значения select следующие:

- если произошла ошибка (EBADF или EINTR), то код возврата -1
- если время ожидания вызова истекло до того, как какой-либо дескриптор стал готовым, то код возврата равен 0
- если один или готовы больше файловых дескрипторов, тогда код возврата представляет собой положительное целое число, которое указывает общее количество файловых дескрипторов во всех трех готовых наборах. Затем каждый набор проверяется индивидуально, чтобы выяснить, какое событие ввода-вывода произошло.

Опрос

Опрос отличается от select только тем, как мы указываем какие дескрипторы для отслеживания.

С помощью select мы передаем три набора дескрипторов, которые хотим отслеживать на предмет чтения, записи и исключительных случаев.

С помощью poll мы передаем набор дескрипторов, каждый из которых отмечен событиями, которые необходимо отслеживать.

Подпись опроса по Дарвину:

int poll(
    struct pollfd fds[],
    nfds_t nfds,
    int timeout
);

А в Linux это:

int poll(
    struct pollfd *fds,
    nfds_t nfds,
    int timeout
);

Первый аргумент poll - это массив всех дескрипторов, которые мы хотим отслеживать.

Структура pollfd содержит три части информации:

- идентификатор опрашиваемого дескриптора (назовем этот дескриптор A)
- битовые маски, указывающие, какие события следует отслеживать для данного дескриптора A (события) < br /> - битовые маски, установленные ядром, указывающие на события, которые действительно произошли в дескрипторе A (revents)

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

Третий тайм-аут указывает, как долго опрос будет блокироваться при каждом его вызове.

- при значении -1 опрос блокируется на неопределенное время, пока один из дескрипторов, перечисленных в первом аргументе, не будет готов или пока не будет обнаружен сигнал
- при значении 0 опрос не блокируется, а возвращается сразу после опроса, чтобы проверить, какие из файловых дескрипторов, если таковые имеются, готовы
- если установлено значение больше 0, poll блокируется до тех пор, пока тайм-аут миллисекунд или до тех пор, пока один из файловых дескрипторов не станет готовым, или пока не будет обнаружен сигнал, в зависимости от того, какой из них наступит первым.

Возвращаемые значения poll следующие:

- если произошла ошибка (EBADF или EINTR), то код возврата -1
- если время ожидания вызова истекло до того, как какой-либо дескриптор стал готовым, то код возврата равен 0
- если один или готовы дополнительные файловые дескрипторы, тогда код возврата - положительное целое число. Это число - общее количество файловых дескрипторов в массиве, в которых произошли события. Если количество файловых дескрипторов в массиве равно 10, и у 4 из них произошли события, то возвращаемое значение равно 4. Поле revents может быть проверено, чтобы узнать, какие из событий действительно произошли для дескриптор файла.

Что происходит в ядре?

И select, и poll не имеют состояния. Каждый раз, когда выполняется системный вызов select или poll, ядро ​​проверяет каждый дескриптор во входном массиве, переданном в качестве первого аргумента для возникновения события и вернуть результат процессу. Это означает, что стоимость опроса / select составляет O (N), где N - количество отслеживаемых дескрипторов.

Кроме того, реализация как select, так и poll состоит из двух уровней - определенного верхнего уровня, который декодирует входящие запрос, а также несколько нижних уровней, специфичных для устройства или сокета. Нижние уровни состоят из функций опроса ядра и используются как select, так и poll.

Какие минусы такого подхода?

Процесс выполняет два системных вызова - select / poll, чтобы узнать, какие дескрипторы готовы выполнять ввод-вывод. и еще один системный вызов для выполнения самой операции (read / write).

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

Кроме того, select и poll не очень хорошо масштабируются, так как количество отслеживаемых дескрипторов увеличивается. Как указано выше, затраты на select / poll равны O (N), что означает, что когда N очень большой (представьте, что веб-сервер обрабатывает десятки тысяч в основном сонных клиентов), каждый раз, когда select / poll вызывается, даже если на самом деле произошло небольшое количество событий (4 в приведенном выше примере), ядру все равно необходимо сканировать каждый дескриптор в списке (10 в приведенном выше примере ) и проверьте все три условия для каждого дескриптора и вызовите соответствующие зарегистрированные обратные вызовы. Это также означает, что как только ядро ​​ответит процессу со статусом каждого дескриптора, процесс должен затем просканировать весь список дескрипторов в ответе, чтобы определить, какой дескриптор готов.

Когда может иметь смысл использовать этот подход?

Возможно, когда количество дескрипторов, отслеживаемых только для ввода-вывода, невелико, и эти дескрипторы в основном заняты?

Опрос событий ядра в BSD

Что происходит с дескрипторами?

Дескрипторы переводятся в неблокирующий режим.

Что происходит в процессе?

Для процесса предоставляется общий интерфейс уведомлений, известный как kevent, который позволяет ему отслеживать широкий спектр событий и получать уведомления об этих действиях ядра масштабируемым образом. Например, процесс может отслеживать:

- изменение атрибутов файла при его переименовании или удалении.
- отправка ему сигналов при разветвлении, execs или выходе
- завершение асинхронного ввода / вывода

Ядро возвращает только список произошедших событий, вместо того, чтобы возвращать статус каждого события, зарегистрированного процессом. Ядро создает структуру уведомления о событиях только один раз, и процесс уведомляется каждый раз, когда происходит одно из событий. Если процесс отслеживает N событий, и произошло лишь небольшое количество этих событий (M), стоимость составит O (M), а не O (N). Таким образом, kevent хорошо масштабируется, когда происходит только небольшое подмножество событий из всех событий, которые интересны процессу. Другими словами, это особенно хорошо, когда имеется большое количество «сонных» или «медленных» клиентов, поскольку количество событий, о которых необходимо уведомить процесс, остается небольшим, даже если количество событий, отслеживаемых процессом, велико.

Системный вызов, используемый для создания дескриптора для использования в качестве дескриптора, на котором могут быть зарегистрированы kevents, - это kqueue в BSD / Darwin. Этот дескриптор является средством, с помощью которого процесс затем получает уведомление о возникновении событий, которые он зарегистрировал. Этот дескриптор также является средством, с помощью которого процесс может добавлять или удалять события, о которых он хочет получать уведомления.

Что происходит в ядре?

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

Асинхронный ввод-вывод в POSIX

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

Linux предоставляет такие функции, как io_setup, io_submit, io_getevents, io_destroy, которые позволяют отправлять запросы ввода-вывода из потока без блокировки потока. Это для дискового ввода-вывода (особенно случайного ввода-вывода на SSD). Особенно интересно то, как добавление поддержки eventfd позволяет использовать его с epoll. Вот хорошее описание того, как это работает.

В Linux другой способ выполнения параллельного ввода-вывода - это реализация на основе потоков, доступная в glibc:

Эти функции являются частью библиотеки с функциями реального времени под названием librt. На самом деле они не являются частью двоичного файла libc. Реализация этих функций может быть выполнена с использованием поддержки в ядре (если доступно) или с использованием реализации, основанной на потоках на уровне пользователя. В последнем случае может потребоваться связать приложения с библиотекой потоков libpthread в дополнение к librt.

Однако у такого подхода есть свои минусы:

Это имеет ряд ограничений, в первую очередь то, что поддержка нескольких потоков для выполнения операций ввода-вывода стоит дорого и плохо масштабируется. Некоторое время велась работа над реализацией асинхронного ввода-вывода на основе конечного автомата ядра (см. io_submit (2), io_setup (2 ), io_cancel (2), io_destroy (2), io_getevents (2)), но в этой реализации нет Однако она уже созрела до такой степени, что реализация POSIX AIO может быть полностью переопределена с помощью системных вызовов ядра.

В FreeBSD POSIX AIO реализован с помощью системного вызова aio . Асинхронный процесс ядра (также называемый демоном ввода-вывода ядра или демоном AIO) выполняет операции ввода-вывода в очереди.

Демоны AIO сгруппированы в настраиваемые пулы. Каждый пул будет добавлять или удалять демонов AIO в зависимости от нагрузки. Один пул демонов AIO используется для обслуживания запросов асинхронного ввода-вывода для сокетов. Второй пул демонов AIO используется для обслуживания всех других запросов асинхронного ввода-вывода, за исключением запросов ввода-вывода к необработанным дискам.

Чтобы выполнить операцию асинхронного ввода-вывода:

- ядро ​​создает структуру запроса асинхронного ввода-вывода со всей информацией, необходимой для выполнения операции
- если запрос не может быть немедленно удовлетворен буферами ядра, этот структурированный запрос помещается в очередь
- Если AIO демон недоступен во время создания запроса, структура запроса ставится в очередь для обработки, и системный вызов возвращается.
- следующий доступный демон AIO обрабатывает запрос, используя путь синхронизации ядра
- когда демон завершает ввод-вывод, структура запроса помечается как завершенная вместе с кодом возврата или ошибки.
- процесс использует системный вызов aio_error для опроса, завершен ли ввод-вывод. Этот вызов реализуется путем проверки состояния структуры запроса асинхронного ввода-вывода, созданной ядром
- если процесс доходит до точки, в которой он не может продолжаться, пока ввод-вывод не будет завершен, он может использовать команду aio_suspend Системный вызов для ожидания завершения ввода / вывода.
- процесс переводится в спящий режим в структуре запроса AIO и пробуждается демоном AIO, когда ввод-вывод завершается, или процесс запрашивает отправку сигнала, когда ввод-вывод завершен
- один раз aio_suspend , aio_error или сигнал, указывающий на завершение ввода / вывода, с помощью системного вызова aio_return получает возвращаемое значение операции ввода / вывода.

Заключение

Этот пост пролил свет только на формы для возможного ввода-вывода и не затронул вообще epoll, что, на мой взгляд, является наиболее интересным из всех. Часто более интересным является управление буфером и памятью. Мой следующий пост исследует epoll более подробно.