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

세마포어와 큐의 캡슐화 & 하드리얼타임스케줄링 짚고 넘어가기

by 뽀짝뉴스 2020. 6. 19.

 

예제에 이어서 기초적인 임베디드 시스템 설계에서 알고 넘어가야 할 것들을 짚어보겠습니다.

 

 

[세마포어의 캡슐화]

 

세마포어가 일으킬 수 있는 다양한 버그에 대해 말한 적이 있었죠. 최소한 버그들의 일부는 정확히 규칙을 준수하지 않아서 생긴다는 걸 아실 겁니다. 많은 서로 다른 모듈에서 같은 세마포어를 허용하고 모든 모듈들이 세마포어를 정확히 사용할 것이라 희망하기 때문이지요. 단순히 세마포어와 데이터를 모듈에 숨겨서 보호하는 것만으로 캡슐화시켜서 버그들이 들끓기 전에 뿌리를 뽑을 수 있습니다.

 

세마포어를 캡슐화시키는 코드에 대해서 말해보죠. 원하는 코드가 ISecondsToday 변수를 직접 읽게 하는 것보다는 ISecondsToday 변수를 얻기 위해 ISecondsSinceMidnight 함수를 호출하도록 해서 더 나은 결과를 기대합니다.

 

ISecondsSinceMidnight 함수가 세마포어를 제대로 사용하면, 이 세마포어는 더 이상 버그를 발생시키지 않습니다. 이에 대조되는 예도 있죠. 사방에 세마포어 버그와 공유 데이터가 널려있는 경우입니다.

 

또 다른 예를 들자면 부표를 측정하는 하드웨어는 한 번에 하나의 탱크에서만 읽을 수 있었다는 것을 기억해봅시다. 레벨 계산 태스크나 넘침 감지 태스크는 모두 부표 레벨을 읽기를 원합니다. 부표 하드웨어에서 값을 읽기 위해서 어떻게 코드를 작성해야 하는지 보여주는 예시도 작성해볼 수 있죠. 순진하게 다양한 모듈에서 세마포어를 제대로 사용할 것이라고 믿기보다 세마포어를 모듈에 캡슐화시켜야 합니다. 영악한 것이 아니라 영리한 사람이 되는 겁니다.

 

 

[큐의 캡슐화]

 

큐에 대해서도 빼놓을 수 없겠죠. 마찬가지로 태스크가 다른 태스크로부터 메시지를 받을 때 사용하는 큐도 캡슐화하는 것을 고려해야 합니다. 공유 플래시 메모리를 다루는 코드를 작성했던 때로 돌아가 봅시다. 그 코드는 정확히 읽는 것과 쓰는 것에 대한 요구를 만족하도록 처리합니다. 이것이 핵심이었는데요, 이 코드를 다음 제품에 선적하는 것은 그리 좋지 못한 생각일 겁니다. 이제부터 잠재 버그를 말해볼 테니 어떤지 고민해봅시다.

 

- 어떤 태스크도 플래시 메모리 태스크의 입력 큐에 쓸 수 있기 때문에, 어떤 프로그래머는 FLASH_MSG 구조가 들어 있지 않은 빈 메시지를 보낼 수 있음.

- 모든 사람들이 올바른 구조를 사용한다고 하더라도, 누군가는 eFlashOp 변수에 두 개의 가능한 값 중의 하나가 아닌 다른 값을 넣을 수 있음.

- 누구든 우연히 플래시 태스크에 쓴다고 하면서 잘못된 큐에다가 메시지를 쓸 수 있음.

- 어떤 태스크든 실수로 플래시 태스크의 입력 큐를 소멸시킬 수 있음.

- 플래시 태스크는 잘못된 큐를 통해서 플래시 값을 읽고 그 데이터를 전송할 수 있음. 또 다른 비슷한 예를 떠올려보면 누군가는 잘못된 큐 ID를 보낼 수 있고, 반환 값을 잘못 해석할 수도 있고, 메시지가 보내지기 전에 큐를 소멸시킬 수도 있음. 그 밖에 어떤 일이 있을지 또 모르는 일임.

- 그 외

 

한 예시에는 이 버그들이 보이지 않을 수 있습니다. 큐는 flash.c 모듈 안에 캡슐화되었고, vReadFlash, vWriteFlash, vHandleFlashTask만이 그것을 사용할 수 있습니다. 이들 함수들이 디버깅을 통해서 검증이 된 후에는 다른 태스크들은 sQueFlash 큐를 망가뜨릴 수 없습니다. 플래시 태스크는 이제 다른 태스크에 함수 호출 인터페이스를 제공하고, 다른 태스크들은 그 안에서 flash.c에 있는 특정한 엔트리 포인트를 호출합니다. 여전히 버그가 발생할 가능성이 있긴 하더라도 컴파일러가 다른 태스크가 정확한 인자를 가지고 vReadFlash와 vWriteFlash를 호출했는지를 점검해주기 때문에, 버그가 생기기 어렵게 만듭니다. 독립된 태스크에서 코드가 얼마나 단순화되었는지 알 수 있는 것이죠.

 

이러한 코드를 작성하기 시작할 때 명심해야 하는 유일한 것은 vReadFlash와 vWriteFlash 함수는 플래시 태스크의 컨택스트에서 실행되지 않고 그들을 호출한 태스크의 컨택스트에서 실행되어야 한다는 점입니다. 그러므로, 이들 함수들이 vHandleFlashTask에 있는 플래시 태스크 코드와 데이터를 공유할 때는 모든 코드가 한 모듈에 있는데도 불구하고, 세마포어로 데이터를 공유해야 합니다. 추가적으로 이들 함수들은 재진입이 가능해야 합니다.

 

 

[하드 리얼타임 스케줄링은 어떻게]

 

하드리 얼타임시스템에 대한 자세한 얘기를 하려면 너무 깊게 들어가야 하기 때문에 여기서 다루기는 힘듭니다. 하지만 이번에는 그런 시스템을 설계하는데 필요한 어느 정도의 생각에 대해서 살펴보는 정도는 가능합니다. 하드리얼타임 시스템에서 고려할 분명한 문제는 시스템이 엄격한 제한 시간을 맞추도록 보장해야 한다는 겁니다. 더 보태자면 엄격한 제한 시간을 맞출 수 있는 능력은 빠른 코드를 작성하는 겁니다. 그러나 응용 프로그램을 위해 빠른 코드를 작성하는 것과 리얼타임 시스템을 위해 빠른 코드를 작성하는 것은 크게 다르진 않습니다. 가령 자주 항목들을 검색하고 거의 더하거나 지우지 않는 종류의 시스템을 작성한다고 하는 경우 관련된 서적에서 균형 이진트리 알고리즘을 복사하거나 효과적으로 이런 조건을 충족시킬 수 있는 다른 데이터 구조를 이용할 수 있습니다. 알고리즘에 대한 책은 많이 있으므로 특정한 경우에 자주 호출하는 함수를 어셈블리 언어로 작성할 필요도 있습니다.

 

하드 리얼타임 시스템은 상당히 학구적인 흥미를 불러일으킬만한 소재이기도 합니다. 하지만 문제를 학문적으로 공부하기 위해서 또한 실제로 시스템을 설계하고 동작하는 것을 보장하기 위해서 몇 가지 단순화된 가정이 필요합니다. 학문적인 이론의 관점에서 간단한 종류의 시스템은 매 Tn 시간 단위마다 동작하고, 다음 Tn 시간 단위 전에 동작을 끝내는 태스크 n입니다. 각 태스크에 대해서 최악의 경우 실행시간을 Cn 시간 단위로 정합시다. 태스크 전환 시간은 0이고, 태스크는 세마포어나 이벤트 등으로 대기 상태가 되는 일은 없다고 가정해봅시다. 각 태스크는 우선순위 Pn을 가지고 있습니다. 그러고 나서, 해결해야 할 질문은 각 태스크는 최악의 경우에서도 제한 시간 전에 끝낼 수 있느냐 없느냐 하는 겁니다.

 

좀 더 복잡한 경우를 가정해보면 Tn이 아닌 Dn의 시간 단위를 제한 시간을 정하는 때를 생각해봅시다. 또, 이 시스템에서 각 태스크의 주기는 약간의 변동성과 아니면 지터라고 불리는 Jn을 가지고 있다고 합시다. 추가로 이 시스템에서 태스크가 주기적이지 않고 산발적으로 실행된다고도 해봅시다. 이런 방향으로 계속 여러 경우를 떠올려 볼 수 있겠죠? 가령 설계하려는 시스템의 태스크를 특성화시킬 수 있다면 연구는 시스템이 제한 시간을 만족할지 여부를 결정하는데 도움을 줄 수도 있습니다.

 

but, 이들은 모든 방정식의 하나의 입력은 각 태스크에 대해 최악의 경우의 수행 시간인 Cn입니다. 이런 이유 때문에 무조건 빨리 실행되게 하는 것보다는 예측 가능하게 하는 것이 더 중요하다는 말을 반복하는 겁니다. 이런 것을 어려운 용어로 말하면 가관측성이라고 합니다. 무조건 빨리 실행되는 것보다 실행 시간을 예측해서 제한 시간에 엄격히 맞춘 것을 하드 리얼타임 시스템이라고 하고요. 다소 변동될 가능성을 포함하고 있다면 소프트 리얼타임 시스템이라고 하긴 하는데 딱 잘라서 구분하기는 힘들고 혼재되어 있는 경우가 많긴 합니다. 그렇기 때문에 하드 리얼타임 시스템을 위해서 항상 같은 시간 동안 실행되는 함수나 최소한 최악의 경우에는 언제나 일정한 시간 동안 실행되는 코드를 작성하는 것이 중요합니다. 이런 관점에서 떠올려보면 거의 모든 버퍼가 비어있든지 꽉 차있던지 관계없이, 버퍼를 할당하는 루틴이 같은 시간 동안 수행되는 고정 크기 버퍼 할당 함수가 호출될 때는 메모리가 얼마나 여분이 있는지에 따라 수행 시간이 달라지는 malloc 같은 함수보다 더 좋습니다. 세마포어를 사용하지 않는 태스크가 최악의 경우에도 다른 세마포어를 사용하는 태스크들의 영향을 받지 않기 때문에 더 좋다고 할 수 있습니다.

댓글