이것저것 일이 생기고 시간을 퍼부울 수 있는 환경이 아니라 틈틈히 작업했다

이전에 범용 자료구조라고 Universal Data Structure 해서 UDS라고 디렉토리 이름을 지어줬는데

좀더 고민해보고 찾아보니까

Universal 보단 General 이 더 어울릴꺼같아서 UDS에서 GDS로 이름을 바꾸었다.

본문으로 넘어가서

아직 멀티테스킹 그런건 아니지만, 그것을 지원하기 위한 첫 단추로 Task를 정의하고

Context Switch 를 구현했다.

뭐 화려하고 멋진 그런 모양세는 아니지만

정적이긴 하지만 테스트로 작성한 두 테스크간의 스위칭을 볼 수 있다.

결과 화면

일단, 테스크를 정의하기에 앞서 Context, 문맥이라고 하는부분을 짚고 넘어갈 필요가 있다.

이 문맥이라는 녀석이, OS 개론을 배웠다면 알겠지만

프로세스면 프로세스, 테스크면 테스크 의 실행 상태를 의미한다.

뭐 실행 상태라고 하니까 뭔가 이상한데

말그대로 Register의 값들, Stack등을 문맥이라고 한다.

그 컨텍스트를 말그대로 교환한다는 뜻이고, 이거 실행하다 저거 실행하는걸 할 수 있다는 뜻이다.

Task는 개별적으로 처리가 가능한 작업을 의미하는데,

테스크에서 Code, Data는 공유가 가능, Stack, Context 는 독립적임을 유지한다.

테스크의 작업을 일괄 처리 방식으로 처리할때보다 시분할로 나누어 처리할때 레이턴시가 많이 증가한다.

따라서, 0SOS는 시분할 멀티테스킹 기법을 사용할 것이다.

시분할 멀티테스킹은 PIT 컨트롤러의 수 밀리세컨드에서 수십 밀리세컨드 단위로 IRQ 0 인터럽트에 맞춰 테스크를 전환하는 기법이다.

아무튼 OS개론적인 이야기는 일단 뒤로 두고, 위 이미지의 실행 코드는 다음과 같다.

//shell.c
static TCB g_task[2] = {0.};
static QWORD test_stack[1024] = {0,};

void TaskTask()
{
    int iteration = 0;
    while(1)
    {
        _Printf("[%d] Message from test task Press any key to switching\n",iteration++);
        _GetCh();
        ContextSwitch(&g_task[1].Context, &g_task[0].Context);
    }

}

void Command_CreateTask(const char* _Parameter)
{
    KEYDATA key;
    InitTask(&g_task[1],1,0, TaskTask, test_stack, sizeof(test_stack));
    
    int iteration = 0;
    while(1)
    {
        _Printf("[%d] message from shell Press any key to switching\n", iteration++);
        if(_GetCh() =='q')
            break;
        ContextSwitch(&g_task[0].Context, &g_task[1].Context);  
        }

}

대충 본다면, CreateTask

테스크를 초기화(생성)하는 함수다.

ContextSwitch Context를 교환해주는 함수다.

Task Control Block 해서 TCB 구조체를 정의해줬고

Context에 대해 정의하고 각 Offset을 만들어줬다.

뭐 이번에 작성한 코드가 그렇게 길지 않다.

일단 Tasking 이라는 디렉터리를 만들어 줬다.

테스킹 관련 코드는 이쪽으로 넣으려고 한다.

일단, Tasking/Task.h

#ifndef __TASK_H__
#define __TASK_H__

#include <Types.h>

//SS, RSP, RFLAGS, CS, RIP.. 등등 Context
#define CONTEXT_REGISTER_COUNT     25
#define CONTEXT_REGISTER_SIZE       8

#define CONTEXT_OFFSET_GS           0
#define CONTEXT_OFFSET_FS           1
#define CONTEXT_OFFSET_ES           2
#define CONTEXT_OFFSET_DS           3
#define CONTEXT_OFFSET_R15          4
#define CONTEXT_OFFSET_R14          5
#define CONTEXT_OFFSET_R13          6
#define CONTEXT_OFFSET_R12          7
#define CONTEXT_OFFSET_R11          8
#define CONTEXT_OFFSET_R10          9
#define CONTEXT_OFFSET_R9           10
#define CONTEXT_OFFSET_R8           11
#define CONTEXT_OFFSET_RSI          12
#define CONTEXT_OFFSET_RDI          13
#define CONTEXT_OFFSET_RDX          14
#define CONTEXT_OFFSET_RCX          15
#define CONTEXT_OFFSET_RBX          16
#define CONTEXT_OFFSET_RAX          17
#define CONTEXT_OFFSET_RBP          18
#define CONTEXT_OFFSET_RIP          19
#define CONTEXT_OFFSET_CS           20
#define CONTEXT_OFFSET_RFLAG        21
#define CONTEXT_OFFSET_RSP          22
#define CONTEXT_OFFSET_SS           23

#pragma pack(push,1)
typedef struct __CONTEXT_STRUCT
{
    QWORD Registers[CONTEXT_REGISTER_COUNT];
} CONTEXT;

typedef struct __TCB_STRUCT
{
    CONTEXT Context;
    QWORD ID;
    QWORD Flags;

    void* StackAddress;
    QWORD StackSize;
} TCB;
#pragma pack(pop)

void InitTask(TCB* _Tcb, QWORD _ID, QWORD _Flags, QWORD _EntryPointAddress, void* _StackAddress, QWORD _StackSize);

//Link Assembly File
void ContextSwitch(CONTEXT* _CurrentContext, CONTEXT* _NextContext);
#endif /*__TASK_H__*/   

Definition 이 많은데 결국 생각해보면 죄다 배열의 Index고 좀더 자세히보면 다 레지스터다

배열에 때려박을때 고정적인 위치에 때려박으려고 (어셈에선 순서를 지켜줘야한다 ㅋㅋ) 정의해줬다.

그 밑으로는 Context (결국 Register 값들의 배열이다.) *아마 나중에 추가 될 것 같다.

그리고 TCB (이게 중의적일거같다. Task Control Block, Thread Control Block)

TCB엔 Cotnext, ID, Flags, 스택주소와 스택 크기가 있다.

그리고 InitTask에 대한 구현인

Tasing/Task.c 이다.


#include "Task.h"
#include <Utility/Memory.h>
#include <Descriptor/GDT.h>
void InitTask(TCB* _Tcb, QWORD _ID, QWORD _Flags, QWORD _EntryPointAddress, void* _StackAddress, QWORD _StackSize)
{
    _MemSet(_Tcb->Context.Registers, 0, sizeof(_Tcb->Context.Registers));
    
    //초기화 과정의 RSP, RBP 해당 Task의 Stack Pointer 기 떄문에 + Size 
    _Tcb->Context.Registers[CONTEXT_OFFSET_RSP] = (QWORD)_StackAddress + _StackSize;
    _Tcb->Context.Registers[CONTEXT_OFFSET_RBP] = (QWORD)_StackAddress + _StackSize;
    
    //Segment Register Setup in Context 
    _Tcb->Context.Registers[CONTEXT_OFFSET_CS] = GDT_KERNEL_CODE_SEGMENT;
    _Tcb->Context.Registers[CONTEXT_OFFSET_DS] = GDT_KERNEL_DATA_SEGMENT;
    _Tcb->Context.Registers[CONTEXT_OFFSET_ES] = GDT_KERNEL_DATA_SEGMENT;
    _Tcb->Context.Registers[CONTEXT_OFFSET_FS] = GDT_KERNEL_DATA_SEGMENT;
    _Tcb->Context.Registers[CONTEXT_OFFSET_GS] = GDT_KERNEL_DATA_SEGMENT;
    _Tcb->Context.Registers[CONTEXT_OFFSET_SS] = GDT_KERNEL_DATA_SEGMENT;
    
    //Next run instruction setup
    _Tcb->Context.Registers[CONTEXT_OFFSET_RIP] = _EntryPointAddress;

    // 0 NT  IOPL  OF DF IF TF SF ZF 0  AF 0  PF 1  CF
    // 0  0  0  0  0  0  1  0  0  0  0  0  0  0  0  0
    _Tcb->Context.Registers[CONTEXT_OFFSET_RFLAG] |= 0x0200;   
    //Setup TCB Block
    _Tcb->ID            = _ID;
    _Tcb->StackAddress  = _StackAddress;
    _Tcb->StackSize     = _StackSize;
    _Tcb->Flags         = _Flags;
 }

결국 각 맴버들 초기화 하는 함수다.

세그먼트는 커널 세그먼트를 등록하고

여러 레지스터의 초깃값을 설정한다.

다음 실행할 코드를 (RIP 레지스터- [PC, Program Counter]) EntryPoint로 등록한다.

그리고 대망의 ContextSwitch 함수 이 함수는 어셈블리 코드와 링킹되어있다.


[BITS 64]

global ContextSwitch

SECTION .text   

%macro SAVECONTEXT 0
    push rbp
    push rax
    push rbx
    push rcx
    push rdx
    push rdi
    push rsi
    push r8
    push r9
    push r10
    push r11
    push r12
    push r13
    push r14
    push r15

    mov ax, ds
    push rax
    mov ax, es
    push rax
    push fs
    push gs

    mov ax, 0x10
    mov ds,ax
    mov es,ax
    mov gs,ax
    mov fs,ax

%endmacro


%macro LOADCONTEXT 0
    pop gs
    pop fs
    pop rax
    mov es, ax
    pop rax
    mov ds, ax

    pop r15
    pop r14
    pop r13
    pop r12
    pop r11
    pop r10
    pop r9
    pop r8
    pop rsi
    pop rdi
    pop rdx
    pop rcx
    pop rbx
    pop rax
    pop rbp

%endmacro

ContextSwitch:
    push rbp
    mov rbp, rsp
    ;Push RFLAGS in stack
    pushfq
    ;if _CurrentContext is NULL
    cmp rdi,0
    je .LoadContext
    popfq
    push rax    ;For use Context Offset 

    ;Save SS RSP RFLAGS CS RIP Registers
    mov ax, ss
    mov qword[rdi + 23 * 8], rax
    mov rax, rbp

    ;RSP를 Contextswitch 호출 이전으로 되돌림 -(ReturnAddress + RBP)
    add rax, 16
    mov qword[rdi + 22 * 8], rax

    ;Push FLAGS in stack
    pushfq
    pop rax
    mov qword[rdi + 21 * 8], rax
    
    ;Save Cs Segment
    mov ax, cs
    mov qword[rdi + 20 * 8], rax

    mov rax, qword[rbp + 8]
    mov qword[rdi + 19 * 8], rax

    pop rax
    pop rbp
    ;Context->Registers를 채워버림
    add rdi, 19*8
    mov rsp, rdi
    sub rdi, 19*8

    SAVECONTEXT

.LoadContext:
    mov rsp, rsi
    LOADCONTEXT
    iretq

일단 첫번째 파라미터가 Null(0)이면 ShellTask라고 간주한다.

.LoadContext로 점프하고

그렇게 된다면 2번째 파라미터의 Context로 Load 하게 된다.

만약 아니라면, 현제 컨텍스트를 저장한다.

저장하고 나서 두번째 파라미터로 로드.

이로써 ContextSwitch 가 끝났다.

코드도 안길고

그렇게 복잡하지도 않지만

이것저것 일때문에 바빠서 오래걸렸다.

쉬엄 쉬엄 취미로 하는 느낌이니까

큰 걱정은 없다.

휴, 다음이 스케줄링이니까 또 달려야지.