또 간만에 C 코딩할 일이 생겼다. 어쩌다 한번씩 코딩을 하다보니 실력이 늘지를 않는건 당연한 일이고 뭐하나 짜려면 시간이 꽤나 걸린다. 하지만 어쩌다 한번 코딩을 하니 재미가 있는건 사실이다.
메모리 할당과 릴리즈 반복 프로그램
이번에 짤 프로그램은 서버의 메모리를 고갈시키는 프로그램이다. 메모리를 할당받았다가 릴리즈하는 작업을 반복하는 프로그램이다. 예를 들어 10개의 프로세스가 각 10 M Byte의 메모리를 100번씩 할당받았다 해제하는 것이다.
즉 10 M Byte * 10 * 100 = 1 Tera Byte 를 할당받았다 해제했다를 반복하는 프로그램이다. 하지만 메모리의 할당과 해제 만으로는 서버에 거의 부하를 줄 수 없다는 것이 중요하다. 실제로 4G Byte의 메인메모리를 가진 서버에서 1 Tera Byte의 메모리를 할당 받는 프로그램을 실행해도 아무런 문제가 없었다. (CPU 사용율이 1%도 증가하지 않을 것이다. 또한 할당(malloc()) 만으로는 실제 메모리 액세스도 거의 발생하지 않는다. 즉, 부하가 없다.)
이 포스트를 방문할 정도의 사람이라면 대부분 “가상 메모리 관리”라는 운영체제의 개념을 알고 있을 것이다.
10개의 프로세스가 10 M Byte의 메모리를 각각 100번 씩 할당 받으면 운영체제는 가상메모리에 우선 할당하고(몇바이트 몇페이지만 할당했다고 표시만 하는 듯..) 프로세스 영역에는 할당된 가상메모리의 각각의 페이지를 관리하기 위한 오버헤드만 기록하고 만다. 그래서 실제로 아래 화면과 같이 ps aux 명령을 실행했을 때 표시되는 메모리 항목 2개의 값중 RSS (RSZ라고도 함) 실제 램에 상주하여 차지하고 있는 메모리의 크기)는 약 4K 정도의 오버헤드 정도만 malloc()에 의해 할당 받을 때 증가한다.
10개의 프로세스가 10 M Byte를 100번 할당 받는 동안 메모리 사용율은 보통 다음 방법으로 모니터링할 수 있다.
첫번째가 ps aux 명령이다. 그 중에서도 VSZ와 RSS 두 항목이 프로세스별 메모리 사용량가 비율을 표시해준다. 하지만 실제로 해당 프로세스에 할당된 메모리의 양이 아니라 현재 실제로 사용중인 메모리의 양이라고 봐야한다. 즉, 할당 받은 메모리를 언제 사용할지 알 수 없는 것이다. 또한 할당 받은 메모리를 어떻게 사용하고 있는지도 알 길이 없다.
두번째는 vmstat다. vmstat는 ps aux와는 달리 프로세스별 메모리가 아닌 시스템 전체의 메모리 사용량을 보여준다.
일반적으로 사용하는 세번째 방법이 바로 top 명령이다.
top은 시스템 전체의 메모리 사용율과 CPU사용율이 상위에 랭크되는 프로세스의 메모리 사용량을 모두 보여준다. 단, 여기서도 할당받은 메모리양이 아닌 할당 받은 뒤 실제 사용되는 메모리 사용량을 보여준다는 것을 꼭 기억해야 한다.
이 세가지 툴로 아무리 봐도 할당만 했을 때는크게 달라지는 점이 없다. 1 Tera의 무지막지한 메모리를 할당해달라고 하면 군소리 없이 할당해준다. 그리고 위의 top 화면에서 보이는 Mem의 Used 항목은 할당 받은 양에 비해 너무 작은 양이 증가한다.
이렇게 많은 메모리를 할당 받았음에도 해당 프로세스의 메모리 사용량을 보면 할당받은 메모리 양보다 너무도 작게 보이는 이유는 할당만 받아놓고 실제 사용하지(접근하지) 않기 때문이다. 또한 실제 접근(사용)하지 않으면 메인메모리(RSS)나 가상메모리(VSZ)를 차지하지 않는다.
할당 받은 메모리를 실제 메모리에 로드하는 방법은 ?
방법은 간단하다. 실제 할당 받은 메모리에 4 K byte 영역마다 한번 씩 실제 액세스를 하는 것이다. 이렇게 4K(1개의 페이지) 마다 실제 액세스(값을 쓰기)하기 시작하면 top의 Mem항목에서 Used 항목의 값이 급격하게 증가하기 시작한다. (당연히 free는 줄어듦) 가상메모리에 할당만 해놨던 메모리의 각 페이지에 값이 저장되므로 실제 메모리에 페이지를 올리기 시작함.)
그리고 실제 메모리가 모두 차게되면 그때부터 Swap의 Used가 증가하기 시작한다. 즉 실제 메모리가 부족하므로 이미 값을 쓴 페이지를 swap 공간으로 내리고 내린만큼 메인메모리를 확보하여 계속 쓰기를 수행하는 것이다. 그리고 Swap의 Used가 Total과 같아지는 순간 더 이상 메인메모리를 확보할 수 없으므로 시스템은 멈춘다. (vi실행 등등 어떠한 작업도 불가능 하다.)
할당만 받았을 때는 실제 데이터가 없으므로 할당받은 것으로 표시만 해둠으로 해서 실 메모리는 4G Byte밖에 없지만 2의64제곱에 해당되는 번지수 만큼 메모리를 할당 받을 수 있지만 4 K마다 실 데이터를 Write 해서 해당 페이지를 실제 사용하면 운영체제는 해당 페이지를 실메모리(램)에 올려두거나 스왑공간에 유지해야 하는데 더 이상 스왑공간조차 모두 사용하게 되면 더 이상 메모리를 확보하지 못하는 상태가 되어 완전한 메모리 사용률 100%를 만들 수 있다.
이제 프로세스의 개수와 하나의 프로세스가 할당 받을 메모리의 양을 적절하게 조절하고 할당과 해제를 반복하면 메모리의 부하를 주는 아주 기본적인 프로그램이 완성된다.
이 소스를 기반으로 메모리에 다양한 부하를 주는 프로그램을 구미(?)에 맞게 만들면 되겠다.
#include <sys/types.h>
#include
#include
#include
int main(void)
{
pid_t pid, ppid;
int intTmp, intTmp2;
// 생성할 자식 프로세스의 개수
int intChildsCt = 10;
//자식프로세스의 PID저장할 목록(불필요)
int intChilds[10];
// 프로세스 당 메모리를 할당 받을 횟수
int intMemAllocCt = 100;
// 위의 횟수만큼 할당받은 메모리의 포인터를 저장할 포인터배열
char *cpStr[100];
// 한번 메모리 할당 받을 때의 메모리 크기(Byte)
int intMemSize=10240000;
// 한번 할당받은 메모리를 4K Byte 단위로 나누었을 때의 조각의 개수
int intPageCt = intMemSize/4096;
// 부모의 PID 획득
ppid = getpid();
pid = 1;
// 자식 프로세스를 intChildsCT에서 지정한 만큼 반복하여 생성
for (intTmp=0; intTmp < intChildsCt; intTmp++)
{
if (pid != 0) // 부모만 fork()가 실행되도록
{
// 자식을 생성(fork())하고 부모프로세스는 pid에 자식 pid를 받음. 자식에게는 0이 리턴됨.
if ((pid=fork()) < 0)
{
printf("%d'th child process fork error\n", intTmp);
return 2;
}
else if (pid != 0) // 부모는 pid를 배열에 저장.
{
intChilds[intTmp] = pid;
}
}
}
// for() 후 부모프로세스가 실행할 루틴.
if ( pid != 0 )
{
printf ("==== ChildProcess List====\n");
for (intTmp=0; intTmp < intChildsCt; intTmp++)
{
printf ("%d'st Child process pid : %d\n", intTmp, intChilds[intTmp]);
}
}
// fork() 후 자식 프로세스가 실행할 루틴.
if ( pid == 0 )
{
printf ("Child Routine...\n");
// 생성된 자식은 intMemAllocCt에서 지장한 횟수만큼 반복하여 메모리 할당
// 총 할당받는 메모리는 intChildsCt(자식의 개수) * intMemAlloct(자식 당 할당받을 횟수) * intMemSize(1회에 할당받을 메모리) 만큼 임.
for (intTmp2=0; intTmp2<intMemAllocCt; intTmp2++)
{
// 메모리를 할당하고 첫 주소를 cpStr[]에 저장. (포인터를 이해하려면 아래 라인을 완전하게 이해해야 함.)
cpStr[intTmp2] = (char *)malloc(intMemSize);
// 할당받은 메모리의 모든 바이트에 알파벳 K를 쓴다. (이 라인을 활성화하면 CPU 100% 침.
// 단, 시스템이 금방 멈추지는 않지만 메모리가 부족하게 되면 멈출 수도 있음.
//for (intTmp=0; intTmp<intMemSize; ++intTmp) *((char *)cpStr+intTmp)='K';
printf("Child %d , Memory Allocate : %d\n", getpid(), intTmp2);
sleep(1);
}
printf("ppid = %d, getpid = %d\n", ppid, getpid());
//printf("%s\n", (char*)cpStr);
// 10초간 대기
sleep (10);
// 각 자식 프로세스는 자신이 할당받은 메모리 횟수 만큼에 대해
for (intTmp2=0; intTmp2<intMemAllocCt; intTmp2++)
{
할당받은 메모리를 4096바이트(page (4K))로 나눈 몫의 횟수만큼 (즉 할당받은 페이지의 수 만큼) 반복
for (intTmp=1; intTmp<intPageCt; intTmp++)
{
//각 페이지의 첫번째 바이트에 'K'를 쓴다. 즉 할당받은 모든 페이지를 실제로 접근 발생시켜 실제 사용하는 페이지로 만든다.
// 아래 라인의 의미를 이해하고 원하는 동작을 하도록 수정할 수 있다면 포인터의 기본을 이해하고 사용할 수 있는 수준이라 볼 수 있음.)
*((char *)cpStr[intTmp2]+(4096*intTmp)) = 'K';
}
}
// 할당받은 메모리의 모든 페이지를 한번씩 액세스하고 2초간 대기
sleep (2);
// 할당받은 메모리를 반환한다.
for (intTmp2=0; intTmp2<intMemAllocCt; intTmp2++) {
free(cpStr[intTmp2]);
printf("pid=%d, %d is free...\n", getpid(), intTmp2);
}
}
//프로그램 종료
return 0;
}