Zephyr v3.7.0 · 커널과 서비스 소스 분석

Zephyr Logging: 로그 한 줄의 여행

LOG_INF() 한 번이 항상 터미널 한 줄이 되지는 않습니다. 빌드 시 제거, 런타임 선택, 버퍼 대기와 백엔드 출력을 각각 나누어 읽습니다.

본문 기준: Zephyr 3.7.0 LTS3 · 이후 유지보수 패치는 별도입니다.

이 글은 Zephyr v3.7.0, commit 36940db938a8f4a1e919496793ed439850a221c2의 기본 로깅 서비스에 대한 독립 원문이다. 그림은 개념을 설명하려고 새로 그린 것이며 실제 보드의 처리 시간·손실률을 나타내지 않는다. 기본 deferred 경로와 설정으로 바뀌는 예외를 구분한다.

1. 로그 출력은 호출 위치보다 선택된 경로에 달려 있다

Zephyr 로깅은 모듈이나 모듈 인스턴스가 만든 메시지를 frontend, core, 활성 backend로 보낸다. LOG_ERR·LOG_WRN·LOG_INF·LOG_DBG는 심각도별 API다. 실행 흐름을 계측하는 tracing의 이벤트 훅이나 printk() 호출 자체와 같은 개념으로 취급하면, 출력이 없는 원인을 잘못 찾는다.

기존 Build / West는 빌드 절차, Thread / Scheduler는 실행 선택을 설명한다. 이 글은 그 코드가 만든 로그 메시지의 생존 경로를 설명한다. 아래 파이프라인은 CONFIG_LOG=y, 기본 frontend, backend 사용, CONFIG_LOG_MODE_DEFERRED=y를 전제로 한다.

2. 먼저 모듈을 등록하고 컴파일 상한을 본다

한 모듈의 한 소스 파일에서 LOG_MODULE_REGISTER(sensor, LOG_LEVEL_INF)로 이름과 컴파일 때 적용할 레벨을 등록한다. 같은 모듈의 나머지 파일은 LOG_MODULE_DECLARE(sensor, LOG_LEVEL_INF)를 사용한다. 별도 레벨을 지정하지 않으면 CONFIG_LOG_DEFAULT_LEVEL을 쓴다. CONFIG_LOG_OVERRIDE_LEVEL=0인 경로에서는 모듈 레벨과 CONFIG_LOG_MAX_LEVEL을 모두 통과해야 호출이 남는다. 이 조건에서 최대 INFO면 DEBUG 호출은 실행 시 필터를 높여도 되살릴 수 없다.

표 1. 빌드 때 정해져 런타임에 되돌릴 수 없는 조건
설정·선언작용점검할 오해
CONFIG_LOG=n로깅 호출이 빌드에 들어가지 않는다.백엔드를 나중에 켜면 이전 메시지가 나올 것이라는 기대
LOG_MODULE_REGISTER()모듈 이름과 지정된 컴파일 레벨을 한 파일에서 등록한다.같은 모듈의 여러 파일에서 각각 등록
CONFIG_LOG_DEFAULT_LEVEL명시한 모듈 레벨이 없을 때 기본값이다.모든 모듈의 명시 레벨을 덮는다는 생각
CONFIG_LOG_MAX_LEVELoverride가 0인 일반 경로에서 최대 포함 레벨을 제한한다.컴파일에서 빠진 DEBUG를 런타임 필터로 복구
CONFIG_LOG_OVERRIDE_LEVEL문서는 전역 하한으로 설명한다. 고정 v3.7.0 매크로에는 override로 허용한 레벨이 MAX_LEVEL 검사와 별도의 분기를 통과하는 예외가 있다.MAX_LEVEL을 override보다 항상 우선하는 상한으로 단정

근거: 3.7.0 모듈 로깅 문서, Kconfig.filtering 4–62행, log.h 35–78행.

3. 기본 deferred 경로에서 메시지는 다섯 단계를 지난다

아래 그림과 레벨 예시는 CONFIG_LOG_OVERRIDE_LEVEL=0을 전제로 한다. 고정 소스의 Z_LOG_CONST_LEVEL_CHECK에서는 override 허용 분기가 LOG_MAX_LEVEL을 다시 검사하지 않는다. 예를 들어 override=DEBUG, max=INFO여도 이 상수 필터는 DEBUG를 허용한다. 이후 런타임 필터·버퍼·backend 통과나 실제 출력까지 보장한다는 뜻은 아니다.

컴파일에서 살아남은 호출도 곧장 UART로 가지 않는다. 런타임 필터가 켜져 있다면 적어도 하나의 backend가 원하는 수준인지 먼저 확인한다. 기본 frontend는 메시지 설명자·타임스탬프·인수를 묶어 원형 패킷 버퍼에 넣는다. 처리 시점에 core는 활성 backend마다 다시 필터를 확인하고 전달한다. CONFIG_LOG_FRONTEND_ONLY=y 또는 minimal 모드는 이 그림의 저장·backend 경로와 다르다.

그림 1. 기본 deferred 로그가 최종 출력에 이르는 경로

↔ 좁은 화면에서는 왼쪽부터 오른쪽으로 이동해 각 단계와 출력 분기를 읽으세요.

① 소스LOG_INF("ready")
모듈·레벨이 붙은 호출
② 컴파일 단계CONFIG_LOG·모듈 레벨·LOG_MAX_LEVEL
제외되면 메시지 없음
③ 런타임 집계LOG_RUNTIME_FILTERING=y라면 하나 이상의 backend가 원할 때 패킷 생성
④ frontend·버퍼타임스탬프·인수 포장 후 MPSC 원형 패킷 버퍼에 적재
⑤ core 처리후속 문맥에서 패킷을 꺼내 backend별 필터 검사
⑥ 활성 backend허용한 backend가 형식화·출력. 다른 backend는 건너뜀
버퍼가 가득 찼을 때의 누락은 ④에서 결정한다. ③의 필터 탈락과 ④의 버퍼 드롭은 같은 현상이 아니다.

한 backend에 보이지 않아도 다른 backend에는 도착할 수 있다. 이 도식은 사용자 정의 frontend·멀티도메인 링크를 생략한 기본 단일 도메인 경로다.

표 2. “로그가 없다”를 네 사건으로 나누기
관문사건남는 단서
컴파일레벨이 빌드에서 제외바이너리에도 해당 로그 코드가 없다.
runtime 집계어느 backend도 해당 수준을 원하지 않음패킷 자체를 만들 필요가 없다.
deferred 버퍼공간 부족으로 새 메시지 또는 오래된 대기 메시지 드롭드롭 정책·버퍼 크기·처리 속도를 확인한다.
backend별 처리패킷은 있으나 이 backend가 허용하는 상세 수준 밖임다른 backend에는 보일 수 있다.

근거: 3.7.0 Default Frontend, log_core.h 238–269행, log_core.c 462–555행.

4. 런타임 레벨은 backend마다 따로 작동한다

CONFIG_LOG_RUNTIME_FILTERING=y이면 모듈 또는 인스턴스의 필터를 backend별로 조정할 수 있다. 전단에는 backend 요구 수준의 집계값이 있어 패킷을 만들지 결정하고, core는 실제 전달 때 각 backend의 값을 다시 본다. 빌드에 없는 레벨은 여기서도 복구할 수 없다.

설명용 예시: override=0, 컴파일 최대 INFO, 모듈 INFO, backend A는 ERR, backend B는 INFO로 두었다고 가정하자. LOG_INF()는 패킷이 생성되어 B에만 전달된다. LOG_ERR()는 두 곳에 전달된다. LOG_DBG()는 컴파일 상한 밖이므로 양쪽 모두에 전달할 패킷이 없다. A/B는 실제 장치 종류를 뜻하지 않는다.
표 3. 동일한 소스, 서로 다른 두 backend의 수신
호출컴파일집계 필터backend A · ERRbackend B · INFO
LOG_ERR포함패킷 생성수신수신
LOG_INF포함패킷 생성제외수신
LOG_DBG제외검사할 패킷 없음제외제외

근거: 3.7.0 Run-time filtering, log_core.h 집계 필터, log_core.c backend 필터.

5. “호출이 끝남”과 “출력이 끝남”은 모드에 따라 다르다

deferred 모드는 호출할 때 메시지를 적재하고 시간이 드는 형식화·전송을 뒤로 미룬다. 다중 스레드 구성에서는 CONFIG_LOG_PROCESS_THREAD를 켜 내부 처리 스레드를 사용할 수 있고, 그렇지 않으면 앱이 초기화와 log_process() 호출을 맡을 수 있다. CONFIG_LOG_PROCESS_TRIGGER_THRESHOLD와 스레드의 주기 설정은 언제 깨워 처리할지에 영향을 준다. 호출 직후 터미널에 문장이 없다고 해서 실패했다고 단정할 수는 없다.

그림 2. 같은 로그 호출, 서로 다른 처리 시간축

↔ 좁은 화면에서 호출 문맥과 후속 처리 문맥을 가로로 비교하세요.

모드
t0 · LOG_INF 호출 중
t1 · core 처리
t2 · backend 출력
호출자에게 보이는 것
deferred
현재 문맥에서 필터·패킷 생성·버퍼 적재
처리 스레드 또는 수동 log_process()가 적재된 패킷을 꺼냄
core가 활성 backend의 필터를 확인한 뒤 호출
API 복귀와 backend 처리의 선후는 고정되지 않음
immediate
현재 문맥에서 로그 처리 시작
현재 문맥에서 backend 처리
출력 작업이 호출 시간을 늘림
ISR에서 호출하면 그 문맥의 비용도 증가

기본 frontend의 정상 경로에서 필터를 통과하고 버퍼 할당에 성공한 deferred 메시지는 LOG_INF 호출 중에 적재된다. 필터로 제외되거나 버퍼 부족으로 버린 메시지와 panic 경로는 이 시간축에 포함하지 않는다. 적재 직후 처리 스레드를 깨울 수 있으므로 API 복귀 뒤에만 처리된다고 단정할 수 없다. t0~t2는 메시지 처리 단계의 순서이며 실측 지연이나 스레드 실행 순서의 보장이 아니다. backend와 전송 장치가 추가 버퍼를 둘 수 있으므로 이 그림이 물리 매체의 전송 완료를 보장하지는 않는다.

표 4. 로그 처리 위치를 바꾸는 빌드 선택
모드메시지 처리선택할 때 확인할 비용
LOG_MODE_DEFERRED버퍼 적재 후 알려진 처리 문맥에서 backend 호출버퍼 용량·처리 스레드/수동 호출·드롭 정책
LOG_MODE_IMMEDIATE로그를 호출한 문맥에서 동기 처리ISR·고우선순위 호출 비용과 backend 동시 접근
LOG_MODE_MINIMAL표준 로그를 printk() 중심으로 출력컴파일 필터는 되지만 런타임 필터·타임스탬프·비동기 경로는 없음

근거: Kconfig.mode 4–36행, Kconfig.processing 35–85행, log_core.c 154–195행.

6. 버퍼가 찰 때 “새 것”과 “오래된 것” 중 무엇을 잃는가

deferred 경로는 CONFIG_LOG_BUFFER_SIZE 바이트의 원형 패킷 버퍼를 사용한다. 생산 속도가 처리 속도를 잠시 앞서면 패킷 할당이 실패할 수 있다. CONFIG_LOG_MODE_OVERFLOW=n에서는 할당에 실패하면 새 메시지를 떨어뜨리고, y에서는 새 패킷을 위한 공간을 얻으려고 가장 오래된 대기 메시지를 버린다. 스레드 문맥에서 별도 대기 옵션을 쓴 경우에는 기다린 뒤의 결과를 봐야 한다. 이 옵션은 v3.7.0 Kconfig에서 기본 y지만 최종 .config가 기준이다.

그림 3. 가득 찬 패킷 버퍼에 N이 도착한 두 갈래

↔ 좁은 화면에서 overflow 설정별로 어느 메시지가 사라지는지 비교하세요.

OVERFLOW=n · 새 메시지 보류 불가

대기 버퍼가 A·B·C로 꽉 찼다.

ABCN

공간이 없으면 새 N을 드롭. A·B·C는 대기한다.

OVERFLOW=y · 오래된 메시지 축출

같은 조건에서 N을 담으려 한다.

ABCN

필요한 공간만큼 오래된 대기 메시지를 버린다. N이 보관된다면 A는 잃는다.

A·B·C·N은 같은 크기의 패킷과 LOG_BLOCK_IN_THREAD=n을 가정한 설명용 기호다. 실제 축출 개수와 새 메시지 수용 여부는 패킷 크기·버퍼 상태에 달려 있다.

CONFIG_LOG_BLOCK_IN_THREAD=y는 할당에 실패했을 때 스레드 문맥에서 설정된 시간까지 기다리는 선택지다. ISR를 기다리게 하는 옵션은 아니다. 처리 스레드가 쓰는 경로에서 무기한 대기하도록 설정하면 교착 위험이 있으므로, 지연 상한과 드롭 허용량을 실제 부하에서 함께 확인한다.

표 5. 출력 누락이 의심될 때 볼 설정과 결과
증상먼저 볼 것판단할 결과
처음부터 일부 레벨이 없음LOG_MAX_LEVEL·모듈 레벨컴파일 제거인지 구분
backend마다 출력이 다름활성 backend와 각 runtime filter집계 패킷 생성과 개별 전달을 분리
부하 때 오래된/새 로그가 없음LOG_BUFFER_SIZE·LOG_MODE_OVERFLOW·드롭 카운트어느 쪽 메시지가 드롭되는지 확인
늦게 출력되거나 출력 안 됨LOG_PROCESS_THREAD·처리 호출·backend 활성대기 중인 패킷과 최종 전달을 분리

근거: 3.7.0 Log message allocation, Kconfig.processing 13–35행, log_core.c 118–136행 및 630–660행.

7. 보드에서 재현할 때 남길 기록

실제 로그 누락을 평가할 때는 출력 문자열 한 장보다 최종 설정·발행량·처리량·backend를 같은 실행에서 기록해야 한다.

  1. Zephyr commit, 보드, 최종 .config의 LOG·모드·컴파일 최대 레벨·모듈 레벨·런타임 필터 사용 여부를 적는다.
  2. 활성 backend별 레벨과 같은 모듈의 ERR/INF/DBG 호출 결과를 분리해 기록한다. runtime filter만 바꾼 경우와 다시 빌드한 경우를 혼동하지 않는다.
  3. deferred라면 버퍼 크기·overflow·thread block 설정, 처리 스레드 또는 수동 log_process() 호출, 드롭 수를 함께 본다.
  4. CONFIG_LOG_PRINTK로 printk()를 로깅으로 우회했는지 확인한다. 켜져 있으면 deferred 모드에서 printk() 출력도 지연될 수 있다.
  5. 오류 후 log_panic()을 이용한다면 활성 backend에 panic 통지 후 대기 메시지를 flush하고 그 뒤 동기 처리한다는 별도 경계를 기록한다.

근거: 3.7.0 Logging panic 및 Printk, log_core.c 422–453행.

8. 고정 버전 근거와 설명의 경계

이 글은 v3.7.0의 기본 단일 도메인 로거를 기준으로 한다. 사용자 정의 frontend, LOG_FRONTEND_ONLY, 멀티도메인 링크, dictionary 출력은 선택에 따라 흐름이 달라지므로 그림 1에 합치지 않았다. 실제 UART/RTT 속도나 로그 손실률은 보드·백엔드·호출 빈도에 의존하며 여기서 측정하지 않았다.

연결해서 읽기