[IT 심층 분석]
임베디드 리눅스의 V4L2 드라이버 프레임 드랍 원인 규명과 최적화 고군분투기
김민준 · IT 시스템 엔지니어|
산업용 리눅스 환경에서 다중 카메라 시스템을 구축하는 엔지니어라면 거의 필연적으로 Video4Linux2 일명 V4L2 드라이버 스택과 마주하게 됩니다. 최근 저희 팀은 무인 자율주행 차량의 사이드 비전을 담당하는 카메라 센서 4개를 동시에 구동하는 과정에서 불규칙하지만 명확한 프레임 유실 문제가 발생하여 시스템 안전성에 큰 위협을 받았습니다. 여러 개의 USB 웹캠이나 이더넷 기반의 카메라가 아님에도 불구하고 보드에 직접 연결된 네거티브 MIPI CSI 통신에서조차 초당 30프레임 중 5프레임 가량이 임의로 사라지는 무시무시한 현상이었습니다. 이 드랍 현상은 차량의 측면 장애물 인식 알고리즘에 엄청난 딜레이를 초래했고 저는 이를 해결하기 위해 무려 한 달간 커널 소스와 씨름해야 했습니다.
가장 첫 번째로 도입한 디버깅 전략은 퍼포먼스 오버헤드를 추적하여 드랍의 발생 지점을 모색하는 것이었습니다. 우선 V4L2 프레임워크가 제공하는 내부 데스 로깅을 활성화하고 커널 내부의 이벤트 링 버퍼를 ftrace를 통해 들여다보았습니다. 데이터를 들여다보는 과정에서 센서 자체에서 전송하는 인터럽트는 한 치의 오차 없이 매우 규칙적으로 제시간에 도착하고 있었습니다. 이는 하드웨어 결함이 아니란 것을 뜻했죠. 문제는 사용자 영역의 애플리케이션 데몬이 커널 영역에서 프레임이 준비되었다는 신호를 poll 함수 등을 통해 대기하고 있다가 데이터를 쓸어담아가는 부분에서 발생했습니다. V4L2 내부 큐에 프레임 버퍼가 한가득 차올라서 더 이상 밀어넣을 자리가 없게 되자 드라이버가 새로 들어온 프레임 데이터를 가차 없이 폐기버리고 있었던 것입니다.
애플리케이션은 왜 제시간에 큐에서 버퍼를 꺼내가지 못했을까요. 근거는 I/O 바운드 병목에 있었습니다. 애플리케이션 스레드는 확보한 프레임 이미지를 즉각적으로 로깅 스토리지 장치로 파일 쓰기를 하고 있었는데 파일 시스템 버퍼가 가득 차 플러시(Flush) 연산이 동기적으로 이루어지는 그 찰나의 순간 스레드가 통째로 멈칫하는 블로킹 상태에 빠진 것입니다. 그 몇 밀리초의 블로킹은 30fps 환경에서는 곧 프레임 한두 개의 유실로 직결됩니다. 이를 해결하는 가장 근본적인 대책으로 프레임을 캡처하는 스레드와 파일 I/O를 담당하는 워커 스레드를 완전히 격리하고 그 사이에 매우 깊고 여유로운 락프리 기반의 링 버퍼(Lock-free Ring Buffer)를 배치하는 아키텍처 수술을 감행했습니다.
나아가 V4L2 스트리밍 파라미터에서도 치명적인 기본값 설정 오류를 발견했습니다. 사용자 앱이 커널에게 요청하는 초기 버퍼의 수용량 곧 버퍼 카운트 할당량이 리눅스 기본 사양에 따라 단 4개에 불과했습니다. 만에 하나 약간의 오차가 발생하더라도 이를 완충시켜줄 수 있는 버퍼 풀이 절대적으로 부족했던 것입니다. 저는 디바이스 메모리의 리저브 영역을 재구성한 뒤 버퍼링 한도치를 16개까지 상향시켜 드라이버에 재차 인가했습니다. 이제 순간적으로 애플리케이션의 응답성이 떨어지는 찰나가 찾아와도 늘어난 큐 공간이 그 타이밍 차이를 부드럽게 감쇠시켜 주게 되었습니다. 수백 킬로미터에 이르는 주행 환경 백업 테스트를 거쳤고 더 이상의 프레임 드랍이나 동기화 깨짐은 발생하지 않았습니다. 현장에서는 단순히 코드 성능을 의심하기 전에 V4L2 커널 큐와 블로킹 I/O 모델의 아키텍처적 한계를 이해하는 시야가 절대적으로 필요하다는 것을 되짚어볼 수 있었습니다.