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

Содержание

Предыстория

Я являюсь обладателем старенького ноутбука 2005 года выпуска, которым особенно горд — Fujitsu Lifebook S2110. Думаю, это самый старый из моих ПК, который ещё можно считать «современным» — ведь в его основе лежит целый 64-битный процессор!

Фото моего ноутбука от декабря 2008. Мне тогда было 12
Фото моего ноутбука от декабря 2008. Мне тогда было 12

Несмотря на то, что сейчас ему уже 20 лет, он по-прежнему с лёгкостью тащит последний релиз Arch, имея на борту всего 2 ГБ RAM и HDD. И после прогрева кэша он становится достаточно шустрым для простого программирования на C. При этом у него очень удобная клавиатура, и мне нравится, как чёткие растровые шрифты смотрятся на его глянцевом экране с разрешением 1024x768.1

Как и во многих буках того времени, у него над основной клавиатурой есть дополнительный медиа-блок клавиш, упрощающий управление некоторыми функциями:

Эти клавиши полностью соответствуют стилю 2005-года :]
Эти клавиши полностью соответствуют стилю 2005-года :]

Клавиша справа с подписью «Application» и «Player» — это аппаратный переключатель между двумя «режимами».

Честно говоря, я лично никогда этими клавишами не пользовался, но мне стало интересно, как их сигналы обрабатываются в Linux.

Механизм работы медиа-клавиш в Linux

Для начала нужно понять, работают ли вообще эти кнопки в Linux. Их нажатие в оконном менеджере i3(1) не вызывают никаких действий ни в одном из режимов. Возможно, события и генерируются, но по умолчанию ни к чему не привязаны. Мне стало интересно, как можно получать информацию о событиях ввода в X11. Выяснилось, что для этой задачи годится xev(1). Когда он запущен, нажатие крайней левой клавиши «А» даёт такой вывод:

KeyPress event, serial 47, synthetic NO, window 0xc00001,
    root 0x18b, subw 0x0, time 5481538, (287,414), root:(803,434),
    state 0x0, keycode 156 (keysym 0x1008ff41, XF86Launch1), same_screen YES,
    XLookupString gives 0 bytes: 
    XmbLookupString gives 0 bytes: 
    XFilterEvent returns: False

KeyRelease event, serial 47, synthetic NO, window 0xc00001,
    root 0x18b, subw 0x0, time 5481642, (287,414), root:(803,434),
    state 0x0, keycode 156 (keysym 0x1008ff41, XF86Launch1), same_screen YES,
    XLookupString gives 0 bytes: 
    XFilterEvent returns: False

В режиме «Application» аналогичные события KeyPress и KeyRelease срабатывают для всех клавиш:

  • A: XF86Launch1

  • B: XF86Launch2

  • Internet: XF86Launch3

  • E-mail: XF86Launch4

Теперь я могу привязать эти события к нужным мне командам в конфигурации i3(1):

bindsym XF86Launch3 exec --no-startup-id firefox
bindsym XF86Launch1 exec i3-sensible-terminal

Изменения внесены, конфиг перезагружен, и теперь нажатие «А» запускает терминал, а клавиша «Internet» открывает Firefox. Класс.

Вот только, если переключиться в режим «Player», никакие события срабатывать для клавиш не будут. Как-то это неправильно. Дальнейшие подсказки о причинах такой неполадки можно увидеть в этих записях журнала ядра:

Mar 25 19:59:02 s2110 kernel: ACPI: \_SB_.FEXT: Unknown GIRB result [40000414]
Mar 25 19:59:04 s2110 kernel: ACPI: \_SB_.FEXT: Unknown GIRB result [40000415]
Mar 25 19:59:06 s2110 kernel: ACPI: \_SB_.FEXT: Unknown GIRB result [40000416]
Mar 25 19:59:07 s2110 kernel: ACPI: \_SB_.FEXT: Unknown GIRB result [40000417]

Подобная строка появляется при каждом нажатии медиа-клавиши в режиме «Player». Похоже, проблема с драйвером.

Поиск нужного драйвера

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

Но сперва опробуем более универсальный метод. Поскольку основная часть событий в ядре происходит без огласки, у нас вряд ли будут строки, от которых можно было бы оттолкнуться, а поиск на основе догадок в столь масштабных кодовых базах обычно сильно затрудняется шумом. Вместо этого можно запросить у ядра информацию об активных драйверах и попробовать по имени определить нужный. Основная часть кода драйверов в Linux находится в модулях ядра, которые загружаются с диска при подключении устройств. Для вывода загруженных модулей используем команду lsmod(8):

$ lsmod
Module                  Size  Used by
8021q                  53248  0
... вырезано ...
i2c_smbus              20480  1 i2c_piix4
fujitsu_laptop         32768  0
sparse_keymap          12288  1 fujitsu_laptop
mac_hid                12288  0
... вырезано ...
mmc_core              290816  5 sdhci_uhs2,sdhci,ssb,cqhci,sdhci_pci
video                  81920  3 fujitsu_laptop,amdgpu,radeon

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

$ cd linux
$ rg 'Unknown GIRB result'
drivers/platform/x86/fujitsu-laptop.c
1036:					 "Unknown GIRB result [%x]\n", irb);

Ага, вот и подтверждение, что это действительно нужный драйвер.

Изучение драйвера fujitsu-laptop

В этом файле fujitsu-laptop.c около тысячи строк кода C, и наряду с медиа-клавишами он обрабатывает очень много других функций. Так что здесь важно придерживаться выбранного пути анализа.3 В ходе дальнейшего повествования я буду пропускать код, который не имеет прямого отношения к теме. Если же вам будет интересно проследить весь процесс в деталях, то код модуля лежит на GitHub.

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

static int __init fujitsu_init(void)
{
                int ret;

                ret = acpi_bus_register_driver(&acpi_fujitsu_bl_driver);
                if (ret)
                               return ret;

                /* Регистрация драйвера платформы. */

                ret = platform_driver_register(&fujitsu_pf_driver);
                if (ret)
                               goto err_unregister_acpi;

                /* Регистрация драйвера ноутбука. */

                ret = acpi_bus_register_driver(&acpi_fujitsu_laptop_driver);
                if (ret)
                               goto err_unregister_platform_driver;

                pr_info("driver " FUJITSU_DRIVER_VERSION " successfully loaded\n");

                return 0;

err_unregister_platform_driver:
                platform_driver_unregister(&fujitsu_pf_driver);
err_unregister_acpi:
                acpi_bus_unregister_driver(&acpi_fujitsu_bl_driver);

                return ret;
}

static void __exit fujitsu_cleanup(void)
{
                acpi_bus_unregister_driver(&acpi_fujitsu_laptop_driver);

                platform_driver_unregister(&fujitsu_pf_driver);

                acpi_bus_unregister_driver(&acpi_fujitsu_bl_driver);

                pr_info("driver unloaded\n");
}

module_init(fujitsu_init);
module_exit(fujitsu_cleanup);

Макрос module_init() в самом конце указывает на функцию fujitsu_init(), которую ядро должно вызвать при загрузке модуля. fujitsu_init() регистрирует различные части драйвера, вызывая соответствующие функции регистрации подсистем и передавая адрес статической структуры, обычно определённой неподалёку. Конкретно здесь она регистрирует:

  • acpi_fujitsu_bl_driver — скорее всего, управляет подсветкой.

  • fujitsu_pf_driver — похоже, является неким «псевдо»-драйвером, связывающим разные части этого драйвера. Тут я не уверен.

  • acpi_fujitsu_laptop_driver — а вот это как раз то, что нас интересует.

Интересно, почему драйвер подсветки регистрируется отдельно… Ну да ладно, перейдём к тому, что для нас действительно важно — acpi_fujitsu_laptop_driver. Вот его определение:

static struct acpi_driver acpi_fujitsu_laptop_driver = {
	.name = ACPI_FUJITSU_LAPTOP_DRIVER_NAME,
	.class = ACPI_FUJITSU_CLASS,
	.ids = fujitsu_laptop_device_ids,
	.ops = {
		.add = acpi_fujitsu_laptop_add,
		.remove = acpi_fujitsu_laptop_remove,
		.notify = acpi_fujitsu_laptop_notify,
		},
};

В этом коде используются замечательные назначенные инициализаторы C99. Нужные поля структур инициализируются через .<field name> = <value>. При этом все те, к которым мы не обращаемся, инициализируются как нулевые.4 Здесь устанавливаются некоторые метаданные, после чего предоставляется таблица идентификаторов устройств, которые этот драйвер поддерживает. В завершение же перечисляются функции, которые подсистема ACPI (Advanced Configuration and Power Interface) будет вызывать при добавлении или удалении устройства, а также при получении им уведомлений.

Предполагается, что медиа-клавиши определяются и настраиваются при добавлении устройства. Значит, нужно начинать с acpi_fujitsu_laptop_add:

static int acpi_fujitsu_laptop_add(struct acpi_device *device)
{
	struct fujitsu_laptop *priv;
	int ret, i = 0;

	priv = devm_kzalloc(&device->dev, sizeof(*priv), GFP_KERNEL);
	if (!priv)
		return -ENOMEM;
	... вырезано ...
	device->driver_data = priv;

Сначала функция выделяет память под структуру fujitsu_laptop, которая будет содержать состояние нашего драйвера, и связывает её с общей структурой device из подсистемы ACPI. В последующих вызовах драйвера подсистема ACPI будет предоставлять эту же структуру, чтобы он мог обращаться к своему состоянию через указатель device->driver_data.

Затем чуть ниже в коде начинается самое интересное:

while (call_fext_func(device, FUNC_BUTTONS, 0x1, 0x0, 0x0) != 0 &&
       i++ < MAX_HOTKEY_RINGBUFFER_SIZE)
	; /* Никаких действий, результат отбрасывается */
acpi_handle_debug(device->handle, "Discarded %i ringbuffer entries\n",
		  i);

Этот цикл очищает кольцевой буфер, содержащий события клавиш, чтобы внутреннее состояние прошивки соответствовало такому, которое ожидает драйвер. Первым делом здесь вызывается call_fext_func(), обёрточная функция для вызовов ACPI, которые взаимодействуют с системной прошивкой.

Я здесь не буду подробно разъяснять про ACPI. Скажу лишь, что в ходе проекта занырнул в эту кроличью нору реально глубоко. Если коротко, то производитель устройства предоставляет системную прошивку, которая содержит независящий от архитектуры байткод, умеющий взаимодействовать с этим устройством. В то же время в ядре есть виртуальная машина, которая этот байткод выполняет. Так вот, call_fext_func() позволяет драйверу вызывать в коде прошивки разные функции для взаимодействия с устройством.

В завершении acpi_fujitsu_laptop_add() мы видим вызовы различных функций, которые завершают настройку:

ret = acpi_fujitsu_laptop_input_setup(device);
if (ret)
	goto err_free_fifo;

ret = acpi_fujitsu_laptop_leds_register(device);
if (ret)
	goto err_free_fifo;

ret = fujitsu_laptop_platform_add(device);
... вырезано ...

Вот интересная функция настройки ввода:

static int acpi_fujitsu_laptop_input_setup(struct acpi_device *device)
{
	struct fujitsu_laptop *priv = acpi_driver_data(device);
	int ret;

	priv->input = devm_input_allocate_device(&device->dev);
	if (!priv->input)
		return -ENOMEM;

	snprintf(priv->phys, sizeof(priv->phys), "%s/input0",
		 acpi_device_hid(device));

	priv->input->name = acpi_device_name(device);
	priv->input->phys = priv->phys;
	priv->input->id.bustype = BUS_HOST;

acpi_driver_data() возвращает нам ранее установленный указатель device->driver_data, то есть priv теперь позволит обращаться к состоянию драйвера в структуре fujitsu_laptop. Затем функция выделяет память под устройство ввода, сохраняя указатель на него в priv->input, и производит начальную настройку этого устройства.

Далее она проверяет fujitsu_laptop_dmi_table, передаёт keymap (раскладку клавиш) другой функции настройки, чтобы та связала её с priv->input, и в завершение регистрирует устройство ввода:

dmi_check_system(fujitsu_laptop_dmi_table);
ret = sparse_keymap_setup(priv->input, keymap, NULL);
if (ret)
	return ret;

return input_register_device(priv->input);

Но что это за keymap, и откуда она берётся? Вот её определение, расположенное чуть выше в том же файле:

static const struct key_entry *keymap = keymap_default;

Таким образом, keymap — это глобальная переменная (отмеченная как static, то есть глобальна она только для этого файла .c), которая инициализируется как keymap_default. Далее функция dmi_check_system() перебирает массив конфигураций устройств:

static const struct dmi_system_id fujitsu_laptop_dmi_table[] = {
	{
		.callback = fujitsu_laptop_dmi_keymap_override,
		.ident = "Fujitsu Siemens S6410",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU SIEMENS"),
			DMI_MATCH(DMI_PRODUCT_NAME, "LIFEBOOK S6410"),
		},
		.driver_data = (void *)keymap_s64x0
	},
	{
		.callback = fujitsu_laptop_dmi_keymap_override,
		.ident = "Fujitsu Siemens S6420",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU SIEMENS"),
			DMI_MATCH(DMI_PRODUCT_NAME, "LIFEBOOK S6420"),
		},
		.driver_data = (void *)keymap_s64x0
	},
	{
		.callback = fujitsu_laptop_dmi_keymap_override,
		.ident = "Fujitsu LifeBook P8010",
		.matches = {
			DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU"),
			DMI_MATCH(DMI_PRODUCT_NAME, "LifeBook P8010"),
		},
		.driver_data = (void *)keymap_p8010
	},
	{}
};

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

Обратный вызов в этом драйвере очень прост:

static int fujitsu_laptop_dmi_keymap_override(const struct dmi_system_id *id)
{
	pr_info("Identified laptop model '%s'\n", id->ident);
	keymap = id->driver_data;
	return 1;
}

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

Иными словами, она ищет подходящую под устройство раскладку и если её не находит, оставляет keymap_default. В журнале ядра на своём ноутбуке я не увидел сообщения Identified laptop model. И это нормально, так как в таблице DMI моего Lifebook S2110 нет.

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

static void acpi_fujitsu_laptop_notify(struct acpi_device *device, u32 event)
{
	struct fujitsu_laptop *priv = acpi_driver_data(device);
	unsigned long flags;
	int scancode, i = 0;
	unsigned int irb;
	... вырезано ...
	while ((irb = call_fext_func(device,
				     FUNC_BUTTONS, 0x1, 0x0, 0x0)) != 0 &&
	       i++ < MAX_HOTKEY_RINGBUFFER_SIZE) {
		scancode = irb & 0x4ff;
		if (sparse_keymap_entry_from_scancode(priv->input, scancode))
			acpi_fujitsu_laptop_press(device, scancode);
		else if (scancode == 0)
			acpi_fujitsu_laptop_release(device);
		else
			acpi_handle_info(device->handle,
					 "Unknown GIRB result [%x]\n", irb);
	}
	... вырезано ...
}

Когда системная прошивка генерирует событие, например, нажатие клавиши, она уведомляет подсистему ACPI, которая следом вызывает эту функцию обработки уведомлений. Здесь мы видим цикл, аналогичный тому, который отвечает за очистку кольцевого буфера. Только этот цикл вместо отбрасывания возвращаемого значения сохраняет его в irb и маскирует часть битов для получения scancode.

Далее выполняется проверка, существует ли этот scancode в текущей раскладке. Если да, прошивка отправляет событие нажатия клавиши в подсистему ввода через acpi_fujitsu_laptop_press(). То же самое происходит при отпускании клавиши, о чём прошивка сообщает скан-кодом 0.

И тут ещё много всего интересного, но я уже примерно понял, как работает эта часть драйвера. Если коротко:

  1. Драйвер инициализируется в различных подсистемах.

  2. Создаётся устройство ввода.

  3. На основе списка известных устройств выбирается подходящая раскладка. Если конкретное устройство в списке не обнаруживается, используется предустановленный вариант keymap_default.

  4. Выбранная раскладка связывается с устройством ввода.

  5. Уведомляющая функция обратного вызова передаёт события нажатия клавиш подсистеме ввода Linux, а при встрече неизвестных кодов клавиш выводит сообщение в журнал ядра.

Модификация драйвера

Итак, чтобы добавить в драйвер поддержку медиа-клавиш на моём ноутбуке, нужно определить новую раскладку. Делается это так:

static const struct key_entry keymap_default[] = {
                { KE_KEY, KEY1_CODE,            { KEY_PROG1 } },
                { KE_KEY, KEY2_CODE,            { KEY_PROG2 } },
                { KE_KEY, KEY3_CODE,            { KEY_PROG3 } },
                { KE_KEY, KEY4_CODE,            { KEY_PROG4 } },
                { KE_KEY, KEY5_CODE,            { KEY_RFKILL } },
                /* Программные клавиши, считываемые из флагов состояния. */
                { KE_KEY, FLAG_RFKILL,          { KEY_RFKILL } },
                { KE_KEY, FLAG_TOUCHPAD_TOGGLE, { KEY_TOUCHPAD_TOGGLE } },
                { KE_KEY, FLAG_MICMUTE,         { KEY_MICMUTE } },
                { KE_END, 0 }
};

static const struct key_entry keymap_s64x0[] = {
                { KE_KEY, KEY1_CODE, { KEY_SCREENLOCK } },     /* "Блокировка" */
                { KE_KEY, KEY2_CODE, { KEY_HELP } },                    /* "Центр мобильности" */
                { KE_KEY, KEY3_CODE, { KEY_PROG3 } },
                { KE_KEY, KEY4_CODE, { KEY_PROG4 } },
                { KE_END, 0 }
};

Хорошо, предустановленная раскладка выглядит адекватно. Похоже, что на других моделях буков есть клавиши для включения радиомодулей, тачпада и микрофона. У моего такие штуки вряд ли найдутся, так что для своей раскладки я возьму за основу эту таблицу s64x0. Первое значение каждой записи в таких таблицах указывает тип этой записи, например, KE_KEY для клавиши и KE_END для конца массива. За ним следует сырой скан-код, получаемый от прошивки. А последнее значение — это соответствующий код клавиши, который передаётся в подсистему ввода Linux.

Эти макросы KEY*_CODE для скан-кодов прошивки определены в верхней части файла:

/* Скан-коды, считываемые из регистра GIRB. */
#define KEY1_CODE                                       0x410
#define KEY2_CODE                                       0x411
#define KEY3_CODE                                       0x412
#define KEY4_CODE                                       0x413
#define KEY5_CODE                                       0x420

Макросы для кодов клавиш Linux, такие как KEY_PROG1 и KEY_RFKILL, определены в файле include/uapi/linux/input-event-codes.h.

Судя по значениям в сообщениях Unknown GIRB result из журнала ядра, если медиа-панель находится в режиме «Player», её клавишам соответствуют коды с 0x414 по 0x417. Похоже на правду. То есть коды просто смещаются на 4. Дополню набор определений четырьмя новыми клавишами:

#define KEY4_CODE          0x413
-#define KEY5_CODE          0x420
+#define KEY5_CODE          0x414
+#define KEY6_CODE          0x415
+#define KEY7_CODE          0x416
+#define KEY8_CODE          0x417
+#define KEY9_CODE          0x420

Теперь существующее значение KEY5_CODE стало KEY9_CODE, и я обновил keymap_default с учётом этого:

@@ -560,7 +564,7 @@ static const struct key_entry keymap_default[] = {
    { KE_KEY, KEY2_CODE,            { KEY_PROG2 } },
    { KE_KEY, KEY3_CODE,            { KEY_PROG3 } },
    { KE_KEY, KEY4_CODE,            { KEY_PROG4 } },
-   { KE_KEY, KEY5_CODE,            { KEY_RFKILL } },
+   { KE_KEY, KEY9_CODE,            { KEY_RFKILL } },
    /* Программные клавиши, считываемые из флагов состояния. */

После обновления всех определений я составил новую раскладку:

static const struct key_entry keymap_s2110[] = {
                { KE_KEY, KEY1_CODE, { KEY_PROG1 } }, /* "A" */
                { KE_KEY, KEY2_CODE, { KEY_PROG2 } }, /* "B" */
                { KE_KEY, KEY3_CODE, { KEY_WWW } },   /* "Internet" */
                { KE_KEY, KEY4_CODE, { KEY_EMAIL } }, /* "E-mail" */
                { KE_KEY, KEY5_CODE, { KEY_STOPCD } },
                { KE_KEY, KEY6_CODE, { KEY_PLAYPAUSE } },
                { KE_KEY, KEY7_CODE, { KEY_PREVIOUSSONG } },
                { KE_KEY, KEY8_CODE, { KEY_NEXTSONG } },
                { KE_END, 0 }
};

Мы уже видели, что в режиме «Application» события клавиш отражаются как XF86Launch1-XF86Launch4 (соответствуя KEY_PROG1-KEY_PROG4 в keymap_default), но поскольку за последними двумя медиа-клавишами на моём ноутбуке закреплены особые функции, можно выразить это с помощью более специфичных макросов KEY_WWW and KEY_EMAIL.

Сопоставить эти коды клавиш с кодами, которые выводит xev(1), было сложно — где-то в пространстве пользователя (в libinput?) происходит их преобразование. Поэтому я решил, что быстрее будет составить конечную таблицу просто путём экспериментов с разными значениями.

Наконец, мне нужно добавить в таблицу DMI новую запись, чтобы для моей модели ноутбука выбиралась именно эта раскладка:

@@ -621,6 +637,15 @@ static const struct dmi_system_id fujitsu_laptop_dmi_table[] = {
        },
        .driver_data = (void *)keymap_p8010
    },
+   {
+       .callback = fujitsu_laptop_dmi_keymap_override,
+       .ident = "Fujitsu LifeBook S2110",
+       .matches = {
+           DMI_MATCH(DMI_SYS_VENDOR, "FUJITSU SIEMENS"),
+           DMI_MATCH(DMI_PRODUCT_NAME, "LIFEBOOK S2110"),
+       },
+       .driver_data = (void *)keymap_s2110
+   },
    {}
 };

Тестирование изменений

Для проверки всех этих изменений я использовал систему сборки Arch. Мне предстояло выполнить makepkg -e, чтобы собрать новый модуль ядра с внесёнными правками, который затем я мог установить вместе с ядром Arch из основной ветки. Я не стал сильно увлекаться доработкой этой конфигурации и просто вручную пропатчил каталог src, который создаёт и использует makepkg. Уверен, можно как-то нацелить PKGBUILD на существующий локальный репозиторий Git, чтобы процесс получился более гладким.

Установив и запустив пропатченное ядро, во время загрузки я увидел сообщение от функции обратного вызова об изменении раскладки:

[   68.921998] fujitsu_laptop: ACPI: Fujitsu FUJ02E3 [FEXT]
[   68.923714] ACPI: \_SB_.FEXT: BTNI: [0xff0101]
[   68.923741] fujitsu_laptop: Identified laptop model 'Fujitsu LifeBook S2110'

И клавиши заработали как надо! Зацените:

> xev -event keyboard | grep keysym
    state 0x0, keycode 156 (keysym 0x1008ff41, XF86Launch1), same_screen YES,
    state 0x0, keycode 157 (keysym 0x1008ff42, XF86Launch2), same_screen YES,
    state 0x0, keycode 158 (keysym 0x1008ff2e, XF86WWW), same_screen YES,
    state 0x0, keycode 223 (keysym 0x1008ff19, XF86Mail), same_screen YES,
	(Switch to Player mode)
    state 0x0, keycode 174 (keysym 0x1008ff15, XF86AudioStop), same_screen YES,
    state 0x0, keycode 172 (keysym 0x1008ff14, XF86AudioPlay), same_screen YES,
    state 0x0, keycode 173 (keysym 0x1008ff16, XF86AudioPrev), same_screen YES,
    state 0x0, keycode 171 (keysym 0x1008ff17, XF86AudioNext), same_screen YES,

Наладив обработку событий клавиш, я смог скорректировать под них свою конфигурацию i3(1):

bindsym XF86AudioStop exec --no-startup-id playerctl stop
bindsym XF86AudioPlay exec --no-startup-id playerctl play-pause
bindsym XF86AudioPrev exec --no-startup-id playerctl previous
bindsym XF86AudioNext exec --no-startup-id playerctl next
bindsym XF86WWW exec --no-startup-id firefox
bindsym XF86Launch1 exec i3-sensible-terminal

С утилитой playerctl(1) я познакомился, пока работал над этим проектом. В ней для управления медиаплеерами по D-Bus используется MPRIS.

Я даже нашёл музыкальный CD-диск5, чтобы протестировать его через VLC и насладиться аутентичным опытом прослушивания музыки, как в старом-добром 2005. 8]

Отправка в апстрим

Ну что ж, я исправил работу медиа-клавиш на своём ноутбуке. Теперь пора подумать об отправке своих доработок мейнтейнерам ядра, чтобы они включили их в основную ветку во благо всех тех, кто использует последнюю версию Linux на своих S2110.6

Начну с локального коммита своих изменений:

$ git checkout -b s2110-mediakeys
$ git add drivers/platform/x86/fujitsu-laptop.c
$ git commit
$ git show --stat HEAD
commit d0e1e0b3e2e6674cce73909d4092874c6c8c2d11 (HEAD -> s2110-mediakeys)
Author: Valtteri Koskivuori <vkoskiv@gmail.com>
Date:   Fri May 9 17:54:50 2025 +0300

    platform/x86: fujitsu-laptop: Support Lifebook S2110 hotkeys
    
    The S2110 has an additional set of media playback control keys enabled
    by a hardware toggle button that switches the keys between "Application"
    and "Player" modes. Toggling "Player" mode just shifts the scancode of
    each hotkey up by 4.
    
    Add defines for new scancodes, and a keymap and dmi id for the S2110.
    
    Tested on a Fujitsu Lifebook S2110.
    
    Signed-off-by: Valtteri Koskivuori <vkoskiv@gmail.com>

 drivers/platform/x86/fujitsu-laptop.c | 33 +++++++++++++++++++++++++++++----
 1 file changed, 29 insertions(+), 4 deletions(-)

Теперь проверю этот коммит с помощью скрипта checkpatch.pl. Он должен указать на любые очевидные проблемы в коде или сообщении коммита:

$ scripts/checkpatch.pl -g HEAD
total: 0 errors, 0 warnings, 68 lines checked

Commit d0e1e0b3e2e6 ("platform/x86: fujitsu-laptop: Support Lifebook S2110 hotkeys") has no obvious style problems and is ready for submission.

Неплохо. Далее выведу список получателей моего письма с помощью scripts/get_maintainer.pl:

$ scripts/get_maintainer.pl -f drivers/platform/x86/fujitsu-laptop.c 
Jonathan Woithe <*******@just42.net> (maintainer:FUJITSU LAPTOP EXTRAS)
Hans de Goede <********@redhat.com> (maintainer:X86 PLATFORM DRIVERS)
"Ilpo Järvinen" <****.********@linux.intel.com> (maintainer:X86 PLATFORM DRIVERS)
platform-driver-x86@vger.kernel.org (open list:FUJITSU LAPTOP EXTRAS)
linux-kernel@vger.kernel.org (open list)

Я уже интегрировал свою почту с Git по вот этому руководству и проверил — всё работает. Так что теперь я могу отправить свой патч простой командой git-send-email(1).

Но для пущей уверенности сначала отправлю патч себе самому:

$ git send-email --to="vkoskiv@gmail.com" HEAD^

Сработало — можно вводить следующее:7

$ git send-email --to *******@just42.net --to ********@redhat.com --to ****.********@linux.intel.com --cc platform-driver-x86@vger.kernel.org --cc linux-kernel@vger.kernel.org HEAD^

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

От мейнтейнеров не поступило запроса на какие-либо изменения этого мелкого патча, так что где-то через месяц после отправки письма я выполнил на своём S2110 команду pacman -Syu и убедился, что мои изменения были внесены в основную ветку Arch Linux. Это был очень приятный момент! :]

Таймлайн проекта

  • 2025-05-09: отправил патч: lore.kernel.org

  • 2025-05-11: мейнтейнер драйвера fujitsu-laptop мой патч принялlore.kernel.org

  • 2025-05-15: мейнтейнер platform-drivers-x86 применил патч в своей ветке для ревью: lore.kernel.org

  • 2025-05-23: мейнтейнер platform-drivers-x86 отправил Линусу пул-реквест на включение в v6.15 – мой патч на борту! lore.kernel.org

  • 2025-05-23: в тот же день Линус включил этот пул-реквест в основную ветку: lore.kernel.org

  • 2025-05-25: Линус объявил о выходе Linux 6.15, в журнале изменений которого указано моё имя! lore.kernel.org

  • 2025-05-27: Саша Левин выбрал мой патч (и несколько других) для бэкпортирования во все поддерживаемые ядра LTS: 6.14, 6.12, 6.6, 6.1, 5.15, 5.10 и 5.4.

  • 2025-06-14: я обновил Arch на своём S2110 — медиа-клавиши заработали в основной версии ядра! :]

Заключение

Было реально круто наблюдать, как мой патч продвигался вверх по цепочке и достиг основной ветки! Ещё мне понравился этот первый опыт знакомства с процессом патчинга через электронную почту. Руководство для новичков хорошо помогло мне правильно подготовить свой патч к отправке. Да и весь процесс оказался намного проще, чем я себе представлял.

Тем не менее это лишь крохотное изменение в маленьком драйвере, так что особой обратной связи я не получил. Но у меня уже наготове ещё комплект патчей, которые я реализовал несколькими месяцами ранее. Они исправляют проблему в работе сетевой карты моей модели ноутбука. Но этот комплект требует небольшой корректировки основного кода ядра в kernel/dma/, так что прежде, чем обращаться к мейнтенерам я хочу на 100% убедиться, что мои изменения реально оправданы.

И в этом смысле я поистине оценил эффективность механизма на основе списка рассылки. Именно он помог мне найти реальный прецедент, который бы оправдал предлагаемые мной решения. Журналы Git и списки рассылки за несколько десятилетий скопили огромный объём ценной информации о внесении изменений в ядро. Так что на все свои вопросы в ходе этого процесса я смог ответить либо через поиск по архивам, либо через просмотр журналов Git.

Спасибо вам за внимание! Буду рад любой обратной связи. Если у вас есть вопросы, корректировки, предложения, да и просто идеи — пишите! :]

Кому интересно, эта статья обсуждалась на Hacker News и lobste.rs.

Сноски

  1. Честно говоря, я разбил дисплей этого ноутбука в 2020 году и прошёл тернистый путь по заказу нового из Китая. 

  2. Я клонировал всю историю и подготовил clangd командой make allmodconfig && bear -- make -j$(nproc), чтобы иметь в распоряжении прекрасные возможности LSP для мгновенного перехода к символам и от них, а также для поиска ссылок на символы. 

  3. Когда я изучаю ядро, меня нередко утягивает в одну кроличью нору за другой. В нём всё так интересно, и функция LSP goto definition (перейти к определению) в моём редакторе сильно мешает отучить себя от этой привычки. Думаю, что составление списков тем для последующего изучения должно хоть как-то помочь. 

  4. Назначенные инициализаторы C99 намного приятнее тех, которые завезли в C++20, но есть и подвох: заполняющие байты между элементами структуры не инициализируются как нулевые, что может создавать проблемы

  5. Альбом Hot Fuss команды The Killers.

  6. Сильно удивлюсь, если узнаю, что кто-то ещё использует последний дистрибутив Linux на такой же системе. :] Если это вы, дайте мне знать! 

  7. Есть способы сгладить этот процесс отправки, но я хотел сделать в��ё вручную, так как это мой первый опыт. 

Комментарии (1)


  1. checkpoint
    12.10.2025 11:34

    Хорошая статья, хороший опыт у товарища получился. Я тоже отправлял патч в ядро Linux, в ветку sunxi (Allwinner), но его не приняли.