백엔드 디바이스 (Backing Device)
개요 (Overview)
Linux 커널의 백엔드 디바이스(Backing Device)는 파일 시스템의 더러운 페이지(dirty pages)를 디스크에 기록하는 쓰기 회수(writeback) 메커니즘의 핵심 인프라입니다. backing_dev_info 구조체는 각 블록 디바이스나 파일 시스템의 I/O 특성, 대역폭 한도, 더러운 페이지 임계값 등을 관리하며, bdi_writeback 구조체는 실제 쓰기 회수 작업을 수행하는 워커입니다.
이 메커니즘은 I/O 스케줄러와 긴밀하게 통합되어 있으며, 시스템의 더러운 페이지 양을 제어하고 디스크 대역폭을 효율적으로 분배하는 역할을 합니다. 컨테이너 환경에서는 cgroup writeback 기능을 통해 메모리 cgroup별로 독립적인 쓰기 회수 통계와 제어가 가능합니다.
백엔드 디바이스는 물류 센터처럼 보면 이해가 쉽습니다. backing_dev_info는 창구와 배차 규칙, bdi_writeback은 출고 담당자, dirty page는 아직 나가지 못한 상자에 가깝습니다. 배경 임계값을 넘으면 page-writeback.c가 담당자를 깨우고, 메모리 압박이 커지면 vmscan.c가 flusher를 다시 움직입니다.
소스 파일 경로:
├── mm/backing-dev.c ← 백엔드 디바이스 코어 로직
├── include/linux/backing-dev.h ← 외부 인터페이스
├── include/linux/backing-dev-defs.h ← 구조체 정의
├── include/linux/writeback.h ← 쓰기 회수 제어 구조체
└── fs/fs-writeback.c ← 쓰기 회수 구현
빠른 점검 명령
# 모든 BDI 디바이스 목록 확인
ls /sys/class/bdi/
# 특정 BDI의 더러운 페이지 통계 확인
cat /sys/class/bdi/259:0/stats 2>/dev/null || echo "디버그 FS 미활성화"
# 시스템 전체 더러운 페이지 현황 확인
cat /proc/meminfo | grep -i "dirty\|writeback"
# 쓰기 회수 관련 커널 로그 확인
dmesg | grep -i "writeback\|bdi"
# cgroup v2 쓰기 회수 제한 확인 (컨테이너 환경)
cat /sys/fs/cgroup/memory.io.max 2>/dev/null || echo "cgroup v2 미설정"
# 블록 디바이스별 BDI 매핑 확인
cat /sys/block/sda/queue/read_ahead_kb
# 더러운 페이지 임계값 확인
cat /proc/sys/vm/dirty_ratio
cat /proc/sys/vm/dirty_background_ratio
# 쓰기 회수 워커 상태 확인
ps aux | grep kflushd | grep -v grep
# BDI 대역폭 통계 확인 (디버그 모드)
cat /sys/kernel/debug/bdi/*/wb_stats 2>/dev/null || echo "디버그 디렉토리 없음"
# cgroup writeback 활성화 상태 확인
grep CONFIG_CGROUP_WRITEBACK /boot/config-$(uname -r)
# dirty/writeback 전역 통계와 임계값 확인
cat /proc/vmstat | grep -E "nr_dirty|nr_writeback|nr_dirtied|nr_written"
cat /proc/sys/vm/dirty_writeback_centisecs
cat /proc/sys/vm/dirty_expire_centisecs
cat /proc/sys/vm/dirty_background_bytes
cat /proc/sys/vm/dirty_bytes
# 현재 플러시 계열 워커 상태 확인
ps -eLo pid,comm | grep -E 'flush-|writeback|jbd2' | grep -v grep
# BDI sysfs와 debugfs를 한 번에 훑기
for f in /sys/class/bdi/*/stats; do cat "$f"; done 2>/dev/null
for f in /sys/kernel/debug/bdi/*/wb_stats; do cat "$f"; done 2>/dev/null
핵심 자료구조
1. `struct backing_dev_info` (include/linux/backing-dev-defs.h:168)
struct backing_dev_info {
u64 id; // 고유 식별자
struct rb_node rb_node; // 레드-블랙 트리 노드 (bdi_tree)
struct list_head bdi_list; // 전역 BDI 목록
unsigned long __data_racy ra_pages; // 최대 리-ahead 크기 (페이지 단위)
unsigned long io_pages; // 최대 I/O 허용 크기
struct kref refcnt; // 참조 카운터
unsigned int capabilities; // 디바이스 기능 플래그
unsigned int min_ratio; // 최소 대역폭 비율 (BDI_RATIO_SCALE 기준)
unsigned int max_ratio, max_prop_frac; // 최대 대역폭 비율 및 비례 분수
atomic_long_t tot_write_bandwidth; // 총 쓰기 대역폭 (더러운 wb 존재 시 > 0)
unsigned long last_bdp_sleep; // 마지막 BDP 스로틀 시점 (jiffies)
struct bdi_writeback wb; // 루트 쓰기 회수 정보
struct list_head wb_list; // 모든 wb 목록
struct radix_tree_root cgwb_tree; // cgroup wb 레디시 트리
struct mutex cgwb_release_mutex; // wb 구조체 종료 보호
struct rw_semaphore wb_switch_rwsem; // cgroup wb 전환 시 쓰기 회수 중단
wait_queue_head_t wb_waitq; // 쓰기 회수 완료 대기 큐
struct device *dev; // 디바이스 구조체
char dev_name[64]; // 디바이스 이름
struct device *owner; // 소유자 디바이스
struct dentry *debug_dir; // 디버그 디렉토리
};
// 각 블록 디바이스나 파일 시스템은 하나의 BDI를 보유
// I/O 대역폭 제한 및 더러운 페이지 관리를 담당
2. `struct bdi_writeback` (include/linux/backing-dev-defs.h:106)
struct bdi_writeback {
struct backing_dev_info *bdi; // 상위 BDI 포인터
unsigned long state; // 상태 비트 (WB_registered 등)
unsigned long last_old_flush; // 마지막 이전 데이터 플러시 시점
struct list_head b_dirty; // 더러운 inode 목록
struct list_head b_io; // 쓰기 회수 중인 inode 목록
struct list_head b_more_io; // 추가 쓰기 회수 대기 inode 목록
struct list_head b_dirty_time; // 시간 기반 더러운 inode 목록
spinlock_t list_lock; // b_* 목록 보호 뮤텍스
atomic_t writeback_inodes; // 쓰기 회수 중인 inode 수
struct percpu_counter stat[NR_WB_STAT_ITEMS]; // 쓰기 회수 통계
unsigned long bw_time_stamp; // 마지막 대역폭 업데이트 시점
unsigned long dirtied_stamp; // 더러워진 페이지 타임스탬프
unsigned long written_stamp; // 기록된 페이지 타임스탬프
unsigned long write_bandwidth; // 추정 쓰기 대역폭
unsigned long avg_write_bandwidth; // 평활화된 쓰기 대역폭
unsigned long dirty_ratelimit; // 기본 더러운 페이지 속도 제한
unsigned long balanced_dirty_ratelimit; // 균형 잡힌 더러운 페이지 속도 제한
struct fprop_local_percpu completions; // 완료 카운터
int dirty_exceeded; // 더러운 페이지 초과 플래그
enum wb_reason start_all_reason; // 전체 시작 이유
spinlock_t work_lock; // work_list 및 dwork 스케줄링 보호
struct list_head work_list; // 쓰기 회수 작업 목록
struct delayed_work dwork; // 지연된 쓰기 회수 작업
struct delayed_work bw_dwork; // 대역폭 추정 지연 작업
struct list_head bdi_node; // bdi->wb_list 연결 노드
struct percpu_ref refcnt; // 참조 카운터 (루트 외 wb)
struct cgroup_subsys_state *memcg_css; // 연결된 memcg
struct cgroup_subsys_state *blkcg_css; // 연결된 blkcg
struct list_head memcg_node; // memcg->cgwb_list 연결 노드
struct list_head blkcg_node; // blkcg->cgwb_list 연결 노드
struct list_head b_attached; // 연결된 inode 목록
struct list_head offline_node; // 오프라인 wb 목록
struct work_struct switch_work; // inode 전환 작업
struct llist_head switch_wbs_ctxs; // 전환 컨텍스트 큐
union {
struct work_struct release_work; // 해제 작업
struct rcu_head rcu; // RCU 해제 헤드
};
};
// cgroup writeback 활성화 시 memcg/blkcg 조합별 독립적 wb 생성
// 각 wb는 독립적으로 쓰기 회수, 통계, 스로틀링 수행
3. `enum wb_state` (include/linux/backing-dev-defs.h:24)
enum wb_state {
WB_registered, // bdi_register() 완료
WB_writeback_running, // 쓰기 회수 진행 중
WB_has_dirty_io, // b_{dirty|io|more_io}에 더러운 inode 존재
WB_start_all, // 전체 시작 작업 대기 중 (nr_pages == 0)
};
// wb->state 비트 필드로 관리됨
// atomic bitops으로 안전하게 접근
4. `enum wb_stat_item` (include/linux/backing-dev-defs.h:31)
enum wb_stat_item {
WB_RECLAIMABLE, // 회수 가능 페이지 수
WB_WRITEBACK, // 쓰기 회수 중인 페이지 수
WB_DIRTIED, // 더러워진 페이지 수
WB_WRITTEN, // 기록된 페이지 수
NR_WB_STAT_ITEMS // 통계 항목 수
};
// percpu_counter로 구현되어 락 없이 빠른 접근 가능
5. `struct wb_stats` (mm/backing-dev.c:42)
struct wb_stats {
unsigned long nr_dirty; // 더러운 inode 수
unsigned long nr_io; // I/O 중인 inode 수
unsigned long nr_more_io; // 추가 I/O 대기 inode 수
unsigned long nr_dirty_time; // 시간 기반 더러운 inode 수
unsigned long nr_writeback; // 쓰기 회수 중인 페이지 수
unsigned long nr_reclaimable; // 회수 가능 페이지 수
unsigned long nr_dirtied; // 더러워진 페이지 수
unsigned long nr_written; // 기록된 페이지 수
unsigned long dirty_thresh; // 전역 더러운 페이지 임계값
unsigned long wb_thresh; // wb별 더러운 페이지 임계값
};
// 디버그 FS를 통해 BDI 통계를 표시하는 데 사용됨
핵심 함수
1. `bdi_init()` (mm/backing-dev.c:1006)
int bdi_init(struct backing_dev_info *bdi)
{
bdi->dev = NULL;
kref_init(&bdi->refcnt); // 참조 카운터 초기화
bdi->min_ratio = 0; // 최소 대역폭 비율 0
bdi->max_ratio = 100 * BDI_RATIO_SCALE; // 최대 대역폭 비율 100%
bdi->max_prop_frac = FPROP_FRAC_BASE;
INIT_LIST_HEAD(&bdi->bdi_list); // 전역 BDI 목록 초기화
INIT_LIST_HEAD(&bdi->wb_list); // wb 목록 초기화
init_waitqueue_head(&bdi->wb_waitq);
bdi->last_bdp_sleep = jiffies;
return cgwb_bdi_init(bdi); // cgroup wb 초기화
}
// BDI 구조체 초기화 시 호출됨
// 기본 대역폭 비율 설정 및 cgroup wb 트리 초기화
2. `wb_init()` (mm/backing-dev.c:515)
static int wb_init(struct bdi_writeback *wb, struct backing_dev_info *bdi, gfp_t gfp)
{
int err;
memset(wb, 0, sizeof(*wb));
wb->bdi = bdi;
wb->last_old_flush = jiffies;
INIT_LIST_HEAD(&wb->b_dirty);
INIT_LIST_HEAD(&wb->b_io);
INIT_LIST_HEAD(&wb->b_more_io);
INIT_LIST_HEAD(&wb->b_dirty_time);
spin_lock_init(&wb->list_lock);
atomic_set(&wb->writeback_inodes, 0);
wb->bw_time_stamp = jiffies;
wb->balanced_dirty_ratelimit = INIT_BW;
wb->dirty_ratelimit = INIT_BW;
wb->write_bandwidth = INIT_BW;
wb->avg_write_bandwidth = INIT_BW;
spin_lock_init(&wb->work_lock);
INIT_LIST_HEAD(&wb->work_list);
INIT_DELAYED_WORK(&wb->dwork, wb_workfn);
INIT_DELAYED_WORK(&wb->bw_dwork, wb_update_bandwidth_workfn);
err = fprop_local_init_percpu(&wb->completions, gfp);
if (err)
return err;
err = percpu_counter_init_many(wb->stat, 0, gfp, NR_WB_STAT_ITEMS);
if (err)
fprop_local_destroy_percpu(&wb->completions);
return err;
}
3. `wb_workfn()` (fs/fs-writeback.c:2409)
void wb_workfn(struct work_struct *work)
{
struct bdi_writeback *wb = container_of(to_delayed_work(work),
struct bdi_writeback, dwork);
long pages_written;
set_worker_desc("flush-%s", bdi_dev_name(wb->bdi));
if (likely(!current_is_workqueue_rescuer() ||
!test_bit(WB_registered, &wb->state))) {
do {
pages_written = wb_do_writeback(wb);
trace_writeback_pages_written(pages_written);
} while (!list_empty(&wb->work_list));
} else {
pages_written = writeback_inodes_wb(wb, 1024,
WB_REASON_FORKER_THREAD);
trace_writeback_pages_written(pages_written);
}
if (!list_empty(&wb->work_list))
wb_wakeup(wb);
else if (wb_has_dirty_io(wb) && dirty_writeback_interval)
wb_wakeup_delayed(wb);
}
4. `bdi_register_va()` (mm/backing-dev.c:1089)
int bdi_register_va(struct backing_dev_info *bdi, const char *fmt, va_list args)
{
struct device *dev;
struct rb_node *parent, **p;
if (bdi->dev) /* 드라이버가 디바이스마다 별도 큐를 써야 하는 경우 */
return 0;
vsnprintf(bdi->dev_name, sizeof(bdi->dev_name), fmt, args);
dev = device_create(&bdi_class, NULL, MKDEV(0, 0), bdi, bdi->dev_name);
if (IS_ERR(dev))
return PTR_ERR(dev);
cgwb_bdi_register(bdi);
bdi->dev = dev;
bdi_debug_register(bdi, dev_name(dev));
set_bit(WB_registered, &bdi->wb.state);
spin_lock_bh(&bdi_lock);
bdi->id = ++bdi_id_cursor;
p = bdi_lookup_rb_node(bdi->id, &parent);
rb_link_node(&bdi->rb_node, parent, p);
rb_insert_color(&bdi->rb_node, &bdi_tree);
list_add_tail_rcu(&bdi->bdi_list, &bdi_list);
spin_unlock_bh(&bdi_lock);
trace_writeback_bdi_register(bdi);
return 0;
}
5. `wb_start_background_writeback()` (fs/fs-writeback.c:1345)
void wb_start_background_writeback(struct bdi_writeback *wb)
{
trace_writeback_wake_background(wb);
wb_wakeup(wb);
}
6. `inode_to_bdi()` (mm/backing-dev.c:1200)
struct backing_dev_info *inode_to_bdi(struct inode *inode)
{
struct super_block *sb;
if (!inode)
return &noop_backing_dev_info;
sb = inode->i_sb;
if (sb_is_blkdev_sb(sb))
return I_BDEV(inode)->bd_disk->bdi;
return sb->s_bdi;
}
7. `cgwb_create()` (mm/backing-dev.c:665)
static int cgwb_create(struct backing_dev_info *bdi,
struct cgroup_subsys_state *memcg_css, gfp_t gfp)
{
struct mem_cgroup *memcg;
struct cgroup_subsys_state *blkcg_css;
struct list_head *memcg_cgwb_list, *blkcg_cgwb_list;
struct bdi_writeback *wb;
unsigned long flags;
int ret = 0;
memcg = mem_cgroup_from_css(memcg_css);
blkcg_css = cgroup_get_e_css(memcg_css->cgroup, &io_cgrp_subsys);
memcg_cgwb_list = &memcg->cgwb_list;
blkcg_cgwb_list = blkcg_get_cgwb_list(blkcg_css);
/* 잠금 아래에서 다시 찾고 blkcg가 다르면 버린다 */
spin_lock_irqsave(&cgwb_lock, flags);
wb = radix_tree_lookup(&bdi->cgwb_tree, memcg_css->id);
if (wb && wb->blkcg_css != blkcg_css) {
cgwb_kill(wb);
wb = NULL;
}
spin_unlock_irqrestore(&cgwb_lock, flags);
if (wb)
goto out_put;
/* 새 wb를 만든다 */
wb = kmalloc_obj(*wb, gfp);
if (!wb) {
ret = -ENOMEM;
goto out_put;
}
ret = wb_init(wb, bdi, gfp);
if (ret)
goto err_free;
ret = percpu_ref_init(&wb->refcnt, cgwb_release, 0, gfp);
if (ret)
goto err_wb_exit;
ret = fprop_local_init_percpu(&wb->memcg_completions, gfp);
if (ret)
goto err_ref_exit;
wb->memcg_css = memcg_css;
wb->blkcg_css = blkcg_css;
INIT_LIST_HEAD(&wb->b_attached);
INIT_WORK(&wb->switch_work, inode_switch_wbs_work_fn);
init_llist_head(&wb->switch_wbs_ctxs);
INIT_WORK(&wb->release_work, cgwb_release_workfn);
set_bit(WB_registered, &wb->state);
bdi_get(bdi);
/* 루트 wb 등록 여부와 두 cgroup 목록의 온라인 상태를 확인한다 */
ret = -ENODEV;
spin_lock_irqsave(&cgwb_lock, flags);
if (test_bit(WB_registered, &bdi->wb.state) &&
blkcg_cgwb_list->next && memcg_cgwb_list->next) {
ret = radix_tree_insert(&bdi->cgwb_tree, memcg_css->id, wb);
if (!ret) {
list_add_tail_rcu(&wb->bdi_node, &bdi->wb_list);
list_add(&wb->memcg_node, memcg_cgwb_list);
list_add(&wb->blkcg_node, blkcg_cgwb_list);
blkcg_pin_online(blkcg_css);
css_get(memcg_css);
css_get(blkcg_css);
}
}
spin_unlock_irqrestore(&cgwb_lock, flags);
if (ret) {
if (ret == -EEXIST)
ret = 0;
goto err_fprop_exit;
}
goto out_put;
err_fprop_exit:
bdi_put(bdi);
fprop_local_destroy_percpu(&wb->memcg_completions);
err_ref_exit:
percpu_ref_exit(&wb->refcnt);
err_wb_exit:
wb_exit(wb);
err_free:
kfree(wb);
out_put:
css_put(blkcg_css);
return ret;
}
호출 흐름
1. BDI 등록 흐름
bdi_alloc() → bdi_init() → cgwb_bdi_init() → wb_init()
↓
bdi_register() → bdi_register_va()
↓
device_create() → cgwb_bdi_register() → bdi_debug_register()
↓
bdi_tree 삽입 → bdi_list 추가
2. 배경 writeback 흐름
balance_dirty_pages() [mm/page-writeback.c:1838-1839]
↓
wb_start_background_writeback() [fs/fs-writeback.c:1345]
↓
wb_wakeup() → wb_workfn()
3. 회수 압력 writeback 흐름
shrink_inactive_list() / shrink_one() [mm/vmscan.c:2052-2053, 4895-4900]
↓
wakeup_flusher_threads(WB_REASON_VMSCAN)
↓
wb_start_writeback() / wb_wakeup()
4. 쓰기 회수 흐름
wb_workfn() [지연된 작업]
↓
wb_do_writeback() → wb_writeback_sb_inodes()
↓
__writeback_single_inode() → do_writepages()
↓
a_ops->writepages() 또는 a_ops->write_iter()
5. cgroup wb 생성 흐름
wb_get_create_current() → wb_find_current()
↓ (미발견 시)
wb_get_create() → cgwb_create()
↓
wb_init() → percpu_ref_init() → radix_tree_insert()
6. BDI 해제 흐름
bdi_unregister()
↓
bdi_remove_from_list() → wb_shutdown() → cgwb_bdi_unregister()
↓
device_unregister() → bdi_put()
조건별 비교
1. BDI 유형 비교
| 유형 | 설명 | 사용 시점 |
| 블록 디바이스 BDI | `I_BDEV(inode)->bd_disk->bdi` | 일반 블록 디바이스 (SATA, NVMe 등) |
| 파일 시스템 BDI | `sb->s_bdi` | 네트워크 파일 시스템 (NFS, CIFS 등) |
| noop BDI | `noop_backing_dev_info` | 더러운 페이지 관리 불필요 시 |
2. 쓰기 회수 이유 비교
| 이유 | enum 값 | 설명 |
| 백그라운드 | `WB_REASON_BACKGROUND` | dirty_ratio 초과 시 백그라운드 쓰기 회수 |
| vmscan | `WB_REASON_VMSCAN` | 메모리 회수 시 강제 쓰기 회수 |
| 동기화 | `WB_REASON_SYNC` | sync() 시스템 콜 호출 |
| 주기적 | `WB_REASON_PERIODIC` | 5초마다 자동 쓰기 회수 |
| 여유 공간 | `WB_REASON_FS_FREE_SPACE` | 파일 시스템 여유 공간 확보 필요 |
3. cgroup writeback 동작 비교
| 조건 | 비활성화 | 활성화 |
| wb 생성 | `bdi->wb` 하나만 사용 | memcg/blkcg 조합별 독립 wb |
| 통계 | 전역 통계만 제공 | cgroup별 독립 통계 |
| 제어 | 전역 대역폭 제한 | cgroup별 대역폭 제한 |
| inode 연결 | 고정 | 동적 전환 가능 |
4. 대역폭 제한 방식 비교
| 방식 | 설정 방법 | 설명 |
| 비율 기반 | `min_ratio`, `max_ratio` | 전체 대역폭 대비 비율 (1/10000 단위) |
| 절대 크기 기반 | `min_bytes`, `max_bytes` | 바이트 단위 절대 크기 |
| 엄격한 제한 | `strict_limit` | 임계값 초과 시 즉시 스로틀링 |
5. 쓰기 유도 경로 비교
| 경로 | 대표 트리거 | 흐름 | 확인 포인트 |
| 배경 writeback | `nr_dirty > bg_thresh` 또는 per-wb 임계값 초과 | `balance_dirty_pages()` → `wb_start_background_writeback()` → `wb_wakeup()` | `dirty_background_bytes`, `dirty_writeback_centisecs` |
| 회수 압력 writeback | dirty folio를 reclaim 중에 발견 | `shrink_inactive_list()` / `shrink_one()` → `wakeup_flusher_threads(WB_REASON_VMSCAN)` | `nr_dirty`, `nr_writeback`, `nr_unqueued_dirty` |
| 동기 writeback | `sync()` / `fsync()` | `writeback_inodes_sb(sb, WB_REASON_SYNC)` → `wakeup_flusher_threads(WB_REASON_SYNC)` | `sync_filesystem()`, `ksys_sync()` |
관련 문서
05 — 페이지 회수: 쓰기 회수와 연계된 페이지 회수 메커니즘
39 — vmpressure: 회수 압력과 dirty/writeback 관찰 지점
14 — Memory Cgroup: cgroup writeback과 memcg 연동
53 — 컨테이너 메모리 관리 실전: 컨테이너 환경에서의 쓰기 회수 설정
35 — Shrinker: 쓰기 회수와 연계된 캐시 회수
07 — Swap / zswap: 쓰기 회수와 연계된 스왑 메커니즘