허용 dirty rate가 정해져도 task를 매번 똑같이 재우면 안 됩니다. 같은 양을 dirty로 만들었어도 한 task는 그동안 CPU 계산을 오래 했고 다른 task는 메모리 복사만 빠르게 반복했을 수 있습니다. 이미 소비한 시간을 빼지 않으면 느린 writer까지 불필요하게 멈추게 됩니다.
balance_dirty_pages()는 최근 dirty량을 허용 rate의 시간으로 환산하고, 이미 흐른 시간을 빼서 pause를 계산합니다. 동시에 worker를 깨우고 제한된 주기로 통계를 갱신합니다. 실제 I/O chunk의 크기와 background 종료 판단도 이 feedback이 계속 진전하도록 돕습니다.
최대 pause는 한 번의 대기 요청을 제한합니다
upstream v6.18 wb_max_pause()는 wb의 dirty·writeback 잔량과 평균 완료 bandwidth를 바탕으로 pause를 줄일 수 있습니다. 작은 dirty pool을 가진 시스템에서 writer를 너무 오래 재우면 flusher가 금방 pool을 비우고 device가 idle해질 수 있기 때문입니다.
MAX_PAUSE는 max(HZ/5, 1)로 약 200ms에 해당합니다. 이것은 함수가 요청하는 한 번의 throttle sleep을 제한하는 값입니다. scheduler 지연까지 포함한 실제 경과 시간이 반드시 200ms 이하라는 보장도 아니고, syscall 전체가 그 안에 끝난다는 보장도 아닙니다. 다른 I/O·lock 대기나 반복 pause가 더해질 수 있습니다. v6.18 wb_max_pause
함수의 분모와 보정에는 0으로 나누지 않기 위한 항과 power-of-two 반올림이 들어갑니다. 이를 무시한 근사식은 직관을 줄 수 있지만 정확한 milliseconds를 예측하는 식으로 사용할 수는 없습니다.
잔량을 1 + bw / roundup_pow_of_two(1 + HZ / 8)로 나누고 1을 더한 뒤 MAX_PAUSE로 제한합니다. 따라서 적은 잔량에서는 상수 최대값보다 짧아질 수 있습니다. 아래는 v6.18의 전체 정의이며 단위는 jiffies입니다.
// 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_max_pause(struct bdi_writeback *wb,
unsigned long wb_dirty)
{
unsigned long bw = READ_ONCE(wb->avg_write_bandwidth);
unsigned long t;
/*
* Limit pause time for small memory systems. If sleeping for too long
* time, a small pool of dirty/writeback pages may go empty and disk go
* idle.
*
* 8 serves as the safety ratio.
*/
t = wb_dirty / (1 + bw / roundup_pow_of_two(1 + HZ / 8));
t++;
return min_t(unsigned long, t, MAX_PAUSE);
}
최소 pause는 다음 점검량도 정합니다
wb_min_pause()는 대략 10ms를 출발점으로 삼고 동시 dirtying task가 많다고 추정될수록 점검 overhead를 줄이도록 목표 시간을 조정합니다. task 수를 정확히 세는 것이 아니라 bandwidth와 base ratelimit의 logarithmic 차이를 이용합니다.
목표는 최대 pause의 절반 부근으로 제한하고, base dirty rate로 그동안 만들 수 있는 page 수를 계산합니다. 이 수가 다음 nr_dirtied_pause입니다. 반환하는 최소 pause 시간과 출력 page 수는 서로 다른 단위입니다.
목표 page 수가 지나치게 작으면 pause·재검사가 너무 빈번해 I/O를 잘게 끊을 수 있습니다. 그래서 DIRTY_POLL_THRESH와 비교하여 가능한 한 적절한 양을 모읍니다. 이 상수는 128 KiB 상당량이며 4 KiB base page에서는 32 pages입니다. 그러나 매우 느린 rate에서도 무조건 최소 32 pages를 보장하는 것은 아닙니다. 최대 pause와 실제 task rate를 다시 대입한 결과 더 작을 수 있습니다.
task rate가 base rate보다 크게 낮아진 경우 다음 pause가 상한을 넘을 수 있으므로 page 수를 다시 줄이는 분기도 있습니다. 점검 overhead 감소와 실제 rate 준수를 함께 고려하는 절차입니다.
최종 반환은 page 수가 DIRTY_POLL_THRESH 이상이면 1 + t / 2, 그보다 작으면 t입니다. page 수를 반환하는 함수가 아니며, page 수는 포인터 출력으로 따로 전달합니다. 다음 전체 함수에서 처음의 목표 t와 후속 분기의 t 재설정을 함께 확인할 수 있습니다. v6.18 wb_min_pause
// 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 wb_min_pause(struct bdi_writeback *wb,
long max_pause,
unsigned long task_ratelimit,
unsigned long dirty_ratelimit,
int *nr_dirtied_pause)
{
long hi = ilog2(READ_ONCE(wb->avg_write_bandwidth));
long lo = ilog2(READ_ONCE(wb->dirty_ratelimit));
long t; /* target pause */
long pause; /* estimated next pause */
int pages; /* target nr_dirtied_pause */
/* target for 10ms pause on 1-dd case */
t = max(1, HZ / 100);
/*
* Scale up pause time for concurrent dirtiers in order to reduce CPU
* overheads.
*
* (N * 10ms) on 2^N concurrent tasks.
*/
if (hi > lo)
t += (hi - lo) * (10 * HZ) / 1024;
/*
* This is a bit convoluted. We try to base the next nr_dirtied_pause
* on the much more stable dirty_ratelimit. However the next pause time
* will be computed based on task_ratelimit and the two rate limits may
* depart considerably at some time. Especially if task_ratelimit goes
* below dirty_ratelimit/2 and the target pause is max_pause, the next
* pause time will be max_pause*2 _trimmed down_ to max_pause. As a
* result task_ratelimit won't be executed faithfully, which could
* eventually bring down dirty_ratelimit.
*
* We apply two rules to fix it up:
* 1) try to estimate the next pause time and if necessary, use a lower
* nr_dirtied_pause so as not to exceed max_pause. When this happens,
* nr_dirtied_pause will be "dancing" with task_ratelimit.
* 2) limit the target pause time to max_pause/2, so that the normal
* small fluctuations of task_ratelimit won't trigger rule (1) and
* nr_dirtied_pause will remain as stable as dirty_ratelimit.
*/
t = min(t, 1 + max_pause / 2);
pages = dirty_ratelimit * t / roundup_pow_of_two(HZ);
/*
* Tiny nr_dirtied_pause is found to hurt I/O performance in the test
* case fio-mmap-randwrite-64k, which does 16*{sync read, async write}.
* When the 16 consecutive reads are often interrupted by some dirty
* throttling pause during the async writes, cfq will go into idles
* (deadline is fine). So push nr_dirtied_pause as high as possible
* until reaches DIRTY_POLL_THRESH=32 pages.
*/
if (pages < DIRTY_POLL_THRESH) {
t = max_pause;
pages = dirty_ratelimit * t / roundup_pow_of_two(HZ);
if (pages > DIRTY_POLL_THRESH) {
pages = DIRTY_POLL_THRESH;
t = HZ * DIRTY_POLL_THRESH / dirty_ratelimit;
}
}
pause = HZ * pages / (task_ratelimit + 1);
if (pause > max_pause) {
t = max_pause;
pages = task_ratelimit * t / roundup_pow_of_two(HZ);
}
*nr_dirtied_pause = pages;
/*
* The minimal pause time will normally be half the target pause time.
*/
return pages >= DIRTY_POLL_THRESH ? 1 + t / 2 : t;
}
이미 흐른 시간은 다시 청구하지 않습니다
pause 계산의 직관은 다음과 같습니다. 아래는 구현코드가 아닌 계산 모형이며 jiffies 변환과 정수 rounding은 생략했습니다.
period = pages_dirtied / task_ratelimit
pause = period - time_since_previous_accounting
설명용으로 48 pages를 600 pages/s의 허용 rate로 만들었다면 필요한 시간은 80ms입니다. 이미 20ms가 지났다면 남은 60ms가 pause 후보입니다. 이미 110ms를 썼다면 계산 결과가 -30ms여서 추가 sleep 예산이 없습니다. 이는 측정값이 아니라 rate와 시간의 단위를 보여 주는 예입니다.

그림은 경과 시간을 차감하는 원리만 보여 줍니다. 최소·최대 pause 보정, integer rounding, domain 선택과 scheduler 지연은 생략했습니다.
pause가 최소값보다 작으면 긴 think time으로 판단하여 accounting을 재설정하거나, 다음 구간에 반영할 virtual time을 옮기거나, 점검 page 수를 늘리고 돌아갑니다. 아주 작은 양에 대해서 매번 timer를 걸지 않습니다. pause가 상한보다 크면 한 번의 요청을 제한하면서 다음 accounting 시각도 조정합니다.
세 분기의 경계는 구체적입니다. pause < -HZ이면 시각을 now로 재설정하고 dirty 누적량을 0으로 합니다. 그렇지 않고 period != 0이면 기존 virtual 시각에 period를 더하고 누적량을 0으로 합니다. period도 0이면 기존 nr_dirtied_pause <= pages_dirtied일 때 다음 점검량에 pages_dirtied를 더합니다. 앞의 두 분기에서 새 local page 목표를 task 필드에 무조건 복사하지는 않습니다. sleep으로 내려간 뒤에야 세 task 필드를 함께 갱신합니다.
task rate가 0이면 한 번 자고 끝나지 않을 수 있습니다
balance_dirty_pages()는 먼저 domain별 dirty량과 threshold를 계산하고 필요하면 background writeback을 시작합니다. 일반 global 경로의 freerun이면 pause 없이 최근 시각과 다음 점검량을 갱신하고 돌아갑니다. strictlimit와 cgroup이 유효하면 그 제약도 적용됩니다.
freerun 밖에서는 wb 한도와 position ratio를 구하고 필요한 시점에 bandwidth·ratelimit를 갱신합니다. cgroup domain도 유효하면 더 제한적인 ratio를 선택할 수 있습니다. 선택한 ratio를 base rate에 곱한 task rate로 pause를 계산합니다.
task rate가 0이면 바로 최대 pause 후보를 사용합니다. 일반적인 0이 아닌 rate에서는 한 번 pause한 뒤 반환할 수 있지만, 0인 상태가 지속되면 통계를 다시 읽으며 반복할 수 있습니다. 다만 해당 wb의 잔량이 통계 오차 수준으로 작거나 fatal signal이 있으면 빠져나오는 조건이 있습니다. 다른 느린 wb 때문에 정상 wb까지 막다른 상태에 빠뜨리지 않으려는 배려입니다.
대기는 TASK_KILLABLE과 io_schedule_timeout()을 사용합니다. 대기할 수 없는 BDP_ASYNC 요청이면 이 지점에서 -EAGAIN을 반환합니다. 이 경로에서 몇 번 잤는지만으로 device 오류나 application progress를 판정할 수는 없습니다.
writeback chunk는 page quota의 엄격한 상한이 아닙니다
flusher가 inode 하나에 너무 오래 머물지 않도록 writeback_chunk_size()는 보통 평균 bandwidth의 약 절반 초 분량, global domain dirty limit의 DIRTY_SCOPE 분량, 남은 work page 수 가운데 작은 값을 재료로 사용합니다.
그 뒤 round_down(pages + MIN_WRITEBACK_PAGES, MIN_WRITEBACK_PAGES)를 적용합니다. 상수를 더한 뒤 round-down한다는 점이 중요합니다. 단순히 작게 내림하여 quota를 엄격히 넘지 않게 하는 계산이 아닙니다. 목표가 4 MiB보다 작아도 최소 chunk 수준으로 올라갈 수 있고 정확한 배수도 다음 단위로 커질 수 있습니다. v6.18의 이 상수는 4 MiB 상당 page 수입니다.
WB_SYNC_ALL 또는 tagged 작업은 LONG_MAX를 사용합니다. 같은 inode를 작은 조각으로 계속 재등록하며 새로운 dirty를 따라가면 integrity 작업의 livelock 방지가 어려워지기 때문입니다. 실제 I/O는 여전히 filesystem의 folio·block·BIO 경계와 error 조건을 따릅니다. v6.18 writeback_chunk_size
아래 전체 정의에서 DIRTY_SCOPE의 분모는 설치된 RAM 전체도 dirtyable memory 전체도 아닌 global_wb_domain.dirty_limit에 적용됩니다. 설명용으로 chunk 단위가 1024 pages일 때 rounding 전 1024라는 값은 2048로 커집니다. 따라서 남은 work량도 최종 결과의 엄격한 상한이 아닙니다.
// SPDX-License-Identifier: GPL-2.0-only
/*
* Complete scope from Linux v6.18 fs/fs-writeback.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/fs/fs-writeback.c
* Kernel excerpt; not a standalone program.
* The book exact source tag is not asserted.
*/
/*
* fs/fs-writeback.c
*
* Copyright (C) 2002, Linus Torvalds.
*
* Contains all the functions related to writing back and waiting
* upon dirty inodes against superblocks, and writing back dirty
* pages against inodes. ie: data writeback. Writeout of the
* inode itself is not handled here.
*
* 10Apr2002 Andrew Morton
* Split out of fs/inode.c
* Additions for address_space-based writeback
*/
static long writeback_chunk_size(struct bdi_writeback *wb,
struct wb_writeback_work *work)
{
long pages;
/*
* WB_SYNC_ALL mode does livelock avoidance by syncing dirty
* inodes/pages in one big loop. Setting wbc.nr_to_write=LONG_MAX
* here avoids calling into writeback_inodes_wb() more than once.
*
* The intended call sequence for WB_SYNC_ALL writeback is:
*
* wb_writeback()
* writeback_sb_inodes() <== called only once
* write_cache_pages() <== called once for each inode
* (quickly) tag currently dirty pages
* (maybe slowly) sync all tagged pages
*/
if (work->sync_mode == WB_SYNC_ALL || work->tagged_writepages)
pages = LONG_MAX;
else {
pages = min(wb->avg_write_bandwidth / 2,
global_wb_domain.dirty_limit / DIRTY_SCOPE);
pages = min(pages, work->nr_pages);
pages = round_down(pages + MIN_WRITEBACK_PAGES,
MIN_WRITEBACK_PAGES);
}
return pages;
}
background 종료는 아직 제출하지 않은 dirty를 봅니다
wb_over_bg_thresh()는 background 작업을 더 진행할지 판단합니다. 여기서는 이미 writeback인 page를 다시 새 제출 수요로 세지 않습니다. global dirty량이 background threshold를 넘는지 보고, wb 수준에서도 reclaimable dirty량과 wb에 배정한 background threshold를 비교합니다.
wb 비교의 기준을 foreground hard dirty threshold와 섞어서는 안 됩니다. 작은 threshold에서는 per-CPU 통계 오차를 줄이는 합산 경로를 사용합니다. cgroup domain이 있으면 그 domain에서도 판단할 수 있습니다. 모든 dirty가 0이 되어야 background 작업이 종료되는 것은 아닙니다.
v6.18에서는 이 동작이 domain_over_bg_thresh()와 wb_bg_dirty_limits()로 분리되어 있습니다. global domain을 먼저 확인하고, 그 결과가 false이면서 memcg domain이 유효하면 memcg도 확인합니다. domain의 dirty량을 얻을 때 include_writeback=false를 넘기고 wb 비교도 WB_RECLAIMABLE만 사용합니다. 각 비교는 >이며 같다는 이유만으로 true가 되지 않습니다. v6.18 complete background checks
// 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.
*/
bool wb_over_bg_thresh(struct bdi_writeback *wb)
{
struct dirty_throttle_control gdtc = { GDTC_INIT(wb) };
struct dirty_throttle_control mdtc = { MDTC_INIT(wb, &gdtc) };
if (domain_over_bg_thresh(&gdtc))
return true;
if (mdtc_valid(&mdtc))
return domain_over_bg_thresh(&mdtc);
return false;
}
// 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 bool domain_over_bg_thresh(struct dirty_throttle_control *dtc)
{
domain_dirty_avail(dtc, false);
domain_dirty_limits(dtc);
if (dtc->dirty > dtc->bg_thresh)
return true;
wb_bg_dirty_limits(dtc);
if (dtc->wb_dirty > dtc->wb_bg_thresh)
return true;
return false;
}
// 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_bg_dirty_limits(struct dirty_throttle_control *dtc)
{
struct bdi_writeback *wb = dtc->wb;
dtc->wb_bg_thresh = __wb_calc_thresh(dtc, dtc->bg_thresh);
if (dtc->wb_bg_thresh < 2 * wb_stat_error())
dtc->wb_dirty = wb_stat_sum(wb, WB_RECLAIMABLE);
else
dtc->wb_dirty = wb_stat(wb, WB_RECLAIMABLE);
}
외부 caller에 threshold 몫을 돌려주는 wb_calc_thresh()는 다음과 같습니다. v6.18은 global dirty·available 상태를 준비한 뒤 인자로 받은 threshold를 __wb_calc_thresh()에 넘깁니다. 이 함수의 이름만 보고 호출 목적이 항상 foreground라고 판단하지 않아야 합니다. 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.
*/
unsigned long wb_calc_thresh(struct bdi_writeback *wb, unsigned long thresh)
{
struct dirty_throttle_control gdtc = { GDTC_INIT(wb) };
domain_dirty_avail(&gdtc, true);
return __wb_calc_thresh(&gdtc, thresh);
}
이처럼 pause 길이, 다음 점검량, inode chunk, background 종료는 서로 다른 조절 손잡이입니다. 어떤 하나만 크게 바꾸면 항상 write latency가 좋아진다는 일반 결론은 나오지 않습니다. dirty 제어의 코드는 device를 굶기지 않으면서 생산을 소비에 맞추고, task를 과도하게 묶어 두지 않기 위한 조건들의 조합으로 읽는 편이 정확합니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 10, Maximum and Minimum Pause Time, Core Dirty Throttling, Writeback Chunk Size, Background Writeback Threshold.
- Linux v6.18 mm/page-writeback.c: pause와 점검량, core balancing, background threshold.
- Linux v6.18 fs/fs-writeback.c: inode writeback chunk 계산.