本文转载自微信公众号「黑光技术」,作者helight 。转载本文请联系黑光技术公众号。
前言
这几天在看 ipvs 相关代码的时候又遇到了 netlink 的事情,所以这两天花了点时间重新把 netlink 的事情梳理了一下。
什么是 netlink
linux 内核一直存在的一个严重问题就是内核态和用户态的交互的问题,对于这个问题内核大佬们一直在研究各种方法,想让内核和用户态交互能够安全高效的进行。如系统调用,proc,sysfs等内存文件系统,但是这些方式一般都比较简单,只能在用户空间轮询访问内核的变化,内核的变化无法主动的推送出来。
而 netlink 的出现比较好的解决了这个问题,而且 netlink 还有以下一些优势:
- 可以直接使用 socket 套接字的 API 进行内核和用户态的通信,开发使用上相对简单了很多。
- 利用内核协议栈有了缓冲队列,是一种异步通信机制。
- 可以是内核和用户态的双向通信,内核可以主动向用户态进程发送消息。这个是以往通信方式不具备的。
- 针对同一个协议类型的所有用户进程,内核可以广播消息给所有的进程,也可以指定进程 pid 进行消息发送。
目前 netlink 的这种机制被广泛使用在各种场景中,在 Linux 内核中使用 netlink 进行应用与内核通信的应用很多; 包括:路由 daemon(NETLINK_ROUTE),用户态 socket 协议(NETLINK_USERSOCK),防火墙(NETLINK_FIREWALL),netfilter 子系统(NETLINK_NETFILTER),内核事件向用户态通知(NETLINK_KOBJECT_UEVENT)等。具体支持的类型可以查看这个文件 include/uapi/linux/netlink.h。
netlink 内核代码走读
netlink 内核相关文件介绍
netlink 的内核代码在内核代码的 net/netlink/ 目录下,我目前看的是 5.7.10 的内核版本,netlink 内核相关的文件不多,还是比较清晰的:
- helightxu@ ~/open_code/linux-5.7.10 ls net/netlink
- config Makefile af_netlink.c af_netlink.h diag.c genetlink.c
- helightxu@ ~/open_code/linux-5.7.10
文件 | 描述 |
---|---|
af_netlink.c 和 af_netlink.h: | 是 netlink 的核心文件,这个也是下面详细走读的内容。 |
diag.c | 对 netlink sock 进行监控,可以插入到内核或者从内核中卸载 |
genetlink.c | 这个可以看作是 netlink 的升级版本,或者说是一种高层封装。 |
注:
- genetlink.c 额外说明:netlink 默认支持了 30 多种的场景,但是对于其它场景并没有具体定义,这个时候这种通用封装就有了很大的好处,可以在不改动内核的前提下进行应用场景扩展,这部分内容可以看这个 wiki:https://wiki.linuxfoundation.org/networking/generic_netlink_howto
还有一个头文件是在 include 目录,如下所示,这个头文件是一些辅助函数、宏定义和相关数据结构,大家学习的同学一定要看这个文件,它里面的注释非常详细。这些注释对理解 netlink 的消息结构非常有用,建议可以详细看看。
- helightxu@ ~/open_code/linux-5.7.10 ls include/net/netlink.h
af_netlink.c 代码走读
在 af_netlink.c 这个文件的最下面有一行代码:
- core_initcall(netlink_proto_init);
这段代码的意思是什么呢?通过看这个代码最终的实现可以看出,就是告诉编译器把 netlink_proto_init 这个函数放入到最终编译出来二进制文件的 .init 段中,内核在启动的时候会从这个端里面的函数挨个的执行。这里也就是说 netlink 是内核默认就直接支持的,是原生内核的一部分(这里其实想和内核的动态插拔模块区别)。
在 netlink_proto_init 函数中最关键的一行代码就是下面最后一行,把 netlink 的协议族注册到网络协议栈中去。
- static const struct net_proto_family netlink_family_ops = {
- .family = PF_NETLINK,
- .create = netlink_create,
- .owner = THIS_MODULE,
- };
- ...
- sock_register(&netlink_family_ops);
PF_NETLINK 是表示 netlink 的协议族,在后面我们在客户端创建 netlink socket 的时候就要用到这个东东。如下面的代码,代码来自我的测试代码 https://github.com/helight/kernel_modules/tree/master/netlink_test 中的客户端代码,可以看出:PF_NETLINK 表示我们所用的就是 netlink 的协议,SOCK_RAW 表示我们使用的是原始协议包,NETLINK_USER 这个我们自己定义的一个协议字段。netlink 我们前面说了有 30 多种应用场景,这些都已经在内核代码中固定了,所以在客户端使用的时候会指定这个字段来表示和内核中的那个应用场景的函数模块进行交互。
- //int socket(int domain, int type, int protocol);
- sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_USER);
sock_register 这个函数的作用主要就是注册 PF_NETLINK 这个协议类型到内核中,让内核认识这个协议,在内核网络协议中建立 socket 的时候知道该使用那个协议为它提供操作支持。
注册了之后内核就支持了 netlink 协议了,接下来就是内核中创建监听 socket,用户态创建链接 socket 了。
netlink 用户态和内核交互过程
这里我简单画一个图来表示一下,socket 通信主要有 2 个操作对象:server 端和 client 端。netlink 的操作原理是这样的:
对象 | 所处位置 | - |
---|---|---|
server 端 | 内核中 | - |
client 端 | 用户态进程 | - |
netlink 关键数据结构和函数
sockaddr_nl 协议套接字
netlink 的地址表示由 sockaddr_nl 负责
- struct sockaddr_nl {
- __kernel_sa_family_t nl_family;
- unsigned short nl_pad;
- __u32 nl_pid;
- __u32 nl_groups;
- };
nl_family 制定了协议族,netlink 有自己独立的值:AF_NETLINK,nl_pid 一般取为进程 pid。nl_groups 用以多播,当不需要多播时,该字段为 0。
nlmsghdr 消息体
netlink 消息是作为套接字缓冲区 sk_buff 的数据部分传递的,其消息本身又分为头部和数据。头部为:
- struct nlmsghdr {
- __u32 nlmsg_len;
- __u16 nlmsg_type;
- __u16 nlmsg_flags;
- __u32 nlmsg_seq;
- __u32 nlmsg_pid;
- };
nlmsg_len 为消息的长度,包含该头部在内。nlmsg_pid 为发送进程的端口 ID,这个用户可以自定义,一般也是使用进程 pid。
msghdr 用户态系发送消息体
使用 sendmsg 和 recvmsg 函数进行发送和接收消息,使用的消息体是这个样子的。
- struct iovec {
- void *iov_base;
- size_t iov_len;
- };
-
- struct msghdr {
- void * msg_name;
- int msg_namelen;
- struct iovec * msg_iov;
- __kernel_size_t msg_iovlen;
- void * msg_control;
- __kernel_size_t msg_controllen;
- unsigned int msg_flags;
- };
-
逻辑结构如下:
socket 也是一种特殊的文件,通过 VFS 的接口同样可以对其进行使用管理。socket 本身就需要实现文件系统的相应接口,有自己的操作方法集。
netlink 常用宏
- #define NLMSG_ALIGNTO 4U
- #define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
-
- #define NLMSG_HDRLEN ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))
-
- #define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)
-
- #define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
-
- #define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
-
- #define NLMSG_NEXT(nlh,len) ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \ (struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))
-
- #define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \ (nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \ (nlh)->nlmsg_len <= (len))
-
- #define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))
netlink 内核常用函数
netlink_kernel_create
这个内核函数用于创建内核 socket,以提供和用户态通信
- static inline struct sock *
- netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
-
-
-
- struct netlink_kernel_cfg {
- unsigned int groups;
- unsigned int flags;
- void (*input)(struct sk_buff *skb);
- struct mutex *cb_mutex;
- void (*bind)(int group);
- bool (*compare)(struct net *net, struct sock *sk);
- };
单播函数 netlink_unicast() 和多播函数 netlink_broadcast()
-
- extern int netlink_unicast(struct sock *ssk, struct sk_buff *skb, __u32 portid, int nonblock);
-
-
-
- extern int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, __u32 portid,
- __u32 group, gfp_t allocation);
-
测试例子代码
netlink 内核建立 socket 过程
内核的代码非常简单,这里给出了核心代码,就这么多,接收函数中直接打印了接收到的消息。
- #include
#include #include #include #include #include #include - #define NETLINK_XUX 31
- static struct sock *xux_sock = NULL;
-
- // 接收消息的回调函数,接收参数是 sk_buff
- static void recv_netlink(struct sk_buff *skb)
- {
- struct nlmsghdr *nlh;
- nlh = nlmsg_hdr(skb); // 取得消息体
- printk("receive data from user process: %s", (char *)NLMSG_DATA(nlh)); // 打印接收的数据内容
-
- ...
- }
-
- int __init init_link(void)
- {
- struct netlink_kernel_cfg cfg = {
- .input = recv_netlink,
- };
- xux_sock = netlink_kernel_create(&init_net, NETLINK_XUX, &cfg); // 创建内核 socket
- if (!xux_sock){
- printk("cannot initialize netlink socket");
- return -1;
- }
-
- printk("Init OK!\n");
- return 0;
- }
netlink 用户态建立链接和收发信息
- ... // 上面的就省了
- #define NETLINK_USER 31 //self defined
- #define MAX_PAYLOAD 1024
- struct sockaddr_nl src_addr, dest_addr;
- struct nlmsghdr *nlh = NULL;
- struct msghdr msg;
- struct iovec iov;
- int sock_fd;
-
- int main(int args, char *argv[])
- {
- sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_USER); // 建立 socket
-
- if(sock_fd < 0)
- return -1;
-
- memset(&src_addr, 0, sizeof(src_addr));
- src_addr.nl_family = AF_NETLINK;
- src_addr.nl_pid = getpid();
-
- if(bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr))){ // 和指定协议进行 socket 绑定
- perror("bind() error\n");
- close(skfd);
- return -1;
- }
-
- memset(&dest_addr, 0, sizeof(dest_addr));
- dest_addr.nl_family = AF_NETLINK;
- dest_addr.nl_pid = 0;
- dest_addr.nl_groups = 0;
-
- nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
- memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
- nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
- nlh->nlmsg_pid = getpid(); //self pid
- nlh->nlmsg_flags = 0;
- // 拷贝信息到发送缓冲中
- strcpy(NLMSG_DATA(nlh), "Hello this is a msg from userspace");
- // 构造发送消息体
- iov.iov_base = (void *)nlh; //iov -> nlh
- iov.iov_len = nlh->nlmsg_len;
- msg.msg_name = (void *)&dest_addr;
- msg.msg_namelen = sizeof(dest_addr);
- msg.msg_iov = &iov; // iov 中存放 netlink 消息头和消息数据
- msg.msg_iovlen = 1;
-
- printf("Sending message to kernel\n");
-
- int ret = sendmsg(sock_fd, &msg, 0); // 发送消息到内核
- printf("send ret: %d\n", ret);
-
- printf("Waiting for message from kernel\n");
-
-
- recvmsg(sock_fd, &msg, 0);
- printf("Received message payload: %s\n", NLMSG_DATA(nlh)); // 打印接收到的消息
-
- close(sock_fd);
- return 0;
- }
以上代码在我的个人仓库中都有,如果有兴趣可以 clone 下来自己测试玩耍一遍。代码仓库:https://github.com/helight/kernel_modules/tree/master/netlink_test 。
总结
netlink 目前感觉还是一个比较好用的内核和用户空间的交互方式,但是也是有他的使用场景,适合用户空间和内核空间主动交互的场景。
但是在单机场景下,大多数的主动权在用户进程,用户进程写数据到内核,用户进程主动读取内核数据。这两种场景覆盖了内核的绝大多数场景。
在内核要主动的场景下,netlink 就比较适合。我能想到的就是内核数据审计,安全触发等,这类场景下内核可以实时的告知用户进程内核发生的情况。
我是在看 ipvs 的代码时候看到了里面有 netlink 的使用,发现早期 iptables 就是使用 netlink 来下发配置指令的,内核中 netfilter 和 iptables 中还有这部分的代码,今天也顺便下载大致走读了一遍,大家可以搜索 NETLINK 这个关键字来看。但是 iptables 后来的代码中没有使用这样的方式,而是采用了一个叫做 iptc 的库,其核心思路还是使用 setsockops 的方式,最终还是 copy_from_user。不过这种方式对于 iptables 这种配置下发的场景来说还是非常实用的。
最后大家如果对这方面有兴趣的可以找我一起再研究研究,我也想继续在深入看看,但是目前没有太好的场景。
明天开始走读 kubernetes 的代码,大家有兴趣的也可以一起来交流。
参考
https://www.cnblogs.com/x_wukong/p/5920437.html