기록공간

2-7장. 메모리 관리 API 본문

OS

2-7장. 메모리 관리 API

입코딩 2020. 3. 4. 21:08
반응형

메모리 공간의 종류

C 프로그램이 실행되면, 두 가지 유형의 메모리 공간이 할당된다. 첫 번째는 스택(Stack) 메모리라고 불리며 할당반환은 프로그래머를 위하여 컴파일러에 의해 암묵적으로 이루어 진다. 이러한 이유 때문에 때로는 자동(Automatic) 메모리라고 불린다. 

 

C 프로그램에서 스택에 메모리를 선언하는 것은 쉽다. 예를 들어, func() 라는 함수 안에서 x라 불리는 정수를 위한 공간이 필요하다고 하자. 이러한 메모리를 선언하려면 다음과 같이 하면 된다.

void func()
{
	int x; // 스택에 int 형을 선언
	// ....
}

컴파일러가 나머지 작업을 수행하여, func()가 호출될때 스택에 공간을 확보한다. 함수에서 리턴하면 컴파일러는 프로그래머 대신에 메모리를 반환한다. 함수 리턴 이후에도 유지되어야 하는 정보는 스택에 저장하지 않는 것이 좋다.

 

오랫동안 값이 유지되어야 하는 변수를 위해 힙(Heap) 메모리라고 불리는 두 번쨰 유형의 메모리가 필요하다. 모든 할당과 반환이 프로그래머에 의해 명시적으로 처리된다. 그렇기 조심스럽게 사용하지 않으면 많은 버그의 원인이 될 수 있다. 다음 코드는 힙 메모리에 정수에 대한 포인터를 할당하는 예를 보여준다.

void func()
{
	int *x = (int*)malloc(sizeof(int));
	// ...
}

이 코드에는 주의 사항이 있다. 한 행에 스택과 힙 할당이 모두 발생한다는 것이다. 우선 컴파일러가 포인터 변수의 선언을 만나면 정수 포인터를 위한 공간을 할당해야 한다는 것을 안다. 프로그램이 malloc()을 호출하여 정수를 위한 공간을 힙으로부터 요구한다. malloc()은 그 정수의 주소를 반환한다.(성공한 경우 한정, 실패한 경우는 NULL반환) 이 반환된 주소는 스택이 저장되어 프로그램에 의해 사용된다.

 

이런 명시적 성질과 다양한 쓰임새 때문에 힙 메모리의 사용은 사용자와 시스템 모두에게 어려운 숙제다. 남은 논의는 힙 메모리에 초점을 맞출 것이다.

 

malloc() 함수

malloc() 호출은 매우 간단하다. 힙에 요청할 공간의 크기를 넘겨 주면, 성공했을 경우 새로 할당된 공간에 대한 포인터를 사용자에게 반환하고 실패했을 경우 NULL을 반환한다.

 

malloc을 사용하기 위해서는 stdlib 라이브러리를 추가해야 한다. 사실 이 일을 할 필요는 없다. 모든 C 프로그램에 디폴트로 링크되는 C 라이브러리는 malloc() 함수를 가지고 있기 때문이다. 헤더 파일을 추가하면 malloc()을 올바르게 호출했는지 컴파일러가 검사할 수 있다. 전달된 인자의 개수가 맞는지 올바른 데이터 타입의 인자를 전달했는지 검사하게 된다.

 

malloc()의 매개변수는 size_t 타입의 변수이고 이 변수는 필요 공간의 크기를 바이트 단위로 표시한 것이다. 대부분 프로그래머는 숫자를 직접 입력하지 않는다. 대신 다양한 루틴과 매크로가활용된다.  double 형의 부동 소수점 값을 위한 공간을 확보하기 위해서는 다음과 같이 하면 된다.

double *d = (double*)malloc(sizeof(double));

 

malloc() 호출에서는 정확한 크기의 공간을 요청하기 위하여 sizeof() 연산자를 사용한다. C 언어에서 sizeof()는 통상 컴파일 시간 연산자이다. 인자의 실제 크기가 컴파일 시간에 결정된다. sizeof()는 숫자로 대체되어 malloc()에 전달된다. 이러한 이유로 sizeof()는 연산자로 간주되는 게 맞으며 함수 호출이 아니다.(함수 호출은 실행 시간에 일어난다)

 

데이터 타입뿐 아니라 변수의 이름도 sizeof()의 인자로 전달할 수 있다. 그러나 원하는 결과를 얻지 못할 때도 있으므로 조심해야 한다. 예를 들어, 다음과 같은 코드를 살펴보자.

 

첫 번째 행에서, 정수형 원소 10개를 가지는 배열을 위한 공간을 선언하였다. 그러나 다음 줄에서 sizeof()를 사용하면 4(32bit) 또는 8(64bit) 값을 반환한다. 이 경우 sizeof()는 동적으로 할당받은 메모리의 크기가 얼마인지가 아니라, 정수를 가리키는 포인터의 크기가 얼마인지 물어본다고 생각하기 때문이다. 그러나 때때로 sizeof()는 기대한 대로 동작할 때도 있다.

 

이 경우에는 변수 x에 40바이트가 할당되었다는 것을 컴파일러가 알 수 있는 정적인 정보가 충분하다. 

 

또 하나 조심해야 하는 경우는 문자열을 다룰 때이다. 문자열을 위한 공간을 선언할 때에는 다음과 같은 문장을 사용한다 : malloc(strlen(s) + 1). 이 문장은 strlen() 함수를 사용하여 문자열의 길이를 얻어낸 뒤 문자열-끝('\0')을 나타내는 문자를 위한 공간을 확보하기 위해 1바이트를 더한다. sizeof() 사용은 여기서 문제를 일으킬 수 있다.

 

malloc()은 void 타입에 대한 포인터를 반환한다. 그렇게 하는 것은 주소만 넘겨주고 해당 주소 공간에 어떤 타입의 자료를 저장할 지는 프로그래머가 결정하게 하는 전형적인 C의 방식이다. 프로그래머는 타입 변환(Type casting)을 이용하여 공간 활용을 결정한다.

 

캐스팅을 실제로 무언가를 하지는 않고, 컴파일러와 다른 프로그래머에게 "예, 제가 알아서 할게요"라고 말할 뿐이다. malloc()의 결과에 타입을 명시함으로써, 프로그래머는 정확한 타입을 "확인"한 정도이며, 프로그램이 제대로 동작하는 데 있어서 캐스트가 반드시 필요한 것은 아니다.

 

free() 함수

메모리 할당은 우리가 고민하고 있는 문제들 중 쉬운 쪽이다. 할당된 메모리를 언제 어떻게 해제하고 더욱이 해제 여부를 확인하는 것이 더 어려운 문제이다. 더 이상 사용되지 않는 힙 메모리를 해제하기 위해 프로그래머는 free()를 호출한다. 다음 그림들은 정수형 메모리를 힙 메모리에 할당하고 해제하는 과정에서의 메모리가 어떻게 변화하는지를 보여준다.

 

 

흔한 오류

malloc()과 free()를 사용하는 데 흔히 발생하는 오류가 많다. 무엇이 있는지 한번 살펴보자.

 

메모리 할당 잊어버리기

많은 루틴을 자신이 호출되기 전에 필요한 메모리가 미리 할당되었다고 가정한다. 예를 들어 strcpy(dst, src) 루틴은 소스 포인터에서 목적 포인터로 문자열을 복사한다. 그러나 주의 하지 않으면, 다음과 같이 코드를 작성할 수 있다.

 

이 코드를 실행하면 세그멘테이션 폴트(Segmentation fault)가 발생할 가능성이 높다. 이 폴트는 "네가 메모리 관련 무언가를 잘못했어, 이 바보 같은 프로그래머야, 그래서 나 화났거든"이라는 메세지이다. 그림으로 표현하면 다음과 같다.

 

이 경우, 올바른 코드와 그림으로 표현하면 다음과 같다.

 

 

메모리를 부족하게 할당받기

이 오류는 메모리를 부족하게 할당받는 것으로, 때때로 버퍼 오버플로우(Buffer overfloaw)라고 불린다. 이러한 오류에 대한 예는 다음과 같다.

 

dst가 hello 문자열을 src로 부터 복사하기에는 dst의 크기가 1바이트 작다. 하지만 strcpy() 함수는 제대로 동작한다. 과연 제대로 동작하는 것일까? 그림으로 한번 살펴보자. 

이 경우 안쓰는 dst가 할당된 공간의 마지막을 지나쳐 한 바이트 만큼 더 공간을 사용하게 된다. 이 공간은 더 이상 사용되지 않는 변수 영역이기 때문에 덮어쓰더라도 아무 피해가 발생하지 않을 때도 있다. 어떤 경우에는 잘 동작하고, 또 어떨때는 프로그램의 고장을 일으키고 크래시 된다. 이러한 오버플로우는 매우 유해할 수 있고 많은 시스템에서 보안 취약점의 원인이 된다. 

 

여기서 교훈을 얻을 수 있다 : 프로그램이 한 번 올바르게 실행된다고 하더라도, 프로그램이 올바르다는 것을 의미하지는 않는다. 

 

할당받은 메모리 초기화 하지 않기

이 오류의 경우, malloc()을 제대로 호출했지만 새로 할당받은 데이터 타입에 특정 값을 넣는 것은 잊는 것이다. 절대로 이러면 안된다. 초기화하지 않는다면, 프로그램은 결국 초기화되지 않은 읽기(Uninitialized read), 즉 힙으로 부터 알 수 없는 값을 읽는 일이 생긴다. 재수가 좋으면 프로그램이 여전히 잘 동작할 수 있는 값이 될것이고, 아니라면 임의의 값이나 해로운 값이 읽힐 것이다.

 

 

메모리 해제하지 않기

다른 일반적인 오류는 메모리 누수(Memory leak) 이다. 메모리 해제를 잊었을 때 발생한다. 장시간 실행되는 응용 프로그램이나 또는 운영체제 자체와 같은 시스템 프로그램에서는 아주 큰 문제이다. 메모리가 천천히 누수되면 결국 메모리가 부족하게 되고 시스템을 재시작 할 수 밖에 없다. 메모리의 사용이 끝나면 반드시 해제해야 한다. 

 

힙에 메모리를 할당하고도 해제를 하지 않아 사용 가능한 힙 메모리 영역이 점점 줄어든다.

쓰레기 수집 기능(Garbage collector) 이 있는 언어(예를들면 JAVA, C# 등)도 이 문제에는 도움이 되지 않는다. 메모리에 대한 참조가 존재하면, 어느 쓰레기 수집기도 그것을 해제하지 않을 것이고, 따라서 이런 현대적인 언어에서도 메모리 누수는 여전히 문제가 된다.

 

경우에 따라서 free()를 호출하지 않는 것이 타당해 보이는 경우도 있다. 예를 들어, 프로그램 실행 시간이 짧아, 실행 시작 후 곧 종료한다고 하자. 이 경우 프로세스가 죽으면, 운영체제는 할당된 페이지를 모두 정리하고 따라서 메모리 누수는 일어나지 않는다. 하지만 이와 같은 전략(?)은 매우 조심해서 사용해야 한다.

 

장기적으로 프로그래머로서 목표는 좋은 습관을 만드는 것이다. 중요한 습관 중 하나가 C 같은 언어에서 어떻게 메모리를 관리하는지 이해하고 할당받았던 메모리 블럭을 해제하는 것이다. 

 

메모리 사용이 끝나기 전에 메모리 해제하기

때때로 프로그램은 메모리 사용이 끝나기 전에 메모리를 해제한다. 그런 실수는 Dangling pointer라고 불리며 심각한 실수이다. 차후 그 포인터를 사용하면 프로그램을 크래시 시키거나 유효 메모리 영역을 덮어쓸 수 있다. 예를 들어, free()를 호출하고, 그 후 다른 용도로 malloc()을 호출하면 잘못 해제된 메모리를 재사용한다.

 

 

반복적으로 메모리 해제하기

프로그램은 가끔씩 메모리를 한 번 이상 해제하며 이중 해제(Double free)라 불린다. 결과는 예측하기 어렵다. 가장 흔히 일어나는 일은 크래시이다.

 

realloc() 함수

realloc() 함수는 현재 힙에 할당된 메모리의 크기를 변경시켜준다. 예를 들어 arr이라는 포인터에 동적 할당으로 메모리 공간을 할당해 주었다면, realloc으로 메모리 크기를 조정할 때 만약 arr이 있는 메모리 위치에 충분한 공간이 있다면 realloc 함수는 추가로 메모리 공간을 할당해준다. 

 

첫번째 매개변수로 이미 할당된 포인터를 받으며, 두번째 매개변수로 변경할 크기의 값을 받는다. 성공하면 할당된 메모리 크기가 변경된 포인터를 반환하며, 실패시 NULL을 반환한다.

 

 실제 시스템 콜

malloc() 함수는 brk() 라는 시스템 콜을 사용한다. brk()는 메인 메모리에 할당된 프로세스(프로그램) 공간에서 주소 공간상의 힙의 끝 지점을 확장하기 위해 호출된다.

 

sbkr() 는 brk() 와 기능이 비슷한 추가적인 호출이다. 프로그래머는 직접 brk()와 sbrk()를 호출해서는 안된다. 

반응형

'OS' 카테고리의 다른 글

2-9장. 세그멘테이션  (0) 2020.03.13
2-8장. 주소 변환의 원리  (0) 2020.03.09
2-6장. 주소 공간의 개념  (0) 2020.03.02
2-5장. 멀티프로세서 스케줄링  (2) 2020.02.28
2-4장. 스케줄링 : 멀티 레벨 피드백 큐(MLFQ)  (2) 2020.02.25
Comments