在 Linux内核驱动 中,有时需要直接通过网络发送数据,这与用户态Socket编程有很大不同。因为内核中没有libc库的封装,必须直接调用内核提供的网络编程接口。
与用户态编程相比,内核态Socket编程有几个核心区别:
- 接口不同:需使用
kernal_connect()、sock_sendmsg()等内核函数。
- 无阻塞调度:内核环境下没有完整的进程调度上下文,长时间阻塞可能导致系统问题,因此必须使用非阻塞模式或在内核线程中操作。
- 依赖与权限:需要包含特定的内核头文件(如
linux/socket.h),编译时需依赖内核源码;驱动运行在内核态,拥有最高权限,但需确保网络协议栈已正常初始化。
本文将提供两个可直接编译、测试的驱动Demo,分别演示UDP和TCP协议下的数据发送。
一、UDP版本驱动Demo(无连接通信)
UDP协议无需建立和维护连接,实现简单,是内核态网络通信的常用选择。
1. 驱动源码 (socket_drv_udp.c)
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/socket.h> // 内核Socket头文件
#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转换为网络序整数
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(内核态IO向量)
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;
}
// 驱动初始化函数
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. 创建内核线程专用于发送数据
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("Demo");
MODULE_DESCRIPTION("Linux内核Socket UDP通信示例驱动");
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. 测试步骤
- 启动用户态UDP服务端(用于接收驱动发来的数据):
# 使用netcat监听8888端口
nc -lu 8888
- 编译并加载驱动模块:
make
sudo insmod socket_drv_udp.ko
- 查看驱动日志,验证发送是否成功:
sudo dmesg -w # 实时查看内核日志,应能看到每2秒一条的发送记录
- 卸载驱动模块:
sudo rmmod socket_drv_udp
二、TCP版本驱动Demo(面向连接通信)
TCP协议需要先建立可靠连接,因此在驱动中需要处理连接建立、维护以及断开重连等逻辑,复杂度相对较高。
1. 核心代码差异 (socket_drv_tcp.c)
以下主要展示与UDP版本不同的核心部分:
...
#define SERVER_PORT 9999 // TCP使用不同端口
static struct socket *tcp_sock = NULL;
// 建立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) {
// 非阻塞连接需要等待完成
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;
}
static int tcp_send_thread(void *data)
{
...
set_freezable();
// 先建立TCP连接
if (tcp_connect() != 0) {
return -1;
}
while (!kthread_should_stop()) {
snprintf(send_buf, BUF_SIZE, "内核TCP消息: %lld", jiffies);
...
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;
}
static int __init socket_drv_init(void)
{
// 创建TCP Socket (SOCK_STREAM)
ret = sock_create(AF_INET, SOCK_STREAM, IPPROTO_TCP, &tcp_sock);
...
}
static void __exit socket_drv_exit(void)
{
...
if (tcp_sock) {
// 关闭TCP连接
kernel_sock_shutdown(tcp_sock, SHUT_RDWR);
sock_release(tcp_sock);
tcp_sock = NULL;
}
...
}
2. TCP测试步骤
- 启动用户态TCP服务端:
nc -l 9999
- 编译并加载驱动(需修改
Makefile中的obj-m为socket_drv_tcp.o):
make
sudo insmod socket_drv_tcp.ko
三、关键注意事项与最佳实践
在Linux内核中进行网络编程,需要特别注意以下事项,这对于系统稳定性和驱动性能至关重要:
- 非阻塞模式:内核环境严禁长时间阻塞操作,所有Socket必须设置为非阻塞模式(
SOCK_NONBLOCK)。
- 使用内核线程:网络IO操作应放在独立的内核线程中执行,切忌在驱动初始化函数(
module_init)或中断上下文中直接进行可能阻塞的发送/接收。
- 协议与许可:驱动模块必须声明
MODULE_LICENSE("GPL"),否则内核可能会拒绝导出网络相关的符号,导致加载失败。
- 完善的错误处理:TCP连接可能因网络波动而断开,代码中需要实现重连机制。
- 资源管理:在驱动退出函数(
module_exit)中,必须按顺序停止内核线程、关闭连接、释放Socket,防止内核资源泄漏。
- 内核版本兼容性:部分API(如
kvec_to_iter)在新版本内核中引入,针对老内核开发时需寻找替代方案(如iov_iter_init)。
四、常见问题排查
- 加载驱动时报错
Unknown symbol:
- 检查内核配置是否启用了网络支持(
CONFIG_NET=y)。
- 确认模块已正确声明GPL许可证。
- 核对
Makefile中的KERNELDIR路径是否正确指向了当前运行内核的源码。
- 发送数据返回
-EAGAIN (或 -11):
- 非阻塞模式下,发送缓冲区已满,需要延时后重试。
- 检查目标服务器IP和端口是否正确,且服务端程序已正常监听。
- TCP连接始终失败:
- 确认服务器防火墙放行了目标端口。
- 检查内核网络参数,如
net.ipv4.tcp_syncookies的设置是否会影响连接建立。
以上Demo提供了Linux内核驱动中基于Socket进行网络通信的基础框架。在实际项目中,你可以在此基础上进行扩展,例如增加更复杂的数据封装协议、实现重传机制、支持多目标发送等。理解Linux网络编程的基础原理,特别是并发处理,对于开发稳定的内核网络驱动非常有帮助。