# 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 的架構。

這是很單純的架構,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。

如圖展示了該種框架的設計。可以看到 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/