Kernel Connector(翻译) taro Posted on Jun 30 2021 ## 介绍 **Kernel Connector**(内核连接器) 是一种新的基于 `netlink`从`用户空间`到`内核空间`的易于使用的通信模块。 Kernel Connector driver使得基于 netlink 的网络连接(?)变得容易使用。 在使用时需要注册一个回调(callback)和一个标识符(identifier)。 从用户空间的角度来看,他用起来很简单,需要实现下面四个方法: ```c socket(); bind(); send(); recv(); ``` 但是如果内核空间想要使用这种连接的全部功能,驱动程序编写者必须创建特殊的套接字,需要了解`struct sk_buff` , `handling` 等等。 Kernel Connector driver 允许任何内核空间以一种非常简单的方式使用基于 netlink 的网络进行进程通信。 ```c int cn_add_callback(struct cb_id *id, char *name, void (*callback) (struct cn_msg *, struct netlink_skb_parms *)); void cn_netlink_send_multi(struct cn_msg *msg, u16 len, u32 portid, u32 __group, int gfp_mask); void cn_netlink_send(struct cn_msg *msg, u32 portid, u32 __group, int gfp_mask); struct cb_id { __u32 idx; __u32 val; }; ``` `idx` 和 `val` 是唯一标识符,必须在`connector.h `的头文件中被定义(?)。 `void (*callback) (void *)` 是一个回调函数,当 connector core 接收到以 idx.val 为唯一标识的消息时候将会被调用,这个函数的形参的类型必须是`struct cn_msg *` ```c struct cn_msg { struct cb_id id; __u32 seq; __u32 ack; __u32 len; /* Length of the following data */ __u8 data[0]; }; ``` ## 接口 ```c // 使用 connector core 注册一个新的回调函数 int cn_add_callback(struct cb_id *id, char *name, void (*callback) (struct cn_msg *, struct netlink_skb_parms *)); // 唯一标识 connector , 对于合法的内核用户来说,必须在connector.h 中注册 struct cb_id *id // conncetor 的回调函数名 char *name // connector 的回调函数。 cn_msg 以及 发送者的凭据(?) void (*callback) (struct cn..) // 使用 connector core 取消一个新的回调函数 void cn_del_callback(struct cb_id *id); // 唯一标识 connector struct cb_id *id // int cn_netlink_send_multi(struct cn_msg *msg, u16 len, u32 portid, u32 __groups, int gfp_mask); int cn_netlink_send(struct cn_msg *msg, u32 portid, u32 __groups, int gfp_mask); Sends message to the specified groups. It can be safely called from softirq context, but may silently fail under strong memory pressure. If there are no listeners for given group -ESRCH can be returned. // 向指定的组发送消息,它可以从软中断的上线文中被安全的调用,但是在内存压力过高的的时候可能会默默失败。 // 如果指定的组没有监听器,会返回 -ESRCH struct cn_msg * // - 消息头(带有附加数据) u16 len // - for *_multi multiple cn_msg messages can be sent u32 port // - 目的端口,如果非零,消息将被发送到给定的端口,应设置为原始发送者(?) u32 __group // - 目的组别 // 如果端口和 __group都为零,则在所有注册连接器的用户进行搜索适当的组,并且消息将被传递到 // 与 msg 中 ID 相同用户的组中 // 如果端口不为零,这个消息将会被发送到指定的组 int gfp_mask // - GFP mask. // Note: 在注册新的回调用户时,connector core 会分配 netlink 组到用户,等于其 id.idx ``` ## 协议说明 当前框架提供了一个带有固定标头的传输层。这使用此类标头的推荐协议如下: `msg->seq` 和 `msg->ack` 用于确定消息确认抵达。当有消息被发送的时候,使用本地唯一的序列和随机确认号码。序列号也可以复制到`nlmsgghdr->nlmsg_seq`。 序列号(seq)随着每条消息的发送而递增。 如果希望收到对消息的回复,则在收到的消息与原始消息中的序列号必须相同,并且确认编号必须 + 1。 如果我们收到一条消息带着非预期的序列号,那么它是一个新的消息。 如果我们收到一条消息并且他的序列号是预期的,但它的确认号(ack)不等于原始序列号(seq)+ 1,则是一条新消息。 显然,协议头包含了上面的id。 连接器允许以下形式的事件通知: 内核驱动程序或用户空间进程可以要求连接器在何时通知它选定的ID将被打开或关闭(已注册或未注册其回调函数)。 它是通过向连接器驱动发送特殊命令来结束(它还使用 id={-1, -1} 注册自己)。 可以在 `cn_test.c` 模块中找到这种用法的示例,其中使用连接器请求通知和发送消息。 > cn_test.c ```c // SPDX-License-Identifier: GPL-2.0-or-later /* * cn_test.c * * 2004+ Copyright (c) Evgeniy Polyakov <zbr@ioremap.net> * All rights reserved. */ #define pr_fmt(fmt) "cn_test: " fmt #include <linux/kernel.h> #include <linux/module.h> #include <linux/moduleparam.h> #include <linux/skbuff.h> #include <linux/slab.h> #include <linux/timer.h> #include <linux/connector.h> static struct cb_id cn_test_id = { CN_NETLINK_USERS + 3, 0x456 }; static char cn_test_name[] = "cn_test"; static struct sock *nls; static struct timer_list cn_test_timer; static void cn_test_callback(struct cn_msg *msg, struct netlink_skb_parms *nsp) { pr_info("%s: %lu: idx=%x, val=%x, seq=%u, ack=%u, len=%d: %s.\n", __func__, jiffies, msg->id.idx, msg->id.val, msg->seq, msg->ack, msg->len, msg->len ? (char *)msg->data : ""); } /* * Do not remove this function even if no one is using it as * this is an example of how to get notifications about new * connector user registration */ #if 0 static int cn_test_want_notify(void) { struct cn_ctl_msg *ctl; struct cn_notify_req *req; struct cn_msg *msg = NULL; int size, size0; struct sk_buff *skb; struct nlmsghdr *nlh; u32 group = 1; size0 = sizeof(*msg) + sizeof(*ctl) + 3 * sizeof(*req); size = NLMSG_SPACE(size0); skb = alloc_skb(size, GFP_ATOMIC); if (!skb) { pr_err("failed to allocate new skb with size=%u\n", size); return -ENOMEM; } nlh = nlmsg_put(skb, 0, 0x123, NLMSG_DONE, size - sizeof(*nlh), 0); if (!nlh) { kfree_skb(skb); return -EMSGSIZE; } msg = nlmsg_data(nlh); memset(msg, 0, size0); msg->id.idx = -1; msg->id.val = -1; msg->seq = 0x123; msg->ack = 0x345; msg->len = size0 - sizeof(*msg); ctl = (struct cn_ctl_msg *)(msg + 1); ctl->idx_notify_num = 1; ctl->val_notify_num = 2; ctl->group = group; ctl->len = msg->len - sizeof(*ctl); req = (struct cn_notify_req *)(ctl + 1); /* * Idx. */ req->first = cn_test_id.idx; req->range = 10; /* * Val 0. */ req++; req->first = cn_test_id.val; req->range = 10; /* * Val 1. */ req++; req->first = cn_test_id.val + 20; req->range = 10; NETLINK_CB(skb).dst_group = ctl->group; //netlink_broadcast(nls, skb, 0, ctl->group, GFP_ATOMIC); netlink_unicast(nls, skb, 0, 0); pr_info("request was sent: group=0x%x\n", ctl->group); return 0; } #endif static u32 cn_test_timer_counter; static void cn_test_timer_func(struct timer_list *unused) { struct cn_msg *m; char data[32]; pr_debug("%s: timer fired\n", __func__); m = kzalloc(sizeof(*m) + sizeof(data), GFP_ATOMIC); if (m) { memcpy(&m->id, &cn_test_id, sizeof(m->id)); m->seq = cn_test_timer_counter; m->len = sizeof(data); m->len = scnprintf(data, sizeof(data), "counter = %u", cn_test_timer_counter) + 1; memcpy(m + 1, data, m->len); cn_netlink_send(m, 0, 0, GFP_ATOMIC); kfree(m); } cn_test_timer_counter++; mod_timer(&cn_test_timer, jiffies + msecs_to_jiffies(1000)); } static int cn_test_init(void) { int err; err = cn_add_callback(&cn_test_id, cn_test_name, cn_test_callback); if (err) goto err_out; cn_test_id.val++; err = cn_add_callback(&cn_test_id, cn_test_name, cn_test_callback); if (err) { cn_del_callback(&cn_test_id); goto err_out; } timer_setup(&cn_test_timer, cn_test_timer_func, 0); mod_timer(&cn_test_timer, jiffies + msecs_to_jiffies(1000)); pr_info("initialized with id={%u.%u}\n", cn_test_id.idx, cn_test_id.val); return 0; err_out: if (nls && nls->sk_socket) sock_release(nls->sk_socket); return err; } static void cn_test_fini(void) { del_timer_sync(&cn_test_timer); cn_del_callback(&cn_test_id); cn_test_id.val--; cn_del_callback(&cn_test_id); if (nls && nls->sk_socket) sock_release(nls->sk_socket); } module_init(cn_test_init); module_exit(cn_test_fini); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Evgeniy Polyakov <zbr@ioremap.net>"); MODULE_DESCRIPTION("Connector's test module"); ``` ## 可靠性 Netlink 本身并不是一个可靠的协议。这意味着消息可以由于内存压力或进程的接收队列溢出而丢失, 所以调用者被必须注意做好丢失的准备。这就是为什么结构 `cn_msg` [主连接器的消息头] 包含 `u32 seq` 和 `u32 ack` 字段。 ## 用户态使用 2.6.14 有一个新的 netlink 套接字实现,默认情况下不会允许将数据发送到 1 以外的 netlink 组。 因此,如果您希望在使用 `netlink套接字`(例如使用连接器)时使用不同的组号,用户空间的应用程序必须先订阅那个组。可以通过以下伪代码实现: ```c s = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_CONNECTOR); l_local.nl_family = AF_NETLINK; l_local.nl_groups = 12345; l_local.nl_pid = 0; if (bind(s, (struct sockaddr *)&l_local, sizeof(struct sockaddr_nl)) == -1) { perror("bind"); close(s); return -1; } { int on = l_local.nl_groups; setsockopt(s, 270, 1, &on, sizeof(on)); } ``` > 其中 270 以上是 `SOL_NETLINK`,1 是 `NETLINK_ADD_MEMBERSHIP` 套接字选项。要删除多播订阅,套接字选项应该定义为 `NETLINK_DROP_MEMBERSHIP` (0) 。 2.6.14 netlink 代码只允许在 netlink_kernel_create() 时使用的组选择小于或等于的组最大组号。 如果是连接器,它是 `CN_NETLINK_USERS` + `0xf`,所以如果你想使用组编号 12345,您必须在租号前增加一个 `CN_NETLINK_USERS`。 额外的 `0xf` 编号被分配给非内核用户使用。 由于此限制,组 0xffffffff 现在不起作用,因此可以不使用添加/删除连接器的组通知,但据我所知, 只有 cn_test.c 测试模块使用了它。 ## 代码示例 可以找到连接器测试模块和用户空间的示例代码在`samples/connector/`。要构建此代码,请启用 `CONFIG_CONNECTOR` 和 `CONFIG_SAMPLES`。 [Connector Samples](https://github.com/torvalds/linux/tree/master/samples/connector) Kprobe Hook Linux Kernel WQL学习