| 3.2 프로세스 메모리 구조 |
| 3.2.1 메모리 주소 |
| 3.2.2 프로세스 주소 공간 |
| 3.2.3 Human OS; 직접 그려보는 프로세스 구성 영역 |
3.2.1 메모리 주소
메모리는 말 그대로 기억하는 장치이다. 메모리하고 하면 여러가지를 의미하지만 여기서 다루는 것은 메인메모리에 해당하는 RAM을 의미한다. CPU는 이 RAM을 자신의 노트처럼 사용한다. 연산에 필요한 내용을 저장해두었다가 꺼내 쓰거나, 필요한 정보를 넣어두기도한다. 프로세스가 적재되는 곳 또한 바로 이 메모리이다.
메모리는 당연히 많으면 많을수록 좋을 것이다. 그런데 메모리가 무한대로 있다면 어떻게 될까? 컴퓨터가 아주 성능이 좋아질 수 있을까?
답은 아니다, 이다. 바로 CPU 주소공간의 존재 때문이다. 이를 물리 메모리라고도 한다. 이는 CPU의 버스 크기에 의해 결정된다. 32 bit, 64 bit 컴퓨터를 구분하는 것은 익숙할 것이다. 요새는 거의 64 bit만 쓰이지만. 어쨌든 바로 이 32 bit, 64 bit가 CPU의 버스 크기이다. 2.1 컴퓨터 구조 포스트를 읽었다면, BUS란 데이터가 이동하는 통로라는 것을 기억할 것이다. 한 번에 이동할 수 있는 데이터의 크기, Word는 곧 CPU의 주소 공간이다. 한 마디로 이 Word보다 큰 숫자는 보내지 못하기 때문에 의미가 없는 것이다. 일반적으로 32bit CPU는 4GB를, 64bit 컴퓨터는 16EB를 적재할 수 있다.
이러한 컴퓨터의 주소 공간은 0번지부터 시작하며 하나의 번지의 저장 공간 크기는 1 바이트, 즉 8비트이다.
간단히 말해, CPU의 주소 공간보다 큰 메모리는 있더라도 엑세스가 불가능하다. 반대로 CPU의 주소 공간보다 작은 메모리는 가능하다. 대신 설치된 메모리를 넘는 주소 영역에 엑세스를 시도할 시 시스템이 오류가 뜰 뿐이다.
3.2.2 프로세스 주소 공간
위에서 간단하게 메모리 공간에 대해 알아보았다. 우리의 주제인 프로세스들은 저 메모리 공간을 나눠서 사용해야한다. 당연히 프로세스는 많은데 메모리는 한정되어있으므로 운영체제는 프로세스들에게 각각 할당된 공간을 나누어준다.
프로세스가 실행중에 접근할 수 있도록 허용된 주소의 최대 범위를 Segmentation이라고 한다. CPU는 할당된 공간에 대한 경계 레지스터와 한계 레지스터를 벗어나는지 감시하고, 벗어났을 경우 즉시 동작을 강제로 중단시킨다.
하지만 프로세스의 관점은 다르다. 프로세스는 마치 자신이 메모리 전체를 다 사용하고 있다고 착각하고 있는 상태다. 매트리스의 가상 공간에서 가상 현실을 보며 행복해하는 사람들처럼. 이를 가상 메모리라고한다.
가상메모리
그렇다면 왜 가상 메모리를 사용할까? 답은 편해서이다.
조금 더 자세하게 설명하자면, 메모리에 대한 확장성이라고 표현할 수 있다. 물리적 메모리는 한정적이지만 가상 메모리는 더 큰 공간으로 구성 가능하기 때문이다. 메모리를 초과한 부분은 보조기억장치들을 활용하고, 가상 주소 공간은 저장장치의 구분 없이 하나의 가상공간으로 활용 가능하다. 이는 Page swap등이 필요하지만 이는 나중에 다룰 것이다.
모든 프로그램에 대한 동일한 메모리 공간 제공. 각 프로세스는 다른 프로세스를 신경 쓸 필요가 없다. 자동으로 각 프로세스간 메모리가 격리되는 것이다. 이는 보호의 효과를 보인다. 그리고 코딩하기도 편하다.
프로세스 주소공간
가상메모리에 적재되는 프로세스의 주소 공간은 크게 두 부분으로 나누어진다. 사용자 공간과 커널 공간이다.
사용자 공간 User space : 코드, 데이터, 힙, 스택 영역
커널 공간 Kernel space : 프로세스가 시스템 호출을 통해 이용하는 커널 공간. 커널 코드, 커널 데이터, 커널 스택. 커널 공간은 모든 사용자 프로세스에 의해 공유.

각 영역에 대해 더 자세하게 알아보도록 하겠다.
1. 코드 영역
실행될 프로그램의 코드가 적재되는 영역.
사용자가 작성한 모든 함수의 코드 및 호출한 라이브러리 함수들의 코드가 적재된다.
Compiled time에 크기가 결정된다.
2. 데이터 영역
전역 변수 공간, 정적 데이터 공간. rdata, data, bss등으로 구분되며, 사용자 프로그램과 라이브러리를 포함한다.
프로세스 적재 시 할당되며 종료 시 소멸된다.
Compiled time에 크기가 결정된다.
3. 힙 영역
프로세스가 실행 도중 동적으로 사용할 수 있도록 할당된 공간. malloc() 등으로 할당 받는 공간은 힙 영역에서 할당된다. 힙 영역에서 아래 번지로 내려가면서 할당된다.
runtime에 크기가 결정된다.
4. 스택 영역
함수가 실행될 때 사용될 데이터를 위해 할당된 공간. 매개 변수, 지역 변수, 함수 종료 후 돌아갈 주소 등을 저장한다.
함수는 호출 될 때, 스택 영역에서 위쪽으로 공간을 할당받으며 함수가 return하면 할당된 공간을 반환한다.
runtime에 크기가 결정된다.
그렇다면 각 영역은 왜 이렇게 생겼을까? 한정된 메모리 공간을 효율적으로 사용하기 위해서이다. 데이터를 공유하며 메모리 사용량을 줄이고, 스택 구조와 전역 변수의 활용성을 위해서라고 말할 수 있겠다.
- Code 영역이 구분 된 이유 : 코드는 컴파일 이후에는 변경되지 않는다. 같은 프로세스가 여러 개 실행될 시, 코드 영역을 공유함으로써 메모리 사용량을 줄일 수 있다.
- Data 영역이 구분 된 이유 : 전역 변수와 정적 변수가 저장되는 영역이다. 프로그램이 구동되는 동안 항상 접근 가능해야하기 때문에 독립적인 영역이 필요하다.
- Heap 영역이 구분된 이유 : 프로그램 사용 도중 필요할 때마다 할당받는 공간이기에 직접 돌려보기 전까지는 얼마나 필요할 지 예측이 불가능하다. Stack 또한 마찬가지다. 동적 공간에서 서로 공간을 유연하게 활용할 수 있도록 위와 아래에서 자라나는 구조로 이루어져있다. (+둘 다 아래로 자랐다가 커널에 들어가면 그닥 좋지 않은 일이 일어날 것이다.)
- Stack 영역이 분리된 이유 : 함수는 프로그램의 실행 단위이다. 프로그램은 함수의 호출로 이루어져 있기 때문이다. 함수에는 지역변수, 매개변수, 반환 값등이 존재한다. 이 때 스택 구조를 사용한다면 함수의 호출 순서와 반환 대상의 관리가 편하기 때문에 Stack 영역이 필요하게 되었다.
아래는 프로세스에서 물리주소로 변환되는 것을 나타낸 그림이다. 추후 다시 학습할 것이다.

3.2.3 Human OS; 직접 그려보는 프로세스 구성 영역
이제부터 직접 인간 운영체제가 되어 C언어로 작성된 코드의 실행 도중 어떻게 데이터가 메모리에 적재되는지 하나하나 알아보도록 하겠다. 운영체제의 소중함을 느끼는 시간을 가져보도록 하자….
사용할 코드는 다음과 같다.
1 #include <stdio.h>
2 #include <stdlib.h>
3
4 int a = 10;
5
6 void f(){
7 int c = 30;
8 printf("%d", c);
9 }
10
11 int main() {
12 int b = 20;
13 int *p = (int*)malloc(100);
14 f();
15 printf("%d", b);
16 return -1;
17 }
위와 같은 코드를 컴퓨터는 위에서부터 아래로 쭉 읽어내려 갈 것이다.
다음 프로그램이 실행한 직후 상태의 프로세스 메모리 공간을 어떻게 생겼을까? 즉, main()이 호출되자마자인 11번째 줄을 막 읽은 시점이다.
| 코드 공간 | main()함수 코드, f()함수 코드, 라이브러리 코드 등... |
| 데이터 공간 | a = 10 |
| 힙 공간 | - |
| 스택 공간 | Stack frame for main() |
당연히 코드공간에는 코드가 적재되었고, 전역변수인 a가 데이터 공간에 배치되었다. 또한 메인함수에 쓰일 스택 프레임이 스택 공간에 들어가 있을 것이다. 그렇다면 이제 다시 13번째 줄까지 실행해보자.
| 코드 공간 | main()함수 코드, f()함수 코드, 라이브러리 코드 등... |
| 데이터 공간 | a=10 |
| 힙 공간 | [100bytes] |
| 스택 공간 | Stack frame for main(), b=20, *p = [100bytes]의 주소 |
먼저 b=20이라는 지역변수가 스택 공간에 들어온다. 그 후 malloc을 통해 할당받은 공간이 heap 영역에 저장되고 또 다른 지역변수인 *p가 스택에 저장된다. 그럼 이제 f()를 호출한 뒤 8번째 줄까지 실행해보자.
| 코드 공간 | main()함수 코드, f()함수 코드, 라이브러리 코드 등... |
| 데이터 공간 | a=10 |
| 힙 공간 | [100bytes] |
| 스택 공간 | Stack frame for main(), b=20, *p = [100bytes]의 주소, Stack frame for f(), c=30 |
함수 f()가 호출되었으므로 그에 대한 스택 프레임이 생성되었다. 또한 지역 변수 c에 대해 공간이 할당된다. 이 둘은 모두 stack 영역에서 이루어진다. 그렇다면 15번째까지 실행해보자.
| 코드 공간 | main()함수 코드, f()함수 코드, 라이브러리 코드 등... |
| 데이터 공간 | a=10 |
| 힙 공간 | [100bytes] |
| 스택 공간 | Stack frame for main(), b=20, *p = [100bytes]의 주소, Stack frame for f(), c=30 |
이 경우 f()가 리턴, 즉 반환되었기때문에 stack frame과 지역변수 값 모두 자동으로 반환된다. 스택에서 제거되는 것이다. 그렇다면 16번째까지 실행하도록 하자. 16번째 코드에서는 메인함수 또한 반환된다.
| 코드 공간 | main()함수 코드, f()함수 코드, 라이브러리 코드 등... |
| 데이터 공간 | a=10 |
| 힙 공간 | [100bytes] |
| 스택 공간 | Stack frame for main(), b=20, *p = [100bytes]의 주소, Stack frame for f(), c=30 |
메인 함수에 대한 stack frame이 제거되고 마찬가지로 변수 b와 *p또한 자동으로 정리되었다. 아직 Heap에 대한 메모리 할당은 여전히 남아있는 것에 주의하자. (이를 메모리 누수라고 한다.)
이제 최종 종료 시킬 시간이다.
| 코드 공간 | main()함수 코드, f()함수 코드, 라이브러리 코드 등... |
| 데이터 공간 | a=10 |
| 힙 공간 | [100bytes] |
| 스택 공간 | Stack frame for main(), b=20, *p = [100bytes]의 주소, Stack frame for f(), c=30 |
모든 작업이 끝나게 되면 운영체제는 프로세스에게 주어진 모든 메모리를 회수해간다. 그런데 만약 OS가 메모리를 회수하지 못하면 어떻게 될까? 사용도 하지 않으면서 계속 메모리의 공간을 잡아먹는 메모리 누수가 일어난다. 당연히 컴퓨터의 성능을 떨어뜨리는 요인이 된다.
바로 이런 현상을 줄이는 것이 OS의 중요한 요소 중 하나이다.
이번 시간은 프로세스를 메모리에 올리는 과정을 배웠다. 다음에는 프로세스가 생성되고 복사되는 과정을 알아볼 것이다.
'CS > 운영 체제' 카테고리의 다른 글
| 3.4 프로세스 계층 구조 (2) | 2024.04.20 |
|---|---|
| 3.3 프로세스의 생성과 복사 (1) | 2024.04.20 |
| 3.1 프로세스의 개념 (4) | 2024.04.17 |
| 3.0 프로세스 (1) | 2024.04.17 |
| 2.1 컴퓨터 구조 (1) | 2024.04.14 |