티스토리 뷰
프로그램이 실행되기 위해서는 메모리 관리가 필수적이다. 현대 운영체제는 가상 메모리 시스템을 사용하여 각 프로세스에게 독립적인 메모리 공간을 제공함으로써 효율적이고 안전한 프로그램 실행 환경을 구축한다. 이번 글에서는 프로그램이 실행될 때 메모리가 어떻게 구성되고 관리되는지, 그리고 가상 메모리 시스템이 어떻게 작동하는지 자세히 살펴보고자 한다.
프로세스 메모리 레이아웃
프로그램이 실행되어 프로세스가 되면 운영체제는 해당 프로세스에게 독립적인 가상 주소 공간을 할당한다. 이 주소 공간은 여러 세그먼트로 나뉘어지며, 각 세그먼트는 서로 다른 종류의 데이터를 저장한다.
텍스트 영역 (코드 영역)
실행 가능한 프로그램 코드가 저장되는 영역으로, 컴파일된 바이너리 코드를 포함하며 프로세서가 실행할 명령어들이 위치한다. 텍스트 세그먼트는 프로그램이 실행 중에 자신의 코드를 수정하는 것을 방지하기 위해 읽기 전용으로 설정되어 있다. 만약 프로그램이 자신의 코드를 변경할 수 있다면 악성 코드가 주입되어 시스템에 심각한 위협이 될 수 있기 때문이다.
텍스트 세그먼트는 여러 프로세스 간에 공유될 수 있기도 하다. 예를 들어 여러 사용자가 동시에 같은 워드 프로세스를 실행할 때, 각 프로세스는 동일한 텍스트 세그먼트를 공유함으로써 메모리를 절약할 수 있다. 텍스트 세그먼트의 크기는 프로그램의 코드 크기에 따라 결정되며, 일단 프로그램이 로드되면 실행 중에는 변경되지 않는다.
데이터 영역
초기화된 전역 변수와 정적 변수가 저장되는 영역이다. C나 C++ 같은 언어에서 명시적으로 초기값이 지정된 전역 변수들 (예를 들면 int counter = 10;과 같은)이 위치한다. 데이터 영역은 크게 두 부분으로 나뉜다.
- 읽기 전용 데이터 영역에는 상수와 같이 프로그램 실행 중에 변경되지 않는 값들을 저장한다.
- 읽기-쓰기 데이터 영역에는 프로그램 실행 중에 값이 변경될 수 있는 초기화된 전역 변수나 정적 변수를 포함한다.
데이터 영역의 내용은 실행 파일에서 직접 로드되며, 프로그램이 시작될 때 이미 해당 초기값으로 설정되어 있다. 데이터 영역의 크기는 프로그램에서 사용하는 초기화된 전역 및 정적 변수의 양에 따라 결정된다.
BSS 영역
BSS(Block Started by Symbol) 영역은 초기화되지 않은 전역 변수와 정적 변수가 저장되는 영역이다. 가장 큰 특징은 실행 파일에 실제 내용을 저장할 필요가 없다는 점이다. BSS 영역의 변수들은 프로그램이 실행을 시작할 때 운영체제에 의해 자동으로 0으로 초기화된다. 이렇게 함으로써 초기화되지 않은 변수들이 예측 불가능한 값(쓰레기값)을 가지는 것을 방지한다. 또한 실행 파일의 크기를 줄일 수 있다는 장점이 있다. BSS 영역의 크기 정보는 실행 파일의 헤더에 포함되어 있으며, 프로그램 로드 시 운영체제가 이 정보를 바탕으로 적절한 크기의 메모리를 할당한다.
힙 (Heap)
프로그램 실행 중에 동적으로 메모리를 할당받기 위한 영역이다. C에서는 malloc(), calloc(), realloc() 함수를, C++에서는 new 연산자, Java에서는 객체 생성 등을 통해 힙 메모리를 할당받는다. 힙은 프로그램의 필요에 따라 크기가 늘어나거나 줄어들 수 있는 유연한 메모리 영역이다.
힙의 특징 중 하나는 낮은 메모리 주소에서 높은 메모리 주소 방향으로 성장한다는 점이다. 즉, 더 많은 메모리가 필요해질수록 힙은 위쪽으로 확장된다. 힙 메모리는 프로그래머가 명시적으로 할당하고 해제해야 하며, 제대로 관리되지 않으면 메모리 누수나 댕글링 포인터(dangling pointer)와 같은 문제가 발생할 수 있다. (이러한 문제들을 방지하기 위해 현대 프로그래밍 언어들은 가비지 컬렉션과 같은 자동 메모리 관리 메커니즘을 도입하고 있다.)
메모리 매핑 영역
파일이나 공유 라이브러리가 메모리에 직접 매핑되는 영역이다. mmap() 시스템 콜을 사용하여 파일을 메모리에 매핑하거나 프로세스 간 통신을 위한 공유 메모리 영역을 생성할 때 사용된다. 동적 라이브러리(.so, .dll, .dylib 등)도 이곳에 로드된다.
메모리 매핑의 주요 이점은 파일 I/O를 최적화할 수 있다는 점이다. 파일을 메모리에 매핑하면 파일 내용을 직접 메모리처럼 접근할 수 있어 일반적인 읽기/쓰기 작업보다 효율적으로 처리할 수 있다. 또한 여러 프로세스가 같은 파일을 메모리에 매핑할 경우 해당 메모리 영역을 공유할 수 있어 프로세스 간 통신에도 유용하다.
스택 (stack)
함수 호출 정보와 로컬 변수, 함수 인자 등이 저장되는 영역으로, LIFO(Last In, First Out) 구조로 작동한다. 함수가 호출될 때마다 새로운 스택 프레임이 생성되고, 함수가 반환되면 해당 프레임이 제거된다. 각 스택 프레임에는 함수의 매개변수 및 반환 주소, 지역 변수, 그리고 호출자 함수의 상태 정보 등이 포함된다.
스택은 높은 메모리 주소에서 낮은 메모리 주소 방향으로 성장한다. 즉, 더 많은 함수 호출이 발생할수록 스택은 아래쪽으로 확장된다. 이는 힙이 성장하는 방향과 반대로 두 영역이 서로를 향해 확장되는 구조를 가지게 된다.
스택의 크기는 일반적으로 제한되어 있는데, 이 제한을 초과하면 스택 오버플로우(stack overflow)가 발생한다. 스택 오버플로우는 주로 재귀 함수가 너무 깊게 호출되거나 대규모 로컬 배열이 선언될 때 발생할 수 있다. 스택 오버플로우가 발생하면 프로그램이 비정상적으로 종료되거나 보안 취약점으로 이어질 수 있어 주의해야 한다.
커널 공간
운영체제 커널의 코드와 데이터가 위치하는 영역으로, 일반 사용자 모드 프로세스는 직접 접근할 수 없다. 모든 프로세스의 가상 주소 공간 상단에는 커널 공간이 매핑되어 있지만 이 영역은 보호되어 있어 커널 모드에서만 접근이 가능하다. 사용자 프로세스가 파일 열기나 네트워크 통신, 메모리 할당과 같은 커널 서비스를 이용하기 위해서는 시스템 콜(system call)을 통해 간접적으로 요청해야 한다. 시스템 콜이 발생하면 CPU는 사용자 모드에서 커널 모드로 전환하여 요청된 작업을 수행한 후 다시 사용자 모드로 돌아오게 된다.
가상 메모리 시스템
현대 운영체제에서는 가상 메모리 시스템을 통해 프로세스에게 물리적 메모리 크기보다 큰 메모리 공간을 제공하고 프로세스 간 메모리 보호를 구현한다. 가상 메모리는 프로그램이 실제 물리적 메모리의 제약에서 벗어나 더 유연하게 동작할 수 있게 해주는 핵심 메커니즘이다.
페이징 시스템
페이징(Paging)은 가상 메모리의 기본 메커니즘으로, 가상 주소 공간과 물리 메모리를 모두 고정 크기의 블록으로 나누는 방식이다. 가상 메모리는 페이지(page)라는 단위로 나뉘며 일반적으로 4KB 크기를 갖는다. 마찬가지로 물리 메모리는 동일한 크기의 페이지 프레임(page frame)으로 나뉜다.
페이징 시스템의 가장 큰 장점은 메모리 할당이 연속적일 필요가 없다는 것이다. 가상 주소 공간에서 연속적으로 보이는 페이지들이 물리 메모리에서는 다른 위치에 불연속적으로 배치될 수 있는데, 이는 메모리 단편화(fragmentation) 문제를 줄이고 메모리 활용도를 높이는 데 도움이 된다.
페이징 시스템에서 주소 변환은 메모리 관리 장치(Memory Management Unit, MMU)라는 하드웨어에 의해 수행된다. 프로세스가 가상 주소를 사용하여 메모리에 접근하면 MMU가 물리 주소로 변환하여 실제 물리 메모리에 접근한다. 이 과정은 프로그램 실행 중에 자동으로 이루어지며, 프로그래머는 이러한 주소 변환 과정을 신경 쓰지 않고 가상 주소 공간만 고려하여 프로그래밍할 수 있게 된다.
페이지 테이블
페이지 테이블(Page Table)은 가상 페이지와 물리 페이지 프레임 간의 매핑 정보를 저장하는 자료 구조이다. 각 프로세스는 자신만의 페이지 테이블을 가지고 독립적인 가상 주소 공간을 유지할 수 있다. 페이지 테이블은 운영체제에 의해 관리되며, MMU가 주소 변환을 수행할 때 참조한다.
페이지 테이블의 각 항목인 페이지 테이블 항목(Page Table Entry, PTE)에는 가상 페이지에 대응하는 물리 페이지 프레임 번호와 함께 여러 메타데이터가 포함된다.
- 존재 비트(Present bit)는 페이지가 현재 물리 메모리에 있는지 여부를 나타낸다.
- 권한 비트는 페이지에 대한 읽기, 쓰기, 실행 권한을 나타낸다.
- 수정 비트(Dirty bit)는 페이지가 메모리에 로드된 이후 수정되었는지를 나타낸다.
- 접근 비트(Accessed bit)는 페이지가 최근에 접근되었는지를 나타낸다.
현대 컴퓨터 시스템에서는 가상 주소 공간이 매우 크기 때문에(ex. 64비트 시스템에서는 2^64바이트의 주소 공간을 지원) 단일 레벨의 페이지 테이블로는 관리가 어렵다. 따라서 다단계 페이지 테이블이나 역방향 페이지 테이블과 같은 최적화된 구조를 사용한다. 예를 들어 x86-64 아키텍처에서는 4단계 페이징을 사용하여 48비트 가상 주소 공간을 효율적으로 관리한다.
TLB (Translation Lookaside Buffer)
최근 사용된 가상-물리 주소 변환 정보를 캐싱하는 하드웨어 구성 요소이다. 페이지 테이블 접근은 메모리 접근을 필요로 하기 때문에 상당한 오버헤드가 발생할 수 있다. 즉, 가상 주소를 물리 주소로 변환하기 위해 메모리의 페이지 테이블을 참조해야 한다면 모든 메모리 접근이 사실상 두 번의 메모리 접근(페이지 테이블 접근 + 실제 데이터 접근)을 필요로 하게 되는 것이다.
TLB는 이러한 문제를 해결하기 위한 솔루션으로, CPU 내에 위치한 특수한 캐시이다. TLB는 자주 사용되는 페이지 테이블 항목을 저장하여 주소 변환 속도를 크게 향상시킨다. 프로세스가 메모리에 접근할 때 MMU는 먼저 TLB를 검사하여 해당 가상 주소에 대한 변환 정보가 있는지 확인한다. TLB 히트(hit)가 발생하면 페이지 테이블을 참조하지 않고도 즉시 물리 주소를 얻을 수 있어 빠른 메모리 접근이 가능하지만 TLB 미스(miss)가 발생하면 여전히 메모리의 페이지 테이블을 참조해야 하며, 이후 해당 정보를 TLB에 추가하게 된다.
TLB는 특히 지역성(locality) 원리에 기반하여 효과적으로 작동한다. 시간적 지역성에 따르면 최근에 접근한 메모리 위치에 다시 접근할 가능성이 높고, 공간적 지역성에 따르면 특정 메모리 위치 근처의 다른 메모리 위치에도 접근할 가능성이 높다. 이러한 지역성 덕분에 TLB는 상대적으로 작은 크기에도 불구하고 높은 적중률(hit rate)을 유지할 수 있다.
페이지 폴트와 스왑
페이지 폴트(Page Fault)는 프로세스가 아직 물리 메모리에 로드되지 않은 가상 페이지에 접근할 때 발생하는 예외이다. 페이지 폴트가 발생하면 CPU는 현재 실행을 중단하고 운영체제의 페이지 폴트 핸들러에게 제어권을 넘긴다. 그러면 운영체제는 요청된 페이지를 디스크에서 물리 메모리로 로드한 후, 페이지 테이블을 업데이트하고 명령어를 재실행한다.
페이지 폴트는 크게 세 가지 유형으로 나눌 수 있다.
- 마이너 페이지 폴트(Minor Page Fault)는 요청된 페이지가 이미 물리 메모리에 있지만 현재 프로세스의 페이지 테이블에 매핑되지 않은 경우 발생한다. 예를 들어 다른 프로세스가 이미 해당 페이지를 로드했거나 페이지가 파일 시스템 캐시에 있는 경우이다. 이 경우에는 디스크 접근 없이 페이지 테이블만 업데이트하면 되므로 상대적으로 빠르게 처리된다.
- 메이저 페이지 폴트(Major Page Fault)는 페이지를 디스크에서 읽어와야 하는 경우 발생한다. 이는 상당한 지연을 초래할 수 있으며 시스템 성능에 영향을 미칠 수 있다. 특히 많은 메이저 페이지 폴트가 연속적으로 발생하면 시스템이 스래싱(thrashing) 상태에 빠질 수 있는데, 이때 CPU는 페이지 교체 작업에 대부분의 시간을 소비하게 된다.
- 잘못된 페이지 폴트(Invalid Page Fault)는 프로세스가 접근 권한이 없거나 매핑되지 않은 가상 주소에 접근할 때 발생한다. 보통 프로그램 오류(NULL 포인터 역참조나 배열 경계 초과 등)로 인해 발생하며, 대부분의 경우 segmentation fault 또는 메모리 접근 위반 오류로 이어진다.
물리적 메모리가 부족할 경우 운영체제는 페이지 교체 알고리즘을 사용하여 어떤 페이지를 디스크로 스왑 아웃할지 결정한다. 가장 오랫동안 사용되지 않은 페이지(LRU), 가장 적게 사용된 페이지(LFU), 무작위 페이지 등 다양한 교체 정책이 있으며 각 운영체제마다 최적화된 알고리즘을 사용한다. 페이지가 스왑 아웃되면 해당 페이지는 디스크의 스왑 영역에 저장되고, 필요할 때 다시 메모리로 로드된다.
이러한 페이징 메커니즘을 통해 운영체제는 실제 물리 메모리보다 큰 가상 메모리 공간을 제공할 수 있으며, 프로그램은 마치 전체 가상 주소 공간이 사용 가능한 것처럼 동작할 수 있게 되는 것이다.
실제 실행 과정에서의 메모리 관리
프로그램 실행이 시작되면 운영체제는 다음과 같은 작업을 수행한다.
1. 실행 파일의 헤더를 읽어 프로그램의 메모리 요구사항(코드, 데이터, 스택, 힙 크기 등)을 파악한다.
2. 프로세스를 위한 가상 주소 공간을 생성하고 페이지 테이블을 초기화한다.
3. 프로그램의 코드(텍스트 영역)와 초기화된 데이터를 가상 메모리에 매핑한다.
4. 초기화되지 않은 데이터(BSS 영역)를 위한 메모리를 할당하고 0으로 초기화한다.
5. 프로세스의 스택을 초기화하고 필요한 환경 변수와 프로그램 인자를 스택에 넣는다.
6. 동적 메모리 할당을 위한 힙 영역을 초기화한다.
7. 프로그램이 의존하는 동적 라이브러리를 메모리에 매핑한다.
8. 프로그램의 시작점(main 함수 등)으로 프로그램 카운터를 설정하고 실행을 시작한다.
'Computer Science' 카테고리의 다른 글
비트 단위로 파헤치는 IPv4와 IPv6의 구조적 차이 (0) | 2025.04.07 |
---|---|
OSI 7계층 모델에 대해 알아보자 (0) | 2025.04.06 |
프로그램이 실행되는 과정 - (1) 컴파일 및 링킹 (1) | 2025.04.04 |
프로세스 상태와 생명주기 (0) | 2025.04.03 |
HTTP 요청의 3가지 데이터 전송 방식 - Params, Query, Body (0) | 2025.04.02 |