当前位置: 首页 > article >正文

NetLink内核套接字案例分析

一、基础知识

        Netlink 是 Linux 系统中一种内核与用户空间通信的高效机制,而 Netlink 消息是这种通信的核心载体。它允许用户态程序(如网络配置工具、监控工具)与内核子系统(如网络协议栈、设备驱动)交换数据,例如获取网络接口信息、配置路由表、接收内核事件通知等。


Netlink 消息的组成

一个完整的 Netlink 消息由两部分构成:

  1. 消息头(struct nlmsghdr
    定义消息的元信息,例如消息类型、长度、序列号等。

    struct nlmsghdr {
        __u32 nlmsg_len;    // 消息总长度(头部 + 数据)
        __u16 nlmsg_type;   // 消息类型(如请求、响应、错误)
        __u16 nlmsg_flags;  // 标志位(如请求标志、多部分消息标志)
        __u32 nlmsg_seq;    // 序列号(用于匹配请求和响应)
        __u32 nlmsg_pid;    // 发送方端口ID(通常为进程ID)
    };
  2. 消息体(Payload)
    具体的数据内容,格式由消息类型决定。例如:

    • 路由消息:struct rtgenmsg(指定地址族)

    • 接口信息:struct ifinfomsg(接口索引、状态等)

    • 属性列表:动态附加的属性(如接口名称、MAC地址等)。


Netlink 消息的作用

Netlink 消息的核心功能是双向通信

1. 用户空间 → 内核

用户程序通过发送 Netlink 消息向内核发起操作请求。例如:

  • 查询信息RTM_GETLINK(获取网络接口列表)、RTM_GETROUTE(获取路由表)。

  • 配置内核RTM_NEWLINK(创建新接口)、RTM_SETLINK(修改接口属性)。

2. 内核 → 用户空间

内核通过 Netlink 消息主动通知用户程序事件。例如:

  • 接口状态变化:网络接口启用/禁用。

  • 新设备插入:USB 设备连接、Wi-Fi 网络扫描结果。

  • 路由表更新:路由条目添加或删除。


为什么用 Netlink?

与其他内核通信方式相比,Netlink 的优势在于:

机制特点适用场景
Netlink双向、异步、支持多播、结构化数据、可扩展动态配置和实时事件通知
Sysfs通过文件系统操作(/sys),读写简单但效率低静态配置(如设置参数)
Procfs通过文件系统(/proc),主要用于状态查询读取系统信息(如进程状态)
ioctl通过设备文件操作,接口不统一,扩展性差设备驱动特定操作
Netlink 的独特优势
  1. 结构化数据
    消息通过二进制格式传递,避免了文本解析(如 procfs/sysfs)的开销。

  2. 异步通信
    支持非阻塞通信,用户程序无需等待内核响应。

  3. 多播支持
    内核可以向多个用户进程广播事件(如接口状态变化)。

  4. 可扩展性
    通过消息类型(nlmsg_type)和属性(struct rtattr)灵活扩展功能。


Netlink 消息的工作流程

以获取网络接口列表为例:

  1. 用户程序构造请求消息

    • 设置 nlmsghdrnlmsg_type = RTM_GETLINKnlmsg_flags = NLM_F_DUMP

    • 设置 rtgenmsgrtgen_family = AF_UNSPEC(获取所有接口)。

  2. 发送消息到内核
    通过 sendmsg 系统调用发送 Netlink 消息。

  3. 内核处理请求
    路由子系统解析消息,收集所有网络接口信息,封装为多个 Netlink 消息(可能分片)。

  4. 用户程序接收响应
    通过 recvmsg 读取消息,解析 nlmsghdr 和消息体,提取接口名称、状态等数据。


典型应用场景

  1. 网络配置工具
    iproute2 工具集(如 ip linkip route)底层使用 Netlink 配置网络。

  1. 设备监控
    监听内核事件,如接口状态变化、新设备连接。

  2. 防火墙和策略路由
    配置 netfilter(iptables/nftables)规则或复杂路由策略。

  3. 容器网络
    容器运行时(如 Docker)通过 Netlink 管理虚拟网络设备。

 

1.nlinterfaces.c

// 程序功能:应用Netlink套接字从Linux内核打印输出所有网络接口名称
#include <bits/types/struct_iovec.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>  //Netlink协议相关定义
#include <linux/rtnetlink.h> // 路由相关的Netlink消息定义

#define BUFSIZE 10240

//定义一个自定义结构体ln_request_s,它包含一个 Netlink 消息头nlmsghdr和一个路由通用消息结构体rtgenmsg
struct In_request_s{
    //Netlink消息头
    struct nlmsghdr hdr;
    //路由消息通用结构,指定地址族
    struct rtgenmsg gen;
};

//功能:解析并打印网络接口信息
void rtnl_print_link(struct nlmsghdr *h){
    //struct ifinfomsg *iface:指向 Netlink 消息中包含的网络接口信息结构体
    struct ifinfomsg *iface;
    //struct rtattr *attr:指向路由属性结构体
    struct  rtattr *attr;
    int len = 0;

    //获取 Netlink 消息中实际的数据部分,计算方式:消息头地址 + 头部大小
    iface = NLMSG_DATA(h);
    //获取 Netlink 消息中有效负载的长度
    len = RTM_PAYLOAD(h);

    //遍历路由属性
    for(attr = IFLA_RTA(iface);RTA_OK(attr,len);attr = RTA_NEXT(attr,len)){
        switch (attr->rta_type){
            //如果属性是接口名称就打印
            case IFLA_IFNAME:
                printf("接口名称%d : %s\n", iface->ifi_index, (char *)RTA_DATA(attr));
                break;
            default:
                break;
        }
    }
}

int main(int argc,char *argv[]){
    //Netlink地址结构,用于绑定套接字
    struct sockaddr_nl nkernel;
    //消息头结构,用于 sendmsg 和 recvmsg
    struct msghdr msg;
    //分散、聚集I/O结构,用于消息传输,主要跟readv、writev等缓冲区合并有关,用于一次I/O操作处理多个缓冲区
    struct iovec io;
    //自定义请求结构
    struct In_request_s req;
    //s为套接字描述符,end为循环结束标志
    int s = -1, end = 0, ret;
    //接收缓冲区
    char buf[BUFSIZE];

    //初始化Netlink地址结构
    memset(&nkernel,0,sizeof(nkernel));
    nkernel.nl_family = AF_NETLINK;    //这里与内核通信所以不使用AF_INET
    nkernel.nl_groups = 0; // 不加入任何组播组

    //创建套接字
    /*
    AF_NETLINK: 使用 Netlink 协议族。
    SOCK_RAW: 原始套接字类型,允许直接操作 Netlink 消息。
    NETLINK_ROUTE: 路由子系统,用于获取网络接口和路由信息。
    */
    if ((s = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) < 0){
        printf("创建Netlink套接字失败.\n");
        exit(EXIT_FAILURE);
    }

    //构造Netlink请求消息
    memset(&req, 0, sizeof(req));
    //#define NLMSG_LENGTH(len) ((len) + NLMSG_ALIGN(sizeof(struct nlmsghdr)))
    //nlmsg_len: 消息总长度(头部 + rtgenmsg 结构体),通过 NLMSG_LENGTH 计算对齐后的长度
    req.hdr.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtgenmsg));
    //请求获取网络接口信息
    req.hdr.nlmsg_type = RTM_GETLINK;
    //标志为请求消息,并要求返回所有条目
    req.hdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
    //序列号,用于匹配请求和响应
    req.hdr.nlmsg_seq = 1;
    //发送方进程 ID
    req.hdr.nlmsg_pid = getpid();
    //指定地址族为 IPv4(可改为 AF_UNSPEC 获取所有接口)
    req.gen.rtgen_family = AF_INET;

    //设置I/O向量和消息头
    memset(&io, 0, sizeof(io));
    io.iov_base = &req;
    io.iov_len = req.hdr.nlmsg_len;

    memset(&msg, 0, sizeof(msg));
    msg.msg_iov = &io;          // 指向 I/O 向量
    msg.msg_iovlen = 1;         // 向量数量为 1
    msg.msg_name = &nkernel;    // 目标地址(内核)
    msg.msg_namelen = sizeof(nkernel);

    //发送请求消息
    if ((ret = sendmsg(s, &msg, 0)) < 0) {
        perror("发送消息失败");
        close(s);
        exit(EXIT_FAILURE);
    }

    //接收并解析内核响应,,当接收到 NLMSG_DONE 消息时,end 会被置为 1,从而退出循环
    while(!end){
        //定义一个指向 nlmsghdr 结构体的指针 msg_ptr,用于遍历接收到的 Netlink 消息
        struct nlmsghdr *msg_ptr;
        //用于记录还未处理的消息长度
        int remaining_len;

        memset(buf, 0, BUFSIZE);
        io.iov_base = buf;
        io.iov_len = BUFSIZE;

        if ((ret = recvmsg(s, &msg, 0)) < 0) {
            if (errno == EINTR) continue;  // 处理中断
            perror("接收消息失败");
            close(s);
            exit(EXIT_FAILURE);
        }

        // 处理消息分片(NLMSG_TRUNC标志)
        if (msg.msg_flags & MSG_TRUNC) {
            fprintf(stderr, "警告:消息被截断,考虑增大缓冲区\n");
        }
        //将 msg_ptr 指针指向接收缓冲区 buf 的起始位置,将其视为第一个 Netlink 消息的头部
        msg_ptr = (struct nlmsghdr *)buf;
        //将 remaining_len 初始化为接收到的消息总长度 ret
        remaining_len = ret;

        for (; NLMSG_OK(msg_ptr, remaining_len);   //NLMSG_OK(msg_ptr, remaining_len):这是一个宏,用于检查 msg_ptr 指向的 Netlink 消息是否有效,即消息长度是否足够且未超出剩余未处理的消息长度
            msg_ptr = NLMSG_NEXT(msg_ptr, remaining_len)) {  //将 msg_ptr 指针移动到下一个 Netlink 消息的头部,并更新 remaining_len 的值
            //内核在回复单播请求时,会将 nlmsg_pid 设置为用户进程的 PID(即 self_pid)
            if (msg_ptr->nlmsg_pid != getpid()) {
                fprintf(stderr, "收到非本进程的消息,已忽略 (PID: %u)\n", msg_ptr->nlmsg_pid);
                continue;
            }   

            switch (msg_ptr->nlmsg_type) {
                case NLMSG_ERROR: {  //如果消息类型为 NLMSG_ERROR,表示内核返回了错误信息
                    struct nlmsgerr *err = NLMSG_DATA(msg_ptr);  //使用 NLMSG_DATA 宏获取消息中的错误信息结构体 nlmsgerr 的指针
                    if (err->error != 0) {
                        fprintf(stderr, "内核返回错误: %s\n", strerror(-err->error));
                        close(s);
                        exit(EXIT_FAILURE);
                    }
                    break;
                }
                case NLMSG_DONE: //如果消息类型为 NLMSG_DONE,表示内核已经发送完所有请求的信息,将 end 标志置为 1,退出循环
                    end = 1;
                    break;
                case RTM_NEWLINK: //如果消息类型为 RTM_NEWLINK,表示接收到了新的网络接口信息。调用 rtnl_print_link 函数处理该消息,打印网络接口的相关信息
                    rtnl_print_link(msg_ptr);
                    break;
                default:   //如果消息类型不是上述几种情况,输出忽略消息的信息,包含消息类型和消息长度
                    printf("忽略消息:type=%d, len=%d\n",
                           msg_ptr->nlmsg_type, msg_ptr->nlmsg_len);
                    break;
            }
        }

        // 处理未对齐的剩余数据
        if (remaining_len > 0) {
            fprintf(stderr, "剩余%d字节未处理数据\n", remaining_len);
        }
    }

    close(s);  // 确保关闭套接字
    return 0;
}



编译运行:

二、ipaddress.c

// 显示IPv4,应用Netlink套接字从Linux内核中获取所有网络接口的IP地址
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <errno.h>

#define BUFFERSIZE 10240

// 定义一个自定义结构体 netlink_reqest_s,它包含一个 Netlink 消息头 nlmsghdr 和一个路由通用消息结构体 rtgenmsg
struct netlink_reqest_s {
    // Netlink 消息头
    struct nlmsghdr hdr;
    // 路由消息通用结构,指定地址族
    struct rtgenmsg gen;
};

// 功能:解析并打印网络接口的 IP 地址信息
void rtnetlink_disp_address(struct nlmsghdr *h) {
    // struct ifaddrmsg *addr:指向 Netlink 消息中包含的网络地址信息结构体
    struct ifaddrmsg *addr;
    // struct rtattr *attr:指向路由属性结构体
    struct rtattr *attr;
    // 用于记录 Netlink 消息中有效负载的长度
    int len;

    // 获取 Netlink 消息中实际的数据部分,计算方式:消息头地址 + 头部大小
    addr = NLMSG_DATA(h);
    // 获取 Netlink 消息中有效负载的长度
    len = RTM_PAYLOAD(h);

    /* 循环输出 Netlink 所有属性消息:网络接口名称及 IP 地址 */
    for (attr = IFA_RTA(addr); RTA_OK(attr, len); attr = RTA_NEXT(attr, len)) {
        switch (attr->rta_type) {
            // 如果属性是接口名称就打印
            case IFA_LABEL:
                printf("网络接口名称 : %s\n", (char *)RTA_DATA(attr));
                break;
            // 如果属性是本地 IP 地址就打印
            case IFA_LOCAL: {
                // 获取 IP 地址的二进制表示
                int ip = *(int *)RTA_DATA(attr);
                // 用于存储 IP 地址的四个字节
                unsigned char bytes[4];
                // 提取 IP 地址的四个字节
                bytes[0] = ip & 0xFF;
                bytes[1] = (ip >> 8) & 0xFF;
                bytes[2] = (ip >> 16) & 0xFF;
                bytes[3] = (ip >> 24) & 0xFF;
                // 打印网络 IP 地址
                printf("网络 IP 地址为 : %d.%d.%d.%d\n\n", bytes[0], bytes[1], bytes[2], bytes[3]);
                break;
            }
            default:
                break;
        }
    }
}

int main(void) {
    // Netlink 地址结构,用于绑定套接字
    struct sockaddr_nl kerl;
    // 套接字描述符
    int s;
    // 循环结束标志
    int end = 0;
    // 接收到的消息长度
    int len;
    // 消息头结构,用于 sendmsg 和 recvmsg
    struct msghdr msg;
    // 自定义请求结构
    struct netlink_reqest_s req;
    // 分散、聚集 I/O 结构,用于消息传输
    struct iovec io;
    // 接收缓冲区
    char buffer[BUFFERSIZE];

    // 初始化 Netlink 地址结构
    memset(&kerl, 0, sizeof(kerl));
    // 使用 Netlink 协议族
    kerl.nl_family = AF_NETLINK;
    // 不加入任何组播组
    kerl.nl_groups = 0;

    // 创建套接字
    /*
    AF_NETLINK: 使用 Netlink 协议族。
    SOCK_RAW: 原始套接字类型,允许直接操作 Netlink 消息。
    NETLINK_ROUTE: 路由子系统,用于获取网络接口和路由信息。
    */
    if ((s = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)) < 0) {
        perror("创建 Netlink 套接字失败");
        exit(EXIT_FAILURE);
    }

    // 构造 Netlink 请求消息
    memset(&req, 0, sizeof(req));
    // 消息总长度(头部 + rtgenmsg 结构体),通过 NLMSG_LENGTH 计算对齐后的长度
    req.hdr.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtgenmsg));
    // 请求获取网络接口地址信息
    req.hdr.nlmsg_type = RTM_GETADDR;
    // 标志为请求消息,并要求返回所有条目
    req.hdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
    // 序列号,用于匹配请求和响应
    req.hdr.nlmsg_seq = 1;
    // 发送方进程 ID
    req.hdr.nlmsg_pid = getpid();
    // 指定地址族为 IPv4
    req.gen.rtgen_family = AF_INET;

    // 设置 I/O 向量和消息头
    memset(&io, 0, sizeof(io));
    io.iov_base = &req;
    io.iov_len = req.hdr.nlmsg_len;

    memset(&msg, 0, sizeof(msg));
    msg.msg_iov = &io;          // 指向 I/O 向量
    msg.msg_iovlen = 1;         // 向量数量为 1
    msg.msg_name = &kerl;       // 目标地址(内核)
    msg.msg_namelen = sizeof(kerl);

    // 发送请求消息
    if (sendmsg(s, &msg, 0) < 0) {
        perror("发送消息失败");
        close(s);
        exit(EXIT_FAILURE);
    }

    // 接收并解析内核响应,当接收到 NLMSG_DONE 消息时,end 会被置为 1,从而退出循环
    while (!end) {
        // 定义一个指向 nlmsghdr 结构体的指针 msg_ptr,用于遍历接收到的 Netlink 消息
        struct nlmsghdr *msg_ptr;
        // 用于记录还未处理的消息长度
        int remaining_len;

        // 清空接收缓冲区
        memset(buffer, 0, BUFFERSIZE);
        io.iov_base = buffer;
        io.iov_len = BUFFERSIZE;

        // 接收消息
        if ((len = recvmsg(s, &msg, 0)) < 0) {
            if (errno == EINTR) continue;  // 处理中断
            perror("接收消息失败");
            close(s);
            exit(EXIT_FAILURE);
        }

        // 处理消息分片(NLMSG_TRUNC 标志)
        if (msg.msg_flags & MSG_TRUNC) {
            fprintf(stderr, "警告:消息被截断,考虑增大缓冲区\n");
        }

        // 将 msg_ptr 指针指向接收缓冲区 buffer 的起始位置,将其视为第一个 Netlink 消息的头部
        msg_ptr = (struct nlmsghdr *)buffer;
        // 将 remaining_len 初始化为接收到的消息总长度 len
        remaining_len = len;

        for (; NLMSG_OK(msg_ptr, remaining_len);  // NLMSG_OK(msg_ptr, remaining_len):这是一个宏,用于检查 msg_ptr 指向的 Netlink 消息是否有效,即消息长度是否足够且未超出剩余未处理的消息长度
             msg_ptr = NLMSG_NEXT(msg_ptr, remaining_len)) {  // 将 msg_ptr 指针移动到下一个 Netlink 消息的头部,并更新 remaining_len 的值
            // 内核在回复单播请求时,会将 nlmsg_pid 设置为用户进程的 PID(即 self_pid)
            if (msg_ptr->nlmsg_pid != getpid()) {
                fprintf(stderr, "收到非本进程的消息,已忽略 (PID: %u)\n", msg_ptr->nlmsg_pid);
                continue;
            }

            switch (msg_ptr->nlmsg_type) {
                // 如果消息类型为 NLMSG_ERROR,表示内核返回了错误信息
                case NLMSG_ERROR: {
                    // 使用 NLMSG_DATA 宏获取消息中的错误信息结构体 nlmsgerr 的指针
                    struct nlmsgerr *err = NLMSG_DATA(msg_ptr);
                    if (err->error != 0) {
                        fprintf(stderr, "内核返回错误: %s\n", strerror(-err->error));
                        close(s);
                        exit(EXIT_FAILURE);
                    }
                    break;
                }
                // 如果消息类型为 NLMSG_DONE,表示内核已经发送完所有请求的信息,将 end 标志置为 1,退出循环
                case NLMSG_DONE:
                    end = 1;
                    break;
                // 如果消息类型为 RTM_NEWADDR,表示接收到了新的网络接口地址信息。调用 rtnetlink_disp_address 函数处理该消息,打印网络接口的相关信息
                case RTM_NEWADDR:
                    rtnetlink_disp_address(msg_ptr);
                    break;
                // 如果消息类型不是上述几种情况,输出忽略消息的信息,包含消息类型和消息长度
                default:
                    printf("忽略消息:type=%d, len=%d\n", msg_ptr->nlmsg_type, msg_ptr->nlmsg_len);
                    break;
            }
        }

        // 处理未对齐的剩余数据
        if (remaining_len > 0) {
            fprintf(stderr, "剩余 %d 字节未处理数据\n", remaining_len);
        }
    }

    // 确保关闭套接字
    close(s);
    return 0;
}    

编译运行:


http://www.kler.cn/a/585500.html

相关文章:

  • 程序化广告行业(13/89):DSP的深入解析与运营要点
  • CH340 模块的作用及其与 JTAG、串口下载和 TTL 电平的关系
  • 【春招笔试】2025.03.13-蚂蚁春招笔试题
  • VisionPro中IPO工具详解
  • 代码随想录二刷|图论7
  • 【品铂科技工业生产应用案例解析】
  • 海马下载 1.0.2 | 纯净无广告,极简设计,不限速下载工具
  • Spring TX配置(声明式事务管理+annotation)
  • C++中,存储持续性、作用域和链接性
  • 鸿蒙应用开发-轻松获取http网络请求
  • MariaDB 10.6.21(安装后实际版本为10.6.19)
  • 67.Harmonyos NEXT 图片预览组件之性能优化策略
  • Redis项目_黑马点评
  • transformer bert 多头自注意力
  • Linux ECM子网掩码常见问题排查
  • Jenkins 集成DingDing 推送
  • qt+opengl 播放yuv视频
  • 类和对象:
  • 【服务器知识】Nginx路由匹配规则说明
  • Kotlin关键字`when`的详细用法