Linux: Netlink 简介

文章目录

  • [1. 前言](#1. 前言)
  • [2. Netlink 范例](#2. Netlink 范例)
  • [3. Netlink 简析](#3. Netlink 简析)
    • [3.1 Netlink 协议簇注册](#3.1 Netlink 协议簇注册)
    • [3.2 创建 用户空间 Netlink 套接字](#3.2 创建 用户空间 Netlink 套接字)
    • [3.3 用户空间 Netlink 套接字 的 绑定](#3.3 用户空间 Netlink 套接字 的 绑定)
    • [3.4 向 内核空间 Netlink 套接字 发消息](#3.4 向 内核空间 Netlink 套接字 发消息)
    • [3.5 从 内核空间 Netlink 套接字 读消息](#3.5 从 内核空间 Netlink 套接字 读消息)
      • [3.5.1 内核空间 Netlink 套接字 向 用户空间 Netlink 套接字 发送消息](#3.5.1 内核空间 Netlink 套接字 向 用户空间 Netlink 套接字 发送消息)
      • [3.5.2 用户空间 Netlink 套接字 读取 内核空间 Netlink 套接字 发送的消息](#3.5.2 用户空间 Netlink 套接字 读取 内核空间 Netlink 套接字 发送的消息)
  • [4. 参考资料](#4. 参考资料)

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

Netlink 通信包括 内核空间用户空间 两部分,具体来讲,是两个分别位于 内核空间用户空间AF_NETLINK 协议簇套接字。来看一个具体的例子,先看 内核空间 部分 netlink_kern_test.c

c 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <net/sock.h>
#include <linux/netlink.h>

extern struct net init_net;

#define NETLINK_TEST	30
#define MSG_LEN			100
#define USER_PORT		66

static struct sock *test_nlsk;

int send_usrmsg(char *buf, uint16_t len)
{
    struct sk_buff *nl_skb;
    struct nlmsghdr *nlh;

	nl_skb = nlmsg_new(len, GFP_ATOMIC);
    if (!nl_skb) {
        pr_err("netlink alloc failure\n");
        return -1;
    }

	nlh = nlmsg_put(nl_skb, 0, 0, NETLINK_TEST, len, 0);
    if (nlh == NULL) {
        pr_err("nlmsg_put failure\n");
        nlmsg_free(nl_skb);
        return -1;
    }

	memcpy(nlmsg_data(nlh), buf, len);

	return netlink_unicast(test_nlsk, nl_skb, USER_PORT, MSG_DONTWAIT);
}

static void netlink_rcv_msg(struct sk_buff *skb)
{   
    struct nlmsghdr *nlh;
    char *umsg = NULL;
    char *kmsg = "Hello user process!";
	
	nlh = nlmsg_hdr(skb);
    umsg = NLMSG_DATA(nlh);
    if (umsg) {
        printk("kernel recv msg from user: %s\n", umsg);
        printk("port id: %d\n", NETLINK_CB(skb).portid);
        send_usrmsg(kmsg, strlen(kmsg));
    }
}

static int __init test_netlink_init(void)
{
	struct netlink_kernel_cfg cfg = {
		.input = netlink_rcv_msg,
	};
	
	 test_nlsk = (struct sock *)netlink_kernel_create(&init_net, NETLINK_TEST, &cfg);

	return test_nlsk ? 0 : -1;
}

static void __exit test_netlink_exit(void)
{
    netlink_kernel_release(test_nlsk);
}

module_init(test_netlink_init);
module_exit(test_netlink_exit);

MODULE_LICENSE("GPL");

内核 Netlink 示例模块的逻辑很简单,调用接口 netlink_kernel_create() 创建一个 Netlink 内核空间套接字,且其协议类型为自定的 NETLINK_TEST,其数据接收接口为 netlink_rcv_msg() 。当其它 Netlink 套接字向它发送数据时,内核调用 netlink_rcv_msg() 处理数据接收,此时可通过调用 netlink_unicast() 向数据发送套接字回送数据。

接下来构建一个 Makefile 来编译这个内核模块,并安装到系统。Makefile 内容如下:

c 复制代码
ifneq ($(KERNELRELEASE),)

obj-m	:= netlink_kern_test.o

else

KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD       := $(shell pwd)

modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

endif

clean:
	rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions .cache.mk modules.order Module.symvers
bash 复制代码
$ make
$ sudo insmod netlink_kern_test.ko

再看 Netlink 示例的用户控件部分 netlink_user_test.c

c 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <string.h>
#include <linux/netlink.h>
#include <stdint.h>
#include <unistd.h>

#define NETLINK_TEST 30
#define USER_PORT 66

#define MSG_LEN 100
#define MAX_PLOAD 200

struct netlink_user_msg {
    struct nlmsghdr hdr;
    char msg[MSG_LEN];
};

int main(int argc,char **argv)
{
    int sockfd;
    struct sockaddr_nl saddr, daddr;
    struct nlmsghdr *nlh;
    struct netlink_user_msg u_info;
    char *msg = "Hello kernel, I am a user process!";
    socklen_t len;

	// 创建 Netlink 用户空间套接字
	sockfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
	if (sockfd < 0) {
		perror("socket");
		return -1;
	}

	// 数据 源 Netlink 套接字 地址
	memset(&saddr, 0, sizeof(saddr));
    saddr.nl_family = AF_NETLINK;
    saddr.nl_pad = 0;
    saddr.nl_pid = USER_PORT; // 端口号
    saddr.nl_groups = 0;
    if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0) {
		perror("socket");
		return -1;
	}

	// Netlink 消息数据
	nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PLOAD));
    memset(nlh, 0, sizeof(struct nlmsghdr));
    nlh->nlmsg_len = NLMSG_SPACE(MAX_PLOAD);
    nlh->nlmsg_flags = 0;
    nlh->nlmsg_type = 0;
    nlh->nlmsg_seq = 0;
    nlh->nlmsg_pid = saddr.nl_pid;
    memcpy(NLMSG_DATA(nlh), msg, strlen(msg));

	// 数据 目的 Netlink 套接字 地址
	memset(&daddr, 0, sizeof(daddr));
    daddr.nl_family = AF_NETLINK;
    daddr.nl_pid = 0; /* 0 表示数据发往 内核空间 Netlink 套接字 */
    daddr.nl_groups = 0;
    sendto(sockfd, nlh, nlh->nlmsg_len, 0, 
			(struct sockaddr *)&daddr, sizeof(struct sockaddr_nl));
    printf("send kernel: %s", msg);

	// 从内核 Netlink 套接字接收数据
	memset(&daddr, 0, sizeof(daddr));
    memset(&u_info, 0, sizeof(u_info));
    len = sizeof(struct sockaddr_nl);
    recvfrom(sockfd, &u_info, sizeof(user_msg_info), 0, 
					(struct sockaddr *)&daddr, &len);
    printf("\n");
    printf("from kernel(%u): %s\n", daddr.nl_pid, u_info.msg);

	close(sockfd);

	return 0;
}

编译执行用户空间测试程序 netlink_user_test

bash 复制代码
$ make netlink_user_test
$ ./netlink_user_test

本文示例是 内核空间用户空间 Netlink 套接字通信,事实上,内核空间的 Netlink 相互之间也可以通信,而且 Netlink 不仅支持单播通信,还支持广播通信,本文对此不做展开。

本文就示例中 内核空间用户空间 Netlink 套接字通信做简单分析。

c 复制代码
static struct proto netlink_proto = {
	.name	  = "NETLINK",
	.owner	  = THIS_MODULE,
	.obj_size = sizeof(struct netlink_sock),
};

static const struct net_proto_family netlink_family_ops = {
	.family = PF_NETLINK,
	.create = netlink_create, /* 创建 用户空间 netlink 套接字 */
	.owner	= THIS_MODULE,	/* for consistency 8) */
};

static int __init netlink_proto_init(void)
{
	int err = proto_register(&netlink_proto, 0);

	...
	nl_table = kcalloc(MAX_LINKS, sizeof(*nl_table), GFP_KERNEL);

	...
	// Netlink 协议簇 PF_NETLINK 注册
	sock_register(&netlink_family_ops);
	...
}

core_initcall(netlink_proto_init);
c 复制代码
sockfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST); /* net/socket.c */
	retval = sock_create(family, type, protocol, &sock);
		__sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
			sock = sock_alloc();
			...
			sock->type = type; // SOCK_RAW
			...
			pf = rcu_dereference(net_families[family]);
			...
			err = pf->create(net, sock, protocol, kern);
				netlink_create() /* net/netlink/af_netlink.c */
					...
					err = __netlink_create(net, sock, cb_mutex, protocol, kern);
						...
						sock->ops = &netlink_ops;
						sk = sk_alloc(net, PF_NETLINK, GFP_KERNEL, &netlink_proto, kern);
						...
						sk->sk_destruct = netlink_sock_destruct;
						sk->sk_protocol = protocol; // NETLINK_TEST
	retval = sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
c 复制代码
struct sockaddr_nl saddr;

memset(&saddr, 0, sizeof(saddr));
saddr.nl_family = AF_NETLINK;
saddr.nl_pad = 0;
saddr.nl_pid = USER_PORT;
saddr.nl_groups = 0;
bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr))
	struct socket *sock;

	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	...
	/* 拷贝用户参数到内核空间: umyaddr ==> address */
	err = move_addr_to_kernel(umyaddr, addrlen, &address);
	...
	err = sock->ops->bind(sock, (struct sockaddr *)&address, addrlen);
		netlink_bind() /* net/netlink/af_netlink.c */
			bool bound;
			...
			bound = nlk->bound;
			...
			if (!bound) { /* 还没有对 netlink 套接字进行过绑定 */
				...
				netlink_insert(sk, nladdr->nl_pid)
					...
					nlk_sk(sk)->portid = portid; /* 设定 netlink 套接字绑定的端口号 */
					...
					/*
					 * 将 @sk 插入到 netlink 协议类型 @sk->sk_protocol
					 * 对应表项 nl_table[sk->sk_protocol] 的 sock 列表。
					 * @sk->sk_protocol => {NETLINK_ROUTE, ...}
					 */
					err = __netlink_insert(table, sk);
					...
					/*
					 * 标记 netlink 套接字 @sk 已经进行了 地址 和 端口 的绑定, 
					 * 即已经调用过了 bind().
					 */
					smp_wmb();
					nlk_sk(sk)->bound = portid;
			}
c 复制代码
struct sockaddr_nl daddr;
struct nlmsghdr *nlh;

nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PLOAD)); // 分配 头部 + 内容 空间 并对齐到 4 字节

memset(nlh, 0, sizeof(struct nlmsghdr));
nlh->nlmsg_len = NLMSG_SPACE(MAX_PLOAD);
nlh->nlmsg_flags = 0;
nlh->nlmsg_type = 0;
nlh->nlmsg_seq = 0;
nlh->nlmsg_pid = saddr.nl_pid; // 指定 源端口号 USER_PORT
memcpy(NLMSG_DATA(nlh), msg, strlen(msg)); // 设置消息内容

// 设置消息发往的 目标 内核空间 netlink 套接字 地址
memset(&daddr, 0, sizeof(daddr));
daddr.nl_family = AF_NETLINK;
daddr.nl_pid = 0; // 发往内核 Netlink 套接字
daddr.nl_groups = 0;

// 往 目标内核 netlink 套接字 发消息
sendto(sockfd, nlh, nlh->nlmsg_len, 0, (struct sockaddr *)&daddr, sizeof(struct sockaddr_nl));
	struct socket *sock;
	...
	struct msghdr msg;
	...
	
	err = import_single_range(WRITE, buff, len, &iov, &msg.msg_iter);
	...
	sock = sockfd_lookup_light(fd, &err, &fput_needed);
	...
	msg.msg_name = NULL;
	msg.msg_control = NULL;
	msg.msg_controllen = 0;
	msg.msg_namelen = 0;
	err = move_addr_to_kernel(addr, addr_len, &address);
	msg.msg_name = (struct sockaddr *)&address;
	msg.msg_namelen = addr_len;
	...
	msg.msg_flags = flags;
	err = sock_sendmsg(sock, &msg);
		sock_sendmsg_nosec(sock, msg);
			int ret = sock->ops->sendmsg(sock, msg, msg_data_left(msg));
				netlink_sendmsg() /* net/netlink/af_netlink.c */
					...
					if (msg->msg_namelen) {
						...
						/*
						 * @dst_portid: 数据发往的 目标 netlink 套接字端口号
						 * @dst_group : 数据发往的 目标 netlink 套接字 组
						 */
						dst_portid = addr->nl_pid;
						dst_group = ffs(addr->nl_groups);
						...
						netlink_skb_flags |= NETLINK_SKB_DST;
					} else {
						...
					}
					...
					skb = netlink_alloc_large_skb(len, dst_group); /* 分配 skb, 准备用来接收来自 @sock 的数据 */
					...
					NETLINK_CB(skb).portid	= nlk->portid; /* 设定 skb 的数据 源端口号 */
					NETLINK_CB(skb).dst_group = dst_group; /* 设定 skb 的数据 目标组 */
					...
					NETLINK_CB(skb).flags	= netlink_skb_flags; // |= NETLINK_SKB_DST
					...
					if (memcpy_from_msg(skb_put(skb, len), msg, len)) { /* 将要发送的数据 @msg 拷贝到 @skb */
						...
					}
					...
					err = netlink_unicast(sk, skb, dst_portid, msg->msg_flags&MSG_DONTWAIT); /* netlink 单播 */
						...
					retry:	
						/* 
						 * 目标 sock (@sk) 是 内核 sock, 如情形:
						 *                               data
						 * 用户空间 netlink 套接字 @ssk ----> 内核空间 netlink 套接字 @sk
						 */
						sk = netlink_getsockbyportid(ssk, portid);
						...
						if (netlink_is_kernel(sk))
							netlink_unicast_kernel()
								nlk->netlink_rcv(skb)
									recv_msg_from_user_land()

从 内核空间 Netlink 套接字 读消息, 这分为两步:

bash 复制代码
1. 内核空间 Netlink 套接字 向 用户空间 Netlink 套接字 发送消息
2. 用户空间 Netlink 套接字 读取 内核空间 Netlink 套接字 发送的消息
c 复制代码
#define USER_PORT 66 // 用户空间 netlink 套接字 端口号

struct sk_buff *nl_skb;
struct nlmsghdr *nlh;

// 构建 skb: 容纳 消息头部 + @len 长度的数据
nl_skb = nlmsg_new(len, GFP_ATOMIC);

// 设置源消息头部: 协议类型、端口号、数据长度等
nlh = nlmsg_put(nl_skb, 0, 0, NETLINK_TEST, len, 0);
	__nlmsg_put(skb, portid, seq, type, payload, flags); // @portid == 0, 指代内核 netlink 套接字
		struct nlmsghdr *nlh;
		int size = nlmsg_msg_size(len);

		nlh = skb_put(skb, NLMSG_ALIGN(size));
		nlh->nlmsg_type = type;
		nlh->nlmsg_len = size;
		nlh->nlmsg_flags = flags;
		nlh->nlmsg_pid = portid;
		nlh->nlmsg_seq = seq;

// 填充要发送的数据内容
memcpy(nlmsg_data(nlh), buf, len);

netlink_unicast(test_nlsk, nl_skb, USER_PORT, MSG_DONTWAIT); // 将数据发送 用户空间 netlink 套接字
	...
retry:	
	/* 
	 * 用 
	 * {sock_net(@ssk), @ssk->sk_protocol, @portid}
	 * 寻找目标 netlink sock, 数据传输方向: 
	 * @ssk (源sock) -> @sk (目标sock)
	 */
	sk = netlink_getsockbyportid(ssk, portid);
	...
	/* 
	 * 目标 sock (@sk) 是 用户空间 sock, 如情形:
	 *                               data
	 * 内核空间 netlink 套接字 @ssk ----> 用户空间 netlink 套接字 @sk
	 */
	return netlink_sendskb(sk, skb);
		int len = __netlink_sendskb(sk, skb);
			...
			/* 将数据添加到 目标 @sk 的 接收 skb 队列 */
			skb_queue_tail(&sk->sk_receive_queue, skb);
			/* 唤醒 可能的、因等待读取数据 而睡眠的进程 */
			sk->sk_data_ready(sk);
				sock_def_readable()
c 复制代码
struct netlink_user_msg {
    struct nlmsghdr hdr;
    char msg[MSG_LEN];
};

struct sockaddr_nl daddr;
struct netlink_user_msg u_info;

memset(&u_info, 0, sizeof(u_info));
len = sizeof(struct sockaddr_nl);
recvfrom(sockfd, &u_info, sizeof(user_msg_info), 0, (struct sockaddr *)&daddr, &len);
	...
	err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter);
	...
	sock = sockfd_lookup_light(fd, &err, &fput_needed);

	msg.msg_control = NULL;
	msg.msg_controllen = 0;
	/* Save some cycles and don't copy the address if not needed */
	msg.msg_name = addr ? (struct sockaddr *)&address : NULL;
	/* We assume all kernel code knows the size of sockaddr_storage */
	msg.msg_namelen = 0;
	msg.msg_iocb = NULL;
	msg.msg_flags = 0;
	...
	err = sock_recvmsg(sock, &msg, flags);
		sock_recvmsg_nosec(sock, msg, flags);
			sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags);
				netlink_recvmsg() /* net/netlink/af_netlink.c */
					...
					/*
					 * 从 @sk 的接收队列 @sk->sk_receive_queue 取就绪的数据 skb.
					 * 阻塞模式下, 可因没有数据而陷入睡眠等待.
					 */
					skb = skb_recv_datagram(sk, flags, noblock, &err);
					...
					data_skb = skb; /* 返回就绪的 skb */
					...
					/* 这里告诉我们, netlink 协议数据位于 传输层(L4) 之上, 即位于 应用层(L5) */
					skb_reset_transport_header(data_skb);
					err = skb_copy_datagram_msg(data_skb, 0, msg, copied); /* 从就绪的 skb 读取数据 */
					
					if (msg->msg_name) {
						DECLARE_SOCKADDR(struct sockaddr_nl *, addr, msg->msg_name);
						addr->nl_family = AF_NETLINK;
						addr->nl_pad    = 0;
						addr->nl_pid	= NETLINK_CB(skb).portid;
						addr->nl_groups	= netlink_group_mask(NETLINK_CB(skb).dst_group);
						msg->msg_namelen = sizeof(*addr);
					}
					...

4. 参考资料

[1] Introduction to Netlink

相关推荐
Strange_Head2 分钟前
《Linux系统编程篇》vim的使用 ——基础篇
linux·vim
W21551 小时前
LINUX:懒汉单例模式线程池
linux·开发语言·c++·单例模式
优橙教育1 小时前
一文了解5G新通话技术演进与业务模型
网络·学习·5g
给生活加糖!1 小时前
5G与未来通信技术
开发语言·网络·物联网·5g·知识图谱
XH722DF1 小时前
haproxy的简单配置
网络
Holen&&Beer2 小时前
17_Shell好用工具:awk
linux·shell·shell脚本
skywalk81632 小时前
ollama编译安装@focal jammy Ubuntu @FreeBSD jail
linux·运维·ubuntu·llm·ollama
oNuoyi2 小时前
linux设置Nginx自动重启
linux·服务器·nginx
笑鸿的学习笔记2 小时前
Linux笔记之显示当前路径下文件的数量
linux·运维·笔记
闻缺陷则喜何志丹3 小时前
【超音速 专利 CN117576413A】基于全连接网络分类模型的AI涂布抓边处理方法及系统
网络·人工智能·专利·超音速·网络分类模型·抓边·涂布