# Linux 核心設計: Block I/O(2): blk-mq API
:::danger
WIP
:::
## blk-mq 的資料結構
### `request_queue`
[`request_queue`](https://elixir.bootlin.com/linux/v6.0-rc1/source/include/linux/blkdev.h#L388) 結構是 `blk_mq` 中的主體結構,描述對於 I/O request 的處理、SW/HW queue 和 I/O scheduler 等資訊。可透過 [`blk_mq_init_queue`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L3916) 進行初始化
### `struct blk_mq_tags`
```cpp
/*
* Tag address space map.
*/
struct blk_mq_tags {
unsigned int nr_tags;
unsigned int nr_reserved_tags;
atomic_t active_queues;
struct sbitmap_queue bitmap_tags;
struct sbitmap_queue breserved_tags;
struct request **rqs;
struct request **static_rqs;
struct list_head page_list;
/*
* used to clear request reference in rqs[] before freeing one
* request pool
*/
spinlock_t lock;
};
```
每個 `struct blk_mq_tags` 中會預先分配好一個 request pool,並對應到一個整數的 tag。
### `struct blk_mq_tag_set`
```cpp
struct blk_mq_tag_set {
struct blk_mq_queue_map map[HCTX_MAX_TYPES];
unsigned int nr_maps;
const struct blk_mq_ops *ops;
unsigned int nr_hw_queues;
unsigned int queue_depth;
unsigned int reserved_tags;
unsigned int cmd_size;
int numa_node;
unsigned int timeout;
unsigned int flags;
void *driver_data;
struct blk_mq_tags **tags;
struct blk_mq_tags *shared_tags;
struct mutex tag_list_lock;
struct list_head tag_list;
};
```
每個 block device 會對應一個 [`blk_mq_tag_set`](https://elixir.bootlin.com/linux/v6.0-rc1/source/include/linux/blk-mq.h#L493),該結構中儲存了 block device 的相關資訊,例如 HW queue 的數量與大小等、從 Hardware Dispatch Queue 提取 request 以操作硬體之方法的抽象(`ops`)、可以把 tag 對應到 `struct request` 的 `blk_mq_tag` 結構(`tags`)等。
一個 tag set 可以被多個 request queue 共享。
### `struct blk_mq_ops`
核心中使用 [`struct blk_mq_ops`](https://elixir.bootlin.com/linux/v6.0-rc1/source/include/linux/blk-mq.h#L531) 結構來抽象 block I/O 在 blk-mq 上的操作。該結構中成員是多個不同的函式指標,讓 block device driver 上實作裝置的不同行為。
```cpp
/**
* struct blk_mq_ops - Callback functions that implements block driver
* behaviour.
*/
struct blk_mq_ops {
/**
* @queue_rq: Queue a new request from block IO.
*/
blk_status_t (*queue_rq)(struct blk_mq_hw_ctx *,
const struct blk_mq_queue_data *);
```
## blk-mq API
### `blk_mq_alloc_tag_set`
```cpp
int blk_mq_alloc_tag_set(struct blk_mq_tag_set *set)
{
int i, ret;
BUILD_BUG_ON(BLK_MQ_MAX_DEPTH > 1 << BLK_MQ_UNIQUE_TAG_BITS);
if (!set->nr_hw_queues)
return -EINVAL;
if (!set->queue_depth)
return -EINVAL;
if (set->queue_depth < set->reserved_tags + BLK_MQ_TAG_MIN)
return -EINVAL;
if (!set->ops->queue_rq)
return -EINVAL;
if (!set->ops->get_budget ^ !set->ops->put_budget)
return -EINVAL;
```
[`blk_mq_alloc_tag_set`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L4285) 的主要作用是將 `struct blk_mq_tag_set` 中相關結構進行完整的建立,包含 `struct blk_mq_tag` 以及 `struct request` 的集合。
在函式進行主要的工作開始前,會先確認好 `struct blk_mq_tag_set` 的狀態滿足特定條件:
* `set` 的成員包含 HW queue 的數量(`nr_hw_queues`)、queue 的大小(`queue_depth`)需要被預先設置好
* `ops` 中則至少要定義 `queue_rq`
* `get_budget` 和 `put_budget` 兩者只能同時被定義或不定義。
```cpp
if (set->queue_depth > BLK_MQ_MAX_DEPTH) {
pr_info("blk-mq: reduced tag depth to %u\n",
BLK_MQ_MAX_DEPTH);
set->queue_depth = BLK_MQ_MAX_DEPTH;
}
if (!set->nr_maps)
set->nr_maps = 1;
else if (set->nr_maps > HCTX_MAX_TYPES)
return -EINVAL;
/*
* If a crashdump is active, then we are potentially in a very
* memory constrained environment. Limit us to 1 queue and
* 64 tags to prevent using too much memory.
*/
if (is_kdump_kernel()) {
set->nr_hw_queues = 1;
set->nr_maps = 1;
set->queue_depth = min(64U, set->queue_depth);
}
/*
* There is no use for more h/w queues than cpus if we just have
* a single map
*/
if (set->nr_maps == 1 && set->nr_hw_queues > nr_cpu_ids)
set->nr_hw_queues = nr_cpu_ids;
```
其他的成員也有限制需要檢查:
* `queue_depth` 不能超過 `BLK_MQ_MAX_DEPTH`
* `nr_maps` 是否使用預設值設置
* `nr_hw_queues` 的數量不能超出 CPU 的最大數量
* `is_kdump_kernel()` 為 true 的話表示特殊的 config 有被設置,又或是在 booting 流程出現一些非預期的 panic 可能導致,此時的 blk_mq 初始化會再受一些限制
```cpp
if (blk_mq_alloc_tag_set_tags(set, set->nr_hw_queues) < 0)
return -ENOMEM;
```
若一切就緒,則先使用 [`blk_mq_alloc_tag_set_tags`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L4273) 去為 `set->tags` 配置 `nr_hw_queues` 數量個的 `*blk_mq_tags`。
```cpp
ret = -ENOMEM;
for (i = 0; i < set->nr_maps; i++) {
set->map[i].mq_map = kcalloc_node(nr_cpu_ids,
sizeof(set->map[i].mq_map[0]),
GFP_KERNEL, set->numa_node);
if (!set->map[i].mq_map)
goto out_free_mq_map;
set->map[i].nr_queues = is_kdump_kernel() ? 1 : set->nr_hw_queues;
}
```
接著配置 `map[i].mq_map` 所需空間。每個 `map` 會描述一種特定類型的 HW queue 所對應到的 SW queue 之關係。其中 `map` 中包含 `nr_cpu_ids` 個 `mq_map`,每個 `mq_map` 會將一個 CPU ID 對應到一個 HW queue,也因此這裡需要 `nr_cpu_ids`(processor 數量) 個 element 大小的陣列。
```cpp
ret = blk_mq_update_queue_map(set);
if (ret)
goto out_free_mq_map;
```
前面我們已經配置出了各 `mq_map` 的空間,而 [`blk_mq_update_queue_map`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L4213) 的目的是將 `mq_map` 的對應關係設定。以 nvme-pci 為例,`nvme_mq_admin_ops` 部分因為 `set->ops->map_queues` 並沒有被實作,因此會使用 default 方法 [`blk_mq_map_queues`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq-cpumap.c#L35) 進行對應。`blk_mq_map_queues` 大致來說會將 HW queue 平均分配到當前可用的 CPU 上。
像是 `nvme_mq_ops` 有定義 `ops->map_queues` 的情形下則會使用其定義的方法來進行 mapping。
```cpp
ret = blk_mq_alloc_set_map_and_rqs(set);
if (ret)
goto out_free_mq_map;
```
前面提到 [`blk_mq_alloc_tag_set_tags`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L4273) 時 `set->tags` 會被配置 `nr_hw_queues` 數量個的 `*blk_mq_tags`。這 `nr_hw_queues` 個 `*blk_mq_tags` 會再通過 [`blk_mq_alloc_set_map_and_rqs`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L4183) 去配置出空間並初始化。
大致的呼叫流程會是 [`blk_mq_alloc_set_map_and_rqs`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L4183) -> [` __blk_mq_alloc_rq_maps`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L4146) -> [`__blk_mq_alloc_map_and_rqs`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L3634) -> [`blk_mq_alloc_map_and_rqs`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L3614),然後包含兩個重要的步驟
1. -> [` blk_mq_alloc_rq_map`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L3158): 配置並初始每個 `*blk_mq_tags`
2. -> [` blk_mq_alloc_rqs`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L3209) 分配出 `set->queue_depth` 個 `stuct request` 結構到 `*blk_mq_tags` 對應的 `struct blk_mq_tags` 中
:::info
`blk_mq_alloc_set_map_and_rqs` 整個函式實際上相當複雜,以上的呼叫流程只是其中重要的部分,並沒有涵蓋完整的細節
:::
```cpp
mutex_init(&set->tag_list_lock);
INIT_LIST_HEAD(&set->tag_list);
return 0;
```
最後,初始化 `set->tag_list` 這個 linked list,其用來描述使用此 set 的 request queue。並且為確保對該 list 的操作之正確性,初始化使用該 linked list 時需要的 mutex lock。
### `blk_mq_init_queue`
[`blk_mq_init_queue`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L3916),主要的實作是透過 [`blk_mq_init_queue_data`](#blk_mq_init_queue_data),但不附加額外的 `queuedata`。
```cpp
struct request_queue *blk_mq_init_queue(struct blk_mq_tag_set *set)
{
return blk_mq_init_queue_data(set, NULL);
}
```
### `blk_mq_init_queue_data`
```cpp
static struct request_queue *blk_mq_init_queue_data(struct blk_mq_tag_set *set,
void *queuedata)
{
struct request_queue *q;
int ret;
q = blk_alloc_queue(set->numa_node, set->flags & BLK_MQ_F_BLOCKING);
if (!q)
return ERR_PTR(-ENOMEM);
q->queuedata = queuedata;
ret = blk_mq_init_allocated_queue(set, q);
if (ret) {
blk_put_queue(q);
return ERR_PTR(ret);
}
return q;
}
```
[`blk_mq_init_queue_data`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L3898) 主要又分成兩個函式:
* [`blk_alloc_queue`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-core.c#L377): 配置出 `struct request_queue` 並進行基本的初始化
* 其中 `struct request_queue` 的 timer 會被設置,該 timer 會週期性的呼叫 [`blk_rq_timed_out_timer`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-core.c#L366) 這個 callback,後者則會啟動 `q->timeout_work`
* [`blk_mq_init_allocated_queue`](#blk_mq_init_allocated_queue): 將該 `request_queue` 與 `blk_mq_tag_set` 關聯
### `blk_mq_init_allocated_queue`
```cpp
int blk_mq_init_allocated_queue(struct blk_mq_tag_set *set,
struct request_queue *q)
{
WARN_ON_ONCE(blk_queue_has_srcu(q) !=
!!(set->flags & BLK_MQ_F_BLOCKING));
/* mark the queue as mq asap */
q->mq_ops = set->ops;
q->poll_cb = blk_stat_alloc_callback(blk_mq_poll_stats_fn,
blk_mq_poll_stats_bkt,
BLK_MQ_POLL_STATS_BKTS, q);
if (!q->poll_cb)
goto err_exit;
if (blk_mq_alloc_ctxs(q))
goto err_poll;
```
[`blk_mq_init_allocated_queue`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L4068) 真正建立起 blk-mq 框架:
* `q->mq_ops`: `request_queue` 中也具有 `blk_mq_ops`
* `q->poll_cb`: 建立出 `blk_stat_callback`,可以用來統計 block device request 相關的數據
* [`blk_mq_alloc_ctxs`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L3842): 建立出與 Software Staging Queue 相關的 [`blk_mq_ctx`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.h#L18) 結構,每個 CPU 會擁有一個 `blk_mq_ctx`
```cpp
/* init q->mq_kobj and sw queues' kobjects */
blk_mq_sysfs_init(q);
INIT_LIST_HEAD(&q->unused_hctx_list);
spin_lock_init(&q->unused_hctx_lock);
xa_init(&q->hctx_table);
blk_mq_realloc_hw_ctxs(set, q);
if (!q->nr_hw_queues)
goto err_hctxs;
```
* [`blk_mq_sysfs_init`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq-sysfs.c#L223): kobj 相關的初始化(?)
* `unused_hctx_list`: 初始化 list 以及關聯的互斥鎖
* `xa_init`: 初始化 `q->hctx_table` 這個 [`xarray`](https://docs.kernel.org/core-api/xarray.html)
* [`blk_mq_realloc_hw_ctxs`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L4014): 建立出與 Hardware Dispatch Queue 相關的 [`blk_mq_hw_ctx`](https://elixir.bootlin.com/linux/v6.0-rc1/source/include/linux/blk-mq.h#L283) 結構,該數量決定於 `set->nr_hw_queues`
* 每個 `blk_mq_hw_ctx` 的 `tag` 成員會被設置成 `set->tags[hctx_idx]`,即之前透過 [`blk_mq_alloc_tag_set_tags`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L4273) 得到的 `*blk_mq_tags`
```cpp
INIT_WORK(&q->timeout_work, blk_mq_timeout_work);
blk_queue_rq_timeout(q, set->timeout ? set->timeout : 30 * HZ);
```
* `INIT_WORK`: 初始化 `q->timeout_work`,之前有提到其會在 timer 時間到時觸發
* `blk_queue_rq_timeout`: 設置 `sturct request_queue` 中對應的 timeout 成員
```cpp
q->tag_set = set;
q->queue_flags |= QUEUE_FLAG_MQ_DEFAULT;
blk_mq_update_poll_flag(q);
INIT_DELAYED_WORK(&q->requeue_work, blk_mq_requeue_work);
INIT_LIST_HEAD(&q->requeue_list);
spin_lock_init(&q->requeue_lock);
q->nr_requests = set->queue_depth;
/*
* Default to classic polling
*/
q->poll_nsec = BLK_MQ_POLL_CLASSIC;
```
* `sturct request_queue` 中某些成員(`tag_set`/`queue_flags`/`nr_requests`/`poll_nsec`)會用預設值或 `tag_set` 中的設定做初始化
* 初始 `q->requeue_list`,也要初始化對應的鎖 `q->requeue_lock`
* `INIT_DELAYED_WORK`: 初始化 `q->requeue_work`(?)
```cpp
blk_mq_init_cpu_queues(q, set->nr_hw_queues);
blk_mq_add_queue_tag_set(set, q);
blk_mq_map_swqueue(q);
return 0;
```
* [`blk_mq_init_cpu_queues`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L3584): 把 SW queue 一一對應到各個 CPU,
* [`blk_mq_add_queue_tag_set`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L3820): 把 `tag_set` 加入到 `sturct request_queue` 的 linked list ([`tag_set_list`](https://elixir.bootlin.com/linux/v6.0-rc1/source/include/linux/blkdev.h#L535))結構下
:::danger
和直接存取 [`tag_set`](https://elixir.bootlin.com/linux/v6.0-rc1/source/include/linux/blkdev.h#L534) 有何不同? 其他的 `tag_set` 在何時加入 `tag_set_list`?
:::
* [`blk_mq_map_swqueue`](https://elixir.bootlin.com/linux/v6.0-rc1/source/block/blk-mq.c#L3668) 是另一個初始化 request queue 重要的函式,之前我們有提到用 `set->ops->map_queues` 或 `blk_mq_map_queues` 的方式來設置 `map[i].mq_map` 將 HW queue 和 SW queue 進行對應,而 `blk_mq_map_swqueue` 就根據這個對應關係去把 HW/SW queue 的資料結構中對此的描述(`blk_mq_hw_ctx` / `blk_mq_ctx`)建立起來
:::info
小複習一下: HW queue 的數量是在 `blk_mq_alloc_tag_set` 由外部參數決定的,其數量不能超過 CPU 的最大數量。而 SW queue 數量則是 CPU 的最大數量,各個 CPU 獨自擁有一個
:::