云计算百科
云计算领域专业知识百科平台

Linux 用户空间与内核空间的 Netlink 通信机制及实现

注:本文为 “Linux Netlink 通信机制” 相关合辑。 略作重排,未整理去重。 如有内容异常,请看原文。


用户空间与内核空间通信——Netlink(上)

wjlkoorey258 2012-11-07 22:00:24

引言

Alan Cox 在内核 1.3 版本的开发阶段首次引入了 Netlink 机制,最初该机制以字符驱动接口的形式,提供内核与用户空间之间的双向数据通信能力;随后,在 2.1 内核版本的开发过程中,Alexey Kuznetsov 将 Netlink 重构为一套更为灵活、且易于扩展的基于消息的通信接口,并将其应用于高级路由子系统的基础框架实现中。自该阶段起,Netlink 便成为 Linux 内核子系统与用户态应用程序之间进行数据通信的主要手段之一。

2001 年,ForCES IETF 委员会正式启动了 Netlink 机制的标准化工作。Jamal Hadi Salim 提议将 Netlink 定义为一种用于网络设备路由引擎组件与控制管理组件之间通信的专用协议,但该提议最终未被采纳。取而代之的是当前的实现格局:Netlink 被设计为一个全新的协议域(domain)。

Linux 创始人 Linus Torvalds 曾提出:“Linux is evolution, not intelligent design”。这一理念同样适用于 Netlink 机制——该机制不存在完整的规范文档与设计文档,其底层细节的获取仅能通过“Read the f**king source code”的方式实现。

本文不涉及 Netlink 在 Linux 系统中的实现机制剖析,仅围绕“什么是 Netlink”与“如何正确使用 Netlink”两个主题展开阐述,仅当实际应用中遇到问题时,才需要查阅内核源码以明确其底层原理。

什么是 Netlink

对 Netlink 机制的理解,需把握以下两个关键要点:

  • 面向数据报的无连接消息子系统
  • 基于通用 BSD Socket 架构实现
  • 关于第一点,其特性与 UDP 协议具有较高的相似性,以 UDP 协议为参考理解 Netlink 机制具有合理性。通过知识的迁移、归纳与总结,可实现对该机制的深入掌握。Netlink 支持内核空间到用户空间、用户空间到内核空间的双向异步数据通信,同时也支持两个用户进程之间、两个内核子系统之间的数据通信。本文不涉及后两种通信场景,仅聚焦于用户空间与内核空间之间的数据通信实现。

    提及第二点,通常会联想到对应的 BSD Socket 架构示意图(如下所示)。

    在后续 Netlink 套接字编程的实战环节中,主要将使用 socket()、bind()、sendmsg() 与 recvmsg() 等系统调用,同时还会用到 Socket 提供的轮询(polling)机制。

    Netlink 通信类型

    Netlink 支持两种通信类型:单播(Unicast)与多播(Multicast)。

    单播

    单播常用于单个用户进程与单个内核子系统之间的

    1

    :

    1

    1:1

    1:1 数据通信,用户空间向内核发送命令,并接收内核返回的命令执行结果。

    多播

    多播常用于单个内核进程与多个用户进程之间的

    1

    :

    N

    1:N

    1:N 数据通信,内核作为会话发起方,用户空间应用程序作为消息接收方。实现该功能的流程为:内核空间程序创建一个多播组,所有对该内核进程发送的消息感兴趣的用户空间进程,均可通过加入该多播组的方式接收对应消息。

    其中,进程 A 与子系统 1 之间为单播通信,进程 B、C 与子系统 2 之间为多播通信。上述示意图还揭示了一个重要特性:从用户空间传递至内核空间的数据无需排队,对应的操作以同步方式完成;而从内核空间传递至用户空间的数据需要排队,对应的操作以异步方式完成。掌握该特性能够在基于 Netlink 开发应用模块时规避诸多潜在问题。例如,当用户空间向内核发送消息以获取路由表等大规模数据时,内核通过 Netlink 返回数据的过程中,开发人员需重点考虑数据的接收策略,充分重视内核空间的输出队列特性。

    Netlink 的消息格式

    Netlink 消息由消息头(Message Header)与有效数据载荷(Payload)两部分组成,整个 Netlink 消息需满足

    4

    4

    4 字节对齐要求,通常以主机字节序进行传递。消息头为固定

    16

    16

    16 字节长度,消息体长度为可变值。

    Netlink 的消息头

    消息头定义在对应内核头文件中,由结构体 struct nlmsghdr 表示,其定义如下:

    struct nlmsghdr
    {
    __u32 nlmsg_len; /* Length of message including header */
    __u16 nlmsg_type; /* Message content */
    __u16 nlmsg_flags; /* Additional flags */
    __u32 nlmsg_seq; /* Sequence number */
    __u32 nlmsg_pid; /* Sending process PID */
    };

    消息头中各成员的属性解释如下:

  • nlmsg_len:整个消息的字节长度,包含 Netlink 消息头本身的长度。
  • nlmsg_type:消息类型,用于区分数据消息与控制消息。在内核 2.6.21 版本中,Netlink 仅支持四种控制消息,具体如下:
    • NLMSG_NOOP:空消息,无任何实际操作;
    • NLMSG_ERROR:标识该消息中包含错误信息;
    • NLMSG_DONE:当内核通过 Netlink 队列返回多条消息时,队列的最后一条消息类型为此值,其余所有消息的 nlmsg_flags 属性均会设置 NLM_F_MULTI 位有效;
    • NLMSG_OVERRUN:暂未启用。
  • nlmsg_flags:附加在消息上的额外说明信息,例如前文提及的 NLM_F_MULTI。部分常用标记及其作用如下表所示:
  • 标记作用及说明
    NLM_F_REQUEST 若消息包含该标记位,表明该消息为请求消息。所有从用户空间发送至内核空间的消息均需设置该位,否则内核将向用户空间返回 EINVAL 无效参数错误
    NLM_F_MULTI 用户空间至内核空间的消息传输为同步即时完成,而内核空间至用户空间的消息传输需要排队。若内核收到用户空间发送的包含 NLM_F_DUMP 位为

    1

    1

    1 的消息,将向用户空间发送一个由多条 Netlink 消息组成的链表。除最后一条消息外,其余每条消息均会设置该位有效

    NLM_F_ACK 该消息是内核对来自用户空间的 NLM_F_REQUEST 消息的响应消息
    NLM_F_ECHO 若用户空间发送至内核的消息中该标记为

    1

    1

    1,表明用户应用进程要求内核将该消息通过单播形式回传给该用户进程,与常规的“回显”功能类似

    关于 nlmsg_flags 的完整取值,可通过查阅内核源码与官方技术文档获取,此处不做进一步展开。

  • nlmsg_seq:消息序列号。由于 Netlink 是面向数据报的通信机制,存在数据丢失的潜在风险,而 Netlink 提供了消息可靠性保障的基础机制,可供程序开发人员根据实际需求进行实现。消息序列号通常与 NLMSG_ACK 类型消息联合使用,若用户应用程序需要确保发送的每条消息均被内核成功接收,发送消息时需自行设置该序列号,内核收到消息后提取该序列号,并在响应消息中设置相同的序列号,该机制与 TCP 协议的应答确认机制具有相似性。

    注意:当内核主动向用户空间发送广播消息时,该字段的值恒为

    0

    0

    0

  • nlmsg_pid:当用户空间进程与内核空间子系统通过 Netlink 建立数据交换通道后,Netlink 会为每个通道分配唯一的数字标识,该字段的作用是将用户空间的请求消息与内核的响应消息进行关联,确保多组“用户-内核”通信进程之间的数据交互不会出现紊乱。例如,当进程 A、B 同时通过 Netlink 向子系统 1 获取信息时,子系统 1 需确保回传给进程 A 的响应数据不会发送至进程 B。该字段通常适用于用户空间进程从内核空间获取数据的场景,用户空间进程向内核发送消息时,一般通过 getpid() 系统调用将当前进程的进程号赋值给该字段(仅当需要获取内核响应时进行该操作)。内核主动向用户空间发送消息时,该字段的值恒为

    0

    0

    0

  • Netlink 的消息体

    Netlink 的消息体采用 TLV(Type-Length-Value)格式进行组织,每个属性均由头文件中的 struct nlattr{} 结构体表示。

    Netlink 提供的错误指示消息

    当用户空间应用程序与内核空间进程通过 Netlink 通信发生错误时,Netlink 会向用户空间通报该错误信息,错误消息采用单独封装的形式,对应的结构体 struct nlmsgerr 定义如下:

    struct nlmsgerr
    {
    int error; // 标准错误码,定义在 errno.h 头文件中,可通过 perror() 函数解析
    struct nlmsghdr msg; // 指明触发该错误的原始消息
    };

    Netlink 编程需要注意的问题

    基于 Netlink 实现的用户-内核空间通信,存在两种可能导致丢包的场景:

  • 系统内存耗尽;
  • 用户空间接收进程的缓冲区溢出。缓冲区溢出的主要诱因包括:用户空间进程运行效率过低,或接收队列长度过短。
  • 若 Netlink 无法将消息正确传递至用户空间接收进程,用户空间接收进程调用 recvmsg() 系统调用时,将返回 ENOBUFS(内存不足)错误,该特性需重点关注。换句话说,缓冲区溢出问题不会出现在从用户空间到内核空间的 sendmsg() 系统调用过程中,其原因前文已进行阐述,可自行进行梳理总结。

    此外,若使用阻塞型 Socket 进行通信,则不存在内存耗尽的潜在风险,相关原理可通过查阅阻塞型 Socket 的官方技术文档进行深入理解。

    Netlink 的地址结构体

    在 TCP 编程相关内容中,曾提及 Internet 编程过程中使用的地址结构体与标准地址结构体,这些结构体与 Netlink 地址结构体存在对应关联。

    Netlink 对应的地址结构体 struct sockaddr_nl{} 详细定义与描述如下:

    struct sockaddr_nl
    {
    sa_family_t nl_family; /* 该字段恒为 AF_NETLINK */
    unsigned short nl_pad; /* 目前未启用,填充为 0 */
    __u32 nl_pid; /* process pid */
    __u32 nl_groups; /* multicast groups mask */
    };

  • nl_pid:该属性为发送或接收消息的进程 ID。前文提及,Netlink 不仅支持用户-内核空间之间的通信,还支持用户空间两个进程之间、内核空间两个进程之间的通信。该属性值为

    0

    0

    0 时,通常适用于以下两种场景:

    • 场景一:消息的接收方为内核空间(即从用户空间发送消息至内核空间),此时构造的 Netlink 地址结构体中,nl_pid 字段通常置为

      0

      0

      0。需要补充说明的是,在 Netlink 规范中,PID 的全称为 Port-ID(

      32

      32

      32 bits),其作用是唯一标识一个基于 Netlink 的 Socket 通道。通常情况下,nl_pid 字段会被设置为当前进程的进程号;但当一个进程的多个线程同时使用 Netlink Socket 时,nl_pid 字段通常采用如下方式进行设置:pthread_self() << 16 | getpid();

    • 场景二:内核向用户空间发送多播报文时,若用户空间进程已加入对应多播组,其地址结构体中的 nl_pid 字段同样置为

      0

      0

      0,同时需结合下述 nl_groups 字段进行配置。

  • nl_groups:若用户空间进程希望加入某个多播组,必须执行 bind() 系统调用。该字段指明了调用者希望加入的多播组号的掩码(注意:并非组号,其详细用法将在后续内容中阐述)。若该字段值为

    0

    0

    0,表明调用者不希望加入任何多播组。对于每个隶属于 Netlink 协议域的协议,最多支持

    32

    32

    32 个多播组(因 nl_groups 字段长度为

    32

    32

    32 比特),每个多播组由一个独立的比特位表示。

  • 关于 Netlink 的其余知识点,将在后续实战环节中结合具体应用场景进行阐述。


    用户空间与内核空间通信——Netlink(中)

    wjlkoorey258 2012-11-12 19:42:22

    本节将通过实际编程演练,展示 Netlink 机制如何实现用户空间与内核空间之间的数据通信,所有实验均基于内核 2.6.21 版本环境完成。

    对应的内核头文件中包含了 Netlink 协议簇预定义的各类协议,具体定义如下:

    #define NETLINK_ROUTE 0 /* Routing/device hook */
    #define NETLINK_UNUSED 1 /* Unused number */
    #define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */
    #define NETLINK_FIREWALL 3 /* Firewalling hook */
    #define NETLINK_INET_DIAG 4 /* INET socket monitoring */
    #define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */
    #define NETLINK_XFRM 6 /* ipsec */
    #define NETLINK_SELINUX 7 /* SELinux event notifications */
    #define NETLINK_ISCSI 8 /* Open-iSCSI */
    #define NETLINK_AUDIT 9 /* auditing */
    #define NETLINK_FIB_LOOKUP 10
    #define NETLINK_CONNECTOR 11
    #define NETLINK_NETFILTER 12 /* netfilter subsystem */
    #define NETLINK_IP6_FW 13
    #define NETLINK_DNRTMSG 14 /* DECnet routing messages */
    #define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */
    #define NETLINK_GENERIC 16
    /* leave room for NETLINK_DM (DM Events) */
    #define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */
    #define NETLINK_ECRYPTFS 19
    #define NETLINK_TEST 20 /* 用户添加的自定义协议 */

    若需在 Netlink 协议簇中开发自定义协议,仅需在该文件中定义对应的协议号即可,例如上述代码中定义的协议号为

    20

    20

    20 的自定义协议 NETLINK_TEST。同时,需要对内核头文件目录中的 netlink.h 进行对应的修改,在本次实验环境中,该文件的路径为:/usr/src/linux-2.6.21/include/linux/netlink.h。

    完成上述配置后,即可在用户空间与内核空间模块的开发过程中使用该自定义协议,整个实验分为三个阶段进行。

    Stage 1:用户->内核单向数据通信

    本阶段实现的功能为用户空间到内核空间的单向数据通信,即用户空间向内核发送一条消息,内核接收该消息并将其打印输出,具体实现如下。

    用户空间示例代码【mynlusr.c】

    #include <sys/stat.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <string.h>
    #include <asm/types.h>
    #include <linux/netlink.h>
    #include <linux/socket.h>

    #define MAX_PAYLOAD 1024 /* 消息最大负载为 1024 字节 */

    int main(int argc, char* argv[])
    {
    struct sockaddr_nl dest_addr;
    struct nlmsghdr *nlh = NULL;
    struct iovec iov;
    int sock_fd=1;
    struct msghdr msg;

    // 创建套接字
    if(1 == (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){
    perror("can't create netlink socket!");
    return 1;
    }
    memset(&dest_addr, 0, sizeof(dest_addr));
    dest_addr.nl_family = AF_NETLINK;
    dest_addr.nl_pid = 0; /* 消息的接收方为内核空间 */
    dest_addr.nl_groups = 0; /* 本示例中无需使用该字段 */

    // 将套接字和 Netlink 地址结构体进行绑定
    if(1 == bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
    perror("can't bind sockfd with sockaddr_nl!");
    return 1;
    }

    if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
    perror("alloc mem failed!");
    return 1;
    }

    memset(nlh,0,MAX_PAYLOAD);
    /* 填充 Netlink 消息头部 */
    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
    nlh->nlmsg_pid = 0;
    nlh->nlmsg_type = NLMSG_NOOP; // 标识该 Netlink 消息负载为一条空消息
    nlh->nlmsg_flags = 0;

    /* 设置 Netlink 的消息内容,数据来自命令行输入的第一个参数 */
    strcpy(NLMSG_DATA(nlh), argv[1]);

    /* 此为固定使用模板,其详细原理将在后续 Socket 深入讲解中阐述 */
    memset(&iov, 0, sizeof(iov));
    iov.iov_base = (void *)nlh;
    iov.iov_len = nlh->nlmsg_len;
    memset(&msg, 0, sizeof(msg));
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;

    // 通过 Netlink socket 向内核发送消息
    sendmsg(sock_fd, &msg, 0);

    /* 关闭 netlink 套接字 */
    close(sock_fd);
    free(nlh);
    return 0;
    }

    上述代码的逻辑基于标准 Socket 编程 API 实现,唯一的差异在于本次编程针对 Netlink 协议簇进行。此处提前引入了 BSD 层的消息结构体 struct msghdr{}(定义在对应头文件中)与数据块结构体 struct iovec{}(定义在对应头文件中),其详细原理将在后续 Socket 深入讲解中阐述,当前仅需掌握其固定使用方式。

    此外,需要重点关注 Netlink 地址结构体与消息头结构体中 pid 字段值为

    0

    0

    0 的场景,避免出现概念混淆,相关总结如下表所示:

    字段值为 0 的适用场景
    netlink 地址结构体.nl_pid 1、内核发出的多播报文;2、消息的接收方为内核空间(即从用户空间发往内核空间的消息)
    netlink 消息头结构体.nlmsg_pid 内核主动向用户空间发送的消息

    本示例实现的是从用户空间到内核空间的单向数据通信,因此在 Netlink 地址结构体中设置 dest_addr.nl_pid = 0(标识消息接收方为内核空间),在填充 Netlink 消息头部时设置 nlh->nlmsg_pid = 0。

    同时,需要掌握以下两个宏的使用方法:

  • NLMSG_SPACE(MAX_PAYLOAD):该宏用于返回不小于 MAX_PAYLOAD 且满足

    4

    4

    4 字节对齐的最小长度值,通常用于内存申请时指定所需的内存字节数。与 NLMSG_LENGTH(len) 的差异在于:前者申请的空间不包含 Netlink 消息头部所占字节数,后者为消息负载与消息头的总长度。

  • NLMSG_DATA(nlh):该宏用于返回 Netlink 消息中数据部分的首地址,在写入与读取消息数据部分时会频繁使用。
  • 内核空间示例代码【mynlkern.c】

    #include <linux/kernel.h>
    #include <linux/module.h>
    #include <linux/skbuff.h>
    #include <linux/init.h>
    #include <linux/ip.h>
    #include <linux/types.h>
    #include <linux/sched.h>
    #include <net/sock.h>
    #include <linux/netlink.h>

    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("Koorey King");

    struct sock *nl_sk = NULL;
    static void nl_data_ready (struct sock *sk, int len)
    {
    struct sk_buff *skb;
    struct nlmsghdr *nlh = NULL;

    while((skb = skb_dequeue(&sk->sk_receive_queue)) != NULL)
    {
    nlh = (struct nlmsghdr *)skb->data;
    printk("%s: received netlink message payload: %s \\n", __FUNCTION__, (char*)NLMSG_DATA(nlh));
    kfree_skb(skb);
    }
    printk("recvied finished!\\n");
    }

    static int __init myinit_module()
    {
    printk("my netlink in\\n");
    nl_sk = netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
    return 0;
    }

    static void __exit mycleanup_module()
    {
    printk("my netlink out!\\n");
    sock_release(nl_sk->sk_socket);
    }

    module_init(myinit_module);
    module_exit(mycleanup_module);

    在内核模块的初始化函数中,通过 netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE) 函数创建了一个内核态 Socket,该函数各参数的含义如下:

  • 第一个参数:自定义协议的协议号(本次实验为 NETLINK_TEST);
  • 第二个参数:多播组号,本阶段实验无需使用,置为

    0

    0

    0

  • 第三个参数:回调函数,当内核的 Netlink Socket 接收到数据时,将触发该函数进行数据处理;
  • 第四个参数:内核模块标识,使用 THIS_MODULE 即可。
  • 在回调函数 nl_data_ready() 中,通过循环从 Socket 的接收队列中获取数据,获取到数据后将其打印输出,并释放对应的缓冲区资源。在协议栈的 INET 层中,数据的存储依赖 sk_buff 结构体实现,因此可通过 nlh = (struct nlmsghdr *)skb->data 获取 Netlink 消息体,再通过 NLMSG_DATA(nlh) 定位到 Netlink 消息的负载数据。

    将上述代码编译后,即可进行测试验证,获取对应的运行结果。

    Stage 2:用户<->内核双向数据通信

    对 Stage 1 中的代码进行小幅修改,即可实现用户空间与内核空间之间的双向数据通信,具体修改如下。

    用户空间代码修改

    #include <sys/stat.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <string.h>
    #include <asm/types.h>
    #include <linux/netlink.h>
    #include <linux/socket.h>

    #define MAX_PAYLOAD 1024 /* 消息最大负载为 1024 字节 */

    int main(int argc, char* argv[])
    {
    struct sockaddr_nl dest_addr;
    struct nlmsghdr *nlh = NULL;
    struct iovec iov;
    int sock_fd=1;
    struct msghdr msg;

    if(1 == (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){
    perror("can't create netlink socket!");
    return 1;
    }
    memset(&dest_addr, 0, sizeof(dest_addr));
    dest_addr.nl_family = AF_NETLINK;
    dest_addr.nl_pid = 0; /* 消息的接收方为内核空间 */
    dest_addr.nl_groups = 0; /* 本示例中无需使用该字段 */

    if(1 == bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
    perror("can't bind sockfd with sockaddr_nl!");
    return 1;
    }
    if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
    perror("alloc mem failed!");
    return 1;
    }

    memset(nlh,0,MAX_PAYLOAD);
    /* 填充 Netlink 消息头部 */
    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
    nlh->nlmsg_pid = getpid(); // 希望获取内核响应,因此设置当前进程 ID 供内核识别
    nlh->nlmsg_type = NLMSG_NOOP; // 标识该 Netlink 消息负载为一条空消息
    nlh->nlmsg_flags = 0;

    /* 设置 Netlink 的消息内容,数据来自命令行输入的第一个参数 */
    strcpy(NLMSG_DATA(nlh), argv[1]);

    /* 此为固定使用模板,其详细原理将在后续 Socket 深入讲解中阐述 */
    memset(&iov, 0, sizeof(iov));
    iov.iov_base = (void *)nlh;
    iov.iov_len = nlh->nlmsg_len;
    memset(&msg, 0, sizeof(msg));
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;

    // 通过 Netlink socket 向内核发送消息
    sendmsg(sock_fd, &msg, 0);

    // 接收内核返回的响应消息
    printf("waiting message from kernel!\\n");
    memset((char*)NLMSG_DATA(nlh),0,1024);
    recvmsg(sock_fd,&msg,0);
    printf("Got response: %s\\n",NLMSG_DATA(nlh));

    /* 关闭 netlink 套接字 */
    close(sock_fd);
    free(nlh);
    return 0;
    }

    内核空间代码修改

    #include <linux/kernel.h>
    #include <linux/module.h>
    #include <linux/skbuff.h>
    #include <linux/init.h>
    #include <linux/ip.h>
    #include <linux/types.h>
    #include <linux/sched.h>
    #include <net/sock.h>
    #include <net/netlink.h> /* 该头文件包含了 linux/netlink.h,同时提供 nlmsg_put() 等 API 函数 */

    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("Koorey King");

    #define MAX_MSGSIZE 1024 /* 消息最大长度为 1024 字节 */

    struct sock *nl_sk = NULL;

    // 向用户空间发送消息的接口函数
    void sendnlmsg(char *message,int dstPID)
    {
    struct sk_buff *skb;
    struct nlmsghdr *nlh;
    int len = NLMSG_SPACE(MAX_MSGSIZE);
    int slen = 0;

    if(!message || !nl_sk){
    return;
    }

    // 为新的 sk_buffer 申请空间
    skb = alloc_skb(len, GFP_KERNEL);
    if(!skb){
    printk(KERN_ERR "my_net_link: alloc_skb Error./n");
    return;
    }

    slen = strlen(message)+1;

    // 用 nlmsg_put() 来设置 netlink 消息头部
    nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);

    // 设置 Netlink 的控制块
    NETLINK_CB(skb).pid = 0; // 消息发送者为内核空间,置为 0
    NETLINK_CB(skb).dst_group = 0; // 目的为单个进程,该字段置为 0

    message[slen] = '\\0';
    memcpy(NLMSG_DATA(nlh), message, slen+1);

    // 通过 netlink_unicast() 将消息发送至用户空间指定 PID 的进程
    netlink_unicast(nl_sk,skb,dstPID,0);
    printk("send OK!\\n");
    return;
    }

    static void nl_data_ready (struct sock *sk, int len)
    {
    struct sk_buff *skb;
    struct nlmsghdr *nlh = NULL;

    while((skb = skb_dequeue(&sk->sk_receive_queue)) != NULL)
    {
    nlh = (struct nlmsghdr *)skb->data;
    printk("%s: received netlink message payload: %s \\n", __FUNCTION__, (char*)NLMSG_DATA(nlh));
    kfree_skb(skb);
    // 提取用户进程 PID,向其发送响应消息
    sendnlmsg("I see you",nlh->nlmsg_pid);
    }
    printk("recvied finished!\\n");
    }

    static int __init myinit_module()
    {
    printk("my netlink in\\n");
    nl_sk = netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
    return 0;
    }

    static void __exit mycleanup_module()
    {
    printk("my netlink out!\\n");
    sock_release(nl_sk->sk_socket);
    }

    module_init(myinit_module);
    module_exit(mycleanup_module);

    将修改后的代码重新编译后,即可进行测试验证,获取对应的双向通信运行结果。

    Stage 3:无 bind() 调用的双向数据通信

    前文提及,用户进程仅在需要加入多播组时才需要调用 bind() 函数。Stage 2 中无多播组相关需求,却调用了 bind() 函数,本次将对代码进行修改,移除 bind() 调用,改用 sendto() 与 recvfrom() 函数实现数据的收发。

    用户空间代码修改

    #include <sys/stat.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <string.h>
    #include <asm/types.h>
    #include <linux/netlink.h>
    #include <linux/socket.h>

    #define MAX_PAYLOAD 1024 /* 消息最大负载为 1024 字节 */

    int main(int argc, char* argv[])
    {
    struct sockaddr_nl dest_addr;
    struct nlmsghdr *nlh = NULL;
    // struct iovec iov; // 无需使用该结构体
    int sock_fd=1;
    // struct msghdr msg; // 无需使用该结构体

    if(1 == (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){
    perror("can't create netlink socket!");
    return 1;
    }
    memset(&dest_addr, 0, sizeof(dest_addr));
    dest_addr.nl_family = AF_NETLINK;
    dest_addr.nl_pid = 0; /* 消息的接收方为内核空间 */
    dest_addr.nl_groups = 0; /* 本示例中无需使用该字段 */

    /* 移除 bind() 函数调用 */
    /*
    if(-1 == bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
    perror("can't bind sockfd with sockaddr_nl!");
    return 1;
    }
    */

    if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
    perror("alloc mem failed!");
    return 1;
    }
    memset(nlh,0,MAX_PAYLOAD);
    /* 填充 Netlink 消息头部 */
    nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
    nlh->nlmsg_pid = getpid();// 希望获取内核响应,因此设置当前进程 ID 供内核识别
    nlh->nlmsg_type = NLMSG_NOOP; // 标识该 Netlink 消息负载为一条空消息
    nlh->nlmsg_flags = 0;

    /* 设置 Netlink 的消息内容,数据来自命令行输入的第一个参数 */
    strcpy(NLMSG_DATA(nlh), argv[1]);

    /* 该模板无需使用 */
    /*
    memset(&iov, 0, sizeof(iov));
    iov.iov_base = (void *)nlh;
    iov.iov_len = nlh->nlmsg_len;
    memset(&msg, 0, sizeof(msg));
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    */

    // 改用 sendto() 函数向内核发送消息
    // sendmsg(sock_fd, &msg, 0);
    sendto(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),sizeof(dest_addr));

    // 接收内核返回的响应消息,改用 recvfrom() 函数
    printf("waiting message from kernel!\\n");
    memset(nlh,0,MAX_PAYLOAD); // 清空整个 Netlink 消息(包含消息头与负载)
    // recvmsg(sock_fd,&msg,0);
    recvfrom(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),NULL);
    printf("Got response: %s\\n",NLMSG_DATA(nlh));

    /* 关闭 netlink 套接字 */
    close(sock_fd);
    free(nlh);
    return 0;
    }

    说明

    内核空间的代码无需进行任何修改,仍通过 netlink_unicast() 函数向用户空间发送响应消息。将修改后的代码重新编译后,其运行效果与 Stage 2 完全一致。

    由此可得结论:在 Netlink 程序开发过程中,若不涉及多播机制,用户空间的 Socket 代码无需执行 bind() 系统调用,此时需使用 sendto() 与 recvfrom() 函数完成数据的收发;若执行了 bind() 系统调用,同样可以使用 sendto() 与 recvfrom() 函数,但需调整对应的参数传递,此时更推荐使用 sendmsg() 与 recvmsg() 函数完成数据收发。开发人员可根据实际应用场景灵活选择对应的实现方式。


    用户空间与内核空间通信——Netlink(下)

    wjlkoorey258 2012-11-15 19:50:53

    Netlink 多播机制的用法

    在上一节的内容中,所有实验场景均以用户空间作为消息发起方,而 Netlink 机制同样支持内核空间作为主动消息发送方,该场景通常用于内核向用户空间主动报告自身状态变化,例如用户空间感知到的 USB 热插拔事件通告,便是基于该机制实现的。

    本次实验的目标为:实现一个内核线程,该线程每隔

    1

    1

    1 秒向一个指定多播组发送一条消息,所有加入该多播组的用户空间进程均可接收并打印该消息内容。

    Netlink 地址结构体中的 nl_groups 字段为

    32

    32

    32 位,这意味着每种 Netlink 协议最多支持

    32

    32

    32 个多播组。此处的“每种 Netlink 协议”指的是 Netlink 协议簇中的各类预定义协议(如下所示),以及本次实验中自定义的 NETLINK_TEST 协议。

    #define NETLINK_ROUTE 0 /* Routing/device hook */
    #define NETLINK_UNUSED 1 /* Unused number */
    #define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */
    #define NETLINK_FIREWALL 3 /* Firewalling hook */
    #define NETLINK_INET_DIAG 4 /* INET socket monitoring */
    #define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */
    #define NETLINK_XFRM 6 /* ipsec */
    #define NETLINK_SELINUX 7 /* SELinux event notifications */
    #define NETLINK_ISCSI 8 /* Open-iSCSI */
    #define NETLINK_AUDIT 9 /* auditing */
    #define NETLINK_FIB_LOOKUP 10
    #define NETLINK_CONNECTOR 11
    #define NETLINK_NETFILTER 12 /* netfilter subsystem */
    #define NETLINK_IP6_FW 13
    #define NETLINK_DNRTMSG 14 /* DECnet routing messages */
    #define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */
    #define NETLINK_GENERIC 16
    /* leave room for NETLINK_DM (DM Events) */
    #define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */
    #define NETLINK_ECRYPTFS 19
    #define NETLINK_TEST 20 /* 用户添加的自定义协议 */

    在自定义的 NETLINK_TEST 协议中,最多允许设置

    32

    32

    32 个多播组,每个多播组由一个独立的比特位表示,不存在多播组重复的情况。开发人员可根据实际需求为每个多播组分配对应的功能,用户空间进程若对某个多播组的消息感兴趣,可通过加入该多播组的方式,接收内核空间向该组发送的多播消息。

    回到 Netlink 地址结构体的 nl_groups 字段,该字段存储的是多播组的地址掩码(并非多播组号)。在 af_netlink.c 文件中,提供了从多播组号转换为多播组掩码的函数,具体定义如下:

    static u32 netlink_group_mask(u32 group)
    {
    return group ? 1 << (group 1) : 0;
    }

    由此可知,在用户空间代码中,若需加入多播组

    1

    1

    1,需将 nl_groups 字段设置为

    1

    1

    1;多播组

    2

    2

    2 对应的掩码为

    2

    2

    2;多播组

    3

    3

    3 对应的掩码为

    4

    4

    4,以此类推。若该字段值为

    0

    0

    0,表明不加入任何多播组。掌握该转换关系具有重要意义,因此可在用户空间代码中实现一个功能类似 netlink_group_mask() 的函数,完成多播组号到多播组掩码的转换。

    实现代码

    用户空间代码

    #include <sys/stat.h>
    #include <unistd.h>
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/socket.h>
    #include <sys/types.h>
    #include <string.h>
    #include <asm/types.h>
    #include <linux/netlink.h>
    #include <linux/socket.h>
    #include <errno.h>

    #define MAX_PAYLOAD 1024 // Netlink 消息的最大载荷的长度

    // 多播组号转多播组掩码函数
    unsigned int netlink_group_mask(unsigned int group)
    {
    return group ? 1 << (group 1) : 0;
    }

    int main(int argc, char* argv[])
    {
    struct sockaddr_nl src_addr;
    struct nlmsghdr *nlh = NULL;
    struct iovec iov;
    struct msghdr msg;
    int sock_fd, retval;

    // 创建 Socket
    sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
    if(sock_fd == 1){
    printf("error getting socket: %s", strerror(errno));
    return 1;
    }

    memset(&src_addr, 0, sizeof(src_addr));
    src_addr.nl_family = PF_NETLINK;
    src_addr.nl_pid = 0; // 标识从内核接收多播消息(另一个含义为消息发送方为内核)
    src_addr.nl_groups = netlink_group_mask(atoi(argv[1])); // 多播组掩码,组号来自命令行输入的第一个参数

    // 加入多播组必须调用 bind() 函数
    retval = bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
    if(retval < 0){
    printf("bind failed: %s", strerror(errno));
    close(sock_fd);
    return 1;
    }

    // 为接收 Netlink 消息申请存储空间
    nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
    if(!nlh){
    printf("malloc nlmsghdr error!\\n");
    close(sock_fd);
    return 1;
    }

    memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
    iov.iov_base = (void *)nlh;
    iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);

    memset(&msg, 0, sizeof(msg));
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;

    // 从内核接收消息
    printf("waiting for…\\n");
    recvmsg(sock_fd, &msg, 0);
    printf("Received message: %s \\n", NLMSG_DATA(nlh));

    close(sock_fd);

    return 0;
    }

    说明

    用户空间程序的整体逻辑与前文保持一致,差异在于 nl_groups 字段的设置,该字段的相关文档较为稀缺,其详细用法可通过查阅内核源码与相关技术资料进一步获取。

    内核空间代码

    #include <linux/kernel.h>
    #include <linux/module.h>
    #include <linux/skbuff.h>
    #include <linux/init.h>
    #include <linux/ip.h>
    #include <linux/types.h>
    #include <linux/sched.h>
    #include <net/sock.h>
    #include <net/netlink.h>

    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("Koorey King");

    #define MAX_MSGSIZE 1024 /* 消息最大长度为 1024 字节 */

    struct sock *nl_sk = NULL;
    static struct task_struct *mythread = NULL; // 内核线程对象

    // 向用户空间发送消息的接口函数
    void sendnlmsg(char *message)
    {
    struct sk_buff *skb;
    struct nlmsghdr *nlh;
    int len = NLMSG_SPACE(MAX_MSGSIZE);
    int slen = 0;

    if(!message || !nl_sk){
    return;
    }

    // 为新的 sk_buffer 申请空间
    skb = alloc_skb(len, GFP_KERNEL);
    if(!skb){
    printk(KERN_ERR "my_net_link: alloc_skb Error./n");
    return;
    }

    slen = strlen(message)+1;

    // 用 nlmsg_put() 来设置 netlink 消息头部
    nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);

    // 设置 Netlink 的控制块里的相关信息
    NETLINK_CB(skb).pid = 0; // 消息发送者为内核空间,置为 0
    NETLINK_CB(skb).dst_group = 5; // 多播组号为 5

    message[slen] = '\\0';
    memcpy(NLMSG_DATA(nlh), message, slen+1);

    // 发送多播消息到多播组 5
    netlink_broadcast(nl_sk, skb, 0,5, GFP_KERNEL);
    printk("send OK!\\n");
    return;
    }

    // 内核线程函数:每隔 1 秒钟发送一条“I am from kernel!”消息,共发送 10 条
    static int sending_thread(void *data)
    {
    int i = 10;
    struct completion cmpl;

    while(i){
    init_completion(&cmpl);
    wait_for_completion_timeout(&cmpl, 1 * HZ);
    sendnlmsg("I am from kernel!");
    }
    printk("sending thread exited!");
    return 0;
    }

    static int __init myinit_module()
    {
    printk("my netlink in\\n");
    nl_sk = netlink_kernel_create(NETLINK_TEST,0,NULL,THIS_MODULE);

    if(!nl_sk){
    printk(KERN_ERR "my_net_link: create netlink socket error.\\n");
    return 1;
    }

    printk("my netlink: create netlink socket ok.\\n");
    mythread = kthread_run(sending_thread,NULL,"thread_sender");
    return 0;
    }

    static void __exit mycleanup_module()
    {
    if(nl_sk != NULL){
    sock_release(nl_sk->sk_socket);
    }
    printk("my netlink out!\\n");
    }

    module_init(myinit_module);
    module_exit(mycleanup_module);

    补充说明

  • 内核函数 netlink_kernel_create(int unit, unsigned int groups,…) 的第二个参数,表示内核进程最多能处理的多播组个数,若该值小于

    32

    32

    32,则默认按

    32

    32

    32 处理。因此,调用该函数时,通常可将第二个参数置为

    0

    0

    0

  • struct sk_buff 结构体中的 cb[48] 字段为控制缓冲区,可供各层协议存储私有变量,Netlink 机制通过将该字段强制转换为 struct netlink_skb_parms{} 结构体,填充 Netlink 通信所需的私有信息,其固定填充模板如下:NETLINK_CB(skb).pid=xx;
    NETLINK_CB(skb).dst_group=xx;
  • 本次实验中,将 NETLINK_CB(skb).dst_group 设置为对应多播组号或

    0

    0

    0,用户空间均能收到多播消息,其底层原因需进一步查阅内核源码与深入分析 Netlink 多播机制实现原理。

  • 测试与注意事项

  • 编译完成后,需先执行 insmod 命令加载内核模块,再运行用户空间程序。若未加载 mynlkern.ko 内核模块而直接运行 ./test 5,bind() 系统调用将返回 No such file or directory 错误。
  • 部分老版本 Netlink 多播教程中提及的“先运行用户空间程序,再加载内核模块”的方式,在内核 2.6.21 版本中已不再适用,需重点注意。
  • 小结

    通过三篇内容的阐述,

  • 明确了 Netlink 的关键特性(无连接、基于 BSD Socket、支持单播/多播)、消息格式(16 字节固定头+可变负载)与编程关键要点(pid 字段取值、多播组号与掩码转换、bind() 调用的适用场景);
  • 梳理了 Netlink 编程的三个阶段(单向通信、双向通信、无 bind() 通信)与多播机制的实现流程,补充了实验注意事项与后续深入学习的方向;
  • 可对 Netlink 机制形成初步认知,并能够开发基于 Netlink 的基础应用程序。但这仅为该机制的入门内容,若要开发高质量、高效率的 Netlink 应用模块,还需进一步深入理解其底层本质,同时掌握内核编程的相关基础能力,例如临界资源的互斥保护、线程安全性保障、大数据量传输的处理策略等,这些均为实际开发中需要重点考虑的问题。


    Linux 内核与用户空间通信之 Netlink 使用方法

    HAOMCU 转载时间:2012-03-20 09:41:52

    1 简介

    本文介绍 Linux 内核中的 Netlink 通信机制,详细阐述 Netlink 的工作原理与技术优势,包括其支持的多种协议类型、异步通信机制及多播特性,并通过实例展示用户空间与内核空间的通信过程。

    Linux 中的进程间通信机制源自 Unix 平台的进程通信机制。Unix 的两大分支 AT&T Unix 和 BSD Unix 在进程通信实现机制上存在差异:前者形成适用于单台计算机的 System V IPC,后者实现基于 Socket 的进程间通信机制。同时,Linux 遵循 IEEE 制定的 Posix IPC 标准,在上述三类机制基础上实现以下主要 IPC 机制:管道(Pipe)及命名管道(Named Pipe)、信号(Signal)、消息队列(Message queue)、共享内存(Shared Memory)、信号量(Semaphore)、套接字(Socket)。借助这些 IPC 机制,用户空间进程间可完成数据交互。为实现内核空间与用户空间的通信,Linux 提供基于 Socket 的 Netlink 通信机制,可实现内核空间与用户空间之间数据的实时交互。

    本文第 2 节概述相关研究工作,第 3 节对比其他 IPC 机制,详细介绍 Netlink 机制及其关键技术,第 4 节采用 KGDB+GDB 组合调试方式,通过示例程序演示 Netlink 通信过程,第 5 节总结并指出 Netlink 通信机制的不足之处。

    2 相关研究

    截至目前,Linux 提供 9 种实现内核空间与用户空间数据交换的机制,分别为内核启动参数、模块参数与 sysfs、sysctl、系统调用、netlink、procfs、seq_file、debugfs 和 relayfs。其中,模块参数与 sysfs、procfs、debugfs、relayfs 属于基于文件系统的通信机制,主要用于内核空间向用户空间输出信息;sysctl、系统调用为用户空间发起的通信机制。由此可见,上述机制均为单工通信机制,在内核空间与用户空间的双向交互式数据交换场景下存在一定的局限性。

    Netlink 是基于 Socket 的通信机制,依托 Socket 本身具备的双向性、突发性、非阻塞特性,能够较好地满足内核空间与用户空间小量数据的实时交互需求,因此在 Linux 2.6 内核中被广泛应用。例如 SELinux 组件,以及 Linux 系统防火墙的内核态组件 netfilter 与用户态组件 iptables 之间的数据交换,均通过 Netlink 机制完成。

    3 Netlink 机制及其关键技术

    3.1 Netlink 机制

    在 Linux 操作系统中,CPU 处于内核态时可分为两种场景:存在用户上下文的状态、执行硬件中断或软件中断的状态。其中,在存在用户上下文的场景下,由于内核态与用户态的内存映射机制不同,无法直接将本地变量传递至用户态内存区域;在执行硬件中断或软件中断的场景下,代码执行过程不可中断,同样无法直接向用户内存区域传递数据。

    传统进程间通信机制均无法直接应用于内核态与用户态之间的通信,具体原因如表 1 所示:

    通信方法无法应用于内核态与用户态的原因
    管道(不含命名管道) 仅支持父子进程间的通信
    消息队列 无法在硬件中断、软件中断中无阻塞接收数据
    信号量 无法跨内核态与用户态使用
    共享内存 需信号量辅助实现同步,而信号量无法跨态使用
    套接字 无法在硬件中断、软件中断中无阻塞接收数据

    表 1(引自 参考文献 5)

    解决内核态与用户态通信问题的方案可分为两类:

  • 存在用户上下文时,可调用 Linux 提供的 copy_from_user() 和 copy_to_user() 函数完成数据传输,但这两个函数可能产生阻塞,因此无法在硬件中断、软件中断过程中调用;
  • 执行硬件中断或软件中断时,可通过以下两种方式实现: 2.1 借助 Linux 内核提供的 spinlock 自旋锁实现内核线程与中断过程的同步。由于内核线程运行在有上下文的进程环境中,因此可在内核线程中通过套接字或消息队列获取用户空间数据,再通过临界区将数据传递至中断过程; 2.2 基于 Netlink 机制实现。Netlink 套接字的通信标识通常为进程的 ID(进程标识符)。Netlink 通信的显著特征是对中断过程的良好支持:内核空间接收用户空间数据时,无需用户自行启动内核线程,而是通过软中断调用用户预先指定的接收函数。这种基于软中断的实现方式相较于自行启动内核线程,能够保障数据传输的实时性。
  • 3.2 Netlink 优势

    相较于其他通信机制,Netlink 具备以下优势:

  • 基于 Netlink 自定义新协议并加入协议族后,即可通过 Socket API 完成数据交换;而 ioctl 和 proc 文件系统需通过程序新增对应的设备或文件才能实现通信;
  • Netlink 采用 Socket 缓存队列实现异步通信,而 ioctl 为同步通信机制,若传输数据量较大,易降低系统性能;
  • Netlink 支持多播特性,归属同一 Netlink 组的模块与进程均可接收该组的多播消息;
  • Netlink 允许内核主动发起会话,而 ioctl 与系统调用仅能由用户空间进程发起。
  • 内核源码中关于 Netlink 协议的头文件定义了预定义协议类型,具体如下:

    #define NETLINK_ROUTE 0
    #define NETLINK_W1 1
    #define NETLINK_USERSOCK 2
    #define NETLINK_FIREWALL 3
    #define NETLINK_INET_DIAG 4
    #define NETLINK_NFLOG 5
    #define NETLINK_XFRM 6
    #define NETLINK_SELINUX 7
    #define NETLINK_ISCSI 8
    #define NETLINK_AUDIT 9
    #define NETLINK_FIB_LOOKUP 10
    #define NETLINK_CONNECTOR 11
    #define NETLINK_NETFILTER 12
    #define NETLINK_IP6_FW 13
    #define NETLINK_DNRTMSG 14
    #define NETLINK_KOBJECT_UEVENT 15
    #define NETLINK_GENERIC 16

    上述协议已适配不同的系统应用场景,每种应用均有专属的传输数据格式。若用户无需使用这些预定义协议,可新增自定义协议号。针对每个 Netlink 协议类型,最多可设置 32 个多播组,每个多播组由 1 个比特位标识。Netlink 的多播特性使得向同一组发送消息仅需一次系统调用,对于需传输多播消息的应用而言,可显著降低系统调用次数。

    建立 Netlink 会话的流程如下: img

    内核通过一套与标准 Socket API 相似的接口完成通信过程,首先调用 netlink_kernel_create() 创建套接字,该函数原型为:

    struct sock *netlink_kernel_create(struct net *net,
    int unit,unsigned int groups,
    void (*input)(struct sk_buff *skb),
    struct mutex *cb_mutex,
    struct module *module);

    其中,net 参数为网络设备命名空间指针;input 函数为 Netlink Socket 接收消息时触发的回调函数指针;module 参数默认值为 THIS_MODULE。

    用户空间进程则通过标准 Socket API 创建套接字,并将进程 ID 发送至内核空间。用户空间调用 socket() 创建套接字,函数原型为:

    int socket(int domain, int type, int protocol);

    其中,domain 参数取值为 PF_NETLINK(即 Netlink 协议族);protocol 参数为 Netlink 预定义协议(如 NETLINK_ROUTE、NETLINK_FIREWALL、NETLINK_ARPD、NETLINK_ROUTE6、NETLINK_IP6_FW)或用户自定义协议。

    随后调用 bind() 函数完成绑定操作。Netlink 的 bind() 函数将本地 Socket 地址(源 Socket 地址)与已打开的 Socket 关联,绑定完成且内核空间接收用户进程 ID 后,即可开展双向通信。

    用户空间进程通过标准 Socket API 中的 sendmsg() 函数发送数据,调用时需构造 struct msghdr 消息结构与 nlmsghdr 消息头。Netlink 消息体由 nlmsghdr 消息头和消息载荷(payload)组成,输入消息后,内核会访问 nlmsghdr 指向的缓冲区。

    内核空间通过独立创建的 sk_buff 缓冲区发送数据,Linux 定义以下宏用于简化缓冲区地址配置: #define \\ NETLINK_CB(skb) \\ ((struct \\ netlink_skb_parms) &((skb)->cb))$$

    缓冲区完成消息地址配置后,可调用 netlink_unicast() 发送单播消息,该函数原型为: int netlink_unicast(struct sock *sk, struct sk_buff *skb, u32 pid, int nonblock); 参数说明:

    • sk:netlink_kernel_create() 函数返回的 Socket 指针;
    • skb:存储待发送消息的缓冲区,其 data 字段指向 Netlink 消息结构,控制块保存消息地址信息(可通过 NETLINK_CB(skb) 宏配置);
    • pid:接收消息进程的进程 ID;
    • nonblock:函数是否为非阻塞模式(取值 1 时,无可用接收缓存则立即返回;取值 0 时,无可用接收缓存则进入睡眠状态)。

    内核模块或子系统可调用 netlink_broadcast() 发送广播消息,函数原型为: void netlink_broadcast(struct sock *sk, struct sk_buff *skb, u32 pid, u32 group, int allocation); 前 3 个参数与 netlink_unicast() 一致;group 为接收消息的多播组标识(需向多个多播组发送消息时,取值为多个组 ID 的按位或结果);allocation 为内核内存分配类型(常用 GFP_ATOMIC 或 GFP_KERNEL,前者适用于原子上下文(不可睡眠),后者适用于非原子上下文)。

    接收数据时,程序需申请足够大小的内存空间,以存储 Netlink 消息头与消息载荷,随后调用标准函数 recvmsg() 接收消息。

    4 Netlink 通信过程

    4.1 调试环境与工具

    调试平台:Vmware 5.5 + Fedora Core 10(两台主机,分别作为 host 机与 target 机); 调试工具:KGDB+GDB 组合(Linux 内核 2.6.26 及以上版本内置 KGDB 选项,编译内核时需启用相关配置;调试时 host 端使用带符号表的 vmlinz 内核,target 端通过 GDB 调试用户空间程序)。

    4.2 调试程序实现

    调试程序分为内核模块与用户空间程序两部分:内核模块加载后,运行用户空间程序,由用户空间发起 Netlink 会话,与内核模块完成数据交换。

    4.2.1 用户空间程序关键代码

    int send_pck_to_kern(u8 op, const u8 *data, u16 data_len)
    {
    struct user_data_ *pck;
    int ret;

    // 分配内存并初始化
    pck = (struct user_data_*)calloc(1, sizeof(*pck) + data_len);
    if(!pck) {
    printf("calloc in %s failed!!!\\n", __FUNCTION__);
    return 1;
    }

    // 填充消息结构体
    pck->magic_num = MAGIC_NUM_RNQ;
    pck->op = op;
    pck->data_len = data_len;
    memcpy(pck->data, data, data_len);

    // 发送数据至内核
    ret = send_to_kern((const u8*)pck, sizeof(*pck) + data_len);
    if(ret)
    printf("send_to_kern in %s failed!!!\\n", __FUNCTION__);

    // 释放内存
    free(pck);
    return ret ? 1 : 0;
    }

    static void recv_from_nl()
    {
    char buf[1000];
    int len;
    struct iovec iov = {buf, sizeof(buf)};
    struct sockaddr_nl sa;
    struct msghdr msg;
    struct nlmsghdr *nh;

    // 初始化消息头
    memset(&msg, 0, sizeof(msg));
    msg.msg_name = (void *)&sa;
    msg.msg_namelen = sizeof(sa);
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;

    // 接收内核消息
    len = recvmsg(nl_sock, &msg, 0);

    // 解析接收到的消息
    for (nh = (struct nlmsghdr *)buf; NLMSG_OK(nh, len); nh = NLMSG_NEXT (nh, len)) {
    // 多段消息结束标识
    if (nh->nlmsg_type == NLMSG_DONE) {
    puts("nh->nlmsg_type == NLMSG_DONE");
    return;
    }
    // 消息错误标识
    if (nh->nlmsg_type == NLMSG_ERROR) {
    puts("nh->nlmsg_type == NLMSG_ERROR");
    return;
    }

    #if 1
    // 打印从内核接收的数据
    puts("Data received from kernel:");
    hex_dump((u8*)NLMSG_DATA(nh), NLMSG_PAYLOAD(nh, 0));
    #endif
    }
    }

    4.2.2 内核模块关键代码

    内核模块需防止资源抢占,确保 Netlink 资源的互斥访问,关键代码如下:

    static void nl_rcv(struct sk_buff *skb)
    {
    // 加锁保证资源互斥访问
    mutex_lock(&nl_mtx);
    netlink_rcv_skb(skb, &nl_rcv_msg);
    mutex_unlock(&nl_mtx);
    }

    static int nl_send_msg(const u8 *data, int data_len)
    {
    struct nlmsghdr *rep;
    u8 *res;
    struct sk_buff *skb;

    // 参数合法性校验
    if(g_pid < 0 || g_nl_sk == NULL) {
    printk("Invalid parameter, g_pid = %d, g_nl_sk = %p\\n", g_pid, g_nl_sk);
    return 1;
    }

    // 分配新的sk_buff缓冲区
    skb = nlmsg_new(data_len, GFP_KERNEL);
    if(!skb) {
    printk("nlmsg_new failed!!!\\n");
    return 1;
    }

    // 调试模式下打印待发送数据
    if(g_debug_level > 0) {
    printk("Data to be send to user space:\\n");
    hex_dump((void*)data, data_len);
    }

    // 填充Netlink消息头
    rep = __nlmsg_put(skb, g_pid, 0, NLMSG_NOOP, data_len, 0);
    res = nlmsg_data(rep);
    memcpy(res, data, data_len);

    // 发送单播消息至用户空间
    netlink_unicast(g_nl_sk, skb, g_pid, MSG_DONTWAIT);
    return 0;
    }

    static int nl_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)
    {
    const u8 res_data[] = "Hello, user";
    size_t data_len;
    u8 *buf;
    struct user_data_ *pck;
    struct user_req *req, *match = NULL;

    // 获取发送方进程ID
    g_pid = NETLINK_CB(skb).pid;
    buf = (u8*)NLMSG_DATA(nlh);
    data_len = nlmsg_len(nlh);

    // 数据长度校验
    if(data_len < sizeof(struct user_data_)) {
    printk("Too short data from user space!!!\\n");
    return 1;
    }

    // 解析用户空间消息
    pck = (struct user_data_ *)buf;
    if(pck->magic_num != MAGIC_NUM_RNQ) {
    printk("Magic number not matched!!!\\n");
    return 1;
    }

    // 调试模式下打印用户空间数据
    if(g_debug_level > 0) {
    printk("Data from user space:\\n");
    hex_dump(buf, data_len);
    }

    // 匹配对应的请求处理函数
    req = user_reqs;
    while(req->op) {
    if(req->op == pck->op) {
    match = req;
    break;
    }
    req++;
    }

    // 执行匹配的处理函数
    if(match) {
    match->handler(buf, data_len);
    }

    // 向用户空间回复消息
    nl_send_msg(res_data, sizeof(res_data));
    return 0;
    }

    5 其他相关说明

    Netlink 是 Linux 特有的一种专用 Socket,功能类似于 BSD 系统中的 AF_ROUTE,但具备更丰富的功能。在 Linux 2.6.14 内核版本中,大量应用通过 Netlink 实现应用层与内核层的通信,典型场景包括:

    • 路由守护进程(NETLINK_ROUTE);
    • 1-wire 子系统(NETLINK_W1);
    • 用户态 Socket 协议(NETLINK_USERSOCK);
    • 防火墙(NETLINK_FIREWALL);
    • Socket 监控(NETLINK_INET_DIAG);
    • netfilter 日志(NETLINK_NFLOG);
    • IPsec 安全策略(NETLINK_XFRM);
    • SELinux 事件通知(NETLINK_SELINUX);
    • iSCSI 子系统(NETLINK_ISCSI);
    • 进程审计(NETLINK_AUDIT);
    • 转发信息表查询(NETLINK_FIB_LOOKUP);
    • Netlink 连接器(NETLINK_CONNECTOR);
    • netfilter 子系统(NETLINK_NETFILTER);
    • IPv6 防火墙(NETLINK_IP6_FW);
    • DECnet 路由信息(NETLINK_DNRTMSG);
    • 内核事件向用户态通知(NETLINK_KOBJECT_UEVENT);
    • 通用 Netlink(NETLINK_GENERIC)。

    Netlink 是实现内核与用户应用间双向数据传输的高效方式:用户态应用可通过标准 Socket API 调用 Netlink 的功能,内核态则需通过专用内核 API 实现 Netlink 通信。

    相较于系统调用、ioctl 及 /proc 文件系统,Netlink 具备以下优势:

  • 使用 Netlink 时,仅需在 include/linux/netlink.h 中新增 Netlink 协议定义(如 #define NETLINK_MYTEST 17),内核与用户态应用即可通过 Socket API 基于该协议完成数据交换;而新增系统调用需修改内核源码并静态编译,ioctl 需新增设备/文件,/proc 文件系统需新增文件/目录,均会增加开发成本或导致 /proc 目录结构混乱;
  • Netlink 为异步通信机制,内核与用户态应用间的消息存储于 Socket 缓存队列,发送消息仅需将其存入接收者的 Socket 接收队列,无需等待接收方确认;而系统调用与 ioctl 为同步通信机制,传输大尺寸数据时会影响系统调度粒度;
  • 基于 Netlink 的内核模块可动态加载,应用层与内核层无编译期依赖;而新增系统调用需静态链接至内核,无法通过模块实现,且应用层编译时需依赖内核头文件;
  • Netlink 支持多播特性,内核模块或应用可将消息多播至指定 Netlink 组,归属该组的所有内核模块/应用均可接收消息(内核事件向用户态通知机制即基于此特性);
  • 内核可主动发起 Netlink 会话,而系统调用与 ioctl 仅能由用户应用发起;
  • Netlink 基于标准 Socket API 实现,开发门槛低;而系统调用与 ioctl 需掌握专用开发规范,学习成本更高。
  • 用户态使用 Netlink

    用户态应用可通过标准 Socket API(socket()、bind()、sendmsg()、recvmsg()、close())便捷使用 Netlink Socket,需包含头文件 linux/netlink.h 与 sys/socket.h(Socket 基础头文件)。

    5.1 创建 Netlink Socket

    调用 socket() 函数创建 Netlink Socket,原型如下: KaTeX parse error: Expected 'EOF', got '_' at position 16: \\text{socket(AF_̲NETLINK, SOCK_R… 参数说明:

    • 第一个参数:必须为 AF_NETLINK 或 PF_NETLINK(Linux 中二者等价),标识使用 Netlink 协议族;
    • 第二个参数:必须为 SOCK_RAW 或 SOCK_DGRAM;
    • 第三个参数:指定 Netlink 协议类型(如自定义的 NETLINK_MYTEST,或通用协议 NETLINK_GENERIC)。

    内核预定义的 Netlink 协议类型如下:

    #define NETLINK_ROUTE 0
    #define NETLINK_W1 1
    #define NETLINK_USERSOCK 2
    #define NETLINK_FIREWALL 3
    #define NETLINK_INET_DIAG 4
    #define NETLINK_NFLOG 5
    #define NETLINK_XFRM 6
    #define NETLINK_SELINUX 7
    #define NETLINK_ISCSI 8
    #define NETLINK_AUDIT 9
    #define NETLINK_FIB_LOOKUP 10
    #define NETLINK_CONNECTOR 11
    #define NETLINK_NETFILTER 12
    #define NETLINK_IP6_FW 13
    #define NETLINK_DNRTMSG 14
    #define NETLINK_KOBJECT_UEVENT 15
    #define NETLINK_GENERIC 16

    每个 Netlink 协议类型最多支持 32 个多播组,每个多播组由 1 个比特位标识。Netlink 的多播特性可显著降低多播场景下的系统调用次数。

    5.2 绑定 Netlink Socket

    bind() 函数用于将已打开的 Netlink Socket 与本地 Netlink 地址绑定,Netlink Socket 地址结构定义如下:

    struct sockaddr_nl {
    sa_family_t nl_family;
    unsigned short nl_pad;
    __u32 nl_pid;
    __u32 nl_groups;
    };

    字段说明:

    • nl_family:必须设置为 AF_NETLINK 或 PF_NETLINK;
    • nl_pad:预留字段,需设置为 0;
    • nl_pid:接收/发送消息的进程 ID(内核处理/多播消息时设为 0,否则设为进程 ID;多线程场景下可自定义,如 pthread_self() << 16 | getpid());
    • nl_groups:多播组标识(设为 0 表示不加入任何多播组)。

    bind() 函数调用示例:

    bind(fd, (struct sockaddr*)&nladdr, sizeof(struct sockaddr_nl));

    其中,fd 为 socket() 函数返回的文件描述符,nladdr 为 struct sockaddr_nl 类型的地址结构体。

    5.3 发送 Netlink 消息

    向内核/其他用户态应用发送 Netlink 消息时,需构造目标 Netlink 地址、struct msghdr、struct nlmsghdr 与 struct iovec 结构体:

    5.3.1 消息头与地址配置

    struct msghdr msg;
    memset(&msg, 0, sizeof(msg));
    msg.msg_name = (void *)&(nladdr); // 目标Netlink地址
    msg.msg_namelen = sizeof(nladdr);

    5.3.2 Netlink 消息头配置

    struct nlmsghdr 为 Netlink 消息头(控制块),定义如下:

    struct nlmsghdr {
    __u32 nlmsg_len; // 消息总长度(含消息头+载荷)
    __u16 nlmsg_type; // 应用自定义消息类型(内核透传,通常设为0)
    __u16 nlmsg_flags; // 消息标志(普通场景设为0)
    __u32 nlmsg_seq; // 消息序列号(应用追踪消息用)
    __u32 nlmsg_pid; // 消息发送进程ID
    };

    消息标志(nlmsg_flags)可选值:

    #define NLM_F_REQUEST 1
    #define NLM_F_MULTI 2
    #define NLM_F_ACK 4
    #define NLM_F_ECHO 8
    #define NLM_F_ROOT 0x100
    #define NLM_F_MATCH 0x200
    #define NLM_F_ATOMIC 0x400
    #define NLM_F_DUMP (NLM_F_ROOT|NLM_F_MATCH)
    #define NLM_F_REPLACE 0x100
    #define NLM_F_EXCL 0x200
    #define NLM_F_CREATE 0x400
    #define NLM_F_APPEND 0x800

    5.3.3 消息载荷与发送示例

    #define MAX_MSGSIZE 1024
    char buffer[] = "An example message";
    struct nlmsghdr *nlhdr;

    // 分配消息缓冲区
    nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_MSGSIZE));
    // 填充消息载荷
    strcpy(NLMSG_DATA(nlhdr),buffer);
    // 设置消息长度
    nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer));
    // 设置发送进程ID
    nlhdr->nlmsg_pid = getpid();
    // 消息标志设为0
    nlhdr->nlmsg_flags = 0;

    // 构造iovec结构体
    struct iovec iov;
    iov.iov_base = (void *)nlhdr;
    iov.iov_len = nlhdr->nlmsg_len;
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;

    // 发送消息
    sendmsg(fd, &msg, 0);

    5.4 接收 Netlink 消息

    接收消息需先分配足够大的缓冲区,再调用 recvmsg() 函数,示例如下:

    #define MAX_NL_MSG_LEN 1024
    struct sockaddr_nl nladdr;
    struct msghdr msg;
    struct iovec iov;
    struct nlmsghdr * nlhdr;

    // 分配缓冲区
    nlhdr = (struct nlmsghdr *)malloc(MAX_NL_MSG_LEN);
    iov.iov_base = (void *)nlhdr;
    iov.iov_len = MAX_NL_MSG_LEN;

    // 初始化消息头
    msg.msg_name = (void *)&(nladdr);
    msg.msg_namelen = sizeof(nladdr);
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;

    // 接收消息
    recvmsg(fd, &msg, 0);

    接收完成后:

    • nlhdr 指向消息头;
    • nladdr 存储消息的目标地址;
    • NLMSG_DATA(nlhdr) 宏返回消息载荷的首地址。
    5.5 Netlink 消息处理宏

    linux/netlink.h 定义了以下常用宏,用于简化消息处理:

  • 字节对齐宏:

    #define \\ NLMSG\\_ALIGNTO \\ 4
    #define \\ NLMSG\\_ALIGN(len) \\ ( ((len)+NLMSG\\_ALIGNTO1) \\& \\sim(NLMSG\\_ALIGNTO1) )

    功能:获取不小于 len 且满足字节对齐的最小数值。

  • 消息长度计算宏:

    #define \\ NLMSG\\_LENGTH(len) \\ ((len)+NLMSG\\_ALIGN(sizeof(struct \\ nlmsghdr)))

    功能:计算载荷长度为 len 时的消息总长度(用于分配缓冲区)。

  • 缓冲区空间计算宏:

    #define \\ NLMSG\\_SPACE(len) \\ NLMSG\\_ALIGN(NLMSG\\_LENGTH(len))

    功能:返回不小于 NLMSG_LENGTH(len) 且字节对齐的最小数值(用于缓冲区分配)。

  • 载荷地址获取宏:

    #define \\ NLMSG\\_DATA(nlh) \\ ((void*)(((char*)nlh) + NLMSG\\_LENGTH(0)))

    功能:返回消息载荷的首地址。

  • 下一条消息获取宏:

    #define \\ NLMSG\\_NEXT(nlh,len) \\ ((len) -= NLMSG\\_ALIGN((nlh)->nlmsg\\_len), \\\\ (struct \\ nlmsghdr*)(((char*)(nlh)) + NLMSG\\_ALIGN((nlh)->nlmsg\\_len)))

    功能:获取下一条消息的首地址,并更新剩余消息长度。

  • 消息合法性校验宏:

    #define \\ NLMSG\\_OK(nlh,len) \\ ((len) >= (int)sizeof(struct \\ nlmsghdr) \\&\\& \\\\ (nlh)->nlmsg\\_len >= sizeof(struct \\ nlmsghdr) \\&\\& \\\\ (nlh)->nlmsg\\_len <= (len))

    功能:校验消息长度是否合法。

  • 载荷长度计算宏:

    #define \\ NLMSG\\_PAYLOAD(nlh,len) \\ ((nlh)->nlmsg\\_len NLMSG\\_SPACE((len)))

    功能:返回消息载荷的实际长度。

  • 5.6 关闭 Netlink Socket

    调用 close(fd) 函数关闭已打开的 Netlink Socket 文件描述符即可。

    Netlink 内核 API

    Netlink 的内核实现在 net/core/af_netlink.c 文件中,内核模块使用 Netlink 需包含 linux/netlink.h 头文件,并调用专用内核 API。

    6.1 新增 Netlink 协议类型

    如需新增自定义 Netlink 协议类型,需修改 linux/netlink.h,示例如下:

    #define \\ NETLINK\\_MYTEST \\ 17

    也可直接使用通用协议类型 NETLINK_GENERIC,无需新增定义。

    6.2 创建内核 Netlink Socket

    调用 netlink_kernel_create() 函数创建内核态 Netlink Socket,原型如下: KaTeX parse error: Expected 'EOF', got '_' at position 28: … sock * netlink_̲kernel_create(i… 参数说明:

    • unit:Netlink 协议类型(如 NETLINK_MYTEST);
    • input:消息处理回调函数(Socket 接收消息时触发)。
    6.3 消息处理回调函数

    回调函数有两种实现方式:

    6.3.1 直接处理消息(适用于短消息)

    void input (struct sock *sk, int len) {
    struct sk_buff *skb;
    struct nlmsghdr *nlh = NULL;
    u8 *data = NULL;
    while ((skb = skb_dequeue(&sk->receive_queue)) != NULL) {
    nlh = (struct nlmsghdr *)skb->data;
    data = NLMSG_DATA(nlh);
    // 处理消息载荷
    }
    }

    6.3.2 唤醒内核线程处理(适用于长消息)

    void input (struct sock *sk, int len) {
    // 唤醒等待队列中的内核线程
    wake_up_interruptible(sk->sk_sleep);
    }

    内核线程可通过 skb_recv_datagram(nl_sk) 接收消息(无消息时进入睡眠状态)。

    6.4 内核态发送消息
    6.4.1 地址配置

    通过 NETLINK_CB(skb) 宏配置消息地址(源/目标):

    NETLINK_CB(skb).pid = 0; // 源地址(内核设为0)
    NETLINK_CB(skb).dst_pid = 0; // 目标进程ID(内核/多播设为0)
    NETLINK_CB(skb).dst_group = 1; // 目标多播组(单播设为0)

    6.4.2 发送单播消息

    调用 netlink_unicast() 函数(原型见 3.2 节)。

    6.4.3 发送广播消息

    调用 netlink_broadcast() 函数(原型见 3.2 节)。

    6.5 释放内核 Netlink Socket

    调用 sock_release() 函数释放 Socket,示例如下: KaTeX parse error: Expected 'EOF', got '_' at position 11: \\text{sock_̲release(sk->sk_… 其中,sk 为 netlink_kernel_create() 函数的返回值。

    6.6 示例程序说明

    配套示例程序包含:

    • 内核模块:netlink-exam-kern.c;
    • 用户态程序:netlink-exam-user-recv.c(接收)、netlink-exam-user-send.c(发送)。

    运行流程:

  • 加载内核模块;
  • 终端 1 运行接收程序;
  • 终端 2 运行发送程序(读取指定文本文件,通过 Netlink 发送至内核);
  • 内核模块接收消息后,通过 /proc/netlink_exam_buffer 暴露数据,并将消息回传给接收程序;
  • 接收程序打印消息内容至屏幕。
  • 总结

  • Netlink 是 Linux 特有的基于 Socket 的双向异步通信机制,支持多播、内核主动发起会话,适配内核态与用户态的通信需求,在 2.6 及以上内核版本中广泛应用;
  • 用户态通过标准 Socket API(socket()/bind()/sendmsg()/recvmsg())使用 Netlink,内核态需调用专用 API(netlink_kernel_create()/netlink_unicast() 等);
  • Netlink 消息由 nlmsghdr 头和载荷组成,可通过内核预定义宏简化消息长度计算、载荷解析等操作,相较于系统调用、ioctl 等机制具备开发成本低、性能优、灵活性高的特点。

  • Linux 内核 netlink 机制 – 用户空间和内核空间数据传输

    hinewcc 原创已于 2024-12-31 09:49:56 修改

    1 简介

    Netlink socket 是 Linux 系统特有的套接字类型,是实现用户空间与内核空间进程间通信(Inter-Process Communication, IPC)的专用机制,同时也是网络应用程序与内核进行交互的主流接口。 Netlink 为内核与用户态应用之间的双向数据传输提供了高效的实现方式:用户态应用可通过标准的 socket API 调用 Netlink 提供的功能,而内核态需借助专用的内核 API 完成 Netlink 相关操作。Netlink 接口分为应用层接口与内核接口两类,在实际开发中,需于应用程序侧实现策略逻辑,并在内核侧实现底层机制。

    用户空间与内核空间的常用通信方式包含以下三类:proc 文件系统、ioctl 调用、Netlink 机制: (1)/proc 文件系统 – 单向通信:作为虚拟文件系统,主要用于用户空间从内核获取信息、输出数据; (2)ioctl 调用 – 单向通信:不支持异步信息发送,仅用于用户空间向内核传递控制命令; (3)Netlink 机制 – 双向通信:内核可主动发起数据传输,而非仅响应用户空间请求并返回信息。

    1.1 netlink 机制的优势

    • 支持全双工、异步通信模式;
    • 用户空间可直接使用标准 socket 接口完成通信操作;
    • 具备多播通信能力;
    • 内核端可在进程上下文与中断上下文环境中使用。

    1.2 netlink 机制的典型应用场景

    • 获取或修改系统路由信息;
    • 监听 TCP 协议数据报文;
    • 防火墙功能实现;
    • netfilter 子系统交互;
    • 内核事件向用户态的主动通知。

    2 netlink 常用数据结构及函数

    2.1 netlink 应用层数据结构及函数

    在网络编程中,通常通过 IP 地址 + 端口号 完成寻址操作;而 netlink 机制则通过 协议类型 + 进程 ID 实现寻址,其中 协议类型 需在调用 socket 接口 时指定。

    2.1.1 消息结构

    netlink 消息由消息头与消息体两部分组成:消息头固定占用 16 字节,其后紧跟消息体(有效数据)。

    img

    • Message Length:消息总长度,包含 netlink 消息头在内的全部字节数,占用 4 字节;
    • Type:消息类型,多用于协议交互流程,内核定义了若干标准消息类型,且已基于 netlink 实现多种协议,每种协议对应特定的类型与功能,占用 2 字节;
    • Flags:消息标志位,占用 2 字节;
    • Sequence number:序列号,为可选字段,功能类比 TCP 协议中的报文序号,占用 4 字节;
    • PID(port ID):端口标识,即发送端的端口 ID 号,占用 4 字节。
    消息头 nlmsghdr 结构体定义:

    struct nlmsghdr {
    __u32 nlmsg_len; /* 包括 netlink 消息头在内,整个消息的总长度 */
    __u16 nlmsg_type; /* 消息类型 */
    __u16 nlmsg_flags; /* 消息标志位 */
    __u32 nlmsg_seq; /* 消息报文的序列号 */
    __u32 nlmsg_pid; /* 发送端口的 ID 号:内核侧该值为 0,用户进程侧为其 socket 绑定的 ID 号 */
    };

    2.1.2 netlink 通信地址 struct sockaddr_nl

    struct sockaddr_nl 是 netlink 机制的通信地址结构体,功能类比普通 socket 编程中的 struct sockaddr_in 结构体:

    struct sockaddr_nl {
    __kernel_sa_family_t nl_family; /* 地址族,固定为 AF_NETLINK */
    unsigned short nl_pad; /* 预留填充字段,暂未使用,需置为 0 */
    __u32 nl_pid; /* 端口 ID(通信端口号),0 表示目标为内核 */
    __u32 nl_groups; /* 多播组掩码 */
    };

    nl_groups 为多播组的地址掩码(非多播组编号),可参考内核源码文件 net/netlink/af_netlink.c 中的如下函数:

    static u32 netlink_group_mask(u32 group)
    {
    return group ? 1 << (group 1) : 0;
    }

    即用户空间代码中,若需加入多播组 1,需将 nl_groups 设为 1;多播组 2 对应的掩码为 2;多播组 3 对应的掩码为 4,依此类推。nl_groups 为 0 时,表示不加入任何多播组。

    nl_groups 多播组掩码为 32 位整型,每 1 个比特位对应 1 个多播组,因此每种 Netlink 协议最多支持 32 个多播组。用户空间进程若关注某一多播组,可加入该组;当内核空间进程向该组发送多播消息时,所有已加入该组的用户进程均可接收此消息。

    2.2 netlink 内核层数据结构及函数

    2.2.1 常用宏定义

    netlink 消息类型及常用操作宏定义如下:

    /******************** netlink 消息类型 ********************/
    #define NETLINK_ROUTE 0 /* 路由/设备钩子函数接口 */
    #define NETLINK_UNUSED 1 /* 未使用编号 */
    #define NETLINK_USERSOCK 2 /* 为用户态 socket 协议预留 */
    #define NETLINK_FIREWALL 3 /* 未使用编号,原用于 ip_queue 模块 */
    #define NETLINK_SOCK_DIAG 4 /* socket 监控接口 */
    #define NETLINK_NFLOG 5 /* netfilter/iptables ULOG 模块 */
    #define NETLINK_XFRM 6 /* IPsec 协议相关 */
    #define NETLINK_SELINUX 7 /* SELinux 事件通知 */
    #define NETLINK_ISCSI 8 /* Open-iSCSI 协议相关 */
    #define NETLINK_AUDIT 9 /* 审计功能相关 */
    #define NETLINK_FIB_LOOKUP 10 /* FIB 查表功能相关 */
    #define NETLINK_CONNECTOR 11 /* 连接器子系统相关 */
    #define NETLINK_NETFILTER 12 /* netfilter 子系统 */
    #define NETLINK_IP6_FW 13 /* IPv6 防火墙相关 */
    #define NETLINK_DNRTMSG 14 /* DECnet 路由消息 */
    #define NETLINK_KOBJECT_UEVENT 15 /* 内核向用户空间发送的对象事件消息 */
    #define NETLINK_GENERIC 16 /* 通用型 netlink 协议 */
    /* 为 NETLINK_DM(DM 事件)预留空间 */
    #define NETLINK_SCSITRANSPORT 18 /* SCSI 传输层相关 */
    #define NETLINK_ECRYPTFS 19 /* ECRYPTFS 加密文件系统相关 */
    #define NETLINK_RDMA 20 /* RDMA 协议相关 */
    #define NETLINK_CRYPTO 21 /* 加密层相关 */
    #define NETLINK_INET_DIAG NETLINK_SOCK_DIAG /* 网络套接字诊断 */
    #define MAX_LINKS 32 /* 最大支持的 netlink 链路数 */

    /******************** netlink 常用宏 ********************/
    #define NLMSG_ALIGNTO 4U /* 字节对齐基准值 */
    /* 计算不小于 len 且满足字节对齐的最小数值 */
    #define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO1) & ~(NLMSG_ALIGNTO1) )
    /* Netlink 消息头长度 */
    #define NLMSG_HDRLEN ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))
    /* 计算包含消息头的消息总长度(消息体长度 + 消息头长度)*/
    #define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)
    /* 返回不小于 NLMSG_LENGTH(len) 且满足字节对齐的最小数值 */
    #define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
    /* 获取消息体数据部分的首地址,读写消息数据时调用 */
    #define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
    /* 获取下一个消息的首地址,同时更新 len 为剩余消息长度 */
    #define NLMSG_NEXT(nlh,len) ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \\
    (struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))

    /* 判断消息长度是否合法 */
    #define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \\
    (nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \\
    (nlh)->nlmsg_len <= (len))

    /* 计算消息载荷(payload)的长度 */
    #define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len NLMSG_SPACE((len)))

    2.2.2 常用函数
    (1)netlink_kernel_create 函数

    函数原型:

    static inline struct sock *
    netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)

    功能:创建内核态 socket,用于实现与用户态的通信。 参数说明:

    • net:指向网络命名空间(namespace)的指针,默认场景下传入全局变量 &init_net(无需额外定义);
    • unit:netlink 协议类型,如 NETLINK_TEST、NETLINK_SELINUX 等;
    • cfg:存放 netlink 内核配置参数的结构体(定义如下)。

    struct netlink_kernel_cfg {
    unsigned int groups; // 该协议类型支持的最大多播组数量,若小于 32 则默认按 32 处理,通常置为 0
    unsigned int flags; // 配置标志位
    void (*input)(struct sk_buff *skb); // 消息接收回调函数
    struct mutex *cb_mutex; // 回调函数互斥锁
    int (*bind)(struct net *net, int group); // 绑定回调函数
    void (*unbind)(struct net *net, int group); // 解绑回调函数
    bool (*compare)(struct net *net, struct sock *sk); // 比较函数
    };

    (2)单播函数 netlink_unicast()

    函数原型:

    int netlink_unicast(struct sock *ssk, struct sk_buff *skb, __u32 portid, int nonblock)

    功能:向指定端口发送单播消息。 参数说明:

    • ssk:netlink socket 指针(netlink_kernel_create 函数的返回值);
    • skb:内核套接字缓冲区(sk_buff)指针;
    • portid:目标通信端口号(接收消息的进程 PID);
    • nonblock:非阻塞标志位,1 表示无可用接收缓存时立即返回,0 表示无可用接收缓存时睡眠等待。
    (3)多播函数 netlink_broadcast()

    函数原型:

    int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, __u32 portid,
    __u32 group, gfp_t allocation)

    功能:向指定多播组发送多播消息。 参数说明:

    • ssk:netlink socket 指针(netlink_kernel_create 函数的返回值);
    • skb:内核套接字缓冲区(sk_buff)指针;
    • portid:发送端端口 ID;
    • group:目标多播组掩码的按位或结果;
    • allocation:内核内存分配方式标识,中断上下文通常使用 GFP_ATOMIC,其他场景使用 GFP_KERNEL;该参数存在的原因是该 API 可能需要分配缓冲区以克隆多播消息。

    3 代码实例

    以下代码示例分别实现用户态与内核态的 netlink 通信逻辑。

    3.1 用户态 netlink 程序

    3.1.1 netlink 应用层编程基本步骤
    (1)创建套接字

    函数原型:

    int socket(int domain, int type, int protocol)

    参数说明:

    • domain:协议族,netlink 机制中固定为 AF_NETLINK;
    • type:套接字类型,netlink 机制中固定为 SOCK_RAW;
    • protocol:协议类型,可使用内核预定义协议或自定义协议。

    自定义协议示例:

    #define NETLINK_TEST 23 // 自定义 netlink 协议类型
    ......
    int skfd = 0;
    skfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);

    (2)绑定本地地址到套接字

    函数原型:

    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) // 地址绑定
    /* sockaddr_nl 是 netlink 专用地址结构体,区别于网络编程的通用地址结构 */
    struct sockaddr_nl {
    __kernel_sa_family_t nl_family; // 地址族,固定为 AF_NETLINK
    unsigned short nl_pad; // 填充字段,无需赋值
    __u32 nl_pid; // 与内核通信的进程 PID,0 表示目标为内核
    __u32 nl_groups; // 多播组地址掩码,netlink 支持多播通信
    };

    绑定示例:

    struct sockaddr_nl nlsrc_addr = {0};
    /* 初始化本地 socket 地址 */
    nlsrc_addr.nl_family = AF_NETLINK;
    nlsrc_addr.nl_pid = getpid();
    nlsrc_addr.nl_groups = 0;
    /* 执行地址绑定 */
    if(bind(skfd, (struct sockaddr*)&nlsrc_addr, addr_len) != 0)
    {
    printf("bind addr error\\n");
    return 1;
    }

    (3)构造通信消息
    (4)发送/接收消息

    netlink 机制提供两套收发消息的接口:sendto/recvfrom、sendmsg/recvmsg。核心接口定义如下:

    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);

    3.1.2 netlink 用户空间完整代码

    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/socket.h>
    #include <string.h>
    #include <linux/netlink.h>
    #define NETLINK_TEST 17
    #define RX_BUF_SIZE 100 // 接收缓冲区大小
    #define MAX_PLOAD 100 // 发送消息内存空间大小

    // 定义接收内核消息的结构体
    typedef struct
    {
    struct nlmsghdr hdr;
    char msg[RX_BUF_SIZE];
    }RX_KERNEL_MSG;

    int main(int argc, char* argv[])
    {
    char *data = "This message is from user's space";
    // 初始化变量
    struct sockaddr_nl src_addr, dest_addr; // netlink 地址结构体
    int skfd, ret, rxlen = sizeof(struct sockaddr_nl);
    struct nlmsghdr *message;
    RX_KERNEL_MSG info;
    char *retval;

    /* 1. 创建 NETLINK 套接字 */
    skfd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
    if(skfd < 0){
    printf("can not create a netlink socket\\n");
    return 1;
    }

    // 初始化 netlink 源地址
    memset(&src_addr, 0, sizeof(src_addr));
    src_addr.nl_family = AF_NETLINK;
    src_addr.nl_pid = getpid(); // 源端口为当前进程 PID
    src_addr.nl_groups = 0; // 不加入任何多播组

    // 绑定套接字到源地址
    if(bind(skfd, (struct sockaddr *)&src_addr, sizeof(src_addr)) != 0){
    printf("bind() error\\n");
    return 1;
    }

    // 初始化 netlink 目标地址
    memset(&dest_addr, 0, sizeof(dest_addr));
    dest_addr.nl_family = AF_NETLINK;
    dest_addr.nl_pid = 0; // 目标端口为内核(PID = 0)
    dest_addr.nl_groups = 0; // 多播组掩码置 0

    /* 2. 构造通信消息 */
    message = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PLOAD));
    memset(message, '\\0', sizeof(struct nlmsghdr));
    message->nlmsg_len = NLMSG_SPACE(strlen(data)); // 消息总长度(含消息头)
    message->nlmsg_flags = 0; // 消息标志位置 0
    message->nlmsg_type = 0; // 消息类型置 0
    message->nlmsg_seq = 0; // 消息序列号置 0
    message->nlmsg_pid = src_addr.nl_pid; // 消息中携带源端口 PID

    // 拷贝有效数据到消息体
    retval = memcpy(NLMSG_DATA(message), data, strlen(data));

    /* 3. 发送消息到内核 */
    ret = sendto(skfd, message, message->nlmsg_len, 0,(struct sockaddr *)&dest_addr, sizeof(dest_addr));
    if(!ret){
    perror("send pid:");
    exit(1);
    }

    /* 4. 接收内核返回的响应消息 */
    ret = recvfrom(skfd, &info, sizeof(RX_KERNEL_MSG), 0, (struct sockaddr*)&dest_addr, &rxlen);
    if(!ret){
    perror("recv form kerner:");
    exit(1);
    }
    printf("User Receive ACK from kernel:%s\\r\\n",(char *)info.msg);

    // 释放资源并关闭套接字
    close(skfd);
    free((void *)message);
    return 0;
    }

    3.2 内核空间 netlink 代码

    3.2.1 netlink 内核态编程基本步骤

    内核侧 netlink 编程接口与应用层逻辑相似,核心步骤如下:

    (1)创建 socket 并注册回调函数

    // netlink 内核配置结构体
    struct netlink_kernel_cfg {
    unsigned int groups;
    unsigned int flags;
    void (*input)(struct sk_buff *skb); /* 消息接收回调函数 */
    struct mutex *cb_mutex;
    int (*bind)(struct net *net, int group);
    void (*unbind)(struct net *net, int group);
    bool (*compare)(struct net *net, struct sock *sk);
    };

    /* 创建内核 netlink socket,参数 net 通常传入 &init_net */
    static inline struct sock *netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
    /* 释放内核 netlink socket */
    void netlink_kernel_release(struct sock *sk);

    (2)构造通信消息

    // sk_buff 是内核中 netlink 消息的载体,以下为其分配、引用、释放接口
    static inline struct sk_buff *alloc_skb(unsigned int size, gfp_t priority)
    static inline struct sk_buff *skb_get(struct sk_buff *skb)
    void kfree_skb(struct sk_buff *skb);

    // 构造 netlink 消息头的接口
    static inline struct nlmsghdr *nlmsg_put(struct sk_buff *skb, u32 portid, u32 seq, int type, int payload, int flags)

    典型调用示例:

    // NLMSG_SPACE 计算包含消息头的消息总长度
    size_t size = max(NLMSG_SPACE(message_size), (size_t)NLMSG_GOODSIZE);
    // 分配 sk_buff 内存空间
    struct sk_buff * log_skb = alloc_skb(size, GFP_ATOMIC);
    // 构造消息头
    struct nlmsghdr *nlh = nlmsg_put(log_skb, /*pid*/0, /*seq*/0, type,
    message_size, 0);
    // 拷贝有效数据到消息体
    if(payload != NULL) {
    memcpy(nlmsg_data(nlh), payload, size);
    }

    (3)发送/接收消息

    内核提供单播、多播两类消息发送接口,可根据业务场景选择:

    int netlink_unicast(struct sock *ssk, struct sk_buff *skb, __u32 portid, int nonblock);
    int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, __u32 portid, __u32 group, gfp_t allocation);

    3.2.2 netlink 内核态模块完整代码

    #include <linux/kernel.h>
    #include <linux/module.h>
    #include <linux/types.h>
    #include <linux/sched.h>
    #include <net/sock.h>
    #include <linux/netlink.h>

    #define NETLINK_TEST 17 // 与用户态一致的自定义协议类型

    // 存储用户进程 PID 的结构体
    struct {
    __u32 pid;
    }user_process;

    static struct sock *netlinkfd = NULL; // netlink socket 指针

    /* 向用户空间发送消息的函数 */
    int send_to_user(int _pid, char *pbuf, uint16_t len)
    {
    struct sk_buff *nl_skb;
    struct nlmsghdr *nlh;
    int ret;

    /* 分配 sk_buff 内存空间 */
    nl_skb = nlmsg_new(len, GFP_ATOMIC);
    if(!nl_skb)
    {
    printk("netlink alloc failure\\n");
    return 1;
    }

    /* 构造 netlink 消息头 */
    nlh = nlmsg_put(nl_skb, 0, 0, NETLINK_TEST, len, 0);
    if(nlh == NULL)
    {
    printk("nlmsg_put failure \\n");
    nlmsg_free(nl_skb);
    return 1;
    }

    /* 拷贝数据到消息体 */
    memcpy(nlmsg_data(nlh), pbuf, len);

    // 发送单播消息到用户进程
    ret = netlink_unicast(netlinkfd, nl_skb, _pid, MSG_DONTWAIT);
    return ret;
    }

    /* 消息接收回调函数 */
    static void netlink_rcv_msg(struct sk_buff *skb)
    {
    struct nlmsghdr *nlh = NULL;
    char *data = NULL;
    char *kmsg = "hello users!!!";

    // 获取消息头
    nlh = nlmsg_hdr(skb);
    // 校验消息长度合法性
    if(skb->len >= NLMSG_SPACE(0))
    {
    data = NLMSG_DATA(nlh); // 提取消息体数据
    if (data)
    {
    // 记录发送端用户进程 PID
    user_process.pid = nlh->nlmsg_pid;
    printk("kernel recv from user pid %d: %s\\n", user_process.pid, data);
    // 向用户进程发送响应消息
    send_to_user(user_process.pid, kmsg, strlen(kmsg));
    }
    } else {
    printk("%s: error skb, length:%d\\n", __func__, skb->len);
    }
    }

    // 定义 netlink 内核配置
    struct netlink_kernel_cfg cfg = {
    .input = netlink_rcv_msg, /* 注册消息接收回调函数 */
    .groups = 0, // 多播组数量置 0
    .flags = 0,
    .cb_mutex = NULL,
    .bind = NULL,
    .compare = NULL,
    };

    // 模块初始化函数
    int __init test_netlink_init(void)
    {
    /* 创建 netlink socket */
    netlinkfd = (struct sock *)netlink_kernel_create(&init_net, NETLINK_TEST, &cfg);
    if(netlinkfd == NULL){
    printk(KERN_ERR "can not create a netlink socket\\n");
    return 1;
    }
    return 0;
    }

    // 模块退出函数
    void __exit test_netlink_exit(void)
    {
    if (netlinkfd){
    netlink_kernel_release(netlinkfd); /* 释放 netlink socket */
    netlinkfd = NULL;
    }
    printk(KERN_DEBUG "test_netlink_exit!!\\n");
    }

    module_init(test_netlink_init);
    module_exit(test_netlink_exit);
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("donga");

    3.3 测试结果

    测试代码下载链接:https://download.csdn.net/download/hinewcc/89590914

    将内核态代码编译为 ko 模块,用户态代码编译为可执行程序;通过 insmod 命令加载 ko 模块后运行用户态程序:

    $ insmod netlink_test.ko # 加载内核模块

    $ ./netlink_app # 运行用户态应用程序

    img

    内核态可接收用户态发送的数据,并向用户空间返回 “hello users!!!” 响应消息。

    3.4 netlink 状态查看命令

    通过如下命令可查看系统中 netlink 接口的 ID 及状态:

    $ cat /proc/net/netlink

    img


    总结

  • netlink 是 Linux 内核与用户空间的双向通信机制,相比 proc、ioctl 具备全双工、异步、多播等特性,是网络类内核交互的主流方式;
  • netlink 消息由 16 字节固定长度的消息头(nlmsghdr)和可变长度的消息体组成,寻址方式为「协议类型 + 进程 PID」,区别于传统网络编程的「IP 地址 + 端口号」;
  • 内核态需通过 netlink_kernel_create 创建 socket 并注册回调函数,用户态可直接使用标准 socket API,核心交互接口包括 sendto/recvfrom(单播)、netlink_broadcast(多播)等。

  • via:

    • 用户空间和内核空间通讯之【Netlink 上】-wjlkoorey258-ChinaUnix博客 http://blog.chinaunix.net/uid-23069658-id-3400761.html
    • 用户空间和内核空间通讯之【Netlink 中】-wjlkoorey258-ChinaUnix博客 http://blog.chinaunix.net/uid-23069658-id-3405954.html
    • 用户空间和内核空间通讯之【Netlink 下】-wjlkoorey258-ChinaUnix博客 http://blog.chinaunix.net/uid-23069658-id-3409786.html
    • linux 内核与用户空间通信之netlink使用方法-CSDN博客 https://blog.csdn.net/HAOMCU/article/details/7371835
    • Linux内核netlink机制 – 用户空间和内核空间数据传输-CSDN博客 https://blog.csdn.net/hinewcc/article/details/139156381
    • linux netlink通信机制 – zhangwju – 博客园 2017 https://www.cnblogs.com/wenqiang/p/6306727.html
    • Netlink 手册 — Linux 内核文档 – Linux 内核 https://linuxkernel.org.cn/doc/html/latest/userspace-api/netlink/index.html
    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Linux 用户空间与内核空间的 Netlink 通信机制及实现
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!