Process Memory

안준성·2024년 7월 18일
0

OperatingSystem

목록 보기
19/22
post-thumbnail

프로그램은 실행되면 메모리에 적재되어 프로세스가 된다.
이 때, 프로세스가 할당 받는 메모리의 구조와 동작방식에 대해 알아보자.

프로세스 메모리 구조

프로세스가 할당 받는 메모리는 각 용도에 맞게 영역이 분리 되어있다.
크게 스택 영역, 힙 영역, text 영역, data 영역이 있는데,
각 영역에 프로세스의 어떤 요소가 적재되는지 알아보자.


Text 영역

프로세스가 실행할 코드는 컴파일 되어 기계어 형태로 텍스트(코드) 영역에 저장된다.
이 외에도 문자열 리터럴이나 상수 데이터, 라이브러리 코드도 이곳에 저장된다.
이 영역은 읽기 전용으로 설정되어 있으며,
동일한 프로그램을 실행하는 여러 프로세스가 공유할 수 있다.

가상 메모리 기법

각 프로세스는 독립적인 메모리 공간에서 실행되는데 어떻게 공유가 가능하다는걸까?
이는 가상 메모리 기법과 연관되어 있는데,
실제 text 영역은 물리 메모리 상에 단일 복사본 형태로 존재한다.

이 단일 복사본은 MMU에 의해 각 프로세스의 가상 주소 공간에 매핑된다.
즉, 코드는 물리메모리에 단일로 존재하지만
프로세스들은 이 코드가 자기 영역에 있다고 생각하는 것이다.

이를 통해 메모리를 절약할 수 있고,
동일한 영역을 여러 프로세스가 접근하므로 캐시 히트율이 높아져 성능이 향상된다.


Data 영역

데이터 영역에는 전역 변수정적 변수가 저장된다.
데이터 영역은 프로세스가 실행되는 동안 계속 유지되기 때문에
어디서든 전역 변수와 정적 변수에 접근할 수 있다.
(지역 변수가 함수가 스택 영역에 적재 되어야지만 접근할 수 있는 데 반해 말이다)

정적 변수의 초기화

데이터 영역에 있는 변수들은 프로세스 시작시에 단 한 번만 초기화 되는데,
명시적으로 해주지 않으면 자동으로 0으로 초기화 된다.

이러한 특성을 이용해 다음과 같은 코드도 가능하다.

void increment() {
    static int count = 0;
    count++;
}

int main() {
    increment();
    increment();
}

정적 변수에 대한 초기화 구문은 해당 구문이 위치한 함수와 상관없이
프로세스가 메모리에 로드될 때 실행되기 때문에,
함수의 호출과는 별개로 최초에 초기화가 진행되고 이후엔 실행되지 않는다.

따라서 위 코드를 실행하면 count의 값은 1이 아닌 2가 된다.

멀티 스레드 환경

데이터 영역은 프로세스 별로 독립적으로 존재하지만, 스레드끼리는 공유된다.
따라서 멀티 스레드를 사용할 때는 데이터의 일관성을 유지하기 위해
동기화 문제를 해결해야 한다.


Text영역과 Data영역 심화

심볼 테이블

text 영역과 data영역의 요소들은 컴파일 단계에서 심볼 테이블에 저장된다.
Text 영역의 심볼 테이블에는 함수 이름, 함수 시작 주소, 함수 크키 등의 정보가 포함되고,
Data 영역의 심볼 테이블에는 변수명이나 타입 정보, 스코프 등이 저장된다.

심볼 테이블은 실행 파일에는 포함되지 않고 오브젝트 파일에 위치한다.
메모리에 적재되는 것은 오직 변수의 값 뿐이다.
심볼 테이블의 정보는 주로 디버깅과 링크 단계에서 사용된다.

메모리 주소 할당

두 영역의 내용은 컴파일 타임에 확정된다.
컴파일러는 컴파일 시에 변수들의 상대 주소를 결정한다.
이 주소는 오브젝트 파일 내에서의 상대적인 위치를 나타내며
오브젝트 파일의 심볼 테이블에 기록된다.

컴파일러는 소스 코드를 기계어로 변환할 때, 변수에 접근하는 모든 위치에서
해당 변수의 상대 주소를 사용하여 접근하는 기계어 명령어를 생성한다.
이 주소는 링크 단계에서 절대 주소로 다시 한 번 변환된다.

주소 재배치

링커는 여러 개의 오브젝트 파일들을 하나의 실행 파일로 결합하면서,
심볼 테이블을 이용해 컴파일 단계에서 결정했던 상대 주소를 절대 주소로 변환한다.

여기서 말하는 "절대 주소"는 실행 파일 내에서의 절대 주소로,
운영체제가 메모리에 로드할 때의 실제 물리적 주소는 아니다.
이러한 재배치 정보는 실행 파일 헤더의 재배치 테이블에 저장된다.

운영체제는 실행 파일을 메모리에 로드할 때,
헤더의 재배치 정보를 이용하여 변수를 실제 물리 메모리에 배치한다.


Heap 영역

힙 영역은 동적 메모리 할당이 이루어지는 영역이다.
C 언어로 개발해 본 사람들은 malloc을 통해 메모리를 할당해 본 경험이 있을 것이다.
다른 언어에서는 주로 new 키워드를 사용하여 객체를 할당할 때 메모리의 동적 할당이 이루어진다.

메모리 할당

메모리 할당은 어떻게 이루어지는 것일까.
메모리 할당이 요청되면 운영체제의 메모리 할당기는
free list에서 적절한 크기의 할당 가능한 메모리 블록을 찾아 필요한 크기만큼 분할한다.
찾은 블록의 상태를 "할당 됨"으로 업데이트하고 블록의 헤더에 할당된 크기와 상태를 기록한다.
이후 메모리 블록의 유효 데이터 영역의 시작 주소를 사용자에게 반환한다.

메모리 블록

메모리 블록은 메모리 블록 헤더와, 유효 데이터 영역으로 구성된다.

헤더는 고정 크기를 가지며, 헤더를 포함한 메모리 블록의 크기, 할당 상태, 포인터 등이 포함된다.
포인터는 다음 또는 이전 빈 블록을 가리키며, 이는 프리 리스트를 구현할 때 사용된다.
할당 요청 시 요청된 크기 외에 헤더 크기만큼 추가 메모리가 필요하다.

유효 데이터 영역(Payload)에는 데이터를 저장하는 영역이다.
이 영역은 헤더 바로 다음에 위치한다.
사용자는 실제 이 영역에 접근하여 데이터를 읽고 쓴다.

메모리 블록 구조 예시

주소데이터 (16진수)설명
0x10000x00000030블록 크기 (48바이트)
0x10040x1블록 상태 (할당됨)
0x10080x00000000다음 블록 포인터 (없음)
0x100C0x00000000(추가 메타데이터)
0x10100x00000000유효 데이터 영역 시작
......
0x1030(유효 데이터 끝)

힙 메모리의 관리

힙 영역에 할당한 메모리는 반드시 실행 중에 회수해야 한다.
물론 프로그램이 종료되면 운영체제가 할당된 메모리를 자동으로 회수하지만,
메모리 누수가 발생하면 프로그램 실행 중에 메모리가 지속적으로 누적될 수 있다.
이는 특히 서버와 같이 계속 실행되어야 하는 프로그램에 치명적일 수 있다.


Stack 영역

스택은 함수 호출과 관련된 데이터가 저장되는 영역이다.
함수가 호출되면 스택 프레임을 생성하여 공간을 할당한다.
처음에는 매개변수, 리턴 주소 값을 저장한다.
이후 함수가 실행되면서 지역 변수, 실행 중 필요한 데이터, 계산 결과 값이 저장된다.

함수가 종료되면 해당 스택 프레임이 제거되고, 리턴 주소를 사용하여 복귀한다.
여기서 리턴 주소는 함수 호출 이후에 실행해야 할 명령어의 주소를 가지며,
호출 규약에 따라 스택 프레임 내의 고정된 위치에 저장된다.
정리하자면 함수가 종료될 때 ret 명령어를 실행 해,
스택의 지정된 위치에서 리턴 주소를 꺼내 명령어 포인터에 로드한다.

스택 프레임의 크기

각 함수의 스택 프레임 크기는 컴파일 타임에 계산되어,
함수 호출 시 적절한 스택 프레임을 할당하도록 어셈블리 코드를 생성한다.


기존에 알고 있던 프로세스 메모리 구조에서
좀 더 자세하게 어떤 일이 일어나는지에 대해 알아보았다.
대충 아는거보다 자세하게 아니 기분이 좋다.

profile
안녕하세요 준갑습니성

0개의 댓글