OS를 만듭시다. 어때요~ 참 쉽죠? (9)
OS. 영어로 풀어서 Operating System. 한자어로 번역해서 운영체제. 이것을 만든다고 하면 사람들은 굉장히 어렵게 생각한다. 리눅스나 윈도우같은 OS를 비교대상으로 본다면 OS를 만드는것은 정말 어렵고 힘들고, 개인이 만들기엔 어쩌면 불가능에 가까운 도전일지도 모른다. 하지만 OS의 기본 개념들은 대학교 학부과정에서 가르칠 정도로 이미 보편화 되어 있고 그 개념 자체들은 그다지 어렵지 않다. 개념 구현을 중심으로 동작하는 것 자체에 의미를 둔 OS를 만드는 것은 어쩌면 도전 해 볼만 한 가치가 있는 시도가 아닐까.
목차
1회
2회
3회
4회
5회
6회
7회
8회
9. 테스크 컨트롤 블록
지난 강좌의 서두에서 OS에서 사용자에게 구체적인 서비스를 제공하는 주체는 메모리로 로딩되어서 동작하는 프로그램(프로세스, 테스크, 쓰레드)이다. 임베디드OS에서는 테스크이다. 임베디드OS에서 테스크는 리눅스나 윈도우에서 처럼 실행파일을 실행시키는 것이 아니라 마치 함수처럼 사용자가 작성하여 커널 소스에 포함시켜 빌드 한다음, 커널쓰레드처럼 동작한다. 다만 커널 쓰레드는 SVC모드에서 동작하고 메모리접근에도 제한이 없는 특권모드로 동작하지만, 임베디드OS의 테스크는 USER모드에서 MMU를 사용할 경우에는 메모리 접근에 제한을 받게 된다. 다만 나빌눅스는 MMU를 사용하지 않기 때문에 USER모드라 하여도 메모리 접근에 제한이 없다. 따라서 사용자 정의 테스크를 작성할때 포인터를 함부로 사용하지 않아야 하는 제약이 있다.
다시 말하지만 서비스를 제공하는 주체는 임베디드OS 에서 테스크이다. 그리고 나빌눅스는 무려 멀티테스킹 임베디드OS를 표방한다. 그러므로 각각의 테스크는 멀티테스킹의 대상이 되어야 하므로 관리되어야 한다. 프로그래밍에서 관리된다는 것은 곧 추상화되어 자료구조로 실체화되고 객체의 인스턴스로 메모리에 로딩된다. 필자가 아는 한 존재하는 모든 OS는 어떠한 형태로든 테스크를 관리한다. PCB(Process Control Block) 혹은 TCB(Task Control Block)이란 이름으로 불리운다.
이번편에서는 나빌눅스의 TCB를 설계하고 작성할 것이다. 하지만 대단한것은 아니다 나빌눅스는 어디까지나 최소화되고 이해하기 쉬우며 작성하기 쉬운 작고 간결한 OS를 표방하므로 TCB 자체의 내용도 별것 없다.
아무리 간단간결간편한 TCB 라도 반드시 들어가야 할 내용이 있다. 바로 CPU 레지스터를 백업해야 할 공간이다. 다른 말로 테스크 컨텍스트를 가지고 있는 영역이다. 멀티테스킹 OS 에서 반드시 필요하고 또 하일라이트라고 할 수 있는 것이 테스크간의 문맥전환. 즉 컨텍스트 스위칭이다. 컨텍스트 스위칭이란 것이 바로 TCB가 가지고 있는 해당 테스크의 컨텍스트 내용을 CPU레지스터에 옮겨 넣어 테스크가 컨텍스트 스위칭 당하기 바로 직전 상태에서 다시 수행될 수 있도록 해주는 작업이다. 컨텍스트 스위칭에 대한 내용은 다음편에서 다를 예정이다.
컨텍스트란 그럼 무엇일까. 오래전 ARM의 레지스터에 대한 내용을 설명했을 때를 기억해 보자.
http://www.segger.com/gif/jlink/arm_registers.gif
직접 그림을 그려서 올리고 싶지만, 본 강좌의 주요 컨셉중 하나가 최대한 그림없는 강좌이므로 (절대 귀찮아서 그러는 것이 아니다.) 본 링크를 클릭해 보기 바란다. 좀더 자세한 그림을 원한다면 ARM System Developer's Guide 란 책의 Figure 2.4를 보기 바란다. 참고로 번역본의 경우 25페이지에 있다.
ARM 은 r0부터 r15까지의 범용레지스터와 cpsr 이라는 상태레지스터를 가지고 있다. 이중 r13, r14, r15는 각각 sp, lr, pc 의 용도로 사용되고 실제로는 r0부터 r12까지 13개의 레지스터를 데이터 레지스터로 사용 할 수 있다. 그리고 FIQ를 제외한 다른 시스템 동작 모드는 각각 독립적인 sp, lr 을 가지고 cpsr을 저장하기 위한 spsr을 가진다. FIQ는 빠른 동작을 위해 r8~r12까지의 데이터 레지스터를 따로 가진다. 나빌눅스에서는 FIQ를 사용하지 않으므로 설명하지 않겠다.
여러번 설명했지만 cpu 는 코어안에 있는 레지스터에 들어가 있는 값 외에는 처리 하지 못한다. 메모리에 있는 두 변수의 값을 더해서 다시 메모리에 있는 변수에 저장한다고 하였을 때, cpu는 메모리에 존재하는 값을 읽어와서 레지스터에 넣고, 이들을 더해서 값을 다시 레지스터에 넣은 다음 레지스터에 있는 결과 값을 메모리에 복사 한다. 즉, 레지스터와 메모리간의 데이터 이동과 cpu에 있는 ALU(Arithmetic Logic Unit)와 레지스터간의 데이터 이동으로 그 흐름은 구분되는 것이다. 다시 말하면 cpu 의 핵심이라고 볼 수 있는 ALU는 사실상 메모리에 뭐가 있는지에 대해서는 관심이 없다. 단지, 레지스터에 무엇이 있는지에만 관심이 있을 뿐이다.
cpu는 현재 실행중인 명령이 커널에 존재하는 명령인지 사용자 테스크에 존재하는 명령인지 알 수 있는 방법이 없다. 단지 인위적으로 cpu 레지스터에 로딩되는 값들을 조절해 줌으로써 테스크간의 전환을 꾀하는 것일 뿐이다. 사실 이 내용은 컨텍스트 스위칭 부분에서 좀더 자세하게 다뤄야 겠지만 TCB에서의 컨텍스트 영역은 컨텍스트 스위칭과 뗄 수 없는 내용이기 때문에 계속해서 반복되는 설명이 나올 수 밖에 없다.
cpu 정확히 ALU의 입장에서 현재 프로세서의 상태는 곧, ALU와 연결되어 있는 레지스터의 값들이라는 것이다. 그러므로 레지스터의 값이 한번에 싹 바뀌면 동작중인 프로그램이 바뀌는 것이다. 동작중인 프로그램은 곧 테스크이고 그래서 테스크는 자기 자신이 동작중이던 레지스터들을 저장하고 있을 공간이 필요하다. 그것이 TCB에 있는 컨텍스트 영역이다.
그럼 컨텍스트 영역을 설계하고 코딩해 보자. 별거 없다. 위에서 계속해서 설명했던 범용레지스터 r0 에서 r15까지와 cpsr 이 복사되어 저장되는 spsr의 저장공간을 담을 수 있으면 된다. 이번에도 파일을 하나 만들자 include/navilnux_task.h 이다.
#ifndef _NAVIL_TASK #define _NAVIL_TASK typedef struct _navil_free_task { unsigned int context_spsr; unsigned int context[13]; unsigned int context_sp; unsigned int context_lr; unsigned int context_pc; } Navil_free_task; #endif
gcc 는 구조체를 컴파일할때 구조체의 멤버변수 선언순서 그대로 메모리주소를 할당한다. 그러므로 위 Navil_free_task 구조체의 멤버 변수들은 순서대로 context_spsr 변수가 선언된 주소에서 4byte를 더하면 context 배열의 첫번째 위치가 된다. 그리고 거기서 (13 * 4) byte 만큼 주소를 증가시키면 context_sp 의 위치가 된다. 마찬가지로 4 byte 씩 더하면 context_lr, context_pc 가 된다. 점점 강좌 순서가 다가오는 임베디드 OS 만들기의 하일라이트라고 할 수 있는 컨텍스트 스위칭을 구현하는 부분에서 다시 자세하게 다루게 될 것이다. 일단 지금은 이렇게만 알아두자.
그 외에도 테스크에 할당되는 테스크ID, TCB들이 링크드리스트로 연결될때의 다음 노드를 가르키는 포인터변수등이 포함될 수도 있지만, 본강좌에서 만드는 나빌눅스는 완벽하고 안정적인 OS가 아니라 일단 만들어보면서 쉽게 이해 할 수 있는 OS를 만드는 것이기 때문에 프로그래밍적 기교나 멋진 코드보다는 가장 기본적인 문법과 테크닉을 이용한 코드를 작성하도록 하겠다.
위에 설명 했듯이 gcc 는 구조체를 멤버변수 선언 순서대로 메모리할당을 해 주므로 저렇게 구분하지 않고, context[17] 이렇게 선언해도 사실상의 차이는 없다. 다만 코드에서 의미상의 구분을 하기 위해서 변수를 따로 선언했다.
달랑 ARM프로세서의 레지스터만 저장할 공간을 설정했을 뿐이지만, 나빌눅스에서의 TCB는 저것으로 충분하다. 라운드로빈 스케줄링을 사용할것이기 때문에 테스크의 우선순위나 테스크의 실행 상태등을 관리할 필요가 없다. 물론 이후 OS를 계속 만들어 가면서 테스크가 개별적으로 관리해야 할 어떤 정보가 있다면 부담없이 Navil_free_task 구조체를 수정 하면 된다.
TCB의 설계가 끝났다. 정말이다.
이제 필요한것은 무엇인가. 스택할당자를 만들때 메모리컨트롤블록을 초기화 하는 작업을 했듯이, 테스크컨트롤블록을 만들었으니 당연히 테스크컨트롤블록을 초기화 하는 코드를 작성해야 한다. 몰론 메모리컨틀롤블록초기화함수가 별 내용 없었듯이, 테스크컨트롤블록초기화함수역시 별 내용 없을 것으로 기대 된다. 기대 해도 좋다. 그리고 실제 테스크함수를 만들고 나면 그 함수를 테스크컨트롤블록에 할당 해 주는 함수를 만들어야 한다. 우선 헤더파일을 수정하자.
#ifndef _NAVIL_TASK #define _NAVIL_TASK #define MAXTASKNUM 40 #define CONTEXTNUM 13 typedef struct _navil_free_task { unsigned int context_spsr; unsigned int context[CONTEXTNUM]; unsigned int context_sp; unsigned int context_lr; unsigned int context_pc; } Navil_free_task; typedef struct _navil_task_mng { Navil_free_task free_task_pool[MAXTASKNUM]; int max_task_id; void (*init)(void); int (*create)(void(*startFunc)(void)); } Navil_task_mng; void task_init(void); int task_create(void(*startFunc)(void)); #endif
큰 골격은 메모리관리자의 그것과 동일하다. 본편에서는 초기 설계일 뿐이고 이후 스케줄링과 컨텍스트 스위칭을 위한 프로토 타입선언등이 추가 될 것이다. 테스크컨트롤블록을 만들고 초기화및 테스크생성 함수를 만드는 본 시점에서는 아직 위의 코드 정도의 프로토타입만으로도 목적을 달성 할 수 있다.
메모리관리자에서 자유메모리블록은 60개를 할당했는데, 위 코드에서 자유테스크블록은 40개를 할당했다. 별다른 이유는 없다. 그냥 여유를 뒀을 뿐이다. 좀더 이유를 찾아 보자면 향후 동적메모리 할당 기능을 추가할때, SDRAM의 상위 영역을 사용할 것이다. 그때에 할당되었으나 사용하지 않는 상위 20개 블럭(20Mbyte)를 동적메모리 영역에 사용할 것이다.
나빌눅스는 설계를 최대한 단순화 하기 위해 테스크ID를 따로 두지 않고, free_task_pool 배열의 인덱스를 테스크ID로 사용한다. 테스크 생성을 하나 요청할때 마다 free_task_pool 배열의 인덱스를 하나씩 증가하며 자유테스크블록을 일종의 스택처럼 사용하고 현재까지 할당된 테스크ID, 즉 스택TOP을 유지하는 변수를 하나 둔다.
그럼 초기화 함수와 테스크생성함수의 본체를 작성해 보자, 역시나 새로운 소스파일을 만든다. 파일명은 navilnux_task.c 로 한다.
#include <navilnux.h> extern Navil_mem_mng memmng; Navil_task_mng taskmng; #define STARTUSRCPSR 0x68000050 int task_create(void(*startFunc)(void)) { int task_idx = 0; unsigned int stack_top = 0; taskmng.max_task_id++; task_idx = taskmng.max_task_id; if(task_idx > MAXTASKNUM){ return -1; } stack_top = memmng.alloc(); if(stack_top == 0){ return -2; } taskmng.free_task_pool[task_idx].context_spsr = STARTUSRCPSR; taskmng.free_task_pool[task_idx].context_sp = stack_top; taskmng.free_task_pool[task_idx].context_pc = (unsigned int)startFunc; return task_idx; } void task_init(void) { int i; for(i = 0 ; i < MAXTASKNUM ; i++){ taskmng.free_task_pool[i].context_spsr = 0x00; memset(taskmng.free_task_pool[i].context, 0, sizeof(unsigned int) * CONTEXTNUM); taskmng.free_task_pool[i].context_sp = 0x00; taskmng.free_task_pool[i].context_lr = 0x00; taskmng.free_task_pool[i].context_pc = 0x00; } taskmng.max_task_id = -1; taskmng.init = task_init; taskmng.create = task_create; }
마찬가지로 모든 헤더파일은 navilnux.h 에 몰아 넣었다. 그리고 navilnux_memory.c 에 선언되어 있는 memmng 커널 전역변수를 extern으로 불러 왔다. 테스크를 생성할때 스택 주소를 할당받기 위해서 메모리관리자의 alloc() 함수를 이용해야 하기 때문이다. 다음에 새로운 커널 전역변수인 taskmng를 선언했다. 앞으로 테스크관련 자료구조나 함수들은 모두 taskmng 커널전역변수를 통해서 접근하게 할 생각이다.
그 아래에 STARTUSRCPSR 라는 레이블을 define 했다. 이것은 사용자테스크에 할당될 spsr의 초기값이다. cpsr의 구조를 다시 살펴 보면 32bit의 크기에 상위 4개 비트(28~31번째 비트)에 NZCV의 오버플로우, 제로비트등을 세팅하는 상태 플래그가 있고 하위 8개비트(0~8번째 비트)에 프로세서모드와 IFT의 IRQ와 FIQ, Thumb모드를 활성/비활성화 하는 제어 비트들이 있다. 값이 0x68000050 인 이유는 하위 비트는 0x50으로 이진수로 01010000 이다. 하위 5개 비트는 프로세서모드로 이진수 10000 은 USER모드이다. 사용자테스크이므로 당연히 프로세서모드는 USER모드여야 한다. 그리고 FIQ와 Thumb모드는 사용하지 않고, IRQ만 사용할것이므로 iFt (대문자로 표시하면 비트가 set(=1) 되었다는 의미이다.) 로 세팅한다. 상위비트가 0x68 인것은, ARM프로세서에서 cpsr의 상위비트 default 값이 무엇인지 찾아보았지만 찾아 볼 수가 없어서 아무것도 안하고 프로세서가 동작되자마자 cpsr의 값을 찍어보는 펌웨어를 올려서 테스트 해 본 결과 cpsr의 상태플래그가 0x68로 찍혔다. 실험적 결과이기 때문에 정확히 저 값을 써야 하는지에 대해서는 확신이 없으나, 저 값을 그대로 써도 별 문제는 없는 것으로 보인다.
다음으로 테스크를 생성하는 task_create() 함수이다. OS에서 테스크 관리를 어떻게 하느냐에 따라서 이 함수의 매개변수는 달라질 수 있다. 만약 사용자테스크함수가 매개변수를 받게 만들고 싶으면 task_create()함수에서 매개변수를 받아서 넘겨주어야 한다. 또는 사용자테스크함수에 우선순위를 적용하고 싶으면 역시 task_create()함수에 우선수위도 매개변수로 받아야 한다. 마찬가지로 스택영역등도 커스터마이징 가능하게 제어하고 싶으면 task_create() 함수에서 매개변수로 받아들이게 하면 된다.(uC/OS-II 가 그렇게 되어 있다.)
하지만 그렇게 하면 너무 복잡해 진다. 나빌눅스의 컨셉은 '단순함' 이다. 단순함을 잃으면 안된다. 단순해 질려면 어쩔 수 없이 사용자의 선택권을 제한 할 수 밖에 없다. 나중에 필요에 따라 수정을 하게 될 지라도 지금은 일단 가장 단순한 형태로 OS를 만들어 가자. 그럼 다른건 다 빼도 뺄 수 없는 한가지는 무엇일까. 바로 사용자테스크로 돌아가게 될 함수의 시작주소. 즉 사용자테스크함수의 함수포인터이다. 함수 선언문을 보면 알겠지만 사용자테스크함수의 형식은 void 함수이름(void) 로 고정된다.
나빌눅스에서 테스크ID는 free_task_pool 배열의 인덱스를 그대로 사용한다고 했다. 현재까지 사용한 free_task_pool 의 인덱스는 max_task_id 구조체 멤버변수가 가지고 있으니, 테스크를 새로 생성할때 이를 하나 증가 시켜 준다. 증가 한 값이 MAXTASKNUM 즉 설정해 놓은 최대 테스크갯수보다 커지면 오류이므로 음수(-1)를 반환한다. free_task_pool의 인덱스를 증가시키고 나면 다음으로 해당 테스크가 사용할 스택주소를 할당받는다. 여기서 드디어 저번 강좌에서 만든 정적메모리 할당함수를 호출한다. 0을 리턴할 경우 오류이므로 넘겨 받은 값이 0 일 경우에는 음수(-2)를 반환한다. 그리고 TCB의 context 영역에 초기값을 세팅 해 준다. spsr에 초기값을 넣고, sp 에는 방금 할당받은 스택영역의 최상위 주소를, 그리고 pc 에는 사용자테스크함수의 시작주소를 넣어준다. 테스크가 최초에 스케줄링을 받으면 테스크함수의 시작위치부터 수행을 해야 하므로 테스크가 생성되는 시점에서 pc는 테스크의 시작 주소여야 한다. 그리고 오류가 없다면 반환값은 방금 사용한 free_task_pool의 인덱스. 즉 테스크ID이다.
테스크관리자 초기화 함수 역시 매우 단순간결하다. 굳이 설명을 해야 할 필요가 있을까~ 싶을 정도다. free_task_pool 배열을 처음부터 끝까지 돌면서 멤버변수를 모두 0으로 초기화 한다. max_task_id를 -1로 초기화 한다. create 함수에서 시작할때 ++로 증가하므로 최초 0 부터 인덱싱 되기 위해서는 -1로 초기화 해야 한다. 그리고 init 와 create 함수의 포인터를 연결해 준다.
새로운 파일이 두개 생겼으므로 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 navilnux_task.c HFILES = include/navilnux.h include/navilnux_memory.h include/navilnux_task.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 $(CC) -c $(CFLAGS) -o navilnux_task.o navilnux_task.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 navilnux_task.o $(OC) $(OCFLAGS) navilnux_elf navilnux_img clean: rm *.o rm navilnux_elf rm navilnux_img
모두 수정을 완료 했으면, 가벼운 마음으로 make 를 쳐 보자. 빌드가 잘 된다. 하지만 아직도 이미지를 보드에 올려서 무언가를 확인 해 볼 수는 없다. 다음 강좌에서는 사용자테스크함수를 작성해 taskmng.create()함수에 넘겨 제대로 스택을 할당받는지 확인 해 보도록 하겠다.
이번 회에도 꽤나 많은(?) 코드 수정이 있었다. 이번 강좌 까지의 작업내용을 담은 소스 파일을 첨부 하오니 모쪼록 공부에 도움이 되시길 바란다.
이 글은 http://raonlife.com/navilera/blog/view/83/ 에 동시 연재 됩니다.
첨부 | 파일 크기 |
---|---|
navilnux_chap9.tgz | 43.29 KB |
댓글
잘보고 있습니다.
요즘 시리즈만 기다라고 있습니다.
그냥 눈으로만 보고 있는데 지금 하는 급한 프로젝트만 끝나면 직접 따라 해보려고 생각중입니다.
혹시 지금 연재하는 시리즈 구성과 언제까지 하실지 물어봐도 되겠습니까.??
읽어주셔서
읽어주셔서 감사합니다.
확실한건 아니지만(저도 뭐 정해놓고 쓰진 않아요..^^)
앞으로 사용자테스크관련 내용과, 컨텍스트 스위칭을 다루고..
이후로는 세마포어, 뮤택스, 인터럽트핸들링
디바이스드라이버, 메모리동적할당, 시스템콜구성
정도를 더 다룰 예정입니다. 대략 20회차쯤에서 끝날것 같군요.
-----
얇은 사 하이얀 고깔은 고이 접어서 나빌레라
----------------------
얇은 사 하이얀 고깔은 고이 접어서 나빌레라
아 그렇군요...
이제 반정도 연재 하신거넹....^^
오늘도 역시
잘 봤어요~:D
---------------------------------
Life is not fair, Get used to it.
---------------------------------
Life is not fair, Get used to it.
댓글 달기