21.3.3.1.1. 호스트 OS(Linux/macOS) 내 POSIX 스레드(pthreads)로 매핑되는 과정

21.3.3.1.1. 호스트 OS(Linux/macOS) 내 POSIX 스레드(pthreads)로 매핑되는 과정

우분투 터미널의 가상 NSH 프롬프트(pxh>)에서 사용자가 custom_app start를 입력했다. NSH 파서는 내부 심볼 테이블을 뒤져 우리의 custom_app_main 함수의 포인터(메모리 주소)를 성공적으로 배를 갈라 찾아내었다.

이제 이 함수를 별도의 독립된 생명력을 가진 스레드로 부화시켜야 한다. 하지만 픽스호크 보드(NuttX 커널)에서 쓰던 task_spawn() API는 여러분의 우분투 리눅스에는 존재하지 않는다. 그렇다면 SITL 빌드 시스템은 이 간극을 어떻게 메꾸는 것일까?

이 마법의 비밀은 PX4의 컴파일러가 심어놓은 **하드웨어 추상화 계층(HAL: Hardware Abstraction Layer)의 조건부 매핑(Conditional Mapping)**에 있다.

1. 조건부 컴파일 분기: __PX4_NUTTX vs __PX4_POSIX

PX4 소스 코드를 주의 깊게 들여다보면, 스레드를 생성하는 핵심 코어 부분은 반드시 다음과 같은 C 전처리기 매크로(Preprocessor Macro)로 감싸져 있는 것을 볼 수 있다.

#if defined(__PX4_NUTTX)
    // 픽스호크 펌웨어 빌드 시 컴파일되는 영역
    ret = task_spawn(name, priority, stack_size, entry_point, argv);

#elif defined(__PX4_POSIX)
    // SITL(리눅스/macOS) 빌드 시 컴파일되는 영역
    pthread_attr_t attr;
    pthread_attr_init(&attr);
    // ... 스케줄링 정책 변환 로직 ...
    ret = pthread_create(&thread_id, &attr, (void *(*)(void *))entry_point, argv);
#endif

여러분이 make px4_sitl jmavsim 명령어를 치는 순간, CMake 빌드 시스템은 전체 소스 코드에 강력한 글로벌 매크로인 __PX4_POSIX를 강제로 주입한다. 반대로 make px4_fmu-v6x_default를 치면 __PX4_NUTTX가 주입된다.

이 전처리기 스위치 덕분에, SITL 환경을 흉내 내는 px4 리눅스 프로세스는 런타임에 커널 함수를 찾지 못해 죽어버리는 일 없이, 리눅스가 가장 사랑하는 네이티브 멀티스레딩 라이브러리인 pthreads (POSIX Threads) 로 부드럽게 매핑(Mapping)되는 것이다.

2. pthreads 매핑 시 발생하는 3가지 기묘한 현상

이러한 강제 변환 과정에서 리눅스와 NuttX의 태생적 차이 때문에 몇 가지 흥미로운 부작용(Side Effect) 내지는 동작 변화가 생긴다.

2.1 램(RAM) 스택 사이즈의 무시 (혹은 오버스케일)

NuttX에서 px4_add_module() 매크로의 STACK_MAIN 2048은 스레드의 생사를 가르는 절대적인 메모리 제한이었다.
하지만 우분투 리눅스의 pthreads는 2KB짜리 스택 사이즈를 요청받으면 코웃음을 친다. 데스크톱 OS는 보통 스레드 하나를 만들 때 최소 2MB~8MB의 스택 메모리를 펑펑 할당해버린다.
결과적으로 SITL 환경에서 여러분의 모듈은 가짜 스택 사이즈를 할당받으며, 픽스호크에서는 즉시 스택 오버플로우로 죽어버렸을 무거운 배열(double matrix[100][100]) 할당이 시뮬레이터에서는 아주 멀쩡하게 돌아가는(그래서 개발자를 방심하게 만드는) 착시 현상을 일으킨다.

2.2 우선순위(Priority)의 하향 평준화

픽스호크에서 SCHED_FIFO와 200번대의 높은 PRIORITY 값은 CPU를 절대적으로 훔쳐 올 수 있는 막강한 권력이었다.
하지만 우분투 OS는 일개 일반 사용자 권한으로 실행된 px4 프로세스가 리눅스 커널의 실시간(Real-time) 스케줄러를 함부로 주무르도록 내버려 두지 않는다.
특별히 sudo 권한을 주고 리눅스 커널 파라미터를 조작하지 않는 이상, SITL 환경에서의 모든 스레드는 리눅스의 일반적인 시분할 스케줄러(SCHED_OTHER) 아래 평범한 우선순위로 강등되어 사이좋게 CPU를 나눠 쓰게 된다.

2.3 하나의 부모 프로세스, 수백 개의 자식 스레드

리눅스의 top 명령어나 시스템 모니터를 켜서 px4 프로세스를 검색해 보라. custom_app이나 ekf2라는 프로세스는 절대 나타나지 않는다. 오직 CPU를 20~30%씩 먹고 있는 거대한 px4 부모 프로세스 하나만이 보일 것이다.

하지만 top -H -p <px4의 PID> (스레드 단위 보기) 명령어를 치면, 비로소 pxh> 셸이 pthreads로 쉴 새 없이 쏴올린 수백 개의 모듈 스레드들이 본명을 잃고(px4라는 껍데기 아래) 징그럽게 격동하고 있는 진풍경을 목격할 수 있다.

이처럼 SITL 환경은 코드는 100% 동일하지만 리눅스의 거대한 자원 위에서 돌아가기 때문에, 이 평온한 노트북 위에서 코드가 완벽히 돌았다고 해서 픽스호크 하드웨어에서도 무조건 동작할 것이라 맹신해서는 안 된다.

이제 스레드가 허공(백그라운드)으로 무사히 발사되는 원리를 알았다. 그렇다면 콘솔에서 커맨드를 친 다음 멍하게 기다리지 않고 곧바로 다른 명령어를 칠 수 있게 해주는 NSH의 ‘백그라운드 티켓 발권’ 메커니즘을 다음 21.3.3.2 단원에서 뜯어보자.