μC/OS-III ch.5 Task Management
서론1
real-time application 설계 과정은 일반적으로 수행할 작업을 각각 일부를 담당하는 작은 task로 나누는 것을 포함한다. μC/OS-III는 응용 프로그램 프로그래머가 이 패러다임을 쉽게 쓸 수 있도록 해준다. task(thread라고 하기도 함)은 CPU를 다 가지고 있다고 생각하는 간단한 프로그램이다. 한 CPU에서는 한번에 하나의 작업만 실행할 수 있다.
μC/OS-III는 multitasking을 지원하며 응용 프로그램이 원하는 수의 작업을 수행할 수 있게 해준다. 실제로 최대 작업 수는 프로세서가 사용할 수 있는 메모리 용량(코드와 데이터 공간 모두)에 의해서만 제한된다. multitasking은 CPU를 여러 작업 사이에 스케줄링하고 전환하는 과정이다(이것은 나중에 확장될 것이다). CPU는 여러 개의 순차적 작업 간에 주의를 전환한다. 멀티태스킹은 CPU가 여러 개인 것 같은 착각을 불러일으키며 실제로 CPU 사용을 극대화한다. 멀티태스킹은 modular application을 생성하는 데에도 도움이 된다. 멀티태스킹의 가장 중요한 측면 중 하나는 응용 프로그램 프로그래머가 real-time application에 내재된 복잡성을 관리할 수 있게 해준다는 것이다. 일반적으로 멀티태스킹을 사용할 경우 응용프로그램의 설계 및 유지보수가 용이하다.
task는 입력처리, 출력, 계산 수행, 루프 제어, 하나 이상의 디스플레이 업데이트, 버튼 및 키보드 읽기, 다른 시스템과 통신 등에 사용된다. 어떤 application은 몇개의 task만 포함할 수 있지만, 다른 application은 수백개의 task가 필요할 수 있다. 또한 어떤 작업은 작업이 몇 마이크로초만에 끝날 수도 있고, 수십 밀리초의 시간이 필요할 수 있다.
작업은 작은 차이점을 제외하고 다른 C 함수처럼 보인다. 작업은 한번실행되는 것(L5-1)과 무한 루프(L5-2)의 두가지 유형이 있다. 대부분의 임베디드 시스템에서 task는 일반적으로 무한 루프입니다. 또한 다른 C함수처럼 return이 있는 작업은 허용되지 않는다. 작업이 정규 C 함수이기 때문에 로컬 변수를 선언할 수 있다.
μC/OS-III 작업이 실행되기 시작하면, p_arg라는 argument를 하나 받는다. p_arg는 void*이다. p_arg를 사용하면 동일한 코드를 사용하고 런타임 특성이 다른 여러 작업을 만들 수 있다. 예를 들어, 각 task에 의해 관리되는 4개의 비동기 직렬 포트가 있을 수 있다. 작업코드는 동일하나, 매개변수(baud rate, I/O포트주소, 인터럽트 벡터 번호 등)를 받아 각기 다르게 실행된다. 즉 동일한 작업 코드를 4번 인스턴스화 하고 각 인스턴스가 관리할 직렬 포트마다 다른 데이터를 전달 할 수 있다.
한번실행되는 task가 끝나면, OSTaskDel()을 호출하여 스스로 삭제해야합니다. 작업을 만들고 삭제하는데 필요한 오버헤드 때문에 임베디드시스템에서는 이런 작업은 많지 않다. task body에서, 원하는 작업 수행을 돕기 위해 μC/OS-III의 대부분의 함수를 호출할 수 있다.
void MyTask (void *p_arg)
{
OS_ERR err;
/* Local variables */
/* Do something with ‘p_arg’ */
/* Task initialization */
/* Task body ... do work! */
OSTaskDel((OS_TCB *)0, &err);
}
//L5-1 Run-To-Completion Task
이 글은 ‘uC/OS-III: The Real-Time Kernel For the STM32 ARM Cortex-M3, Jean J. Labrosse, Micrium, 2009’를 번역한 글입니다. 오역이 있을 수 있으며, 발견하시면 github에 issue나 댓글 남겨주시기 바랍니다.
μC/OS-III의 경우, task에서 C 함수나 어셈블리어 함수를 호출할 수 있다. 실제로 함수들이 재진입되기만 하면 다른 task에서 동일한 C 함수를 호출할 수 있다. 재진입(reentrant) 함수는 정적 변수나 다중 액세스로부터 보호되지 않는 한 전역 변수를 사용하지 않는 함수이다(μC/OS-III는 이에 대한 메커니즘을 제공한다). 공유된 C 함수가 지역 변수만 쓴다면 일반적으로 reentrant한다고 한다(컴파일러가 재진입 코드를 생성한다고 가정).
재진입하지 않는 함수의 예로는 strtok()이 있다. 이 함수는 ASCII 문자열을 토큰(예를 들어 ‘ ‘, ‘,’ 등)으로 구분할 때 사용한다. 이 함수는 첫 번째 토큰을 찾자마자 토큰을 찾기 이전 문자열을 반환한다. 만약 한번더 호출하면 토큰을 찾은 이후 부터 다음 토큰 이전까지 인덱스의 문자열을 반환한다. 즉 reentrant 하지 않다.(https://blockdmask.tistory.com/382 참조)
무한 루프를 사용하는 것은 임베디드 시스템에서 흔한데, 이는 시스템에서 반복적인 작업(입력 읽기 ,디스플레이 업데이트 등)을 많이 수행하기 때문이다. 이것은 일반적인 C함수와는 다른 task를 만든다. 무한 루프를 구현하기 위해 ‘while(1)’이나 ‘ ‘for(;;)’ 을 사용할 수 있다. Micrium에서는 ‘while(DEF_ON)’을 사용한다.
무한 루프는 이벤트 발생을 기다리게 만드는 μC/OS-III 서비스를 호출해야한다. 각 task는 어떤 이벤트가 발생하기를 기다리는 것이 중요하다. 그렇지 않으면, 작업은 진정한 무한 루프가 되고, 우선순위가 낮은 작업이 실행되기 어려워진다.
void MyTask (void *p_arg)
{
/* Local variables */
/* Do something with “p_arg” */
/* Task initialization */
while (DEF_ON) { /* Task body, as an infinite loop. */
:
/* Task body ... do work! */
:
/* Must call one of the following services: */
/* OSFlagPend() */
/* OSMutexPend() */
/* OSPendMulti() */
/* OSQPend() */
/* OSSemPend() */
/* OSTimeDly() */
/* OSTimeDlyHMSM() */
/* OSTaskQPend() */
/* OSTaskSemPend() */
/* OSTaskSuspend() (Suspend self) */
/* OSTaskDel() (Delete self) */
:
/* Task body ... do work! */
:
}
}
//L5-2 Infinite Loop task
task가 기다리는 이벤트는 단순히 일정 시간을 기다리는 것일 수 있다(OSTimeDly() 또는 OSTimeDlyHMSM()을 호출하였을 때). 예를 들어, 키보드를 100 밀리초마다 스캔하도록 설계해야할 수도 있다. 이 경우, task를 100밀리초 지연시킨 다음 키보드에서 키가 눌렸는지 확인하고, 어떤 키를 눌렀는지에 따라 작업을 수행할 수 있다. 그러나 일반적으로 키보드 스캔 작업은 눌린 키에 따라 고유한 식별자를 버퍼에 넣고 다른 작업을 사용하여 각 고유한식별자에 따라 수행할 작업을 결정해야한다.
마찬가지로 task가 기다리는 이벤트는 이더넷 컨트롤러로부터 패킷이 도착하는 것일 수 있다. 이 경우 task는 OS???Pend()중 하나를 호출한다(Pend는 Wait와 같은 말이다). task는 패킷이 도착할때 까지 아무것도 하지 않는다. 패킷이 도착하면 task는 패킷의 내용을 처리하고 네트워크 스택을 따라 패킷을 이동시킬 수 있다.
task가 이벤트를 기다릴때, CPU 시간을 소비하지 않는다는 것을 기억하는 것이 중요하다.
μC/OS-III 가 해당 task를 알고 있는 상태에서 task가 만들어져야한다. 3장에서 살펴보았듯이, 단순히 OSTaskCreate()를 호출하면 작업을 생성할 수 있다. OSTaskCreate()의 함수 프로토타입은 다음과 같다.
void OSTaskCreate (OS_TCB *p_tcb,
OS_CHAR *p_name,
OS_TASK_PTR p_task,
void *p_arg,
OS_PRIO prio,
CPU_STK *p_stk_base,
CPU_STK_SIZE stk_limit,
CPU_STK_SIZE stk_size,
OS_MSG_QTY q_size,
OS_TICK time_slice,
void *p_ext,
OS_OPT opt,
OS_ERR *p_err)
OSTaskCreate() 및 arguments에 대한 정보는 “μC/OS-III API Reference”(443페이지)에 나와있다.
그림 5-1과 같이, task는 OSTaskCreate()에 의해 초기화된 TCB(Task Control Block), 스택, 우선순위 및 몇 가지 다른 파라미터를 할당해야 함을 이해하는 것이 중요하다.
F5-1(1)
OSTaskCreate()를 호출할 때, task에서 사용할 스택의 base address(p_stk_base), stack growth의 watermark limit(stk_limit), 그리고 stack의 크기(stk_size)를 전달한다. watermark limit과 stack의 크기는 CPU_STK단위로 저장된다.
F5-1(2)
OSTaskCreate()의 opt arguments를 OS_OPT_TASK_CHK + OS_OPT_TASK_STK_CLR로 지정하면 μC/OS-III는 모두 0으로 task의 스택을 초기화한다.
F5-1(3)
μC/OS-III는 스택의 상단부분에 CPU 레지스터의 복사본을 저장한다(ISR 시작시 저장된 것처럼 같은 stacking order로). 이를 통해 문맥 교환을 쉽게 수행할 수 있다. 여기서는 스택이 높은 주소에서 낮은 주소로 성장한다는 가정을 하지만, 스택을 역순으로 사용하는 CPU의 경우에도 동일한 개념이 적용된다.
F5-1(4)
stack pointer(SP)는 TCB에 저장된다. SP는 top-of-stack이라 불리기도 한다.
F5-1(5)
TCB의 나머지 필드(task 우선순위, task이름, task 상태, 내부 메시지 큐, 내부 semaphore 등)는 초기화된다.
서론2
다음으로, CPU 포트에 정의된 함수인 OSTaskCreateHook()을 호출한다(os_cpu_c.c 참조). OSTaskCreateHook()은 새로운 TCB의 포인터를 전달받고, 이 함수를 통해 사용자 또는 포트 설계자는 OSTaskCreate() 기능을 확장할 수 있다.
예를 들어 디버깅 목적으로 새로 생성된 TCB의 필드 내용을 단말기에 출력할 수 있다.
그런 다음 task가 Ready list에 배치되고(6장 ready list 참조) 마지막으로 멀티태스킹이 시작된 경우 μC/OS-III가 스케줄러를 호출하여 만들어진 작업이 가장 우선 순위가 높은 작업인지 확인하고, 만약 그렇다면 컨텍스트가 이 작업으로 교환된다(즉 새 작업으로 context-switch 된다).
task의 body부분은 μC/OS-III가 제공하는 다른 서비스를 호출할 수 있다. 구체적으로, task는 다른 task를 생성(OSTaskCreate())하고, 다른 task를 일시 중단 및 재개(OSTaskSuspend(), OSTaskResume())하고, 또는 다른 task에게 신호나 메시지를 전달(OS??Post())하거나, 자원을 다른 task와 공유하는 등의 작업을 수행할 수 있다. 즉 task가 이벤트 대기 함수 호출에만 국한되지는 않는다.
Figure 5-2는 task가 일반적으로 상호작용하는 자원을 보여준다.
F5-2(1)
task에서 중요한 것은 코드이다. 코드는 일반적으로 무한 루프로 구현되고, task가 무언가를 반환하지 않는다는 점을 제외하고는 다른 C함수처럼 보인다.
F5-2(2)
각 task에는 중요도에 따라 우선순위가 할당된다. μC/OS-III의 일은 CPU에서 실행할 작업을 결정하는 것이다. 일반적으로, 가장 중요한 Ready-to-run 작업(가장 높은 우선 순위)을 실행하는 것이다.
μC/OS-III의 경우, 낮은 우선순위 번호는 높은 우선순위를 나타낸다. 즉, 우선순위 1의 작업이 우선순위 10의 작업보다 더 중요하다.
μC/OS-III는 다양한 우선순위의 수를 지원한다(OS_cfg.h의 OS_PRIO_MAX 참조). 따라서 사용자가 애플리케이션이 사용할 수 있는 우선순위 레벨의 수를 결정할 수 있으며, 또한 μC/OS-III은 동일한 우선순위에서 무제한의 작업을 지원한다. 예를 들어, μC/OS-III는 64개의 다른 우선 순위 레벨을 갖도록 구성될 수 있고, 각 우선순위별로 수십 개의 작업을 할당할 수 있다(5-1 참조).
F5-2(3)
task는 자신의 CPU 레지스터 세트를 가지고 있다. task는 자신이 CPU를 독점하고 있다고 생각한다.
F5-2(4)
μC/OS-III는 preemptive kernel(우선순위가 높은 task는 우선순위가 낮은 task가 실행중일 때에도 CPU를 먼저 선점할 수 있음)이라서, 각 task는 stack area를 가져야한다. stack은 RAM에 저장되어있고, stack은 context, 지역 변수, 함수, task가 실행될 때 호출된 ISR(ISR nesting) 등을 저장한다.
스택 공간은 정적(컴파일 시간에) 또는 동적으로(실행시간에) 할당할 수 있다. 정적 스택 선언이 아래에 나와있고, 이 선언은 함수 외부에서 이루어진다.
static CPU_STK MyTaskStk[???];
//OR
CPU_STK MyTaskStk[???];
”???”는 스택의 크기가 task stack이 얼마나 필요한지에 따라 달라진다. 스택 공간은 아래와 같이 malloc()을 이용하여 동적으로 할당할 수 있다. 그러나 조각화에 주의를 기울여야한다. task를 생성하고 삭제할 경우 힙이 조각화되므로 task에 stack을 제공하지 못할 수 있다. 이러한 이유로 임베디드 시스템에서 동적으로 스택 공간을 할당하는 것은 일반적으로 허용되지만, 일단 할당된 스택은 해제되지 않아야한다. 즉, 할당 해제하지 않는 한, 힙으로부터 task의 stack을 생성하는 것도 괜찮다.
void SomeCode (void)
{
CPU_STK *p_stk;
:
:
p_stk = (CPU_STK *)malloc(stk_size);
if (p_stk != (CPU_STK *)0) {
Create the task and pass it “p_stk” as the base address of the stack;
}
:
:
}
(5-2 참조)
F5-2(5)
task는 전역변수에 접근할 수 있다. 그러나 μC/OS-III는 선점형(preemptive) 커널이고, 전역변수는 다수의 태스크들간에 공유될 수 있기 때문에 주의하여야한다.μC/OS-III은 그러한 공유 리소스들(세마포어, 뮤텍스 등)의 관리를 돕기 위한 메커니즘들을 제공한다.
F5-2(6)
task는 하나 이상의 I/O 디바이스(주변기기)에 접근할 수 있다. 보통 디바이스들을 관리하기 위해 task를 할당하는 것이 관례이다.
5-1 task 우선순위 지정
때때로 작업 우선순위는 명백하고 직관적이다. 예를 들어, 임베디드 시스템의 가장 중요한 측면이 제어를 수행하는 것이고,제어 알고리즘이 반응성이 있어야할 경우 높은 우선순위로 할당하는 것이 좋다(display나 operator interface task는 낮은 우선순위를 받는다). real-time 시스템의 복잡한 특성 때문에 우선순위가 변경불가능하지는 않다. 대부분의 시스템에서 모든 작업이 중요한 것으로 간주되지는 않으며 중요하지 않은 작업에는 낮은 우선순위가 부여된다.
Rate Monotonic Scheduling(RMS)라는 기술은 task가 얼마나 자주 실행되는지에 따라 task 우선순위를 부여한다. 간단히 말하면, 실행률이 가장 높은 task가 가장 높은 우선순위를 부여받는다. 여기서 RMS는 다음과 같은 여러가지 가정을 한다:
- 모든 작업은 주기적이다(일정한 간격으로 실행된다)
- task는 서로 동기화되지 않으며, 자원을 공유하거나 데이터를 공유하지 않는다.
- CPU는 ready-to-run 상태의 가장 높은 우선순위의 task를 실행해야한다. 즉 preemptive 스케줄링을 사용해야한다.
RMS에서 우선순위가 할당된 N개의 작업 집합이 주어졌을 때 기본 RMS 정리는 다음과 같은 부등식이 참일 경우 모든task에서 hard real-time deadline이 만족된다는 것을 의미한다.
\[\sum \frac{E_i}{T_i} \leq n(2^{\frac{1}{n}}-1)\]여기서 $E_i$는 task i의 최대 실행 시간이고, $T_i$는 task i의 실행 간격에 해당한다. 즉, $\frac{E_i}{T_i}$ 는 task i를 실행하는데 필요한 CPU시간의 분율을 나타낸다.
Table 5-1은 task 수 n을 기준으로 $n(2^{\frac{1}{n}}-1)$의 값을 보여준다. 무한한 수의 task일 경우 ln(2) 또는 0.693으로 주어지는데, 이는 RMS를 기준으로 모든 hard real-time deadline을 만족하려면 시간이 중요한 모든 task의 CPU사용률은 70%이하여야한다.
Number of Tasks | n(2^(1/n) - 1) |
---|---|
1 | 1.00 |
2 | 0.828 |
3 | 0.779 |
4 | 0.756 |
5 | 0.743 |
… | … |
infinite | 0.693 |
(Table 5-1: Allowable CPU usage based on number of tasks)
중요하지 않은 작업만을 수행할 때에는, CPU time의 100%에 가깝게 사용할 수 있다. 그러나 cpu time의 100%를 사용하는 것은 코드 변경이나 추가 기능을 허용하지 않으므로, 바람직하지 않다. 보통 경험으로, 항상 CPU시간의 60%~70% 미만의 CPU를 사용하도록 시스템을 설계해야 한다.
RMS는 가장 높은 비율의 task가 가장 높은 우선순위를 갖는다. 그러나 어떤 경우에는 가장 높은 비율의 task가 가장 중요한 task가 아닐 수 있다. 그러나 RMS는 흥미로운 알고리즘이다.
5-2 스택 크기 지정
stack의 크기는 응용 프로그램마다 다르다. 그러나 스택의 크기를 설정할 때는 task에 의해 호출된 모든 함수, task에 의해 호출된 모든 함수에 의해 할당될 로컬 변수의 수, 중첩 ISR에 대한 스택 요구사항을 고려해야한다. 또한 stack은 프로세서에 FPU(Floating-Point Unit)가 있는 경우 CPU 레지스터와 FPU 레지스터를 저장할 수 있어야한다. 또한 임베디드 시스템의 일반적인 규칙으로, 재귀적 코드를 쓰는 것은 피한다.
모든 함수 호출에 필요한 모든 메모리(반환 주소를 위해 각 함수 호출마다 1개의 포인터가 필요함)와 그 함수 호출에서 전달된 모든 인수에 필요한 모든 메모리, (CPU에 따라 다름) 전체 CPU context 저장, 그리고 (CPU가 ISR을 처리할 별도의 스택이 없다면) 각 중첩된 ISR에 필요한 다른 전체 CPU 컨텍스트, 그리고 그 ISR에 필요한 모든 스택 공간을 추가하여 필요한 스택 공간을 수동으로 계산할 수 있다. 이 모든 것을 더하는 것은 지루한 작업이며 그 결과로 나오는 숫자는 최소한의 요구 사항이다. 계산된 숫자에 아마도 1.5에서 2.0 정도의 안전 계수를 곱해야 할 것이다. 이 계산은 코드의 정확한 경로가 항상 알려져 있다고 가정하는데, 이것이 항상 가능한 것은 아니다. 특히 printf()나 다른 라이브러리 함수와 같은 함수를 호출할 때 printf()의 스택 공간이 얼마나 필요할지 추측하는 것은 어렵거나 거의 불가능할 수 있다. 이 경우 꽤 큰 스택 공간에서 시작하여 런타임에 스택 사용량을 모니터링하여 응용 프로그램이 잠시 실행된 후 실제로 얼마나 많은 스택 공간이 사용되는지 확인한다.
이 정보를 링크 맵으로 제공하는 정말 멋지고 영리한 컴파일러/링커들이 있다. 각 함수에 대해 링크 맵은 최악의 경우의 스택 사용량을 나타낸다. 이 기능을 통해 각 작업의 스택 사용량을 더 잘 평가할 수 있다. 그러나 여전히 전체 CPU 컨텍스트와 각 중첩된 ISR에 대한 다른 전체 CPU 컨텍스트, 그리고 해당 ISR에 필요한 스택 공간을 추가해야 한다.
제품을 개발하고 테스트하는 동안 항상 런타임 스택 사용량을 모니터링해야하는데, 스택오버플로우가 발생할 수 있기 때문이다.
5-3 stack overflow 감지하기
MMU 나 MPU 사용하기
스택 오버플로우는 프로세서에 MMU(Memory Management Unit)이나 MPU(Memory Protection Unit)이 있으면 쉽게 탐지될 수 있다. 기본적으로 MMU와 MPU는 CPU와 함께 통합된 특수 하드웨어 장치로, 코드, 데이터, 스택 등에서 유효하지 않은 메모리 위치에 엑세스하려고 할 때를 탐지할 수 있다.
CPU에서 stack overflow detection 사용하기
그러나 어떤 프로세서들은 스택 포인터 오버플로우 탐지 레지스터를 가지고 있다. CPU의 스택 포인터가 이 레지스터에 설정된 값 아래로 내려갈 때 예외가 생성되고, 예외 핸들러는 그 코드가 더이상 실행되지 않도록 한다(경고를 발생시키거나, 종료시킬 수 있음). OS_TCB의 .StkLimitPtr 필드(Task Control Blocks 참조)는 Fig 5-3과 같이 그러한 목적으로 제공된다. stack limit 위치는 일반적으로 예외를 처리할 수 있는 공간을 남겨두고 설정된다(CPU에 별도의 예외 스택이 없다고 가정). 대부분의 경우 위치는 &MyTaskStk[0]에 가까울 수 있다.
참고로, .StkLimitPtr의 위치는 task가 아래와 같이 생성될 때 OSTaskCreate()로 전달되는 stk_limit 인수에 의해 결정된다.
OS_TCB MyTaskTCB;
CPU_STK MyTaskStk[1000];
OSTaskCreate(&MyTaskTCB,
“MyTaskName”,
MyTask,
&MyTaskArg,
MyPrio,
&MyTaskStk[0], /* Stack base address */
100, /* Used to set .StkLimitPtr to trigger exception ... */
/* ... at stack usage > 90% */
1000, /* Total stack size (in CPU_STK elements) */
MyTaskQSize,
MyTaskTimeQuanta,
(void *)0,
MY_TASK_OPT,
&err);
물론, CPU의 스택 오버플로우 검출 하드웨어가 사용하는 .StkLimitPtr 값은 μC/OS-III가 문맥교환(context-switch)을 수행할 때마다 변경될 필요가 있다. 스택 오버플로우 탐지 레지스터의 값이 NULL을 먼저 가리키도록 한 다음, CPU의 스택 포인터를 변경하고 스택 오버플로우 탐지 레지스터의 값을 TCB의 .StkLimitPtr로 변경해야한다. 왜냐하면 NULL을 가리키도록 하지 않고, CPU의 스택 포인터를 변경하거나 스택 오버플로 탐지 레지스터를 변경하면 예외가 발생할 수 있기 때문이다. 이때 스택 오버플로우 탐지 레지스터를 NULL로 하여 이 문제를 해결할 수 있다.
소프트웨어기반 stack overflow detection
μC/OS-III가 한 태스크에서 다른 태스크로 전환될 때마다, 그것은 “Hook” 함수(OSTaskSwHook())를 호출하며, 이로써 μC/OS-III 포트 프로그래머가 문맥교환 함수의 기능을 확장할 수 있게 된다. 따라서 프로세서에 하드웨어 스택 포인터 오버플로 감지 기능이 없더라도 context switch hook 함수에 코드를 추가하여 이 기능을 “시뮬레이션”하고 소프트웨어에서 오버플로 감지를 수행할 수 있다. 특히 태스크가 전환되기 전에 CPU에 로드할 스택 포인터가 .StkLimitPtr에 있는 제한을 초과하지 않도록 코드가 보장해야 한다. 왜냐하면 소프트웨어 구현은 스택 포인터가 .StkLimitPtr 값을 초과하면 “곧” 스택 오버플로를 검출할 수 없다. 그림 5-4와 같이 스택 내에서 .StkLimitPtr 값을 &MyTaskStk[0]에서 상당히 멀리 위치시키는 것이 중요하다. 이와 같은 소프트웨어 구현은 하드웨어 기반 검출 메커니즘만큼 신뢰할 수는 없지만 발생 가능한 스택 오버플로를 방지한다. 물론 위와 같이 OSTaskCreate()를 사용하여 .StkLimitPtr 필드를 설정할 것이지만 이번에는 &MyTaskStk[0]에서 더 떨어진 위치로 설정한다.
counting the amount of free stack space
스택 오버플로를 확인하는 또 다른 방법은 사용될 것으로 예상되는 것보다 더 많은 스택 공간을 할당한 다음 모니터링하여 런타임에 실제 최대 스택 사용량을 표시하는 것이다. 이는 상당히 쉽게 수행할 수 있다. 우선 task stack은 task가 생성될 때 초기화(즉, 0으로 채워짐)해야 한다. 다음으로, 낮은 우선 순위의 작업은 0을 세면서 맨 아래(&MyTaskStk[0])에서 맨 위를 향해 생성된 각 작업의 스택을 검사한다. task가 stack에서 0이 아닌 값을 찾으면 프로세스가 중지되고 스택의 사용량이 (사용된 바이트 수 또는 백분율로) 계산될 수 있다. 그런 다음 (코드를 재컴파일하여) 더 합리적인 값을 할당(각 작업에 대한 스택 공간의 양을 늘리거나 줄이는)하도록 스택의 크기를 조정할 수 있다. 그러나 이를 효과적으로 수행하려면 task가 스택을 끝까지 셀수 있도록 충분히 오래 응용 프로그램을 실행해야 한다. 이는 그림 5-5에 나타나 있다. μC/OS-III는 런타임에 이 계산을 수행하는 함수인 OSTaskStkChk()를 제공하며 실제로 이 함수는 OS_StatTask()에 의해 호출되어 응용 프로그램에서 생성된 모든 작업에 대한 스택 사용량을 계산한다.
사용자는 컨텍스트 스택 포인터는 알고 있지만 다른 로컬 정보에 대해서는 알지 못한다. 따라서 스택 오버플로를 탐지하기 위해 사용 가능한 스택을 세야 한다.
5-4 Task Management Services
μC/OS-III는 애플리케이션에서 호출할 수 있는 수많은 태스크 관련 서비스를 제공한다. 이러한 서비스들은 os_task.c에 있으며 모두 OSTask????()로 시작한다. 이들이 수행하는 서비스의 종류는 태스크 관련 서비스를 그룹화할 수 있다.:
Group | Function |
---|---|
General | OSTaskCreate() |
OSTaskDel() | |
OSTaskChangePrio() | |
OSTaskRegSet() | |
OSTaskRegGet() | |
OSTaskSuspend() | |
OSTaskResume() | |
OSTaskTimeQuantaSet() | |
Signaling a Task (See Chapter 14, “Synchronization” on page 273) | OSTaskSemPend() |
OSTaskSemPost() | |
OSTaskSemPendAbort() | |
Sending Messages to a Task(See Chapter 15, “Message Passing” on page 309) | OSTaskQPend() |
OSTaskQPost() | |
OSTaskQPendAbort() | |
OSTaskQFlush() |
(Table 5-2: Task Management Services)
5-5 Task Management Internals
5-5-1 Task States1
μC/OS-III 사용자 관점에서는 Fig 5-6과 같이 다섯 가지 상태 중 어느 하나의 상태에 태스크가 있을 수 있다. 내부적으로는 μC/OS-III가 Dormant 상태를 계속 추적할 필요가 없고 다른 상태들은 약간 다르게 추적된다. Fig 5-6은 어떤 μC/OS-III 함수를 이용하여 한 상태에서 다른 상태로 이동하는지도 보여준다. 이 그림은 단순화된 그림이다.
F5-6(1)
Dormant state는 메모리에 있지만 μC/OS-III에서 사용할 수 없는 작업에 해당된다.
task를 생성하기 위한 함수인 OSTaskCreate()를 호출함으로써 μC/OS-III가 task를 사용할 수 있게 된다. 코드는 실제로 코드 공간에 존재하지만 μC/OS-III에게 이에 대한 정보를 제공해야 한다.
μC/OS-III가 더 이상 task를 관리할 필요가 없을 때 코드는 작업 삭제 함수인 OSTaskDel()을 호출할 수 있다. OSTaskDel()은 실제로 코드를 삭제하는 것이 아니라 CPU에 접근할 자격을 없애는 것이다.
F5-6(2)
task는 실행 준비가 되었을 때 Ready state이다. ready state인 task는 얼마든지 있을 수 있으며, μC/OS-III는 Ready list에서 모든 ready state인 task를 추적한다. 이 목록은 우선 순위에 따라 정렬된다.
F5-6(3)
가장 중요한 Ready-to-run 작업은 Running 상태에 놓이게 된다. 단일 CPU에서는 한 번에 하나의 작업만 실행할 수 있다.
CPU에서 실행하도록 선택된 작업은 응용 프로그램 코드가 OSStart()를 호출할 때 μC/OS-III에 의해 전환되거나, μC/OS-III가 OSIntExit() 또는 OS_TASK_SW()를 호출할 때 전환된다.
앞서 논의한 바와 같이 태스크는 이벤트가 발생하기를 기다려야 한다. 태스크는 태스크를 pending state로 가져오는 함수 중 하나를 호출하여 이벤트가 발생하지 않은 경우 이벤트를 기다린다.
F5-6(4)
pending state 의 task는 task가 기다리고 있는 이벤트와 연관된 pend-list(또는 wait-list)라고 불리는 특별한 리스트에 배치된다. 이벤트가 발생하기를 기다릴 때, task는 CPU시간을 소모하지 않는다. 이벤트가 발생하면, task는 ready list에 배치되고, μC/OS-III는 새로 ready list에 들어온 task가 가장 중요한 ready-to-run task인지 결정한다. 만약 가장 중요한 리스트라면 현재 실행 중인 테스크가 선점될 것이고, 새로 ready list에 들어온 task가 CPU에 대한 제어권을 갖는다.
OSTaskSuspend() 함수는 task를 무조건 멈추고, 이 task는 실제로 이벤트가 발생할 때까지 기다리지 않고 다른 작업이 OSTaskResume()을 호출할 때 까지 기다린다.
F5-6(5)
CPU 인터럽트가 enable 되어있다고 가정했을 때, 인터럽트 장치는 task의 실행을 중지하고 인터럽트 서비스 루틴(ISR)을 실행한다. ISR은 일반적으로 task가 대기하는 이벤트이다. 일반적으로 ISR은 단순히 이벤트가 발생했음을 태스크에게 알리고 해당 task가 해당 이벤트를 처리하도록 해야 한다. ISR은 가능한 한 짧아야 하며 인터럽트 장치를 처리하는 대부분의 작업은 μC/OS-III에서 관리할 수 있는 작업 수준에서 수행되어야 한다. ISR은 “Post” 호출(즉, OSFlagPost(), OSQPost(), OSSemPost(), OSTaskQPost(), OSTaskSemPost() 등)만 가능하다. ISR에서 허용되지 않는 유일한 포스트 콜은 OSMutexPost()뿐인데, 이는 나중에 다룰 것처럼 mutex가 작업 수준에서만 접근 가능한 서비스라고 가정하기 때문이다.
상태도에서 알 수 있듯이 인터럽트는 또 다른 인터럽트를 인터럽트할 수 있다. 이를 인터럽트 네스팅이라고 하며 대부분의 프로세서는 이를 허용한다. 그러나 인터럽트 네스팅은 적절하게 관리되지 않으면 쉽게 스택 오버플로를 초래한다.
5-5-1 Task States2
내부적으로 μC/OS-III는 그림 5-7에 나타낸 state machine을 사용하여 작업 상태를 추적한다. task state는 실제로 각 작업과 연관된 자료구조의 일부인 변수, 작업의 TCB에 유지된다. task state diagram은 μC/OS-III의 서비스 대부분을 구현할 때 μC/OS-III의 설계 전반에 걸쳐 참조되었다. 괄호 안의 숫자는 작업의 상태 번호이므로 작업은 8개의 상태 중 어느 하나에 있을 수 있다(OS.h, OS_TASK_STATE_?? 참조).
이 다이어그램 dormant task를 추적하지 않는다는 것에 유의한다. 왜냐하면 dormant task는 μC/OS-III가 모르기 때문이다.
이 state diagram은 여러 함수를 사용하는 방법과 task state에 미치는 영향을 이해하는 데 상당히 유용할 것이다.
F5-7(0)
task가 실행 준비(ready-to-run)가 되었을 때 태스크는 0 상태에 있다. 모든 task는 실행 준비가 되기를 “원하며” 그래야 자신의 임무를 수행할 수 있기 때문이다.
F5-7(1)
task는 OSTimeDly() 또는 OSTimeDlyHMSM()을 호출하여 시간이 만료될 때까지 기다릴 수 있다.. 시간이 만료되거나 지연이 취소되면(OSTimeDlyResume() 호출) task는 ready state로 복귀한다.
F5-7(2)
task는 pend(wait) 함수들 중 하나(OSFlagPend(), OSMutexPend(), OSQPend(), OSSemPend(), OSTaskQPend(), OSTaskSemPend()) 중 하나를 호출하여 이벤트가 발생하기를 기다리고, 이벤트가 발생하기를 영원히 기다리도록 지정할 수 있다. 이벤트가 발생할 때(즉 task 또는 ISR이 post를 수행), 대기하고 있는 객체(이벤트)가 삭제되거나, 다른 task가 pend를 중단하기로 결정할 때 pend는 종료된다.
F5-7(3)
task는 이벤트가 발생하기를 기다릴수 있지만, 얼마정도 기다릴지 지정한다. 이벤트가 그 시간 내에 post 되지 않으면, task는 준비상태가 되고, task는 타임아웃이 발생했음을 통지 받는다.
F5-7(4)
task는 OSTaskSuspend()를 호출하여 자신 또는 다른 task를 일시 중단할 수 있다. task가 실행을 재개하도록 허용되는 유일한 방법은 OSTaskResume()을 호출하는 것이다. task를 일시 중단한다는 것은 task가 재개될 때까지 CPU에서 task를 실행할 수 없음을 의미한다. task가 스스로 일시 중단되면 다른 task에 의해 다시 시작되어야 한다.
F5-7(5)
일정 시간을 기다리고 있는 task(delayed task)는 다른 task에 의해 suspend(일시 중단)될 수 있다. 이 경우, task가 실행될려면 지연이 완료되어야하고(또는 OSTimeDlyResume()으로 delay 중단) suspend 상태가 제거되어야한다(다른 task가 OSTaskResume()을 호출하여).
F5-7(6)
이벤트가 발생하기를 기다리는 task는 다른 task에 의해 중단될 수 있다. task가 실행될려면 이벤트가 발생하고, suspend가 제거되어야한다. 물론, task가 기다리고 있는 객체(이벤트)가 삭제되거나 다른 task에 의해 pend가 중단되면, 두 조건 중 하나가 제거된다. 그러나 suspend는 명시적으로 제거되어야한다.
F5-7(7)
task는 일정시간동안만 이벤트를 대기할 수 있다. task는 다른 task에 의해 suspend될 수 있다. suspend는 다른 task에 의해 제거되어야하며, 이벤트는 이벤트를 기다리는 동안 발생하거나 타임아웃될 필요가 있다.
5-5-2 Task Control Blocks(TCBs)
task control block(TCB)는 task에 대한 정보를 저장하기 위해 커널에 의해 사용되는 자료구조이다. 각각의 task는 자신만의 TCB가 필요하고, 사용자는 메모리공간(RAM)에 TCB를 할당한다. task의 TCB 주소는 task 관련 서비스(즉, OSTask???() 함수)를 호출할때 μC/OS-III에 제공된다. TCB 구조는 L5-3과 같이 os.h안에 선언되어있다. 필드 중 일부는 특정 기능이 요구되는지 여부에 따라 조건부로 컴파일된다.
또한, 애플리케이션 코드는 결코 이것들에 직접 액세스해서는 안 되며 특히 그것들을 변경하면 안 된다는 점에 유의해야 한다. 즉, OS_TCB 필드들은 오직 μC/OS-III에 의해서만 액세스되어야 한다.
struct os_tcb {
CPU_STK *StkPtr;
void *ExtPtr;
CPU_STK *StkLimitPtr;
OS_TCB *NextPtr;
OS_TCB *PrevPtr;
OS_TCB *TickNextPtr;
OS_TCB *TickPrevPtr;
OS_TICK_SPOKE *TickSpokePtr;
OS_CHAR *NamePtr;
CPU_STK *StkBasePtr;
OS_TASK_PTR TaskEntryAddr;
void *TaskEntryArg;
OS_PEND_DATA *PendDataTblPtr;
OS_STATE PendOn;
OS_STATUS PendStatus;
OS_STATE TaskState;
OS_PRIO Prio;
CPU_STK_SIZE StkSize;
OS_OPT Opt;
OS_OBJ_QTY PendDataEntries;
CPU_TS TS;
OS_SEM_CTR SemCtr;
OS_TICK TickCtrPrev;
OS_TICK TickCtrMatch;
OS_TICK TickRemain;
OS_TICK TimeQuanta;
OS_TICK TimeQuantaCtr;
void *MsgPtr;
OS_MSG_SIZE MsgSize;
OS_MSG_Q MsgQ;
CPU_TS MsgQPendTime;
CPU_TS MsgQPendTimeMax;
OS_REG RegTbl[OS_TASK_REG_TBL_SIZE];
OS_FLAGS FlagsPend;
OS_FLAGS FlagsRdy;
OS_OPT FlagsOpt;
OS_NESTING_CTR SuspendCtr;
OS_CPU_USAGE CPUUsage;
OS_CTX_SW_CTR CtxSwCtr;
CPU_TS CyclesDelta;
CPU_TS CyclesStart;
OS_CYCLES CyclesTotal;
OS_CYCLES CyclesTotalPrev;
CPU_TS SemPendTime;
CPU_TS SemPendTimeMax;
CPU_STK_SIZE StkUsed;
CPU_STK_SIZE StkFree;
CPU_TS IntDisTimeMax;
CPU SchedLockTimeMax;
OS_TCB DbgNextPtr;
OS_TCB DbgPrevPtr;
CPU_CHAR DbgNamePtr;
};
StkPtr
이 필드는 task에 대한 현재 스택 상단에 대한 포인터를 포함한다. StkPtr은 문맥교환을 위해 어셈블리 코드로 부터 엑세스되는 OS_TCB 자료구조의 유일한 필드여야한다. 따라서 이 필드는 어셈블리 코드로부터 엑세스를 더 쉽게하기 위해 첫번째 엔트리에 배치된다(즉 offset 0에 있다.).
StkLimitPtr
스택 증가에 제한을 두기 위해 task의 스택의 특정 위치의 포인터를 가지고 있으며, OSTaskCreate()로 부터 전달된 stk_limit 인수의 값으로부터 결정된다. 일부 프로세서는 스택이 오버플로우되지 않도록 하기 위해 런타임에 스택 포인터의 값을 자동으로 체크하는 특수 레지스터를 가지고 있다. StkLimitPtr은 문맥교환 중에 이 레지스터를 설정하는데 사용될 수 있다. 이러한 레지스터가 없는 경우 하드웨어만큼 신뢰할 수 없지만 소프트웨어로 시뮬레이션 할 수 있다. 이 기능을 사용하지 않을 경우 stk_limit 값을 0으로 설정하면 된다.
StkBasePtr
task stack의 base 주소를 가리킨다. stack의 base는 일반적으로 stack에서 가장 낮은 주소이다. task stack 은 보통 아래와 같이 선언된다.
CPU_STK MyTaskStk[???];
CPU_STK은 task stack을 선언하는데 사용하는 자료형이며 ???는 stack의 크기이다. base address는 항상
&MyTaskStk[0]
이다.
StkSize
task stack의 크기를 지정한다. 즉 CPU_STK의 개수를 지정한다. task stack이 선언되는 코드
CPU_STK MyTaskStk[???];
에서 StkSize는 위 배열의 크기를 지정한다.
StkUsed and StkFree
μC/OS-III는 (런타임시) task가 실제로 사용하는 스택 공간의 양과 남은 스택 공간의 양을 계산할 수 있다. 이는 OSTaskStkChk()라는 함수에 의해 이루어진다. 스택 사용 계산은 task가 생성될 때 stack이 초기화되었다고 가정한다. 즉, OSTaskCreate()를 호출할 때 OS_TASK_OPT_STK_CLR및 OS_TASK_STK_CHK라는 옵션이 지정될 것으로 예상한다. 이 옵션을 활성화하면 OSTaskCreate()는 task의 스택에 사용되는 메모리를 초기화한다.
μC/OS-III는 런타임에 각각의 task의 스택을 검사하는 OS_StatTask()라는 내부 task를 제공한다. OS_StatTask()는 애플리케이션 코드에 영향이 가지 않도록 일반적으로 낮은 우선순위로 실행된다. OS_StatTask()는 각 태스크에 대해 계산된 값을 각 task의 TCB의 StkUsed와 StkFree에 저장하는데, 이는 사용되는 최대 스택 바이트 수와 사용되지 않는 스택 공간의 양을 나타낸다. 이 필드들은 컴파일 시간에 통계 태스크가 활성화된 경우(OS_cfg.h에서 OS_CFG_STAT_TASK_STK_CHK_EN이 1로 설정됨)에만 존재한다.
CPUUsage
이 필드는 OS_cfg에서 OS_CFG_TASK_PROFILE_EN이 1로 설정된 경우 OS_StatTask()에 의해 계산된다. CPUUsage는 task의 CPU 사용량을 백분율(0 ~ 100%)로 표시한다. 버전 V3.03.00에서는 .CPUUsage에 100을 곱한다. 즉, 10000은 100.00%를 의미한다.
5-6 Internal tasks
초기화 동안 μC/OS-III는 최소 2개의 내부 태스크(OS_IdleTask(), OS_TickTask())와 3개의 선택 태스크(OS_StatTask(), OS_TmrTask() 및 OS_IntQTask())를 생성한다. 선택 태스크는 os_cfg.h에 있는 컴파일-타임 #defines 값을 기반으로 생성된다.
OS_CFG_STAT_TASK_EN enables OS_StatTask()
OS_CFG_TMR_EN enables OS_TmrTask()
OS_CFG_ISR_POST_DEFERRED_EN enables OS_IntQTask()
5-6-1 Idle task(OS_IdleTask(),os_core.c)
OS_IdleTask()는 μC/OS-III에 의해 처음 만들어지는 작업으로 μC/OS-III 기반 응용 프로그램에 항상 존재한다. idle task의 우선 순위는 항상 OS_CFG_PRIO_MAX-1로 설정된다. 사실 OS_IdleTask()는 이 우선 순위에 있도록 허용된 유일한 작업이며, 다른 작업이 생성될 때 OSTaskCreate()는 idle task와 같은 우선 순위에 생성된 다른 작업이 없도록 보장한다. idle task는 실행 준비가 된(ready-to-run) 다른 작업이 없을 때마다 실행된다. idle task에 대한 코드의 중요한 부분은 아래에 나와 있다(전체 코드는 os_core.c 참조).
void OS_IdleTask (void *p_arg)
{
while (DEF_ON) { (1)
OS_CRITICAL_ENTER();
OSIdleTaskCtr++; (2)
OSStatTaskCtr++;
OS_CRITICAL_EXIT();
OSIdleTaskHook(); (3)
}
}
L5-4(1)
idle task는 이벤트를 기다리는 함수를 호출하지 않는 진정한 무한루프이다. 대부분의 프로세서는 할일이 없을 때도 여전히 명령어를 실행하기 때문이다. μC/OS-III가 실행할 (idle task보다) 더 높은 우선순위의 task가 없다고 확인하면 idle task를 실행한다. μC/OS-III는 아무것도 하지 않는 빈 “무한 루프”를 갖는 대신, 유용한 일을 하기 위해 이 idle 시간을 활용한다.
L5-4(2)
idle task가 실행될 때마다 2개의 카운터 변수가 1씩 증가한다.
OSIdleTaskCtr은 32비트 부호없는 정수(unsigned integer)로 정의된다(os.h 참조). OSIdleTaskCtr은 μC/OS-III가 초기화될 때 한 번 초기화된다. OSIdleTaskCtr은 idle task가 얼마나 많이 실행되는지 나타내기 위해 사용된다. 만약 OSIdleTaskCtr을 모니터링하여 표시한다면 0x00000000에서 0xFFFFFFFF사이의 값을 보게 된다. OSIdleTaskCtr이 증가하는 속도는 CPU가 얼마나 바쁜지에 따라 달라진다. 증가 속도가 빠를 수록, CPU가 응용 프로그램에서 해야할 일이 적다는 의미이다.
OSStatTaskCtr은 32비트 부호없는 정수(unsigned integer, os.h 참조)로 정의되며, run time 시 CPU 사용량을 얻기 위해 통계 작업(statistic task)에서 사용된다.
L5-4(3)
루프를 돌때 마다 OS_IdleTask()는 OSIdleTaskHook()을 호출하는데, 이것은 프로세서가 사용하는 μC/OS-III 포트에 선언된 함수이다. OSIdleTaskHook()은 μC/OS-III 포트 프로그래머가 idle 동안 추가적인 처리를 수행할 수 있게 해준다. 이 때 이벤트를 기다리는 함수를 호출해서는 안된다.
OSIdleTaskHook()은 배터리로 구동되는 응용 프로그램들을 위해 CPU를 저전력 모드로 설정하는데 사용될 수 있으므로 에너지 낭비를 피할 수 있다. 그러나 이렇게 하는 것은 OSStatTaskCtr을 CPU 사용률을 측정하는 데 사용할 수 없음을 의미한다.
void OSIdleTaskHook (void)
{
/* Place the CPU in low power mode */
}
일반적으로 대부분의 프로세서는 인터럽트가 발생하면 저전력 모드를 종료한다. 그러나 프로세서에 따라 인터럽트 서비스 루틴(ISR)은 CPU를 최대 또는 원하는 속도로 되돌리기 위해 특별한 레지스터에 기록해야 할 수도 있다. ISR이 우선 순위가 높은 task(모든 작업이 idle task보다 우선 순위가 높음)를 깨우면 ISR은 중단된 idle task로 돌아가지 않고 대신 우선 순위가 높은 작업으로 전환(context switch)한다. 우선순위가 높은 task가 할일을 다 끝내고 이벤트를 기다리면, μC/OS-III는 OSIdleTaskHook()으로 전환(context-switch)한다. 이때 저전력모드 명령 이후 부터 실행되며, OSIdleTaskHook()은 종료되고 다시 OS_IdleTask()로 돌아가며, 다시 무한루프를 돌며 OSIldeTaskHook()를 호출하면 저전력모드로 들어간다.
5-6-2 Tick task(OS_TickTask(), os_tick.c)
대부분의 RTOS는 시간 지연과 타임아웃을 추적하기 위해 ‘Clock Tick’ 또는 ‘System Tick’이라고 불리는 주기적인 시간 소스가 필요하다. μC/OS-III의 클럭 틱 처리는 os_tick.c 파일에 캡슐화되어 있다.
OS_TickTask()는 μC/OS-III에 의해 생성된 태스크이며, 사용자는 μC/OS-III의 설정 파일 os_cfg_app.h를 통해 그 우선순위를 설정할 수 있다(참조 OS_CFG_TICK_TASK_PRIO). 일반적으로 OS_TickTask()는 상대적으로 높은 우선순위로 설정된다. 실제로 이 태스크의 우선순위는 가장 중요한 태스크보다 약간 낮게 설정된다.
μC/OS-III는 OS_TickTask()를 사용하여 시간이 만료되기를 기다리는 태스크를 추적하거나, 타임아웃을 가진 커널 객체에 대기 중인 태스크를 추적한다. OS_TickTask()는 주기적인 태스크이며, 그림 5-8에서 보여지는 것처럼 틱 ISR(175페이지의 “Interrupt Management” 장에서 설명됨)로부터의 신호를 기다린다.
F5-8(1)
하드웨어 타이머가 일반적으로 쓰이며, 10Hz와 1000Hz 사이의 주파수로 인터럽트를 생성하도록 설정된다(os_cfg_app.h의 OS_CFG_TICK_RATE 참조). 이 타이머는 일반적으로 Tick Timer라고 불린다. 사용될 주파수는 프로세서 속도, 원하는 시간 해상도, tick timer를 처리하는데 허용되는 오버헤드 등과 같은 요소에 따라 다르다.
틱 인터럽트는 타이머에 의해 생성될 필요는 없으며, 상당히 정확하다고 알려진 전원 주파수(50또는 60Hz)와 같은 정규 시간 소스에서 올 수 있다.
F5-8(2)
CPU 인터럽트가 활성화된 상태라면, CPU는 tick interrupt를 수락하고, 현재 task를 선점한 다음, tick ISR를 호출한다. tick ISR은 OSTimeTick()을 호출해야한다(os_time.c참조). 이 함수는 μC/OS-III에 필요한 대부분의 작업을 수행한다. 그 다음 tick ISR은 타이머 인터럽트를 클리어한다(그리고 다음 인터럽트를 위해 타이머를 다시 로드할 수도 있음). 그러나 아래와 같이 일부 타이머는 OSTimeTick()을 호출한 후가 아닌 그 전에 처리되어야할 수도 있다.
void TickISR (void)
{
OSTimeTick();
/* Clear tick interrupt source */
/* Reload the timer for the next interrupt */
}
또는
void TickISR (void)
{
/* Clear tick interrupt source */
/* Reload the timer for the next interrupt */
OSTimeTick();
}
OSTimeTick()의 맨 처음에 OSTimeTickHook()을 호출한다. 왜냐하면 포트 개발자가 틱 인터럽트가 서비스될 때 즉시 수행할 일을 만들 수 있게 하기 위함이다.
F5-8(3)
OSTimeTick()은 μC/OS-III에 의해 제공되는 서비스를 호출하여 tick task에 신호를 보내고 그 태스크를 ready-to-run 상태로 만든다. tick task는 가장 중요한 task가 되자마자 실행된다. tick task가 즉시 실행되지 않는 이유는 틱 인터럽트가 tick task보다 우선순위가 높은 task를 중단시켰을 수 있기 때문이며, tick ISR이 완료되면, μC/OS-III는 중단된 태스크를 다시 시작한다.
F5-8(4)
tick task가 실행될 때, 시간이 만료되기를 기다리거나 타임아웃이 있는 커널 객체에서 대기 중인 모든 task의 목록을 검토한다. 이것을 tick list라 하고, tick task는 시간이 만료되거나 타임아웃이 된 모든 task를 ready-to-run 상태로 만든다. 아래에서 더 자세히 설명된다.
5-6-2 Tick task(OS_TickTask(), os_tick.c)(2)
μC/OS-III는 수백 개의 task를 tick list에 넣을 수 있다. tick list는 tick list에 배치된 task의 시간이 만료되었는지 확인하는데 많은 CPU시간을 소비하지 않고, 그 task를 ready-to-run 상태로 만들 수 있도록 구현되어있다. tick list는 Fig 5-9에 표시된대로 구현된다.
F5-9(1)
tick list는 테이블(OSCfg_TickWheel[], os_cfg.app.c 참조)과 카운터(OSTickCtr)로 구현되어있다.
F5-9(2)
테이블은 최대 OS_CFG_TICK_WHEEl_SIZE 개의 항목을 포함할 수 있으며(os_cfg_app.h 참조) 이 값은 컴파일 시간에 결정되는 값이다. 항목의 수는 프로세서가 사용할 수 있는 메모리(RAM)의 양과 응용 프로그램의 최대 task 수에 따라 달라진다. OS_CFG_TICK_WHEEL_SIZE의 좋은 시작점은 (task의 수)/4일 수 있다. 그리고 tick 주파수의 짝수 배로 OS_CFG_TICK_WHEEL_SIZE를 설정하는 것은 권장되지 않는다. 예를 들어, tick rate가 1000Hz이고 응용 프로그램 내에 50개의 task가 있다면 OS_CFG-TICK_WHEEL_SIZE를 10 또는 20으로 설정하는 것을 피해야 한다. 대신 11 또는 23을 사용해야한다. 실제로 소수는 좋은 선택이다. 실행 시간에 어떤 일이 일어날지를 예상하는 것은 가능하지는 않지만, 이론적으로, 테이블의 각 항목에서 대기 중인 작업의 수는 균일하게 분포될 것이다.
F5-9(3)
각 항목은 .NbrEntriesMax, .NbrEntries, .FirstPtr로 구성된다.
.NbrEntries는 이 항목에 연결된 task의 수를 나타낸다.
.NbrEntriesMax는 이 항목에 연결되어있던 task 수의 최댓값을 나타낸다.
.FirstPtr은 이 항목에 연결된 task의 double linked list를 가라키는 포인터를 가지고 있다.
5-6-2 Tick task(OS_TickTask(), os_tick.c)(3)
OSTickCtr은 OS_TickTask()가 tick ISR로부터 신호를 받을 때 마다 OS_TickTask()에 의해 증가된다.
응용 프로그램 개발자가 OSTimeDly???()함수를 호출하거나, 0이 아닌 타임아웃 값과 함께 OS???Pend()를 호출할 때 task는 자동으로 tick list에 들어간다.
Example 5-1
tick list에 task를 넣는 과정을 설명하기 위한 예로, 틱 목록이 완전히 비어있고, OS_CFG_TICK_WHEEL_SIZE가 12로 설정되어있으며, OSTickCtr의 현재 값이 그림 5-10에 표시된 것처럼 10이라고 가정하자. OSTimeDly()가 호출될때 task는 tick list에 배치된다. OSTimeDly()가 다음과 같이 호출된다고 가정하자
:
OSTimeDly(1, OS_OPT_TIME_DLY, &err);
:
μC/OS-III reference 매뉴얼의 부록 A를 참조하면, 위 코드는 μC/OS-III에게 현재 task를 1틱 동안 지연시키라는 것을 알린다. OSTickCtr이 10의 값을 가지고 있으므로, task는 OSTickCtr이 11에 도달할 때까지 대기상태가 된다. task는 다음의 수식을 사용하여 OSCfg_TickWheel[] 테이블에 들어간다.
MatchValue = OSTickCtr + dly
Index into OSCfg_TickWheel[] = MatchValue % OS_CFG_TICK_WHEEL_SIZE
여기서 ‘dly’는 OSTimeDly()의 첫번째 argument이며 이 예제에서는 1이다. 따라서 예제에서는 다음과 같이 적용할 수 있다.
MatchValue = 10 + 1
Index into OSCfg_TickWheel[] = (10 + 1) % 12
or,
MatchValue = 11
Index into OSCfg_TickWheel[] = 11
table의 순환적 특성(table의 크기를 이용한 나머지 연산)때문에, 이 table은 tick wheel로 불리며, 각 항목은 휠의 spoke로 간주된다.
delay 중인 task의 OS_TCB는 OSCfg_TickWheel[]의 인덱스 11에 입력된다. task의 OS_TCB는 11번째 인덱스의 첫번째 항목으로 삽입된다(즉, OSCfg_TickWheel[11].FirstPtr가 가리키고 있음). 그리고 spoke 11의 항목의 개수가 1 증가한다(즉, OSCfg_TickWheel[11].NbrEntries가 1이 된다). 또한 OS_TCB는 &OSCfg_TickWheel[11]를 가리키며, MatchValue는 OS_TCB의 .TickCtrMatch에 저장된다. 또한 spoke 11에서 tick list에 삽입된 첫 번째 task이므로, task의 OS_TCB의 .TickNextPtr과 .TickPrevPtr 모두 NULL을 가리킨다.
task가 OSTimeDly()를 실행하면, task는 더이상 실행 대상이 아니기 때문에 μC/OS-III의 ready list에서 제거된다(141 페이지의 “The Ready List” 장에서 설명됨). 또한, μC/OS-III는 다음으로 중요한 ready 상태의 task를 실행해야 하므로 스케줄러가 호출된다.
만약 다음으로 실행될 태스크가 다음 틱이 도착하기 “전에” OSTimeDly()를 호출하게 되고 다음과 같이 OSTimeDly()를 호출한다면:
:
OSTimeDly(13, OS_OPT_TIME_DLY, &err);
:
μC/OS-III는 matchvalue와 spoke를 다음과같이 계산한다.
MatchValue = 10 + 13
OSCfg_TickWheel[] spoke number = (10 + 13) % 12
or,
MatchValue = 23
OSCfg_TickWheel[] spoke number = 11
2번째 task는 그림 5-11과 같이 같은 테이블 엔트리에 들어간다. 같은 spoke를 공유하고 있는 task는 오름차순으로 정렬되는데, 즉 시간이 가장 적게 남은 task가 리스트의 head에 위치하게 된다.
tick task가 실행될때(os_tick.c의 OS_TickTask()와 OS_TickListUpdate()참조), OSTickCtr을 증가시키기 시작하고 어떤 테이블 엔트리(즉, 어떤 spoke)를 처리해야하는지 결정한다. 그런 다음 그 엔트리의 list에 task가 있으면(즉, .FirstPtr이 NULL이 아니면) 각 OS_TCB는 .TickCtrmatch 값이 OSTickCtr과 일치하는지 확인하고, 일치하면 OS_TCB를 list에서 제거한다. task가 일정시간이 만료될때까지만 기다린다면 ready list에 배치된다(나중에 설명함). task가 어떤 object에 대기(pending)중이면 task는 tick list에서 제거되고, 그 객체를 기다리는 task의 list에서도 제거됩니다. list를 탐색하는 과정은 OSTickCtr이 task의 .TickCtrMatch 값과 일치하지 않는 순간 종료된다. 목록에서 더이상 찾을 필요가 없기 때문이다.
OS_TickTask()는 tick list를 업데이트할때 대부분의 작업을 임계구역(critical section)에서 수행한다. 그러나 목록이 정렬되어있기 때문에, 임계구역은 상당히 짧게 유지될 가능성이 높다.
The statistic task(OS_StatTask(), os_stat.c)
μC/OS-III는 전체 CPU 사용률(0.00%에서 100.00%까지), 태스크별 CPU 사용률(0.00%에서 100.00%까지), 그리고 task별 스택 사용량과 같은 런타임 통계를 제공하는 내부 task를 포함하고 있다. V3.03.00 버전부터 CPU 사용률은 0부터 10,000까지의 정수(0.00%에서 100.00%까지)로 표시된다. V3.03.00 이전에는 CPU 사용률이 0부터 100 사이의 정수로 표현되었다.
statistic task는 μC/OS-III 에서 선택사항이며, os_cfg.h에 정의된 컴파일 타임에 설정되는 상수 OS_CFG_STAT_TASK_EN이 1로 설정되면 빌드 시 이와 관련된 코드가 들어간다.
또한, 이 task의 우선순위 및 stack의 위치와 크기는 os_cfg_app.h에 선언된 OS_CFG_STAT_TASK_PRIO를 통해 설정할 수 있다.
응용 프로그램에서 statistic task를 사용하는 경우, L5-5에 표시된 것처럼 main() 함수에서 생성된 첫번째이자 유일한 응용 프로그램 task에서 OSStatTaskCPUUsageInit()을 호출해야한다. 시작코드는 OSStart()를 호출하기 전에 하나의 task만 생성해야한다. 물론, 생성된 단일 task는 OSStatTaskCPuUsageInit()을 호출한 후에 다른 task를 생성할 수 있다.
void main (void) (1)
{
OS_ERR err;
:
OSInit(&err); (2)
if (err != OS_ERR_NONE) {
/* Something wasn’t configured properly, μC/OS-III not properly initialized */
}
/* (3) Create ONE task (we’ll call it AppTaskStart() for sake of discussion) */
:
OSStart(&err); (4)
}
void AppTaskStart (void *p_arg)
{
OS_ERR err;
:
/* (5) Initialize the tick interrupt */
#if OS_CFG_STAT_TASK_EN > 0
OSStatTaskCPUUsageInit(&err); (6)
#endif
:
/* (7) Create other tasks */
while (DEF_ON) {
/* AppTaskStart() body */
}
}
//L5-5 Proper startup for computing CPU utilization
L5-5(1)
C 컴파일러는 대부분의 C 응용 프로그램에서 흔히 그렇듯이 CPU를 main() 함수로 이동시켜야 한다.
L5-5(2)
main()은 μC/OS-III를 초기화하기 위해 OSInit()을 호출한다. os_cfg.h에서 OS_CFG_STAT_TASK_EN을 1로 설정함으로써 statistic task가 활성화되었다고 가정한다. 호출이 제대로 수행되었는지 확인하기 위해 항상 μC/OS-III에서 반환한 오류 코드를 검사해야한다. 가능한 오류 목록, OS_ERR_???에 대해서는 os.h를 참조하면된다.
L5-5(3)
주석에 적혀있는 대로, 예제에서 AppTaskStart()라는 단일 태스크를 생성해야 한다.(이름은 작성자가 결정한다). 이 태스크를 생성할 때, 상당히 높은 우선순위를 부여해야한다(단, μC/OS-III를 위해 예약된 우선순위 0은 사용하면 안된다).
일반적으로 μC/OS-III는 사용자가 OSStart()를 호출하기 전에 필요한 만큼의 태스크를 생성할 수 있도록 허용한다. 그러나 통계 태스크를 전체 CPU 사용률을 계산하는 데 사용할 때는 하나의 태스크만 생성해야 한다.
L5-5(4)
μC/OS-III가 AppTaskStart()와 같은 최고 우선순위의 태스크를 시작하게 하려면 OSStart()를 호출해야한다. 이 시점에서 구성 옵션에 따라 네 개에서 여섯 개의 태스크가 생성되어야 한다: OS_IdleTask(), OS_TickTask(), OS_StatTask(), OS_TmrTask() (선택사항), OS_IntQTask() (선택사항) 그리고 AppTaskStart().
L5-5(5)
처음 시작되는 task는 tick interrupt를 구성하고 활성화해야한다. 보통 clock tick 에 사용되는 하드웨어 타이머를 초기화하고 os_cfg_app.h에 정의된 OS_CFG_TICK_RATE_HZ에 지정된 Hz로 인터럽트하도록 해야한다. 추가적으로, Micriμm은 기본 보드 지원 패키지(BSP)를 포함하는 샘플 프로젝트를 제공한다. BSP는 CPU의 많은 측면뿐만 아니라 μC/OS-III에 의해 필요로 하는 주기적인 시간 소스도 초기화한다. 사용 가능한 경우, 사용자는 시작 태스크에서 BSP_Init()을 호출하여 BSP 서비스를 사용할 수 있다. 이 시점 이후에는 사용자에 의한 추가 시간 소스 초기화가 필요하지 않다.
L5-5(6)
OSStatTaskCPUUsageInit()는 μC/OS-III 태스크를 제외한 다른 task가 시스템에서 실행되지 않을 때 1/OS_CFG_STAT_TASK_RATE_HZ 초 동안 OSStatTaskCtr (OS_IdleTask() 참조)가 셀 수 있는 최대 값을 확인하기 위해 호출된다. 예를 들어, 시스템에 응용 프로그램 task가 없고 OSStatTaskCtr이 1/OS_CFG_STAT_TASK_RATE_HZ 초 동안 0에서 10,000,000까지 카운트되고, 이 테스트는 1/OS_CFG_STAT_TASK_RATE_HZ 초마다 실행되며, 만약 다른 task가 추가되는 경우, OSStatTaskCtr은 10,000,000에 도달하지 않게 되고, 실제 CPU 사용률은 다음과 같이 결정된다(단위는 %이다).
\[CPU-Utilization = (100 - \frac{100 \times OSStatTaskCtr}{OSStatTaskCtrMax})\]예를 들어, OSStatTaskCtr이 7,500,000이라면 CPU의 25%정도를 응용프로그램 task를 실행하는데 쓰고 있는 것이다: \(25 = (100 - \frac{100 \times 7500000}{10000000})\)
L5-5(7)
AppTaskStart()는 필요한만큼 다른 응용 프로그램 task를 만든다.
이전에 설명한대로, μC/OS-III는 run-time 통계를 각 task의 OS_TCB에 저장한다.
OS_StatTask()는 또한 OSTaskStkChk() (os_task.c 참조)를 호출하여 모든 생성된 task의 스택 사용량을 계산하고 이 함수의 반환 값(사용되지 않는 스택 공간 및 사용된 스택 공간)을 task의 OS_TCB의 .StkFree 및 .StkUsed 필드에 각각 저장합니다.
The Timer Task (OS_TmrTask(), os_tmr.c)
μC/OS-III는 애플리케이션 프로그래머에게 타이머 서비스를 제공하며, 이 코드는 os_tmr.c에 있다.
timer task는 μC/OS-III 응용 프로그램에서 선택적(optional)이며, os_cfg.h에 정의된 컴파일 시간에 설정되는 상수인 OS_CFG_TMR_EN에 의해 제어된다. 구체적으로, OS_CFG_TMR_EN이 1로 설정되면 빌드 시 timer task 관련 코드가 포함된다.
timer는 카운터가 0에 도달했을때 어떤 일을 수행하는 카운트다운 카운터이다. ‘어떤 일’은 사용자가 callback 함수를 통해 제공한다. callback함수는 사용자가 선언하며, 타이머가 만료될 때 호출될 함수이다. 따라서 callback은 모터 등을 켜거나 끄는 등의 필요한 동작을 수행하는데 사용될 수 있다. callback함수는 timer task에서 호출된다는 것을 주의해야한다. 프로그래머는 무제한의 timer를 생성할 수 있다(RAM양으로는 제한). 타이머 관리는 213페이지의 Timer Management 장에서 설명되며, 프로그래머가 사용 가능한 타이머 서비스는 443페이지의 “μC/OS-III API Reference” 부록 A에서 설명된다.
OS_TmrTask()는 μC/OS-III에 의해 생성된 task이고(os_cfg.h에서 OS_CFG_TMR_EN을 1로 설정하는 것을 가정한다) 그 우선순위는 사용자가 os_cfg_app.h에서 찾을 수 있는 μC/OS-III의 설정 상수 OS_CFG_TMR_TASK_PRIO를 통해 설정할 수 있다. OS_TmrTask()는 일반적으로 중간 우선순위로 설정된다.
OS_TmrTask()는 clock tick을 생성하는데 사용된 것과 동일한 인터럽트 소스를 사용한는 주기적인 task이다. 그러나 timer는 일반적으로 더 느린 속도(일반적으로 10Hz)마다 업데이트된다. 간격은 소프트웨어에서 tick Hz를 timer Hz로 나누어 구한다. 예를 들어, tick rate가 1000Hz이고 원하는 timer rate가 10Hz이면, timer task는 Fig 5-12에 표시된대로 매 100번째 tick interrupt 마다 신호를 받는다.
5-6-5 The ISR Handler Task (OS_IntQTask(), os_int.c)
os_cfg.h에서 컴파일 시간 구성 상수 OS_CFG_ISR_POST_DEFERRED_EN을 1로 설정하면, μC/OS-III는 ISR로부터의 OS post service 요청에 대한 응답을 미루는 task (OS_IntQTask()라고 불린다)를 생성한다.
85 페이지의 “Critical Sections” 장에서 설명된 것처럼 μC/OS-III는 인터럽트를 비활성화/활성화하거나 스케줄러를 잠그거나/잠금 해제함으로써 임계 구역을 관리한다. 후자의 방법을 선택하는 경우 (즉, OS_CFG_ISR_POST_DEFERRED_EN을 1로 설정하는 경우), 인터럽트에서 호출되는 μC/OS-III “post” 함수는 ready list나 pend list 같은 내부 데이터 구조를 조작할 수 없다.
ISR이 μC/OS-III에 의해 제공된 “post” 함수 중 하나를 호출하면, post 데이터의 복사본과 원하는 목적지가 “holding” 큐에 배치된다. 모든 중첩된 ISR이 완료되면, μC/OS-III는 ISR handler task(OS_IntQTask())로 context switch를 수행하며, ISR handler task는 holding 큐에 배치된 정보를 적절한 task에게 다시 post한다. 이 추가 단계는 wait list에서 task를 제거하고, ready list에 넣는 등의 시간을 소모하는 동작을 수행하는데 필요할 수 있는 interrupt disable의 시간을 줄이기 위해 수행된다.
위 그림은 OS_CFG_ISR_POST_DEFERRED_EN이 1이 되었을 때 인터럽트를 처리하는 방식으로, ISR이 post함수를 호출하면(이때 post함수는 내부 데이터 구조를 조작할 수 없다.) 관련 정보가 queue에 배치되고, ISR handler task에게 넘긴다. ISR handler task는 queue에서 정보를 추출해 실제로 post call을 수행한다(ISR handler task는 우선순위가 0이므로, 자동으로 스케쥴러가 비활성화된다. 이때 post함수는 내부 데이터 구조를 변경할 수 있다).
만약 OS_CFG_ISR_POST_DEFERRED_EN이 0이면 ISR이 인터럽트를 비활성화하고, post call을 다 처리하게 된다.
OS_IntQTask()는 μC/OS-III에 의해 생성되며 항상 우선 순위 0(즉, 가장 높은 우선 순위)에서 실행된다. OS_CFG_ISR_POST_DEFERED_EN이 1로 설정되면 우선 순위 0을 사용하는 다른 task는 허용되지 않는다.
5-7 summary
task는 자신에게 모든 CPU가 있다고 생각하는 단순한 프로그램이다. 단일 CPU에서는 임의의 주어진 시간에 하나의 태스크만 실행된다. μC/OS-III는 멀티태스킹을 지원하며 응용 프로그램이 임의의 수의 task를 가지도록 허용한다. 최대 task 수는 실제로 프로세서가 사용할 수 있는 메모리 양(코드와 데이터 공간 모두)에 의해서만 제한된다.
task는 1번 실행되고 삭제되는 run-to-completion 이거나 무한 루프(이벤트가 발생할 때 까지 기다렸다가 해당 이벤트를 처리함)로 설계된다.
task를 생성할 때, task에 의해 사용될 OS_TCB의 주소, task의 우선순위, task의 스택을 위한 RAM 영역 및 몇 개의 파라미터를 지정할 필요가 있다. 태스크는 계산들(CPU bound task)을 수행하거나, 하나 이상의 I/O(Input/Output) 디바이스들을 관리할 수 있다.
μC/OS-III는 idle task, tick task, ISR handler task, statistic task, timer task 등 최대 5개의 internal task를 생성한다. idle task와 tick task는 항상 생성되지만, statistic task, timer task, ISR handler task는 옵션이다.
Reference
-
uC/OS-III: The Real-Time Kernel For the STM32 ARM Cortex-M3, Jean J. Labrosse, Micrium, 2009
-
https://blockdmask.tistory.com/382
-
https://d2.naver.com/helloworld/47667
댓글남기기