brunch

You can make anything
by writing

C.S.Lewis

by 강진우 May 27. 2016

리눅스의 페이지 캐시와 버퍼 캐시

ftrace로 커널 읽기 - #2

아마 리눅스를 운영하시는 분들은 free 명령을 입력한 후 한 번쯤은 이런 궁금증을 가지셨을 겁니다.

buffers와 cached는.. 뭐지?

라는 궁금증이죠. 저도 처음 업무를 하면서부터 벌써 몇 년째 가지고 있던 궁금증이었고, 무엇을 의미하는지는 대충 알았지만 조금 더 명확하게 알고 싶다는 생각을 했었습니다.

바로 이런 출력 결과가..

그리고 이 글에서 그동안 제가 조사하고 알아본 내용을 토대로 버퍼 캐시와 페이지 캐시에 대해서 이야기해 볼까 합니다. 잘못된 부분이 있으면 언제든지 말씀해 주세요.


페이지 캐시


먼저 페이지 캐시에 대한 이야기를 해 볼까 합니다. 리눅스는 파일 I/O의 성능 향상을 위해 페이지 캐시라는 메모리 영역을 만들어서 사용합니다. 상대적으로 많이 느린 디스크로의 접근을 최대한 피하기 위해 사용되는 메모리 영역입니다. 한 번 읽은 파일의 내용을 페이지 캐시라는 영역에 저장시켜 놨다가 다시 한 번 동일한 파일 접근이 일어나면 디스크에서 읽지 않고 페이지 캐시에서 읽어서 제공해 주는 방식입니다. 이를 통해 디스크 접근을 줄일 수 있고 파일에 대한 빠른 접근을 가능하게 합니다. 간단하게 테스트해 볼까요?

파일을 읽기 전의 메모리 양을 free 명령으로 확인해 봅니다.

테스트 전의 free 결과

cached 영역이 78516 KB 임을 확인할 수 있습니다. cat 명령을 이용해서 24 KB 정도 되는 파일을 읽은 후의 결과는 어떨까요?

테스트 후의 free 결과

 24 KB 만큼 cached 영역이 늘어났습니다. 간단한 테스트니까 직접 해보셔도 됩니다.

조금 더 자세히 살펴볼까요? ftrace를 통해서 트레이스를 걸고 난 결과를 살펴보겠습니다. 일부분은 생략했습니다.

cat-2681  [001]  3740.770826: do_sync_read <-vfs_read             
cat-2681  [001]  3740.770826: generic_file_aio_read <-do_sync_read             
cat-2681  [001]  3740.770826: generic_segment_checks <-generic_file_aio_read       
cat-2681  [001]  3740.770827: find_get_page <-generic_file_aio_read             
cat-2681  [001]  3740.770827: page_cache_sync_readahead <-generic_file_aio_read       cat-2681  [001]  3740.770828: __page_cache_alloc <-__do_page_cache_readahead
cat-2681  [001]  3740.770828: alloc_pages_current <-__page_cache_alloc             
cat-2681  [001]  3740.770829: __alloc_pages_nodemask <-alloc_pages_current           cat-2681  [001]  3740.770829: get_page_from_freelist <-__alloc_pages_nodemask   
cat-2681  [001]  3740.770830: mm_page_alloc: page=ffffea0007870ad0 pfn=2255190 order=0 migratetype=2 gfp_flags=GFP_HIGHUSER_MOVABLE|GFP_COLD|GFP_NOWARN|GFP_NORETRY 
cat-2681  [001]  3740.770830: ext4_readpages <-__do_page_cache_readahead 
cat-2681  [001]  3740.770830: mpage_readpages <-ext4_readpages             
cat-2681  [001]  3740.770830: add_to_page_cache_lru <-mpage_readpages             
cat-2681  [001]  3740.770831: add_to_page_cache_locked <-add_to_page_cache_lru cat-2681  [001]  3740.770831: mem_cgroup_cache_charge <-add_to_page_cache_locked         
cat-2681  [001]  3740.770841: submit_bio <-mpage_bio_submit             
cat-2681  [001]  3740.770841: generic_make_request <-submit_bio             
cat-2681  [001]  3740.770841: _cond_resched <-generic_make_request            
cat-2681  [001]  3740.770842: block_remap: 8,0 R 34425376 + 8 <- (8,3) 13042208           

천천히 보시면 vfs_read() 가 호출되고 find_get_page()를 통해 해당 영역이 페이지 캐시에 있는지를 확인합니다. 아마도 한 번 열었던 파일이라면 여기서 I/O 요청이 끝이 나겠죠. 그렇지 않다면 __page_cache_alloc() 함수를 이용해서 해당 파일의 내용을 저장할 페이지 캐시 영역을 할당받습니다. 그 후 실제 블록 디바이스로의 읽기 요청이 진행되는데요, 블록 디바이스로의 요청을 진행하기 위해 bio 구조체를 초기화하고 bio 구조체의 멤버 변수에 위에서 할당받은 페이지 캐시를 추가합니다. 

bio 구조체는 블록 디바이스와의 읽기/쓰기 작업을 위해 사용하는 구조체입니다.

그리고 cat을 통해 같은 파일을 한 번 더 읽으면 아래와 같은 ftrace 결과를 볼 수 있습니다.

cat-2381  [001]  2296.226064: generic_file_aio_read <-do_sync_read
cat-2381  [001]  2296.226064: generic_segment_checks <-generic_file_aio_read
cat-2381  [001]  2296.226064: find_get_page <-generic_file_aio_read
cat-2381  [001]  2296.226064: mark_page_accessed <-generic_file_aio_read
cat-2381  [001]  2296.226064: file_read_actor <-generic_file_aio_read
cat-2381  [001]  2296.226064: copy_user_generic <-file_read_actor
cat-2381  [001]  2296.226065: put_page <-generic_file_aio_read

앞선 결과와는 다르게 find_get_page() 함수 다음으로 mark_page_accessed() 함수가 호출되는 것을 볼 수 있습니다. 이는 페이지 캐시에서 해당 데이터를 찾았다는 것을 의미하고, 이렇게 찾은 메모리 영역에 대해 참조했음을 표시합니다. 이 작업은 Active LRU List, Inactive LRU List를 관리하기 위해 필요한 작업입니다.

함수 호출 관계

생략한 부분들이 있긴 하지만 위와 같은 플로우를 따라서 I/O가 일어난다고 할 수 있습니다.


버퍼 캐시


그렇다면 버퍼 캐시의 용도는 무엇일까요? 버퍼 캐시는 블록 디바이스가 가지고 있는 블록 자체에 대한 캐시입니다. 커널이 데이터를 읽기 위해서는 블록 디바이스의 특정 블록에 접근해야 하는데, 이때 해당 블록에 대한 내용을 버퍼 캐시에 담아두고, 동일한 블록에 접근할 경우에는 블록 디바이스까지 요청을 하지 않고 버퍼 캐시에서 바로 읽어서 보여 줍니다. 여기까지 보면 페이지 캐시와 비슷한 용도 임을 알 수 있습니다. 사실 2.4 이전의 커널은 버퍼 캐시와 페이지 캐시가 명확하게 분리되어 있었습니다. 하지만 위에서 언급했던 것에서 알 수 있듯이 대부분의 블록은 데이터를 저장하고 있는 블록이기 때문에 파일의 내용이 버퍼 캐시에도 존재하고 페이지 캐시에도 존재하게 되는 이중 캐시의 문제가 있었습니다. 그래서 2.4 버전 이후부터 버퍼 캐시는 페이지 캐시 내부로 통합이 되어 그 존재가 많이 작아졌습니다.

하지만 그럼에도 불구하고 버퍼 캐시는 여전히 커널에 남아 있습니다. 어떤 용도로 남아 있을까요? 앞에서 이야기한 것처럼 파일 읽기/쓰기를 하기 위해서는 bio 구조체를 이용해야 하는데 파일 I/O 중에 bio를 사용하지 않는 연산들이 있습니다. 바로 super block을 읽거나 inode block 등을 읽을 때입니다. 해당 블록들은 크기가 작기 때문에 bio 구조체와 같은 별도의 구조체를 두는 것보다는 기존처럼 블록 디바이스에 대한 직접 접근이 성능이 더 좋습니다. 

그럼 커널 코드 흐름을 살펴보겠습니다. 이번엔 /proc/meminfo 에서 출발해 보겠습니다.

fs/proc/meminfo.c

36번째 줄을 보면 si_meminfo()라는 함수를 호출해서 데이터들을 구조체에 저장하는 것을 볼 수 있습니다.

mm/page_alloc.c

2129번째 줄에서 보면 si_meminfo() 함수는 bufferram의 값을 nr_blockdev_pages()로 채워 넣는 것을 볼 수 있습니다. 

fs/block_dev.c

이제야 어느 부분을 세는지 찾았습니다. bdev라는 블록 디바이스 장치에 연결된 bd_inode에서 사용하는 메모리 영역의 페이지 수였습니다. 이제 해당 영역을 어디서 할당하는지를 찾아보면 되겠네요.

해당 메모리 영역은 grow_dev_pages()라는 함수에서 할당하고 있습니다.

fs/buffer.c

1008번 줄을 보면 bdev->bd_inodeinode 변수에 저장한 후에 해당 inode가 사용하는 i_mapping 영역에 대해 페이지를 할당하는 것을 볼 수 있습니다. 이 함수는 grow_buffers()라는 함수에서 호출하며 호출 관계를 주욱 따라가다 보면 아래와 같은 순서를 만날 수 있습니다.

버퍼 캐시 호출 순서

위 함수들은 언제 호출이 될까요? 대표적으로 ext4 파일 시스템의 경우 ext4_readdir() 함수에서 ext4_bread()를 호출하고 ext4_getblk() 함수를 호출하게 됩니다. 즉 디렉터리를 읽고자 하는 경우에 디렉터리에 포함된 파일들의 inode 블록들을 블록 디바이스로부터 읽은 후 버퍼 캐시에 저장하는 것입니다. 실제로 ls 등의 명령을 입력하면 free 명령의 buffers 결과가 달라지는 것을 확인할 수 있습니다.


마치며


이번 글에서는 페이지 캐시와 버퍼 캐시에 대해 다뤄 봤습니다. 결론을 정리하자면 아래와 같이 할 수 있습니다.

버퍼 캐시는 파일 시스템의 메타 데이터와 관련된 블록들을 저장하고 있는 캐시이며, 페이지 캐시는 파일의 내용을 저장하고 있는 캐시이다. 

해당 영역들은 그야말로 캐시 용도로 사용하기 때문에 프로세스에게 할당할 메모리가 부족해지면 자연스럽게 반환되어 해제됩니다. 그렇기 때문에 free 명령에서도 두 번째 줄을 통해서 buffers/cached 영역을 제외한 부분을 가용 메모리로 보여 주고 있습니다.

이상으로 페이지 캐시와 버퍼 캐시에 대한 글을 마치겠습니다. 긴 글 읽어 주셔서 감사합니다.

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari