Zephyr v3.7.0 · SMP

Zephyr SMP: 두 CPU의 동시 실행

보조 CPU를 켜는 시점, 공유 ready queue에서 실행 대상을 고르는 조건, 두 CPU가 한 데이터를 만질 때 필요한 잠금을 분리해 읽습니다.

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

이 글은 Zephyr v3.7.0, commit 36940db938a8f4a1e919496793ed439850a221c2를 기준으로 한 원본 설명이다. 두 CPU 예시는 SMP를 지원하는 보드에서 CONFIG_SMP=y이고 실제로 보조 CPU가 시작된 경우다. 모든 보드·설정의 실행 결과를 단정하지 않는다. 그림은 소스 구조를 설명하기 위해 새로 작성했으며 실기 측정값이 아니다.

1. CPU 두 개가 있어도 설정과 시작 상태를 먼저 확인한다

같은 순간 CPU0과 CPU1은 서로 다른 Zephyr 스레드를 실행할 수 있다. 그러나 물리 CPU 수만으로 SMP 동작을 추정하면 안 된다. CONFIG_SMP=n이면 단일 CPU 커널을 빌드하고, CONFIG_MP_MAX_NUM_CPUS는 빌드에서 허용할 CPU 수다. 보드의 지원 범위와 실제 시작된 CPU 수를 함께 확인해야 한다. v3.7.0 SMP 문서는 지원 보드의 기본값을 설명하지만 모든 애플리케이션이 임의의 수로 늘릴 수 있다는 뜻은 아니다.

기존 Thread / Scheduler 글은 ready와 running, 한 CPU의 선점 판단을 중심으로 한다. 여기서는 CPU마다 현재 실행 중인 스레드가 존재하며 공유 데이터는 다른 CPU에서도 바뀔 수 있다는 점을 더한다.

표 1. 빌드할 때 정해지는 범위와 런타임 관찰을 분리하기
조건v3.7.0 의미잘못된 단정
CONFIG_SMPy일 때 SMP 경로를 포함한다. n은 UP 빌드다.칩에 코어가 두 개면 언제나 두 스레드가 병렬 실행된다는 말
CONFIG_MP_MAX_NUM_CPUS빌드에서 사용할 CPU 수의 상한이다. 보드 지원 범위를 넘길 수 없다.이 숫자만으로 모든 보조 CPU가 현재 온라인임을 증명한다는 말
CONFIG_SMP_BOOT_DELAY이 버전의 bg_thread_main()은 이것이 꺼진 경우 부트 중 z_smp_init()을 호출한다.모든 SMP 구성에서 main() 직전 자동 시작한다는 말
CONFIG_SCHED_CPU_MASKCPU 허용 집합 API를 켠다. SCHED_DUMB가 필요하다.CPU별 독립 ready queue가 일반적으로 생긴다는 말

근거: SMP 문서, 구성과 CPU mask, kernel/init.c 439–453행, kernel/Kconfig 121–153행.

2. 보조 CPU는 초기화가 끝난 뒤 시작 관문을 지난다

일반적인 부트 경로에서 초기화와 장치 초기화는 먼저 한 CPU에서 진행된다. bg_thread_main()은 APPLICATION 단계와 정적 스레드 준비를 마친 뒤 조건이 맞으면 z_smp_init()을 호출한다. v3.7.0의 z_smp_init()은 시작 플래그를 내리고, arch_num_cpus()가 알려준 범위의 보조 CPU를 한 개씩 시작하고, 마지막에 플래그를 올린다. 보조 CPU는 arch_cpu_start()가 넘긴 smp_init_top()에서 자신이 켜졌음을 알리고 그 플래그를 기다린다.

그림 1. CPU0 초기화와 보조 CPU 시작 관문 — 일반 부트 경로
CPU0
초기화POST_KERNEL·APPLICATION 및 정적 스레드 준비
z_smp_init()start flag=0; 보조 CPU를 순서대로 시작
시작 허용보조 CPU ready 확인 뒤 start flag=1
SMP 단계·main()두 CPU가 스레드 실행 가능
CPU1
아직 미시작이 그림의 전제에서는 앱 스레드 실행 없음
arch_cpu_start()smp_init_top 진입·ready 알림
플래그 대기초기화 관문이 열릴 때까지 회전
scheduler 진입필요한 CPU별 초기화 뒤 z_swap_unlocked()

각 행은 코드의 순서를 요약한 도식이다. CONFIG_SMP_BOOT_DELAY 등 시작 조건을 바꾸면 이 기본 시간축을 그대로 적용하지 않는다.

표 2. 부팅 때 생기는 세 가지 신호
신호·호출누가 수행하는가확인할 계약
ready_flag보조 CPU의 smp_init_top()CPU 전원이 올라와 callback에 도착했음을 시작 측에 알린다. 곧바로 앱 실행 완료라는 뜻은 아니다.
cpu_start_flagCPU0의 z_smp_init()모든 보조 CPU 시작 시도 후 스케줄러 진입 관문을 연다.
z_swap_unlocked()보조 CPU의 smp_init_top()CPU별 초기화 뒤 다음 실행 스레드 선택으로 넘긴다.

근거: kernel/smp.c 110–163행, 222–240행.

3. 실행 중인 스레드와 ready queue를 같은 칸에 두지 않는다

기본 SMP 경로의 ready queue는 _kernel.ready_q.runq를 공유한다. 각 CPU에는 자기 _current와 idle 스레드가 있다. v3.7.0 kernel/sched.c는 SMP에서 현재 실행 중인 스레드를 queue 내부에 그대로 두지 않는다. 다음 선택 때 현재 스레드와 queue의 최상위 후보를 비교하고, 문맥을 넘기면 이전 스레드를 조건에 따라 다시 queue에 놓는다. 이렇게 해야 하나의 스레드를 두 CPU가 동시에 실행 대상으로 뽑지 않는다.

그림 2. 기본 공유 ready queue와 CPU별 current — 두 runnable 스레드 예시
공유 ready queue
  • 아직 CPU를 점유하지 않은 runnable 후보
  • 우선순위 및 선택 backend
  • 선택·재삽입은 scheduler spinlock 보호
CPU0 · current A

A가 이 CPU에서 실행 중이면 일반 SMP queue 후보와 분리한다.

CPU1 · current B

B도 별도 실행 문맥이다. A와 B가 같은 데이터를 만지면 동기화가 필요하다.

CONFIG_SCHED_CPU_MASK_PIN_ONLY는 CPU별 ready queue를 쓰는 별도 구성이다. 이 예외를 기본 SMP 모델에 섞지 않는다.

표 3. Ready, Running, 선택 이벤트를 구분하기
대상기본 SMP에서의 위치오해하기 쉬운 점
ready 후보공유 ready queue의 우선순위 후보ready이면 곧바로 특정 CPU에서 running이라는 뜻은 아니다.
CPU별 current각 CPU에서 지금 실행하는 스레드SMP에서는 current를 queue에 계속 넣어 중복 선택하지 않는다.
문맥 전환queue 후보와 current를 lock 아래 비교해 선택CPU0의 과거 선택 cache를 CPU1의 변경 뒤에도 그대로 믿지 않는다.
선택적 CPU pin-onlyCPU마다 별도 ready queue를 쓰는 구성일반 SCHED_CPU_MASK와 동일한 구조가 아니다.

근거: kernel/sched.c 90–166행, 202–286행, 916–980행.

4. CPU mask는 실행 가능한 CPU를 제한한다

CONFIG_SCHED_CPU_MASK=y를 선택하면 스레드마다 허용 CPU 집합을 지정할 수 있다. 일반 mask 구성(CONFIG_SCHED_CPU_MASK_PIN_ONLY=n)에서 새 스레드는 모든 CPU가 허용된 상태로 생성된다. 이 구성은 runnable 상태가 아닌 스레드의 mask를 변경할 수 있지만, PIN_ONLY=y이면 규칙이 달라진다. 새 스레드는 CPU0에만 배정되고 mask는 스레드 시작 전에만 변경할 수 있는 속성이다. 이 경우 k_thread_cpu_pin() 등으로 시작 전에 CPU를 정한다. 시작한 뒤에는 blocked·suspended 상태가 되어도 재배정할 수 없다. 두 mask 구성 모두 v3.7.0 Kconfig에서 SCHED_DUMB에 의존한다.

예시의 범위: CPU0에 묶인 T2와 두 CPU에서 실행 가능한 T1이 있을 때, CPU0에서 T1·CPU1에서 idle인 배치가 일시적으로 나올 수 있다. 이것만으로 CPU1에 당장 T1을 배치하는 최적성이 보장되지 않는다. 공식 문서는 IPI cascade 옵션을 이 예외와 함께 설명한다.
표 4. CPU mask를 사용할 때 먼저 볼 것
검사원인과 조건확인 위치
mask API가 없음CONFIG_SCHED_CPU_MASK가 꺼져 있을 수 있다.최종 .config와 Kconfig
일반 mask 설정 거부PIN_ONLY=n에서는 runnable 스레드의 mask 변경이 허용되지 않는다.호출할 때의 스레드 상태·반환값
PIN_ONLY 배정 시점새 스레드는 CPU0에 배정된다. 다른 CPU로 묶으려면 k_thread_cpu_pin() 등을 시작 전에 호출한다. 시작 후에는 대기·중단 중에도 mask를 바꾸지 않는다.PIN_ONLY 설정·생성/시작 순서
예상과 다른 CPU 배치허용 집합, 선점, 다른 CPU의 current와 IPI 시점을 함께 본다.마스크·CPU별 실행 기록
CPU별 queue라고 생각함별도의 CONFIG_SCHED_CPU_MASK_PIN_ONLY 구성일 때만 CPU별 runqueue 분기다.최종 .config와 thread_runq()

근거: SMP CPU Mask 문서, kernel/thread.c 616–622행, kernel/Kconfig 121–153행, include/zephyr/kernel.h 885–954행, kernel/sched.c 90–116행.

5. 로컬 인터럽트 마스킹만으로 다른 CPU는 멈추지 않는다

한 CPU에서 인터럽트를 가려도 다른 CPU는 같은 공유 데이터를 읽거나 쓸 수 있다. Zephyr v3.7.0의 k_spin_lock()은 호출 CPU의 인터럽트를 가린 다음 공유 lock 변수의 원자적 취득을 확인한다. 다른 CPU가 같은 lock을 쥐고 있으면 잠시 회전한다. 같은 lock을 다시 얻는 재귀 호출은 금지다. 반환된 key는 짝을 이루는 k_spin_unlock()에 전달해야 한다.

기존 UP 코드의 irq_lock()은 SMP에서 호환을 위해 단일 전역 spinlock을 쓰는 계층으로 구현된다. 따라서 단순한 로컬 마스크 한 명령이라고 보거나 개별 데이터 구조의 lock과 같은 비용이라고 보면 틀린다. k_sem_take()처럼 대기할 수 있는 API는 CPU를 계속 점유하는 spin과 목적이 다르다.

그림 3. 같은 데이터에 접근하는 CPU0·CPU1의 잠금 경계
시간
CPU0
공유 데이터·lock L
CPU1
t0 · 로컬 IRQ만 마스크
CPU0의 ISR은 막힘
L을 취득하지 않았으므로 보호 계약 없음
CPU1은 계속 실행·수정 가능
t1 · k_spin_lock(L)
로컬 IRQ 마스크 + L 취득
CPU0만 L 아래 임계구역 실행
같은 L 요청 시 원자 검사 후 대기
t2 · k_spin_unlock(L)
L 해제 + key로 로컬 IRQ 상태 복원
다른 CPU의 취득 가능
CPU1이 L을 얻으면 실행

그림의 t0는 하드웨어 로컬 IRQ 마스크 개념을 분리한 것이다. Zephyr의 공개 irq_lock()을 SMP에서 호출하면 전역 lock 호환 계층이 더해지므로 t0와 동일하다고 읽지 않는다.

표 5. 목적에 맞는 동기화 선택
도구UP에서SMP에서주의
irq_lock()현재 CPU 인터럽트 마스크전역 spinlock 호환 계층과 중첩 횟수전역 경합·비용을 예상해야 한다.
k_spin_lock(&L)원자 lock 데이터는 생략 가능; 로컬 IRQ 보호로컬 IRQ 마스크 + L의 원자 취득재귀 금지, 짧은 임계구역, 같은 key로 해제
semaphore조건이 없으면 스레드 대기 가능동일한 앱 수준 IPC 계약ISR·timeout·반환값 조건을 API별로 확인

근거: SMP Synchronization 문서, include/zephyr/spinlock.h 155–214행, kernel/smp.c 전역 lock 구현.

6. 다른 CPU를 다시 보게 하는 IPI는 조건부다

CPU0에서 새 스레드가 ready가 되었을 때 CPU1이 idle 또는 다른 실행을 하고 있다면, CPU0의 상태 변경만으로 CPU1이 곧장 새 후보를 고른다고 단정할 수 없다. 아키텍처가 지원하는 스케줄러 IPI는 다른 CPU가 인터럽트 종료 때 다시 스케줄러를 보게 한다. v3.7.0의 원격 스레드 abort/suspend 경로는 대상이 다른 CPU에서 실행 중인지 검사한 다음, CONFIG_SCHED_IPI_SUPPORTED와 directed IPI 지원 여부에 따라 지정 CPU 또는 broadcast IPI를 선택한다.

원격 실행 스레드에 대한 abort/suspend 호출은 먼저 대상 스레드에 중단 요청을 표시한다. 이후 호출자는 대상이 실제로 중단될 때까지 기다린다. 스레드 문맥의 호출자는 대기 상태로 전환되고, ISR 문맥에서는 회전하며 기다리는 경로다. k_thread_abort()가 반환한 뒤에는 대상 스레드가 어느 CPU에서도 실행 중이거나 다시 runnable이 되지 않는다는 API 보장이 있다. 이것은 원격 CPU 자체를 정지시킨다는 뜻이 아니다. k_thread_suspend()도 대상이 원격 CPU에서 실행 중이면 중단될 때까지 기다린다.

공식 문서는 IPI 없는 구현에도 정확성 우회 경로가 있지만 반응 시간·저전력 idle에는 불리할 수 있다고 설명한다. CPU mask가 있으면 한 번의 IPI로 늘 최적 배치가 이루어지는 것도 아니다. CONFIG_SCHED_IPI_CASCADE는 조건부 보정 선택지이며 기본적인 스케줄링 보장을 무조건 강화한다고 쓰지 않는다.

표 6. 사건과 원격 CPU의 관찰 시점
사건필요한 관찰오해하기 쉬운 표현
다른 CPU의 스레드 abort/suspend대상 스레드에 중단 요청을 표시하고, 지원하면 directed/broadcast IPI를 보낸다. 호출자는 대상 스레드의 중단을 기다린다.요청 직후 대상이 멈췄거나, API 반환 후 원격 CPU 자체가 정지했다고 해석
idle 중 새 ready 후보IPI가 있으면 깨움·재선택 가능; 없으면 idle 구현 제약 확인CPU0의 queue 갱신이 자동으로 CPU1을 깨운다는 말
mask로 실행 CPU 제한허용 집합과 현재 CPU들의 후보 선택 결과 확인한 번의 IPI가 항상 최적 배치를 완성한다는 말

근거: SMP Interprocessor Interrupts 문서, v3.7.0 스레드 API의 abort/suspend 계약, kernel/sched.c 415–521행.

7. 보드에서 확인할 때 남길 기록

이 글은 보드 빌드나 두 CPU의 실제 trace를 제공하지 않는다. 특정 제품에 적용하려면 먼저 최종 설정과 보드의 CPU 시작 경로를 확인하고, 다음 사실을 같은 실행에서 기록한다.

  1. Zephyr commit, 보드·SoC, 최종 .config의 CONFIG_SMP·CPU 수·CONFIG_SMP_BOOT_DELAY·scheduler backend·IPI 지원을 함께 적는다.
  2. CPU별 보조 시작·ready·start flag 통과·첫 스레드 실행 사건을 분리해 기록한다. ready_flag만으로 앱 스레드 실행을 판정하지 않는다.
  3. 두 스레드가 서로 다른 CPU에서 실제로 겹쳐 실행한 구간과 공유 데이터 접근을 기록한다. CPU별 clock 값이 바로 비교 가능한지는 플랫폼 계측 조건을 확인한다.
  4. CPU mask를 바꾼다면 최종 .config의 CONFIG_SCHED_CPU_MASK_PIN_ONLY를 먼저 확인한다. n이면 대상이 runnable이 아닐 때 변경하고, y이면 스레드를 시작하기 전에 k_thread_cpu_pin() 등으로 배정한다. 호출 결과와 이후 CPU별 실행 위치를 기록한다.
  5. 임계구역 경쟁은 lock 획득 전후와 대기 시간을 계측한다. irq_lock()을 전역 lock 경합과 분리해 로컬 IRQ 한 줄짜리 비용으로 계산하지 않는다.

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

부팅 설명은 v3.7.0의 kernel/init.c와 kernel/smp.c, ready queue·원격 실행 관찰은 kernel/sched.c, lock은 include/zephyr/spinlock.h와 SMP 문서를 기준으로 했다. 모든 아키텍처의 보조 CPU assembly, 실기 시간, 전력 동작은 검증하지 않았다. 본문 그림의 A·B·T1·T2는 설명용 이름이다.

연결해서 읽기