본문 바로가기
임베디드 오에스(OS)의 용어 정의


뭘 알아야 이해를 하지!

임베디드 오에스 종류는 정말 많이 있답니다. 각 오에스마다 사용되는 커널에는 많은 구성요소들이 있으며 오에스마다 불리는 용어가 대부분 비슷하지요. 그러나 용어는 비슷한데 기능적인 부분에 있어서 헷갈리거나 불리는 용어가 조금 달라서 마치 새로운 것처럼 들릴 때가 있답니다. 그럼 오에스에서 가장 중요한 커널 부분을 살펴 보아요.

태스크는 실행중인 프로그램을 말하며, 스케줄링의 최소 단위이기도 하지요. 5개의 태스크가 실행 중이라고 할 때, 태스크들은 어떤 순서에 의해 실행 시켜야 할지를 결정해야 하는데 바로 스케줄러가 해야 될 일이죠. 알티오에스(RTOS)의 대부분의 스케줄러는 우선순위가 높은 태스크부터 실행하도록 설계 되어 있으며, 동일한 우선순위내의 태스크들은 라운드 로빈이라고 해서 정해진 시간만큼 균등하게 실행을 한답니다. 우선순위 설정은 고정된 우선순위(Static priority)와 동적인 우선순위(Dynamic priority)로 나눌 수가 있어요.
고정된 우선순위는 태스크가 생성될 때 결정되며, 동적인 우선순위는 가장 임박한 데드라인(Deadline)을 가지는 태스크가 가장 높답니다. 하지만 이 방법에는 결정적인 문제가 있는데, 태스크들이 실행 중에 우선순위를 다시 계산하는 것이랍니다. 태스크 실행보다 우선순위 계산하는 시간이 더 많이 걸려서 시피유의 자원 낭비가 될 수 있답니다. 그래서 대부분의 알티오에스는 고정된 우선순위 할당 방법을 많이 사용한답니다. 만약 태스크에 우선순위를 주지 않고도 실행이 될까요? 우선순위 없이 태스크를 실행할 수는 있지만 더 급하게 처리해야 되는 태스크나 다른 태스크 때문에 데드라인을 넘길 가능성이 있는 경우라면 차라리 간단하게 우선순위를 할당하는 것이 더 낫답니다. 우선순위가 이렇게 결정되면 태스크들은 서로 각각의 역할에 맞게 일을 한답니다. 그러다가 인터럽트가 발생하면 인터럽트를 요청한 태스크로 바뀌게 된답니다. A 태스크에서 B 태스크로 전환될 때를 컨텍스트 스위칭(Context switching)이라고 해요. 각각의 태스크는 레지스트 값들, 프로그램 카운트(PC), 스택 등을 포함하는 고유의 컨텍스트(Context)를 가진답니다. 그래서 A 태스크에서 B 태스크로 전환이 됐을 때 태스크의 컨텍스트가 바뀐답니다. 태스크 고유의 컨텍스트를 제외한 다른 글로벌 변수, 정적 변수, 초기화된 변수, 초기화 되지 않는 변수 등의 다른 데이타들은 태스크 사이에서 공유를 하는 공유 자원이 된답니다.

인터럽트가 발생했을 때, A 태스크에서 B 태스크로 전환하는 과정에 대해 알아 보아요.
태스크는 준비(Ready), 대기(Blocked), 실행(Running) 상태가 있어요.

Dorment는 메모리에 존재하지만 아직 실행할 수 없는 상태랍니다. 태스크 생성하자마자 바로 이 상태가 된답니다. Ready는 현재 시피유를 사용하고 있는 태스크를보다 우선 순위가 낮아서 시피유 사용을 기다리고 있는 상태랍니다. Waiting는 어떤 이벤트(semarfore 등)을 기다리고 있는 상태인데, 이벤트가 발생하면 Ready상태로 바뀌고 시피유를 사용할 수 있는 기회가 생긴답니다. Running은 현재 시피유를 사용하고 있는 상태랍니다. ISR(Interrupt Service Routine)은 인터럽트 처리를 위해 실행되는 코드인데, Running 상태인 태스크가 인터럽트 발생하면 시피유는 바로 인터럽트를 처리한답니다.
한가지 예를 들어 볼게요.
휴대폰으로 재미있는 게임을 하고 있는 중이며 A 태스크라고 할게요. 현재 A 태스크는 실행 상태랍니다. 게임 도중에 갑자기 문자 메시지가 왔고 B 태스크라고 하죠. 문자 메시지인 B 태스크가 인터럽트를 발생시켰는데 스케줄러는 가장 먼저 현재 게임 중인 A 태스크의 컨텍스트를 저장한답니다. 그리고 문자 메시지를 알려 주기 위해 인터럽트 벡터 테이블로 점프를 한답니다. 인터럽트 벡터 테이블에서는 문자 메시지 출력하는 프로그램 코드로 가기 전에 B 태스크를 준비 상태로 만들어 놓는답니다. 문자 메시지가 오기 전까지는 B 태스크가 대기 상태였으니깐요. 이제 컨텍스트 스위칭이 A 태스크에서 B 태스크로 전환되고 A 태스크는 준비 상태로 바뀐답니다. 이제 B 태스크는 저장되어 있었던 컨텍스트를 복원하고 준비 상태에서 실행 상태로 바뀌면서 문자 메시지를 보여 주게 된답니다.

일반적으로 RTOS의 태스크는 다음과 같이 만든답니다.

프로그램 소스에 대해 자세히 분석을 해 보아요.
글로벌 변수(Global virable)나 스태틱 변수(Static variable) 같은 경우에는 주로 공유 자원으로 사용된답니다.
공유자원은 주로 A 태스크와 B 태스크의 모두가 사용하는 변수랍니다. 공유 자원은 태스크들 사이에서 데이타를 이동시키거나 두 개 이상의 태스크가 동일한 변수를 접근할 수 있도록 한답니다. 하지만 이 공유 자원은 A 태스크와 B 태스크가 동시에 접근을 하면 안 된답니다. 즉, A 태스크가 공유 자원을 사용 중일 때는 B 태스크는 기다려 줘야 해요. 예를 들면, A태스크는 i++;을 하고 B 태스크는 i--; 한다고 했을 때, 두 개의 태스크가 i 변수를 공유해서 사용한다면 어떻게 될까요? 우선순위에 따라 동작을 하게 되면 엉뚱한 결과가 나올 수가 있지요. A 태스크가 동작할 때 B 태스크 동작하지 않게 한다면 서로의 역할을 충실히 할 수가 있답니다.
A 태크스가 공유변수 접근하는 블록의 시작에서 마지막 블록까지를 실행 끝나고 나면 B 태스크가 공유 자원을 사용해야 하는데, 이것을 뮤추얼 익스크루션(Mutual Exclusion)이라고 부르죠. 그리고 A 태스크 공유 변수 블록 시작에서 마지막 블록까지 영역을 크리티컬 섹션(Critical Section)이라고 해요. 시피유가 크리티컬 섹션 영역에서 동작 중일 때는 컨텍스트 스위칭이 발생하지 않게 해야 한답니다. 그럼 어떻게 하면 컨텍스크 스위칭이 발생하지 않게 할 수 있을까요?
3가지가 있으며, 인터럽트 디세이블(Disable), 스케줄링을 디세이블 그리고 마지막으로 세마포어(Semaphore)를 사용한답니다. 암 코어에서는 IRQ, FIQ 인터럽트가 있는데, A 태스크가 공유 변수를 사용하기 직전에 인터럽트를 디세이블을 시켜 놓으면 되고, 공유 변수 사용이 끝나면 다시 인터럽트를 인에이블(Enable) 시켜 놓으면 됩니다.

Disable IRQ, FIQ Interrupt;
Read/Write the shared variable;
Enable IRQ, FIQ Interrupt;

스케줄링과 세마포어도 인터럽트 방식처럼 사용할 수가 있답니다.

하지만 공유 변수 사용시간이 매우 짧을 경우에는 세마포어 코드 수행시간이 더 많이 걸릴 수가 있으니 인터럽트 방식이나 스케줄링 방식을 사용하는 편이 더 낫답니다.

세마포어는 Key라고 할 수 있어요. 공유 변수를 사용하고자 하는 태스크는 반드시 Key를 확보해야 하며, Key는 태스크 실행이 끝나고 나면 Key 반납을 해서 기다리는 태스크에게 넘겨 준답니다. 세마포어에는 바이너리(Binary)와 카운터(Count) 세마포어 두 가지가 있어요. 바이너리 세마포어는 0, 1로 표현이 되고, 카운터 세마포어는 0, 1, .., n까지 있답니다. 바이너리 세마포어는 오직 한 태스크만 가질 수가 있어요.
세마포어는 뮤추얼 익스크루션 기능 외에 태스크와 태스크간, 태스크와 ISR간의 동기화를 시켜 주는 역할도 한답니다.

TaskA와 TaskB에서 "Embedded World" 문자를 출력시켜 주는 예제랍니다. 여기서 semaphore를 사용하지 않았다면 TaskA에서 "Embedded World" 문자 출력 후 TaskB에서 "Embedded World" 문자를 출력하죠. 하지만 여기서 semaphore를 사용한다면 두 개의 태스크를 동기화 할 수가 있답니다. 즉, TaskB에서 "E"라는 문자 출력 후 TaskA에서도 "E"라는 문자를 출력하고 다시 태스크 B가 다음 문자를 출력하면 태스크 A도 다음 문자를 출력한답니다.
태스크 A에서 세마포어를 얻기 위해서 a-1 실행하지만 현재 세마포어는 '0'이라서 waiting 상태가 되고, 태스크 B로 넘어가죠. 태스크 B는 d-1에서 세마포어를 얻고 e-1을 실행해서 'E' 메시지를 출력해 준답니다. 그리고 f-1에서 세마포어를 넘겨 주면 태스크 A는 재빨리 Running 상태로 바뀌면서 b-1 실행하며 'E' 메시지를 출력해 주죠. 그리고 c-1에서 다시 세마포어를 넘겨 주어 태스크 B로 가서 다음 메시지를 출력해 준답니다. 이 과정을 반복적으로 수행하면서 태스크간의 동기화를 맞출 수가 있게 되죠.

이제 태스크간의 메시지는 어떻게 주고 받는지에 대해 알아보아요.
태스크간의 메시지는 메시지 큐 또는 메시지 메일박스 의해 전달이 된답니다.

두 개의 차이점은 메시지를 하나만 가지고 있을지 와 여러 개를 가지고 있을지 랍니다. 여기서 하나의 메시지는 워드 크기로 구별된답니다.
메시지 메일박스는 하나의 메시지만 저장하기 때문에 데이타 크기가 한계가 있어서 대용량의 데이타를 보내지 않고 간단한 메시지를 태스크에게 전달할 때 사용할 수 있는 방법이랍니다. 보내는 태스크는 한번에 하나의 메시지를 보내기 때문에 만약 메일박스에 이미 다른 데이타가 들어 있다면 waiting 상태 또는 에러 처리를 해야 한답니다. 받는 태스크도 데이타가 메일 박스에 없다면 대기 상태 또는 에러 값을 돌려 받아야 하죠. 그래서 메일 박스는 동기화된 전송 방법이라고 하죠.
메시지 큐는 하나의 메시지가 아닌 여러 개의 메시지 이기 때문에 메시지를 보내기 전에 메시지 크기를 알려 줘야 해요. 고정된 크기로 메시지를 보내거나 가변 크기로 메시지를 보낼 수가 있는데, 오에스마다 두 가지 모두 가능하게 만들거나 한 가지만 가능하게 선택해서 만들 수가 있답니다.

이제 소프트웨어 인터럽트에 대해 알아 보아요. 커널 스케줄러는 태스크의 우선 순위를 기준으로 해서 동작이 되지만 상황에 따라 소프트웨어 인터럽트에 영향을 받아서 일을 하기도 한답니다. 소프트웨어 인터럽트을 사용하는 대표적인 예는 바로 이벤트와 시그널이랍니다.
이벤트(Event)는 하나의 글로벌 변수이며 모든 태스크가 공유하는 공유 데이타라고 보시면 된답니다. A 태스크가 32bit의 플래그(flag)에 비트 값을 설정하고, 이벤트 대기 중인 B 태스크는 원하는 플래그 값이 만들어지면 비로소 B 태스크는 실행하게 되면서 태스크들의 실행 순서를 조절할 수가 있죠.

시그널(Signal)은 임의의 태스크에 소프트웨어 인터럽트를 거는 방법으로 태스크간의 수행하는 일들의 순서를 저장하죠. 이벤트 그룹과는 다르게 태스크마다 하나씩 존재하죠. 시그널은 태스크 내에서 인에이블을 해야 하고, 시그널 핸들러를 등록해 줘야 동작한답니다. 시그널은 사용하는 태스크는 32bit 비트의 플래그(flag)를 세트 해서 전달을 하고, 받아들이는 태스크는 시그널과 일치하는 플래그 값이 하나라도 있으면 동작 시키게 한답니다. 아래와 같은 경우라는 Task 2가 동작하게 되죠.

 

히언등장! 시즌1의 RTOS 팩토리 - Kernel 이야기 챕터를 읽어보시고 오시면 더욱 정리가 잘 될거라고 생각되는군요.

 
 
Linked at 친절한 임베디드 시스템 개발자.. at 2010/08/01 18:16

... 400 모놀리틱 커널(Monolithic kernel)과 마이크로커널(Micro kernel)의 차이점은? 401 임베디드 오에스(OS) 용어정리 402 ARM 프로세서에 uC/OS2 포팅을 위한 준비사항은? 4 ... more

Commented by ruring at 2010/08/02 14:39
나름 정독하면서 용어정리를 해봤습니다. 약간 아리송한게 한번더읽어봐야겠어요 ㅎㅎ
Commented by soto at 2010/08/03 08:10
ㅋㅋ...네

머리속에 있는 것을 글로 표현한다는게 쉽지만은 안네요. ^^;
Commented by star at 2010/08/06 01:40
아... 역시 쉬운 일은 없네요.
잘 보고 갑니다.
Commented by soto at 2010/08/06 09:49
네. 감사요~~!! ㅋㅋ
※ 이 포스트는 더 이상 덧글을 남길 수 없습니다.
친절한 임베디드 시스템 개발자 되기 강좌 글 전체 리스트 (링크) -



댓글





친절한 임베디드 개발자 되기 강좌 글 전체 리스트 (링크) -