Branch Log · Open in interactive viewer →

Chapter 03 Kernel Structure


3.6 Task Level Context Switching: OS_Sched()

task 수준에서 context switching을 수행하는 OS_Sched() 함수를 살펴보자. 다음은 OS_Sched()에서 활용되는 주요 변수와 함수이다.

주요 변수/함수 설명 특이사항
OSPrioHighRdy 가장 높은 우선순위 OSRdyGrp, OSRdyTbl을 활용하여 계산
OSTCBHighRdy 가장 높은 우선순위 task의 TCB OSTCBPrioTbl 통해 획득
OSTCBCur 현재 실행중인 task의 TCB
OSIntNesting 인터럽트 중첩 수준 인터럽트 시 +1 증가하며, 인터럽트 중에는 context switching이 발생하지 않음
OSLockNesting scheduler lock 중첩 수준 특정 함수를 임의로 호출하면 scheduler를 잠금할 수 있음(0~255)
OSCtxSwCtr #context switching 통계 목적
OS_TASK_SW() context switching 수행 소프트웨어 인터럽트(SWI)를 발생시키는 매크로

단, 다음 조건에서는 context switching이 발생하지 않는다.

void OS_Sched(void)
{
  #if OS_CRITICAL_METHOD == 3
    OS_CPU_SR    cpu_sr;
  #endif
    INT8U  y;

    OS_ENTER_CRITICAL();

    /* if all ISRs done & not locked */
    if ((OSIntNesting == 0) && (OSLockNesting == 0)) {
      y = OSUnMapTbl[OSRdyGrp];
      OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]);
      if (OSPrioHighRdy != OSPrioCur) {
        OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
        OSCtxSwCtr++;
        OS_TASK_SW();
      }
    }
    OS_EXIT_CRITICAL();
}

3.6.1 OS_TASK_SW()

OS_TASK_SW()는 우선순위가 낮은 task에서, 우선순위가 가장 높은 task로 전환할 때 호출되는 매크로이다. 다음 7개 레지스터를 갖는 간단한 CPU에서 과정을 살펴보자.

참고로, uC/OS-II는 인터럽트 레벨에서 context switching을 수행하도록 설계되었고, 따라서, OS_TASK_SW()를 통해 소프트웨어 인터럽트를 발생시킨다.(이후, SWI handler가 context switching을 수행한다.)

다음은 OS_TASK_SW() 호출 직전, 프로세서의 변수 및 자료 구조를 나타낸 그림이다.

OS_TASK_SW 1

다음은 context switching 과정을 나타낸 도식이다.

$\rightarrow$ 기존 task의 context를 저장 $\rightarrow$ 새롭게 선점한 task를 재개
OS_TASK_SW 2 OS_TASK_SW 3
(1) PC, PSW를 stack으로 push
(hw에서 수행)
(1) OSTCBCur = OSTCBHighRdy,
OSPrioCur = OSPrioHighRdy
(2) R1, R2, R3, R4를 stack으로 push (2) SP를 최고 우선순위 task의 stack pointer로 설정
(3) 현재 SPOSTCBStkPtr 변수에 저장 (3) 해당 stack에서 범용 레지스터 pop
- (4) 해당 stack에서 PC, PSW pop
(return 시 hw에서 수행)

다음은 SWI handler의 context switching 과정을 나타낸 의사 코드이다. CPU 레지스터를 조작할 필요가 있으므로, 실제로는 어셈블리 언어로 작성된다.

// SWI handler
void OSCtxSw(void)
{
  PUSH R1, R2, R3 and R4 onto the current stack;
  OSTCBCur -> OSTCBStkPtr = SP;
  OSTCBCur = OSTCBHighRdy;
  SP = OSTCBHighRdy -> OSTCBStkPtr;
  POP R1, R2, R3 and R4 from the new stack;
  Execute a return from interrupt instruction;     // POP PSW, POP PC
}

define OS_TASK_SW() asm("swi 2")

"swi 2": PUSH PC, PUSH PSW


3.7 Lock/Unlocking Scheduler: OSSchedLock(), OSSchedUnlock()

필요에 따라 현재 task가 높은 우선순위 task에 의해 선점되지 않도록, OSSchedLock(), OSSchedUnlock() 함수를 사용해 스케줄링을 잠글 수 있다.

예를 들어, 낮은 우선순위 task가, 메시지를 여러 메일 박스/큐/세마포어에 보내야 하는 경우

이때, 인터럽트는 여전히 발생하므로 주의해야 한다. 또한, 현재 task 수행을 지연시키는 시스템 호출(OSFlagPend(), OSMboxPend(), OSTimeDly() 등)은 사용하지 않아야 한다.

void OSSchedLock(void)
{
  #if OS_CRITICAL_METHOD == 3
    OS_CPU_SR cpu_sr;
  #endif

  if (OSRunning == TRUE) {
    OS_ENTER_CRITICAL();
    if (OSLockNesting < 255) {
      OSLockNesting++;
    }
    OS_EXIT_CRITICAL();
  }
}
void OSSchedUnlock(void)
{
  #if OS_CRITICAL_METHOD == 3
    OS_CPU_SR cpu_sr;
  #endif

  if (OSRunning == TRUE) {
    OS_ENTER_CRITICAL();
    if (OSLockNesting > 0) {
      OSLockNesting++;
      if ((OSIntNesting == 0) && (OSLockNesting == 1)) {
        OS_EXIT_CRITICAL();
        OS_Sched();
      } else {
        OS_EXIT_CRITICAL();
      }
    } else {
      OS_EXIT_CRITICAL();
    }
  }
}

3.8 Idle Task: OS_TaskIdle()

어떠한 task도 Task Ready 상태가 아닐 경우, uC/OS-II에서는 Idle Task를 수행한다. 참고로, idle task는 가장 낮은 우선순위(OS_LOWEST_PRIO)를 가진다.

참고로, 무한 루프를 수행하며 OSIdleCtr 변수(32 bit counter)를 증가시키는데, 이를 기반으로 CPU 사용률(OSCPUUsage)을 측정하는 데 사용할 수 있다.

OSTaskIdleHook(): 껍데기만 있는 사용자 함수로, idle task에서 할 작업을 사용자가 정의할 수 있다.

void OS_TaskIdle(void *pdata)
{
  #if OS_CRITICAL_METHOD == 3
    OS_CPU_SR cpu_sr;
  #endif

  pdata = pdata;  // Prevent compiler warning
  for (;;) {
    OS_ENTER_CRITICAL();
    OSIdleCtr++;
    OS_EXIT_CRITICAL();
    OSTakeIdleHook();
  }
}

3.9 Statistics Task: OS_TaskStat()

uC/OS-II에서는 OS_TaskStat()을 매초 실행하여 CPU 사용률을 측정할 수 있다.


3.9.1 Initializing Statistics Task

OSStatInit() 함수에서 Idle Task에서 카운터가 얼마나 증가하는지 측정한다.

단, OSStart() 이전, 처음으로 생성된 task에서 호출되어야 한다.

(세부 설명 생략)

void main (void)
{
  OSInit();                     /* initialize uC/OS-II */
  OSTaskCreate(TaskStart, (void *)0, TASK_STK_SIZE, 10);    
  OSStart();                    /* start multitasking */
}

void TaskStart (void *pdata)
{
  OSStatInit();           /* initialize statistics task */

  for (;;) {
    /* code for TaskStart */
  }
}

다음은 OSStatInit() 함수이다.

void OSSStatInit(void)
{
  #if OS_CRITICAL_METHOD == 3
    OS_CPU_SR cpu_sr;
  #endif

    OSTimeDly(2);   /* Synchronize with clock tick */
    OS_ENTER_CRITICAL();
    OSIdleCtr = 0L; /* clear idle counter */
    OS_EXIT_CRITICAL();

    OSTimeDly(OS_TICKS_PER_SEC);
    OS_ENTER_CRITICAL();
    OSIdleCtrMax = OSIdleCtr;
    OSStatRdy = TRUE;
    OS_EXIT_CRITICAL();
}

3.10 Interrupt Level Context Switching: OSIntExit()

inline assembly를 지원하는 C 컴파일러를 사용할 경우, C 소스 코드에서 ISR을 작성할 수 있다.

본래 uC/OS-II에서, ISR은 어셈블리로 작성해야 한다.

주요 함수 설명 특이사항
OSIntEnter() 인터럽트 진입 시점에서 호출 ISR에 진입
OSIntExit() 인터럽트 종료 시점에서 호출 필요에 따라 context switching 위한 OSIntCtxSw() 호출
OSIntCtxSw() 종료 시점에서 context switching ISR 진입 과정에서 이미 push를 수행했으므로, pop만 필요.

다음은 인터럽트 레벨에서 context switching을 수행하는 의사 코드이다.

YourISR:
    Save all CPU registers;
    Call OSIntEnter(); or, increment OSIIntNesting;
    if (OSIntNesting == 1) {
      OSTCBCur -> OSTCBStkPtr = SP;
    }
    Clear interrupting device;
    Re-enable interrupts; (optional)
    Execute user code to service ISR; (nested interrupts가 필요할 경우)
    Call OSIntExit();
    Restore all CPU registers;
    Execute return from interrupt instruction;

다음은 인터럽트 서비스가 수행되는 과정을 나타낸 도식이다.

interrupt service

순서 설명
(1) 인터럽스 요청을 보냈으나, 인터럽트가 비활성화(혹은 task가 마무리되지 않아서) CPU에서 인식되지 않음
(2) & (3) CPU가 인터럽트를 인식하고, CPU 벡터가 ISR에 전달됨
(4) ISR이 CPU context를 저장한다.
(5) (4)가 끝나면 OSIntEnter()(혹은 OSIntNesting++)에 알리고, 현재 task의 stack pointer를 해당 OS_TCB에 저장한다.
(6) ISR 코드가 실행된다.
(7) ISR이 끝나면 OSIntExit()을 호출한다.
(8) & (9) CPU 레지스터가 복구되고, task로 return한다.
(10) 단, ISR이 높은 순위 task를 실행하도록 설정되어 있다면, context switching이 발생한다.
(11), (12) 새롭게 선점된 task의 레지스터를 복구하고, 해당 task로 return한다.

다음은 OSIntEnter()를 정의한 코드이다.

void  OSIntEnter (void)
{
    if (OSRunning == TRUE) {
        if (OSIntNesting < 255u) {
            OSIntNesting++;                        
        }
    }
}

3.11 Tick ISR: OSTimeTick()

uC/OS-II에서는 timeout이나 timedly 등 기능이 작동하도록, clock tick을 사용한다.

발생 주기가 짧을수록 overhead 증가하며, 실제 빈도는 application에서 얼마 만큼의 시간 정밀도가 필요한지 여부에 따라 달라진다.

clock tick interrupt 발생 시, OSTimeTick() 함수를 호출하여 모든 task delay를 감소시킨다. 0까지 감소할 경우, 해당 task는 Task Ready 상태로 전환된다.

void OSTimeTick (void)
{
  #if OS_CRITICAL_METHOD == 3
    OS_CPU_SR cpu_sr;
  #endif

  OS_TCB *ptcb;
  #if OS_TIME_GET_SET_EN > 0
    OS_ENTER_CRITICAL();
    OSTime++;
    OS_EXIT_CRITICAL();
  #endif
  if (OSRunning == TRUE) {
    ptcb = OSTCBList;
    while (ptcb -> OSTCBPrio != OS_IDLE_PRIO) {
      OS_ENTER_CRITICAL();
      if (ptcb->OSTCBDlt != 0) {
        if (--ptcb->OSTCBDly == 0) {
          if ((ptcb->OSTCBStat & OS_STAT_SUSPEND) == 0) {
            OSRdyGrp |= ptcb->OSTCBBitY;
            OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX;
          } else {
            ptcb->OSTCBDly = 1;    // 루프를 돌며 계속 점검할 수 있도록, 1로 설정
          }
        }
      }
      ptcb = ptcb->OSTCBNext;
      OS_EXIT_CRITICAL();
    }
  }
}

OS_STAT_SUSPEND: task가 suspend된 상태인지 여부를 나타내는 flag

while문 내부에 추가로 OS_ENTER_CRITICAL(), EXIT_CRITICAL()를 삽입한 이유는, interrupt response를 최소화하기 위함이다.

다음은 OSTickISR의 의사 코드이다. (어셈블리 언어로 작성된다.)

void OSTickISR(void)
{
    Save processor registers;
    Call OSIntEnter() or increment OSIntNesting;
    if (OSIntNesting == 1u) {
        OSTCBCur->OSTCBStkPtr = SP;
    }
    Call OSTimeTick();
    Clear interrupting device;
    Re-enable interrupts (optional);
    Call OSIntExit();
    Restore processor registers;
    Execute a return from interrupt instruction;
}

3.11.1 TickTask

이때 최대한 ISR을 짧게 만들기 위해서, 다음과 같은 이중 구조를 통해 task 수준에서 OSTimeTick()을 호출하도록 구성할 수 있다.

void TickTask (void *pdata)
{
  pdata = pdata;      /* Prevent compiler warning */
  for (;;) {
    OSMboxPend(...);  /* Wait for signal from OSTimeTick() */
    OSTimeTick();
    OS_Sched();
  }
}
void OSTickISR(void)
{
    Save processor registers;
    Call OSIntEnter() or increment OSIntNesting;
    if (OSIntNesting == 1u) {
        OSTCBCur->OSTCBStkPtr = SP;
    }

    Post a 'dummy' message (e.g. (void *)1) to the tick mailbox;

    Call OSIntExit();
    Restore processor registers;
    Execute a return from interrupt instruction;
}

3.12 OSInit()

uC/OS-II의 multitasking을 사용하기 위해서는 OSInit()을 호출하여 초기화를 수행해야 한다. 다음은 OS_CFG.H 내 상수 설정과, 이에 대응되는 OSInit() 초기화 시 변수와 자료 구조를 나타낸 도식이다.

63: idle task(lowest_prio), 62: statistics task(lowest_prio-1)

calling OSInit
free pools

3.13 Starting uC/OS-II

OSStart() 이후, 무한루프를 돌며 더 이상 return하지 않는다.

void main (void)
{
  OSInit();

  Create at least one task using OSTaskCreate() or OSTaskCreateExt();

  OSStart();
}