一、基础知识
Netlink 是 Linux 系统中一种内核与用户空间通信的高效机制,而 Netlink 消息是这种通信的核心载体。它允许用户态程序(如网络配置工具、监控工具)与内核子系统(如网络协议栈、设备驱动)交换数据,例如获取网络接口信息、配置路由表、接收内核事件通知等。
Netlink 消息的组成
一个完整的 Netlink 消息由两部分构成:
消息头(
struct nlmsghdr
)
定义消息的元信息,例如消息类型、长度、序列号等。struct nlmsghdr {__u32 nlmsg_len; // 消息总长度(头部 + 数据)__u16 nlmsg_type; // 消息类型(如请求、响应、错误)__u16 nlmsg_flags; // 标志位(如请求标志、多部分消息标志)__u32 nlmsg_seq; // 序列号(用于匹配请求和响应)__u32 nlmsg_pid; // 发送方端口ID(通常为进程ID) };
消息体(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 的独特优势
结构化数据
消息通过二进制格式传递,避免了文本解析(如procfs
/sysfs
)的开销。异步通信
支持非阻塞通信,用户程序无需等待内核响应。多播支持
内核可以向多个用户进程广播事件(如接口状态变化)。可扩展性
通过消息类型(nlmsg_type
)和属性(struct rtattr
)灵活扩展功能。
Netlink 消息的工作流程
以获取网络接口列表为例:
用户程序构造请求消息
设置
nlmsghdr
:nlmsg_type = RTM_GETLINK
,nlmsg_flags = NLM_F_DUMP
。设置
rtgenmsg
:rtgen_family = AF_UNSPEC
(获取所有接口)。发送消息到内核
通过sendmsg
系统调用发送 Netlink 消息。内核处理请求
路由子系统解析消息,收集所有网络接口信息,封装为多个 Netlink 消息(可能分片)。用户程序接收响应
通过recvmsg
读取消息,解析nlmsghdr
和消息体,提取接口名称、状态等数据。
典型应用场景
网络配置工具
iproute2
工具集(如ip link
、ip route
)底层使用 Netlink 配置网络。
设备监控
监听内核事件,如接口状态变化、新设备连接。防火墙和策略路由
配置netfilter
(iptables/nftables)规则或复杂路由策略。容器网络
容器运行时(如 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 和 recvmsgstruct 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_INETnkernel.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;//发送方进程 IDreq.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; // 向量数量为 1msg.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 初始化为接收到的消息总长度 retremaining_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 和 recvmsgstruct 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;// 发送方进程 IDreq.hdr.nlmsg_pid = getpid();// 指定地址族为 IPv4req.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; // 向量数量为 1msg.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 初始化为接收到的消息总长度 lenremaining_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;
}
编译运行: