В процессе поиска решения одной интересной задачи, связанной с zero-copy между ядром Linux и приложением пользователя, я наткнулся на реализацию mmaped Netlink IO от Patrick McHardy.
Если кратко, Netlink это способ межпроцессной коммуникации между ядром Linux и приложениями пользователя, использующий существующее socket API.
Приложение открывает сокет, тип которого AF_NETLINK, указывает получателем ядро, и с помощью функций socket API, передает через открытый сокет данные. Все хорошо, если бы не накладные расходы на копирование данных между user-space и kernel-space. Для больших объемов данных такой способ не очень подходит.
Patrick McHardy в 2012 году анонсировал набор патчей для ядра, позволяющих избежать копирования данных, а вместо этого позволяющих отобразить в пространство пользователя участок памяти, с которым работает и ядро и приложение пользователя.
Этот участок памяти представляет из себя очереди - RX/TX, в которых содержатся фреймы.
RX очередь используется для получения данных из ядра, TX - для передачи в ядро.
Очереди выглядят следующим образом:
[ block 0 ] | |
[ frame 0 ] | |
[ frame 1 ] | |
[ block 1 ] | |
[ frame 2 ] | |
[ frame 3 ] | |
... | |
[ block n ] | |
[ frame 2 * n ] | |
[ frame 2 * n + 1 ] |
Блоки видны только ядру, тогда как в user-space приложении эта область память выглядит как непрерывный набор фреймов.
Заголовок фрейма выглядит как:
|
Значения поля nm_status подробно описаны в документации.
Начиная с версии 3.10, патчи включены в официальные исходники ядра Linux.
В моей системе - Debian 8.7, ядро уже собрано с поддержкой NETLINK_MMAP.
Проверить поддержку в системе можно следующим образом:
cat /boot/config-`uname -r` | grep CONFIG_NETLINK_MMAP
Так же, библиотека libnml вроде тоже уже поддерживает mmap операции, но сам я не проверял.
Итак, как пользоваться:
открываем сокет:
открываем сокет:
struct sockaddr_nl src_addr;
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK;
src_addr.nl_pid = getpid();
fd = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_USERSOCK);
bind(fd, (struct sockaddr *)&src_addr, sizeof(src_addr));
конфигурируем очереди:
struct nl_mmap_req req;
req.nm_block_size = blk_sz;
req.nm_block_nr = (unsigned int)ring_sz / blk_sz;
req.nm_frame_size = NL_FR_SZ;
req.nm_frame_nr = ring_sz / NL_FR_SZ;
if (setsockopt(fd, SOL_NETLINK, NETLINK_RX_RING, &req, sizeof(req)) < 0)
err(EXIT_FAILURE, "cannot setup netlink rx ring ");
if (setsockopt(fd, SOL_NETLINK, NETLINK_TX_RING, &req, sizeof(req)) < 0)
err(EXIT_FAILURE, "cannot setup netlink tx ring ");
rx_ring = mmap(NULL, MMAP_SZ, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);Последняя операция нужна для получения указателя на TX очередь, так как память выделяется непрерывным куском, очереди следуют друг за другом и имеют одинаковый размер. На выбор пользователя, можно конфигурировать только одну конкретную очередь.
tx_ring = rx_ring + ring_sz;
Теперь можно начинать общаться с ядром используя указанные очереди.
В своем примере я решил передавать не просто строку, а использовать простенький протокол.
Протокол описывается следующими структурами данных:
typedef enum { MSG_OK = 1, MSG_PING = 2, MSG_PONG = 4, MSG_DATA = 8 } m_type_t;
typedef struct __nl_msg_ {
m_type_t type; /* Message type, bitmask */
size_t len; /* Payload length */
} __attribute__((packed)) us_nl_msg_t;
И отправлять в ядро я буду эту структуру, за которой, опционально, будет следовать некий набор байт(для простоты - строка ASCII символов).
Модуль ядра, который я написал, при получении сообщения типа MSG_PING, будет отвечать сообщением MSG_PONG. Наличие флага MSG_DATA будет означать , что после структуры идет len байт данных, которые ядро будет отправлять обратно в user-space.
С точки зрения кода в ядре, разницы между обычной отправкой и работой через maped memory - нет. Исходник модуля ядра находится тут.
Там все просто.
При загрузке модуля, открываем Netlink сокет:
struct netlink_kernel_cfg cfg = {
.input = if_rcv,
};
nl_sk = netlink_kernel_create(&init_net, NETLINK_USERSOCK, &cfg);
if (!nl_sk) {
printk(KERN_ALERT "Error creating socket.\n");
return -10;
} else
printk(KERN_INFO "Netlink socket created.\n");
if_rcv это callback функция, которая будет вызываться при поступлении данных в сокет.
Она, в свою очередь, вызывает функцию netlink_rcv_skb(skb, &hello_nl_recv_msg), которая проверяет валидность запроса и вызывает уже реальный обработчик - hello_nl_recv_msg.
NETLINK_USERSOCK - указывает на конкретный протокол. У Netlink их много, все можно посмотреть в исходниках ядра, в файле: include/uapi/linux/netlink.h Большая часть используется сетевой подсистемой ядра и соответствующими приложениями(ip, расширения Netfilter).
Данный конкретный может использоваться в произвольных пользовательских приложениях.
Данный конкретный может использоваться в произвольных пользовательских приложениях.
Функция hello_nl_recv_msg выглядит следующим образом:
us_nl_msg_t *msg;
char *usr_message;
msg = nlmsg_data(nlh);
usr_message = (char *)((void *)msg + sizeof(us_nl_msg_t));
printk(KERN_INFO "From [%u] msg type %s , payload len: %d, message %.*s\n", nlh->nlmsg_pid, print_out_m_type(msg->type), (int)msg->len, (int)msg->len, usr_message);
struct netlink_dump_control c = {
.dump = hello_nl_send_msg,
.data = msg,
.min_dump_alloc = NL_FR_SZ / 2, };
return netlink_dump_start(nl_sk, skb, nlh, &c);В ней мы получаем указатель на структуру nlmsghdr , а затем указатель на передаваемую структуру us_nl_msg и указатель на пользовательской сообщение.
Функция netlink_dump_start позволяет подготовить и отправить ответ. Поле dump структуры netlink_dump_control указывает на callback функцию, которая подготовит пользовательские данные для отправки, data - указатель на произвольные данные, которые, при необходимости, нужно передать в callback.
Ключевым моментом в отправке сообщения, является вызов:
nlh = nlmsg_put(skb, NETLINK_CB(cb->skb).portid, cb->nlh->nlmsg_seq, cb->nlh->nlmsg_type, sizeof(us_nl_msg_t) + msg_len, 0));
Он возвращает указатель на заголовок подготовленного пакета, за которым пойдут наши данные.
us_nl_msg_t *resp, *req = cb->data;
resp = (us_nl_msg_t *)nlmsg_data(nlh);
resp_msg = (char *)((void *)req + sizeof(us_nl_msg_t));
resp->type = MSG_OK | MSG_DATA;
resp->len = msg_len;
memcpy((void *)((void *)resp + sizeof(us_nl_msg_t)), resp_msg, msg_len);
Теперь можно перейти к user-space части.
Отправка работает следующим образом:
Получаем указатель на структуру nl_mmap_hdr и проверяем статус фрейма:
fr_hdr = tx_ring + tx_offset;Если статус не NL_MMAP_STATUS_UNUSED, значит доступных для заполнения фреймов на данный момент нет, можно вызвать poll и ожидать события POLLOUT.
if (fr_hdr->nm_status != NL_MMAP_STATUS_UNUSED) return 0;
Если же все в порядке, заполняем заголовок Netlink пакета:
nlh->nlmsg_len = NLMSG_SPACE(size);
nlh->nlmsg_pid = sender_pid;
nlh->nlmsg_flags |= NLM_F_REQUEST;
nlh->nlmsg_type = NLMSG_MIN_TYPE + 1;
Флаг NLM_F_REQUEST указывает на то, что сообщение содержит единичный запрос. Флаги описаны в исходниках ядра, файл include/uapi/linux/netlink.h
Получаем указатель на память, которую заполняем нашими данными:
us_nl_msg_t *message = NLMSG_DATA(nlh);
Заполняем пользовательские данные:message->type = MSG_OK | MSG_DATA; message->len = data_len; user_data = (char *)((void *)message + sizeof(us_nl_msg_t)); memcpy(user_data, data, data_len);А затем заполняем поля в заголовке фрейма:
fr_hdr->nm_len = nlh->nlmsg_len;
fr_hdr->nm_status = NL_MMAP_STATUS_VALID;
fr_hdr->nm_group = 0;
fr_hdr->nm_pid = 0;
И наконец-то отправляем:
struct sockaddr_nl addr = { .nl_family = AF_NETLINK, .nl_pid = 0, .nl_groups = 0, };
sendto(fd, NULL, 0, 0, (const struct sockaddr *)&addr, sizeof(addr));
Получение данных от ядра выглядит следующим образом:
while (1) {
pfds[0].fd = fd;
pfds[0].events = POLLIN | POLLERR;
pfds[0].revents = 0;
if (poll(pfds, 1, -1) < 0 && errno != -EINTR) return 0;
if (pfds[0].revents & POLLERR) return 0;
if (!(pfds[0].revents & POLLIN)) continue;
exit_loop = 0;
while (1) {
fr_hdr = (struct nl_mmap_hdr *)(rx_ring + rx_offset);
switch (fr_hdr->nm_status) {
case NL_MMAP_STATUS_VALID:
nlh = (struct nlmsghdr *)((void *)fr_hdr + NL_MMAP_HDRLEN);
len = fr_hdr->nm_len; if (len != 0) process_msg(nlh);
break;
case NL_MMAP_STATUS_COPY:
printf("Frame could not mapped. Back to regular recv()\n");
if ((len = recv(fd, buf, sizeof(buf), MSG_DONTWAIT)) <= 0) break;
nlh = (struct nlmsghdr *)buf; process_msg(nlh);
break;
default:
exit_loop++; break;
}
if (exit_loop) break;
fr_hdr->nm_status = NL_MMAP_STATUS_UNUSED;
adv_offset(&rx_offset, NL_FR_SZ, &ring_sz);
}
}
Просто крутимся на poll и обрабатываем получаемые данные. Важно не забывать менять статус фрейма на NL_MMAP_STATUS_UNUSED после его обработки. Еще одним важным моментом является необходимость получать данные обычным образом, если ядро по каким-то причинам не смогло передать данные через выделенную область память. В этом случае фрейм имеет статус NL_MMAP_STATUS_COPY.
Вот и все.
Ссылка на полный репозиторий примера:
И другие полезные ссылки:
http://workshop.netfilter.org/2011/wiki/images/1/17/Mmaped-netlink.pdf
http://workshop.netfilter.org/2011/wiki/images/1/17/Mmaped-netlink.pdf
Комментариев нет:
Отправить комментарий