뚝딱뚝딱 블로그
리눅스 스터디 #4 메모리 관리 시스템 본문
메모리 관리 정보 수집하기
시스템에 설치된 메모리 용량, 사용 중인 메모리 용량은 free 명령어로 확인이 가능하다.
$ free
total used free shared buff/cache available
Mem: 7729028 1552460 4177240 536648 1999328 5375580
Swap: 0 0 0
free 명령어를 통해 얻은 정보는 아래와 같다.
필드명 | 의미 |
total | 시스템에 설치된 전체 메모리 용량. |
free | 명목상 비어 있는 메모리(자세한 건 available 참고) |
buff/cache | 버퍼, 캐시, 페이지 캐시가 이용하는 메모리. 시스템의 비어 있는 메모리(free 필드 값)가 줄어들면 커널이 해제시킴 |
available | 실제로 사용 가능한 메모리. free 필드값과 비어 있는 메모리가 줄어 들었을 때 해제 가능한 커널 내부 메모리 영역(ex - 페이지 캐시) 크기를 더한 값 |
used | 시스템이 사용 중인 메모리에서 buff/cache를 뺀 값 |
total | |||||||
커널이 사용중 | |||||||
해제 불가능 | available | ||||||
해제 가능 | free | 프로세스가 사용중 | |||||
buff/cache |
used = total - free -buff/cache
used
used 값은 프로세스가 사용하는 메모리 + 커널이 사용하는 메모리를 모두 포함함. used는 프로세스 메모리 사용량에 따라 늘어나고, 종료되면 커널은 해당 프로세스의 메모리를 모두 해제한다.
buff/cache
페이지 캐시와 버퍼 캐시가 사용하는 메모리 용량을 뜻함. 접근 속도가 느린 저장 장치에 있는 파일 데이터를 접근 속도가 빠른 메모리에 일시적으로 저장해서 접근 속도가 빨라진 것처럼 보이게 하는 커널 기능임. 저장 장치에 있는 파일 데이터를 읽어와서 메모리에 데이터를 캐시한다. 파일 작성시 변하게 되는 값.
sar -r 명령어
$ sar -r 1 5 # 5초동안 1초 간격으로 메모리 정보 데이터 수집
Linux 5.4.-74-generic (coffee) 12/04/2021 _x86_64_(8 CPU)
03:08:24 PM kbmemfree kbavail kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive kbinact kbdirty
03:08:25 PM 4225432 5460160 1397816 18.09 67080 1869944 6410488 65.24 722640 2019192 152
...
free 명령어 필드 | sar -r 명령어 필드 |
total | 해당 사항 없음 |
free | kbmemfree |
buff/cache | kbuffers + kbcached |
available | 해당 사항 없음 |
메모리 재활용 처리
시스템 부하가 높아지는 경우 -> free 영역의 메모리가 줄어들게 된다.
이때 커널은 재활용 가능한 메모리 영역(즉, 커널에서 해제 가능한 메모리 영역)을 해제하여 free 값을 늘려준다.
예를 들면, 디스크에서 data를 읽고 변경이 일어나지 않은 페이지 캐시. 이런 경우, 동일한 data가 disk에 존재하니, 메모리를 해제시킨다.
프로세스 삭제와 메모리 강제 해제
재활용 가능한 메모리를 해제하다가, 더이상 메모리 부족이 해결되지 않는 경우, 시스템은 Out Of Memory(OOM)상태가 된다.
이런 경우 메모리 관리 시스템이 적당히 프로세스를 골라서 강제 종료 시키고, 메모리에 빈 공간을 늘린다(free 영역 늘림). OOM Killer에 의해 이루어진다.
OOM Killer가 동작하는 시스템이라면, 메모리가 충분하지 않을 수 있는데, 동시 실행 중인 프로세스 개수를 줄여서 메모리 사용량을 줄이거나, 메모리를 추가로 설치하거나.. 아니면 메모리가 충분한데 OOM Killer가 동작한다면 프로세스/커널에 메모리 누수가 일어나고 있을지도 모른다.
가상 메모리
가상 메모리가 없을 때 생기는 문제점
가상 메모리가 없이 메모리를 직접 관리한다고 생각해보자.
- 메모리 단편화
- 멀티 프로세스 구현이 어려움
- 비정상적인 메모리 접근
메모리 단편화
프로세스를 생성하고 메모리 확보/해제 작업을 반복하다 보면 메모리 단편화 문제가 발생한다.
주소 0: 커널이나 다른 프로세스 등에서 사용중인 메모리 |
주소 400: 비어있는 영역 (100) |
주소 500: 프로세스 메모리 |
주소 700: 비어 있는 영역 (100) |
주소 800: 프로세스 메모리 |
주소 900: 비어 있는 영역 (100) |
위와 같은 메모리 상태인 경우, 비어 있는 영역이 100 바이트 사이즈 간격이기 때문에 100바이트 보다 큰 사이즈의 메모리를 할당하여 사용할 수 없다.
왜냐하면, 프로그램이 메모리를 확보할 때마다 확보한 메모리가 몇 개의 영역으로 쪼개져 있는지 일일히 관리하고 트래킹 해야한다.
또한, 100바이트보다 큰 연속된 데이터 묶음, 300바이트짜리 배열을 작성하려는 용도로 사용할 수 없음.
멀티 프로세스 구현이 어려움
주소 0: 커널이나 다른 프로세스 등에서 사용중인 메모리 |
주소 300: 프로세스 A 코드 |
주소 400: 프로세스 A 데이터 |
주소 500: 빈 영역 (500) |
프로세스 A 실행시 코드 영역이 주소 300 - 400이고, data가 주소 400 - 500으로 매핑된다고 생각해보자.
위의 메모리 상태에서, 동일한 프로그램을 실행하여 프로세스 B가 실행되었다고 생각해보자. 그같은 주소영역에 코드와 data가 위치하게 될텐데, 이미 프로세스 A가 사용하고 있어 불가능하다. 억지로 다른 장소 (ex - 주소 500부터 700)로 매핑해서 동작시켜도 명령어나 데이터가 가리키는 메모리 주소가 달라서 올바르게 동작하지 않을 것이다.
즉, 동시에 여러 프로그램이 실행되려면, 사용자가 모든 프로그램의 배치 장소가 겹치지 않도록 의식해서 관리해야한다.
비정상적인 메모리 접근
다른 프로세스, 커널에 할당된 메모리 주소를 지정하여 사용하게 된다면, 자신이 사용하는 영역이 아님에도 불구하고 다른 메모리 영역에도 비정상적으로 접근하여 데이터 누출/손상의 위험성이 생긴다.
가상메모리 기능
가상 메모리는 프로세스가 메모리에 접근할때 시스템에 설치된 메모리에 직접 접근하는 대신에 가상 주소를 사용해서 간접적으로 접근하는 기능이다.
가상 주소에 대비되는 시스템에 설치된 메모리의 실제 주소는 물리 주소라고 하며, 이런 주소를 사용해서 접근 가능한 범위를 주소 공간이라고 한다.
가상 주소 공간 | 물리 메모리(할당된 메모리) |
0 - 300(프로세스가 사용할 수 있는 주소 공간) | 500 - 800 (실제 총 범위는 0 - 1000) |
위의 예제 상태에서, 프로세스가 주소공간 100에 접근한다면 주소 변환을 통해 실제 물리 메모리 주소 600에 접근하게 될 것이다.
readelf나 cat /proc/<pid>/maps 출력 결과에 나오는 주소는 실제로 모두 가상 주소이다. 프로세스에서 실제 메모리에 직접 접근하는 방법, 물리 주소를 직접 지정하는 방법은 없다.
페이지 테이블
가상 주소를 물리 주소로 변환하려면 커널 메모리 내부에 저장된 페이지 테이블을 사용한다. CPU는 모든 메모리를 페이지 단위로 쪼개서 관리하는데, 주소는 페이지 단위로 변환됨.
페이지 테이블에서 한 페이지에 대응하는 데이터를 페이지 테이블 엔트리라고 부른다. 페이지 테이블 엔트리는 가상 주소와 물리 주소 대응 정보를 포함한다.
예를 들어 가상 주소 공간 0 - 300을 사용하는 위와 같은 프로세스가 있다고 하자.
페이지 테이블은 아래와 같을 것이다.
가상 주소 | 물리 주소 |
0 - 100 | 500 - 600 |
100 - 200 | 600 - 700 |
200 - 300 | 700 - 800 |
페이지 테이블은 커널이 작성한다. 커널은 프로세스 생성 시 프로세스 메모리를 확보하고 확보한 메모리에 실행 파일 내용을 복사한다. 동시에 프로세스용 페이지 테이블도 작성한다. 프로세스가 가상 주소에 접근할 때 물리 주소로 변환하는 건 CPU가 하는 작업이다.
만약 할당받은 주소 외에 300 이상의 주소를 접근하면 어떻게 될까?
페이지 테이블 엔트리에는 페이지와 대응하는 물리 메모리의 존재 여부를 관리하는 데이터가 있어서 해당 페이지에 접근하는 순간 CPU에서 page fault라고 하는 exception이 발생한다.
CPU에서 실행 중인 명령이 중단되고, 커널 메모리에 배치된 페이지 폴트 핸들러 처리가 실행된다.
커널은 페이지 폴트 핸들러를 이용해서 프로세스가 비정상적인 메모리 접근을 일으켰다는 걸 감지한다. 이후 SIGSEGV 시그널을 프로세스에 보낸다. 보통 해당 시그널을 받은 프로세스는 강제 종료된다.
가상 메모리로 문제 해결하기
메모리 단편화
메모리 단편화가 일어나더라도, 페이지 테이블을 잘 설정하면 프로세스 가상 주소공간에서는 커다란 하나의 영역으로 다룰 수 있다.
가상 주소 | 물리 주소 |
0 - 100 | 400 - 500 |
100 - 200 | 700 - 800 |
200 - 300 | 900 - 1000 |
멀티 프로세스 구현이 어려움
가상주소 공간은 프로세스마다 만들어지기 때문에, 각자의 프로그램이 다른 프로그램과 주소가 중복되는 걸 피할 수 있다.
프로세스 A의 페이지 테이블
가상 주소 | 물리 주소 |
0 - 100 | 500 - 600 |
100 - 200 | 600 - 700 |
200 - 300 | 700 - 800 |
프로세스 B의 페이지 테이블
가상 주소 | 물리 주소 |
0 - 100 | 800 - 900 |
100 - 200 | 900 - 1000 |
비정상적인 메모리 접근
프로세스마다 가상 주소 공간이 있다는 말은 애초에 다른 프로세스의 메모리가 어떻게 되어있는지 알 수가 없어 접근할 수 없다는 뜻임.
커널 메모리도 보통 가상주소 공간에 매핑되지 않기 때문에 비정상적인 접근을 할 수 없다.
프로세스에 새로운 메모리 할당하기
일반적으로 커널이 프로세스에 메모리를 할당하는 절차는 다음과 같은 순서로 시스템 콜을 호출할 것이다.
1. 프로세스에서 N바이트 메모리가 필요하다고 시스템 콜 호출로 커널에 요청
2. 커널은 시스템의 비어 있는 메모리에서 N바이트 영역 확보
3. 확보한 메모리 영역을 프로세스의 가상 주소 공간에 매핑
4. 가상 주소 공간의 시작 위치 주소를 프로세스에 돌려줌
하지만 메모리는 확보한 순간 당장 사용하기보다는 조금 시간이 지난 후에 사용하는 일이 많아서, 리눅스는 두단계 절차로 메모리를 확보한다.
1. 메모리 영역 할당: 가상 주소 공간에 새롭게 접근 가능한 메모리 영역을 매핑한다.
2. 메모리 할당: 확보한 메모리 영역에 물리 메모리를 할당한다.
메모리 영역 할당: mmap() 시스템 콜
동작 중인 프로세스에 새로운 메모리 영역을 할당하려면 mmap() 시스템 콜을 사용한다. 시스템 콜을 호출하면 커널 메모리 관리 시스템이 프로세스의 페이지 테이블을 변경하고, 요청된 크기만큼 영역을 페이지 테이블에 추가로 매핑하고 매핑된 영역의 시작 주소를 프로세스에 돌려준다.
/proc/<pid>/maps 명령어를 통해 특정 프로세스가 메모리를 확보한 경우, 주소값 차이의 계산을 통해 얼만큼의 영역을 할당받았는지 알 수 있다.
메모리 할당: Demand paging
mmap() 시스템 콜을 호출한 직후라면 새로운 메모리 영역에 대응하는 물리 메모리는 아직 존재하지 않는다. 새롭게 확보한 페이지에 처음으로 접근할때, 물리 메모리를 할당하는데, 이런 방식을 Demand paging이라고 한다. 해당 방식을 구현하기 위해 메모리 관리 시스템이 페이지마다 해당 페이지의 물리 메모리 할당 여부 상태를 관리한다.
예를 들어, 아래 200 - 300 주소에 해당하는 100바이트를 추가로 확보했다고 가장하자.
가상 주소 | 물리 주소 |
0 - 100 | 500 - 600 |
100 - 200 | 600 - 700 |
200 - 300 | 아직은 없음 -> 프로세스가 접근하고 나서야 커널에 의해 물리 주소할당 |
프로세스가 새로할당 받은 페이지에 접근하게 되면,
1. 프로세스가 페이지에 접근함.
2. 페이지 폴트가 발생
3. 커널의 페이지 폴트 핸들러가 동작해서, 페이지에 대응하는 물리 메모리 할당함.
페이지 폴트 핸들러는 page table entry가 존재하지 않는 페이지에 접근하면 프로세스에 SIGSEGV를 보내지만, page table entry는 존재해도 대응하는 물리 메모리가 할당되지 않은 경우 -> 새로운 메모리를 할당하는 처리로 분기한다.
sar -r 명령어를 통해 시스템 메모리 변화를 볼 수 있는데,
메모리 영역을 확보해도 해당 영역에 접근하지 않으면 메모리 사용량(kbmemused 필드값)은 변하지 않는다.
또한 확보한 메모리 영역에 접근했을 때는 초당 페이지 폴트 횟수를 뜻하는 fault/s 필드값은 증가한다.
페이지 테이블 계층화
페이지 테이블은 얼마나 많은 메모리를 소비할까?
x86_64 아키텍처라면, 가상 주소 공간 크기는 128TiB까지, 1페이지 크기는 4KiB, 페이지 페이블 엔트리 크기는 8바이트가 된다.
단순계산으로 프로세스 1개당 페이지테이블에 256GiB(=8바이트X128TiB/4KiB)가 필요하게 된다. 실제 시스템 메모리는 16GiB인데, 어떻게 다루고 있는 것일까?
사실 페이지 테이블은 1차원 구조가 아니라, 메모리 소비량이 적은 계층화 구조를 사용한다.
만약 1페이지가 100바이트이고, 가상주소가 1600 바이트인 예제가 있다고 생각해보자.
가상 주소 | 물리 주소 |
0 - 100 | 500 - 600 |
100 - 200 | 600 - 700 |
200 - 300 | 700 - 800 |
300 - 400 | 800 - 900 |
... | ... |
하지만 4페이지가 한 단위인 2단구조로 계층형 페이지 테이블을 사용한다면?
가상 주소 | 하위 페이지 테이블 |
0 - 400 | 페이지 테이블 A를 가리킴 |
400 - 800 | X |
800 - 1200 | X |
1200 - 1600 | X |
페이지 테이블 A
가상 주소 | 물리 주소 |
0 - 100 | 500 - 600 |
100 - 200 | 600 - 700 |
200 - 300 | 700 - 800 |
300 - 400 | 800 - 900 |
이렇게 하면, 100바이트 단위의 16개 페이지 테이블 엔트리를 관리하다가 -> 8개로 줄어든다.
가상 메모리 용량이 어느 크기 이상이 되면 계층형 페이지 테이블이 평탄한 페이지 테이블보다 메모리 사용량이 많아진다고 한다. 하지만 그런 경우는 매우 드물고, 모든 프로세스의 페이지 테이블에 필요한 합계 메모리 용량은 평탄한 페이지 테이블보다 계층형 페이지 테이블 쪽인 작은 경우가 대부분이다.
실제 하드웨어에서 x86_64 아키텍처라면 4단 구조 페이지 테이블을 사용한다고 함.
Huge Page
앞에서 말한것처럼 2단구조를 사용하는데, 모든 페이지를 다 사용하고 있다고 생각해보자.
그럼 첫번째단 페이지 테이블의 4개 엔트리 + 4개의 페이지 테이블 엔트리 * 4 = 즉 20개의 페이지 테이블 엔트리를 사용하게된다.
Huge Page를 사용한다면
가상 주소 | 물리 주소 |
0 - 400 | 400 - 800 |
400 - 800 | 800 - 1200 |
800 - 1200 | 1200 - 1600 |
1200 - 1600 | 1600 - 2200 |
이렇게 표현할 수 있게되는데, 페이지 테이블 엔트리 갯수가 20개에서 -> 4개로 준다. 페이지 테이블이 사용하는 메모리 사용량도 줄고, fork()에서 페이지 테이블을 복사하는 비용도 줄어서 fork()가 빨라지는 효과도 기대할 수 있다.
mmap() 함수의 flags인수에 MAP_HUGETLB 플래그를 지정해서 사용 가능하다고 함. DB나 가상 머신 매니저처럼 가상 메모리를 대량으로 사용하는 소프트웨어라면 Huge Page를 필요에 따라 사용해보면 된다.
Transparent Huge Page(THP)
위에서 말했듯, 일일이 플래그를 통해 Huge Page를 요청한다면 프로그래머 입장에서 번거로울 수 있다. 해당 문제를 해결하기 위해 리눅스에는 Transparent Huge Page라는 기능을 제공한다.
이 기능은 가상 주소 공간 내부에 있는 연속된 여러 4KiB 페이지가 어떤 조건을 만족하면 그걸 하나로 묶어서 자동으로 Huge Page로 바꾼다. 하지만 여러 페이지를 합쳐서 Huge Page를 만드는 처리 비용, 다시 분할하는 처리 때문에 부분적으로 성능이 떨어지는 경우가 있다. 따라서 해당 기능을 사용할지 여부는 시스템 관리자가 선택적으로 결정한다.
/sys/kernel/mm/transparent_hugepage/enabled 파일을 보면 알 수 있고, 3종류의 값을 설정할 수 있다.
- always: 시스템에 존재하는 프로세스의 모든 메모리를 대상으로 유효화한다.
- madvise: madvise() 시스템 콜에 MADV_HUGEPAGE 플래그를 설정하면 지정한 메모리 영역에서만 유효화한다.
- never: 무효화한다.
우분투 20.04는 기본값이 madvise라고 함.
'Linux' 카테고리의 다른 글
리눅스 스터디 #6 장치 접근 (1) | 2024.12.16 |
---|---|
리눅스 스터디 #5 프로세스 관리(응용) (0) | 2024.12.16 |
리눅스 스터디 #3 프로세스 스케줄러 (2) | 2024.12.11 |
리눅스 스터디 #2 프로세스 관리 (1) | 2024.12.01 |
리눅스 스터디 #1 개요 (1) | 2024.12.01 |