# Linux 核心設計: Block I/O(1): Multi-Queue Block I/O Queueing :::danger WIP ::: v6.0-rc1 > * [Multi-Queue Block IO Queueing Mechanism (blk-mq)](https://docs.kernel.org/block/blk-mq.html) > * [Linux Block IO: Introducing Multi-queue SSD Access on Multi-core Systems](https://kernel.dk/blk-mq.pdf) ## Introduction 在 Linux 開發之初,儲存方式都是默認為使用磁碟(Magnetic hard disks)的。該種儲存方式的存取受限於裝置的機械式部件,磁頭的移動在隨機存取(random access)的方式中效率不佳,因此以往的 I/O 瓶頸都是來自硬體。 但隨著像 SSD 這種非揮發式的儲存裝置的誕生與普及,這類儲存裝置不再對 random access 具有差勁的效率,並且還擁有平行存取的能力。隨著硬體的 I/O 速度變快,於是 I/O 瓶頸從存儲設備轉移到了 OS 本身。為了利用這些設備設計中的平行能力,multi-queue 的機制在核心中被引入。 ### Single queue 在 2.6.10 的 Linux 早期,kernel 中使用的 block I/O 框架是 single queue 的架構。 ![](https://i.imgur.com/Il6JfIh.png) 這是很單純的架構,block layer 維護單一個 request queue,每個 user 端的 I/O request 會被加入到尾端(tail),而底層的 driver 則可以從頂端(head)取得 request。block layer 本身也可以針對該 queue 將請求重排、合併以優化以滿足公平性或是提升 bandwidth。 然而由於整個架構中都基於單一 queue 上的處理,queue 的操作上就需要仰賴 lock 來避免 CPU 之間的競爭。在吞吐可以達到每秒百萬次 I/O 等級的現代 block device 上,這樣的 block layer 設計就成了進行 I/O 流程上的瓶頸。 ### Multi queue 顯然,single queue 設計已經不適用於現代的儲存裝置。為了改善其設計上的缺點,multi queue 的 I/O 方式因此被提出。該架構又可以稱為 blk-mq。 ![](https://i.imgur.com/4I9g289.png =400x) 如圖展示了該種框架的設計。可以看到 blk-mq 有兩層的 queue,分別是 Software Staging Queue 和 Hardware Dispatch Queue。在這種架構的流程上,當 request 到達 block layer 時,它首先會嘗試最短路徑: 直接發送其到 Hardware Dispatch Queue。但是,如果在 block layer 中有附加 IO scheduler,或者如果我們想嘗試 merge request。在這兩種情況下,request 就會先被發送到 Software Staging Queue。 原本 single queue 的設計中,單一個 queue 的主要任務包含了: 1. I/O 的合併、重排序等 I/O scheduing 2. 對 request 到對應硬體的 submit 進行限流,避免 device buffer 使用過剩 由於兩者的進行是可以獨立的,blk-mq 架構透過將兩項任務拆分至兩層的 queue 來因應。並且,block layer 收取 request 修改為 per-core 方式,分散至多個 queue 中來解決需要競爭 lock 的問題。 需要注意到的盲點是,由於 request 被分散到各異的 queue 中,而 request 是沒辦法在不同的 queue 之間進行合併與重排的,因此在透過合併與重排以優化 I/O request 這點上 multi queue 可能不及 single queue 來得優異。不過一方面,重排 I/O request 對現代的儲存裝置(i.e. SSD)的效益較小。而另一方面,由於需要合併的 request 經常來自同個 process,這代表它們也容易位於相同的 CPU queuue,因此不同 queue 之間的 request 合併並非必需。 ## Multi-Queue Block IO Queueing Mechanism (blk-mq) 以下,讓我們更深入探討 blk-mq 各組件的作用與特性。 ### Software Staging Queue Software Staging Queue 會以 `struct blk_mq_ctx` 這個結構來表示。 當使用者從 user space 對 block device 發出 I/O 的請求(例如讀寫檔案),如果無法直接走前述的最短路徑,則首先會進入的第一層 queue 是 Software Staging Queue。每個 cpu 各自擁有獨立的 Software Staging Queue,所以 CPU 上的各自的 I/O 就可以同時進行,不受 lock 的限制互相牽制。 Software Staging Queue 可用於進行 merge request。舉例來說,若有 3 個 request 分別是對 sector 3-6、6-7、7-9 進行,它們可以被合併成是對 3-9 的一個 request。即使對 SSD 這種非揮發的儲存裝置在 random access 與 sequential 上具有相近的速度,這也可以減少需被處理的 request 數量。術語上,這種合併請求的動作成為 plugging。 另一方面,若 block layer 中有加入 I/O scheduler,也可以在此層 queue 分配道不同的 CPU 上以提升 I/O 效率。 ### I/O scheduler 在 blk-mq 中,block layer 中包含 I/O scheduler。各種的 scheduler 擁有各自的一套規則,可以排序和合併 I/O request 來提高 I/O 的效率。I/O scheduler 可以在執行期透過 sysfs 進行選擇。 > * [Improving Linux System Performance with I/O Scheduler Tuning](https://www.cloudbees.com/blog/linux-io-scheduler-tuning) 需注意到排程只發生在同一個 queue 的 request 之間,否則 scheduler 就還需要考慮到 cache 衍生的問題,且每個 queue 會需要額外設置 lock。舉例來說,你可以選擇 NONE scheduler 這種規則最直接的: 它只會將 request 放在 process 正在運行 CPU 對應的 queue 上,而沒有額外的重新排序。 ### Hardware Dispatch Queue Hardware Dispatch Queue 會以 `struct blk_mq_hw_ctx` 這個結構來表示。 每個儲存裝置的 submission queues(或 device DMA ring buffer)各自被對應到一個 Hardware Dispatch Queue。 Hardware Dispatch Queue 會負責處理來自 Software Staging Queue 的 I/O request。在初始化階段,每個 Software Staging Queue 會擁有一個允許發送 request 的 Hardware Dispatch Queue 集合。 ### "Tag" 為了表示哪個 request 已經完成,每個 request 會應一個整數 ID,範圍是 0 到 queue 的大小。tag 會由 block layer 生成,且後續底層的 device driver layer 也可重用。當底層 driver 完成一個 request 後,tag 會被發送回 block layer 去通知 request 的結束,這個方法避免了還需要重回 queue 查找對應 request。 ## 閱讀原始碼: 以 NVME PCI 為例 [`drivers/nvme/host/pci.c`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c) 檔案中實作了讓使用者可以通過 PCI 介面使用 NVME 裝置的 driver。在模組的設計中就用到了 `blk_mq`。因此我們將透過該案例來探討 `blk_mq` 介面的使用方式。 :::info 此章節只會把焦點放在 `blk_mq` 上,之後若有機會會再深入 NVME driver 本身的設計。 ::: :::success [`null_blk`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/block/null_blk) 或許會是更易理解的範例,這裡選擇 NVMe driver 只是單純的作者喜好:D ::: ### `nvme_init` ```cpp static int __init nvme_init(void) { BUILD_BUG_ON(sizeof(struct nvme_create_cq) != 64); BUILD_BUG_ON(sizeof(struct nvme_create_sq) != 64); BUILD_BUG_ON(sizeof(struct nvme_delete_queue) != 64); BUILD_BUG_ON(IRQ_AFFINITY_MAX_SETS < 2); return pci_register_driver(&nvme_driver); } ``` 被做為 `module_init` 這個 macro 之參數的是 kernel module 的初始化函式,會在 module 被掛載時自動運行。以 NVMe driver 為例就是 [`nvme_init`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L3560)。 [`nvme_init`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L3560) 透過 `pci_register_driver` 嘗試將 [`nvme_driver`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L3545) 註冊到核心之中。`pci_register_driver` 會嘗試透過 driver 的名稱、id_table([`nvme_id_table`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L3423))和 bus 類型去匹配對應的 device,若匹配成功,則 `nvme_driver.probe` 就會被執行以讓我們初始化相關的結構,並綁定 device 和 driver 之間的關係。 :::info 雖然不完全相關,但 [今晚我想來點不一樣的 Driver: I2C Device](https://hackmd.io/@RinHizakura/BJDTZnUsF) 有討論 driver-bus-device 之間的關係和相關函式是如何運作的,有興趣者可以參考,本章就先忽略相關細節。 ::: #### `nvme_probe` ```cpp static int nvme_probe(struct pci_dev *pdev, const struct pci_device_id *id) { ... INIT_WORK(&dev->ctrl.reset_work, nvme_reset_work); INIT_WORK(&dev->remove_work, nvme_remove_dead_ctrl_work); mutex_init(&dev->shutdown_lock); ``` 一旦 `pci_register_driver` 註冊成功,[`nvme_probe`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L3087) 就會被執行。此時我們就需要將 `blk_mq` 結構進行適當的初始化了。關鍵的在如上所示之處,driver 透過 workqueue(`INIT_WORK`) 去建立一個延時的任務 [`nvme_reset_work`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L2798),後者就涉及 `blk_mq` 相關的操作。 ### `nvme_reset_work` 在 `flush_work(&dev->ctrl.reset_work)` 被呼叫時(例如一般的初始化流程是透過 [`nvme_async_probe`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L3078)),其會被 block 直到 [`nvme_reset_work`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L2798) 完成。 ```cpp static void nvme_reset_work(struct work_struct *work) { struct nvme_dev *dev = container_of(work, struct nvme_dev, ctrl.reset_work); bool was_suspend = !!(dev->ctrl.ctrl_config & NVME_CC_SHN_NORMAL); int result; if (dev->ctrl.state != NVME_CTRL_RESETTING) { dev_warn(dev->ctrl.device, "ctrl state %d is not RESETTING\n", dev->ctrl.state); result = -ENODEV; goto out; } ``` 如果 `nvme_reset_work` 是在 probe 之後,當時 [`nvme_reset_ctrl`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/core.c#L185) 會將 NVMe controller 的 state 切換到 `NVME_CTRL_RESETTING` state。因此執行 `nvme_reset_work` 時就會進入主要的初始化流程,否則的話會直接結束,避免重複的 reset。 ```cpp if (!dev->ctrl.admin_q) { result = nvme_pci_alloc_admin_tag_set(dev); if (result) goto out_unlock; } else { nvme_start_admin_queue(&dev->ctrl); } ``` 與 `blk_mq` 相關的初始化則在上述部分。若 `dev->ctrl.admin_q` 為 NULL,首先透過 [`nvme_pci_alloc_admin_tag_set`](#nvme_pci_alloc_admin_tag_set) 去建立起該 request_queue。 * 如果 `admin_q` 已經被建立過的狀況下,[`nvme_start_admin_queue`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/core.c#L5105) 下會使用 [`blk_mq_unquiesce_queue`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L294) 去完成 reset ### `nvme_pci_alloc_admin_tag_set` ```cpp static int nvme_pci_alloc_admin_tag_set(struct nvme_dev *dev) { struct blk_mq_tag_set *set = &dev->admin_tagset; set->ops = &nvme_mq_admin_ops; set->nr_hw_queues = 1; set->queue_depth = NVME_AQ_MQ_TAG_DEPTH; set->timeout = NVME_ADMIN_TIMEOUT; set->numa_node = dev->ctrl.numa_node; set->cmd_size = sizeof(struct nvme_iod); set->flags = BLK_MQ_F_NO_SCHED; set->driver_data = dev; if (blk_mq_alloc_tag_set(set)) return -ENOMEM; dev->ctrl.admin_tagset = set; } ``` [`nvme_pci_alloc_admin_tag_set`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L1758) 首先先設置 `struct blk_mq_tag_set` 的部份成員,例如 [`nvme_mq_admin_ops`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L1724) 是處理 admin request 的相關函式。其他還包含了 queue 的數量、大小等設定也都在此函數中被完成。之後就可以呼叫 `blk_mq_alloc_tag_set`。 `blk_mq_alloc_tag_set` 主要完成了 `struct blk_mq_tag_set` 進一步的初始化,主要是 [`tags`](https://elixir.bootlin.com/linux/v6.0-rc1/source/include/linux/blk-mq.h#L506)/[`shared_tags`](https://elixir.bootlin.com/linux/v6.0-rc1/source/include/linux/blk-mq.h#L508) 的相關設定,同時也建立了 [`struct request`](https://elixir.bootlin.com/linux/v6.0-rc1/source/include/linux/blk-mq.h#L736) 的集合。 ```cpp dev->ctrl.admin_q = blk_mq_init_queue(set); if (IS_ERR(dev->ctrl.admin_q)) { blk_mq_free_tag_set(set); dev->ctrl.admin_q = NULL; return -ENOMEM; } ``` 第二階段的初始化是透過 [`blk_mq_init_queue`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L3916) 去建立 `struct request_queue`,後者會透過 tag set 進行初始化,並與其關聯在一起。 ```cpp if (!blk_get_queue(dev->ctrl.admin_q)) { nvme_dev_remove_admin(dev); dev->ctrl.admin_q = NULL; return -ENODEV; } return 0; ``` 然後藉由 [`blk_get_queue`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-core.c#L455) 去將 reference count 加一: * 與之相對的 [ `nvme_dev_remove_admin`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L1744) 會藉由 [`blk_mq_destroy_queue`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L3931) -> [`blk_put_queue`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-core.c#L267) 去扣除 reference count admin 類別的 `blk-mq` 建立之基本流程至此就算是完成了。 ### `nvme_mq_admin_ops` ```cpp static const struct blk_mq_ops nvme_mq_admin_ops = { .queue_rq = nvme_queue_rq, .complete = nvme_pci_complete_rq, .init_hctx = nvme_admin_init_hctx, .init_request = nvme_pci_init_request, .timeout = nvme_timeout, }; ``` 在 `nvme_pci_alloc_admin_tag_set` 中我們將 [`nvme_mq_admin_ops`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L1724) assign 到 `blk_mq_tag_set` 結構中,然後使用其來呼叫 `blk_mq_alloc_tag_set`。這是 C 語言中常見的將方法的抽象與實際行為分離的手法: 通過定義一個成員為多個函式指標的結構,則 blk-mq 可以將依賴於硬體的部分抽離出來,讓使用該介面的開發者自行去定義相關的行為。 換句話說,blk-mq 之 API 的使用關鍵其實就是在如何正確實作 `blk_mq_ops` 中的每個方法(如果必要)而已,再剩下只要在 probe function 呼叫 `blk_mq_alloc_tag_set` 和 `blk_mq_init_queue` 就能使用 blk-mq。下面我們就來看看各個方法分別需要完成哪些行為吧! ### `nvme_admin_init_hctx` ```cpp static int nvme_admin_init_hctx(struct blk_mq_hw_ctx *hctx, void *data, unsigned int hctx_idx) { struct nvme_dev *dev = data; struct nvme_queue *nvmeq = &dev->queues[0]; WARN_ON(hctx_idx != 0); WARN_ON(dev->admin_tagset.tags[0] != hctx->tags); hctx->driver_data = nvmeq; return 0; } ``` 我們在 `nvme_mq_admin_ops` 中會設置 `init_hctx` 的方法 [`nvme_admin_init_hctx`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L403),這會在 `blk_mq_init_queue` 需要初始化 `blk_mq_hw_ctx` 結構時被使用(具體使用到的地方是 [`blk_mq_init_hctx`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L3495))。 實際上的動作只是把 `driver_data` 設置成 NVMe 裝置的主結構 `nvme_dev` 下的 [`struct nvme_queue`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L190)。後者是用來描述 NVMe 標準中定義的 admin queue 或 I/O queue。 * `WARN_ON` 的前提來自 `set->nr_hw_queues = 1` ### `nvme_pci_init_request` ```cpp static int nvme_pci_init_request(struct blk_mq_tag_set *set, struct request *req, unsigned int hctx_idx, unsigned int numa_node) { struct nvme_dev *dev = set->driver_data; struct nvme_iod *iod = blk_mq_rq_to_pdu(req); int queue_idx = (set == &dev->tagset) ? hctx_idx + 1 : 0; struct nvme_queue *nvmeq = &dev->queues[queue_idx]; BUG_ON(!nvmeq); iod->nvmeq = nvmeq; nvme_req(req)->ctrl = &dev->ctrl; nvme_req(req)->cmd = &iod->cmd; return 0; } ``` 在 `nvme_mq_admin_ops` 等 `struct blk_mq_ops` 結構中可以定義 `init_request` 方法,這被用來實作初始化 `struct request` 的方式。 舉例來說,[`blk_mq_tags`](https://elixir.bootlin.com/linux/v6.0-rc1/source/include/linux/blk-mq.h#L727) 中有一個 `stuct request` 結構的成員 `**static_rqs`,其會指向在 [`blk_mq_alloc_rqs`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L3270) 預分配的 `struct page` 作為 pool。則每次要建立一個新的 request 時就可以從該 pool 中取得空間來描述對應操作。當這個 pool 被建立出來之後,就可以藉由 `init_request` 方法初始化 pool 中的每個 instance。`init_request` 具體的呼叫處是在 [`blk_mq_init_request`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L3194)。 以 [`nvme_pci_init_request`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L427) 而言,首先是要找到要初始化的這個 request 所對應的 `nvme_queue`。如果 `(set == &dev->tagset)` 為 false,表示 init 的 `struct request` 是 admin command 相關,因此對應的 `nvme_queue` 是 `queue_idx = 0` 者。反之,`queue_idx` 則可以對應到 `hctx_idx + 1` 的 `nvme_queue`。 * [`blk_mq_rq_to_pdu`](https://elixir.bootlin.com/linux/v6.0-rc1/source/include/linux/blk-mq.h#L923) 這函式的作用是得到 `req` 的 command 相關的資料,後者被放在 `req` 後的連續空間,仔細去看 `blk_mq_alloc_rqs` 的實作的話,會發現配置每個 `struct request` 時後面會加上一段額外的 `set->cmd_size` 空間,以 nvme-pci 來說這個大小是 `sizeof(struct nvme_iod)` * `nvme_req(req)` 內部也是呼叫 `blk_mq_rq_to_pdu`,為甚麼 `blk_mq_rq_to_pdu` 明明是轉換為同一位置,卻可以存取到 `struct nvme_request` 和 `struct nvme_iod` 兩種結構? 這實際上是因為 [`struct nvme_iod`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L226) 的第一個 member 就是 `struct nvme_request` > [In the Linux function blk_mq_rq_to_pdu what is a pdu?](https://stackoverflow.com/questions/68785800/in-the-linux-function-blk-mq-rq-to-pdu-what-is-a-pdu) ### `nvme_queue_rq` ```cpp static blk_status_t nvme_queue_rq(struct blk_mq_hw_ctx *hctx, const struct blk_mq_queue_data *bd) { struct nvme_queue *nvmeq = hctx->driver_data; struct nvme_dev *dev = nvmeq->dev; struct request *req = bd->rq; struct nvme_iod *iod = blk_mq_rq_to_pdu(req); blk_status_t ret; /* * We should not need to do this, but we're still using this to * ensure we can drain requests on a dying queue. */ if (unlikely(!test_bit(NVMEQ_ENABLED, &nvmeq->flags))) return BLK_STS_IOERR; if (unlikely(!nvme_check_ready(&dev->ctrl, req, true))) return nvme_fail_nonready_command(&dev->ctrl, req); ret = nvme_prep_rq(dev, req); if (unlikely(ret)) return ret; spin_lock(&nvmeq->sq_lock); nvme_sq_copy_cmd(nvmeq, &iod->cmd); nvme_write_sq_db(nvmeq, bd->last); spin_unlock(&nvmeq->sq_lock); return BLK_STS_OK; } ``` `queue_rq` 是註冊一個新的 `blk-mq` 所最底限度要實現的方法。在 `queue_rq` 中,我們要定義的是一個 request 如何從 HW queue 被轉換為對 block device 硬體的實際操作。以 NVMe 的 [`nvme_queue_rq`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L933) 為例,其目的是將 NVMe 的 command 搬動到裝置的 SQ 中([`nvme_sq_copy_cmd`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L504))。 ### `nvme_prep_rq` 而在 `nvme_queue_rq` 中我們最需關注的函式是 `nvme_prep_rq`。 :::warning TODO ::: ### `nvme_pci_complete_rq` ```cpp static void nvme_pci_complete_rq(struct request *req) { nvme_pci_unmap_rq(req); nvme_complete_rq(req); } ``` `struct blk_mq_ops` 結構中的 `complete` 方法需要定義的是當 block device 處理完一筆 request 後,driver 需對其結果進行對應行為。 以 NVMe 為例 `complete` 的實作是 [`nvme_pci_complete_rq`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L1032),後者又可以拆分成兩段函式: * [`nvme_pci_unmap_rq`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L1020): 在此前介紹的 `queue_rq` 方法 `nvme_queue_rq` 中,[`nvme_prep_rq`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L896) 會利用 [`nvme_map_data`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L828) 等 API 進行 DMA mapping,在 req 完成後則需要解除此 mapping * [`nvme_complete_rq`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/core.c#L390): 判斷是否 request 已經完成需要釋放相關資源,或是可以 retry request 等 :::warning 底層實做涉及許多 NVMe 本身的細節,筆者功力不足無法仔細解釋QQ ::: ### `nvme_timeout` `struct blk_mq_ops` 中的 `timeout` 方法定義當透過 blk-mq 送出的 request 發生 timeout(送出 request 過了一定時間沒有 complete) 時的處理方式。 在最開始初始化時使用的 `blk_mq_init_queue` 之中分別會呼叫 [`blk_alloc_queue`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-core.c#L377) 和 [`blk_mq_init_allocated_queue`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L4068) 兩個關鍵的初始化函式(更多細節可以參考 [Linux 核心設計: blk-mq(2): 深入剖析 blk-mq API](https://hackmd.io/@RinHizakura/Hk45dQJ7s))。其中與 timeout 相關的初始化部分是: * `blk_alloc_queue`: * 首先其會藉由 Linux 中的 `timer_setup` 建立起一個 timer work `blk_rq_timed_out_timer`,後者會在設置的 timer 到點時被執行 * [`blk_rq_timed_out_timer`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-core.c#L366) 會啟動 `&q->timeout_work` 中註冊的 workqueue 函式 * `blk_mq_init_allocated_queue`: * 其會將 `&q->timeout_work` 註冊的函式變更為 [`blk_mq_timeout_work`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L1488) 綜合以上,當 [`blk_mq_start_request`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L1137) 被呼叫而開始一個 request 的處理流程。`blk_mq_start_request` 會執行 [`blk_add_timer`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-timeout.c#L128) -> `mod_timer`。藉此讓之前提到的 timer task `blk_rq_timed_out_timer` 在一段時間後被執行。而這個 timer task 最後就會啟動 `&q->timeout_work` 下註冊的 `blk_mq_timeout_work`,後者最後可以透過 [`blk_mq_check_expired`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L1472) -> [`blk_mq_rq_timed_out`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L1429) 去觸發 `timeout` 方法。 而 NVMe driver 中定義的 `timeout` 方法為 [`nvme_timeout`](https://elixir.bootlin.com/linux/v6.0-rc1/source/drivers/nvme/host/pci.c#L1335)。不過由於其本身內容與 blk-mq 自身機制關係較少,比較多是與 NVMe 相關,細節我們就不在此章節釐清。 ## 小結 在此章節中,我們介紹了 blk-mq 機制被提出的原因背景,以及其實作上的核心思想,並以 NVMe 為範例簡單介紹了 blk-mq 的使用方式,包含初始化以及幾個不同的 `struct blk_mq_ops` 中方法需實作的功能。不過這些 API 實際上內部又是如何運作的呢? 這些細節我們會嘗試在另一個章節再行探討。 ## Reference https://blog.csdn.net/hu1610552336/article/details/111464548 http://www.liuhaihua.cn/archives/686055.html https://kernel.dk/blk-mq.pdf https://lwn.net/Articles/736534/ https://lwn.net/Articles/738449/ https://hyunyoung2.github.io/2016/09/14/Multi_Queue/ https://blog.csdn.net/qq_32740107/article/details/106302376 https://lwn.net/Articles/552904/