21.10.2.1.2. work_queue status 명령어에서 큐 대기 시간(Waiting Queue Time)이 길어지는 워커를 찾아 별도의 독립 스레드로 분리(Offloading)하는 리팩토링 전략
스택 오버플로우는 약과다. 임베디드 코딩에서 겪을 수 있는 가장 악랄하고 추적하기 힘든 버그는 바로 ‘다른 녀석 때문에 내가 피해를 보는’ 스케줄링 간섭(Scheduling Interference) 이다.
PX4의 모듈 아키텍처는 효율성을 위해 기본적으로 여러 모듈을 하나의 거대한 공용 방(Work Queue)에 몰아넣고, 하나의 코어 스레드(Thread)가 이 방을 순회하며 각 방에 있는 모듈들의 Run() 함수를 순서대로 한 번씩 쳐주는 협력적 멀티태스킹(Cooperative Multitasking) 방식을 쓴다.
1. 대기 시간(Waiting Queue Time)의 함정
터미널에서 work_queue status를 쳤을 때 나타나는 로그를 다시 한번 소환해보자.
nsh> work_queue status
...
Work Queue: wq:nav_and_controllers (priority: 250)
Threads: 1
Tasks:
fw_pos_control_l1 (1)
navigator (1)
payload_autodrop (1) --> Waiting: 12000 us (WARNING!)
...
나의 payload_autodrop 모듈의 소요 시간(Elapsed Time)은 앞서 perf 명령어 측정에서 단 24us에 불과한 아주 날쌘 코드임이 증명되었다.
그런데 이 모듈이 이 거대한 공용 방(wq:nav_and_controllers)에 들어오는 순간, Waiting(대기 시간)이 무려 12,000us (12ms) 나 찍혀있다.
이것은 내 앞에 줄 서 있는 뚱뚱한 데몬(예: 복잡한 수학 연산을 하는 fw_pos_control 이나 navigator)이 CPU를 잡아먹고 놔주지 않아서, 내 날쌘 모듈이 무려 12ms 동안이나 실행되지 못하고 손가락만 빨고 있었다는 뜻이다.
이렇게 되면 내 FSM 루프는 12ms 늦게 트리거를 쏘게 되고, 시속 60km로 날아가는 드론은 12ms 동안 무려 0.2m나 엉뚱한 곳으로 날아가서 페이로드를 투하해 버리게 된다.
2. 스레드 오프로딩(Thread Offloading) 수술
이 부당한 처사를 견디다 못한 시스템 엔지니어가 내릴 수 있는 최후의 통첩은 코드를 뒤엎어버리는 ’스레드 오프로딩(Offloading)’이다. “저 뚱뚱한 녀석들과 같은 방을 쓰지 않겠다!” 며 내 모듈만의 독방(Custom Work Queue)을 따로 차려버리는 것이다.
우리가 처음에 정의했던 헤더 파일(hpp)의 ModuleBase 템플릿 호출부로 돌아가자.
// [기존 방식: 공용 방에 기생하기 - src/modules/payload_autodrop/PayloadAutoDrop.hpp]
class PayloadAutoDrop : public ModuleBase<PayloadAutoDrop>, public ModuleParams {
...
};
우리는 저주받은 공용 워크 큐에서 탈출하기 위해 펌웨어를 재설계한다.
모듈이 실행될 때 자신만을 위한 전용 워커(Custom Worker) 스레드를 통째로 하나 판 뒤 거기에 기생하도록 C++ 소스 코드 최상단 속성을 비틀어버릴 수 있다.
// [오프로딩 방식: 나홀로 독방 쓰기 - src/modules/payload_autodrop/PayloadAutoDrop.cpp]
// [변경 전]
// int PayloadAutoDrop::task_spawn(int argc, char *argv[]) {
// return px4_task_spawn_cmd("payload_autodrop",
// SCHED_DEFAULT, SCHED_PRIORITY_DEFAULT, 1200, // 스택 크기
// (px4_main_t)&run_trampoline, (char *const *)argv);
// }
// [변경 후: 워크 큐 생성 함수 자체를 우회]
int PayloadAutoDrop::task_spawn(int argc, char *argv[]) {
// px4_work_queue 모듈군에 편입되는 대신,
// 아예 리눅스/NuttX 스레드를 하나 새로 창조(Spawn)해서 그 안에 나를 박아 넣는다.
// * 주의: 스레드 하나를 파는 행위는 픽스호크에 엄청난 리소스(SRAM) 오버헤드를 가져온다.
// 정말로 1ms 단위의 타이밍이 중요한 하드 리얼타임 비전/페이로드에만 이 칼을 빼 들어야 한다.
_task_id = px4_task_spawn_cmd("payload_autodrop",
SCHED_DEFAULT,
SCHED_PRIORITY_CONTROLLER, // 티어를 무려 컨트롤러 급으로 올려버림!
1500, // 전용 스택 할당
(px4_main_t)&PayloadAutoDrop::custom_trampoline,
(char *const *)argv);
// ...
}
이 엄청난 수술(Offloading Refactoring)을 마치고 펌웨어를 다시 굽게 되면, work_queue status 출력창 어딘가에 wq:payload_autodrop 이라는 나만의 위풍당당한 방(Queue)이 생겨난 것을 볼 수 있다.
이제 내 모듈 앞에는 아무도 줄 서 있지 않다. 센서가 트리거 되는 그 찰나의 순간, 즉시 0ms 지연으로 FSM 코드가 불타오른다.
단일 모듈의 성능 영점 조절까지 끝났다.
그렇다면 비행기가 멀쩡히 비행하다가 커널이 멈추어서 추락해 버렸을 때, 그 처참한 기체의 무덤(MicroSD 카드)에서 최후의 순간을 기록한 엑셀 파일(크래시 덤프)을 복원해 JTAG 디버거로 원인을 부검하는 마지막 여정 21.10.3장으로 넘어가 보자.