Zephyr 内核服务(1)

 

Zephyr内核的调试、中断与同步

  • Zephyr内核的调试、中断与同步

Kernel Services 内核服务

The Zephyr kernel lies at the heart of every Zephyr application. It provides a low footprint, high performance, multi-threaded execution environment with a rich set of available features. The rest of the Zephyr ecosystem, including device drivers, networking stack, and application-specific code, uses the kernel’s features to create a complete application.

west 核心位于每一个 Zephyr 应用的核心。它提供了一个低占用空间、高性能、多线程的执行环境,并具有丰富的可用特性。Zephyr 生态系统的其余部分,包括设备驱动程序、网络堆栈和特定于应用程序的代码,使用内核的特性来创建一个完整的应用程序。

The configurable nature of the kernel allows you to incorporate only those features needed by your application, making it ideal for systems with limited amounts of memory (as little as 2 KB!) or with simple multi-threading requirements (such as a set of interrupt handlers and a single background task). Examples of such systems include: embedded sensor hubs, environmental sensors, simple LED wearable, and store inventory tags.

内核的可配置特性允许您只合并应用程序所需的那些特性,使其成为内存有限(只有2 KB!)系统的理想选择或者具有简单的多线程需求(例如一组中断处理程序和单个后台任务)。这类系统的例子包括: 嵌入式传感器集线器,环境传感器,简单的 LED 可穿戴,和存储库存标签。

Applications requiring more memory (50 to 900 KB), multiple communication devices (like Wi-Fi and Bluetooth Low Energy), and complex multi-threading, can also be developed using the Zephyr kernel. Examples of such systems include: fitness wearables, smart watches, and IoT wireless gateways.

使用 Zephyr 内核还可以开发需要更多内存(50到900 KB)、多通信设备(如 Wi-Fi 和低耗电蓝牙)和复杂多线程的应用程序。这类系统的例子包括: 健身可穿戴设备、智能手表和物联网无线网关。

Scheduling, Interrupts, and Synchronization 调度、中断和同步

These pages cover basic kernel services related to thread scheduling and synchronization.

这些页面涵盖了与线程调度和同步相关的基本内核服务:

Threads 线程¶

This section describes kernel services for creating, scheduling, and deleting independently executable threads of instructions.

本节描述用于创建、调度和删除指令的独立可执行线程的内核服务。

A thread is a kernel object that is used for application processing that is too lengthy or too complex to be performed by an ISR.

线程是用于应用程序处理的内核对象,它太长或太复杂,ISR 无法执行

Any number of threads can be defined by an application (limited only by available RAM). Each thread is referenced by a thread id that is assigned when the thread is spawned.

应用程序可以定义任意数量的线程(仅受可用 RAM 的限制)。每个线程都由一个线程 id 引用,该线程 id 是在派生线程时分配的。

A thread has the following key properties:

线程具有以下关键属性:

  • A stack area, which is a region of memory used for the thread’s stack. The size of the stack area can be tailored to conform to the actual needs of the thread’s processing. Special macros exist to create and work with stack memory regions.

    堆栈区域,是用于线程堆栈的内存区域。堆栈区域的大小可以根据线程处理的实际需要进行调整。存在特殊的宏来创建和处理堆栈内存区域。

  • A thread control block for private kernel bookkeeping of the thread’s metadata. This is an instance of type k_thread.

    一个线程控制块,用于线程元数据的私有内核簿记。这是一个类型为 k_thread 的实例。

  • An entry point function, which is invoked when the thread is started. Up to 3 argument values can be passed to this function.

    一个入口点函数,在线程启动时调用。最多可以将3个参数值传递给这个函数。

  • A scheduling priority, which instructs the kernel’s scheduler how to allocate CPU time to the thread. (See Scheduling.)

    调度优先级,指示内核的调度程序如何为线程分配 CPU 时间

  • A set of thread options, which allow the thread to receive special treatment by the kernel under specific circumstances. (See Thread Options.)

    一组线程选项,允许线程在特定情况下接受内核的特殊处理。(见线程选项。)

  • A start delay, which specifies how long the kernel should wait before starting the thread.

    开始延迟,指定内核在启动线程之前应该等待多长时间

  • An execution mode, which can either be supervisor or user mode. By default, threads run in supervisor mode and allow access to privileged CPU instructions, the entire memory address space, and peripherals. User mode threads have a reduced set of privileges. This depends on the CONFIG_USERSPACE option. See User Mode.

    执行模式,可以是管理器模式或用户模式。默认情况下,线程以管理器模式运行,并允许访问特权 CPU 指令、整个内存地址空间和外围设备。用户模式线程拥有一组减少的特权。这取决于 config_userspace 选项。参见用户模式。

Lifecycle 生命周期

Thread Creation 线程创建

A thread must be created before it can be used. The kernel initializes the thread control block as well as one end of the stack portion. The remainder of the thread’s stack is typically left uninitialized.

线程必须先创建,然后才能使用。内核初始化线程控制块以及堆栈部分的一端。线程堆栈的其余部分通常未初始化。

Specifying a start delay of K_NO_WAIT instructs the kernel to start thread execution immediately. Alternatively, the kernel can be instructed to delay execution of the thread by specifying a timeout value – for example, to allow device hardware used by the thread to become available.

指定 k_no_wait 的启动延迟会指示内核立即启动线程执行。或者,可以指示内核通过指定超时值来延迟线程的执行——例如,允许线程使用的设备硬件可用。

The kernel allows a delayed start to be canceled before the thread begins executing. A cancellation request has no effect if the thread has already started. A thread whose delayed start was successfully canceled must be re-spawned before it can be used.

内核允许在线程开始执行之前取消延迟启动。如果线程已经启动,则取消请求无效。成功取消延迟启动的线程必须重新派生才能使用。

Thread Termination 线程终止

Once a thread is started it typically executes forever. However, a thread may synchronously end its execution by returning from its entry point function. This is known as termination.

一旦线程启动,它通常会永久执行。但是,线程可以通过从其入口点函数返回来同步结束其执行。这就是所谓的终止。

A thread that terminates is responsible for releasing any shared resources it may own (such as mutexes and dynamically allocated memory) prior to returning, since the kernel does not reclaim them automatically.

终止线程负责在返回之前释放它可能拥有的任何共享资源(比如互斥和动态分配的内存) ,因为内核不会自动回收它们。

In some cases a thread may want to sleep until another thread terminates. This can be accomplished with the k_thread_join() API. This will block the calling thread until either the timeout expires, the target thread self-exits, or the target thread aborts (either due to a k_thread_abort() call or triggering a fatal error).

在某些情况下,线程可能希望睡眠,直到另一个线程终止。这可以通过 k_thread_join() API 来实现。这将阻塞调用线程,直到超时过期、目标线程自我退出或目标线程中止(由于 k_thread_abort()调用或触发致命错误)。

Once a thread has terminated, the kernel guarantees that no use will be made of the thread struct. The memory of such a struct can then be re-used for any purpose, including spawning a new thread. Note that the thread must be fully terminated, which presents race conditions where a thread’s own logic signals completion which is seen by another thread before the kernel processing is complete. Under normal circumstances, application code should use k_thread_join() or k_thread_abort() to synchronize on thread termination state and not rely on signaling from within application logic.

一旦线程终止,内核保证线程结构不会被使用。然后,可以将这种结构的内存用于任何用途,包括生成新线程。注意,线程必须完全终止,这表示线程自己的逻辑信号完成的竞争条件,在内核处理完成之前,另一个线程可以看到这个信号。在正常情况下,应用程序代码应该使用 k_thread_join()或 k_thread_abort()来同步线程终止状态,而不是依赖于应用程序逻辑中的信令。

Thread Aborting 线程终止

A thread may asynchronously end its execution by aborting. The kernel automatically aborts a thread if the thread triggers a fatal error condition, such as dereferencing a null pointer.

线程可以通过中止异步地结束其执行。如果线程触发致命错误条件(如解引用空指针) ,内核将自动中止线程。

A thread can also be aborted by another thread (or by itself) by calling k_thread_abort(). However, it is typically preferable to signal a thread to terminate itself gracefully, rather than aborting it.

一个线程也可以通过调用 k_thread_abort()被另一个线程(或自己)终止。但是,通常最好是发出信号让线程优雅地终止自己,而不是中止它。

As with thread termination, the kernel does not reclaim shared resources owned by an aborted thread.

与线程终止一样,内核不回收已终止线程所拥有的共享资源。

Note 注意

The kernel does not currently make any claims regarding an application’s ability to respawn a thread that aborts.

内核目前没有对应用程序重新启动一个中止的线程的能力做出任何声明。

Thread Suspension

A thread can be prevented from executing for an indefinite period of time if it becomes suspended. The function k_thread_suspend() can be used to suspend any thread, including the calling thread. Suspending a thread that is already suspended has no additional effect.

如果线程变为挂起,则可以阻止它无限期地执行。函数 k_thread_suspend()可用于挂起任何线程,包括调用线程。挂起已经挂起的线程不会产生额外的效果。

Once suspended, a thread cannot be scheduled until another thread calls k_thread_resume() to remove the suspension.

一旦挂起,就不能调度线程,直到另一个线程调用 k_thread_resume()来删除挂起。

Note 注意

A thread can prevent itself from executing for a specified period of time using k_sleep(). However, this is different from suspending a thread since a sleeping thread becomes executable automatically when the time limit is reached.

线程可以使用 k_sleep()阻止自己在指定的时间段内执行。但是,这不同于挂起线程,因为睡眠线程在达到时间限制时会自动成为可执行线程。

Thread States 线程状态

A thread that has no factors that prevent its execution is deemed to be ready, and is eligible to be selected as the current thread.

没有阻止其执行的因素的线程被认为已经就绪,并且有资格被选择为当前线程。

A thread that has one or more factors that prevent its execution is deemed to be unready, and cannot be selected as the current thread.

具有一个或多个阻止其执行的因素的线程被认为是未就绪的,并且不能被选择为当前线程。

The following factors make a thread unready:

下列因素造成线程不准备好:

  • The thread has not been started.

    线程尚未启动。

  • The thread is waiting for a kernel object to complete an operation. (For example, the thread is taking a semaphore that is unavailable.)

    线程正在等待内核对象完成一个操作。(例如,线程正在使用不可用的信号量。)

  • The thread is waiting for a timeout to occur.

    线程正在等待超时。

  • The thread has been suspended.

    线程已被挂起。

  • The thread has terminated or aborted.

    线程已终止或中止。

thread_states

Note 注意

Although the diagram above may appear to suggest that both Ready and Running are distinct thread states, that is not the correct interpretation. Ready is a thread state, and Running is a schedule state that only applies to Ready threads.

尽管上面的图表似乎表明 Ready 和 Running 都是不同的线程状态,但这并不是正确的解释。就绪是线程状态,运行是只应用于就绪线程的调度状态。

Thread Stack objects 线程堆栈对象

Every thread requires its own stack buffer for the CPU to push context. Depending on configuration, there are several constraints that must be met:

每个线程都需要自己的堆栈缓冲区,以便 CPU 推送上下文。根据配置的不同,必须满足以下几个约束:

  • There may need to be additional memory reserved for memory management structures

    可能需要为内存管理结构保留额外的内存

  • If guard-based stack overflow detection is enabled, a small write- protected memory management region must immediately precede the stack buffer to catch overflows.

    如果启用了基于保护的堆栈溢出检测,则必须在堆栈缓冲区之前立即有一个小的写保护内存管理区域来捕获溢出。

  • If userspace is enabled, a separate fixed-size privilege elevation stack must be reserved to serve as a private kernel stack for handling system calls.

    如果启用了 userspace,则必须保留一个单独的固定大小的权限提升堆栈,作为处理系统调用的私有内核堆栈。

  • If userspace is enabled, the thread’s stack buffer must be appropriately sized and aligned such that a memory protection region may be programmed to exactly fit.

    如果启用了用户空间,线程的堆栈缓冲区必须适当调整大小和对齐,以便可以对内存保护区域进行精确编程。

The alignment constraints can be quite restrictive, for example some MPUs require their regions to be of some power of two in size, and aligned to its own size.

对齐约束可能非常具有限制性,例如,某些 mpu 要求其区域的大小为2的某个幂,并与其自身的大小对齐。

Because of this, portable code can’t simply pass an arbitrary character buffer to k_thread_create(). Special macros exist to instantiate stacks, prefixed with K_KERNEL_STACK and K_THREAD_STACK.

因此,可移植代码不能简单地将任意字符缓冲区传递给 k_thread_create() 。有一些特殊的宏可以实例化堆栈,以 k_kernel_stack 和 k_thread_stack 为前缀。

Kernel-only Stacks 只有内核的栈

If it is known that a thread will never run in user mode, or the stack is being used for special contexts like handling interrupts, it is best to define stacks using the K_KERNEL_STACK macros.

如果已经知道线程永远不会在用户模式下运行,或者堆栈被用于处理中断等特殊上下文,那么最好使用 k_kernel_stack 宏来定义堆栈。

These stacks save memory because an MPU region will never need to be programmed to cover the stack buffer itself, and the kernel will not need to reserve additional room for the privilege elevation stack, or memory management data structures which only pertain to user mode threads.

这些堆栈节省了内存,因为微处理器区域不需要编程来覆盖堆栈缓冲区本身,内核也不需要为权限提升堆栈或只属于用户模式线程的内存管理数据结构保留额外的空间。

Attempts from user mode to use stacks declared in this way will result in a fatal error for the caller.

从用户模式尝试使用以这种方式声明的栈将导致调用方出现致命错误。

If CONFIG_USERSPACE is not enabled, the set of K_THREAD_STACK macros have an identical effect to the K_KERNEL_STACK macros.

如果没有启用 CONFIG_USERSPACE,那么 k_thread_stack 宏集的效果与 k_kernel_stack 宏完全相同。

Thread stacks 线程堆栈

If it is known that a stack will need to host user threads, or if this cannot be determined, define the stack with K_THREAD_STACK macros. This may use more memory but the stack object is suitable for hosting user threads.

如果已知堆栈需要宿主用户线程,或者无法确定,则使用 k_thread_stack 宏定义堆栈。这可能会占用更多内存,但堆栈对象适合用于托管用户线程。

If CONFIG_USERSPACE is not enabled, the set of K_THREAD_STACK macros have an identical effect to the K_KERNEL_STACK macros.

如果没有启用 CONFIG_USERSPACE,那么 k_thread_stack 宏集的效果与 k_kernel_stack 宏完全相同。

Thread Priorities 线程优先级

A thread’s priority is an integer value, and can be either negative or non-negative. Numerically lower priorities takes precedence over numerically higher values. For example, the scheduler gives thread A of priority 4 higher priority over thread B of priority 7; likewise thread C of priority -2 has higher priority than both thread A and thread B.

线程的优先级是一个整数值,可以是负数,也可以是非负数。从数值上看,优先级较低的优先级优先于数值较高的优先级。例如,调度程序给予优先级为4的线程 a 的优先级高于优先级为7的线程 b; 同样,优先级为 -2的线程 c 的优先级高于线程 a 和线程 b。

The scheduler distinguishes between two classes of threads, based on each thread’s priority.

调度程序根据每个线程的优先级区分两类线程。

  • A cooperative thread has a negative priority value. Once it becomes the current thread, a cooperative thread remains the current thread until it performs an action that makes it unready.

    合作线程具有负的优先级值。一旦它成为当前线程,协作线程将保持当前线程,直到它执行一个操作,使其未就绪为止。

  • A preemptible thread has a non-negative priority value. Once it becomes the current thread, a preemptible thread may be supplanted at any time if a cooperative thread, or a preemptible thread of higher or equal priority, becomes ready.

    可抢占线程具有非负的优先级值。一旦它成为当前线程,如果合作线程或具有更高或同等优先级的可抢占线程就绪,那么可抢占线程随时可能被取代。

A thread’s initial priority value can be altered up or down after the thread has been started. Thus it is possible for a preemptible thread to become a cooperative thread, and vice versa, by changing its priority.

线程的初始优先级值可以在线程启动后上升或下降。因此,通过改变优先级,可抢占线程可能成为合作线程,反之亦然。

Note 注意

The scheduler does not make heuristic decisions to re-prioritize threads. Thread priorities are set and changed only at the application’s request.

调度程序不会进行启发式决策来重新排列线程的优先级。线程优先级仅在应用程序的请求下设置和更改。

The kernel supports a virtually unlimited number of thread priority levels. The configuration options CONFIG_NUM_COOP_PRIORITIES and CONFIG_NUM_PREEMPT_PRIORITIES specify the number of priority levels for each class of thread, resulting in the following usable priority ranges:

内核实际上支持无限个线程优先级级别。配置选项 CONFIG_NUM_COOP_PRIORITIES 和 CONFIG_NUM_PREEMPT_PRIORITIES 指定了每类线程的优先级数量,结果是下列可用的优先级范围:

priorities.svg

For example, configuring 5 cooperative priorities and 10 preemptive priorities results in the ranges -5 to -1 and 0 to 9, respectively.

例如,配置5个协作优先级和10个抢占优先级的结果分别在 -5到 -1和0到9的范围内。

Meta-IRQ Priorities

When enabled (see CONFIG_NUM_METAIRQ_PRIORITIES), there is a special subclass of cooperative priorities at the highest (numerically lowest) end of the priority space: meta-IRQ threads. These are scheduled according to their normal priority, but also have the special ability to preempt all other threads (and other meta-IRQ threads) at lower priorities, even if those threads are cooperative and/or have taken a scheduler lock. Meta-IRQ threads are still threads, however, and can still be interrupted by any hardware interrupt.

当启用时(参见 CONFIG_NUM_METAIRQ_PRIORITIES) ,在优先级空间的最高端(数值最低)有一个特殊的协作优先级子类: meta-IRQ 线程。这些线程按照它们的正常优先级进行调度,但是它们还具有以较低优先级抢占所有其他线程(和其他元 irq 线程)的特殊能力,即使这些线程是协作的,并且/或者已经获得了一个调度程序锁。然而,Meta-IRQ 线程仍然是线程,仍然可以被任何硬件中断中断。

This behavior makes the act of unblocking a meta-IRQ thread (by any means, e.g. creating it, calling k_sem_give(), etc.) into the equivalent of a synchronous system call when done by a lower priority thread, or an ARM-like “pended IRQ” when done from true interrupt context. The intent is that this feature will be used to implement interrupt “bottom half” processing and/or “tasklet” features in driver subsystems. The thread, once woken, will be guaranteed to run before the current CPU returns into application code.

这种行为使得解除对元 IRQ 线程的阻塞(通过任何方式,例如创建它,调用 k_sem_give()等)成为等效的同步系统调用(当由低优先级线程完成时) ,或者类似 arm 的“ pended IRQ”(当由真正的中断上下文完成时)。其目的是使用这个特性在驱动子系统中实现中断“bottom half(下半部分)”处理和/或“tasklet(小任务)”特性。线程一旦被唤醒,将保证在当前 CPU 返回到应用程序代码之前运行。

Unlike similar features in other OSes, meta-IRQ threads are true threads and run on their own stack (which must be allocated normally), not the per-CPU interrupt stack. Design work to enable the use of the IRQ stack on supported architectures is pending.

与其他操作系统中的类似特性不同,meta-IRQ 线程是真正的线程,在它们自己的堆栈(必须正常分配)上运行,而不是在单 cpu 中断堆栈上运行。在支持的体系结构上启用 IRQ 堆栈的设计工作尚未完成。

Note that because this breaks the promise made to cooperative threads by the Zephyr API (namely that the OS won’t schedule other thread until the current thread deliberately blocks), it should be used only with great care from application code. These are not simply very high priority threads and should not be used as such.

请注意,由于这违背了 Zephyr API 对协作线程的承诺(即操作系统不会调度其他线程,直到当前线程故意阻塞) ,因此应该只在应用程序代码中非常谨慎地使用它。这些线程不仅仅是非常高优先级的线程,不应该这样使用。

Thread Options 线程选项

The kernel supports a small set of thread options that allow a thread to receive special treatment under specific circumstances. The set of options associated with a thread are specified when the thread is spawned.

内核支持一小组线程选项,允许线程在特定情况下接受特殊处理。当派生线程时,将指定与该线程关联的一组选项。

A thread that does not require any thread option has an option value of zero. A thread that requires a thread option specifies it by name, using the | character as a separator if multiple options are needed (i.e. combine options using the bitwise OR operator).

不需要任何线程选项的线程的选项值为零。需要线程选项的线程按名称指定它,如果需要多个选项,则使用 | 字符作为分隔符(即使用按位 OR 运算符合并选项)。

The following thread options are supported.

支持以下线程选项。

  • K_ESSENTIAL

    This option tags the thread as an essential thread. This instructs the kernel to treat the termination or aborting of the thread as a fatal system error.

    此选项将线程标记为一个基本线程。这指示内核将线程的终止或终止视为一个致命的系统错误。

    By default, the thread is not considered to be an essential thread.

    默认情况下,该线程不被认为是一个基本线程。

  • K_SSE_REGS

    This x86-specific option indicate that the thread uses the CPU’s SSE registers. Also see K_FP_REGS.

    这个 x86特定的选项指示线程使用 CPU 的 SSE 寄存器。

    By default, the kernel does not attempt to save and restore the contents of these registers when scheduling the thread.

    默认情况下,内核在调度线程时不会尝试保存和恢复这些寄存器的内容。

  • K_FP_REGS

    This option indicate that the thread uses the CPU’s floating point registers. This instructs the kernel to take additional steps to save and restore the contents of these registers when scheduling the thread. (For more information see Floating Point Services.

    此选项指示线程使用 CPU 的浮点寄存器。这指示内核在调度线程时采取其他步骤保存和恢复这些寄存器的内容。(有关更多信息,请参见浮点服务。)

    By default, the kernel does not attempt to save and restore the contents of this register when scheduling the thread.

    默认情况下,内核在调度线程时不会尝试保存和恢复此寄存器的内容。

  • K_USER

    If CONFIG_USERSPACE is enabled, this thread will be created in user mode and will have reduced privileges. See User Mode. Otherwise this flag does nothing.

    如果启用了 config_userspace,则此线程将以用户模式创建,并将拥有减少的特权。参见用户模式。否则这面旗子什么也不会做。

  • K_INHERIT_PERMS

    If CONFIG_USERSPACE is enabled, this thread will inherit all kernel object permissions that the parent thread had, except the parent thread object. See User Mode.

    如果启用 CONFIG_USERSPACE,则此线程将继承父线程拥有的所有内核对象权限,但父线程对象除外。参见用户模式。

Thread Custom Data 线程自定义数据

Every thread has a 32-bit custom data area, accessible only by the thread itself, and may be used by the application for any purpose it chooses. The default custom data value for a thread is zero.

每个线程都有一个32位的自定义数据区域,只有线程本身可以访问,应用程序可以将其用于它选择的任何用途。线程的默认自定义数据值为零。

Note 注意

Custom data support is not available to ISRs because they operate within a single shared kernel interrupt handling context.

自定义数据支持对于 isr 是不可用的,因为他们在一个共享的内核中断处理上下文中操作。

By default, thread custom data support is disabled. The configuration option CONFIG_THREAD_CUSTOM_DATA can be used to enable support.

默认情况下,线程自定义数据支持是禁用的。可以使用配置选项 config_thread_custom_data 来启用支持。

The k_thread_custom_data_set() and k_thread_custom_data_get() functions are used to write and read a thread’s custom data, respectively. A thread can only access its own custom data, and not that of another thread.

分别使用 k_thread_custom_data_set()和 k_thread_custom_data_get()函数来写入和读取线程的定制数据。线程只能访问自己的自定义数据,而不能访问其他线程的数据。

The following code uses the custom data feature to record the number of times each thread calls a specific routine.

下面的代码使用自定义数据特性来记录每个线程调用特定例程的次数。

Note 注意

Obviously, only a single routine can use this technique, since it monopolizes the use of the custom data feature.

显然,只有一个例程可以使用这种技术,因为它独占了自定义数据特性的使用

int call_tracking_routine(void)
{
    uint32_t call_count;

    if (k_is_in_isr()) {
        /* ignore any call made by an ISR */
    } else {
        call_count = (uint32_t)k_thread_custom_data_get();
        call_count++;
        k_thread_custom_data_set((void *)call_count);
    }

    /* do rest of routine's processing */
    ...
}

Use thread custom data to allow a routine to access thread-specific information, by using the custom data as a pointer to a data structure owned by the thread.

使用线程自定义数据,允许一个例程访问线程特定的信息,方法是将自定义数据作为一个指向线程拥有的数据结构的指针。

Implementation

Spawning a Thread 生成一个线程

A thread is spawned by defining its stack area and its thread control block, and then calling k_thread_create().

通过定义其堆栈区域和线程控制块,然后调用 k_thread_create() ,产生线程。

The stack area must be defined using K_THREAD_STACK_DEFINE or K_KERNEL_STACK_DEFINE to ensure it is properly set up in memory.

堆栈区域必须使用 k_thread_stack_define 或 k_kernel_stack_define 来定义,以确保它在内存中正确设置。

The size parameter for the stack must be one of three values:

堆栈的 size 参数必须是以下三个值之一:

  • The original requested stack size passed to K_THREAD_STACK or K_KERNEL_STACK family of stack instantiation macros.

    传递给K_THREAD_STACK或K_KERNEL_STACK系列堆栈实例化宏的原始请求堆栈大小。

  • For a stack object defined with the K_THREAD_STACK family of macros, the return value of K_THREAD_STACK_SIZEOF() for that’ object.

    对于用 k_thread_stack 宏家族定义的堆栈对象,返回该对象的 k_thread_stack_sizeof()值。

  • For a stack object defined with the K_KERNEL_STACK family of macros, the return value of K_KERNEL_STACK_SIZEOF() for that object.

    对于用 k_kernel_stack 宏家族定义的堆栈对象,返回该对象的 k_kernel_stacksizeof()值。

The thread spawning function returns its thread id, which can be used to reference the thread.

线程生成函数返回其线程 id,该 id 可用于引用线程。

The following code spawns a thread that starts immediately.

下面的代码生成一个立即启动的线程。

#define MY_STACK_SIZE 500
#define MY_PRIORITY 5

extern void my_entry_point(void *, void *, void *);

K_THREAD_STACK_DEFINE(my_stack_area, MY_STACK_SIZE);
struct k_thread my_thread_data;

k_tid_t my_tid = k_thread_create(&my_thread_data, my_stack_area,
                                 K_THREAD_STACK_SIZEOF(my_stack_area),
                                 my_entry_point,
                                 NULL, NULL, NULL,
                                 MY_PRIORITY, 0, K_NO_WAIT);

Alternatively, a thread can be declared at compile time by calling K_THREAD_DEFINE. Observe that the macro defines the stack area, control block, and thread id variables automatically.

或者,可以在编译时调用 k_thread_define 声明线程。注意,宏自动定义堆栈区域、控制块和线程 id 变量。

The following code has the same effect as the code segment above.

下面的代码与上面的代码段具有相同的效果。

#define MY_STACK_SIZE 500
#define MY_PRIORITY 5

extern void my_entry_point(void *, void *, void *);

K_THREAD_DEFINE(my_tid, MY_STACK_SIZE,
                my_entry_point, NULL, NULL, NULL,
                MY_PRIORITY, 0, 0);

Note

注意

The delay parameter to k_thread_create() is a k_timeout_t value, so K_NO_WAIT means to start the thread immediately. The corresponding parameter to K_THREAD_DEFINE is a duration in integral milliseconds, so the equivalent argument is 0.

k_thread_create()的延迟参数是 k_timeout_t 值,因此 knowait 表示立即启动线程。K_THREAD_DEFINE 的对应参数的持续时间以整毫秒为单位,因此等效的参数为0。

User Mode Constraints 用户模式约束

This section only applies if CONFIG_USERSPACE is enabled, and a user thread tries to create a new thread. The k_thread_create() API is still used, but there are additional constraints which must be met or the calling thread will be terminated:

只有在启用 config_userspace 并且用户线程尝试创建新线程时,本节才会应用。仍然使用 k_thread_create() API,但是必须满足一些额外的约束,否则调用线程将被终止:

  • The calling thread must have permissions granted on both the child thread and stack parameters; both are tracked by the kernel as kernel objects.

    调用线程必须对子线程和堆栈参数都授予权限;两者都被内核作为内核对象进行跟踪。

  • The child thread and stack objects must be in an uninitialized state, i.e. it is not currently running and the stack memory is unused.

    子线程和堆栈对象必须处于未初始化状态,即它当前没有运行,堆栈内存未使用。

  • The stack size parameter passed in must be equal to or less than the bounds of the stack object when it was declared.

    传入的堆栈大小参数必须等于或小于声明时的堆栈对象的边界。

  • The K_USER option must be used, as user threads can only create other user threads.

    必须使用 k_user 选项,因为用户线程只能创建其他用户线程。

  • The K_ESSENTIAL option must not be used, user threads may not be considered essential threads.

    不能使用 K_ESSENTIAL 选项,用户线程可能不被视为基本线程

  • The priority of the child thread must be a valid priority value, and equal to or lower than the parent thread.

    子线程的优先级必须是有效的优先级值,并且等于或低于父线程。

Dropping Permissions 删除权限

If CONFIG_USERSPACE is enabled, a thread running in supervisor mode may perform a one-way transition to user mode using the k_thread_user_mode_enter() API. This is a one-way operation which will reset and zero the thread’s stack memory. The thread will be marked as non-essential.

如果CONFIG_USERSPACE被启用,运行在监督者模式下的线程可以使用k_thread_user_mode_enter()API执行到用户模式的单向转换。这是一个单向的操作,它将重置线程的堆栈内存并归零。该线程将被标记为非必需的。

Terminating a Thread 终止线程

A thread terminates itself by returning from its entry point function.

线程通过从其入口点函数返回来终止自己

The following code illustrates the ways a thread can terminate.

下面的代码演示了线程终止的方式。

void my_entry_point(int unused1, int unused2, int unused3)
{
    while (1) {
        ...
        if (<some condition>) {
            return; /* thread terminates from mid-entry point function */
        }
        ...
    }

    /* thread terminates at end of entry point function */
}

If CONFIG_USERSPACE is enabled, aborting a thread will additionally mark the thread and stack objects as uninitialized so that they may be re-used.

如果 CONFIG_USERSPACE 被启用,中止一个线程将额外标记线程和堆栈对象为未初始化,以便它们可以被重新使用。

Runtime Statistics 运行时统计

Thread runtime statistics can be gathered and retrieved if CONFIG_THREAD_RUNTIME_STATS is enabled, for example, total number of execution cycles of a thread.

如果启用 CONFIG_THREAD_RUNTIME_STATS,则可以收集和检索线程运行时统计数据,例如,一个线程的执行周期总数。

By default, the runtime statistics are gathered using the default kernel timer. For some architectures, SoCs or boards, there are timers with higher resolution available via timing functions. Using of these timers can be enabled via CONFIG_THREAD_RUNTIME_STATS_USE_TIMING_FUNCTIONS.

默认情况下,使用默认的内核计时器收集运行时统计信息。对于某些体系结构、 soc 或主板,有通过计时功能提供的具有更高分辨率的计时器。使用这些计时器可以通过 CONFIG_THREAD_RUNTIME_STATS_USE_TIMING_FUNCTIONS 启用。

Here is an example:

下面是一个例子:

k_thread_runtime_stats_t rt_stats_thread;

k_thread_runtime_stats_get(k_current_get(), &rt_stats_thread);

printk("Cycles: %llu\n", rt_stats_thread.execution_cycles);

Suggested Uses 建议的用途

Use threads to handle processing that cannot be handled in an ISR.

使用线程来处理在ISR中无法处理的处理。

Use separate threads to handle logically distinct processing operations that can execute in parallel.

使用独立的线程来处理逻辑上不同的处理操作,可以并行执行

Configuration Options 配置选项

Related configuration options:

相关配置选项:

线程 API 参考

Scheduling 调度

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

内核的基于优先级的调度器允许应用程序的线程共享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 k_sem_take() or k_sleep().

    线程从运行状态到暂停或等待状态的转换,例如通过 k_sem_take()或 k_sleep()。

  • transition of a thread to the ready state, for example by k_sem_give() or k_thread_start()

    线程到就绪状态的过渡,例如用 k_sem_give()或 k_thread_start()

  • return to thread context after processing an interrupt

    处理中断后返回线程上下文

  • when a running thread invokes k_yield()

    当一个正在运行的线程调用 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.

每当调度程序更改当前线程的标识时,或者当前线程的执行被 ISR 替换时,内核首先保存当前线程的 CPU 寄存器值。这些寄存器值在线程稍后恢复执行时得到恢复。

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 (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 k_thread_deadline_set() is used to set a thread’s deadline.

一个线程的相对优先级主要由其静态优先级决定。然而,当最早截止日期优先的调度被启用(CONFIG_SCHED_DEADLINE),并且选择的线程具有相同的静态优先级时,那么具有较早截止日期的线程被认为具有较高的优先级。因此,当启用最早截止日期优先的调度时,只有当两个线程的静态优先级和截止日期都相等时,才会被认为具有相同的优先级。例程k_thread_deadline_set()被用来设置线程的最后期限。

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.

ISR 的执行优先于线程的执行,因此当前线程的执行可以在任何时候被 ISR 替换,除非中断被屏蔽。这适用于协作线程和抢占线程。

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 (CONFIG_SCHED_DUMB)

    简单的链表就绪队列(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.

    调度程序就绪队列将作为一个简单的无序列表实现,对于单个线程具有非常快的常量时间性能和非常低的代码大小。应该在代码大小受限的系统上选择此实现,这些系统在任何给定时间都不会在队列中看到超过少量(可能是3个)的可运行线程。在大多数平台(没有使用红/黑树的平台)上,这将节省大约2k 的代码大小。

  • Red/black tree ready queue (CONFIG_SCHED_SCALABLE)

    红/黑树准备队列(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.

    调度程序就绪队列将作为红/黑树实现。这会有相当慢的常量时间插入和删除开销,而且在大多数平台上(在其他地方没有使用红/黑树)需要额外的2kb 代码。由此产生的行为将清晰而快速地扩展到成千上万的线程中。

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

    对于需要多个并发可运行线程(大约20个)的应用程序,可以使用此方法。大多数应用程序不需要这个就绪队列实现。

  • Traditional multi-queue ready queue (CONFIG_SCHED_MULTIQ)

    传统的多队列就绪队列(CONFIG_SCHED_MULTIQ)

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

    当选中时,调度程序就绪队列将作为经典的/教科书式的列表数组实现,每个优先级一个(最多32个优先级)。

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

    这与1.12之前 Zephyr 版本中使用的调度器算法相对应。

    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.

    与 “哑巴 “调度器相比,它只产生了很小的代码大小的开销,而且几乎在所有情况下都能以O(1)的时间运行,常数非常低。但它需要相当大的RAM预算来存储这些列表头,而且有限的功能使它与需要对线程进行更精细排序的截止日期调度以及需要遍历线程列表的SMP亲和性等功能不兼容。

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

    具有少量可运行线程的典型应用程序可能需要 DUMB 调度程序。

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.

IPC原语中使用的wait_q抽象(用于暂缓线程以便稍后唤醒),与调度器共享相同的后端数据结构选择,并可以使用相同的选项。

  • Scalable wait_q implementation (CONFIG_WAITQ_SCALABLE)

    可伸缩的 wait_q 实现(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 CONFIG_WAITQ_DUMB (which may be shared with 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).

    选择后,wait_q将用平衡树来实现。如果你期望有许多线程等待单个原语,请选择此选项。如果应用程序中的其他地方没有使用红/黑树,那么与CONFIG_WAITQ_DUMB(可与CONFIG_SCHED_SCALABLE共享)相比,代码大小会增加~2kb,而且 “小 “队列上的pend/unpend操作会稍慢一些(尽管这通常不是性能路径)。

  • Simple linked-list wait_q (CONFIG_WAITQ_DUMB)

    简单链表 wait_q (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.

    当选中时,wait_q 将通过双向链接列表实现。如果您希望在任何一个 IPC 原语上只阻塞少量线程,请选择此选项。

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.

一旦一个合作线程成为当前线程,它就一直是当前线程,直到它执行了一个使它成为未就绪状态的动作。因此,如果一个合作线程执行冗长的计算,它可能会导致其他线程的调度出现不可接受的延迟,包括那些优先级较高和优先级相同的线程。

cooperative.svg

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:

为了克服这些问题,协作线程可以不时地自动放弃 CPU,以允许其他线程执行。一个线程可以通过两种方式放弃 CPU:

  • Calling 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.

    调用k_yield()会将线程放在调度程序的已排列优先级的就绪线程列表的后面,然后调用调度程序。所有优先级高于或等于让渡线程的就绪线程被允许在让渡线程被重新安排之前执行。如果没有这样的就绪线程,调度器会立即重新安排让渡线程的工作,而不进行上下文切换。

  • Calling 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.

    调用 k_sleep()会使线程在指定的时间段内处于未就绪状态。然后允许执行所有优先级的就绪线程; 但是,不能保证优先级低于睡眠线程的线程在睡眠线程再次就绪之前实际得到调度。

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.

一旦抢占式线程成为当前线程,它将保持当前线程,直到更高优先级的线程准备就绪,或者直到线程执行一个操作使其不准备就绪。因此,如果一个抢占式线程执行冗长的计算,可能会导致其他线程(包括具有相同优先级的线程)的调度出现不可接受的延迟。

preemptive.svg

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.

为了克服这些问题,抢占式线程可以执行协作时间片(如上所述) ,或者可以使用调度程序的时间片功能来允许具有相同优先级的其他线程执行。

timeslicing.svg

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 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.

在每个时间片的末尾,调度程序检查当前线程是否可抢占,如果可抢占,则隐式代表线程调用 k_yield()。这使得具有相同优先级的其他就绪线程有机会在再次调度当前线程之前执行。如果没有同等优先级的线程就绪,则当前线程仍然是当前线程。

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.

内核的时间片算法不能确保一组具有相同优先级的线程获得公平的 CPU 时间,因为它不能度量线程实际执行的时间。但是,该算法确保线程执行时间不会超过一个时间片而不需要让渡。

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 k_sched_lock(). This prevents other threads from interfering while the critical operation is being performed.

执行关键操作时不希望被抢占的可抢占线程可以通过调用 k_sched_lock()指示调度程序暂时将其视为协作线程。这可以防止其他线程在执行关键操作时发生干扰。

Once the critical operation is complete the preemptible thread must call k_sched_unlock() to restore its normal, preemptible status.

关键操作完成后,可抢占线程必须调用 k_sched_unlock()以恢复其正常的可抢占状态。

If a thread calls 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.

如果一个线程调用了 k_sched_lock(),并且随后执行了一个使其未就绪的操作,那么调度程序将切换锁定线程并允许其他线程执行。当锁定线程再次成为当前线程时,它的不可抢占状态将得到维护。

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 线程睡眠

A thread can call 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.

线程可以调用 k_sleep()将其处理延迟到指定的时间段。在线程休眠期间,CPU 被释放以允许其他就绪线程执行。一旦指定的延迟已经过去,线程就可以准备好并且可以再次调度。

A sleeping thread can be woken up prematurely by another thread using 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.

一个沉睡的线程可以被另一个线程提前唤醒,使用 k_wakeup()。这种技术有时可以用来允许辅助线程向睡眠线程发出信号,告诉它发生了某些事情,而不需要线程定义内核同步对象,例如信号量。唤醒一个没有睡眠的线程是允许的,但是没有任何效果。

Busy Waiting 忙碌的等待

A thread can call 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.

线程可以调用 k_busy_wait()来执行一个繁忙的等待,这个等待将其处理延迟到指定的时间段,而不会将 CPU 让给另一个就绪线程。

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 Idling 空闲CPU

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

虽然通常为空闲线程保留,但在某些特殊应用程序中,线程可能希望使 CPU 空闲。

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.

使 CPU 空闲会导致内核暂停所有操作,直到一个事件(通常是一个中断)唤醒 CPU。在常规系统中,空闲线程负责这一点。但是,在一些受限制的系统中,可能会有另一个线程承担这个任务。

Implementation 实施

Making the CPU idle 使 CPU 空闲

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.

使 CPU 空闲很简单: 调用 k_cpu_idle()API。CPU 将停止执行指令,直到发生事件。最有可能的是,函数将在循环中调用。请注意,在某些架构中,在返回时,k_cpu_idle()会无条件地解除中断屏蔽。

static k_sem my_sem;

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

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

    /* 等待ISR的信号,然后做相关工作 */

    for (;;) {

        /*  等待ISR触发工作来执行 */
        if (k_sem_take(&my_sem, K_NO_WAIT) == 0) {

            /* ... do processing */

        }

        /* 让CPU进入睡眠状态以节省电力 */
        k_cpu_idle();
    }
}

Making the CPU idle in an atomic fashion以原子的方式使 CPU 空闲

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.

在使 CPU 空闲之前,可能需要以原子方式执行某些工作。在这种情况下,应该使用 k_cpu_atomic_idle()。

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.

事实上,在前面的例子中有一个竞争条件: 中断可能发生在获取信号量、发现信号量不可用并使 CPU 再次空闲之间。在某些系统中,这可能导致 CPU 空闲,直到另一个中断发生,这可能永远不会发生,因此完全挂起系统。为了防止这种情况,应该使用 k_cpu_atomic_idle(),如本例所示。

static k_sem my_sem;

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

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

    for (;;) {

        unsigned int key = irq_lock();

        /*
         * 等待来自ISR的信号;如果获得了信号,就做相关的工作,然后进入下一个循环迭代(信号可能已经再次给出);否则,让CPU空闲。
         */

        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.

当线程不得不做一些真正的工作时,使用 k_CPU_atomic_idle(),此外还要空闲 CPU 来等待事件。见上面的例子。

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.

仅当线程只负责空闲 CPU 时才使用 k_CPU_idle(),也就是说不做任何真正的工作,如下面的例子所示。

void 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。在正常的系统中,空闲线程负责电源管理,包括 CPU 空闲。

System Threads 系统线程

A system thread is a thread that the kernel spawns automatically during system initialization.

系统线程是在系统初始化期间内核自动生成的线程。

The kernel spawns the following system threads:

内核产生了以下系统线程:

  • Main thread 主线程

    This thread performs kernel initialization, then calls the application’s main() function (if one is defined).

    这个线程执行内核初始化,然后调用应用程序的 main()函数(如果定义了 main()函数)

    By default, the main thread uses the highest configured preemptible thread priority (i.e. 0). If the kernel is not configured to support preemptible threads, the main thread uses the lowest configured cooperative thread priority (i.e. -1).

    默认情况下,主线程使用最高配置的可抢占线程优先级(即0)。如果内核没有配置为支持抢占线程,则主线程使用配置的最低协作线程优先级(即 -1)。

    The main thread is an essential thread while it is performing kernel initialization or executing the application’s main() function; this means a fatal system error is raised if the thread aborts. If main() is not defined, or if it executes and then does a normal return, the main thread terminates normally and no error is raised.

    在执行内核初始化或执行应用程序的 main()函数时,主线程是一个必不可少的线程; 这意味着如果线程中止,将引发严重的系统错误。如果没有定义 main() ,或者如果它执行然后进行正常返回,那么主线程将正常终止并且没有引发错误。

  • Idle thread 空闲线程

    This thread executes when there is no other work for the system to do. If possible, the idle thread activates the board’s power management support to save power; otherwise, the idle thread simply performs a “do nothing” loop. The idle thread remains in existence as long as the system is running and never terminates.

    此线程在系统没有其他工作可做时执行。如果可能的话,空闲线程激活板的电源管理支持以节省电源; 否则,空闲线程只是执行一个“什么也不做”循环。只要系统在运行,空闲线程就会一直存在,并且永远不会终止。

    The idle thread always uses the lowest configured thread priority. If this makes it a cooperative thread, the idle thread repeatedly yields the CPU to allow the application’s other threads to run when they need to.

    空闲线程总是使用最低配置的线程优先级。如果这使它成为一个协作线程,那么空闲线程将重复地产生 CPU,以允许应用程序的其他线程在需要时运行。

    The idle thread is an essential thread, which means a fatal system error is raised if the thread aborts.

    空闲线程是一个必不可少的线程,这意味着如果线程中止,将引发严重的系统错误。

Additional system threads may also be spawned, depending on the kernel and board configuration options specified by the application. For example, enabling the system workqueue spawns a system thread that services the work items submitted to it. (See Workqueue Threads.)

根据应用程序指定的内核和主板配置选项,还可能派生其他系统线程。例如,启用系统工作队列会生成一个系统线程,为提交给它的工作项提供服务。(参见工作队列线程.)

Implementation 实施

Writing a main() function 编写 main()函数

An application-supplied main() function begins executing once kernel initialization is complete. The kernel does not pass any arguments to the function.

应用程序提供的 main()函数在内核初始化完成后开始执行。内核不向函数传递任何参数。

The following code outlines a trivial main() function. The function used by a real application can be as complex as needed.

下面的代码概述了一个简单的 main()函数。实际应用程序使用的函数可以根据需要而变得复杂。

void main(void)
{
    /* initialize a semaphore */
    ...

    /* register an ISR that gives the semaphore */
    ...

    /* monitor the semaphore forever */
    while (1) {
        /* wait for the semaphore to be given by the ISR */
        ...
        /* do whatever processing is now needed */
        ...
    }
}

Suggested Uses 建议的用途

Use the main thread to perform thread-based processing in an application that only requires a single thread, rather than defining an additional application-specific thread.

在一个只需要一个线程的应用程序中,使用主线程来执行基于线程的处理,而不是定义一个额外的应用程序专用线程。

Workqueue Threads 工作队列线程

A workqueue is a kernel object that uses a dedicated thread to process work items in a first in, first out manner. Each work item is processed by calling the function specified by the work item. A workqueue is typically used by an ISR or a high-priority thread to offload non-urgent processing to a lower-priority thread so it does not impact time-sensitive processing.

工作队列是一个内核对象,它使用专用线程以先进先出的方式处理工作项。通过调用工作项指定的函数来处理每个工作项。工作队列通常由 ISR 或高优先级线程使用,用于将非紧急处理卸载到低优先级线程,这样就不会影响时间敏感的处理。

Any number of workqueues can be defined (limited only by available RAM). Each workqueue is referenced by its memory address.

可以定义任意数量的工作队列(仅受可用 RAM 的限制)。

A workqueue has the following key properties:

工作队列具有以下关键属性:

  • A queue of work items that have been added, but not yet processed.

    已添加但尚未处理的工作项的队列。

  • A thread that processes the work items in the queue. The priority of the thread is configurable, allowing it to be either cooperative or preemptive as required.

    处理队列中工作项的线程。线程的优先级是可配置的,允许它根据需要进行协作或抢占。

Regardless of workqueue thread priority the workqueue thread will yield between each submitted work item, to prevent a cooperative workqueue from starving other threads.

不管工作队线程的优先级如何,工作队线程将在每个提交的工作项之间yeild让步,以防止合作工作队列饿死其他线程。

A workqueue must be initialized before it can be used. This sets its queue to empty and spawns the workqueue’s thread. The thread runs forever, but sleeps when no work items are available.

工作队列必须初始化后才能使用。这将其队列设置为 empty,并生成工作队列的线程。线程将永远运行,但当没有可用的工作项时,线程将处于睡眠状态。

Note 注意

The behavior described here is changed from the Zephyr workqueue implementation used prior to release 2.6. Among the changes are:

这里描述的行为与2.6版本之前使用的 Zephyr 工作队列实现不同:

  • Precise tracking of the status of cancelled work items, so that the caller need not be concerned that an item may be processing when the cancellation returns. Checking of return values on cancellation is still required.

    精确跟踪已取消工作项的状态,以便调用方不必担心在取消返回时可能正在处理某个项。仍然需要在取消时检查返回值。

  • Direct submission of delayable work items to the queue with K_NO_WAIT rather than always going through the timeout API, which could introduce delays.

    用K_NO_WAIT直接向队列提交可延迟的工作项,而不是总是通过超时的API,这可能会带来延迟。

  • The ability to wait until a work item has completed or a queue has been drained.

    能够等到一个工作项完成或一个队列被耗尽

  • Finer control of behavior when scheduling a delayable work item, specifically allowing a previous deadline to remain unchanged when a work item is scheduled again.

    在调度可延迟的工作项目时,对行为进行更精细的控制,特别是在再次安排工作项目时,允许以前的最后期限保持不变。

  • Safe handling of work item resubmission when the item is being processed on another workqueue.

    当项目正在另一个工作队列中处理时,安全地处理工作项目的重新提交。

Using the return values of k_work_busy_get() or k_work_is_pending(), or measurements of remaining time until delayable work is scheduled, should be avoided to prevent race conditions of the type observed with the previous implementation. See also Workqueue Best Practices.

应该避免使用k_work_busy_get()或k_work_is_pending()的返回值,或者使用可延迟工作被安排之前的剩余时间的测量,以防止在以前的实现中观察到的那种竞赛条件。参见Workqueue最佳实践。

Work Item Lifecycle 工作项目生命周期

Any number of work items can be defined. Each work item is referenced by its memory address.

可以定义任意数量的工作项。每个工作项都由其内存地址引用。

A work item is assigned a handler function, which is the function executed by the workqueue’s thread when the work item is processed. This function accepts a single argument, which is the address of the work item itself. The work item also maintains information about its status.

为工作项分配一个处理程序函数,该函数是处理工作项时由工作队列的线程执行的函数。此函数接受一个参数,即工作项本身的地址。工作项还维护有关其状态的信息。

A work item must be initialized before it can be used. This records the work item’s handler function and marks it as not pending.

工作项必须初始化后才能使用。这将记录工作项的处理程序函数,并将其标记为未挂起。

A work item may be queued (K_WORK_QUEUED) by submitting it to a workqueue by an ISR or a thread. Submitting a work item appends the work item to the workqueue’s queue. Once the workqueue’s thread has processed all of the preceding work items in its queue the thread will remove the next work item from the queue and invoke the work item’s handler function. Depending on the scheduling priority of the workqueue’s thread, and the work required by other items in the queue, a queued work item may be processed quickly or it may remain in the queue for an extended period of time.

一个工作项目可以通过ISR或线程提交到工作队列中而被排队(K_WORK_QUEUED)。提交一个工作项目会将该工作项目追加到工作队列中。一旦工作队列的线程处理完其队列中所有前面的工作项目,该线程将从队列中移除下一个工作项目并调用该工作项目的处理函数。根据工作队列线程的调度优先级,以及队列中其他项目所需的工作,一个队列中的工作项目可能被快速处理,也可能在队列中停留较长的一段时间。

A delayable work item may be scheduled (K_WORK_DELAYED) to a workqueue; see Delayable Work.

可延迟的工作项可能被调度到一个工作队列(k_work_delayed) ; 请参阅可延迟的工作。

A work item will be running (K_WORK_RUNNING) when it is running on a work queue, and may also be canceling (K_WORK_CANCELING) if it started running before a thread has requested that it be cancelled.

当工作项在工作队列上运行时,它将处于运行态(k_work_running) ,如果在线程请求取消它之前已经开始运行,它也可能被取消态(k_work_canceling)。

A work item can be in multiple states; for example it can be:

一个工作项可以处于多个状态,例如:

  • running on a queue;

    在队列中运行;

  • marked canceling (because a thread used k_work_cancel_sync() to wait until the work item completed);

    标记为取消(因为线程使用 k_work_cancel_sync()等待工作项完成) ;

  • queued to run again on the same queue;

    在同一队列上排队以便再次运行;

  • scheduled to be submitted to a (possibly different) queue

    预定提交给一个(可能是不同的)队列

all simultaneously. A work item that is in any of these states is pending (k_work_is_pending()) or busy (k_work_busy_get()).

处于这些状态中任何一种的工作项都处于挂起状态(k_work_is_pending())或忙碌状态(k_work_busy_get())。

A handler function can use any kernel API available to threads. However, operations that are potentially blocking (e.g. taking a semaphore) must be used with care, since the workqueue cannot process subsequent work items in its queue until the handler function finishes executing.

处理程序函数可以使用线程可用的任何内核 API。但是,必须小心使用可能阻塞的操作(例如获取信号量) ,因为工作队列无法处理队列中的后续工作项,直到处理程序函数完成执行。

The single argument that is passed to a handler function can be ignored if it is not required. If the handler function requires additional information about the work it is to perform, the work item can be embedded in a larger data structure. The handler function can then use the argument value to compute the address of the enclosing data structure with CONTAINER_OF, and thereby obtain access to the additional information it needs.

传递给处理程序函数的单个参数如果不是必需的,则可以忽略它。如果处理程序函数需要有关其要执行的工作的附加信息,则可以将工作项嵌入到更大的数据结构中。然后,处理程序函数可以使用参数值计算带 CONTAINER_of 的封闭数据结构的地址,从而获得对其所需的附加信息的访问权。

A work item is typically initialized once and then submitted to a specific workqueue whenever work needs to be performed. If an ISR or a thread attempts to submit a work item that is already queued the work item is not affected; the work item remains in its current place in the workqueue’s queue, and the work is only performed once.

工作项通常初始化一次,然后在需要执行工作时提交到特定的工作队列。如果 ISR 或线程尝试提交已经排队的工作项,则不受影响; 工作项将保留在工作队列的当前位置,并且只执行一次工作。

A handler function is permitted to re-submit its work item argument to the workqueue, since the work item is no longer queued at that time. This allows the handler to execute work in stages, without unduly delaying the processing of other work items in the workqueue’s queue.

一个处理程序函数被允许重新向工作队列提交它的工作项目参数,因为工作项目在那个时候已经不再被排队了。这允许处理程序分阶段执行工作,而不会不适当地拖延工作队列中其他工作项目的处理。

Important 重要事项

A pending work item must not be altered until the item has been processed by the workqueue thread. This means a work item must not be re-initialized while it is busy. Furthermore, any additional information the work item’s handler function needs to perform its work must not be altered until the handler function has finished executing.

在一个待处理的工作项目被工作队列线程处理之前,不得对该项目进行更改。这意味着当一个工作项目处于繁忙状态时,不得重新初始化。此外,工作项目的处理函数在执行其工作时需要的任何额外信息,在处理函数完成执行之前都不能被改变。

Delayable Work 可推迟的工作

An ISR or a thread may need to schedule a work item that is to be processed only after a specified period of time, rather than immediately. This can be done by scheduling a delayable work item to be submitted to a workqueue at a future time.

ISR 或线程可能需要安排只在指定时间段之后进行处理的工作项,而不是立即进行处理。这可以通过调度一个可延迟的工作项来实现,该工作项将在未来某个时间提交给工作队列。

A delayable work item contains a standard work item but adds fields that record when and where the item should be submitted.

可延迟的工作项包含一个标准工作项,但添加了一些字段,用于记录应该在何时何地提交该项。

A delayable work item is initialized and scheduled to a workqueue in a similar manner to a standard work item, although different kernel APIs are used. When the schedule request is made the kernel initiates a timeout mechanism that is triggered after the specified delay has elapsed. Once the timeout occurs the kernel submits the work item to the specified workqueue, where it remains queued until it is processed in the standard manner.

可延迟的工作项以类似于标准工作项的方式初始化并调度到工作队列中,尽管使用了不同的内核 api。当发出调度请求时,内核启动一个超时机制,该机制在指定的延迟过后触发。一旦超时发生,内核将工作项提交给指定的工作队列,在那里它将保持队列状态,直到按标准方式处理它。

Note that work handler used for delayable still receives a pointer to the underlying non-delayable work structure, which is not publicly accessible from k_work_delayable. To get access to an object that contains the delayable work object use this idiom:

请注意,用于delayable的工作处理程序仍然会收到一个指向底层不可延迟工作结构的指针,该指针不能从k_work_delayable公开访问。为了获得对包含可延迟工作对象的对象的访问权,请使用这个习惯用语:

static void work_handler(struct k_work *work)
{
        struct k_work_delayable *dwork = k_work_delayable_from_work(work);
        struct work_context *ctx = CONTAINER_OF(dwork, struct work_context,
                                                timed_work);
        ...

Triggered Work 触发式工作

The k_work_poll_submit() interface schedules a triggered work item in response to a poll event (see Polling API), that will call a user-defined function when a monitored resource becomes available or poll signal is raised, or a timeout occurs. In contrast to k_poll(), the triggered work does not require a dedicated thread waiting or actively polling for a poll event.

K_ work_poll_submit()接口调度一个触发的工作项,以响应一个 poll 事件(请参阅 Polling API) ,该事件将在受监视的资源变得可用、提出轮询信号或出现超时时调用用户定义的函数。与 k_poll()相反,触发的工作不需要专门的线程等待或主动轮询轮询轮询事件。

A triggered work item is a standard work item that has the following added properties:

触发的工作项是标准工作项,具有以下附加属性:

  • A pointer to an array of poll events that will trigger work item submissions to the workqueue

    指向将触发工作项提交到工作队列的轮询事件数组的指针

  • A size of the array containing poll events.

    包含轮询事件的数组的大小。

A triggered work item is initialized and submitted to a workqueue in a similar manner to a standard work item, although dedicated kernel APIs are used. When a submit request is made, the kernel begins observing kernel objects specified by the poll events. Once at least one of the observed kernel object’s changes state, the work item is submitted to the specified workqueue, where it remains queued until it is processed in the standard manner.

触发的工作项以类似于标准工作项的方式初始化并提交到工作队列中,尽管使用了专用的内核 api。当发出提交请求时,内核开始观察由 poll 事件指定的内核对象。一旦观察到的内核对象至少有一个更改状态,工作项就会提交给指定的工作队列,在那里它将保持队列状态,直到按标准方式处理它。

Important 重要事项

The triggered work item as well as the referenced array of poll events have to be valid and cannot be modified for a complete triggered work item lifecycle, from submission to work item execution or cancellation.

被触发的工作项以及被引用的 poll 事件数组必须有效,并且不能为完整的被触发的工作项生命周期(从提交到工作项执行或取消)而修改。

An ISR or a thread may cancel a triggered work item it has submitted as long as it is still waiting for a poll event. In such case, the kernel stops waiting for attached poll events and the specified work is not executed. Otherwise the cancellation cannot be performed.

ISR 或线程可以取消已提交的已触发工作项,只要它仍在等待轮询事件。在这种情况下,内核停止等待附加的轮询事件,并且不执行指定的工作。否则无法执行取消操作。

System Workqueue 系统工作队列

The kernel defines a workqueue known as the system workqueue, which is available to any application or kernel code that requires workqueue support. The system workqueue is optional, and only exists if the application makes use of it.

内核定义了一个称为系统工作队列的工作队列,它可以用于任何需要工作队列支持的应用程序或内核代码。系统工作队列是可选的,只有在应用程序使用它时才存在。

Important 重要事项

Additional workqueues should only be defined when it is not possible to submit new work items to the system workqueue, since each new workqueue incurs a significant cost in memory footprint. A new workqueue can be justified if it is not possible for its work items to co-exist with existing system workqueue work items without an unacceptable impact; for example, if the new work items perform blocking operations that would delay other system workqueue processing to an unacceptable degree.

只有当不可能向系统工作队列提交新的工作项时,才应该定义额外的工作队列,因为每个新的工作队列都会在内存占用中产生巨大的成本。如果新工作队列的工作项不可能与现有系统工作队列工作项共存而不产生不可接受的影响,则可以对其进行调整; 例如,如果新工作项执行阻塞操作,将其他系统工作队列处理延迟到不可接受的程度。

How to Use Workqueues 如何使用工作队列

Defining and Controlling a Workqueue 定义和控制工作队列

A workqueue is defined using a variable of type k_work_q. The workqueue is initialized by defining the stack area used by its thread, initializing the k_work_q, either zeroing its memory or calling k_work_queue_init(), and then calling k_work_queue_start(). The stack area must be defined using K_THREAD_STACK_DEFINE to ensure it is properly set up in memory.

使用类型为 k_work_q 的变量定义工作队列。工作队列通过定义其线程使用的堆栈区域进行初始化,初始化 k_work_q,调用 k_work_queue_init()或内存调零,然后调用 k_work_queue_start()。堆栈区域必须使用 k_thread_stack_define 来定义,以确保正确地在内存中设置。

The following code defines and initializes a workqueue:

下面的代码定义和初始化一个工作队列:

#define MY_STACK_SIZE 512
#define MY_PRIORITY 5

K_THREAD_STACK_DEFINE(my_stack_area, MY_STACK_SIZE);

struct k_work_q my_work_q;

k_work_queue_init(&my_work_q);

k_work_queue_start(&my_work_q, my_stack_area,
                   K_THREAD_STACK_SIZEOF(my_stack_area), MY_PRIORITY,
                   NULL);

In addition the queue identity and certain behavior related to thread rescheduling can be controlled by the optional final parameter; see k_work_queue_start() for details.

此外,队列标识和某些与线程重调度相关的行为可以由可选的最终参数控制; 详细信息请参阅 k_work_queue_start()。

The following API can be used to interact with a workqueue:

下面的 API 可以用来与工作队列交互:

  • k_work_queue_drain() can be used to block the caller until the work queue has no items left. Work items resubmitted from the workqueue thread are accepted while a queue is draining, but work items from any other thread or ISR are rejected. The restriction on submitting more work can be extended past the completion of the drain operation in order to allow the blocking thread to perform additional work while the queue is “plugged”. Note that draining a queue has no effect on scheduling or processing delayable items, but if the queue is plugged and the deadline expires the item will silently fail to be submitted.

    可以使用 k_work_queue_drain()阻塞调用方,直到工作队列中没有任何项目为止。当一个队列正在耗尽时,可以接受从工作队列线程重新提交的工作项,但是拒绝来自任何其他线程或 ISR 的工作项。对于提交更多工作的限制可以扩展到排出操作完成之后,以便允许阻塞线程在队列“被堵塞”时执行额外的工作。请注意,排空队列对调度或处理可延迟的项目没有影响,但是如果插入队列并且截止日期过期,则该项目将无声地无法提交。

  • k_work_queue_unplug() removes any previous block on submission to the queue due to a previous drain operation.

    K_work_queue_unplug()删除提交到队列上的任何以前的块,这是由于以前的排出操作。

Submitting a Work Item 提交工作项

A work item is defined using a variable of type k_work. It must be initialized by calling k_work_init(), unless it is defined using K_WORK_DEFINE in which case initialization is performed at compile-time.

使用类型为 k_work 的变量定义工作项。它必须通过调用 k_work_init()进行初始化,除非它是使用 k_work_define 定义的,在这种情况下,初始化是在编译时执行的。

An initialized work item can be submitted to the system workqueue by calling k_work_submit(), or to a specified workqueue by calling k_work_submit_to_queue().

可以通过调用 k_work_submit()将已初始化的工作项提交给系统工作队列,也可以通过调用 k_work_submit_to_queue()将初始化的工作项提交给指定的工作队列。

The following code demonstrates how an ISR can offload the printing of error messages to the system workqueue. Note that if the ISR attempts to resubmit the work item while it is still queued, the work item is left unchanged and the associated error message will not be printed.

下面的代码演示了 ISR 如何将错误消息的打印卸载到系统工作队列中。请注意,如果 ISR 试图在工作项仍在排队时重新提交该工作项,则工作项将保持不变,并且不会打印相关的错误消息。

struct device_info {
    struct k_work work;
    char name[16]
} my_device;

void my_isr(void *arg)
{
    ...
    if (error detected) {
        k_work_submit(&my_device.work);
    }
    ...
}

void print_error(struct k_work *item)
{
    struct device_info *the_device =
        CONTAINER_OF(item, struct device_info, work);
    printk("Got error on device %s\n", the_device->name);
}

/* initialize name info for a device */
strcpy(my_device.name, "FOO_dev");

/* initialize work item for printing device's error messages */
k_work_init(&my_device.work, print_error);

/* install my_isr() as interrupt handler for the device (not shown) */
...

The following API can be used to check the status of or synchronize with the work item:

下面的 API 可用于检查工作项的状态或与工作项同步:

  • k_work_busy_get() returns a snapshot of flags indicating work item state. A zero value indicates the work is not scheduled, submitted, being executed, or otherwise still being referenced by the workqueue infrastructure.

    K_work_busy_get()返回表示工作项状态的标志的快照。零值表示工作没有被计划、提交、执行,或者工作队列基础结构仍然引用。

  • k_work_is_pending() is a helper that indicates true if and only if the work is scheduled, queued, or running.

    K_ work_is_pending()是一个帮助器,它指示当且仅当工作被调度、排队或运行时为 true。

  • k_work_flush() may be invoked from threads to block until the work item has completed. It returns immediately if the work is not pending.

    可以从线程调用 k_work_flush()以阻止工作项,直到工作项完成为止。如果工作没有挂起,它立即返回。

  • k_work_cancel() attempts to prevent the work item from being executed. This may or may not be successful. This is safe to invoke from ISRs.

    K_work_cancel()尝试阻止执行工作项。这可能会成功,也可能不会成功。这对于从 ISRs 调用是安全的。

  • k_work_cancel_sync() may be invoked from threads to block until the work completes; it will return immediately if the cancellation was successful or not necessary (the work wasn’t submitted or running). This can be used after k_work_cancel() is invoked (from an ISR)` to confirm completion of an ISR-initiated cancellation.

    可以从线程调用 k_work_cancel_sync()以阻止工作完成; 如果取消成功或不必要(工作没有提交或运行) ,它将立即返回。这可以在调用 k_work_cancel()(从 ISR 调用)之后使用,以确认 ISR 启动的取消的完成。

Scheduling a Delayable Work Item 安排可推迟的工作项

A delayable work item is defined using a variable of type k_work_delayable. It must be initialized by calling k_work_init_delayable().

可延迟的工作项使用类型为 k_work_delayable 的变量来定义,它必须通过调用 k_work_init_delayable()来初始化。

For delayed work there are two common use cases, depending on whether a deadline should be extended if a new event occurs. An example is collecting data that comes in asynchronously, e.g. characters from a UART associated with a keyboard. There are two APIs that submit work after a delay:

对于延迟的工作,有两种常见的用例,这取决于在发生新事件时是否应该延长最后期限。一个例子是异步收集数据,例如来自与键盘相关联的 UART 的字符。有两个 api 在延迟后提交作品:

  • k_work_schedule() (or k_work_schedule_for_queue()) schedules work to be executed at a specific time or after a delay. Further attempts to schedule the same item with this API before the delay completes will not change the time at which the item will be submitted to its queue. Use this if the policy is to keep collecting data until a specified delay since the first unprocessed data was received;

    K_work_schedule()(或者 k_work_schedule_for_queue())调度在特定时间或延迟后执行的工作。在延迟完成之前,进一步尝试使用此 API 调度相同的项目,并不会改变将该项目提交到其队列的时间。如果策略是继续收集数据,直到收到第一个未处理的数据后的指定延迟,则使用此策略;

  • k_work_reschedule() (or k_work_reschedule_for_queue()) unconditionally sets the deadline for the work, replacing any previous incomplete delay and changing the destination queue if necessary. Use this if the policy is to keep collecting data until a specified delay since the last unprocessed data was received.

    K_work_redatation()(或者 k_work_redatation_for_queue())无条件地设置工作的截止日期,替换以前任何不完全延迟,并在必要时更改目标队列。如果策略是继续收集数据,直到收到最后一个未处理的数据后的指定延迟,则使用此策略。

If the work item is not scheduled both APIs behave the same. If K_NO_WAIT is specified as the delay the behavior is as if the item was immediately submitted directly to the target queue, without waiting for a minimal timeout (unless k_work_schedule() is used and a previous delay has not completed).

如果工作项没有被安排,那么两个 api 的行为是相同的。如果 kno_wait 被指定为延迟,那么这种行为就好像项目直接被提交到目标队列,而不需要等待最小超时(除非使用了 kwork_schedule()并且以前的延迟没有完成)。

Both also have variants that allow control of the queue used for submission.

两者都有允许控制用于提交的队列的变体。

The helper function k_work_delayable_from_work() can be used to get a pointer to the containing k_work_delayable from a pointer to k_work that is passed to a work handler function.

可以使用 helper 函数 k_work_delayable_from_work()从传递给工作处理程序函数的 k_work_delayable 指针获得包含 k_work_delayable 的指针。

The following additional API can be used to check the status of or synchronize with the work item:

以下附加 API 可用于检查工作项的状态或与工作项同步:

Synchronizing with Work Items 与工作项同步

While the state of both regular and delayable work items can be determined from any context using k_work_busy_get() and k_work_delayable_busy_get() some use cases require synchronizing with work items after they’ve been submitted. k_work_flush(), k_work_cancel_sync(), and k_work_cancel_delayable_sync() can be invoked from thread context to wait until the requested state has been reached.

虽然通过使用 k work busy get()和 k work delayable busy get()可以从任何上下文确定常规工作项和可延迟工作项的状态,但有些用例需要在工作项提交后与其同步。可以从线程上下文调用 k_work_flush()、 k_work_cancel_sync()和 k_work_cancel_delayable_sync()等待到达请求的状态。

These APIs must be provided with a k_work_sync object that has no application-inspectable components but is needed to provide the synchronization objects. These objects should not be allocated on a stack if the code is expected to work on architectures with CONFIG_KERNEL_COHERENCE.

这些 api 必须提供一个 k_work_sync 对象,该对象没有可检查的应用程序组件,但需要提供同步对象。如果代码需要在 CONFIG_kernel_coherence 体系结构上工作,那么就不应该在堆栈上分配这些对象。

Workqueue Best Practices 工作队列最佳实践

Avoid Race Conditions 避免竞争条件

Sometimes the data a work item must process is naturally thread-safe, for example when it’s put into a k_queue by some thread and processed in the work thread. More often external synchronization is required to avoid data races: cases where the work thread might inspect or manipulate shared state that’s being accessed by another thread or interrupt. Such state might be a flag indicating that work needs to be done, or a shared object that is filled by an ISR or thread and read by the work handler.

有时,工作项必须处理的数据自然是线程安全的,例如,当某个线程将其放入 k 队列并在工作线程中处理时。通常需要外部同步来避免数据竞争: 工作线程可能检查或操作被另一个线程或中断访问的共享状态的情况。这种状态可能是指示需要完成工作的标志,或者是由 ISR 或线程填充并由工作处理程序读取的共享对象。

For simple flags Atomic Services may be sufficient. In other cases spin locks (k_spinlock_t) or thread-aware locks (k_sem, k_mutex , …) may be used to ensure data races don’t occur.

对于简单的标志,Atomic Services 可能就足够了。在其他情况下,可以使用旋转锁(k_spinlock_t)或线程感知锁(k_sem,k_mutex,…)来确保数据竞争不会发生。

If the selected lock mechanism can sleep then allowing the work thread to sleep will starve other work queue items, which may need to make progress in order to get the lock released. Work handlers should try to take the lock with its no-wait path. For example:

如果选择的锁机制可以睡眠,那么允许工作线程睡眠将饿死其他工作队列项,这可能需要取得进展,以获得锁释放。工作处理程序应该尝试获取具有无等待路径的锁。例如:

static void work_handler(struct work *work)
{
        struct work_context *parent = CONTAINER_OF(work, struct work_context,
                                                   work_item);

        if (k_mutex_lock(&parent->lock, K_NO_WAIT) != 0) {
                /* NB: Submit will fail if the work item is being cancelled. */
                (void)k_work_submit(work);
                return;
        }

        /* do stuff under lock */
        k_mutex_unlock(&parent->lock);
        /* do stuff without lock */
}

Be aware that if the lock is held by a thread with a lower priority than the work queue the resubmission may starve the thread that would release the lock, causing the application to fail. Where the idiom above is required a delayable work item is preferred, and the work should be (re-)scheduled with a non-zero delay to allow the thread holding the lock to make progress.

请注意,如果锁由优先级低于工作队列的线程持有,则重新提交可能会饿死将释放锁的线程,从而导致应用程序失败。如果需要上面的习惯用法,首选可延迟的工作项,并且工作应该以非零延迟(重新)调度,以允许持有锁的线程取得进展。

Note that submitting from the work handler can fail if the work item had been cancelled. Generally this is acceptable, since the cancellation will complete once the handler finishes. If it is not, the code above must take other steps to notify the application that the work could not be performed.

注意,如果工作项被取消,从工作处理程序提交可能会失败。通常情况下,这是可以接受的,因为取消操作将在处理程序完成后完成。如果不是,上面的代码必须采取其他步骤通知应用程序无法执行工作。

Work items in isolation are self-locking, so you don’t need to hold an external lock just to submit or schedule them. Even if you use external state protected by such a lock to prevent further resubmission, it’s safe to do the resubmit as long as you’re sure that eventually the item will take its lock and check that state to determine whether it should do anything. Where a delayable work item is being rescheduled in its handler due to inability to take the lock some other self-locking state, such as an atomic flag set by the application/driver when the cancel is initiated, would be required to detect the cancellation and avoid the cancelled work item being submitted again after the deadline.

隔离的工作项是自锁的,因此您不需要持有外部锁来提交或安排它们。即使你使用这种锁保护的外部状态来防止进一步的重新提交,只要你确信最终这个项目将获得它的锁并检查这个状态来决定它是否应该做任何事情,那么重新提交是安全的。如果可延迟的工作项由于无法获取锁而在处理程序中重新安排,则需要使用其他一些自锁状态(如应用程序/驱动程序在启动取消时设置的原子标志)来检测取消,并避免在截止日期之后再次提交被取消的工作项。

Check Return Values 检查返回值

All work API functions return status of the underlying operation, and in many cases it is important to verify that the intended result was obtained.

所有工作 API 函数都返回底层操作的状态,在许多情况下,重要的是验证是否获得了预期的结果。

  • Submitting a work item (k_work_submit_to_queue()) can fail if the work is being cancelled or the queue is not accepting new items. If this happens the work will not be executed, which could cause a subsystem that is animated by work handler activity to become non-responsive.

    如果工作被取消或者队列不接受新项目,则提交工作项(k_work_submit_to_queue())可能会失败。如果发生这种情况,工作将不会被执行,这可能导致由工作处理程序活动生成的子系统变成非响应性的。

  • Asynchronous cancellation (k_work_cancel() or k_work_cancel_delayable()) can complete while the work item is still being run by a handler. Proceeding to manipulate state shared with the work handler will result in data races that can cause failures.

    异步取消(k_work_cancel()或 k_work_cancel_delayable())可以在处理程序仍在运行工作项时完成。继续操作与工作处理程序共享的状态将导致可能导致失败的数据竞争。

Many race conditions have been present in Zephyr code because the results of an operation were not checked.

许多竞态条件出现在泽弗代码中,因为没有检查操作的结果。

There may be good reason to believe that a return value indicating that the operation did not complete as expected is not a problem. In those cases the code should clearly document this, by (1) casting the return value to void to indicate that the result is intentionally ignored, and (2) documenting what happens in the unexpected case. For example:

可能有充分的理由相信,指示操作没有按预期完成的返回值不是问题。在这些情况下,代码应该清楚地记录这一点,方法是(1)将返回值转换为 void,以表明结果被故意忽略,(2)记录在意外情况下发生的事情。例如:

/* If this fails, the work handler will check pub->active and
 * exit without transmitting.
 */
(void)k_work_cancel_delayable(&pub->timer);

However in such a case the following code must still avoid data races, as it cannot guarantee that the work thread is not accessing work-related state.

但是,在这种情况下,下面的代码仍然必须避免数据竞争,因为它不能保证工作线程不访问与工作相关的状态。

Don’t Optimize Prematurely 不要过早优化

The workqueue API is designed to be safe when invoked from multiple threads and interrupts. Attempts to externally inspect a work item’s state and make decisions based on the result are likely to create new problems.

当从多个线程和中断调用时,工作队列 API 被设计为安全的。试图从外部检查工作项的状态并根据结果做出决策可能会产生新的问题。

So when new work comes in, just submit it. Don’t attempt to “optimize” by checking whether the work item is already submitted by inspecting snapshot state with k_work_is_pending() or k_work_busy_get(), or checking for a non-zero delay from k_work_delayable_remaining_get(). Those checks are fragile: a “busy” indication can be obsolete by the time the test is returned, and a “not-busy” indication can also be wrong if work is submitted from multiple contexts, or (for delayable work) if the deadline has completed but the work is still in queued or running state.

因此,当新的作品出现时,只要提交就行了。不要试图通过检查快照状态是否已经提交工作项来进行“优化”,检查快照状态是否具有 k_work_is_pending()或 k_work_busy_get() ,或者检查 k_work_delayable_remaining_get()是否存在非零延迟。这些检查是脆弱的: 在返回测试时,”繁忙”指示可能已经过时,如果工作是从多个上下文提交的,或者(对于可延迟的工作)如果截止日期已经完成,但工作仍处于排队或运行状态,则”非繁忙”指示也可能是错误的。

A general best practice is to always maintain in shared state some condition that can be checked by the handler to confirm whether there is work to be done. This way you can use the work handler as the standard cleanup path: rather than having to deal with cancellation and cleanup at points where items are submitted, you may be able to have everything done in the work handler itself.

一般的最佳实践是始终维护在共享状态下的某些条件,这些条件可以由处理程序检查,以确认是否有工作要完成。通过这种方式,您可以使用工作处理程序作为标准的清理路径: 您可以在工作处理程序本身中完成所有操作,而不必在提交项的地方处理取消和清理操作。

A rare case where you could safely use k_work_is_pending() is as a check to avoid invoking k_work_flush() or k_work_cancel_sync(), if you are certain that nothing else might submit the work while you’re checking (generally because you’re holding a lock that prevents access to state used for submission).

如果您确定在检查期间没有其他任何东西可以提交工作(通常是因为您持有一个锁,它阻止访问用于提交的状态) ,那么您可以安全地使用 kwork_pending()的一种罕见情况是作为一种检查,以避免调用 kwork_flush()或 kwork_cancel_sync()。

Suggested Uses 建议的用途

Use the system workqueue to defer complex interrupt-related processing from an ISR to a shared thread. This allows the interrupt-related processing to be done promptly without compromising the system’s ability to respond to subsequent interrupts, and does not require the application to define and manage an additional thread to do the processing.

使用系统工作队列将复杂的与中断相关的处理从 ISR 推迟到共享线程。这使得与中断相关的处理能够在不损害系统对后续中断的响应能力的情况下迅速完成,并且不需要应用程序定义和管理一个额外的线程来完成处理。

Configuration Options 配置选项

Related configuration options:

相关配置选项:

Operation without Threads 无线程操作

Thread support is not necessary in some applications:

线程支持在某些应用程序中是不必要的:

  • Bootloaders

  • Simple event-driven applications

    简单事件驱动的应用程序

  • Examples intended to demonstrate core functionality

    用于演示核心功能的示例

Thread support can be disabled by setting CONFIG_MULTITHREADING to n. Since this configuration has a significant impact on Zephyr’s functionality and testing of it has been limited, there are conditions on what can be expected to work in this configuration.

可以通过将 config_multithreading 设置为 n 来禁用线程支持。由于这种配置对 Zephyr 的功能有重大影响,并且对它的测试一直很有限,因此在这种配置中可以期望什么工作是有条件的。

What Can be Expected to Work

什么是可以期待的工作

These core capabilities shall function correctly when CONFIG_MULTITHREADING is disabled:

当关闭 CONFIG_multithreading 时,这些核心功能将正常工作:

  • The build system

    构建系统

  • The ability to boot the application to main()

    将应用程序引导到 main()的能力

  • Interrupt management

    中断管理

  • The system clock including k_uptime_get()

    系统时钟包括 k_uptime_get()

  • Timers, i.e. k_timer()

    计时器,即 k_timer()

  • Non-sleeping delays e.g. k_busy_wait().

    非睡眠延迟,例如 k_busy_wait()。

  • Sleeping k_cpu_idle().

    正在睡眠的 k_cpu_idle() 。

  • Pre main() drivers and subsystems initialization e.g. SYS_INIT.

    预主()驱动程序和子系统初始化,例如 SYS_init。

  • Memory Management

    内存管理

  • Specifically identified drivers in certain subsystems, listed below.

    特定子系统中特别标识的驱动程序,如下所示。

The expectations above affect selection of other features; for example CONFIG_SYS_CLOCK_EXISTS cannot be set to n.

上述期望影响其他特性的选择; 例如 CONFIG_sys_clock_exists 不能设置为 n。

What Cannot be Expected to Work

不能期望什么工作

Functionality that will not work with CONFIG_MULTITHREADING includes majority of the kernel API:

不能使用 CONFIG 多线程的功能包括大部分内核 API:

Subsystem Behavior Without Thread Support 没有线程支持的子系统行为

The sections below list driver and functional subsystems that are expected to work to some degree when CONFIG_MULTITHREADING is disabled. Subsystems that are not listed here should not be expected to work.

下面的部分列出了在禁用 config_multithreading 时预期在一定程度上可以工作的驱动程序和功能子系统。这里没有列出的子系统不应该被期望工作。

Some existing drivers within the listed subsystems do not work when threading is disabled, but are within scope based on their subsystem, or may be sufficiently isolated that supporting them on a particular platform is low-impact. Enhancements to add support to existing capabilities that were not originally implemented to work with threads disabled will be considered.

在禁用线程时,列出的子系统中的一些现有驱动程序无法工作,但是基于它们的子系统,这些驱动程序在范围之内,或者可能已经被充分隔离,因此在特定平台上支持它们的影响。我们将考虑增强对现有功能的支持,这些功能最初并未实现用于禁用线程的情况。

Flash

The Flash is expected to work for all SoC flash peripheral drivers. Bus-accessed devices like serial memories may not be supported.

预计 Flash 将适用于所有 SoC Flash 外设驱动程序。可能不支持串行存储器等总线访问的设备。

List/table of supported drivers to go here

支持的驱动程序列表/表到这里

GPIO

The GPIO is expected to work for all SoC GPIO peripheral drivers. Bus-accessed devices like GPIO extenders may not be supported.

GPIO 预计将为所有 SoC GPIO 外设驱动程序工作。可能不支持像 GPIO 扩展程序这样的总线访问设备。

List/table of supported drivers to go here

支持的驱动程序列表/表到这里

UART 异步收发器

A subset of the UART is expected to work for all SoC UART peripheral drivers.

UART 的一个子集应该适用于所有 SoC UART 外围驱动程序。

  • Applications that select CONFIG_UART_INTERRUPT_DRIVEN may work, depending on driver implementation.

    选择 config_uart_interrupt_driven 的应用程序可以工作,这取决于驱动程序的实现。

  • Applications that select CONFIG_UART_ASYNC_API may work, depending on driver implementation.

    选择 config_uart_async_api 的应用程序可以工作,这取决于驱动程序的实现。

  • Applications that do not select either CONFIG_UART_ASYNC_API or CONFIG_UART_INTERRUPT_DRIVEN are expected to work.

    不选择 config_uart_async_api 或 config_uart_interrupt_driven 的应用程序可以工作。

List/table of supported drivers to go here, including which API options are supported

支持的驱动程序的列表/表格到这里,包括哪些 API 选项支持

Interrupts 中断

An interrupt service routine (ISR) is a function that executes asynchronously in response to a hardware or software interrupt. An ISR normally preempts the execution of the current thread, allowing the response to occur with very low overhead. Thread execution resumes only once all ISR work has been completed.

Interrupt handler 中断(ISR)是一个响应硬件或软件中断异步执行的函数。ISR 通常会抢占当前线程的执行,允许以非常低的开销执行响应。线程执行只有在所有 ISR 工作完成后才恢复。

Concepts 概念

Any number of ISRs can be defined (limited only by available RAM), subject to the constraints imposed by underlying hardware.

可以定义任意数量的 ISRs (仅受可用 RAM 的限制) ,但要受到底层硬件所施加的约束。

An ISR has the following key properties:

ISR 具有以下关键属性:

  • An interrupt request (IRQ) signal that triggers the ISR.

    触发 ISR 的中断请求(IRQ)信号。

  • A priority level associated with the IRQ.

    与 IRQ 相关的优先级。

  • An interrupt handler function that is invoked to handle the interrupt.

    一个用来处理中断的 interrupt handler 函数。

  • An argument value that is passed to that function.

    传递给该函数的参数值。

An IDT or a vector table is used to associate a given interrupt source with a given ISR. Only a single ISR can be associated with a specific IRQ at any given time.

IDT 或向量表用于将给定中断源与给定 ISR 关联起来。在任何给定时间,只有一个 ISR 可以与特定的 IRQ 关联。

Multiple ISRs can utilize the same function to process interrupts, allowing a single function to service a device that generates multiple types of interrupts or to service multiple devices (usually of the same type). The argument value passed to an ISR’s function allows the function to determine which interrupt has been signaled.

多个 isr 可以利用同一功能处理中断,允许单一功能服务于生成多种类型中断的设备或服务于多种设备(通常是同一类型)。传递给 ISR 函数的参数值允许该函数确定哪个中断已被标记。

The kernel provides a default ISR for all unused IDT entries. This ISR generates a fatal system error if an unexpected interrupt is signaled.

内核为所有未使用的 IDT 条目提供了默认的 ISR。如果有意外的中断发出信号,这个 ISR 会产生一个致命的系统错误。

The kernel supports interrupt nesting. This allows an ISR to be preempted in mid-execution if a higher priority interrupt is signaled. The lower priority ISR resumes execution once the higher priority ISR has completed its processing.

内核支持中断嵌套。这允许在高优先级中断信号发出时,ISR 在执行中被抢占。一旦较高优先级的 ISR 完成处理,较低优先级的 ISR 将恢复执行。

An ISR’s interrupt handler function executes in the kernel’s interrupt context. This context has its own dedicated stack area (or, on some architectures, stack areas). The size of the interrupt context stack must be capable of handling the execution of multiple concurrent ISRs if interrupt nesting support is enabled.

ISR 的 interrupt handler/值函数在内核的中断上下文中执行。这个上下文有它自己的专用堆栈区域(或者,在某些架构中,堆栈区域)。如果启用了中断嵌套支持,中断上下文堆栈的大小必须能够处理多个并发 isr 的执行。

Important

重要事项

Many kernel APIs can be used only by threads, and not by ISRs. In cases where a routine may be invoked by both threads and ISRs the kernel provides the k_is_in_isr() API to allow the routine to alter its behavior depending on whether it is executing as part of a thread or as part of an ISR.

许多内核 api 只能由线程使用,而不能由 ISRs 使用。在线程和 ISR 都可以调用例程的情况下,内核提供了 k_is_In_ISR() API,允许例程根据它是作为线程的一部分还是作为 ISR 的一部分执行来改变其行为。

Multi-level Interrupt handling 多级中断处理

A hardware platform can support more interrupt lines than natively-provided through the use of one or more nested interrupt controllers. Sources of hardware interrupts are combined into one line that is then routed to the parent controller.

通过使用一个或多个嵌套的中断控制器,硬件平台可以支持比原生提供的更多的中断线。硬件中断的源被组合成一条线路,然后被路由到父控制器。

If nested interrupt controllers are supported, CONFIG_MULTI_LEVEL_INTERRUPTS should be set to 1, and CONFIG_2ND_LEVEL_INTERRUPTS and CONFIG_3RD_LEVEL_INTERRUPTS configured as well, based on the hardware architecture.

如果支持嵌套的中断控制器,则应该根据硬件结构将 config_multi_level_interrupts 设置为1,并配置 config_2nd_level_interrupts 和 config_3rd_level_interrupts。

A unique 32-bit interrupt number is assigned with information embedded in it to select and invoke the correct Interrupt Service Routine (ISR). Each interrupt level is given a byte within this 32-bit number, providing support for up to four interrupt levels using this arch, as illustrated and explained below:

一个唯一的32位中断号被分配了嵌入在其中的信息来选择和调用正确的中断 interrupt handler (ISR)。每个中断级别都在这个32位数字内给出一个字节,使用这个拱形提供最多4个中断级别的支持,如下所示:

          9             2   0
   _____________        (LEVEL 1)
  5       |         A   |
_______        _______  (LEVEL 2)
  |   C                       B
_______                         (LEVEL 3)
        D

There are three interrupt levels shown here.

这里显示了三个中断级别。

  • ‘-’ means interrupt line and is numbered from 0 (right most).

    ‘-’表示中断行,从0开始编号(最右边)。

  • LEVEL 1 has 12 interrupt lines, with two lines (2 and 9) connected to nested controllers and one device ‘A’ on line 4.

    LEVEL 1有12条中断线,其中两条线(2和9)连接到嵌套控制器,一条线(4)上的设备“ a”。

  • One of the LEVEL 2 controllers has interrupt line 5 connected to a LEVEL 3 nested controller and one device ‘C’ on line 3.

    其中一个 LEVEL 2控制器的中断线路5连接到 LEVEL 3嵌套控制器和第3线上的一个设备 c。

  • The other LEVEL 2 controller has no nested controllers but has one device ‘B’ on line 2.

    另一个 LEVEL 2控制器没有嵌套的控制器,但在第2行有一个设备 b。

  • The LEVEL 3 controller has one device ‘D’ on line 2.

    LEVEL 3控制器在2号线上有一个 d 设备。

Here’s how unique interrupt numbers are generated for each hardware interrupt. Let’s consider four interrupts shown above as A, B, C, and D:

下面是如何为每个硬件中断生成唯一的中断号。让我们考虑上面显示的四个中断: a、 b、 c 和 d:

A -> 0x00000004
B -> 0x00000302
C -> 0x00000409
D -> 0x00030609

Note 注意

The bit positions for LEVEL 2 and onward are offset by 1, as 0 means that interrupt number is not present for that level. For our example, the LEVEL 3 controller has device D on line 2, connected to the LEVEL 2 controller’s line 5, that is connected to the LEVEL 1 controller’s line 9 (2 -> 5 -> 9). Because of the encoding offset for LEVEL 2 and onward, device D is given the number 0x00030609.

级别2及以上级别的位位置被1抵消,因为0表示该级别不存在中断数。对于我们的示例,LEVEL 3控制器在第2行有设备 d,连接到 LEVEL 2控制器的第5行,该第5行连接到 LEVEL 1控制器的第9行(2-> 5-> 9)。由于 LEVEL 2及以后的编码偏移量,设备 d 被赋予编号0x00030609。

Preventing Interruptions 防止干扰

In certain situations it may be necessary for the current thread to prevent ISRs from executing while it is performing time-sensitive or critical section operations.

在某些情况下,当前线程在执行时间敏感或关键节操作时,可能需要阻止 isr 的执行。

A thread may temporarily prevent all IRQ handling in the system using an IRQ lock. This lock can be applied even when it is already in effect, so routines can use it without having to know if it is already in effect. The thread must unlock its IRQ lock the same number of times it was locked before interrupts can be once again processed by the kernel while the thread is running.

线程可以使用 IRQ 锁临时阻止系统中的所有 IRQ 处理。这个锁即使已经生效,也可以应用,因此例程可以使用它,而不必知道它是否已经生效。线程必须解锁它的 IRQ 锁,解锁次数必须与它被锁定的次数相同,然后在线程运行时,内核才能再次处理中断。

Important 重要事项

The IRQ lock is thread-specific. If thread A locks out interrupts then performs an operation that puts itself to sleep (e.g. sleeping for N milliseconds), the thread’s IRQ lock no longer applies once thread A is swapped out and the next ready thread B starts to run.

IRQ 锁是线程特定的。如果线程 a 锁定了中断,然后执行一个使自己处于休眠状态的操作(例如,休眠 n 毫秒) ,一旦线程 a 被换出,下一个准备好的线程 b 开始运行,线程的 IRQ 锁就不再适用。

This means that interrupts can be processed while thread B is running unless thread B has also locked out interrupts using its own IRQ lock. (Whether interrupts can be processed while the kernel is switching between two threads that are using the IRQ lock is architecture-specific.)

这意味着在线程 b 运行时可以处理中断,除非线程 b 也使用自己的 IRQ 锁来锁定中断。(当内核在使用 IRQ 锁的两个线程之间切换时,是否可以处理中断是特定于体系结构的。)

When thread A eventually becomes the current thread once again, the kernel re-establishes thread A’s IRQ lock. This ensures thread A won’t be interrupted until it has explicitly unlocked its IRQ lock.

当线程 a 最终再次成为当前线程时,内核重新建立线程 a 的 IRQ 锁。这确保线程 a 在显式解锁其 IRQ 锁之前不会被中断。

If thread A does not sleep but does make a higher-priority thread B ready, the IRQ lock will inhibit any preemption that would otherwise occur. Thread B will not run until the next reschedule point reached after releasing the IRQ lock.

如果线程 a 没有休眠,但是确实使高优先级线程 b 准备好了,则 IRQ 锁将禁止否则会发生的任何抢占。线程 b 将不会运行,直到释放 IRQ 锁后到达的下一个重新调度点。

Alternatively, a thread may temporarily disable a specified IRQ so its associated ISR does not execute when the IRQ is signaled. The IRQ must be subsequently enabled to permit the ISR to execute.

或者,一个线程可以临时禁用指定的 IRQ,这样当 IRQ 发出信号时,其关联的 ISR 不会执行。必须随后启用 IRQ 以允许 ISR 执行。

Important 重要事项

Disabling an IRQ prevents all threads in the system from being preempted by the associated ISR, not just the thread that disabled the IRQ.

禁用 IRQ 可以防止系统中的所有线程被相关的 ISR 抢占,而不仅仅是禁用 IRQ 的线程。

Zero Latency Interrupts 零延迟中断

Preventing interruptions by applying an IRQ lock may increase the observed interrupt latency. A high interrupt latency, however, may not be acceptable for certain low-latency use-cases.

通过应用 IRQ 锁来防止中断可能会增加观察到的中断延迟。然而,对于某些低延迟的用例来说,高中断延迟可能是不可接受的。

The kernel addresses such use-cases by allowing interrupts with critical latency constraints to execute at a priority level that cannot be blocked by interrupt locking. These interrupts are defined as zero-latency interrupts. The support for zero-latency interrupts requires CONFIG_ZERO_LATENCY_IRQS to be enabled. In addition to that, the flag IRQ_ZERO_LATENCY must be passed to IRQ_CONNECT or IRQ_DIRECT_CONNECT macros to configure the particular interrupt with zero latency.

内核通过允许具有关键延迟约束的中断在优先级上执行,而中断锁定不能阻止这种情况来处理这种用例。这些中断被定义为零延迟中断。对零延迟中断的支持需要启用 config_zero_latency_irqs。除此之外,IRQ_zero_latency 标志必须传递给 IRQ_connect 或 IRQ_direct_connect 宏来配置零延迟的特定中断。

Zero-latency interrupts are expected to be used to manage hardware events directly, and not to interoperate with the kernel code at all. They should treat all kernel APIs as undefined behavior (i.e. an application that uses the APIs inside a zero-latency interrupt context is responsible for directly verifying correct behavior). Zero-latency interrupts may not modify any data inspected by kernel APIs invoked from normal Zephyr contexts and shall not generate exceptions that need to be handled synchronously (e.g. kernel panic).

零延迟中断被期望用于直接管理硬件事件,而根本不与内核代码进行互操作。他们应该把所有的内核 api 都当作未定义行为(即一个在零延迟中断上下文中使用 api 的应用程序负责直接验证正确的行为)。零延迟中断不能修改从普通 Zephyr 上下文调用的内核 api 检查的任何数据,也不能生成需要同步处理的异常(例如内核恐慌)。

Important 重要事项

Zero-latency interrupts are supported on an architecture-specific basis. The feature is currently implemented in the ARM Cortex-M architecture variant.

零延迟中断在架构特定的基础上得到支持。该特性目前在 ARM Cortex-M 架构变体中实现。

Offloading ISR Work 卸载 ISR 工作

An ISR should execute quickly to ensure predictable system operation. If time consuming processing is required the ISR should offload some or all processing to a thread, thereby restoring the kernel’s ability to respond to other interrupts.

ISR 应该快速执行,以确保可预测的系统操作。如果需要耗时的处理,ISR 应该将部分或全部处理卸载到一个线程中,从而恢复内核响应其他中断的能力。

The kernel supports several mechanisms for offloading interrupt-related processing to a thread.

内核支持几种将与中断相关的处理卸载到线程的机制。

  • An ISR can signal a helper thread to do interrupt-related processing using a kernel object, such as a FIFO, LIFO, or semaphore.

    ISR 可以通过使用内核对象(如 FIFO、 LIFO 或信号量)向辅助线程发出信号,让它执行与中断相关的处理。

  • An ISR can instruct the system workqueue thread to execute a work item. (See Workqueue Threads.)

    ISR 可以指示系统工作队列线程执行工作项

When an ISR offloads work to a thread, there is typically a single context switch to that thread when the ISR completes, allowing interrupt-related processing to continue almost immediately. However, depending on the priority of the thread handling the offload, it is possible that the currently executing cooperative thread or other higher-priority threads may execute before the thread handling the offload is scheduled.

当 ISR 将工作卸载给一个线程时,通常在 ISR 完成时有一个单独的上下文切换到该线程,允许中断相关的处理几乎立即继续。然而,根据处理卸载的线程的优先级,当前正在执行的协作线程或其他高优先级线程可能会在调度处理卸载的线程之前执行。

Implementation 实施

Defining a regular ISR 定义常规 ISR

An ISR is defined at runtime by calling IRQ_CONNECT. It must then be enabled by calling irq_enable().

ISR 在运行时通过调用 IRQ_connect 来定义,然后必须通过调用 IRQ_enable()来启用。

Important 重要事项

IRQ_CONNECT() is not a C function and does some inline assembly magic behind the scenes. All its arguments must be known at build time. Drivers that have multiple instances may need to define per-instance config functions to configure each instance of the interrupt.

IRQ_connect()不是一个 c 函数,它在幕后执行一些内联汇编魔术。在构建时必须知道它的所有参数。具有多个实例的驱动程序可能需要定义每个实例的配置函数来配置中断的每个实例。

The following code defines and enables an ISR.

下面的代码定义并启用 ISR。

#define MY_DEV_IRQ  24       /* device uses IRQ 24 */
#define MY_DEV_PRIO  2       /* device uses interrupt priority 2 */
/* argument passed to my_isr(), in this case a pointer to the device */
#define MY_ISR_ARG  DEVICE_GET(my_device)
#define MY_IRQ_FLAGS 0       /* IRQ flags */

void my_isr(void *arg)
{
   ... /* ISR code */
}

void my_isr_installer(void)
{
   ...
   IRQ_CONNECT(MY_DEV_IRQ, MY_DEV_PRIO, my_isr, MY_ISR_ARG, MY_IRQ_FLAGS);
   irq_enable(MY_DEV_IRQ);
   ...
}

Since the IRQ_CONNECT macro requires that all its parameters be known at build time, in some cases this may not be acceptable. It is also possible to install interrupts at runtime with irq_connect_dynamic(). It is used in exactly the same way as IRQ_CONNECT:

因为 IRQ_connect 宏要求在构建时知道它的所有参数,在某些情况下这可能是不可接受的。还可以在运行时使用 irq_connect_dynamic()安装中断。它的使用方式与 IRQ connect 完全相同:

void my_isr_installer(void)
{
   ...
   irq_connect_dynamic(MY_DEV_IRQ, MY_DEV_PRIO, my_isr, MY_ISR_ARG,
                       MY_IRQ_FLAGS);
   irq_enable(MY_DEV_IRQ);
   ...
}

Dynamic interrupts require the CONFIG_DYNAMIC_INTERRUPTS option to be enabled. Removing or re-configuring a dynamic interrupt is currently unsupported.

动态中断需要启用 config_dynamic_interrupts 选项。目前不支持删除或重新配置动态中断。

Defining a ‘direct’ ISR 定义“直接”ISR

Regular Zephyr interrupts introduce some overhead which may be unacceptable for some low-latency use-cases. Specifically:

常规的 Zephyr 中断引入了一些开销,这对于一些低延迟的用例来说可能是不可接受的:

  • The argument to the ISR is retrieved and passed to the ISR

    检索到 ISR 的参数并将其传递给 ISR

  • If power management is enabled and the system was idle, all the hardware will be resumed from low-power state before the ISR is executed, which can be very time-consuming

    如果启用了电源管理并且系统处于空闲状态,那么在执行 ISR 之前,所有的硬件都将从低功耗状态恢复,这可能非常耗时

  • Although some architectures will do this in hardware, other architectures need to switch to the interrupt stack in code

    尽管一些体系结构在硬件上可以做到这一点,但是其他体系结构需要在代码中切换到中断堆栈

  • After the interrupt is serviced, the OS then performs some logic to potentially make a scheduling decision.

    在中断服务之后,操作系统执行一些逻辑来潜在地做出调度决策。

Zephyr supports so-called ‘direct’ interrupts, which are installed via IRQ_DIRECT_CONNECT. These direct interrupts have some special implementation requirements and a reduced feature set; see the definition of IRQ_DIRECT_CONNECT for details.

Zephyr 支持所谓的“直接”中断,它是通过 IRQ_direct_connect 安装的。这些直接中断有一些特殊的实现需求和一个简化的特性集; 详细信息请参阅 IRQ_direct_connect 的定义。

The following code demonstrates a direct ISR:

下面的代码展示了一个直接的 ISR:

#define MY_DEV_IRQ  24       /* device uses IRQ 24 */
#define MY_DEV_PRIO  2       /* device uses interrupt priority 2 */
/* argument passed to my_isr(), in this case a pointer to the device */
#define MY_IRQ_FLAGS 0       /* IRQ flags */

ISR_DIRECT_DECLARE(my_isr)
{
   do_stuff();
   ISR_DIRECT_PM(); /* PM done after servicing interrupt for best latency */
   return 1; /* We should check if scheduling decision should be made */
}

void my_isr_installer(void)
{
   ...
   IRQ_DIRECT_CONNECT(MY_DEV_IRQ, MY_DEV_PRIO, my_isr, MY_IRQ_FLAGS);
   irq_enable(MY_DEV_IRQ);
   ...
}

Installation of dynamic direct interrupts is supported on an architecture-specific basis. (The feature is currently implemented in ARM Cortex-M architecture variant. Dynamic direct interrupts feature is exposed to the user via an ARM-only API.)

在特定于体系结构的基础上支持动态直接中断的安装。(该特性目前已在 ARM Cortex-M 体系结构变体中实现。动态直接中断特性是通过一个只支持 arm 的 API.)向用户公开的

Implementation Details 实施细节

Interrupt tables are set up at build time using some special build tools. The details laid out here apply to all architectures except x86, which are covered in the x86 Details section below.

中断表是在构建时使用一些特殊的构建工具设置的。这里列出的详细信息适用于除 x86之外的所有体系结构,x86在下面的 x86详细信息部分中介绍。

Any invocation of IRQ_CONNECT will declare an instance of struct _isr_list which is placed in a special .intList section:

任何对 IRQ_connect 的调用都会声明一个 struct_isr_list 的实例,该实例被放置在一个特殊的.intList 部分:

struct _isr_list {
    /** IRQ line number */
    int32_t irq;
    /** Flags for this IRQ, see ISR_FLAG_* definitions */
    int32_t flags;
    /** ISR to call */
    void *func;
    /** Parameter for non-direct IRQs */
    void *param;
};

Zephyr is built in two phases; the first phase of the build produces ${ZEPHYR_PREBUILT_EXECUTABLE}.elf which contains all the entries in the .intList section preceded by a header:

ZEPHYR 分两个阶段构建; 构建的第一阶段生成 ${ ZEPHYR_prebuilt_executable }。包含了所有的条目。在 intList 部分前面有一个头:

struct {
    void *spurious_irq_handler;
    void *sw_irq_handler;
    uint32_t num_isrs;
    uint32_t num_vectors;
    struct _isr_list isrs[];  <- of size num_isrs
};

This data consisting of the header and instances of struct _isr_list inside ${ZEPHYR_PREBUILT_EXECUTABLE}.elf is then used by the gen_isr_tables.py script to generate a C file defining a vector table and software ISR table that are then compiled and linked into the final application.

该数据包含 ${ ZEPHYR_prebuilt_executable }中 struct_isr_list 的头部和实例。然后,gen_ISR_tables.py 脚本使用 elf 生成一个定义向量表和软件 ISR 表的 c 文件,然后编译并链接到最终应用程序中。

The priority level of any interrupt is not encoded in these tables, instead IRQ_CONNECT also has a runtime component which programs the desired priority level of the interrupt to the interrupt controller. Some architectures do not support the notion of interrupt priority, in which case the priority argument is ignored.

任何中断的优先级别都不会被编码在这些表中,相反,IRQ_connect 还有一个运行时组件,该组件将中断的所需优先级编程到中断控制器。有些体系结构不支持中断优先级的概念,在这种情况下,优先级参数被忽略。

Vector Table 矢量表

A vector table is generated when CONFIG_GEN_IRQ_VECTOR_TABLE is enabled. This data structure is used natively by the CPU and is simply an array of function pointers, where each element n corresponds to the IRQ handler for IRQ line n, and the function pointers are:

在启用 config_gen_irq_vector_table 时生成一个向量表。这个数据结构由 CPU 本身使用,只是一个函数指针数组,其中每个元素 n 对应于 IRQ 行 n 的 IRQ 处理程序,函数指针如下:

  1. For ‘direct’ interrupts declared with IRQ_DIRECT_CONNECT, the handler function will be placed here.

    对于用 IRQ_direct_connect 声明的“直接”中断,处理程序函数将放在这里。

  2. For regular interrupts declared with IRQ_CONNECT, the address of the common software IRQ handler is placed here. This code does common kernel interrupt bookkeeping and looks up the ISR and parameter from the software ISR table.

    对于使用 IRQ_connect 声明的常规中断,通用软件 IRQ 处理程序的地址放在这里。这段代码执行常见的内核中断簿记,并从软件 ISR 表中查找 ISR 和参数。

  3. For interrupt lines that are not configured at all, the address of the spurious IRQ handler will be placed here. The spurious IRQ handler causes a system fatal error if encountered.

    对于根本没有配置的中断行,假的 IRQ 处理程序的地址将放在这里。如果遇到虚假的 IRQ 处理程序,将导致系统致命错误。

Some architectures (such as the Nios II internal interrupt controller) have a common entry point for all interrupts and do not support a vector table, in which case the CONFIG_GEN_IRQ_VECTOR_TABLE option should be disabled.

有些体系结构(例如 Nios II 内部中断控制器)对所有中断都有一个公共的入口点,并且不支持向量表,在这种情况下应该禁用 CONFIG_gen_irq_vector_table 选项。

Some architectures may reserve some initial vectors for system exceptions and declare this in a table elsewhere, in which case CONFIG_GEN_IRQ_START_VECTOR needs to be set to properly offset the indices in the table.

一些架构可能会为系统异常保留一些初始向量,并在其他地方的表中声明它,在这种情况下,需要设置 CONFIG_gen_irq_start_vector 以正确地抵消表中的索引。

SW ISR Table

This is an array of struct _isr_table_entry:

这是一个 struct_isr_table_entry 数组:

struct _isr_table_entry {
    void *arg;
    void (*isr)(void *);
};

This is used by the common software IRQ handler to look up the ISR and its argument and execute it. The active IRQ line is looked up in an interrupt controller register and used to index this table.

通用软件 IRQ 处理程序使用它来查找 ISR 及其参数并执行它。活动的 IRQ 行在中断控制器寄存器中查找,并用于索引该表。

x86 Details 86 Details

The x86 architecture has a special type of vector table called the Interrupt Descriptor Table (IDT) which must be laid out in a certain way per the x86 processor documentation. It is still fundamentally a vector table, and the arch/x86/gen_idt.py tool uses the .intList section to create it. However, on APIC-based systems the indexes in the vector table do not correspond to the IRQ line. The first 32 vectors are reserved for CPU exceptions, and all remaining vectors (up to index 255) correspond to the priority level, in groups of 16. In this scheme, interrupts of priority level 0 will be placed in vectors 32-47, level 1 48-63, and so forth. When the arch/x86/gen_idt.py tool is constructing the IDT, when it configures an interrupt it will look for a free vector in the appropriate range for the requested priority level and set the handler there.

X86体系结构具有一种特殊类型的向量表,称为中断描述符表(Interrupt Descriptor Table,IDT) ,根据 x86处理器文档,必须以某种方式对其进行布局。它基本上仍然是一个向量表,arch/x86/gen_idt.py 工具使用。来创建它。但是,在基于 apic 的系统中,向量表中的索引不对应于 IRQ 行。前32个向量是为 CPU 异常保留的,所有剩下的向量(直到索引255)都对应于优先级,以16为一组。在这个方案中,优先级0的中断将被放置在向量32-47、148-63等等中。当 arch/x86/gen_IDT.py 工具构造 IDT 时,当它配置一个中断时,它会在请求的优先级的适当范围内寻找一个空闲向量,并在那里设置处理程序。

On x86 when an interrupt or exception vector is executed by the CPU, there is no foolproof way to determine which vector was fired, so a software ISR table indexed by IRQ line is not used. Instead, the IRQ_CONNECT call creates a small assembly language function which calls the common interrupt code in _interrupt_enter() with the ISR and parameter as arguments. It is the address of this assembly interrupt stub which gets placed in the IDT. For interrupts declared with IRQ_DIRECT_CONNECT the parameterless ISR is placed directly in the IDT.

在 x86上,当 CPU 执行中断或异常向量时,没有万无一失的方法来确定触发了哪个向量,因此不使用按 IRQ 行编制索引的软件 ISR 表。相反,IRQ_connect 调用创建了一个小的汇编语言函数,该函数使用 ISR 和参数作为参数调用_interrupt_enter()中的公共中断代码。它是这个程序集中断存根的地址,它被放置在 IDT 中。对于用 IRQ_direct_connect 声明的中断,无参数的 ISR 直接放置在 IDT 中。

On systems where the position in the vector table corresponds to the interrupt’s priority level, the interrupt controller needs to know at runtime what vector is associated with an IRQ line. arch/x86/gen_idt.py additionally creates an _irq_to_interrupt_vector array which maps an IRQ line to its configured vector in the IDT. This is used at runtime by IRQ_CONNECT to program the IRQ-to-vector association in the interrupt controller.

在向量表中的位置对应于中断的优先级别的系统中,中断控制器需要在运行时知道与 IRQ 行相关的向量。Arch/x86/gen_IDT.py 另外创建了一个 _irq_to_interrupt_vector 数组,该数组将 IRQ 行映射到 IDT 中配置的向量。在运行时,IRQ_connect 使用它在中断控制器中编写 IRQ-to-vector 关联程序。

For dynamic interrupts, the build must generate some 4-byte dynamic interrupt stubs, one stub per dynamic interrupt in use. The number of stubs is controlled by the CONFIG_X86_DYNAMIC_IRQ_STUBS option. Each stub pushes an unique identifier which is then used to fetch the appropriate handler function and parameter out of a table populated when the dynamic interrupt was connected.

对于动态中断,构建必须生成一些4字节的动态中断存根,每个使用的动态中断一个存根。存根的数量由 config_x86_dynamic_irq_stubs 选项控制。每个存根推出一个唯一标识符,然后用于在连接动态中断时从填充的表中获取适当的处理程序函数和参数。

Suggested Uses 建议的用途

Use a regular or direct ISR to perform interrupt processing that requires a very rapid response, and can be done quickly without blocking.

使用常规或直接的 ISR 来执行需要非常快速响应的中断处理,并且可以在不阻塞的情况下快速完成。

Note

注意

Interrupt processing that is time consuming, or involves blocking, should be handed off to a thread. See Offloading ISR Work for a description of various techniques that can be used in an application.

耗时的中断处理,或者包含阻塞,应该交给一个线程。有关可以在应用程序中使用的各种技术的说明,请参阅卸载 ISR 工作。

Configuration Options 配置选项

Related configuration options:

相关配置选项:

Additional architecture-specific and device-specific configuration options also exist.

还有其他特定于体系结构和特定于设备的配置选项。

Polling API 轮询 API

The polling API is used to wait concurrently for any one of multiple conditions to be fulfilled.

轮询 API 用于并发等待满足多个条件中的任何一个。

Concepts 概念

The polling API’s main function is k_poll(), which is very similar in concept to the POSIX poll() function, except that it operates on kernel objects rather than on file descriptors.

轮询 API 的主函数是 k_poll() ,它在概念上与 POSIX poll()函数非常相似,只是它对内核对象而不是文件描述符进行操作。

The polling API allows a single thread to wait concurrently for one or more conditions to be fulfilled without actively looking at each one individually.

轮询 API 允许单个线程并发地等待一个或多个条件得到满足,而不需要单独地查看每个条件。

There is a limited set of such conditions:

此类条件有限:

  • a semaphore becomes available

    一个信号量变得可用

  • a kernel FIFO contains data ready to be retrieved

    内核的 FIFO 包含可以检索的数据

  • a poll signal is raised

    民意测验结果出来了

A thread that wants to wait on multiple conditions must define an array of poll events, one for each condition.

想要在多个条件下等待的线程必须定义一个轮询事件数组,每个条件一个。

All events in the array must be initialized before the array can be polled on.

必须初始化数组中的所有事件,然后才能对数组进行轮询。

Each event must specify which type of condition must be satisfied so that its state is changed to signal the requested condition has been met.

每个事件必须指定必须满足的条件类型,以便其状态被改变为满足请求条件的信号。

Each event must specify what kernel object it wants the condition to be satisfied.

每个事件必须指定它希望满足条件的内核对象。

Each event must specify which mode of operation is used when the condition is satisfied.

每个事件必须指定满足条件时使用的操作模式。

Each event can optionally specify a tag to group multiple events together, to the user’s discretion.

每个事件都可以选择指定一个标记,将多个事件分组到一起,由用户决定。

Apart from the kernel objects, there is also a poll signal pseudo-object type that be directly signaled.

除了内核对象之外,还有一个轮询信号伪对象类型,可以直接用信号表示。

The k_poll() function returns as soon as one of the conditions it is waiting for is fulfilled. It is possible for more than one to be fulfilled when k_poll() returns, if they were fulfilled before k_poll() was called, or due to the preemptive multi-threading nature of the kernel. The caller must look at the state of all the poll events in the array to figured out which ones were fulfilled and what actions to take.

函数的 k_poll()函数在满足等待的条件之一时返回。当 k_poll()返回时,如果它们在调用 k_poll()之前就已满足,或者由于内核的抢占式多线程特性,则可能会有多个实现。调用方必须查看数组中所有 poll 事件的状态,以便确定哪些事件已经完成,以及应该采取哪些操作。

Currently, there is only one mode of operation available: the object is not acquired. As an example, this means that when k_poll() returns and the poll event states that the semaphore is available, the caller of k_poll() must then invoke k_sem_take() to take ownership of the semaphore. If the semaphore is contested, there is no guarantee that it will be still available when k_sem_give() is called.

目前,只有一种操作模式可用: 对象不是被获取的。例如,这意味着当 k_poll()返回并且 poll 事件声明信号量可用时,k_poll()的调用者必须调用 k_sem_take()来获得信号量的所有权。如果信号量存在争用,则不能保证在调用 k_sem_give()时它仍然可用。

Implementation 实施

Using k_poll() 使用 k_poll()

The main API is k_poll(), which operates on an array of poll events of type k_poll_event. Each entry in the array represents one event a call to k_poll() will wait for its condition to be fulfilled.

主要 API 是 k_poll() ,它操作类型为 k_poll_event 的 poll 事件数组。数组中的每个条目表示一个事件,对 k_poll()的调用将等待满足其条件。

They can be initialized using either the runtime initializers K_POLL_EVENT_INITIALIZER() or k_poll_event_init(), or the static initializer K_POLL_EVENT_STATIC_INITIALIZER(). An object that matches the type specified must be passed to the initializers. The mode must be set to K_POLL_MODE_NOTIFY_ONLY. The state must be set to K_POLL_STATE_NOT_READY (the initializers take care of this). The user tag is optional and completely opaque to the API: it is there to help a user to group similar events together. Being optional, it is passed to the static initializer, but not the runtime ones for performance reasons. If using runtime initializers, the user must set it separately in the k_poll_event data structure. If an event in the array is to be ignored, most likely temporarily, its type can be set to K_POLL_TYPE_IGNORE.

它们可以使用运行时初始化程序 k_poll_event_initializer()或者 k_poll_event_init() ,或者静态初始化程序 k_poll_event_static_initializer()进行初始化。必须将与指定类型匹配的对象传递给初始值设定项。模式必须设置为 k_poll_mode_notify_only。状态必须设置为 k_poll_state_not_ready (由初始化器负责)。用户标记是可选的,对于 API 完全不透明: 它是用来帮助用户将类似的事件组织在一起的。由于是可选的,它被传递给静态初始值设定项,但出于性能原因,不传递给运行时初始值设定项。如果使用运行时初始值设定项,用户必须在 k_poll_event 数据结构中单独设置它。如果要忽略数组中的某个事件,很可能是暂时的,那么可以将其类型设置为 k_poll_type_ignore。

struct k_poll_event events[2] = {
    K_POLL_EVENT_STATIC_INITIALIZER(K_POLL_TYPE_SEM_AVAILABLE,
                                    K_POLL_MODE_NOTIFY_ONLY,
                                    &my_sem, 0),
    K_POLL_EVENT_STATIC_INITIALIZER(K_POLL_TYPE_FIFO_DATA_AVAILABLE,
                                    K_POLL_MODE_NOTIFY_ONLY,
                                    &my_fifo, 0),
};

or at runtime

或者在运行时

struct k_poll_event events[2];
void some_init(void)
{
    k_poll_event_init(&events[0],
                      K_POLL_TYPE_SEM_AVAILABLE,
                      K_POLL_MODE_NOTIFY_ONLY,
                      &my_sem);

    k_poll_event_init(&events[1],
                      K_POLL_TYPE_FIFO_DATA_AVAILABLE,
                      K_POLL_MODE_NOTIFY_ONLY,
                      &my_fifo);

    // tags are left uninitialized if unused
}

After the events are initialized, the array can be passed to k_poll(). A timeout can be specified to wait only for a specified amount of time, or the special values K_NO_WAIT and K_FOREVER to either not wait or wait until an event condition is satisfied and not sooner.

事件初始化后,数组可以传递给 k_poll()。超时可以指定为只等待指定的时间,或者特殊值 k_no_wait 和 k_forever 不等待或等待直到事件条件得到满足,而不是更早。

A list of pollers is offered on each semaphore or FIFO and as many events can wait in it as the app wants. Notice that the waiters will be served in first-come-first-serve order, not in priority order.

每个信号量或 FIFO 上都提供了一个 poller 列表,并且可以根据应用程序的需要在其中等待尽可能多的事件。请注意,服务员是按先到先服务的顺序服务,而不是按优先次序服务。

In case of success, k_poll() returns 0. If it times out, it returns -EAGAIN.

如果成功,k_poll()返回0。如果超时,返回 -EAGAIN。

// assume there is no contention on this semaphore and FIFO
// -EADDRINUSE will not occur; the semaphore and/or data will be available

void do_stuff(void)
{
    rc = k_poll(events, 2, 1000);
    if (rc == 0) {
        if (events[0].state == K_POLL_STATE_SEM_AVAILABLE) {
            k_sem_take(events[0].sem, 0);
        } else if (events[1].state == K_POLL_STATE_FIFO_DATA_AVAILABLE) {
            data = k_fifo_get(events[1].fifo, 0);
            // handle data
        }
    } else {
        // handle timeout
    }
}

When k_poll() is called in a loop, the events state must be reset to K_POLL_STATE_NOT_READY by the user.

在循环中调用 k_poll()时,用户必须将事件状态重置为 k_poll_state_not_ready。

void do_stuff(void)
{
    for(;;) {
        rc = k_poll(events, 2, K_FOREVER);
        if (events[0].state == K_POLL_STATE_SEM_AVAILABLE) {
            k_sem_take(events[0].sem, 0);
        } else if (events[1].state == K_POLL_STATE_FIFO_DATA_AVAILABLE) {
            data = k_fifo_get(events[1].fifo, 0);
            // handle data
        }
        events[0].state = K_POLL_STATE_NOT_READY;
        events[1].state = K_POLL_STATE_NOT_READY;
    }
}

Using k_poll_signal_raise() 使用 k_poll_signal_raise()

One of the types of events is K_POLL_TYPE_SIGNAL: this is a “direct” signal to a poll event. This can be seen as a lightweight binary semaphore only one thread can wait for.

其中一种类型的事件是 k_poll_type_signal: 这是一个“直接”发送给轮询事件的信号。这可以看作是一个只有一个线程可以等待的轻量级二进制信号量。

A poll signal is a separate object of type k_poll_signal that must be attached to a k_poll_event, similar to a semaphore or FIFO. It must first be initialized either via K_POLL_SIGNAL_INITIALIZER() or k_poll_signal_init().

轮询信号是类型为 k_poll_信号的单独对象,必须附加到 k_poll_事件,类似于信号量或 FIFO。它必须首先通过 k_poll_signal_initializer()或 k_poll_signal_init()进行初始化。

struct k_poll_signal signal;
void do_stuff(void)
{
    k_poll_signal_init(&signal);
}

It is signaled via the k_poll_signal_raise() function. This function takes a user result parameter that is opaque to the API and can be used to pass extra information to the thread waiting on the event.

它是通过 k_poll_signal_raise()函数发出信号的。该函数接受一个对 API 不透明的用户结果参数,该参数可用于向等待事件的线程传递额外的信息。

struct k_poll_signal signal;

// thread A
void do_stuff(void)
{
    k_poll_signal_init(&signal);

    struct k_poll_event events[1] = {
        K_POLL_EVENT_INITIALIZER(K_POLL_TYPE_SIGNAL,
                                 K_POLL_MODE_NOTIFY_ONLY,
                                 &signal),
    };

    k_poll(events, 1, K_FOREVER);

    if (events.signal->result == 0x1337) {
        // A-OK!
    } else {
        // weird error
    }
}

// thread B
void signal_do_stuff(void)
{
    k_poll_signal_raise(&signal, 0x1337);
}

If the signal is to be polled in a loop, both its event state and its signaled field must be reset on each iteration if it has been signaled.

如果信号要在循环中轮询,那么如果已经发出信号,那么在每次迭代中都必须重置它的事件状态和已发出的字段。

struct k_poll_signal signal;
void do_stuff(void)
{
    k_poll_signal_init(&signal);

    struct k_poll_event events[1] = {
        K_POLL_EVENT_INITIALIZER(K_POLL_TYPE_SIGNAL,
                                 K_POLL_MODE_NOTIFY_ONLY,
                                 &signal),
    };

    for (;;) {
        k_poll(events, 1, K_FOREVER);

        if (events[0].signal->result == 0x1337) {
            // A-OK!
        } else {
            // weird error
        }

        events[0].signal->signaled = 0;
        events[0].state = K_POLL_STATE_NOT_READY;
    }
}

Note that poll signals are not internally synchronized. A k_poll call that is passed a signal will return after any code in the system calls k_poll_signal_raise(). But if the signal is being externally managed and reset via k_poll_signal_init(), it is possible that by the time the application checks, the event state may no longer be equal to K_POLL_STATE_SIGNALED, and a (naive) application will miss events. Best practice is always to reset the signal only from within the thread invoking the k_poll() loop, or else to use some other event type which tracks event counts: semaphores and FIFOs more more error-proof in this sense because they can’t “miss” events, architecturally.

请注意,轮询信号不是内部同步的。传递信号的 k_poll 调用将在系统中的任何代码调用 k_poll_signal_raise()之后返回。但是如果信号是通过 k_poll_signal_init()进行外部管理和重置的,那么在应用程序检查时,事件状态可能不再等于 k_poll_state_signalded,并且一个(幼稚的)应用程序将遗漏事件。最佳实践始终是只从调用 k_poll()循环的线程中重置信号,或者使用其他事件类型来跟踪事件计数: 信号量和 FIFOs 在这个意义上更加防错,因为它们在架构上不能“错过”事件。

Suggested Uses 建议的用途

Use k_poll() to consolidate multiple threads that would be pending on one object each, saving possibly large amounts of stack space.

使用 k_poll()合并将挂起在每个对象上的多个线程,可能会节省大量堆栈空间。

Use a poll signal as a lightweight binary semaphore if only one thread pends on it.

如果只有一个线程在轮询信号上,则使用轮询信号作为轻量级二进制信号量。

Note

注意

Because objects are only signaled if no other thread is waiting for them to become available and only one thread can poll on a specific object, polling is best used when objects are not subject of contention between multiple threads, basically when a single thread operates as a main “server” or “dispatcher” for multiple objects and is the only one trying to acquire these objects.

因为只有当没有其他线程等待对象变为可用且只有一个线程可以轮询特定对象时,对象才是有信号的,所以当对象不是多个线程之间争用的对象时,轮询最好使用,基本上就是当一个线程作为多个对象的主“服务器”或“调度器”进行操作,并且是唯一一个试图获取这些对象的对象时。

Configuration Options 配置选项

Related configuration options:

相关配置选项:

Semaphores 信号量

A semaphore is a kernel object that implements a traditional counting semaphore.

信号量是实现传统计数信号量的内核对象。

Concepts 概念

Any number of semaphores can be defined (limited only by available RAM). Each semaphore is referenced by its memory address.

可以定义任意数量的信号量(仅限于可用的 RAM)。每个信号量都由其内存地址引用。

A semaphore has the following key properties:

信号量具有以下关键属性:

  • A count that indicates the number of times the semaphore can be taken. A count of zero indicates that the semaphore is unavailable.

    一个计数器,指示可以使用信号量的次数。计数为零表示信号量不可用。

  • A limit that indicates the maximum value the semaphore’s count can reach.

    指示信号量计数器可达到的最大值的限制。

A semaphore must be initialized before it can be used. Its count must be set to a non-negative value that is less than or equal to its limit.

信号量在使用之前必须进行初始化。其计数必须设置为小于或等于其极限的非负值。

A semaphore may be given by a thread or an ISR. Giving the semaphore increments its count, unless the count is already equal to the limit.

信号量可以由线程或 ISR 给定。给予信号量增加其计数,除非计数已经等于限制。

A semaphore may be taken by a thread. Taking the semaphore decrements its count, unless the semaphore is unavailable (i.e. at zero). When a semaphore is unavailable a thread may choose to wait for it to be given. Any number of threads may wait on an unavailable semaphore simultaneously. When the semaphore is given, it is taken by the highest priority thread that has waited longest.

一个信号量可以由一个线程取得。获取信号量会减少其计数,除非信号量不可用(即为零)。当一个信号量不可用时,线程可以选择等待它被给出。任意数量的线程可以同时等待一个不可用的信号量。当给定信号量时,等待时间最长的优先级最高的线程将使用该信号量。

Note

注意

You may initialize a “full” semaphore (count equal to limit) to limit the number of threads able to execute the critical section at the same time. You may also initialize an empty semaphore (count equal to 0, with a limit greater than 0) to create a gate through which no waiting thread may pass until the semaphore is incremented. All standard use cases of the common semaphore are supported.

您可以初始化一个“完整的”信号量(计数等于 limit) ,以限制能够同时执行临界区的线程数量。您还可以初始化一个空信号量(计数等于0,限制大于0) ,以创建一个门,在信号量递增之前,任何等待的线程都不能通过这个门。支持公共信号量的所有标准用例。

Note

注意

The kernel does allow an ISR to take a semaphore, however the ISR must not attempt to wait if the semaphore is unavailable.

内核允许 ISR 采用信号量,但是如果信号量不可用,ISR 就不能尝试等待。

Implementation 实施

Defining a Semaphore 定义信号量

A semaphore is defined using a variable of type k_sem. It must then be initialized by calling k_sem_init().

信号量使用类型为 k_sem 的变量来定义,然后必须通过调用 k_sem_init()来初始化它。

The following code defines a semaphore, then configures it as a binary semaphore by setting its count to 0 and its limit to 1.

下面的代码定义了一个信号量,然后通过将其计数设置为0并将其限制为1来将其配置为二进制信号量。

struct k_sem my_sem;

k_sem_init(&my_sem, 0, 1);

Alternatively, a semaphore can be defined and initialized at compile time by calling K_SEM_DEFINE.

另外,可以通过调用 k_sem_define 在编译时定义和初始化信号量。

The following code has the same effect as the code segment above.

下面的代码与上面的代码段具有相同的效果。

K_SEM_DEFINE(my_sem, 0, 1);

Giving a Semaphore 发送信号

A semaphore is given by calling k_sem_give().

通过调用 k_sem_give()给出一个信号量。

The following code builds on the example above, and gives the semaphore to indicate that a unit of data is available for processing by a consumer thread.

下面的代码构建在上面的示例之上,并给出信号量,以表明一个数据单元可供使用者线程处理。

void input_data_interrupt_handler(void *arg)
{
    /* notify thread that data is available */
    k_sem_give(&my_sem);

    ...
}

Taking a Semaphore 使用信号灯

A semaphore is taken by calling k_sem_take().

通过调用 k_sem_take()获取信号量。

The following code builds on the example above, and waits up to 50 milliseconds for the semaphore to be given. A warning is issued if the semaphore is not obtained in time.

下面的代码构建在上面的示例之上,并等待最长50毫秒的信号量。如果未能及时获得信号量,则发出警告。

void consumer_thread(void)
{
    ...

    if (k_sem_take(&my_sem, K_MSEC(50)) != 0) {
        printk("Input data not available!");
    } else {
        /* fetch available data */
        ...
    }
    ...
}

Suggested Uses 建议的用途

Use a semaphore to control access to a set of resources by multiple threads.

使用信号量控制多个线程对一组资源的访问。

Use a semaphore to synchronize processing between a producing and consuming threads or ISRs.

使用信号量来同步生产线程和消费线程或 ISRs 之间的处理。

Configuration Options 配置选项

Related configuration options:

相关配置选项:

  • None.

    没有。

Mutexes 互斥

A mutex is a kernel object that implements a traditional reentrant mutex. A mutex allows multiple threads to safely share an associated hardware or software resource by ensuring mutually exclusive access to the resource.

互斥对象是实现传统可重入互斥对象的内核对象。互斥通过确保相互排斥地访问资源,使多个线程能够安全地共享相关的硬件或软件资源。

Concepts 概念

Any number of mutexes can be defined (limited only by available RAM). Each mutex is referenced by its memory address.

可以定义任意数量的互斥锁(仅受可用 RAM 的限制)。

A mutex has the following key properties:

互斥体具有以下关键属性:

  • A lock count that indicates the number of times the mutex has be locked by the thread that has locked it. A count of zero indicates that the mutex is unlocked.

    锁定计数,指示已锁定的线程锁定互斥对象的次数。计数为零表示互斥对象已解锁。

  • An owning thread that identifies the thread that has locked the mutex, when it is locked.

    一个拥有线程,标识锁定互斥锁的线程。

A mutex must be initialized before it can be used. This sets its lock count to zero.

互斥量在使用之前必须进行初始化,这样可以将其锁定计数设置为零。

A thread that needs to use a shared resource must first gain exclusive rights to access it by locking the associated mutex. If the mutex is already locked by another thread, the requesting thread may choose to wait for the mutex to be unlocked.

需要使用共享资源的线程必须首先通过锁定相关联的互斥体获得访问共享资源的独占权限。如果互斥对象已经被另一个线程锁定,请求线程可以选择等待互斥对象被解锁。

After locking a mutex, the thread may safely use the associated resource for as long as needed; however, it is considered good practice to hold the lock for as short a time as possible to avoid negatively impacting other threads that want to use the resource. When the thread no longer needs the resource it must unlock the mutex to allow other threads to use the resource.

锁定互斥锁之后,线程可以安全地使用相关的资源,直到需要的时间为止; 然而,最好的做法是尽可能短地持有锁,以避免对希望使用该资源的其他线程造成负面影响。当线程不再需要该资源时,它必须解除互斥锁以允许其他线程使用该资源。

Any number of threads may wait on a locked mutex simultaneously. When the mutex becomes unlocked it is then locked by the highest-priority thread that has waited the longest.

任意数量的线程可以同时等待一个锁定的互斥对象。当互斥锁解锁后,等待时间最长的优先级最高的线程将锁定互斥锁。

Note

注意

Mutex objects are not designed for use by ISRs.

互斥对象不是为 ISRs 而设计的。

Reentrant Locking 可重入锁定

A thread is permitted to lock a mutex it has already locked. This allows the thread to access the associated resource at a point in its execution when the mutex may or may not already be locked.

线程可以锁定它已经锁定的互斥对象。这允许线程在其执行的某个点访问相关资源,而互斥量可能已经锁定,也可能尚未锁定。

A mutex that is repeatedly locked by a thread must be unlocked an equal number of times before the mutex becomes fully unlocked so it can be claimed by another thread.

被线程重复锁定的互斥对象必须解锁相同次数,然后互斥对象才能完全解锁,以便被另一个线程声明。

Priority Inheritance 优先级继承

The thread that has locked a mutex is eligible for priority inheritance. This means the kernel will temporarily elevate the thread’s priority if a higher priority thread begins waiting on the mutex. This allows the owning thread to complete its work and release the mutex more rapidly by executing at the same priority as the waiting thread. Once the mutex has been unlocked, the unlocking thread resets its priority to the level it had before locking that mutex.

锁定互斥对象的线程可以使用优先级继承。这意味着,如果一个优先级更高的线程开始等待互斥锁,内核将暂时提高线程的优先级。这允许拥有线程以与等待线程相同的优先级执行,以更快的速度完成工作并释放互斥锁。一旦互斥锁被解锁,解锁线程将其优先级重置为锁定该互斥锁之前的级别。

Note

注意

The CONFIG_PRIORITY_CEILING configuration option limits how high the kernel can raise a thread’s priority due to priority inheritance. The default value of 0 permits unlimited elevation.

配置选项限制了内核由于使用优先级继承而提高线程优先级的程度。默认值0允许无限制的高度。

When two or more threads wait on a mutex held by a lower priority thread, the kernel adjusts the owning thread’s priority each time a thread begins waiting (or gives up waiting). When the mutex is eventually unlocked, the unlocking thread’s priority correctly reverts to its original non-elevated priority.

当两个或多个线程等待由低优先级线程持有的互斥对象时,内核在每次线程开始等待(或放弃等待)时调整所属线程的优先级。当互斥锁最终解锁时,解锁线程的优先级将正确恢复到其原来的非提升优先级。

The kernel does not fully support priority inheritance when a thread holds two or more mutexes simultaneously. This situation can result in the thread’s priority not reverting to its original non-elevated priority when all mutexes have been released. It is recommended that a thread lock only a single mutex at a time when multiple mutexes are shared between threads of different priorities.

当一个线程同时拥有两个或多个互斥优先级继承时,内核并不完全支持互斥。这种情况可能导致当所有互斥锁都被释放时,线程的优先级无法恢复到原来的非提升优先级。当多个互斥锁在具有不同优先级的线程之间共享时,建议线程只锁定一个互斥锁。

Implementation 实施

Defining a Mutex 定义互斥对象

A mutex is defined using a variable of type k_mutex. It must then be initialized by calling k_mutex_init().

互斥对象使用类型为 k_mutex 的变量来定义,然后必须通过调用 k_mutex_init()来初始化它。

The following code defines and initializes a mutex.

下面的代码定义和初始化互斥对象。

struct k_mutex my_mutex;

k_mutex_init(&my_mutex);

Alternatively, a mutex can be defined and initialized at compile time by calling K_MUTEX_DEFINE.

另外,可以通过调用 k_mutex_define 在编译时定义和初始化互斥量。

The following code has the same effect as the code segment above.

下面的代码与上面的代码段具有相同的效果。

K_MUTEX_DEFINE(my_mutex);

Locking a Mutex 锁定互斥对象

A mutex is locked by calling k_mutex_lock().

通过调用 k_mutex_lock()锁定互斥对象。

The following code builds on the example above, and waits indefinitely for the mutex to become available if it is already locked by another thread.

下面的代码构建在上面的示例之上,并且无限期地等待互斥对象变得可用(如果它已经被另一个线程锁定)。

k_mutex_lock(&my_mutex, K_FOREVER);

The following code waits up to 100 milliseconds for the mutex to become available, and gives a warning if the mutex does not become available.

下面的代码将等待100毫秒,等待互斥对象可用,并在互斥对象不可用时发出警告。

if (k_mutex_lock(&my_mutex, K_MSEC(100)) == 0) {
    /* mutex successfully locked */
} else {
    printf("Cannot lock XYZ display\n");
}

Unlocking a Mutex 解锁互斥对象

A mutex is unlocked by calling k_mutex_unlock().

通过调用 k_mutex_unlock()解锁互斥对象。

The following code builds on the example above, and unlocks the mutex that was previously locked by the thread.

下面的代码构建在上面的示例之上,并解锁以前被线程锁定的互斥对象。

k_mutex_unlock(&my_mutex);

Suggested Uses 建议的用途

Use a mutex to provide exclusive access to a resource, such as a physical device.

使用互斥提供对资源(如物理设备)的独占访问。

Configuration Options 配置选项

Related configuration options:

相关配置选项:

Condition Variables 条件变量

A condition variable is a synchronization primitive that enables threads to wait until a particular condition occurs.

条件变量是一个同步基元,它使线程能够等待,直到出现特定条件。

Concepts 概念

Any number of condition variables can be defined (limited only by available RAM). Each condition variable is referenced by its memory address.

可以定义任意数量的条件变量(仅受可用 RAM 的限制)。每个条件变量都由其内存地址引用。

To wait for a condition to become true, a thread can make use of a condition variable.

为了等待条件为真,线程可以使用条件变量。

A condition variable is basically a queue of threads that threads can put themselves on when some state of execution (i.e., some condition) is not as desired (by waiting on the condition). The function k_condvar_wait() performs atomically the following steps;

条件变量基本上是一个线程队列,当某些执行状态(即某些条件)不如预期时(通过等待该条件) ,线程可以将自己置于该队列中。函数 k_condvar_wait()原子地执行以下步骤;

  1. Releases the last acquired mutex.

    释放最后获取的互斥对象。

  2. Puts the current thread in the condition variable queue.

    将当前线程放入条件变量队列中。

Some other thread, when it changes said state, can then wake one (or more) of those waiting threads and thus allow them to continue by signaling on the condition using k_condvar_signal() or k_condvar_broadcast() then it:

其他一些线程,当它改变所说的状态时,可以唤醒那些等待线程中的一个(或多个) ,从而允许它们在条件下继续使用 k_condvar_signal()或 k_condvar_broadcast()发送信号,然后它:

  1. Re-acquires the mutex previously released.

    重新获取以前释放的互斥对象。

  2. Returns from k_condvar_wait().

    从 k_condvar_wait()返回。

A condition variable must be initialized before it can be used.

条件变量必须先初始化,才能使用。

Implementation 实施

Defining a Condition Variable 定义条件变量

A condition variable is defined using a variable of type k_condvar. It must then be initialized by calling k_condvar_init().

条件变量使用类型为 k_condvar 的变量来定义,然后必须通过调用 k_condvar_init()来初始化它。

The following code defines a condition variable:

下面的代码定义了一个条件变量:

struct k_condvar my_condvar;

k_condvar_init(&my_condvar);

Alternatively, a condition variable can be defined and initialized at compile time by calling K_CONDVAR_DEFINE.

或者,可以通过调用 k_condvar_define 在编译时定义和初始化条件变量。

The following code has the same effect as the code segment above.

下面的代码与上面的代码段具有相同的效果。

K_CONDVAR_DEFINE(my_condvar);

Waiting on a Condition Variable 等待条件变量

A thread can wait on a condition by calling k_condvar_wait().

线程可以通过调用 k_condvar_wait()等待条件。

The following code waits on the condition variable.

下面的代码等待条件变量。

K_MUTEX_DEFINE(mutex);
K_CONDVAR_DEFINE(condvar)

void main(void)
{
    k_mutex_lock(&mutex, K_FOREVER);

    /* block this thread until another thread signals cond. While
     * blocked, the mutex is released, then re-acquired before this
     * thread is woken up and the call returns.
     */
    k_condvar_wait(&condvar, &mutex, K_FOREVER);
    ...
    k_mutex_unlock(&mutex);
}

Signaling a Condition Variable 给条件变量发信号

A condition variable is signaled on by calling k_condvar_signal() for one thread or by calling k_condvar_broadcast() for multiple threads.

条件变量通过为一个线程调用 k_condvar_signal()或为多个线程调用 k_condvar_broadcast()来发出信号。

The following code builds on the example above.

下面的代码基于上面的示例。

void worker_thread(void)
{
    k_mutex_lock(&mutex, K_FOREVER);

    /*
     * Do some work and fulfill the condition
     */
    ...
    ...
    k_condvar_signal(&condvar);
    k_mutex_unlock(&mutex);
}

Suggested Uses 建议的用途

Use condition variables with a mutex to signal changing states (conditions) from one thread to another thread. Condition variables are not the condition itself and they are not events. The condition is contained in the surrounding programming logic.

使用带有互斥锁的条件变量来表示从一个线程到另一个线程的状态变化(条件)。条件变量不是条件本身,也不是事件。该条件包含在周围的编程逻辑中。

Mutexes alone are not designed for use as a notification/synchronization mechanism. They are meant to provide mutually exclusive access to a shared resource only.

单独设计互斥锁不是为了用作通知/同步机制。它们只是为了提供对共享资源的互斥访问。

Configuration Options 配置选项

Related configuration options:

相关配置选项:

  • None.

    没有。

Events 活动

An event object is a kernel object that implements traditional events.

事件对象是实现传统事件的内核对象。

Concepts 概念

Any number of event objects can be defined (limited only by available RAM). Each event object is referenced by its memory address. One or more threads may wait on an event object until the desired set of events has been delivered to the event object. When new events are delivered to the event object, all threads whose wait conditions have been satisfied become ready simultaneously.

可以定义任意数量的事件对象(仅受可用 RAM 的限制)。每个事件对象都由其内存地址引用。一个或多个线程可以在事件对象上等待,直到所需的事件集已交付给事件对象。当新事件传递到事件对象时,所有等待条件已满足的线程同时就绪。

An event object has the following key properties:

事件对象具有以下关键属性:

  • A 32-bit value that tracks which events have been delivered to it.

    一个32位的值,用于跟踪已经传递给它的事件。

An event object must be initialized before it can be used.

事件对象必须初始化后才能使用。

Events may be delivered by a thread or an ISR. When delivering events, the events may either overwrite the existing set of events or add to them in a bitwise fashion. When overwriting the existing set of events, this is referred to as setting. When adding to them in a bitwise fashion, this is referred to as posting. Both posting and setting events have the potential to fulfill match conditions of multiple threads waiting on the event object. All threads whose match conditions have been met are made active at the same time.

事件可以通过线程或 ISR 传递。交付事件时,事件可以覆盖现有的事件集,也可以按位添加事件集。当覆盖现有的事件集时,这被称为设置。当以按位方式添加到它们时,这被称为 post。发布和设置事件都可能满足等待事件对象的多个线程的匹配条件。所有满足匹配条件的线程同时处于活动状态。

Threads may wait on one or more events. They may either wait for all of the the requested events, or for any of them. Furthermore, threads making a wait request have the option of resetting the current set of events tracked by the event object prior to waiting. Care must be taken with this option when multiple threads wait on the same event object.

线程可以等待一个或多个事件。它们可以等待所有请求的事件,也可以等待其中的任何事件。此外,发出等待请求的线程可以选择在等待之前重置事件对象跟踪的当前事件集。当多个线程等待同一个事件对象时,必须小心使用此选项。

Note

注意

The kernel does allow an ISR to query an event object, however the ISR must not attempt to wait for the events.

内核允许 ISR 查询事件对象,但是 ISR 不能尝试等待事件。

Implementation 实施

Defining an Event Object 定义事件对象

An event object is defined using a variable of type k_event. It must then be initialized by calling k_event_init().

事件对象使用类型为 k_event 的变量来定义,然后必须调用 k_event_init()来初始化它。

The following code defines an event object.

下面的代码定义了一个事件对象。

struct k_event my_event;

k_event_init(&my_event);

Alternatively, an event object can be defined and initialized at compile time by calling K_EVENT_DEFINE.

或者,可以通过调用 k_ event_define 在编译时定义和初始化事件对象。

The following code has the same effect as the code segment above.

下面的代码与上面的代码段具有相同的效果。

K_EVENT_DEFINE(my_event);

Setting Events 设置活动

Events in an event object are set by calling k_event_set().

通过调用 k_event_set()来设置事件对象中的事件。

The following code builds on the example above, and sets the events tracked by the event object to 0x001.

下面的代码基于上面的示例,并将事件对象跟踪的事件设置为0x001。

void input_available_interrupt_handler(void *arg)
{
    /* notify threads that data is available */

    k_event_set(&my_event, 0x001);

    ...
}

Posting Events 投寄活动

Events are posted to an event object by calling k_event_post().

通过调用 k_event_post()将事件发布到事件对象。

The following code builds on the example above, and posts a set of events to the event object.

下面的代码基于上面的示例,并将一组事件发布到事件对象。

void input_available_interrupt_handler(void *arg)
{
    ...

    /* notify threads that more data is available */

    k_event_post(&my_event, 0x120);

    ...
}

Waiting for Events 等待活动

Threads wait for events by calling k_event_wait().

线程通过调用 k_event_wait()等待事件。

The following code builds on the example above, and waits up to 50 milliseconds for any of the specified events to be posted. A warning is issued if none of the events are posted in time.

下面的代码以上面的示例为基础,等待最长50毫秒的时间来发布任何指定的事件。如果没有及时发布任何事件,则发出警告。

void consumer_thread(void)
{
    uint32_t  events;

    events = k_event_wait(&my_event, 0xFFF, false, K_MSEC(50));
    if (events == 0) {
        printk("No input devices are available!");
    } else {
        /* Access the desired input device(s) */
        ...
    }
    ...
}

Alternatively, the consumer thread may desire to wait for all the events before continuing.

或者,消费者线程可能希望在继续之前等待所有事件。

void consumer_thread(void)
{
    uint32_t  events;

    events = k_event_wait_all(&my_event, 0x121, false, K_MSEC(50));
    if (events == 0) {
        printk("At least one input device is not available!");
    } else {
        /* Access the desired input devices */
        ...
    }
    ...
}

Suggested Uses 建议的用途

Use events to indicate that a set of conditions have occurred.

使用事件指示一组条件已经发生。

Use events to pass small amounts of data to multiple threads at once.

使用事件将少量数据一次传递给多个线程。

Configuration Options 配置选项

Related configuration options:

相关配置选项:

Symmetric Multiprocessing 对称多处理机

On multiprocessor architectures, Zephyr supports the use of multiple physical CPUs running Zephyr application code. This support is “symmetric” in the sense that no specific CPU is treated specially by default. Any processor is capable of running any Zephyr thread, with access to all standard Zephyr APIs supported.

在多处理器体系结构中,Zephyr 支持使用运行 Zephyr 应用程序代码的多个物理 cpu。这种支持是“对称的”,因为默认情况下没有特定的 CPU 被特殊对待。任何处理器都能够运行任何 Zephyr 线程,并支持所有标准 Zephyr api。

No special application code needs to be written to take advantage of this feature. If there are two Zephyr application threads runnable on a supported dual processor device, they will both run simultaneously.

不需要编写特殊的应用程序代码来利用这个特性。如果有两个 Zephyr 应用程序线程可以在支持的双处理器设备上运行,那么它们将同时运行。

SMP configuration is controlled under the CONFIG_SMP kconfig variable. This must be set to “y” to enable SMP features, otherwise a uniprocessor kernel will be built. In general the platform default will have enabled this anywhere it’s supported. When enabled, the number of physical CPUs available is visible at build time as CONFIG_MP_NUM_CPUS. Likewise, the default for this will be the number of available CPUs on the platform and it is not expected that typical apps will change it. But it is legal and supported to set this to a smaller (but obviously not larger) number for special purposes (e.g. for testing, or to reserve a physical CPU for running non-Zephyr code).

SMP 配置由 config_smp kconfig 变量控制。必须将其设置为“ y”以启用 SMP 特性,否则将构建单处理器内核。一般来说,平台默认在任何支持的地方都会启用这个功能。当启用时,可用的物理 cpu 数量在构建时显示为 config_mp_num_cpu。同样,默认情况下,这将是平台上可用 cpu 的数量,并且不期望典型的应用程序会改变它。但是,为了特殊目的(例如用于测试,或者为运行非 zephyr 代码保留一个物理 CPU) ,将其设置为一个较小的数字是合法的,并且是受支持的。

Synchronization 同步

At the application level, core Zephyr IPC and synchronization primitives all behave identically under an SMP kernel. For example semaphores used to implement blocking mutual exclusion continue to be a proper application choice.

在应用程序级别,核心 zephyripc 和同步原语在 SMP 核下的行为完全相同。例如,用于实现阻塞互斥锁的信号量仍然是一个合适的应用程序选择。

At the lowest level, however, Zephyr code has often used the irq_lock()/irq_unlock() primitives to implement fine grained critical sections using interrupt masking. These APIs continue to work via an emulation layer (see below), but the masking technique does not: the fact that your CPU will not be interrupted while you are in your critical section says nothing about whether a different CPU will be running simultaneously and be inspecting or modifying the same data!

然而,在最低级别上,Zephyr 代码经常使用 irq_lock ()/irq_unlock ()原语来使用中断掩码实现细粒度的临界节。这些 api 通过一个模拟层(见下文)继续工作,但屏蔽技术不能: 事实上,当您处于临界区时,您的 CPU 不会被中断,这并不能说明是否会有不同的 CPU 同时运行并检查或修改相同的数据!

Spinlocks 自旋锁

SMP systems provide a more constrained k_spin_lock() primitive that not only masks interrupts locally, as done by irq_lock(), but also atomically validates that a shared lock variable has been modified before returning to the caller, “spinning” on the check if needed to wait for the other CPU to exit the lock. The default Zephyr implementation of k_spin_lock() and k_spin_unlock() is built on top of the pre-existing atomic_ layer (itself usually implemented using compiler intrinsics), though facilities exist for architectures to define their own for performance reasons.

SMP 系统提供了一个更加有约束的 k_spin_lock ()原语,它不仅像 irq_lock ()那样在本地掩盖中断,而且还原子地验证共享锁变量在返回到调用者之前是否已经被修改,如果需要等待其他 CPU 退出锁,则在检查上“旋转”。默认的 Zephyr 实现的 k_spin_lock ()和 k_spin_unlock ()是建立在已经存在的原子层之上的(本身通常使用编译器 intrinsic 实现) ,尽管架构出于性能原因可以自己定义它们自己的工具。

One important difference between IRQ locks and spinlocks is that the earlier API was naturally recursive: the lock was global, so it was legal to acquire a nested lock inside of a critical section. Spinlocks are separable: you can have many locks for separate subsystems or data structures, preventing CPUs from contending on a single global resource. But that means that spinlocks must not be used recursively. Code that holds a specific lock must not try to re-acquire it or it will deadlock (it is perfectly legal to nest distinct spinlocks, however). A validation layer is available to detect and report bugs like this.

IRQ 锁和自旋锁之间的一个重要区别是,早期的 API 是自然递归的: 锁是全局的,因此获取临界区内的嵌套锁是合法的。自旋锁是可分离的: 您可以为单独的子系统或数据结构设置许多锁,从而防止 cpu 与单个全局资源发生冲突。但这意味着不能递归地使用自旋锁。持有特定锁的代码不能尝试重新获取它,否则它将陷入死锁(然而,嵌套不同的自旋锁是完全合法的)。验证层可用于检测和报告这样的错误。

When used on a uniprocessor system, the data component of the spinlock (the atomic lock variable) is unnecessary and elided. Except for the recursive semantics above, spinlocks in single-CPU contexts produce identical code to legacy IRQ locks. In fact the entirety of the Zephyr core kernel has now been ported to use spinlocks exclusively.

在单处理器系统上使用时,自旋锁的数据组件(原子锁变量)是不必要的,并且被省略了。除了上面的递归语义之外,单 cpu 上下文中的自旋锁产生的代码与遗留的 IRQ 锁相同。事实上,整个 Zephyr 核心内核现在已经移植到专门使用自旋锁。

Legacy irq_lock() emulation 遗留 irq_lock ()模拟

For the benefit of applications written to the uniprocessor locking API, irq_lock() and irq_unlock() continue to work compatibly on SMP systems with identical semantics to their legacy versions. They are implemented as a single global spinlock, with a nesting count and the ability to be atomically reacquired on context switch into locked threads. The kernel will ensure that only one thread across all CPUs can hold the lock at any time, that it is released on context switch, and that it is re-acquired when necessary to restore the lock state when a thread is switched in. Other CPUs will spin waiting for the release to happen.

为了使编写到单处理器锁定 API 的应用程序受益,irq_lock ()和 irq_unlock ()继续在 SMP 系统上兼容地工作,其语义与旧版本相同。它们被实现为一个单一的全局自旋锁,具有嵌套计数和在上下文切换到锁定线程时自动重新获取的能力。内核将确保在任何时候只有一个跨所有 cpu 的线程可以持有锁,在上下文切换时释放它,并且在必要时重新获取它,以便在切换线程时恢复锁状态。其他 cpu 将继续运行,等待发布。

The overhead involved in this process has measurable performance impact, however. Unlike uniprocessor apps, SMP apps using irq_lock() are not simply invoking a very short (often ~1 instruction) interrupt masking operation. That, and the fact that the IRQ lock is global, means that code expecting to be run in an SMP context should be using the spinlock API wherever possible.

但是,这个过程中涉及的开销会对性能产生可度量的影响。与单处理器应用程序不同,使用 irq_lock ()的 SMP 应用程序并不简单地调用一个非常短(通常是 ~ 1指令)的中断屏蔽操作。这一点,以及 IRQ 锁是全局性的事实,意味着希望在 SMP 上下文中运行的代码应该尽可能使用自旋锁 API。

CPU Mask CPU 掩码

It is often desirable for real time applications to deliberately partition work across physical CPUs instead of relying solely on the kernel scheduler to decide on which threads to execute. Zephyr provides an API, controlled by the CONFIG_SCHED_CPU_MASK kconfig variable, which can associate a specific set of CPUs with each thread, indicating on which CPUs it can run.

对于实时应用程序来说,通常需要有意识地跨物理 cpu 分区工作,而不是仅仅依靠内核调度程序来决定执行哪些线程。Zephyr 提供了一个 API,由 CONFIG_sched_cpu_mask kconfig 变量控制,它可以将一组特定的 cpu 与每个线程关联起来,指示它可以在哪些 cpu 上运行。

By default, new threads can run on any CPU. Calling k_thread_cpu_mask_disable() with a particular CPU ID will prevent that thread from running on that CPU in the future. Likewise k_thread_cpu_mask_enable() will re-enable execution. There are also k_thread_cpu_mask_clear() and k_thread_cpu_mask_enable_all() APIs available for convenience. For obvious reasons, these APIs are illegal if called on a runnable thread. The thread must be blocked or suspended, otherwise an -EINVAL will be returned.

默认情况下,新线程可以在任何 CPU 上运行。使用特定的 CPU ID 调用 k_thread_CPU_mask_disable ()将防止该线程将来在该 CPU 上运行。同样地,k_thread_cpu_mask_enable ()将重新启用执行。为方便起见,还有 k_thread_cpu_mask_clear ()和 k_thread_cpu_mask_enable_all()api。由于显而易见的原因,如果在可运行的线程上调用这些 api,则它们是非法的。线程必须被阻塞或挂起,否则将返回一个 -EINVAL。

Note that when this feature is enabled, the scheduler algorithm involved in doing the per-CPU mask test requires that the list be traversed in full. The kernel does not keep a per-CPU run queue. That means that the performance benefits from the CONFIG_SCHED_SCALABLE and CONFIG_SCHED_MULTIQ scheduler backends cannot be realized. CPU mask processing is available only when CONFIG_SCHED_DUMB is the selected backend. This requirement is enforced in the configuration layer.

请注意,当启用这个特性时,执行 per-CPU 掩码测试所涉及的调度程序算法要求完整遍历列表。内核不保持每 cpu 运行队列。这意味着无法实现 config_sched_scalable 和 config_sched_multiq 调度程序后端的性能优势。CPU 掩码处理只有在 config_sched_dumb 是选定的后端时才可用。此要求在配置层中强制执行。

SMP Boot Process SMP 启动过程

A Zephyr SMP kernel begins boot identically to a uniprocessor kernel. Auxiliary CPUs begin in a disabled state in the architecture layer. All standard kernel initialization, including device initialization, happens on a single CPU before other CPUs are brought online.

Zephyrsmp 内核开始以相同的方式引导到单处理器内核。辅助 cpu 在体系结构层中处于禁用状态。所有标准的内核初始化,包括设备初始化,在其他 CPU 联机之前都在单个 CPU 上进行。

Just before entering the application main() function, the kernel calls z_smp_init() to launch the SMP initialization process. This enumerates over the configured CPUs, calling into the architecture layer using arch_start_cpu() for each one. This function is passed a memory region to use as a stack on the foreign CPU (in practice it uses the area that will become that CPU’s interrupt stack), the address of a local smp_init_top() callback function to run on that CPU, and a pointer to a “start flag” address which will be used as an atomic signal.

在进入应用程序 main ()函数之前,内核调用 z_smp_init ()启动 SMP 初始化进程。这将枚举已配置的 cpu,对每个 cpu 使用 arch_start_cpu ()调用体系结构层。这个函数被传递一个内存区域作为外部 CPU 的堆栈(实际上它使用的区域将成为 CPU 的中断堆栈) ,一个局部 smp_init_top ()回调函数的地址在该 CPU 上运行,以及一个指向“ start flag”地址的指针,该地址将被用作原子信号。

The local SMP initialization (smp_init_top()) on each CPU is then invoked by the architecture layer. Note that interrupts are still masked at this point. This routine is responsible for calling smp_timer_init() to set up any needed stat in the timer driver. On many architectures the timer is a per-CPU device and needs to be configured specially on auxiliary CPUs. Then it waits (spinning) for the atomic “start flag” to be released in the main thread, to guarantee that all SMP initialization is complete before any Zephyr application code runs, and finally calls z_swap() to transfer control to the appropriate runnable thread via the standard scheduler API.

然后,架构层调用每个 CPU 上的本地 SMP 初始化(SMP_init_top ())。请注意,此时中断仍然是屏蔽的。这个例程负责调用 smp_timer_init ()来设置计时器驱动程序中所需的任何属性。在许多体系结构中,定时器是单 cpu 设备,需要在辅助 cpu 上进行特殊配置。然后等待(旋转)在主线程中释放原子“ start 标志”,以保证在任何 Zephyr 应用程序代码运行之前所有 SMP 初始化都已完成,最后调用 z_swap ()通过标准调度器 API 将控制权转移到适当的可运行线程。

SMP Initialization

Fig. 4 Example SMP initialization process, showing a configuration with two CPUs and two app threads which begin operating simultaneously.

图4示例 SMP 初始化过程,显示了一个配置,其中有两个 cpu 和两个应用程序线程,它们同时开始运行

Interprocessor Interrupts 处理器间中断

When running in multiprocessor environments, it is occasionally the case that state modified on the local CPU needs to be synchronously handled on a different processor.

在多处理器环境中运行时,偶尔需要在不同的处理器上同步处理在本地 CPU 上修改的状态。

One example is the Zephyr k_thread_abort() API, which cannot return until the thread that had been aborted is no longer runnable. If it is currently running on another CPU, that becomes difficult to implement.

一个例子是 Zephyr k_thread_abort()API,它只有在中止的线程不再可运行时才能返回。如果它当前正在另一个 CPU 上运行,那就很难实现。

Another is low power idle. It is a firm requirement on many devices that system idle be implemented using a low-power mode with as many interrupts (including periodic timer interrupts) disabled or deferred as is possible. If a CPU is in such a state, and on another CPU a thread becomes runnable, the idle CPU has no way to “wake up” to handle the newly-runnable load.

另一个是低功耗空闲。这是一个对许多设备的坚定要求,系统空闲实施使用低功耗模式与尽可能多的中断(包括周期定时器中断)禁用或延迟。如果一个 CPU 处于这种状态,并且在另一个 CPU 上一个线程变成可运行的,那么空闲 CPU 就没有办法“唤醒”来处理新的可运行负载。

So where possible, Zephyr SMP architectures should implement an interprocessor interrupt. The current framework is very simple: the architecture provides a arch_sched_ipi() call, which when invoked will flag an interrupt on all CPUs (except the current one, though that is allowed behavior) which will then invoke the z_sched_ipi() function implemented in the scheduler. The expectation is that these APIs will evolve over time to encompass more functionality (e.g. cross-CPU calls), and that the scheduler-specific calls here will be implemented in terms of a more general framework.

因此,在可能的情况下,zephyrsmp 体系结构应该实现一个处理器间中断。当前的框架非常简单: 体系结构提供了一个 arch_sched_ipi ()调用,当被调用时,它将在所有 cpu 上标记一个中断(当前 cpu 除外,不过这是允许的行为) ,然后调用在调度程序中实现的 z_sched_ipi ()函数。我们的期望是,这些 api 将随着时间的推移而发展,以包含更多的功能(例如跨 cpu 调用) ,并且这里特定于调度程序的调用将在一个更通用的框架中实现。

Note that not all SMP architectures will have a usable IPI mechanism (either missing, or just undocumented/unimplemented). In those cases Zephyr provides fallback behavior that is correct, but perhaps suboptimal.

请注意,并非所有 SMP 体系结构都具有可用的 IPI 机制(要么缺少,要么没有文档说明/未实现)。在这些情况下,Zephyr 提供的备用行为是正确的,但也许是次优的。

Using this, k_thread_abort() becomes only slightly more complicated in SMP: for the case where a thread is actually running on another CPU (we can detect this atomically inside the scheduler), we broadcast an IPI and spin, waiting for the thread to either become “DEAD” or for it to re-enter the queue (in which case we terminate it the same way we would have in uniprocessor mode). Note that the “aborted” check happens on any interrupt exit, so there is no special handling needed in the IPI per se. This allows us to implement a reasonable fallback when IPI is not available: we can simply spin, waiting until the foreign CPU receives any interrupt, though this may be a much longer time!

使用这个,在 SMP 中 k_thread_abort ()变得稍微复杂一些: 对于线程实际上在另一个 CPU 上运行的情况(我们可以在调度程序中自动检测这个) ,我们广播一个 IPI 并旋转,等待线程变成“ DEAD”或者重新进入队列(在这种情况下,我们用单处理器模式中的同样方式终止它)。注意,“中止”检查发生在任何中断出口上,因此 IPI 本身不需要特殊的处理。这允许我们在 IPI 不可用时实现一个合理的回退: 我们可以简单地旋转,等待外部 CPU 接收到任何中断,尽管这可能需要更长的时间!

Likewise idle wakeups are trivially implementable with an empty IPI handler. If a thread is added to an empty run queue (i.e. there may have been idle CPUs), we broadcast an IPI. A foreign CPU will then be able to see the new thread when exiting from the interrupt and will switch to it if available.

同样,空闲唤醒在一个空的 IPI 处理程序中是很容易实现的。如果一个线程被添加到一个空的运行队列(也就是说可能有空闲的 cpu) ,我们就广播一个 IPI。然后,外部 CPU 在从中断退出时将能够看到新线程,并在可用时切换到新线程。

Without an IPI, however, a low power idle that requires an interrupt will not work to synchronously run new threads. The workaround in that case is more invasive: Zephyr will not enter the system idle handler and will instead spin in its idle loop, testing the scheduler state at high frequency (not spinning on it though, as that would involve severe lock contention) for new threads. The expectation is that power constrained SMP applications are always going to provide an IPI, and this code will only be used for testing purposes or on systems without power consumption requirements.

但是,如果没有 IPI,需要中断的低功耗空闲将无法同步运行新线程。这种情况下的解决方案更具侵略性: Zephyr 将不会进入系统空闲处理程序,而是在空闲循环中自旋,以高频率测试调度程序状态(但不会在其上旋转,因为这将涉及新线程的严重锁争用)。我们的期望是,功耗受限的 SMP 应用程序总是会提供一个 IPI,而该代码只用于测试目的或者在没有功耗要求的系统上。

SMP Kernel Internals SMP 内核内部构件

In general, Zephyr kernel code is SMP-agnostic and, like application code, will work correctly regardless of the number of CPUs available. But in a few areas there are notable changes in structure or behavior.

一般来说,Zephyr 内核代码与 smp 无关,与应用程序代码一样,无论可用的 cpu 数量多少,它都能正常工作。但在一些领域,结构或行为发生了显著的变化。

Per-CPU data 每个 cpu 的数据

Many elements of the core kernel data need to be implemented for each CPU in SMP mode. For example, the _current thread pointer obviously needs to reflect what is running locally, there are many threads running concurrently. Likewise a kernel-provided interrupt stack needs to be created and assigned for each physical CPU, as does the interrupt nesting count used to detect ISR state.

在 SMP 模式下,需要为每个 CPU 实现核心内核数据的许多元素。例如,_ current 线程指针显然需要反映本地运行的内容,因为有许多线程并发运行。同样,需要为每个物理 CPU 创建并分配一个内核提供的中断堆栈,用于检测 ISR 状态的中断嵌套计数也是如此。

These fields are now moved into a separate struct _cpu instance within the _kernel struct, which has a cpus[] array indexed by ID. Compatibility fields are provided for legacy uniprocessor code trying to access the fields of cpus[0] using the older syntax and assembly offsets.

这些字段现在移动到 _kernel struct 中的一个单独的 struct_cpu 实例中,该实例有一个按 ID 索引的 cpu []数组。为试图使用旧语法和程序集偏移量访问 cpu [0]字段的遗留单处理器代码提供了兼容性字段。

Note that an important requirement on the architecture layer is that the pointer to this CPU struct be available rapidly when in kernel context. The expectation is that arch_curr_cpu() will be implemented using a CPU-provided register or addressing mode that can store this value across arbitrary context switches or interrupts and make it available to any kernel-mode code.

注意,体系结构层的一个重要要求是,当在内核上下文中时,指向这个 CPU 结构的指针可以快速使用。我们的期望是 arch_curr_cpu ()将使用 cpu 提供的寄存器或寻址模式来实现,这些寄存器或寻址模式可以跨任意上下文切换或中断存储这个值,并使其可用于任何内核模式代码。

Similarly, where on a uniprocessor system Zephyr could simply create a global “idle thread” at the lowest priority, in SMP we may need one for each CPU. This makes the internal predicate test for “_is_idle()” in the scheduler, which is a hot path performance environment, more complicated than simply testing the thread pointer for equality with a known static variable. In SMP mode, idle threads are distinguished by a separate field in the thread struct.

类似地,在单处理器系统 Zephyr 上可以简单地以最低优先级创建一个全局“空闲线程”,在 SMP 中,我们可能需要为每个 CPU 创建一个空闲线程。这使得调度程序中“_is_idle ()”的内部谓词测试(这是一个热路径性能环境)比简单地测试线程指针是否与已知的静态变量相等更加复杂。在 SMP 模式下,空闲线程通过线程结构中的单独字段来区分。

Switch-based context switching 基于切换的上下文切换

The traditional Zephyr context switch primitive has been z_swap(). Unfortunately, this function takes no argument specifying a thread to switch to. The expectation has always been that the scheduler has already made its preemption decision when its state was last modified and cached the resulting “next thread” pointer in a location where architecture context switch primitives can find it via a simple struct offset. That technique will not work in SMP, because the other CPU may have modified scheduler state since the current CPU last exited the scheduler (for example: it might already be running that cached thread!).

传统的 Zephyr 上下文切换原语是 z_swap ()。不幸的是,这个函数没有指定要切换到的线程的参数。预期一直是,调度程序已经做出了它的先占决定,当它的状态最后一次被修改,并缓存产生的“下一个线程”指针的位置,架构上下文切换原语可以找到它通过一个简单的结构偏移。这种技术在 SMP 中不起作用,因为自从当前 CPU 上次退出调度程序以来,其他 CPU 可能已经修改了调度程序的状态(例如: 它可能已经在运行缓存的线程!).

Instead, the SMP “switch to” decision needs to be made synchronously with the swap call, and as we don’t want per-architecture assembly code to be handling scheduler internal state, Zephyr requires a somewhat lower-level context switch primitives for SMP systems: arch_switch() is always called with interrupts masked, and takes exactly two arguments. The first is an opaque (architecture defined) handle to the context to which it should switch, and the second is a pointer to such a handle into which it should store the handle resulting from the thread that is being switched out. The kernel then implements a portable z_swap() implementation on top of this primitive which includes the relevant scheduler logic in a location where the architecture doesn’t need to understand it.

相反,SMP 的“切换到”决策需要与交换调用同步进行,而且由于我们不希望每个体系结构的汇编代码处理调度程序的内部状态,Zephyr 需要为 SMP 系统提供一个稍微低级的上下文切换原语: arch_switch ()总是使用中断掩码调用,并且只接受两个参数。第一个是它应该切换到的上下文的不透明句柄(体系结构定义的) ,第二个是指向这样一个句柄的指针,它应该将切换出来的线程产生的句柄存储到这个句柄中。然后,内核在这个原语的基础上实现了一个可移植的 z_swap ()实现,该实现包含了相关的调度器逻辑,而这个位置的体系结构并不需要理解它。

Similarly, on interrupt exit, switch-based architectures are expected to call z_get_next_switch_handle() to retrieve the next thread to run from the scheduler. The argument to z_get_next_switch_handle() is either the interrupted thread’s “handle” reflecting the same opaque type used by arch_switch(), or NULL if that thread cannot be released to the scheduler just yet. The choice between a handle value or NULL depends on the way CPU interrupt mode is implemented.

类似地,在中断退出时,基于交换机的体系结构需要调用 z_get_next_switch_handle ()来检索从调度程序运行的下一个线程。Z_get_next_switch_handle ()的参数要么是中断线程的“ handle”,反映了 arch_switch ()使用的相同的不透明类型,要么是 NULL,如果该线程还不能释放到调度程序。在句柄值或 NULL 之间的选择取决于 CPU 中断模式的实现方式。

Architectures with a large CPU register file would typically preserve only the caller-saved registers on the current thread’s stack when interrupted in order to minimize interrupt latency, and preserve the callee-saved registers only when arch_switch() is called to minimize context switching latency. Such architectures must use NULL as the argument to z_get_next_switch_handle() to determine if there is a new thread to schedule, and follow through with their own arch_switch() or derrivative if so, or directly leave interrupt mode otherwise. In the former case it is up to that switch code to store the handle resulting from the thread that is being switched out in that thread’s “switch_handle” field after its context has fully been saved.

具有大型 CPU 寄存器文件的架构通常只会在中断时保留当前线程堆栈中的调用方保存的寄存器,以尽量减少中断延迟,并且只有在调用 arch_switch ()以尽量减少上下文切换延迟时才保留被调用方保存的寄存器。这样的体系结构必须使用 NULL 作为参数来调度 z_get_next_switch_handle(),以确定是否有一个新线程需要调度,如果有,则使用它们自己的 arch_switch ()或 derrivative (如果有的话) ,或者直接离开中断模式。在前一种情况下,在线程的上下文完全保存之后,由交换代码来存储从线程的“ switch_handle”字段中被切换出来的句柄。

Architectures whose entry in interrupt mode already preserves the entire thread state may pass that thread’s handle directly to z_get_next_switch_handle() and be done in one step.

在中断模式下进入已经保留了整个线程状态的体系结构可以将该线程的句柄直接传递给 z_get_next_switch_handle(),并且一步完成。

Note that while SMP requires CONFIG_USE_SWITCH, the reverse is not true. A uniprocessor architecture built with CONFIG_SMP set to No might still decide to implement its context switching using arch_switch().

请注意,虽然 SMP 需要 config_use_switch,但是反过来就不正确了。使用 CONFIG_smp 设置为 No 构建的单处理器体系结构仍然可能决定使用 arch_switch ()实现其上下文切换。

Data Passing 数据传递

These pages cover kernel objects which can be used to pass data between threads and ISRs.

这些页面涉及可用于在线程和 isr 之间传递数据的内核对象。

The following table summarizes their high-level features.

下表总结了它们的高级特性:

Object对象 Bidirectional?
双向? ?
Data structure
数据结构
Data item size
数据项大小
Data Alignment
数据对齐
ISRs can receive?
是否可以接收?
ISRs can send?ISRs
可以发送?
Overrun handling
超支处理
FIFO
先进先出法
No
没有
Queue
排队
Arbitrary [1]
任意[1]
4 B [2] Yes [3]
是的[3]
Yes是的 N/A不适用
LIFO
后进先出法
No
没有
Queue
排队
Arbitrary [1]
任意[1]
4 B [2] Yes [3]
是的[3]
Yes是的 N/A不适用
Stack No
没有
Array
数组
Word Word Yes [3]
是的[3]
Yes是的 Undefined behavior
未定义行为
Message queue
消息队列
No
没有
Ring buffer
环形缓冲器
Power of two
2的幂
Power of two
2的幂
Yes [3]
是的[3]
Yes是的 Pend thread or return -errnoPend
线程或返回-errno
Mailbox
邮箱
Yes
是的
Queue
排队
Arbitrary [1]
任意[1]
Arbitrary
任意
No没有 No没有 N/A不适用
Pipe No
没有
Ring buffer [4]
环形缓冲区[4]
Arbitrary
任意
Arbitrary
任意
No没有 No没有 Pend thread or return -errnoPend 线程或返回-errno

[1] Callers allocate space for queue overhead in the data elements themselves.

[1] 调用方为数据元素本身的队列开销分配空间。

[2] Objects added with k_fifo_alloc_put() and k_lifo_alloc_put() do not have alignment constraints, but use temporary memory from the calling thread’s resource pool.

[2] 添加了 k_fifo_alloc_put ()和 k_lifo_alloc_put ()的对象没有对齐约束,但是使用调用线程资源池中的临时内存。

[3] ISRs can receive only when passing K_NO_WAIT as the timeout argument.

[3] 只有当传递 k_no_wait 作为超时参数时,ISRs 才能接收。

[4] Optional.

[4] 可选。