# 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 獨自擁有一個 :::