Help us understand the problem. What is going on with this article?

Linuxのdirty page関連パラメータからコードを読む

More than 1 year has passed since last update.

はじめに

Linuxの、特にdirty page関連を制御できるパラメータを入り口にして、kernelのコードを読んでみてそれを記録し、意識高そうな雰囲気を醸し出してドヤろうと思ったけど、balance_dirty_pages()には勝てなかったよ・・・という雑記です。

なお、Linux-4.15くらいを見ています。

パラメータの登場人物

みな /proc/sys/vm あたりに生えている。

設定値の注意事項

dirty_background_ratiodirty_background_bytesはどちらか一方だけ設定できる。片方を設定するともう片方が自動でゼロになるようにしている。何らかの方法で(例えばライブRAM書き換え)両方を設定した場合、bytesのほうが優先される。

同様に、dirty_ratiodirty_bytesもどちらか一方だけ設定できる。

dirty_background_ratio

概要

kernelがバックグラウンドでディスクへの書き出しを開始するdirty pageの使用メモリ量のしきい値をパーセントで設定。

設定できるのは0から100までの整数値。デフォルトは10。書くと自動でdirty_background_bytes(下記にある)が0になる。

Documentation

kernel/Documentation/sysctl/vm.txtより、

Documentation/sysctl/vm.txt
dirty_background_ratio

Contains, as a percentage of total available memory that contains free pages
and reclaimable pages, the number of pages at which the background kernel
flusher threads will start writing out dirty data.

The total available memory is not equal to total system memory.

total available memoryとわざわざ強調してる点あたりが気になる。

dirty_background_bytes

概要

kernelがバックグラウンドでディスクへの書き出しを開始するdirty pageの使用メモリ量のしきい値をbyteで設定。

設定できるのは1からunsigned logまでの整数値。デフォルトは0。書くと自動でdirty_background_ratio(上記にある)が0になる。

Documentation

kernel/Documentation/sysctl/vm.txtより、

Documentation/sysctl/vm.txt
dirty_background_bytes

Contains the amount of dirty memory at which the background kernel
flusher threads will start writeback.

Note: dirty_background_bytes is the counterpart of dirty_background_ratio. Only
one of them may be specified at a time. When one sysctl is written it is
immediately taken into account to evaluate the dirty memory limits and the
other appears as 0 when read.

dirty_ratio

概要

writeしてるプロセス自身でディスクへの書き出しを開始するdirty pageの使用メモリ量のしきい値をパーセントで設定。

設定できるのは0から100までの整数値。デフォルトは20。書くと自動でdirty_bytes(下記にある)が0になる。

Documentation

kernel/Documentation/sysctl/vm.txtより、

Documentation/sysctl/vm.txt
dirty_ratio

Contains, as a percentage of total available memory that contains free pages
and reclaimable pages, the number of pages at which a process which is
generating disk writes will itself start writing out dirty data.

The total available memory is not equal to total system memory.

ここでもtotal available memoryとわざわざ強調してる。一部の日本語サイトでは「フォアグラウンドで書く」と解説していることがあるが、ここにはprocess itselfと書いてあり、ニュアンスが異なる。

dirty_bytes

概要

writeしてるプロセス自身でディスクへの書き出しを開始するdirty pageの使用メモリ量のしきい値をbyteで設定。

設定できるのは2ページからunsigned logまでの整数値。デフォルトは0。書くと自動でdirty_ratio(上記にある)が0になる。x86_64だと1ページが4KBなので、最小値は8192となる。

Documentation

kernel/Documentation/sysctl/vm.txtより、

Documentation/sysctl/vm.txt
dirty_bytes

Contains the amount of dirty memory at which a process generating disk writes
will itself start writeback.

Note: dirty_bytes is the counterpart of dirty_ratio. Only one of them may be
specified at a time. When one sysctl is written it is immediately taken into
account to evaluate the dirty memory limits and the other appears as 0 when
read.

Note: the minimum value allowed for dirty_bytes is two pages (in bytes); any
value lower than this limit will be ignored and the old configuration will be
retained.

dirty_expire_centisecs

概要

dirty pageをディスクへ書き出すまでの遅延時間で単位は10msec。これを超えると、kernelのflusher threadに書き出し対象であると扱われる。

設定できるのは0からunsigned logまでの整数値。デフォルトは3000なので30秒。

Documentation

kernel/Documentation/sysctl/vm.txtより、

Documentation/sysctl/vm.txt
dirty_expire_centisecs

This tunable is used to define when dirty data is old enough to be eligible
for writeout by the kernel flusher threads.  It is expressed in 100'ths
of a second.  Data which has been dirty in-memory for longer than this
interval will be written out next time a flusher thread wakes up.

dirty_writeback_centisecs

概要

kernelのflusher threadは定期的に起きてdirty pageの書き出しを行うが、そのときの周期を設定する。単位は10msec。0にするとdirty pageが自動では書き出されなくなる。

設定できるのは0からunsigned logまでの整数値。デフォルトは500なので5秒。

Documentation

kernel/Documentation/sysctl/vm.txtより、

Documentation/sysctl/vm.txt
dirty_writeback_centisecs

The kernel flusher threads will periodically wake up and write `old' data
out to disk.  This tunable expresses the interval between those wakeups, in
100'ths of a second.

Setting this to zero disables periodic writeback altogether.

ゼロにすると「定期的なディスク書き出しをやめる」あたりが注意点か。

dirtytime_expire_interval

概要

lazytime時にinodeのメタデータ(特にatime)を更新する間隔を秒数で設定する。

設定できるのは0からunsigned logまでの整数値。デフォルトは12*60*60で12時間となる。

0にしても書き込みがスキップされるわけではなく即時書き込みになる模様。なお実装上の問題で2周(==24時間)しないと書かれないこともある模様。

Documentation

ドキュメントに記載がない。lazytime絡みのパラメータで、dirty pageと直接は関係ないので、今回はこのへんまでにしておく。

dirty_background_ratio

パラメータの設定箇所

初期値はハードコードされている。kernel/mm/page-writeback.cより、

kernel/mm/page-writeback.c
/*
 * Start background writeback (via writeback threads) at this percentage
 */
int dirty_background_ratio = 10;

ちなみに、今回紹介している他のパラメータも同じような感じにハードコードされている。

sysctlパラメータとして見せている箇所は、kernel/kernel/sysctl.cより、

kernel/kernel/sysctl.c
    {
        .procname   = "dirty_background_ratio",
        .data       = &dirty_background_ratio,
        .maxlen     = sizeof(dirty_background_ratio),
        .mode       = 0644,
        .proc_handler   = dirty_background_ratio_handler,
        .extra1     = &zero,
        .extra2     = &one_hundred,
    },

dirty_background_ratio_handler()は、kernel/mm/page-writeback.cより、

kernel/mm/page-writeback.c
int dirty_background_ratio_handler(struct ctl_table *table, int write,
        void __user *buffer, size_t *lenp,
        loff_t *ppos)
{
    int ret;

    ret = proc_dointvec_minmax(table, write, buffer, lenp, ppos);
    if (ret == 0 && write)
        dirty_background_bytes = 0;
    return ret;
}

以上より、最小値0、最大値100、書き込んだたらdirty_background_bytesがゼロになることがわかる。

パラメータの使われ方

で、変数のdirty_background_ratioがどこで使われているかを追うと、実質1箇所で、kernel/mm/page-writeback.cdomain_dirty_limits()より、

kernel/mm/page-writeback.c
/**
 * domain_dirty_limits - calculate thresh and bg_thresh for a wb_domain
 * @dtc: dirty_throttle_control of interest
 *
 * Calculate @dtc->thresh and ->bg_thresh considering
 * vm_dirty_{bytes|ratio} and dirty_background_{bytes|ratio}.  The caller
 * must ensure that @dtc->avail is set before calling this function.  The
 * dirty limits will be lifted by 1/4 for PF_LESS_THROTTLE (ie. nfsd) and
 * real-time tasks.
 */
static void domain_dirty_limits(struct dirty_throttle_control *dtc)
{
    const unsigned long available_memory = dtc->avail;
    struct dirty_throttle_control *gdtc = mdtc_gdtc(dtc);
    unsigned long bytes = vm_dirty_bytes;
    unsigned long bg_bytes = dirty_background_bytes;
    /* convert ratios to per-PAGE_SIZE for higher precision */
    unsigned long ratio = (vm_dirty_ratio * PAGE_SIZE) / 100;
    unsigned long bg_ratio = (dirty_background_ratio * PAGE_SIZE) / 100;
    unsigned long thresh;
    unsigned long bg_thresh;
    struct task_struct *tsk;

    /* gdtc is !NULL iff @dtc is for memcg domain */
    if (gdtc) {
        unsigned long global_avail = gdtc->avail;

        /*
         * The byte settings can't be applied directly to memcg
         * domains.  Convert them to ratios by scaling against
         * globally available memory.  As the ratios are in
         * per-PAGE_SIZE, they can be obtained by dividing bytes by
         * number of pages.
         */
        if (bytes)
            ratio = min(DIV_ROUND_UP(bytes, global_avail),
                    PAGE_SIZE);
        if (bg_bytes)
            bg_ratio = min(DIV_ROUND_UP(bg_bytes, global_avail),
                       PAGE_SIZE);
        bytes = bg_bytes = 0;
    }

    if (bytes)
        thresh = DIV_ROUND_UP(bytes, PAGE_SIZE);
    else
        thresh = (ratio * available_memory) / PAGE_SIZE;

    if (bg_bytes)
        bg_thresh = DIV_ROUND_UP(bg_bytes, PAGE_SIZE);
    else
        bg_thresh = (bg_ratio * available_memory) / PAGE_SIZE;

    if (bg_thresh >= thresh)
        bg_thresh = thresh / 2;
    tsk = current;
    if (tsk->flags & PF_LESS_THROTTLE || rt_task(tsk)) {
        bg_thresh += bg_thresh / 4 + global_wb_domain.dirty_limit / 32;
        thresh += thresh / 4 + global_wb_domain.dirty_limit / 32;
    }
    dtc->thresh = thresh;
    dtc->bg_thresh = bg_thresh;

    /* we should eventually report the domain in the TP */
    if (!gdtc)
        trace_global_dirty_state(bg_thresh, thresh);
}

と、登場人物が一気に出てくる。大雑把にやってる内容をかいつまんで書くと、

  1. gdtcがある場合(memcg==メモリをドメイン毎で考える場合)は、ratioをmemcfg毎に計算するようだ。(・・・だけど、byteで指定がある場合にratioに変換する式なんかおかしくない?わざわざここまで読んだ人の誰か解説求む)
  2. byte指定がある場合は、ratio指定よりもbyte指定を優先する
  3. なぜがbg_threshがthreshより大きかったら、bg_threshはthreshの半分とする
  4. PF_LESS_THROTTLEがたったタスクまたはリアルタイムタスク(==rt_task())だったら、設定値の25%と全体量の32分の1が猶予され、ディスク書き出ししにくくなる。PF_LESS_THROTTLEは、loopbackデバイスの中の人とnsfdの中の人のときに立ててるみたい。

となる。結局のところ、domain_dirty_limits()は、memcfg毎に、availを設定して呼ぶとthresh,bg_threshを設定してくれる関数となる。domain_dirty_limits()を使っているのは大きく下記の3箇所となる

  • global_dirty_limits()
  • wb_over_bg_thresh()
  • balance_dirty_pages()

global_dirty_limits()から来る系

下記の系から来るデバッグ出力がまずある。/proc/vmstat, /sys/class/sys/block/sda/bdi/writeback

もう一つが、writeback_set_ratelimit()->global_dirty_limits()->domain_dirty_limits()と呼ばれる系で、これは結局のところ、グローバル変数のratelimit_pagesを再計算したいみたい。具体的には、CPUが増えた場合、メモリが増えた場合、メモリが減った場合、dirty_ratio類が変化した場合、がある模様。

wb_over_bg_thresh()から来る系

wb_over_bg_thresh()は何をやってるのか

まずはwb_over_bg_thresh()が何をやっているのかから。関数名からすると「backgroundな書き出しを開始すべきかどうか」を判断し、trueなら書き出すべき、falseなら書き出さなくて良い、というあたりか。kernel/mm/page-writeback.cより、

kernel/mm/page-writeback.c
/**
 * wb_over_bg_thresh - does @wb need to be written back?
 * @wb: bdi_writeback of interest
 *
 * Determines whether background writeback should keep writing @wb or it's
 * clean enough.  Returns %true if writeback should continue.
 */
bool wb_over_bg_thresh(struct bdi_writeback *wb)
{
    struct dirty_throttle_control gdtc_stor = { GDTC_INIT(wb) };
    struct dirty_throttle_control mdtc_stor = { MDTC_INIT(wb, &gdtc_stor) };
    struct dirty_throttle_control * const gdtc = &gdtc_stor;
    struct dirty_throttle_control * const mdtc = mdtc_valid(&mdtc_stor) ?
                             &mdtc_stor : NULL;

    /*
     * Similar to balance_dirty_pages() but ignores pages being written
     * as we're trying to decide whether to put more under writeback.
     */
    gdtc->avail = global_dirtyable_memory();
    gdtc->dirty = global_node_page_state(NR_FILE_DIRTY) +
              global_node_page_state(NR_UNSTABLE_NFS);
    domain_dirty_limits(gdtc);

    if (gdtc->dirty > gdtc->bg_thresh)
        return true;

    if (wb_stat(wb, WB_RECLAIMABLE) >
        wb_calc_thresh(gdtc->wb, gdtc->bg_thresh))
        return true;

    if (mdtc) {
        unsigned long filepages, headroom, writeback;

        mem_cgroup_wb_stats(wb, &filepages, &headroom, &mdtc->dirty,
                    &writeback);
        mdtc_calc_avail(mdtc, filepages, headroom);
        domain_dirty_limits(mdtc);  /* ditto, ignore writeback */

        if (mdtc->dirty > mdtc->bg_thresh)
            return true;

        if (wb_stat(wb, WB_RECLAIMABLE) >
            wb_calc_thresh(mdtc->wb, mdtc->bg_thresh))
            return true;
    }

    return false;
}

dirtybg_threshとの比較の箇所はわかりやすい。が、続きの、wbごとのWB_RECLAIMABLEwb_calc_thresh()の箇所がわかりにくい。wbは型「struct bdi_writeback」より、bdi(ブロックデバイス毎)のもので、ブロックデバイス毎のしきい値を超えているかどうかのチェックとなる。wb_calc_thresh()がキーポイントになるのでこれはあとで確認することにする。続けて、mdtcがNULLじゃないのはmemcgを考慮する場合で、mdtcがNULLじゃない場合はmemcg毎にも似たような確認をすることになる。

bdi毎の確認をする箇所で使われるwb_calc_thresh()は、kernel/mm/page-writeback.cより、

kernel/mm/page-writeback.c
unsigned long wb_calc_thresh(struct bdi_writeback *wb, unsigned long thresh)
{
    struct dirty_throttle_control gdtc = { GDTC_INIT(wb),
                           .thresh = thresh };
    return __wb_calc_thresh(&gdtc);
}
kernel/mm/page-writeback.c
/**
 * __wb_calc_thresh - @wb's share of dirty throttling threshold
 * @dtc: dirty_throttle_context of interest
 *
 * Returns @wb's dirty limit in pages. The term "dirty" in the context of
 * dirty balancing includes all PG_dirty, PG_writeback and NFS unstable pages.
 *
 * Note that balance_dirty_pages() will only seriously take it as a hard limit
 * when sleeping max_pause per page is not enough to keep the dirty pages under
 * control. For example, when the device is completely stalled due to some error
 * conditions, or when there are 1000 dd tasks writing to a slow 10MB/s USB key.
 * In the other normal situations, it acts more gently by throttling the tasks
 * more (rather than completely block them) when the wb dirty pages go high.
 *
 * It allocates high/low dirty limits to fast/slow devices, in order to prevent
 * - starving fast devices
 * - piling up dirty pages (that will take long time to sync) on slow devices
 *
 * The wb's share of dirty limit will be adapting to its throughput and
 * bounded by the bdi->min_ratio and/or bdi->max_ratio parameters, if set.
 */
static unsigned long __wb_calc_thresh(struct dirty_throttle_control *dtc)
{
    struct wb_domain *dom = dtc_dom(dtc);
    unsigned long thresh = dtc->thresh;
    u64 wb_thresh;
    long numerator, denominator;
    unsigned long wb_min_ratio, wb_max_ratio;

    /*
     * Calculate this BDI's share of the thresh ratio.
     */
    fprop_fraction_percpu(&dom->completions, dtc->wb_completions,
                  &numerator, &denominator);

    wb_thresh = (thresh * (100 - bdi_min_ratio)) / 100;
    wb_thresh *= numerator;
    do_div(wb_thresh, denominator);

    wb_min_max_ratio(dtc->wb, &wb_min_ratio, &wb_max_ratio);

    wb_thresh += (thresh * wb_min_ratio) / 100;
    if (wb_thresh > (thresh * wb_max_ratio) / 100)
        wb_thresh = thresh * wb_max_ratio / 100;

    return wb_thresh;
}

bdi毎のratioで調整されたり調整戻しされたり、正直わかりにくすぎる。結局のところ、コメントに書かれた例のように、スループット特性の異なるデバイスをうまく扱えるようにする調整をしたいだけっぽい?kernel/Documentation/ABI/testing/sysfs-class-bdiを見るのがよさそう。

wb_over_bg_thresh()はどこから呼ばれるか

そんなwb_over_bg_thresh()がどう使われるのかと言うと、
- wb_workfn()->wb_do_writeback()->wb_check_background_flush()->wb_over_bg_thresh()
の周辺で多重に呼ばれていてたくさんのpathがありややこしい。ただ、結局はwb_writeback()を呼ぶかどうかの判断に使われることになる。そのwb_writeback()も何通りかの呼び方があるようだ。

  • ファイルなどに書こうとしたときに毎回非同期kworker経由でkickされる系で、しきい値を超えてたらBackgroundディスク書き込みを開始(wb_check_background_flush())、for_background=1 でcallする
  • 時間切れ(dirty_expire_centisecs)していたらディスク書き込みを開始(wb_check_background_flush())、for_kupdate=1 でcallする
  • その他(struct wb_writeback_workで要求をそれぞれごとに特徴づけるようだが、wb_writeback()を経由しない系もあるようで、いまいち読みきれない)

そのwb_do_writeback()はなにをやっているかというと、ちょっと長いけど大事なところなので全部載せることにして、kernel/fs/fs-writeback.cより、

kernel/mm/page-writeback.c
/*
 * Explicit flushing or periodic writeback of "old" data.
 *
 * Define "old": the first time one of an inode's pages is dirtied, we mark the
 * dirtying-time in the inode's address_space.  So this periodic writeback code
 * just walks the superblock inode list, writing back any inodes which are
 * older than a specific point in time.
 *
 * Try to run once per dirty_writeback_interval.  But if a writeback event
 * takes longer than a dirty_writeback_interval interval, then leave a
 * one-second gap.
 *
 * older_than_this takes precedence over nr_to_write.  So we'll only write back
 * all dirty pages if they are all attached to "old" mappings.
 */
static long wb_writeback(struct bdi_writeback *wb,
             struct wb_writeback_work *work)
{
    unsigned long wb_start = jiffies;
    long nr_pages = work->nr_pages;
    unsigned long oldest_jif;
    struct inode *inode;
    long progress;
    struct blk_plug plug;

    oldest_jif = jiffies;
    work->older_than_this = &oldest_jif;

    blk_start_plug(&plug);
    spin_lock(&wb->list_lock);
    for (;;) {
        /*
         * Stop writeback when nr_pages has been consumed
         */
        if (work->nr_pages <= 0)
            break;

        /*
         * Background writeout and kupdate-style writeback may
         * run forever. Stop them if there is other work to do
         * so that e.g. sync can proceed. They'll be restarted
         * after the other works are all done.
         */
        if ((work->for_background || work->for_kupdate) &&
            !list_empty(&wb->work_list))
            break;

        /*
         * For background writeout, stop when we are below the
         * background dirty threshold
         */
        if (work->for_background && !wb_over_bg_thresh(wb))
            break;

        /*
         * Kupdate and background works are special and we want to
         * include all inodes that need writing. Livelock avoidance is
         * handled by these works yielding to any other work so we are
         * safe.
         */
        if (work->for_kupdate) {
            oldest_jif = jiffies -
                msecs_to_jiffies(dirty_expire_interval * 10);
        } else if (work->for_background)
            oldest_jif = jiffies;

        trace_writeback_start(wb, work);
        if (list_empty(&wb->b_io))
            queue_io(wb, work);
        if (work->sb)
            progress = writeback_sb_inodes(work->sb, wb, work);
        else
            progress = __writeback_inodes_wb(wb, work);
        trace_writeback_written(wb, work);

        wb_update_bandwidth(wb, wb_start);

        /*
         * Did we write something? Try for more
         *
         * Dirty inodes are moved to b_io for writeback in batches.
         * The completion of the current batch does not necessarily
         * mean the overall work is done. So we keep looping as long
         * as made some progress on cleaning pages or inodes.
         */
        if (progress)
            continue;
        /*
         * No more inodes for IO, bail
         */
        if (list_empty(&wb->b_more_io))
            break;
        /*
         * Nothing written. Wait for some inode to
         * become available for writeback. Otherwise
         * we'll just busyloop.
         */
        trace_writeback_wait(wb, work);
        inode = wb_inode(wb->b_more_io.prev);
        spin_lock(&inode->i_lock);
        spin_unlock(&wb->list_lock);
        /* This function drops i_lock... */
        inode_sleep_on_writeback(inode);
        spin_lock(&wb->list_lock);
    }
    spin_unlock(&wb->list_lock);
    blk_finish_plug(&plug);

    return nr_pages - work->nr_pages;
}

wb_writeback()->queue_io()->move_expired_inodes()の系にて、older_than_thisとの時間比較を行い、書き出すinodeを判別し、b_dirtyのリストからb_ioのリストへ移し替えし、__writeback_inodes_wb()もしくはwriteback_sb_inodes()b_ioのリストの処理をしている。実際に書いているのは、writeback_sb_inodes()の中でwbcを調整した後の__writeback_single_inode()の呼び出しの模様。

メジャーなのは、work->for_kupdate=1の場合と、work->for_background=1の場合なので、それ以外はいったん忘れて読んでいいのかな?ただ本当に忘れていいかどうかはよくわからない。

ちなみに、wb_workfn()を実行するkworkerスレッドの名前は、kernel/fs/fs-writeback.cより、

fs/fs-writeback.c
/*
 * Handle writeback of dirty data for the device backed by this bdi. Also
 * reschedules periodically and does kupdated style flushing.
 */
void wb_workfn(struct work_struct *work)
{
    struct bdi_writeback *wb = container_of(to_delayed_work(work),
                        struct bdi_writeback, dwork);
    long pages_written;

    set_worker_desc("flush-%s", dev_name(wb->bdi->dev));
    current->flags |= PF_SWAPWRITE;

となっていて、kworkerのスレッドのflush-8:0などの名前になる。8がmajor番号で0がminor番号だが、8は/dev/sd[a-z]+のよく見るscsiディスクもので、minorはここの層では0しか出てこないと思われる。特に日本語のページで「pdflushのkernelスレッドが」と書かれたページがたくさん見つかるが、pdflushというスレッドは少なくとも今は存在しない。

balance_dirty_pages()から来る系

ここ、やばい。厳密にはbalance_dirty_pages_ratelimited()->balance_dirty_pages()と呼ばれる系だけど、いつこれが呼ばれるべきか、呼ばれると何をしているか、読んでもあまり理解できなかった。ratelimitの意味を理解しないといけないけど、どこにも何も書いてくれてない。コメント含めて載せるだけにして許してもらうことにしよう・・・kernel/mm/page-writeback.cより、

kernel/mm/page-writeback.c
/**
 * balance_dirty_pages_ratelimited - balance dirty memory state
 * @mapping: address_space which was dirtied
 *
 * Processes which are dirtying memory should call in here once for each page
 * which was newly dirtied.  The function will periodically check the system's
 * dirty state and will initiate writeback if needed.
 *
 * On really big machines, get_writeback_state is expensive, so try to avoid
 * calling it too often (ratelimiting).  But once we're over the dirty memory
 * limit we decrease the ratelimiting by a lot, to prevent individual processes
 * from overshooting the limit by (ratelimit_pages) each.
 */
void balance_dirty_pages_ratelimited(struct address_space *mapping)
{
    struct inode *inode = mapping->host;
    struct backing_dev_info *bdi = inode_to_bdi(inode);
    struct bdi_writeback *wb = NULL;
    int ratelimit;
    int *p;

    if (!bdi_cap_account_dirty(bdi))
        return;

    if (inode_cgwb_enabled(inode))
        wb = wb_get_create_current(bdi, GFP_KERNEL);
    if (!wb)
        wb = &bdi->wb;

    ratelimit = current->nr_dirtied_pause;
    if (wb->dirty_exceeded)
        ratelimit = min(ratelimit, 32 >> (PAGE_SHIFT - 10));

    preempt_disable();
    /*
     * This prevents one CPU to accumulate too many dirtied pages without
     * calling into balance_dirty_pages(), which can happen when there are
     * 1000+ tasks, all of them start dirtying pages at exactly the same
     * time, hence all honoured too large initial task->nr_dirtied_pause.
     */
    p =  this_cpu_ptr(&bdp_ratelimits);
    if (unlikely(current->nr_dirtied >= ratelimit))
        *p = 0;
    else if (unlikely(*p >= ratelimit_pages)) {
        *p = 0;
        ratelimit = 0;
    }
    /*
     * Pick up the dirtied pages by the exited tasks. This avoids lots of
     * short-lived tasks (eg. gcc invocations in a kernel build) escaping
     * the dirty throttling and livelock other long-run dirtiers.
     */
    p = this_cpu_ptr(&dirty_throttle_leaks);
    if (*p > 0 && current->nr_dirtied < ratelimit) {
        unsigned long nr_pages_dirtied;
        nr_pages_dirtied = min(*p, ratelimit - current->nr_dirtied);
        *p -= nr_pages_dirtied;
        current->nr_dirtied += nr_pages_dirtied;
    }
    preempt_enable();

    if (unlikely(current->nr_dirtied >= ratelimit))
        balance_dirty_pages(wb, current->nr_dirtied);

    wb_put(wb);
}
EXPORT_SYMBOL(balance_dirty_pages_ratelimited);

dirty_ratio

パラメータの設定箇所

kernel/kernel/sysctl.cより、

kernel/kernel/sysctl.c
    {
        .procname   = "dirty_ratio",
        .data       = &vm_dirty_ratio,
        .maxlen     = sizeof(vm_dirty_ratio),
        .mode       = 0644,
        .proc_handler   = dirty_ratio_handler,
        .extra1     = &zero,
        .extra2     = &one_hundred,
    },

dirty_ratio_handler()は、kernel/mm/page-writeback.cより、

kernel/mm/page-writeback.c
int dirty_ratio_handler(struct ctl_table *table, int write,
        void __user *buffer, size_t *lenp,
        loff_t *ppos)
{
    int old_ratio = vm_dirty_ratio;
    int ret;

    ret = proc_dointvec_minmax(table, write, buffer, lenp, ppos);
    if (ret == 0 && write && vm_dirty_ratio != old_ratio) {
        writeback_set_ratelimit();
        vm_dirty_bytes = 0;
    }
    return ret;
}

0から100までの値を設定でき、また値が更新されたときにwriteback_set_ratelimit()を呼び(先程「やばい」で飛ばしたratelimit関連)、さらにvm_dirty_bytes(dirty_bytesのパラメータ)をゼロにしている。

パラメータの使われ方(domain_dirty_limits()の系)

vm_dirty_ratioがどこで使われるかというと、2箇所あり、1つ目がdomain_dirty_limits()となる。ここはdirty_background_ratioの箇所でも出てきた関数で、結局の所「やばい」と書いたbalance_dirty_pages()で参照されることになる。

パラメータの使われ方(node_dirty_limit()の系)

vm_dirty_ratioの使われている箇所の2つ目が、node_dirty_limit()kernel/mm/page-writeback.cより、

kernel/mm/page-writeback.c
/**
 * node_dirty_limit - maximum number of dirty pages allowed in a node
 * @pgdat: the node
 *
 * Returns the maximum number of dirty pages allowed in a node, based
 * on the node's dirtyable memory.
 */
static unsigned long node_dirty_limit(struct pglist_data *pgdat)
{
    unsigned long node_memory = node_dirtyable_memory(pgdat);
    struct task_struct *tsk = current;
    unsigned long dirty;

    if (vm_dirty_bytes)
        dirty = DIV_ROUND_UP(vm_dirty_bytes, PAGE_SIZE) *
            node_memory / global_dirtyable_memory();
    else
        dirty = vm_dirty_ratio * node_memory / 100;

    if (tsk->flags & PF_LESS_THROTTLE || rt_task(tsk))
        dirty += dirty / 4;

    return dirty;
}

/**
 * node_dirty_ok - tells whether a node is within its dirty limits
 * @pgdat: the node to check
 *
 * Returns %true when the dirty pages in @pgdat are within the node's
 * dirty limit, %false if the limit is exceeded.
 */
bool node_dirty_ok(struct pglist_data *pgdat)
{
    unsigned long limit = node_dirty_limit(pgdat);
    unsigned long nr_pages = 0;

    nr_pages += node_page_state(pgdat, NR_FILE_DIRTY);
    nr_pages += node_page_state(pgdat, NR_UNSTABLE_NFS);
    nr_pages += node_page_state(pgdat, NR_WRITEBACK);

    return nr_pages <= limit;
}

と、memcgのnodeごとにlimitを超えていないかどうかのチェックに使われる。PF_LESS_THROTTLEまたはrt_task()の場合は25%増しになっている。そんなnode_dirty_ok()がどこで使われているかと言うと、get_page_from_freelist()の1箇所だけで、kernel/mm/page_alloc.cより、

kernel/mm/page_alloc.c
/*
 * get_page_from_freelist goes through the zonelist trying to allocate
 * a page.
 */
static struct page *
get_page_from_freelist(gfp_t gfp_mask, unsigned int order, int alloc_flags,
                        const struct alloc_context *ac)
{
    struct zoneref *z = ac->preferred_zoneref;
    struct zone *zone;
    struct pglist_data *last_pgdat_dirty_limit = NULL;

    /*
     * Scan zonelist, looking for a zone with enough free.
     * See also __cpuset_node_allowed() comment in kernel/cpuset.c.
     */
    for_next_zone_zonelist_nodemask(zone, z, ac->zonelist, ac->high_zoneidx,
                                ac->nodemask) {
        struct page *page;
        unsigned long mark;

        if (cpusets_enabled() &&
            (alloc_flags & ALLOC_CPUSET) &&
            !__cpuset_zone_allowed(zone, gfp_mask))
                continue;
        /*
         * When allocating a page cache page for writing, we
         * want to get it from a node that is within its dirty
         * limit, such that no single node holds more than its
         * proportional share of globally allowed dirty pages.
         * The dirty limits take into account the node's
         * lowmem reserves and high watermark so that kswapd
         * should be able to balance it without having to
         * write pages from its LRU list.
         *
         * XXX: For now, allow allocations to potentially
         * exceed the per-node dirty limit in the slowpath
         * (spread_dirty_pages unset) before going into reclaim,
         * which is important when on a NUMA setup the allowed
         * nodes are together not big enough to reach the
         * global limit.  The proper fix for these situations
         * will require awareness of nodes in the
         * dirty-throttling and the flusher threads.
         */
        if (ac->spread_dirty_pages) {
            if (last_pgdat_dirty_limit == zone->zone_pgdat)
                continue;

            if (!node_dirty_ok(zone->zone_pgdat)) {
                last_pgdat_dirty_limit = zone->zone_pgdat;
                continue;
            }
        }

memcgを考慮して、特定のnodeからメモリを確保したかったけど、そのnodeはdirty pageだらけで、別のとこから取ったほうが良かったよ、というような判定に使われる。ただ、コメントのXXXにあるように、「dirtyを追い出してでもこのnodeからメモリを確保」した方がいいかどうかという問題があるけど、そのチェックをやるには今はコストが大きいのでやめとく、という感じみたい。

dirty_expire_centisecs

パラメータの設定箇所

kernel/kernel/sysctl.cより、

kernel/kernel/sysctl.c
    {
        .procname   = "dirty_expire_centisecs",
        .data       = &dirty_expire_interval,
        .maxlen     = sizeof(dirty_expire_interval),
        .mode       = 0644,
        .proc_handler   = proc_dointvec_minmax,
        .extra1     = &zero,
    },

0以上の値を設定できるとわかる。ちなみに、このすぐ下、

    {
        .procname   = "dirtytime_expire_seconds",
        .data       = &dirtytime_expire_interval,
        .maxlen     = sizeof(dirty_expire_interval),
        .mode       = 0644,
        .proc_handler   = dirtytime_interval_handler,
        .extra1     = &zero,
    },

sizeof(dirty_expire_interval)が間違ってますね。ただ、dirtytime_expire_intervaldirty_expire_intervalも、どちらもunsigned intなので、実害はない模様。

パラメータの使われ方

dirty_expire_intervalが使われている箇所は、wb_writebackno()の1箇所だけで、、kernel/fs/fs-writeback.cより、

kernel/fs/fs-writeback.c
        /*
         * Kupdate and background works are special and we want to
         * include all inodes that need writing. Livelock avoidance is
         * handled by these works yielding to any other work so we are
         * safe.
         */
        if (work->for_kupdate) {
            oldest_jif = jiffies -
                msecs_to_jiffies(dirty_expire_interval * 10);
        } else if (work->for_background)
            oldest_jif = jiffies;

・・・うん、dirty_background_ratioのときにも出てきたとこですね。work->for_kupdate=1の場合に使われ、書き出し対象のinodeをフィルタする時間の計算に用いられる。ものすごくシンプルでした。

dirty_writeback_centisecs

パラメータの設定箇所

kernel/kernel/sysctl.cより、

kernel/kernel/sysctl.c
    {
        .procname   = "dirty_writeback_centisecs",
        .data       = &dirty_writeback_interval,
        .maxlen     = sizeof(dirty_writeback_interval),
        .mode       = 0644,
        .proc_handler   = dirty_writeback_centisecs_handler,
    },

dirty_writeback_centisecs_handler()は、kernel/mm/page-writeback.cより、

kernel/mm/page-writeback.c
/*
 * sysctl handler for /proc/sys/vm/dirty_writeback_centisecs
 */
int dirty_writeback_centisecs_handler(struct ctl_table *table, int write,
    void __user *buffer, size_t *length, loff_t *ppos)
{
    unsigned int old_interval = dirty_writeback_interval;
    int ret;

    ret = proc_dointvec(table, write, buffer, length, ppos);

    /*
     * Writing 0 to dirty_writeback_interval will disable periodic writeback
     * and a different non-zero value will wakeup the writeback threads.
     * wb_wakeup_delayed() would be more appropriate, but it's a pain to
     * iterate over all bdis and wbs.
     * The reason we do this is to make the change take effect immediately.
     */
    if (!ret && write && dirty_writeback_interval &&
        dirty_writeback_interval != old_interval)
        wakeup_flusher_threads(WB_REASON_PERIODIC);

    return ret;
}

dirty_writeback_intervalunsigned intなのにもかかわらず、proc_dointvec()を使っている影響で、procfsを介したユーザランドからはマイナスの値の読み書きができるように見えつつ、内部ではsigned int <-> unsigned intな変換が起こる。紛らわしい。

wakeup_flusher_threads()の呼び出しは、素直に、wb_workfn()を非同期(kworker)で呼び出すということでよさそう。

パラメータの使われ方(Filesystem固有部分)

dirty_writeback_intervalは比較的たくさんの箇所で使われている。が、半分以上は、Filesystem固有部となっている。これを順に見ていく。

まずはreiserfsから。kernel/fs/reiserfs/super.c

kernel/fs/reiserfs/super.c
void reiserfs_schedule_old_flush(struct super_block *s)
{
    struct reiserfs_sb_info *sbi = REISERFS_SB(s);
    unsigned long delay;

    /*
     * Avoid scheduling flush when sb is being shut down. It can race
     * with journal shutdown and free still queued delayed work.
     */
    if (sb_rdonly(s) || !(s->s_flags & SB_ACTIVE))
        return;

    spin_lock(&sbi->old_work_lock);
    if (!sbi->work_queued) {
        delay = msecs_to_jiffies(dirty_writeback_interval * 10);
        queue_delayed_work(system_long_wq, &sbi->old_work, delay);
        sbi->work_queued = 1;
    }
    spin_unlock(&sbi->old_work_lock);
}

結局のところ、inodeに直接は結びつかないjournal関連を定期的に処理したいようだ。次に移り、ubifsで、kernel/fs/ubifs/io.cより、

kernel/fs/ubifs/io.c
/**
 * new_wbuf_timer - start new write-buffer timer.
 * @wbuf: write-buffer descriptor
 */
static void new_wbuf_timer_nolock(struct ubifs_wbuf *wbuf)
{
    ktime_t softlimit = ms_to_ktime(dirty_writeback_interval * 10);
    unsigned long long delta = dirty_writeback_interval;

    /* centi to milli, milli to nano, then 10% */
    delta *= 10ULL * NSEC_PER_MSEC / 10ULL;

    ubifs_assert(!hrtimer_active(&wbuf->timer));
    ubifs_assert(delta <= ULONG_MAX);

    if (wbuf->no_timer)
        return;
    dbg_io("set timer for jhead %s, %llu-%llu millisecs",
           dbg_jhead(wbuf->jhead),
           div_u64(ktime_to_ns(softlimit), USEC_PER_SEC),
           div_u64(ktime_to_ns(softlimit) + delta, USEC_PER_SEC));
    hrtimer_start_range_ns(&wbuf->timer, softlimit, delta,
                   HRTIMER_MODE_REL);
}

ubifs用のBackGroundThread(c->bgt)を制御したい模様。ubi上のubifsだから、inodeのpage cache機構と必ずしも相性が良いとは言えないから、なのだろうか。次へいって、ufskernel/fs/ufs/super.cより、

kernel/fs/ufs/super.c
void ufs_mark_sb_dirty(struct super_block *sb)
{
    struct ufs_sb_info *sbi = UFS_SB(sb);
    unsigned long delay;

    spin_lock(&sbi->work_lock);
    if (!sbi->work_queued) {
        delay = msecs_to_jiffies(dirty_writeback_interval * 10);
        queue_delayed_work(system_long_wq, &sbi->sync_work, delay);
        sbi->work_queued = 1;
    }
    spin_unlock(&sbi->work_lock);
}

journalないし特殊なことも必要ないはずなのに、ufsが独自にタイマー設けてたりするの理由がいまいちわからない。まぁおいといて、affskernel/fs/affs/super.cより、

kernel/fs/affs/super.c
void affs_mark_sb_dirty(struct super_block *sb)
{
    struct affs_sb_info *sbi = AFFS_SB(sb);
    unsigned long delay;

    if (sb_rdonly(sb))
           return;

    spin_lock(&sbi->work_lock);
    if (!sbi->work_queued) {
           delay = msecs_to_jiffies(dirty_writeback_interval * 10);
           queue_delayed_work(system_long_wq, &sbi->sb_work, delay);
           sbi->work_queued = 1;
    }
    spin_unlock(&sbi->work_lock);
}

ここもufsと同じく、本当にこれが必要なのかよくわからない。mark_sb_dirty()でほぼ同じ構造だから、とりあえず大味にやっつけた最初の実装があって、他のFilesystemがそれを順に真似ていった、とかそんなところじゃないだろうか。hfskernel/fs/hfs/super.cより、

kernel/fs/hfs/super.c
void hfs_mark_mdb_dirty(struct super_block *sb)
{
    struct hfs_sb_info *sbi = HFS_SB(sb);
    unsigned long delay;

    if (sb_rdonly(sb))
        return;

    spin_lock(&sbi->work_lock);
    if (!sbi->work_queued) {
        delay = msecs_to_jiffies(dirty_writeback_interval * 10);
        queue_delayed_work(system_long_wq, &sbi->mdb_work, delay);
        sbi->work_queued = 1;
    }
    spin_unlock(&sbi->work_lock);
}

全く同じだからもうコメントもなしで。jffs2kernel/fs/jffs2/wbuf.cより、

kernel/fs/jffs2/wbuf.c
void jffs2_dirty_trigger(struct jffs2_sb_info *c)
{
    struct super_block *sb = OFNI_BS_2SFFJ(c);
    unsigned long delay;

    if (sb_rdonly(sb))
        return;

    delay = msecs_to_jiffies(dirty_writeback_interval * 10);
    if (queue_delayed_work(system_long_wq, &c->wbuf_dwork, delay))
        jffs2_dbg(1, "%s()\n", __func__);
}

mtd上に作られるFilesystemなので、こちうは少なからずGarbageCollectなどをやっている模様。hfspluskernel/fs/hfsplus/super.cより、

kernel/fs/hfsplus/super.c
void hfsplus_mark_mdb_dirty(struct super_block *sb)
{
    struct hfsplus_sb_info *sbi = HFSPLUS_SB(sb);
    unsigned long delay;

    if (sb_rdonly(sb))
        return;

    spin_lock(&sbi->work_lock);
    if (!sbi->work_queued) {
        delay = msecs_to_jiffies(dirty_writeback_interval * 10);
        queue_delayed_work(system_long_wq, &sbi->sync_work, delay);
        sbi->work_queued = 1;
    }
    spin_unlock(&sbi->work_lock);
}

またおんなじだった。Filesystem固有部分は以上となる。Filesystem固有部分以外は、大きく3箇所ある。

パラメータの使われ方(wb_check_old_data_flush()から来る系)

1箇所目が、wb_check_old_data_flush()で、kernel/fs/fs-writeback.cより、

kernel/fs/fs-writeback.c
static long wb_check_old_data_flush(struct bdi_writeback *wb)
{
    unsigned long expired;
    long nr_pages;

    /*
     * When set to zero, disable periodic writeback
     */
    if (!dirty_writeback_interval)
        return 0;

    expired = wb->last_old_flush +
            msecs_to_jiffies(dirty_writeback_interval * 10);
    if (time_before(jiffies, expired))
        return 0;

    wb->last_old_flush = jiffies;
    nr_pages = get_nr_dirty_pages();

    if (nr_pages) {
        struct wb_writeback_work work = {
            .nr_pages   = nr_pages,
            .sync_mode  = WB_SYNC_NONE,
            .for_kupdate    = 1,
            .range_cyclic   = 1,
            .reason     = WB_REASON_PERIODIC,
        };

        return wb_writeback(wb, &work);
    }

    return 0;
}

過剰にwb_writeback()を呼ばないように保護する程度に使っていて、それ以上の意味はなさそう。ここは、wb_workfn()->wb_do_writeback()->wb_check_old_data_flush()という系で呼ばれるので、頻繁に来る可能性があるので、コストの高いdirty inode探索を事前に低コストで省いておきたい意図があると思われる。

パラメータの使われ方(wb_workfn()から来る系)

2箇所目がwb_workfn()で、kernel/fs/fs-writeback.cより、

kernel/fs/fs-writeback.c
    if (!list_empty(&wb->work_list))
        mod_delayed_work(bdi_wq, &wb->dwork, 0);
    else if (wb_has_dirty_io(wb) && dirty_writeback_interval)
        wb_wakeup_delayed(wb);

mod_delayed_work()は、属性変更して非同期実行する(modはmodifyの意味)、wb_wakeup_delayed()はあとで確認する。これにより、このコードは、前者は「書き出せ」と指示されたものがまだ残っていたら非同期でもう一度wb_workfn()を呼ぶ、後者がdirty inodeがまだあったら少ししてからもう一度wb_workfn()を呼ぶ、という感じか。wb_has_dirty_io()は、kernel/include/linux/backing-dev.hより、

kernel/include/linux/backing-dev.h
static inline bool wb_has_dirty_io(struct bdi_writeback *wb)
{
    return test_bit(WB_has_dirty_io, &wb->state);
}

WB_has_dirty_ioが立っているかどうかを確認するinline関数。WB_has_dirty_ioは、kernel/include/linux/backing-dev-defs.hより、

include/linux/backing-dev-defs.h
/*
 * Bits in bdi_writeback.state
 */
enum wb_state {
    WB_registered,      /* bdi_register() was done */
    WB_shutting_down,   /* wb_shutdown() in progress */
    WB_writeback_running,   /* Writeback is in progress */
    WB_has_dirty_io,    /* Dirty inodes on ->b_{dirty|io|more_io} */
    WB_start_all,       /* nr_pages == 0 (all) work pending */
};

で、広い意味でdirty inodeがあるかどうかを表す。

後回しにしていたwb_wakeup_delayed()は、kernel/mm/backing-dev.cより、

kernel/mm/backing-dev.c
/*
 * This function is used when the first inode for this wb is marked dirty. It
 * wakes-up the corresponding bdi thread which should then take care of the
 * periodic background write-out of dirty inodes. Since the write-out would
 * starts only 'dirty_writeback_interval' centisecs from now anyway, we just
 * set up a timer which wakes the bdi thread up later.
 *
 * Note, we wouldn't bother setting up the timer, but this function is on the
 * fast-path (used by '__mark_inode_dirty()'), so we save few context switches
 * by delaying the wake-up.
 *
 * We have to be careful not to postpone flush work if it is scheduled for
 * earlier. Thus we use queue_delayed_work().
 */
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);
}

期せずdirty_writeback_intervalを使っている3箇所目が登場してしまった。dirtyなinodeを作ったときに、periodicにディスク書き出しをチェックさせるために、タイマーつけて非同期実行させるという感じか。

dirty_writeback_intervalはいろいろ散らばってしまって入るけど、結局のところ、dirty inodeの期限付き書き出しを行うかどうかと、それのチェック間隔にだけ寄与しているようだ。ただ、Filesystem固有部の実装からもわかるように、0にしたところで必ずしも期限付き書き出しが行われなくなるわけではなく、逆に、即時書き出しになってしまうFilesystemもあるように読める。

その他雑多な話

cgroupとの関係

kernel/Documentation/cgroup-v2.txtに書かれてるんだけど、cgroupとdirty page cacheのownershipの問題がややこしいみたい。

kernel/Documentation/cgroup-v2.txt
Writeback

Page cache is dirtied through buffered writes and shared mmaps and
written asynchronously to the backing filesystem by the writeback
mechanism.  Writeback sits between the memory and IO domains and
regulates the proportion of dirty memory by balancing dirtying and
write IOs.

The io controller, in conjunction with the memory controller,
implements control of page cache writeback IOs.  The memory controller
defines the memory domain that dirty memory ratio is calculated and
maintained for and the io controller defines the io domain which
writes out dirty pages for the memory domain.  Both system-wide and
per-cgroup dirty memory states are examined and the more restrictive
of the two is enforced.

cgroup writeback requires explicit support from the underlying
filesystem.  Currently, cgroup writeback is implemented on ext2, ext4
and btrfs.  On other filesystems, all writeback IOs are attributed to
the root cgroup.

There are inherent differences in memory and writeback management
which affects how cgroup ownership is tracked.  Memory is tracked per
page while writeback per inode.  For the purpose of writeback, an
inode is assigned to a cgroup and all IO requests to write dirty pages
from the inode are attributed to that cgroup.

As cgroup ownership for memory is tracked per page, there can be pages
which are associated with different cgroups than the one the inode is
associated with.  These are called foreign pages.  The writeback
constantly keeps track of foreign pages and, if a particular foreign
cgroup becomes the majority over a certain period of time, switches
the ownership of the inode to that cgroup.

While this model is enough for most use cases where a given inode is
mostly dirtied by a single cgroup even when the main writing cgroup
changes over time, use cases where multiple cgroups write to a single
inode simultaneously are not supported well.  In such circumstances, a
significant portion of IOs are likely to be attributed incorrectly.
As memory controller assigns page ownership on the first use and
doesn't update it until the page is released, even if writeback
strictly follows page ownership, multiple cgroups dirtying overlapping
areas wouldn't work as expected.  It's recommended to avoid such usage
patterns.

The sysctl knobs which affect writeback behavior are applied to cgroup
writeback as follows.

  vm.dirty_background_ratio, vm.dirty_ratio
    These ratios apply the same to cgroup writeback with the
    amount of available memory capped by limits imposed by the
    memory controller and system-wide clean memory.

  vm.dirty_background_bytes, vm.dirty_bytes
    For cgroup writeback, this is calculated into ratio against
    total available memory and applied the same way as
    vm.dirty[_background]_ratio.

foreign pages問題、ownership問題、特に複数のcgroup mainから同じpageをdirtyにする場合というのを、性能を語る上では気にしたほうが良い模様。

available memoryの意味

Documentationでも強調されていたavailableについては、例えばkernel/mm/page-writeback.cbalance_dirty_pages()より、

kernel/mm/page-writeback.c
        /*
         * Unstable writes are a feature of certain networked
         * filesystems (i.e. NFS) in which data may have been
         * written to the server's write cache, but has not yet
         * been flushed to permanent storage.
         */
        nr_reclaimable = global_node_page_state(NR_FILE_DIRTY) +
                    global_node_page_state(NR_UNSTABLE_NFS);
        gdtc->avail = global_dirtyable_memory();
        gdtc->dirty = nr_reclaimable + global_node_page_state(NR_WRITEBACK);

        domain_dirty_limits(gdtc);

と、domain_dirty_limits)()を呼ぶ前のavailglobal_dirtyable_memory()の値を入れている。global_dirtyable_memory()は、kernel/mm/page-writeback.cより、

mm/page-writeback.c
/**
 * global_dirtyable_memory - number of globally dirtyable pages
 *
 * Returns the global number of pages potentially available for dirty
 * page cache.  This is the base value for the global dirty limits.
 */
static unsigned long global_dirtyable_memory(void)
{
    unsigned long x;

    x = global_zone_page_state(NR_FREE_PAGES);
    /*
     * Pages reserved for the kernel should not be considered
     * dirtyable, to prevent a situation where reclaim has to
     * clean pages in order to balance the zones.
     */
    x -= min(x, totalreserve_pages);

    x += global_node_page_state(NR_INACTIVE_FILE);
    x += global_node_page_state(NR_ACTIVE_FILE);

    if (!vm_highmem_is_dirtyable)
        x -= highmem_dirtyable_memory(x);

    return x + 1;   /* Ensure that we never return 0 */
}

と、「free - reserve + inactive + active - highmem(使えない場合)」となっている。reserveとhighmemがキーになるのかな。vm_highmem_is_dirtyableは、ただのflag的なsysctlパラメータになっていて、kernel/Documentation/sysctl/vm.txtより、

kernel/Documentation/sysctl/vm.txt
highmem_is_dirtyable

Available only for systems with CONFIG_HIGHMEM enabled (32b systems).

This parameter controls whether the high memory is considered for dirty
writers throttling.  This is not the case by default which means that
only the amount of memory directly visible/usable by the kernel can
be dirtied. As a result, on systems with a large amount of memory and
lowmem basically depleted writers might be throttled too early and
streaming writes can get very slow.

Changing the value to non zero would allow more memory to be dirtied
and thus allow writers to write more data which can be flushed to the
storage more effectively. Note this also comes with a risk of pre-mature
OOM killer because some writers (e.g. direct block device writes) can
only use the low memory and they can fill it up with dirty data without
any throttling.

と、HIGHMEMがpage cacheに使えるか(特にdirty pageに使えるか)どうかを気にして設定せよということになっている。x86_64な世界ではもはや気にする必要がないと思うけど、組み込みなどでよくあるメモリを特殊化して特定用途にガメるようなやつでメモリサイズ計算を間違えているとハマることになるかと思う(何も考えていないと、dirty page以前に、page outで同じ問題を踏むことになる) 例えば、iommuに絡むION HEAPの話ION HEAPとは
なども参考に。

その他

(急いで書いたので見落とした点があるかもしれない、そんなのをあとで追記するかもしれない)

あとがき

dirty pageがらみのパラメータは、英語でも日本語でも比較的解説されている箇所で、しかもその意味はおおむね正しく理解されているようだ。メモリとストレージという、性能と障害時のデータ損失に直結する箇所でもあるので、まぁいい加減なシステム監視なんてやってられないという事情も関係してるんだろうなぁと思う。

今回、balance_dirty_pages_ratelimited()->balance_dirty_pages()の系が結局よくわからなかったので、ここだけ特出しで徹底的に解説するような記事を書いてくれる運命の人を待っています。

参考サイト

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした