Linux块设备模块死锁排查:挂载只读文件系统时挂起问题
Linux块设备模块挂载只读时死锁问题排查
问题描述
基于文件实现的Linux块设备模块,执行mount -o ro /dev/mydev /mnt挂载只读文件系统时会挂起,该问题在旧版4.x内核中未出现,同时存在以下疑问:
MyBlkDrvClose为何出现在调用栈中?它不应仅在设备被移除时才调用吗?- 代码中的锁处理逻辑是否存在问题?
- 同一线程能否重复获取同一自旋锁?
栈回溯信息
rcu: INFO: rcu_preempt detected stalls on CPUs/tasks: rcu: 2-...0: (303 ticks this GP) idle=f8a4/1/0x4000000000000000 softirq=13719/13720 fqs=13793 rcu: (detected by 1, t=60002 jiffies, g=24809, q=5447 ncpus=4) Sending NMI from CPU 1 to CPUs 2: NMI backtrace for cpu 2 CPU: 2 PID: 2674 Comm: tbia Tainted: P O 6.6.29-amd64-iflnet #1 Hardware name: /DH77KC, BIOS KCH7710H.86A.0111.2018.0329.1405 03/29/2018 RIP: 0010:native_queued_spin_lock_slowpath+0x7c/0x193 Code: 0f 92 c0 0f b6 c0 c1 e0 08 89 c2 8b 07 30 e4 09 d0 a9 00 01 ff ff 74 0c 0f ba e0 08 72 1a c6 47 01 00 eb 14 85 c0 74 0a 8a 07 <84> c0 74 04 f3 90 eb f6 66 c7 07 01 00 c3 48 c7 c0 40 38 02 00 65 RSP: 0018:ffffc90008d67e10 EFLAGS: 00000002 RAX: 0000000000000001 RBX: ffff8880607c0048 RCX: 0000000000000008 RDX: 0000000000000000 RSI: 0000000000000000 RDI: ffff8880618dc408 RBP: ffffc90008d67e18 R08: 0000000000000000 R09: 0000000000000000 R10: 0000000000000000 R11: 0000000000000008 R12: 0000000000000282 R13: ffff888001c8f120 R14: 0000000000000000 R15: ffff888001c8f120 FS: 0000000000000000(0000) GS:ffff888100300000(0000) knlGS:0000000000000000 CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033 CR2: 00000000f6a5b000 CR3: 0000000003229005 CR4: 00000000000606e0 Call Trace: <NMI> ? show_regs+0x64/0x68 ? nmi_cpu_backtrace+0xa2/0xcd ? nmi_cpu_backtrace_handler+0x10/0x15 ? nmi_handle+0x53/0xdc ? default_do_nmi+0x47/0x20c ? exc_nmi+0xa5/0xf6 ? end_repeat_nmi+0x16/0x1f ? native_queued_spin_lock_slowpath+0x7c/0x193 ? native_queued_spin_lock_slowpath+0x7c/0x193 ? native_queued_spin_lock_slowpath+0x7c/0x193 </NMI> <TASK> ? do_raw_spin_lock+0x18/0x1c _raw_spin_lock_irqsave+0x1a/0x21 MyBlkDrvWorkerThread+0x314/0x34c [MyBlkDrv] ? sugov_irq_work+0x17/0x17 kthread+0xf3/0xfb ? MyBlkDrvClose+0x2a/0x2a [MyBlkDrv] ? kthread_complete_and_exit+0x1f/0x1f ret_from_fork+0x25/0x3d ? kthread_complete_and_exit+0x1f/0x1f ret_from_fork_asm+0x11/0x20 </TASK>
核心代码片段
static void MyBlkDrvClose(struct gendisk *disk) { // keep count of open requests sMyBlkDrvDev *mydev = (sMyBlkDrvDev*) disk->private_data; spin_lock(&mydev->Lock); mydev->OpenCount--; spin_unlock(&mydev->Lock); } // ---------------------------------------------------- static struct block_device_operations MyBlkDrvBlockOps= { .owner = THIS_MODULE, // required initialization .open = MyBlkDrvOpen, // open device function .release = MyBlkDrvClose, // close device function .ioctl = MyBlkDrvIOCTL, // the ioctl handler #if defined(CONFIG_COMPAT) .compat_ioctl = MyBlkDrvIOCTL, // no special version needed #endif }; // ---------------------------------------------------- static int MyBlkDrvWorkerThread(void *data) { // move to local var sMyBlkDrvDev *mydev=data; struct request *req; set_user_nice(current, MIN_NICE); while (1) { // wait for event wait_event_interruptible(mydev->WorkerWait, kthread_should_stop() || !list_empty(&mydev->WorkerQueue)); // check if list is empty if (list_empty(&mydev->WorkerQueue)) { break; } // get queued request spin_lock_irq(&mydev->WorkerQueueLock); req=list_entry(mydev->WorkerQueue.next, struct request, queuelist); list_del_init(&req->queuelist); spin_unlock_irq(&mydev->WorkerQueueLock); // handle transfer request blk_mq_start_request(req); // do data transfer and ends transfer request (via MyBlkDrvEndRequest()) MyBlkDrvTransfer(req); } return 0; } // ---------------------------------------------------- static void MyBlkDrvEndRequest(struct request *req, int ret) { struct request_queue *q = req->q; unsigned long flags; spin_lock_irqsave(&q->queue_lock, flags); blk_mq_end_request (req, ret); spin_unlock_irqrestore(&q->queue_lock, flags); } // ---------------------------------------------------- static blk_status_t MyBlkDrvRequest (struct blk_mq_hw_ctx * hctx, const struct blk_mq_queue_data * bd) { struct request *req = bd->rq; struct request_queue *rq = bd->rq->q; sMyBlkDrvDev *mydev=(sMyBlkDrvDev *)(rq->queuedata); // master device is for ioctl only if (mydev->MyBlkDrvObj==NULL) { blk_mq_start_request(req); blk_mq_end_request(req, BLK_STS_IOERR); } else { // allow additional requests to be added while we handle req spin_unlock_irq(&rq->queue_lock); // add to our workers list spin_lock_irq(&mydev->WorkerQueueLock); list_add_tail(&req->queuelist, &mydev->WorkerQueue); spin_unlock_irq(&mydev->WorkerQueueLock); // wake up worker wake_up(&mydev->WorkerWait); // put back lock before returning spin_lock_irq(&rq->queue_lock); } return BLK_STS_OK; // always return ok } // ---------------------------------------------------- static struct blk_mq_ops _mq_ops = { .queue_rq = MyBlkDrvRequest, };
问题解答
1. 为什么MyBlkDrvClose出现在调用栈中?
MyBlkDrvClose是你注册到block_device_operations的.release回调,触发时机不是仅当设备被移除时,而是每次设备文件的打开实例被关闭时都会调用。比如挂载过程中,内核可能会多次打开/关闭设备文件(如挂载前的探测、校验操作),所以调用栈中出现这个函数是正常的。调用栈里它带?,说明是栈回溯的模糊匹配,实际是线程退出路径中关联的清理逻辑触发了它的引用,而非当前执行流直接调用。
2. 代码中的锁处理逻辑存在哪些问题?
代码有两处关键锁问题,是死锁的核心诱因:
MyBlkDrvRequest违规操作队列锁:blk-mq框架调用queue_rq回调时,已经持有rq->queue_lock。你手动调用spin_unlock_irq(&rq->queue_lock)释放锁后又重新获取,违反了blk-mq的锁管理规则。4.x内核对这种不规范操作容忍度较高,但6.x内核锁校验更严格,会引发锁顺序混乱,进而导致死锁。MyBlkDrvEndRequest错误加锁:blk_mq_end_request不需要调用者持有队列锁,手动加锁会造成锁嵌套。如果请求处理线程持有其他锁时调用该函数,会触发锁顺序反转,直接引发死锁。
结合栈回溯中工作线程卡在WorkerQueueLock的自旋等待,推测挂载只读场景触发了设备关闭逻辑,关闭逻辑与工作线程的锁顺序冲突,最终导致死锁。
3. 同一线程能否重复获取同一自旋锁?
普通spinlock_t不支持递归获取,同一线程重复获取会直接死锁——自旋锁会持续等待锁释放,但当前线程就是锁持有者,永远不会释放,陷入死循环。如果确实需要递归获取,可使用递归自旋锁DEFINE_RECURSIVE_SPINLOCK,但内核不推荐使用递归锁,因为它会掩盖锁顺序设计缺陷。
修复建议
- 移除
MyBlkDrvRequest中对rq->queue_lock的手动释放和重获取,由blk-mq框架自动管理该锁。 - 修改
MyBlkDrvEndRequest,去掉手动添加的queue_lock,直接调用blk_mq_end_request即可。 - 检查
MyBlkDrvOpen/Close中的OpenCount逻辑,确保设备关闭时正确同步工作线程退出,避免工作线程仍在处理请求时,设备资源被提前释放。
内容的提问来源于stack exchange,提问作者user3161924
相关产品推荐
相关产品推荐

