참고. 멀티 태스킹(Multi tasking) 구현 방법

 

들어가기 전에...


1.멀티 태스킹(Multitasking)이란?

 멀티 태스킹을 구현하는 방법을 간단하게 설명하면 기존에 실행 중인 태스크의 컨텍스트(Context)를 저장하고 실행할 태스크의 컨텍스트를 복원하는 것이다. 솔찍히 이게 전부인데... 실제 코드로 구현하게되면 두가지 방법 정도로 나누어진다.

 

  • 하드웨어의 기능을 사용하는 방법 : Intel과 같은 Architecture에서는 하드웨어적으로 TaskSwitching을 지원한다. Jmp 명령으로 태스크 스위칭이 가능하다. 하드웨어적인 방법으로 구현하게 되면 아주 일이 간단해 진다.
  • 소프트웨어 적으로 구현하는 방법 : 개발자가 일일이 레지스터를 스택이나 특수한 메모리 공간에 저장하고 일일이 복원하는 방법이다. 일이 좀 복잡하긴한데, 입맛대로 저장하고 복원할 수 있어서 좋은 점도 있으나 세심한 주의가 필요하다.

 

 프레임워크에서는 하드웨어적인 방법을 사용하지 않고 일일이 레지스터를 저장하고 복원하는 방법을 선택했다. 이유는 내가 하드웨어적인 방법은 이미 해봤기 때문이다.(ㅡ_ㅡa..).

 

 자 그럼 소프트웨어적으로 어떻게 구현하는지 알아보자. 우리가 해야 할 일이 결국 현재 동작중이던 것을 저장했다가, 다시 복원하는 것 아닌가? 그럼 현재 동작중인 태스크라는 것은 무엇이란 말인가?

 

 작은 범위에서 동작중인 상태라 하면 CPU의 각 레지스터의 상태정보라고 생각 할 수 있고, 좀더 큰 범위에서 보자면 메모리의 내용까지도 포함할 수 있을 것 같다.

 메모리의 내용까지 포함하면 일이 점점 커지므로 일단 CPU의 레지스터 정보를 저장하는 것으로 하고 어디에다 저장할 것인지를 생각해 보자.

 

 가장 쉽게 생각할 수 있는 것이 바로 스택이다. 스택은 프로그램의 실행에서는 빠져서는 안되는 공간이며, 역시 인터럽터 처리 시에도 스택의 역할은 중요하다(에러코드의 저장이라던지, 아니면 부분적인 컨택스트의 저장이라던지 하는 부분...).

 

 그럼 스택만 사용하나? 꼭 그런것은 아니다. 별도의 공간에 컨텍스트를 저장하는 공간을 만들어서 거기다 저장하고 복원하는 방법이 있다.

 어느 것이 더 좋은지 굳이 따지자면 별도의 공간에 컨텍스트를 저장하는 것이 조금 더 나은 방법인 것 같다. 왜냐하면 스택같은 경우 스택의 오버플로우 같은 상황에서 컨택스트 스위칭이 되면 결과는 예측 할 수 없기 때문이다.

 하지만 스택을 사용하는 방법은 비교적 코드가 간단하기 때문에, 프레임워크에서는 스택에 저장하기로 했다.(컨텍스트를 관리하는 별도 공간이 필요없는 점도 큰 역할을 했다.)

 

2.스택(Stack)에 저장되는 레지스터의 값 및 위치

 그럼 어떻게 저장할까? 아래 그림은 프레임워크에서 레지스터를 저장하는 것을 표시한 것이다.

 초기설정.PNG

<초기상태>

 

 위 처럼 모든 레지스터를 스택의 바닥(bottom)부터 레지스터를 차곡 차곡 쌓아서 올린다. 태스트를 처음 생성했을 때는 태스크가 복원되자 마자 Task 함수의 처음으로 이동해야 하므로 스택의 포인터는 top에서 4byte아래인 태스크 엔트리 포인트(Start)를 가리키게 하고 스택의 맨 꼭대기는 태스크가 종료됬을 때, 호출될 마감 함수의 엔트리 포인트를 넣어둔다. 이렇게 함으로써 태스크의 첫 실행이 가능하다.

 

 그렇다면 한참을 실행하는 도중에 태스크가 저장되면 어떻게 될까? 스택의 top만 다르고 동일하다.

실행중.PNG

 

<실행 도중>

 

 레지스터를 저장하는 시작 영역을 스택의 바닥으로 고정하였는데, 이렇게 하면 컨택스트를 저장하고 복원하는 루틴이 간단해진다. 복원은 저장의 역순으로 하면 되니 생략한다.

 

3.구현

 자 그럼 이제 어떻게 저장하고 복원하는 알아봤으니 실제 코드를 보자.

 아래의 코드는 Task를 정의한 구조체이다. 스택만 존재하는 것을 알 수 있다. 별도의 정보가 필요하면 스택 영역 뒤에 붙이면 된다.


  1. // Task에 대한 정보 저장
    typedef struct kTaskStruct
    {
        // cs, ds, ss, es, fs, gs에서
        // EAX, ECX, EDX, EBX, ESP, EBP, ESI, EDI 순으로 Push 한다.
        // Stack의 14DWORD를 Register를 저장하는데 사용한다.
        DWORD vdwStack[ MAX_STACKSIZE ];
    1. // 별도의 추가적인 데이터가 필요하면 여기에 삽입하면 된다. 
    2. // 여기!!
  2. } TASK, *PTASK;

 

 아래의 코드는 실제로 스위칭을 시도하는 어셈블리어 코드이다.

  1. ;Task Switching을 수행한다.
    _kSwitchTask :
     cli
  2.  push eax
     ; General Register
     ; ESP, EBP, EAX, EBX, ECX, EDX, ESI, EDI의 순서로 Push한다.
     ; Current Task
     mov eax, dword [ss:esp + 8]
     
     ; esp의 계산
     push ebx
     mov ebx, esp
     add ebx, 8
     mov dword [eax + 52], ebx
     pop ebx
  3.  mov dword [eax + 48], ebp
     mov dword [eax + 40], ebx
     mov dword [eax + 36], ecx
     mov dword [eax + 32], edx
     mov dword [eax + 28], esi
     mov dword [eax + 24], edi
     ; eax를 집넣는다.
     mov ebx, eax
     pop eax
     mov dword [ebx + 44], eax
  4.  ; Segment Register
     ; cs, ds, ss, es, fs, gs의 순서로 Push한다.
     mov ax, cs
     mov dword [ebx + 20], eax
     mov ax, ds
     mov dword [ebx + 16], eax
     mov ax, ss
     mov dword [ebx + 12], eax
     mov ax, es
     mov dword [ebx + 8], eax
     mov ax, fs
     mov dword [ebx + 4], eax
     mov ax, gs
     mov dword [ebx + 0], eax

  5.  ; Next Task
     mov eax, dword [ss:esp + 8]
     mov esp, eax
  6.  ; segment register
     pop eax
     mov gs, ax
     pop eax
     mov fs, ax
     pop eax
     mov es, ax
     pop eax
     mov ss, ax
     pop eax
     mov ds, ax
     ; cs의 값은 지금 쓰지 않는다.
     pop eax
  7.  ; General Register
     pop edi
     pop esi
     pop edx
     pop ecx
     pop ebx
     pop eax
     pop ebp
     pop esp
  8.  sti
     ret

 

 어셈블리어를 잘 모르는 사람은 약간 낯설겠지만 위에 나온 인덱스만 봐도 순서대로 저장하는구나 정도는 알 수 있을 것이다. 여기서 눈여겨 봐야할 것이 cli와 sti인데, cli는 인터럽터를 불가시키는 명령이고 sti는 인터럽터를 가능시키는 명령이다.

 

 왜 이렇게 하는 것일까? 그것은 혹시나 태스크를 저장하는 과정에서 다시 스위칭이 일어나서 다시 또 저장하게 되고 그 저장하는 와중에 또 스위칭이 일어나는 계층적으로 계속 저장되는 상황이 오면 스택이 계속 깊어지기때문에 그것을 막기위해서 넣어놨다.(사실 그렇게 많이 깊어지지는 않는다. 워낙 간단한 코드라서 ㅡ_ㅡ;;; 시간이 별로 안걸리기 때문이다.),

또 다른 이유는 타이머 인터럽트 내와 유저 코드 내에서 마음 껏 호출 하기 위해서이다. 인터럽터 불가로 하지 않으면 유저 코드에서 스위칭하는 동안에 타이머 인터럽트에 의해서 다시 스위칭 될 수 있다.

 

3.1 기존 태스크 스위칭의 문제점

 그런데 무엇인가 이상한 생각이 들지 않는가? 태스크 스위칭 루틴에서 보면 인터럽트를 조작하는 부분에서 이상한 느낌이 들지 않는가? 만약 복원되는 태스크의 인터럽트 가능/불가 상태가 불가로 저장된 상태였다면? 복원된 후에는 가능으로 바뀐다는 이야기? @0@)/~~!!

 그렇다. 이 방식의 문제는 일단 태스크가 복원되고 나면 인터럽터가 가능이 된다는 문제가 있다. 그럼 어떻게 해야 될까? 간단한 해결방법은 인터럽트 및 각종 상태에 대한 정보를 가지고 있는 FLAG 레지스터의 값도 같이 저장했다가 복원하면 된다.

 이 방법을 사용하기 위해서 스택에 저장하는 컨텍스트 부분과 스위칭 어셈블리어 함수 부분에 약간의 수정을 해주어야 한다.

 

3.2 스택(Stack)의 내용 수정

 스택에 EBP와 EAX 레지스터 사이에 EFLAG 레지스터를 저장하는 공간을 확보한 뒤에 태스크를 생성할때 디폴트의 EFLAG 레지스터 값을 넣어준다.

 아래는 변경된 태스크 설정 함수이다.

  1. /**
        TASK 구조체를 설정한다.
    */
    BOOL kSetupTask( PTASK pstTask, void* pfStartAddr, void* pfEndAddr )
    {
        DWORD* pdwStackTop;
  2.     // 구조체를 초기화 한다.
        kMemSet( pstTask, 0, sizeof( TASK ) );
  3.     // Stack의 Top
        pdwStackTop = ( DWORD* ) ( pstTask->vdwStack + ( MAX_STACKSIZE - 1 ) );
  4.     // ESP, EBP, EFLAG, EAX, EBX, ECX, EDX, ESI, EDI 순으로 Push 한다.
        pstTask->vdwStack[ 14 ] = ( DWORD ) ( pdwStackTop - 1 );
        pstTask->vdwStack[ 13 ] = ( DWORD ) ( pdwStackTop - 1 );
        pstTask->vdwStack[ 12 ] = EFLAG_DEFAULT;
  5.     // cs, ds, ss, es, fs, gs는 kernel의 기본값으로 설정해 준다.
        pstTask->vdwStack[ 5 ] = GDT_VAR_KERNELCODEDESC;
        pstTask->vdwStack[ 4 ] = GDT_VAR_KERNELDATADESC;
        pstTask->vdwStack[ 3 ] = GDT_VAR_KERNELDATADESC;
        pstTask->vdwStack[ 2 ] = GDT_VAR_KERNELDATADESC;
        pstTask->vdwStack[ 1 ] = GDT_VAR_KERNELDATADESC;
        pstTask->vdwStack[ 0 ] = GDT_VAR_KERNELDATADESC;
  6.     // 마지막으로 Stack의 Return Address를 pfAddr로 설정해 준다.
        pdwStackTop[ -1 ] = ( DWORD ) pfStartAddr;
        // 만약 Task 종료 함수가 설정되지 않으면 Default를 설정
        if( pfEndAddr == NULL )
        {
            pdwStackTop[ 0 ] = ( DWORD ) pfEndAddr;
        }
        else
        {
            pdwStackTop[ 0 ] = ( DWORD ) kTaskEnd;
        }
        return TRUE;
    }

 

3.3 태스크 스위칭 코드 수정

 기존의 코드는 EFLAG에 대한 부분이 없었다. 따라서 이부분에 대한 저장 및 복원에 대한 코드를 추가하고 마지막으로 sti 부분을 제거한다.

  1. ;Task Switching을 수행한다.
    _kSwitchTask
     push eax
     ; General Register
     ; ESP, EBP, EAX, EBX, ECX, EDX, ESI, EDI의 순서로 Push한다.
     
     ; Current Task의 저장
     mov eax, dword [ss:esp + 8]
     push ebx
  2. ; EFlag의 계산
     pushfd
     mov ebx, dword [ss:esp]
     mov dword [eax + 48], ebx
     add esp, 4
  3.  cli
     ; esp의 계산
     mov ebx, esp
     add ebx, 8
     mov dword [eax + 56], ebx
     pop ebx
     mov dword [eax + 52], ebp
     mov dword [eax + 40], ebx
     mov dword [eax + 36], ecx
     mov dword [eax + 32], edx
     mov dword [eax + 28], esi
     mov dword [eax + 24], edi
     ; eax를 집넣는다.
     mov ebx, eax
     pop eax
     mov dword [ebx + 44], eax
  4.  ; Segment Register
     ; cs, ds, ss, es, fs, gs의 순서로 Push한다.
     mov ax, cs
     mov dword [ebx + 20], eax
     mov ax, ds
     mov dword [ebx + 16], eax
     mov ax, ss
     mov dword [ebx + 12], eax
     mov ax, es
     mov dword [ebx + 8], eax
     mov ax, fs
     mov dword [ebx + 4], eax
     mov ax, gs
     mov dword [ebx + 0], eax

  5.  ; Next Task의 복원
     mov eax, dword [ss:esp + 8]
     mov esp, eax
  6.  ; segment register
     pop eax
     mov gs, ax
     pop eax
     mov fs, ax
     pop eax
     mov es, ax
     pop eax
     mov ss, ax
     pop eax
     mov ds, ax
     ; cs의 값은 지금 쓰지 않는다.
     pop eax
  7.  ; General Register
     pop edi
     pop esi
     pop edx
     pop ecx
     pop ebx
     pop eax
     popfd
     pop ebp
     pop esp
     ret

 

 

4.마치면서...

 이상으로 태스크 스위칭에 대해서 알아봤다. 아.. 생각보다 글 쓰는데 시간이 많이든다.. ㅜ_ㅜ

 

 뭐 여튼 오늘 코드 테스트한 기념으로 스크린샷 한방...

 간단한 Shell과 콘솔 아래쪽에 EdgeDraw Task가 동시에 동작하고 있는 화면이다.

Multi_Task.PNG

 

 

 

5.첨부

 

이 글은 스프링노트에서 작성되었습니다.

+ Recent posts