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

Пишем операционную систему Триалогия - Общение между приложением и оконным менеджером

В конце прошлой части я обещал посмотреть, что ещё стоит поверх kcall (Kernel Calls): как программы "разговаривают" друг с другом и с оконным менеджером. Ровно этим мы сейчас и займёмся, на самом наглядном примере, какой у меня есть, на приложении, которое открывает окно. К этому надо привыкнуть. Во многих системах вся возня с окнами сидит глубоко внутри. У меня нет. Ядро вообще ничего не знает про окна. Оно знает экран и поток, который записывает туда пиксели, и больше ничего. Всё, что выглядит как рабочий стол, окна, заголовки, Z-порядок, фокус, делает оконный менеджер (WindowManager - WM), а он сам, это всего лишь программа в ring 3, ровно как твоё приложение. Это значит: когда приложению нужно окно, оно "разговаривает" не с ядром (конечно совсем без ядра не обходится). Оно "разговаривает" с другой программой пользовательского пространства, которая по случаю заведует рабочим столом. Два равноправных процесса, которым надо договориться. А техника для этого тебе уже знакома по прошло
Оглавление

В конце прошлой части я обещал посмотреть, что ещё стоит поверх kcall (Kernel Calls): как программы "разговаривают" друг с другом и с оконным менеджером. Ровно этим мы сейчас и займёмся, на самом наглядном примере, какой у меня есть, на приложении, которое открывает окно.

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

К этому надо привыкнуть. Во многих системах вся возня с окнами сидит глубоко внутри. У меня нет. Ядро вообще ничего не знает про окна. Оно знает экран и поток, который записывает туда пиксели, и больше ничего. Всё, что выглядит как рабочий стол, окна, заголовки, Z-порядок, фокус, делает оконный менеджер (WindowManager - WM), а он сам, это всего лишь программа в ring 3, ровно как твоё приложение.

Это значит: когда приложению нужно окно, оно "разговаривает" не с ядром (конечно совсем без ядра не обходится). Оно "разговаривает" с другой программой пользовательского пространства, которая по случаю заведует рабочим столом. Два равноправных процесса, которым надо договориться. А техника для этого тебе уже знакома по прошлой части: общая память (SHM) плюс события (Events). Несмотря на то что оба процесса выполняются в ring 3, оконный менеджер (WM) имеет по отношению к ядру немножко больше возможности общения (он знает пароль :-) ). Он является подписанным сервисом (signed service) и ему ядро доверяет больше.

Как возникает соединение

При создании первого окна маленькая клиентская библиотека libwm один раз вызывает display_client_connect(). И это ровно один из opcode'ов kcall из прошлой части. Ядро лишь осуществляет "знакомство" пользовательского приложения и оконного сервиса: создаёт область общей памяти, говорит WM "вот новый клиент" и отдаёт приложению идентификаторы (ID).

int libwm_init(void) {

display_client_connect_result_t res;

display_client_connect(&res); // opcode kcall из прошлой части

void* base = shm_map(res.shm_id, 0); // один SHM, два кольца внутри

g_libwm.req_ring = base; // Ring A: приложение -> WM (запросы)

g_libwm.resp_ring = base + ring_a_size; // Ring B: WM -> приложение (ответы + события)

g_libwm.req_event = res.req_event_id; // будит WM

g_libwm.resp_event = res.resp_event_id; // будит приложение

return 0;

}

После этого приложение и WM висят на одних и тех же двух кольцах. Через Ring A приложение шлёт свои запросы, через Ring B WM отвечает и позже досылает события ввода если необходимо. приложение кладёт что-то в кольцо, сигналит (Event) с помошью соответствующего соответствующего события, чтобы другая сторона проснулась и обработало данные или результат.

Окно появляется на свет

Теперь приложение может запросить своё окно. Оно кладёт сообщение типа WM_MSG_CREATE_WINDOW в Ring A (позиция, размер, заголовок) и будит WM. Тот ищет свободный слот клиента, создаёт окно, резервирует под него буфер пикселей и отвечает:

typedef struct {

uint32_t window_id; // 0 = ошибка

int surface_shm_id; // этот буфер приложение отображает себе

uint32_t surface_width, surface_height, surface_pitch;

uint8_t surface_bpp;

} wm_create_resp_t;

Главное сидит в surface_shm_id. Это поверхность окна, вторая область общей памяти, на этот раз только под пиксели этого одного окна. Приложение отображает её себе и получает прямой доступ к памяти изображения своего окна, ARGB32, пиксель за пикселем.

Весь путь на картинке

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

Рисование

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

libwm_window_t* win = libwm_create_window(100, 100, 400, 300, "Demo", 0);

uint32_t w = libwm_get_width(win), h = libwm_get_height(win);

for (uint32_t y = 0; y < h; y++)

for (uint32_t x = 0; x < w; x++)

libwm_draw_pixel(win, x, y, make_argb(0xFF, r, g, 0x40)); // прямо в поверхность

libwm_invalidate_rect(win, 0, 0, w, h); // "пожалуйста, вынеси этот прямоугольник на экран заново"

То есть приложение пишет прямо в общий буфер пикселей, а потом лишь сообщает WM "в этом прямоугольнике кое-что изменилось". WM не нужно принимать пиксели, они у него уже есть, они лежат в той же памяти (Zero-Copy).

Кому не хочется делать всё пиксель за пикселем, может слать и готовые команды рисования через Ring A: залитый прямоугольник (FILL_RECT), блок пикселей (BLIT_PIXELS), символ шрифтом WM (DRAW_GLYPH) или сдвинуть блок при прокрутке (SCROLL_REGION). Терминал, например, использует SCROLL_REGION, чтобы при прокрутке вверх не передавать каждую строку заново по отдельности.

И обратно в приложение: события (Events)

Обратное направление идёт по Ring B. Как только ты нажимаешь клавишу или двигаешь мышь над окном, WM досылает событие (Event) приложению. Приложение забирает его в своём цикле:

libwm_event_t ev;

while (running) {

if (libwm_get_event(win, &ev, (uint32_t)-1)) { //блокирующее ожидание на Ring B

if (ev.type == LIBWM_EVENT_CLOSE) break; // нажат крестик окна

if (ev.type == LIBWM_EVENT_KEY_DOWN) handle_key(ev.u.key.ch);

if (ev.type == LIBWM_EVENT_MOUSE_MOVE) move_cursor(ev.u.mouse.x, ev.u.mouse.y);

}

}

libwm_destroy_window(win);

События, которые так приходят, это клавиатура, мышь, просьба о закрытии, "кадр готов" и изменение размера. Для мыши WM отдаёт сразу две позиции : один раз относительно окна (0,0, это верхний левый угол твоего окна) и один раз абсолютно на экране. Так приложению не нужно ничего пересчитывать самому, а вещи вроде перетаскивания (Drag and Drop) или всплывающих окон всё равно работают.

Кто убирает за собой

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

От окна на экран

Остаётся вопрос, как из множества отдельных поверхностей окон получается одно изображение. Это работа WM как композитора (compositor): он берёт все видимые поверхности, складывает их в правильном порядке друг на друга, дорисовывает заголовки и рамки и пишет итоговое изображение в ещё одну общую память fb_shm. Оттуда его забирает поток блита в ядре и записывает/копирует на видеокарту только изменившиеся области.

Ядро делает тупое, быстрое проталкивание пикселей через драйвер видеокарты (softrendering или DMA), вся логика рисования живёт в пространстве пользователя в сервисе под названием WindowManager- WM.

Какое то специальное имя для WM я еще пока не придумал, делайте предложения в комментариях.

Правда жизни как обычно

Это ровно тот же приём, что и у кольцевого буфера kcall, уровнем выше: общая память под данные, маленький кольцевой буфер под управление, события (Events) для пробуждения. Только тут друг с другом "говорят" две равноправные (ну почти равноправные) программы, а не программа с ядром. И как и мост из прошлой части, этот канал тоже хрупок, если WM теряет соединение к клиенту (приложению), это быстро становится неприятным, замёрзшие окна, мёртвый ввод. Пару таких историй я уже прошёл. Но в целом оно работает и ты кликаешь по рабочему столу который использует ровно эту "механику".

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

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