포스트

6. RTOS 팩토리 - Kernel 이야기

Embedded Recipes

목차


ⓐ RTOS와 Kernel


  • OS가 없던 시절에는 사용자가 모든 기능을 작성했음
  • 편의를 위해 소프트웨어를 하드웨어에 자동으로 적재하고, 실행해주는 모니터 환경을 만듬
  • 모든 소프트웨어는 I/O를 가지고 있다는 공통점이 있음
    • 이 부분을 모니터링 환경에 아예 집어넣고 사용하기 시작함
  • 태초의 OS는 DOS 형태로서 실행만 해주면 자동으로 메모리에 올려서 실행 가능하게 만들어주고, 특별한 처리과정 없이 I/O를 사용할 수 있도록 함
  • RTOS
    • 임베디드 시스템에서 사용되는 OS
    • 특정 목적을 위한 작은 시스템이다보니, Real Time이라는 단어가 붙어 RTOS가 됨
    • 멀티태스킹과 인터럽트 처리 기능을 가지고 있는 작은 OS
    • RTOS에서 중요한 부분만 따로 떼어 Kernel이라 부르는 부분들이 생겨남
  • Kernel
    • Switching, Scheduling, Memory Management, ISR Management 등
    • Kernel이 활성화 되려면, 커널의 API가 불리던 인터럽트가 불리던 호출되어야함
    • Kernel의 정의와 Micro Kernel, Monolithic Kernel간의 정리 필요
    • 핵심 기능을 하는 부분은 “OS의 커널”이라 부름
      • Micro Kernel은 진짜 중요한 부분만 커널이라 부름
      • Monolithic Kernel은 다른 큰 부분을 포함해서 엮어 커널이라 부름
  • 리눅스는 Monolithic Kernel 형태이므로, 리눅스 자체 = 커널 = OS
  • L4같은 운영체제는 Micro Kernel 형태이므로, Core만 krenel이고, 나머지 Services들은 Server라는 Process 형태로 존재함

ⓑ Embedded Software는 무한 Loop


  • 대부분의 임베디드 소프트웨어는 특정한 목적을 가진 시스템이 대부분
  • 한 개의 소프트웨어가 동작하는게 정석
  • 대부분 무한 Loop를 이용해 구현함 ```c void main ()
    {

   Lamp_init();

   while (1)
   {
      Lamp_on();
      wait (100); /* wait 100uS /
      Lamp_off();            wait (100); /
wait 100uS */
   }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
## ⓒ Task 구조와 Signal
---
- 무한 루프에 기능을 추가하려면 함수를 추가해야함
- RTOS는 Hard / Soft 타입으로 구분됨
	- Hard 타입은 정해진 시간 안에 응답을 주어야하는 소프트웨어. 위성, 미사일등에 사용
	- Soft 타입은 응답을 주기만 하면됨
	- 현재에는 컴퓨팅 파워가 좋아져서 크게 구분하지 않음
- RTOS에 Scheduler 기능을 통해 무한 루프 내의 기능들을 관리하기 시작함
	- Signal을 통해 서로 통신. 함수들끼리 서로 작업상황을 알 수 있음
```c
void **main** ()  
{  
     Make_Ready (Lamp_task());         
     wait (DONE);        /* Lamp task는 init을 하고 DONE을 주고 WORK을 기다리겠지? */  
     Make_Ready (Motor_task());       
     wait (DONE);        /* Motor task는 init을 하고 DONE을 주고 WORK을 기다리겠지? */  
     send (LAMP, WORK)  /* Lamp task야 깜빡여 보렴 */  
    
     return;   /* Good bye forever */  
}  
   
void **Lamp_task** ()  
{  
   
     Lamp_init();  
                                    /* 우선은 초기화는 무조건 하네 */  
     send (main, DONE);   /* wait을 만나면 main으로 돌아가~ */  
  
     while (1)  
     {  
          wait (WORK);        /* 여기서 WORK이라는 signal을 일단 무작정 기다리죠 */  
                                    /* while(1)이니까 한번 깜빡이면 항상 여기서 signal을 기다리죠 */  
                                    /* Signal을 받기만 하면, 일을 시작 할테야 */  
           clear (WORK);       /* WORK을 받았으니 다음번에도 WORK을 받을 수 있도록 초기화 해주자 */  
            Lamp_on();  
            time_wait (100); /* wait 100uS */  
   
            Lamp_off();  
            time_wait (100); /* wait 100uS */  
   
          send (MOTOR, WORK);  /* Motor task야 Motor를 돌려봐 */  
      }  
}  
   
void **Motor_task** ()  
{  
   
    Motor_init();  
                                    /* 우선은 초기화는 무조건 하네 */  
    send (main, DONE);   /* wait을 만나면 main으로 돌아가~ */  
  
     while (1)  
     {  
          wait (WORK);        /* 여기서 WORK이라는 signal을 일단 무작정 기다린다 */  
                                    /* while(1)이니까 한번 깜빡이면 항상 여기서 signal을 기다린다 */  
                                    /* Signal을 받기만 하면, 일을 시작 할테야 */■  
          clear (WORK);       /* WORK을 받았으니, 다음번에도 WORK을 받을 수 있도록 초기화 해 주자 */  
            Motor_on();  
            time_wait (100); /* wait 100uS */  
   
            Motor_off();  
            time_wait (100); /* wait 100uS */  
   
            send (LAMP, WORK);   /* Lamp task야 Lamp를 깜빡여봐 */  
      }  
}

task

  • Task : 스케줄링의 기본 단위. 시스템은 태스크 단위로 일을 나눔
  • Task ∋ Process, Thread
    • 즉, Task는 프로세스 혹은 Thread로 구현됨
    • 프로세스와 쓰레드는 별도로 다룰 예정

ⓓ Task 상태, Task는 Service단위


diagram

  • Task의 State Transition Diagram
    • 커널 레벨에서 관리함. TCB에 포함
      1. Init State : 초기화. 태스크 관리 테이블에 등록됨
      2. Wait State : 대기 상태. 어떤 조건을 만족하지 못해 실행을 대기함
      3. Ready State : 실행될 준비가 된 상태. 모든 조건을 만족하여 대기중
      4. Running : 실행 단계
  • Interrupt
    • Hardware로 부터 진짜 전기 신호가 비정기적으로(Asynchronous) 전달됨

ⓔ Preemptive (선점형) Multitasking 이란 도대체


  • 비선점형 멀티태스킹 방식(Non-preemptive)
    • 하나의 Task가 스케줄러부터 CPU 사용권을 할당 받았을때,
      Task가 자발적으로 반납할 때 까지 강제적으로 빼앗을 수 없는 멀티태스킹 방식
      • 비선점형 멀티태스킹 방식에서 시분할 방식을 이용해 , 모든 Task들이 동시에 실행되는 것 처럼 보이는 테크닉을 사용함
      • nonpreemptive
    • Wait_signal 방식이 비선점형 방식의 한 종류
  • 반대로 빼앗을 수 있는걸 선점형(Preemptive)이라고 함
    • 선점이라 먼저 점령하고 있으니 못뺏는게 아니라,
      B가 쓰고있는걸 A가 빼앗아 쓸 수 있다(선점 보다는 강탈)는걸로 이해하면 쉬움 preemptive
    • 선점 방식은 어떻게 정하느냐에 따라 다름. 우선순위, 시간 등 다양한 조건
    • Interrupt를 통해 끝남을 알림

ⓕ Context Swtiching과 TCB - Task의 상태변화


  • Kernel이 어떻게 Task를 관리하는가
  • 모든 Task는 자신만의 Stack과 TCB를 가짐
    • TCB는 Task를 제어하기 위해 Task의 정보를 저장해놓은 자료 구조
    • 스케쥴링과 문맥 교환을 위한 정보를 저장 tcb
  • TCB는 각 Task의 우선순위와 Stack Pointer를 가짐
  • 현재 Wait, Ready 상태인 Task의 TCB의 SP는 자기가 어디까지 실행되었는지를 저장해둠
    • Context가 넣어진 그 Stack의 끝(Full Descending Stack)
    • 해당 Task가 실행할 때가 되면, SP가 가리키고 있던 곳의 Context를 그대로 다시 CPU의 레지스터에 복사해서 context switching을 수행
  • TCB의 Status는 스케줄러에게 현재 Task의 상태를 알려주는 flag와 비슷하게 수행
    • Status에 두가지 구분을 두면 그 상태를 구분할 수 있음
    • Task_A()와 Task_B()가 있을 때, wait_signal과 receive_signal이 존재
      • wait_signal : 해당 Task가 일할 준비가 완료됨
      • receive_signal : B Task가 A Task에게 send_signal()이 된 signal을 A의 receive_signal에 넣어둠
    • Task_A가 receive_signal과 wait_signal 두개의 값이 같으면, wait_signal의 값을 0으로 clear
      • wait_signal이 0인 경우라면 스케줄러가 보기에 signal을 누군가로부터 받았고, 일할 준비가 된 ready 상태라고 판단함

ⓖ TCB - Task Control Block


  • Kernel이 Task들을 관리하기 위한 Meata Data*
    • 메타 데이터 : 데이터의 속성들으 따로 데이터화 해놓은 데이터
      • 예를 들어 워드 파일에서 폰트, 글씨 색상들을 지정하여 사용자에게 주요 정보를 표현. 이런 폰트 정보들을 저장하는 것이 메타 데이터 ```c typedef struct {
           struct task_tcb_struct *next_ptr;
           struct task_tcb_struct *prev_ptr;
        task_tcb_link_type ;

typedef struct task_tcb_struct {
   char                        task_name[200];
   void                         sp;                                  / 스택 포인터 /
   uint32               receive_siganl ;                          /
받은 Signal /
   uint32               wait_signal ;                             /
기다리는 Signal   /
   uint32               pri;                                          /
Task의 Priority /
   task_tcb_link_type    link;                                   /
for TCB list */
task_tcb_type ;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
1. Task_name : 이름. 디버깅 목적으로 사람이 편하게 식별하기 위해 사용
2. sp : Stack Pointer. 각 task가 실행될 때 지역 변수 등을 저장하는 stack 고유 용도로 사용. 혹은 context switching 시에 해당 task의 context를 저장하는 용도로 사용.
3. sigs와 wait : Task끼리 주고 받는 신호들을 저장하여 Task의 state를 표현
4. Link Pointer : TCB를 double linked list로 관리할 수 있도록 함

## ⓗ Scheduler의 구현
---
- 스케줄러가 해야할 일
	1. 다음에 순서를 받을 Task 선정
	2. Context Switching. 현재 실행중인 작업을 저장하고, 다음번 순서의 작업을 가져오기
1. 다음 순서 Task 선정 방법
	- 다양한 방법 존재
	- Priority base인 경우, 우선순위대로 TCB를 linked list로 연결해놓고, head부터 처리
	- Ready Task만 따로 Ready Task List로 관리하는 방법도 존재
2. Context Switching 방법
	1. CPSR를 현재 상태 그대로 저장해야함
	2. Context를 제대로 저장해야함
	3. 현재 Task TCB의 SP가 가리키는 stack에 context를 push 한 후, update된 sp를 마지막 tcb에 저장해야함
3. 다음 New Task의 Context를 다시 CPU register에 Load하는 방법
	1. CPSR를 SPSR에 가져옴
	2. Context를 모두 가져옴
	3. SP를 복구

## ⓘ ISR은 어떻게 구현해 - 선점형과 비선점형
---
- IRQ Exception이 발생하여, IRQ Handler로 branch하여, Interrupt를 처리
- Exception vector에 저장된 IRQ_Handler의 주소로 branch.
	- IRQ_Handler 구현 시 pipe line 때문에 돌아갈 때 lr 값에서 -4를 해줘야함. 주의
	- IRQ mode로 들어서는 순간 SP는 IRQ mode의 SP를 가리키고 있음
	- IRQ mode로 들어가면, 
		1. lr 을 lr = lr-4로 보정하기
		2. 이전 mode에서 사용하던 register들을 backup.
- 인터럽트 처리 후 복귀하는 방법
	1. SPSR을 다시 CPSR로 복구. lr을 pc에 넣어준다
- 보통 시스템에 여러 인터럽트가 존재
	- SoC에 Interrupt Controller가 붙어있어, 어떤 Interrupt가 걸렸는지 확인해줌
- IRQ_Handler 내용을 너무 길게 짜면 안된다는 통설 존재
	- 보통 watchdog라는 timer를 통해 시스템이 이상해지는 것을 방지함
- 중첩 ISR은 우선순위를  이용한 Nesting을 많이 사용함

## ⓙ 선점형 Kernel에서 wait, send, clr signal의 구현과 IRQ Handler
---
- Kernel에게 현재 Task가 해야할 일과 상태를 알려주기 위한 Kernel API
	- wait_signal(), send_signal(), clr_signal()을 구현하는 방법
	- wait_signal() : Task가 스스로 CPU 점유권을 놓으면서 특정 Signal을 대기
	- send_signal() : 자신이 아닌 다른 Task에게 signal을 전송하여 작업을 시작하도록 함
	- clr_signal() : 방금 활성화 된 Running Task가 방금 받은 signal을 지워서, 곧바로 다시 받을 수 있는 환경을 만들어주겠다는 의미
![wait](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FG1qUY%2FbtsijQe8JBm%2FyzfA5oWyaAMDeBAOxJkwXK%2Fimg.jpg)
- wait_signal, receive_signal을 이용해서 API 구현
![send](https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FdlYODV%2FbtsikVmDgly%2F6dXoK31eQ80yXzJubJZuk1%2Fimg.jpg)
- send_signal 구현

- clr_signal 구현
```c
void **clr_signal** (uint32 signal)  
{  
   uint32 tmp_signal;  
   
   signal = ~signal;  
   curr_task_tcb->receive_signal = curr_task_tcb->receive_signal & signal;  
   return;  
   
}
  • 원하는 signal만 지우는 bit operation

ⓚ Clock tick ISR - Timer Service


  • Clock Tick ISR : 정해진 시간에 한번씩 걸리는 ISR
    • 특정 시간 후에 어떤 작업을 시킬 때 사용
  • Time service : Clock Tick ISR을 이용한 Kernel Service
  • 보통 타이머 서비스를 사용할 때, 매개변수로 시간을 넘겨줌
  • Clock tick ISR이 한번 불릴 때 마다 clk_register()를 통해서 register된 call back function들의 시간을 정해진 시간만큼 빼줌
  • 시간이 0이 되는 함수들(Expired된)을 Clock tick ISR에서 등록된 call back function을 직접 실행함
  • linked list에 남은 시간 순으로 callback을 연결하는 식으로 구현됨

ⓛ ATOMIC - Critical Section Mutex Semaphore


  • 공유 데이터의 문제 : ISR과 일반 Code가 데이터를 공유하게 되면 문제가 발생함
    • 이를 방지하기 위해 Interrupt_lock을 걺
    • 공유 데이터가 있을때, 반드시 인터럽트가 걸려서는 안되는 영역인 Atomic 영역을 사용해야함
    • Atomic 영역, 혹은 Critical Section이라고 함
    • RTOS는 Kernel service로, Task끼리 발생하는 임계영역 문제를 해결하기 위해 semaphore, mutex 등을 제공함
    • Semaphore : 철도의 신호기라는 뜻. 철로가 하나밖에 없는 구간은 하나의 기차만 이용 가능
      • obtaion, release를 통해, 자원을 얻고, 반납함
      • 여러 Task들이 동시에 대기하고 있을 때, 어떤 Task에게 자원을 줄지 정하는 방법들
        • 우선순위에 따라, 선착순 등 RTOS에 따라 다름
    • Mutex는 1개의 semaphore

ⓜ Interrupt VS. Polling


  • Interrput는 Hardware의 변화를 감지해서 외부로부터의 입력을 CPU가 알아채는 방법
    • 하던 일을 멈추고, 미리 약속된 ISR을 실행하여, 그 변화에 대한 응답을 처리
    • 각 CPU마다 Interrupt를 어떻게 처리하는지가 다름
    • CPU가 다른일을 할 수 있는 상태지만, 다시 해당 task 로 순서가 돌아오려면 시간 차이 발생(overhead)
    • 거의 즉각적으로 반응함
    • Context switching이 필요함
  • Polling은 하드웨어의 변화를 지속적으로 읽어들여, 그 변화를 알아채는 방법
    • 인터럽트와는 다르게, CPU나 RTOS에 의존하지 않고, 무작정 계속 읽어들이면 됨
    • 무한정 확인하고, 완료시 종료
    • CPU가 다른 일을 하지 못하지만, 즉각적으로 상태를 알아낼 수 있음
    • polling loop가 길어질수록, 점점 더 늦게 반응함

ⓝ Queue와 Inter Task Communication


  • Queue를 이용해 제어 Task가 아닌, 다른 Task들도 작업을 지시할 수 있음
  • 작업을 수행하는 Task는 큐에서 하나씩 꺼내어 작업을 수행함
  • 이런 Queue를 각각의 Task마다 가지고 있을 수 있게 하면, Task끼리의 통신을 Queue를 통해 가능

ⓞ DPC나 APC, 그리고 Bottom Half


  • 할 일이 많은 ISR은 어떻게 작성해야하는가
    • ISR중 너무 긴 프로시저는 ‘지금 당장 처리할 부분’과 ‘나중에 처리할 부분’ 으로 두 Stage로 나눈다
  • Deferred Procedure Call, Asynchronous Procedure Call, 또는 Linux에서 말하는 Bottom half
  • Bottom Half = APC + DPC
    • Asynchronous Procedure Call(APC) : 지금 처리하지 않고, 나중에 Task Level로 처리할 수 있도록 처리하는 부분
    • Deferred Procedure Call(DPC) : APC를 직접 처리하는 부분
  • Callback Function을 등록해놨는데, 처리할 내용이 너무 많은 경우 -> Task Level에서 실행하도록 변경
  • 큐를 이용하여, call back function을 / ACP를 처리할 / DPC task의 Queue Command로 보내어 처리
    • ISR routine에서 queue를 이용해서 DPC를 처리할 Task의 queue에 callback function을 넣음

ⓟ Watch dog task


  • 일반적인 임베디드 시스템에는 Watdog라는 하드웨어 타이머가 존재
    • 모든 Task가 제 때 응답할 수 있는지를 체크
    • 문제가 있을경우, 하드웨어적으로 타겟을 리셋시키는 목적
    • 기아 상태, 교착 상태 등을 체크
  • Watchdog timer를 관리하는 Task 존재
    • 모든 Task가 제시간에 report를 보내는지 체크
    • report를 보내지 못한다면 시스템 Lock up, watchdog timer를 expire 시킴
  • 구현 방법
    • 모든 Task가 Timer Callback을 등록하여 report 할 수 있도록 구성
    • 각 Task별로 필요한 시간만큼 할당
      • 각별한 주의가 필요한 Task는 주기를 짧게 설정
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        
        // 다른 Task들
        void 다른모든_task ()  
        {  
             set_timer (호출한 자기 자신, DOG_REPORT_SIG, 자기한테 알맞은 시간);  
             wait_signal(DOG_REPORT_SIG)  
             if (get_signal () == DOG_REPORT_SIG){  
                   send_signal (Watchdog_task, 자기 자신); /* report to watch dog task */  
                   clr_signal (DOG_REPORT_SIG)  
             }  
        }
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        
        // Watchdog Task
        void Watch_dog_task ()  
        {  
          set_timer (호출한 자기 자신, WAKE_UP_SIG, 적당한 주기);  
          wait_signal(WAKE_UP_SIG)  
          if (get_signal () == WAKE_UP_SIG){  
          for (Task 개수){  
           task 시간을 watchdog 일어나는 주기인 "적당한 주기"만큼  빼주고,
          빼준 값이 0보다 작아진다면, 문제 발생!!
          Interrupt_lock() 걸어버리고 While(1) 걸어 버려서  
          Watch dog timer expire 되어 Reset 시킨다  
          report Task 있으면 다시 원래의 interval 값을 setting 준다  
          }  
          clr_signal (DOG_REPORT_SIG)  
          }  
        }
        

ⓠ Bootup 중 Kernel로의 진입 - main() 함수 -


  • 보통 Kernel로의 진입은 main 함수에서 이루어짐
  • main 함수는 기본적인 Hardware setup-CPU, MCU, clock, memory init 이후, 순수하게 software가 동작 가능한 상태가 되었을 때 호출됨
  • kernel_init 함수를 통해 IDLE task를 살리는 일을 수행
    • IDLE task란, 아무것도 ready거나, running이 아닌 상태인 경우, 커널을 점유할 task를 하나 넣는 것
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      
      dword idle_stack [IDLE_STACK_SIZ];     /* Stack for MC task.     */  
         
      int **main** (void)  
      {  
        kernel_init(   
                  &idle_task_tcb,             /* IDLE task TCB */  
                  (void *)idle_stack,        /* IDLE task stack */  
                  IDLE_PRI,                    /* IDLE task PRIORITY */  
                  idle_task,                    /* IDLE task 본체 */  
                  "IDLE_task")  
         return 0;    /* Never Returns : 여기로는 다시는 안돌아와요 */  
      }
      
  • idle_task_tcb : task tcb list에 등록 후, IDLE task를 실행하기 위함
  • idle_stack : tcb에 IDLE task가 사용하게 될 stack
  • IDLE_PRI : 우선순위
  • idle_task : task 본체
  • taskname idletask
  • 다음과 같은 구조로 idle task 작업 수행
    • Kernel에 더이상 Task가 없는 경우, idle task를 수행함
    • 보통은 Battery Saving을 위해, Task가 일을 안할 때는, Power Saving mode로 시스템을 셧다운 시킴

ⓡ Kernel을 Porting 한다는 것


  • Porting : 이미 만들어져 있는 소프트웨어를 타겟에서 동작할 수 있도록 수정한다던가, 특정 루틴을 만든다던가 해서 타겟에서 잘 동작할 수 있도록 만드는 것
  • 이미 만들어져 있는 커널을 타겟에서 제대로 동작할 수 있도록 만듬
  • 리눅스 커널을 예시로 들면, 리눅스 커널은 소프트웨어 그 자체로서 어느 CPU든지 컴파일만 새로 한다면 동작 할 수 있도록 구성되어 있음
    • 범용성이 높ㅇ느 C로 짜여있는 경우가 대부분이므로, C는 많은 CPU에서 C compiler를 제공하므로, 이를 타겟에 맞는 컴파일러로 다시 컴파일만 하면 됨
  • 어느 부분을 작업해야하는가 -> CPU마다 다른 부분들
    1. Interrupt Lock / Unlock
    2. Context Switching 할 때, Backup 해야하는 Context
    3. Stack이 자라는 방향
    4. SWI를 호출하는 방법
    5. Interrupt 걸린 후 처리방법
    6. ARM경우, Mode마다 사용되는 Stack
    7. Watchdog Timer 또한 MCU마다 다름
  • 커널을 포팅하는 작업은 쉬운 작업이 아니므로, 커널이 어떻게 동작하는지, 타겟 CPU가 어떻게 동작하는지를 잘 알아야 함
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.