一、基础知识
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 和 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;
}
编译运行: