[OS] Firmware Project (7) - Task
Task는 Process/ Thread와 같다 임베디드 OS에서는 Task가 Process/Thread 역할을 대신한다 즉 모든 프로세스의 처리는 Task단위로 처리가되며 이러한 Task를 관리하기위한 Task control block 등을 만들고 연결해주는 작업이 필요하다.
task를 관리하기위해 OS는 tcb(task control block)을 만들고 이 구조체를 가지고 task 관리를 진행한다.
1. TCB
TCB에는 보통 해당 task의 stack pointer, stack_base정보 등을 가지고 있어 task의 주소 값을 알 수 있다. 그리고 OS입장에서는 이렇게 만들어진 여러 TCB를 배열 상태로가지고 있다가 처리한다. tcb를 처리하는 방법은 tcb안에 bool 변수를 넣어 true/false를 통해 동작하는 tcb인지 아닌지를 확인하는 방법이 있을 수도 있고
아니면 배열 idx를 이용해서 동작하는 tcb만 넣어줌으로써 처리할 수도 있다.
각각의 tcb는 각자 자신과 연결된 task를 관리하는 것이고 task를 처리한다는 것은 하나의 context를 처리하는 것 즉 연결된 함수가 있으며 그 동작을 수행하는 것이다.
마치 new Thread.run({}); 과 같이 Thread 생성함과 동시에 함수 하나를 연결하고 동작하는 것과 같다.
위와 같이 OS는 TCB 배열을 가지고있으며 하나의 Task가 생성되면 해당 Task를 생성하고 주소 값을 미리 잡아둔 Task Stack 영역(TCB 배열)로 지정한다.
즉 Task Stack영역에 생성된 Task를 올리는 것으로 Task를 TCB에 연결한다고 보면된다. 그리고 Task가 동작시키는 fucntion(보통 매개변수로 받음) 의 주소값을 TCB 배열의 PC값으로 올려준다.
이렇게 되면 TCB배열을 통해 Tcb0을 스케줄러가 context로 올려주면 (Process를 running status로바꾸는 것을 의미)
가장 먼저 Task Stack을통해 Task에 접근하고 여기서 PC 레지스터를 통해 함수를 실행할 수 있다.
2. 스케줄러
스케줄러는 말 그대로 Task를 Context로 올리는 역할을 하며 Tcb배열에 여러 개의 task가 동시에 ON일 때 어떤 tcb를 먼저 올릴지를 관리한다.
이러한 방법으로는 round-robin, 최소 시간, task 처리시간, 우선순위 등 다양한 방법이존재한다.
스케줄러의 처리 방식은 만들고자하는 OS의 성격에 따라 다르기 때문에 어떤 기능이 가장 좋다라고 뽑기는 어렵다.
결과적으로 스케줄러 함수는 tcb 배열에 ON된 task들 중 먼저처리할 얘들을 줄세워서 return해주는 함수로 생각하면 된다.
3. Context Switch
가장 기초적으로 생각하면 한 번에 OS가 처리할 수 있는 task는 1개이다. 때문에 task를 동시에 사용하는 느낌을 사용자에게 주거나 혹은 실시간으로 반영되는 여러 task들을 정상적으로 동시에 돌리기 위해서는 위 task들을 짧은 시간내에 번갈아가면서 context해줌으로써 OS가 많은 task를 실시간으로 처리할 수 있도록 해야한다.
이렇게 여러 task를 계속 바꿔가며 처리하는 방법을 context switch라고하며 이 또한 만들고자 하는 OS에 따라서 여러가지 알고리즘으로 구현할 수 있다.
가장 기본적으로는 시분할 시스템으로 일정 시간마다 context switch를 발생 시키는 것이다.
즉 이러한 방법은 timer로 delay를 두고 반복문을 통해 스케줄러 함수를 호출하고 리턴받은 task로 교체해주도록 구현하면 된다.
Context Switch를 하기 위해서는 기존에 동작하고 있던 task를 저장하고 새 task를 올려주고 해당 동작이 끝나면 다시 기존 저장했던 기존 task 정보를 불러와 동작해야 한다.
이는 __attribuite__ { _asm__ } 를 통해 어셈블리어로 작성할 수 있으며 해당 과정은 단순히 PC값과 sp만을 저장하는 것이아니라 기존 task가 사용하던 register정보도 저장해줘야한다. 만약 기존 task가 계산기 동작을 수행하던 중 context switch가 일어났는데 이 때 register값이 정상적으로 저장되지않아 새로운 task에서 해당 register를 사용한다면 다시 기존 task로 돌아왔을 때 기존 작업을 이어서 하는 것은 불가능하다.
그리고 register값을 저장하는 방법은 ((naked))를 이용해서 진행하는 것이 안전하다.
왜냐하면 어셈블리어로 작성을하더라도 컴파일러는 이를 또 한번 해석해서 데이터들을 저장, 처리하기위해 ldr, mov 등의 어셈블리어 구문들이 우리가 구현한 어셈블리어 사이사이에 추가되는데 이 때 기존 task에서 사용하던 register를 이용해서 데이터를 저장 처리할 수 있다.
즉 이렇게 되는 경우 기존 task에서 register에저장해둔 값이 날아가게되므로 컴파일러가 우리가 직접 구현한 어셈블리어 구문만 이용하도록 하기 위해 ((naked))를 사용하여 어셈블리어를 구현하는 것이 코드상 더 안전하다.
- ref
임베디드 OS 만들기