четверг, 16 февраля 2017 г.

Mmaped Netlink in Linux kernel

В процессе поиска решения одной интересной задачи, связанной с 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 приложении эта область память выглядит как непрерывный набор фреймов.

Заголовок фрейма выглядит как:

struct nl_mmap_hdr {
     unsigned int nm_status;
     unsigned int nm_len;
     __u32 nm_group;
/* credentials */
     __u32 nm_pid;
     __u32 nm_uid;
     __u32 nm_gid;
};
Значения поля 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 ");
Подробно параметры структуры nl_mmap_req описаны тут.

просим у ядра указатель на память очередей:
rx_ring = mmap(NULL, MMAP_SZ, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
tx_ring = rx_ring + ring_sz;
Последняя операция нужна для получения указателя на TX очередь, так как память выделяется непрерывным куском, очереди следуют друг за другом и имеют одинаковый размер. На выбор пользователя, можно конфигурировать только одну конкретную очередь.

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

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;
if (fr_hdr->nm_status != NL_MMAP_STATUS_UNUSED) return 0;
Если статус не NL_MMAP_STATUS_UNUSED, значит доступных для заполнения фреймов  на данный момент нет, можно вызвать poll и ожидать события POLLOUT.
Если же все в порядке, заполняем заголовок 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

Комментариев нет:

Отправить комментарий