SE로서 시스템을 어떻게 더 공부해야 할지 갈피를 알 수 없는 과중에 새로운 challenge가 되는 책을 읽고 정리한 내용입니다. 좋은 책을 출판해주신 저자님께 감사드립니다 ):
http://www.yes24.com/Product/Goods/44376723
DevOps와 SE를 위한 리눅스 커널 이야기 - 예스24
커널은 오랜 세월 기능이 추가되고 개선되어 오면서 완벽하게 이해하기 힘들 정도로 방대해졌다. 하지만 변하지 않는 기본 기능들이 있다. 이런 근간이 되는 기능에 대한 이해를 바탕으로 시스
www.yes24.com
목차
- 더티 페이지와 sync 명령어
- 더티 페이지와 관련된 커널 파라미터
- 더티 페이지의 동기화와 I/O throttling
- 더티 페이지 설정과 iostat
- 결론
- Dirty Page와 sync 명령어
dirty page란 페이지 캐시 중 쓰기 작업이 이루어진 메모리입니다. 즉, 기존의 파일 I/O 작업을 통해 이미 페이지 캐시에 적재된 데이터에 어떠한 변화가 발생했지만, 이 수정 사항이 디스크까지 도달하지 못하여 페이지 캐시와 디스크의 데이터가 서로 맞지 않는 상태의 페이지 캐시가 바로 dirty page입니다.
리눅스를 다루면서 누구나 한 번 쯤은 'sync' 명령어를 입력해 보셨을거라는 생각을 합니다. 이 명령어는 바로 dirty page와 디스크를 명시적으로 동기화(Page Writeback)시키는 명령어입니다.
dirty page가 발생한다고 해서 디스크 sync를 자주 하게 되면 I/O 작업이 많아져서 시스템 성능이 저하될 수 있습니다. 그래서 커널은 몇 가지 조건을 만들고, 거기에 일치하는 dirty page와 디스크를 동기화시킵니다. dirty page를 언제 얼마나 동기화시키느냐가 바로 성능 튜닝의 요소입니다.
커널 버전마다 다르지만 pdflush, flush, bdflush 등의 스레드가 동기화 작업을 합니다.
* 더티 페이지와 관련된 커널 파라미터
root@compute-2-3:~# sysctl –a | grep –i dirty
vm.dirty_background_ratio = 10 # 백그라운드 동기화 임계치
vm.dirty_ratio = 20 # I/O Trottling 임계치
vm.dirty_background_bytes = 0 # byte와 ratio는 동시 적용 불가하며 하나만 사용 가능
vm.dirty_bytes = 0
vm.dirty_writeback_centisecs = 500 # flush 커널 스레드의 wakeup 주기(ms)
vm.dirty_expire_centisecs = 3000 # 동기화할 페이지의 interval 기준(ms)
- vm.dirty_background_ratio & vm.dirty_background_bytes
dirty page의 내용을 백그라운드로 동기화할 때 기준이 되는 비율입니다. ratio가 10인 경우 메모리의 10%를 의미합니다.
- vm.drity_ratio & vm.dirty_bytes
dirty page가 ratio 값을 초과하면 해당 프로세스의 I/O 작업을 sleep하고 I/O throttling을 수행합니다.
- vm.dirty_writeback_centisecs & vm.dirty_expire_centises
writeback_centisecs는 flush 커널 스레드(kworker 스레드)를 몇 초 간격으로 깨울 것인지 결정하는 값입니다. 10ms 단위로 동작하며, 값이 500이면 5초를 의미합니다. 그리고 expire_centisecs은 생성된지 얼마가 지난 더티 페이지를 동기화할 것인지 결정하는 기준이 됩니다.
위 값의 경우 커널 스레드가 5초마다 깨어나서 생성된지 30초가 지난 더티 페이지들을 동기화합니다.
- vm.dirty_background_ratio & vm.dirty_background_bytes
vm.dirty_writeback_centisecs로 깨어난 Flush Kernel 스레드가 디스크에 sync할 dirty page의 기준을 찾을때 이 값을 사용합니다. 마찬가지로 10ms 단위로 동작하며, 값이 3000이면 drity page로 분류된 페이지들 중 30초 동안 디스크에 동기화되지 않은 페이지들을 동기화합니다.
- 더티 페이지의 동기화와 I/O throttling
각 파라미터들이 어떻게 사용되어 background 동기화가 일어나는지 알아보기 위해 간단한 I/O 작업을 하는 스크립트를 동작시킨 뒤 ftrace를 통해 커널을 추적해 보도록 하겠습니다.
### ftrace 마운트
$ mount -t debugfs debugfs /sys/kernel/debug
$ cd /sys/kernel/debug/tracing
### 현재 사용가능한 tracer의 종류
$ cat available_tracers
hwlat blk mmiotrace function_graph wakeup_dl wakeup_rt wakeup function nop
### 특정 함수를 기준으로 추적
$ echo function > ./current_tracer
### function tracing으로 추적가능한 함수의 목록
$ cat available_filter_functions
### 추적할 함수 필터
$ echo *write* >> set_ftrace_filter
$ echo *dirty* >> set_ftrace_filter
$ echo *wb* >> set_ftrace_filter
$ cat trace
$ cat -v trace_pipe ### 실시간 확인
먼저, 주기적 동기화가 발생하지 않도록 vm.dirty_writeback_centisecs 값을 0으로 설정하고, dirty page가 8MB가 되었을 때 background 동기화가 발생하도록 vm.dirty_background_bytes 값을 아래처럼 설정해줍니다.
$ sysctl -w vm.dirty_writeback_centisecs=0
vm.dirty_writeback_centisecs = 0
$ sysctl -w vm.dirty_background_bytes=8388608
vm.dirty_background_bytes = 8388608
그리고 아래 코드를 실행하여 background 동기화를 발생시키면서 호출되는 함수를 살펴보겠습니다.
#!/usr/bin/python
import time
MEGABYTE = 1024*1024
DUMMY_STR = ' ' * MEGABYTE
i = 0
f = open("/io_test.txt", 'w')
while True:
i += 1
f.write(DUMMY_STR)
print("write File - Current Size: {} KB".format(i*1024))
time.sleep(1)
커널 트레이스의 결과처럼 아래의 순서대로 함수가 호출됩니다.
generic_perform_write() -> balance_dirty_pages_ratelimited() -> balance_dirty_pages()
그리고 wb_wakup() 함수에 의해 flush 커널 워커 스레드가 깨어나 writeback 하게 됩니다.
$ cat -v trace_pipe | grep 'balance_dirty_pages '
test.py-7782 [023] .... 13327571.607452: balance_dirty_pages_ratelimited <-generic_perform_write
test.py-7782 [023] .... 13327571.607453: balance_dirty_pages <-balance_dirty_pages_ratelimited
test.py-7782 [023] .... 13327571.607453: mem_cgroup_wb_domain <-balance_dirty_pages
test.py-7782 [023] .... 13327571.607454: domain_dirty_limits <-balance_dirty_pages
test.py-7782 [023] .... 13327571.607454: dirty_poll_interval.part.23 <-balance_dirty_pages
test.py-7782 [023] .... 13327571.607455: wb_start_background_writeback <-balance_dirty_pages
test.py-7782 [023] .... 13327571.607455: wb_wakeup <-wb_start_background_writeback
...
kworker/u98:1-3893 [015] .... 13327571.607493: wb_workfn <-process_one_work
kworker/u98:1-3893 [015] .... 13327571.607496: wb_over_bg_thresh <-wb_workfn
kworker/u98:1-3893 [015] .... 13327571.607498: mem_cgroup_wb_domain <-wb_over_bg_thresh
kworker/u98:1-3893 [015] .... 13327571.607499: domain_dirty_limits <-wb_over_bg_thresh
kworker/u98:1-3893 [015] .... 13327571.607499: wb_writeback <-wb_workfn
kworker/u98:1-3893 [015] .... 13327571.607500: wb_over_bg_thresh <-wb_writeback
kworker/u98:1-3893 [015] .... 13327571.607500: mem_cgroup_wb_domain <-wb_over_bg_thresh
kworker/u98:1-3893 [015] .... 13327571.607500: domain_dirty_limits <-wb_over_bg_thresh
kworker/u98:1-3893 [015] .... 13327571.607502: __writeback_inodes_wb <-wb_writeback
kworker/u98:1-3893 [015] .... 13327571.607502: writeback_sb_inodes <-__writeback_inodes_wb
kworker/u98:1-3893 [015] .... 13327571.607503: wbc_attach_and_unlock_inode <-writeback_sb_inodes
kworker/u98:1-3893 [015] .... 13327571.607504: __writeback_single_inode <-writeback_sb_inodes
kworker/u98:1-3893 [015] .... 13327571.607504: do_writepages <-__writeback_single_inode
kworker/u98:1-3893 [015] .... 13327571.607505: ext4_writepages <-do_writepages
kworker/u98:1-3893 [015] .... 13327571.607510: clear_page_dirty_for_io <-mpage_submit_page
kworker/u98:1-3893 [015] .... 13327571.607512: ext4_bio_write_page <-mpage_submit_page
kworker/u98:1-3893 [015] .... 13327571.607512: __test_set_page_writeback <-ext4_bio_write_page
kworker/u98:1-3893 [015] d... 13327571.607513: sb_mark_inode_writeback <-__test_set_page_writeback
kworker/u98:1-3893 [015] .... 13327571.607517: wbc_account_io <-ext4_bio_write_page
kworker/u98:1-3893 [015] .... 13327571.607517: clear_page_dirty_for_io <-mpage_submit_page
kworker/u98:1-3893 [015] .... 13327571.607518: ext4_bio_write_page <-mpage_submit_page
kworker/u98:1-3893 [015] .... 13327571.607518: __test_set_page_writeback <-ext4_bio_write_page
kworker/u98:1-3893 [015] .... 13327571.607519: wbc_account_io <-ext4_bio_write_page
generic_perform_write() 함수는 사용자 프로세스에 의해 buffered I/O가 발생하면 write() 시스템콜에 의해 호출됩니다. 그리고 I/O state 구조체인 iocb와 iov_iter(I/O vector iterator) 구조체를 인자로 받아 어플리케이션에서 쓰고자 하는 데이터를 모두 buffered write 할 때까지 아래의 순서로 반복문을 수행합니다.
1. 쓰기 작업을 진행할 page를 찾거나 새로 생성 - write_begin()
2. 사용자 영역의 데이터를 커널 영역의 데이터로 복사 - copy_page_from_iter_atomic()
3. 쓰기를 완료한 페이지를 dirty 상태로 변경하고 복사된 데이터의 양(copied)를 반환 - write_end()
4. 사용자 데이터가 페이지에 성공적으로 쓰여지면 balance_dirty_pages_ratelimited() 함수 호출
/* mm/filemap.c */
ssize_t generic_perform_write(struct kiocb *iocb, struct iov_iter *i) {
struct file *file = iocb->ki_filp;
loff_t pos = iocb->ki_pos;
struct address_space *mapping = file->f_mapping;
const struct address_space_operations *a_ops = mapping->a_ops;
long status = 0;
ssize_t written = 0;
do {
struct page *page;
unsigned long offset; /* Offset into pagecache page */
unsigned long bytes; /* Bytes to write to page */
size_t copied; /* Bytes copied from user */
void *fsdata = NULL;
offset = (pos & (PAGE_SIZE - 1));
bytes = min_t(unsigned long, PAGE_SIZE - offset, iov_iter_count(i));
again:
...
// 파일 포인터와 offset, length 등을 인자로 받아, 해당 파일의 offset에 쓰기가 가능한지 확인.
// 쓰기를 진행할 페이지(*pagep) 반환, Success 이면 return 0.
status = a_ops->write_begin(file, mapping, pos, bytes, &page, &fsdata);
...
// 사용자 프로세스가 쓴 User space의 데이터를 Kernel space의 페이지로 복사.
// copyin() + memcpy()
copied = copy_page_from_iter_atomic(page, offset, bytes, i);
flush_dcache_page(page);
// 쓰기를 완료한 페이지를 dirty로 변경.
// 실패 시 음수를 반환하고, 성공 시 복사된 데이터의 양(copied)를 반환.
status = a_ops->write_end(file, mapping, pos, bytes, copied, page, fsdata);
...
// 사용자 프로세스가 쓴 데이터의 양(bytes)와 페이지에 복사된 데이터의 양(copied)가 다르면
// again부터 다시 수행.
if (unlikely(status == 0)) {
if (copied)
bytes = copied;
goto again;
}
pos += status;
written += status;
// page의 dirty가 발생하였으므로 balance_dirty_pages_ratelimited() 함수 호출.
balance_dirty_pages_ratelimited(mapping);
} while (iov_iter_count(i));
return written ? written : status;
}
프로세스가 dirty page를 생성할 때 마다 동기화를 하는 것은 오버헤드가 크기 때문에, 일정 수치나 비율 이상인 경우에 동기화를 하도록 합니다.
generic_perform_write() 함수에 의해 호출된 balance_dirty_pages_ratelimited() 함수는 현재 실행중인 프로세스의 dirty page 개수(current->nr_dirtied)가 threshold 인 current->nr_dirtied_pause 값보다 큰 경우 balance_dirty_pages() 함수를 호출하여 더티 페이지를 동기화합니다.
여기서 pause 값은 CPU가 I/O 요청을 처리하는 속도에 비해 느린 저장 장치의 쓰기 속도를 고려하는 것입니다. 그리고 이것은 더티 페이지가 vm.dirty_ratio(Hard limit)를 초과하여 I/O throttling에 동작하는 경우 pause 값은 write() 시스템 콜이 얼마만큼 sleep할지 결정하는 period 값(20ms ~ 200ms)으로 사용하여 I/O 속도를 조절한다는 것입니다.
/* mm/page-writeback.c */
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;
// Device의 용량이 부족한지, Device dirver가 cache writeback을 지원하는지 확인.
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;
// 현재 실행중인 프로세스의 task_struct 구조체 내 nr_dirtied_pause 값을 ratelimit로 받음.
ratelimit = current->nr_dirtied_pause;
if (wb->dirty_exceeded)
ratelimit = min(ratelimit, 32 >> (PAGE_SHIFT - 10));
// 커널 스케줄러에 의한 인터럽트 Disable
preempt_disable();
// per CPU 변수 bdp_ratelimits의 값 초기화.
// 과도한 task로 한 개의 CPU core에 너무 많은 nr_dirtied가 쌓이는 것을 방지.
p = this_cpu_ptr(&bdp_ratelimits);
if (unlikely(current->nr_dirtied >= ratelimit))
*p = 0;
else if (unlikely(*p >= ratelimit_pages)) {
*p = 0;
ratelimit = 0;
}
// life cycle이 짧은 task의 페이지를 nr_dirted에 반영(?)
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();
// 현재 프로세스의 dirty page의 개수가 ratelimit 이상이라면 balance_dirty_pages() 함수 호출.
if (unlikely(current->nr_dirtied >= ratelimit))
ret = balance_dirty_pages(wb, current->nr_dirtied, flags);
wb_put(wb);
return ret;
}
/* likely()와 unlikely()는 분기 예측으로 조건문이 주로 참인지 거짓인지에 따라
* 컴파일러에게 힌트를 주어 최적화를 할 수 있습니다.
* likely()는 주로 참이 오는 경우이고,
* 반대로 unlikely()는 주로 거짓이 오는 경우에 사용합니다. */
balance_dirty_pages() 함수는 인자로 받은 bdi 구조체의 'CGROUP_WRITEBACK' 플래그에 따라 글로벌 페이지와 cgroup에서 격리된 페이지를 구분하여 실행합니다. 먼저, 시스템 전체 더티 페이지의 개수와 더티 페이로 사용가능한 글로벌 페이지의 개수, 커널 파리미터의 threshold 값을 dirty_throttle_control(dtc) 구조체에 업데이트합니다. 만약 writeback을 하려는 device가 HDD나 JBOD, USB 등의 느린 매체라면, 개별 device에 맞는 I/O bandwidth로 조정하고 그 값을 dtc 구조체에 업데이트합니다.
/* mm/page-writeback.c */
static int balance_dirty_pages(struct bdi_writeback *wb, unsigned long pages_dirtied, unsigned int flags) {
struct dirty_throttle_control gdtc_stor = { GDTC_INIT(wb) };
struct dirty_throttle_control mdtc_stor = { MDTC_INIT(wb, &gdtc_stor) };
struct dirty_throttle_control * const gdtc = &gdtc_stor;
struct dirty_throttle_control * const mdtc = mdtc_valid(&mdtc_stor) ? &mdtc_stor : NULL;
struct dirty_throttle_control *sdtc;
unsigned long nr_reclaimable; /* = file_dirty */
...
bool dirty_exceeded = false;
unsigned long task_ratelimit;
unsigned long dirty_ratelimit;
struct backing_dev_info *bdi = wb->bdi;
bool strictlimit = bdi->capabilities & BDI_CAP_STRICTLIMIT;
unsigned long start_time = jiffies;
int ret = 0;
for (;;) {
...
// 시스템 전체 더티 페이지의 개수
nr_reclaimable = global_node_page_state(NR_FILE_DIRTY);
// 더티페이지로 사용가능한 global 페이지 개수.
// (NR_FREE_PAGES + NR_INACTIVE_FILE + NR_ACTIVE_FILE – reserved pages – HIGH Memory)
gdtc->avail = global_dirtyable_memory();
gdtc->dirty = nr_reclaimable + global_node_page_state(NR_WRITEBACK);
// vm_dirty_{bytes|ratio}와 dirty_background_{bytes|ratio} 값을
// dirty_throttle_control 구조체에 반영.
domain_dirty_limits(gdtc);
// HDD, JBOD, USB 등 상대적으로 느린 저장장치의 Bandwidth에 맞게 조정.
if (unlikely(strictlimit)) {
wb_dirty_limits(gdtc);
// 시스템 전체 dirty가 아닌 개별 저장장치에 대한 dirty(wb_dirty) 기준 적용.
dirty = gdtc->wb_dirty;
thresh = gdtc->wb_thresh;
bg_thresh = gdtc->wb_bg_thresh;
} else {
dirty = gdtc->dirty;
thresh = gdtc->thresh;
bg_thresh = gdtc->bg_thresh;
}
...
domain_dirty_limists() 함수는 '/proc/sys/vm' 경로에 선언된 커널 파라미터 값을 받아 dtc구조체에 업데이트합니다.
/* mm/page-writeback.c */
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;
// PAGE_SIZE는 page의 크기와 같은 4KB.
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;
...
// vm_dirty_{background}_bytes와 vm_dirty_{background}_ratio 중 1가지 Thres만 설정 가능.
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;
// background threshold가 Hard limit보다 크다면, dirty 값의 절반으로 적용.
if (bg_thresh >= thresh)
bg_thresh = thresh / 2;
...
// 계산된 thresh 값을 dirty_throttle_control 구조체에 업데이트.
dtc->thresh = thresh;
dtc->bg_thresh = bg_thresh;
...
그리고 각 변수들의 값이 업데이트되면 nr_reclaimable(시스템 전체 더티페이지 개수) 값이 background threshold(bg_thres)보다 크다면 wb_start_background_writeback() 함수를 호출하며 백그라운드 동기화를 실행합니다.
static int balance_dirty_pages(struct bdi_writeback *wb, unsigned long pages_dirtied, unsigned int flags) {
...
// 더티 페이지로 사용 가능한 페이지의 수가 vm.dirty_background_ratio의 값보다 크다면
// wb_start_background_writeback() 함수로 해당 블록 디바이스에 해당하는 inode들의
// 더티 페이지를 writeback.
if (!laptop_mode && nr_reclaimable > gdtc->bg_thresh && !writeback_in_progress(wb))
wb_start_background_writeback(wb);
// laptop_mode = vm.laptop_mode (절전모드?)
...
balance_dirty_pages() 함수에서 호출된 wb_start_background_writeback() 함수는 flush 커널 워커 스레드를 깨우고 bdi 구조체를 인자로 넘겨줍니다.
/* mm/fs-writeback.c */
void wb_start_background_writeback(struct bdi_writeback *wb) {
...
// flush 커널 스레드를 깨움.
wb_wakeup(wb);
}
그리고 깨어난 kworker 스레드는 wb_workfn() 함수를 시작으로 wb_writeback() 함수를 통해 writeback을 수행합니다.
/* fs/fs-writeback.c */
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);
...
}
static long wb_do_writeback(struct bdi_writeback *wb) {
struct wb_writeback_work *work;
long wrote = 0;
set_bit(WB_writeback_running, &wb->state);
// bdi_writeback 구조체에 연결된 wb_writeback_work 리스트를 순회하며 writeback.
while ((work = get_next_work_item(wb)) != NULL) {
trace_writeback_exec(wb, work);
wrote += wb_writeback(wb, work); // 명시적 동기화 수행. (sync)
finish_writeback_work(wb, work);
}
wrote += wb_check_start_all(wb);
wrote += wb_check_old_data_flush(wb); // 주기적 동기화 수행.
wrote += wb_check_background_flush(wb); // 백그라운드 동기화 수행.
clear_bit(WB_writeback_running, &wb->state);
return wrote;
}
struct wb_writeback_work {
long nr_pages;
struct super_block *sb;
enum writeback_sync_modes sync_mode;
unsigned int tagged_writepages:1;
unsigned int for_kupdate:1;
unsigned int range_cyclic:1;
unsigned int for_background:1;
unsigned int for_sync:1; /* sync(2) WB_SYNC_ALL writeback */
unsigned int auto_free:1; /* free on completion */
enum wb_reason reason; /* why was writeback initiated? */
struct list_head list; /* pending work list */
struct wb_completion *done; /* set if the caller waits */
};
wb_do_writeback() 함수는 bdi_writeback 구조체를 받아 아래 그림처럼 연결리스트인 wb_writeback_work 구조체를 순회하며 각 item을 동기화합니다. 이때 wb_writeback() 함수는 wb_writeback_work 구조체에 선언된 flag에 따라 그에 맞는 동기화 작업을 수행합니다.
다시 balacne_dirty_pages() 함수로 돌아오겠습니다. 백그라운드 동기화가 진행된 이후 더티 페이지의 개수가 '(thresh + bg_thresh) / 2' 값보다 작다면 현재 프로세스의 task 구조체 내 더티 페이지 개수를 초기화하고, dirty_poll_interval() 함수를 통해 ((log₂(thresh – dirty) / 2)만큼 pause값을 업데이트하고 종료합니다.
반대로 더티 페이지의 개수가 해당 값보다 크다면 pause 값은 balance_dirty_pages() 함수가 인자로 받은 글로벌 더티 페이지의 개수와 bdi 구조체에 선언된 개별 저장 장치의 dirty_ratelimit을 기준으로 계산된 period 값으로 적용됩니다. 만약 pause 값이 balance_dirty_pages() 함수를 호출할 만큼 크지만 min_pause보다 작은 경우라면 balance_dirty_pages() 함수의 잦은 호출로 인한 I/O 성능저하로 이어질 수 있으므로 break합니다.
위 조건에 해당되지 않는다면 동기화로 writeback하는 속도보다 더티 페이지를 생성하는 속도가 더 빠른 경우로 간주하여 I/O throttling을 수행하여 해당 시간만큼 write()를 sleep합니다.
/* mm/page-writeback.c */
static int balance_dirty_pages(struct bdi_writeback *wb, unsigned long pages_dirtied, unsigned int flags) {
...
// 더티 페이지가 해당 값 이하라면 I/O throttling을 진행하지 않고 종료.
// dirty_freerun_ceiling(thresh, bg_thresh) = (thresh + bg_thresh) / 2
if (dirty <= dirty_freerun_ceiling(thresh, bg_thresh) && (!mdtc || m_dirty <= dirty_freerun_ceiling(m_thresh, m_bg_thresh))) {
unsigned long intv;
unsigned long m_intv;
free_running:
intv = dirty_poll_interval(dirty, thresh); // ((log₂(thresh – dirty)) / 2)
m_intv = ULONG_MAX;
current->dirty_paused_when = now;
current->nr_dirtied = 0;
if (mdtc)
m_intv = dirty_poll_interval(m_dirty, m_thresh);
// global과 cgroup의 interval 중 작은 값을 pause로 업데이트.
current->nr_dirtied_pause = min(intv, m_intv);
break;
}
...
dirty_exceeded = (gdtc->wb_dirty > gdtc->wb_thresh) && ((gdtc->dirty > gdtc->thresh) || strictlimit);
wb_position_ratio(gdtc);
sdtc = gdtc;
...
// bdi 구조체에 dirty_exceeded flag 반영
if (dirty_exceeded != wb->dirty_exceeded)
wb->dirty_exceeded = dirty_exceeded;
...
dirty_ratelimit = READ_ONCE(wb->dirty_ratelimit);
task_ratelimit = ((u64)dirty_ratelimit * sdtc->pos_ratio) >> RATELIMIT_CALC_SHIFT;
max_pause = wb_max_pause(wb, sdtc->wb_dirty);
min_pause = wb_min_pause(wb, max_pause, task_ratelimit, dirty_ratelimit, &nr_dirtied_pause);
...
// 인자로 받은 글로벌 더티 페이지 개수와 스토리지의 dirty rate를 기준으로 pause 값 업데이트.
period = HZ * pages_dirtied / task_ratelimit;
pause = period;
if (current->dirty_paused_when)
pause -= now - current->dirty_paused_when;
// pause 값이 작아서 balacing이 자주 발생하면 I/O 성능 저하로 이어질 수 있음.
if (pause < min_pause) {
...
if (pause < -HZ) {
current->dirty_paused_when = now;
current->nr_dirtied = 0;
} else if (period) {
current->dirty_paused_when += period;
current->nr_dirtied = 0;
} else if (current->nr_dirtied_pause <= pages_dirtied)
current->nr_dirtied_pause += pages_dirtied;
break;
}
if (unlikely(pause > max_pause)) {
now += min(pause - max_pause, max_pause);
pause = max_pause;
}
pause:
...
wb->dirty_sleep = now;
io_schedule_timeout(pause); // pause만큼 더티 페이지 생성 제한.
current->dirty_paused_when = now + pause;
current->nr_dirtied = 0;
current->nr_dirtied_pause = nr_dirtied_pause;
...
}
return ret;
}
- 더티 페이지 동기화와 iostat
아래는 초당 1GB씩 쓰기 작업을 하면서 iostat을 확인한 통계입니다. flush 커널 스레드가 깨어나기 전까지 %util 값이 낮은 상태를 유지하다가 writeback이 발생하면 %util 값이 순간적으로 증가하게 됩니다.
# iostat -mx -p sdb 1
avg-cpu: %user %nice %system %iowait %steal %idle
3.81 0.00 0.89 94.09 0.00 1.22
Device r/s w/s rMB/s wMB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
sdb 1.00 69.00 0.50 0.42 0.00 0.00 0.00 0.00 2.00 0.06 0.01 512.00 6.27 1.14 8.00
avg-cpu: %user %nice %system %iowait %steal %idle
3.28 0.00 2.03 93.47 0.00 1.22
Device r/s w/s rMB/s wMB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
sdb 0.00 193.00 0.00 111.73 0.00 4.00 0.00 2.03 0.00 5.94 1.15 0.00 592.80 1.60 30.80
avg-cpu: %user %nice %system %iowait %steal %idle
3.10 0.00 1.33 94.33 0.00 1.24
Device r/s w/s rMB/s wMB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
sdb 0.00 561.00 0.00 499.09 0.00 3.00 0.00 0.53 0.00 9.29 5.21 0.00 911.00 1.78 100.00
avg-cpu: %user %nice %system %iowait %steal %idle
5.92 0.00 1.43 91.49 0.00 1.17
Device r/s w/s rMB/s wMB/s rrqm/s wrqm/s %rrqm %wrqm r_await w_await aqu-sz rareq-sz wareq-sz svctm %util
sdb 0.00 554.00 0.00 498.58 0.00 2.00 0.00 0.36 0.00 9.15 5.07 0.00 921.56 1.81 100.00
- 결론
threshold 또는 cemtosecs의 값이 작으면 flush 커널 스레드가 너무 자주 꺠어나서 스케줄링에 대한 오버헤드가 발생할 수 있습니다. 반대로 값이 크면 I/O %util 값이 높아집니다.
- Reference
https://medium.com/@hyoje420/linux-ftrace-%EC%82%AC%EC%9A%A9%EB%B2%95-31b4dc7ac93c
[Linux] ftrace 사용법
medium 플랫폼에서 앞으로 공부하는 것들을 하나씩 정리해 나갈 예정이다. 예쁜 디자인을 적용하는 것에 어려움을 느끼는 나로써는 글 자체에 집중할 수 있는 플랫폼인것 같아서 상당히 마음에
medium.com
https://www.xml.com/ldd/chapter/book/ch02.html
Linux Device Drivers, 2nd Edition: Chapter 2: Building and Running Modules
It's high time now to begin programming. This chapter introduces all the essential concepts about modules and kernel programming. In these few pages, we build and run a complete module. Developing such expertise is an essential foundation for any kind of m
www.xml.com
https://kkikyul.tistory.com/90
[운영체제] [리눅스 커널 5.0 동작과정 이해와 tracing 실습] 3일차 - Write/Read/Open 처리 과정 이해 (Writ
| Write/Read/Open 처리 과정 이해 [정리] 1. 주제 - Linux Write/Read/Open의 처리 과정에 대한 이해 2. 전체 내용 요약 1) Write [1) Write] I/O에 대해 크게 2가지로 direct i/o와 buffered i/o 나눠짐 무조건적으로 block la
kkikyul.tistory.com
https://oslab.kaist.ac.kr/wp-content/uploads/esos_files/publication/conferences/korean/WC_ojt.pdf
https://www.quora.com/Where-does-VFS-lie-in-the-Linux-Kernel
Where does VFS lie in the Linux Kernel?
Answer: Virtual file system (VFS) or Virtual filesystem switch is an abstraction layer on top of a more concrete file system. The purpose of a VFS is to allow for client applications to access different types of concrete file systems in a uniform way. A VF
www.quora.com
https://www.slideshare.net/AdrianHuang/linux-kernel-virtual-file-system
Linux Kernel - Virtual File System
Linux Kernel - Virtual File System - Download as a PDF or view online for free
www.slideshare.net
https://students.mimuw.edu.pl/ZSO/Wyklady/08_VFS1/VFS-1.pdf
https://lwn.net/Articles/404439/
Kernel development [LWN.net]
Brief items The current development kernel is 2.6.36-rc4, released on September 12. "Nothing in particular stands out, although there's been more noise in GPU development than I'd like at this point (both Radeon and i915). But that should hopefully all be
lwn.net
https://junsoolee.gitbook.io/linux-insides-ko/summary/concepts/linux-cpu-1
Per-CPU variables - linux-insides-ko
만약 첫번째 청크 할당자가 PCPU_FC_PAGE라면, 우리는 pcpu_embed_first_chunk 대신에 pcpu_page_first_chunk를 사용할 것입니다. 그 percpu 영역이 할당된 이후, 우리는 percpu 오프셋과 그 세그먼트를 설정합니다.
junsoolee.gitbook.io
https://www.kernel.org/doc/Documentation/this_cpu_ops.txt
https://lwn.net/Articles/648292/
Writeback and control groups [LWN.net]
Did you know...?LWN.net is a subscriber-supported publication; we rely on subscribers to keep the entire operation going. Please help out by buying a subscription and keeping LWN on the net. By Jonathan Corbet June 17, 2015 LinuxCon Japan "Writeback" is th
lwn.net
https://zhuanlan.zhihu.com/p/93480009
/proc/sys/vm下的那些dirty*
今天,简单介绍一下/proc/sys/vm下的那些以dirty开头的参数具体有什么作用。 首先,这些参数都定义在kernel/sysctl.c中,下面依次进行介绍。 1)dirty_background_bytes/dirty_background_ratio dirty_bytes/dirty…
zhuanlan.zhihu.com
linux io过程自顶向下分析
前言 IO是操作系统内核最重要的组成部分之一,它的概念很广,本文主要针对的是普通文件与设备文件的IO原理,采用自顶向下的方式,去探究从用户态的fread,fwrite函数到底层的数据是如何被
wjqwsp.github.io
https://blog.csdn.net/weixin_33963189/article/details/92562519
linux io过程自顶向下分析-CSDN博客
前言 IO是操作系统内核最重要的组成部分之一,它的概念很广,本文主要针对的是普通文件与设备文件的IO原理,采用自顶向下的方式,去探究从用户态的fread,fwrite函数到底层的数据是如何
blog.csdn.net
'System Engineering > Linux' 카테고리의 다른 글
[커널이야기] TCP handshake와 TIME_WAIT 소켓 (0) | 2023.09.23 |
---|---|
[Linux] NTPv4(RFC5905)와 chrony 그리고 timex (0) | 2023.07.21 |
[커널이야기] 리눅스 메모리 1 - 메모리를 확인하는 방법과 slab/swap 메모리 (2) | 2023.06.15 |
[커널이야기] Load Average로 시스템 콜 추적하기 (0) | 2023.06.09 |
폐쇄망 환경에서 휴대폰 테더링으로 yum/apt 사용하기 (0) | 2023.04.20 |