Linux之Netlink

Netlink 是 Linux 系统中一种 用户态与内核态之间进行双向通信的机制。它被设计为一个基于套接字(Socket)的接口,使用一个基于消息的协议。它弥补了传统 ioctlprocfs 等方法的不足,成为现代 Linux 网络子系统(如 iproute2iptablesNetworkManager)和内核模块开发的核心通信技术。

可以把它想象成一条专门用于系统控制的“数据总线”:用户态程序通过这条总线向内核发送请求或配置指令,内核也可以通过这条总线主动向用户态程序发送事件通知(如网线被拔掉、有新设备插入)。

为什么需要 Netlink?

在 Netlink 出现之前,内核与用户空间的通信主要依赖以下方式,但它们都有明显缺点:

机制 缺点 Netlink 优势
ioctl() 单向通信(用户→内核),简单设备控制,难以传递复杂数据 双向通信,支持复杂结构数据
procfs/sysfs 同步阻塞,效率低,静态参数读写,不适合事件通知 异步通信,支持事件通知(多播)
syscall 扩展困难,破坏 ABI 稳定性 协议化设计,易于扩展新功能
netfilter 仅限网络包过滤等处理场景 通用框架,适用于多种内核子系统

核心价值:Netlink 提供了结构化、高效、事件驱动的用户-内核通信能力。

Netlink 的优势

  • 双向异步:支持用户态到内核,也支持内核到用户态的消息传递。
  • 结构化数据:使用二进制消息格式,高效且易于程序解析。
  • 面向数据包:基于 Socket,天然适合网络编程模型。
  • 可扩展:通过定义新的消息类型和属性来添加功能,无需创建新的系统调用或设备文件。
  • 多播支持:多个用户进程可以加入一个“多播组”,同时接收内核发出的同一类事件通知(这是实现 ip monitor 等功能的关键)。

基础架构

  • 地址族AF_NETLINK (在 socket(2) 中使用)
  • 消息载体struct nlmsghdr (Netlink 消息头)
  • 通信模型
    • 用户态:创建 NETLINK_* 类型的 socket
    • 内核态:注册 Netlink 回调函数处理消息

地址族 (Address Family)

创建 Netlink Socket 时,使用 AF_NETLINK 作为地址族。

int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);

AF_NETLINK 地址族下又细分了多个协议,每个协议用于不同的子系统通信。socket() 的第三个参数用于指定协议。常见的有:

协议族 (#define) 用途 用户工具示例
NETLINK_ROUTE (0) 获取和设置网络配置 (路由、接口、地址) ip, ss, tc
NETLINK_USERSOCK (2) 为用户空间程序预留 用于自定义通信
NETLINK_SOCK_DIAG(4) socket monitoring 用于自定义通信
NETLINK_NFLOG (5) Netfilter/iptables 日志 ulogd
NETLINK_XFRM (6) IPsec 安全策略/关联 ip xfrm
NETLINK_NETFILTER (12) Netfilter 子系统 (现代防火墙) iptables, nft
NETLINK_GENERIC (16) 通用 Netlink (genetlink), NETLINK_DM (DM Events) 自定义内核模块通信 wlantool

💡 重点NETLINK_ROUTE (rtnetlink) 和 NETLINK_GENERIC (genetlink) 是当前最主流的协议。

消息格式 (Message Format)

Netlink 消息是自描述的,由消息头消息体组成,消息体又由属性构成。这是其可扩展性的基础。

每个消息的开头都是这个标准头。

struct nlmsghdr {
    __u32 nlmsg_len;    // 整个消息的长度,包括头部
    __u16 nlmsg_type;   // 消息类型(如 RTM_NEWLINK, RTM_GETLINK)
    __u16 nlmsg_flags;  // 标志(如 NLM_F_REQUEST, NLM_F_MULTI, NLM_F_ACK)
    __u32 nlmsg_seq;    // 序列号,用于匹配请求和响应
    __u32 nlmsg_pid;    // 发送方的端口ID (Port ID),通常是进程ID
};
  • 对齐要求:所有 Netlink 消息必须 4 字节对齐
  • nlmsg_type: 区分是请求、响应、通知还是错误等。常见类型有:
    • NLMSG_NOOP:空操作
    • NLMSG_ERROR:错误消息 (含 struct nlmsgerr)
    • NLMSG_DONE:多消息序列结束
    • RTM_NEWLINK/RTM_DELLINK:网络接口事件 (rtnetlink)
  • nlmsg_flags:
    • NLM_F_REQUEST 表示这是一个请求;
    • NLM_F_MULTI 表示这是多部分消息的一部分;
    • NLM_F_ACK 要求接收方回复确认。

消息头后面跟随着消息的有效载荷。Netlink 采用 TLV (Type-Length-Value) 格式来组织这些数据,称为属性

  • **T (Type)**: 一个整数,标识属性的类型。
  • **L (Length)**: 整个属性(包括头和数据)的长度。
  • **V (Value)**: 属性的实际数据。

属性头由 struct rtattr 表示(在通用 Netlink 中可能是 struct nlattr):

struct rtattr {
    unsigned short rta_len;    // Length of entire attribute (T+L+V)
    unsigned short rta_type;   // Type of attribute (T)
    // Then follows the value data (V)
};

这种结构使得解析消息时可以轻松跳过未知的属性类型,保证了向后兼容性。

通信模式

模式 流程 应用场景
请求-响应 用户态 → 内核 → 用户态 (同步/异步) 配置路由 (ip route add)
内核事件通知 内核 → 用户态 (通过多播组) 监听网卡 UP/DOWN 事件
用户态进程通信 用户态 A → 内核 → 用户态 B (较少用) 特定场景

多播组 (Multicast Groups)

用户进程可以订阅到一个或多个 Netlink 多播组。当内核发生相关事件时,它会向所有订阅了该组的进程广播消息。例如,监听网络接口状态变化的程序会订阅 RTMGRP_LINK 组。

  • 内核将事件广播到特定多播组
  • 用户态进程订阅感兴趣的组
  • 常见组 (rtnetlink 示例):
    #define RTNLGRP_LINK      1  // 网络接口状态变化
    #define RTNLGRP_IPV4_IFADDR 5 // IPv4 地址变化
    #define RTNLGRP_IPV6_ROUTE 12 // IPv6 路由变化
  • 用户态订阅
    int group = RTNLGRP_LINK | RTNLGRP_IPV4_IFADDR;
    setsockopt(fd, SOL_NETLINK, NETLINK_ADD_MEMBERSHIP, &group, sizeof(group));

    Netlink的通信流程

我们通过一个简单的例子(用户空间请求内核返回所有网络接口信息)来看 Netlink 如何工作:

  1. 用户空间:构建请求

    • 创建一个 AF_NETLINK 套接字,协议为 NETLINK_ROUTE
    • 构建一个 Netlink 消息:
      • 消息头:nlmsg_type = RTM_GETLINK(获取链接),nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_DUMP(这是一个请求,要求回复确认,并要求内核“dump”出所有信息)。
      • 消息体:可能包含一些属性来过滤请求(例如只获取特定接口的信息),但对于获取全部信息,消息体可能为空。
    • 调用 sendmsg() 将消息发送到内核。
  2. 内核:处理请求

    • 内核的 Netlink 子系统收到 RTM_GETLINK 请求。
    • 路由子系统处理该请求,遍历所有的网络接口。
    • 为每个接口构建一个 RTM_NEWLINK 响应消息。每个消息都包含一个 struct ifinfomsg 和多个属性(IFLA_IFNAME, IFLA_ADDRESS 等)。
  3. 内核:返回响应

    • 内核通过同一个 Netlink 套接字将一系列 RTM_NEWLINK 消息发回给用户进程。
    • 最后一个消息会设置 NLMSG_DONE 标志,表示所有数据已发送完毕。
  4. 用户空间:解析响应

    • 用户进程在一个循环中调用 recvmsg() 读取数据。
    • 对接收到的每个消息缓冲区进行解析:
      • 检查 nlmsghdr->nlmsg_type:是 RTM_NEWLINK 还是 NLMSG_DONE 等。
      • 对于 RTM_NEWLINK,跳过 nlmsghdr,找到后面的 struct ifinfomsg
      • 再之后,使用 RTA_DATA(), RTA_NEXT() 等宏来遍历所有的属性,提取出接口名、MAC 地址等信息。

Netlink的应用

用户态开发

  • 直接使用原生 Socket APIsocket, bind, sendmsg, recvmsg)和数据结构(struct nlmsghdr, struct rtattr)进行开发,灵活性最高但也最复杂。
  • 使用封装库来简化操作:
    • libnl: 最著名和常用的 Netlink 开发库,提供了高层次、易于使用的 API。
    • libmnl: (Minimalistic Netlink Library) 轻量级,只处理消息的构造和解析,不管理套接字通信。

典型应用场景

  1. 网络配置 (iproute2)

    • ip linkRTM_GETLINK/RTM_NEWLINK
    • ip routeRTM_GETROUTE/RTM_NEWROUTE
    • ip monitor link→ 订阅 RTMGRP_LINK 多播组实现实时监控
    • 旧的 net-toolsifconfig, route, netstat)则使用 IOCTL。
  2. 防火墙管理 (nftables)

    • 通过 NETLINK_NETFILTER 与内核通信
  3. 网络监控工具

    • conntrack 订阅连接跟踪事件
    • ss 获取套接字信息
  4. 自定义内核模块

    • 使用 Generic Netlink 暴露调试接口
    • 例如:eBPF 程序加载 (BPF_PROG_LOAD 通过 genetlink)
  5. 系统事件监听

    • systemd 监听网卡状态变化 (加入 RTNLGRP_LINK 组)

📚 延伸学习:

  • 内核源码:net/netlink/
  • 内核官方文档:
    • Documentation/userspace-api/netlink
    • Documentation/core-api/netlink.rst
    • Documentation/netlink
  • 工具:libmnl 示例 (https://www.netfilter.org/projects/libmnl/)
  • 协议:RFC 3549 “Linux Netlink as an IP Services Protocol”