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

983

积分

0

好友

139

主题
发表于 14 小时前 | 查看: 2| 回复: 0

在 Linux 内核驱动中使用 Socket 发送数据与用户态编程有本质区别。内核中没有libc库的封装,需要直接调用内核提供的网络编程接口。本文提供了可在驱动中直接使用的 UDP 和 TCP Socket 完整示例代码,包含详细的注释、编译方法和测试步骤。

核心要点与前置条件:

  1. 内核态 Socket 仅支持 AF_INET (IPv4) 和 AF_INET6 (IPv6) 协议族,不支持 AF_UNIX (本地套接字)。
  2. 驱动中的网络操作必须避免阻塞,因为内核没有用户态的进程调度上下文。建议使用非阻塞模式或在内核线程中执行。
  3. 需要包含正确的内核网络头文件(如 linux/socket.h, linux/in.h 等),并且驱动编译需依赖对应版本的内核源码。
  4. 权限:驱动运行在内核态,本身无需额外权限,但需确保系统网络栈已正常初始化。

一、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-msocket_drv_tcp.o):

make
sudo insmod socket_drv_tcp.ko

三、开发与部署关键注意事项

  1. 非阻塞模式是必须的:内核态严禁长时间阻塞,所有Socket操作都应设置为非阻塞(SOCK_NONBLOCK)。
  2. 使用内核线程:网络发送等可能引起等待的操作,必须在独立的内核线程中执行,绝不能在module_init()或中断上下文中直接调用。
  3. 协议声明:驱动模块必须使用MODULE_LICENSE(“GPL”)声明为GPL协议,否则内核可能会拒绝导出网络相关的符号,导致加载失败。
  4. 连接容错:TCP连接可能中断,代码中应实现适当的错误处理和重连逻辑。
  5. 资源管理:在驱动的退出函数中,必须确保释放所有Socket资源并正确停止内核线程,防止内核内存泄漏。
  6. 内核版本兼容性:部分内核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编程最基础的实现框架,开发者可以在此基础上进行扩展,例如增加更复杂的数据封装协议、实现错误重传机制、支持向多个目标发送数据等,以满足具体的业务需求。




上一篇:MCP PL-600功能测试:从环境搭建到场景验证的完整实践指南
下一篇:Kubernetes Service与kube-proxy工作原理详解:ClusterIP/NodePort流量路径剖析
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2025-12-17 16:47 , Processed in 0.152586 second(s), 39 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

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