在 Linux 内核驱动中使用 Socket 发送数据与用户态编程有本质区别。内核中没有libc库的封装,需要直接调用内核提供的网络编程接口。本文提供了可在驱动中直接使用的 UDP 和 TCP Socket 完整示例代码,包含详细的注释、编译方法和测试步骤。
核心要点与前置条件:
- 内核态 Socket 仅支持
AF_INET (IPv4) 和 AF_INET6 (IPv6) 协议族,不支持 AF_UNIX (本地套接字)。
- 驱动中的网络操作必须避免阻塞,因为内核没有用户态的进程调度上下文。建议使用非阻塞模式或在内核线程中执行。
- 需要包含正确的内核网络头文件(如
linux/socket.h, linux/in.h 等),并且驱动编译需依赖对应版本的内核源码。
- 权限:驱动运行在内核态,本身无需额外权限,但需确保系统网络栈已正常初始化。
一、UDP版本Demo详解(无连接,简单高效)
UDP协议无需建立连接,是内核态驱动中进行简单数据发送的常用选择。
1. 驱动源码 (socket_drv_udp.c)
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/socket.h>
#include <linux/in.h>
#include <linux/net.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <net/sock.h>
// 目标服务器配置(请根据实际情况修改)
#define SERVER_IP "192.168.1.100" // 接收端IP地址
#define SERVER_PORT 8888 // 接收端端口号
#define BUF_SIZE 128 // 发送缓冲区大小
static struct socket *udp_sock = NULL; // 内核Socket结构体指针
static struct task_struct *send_thread; // 用于发送数据的内核线程
// 工具函数:将点分十进制IP字符串转换为网络字节序的32位整数
static __be32 ip_str_to_u32(const char *ip_str)
{
unsigned char a, b, c, d;
sscanf(ip_str, "%hhu.%hhu.%hhu.%hhu", &a, &b, &c, &d);
return (a << 24) | (b << 16) | (c << 8) | d;
}
// 内核线程函数:循环发送UDP数据
static int udp_send_thread(void *data)
{
struct msghdr msg;
struct kvec vec;
struct sockaddr_in dest_addr;
char send_buf[BUF_SIZE];
int ret, len;
// 设置为可中断的睡眠状态,确保线程可以被正确停止
set_freezable();
// 初始化目标服务器地址结构
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(SERVER_PORT);
dest_addr.sin_addr.s_addr = ip_str_to_u32(SERVER_IP);
printk(KERN_INFO "UDP发送线程启动,目标地址: %s:%d\n", SERVER_IP, SERVER_PORT);
// 主循环:每2秒发送一次数据
while (!kthread_should_stop()) {
// 构造要发送的数据内容
snprintf(send_buf, BUF_SIZE, "来自内核驱动的UDP消息: %lld", jiffies);
len = strlen(send_buf);
// 初始化kvec(内核态的I/O向量)
vec.iov_base = send_buf;
vec.iov_len = len;
// 初始化msghdr(消息头)
memset(&msg, 0, sizeof(msg));
msg.msg_name = &dest_addr;
msg.msg_namelen = sizeof(dest_addr);
msg.msg_iter = kvec_to_iter(&vec, 1, len);
// 调用内核接口发送数据
ret = sock_sendmsg(udp_sock, &msg, len);
if (ret < 0) {
printk(KERN_ERR "UDP发送失败,错误码: %d\n", ret);
} else {
printk(KERN_INFO "UDP发送成功: %s (长度: %d)\n", send_buf, ret);
}
// 睡眠2秒(使用内核态的延时函数)
schedule_timeout_interruptible(msecs_to_jiffies(2000));
}
printk(KERN_INFO "UDP发送线程退出\n");
return 0;
}
// 驱动初始化函数:创建Socket并启动发送线程
static int __init socket_drv_init(void)
{
int ret;
// 1. 创建内核态的UDP Socket
ret = sock_create(AF_INET, SOCK_DGRAM, IPPROTO_UDP, &udp_sock);
if (ret < 0) {
printk(KERN_ERR "创建UDP Socket失败,错误码: %d\n", ret);
return ret;
}
// 2. 设置Socket为非阻塞模式(避免在驱动中发生阻塞)
udp_sock->sk->sk_flags |= SOCK_NONBLOCK;
// 3. 创建内核线程来执行数据发送(禁止在init函数中直接进行可能阻塞的操作)
send_thread = kthread_run(udp_send_thread, NULL, "udp_send_thread");
if (IS_ERR(send_thread)) {
ret = PTR_ERR(send_thread);
printk(KERN_ERR "创建发送线程失败,错误码: %d\n", ret);
sock_release(udp_sock);
return ret;
}
printk(KERN_INFO "Socket驱动(UDP)初始化成功\n");
return 0;
}
// 驱动退出函数:停止线程并释放所有资源
static void __exit socket_drv_exit(void)
{
// 停止内核线程
if (send_thread && !IS_ERR(send_thread)) {
kthread_stop(send_thread);
}
// 释放Socket资源
if (udp_sock) {
sock_release(udp_sock);
udp_sock = NULL;
}
printk(KERN_INFO "Socket驱动(UDP)退出成功\n");
}
module_init(socket_drv_init);
module_exit(socket_drv_exit);
MODULE_LICENSE("GPL"); // 必须声明为GPL协议,否则可能无法使用内核网络符号
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Linux内核态UDP Socket通信示例驱动");
2. 编译脚本 (Makefile)
obj-m += socket_drv_udp.o
KERNELDIR ?= /lib/modules/$(shell uname -r)/build # 指向当前内核的源码路径
PWD := $(shell pwd)
all:
make -C $(KERNELDIR) M=$(PWD) modules
clean:
make -C $(KERNELDIR) M=$(PWD) clean
3. 测试与验证步骤
① 准备接收端:在目标服务器上,使用 netcat 工具监听指定的UDP端口。
nc -lu 8888
② 编译与加载驱动:
make
sudo insmod socket_drv_udp.ko
③ 查看驱动日志:使用 dmesg 命令实时观察内核打印信息,确认数据正在周期性发送。
sudo dmesg -w
④ 卸载驱动:
sudo rmmod socket_drv_udp
二、TCP版本Demo详解(面向连接,可靠传输)
TCP协议需要先建立连接,内核态的实现比UDP稍复杂,需处理连接过程。以下是核心代码,着重展示与UDP的差异部分。
1. TCP核心源码 (socket_drv_tcp.c)
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/socket.h>
#include <linux/in.h>
#include <linux/net.h>
#include <linux/sched.h>
#include <linux/delay.h>
#include <net/sock.h>
#define SERVER_IP "192.168.1.100"
#define SERVER_PORT 9999
#define BUF_SIZE 128
static struct socket *tcp_sock = NULL;
static struct task_struct *send_thread;
// IP转换函数(同上,略)
static __be32 ip_str_to_u32(const char *ip_str){...}
// 建立TCP连接
static int tcp_connect(void)
{
struct sockaddr_in dest_addr;
int ret;
memset(&dest_addr, 0, sizeof(dest_addr));
dest_addr.sin_family = AF_INET;
dest_addr.sin_port = htons(SERVER_PORT);
dest_addr.sin_addr.s_addr = ip_str_to_u32(SERVER_IP);
// 内核态发起TCP连接(非阻塞模式)
ret = kernel_connect(tcp_sock, (struct sockaddr *)&dest_addr, sizeof(dest_addr), 0);
if (ret == -EINPROGRESS) {
// 非阻塞连接返回EINPROGRESS,需要等待连接完成
wait_for_completion(&tcp_sock->sk->sk_completion);
ret = tcp_sock->sk->sk_err;
if (ret) {
printk(KERN_ERR "TCP连接失败,错误: %d\n", ret);
return ret;
}
ret = 0;
}
if (ret == 0) {
printk(KERN_INFO "TCP连接成功\n");
} else {
printk(KERN_ERR "TCP连接失败,返回码: %d\n", ret);
}
return ret;
}
// TCP发送线程
static int tcp_send_thread(void *data)
{
struct msghdr msg;
struct kvec vec;
char send_buf[BUF_SIZE];
int ret, len;
set_freezable();
// 首先建立TCP连接
if (tcp_connect() != 0) {
return -1;
}
while (!kthread_should_stop()) {
snprintf(send_buf, BUF_SIZE, "来自内核驱动的TCP消息: %lld", jiffies);
len = strlen(send_buf);
vec.iov_base = send_buf;
vec.iov_len = len;
memset(&msg, 0, sizeof(msg));
msg.msg_iter = kvec_to_iter(&vec, 1, len);
// 发送TCP数据
ret = sock_sendmsg(tcp_sock, &msg, len);
if (ret < 0) {
printk(KERN_ERR "TCP发送失败,错误码: %d\n", ret);
// 如果发送失败,可能是连接断开,尝试重连
tcp_connect();
} else {
printk(KERN_INFO "TCP发送成功: %s (长度: %d)\n", send_buf, ret);
}
schedule_timeout_interruptible(msecs_to_jiffies(2000));
}
return 0;
}
// 驱动初始化(TCP)
static int __init socket_drv_init(void)
{
int ret;
// 创建TCP Socket
ret = sock_create(AF_INET, SOCK_STREAM, IPPROTO_TCP, &tcp_sock);
if (ret < 0) {
printk(KERN_ERR "创建TCP Socket失败,错误码: %d\n", ret);
return ret;
}
// 设置非阻塞
tcp_sock->sk->sk_flags |= SOCK_NONBLOCK;
send_thread = kthread_run(tcp_send_thread, NULL, "tcp_send_thread");
if (IS_ERR(send_thread)) {
ret = PTR_ERR(send_thread);
printk(KERN_ERR "创建TCP发送线程失败,错误码: %d\n", ret);
sock_release(tcp_sock);
return ret;
}
printk(KERN_INFO "TCP Socket驱动初始化成功\n");
return 0;
}
// 驱动退出(TCP)
static void __exit socket_drv_exit(void)
{
if (send_thread && !IS_ERR(send_thread)) {
kthread_stop(send_thread);
}
if (tcp_sock) {
// 优雅关闭TCP连接
kernel_sock_shutdown(tcp_sock, SHUT_RDWR);
sock_release(tcp_sock);
tcp_sock = NULL;
}
printk(KERN_INFO "TCP Socket驱动退出成功\n");
}
module_init(socket_drv_init);
module_exit(socket_drv_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Linux内核态TCP Socket通信示例驱动");
2. TCP测试步骤
① 启动TCP服务端:
nc -l 9999
② 编译与加载驱动(修改Makefile中的obj-m为socket_drv_tcp.o):
make
sudo insmod socket_drv_tcp.ko
三、开发与部署关键注意事项
- 非阻塞模式是必须的:内核态严禁长时间阻塞,所有Socket操作都应设置为非阻塞(
SOCK_NONBLOCK)。
- 使用内核线程:网络发送等可能引起等待的操作,必须在独立的内核线程中执行,绝不能在
module_init()或中断上下文中直接调用。
- 协议声明:驱动模块必须使用
MODULE_LICENSE(“GPL”)声明为GPL协议,否则内核可能会拒绝导出网络相关的符号,导致加载失败。
- 连接容错:TCP连接可能中断,代码中应实现适当的错误处理和重连逻辑。
- 资源管理:在驱动的退出函数中,必须确保释放所有Socket资源并正确停止内核线程,防止内核内存泄漏。
- 内核版本兼容性:部分内核API(如
kvec_to_iter)在新版本中引入,如果你的Linux驱动需要兼容老内核(如4.19之前),可能需要使用iov_iter_init等替代函数,并做好条件编译。
四、常见问题与排查方法
- 加载驱动时报错 “Unknown symbol”:
- 检查内核配置是否启用了
CONFIG_NET(网络子系统支持)。
- 确认模块已正确声明
MODULE_LICENSE(“GPL”)。
- 核对Makefile中的
KERNELDIR路径是否指向了正确的内核源码。
- 发送数据返回 -EAGAIN 错误:
- 在非阻塞模式下,发送缓冲区满时会返回此错误。解决方案是加入延时后重试。
- 检查目标服务器的IP和端口是否正确,且服务端程序已正常监听。
- TCP连接始终失败:
- 确认服务器IP、端口无误,且防火墙已放行对应端口。
- 检查内核参数
net.ipv4.tcp_syncookies的设置,有时会影响连接建立。
本文提供的两个Demo是内核态Socket编程最基础的实现框架,开发者可以在此基础上进行扩展,例如增加更复杂的数据封装协议、实现错误重传机制、支持向多个目标发送数据等,以满足具体的业务需求。