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에서도 바뀔 수 있다는 점을 더한다.
| 조건 | v3.7.0 의미 | 잘못된 단정 |
|---|---|---|
CONFIG_SMP | y일 때 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_MASK | CPU 허용 집합 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()에서 자신이 켜졌음을 알리고 그 플래그를 기다린다.
각 행은 코드의 순서를 요약한 도식이다. CONFIG_SMP_BOOT_DELAY 등 시작 조건을 바꾸면 이 기본 시간축을 그대로 적용하지 않는다.
| 신호·호출 | 누가 수행하는가 | 확인할 계약 |
|---|---|---|
ready_flag | 보조 CPU의 smp_init_top() | CPU 전원이 올라와 callback에 도착했음을 시작 측에 알린다. 곧바로 앱 실행 완료라는 뜻은 아니다. |
cpu_start_flag | CPU0의 z_smp_init() | 모든 보조 CPU 시작 시도 후 스케줄러 진입 관문을 연다. |
z_swap_unlocked() | 보조 CPU의 smp_init_top() | CPU별 초기화 뒤 다음 실행 스레드 선택으로 넘긴다. |
3. 실행 중인 스레드와 ready queue를 같은 칸에 두지 않는다
기본 SMP 경로의 ready queue는 _kernel.ready_q.runq를 공유한다. 각 CPU에는 자기 _current와 idle 스레드가 있다. v3.7.0 kernel/sched.c는 SMP에서 현재 실행 중인 스레드를 queue 내부에 그대로 두지 않는다. 다음 선택 때 현재 스레드와 queue의 최상위 후보를 비교하고, 문맥을 넘기면 이전 스레드를 조건에 따라 다시 queue에 놓는다. 이렇게 해야 하나의 스레드를 두 CPU가 동시에 실행 대상으로 뽑지 않는다.
- 아직 CPU를 점유하지 않은 runnable 후보
- 우선순위 및 선택 backend
- 선택·재삽입은 scheduler spinlock 보호
A가 이 CPU에서 실행 중이면 일반 SMP queue 후보와 분리한다.
B도 별도 실행 문맥이다. A와 B가 같은 데이터를 만지면 동기화가 필요하다.
CONFIG_SCHED_CPU_MASK_PIN_ONLY는 CPU별 ready queue를 쓰는 별도 구성이다. 이 예외를 기본 SMP 모델에 섞지 않는다.
| 대상 | 기본 SMP에서의 위치 | 오해하기 쉬운 점 |
|---|---|---|
| ready 후보 | 공유 ready queue의 우선순위 후보 | ready이면 곧바로 특정 CPU에서 running이라는 뜻은 아니다. |
| CPU별 current | 각 CPU에서 지금 실행하는 스레드 | SMP에서는 current를 queue에 계속 넣어 중복 선택하지 않는다. |
| 문맥 전환 | queue 후보와 current를 lock 아래 비교해 선택 | CPU0의 과거 선택 cache를 CPU1의 변경 뒤에도 그대로 믿지 않는다. |
| 선택적 CPU pin-only | CPU마다 별도 ready queue를 쓰는 구성 | 일반 SCHED_CPU_MASK와 동일한 구조가 아니다. |
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에 의존한다.
| 검사 | 원인과 조건 | 확인 위치 |
|---|---|---|
| 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과 목적이 다르다.
그림의 t0는 하드웨어 로컬 IRQ 마스크 개념을 분리한 것이다. Zephyr의 공개 irq_lock()을 SMP에서 호출하면 전역 lock 호환 계층이 더해지므로 t0와 동일하다고 읽지 않는다.
| 도구 | 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는 조건부 보정 선택지이며 기본적인 스케줄링 보장을 무조건 강화한다고 쓰지 않는다.
| 사건 | 필요한 관찰 | 오해하기 쉬운 표현 |
|---|---|---|
| 다른 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 시작 경로를 확인하고, 다음 사실을 같은 실행에서 기록한다.
- Zephyr commit, 보드·SoC, 최종
.config의CONFIG_SMP·CPU 수·CONFIG_SMP_BOOT_DELAY·scheduler backend·IPI 지원을 함께 적는다. - CPU별 보조 시작·ready·start flag 통과·첫 스레드 실행 사건을 분리해 기록한다.
ready_flag만으로 앱 스레드 실행을 판정하지 않는다. - 두 스레드가 서로 다른 CPU에서 실제로 겹쳐 실행한 구간과 공유 데이터 접근을 기록한다. CPU별 clock 값이 바로 비교 가능한지는 플랫폼 계측 조건을 확인한다.
- CPU mask를 바꾼다면 최종
.config의CONFIG_SCHED_CPU_MASK_PIN_ONLY를 먼저 확인한다.n이면 대상이 runnable이 아닐 때 변경하고,y이면 스레드를 시작하기 전에k_thread_cpu_pin()등으로 배정한다. 호출 결과와 이후 CPU별 실행 위치를 기록한다. - 임계구역 경쟁은 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는 설명용 이름이다.