[임베디드 OS 개발 프로젝트] 8. Task

Sijin·2025년 7월 6일

Task

  • RTOS의 Task는 리눅스 같은 범용 OS의 프로세스와 같은 의미이다
  • Task는 우선순위를 갖는다
    • 현재 진행 중인 Task가 UART 인터럽트에 의한 Task보다 우선순위가 높다면 UART Task는 기존 Task가 끝날 때 까지 기다려야 한다
    • 반대로, 모든 입력에 대한 응답은 10초 안에 처리되어야 한다는 조건이 있다면, 우선순위가 낮은 Task라도 10초 이전에 실행될 수 있다

Task control block

  • Task control block은 개별 태스크 자체를 추상화 하는 자료 구조이다
  • Task control block은 대표적으로 아래와 같은 내용을 포함한다
    • Task의 이름, 번호
    • Task의 우선순위
    • Task의 상태 정보인 context
    • 기타 등등 부수적 정보
  • task control block은 kernel 디렉토리를 만들어서 구현하자
# kernel/task.h

#ifndef KERNEL_TASK_H
#define KERNEL_TASK_H

#include "MemoryMap.h"

#define NOT_ENOUGH_TASK_NUM 0xFFFFFFFF
#define USR_TASK_STACK_SIZE 0x00100000
#define MAX_TASK_NUM        (TASK_STACK_SIZE / USR_TASK_STACK_SIZE)

typedef struct KernelTaskContext_t
{
    uint32_t spsr;
    uint32_t r0_r12[13];
    uint32_t pc;
} KernelTaskContext_t;

typedef struct KernelTcb_t
{
    uint32_t sp;
    uint8_t* stack_base;
} KernelTcb_t;

typedef void (*KernelTaskFunc_t)(void);

void Kernel_task_init(void);
uint32_t Kernel_task_create(KernelTaskFunc_t startFunc);

#endif /* KERNEL_TASK_H */
  • task stack 크기
    • USR_TASK_STACK_SIZE는 개별 task의 스택 크기로, 1mb이다
      • 개별 task마다 필요에 따라 스택 크기가 다른게 유연한 설계이지만, 우리는 간단하게 구현하자
    • TASK_STACK_SIZE는 task 스택용으로 memory map에서 할당해 놓은 64mb 크기이다
    • 즉 위 구성에서는 task를 최대 64개 만들 수 있다
  • task control block
    • KernelTaskContext_t는 context를 추상화 한 자료구조이다
      • 상태 레지스터, 범용 레지스터를 백업할 수 있는 영역이 있다
    • KernelTcb_t는 스택 관련 정보만 저장한다
      • sp는 범용 레지스터에 있는 stack pointer이다
      • stack_base는 context에 포함되지 않는 부가 데이터들이다
    • task context는 결국 레지스터와 stack pointer 값이다
    • 즉, task context를 전환한다는 말은 코어의 레지스터 값을 다른 task의 것으로 바꾼다는 말과 같다

task control block 초기화

  • 메모리에 task control block 인스턴스를 만들고 초기화하는 코드 Kernel_task_init 함수를 만들어보자
# kernel/task.c

static KernelTcb_t sTask_list[MAX_TASK_NUM];
static uint32_t sAllocated_tcb_index;

void Kernel_task_init(void)
{
    sAllocated_tcb_index = 0;

    for (uint32_t i = 0; i < MAX_TASK_NUM; i++)
    {
        sTask_list[i].stack_base = (uint8_t*)(TASK_STACK_START + i * USR_TASK_STACK_SIZE);
        sTask_list[i].sp = (uint32_t)(sTask_list[i].stack_base) + USR_TASK_STACK_SIZE - 4;
        sTask_list[i].sp -= sizeof(KernelTaskContext_t);
        KernelTaskContext_t* ctx = (KernelTaskContext_t*)sTask_list[i].sp;
        ctx->pc = 0;
        ctx->spsr = ARM_MODE_BIT_SYS;
    }
}
  • task control block들은 sTask_list에 선언된다
  • task control block 개수는 sAllocated_tcb_index에 저장된다
  • init 함수에서 sTask_list를 순회하면서 task control block을 모두 초기화한다
    • stack_base는 task 스택의 시작 주소다
    • 스택 포인터 sp는 거꾸로 내려가므로 stack의 마지막 주소에서 4byte를 뺀 값으로 설정하였다
      • sp에서 4를 빼주는건 task 사이에 경계를 표시하고자 함이다
    • sp에서 추가로 task control block 공간만큼 더 빼주었다
    • pc는 0으로 초기화하였다
    • 프로그램 상태 레지스터 spsr은 기본값으로 SYS로 설정하였다

Task 생성

  • 이어서 task 생성을 위한 Kernel_task_create 함수를 만들어보자
  • 이 함수는 task로 동작할 함수를 task control block에 등록하고 task control block을 커널에 만든다
uint32_t Kernel_task_create(KernelTaskFunc_t startFunc)
{
    KernelTcb_t* new_tcb = &sTask_list[sAllocated_tcb_index++];
    if (sAllocated_tcb_index > MAX_TASK_NUM)
    {
        return NOT_ENOUGH_TASK_NUM;
    }
    KernelTaskContext_t* ctx = (KernelTaskContext_t*)new_tcb->sp;
    ctx->pc = (uint32_t)startFunc;

    return sAllocated_tcb_index - 1;
}
  • sAllocated_tcb_index로 비어있는 task list 인덱스를 가져온다
    • task가 끝난 경우 index 관리는?
  • init 단계에서 스택에 저장해두었던 context 메모리 주소 포인터를 ctx로 가져온다
  • startFunc의 주소를 ctx->pc에 넣어준다
  • 이 부분이 task 함수를 task control block에 등록하는 과정이다
  • sAllocated_tcb_index가 이미 1이 올라가 있으므로, 현재 task의 index를 가르키기 위해 1을 빼서 return 한다

Task 등록

  • 현재는 스케줄러, 컨텍스트 스위칭이 구현되지 않아서 task를 실행시켜볼 수는 없다
  • 보통 전체 시스템을 각 기능별로 나누어 개발하고, 해당 기능을 실행하는 task 함수를 대표로 하나씩 만들어 펌웨어 시작 후 RTOS 초기화 코드에서 개별적으로 task를 등록한다
    • 예를 들어, 네트워크 모듈을 위한 별도 디렉토리가 있고, 그 중 네트워크 모듈 시작을 위한 task 함수도 있다
  • 우리는 그런 모듈들이 없으므로, Main.c 파일에서 Kernel을 초기화하고 dummy task 함수를 만들어 커널에 등록시켜보자
void dummy_task(void)
{
    debug_printf("dummy task\n");
    while (true);
}

static void Kernel_init(void)
{
    uint32_t taskId;
    Kernel_task_init();

    taskId = Kernel_task_create(dummy_task);
    if (taskId == NOT_ENOUGH_TASK_NUM)
    {
        debug_printf("dummy task creation failed\n");
    }
}

void main(void)
{
    Hw_init();
    Kernel_init();
    ...
}
  • Kernel_task_create 함수에 넘어간 함수 포인터는 task control block의 pc에 저장되고 해당 task 함수가 호출 될 것이다
  • 현재 task 관리 설계에서는 task 종료를 보장하는 기능이 없으므로 task는 while(true)로 종료되지 않도록 하였다

0개의 댓글