Writeback bandwidth는 제출량이 아니라 완료량에서 추정합니다

device queue에 data를 많이 넣었다고 그 속도로 저장할 수 있다는 뜻은 아닙니다. dirty 제어에 필요한 값은 실제로 얼마나 빠르게 data가 빠져나가는가입니다. Linux는 writeback 완료량과 시간을 누적하고, 짧은 구간의 변동을 완화하여 bandwidth를 추정합니다.

여기서 누적 counter, 현재 잔량, 초당 rate는 다른 종류의 값입니다. 이 구분 없이 WB_DIRTIED와 WB_RECLAIMABLE을 같은 dirty page 수로 읽으면 rate 계산을 잘못 이해하게 됩니다.

현재 남은 양과 누적 전환을 따로 셉니다

WB_RECLAIMABLE은 writeback을 기다리는 dirty량, WB_WRITEBACK은 진행 중인 writeback량과 관계됩니다. task throttling의 wb dirty 판단에는 보통 둘을 합칩니다. 이미 I/O에 넘겼다고 메모리와 device 부담이 사라지는 것은 아니기 때문입니다.

WB_DIRTIED와 WB_WRITTEN은 누적 전환을 통해 rate를 추정하는 데 쓰입니다. folio가 처음 dirty로 바뀔 때 전자가 증가하고 writeback 완료 시 후자가 증가합니다. 단위는 base page이며 큰 folio 하나는 여러 page로 반영될 수 있습니다. 이는 사용자 byte-level 수정 횟수나 BIO 개수와 다릅니다.

redirty나 거절한 writepage를 보정하는 경로도 있으므로 counter를 무조건 단조 증가하는 단순 application 통계라고 보아서는 안 됩니다. per-CPU counter는 비용을 낮추기 위해 batch 오차를 가질 수 있습니다. 아주 작은 wb threshold에서 이 오차가 제어 판단을 압도하지 않도록 정확한 합산을 선택하는 경로도 있습니다. v6.18 mm/page-writeback.c

같은 timestamp에서 얻은 차이를 사용합니다

__wb_update_bandwidth()는 wb의 list_lock 아래에서 현재 시각과 dirtied·written 값을 읽습니다. 이전 bw_time_stamp와의 차이는 최소 1 jiffy로 만들어 0으로 나누는 일을 피합니다.

갱신이 끝나면 dirtied_stamp, written_stamp, bw_time_stamp를 함께 옮깁니다. 이전 numerator와 새로운 시간 구간을 무관하게 섞지 않아야 합니다. jiffies 차이를 초당 page rate로 바꿀 때에는 HZ가 들어갑니다.

정기 bandwidth 갱신과 task dirty-rate 갱신도 구별됩니다. wb_update_bandwidth()의 주기적인 경로는 bandwidth를 갱신하지만 task의 dirty ratelimit 계산을 무조건 수행하지 않습니다. balancing이 실제 rate 결정을 앞둔 경로에서는 update_ratelimit를 요청하여 관련 domain limit와 dirty-rate 계산도 수행합니다.

아래 전체 정의에서 update_ratelimit 분기 밖의 bandwidth 갱신과 stamp 저장을 구별할 수 있습니다. mdtc는 cgroup domain이 유효할 때만 사용하며, 같은 wb라도 global domain과 memcg domain의 limit 계산은 별개입니다. now는 lock 획득 전에 읽으므로 elapsed를 최소 1로 만드는 방어가 중요합니다. v6.18 bandwidth update

__wb_update_bandwidth_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * mm/page-writeback.c
 *
 * Copyright (C) 2002, Linus Torvalds.
 * Copyright (C) 2007 Red Hat, Inc., Peter Zijlstra
 *
 * Contains functions related to writing back dirty pages at the
 * address_space level.
 *
 * 10Apr2002	Andrew Morton
 *		Initial version
 */

/*
 * Complete scope from Linux v6.18 mm/page-writeback.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/mm/page-writeback.c
 * Kernel excerpt; not a standalone program.
 * The book exact source tag is not asserted.
 */

static void __wb_update_bandwidth(struct dirty_throttle_control *gdtc,
				  struct dirty_throttle_control *mdtc,
				  bool update_ratelimit)
{
	struct bdi_writeback *wb = gdtc->wb;
	unsigned long now = jiffies;
	unsigned long elapsed;
	unsigned long dirtied;
	unsigned long written;

	spin_lock(&wb->list_lock);

	/*
	 * Lockless checks for elapsed time are racy and delayed update after
	 * IO completion doesn't do it at all (to make sure written pages are
	 * accounted reasonably quickly). Make sure elapsed >= 1 to avoid
	 * division errors.
	 */
	elapsed = max(now - wb->bw_time_stamp, 1UL);
	dirtied = percpu_counter_read(&wb->stat[WB_DIRTIED]);
	written = percpu_counter_read(&wb->stat[WB_WRITTEN]);

	if (update_ratelimit) {
		domain_update_dirty_limit(gdtc, now);
		wb_update_dirty_ratelimit(gdtc, dirtied, elapsed);

		/*
		 * @mdtc is always NULL if !CGROUP_WRITEBACK but the
		 * compiler has no way to figure that out.  Help it.
		 */
		if (IS_ENABLED(CONFIG_CGROUP_WRITEBACK) && mdtc) {
			domain_update_dirty_limit(mdtc, now);
			wb_update_dirty_ratelimit(mdtc, dirtied, elapsed);
		}
	}
	wb_update_write_bandwidth(wb, elapsed, written);

	wb->dirtied_stamp = dirtied;
	wb->written_stamp = written;
	WRITE_ONCE(wb->bw_time_stamp, now);
	spin_unlock(&wb->list_lock);
}

wb_update_bandwidth_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * mm/page-writeback.c
 *
 * Copyright (C) 2002, Linus Torvalds.
 * Copyright (C) 2007 Red Hat, Inc., Peter Zijlstra
 *
 * Contains functions related to writing back dirty pages at the
 * address_space level.
 *
 * 10Apr2002	Andrew Morton
 *		Initial version
 */

/*
 * Complete scope from Linux v6.18 mm/page-writeback.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/mm/page-writeback.c
 * Kernel excerpt; not a standalone program.
 * The book exact source tag is not asserted.
 */

void wb_update_bandwidth(struct bdi_writeback *wb)
{
	struct dirty_throttle_control gdtc = { GDTC_INIT(wb) };

	__wb_update_bandwidth(&gdtc, NULL, false);
}

평균 구간은 정확히 고정 3초가 아닙니다

v6.18 wb_update_write_bandwidth()의 period는 roundup_pow_of_two(3 * HZ)입니다. 3초에 해당하는 jiffies를 power of two로 올림하여 shift 기반 계산을 할 수 있게 합니다. 따라서 정확히 최근 3초의 모든 sample을 저장해 산술평균하는 sliding window가 아닙니다.

개념적인 rate는 delta_written * HZ / elapsed입니다. 구현은 현재 written과 이전 stamp의 차이를 underflow 없이 구하고, 이전 bandwidth와 새 완료량을 가중하여 write_bandwidth를 갱신합니다. elapsed가 period보다 크면 오래된 평균을 같은 방식으로 끌고 가지 않고 새 구간의 rate로 다시 잡는 분기가 있습니다.

avg_write_bandwidth에는 추가 smoothing이 적용됩니다. 이전 평균과 직전 bandwidth, 새 추정치의 방향을 비교하여 급격한 spike를 그대로 반영하지 않도록 합니다. 평균은 최소 1로 유지하여 active wb들의 total bandwidth가 0이 되는 계산 문제도 피합니다.

이는 application이 관찰한 fsync latency나 device 사양의 최대 throughput과 같은 측정치가 아닙니다. writeback 제어를 위한 추정치이며, I/O pattern과 완료 timing에 따라 달라집니다.

가중 계산에서 새 완료량에는 HZ를 곱하고 이전 bandwidth에는 period - elapsed를 곱합니다. 둘 다 page 수와 jiffies를 조합한 동일한 중간 단위가 된 뒤 period로 나눕니다. elapsed > period 분기에서도 HZ가 없어지지 않습니다. 전체 코드는 다음과 같습니다. v6.18 wb_update_write_bandwidth

wb_update_write_bandwidth_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * mm/page-writeback.c
 *
 * Copyright (C) 2002, Linus Torvalds.
 * Copyright (C) 2007 Red Hat, Inc., Peter Zijlstra
 *
 * Contains functions related to writing back dirty pages at the
 * address_space level.
 *
 * 10Apr2002	Andrew Morton
 *		Initial version
 */

/*
 * Complete scope from Linux v6.18 mm/page-writeback.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/mm/page-writeback.c
 * Kernel excerpt; not a standalone program.
 * The book exact source tag is not asserted.
 */

static void wb_update_write_bandwidth(struct bdi_writeback *wb,
				      unsigned long elapsed,
				      unsigned long written)
{
	const unsigned long period = roundup_pow_of_two(3 * HZ);
	unsigned long avg = wb->avg_write_bandwidth;
	unsigned long old = wb->write_bandwidth;
	u64 bw;

	/*
	 * bw = written * HZ / elapsed
	 *
	 *                   bw * elapsed + write_bandwidth * (period - elapsed)
	 * write_bandwidth = ---------------------------------------------------
	 *                                          period
	 *
	 * @written may have decreased due to folio_redirty_for_writepage().
	 * Avoid underflowing @bw calculation.
	 */
	bw = written - min(written, wb->written_stamp);
	bw *= HZ;
	if (unlikely(elapsed > period)) {
		bw = div64_ul(bw, elapsed);
		avg = bw;
		goto out;
	}
	bw += (u64)wb->write_bandwidth * (period - elapsed);
	bw >>= ilog2(period);

	/*
	 * one more level of smoothing, for filtering out sudden spikes
	 */
	if (avg > old && old >= (unsigned long)bw)
		avg -= (avg - old) >> 3;

	if (avg < old && old <= (unsigned long)bw)
		avg += (old - avg) >> 3;

out:
	/* keep avg > 0 to guarantee that tot > 0 if there are dirty wbs */
	avg = max(avg, 1LU);
	if (wb_has_dirty_io(wb)) {
		long delta = avg - wb->avg_write_bandwidth;
		WARN_ON_ONCE(atomic_long_add_return(delta,
					&wb->bdi->tot_write_bandwidth) <= 0);
	}
	wb->write_bandwidth = bw;
	WRITE_ONCE(wb->avg_write_bandwidth, avg);
}

마지막 완료 뒤에도 평균 갱신이 필요합니다

writeback을 계속 제출하는 동안에는 do_writepages()가 일정 간격으로 bandwidth 갱신을 유도할 수 있습니다. 그런데 마지막 I/O가 완료된 뒤 새 제출이 없다면, 제출 쪽에서만 갱신하는 설계는 마지막 완료량을 늦게 반영하게 됩니다.

그래서 inode의 마지막 writeback이 끝나는 경로는 bw_dwork를 지연 예약할 수 있습니다. 여러 inode가 비슷한 시각에 끝나면 갱신도 묶을 수 있습니다. v6.18의 BANDWIDTH_INTERVAL은 max(HZ/5, 1)로 약 200ms를 의도하지만 실제 scheduling이 정확한 실시간 주기를 보장하는 것은 아닙니다.

wb_inode_writeback_end()는 inode의 writeback 종료에 따라 wb의 진행 inode 수를 줄입니다. 이어 work_lock을 IRQ-safe하게 잡고 WB_registered가 남아 있을 때만 delayed work를 예약합니다. wb 전체의 inode 수가 0일 때만 예약한다는 조건은 없습니다. callback은 wb를 찾아 wb_update_bandwidth()를 호출하므로 dirty ratelimit를 직접 재계산하지 않습니다. v6.18 completion hook, v6.18 delayed-work callback

wb_inode_writeback_end_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * mm/page-writeback.c
 *
 * Copyright (C) 2002, Linus Torvalds.
 * Copyright (C) 2007 Red Hat, Inc., Peter Zijlstra
 *
 * Contains functions related to writing back dirty pages at the
 * address_space level.
 *
 * 10Apr2002	Andrew Morton
 *		Initial version
 */

/*
 * Complete scope from Linux v6.18 mm/page-writeback.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/mm/page-writeback.c
 * Kernel excerpt; not a standalone program.
 * The book exact source tag is not asserted.
 */

static void wb_inode_writeback_end(struct bdi_writeback *wb)
{
	unsigned long flags;
	atomic_dec(&wb->writeback_inodes);
	/*
	 * Make sure estimate of writeback throughput gets updated after
	 * writeback completed. We delay the update by BANDWIDTH_INTERVAL
	 * (which is the interval other bandwidth updates use for batching) so
	 * that if multiple inodes end writeback at a similar time, they get
	 * batched into one bandwidth update.
	 */
	spin_lock_irqsave(&wb->work_lock, flags);
	if (test_bit(WB_registered, &wb->state))
		queue_delayed_work(bdi_wq, &wb->bw_dwork, BANDWIDTH_INTERVAL);
	spin_unlock_irqrestore(&wb->work_lock, flags);
}

wb_update_bandwidth_workfn_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only

/*
 * Complete scope from Linux v6.18 mm/backing-dev.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/mm/backing-dev.c
 * Kernel excerpt; not a standalone program.
 * The book exact source tag is not asserted.
 */

static void wb_update_bandwidth_workfn(struct work_struct *work)
{
	struct bdi_writeback *wb = container_of(to_delayed_work(work),
						struct bdi_writeback, bw_dwork);

	wb_update_bandwidth(wb);
}

반대로 오랫동안 writeback이 없었던 wb에서 새 I/O를 시작하면 idle 시간까지 분모에 넣어 bandwidth를 과소평가할 수 있습니다. wb_bandwidth_estimate_start()는 충분한 idle 간격과 진행 중 writeback inode가 없다는 조건을 보고 stamp를 새로 잡습니다. 이 함수도 stamp 변경에는 list_lock을 사용합니다. v6.18 estimate start

wb_bandwidth_estimate_start_v6_18.c

// SPDX-License-Identifier: GPL-2.0-only
/*
 * mm/page-writeback.c
 *
 * Copyright (C) 2002, Linus Torvalds.
 * Copyright (C) 2007 Red Hat, Inc., Peter Zijlstra
 *
 * Contains functions related to writing back dirty pages at the
 * address_space level.
 *
 * 10Apr2002	Andrew Morton
 *		Initial version
 */

/*
 * Complete scope from Linux v6.18 mm/page-writeback.c.
 * Source: https://github.com/torvalds/linux/blob/v6.18/mm/page-writeback.c
 * Kernel excerpt; not a standalone program.
 * The book exact source tag is not asserted.
 */

static void wb_bandwidth_estimate_start(struct bdi_writeback *wb)
{
	unsigned long now = jiffies;
	unsigned long elapsed = now - READ_ONCE(wb->bw_time_stamp);

	if (elapsed > WB_BANDWIDTH_IDLE_JIF &&
	    !atomic_read(&wb->writeback_inodes)) {
		spin_lock(&wb->list_lock);
		wb->dirtied_stamp = wb_stat(wb, WB_DIRTIED);
		wb->written_stamp = wb_stat(wb, WB_WRITTEN);
		WRITE_ONCE(wb->bw_time_stamp, now);
		spin_unlock(&wb->list_lock);
	}
}

wb가 dirty I/O 목록에 들어오고 나갈 때에는 BDI의 total average bandwidth에도 기여를 더하거나 뺍니다. 평소 inactive인 wb까지 모두 같은 active 몫으로 세지 않도록 목록 상태와 통계가 연결됩니다.

완료 비율은 dirty budget의 몫에도 쓰입니다

wb_domain은 전체 또는 cgroup domain의 완료 추적을 담습니다. global domain의 completions와 wb의 completion 비율은 해당 wb에 dirty threshold의 어느 정도를 배정할지 계산하는 재료입니다.

이 비율은 부팅 이후의 lifetime byte 수를 단순히 나눈 고정 몫이 아닙니다. 시간에 따라 감쇠되는 proportional tracking이 현재 활동 변화를 따라갈 수 있게 합니다. 빠르게 완료시키는 wb가 더 많은 buffering을 활용하고, 오래 idle이던 wb도 다시 활동할 여유를 얻도록 별도 보정이 붙습니다.

따라서 제어값을 읽을 때에는 현재 잔량, 최근 변화량, smoothing된 rate, domain 안의 상대적인 완료 몫을 나누어 보는 것이 좋습니다. 같은 숫자를 다르게 표현한 필드들이 아니라 각각 다른 시간 범위와 질문에 답하는 상태입니다.

참고 자료

  • Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 10, Dirty Throttle Statistics 및 Bandwidth Updates.
  • Linux v6.18 mm/page-writeback.c: __wb_update_bandwidth(), wb_update_write_bandwidth(), completion과 accounting.
  • Linux v6.18 fs/fs-writeback.c: active dirty 목록과 total bandwidth.