저장 대기 data가 한도에 도달한 순간에만 writer를 멈추면 큰 진동이 생깁니다. 잠깐 빠르게 쓰고, 길게 멈추고, 다시 몰아서 쓰는 패턴입니다. dirty 제어는 한도를 넘지 않는 것뿐 아니라 device를 계속 활용하면서 writer의 속도를 안정시키는 것도 목표로 삼습니다.
이를 위해 global threshold, wb별 몫, freerun ceiling, setpoint를 구별합니다. 단순한 ratio 설정에서 출발하지만 실제 task에 적용하는 보정은 여러 단계입니다. 여기서는 upstream v6.18의 일반 global 경로를 설명하고 strictlimit와 cgroup의 예외도 필요한 지점에서 구분합니다.
threshold는 dirtyable page 수로 바꿉니다
domain_dirty_limits()는 앞서 계산한 dirtyable page 수인 avail을 입력으로 받습니다. byte 설정이 있으면 page 크기로 올림하여 page 수를 구하고, ratio 설정이면 dirtyable memory에 그 비율을 적용합니다. background threshold도 같은 단위로 계산합니다.
RT 또는 deadline task에는 추가 여유를 주는 분기가 있으며, 계산 결과가 내부 표현 범위를 넘지 않도록 제한합니다. background threshold가 dirty threshold 이상이면 전자의 값을 후자의 절반으로 낮춥니다. 사용자 설정 두 개가 어느 값이든 그대로 독립적으로 통과하는 것은 아닙니다. v6.18 domain_dirty_limits
task throttling에 사용하는 dirty량에는 일반적으로 NR_FILE_DIRTY뿐 아니라 NR_WRITEBACK도 들어갑니다. 제출한 data가 아직 메모리·I/O 자원을 차지하기 때문입니다. 이와 달리 새 background I/O가 더 필요한지 보는 경로는 이미 writeback인 양을 제외할 수 있습니다. 같은 이름의 dirty 변수라도 호출 목적을 확인해야 합니다.
계산에 사용하는 dirty_throttle_control은 영속적인 per-task rate 저장소가 아니라 한 번의 계산에 필요한 domain·wb 상태를 전달하는 구조체입니다. avail, dirty, threshold들은 page 단위이고 pos_ratio는 fixed-point 비율입니다. v6.18에서는 선언이 include/linux/writeback.h에 있으며, 아래에는 CONFIG_CGROUP_WRITEBACK 필드도 포함한 전체 선언을 보였습니다. v6.18 control declaration
dirty_throttle_control_v6_18.c
/* SPDX-License-Identifier: GPL-2.0 */
/*
* include/linux/writeback.h
*/
/*
* Complete scope from Linux v6.18 include/linux/writeback.h.
* Source: https://github.com/torvalds/linux/blob/v6.18/include/linux/writeback.h
* Kernel excerpt; not a standalone program.
* The book exact source tag is not asserted.
*/
struct dirty_throttle_control {
#ifdef CONFIG_CGROUP_WRITEBACK
struct wb_domain *dom;
struct dirty_throttle_control *gdtc; /* only set in memcg dtc's */
#endif
struct bdi_writeback *wb;
struct fprop_local_percpu *wb_completions;
unsigned long avail; /* dirtyable */
unsigned long dirty; /* file_dirty + write + nfs */
unsigned long thresh; /* dirty threshold */
unsigned long bg_thresh; /* dirty background threshold */
unsigned long limit; /* hard dirty limit */
unsigned long wb_dirty; /* per-wb counterparts */
unsigned long wb_thresh;
unsigned long wb_bg_thresh;
unsigned long pos_ratio;
bool freerun;
bool dirty_exceeded;
};
global byte 설정은 DIV_ROUND_UP(bytes, PAGE_SIZE)로 변환합니다. ratio는 먼저 PAGE_SIZE 기준 정밀도로 바꾼 뒤 이미 page 단위인 avail에 적용합니다. memcg domain에 global byte 수를 그대로 복제하지는 않습니다. global available page 수로 비율을 구해 그 domain의 available량에 적용합니다. RT·deadline 여유는 각 threshold의 1/4과 global domain dirty limit의 1/32이며, v6.18은 이 가산 뒤 UINT_MAX 제한과 background 절반 보정을 수행합니다. v6.18 domain_dirty_limits 전체 정의
// 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 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;
tsk = current;
if (rt_or_dl_task(tsk)) {
bg_thresh += bg_thresh / 4 + global_wb_domain.dirty_limit / 32;
thresh += thresh / 4 + global_wb_domain.dirty_limit / 32;
}
/*
* Dirty throttling logic assumes the limits in page units fit into
* 32-bits. This gives 16TB dirty limits max which is hopefully enough.
*/
if (thresh > UINT_MAX)
thresh = UINT_MAX;
/* This makes sure bg_thresh is within 32-bits as well */
if (bg_thresh >= thresh)
bg_thresh = thresh / 2;
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);
}
hard limit는 현재 threshold만 보지 않습니다
메모리 사용량이 변하면 ratio로 계산한 threshold가 갑자기 내려갈 수 있습니다. 이미 쌓인 dirty량이 새 한도보다 훨씬 많은 상황에서 모든 제어값을 즉시 급락시키면 긴 stall을 만들 수 있습니다.
v6.18의 hard_dirty_limit()는 현재 threshold와 domain의 dirty_limit 중 큰 값을 사용합니다. domain limit는 상승 시 빠르게 따라가고 하강 시에는 현재 dirty량도 고려하며 서서히 내려가는 보정을 가집니다. 이 때문에 cgroup을 사용하지 않아도 hard limit가 항상 그 순간의 global threshold와 정확히 같지는 않습니다.
이 값은 dirty가 한 byte도 넘을 수 없는 allocation cap도 아닙니다. 통계의 지연, 여러 task의 진행, folio 크기 등의 이유로 제어에는 overshoot가 가능합니다. limit는 feedback과 대기를 결정하는 기준으로 읽어야 합니다.
하강 target은 max(thresh, dirty)이며 limit가 그보다 클 때 차이의 1/32만 줄입니다. limit가 threshold보다 작으면 한 번에 올립니다. domain_update_dirty_limit()는 먼저 lock 없이 갱신 시각을 확인하고, dom->lock 아래에서 다시 확인하여 같은 간격의 중복 갱신을 줄입니다. 이 동작은 global domain에도 적용됩니다. v6.18 domain limit helpers
// 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 unsigned long hard_dirty_limit(struct wb_domain *dom,
unsigned long thresh)
{
return max(thresh, dom->dirty_limit);
}
// 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 update_dirty_limit(struct dirty_throttle_control *dtc)
{
struct wb_domain *dom = dtc_dom(dtc);
unsigned long thresh = dtc->thresh;
unsigned long limit = dom->dirty_limit;
/*
* Follow up in one step.
*/
if (limit < thresh) {
limit = thresh;
goto update;
}
/*
* Follow down slowly. Use the higher one as the target, because thresh
* may drop below dirty. This is exactly the reason to introduce
* dom->dirty_limit which is guaranteed to lie above the dirty pages.
*/
thresh = max(thresh, dtc->dirty);
if (limit > thresh) {
limit -= (limit - thresh) >> 5;
goto update;
}
return;
update:
dom->dirty_limit = limit;
}
domain_update_dirty_limit_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 domain_update_dirty_limit(struct dirty_throttle_control *dtc,
unsigned long now)
{
struct wb_domain *dom = dtc_dom(dtc);
/*
* check locklessly first to optimize away locking for the most time
*/
if (time_before(now, dom->dirty_limit_tstamp + BANDWIDTH_INTERVAL))
return;
spin_lock(&dom->lock);
if (time_after_eq(now, dom->dirty_limit_tstamp + BANDWIDTH_INTERVAL)) {
update_dirty_limit(dtc);
dom->dirty_limit_tstamp = now;
}
spin_unlock(&dom->lock);
}
setpoint 부근에서는 보정을 작게 만듭니다
일반 global 경로의 freerun ceiling은 background와 dirty threshold의 중간입니다. setpoint는 이 freerun과 hard limit의 중간입니다. 설정이 background 10%, dirty 20%이고 hard limit도 그 threshold와 같다는 단순 조건에서는 freerun 15%, setpoint 17.5%라는 관계가 나옵니다. 이는 실제 시스템에서 측정한 점유율이나 모든 wb의 고정 목표는 아닙니다.
pos_ratio_polynom()은 setpoint와 dirty량의 거리를 cubic 형태로 rate 보정에 바꿉니다. 다음은 구현코드가 아닌 계산 모형입니다. kernel 구현에는 fixed-point shift, 정수 나눗셈과 0 분모 방지가 들어갑니다.
x = (setpoint - dirty) / (limit - setpoint)
global_ratio = clamp(1 + x * x * x, 0, 2)
dirty가 setpoint이면 ratio는 1입니다. setpoint보다 낮으면 허용 rate를 키우고, 높으면 낮춥니다. limit에 도달하면 0이 됩니다. cubic을 사용하면 목표 부근에서 변화가 작고 멀어질수록 더 강하게 반응할 수 있습니다.
v6.18은 RATELIMIT_CALC_SHIFT = 10의 fixed-point 표현을 사용합니다. 내부 정수 1024는 이 표현에서 1.0에 해당합니다. 이를 page 수나 byte rate로 바로 읽으면 단위가 맞지 않습니다. v6.18 pos_ratio_polynom
실제 분모는 (limit - setpoint) | 1입니다. 이는 0을 피하는 동시에 짝수 분모도 1만큼 바꾸므로 실수 모형과 bit 단위로 같은 함수는 아닙니다. numerator는 signed 차이로 만들어 dirty가 setpoint보다 높을 때 음수 feedback을 표현합니다. 아래 전체 정의의 세 번 곱셈과 shift가 cubic을 구성합니다.
// 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 long long pos_ratio_polynom(unsigned long setpoint,
unsigned long dirty,
unsigned long limit)
{
long long pos_ratio;
long x;
x = div64_s64(((s64)setpoint - (s64)dirty) << RATELIMIT_CALC_SHIFT,
(limit - setpoint) | 1);
pos_ratio = x;
pos_ratio = pos_ratio * x >> RATELIMIT_CALC_SHIFT;
pos_ratio = pos_ratio * x >> RATELIMIT_CALC_SHIFT;
pos_ratio += 1 << RATELIMIT_CALC_SHIFT;
return clamp(pos_ratio, 0LL, 2LL << RATELIMIT_CALC_SHIFT);
}
wb의 몫은 최근 완료 비율에서 출발합니다
global 한도를 여러 wb에 동일하게 나누면 느린 device가 지나치게 많은 dirty data를 잡아두거나 빠른 device를 충분히 활용하지 못할 수 있습니다. __wb_calc_thresh()는 domain의 완료량에 대한 wb의 상대적인 완료 몫을 사용합니다.
단순화하면 global threshold에 completion fraction을 곱하는 관계이지만 실제 구현에는 BDI의 minimum·maximum ratio가 붙습니다. 다른 BDI에 예약한 minimum 몫을 고려하고, 자신의 minimum을 더하며 maximum을 넘지 않게 합니다.
오래 idle이던 wb는 completion fraction이 아주 작을 수 있습니다. 그 상태에서 처음부터 작은 quota로 강하게 막으면 다시 bandwidth를 보여 줄 기회가 없습니다. v6.18은 domain에 여유가 있으면 그 일부로 wb threshold의 하한을 보완합니다. strictlimit일 때는 이 여유도 더 작게 다룹니다. min·max tunable이 기본값이라고 나머지 보정까지 없어지는 것은 아닙니다.
구체적으로 domain의 thresh > dirty일 때 일반 wb는 여유의 1/8, strictlimit wb는 1/100을 하한 후보로 삼습니다. 이 보완 뒤에도 wb maximum으로 제한합니다. v6.18 min·max ratio의 내부 단위는 단순 0–100 정수만이 아니라 BDI_RATIO_SCALE을 포함하므로 오래된 식의 100을 그대로 대입하면 안 됩니다. v6.18 __wb_calc_thresh
// 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 unsigned long __wb_calc_thresh(struct dirty_throttle_control *dtc,
unsigned long thresh)
{
struct wb_domain *dom = dtc_dom(dtc);
struct bdi_writeback *wb = dtc->wb;
u64 wb_thresh;
u64 wb_max_thresh;
unsigned long numerator, denominator;
unsigned long wb_min_ratio, wb_max_ratio;
/*
* Calculate this wb's share of the thresh ratio.
*/
fprop_fraction_percpu(&dom->completions, dtc->wb_completions,
&numerator, &denominator);
wb_thresh = (thresh * (100 * BDI_RATIO_SCALE - bdi_min_ratio)) / (100 * BDI_RATIO_SCALE);
wb_thresh *= numerator;
wb_thresh = div64_ul(wb_thresh, denominator);
wb_min_max_ratio(wb, &wb_min_ratio, &wb_max_ratio);
wb_thresh += (thresh * wb_min_ratio) / (100 * BDI_RATIO_SCALE);
/*
* It's very possible that wb_thresh is close to 0 not because the
* device is slow, but that it has remained inactive for long time.
* Honour such devices a reasonable good (hopefully IO efficient)
* threshold, so that the occasional writes won't be blocked and active
* writes can rampup the threshold quickly.
*/
if (thresh > dtc->dirty) {
if (unlikely(wb->bdi->capabilities & BDI_CAP_STRICTLIMIT))
wb_thresh = max(wb_thresh, (thresh - dtc->dirty) / 100);
else
wb_thresh = max(wb_thresh, (thresh - dtc->dirty) / 8);
}
wb_max_thresh = thresh * wb_max_ratio / (100 * BDI_RATIO_SCALE);
if (wb_thresh > wb_max_thresh)
wb_thresh = wb_max_thresh;
return wb_thresh;
}
BDI 안에 여러 cgroup wb가 있을 때에는 BDI min·max 몫을 active bandwidth 비율로 나누는 경로도 있습니다. wb_min_max_ratio()의 다음 두 전체 정의는 각각 CONFIG_CGROUP_WRITEBACK 활성·비활성 대안입니다. 둘을 같은 compilation unit에 중복 정의하는 예제가 아닙니다. v6.18 ratio configuration branches
// 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_min_max_ratio(struct bdi_writeback *wb,
unsigned long *minp, unsigned long *maxp)
{
unsigned long this_bw = READ_ONCE(wb->avg_write_bandwidth);
unsigned long tot_bw = atomic_long_read(&wb->bdi->tot_write_bandwidth);
unsigned long long min = wb->bdi->min_ratio;
unsigned long long max = wb->bdi->max_ratio;
/*
* @wb may already be clean by the time control reaches here and
* the total may not include its bw.
*/
if (this_bw < tot_bw) {
if (min) {
min *= this_bw;
min = div64_ul(min, tot_bw);
}
if (max < 100 * BDI_RATIO_SCALE) {
max *= this_bw;
max = div64_ul(max, tot_bw);
}
}
*minp = min;
*maxp = max;
}
wb_min_max_ratio_no_cgroup_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_min_max_ratio(struct bdi_writeback *wb,
unsigned long *minp, unsigned long *maxp)
{
*minp = wb->bdi->min_ratio;
*maxp = wb->bdi->max_ratio;
}
wb_dirty_limits()는 wb의 reclaimable과 writeback을 합치고 background 몫도 계산합니다. threshold가 counter batch 오차에 비해 작으면 빠른 근사값 대신 합산 값을 사용합니다. 오차 때문에 실제보다 dirty가 많다고 판단하여 잘못 막는 문제, 특히 stacked writeback의 의존관계를 줄이려는 조건입니다.
정확한 분기는 wb_thresh < 2 * wb_stat_error()입니다. background 몫은 foreground wb threshold에 domain의 background/foreground 비율을 적용하되 domain threshold가 0이면 0으로 처리합니다. 이는 새 background I/O를 결정할 때 background threshold 자체로 다시 계산하는 별도 helper와 구별됩니다. v6.18 wb_dirty_limits
// 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 inline void wb_dirty_limits(struct dirty_throttle_control *dtc)
{
struct bdi_writeback *wb = dtc->wb;
unsigned long wb_reclaimable;
/*
* wb_thresh is not treated as some limiting factor as
* dirty_thresh, due to reasons
* - in JBOD setup, wb_thresh can fluctuate a lot
* - in a system with HDD and USB key, the USB key may somehow
* go into state (wb_dirty >> wb_thresh) either because
* wb_dirty starts high, or because wb_thresh drops low.
* In this case we don't want to hard throttle the USB key
* dirtiers for 100 seconds until wb_dirty drops under
* wb_thresh. Instead the auxiliary wb control line in
* wb_position_ratio() will let the dirtier task progress
* at some rate <= (write_bw / 2) for bringing down wb_dirty.
*/
dtc->wb_thresh = __wb_calc_thresh(dtc, dtc->thresh);
dtc->wb_bg_thresh = dtc->thresh ?
div_u64((u64)dtc->wb_thresh * dtc->bg_thresh, dtc->thresh) : 0;
/*
* In order to avoid the stacked BDI deadlock we need
* to ensure we accurately count the 'dirty' pages when
* the threshold is low.
*
* Otherwise it would be possible to get thresh+n pages
* reported dirty, even though there are thresh-m pages
* actually dirty; with m+n sitting in the percpu
* deltas.
*/
if (dtc->wb_thresh < 2 * wb_stat_error()) {
wb_reclaimable = wb_stat_sum(wb, WB_RECLAIMABLE);
dtc->wb_dirty = wb_reclaimable + wb_stat_sum(wb, WB_WRITEBACK);
} else {
wb_reclaimable = wb_stat(wb, WB_RECLAIMABLE);
dtc->wb_dirty = wb_reclaimable + wb_stat(wb, WB_WRITEBACK);
}
}
최종 pos_ratio가 언제나 0에서 2인 것은 아닙니다
global polynomial이 0~2로 clamp된다는 사실과 wb_position_ratio()의 최종 출력 범위는 같지 않습니다. 일반 경로는 global setpoint를 wb threshold에 맞춰 축소하고, wb dirty량과 bandwidth를 고려한 보정선을 추가합니다.
wb가 자기 몫보다 과도하게 dirty이면 rate를 더 낮춥니다. 반대로 wb의 dirty pool이 너무 작아 device가 idle해질 위험이 있으면 reserve-area 보정으로 ratio를 더 키울 수 있습니다. 따라서 최종 ratio까지 무조건 2 이하라는 설명은 일반 경로에 맞지 않습니다.
strictlimit는 별도 분기입니다. global 상태가 freerun이어도 해당 wb의 제한을 먼저 적용할 수 있으며, global·wb polynomial의 더 제한적인 쪽을 사용합니다. cgroup domain이 유효한 경우에는 전역 조건 외에 domain 조건도 만족해야 합니다. 단순 global 그래프만으로 모든 throttle 지점을 예측하지 못하는 이유입니다.
일반 경로에서는 wb setpoint를 16-bit 중간 정밀도로 축소하고 span으로 선형 보정선의 폭을 정합니다. wb dirty가 보정선 오른쪽으로 멀어져도 이 단계는 global ratio의 1/4 아래로 계속 내리지 않으며, reserve 영역에서는 wb dirty가 wb threshold의 절반보다 작을 때 추가 boost를 줍니다. 아주 작은 잔량에서는 이 boost가 8배입니다. 반면 domain dirty가 hard limit 이상이면 시작부터 ratio 0으로 반환합니다. strictlimit에서 wb threshold에 도달했거나 유효한 wb setpoint를 만들 수 없는 경우도 0으로 돌아옵니다. 이 반환 조건과 reserve 보정을 함께 읽어야 합니다. v6.18 wb_position_ratio 전체 정의
// 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_position_ratio(struct dirty_throttle_control *dtc)
{
struct bdi_writeback *wb = dtc->wb;
unsigned long write_bw = READ_ONCE(wb->avg_write_bandwidth);
unsigned long freerun = dirty_freerun_ceiling(dtc->thresh, dtc->bg_thresh);
unsigned long limit = dtc->limit = hard_dirty_limit(dtc_dom(dtc), dtc->thresh);
unsigned long wb_thresh = dtc->wb_thresh;
unsigned long x_intercept;
unsigned long setpoint; /* dirty pages' target balance point */
unsigned long wb_setpoint;
unsigned long span;
long long pos_ratio; /* for scaling up/down the rate limit */
long x;
dtc->pos_ratio = 0;
if (unlikely(dtc->dirty >= limit))
return;
/*
* global setpoint
*
* See comment for pos_ratio_polynom().
*/
setpoint = (freerun + limit) / 2;
pos_ratio = pos_ratio_polynom(setpoint, dtc->dirty, limit);
/*
* The strictlimit feature is a tool preventing mistrusted filesystems
* from growing a large number of dirty pages before throttling. For
* such filesystems balance_dirty_pages always checks wb counters
* against wb limits. Even if global "nr_dirty" is under "freerun".
* This is especially important for fuse which sets bdi->max_ratio to
* 1% by default.
*
* Here, in wb_position_ratio(), we calculate pos_ratio based on
* two values: wb_dirty and wb_thresh. Let's consider an example:
* total amount of RAM is 16GB, bdi->max_ratio is equal to 1%, global
* limits are set by default to 10% and 20% (background and throttle).
* Then wb_thresh is 1% of 20% of 16GB. This amounts to ~8K pages.
* wb_calc_thresh(wb, bg_thresh) is about ~4K pages. wb_setpoint is
* about ~6K pages (as the average of background and throttle wb
* limits). The 3rd order polynomial will provide positive feedback if
* wb_dirty is under wb_setpoint and vice versa.
*
* Note, that we cannot use global counters in these calculations
* because we want to throttle process writing to a strictlimit wb
* much earlier than global "freerun" is reached (~23MB vs. ~2.3GB
* in the example above).
*/
if (unlikely(wb->bdi->capabilities & BDI_CAP_STRICTLIMIT)) {
long long wb_pos_ratio;
if (dtc->wb_dirty >= wb_thresh)
return;
wb_setpoint = dirty_freerun_ceiling(wb_thresh,
dtc->wb_bg_thresh);
if (wb_setpoint == 0 || wb_setpoint == wb_thresh)
return;
wb_pos_ratio = pos_ratio_polynom(wb_setpoint, dtc->wb_dirty,
wb_thresh);
/*
* Typically, for strictlimit case, wb_setpoint << setpoint
* and pos_ratio >> wb_pos_ratio. In the other words global
* state ("dirty") is not limiting factor and we have to
* make decision based on wb counters. But there is an
* important case when global pos_ratio should get precedence:
* global limits are exceeded (e.g. due to activities on other
* wb's) while given strictlimit wb is below limit.
*
* "pos_ratio * wb_pos_ratio" would work for the case above,
* but it would look too non-natural for the case of all
* activity in the system coming from a single strictlimit wb
* with bdi->max_ratio == 100%.
*
* Note that min() below somewhat changes the dynamics of the
* control system. Normally, pos_ratio value can be well over 3
* (when globally we are at freerun and wb is well below wb
* setpoint). Now the maximum pos_ratio in the same situation
* is 2. We might want to tweak this if we observe the control
* system is too slow to adapt.
*/
dtc->pos_ratio = min(pos_ratio, wb_pos_ratio);
return;
}
/*
* We have computed basic pos_ratio above based on global situation. If
* the wb is over/under its share of dirty pages, we want to scale
* pos_ratio further down/up. That is done by the following mechanism.
*/
/*
* wb setpoint
*
* f(wb_dirty) := 1.0 + k * (wb_dirty - wb_setpoint)
*
* x_intercept - wb_dirty
* := --------------------------
* x_intercept - wb_setpoint
*
* The main wb control line is a linear function that subjects to
*
* (1) f(wb_setpoint) = 1.0
* (2) k = - 1 / (8 * write_bw) (in single wb case)
* or equally: x_intercept = wb_setpoint + 8 * write_bw
*
* For single wb case, the dirty pages are observed to fluctuate
* regularly within range
* [wb_setpoint - write_bw/2, wb_setpoint + write_bw/2]
* for various filesystems, where (2) can yield in a reasonable 12.5%
* fluctuation range for pos_ratio.
*
* For JBOD case, wb_thresh (not wb_dirty!) could fluctuate up to its
* own size, so move the slope over accordingly and choose a slope that
* yields 100% pos_ratio fluctuation on suddenly doubled wb_thresh.
*/
if (unlikely(wb_thresh > dtc->thresh))
wb_thresh = dtc->thresh;
/*
* scale global setpoint to wb's:
* wb_setpoint = setpoint * wb_thresh / thresh
*/
x = div_u64((u64)wb_thresh << 16, dtc->thresh | 1);
wb_setpoint = setpoint * (u64)x >> 16;
/*
* Use span=(8*write_bw) in single wb case as indicated by
* (thresh - wb_thresh ~= 0) and transit to wb_thresh in JBOD case.
*
* wb_thresh thresh - wb_thresh
* span = --------- * (8 * write_bw) + ------------------ * wb_thresh
* thresh thresh
*/
span = (dtc->thresh - wb_thresh + 8 * write_bw) * (u64)x >> 16;
x_intercept = wb_setpoint + span;
if (dtc->wb_dirty < x_intercept - span / 4) {
pos_ratio = div64_u64(pos_ratio * (x_intercept - dtc->wb_dirty),
(x_intercept - wb_setpoint) | 1);
} else
pos_ratio /= 4;
/*
* wb reserve area, safeguard against dirty pool underrun and disk idle
* It may push the desired control point of global dirty pages higher
* than setpoint.
*/
x_intercept = wb_thresh / 2;
if (dtc->wb_dirty < x_intercept) {
if (dtc->wb_dirty > x_intercept / 8)
pos_ratio = div_u64(pos_ratio * x_intercept,
dtc->wb_dirty);
else
pos_ratio *= 8;
}
dtc->pos_ratio = pos_ratio;
}
다음 점검은 남은 여유에 따라 가까워집니다
freerun 중에도 다시 점검할 필요가 있습니다. v6.18 dirty_poll_interval()은 여유 page 수 thresh - dirty에 대해 1 << (ilog2(margin) >> 1)을 사용하고 여유가 없으면 1을 반환합니다.
이 값은 margin 자체가 아니라 그 제곱근에 가까운 power-of-two 근사입니다. 예를 들어 margin이 4096 pages이면 interval은 64 pages입니다. 여유 전체를 한 task가 소모할 때까지 기다리는 것이 아닙니다. threshold에 가까워질수록 더 자주 점검하고, 충분히 멀면 비싼 전역 통계 조회 횟수를 줄입니다.
thresh <= dirty인 경우에 1을 반환합니다. threshold가 dirty보다 높을 때 1을 반환한다는 반대 해석은 코드와 맞지 않습니다. v6.18 dirty_poll_interval 전체 정의
// 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 unsigned long dirty_poll_interval(unsigned long dirty,
unsigned long thresh)
{
if (thresh > dirty)
return 1UL << (ilog2(thresh - dirty) >> 1);
return 1;
}
setpoint와 limit, wb 몫, poll interval은 모두 같은 목적을 다른 시간 규모에서 지원합니다. 한 번의 조건문으로 writer를 on/off하기보다, dirty량과 device의 소비 능력을 계속 맞추기 위한 제어입니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 10, Dirty Position Control Ratio, Dirty Limits, Dirty Poll Interval.
- Linux v6.18 mm/page-writeback.c: domain·wb threshold, hard limit, position ratio, poll interval 전체 구현.
- Linux v6.18 VM tunables: dirty 기준의 사용자 interface.