Zephyr v3.7.0 LTS3 · 소스 대조 2026-09-24

Zephyr Thread / Scheduler: 실행 가능 상태부터 문맥 전환까지

스레드를 만들었다고 곧 실행되는 것은 아닙니다. 대기 원인이 사라지는 과정, 우선순위 비교, 선점 허용 여부를 나눠서 읽습니다.

분석 기준: Zephyr v3.7.0. 아래의 두 스레드 예시는 단일 CPU, 일반 선점형 스레드를 가정합니다. SMP와 meta-IRQ 예외를 따로 설명합니다. 소스 분석용 버전이며 제품의 최신 보안 패치를 권하는 표기가 아닙니다.

사용 예제와 예상 타임라인은 직접 작성한 설명용 예시입니다. upstream 발췌에는 파일·행 번호를 별도로 표시했습니다.

1. Ready와 Running은 다르다

CPU가 한 개라면 어떤 순간 실제로 실행하는 스레드는 하나입니다. 실행 가능한 스레드는 여러 개일 수 있습니다. 스케줄러는 그중 우선순위가 높은 후보를 고른 뒤 현재 스레드를 교체해도 되는지 판단합니다. 세마포어를 기다리는 스레드는 우선순위가 높아도 후보가 아닙니다.

그림 1. 생성에서 실행까지의 조건
  1. 생성: k_thread와 stack 준비엔트리·인수·초기 문맥을 설정합니다. 이때 PRESTART 상태로 초기화합니다.
  2. 시작 조건 충족K_NO_WAIT이면 바로 시작 경로, 지연 시작이면 timeout, K_FOREVER이면 명시적 시작을 기다립니다.
  3. 실행 가능 여부 판단실행을 막는 state bit와 active timeout이 없어야 합니다.
  4. 우선순위와 선점 조건 비교높은 우선순위 후보가 생겨도 협력형 실행이나 scheduler lock 등의 조건을 구분합니다.
  5. 문맥 전환선택된 스레드의 저장된 레지스터·스택 문맥으로 실행을 이어갑니다.

2. 상태는 범용 5단계가 아니라 비트와 timeout

실제 thread_state 비트 정의

include/zephyr/kernel_structs.h:49-73 · 로컬 원문

/* states: common uses low bits, arch-specific use high bits */

/* Not a real thread */
#define _THREAD_DUMMY (BIT(0))

/* Thread is waiting on an object */
#define _THREAD_PENDING (BIT(1))

/* Thread has not yet started */
#define _THREAD_PRESTART (BIT(2))

/* Thread has terminated */
#define _THREAD_DEAD (BIT(3))

/* Thread is suspended */
#define _THREAD_SUSPENDED (BIT(4))

/* Thread is in the process of aborting */
#define _THREAD_ABORTING (BIT(5))

/* Thread is in the process of suspending */
#define _THREAD_SUSPENDING (BIT(6))

/* Thread is present in the ready queue */
#define _THREAD_QUEUED (BIT(7))

Ready를 결정하는 실제 조건

kernel/include/kthread.h:96-114 · 로컬 원문

static inline bool z_is_thread_prevented_from_running(struct k_thread *thread)
{
	uint8_t state = thread->base.thread_state;

	return (state & (_THREAD_PENDING | _THREAD_PRESTART | _THREAD_DEAD |
			 _THREAD_DUMMY | _THREAD_SUSPENDED)) != 0U;

}

static inline bool z_is_thread_timeout_active(struct k_thread *thread)
{
	return !z_is_inactive_timeout(&thread->base.timeout);
}

static inline bool z_is_thread_ready(struct k_thread *thread)
{
	return !((z_is_thread_prevented_from_running(thread)) != 0U ||
		 z_is_thread_timeout_active(thread));
}
항목뜻구분
_THREAD_PRESTART아직 시작되지 않음k_thread 생성과 실행 시작은 별개입니다.
_THREAD_PENDING객체의 wait queue에서 기다림세마포어 take 등에서 사용합니다.
_THREAD_SUSPENDED실행을 멈춘 상태v3.7.0의 sleep 경로에서도 사용합니다.
_THREAD_DEAD / _THREAD_DUMMY종료 또는 내부 dummy 상태일반 실행 후보에서 제외됩니다.
_THREAD_QUEUEDready queue 포함 여부이 비트 하나가 Ready의 정의는 아닙니다.
ABORTING / SUSPENDING중단·일시정지 진행 중의 제어 비트서로 배타적인 사용자 상태 이름 목록으로 읽지 않습니다.
active timeout예약된 시간 조건위 state 검사와 함께 Ready를 결정합니다.

z_is_thread_ready()에는 별도의 RUNNING enum 검사가 없습니다. Running은 CPU에서 현재 실행하는 관계입니다. 이전의 declared/linked/initialized/ready/failed 그림은 Zephyr thread 상태 머신이 아니므로 이 정의로 대체했습니다.

3. 생성 API와 실제 구현을 구분하기

직접 작성한 사용 예: upstream 발췌 아님

K_THREAD_STACK_DEFINE(worker_stack, 1024);
static struct k_thread worker;

/* worker_entry(void *, void *, void *)는 애플리케이션이 정의합니다. */
k_tid_t tid = k_thread_create(&worker, worker_stack,
    K_THREAD_STACK_SIZEOF(worker_stack), worker_entry,
    NULL, NULL, NULL, 5, 0, K_NO_WAIT);

이 예의 1024바이트는 설명용 크기입니다. 실제 stack 크기는 호출 깊이, 지역 변수, 보호·정렬 요구와 측정 결과로 결정합니다. stack 매크로와 K_THREAD_STACK_SIZEOF를 쓰는 이유는 단순 byte 배열 크기와 커널이 허용하는 stack 영역을 혼동하지 않기 위해서입니다. worker와 stack은 스레드가 사용하는 동안 살아 있어야 합니다.

k_thread_create의 커널 구현

kernel/thread.c:657-674 · 로컬 원문

k_tid_t z_impl_k_thread_create(struct k_thread *new_thread,
			      k_thread_stack_t *stack,
			      size_t stack_size, k_thread_entry_t entry,
			      void *p1, void *p2, void *p3,
			      int prio, uint32_t options, k_timeout_t delay)
{
	__ASSERT(!arch_is_in_isr(), "Threads may not be created in ISRs");

	z_setup_new_thread(new_thread, stack, stack_size, entry, p1, p2, p3,
			  prio, options, NULL);

	if (!K_TIMEOUT_EQ(delay, K_FOREVER)) {
		thread_schedule_new(new_thread, delay);
	}

	return new_thread;
}

이 구현은 ISR에서 호출하지 못하도록 검사하고 z_setup_new_thread()를 실행합니다. delay가 K_FOREVER가 아니면 thread_schedule_new()를 호출합니다. 반환된 tid는 생성된 스레드의 식별자이지 “worker_entry 실행 완료” 증거가 아닙니다. 우선순위에 따라 새 스레드가 호출자의 다음 줄보다 먼저 실행될 수도 있습니다.

4. 후보 선택과 선점 허용은 별도 판단

일반 우선순위는 숫자가 작을수록 높습니다. 보통 음수 범위는 협력형, 0 이상의 범위는 선점형이며 사용 가능한 범위는 설정에 따릅니다. 같은 static priority에서 CONFIG_SCHED_DEADLINE을 켠 경우의 deadline 비교도 별도 조건입니다.

should_preempt: 교체를 허용하는 조건

kernel/include/kthread.h:190-225 · 로컬 원문

static ALWAYS_INLINE bool should_preempt(struct k_thread *thread,
					 int preempt_ok)
{
	/* Preemption is OK if it's being explicitly allowed by
	 * software state (e.g. the thread called k_yield())
	 */
	if (preempt_ok != 0) {
		return true;
	}

	__ASSERT(_current != NULL, "");

	/* Or if we're pended/suspended/dummy (duh) */
	if (z_is_thread_prevented_from_running(_current)) {
		return true;
	}

	/* Edge case on ARM where a thread can be pended out of an
	 * interrupt handler before the "synchronous" swap starts
	 * context switching.  Platforms with atomic swap can never
	 * hit this.
	 */
	if (IS_ENABLED(CONFIG_SWAP_NONATOMIC)
	    && z_is_thread_timeout_active(thread)) {
		return true;
	}

	/* Otherwise we have to be running a preemptible thread or
	 * switching to a metairq
	 */
	if (thread_is_preemptible(_current) || thread_is_metairq(thread)) {
		return true;
	}

	return false;
}

명시적으로 교체를 허용한 경우나 현재 스레드가 대기·중단 상태가 된 경우에는 교체할 수 있습니다. 그 밖에는 현재 스레드가 preemptible인지, 다음 스레드가 meta-IRQ인지 봅니다. 이 helper가 true라는 사실만으로 낮은 우선순위 스레드가 선택되지는 않습니다. 후보 선택은 앞선 ready queue 처리에서 이루어집니다.

상황결과를 읽는 기준
높은 우선순위의 일반 스레드가 ready현재 문맥이 선점 가능한지 확인합니다. 협력형은 일반 스레드가 임의로 빼앗지 못합니다.
하드웨어 ISR 도착협력형/선점형이라는 스레드 분류와 별개입니다. scheduler lock은 IRQ 마스킹이 아닙니다.
meta-IRQ 설정 사용일반 협력형 규칙의 예외입니다. CONFIG_NUM_METAIRQ_PRIORITIES를 확인합니다.
SMP다른 CPU에서 이미 실행하는 후보와 affinity 등을 고려합니다. 단일 CPU 그림을 그대로 적용하지 않습니다.

Linux의 SCHED_NORMAL fair class는 v6.18.37에서 EEVDF 선택을 사용합니다. Zephyr의 이 경로를 Linux fair class의 vruntime 배분과 같은 모델로 설명하지 않습니다. Linux에도 RT scheduling class가 있으므로 “Linux 전체는 fairness, Zephyr 전체는 priority”라는 이분법도 부정확합니다.

5. 세마포어 하나로 상태 변화를 따라가기

count=0인 세마포어를 높은 우선순위 A가 기다리고, 낮은 우선순위 B가 나중에 give한다고 가정합니다. A는 기다리는 동안 CPU를 점유하지 않습니다. B의 give는 A를 ready로 만들지만, ready와 즉시 실행은 같은 말이 아닙니다.

k_sem_take: 즉시 성공, 즉시 실패, 대기

kernel/sem.c:132-164 · 로컬 원문

int z_impl_k_sem_take(struct k_sem *sem, k_timeout_t timeout)
{
	int ret;

	__ASSERT(((arch_is_in_isr() == false) ||
		  K_TIMEOUT_EQ(timeout, K_NO_WAIT)), "");

	k_spinlock_key_t key = k_spin_lock(&lock);

	SYS_PORT_TRACING_OBJ_FUNC_ENTER(k_sem, take, sem, timeout);

	if (likely(sem->count > 0U)) {
		sem->count--;
		k_spin_unlock(&lock, key);
		ret = 0;
		goto out;
	}

	if (K_TIMEOUT_EQ(timeout, K_NO_WAIT)) {
		k_spin_unlock(&lock, key);
		ret = -EBUSY;
		goto out;
	}

	SYS_PORT_TRACING_OBJ_FUNC_BLOCKING(k_sem, take, sem, timeout);

	ret = z_pend_curr(&lock, key, &sem->wait_q, timeout);

out:
	SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_sem, take, sem, timeout, ret);

	return ret;
}

count가 있으면 줄이고 0을 반환합니다. 없고 K_NO_WAIT이면 -EBUSY입니다. 기다릴 수 있으면 z_pend_curr()로 wait queue와 timeout을 설정합니다. ISR에서는 K_NO_WAIT이어야 합니다. 세마포어를 기다리는 순간 scheduler가 실행할 다른 스레드를 고를 수 있습니다.

k_sem_give: 대기자를 깨우거나 count 증가

kernel/sem.c:96-121 · 로컬 원문

void z_impl_k_sem_give(struct k_sem *sem)
{
	k_spinlock_key_t key = k_spin_lock(&lock);
	struct k_thread *thread;
	bool resched = true;

	SYS_PORT_TRACING_OBJ_FUNC_ENTER(k_sem, give, sem);

	thread = z_unpend_first_thread(&sem->wait_q);

	if (thread != NULL) {
		arch_thread_return_value_set(thread, 0);
		z_ready_thread(thread);
	} else {
		sem->count += (sem->count != sem->limit) ? 1U : 0U;
		resched = handle_poll_events(sem);
	}

	if (resched) {
		z_reschedule(&lock, key);
	} else {
		k_spin_unlock(&lock, key);
	}

	SYS_PORT_TRACING_OBJ_FUNC_EXIT(k_sem, give, sem);
}

waiter가 있으면 첫 대기자를 꺼내 반환값 0을 설정하고 ready로 만듭니다. 이 분기에서 count까지 증가시키지는 않습니다. waiter가 없을 때만 limit 이내에서 count를 올립니다. lock으로 count 검사와 대기자 선택을 보호하고, 깨운 경우 reschedule 경로로 연결합니다.

그림 2. 단일 CPU의 A/B 예상 타임라인
  1. A: k_sem_take(..., K_FOREVER)count=0. A가 wait queue에 들어가며 실행 후보에서 빠집니다.
  2. B: CPU에서 실행A의 높은 우선순위는 대기 조건을 해소하지 못합니다.
  3. B: k_sem_give()A를 wait queue에서 꺼내 ready로 만들고 reschedule을 요청합니다.
  4. A: take에서 0으로 복귀B가 일반 선점형이고 다른 제약이 없다면 A가 B보다 먼저 계속 실행할 수 있습니다.

give와 timeout이 경합할 때 “둘 다 성공하고 count도 남는다”고 추측하면 안 됩니다. wait queue·timeout 제거와 wake-up은 커널 동기화 경로를 따릅니다. 실제 결과는 API 반환값과 재현 조건을 함께 기록해야 합니다.

6. yield, sleep, time slice의 차이

k_yield: queue 순서를 바꾸고 다시 선택

kernel/sched.c:1115-1130 · 로컬 원문

void z_impl_k_yield(void)
{
	__ASSERT(!arch_is_in_isr(), "");

	SYS_PORT_TRACING_FUNC(k_thread, yield);

	k_spinlock_key_t key = k_spin_lock(&_sched_spinlock);

	if (!IS_ENABLED(CONFIG_SMP) ||
	    z_is_thread_queued(_current)) {
		dequeue_thread(_current);
	}
	queue_thread(_current);
	update_cache(1);
	z_swap(&_sched_spinlock, key);
}

k_yield()는 현재 스레드를 다시 queue에 넣고 명시적으로 교체를 허용합니다. 그러나 자신보다 높은 또는 같은 우선순위의 다른 후보가 없으면 자신이 계속 실행할 수 있습니다. 낮은 우선순위 worker에게 CPU를 반드시 주려면 yield만 반복해서는 안 됩니다.

동작실행 가능 상태흔한 착각
k_yield()현재 스레드를 여전히 실행 후보로 둠낮은 우선순위에게도 무조건 양보
k_sleep(양의 timeout)대기 시간 동안 실행 후보에서 빠짐CPU를 바쁜 루프로 점유
k_busy_wait()현재 문맥에서 바쁘게 대기sleep과 동일하게 다른 스레드 실행 보장
time slicing설정된 대상의 같은 우선순위 경쟁을 나눔서로 다른 우선순위에 공평한 CPU 시간 보장

이 버전의 z_tick_sleep()는 unready 처리, timeout 등록, suspended 표시와 swap으로 이어집니다. timeout이 만료되면 wake-up 경로에서 다시 실행 가능해집니다. K_FOREVER sleep은 별도 suspend 경로입니다. 시간 분할은 장기 CPU 점유율을 같게 만드는 fairness 계량기가 아닙니다.

7. 증상에서 원인까지 좁히기

증상먼저 볼 항목고칠 방향
낮은 우선순위 worker가 실행되지 않음상위 스레드의 busy loop/yield 반복필요한 사건을 객체 대기로 표현해 실행 후보에서 빠지게 함
give 이후에도 A가 당장 실행되지 않음현재 협력형 여부, scheduler lock, ISR 문맥ready와 실제 switch 시점을 따로 관측
sem_take가 실패K_NO_WAIT, timeout 또는 reset 경합반환값 -EBUSY/-EAGAIN을 구분하고 오류 처리
가끔 stack 손상stack 수명·크기, 함수별 최대 사용량측정과 보호 설정을 확인하고 자동 변수 stack의 조기 소멸 방지

스레드 수를 늘리거나 priority를 무작정 올리기 전에 어떤 대기 조건이 풀리지 않는지 찾습니다. priority는 잘못된 객체 수명이나 race를 고치는 방법이 아닙니다.

8. 재현 조건과 로그를 함께 남기기

검증용 애플리케이션에서 A의 take 직전·반환 직후, B의 give 직전·직후에 단조 증가 시각과 반환값을 기록합니다. 직접 작성한 계측을 넣으면 실행 타이밍이 바뀌므로 출력만으로 모든 race가 사라졌다고 판단하지 않습니다.

기록할 조건이유
정확한 tag, board, toolchain, .configCPU 수와 scheduler backend, tick 설정이 결과를 바꿉니다.
A/B priority와 생성 delay시작 순서와 선점 가능성을 재현합니다.
CONFIG_SMP, CONFIG_PREEMPT_ENABLED, meta-IRQ 설정단일 CPU 설명의 적용 범위를 확인합니다.
반환값과 이벤트 시각ready 요청과 실제 API 복귀를 구별합니다.

이 글은 실제 장비 실행 로그를 제공하지 않습니다. 예상 타임라인은 위 조건을 만족할 때의 코드 해석이며 측정값으로 표시하지 않습니다.

9. 원문과 검증 범위

thread_state 정의, 생성 함수, ready 판단, 선점 조건, 세마포어 take/give, yield 발췌를 v3.7.0 소스와 대조했습니다. 모든 아키텍처의 context-switch assembly나 SMP race를 전수 검증한 것은 아닙니다.