Netlink 核心通信模型
Netlink = 全双工、面向消息、内核 / 用户双向通信支持:
- 单播(1 对 1)
- 多播(1 对多)
- 双向主动发送(内核可主动发,用户也可主动发)
- 多个协议隔离通道
- 多个 socket 并行
5 种标准通信模式
1.用户 → 内核 单播(最常用)
用户主动发消息给内核。
- 用户:目标 portid = 0
- 内核:收到消息,知道是谁发的(portid)
适用:命令下发、配置、查询。
dest_addr.nl_pid = 0; // 发往内核 sendmsg(fd, &msg, 0);2.内核 → 用户 单播(最常用)
内核主动发消息给某个用户 socket。
- 内核必须知道用户 portid(从接收消息里拿)
- 精准发给某一个 socket
u32 portid = NETLINK_CB(skb).portid; netlink_unicast(nl_sk, skb, portid, MSG_DONTWAIT);3.双向请求应答模式(标准用法)
用户发请求 → 内核回响应这是 90% 项目的真实用法。
流程:
- 用户 send → 内核
- 内核 receive → 获取 portid
- 内核 unicast → 回复用户
- 用户 receive → 拿到结果
全双工,可靠通信。
4.内核 → 多用户 多播(事件 / 通知)
内核向一组用户广播消息(比如热插拔、状态变化)。
- 内核:
netlink_broadcast() - 用户:bind 时加入多播组
nl_groups = 掩码
netlink_broadcast(sk, skb, 0, group, GFP_KERNEL);适用:内核事件通知、uevent、状态同步。
5.用户 ↔ 用户 通信(少用)
Netlink 允许用户进程之间直接通信,不经过内核。
- A 用户:portid=1000
- B 用户:portid=2000
- A 发消息目标 portid=2000
内核只做转发,不处理。
内核主动推送模式
✔ Netlink 支持内核主动发消息给用户
不需要用户先发送!
这是 Netlink 比 ioctl /proc 强 100 倍的地方。
内核可以:
- 定时发消息
- 中断里发消息
- 硬件事件触发发消息
- 多播给所有用户
唯一寻址规则 =协议号 + portid
- 协议号:区分哪个内核模块 / 通道
- portid:区分哪个用户 socket
只要协议号不同,哪怕用户态所有 socket 都用同一个 portid = getpid ()
内核会精准投递,完全不会乱!
两个内核模块 = 两个独立 Netlink 服务
- 模块 A:使用协议号
NETLINK_TEST1 = 17 - 模块 B:使用协议号
NETLINK_TEST2 = 18
协议号必须不同,内核才能区分两个 Netlink 通道。
同一个用户进程 = 两个独立客户端 Socket
- Socket 1:连接模块 A(协议 17),portid =
getpid() - Socket 2:连接模块 B(协议 18),portid =
getpid() + 1
通信规则
- 内核 A ↔ 用户 Socket1
- 内核 B ↔ 用户 Socket2
完全隔离、互不干扰
不同 Netlink 协议号,为什么可以用同一个 pid?
Netlink 消息路由公式(内核内部就是这么干的):
匹配 = (协议号) + (目标portid)- 协议 17 → 只给内核模块 A
- 协议 18 → 只给内核模块 B
- portid 相同 = 同一个用户 socket 接收
- 两条通道物理隔离
所以:同一个用户进程,N 个不同协议的 Netlink Socket,全部可以 bind 同一个 portid(getpid ())!
推荐用法
用户态:同一个进程 + 同一个 pid + 不同协议
// socket A:协议17,portid = getpid() int fd1 = socket(AF_NETLINK, SOCK_RAW, 17); bind(fd1, pid = getpid()); // socket B:协议18,portid = getpid() int fd2 = socket(AF_NETLINK, SOCK_RAW, 18); bind(fd2, pid = getpid()); // same pid!完全没问题内核态:两个模块,不同协议
模块A:协议17 模块B:协议18通信结果
- 用户 fd1 <-> 内核 A(协议 17)
- 用户 fd2 <-> 内核 B(协议 18)
互不干扰,portid 相同也完全安全!
关键要点
两个内核模块靠什么区分?
靠不同的 Netlink 协议号(17、18)
- 内核创建 socket 时指定协议号
- 用户创建 socket 时也指定相同协议
- 协议不同 → 完全两条通道
同一个用户进程怎么开两个 Socket?
靠不同 portid(nl_pid)
- Socket1:
pid - Socket2:
pid + 1
内核通过portid知道消息要发给哪个 Socket
内核回复消息时怎么精准回对 Socket?
内核从skb->portid拿到用户端 portid,直接用:
netlink_unicast(nl_sk, skb, portid, MSG_DONTWAIT);最实用口诀
内核地址永远 0, 用户地址看 portid, 不同协议隔离走, 同pid也不冲突, 单播精准点对点, 多播广播一组人, 内核能主动推送, 双向全双工最稳。完整代码案例
1. 内核模块 A(netlink_mod_a.c)
#include <linux/module.h> #include <linux/netlink.h> #include <linux/skbuff.h> #define NETLINK_TEST1 17 // 模块A专用协议 static struct sock *nl_sk; // 接收用户消息 static void nl_a_rcv(struct sk_buff *skb) { struct nlmsghdr *nlh = nlmsg_hdr(skb); u32 portid = NETLINK_CB(skb).portid; printk("模块A收到:portid=%u, 消息=%s\n", portid, (char*)NLMSG_DATA(nlh)); } static int __init nl_a_init(void) { struct netlink_kernel_cfg cfg = { .input = nl_a_rcv }; nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST1, &cfg); printk("模块A Netlink 加载成功\n"); return 0; } static void __exit nl_a_exit(void) { netlink_kernel_release(nl_sk); printk("模块A 卸载\n"); } module_init(nl_a_init); module_exit(nl_a_exit); MODULE_LICENSE("GPL");2. 内核模块 B(netlink_mod_b.c)
#include <linux/module.h> #include <linux/netlink.h> #include <linux/skbuff.h> #define NETLINK_TEST2 18 // 模块B专用协议 static struct sock *nl_sk; static void nl_b_rcv(struct sk_buff *skb) { struct nlmsghdr *nlh = nlmsg_hdr(skb); u32 portid = NETLINK_CB(skb).portid; printk("模块B收到:portid=%u, 消息=%s\n", portid, (char*)NLMSG_DATA(nlh)); } static int __init nl_b_init(void) { struct netlink_kernel_cfg cfg = { .input = nl_b_rcv }; nl_sk = netlink_kernel_create(&init_net, NETLINK_TEST2, &cfg); printk("模块B Netlink 加载成功\n"); return 0; } static void __exit nl_b_exit(void) { netlink_kernel_release(nl_sk); printk("模块B 卸载\n"); } module_init(nl_b_init); module_exit(nl_b_exit); MODULE_LICENSE("GPL");3. 用户态测试程序(单进程双 Socket)
#include <stdio.h> #include <sys/socket.h> #include <linux/netlink.h> #include <string.h> #include <unistd.h> #include <getopt.h> #define NETLINK_TEST1 17 #define NETLINK_TEST2 18 // 创建一个连接指定内核协议的 netlink socket int create_socket(int proto, __u32 portid) { int fd = socket(AF_NETLINK, SOCK_RAW, proto); struct sockaddr_nl sa = {0}; sa.nl_family = AF_NETLINK; sa.nl_pid = portid; bind(fd, (struct sockaddr*)&sa, sizeof(sa)); return fd; } // 发送消息到内核 void send_msg(int fd, int proto, char *msg) { struct sockaddr_nl dest = {0}; dest.nl_family = AF_NETLINK; dest.nl_pid = 0; // 内核 struct nlmsghdr *nlh; struct iovec iov; struct msghdr msg_hdr = {0}; nlh = malloc(NLMSG_SPACE(strlen(msg)+1)); nlh->nlmsg_len = NLMSG_SPACE(strlen(msg)+1); nlh->nlmsg_pid = getpid(); memcpy(NLMSG_DATA(nlh), msg, strlen(msg)+1); iov.iov_base = nlh; iov.iov_len = nlh->nlmsg_len; msg_hdr.msg_name = &dest; msg_hdr.msg_namelen = sizeof(dest); msg_hdr.msg_iov = &iov; msg_hdr.msg_iovlen = 1; sendmsg(fd, &msg_hdr, 0); free(nlh); } int main() { pid_t pid = getpid(); int fd1 = create_socket(NETLINK_TEST1, pid); int fd2 = create_socket(NETLINK_TEST2, pid+1); send_msg(fd1, NETLINK_TEST1, "Hello 模块A"); send_msg(fd2, NETLINK_TEST2, "Hello 模块B"); close(fd1); close(fd2); return 0; }