스케줄링 = 스케줄러 + 컨텍스트 스위칭

라운드 로빈 스케줄러와 컨텍스트 스위칭 구현했으니 스케줄링 시점 결정이 필요

 

시분할 시스템과 선점형 비선점형 멀티테스킹

- 시분할 시스템 : 정기적인 타이머 인터럽트로 태스크를 일정 시간만 동작하고 다음으로 바꾸도록 스케줄링하는 운영체제

- 선점형 멀티 태스킹 : 태스크가 스케줄링 요청안해도 커널이 강제로 스케줄링

 -> 시분할 시스템은 선점형 멀티태스킹에 가까움.

- 비선점형 멀티태스킹 : 태스크가 스케줄링 요청안하면 커널이 스케줄링 안함

- 선점형/ 비선점형 결정 : 임베디드 시스템 요구 사항에 달림.

* 나빌로스는 비선점형 스케줄링 = 태스크가 커널에 스케줄링 요청 필요

  = >현재 태스크가 다음 태스크에게 자원을 양보(yield) 하도록 구현하자!

 

 

yield(양보) 구현하기

- 태스크보다는 커널 영역 -> kernel/Kernel.c, kernel/Kernel.h 구현

Kernel_yield 호출 즉시 kernel_task_scheduler(task.c) 함수로 스케줄러에서 다음에 동작할 태스크 선정

-> 호출한 태스크 컨텍스트 백업

-> 스케줄러가 선정한 태스크 스택 포인터 복구

-> 태스크 스택 포인터를 이용해 컨택스트 복구

-> Kernel_yield()가 마치면서 다음 태스크 동작

kernel/task.c

 

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를 호출하면서

각자의 스택포인터를 출력하는걸 확인할수 있다.

 

 

+ Recent posts