본문 바로가기
IT/임베디드 시스템

임베디드 시스템 설계 기본이론2 - 태스크의 수, 우선순위 그리고 캡슐화

by 뽀짝뉴스 2020. 6. 11.

 

[태스크는 몇 개가 적당할까?]

 

임베디드 시스템 설계에서의 1번째 문제는 시스템의 작업들을 RTOS의 태스크로 분배하는 겁니다. 그러면 궁금증이 생기는데 "더 많은 태스크가 좋은 것인가, 더 적은 게 좋은 것인가?"입니다. 대답을 하기 위해서는 많은 수의 태스크를 사용할 때의 장점과 단점을 생각해보면 되겠지요. 우선 장점을 나열해봅시다.

 

- 태스크가 더 많을수록 각각 다른 부분에 있는 시스템의 작업에 대한 응답 시간을 조정하기가 쉬워집니다. 가령 작업을 8개의 태스크로 나누면, 8개의 다른 우선순위 레벨을 할당할 수 있습니다. 더 높은 순위의 태스크에서는 더 좋은 시스템 응답을 얻는 것이 가능합니다. 좋은 응답에 대한 대가는 낮은 순위의 태스크가 대신 치르게 될 것이고요. 모든 같은 작업을 한 태스크에 넣을 경우, 5장에서 다뤘던 라운드 로빈 구조와 비슷한 시스템 응답을 얻게 됩니다. 태스크를 한 개에서 8개 사이를 사용하면, 시스템의 응답 역시 그 사이에서 얻게 될 것입니다.

 

- 태스크가 더 많을수록 시스템은 더 모듈화에 가까워집니다. 시스템이 프린터, 시리얼 포트, 네트워크 접속, 키보드 등을 가지고 있고, 이 모든 장치를 하나의 태스크에서 관리한다면 태스크는 필연적으로 혼잡스럽게 됩니다. 각각의 장치에 대해서 별도의 태스크를 사용하는 것은 코드를 깔끔하게 만들어요.

 

-태스크가 많을수록 시스템은 보통의 경우 데이터를 효과적으로 캡슐화할 수 있습니다. 네트워크 접속이 별도의 태스크에서 관리된다면, 태스크에 있는 코드만이 네트워크 접속 장치의 상태를 나타내는 변수에 접근이 가능합니다.

 

장점 다음에는 단점도 짚어야겠죠. 참고로 길기는 더 깁니다.

 

- 태스크가 많으면 많을수록 시스템은 두 개 또는 그 이상의 태스크들 사이에서 다수의 공유 데이터를 갖게 됩니다. 이건 다시 말하자면 더 많은 세마포어를 요구하고, 결론적으로 세마포어를 관리하기 위해 마이크로프로세서의 시간을 크게 소비하게 하고, 세마포어에 관련된 버그들을 다수 만들어 낸다는 뜻입니다.

 

- 태스크가 더 많을수록 시스템은 한 태스크에서 다른 태스크로 파이프, 메일박스, 큐 등을 통해 메시지를 전송해야 할 필요가 많아집니다. 다시 말해 더 많은 마이크로프로세서 시간이 필요하고 그만큼 버그가 생길 위험이 높습니다. 다른 이유로 같은 문제가 반복적으로 도출되죠.

 

- 각 태스크는 스택을 필요로 합니다. 그러므로 태스크가 많으면 많을수록 스택 공간과 태스크 간 메시지 공간을 위해서 더 많은 메모리를 필요로 합니다.

 

- 매번 RTOS가 태스크를 전환할 때, 중단하려는 태스크의 컨택스트를 저장하고 새로 실행되는 태스크의 컨택스트를 복원하는 동안 어느 정도는 마이크로프로세서의 시간이 소모됩니다. 다른 조건이 같다는 가정하에 많은 태스크를 가진 시스템은 알토스가 더 자주 태스크 전환을 하게 되고 그렇게 흘러가면 시스템이 처리할 수 있는 작업량은 적어집니다.

 

- 태스크가 더 많다는 것은 RTOS를 더 자주 호출한다는 것을 의미합니다. RTOS 회사 들은 자신들이 가진 것이 얼마나 빨리 태스크를 전환할 수 있고, 메시지를 메일 박스에 넣을 수 있고, 이베트를 설정할 수 있는지 등을 홍보에 사용합니다. 아마 실제로 회사들은 그들의 알토스를 정말 빠르게 만들긴 했을 테지만 제품의 고객들은 안에 내장된 RTOS의 성능에는 별로 관심이 없을 겁니다. 일반적인 레이저 프린터 고객은 프린터가 초당 3~4000번이나 태스크 전환을 할 수 있다는 것에는 아무 감동을 받지 못합니다. 그저 프린터가 얼마나 빨리 인쇄를 하고 정확하게 하며 잉크가 번지지 않는지에만 관심이 있죠. RTOS 기능들을 사용하지 않는다면 시스템은 아마 더 빨리 실행될 가능성이 높습니다. 하지만 사용하기로 결정했을 때의 모순은 가장 훌륭한 설계가 때때로 RTOS를 가장 적게 사용해야 한다는 말입니다. 상대적으로 빠른 프로세서인 20 MHz의 인텔 80386에서 실행되는 알토스의 각 서비스 별 타이밍을 살펴봅시다. 매우 짧은 시간을 나타내는 자료가 있지만 0은 아닙니다. 이런 서비스들을 자주 호출하는 건 시스템의 부하가 가중된다는 의미가 되겠죠.

 

가장 고약한 점은 많은 태스크를 갖는 단점은 대부분 스스로 찾아오는데 비해서, 장점을 얻기 위해서는 주의 깊게 시스템 작업을 태스크들로부터 나눠야만 한다는 데에 있습니다. 얻을 수 있는 교훈은 타 조건이 동일할 시 꼭 필요한 개수의 태스크를 사용해야 한다는 겁니다. 정확히 쓸모가 있는 이유를 찾았을 경우에만 추가적인 태스크를 설계에 더해야 합니다.

 

 

[태스크에 우선순위를 주어야 하는 이유]

 

너무 많은 태스크를 사용하는 것에 관해 일반적인 제한을 두기 위해서, 우선 태스크를 더 추가하는 일이 도움이 되는 상황을 찾아봅시다.

 

다른 소프트웨어 구조보다 RTOS 구조의 분명한 장점은 태스크 코드 응답의 향상된 제어를 들 수 있습니다. 따라서 여러 개의 태스크를 갖는 확실한 이유는 빠른 응답을 요구하는 작업에 높은 우선순위를 할당할 수 있다는 겁니다. 가령 우유 저장 창고 감시 시스템에서 창고에 우유가 얼마나 남아 있는지를 계산하는 소모성 작업보다는 직원이 버튼을 누르는 것에 대해 더 빠른 응답을 필요로 합니다. 이런 두 종류의 작업을 위한 코드는 분리된 태스크에 들어가야 하겠죠. 같은 맥락에서 원자로 제어 시스템에서 이상이 발생한 원자로를 멈추게 하는 건 제일 중요하고 긴급한 작업입니다. 이런 일을 위한 코드는 원자로를 멈추게 하기 위해 필요한 모든 작업을 선점하기 위해서 그 코드 만을 위한 제일 우선시 되는 태스크에 할당되어야만 합니다.

 

 

[태스크를 캡슐화해야 하는 이유]

 

종종 시스템의 서로 다른 부분에서 공유되는 하드웨어를 별도의 독립된 태스크들을 통하여 다루는 것이 의미가 있을 수도 있습니다. 레이저 프린터 프런트 패널의 버튼을 다루는 코드는 사용자가에게 응답하기 위해서 프린터의 표지장치를 사용하고, 프린터의 기계장치를 이용해서 종이를 옮기는 코드는 용지가 부족하다거나 용지가 걸렸다는 것을 알리기 위해서 프린터의 표시장치를 사용합니다. 두 부분 모두 표시장치에 직접 쓸 수 있는 경우가 이어진다면 끊임없는 혼란이 일어날 겁니다. 두 부분 각각은 동시에 표시장치에 쓰려고 할 것이고, 서로 다른 메시지들이 읽을 수 없을 정도로 깜빡거리거나, '종이 걸림 부족' 같은 메시지가 뒤엉켜서 나오거나 알아보기 어려운 문자들이 오류처럼 찍혀 나올 겁니다.

 

표시장치 하드웨어를 제어하는 한 태스크는 이런 문제를 해결하는 능력이 있습니다. 시스템의 다른 태스크들이 표시하려는 데이터를 가지고 있을 때는 그 태스크들은 메시지를 디스플레이 태스크에 보낼 겁니다. RTOS는 디스플레이 태스크에 보내진 메시지들이 큐에 정확히 저장되도록 합니다. 디스플레이 태스크에 있는 간단한 논리는 어떤 메시지가 먼저 표시창에 표시되어야 하는지를 결정합니다.

 

동일하게 시스템의 다양한 부분들이 플래시 메모리에 데이터를 저장하려고 하는 경우에는 플래시 메모리 하드웨어를 다루는 하나의 태스크는 시스템을 깔끔하게 만들 수 있습니다. 플래시 메모리에 어떤 데이터를 쓸 때 한동안 플래시 메모리에 읽거나 쓸 수 없다는 사실을 기억하십시오. 이런 태스크가 없다면 어떤 태스크가 플래시 메모리에 값을 쓸 때마다 플래그를 설정해야만 하고, 플래시 메모리가 다시 사용 가능할 때 플래그를 다시 리셋하는 방법을 준비해야만 합니다. 플래시 메모리를 사용하는 각 태스크는 그 플래그 상태를 항상 점검해야만 하고 태스크가 플래시로부터 데이터를 읽고 싶을 때 플래그가 설정되어 있다고 하면 다시 플래그를 리셋하는 방법을 알고 있어야만 합니다. 독립된 플래시 태스크는 내부에 이런 문제들을 숨깁니다.

 

그런 태스크를 위한 코드를 떠올려봅시다. 플래시에 쓰기를 원하는 시스템의 태스크는 FLASH_MSG 구조체를 담고 있는 메시지를 vHandleFlashTask에 보냅니다. vHandleFlashTask 태스크는 FLASH_MSG 구조체에 있는 a_byData의 내용을 iSector 가 가리키는 섹터에 써넣습니다. 플래시로부터 값을 읽고 싶어 하는 태스크는 FLASH_READ라는 값으로 설정된 eFlashOp 변수를 가지고 있는 FLASH_MSG 구조체를 담고 있는 메시지를 vHandleFlashTask에 보냅니다. vHandleFlashTask 태스크는 플래시로부터 받은 데이터를 sQueueResponse 변수에서 정의된 큐에 보낼 겁니다. 플래시 메모리에 값을 쓸 때마다 태스크는 알토스의 시간 지연 함수인 nanosleep을 써서 다시 플래시가 가능할 때까지 태스크를 잠시 기다리게 합니다. 이런 기간 동안에 플래시 메모리에 대한 다른 요구들은 vHandleFlashTask의 입력 큐에서 단순히 순서를 기다리면서 대기합니다.

 

큐를 정의하는 구조체를 나타내는 mdt_q 타입과 mq_open, mq_receive, mq_send 그리고 nanosleep 함수들은 RTOS 인터페이스를 위한 표준인 POSIX 에서부터 온 겁니다. mq_send 함수는 태스크의 지역 변수로부터 데이터를 큐에 넣고, mq_receive 함수는 큐로부터 태스크의 지역 변수에 데이터를 넣습니다.

 

공유 하드웨어를 다루는 독립된 태스크를 갖는 것이 의미가 있는 것과 마찬가지로 공유 데이터를 다루는 독립된 태스크를 갖는 것도 의미가 있습니다. 그런 공유 데이터 구조의 예시를 들면 다수의 태스크들이 에러를 기록할 수 있는 에러 로그가 있습니다. 로그가 별도의 독립된 태스크에 의해서 관리가 된다면, 새로운 에러를 기록하는 다양한 함수들을 한데 모을 수 있고, 로그가 꽉 찼을 때 오래된 데이터를 버릴 수 도 있고, 필요하다면 중첩된 기록을 도려낼 수 있는 등 이점이 많습니다.

댓글