CH14 막간 - 메모리 관리 API
이번 장에서는 UNIX 의 메모리 관리 인터페이스에 대해 논의한다
핵심 질문 - 어떻게 메모리를 할당하고 관리해야 하는가
UNIX/ C 프로그램에서 메모리를 할당하고 관리하는 방법은 강력하고 안정적인 소프트웨어를 구축하는 데 중요하다
일반적으로 어떤 인터페이스가 사용되는가? 어떤 실수를 해서는 안 되는가?
14.1 메모리 공간의 종류
C 프로그램이 실행되면, 두 가지 유형의 메모리 공간이 할당된다
1) 스택 Stack - 컴파일러가 관리
컴파일러에 의해 할당과 반환이 암묵적으로 이루어지기 때문에 자동 Automatic 메모리라고 불린다
func() 라는 함수 안에서 x 라 불리는 정수를 선언할 때 아래와 같이 선언한다
void func() {
int x; // 스택에 int 형을 선언
...
}
그래서 func() 이 호출될 때 컴파일러가 함수를 위해 필요한 메모리 공간을 스택에 할당하고
함수가 종료될 때 컴파일러가 스택에 할당되었던 메모리를 해제하고, 할당되었던 메모리는 자동으로 반환된다
2) 힙 Heap - 프로그래머가 관리
모든 할당과 반환이 프로그래머에 의해 명시적으로 처리된다
void func() {
int * x = (int *) malloc (sizeof(int)));
...
}
컴파일러가 포인터 변수의 선언 (int * x) 을 만나면 정수를 가리키는 포인터를 위한 공간을 할당해야 한다는 것을 알게 된다
프로그램이 malloc() 을 호출하면, 힙에서 정수를 저장할 공간을 요청하게 되며, malloc() 은 그 공간의 주소를 반환한다
(실패한 경우에는 NULL 반환한다) 이 반환된 주소는 스택에 저장된 포인터를 통해 프로그램에서 사용된다
14.2 malloc() 함수
#include <stdlib.h>
...
void *malloc(size_t size);
// void 형으로 반환해서 선언할 때 캐스팅을 해 줌
malloc() 의 인자는 size_t 타입의 변수이고 이 변수는 필요 공간의 크기를 바이트 단위로 표시한 것이다
C 언어에서 sizeof() 는 통상 컴파일 시간 연산자이고, 인자의 실제 크기가 컴파일 시간에 결정된다
sizeof() 는 숫자로 대체되어 malloc() 에 전달되기 때문에 sizeof() 는 연산자로 간주된다 (함수 호출이 아니다)
-> 컴파일 시에 돌아가면 연산이고, 런타임에 돌아가면 함수다
sizeof() 로 문자열을 다룰 때 조심해야 한다, 문자열을 위한 공간을 선언할 때는 malloc(strlen(s) + 1) 문장을 사용한다
strlen(s) 함수는 널 종료 문자인 '\0' 을 제외하고 문자열의 길이를 계산하기 때문에 malloc(strlen(s)) 을 쓰면 문제를 일으킬 수 있다
14.3 free() 함수
더 이상 사용되지 않는 힙 메모리를 해제하기 위해 프로그래머는 free() 를 호출한다
int * x = malloc(10 * sizeof(int));
...
free(x);
한 개의 인자, malloc() 에 의해 반환된 포인터를 받는다
할당된 영역의 크기는 전달되지 않지만, 메모리 할당 라이브러리는 할당된 메모리의 크기를 알고 있어야 한다
14.4 흔한 오류
많은 새로운 언어들이 자동 메모리 관리 Automatic Memory Management 를 지원한다
그러한 언어들에서는 공간을 해제하기 위해서 아무것도 호출하지 않고
쓰레기 수집기 Garbage Colletor 가 실행되어 참조되지 않는 메모리를 찾아 알아서 해제한다
메모리 할당 잊어버리기
많은 루틴은 자신이 호출되기 전에 필요한 메모리가 미리 할당되었다고 가정한다
예를 들어 strcpy(dst, src) 루틴은 소스 포인터에서 목적 포인터로 문자열을 복사한다
// 메모리 할당 잊어버리기의 예시
char *src = "Hello";
char *dst; // 할당이 안 되어 있다
strcpy(dst, src); // segfault 그리고 죽는다
이 코드를 실행하면 세그멘테이션 폴트 Segmentaion Fault 가 발생할 가능성이 높고,
이 폴트는 "네가 메모리 관련 무언가를 잘못했어, 이 바보 같은 프로그래머야, 그래서 나 화났거든" 이라는 메시지이다
// 메모리 할당을 잘 한 예시
char *src = "Hello";
char *dst = (char *) malloc(strlen(src)+1);
strcpy(dst, src); // 제대로 동작
메모리를 부족하게 할당받기
버퍼 오버플로우 Buffer Overflow 라고 불린다
char *src = "Hello";
char *dst = (char *) malloc(strlen(src)); // 너무 작다
strcpy(dst, scr); // 동작은 제대로 한다
이런 경우에 문자열 복사가 실행될 때 할당된 공간의 마지막을 지나쳐 한 바이트만큼 공간을 더 사용한다
이 공간이 더 이상 사용되지 않는 변수 영역이라면 덮어쓰더라도 피해가 발생하지 않을 수 있지만
다른 때에는 이러한 오버플로우가 매우 유해할 수 있고, 사실 많은 시스템에서 보안 취약점의 원인이다 [Wer06]
할당받은 메모리 초기화하지 않기
malloc()을 제대로 호출했지만 새로 할당받은 데이터 타입에 특정 값을 넣는 것을 종종 잊는다
초기화되지 않는 읽기 Uninitialized Read 는 알 수 없는 값을 읽는 일이다
메모리 해제하지 않기
메모리 누수 Memory Leak 은 메모리 해제를 잊었을 때 발생한다
메모리 청크의 사용이 끝나면 반드시 해제해야 한다, 쓰레기 수집 기능이 있는 언어도
메모리 청크에 대한 참조가 존재하면 그 청크를 해제하지 않아 메모리 누수가 생길 것이다
메모리 사용이 끝나기 전에 메모리 해제하기
메모리 사용이 끝나기 전에 메모리를 해제하는 실수는 Dangling Pointer 라고 불리며 심각한 실수이다
차후 그 포인터를 사용하면 프로그램을 크래시 시키거나 유효 메모리 영역을 덮어쓸 수 있다
ex. free() 를 호출하고, 그 후 다른 용도로 malloc() 을 호출하면 잘못 해제된 메모리를 재사용한다
반복적으로 메모리 해제하기
프로그램은 가끔씩 메모리를 한 번 이상 해제하며 이중 해제 Double Free 라 불린다
그런 상황에서 메모리 할당 라이브러리는 크래시를 비롯한 모든 종류의 이상한 일을 하게 된다
free() 잘못 호출하기
malloc() 받은 포인터 값 외에 값으로 free() 를 전달하면 문제가 발생한다
유효하지 않은 해제 Invalid Frees 는 매우 위험하고 당연히 피해야한다
14.5 운영체제의 지원
malloc 라이브러리는 프로세스 가상 메모리 공간안의 공간을 효율적으로 관리하는 역할을 하지만
라이브러리 자체는 시스템에게 더 많은 메모리를 요구하고 반환하는 시스템 콜을 기반으로 구축된다
그런 시스템 콜 중 하나가 brk 라고 불리는, 프로그램의 break 위치를 변경하는 시스템 콜이다
break 는 힙의 마지막 위치를 나타내고, brk 는 새로운 break 주소를 나타내는 한 개의 인자를 받는다
### brk()와 sbrk()
둘다 힙 영역의 크기를 조정하기위한 시스템 호출
- brk() : 힙의 끝을 설정하는데 사용
- sbrk() : 힙 크기를 증가 또는 감소시킴
두 함수를 직접 호출해서는 안된다. malloc()과 free()를 사용할 것
mmap() 함수를 사용하여 운영체제로부터 메모리를 얻을 수 있다
올바른 인자를 전달하면 mmap() 은 프로그램에 anonymous 의 메모리 영역을 만든다
-> anonymous 영역은 특정 파일과 연결되어 있지 않고 스왑 공간 swap space 에 연결된 영역이고,
이 메모리는 힙처럼 취급되고 관리된다
### mmap()
익명 메모리 영역을 할당하는데 사용하는 시스템 호출
스왑 공간에 연결되어있다? → 운영체제가 물리 메모리 부족 시 해당 메모리 페이지 영역을 스왑 공간으로 옮길 수 있다는 것을 의미.
**스왑 공간이란, 디스크의 일부를 가상 메모리의 확장으로 사용하는 영역이다.** 물리 메모리가 부족할 때, 운영체제는 현재 사용하지 않는 메모리 페이지를 스왑 공간에 저장한다.
즉, 물리 메모리를 다 썼을때, 추가적으로 메모리 페이지를 저장할 수 있는 디스크 공간이다.
디스크를 왔다갔다해야한다면 속도가 비교적 느려 프로그램 성능 저하 발생가능.
mmap을 왜쓰는거지?
- 익명 메모리는 특정 파일과 연결되어 있지 않기 때문에, 프로그램 내부에서 자유롭게 사용할 수 있는 빈 메모리 공간을 제공한다.
- malloc()보다 더 유연하게 큰 메모리 공간을 다룰 수 있다. 특히 대용량 메모리 처리시 유용하다.
- 익명 메모리는 물리 메모리 부족 시 스왑 공간으로 이동할 수 있어 메모리 관리를 유연하게 해줌.
14.6 기타 함수들
calloc() 은 메모리 할당 영역을 0 으로 채워서 반환한다
realloc() 은 이미 할당된 공간에 대해 추가의 공간이 필요할 때 더 큰 새로운 영역을 확보하고
옛 영역의 내용을 복사한 후에 새 영역에 대한 포인터를 반환한다
CH15 주소 변환의 원리
CPU 가상화에서 나온 제한적 직접 실행 LDE 이라는 기법의 아이디어는
대부분의 프로그램은 하드웨어에서 직접 실행되지만 프로세스가 시스템 콜을 호출하거나
타이머 인터럽트가 발생할 때 등의 특정한 순간에는 운영체제가 개입한다는 것이다
-> 사용자 모드에서 실행되는 코드를 최대한 효율적으로 실행하면서도 특정한 권한이 필요한 명령을 제한하는 방식
메모리 가상화도 CPU 가상화와 마찬가지로 효율성 Effiency 와 제어 Control 를 목표로 한다
효율성을 보장하기 위해서는 하드웨어의 지원이 필수적이며,
제어는 응용 프로그램이 자기 자신의 메모리 외에는 접근하지 못하도록 운영체제가 보장하는 것을 의미한다
프로그램이 다른 프로그램으로부터 보호 받고 운영체제가 프로그램으로부터 보호 받기 위해서 하드웨어의 도움이 필요하다는 것이다
또한 유연성 Flexiablity 측면에서, 프로그래머가 원하는 대로 주소 공간을 사용할 수 있고 프로그래밍하기 쉬운 시스템을 만들길 원한다
-> + 투명성 : 프로세스는 메모리 참조가 변환되는 사실을 인식하지 못함. 이를 통해 메모리 가상화의 효과적인 구현이 가능해진다.
핵심 질문 - 어떻게 효율적이고 유연하게 메모리를 가상화하는가
어떻게 효율적인 메모리 가상화를 구축할 수 있을까? 프로그램이 필요로 하는 유연성을 어떻게 제공하는가?
프로그램이 접근할 수 있는 메모리의 위치에 대한 제어를 어떻게 유지하는가?
메모리 접근을 어떻게 적절히 제한할 수 있는가? 어떻게 이 모든 것을 효율적으로 할 수 있는가?
하드웨어 기반 주소 변환 Hardware-based Address Translation, 주소 변환 Address Translation 은
제한적 직접 실행 방식에 부가적으로 사용되는 기능이라고 볼 수 있다
하드웨어는 주소 변환을 통해 가상 주소를, 실제 존재하는 물리 주소로 변환하고
정확한 변환이 일어날 수 있도록 운영체제가 하드웨어를 셋업하는 것에 관여한다
운영체제는 메모리의 빈 공간과 사용 중인 공간을 항상 알고 있어야 하고, 메모리 사용을 제어하고 관리한다
15.1 가정
당분간 사용자 주소 공간은 물리 메모리에 연속적으로 배치되어야 한다
논의를 단순화하기 위해 주소 공간의 크기가 너무 크지 않다
주소 공간은 물리 메모리 크기보다 작다
각 주소 공간의 크기는 같다
15.2 사례
주소 공간이 그림 15.1 과 같은 프로세스가 있다고 할 때,
세 개의 명령어 코드는 주소 128에 위치하고 (상단 코드 섹션에),
변수 x의 값은 주소 15KB (아래쪽 스택에) 위치한다
그림에서 x의 초기 값은 3000이다 (스택 영역 참조)
명령어가 실행되면 프로세스 관점에서 다음과 같은 메모리 접근이 일어난다
1. 주소 128의 명령어를 반입
2. 이 명령어 실행 (주소 15KB에서 탑재)
3. 주소 132의 명령어를 반입
4. 이 명령어 실행 (메모리 참조 없음)
5. 주소 135의 명령어를 반입
6. 이 명령어 실행 (15KB에 저장)
프로그램 관점에서 주소 공간은 주소 0부터 시작하여 최대 16KB까지이고
프로그램이 생성하는 모든 메모리 참조는 이 범위 안에 있어야 한다
메모리 가상화를 위해 운영체제는 프로세스 모르게 메모리를 다른 위치에 재배치하고자 한다
그림 15.2 에 이 프로세스의 주소 공간이 메모리에 배치되었을 때 가능한 물리 메모리 배치의 예시가 나와 있다
그림에서 물리 메모리의 첫번째 슬롯은 운영체제가 사용하고, 15.1 의 프로세스는 물리 주소 32KB 에서 시작하는 슬록에 재배치 되어 있다
15.3 동적 (하드웨어 기반) 재배치
동적 재배치 Dynamic Relocation, 베이스와 바운드 Base and Bound
-> 동적 재배치는 커널 모드에서만 사용 가능!!
각 CPU 마다 두 개의 하드웨어 레지스터가 필요하다, 베이스 Base 레지스터와 바운드 Bound (또는 한계 Limit) 레지스터
이 베이스와 바운드를 통해 원하는 주소 공간을 설정할 수 있으며, 프로세스는 배치된 자신의 주소 공간에만 접근할 수 있도록 보장된다
이 설정에서 모든 프로그램은 주소 0에 배치된 것처럼 작성하고 컴파일된다
프로그램이 시작되면 운영체제가 실제 메모리에서 프로그램이 배치될 물리적 위치를 정하고 베이스 레지스터를 그 주소로 설정한다
(그림 15.2 의 경우 프로세스를 물리 주소 32KB 에 저장하기로 결정하고 베이스 레지스터를 이 값으로 설정)
프로세서는 프로세스가 실행되면 프로세스에 의해 생성되는 모든 주소를 아래와 같이 변환한다
physical address = virtual address + base
프로세스가 생성하는 메모리 참조는 가상 주소이며, 하드웨어는 베이스 레지스터의 내용을 이 가상 주소에 더하여 물리 주소를 생성한다
128: movl 0x0 (%EBX), % eax
위와 같은 명령어가 있을 때, 프로그램 카운터 PC 는 128로 설정되고, 하드웨어가 이 명령어를 반입할 때
먼저 PC 값을 베이스 레지스터 값 32KB (32768) 에 더해 32896 의 물리 주소를 얻는다
그런 후에 하드웨어는 해당 물리 주소에서 명령어를 가져와서, 프로세서가 명령어를 실행한다
프로세스가 참조하는 가상 주소를 받아들여 하드웨어가 데이터가 실제로 존재하는 물리 주소로 변환하는 것이 주소 변환이다
주소의 재배치는 실행 시에 일어나고, 프로세스가 실행을 시작한 이후에도 주소 공간을 이동할 수 있기 때문에 동적 재배치라고도 불린다
바운드 프로세서는 보호를 지원하기 위해 존재하며, 프로세서가 메모리 참조가 합법적인지 확인할 때
가상 주소가 바운드 안에 있는지 확인한다, 참조하려는 가상 주소가 바운드보다 큰 가상 주소 또는 음수인 경우
CPU는 예외를 발생시키고 프로세스는 종료된다
베이스와 바운드 레지스터는 CPU 칩 상에 존재하는 한 쌍의 하드웨어 구조이며
주소 변환에 도움을 주는 프로세서의 일부를 MMU Memory Management Unit 라고도 부른다
-> 베이스 레지스터와 바운드 레지스터는 주로 프로세스의 메모리 영역을 제한하고 보호하는 역할 (접근 범위 설정)
-> MMU는 위 기능 + 페이지 테이블 관리, 가상 주소 > 물리 주소 변환 등 복잡한 역할 수행
15.4 하드웨어 지원 요약
운영체제는 커널 모드 (또는 특권 모드) 에서 실행되며 컴퓨터 전체에 대한 접근 권한을 가진다
응용 프로그램은 사용자 모드에서 실행되며, 할 수 있는 일에 제한이 있다
프로세서 상태 워드 Processor Status Word 레지스터의 한 비트가 CPU의 현재 실행 모드를 나타낸다
-> 프로세서 상태 워드는 CPU의 현재 상태를 나타내는 레지스터로, CPU 실행 모드에는 사용자 모드/ 일반 모드가 있다
15.5 운영체제 이슈
가상 메모리를 구현하기 위해 운영체제가 반드시 개입되어야 하는 중요한 세 개의 시점은 아래와 같다
프로세스 구조체 Process Structure 또는 프로세스 제어 블럭 Process Control Block
각 프로세스마다 운영체제가 관리하는 자료 구조 (베이스와 바운드 레지스터의 값을 포함한 프로세스의 상태를 저장한다)
아래 그림 15.5 와 15.6 은 하드웨어/ OS 의 상호작용을 타임라인으로 보여주며, 부팅할 때 컴퓨터를 사용 가능한 상태로 만들기 위해 운영체제가 하는 작업과 프로세스 A 가 실행을 시작할 때 무슨 일이 일어나는지를 보여준다
운영체제는 프로세스가 CPU에서 직접 실행될 수 있도록 관리하고, 프로세스가 잘못된 행동을 할 때에만 개입해야 한다
15.6 요약
주소 변환은 하드웨어의 지원을 받아 가상 주소를 물리 주소로 변환하는 과정이며
주소 변환을 통해 운영체제가 프로세스의 모든 메모리 접근을 제어할 수 있고, 주소 공간의 범위 내에서 접근이 이루어지도록 보장할 수 있다
베이스와 바운드 (Base-and-bound) 가상화는 베이스 레지스터를 가상 주소에 더하고 생성된 주소가 바운드를 벗어나는지 검사하기 위한 간단한 하드웨어 회로만 추가하면 되고, 보호 기능도 제공하기 때문에 매우 효율적이다
하지만 그림 15.2 에서 볼 수 있듯이, 동적 재배치 가상화로 재배치된 프로세스에서는 프로세스 스택과 힙의 크기가 작아 둘 사이의 공간이 낭비되고 있고 이런 유형의 낭비를 내부 단편화 Internal Fragmentation 이라고 한다
내부 단편화를 방지하기 위해, Base-and-bound 를 일반화하는 세그멘테이션 Segmentation 기법에 대해서 다음 장에서 배울 예정이다
'크래프톤정글 > 운영체제' 카테고리의 다른 글
[운영체제] CH25 - CH27 (0) | 2024.10.25 |
---|---|
[운영체제] CH16 - CH17 (0) | 2024.10.24 |
[OSTEP] 가상화 CH10-CH13 (1) | 2024.10.18 |
[OSTEP] 가상화 CH08- CH09 (2) | 2024.10.16 |
[OSTEP] 가상화 CH06 - CH07 (3) | 2024.10.14 |