OS를 만듭시다. 어때요~ 참 쉽죠? (8)

나빌레라의 이미지

OS. 영어로 풀어서 Operating System. 한자어로 번역해서 운영체제. 이것을 만든다고 하면 사람들은 굉장히 어렵게 생각한다. 리눅스나 윈도우같은 OS를 비교대상으로 본다면 OS를 만드는것은 정말 어렵고 힘들고, 개인이 만들기엔 어쩌면 불가능에 가까운 도전일지도 모른다. 하지만 OS의 기본 개념들은 대학교 학부과정에서 가르칠 정도로 이미 보편화 되어 있고 그 개념 자체들은 그다지 어렵지 않다. 개념 구현을 중심으로 동작하는 것 자체에 의미를 둔 OS를 만드는 것은 어쩌면 도전 해 볼만 한 가치가 있는 시도가 아닐까.

목차


1회
2회
3회
4회
5회
6회
7회

8. 테스크 스택 할당자

오늘도 우리는 버릇처럼 컴퓨터를 켜고, 각자 자신이 사용하는 OS가 부팅되기를 기다린다. 그리고 OS가 부팅되고 나면 어떤이는 웹서핑을 하고 어떤이는 게임을 하고 어떤이는 메일확인을 하고 어떤이는 음악을 듣고... 자기가 컴퓨터를 이용해 하고 싶은 것을 한다.

웹서핑, 게임, 메일확인, 음악... 어떻게 하는 걸까. 파이어폭스나 익스플로어를 실행시켜서 웹서핑을 하고 스타크레프트같은 프로그램을 실행시켜서 게임을 하고 에볼루션이나 아웃룩같은걸 실행시켜 메일확인을 한다. 앞 문장에서 반복되는 말이 있다. '프로그램을 실행시킨다'. OS 에서 실질적으로 사용자가 원하는 동작을 하는 녀석들은 바로 프로그램들이다. 이들 프로그램들을 실행시켜서 우리는 컴퓨터로 글도 쓰고 음악도 듣고 웹서핑도 하는 것이다.

이렇게 실행되어 돌고 있는 프로그램들을 보통 프로세스라고 부른다. 관점의 차이가 있을 지 모르겠지만 프로세스는 여러개의 쓰레드를 포함한다. 하지만 리눅스에서는 프로세스와 쓰레드의 구분이 모호하기도 하다. 이 강좌에서 프로세스와 쓰레드의 구분을 명확히 하는 것은 중요하지 않다. 단지 OS 에서 동작하는 개별 프로그램들을 프로세스 혹은 쓰레드 이런식으로 부른다는 것을 알아두기 바란다.

그리고 또 나오는 용어로 테스크라는 것이 있다. 나빌눅스는 임베디드OS 이다. 리눅스나 윈도우 처럼 응용프로그램이 별도로 파일 형태로 존재하고 있다가 명시적으로 실행을 시키면(마우스로 실행파일을 클릭하거나 쉘에서 실행파일이름을 타이핑하고 엔터를 누르는 행위를 말한다.) 메모리에 로드되어 프로세스가 되는 것이 아니라 커널이미지에 "응용프로그램의 역할을 하는 코드" 가 포함되어서 커널이 부팅되고 나면 같이 동작 하게 된다. 마치 리눅스의 커널쓰레드 같은 개념으로 생각 할 수 있다.

임베디드OS의 커널이 기본적인 부팅작업을 마치고, 커널이미지에 포함되어 같이 로딩되는 "응용프로그램의 역할을 하는 코드"를 프로세서가 수행중이라면 해당 코드 영역에 대응되는 테스크가 수행중이라고 말한다.

즉, 테스크는 임베디드OS에서 독립적인 실행단위로 볼 수 있다. 독립적인 실행단위는 독립적인 스택영역을 가져야 한다. 그래서 각 테스크를 추상화 하는 자료구조를 만들어 커널이 전역변수로 유지하고 있어야 하는데 이를 TCB(Task Control Block) 이라고 한다. TCB에 대해서는 다음 편에서 다룰 예정이다.

테스크는 독립적인 스택영역을 받는 다고 하였다. 누구로 부터 받는냐. 커널로 부터 받는다. 그럼 커널의 누구한테서 받느냐. 메모리 관리자로 부터 받는다. 리눅스나 윈도우의 메모리 관리자는 가상메모리도 사용하고 메모리 캐시도 사용하고 동적할당자, 커널슬랩 할당자 등등 메모리 관리자가 매우 복잡하다. 하지만 나빌눅스의 메모리 관리자는 여기서 더이상 간단해 질 수 없다. 라는 생각이 들 정도로 간단하다. 복잡하고 유용한 OS 보다는 이해하기 쉽고, 학습에 용이한 OS 를 지향하는 것이 나빌눅스이기에 나빌눅스를 이루고 있는 각 모듈은 더없이 간단하다. 강좌가 진행 될 수록 느끼게 될 것이다.

현 시점에서 구현하게될 나빌눅스의 메모리 관리자는 사실 메모리 관리자가 아니다. 관리를 안하기 때문에 관리자라고 부르기 좀 과분하다. 차라리 그냥 스택할당자 정도 수준이다. 테스크가 생성될때 스택할당자로 부터 스택의 시작 주소를 받아서 테스크 초기값에 넣는것이 테스크 생성과정에서 메모리 관련된 내용의 전부이다. 스택 할당자는 바로 여기서 테스크에 스택 시작주소를 할당해 주는 역할을 한다.

전 강좌에서 나빌눅스는 SDRAM의 상위 60Mbyte 를 테스크 스택영역으로 할당한다고 하였다. 그리고 각 테스크에는 무려 1Mbyte 씩의 영역을 할당한다고 하였다. 그럼 스택할당자가 하는 일은 어떤 순서로 되어야 할까.

우선 사용자 테스크로 할당할 메모리 영역의 시작 주소를 세팅한다. 나빌눅스의 경우는 4Mbyte 되는 지점이다. 그리고 스택영영의 끝이 될 주소도 세팅한다. 64Mbyte 가 되는 지점이다. 다음에 한번에 할당해줄 크기를 세팅한다. 1Mbyte 씩이다. 그리고서 이제 할당에 대한 API 인터페이스를 설계한다.

나빌눅스의 메모리 관리자는 실상 메모리를 관리하지 않고 있다고 위에 서술했다. 그래도 테스크에 할당할 메모리 각 블럭에 대해서 추상화 된 자료구조는 가지고 있어야 좀 OS 스럽지 않을까? 물론 그 내용은 아주 간단하겠지만 혹시라도 확장 가능성에 대해서 염두를 해 두야 하지 않을까 하는 생각이 든다.

메모리 블럭을 추상화 하는 자료구조에는 어떤 데이터가 있어야 할까. 우선 해당 블럭의 시작주소와 끝주소를 가지고 있어야 할것 같다. 그리고 해당 블럭이 사용중인지 아닌지에 대한 데이터가 있어야 할듯 하다. 그 외엔 뭐가 더 있어야 할까. 현 시점에선 그게 전부이다. 뭐 더 필요하다면 해당 블럭을 사용하고 있는 테스크 아이디 정도 들어가면 좋을랑가. 하지만 메모리 블럭 자료구조로 부터 역으로 테스크 컨트롤 블럭에 접근할 일은 별로 없을 것 같다. 일단은 해당 메모리 블럭의 시작 주소, 끝 주소, 사용중인지에 대한 플래그만 포함하는 메모리블럭 자료 구조를 만들어 보자, 이름은 너무도 평범하게 "자유 메모리 블럭 리스트" 라고 하겠다.

메모리관리자를 추가 할것이니 파일을 하나 새로 만들자, include 디렉토리 밑에 navilnux_memory.h 이다. 내용은 아래와 같다.

#ifndef _NAVIL_MEM
#define _NAVIL_MEM
 
typedef struct _navil_free_mem {
    unsigned int block_start_addr;
    unsigned int block_end_addr;
    int is_used;
} Navil_free_mem;
 
 
 
#endif

Navil_free_mem 이라는 구조체를 하나 선언해 놨다. 시작주소, 끝주소, 사용중인지. 이렇게 덜렁 세개의 변수만 있다. 정말 간단하지 않은가! OS 별거 없다. 그냥 계속 이런식으로 만들어 가는 것이다. 이제 필요한 것은 무엇인가... 저 자유메모리블럭리스트를 초기화 하고, 테스크 생성자가 메모리블럭을 하나 요청할때 마다 메모리 블럭을 할당해주는 할당자가 필요할 것이다. 그리고 이것들을 묶어서 그냥 메모리 관리자라고 하기로 했다.

같은 파일을 계속 수정한다. 코드는 아래와 같다.

#ifndef _NAVIL_MEM
#define _NAVIL_MEM
 
#define MAXMEMBLK 60
 
 
typedef struct _navil_free_mem {
    unsigned int block_start_addr;
    unsigned int block_end_addr;
    int is_used;
} Navil_free_mem;
 
typedef struct _navil_mem_mng {
    Navil_free_mem free_mem_pool[MAXMEMBLK];
 
    void (*init)(void);
    unsigned int alloc(void);
} Navil_mem_mng;
 
void mem_init(void);
unsigned int mem_alloc(void);
 
#endif 

왠지 메모리 관리자는 이걸로 끝날 것 같은 느낌이 든다. init() 함수는 메모리관리자를 초기화 해주는 일을 할것이고, alloc() 함수는 한번 호출할때마다 자유메모리블록리스트에서 가장 먼저 나오는 사용하지 않는 블럭의 시작주소를 반환해 주면서 해당 블럭을 사용중으로 표시해 주는 역할을 할것이다.

vm관련 처리를 하지 않으니 더이상 메모리 관련해서 해줄일은 별로 없다. 이후에 동적메모리 관리에 대한 부분을 설명하게 될때 메모리 관리자를 더 수정할 것이다. 하지만 지금 우리는 우선 먼저 동작하는 OS를 만드는 것이 목적이기 때문에 각 부분은 최대한 간결하고 쉽게 그리고 빠르게 작성하면서 넘어가도록 하자.

Navil_mem_mng 구조체에 init() 와 alloc() 의 함수포인터를 넣은것은 메모리 관리자 관련 자료구조와 함수들은 모두 Navil_mem_mng 구조체 하나의 접근경로를 통해서 접근 하도록 하기 위함이다. C++의 클래스 개념이라고 생각해도 좋고, 아니면 그냥 Navil_mem_mng 를 일종의 네임스페이스로 생각 하셔도 좋다. 함수이름이나 변수이름에 일정 규칙을 가지는 prefix 나 postfix 를 두어서 각 기능별 영역을 구분하는 방법도 나쁘진 않지만 필자는 구조체에 함수포인터와 자료구조를 몰아넣고 구조체 이름 자체를 통해서 접근하는 방식을 선호하는 편이다.

그럼 이제 init() 함수와 alloc() 함수를 구현 해 보자. 역시 새로운 파일 하나를 만들자 navilnux_memory.c 로 이름을 짓겠다.

#include <navilnux.h>
 
Navil_mem_mng memmng;
 
#define STARTUSRSTACKADDR   0xA0400000  // 4M
#define ENDUSRSTACKADDR     0xA4000000  // 64M
#define USRSTACKSIZE        0x00100000  // 1M
 
unsigned int mem_alloc(void)
{
    int i;
    for(i = 0 ; i < MAXMEMBLK ; i++){
        if(memmng.free_mem_pool[i].is_used == 0){
            memmng.free_mem_pool[i].is_used = 1;
            return memmng.free_mem_pool[i].block_end_addr;
        }
    }
    return 0;
}
 
void mem_init(void)
{
    unsigned int pt = STARTUSRSTACKADDR;
    int i;
 
    for(i = 0 ; i < MAXMEMBLK ; i++){
        memmng.free_mem_pool[i].block_start_addr = pt;
        memmng.free_mem_pool[i].block_end_addr = pt + USRSTACKSIZE -4;
        memmng.free_mem_pool[i].is_used = 0;
        pt += USRSTACKSIZE;
    }
 
    memmng.init = mem_init;
    memmng.alloc = mem_alloc;
}

정말 짧고 간결한 소스이다. 일단 앞서 작성한 navilnux_memory.h 를 include 하는 것은 navilnux.h 에 모두 몰아 넣는다. 그리고 Navil_mem_mng 형식의 memmng 구조체 변수를 전역으로 설정한다. memmng 는 앞으로 커널전역변수라는 거창한 이름으로 불리우게 된다.

사용자 스택 영역의 시작 주소, 사용자 스택영역의 끝 주소, 개별 사용자 테스크에게 할당하는 스택블럭의 크기를 define 해 놓았다. 왜 저런값이 define 되었는지는 이전강좌에서 설명한 메모리 맵을 참고하기 바란다. 면밀하게(?) 설계한 메모리 맵에서 설정한대로 값을 세팅 해 놓는다.

메모리의 스택영역을 하나씩 할당해 주는 mem_alloc() 함수의 내용이 나온다. 소스코드의 내용을 일일이 설명하기가 민망할 정도로 간단하다. for 루프 돌면서 자유메모리블록리스트의 is_used 플래그를 검사하고 가장 먼저 나오는 is_used가 0인 블럭을 찾아, is_used를 1로 바꾸어서 사용중임으로 표시하고 block_end_addr 의 주소를 리턴한다. 왜 block_start_addr 이 아니라 block_end_addr 인가? 이전 강좌에서 설명 했듯이 arm-linux-gcc 는 스택을 메모리 주소가 감소하는 방향으로 사용한다. 그러므로 스택의 시작값은 가장 큰 메모리 번지 값으로 해야 한다.

다음엔 메모리관리자를 초기화 해주는 mem_init() 함수가 나온다. 역시나 마찬가지로 지극히 간단하고 이해 하기 쉬운 소스이다. OS 는 이처럼 설계를 간단하게 하면 개별 코드는 전혀 어렵지 않다. 설계가 크고 복잡하고 만족해야 할 사항들이 많아 질 수록 어려워지고 복잡해 지는 것이다. 나빌눅스는 그런것 신경쓰지 않는다. 우선 돌아가고, 만들면서 공부하는데 가장 큰 목적을 두고 있다. 사용자 스택의 시작 주소 부터 개별 블록 사이즈 만큼 자유메모리블록리스트를 돌면서 시작값과 끝값을 설정한다. 그리고 두개의 함수 포인터를 세팅해 준다.

새로운 파일이 추가 되었으니 Makefile 이 바뀌어야 한다.

CC = arm-linux-gcc
LD = arm-linux-ld
OC = arm-linux-objcopy
 
CFLAGS    = -nostdinc -I. -I./include 
CFLAGS   += -Wall -Wstrict-prototypes -Wno-trigraphs -O0
CFLAGS   += -fno-strict-aliasing -fno-common -pipe -mapcs-32 
CFLAGS   += -mcpu=xscale -mshort-load-bytes -msoft-float -fno-builtin
 
LDFLAGS   = -static -nostdlib -nostartfiles -nodefaultlibs -p -X -T ./main-ld-script
 
OCFLAGS = -O binary -R .note -R .comment -S
 
CFILES = navilnux.c navilnux_memory.c
HFILES = include/navilnux_memory.h
 
all: $(CFILES) $(HFILES)
	$(CC) -c $(CFLAGS) -o entry.o entry.S
	$(CC) -c $(CFLAGS) -o gpio.o gpio.c
	$(CC) -c $(CFLAGS) -o time.o time.c
	$(CC) -c $(CFLAGS) -o vsprintf.o vsprintf.c
	$(CC) -c $(CFLAGS) -o printf.o printf.c
	$(CC) -c $(CFLAGS) -o string.o string.c
	$(CC) -c $(CFLAGS) -o serial.o serial.c
	$(CC) -c $(CFLAGS) -o lib1funcs.o lib1funcs.S
	$(CC) -c $(CFLAGS) -o navilnux.o navilnux.c
	$(CC) -c $(CFLAGS) -o navilnux_memory.o navilnux_memory.c
	$(LD) $(LDFLAGS) -o navilnux_elf entry.o gpio.o time.o vsprintf.o printf.o string.o serial.o lib1funcs.o navilnux.o navilnux_memory.o
	$(OC) $(OCFLAGS) navilnux_elf navilnux_img
 
clean:
	rm *.o
	rm navilnux_elf
	rm navilnux_img

몇줄 바뀌지 않았다. Makefile 의 변경내용에 대해서는 설명하지 않겠다. 그냥 보면 안다. Makefile 까지 바꾸고 과감하게 make 를 쳐 보자. 빌드가 잘 된다. 하지만 아직 동작하는지는 모른다. 당연하다. 코딩만 해놓고 함수를 호출해서 사용하질 않으니깐. 아직 때가 오지 않았다. 때가 오면 다 사용하게 된다. 조금만 참고 기다려라.

어쩌다 보니 이렇게 테스크에 스택을 할당하기 위한 준비가 다 끝났다. 그럼 다음엔 무엇해야 하는가. 준비가 끝났으니 테스크를 만들어야 하지 않는가. 테스크를 만드는건 역시나 크게 두개의 단계로 나눌 수 있다. 테스크관련 자료구조, 특히 테스크컨트롤블록(TCB)를 설계하는 작업과 테스크 생성자, 소멸자 등을 작성하는 단계이다. 메모리에도 메모리 해제하는 녀석이 없듯이 나빌눅스의 현재 설계에서는 테스크소멸자도 크게 의미가 없다. 그러면 달랑하나 테스크 생성자 하나만 필요 할것 같다. 물론 테스크관리자 초기화 하는 녀석은 당연히 필요하다.

다음강좌에서는 테스크를 만들어 보자. 강좌 초창기에 약간 아키텍쳐 적인 설명이 어느정도 끝나고 나니깐 강좌가 점점 쉬워지고 있다. 더불어 필자의 부족한 실력이 조금씩 드러나고 있다. 그래도 양해 하고 읽어주기 바란다. 다시 한번 말하지만 OS 만들기는 쉽다. 쉬운만큼 그다지 엄청난 실력을 요구 하지 않는다. 인내심을 가지고 강좌를 따라 오기만 하면 된다. 필자 같은 사람도 만들었지 않는가!

이번회에도 소스코드를 수정했다. 수정된 코드를 첨부 하오니 모쪼록 놀이에 도움이 되시기 바란다.

이 글은 http://raonlife.com/navilera/blog/view/82/ 에 동시 연재 됩니다.

File attachments: 
첨부파일 크기
파일 navilnux_chap8.tgz60.33 KB

댓글

병맛의 이미지

님 좀 짱인듯

gunman의 이미지

오래전 Study 용으로 제작했던 커널입니다.
좀 더 많은 구현은 뒤로 하고, 바로 PC용으로 포팅을 하는 중입니다.
http://gunman.tistory.com/119
같은 관심사를 가지는 분들이 많아 지는 것 같네요.^^

울트라 파이팅 건맨!

나빌레라의 이미지

멋진데요..^^;

좀더 문서화 하셔서 OS를 공부하는 많은 사람들에게

도움이 되었으면 좋겠네요.

---
얇은 사 하이얀 고깔은 고이 접어서 나빌레라

----------------------
얇은 사 하이얀 고깔은 고이 접어서 나빌레라

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.