Как в Linux работает функционал рабочего стола

Опубликовано: 14.04.2025

Среди моих знакомых немало людей пользуется Linux’ом как основной операционной системой. Они работают в разных окружения рабочего стола, а кто-то предпочитает минималистичные оконные менеджеры. Но мне стало интересно — многие ли из них пытаются разобраться в том, как на самом деле работает их операционная система. И, как оказалось, не все понимают, что происходит «под капотом».

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

Некоторые из тех, кого я спрашивал, попросили рассказать подробнее о некоторых аспектах Linux’а на десктопе. Так и появилась идея написать эту статью. Я, определённо, не являюсь экспертом — мне просто нравится докапываться до сути.

Сегодня мы разберёмся, как Linux реализует некоторые из функций рабочего стола: уведомления, системный лоток и другие.

Проблема

Представим, мы хотим отправить пользователю уведомление. Как это можно сделать?

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

И так можно сказать про многие функции. Например, есть системный лоток (часто его называют трей). Было бы очень неудобно, если бы их было несколько. Или что, если бы каждому проигрывателю пришлось бы поддерживать множество различных медиа-систем, отправляя каждой информацию в специфичном формате?

Все эти проблемы решил бы некий стандарт — набор протоколов и интерфейсов, которые использовались бы всеми приложениями.

Что такое D-Bus?

D-Bus (desktop bus) — это механизм межпроцессного взаимодействия в Linux и других UNIX-подобных системах, шина событий. По сути, он позволяет нескольким процессам — например, приложениям, — общаться между собой.

D-Bus имеет слоистую архитектуру. В самом низу — протокол шины D-Bus, который описан в спецификации . Затем идёт реализация этого протокола — библиотека libdbus. Она предоставляет интерфейс взаимодействия для языка программирования C. Наконец, демон D-Bus, который реализует взаимодействие между различными компонентами системы. Таких демонов может быть несколько; как правило, есть системный демон D-Bus, который взаимодействует с ядром и системными сервисами, а также сессионный демон D-Bus, предназначенный для связи между пользовательскими процессами и рабочим столом.

Как D-Bus работает?

Сообщения

Сообщение — это дискретная единица передачи данных в D-Bus. Каждое сообщение имеет отправителя, получателя, имя метода или сигнала, а также содержит полезную нагрузку в виде данных.

Существует 4 типа сообщений:

  1. Сигнал — сообщение, которое транслируется процессом и может быть получено другими процессами.

  2. Вызов метода — запрос некоторого процесса, передаваемый другому процессу, на выполнение той или иной операции. Процесс, который вызывает метод, часто называют клиентом, а процесс, выполняющий метод — сервером.

  3. Возврат метода — ответ, отправляемый сервером на вызванный метод.

  4. Ошибка — то же, что и возврат метода, но сигнализирует о том, что операция не удалась и завершилась с ошибкой.

Сообщения передаются по шине событий, которую реализует демон D-Bus.

Интерфейсы

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

Интерфейсы описываются с помощью специальных файлов в формате XML.

Сервисы

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

Как всё это связано

Покажем связь на всё том же примере с уведомлениями. Допустим, есть некоторый клиент — приложение, которому нужно отправить некоторую информацию пользователю. Клиент может воспользоваться соответствующим интерфейсом — org.freedesktop.Notifications — в частности, вызвать метод Notify:

...
<method name="Notify">
  <arg type="u" direction="out"/>
  <arg name="app_name" type="s" direction="in"/>
  <arg name="replaces_id" type="u" direction="in"/>
  <arg name="app_icon" type="s" direction="in"/>
  <arg name="summary" type="s" direction="in"/>
  <arg name="body" type="s" direction="in"/>
  <arg name="actions" type="as" direction="in"/>
  <arg name="hints" type="a{sv}" direction="in"/>
  <arg name="timeout" type="i" direction="in"/>
</method>
...

После того, как сообщение было отправлено, D-Bus передаст его демону уведомлений — сервису, который реализует этот интерфейс. Он получит сообщение, обработает аргументы из тела вызова и отобразит уведомление.

Схема работы D-Bus

Схема работы D-Bus

Всё, что потребовалось приложению — это вызвать метод через D-Bus.

Разумеется, клиентов может быть сколько угодно. Каждый из них мог бы вызвать нужный метод, который был бы обработан сервисом. Все уведомления стандартизированы и работают одинаково — ведь демон уведомлений один.

Очень простой пример

Давайте напишем минимальный сервис D-Bus на языке Go. Не пугайтесь, если не знакомы с этим языком — я поясню написанный код.

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

<node>
    <interface name="ru.shelepugin.DBusExample">
        <method name="Add">
            <arg direction="in" type="i" name="a"/>
            <arg direction="in" type="i" name="b"/>
            <arg direction="out" type="i" name="result"/>
        </method>
    </interface>
</node>

Для начала создадим сервис:

package main

import (
	"fmt"

	"github.com/godbus/dbus/v5"
)

const ServiceName = "ru.shelepugin.DBusExample"
const ServicePath = "/ru/shelepugin/DBusExample"

type Service struct{}

func (s *Service) Add(a, b int) (int, *dbus.Error) {
	fmt.Printf("Метод %s.Add вызван с аргументами [%d, %d]\n", ServiceName, a, b)
	return a + b, nil
}

Обратите внимание на структуру Service — именно она реализует интерфейс. В частности, у неё есть метод Add, который складывает два числа. Для наглядности, этот метод также выводит информацию о вызове.

Теперь экспортируем этот сервис в D-Bus:

func main() {
    // 1. Получение соединения с сессионной шиной D-Bus.
    conn, err := dbus.SessionBus()
    if err != nil {
    	panic(err)
    }
    
    // 2. Запрос информации об интерфейсе.
    reply, err := conn.RequestName(ServiceName, dbus.NameFlagDoNotQueue)
    if err != nil {
    	panic(err)
    }
    if reply != dbus.RequestNameReplyPrimaryOwner {
    	panic("Этот интерфейс уже занят")
    }
    
    // 3. Экспорт сервиса в D-Bus.
    srv := &Service{}
    conn.Export(srv, ServicePath, ServiceName)
    
    // 4. Ожидание вызовов.
    select {}
}
  1. Вначале, мы получаем соединение с сессионной шиной D-Bus
  2. Затем, запрашиваем данные об интерфейсе ru.shelepugin.DBusExample. Если интерфейс уже занят, программа завершится с ошибкой.
  3. Далее, мы создаём экземпляр сервиса и экспортируем его в D-Bus. Теперь при вызове метода ru.shelepugin.DBusExample.Add наш сервис обработает вызов.
  4. Наконец, select {} предотвращает завершение программы. В противном случае она бы сразу завершилась, не обработав ни одного вызова.

Запишем программу в файл main.go, настроим Go и запустим нашу программу:

go mod init shelepugin.ru/dbus_example
go mod tidy
go run .

Попробуем вызвать метод Add. Для этого воспользуемся dbus-send — консольным клиентом D-Bus. Он поставляется вместе с пакетом dbus, поэтому присутствует на большинстве систем.

dbus-send \
    --session \
    --dest=ru.shelepugin.DBusExample \
    --print-reply \
    --type=method_call \
    /ru/shelepugin/DBusExample \
    ru.shelepugin.DBusExample.Add \
    int32:42 int32:69

Пояснение команды:

Выполнив эту команду, мы получим следующее:

method return time=1744656601.549794 sender=:1.355 -> destination=:1.356 serial=3 reply_serial=2
   int32 111

Первая строка — это информация о сообщении. Мы можем увидеть тип — возврат метода, время отправки, отправителя и получателя и т.д. Вторая строка — это тело ответа. В данном случае метод вернул сумму аргументов: 42 + 69 = 111.

Запущенный сервис при этом выведет

Метод ru.shelepugin.DBusExample.Add вызван с аргументами [42, 69]

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

Заключение

Сегодня мы разобрались в том, как некоторый функционал рабочего стола реализован в Linux и других UNIX-подобных системах. Надеюсь, было это интересно и понятно.

Спасибо, что дочитали до конца!