device가 초당 처리할 수 있는 양을 알아도 task마다 얼마를 허용할지는 남습니다. writer가 하나라면 bandwidth를 거의 모두 사용할 수 있지만 여러 task가 동시에 쓰면 나누어야 합니다. 그런데 순간적으로 몇 task가 실제 dirtying을 하는지 정확히 세는 일은 쉽지 않습니다.
wb_update_dirty_ratelimit()는 writer 수를 별도의 정확한 counter로 세는 대신 관측한 dirty rate와 직전 task rate에서 균형점을 추정합니다. 이 추정과 position feedback이 함께 작동해야 dirty량이 목표 부근에 머물 수 있습니다.
raw dirty rate는 wb 전체의 속도입니다
upstream v6.18 구현은 현재 WB_DIRTIED와 이전 dirtied_stamp의 차이를 경과 시간으로 나누어 dirty rate를 구합니다. HZ를 곱하는 것은 jiffies 기반 시간을 초당 page 단위로 바꾸기 위해서입니다.
이 값은 한 task의 속도가 아니라 해당 wb에 대한 dirtying의 합입니다. 반면 dirty_ratelimit는 같은 wb의 task들에 사용할 base rate입니다. task에 실제 적용하려는 rate에는 position ratio를 곱합니다. 단위가 같다고 집계 범위까지 같은 것은 아닙니다. v6.18 wb_update_dirty_ratelimit
rate 추정에서는 task rate에 1을 더하여 극히 작은 값에서 다시 올라올 수 있게 하는 처리가 있습니다. 나중에 pause를 결정할 때의 task rate 계산과 이 추정 중간값이 완전히 동일하다고 외워서는 안 됩니다. 0으로 보정되어 pause가 필요해지는 경로도 남아 있습니다.
관측값에서 writer 수를 소거합니다
활동 중인 task 수를 N, 각 task에 적용한 rate를 T, wb 전체의 dirty rate를 D, 완료 bandwidth를 W라고 하겠습니다. 비슷한 rate로 제어되는 정상 상태에서는 D가 대략 N 곱하기 T에 가까워집니다.
다음은 구현코드가 아닌 추정 관계입니다. 실제 구현은 정수 연산, 분모 보정, 상한과 smoothing을 포함합니다.
D ~= N * T
balanced_rate ~= T * W / D
balanced_rate ~= W / N
예를 들어 완료 bandwidth가 12000 pages/s이고 관측 dirty rate가 18000 pages/s, 적용한 task rate가 3000 pages/s라면 계산 모형의 새 balanced rate는 2000 pages/s입니다. N을 직접 세지 않고도 현재 생산 속도가 소비 능력보다 높은 비율을 반영합니다. 이 숫자는 설명을 위한 계산이며 실측값이 아닙니다.
v6.18은 추정 balanced rate를 write bandwidth보다 크지 않게 제한합니다. writer가 갑자기 사라지거나 활동 pattern이 바뀐 짧은 구간에서 단순한 역산이 과도한 값으로 튀는 것을 줄이는 조건입니다.
position ratio를 빼면 잘못된 균형에 머물 수 있습니다
완료 rate와 dirty rate의 비율만으로 base rate를 갱신하면 충분해 보일 수 있습니다. 하지만 실제 task rate에는 이미 position ratio가 곱해집니다.
예를 들어 base rate가 필요한 균등 몫의 두 배이고 position ratio가 0.5라면 실제 생산량은 우연히 완료량과 맞을 수 있습니다. W와 D만 비교하면 같다고 판단하여 base rate를 유지합니다. 그 상태에서는 position ratio가 1로 돌아오지 않아 dirty 목표 위치의 feedback이 계속 보정 상태에 머무를 수 있습니다.
그래서 balanced rate 추정에는 base rate 자체가 아니라 position ratio를 적용한 task rate를 사용합니다. rate의 균형과 dirty 위치의 균형을 독립적인 두 숫자로 고정하지 않고, 서로 일관된 목표로 수렴하도록 만드는 선택입니다.
balanced 값으로 즉시 뛰지 않습니다
200ms 정도의 짧은 추정 구간에서는 task 활동과 완료 batch 때문에 raw rate가 흔들릴 수 있습니다. 계산한 balanced_dirty_ratelimit로 곧바로 base rate를 바꾸면 application은 pause 길이가 들쭉날쭉한 형태로 그 변동을 느낄 수 있습니다.
구현은 dirty량이 setpoint보다 낮은지 높은지를 보고 변경 방향을 제한합니다. 낮을 때에는 이전 balanced 값, 새 balanced 값, task rate의 보수적인 쪽이 상승을 뒷받침할 때 올립니다. 높을 때에는 반대 방향의 조건을 사용합니다. rate 목표만 좋아지고 dirty 위치는 나빠지는 갱신을 피하려는 것입니다.
step 크기도 줄입니다. base rate에 비해 step이 작을수록 추적 속도를 늦추고, 목표에 가까워졌을 때 작은 통계 변동을 계속 따라가지 않게 합니다. 결과 base rate는 최소 1로 저장하지만 raw balanced 추정치는 별도로 남깁니다. 두 필드가 있는 이유는 같은 값을 다른 이름으로 저장하려는 것이 아닙니다.
dirty가 setpoint보다 낮으면 min3(old_balanced, new_balanced, task_rate)가 현재 base rate보다 클 때만 상승 step을 만듭니다. 그 외에는 max3(...)가 현재 base rate보다 작을 때만 하강 step을 만듭니다. 이어 shift = base / (2 * step + 1)을 구하고 shift가 word 크기 미만일 때 DIV_ROUND_UP(step >> shift, 8)로 줄입니다. word 크기 이상이면 step은 0입니다. 정수 shift 범위를 지키면서 목표 근처의 작은 흔들림을 추적하지 않도록 하는 조건입니다.
strictlimit 경로에서는 global dirty 위치가 아니라 wb 자체의 dirty량과 setpoint를 이 방향 판단에 사용합니다. cgroup writeback이 있다면 해당 domain을 반영한 control state도 참여합니다. 이 글의 계산 모형만으로 특정 task의 pause를 그대로 재현할 수는 없습니다.
아래는 v6.18 wb_update_dirty_ratelimit()의 전체 정의입니다. 앞의 추정 관계, 실패하기 쉬운 순수 rate feedback의 반례, strictlimit 선택과 최종 저장까지 한 scope 안에서 확인할 수 있습니다. source의 dirty_rate | 1은 0뿐 아니라 짝수 분모도 바꾸는 정수 보정입니다. 결과를 정확한 실수 나눗셈으로 취급하지 않습니다. 이 함수는 호출 경로의 wb list_lock 아래에서 갱신하며 kernel 소스 문맥이 필요한 함수이지 단독 측정 도구는 아닙니다. v6.18 complete wb_update_dirty_ratelimit
wb_update_dirty_ratelimit_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_dirty_ratelimit(struct dirty_throttle_control *dtc,
unsigned long dirtied,
unsigned long elapsed)
{
struct bdi_writeback *wb = dtc->wb;
unsigned long dirty = dtc->dirty;
unsigned long freerun = dirty_freerun_ceiling(dtc->thresh, dtc->bg_thresh);
unsigned long limit = hard_dirty_limit(dtc_dom(dtc), dtc->thresh);
unsigned long setpoint = (freerun + limit) / 2;
unsigned long write_bw = wb->avg_write_bandwidth;
unsigned long dirty_ratelimit = wb->dirty_ratelimit;
unsigned long dirty_rate;
unsigned long task_ratelimit;
unsigned long balanced_dirty_ratelimit;
unsigned long step;
unsigned long x;
unsigned long shift;
/*
* The dirty rate will match the writeout rate in long term, except
* when dirty pages are truncated by userspace or re-dirtied by FS.
*/
dirty_rate = (dirtied - wb->dirtied_stamp) * HZ / elapsed;
/*
* task_ratelimit reflects each dd's dirty rate for the past 200ms.
*/
task_ratelimit = (u64)dirty_ratelimit *
dtc->pos_ratio >> RATELIMIT_CALC_SHIFT;
task_ratelimit++; /* it helps rampup dirty_ratelimit from tiny values */
/*
* A linear estimation of the "balanced" throttle rate. The theory is,
* if there are N dd tasks, each throttled at task_ratelimit, the wb's
* dirty_rate will be measured to be (N * task_ratelimit). So the below
* formula will yield the balanced rate limit (write_bw / N).
*
* Note that the expanded form is not a pure rate feedback:
* rate_(i+1) = rate_(i) * (write_bw / dirty_rate) (1)
* but also takes pos_ratio into account:
* rate_(i+1) = rate_(i) * (write_bw / dirty_rate) * pos_ratio (2)
*
* (1) is not realistic because pos_ratio also takes part in balancing
* the dirty rate. Consider the state
* pos_ratio = 0.5 (3)
* rate = 2 * (write_bw / N) (4)
* If (1) is used, it will stuck in that state! Because each dd will
* be throttled at
* task_ratelimit = pos_ratio * rate = (write_bw / N) (5)
* yielding
* dirty_rate = N * task_ratelimit = write_bw (6)
* put (6) into (1) we get
* rate_(i+1) = rate_(i) (7)
*
* So we end up using (2) to always keep
* rate_(i+1) ~= (write_bw / N) (8)
* regardless of the value of pos_ratio. As long as (8) is satisfied,
* pos_ratio is able to drive itself to 1.0, which is not only where
* the dirty count meet the setpoint, but also where the slope of
* pos_ratio is most flat and hence task_ratelimit is least fluctuated.
*/
balanced_dirty_ratelimit = div_u64((u64)task_ratelimit * write_bw,
dirty_rate | 1);
/*
* balanced_dirty_ratelimit ~= (write_bw / N) <= write_bw
*/
if (unlikely(balanced_dirty_ratelimit > write_bw))
balanced_dirty_ratelimit = write_bw;
/*
* We could safely do this and return immediately:
*
* wb->dirty_ratelimit = balanced_dirty_ratelimit;
*
* However to get a more stable dirty_ratelimit, the below elaborated
* code makes use of task_ratelimit to filter out singular points and
* limit the step size.
*
* The below code essentially only uses the relative value of
*
* task_ratelimit - dirty_ratelimit
* = (pos_ratio - 1) * dirty_ratelimit
*
* which reflects the direction and size of dirty position error.
*/
/*
* dirty_ratelimit will follow balanced_dirty_ratelimit iff
* task_ratelimit is on the same side of dirty_ratelimit, too.
* For example, when
* - dirty_ratelimit > balanced_dirty_ratelimit
* - dirty_ratelimit > task_ratelimit (dirty pages are above setpoint)
* lowering dirty_ratelimit will help meet both the position and rate
* control targets. Otherwise, don't update dirty_ratelimit if it will
* only help meet the rate target. After all, what the users ultimately
* feel and care are stable dirty rate and small position error.
*
* |task_ratelimit - dirty_ratelimit| is used to limit the step size
* and filter out the singular points of balanced_dirty_ratelimit. Which
* keeps jumping around randomly and can even leap far away at times
* due to the small 200ms estimation period of dirty_rate (we want to
* keep that period small to reduce time lags).
*/
step = 0;
/*
* For strictlimit case, calculations above were based on wb counters
* and limits (starting from pos_ratio = wb_position_ratio() and up to
* balanced_dirty_ratelimit = task_ratelimit * write_bw / dirty_rate).
* Hence, to calculate "step" properly, we have to use wb_dirty as
* "dirty" and wb_setpoint as "setpoint".
*/
if (unlikely(wb->bdi->capabilities & BDI_CAP_STRICTLIMIT)) {
dirty = dtc->wb_dirty;
setpoint = (dtc->wb_thresh + dtc->wb_bg_thresh) / 2;
}
if (dirty < setpoint) {
x = min3(wb->balanced_dirty_ratelimit,
balanced_dirty_ratelimit, task_ratelimit);
if (dirty_ratelimit < x)
step = x - dirty_ratelimit;
} else {
x = max3(wb->balanced_dirty_ratelimit,
balanced_dirty_ratelimit, task_ratelimit);
if (dirty_ratelimit > x)
step = dirty_ratelimit - x;
}
/*
* Don't pursue 100% rate matching. It's impossible since the balanced
* rate itself is constantly fluctuating. So decrease the track speed
* when it gets close to the target. Helps eliminate pointless tremors.
*/
shift = dirty_ratelimit / (2 * step + 1);
if (shift < BITS_PER_LONG)
step = DIV_ROUND_UP(step >> shift, 8);
else
step = 0;
if (dirty_ratelimit < balanced_dirty_ratelimit)
dirty_ratelimit += step;
else
dirty_ratelimit -= step;
WRITE_ONCE(wb->dirty_ratelimit, max(dirty_ratelimit, 1UL));
wb->balanced_dirty_ratelimit = balanced_dirty_ratelimit;
trace_bdi_dirty_ratelimit(wb, dirty_rate, task_ratelimit);
}
task별 허용량은 throughput 예약이 아닙니다
이 제어는 모든 writer에게 동일한 device I/O 성능을 예약해 주는 scheduler가 아닙니다. 이미 dirty인 folio를 반복 수정하는 task와 새 folio를 대량 dirty로 만드는 task는 accounting에서 다르게 보일 수 있습니다. filesystem의 allocation, lock, journal, device queue도 별도의 지연을 만듭니다.
따라서 dirty ratelimit를 application의 고정 byte/s 상한으로 해석해서는 안 됩니다. 목적은 dirty page cache의 생산 속도를 완료 능력과 맞추고 global·wb dirty 위치가 지속적으로 한쪽에 몰리지 않게 하는 것입니다. 관측치에 기반한 추정이며, sleep 길이 계산은 그 다음 단계입니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 10, Dirty Rate Limit.
- Linux v6.18 mm/page-writeback.c:
wb_update_dirty_ratelimit()의 추정, position 결합 및 step 제한 전체 구현.