You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Linux字符驱动中优雅实现写阻塞功能?

字符驱动中阻塞式write的正确实现(解决FIFO满时进程无法中断的问题)

我正在编写一个实现“tagged MUX”功能的Linux字符驱动,用于向特殊硬件链路写入数据。需求是:write()请求将数据送入kfifo,由独立内核线程按需从FIFO取数;当FIFO已满时,用户空间的写操作需要阻塞,直到FIFO有可用空间。

我之前在write处理函数里写了一段代码,虽然能让进程在FIFO满时阻塞,但进程无法被中断或杀死,最后只能重启机器。代码如下:

ssize_t
fifoWrite(struct file *pFile, const char __user *pUserData, size_t nBytes, loff_t *pOffset)
{
    unsigned char         *pktData = 0;

    pktData = memdup_user(pUserData, nBytes); // copy n bytes from user to kernel
    if (IS_ERR(pktData))
        return PTR_ERR(pktData);

    TDataPacket pkt;
    memcpy(pkt.buf, pktData, nBytes);

    while (kfifo_put(&pktFifo, pkt) == 0)
    {
        printk(KERN_ERR MODULE_NAME": write - FIFO full\n");
        msleep(1000);
    }

    int fifoLen = kfifo_len(&clientInfo->pktFifo);

    printk(KERN_INFO MODULE_NAME": WRITE - %zu bytes. LEN %d\n", nBytes, fifoLen);

    kfree(pktData);

    return nBytes;
}

问题根源

你的代码使用msleep进行不可中断睡眠,且未处理信号。处于msleep状态的进程会进入TASK_UNINTERRUPTIBLE状态,无法响应任何信号(包括SIGKILL),最终导致进程僵死,只能重启系统。

正确实现方案

Linux驱动中实现可中断阻塞的标准方式是等待队列(wait queue),它能让进程进入可中断睡眠状态,既可以响应信号,又能在FIFO有空间时被唤醒。

步骤1:定义并初始化等待队列

在驱动的全局或私有上下文结构体中添加等待队列头,并在驱动初始化阶段完成初始化:

#include <linux/wait.h>

// 用于阻塞写操作的等待队列头
wait_queue_head_t write_wait;

static int __init mux_driver_init(void) {
    // ... 其他驱动初始化逻辑(如注册字符设备)
    init_waitqueue_head(&write_wait);
    // ...
}

步骤2:修改write函数为可中断阻塞逻辑

替换原有的死循环+msleep逻辑,使用wait_event_interruptible实现可中断等待:

ssize_t fifoWrite(struct file *pFile, const char __user *pUserData, size_t nBytes, loff_t *pOffset)
{
    unsigned char *pktData = NULL;
    TDataPacket pkt;
    int ret;

    // 先将用户数据拷贝到内核空间,避免阻塞期间占用用户内存
    pktData = memdup_user(pUserData, nBytes);
    if (IS_ERR(pktData))
        return PTR_ERR(pktData);
    memcpy(pkt.buf, pktData, nBytes);
    kfree(pktData); // 提前释放临时内存,避免资源浪费

    // 进入可中断等待:直到FIFO非满,或收到信号被唤醒
    ret = wait_event_interruptible(write_wait, !kfifo_is_full(&pktFifo));
    if (ret != 0) {
        // 被信号中断,返回内核标准错误码
        printk(KERN_WARNING MODULE_NAME": write interrupted by signal\n");
        return -ERESTARTSYS;
    }

    // FIFO有空间,写入数据
    if (kfifo_put(&pktFifo, pkt) == 0) {
        printk(KERN_ERR MODULE_NAME": failed to put data into FIFO\n");
        return -EIO;
    }

    // 打印当前FIFO状态
    int fifoLen = kfifo_len(&pktFifo);
    printk(KERN_INFO MODULE_NAME": WRITE - %zu bytes. LEN %d\n", nBytes, fifoLen);

    return nBytes;
}

步骤3:在取数据的内核线程中唤醒等待队列

当内核线程从FIFO中取出数据后,必须主动唤醒等待的写进程,否则进程会一直处于睡眠状态:

static int mux_kthread_func(void *data) {
    TDataPacket pkt;
    set_current_state(TASK_INTERRUPTIBLE);

    while (!kthread_should_stop()) {
        // 尝试从FIFO取数据
        if (kfifo_get(&pktFifo, &pkt) != 0) {
            // 向硬件发送数据的业务逻辑...
            
            // 数据取出后FIFO有空间,唤醒等待的写进程
            wake_up_interruptible(&write_wait);
        } else {
            // 无数据时进入可中断睡眠,避免空转占用CPU
            schedule();
            set_current_state(TASK_INTERRUPTIBLE);
        }
    }

    set_current_state(TASK_RUNNING);
    return 0;
}

关键说明

  • 可中断睡眠:wait_event_interruptible会将进程置于TASK_INTERRUPTIBLE状态,此时进程可以被信号唤醒,用户可通过Ctrl+C或kill命令正常终止进程。
  • 内存优化:提前释放用户数据拷贝的临时内存,避免阻塞期间持续占用内核内存资源。
  • 唤醒时机:必须在FIFO有剩余空间时调用wake_up_interruptible,确保等待的进程能及时被唤醒继续执行。
  • 信号处理:返回-ERESTARTSYS是内核处理中断系统调用的标准方式,用户空间会收到EINTR错误,或由内核自动重启系统调用(取决于用户空间的信号处理配置)。

内容的提问来源于stack exchange,提问作者Danny

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 12:25:16