一、基礎知識
????????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;
}
編譯運行: