스케줄링 = 스케줄러 + 컨텍스트 스위칭
라운드 로빈 스케줄러와 컨텍스트 스위칭 구현했으니 스케줄링 시점 결정이 필요
시분할 시스템과 선점형 비선점형 멀티테스킹
- 시분할 시스템 : 정기적인 타이머 인터럽트로 태스크를 일정 시간만 동작하고 다음으로 바꾸도록 스케줄링하는 운영체제
- 선점형 멀티 태스킹 : 태스크가 스케줄링 요청안해도 커널이 강제로 스케줄링
-> 시분할 시스템은 선점형 멀티태스킹에 가까움.
- 비선점형 멀티태스킹 : 태스크가 스케줄링 요청안하면 커널이 스케줄링 안함
- 선점형/ 비선점형 결정 : 임베디드 시스템 요구 사항에 달림.
* 나빌로스는 비선점형 스케줄링 = 태스크가 커널에 스케줄링 요청 필요
= >현재 태스크가 다음 태스크에게 자원을 양보(yield) 하도록 구현하자!
yield(양보) 구현하기
- 태스크보다는 커널 영역 -> kernel/Kernel.c, kernel/Kernel.h 구현
Kernel_yield 호출 즉시 kernel_task_scheduler(task.c) 함수로 스케줄러에서 다음에 동작할 태스크 선정
-> 호출한 태스크 컨텍스트 백업
-> 스케줄러가 선정한 태스크 스택 포인터 복구
-> 태스크 스택 포인터를 이용해 컨택스트 복구
-> Kernel_yield()가 마치면서 다음 태스크 동작
kernel/Kernel.h
#ifndef KERNEL_KERNEL_H_
#define KERNEL_KERNEL_H
#include "task.h"
void Kernel_yield(void);
#endif
kernel/Kernel.c
#include <stdint.h>
#include <stdbool.h>
#include "Kernel.h"
void Kernel_yield(void)
{
Kernel_task_scheduler();
}
이전 코드는 커널 시작 시 동작 태스크가 없어서 동작하지 않음.
컨텍스트 스위칭 과정 정리
- 호출 과정 : Restore_context<-Save_context <- kernel_task_context_switching <- kernel_task_scheduler <- kernel_yield
- Saved_context 함수에서 1. 현재 태스크 컨택스트를 스택에 백업 2. TCB에 SP 백업
- Restore_context 함수에서 1. 다음 TCB에서 SP 가져옴 2. SP에서 컨택스트 읽음
<-> 커널 시작시에는 동작 태스크가 없어 저장도 못하고, 복원할거도 없다!
=> 해결책 : 첫 스케줄링 시 컨텍스트 백업(Saved_context) 없이, 0번 TCB을 복구하자(=TCB 정적 전역변수 초기값 사용)
kernel/task.c
추가 사항
1. static uint32_t sCurrent_tcb_index;
2. kernel_task_int() - sCurrent_tcb_index = 0;
3. Kernel_Task_start() 함수
Kernel_task_start 커널 시작 시 한번만 호출
- 0번 tcb 주소 가져와 - restore_context
-> 기존 태스크 컨택스트는 사라지고 다음 태스크 컨택스트를 가져옴.
#include <stdint.h>
#include <stdbool.h>
#include "ARMv7AR.h"
#include "task.h"
//task
static KernelTcb_t sTask_list[MAX_TASK_NUM];
static uint32_t sAllocated_tcb_index;
static uint32_t sCurrent_tcb_index;
//scheduling
static uint32_t sCurrent_tcb_index;
static KernelTcb_t* Scheduler_round_robin_algorithm(void);
//context switching
static KernelTcb_t* sCurrent_tcb;
static KernelTcb_t* sNext_tcb;
void Kernel_task_init(void)
{
sAllocated_tcb_index = 0;
sCurrent_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)
{
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);
}
static KernelTcb_t* Scheduler_round_robin_algorithm(void)
{
sCurrent_tcb_index++;
sCurrent_tcb_index %= sAllocated_tcb_index;
return &sTask_list[sCurrent_tcb_index];
}
void Kernel_task_scheduler(void)
{
sCurrent_tcb = &sTask_list[sCurrent_tcb_index];
sNext_tcb = Scheduler_round_robin_algorithm();
Kernel_task_context_switching();
}
__attribute__ ((naked)) void Kernel_task_context_switching(void)
{
__asm__ ("B Save_context");
__asm__ ("B Restore_context");
}
static __attribute__ ((naked)) void Saved_context(void)
{
//save current task context into the current task stack
__asm__ ("PUSH {lr}");
__asm__ ("PUSH {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}");
__asm__ ("MRS r0, cpsr");
__asm__ ("PUSH {r0}");
//save current task stack pointer into the current TCB
__asm__ ("LDR r0, =sCurrent_tcb");
__asm__ ("LDR r0, [r0]");
__asm__ ("STMIA r0!, {sp}");
}
static __attribute__ ((naked)) void Restore_context(void)
{
//restore next task stack pointer from the next TCB
__asm__ ("LDR r0, =sNext_tcb");
__asm__ ("LDR r0, [r0]");
__asm__ ("LDMIA r0!, {sp}");
// restore next task context from the next task stack
__asm__ ("POP {r0}");
__asm__ ("MSR cpsr, r0");
__asm__ ("POP {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11, r12}");
__asm__ ("POP {pc}");
}
void Kernel_task_start(void)
{
sNext_tcb = &sTask_list[sCurrent_tcb_index];
Restore_context();
}
메인 함수에서 커널 관련 초기화 함수를 모아서 한번에 쓸수 있도록
task.c/.h의 Kernel_task_start() 함수를
Kernel.c/.hdp 커널 API인 Kernel_start() 구현해서 호출
kernel/Kernel.h
#ifndef KERNEL_KERNEL_H_
#define KERNEL_KERNEL_H
#include "task.h"
void Kernel_start(void);
void Kernel_yield(void);
#endif
kernel/Kernel.c
#include <stdint.h>
#include <stdbool.h>
#include "Kernel.h"
void Kernel_start(void)
{
Kernel_task_start();
}
void Kernel_yield(void)
{
Kernel_task_scheduler();
}
Main 함수 수정 : 첫 태스크 컨택스트를 복원해서 실행 추가
Kernel_init() 함수 맨 끝에 Kernel_start() 추가
Kernel_init() 정리
1. Kernel_task_int()으로 64개의 태스크 스택 배이스, SP 등록, 컨텍스트 영역 추가
2. Kernel_task_create를 세번 호출
- Kernel_task_create 동작
1. 호출할 함수 매개변수로 받음
2. 현재 tcb가져옴 + sAllocated_tcb_index를 1 추가(다음 tcb로 넘어가도록)
3. tcb의 스택포인터를 가져온다 = 현재 스택 포인태 위에는 컨텍스트가 존재
4. 컨택스트의 pc에 인자로 전달받은 함수 저장 = 이 함수 실행
5. 현재 tcb 인덱스 반환
3. Kernel_start() 호출
1. sCurrent_tcb_index는 0번이므로 맨 처음 만든 tcb 주소 가져옴
2. 가져온 tcb의 컨택스트를 로드
3. tcb로 로드한 컨택스트에 pc에 등록된 함수(=태스크 생성시 등록한 함수) 호출
주의 : 아직 각 태스크 등록 함수는 Kernel_yield 호출 하지 않았다! = 다음 태스크에게 양보안한다
boot/Main.c
#include <stdint.h>
#include <stdbool.h>
#include "HalInterrupt.h"
#include "HalUart.h"
#include "HalTimer.h"
#include "task.h"
#include "Kernel.h"
#include "stdio.h"
#include "stdlib.h"
static void Hw_init(void);
static void Kernel_init(void);
static void Printf_test(void);
static void Timer_test(void);
void User_task0(void);
void User_task1(void);
void User_task2(void);
void main(void)
{
Hw_init();
uint32_t i = 100;
while(i--)
{
Hal_uart_put_char('N');
}
Hal_uart_put_char('\n');
putstr("Hello World!\n");
Printf_test();
Timer_test();
}
static void Hw_init(void)
{
Hal_interrupt_init();
Hal_uart_init();
Hal_timer_init();
}
static void Printf_test(void)
{
char* str = "printf pointer test";
char* nullptr = 0;
uint32_t i = 5;
uint32_t* sysctrl0 = (uint32_t*)0x10001000;
debug_printf("%s\b", "Hello printf");
debug_printf("output string pointer: %s\n", str);
debug_printf("%s is null pointer, %u number\n", nullptr, 10);
debug_printf("%u=5\n", i);
debug_printf("dec=%u hex=%x\n", 0xff, 0xff);
debug_printf("print zero %u\n", 0);
debug_printf("SYSCTRL0 %x\n", *sysctrl0);
}
static void Timer_test(void)
{
while(true)
{
debug_printf("current count : %u\n", Hal_timer_get_1ms_counter());
delay(1000);
}
}
static void Kernel_init(void)
{
uint32_t taskId;
Kernel_task_init();
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");
}
Kernel_start();
}
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);
}
스택의 스택 포인터를 가져와서 볼수 있도록
지역변수 선언
해당 지역변수의 주소= 스택포인터를 확인할수 있도록 debug_printf 와
다음 태스크에게 넘겨줄수 있도록 kernel_yield() 추가.
#include <stdint.h>
#include <stdbool.h>
#include "HalInterrupt.h"
#include "HalUart.h"
#include "HalTimer.h"
#include "task.h"
#include "Kernel.h"
#include "stdio.h"
#include "stdlib.h"
static void Hw_init(void);
static void Kernel_init(void);
static void Printf_test(void);
static void Timer_test(void);
void User_task0(void);
void User_task1(void);
void User_task2(void);
void main(void)
{
Hw_init();
uint32_t i = 100;
while(i--)
{
Hal_uart_put_char('N');
}
Hal_uart_put_char('\n');
putstr("Hello World!\n");
Printf_test();
Timer_test();
Kernel_init();
while(true);
}
static void Hw_init(void)
{
Hal_interrupt_init();
Hal_uart_init();
Hal_timer_init();
}
static void Printf_test(void)
{
char* str = "printf pointer test";
char* nullptr = 0;
uint32_t i = 5;
uint32_t* sysctrl0 = (uint32_t*)0x10001000;
debug_printf("%s\b", "Hello printf");
debug_printf("output string pointer: %s\n", str);
debug_printf("%s is null pointer, %u number\n", nullptr, 10);
debug_printf("%u=5\n", i);
debug_printf("dec=%u hex=%x\n", 0xff, 0xff);
debug_printf("print zero %u\n", 0);
debug_printf("SYSCTRL0 %x\n", *sysctrl0);
}
static void Timer_test(void)
{
for(uint32_t i = 0; i < 5 ; i++)
{
debug_printf("current count : %u\n", Hal_timer_get_1ms_counter());
delay(1000);
}
}
static void Kernel_init(void)
{
uint32_t taskId;
Kernel_task_init();
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");
}
Kernel_start();
}
void User_task0(void)
{
uint32_t local = 0;
while(true)
{
debug_printf("User Task #0 SP = 0x%x\n", &local);
Kernel_yield();
}
}
void User_task1(void)
{
uint32_t local = 0;
while(true)
{
debug_printf("User Task #1 SP = 0x%x\n", &local);
Kernel_yield();
}
}
void User_task2(void)
{
uint32_t local = 0;
while(true)
{
debug_printf("User Task #2 SP = 0x%x\n", &local);
Kernel_yield();
}
}
kernel 폴더와 관련 파일들이 추가됬으니 makefile도 수정하자
ARCH=armv7-a
MCPU=cortex-a8
TARGET=rvpb
CC=arm-none-eabi-gcc
AS=arm-none-eabi-as
LD=arm-none-eabi-gcc
OC=arm-none-eabi-objcopy
LINKER_SCRIPT=./navilos.ld
MAP_FILE=build/navilos.map
ASM_SRCS=$(wildcard boot/*.S)
ASM_OBJS=$(patsubst boot/%.S, build/%.os, $(ASM_SRCS))
VPATH = boot \
hal/$(TARGET) \
kernel \
lib
C_SRCS = $(notdir $(wildcard boot/*.c))
C_SRCS += $(notdir $(wildcard hal/$(TARGET)/*.c))
C_SRCS += $(notdir $(wildcard lib/*.c))
C_OBJS = $(patsubst %.c, build/%.o, $(C_SRCS))
INC_DIRS = -I include \
-I hal \
-I hal/$(TARGET) \
-I kernel \
-I lib
CFLAGS = -c -g -std=c11
LDFLAGS = -nostartfiles -nostdlib -nodefaultlibs -static -lgcc
navilos=build/navilos.axf
navilos_bin=build/navilos.bin
.PHONY: all clean run debug gdb
all:$(navilos)
clean:
@rm -fr build
run: $(navilos)
qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -nographic
debug: $(navilos)
qemu-system-arm -M realview-pb-a8 -kernel $(navilos) -S -gdb tcp::1234,ipv4
gdb:
gdb-multiarch
$(navilos):$(ASM_OBJS) $(C_OBJS) $(LINKER_SCRIPT)
$(LD) -n -T $(LINKER_SCRIPT) -o $(navilos) $(ASM_OBJS) $(C_OBJS) -Wl,-Map=$(MAP_FILE) $(LDFLAGS)
$(OC) -O binary $(navilos) $(navilos_bin)
build/%.os: %.S
mkdir -p $(shell dirname $@)
$(CC) -mcpu=$(MCPU) $(INC_DIRS) $(CFLAGS) -o $@ $<
build/%.o: %.c
mkdir -p $(shell dirname $@)
$(CC) -mcpu=$(MCPU) $(INC_DIRS) $(CFLAGS) -o $@ $<
이전에 문법 에러한것도 다 고치고,
책에 빠진 부분이 있어서 로직이 제대로 돌아가지 않은 부분이있었다.
깃헙 메인함수 참고해서 수정하고 빌드 후 돌리면
테스트 마친 후 테스크 0, 1, 2, 3이 서로 돌아가면서
커널 생성 때 등록한 함수 User_task0, 1, 2를 호출하면서
각자의 스택포인터를 출력하는걸 확인할수 있다.