Об очередях событий ядра
2014-11-06 08:43 pmДля асинхронной, основанной на событиях работы с сокетами, файлами, пайпами и так далее в Windows есть I/O completion port, а в FreeBSD — kqueue.
С точки зрения реализации ядра, это довольно разные вещи. Порт завершения — это тонкий интерфейс над функциями KeInitializeQueue/KeInsertQueue/KeRemoveQueue, безразмерная очередь событий, которая, в худшем случае, может занять весь невыгружаемый пул и обвалить ядро в BSoD. В порт завершения приземляются все IRP-пакеты по заданным объектам ядра, в том же порядке и виде, как они обрабатываются драйверами устройств. Если порта завершения нет, тогда на последнем этапе эти IRP-пакеты просто удаляются. Также порт завершения можно пополнять т.н. мини-пакетами из пользовательского режима с помощью PostQueuedCompletionStatus(). Очень удобно для межпоточного взаимодействия в рамках одного процесса, ведь адресное пространство общее и достаточно передать только указатель.
kqueue — это целая подсистема, которая предполагает создание в пространстве ядра объектов-наблюдателей (struct knote). Наблюдатели — это никакая не очередь, они либо просто переходят в сигнальное состояние (типа "буфер сокета не полон, можешь отправлять данные"), либо арифметически агрегируют события. Например, если за сигналами никто не наблюдает, тогда это просто 32-разрядное битовое поле в объекте ядра "процесс" и два подряд доставленных одинаковых сигнала "слипаются"; а если есть наблюдатели, созданные через kevent(2), тогда эти наблюдатели подсчитывают, сколько раз сигнал был доставлен. Очевидно, что это более консервативный дизайн — память, которую занимают наблюдатели, не может разрастаться до бесконечности, какие бы события в ядре не происходили. Это в чём-то неудобно: нельзя дважды отправить в одну очередь пользовательское событие EVFILT_USER с разными указателями на пользовательские объекты (события могут слипнуться, если между их отправкой не вмешается планировщик задач), приходится применять другие средства (POSIX mq или пайпы).
Для решения проблемы исчерпания памяти ядра, в Windows Vista реализация порта завершения была немножко изменена. Появился новый объект ядра (IoCompletionReserve, создаётся через NtAllocateReserveObject), который как-то связан с портом завершения, в частности функция отправки мини-пакетов в порт теперь существует в виде NtSetIoCompletionEx, принимающем хэндл на этот объект.
http://wj32.org/wp/2010/07/18/the-nt-reserve-object/
Дизассемлирование показывает, что старая функция NtSetIoCompletion — теперь просто короткая заглушка, которая сразу передаёт работу новой. Ну и разумеется эта NtSetIoCompletionEx не пытается распределять память напрямую в невыгружаемом пуле.
К сожалению, документации на эту тему нет вообще. И в утёкших исходниках Windows 2000, и в Windows Research Kernel мы видим ещё старую версию за авторством Дейва Катлера от 1993 года, которая черпает память напрямую из невыгружаемого пула. Даже в ReactOS всё слизали с хрюши подчистую.
Кстати, у CreateIoCompletionPort() отвратительная документация в MSDN. Не только у неё, но всё же: когда первый раз читаешь, создаётся впечатление, что параметр NumberOfConcurrentThreads влияет на количество запускаемых в фоновом режиме потоков или что-то в этом роде. На самом деле, никаких потоков не создаётся вообще, а этот параметр напрямую передаётся в KeInitializeQueue(). Честно говоря, не понимаю, кому и для чего может понадобиться что-то отличное от нуля.
С точки зрения реализации ядра, это довольно разные вещи. Порт завершения — это тонкий интерфейс над функциями KeInitializeQueue/KeInsertQueue/KeRemoveQueue, безразмерная очередь событий, которая, в худшем случае, может занять весь невыгружаемый пул и обвалить ядро в BSoD. В порт завершения приземляются все IRP-пакеты по заданным объектам ядра, в том же порядке и виде, как они обрабатываются драйверами устройств. Если порта завершения нет, тогда на последнем этапе эти IRP-пакеты просто удаляются. Также порт завершения можно пополнять т.н. мини-пакетами из пользовательского режима с помощью PostQueuedCompletionStatus(). Очень удобно для межпоточного взаимодействия в рамках одного процесса, ведь адресное пространство общее и достаточно передать только указатель.
kqueue — это целая подсистема, которая предполагает создание в пространстве ядра объектов-наблюдателей (struct knote). Наблюдатели — это никакая не очередь, они либо просто переходят в сигнальное состояние (типа "буфер сокета не полон, можешь отправлять данные"), либо арифметически агрегируют события. Например, если за сигналами никто не наблюдает, тогда это просто 32-разрядное битовое поле в объекте ядра "процесс" и два подряд доставленных одинаковых сигнала "слипаются"; а если есть наблюдатели, созданные через kevent(2), тогда эти наблюдатели подсчитывают, сколько раз сигнал был доставлен. Очевидно, что это более консервативный дизайн — память, которую занимают наблюдатели, не может разрастаться до бесконечности, какие бы события в ядре не происходили. Это в чём-то неудобно: нельзя дважды отправить в одну очередь пользовательское событие EVFILT_USER с разными указателями на пользовательские объекты (события могут слипнуться, если между их отправкой не вмешается планировщик задач), приходится применять другие средства (POSIX mq или пайпы).
Для решения проблемы исчерпания памяти ядра, в Windows Vista реализация порта завершения была немножко изменена. Появился новый объект ядра (IoCompletionReserve, создаётся через NtAllocateReserveObject), который как-то связан с портом завершения, в частности функция отправки мини-пакетов в порт теперь существует в виде NtSetIoCompletionEx, принимающем хэндл на этот объект.
http://wj32.org/wp/2010/07/18/the-nt-reserve-object/
Дизассемлирование показывает, что старая функция NtSetIoCompletion — теперь просто короткая заглушка, которая сразу передаёт работу новой. Ну и разумеется эта NtSetIoCompletionEx не пытается распределять память напрямую в невыгружаемом пуле.
К сожалению, документации на эту тему нет вообще. И в утёкших исходниках Windows 2000, и в Windows Research Kernel мы видим ещё старую версию за авторством Дейва Катлера от 1993 года, которая черпает память напрямую из невыгружаемого пула. Даже в ReactOS всё слизали с хрюши подчистую.
Кстати, у CreateIoCompletionPort() отвратительная документация в MSDN. Не только у неё, но всё же: когда первый раз читаешь, создаётся впечатление, что параметр NumberOfConcurrentThreads влияет на количество запускаемых в фоновом режиме потоков или что-то в этом роде. На самом деле, никаких потоков не создаётся вообще, а этот параметр напрямую передаётся в KeInitializeQueue(). Честно говоря, не понимаю, кому и для чего может понадобиться что-то отличное от нуля.