注:本文为 "Linux Netlink 通信机制" 相关合辑。
略作重排,未整理去重。
如有内容异常,请看原文。
Linux Kernel Module Communication: Beyond /dev, /proc, and ioctl() -- What Other Options Exist?
Linux 内核模块通信:除了 /dev、/proc 和 ioctl(),还有哪些其他选择?
2025-12
Communication between Linux kernel modules and user-space applications is a critical aspect of system programming. Traditionally, developers rely on familiar interfaces like character devices (/dev), the /proc filesystem, and ioctl() for this purpose. While these methods work for simple use cases, they often fall short in scenarios requiring high performance, scalability, asynchronous messaging, or advanced tracing capabilities.
Linux 内核模块与用户空间应用程序之间的通信是系统编程的一个关键环节。传统上,开发者会依赖字符设备(/dev)、/proc 文件系统以及 ioctl() 这些熟悉的接口来实现该功能。这些方法在简单使用场景下可以正常工作,但在需要高性能、可扩展性、异步消息传递或高级跟踪能力的场景中,往往难以满足需求。
For example:
例如:
-
/devandioctl()are limited by fixed command sets and struggle with complex data structures.
/dev和ioctl()受限于固定的命令集,且难以处理复杂的数据结构。 -
/procis inefficient for frequent data exchange due to filesystem overhead.由于文件系统的开销,
/proc在高频数据交换场景下效率低下。
This blog explores alternative communication mechanisms that address these limitations. We'll dive into their inner workings, use cases, pros/cons, and provide practical examples to help you choose the right tool for your next kernel module project.
本文将探讨能够解决这些局限性的替代通信机制。我们会深入剖析这些机制的内部工作原理、适用场景、优缺点,并提供实用示例,帮助你为下一个内核模块项目选择合适的工具。
1. Netlink Sockets: Bidirectional Socket-Based Communication
1. Netlink 套接字:基于套接字的双向通信
What Are Netlink Sockets?
什么是 Netlink 套接字?
Netlink is a socket-based inter-process communication (IPC) protocol designed explicitly for kernel-user space interaction. Unlike ioctl() (which is synchronous and command-based), Netlink supports bidirectional, asynchronous messaging and is optimized for dynamic, complex data exchange. It is widely used in the kernel for subsystems like networking (e.g., iproute2 tools), udev, and SELinux.
Netlink 是一种基于套接字的进程间通信( I P C IPC IPC)协议 ,专门为内核-用户空间交互而设计。与 ioctl()(同步且基于命令)不同,Netlink 支持双向、异步消息传递 ,并针对动态、复杂的数据交换进行了优化。它在内核中的网络(例如 iproute2 工具)、udev 以及 SELinux 等子系统中被广泛使用。
How It Works
工作原理
-
Socket Family : Netlink uses the
AF_NETLINKsocket family, with kernel and user-space endpoints.
套接字家族 :Netlink 使用 A F _ N E T L I N K AF\_NETLINK AF_NETLINK 套接字家族,拥有内核和用户空间两个端点。 -
Message Format : Messages are structured with a
nlmsghdrheader, supporting nested attributes (vianlattr), making it easy to send complex data (e.g., lists, key-value pairs).
消息格式 :消息以nlmsghdr头部进行结构化,支持通过nlattr实现嵌套属性,便于发送列表、键值对这类复杂数据。 -
Multicast Support : Kernel modules can broadcast messages to multiple user-space listeners via multicast groups.
组播支持:内核模块可以通过组播组向多个用户空间监听器广播消息。
Use Cases
适用场景
-
Dynamic configuration (e.g., updating routing tables from user space).
动态配置(例如,从用户空间更新路由表)。
-
Asynchronous event notifications (e.g., device hotplug events).
异步事件通知(例如,设备热插拔事件)。
-
High-frequency data streaming (e.g., network statistics).
高频数据流传输(例如,网络统计信息)。
Pros & Cons
优缺点
| Pros | Cons |
|---|---|
| Bidirectional and asynchronous.(双向且异步) | More complex setup than ioctl().(配置比 ioctl() 更复杂) |
| Supports structured/nested data.(支持结构化/嵌套数据) | Message size limited by socket buffers.(消息大小受套接字缓冲区限制) |
| Kernel and user-space APIs are standardized.(内核和用户空间 API 已标准化) | Higher overhead than shared memory for bulk data.(传输海量数据时,开销比共享内存更高) |
Example: Kernel-to-User Space Messaging
示例:内核到用户空间的消息传递
Kernel Module (Sending a Message)
内核模块(发送消息)
c
#include <linux/module.h>
#include <linux/netlink.h>
#include <net/sock.h>
#define NETLINK_USER 31 // Custom Netlink family (1-31 are reserved)
static struct sock *nl_sk = NULL;
// Send a message to user space
static void send_to_user(const char *msg) {
struct sk_buff *skb;
struct nlmsghdr *nlh;
int msg_size = strlen(msg) + 1;
int res;
// Allocate socket buffer (skb)
skb = nlmsg_new(msg_size, GFP_KERNEL);
if (!skb) {
pr_err("Failed to allocate skb\n");
return;
}
// Populate Netlink message header (nlh)
nlh = nlmsg_put(skb, 0, 1, NLMSG_DONE, msg_size, 0);
NETLINK_CB(skb).dst_group = 0; // Unicast to a specific port
strncpy(nlmsg_data(nlh), msg, msg_size);
// Send message to user space
res = nlmsg_unicast(nl_sk, skb, 0); // 0 = user-space port ID
if (res < 0)
pr_err("nlmsg_unicast failed: %d\n", res);
}
static int __init netlink_init(void) {
// Create Netlink socket
struct netlink_kernel_cfg cfg = {
.input = NULL, // No user-space to kernel handling (for this example)
};
nl_sk = netlink_kernel_create(&init_net, NETLINK_USER, &cfg);
if (!nl_sk) {
pr_err("Failed to create Netlink socket\n");
return -ENOMEM;
}
// Send test message
send_to_user("Hello from kernel module!");
pr_info("Netlink module loaded\n");
return 0;
}
static void __exit netlink_exit(void) {
netlink_kernel_release(nl_sk);
pr_info("Netlink module unloaded\n");
}
module_init(netlink_init);
module_exit(netlink_exit);
MODULE_LICENSE("GPL");
User-Space Program (Receiving the Message)
用户空间程序(接收消息)
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#define NETLINK_USER 31
#define MAX_PAYLOAD 1024
int main() {
struct sockaddr_nl src_addr, dest_addr;
struct nlmsghdr *nlh;
struct iovec iov;
int sock_fd;
struct msghdr msg;
// Create Netlink socket
sock_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_USER);
if (sock_fd < 0)
return -1;
// Bind to Netlink port
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK;
src_addr.nl_pid = getpid(); // User-space port ID (unique per process)
bind(sock_fd, (struct sockaddr *)&src_addr, sizeof(src_addr));
// Prepare to receive message
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
nlh->nlmsg_pid = getpid();
nlh->nlmsg_flags = 0;
iov.iov_base = (void *)nlh;
iov.iov_len = nlh->nlmsg_len;
msg.msg_name = (void *)&dest_addr;
msg.msg_namelen = sizeof(dest_addr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
printf("Waiting for kernel message...\n");
recvmsg(sock_fd, &msg, 0); // Block until message is received
printf("Received: %s\n", (char *)NLMSG_DATA(nlh));
close(sock_fd);
free(nlh);
return 0;
}
2. eBPF: Extended Berkeley Packet Filter (Maps & Programs)
2. eBPF:扩展伯克利包过滤器(映射与程序)
What Is eBPF?
什么是 eBPF?
eBPF (Extended Berkeley Packet Filter) is a revolutionary technology that allows user-space programs to load custom bytecode into the kernel for execution in a sandboxed environment. eBPF programs run with kernel privileges but are verified for safety, making them ideal for high-performance networking, tracing, and monitoring.
eBPF(扩展伯克利包过滤器)是一项革命性技术,它允许用户空间程序将自定义字节码加载到内核中,并在沙箱环境中执行。eBPF 程序拥有内核权限运行,但会经过安全性验证,非常适合用于高性能网络、跟踪和监控场景。
Communication between eBPF (kernel space) and user space happens via eBPF maps ---shared data structures (e.g., hash tables, arrays) that persist across program runs.
eBPF(内核空间)与用户空间之间的通信通过eBPF 映射实现,这是一类在程序运行期间持久化的共享数据结构(例如哈希表、数组)。
How It Works
工作原理
-
A user-space application compiles an eBPF program (written in restricted C) to BPF bytecode.
用户空间应用程序将 eBPF 程序(用受限的 C 语言编写)编译为 BPF 字节码。
-
The bytecode is loaded into the kernel via the
bpf()system call, where it is verified for safety.该字节码通过
bpf()系统调用加载到内核中,并在内核中完成安全性验证。 -
The eBPF program runs in kernel space (e.g., attached to a network interface or tracepoint) and writes data to eBPF maps.
eBPF 程序在内核空间运行(例如,附加到网络接口或跟踪点上),并将数据写入 eBPF 映射。
-
User-space applications read/write to these maps to exchange data with the eBPF program.
用户空间应用程序通过读写这些映射,与 eBPF 程序进行数据交换。
Use Cases
适用场景
-
Networking (e.g., XDP for high-speed packet processing).
网络通信(例如,用于高速数据包处理的 XDP)。
-
Tracing (e.g., profiling kernel functions with
bpftrace).跟踪(例如,使用
bpftrace对内核函数进行性能剖析)。 -
Security (e.g., runtime policy enforcement).
安全防护(例如,运行时策略执行)。
-
Observability (e.g., metrics collection).
可观测性(例如,指标收集)。
Pros & Cons
优缺点
| Pros | Cons |
|---|---|
| Near-kernel performance with user-space flexibility.(兼具接近内核的性能和用户空间的灵活性) | Steep learning curve (requires BPF knowledge).(学习曲线陡峭,需要掌握 BPF 相关知识) |
| Sandboxed execution (safe for untrusted code).(沙箱环境执行,对不可信代码安全友好) | Kernel version dependencies (requires CONFIG_BPF_SYSCALL).(依赖内核版本,需要开启 CONFIG_BPF_SYSCALL 配置) |
| Rich set of map types for diverse data needs.(映射类型丰富,可满足多样化数据需求) | Limited access to kernel APIs (restricted for safety).(访问内核 API 受限,出于安全性考虑) |
Example: eBPF Map for Data Sharing
示例:用于数据共享的 eBPF 映射
eBPF Program (Writing to a Map)
eBPF 程序(写入映射)
c
// ebpf_program.c (compiled to BPF bytecode with clang)
#include <vmlinux.h>
#include <bpf/bpf_helpers.h>
// Define a hash map (key: u32, value: u64)
struct {
__uint(type, BPF_MAP_TYPE_HASH);
__uint(max_entries, 1024);
__type(key, u32);
__type(value, u64);
} stats_map SEC(".maps"); // Mark as a BPF map
// Tracepoint: sched_process_exec (triggers on process execution)
SEC("tracepoint/sched/sched_process_exec")
int tracepoint__sched_process_exec(struct trace_event_raw_sched_process_exec *ctx) {
u32 pid = bpf_get_current_pid_tgid() >> 32; // Get PID
u64 *count;
// Increment count for this PID in the map
count = bpf_map_lookup_elem(&stats_map, &pid);
if (!count) {
u64 init = 1;
bpf_map_update_elem(&stats_map, &pid, &init, BPF_ANY);
} else {
(*count)++;
}
return 0;
}
char LICENSE[] SEC("license") = "GPL"; // Required for GPL kernel helpers
User-Space Loader (Reading the Map)
用户空间加载器(读取映射)
c
// user_loader.c (uses libbpf to load eBPF program)
#include <stdio.h>
#include <bpf/libbpf.h>
#include "ebpf_program.skel.h" // Auto-generated header from BPF program
int main() {
struct ebpf_program_bpf *skel;
int err;
// Load and verify BPF program
skel = ebpf_program_bpf__open_and_load();
if (!skel) {
fprintf(stderr, "Failed to open/load BPF skeleton\n");
return 1;
}
// Attach tracepoint
err = ebpf_program_bpf__attach(skel);
if (err) {
fprintf(stderr, "Failed to attach BPF skeleton: %d\n", err);
goto cleanup;
}
printf("Tracing process executions (Ctrl+C to exit)...\n");
while (1) {
u32 pid;
u64 *count;
// Iterate over the BPF map and print stats
printf("\nPID\tExec Count\n");
bpf_map__for_each_key_value(&skel->maps.stats_map, &pid, &count) {
printf("%d\t%llu\n", pid, *count);
}
sleep(5);
}
cleanup:
ebpf_program_bpf__destroy(skel);
return err;
}
3. Tracepoints & Kprobes: Event Tracing for Monitoring & Debugging
3. 跟踪点与内核探针:用于监控和调试的事件跟踪
What Are Tracepoints & Kprobes?
什么是跟踪点与内核探针?
-
Tracepoints : Static, pre-defined markers in the kernel source code (e.g.,
sched_process_fork) that emit events when triggered. They are lightweight and safe to use in production.
跟踪点 :内核源代码中静态的、预定义的标记(例如sched_process_fork),被触发时会发出事件。它们开销极低,在生产环境中使用安全可靠。 -
Kprobes : Dynamic tracing tools that allow attaching handlers to any kernel function (even those without tracepoints). They are flexible but carry higher overhead than tracepoints.
内核探针 :动态跟踪工具,允许将处理程序附加到任意内核函数(即使该函数没有跟踪点)。它们灵活性强,但开销比跟踪点更高。
Both mechanisms enable user-space tools (e.g., perf, bpftrace) to capture kernel events and metrics without modifying kernel code.
这两种机制都能让用户空间工具(例如 perf、bpftrace)在不修改内核代码的情况下,捕获内核事件和相关指标。
How They Work
工作原理
-
Tracepoints : Kernel developers add tracepoints to critical code paths (e.g., process creation, disk I/O). User-space tools like
perfortrace-cmdattach listeners to these points to collect event data.
跟踪点 :内核开发者会在关键代码路径(例如进程创建、磁盘 I/O)中添加跟踪点。perf或trace-cmd等用户空间工具会向这些跟踪点附加监听器,以收集事件数据。 -
Kprobes : A user-space tool specifies a kernel function (e.g.,
sys_write) and a handler. When the function is called, the kernel pauses execution, runs the handler, and resumes.
内核探针 :用户空间工具指定一个内核函数(例如sys_write)和一个处理程序。当该函数被调用时,内核会暂停执行,运行处理程序,然后恢复原有执行流程。
Use Cases
适用场景
-
Debugging kernel module behavior (e.g., tracking function calls).
调试内核模块行为(例如,跟踪函数调用)。
-
Monitoring system-wide events (e.g., file opens, network packets).
监控系统级事件(例如,文件打开、网络数据包传输)。
-
Profiling performance bottlenecks (e.g., CPU usage per process).
剖析性能瓶颈(例如,每个进程的 CPU 使用率)。
Pros & Cons
优缺点
| Tracepoints | Kprobes |
|---|---|
| Low overhead (static, no runtime modification).(开销低,静态存在,无运行时修改) | Can trace any kernel function (no static markers needed).(可跟踪任意内核函数,无需静态标记) |
| Safe (pre-validated by kernel developers).(安全可靠,已由内核开发者预先验证) | Higher overhead (dynamic patching of kernel code).(开销较高,需要动态修补内核代码) |
| Limited to predefined events.(仅支持预定义事件) | Risk of instability if misused (e.g., attaching to critical functions).(使用不当存在不稳定风险,例如附加到核心关键函数) |
Example: Tracing with perf and Tracepoints
示例:使用 perf 和跟踪点进行跟踪
To trace process creation events using the sched_process_fork tracepoint:
使用 sched_process_fork 跟踪点跟踪进程创建事件的操作如下:
bash
# List available tracepoints
perf list tracepoint
# Trace process forks (output PID, parent PID, and comm)
perf record -e sched:sched_process_fork -a
perf script # View captured events
4. Shared Memory: High-Performance Data Exchange
4. 共享内存:高性能数据交换
What Is Shared Memory?
什么是共享内存?
Shared memory allows kernel modules and user-space applications to exchange data via a common memory region mapped into both address spaces. This avoids the overhead of copying data between kernel and user space, making it ideal for high-throughput scenarios.
共享内存允许内核模块和用户空间应用程序通过一个映射到双方地址空间的公共内存区域进行数据交换。这避免了内核与用户空间之间的数据拷贝开销,非常适合高吞吐量场景。
Common shared memory backends include:
常见的共享内存后端包括:
-
tmpfs: In-memory filesystem (e.g.,/dev/shm).
tmpfs:内存文件系统(例如/dev/shm)。 -
memfd_create(): Anonymous file descriptors (no filesystem entry).
memfd_create():匿名文件描述符(无文件系统条目)。 -
ashmem(Android): Optimized for mobile devices (less common on desktop).
ashmem(安卓专属):针对移动设备优化(在桌面系统中较少见)。
How It Works
工作原理
-
A user-space application creates a shared memory region (e.g., via
memfd_create()).用户空间应用程序创建一个共享内存区域(例如,通过
memfd_create())。 -
The kernel module maps this region into its address space using
remap_pfn_range()orvm_map_ram().内核模块通过
remap_pfn_range()或vm_map_ram()将该区域映射到自身的地址空间。 -
Both kernel and user space read/write directly to the shared region. Synchronization (e.g., mutexes, semaphores) ensures thread safety.
内核和用户空间都直接对该共享区域进行读写操作。通过互斥锁、信号量等同步机制保障线程安全。
Use Cases
适用场景
-
Real-time data acquisition (e.g., sensor data).
实时数据采集(例如,传感器数据)。
-
High-speed video streaming.
高速视频流传输。
-
Bulk data transfer (e.g., database logs).
海量数据传输(例如,数据库日志)。
Pros & Cons
优缺点
| Pros | Cons |
|---|---|
| Near-zero overhead (no data copying).(开销接近零,无数据拷贝) | Requires explicit synchronization (risk of race conditions).(需要显式同步,存在竞态条件风险) |
| Ideal for large/bulk data.(非常适合大型/海量数据传输) | Security risks (exposing kernel memory to user space).(存在安全风险,内核内存会暴露给用户空间) |
| Simple to implement for read-only/write-only workflows.(对于只读/只写工作流,实现简单) | Not suitable for small, frequent messages (synchronization overhead).(不适合小型、高频消息,同步开销过高) |
Example: Shared Memory with memfd_create()
示例:使用 memfd_create() 实现共享内存
User-Space (Creating a Shared Region)
用户空间(创建共享区域)
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <fcntl.h>
#define SHM_SIZE 4096 // 4KB shared region
int main() {
int fd;
char *shm_ptr;
// Create anonymous shared memory (memfd)
fd = syscall(SYS_memfd_create, "my_shm", MFD_CLOEXEC);
if (fd < 0) {
perror("memfd_create");
return 1;
}
// Resize the memfd to SHM_SIZE
if (ftruncate(fd, SHM_SIZE) < 0) {
perror("ftruncate");
return 1;
}
// Map the memfd into user-space address space
shm_ptr = mmap(NULL, SHM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (shm_ptr == MAP_FAILED) {
perror("mmap");
return 1;
}
// Write data to shared memory
sprintf(shm_ptr, "Hello from user space!");
printf("User-space wrote: %s\n", shm_ptr);
// Keep the region alive (e.g., wait for kernel to read)
printf("Press Enter to exit...\n");
getchar();
munmap(shm_ptr, SHM_SIZE);
close(fd);
return 0;
}
Kernel Module (Mapping the Shared Region)
内核模块(映射共享区域)
c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/mm.h>
#include <linux/memfd.h>
#define SHM_SIZE 4096
// Called when user-space mmaps the shared region
static int shm_mmap(struct file *file, struct vm_area_struct *vma) {
// Map the memfd's pages into the user-space VMA
if (remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff,
vma->vm_end - vma->vm_start, vma->vm_page_prot)) {
return -EAGAIN;
}
return 0;
}
static const struct file_operations shm_fops = {
.mmap = shm_mmap,
};
static int __init shared_mem_init(void) {
// In practice, the kernel module would need the memfd's file descriptor
// (passed via Netlink, sysfs, or another mechanism) to map the region.
pr_info("Shared memory kernel module loaded\n");
return 0;
}
static void __exit shared_mem_exit(void) {
pr_info("Shared memory kernel module unloaded\n");
}
module_init(shared_mem_init);
module_exit(shared_mem_exit);
MODULE_LICENSE("GPL");
5. Sysfs & Debugfs: Exposing Kernel Attributes
5. Sysfs 与 Debugfs:暴露内核属性
What Are Sysfs and Debugfs?
什么是 Sysfs 与 Debugfs?
-
Sysfs : A filesystem (
/sys) designed to expose kernel objects and their attributes (e.g., device status, driver parameters) in a standardized way. It is intended for stable, production-facing interfaces .
Sysfs :一种文件系统(/sys),旨在以标准化方式暴露内核对象及其属性(例如设备状态、驱动参数)。它适用于稳定的、面向生产环境的接口。 -
Debugfs : A lightweight filesystem (
/sys/kernel/debug) for debugging and development . It has fewer restrictions than sysfs and is ideal for exposing temporary or verbose data (e.g., module internal state).
Debugfs :一种轻量级文件系统(/sys/kernel/debug),用于调试和开发工作。它的限制比 sysfs 更少,非常适合暴露临时数据或详细数据(例如模块内部状态)。
How They Work
工作原理
-
Sysfs : Kernel modules create
kobjectstructures (representing objects like devices or drivers) and attach attributes (files) to them. Attributes are accessed viashow()(read) andstore()(write) callbacks.
Sysfs :内核模块创建kobject结构(代表设备、驱动等对象),并为其附加属性(文件)。属性通过show()(读)和store()(写)回调函数进行访问。 -
Debugfs : Modules create files directly in
/sys/kernel/debugusingdebugfs_create_file(), with read/write callbacks for data exchange.
Debugfs :内核模块通过debugfs_create_file()在/sys/kernel/debug中直接创建文件,通过读写回调函数实现数据交换。
Use Cases
适用场景
- Sysfs : Exposing device firmware versions, battery status, or driver configuration.
Sysfs:暴露设备固件版本、电池状态或驱动配置。 - Debugfs : Logging module debug messages, exposing internal counters, or testing new features.
Debugfs:记录模块调试日志、暴露内部计数器或测试新功能。
Pros & Cons
优缺点
| Sysfs | Debugfs |
|---|---|
| Standardized and stable (API unlikely to change).(标准化且稳定,API 几乎不会变更) | Simple to implement (no kobject boilerplate).(实现简单,无需 kobject 样板代码) |
| Designed for production use.(专为生产环境设计) | Not guaranteed to be present (disabled in some kernels).(不保证一定存在,部分内核中会被禁用) |
Requires kobject setup (boilerplate code).(需要配置 kobject,存在样板代码) |
Intended for debugging (not for user-facing interfaces).(仅用于调试,不适合面向用户的接口) |
Example: Exposing a Debugfs File
示例:暴露一个 Debugfs 文件
c
#include <linux/module.h>
#include <linux/debugfs.h>
static struct dentry *debug_dir;
static u32 debug_counter = 0;
// Read callback: return the current counter value
static ssize_t counter_read(struct file *file, char __user *buf, size_t len, loff_t *ppos) {
char tmp[32];
int ret;
ret = snprintf(tmp, sizeof(tmp), "%u\n", debug_counter);
return simple_read_from_buffer(buf, len, ppos, tmp, ret);
}
// Write callback: increment the counter
static ssize_t counter_write(struct file *file, const char __user *buf, size_t len, loff_t *ppos) {
debug_counter++;
return len; // Acknowledge all bytes written
}
static const struct file_operations counter_fops = {
.read = counter_read,
.write = counter_write,
};
static int __init debugfs_init(void) {
// Create a directory in debugfs: /sys/kernel/debug/my_module
debug_dir = debugfs_create_dir("my_module", NULL);
if (!debug_dir)
return -ENOMEM;
// Create a file: /sys/kernel/debug/my_module/counter
debugfs_create_file("counter", 0644, debug_dir, NULL, &counter_fops);
pr_info("Debugfs example loaded\n");
return 0;
}
static void __exit debugfs_exit(void) {
debugfs_remove_recursive(debug_dir); // Cleanup directory and files
pr_info("Debugfs example unloaded\n");
}
module_init(debugfs_init);
module_exit(debugfs_exit);
MODULE_LICENSE("GPL");
User-space can now read/write the counter:
用户空间现在可以对该计数器进行读写操作:
bash
# Read the counter
cat /sys/kernel/debug/my_module/counter
# Increment the counter
echo 1 > /sys/kernel/debug/my_module/counter
6. Userfaultfd: Handling Page Faults for On-Demand Communication
6. Userfaultfd:处理页面错误以实现按需通信
What Is Userfaultfd?
什么是 Userfaultfd?
userfaultfd is a system call that allows user-space applications to handle page faults in a shared memory region. When a user-space process accesses an unmapped page in the region, the kernel notifies the application via a userfaultfd file descriptor, giving it time to populate the page (e.g., with data from the kernel module).
userfaultfd 是一个系统调用,允许用户空间应用程序处理共享内存区域中的页面错误 。当用户空间进程访问该区域中未映射的页面时,内核会通过 userfaultfd 文件描述符通知应用程序,为其留出填充该页面数据的时间(例如,从内核模块获取数据填充)。
How It Works
工作原理
-
A user-space process creates a
userfaultfdand registers a memory region withUFFDIO_REGISTER.用户空间进程创建一个
userfaultfd,并通过UFFDIO_REGISTER注册一个内存区域。 -
When the process accesses an unmapped page in this region, the kernel sends a fault notification to the
userfaultfd.当该进程访问该区域中未映射的页面时,内核会向
userfaultfd发送一个错误通知。 -
The user-space process handles the fault (e.g., requests data from the kernel module via Netlink or sysfs).
用户空间进程处理该错误(例如,通过 Netlink 或 sysfs 从内核模块请求数据)。
-
The kernel module populates the page, and the process resumes execution.
内核模块填充该页面的数据,进程恢复执行。
Use Cases
适用场景
-
On-demand data loading (e.g., large datasets that fit in memory).
按需数据加载(例如,可放入内存的大型数据集)。
-
Implementing custom memory management (e.g., caching).
实现自定义内存管理(例如,缓存)。
Pros & Cons
优缺点
| Pros | Cons |
|---|---|
| Efficient for sparse or large datasets (only load needed pages).(对稀疏或大型数据集高效,仅加载所需页面) | Complex to implement (requires handling fault events).(实现复杂,需要处理错误事件) |
| Works with shared memory (low overhead).(与共享内存配合使用,开销低) | Limited to page-sized chunks of data.(仅支持页面大小的数据块) |
7. Conclusion: Choosing the Right Mechanism
7. 结论:选择合适的通信机制
The choice of communication mechanism depends on your use case:
通信机制的选择取决于具体的使用场景:
| Use Case | Recommended Mechanism |
|---|---|
| Bidirectional, structured messaging(双向、结构化消息传递) | Netlink Sockets(Netlink 套接字) |
| High-performance tracing/monitoring(高性能跟踪/监控) | eBPF (Maps)(eBPF 映射) |
| Debugging kernel events(调试内核事件) | Tracepoints/Kprobes(跟踪点/内核探针) |
| Bulk data transfer (high throughput)(海量数据传输(高吞吐量)) | Shared Memory (tmpfs/memfd)(共享内存(tmpfs/memfd)) |
| Exposing device attributes (production)(暴露设备属性(生产环境)) | Sysfs |
| Debugging/development(调试/开发) | Debugfs |
| On-demand data loading(按需数据加载) | Userfaultfd |
8. References
8. 参考文献
-
Linux Kernel Documentation\]: Netlink, eBPF, sysfs, and debugfs. Linux 内核文档:Netlink、eBPF、sysfs 与 debugfs。
eBPF.io:eBPF 开发教程与工具。
-
man7.org\]: Man pages for `netlink`, `bpf`, `userfaultfd`, and `memfd_create`. man7.org:`netlink`、`bpf`、`userfaultfd` 与 `memfd_create` 的手册页。
《BPF 性能工具》(布兰登·格雷格,奥莱利出版)。
-
Linux Kernel Programming\] (Packt Publishing). 《Linux 内核编程》(帕克特出版公司)。
用户空间与内核空间通信------Netlink(上)
wjlkoorey258 2012-11-07 22:00:24
引言
Alan Cox 在内核 1.3 版本的开发阶段首次引入了 Netlink 机制,最初该机制以字符驱动接口的形式,提供内核与用户空间之间的双向数据通信能力;随后,在 2.1 内核版本的开发过程中,Alexey Kuznetsov 将 Netlink 重构为一套更为灵活、且易于扩展的基于消息的通信接口,并将其应用于高级路由子系统的基础框架实现中。自该阶段起,Netlink 便成为 Linux 内核子系统与用户态应用程序之间进行数据通信的主要手段之一。
2001 年,ForCES IETF 委员会正式启动了 Netlink 机制的标准化工作。Jamal Hadi Salim 提议将 Netlink 定义为一种用于网络设备路由引擎组件与控制管理组件之间通信的专用协议,但该提议最终未被采纳。取而代之的是当前的实现格局:Netlink 被设计为一个全新的协议域(domain)。
Linux 创始人 Linus Torvalds 曾提出:"Linux is evolution, not intelligent design"。这一理念同样适用于 Netlink 机制------该机制不存在完整的规范文档与设计文档,其底层细节的获取仅能通过"Read the f**king source code"的方式实现。
本文不涉及 Netlink 在 Linux 系统中的实现机制剖析,仅围绕"什么是 Netlink"与"如何正确使用 Netlink"两个主题展开阐述,仅当实际应用中遇到问题时,才需要查阅内核源码以明确其底层原理。
什么是 Netlink
对 Netlink 机制的理解,需把握以下两个关键要点:
- 面向数据报的无连接消息子系统
- 基于通用 BSD Socket 架构实现
关于第一点,其特性与 UDP 协议具有较高的相似性,以 UDP 协议为参考理解 Netlink 机制具有合理性。通过知识的迁移、归纳与总结,可实现对该机制的深入掌握。Netlink 支持内核空间到用户空间、用户空间到内核空间的双向异步数据通信,同时也支持两个用户进程之间、两个内核子系统之间的数据通信。本文不涉及后两种通信场景,仅聚焦于用户空间与内核空间之间的数据通信实现。
提及第二点,通常会联想到对应的 BSD Socket 架构示意图(如下所示)。
在后续 Netlink 套接字编程的实战环节中,主要将使用 socket()、bind()、sendmsg() 与 recvmsg() 等系统调用,同时还会用到 Socket 提供的轮询(polling)机制。
Netlink 通信类型
Netlink 支持两种通信类型:单播(Unicast)与多播(Multicast)。
单播
单播常用于单个用户进程与单个内核子系统之间的 1 : 1 1:1 1:1 数据通信,用户空间向内核发送命令,并接收内核返回的命令执行结果。
多播
多播常用于单个内核进程与多个用户进程之间的 1 : N 1:N 1:N 数据通信,内核作为会话发起方,用户空间应用程序作为消息接收方。实现该功能的流程为:内核空间程序创建一个多播组,所有对该内核进程发送的消息感兴趣的用户空间进程,均可通过加入该多播组的方式接收对应消息。
其中,进程 A 与子系统 1 之间为单播通信,进程 B、C 与子系统 2 之间为多播通信。上述示意图还揭示了一个重要特性:从用户空间传递至内核空间的数据无需排队,对应的操作以同步方式完成;而从内核空间传递至用户空间的数据需要排队,对应的操作以异步方式完成。掌握该特性能够在基于 Netlink 开发应用模块时规避诸多潜在问题。例如,当用户空间向内核发送消息以获取路由表等大规模数据时,内核通过 Netlink 返回数据的过程中,开发人员需重点考虑数据的接收策略,充分重视内核空间的输出队列特性。
Netlink 的消息格式
Netlink 消息由消息头(Message Header)与有效数据载荷(Payload)两部分组成,整个 Netlink 消息需满足 4 4 4 字节对齐要求,通常以主机字节序进行传递。消息头为固定 16 16 16 字节长度,消息体长度为可变值。
Netlink 的消息头
消息头定义在对应内核头文件中,由结构体 struct nlmsghdr 表示,其定义如下:
c
struct nlmsghdr
{
__u32 nlmsg_len; /* Length of message including header */
__u16 nlmsg_type; /* Message content */
__u16 nlmsg_flags; /* Additional flags */
__u32 nlmsg_seq; /* Sequence number */
__u32 nlmsg_pid; /* Sending process PID */
};
消息头中各成员的属性解释如下:
nlmsg_len:整个消息的字节长度,包含 Netlink 消息头本身的长度。nlmsg_type:消息类型,用于区分数据消息与控制消息。在内核 2.6.21 版本中,Netlink 仅支持四种控制消息,具体如下:NLMSG_NOOP:空消息,无任何实际操作;NLMSG_ERROR:标识该消息中包含错误信息;NLMSG_DONE:当内核通过 Netlink 队列返回多条消息时,队列的最后一条消息类型为此值,其余所有消息的nlmsg_flags属性均会设置NLM_F_MULTI位有效;NLMSG_OVERRUN:暂未启用。
nlmsg_flags:附加在消息上的额外说明信息,例如前文提及的NLM_F_MULTI。部分常用标记及其作用如下表所示:
| 标记 | 作用及说明 |
|---|---|
NLM_F_REQUEST |
若消息包含该标记位,表明该消息为请求消息。所有从用户空间发送至内核空间的消息均需设置该位,否则内核将向用户空间返回 EINVAL 无效参数错误 |
NLM_F_MULTI |
用户空间至内核空间的消息传输为同步即时完成,而内核空间至用户空间的消息传输需要排队。若内核收到用户空间发送的包含 NLM_F_DUMP 位为 1 1 1 的消息,将向用户空间发送一个由多条 Netlink 消息组成的链表。除最后一条消息外,其余每条消息均会设置该位有效 |
NLM_F_ACK |
该消息是内核对来自用户空间的 NLM_F_REQUEST 消息的响应消息 |
NLM_F_ECHO |
若用户空间发送至内核的消息中该标记为 1 1 1,表明用户应用进程要求内核将该消息通过单播形式回传给该用户进程,与常规的"回显"功能类似 |
| ... | ... |
关于 nlmsg_flags 的完整取值,可通过查阅内核源码与官方技术文档获取,此处不做进一步展开。
-
nlmsg_seq:消息序列号。由于 Netlink 是面向数据报的通信机制,存在数据丢失的潜在风险,而 Netlink 提供了消息可靠性保障的基础机制,可供程序开发人员根据实际需求进行实现。消息序列号通常与NLMSG_ACK类型消息联合使用,若用户应用程序需要确保发送的每条消息均被内核成功接收,发送消息时需自行设置该序列号,内核收到消息后提取该序列号,并在响应消息中设置相同的序列号,该机制与 TCP 协议的应答确认机制具有相似性。注意 :当内核主动向用户空间发送广播消息时,该字段的值恒为 0 0 0。
-
nlmsg_pid:当用户空间进程与内核空间子系统通过 Netlink 建立数据交换通道后,Netlink 会为每个通道分配唯一的数字标识,该字段的作用是将用户空间的请求消息与内核的响应消息进行关联,确保多组"用户-内核"通信进程之间的数据交互不会出现紊乱。例如,当进程 A、B 同时通过 Netlink 向子系统 1 获取信息时,子系统 1 需确保回传给进程 A 的响应数据不会发送至进程 B。该字段通常适用于用户空间进程从内核空间获取数据的场景,用户空间进程向内核发送消息时,一般通过getpid()系统调用将当前进程的进程号赋值给该字段(仅当需要获取内核响应时进行该操作)。内核主动向用户空间发送消息时,该字段的值恒为 0 0 0。
Netlink 的消息体
Netlink 的消息体采用 TLV(Type-Length-Value)格式进行组织,每个属性均由头文件中的 struct nlattr{} 结构体表示。
Netlink 提供的错误指示消息
当用户空间应用程序与内核空间进程通过 Netlink 通信发生错误时,Netlink 会向用户空间通报该错误信息,错误消息采用单独封装的形式,对应的结构体 struct nlmsgerr 定义如下:
c
struct nlmsgerr
{
int error; // 标准错误码,定义在 errno.h 头文件中,可通过 perror() 函数解析
struct nlmsghdr msg; // 指明触发该错误的原始消息
};
Netlink 编程需要注意的问题
基于 Netlink 实现的用户-内核空间通信,存在两种可能导致丢包的场景:
- 系统内存耗尽;
- 用户空间接收进程的缓冲区溢出。缓冲区溢出的主要诱因包括:用户空间进程运行效率过低,或接收队列长度过短。
若 Netlink 无法将消息正确传递至用户空间接收进程,用户空间接收进程调用 recvmsg() 系统调用时,将返回 ENOBUFS(内存不足)错误,该特性需重点关注。换句话说,缓冲区溢出问题不会出现在从用户空间到内核空间的 sendmsg() 系统调用过程中,其原因前文已进行阐述,可自行进行梳理总结。
此外,若使用阻塞型 Socket 进行通信,则不存在内存耗尽的潜在风险,相关原理可通过查阅阻塞型 Socket 的官方技术文档进行深入理解。
Netlink 的地址结构体
在 TCP 编程相关内容中,曾提及 Internet 编程过程中使用的地址结构体与标准地址结构体,这些结构体与 Netlink 地址结构体存在对应关联。
Netlink 对应的地址结构体 struct sockaddr_nl{} 详细定义与描述如下:
c
struct sockaddr_nl
{
sa_family_t nl_family; /* 该字段恒为 AF_NETLINK */
unsigned short nl_pad; /* 目前未启用,填充为 0 */
__u32 nl_pid; /* process pid */
__u32 nl_groups; /* multicast groups mask */
};
-
nl_pid:该属性为发送或接收消息的进程 ID。前文提及,Netlink 不仅支持用户-内核空间之间的通信,还支持用户空间两个进程之间、内核空间两个进程之间的通信。该属性值为 0 0 0 时,通常适用于以下两种场景:-
场景一:消息的接收方为内核空间(即从用户空间发送消息至内核空间),此时构造的 Netlink 地址结构体中,
nl_pid字段通常置为 0 0 0。需要补充说明的是,在 Netlink 规范中,PID 的全称为 Port-ID( 32 32 32 bits),其作用是唯一标识一个基于 Netlink 的 Socket 通道。通常情况下,nl_pid字段会被设置为当前进程的进程号;但当一个进程的多个线程同时使用 Netlink Socket 时,nl_pid字段通常采用如下方式进行设置:cpthread_self() << 16 | getpid(); -
场景二:内核向用户空间发送多播报文时,若用户空间进程已加入对应多播组,其地址结构体中的
nl_pid字段同样置为 0 0 0,同时需结合下述nl_groups字段进行配置。
-
-
nl_groups:若用户空间进程希望加入某个多播组,必须执行bind()系统调用。该字段指明了调用者希望加入的多播组号的掩码 (注意:并非组号,其详细用法将在后续内容中阐述)。若该字段值为 0 0 0,表明调用者不希望加入任何多播组。对于每个隶属于 Netlink 协议域的协议,最多支持 32 32 32 个多播组(因nl_groups字段长度为 32 32 32 比特),每个多播组由一个独立的比特位表示。
关于 Netlink 的其余知识点,将在后续实战环节中结合具体应用场景进行阐述。
用户空间与内核空间通信------Netlink(中)
wjlkoorey258 2012-11-12 19:42:22
本节将通过实际编程演练,展示 Netlink 机制如何实现用户空间与内核空间之间的数据通信,所有实验均基于内核 2.6.21 版本环境完成。
对应的内核头文件中包含了 Netlink 协议簇预定义的各类协议,具体定义如下:
c
#define NETLINK_ROUTE 0 /* Routing/device hook */
#define NETLINK_UNUSED 1 /* Unused number */
#define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */
#define NETLINK_FIREWALL 3 /* Firewalling hook */
#define NETLINK_INET_DIAG 4 /* INET socket monitoring */
#define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */
#define NETLINK_XFRM 6 /* ipsec */
#define NETLINK_SELINUX 7 /* SELinux event notifications */
#define NETLINK_ISCSI 8 /* Open-iSCSI */
#define NETLINK_AUDIT 9 /* auditing */
#define NETLINK_FIB_LOOKUP 10
#define NETLINK_CONNECTOR 11
#define NETLINK_NETFILTER 12 /* netfilter subsystem */
#define NETLINK_IP6_FW 13
#define NETLINK_DNRTMSG 14 /* DECnet routing messages */
#define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */
#define NETLINK_GENERIC 16
/* leave room for NETLINK_DM (DM Events) */
#define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */
#define NETLINK_ECRYPTFS 19
#define NETLINK_TEST 20 /* 用户添加的自定义协议 */
若需在 Netlink 协议簇中开发自定义协议,仅需在该文件中定义对应的协议号即可,例如上述代码中定义的协议号为 20 20 20 的自定义协议 NETLINK_TEST。同时,需要对内核头文件目录中的 netlink.h 进行对应的修改,在本次实验环境中,该文件的路径为:/usr/src/linux-2.6.21/include/linux/netlink.h。
完成上述配置后,即可在用户空间与内核空间模块的开发过程中使用该自定义协议,整个实验分为三个阶段进行。
Stage 1:用户->内核单向数据通信
本阶段实现的功能为用户空间到内核空间的单向数据通信,即用户空间向内核发送一条消息,内核接收该消息并将其打印输出,具体实现如下。
用户空间示例代码【mynlusr.c】
c
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/socket.h>
#define MAX_PAYLOAD 1024 /* 消息最大负载为 1024 字节 */
int main(int argc, char* argv[])
{
struct sockaddr_nl dest_addr;
struct nlmsghdr *nlh = NULL;
struct iovec iov;
int sock_fd=-1;
struct msghdr msg;
// 创建套接字
if(-1 == (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){
perror("can't create netlink socket!");
return 1;
}
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0; /* 消息的接收方为内核空间 */
dest_addr.nl_groups = 0; /* 本示例中无需使用该字段 */
// 将套接字和 Netlink 地址结构体进行绑定
if(-1 == bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
perror("can't bind sockfd with sockaddr_nl!");
return 1;
}
if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
perror("alloc mem failed!");
return 1;
}
memset(nlh,0,MAX_PAYLOAD);
/* 填充 Netlink 消息头部 */
nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
nlh->nlmsg_pid = 0;
nlh->nlmsg_type = NLMSG_NOOP; // 标识该 Netlink 消息负载为一条空消息
nlh->nlmsg_flags = 0;
/* 设置 Netlink 的消息内容,数据来自命令行输入的第一个参数 */
strcpy(NLMSG_DATA(nlh), argv[1]);
/* 此为固定使用模板,其详细原理将在后续 Socket 深入讲解中阐述 */
memset(&iov, 0, sizeof(iov));
iov.iov_base = (void *)nlh;
iov.iov_len = nlh->nlmsg_len;
memset(&msg, 0, sizeof(msg));
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
// 通过 Netlink socket 向内核发送消息
sendmsg(sock_fd, &msg, 0);
/* 关闭 netlink 套接字 */
close(sock_fd);
free(nlh);
return 0;
}
上述代码的逻辑基于标准 Socket 编程 API 实现,唯一的差异在于本次编程针对 Netlink 协议簇进行。此处提前引入了 BSD 层的消息结构体 struct msghdr{}(定义在对应头文件中)与数据块结构体 struct iovec{}(定义在对应头文件中),其详细原理将在后续 Socket 深入讲解中阐述,当前仅需掌握其固定使用方式。
此外,需要重点关注 Netlink 地址结构体与消息头结构体中 pid 字段值为 0 0 0 的场景,避免出现概念混淆,相关总结如下表所示:
| 字段 | 值为 0 的适用场景 |
|---|---|
netlink 地址结构体.nl_pid |
1、内核发出的多播报文;2、消息的接收方为内核空间(即从用户空间发往内核空间的消息) |
netlink 消息头结构体.nlmsg_pid |
内核主动向用户空间发送的消息 |
本示例实现的是从用户空间到内核空间的单向数据通信,因此在 Netlink 地址结构体中设置 dest_addr.nl_pid = 0(标识消息接收方为内核空间),在填充 Netlink 消息头部时设置 nlh->nlmsg_pid = 0。
同时,需要掌握以下两个宏的使用方法:
NLMSG_SPACE(MAX_PAYLOAD):该宏用于返回不小于MAX_PAYLOAD且满足 4 4 4 字节对齐的最小长度值,通常用于内存申请时指定所需的内存字节数。与NLMSG_LENGTH(len)的差异在于:前者申请的空间不包含 Netlink 消息头部所占字节数,后者为消息负载与消息头的总长度。NLMSG_DATA(nlh):该宏用于返回 Netlink 消息中数据部分的首地址,在写入与读取消息数据部分时会频繁使用。
内核空间示例代码【mynlkern.c】
c
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/skbuff.h>
#include <linux/init.h>
#include <linux/ip.h>
#include <linux/types.h>
#include <linux/sched.h>
#include <net/sock.h>
#include <linux/netlink.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Koorey King");
struct sock *nl_sk = NULL;
static void nl_data_ready (struct sock *sk, int len)
{
struct sk_buff *skb;
struct nlmsghdr *nlh = NULL;
while((skb = skb_dequeue(&sk->sk_receive_queue)) != NULL)
{
nlh = (struct nlmsghdr *)skb->data;
printk("%s: received netlink message payload: %s \n", __FUNCTION__, (char*)NLMSG_DATA(nlh));
kfree_skb(skb);
}
printk("recvied finished!\n");
}
static int __init myinit_module()
{
printk("my netlink in\n");
nl_sk = netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
return 0;
}
static void __exit mycleanup_module()
{
printk("my netlink out!\n");
sock_release(nl_sk->sk_socket);
}
module_init(myinit_module);
module_exit(mycleanup_module);
在内核模块的初始化函数中,通过 netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE) 函数创建了一个内核态 Socket,该函数各参数的含义如下:
- 第一个参数:自定义协议的协议号(本次实验为
NETLINK_TEST); - 第二个参数:多播组号,本阶段实验无需使用,置为 0 0 0;
- 第三个参数:回调函数,当内核的 Netlink Socket 接收到数据时,将触发该函数进行数据处理;
- 第四个参数:内核模块标识,使用
THIS_MODULE即可。
在回调函数 nl_data_ready() 中,通过循环从 Socket 的接收队列中获取数据,获取到数据后将其打印输出,并释放对应的缓冲区资源。在协议栈的 INET 层中,数据的存储依赖 sk_buff 结构体实现,因此可通过 nlh = (struct nlmsghdr *)skb->data 获取 Netlink 消息体,再通过 NLMSG_DATA(nlh) 定位到 Netlink 消息的负载数据。
将上述代码编译后,即可进行测试验证,获取对应的运行结果。
Stage 2:用户<->内核双向数据通信
对 Stage 1 中的代码进行小幅修改,即可实现用户空间与内核空间之间的双向数据通信,具体修改如下。
用户空间代码修改
c
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/socket.h>
#define MAX_PAYLOAD 1024 /* 消息最大负载为 1024 字节 */
int main(int argc, char* argv[])
{
struct sockaddr_nl dest_addr;
struct nlmsghdr *nlh = NULL;
struct iovec iov;
int sock_fd=-1;
struct msghdr msg;
if(-1 == (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){
perror("can't create netlink socket!");
return 1;
}
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0; /* 消息的接收方为内核空间 */
dest_addr.nl_groups = 0; /* 本示例中无需使用该字段 */
if(-1 == bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
perror("can't bind sockfd with sockaddr_nl!");
return 1;
}
if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
perror("alloc mem failed!");
return 1;
}
memset(nlh,0,MAX_PAYLOAD);
/* 填充 Netlink 消息头部 */
nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
nlh->nlmsg_pid = getpid(); // 希望获取内核响应,因此设置当前进程 ID 供内核识别
nlh->nlmsg_type = NLMSG_NOOP; // 标识该 Netlink 消息负载为一条空消息
nlh->nlmsg_flags = 0;
/* 设置 Netlink 的消息内容,数据来自命令行输入的第一个参数 */
strcpy(NLMSG_DATA(nlh), argv[1]);
/* 此为固定使用模板,其详细原理将在后续 Socket 深入讲解中阐述 */
memset(&iov, 0, sizeof(iov));
iov.iov_base = (void *)nlh;
iov.iov_len = nlh->nlmsg_len;
memset(&msg, 0, sizeof(msg));
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
// 通过 Netlink socket 向内核发送消息
sendmsg(sock_fd, &msg, 0);
// 接收内核返回的响应消息
printf("waiting message from kernel!\n");
memset((char*)NLMSG_DATA(nlh),0,1024);
recvmsg(sock_fd,&msg,0);
printf("Got response: %s\n",NLMSG_DATA(nlh));
/* 关闭 netlink 套接字 */
close(sock_fd);
free(nlh);
return 0;
}
内核空间代码修改
c
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/skbuff.h>
#include <linux/init.h>
#include <linux/ip.h>
#include <linux/types.h>
#include <linux/sched.h>
#include <net/sock.h>
#include <net/netlink.h> /* 该头文件包含了 linux/netlink.h,同时提供 nlmsg_put() 等 API 函数 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Koorey King");
#define MAX_MSGSIZE 1024 /* 消息最大长度为 1024 字节 */
struct sock *nl_sk = NULL;
// 向用户空间发送消息的接口函数
void sendnlmsg(char *message,int dstPID)
{
struct sk_buff *skb;
struct nlmsghdr *nlh;
int len = NLMSG_SPACE(MAX_MSGSIZE);
int slen = 0;
if(!message || !nl_sk){
return;
}
// 为新的 sk_buffer 申请空间
skb = alloc_skb(len, GFP_KERNEL);
if(!skb){
printk(KERN_ERR "my_net_link: alloc_skb Error./n");
return;
}
slen = strlen(message)+1;
// 用 nlmsg_put() 来设置 netlink 消息头部
nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);
// 设置 Netlink 的控制块
NETLINK_CB(skb).pid = 0; // 消息发送者为内核空间,置为 0
NETLINK_CB(skb).dst_group = 0; // 目的为单个进程,该字段置为 0
message[slen] = '\0';
memcpy(NLMSG_DATA(nlh), message, slen+1);
// 通过 netlink_unicast() 将消息发送至用户空间指定 PID 的进程
netlink_unicast(nl_sk,skb,dstPID,0);
printk("send OK!\n");
return;
}
static void nl_data_ready (struct sock *sk, int len)
{
struct sk_buff *skb;
struct nlmsghdr *nlh = NULL;
while((skb = skb_dequeue(&sk->sk_receive_queue)) != NULL)
{
nlh = (struct nlmsghdr *)skb->data;
printk("%s: received netlink message payload: %s \n", __FUNCTION__, (char*)NLMSG_DATA(nlh));
kfree_skb(skb);
// 提取用户进程 PID,向其发送响应消息
sendnlmsg("I see you",nlh->nlmsg_pid);
}
printk("recvied finished!\n");
}
static int __init myinit_module()
{
printk("my netlink in\n");
nl_sk = netlink_kernel_create(NETLINK_TEST,0,nl_data_ready,THIS_MODULE);
return 0;
}
static void __exit mycleanup_module()
{
printk("my netlink out!\n");
sock_release(nl_sk->sk_socket);
}
module_init(myinit_module);
module_exit(mycleanup_module);
将修改后的代码重新编译后,即可进行测试验证,获取对应的双向通信运行结果。
Stage 3:无 bind() 调用的双向数据通信
前文提及,用户进程仅在需要加入多播组时才需要调用 bind() 函数。Stage 2 中无多播组相关需求,却调用了 bind() 函数,本次将对代码进行修改,移除 bind() 调用,改用 sendto() 与 recvfrom() 函数实现数据的收发。
用户空间代码修改
c
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/socket.h>
#define MAX_PAYLOAD 1024 /* 消息最大负载为 1024 字节 */
int main(int argc, char* argv[])
{
struct sockaddr_nl dest_addr;
struct nlmsghdr *nlh = NULL;
// struct iovec iov; // 无需使用该结构体
int sock_fd=-1;
// struct msghdr msg; // 无需使用该结构体
if(-1 == (sock_fd=socket(PF_NETLINK, SOCK_RAW,NETLINK_TEST))){
perror("can't create netlink socket!");
return 1;
}
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0; /* 消息的接收方为内核空间 */
dest_addr.nl_groups = 0; /* 本示例中无需使用该字段 */
/* 移除 bind() 函数调用 */
/*
if(-1 == bind(sock_fd, (struct sockaddr*)&dest_addr, sizeof(dest_addr))){
perror("can't bind sockfd with sockaddr_nl!");
return 1;
}
*/
if(NULL == (nlh=(struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD)))){
perror("alloc mem failed!");
return 1;
}
memset(nlh,0,MAX_PAYLOAD);
/* 填充 Netlink 消息头部 */
nlh->nlmsg_len = NLMSG_SPACE(MAX_PAYLOAD);
nlh->nlmsg_pid = getpid();// 希望获取内核响应,因此设置当前进程 ID 供内核识别
nlh->nlmsg_type = NLMSG_NOOP; // 标识该 Netlink 消息负载为一条空消息
nlh->nlmsg_flags = 0;
/* 设置 Netlink 的消息内容,数据来自命令行输入的第一个参数 */
strcpy(NLMSG_DATA(nlh), argv[1]);
/* 该模板无需使用 */
/*
memset(&iov, 0, sizeof(iov));
iov.iov_base = (void *)nlh;
iov.iov_len = nlh->nlmsg_len;
memset(&msg, 0, sizeof(msg));
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
*/
// 改用 sendto() 函数向内核发送消息
// sendmsg(sock_fd, &msg, 0);
sendto(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),sizeof(dest_addr));
// 接收内核返回的响应消息,改用 recvfrom() 函数
printf("waiting message from kernel!\n");
memset(nlh,0,MAX_PAYLOAD); // 清空整个 Netlink 消息(包含消息头与负载)
// recvmsg(sock_fd,&msg,0);
recvfrom(sock_fd,nlh,NLMSG_LENGTH(MAX_PAYLOAD),0,(struct sockaddr*)(&dest_addr),NULL);
printf("Got response: %s\n",NLMSG_DATA(nlh));
/* 关闭 netlink 套接字 */
close(sock_fd);
free(nlh);
return 0;
}
说明
内核空间的代码无需进行任何修改,仍通过 netlink_unicast() 函数向用户空间发送响应消息。将修改后的代码重新编译后,其运行效果与 Stage 2 完全一致。
由此可得结论:在 Netlink 程序开发过程中,若不涉及多播机制,用户空间的 Socket 代码无需执行 bind() 系统调用,此时需使用 sendto() 与 recvfrom() 函数完成数据的收发;若执行了 bind() 系统调用,同样可以使用 sendto() 与 recvfrom() 函数,但需调整对应的参数传递,此时更推荐使用 sendmsg() 与 recvmsg() 函数完成数据收发。开发人员可根据实际应用场景灵活选择对应的实现方式。
用户空间与内核空间通信------Netlink(下)
wjlkoorey258 2012-11-15 19:50:53
Netlink 多播机制的用法
在上一节的内容中,所有实验场景均以用户空间作为消息发起方,而 Netlink 机制同样支持内核空间作为主动消息发送方,该场景通常用于内核向用户空间主动报告自身状态变化,例如用户空间感知到的 USB 热插拔事件通告,便是基于该机制实现的。
本次实验的目标为:实现一个内核线程,该线程每隔 1 1 1 秒向一个指定多播组发送一条消息,所有加入该多播组的用户空间进程均可接收并打印该消息内容。
Netlink 地址结构体中的 nl_groups 字段为 32 32 32 位,这意味着每种 Netlink 协议最多支持 32 32 32 个多播组。此处的"每种 Netlink 协议"指的是 Netlink 协议簇中的各类预定义协议(如下所示),以及本次实验中自定义的 NETLINK_TEST 协议。
c
#define NETLINK_ROUTE 0 /* Routing/device hook */
#define NETLINK_UNUSED 1 /* Unused number */
#define NETLINK_USERSOCK 2 /* Reserved for user mode socket protocols */
#define NETLINK_FIREWALL 3 /* Firewalling hook */
#define NETLINK_INET_DIAG 4 /* INET socket monitoring */
#define NETLINK_NFLOG 5 /* netfilter/iptables ULOG */
#define NETLINK_XFRM 6 /* ipsec */
#define NETLINK_SELINUX 7 /* SELinux event notifications */
#define NETLINK_ISCSI 8 /* Open-iSCSI */
#define NETLINK_AUDIT 9 /* auditing */
#define NETLINK_FIB_LOOKUP 10
#define NETLINK_CONNECTOR 11
#define NETLINK_NETFILTER 12 /* netfilter subsystem */
#define NETLINK_IP6_FW 13
#define NETLINK_DNRTMSG 14 /* DECnet routing messages */
#define NETLINK_KOBJECT_UEVENT 15 /* Kernel messages to userspace */
#define NETLINK_GENERIC 16
/* leave room for NETLINK_DM (DM Events) */
#define NETLINK_SCSITRANSPORT 18 /* SCSI Transports */
#define NETLINK_ECRYPTFS 19
#define NETLINK_TEST 20 /* 用户添加的自定义协议 */
在自定义的 NETLINK_TEST 协议中,最多允许设置 32 32 32 个多播组,每个多播组由一个独立的比特位表示,不存在多播组重复的情况。开发人员可根据实际需求为每个多播组分配对应的功能,用户空间进程若对某个多播组的消息感兴趣,可通过加入该多播组的方式,接收内核空间向该组发送的多播消息。
回到 Netlink 地址结构体的 nl_groups 字段,该字段存储的是多播组的地址掩码(并非多播组号)。在 af_netlink.c 文件中,提供了从多播组号转换为多播组掩码的函数,具体定义如下:
c
static u32 netlink_group_mask(u32 group)
{
return group ? 1 << (group - 1) : 0;
}
由此可知,在用户空间代码中,若需加入多播组 1 1 1,需将 nl_groups 字段设置为 1 1 1;多播组 2 2 2 对应的掩码为 2 2 2;多播组 3 3 3 对应的掩码为 4 4 4,以此类推。若该字段值为 0 0 0,表明不加入任何多播组。掌握该转换关系具有重要意义,因此可在用户空间代码中实现一个功能类似 netlink_group_mask() 的函数,完成多播组号到多播组掩码的转换。
实现代码
用户空间代码
c
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <string.h>
#include <asm/types.h>
#include <linux/netlink.h>
#include <linux/socket.h>
#include <errno.h>
#define MAX_PAYLOAD 1024 // Netlink 消息的最大载荷的长度
// 多播组号转多播组掩码函数
unsigned int netlink_group_mask(unsigned int group)
{
return group ? 1 << (group - 1) : 0;
}
int main(int argc, char* argv[])
{
struct sockaddr_nl src_addr;
struct nlmsghdr *nlh = NULL;
struct iovec iov;
struct msghdr msg;
int sock_fd, retval;
// 创建 Socket
sock_fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
if(sock_fd == -1){
printf("error getting socket: %s", strerror(errno));
return -1;
}
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = PF_NETLINK;
src_addr.nl_pid = 0; // 标识从内核接收多播消息(另一个含义为消息发送方为内核)
src_addr.nl_groups = netlink_group_mask(atoi(argv[1])); // 多播组掩码,组号来自命令行输入的第一个参数
// 加入多播组必须调用 bind() 函数
retval = bind(sock_fd, (struct sockaddr*)&src_addr, sizeof(src_addr));
if(retval < 0){
printf("bind failed: %s", strerror(errno));
close(sock_fd);
return -1;
}
// 为接收 Netlink 消息申请存储空间
nlh = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PAYLOAD));
if(!nlh){
printf("malloc nlmsghdr error!\n");
close(sock_fd);
return -1;
}
memset(nlh, 0, NLMSG_SPACE(MAX_PAYLOAD));
iov.iov_base = (void *)nlh;
iov.iov_len = NLMSG_SPACE(MAX_PAYLOAD);
memset(&msg, 0, sizeof(msg));
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
// 从内核接收消息
printf("waiting for...\n");
recvmsg(sock_fd, &msg, 0);
printf("Received message: %s \n", NLMSG_DATA(nlh));
close(sock_fd);
return 0;
}
说明
用户空间程序的整体逻辑与前文保持一致,差异在于 nl_groups 字段的设置,该字段的相关文档较为稀缺,其详细用法可通过查阅内核源码与相关技术资料进一步获取。
内核空间代码
c
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/skbuff.h>
#include <linux/init.h>
#include <linux/ip.h>
#include <linux/types.h>
#include <linux/sched.h>
#include <net/sock.h>
#include <net/netlink.h>
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Koorey King");
#define MAX_MSGSIZE 1024 /* 消息最大长度为 1024 字节 */
struct sock *nl_sk = NULL;
static struct task_struct *mythread = NULL; // 内核线程对象
// 向用户空间发送消息的接口函数
void sendnlmsg(char *message)
{
struct sk_buff *skb;
struct nlmsghdr *nlh;
int len = NLMSG_SPACE(MAX_MSGSIZE);
int slen = 0;
if(!message || !nl_sk){
return;
}
// 为新的 sk_buffer 申请空间
skb = alloc_skb(len, GFP_KERNEL);
if(!skb){
printk(KERN_ERR "my_net_link: alloc_skb Error./n");
return;
}
slen = strlen(message)+1;
// 用 nlmsg_put() 来设置 netlink 消息头部
nlh = nlmsg_put(skb, 0, 0, 0, MAX_MSGSIZE, 0);
// 设置 Netlink 的控制块里的相关信息
NETLINK_CB(skb).pid = 0; // 消息发送者为内核空间,置为 0
NETLINK_CB(skb).dst_group = 5; // 多播组号为 5
message[slen] = '\0';
memcpy(NLMSG_DATA(nlh), message, slen+1);
// 发送多播消息到多播组 5
netlink_broadcast(nl_sk, skb, 0,5, GFP_KERNEL);
printk("send OK!\n");
return;
}
// 内核线程函数:每隔 1 秒钟发送一条"I am from kernel!"消息,共发送 10 条
static int sending_thread(void *data)
{
int i = 10;
struct completion cmpl;
while(i--){
init_completion(&cmpl);
wait_for_completion_timeout(&cmpl, 1 * HZ);
sendnlmsg("I am from kernel!");
}
printk("sending thread exited!");
return 0;
}
static int __init myinit_module()
{
printk("my netlink in\n");
nl_sk = netlink_kernel_create(NETLINK_TEST,0,NULL,THIS_MODULE);
if(!nl_sk){
printk(KERN_ERR "my_net_link: create netlink socket error.\n");
return 1;
}
printk("my netlink: create netlink socket ok.\n");
mythread = kthread_run(sending_thread,NULL,"thread_sender");
return 0;
}
static void __exit mycleanup_module()
{
if(nl_sk != NULL){
sock_release(nl_sk->sk_socket);
}
printk("my netlink out!\n");
}
module_init(myinit_module);
module_exit(mycleanup_module);
补充说明
-
内核函数
netlink_kernel_create(int unit, unsigned int groups,...)的第二个参数,表示内核进程最多能处理的多播组个数,若该值小于 32 32 32,则默认按 32 32 32 处理。因此,调用该函数时,通常可将第二个参数置为 0 0 0。 -
struct sk_buff结构体中的cb[48]字段为控制缓冲区,可供各层协议存储私有变量,Netlink 机制通过将该字段强制转换为struct netlink_skb_parms{}结构体,填充 Netlink 通信所需的私有信息,其固定填充模板如下:cNETLINK_CB(skb).pid=xx; NETLINK_CB(skb).dst_group=xx; -
本次实验中,将
NETLINK_CB(skb).dst_group设置为对应多播组号或 0 0 0,用户空间均能收到多播消息,其底层原因需进一步查阅内核源码与深入分析 Netlink 多播机制实现原理。
测试与注意事项
- 编译完成后,需先执行
insmod命令加载内核模块,再运行用户空间程序。若未加载mynlkern.ko内核模块而直接运行./test 5,bind()系统调用将返回No such file or directory错误。 - 部分老版本 Netlink 多播教程中提及的"先运行用户空间程序,再加载内核模块"的方式,在内核 2.6.21 版本中已不再适用,需重点注意。
小结
通过三篇内容的阐述,
- 明确了 Netlink 的关键特性(无连接、基于 BSD Socket、支持单播/多播)、消息格式(16 字节固定头+可变负载)与编程关键要点(
pid字段取值、多播组号与掩码转换、bind()调用的适用场景); - 梳理了 Netlink 编程的三个阶段(单向通信、双向通信、无
bind()通信)与多播机制的实现流程,补充了实验注意事项与后续深入学习的方向;
可对 Netlink 机制形成初步认知,并能够开发基于 Netlink 的基础应用程序。但这仅为该机制的入门内容,若要开发高质量、高效率的 Netlink 应用模块,还需进一步深入理解其底层本质,同时掌握内核编程的相关基础能力,例如临界资源的互斥保护、线程安全性保障、大数据量传输的处理策略等,这些均为实际开发中需要重点考虑的问题。
Linux 内核与用户空间通信之 Netlink 使用方法
HAOMCU 转载时间:2012-03-20 09:41:52
1 简介
本文介绍 Linux 内核中的 Netlink 通信机制,详细阐述 Netlink 的工作原理与技术优势,包括其支持的多种协议类型、异步通信机制及多播特性,并通过实例展示用户空间与内核空间的通信过程。
Linux 中的进程间通信机制源自 Unix 平台的进程通信机制。Unix 的两大分支 AT&T Unix 和 BSD Unix 在进程通信实现机制上存在差异:前者形成适用于单台计算机的 System V IPC,后者实现基于 Socket 的进程间通信机制。同时,Linux 遵循 IEEE 制定的 Posix IPC 标准,在上述三类机制基础上实现以下主要 IPC 机制:管道(Pipe)及命名管道(Named Pipe)、信号(Signal)、消息队列(Message queue)、共享内存(Shared Memory)、信号量(Semaphore)、套接字(Socket)。借助这些 IPC 机制,用户空间进程间可完成数据交互。为实现内核空间与用户空间的通信,Linux 提供基于 Socket 的 Netlink 通信机制,可实现内核空间与用户空间之间数据的实时交互。
本文第 2 节概述相关研究工作,第 3 节对比其他 IPC 机制,详细介绍 Netlink 机制及其关键技术,第 4 节采用 KGDB+GDB 组合调试方式,通过示例程序演示 Netlink 通信过程,第 5 节总结并指出 Netlink 通信机制的不足之处。
2 相关研究
截至目前,Linux 提供 9 种实现内核空间与用户空间数据交换的机制,分别为内核启动参数、模块参数与 sysfs、sysctl、系统调用、netlink、procfs、seq_file、debugfs 和 relayfs。其中,模块参数与 sysfs、procfs、debugfs、relayfs 属于基于文件系统的通信机制,主要用于内核空间向用户空间输出信息;sysctl、系统调用为用户空间发起的通信机制。由此可见,上述机制均为单工通信机制,在内核空间与用户空间的双向交互式数据交换场景下存在一定的局限性。
Netlink 是基于 Socket 的通信机制,依托 Socket 本身具备的双向性、突发性、非阻塞特性,能够较好地满足内核空间与用户空间小量数据的实时交互需求,因此在 Linux 2.6 内核中被广泛应用。例如 SELinux 组件,以及 Linux 系统防火墙的内核态组件 netfilter 与用户态组件 iptables 之间的数据交换,均通过 Netlink 机制完成。
3 Netlink 机制及其关键技术
3.1 Netlink 机制
在 Linux 操作系统中,CPU 处于内核态时可分为两种场景:存在用户上下文的状态、执行硬件中断或软件中断的状态。其中,在存在用户上下文的场景下,由于内核态与用户态的内存映射机制不同,无法直接将本地变量传递至用户态内存区域;在执行硬件中断或软件中断的场景下,代码执行过程不可中断,同样无法直接向用户内存区域传递数据。
传统进程间通信机制均无法直接应用于内核态与用户态之间的通信,具体原因如表 1 所示:
| 通信方法 | 无法应用于内核态与用户态的原因 |
|---|---|
| 管道(不含命名管道) | 仅支持父子进程间的通信 |
| 消息队列 | 无法在硬件中断、软件中断中无阻塞接收数据 |
| 信号量 | 无法跨内核态与用户态使用 |
| 共享内存 | 需信号量辅助实现同步,而信号量无法跨态使用 |
| 套接字 | 无法在硬件中断、软件中断中无阻塞接收数据 |
表 1(引自 参考文献 5)
解决内核态与用户态通信问题的方案可分为两类:
- 存在用户上下文时,可调用 Linux 提供的
copy_from_user()和copy_to_user()函数完成数据传输,但这两个函数可能产生阻塞,因此无法在硬件中断、软件中断过程中调用; - 执行硬件中断或软件中断时,可通过以下两种方式实现:
2.1 借助 Linux 内核提供的 spinlock 自旋锁实现内核线程与中断过程的同步。由于内核线程运行在有上下文的进程环境中,因此可在内核线程中通过套接字或消息队列获取用户空间数据,再通过临界区将数据传递至中断过程;
2.2 基于 Netlink 机制实现。Netlink 套接字的通信标识通常为进程的 ID(进程标识符)。Netlink 通信的显著特征是对中断过程的良好支持:内核空间接收用户空间数据时,无需用户自行启动内核线程,而是通过软中断调用用户预先指定的接收函数。这种基于软中断的实现方式相较于自行启动内核线程,能够保障数据传输的实时性。
3.2 Netlink 优势
相较于其他通信机制,Netlink 具备以下优势:
- 基于 Netlink 自定义新协议并加入协议族后,即可通过 Socket API 完成数据交换;而 ioctl 和 proc 文件系统需通过程序新增对应的设备或文件才能实现通信;
- Netlink 采用 Socket 缓存队列实现异步通信,而 ioctl 为同步通信机制,若传输数据量较大,易降低系统性能;
- Netlink 支持多播特性,归属同一 Netlink 组的模块与进程均可接收该组的多播消息;
- Netlink 允许内核主动发起会话,而 ioctl 与系统调用仅能由用户空间进程发起。
内核源码中关于 Netlink 协议的头文件定义了预定义协议类型,具体如下:
c
#define NETLINK_ROUTE 0
#define NETLINK_W1 1
#define NETLINK_USERSOCK 2
#define NETLINK_FIREWALL 3
#define NETLINK_INET_DIAG 4
#define NETLINK_NFLOG 5
#define NETLINK_XFRM 6
#define NETLINK_SELINUX 7
#define NETLINK_ISCSI 8
#define NETLINK_AUDIT 9
#define NETLINK_FIB_LOOKUP 10
#define NETLINK_CONNECTOR 11
#define NETLINK_NETFILTER 12
#define NETLINK_IP6_FW 13
#define NETLINK_DNRTMSG 14
#define NETLINK_KOBJECT_UEVENT 15
#define NETLINK_GENERIC 16
上述协议已适配不同的系统应用场景,每种应用均有专属的传输数据格式。若用户无需使用这些预定义协议,可新增自定义协议号。针对每个 Netlink 协议类型,最多可设置 32 个多播组,每个多播组由 1 个比特位标识。Netlink 的多播特性使得向同一组发送消息仅需一次系统调用,对于需传输多播消息的应用而言,可显著降低系统调用次数。
建立 Netlink 会话的流程如下:

内核通过一套与标准 Socket API 相似的接口完成通信过程,首先调用 netlink_kernel_create() 创建套接字,该函数原型为:
c
struct sock *netlink_kernel_create(struct net *net,
int unit,unsigned int groups,
void (*input)(struct sk_buff *skb),
struct mutex *cb_mutex,
struct module *module);
其中,net 参数为网络设备命名空间指针;input 函数为 Netlink Socket 接收消息时触发的回调函数指针;module 参数默认值为 THIS_MODULE。
用户空间进程则通过标准 Socket API 创建套接字,并将进程 ID 发送至内核空间。用户空间调用 socket() 创建套接字,函数原型为:
c
int socket(int domain, int type, int protocol);
其中,domain 参数取值为 PF_NETLINK(即 Netlink 协议族);protocol 参数为 Netlink 预定义协议(如 NETLINK_ROUTE、NETLINK_FIREWALL、NETLINK_ARPD、NETLINK_ROUTE6、NETLINK_IP6_FW)或用户自定义协议。
随后调用 bind() 函数完成绑定操作。Netlink 的 bind() 函数将本地 Socket 地址(源 Socket 地址)与已打开的 Socket 关联,绑定完成且内核空间接收用户进程 ID 后,即可开展双向通信。
用户空间进程通过标准 Socket API 中的 sendmsg() 函数发送数据,调用时需构造 struct msghdr 消息结构与 nlmsghdr 消息头。Netlink 消息体由 nlmsghdr 消息头和消息载荷(payload)组成,输入消息后,内核会访问 nlmsghdr 指向的缓冲区。
内核空间通过独立创建的 sk_buff 缓冲区发送数据,Linux 定义以下宏用于简化缓冲区地址配置:
#define \ NETLINK_CB(skb) \ ((struct \ netlink_skb_parms) &((skb)->cb))$$
缓冲区完成消息地址配置后,可调用 netlink_unicast() 发送单播消息,该函数原型为:
int netlink_unicast(struct sock *sk, struct sk_buff *skb, u32 pid, int nonblock);
参数说明:
sk:netlink_kernel_create()函数返回的 Socket 指针;skb:存储待发送消息的缓冲区,其data字段指向 Netlink 消息结构,控制块保存消息地址信息(可通过NETLINK_CB(skb)宏配置);pid:接收消息进程的进程 ID;nonblock:函数是否为非阻塞模式(取值 1 时,无可用接收缓存则立即返回;取值 0 时,无可用接收缓存则进入睡眠状态)。
内核模块或子系统可调用 netlink_broadcast() 发送广播消息,函数原型为:
void netlink_broadcast(struct sock *sk, struct sk_buff *skb, u32 pid, u32 group, int allocation);
前 3 个参数与 netlink_unicast() 一致;group 为接收消息的多播组标识(需向多个多播组发送消息时,取值为多个组 ID 的按位或结果);allocation 为内核内存分配类型(常用 GFP_ATOMIC 或 GFP_KERNEL,前者适用于原子上下文(不可睡眠),后者适用于非原子上下文)。
接收数据时,程序需申请足够大小的内存空间,以存储 Netlink 消息头与消息载荷,随后调用标准函数 recvmsg() 接收消息。
4 Netlink 通信过程
4.1 调试环境与工具
调试平台:Vmware 5.5 + Fedora Core 10(两台主机,分别作为 host 机与 target 机);
调试工具:KGDB+GDB 组合(Linux 内核 2.6.26 及以上版本内置 KGDB 选项,编译内核时需启用相关配置;调试时 host 端使用带符号表的 vmlinz 内核,target 端通过 GDB 调试用户空间程序)。
4.2 调试程序实现
调试程序分为内核模块与用户空间程序两部分:内核模块加载后,运行用户空间程序,由用户空间发起 Netlink 会话,与内核模块完成数据交换。
4.2.1 用户空间程序关键代码
c
int send_pck_to_kern(u8 op, const u8 *data, u16 data_len)
{
struct user_data_ *pck;
int ret;
// 分配内存并初始化
pck = (struct user_data_*)calloc(1, sizeof(*pck) + data_len);
if(!pck) {
printf("calloc in %s failed!!!\n", __FUNCTION__);
return -1;
}
// 填充消息结构体
pck->magic_num = MAGIC_NUM_RNQ;
pck->op = op;
pck->data_len = data_len;
memcpy(pck->data, data, data_len);
// 发送数据至内核
ret = send_to_kern((const u8*)pck, sizeof(*pck) + data_len);
if(ret)
printf("send_to_kern in %s failed!!!\n", __FUNCTION__);
// 释放内存
free(pck);
return ret ? -1 : 0;
}
static void recv_from_nl()
{
char buf[1000];
int len;
struct iovec iov = {buf, sizeof(buf)};
struct sockaddr_nl sa;
struct msghdr msg;
struct nlmsghdr *nh;
// 初始化消息头
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void *)&sa;
msg.msg_namelen = sizeof(sa);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
// 接收内核消息
len = recvmsg(nl_sock, &msg, 0);
// 解析接收到的消息
for (nh = (struct nlmsghdr *)buf; NLMSG_OK(nh, len); nh = NLMSG_NEXT (nh, len)) {
// 多段消息结束标识
if (nh->nlmsg_type == NLMSG_DONE) {
puts("nh->nlmsg_type == NLMSG_DONE");
return;
}
// 消息错误标识
if (nh->nlmsg_type == NLMSG_ERROR) {
puts("nh->nlmsg_type == NLMSG_ERROR");
return;
}
#if 1
// 打印从内核接收的数据
puts("Data received from kernel:");
hex_dump((u8*)NLMSG_DATA(nh), NLMSG_PAYLOAD(nh, 0));
#endif
}
}
4.2.2 内核模块关键代码
内核模块需防止资源抢占,确保 Netlink 资源的互斥访问,关键代码如下:
c
static void nl_rcv(struct sk_buff *skb)
{
// 加锁保证资源互斥访问
mutex_lock(&nl_mtx);
netlink_rcv_skb(skb, &nl_rcv_msg);
mutex_unlock(&nl_mtx);
}
static int nl_send_msg(const u8 *data, int data_len)
{
struct nlmsghdr *rep;
u8 *res;
struct sk_buff *skb;
// 参数合法性校验
if(g_pid < 0 || g_nl_sk == NULL) {
printk("Invalid parameter, g_pid = %d, g_nl_sk = %p\n", g_pid, g_nl_sk);
return -1;
}
// 分配新的sk_buff缓冲区
skb = nlmsg_new(data_len, GFP_KERNEL);
if(!skb) {
printk("nlmsg_new failed!!!\n");
return -1;
}
// 调试模式下打印待发送数据
if(g_debug_level > 0) {
printk("Data to be send to user space:\n");
hex_dump((void*)data, data_len);
}
// 填充Netlink消息头
rep = __nlmsg_put(skb, g_pid, 0, NLMSG_NOOP, data_len, 0);
res = nlmsg_data(rep);
memcpy(res, data, data_len);
// 发送单播消息至用户空间
netlink_unicast(g_nl_sk, skb, g_pid, MSG_DONTWAIT);
return 0;
}
static int nl_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)
{
const u8 res_data[] = "Hello, user";
size_t data_len;
u8 *buf;
struct user_data_ *pck;
struct user_req *req, *match = NULL;
// 获取发送方进程ID
g_pid = NETLINK_CB(skb).pid;
buf = (u8*)NLMSG_DATA(nlh);
data_len = nlmsg_len(nlh);
// 数据长度校验
if(data_len < sizeof(struct user_data_)) {
printk("Too short data from user space!!!\n");
return -1;
}
// 解析用户空间消息
pck = (struct user_data_ *)buf;
if(pck->magic_num != MAGIC_NUM_RNQ) {
printk("Magic number not matched!!!\n");
return -1;
}
// 调试模式下打印用户空间数据
if(g_debug_level > 0) {
printk("Data from user space:\n");
hex_dump(buf, data_len);
}
// 匹配对应的请求处理函数
req = user_reqs;
while(req->op) {
if(req->op == pck->op) {
match = req;
break;
}
req++;
}
// 执行匹配的处理函数
if(match) {
match->handler(buf, data_len);
}
// 向用户空间回复消息
nl_send_msg(res_data, sizeof(res_data));
return 0;
}
5 其他相关说明
Netlink 是 Linux 特有的一种专用 Socket,功能类似于 BSD 系统中的 AF_ROUTE,但具备更丰富的功能。在 Linux 2.6.14 内核版本中,大量应用通过 Netlink 实现应用层与内核层的通信,典型场景包括:
- 路由守护进程(NETLINK_ROUTE);
- 1-wire 子系统(NETLINK_W1);
- 用户态 Socket 协议(NETLINK_USERSOCK);
- 防火墙(NETLINK_FIREWALL);
- Socket 监控(NETLINK_INET_DIAG);
- netfilter 日志(NETLINK_NFLOG);
- IPsec 安全策略(NETLINK_XFRM);
- SELinux 事件通知(NETLINK_SELINUX);
- iSCSI 子系统(NETLINK_ISCSI);
- 进程审计(NETLINK_AUDIT);
- 转发信息表查询(NETLINK_FIB_LOOKUP);
- Netlink 连接器(NETLINK_CONNECTOR);
- netfilter 子系统(NETLINK_NETFILTER);
- IPv6 防火墙(NETLINK_IP6_FW);
- DECnet 路由信息(NETLINK_DNRTMSG);
- 内核事件向用户态通知(NETLINK_KOBJECT_UEVENT);
- 通用 Netlink(NETLINK_GENERIC)。
Netlink 是实现内核与用户应用间双向数据传输的高效方式:用户态应用可通过标准 Socket API 调用 Netlink 的功能,内核态则需通过专用内核 API 实现 Netlink 通信。
相较于系统调用、ioctl 及 /proc 文件系统,Netlink 具备以下优势:
- 使用 Netlink 时,仅需在
include/linux/netlink.h中新增 Netlink 协议定义(如#define NETLINK_MYTEST 17),内核与用户态应用即可通过 Socket API 基于该协议完成数据交换;而新增系统调用需修改内核源码并静态编译,ioctl 需新增设备/文件,/proc 文件系统需新增文件/目录,均会增加开发成本或导致 /proc 目录结构混乱; - Netlink 为异步通信机制,内核与用户态应用间的消息存储于 Socket 缓存队列,发送消息仅需将其存入接收者的 Socket 接收队列,无需等待接收方确认;而系统调用与 ioctl 为同步通信机制,传输大尺寸数据时会影响系统调度粒度;
- 基于 Netlink 的内核模块可动态加载,应用层与内核层无编译期依赖;而新增系统调用需静态链接至内核,无法通过模块实现,且应用层编译时需依赖内核头文件;
- Netlink 支持多播特性,内核模块或应用可将消息多播至指定 Netlink 组,归属该组的所有内核模块/应用均可接收消息(内核事件向用户态通知机制即基于此特性);
- 内核可主动发起 Netlink 会话,而系统调用与 ioctl 仅能由用户应用发起;
- Netlink 基于标准 Socket API 实现,开发门槛低;而系统调用与 ioctl 需掌握专用开发规范,学习成本更高。
用户态使用 Netlink
用户态应用可通过标准 Socket API(socket()、bind()、sendmsg()、recvmsg()、close())便捷使用 Netlink Socket,需包含头文件 linux/netlink.h 与 sys/socket.h(Socket 基础头文件)。
5.1 创建 Netlink Socket
调用 socket() 函数创建 Netlink Socket,原型如下:
KaTeX parse error: Expected 'EOF', got '_' at position 16: \text{socket(AF_̲NETLINK, SOCK_R...
参数说明:
- 第一个参数:必须为
AF_NETLINK或PF_NETLINK(Linux 中二者等价),标识使用 Netlink 协议族; - 第二个参数:必须为
SOCK_RAW或SOCK_DGRAM; - 第三个参数:指定 Netlink 协议类型(如自定义的
NETLINK_MYTEST,或通用协议NETLINK_GENERIC)。
内核预定义的 Netlink 协议类型如下:
c
#define NETLINK_ROUTE 0
#define NETLINK_W1 1
#define NETLINK_USERSOCK 2
#define NETLINK_FIREWALL 3
#define NETLINK_INET_DIAG 4
#define NETLINK_NFLOG 5
#define NETLINK_XFRM 6
#define NETLINK_SELINUX 7
#define NETLINK_ISCSI 8
#define NETLINK_AUDIT 9
#define NETLINK_FIB_LOOKUP 10
#define NETLINK_CONNECTOR 11
#define NETLINK_NETFILTER 12
#define NETLINK_IP6_FW 13
#define NETLINK_DNRTMSG 14
#define NETLINK_KOBJECT_UEVENT 15
#define NETLINK_GENERIC 16
每个 Netlink 协议类型最多支持 32 个多播组,每个多播组由 1 个比特位标识。Netlink 的多播特性可显著降低多播场景下的系统调用次数。
5.2 绑定 Netlink Socket
bind() 函数用于将已打开的 Netlink Socket 与本地 Netlink 地址绑定,Netlink Socket 地址结构定义如下:
c
struct sockaddr_nl {
sa_family_t nl_family;
unsigned short nl_pad;
__u32 nl_pid;
__u32 nl_groups;
};
字段说明:
nl_family:必须设置为AF_NETLINK或PF_NETLINK;nl_pad:预留字段,需设置为 0;nl_pid:接收/发送消息的进程 ID(内核处理/多播消息时设为 0,否则设为进程 ID;多线程场景下可自定义,如pthread_self() << 16 | getpid());nl_groups:多播组标识(设为 0 表示不加入任何多播组)。
bind() 函数调用示例:
c
bind(fd, (struct sockaddr*)&nladdr, sizeof(struct sockaddr_nl));
其中,fd 为 socket() 函数返回的文件描述符,nladdr 为 struct sockaddr_nl 类型的地址结构体。
5.3 发送 Netlink 消息
向内核/其他用户态应用发送 Netlink 消息时,需构造目标 Netlink 地址、struct msghdr、struct nlmsghdr 与 struct iovec 结构体:
5.3.1 消息头与地址配置
c
struct msghdr msg;
memset(&msg, 0, sizeof(msg));
msg.msg_name = (void *)&(nladdr); // 目标Netlink地址
msg.msg_namelen = sizeof(nladdr);
5.3.2 Netlink 消息头配置
struct nlmsghdr 为 Netlink 消息头(控制块),定义如下:
c
struct nlmsghdr {
__u32 nlmsg_len; // 消息总长度(含消息头+载荷)
__u16 nlmsg_type; // 应用自定义消息类型(内核透传,通常设为0)
__u16 nlmsg_flags; // 消息标志(普通场景设为0)
__u32 nlmsg_seq; // 消息序列号(应用追踪消息用)
__u32 nlmsg_pid; // 消息发送进程ID
};
消息标志(nlmsg_flags)可选值:
c
#define NLM_F_REQUEST 1
#define NLM_F_MULTI 2
#define NLM_F_ACK 4
#define NLM_F_ECHO 8
#define NLM_F_ROOT 0x100
#define NLM_F_MATCH 0x200
#define NLM_F_ATOMIC 0x400
#define NLM_F_DUMP (NLM_F_ROOT|NLM_F_MATCH)
#define NLM_F_REPLACE 0x100
#define NLM_F_EXCL 0x200
#define NLM_F_CREATE 0x400
#define NLM_F_APPEND 0x800
5.3.3 消息载荷与发送示例
c
#define MAX_MSGSIZE 1024
char buffer[] = "An example message";
struct nlmsghdr *nlhdr;
// 分配消息缓冲区
nlhdr = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_MSGSIZE));
// 填充消息载荷
strcpy(NLMSG_DATA(nlhdr),buffer);
// 设置消息长度
nlhdr->nlmsg_len = NLMSG_LENGTH(strlen(buffer));
// 设置发送进程ID
nlhdr->nlmsg_pid = getpid();
// 消息标志设为0
nlhdr->nlmsg_flags = 0;
// 构造iovec结构体
struct iovec iov;
iov.iov_base = (void *)nlhdr;
iov.iov_len = nlhdr->nlmsg_len;
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
// 发送消息
sendmsg(fd, &msg, 0);
5.4 接收 Netlink 消息
接收消息需先分配足够大的缓冲区,再调用 recvmsg() 函数,示例如下:
c
#define MAX_NL_MSG_LEN 1024
struct sockaddr_nl nladdr;
struct msghdr msg;
struct iovec iov;
struct nlmsghdr * nlhdr;
// 分配缓冲区
nlhdr = (struct nlmsghdr *)malloc(MAX_NL_MSG_LEN);
iov.iov_base = (void *)nlhdr;
iov.iov_len = MAX_NL_MSG_LEN;
// 初始化消息头
msg.msg_name = (void *)&(nladdr);
msg.msg_namelen = sizeof(nladdr);
msg.msg_iov = &iov;
msg.msg_iovlen = 1;
// 接收消息
recvmsg(fd, &msg, 0);
接收完成后:
nlhdr指向消息头;nladdr存储消息的目标地址;NLMSG_DATA(nlhdr)宏返回消息载荷的首地址。
5.5 Netlink 消息处理宏
linux/netlink.h 定义了以下常用宏,用于简化消息处理:
-
字节对齐宏:
c#define \ NLMSG\_ALIGNTO \ 4 #define \ NLMSG\_ALIGN(len) \ ( ((len)+NLMSG\_ALIGNTO-1) \& \sim(NLMSG\_ALIGNTO-1) )功能:获取不小于
len且满足字节对齐的最小数值。 -
消息长度计算宏:
c#define \ NLMSG\_LENGTH(len) \ ((len)+NLMSG\_ALIGN(sizeof(struct \ nlmsghdr)))功能:计算载荷长度为
len时的消息总长度(用于分配缓冲区)。 -
缓冲区空间计算宏:
c#define \ NLMSG\_SPACE(len) \ NLMSG\_ALIGN(NLMSG\_LENGTH(len))功能:返回不小于
NLMSG_LENGTH(len)且字节对齐的最小数值(用于缓冲区分配)。 -
载荷地址获取宏:
c#define \ NLMSG\_DATA(nlh) \ ((void*)(((char*)nlh) + NLMSG\_LENGTH(0)))功能:返回消息载荷的首地址。
-
下一条消息获取宏:
c#define \ NLMSG\_NEXT(nlh,len) \ ((len) -= NLMSG\_ALIGN((nlh)->nlmsg\_len), \\ (struct \ nlmsghdr*)(((char*)(nlh)) + NLMSG\_ALIGN((nlh)->nlmsg\_len)))功能:获取下一条消息的首地址,并更新剩余消息长度。
-
消息合法性校验宏:
c#define \ NLMSG\_OK(nlh,len) \ ((len) >= (int)sizeof(struct \ nlmsghdr) \&\& \\ (nlh)->nlmsg\_len >= sizeof(struct \ nlmsghdr) \&\& \\ (nlh)->nlmsg\_len <= (len))功能:校验消息长度是否合法。
-
载荷长度计算宏:
c#define \ NLMSG\_PAYLOAD(nlh,len) \ ((nlh)->nlmsg\_len - NLMSG\_SPACE((len)))功能:返回消息载荷的实际长度。
5.6 关闭 Netlink Socket
调用 close(fd) 函数关闭已打开的 Netlink Socket 文件描述符即可。
Netlink 内核 API
Netlink 的内核实现在 net/core/af_netlink.c 文件中,内核模块使用 Netlink 需包含 linux/netlink.h 头文件,并调用专用内核 API。
6.1 新增 Netlink 协议类型
如需新增自定义 Netlink 协议类型,需修改 linux/netlink.h,示例如下:
c
#define \ NETLINK\_MYTEST \ 17
也可直接使用通用协议类型 NETLINK_GENERIC,无需新增定义。
6.2 创建内核 Netlink Socket
调用 netlink_kernel_create() 函数创建内核态 Netlink Socket,原型如下:
KaTeX parse error: Expected 'EOF', got '_' at position 28: ... sock * netlink_̲kernel_create(i...
参数说明:
unit:Netlink 协议类型(如NETLINK_MYTEST);input:消息处理回调函数(Socket 接收消息时触发)。
6.3 消息处理回调函数
回调函数有两种实现方式:
6.3.1 直接处理消息(适用于短消息)
c
void input (struct sock *sk, int len) {
struct sk_buff *skb;
struct nlmsghdr *nlh = NULL;
u8 *data = NULL;
while ((skb = skb_dequeue(&sk->receive_queue)) != NULL) {
nlh = (struct nlmsghdr *)skb->data;
data = NLMSG_DATA(nlh);
// 处理消息载荷
}
}
6.3.2 唤醒内核线程处理(适用于长消息)
c
void input (struct sock *sk, int len) {
// 唤醒等待队列中的内核线程
wake_up_interruptible(sk->sk_sleep);
}
内核线程可通过 skb_recv_datagram(nl_sk) 接收消息(无消息时进入睡眠状态)。
6.4 内核态发送消息
6.4.1 地址配置
通过 NETLINK_CB(skb) 宏配置消息地址(源/目标):
c
NETLINK_CB(skb).pid = 0; // 源地址(内核设为0)
NETLINK_CB(skb).dst_pid = 0; // 目标进程ID(内核/多播设为0)
NETLINK_CB(skb).dst_group = 1; // 目标多播组(单播设为0)
6.4.2 发送单播消息
调用 netlink_unicast() 函数(原型见 3.2 节)。
6.4.3 发送广播消息
调用 netlink_broadcast() 函数(原型见 3.2 节)。
6.5 释放内核 Netlink Socket
调用 sock_release() 函数释放 Socket,示例如下:
KaTeX parse error: Expected 'EOF', got '' at position 11: \text{sock_̲release(sk->sk...
其中,sk 为 netlink_kernel_create() 函数的返回值。
6.6 示例程序说明
配套示例程序包含:
- 内核模块:
netlink-exam-kern.c; - 用户态程序:
netlink-exam-user-recv.c(接收)、netlink-exam-user-send.c(发送)。
运行流程:
- 加载内核模块;
- 终端 1 运行接收程序;
- 终端 2 运行发送程序(读取指定文本文件,通过 Netlink 发送至内核);
- 内核模块接收消息后,通过
/proc/netlink_exam_buffer暴露数据,并将消息回传给接收程序; - 接收程序打印消息内容至屏幕。
总结
- Netlink 是 Linux 特有的基于 Socket 的双向异步通信机制,支持多播、内核主动发起会话,适配内核态与用户态的通信需求,在 2.6 及以上内核版本中广泛应用;
- 用户态通过标准 Socket API(
socket()/bind()/sendmsg()/recvmsg())使用 Netlink,内核态需调用专用 API(netlink_kernel_create()/netlink_unicast()等); - Netlink 消息由
nlmsghdr头和载荷组成,可通过内核预定义宏简化消息长度计算、载荷解析等操作,相较于系统调用、ioctl 等机制具备开发成本低、性能优、灵活性高的特点。
Linux 内核 netlink 机制 - 用户空间和内核空间数据传输
hinewcc 原创已于 2024-12-31 09:49:56 修改
1 简介
Netlink socket 是 Linux 系统特有的套接字类型,是实现用户空间 与内核空间 进程间通信(Inter-Process Communication, IPC)的专用机制,同时也是网络应用程序与内核进行交互的主流接口。
Netlink 为内核与用户态应用之间的双向数据传输提供了高效的实现方式:用户态应用可通过标准的 socket API 调用 Netlink 提供的功能,而内核态需借助专用的内核 API 完成 Netlink 相关操作。Netlink 接口分为应用层 接口与内核 接口两类,在实际开发中,需于应用程序侧实现策略逻辑 ,并在内核侧实现底层机制。
用户空间与内核空间的常用通信方式包含以下三类:proc 文件系统、ioctl 调用、Netlink 机制:
(1)/proc 文件系统 - 单向通信:作为虚拟文件系统,主要用于用户空间从内核获取信息、输出数据;
(2)ioctl 调用 - 单向通信:不支持异步信息发送,仅用于用户空间向内核传递控制命令;
(3)Netlink 机制 - 双向通信:内核可主动发起数据传输,而非仅响应用户空间请求并返回信息。
1.1 netlink 机制的优势
- 支持全双工、异步通信模式;
- 用户空间可直接使用标准 socket 接口完成通信操作;
- 具备多播通信能力;
- 内核端可在进程上下文与中断上下文环境中使用。
1.2 netlink 机制的典型应用场景
- 获取或修改系统路由信息;
- 监听 TCP 协议数据报文;
- 防火墙功能实现;
- netfilter 子系统交互;
- 内核事件向用户态的主动通知。
2 netlink 常用数据结构及函数
2.1 netlink 应用层数据结构及函数
在网络编程中,通常通过 IP 地址 + 端口号 完成寻址操作;而 netlink 机制则通过 协议类型 + 进程 ID 实现寻址,其中 协议类型 需在调用 socket 接口 时指定。
2.1.1 消息结构
netlink 消息由消息头与消息体两部分组成:消息头固定占用 16 字节,其后紧跟消息体(有效数据)。

- Message Length:消息总长度,包含 netlink 消息头在内的全部字节数,占用 4 字节;
- Type:消息类型,多用于协议交互流程,内核定义了若干标准消息类型,且已基于 netlink 实现多种协议,每种协议对应特定的类型与功能,占用 2 字节;
- Flags:消息标志位,占用 2 字节;
- Sequence number:序列号,为可选字段,功能类比 TCP 协议中的报文序号,占用 4 字节;
- PID(port ID):端口标识,即发送端的端口 ID 号,占用 4 字节。
消息头 nlmsghdr 结构体定义:
cpp
struct nlmsghdr {
__u32 nlmsg_len; /* 包括 netlink 消息头在内,整个消息的总长度 */
__u16 nlmsg_type; /* 消息类型 */
__u16 nlmsg_flags; /* 消息标志位 */
__u32 nlmsg_seq; /* 消息报文的序列号 */
__u32 nlmsg_pid; /* 发送端口的 ID 号:内核侧该值为 0,用户进程侧为其 socket 绑定的 ID 号 */
};
2.1.2 netlink 通信地址 struct sockaddr_nl
struct sockaddr_nl 是 netlink 机制的通信地址结构体,功能类比普通 socket 编程中的 struct sockaddr_in 结构体:
cpp
struct sockaddr_nl {
__kernel_sa_family_t nl_family; /* 地址族,固定为 AF_NETLINK */
unsigned short nl_pad; /* 预留填充字段,暂未使用,需置为 0 */
__u32 nl_pid; /* 端口 ID(通信端口号),0 表示目标为内核 */
__u32 nl_groups; /* 多播组掩码 */
};
nl_groups 为多播组的地址掩码(非多播组编号),可参考内核源码文件 net/netlink/af_netlink.c 中的如下函数:
cpp
static u32 netlink_group_mask(u32 group)
{
return group ? 1 << (group - 1) : 0;
}
即用户空间代码中,若需加入多播组 1,需将 nl_groups 设为 1;多播组 2 对应的掩码为 2;多播组 3 对应的掩码为 4,依此类推。nl_groups 为 0 时,表示不加入任何多播组。
nl_groups 多播组掩码为 32 位整型,每 1 个比特位对应 1 个多播组,因此每种 Netlink 协议最多支持 32 个多播组。用户空间进程若关注某一多播组,可加入该组;当内核空间进程向该组发送多播消息时,所有已加入该组的用户进程均可接收此消息。
2.2 netlink 内核层数据结构及函数
2.2.1 常用宏定义
netlink 消息类型及常用操作宏定义如下:
cpp
/******************** netlink 消息类型 ********************/
#define NETLINK_ROUTE 0 /* 路由/设备钩子函数接口 */
#define NETLINK_UNUSED 1 /* 未使用编号 */
#define NETLINK_USERSOCK 2 /* 为用户态 socket 协议预留 */
#define NETLINK_FIREWALL 3 /* 未使用编号,原用于 ip_queue 模块 */
#define NETLINK_SOCK_DIAG 4 /* socket 监控接口 */
#define NETLINK_NFLOG 5 /* netfilter/iptables ULOG 模块 */
#define NETLINK_XFRM 6 /* IPsec 协议相关 */
#define NETLINK_SELINUX 7 /* SELinux 事件通知 */
#define NETLINK_ISCSI 8 /* Open-iSCSI 协议相关 */
#define NETLINK_AUDIT 9 /* 审计功能相关 */
#define NETLINK_FIB_LOOKUP 10 /* FIB 查表功能相关 */
#define NETLINK_CONNECTOR 11 /* 连接器子系统相关 */
#define NETLINK_NETFILTER 12 /* netfilter 子系统 */
#define NETLINK_IP6_FW 13 /* IPv6 防火墙相关 */
#define NETLINK_DNRTMSG 14 /* DECnet 路由消息 */
#define NETLINK_KOBJECT_UEVENT 15 /* 内核向用户空间发送的对象事件消息 */
#define NETLINK_GENERIC 16 /* 通用型 netlink 协议 */
/* 为 NETLINK_DM(DM 事件)预留空间 */
#define NETLINK_SCSITRANSPORT 18 /* SCSI 传输层相关 */
#define NETLINK_ECRYPTFS 19 /* ECRYPTFS 加密文件系统相关 */
#define NETLINK_RDMA 20 /* RDMA 协议相关 */
#define NETLINK_CRYPTO 21 /* 加密层相关 */
#define NETLINK_INET_DIAG NETLINK_SOCK_DIAG /* 网络套接字诊断 */
#define MAX_LINKS 32 /* 最大支持的 netlink 链路数 */
/******************** netlink 常用宏 ********************/
#define NLMSG_ALIGNTO 4U /* 字节对齐基准值 */
/* 计算不小于 len 且满足字节对齐的最小数值 */
#define NLMSG_ALIGN(len) ( ((len)+NLMSG_ALIGNTO-1) & ~(NLMSG_ALIGNTO-1) )
/* Netlink 消息头长度 */
#define NLMSG_HDRLEN ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))
/* 计算包含消息头的消息总长度(消息体长度 + 消息头长度)*/
#define NLMSG_LENGTH(len) ((len) + NLMSG_HDRLEN)
/* 返回不小于 NLMSG_LENGTH(len) 且满足字节对齐的最小数值 */
#define NLMSG_SPACE(len) NLMSG_ALIGN(NLMSG_LENGTH(len))
/* 获取消息体数据部分的首地址,读写消息数据时调用 */
#define NLMSG_DATA(nlh) ((void*)(((char*)nlh) + NLMSG_LENGTH(0)))
/* 获取下一个消息的首地址,同时更新 len 为剩余消息长度 */
#define NLMSG_NEXT(nlh,len) ((len) -= NLMSG_ALIGN((nlh)->nlmsg_len), \
(struct nlmsghdr*)(((char*)(nlh)) + NLMSG_ALIGN((nlh)->nlmsg_len)))
/* 判断消息长度是否合法 */
#define NLMSG_OK(nlh,len) ((len) >= (int)sizeof(struct nlmsghdr) && \
(nlh)->nlmsg_len >= sizeof(struct nlmsghdr) && \
(nlh)->nlmsg_len <= (len))
/* 计算消息载荷(payload)的长度 */
#define NLMSG_PAYLOAD(nlh,len) ((nlh)->nlmsg_len - NLMSG_SPACE((len)))
2.2.2 常用函数
(1)netlink_kernel_create 函数
函数原型:
cpp
static inline struct sock *
netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
功能 :创建内核态 socket,用于实现与用户态的通信。
参数说明:
- net:指向网络命名空间(namespace)的指针,默认场景下传入全局变量 &init_net(无需额外定义);
- unit:netlink 协议类型,如 NETLINK_TEST、NETLINK_SELINUX 等;
- cfg:存放 netlink 内核配置参数的结构体(定义如下)。
cpp
struct netlink_kernel_cfg {
unsigned int groups; // 该协议类型支持的最大多播组数量,若小于 32 则默认按 32 处理,通常置为 0
unsigned int flags; // 配置标志位
void (*input)(struct sk_buff *skb); // 消息接收回调函数
struct mutex *cb_mutex; // 回调函数互斥锁
int (*bind)(struct net *net, int group); // 绑定回调函数
void (*unbind)(struct net *net, int group); // 解绑回调函数
bool (*compare)(struct net *net, struct sock *sk); // 比较函数
};
(2)单播函数 netlink_unicast()
函数原型:
cpp
int netlink_unicast(struct sock *ssk, struct sk_buff *skb, __u32 portid, int nonblock)
功能 :向指定端口发送单播消息。
参数说明:
- ssk:netlink socket 指针(netlink_kernel_create 函数的返回值);
- skb:内核套接字缓冲区(sk_buff)指针;
- portid:目标通信端口号(接收消息的进程 PID);
- nonblock:非阻塞标志位,1 表示无可用接收缓存时立即返回,0 表示无可用接收缓存时睡眠等待。
(3)多播函数 netlink_broadcast()
函数原型:
cpp
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, __u32 portid,
__u32 group, gfp_t allocation)
功能 :向指定多播组发送多播消息。
参数说明:
- ssk:netlink socket 指针(netlink_kernel_create 函数的返回值);
- skb:内核套接字缓冲区(sk_buff)指针;
- portid:发送端端口 ID;
- group:目标多播组掩码的按位或结果;
- allocation:内核内存分配方式标识,中断上下文通常使用 GFP_ATOMIC,其他场景使用 GFP_KERNEL;该参数存在的原因是该 API 可能需要分配缓冲区以克隆多播消息。
3 代码实例
以下代码示例分别实现用户态与内核态的 netlink 通信逻辑。
3.1 用户态 netlink 程序
3.1.1 netlink 应用层编程基本步骤
(1)创建套接字
函数原型:
cpp
int socket(int domain, int type, int protocol)
参数说明:
- domain:协议族,netlink 机制中固定为 AF_NETLINK;
- type:套接字类型,netlink 机制中固定为 SOCK_RAW;
- protocol:协议类型,可使用内核预定义协议或自定义协议。
自定义协议示例:
cpp
#define NETLINK_TEST 23 // 自定义 netlink 协议类型
......
int skfd = 0;
skfd = socket(AF_NETLINK, SOCK_RAW, NETLINK_TEST);
(2)绑定本地地址到套接字
函数原型:
cpp
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen) // 地址绑定
/* sockaddr_nl 是 netlink 专用地址结构体,区别于网络编程的通用地址结构 */
struct sockaddr_nl {
__kernel_sa_family_t nl_family; // 地址族,固定为 AF_NETLINK
unsigned short nl_pad; // 填充字段,无需赋值
__u32 nl_pid; // 与内核通信的进程 PID,0 表示目标为内核
__u32 nl_groups; // 多播组地址掩码,netlink 支持多播通信
};
绑定示例:
cpp
struct sockaddr_nl nlsrc_addr = {0};
/* 初始化本地 socket 地址 */
nlsrc_addr.nl_family = AF_NETLINK;
nlsrc_addr.nl_pid = getpid();
nlsrc_addr.nl_groups = 0;
/* 执行地址绑定 */
if(bind(skfd, (struct sockaddr*)&nlsrc_addr, addr_len) != 0)
{
printf("bind addr error\n");
return -1;
}
(3)构造通信消息
(4)发送/接收消息
netlink 机制提供两套收发消息的接口:sendto/recvfrom、sendmsg/recvmsg。核心接口定义如下:
cpp
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
3.1.2 netlink 用户空间完整代码
cpp
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <string.h>
#include <linux/netlink.h>
#define NETLINK_TEST 17
#define RX_BUF_SIZE 100 // 接收缓冲区大小
#define MAX_PLOAD 100 // 发送消息内存空间大小
// 定义接收内核消息的结构体
typedef struct
{
struct nlmsghdr hdr;
char msg[RX_BUF_SIZE];
}RX_KERNEL_MSG;
int main(int argc, char* argv[])
{
char *data = "This message is from user's space";
// 初始化变量
struct sockaddr_nl src_addr, dest_addr; // netlink 地址结构体
int skfd, ret, rxlen = sizeof(struct sockaddr_nl);
struct nlmsghdr *message;
RX_KERNEL_MSG info;
char *retval;
/* 1. 创建 NETLINK 套接字 */
skfd = socket(PF_NETLINK, SOCK_RAW, NETLINK_TEST);
if(skfd < 0){
printf("can not create a netlink socket\n");
return -1;
}
// 初始化 netlink 源地址
memset(&src_addr, 0, sizeof(src_addr));
src_addr.nl_family = AF_NETLINK;
src_addr.nl_pid = getpid(); // 源端口为当前进程 PID
src_addr.nl_groups = 0; // 不加入任何多播组
// 绑定套接字到源地址
if(bind(skfd, (struct sockaddr *)&src_addr, sizeof(src_addr)) != 0){
printf("bind() error\n");
return -1;
}
// 初始化 netlink 目标地址
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.nl_family = AF_NETLINK;
dest_addr.nl_pid = 0; // 目标端口为内核(PID = 0)
dest_addr.nl_groups = 0; // 多播组掩码置 0
/* 2. 构造通信消息 */
message = (struct nlmsghdr *)malloc(NLMSG_SPACE(MAX_PLOAD));
memset(message, '\0', sizeof(struct nlmsghdr));
message->nlmsg_len = NLMSG_SPACE(strlen(data)); // 消息总长度(含消息头)
message->nlmsg_flags = 0; // 消息标志位置 0
message->nlmsg_type = 0; // 消息类型置 0
message->nlmsg_seq = 0; // 消息序列号置 0
message->nlmsg_pid = src_addr.nl_pid; // 消息中携带源端口 PID
// 拷贝有效数据到消息体
retval = memcpy(NLMSG_DATA(message), data, strlen(data));
/* 3. 发送消息到内核 */
ret = sendto(skfd, message, message->nlmsg_len, 0,(struct sockaddr *)&dest_addr, sizeof(dest_addr));
if(!ret){
perror("send pid:");
exit(-1);
}
/* 4. 接收内核返回的响应消息 */
ret = recvfrom(skfd, &info, sizeof(RX_KERNEL_MSG), 0, (struct sockaddr*)&dest_addr, &rxlen);
if(!ret){
perror("recv form kerner:");
exit(-1);
}
printf("User Receive ACK from kernel:%s\r\n",(char *)info.msg);
// 释放资源并关闭套接字
close(skfd);
free((void *)message);
return 0;
}
3.2 内核空间 netlink 代码
3.2.1 netlink 内核态编程基本步骤
内核侧 netlink 编程接口与应用层逻辑相似,核心步骤如下:
(1)创建 socket 并注册回调函数
cpp
// netlink 内核配置结构体
struct netlink_kernel_cfg {
unsigned int groups;
unsigned int flags;
void (*input)(struct sk_buff *skb); /* 消息接收回调函数 */
struct mutex *cb_mutex;
int (*bind)(struct net *net, int group);
void (*unbind)(struct net *net, int group);
bool (*compare)(struct net *net, struct sock *sk);
};
/* 创建内核 netlink socket,参数 net 通常传入 &init_net */
static inline struct sock *netlink_kernel_create(struct net *net, int unit, struct netlink_kernel_cfg *cfg)
/* 释放内核 netlink socket */
void netlink_kernel_release(struct sock *sk);
(2)构造通信消息
cpp
// sk_buff 是内核中 netlink 消息的载体,以下为其分配、引用、释放接口
static inline struct sk_buff *alloc_skb(unsigned int size, gfp_t priority)
static inline struct sk_buff *skb_get(struct sk_buff *skb)
void kfree_skb(struct sk_buff *skb);
// 构造 netlink 消息头的接口
static inline struct nlmsghdr *nlmsg_put(struct sk_buff *skb, u32 portid, u32 seq, int type, int payload, int flags)
典型调用示例:
cpp
// NLMSG_SPACE 计算包含消息头的消息总长度
size_t size = max(NLMSG_SPACE(message_size), (size_t)NLMSG_GOODSIZE);
// 分配 sk_buff 内存空间
struct sk_buff * log_skb = alloc_skb(size, GFP_ATOMIC);
// 构造消息头
struct nlmsghdr *nlh = nlmsg_put(log_skb, /*pid*/0, /*seq*/0, type,
message_size, 0);
// 拷贝有效数据到消息体
if(payload != NULL) {
memcpy(nlmsg_data(nlh), payload, size);
}
(3)发送/接收消息
内核提供单播、多播两类消息发送接口,可根据业务场景选择:
cpp
int netlink_unicast(struct sock *ssk, struct sk_buff *skb, __u32 portid, int nonblock);
int netlink_broadcast(struct sock *ssk, struct sk_buff *skb, __u32 portid, __u32 group, gfp_t allocation);
3.2.2 netlink 内核态模块完整代码
cpp
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/sched.h>
#include <net/sock.h>
#include <linux/netlink.h>
#define NETLINK_TEST 17 // 与用户态一致的自定义协议类型
// 存储用户进程 PID 的结构体
struct {
__u32 pid;
}user_process;
static struct sock *netlinkfd = NULL; // netlink socket 指针
/* 向用户空间发送消息的函数 */
int send_to_user(int _pid, char *pbuf, uint16_t len)
{
struct sk_buff *nl_skb;
struct nlmsghdr *nlh;
int ret;
/* 分配 sk_buff 内存空间 */
nl_skb = nlmsg_new(len, GFP_ATOMIC);
if(!nl_skb)
{
printk("netlink alloc failure\n");
return -1;
}
/* 构造 netlink 消息头 */
nlh = nlmsg_put(nl_skb, 0, 0, NETLINK_TEST, len, 0);
if(nlh == NULL)
{
printk("nlmsg_put failure \n");
nlmsg_free(nl_skb);
return -1;
}
/* 拷贝数据到消息体 */
memcpy(nlmsg_data(nlh), pbuf, len);
// 发送单播消息到用户进程
ret = netlink_unicast(netlinkfd, nl_skb, _pid, MSG_DONTWAIT);
return ret;
}
/* 消息接收回调函数 */
static void netlink_rcv_msg(struct sk_buff *skb)
{
struct nlmsghdr *nlh = NULL;
char *data = NULL;
char *kmsg = "hello users!!!";
// 获取消息头
nlh = nlmsg_hdr(skb);
// 校验消息长度合法性
if(skb->len >= NLMSG_SPACE(0))
{
data = NLMSG_DATA(nlh); // 提取消息体数据
if (data)
{
// 记录发送端用户进程 PID
user_process.pid = nlh->nlmsg_pid;
printk("kernel recv from user pid %d: %s\n", user_process.pid, data);
// 向用户进程发送响应消息
send_to_user(user_process.pid, kmsg, strlen(kmsg));
}
} else {
printk("%s: error skb, length:%d\n", __func__, skb->len);
}
}
// 定义 netlink 内核配置
struct netlink_kernel_cfg cfg = {
.input = netlink_rcv_msg, /* 注册消息接收回调函数 */
.groups = 0, // 多播组数量置 0
.flags = 0,
.cb_mutex = NULL,
.bind = NULL,
.compare = NULL,
};
// 模块初始化函数
int __init test_netlink_init(void)
{
/* 创建 netlink socket */
netlinkfd = (struct sock *)netlink_kernel_create(&init_net, NETLINK_TEST, &cfg);
if(netlinkfd == NULL){
printk(KERN_ERR "can not create a netlink socket\n");
return -1;
}
return 0;
}
// 模块退出函数
void __exit test_netlink_exit(void)
{
if (netlinkfd){
netlink_kernel_release(netlinkfd); /* 释放 netlink socket */
netlinkfd = NULL;
}
printk(KERN_DEBUG "test_netlink_exit!!\n");
}
module_init(test_netlink_init);
module_exit(test_netlink_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("donga");
3.3 测试结果
测试代码下载链接:https://download.csdn.net/download/hinewcc/89590914
将内核态代码编译为 ko 模块,用户态代码编译为可执行程序;通过 insmod 命令加载 ko 模块后运行用户态程序:
bash
$ insmod netlink_test.ko # 加载内核模块
bash
$ ./netlink_app # 运行用户态应用程序

内核态可接收用户态发送的数据,并向用户空间返回 "hello users!!!" 响应消息。
3.4 netlink 状态查看命令
通过如下命令可查看系统中 netlink 接口的 ID 及状态:
bash
$ cat /proc/net/netlink

总结
- netlink 是 Linux 内核与用户空间的双向通信机制,相比 proc、ioctl 具备全双工、异步、多播等特性,是网络类内核交互的主流方式;
- netlink 消息由 16 字节固定长度的消息头(nlmsghdr)和可变长度的消息体组成,寻址方式为「协议类型 + 进程 PID」,区别于传统网络编程的「IP 地址 + 端口号」;
- 内核态需通过 netlink_kernel_create 创建 socket 并注册回调函数,用户态可直接使用标准 socket API,核心交互接口包括 sendto/recvfrom(单播)、netlink_broadcast(多播)等。
via:
- Linux Kernel Module Communication: Beyond /dev, /proc, and ioctl() -- What Other Options Exist?
https://linuxvox.com/blog/what-options-do-we-have-for-communication-between-a-user-program-and-a-linux-kernel-module/ - 用户空间和内核空间通讯之【Netlink 上】-wjlkoorey258-ChinaUnix博客
http://blog.chinaunix.net/uid-23069658-id-3400761.html - 用户空间和内核空间通讯之【Netlink 中】-wjlkoorey258-ChinaUnix博客
http://blog.chinaunix.net/uid-23069658-id-3405954.html - 用户空间和内核空间通讯之【Netlink 下】-wjlkoorey258-ChinaUnix博客
http://blog.chinaunix.net/uid-23069658-id-3409786.html - linux 内核与用户空间通信之netlink使用方法-CSDN博客
https://blog.csdn.net/HAOMCU/article/details/7371835 - Linux内核netlink机制 - 用户空间和内核空间数据传输-CSDN博客
https://blog.csdn.net/hinewcc/article/details/139156381 - linux netlink通信机制 - zhangwju - 博客园 2017
https://www.cnblogs.com/wenqiang/p/6306727.html - Netlink 手册 --- Linux 内核文档 - Linux 内核
https://linuxkernel.org.cn/doc/html/latest/userspace-api/netlink/index.html