kernel/services/scheduling/index.html · kernel/services/scheduling/index.rst 공식 원문 ↗

Zephyr 3.7.0 · Kernel · Services

Scheduling과 CPU Idling

Priority와 deadline으로 current thread를 정하고 cooperative·preemptive 실행과 low-power idle을 관리합니다.

Official pathkernel/services/scheduling/index.html
Source filekernel/services/scheduling/index.rst
Source versionZephyr 3.7.0
TranslationFull · reviewed

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

Part 1

요약·해설

세 ready queue backend, wait_q 선택, time slicing과 atomic idle race를 구조화한 흐름도로 설명합니다.

Ready queue

DUMB · SCALABLE · MULTIQ

Execution

cooperative · preemptive

Idle

normal · atomic

Part 2

접을 수 있는 영어 원문 전체

영어 원문 전체 펼치기
원문 SHA-256 E93BDE6E17520C136C72824411EADE02D00909472E44DFB9135EF95524990FF5
.. _scheduling_v2:

Scheduling
##########

The kernel's priority-based scheduler allows an application's threads
to share the CPU.

Concepts
********

The scheduler determines which thread is allowed to execute
at any point in time; this thread is known as the **current thread**.

There are various points in time when the scheduler is given an
opportunity to change the identity of the current thread.  These points
are called **reschedule points**. Some potential reschedule points are:

- transition of a thread from running state to a suspended or waiting
  state, for example by :c:func:`k_sem_take` or :c:func:`k_sleep`.
- transition of a thread to the :ref:`ready state <thread_states>`, for
  example by :c:func:`k_sem_give` or :c:func:`k_thread_start`
- return to thread context after processing an interrupt
- when a running thread invokes :c:func:`k_yield`

A thread **sleeps** when it voluntarily initiates an operation that
transitions itself to a suspended or waiting state.

Whenever the scheduler changes the identity of the current thread,
or when execution of the current thread is replaced by an ISR,
the kernel first saves the current thread's CPU register values.
These register values get restored when the thread later resumes execution.


Scheduling Algorithm
====================

The kernel's scheduler selects the highest priority ready thread
to be the current thread. When multiple ready threads of the same priority
exist, the scheduler chooses the one that has been waiting longest.

A thread's relative priority is primarily determined by its static priority.
However, when both earliest-deadline-first scheduling is enabled
(:kconfig:option:`CONFIG_SCHED_DEADLINE`) and a choice of threads have equal
static priority, then the thread with the earlier deadline is considered
to have the higher priority. Thus, when earliest-deadline-first scheduling is
enabled, two threads are only considered to have the same priority when both
their static priorities and deadlines are equal. The routine
:c:func:`k_thread_deadline_set` is used to set a thread's deadline.

.. note::
    Execution of ISRs takes precedence over thread execution,
    so the execution of the current thread may be replaced by an ISR
    at any time unless interrupts have been masked. This applies to both
    cooperative threads and preemptive threads.


The kernel can be built with one of several choices for the ready queue
implementation, offering different choices between code size, constant factor
runtime overhead and performance scaling when many threads are added.

* Simple linked-list ready queue (:kconfig:option:`CONFIG_SCHED_DUMB`)

  The scheduler ready queue will be implemented as a simple unordered list, with
  very fast constant time performance for single threads and very low code size.
  This implementation should be selected on systems with constrained code size
  that will never see more than a small number (3, maybe) of runnable threads in
  the queue at any given time.  On most platforms (that are not otherwise using
  the red/black tree) this results in a savings of ~2k of code size.

* Red/black tree ready queue (:kconfig:option:`CONFIG_SCHED_SCALABLE`)

  The scheduler ready queue will be implemented as a red/black tree.  This has
  rather slower constant-time insertion and removal overhead, and on most
  platforms (that are not otherwise using the red/black tree somewhere) requires
  an extra ~2kb of code. The resulting behavior will scale cleanly and
  quickly into the many thousands of threads.

  Use this for applications needing many concurrent runnable threads (> 20 or
  so).  Most applications won't need this ready queue implementation.

* Traditional multi-queue ready queue (:kconfig:option:`CONFIG_SCHED_MULTIQ`)

  When selected, the scheduler ready queue will be implemented as the
  classic/textbook array of lists, one per priority.

  This corresponds to the scheduler algorithm used in Zephyr versions prior to
  1.12.

  It incurs only a tiny code size overhead vs. the "dumb" scheduler and runs in
  O(1) time in almost all circumstances with very low constant factor.  But it
  requires a fairly large RAM budget to store those list heads, and the limited
  features make it incompatible with features like deadline scheduling that
  need to sort threads more finely, and SMP affinity which need to traverse the
  list of threads.

  Typical applications with small numbers of runnable threads probably want the
  DUMB scheduler.


The wait_q abstraction used in IPC primitives to pend threads for later wakeup
shares the same backend data structure choices as the scheduler, and can use
the same options.

* Scalable wait_q implementation (:kconfig:option:`CONFIG_WAITQ_SCALABLE`)

  When selected, the wait_q will be implemented with a balanced tree.  Choose
  this if you expect to have many threads waiting on individual primitives.
  There is a ~2kb code size increase over :kconfig:option:`CONFIG_WAITQ_DUMB` (which may
  be shared with :kconfig:option:`CONFIG_SCHED_SCALABLE`) if the red/black tree is not
  used elsewhere in the application, and pend/unpend operations on "small"
  queues will be somewhat slower (though this is not generally a performance
  path).

* Simple linked-list wait_q (:kconfig:option:`CONFIG_WAITQ_DUMB`)

  When selected, the wait_q will be implemented with a doubly-linked list.
  Choose this if you expect to have only a few threads blocked on any single
  IPC primitive.

Cooperative Time Slicing
========================

Once a cooperative thread becomes the current thread, it remains
the current thread until it performs an action that makes it unready.
Consequently, if a cooperative thread performs lengthy computations,
it may cause an unacceptable delay in the scheduling of other threads,
including those of higher priority and equal priority.


  .. image:: ../../../../../kernel/services/scheduling/cooperative.svg
     :align: center

To overcome such problems, a cooperative thread can voluntarily relinquish
the CPU from time to time to permit other threads to execute.
A thread can relinquish the CPU in two ways:

* Calling :c:func:`k_yield` puts the thread at the back of the scheduler's
  prioritized list of ready threads, and then invokes the scheduler.
  All ready threads whose priority is higher or equal to that of the
  yielding thread are then allowed to execute before the yielding thread is
  rescheduled. If no such ready threads exist, the scheduler immediately
  reschedules the yielding thread without context switching.

* Calling :c:func:`k_sleep` makes the thread unready for a specified
  time period. Ready threads of *all* priorities are then allowed to execute;
  however, there is no guarantee that threads whose priority is lower
  than that of the sleeping thread will actually be scheduled before
  the sleeping thread becomes ready once again.

Preemptive Time Slicing
=======================

Once a preemptive thread becomes the current thread, it remains
the current thread until a higher priority thread becomes ready,
or until the thread performs an action that makes it unready.
Consequently, if a preemptive thread performs lengthy computations,
it may cause an unacceptable delay in the scheduling of other threads,
including those of equal priority.


  .. image:: ../../../../../kernel/services/scheduling/preemptive.svg
     :align: center

To overcome such problems, a preemptive thread can perform cooperative
time slicing (as described above), or the scheduler's time slicing capability
can be used to allow other threads of the same priority to execute.

.. image:: ../../../../../kernel/services/scheduling/timeslicing.svg
   :align: center

The scheduler divides time into a series of **time slices**, where slices
are measured in system clock ticks. The time slice size is configurable,
but this size can be changed while the application is running.

At the end of every time slice, the scheduler checks to see if the current
thread is preemptible and, if so, implicitly invokes :c:func:`k_yield`
on behalf of the thread. This gives other ready threads of the same priority
the opportunity to execute before the current thread is scheduled again.
If no threads of equal priority are ready, the current thread remains
the current thread.

Threads with a priority higher than specified limit are exempt from preemptive
time slicing, and are never preempted by a thread of equal priority.
This allows an application to use preemptive time slicing
only when dealing with lower priority threads that are less time-sensitive.

.. note::
   The kernel's time slicing algorithm does *not* ensure that a set
   of equal-priority threads receive an equitable amount of CPU time,
   since it does not measure the amount of time a thread actually gets to
   execute. However, the algorithm *does* ensure that a thread never executes
   for longer than a single time slice without being required to yield.

Scheduler Locking
=================

A preemptible thread that does not wish to be preempted while performing
a critical operation can instruct the scheduler to temporarily treat it
as a cooperative thread by calling :c:func:`k_sched_lock`. This prevents
other threads from interfering while the critical operation is being performed.

Once the critical operation is complete the preemptible thread must call
:c:func:`k_sched_unlock` to restore its normal, preemptible status.

If a thread calls :c:func:`k_sched_lock` and subsequently performs an
action that makes it unready, the scheduler will switch the locking thread out
and allow other threads to execute. When the locking thread again
becomes the current thread, its non-preemptible status is maintained.

.. note::
    Locking out the scheduler is a more efficient way for a preemptible thread
    to prevent preemption than changing its priority level to a negative value.


.. _thread_sleeping:

Thread Sleeping
===============

A thread can call :c:func:`k_sleep` to delay its processing
for a specified time period. During the time the thread is sleeping
the CPU is relinquished to allow other ready threads to execute.
Once the specified delay has elapsed the thread becomes ready
and is eligible to be scheduled once again.

A sleeping thread can be woken up prematurely by another thread using
:c:func:`k_wakeup`. This technique can sometimes be used
to permit the secondary thread to signal the sleeping thread
that something has occurred *without* requiring the threads
to define a kernel synchronization object, such as a semaphore.
Waking up a thread that is not sleeping is allowed, but has no effect.

.. _busy_waiting:

Busy Waiting
============

A thread can call :c:func:`k_busy_wait` to perform a ``busy wait``
that delays its processing for a specified time period
*without* relinquishing the CPU to another ready thread.

A busy wait is typically used instead of thread sleeping
when the required delay is too short to warrant having the scheduler
context switch from the current thread to another thread and then back again.

Suggested Uses
**************

Use cooperative threads for device drivers and other performance-critical work.

Use cooperative threads to implement mutually exclusion without the need
for a kernel object, such as a mutex.

Use preemptive threads to give priority to time-sensitive processing
over less time-sensitive processing.

.. _cpu_idle:

CPU Idling
##########

Although normally reserved for the idle thread, in certain special
applications, a thread might want to make the CPU idle.

.. contents::
    :local:
    :depth: 2

Concepts
********

Making the CPU idle causes the kernel to pause all operations until an event,
normally an interrupt, wakes up the CPU. In a regular system, the idle thread
is responsible for this. However, in some constrained systems, it is possible
that another thread takes this duty.

Implementation
**************

Making the CPU idle
===================

Making the CPU idle is simple: call the k_cpu_idle() API. The CPU will stop
executing instructions until an event occurs. Most likely, the function will
be called within a loop. Note that in certain architectures, upon return,
k_cpu_idle() unconditionally unmasks interrupts.

.. code-block:: c

    static k_sem my_sem;

    void my_isr(void *unused)
    {
        k_sem_give(&my_sem);
    }

    int main(void)
    {
        k_sem_init(&my_sem, 0, 1);

        /* wait for semaphore from ISR, then do related work */

        for (;;) {

            /* wait for ISR to trigger work to perform */
            if (k_sem_take(&my_sem, K_NO_WAIT) == 0) {

                /* ... do processing */

            }

            /* put CPU to sleep to save power */
            k_cpu_idle();
        }
    }

Making the CPU idle in an atomic fashion
========================================

It is possible that there is a need to do some work atomically before making
the CPU idle. In such a case, k_cpu_atomic_idle() should be used instead.

In fact, there is a race condition in the previous example: the interrupt could
occur between the time the semaphore is taken, finding out it is not available
and making the CPU idle again. In some systems, this can cause the CPU to idle
until *another* interrupt occurs, which might be *never*, thus hanging the
system completely. To prevent this, k_cpu_atomic_idle() should have been used,
like in this example.

.. code-block:: c

    static k_sem my_sem;

    void my_isr(void *unused)
    {
        k_sem_give(&my_sem);
    }

    int main(void)
    {
        k_sem_init(&my_sem, 0, 1);

        for (;;) {

            unsigned int key = irq_lock();

            /*
             * Wait for semaphore from ISR; if acquired, do related work, then
             * go to next loop iteration (the semaphore might have been given
             * again); else, make the CPU idle.
             */

            if (k_sem_take(&my_sem, K_NO_WAIT) == 0) {

                irq_unlock(key);

                /* ... do processing */


            } else {
                /* put CPU to sleep to save power */
                k_cpu_atomic_idle(key);
            }
        }
    }


Suggested Uses
**************

Use k_cpu_atomic_idle() when a thread has to do some real work in addition to
idling the CPU to wait for an event. See example above.

Use k_cpu_idle() only when a thread is only responsible for idling the CPU,
i.e. not doing any real work, like in this example below.

.. code-block:: c

    int main(void)
    {
        /* ... do some system/application initialization */


        /* thread is only used for CPU idling from this point on */
        for (;;) {
            k_cpu_idle();
        }
    }

.. note::
     **Do not use these APIs unless absolutely necessary.** In a normal system,
     the idle thread takes care of power management, including CPU idling.

API Reference
*************

.. doxygengroup:: cpu_idle_apis

Part 3

한국어 전문 번역

Kernel의 priority 기반 scheduler는 application thread들이 CPU를 공유하게 합니다.

개념

Scheduler가 특정 시점에 실행하도록 허용한 thread를 current thread라고 합니다. Current thread를 바꿀 기회를 reschedule point라고 하며, k_sem_take()·k_sleep()으로 running thread가 suspended/waiting으로 전환될 때, k_sem_give()·k_thread_start()로 thread가 ready가 될 때, interrupt 처리 뒤 thread context로 돌아갈 때, running thread가 k_yield()를 호출할 때가 대표적입니다.

Thread가 스스로 suspended 또는 waiting state로 가는 operation을 시작하면 sleep한다고 표현합니다. Current thread가 바뀌거나 ISR이 대신 실행되기 전 kernel은 CPU register를 저장하고, thread가 다시 실행될 때 복구합니다.

Scheduling algorithm

Scheduler는 ready thread 중 priority가 가장 높은 thread를 선택합니다. Priority가 같은 thread가 여러 개면 가장 오래 기다린 thread를 선택합니다.

CONFIG_SCHED_DEADLINE으로 earliest-deadline-first를 켜고 static priority가 같다면 deadline이 더 이른 thread를 더 높은 priority로 취급합니다. 이때 두 thread의 static priority와 deadline이 모두 같아야 동일 priority이며 k_thread_deadline_set()으로 deadline을 설정합니다.

Ready queue backend는 code size, constant overhead와 많은 thread에서의 확장성 사이에서 선택합니다.

  • CONFIG_SCHED_DUMB: unordered linked list입니다. Thread 하나일 때 빠르고 code가 작으며 runnable thread가 보통 3개 이하인 제약 system에 알맞습니다. 다른 red-black tree 사용이 없다면 약 2KB를 절약합니다.
  • CONFIG_SCHED_SCALABLE: red-black tree입니다. 삽입·삭제 constant overhead와 약 2KB code가 추가되지만 수천 thread까지 잘 확장됩니다. Runnable thread가 약 20개를 넘는 application에 적합합니다.
  • CONFIG_SCHED_MULTIQ: priority마다 list 하나를 둔 전통적 array-of-lists입니다. Zephyr 1.12 이전 algorithm이며 거의 항상 O(1)이고 code overhead가 작지만 list head용 RAM이 크고 deadline scheduling이나 SMP affinity처럼 정교한 정렬·순회 기능과 호환되지 않습니다. Runnable thread가 적은 일반 application은 보통 DUMB를 선택합니다.

IPC primitive가 blocked thread를 보관하는 wait_q도 같은 backend 선택을 공유합니다. CONFIG_WAITQ_SCALABLE은 balanced tree를 써 object 하나에 많은 thread가 기다릴 때 적합하며, tree를 공유하지 못하면 CONFIG_WAITQ_DUMB보다 약 2KB code가 늘고 작은 queue에서는 pend/unpend가 다소 느립니다. CONFIG_WAITQ_DUMB는 doubly-linked list라 각 primitive에 기다리는 thread가 적을 때 적합합니다.

Cooperative time slicing

Cooperative thread가 current thread가 되면 스스로 unready가 될 때까지 계속 실행합니다. 계산이 길면 higher/equal-priority thread의 scheduling도 오래 지연할 수 있습니다.

Cooperative thread의 CPU 양보
Current cooperative thread

스스로 unready가 될 때까지 계속 실행

k_yield()

higher 또는 equal priority ready thread를 먼저 실행

k_sleep()

모든 priority의 ready thread에 실행 기회를 제공

k_yield()는 thread를 priority별 ready list 뒤로 보내고 scheduler를 호출합니다. Higher/equal-priority ready thread를 모두 먼저 실행하며 그런 thread가 없으면 context switch 없이 즉시 자신을 다시 schedule합니다. k_sleep()은 지정 기간 thread를 unready로 만들어 모든 priority가 실행 가능하지만 sleeping thread가 다시 ready되기 전 lower-priority thread가 실제 실행된다는 보장은 없습니다.

Preemptive time slicing

Preemptive thread는 higher-priority thread가 ready되거나 자신이 unready가 될 때까지 current thread로 남습니다. 긴 계산은 equal-priority thread를 오래 지연할 수 있습니다.

Preemptive thread의 선점
Current preemptive thread

실행 중

Higher priority ready

즉시 current thread를 선점

Equal priority ready

자동 선점하지 않고 time slice 또는 yield를 기다림

Cooperative 방식으로 직접 CPU를 양보하거나 scheduler time slicing으로 같은 priority thread를 실행할 수 있습니다.

같은 priority thread의 time slicing
Time slice 시작

Current preemptible thread 실행

Slice 종료

Scheduler가 preemptible 여부 검사

Implicit k_yield()

같은 priority ready thread에 실행 기회

Equal peer 없음

기존 current thread가 계속 실행

Time slice는 system clock tick 단위이며 크기는 설정할 수 있고 runtime에도 바꿀 수 있습니다. 지정 limit보다 priority가 높은 thread는 time slicing에서 제외되어 equal-priority thread에 선점되지 않으므로 덜 민감한 lower-priority thread에만 slicing을 적용할 수 있습니다.

Scheduler locking

Preemptible thread는 critical operation 동안 k_sched_lock()을 호출해 일시적으로 cooperative thread처럼 취급되게 하고, 끝나면 반드시 k_sched_unlock()으로 normal preemptible status를 복구합니다.

Lock을 잡은 thread가 unready가 되면 scheduler는 다른 thread를 실행합니다. 원래 thread가 다시 current가 되어도 non-preemptible status는 유지됩니다. Priority를 negative로 바꾸는 것보다 scheduler lock이 효율적입니다.

Thread sleeping

k_sleep()은 지정 기간 processing을 미루고 CPU를 다른 ready thread에 넘깁니다. 시간이 지나면 다시 ready가 됩니다. 다른 thread가 k_wakeup()으로 조기 wakeup할 수 있어 semaphore 같은 kernel synchronization object 없이 event를 알리는 데 쓸 수 있습니다. Sleeping 중이 아닌 thread를 wakeup해도 허용되지만 효과는 없습니다.

Busy waiting

k_busy_wait()은 CPU를 다른 ready thread에 넘기지 않고 지정 시간 busy wait합니다. Scheduler context switch 왕복이 부담일 만큼 delay가 짧을 때 thread sleep 대신 사용합니다.

권장 용도

Device driver와 performance-critical work에는 cooperative thread를 사용합니다. Cooperative thread로 mutex 없이 mutual exclusion을 구현할 수도 있습니다. Time-sensitive processing을 덜 민감한 processing보다 우선하려면 preemptive thread를 사용합니다.

CPU idling

보통 idle thread만 CPU를 idle로 만들지만 일부 특수 application에서는 다른 thread가 이 역할을 맡을 수 있습니다. CPU idle은 event, 보통 interrupt가 깨울 때까지 kernel operation을 모두 멈춥니다.

CPU를 idle로 만들기

k_cpu_idle()을 호출하면 event가 올 때까지 instruction 실행을 멈춥니다. 보통 loop 안에서 호출하며 일부 architecture는 반환 시 interrupt를 무조건 unmask합니다.

static k_sem my_sem;

void my_isr(void *unused)
{
    k_sem_give(&my_sem);
}

int main(void)
{
    k_sem_init(&my_sem, 0, 1);

    /* wait for semaphore from ISR, then do related work */

    for (;;) {

        /* wait for ISR to trigger work to perform */
        if (k_sem_take(&my_sem, K_NO_WAIT) == 0) {

            /* ... do processing */

        }

        /* put CPU to sleep to save power */
        k_cpu_idle();
    }
}

Atomic하게 CPU idle 진입

Idle 직전 작업을 atomic하게 해야 한다면 k_cpu_atomic_idle()을 사용합니다. 앞 예제에는 semaphore가 unavailable임을 확인한 뒤 CPU idle에 들어가기 전에 interrupt가 발생할 수 있는 race가 있습니다. 이 경우 다음 interrupt가 영원히 오지 않으면 system 전체가 hang할 수 있습니다.

static k_sem my_sem;

void my_isr(void *unused)
{
    k_sem_give(&my_sem);
}

int main(void)
{
    k_sem_init(&my_sem, 0, 1);

    for (;;) {

        unsigned int key = irq_lock();

        /*
         * Wait for semaphore from ISR; if acquired, do related work, then
         * go to next loop iteration (the semaphore might have been given
         * again); else, make the CPU idle.
         */

        if (k_sem_take(&my_sem, K_NO_WAIT) == 0) {

            irq_unlock(key);

            /* ... do processing */


        } else {
            /* put CPU to sleep to save power */
            k_cpu_atomic_idle(key);
        }
    }
}

두 번째 예제는 irq_lock() key를 유지한 채 semaphore를 확인하고, acquired면 unlock 후 처리하며, 아니면 k_cpu_atomic_idle(key)로 interrupt enable과 idle 진입을 atomic하게 수행합니다.

권장 용도

Event를 기다리면서 실제 작업도 하는 thread는 k_cpu_atomic_idle()을 사용합니다. CPU idling만 전담하고 실제 작업은 하지 않는 thread에만 k_cpu_idle()을 사용합니다.

int main(void)
{
    /* ... do some system/application initialization */


    /* thread is only used for CPU idling from this point on */
    for (;;) {
        k_cpu_idle();
    }
}

API 참고: cpu_idle_apis

k_cpu_idle(void)

Event가 깨울 때까지 CPU를 idle 상태로 만듭니다. 일반 system에서는 idle thread만 CPU idle과 power management를 맡아야 하지만 single-threaded 같은 제약 system에서는 유일한 thread가 필요에 따라 수행할 수 있습니다.

k_cpu_atomic_idle(key)

Interrupt가 잠긴 상태에서 CPU를 atomic하게 idle로 전환합니다. Interrupt enable과 low-power mode 진입 사이에 interrupt-enabled gap이 없으며, wakeup 뒤에는 irq_unlock(key)를 호출한 것처럼 lockout state를 복구합니다.

매개변수

  • key: irq_lock()에서 얻은 interrupt locking key

Source

출처

영어 원문에는 문단, 문서 지시문, 표, 코드, 코드 심벌, 경로를 그대로 보존했습니다.