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
- 内核:永远是 0
- nl_groups
- 不监听事件:填 0
- 监听内核广播事件:填组播号(如网卡、热插拔)
一句话记住
sockaddr_nl = Netlink 的 "地址" 用户:
nl_pid = 自己PID内核:nl_pid = 0
2.
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_flags
NLM_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 | 属性1(IP) +-------------------+ | 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); // 随便填我们自己的数据 data->id = 123; strcpy(data->name, "Hello Netlink");第 5 步:发送
sendto(fd, nlh, nlh->nlmsg_len, 0, &kernel_addr, sizeof(kernel_addr));
5. 你看!就这么简单!
Netlink 消息组装 =
- 开内存
- 填头
- 填你自己的数据
- 发送
struct nlmsghdr *nlh = malloc(total_len);这一句,struct nlmsghdr *nlh也不包含消息体,为啥返回这个类型?我来把这个最让人困惑的指针问题彻底讲透!
保证你看完100% 明白,再也不晕。
先直接回答你的疑问:
为什么 nlh 是 nlmsghdr 类型指针,却能装下消息体?
因为:这是 C 语言的指针技巧 + 内存连续布局,不是结构体包含关系!
1. 核心真相
malloc(total_len)申请了一整块连续内存 ,大小是:消息头 (nlmsghdr) + 你的自定义数据体
内存布局: <------------------ 整块内存 total_len ------------------> +---------------------+------------------------------------+ | struct nlmsghdr | 你的自定义数据 (my_data) | +---------------------+------------------------------------+ ↑ 这个指针指向这里,类型是 struct nlmsghdr *类型
struct nlmsghdr *nlh只是告诉你:这块内存的开头部分,是消息头。
后面的消息体,是紧跟在头后面的内存,不是在结构体里!
2. 为什么不定义一个 "头 + 数据" 的结构体?
比如你会想,为什么不这样写:
// 理论上可以,但内核不这么干! struct my_msg { struct nlmsghdr hdr; struct my_data data; };原因:Netlink 消息是变长、可扩展的,不能写死结构体!
- 有时候数据体是 10 字节
- 有时候是 100 字节
- 有时候后面还带一堆属性 nlattr
所以不能用固定结构体 ,只能用:头指针 + 动态计算偏移 的方式。
3. 那怎么拿到后面的消息体?
用宏 NLMSG_DATA(nlh)!
// 宏的本质:指针偏移 (struct my_data *) ( (char *)nlh + NLMSG_HDRLEN );意思:从消息头指针开始,往后跳过头部长度,剩下的就是数据体!
4. 最直白的比喻
你把内存当成一根管子:
管子总长度:total_len 前段固定放:消息头 nlmsghdr 后段放:你的数据
nlh是指向管子开头的指针- 你通过它能访问头部
- 也能通过偏移访问后面的数据
- 但它的类型只代表开头那段的类型
5. 一句话总结(最重要)
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. 那它到底要做什么?
只需要两步:
创建 socket
socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);bind 绑定自己的地址
- 主要是绑定:
nl_pid(一般用自己的 pid)- 内核那边固定是
pid=0- 不需要绑定对方地址
之后就可以:
- 用户态 → 内核:直接
sendto发给pid=0- 内核 → 用户态:直接发给对应进程的
nl_pid
3. 和 TCP/UDP 对比一下更清楚
方式 连接 是否需要 connect 收发方式 TCP 面向连接 必须 connect 流式 read/write UDP 无连接 可选 数据报 sendto/recvfrom Netlink 无连接 不需要 数据报 sendto/recvfrom
4. 那 "双向通信" 怎么实现?
非常简单:
用户态:
- 发:
sendto到内核(pid=0)- 收:
recvfrom等着内核主动发消息内核:
- 可以主动发消息给用户态
nl_pid- 也可以等用户态发消息过来再回复
全程没有 "建立连接" 这一步。
5. 一句话记忆
Netlink = 内核版的 UDP,无连接、全双工、谁都能主动发。
使用流程(用户态-发送)
Netlink 用户态 发送数据 完整步骤(超清晰、不绕弯)
我完全按步骤拆解 ,只讲发送,不讲接收,每一步做什么、为什么、怎么写,全部讲明白。你跟着走,就能100% 成功发送 Netlink 消息。
前提(必须记住)
- Netlink 像 UDP,无连接,不用 connect
- 必须 bind 自己,否则内核不知道你是谁
- 发给内核:目标 pid = 0
- 消息格式 = 消息头 (nlmsghdr) + 自定义数据
发送数据一共 7 步
第 1 步:创建 Netlink Socket
int 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); // 赋值 data->id = 10; strcpy(data->name, "Hello Netlink");
第 6 步:发送消息(sendto)
sendto( fd, // socket nlh, // 消息指针(指向头+数据) nlh->nlmsg_len, // 总长度 0, (struct sockaddr*)&kernel, // 目标:内核 sizeof(kernel) );发送完成!
第 7 步:释放内存 + 关闭 fd
free(nlh); close(fd);
最关键的 2 句话(一定要懂)
nlh只是指向内存开头,不是只存头 内存里是:头 + 数据 连在一起
NLMSG_DATA(nlh)就是取头后面的数据本质就是指针偏移。
7 步极简总结
1. 创建 socket 2. bind 自己PID 3. 目标设为内核 0 4. 定义你自己的数据 5. 申请内存:头+数据 6. 填头 → 填数据 7. sendto 发送
我可以下一步再给你讲:用户态如何接收内核回复(一步一步)
使用流程(用户态-接收)
Netlink 用户态 ------ 接收数据(完整步骤)
前提:
- 你已经有了一个 netlink socket(已创建、已 bind)
- 内核那边会给你回消息(或者主动推送事件)
接收一共 6 步,非常固定,背下来就能用。
步骤 1:准备一块足够大的接收缓冲区
Netlink 消息是整包来的,一般开个 4k/8k 就够用:
char recv_buf[8192]; // 接收缓冲区
步骤 2:调用 recvfrom /recv 阻塞等待消息
可以用
recv或recvfrom,最简单用recv:
int msg_len = recv( fd, // 你的 netlink socket recv_buf, // 接收缓冲区 sizeof(recv_buf), // 缓冲区大小 0 // 标志位 );
- 内核没消息时,这里会阻塞
- 收到消息后,
msg_len是实际收到的总字节数
步骤 3:把收到的数据强转成 struct nlmsghdr*
数据开头就是消息头,所以直接转:
struct nlmsghdr *nlh_recv = (struct nlmsghdr *)recv_buf;此时内存布局:
recv_buf: +----------------+------------------+ | nlmsghdr | 数据体 | +----------------+------------------+ ↑ nlh_recv 指向这里
步骤 4:检查消息是否合法(必须做)
用内核提供的宏 NLMSG_OK:
if (!NLMSG_OK(nlh_recv, msg_len)) { printf("消息格式错误\n"); return; }作用:防止截断、错误包、越界访问。
步骤 5:取出消息体(关键一步)
用宏 NLMSG_DATA(nlh_recv) 获取头后面的数据:
// 这里的 struct my_msg 要和发送时自定义的结构体一致 struct my_msg *data_recv = (struct my_msg *)NLMSG_DATA(nlh_recv);然后你就可以正常访问:
printf("id = %d\n", data_recv->id); printf("name = %s\n", data_recv->name);
步骤 6:判断消息类型(可选但常用)
根据
nlmsg_type做不同处理:
switch (nlh_recv->nlmsg_type) { case NLMSG_ERROR: // 内核返回错误 break; case 100: // 我们自定义的类型 break; // ... 其他类型 }
接收流程极简总结
1. 开一块大缓冲区 2. recv 阻塞收消息 3. 转成 struct nlmsghdr* 4. NLMSG_OK 检查合法性 5. NLMSG_DATA 取出自定义数据 6. 根据类型解析使用
使用流程(内核-发送)
在内核模块里,怎么通过 Netlink 把消息发给用户态。完全按步骤讲,不讲虚的,和你前面用户态流程对应起来。
一、内核态 Netlink 发送的核心前提
- 内核里不是用 socket,而是用
struct sock *- 内核发消息要用 API:
netlink_unicast():发给某个特定用户态进程netlink_broadcast():群发(组播)- 要发消息,必须先创建 Netlink 内核套接字
二、内核发送 Netlink 消息完整步骤(共 6 步)
步骤 1:创建内核 Netlink 套接字
这一步在模块初始化时做一次就行。
#include <linux/module.h> #include <linux/netlink.h> #include <linux/skbuff.h> // 全局保存 netlink sock static struct sock *nl_sk = NULL; // 收到用户态消息时的回调(可选) static void nl_recv_callback(struct sk_buff *skb) { // 这里可以处理用户态发来的数据 } static int __init my_nl_init(void) { struct netlink_kernel_cfg cfg = { .input = nl_recv_callback, // 接收用户态消息的回调 }; // 创建 NETLINK_USERSOCK 类型的内核 socket nl_sk = netlink_kernel_create(&init_net, NETLINK_USERSOCK, &cfg); if (!nl_sk) { printk("netlink_kernel_create failed\n"); return -ENOMEM; } return 0; } static void __exit my_nl_exit(void) { netlink_kernel_release(nl_sk); } module_init(my_nl_init); module_exit(my_nl_exit); MODULE_LICENSE("GPL");
步骤 2:分配一个 sk_buff(内核发包必备)
用户态是 malloc 一段内存放
nlmsghdr + 数据内核里是 alloc_skb 分配sk_buff。
// 自定义数据结构 struct my_data { int id; char name[32]; }; // 消息总长度:头 + 数据 int total_size = NLMSG_LENGTH(sizeof(struct my_data)); // 分配 skb struct sk_buff *skb = alloc_skb(total_size, GFP_KERNEL);
步骤 3:在 skb 中放入 nlmsghdr 头
和用户态完全一样:先填消息头。
// 获取 nlmsghdr 指针 struct nlmsghdr *nlh = nlmsg_put( skb, // 放到哪个 skb 0, // 发送者 pid:内核固定 0 0, // 消息类型(自定义) sizeof(struct my_data), // 数据体长度 0); // flags // 可以再补充一些字段 nlh->nlmsg_seq = 123;
nlmsg_put会自动帮你填好头部并把 skb 指针后移。
步骤 4:填充自定义数据
和用户态的
NLMSG_DATA一模一样。
// 拿到数据区指针 struct my_data *data = NLMSG_DATA(nlh); // 填数据 data->id = 666; strcpy(data->name, "msg from kernel");此时 skb 内部布局:
nlmsghdr + my_data
步骤 5:调用 netlink_unicast 发给用户态
这就是内核发送的核心 API。
// 目标用户态进程的 nl_pid(一般是用户进程的 PID) u32 dest_pid = 12345; // 你要知道发给谁 netlink_unicast( nl_sk, // 我们创建的内核 sock skb, // 装好消息的 skb dest_pid, // 目标用户态 pid MSG_DONTWAIT);发送完成!内核会把这条消息直接送到用户态的 netlink socket。
步骤 6:发送失败要释放 skb
netlink_unicast成功会自动释放 skb,失败不会,所以要判断:
int ret = netlink_unicast(nl_sk, skb, dest_pid, MSG_DONTWAIT); if (ret < 0) { printk("send failed: %d\n", ret); kfree_skb(skb); // 发送失败必须自己释放 }
三、内核发送 Netlink 极简流程总结(背会)
1. 创建 nl_sk(netlink_kernel_create) 2. alloc_skb 申请包 3. nlmsg_put 放入 nlmsghdr 4. NLMSG_DATA 填自定义数据 5. netlink_unicast 发给用户态 PID 6. 失败则 kfree_skb
四、和用户态发送对比(一看就懂)
- 用户态:
malloc→ 填nlmsghdr+ 数据 →sendto- 内核态:
alloc_skb→nlmsg_put+ 数据 →netlink_unicast思路完全一样,只是接口不同。
五、你最可能关心的 2 个问题
1)内核怎么知道要发给哪个用户进程?
靠
dest_pid,即用户态bind时用的nl_pid(一般就是getpid())。2)内核能不能主动随便发?
能!不需要用户先发送,内核可以任何时候主动发。
用户态绑定的pid,一定要是进程本身的pid吗,还是只要和内核保持一致就行?直接给你最准确、最关键的结论:
结论
用户态 bind 的 nl_pid 不一定要等于真实进程 PID, 只要满足一个条件:唯一 + 内核知道发给这个 ID 就行。
详细讲清楚
1. 这个 nl_pid 到底是什么?
它不是操作系统的 PID,而是 Netlink 内部的 "端口号" 。内核文档里叫:NL_PID = Netlink Port ID
作用只有一个:让内核知道这条消息应该发给哪个用户态 Netlink socket。
2. 能不能不填真实 PID?
能!
你可以随便填一个数字,比如:
local.nl_pid = 123456; // 不是真实进程PID只要满足:
- 系统内唯一同一时刻不能有两个 socket bind 同一个 nl_pid
- 内核发送时,知道要发给这个数字 内核调用
netlink_unicast(..., dest_pid=123456, ...)那就完全可以正常收发。
3. 内核怎么看待这个 nl_pid?
内核根本不关心它是不是真实 PID,内核只把它当作一个 32 位的标识,用来路由消息。
- 用户态 bind (1234)
- 内核发消息给 portid=1234
- 内核就找到那个 socket 投递
和操作系统 PID 没有强制关联。
4. 极端但合法的例子
- 进程 A(PID=1000)bind nl_pid=9999
- 进程 B(PID=1001)bind nl_pid=8888
- 内核给 9999 发 → 进程 A 收
- 内核给 8888 发 → 进程 B 收
完全合法,正常工作。
5. 但有一个坑必须注意
如果使用 NETLINK_ROUTE 等内核标准协议:内核默认会认为 nl_pid == 进程 PID,你乱填可能导致部分内核功能不识别。
所以:
- 自定义 Netlink(NETLINK_USERSOCK / GENERIC)→ 可以随便填唯一数字
- 系统标准 Netlink(ROUTE、NFT、UEVENT) → 必须填自己的真实 PID
最终一句话总结
nl_pid 是 Netlink 内部端口号,不是系统 PID, 不一定要等于进程 PID,只要唯一、双方一致即可。 但实际开发推荐直接填 getpid (),最简单安全。
使用流程(内核-接收)
现在只讲内核态如何接收用户态发来的 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 端口 → HTTP
- 22 端口 → SSH
在内核里:
- NETLINK_ROUTE → 网络配置服务(ip addr/ip link)
- NETLINK_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_STREAM(TCP 模式)
- Netlink 支持 SOCK_DGRAM
- Netlink 支持 SOCK_RAW
- Netlink 支持 SOCK_SEQPACKET
五、它们在 Netlink 中的区别(超级简单)
1. SOCK_RAW(最常用、推荐)
- 你自己提供完整 nlmsghdr 消息头
- 内核不处理、不填充
- 完全可控
- 自定义通信首选
我们前面所有代码都用这个。
2. SOCK_DGRAM(数据报)
- 内核自动帮你填充 nlmsghdr 部分字段
- 你不用自己写消息头,只发数据
- 简化使用
- 但灵活性降低
3. SOCK_SEQPACKET
- 保证顺序
- 保留消息边界
- Netlink 很少用
4. SOCK_STREAM(TCP)
- Netlink 不支持!
- 因为 Netlink 是无连接的,像 UDP
六、最简单总结(背会)
Netlink 中:
- 99% 的情况用 SOCK_RAW
- 你自己组 nlmsghdr,最灵活
- 内核不干涉你的消息格式
七、你最该记住的一句话
写 Netlink 自定义通信 → 固定写 SOCK_RAW!
socket(AF_NETLINK, SOCK_RAW, NETLINK_USERSOCK);不需要改,不需要换,这就是标准写法。