21.3.2.1.1. `while ((myopt = getopt(argc, argv, "ht:v:")) != EOF)` 루프의 포인터 제어

21.3.2.1.1. while ((myopt = getopt(argc, argv, "ht:v:")) != EOF) 루프의 포인터 제어

PX4 커스텀 모듈의 메인 함수를 설계할 때 핵심이 되는 파싱 루프는 보통 아래와 같이 시작한다.

    int myoptind = 1;
    const char *myoptarg = nullptr;
    int ch;

    while ((ch = px4_getopt(argc, argv, "ht:v:", &myoptind, &myoptarg)) != EOF) {
        // ... switch-case 파싱 로직 ...
    }

이 코드는 표준 POSIX getopt()와 매우 유사하지만, 한 가지 결정적인 차이가 있다. 바로 전역 변수(Global Variable)인 optindoptarg를 절대 쓰지 않고, 굳이 로컬 지역 변수를 포인터(&) 형태로 매번 넘겨준다는 점이다. 여기에는 NuttX라는 RTOS의 치명적인 한계와, 그것을 극복하기 위한 눈물겨운 설계가 숨어있다.

1. px4_getopt 탄생의 비화: 스레드 메모리의 비극

초창기 PX4 시절(혹은 데스크톱 개발 환경), 사람들은 그냥 표준 C 라이브러리의 getopt()를 즐겨 썼다. 이 표준 함수는 옵션 배열 몇 번째 구역까지 읽었는지를 optind라는 전역 변수에 저장해 두고, 옵션 값을 찾으면 그 주소를 optarg라는 이름의 한정된 전역 힙 영역에 캐싱해두었다(Global State).

데스크톱 리눅스에서는 프로세스마다 독립된 빵빵한 가상 메모리(Virtual Memory)를 가지므로 이 방식이 완벽하게 안전하다. 하지만 픽스호크 보드의 기반인 **NuttX 플랫 메모리 모델(Flat Memory Model)**에서는 이 전역 변수를 모든 모듈(스레드)이 공유해버리는 참사가 벌어진다.

만약 조종사가 명령어를 굉장히 빠르게 연달아 날려서, custom_app start -v 5ekf2 start -t 10 이 동시에 실행(스폰)되었다고 상상해보자.

  1. custom_app 스레드가 getopt를 돌면서 전역 변수 optind를 2로 만들었다.
  2. 바로 그 찰나(Context Switch), ekf2 스레드가 getopt를 호출하여 저 공용 전역 변수 optind를 3으로 덮어써 버린다.
  3. 다시 제어권을 돌려받은 custom_app은 엉뚱하게 3번 인덱스부터 배열을 읽기 시작하며 처참한 세그먼테이션 폴트를 낸다.

이것이 바로 임베디드 코딩에서 전역 변수를 썼을 때 벌어지는 악명 높은 **Race Condition(경쟁 상태)**이자 Thread-Unsafe 이슈이다.

2. 상태의 캡슐화(Encapsulation): 포인터 넘기기

이 지옥도를 막기 위해 로컬(Local) 스택에 상태 변수를 격리하는 px4_getopt라는 스레드 안전(Thread-Safe)한 구원자가 탄생했다.

int myoptind = 1; // 내 스레드 스택(Stack) 메모리의 고유 번지수
const char *myoptarg = nullptr;

우리가 만든 이 myoptind라는 지역 변수는 내 함수가 돌고 있는 딱 1개의 TCB(Task Control Block) 스택 영역 안에 존재하므로, 다른 모듈의 스레드가 무슨 짓을 해도 이 값은 절대 침범받지 않는다.

우리는 이 안전한 방어벽의 메모리 주소(&myoptind, &myoptarg)만을 px4_getopt() 함수에 파라미터로 넘긴다. 파서는 C 내부 포인터 산술을 이용해 배열(argv)의 메모리를 훑고 지나가면서, 읽어낸 현재 인덱스 위치를 내 소중한 스택 로컬 변수에만 업데이트해 준다.

3. 루프 종료와 EOF (End Of File) 문자

파서는 단어들을 차례대로 삼키다가, 더 이상 - 로 시작하는 옵션 깃발이 없거나 널 포인터를 만나게 되면 깔끔하게 -1을 반환한다. C 언어 라이브러리에서는 통상 이 -1을 EOF (End Of File) 상수로 매핑하여 쓴다.

!= EOF 조건에 걸려 루프가 깨지고 나면, 우리의 지역 변수 myoptind에는 옵션 덩어리들을 모두 포식하고 난 파서가 마지막으로 뱉어낸 인덱스가 예쁘게 담겨 있다. 우리는 이제 마음 놓고 배열(argv[myoptind])에 접근하여 start인지 stop인지 본 게임의 명령어를 안전하게 조작할 수 있다.

이때, 저 myoptarg에 담긴 텍스트 값을 숫자로 안전하게 바꾸는 과정 역시 함정이 도사리고 있다. 다음 단원(21.3.2.1.2)에서 캐스팅 에러 핸들링에 대해 파고들어 보자.