找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1561

积分

0

好友

231

主题
发表于 7 天前 | 查看: 18| 回复: 0

Linux内核驱动 中,有时需要直接通过网络发送数据,这与用户态Socket编程有很大不同。因为内核中没有libc库的封装,必须直接调用内核提供的网络编程接口。

与用户态编程相比,内核态Socket编程有几个核心区别:

  1. 接口不同:需使用kernal_connect()sock_sendmsg()等内核函数。
  2. 无阻塞调度:内核环境下没有完整的进程调度上下文,长时间阻塞可能导致系统问题,因此必须使用非阻塞模式或在内核线程中操作。
  3. 依赖与权限:需要包含特定的内核头文件(如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. 测试步骤
  1. 启动用户态UDP服务端(用于接收驱动发来的数据):
    # 使用netcat监听8888端口
    nc -lu 8888
  2. 编译并加载驱动模块
    make
    sudo insmod socket_drv_udp.ko
  3. 查看驱动日志,验证发送是否成功:
    sudo dmesg -w  # 实时查看内核日志,应能看到每2秒一条的发送记录
  4. 卸载驱动模块
    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测试步骤
  1. 启动用户态TCP服务端
    nc -l 9999
  2. 编译并加载驱动(需修改Makefile中的obj-msocket_drv_tcp.o):
    make
    sudo insmod socket_drv_tcp.ko

三、关键注意事项与最佳实践

Linux内核中进行网络编程,需要特别注意以下事项,这对于系统稳定性和驱动性能至关重要:

  1. 非阻塞模式:内核环境严禁长时间阻塞操作,所有Socket必须设置为非阻塞模式(SOCK_NONBLOCK)。
  2. 使用内核线程:网络IO操作应放在独立的内核线程中执行,切忌在驱动初始化函数(module_init)或中断上下文中直接进行可能阻塞的发送/接收。
  3. 协议与许可:驱动模块必须声明MODULE_LICENSE("GPL"),否则内核可能会拒绝导出网络相关的符号,导致加载失败。
  4. 完善的错误处理:TCP连接可能因网络波动而断开,代码中需要实现重连机制。
  5. 资源管理:在驱动退出函数(module_exit)中,必须按顺序停止内核线程、关闭连接、释放Socket,防止内核资源泄漏。
  6. 内核版本兼容性:部分API(如kvec_to_iter)在新版本内核中引入,针对老内核开发时需寻找替代方案(如iov_iter_init)。

四、常见问题排查

  1. 加载驱动时报错 Unknown symbol
    • 检查内核配置是否启用了网络支持(CONFIG_NET=y)。
    • 确认模块已正确声明GPL许可证。
    • 核对Makefile中的KERNELDIR路径是否正确指向了当前运行内核的源码。
  2. 发送数据返回 -EAGAIN (或 -11)
    • 非阻塞模式下,发送缓冲区已满,需要延时后重试。
    • 检查目标服务器IP和端口是否正确,且服务端程序已正常监听。
  3. TCP连接始终失败
    • 确认服务器防火墙放行了目标端口。
    • 检查内核网络参数,如net.ipv4.tcp_syncookies的设置是否会影响连接建立。

以上Demo提供了Linux内核驱动中基于Socket进行网络通信的基础框架。在实际项目中,你可以在此基础上进行扩展,例如增加更复杂的数据封装协议、实现重传机制、支持多目标发送等。理解Linux网络编程的基础原理,特别是并发处理,对于开发稳定的内核网络驱动非常有帮助。




上一篇:ESP32-S3电容触摸按键开发指南:从原理到实战应用
下一篇:快速将Python业务函数封装为HTTP服务:Flask与FastAPI实战指南
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-24 17:17 , Processed in 0.166907 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表