跳到主要内容Linux Netlink Socket 原理与实战:全面对比 TCP 通信 | 极客日志C
Linux Netlink Socket 原理与实战:全面对比 TCP 通信
综述由AI生成Linux Netlink Socket 是内核与用户空间进程间通信的 IPC 机制,支持双向通信和多播。 Netlink 的核心特点、常见协议类型及发送消息的完整步骤,包括创建 Socket、绑定地址、构造消息、指定目标及接收回复。通过与 TCP Socket 的对比,阐述了两者在通信对象、协议栈、连接语义等方面的差异,并给出了适用场景建议,帮助开发者根据需求选择合适的通信方式。
黑客34 浏览 Linux Netlink Socket 原理与实战:全面对比 TCP 通信
引言:当内核需要和你'私聊'
想象一下这样的场景:你的应用程序正在运行,突然内核发现网卡被拔掉了——这时候内核需要立刻通知你的程序。在 Linux 世界里,这种内核与用户态程序之间的'私聊'需求非常普遍:路由表变化、IP 地址变更、新设备插入…这些事件都需要一种高效的通信机制。
你可能会问:为什么不能用我们熟悉的 TCP socket?毕竟它已经这么成熟了。答案是:TCP 是为跨机器通信设计的,而我们需要的是本机内部内核与用户程序的对话。
这就是 Netlink 登场的地方。
一、Netlink 是什么?
Netlink 是 Linux 内核提供的一种特殊的进程间通信(IPC)机制,专门用于内核与用户空间进程之间的双向通信。它从 Linux 2.2 版本开始引入,现在已经成为了内核与用户态通信的事实标准。
Netlink 的核心特点
- 基于 socket API:使用标准的 socket 接口(
socket、bind、sendmsg、recvmsg),开发者无需学习全新的 API
- 全双工通信:不仅用户程序可以主动发消息给内核,内核也可以主动'推送'消息给用户程序
- 支持多播:一个消息可以同时发送给多个接收者,非常适合事件通知场景
- 异步处理:消息有队列缓冲,不会阻塞发送方
常见的 Netlink 协议类型
Netlink 不是一个单一的协议,而是一个协议家族。通过 socket() 的第三个参数指定具体的协议类型:
| 协议类型 | 用途 |
|---|
NETLINK_ROUTE | 路由、链路、地址等网络配置(最常用) |
NETLINK_FIREWALL | 防火墙规则管理 |
NETLINK_NFLOG | Netfilter 日志(iptables/UFW 的后端) |
NETLINK_KOBJECT_UEVENT | 内核热插拔事件(udev 就是用它) |
NETLINK_GENERIC | 通用 Netlink,用于扩展自定义协议 |
NETLINK_AUDIT | 审计子系统 |
实际上,你在终端里用的 ip 命令,底层就是通过 NETLINK_ROUTE 与内核通信的。
二、如何使用 Netlink 发送消息(实战篇)
让我们一步步实现一个 Netlink 通信程序。虽然最终目标是发送消息,但 Netlink 的流程比 TCP 多几个关键步骤。
步骤 1:创建 Socket
#include <sys/socket.h>
#include <linux/netlink.h>
sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
(sock_fd < ) {
perror();
;
}
int
if
0
"socket"
return
-1
AF_NETLINK:地址族,告诉内核这是 Netlink 通信
SOCK_RAW:Netlink 只有这种类型(或者 SOCK_DGRAM,效果相同)
NETLINK_ROUTE:我们要与内核的路由子系统对话
步骤 2:Bind(绑定本地地址)
这一步可能让你困惑:本机通信为什么还要 bind?因为 Netlink 需要给每个 socket 一个唯一的'地址',这样内核才能把消息正确地投递给你。
struct sockaddr_nl local_addr;
memset(&local_addr, 0, sizeof(local_addr));
local_addr.nl_family = AF_NETLINK;
local_addr.nl_pid = getpid();
local_addr.nl_groups = 0;
if (bind(sock_fd, (struct sockaddr*)&local_addr, sizeof(local_addr)) < 0) {
perror("bind");
close(sock_fd);
return -1;
}
nl_pid:不一定是进程 PID,但必须是唯一的 32 位整数。通常单线程程序用 getpid() 就足够了;如果同一进程有多个 Netlink socket,可以用 pthread_self() << 16 | getpid() 生成
nl_groups:如果想接收多播消息(比如路由变化事件),设置对应的位掩码;否则填 0
步骤 3:构造 Netlink 消息
Netlink 消息有固定的格式,每个消息都必须包含一个头部(struct nlmsghdr):
struct nlmsghdr {
__u32 nlmsg_len;
__u16 nlmsg_type;
__u16 nlmsg_flags;
__u32 nlmsg_seq;
__u32 nlmsg_pid;
};
#define MAX_PAYLOAD 1024
char buffer[NLMSG_SPACE(MAX_PAYLOAD)];
struct nlmsghdr* nlh = (struct nlmsghdr*)buffer;
nlh->nlmsg_len = NLMSG_LENGTH(MAX_PAYLOAD);
nlh->nlmsg_type = 1;
nlh->nlmsg_flags = NLM_F_REQUEST;
nlh->nlmsg_seq = 1;
nlh->nlmsg_pid = getpid();
char* payload = NLMSG_DATA(nlh);
strcpy(payload, "Hello Kernel!");
NLMSG_SPACE(len):计算给定负载长度所需的缓冲区总大小(包括对齐)
NLMSG_LENGTH(len):计算消息总长度(不包括对齐填充)
NLMSG_DATA(nlh):获取负载部分的指针
步骤 4:指定目标地址并发送
我们要发给内核,所以目标地址的 nl_pid 设为 0(内核的'端口号'):
struct sockaddr_nl kernel_addr;
memset(&kernel_addr, 0, sizeof(kernel_addr));
kernel_addr.nl_family = AF_NETLINK;
kernel_addr.nl_pid = 0;
kernel_addr.nl_groups = 0;
struct iovec iov;
iov.iov_base = (void*)nlh;
iov.iov_len = nlh->nlmsg_len;
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void*)&kernel_addr;
msg.msg_namelen = sizeof(kernel_addr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
int ret = sendmsg(sock_fd, &msg, 0);
if (ret < 0) {
perror("sendmsg");
}
这里使用 sendmsg 而不是 sendto,因为 Netlink 消息需要同时传递目标地址和消息体(通过 iovec 结构)。
步骤 5:接收回复
发送请求后,内核会回复一个或多个消息。需要循环接收直到收到结束标记:
char recv_buf[8192];
struct sockaddr_nl src_addr;
socklen_t addr_len = sizeof(src_addr);
int len = recvfrom(sock_fd, recv_buf, sizeof(recv_buf), 0, (struct sockaddr*)&src_addr, &addr_len);
struct nlmsghdr* nlh_reply = (struct nlmsghdr*)recv_buf;
while (NLMSG_OK(nlh_reply, len)) {
if (nlh_reply->nlmsg_type == NLMSG_DONE) {
break;
}
if (nlh_reply->nlmsg_type == NLMSG_ERROR) {
struct nlmsgerr* err = (struct nlmsgerr*)NLMSG_DATA(nlh_reply);
printf("错误码:%d\n", err->error);
break;
}
printf("收到回复:%s\n", (char*)NLMSG_DATA(nlh_reply));
nlh_reply = NLMSG_NEXT(nlh_reply, len);
}
宏 NLMSG_OK 用于遍历可能的多条消息,它会检查消息长度和对齐。
完整示例:获取路由表信息
下面是一个完整的例子,用 Netlink 获取系统的路由表信息:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
int main() {
int sock_fd;
struct sockaddr_nl local_addr, kernel_addr;
struct nlmsghdr* nlh;
struct msghdr msg;
struct iovec iov;
char buf[8192];
sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
memset(&local_addr, 0, sizeof(local_addr));
local_addr.nl_family = AF_NETLINK;
local_addr.nl_pid = getpid();
bind(sock_fd, (struct sockaddr*)&local_addr, sizeof(local_addr));
nlh = (struct nlmsghdr*)buf;
nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
nlh->nlmsg_type = RTM_GETROUTE;
nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
nlh->nlmsg_seq = 1;
nlh->nlmsg_pid = getpid();
memset(&kernel_addr, 0, sizeof(kernel_addr));
kernel_addr.nl_family = AF_NETLINK;
kernel_addr.nl_pid = 0;
iov.iov_base = (void*)nlh;
iov.iov_len = nlh->nlmsg_len;
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void*)&kernel_addr;
msg.msg_namelen = sizeof(kernel_addr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
sendmsg(sock_fd, &msg, 0);
int len;
while ((len = recv(sock_fd, buf, sizeof(buf), 0)) > 0) {
struct nlmsghdr* nlp = (struct nlmsghdr*)buf;
for (; NLMSG_OK(nlp, len); nlp = NLMSG_NEXT(nlp, len)) {
if (nlp->nlmsg_type == NLMSG_DONE) {
goto done;
}
if (nlp->nlmsg_type == RTM_NEWROUTE) {
printf("收到一条路由条目\n");
}
}
}
done:
close(sock_fd);
return 0;
}
三、Netlink vs TCP:殊途不同归
现在我们把 Netlink 和我们最熟悉的 TCP socket 放在一起比较。虽然它们都用 socket API,但设计哲学完全不同。
对比表格
| 对比维度 | Netlink Socket | TCP Socket |
|---|
| 通信对象 | 本机内核 或 本机其他进程 | 远程主机 上的进程 |
| 地址族 | AF_NETLINK | AF_INET 或 AF_INET6 |
| 协议类型 | NETLINK_ROUTE、NETLINK_GENERIC 等 | IPPROTO_TCP |
| 套接字类型 | SOCK_RAW 或 SOCK_DGRAM | SOCK_STREAM |
| 通信模式 | 消息边界(datagram) | 字节流(stream) |
| 可靠性 | 不可靠(但内核队列一般不会丢) | 可靠(ACK、重传、顺序保证) |
| 流量控制 | 简单的队列机制 | 滑动窗口、拥塞控制 |
| 连接概念 | 无连接(但可用 connect 绑定默认对端) | 面向连接(三次握手) |
| 多播支持 | 原生支持,一个消息可发到多播组 | 不支持(需上层应用实现) |
| 双向性 | 全双工,内核可主动发起通信 | 全双工,但只能由客户端发起连接 |
| 性能开销 | 低(无需网络协议栈) | 较高(协议栈处理、数据拷贝) |
| 使用场景 | 网络配置、设备监控、内核事件通知 | Web 服务、文件传输、远程访问 |
核心差异详解
1. 通信对象和范围
TCP 设计的初衷是跨越网络边界,让不同机器上的进程可以通信。它要处理复杂的网络环境:丢包、乱序、拥塞、MTU 分片等等。
Netlink 则完全不需要操心这些——它的通信范围仅限于本机。消息从用户态进程发出,直接进入内核的消息队列,没有网络层的参与。
2. 协议栈的差异
用户程序 → socket 缓冲区 → TCP 层(加 TCP 头)→ IP 层(加 IP 头)→ 链路层(加 MAC 头)→ 网卡 → 网络
用户程序 → 构造 Netlink 消息 → socket 缓冲区 → 内核 Netlink 核心 → 目标内核模块
没有协议头的层层封装,没有网卡的参与,效率自然更高。
3. 连接的语义
TCP 是面向连接的。在通信之前,必须通过三次握手建立连接,通信结束后还要四次挥手断开连接。连接是 TCP 可靠性的基础。
Netlink 是无连接的。你随时可以发送消息给内核(nl_pid=0)或其他进程,不需要事先建立连接。当然,如果你只想和一个对端通信,可以用 connect() 绑定默认地址,之后直接用 send() 而不用每次都指定目标。
4. 谁可以主动发起通信?
在 TCP 世界里,服务器可以被动接受连接,但主动发起数据推送的永远是客户端(除非客户端先请求)。但在系统管理场景中,内核经常需要主动通知用户程序:网线掉了、新设备插入了、路由变了…
Netlink 完美解决了这个问题:内核可以在任何时间向绑定了特定多播组的用户程序发送消息。这就是所谓的全双工通信——双方都可以随时发起对话。
5. 消息边界 vs 字节流
TCP 是字节流协议,它不保留消息边界。如果你连续发送两个 'hello',接收方可能一次收到 'hellohello',也可能分多次收到。应用层需要自己设计消息边界(如加长度头、特殊分隔符)。
Netlink 是消息协议,每条 sendmsg 发送的数据对应一个完整的 Netlink 消息,接收方 recvmsg 一次正好拿到一条消息(如果缓冲区够大)。这对应用层开发来说省心不少。
四、什么时候用 Netlink,什么时候用 TCP?
用 Netlink 的场景
- 网络配置管理:你想实现一个类似
ip 命令的工具,修改 IP 地址、路由表
- 监控网络事件:监听网卡状态变化、路由更新
- 与内核模块通信:你写了一个内核模块,需要和用户态程序交换数据
- 获取系统信息:获取链路状态、ARP 表、邻居信息等
- 设备热插拔监控:监听 USB、SD 卡等设备的插拔事件
用 TCP 的场景
- Web 服务器/客户端:HTTP 协议基于 TCP
- 数据库连接:MySQL、PostgreSQL 都使用 TCP
- 远程登录:SSH、Telnet
- 文件传输:FTP、SCP
- 任何需要跨机器通信的场景
五、总结:殊途同归的 socket API
Netlink 和 TCP 共用同一套 socket API,这是 Linux 设计哲学的美妙之处——统一的接口,多样的实现。
- 创建:都是
socket(),只是参数不同
- 绑定:都是
bind(),只是地址结构不同
- 发送:都是
sendmsg()/sendto(),只是目标地址含义不同
- 接收:都是
recvmsg()/recvfrom()
- TCP 是网络通信协议,用于跨机器
- Netlink 是内核 IPC 机制,用于本机内核 - 用户通信
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online