Добавить в корзинуПозвонить
Найти в Дзене
Триалогия

Пишем операционную систему Триалогия - ULES, Userspace Lazy (deferred) Event System: события для программ

С device manifest я покинул машинное отделение. Ядро теперь знает, какое железо есть и как им управлять. Но это оставляет открытым новый вопрос: как, собственно, пользовательская программа узнаёт, что что-то случилось? Часы в строке состояния должны тикать каждую секунду. Индикатор сети должен знать, когда воткнули кабель и запустить DHCP запрос. Эти события рождаются глубоко в ядре, а получатель, это приложение в ring 3. Между ними зияет пропасть или стена. Её можно закрыть наивно. Приложение просто спрашивает каждую миллисекунду (pooling) - Прошла уже секунда? А сейчас? А сейчас? Это опрос, и он жжёт процессорное время впустую. Или ядро напрямую зовёт функцию в приложении, как только что-то случилось (call up - плохо, очень плохо). Это связывает обоих слишком тесно и тянет чужой код в ядро. Я не хотел ни того, ни другого. Вместо этого появился ULES Userspace Lazy (deferred) Event System. Имя говорит само за себя, и к "lazy" и "deferred" я ещё вернусь. Суть идеи в том, что никто не го
Оглавление
Триалогия - Пишем операционную систему
Триалогия - Пишем операционную систему

ULES - как события ядра становятся сообщениями для программ

С device manifest я покинул машинное отделение. Ядро теперь знает, какое железо есть и как им управлять. Но это оставляет открытым новый вопрос: как, собственно, пользовательская программа узнаёт, что что-то случилось? Часы в строке состояния должны тикать каждую секунду. Индикатор сети должен знать, когда воткнули кабель и запустить DHCP запрос. Эти события рождаются глубоко в ядре, а получатель, это приложение в ring 3. Между ними зияет пропасть или стена.

Её можно закрыть наивно. Приложение просто спрашивает каждую миллисекунду (pooling) - Прошла уже секунда? А сейчас? А сейчас? Это опрос, и он жжёт процессорное время впустую. Или ядро напрямую зовёт функцию в приложении, как только что-то случилось (call up - плохо, очень плохо). Это связывает обоих слишком тесно и тянет чужой код в ядро. Я не хотел ни того, ни другого. Вместо этого появился ULES Userspace Lazy (deferred) Event System. Имя говорит само за себя, и к "lazy" и "deferred" я ещё вернусь.

Три пункта: источник, брокер, получатель

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

Путь события через три станции от ядра к приложению
Путь события через три станции от ядра к приложению

Первая станция, это ядро как источник. Маленький поток таймера ждёт секунду и затем помещает в кольцевой буфер событие, обработчики IRQ делают то же для аппаратных сообщений. Сам процесс предельно прост:

static void ules_timer_loop(void* arg) {

while (ules_timer_running) {

timer_wait_ms(1000);

if (ules_state.initialized)

ules_kernel_push(ULES_EVT_TIMER_1S, 0, 0);

}

}

ules_kernel_push пишет событие атомарно и без блокировок в кольцо и сразу возвращается. Ядро вовсе не знает получателей. Оно не знает, слушает ли кто-то, и ему всё равно. Оно кладёт ( :-) ) и идёт дальше.

Вторая станция, это брокер и он работает не в ядре, а как совершенно обычный сервис в ring 3, /bin/ules_broker. Он опустошает кольцо ядра и раздаёт каждое событие всем программам, что заказали именно это событие. То, что раздача идёт в userspace, сделано намеренно. Кто получает какое событие, это вопрос политики, а не механики, и такой политике не место в ядре. Так ядро остаётся тонким и делает лишь самое необходимое - генерирует события.

Третья станция это приложение как получатель события. Оно подписалось через библиотеку libules и забирает свои события когда ему удобно. Строка состояния, например, перерисовывает часы на каждом секундном тике. Ни один шаг на этом пути не останавливает другой.

Кто что получает - один бит, одна маска, одно &

Когда брокер раздаёт событие, он должен решить, кто его получит. Это решение я сделал максимально дешёвым: каждый тип события, это один бит, а каждый клиент заказывает маску из таких битов.

Маршрутизация брокера по битовой маске
Маршрутизация брокера по битовой маске

Типы, это просто константы с одним установленным битом каждая:

#define ULES_EVT_NIC_UP 0x0001

#define ULES_EVT_NIC_DOWN 0x0002

#define ULES_EVT_TIMER_1S 0x0004

#define ULES_EVT_PCI_RESCAN 0x0008 /* план, ещё не активно */

Приложение, которому интересны тик в секунду и втыкание сетевого кабеля, соединяет биты через ИЛИ в маску:

ules_subscribe(ULES_EVT_TIMER_1S | ULES_EVT_NIC_UP);

Брокер запоминает эту маску для каждого клиента. Когда приходит событие типа TIMER_1S (то есть 0x0004), он обходит каждого клиента и задаёт один вопрос:

for (каждый client c) {

if (!(c->subscribed_mask & type)) continue; // не заказано

ring_enqueue(c->resp_ring, &ev); // доставить

event_signal(c->resp_event); // разбудить

}

Одно логическое "И" (&) и дело решено. У кого бит установлен, тому событие кладётся в кольцо и его будят. У кого не установлен, того пропускают. Никаких списков, никакого поиска, лишь одно число на клиента. А новый тип события, это просто новый бит. Насколько дешева эта схема, настолько же легко в ней и ошибиться, об этом ниже.

Lazy и deferred - шина, что прощает простои

Теперь к имени. "lazy" и "deferred" это не украшение, они описывают ровно те два свойства, что делают систему пригодной для описанного выше.

Принцип lazy и deferred
Принцип lazy и deferred

Deferred значит отложенный. Ядро никогда не вызывает приложение напрямую. Оно помещает событие в кольцо и тут же снова свободно. Брокер раздаёт его, когда придёт его черёд, а приложение опустошает своё кольцо, когда ему удобно. Именно поэтому приложение опрашивает без блокировки.

while (ules_wait_event(&ev, 0)) { // 0 = только взглянуть, не ждать

if (ev.type == ULES_EVT_TIMER_1S)

statusbar_update_time();

}

Никто никого не ждёт. Доставка отвязана от создания.

Lazy значит ленивый, в смысле поздний. У системы, что при загрузке поднимает всё сразу, нет гарантии, в каком порядке части станут готовы. Поэтому брокер просто оставляет свой адрес в файле /conf/ules.evt со своим PID и id для разделяемой памяти. Приложение, что поднимается раньше брокера, не падает жёстко на ules_init. Оно пробует снова в следующем кадре. Оконный менеджер, например, тем временем берёт время напрямую через kernel call и переходит на ULES, как только брокер появился. Без падения, самое большее секунда задержки.

Важно и то, чем ULES не является - новым транспортом. Кольца в разделяемой памяти и пробуждение через event_signal и event_wait пришли из части про IPC, которую я описывал раньше. ULES, это лишь политика поверх существующей системы - кто что заказал и кто раздаёт. Механика лежит слоем ниже.

Правда жизни или честное место

На ULES висит один из самых поучительных моих багов, как раз потому что он был так мал. Однажды часы в строке состояния замёрзли. Они показывали время, но стояли. Без падения, без сообщения об ошибке, просто застой.

Причиной было одно неверное число. У брокера внутри для ULES_EVT_TIMER_1S стояло 0x10, а у библиотеки и ядра, верное значение 0x04. Приложение исправно заказывало 0x04, но при раздаче брокер спрашивал subscribed_mask & 0x10 и это & всегда давало ноль. Событие не доставлялось никому. Часы тикнули ровно раз при старте, потому что оконный менеджер отрисовал их тогда напрямую и после этого никогда. То, что выглядело как мёртвый процесс или зависшее ядро, на деле было одной несовпадающей константой.

Урок я вылил в код - теперь есть единый источник истины для этих значений, userspace/include/ules_events.h. Подвох остаётся в том, что у ядра свой собственный сборочный путь и значения там лежат зеркально. Над заголовком ядра сегодня стоит жирное предупреждение, что оба файла должны оставаться синхронными, ведь компилятор не может поймать этот дрейф за меня.

Ещё два честных места поменьше, но вполне реальны. У брокера фиксированное число клиентских мест. Если подключается больше приложений, следующее тихо проваливается, и приложение об этом не узнаёт. А тип ULES_EVT_PCI_RESCAN хоть и определён как бит, но ещё нигде не вызывается, это заглушка на день, когда только что воткнутое устройство должно будет найти путь до самой поверхности.

Что дальше

ULES это провод, на котором висит мир userspace. Крупнейший потребитель этого провода, это оконный менеджер WindowManager, сервис, что управляет рабочим столом, окнами, строкой состояния и курсором мыши. Он использует ULES для часов, он сам, это программа ring 3 и он то место, где сходятся все кусочки: IPC, разделяемая память, дисплейный прокси и события. На него я и взгляну изнутри в следующей статье.

Было бы интересно увидеть ваши комментарии и улучшить статьи.

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

*Система не стоит на месте, поэтому в дальнейшем тексты могут не совпадать с реальным положением