Введение в Generic Netlink, или Как Общаться с Linux Kernel

Опубликовано 2023-02-10 в: Веб-журнал Ярослава

Данный текст также доступен на других языках: English

Туксу написали

Пишите какой-то код в ядерном пространстве и хотите обращаться к нему из уютного пользовательского пространства? Вы не пишите ядерный код, но хотите обращаться к ядру Линукс? Налейте себе немного кофе или чая, присаживаетесь поудобнее, и читайте далее!

Недавно я начал работать в ядерном пространстве Линукса, а точнее разрабатывать модули ядра. Одно из API которое мне пришлось изучить это Generic Netlink. Как часто бывает с большинством API Линукс, за исключением чётко задокументированного кода, документация устаревшая, желает оставлять лучшего, или вовсе отсутствует.

Соответственно я решил сделать маленький пример для себя чтобы научиться пользоваться Generic Netlink'ом для общения с ядерным кодом, и вместе с ним простое вводящее руководство для своих коллег и любого другого человека, заинтересованный в использовании Generic Netlink, но не очень понимающий откуда начинать.

В данном руководстве будет рассказано о следующем:

  • Регистрация семейств (families) Generic Netlink в ядре.
  • Регистрация операции Generic Netlink и их обработка в ядре.
  • Регистрация мультикастных групп Generic Netlink в ядре.
  • Как отправлять "уведомления" через Generic Netlink из ядра.
  • Подключение к Generic Netlink с пользовательской программы.
  • Разрешение семейств и мультикастных групп Generic Netlink с пользовательской программы.
  • Отправка сообщения в семейство Generic Netlink с пользовательской программы.
  • Подписка к мультикастной группы Generic Netlink с пользовательской программы.
  • Получение сообщений и событий Generic Netlink с пользовательской программы.

Netlink — сокетный домен созданный с целью предоставления интерфейса IPC для ядра Linux, в основном для коммуникации ядро<->пользователь. Netlink был создан изначально с целью замены устаревшего ioctl(), предоставляя более гибкий и удобный способ общения между ядром и пользовательскими программами.

Коммуникации в Netlink'е происходят через обычные BSD-сокеты используя домен AF_NETLINK. Тем не менее, есть удобные готовые библиотеки которые упрощают работу с Netlink с пользовательского пространства например, libnl1.

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

Generic Netlink был создан с целью улучшения Netlink и упрощения работы с ним. Он не является отдельным доменном, а является расширением Netlink. Более того, он является семейством Netlink — NETLINK_GENERIC.

Generic Netlink существует ещё с 2005 года и является уже довольно известным и хорошо установившимся интерфейсом для IPC между ядром и пользовательским пространством. Среди хорошо известных пользователей Generic Netlink подсистемы 802.11, ACPI, Wireguard и другие.

Основные особенности Generic Netlink по сравнению с классическим Netlink'ом — динамическая регистрация семейств, интроспекция и упрощенная API со стороны ядра. Данное руководство сосредоточенное в основном на Generic Netlink, так как это ныне стандартный способ общения с пространством ядра.

Generic Netlink bus diagram

Диаграмма шины Generic Netlink

Немного теории

Как я выше упомянул, Netlink работает через стандартные BSD-сокеты. Сообщения в Netlink всегда начинаются с заголовка Netlink, после которого идёт заголовка протокола, то есть, в случае с Generic Netlink, его заголовок.

Заголовки выглядит следующим образом:

Netlink header

Заголовок Netlink

Generic Netlink header

Заголовок Generic Netlink

Или же, если перевести в код C:

struct nlmsghdr {
	__u32		nlmsg_len;
	__u16		nlmsg_type;
	__u16		nlmsg_flags;
	__u32		nlmsg_seq;
	__u32		nlmsg_pid;
};

struct genlmsghdr {
	__u8	cmd;
	__u8	version;
	__u16	reserved;
};

Значение полей в заголовках:

  • Length — длина всего сообщения, включая заголовок.
  • Type — идентификатор семейства Netlink, в нашем случае Generic Netlink.
  • Flags — «do» или «dump»; чуть ниже будет подробнее.
  • Sequence — номер последовательности; также ниже подробнее.
  • Port ID — ставим в него 0, так как мы отправляем с ядра.

и для Generic Netlink:

  • Command: идентификатор операции для данного семейства Generic Netlink.
  • Version: версия протокола нашего семейства Generic Netlink.
  • Reserved: зарезервированное поле, не используется.

Большинство полей довольно простые и понятные и заголовок обычно не заполняется вручную самим пользователем Netlink. Большинство информации, которая содержится в заголовках, предоставляется пользователю через API, когда тот обращается к разным его функциям. Часть из этой информации это такие вещи как флаги и номер последовательности.

Существуют три вида операций-сообщений, которые чаще всего исполняются через сокет Netlink:

  • Операция «do»
  • Операция «dump»
  • И мультикастные сообщения или асинхронные уведомления.

Существует множество разных способов отправить сообщения через Netlink, но это те, которые чаще всего применяются в Generic Netlink.

Операция «do» это сообщения предназначено для вида операции где пользовательская программа отправляет сообщение и получает ответ в виде подтверждения (acknowledgment) ошибки, или возможно даже сообщение с запрашиваемой информации.

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

Вид операции, то есть, будь это «do» или «dump», задаётся с помощью поля «flags»:

  • NLM_F_REQUEST | NLM_F_ACK — do.
  • NLM_F_REQUEST | NLM_F_ACK | NLM_F_DUMP — dump.

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

Как мы уже видели, также существует поле «sequence number» в заголовке Netlink. Несмотря на это, в отличие от других протоколов коммуникации, Netlink не манипулирует и не проверяет правильность номера последовательности. Он предоставлен для удобства пользователей, чтобы было проще отслеживать запросы и ответы. На практике чаще всего пользовательская программа сама увеличивает с каждым отправленным сообщением этот номер, а модуль ядра отправляет ответ с тем же номером. Мультикастные сообщения обычно отправляются с номером последовательности равным 0.

Груз сообщения

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

Также возможна кодировка своих собственных типов, таких как собственные структуры в одном атрибуте. Тем не менее, рекомендуется использовать один атрибут строго на одно поле данных.

Атрибуты кодируются в формате LTV (длина-тип-значение), и они заполнены так чтобы каждый атрибут был выравнен по 4 байта. Длина полей, как в заголовке как в атрибуте, всегда включает в себя заголовок, но не заполнение для выравнивания.

Netlink attribute diagram

Диаграмма атрибутов Netlink

Порядок атрибутов в сообщении Netlink, следовательно и в сообщении Generic Netlink, не гарантирован. По этой причине следует пройтись по ним и проанализировать.

Netlink предоставляет возможность проверять правильность сообщения используя так называемые "политики валидации атрибутов", представлены в структуре struct nla_policy. Мне лично кажется немного странным и недостатком то, что это структура не экспортирована в пользовательское пространство. Получается что валидация происходит по умолчанию только в пространстве ядра. Проверку также можно осуществить с стороны пользовательского пространства, и libnl предоставляет свою собственную struct nla_policy но оно отличается от ядерного что означает что по сути нужно дублировать код для того, чтобы делать проверку с обеих сторон.

Виды сообщения и операции определяются с так называемыми командами Netlink. Команда коррелирует к одному виду сообщения и также может коррелировать к одной операции.

Каждая команда или вид сообщения могут иметь один и более атрибутов.

Семейства

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

У сокетов есть семейства, для наших нужд мы используем семейство AF_NETLINK. У Netlink'а тоже есть семейства, которых всего 32. Их не планируется расширять и добавлять новые семейства в ядро Linux; семейство, которым мы пользуемся, NETLINK_GENERIC. И наконец, у нас есть семейства Generic Netlink, в отличие от семейств Netlink их можно динамически регистрировать и их может быть зарегистрировано до 1024 в одно и то же время.

Семейства Generic Netlink идентифицируются с помощью строк, например, "nl80211". Так как семейства регистрируются динамическим образом, это значит что их идентификационный номер может отличаться от компьютера к компьютеру и от сеанса к сеансу, поэтому сначала нам нужно разрешить семейство перед тем, как отправлять сообщения.

Generic Netlink сам по себе предоставляет одно статически выделено семейство, которое называется nlctrl, которое предоставляет команду для разрешения всех остальных семейств Generic Netlink. Оно также, с недавних пор, предоставляет команду для интроспекции операций и получения политик с пользовательское пространства, тем не менее в данной статье мы ей не будем пользоваться.

Ещё один момент, который стоит учитывать, это то, что один сокет Generic Netlink не привязан к одному только семейству. Сокет Generic Netlink может общаться с любым семейством, ему лишь нужно предоставить ID семейства в сообщении, размещая его в поле "type".

Мультикастные группы

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

Мультикастные группы Generic Netlink, так же как и семейства, регистрируются динамически с помощью названия, содержащееся в строке, и получают численное ID после успешной регистрации. То есть, их также необходимо разрешать перед тем как подписаться на них. Как только программа на них подпишется, она начнёт получать все сообщения отправленные в группу.

Дабы предотвратить путаницы чисел последовательности с уникастными сообщениями, и упростить обработку полученных ответов, рекомендуется использовать отдельный сокет для мультикастных сообщений.

Пора руки помочить

Это далеко не всё, о чём можно написать про Generic Netlink и особенно классический Netlink. Тем не менее это были самые важные понятия которые нужные для работы с Generic Netlink. Как бы то не было, не столь интересно просто знать о чём-то, сколько применять эти знания на практике.

Я сделал пример применения Generic Netlink, который состоит из двух частей. Модуль ядра и пользовательская программа.

Модуль ядра предоставляет одну операцию Generic Netlink и одну мультикастную группу. Структура сообщения одинаковая как для операции do как для мультикастного сообщения. Первое принимает сообщение с строкой текста, выводит его в журнал ядра, и отправляет обратно своё собственное собщение; второе отправляет уведомление при получении строки текста из sysfs, дублируя его.

Пользовательская программа подключается к Generic Netlink, подписывается к мультикастной группу, отправляет сообщение нашему семейству и выводит на экран полученное сообщение.

Далее я разъясню всё по шагам с помощью листингов кода. Полный исходный код для обеих частей можно найти здесь: https://git.yaroslavps.com/genltest/.

Пространство ядра

Использовать Generic Netlink с ядерного пространства довольно просто. Единственное что нам нужно чтобы начать работу, это подключить хедер net/genetlink.h. Вот все заголовочные, которые нам нужны для нашего модуля:

#include <linux/module.h>
#include <net/genetlink.h>

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

#define GENLTEST_GENL_NAME "genltest"
#define GENLTEST_GENL_VERSION 1
#define GENLTEST_MC_GRP_NAME "mcgrp"

/* Attributes */
enum genltest_attrs {
	GENLTEST_A_UNSPEC,
	GENLTEST_A_MSG,
	__GENLTEST_A_MAX,
};

#define GENLTEST_A_MAX (__GENLTEST_A_MAX - 1)

/* Commands */
enum genltest_cmds {
	GENLTEST_CMD_UNSPEC,
	GENLTEST_CMD_ECHO,
	__GENLTEST_CMD_MAX,
};

#define GENLTEST_CMD_MAX (__GENLTEST_CMD_MAX - 1)

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

Обратно в нашем ядерном коде, мы создаём политику валидации для нашей команды «echo»:

/* Attribute validation policy for our echo command */
static struct nla_policy echo_pol[GENLTEST_A_MAX + 1] = {
	[GENLTEST_A_MSG] = { .type = NLA_NUL_STRING },
};

Делаем массив с нашими операциями:

/* Operations for our Generic Netlink family */
static struct genl_ops genl_ops[] = {
	{
		.cmd	= GENLTEST_CMD_ECHO,
		.policy = echo_pol,
		.doit	= echo_doit,
	 },
};

Массив с нашими мультикастными группами:

/* Multicast groups for our family */
static const struct genl_multicast_group genl_mcgrps[] = {
	{ .name = GENLTEST_MC_GRP_NAME },
};

И наконец структуру, описывающую наше семейство, содержащее всего, что мы сейчас определяли:

/* Generic Netlink family */
static struct genl_family genl_fam = {
	.name	  = GENLTEST_GENL_NAME,
	.version  = GENLTEST_GENL_VERSION,
	.maxattr  = GENLTEST_A_MAX,
	.ops	  = genl_ops,
	.n_ops	  = ARRAY_SIZE(genl_ops),
	.mcgrps	  = genl_mcgrps,
	.n_mcgrps = ARRAY_SIZE(genl_mcgrps),
};

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

ret = genl_register_family(&genl_fam);
if (unlikely(ret)) {
	pr_crit("failed to register generic netlink family\n");
	// etc...
}

И конечно не забыть освободить всё что с ним связано при выходе:

if (unlikely(genl_unregister_family(&genl_fam))) {
	pr_err("failed to unregister generic netlink family\n");
}

Как вы наверняка уже заметили, мы предоставили функцию echo_doit для обратного вызова для нашей команды "echo". Она выглядит следующим образом:

/* Handler for GENLTEST_CMD_ECHO messages received */
static int echo_doit(struct sk_buff *skb, struct genl_info *info)
{
	int		ret = 0;
	void	       *hdr;
	struct sk_buff *msg;

	/* Check if the attribute is present and print it */
	if (info->attrs[GENLTEST_A_MSG]) {
		char *str = nla_data(info->attrs[GENLTEST_A_MSG]);
		pr_info("message received: %s\n", str);
	} else {
		pr_info("empty message received\n");
	}

	/* Allocate a new buffer for the reply */
	msg = nlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
	if (!msg) {
		pr_err("failed to allocate message buffer\n");
		return -ENOMEM;
	}

	/* Put the Generic Netlink header */
	hdr = genlmsg_put(msg, info->snd_portid, info->snd_seq, &genl_fam, 0,
			  GENLTEST_CMD_ECHO);
	if (!hdr) {
		pr_err("failed to create genetlink header\n");
		nlmsg_free(msg);
		return -EMSGSIZE;
	}
	/* And the message */
	if ((ret = nla_put_string(msg, GENLTEST_A_MSG,
				  "Hello from Kernel Space, Netlink!"))) {
		pr_err("failed to create message string\n");
		genlmsg_cancel(msg, hdr);
		nlmsg_free(msg);
		goto out;
	}

	/* Finalize the message and send it */
	genlmsg_end(msg, hdr);

	ret = genlmsg_reply(msg, info);
	pr_info("reply sent\n");

out:
	return ret;
}

Если вкратце, для обработки команды вида «do» нужно:

  1. Извлечь данные входящего сообщения со структуры genl_info.
  2. Выделить память для ответа.
  3. Вставить заголовок Generic Netlink в буфере сообщения; обратите внимания на то, что мы используем тот же ID порта и номер последовательности что и во входящем сообщении, так как это ответное сообщение.
  4. Вставить все атрибуты с полезным грузом.
  5. Отправить ответ.

Теперь посмотрим как отправить мультикастное сообщение. Я использовал sysfs в этом примере дабы сделать его более интересным. Я создал «kobj», который называется genltest, который содержит атрибут ping, с которого мы собственно продублируем то, что в него будет записано. Для краткости, я упущу со статьи код с sysfs, просто предоставлю функцию, которая формирует и отправляет сообщение:

/* Multicast ping message to our genl multicast group */
static int echo_ping(const char *buf, size_t cnt)
{
	int		ret = 0;
	void	       *hdr;
	/* Allocate message buffer */
	struct sk_buff *skb = genlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);

	if (unlikely(!skb)) {
		pr_err("failed to allocate memory for genl message\n");
		return -ENOMEM;
	}

	/* Put the Generic Netlink header */
	hdr = genlmsg_put(skb, 0, 0, &genl_fam, 0, GENLTEST_CMD_ECHO);
	if (unlikely(!hdr)) {
		pr_err("failed to allocate memory for genl header\n");
		nlmsg_free(skb);
		return -ENOMEM;
	}

	/* And the message */
	if ((ret = nla_put_string(skb, GENLTEST_A_MSG, buf))) {
		pr_err("unable to create message string\n");
		genlmsg_cancel(skb, hdr);
		nlmsg_free(skb);
		return ret;
	}

	/* Finalize the message */
	genlmsg_end(skb, hdr);

	/* Send it over multicast to the 0-th mc group in our array. */
	ret = genlmsg_multicast(&genl_fam, skb, 0, 0, GFP_KERNEL);
	if (ret == -ESRCH) {
		pr_warn("multicast message sent, but nobody was listening...\n");
	} else if (ret) {
		pr_err("failed to send multicast genl message\n");
	} else {
		pr_info("multicast message sent\n");
	}

	return ret;
}

Процесс довольно похож на операцию «do», с тем отличием что мы не отвечаем на запрос, а отправляем асинхронное сообщение. В связи с этим, мы задаём 0 в качестве последовательного номера, так как он нам в данном случае не нужен, и также задаём 0 для ID порта, порт/PID ядра. Также мы отправляем сообщение через функцию genlmsg_multicast().

На этом всё со стороны ядра. Теперь взглянем на пользовательскую часть.

Пользовательское пространство

Netlink является сокетным семейством, что делает возможным пользоваться им через стандартное API сокетов. Достаточно открыть сокет и далее писать и читать сообщения через него, примерно так:

int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_GENERIC);

/* Format request message... */
/* ... */

/* Send it */
send(fd, &req, sizeof(req), 0);
/* Receive response */
recv(fd, &resp, BUF_SIZE, 0);

/* Do something with response... */
/* ... */

Тем не менее лучше использовать libnl1 или похожую библиотеку, так как они предоставляют более удобный интерфейс к Generic Netlink, что предотвращает некоторые ошибки, и реализует код, который бы всего равно пришлось написать. Эту же библиотеку я буду использовать для своего примера.

Для начала нам нужно подключить некоторые хедеры с libnl:

#include <netlink/socket.h>
#include <netlink/netlink.h>
#include <netlink/genl/ctrl.h>
#include <netlink/genl/genl.h>
#include <netlink/genl/family.h>

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

#include "../ks/genltest.h"

В примере я также сделал маленький макрос для вывода ошибок:

#define prerr(...) fprintf(stderr, "error: " __VA_ARGS__)

Как я упомянул ранее, проще использовать отдельные сокеты под юникаст и мультикаст сообщения. Поэтому здесь открываем два сокета для общения через Generic Netlink:

/* Allocate netlink socket and connect to generic netlink */
static int conn(struct nl_sock **sk)
{
	*sk = nl_socket_alloc();
	if (!sk) {
		return -ENOMEM;
	}

	return genl_connect(*sk);
}

/*
 * ...
 */

struct nl_sock *ucsk, *mcsk;

/*
 * We use one socket to receive asynchronous "notifications" over
 * multicast group, and another for ops. We do this so that we don't mix
 * up responses from ops with notifications to make handling easier.
 */
if ((ret = conn(&ucsk)) || (ret = conn(&mcsk))) {
	prerr("failed to connect to generic netlink\n");
	goto out;
}

Далее нам нужно разрешить ID семейства Generic Netlink, к которому мы хотим подключиться:

/* Resolve the genl family. One family for both unicast and multicast. */
int fam = genl_ctrl_resolve(ucsk, GENLTEST_GENL_NAME);
if (fam < 0) {
	prerr("failed to resolve generic netlink family: %s\n",
	      strerror(-fam));
	goto out;
}

Сокет (Generic) Netlink никак не ассоциируется с семейством, а ID семейства нам пригодится позже для отправки сообщения.

Библиотека libnl может отслеживать номер последовательности за нас, но нам это не нужно для мультикастных сообщений, поэтому и выключаем его:

nl_socket_disable_seq_check(mcsk);

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

/* Resolve the multicast group. */
int mcgrp = genl_ctrl_resolve_grp(mcsk, GENLTEST_GENL_NAME,
				  GENLTEST_MC_GRP_NAME);
if (mcgrp < 0) {
	prerr("failed to resolve generic netlink multicast group: %s\n",
	      strerror(-mcgrp));
	goto out;
}
/* Join the multicast group. */
if ((ret = nl_socket_add_membership(mcsk, mcgrp) < 0)) {
	prerr("failed to join multicast group: %s\n", strerror(-ret));
	goto out;
}

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

/* Modify the callback for replies to handle all received messages */
static inline int set_cb(struct nl_sock *sk)
{
	return -nl_socket_modify_cb(sk, NL_CB_VALID, NL_CB_CUSTOM,
				    echo_reply_handler, NULL);
}

/*
 * ...
 */

if ((ret = set_cb(ucsk)) || (ret = set_cb(mcsk))) {
	prerr("failed to set callback: %s\n", strerror(-ret));
	goto out;
}

Как вы могли заметить, обратный вызов одинаков для обеих сокетов, так как мы по сути получаем одинаковое по типу сообщение как для операции «do» так для асинхронных сообщений. Функция выглядит следующим образом:

/*
 * Handler for all received messages from our Generic Netlink family, both
 * unicast and multicast.
 */
static int echo_reply_handler(struct nl_msg *msg, void *arg)
{
	int		   err	   = 0;
	struct genlmsghdr *genlhdr = nlmsg_data(nlmsg_hdr(msg));
	struct nlattr	  *tb[GENLTEST_A_MAX + 1];

	/* Parse the attributes */
	err = nla_parse(tb, GENLTEST_A_MAX, genlmsg_attrdata(genlhdr, 0),
			genlmsg_attrlen(genlhdr, 0), NULL);
	if (err) {
		prerr("unable to parse message: %s\n", strerror(-err));
		return NL_SKIP;
	}
	/* Check that there's actually a payload */
	if (!tb[GENLTEST_A_MSG]) {
		prerr("msg attribute missing from message\n");
		return NL_SKIP;
	}

	/* Print it! */
	printf("message received: %s\n", nla_get_string(tb[GENLTEST_A_MSG]));

	return NL_OK;
}

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

Далее, нам надо отправить сообщение в ядерное пространство:

/* Send (unicast) GENLTEST_CMD_ECHO request message */
static int send_echo_msg(struct nl_sock *sk, int fam)
{
	int	       err = 0;
	struct nl_msg *msg = nlmsg_alloc();
	if (!msg) {
		return -ENOMEM;
	}

	/* Put the genl header inside message buffer */
	void *hdr = genlmsg_put(msg, NL_AUTO_PORT, NL_AUTO_SEQ, fam, 0, 0,
				GENLTEST_CMD_ECHO, GENLTEST_GENL_VERSION);
	if (!hdr) {
		return -EMSGSIZE;
	}

	/* Put the string inside the message. */
	err = nla_put_string(msg, GENLTEST_A_MSG,
			     "Hello from User Space, Netlink!");
	if (err < 0) {
		return -err;
	}
	printf("message sent\n");

	/* Send the message. */
	err = nl_send_auto(sk, msg);
	err = err >= 0 ? 0 : err;

	nlmsg_free(msg);

	return err;
}

/*
 * ...
 */

/* Send unicast message and listen for response. */
if ((ret = send_echo_msg(ucsk, fam))) {
	prerr("failed to send message: %s\n", strerror(-ret));
}

Также не сильно отличается от ядерного API. Теперь нам следует единовременное слушать для получения ответа и "бесконечно" для получения уведомлений:

printf("listening for messages\n");
nl_recvmsgs_default(ucsk);

/* Listen for "notifications". */
while (1) {
	nl_recvmsgs_default(mcsk);
}

Не забываем перед выходом программы закрывать и освобождать наши сокеты:

/* Disconnect and release socket */
static void disconn(struct nl_sock *sk)
{
	nl_close(sk);
	nl_socket_free(sk);
}

/*
 * ...
 */

disconn(ucsk);
disconn(mcsk);

На этом всё!

Подводя итоги

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

Netlink, и его расширенная форма, Generic Netlink, предоставляют очень гибкий способ общения с ядром, и они решают многие недостатки и проблемы интерфейсов былых времён. Это не идеальное решение (например, не совсем по-Юниксовому), но это однозначно лучший способ который есть, на данным момент, для того, чтобы передавать в и из ядра более сложные вещи чем простые параметры.

Post scriptum

Когда я начал изучать Generic Netlink, версия 6.0 ядра ещё не была выпущенной (stable). Ввиду того что только недавно в этой же версии была добавлена в документацию замечательная страница-введение в Netlink2, и того что я смотрел в версии документации «latest» (на тот момент v5.9), я её не заметил до того, как я уже начал писать свою статью.

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

Тем не менее, несомненно прочитайте официальную документацию ядра! Та страница покрывает некоторые моменты, о которых я мог не упомянуть.

1

Оригинальный сайт с информацией https://www.infradead.org/~tgr/libnl/, актуальное репо: https://github.com/thom311/libnl

2

Хорошая вводная статья по Netlink, с самих доков ядра: https://kernel.org/doc/html/latest/userspace-api/netlink/intro.html

© 2018—2024 Yaroslav de la Peña Smirnov.