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](https://www.kernel.org/doc/html/next/userspace-api/netlink/intro.html)

相关推荐
深夜情感老师20 分钟前
centos离线安装ssh
linux·centos·ssh
EasyDSS22 分钟前
视频监控从安装到优化的技术指南,视频汇聚系统EasyCVR智能安防系统构建之道
大数据·网络·网络协议·音视频
rufeike28 分钟前
UDP协议理解
网络·网络协议·udp
江理不变情1 小时前
海思ISP调试记录
网络·接口隔离原则
世界尽头与你2 小时前
【安全扫描器原理】网络扫描算法
网络·安全
GKoSon2 小时前
加入RPC shell指令 温箱长时间监控
网络·网络协议·rpc
hgdlip3 小时前
关闭IP属地显示会影响账号的正常使用吗
网络·网络协议·tcp/ip·ip属地
Zz_waiting.4 小时前
网络原理 - 7(TCP - 4)
网络·网络协议·tcp/ip
RECRUITGUY4 小时前
用交换机连接两台电脑,电脑A读取/写电脑B的数据
服务器·网络·负载均衡
zheshiyangyang4 小时前
HTTP相关
网络·网络协议·http