티스토리 뷰
이번 글에서는 타스크와 관련된 내용인 TCB와 context에 대한 설명과 함께 실제로 어떻게 새로운 타스크를 만드는지 구현해 보도록 하겠다.
위의 그림은 uC/OS-II 문서에 나와 있는 그림이다. 이 그림을 보면 전체적인 타스크와 관련된 내용을 한눈에 볼수가 있다.
Task Control Block이라고 하는 TCB는 각 타스크에 대한 정보를 가지고 있다. RTOS 글에서 나오는 아무추어 바둑선수들의 프로필 정보라고 생각하면 될것이다. TCB에는 타스크의 우선순위, 상태, 스택 포인터 등등 많은 정보를 가지고 있다.
그림에서 볼수있듯이 각 타스크는 별도의 스택 영역을 가지고 있어야 한다. 스택의 크기는 타스크의 기능에 따라 그 크기를 알맞게 설정할 수 있다.
Context라고 하는것은 위의 그림에 표시되어 있는데, 타스크 교환이 일어날 때 CPU안에 있는 레지스터들을 각 타스크에 저장하고, 읽어 오는 정보들이라고 보면 된다.
예를 들어 현재 Task#1이 동작되고 있는 상황에서 Task#2로 타스크 스위칭을 하게 되면, CPU안에 있는 레지스터들의 값을 Task#1의 TCB에 있는 스택 포인터 주소에 복사를 한다. 그 다음에는 Task#2의 TCB에 있는 스택 포인터 주소가 가리키고 있는 곳에서 이전에 저장 되어 있던 레지스터 정보를 CPU로 복사를 해준다.
이런 과정을 거치면, Task#2는 이전에 수행되다가 멈춘 시점부터 재시작 되므로 Task#2의 입장에서는 항상 자기 자신만 CPU를 점유하고 있는줄 알게 된다.
그러다가 Task#2가 특별히 할일이 없게되면, 즉, Task#2가 처리해야할 일이 없는 상황이 되면, 좀전에 수행되다가 멈춘 Task#1을 다시 수행시키기 위해서 타스크 스위칭을 하면 된다.
역시, Task#2가 수행하는데 사용되었던 CPU 레지스터들을 Task#2의 TCB의 스택 포인터가 가리키고 있는곳에 저장을 하고, Task#1의 스택에서 저장되었던 CPU 레지스터들을 CPU로 복사해 오면 Task#1은 이전에 멈춘 위치에서부터 계속해서 작업을 수행하게 된다.
간단하게 타스크가 동작하는 원리를 설명하였으므로 실제로 타스크와 관련된 내용을 구현해 보도록 하겠다.
os.h에 타스크와 관련된 내용을 추가하는것으로 시작하겠다.
위에서 설명했듯이 각 타스크는 개별로 스택을 가지고 있어야 하는데 최소한의 스택 크기를 미리 지정해 놓아야 한다. 그 크기는 보드에서 돌아가는 타스크의 기능에 따라 달라질수 있으므로 board.h에 BOARD_MIN_STACK_SIZE로 지정해 주면 이 값을 OS_MIN_STACK_SIZE로 설정해서 사용한다. 특별하게 지정해 주지 않아도 되는 경우에는 디폴트 값으로 64바이트 크기를 최소 스택 사이즈로 설정하게 한다. 스택은 함수 호출이 될 때 조금씩 사용되는데 특별한 경우가 아니면 그렇게 많은 스택이 필요하지는 않다. 따라서 타스크에 할당하는 스택 사이즈를 너무 크게 잡을 필요는 없다.
타스크에서 필요한 스택 사이즈를 알려면 타스크에서 수행되는 함수들을 따라가면서 몇바이트크기를 스택에 저장하는지 계산해 보면 대충 알수있다. 이 작업은 lss 파일을 열어서 "push", "pop" 명령어를 찾아보면 쉽게 알 수 있다.
한가지 주의할 점은 함수 내부에서 선언하는 변수들은 주로 CPU내부 레지스터나 스택메모리에 할당되는 경우가 있으므로 변수 선언할 때 유의하여야 한다.
4번줄은 포인터 변수의 주소를 정수형으로 표시하기 위하여 사용되는 데이터형을 선언해 주는것이다. AVR은 포인터의 크기가 2바이트이므로 uint16_t로 선언하였다.
7~16은 타스크의 현재 상태가 무엇인지 설정할 때 사용하기 위하여 선언해 놓은 것이다.
18~28은 타스크의 상태가 변할때 어떤 이유로 타스크의 상태가 변경되었는지 그 이유를 알려주기 위하여 사용된다.
35번은 생성된 타스크가 실행 될 때 어떤 함수를 실행할지 알아야 하는데, 그 때 사용되는 함수 포인터 모습이다.
36~48이 TCB의 구조체를 정의한 것이다.
타스크에 할당된 스택을 관리하기 위하여 stack_ptr, stack_base, stack_size가 필요하다.
entry는 타스크의 시작 함수 주소를 저장하기 위한 필드이다.
data는 타스크가 처음 시작 되는 함수의 인자로 들어갈 내용을 저장하기 위한 필드이다.
priority는 타스크의 우선 순위를 저장하기 위한 필드이다.
name은 타스크의 이름을 저장하기 위한 필드이다.
state는 현재의 타스크 상태가 무엇인지 저장하기 위한 필드이다.
실제로는 이것보다 더 많은 항목들이 있는데, 그런 내용은 차차 추가해 나가기로 하겠다.
kernel/os_sched.c 파일에 있는 os_init() 함수에 task_init()을 추가한다.
kernel폴더에 os_task.c 파일을 만들고 그 안에 task_init() 함수를 다음과 같이 작성한다.
task_init() 함수에서는 os_task_create()를 이용하여 idle_task에 대한 TCB와 스택영역을 만드는 동작을 수행한다.
os_task_create()는 다음과 같은 모습을 하고 있다.
os_task_create()는 모두 5개의 인자를 입력으로 받는다.
entry는 타스크가 생성된 후 해당 타스크의 진입점으로 삼는 함수 포인터 주소이다.
data는 entry가 수행될 때 입력으로 들어가는 인자값에 해당한다.
priority는 생성되는 타스크의 우선순위 값이다.
stack_size는 생성되는 타스크에게 할당되는 스택 크기이다.
마지막으로 name은 타스크의 이름이다.
예를 들어 task_init() 함수에서 생성되는 idle task는 다음과 같은 정보를 입력으로 넣어주는 것이다.
idle task가 실행될 때 수행되는 함수의 이름은 idle_task()가 된다.
그리고 idle_task()가 실행될 때 입력 인자로 들어가게되는 값은 0x1234가 된다.
idle task의 우선순위는 제일 낮은 우선 순위값을 할당하게 된다.
그리고 idle task의 스택 크기도 최소 크기를 할당하도록 하는것이다.
마지막으로 idle task의 이름을 "Idle"이라고 하였다.
스케줄러에 의해 idle task가 실행 될 때 수행되는 idle_task()함수를 다음과 같이 만든다.
idle_task()가 수행되면 "Idle Task running ... [0x1234]"라고 출력되어야 한다.
idle_task()는 특별히 하는일이 없으므로 그냥 무한 루프를 수행하도록 만든다.
idle_task() 함수가 제대로 수행되는것을 확인하게 되면 main()의 마지막 줄에 있는 while(1); 은 이제 더이상 필요없게 되므로 삭제하여도 된다.
그럼, 실제로 os_task_create() 함수의 내용은 어떻게 생겼는지 알아보도록 하겠다.
7번줄은 새로 생성되는 타스크를 관리하기 위한 TCB 구조체를 heap 영역에 할당 받는 코드이다.
os_alloc() 함수는 os.h 파일에 다음과 같은 inline 함수로 만들어 두었다.
heap 메모리 영역에서 필요한 크기만큼 메모리를 할당 받은 다음 0으로 초기화 하기 위해서 calloc() 함수를 이용하였다. 지금은 디버깅을 위해서 printf()를 넣어 두었지만 실제 프로젝트에서는 삭제할 예정이다. 만약 메모리 할당에 실패하게 되면 다른 방법으로 에러 상황을 알려주면 좋을것 같다.
calloc을 사용하기 위하여 링커 스크립트 파일을 조금 수정해 주어야 한다.
heap 메모리를 사용하려면 heap의 시작 주소와 끝 주소를 알려주어야 한다.
__heap_start는 .bss 섹션 다음부터 시작되도록 하고, __heap_end는 SRAM의 마지막 주소를 알려주도록 하였다.
다시 os_task_create()로 돌아와서, 소스코드의 9~10은 입력으로 들어온 entry와 data값을 TCB구조체에 저장해 두는 코드이다.
11번줄은 입력으로 들어온 스택 크기만큼 메모리에서 할당 받아 TCB구조체에 알려주는 동작을 수행하는 코드이다.
attach_stack()은 다음과 같다.
입력으로 들어온 stack_size가 커널에서 요구하는 최소 스택 크기보다 작은 값이 들어오면 최소 스택 크기만큼은 스택을 할당할 수 있도록 해준다.
역시 os_alloc()을 이용해서 필요한 크기만큼 메모리를 할당 받고,
할당 받은 주소를 stack_base에 기록하고, 할당받은 메모리의 마지막 주소값을 stack_ptr에 기록해 둔다.
os_task_create()의 13~14는 입력으로 들어온 우선 순위와 타스크의 이름을 구조체에 저장해 두는 코드이다.
16번줄은 생성된 타스크의 초기 context정보를 설정하는 코드이다.
ARCH_TCB_INIT_CONTEXT()는 arch.h 파일에 선언된 매크로 코드이다.
1~10번에 나와있는 context 구조체가 실제로 스택 메모리에 저장되는 context의 내용과 순서이다.
12~27은 context의 내용을 차례로 초기화 하는 코드이다.
참고로 MACRO_START와 MACRO_END는 os.h 에 다음과 같이 정의 되어 있다.
이런 코드를 집어 넣는 이유는 두줄 이상의 매크로를 만들때 코드상의 오류를 방지하기 위해서이다. 왜 그런지는 인터넷에서 찾아보기 바란다.
context의 초기값은 다음과 같다.
r로 시작되는 context는 레지스터 번호에 해당되며, 초기값도 레지스터 번호를 넣어주도록 하였다. 이렇게 하는 이유는 디버깅을 위해서이다.
arg[]에는 타스크의 진입 함수의 인자로 들어가는 값을 넣어준다.
sreg에는 AVR의 SREG값의 초기값을 넣어준다. 0x80을 넣어주도록 하였는데, SREG 설명을 보면 알겠지만 master interrupt를 enable하도록 하는 동작을 수행한다.
pc[]에는 타스크의 진입 함수의 주소값을 넣어준다.
26번줄은 TCB의 stack_ptr 필드에 context 구조체 크기 만큼 위치를 옮겨 주는 역할을 한다.
스케줄러에 의해서 스택에 저장된 context를 CPU로 복사할때 "pop" 명령을 수행하기 때문에 stack_ptr값을 옮겨주어야 한다.
context 초기화가 끝난 스택 메모리의 모습을 그림으로 표현하면 다음과 같이 된다.
AVR의 레지스터는 r0~r31까지 있음에도 불구하고 모든 레지스터를 스택에 저장하지 않는 이유는 EABI 글을 참고하기 바란다.
os_task_create()의 18번줄은 TCB 초기화와 context 초기화를 모두 끝낸다음 만들어진 TCB를 return 해주는 코드이다.
'심화' 카테고리의 다른 글
megaOS - 7. Context(Task) Switching (0) | 2016.10.27 |
---|---|
megaOS - 6. Context load (0) | 2016.10.24 |
megaOS - 4. Scheduler(스케줄러) (0) | 2016.10.22 |
megaOS - 3. Double linked list (0) | 2016.10.20 |
megaOS - 2. 초기화 (3) | 2016.10.18 |