OS를 만듭시다. 어때요~ 참 쉽죠? (16)
OS. 영어로 풀어서 Operating System. 한자어로 번역해서 운영체제. 이것을 만든다고 하면 사람들은 굉장히 어렵게 생각한다. 리눅스나 윈도우같은 OS를 비교대상으로 본다면 OS를 만드는것은 정말 어렵고 힘들고, 개인이 만들기엔 어쩌면 불가능에 가까운 도전일지도 모른다. 하지만 OS의 기본 개념들은 대학교 학부과정에서 가르칠 정도로 이미 보편화 되어 있고 그 개념 자체들은 그다지 어렵지 않다. 개념 구현을 중심으로 동작하는 것 자체에 의미를 둔 OS를 만드는 것은 어쩌면 도전 해 볼만 한 가치가 있는 시도가 아닐까.
목차
1회
2회
3회
4회
5회
6회
7회
8회
9회
10회
11회
12회
13회
14회
15회
16. 동기화 - 뮤택스와 세마포어 그리고 커널카운터
리눅스나 유닉스, 윈도우에서 쓰레드 프로그래밍을 해본 분이라면 쓰레드 동기화 라는 것을 들어 봤을 것이다. 아니면 프로세스 동기화라는 말이라도 들어 봤을 것이다. 다 제쳐 놓고 동기화라는 말은 한번 정도는 들어 봤을 것이다. 동기화라는 용어 자체에 대해서 만으로도 한 회 분량의 강좌를 쓰고 남을 만큼 많아서, 자세히는 설명하지 않고 필요한 것만 설명하겠다. 본강좌의 컨셉이 있지 않은가. 강좌에 필요치 않은 내용은 검색엔진에 맡긴다. 좋아하는 검색엔진에 '동기화' 라고 써 보아라 아주 많이 나온다.
시스템의 자원은 한정되어 있다. 한정된 자원을 여러개의 쓰레드가 동시에 접근하려 하면 어떤 경우에는 심각한 문제가 발생 할 수도 있다. 예를 들어 자원.dat 라는 파일이 있다고 하자. 그리고 쓰레드A 와 쓰레드B 가 동시에 자원.dat 에 쓰기 작업을 하려 한다. 쓰레드A 는 "동해물과" 를 쓰려 하고, 쓰레드B 는 "백두산이" 를 쓰려고 한다. 이 두 접근이 순서대로 잘 되면 좋겠지만 그렇게 되지 않고 정말 동시에 이루워진다면 자원.dat의 내용은 "동해물과 백두산이" 가 아니라, "동해백두물산 이과" 이런식으로 들어 갈 수도 있다. 그래서 이럴때는 자원.dat 에 쓰기를 하는 부분을 크리티컬섹션으로 묶은 다음, 크리티컬섹션의 시작부분에 동기화 진입함수, 끝나는 부분에 동기화 마무리 함수를 써 줘서 쓰레드A 가 자원.dat에 대한 처리를 다 끝낼 때 까지 쓰레드B를 대기 시키고, 쓰레드A 가 자원.dat에 대한 작업을 다 하고 동기화 마무리 함수를 거치고 나면 그제서야 쓰레드B 가 자원.dat 에 접근하게끔 순서를 조정 해 주는 작업이 필요하다. 이와 같은 작업을 동기화 라고 한다.
동기화의 구현에는 많은 종류가 있다. 리눅스 커널이 지원하는 것만 해도 뮤택스, 세마포어, 원자적연산 등등 여러가지가 있다. 나빌눅스에서는 이중 가장 대표적이면서 가장많이 사용하고 이해하기 쉬운 뮤택스와 세마포어 두가지를 구현 하도록 하겠다.
뮤택스와 세마포어의 구현은 전강좌에서 다루었던 ITC의 구현과 매우 유사하다. 아니 거의 같다. 거의 별 다른 차이가 없을 것이다. 서로간의 다른 개념으로 인해서 blocking 의 활성/비활성화 매커니즘이 살짝 다를 뿐이다.
그럼 먼저 뮤택스를 구현 하도록 하겠다. 뮤택스는 어떻게 구현 해야 할까. 간단하게 커널전역변수를 두고, 커널전역변수에 플래그를 셋 한다음 크리티컬섹션에 진입하고 크리티컬 섹션의 작업이 끝난 다음에는 해당 커널전역변수의 플래그를 언셋한다. 플래그를 셋 하는 함수는 wait() 이라고 하고 플래그를 언셋하는 함수는 release() 라고 하자. wait() 함수는 한가지 할일이 더 있다. 셋 할려고 하는 커널전역변수가 이미 다른 사용자테스크에 의해서 셋 되어 있는 상태라면 플래그가 언셋될 때 까지 blocking 걸려있어야 한다.
무언가 동작이 ITC랑 매우 흡사 하다. 커널전역변수에 메시지를 주고, 메시지를 받는 쪽에서는 메시지가 올때 까지 blocking 되는 ITC 와, 커널전역변수에 플래그를 세팅하고, 세팅한놈이 플래그를 풀때 까지 blocking 되는 뮤택스 뭔가 닮았다. 다른 레이어를 추가 할 필요 없이 메시지관리자를 확장 하면 될것 같다. 그러므로 별도의 파일을 만들 필요 없이 navilnux_msg.h 에 그대로 추가 하도록 하자.
#ifndef _NAVIL_MSG #define _NAVIL_MSG #define MAXMSG 255 #define ITCSTART 0 #define ITCEND 99 #define MUTEXSTART 100 #define MUTEXEND 199 typedef struct _navil_free_msg { int data; int flag; } Navil_free_msg; typedef struct _navil_msg_mng { Navil_free_msg free_msg_pool[MAXMSG]; void (*init)(void); int (*itc_send)(int, int); int (*itc_get)(int, int*); int (*mutex_wait)(int); int (*mutex_release)(int); } Navil_msg_mng; void msg_init(void); int msg_itc_send(int, int); int msg_itc_get(int, int*); int msg_mutex_wait(int); int msg_mutex_release(int); #endif
무엇이 바뀌었을까. 우선 255개의 자유메시지 블럭을 ITC 가 100개, 뮤택스가 100개 사용하게끔 나누었다. 각각 ITCSTART, ITCEND 와 MUTEXSTART, MUTEXTEND 로 경계값을 define 해 놓아서 나중에 에러 체크할때 사용 하게 하였다. 그리고 추가된것은 mutext_wait 함수 포인터와 이 함수포인터의 본체가 될 msg_mutex_wait() 함수의 프로토타입. 마찬가지로 mutext_release 함수포인터와 이 함수의 본체가 될 msg_mutex_release() 함수의 프로토타입이 추가 되었다. 이 외에는 바뀐것이 없다. 실질적으로 저 두 함수를 추가 하는 것이 뮤택스 추가작업의 전부 이다.
그러면 함수를 추가 해 보자 navilnux_msg.c 이다.
#include <navilnux.h> Navil_msg_mng msgmng; int msg_itc_send(int itcnum, int data) { if(itcnum > ITCEND || itcnum < ITCSTART){ return -1; } msgmng.free_msg_pool[itcnum].data = data; msgmng.free_msg_pool[itcnum].flag = 1; return itcnum; } int msg_itc_get(int itcnum, int *data) { if(itcnum > ITCEND || itcnum < ITCSTART){ return -1; } if(msgmng.free_msg_pool[itcnum].flag == 0){ return -2; } *data = msgmng.free_msg_pool[itcnum].data; msgmng.free_msg_pool[itcnum].flag = 0; msgmng.free_msg_pool[itcnum].data = 0; return 0; } int msg_mutex_wait(int mutexnum) { mutexnum += MUTEXSTART; if(mutexnum > MUTEXEND || mutexnum < MUTEXSTART){ return -1; } if(msgmng.free_msg_pool[mutexnum].flag == 0){ msgmng.free_msg_pool[mutexnum].flag = 1; }else{ return -2; } return 0; } int msg_mutex_release(int mutexnum) { mutexnum += MUTEXSTART; if(mutexnum > MUTEXEND || mutexnum < MUTEXSTART){ return -1; } msgmng.free_msg_pool[mutexnum].flag = 0; return 0; } void msg_init(void) { int i; for (i = 0 ; i < MAXMSG ; i++){ msgmng.free_msg_pool[i].data = 0; msgmng.free_msg_pool[i].flag = 0; } msgmng.init = msg_init; msgmng.itc_send = msg_itc_send; msgmng.itc_get = msg_itc_get; msgmng.mutex_wait = msg_mutex_wait; msgmng.mutex_release = msg_mutex_release; }
ITC 관련 함수도 코드가 조금 바뀌었다. 바뀐부분은 그전에 0 과 MAXMSG 사이에서 경계값 체크를 하던것을 ITCSTART 와 ITCEND 사이에서 경계값을 체크하도록 코드를 수정한것이 전부이다. 그리고 msg_mutex_wait() 함수와 msg_mutex_release() 함수가 추가 되었다.
msg_mutex_wait() 함수는 msg_itc_get() 함수와 흡사하다. 함수내용은 역시나 지극히 단순하고 이해 하기 쉽다. 뮤택스 별거 아니다. 그냥 저렇게 만들어진다. 자유메모리블럭의 flag 가 0 이면 이것을 1 로 세팅하고 함수가 끝난다. 만약 자유메모리블럭의 flag 가 누군가에 의해 미리 1 로 세팅되어져 있다면 아무것도 안하고 -2를 리턴한다. 이게 다 이다. 끝이다. 진짜다.
msg_mutex_release() 함수는 더 단순하다. 그냥 마냥 아무 대책없이 자유메모리 블럭의 flag 를 0 으로 언셋 해 줄 뿐이다.
위 두함수에서 유념해 둘것은 함수의 맨 첫줄에서 mutexnum += MUTEXSTART; 로 하여 뮤택스번호를 실제로 사용하는 자유메모리블럭의 인덱스와 맞춰 준다는 것이다.
이제부터는 전강좌에서 몇회에 걸쳐 수행했던 시스템콜 추가 절차가 남아 있다. 뮤택스 역시 커널의 자원(자유메시지블럭)을 사용자 테스크가 사용하는 것이므로 시스템콜로 구현 되어야 한다. syscalltbl.h 와 navilnux_sys.h, navilnux_sys.c, navilnux_lib.h, navilnux_lib.S 의 내용은 별도의 설명 없이 그대로 소스코드만 소개 하겠다. 사실 설명 할것도 이젠 없다.
syscalltbl.h
#ifndef _NAVIL_SYS_TBL #define _NAVIL_SYS_TBL #define SYS_MYSYSCALL 0 #define SYS_MYSYSCALL4 1 #define SYS_ITCSEND 2 #define SYS_ITCGET 3 #define SYS_MUTEXTWAIT 4 #define SYS_MUTEXREL 5 #define SYS_CALLSCHED 255 #endif
navilnux_sys.h
#ifndef _NAVIL_SYS #define _NAVIL_SYS #include <syscalltbl.h> #define SYSCALLNUM 255 void syscall_init(void); int sys_mysyscall(int, int, int); int sys_mysyscall4(int, int, int, int); int sys_itcsend(int, int); int sys_itcget(int, int*); int sys_mutexwait(int); int sys_mutexrelease(int); extern void sys_scheduler(void); #endif
navilnux_sys.c
#include <navilnux.h> extern Navil_msg_mng msgmng; unsigned int navilnux_syscallvec[SYSCALLNUM]; int sys_mysyscall(int a, int b, int c) { printf("My Systemcall - %d , %d , %d\n", a, b, c); return 333; } int sys_mysyscall4(int a, int b, int c, int d) { printf("My Systemcall4 - %d , %d , %d , %d\n", a, b, c, d); return 3413; } int sys_itcsend(int itcnum, int data) { return msgmng.itc_send(itcnum, data); } int sys_itcget(int itcnum, int *data) { return msgmng.itc_get(itcnum, data); } int sys_mutexwait(int mutexnum) { return msgmng.mutex_wait(mutexnum); } int sys_mutexrelease(int mutexnum) { return msgmng.mutex_release(mutexnum); } void syscall_init(void) { navilnux_syscallvec[SYS_MYSYSCALL] = (unsigned int)sys_mysyscall; navilnux_syscallvec[SYS_MYSYSCALL4] = (unsigned int)sys_mysyscall4; navilnux_syscallvec[SYS_ITCSEND] = (unsigned int)sys_itcsend; navilnux_syscallvec[SYS_ITCGET] = (unsigned int)sys_itcget; navilnux_syscallvec[SYS_MUTEXTWAIT] = (unsigned int)sys_mutexwait; navilnux_syscallvec[SYS_MUTEXREL] = (unsigned int)sys_mutexrelease; }
navilnux_lib.h
#ifndef _NAVIL_LIB #define _NAVIL_LIB extern int mysyscall(int, int, int); extern int mysyscall4(int, int, int, int); extern int itc_send(int, int); extern int itc_get(int, int*); extern int mutex_wait(int); extern int mutex_release(int); extern void call_scheduler(void); int navilnux_itc_send(int, int); int navilnux_itc_get(int); int navilnux_mutex_wait(int); int navilnux_mutex_release(int); #endif
navilnux_lib.S
#include <syscalltbl.h> .global mysyscall mysyscall: swi SYS_MYSYSCALL mov pc, lr .global mysyscall4 mysyscall4: swi SYS_MYSYSCALL4 mov pc, lr .global itc_send itc_send: swi SYS_ITCSEND mov pc, lr .global itc_get itc_get: swi SYS_ITCGET mov pc, lr .global mutex_wait mutex_wait: swi SYS_MUTEXTWAIT mov pc, lr .global mutex_release mutex_release: swi SYS_MUTEXREL mov pc, lr .global call_scheduler call_scheduler: swi SYS_CALLSCHED mov pc, lr
역시나 ITC 랑 별다를것 없긴 하지만 그래도 왜 그렇게 구현 했는지에 대한 설명이 필요하기에 navilnux_clib.c 의 수정 내용은 약간의 설명을 덧 붙이도록 하겠다. 일단 소스파일의 내용을 보자
#include <navilnux.h> int navilnux_itc_send(int itcnum, int data) { return itc_send(itcnum, data); } int navilnux_itc_get(int itcnum) { int ret_value = 0; int data = 0; while(1){ ret_value = itc_get(itcnum, &data); if(ret_value == 0){ return data; }else if(ret_value == -1){ return ret_value; }else{ call_scheduler(); } } } int navilnux_mutex_wait(int mutexnum) { int ret_value = 0; while(1){ ret_value = mutex_wait(mutexnum); if(ret_value == 0){ return 0; }else if(ret_value == -1){ return -1; }else{ call_scheduler(); } } } int navilnux_mutex_release(int mutexnum) { return mutex_release(mutexnum); }
navilnux_mutex_wait() 과 navilux_mutex_release() 함수가 새로 추가 되었다. navilnux_mutex_release() 함수는 설명할 내용이 없다. 아무것도 하지 않고 그냥 그대로 시스템콜을 호출 하기 때문이다. navilnux_wait() 는 역시 navilnux_itc_get() 과 그 구현이 흡사하다. 값을 리턴하지 않을 뿐, 정상종료 한다면 함수를 끝내고, 에러가 발생하면 에러코드를 리턴하고, blocking 상태에 들어가야 한다면 call_scheduler() 를 호출하여 강제로 컨택스트 스위칭을 발생시켜 다음 테스크로 실행을 넘겨 버린다.
함수를 만들었으니 이제 사용해 주어야 한다. 이전까지 3개의 사용자 테스크로 어떻게 계속 예제를 돌리면서 잘 버티었는데 이제는 조금 부족한 느낌이 든다. 뭐 별로 걱정할 것 없다. 나빌눅스는 넉넉한 이지보드의 하드웨어 지원 덕분에 사용자 테스크를 40개 까지 만들 수 있다. 이제 겨우 3개 만들었다. 37개 더 만들어 써도 된다. :) 하지만 욕심 부리지 않고 2개만 더 추가 하도록 하겠다. navilnux_user.h 를 수정한다.
#ifndef _NAVIL_USER #define _NAVIL_USER void user_task_1(void); void user_task_2(void); void user_task_3(void); void user_task_4(void); void user_task_5(void); #endif
별거 없다. user_task_4() 와 user_task_5() 함수의 프로토타입을 추가 했을 뿐이다.
: : void user_task_4(void) { int a, b, c; a = 1; b = 2; c = a + b; while(1){ navilnux_mutex_wait(3); printf("TASK4 enter critical section\n"); printf("TASK4 - a:%p\tb:%p\tc:%p\n", &a, &b, &c); msleep(4000); navilnux_mutex_release(3); printf("TASK4 out critical section\n"); msleep(3000); } } void user_task_5(void) { int a, b, c; a = 1; b = 2; c = a + b; while(1){ navilnux_mutex_wait(3); printf("TASK5 enter critical section\n"); printf("TASK5 - a:%p\tb:%p\tc:%p\n", &a, &b, &c); msleep(2000); navilnux_mutex_release(3); printf("TASK5 out critical section\n"); msleep(4000); } } void navilnux_user(void) { taskmng.create(user_task_1); taskmng.create(user_task_2); taskmng.create(user_task_3); taskmng.create(user_task_4); taskmng.create(user_task_5); }
navilnux_user.c 의 앞부분은 수정된것도 없고 반복되면서 길기만 하니깐 생략하고, 추가된 부분과 수정된 부분만 나타내었다. 일단 navilnux_user() 함수에서 새로 추가된 user_task_4 와 user_task_5 에 TCB 를 할당하는 코드가 두줄 추가 되었음을 확인하자.
그리고 TASK4 에서는 일단 루프에 들어가자마자 navilnux_mutex_wait() 시스템콜을 사용해서 3번 뮤택스를 이용해 락을 건다. 크리티컬 섹션 안의 내용은 별거 없이 그냥 TASK4가 크리티컬섹션에 들어왔다! 라고 출력하고, TASK4에 대해서 역시나 내부변수의 주소를 출력해주고 4초를 대기한다. 그리고 락을 해제 해서 크리티컬 섹션을 종료하고 다시 3초를 대기한다음 루프를 반복한다.
TASK5 도 루프에 들어가자마자 navilnux_mutex_wait()으로 TASK4와 마찬가지로 3번뮤택스에 대해서 락을 건다. 크리티컬 섹션안의 내용도 TASK4 와 동일하다 다만 TASK4는 4초를 대기 하는데, TASK5는 2초를 대기 한다. 크리티컬 섹션을 종료한다음에도 TASK4는 3초 대기하는데 TASK5는 4초르 대기 한다. 이렇게 서로 대기하는 시간을 다르게 한것은 둘의 시간을 엇갈리게 해야만 뮤택스가 올바르게 동작하는지를 확인 할 수 있기 때문이다.
현재 나빌눅스는 1초에 한번씩 컨택스트 스위칭이 돌기 때문에, TASK4의 크리티컬 섹션에서 4초를 쉬는 동안 컨택스트 스위칭이 발생해서 실행권한은 TASK5로 넘어간다. 하지만 TASK5는 컨텍스트를 받고 수행하던중 navilnux_mutex_wait() 에 의해서 뮤택스를 확인하게 되고, TASK4 와 TASK5 는 같은 3번 뮤택스를 사용하므로 TASK4가 앞서에서 3번뮤택스에 대해서 이미 잠금을 수행해 놨다. 그러므로 TASK5는 더 진행되지 못하고 blocking 되어 버리고, 실행은 다시 TASK1로 넘어간다. 이와 같은 작업이 TASK4의 대기시간이 끝나는 만큼 지연된 후에 TASK4가 뮤택스를 풀어주고 3초 대기 하는 동안 TASK5는 뮤택스를 잠그고 크리티컬 섹션에 진입해서 2초 대기 한다. 그 2초가 지나고 TASK5가 4초 대기하는동안 다시 TASK4가 컨택스트를 얻어서 크리티컬 섹션에 진입하고 하는 과정이 반복된다.
장황하게 설명했지만, 확실한것은 TASK4 enter critical section 메시지와 TASK5 enter critical section 메시지는 연달아서 절대 나오지 않는다는 것이다. TASK4 enter critical section 메시지가 나온다음에는 반드시 TASK4 out critical section 메시지가 나와야 하고 그다음에 다시 TASK4 .. 메시지가 나오던 아니면 TASK5 enter critical section 메시지가 나와야 한다. TASK5의 크리티컬 섹션안에서의 메시지도 마찬가지로 enter - out 메시지가 짝으로 나와야지 서로 다른 두개의 테스크의 enter - enter 혹은 out - out 이런식으로 나오면 뮤택스가 제대로 동작하지 않는 것이라고 봐야 한다.
위 부분 까지 작업했으면 가벼운 마음으로 make 를 치고 빌드 해서 보드에 다운로드 한다음 부팅 시켜 보자. 아래와 같은 메시지가 출력될 것이다.
TCB : TASK1 - init PC(a000c368) init SP(a04ffffc) TCB : TASK2 - init PC(a000c3c4) init SP(a05ffffc) TCB : TASK3 - init PC(a000c46c) init SP(a06ffffc) TCB : TASK4 - init PC(a000c4e8) init SP(a07ffffc) TCB : TASK5 - init PC(a000c578) init SP(a08ffffc) TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 1 TASK4 enter critical section TASK4 - a:a07fffe8 b:a07fffe4 c:a07fffe0 TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 2 TASK4 out critical section TASK5 enter critical section TASK5 - a:a08fffe8 b:a08fffe4 c:a08fffe0 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 3 ITC send!!! TASK3 - a:a06fffe8 b:a06fffe4 c:a06fffe0 ITC get!!!! ---> 342 TASK5 out critical section TASK4 enter critical section TASK4 - a:a07fffe8 b:a07fffe4 c:a07fffe0 TASK4 out critical section TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 2 TASK4 enter critical section TASK4 - a:a07fffe8 b:a07fffe4 c:a07fffe0 TASK4 out critical section TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 3 ITC send!!! TASK3 - a:a06fffe8 b:a06fffe4 c:a06fffe0 ITC get!!!! ---> 342 TASK4 enter critical section TASK4 - a:a07fffe8 b:a07fffe4 c:a07fffe0 TASK4 out critical section TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0
뭔가 제대로 되는 것 같지만 조금 이상하다. 스케줄링 순서가 조금씩 안맞는 것 같다. 그것은 테스크안에서의 대기를 msleep() 함수를 사용하기 때문이다. msleep() 함수는 엄밀히 말해 나빌눅스의 함수가 아니라 이지보드에서 가져온 함수이다. 그리고 나빌눅스가 OS timer 1 번을 사용해서 시스템 타이머를 돌리고 있는데, msleep() 은 OS timer 0 번을 사용해서 자체적으로 카운터를 돌려 대기시간을 정한다. 커널의 타이머와 함수의 타이머가 따로 돌아서, 타이머 간에 동기가 잘 안맞는 듯 하다. 그래서 나빌눅스가 사용 할 sleep() 함수를 자체적으로 만들어 보도록 하자.
일단 sleep() 함수를 구현할려면 커널안에서 시간을 세어주는 그 무엇이 있어야 한다. 리눅스에서는 1/Hz 초마다 증가하는 jiffies 라는 커널전역변수가 존재 한다. 나빌눅스에서는 cpu 의 클럭이 아니라 OS timer 에 의해 증가하는 navilnux_time_tick 이라는 전역변수를 만들도록 하겠다. 딱히 어느 소속이 없는 커널 전역변수 이므로 navilnux.c 에 선언한다.
#include <navilnux.h> extern Navil_task_mng taskmng; Navil_free_task *navilnux_current; Navil_free_task *navilnux_next; Navil_free_task dummyTCB; int navilnux_current_index; unsigned int navilnux_time_tick; void scheduler(void) { navilnux_current_index++; navilnux_current_index %= (taskmng.max_task_id + 1); navilnux_next = &taskmng.free_task_pool[navilnux_current_index]; navilnux_current = navilnux_next; } void irqHandler(void) { if( (ICIP&(1<<27)) != 0 ){ OSSR = OSSR_M1; OSMR1 = OSCR + 3686400; navilnux_time_tick++; scheduler(); } if ( (ICIP&(1<<8)) != 0 ){ GEDR0 = 1; printf("Switch Push!!\n"); } } void os_timer_init(void) { ICCR = 0x00; ICMR |= (1 << 27); ICLR &= ~(1 << 27); OSCR = 0; OSMR1 = OSCR + 3686400; OSSR = OSSR_M1; } void os_timer_start(void) { OIER |= (1<<1); OSSR = OSSR_M1; } void gpio0_init(void) { GPDR0 &= ~( 1 << 0 ); GAFR0_L &= ~( 0x03 ); GRER0 &= ~( 1 << 0 ); GFER0 |= ( 1 << 0 ); ICMR |= ( 1 << 8 ); ICLR &= ~( 1 << 8 ); } void irq_enable(void) { __asm__("msr cpsr_c,#0x40|0x13"); } void irq_disable(void) { __asm__("msr cpsr_c,#0xc0|0x13"); } int sched_init(void) { if(taskmng.max_task_id < 0){ return -1; } navilnux_current = &dummyTCB; navilnux_next = &taskmng.free_task_pool[0]; navilnux_current_index = -1; return 0; } void navilnux_init(void) { navilnux_time_tick = 0; mem_init(); task_init(); msg_init(); syscall_init(); os_timer_init(); gpio0_init(); os_timer_start(); } int main(void) { navilnux_init(); navilnux_user(); if(sched_init() < 0){ printf("Kernel Pannic!\n"); return -1; } int i; for(i = 0 ; i <= taskmng.max_task_id ; i++){ printf("TCB : TASK%d - init PC(%p) \t init SP(%p)\n", i+1, taskmng.free_task_pool[i].context_pc, taskmng.free_task_pool[i].context_sp); } irq_enable(); while(1){ msleep(1000); } return 0; }
navilnux.c 파일에서 수정된 부분은 세군데 이다. 우선 파일의 시작 부분에 unsigned int 형의 navilnux_time_tick 커널전역 변수가 선언되어 있다. 이 변수는 커널이 부팅되고 OS timer 인터럽트가 동작하기 시작하면 OS timer 인터럽트가 한번씩 발생 할 때마다 1씩 계속 증가 하게 된다. 그러면 OS timer 인터럽트 핸들러에서는 당연히 navilnux_time_tick 함수를 증가 시켜주는 코드가 들어가야 할것이다. 당연히 들어가 있다.
void irqHandler(void) { if( (ICIP&(1<<27)) != 0 ){ OSSR = OSSR_M1; OSMR1 = OSCR + 3686400; navilnux_time_tick++; scheduler(); } if ( (ICIP&(1<<8)) != 0 ){ GEDR0 = 1; printf("Switch Push!!\n"); } }
scheduler() 를 호출 하기 전에 navilnux_time_tick++ 해서 커널 카운터를 하나 증가 시켜주는 코드가 한줄 추가 되었다. 그리고 navilnux_init() 함수에서는 제일 처음에 navilnux_time_tick 변수를 0 으로 초기화 해 주는 코드가 한 줄 추가 되었다. 이렇게 navilnux.c 파일에 딱 세줄을 추가해서 간단하게 커널 타이머를 추가 하였다. 매번 강조하지만 OS 만들기는 참 쉽다. :)
이시점에서 sleep()을 어떻게 구현 할지에 대해서 살짝 생각을 해보자. 뭐 일종의 설계를 위한 시간이라고 생각 해도 좋다. 우선 사용자 테스크에서 sleep()을 호출 하면 어딘가에는 sleep()을 호출 할때 sleep() 을 탈출 하기 위한 목표 커널 카운터 값을 가지고 있어야 한다. 그래서 컨택스트를 받을 때마다 해당값을 커널카운터와 비교해서 그 값이 커널카운터보다 작아지는 순간 sleep()의 blocking 을 해제하고 함수를 탈출해야 한다.
그렇다면 그 목표로 하는 탈출값은 어디에 있어야 하는가. 고민할 필요 없다. sleep()은 해당 함수를 호출한 테스크의 동작을 지정한 시간만큼 중지 시키는 역할을 한다. 즉 해당 테스크에 sleep()을 탈출 하기 위한 정보가 있으면 된다. 테스크란 커널 내부에서 TCB로 추상화 되어 관리 된다. TCB 가 정의되어 있는 navilnux_task.h 를 수정 한다.
#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; unsigned int sleep_end_tick; } 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
컨택스트를 백업하기 위한 공간 바로 밑에 sleep_end_tick 이라는 변수가 추가로 선언되어 있다. 이 변수에 sleep()을 통해 들어오는 시간값과 현재의 navilnux_time_tick 의 값을 더해서 저장해 놓고서, 지속적으로 현재의 navilnux_time_tick 과 값을 비교하다가 어느 순간 navilnux_time_tick 의 값이 sleep_end_tick 의 값보다 커진다면 sleep()을 탈출 하도록 하면 된다.
변수가 하나 추가 되었으니 초기화 함수에도 당연히 초기화 하는 코드가 들어간다.
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.free_task_pool[i].sleep_end_tick = 0; } taskmng.max_task_id = -1; taskmng.init = task_init; taskmng.create = task_create; }
별로 하는 일은 없다. sleep_end_tick 을 0 으로 초기화 해주는 코드가 한줄 추가된것이 전부이다.
커널 타이머와 TCB에 sleep 을 위한 변수가 추가되었으니 이제 본격적으로 sleep() 함수를 만들어 보도록 하자. sleep() 계열 함수들은 이지부트에서 가져온 time.c 파일에 다 들어가 있다. 새로 구현하게될 sleep() 함수도 이 파일에 넣을까? 그래도 상관은 없다. 하지만 이왕이면 나빌눅스 고유의 코드들은 이지부트로 부터 가져온 코드들과 분리 해 놓고 싶다. 그래서 새로 구현하게 될 sleep() 함수는 navilnux_clib.c 파일에 넣기로 결정했다. navilnux_clib.c 파일에 넣기로 결정하였으므로 프로토 타입은 navilnux_lib.h 이다.
#ifndef _NAVIL_LIB #define _NAVIL_LIB extern int mysyscall(int, int, int); extern int mysyscall4(int, int, int, int); extern int itc_send(int, int); extern int itc_get(int, int*); extern int mutex_wait(int); extern int mutex_release(int); extern void call_scheduler(void); int navilnux_itc_send(int, int); int navilnux_itc_get(int); int navilnux_mutex_wait(int); int navilnux_mutex_release(int); int navilnux_sleep(int); #endif
맨 아래에 navilnux_sleep(int) 라고 하여 프로토타입을 선언한 것이 보일 것이다. 그냥 sleep()이라고 해도 되지만 나빌눅스의 네이밍컨벤션을 따라서 navilnux_ 라는 prefix 를 붙였다. 인자로 넘어가는 int 값은 밀리초가 아니라 그냥 초로 할 생각이다. 밀리초로 하고 싶다면 OS timer 의 발생 주기를 1밀리초로 줄이고, 그에 따라 연동되는 navilnux_time_tick 에 의해 연결되는 코드들을 수정 하면 될 것이다. 하지만 일단은 초를 인자로 받도록 구현 하겠다. 구현체는 navilnux_clib.c 함수에 구현한다.
#include <navilnux.h> extern Navil_free_task *navilnux_current; extern unsigned int navilnux_time_tick; : (중략) : int navilnux_sleep(int sec) { while(1){ if(navilnux_current->sleep_end_tick == 0){ navilnux_current->sleep_end_tick = navilnux_time_tick + sec; }else if(navilnux_current->sleep_end_tick <= navilnux_time_tick){ navilnux_current->sleep_end_tick = 0; return 0; }else{ call_scheduler(); } } return 0; }
역시 구현한 코드 자체의 난이도는 거의 zero 에 가깝다. 누누이 말하지만 OS만들기는 쉽다. :) 커널전역변수인 navilnux_current 와 navilnux_time_tick 변수를 extern 으로 불러왔다. navilnux_current 는 컨텍스트 스위칭과 스케줄러를 설명할때 언급했듯이 현재 동작중인 테스크를 가르키고 있다. 즉, navilnux_sleep() 을 호출한 테스크 자신이다.
현재 테스크의 TCB의 sleep_end_tick 이 0 이라면 최초의 navilnux_sleep() 호출이므로 sleep_end_tick 에다가 현재의 navilnux_time_tick 의 값에 인자로 넘어온 sec 의 값을 더해서 지정해 준다. navilnux_time_tick 은 1 초에 하나씩 증가 하므로 sec 만큼을 더하면 sec 초 만큼의 시간이 실제로 지나야 navilnux_time_tick 과 sleep_end_tick 의 값이 같아진다. 그리고 더 시간이 지나면 sleep_end_tick 의 값이 navilnux_time_tick 의 값 보다 작아진다. 그러므로 sleep_end_tick 의 값이 navilnux_time_tick 과 같거나 작아지면 navilnux_sleep() 을 탈출해야 한다. sleep_end_tick 의 값을 다시 0으로 초기화 하고 return 문으로 함수를 탈출 한다. 그외의 경우는 아직 sleep_end_tick 의 값이 navilnux_time_tick 보다 큰 경우이므로 navilnux_sleep() 은 계속 blocking 걸리게 된다.
navilnux_user.c 에 있는 사용자 테스크 함수의 실행 지연을 위한 msleep() 함수를 모두 navilnux_sleep() 함수로 대체 한다.
#include <navilnux.h> extern Navil_task_mng taskmng; void user_task_1(void) { int a, b, c; a = 1; b = 2; c = a + b; while(1){ printf("TASK1 - a:%p\tb:%p\tc:%p\n", &a, &b, &c); navilnux_sleep(1); } } void user_task_2(void) { int a, b, c; a = 1; b = 2; c = a + b; while(1){ printf("TASK2 - a:%p\tb:%p\tc:%p\n", &a, &b, &c); printf("ITC Count is %d\n", a); if (a == 3){ navilnux_itc_send(2, 342); a = 1; printf("ITC send!!!\n"); } a++; navilnux_sleep(1); } } void user_task_3(void) { int a, b, c; a = 1; b = 2; c = a + b; while(1){ c = navilnux_itc_get(2); printf("TASK3 - a:%p\tb:%p\tc:%p\n", &a, &b, &c); printf("ITC get!!!! ---> %d\n", c); navilnux_sleep(1); } } void user_task_4(void) { int a, b, c; a = 1; b = 2; c = a + b; while(1){ navilnux_mutex_wait(3); printf("TASK4 enter critical section\n"); printf("TASK4 - a:%p\tb:%p\tc:%p\n", &a, &b, &c); navilnux_sleep(4); navilnux_mutex_release(3); printf("TASK4 out critical section\n"); navilnux_sleep(3); } } void user_task_5(void) { int a, b, c; a = 1; b = 2; c = a + b; while(1){ navilnux_mutex_wait(3); printf("TASK5 enter critical section\n"); printf("TASK5 - a:%p\tb:%p\tc:%p\n", &a, &b, &c); navilnux_sleep(2); navilnux_mutex_release(3); printf("TASK5 out critical section\n"); navilnux_sleep(4); } } void navilnux_user(void) { taskmng.create(user_task_1); taskmng.create(user_task_2); taskmng.create(user_task_3); taskmng.create(user_task_4); taskmng.create(user_task_5); }
밀리초대신 초를 인자로 넣어서 1000 이 1 로 바뀐거 외에는 msleep() 이 navilnux_sleep() 으로 바뀌어 전체 사용자 테스크의 코드가 수정 되었다. 커널 이미지를 빌드 해서 보드에 다운로드 한 다음 부팅 해보자.
TCB : TASK1 - init PC(a000c454) init SP(a04ffffc) TCB : TASK2 - init PC(a000c4b0) init SP(a05ffffc) TCB : TASK3 - init PC(a000c558) init SP(a06ffffc) TCB : TASK4 - init PC(a000c5d4) init SP(a07ffffc) TCB : TASK5 - init PC(a000c660) init SP(a08ffffc) TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 1 TASK4 enter critical section TASK4 - a:a07fffe8 b:a07fffe4 c:a07fffe0 TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 2 TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 3 ITC send!!! TASK3 - a:a06fffe8 b:a06fffe4 c:a06fffe0 ITC get!!!! ---> 342 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 2 TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK4 out critical section TASK5 enter critical section TASK5 - a:a08fffe8 b:a08fffe4 c:a08fffe0 TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 3 ITC send!!! TASK3 - a:a06fffe8 b:a06fffe4 c:a06fffe0 ITC get!!!! ---> 342 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 2 TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK5 out critical section TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 3 ITC send!!! TASK3 - a:a06fffe8 b:a06fffe4 c:a06fffe0 ITC get!!!! ---> 342 TASK4 enter critical section TASK4 - a:a07fffe8 b:a07fffe4 c:a07fffe0 TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 2 TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 3 ITC send!!! TASK3 - a:a06fffe8 b:a06fffe4 c:a06fffe0 ITC get!!!! ---> 342 TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 2 TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 3 ITC send!!! TASK3 - a:a06fffe8 b:a06fffe4 c:a06fffe0 ITC get!!!! ---> 342 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 2 TASK4 out critical section TASK5 enter critical section TASK5 - a:a08fffe8 b:a08fffe4 c:a08fffe0 TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 3 ITC send!!! TASK3 - a:a06fffe8 b:a06fffe4 c:a06fffe0 ITC get!!!! ---> 342 TASK5 out critical section : :
여기서 독자들이 염두 해 두어야 할 점은 나빌눅스는 RTOS 가 아니라는 점이다. 즉, 나빌눅스는 사용자 테스크의 실행시간을 보장 해 주지 않는다. 그래서 2초 지연이라고 정확하게 2초를 지연 하는 것이 아니라, 2초 지연 중에 스케줄러에 의해 스케줄링이 계속돌고, 비로소 자기 자신의 차례가 왔을때에야 blocking 이 끝나게 된다. 게다가 우선순위를 이용하는 스케줄링이 아니라 라운드로빈 스케줄링이기 때문에 TASK5에서 sleep() 을 들어갈때 그 지연시간이 1초라 하더라도 순서대로 TASK1, TASK2, TASK3, TASK4 가 다 돌고 나서야 TASK5 에 이르러 sleep() 을 벗어 나게 된다. 지연 시간이 끝나 더라도 다시 스케줄링을 받아서 실제 탈출코드를 수행하기 전까지는 계속 block 걸린다는 점이다. 이점을 미리 설명 해 두는 바이다.
이렇게 나빌눅스 고유의 sleep() 함수를 구현 하였다. 그럼 다시 이어서 세마포어를 구현 하도록 하자. 뮤택스는 오로지 하나의 테스크만 크리티컬섹션에 진입하는 것을 허용 한다. 그래서 뮤택스를 다른 말로 상호배제 세마포어 라고도 한다. 세마포어는 뮤택스보다 좀 느슨하게 프로그래머가 지정한 수 만큼의 테스크가 크리티컬섹션에 진입하는 것을 허용한다. 즉 뮤택스는 크리티컬 섹션에 접근 가능한 갯수가 1 인 세마포어 인 것이다. 그렇다면 구현에 있어서 세마포어와 뮤택스는 매우 비슷 할 것이라 예상된다.
우선 다이젝스트라(필자는 아직도 이분의 이름을 어떻게 발음 해야 할지 모르겠다.)가 쓴 논문에 나오는 세마포어의 의사코드를 보자.
P(S) { while S <=0 ; // do nothing S--; } V(S) { S++; }
정말 아름답고 간결한 코드이다. 위 코드를 그대로 따라서 구현 할 것이다. 물론 완전 똑같이는 아니다. 다이젝스트라의 아이디어가 나빌눅스 안에서 어떻게 구현되는지를 이제부터 보도록 하자. 세마포어도 뮤택스와 구현상 별다를 것이 없을 것이라고 위에서 썻다. 그러므로 세마포어도 메시지관리자를 그대로 이용한다. navilnux_msg.h 에 세마포어 관련 내용을 추가 하자.
#define MAXMSG 255 #define ITCSTART 0 #define ITCEND 99 #define MUTEXSTART 100 #define MUTEXEND 199 #define SEMSTART 200 #define SEMEND 254 typedef struct _navil_free_msg { int data; int flag; } Navil_free_msg; typedef struct _navil_msg_mng { Navil_free_msg free_msg_pool[MAXMSG]; void (*init)(void); int (*itc_send)(int, int); int (*itc_get)(int, int*); int (*mutex_wait)(int); int (*mutex_release)(int); int (*sem_init)(int, int); int (*sem_p)(int); int (*sem_v)(int); } Navil_msg_mng; void msg_init(void); int msg_itc_send(int, int); int msg_itc_get(int, int*); int msg_mutex_wait(int); int msg_mutex_release(int); int msg_sem_init(int, int); int msg_sem_p(int); int msg_sem_v(int); #endif
세마포어와 관련해서 msg_sem_init() 함수와 msg_sem_p(), msg_sem_v() 함수가 추가 되었다. msg_sem_init() 함수는 세마포어 변수를 초기화 하는 함수이다. 뮤택스는 크리티컬섹션에 진입할때 무조건 0이 되고 나올때 1 이 되므로 별도의 초기화가 필요하지 않지만, 세마포어는 S 값을 지정해 주어서 크리티컬 섹션에 들어갈때 마다 S 값을 하나씩 감소 시키므로 바로 이 S 값을 지정해주는 초기화 함수가 별도로 필요하다. msg_sem_p() 함수는 다이젝스트라가 작성한 알고리즘에서 P() 함수를 그대로 구현 할것이다. 즉, S가 0보다 작거나 같으면 blocking 걸리고, 그렇지 않으면 S를 감소시킨다. 마찬가지로 msg_sem_v()함수는 V()함수를 구현한다. S를 단순히 하나 증가 시키기만 한다.
실제로 어떻게 구현 되었는지는 navilnux_msg.c 를 보자.
: (중략) : int msg_sem_init(int semnum, int s) { semnum += SEMSTART; if(semnum > SEMEND || semnum < SEMSTART){ return -1; } msgmng.free_msg_pool[semnum].flag = s; return 0; } int msg_sem_p(int semnum) { semnum += SEMSTART; if(semnum > SEMEND || semnum < SEMSTART){ return -1; } if(msgmng.free_msg_pool[semnum].flag <= 0){ return -2; } msgmng.free_msg_pool[semnum].flag--; return 0; } int msg_sem_v(int semnum) { semnum += SEMSTART; if(semnum > SEMEND || semnum < SEMSTART){ return -1; } msgmng.free_msg_pool[semnum].flag++; return 0; } void msg_init(void) { int i; for (i = 0 ; i < MAXMSG ; i++){ msgmng.free_msg_pool[i].data = 0; msgmng.free_msg_pool[i].flag = 0; } msgmng.init = msg_init; msgmng.itc_send = msg_itc_send; msgmng.itc_get = msg_itc_get; msgmng.mutex_wait = msg_mutex_wait; msgmng.mutex_release = msg_mutex_release; msgmng.sem_init = msg_sem_init; msgmng.sem_p = msg_sem_p; msgmng.sem_v = msg_sem_v; }
msg_sem_init(), msg_sem_p(), msg_sem_v()함수 모두 동작하기 전에 경계값을 체크한다. 경계값을 체크하는 테크닉은 뮤택스와 동일하다. 메시지관리자의 자유메시지블럭에 있는 두개의 변수를 수정없이 사용하기 위해 S 대신 flag를 사용했다. msg_sem_init() 함수에서는 이 flag 에다가 인자로 들어온 s 의 값을 지정한다. 그리고 msg_sem_p() 함수는 다이젝스트라의 알고리즘 그대로, flag 가 0보다 작거나 같으면 -2 를 반환한다. 뮤택스의 구현에서 보면 알겠지만 -2를 반환하면 사용자라이브러리에서 blocking 걸리게 된다. 그렇지 않다면 flag 를 하나 감소시킨다. msg_sem_v()는 단순하게 flag 를 하나 증가시키고 종료 한다. 정말 다이젝스트라의 알고리즘을 너무도 충실히 구현 한 소스이다. :)
이제 남은 작업은 또 반복 되는 시스템콜 추가 작업이다. 사람이 가장 견디기 어려워 하는 것이 반복 작업이라고 하였던가. 이젠 비슷한 코드 붙이고 설명하기도 힘들다. 시스템콜 추가에 대한 내용은 생략하도록 하겠다. 추가한 시스템콜은 다음과 같다.
syscalltbl.h SYS_SEMP 6 SYS_SEMV 7 navilnux_sys sys_semp() sys_semv() navilnux_lib sem_p() sem_v() navilnux_clib navilnux_sem_p() navilnux_sem_v()
세마포어를 하나 감소 시키는 P() 는 6번, 세마포어를 증가 시키는 V() 는 7번 시스템콜 번호를 부여 받았다. 그리고 시스템콜 등록 절차에 따라 sys_semp() 와 sys_semv() 는 메시지관리자에 구현한 함수를 그대로 호출 하였고, sem_p() 와 sem_v() 역시 swi 명령을 하는 것 외에는 아무것도 하지 않는다. navilnux_sem_p() 는 뮤택스와 마찬가지로 시스템콜의 반환값에 따라 blocking 을 하거나 함수를 종료하는 작업을 하고 navilnux_sem_v() 는 그대로 시스템콜과 같은 작업을 한다.
세마포어를 제대로 테스트 할려면 두개의 테스크로는 부족하다. S 값을 2 로 하고 최소 3개이상의 테스크가 있어야먄 제대로 동작하는 것을 확인 할 수 있다. 그래서 테스크를 3개 더 추가 한다. navilnux_user.h 에 프로토타입을 추가 한다.
#ifndef _NAVIL_USER #define _NAVIL_USER void user_task_1(void); void user_task_2(void); void user_task_3(void); void user_task_4(void); void user_task_5(void); void user_task_6(void); void user_task_7(void); void user_task_8(void); #endif
테스크가 8번까지 늘었다. 점점 OS 가 커지고 있다. 하지만 걱정 하지 말아라, 잘 동작 할 것이다. :) 그럼 navilnux_user.c 에 새로 추가한 TASK6, TASK7, TASK8 의 본체를 작성하자.
: (중략) : void user_task_6(void) { int a, b, c; a = 1; b = 2; c = a + b; while(1){ navilnux_sem_p(5); printf("TASK6 enter critical section SEMAPHORE\n"); printf("TASK6 - a:%p\tb:%p\tc:%p\n", &a, &b, &c); navilnux_sleep(2); navilnux_sem_v(5); printf("TASK6 out critical section SEMAPHORE\n"); navilnux_sleep(4); } } void user_task_7(void) { int a, b, c; a = 1; b = 2; c = a + b; while(1){ navilnux_sem_p(5); printf("TASK7 enter critical section SEMAPHORE\n"); printf("TASK7 - a:%p\tb:%p\tc:%p\n", &a, &b, &c); navilnux_sleep(2); navilnux_sem_v(5); printf("TASK7 out critical section SEMAPHORE\n"); navilnux_sleep(4); } } void user_task_8(void) { int a, b, c; a = 1; b = 2; c = a + b; while(1){ navilnux_sem_p(5); printf("TASK8 enter critical section SEMAPHORE\n"); printf("TASK8 - a:%p\tb:%p\tc:%p\n", &a, &b, &c); navilnux_sleep(2); navilnux_sem_v(5); printf("TASK8 out critical section SEMAPHORE\n"); navilnux_sleep(4); } } void navilnux_user(void) { taskmng.create(user_task_1); taskmng.create(user_task_2); taskmng.create(user_task_3); taskmng.create(user_task_4); taskmng.create(user_task_5); taskmng.create(user_task_6); taskmng.create(user_task_7); taskmng.create(user_task_8); msgmng.sem_init(5,2); }
사용자 테스크를 보기에 앞서 navilnux_user() 함수를 보자. TCB에 사용자 테스크 함수를 연결하는 동작이 다 끝나고 세마포어를 초기화 한다. 나빌눅스에서 세마포어는 테스크간의 동기화를 위해 존재하므로 그 초기화는 사용자테스크의 역할이 아니라 커널의 역할이다. 그래서 시스템콜에도 sem_init() 에 해당하는 시스템콜은 만들지 않았다. 커널에서 직접 호출 하기 때문에 시스템콜 레이어를 거칠 필요가 없다. 위 소스에서는 5번 세마포어에 2를 할당하여 초기화 하였다.
TASK6, TASK7, TASK8 의 출력 내용은 본질적으로 TASK4, TASK5와 다르지 않다. 다만 세개의 테스크가 하나의 세마포어(5번 세마포어)를 사용하여 동기화를 처리 한다는 것이다. 위와 같이 작성 한다면, TASK6, TASK7, TASK8 세개중 두개는 같은 시점에 크리티컬 섹션에 존재 할 수 있고, 그 두개의 테스크중 하나가 크리티컬 섹션을 빠져 나와야 나머지 테스크가 크리티컬 섹션에 진입 할 수 있을 것이다. 라운드로빈스케줄링을 하므로 순서대로 컨텍스트를 받는다고 했을 때, TASK6 과 TASK7 이 먼저 크리티컬 섹션에 들어가고 정해진 지연시간을 다 채우고 나면 아마 TASK6 이 먼저 크리티컬 섹션을 빠져 나갈 것이다. 그러면 바로 이어서 TASK8이 크리티컬섹션에 진입하고, TASK7 이 크리티컬섹션을 나오면 대기중이던 TASK6이 크리티컬섹션에 진입하고... 하는 과정이 반복될 것이다. 순서는 틀릴지 모르겠지만 이런식으로 동작해야 세마포어가 정상동작한다고 확신 할 수 있다.
여기까지 작업에서 새로운 파일이 추가되지는 않았으므로 Makefile은 수정할 필요 없이 make 를 실행해서 커널을 빌드 하자. 빌드된 이미지 파일을 보드에 다운로드 하고 부팅시켜서 제대로 메시지가 나오는지를 확인 해 보자.
TCB : TASK1 - init PC(a000c798) init SP(a04ffffc) TCB : TASK2 - init PC(a000c7f4) init SP(a05ffffc) TCB : TASK3 - init PC(a000c89c) init SP(a06ffffc) TCB : TASK4 - init PC(a000c918) init SP(a07ffffc) TCB : TASK5 - init PC(a000c9a4) init SP(a08ffffc) TCB : TASK6 - init PC(a000ca30) init SP(a09ffffc) TCB : TASK7 - init PC(a000cabc) init SP(a0affffc) TCB : TASK8 - init PC(a000cb48) init SP(a0bffffc) TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 1 TASK4 enter critical section TASK4 - a:a07fffe8 b:a07fffe4 c:a07fffe0 TASK6 enter critical section SEMAPHORE TASK6 - a:a09fffe8 b:a09fffe4 c:a09fffe0 TASK7 enter critical section SEMAPHORE TASK7 - a:a0afffe8 b:a0afffe4 c:a0afffe0 TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 2 TASK7 out critical section SEMAPHORE TASK8 enter critical section SEMAPHORE TASK8 - a:a0bfffe8 b:a0bfffe4 c:a0bfffe0 TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 3 ITC send!!! TASK3 - a:a06fffe8 b:a06fffe4 c:a06fffe0 ITC get!!!! ---> 342 TASK6 out critical section SEMAPHORE TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 2 TASK8 out critical section SEMAPHORE TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 3 ITC send!!! TASK3 - a:a06fffe8 b:a06fffe4 c:a06fffe0 ITC get!!!! ---> 342 TASK4 out critical section TASK5 enter critical section TASK5 - a:a08fffe8 b:a08fffe4 c:a08fffe0 TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 TASK2 - a:a05fffe8 b:a05fffe4 c:a05fffe0 ITC Count is 2 TASK6 enter critical section SEMAPHORE TASK6 - a:a09fffe8 b:a09fffe4 c:a09fffe0 TASK7 enter critical section SEMAPHORE TASK7 - a:a0afffe8 b:a0afffe4 c:a0afffe0 TASK1 - a:a04fffe8 b:a04fffe4 c:a04fffe0 : :
위의 메시지상으로 보면 TASK6과 TASK7 이 연달아 크리티컬 섹션에 진입했고, 이내 TASK7이 크리티컬섹션에서 빠져 나왔다. 그러자 바로 TASK8이 크리티컬섹션에 진입했고, 잠시후에 TASK6이 크리티컬 섹션에서 빠져 나왔다. 그리고 바로 이어서 TASK8도 크리티컬 섹션에서 나왔고, 잠시후에 TASK6과 TASK7이 다시 크리티컬 섹션에 진입했다. 순서 자체는 예상과 조금 틀렸지만 확실한것은 동시에 최대 두개까지의 테스크만 크리티컬섹션에 존재 한다는 것이다. 세마포어가 정상 동작 하는 것을 확인 하였다.
예상외로 강좌가 길어졌다. 이번회차 강좌를 마무리 하면서 나빌눅스는 멀티테스킹에 테스크동기화까지 지원하는 아주 강력한 임베디드OS가 되었다. 멋있지 않은가? 몇줄 안되면서도 전혀 복잡하지도 않은 코드가 추가 되면서 운영체제의 기능이 하나씩 늘어나는 과정이! 계속 강조하지만 OS 만들기는 결코 어렵지 않다. 최소한 코딩자체는 전혀 어렵지 않다. 다만 OS의 각부분에 대한 정책을 정하고, 그것에 맞추어 프로그램을 작성하는 것과 타겟 하드웨어(cpu, 주변장치)에 대한 이해가 필요하다는 것이 약간 부담 될 뿐이다. 물론 리눅스 같은 초대형 운영체제는 코드도 어렵다. ^^;
이제 연재도 막바지로 치닫고 있다. 읽어보시면서 오류가 발견되면 기탄없이 댓글을 달아주시며 같이 공부 해 나가도록 하자.
이 글은 http://raonlife.com/navilera/blog/view/90/에 동시 연재 됩니다.
첨부 | 파일 크기 |
---|---|
navilnux_chap16.tgz | 28.53 KB |
댓글
드디어 16화가 올라왔군요!!
잘봤습니다 ^^
---------------------------------
Life is not fair, Get used to it.
---------------------------------
Life is not fair, Get used to it.
음... 1회부터 읽어볼
음... 1회부터 읽어볼 생각을 하니 앞이 캄캄하네요. 그래도 힘을 내서! :)
댓글 달기