도입
메모리 누수가 있는 프로그램은
메모리 누수가 있는 프로그램은 여러가지 문제를 만듭니다. 예상치 못한 메모리 누수로, 가동 시간이 길어질 수록 더 많은 램을 잡고 놓아주지 않는 프로그램은 램 가용율 뿐 아니라 보안에도 큰 영향을 미칠 수 있습니다.
어떤 운영체제는 프로세스가 종료되면
어떤 운영체제들은 프로세스가 종료되면 자동으로 사용중인 메모리를 모두 모아 반환하는 기능을 가지고 있어서 메모리 누수가 발생하더라도 프로세스가 종료되면 새어나간 메모리가 반환되지만, 이 기능만을 믿고 할당한 메모리를 제대로 반환하지 않는 프로그램을 작성한다면, 해당 기능이 없는 OS에서는 프로그램을 정상적으로 사용할 수 없습니다. 이 프로그램은 OS의 해당 기능에 종속되어 있다고 생각할 수도 있겠습니다.
메모리 누수를 막기 위해서는
메모리 누수를 막기 위해서는 빌려서 사용한 메모리 공간을 free 함수를 이용해 반환 해 주어야 합니다. 필요할 때 할당해서 잘 쓰고, 함수가 끝나기 전에, 또는 프로그램이 종료되기 전에 free 함수를 호출해 주기만 하면 됩니다. 매우 간단하지요?
할당해제를 복잡하게 만드는 요소가 있는데
이 간단한 규칙을 복잡하게 만드는 요소가 있는데, 바로 스코프와 분기문입니다.
예를 들어
main 함수는 a함수를 호출하고, a 함수는 b 함수를 호출합니다.
main 은 a함수를 호출하기 전에 m에 메모리를 할당합니다.
int main(void)
{
...
m = malloc(sizeof(int) * 3);
if (m == 0)
return(1);
...
temp = a(var1, var2);
...
}
C
복사
int a(int v1, int v2)
{
...
b(var3, var4);
...
}
C
복사
b 에서 오류가 난 경우, Error를 출력하고 프로그램을 종료해야 한다고 해 봅시다, 이때 메모리 누수를 막기 위해 m을 할당해제 해야 한다면, b와 b에서 실행하는 함수들은 m의 값에 접근할 수 없기 때문에, b에서 바로 m을 할당해제하고 프로그램을 종료할 수가 없습니다.
이러한 문제들은 함수의 관계와 프로그램의 흐름을 잘 정리하는 것으로 해결할 수 있지만, 프로그램이 커질수록 많은 고민이 필요하게 됩니다.
Collector
자바, 파이썬 등에서 볼 수 있는 가비지 컬렉터를 수동으로 구현한 것입니다.
Collector는 프로그램이 종료될 때 반환되어야 하는 메모리를 효과적으로 관리할 수 있게 해 줍니다.
콜렉터는 입력에 따라 다음과 같이 동작합니다.
void collector(void *pointer);
1. pointer != 0 : 해당 포인터를 저장한다.
2. pointer == 0 : 지금까지 저장한 포인터를 모두 지우고 free한다.
C
복사
콜렉터는 다음과 같이 사용할 수 있습니다.
1.
다음은 위의 예시에서 exit과 Collector 을 이용해 예외처리를 하는 방법입니다. “→ collector()”
int main(void)
{
...
m = malloc(sizeof(int) * 3);
-> collector(m);
if (m == 0)
return(1);
...
temp = a(var1, var2);
...
}
C
복사
int a(int v1, int v2)
{
...
b(var3, var4);
...
}
C
복사
int b(int v3, int v4)
{
...
if (예외)
-> collector(0);
exit(1);
...
}
C
복사
2.
Collector는 여러 변수와 함수를 걸처 널가드를 할 때 매우 유용합니다. 다음은 Collector을 이용해 널가드를 한 예시입니다.
int main(void)
{
...
a = malloc(sizeof(int) * 3);
-> collector(a);
if (a == 0)
return(1);
b = malloc(sizeof(int) * 3);
-> collector(m);
if (b == 0)
return(1);
...
temp = func_1(var1, var2);
...
}
C
복사
int func_1(int v1, int v2)
{
...
c = malloc(sizeof(int) * 3);
-> collector(m);
if (c == 0)
-> exit(1);
...
}
C
복사
Collector의 기본 소스는 다음과 같습니다.
void collector(void *pointer)
{
static int cur;
static void *tape[10000];
if (pointer)
{
tape[cur++] = pointer;
}
else
{
while (cur--)
{
free(tape[cur]);
}
cur = 0;
}
}
C
복사
팁
•
구조체를 이용해 tape과 cur을 함수 밖에 저장한다면, 동시에 여러가지 tape를 사용할 수 있습니다.
◦
이렇게 사용하면, 프로그램 뿐 아니라, 각각의 함수마다 Collector을 이용해서 메모리를 관리할 수 있습니다.
◦
이렇게 사용하는 경우, 콜렉터가 다 사용한 tape도 함께 free 하도록 할 수 도 있습니다.
•
Collector에서 pointer 변수에 0이 들어왔을 때, free 뿐만 아니라, exit 함수 또한 호출한다면, 널가드 코드를 더 간결하게 작성할 수 있습니다.
//before
a = malloc(sizeof(int) * 3);
collector(a);
if (a == 0)
return(1);
//after
a = malloc(sizeof(int) * 3);
collector(a);
C
복사
•
널가드가 프로그램 대신 함수를 종료하도록 하고 싶다면 collector 가 int를 반환하도록 한 뒤 다음과 같이 작성할 수 도 있습니다.
//after
a = malloc(sizeof(int) * 3);
if (collector(a))
return(1);
C
복사
•
할당한 메모리와 더불어, 복잡한 구조체도 저장하고 한번에 지우고 싶다면
◦
tape의 짝수 칸에는 포인터를, tape의 홀수 칸에는 삭제 함수 포인터를 저장하도록 할 수도 있습니다.
◦
구조체 내부 변수를 위한 공간을 할당할 때마다 collector에 담으면, 기존 함수로도 복잡한 구조체의 메모리를 할당 해제할 수 있습니다. 이 방법을 추천합니다.
주의사항
•
collector에 추가한 메모리는 collector(0) 이외의 방법으로 free되면 안 됩니다.
◦
이 경우 더블프리 오류가 발생할 수 있습니다.
◦
구조체와 할당 해제 함수 포인터를 같이 저장하도록 수정한 경우, 구조체 내부의 변수가 collector에 추가되어 double free 되자 않도록 주의 해야 합니다.
•
한번 추가한 포인터는 추가된 다른 포인터와 동시에 free 되어야 합니다.
◦
반복문에서 메모리를 할당해 collector에 저장하는 경우, 메모리를 비효율적으로 사용할 가능성이 있습니다.
◦
프로그램 또는 함수가 종료되기 직전까지 사용하는 메모리가 아닌 경우 tape를 나누어 관리하거나, collector가 아닌 다른 방법을 사용해서 관리하는 것이 좋습니다.
•
기본 함수는 Static으로 구현되어 있기 때문에, 어디서 호출하더라도 scope에 영향을 받지 않습니다.
정리
1.
Collector는 프로그램 또는 함수가 종료될 때, 반환해야 하는 메모리를 효과적으로 관리해 줍니다.
2.
Collector는 여러 함수에 걸쳐 여러 변수에 할당된 메모리를 간편하게 관리할 수 있습니다.
3.
Collector는 용도에 맞추어 쉽게 수정해서 사용할 수 있습니다.
4.
Collector는 코드를 간결하게 만드는데 도움을 줍니다.
© snoh@42seoul
C
복사