임베디드 OS 개발 프로젝트를 읽고 작성하였습니다.
시작하기 전에
RTOS를 포함한 운영체제라는 것을 한 문장으로?
-> 태스크를 관리하여 사용자가 하고 싶은 것을 할 수 있도록 도와주는 것
8장 태스크
8.1 태스크 컨트롤 블록
태스크 컨트롤 블록 (task control block)
: 개별 태스크 자체를 추상화하는 자료 구조
태스크 (task)
: 운영체제에서 동작하는 프로그램 그 자체
태스크가 바뀐다는 것은 동작하는 프로그램이 바뀐다는 말
전환 (switching)
: 현재 실행 중인 태스크가 태스크 1에서 태스크2로 전환되었다.
컨텍스트 (context)
: 프로그램의 현재 상태 정보, 태스크 블록은 현재 진행 중인 프로그램의 현재 상태 정보를 기록하고 있어야 하므로
1. RTOS 커널 만들기
kernel/task.h
태스크 컨트롤 블록과 태스크 관련 API 함수들을 정의
#ifndef KERNEL_TASK_H_
#define KERNEL_TASK_H_
#include "MemoryMap.h"
#define NOT_ENOUGH_TASK_NUM 0xFFFFFFFF
#define USR_TASK_STACK_SIZE 0x100000
#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_ */
코드 8.1 태스크 컨트롤 블록과 관련 함수 정의 task.h
코드 8.1 설명은 더보기 클릭!
4번째 줄 | #include "MemoryMap.h" | 9번째 줄에서 TASK_STACK_SIZE를 사용하기 때문 (4.3.1절에서 동작 모드별 스택을 리셋 핸들러에서 설정할 때 만든 값, 태스크 스택용으로 사용하려고 미리 계획한 값) |
8번째 줄 | USR_TASK_STACK_SIZE | 개별 태스크의 스택 크기 0x100000 = 1024 X 1024 (10진수) = 1MB |
11~22번째 줄 | 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; |
KernelTcb_t 구조체 : 태스크 컨트롤 블록 sp : 범용 레지스터에 있는 스택 포인터 stack_base 멤버 변수 : 컨텍스트에 포함되지 않는 부가 데이터, 개별 태스크의 스택 베이스 주소를 저장하려고 만듦 |
26번째 줄 | Kernel_task_init() | 커널의 태스크 관련 기능을 초기화하는 함수 |
27번째 줄 | Kernel_task_create() | 커널의 태스크를 생성(등록)하는 함수 |
태스크 스택용으로 64MB를 할당해 놓았고 각각의 태스크가 동일하게 1MB씩 스택을 쓸 수 있으므로 코드 8.1 구현에서 나빌로스는 태스크를 최대 64개까지 사용할 수 있음
8.2 태스크 컨트롤 블록 초기화
실제 메모리에 태스크 컨트롤 블록 인스턴스를 만들고 기본값을 할당하는 코드를 작성할 예정
kernel/task.c
Kernel_task_init() 함수를 구현
#include "stdint.h"
#include "stdbool.h"
#include "ARMv7AR.h"
#include "task.h"
static KernelTcb_t sFree_task_pool[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;
}
}
uint32_t Kernel_task_create(KernelTaskFunc_t startFunc)
{
return NOT_ENOUGH_TASK_NUM;
}
코드 8.2 태스크 컨트롤 블록 초기화 코드 task.c
코드 8.2 설명은 더보기 클릭!
7번째 줄 | static KernelTcb_t sFree_task_pool[MAX_TASK_NUM]; | 태스크 컨트롤 블록을 64개 배열로 선언 -> 메모리에 태스크 컨트롤 블록용으로 자리를 잡아 놓은 것, 동적 메모리 할당을 피하기 위해 일종의 객체 풀 (object pool)로 만든 것, 객체 풀에서 객체를 하나씩 가져와서 관리하려고 만든 배열임. |
8번째 줄 | sAllocated_tcb_index | 생성한 태스크 컨트롤 블록 인덱스를 저장하고 있는 변수, 태스크를 생성할 때마다 하나씩 이 변수의 값을 늘림 -> 태스크를 몇 개까지 생성했는지 이 변수 값을 보고 추적 가능 |
12번째 줄 | sAllocated_tcb_index = 0; | 배열의 시작이 0이므로 0으로 초기화 |
14~23번째 줄 | 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)); // STACK Base calculate sTask_list[i].sp = (uint32_t)sTask_list[i].stack_base + USR_TASK_STACK_SIZE -4; // //STACK pointer allocate sTask_list[i].sp -= sizeof(KernelTaskContext_t); KernelTaskContext_t* ctx = (KernelTaskContext_t*)sTask_list[i].sp; ctx->pc = 0; // PC 초기화 ctx->spsr = ARM_MODE_BIT_SYS; // 태스크의 프로그램 상태 레지스터 기본값을 SYS 모드로 설정 } 태스크 컨트롤 블록 배열을 모두 순회하면서 초기화하는 코드 16번째 줄 -> 1MB씩 늘려가면서 지정해주는 것 17번째 줄 -> 스택은 거꾸로 내려가므로 포인터는 stack_base 값에서 USR_TASK_STACK_SIZE만큼 증가한 값을 지정 & 태스크 간의 스택 경계를 표시하고자 4바이트를 비움 ⭐ 18,19번째 줄 -> 나빌로스의 태스크 관리의 중요 특징을 만듬 나빌로스는 태스크의 컨텍스트를 태스크 컨트롤 블록이 아니라 해당 태스크의 스택에 저장함 |
< 코드 8.2에서 초기화 한 태스크 스택 구조 그림 >
스택 포인터가 태스크 컨텍스트 다음에 위치하긴 하지만 태스크 컨텍스트는 앞으로 설명할 컨텍스트 스위칭 작업에 의해서 모두 레지스터로 복사되고 스택 포인터는 태스크 컨텍스트가 없는 위치로 이동하게 됨.
-> 동작 중인 태스크의 스택에는 태스크 컨텍스트가 존재하지 않음
8.3 태스크 생성
kernel/task.c
Kernel_task_create() 함수 만들기, 태스크로 동작할 함수를 태스크 컨트롤 블록에 등록하고 태스크 컨트롤 블록을 커널에 만듦.
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);
}
코드 8.3 태스크 생성 task.c
코드 8.3 설명은 더보기 클릭!
3번째 줄 | KernelTcb_t* new_tcb = &sTask_list[sAllocated_tcb_index++]; | 태스크 리스트에서 사용하지 않은 태스크 컨트롤 블록 객체를 하나 가져오는 코드 sAllocated_tcb_index는 바로 하나 증가해서 그 다음 비어 있는 배열 인덱스를 표시 |
5~8번째 줄 | if (sAllocated_tcb_index > MAX_TASK_NUM) { return NOT_ENOUGH_TASK_NUM; } |
[에러 검사 코드] sAllocated_tcb_index가 태스크 리스트의 최대 크기보다 커지면 안됨 |
10번째 줄 | KernelTaskContext_t* ctx = (KernelTaskContext_t*)new_tcb->sp; | 현재 스택에 저장되어 있는 컨텍스트 메모리 주소 포인터를 가져오기 |
11번째 줄 | ctx->pc = (uint32_t)startFunc; | 파라미터로 넘어오는 함수의 시작 수조를 PC에 넣기 -> 태스크 함수를 태스크 컨트롤 블록에 등록하는 것 |
13번째 줄 | return (sAllocated_tcb_index - 1); | 현재 작업한 인덱스 값은 1을 뺀 값이어야 함! why? 3번째 줄에서 sAllocated_tcb_index 의 값은 이미 1 올라가 있기 때문에 |
현 시점에서 아직 태스크를 동작시킬 수 없음
-> 스케줄러와 컨텍스트 스위칭까지 만들어야 함!!!
boot/Main.c
일단 더미 테스크 함수를 만들고 커널에 등록하기
void User_task0(void);
void User_task1(void);
void User_task2(void);
...
중략
...
static void Kernel_init(void)
{
uint32_t taskId;
taskId = Kernel_task_create(User_task0);
if (NOT_ENOUGH_TASK_NUM == taskId)
{
putstr("Task0 creation fail\n");
}
taskId = Kernel_task_create(User_task1);
if (NOT_ENOUGH_TASK_NUM == taskId)
{
putstr("Task1 creation fail\n");
}
taskId = Kernel_task_create(User_task2);
if (NOT_ENOUGH_TASK_NUM == taskId)
{
putstr("Task2 creation fail\n");
}
}
...
중략
...
void User_task0(void)
{
debug_printf("User Task #0\n");
while(true);
}
void User_task1(void)
{
debug_printf("User Task #1\n");
while(true);
}
void User_task2(void)
{
debug_printf("User Task #2\n");
while(true);
}
코드 8.4 태스크 등록 Main.c
더미 태스크 함수를 등록하는 코드가 3번 반복됨!
static void Kernel_init(void)
{
uint32_t taskId;
taskId = Kernel_task_create(User_task0); //함수 포인터를 Kernel_task_create()에 넘김, 태스크 컨트롤 블록의 PC에 저장됨
if (NOT_ENOUGH_TASK_NUM == taskId)
{
putstr("Task0 creation fail\n");
}
}
void User_task0(void)
{
debug_printf("User Task #0\n");
while(true); //계속 실행 WHY? 현재 나빌로스의 태스크 관리 설계에는 태스크의 종료를 보장하는 기능이 없음!
}
8.4 요약
태스크 컨트롤 블록 자료 구조를 설계하고 구현함, 태스크 컨트롤 블록에 함수 포인터를 연결해서 함수를 태스크로 만듦.
각 태스크 함수는 겉보기에 그냥 C 언어 함수와 다를 바 없이 생겼지만각 태스크 함수는 스택 주소와 레지스터를 독립적으로 가짐!=> 기능적으로 완전히 독립된 프로세스
'개발 > 임베디드 os 개발 프로젝트' 카테고리의 다른 글
10장 컨텍스트 스위칭 (0) | 2023.10.12 |
---|---|
9장 스케줄러 (0) | 2023.10.09 |
7장 타이머 (1) | 2023.10.07 |
6장 인터럽트 (1) | 2023.06.17 |
5장 UART (3) | 2023.06.15 |