linux 中nvme 的中斷申請及處理


/**
 * struct irq_desc - interrupt descriptor
 * @irq_data:        per irq and chip data passed down to chip functions
 * @kstat_irqs:        irq stats per cpu
 * @handle_irq:        highlevel irq-events handler
 * @preflow_handler:    handler called before the flow handler (currently used by sparc)
 * @action:        the irq action chain
 * @status:        status information
 * @core_internal_state__do_not_mess_with_it: core internal status information
 * @depth:        disable-depth, for nested irq_disable() calls
 * @wake_depth:        enable depth, for multiple irq_set_irq_wake() callers
 * @irq_count:        stats field to detect stalled irqs
 * @last_unhandled:    aging timer for unhandled count
 * @irqs_unhandled:    stats field for spurious unhandled interrupts
 * @lock:        locking for SMP
 * @affinity_hint:    hint to user space for preferred irq affinity
 * @affinity_notify:    context for notification of affinity changes
 * @pending_mask:    pending rebalanced interrupts
 * @threads_oneshot:    bitfield to handle shared oneshot threads
 * @threads_active:    number of irqaction threads currently running
 * @wait_for_threads:    wait queue for sync_irq to wait for threaded handlers
 * @dir:        /proc/irq/ procfs entry
 * @name:        flow handler name for /proc/interrupts output
 */
struct irq_desc {
    struct irq_data        irq_data;
    unsigned int __percpu    *kstat_irqs;///* irq的統計信息,在proc中可查到 */
    irq_flow_handler_t    handle_irq;
    /* 回調函數,當此中斷產生中斷時,會調用handle_irq,在handle_irq中進行遍歷irqaction鏈表*/
         /* handle_simple_irq  用於簡單處理;
     * handle_level_irq  用於電平觸發中斷的流控處理;
     * handle_edge_irq  用於邊沿觸發中斷的流控處理;
     * handle_fasteoi_irq  用於需要響應eoi的中斷控制器;
     * handle_percpu_irq  用於只在單一cpu響應的中斷;
     * handle_nested_irq  用於處理使用線程的嵌套中斷;
     */
#ifdef CONFIG_IRQ_PREFLOW_FASTEOI
    irq_preflow_handler_t    preflow_handler;
#endif
    struct irqaction    *action;    /* IRQ action list */

    unsigned int        status_use_accessors;
    unsigned int        core_internal_state__do_not_mess_with_it;
    unsigned int        depth;        /* nested irq disables */ /* 嵌套深度,中斷線被激活顯示0,如果為正數,表示被禁止次數 */
    unsigned int        wake_depth;    /* nested wake enables */
    unsigned int        irq_count;    /* For detecting broken IRQs *//* 此中斷線上發生的中斷次數 */
    unsigned long        last_unhandled;    /* Aging timer for unhandled count */ /* 上次發生未處理中斷時的jiffies值 */
    unsigned int        irqs_unhandled;/* 中斷線上無法處理的中斷次數,如果當第100000次中斷發生時,有超過99900次是意外中斷,系統會禁止這條中斷線 */
    raw_spinlock_t        lock;
    struct cpumask        *percpu_enabled;
#ifdef CONFIG_SMP
    const struct cpumask    *affinity_hint; /* CPU親和力關系,其實就是每個CPU是占一個bit長度,某CPU上置為1表明該CPU可以進行這個中斷的處理 */
    struct irq_affinity_notify *affinity_notify;
#ifdef CONFIG_GENERIC_PENDING_IRQ
    cpumask_var_t        pending_mask; /* 用於調整irq在各個cpu之間的平衡 */
#endif
#endif
    unsigned long        threads_oneshot;
    atomic_t        threads_active;
    wait_queue_head_t       wait_for_threads; /* 用於synchronize_irq(),等待該irq所有線程完成 */
#ifdef CONFIG_PROC_FS
    struct proc_dir_entry    *dir; /* 指向與IRQn相關的/proc/irq/n目錄的描述符 */
#endif
    int            parent_irq;
    struct module        *owner;
    const char        *name;/* 在/proc/interrupts所顯示名稱 */
} ____cacheline_internodealigned_in_smp;

中斷的描述符如上所述。作為背景知識,可以理解下面的內容,本文討論基於的內核版本信息如下:

uname -a
Linux localhost.localdomain 3.10.0

 

我們知道,nvme的多隊列,默認按照核數的多少來設置,目前nvme的隊列有兩種,admin隊列,IO隊列,兩者都屬於nvme_queue對象,submit queue,complete queue是一個nvme_queue對象的一個成員,其中submit queue在代碼中會簡寫為sq,complete queue會簡寫成cq。兩者是Queue Pair(QP),也就是submitqueue·completequeue和admin queue不是同一個級別的對象,對於admin隊列來說,它也有自己的submitquque和completequeue,第一次看代碼時往往容易混淆。

首先,我們來看一下nvme總共用的中斷數。

# cat /proc/interrupts |grep nvme |wc -l
320

 該系統上一共4塊盤,80個核,就有320個中斷,一個核對應一個隊列,一個中斷號。按道理IOqueue有80個,adminqueue也需要用中斷,

那么中斷數應該是81*4=324才對。

# cat /proc/interrupts |grep -i nvme[0-3]q0|awk '{print $1,$(NF-1),$NF}'
1762: nvme2q0, nvme2q1
1766: nvme3q0, nvme3q1
1767: nvme0q0, nvme0q1
1768: nvme1q0, nvme1q1

我們發現,nvme0q0 和 nvme0q1 是共享中斷的。而其他的sq都是雖然帶的參數也是共享,但是從實際情況看,是獨占的。所以數量是320個。

nvme0q0 就是我們可愛的admin queue,從申請的角度看,我們可以看出來,一開始adminqueue申請,用的是裸命令,后面的ioqueues申請,利用的是admin queue的隊列。

 

由於admin的queue是最先申請的,所以包括中斷號也是單獨申請的,nvme_configure_admin_queue 中,調用靜態函數 queue_request_irq來初始化admin的隊列的中斷,而且它傳入的參數是共享的,也就是不需要獨占中斷,IRQF_SHARED。admin的隊列編號是0。

static int nvme_configure_admin_queue(struct nvme_dev *dev)
{
    int result;
    u32 aqa;
    u64 cap = lo_hi_readq(dev->bar + NVME_REG_CAP);
    struct nvme_queue *nvmeq;

    dev->subsystem = readl(dev->bar + NVME_REG_VS) >= NVME_VS(1, 1, 0) ?
                        NVME_CAP_NSSRC(cap) : 0;

    if (dev->subsystem &&
        (readl(dev->bar + NVME_REG_CSTS) & NVME_CSTS_NSSRO))
        writel(NVME_CSTS_NSSRO, dev->bar + NVME_REG_CSTS);

    result = nvme_disable_ctrl(&dev->ctrl, cap);
    if (result < 0)
        return result;

    nvmeq = dev->queues[0];//admin是第一個queue,隊列編號肯定是0
    if (!nvmeq) {//admin的queue,深度為256
        nvmeq = nvme_alloc_queue(dev, 0, NVME_AQ_DEPTH);//2
        if (!nvmeq)
            return -ENOMEM;
    }

    aqa = nvmeq->q_depth - 1;
    aqa |= aqa << 16;
//將sq_dma_addr 和 cq_dma_addr 分別系到bar 空間偏移為NVME_REG_ASQ和NVME_REG_ACQ,這個地址都是在nvme_alloc_queue 中申請的.
    writel(aqa, dev->bar + NVME_REG_AQA);
    lo_hi_writeq(nvmeq->sq_dma_addr, dev->bar + NVME_REG_ASQ);
    lo_hi_writeq(nvmeq->cq_dma_addr, dev->bar + NVME_REG_ACQ);

    result = nvme_enable_ctrl(&dev->ctrl, cap);
    if (result)
        return result;

    nvmeq->cq_vector = 0;
    result = queue_request_irq(dev, nvmeq, nvmeq->irqname);//為admin隊列申請中斷,這個是nvme驅動最先申請的中斷號
    if (result) {
        nvmeq->cq_vector = -1;
        return result;
    }

    return result;
}

 

而ioquue,都是調用的nvme_pci_enable來完成中斷的申請,

中斷注冊,也是在隊列創建的時候完成,nvme_create_queue ,其中需要注意的是,admin的中斷,會先注冊,然后再取消注冊,然后再注冊一次。先注冊的目的是為了借助這個中斷來返回處理創建sq和cq等命令的結果。

nvme_create_io_queues---|nvme_alloc_queue----分配nvmeq結構體,並記錄到dev->queues[]數組中,並分配submit queue 和complete queue命令所需要的空間。

                                       ---|nvme_create_queue---|adapter_alloc_cq----構建cmd,利用admin 的queue發送控制消息,分配sq相關信息

                                                                             ---|adapter_alloc_sq----這個是分配submitqueue隊列的相關信息,與cq類似。

                                                                             ---|queue_request_irq---這個是申請中斷

                                                                             ---|nvme_init_queue---初始化隊列

下面,重點了解下queue_request_irq 的傳入參數:

static int queue_request_irq(struct nvme_dev *dev, struct nvme_queue *nvmeq,
                            const char *name)
{
    if (use_threaded_interrupts)//中斷線程化使能,默認沒有開啟
        return request_threaded_irq(dev->entry[nvmeq->cq_vector].vector,
                    nvme_irq_check, nvme_irq, IRQF_SHARED,
                    name, nvmeq);
    return request_irq(dev->entry[nvmeq->cq_vector].vector, nvme_irq,
                IRQF_SHARED, name, nvmeq);
}

static int use_threaded_interrupts;默認就是0了。

也就是nvme驅動默認沒有使能中斷線程化功能。request_irq 是中斷的申請接口了,定義在interrupt.h中,調用request_threaded_irq,其中第三個傳入傳入的是NULL

static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
        const char *name, void *dev)
{
    return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}

request_threaded_irq定義在manager.c中,后面就是中斷的通用流程了,我們主要針對傳入的參數分析一下:

int request_threaded_irq(unsigned int irq, irq_handler_t handler,
             irq_handler_t thread_fn, unsigned long irqflags,
             const char *devname, void *dev_id)
{.....
......
    action->handler = handler;---------就是我們的nvme_irq
    action->thread_fn = thread_fn;-----nvme中傳入的是NULL
action
->flags = irqflags;
action
->name = devname;-------------nvmeq->irqname,中斷名稱,就是/proc/interrupts每行的最后那列,當中斷共享的時候,會顯示注冊的多個名稱。
action
->dev_id = dev_id;------------nvmeq,作為對象,會在thread_fn 作為最后一個參數傳回來
..... 
.....
}
static irqreturn_t nvme_irq(int irq, void *data)------處理nvme中斷
{
    irqreturn_t result;
    struct nvme_queue *nvmeq = data;--------回調nvme_irq的時候,傳入的data就是之前注冊的時候傳入的dev_id
    spin_lock(&nvmeq->q_lock);-------------由於默認每個隊列是在一個cpu上,所以這里自旋鎖的消耗很少,是一種無鎖設計的保護而已。
    nvme_process_cq(nvmeq);
    result = nvmeq->cqe_seen ? IRQ_HANDLED : IRQ_NONE;
    nvmeq->cqe_seen = 0;
    spin_unlock(&nvmeq->q_lock);
    return result;
}

去掉包裹函數,真正干活的就是nvme_process_cq 了,又看到了熟悉的head,tail標志,這個機制的描述在網上已經爛大街了,借用一下:

Head/Tail機制

Submission Queue使用Tail,Completion Queue使用Head,兩者均由Host操作。處理完一個Command,Tail或Head加1,當大於Queue Depth時,則回到0。通過對比Head和Tail的值,就知道一個Queue中有多少未處理的Submission Command。下面的圖摘自NVMe Spec,有興趣的同學可以據此琢磨下Empty Queue和Full Queue的定義。

static int nvme_process_cq(struct nvme_queue *nvmeq)
{
    u16 head, phase;

    head = nvmeq->cq_head;
    phase = nvmeq->cq_phase;

    while (nvme_cqe_valid(nvmeq, head, phase)) {
        struct nvme_completion cqe = nvmeq->cqes[head];
        struct request *req;

        if (++head == nvmeq->q_depth) {
            head = 0;
            phase = !phase;
        }


        if (unlikely(cqe.command_id >= nvmeq->q_depth)) {
            dev_warn(nvmeq->dev->ctrl.device,
                "invalid id %d completed on queue %d\n",
                cqe.command_id, le16_to_cpu(cqe.sq_id));
            continue;
        }

        /*
         * AEN requests are special as they don't time out and can
         * survive any kind of queue freeze and often don't respond to
         * aborts.  We don't even bother to allocate a struct request
         * for them but rather special case them here.
         */
        if (unlikely(nvmeq->qid == 0 &&
                cqe.command_id >= NVME_AQ_BLKMQ_DEPTH)) {
            nvme_complete_async_event(&nvmeq->dev->ctrl,
                    cqe.status, &cqe.result);
            continue;
        }

        req = blk_mq_tag_to_rq(*nvmeq->tags, cqe.command_id);
        nvme_req(req)->result = cqe.result;
        blk_mq_complete_request(req, le16_to_cpu(cqe.status) >> 1);
    }

    if (head == nvmeq->cq_head && phase == nvmeq->cq_phase)
        return 0;

    if (likely(nvmeq->cq_vector >= 0))
        writel(head, nvmeq->q_db + nvmeq->dev->db_stride);
    nvmeq->cq_head = head;
    nvmeq->cq_phase = phase;

    nvmeq->cqe_seen = 1;
    return 1;
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM