如何在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
相关产品推荐
相关产品推荐

