free 명령이 숨기고 있는 것들
아래 내용은 DevOps와 SE를 위한 리눅스 커널 이야기 4장 free 명령이 숨기고 있는 것들을 읽고 정리한 내용입니다.
command free
vagrant@ubuntu-xenial:~$ free -m
total used free shared buff/cache available
Mem: 3951 46 3700 5 204 3677
Swap: 0 0 0
total
: 전체 메모리양used
: 사용하고 있는 메모리의 양shared
: 프로세스 사이에 공유하고 있는 메모리의 양buffers
: 버퍼 용도로 사용하고 있는 메모리 양 (시스템 성능 향상을 위해 커널에서 사용하는 영역)cached
: 페이지 캐시라고 불리는 캐시 영역에 있는 메모리 양 (I/O 작업을 더 빠르게 하기 위해 사용하는 영역)
-/+ buffers/cache:
칸이 제가 실행한 명령 결과에서는 보이지 않지만 표시될 경우 두 번째 줄에 표시되는 이 줄은 buffers와 cache를 제외한 영역에 대해 보여줍니다. 이 buffers와 cache를 제외한 영역을 왜 보여주는지 이해하기 위해 먼저 커널의 동작에 대해 살펴봅니다
과거의 free 명령의 출력에서는 "buffers/cache" 행이 사용되어 버퍼와 캐시의 메모리 사용량을 따로 나타냈지만, 최근 버전에서는 이 행이 사라지고 "available" 행으로 대체되어 전체 사용 가능한 메모리를 보여주게 되었습니다.
buffers와 cached 영역
커널은 블록 디바이스라고 부르는 디스크로부터 데이터를 읽거나 사용자의 데이터를 디스크에 저장합니다. 하지만 디스크는 다른 장치들에 비해 매우 느리기 때문에 디스크에 대한 요청을 기다리는 시간이 상당히 많이 소요되고 이로 인해 시스템에 부하가 일어나기도 합니다. 커널은 이렇게 상대적으로 느린 디스크에 대한 요청을 좀 더 빠르게 하기 위해 메모리의 일부를 디스크 요청에 대한 캐싱 영역으로 할당해서 사용합니다. 즉 한번 읽은 디스크의 내용을 메모리에 저장해 두어서, 동일한 내용을 읽고자 하면 디스크로 요청하지 않고 메모리로 요청하며 이런 캐싱 기능을 통해서 커널은 다수의 디스크 요청을 좀 더 빠르게 처리할 수 있게 됩니다. 그리고 이때 사용되는 캐싱 영역을 buffers, cached라고 부릅니다.
Page Cache와 Buffer Cache
커널은 블록 디바이스에서 데이터를 읽을 때 데이터가 위치한 특정 블록의 주소를 넘겨주고 블록 디바이스는 해당 블록 주소의 데이터를 커널에 전달합니다. 이 과정에 디바이스 드라이버도 존재합니다.
Page Cache (cache)
- 파일의 내용을 저장
- 커널이 읽어야 할 데이터가 파일의 내용이라면 커널은 bio 구조체를 만들고 해당 구조체에 Page Cache 용도로 할당한 메모리 영역을 연결해줍니다. 그리고
bio 구조체
는 디바이스 드라이버와 통신하여 디스크로부터 데이터를 읽어 Page Cache에 파일의 내용을 채웁니다.
Buffer Cache (buffers)
- 파일 시스템의 메타 데이터를 담고 있는 블록을 저장
- super block, inode block처럼 파일의 내용이 아닌 파일 시스템을 관리하기 위한 메타 데이터를 읽어올 때는 bio 구조체를 사용하지 않고
_get_blk()
와 같은 내부 함수를 통해 블록 디바이스와 직접 통신하며 이때 가져온 블록 디바이스의 특정 블록 내용을 Buffer Cache 영역에 저장합니다.
- 커널은 가용영역 중 일부를 cache 영역으로 사용
- 애플리케이션에서 사용하게 되는 영역이 점차 넓어져 가용영역의 메모리를 가져다가 사용 (Cache 영역이 충분히 있어야 I/O 성능 효과를 받을 수 있으므로)
- 사용영역이 점차 커져 일정 수준 이상이 되면 커널은 Cache 영역으로 사용하던 영역을 애플리케이션이 사용할 수 있도록 메모리 관리 시스템에 반환 (Cache 영역 감소, 사용 영역 증가)
- 점차 더 이상 반환할 메모리도 없고 가용할 메모리가 없어지는 순간 시스템은 swap 영역을 사용하고 시스템의 성능은 줄어듭니다.
/proc/meminfo
자세한 메모리 현황을 보기 위해서는 /proc/meminfo 파일을 확인할 수 있습니다.
vagrant@ubuntu-xenial:~$ cat /proc/meminfo
MemTotal: 4046356 kB
MemFree: 3790044 kB
MemAvailable: 3766664 kB
Buffers: 10820 kB
Cached: 168252 kB
SwapCached: 0 kB # 1
Active: 122736 kB
Inactive: 76772 kB
Active(anon): 23256 kB # 2
Inactive(anon): 5260 kB # 3
Active(file): 99480 kB # 4
Inactive(file): 71512 kB # 5
Unevictable: 3652 kB
Mlocked: 3652 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 0 kB # 6
...
- SwapCached: swap으로 빠진 메모리 영역 중 다시 메모리로 돌아온 영역, swapCache가 발생하는 경우는 시스템에 메모리가 부족하면 커널은 프로세스의 주소 공간 중 swap 영역으로 이동시킬 수 있는 메모리를 선택해서 swap 영역으로 이동시킵니다. 이 과정에서 I/O가 일어나기 때문에 성능 저하가 발생하며 그 후 다시 메모리가 확보되서 swap 영역으로 빠졌던 영역이 다시 메모리로 돌아가는 데 커널은 swap 영역에서 해당 메모리를 삭제하지 않습니다.
- Active(anon): Anone은 Anonymous를 의미, 특정 파일의 내용을 저장하고 있는 Page Cache 영역을 제외한 메모리 영역 즉, 프로세스들이 사용하는 메모리 영역
- InActive(anon): 비교적 참조된 지 오래되어 swap 영역으로 이동될 수 있는 메모리 영역
- Active(file): buffers와 cached 영역, I/O 성능 향상을 위해 사용하는 영역
- InActive(file): buffers와 cached 영역, 비교적 참조된지 오래되어 swap 영역으로 이동될 수 있는 메모리 영역
- Dirty: 커널은 기본적으로 I/O 쓰기 요청이 발생했을 때 바로 블록 디바이스로 명령을 내리지 않고 일정량이 될 때까지 모았다가 한 번에 쓰는 일종의 지연 쓰기 작업을 하는데 I/O 성능 향상을 위해 커널이 캐시 목적으로 사용하는 영역 중 쓰기 작업이 이루어져서 실제 블록 디바이스의 목록에 씌어져야 할 영역
Active와 InActive를 구분
fs/proc/meminfo 내용을 그림으로 표현하면 아래와 같음
기본적으로 프로세스가 메모리 할당을 요청하면 해당 메모리의 페이지가 Active 리스트에 연결되고 메모리 할당이 실패하거나 메모리가 부족하게 되면 kswapd
혹은 커널 내부에서 try_to_free_pages()
함수를 통해서 LRU 리스트에 있는 메모리들을 확인한다. 이 과정에서 Active 리스트에 있던 페이지가 Inactive 리스트로 옮겨가거나 Inactive 리스트에 있던 페이지가 해제되어 다른 프로세스에게 할당되는 작업이 이루어진다.
- 자주 사용되는 메모리 영역은 Active
- 자주 사용되지 않는 메모 리 영역은 InActive
본문 예제에서는 c언어의 malloc() 함수로 10번의 메모리 할당 후에 10분간 sleep하도록 하였지만 Active 리스트에 있는 메모리가 InActive 리스트로 옮겨지지 않았다. 즉, 메모리 부족 현상이 발생해 해제해야 하는 메모리를 찾아야 할 때 커널은 LRU 리스트를 확인한다.
커널이 LRU 리스트를 확인하도록 이 책에서는 최소 free 메모리 할당량을 높게 설정(sysctl -w vm.min_free_kbytes=6553500
)한 뒤에 top 명령어로 kswapd 데몬이 실행되면서 Active 영역에 있는 페이지 중 오래된 페이지를 우선적으로 Inactive로 옮긴 후 메모리를 해제하는 작업을 관측하였다.
Slab (커널 영역)
vagrant@ubuntu-xenial:~$ cat /proc/meminfo
...
Slab: 30324 kB
SReclaimable: 17512 kB
SUnreclaim: 12812 kB
- Slab: 메모리 영역 중 커널이 직접 사용하는 영역
- SReclaimable: Slab 영역 중 재사용될 수 있는 영역, 캐시 용도로 사용하는 메모리들이 주로 여기에 포함
- SUnreclaim: Slab 영역 중 재사용될 수 없는 영역, 커널이 현재 사용중인 영역으로 해제하여 다른 용도로 사용 불가
vagrant@ubuntu-xenial:~$ sudo slabtop -o
Active / Total Objects (% used) : 111359 / 112394 (99.1%)
Active / Total Slabs (% used) : 4189 / 4189 (100.0%)
Active / Total Caches (% used) : 73 / 120 (60.8%)
Active / Total Size (% used) : 29406.76K / 29964.71K (98.1%)
Minimum / Average / Maximum Object : 0.01K / 0.27K / 8.00K
OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
22168 22168 100% 0.12K 652 34 2608K kernfs_node_cache
20118 20118 100% 0.19K 958 21 3832K dentry
11424 11424 100% 0.56K 816 14 6528K inode_cache
6912 6912 100% 0.03K 54 128 216K kmalloc-32
5376 5376 100% 0.06K 84 64 336K kmalloc-64
4125 4125 100% 1.05K 275 15 4400K ext4_inode_cache
3840 3840 100% 0.02K 15 256 60K kmalloc-16
3162 3162 100% 0.04K 31 102 124K ext4_extent_status
3072 3072 100% 0.01K 6 512 24K kmalloc-8
2975 2975 100% 0.05K 35 85 140K ftrace_event_field
2640 2640 100% 0.20K 132 20 528K vm_area_struct
2613 2613 100% 0.10K 67 39 268K buffer_head
1950 1792 91% 0.62K 78 25 1248K proc_inode_cache
1936 1395 72% 0.25K 121 16 484K kmalloc-256
1887 1835 97% 0.08K 37 51 148K anon_vma
1722 1722 100% 0.09K 41 42 164K kmalloc-96
1680 1680 100% 0.07K 30 56 120K Acpi-Operand
1659 1659 100% 0.19K 79 21 316K kmalloc-192
모든 프로세스는 작업을 하기 위해 메모리가 필요하고 이는 커널도 예외가 아니다. I/O 작업을 조금이라도 더 빠르게 하기 위해 inode cache, dentry cache등을 사용하거나, 네트워크 소켓을 위한 메모리 영역을 확보하거나 하는 작업들은 커널(디스크 드라이버)이 하게 되는데 이 과정에서 메모리가 필요하다. 하지만 메모리를 할당해 주는 버디 시스템은 4KB의 페이지 단위로 메모리를 할당한다.
버디 시스템을 통해서 페이지 크기인 기본 4KB의 영역을 할당 받은 후에 각각의 캐시 크기에 맞게 영역을 나눠서 사용한다.
- 버디 시스템: 외부 단편화가 발생하지 않도록 물리적 메모리를 다양한 크기의 블록으로 나누고 부분 사용, 미사용 또는 완전 사용 여부를 추적하여 요청을 받으면 추적할 수 있는 가장 작은 여유 블록을 찾고 필요한 경우 더 작게 분할하여 프로세스에 메모리를 할당
- Slab 영역 중 가장 많이 사용되는 캐시는
dentry
와inode_cache
가 존재 dentry
: 디렉터리의 계층 관계를 저장해 두는 캐시inode_cache
: 파일의 inode에 대한 정보를 저장해 두는 캐시
디렉터리의 계층 관계를 저장해 두는 캐시 dentry를 활용하여 sudo slabtop -o | grep -i dentry
로 캐시 사이즈를 확인하고 ls -al 또는 cd 명령어로 디렉토리를 이동한 뒤에 동일하게 slabtop 명령어를 실행하면 사이즈가 증가된 것을 확인할 수 있습니다.
Slab 할당자는 free 명령에서 used로 계산되기 때문에 프로세스들이 사용하는 메모리 영역을 모두 더하고도 used와 맞지 않을 경우 Slab 메모리에서 누수가 발생하는 것일 수도 있습니다.
메모리의 동작을 확인할 때는 free 명령 뿐만 아니라 /proc/meminfo를 통해서 좀 더 정확한 정보를 수집할 수 있습니다.