티스토리 뷰

심화

megaOS - 7. Context(Task) Switching

Just4Fun 2016. 10. 27. 20:17

앞의 글에서는 idle task의 context들을 스택메모리에 초기화 시킨 다음, arch_load_context() 함수를 불러서 스택메모리에 있던 context data들을 CPU 내부 레지스터로 복사하여 idle_task()라는 함수를 호출하는 과정에 대해서 설명하였다.


이번 글에서는 동작 중이던 어떤 타스크를 중지하고, 다른 타스크를 수행하는 과정에 대해서 설명하도록 하겠다.  이렇게 타스크를 바꾸는 동작을 task switching 또는 context switching이라고 한다.


먼저, TCB 구조체에 몇가지 필드를 추가해 보도록 하겠다.

suspend_count와 sched_link를 추가하였다.


이제, main()에서부터 다시 시작하면서 필요한 내용을 추가해가며 설명하도록 하겠다.

앞의 글에서도 얘기했지만, idle_task가 while(1)로 무한 루프를 수행하기 때문에 굳이 main() 함수에도 동일한 코드가 들어갈 필요가 없다. os_start()로 진입하게 되면 더이상 main()으로 다시 돌아오지 않기 때문이다.  따라서 main()에 있던 while(1);를 삭제한 다음 os_start() 함수로 넘어가겠다.

계속 함수를 따라가다가 task_init()에서 idle task를 생성하기 위하여 os_task_create()를 부르는 부분까지 진행한다.


os_task_create()를 다음과 같이 몇줄을 더 추가한다.

16번줄은 생성되는 타스크의 시작 상태를 SUSPEND상태로 초기화한다.

17번줄은 때에 따라 이미 타스크가 suspend 상태임에도 불구하고 계속해서 suspend 시킬 경우가 있다.  이런 경우를 처리하기 위해서 suspend_count를 사용하는데, 초기값으로 1을 사용한다.

19번 줄은 타스크가 running 상태가 될 경우 스케줄러의 sched_list[]에 등록하기 위하여 sched_link를 초기화 하는 코드이다.  sched_link의 item 포인터는 생성되는 타스크의 TCB를 저장하고 있다.

21번 줄도 내용이 바뀌었으므로 어느 부분이 달라졌는지 확인해 보기 바란다.

앞의 글에서는 두번째 인자에, 생성되는 타스크의 진입함수의 입력인자에 해당하는 값을 넣어 주었고, 세번째 인자에는 진입함수 자체의 함수 포인터값을 넘겨 주었는데,

이번에는 함수 인자로 TCB를 넘겨 주는걸로 바뀌었고, 진입 함수도 task_entry라는 함수로 바뀌었다.

여기까지 진행되었으면 idle task의 TCB와 스택메모리를 할당하고 context 초기화까지 완성된 상태이다.


task_init() 함수에 sched_set_current_tcb(idle_tcb);를 추가한다.

sched_set_current_tcb() 함수는 os_sched.c 파일에 만들것이다.


sched_set_current_tcb() 함수를 만들기에 앞서, os_sched.c 파일에서 사용하는 몇가지 변수들을 선언해 보도록 하겠다.

os_sched_lock_count는 프로그램이 특정 동작을 수행하는 도중에 타스크 스위칭이 되지 않도록 하기 위해서 사용한다.  이 변수는 인터럽트 코드에서 참조할 필요가 있기 때문에 전역 변수로 선언하였다.

current_tcb는 CPU를 점유하고 있는 타스크가 어떤것인지 표시를 하기 위해서 사용한다.

need_reschedule은 타스크 스위칭이 필요하다는 것을 알려주기 위하여 사용된다.

current_tcb와 need_reschedule은 os_sched.c 파일 내에서만 사용하므로 static 키워드를 이용하여 범위를 파일 내부로 제한 하였다.

변수를 선언했으므로 초기화를 시켜야 하겠다.


변수 선언과 초기화를 했으므로, sched_set_current_tcb() 함수를 만들어 보겠다.

내용은 간단하다. 인자로 넘어온 TCB를 current_tcb에 알려주는것만 수행한다.  즉, 이제 current_tcb는 idle_tcb를 가리키게 되는 것이다.


다시, task_init()로 와서, os_task_resume(idle_tcb);를 추가한다.

이렇게 되면 task_init() 함수의 내용은 다음과 같게 된다.


os_task_resume() 함수는 suspend 되어 있는 타스크를 깨워서 다시 실행하도록 만드는 함수이다.

3번줄의 os_sched_lock()은 타스크 스위치가 일어나지 않도록 잠시 막아두기 위해서 사용하는것이고,

18번줄의 os_sched_unlock()은 필요한 동작이 끝났으니까 타스크 스위칭을 해도 된다라는것을 알려주기 위하여 사용한다.  이 두함수는 항상 쌍으로 나오게 된다.

TCB의 suspend_count가 0이 아니고, 1보다 큰 경우에는 suspend_count값을 하나 감소시키기만 하면 된다.

suspend_count가 1인 경우, 그 값을 0으로 만들고, 타스크 상태값에서 SUSPEND 상태를 clear 한다.

이렇게 했을때 타스크의 상태가 running 상태가 되면 sched_add_tcb()를 불러서 sched_list에 링크 시키도록 한다.

그렇다면 이제 idle_tcb가 run_list에 연결된다는 것을 의미한다.


sched_add_tcb() 함수를 os_sched.c에 다음과 같이 작성한다.

인자로 넘어온 TCB 구조체에서 priority값을 읽어서 우선순위에 해당되는 shed_list를 찾는다.

8번줄은 혹시 TCB가 이미 sched_list 어딘가에 연결있는 상태라면, 그 리스트에 link를 빼준다.

13번줄은 선택된 리스트에 아무런 TCB가 연결되어 있지 않은 상태라면, sched_map의 해당 우선순위 위치의 bit값을 1로 만든다.  지금 같은 경우라면 idle task의 우선순위에 해당되는 7번 bit값이 1로 될 것이다.

이미 1로 설정되어 있는 경우라면 굳이 다시 1로 만들 필요는 없게 된다.

15번 줄은 sched_list에 해당 TCB의 sched_link를 연결시켜준다.

16~18번은 새로 추가된 TCB가 현재 CPU를 점유중인 타스크보다 우선 순위가 높거나,

현재 수행중인 타스크가 running 상태가 아닌 경우, 타스크 스위칭이 필요하게 되었다는 것을 알려주는 코드이다.  그러나, 지금 순간에는 이미 sched_init()에서 need_reschedule을 true로 만들어 놓았기 때문에 큰 의미는 없다.  그러나 프로그램이 정상적으로 진행되어 여러 타스크들이 돌고 있는 상황이라면 이제 타스크 스위칭이 필요한 때가 되었음을 알려주게 되는 것이다.


지금까지 진행된 내용을 정리해보면, idle 타스크를 생성 시켰고, sched_list에 링크 시켜놓았으므로 스케줄러가 동작하게 되면 첫번째로 idle task를 실행할 수 있게하는 상태를 만들어 놓은것이다.


os_task_resume() 함수의 시작과 끝 부분에 있었던 os_sched_lock()과 os_sched_unlock() 코드는 다음과 같다.

os_sched_lock()은 단지 os_sched_lock_count값을 증가 시키는 동작만 수행한다.

os_sched_lock_count의 초기값으로 1을 가지고 있으므로 지금은 이제 2가 되었을 것이다.

os_sched_unlock()은 os_sched_lock_count값을 하나 감소시켰을 때 그 값이 0이 되면 unlock()이라는 함수를 부르도록 되어있다.

unlock()은 스케줄러 안에서도 가장 중요한 부분이므로 뒤에서 따로 설명하도록 하겠다.

어쨋든 지금은 다시 os_sched_lock_count이 1이 되었으므로 unlock() 부르지 않고 그냥 빠져나가게 될것이다.


이제 더이상 진행되는 코드가 없으므로 지금까지 진행되어왔던 순서를 거꾸로 되돌아 가서 os_start()까지 오게 된다.


os_start() 함수에 다음과 같은 코드를 만든다.

arch_load_context()에 대한 설명은 앞의 글에서 설명하였으므로 schedule()에 대해서만 설명하도록 하겠다.

ARCH_LSBIT_INDEX()는 두번째 인자에서 첫번째로 1의 값을 가지는 비트의 위치를 찾아서 첫번째 인자에 넣어주는 기능을 수행한다.

schedule() 함수는 상당히 자주 불리워지는 함수이기 때문에 최대한 빨리 처리하여야 한다.  이러한 이유로 CPU에서 제공하는 가장 빠른 방법을 동원하여 비트 위치를 찾도록 하여야 한다.

AVR에는 이러한 기능을 제공하는 명령어가 없기 때문에 어쩔수 없이 코드를 잘 만들어야 한다.


arch.h에 다음과 같은 매크로를 만든다.

위의 매크로를 이용하여 sched_map에서 1로 되어 있는 비트의 위치를 찾은 다음 sched_list에서 list_peek()를 이용하여 해당 TCB 구조체를 가지고 온다.

지금은 idle task만 running 상태로 되어 있으므로 비트 위치는 7이 될것이고, sched_list[7]에 연결된 링크를 찾으면 idle task의 TCB 구조체를 찾을수 있게 된다.


idle TCB의 스택메모리에서 context를 읽어 CPU에 전달하면 TCB에 등록된 entry 함수가 실행될 것이다.

앞의 글에서는 ARCH_TCB_INIT_CONTEXT() 매크로에서 idle_task함수를 직접 넣어 주었지만, 이번 글에서는 task_entry()라는 함수를 넣어 주었고, 그 입력 인자로 TCB를 넣어 주었던것을 기억하여야 한다.


task_entry()를 os_task.c에 다음과 같이 입력한다.

4번줄에서 TCB의 entry 함수를 불러주고 입력 인자로는 TCB의 data를 넘겨주는 것을 볼 수 있다.

sched_tcb_entry()는 스케줄러에게 새로운 타스크가 생성되어 실행되므로 그런 사실을 알고 있어라라고 알려주기 위하여 사용한다.


os_sched.c 에 다음과 같이 만든다.

새로운 타스크가 시작되므로 타스크 스위칭이 필요없다는 것을 알려주고, 새로 시작되는 TCB를 current_tcb로 만든다.

타스크가 새로 시작되므로 혹시라도 os_sched_lock_count 값이 1이 아니면 1이 될때까지 os_sched_unlock()을 수행한다.  마지막으로 os_sched_lock_count가 1이 된 상태에서 다시 os_sched_unlock()을 부르면, 그 안에 있던 unlock(0)이 불리워 지게 될 것이다.



그럼, 스케줄러의 핵심에 해당되는 unlock() 함수에 대해서 알아보겠다.

현재 CPU를 점유중인 타스크의 상태가 running이 아니거나, need_reschedule이 true가 되면 schedule()을 이용하여 새로운 타스크에 대한 TCB를 찾아낸다.  새로찾은 타스크가 지금 돌고 있는 타스크가 아니라면 arch_switch_context()를 불러서 context switching 동작을 수행한다.

그렇게 되면 현재 수행중이던 타스크의 context들이 모두 스택 메모리에 저장되고, 새로운 타스크의 context들을 CPU로 복사하여 새로운 타스크가 CPU를 점유하게 된다.

타스크가 바뀌었으므로 current_tcb를 새로 수행되는 타스크의 TCB를 가리키도록 만들어야 한다.

13번 줄이 current_tcb를 새로운 TCB를 가리키도록 하는것이다.

15번 줄은 어찌되었던 스케줄링을 수행했으므로 새로운 이벤트가 발생하기 전까지 스케줄링을 할 필요가 없게 되었으므로 need_reschedule을 false로 만든다.

18번 줄에서 입력 인자로 들어온 lock 값을 os_sched_lock_count에 넣어준다.  0의 값이 들어 왔으므로 os_sched_lock_count의 값이 0이 된다.  그렇다면 sched_tcb_entry()에서 while() 조건이 끝나게 되므로 sched_tcb_entry() 함수를 끝내게 된다.


지금 시점에는 생성된 타스크가 idle 타스크 하나뿐이 없으므로 unlock()에서는 타스크 스위칭이 일어나지 않고 그냥 빠져 나오게 된다.

그 다음에는 task_entry()로 다시 돌아와서 tcb->entry(tcb->data);가 수행되게 된다.

즉, idle_task(0x1234)가 실행되는 것이다.


idle_task()는 다음과 같이 수정한다.

idle 타스크는 가장 낮은 우선 순위를 가지면서 항상 running 상태를 유지하고 있어야 한다.

실제 프로젝트를 수행하기 위해서는 다른 타스크들을 만들어야 한다.

이렇게 하기 위해서 idle 타스크에서는 main 타스크를 생성시켜 준다.

main 타스크는 프로젝트마다 다른 동작을 수행하여야 하므로 board.c 파일에 만들도록 하겠다.

타스크를 만들때 인자로 들어가야 하는 priority와 스택 사이즈는 board.h 에서 설정하면 된다.  만약 특별히 지정해 주지 않고 디폴트 값을 사용할 수 있도록 하기 위해서 os.h 에 다음과 같은 코드를 추가한다.

main 타스크의 디폴트 우선순위는 1이고 스택 크기는 최소 스택 크기로 하였다.  디폴트 값을 변경하고 싶으면 위의 값을 수정하면 되고, 그냥 프로젝트별로 따로 지정해 주고 싶으면 BOARD_MAIN_TASK_PRI, BOARD_MAIN_TASK_STACK_SIZE를 이용하여 설정해 주면 된다.


시험을 위하여 board.c 파일에 다음과 같은 main_task()를 만들어 보겠다.

main_task()의 내용은 idle_task와 동일하게 만들어 봤다.


idle 타스크보다 main 타스크의 우선 순위가 높으므로 idle_task() 안에서 main_task를 생성하고 os_task_resume()를 부르면 unlock() 함수에서 스케줄링이 발생하게 된다.


그렇게 되면 드디어 arch_switch_context()이 불리워져서 context 스위칭이 일어나게 된다.

arch_switch_context()는 context.S 파일에서 arch_load_context() 함수 위에 놓여야 한다.

그렇게 하는 이유는 현재 타스크의 context를 store 한 후, 새로운 타스크의 context를 load 하는 동작을 연속해서 수행하여야 하기 때문이다.

arch_switch_context()는 다음과 같이 만든다.

눈여겨 볼것은 4번줄에서 cli 명령어를 이용하여 인터럽트를 diable 시킨다는 것이다.  context switching 도중에 인터럽트가 발생하는 것을 막기 위해서 이다.


실제로 프로그램이 어떻게 진행되는지 하나씩 하나씩 확인해 보고 싶으면 앞의 글에서 설명한 내용을 참고하여 AtmelStudio의 시뮬레이터 기능을 동작시켜 보기 바란다.



프로그램을 모두 작성한 후 프로젝트를 빌드하여 실행 시키면 다음과 같은 결과를 확인할 수 있다.


idle 타스크와 main 타스크를 생성하기 위하여 메모리가 할당된 내역을 볼 수 있고, 각각의 진입함수가 제대로 실행되었음을 알 수 있다.



megaOS.zip



심화 과정 목차

'심화' 카테고리의 다른 글

megaOS - 9. Counter  (0) 2016.10.29
megaOS - 8. Interrupt  (0) 2016.10.29
megaOS - 6. Context load  (0) 2016.10.24
megaOS - 5. TCB와 Context  (2) 2016.10.22
megaOS - 4. Scheduler(스케줄러)  (0) 2016.10.22
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함