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
    • 内核:永远是 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:要求内核回复 ACK
    • NLM_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 大结构关系总结(超级清晰)

  1. sockaddr_nl:我是谁、发给谁
  2. nlmsghdr:消息头,控制消息类型、序列号、标志
  3. nlattr:携带真正的数据(IP、MAC、路由等)
  4. 辅助宏:简化解析、遍历、取值

它们合在一起 = 完整的 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 消息组装 =

  1. 开内存
  2. 填头
  3. 填你自己的数据
  4. 发送
    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. 你以后只要记住这个规则

  1. malloc 申请 头 + 数据 的总大小
  2. struct nlmsghdr * 指向这块内存开头
  3. 填好头部字段
  4. NLMSG_DATA(nlh) 获取后面的数据指针
  5. 发送整块内存

超级简化记忆

指针只管开头, 内存长度管全部, 后面的数据靠偏移!

你现在是不是彻底懂了?不懂我再用画图给你讲!

无需提前建立连接

需要和常规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 消息。


前提(必须记住)

  1. Netlink 像 UDP,无连接,不用 connect
  2. 必须 bind 自己,否则内核不知道你是谁
  3. 发给内核:目标 pid = 0
  4. 消息格式 = 消息头 (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();     // 你的ID

5.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 句话(一定要懂)

  1. nlh 只是指向内存开头,不是只存头 内存里是:头 + 数据 连在一起

  2. 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 阻塞等待消息

可以用 recvrecvfrom,最简单用 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_skbnlmsg_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 消息,一步一步讲清楚,和你前面学的用户态收发完全对应。

一、内核接收的核心逻辑

  1. 内核不主动调用 recv ,而是靠回调函数
  2. 用户态发消息 → 内核收到后 自动调用你注册的回调
  3. 消息放在 struct sk_buff *skb
  4. 从 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);

六、最关键的总结(背会)

  1. Netlink 类型 = 内核服务号
  2. USERSOCK:自定义通信,最简单,学习首选
  3. ROUTE:网络配置,ip 命令底层
  4. GENERIC:官方推荐自定义通信(比 USERSOCK 高级)
  5. UEVENT:热插拔事件
  6. 使用方法: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 种类型:

  1. SOCK_RAW → 原始报文(自己组头)
  2. SOCK_DGRAM → 数据报(UDP 模式)
  3. SOCK_STREAM → 流式(TCP 模式)
  4. 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);

不需要改,不需要换,这就是标准写法。

相关推荐
大新软件技术部2 小时前
Linux 服务器下dotnetcore 程序监控
linux·运维·服务器
程序猿编码2 小时前
Linux内核级隐身术:进程与端口隐藏技术剖析
linux·运维·服务器·linux内核·进程
Mr_Xuhhh2 小时前
[特殊字符] 《网络知识和Servlet重点知识整理》
网络·servlet
952362 小时前
网络原理 - HTTP / HTTPS
网络·http·https
爱分享的阿Q2 小时前
RISC-V驱动开发合规解析
驱动开发·risc-v
克莱因3582 小时前
思科 单区域OSPF(1
网络·路由·思科
Oll Correct2 小时前
实验十四:IPv4地址的无分类编址方法
网络·笔记
Bohemian—Rhapsody2 小时前
麒麟v10-arm架构部署rabbitmq
arm开发·架构·rabbitmq
23zhgjx-ctl2 小时前
111111
网络·智能路由器