본문 바로가기

OS 개발일지

[OS개발] 9일차 - 메모리를 깔끔하게 관리해보자

9일차

9일차에서는 메모리를 페이지 단위로 효율적으로 관리하는 방법을 알아본다.


# 본 내용으로 들어가기 전에 현재까지 OS가 사용하고 있는 메모리 테이블을 정리해보자.

 0x00000000 - 0x000FFFFF

 1 MB

 부팅용, 부팅 후에는 비어있음

 0x00100000 - 0x00267FFF

 1440 KB

 플로피 디스크 내용 기억

 0x00268000 - 0x0026F7FF

 30 KB

 비어있음

 0x0026F800 - 0x0026FFFF

 2 KB

 IDT

 0x00270000 - 0x0027FFFF

 64 KB

 GDT

 0x00280000 - 0x002FFFFF

 512 KB

 bootpack.hrb

 0x00300000 - 0x003FFFFF

 1 MB

 스택 등

 0x00400000 - 

 

 비어있음



# 먼저 메모리의 용량을 체크해본다. QEMU에뮬에서는 32MB 만큼의 메모리를 사용할 수 있게 되어있다.에게... 빌 뭐시기 누가 떠오른다.

메모리 체크전에 먼저 해야할 일은 CPU의 캐시 기능을 끄는 것이다. 캐시는 최근에 읽은 메모리의 주소와 값을 저장해 놓았다가 나중에 필요하면 메모리를 읽지 않고 캐시에서 바로 값을 전달한다. 따라서 메모리 체크시 제멋대로 끼어들어버리므로 정확한 체크가 불가능하다.

캐시 기능을 끄는 함수는 다음과 같다.

#define EFLAGS_AC_BIT		0x00040000
#define CR0_CACHE_DISABLE	0x60000000

unsigned int memtest(unsigned int start, unsigned int end)
{
	char flg486 = 0;
	unsigned int eflg, cr0, i;

	/* 386인가,  486이후인가의 확인 */
	eflg = io_load_eflags();
	eflg |= EFLAGS_AC_BIT; /* AC-bit = 1 */
	io_store_eflags(eflg);
	eflg = io_load_eflags();
	if ((eflg & EFLAGS_AC_BIT) != 0) { /* 386에서는 AC=1으로 해도 자동으로 0에 돌아와 버린다 */
		flg486 = 1;
	}
	eflg &= ~EFLAGS_AC_BIT; /* AC-bit = 0 */
	io_store_eflags(eflg);

	if (flg486 != 0) {
		cr0 = load_cr0();
		cr0 |= CR0_CACHE_DISABLE; /* 캐쉬 금지 */
		store_cr0(cr0);
	}

	i = memtest_sub(start, end);

	if (flg486 != 0) {
		cr0 = load_cr0();
		cr0 &= ~CR0_CACHE_DISABLE; /* 캐쉬 허가 */
		store_cr0(cr0);
	}

	return i;
}

486 이후의 CPU만을 대상으로 캐시 기능을 OFF 시키는데, 검사방법으로 AC 플래그를 사용한다.
0x00040000 자리에 AC플래그가 존재하므로 eflags를 끄집어 내서 OR 명령으로 AC플래그를 1로 만든다.
이 녀석을 다시 eflags에 로드 시켰을때 486 이전 386 CPU는 AC플래그 값이 0으로 바뀐다.
따라서 다시 꺼낸 eflags의 AC플래그 값이 0이 아닌 경우 캐시 기능을 OFF 시킨다.

캐시 금지 및 허가 함수는 어셈블러로 작성되어 있다.

_load_cr0:		; int load_cr0(void);
		MOV		EAX,CR0
		RET

_store_cr0:		; void store_cr0(int cr0);
		MOV		EAX,[ESP+4]
		MOV		CR0,EAX
		RET


필자가 메모리 체크를 위해 사용하는 방법은 이렇다.

   우선 메모리의 값을 백업
-> 메모리에 특정값을 써본다
-> 그 값을 반전시켜본다
-> 잘 반전되었나?
-> 다시 한번 반전시켜본다
-> 잘 돌아왔나?
-> 백업값을 복구

이 프로세스를 코드로 작성하면...

unsigned int memtest_sub(unsigned int start, unsigned int end)
{
	unsigned int i, *p, old, pat0 = 0xaa55aa55, pat1 = 0x55aa55aa;
	for (i = start; i <= end; i += 0x1000) {
		p = (unsigned int *) (i + 0xffc);
		old = *p;			/* 이전의 값을 기억해 둔다 */
		*p = pat0;			/* 시험삼아 써 본다 */
		*p ^= 0xffffffff;	/* 그리고 그것을 반전해 본다 */
		if (*p != pat1) {	/* 반전 결과가 되었는지?  */
not_memory:
			*p = old;
			break;
		}
		*p ^= 0xffffffff;	/* 한번 더 반전해 본다 */
		if (*p != pat0) {	/* 원래대로 돌아갔는지?  */
			goto not_memory;
		}
		*p = old;			/* 원래대로 되돌린다 */
	}
	return i;
}

0x1000 씩 이동하며 메모리를 체크하는 함수이다.

메인함수에서는 이렇게 쓰이고 있다.

/* HariMain() */
i = memtest(0x00400000, 0xbfffffff) / (1024 * 1024);
sprintf(s, "memory %dMB", i);
putfonts8_asc(binfo->vram, binfo->scrnx, 0, 32, COL8_FFFFFF, s);

0x00400000 이전은 이미 사용하고 있으니 패스, 0xBFFFFFFF 까지 검사하므로 3GB까지만 검사한다. 그리고 결과는 1024의 제곱으로 나누어 MB로 표기한다.



# 여기서 문ㅋ제ㅋ발ㅋ생ㅋ 이대로 OS를 실행시켜보면 메모리가 3072MB(최대값)라고 나온다.

문제가 뭐인고 하니, 컴파일러의 최적화 기능 때문이란다. 컴파일된 memtest_sub의 어셈블로 코드를 확인해보면

_memtest_sub:
	PUSH	EBP
	MOV	EBP,ESP
	MOV	EDX,DWORD [12+EBP]
	MOV	EAX,DWORD [8+EBP]
	CMP	EAX,EDX
	JA	L30

딱 보기에도 뭔가 짧다...


이것을 C언어로 옮기면

unsigned int memtest_sub(unsigned int start, unsigned int end)
{
	unsigned int ;
	for (i = start; i <= end; i += 0x1000) { }
	return i;
}


메모리에 값을 쓰거나 읽지만 사용하지도 않고 원래대로 돌리기까지 하니 컴파일러 입장에서는 불필요한 코드라 생각하고 po생략wer 하기 때문에 모든 메모리가 정상으로 표시되는것이다. 따라서 이 코드는 어셈블러로 작성해야한다.

_memtest_sub:	; unsigned int memtest_sub(unsigned int start, unsigned int end)
		PUSH	EDI						; (EBX, ESI, EDI 도 사용하고 싶기 때문에)
		PUSH	ESI
		PUSH	EBX
		MOV		ESI, 0xaa55aa55			; pat0 = 0xaa55aa55;
		MOV		EDI, 0x55aa55aa			; pat1 = 0x55aa55aa;
		MOV		EAX,[ESP+12+4]			; i = start;
mts_loop:
		MOV		EBX,EAX
		ADD		EBX, 0xffc				; p = i + 0xffc;
		MOV		EDX,[EBX]				; old = *p;
		MOV		[EBX], ESI				; *p = pat0;
		XOR		DWORD [EBX], 0xffffffff	; *p ^= 0xffffffff;
		CMP		EDI,[EBX]				; if (*p ! = pat1) goto fin;
		JNE		mts_fin
		XOR		DWORD [EBX], 0xffffffff	; *p ^= 0xffffffff;
		CMP		ESI,[EBX]				; if (*p ! = pat0) goto fin;
		JNE		mts_fin
		MOV		[EBX], EDX				; *p = old;
		ADD		EAX, 0x1000				; i += 0x1000;
		CMP		EAX,[ESP+12+8]			; if (i <= end) goto mts_loop;
		JBE		mts_loop
		POP		EBX
		POP		ESI
		POP		EDI
		RET
mts_fin:
		MOV		[EBX], EDX				; *p = old;
		POP		EBX
		POP		ESI
		POP		EDI
		RET

그리고 다시 테스트 하면...



# 본격적으로 메모리 관리에 들어간당.
메모리 관리는 사용중인 메모리와 남은 메모리의 상태을 기록하여 메모리의 침범을 막도록 도와준다.

먼저 메모리의 상태를 기록할 방법에 대해서이다. 하리보테 OS에서는 빈 메모리의 시작 번지와 크기를 기록해두는 방법을 사용한다. size단위는 100KB이다. 이를 구조체로 나타내면-

struct FREEINFO {	/* 빈 정보 */
	unsigned int addr, size;
};

struct MEMMAN {		/* 메모리 메니지먼트 */
	int frees, maxfrees, lostsize, losts;
	struct FREEINFO free[MEMMAN_FREES];
};


다음은 메모리의 할당과 해제에 대한 이야기이다. 할당 시에는 위 MEMMAN 구조체를 뒤져서 필요한 만큼의 빈 메모리가 있는지 확인하고 있다면 그 블록의 시작 번지를 할당한 만큼 뒤로 민다.

unsigned int memman_alloc(struct MEMMAN *man, unsigned int size)
/* 확보 */
{
	unsigned int i, a;
	for (i = 0; i < man->frees; i++) {
		if (man->free[i].size >= size) {
			/* 충분한 넓이의 빈 영역을 발견 */
			a = man->free[i].addr;
			man->free[i].addr += size;
			man->free[i].size -= size;
			if (man->free[i].size == 0) {
				/* free[i]가 없어졌으므로 앞으로 채운다 */
				man->frees--;
				for (; i < man->frees; i++) {
					man->free[i] = man->free[i + 1]; /* 구조체의 대입 */
				}
			}
			return a;
		}
	}
	return 0; /* 빈 곳이 없다 */
}

해제 시가 조금 복잡한데, 일단 앞뒤의 블록과의 연속성을 조사해서 한 덩어리로 합칠 수 있다면 합치고, 그렇지 못하면 새로운 블록을 만들고 MEMMAN의 배열도 재정렬해야한다. 백문이 불여일견이라고, 직접보자.

int memman_free(struct MEMMAN *man, unsigned int addr, unsigned int size)
/* 해방 */
{
	int i, j;
	/* 정리하기 쉽게 생각하면 free[]가 addr순서로 줄지어 있는 편이 좋다 */
	/* 그러니까 우선 어디에 들어갈 것인가를 결정한다 */
	for (i = 0; i < man->frees; i++) {
		if (man->free[i].addr > addr) {
			break;
		}
	}
	/* free[i - 1].addr < addr < free[i].addr */
	if (i > 0) {
		/* 앞이 있다 */
		if (man->free[i - 1].addr + man->free[i - 1].size == addr) {
			/* 전의 빈 영역을 정리한다 */
			man->free[i - 1].size += size;
			if (i < man->frees) {
				/* 뒤도 있다 */
				if (addr + size == man->free[i].addr) {
					/* 뒤와도 정리한다 */
					man->free[i - 1].size += man->free[i].size;
					/* man->free[i]의 삭제 */
					/* free[i]가 없어졌으므로 앞으로 채운다 */
					man->frees--;
					for (; i < man->frees; i++) {
						man->free[i] = man->free[i + 1]; /* 구조체의 대입 */
					}
				}
			}
			return 0; /* 성공 종료 */
		}
	}
	/* 앞과는 정리하지 않았다 */
	if (i < man->frees) {
		/* 뒤가 있다 */
		if (addr + size == man->free[i].addr) {
			/* 뒤와는 정리한다 */
			man->free[i].addr = addr;
			man->free[i].size += size;
			return 0; /* 성공 종료 */
		}
	}
	/* 앞에도 뒤에도 정리하지 않는다 */
	if (man->frees < MEMMAN_FREES) {
		/* free[i]보다 뒤를, 뒤로 이동시키고 사이를 만든다 */
		for (j = man->frees; j > i; j--) {
			man->free[j] = man->free[j - 1];
		}
		man->frees++;
		if (man->maxfrees < man->frees) {
			man->maxfrees = man->frees; /* 최대치를 갱신 */
		}
		man->free[i].addr = addr;
		man->free[i].size = size;
		return 0; /* 성공 종료 */
	}
	/* 뒤로 옮겨 놓을 수 없었다 */
	man->losts++;
	man->lostsize += size;
	return -1; /* 실패 종료 */
}

좀 길지만 난해한 코드는 없으므로 차분히 읽어내려가면 충분히 이해할 수 있다.


그 밖에 필요한 함수를 작성해보자

void memman_init(struct MEMMAN *man)
{
	man->frees = 0;			/* 빈 영역 정보의 개수 */
	man->maxfrees = 0;		/* 상황 관찰용:frees의 최대치 */
	man->lostsize = 0;		/* 해방에 실패한 합계 사이즈 */
	man->losts = 0;			/* 해방에 실패한 회수 */
	return;
}

unsigned int memman_total(struct MEMMAN *man)
/* 빈 영역 사이즈의 합계를 보고 */
{
	unsigned int i, t = 0;
	for (i = 0; i < man->frees; i++) {
		t += man->free[i].size;
	}
	return t;
}

그리고 메인 함수에 적용!

/* HariMain() */
memtotal = memtest(0x00400000, 0xbfffffff);
memman_init(memman);
memman_free(memman, 0x00001000, 0x0009e000); /* 0x00001000 - 0x0009efff */
memman_free(memman, 0x00400000, memtotal - 0x00400000);


632 + 28672 = 29304KB


예이! 방학!...한지 벌써 일주일 후지만ㅋ 요즘은 아두이노랑 라즈베리 파이가 재미있어서 이쪽은 속도가 영 안 붙는다. 그래도 끝은 봐야지!


참조: <OS 구조와 원리> - 카와이 히데미