page cache는 device보다 빠르게 write를 받아 줄 수 있습니다. 이 차이가 잠깐이면 유용한 buffering이지만 계속되면 저장하지 못한 dirty data가 메모리를 채웁니다. kernel은 worker를 더 자주 깨우는 것뿐 아니라 data를 만드는 task의 속도도 늦춰야 합니다.
dirty throttling의 핵심은 writer가 자기 data를 그 자리에서 전부 writeback하는 것이 아닙니다. flusher가 I/O를 진행하도록 하고, 필요할 때 dirtying task를 잠시 멈추어 생산 속도를 조절합니다. 모든 writer를 시스템 전체의 단일 장벽 뒤에 세우지 않도록 wb별 상태도 사용합니다.
저장하지 못한 dirty data가 많아지면 crash 때 잃을 수 있는 미저장 변경도 많아지고, 같은 storage를 쓰는 다른 I/O의 응답성에도 영향을 줄 수 있습니다. 다만 dirty와 disk의 차이가 있다는 사실 자체를 filesystem corruption으로 판정해서는 안 됩니다. throttling은 data durability를 보증하는 sync interface의 대체물이 아니라 메모리의 생산량과 writeback의 소비량을 맞추는 제어입니다.
dirty ratio의 분모는 설치된 RAM 전체가 아닙니다
vm.dirty_ratio와 vm.dirty_background_ratio는 dirtyable memory를 기준으로 계산합니다. upstream v6.18 global_dirtyable_memory()는 free page 수에서 kernel reserve를 넘지 않게 빼고 active·inactive file page 수를 더합니다. 설정에 따라 highmem 중 제외할 부분도 빼며, 0 반환을 피하는 보정이 있습니다.
이미 anonymous memory 등으로 쓰는 모든 RAM을 그대로 dirty page cache 후보로 세는 계산이 아닙니다. 따라서 RAM이 일정해도 다른 workload와 reserve 조건에 따라 같은 ratio의 page 수는 달라질 수 있습니다. 메모리 모니터의 일반적인 available 값과 무조건 같은 값도 아닙니다. v6.18 global_dirtyable_memory
다음 두 정의는 v6.18의 전체 함수입니다. reserve 차감은 free량보다 커지지 않게 제한하며, highmem 차감도 계산한 전체량을 넘지 않게 합니다. CONFIG_HIGHMEM이 없으면 highmem helper는 0을 반환합니다. 이 코드들은 kernel 문맥의 함수이며 단독 실행 프로그램은 아닙니다. GPL-2.0-only 고지와 원래 attribution을 함께 보존했습니다.
global_dirtyable_memory_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 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 */
}
highmem_dirtyable_memory_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 unsigned long highmem_dirtyable_memory(unsigned long total)
{
#ifdef CONFIG_HIGHMEM
int node;
unsigned long x = 0;
int i;
for_each_node_state(node, N_HIGH_MEMORY) {
for (i = ZONE_NORMAL + 1; i < MAX_NR_ZONES; i++) {
struct zone *z;
unsigned long nr_pages;
if (!is_highmem_idx(i))
continue;
z = &NODE_DATA(node)->node_zones[i];
if (!populated_zone(z))
continue;
nr_pages = zone_page_state(z, NR_FREE_PAGES);
/* watch for underflows */
nr_pages -= min(nr_pages, high_wmark_pages(z));
nr_pages += zone_page_state(z, NR_ZONE_INACTIVE_FILE);
nr_pages += zone_page_state(z, NR_ZONE_ACTIVE_FILE);
x += nr_pages;
}
}
/*
* Make sure that the number of highmem pages is never larger
* than the number of the total dirtyable memory. This can only
* occur in very strange VM situations but we want to make sure
* that this does not occur.
*/
return min(x, total);
#else
return 0;
#endif
}
byte 설정은 ratio의 대안입니다. dirty limit의 bytes와 ratio, background limit의 bytes와 ratio는 각각 한 쌍이며 한쪽을 설정하면 대응하는 다른 방식이 해제됩니다. background와 foreground 설정 전체가 한꺼번에 하나만 유효하다는 뜻은 아닙니다. VM tunable documentation
background 시작과 task pause 시작은 다릅니다
dirty background threshold는 flusher가 더 적극적으로 처리할 필요가 있는 기준입니다. task를 바로 멈추는 기준과 동일하지 않습니다. 일반적인 global 제어에서는 background threshold와 dirty threshold의 중간인 freerun ceiling을 사용합니다.
freerun에서는 이 dirty control로 인한 pause를 하지 않습니다. 그 위에서는 wb의 bandwidth와 목표 dirty 위치에 맞춰 허용 rate를 조절합니다. dirty limit를 넘을 때까지 아무 일도 하지 않다가 한꺼번에 모든 task를 멈추는 방식이 아닙니다.

그림은 background 10%, dirty 20%이고 hard limit가 dirty threshold와 같은 경우의 global feedback을 보여 줍니다. strictlimit, cgroup domain, task 우선순위 및 counter 오차는 생략했습니다. 표시된 구간은 제어 목적을 설명하며 일정한 write latency를 보장하지 않습니다.
v6.18 초기값은 background ratio 10, dirty ratio 20이지만 실제 시스템 설정을 읽은 측정값은 아닙니다. strictlimit BDI에서는 global freerun보다 wb 자체의 작은 한도가 먼저 적용될 수 있습니다. cgroup domain도 별도 제약을 추가할 수 있습니다.
중간값을 반환하는 helper 자체는 짧습니다. 실제 caller가 global 값과 wb 값 중 무엇을 넘기는지는 별도 조건입니다. v6.18 dirty_freerun_ceiling
// 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_freerun_ceiling(unsigned long thresh,
unsigned long bg_thresh)
{
return (thresh + bg_thresh) / 2;
}
검사 자체도 rate limit합니다
전역 page 통계와 wb 상태를 매 byte마다 조사하면 제어 비용이 커집니다. balance_dirty_pages_ratelimited_flags()는 task의 nr_dirtied가 nr_dirtied_pause에 도달했을 때 주된 balancing을 호출합니다.
nr_dirtied_pause는 초나 jiffies가 아니라 다음 점검 전까지 허용할 dirty page 수입니다. freerun이면 여유 공간에 따른 poll interval을 사용하고, throttle 중에는 최소 pause 목표와 rate에서 계산한 양을 사용합니다. 이름에 pause가 있어도 시간값과 혼동하면 안 됩니다.
caller 안의 위치도 버전마다 같다고 가정하면 안 됩니다. v6.18 generic_perform_write()는 매 iteration의 write-begin 전에 이 검사를 호출합니다. 이전 진행에서 누적된 dirty량을 검사할 수 있으므로 방금 처리한 folio의 write-end 직후여야 한다는 조건이 아닙니다. buffered write의 전체 loop 비교에서는 진행 뒤 balancing을 하던 v6.0도 구별했습니다. shared write fault의 fault_dirty_shared_page()는 folio를 dirty로 표시하고 mapping을 보존한 뒤 folio lock을 놓으며, (dirtied || page_mkwrite) && mapping일 때 balancing합니다. 가능한 경로에서는 I/O 대기 전에 mmap lock도 놓습니다. 모든 PTE write마다 같은 helper가 반드시 다시 호출된다는 뜻은 아닙니다. v6.18 shared fault caller
shared fault 쪽의 전체 정의도 함께 보겠습니다. folio unlock 뒤 truncate가 folio의 mapping을 지울 수 있으므로 local mapping을 먼저 보존하며, mapping의 lifetime은 file 참조가 지탱합니다. maybe_unlock_mmap_for_io()가 file pin을 반환한 경우에는 balancing 뒤 그 참조를 놓고 VM_FAULT_COMPLETED로 돌아옵니다. 이는 아직 mmap lock을 가진 일반 0 반환과 caller 계약이 다릅니다.
fault_dirty_shared_page_v6_18.c
// SPDX-License-Identifier: GPL-2.0-only
/*
* linux/mm/memory.c
*
* Copyright (C) 1991, 1992, 1993, 1994 Linus Torvalds
*/
/*
* Complete scope from Linux v6.18 mm/memory.c.
* Source: https://github.com/torvalds/linux/blob/v6.18/mm/memory.c
* Kernel excerpt; not a standalone program.
* The book exact source tag is not asserted.
*/
static vm_fault_t fault_dirty_shared_page(struct vm_fault *vmf)
{
struct vm_area_struct *vma = vmf->vma;
struct address_space *mapping;
struct folio *folio = page_folio(vmf->page);
bool dirtied;
bool page_mkwrite = vma->vm_ops && vma->vm_ops->page_mkwrite;
dirtied = folio_mark_dirty(folio);
VM_BUG_ON_FOLIO(folio_test_anon(folio), folio);
/*
* Take a local copy of the address_space - folio.mapping may be zeroed
* by truncate after folio_unlock(). The address_space itself remains
* pinned by vma->vm_file's reference. We rely on folio_unlock()'s
* release semantics to prevent the compiler from undoing this copying.
*/
mapping = folio_raw_mapping(folio);
folio_unlock(folio);
if (!page_mkwrite)
file_update_time(vma->vm_file);
/*
* Throttle page dirtying rate down to writeback speed.
*
* mapping may be NULL here because some device drivers do not
* set page.mapping but still dirty their pages
*
* Drop the mmap_lock before waiting on IO, if we can. The file
* is pinning the mapping, as per above.
*/
if ((dirtied || page_mkwrite) && mapping) {
struct file *fpin;
fpin = maybe_unlock_mmap_for_io(vmf, NULL);
balance_dirty_pages_ratelimited(mapping);
if (fpin) {
fput(fpin);
return VM_FAULT_COMPLETED;
}
}
return 0;
}
writeback 능력이 없는 BDI에서는 이 경로를 적용하지 않습니다. cgroup writeback 객체를 얻지 못하면 기본 wb를 사용할 수 있으며, 얻은 wb 참조는 끝에서 반환합니다. lifetime 처리는 rate 계산과 별도로 필요한 조건입니다.
flags를 받지 않는 wrapper는 결과를 반환하지 않습니다. 반환값이 필요한 caller는 flags 버전의 계약을 사용해야 합니다. 아래는 wrapper와 그 대상 함수의 전체 정의입니다. source의 preempt_disable() 구간은 CPU별 counter를 다루는 구간이지, 실제 throttle sleep 전체를 preemption 비활성 상태로 실행하는 구간이 아닙니다. v6.18 rate-limited entry
balance_dirty_pages_ratelimited_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 balance_dirty_pages_ratelimited(struct address_space *mapping)
{
balance_dirty_pages_ratelimited_flags(mapping, 0);
}
balance_dirty_pages_ratelimited_flags_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.
*/
int balance_dirty_pages_ratelimited_flags(struct address_space *mapping,
unsigned int flags)
{
struct inode *inode = mapping->host;
struct backing_dev_info *bdi = inode_to_bdi(inode);
struct bdi_writeback *wb = NULL;
int ratelimit;
int ret = 0;
int *p;
if (!(bdi->capabilities & BDI_CAP_WRITEBACK))
return ret;
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))
ret = balance_dirty_pages(wb, current->nr_dirtied, flags);
wb_put(wb);
return ret;
}
많은 task가 동시에 시작하는 경우를 따로 막습니다
각 task가 자기 임계값 직전까지만 dirty를 만들면 task 하나의 검사로는 시스템 전체 급증을 빨리 발견하지 못할 수 있습니다. CPU별 bdp_ratelimits는 이 상황에서 더 이른 balancing을 유도합니다. CPU별 값을 확인하는 짧은 구간은 preemption을 막아 같은 CPU의 상태를 일관되게 다룹니다.
ratelimit_pages의 선언 초기값은 32이지만 이를 모든 시스템의 영구 고정 상수라고 읽으면 안 됩니다. 초기화·CPU 상태 갱신 경로는 dirty threshold와 online CPU 수를 바탕으로 조정합니다. 반면 dirty_exceeded 상태에서 task 점검량을 줄이는 식은 32 >> (PAGE_SHIFT - 10)이며 4 KiB base page에서는 8 pages, 즉 32 KiB입니다. 서로 다른 두 32를 섞지 않아야 합니다.
실제 갱신은 dirty_thresh / (num_online_cpus() * 32)이고 하한은 16 pages입니다. CPU별 누적량이 이 값에 도달하면 local ratelimit를 0으로 낮추어 balancing을 유도합니다. 모든 task에 영구적으로 32 pages만 허용하는 allocation 제한이 아닙니다. v6.18 writeback_set_ratelimit
writeback_set_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.
*/
void writeback_set_ratelimit(void)
{
struct wb_domain *dom = &global_wb_domain;
unsigned long background_thresh;
unsigned long dirty_thresh;
global_dirty_limits(&background_thresh, &dirty_thresh);
dom->dirty_limit = dirty_thresh;
ratelimit_pages = dirty_thresh / (num_online_cpus() * 32);
if (ratelimit_pages < 16)
ratelimit_pages = 16;
}
짧게 살고 종료하는 task도 따로 고려합니다. task가 검사 직전에 종료하여 누적량을 계속 버리면 새 task들이 dirty 제한을 피해 갈 수 있습니다. 종료 task의 미반영 양은 dirty_throttle_leaks로 넘겨 실행 중인 task가 일부를 이어받습니다. 이는 이 task가 실제로 다른 task의 data를 소유한다는 뜻이 아니라 throttling 비용의 누락을 막는 accounting입니다.
점검량과 실제 bandwidth를 구별합니다
검사 빈도를 높인다고 storage bandwidth가 늘지는 않습니다. rate limit helper는 필요한 순간에 balance_dirty_pages()로 들어가는 장치입니다. 그 안에서는 dirty 위치, wb 완료량으로 추정한 bandwidth, task가 최근 만든 양을 조합하여 pause 여부를 결정합니다.
BDP_ASYNC처럼 대기할 수 없다는 요청을 전달하는 경로에서는 pause가 필요할 때 -EAGAIN을 반환할 수 있습니다. 무조건 잠들 수 있는 일반 wrapper와 계약이 다릅니다. 따라서 dirtying이 가능한 문맥과 caller의 반환값 처리를 함께 읽어야 합니다.
참고 자료
- Lorenzo Stoakes, The Linux Memory Manager, Early Access 2025, Chapter 10, Dirty Throttling 및 Rate-Limited Dirty Throttling.
- Linux v6.18 mm/page-writeback.c: dirtyable 계산, task·CPU rate limiting, leak accounting.
- Linux v6.18 VM tunables: dirty ratio·byte 설정과 주기.