목차
- 서론
- 메모리 재활용 처리
- OOM(Out Of Memory)
- 가상 주소(Virtual Address)
- 어셈블리로 알아보는 가상 주소
- 페이지 테이블
- 페이지 테이블 계층화
- 결론
- Reference
서론
컴퓨터 시스템에서 메모리 관리의 효율성은 성능과 안정성에 중요한 영향을 미친다.
특히, 메모리 재활용 기법은 시스템의 원활한 운영을 위해 필수적이다.
이 글에서는 메모리 사용 현황을 확인하는 방법, OOM(Out Of Memory) 상황과 그 해결 방법, 가상 주소 체계, 페이지 테이블과 Huge Page의 개념을 정리하여 효과적인 메모리 관리 전략을 이해하고자 한다.
메모리 재활용 처리
free 명령어를 통해 메모리 사용 현황을 알 수 있다.
$ free
total used free shared **buff/cache** available
Mem: 2032756 57016 1946872 68 28868 1885784
Swap: 1048576 0 1048576
주목할 항목은 다음 3가지다.
- free: 명목상 비어있는 메모리
- available: 실제로 사용 가능한 메모리
- buff/cache: 버퍼 및 페이지 캐시 메모리
여기서 buff/cache 항목은 free로 분류되며, available과 합하면 free의 값이 나온다.
해당 영역은 커널이 사용하는 공간이지만
시스템 부하가 높아져 여유 공간이 없어지면(free 메모리가 줄어듦)
이 buff/cache 영역을 일부 해제해 free로 전환한다.
그럼에도 충분한 메모리 공간이 없다면 Out Of Memory 에러가 발생해
OOM Killer를 실행한다.
TMI) 커널의 메모리 정책
커널은 기본적으로 낙관적으로 메모리를 할당한다.
접근하기 전까진 아무도 모른다.(슈뢰딩거의 메모리)
그래서 malloc 함수의 매뉴얼을 보면 반환 값이 NULL이 아니더라도
해당 주소를 사용하지 못할 수 있다고 설명돼있다.
NULL 발생 시, 메모리 공간이 부족하다면 OOM Killer를 호출하지만
반대의 경우, 할당받은 공간을 사용할 수 없으면 OOM Killer를 호출한다.
OOM(Out Of Memory)
메모리를 할당할 공간이 없을 때 발생하는 에러
C malloc에 대한 설명을 보면, 메모리 공간이 부족하다 판정되면
OOM Killer가 “적당한” 프로세스를 종료시킨다.
아무 프로세스나 종료시키진 않을 것이지만 그 기준이 궁금해 개인적으로 찾아봤다.
oom_score_adj이 메모리 및 스왑 영역 사용량에 따른 허용 메모리 범위를 계산한다.
-1000~1000점 까지를 매겨, 값이 클수록 종료 우선순위가 높다.
이 OOM 관련 이슈에 대한 글
https://engineering.linecorp.com/ko/blog/prometheus-container-kubernetes-cluster
2.6
이전엔 다음 요소들로 측정했다.
점수가 올라가는 요소(종료 가능성⬆️)
- fork로 생성한 자식 프로세스가 많음
- 우선순위가 낮음(nice 값이 20에 가까움)
점수가 내려가는 요소(종료 가능성⬇️)
- 장시간 실행된 프로세스 or 많은 CPU 시간 사용
- root 권한으로 실행(UID = 0)=
- 하드웨어와 직접적으로 연결됨
Java는 JVM -XX 옵션을 통해 OOM 대처를 할 수 있다.
하지만, 메인 스레드 자체가 부족하면 아무것도 못하고 종료된다.
대표적으로 2가지가 있다.
ExitOnOutOfMemoryError: JVM 즉시 종료
CrashOnOutOfMemoryError: 코어 덤프를 만들고 종료
가상 주소(Virtual Address)
가상화된 주소를 통해 물리 메모리를 간접적으로 접근하는 방법
만약 물리 메모리에 직접 접근해야 한다면 다음 문제들로 관리 비용이 증가한다.
- 메모리 단편화
- 멀티 프로세스 구현 어려움
- 비정상적인 메모리 접근
파일 포맷에 명시된 각 영역들이 차지하는 공간만 알게 된다면
OS가 적당한 공간에 영역들을 load 해, 사용자들의 메모리 관리 부담을 줄인다.
어셈블리로 알아보는 가상 주소
가상 주소 부분이 어셈블리 단에서 변수 접근과 유사하다 생각해 개인적으로 정리했다.
덧셈 연산을 하는 C 코드를 어셈블리어로 표현한 결과다.
#include <stdio.h>
int main() {
int num1 = 5, num2 = 10, sum;
sum = num1 + num2;
printf("두 숫자의 합: %d\n", sum);
return 0;
}
어셈블리 코드
(gdb) disas main
Dump of assembler code for function main:
=> 0x149 <+0>: endbr64
0x14d <+4>: push rbp - 1
0x14e <+5>: mov rbp,rsp
0x151 <+8>: sub rsp,0x10
0x155 <+12>: mov DWORD PTR [rbp-0xc],0x5 - 2
0x15c <+19>: mov DWORD PTR [rbp-0x8],0xa - 3
0x163 <+26>: mov edx,DWORD PTR [rbp-0xc]
0x166 <+29>: mov eax,DWORD PTR [rbp-0x8]
0x169 <+32>: add eax,edx
0x16b <+34>: mov DWORD PTR [rbp-0x4],eax - 4
0x16e <+37>: mov eax,DWORD PTR [rbp-0x4]
...
0x182 <+57>: call 0x555555555050 <printf@plt>
0x187 <+62>: mov eax,0x0
0x18c <+67>: leave
0x18d <+68>: ret
End of assembler dump.
1번: main 함수의 시작점 등록
2번: 변수 num1에 int 5 초기화
3번: 변수 num2에 int 10 초기화
4번: 변수 sum에 num1 + num2 결과 초기화
여기서 rbp는 함수 시작 주소에 대한 레지스터로 가상 주소를 가리키고 있다.
변수 num1, num2, sum 변수는 구체적인 주소를 명시하지 않고
rbp에서 4만큼, 8만큼, 12만큼 뺀 위치라고 명시돼있다.
이렇게 프로세스는 가상 주소를 통해 상대 위치만으로 원하는 데이터에 접근할 수 있다.
페이지 테이블
커널 메모리 내부에 저장된 가상 주소 ↔ 물리 주소 매핑 테이블
cpu는 메모리를 페이지 단위로 관리하고, 페이지 크기는 아키텍처마다 다르다.
intel x64 기준 4KB이다.
페이지 테이블은 프로세스의 각 영역별로 테이블을 만드는 게, 아니라
페이지와 매핑되는 물리 주소(프레임)에 대한 테이블을 만든다.
물리 메모리 상에서는 프로세스 데이터가 연속된 위치에 없을 수 있지만
우리는 가상 주소로 프로세스를 보기 때문에 메모리가 연속된 것처럼 보인다.
즉, 페이지 테이블 엔트리 ↔ 물리 메모리 대응 여부를 관리한다.
만약, 프레임이 할당되지 않은 가상 주소로 접근하면 페이지 폴트가 발생한다.
커널은 새로운 물리 메모리 프레임을 매칭하거나, 매칭할 수 없다면 오류를 반환한다.
메모리 단위에 개인적으로 찾아본 결과 프레임이란 단위도 있었다.
- 프레임: 물리 메모리 단위
- 페이지: 가상 메모리 단위
한 프레임엔 여러 페이지가 매핑될 수 있지만 특정 시점에 페이지가 프레임에 할당되지 않을 수 있다고 한다.
페이지 테이블 계층화
실제 페이지 테이블은 여러 차원으로 구성돼있다.
가상 주소 범위를 두고, 해당 범위의 하위 페이지 테이블에 매핑하는 방식이다.
처음 설명을 봤을 때 다음 생각이 들었다.
- 결국 생성되는 페이지 테이블 개수는 같음
- 간접 참조 비용 증가 (하위 페이지를 거쳐가므로)
설명을 다시 읽어보니 다음 특징이 있었다.
- 필요한 만큼의 페이지 테이블만 생성
- 계층형 구조가 손해일 정도로 메모리 사용량이 많아지는 경우는 드묾
만약 한 프로세스가 사용하는 메모리가 많아지면?
운영 환경에선 프로세스들이 사이좋게 최소한의 메모리만 가져다 쓰지 않는다.
상황에 따라서 한 프로세스의 메모리 점유율이 점점 올라가는 상황도 발생한다.
이렇게 되면 계층화 구조의 장점이 무용지물이 되고
쓸데없이 용량, 간접 참조 비용만 축내게 된다.
이런 상황에 대비해 Huge Page란 기법이 있다.
말 그대로 기존의 페이지 테이블보다 더 큰 구조이다.
구조는 간단한다.
아래 그림처럼 기존 페이지 테이블보다 더 많은 범위로 묶고, 바로 물리 주소와 매핑한다.
일일이 Huge Page 만들기 번거롭다면?
간혹 여러 프로세스들의 메모리 점유율이 증가하는 경우가 있다.
Huge Page를 만들어야 할지 말아야 할지 고민하는 프로그래머를 위해
OS에서는 Transparent Huge Page(THP)란 기능을 제공해, 특정 조건을 만족하면 페이지 테이블을 묶어 Huge Page로 만든다.
하지만 생성, 분할 비용이 있기 때문에 시스템 관리자가 사용 여부를 결정해야 한다.
다음 경로에서 THP 여부를 확인할 수 있다.
$ sudo cat /sys/kernel/mm/transparent_hugepage/enabled
[sudo] password for brorica:
[always] madvise never
THP 옵션은 3가지 상태를 가진다.
- always: 시스템에 존재하는 모든 프로세스
- madvise: madvise()
시스템 콜에 MADV_HUGEPAGE
플래그 설정한 프로세스만
- never: 무효
우분투 20.04는 기본 값이 madvise이다.
결론
메모리 관리는 시스템 성능 최적화와 안정성을 위해 필수적인 요소이다.
free 명령어를 통해 현재 메모리 상태를 파악하고, OOM 발생 시 프로세스 종료 기준을 이해하면 예상치 못한 상황에 대비할 수 있다.
또한, 가상 주소와 페이지 테이블의 개념을 이해해 OS가 어떻게 메모리 관리를 효율적으로 다루는지 알 수 있으며
Huge Page 및 THP 개념을 통해 갑작스러운 메모리 사용량 증가를 어떻게 대처하는지 알 수 있다.
메모리 관리에 주로 사용되는 개념을 통해 운영 환경에서 안정적인 운영이 가능할 것이다.
Reference
https://stackoverflow.com/questions/1119134/how-do-malloc-and-free-work
https://linux.die.net/man/3/malloc
https://linux.die.net/man/5/proc
https://stackoverflow.com/questions/12096403/java-shutting-down-on-out-of-memory-error
https://engineering.linecorp.com/ko/blog/prometheus-container-kubernetes-cluster
'개발' 카테고리의 다른 글
그림으로 배우는 리눅스 구조 3주차 (0) | 2025.04.10 |
---|---|
그림으로 배우는 리눅스 구조 2주차 (0) | 2025.03.29 |
그림으로 배우는 리눅스 구조 1주차 (0) | 2025.03.16 |
[JPA] FindAll이 같은 값만 나와요 (2) | 2025.02.15 |
[PostgreSQL] FAQ를 번역해 보았다. (스압) (2) | 2025.01.29 |