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

Linux块设备模块死锁排查:挂载只读文件系统时挂起问题

Linux块设备模块挂载只读时死锁问题排查

问题描述

基于文件实现的Linux块设备模块,执行mount -o ro /dev/mydev /mnt挂载只读文件系统时会挂起,该问题在旧版4.x内核中未出现,同时存在以下疑问:

  1. MyBlkDrvClose为何出现在调用栈中?它不应仅在设备被移除时才调用吗?
  2. 代码中的锁处理逻辑是否存在问题?
  3. 同一线程能否重复获取同一自旋锁?

栈回溯信息

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,但内核不推荐使用递归锁,因为它会掩盖锁顺序设计缺陷。

修复建议

  1. 移除MyBlkDrvRequest中对rq->queue_lock的手动释放和重获取,由blk-mq框架自动管理该锁。
  2. 修改MyBlkDrvEndRequest,去掉手动添加的queue_lock,直接调用blk_mq_end_request即可。
  3. 检查MyBlkDrvOpen/Close中的OpenCount逻辑,确保设备关闭时正确同步工作线程退出,避免工作线程仍在处理请求时,设备资源被提前释放。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 08:51:01