Linux中Netlink简介和使用总结

张开发
2026/4/11 4:27:08 15 分钟阅读

分享文章

Linux中Netlink简介和使用总结
Netlink 是 Linux 特有的基于AF_NETLINK套接字的 IPC 机制核心用于内核空间与用户空间的双向通信支持全双工、异步与组播常被视作 ioctl 的现代化替代广泛用于网络配置、设备事件与内核子系统交互。简介核心定位与设计目标解决传统方式ioctl/procfs/sysfs同步弱、扩展性差、事件推送低效的问题。提供标准 Socket 接口用户态易用内核侧提供模块化 API支持动态扩展。支持单播 / 多播内核可主动推送事件如链路变化、设备热插拔。关键特性特性说明全双工异步用户态↔内核双向通信支持同步请求 / 异步通知消息导向基于自包含消息非字节流解析简单多协议族按功能划分如 NETLINK_ROUTE、NETLINK_KOBJECT_UEVENT隔离职责组播支持多进程订阅同一事件高效广播结构化封装消息头 nlmsghdr 属性 nlattr支持扩展与嵌套权限控制依赖能力如 CAP_NET_ADMIN提升安全性与传统方式对比方式通信方向扩展性事件推送易用性ioctl单向差固定结构弱中procfs/sysfs单向读取中轮询低效中Netlink双向强动态扩展主动异步高Socket 接口典型应用场景网络配置ip route/ip addr/ss等工具依赖 NETLINK_ROUTE设备管理udev/systemd 监听热插拔事件NETLINK_KOBJECT_UEVENT防火墙iptables/nftables 规则交互与日志NETLINK_NETFILTER容器网络CNI 插件与内核网络栈通信eBPF程序加载、事件收集与状态查询局限与注意事项仅限 Linux 平台无跨平台支持。消息长度有限受内核配置限制超大数据需分片。需正确处理权限如 CAP_NET_ADMIN与并发避免竞争。组播需管理订阅关系防止消息泛滥。总结Netlink 是 Linux 内核与用户态通信的标准、高效、可扩展方案替代 ioctl 成为现代系统管理与网络工具的底层通信基础。掌握其消息结构、协议族与 Socket 接口是开发网络工具、驱动与内核模块的关键能力。核心数据结构一、Netlink 4 大核心数据结构1.struct sockaddr_nl—— Netlink 地址结构体作用标识通信双方是谁类似 IP 端口struct sockaddr_nl { sa_family_t nl_family; // 固定写 AF_NETLINK unsigned short nl_pad; // 填充位必须填 0 __u32 nl_pid; // 端口ID进程自己填 getpid()内核固定是 0 __u32 nl_groups; // 组播掩码监听内核事件用 };关键说明nl_pid用户态进程填自己的 PID内核永远是 0nl_groups不监听事件填 0监听内核广播事件填组播号如网卡、热插拔一句话记住sockaddr_nl Netlink 的 “地址”用户nl_pid 自己PID内核nl_pid 02.struct nlmsghdr—— Netlink 消息头最重要作用所有 Netlink 消息的头部必须有没有无法通信struct nlmsghdr { __u32 nlmsg_len; // 消息总长度 头部 数据体 __u16 nlmsg_type; // 消息类型如获取路由、获取网卡 __u16 nlmsg_flags; // 标志位请求、应答、创建、删除 __u32 nlmsg_seq; // 序列号用来匹配请求和应答 __u32 nlmsg_pid; // 发送方的 PID谁发的填谁 };关键字段解释nlmsg_len必须包含头部本身长度用宏NLMSG_LENGTH(len)计算nlmsg_type内核预定义RTM_NEWADDR、RTM_DELROUTE、RTM_GETLINK…自定义协议可以自己定义类型nlmsg_flagsNLM_F_REQUEST用户→内核请求NLM_F_ACK要求内核回复 ACKNLM_F_CREATE创建NLM_F_EXCL不存在才创建nlmsg_seq自增数字用来对应 “请求 ↔ 应答”nlmsg_pid发送者 PID一句话记住nlmsghdr 每个 Netlink 消息的 “身份证”没有它内核不知道你要干嘛。3.struct nlattr—— Netlink 属性结构可扩展作用携带真正的数据可嵌套、可扩展struct nlattr { __u16 nla_len; // 属性总长度 头部 数据 __u16 nla_type; // 属性类型如 IP地址、子网掩码、网卡名 };特点像 TLV 结构Type Length Value可以嵌套内核自动解析非常灵活所有网络配置IP、MAC、路由、网卡都用它传常用宏NLMSG_DATA(nlh) → 获取数据起始地址 NLA_DATA(nla) → 获取属性数据 NLA_PAYLOAD(nla) → 获取数据长度一句话记住nlattr Netlink 消息的 “数据载体”4.struct nlmsghdr 数据 nlattr整体结构一个完整 Netlink 消息长这样------------------- | struct nlmsghdr | 消息头必须 ------------------- | 消息体 | 如 ifaddrmsg、ifinfomsg 等 ------------------- | struct nlattr | 属性1IP ------------------- | struct nlattr | 属性2掩码 -------------------内核只认这种格式。二、最常用的辅助宏必须背这些宏是操作上面 4 个结构的快捷键// 计算消息总长度 NLMSG_LENGTH(len) // 取消息头后面的数据指针 NLMSG_DATA(nlh) // 判断消息是否合法 NLMSG_OK(nlh, len) // 跳到下一条消息 NLMSG_NEXT(nlh, len)三、4 大结构关系总结超级清晰sockaddr_nl我是谁、发给谁nlmsghdr消息头控制消息类型、序列号、标志nlattr携带真正的数据IP、MAC、路由等辅助宏简化解析、遍历、取值它们合在一起 完整的 Netlink 通信协议格式组装 Netlink 消息用【自定义数据】教你组装 Netlink 消息我们自己定义一个最简单的数据结构// 这是我们【自己定义】的消息体 // 不是内核的不是 ifinfomsg struct my_data { int id; char name[32]; };我们要发送的 Netlink 消息长这样------------------ | struct nlmsghdr | 消息头必须 ------------------ | struct my_data | 我们自己的数据自定义 ------------------这就是最干净、最简单的 Netlink 消息4. 一步一步组装超级简单第 1 步计算总长度// 消息总长度 头长度 我们自定义数据长度 int total_len NLMSG_LENGTH( sizeof(struct my_data) );第 2 步申请内存struct nlmsghdr *nlh malloc(total_len); memset(nlh, 0, total_len);第 3 步填消息头固定 5 个字段nlh-nlmsg_len total_len; // 总长度 nlh-nlmsg_type 100; // 自定义消息类型随便写 nlh-nlmsg_flags NLM_F_REQUEST;// 表示这是请求 nlh-nlmsg_seq 1; // 序列号 nlh-nlmsg_pid getpid(); // 自己的PID第 4 步填【自定义数据】// 获取数据指针 → 头后面就是我们的自定义数据 struct my_data *data NLMSG_DATA(nlh); // 随便填我们自己的数据>struct nlmsghdr *nlh不代表它只指向一个头而是指向一整块内存的起始地址这块内存 头 数据。6. 你以后只要记住这个规则malloc申请头 数据的总大小用struct nlmsghdr *指向这块内存开头填好头部字段用NLMSG_DATA(nlh)获取后面的数据指针发送整块内存超级简化记忆指针只管开头内存长度管全部后面的数据靠偏移你现在是不是彻底懂了不懂我再用画图给你讲无需提前建立连接需要和常规socket一样先发起连接吗完全不需要像 TCP 那样 connect、三次握手、建立连接。Netlink 是无连接、面向数据报的通信方式更像UDP而不是 TCP。1. 核心结论Netlink 是无连接connectionless套接字没有 “客户端 / 服务器” 之分不用connect()不用监听listen()/accept()谁都可以随时直接sendto/recvfrom发消息2. 那它到底要做什么只需要两步创建 socketsocket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);bind 绑定自己的地址主要是绑定nl_pid一般用自己的 pid内核那边固定是pid0不需要绑定对方地址之后就可以用户态 → 内核直接sendto发给pid0内核 → 用户态直接发给对应进程的nl_pid3. 和 TCP/UDP 对比一下更清楚方式连接是否需要 connect收发方式TCP面向连接必须 connect流式 read/writeUDP无连接可选数据报 sendto/recvfromNetlink无连接不需要数据报 sendto/recvfrom4. 那 “双向通信” 怎么实现非常简单用户态发sendto到内核pid0收recvfrom等着内核主动发消息内核可以主动发消息给用户态nl_pid也可以等用户态发消息过来再回复全程没有 “建立连接” 这一步。5. 一句话记忆Netlink 内核版的 UDP无连接、全双工、谁都能主动发。使用流程用户态-发送Netlink 用户态 发送数据 完整步骤超清晰、不绕弯我完全按步骤拆解只讲发送不讲接收每一步做什么、为什么、怎么写全部讲明白。你跟着走就能100% 成功发送Netlink 消息。前提必须记住Netlink 像UDP无连接不用 connect必须bind 自己否则内核不知道你是谁发给内核目标pid 0消息格式 消息头 (nlmsghdr) 自定义数据发送数据一共 7 步第 1 步创建 Netlink Socketint fd socket( AF_NETLINK, // 固定Netlink 协议 SOCK_RAW, // 固定原始报文 NETLINK_USERSOCK // 用户自定义协议最简单 );作用创建一个可以和内核通信的套接字。第 2 步绑定自己的地址必须做struct sockaddr_nl local; memset(local, 0, sizeof(local)); local.nl_family AF_NETLINK; local.nl_pid getpid(); // 用自己的进程ID local.nl_groups 0; // 不加入组播 bind(fd, (struct sockaddr*)local, sizeof(local));作用告诉内核 “我是谁”内核才能回复你。第 3 步设置目标地址发给内核struct sockaddr_nl kernel; memset(kernel, 0, sizeof(kernel)); kernel.nl_family AF_NETLINK; kernel.nl_pid 0; // 内核固定是 0 kernel.nl_groups 0;作用指定消息发给内核。第 4 步定义你要发送的数据自定义// 你自己随便定义 struct my_msg { int id; char name[20]; };作用这是你真正要发的数据不是内核规定的。第 5 步【核心】组装 Netlink 消息5.1 计算消息总长度int total_len NLMSG_LENGTH( sizeof(struct my_msg) );含义消息总长度 消息头 你的数据5.2 申请一整块内存struct nlmsghdr *nlh malloc(total_len); memset(nlh, 0, total_len);这块内存布局---------------------------------- | nlmsghdr 头 | 你的数据 my_msg | ----------------------------------5.3 填写消息头必须填nlh-nlmsg_len total_len; // 总长度 nlh-nlmsg_type 100; // 自定义类型随便写 nlh-nlmsg_flags NLM_F_REQUEST;// 表示这是请求 nlh-nlmsg_seq 1; // 序列号 nlh-nlmsg_pid getpid(); // 你的ID5.4 填写你自己的数据// 拿到数据部分的指针关键宏 struct my_msg *data NLMSG_DATA(nlh); // 赋值>使用流程内核-接收现在只讲内核态如何接收用户态发来的 Netlink 消息一步一步讲清楚和你前面学的用户态收发完全对应。一、内核接收的核心逻辑内核不主动调用 recv而是靠回调函数用户态发消息 → 内核收到后自动调用你注册的回调消息放在struct sk_buff *skb里从 skb 里解析出nlmsghdr和数据一句话用户发 → 内核回调被触发 → 解析 skb二、内核接收完整流程共 5 步步骤 1创建内核 netlink 套接字并注册回调这是接收的前提必须在模块初始化时做。#include linux/module.h #include linux/netlink.h #include linux/skbuff.h #include linux/kernel.h static struct sock *nl_sk NULL; // 【核心】接收回调函数 // 用户一发消息内核就调用这个函数 static void nl_data_ready(struct sk_buff *skb) { // 所有接收逻辑都在这里 } static int __init nl_recv_init(void) { struct netlink_kernel_cfg cfg { .input nl_data_ready, // 绑定回调 }; // 创建 netlink 套接字 nl_sk netlink_kernel_create(init_net, NETLINK_USERSOCK, cfg); if (!nl_sk) { printk(netlink create fail\n); return -ENOMEM; } return 0; } static void __exit nl_recv_exit(void) { netlink_kernel_release(nl_sk); } module_init(nl_recv_init); module_exit(nl_recv_exit); MODULE_LICENSE(GPL);步骤 2在回调里获取 nlmsghdr用户发来的消息格式nlmsghdr 自定义数据内核里这样拿到消息头static void nl_data_ready(struct sk_buff *skb) { // 1. 从 skb 中取出 netlink 消息头 struct nlmsghdr *nlh nlmsg_hdr(skb); // 2. 检查消息是否合法必须做 if (!nlmsg_ok(nlh, skb-len)) { printk(bad msg\n); return; } }步骤 3获取发送者的 portid关键这就是用户 bind 的那个 nl_pid内核自动拿到不用配置。u32 user_portid nlh-nlmsg_pid; printk(recv from user portid: %u\n, user_portid);以后内核要回复用户就用这个user_portid。步骤 4获取用户发来的数据和用户态完全一样用宏NLMSG_DATA(nlh)拿到数据体。假设用户发的是自定义结构struct my_data { int id; char name[32]; };内核解析// 获取数据指针 struct my_data *data NLMSG_DATA(nlh); // 直接读取 printk(id: %d\n, data-id); printk(name: %s\n, data-name);步骤 5可以选择立即回复用户内核收到后可以直接在回调里回复一条消息给用户// 分配 skb int size NLMSG_LENGTH(sizeof(struct my_data)); struct sk_buff *skb_resp alloc_skb(size, GFP_KERNEL); // 填充消息头 struct nlmsghdr *nlh_resp nlmsg_put(skb_resp, 0, 0, sizeof(struct my_data), 0); // 填充数据 struct my_data *resp_data NLMSG_DATA(nlh_resp); resp_data-id 888; strcpy(resp_data-name, kernel received); // 发送给用户 netlink_unicast(nl_sk, skb_resp, user_portid, MSG_DONTWAIT);三、极简总结内核接收1. 创建 sock 时注册 input 回调 2. 用户发消息 → 回调自动触发 3. 从 skb 获取 nlmsghdr 4. 用 NLMSG_DATA 取数据 5. 拿到 user_portid 用于回复四、和用户态接收对比用户态recv()阻塞等消息内核态回调被主动调用不用阻塞这是唯一区别消息结构、解析方式完全一样。Netlink协议类型一次性彻底讲透Netlink 协议类型Family你现在问的是NETLINK_USERSOCK、NETLINK_ROUTE、NETLINK_GENERIC... 这些到底是什么有哪些怎么选怎么用我用最通俗、最工程化的方式讲清楚保证你看完再也不迷糊。一、先讲本质它们是什么Netlink 协议类型 内核里的 “服务端口号”就像80 端口 → HTTP22 端口 → SSH在内核里NETLINK_ROUTE→ 网络配置服务ip addr/ip linkNETLINK_USERSOCK→ 用户自定义通信服务NETLINK_GENERIC→ 通用可扩展服务NETLINK_KOBJECT_UEVENT→ 设备热插拔服务你选哪个类型就等于连接内核里哪个子系统。二、有哪些常用类型必须记住的 5 个我只讲实际开发会用到的不讲冷门废的。1. NETLINK_USERSOCK你现在用的用途用户态 ↔ 内核模块 自定义双向通信最简单、最自由你自己定义消息格式适合自己写内核模块通信没有固定消息结构随便发适合你自己写内核模块 自己写应用通信2. NETLINK_ROUTE最常用系统协议用途网络配置获取网卡、IP、路由、ARP命令ip addr、ip link、ip route底层就是它消息格式固定ifinfomsg、ifaddrmsg...适合写网络管理工具3. NETLINK_GENERIC推荐现代自定义用用途更规范、更安全的自定义通信比 USERSOCK 高级支持动态注册 “子命令”不冲突、可扩展官方推荐替代 USERSOCK适合产品级内核模块通信4. NETLINK_KOBJECT_UEVENT用途设备热插拔U 盘插入、拔出摄像头、声卡加载udev 就是用它适合设备监控5. NETLINK_NETFILTER用途防火墙、iptables、nftables三、它们的核心区别一张表看懂类型用途消息格式难度适合场景NETLINK_USERSOCK自定义通信完全自定义最简单学习、自己写内核模块NETLINK_ROUTE网络配置内核固定中等网络工具NETLINK_GENERIC规范自定义自定义 规范中等产品开发NETLINK_UEVENT热插拔内核固定简单设备事件NETLINK_NETFILTER防火墙内核固定复杂安全类工具四、最重要的问题我该用哪个 你现在学习、写 demo → 用 NETLINK_USERSOCK最简单不用遵守内核消息格式随便定义结构体收发最适合理解原理 以后写产品 → 用 NETLINK_GENERIC 写网络工具 → 用 NETLINK_ROUTE 监听热插拔 → 用 NETLINK_UEVENT五、怎么使用一句话创建 socket 时第三个参数填类型即可socket(AF_NETLINK, SOCK_RAW, 类型);例子// 自定义通信 socket(AF_NETLINK, SOCK_RAW, NETLINK_USERSOCK); // 网络配置 socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); // 热插拔 socket(AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT);六、最关键的总结背会Netlink 类型 内核服务号USERSOCK自定义通信最简单学习首选ROUTE网络配置ip 命令底层GENERIC官方推荐自定义通信比 USERSOCK 高级UEVENT热插拔事件使用方法socket () 第三个参数传入socket类型SOCK_RAW又是啥还有哪些选项一次性讲透Netlink 里的 SOCK_RAW 到底是什么有哪些选项你现在卡在创建 socket 时第二个参数SOCK_RAW到底啥意思能不能换我用最简单、最准确、不绕弯的方式讲清楚。一、先给结论最重要在 Netlink 中几乎永远只用 SOCK_RAW其他选项基本不用、也不推荐用。二、SOCK_RAW是什么它是socket 类型表示原始报文套接字Raw Socket意思是你自己组装完整的消息头nlmsghdr 数据内核不帮你处理直接传给对方。对应 Netlink你自己填nlmsghdr你自己定义数据格式内核只负责收发不解析、不修改你的消息完全自由这就是为什么我们前面写代码都用socket(AF_NETLINK, SOCK_RAW, NETLINK_USERSOCK);三、socket 类型有哪些Linux 通用标准 socket 有 4 种类型SOCK_RAW→ 原始报文自己组头SOCK_DGRAM→ 数据报UDP 模式SOCK_STREAM→ 流式TCP 模式SOCK_SEQPACKET→ 有序数据包四、这些类型在 Netlink 中 能使用吗答案Netlink 不支持 SOCK_STREAMTCP 模式Netlink 支持 SOCK_DGRAMNetlink 支持 SOCK_RAWNetlink 支持 SOCK_SEQPACKET五、它们在 Netlink 中的区别超级简单1. SOCK_RAW最常用、推荐你自己提供完整 nlmsghdr 消息头内核不处理、不填充完全可控自定义通信首选我们前面所有代码都用这个。2. SOCK_DGRAM数据报内核自动帮你填充 nlmsghdr 部分字段你不用自己写消息头只发数据简化使用但灵活性降低3. SOCK_SEQPACKET保证顺序保留消息边界Netlink 很少用4. SOCK_STREAMTCPNetlink 不支持因为 Netlink 是无连接的像 UDP六、最简单总结背会Netlink 中99% 的情况用 SOCK_RAW你自己组 nlmsghdr最灵活内核不干涉你的消息格式七、你最该记住的一句话写 Netlink 自定义通信 → 固定写 SOCK_RAWsocket(AF_NETLINK, SOCK_RAW, NETLINK_USERSOCK);不需要改不需要换这就是标准写法。

更多文章