ブロックデバイスのプラグ/アンプラグ
プラグ/アンプラグとはまさに炭酸飲料を温めて、圧が溜まった頃を見計らって栓を抜いて圧を逃がし、再度栓をして圧が溜まるのを待つ。という設計者の思いが有ったからでないでしょうか?(それにしてもITのネーミングは粋なものが多いいです。)で、実装するには?って事ですが、実装は至って常識的なものでした。
プラグ/アンプラグはデバイスドライバを起動させるかどうかの処理にすぎません。ストレージIO処理を効率に行うには、ある程度IO要求を溜め込んで、それをセクタの順序に並び替え、一気に読み書き込みを行ったほうがいいわけです。
そのため、ブロックデバイスを表すstruct gendisk構造体に要求キューリストが設定され、そのキューに呼び出すセクタが要求としてリストされるわけですが、この要求キューリストにはこの要求を処理するためのコールバック関数もセットされています。
blk_init_queue関数から呼ばれるblk_init_queue_node関数から、blk_alloc_queue_node関数でスラブから要求キューを取得します。この時q->request_fnにストラテジルーチンとして物理デバイスのIO処理を行う関数を設定します。q->unplug_fnはストラテジルーチンを起動するためのアンプラグ関数で、generic_unplug_deviceの共通の関数がコールされるようになっています。なおq->plug_fnに相当するプラグルーチンはありません。プラグ処理はstruct request_queue->queue_flagsの所定のビットをセットするだけで、デバイスに依存しないからだと思います。
blk_queue_make_request関数は要求を要求キューに挿入したり、アンプラグにかかるコールバックの設定を行っています。
ここではBIOから要求キューを作成し、その要求が要求キュー内のリストの最適な位置に配置します。そして物理的にセクタが隣接すると要求をマージしたりいたします。(たぶん)
要求がマージされたらラベルのout:へ、そうでないならget_rq:へとジャンプします。out:以降は同期フラグが設定されていれば、__generic_unplug_device関数をコールしてアンプラグです。マージされない場合、bioから要求を作成し、add_request関数で要求キューリストに投入するわけですが、その前にif (elv_queue_empty(q))で要求キューが空なら、プラグするようになっています。
追記
ブロックデバイス周りはIO処理の効率化ということで、かなり厄介です。
プラグ/アンプラグはデバイスドライバを起動させるかどうかの処理にすぎません。ストレージIO処理を効率に行うには、ある程度IO要求を溜め込んで、それをセクタの順序に並び替え、一気に読み書き込みを行ったほうがいいわけです。
そのため、ブロックデバイスを表すstruct gendisk構造体に要求キューリストが設定され、そのキューに呼び出すセクタが要求としてリストされるわけですが、この要求キューリストにはこの要求を処理するためのコールバック関数もセットされています。
blk_init_queue関数から呼ばれるblk_init_queue_node関数から、blk_alloc_queue_node関数でスラブから要求キューを取得します。この時q->request_fnにストラテジルーチンとして物理デバイスのIO処理を行う関数を設定します。q->unplug_fnはストラテジルーチンを起動するためのアンプラグ関数で、generic_unplug_deviceの共通の関数がコールされるようになっています。なおq->plug_fnに相当するプラグルーチンはありません。プラグ処理はstruct request_queue->queue_flagsの所定のビットをセットするだけで、デバイスに依存しないからだと思います。
blk_queue_make_request関数は要求を要求キューに挿入したり、アンプラグにかかるコールバックの設定を行っています。
struct request_queue * blk_init_queue_node(request_fn_proc *rfn, spinlock_t *lock, int node_id) { struct request_queue *q = blk_alloc_queue_node(GFP_KERNEL, node_id); if (!q) return NULL; q->node = node_id; if (blk_init_free_list(q)) { kmem_cache_free(blk_requestq_cachep, q); return NULL; } if (!lock) lock = &q->__queue_lock; q->request_fn = rfn; q->prep_rq_fn = NULL; q->unplug_fn = generic_unplug_device; q->queue_flags = (1 << QUEUE_FLAG_CLUSTER); q->queue_lock = lock; blk_queue_segment_boundary(q, 0xffffffff); blk_queue_make_request(q, __make_request); blk_queue_max_segment_size(q, MAX_SEGMENT_SIZE); blk_queue_max_hw_segments(q, MAX_HW_SEGMENTS); blk_queue_max_phys_segments(q, MAX_PHYS_SEGMENTS); q->sg_reserved_size = INT_MAX; blk_set_cmd_filter_defaults(&q->cmd_filter); if (!elevator_init(q, NULL)) { blk_queue_congestion_threshold(q); return q; } blk_put_queue(q); return NULL; }blk_queue_make_request関数は、要求キュー関連の初期化の処理を行っています。q->unplug_threshは要求キューに要求がたまった時、プラグ関数を呼ぶための値です。q->unplug_delay/q->unplug_work/q->unplug_timer.functionは最初の要求が要求キューに投入される時、q->unplug_delay(3ms)間アンプラグされなかった時、タイムアウトとしてq->unplug_timer.functionが起動され、そこからワークスレッドとしてq->unplug_workが起動し、アンプラグするようになっています。
void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn) { q->nr_requests = BLKDEV_MAX_RQ; blk_queue_max_phys_segments(q, MAX_PHYS_SEGMENTS); blk_queue_max_hw_segments(q, MAX_HW_SEGMENTS); q->make_request_fn = mfn; q->backing_dev_info.ra_pages = (VM_MAX_READAHEAD * 1024) / PAGE_CACHE_SIZE; q->backing_dev_info.state = 0; q->backing_dev_info.capabilities = BDI_CAP_MAP_COPY; blk_queue_max_sectors(q, SAFE_MAX_SECTORS); blk_queue_hardsect_size(q, 512); blk_queue_dma_alignment(q, 511); blk_queue_congestion_threshold(q); q->nr_batching = BLK_BATCH_REQ; q->unplug_thresh = 4; /* hmm */ q->unplug_delay = (3 * HZ) / 1000; /* 3 milliseconds */ if (q->unplug_delay == 0) q->unplug_delay = 1; INIT_WORK(&q->unplug_work, blk_unplug_work); q->unplug_timer.function = blk_unplug_timeout; q->unplug_timer.data = (unsigned long)q; blk_queue_bounce_limit(q, BLK_BOUNCE_HIGH); }ストラテジルーチンをコールするタイミングはq->unplug_fnすなわちgeneric_unplug_device関数にあります。
void generic_unplug_device(struct request_queue *q) { if (blk_queue_plugged(q)) { spin_lock_irq(q->queue_lock); __generic_unplug_device(q); spin_unlock_irq(q->queue_lock); } } void __generic_unplug_device(struct request_queue *q) { if (unlikely(blk_queue_stopped(q))) return; if (!blk_remove_plug(q)) return; q->request_fn(q); }通常の流れでの(たぶん)プラグ/アンプラグの流れです。__make_requestは要求キューのmake_request_fnに設定されている関数でした。これはBIO構造体を要求キューに投入する祭に呼ばれます。
ここではBIOから要求キューを作成し、その要求が要求キュー内のリストの最適な位置に配置します。そして物理的にセクタが隣接すると要求をマージしたりいたします。(たぶん)
要求がマージされたらラベルのout:へ、そうでないならget_rq:へとジャンプします。out:以降は同期フラグが設定されていれば、__generic_unplug_device関数をコールしてアンプラグです。マージされない場合、bioから要求を作成し、add_request関数で要求キューリストに投入するわけですが、その前にif (elv_queue_empty(q))で要求キューが空なら、プラグするようになっています。
static int __make_request(struct request_queue *q, struct bio *bio) { : : el_ret = elv_merge(q, &req, bio); switch (el_ret) { case ELEVATOR_BACK_MERGE: : : goto out; case ELEVATOR_FRONT_MERGE: : : goto out; default: ; } get_rq: rw_flags = bio_data_dir(bio); if (sync) rw_flags |= REQ_RW_SYNC; req = get_request_wait(q, rw_flags, bio); init_request_from_bio(req, bio); spin_lock_irq(q->queue_lock); if (elv_queue_empty(q)) blk_plug_device(q); add_request(q, req); out: if (sync) __generic_unplug_device(q); spin_unlock_irq(q->queue_lock); return 0; end_io: bio_endio(bio, err); return 0; }add_request関数からelv_insert関数を呼び出し、要求キューに要求を投入して後、要求キューリストにある読/書き込み全要求数が、q->unplug_threshを超えていれば、アンプラグするようになっています。なお、q->in_flightは処理中の要求数ではないかと思います。
void elv_insert(struct request_queue *q, struct request *rq, int where) { : : : if (unplug_it && blk_queue_plugged(q)) { int nrq = q->rq.count[READ] + q->rq.count[WRITE] - q->in_flight; if (nrq >= q->unplug_thresh) __generic_unplug_device(q); } }プラグ処理ではq->queue_flagsにQUEUE_FLAG_PLUGGEDをセットし、q->unplug_timerタイマーをq->unplug_delay(3ms)で起動し、アンプラグ処理ワークスレッドが起動します。
void blk_plug_device(struct request_queue *q) { if (blk_queue_stopped(q)) return; if (!queue_flag_test_and_set(QUEUE_FLAG_PLUGGED, q)) { mod_timer(&q->unplug_timer, jiffies + q->unplug_delay); blk_add_trace_generic(q, NULL, 0, BLK_TA_PLUG); } }
追記
ブロックデバイス周りはIO処理の効率化ということで、かなり厄介です。