LeavaTailの日記

LeavaTailの日記

Linuxエンジニアを目指した技術者の備忘録

Linuxカーネルのファイルアクセスの処理を追いかける (8) work Queue

関連記事

概要

QEMUの vexpress-a9 (arm) で Linux 5.15を起動させながら、ファイル書き込みのカーネル処理を確認していく。

本章では、writebackワークキューの作成から、ワーカーの追加・取り出しに関係する次の関数を確認した。

  • wb_wakeup_delayed関数
  • wb_wakeup関数
  • wb_queue_work関数

はじめに

ユーザプロセスはファイルシステムという機構によって記憶装置上のデータをファイルという形式で書き込み・読み込みすることができる。
本調査では、ユーザプロセスがファイルに書き込み要求を実行したときにLinuxカーネルではどのような処理が実行されるかを読み解いていく。

調査対象や環境などはPart 1: 環境セットアップを参照。

調査対象(シーケンス図)

本記事では、writebackカーネルスレッドが起床するために用いる機構work Queueについて確認する。

writeback kthreadの概要

メインメモリから記憶装置に書き込む手法は大きく分けて「write back」と「write through」の二通り存在する。

itmanabi.com

PCのようなシステムでは、記憶装置の書き込み上限や速度の観点から「write back」方式を利用することが多い。
ここでは、write back方式におけるデータの書き込み方法を確認する。

wiki.bit-hive.com

write back方式では、ユーザプロセスからファイル書き込みをすると、該当するページキャッシュやinodeキャッシュに対して、Dirtyのフラグを立て処理を終了する。
その後、write back用のカーネルスレッドがDirtyになっているキャッシュの記憶装置への書き込みを実施する。

Linuxでは、主に次のようなタイミングでライトバック処理が実行される。

  • sync(1)fsync(2)が実行されたタイミング
  • ページ回収のタイミング
  • ファイル操作などによる遅延実行のタイミング

writeback用のワーカスレッドの作成

Linux v5.15では、ライトバックを実現するためにWork Queueと呼ばれる機構を用いている。

writebackワークキュー概要

Work Queueでは、指定した処理を指定した時間経過後に呼び出すことのできる仕組みとなっている。(詳細な説明は下記を参照)

www.coins.tsukuba.ac.jp

writeback用のWork Queueは、Linuxカーネルの起動時に下記の関数によって生成される。

// 234:
static int __init default_bdi_init(void)
{
    int err;

    bdi_wq = alloc_workqueue("writeback", WQ_MEM_RECLAIM | WQ_UNBOUND |
                 WQ_SYSFS, 0);
    if (!bdi_wq)
        return -ENOMEM;

    err = bdi_init(&noop_backing_dev_info);

    return err;
}
subsys_initcall(default_bdi_init);

ここで、write back用のWork Queue(bdi_wq)はグローバル変数である。

Work Queueの生成には、alloc_workqueue関数を利用する。

lwn.net

writeback用のWork Queueには下記のフラグを指定している。

  • WQ_MEM_RECLAIM: ページフレームの回収に利用されることがある
  • WQ_UNBOUND: Work Queueを一つのCPUに割り付けない
  • WQ_SYSFS: sysfs (devices/virtual/workqueue/writeback)を生成する

その後、NFSといったblock deviceの実態が存在しないファイルシステムのためにbdi_init関数によって、noop_backing_bdi_initを初期化する。
noop_backing_bdi_initは、backing_dev_info型の変数であり、SDカードなど含めた周辺機器に対する情報を保持する。

default_bdi_init関数で作成されたwriteback用のWork Queueは、下記の3つの関数にて使用される。

  1. wb_wakeup_delayed: dirty_writeback_centisecsで指定されたセンチ秒後に、bdi_writebackに紐づいている関数を実行する。
  2. wb_wakeup: bdi_writebackに紐づいている関数を実行する。
  3. wb_queue_work: Writeback用のキューのリストを末尾に追加して、bdi_writebackに紐づいている関数を実行する。

それぞれの関数は共通して、bdi_writeback型の変数を引数としている。
bdi_writeback型は、それぞれのブロックデバイスにおけるwritebackに関連するパラメータを保持した構造体となっている。 今回の環境では、対象デバイスがSDカードであるのでカーネルがSDカードを認識したタイミング(mmc_rescan)でSDカード用のbdi_writeback型のデータを生成する。

wb_init関数が、**指定時間経過後にwb_workfn関数を呼び出すようにbdi_writebackを初期化する。

// 287:
static int wb_init(struct bdi_writeback *wb, struct backing_dev_info *bdi,
           gfp_t gfp)
{
    int i, err;

    memset(wb, 0, sizeof(*wb));

    if (wb != &bdi->wb)
        bdi_get(bdi);
    wb->bdi = bdi;
    wb->last_old_flush = jiffies;
    INIT_LIST_HEAD(&wb->b_dirty);
    INIT_LIST_HEAD(&wb->b_io);
    INIT_LIST_HEAD(&wb->b_more_io);
    INIT_LIST_HEAD(&wb->b_dirty_time);
    spin_lock_init(&wb->list_lock);

    atomic_set(&wb->writeback_inodes, 0);
    wb->bw_time_stamp = jiffies;
    wb->balanced_dirty_ratelimit = INIT_BW;
    wb->dirty_ratelimit = INIT_BW;
    wb->write_bandwidth = INIT_BW;
    wb->avg_write_bandwidth = INIT_BW;

    spin_lock_init(&wb->work_lock);
    INIT_LIST_HEAD(&wb->work_list);
    INIT_DELAYED_WORK(&wb->dwork, wb_workfn);
    INIT_DELAYED_WORK(&wb->bw_dwork, wb_update_bandwidth_workfn);
    wb->dirty_sleep = jiffies;

    err = fprop_local_init_percpu(&wb->completions, gfp);
    if (err)
        goto out_put_bdi;

    for (i = 0; i < NR_WB_STAT_ITEMS; i++) {
        err = percpu_counter_init(&wb->stat[i], 0, gfp);
        if (err)
            goto out_destroy_stat;
    }

    return 0;

out_destroy_stat:
    while (i--)
        percpu_counter_destroy(&wb->stat[i]);
    fprop_local_destroy_percpu(&wb->completions);
out_put_bdi:
    if (wb != &bdi->wb)
        bdi_put(bdi);
    return err;
}

writebackワークキューにキューを追加

writeback用のWork Queueは以下の3つの関数で使用される。

  • wb_wakeup_delayed
  • wb_wakeup
  • wb_queue_work

wb_wakeup_delayed関数

elixir.bootlin.com

  • wb_workfn: dirty_writeback_intervalが経過した場合に、呼び出す。
  • __mark_inode_dirty: 対象のbdi_writeback型のデータに対して、初めての書き込みの場合のみ呼び出す。

wb_wakeup関数

elixir.bootlin.com

  • inode_switch_wbs_work_fn関数
  • wb_start_writeback関数
    • wakeup_flusher_threads_bdi関数: 下記の関数から呼ばれる
      • laptop_mode_timer_fn関数: Laptop Modeによるタイマのコールバック関数。
    • wakeup_flusher_threads 下記の3つの関数から呼ばれる。
      • ksys_sync関数: syncシステムコールなどから呼ばれる。
      • dirty_writeback_centisecs_handler関数: /proc/sys/vm/dirty_writeback_centisecsを書き換えた場合かつ、経過時間が過ぎている場合に呼ばれる。
      • shrink_inactive_list: 今回は調査を省略。
  • wb_start_background_writeback関数
    • balance_dirty_pages関数: Dirtyの閾値に応じて、カーネルスレッドに (または、現在のプロセス) でwritebackを実施する。
  • wb_workfn関数: 後述するwb_queue_workで追加されたキューがある場合に呼び出す。
  • wakeup_dirtytime_writeback関数: dirty_expire_centisecsミリ秒経過毎に呼び出される。

wb_queue_work関数

elixir.bootlin.com

  • bdi_split_work_to_wbs関数: backing_dev_infoのノードをキューに分割する。
  • cgroup_writeback_by_id関数: 今回は調査を省略。

writebackワークキューからキューを取り出す

ext2ファイルシステムのwrite処理内の__mark_inode_dirtyでは、前述で紹介したwb_wakeup_delayed関数を呼び出す。

leavatail.hatenablog.com

// 263:
void wb_wakeup_delayed(struct bdi_writeback *wb)
{
    unsigned long timeout;

    timeout = msecs_to_jiffies(dirty_writeback_interval * 10);
    spin_lock_bh(&wb->work_lock);
    if (test_bit(WB_registered, &wb->state))
        queue_delayed_work(bdi_wq, &wb->dwork, timeout);
    spin_unlock_bh(&wb->work_lock);
}

おわりに

本記事では、Linux v5.15におけるwriteback用のWorkqueueを解説した。

変更履歴

  • 2022/1/1: 記事公開
  • 2022/09/19: カーネルバージョンを5.15に変更

参考