From 4cc958ab78ca8911b139d3664b819094e4c05b10 Mon Sep 17 00:00:00 2001 From: Alexander pinheiro paschoaletto <1222703@isep.ipp.pt> Date: Sun, 29 Dec 2024 17:38:15 +0000 Subject: [PATCH 1/3] kernel: CBS: Adding support for Constant Bandwidth Server (CBS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces kernel support for the Constant Bandwidth Server (CBS), an extension of the Earliest Deadline First (EDF) scheduler that allows tasks to be executed virtually isolated from each other, in a way that if a task executes for longer than expected it doesn’t interfere on the execution of the others. In other words, the CBS prevents that a task misbehavior causes other tasks to miss their own deadlines. In a nutshell, the CBS is a work-conserving wrapper for the tasks that automatically recalculates their deadlines when they exceed their configured timing constraints, thus acting as a fail-safe for EDF overruns. Signed-off-by: Alexander pinheiro paschoaletto <1222703@isep.ipp.pt> --- include/zephyr/kernel/thread.h | 5 + include/zephyr/sched_server/cbs.h | 169 +++++++++++ kernel/CMakeLists.txt | 10 + kernel/Kconfig | 1 + kernel/Kconfig.sched_server | 130 ++++++++ kernel/include/kswap.h | 16 + kernel/include/sched_server/cbs_internal.h | 34 +++ kernel/include/sched_server/cbs_log.h | 31 ++ kernel/sched.c | 18 ++ kernel/sched_server/cbs.c | 333 +++++++++++++++++++++ kernel/sched_server/cbs_log.c | 34 +++ kernel/thread.c | 3 + 12 files changed, 784 insertions(+) create mode 100644 include/zephyr/sched_server/cbs.h create mode 100644 kernel/Kconfig.sched_server create mode 100644 kernel/include/sched_server/cbs_internal.h create mode 100644 kernel/include/sched_server/cbs_log.h create mode 100644 kernel/sched_server/cbs.c create mode 100644 kernel/sched_server/cbs_log.c diff --git a/include/zephyr/kernel/thread.h b/include/zephyr/kernel/thread.h index fd8e4c02f235e..319a1e76521de 100644 --- a/include/zephyr/kernel/thread.h +++ b/include/zephyr/kernel/thread.h @@ -372,6 +372,11 @@ struct k_thread { _wait_q_t halt_queue; #endif /* CONFIG_SMP */ +#ifdef CONFIG_CBS + /** Constant Bandwidth Server (CBS) struct */ + void *cbs; +#endif + /** arch-specifics: must always be at the end */ struct _thread_arch arch; }; diff --git a/include/zephyr/sched_server/cbs.h b/include/zephyr/sched_server/cbs.h new file mode 100644 index 0000000000000..2e8b90a243ec0 --- /dev/null +++ b/include/zephyr/sched_server/cbs.h @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2024 Instituto Superior de Engenharia do Porto (ISEP) + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * + * @brief Constant Bandwidth Server (CBS) public API + */ + +#ifndef ZEPHYR_CBS +#define ZEPHYR_CBS + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef CONFIG_CBS + +/** + * @defgroup cbs_apis Constant Bandwidth Server (CBS) APIs + * @ingroup kernel_apis + * @{ + */ + +/** + * @brief CBS job format. + * + * A job pushed to a CBS is a regular function that must + * have a void pointer as an argument (can be NULL if not + * needed) and return nothing. jobs are pushed to the CBS by + * invoking k_cbs_push_job(). + */ +typedef void (*cbs_callback_t)(void *arg); + +/** + * @cond INTERNAL_HIDDEN + */ + +#ifdef CONFIG_TIMER_HAS_64BIT_CYCLE_COUNTER +typedef uint64_t cbs_cycle_t; +#else +typedef uint32_t cbs_cycle_t; +#endif + +struct cbs_job { + cbs_callback_t function; + void *arg; +}; + +struct cbs_budget { + cbs_cycle_t current; + cbs_cycle_t max; +}; + +struct cbs_arg { + k_timeout_t budget; + k_timeout_t period; +}; + +struct k_cbs { + struct k_timer timer; + struct k_msgq *queue; + struct k_thread *thread; + struct cbs_budget budget; + cbs_cycle_t period; + cbs_cycle_t abs_deadline; + cbs_cycle_t start_cycle; + cbs_cycle_t bandwidth; + bool is_active; + unsigned int left_shift; +#ifdef CONFIG_CBS_LOG + char name[CONFIG_CBS_THREAD_MAX_NAME_LEN]; +#endif +}; + +extern void cbs_thread(void *job_queue, void *cbs_struct, void *unused); + +/** @endcond */ + +/** + * @brief pushes a job to a CBS queue. + * + * This routine inserts a job (i.e. a regular C function) in a CBS queue, + * which will eventually execute it when the CBS deadline becomes the + * earliest of the taskset. Inserted jobs are always served in a FIFO + * manner. The job queue can store up to @kconfig{CONFIG_CBS_QUEUE_LENGTH} + * jobs at once. + * + * @param cbs Name of the CBS. + * @param job_function Function of the job. + * @param job_arg Argument to be passed to the job function. + * @param timeout Waiting period to push the job, or one of the special + * values K_NO_WAIT and K_FOREVER. + * + * @retval 0 the job was pushed to the CBS queue. + * @retval -ENOMSG if CBS is not defined, returned without waiting or CBS queue purged. + * @retval -EAGAIN if waiting period timed out. + */ +int k_cbs_push_job(struct k_cbs *cbs, cbs_callback_t job_function, void *job_arg, + k_timeout_t timeout); + +/** + * @brief Statically define and initialize a Constant Bandwidth Server (CBS). + * + * A CBS is an extension of the Earliest Deadline First (EDF) scheduler + * that allows tasks to be executed virtually isolated from each other, + * in a way that if a task executes for longer than expected it doesn’t + * interfere on the execution of the others. In other words, the CBS + * prevents that a task misbehavior causes other tasks to miss their + * own deadlines. + * + * In a nutshell, the CBS is a work-conserving wrapper for the tasks + * that automatically recalculates their deadlines when they exceed their + * allowed execution time slice. This time slice is known as the CBS "budget". + * The value used to recalculate the deadline is known as the CBS "period". + * + * When a task instance (i.e. job) runs within a CBS, it consumes the "budget". + * When the "budget" runs out, the deadline is postponed by "period" time units + * and the "budget" is replenished to its maximum capacity. when there are no jobs + * left for a CBS to execute, it remains idle and takes no CPU time. + * + * Finally, whenever a new job is pushed to an idle server, the kernel verifies + * if the current pair of (budget, deadline) are proportionally compatible with + * the configured values. If not, the deadline is also recalculated here. + * These two procedures ensure the CBS will never use the CPU more than + * what was configured, and that it will not endanger other thread's deadlines. + * + * Once created, the CBS can be referenced through its name: + * + * @code extern const struct k_cbs ; @endcode + * + * @param cbs_name Name of the CBS. + * @param cbs_budget Budget of the CBS thread, in system ticks. + * Used for triggering deadline recalculations. + * @param cbs_period Period of the CBS thread. in system ticks. + * Used for recalculating the absolute deadline. + * @param cbs_static_priority Static priority of the CBS thread. + * + * @note The CBS is meant to be used alongside the EDF policy, which in Zephyr + * is effectively used as a "tie-breaker" when two threads of equal static priorities + * are ready for execution. Therefore it is recommended that all user threads feature + * the same preemptive static priority (e.g. 5) in order to ensure the scheduling + * to work as expected, and that this same value is passed as @a cbs_static_priority. + * + * @note you should have @kconfig{CONFIG_CBS} enabled in your project to use the CBS. + */ +#define K_CBS_DEFINE(cbs_name, cbs_budget, cbs_period, cbs_static_priority) \ + K_MSGQ_DEFINE(queue_##cbs_name, sizeof(struct cbs_job), CONFIG_CBS_QUEUE_LENGTH, 1); \ + static struct k_cbs cbs_name = { \ + .queue = &queue_##cbs_name, \ + .is_active = false, \ + }; \ + static struct cbs_arg args_##cbs_name = {.budget = cbs_budget, .period = cbs_period}; \ + K_THREAD_DEFINE(thread_##cbs_name, CONFIG_CBS_THREAD_STACK_SIZE, cbs_thread, \ + (void *)STRINGIFY(cbs_name), (void *)&cbs_name, (void *)&args_##cbs_name, \ + cbs_static_priority, 0, CONFIG_CBS_INITIAL_DELAY) +/** @} */ /* end of Constant Bandwidth Server (CBS) APIs */ + +#endif /* CONFIG_CBS */ + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_CBS */ diff --git a/kernel/CMakeLists.txt b/kernel/CMakeLists.txt index 8ba95f6c5708c..2f9194a3718e0 100644 --- a/kernel/CMakeLists.txt +++ b/kernel/CMakeLists.txt @@ -115,6 +115,16 @@ list(APPEND kernel_files ) endif() +if(CONFIG_CBS) +list(APPEND kernel_files + sched_server/cbs.c + ) +if(CONFIG_CBS_LOG) +list(APPEND kernel_files + sched_server/cbs_log.c + ) +endif() # CONFIG_CBS_LOG +endif() if(CONFIG_THREAD_MONITOR) list(APPEND kernel_files diff --git a/kernel/Kconfig b/kernel/Kconfig index 28177a1d76a6f..cb4b18d9cd4b1 100644 --- a/kernel/Kconfig +++ b/kernel/Kconfig @@ -1083,3 +1083,4 @@ endmenu rsource "Kconfig.device" rsource "Kconfig.vm" rsource "Kconfig.init" +rsource "Kconfig.sched_server" diff --git a/kernel/Kconfig.sched_server b/kernel/Kconfig.sched_server new file mode 100644 index 0000000000000..d9916cf0870dc --- /dev/null +++ b/kernel/Kconfig.sched_server @@ -0,0 +1,130 @@ +# config options for Scheduling Servers +# +# Copyright (c) 2024 Instituto Superior de Engenharia do Porto (ISEP) +# SPDX-License-Identifier: Apache-2.0 + +menu "Constant Bandwidth Server (CBS)" + +config CBS + bool "Constant Bandwidth Server (CBS)" + depends on USE_SWITCH + select SCHED_DEADLINE + default n + help + Enables the Constant Bandwidth Server (CBS), an extension + of the Earliest Deadline First (EDF) scheduler that allows + tasks to execute with guaranteed bandwidth. Selecting + this option automatically enables the EDF scheduler + (CONFIG_SCHED_DEADLINE=y). The CBS is also currently + supported only in targets that use the _arch_switch + context switching primitive (CONFIG_USE_SWITCH=y). + +config CBS_LOG + bool "CBS event logging" + depends on CBS + select LOG + default n + help + Enables logging of Constant Bandwidth Server (CBS) events, + such as jobs being pushed to the server, budget consumpion + and CBS thread switching in/out of the CPU. This option + requires CONFIG_CBS to be included within the project. + +if CBS_LOG +menu "CBS events to be logged" + + config CBS_LOG_JOB_PUSH + bool "J_PUSH log" + default y + help + Enables logging of when + a job is pushed to a CBS. + + config CBS_LOG_JOB_COMPLETE + bool "J_COMP log" + default y + help + Enables logging of when + a job is completed. + + config CBS_LOG_BUDGET_CONDITION + bool "B_COND log" + default y + help + Enables logging of when + the CBS condition gets met, + causing the CBS deadline to + be updated and the budget to + be replenished. + + config CBS_LOG_BUDGET_RAN_OUT + bool "B_ROUT log" + default y + help + Enables logging of when + the CBS budget runs out, + causing the CBS deadline to + be postponed and the budget to + be replenished. + + config CBS_LOG_SWITCHED_IN + bool "SWT_TO log" + default y + help + Enables logging of when + the CBS thread enters the + CPU for execution of jobs. + + config CBS_LOG_SWITCHED_OUT + bool "SWT_AY log" + default y + help + Enables logging of when + the CBS thread leaves the + CPU after completing a job + or being preempted by + another thread. + +endmenu +endif + +if CBS +menu "CBS constants" + + config CBS_THREAD_STACK_SIZE + int "CBS thread memory" + default 2048 + help + The amount of memory to be + allocated for the CBS thread. + + config CBS_QUEUE_LENGTH + int "CBS job queue length" + default 16 + help + The highest number of jobs that can + be pushed to a CBS job queue at once. + + config CBS_INITIAL_DELAY + int "CBS thread initial delay" + default 0 + help + Scheduling delay (in milliseconds) for the CBS thread. + + if CBS_LOG + config CBS_THREAD_MAX_NAME_LEN + int "Max length of the CBS thread name" + default 20 + help + CBS thread names are the same as the variables created with + K_CBS_DEFINE and get stored in the k_cbs struct. This option + indicates the maximum name length of the CBS server to be + displayed on log events, including the terminating NULL byte. + Reducing this value wil help conserving memory, but as a tradeoff + it might chop a portion the server name on the aforementioned logs. + endif + +endmenu +endif + +endmenu # Constant Bandwidth Server (CBS) diff --git a/kernel/include/kswap.h b/kernel/include/kswap.h index 03c4a991bb08c..0dcea67a5032f 100644 --- a/kernel/include/kswap.h +++ b/kernel/include/kswap.h @@ -11,6 +11,10 @@ #include #include +#ifdef CONFIG_CBS +#include +#endif + #ifdef CONFIG_STACK_SENTINEL extern void z_check_stack_sentinel(void); #else @@ -130,9 +134,21 @@ static ALWAYS_INLINE unsigned int do_swap(unsigned int key, z_smp_release_global_lock(new_thread); } #endif /* CONFIG_SMP */ +#ifdef CONFIG_CBS + /* if old_thread belongs to an active CBS, stop its budget timer */ + if (old_thread->cbs) { + cbs_switched_out(old_thread->cbs); + } +#endif z_thread_mark_switched_out(); z_sched_switch_spin(new_thread); z_current_thread_set(new_thread); +#ifdef CONFIG_CBS + /* if new_thread belongs to an active CBS, start its budget timer */ + if (new_thread->cbs) { + cbs_switched_in(new_thread->cbs); + } +#endif #ifdef CONFIG_TIMESLICING z_reset_time_slice(new_thread); diff --git a/kernel/include/sched_server/cbs_internal.h b/kernel/include/sched_server/cbs_internal.h new file mode 100644 index 0000000000000..24fe0dd53aaa8 --- /dev/null +++ b/kernel/include/sched_server/cbs_internal.h @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 Instituto Superior de Engenharia do Porto (ISEP) + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * + * @brief Constant Bandwidth Server (CBS) internal APIs and definitions + */ + +#ifndef ZEPHYR_CBS_INTERNAL +#define ZEPHYR_CBS_INTERNAL + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifdef CONFIG_TIMEOUT_64BIT +#define CBS_TICKS_TO_CYC(t) k_ticks_to_cyc_floor32(t) +#else +#define CBS_TICKS_TO_CYC(t) k_ticks_to_cyc_floor64(t) +#endif + +void cbs_switched_in(struct k_cbs *cbs); +void cbs_switched_out(struct k_cbs *cbs); + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_CBS_INTERNAL */ diff --git a/kernel/include/sched_server/cbs_log.h b/kernel/include/sched_server/cbs_log.h new file mode 100644 index 0000000000000..dbb40af7ee881 --- /dev/null +++ b/kernel/include/sched_server/cbs_log.h @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2024 Instituto Superior de Engenharia do Porto (ISEP) + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_CBS_LOG_H_ +#define ZEPHYR_CBS_LOG_H_ + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +enum cbs_evt { + CBS_PUSH_JOB, + CBS_SWITCH_TO, + CBS_SWITCH_AWAY, + CBS_COMPLETED_JOB, + CBS_BUDGET_RAN_OUT, + CBS_BUDGET_CONDITION_MET +}; + +void cbs_log(enum cbs_evt event, struct k_cbs *cbs); + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_CBS_LOG_H_ */ diff --git a/kernel/sched.c b/kernel/sched.c index d4de81b19d7cd..9db27ef6c5a31 100644 --- a/kernel/sched.c +++ b/kernel/sched.c @@ -22,6 +22,10 @@ #include #include +#ifdef CONFIG_CBS +#include +#endif + LOG_MODULE_DECLARE(os, CONFIG_KERNEL_LOG_LEVEL); #if defined(CONFIG_SWAP_NONATOMIC) && defined(CONFIG_TIMESLICING) @@ -800,8 +804,22 @@ struct k_thread *z_swap_next_thread(void) /* Just a wrapper around z_current_thread_set(xxx) with tracing */ static inline void set_current(struct k_thread *new_thread) { +#ifdef CONFIG_CBS + /* if preempted thread is an active CBS, stop its timer */ + if (_current->cbs) { + cbs_switched_out(_current->cbs); + } +#endif /* CONFIG_CBS */ + z_thread_mark_switched_out(); z_current_thread_set(new_thread); + +#ifdef CONFIG_CBS + /* if new thread is an active CBS, start its timer */ + if (new_thread->cbs) { + cbs_switched_in(new_thread->cbs); + } +#endif /* CONFIG_CBS */ } /** diff --git a/kernel/sched_server/cbs.c b/kernel/sched_server/cbs.c new file mode 100644 index 0000000000000..f0674378f55d0 --- /dev/null +++ b/kernel/sched_server/cbs.c @@ -0,0 +1,333 @@ +/* + * Copyright (c) 2024 Instituto Superior de Engenharia do Porto (ISEP) + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file + * @brief Constant Bandwidth Server (CBS) implementation + * + * This module provides the implementation of the Constant + * Bandwidth Server (CBS), an extension of the EDF scheduler + * which isolates thread execution time unpredictabilities + * from the rest of the system, thus reducing interference + * in other thread's execution. + */ +#include +#include +#include +#include + +#ifdef CONFIG_CBS_LOG +#include +#endif /* CONFIG_CBS_LOG */ + +#ifdef CONFIG_TIMER_HAS_64BIT_CYCLE_COUNTER +#define cbs_get_now() k_cycle_get_64() +#else +#define cbs_get_now() k_cycle_get_32() +#endif + +static void cbs_replenish_due_to_condition(struct k_cbs *cbs, cbs_cycle_t cycle) +{ + /* + * if the budget replenishing condition is + * met when a new job comes to the server, + * this function is executed. + */ + cbs->abs_deadline = cycle + cbs->period; + cbs->budget.current = cbs->budget.max; + +#if defined(CONFIG_CBS_LOG) && defined(CONFIG_CBS_LOG_BUDGET_CONDITION) + cbs_log(CBS_BUDGET_CONDITION_MET, cbs); +#endif +} + +static inline void cbs_replenish_due_to_run_out(struct k_cbs *cbs, cbs_cycle_t cycle) +{ + /* + * if job is still running when the budget + * runs out, this function is executed. + */ + cbs->start_cycle = cycle; + cbs->abs_deadline += cbs->period; + cbs->budget.current = cbs->budget.max; + +#if defined(CONFIG_CBS_LOG) && defined(CONFIG_CBS_LOG_BUDGET_RAN_OUT) + cbs_log(CBS_BUDGET_RAN_OUT, cbs); +#endif +} + +static inline void cbs_budget_update_consumption(struct k_cbs *cbs) +{ + if (!cbs->start_cycle) { + return; + } + cbs_cycle_t now = cbs_get_now(); + cbs_cycle_t budget_used = now - cbs->start_cycle; + + if (budget_used < cbs->budget.current) { + /* + * This is the expected case when this function + * is called: we have used less budget than what + * we have available. A simple subtracion will + * take care of updating what's left. + */ + cbs->budget.current -= budget_used; + return; + } + /* + * If we end up here, the job has finished before + * the timer expired but also spent more than + * the allowed budget. We therefore need to + * compensate that on replenishing. + + * It should be noted that the chances of falling + * into these edge cases are higher on low-resolution + * timers. The default 1ms resolution of Zephyr is + * relatively low and might result in many budget + * tracking inaccuracies depending on the types of + * jobs executed. It is therefore recommended to + * have a 100us resolution or lower instead. + */ + cbs_cycle_t excess; + + if (budget_used > cbs->budget.max) { + /* + * Very edge case where the job surpassed + * the max allowed value for the budget + * itself one or more times in a row. + * It is more likely to happen when the + * tick resolution is too coarse (e.g. 1ms) + * and the budget is too small (e.g. 400us) + */ + for (; budget_used > cbs->budget.max; budget_used -= cbs->budget.max) { + cbs_replenish_due_to_run_out(cbs, cbs->start_cycle + cbs->budget.max); + } + excess = budget_used; + } else { + excess = budget_used - cbs->budget.current; + } + K_SPINLOCK(&_sched_spinlock) { + cbs_replenish_due_to_run_out(cbs, (now - excess)); + k_thread_deadline_set(cbs->thread, (int)(cbs->abs_deadline - cbs->start_cycle)); + } + z_impl_k_reschedule(); + cbs->budget.current -= excess; +} + +static void cbs_budget_timer_expired_callback(struct k_timer *timer) +{ + /* + * This function is called by the timer when + * it expires, which means the budget has been + * entirely used and therefore needs replenishing. + */ + cbs_cycle_t now = cbs_get_now(); + struct k_cbs *cbs = CONTAINER_OF(timer, struct k_cbs, timer); + + K_SPINLOCK(&_sched_spinlock) { + cbs_replenish_due_to_run_out(cbs, now); + k_thread_deadline_set(cbs->thread, (int)(cbs->abs_deadline - now)); + k_timer_start(timer, K_CYC((uint32_t)cbs->budget.current), K_NO_WAIT); + } + z_impl_k_reschedule(); +} + +static void cbs_budget_timer_stop_callback(struct k_timer *timer) +{ + /* + * This function is called by the timer when it is stopped + * before expiring, which means the thread has finished + * executing the job or was preempted. There is still + * budget left, which is why we need to update its value. + */ + struct k_cbs *cbs = CONTAINER_OF(timer, struct k_cbs, timer); + + cbs_budget_update_consumption(cbs); + cbs->start_cycle = 0; +} + +static inline void cbs_check_current_bandwidth(struct k_cbs *cbs) +{ + cbs_cycle_t arrival = cbs_get_now(); + cbs_cycle_t deadline = cbs->abs_deadline; + /* + * This function is a way to see, when a new job is + * pushed to the server, if the current server bandwidth + * (budget / deadline) is lower than the configured + * bandwidth. If that is true, the job can be served + * with the current budget and deadline. If not, the + * deadline needs to be postponed. + * + * So basically, if the condition below is met, + * we need to recalculate things. We only check + * it when a new job comes: + * + * Cs >= (ds - rjob) * U + * + * 'Cs' is the budget left + * 'ds' is the absolute deadline, + * 'rjob' is the arrival instant of the job + * 'U' is the server bandwidth (max budget / period) + * + * Note that if arrival >= deadline, condition is instantly + * met. Otherwise, we need to check more stuff. We exit + * without doing anything if the condition is not met. + */ + if (deadline > arrival) { + cbs_cycle_t budget = (cbs->budget.current << cbs->left_shift); + + if (budget < (deadline - arrival) * cbs->bandwidth) { + return; + } + } + K_SPINLOCK(&_sched_spinlock) { + cbs_replenish_due_to_condition(cbs, arrival); + k_thread_deadline_set(cbs->thread, (int)cbs->period); + } + z_impl_k_reschedule(); +} + +static inline void cbs_budget_timer_start(struct k_cbs *cbs) +{ + if (cbs->start_cycle > 0) { + return; + } + cbs->start_cycle = cbs_get_now(); + k_timer_start(&(cbs->timer), K_CYC((uint32_t)cbs->budget.current), K_NO_WAIT); +} + +static inline void cbs_budget_timer_stop(struct k_cbs *cbs) +{ + k_timer_stop(&(cbs->timer)); +} + +static inline void cbs_find_highest_shift_for(struct k_cbs *cbs) +{ + /* + * This for loop finds the highest left-shift value + * we can apply to the budget before overflowing it. + * + * The actual left shift logical is done because the + * CBS condition checks an equation that, in paper, + * uses decimal numbers within it. However, all math + * is made here keeps the unsigned integer types of the + * variables involved for performance reasons. So + * the shift is applied in both sides of the equation + * in an attempt to preserve the value resolution. + * + * more details in cbs_check_current_bandwidth(). + */ + for (unsigned int right_shift = 1; right_shift < 32; right_shift++) { + if ((INT_MAX >> right_shift) < cbs->budget.max) { + cbs->left_shift = right_shift - 1; + cbs->bandwidth = (cbs->budget.max << cbs->left_shift) / cbs->period; + break; + } + } +} + +void cbs_thread(void *server_name, void *cbs_struct, void *cbs_args) +{ + struct cbs_arg *args = (struct cbs_arg *)cbs_args; + struct k_cbs *cbs = (struct k_cbs *)cbs_struct; + struct cbs_job job; + + cbs->thread = k_current_get(); + cbs->thread->cbs = cbs; + cbs->start_cycle = 0; + cbs->period = CBS_TICKS_TO_CYC(args->period.ticks); + cbs->budget.max = CBS_TICKS_TO_CYC(args->budget.ticks); + cbs->budget.current = cbs->budget.max; + cbs->left_shift = 0; + cbs->abs_deadline = 0; + cbs_find_highest_shift_for(cbs); + k_timer_init(&(cbs->timer), cbs_budget_timer_expired_callback, + cbs_budget_timer_stop_callback); + +#ifdef CONFIG_CBS_LOG + strncpy(cbs->name, (char *)server_name, CONFIG_CBS_THREAD_MAX_NAME_LEN - 1); +#endif + + for (;;) { + /* + * The CBS thread remains dormant while + * there are no jobs to execute. Once they + * arrive, timer starts, execution is made, + * timer ends, budget is updated and + * the cycle repeats. + */ + k_msgq_get(cbs->queue, &job, K_FOREVER); + cbs_budget_timer_start(cbs); + job.function(job.arg); + cbs_budget_timer_stop(cbs); + +#if defined(CONFIG_CBS_LOG) && defined(CONFIG_CBS_LOG_JOB_COMPLETE) + cbs_log(CBS_COMPLETED_JOB, cbs); +#endif + cbs->is_active = (cbs->queue->used_msgs > 0); + } +} + +void cbs_switched_in(struct k_cbs *cbs) +{ + /* + * this function is called at every context switch + * and just starts the budget timer if the incoming + * thread belongs to an active CBS. If it doesn't, + * there's nothing to do. + */ + if (cbs->is_active) { + cbs_budget_timer_start(cbs); + } +#if defined(CONFIG_CBS_LOG) && defined(CONFIG_CBS_LOG_SWITCHED_IN) + cbs_log(CBS_SWITCH_TO, cbs); +#endif +} + +void cbs_switched_out(struct k_cbs *cbs) +{ + /* + * this function is called at every context switch + * and just stops the budget timer if the outgoing + * thread belongs to an active CBS. If it doesn't, + * there's nothing to do. + */ + if (cbs->is_active) { + cbs_budget_timer_stop(cbs); + } +#if defined(CONFIG_CBS_LOG) && defined(CONFIG_CBS_LOG_SWITCHED_OUT) + cbs_log(CBS_SWITCH_AWAY, cbs); +#endif +} + +int k_cbs_push_job(struct k_cbs *cbs, cbs_callback_t job_function, void *job_arg, + k_timeout_t timeout) +{ + /* + * this function is the public API to insert a job + * within the CBS queue. If the CBS wasn't executing + * anything before, it will become active and check + * if the existing bandwidth (budget / deadline) is + * compatible with the configured (budget / period). + */ + if (!cbs) { + return -ENOMSG; + } + + struct cbs_job job = {job_function, job_arg}; + int result = k_msgq_put(cbs->queue, &job, timeout); + + if (result == 0) { +#if defined(CONFIG_CBS_LOG) && defined(CONFIG_CBS_LOG_JOB_PUSH) + cbs_log(CBS_PUSH_JOB, cbs); +#endif + if (!cbs->is_active) { + cbs_check_current_bandwidth(cbs); + cbs->is_active = true; + } + } + + return result; +} diff --git a/kernel/sched_server/cbs_log.c b/kernel/sched_server/cbs_log.c new file mode 100644 index 0000000000000..f3690560863ac --- /dev/null +++ b/kernel/sched_server/cbs_log.c @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2024 Instituto Superior de Engenharia do Porto (ISEP) + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +LOG_MODULE_REGISTER(CBS); + +void cbs_log(enum cbs_evt event, struct k_cbs *cbs) +{ + switch (event) { + case CBS_PUSH_JOB: /* when a job is pushed to a server */ + LOG_INF("%s\tJ_PUSH\t%u", (char *)cbs->name, (uint32_t)cbs->budget.current); + break; + case CBS_SWITCH_TO: /* when cbs thread enters the CPU */ + LOG_INF("%s\tSWT_TO\t%u", (char *)cbs->name, (uint32_t)cbs->budget.current); + break; + case CBS_SWITCH_AWAY: /* when cbs thread leaves the CPU */ + LOG_INF("%s\tSWT_AY\t%u", (char *)cbs->name, (uint32_t)cbs->budget.current); + break; + case CBS_COMPLETED_JOB: /* when cbs completes the execution of a job */ + LOG_INF("%s\tJ_COMP\t%u", (char *)cbs->name, (uint32_t)cbs->budget.current); + break; + case CBS_BUDGET_RAN_OUT: /* when cbs budget runs out */ + LOG_INF("%s\tB_ROUT\t%u", (char *)cbs->name, (uint32_t)cbs->budget.current); + break; + case CBS_BUDGET_CONDITION_MET: + /* when new job arrives and condition Cs >= (ds - rjob)*(Qs / T) is met */ + LOG_INF("%s\tB_COND\t%u", (char *)cbs->name, (uint32_t)cbs->budget.current); + break; + } +} diff --git a/kernel/thread.c b/kernel/thread.c index 184343a70de6b..e9d2dff4d0651 100644 --- a/kernel/thread.c +++ b/kernel/thread.c @@ -630,6 +630,9 @@ char *z_setup_new_thread(struct k_thread *new_thread, #ifdef CONFIG_SCHED_DEADLINE new_thread->base.prio_deadline = 0; #endif /* CONFIG_SCHED_DEADLINE */ +#ifdef CONFIG_CBS + new_thread->cbs = NULL; +#endif /* CONFIG_CBS */ new_thread->resource_pool = _current->resource_pool; #ifdef CONFIG_SMP From 97650f1d02c4a6ac07465e83d27572e597fc7035 Mon Sep 17 00:00:00 2001 From: Alexander pinheiro paschoaletto <1222703@isep.ipp.pt> Date: Mon, 13 Jan 2025 12:32:42 +0000 Subject: [PATCH 2/3] docs: CBS: adding docs for the Constant Bandwidth Server (CBS) This commit simply adds the documentation for the CBS. Signed-off-by: Alexander pinheiro paschoaletto <1222703@isep.ipp.pt> --- doc/introduction/index.rst | 1 + doc/kernel/services/index.rst | 1 + doc/kernel/services/sched_server/cbs.rst | 374 ++++++++++++++++++ .../services/sched_server/example-1.svg | 3 + .../services/sched_server/example-2.svg | 3 + .../services/sched_server/example-3.svg | 3 + doc/zephyr.doxyfile.in | 2 +- 7 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 doc/kernel/services/sched_server/cbs.rst create mode 100644 doc/kernel/services/sched_server/example-1.svg create mode 100644 doc/kernel/services/sched_server/example-2.svg create mode 100644 doc/kernel/services/sched_server/example-3.svg diff --git a/doc/introduction/index.rst b/doc/introduction/index.rst index af67331aa2991..3d24e4e5601a2 100644 --- a/doc/introduction/index.rst +++ b/doc/introduction/index.rst @@ -78,6 +78,7 @@ Zephyr offers a large and ever growing number of features including: * Cooperative and Preemptive Scheduling * Earliest Deadline First (EDF) + * Constant Bandwidth Server (CBS) * Meta IRQ scheduling implementing "interrupt bottom half" or "tasklet" behavior * Timeslicing: Enables time slicing between preemptible threads of equal diff --git a/doc/kernel/services/index.rst b/doc/kernel/services/index.rst index cb56ccd3673b4..4504a4fb178af 100644 --- a/doc/kernel/services/index.rst +++ b/doc/kernel/services/index.rst @@ -32,6 +32,7 @@ synchronization. threads/index.rst scheduling/index.rst + sched_server/cbs.rst threads/system_threads.rst threads/workqueue.rst threads/nothread.rst diff --git a/doc/kernel/services/sched_server/cbs.rst b/doc/kernel/services/sched_server/cbs.rst new file mode 100644 index 0000000000000..87e5b02a44304 --- /dev/null +++ b/doc/kernel/services/sched_server/cbs.rst @@ -0,0 +1,374 @@ +.. _constant_bandwidth_server_v1: + +Constant Bandwidth Server (CBS) +############################### + +A :dfn:`CBS` is an extension of the Earliest Deadline First (EDF) scheduler +that allows tasks to be executed virtually isolated from each other, in a way +that if a task executes for longer than expected it doesn't interfere on the +execution of the others. In other words, the CBS prevents that a task +misbehavior causes other tasks to miss their own deadlines. + +.. contents:: + :local: + :depth: 1 + +Introduction +************ + +The regular EDF scheduler selects which task should be executing based on +whichever has the earliest absolute deadline of all. The absolute deadline +of a task, then, is the time instant in which the task *should* have already +be completely executed. In *hard* real-time systems, not meeting the deadline +means a catastrophic failure, whereas in *soft* real-time systems, it means +annoyance to the end user. Many complex systems, however, feature both *hard* +and *soft* tasks to execute, in which case they are called *hybrid* real-time +systems. + +Either way, having a missed deadline is never desirable. On single-core systems, +the EDF guarantees schedulability (i.e. no task will miss a deadline) when the +sum of each task's **bandwidth** - that is, the task execution time divided by +its deadline - is less than or equal to **1** (which means 100% of CPU usage). +However, that implies that the execution times are known beforehand - for hard +tasks, the Worst-Case Execution Time (WCET) is used, and for soft tasks, the +average execution time is preferred. These values are still prone to estimation +errors if the system is not thoroughly tested, and even if it is, a true +worst-case scenario might never truly show up during development phase. + +However, these calculations are not relevant to the regular EDF scheduler at +runtime - it doesn't actively account for tasks that execute for more than the +estimated time, it just cares for the configured deadline of the threads. As +a result, a system which features tasks with variable execution times might +often cause other tasks to miss their deadlines. + +The Constant Bandwidth Server (CBS) +*********************************** + +The CBS solves this problem in EDF by trading the aforementioned execution +time for a time *budget* that represents for how long the task is *allowed* +to execute, somewhat similar to the time slice in the round-robin scheduler. +Unlike round-robin, though, the kernel automatically downgrades the thread +priority by postponing its deadline when the budget runs out. So, if even +with the lowered priority the task in question remains the one with the +earliest deadline, it keeps executing as if nothing happened. + +In a nutshell, the CBS is a work-conserving wrapper for the tasks that +automatically recalculates their deadlines when they exceed their +configured timing constraints. If a task takes longer than expected, +it can be preempted by awaiting tasks instead of jeopardizing their +deadlines. The key features of a CBS are: + +* A **budget**, which represents the maximum amount of time a CBS can + serve the tasks given to it before triggering a deadline recalculation; +* A **period**, which is used to recalculate the absolute deadline of + the server when the kernel intervenes; and +* A **job queue**, dedicated to receive the jobs that the CBS should + execute. All jobs received are executed in a First-In-First-Out manner. + +.. note:: + For the remainder of this documentation, a **job** is an instance of a + task (similar to what objects are to classes in OOP). So while the task + is the function *declaration*, the job is the function *call*. + + Tasks assigned to a CBS don't have a deadline of their own. Instead, each + time a job is pushed to a CBS, it receives the deadline of that CBS. + +In the following section we'll go through the basics of the CBS mechanism. + +How it works +============ + +Assume we have two jobs of tasks *A* and *B*, with an execution time of 2 and 3 +seconds each, that are pushed to a CBS with (budget, period) = (3,7) seconds. +Both jobs are pushed sequentially to the CBS, so job A will be served first +and job B will go next. If the CBS is running alone in the system, this is +what would happen: + +.. image:: example-1.svg + :align: center + :width: 80% + +* At instant t=1, both jobs are pushed. The CBS was idle, so the + deadline is calculated as (t + period) = (1 + 7) = 8. As there + is no other contending task in the system, the CBS starts + executing job A immediately. +* At instant t=3, job A finishes execution. The remaining budget + is of 1 second, since job A spent 2 seconds with its own execution. + Job B, the next one in line, starts here. +* At instant t=4, the budget runs out. This triggers the kernel, + which replenishes the budget at the cost of postponing the CBS + deadline by 7 units (the CBS period). So the new deadline is + (8 + 7) = 15. Since there is no other contending task with an + earliest deadline, the CBS resumes execution of job B. +* At instant t=6, job B is completed. The remaining budget is of + 1 second and will be like this until a new job is pushed. + +Note that, originally, the CBS has an utilization factor (bandwidth) of +(budget / period) = (3 / 7) = 0.4286. When the budget runs out at t=4, +the CBS is granted 3 more seconds of execution at the expense of an +increased deadline, then becoming (6 / 14) = 0.4286. That means the CBS +is always expected to use at most 42.86% of the CPU - hence the name +*Constant Bandwidth Server*. + +Now let's add to this example by imagining two more jobs C and D, with +execution times of 1.3 and 1, are pushed to the CBS at instants t=8 and +t=16, respectively. This is the execution outcome: + +.. image:: example-2.svg + :align: center + :width: 80% + + +* At instant t=8, job C is pushed. The CBS was idle, so the kernel + checks if the available bandwidth of the server - that is, the + budget left (1) divided by the current time until the deadline (7, + since at this point the deadline is set at t=15) - is greater or + equal than the configured server bandwidth (0.4286). Since + (1 / 7) = 0.14, the current bandwidth is lower than configured. + Thus, job C is served with the current budget and deadline. +* At instant t=9, the budget runs out. The kernel then replenishes + the budget and postpones the deadline in 7 seconds, leading to a + new value of (15 + 7) = 22. As there are no other contending tasks, + job C resumes execution. +* At instant t=9.3, job C is completed. The remaining budget is + 2.7 seconds. +* At instant t=16, job D is pushed. The CBS was idle, so the kernel + performs the same bandwidth check as above. This time the current + budget is 2.7 seconds and the deadine is 22, so the available + bandwidth is (2.7 / (22 - 16)) = (2.7 / 6) = 0.45. As it is higher + than the configured 0.4286, serving job D with the current values + could jeopardize the system schedulability (thus, it's better to + intervene). The budget is replenished to 3, and the new deadine is + set at (t + period) = (16 + 7) = 23. +* At instant t=17, job D is completed. The remaining budget is of + 2 seconds and will be like this until a new job is pushed. + +The working principle is simple: the CBS executes jobs that might come +at random intervals in FIFO order, and every time a new job comes to an +idle server, the kernel checks if the available bandwidth is higher +than configured. If that's the case, an intervention happen and the +server deadline is recalculated as to not risk the system schedulability. +Moreover, the deadline is postponed whenever the running job exhausts +the budget. + +Use Cases +********* + +The CBS is optimal for situations where tasks have an unpredictable period +between activations and variable (or unknown) execution times. For these +scenarios, it introduces safeguards that add predictability to the +execution. There are two main ways of using a CBS: within a hard taskset or +to aid in the execution of a soft taskset. + +Case 1: CBS + Hard tasks +======================== + +This is the use case where the hard real-time tasks are scheduled directly +under EDF, and the soft real-time tasks are wrapped by one (or more) CBS's. +Such might be the case for hard tasks because it might not be of interest +that those have their deadlines automatically recalculated when running +for longer than expected. For example, a hard task could be the sampling of +a sensor for a control loop while a soft task could be the rendering of +the user interface. + +When selecting this approach, the CBS can behave like an EDF version of +the :ref:`Workqueue Threads`, offering improved response +times for offloaded work without risking the deadlines of the hard taskset. +The following diagram illustrates the execution outcome for each approach +considering the exact same set of two periodic hard tasks (with periods = +deadlines) and one CBS for the handling of aperiodic jobs A, B and C. + +.. image:: example-3.svg + :align: center + :width: 80% + + +the main difference between these alternatives is that, due to the fact +that the workqueue thread only executes when no hard task is active, it +is subject to a considerable amount of delay for the soft tasks. In fact, +job A is still attempting to finish when job B becomes active. The CBS, +however, offers a deadline of its own to the EDF scheduler and thus manages +to finish job A sooner (also causing less context switches). A downside +is that the hard tasks T1 an T2 become prone to more latency, but they +still never miss a deadline. + +Case 2: Soft tasks +================== + +This use case is somewhat similar to what the Linux kernel does in its +SCHED_DEADLINE algorithm. Here, each application task, periodic or not, +has one exclusive CBS to handle their jobs. This solution is optimal in +the sense that, while the system behaves just like the regular EDF when +all execution times are equal to or lower than the respective budget, +any overrun is guaranteed not to influence the rest of the system. Thus, +in this use case the CBS acts as a *fail-safe* for EDF overruns. + +API Usage +********* + +The CBS API consists of basically two elements, a :c:macro:`K_CBS_DEFINE` +to statically define a CBS and a :c:func:`k_cbs_push_job` function to insert +jobs on its execution queue. To define the CBS, we need to specify its +name, budget, period and the static priority it will have. The latter is +needed because the EDF scheduler is a tie-breaker for competing threads of +the same static priority (hence, for "pure" EDF results, all application +threads should have the same static value). In the following code snippet, +the CBS is defined with a budget of 50 milliseconds and a period of 200 +milliseconds, which yields a bandwidth of 0.25 (up to 25% of the CPU usage): + +.. code-block:: c + + #define BUDGET K_MSEC(50) + #define PERIOD K_MSEC(200) + #define STATIC_PRIORITY 5 + + K_CBS_DEFINE(my_cbs, BUDGET, PERIOD, STATIC_PRIORITY); + +now, all that's left to do is insert a job into the CBS queue, which will +then be served in a FIFO manner. A job is a regular C function that expects +a void pointer as an argument. Similar to the :ref:`Message Queues`, +the :c:func:`k_cbs_push_job` function also accepts a timeout value to wait +for an open slot if the CBS queue happens to be full. + +.. note:: + one can call :c:func:`k_cbs_push_job` from an ISR or a thread context, + but ISRs should **not** attempt to wait trying to push a job on the CBS + queue. + +.. code-block:: c + + void some_job(void *arg){ + printf("I am job '%c'.\n", (char) arg); + } + + int main(){ + ... + k_cbs_push_job(my_cbs, some_job, 'A', K_NO_WAIT); + ... + } + +In this example, the CBS will execute ``some_job`` and suspend afterwards, +remaining dormant and away from the CPU for as long as no other jobs are +pushed to it. In the event of a job having an infinite loop inside it, say: + +.. code-block:: c + + void some_job(void *arg){ + for(;;){ + printf("Hello, world!\n"); + k_sleep(K_SECONDS(1)); + } + } + +The CBS will be active and measuring the budget consumption *whenever the job +is actively running* - that is, whevener it's not suspended as well. When a +job calls :c:func:`k_sleep`, it suspends the CBS with it. If one desires +that a *busy wait* happens instead, :c:func:`k_busy_wait` should be issued. + +.. note:: + At implementation level, the CBS really is just a kernel-level :ref:`thread` + packed with a :ref:`timer` and a :ref:`message queue` + to deliver the functionality described in the previous sections. + Thus, it makes no sense to "push a thread" to a CBS, just regular + functions as shown above. + + One can see a CBS as an EDF thread that automatically recalculates + its deadline, thus not needing to worry about doing it manually. Users + that desire to adopt this approach can push the thread entry point as + a job, like so: + + .. code-block:: c + + void my_thread_entry_point(void *arg){ + ... + /* your thread code goes here */ + ... + } + + /* + you can consider the budget equal + to the expected execution time, + and the period equal to the + relative deadline of the thread. + */ + + K_CBS_DEFINE(my_cbs, budget, period, static_priority); + + int main(){ + ... + k_cbs_push_job(my_cbs, my_thread_entry_point, (void *) &args, K_NO_WAIT); + ... + } + + With the only adaptation needed being to concentrate the thread + arguments into one single argument (which could be easily done + with a struct). + + +Logging CBS events +================== + +The CBS API provides a logging feature of the main events regarding the +server execution. Those can be viewed on the chosen console by enabling +:kconfig:option:`CONFIG_CBS_LOG` on the ``prj.conf`` file or through +``menuconfig``. The following events are supported and, although registered +synchronously as they happen, will be effectivelly logged on-screen by a +low-priority background task by default, in a strategy to keep the overhead +as minimal as possible. + +.. list-table:: Constant Bandwidth Server (CBS) events + :widths: 10 90 + :header-rows: 1 + + * - Event + - Description + * - J_PUSH + - a job has been pushed to the CBS job queue. + * - J_COMP + - a job has been completed. + * - B_COND + - the kernel verified that the available (budget, deadline) pair yielded + a higher bandwidth than what's configured for that CBS. This condition + is only checked right after J_PUSH happens. When true, the deadline is + recalculated as (t + period) and the budget is replenished. + * - B_ROUT + - the budget ran out mid-execution and was replenished. The deadline is + postponed in (period) units (i.e. deadline += period). + * - SWT_TO + - the CBS thread entered the CPU to start or resume the execution of a job. + * - SWT_AY + - the CBS thread left the CPU due to preemption or completing a job execution. + +The example below shows an example output when :kconfig:option:`CONFIG_CBS_LOG` is +enabled. The value alongside the event log is the budget level, in hardware cycles. +The CBS thread does an underlying conversion from timeout units passed on +:c:macro:`K_CBS_DEFINE` (e.g. :c:macro:`K_USEC`) to ensure units compatibility +with :c:func:`k_thread_deadline_set()`, which currently accepts only hardware cycles. + +.. code-block:: console + + [00:00:12.028,000] CBS: cbs_1 J_PUSH 43543 // first job is pushed + [00:00:12.028,000] CBS: cbs_1 B_COND 100000 // condition met, budget replenished + [00:00:12.028,000] CBS: cbs_1 J_PUSH 100000 // one more job pushed + [00:00:12.028,000] CBS: cbs_1 SWT_TO 100000 // CBS thread enters CPU to execute + [00:00:12.031,000] CBS: cbs_1 J_COMP 68954 // first job completed + [00:00:12.034,000] CBS: cbs_1 J_COMP 38914 // second job completed + [00:00:12.034,000] CBS: cbs_1 SWT_AY 38914 // CBS thread leaves the CPU + + +Configuration Options +********************** + +Related configuration options: + +* :kconfig:option:`CONFIG_CBS` +* :kconfig:option:`CONFIG_CBS_LOG` +* :kconfig:option:`CONFIG_CBS_THREAD_STACK_SIZE` +* :kconfig:option:`CONFIG_CBS_QUEUE_LENGTH` +* :kconfig:option:`CONFIG_CBS_INITIAL_DELAY` +* :kconfig:option:`CONFIG_CBS_THREAD_MAX_NAME_LEN` + +API Reference +************** + +.. doxygengroup:: cbs_apis diff --git a/doc/kernel/services/sched_server/example-1.svg b/doc/kernel/services/sched_server/example-1.svg new file mode 100644 index 0000000000000..c2e622c2a4e3a --- /dev/null +++ b/doc/kernel/services/sched_server/example-1.svg @@ -0,0 +1,3 @@ + + +
CBS
(3,7)
CBS...
1
1
2
2
3
3
A
A
4
4
5
5
7
7
8
8
6
6
9
9
10
10
12
12
13
13
11
11
14
14
15
15
B_ROUT
B_ROUT
B
B
B_COND
B_COND
t
t
budget
level
budget...
A, B
A, B
3
3
0
0
1
1
1
1
16
16
17
17
18
18
\ No newline at end of file diff --git a/doc/kernel/services/sched_server/example-2.svg b/doc/kernel/services/sched_server/example-2.svg new file mode 100644 index 0000000000000..32ef06b6a1b02 --- /dev/null +++ b/doc/kernel/services/sched_server/example-2.svg @@ -0,0 +1,3 @@ + + +
CBS
(3,7)
CBS...
1
1
2
2
3
3
A
A
4
4
5
5
7
7
8
8
6
6
9
9
10
10
12
12
13
13
11
11
14
14
15
15
B_ROUT
B_ROUT
B
B
B_COND
B_COND
t
t
budget
level
budget...
A, B
A, B
3
3
0
0
1
1
1
1
C
C
D
D
D
D
B_COND
B_COND
2.7
2.7
C
C
16
16
0
0
2
2
B_ROUT
B_ROUT
17
17
18
18
\ No newline at end of file diff --git a/doc/kernel/services/sched_server/example-3.svg b/doc/kernel/services/sched_server/example-3.svg new file mode 100644 index 0000000000000..80f484844d8aa --- /dev/null +++ b/doc/kernel/services/sched_server/example-3.svg @@ -0,0 +1,3 @@ + + +
2
2
A
A
0
0
1
1
2
2
3
3
A
A
1
1
4
4
5
5
7
7
8
8
6
6
9
9
10
10
12
12
13
13
11
11
14
14
15
15
16
16
18
18
19
19
17
17
20
20
21
21
3
3
B
B
CBS
(2,6)
CBS...
T1
(2,6)
T1...
0
0
1
1
T2
(3,9)
T2...
C
C
2
2
22
22
23
23
24
24
2
2
0
0
A
A
1
1
3
3
B
B
T1
(2,6)
T1...
0
0
1
1
T2
(3,9)
T2...
C
C
2
2
A
A
B
B
C
C
B
B
C
C
Work
queue
Work...
A
A
A
A
1
1
2
2
3
3
4
4
5
5
7
7
8
8
6
6
9
9
10
10
12
12
13
13
11
11
14
14
15
15
16
16
18
18
19
19
17
17
20
20
21
21
22
22
23
23
24
24
B_ROUT
B_ROUT
B_COND
B_COND
B_ROUT
B_ROUT
B_COND
B_COND
\ No newline at end of file diff --git a/doc/zephyr.doxyfile.in b/doc/zephyr.doxyfile.in index 036bef82c6624..d1e7cd22082c6 100644 --- a/doc/zephyr.doxyfile.in +++ b/doc/zephyr.doxyfile.in @@ -2493,7 +2493,7 @@ PREDEFINED = __DOXYGEN__ \ CONFIG_NET_UDP \ CONFIG_SCHED_CPU_MASK \ CONFIG_SCHED_DEADLINE \ - CONFIG_SCHED_DEADLINE \ + CONFIG_CBS \ CONFIG_SETTINGS_RUNTIME \ CONFIG_SMP \ CONFIG_SYS_CLOCK_EXISTS \ From 533d43ada15d7df465b1de03805ef91817422733 Mon Sep 17 00:00:00 2001 From: Alexander pinheiro paschoaletto <1222703@isep.ipp.pt> Date: Wed, 8 Jan 2025 19:30:45 +0000 Subject: [PATCH 3/3] samples: CBS: adding code samples for the Constant Bandwidth Server (CBS) This commit adds two sample codes for the the CBS, one for testing a very simple logging application and other for effectively implementing it alongside other hard real-time threads. Signed-off-by: Alexander pinheiro paschoaletto <1222703@isep.ipp.pt> --- samples/cbs/cbs.rst | 10 ++ samples/cbs/edf/CMakeLists.txt | 8 + samples/cbs/edf/README.rst | 179 +++++++++++++++++++++ samples/cbs/edf/doc/example-1-cbs.svg | 3 + samples/cbs/edf/doc/example-1.svg | 3 + samples/cbs/edf/prj.conf | 10 ++ samples/cbs/edf/sample.yaml | 18 +++ samples/cbs/edf/src/lib/helper.h | 78 +++++++++ samples/cbs/edf/src/lib/tracer.h | 89 +++++++++++ samples/cbs/edf/src/main.c | 221 ++++++++++++++++++++++++++ samples/cbs/simple/CMakeLists.txt | 8 + samples/cbs/simple/README.rst | 168 ++++++++++++++++++++ samples/cbs/simple/prj.conf | 11 ++ samples/cbs/simple/sample.yaml | 15 ++ samples/cbs/simple/src/lib/helper.h | 38 +++++ samples/cbs/simple/src/main.c | 98 ++++++++++++ 16 files changed, 957 insertions(+) create mode 100644 samples/cbs/cbs.rst create mode 100644 samples/cbs/edf/CMakeLists.txt create mode 100644 samples/cbs/edf/README.rst create mode 100644 samples/cbs/edf/doc/example-1-cbs.svg create mode 100644 samples/cbs/edf/doc/example-1.svg create mode 100644 samples/cbs/edf/prj.conf create mode 100644 samples/cbs/edf/sample.yaml create mode 100644 samples/cbs/edf/src/lib/helper.h create mode 100644 samples/cbs/edf/src/lib/tracer.h create mode 100644 samples/cbs/edf/src/main.c create mode 100644 samples/cbs/simple/CMakeLists.txt create mode 100644 samples/cbs/simple/README.rst create mode 100644 samples/cbs/simple/prj.conf create mode 100644 samples/cbs/simple/sample.yaml create mode 100644 samples/cbs/simple/src/lib/helper.h create mode 100644 samples/cbs/simple/src/main.c diff --git a/samples/cbs/cbs.rst b/samples/cbs/cbs.rst new file mode 100644 index 0000000000000..bcf20b7ff4d38 --- /dev/null +++ b/samples/cbs/cbs.rst @@ -0,0 +1,10 @@ +.. zephyr:code-sample-category:: cbs_samples + :name: Constant Bandwidth Server (CBS) + :show-listing: + + Samples that demonstrate the usage of the Constant Bandwidth Server (CBS). + The CBS is an extension of the Earliest Deadline First (EDF) scheduler + that tasks that automatically recalculates their deadlines when they + exceed their configured timing constraints. If a task takes longer than + expected, it can be preempted by awaiting tasks instead of jeopardizing + their deadlines. diff --git a/samples/cbs/edf/CMakeLists.txt b/samples/cbs/edf/CMakeLists.txt new file mode 100644 index 0000000000000..51ede44f269d9 --- /dev/null +++ b/samples/cbs/edf/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(cbs_sample) + +target_sources(app PRIVATE src/main.c) diff --git a/samples/cbs/edf/README.rst b/samples/cbs/edf/README.rst new file mode 100644 index 0000000000000..d477fc04f2e67 --- /dev/null +++ b/samples/cbs/edf/README.rst @@ -0,0 +1,179 @@ +.. zephyr:code-sample:: cbs_edf + :name: CBS + EDF + + A sample to understand how the CBS works alongside regular EDF threads. + + +Overview +******** + +A sample that creates a Constant Bandwidth Server (CBS) to run +aperiodic jobs within a periodic EDF taskset. The user can select +between two examples: + +* 1: one CBS with (budget, period) = (3, 8) time units and one + periodic EDF task with (execution_time, period) = (4, 7) + time units. The EDF task period is equal to the deadline. + +* 2: one CBS with (budget, period) = (2, 6) time units and two + periodic EDF tasks with (execution_time, relative_deadline) = + (2, 6) and (3, 9) time units, respectively. The EDF tasks have + periods equal to deadlines. + + +the time units have millisecond resolution, and are defined by U: + +.. code-block:: c + + /* all time values are multiplied by U (ms) */ + #define U 1000 + +This is the recommended setup when the application has both tasks +that have hard deadlines (directly scheduled by EDF) and soft +deadlines (wrapped by a CBS). The CBS in this case guarantees that +any overruns caused by the soft tasks will not jeopardize the +execution environment of the hard tasks. It can be seen as the +EDF equivalent of a :ref:`workqueue thread`, and will +deliver better overall performance than a workqueue. + +.. note:: + If the system prioritizes schedulability and no task has hard + deadlines, then all tasks can be wrapped by their own personal + CBS. This is specially useful if the execution times of the tasks + are unknown, as the CBS budget can be used instead. If the budget + matches the task worst-case execution time (WCET), it is guaranteed + that the system will behave just like a regular EDF. + + If this is the case, all the application needs to do is guarantee + that the sum of all CBS's (budget / period) ratio is lower than 1. + + +Building and Running +******************** + +This application can be built and executed on QEMU as follows: + +.. zephyr-app-commands:: + :zephyr-app: samples/cbs/edf + :host-os: unix + :board: qemu_riscv32 + :goals: run + :compact: + +To build and run on a physical target (i.e. XIAO ESP32-C3) instead, +run the following: + +.. zephyr-app-commands:: + :zephyr-app: samples/cbs/edf + :board: xiao_esp32c3 + :goals: build flash + :compact: + +Sample Output +============= + +Each task, in its cycle, will print its own ID at every tenth of its +execution time, with square brackets at the edges to signal the start +and end of the cycle. For a task of ID = '1', This is the output: + +.. code-block:: console + + [1-1-1-1-1-1-1-1-1-1] + +EDF tasks have a numeric ID (1, 2, ...), while the CBS jobs have an +alphabetic ID (A, B, ...). After a few seconds, the system will dump +a tracing of the events in the following format: + +.. code-block:: console + + ======================== + EDF events: + 1034 [ 1 ] TRIG 0 + 1036 [ 1 ] START 0 + 5093 [ 1 ] END 0 + ======================== + +What it means is that EDF thread 1 was triggered at system tick 1034, +started executing in tick 1036 and finished running in tick 5093. Thus, +for the example above, thread 1 takes roughly 4057 ticks to execute. +In a diagram, that would mean: + +.. image:: doc/example-1.svg + :align: center + :width: 80% + +In the given example, thread 1 is periodic and has period = deadline = 7000. +So if activating at instant 1034, thread 1 will be set an absolute deadline +of 8034, which is also its next triggering event. In other words, the next +sequence of events would be likely the following: + +.. code-block:: console + + [1-1-1-1-1-1-1-1-1-1]-[1-1-1-1-1-1-1-1-1-1] + + ======================== + EDF events: + 1034 [ 1 ] TRIG 0 + 1036 [ 1 ] START 0 + 5093 [ 1 ] END 0 + ... + 8034 [ 1 ] TRIG 1 + 8035 [ 1 ] START 1 + 12032 [ 1 ] END 1 + ... + ======================== + +Now, if we add up a CBS with (budget, period) of (3000, 8000), we might see a +somewhat different execution outcome. If the CBS receives a job A at instant +4035 with an expected execution time of 4000 ticks, this is what happens: + +.. code-block:: console + + [1-1-1-1-1-1-1-1-1-1-1]-[A-A-A-A-A-A-A-A-[1-1-1-1-1-1-1-1-1-1-1]-A-A-A] + + ======================== + EDF events: + 1034 [ 1 ] TRIG 0 + 1036 [ 1 ] START 0 + 4035 [ A ] TRIG 0 + 5093 [ 1 ] END 0 + 5093 [ A ] START 0 + 8034 [ 1 ] TRIG 1 + 8095 [ 1 ] START 1 + 12182 [ 1 ] END 1 + 13002 [ A ] END 0 + ======================== + +* As job A is triggered at instant 4035 and the CBS was idle, its absolute + deadline is calculated as (4035 + CBS period) = 12035. However, at this very + event thread 1 was mid-execution and its deadline, as said before, was + set at 8034. Therefore, job A is triggered but not yet executed. It only + starts to run at instant 5093, right after thread 1 finished its cycle. + +* At instant 8034, thread A is triggered for its next cycle. Its deadline + is set at (8034 + 7000) = 15034. However, the CBS is still running job A + and its deadline is 12035, so thread 1 cannot start just yet. + +* At instant 8093, 3000 ticks after job A started, the CBS budget runs out. + In other words, it executed for its allowed time slice. The kernel then + intervenes, replenishing the CBS budget at the cost of postponing its + deadline. The new CBS absolute deadline is then 8093 + 8000 = 16093. + +* Thread 1 now has the earliest absolute deadline (15034), so it *preempts* + the CBS (and by extension, job A) and starts at tick 8095. That's why we + see a [1-1-1] nested within the [A-A-A] logs above. + +* Thread 1 finishes at tick 12182. The CBS is now allowed to resume. It + finishes job A soon after, at tick 13002. + +In a diagram, the events listed above translate to the following: + +.. image:: doc/example-1-cbs.svg + :align: center + :width: 80% + +Note that the CBS schedulability and deadlines only depend of the configured +values of budget and period. It doesn't depend on how many jobs are received, +nor it takes into account the expected duration of these jobs. The CBS thus +wraps the jobs within a know scheduling context which ensures that the remaining +application threads will never miss a deadline. diff --git a/samples/cbs/edf/doc/example-1-cbs.svg b/samples/cbs/edf/doc/example-1-cbs.svg new file mode 100644 index 0000000000000..8f179c5669cb1 --- /dev/null +++ b/samples/cbs/edf/doc/example-1-cbs.svg @@ -0,0 +1,3 @@ + + +
8093
8093
A
A
0
0
A
A
1
1
B_ROUT
B_ROUT
B_COND
B_COND
CBS
(3000, 8000)
CBS...
Thread 1
(4000, 7000)
Thread 1...
budget
level
budget...
1034
1034
8034
8034
1036
1036
5093
5093
1034
1034
5093
5093
8095
8095
12182
12182
13002
13002
\ No newline at end of file diff --git a/samples/cbs/edf/doc/example-1.svg b/samples/cbs/edf/doc/example-1.svg new file mode 100644 index 0000000000000..eb457dd545dcc --- /dev/null +++ b/samples/cbs/edf/doc/example-1.svg @@ -0,0 +1,3 @@ + + +
T1
T1
1
1
t
t
1034
1034
1036
1036
5093
5093
\ No newline at end of file diff --git a/samples/cbs/edf/prj.conf b/samples/cbs/edf/prj.conf new file mode 100644 index 0000000000000..a23af76e69919 --- /dev/null +++ b/samples/cbs/edf/prj.conf @@ -0,0 +1,10 @@ +CONFIG_SYS_CLOCK_TICKS_PER_SEC=10000 +CONFIG_HEAP_MEM_POOL_SIZE=256 +CONFIG_CBS=y + +############################################################################### +### Uncomment the following if using QEMU targets. ############################ +############################################################################### + +CONFIG_QEMU_ICOUNT=n +CONFIG_STDOUT_CONSOLE=y diff --git a/samples/cbs/edf/sample.yaml b/samples/cbs/edf/sample.yaml new file mode 100644 index 0000000000000..96341d36266e8 --- /dev/null +++ b/samples/cbs/edf/sample.yaml @@ -0,0 +1,18 @@ +sample: + description: An Earliest Deadline First with Constant Bandwidth Server CBS sample + name: EDF with CBS sample +common: + tags: + - EDF + - CBS +tests: + sample.cbs.edf: + build_only: true + platform_allow: + - qemu_riscv32 + - qemu_cortex_a53 + integration_platforms: + - qemu_riscv32 + tags: + - cbs + - edf diff --git a/samples/cbs/edf/src/lib/helper.h b/samples/cbs/edf/src/lib/helper.h new file mode 100644 index 0000000000000..a0a7dd4aac260 --- /dev/null +++ b/samples/cbs/edf/src/lib/helper.h @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2024 Instituto Superior de Engenharia do Porto (ISEP). + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef _APP_HELPER_H_ +#define _APP_HELPER_H_ + +/* + * Remember that for "pure" EDF results, all + * user threads must have the same static priority. + * That happens because EDF will be a tie-breaker + * among two or more ready tasks of the same static + * priority. An arbitrary positive number is chosen here. + */ +#define EDF_PRIORITY 5 +#define INACTIVE -1 +#define MSEC_TO_CYC(msec) k_ms_to_cyc_near32(msec) +#define MSEC_TO_USEC(msec) (msec * USEC_PER_MSEC) + +typedef struct { + char id; + int counter; + int32_t initial_delay_msec; + int32_t rel_deadline_msec; + int32_t period_msec; + uint32_t wcet_msec; + k_tid_t thread; + struct k_msgq queue; + struct k_timer timer; +} edf_t; + +typedef struct { + char id; + int counter; + uint32_t wcet_msec; +} job_t; + +job_t *create_job(char id, int counter, uint32_t wcet_msec) +{ + job_t *job = (job_t *)k_malloc(sizeof(job_t)); + + job->id = id; + job->counter = counter; + job->wcet_msec = wcet_msec; + return job; +} + +void destroy_job(job_t *job) +{ + k_free(job); +} + +void report_cbs_settings(void) +{ + printf("\n/////////////////////////////////////////////////////////////////////////////////" + "/////\n"); + printf("\nBoard:\t\t%s\n", CONFIG_BOARD_TARGET); +#ifdef CONFIG_CBS + printf("[CBS]\t\tCBS enabled.\n"); +#ifdef CONFIG_CBS_LOG + printf("[CBS]\t\tCBS events logging: enabled.\n"); +#else + printf("[CBS]\t\tCBS events logging: disabled.\n"); +#endif +#ifdef CONFIG_TIMEOUT_64BIT + printf("[CBS]\t\tSYS 64-bit timeouts: supported.\n"); +#else + printf("[CBS]\t\tSYS 64-bit timeouts: not supported. using 32-bit API instead.\n"); +#endif +#else + printf("[CBS]\t\tCBS disabled.\n"); +#endif + printf("\n/////////////////////////////////////////////////////////////////////////////////" + "/////\n\n"); +} + +#endif diff --git a/samples/cbs/edf/src/lib/tracer.h b/samples/cbs/edf/src/lib/tracer.h new file mode 100644 index 0000000000000..870ae2edd1e2d --- /dev/null +++ b/samples/cbs/edf/src/lib/tracer.h @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2024 Instituto Superior de Engenharia do Porto (ISEP). + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef _APP_TRACER_H_ +#define _APP_TRACER_H_ + +#include + +#define TRIGGER 0 +#define START 1 +#define END 2 +#define BUF_SIZE 100 + +/* + * This struct will hold execution + * metadata for the threads + */ +typedef struct { + uint32_t timestamp; + char thread_id; + int counter; + int event; +} trace_t; + +trace_t events[BUF_SIZE]; +int event_count; +uint32_t offset; + +void toString(int evt, char *target) +{ + switch (evt) { + case TRIGGER: + strcpy(target, "TRIG "); + break; + case START: + strcpy(target, "START"); + break; + case END: + strcpy(target, "END "); + break; + default: + strcpy(target, "-----"); + } +} + +void begin_trace(void) +{ + offset = k_uptime_get_32(); + event_count = 0; +} + +void reset_trace(void) +{ + event_count = 0; +} + +void print_trace(struct k_timer *timer) +{ + (void) timer; + char event[10]; + + printf("\n========================\nEDF events:\n"); + + for (int i = 0; i < event_count; i++) { + uint32_t timestamp = events[i].timestamp - offset; + + toString(events[i].event, event); + printf("%u \t[ %c ] %s %d\n", timestamp, events[i].thread_id, event, + events[i].counter); + } + printf("========================\n"); + reset_trace(); +} + +void trace(char thread_id, int thread_counter, int event) +{ + if (event_count >= BUF_SIZE) { + return; + } + events[event_count].timestamp = k_uptime_get_32(); + events[event_count].thread_id = thread_id; + events[event_count].counter = thread_counter; + events[event_count].event = event; + event_count++; +} + +#endif diff --git a/samples/cbs/edf/src/main.c b/samples/cbs/edf/src/main.c new file mode 100644 index 0000000000000..7af8df67d2c24 --- /dev/null +++ b/samples/cbs/edf/src/main.c @@ -0,0 +1,221 @@ +/* + * Copyright (c) 2024 Instituto Superior de Engenharia do Porto (ISEP). + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include +#include +#include +#include "lib/helper.h" +#include "lib/tracer.h" + +/* + * set EXAMPLE to 1 for a simple example of 1 EDF thread and 1 CBS. + * set EXAMPLE to 2 for a more complex example of 2 EDF threads and 1 CBS. + */ +#define EXAMPLE 1 + +/* + * the tasks and CBS will have their execution times, period and + * relative deadlines all multiplied by U, in milliseconds. + */ +#define U 1000 + +void trigger(struct k_timer *timer) +{ + /* + * This function triggers a cycle for + * the periodic EDF thread. + * + * the message itself is not relevant - + * the *act* of sending the message is. + * That's what unblocks the thread to + * execute a cycle. + */ + const char t = 1; + edf_t *task = (edf_t *)k_timer_user_data_get(timer); + int deadline = MSEC_TO_CYC(task->rel_deadline_msec); + + k_msgq_put(&task->queue, &t, K_NO_WAIT); + k_thread_deadline_set(task->thread, deadline); + k_reschedule(); + trace(task->id, task->counter, TRIGGER); +} + +void cycle(char id, uint32_t wcet) +{ + printf("[%c-", id); + k_busy_wait(wcet / 10); + for (int i = 0; i < 8; i++) { + printf("%c-", id); + k_busy_wait(wcet / 10); + } + printf("%c]-", id); + k_busy_wait(wcet / 10); +} + +void job_function(void *job_params) +{ + job_t *job = (job_t *)job_params; + + trace(job->id, job->counter, START); + uint32_t wcet = MSEC_TO_USEC(job->wcet_msec); + + cycle(job->id, wcet); + trace(job->id, job->counter, END); +} + +void thread_function(void *task_props, void *a2, void *a3) +{ + char buffer[10]; + char message; + edf_t *task = (edf_t *)task_props; + + task->thread = k_current_get(); + + /* value passed to k_busy_wait needs to be in usec */ + uint32_t wcet = MSEC_TO_USEC(task->wcet_msec); + + k_msgq_init(&task->queue, buffer, sizeof(char), 10); + k_timer_init(&task->timer, trigger, NULL); + k_timer_user_data_set(&task->timer, (void *)task); + k_timer_start(&task->timer, K_MSEC(task->initial_delay_msec), K_MSEC(task->period_msec)); + + /* allow other threads to start up */ + k_sleep(K_USEC(500)); + + for (;;) { + k_msgq_get(&task->queue, &message, K_FOREVER); + trace(task->id, task->counter, START); + cycle(task->id, wcet); + trace(task->id, task->counter, END); + task->counter++; + } +} + +/***********************************************************************************************/ +#if EXAMPLE == 1 /* this is the taskset if you choose example 1. */ + +#define CBS_BUDGET K_MSEC(3 * U) +#define CBS_PERIOD K_MSEC(8 * U) + +edf_t tasks[] = {{.id = '1', + .counter = 0, + .initial_delay_msec = 0, + .rel_deadline_msec = 7 * U, + .period_msec = 7 * U, + .wcet_msec = 4 * U}}; + +K_THREAD_DEFINE(task1, 2048, thread_function, &tasks[0], NULL, NULL, EDF_PRIORITY, 0, INACTIVE); + +/***********************************************************************************************/ +#elif EXAMPLE == 2 /* this is the taskset if you choose example 2. */ + +#define CBS_BUDGET K_MSEC(2 * U) +#define CBS_PERIOD K_MSEC(6 * U) + +edf_t tasks[] = {{.id = '1', + .counter = 0, + .initial_delay_msec = 0, + .rel_deadline_msec = 6 * U, + .period_msec = 6 * U, + .wcet_msec = 2 * U}, + {.id = '2', + .counter = 0, + .initial_delay_msec = 0, + .rel_deadline_msec = 9 * U, + .period_msec = 9 * U, + .wcet_msec = 3 * U}}; + +K_THREAD_DEFINE(task1, 2048, thread_function, &tasks[0], NULL, NULL, EDF_PRIORITY, 0, INACTIVE); +K_THREAD_DEFINE(task2, 2048, thread_function, &tasks[1], NULL, NULL, EDF_PRIORITY, 0, INACTIVE); + +#endif +/***********************************************************************************************/ + +K_CBS_DEFINE(cbs_1, CBS_BUDGET, CBS_PERIOD, EDF_PRIORITY); +K_TIMER_DEFINE(trace_timer, print_trace, NULL); + +int main(void) +{ + k_sleep(K_SECONDS(1)); + k_timer_start(&trace_timer, K_MSEC(20 * U), K_MSEC(20 * U)); + + report_cbs_settings(); + begin_trace(); + +#if EXAMPLE == 1 + k_thread_start(task1); + /* + * The CBS is most useful when a given code + * needs to execute at arbitrary intervals + * alongside a hard and periodic taskset. + * To demonstrate that, in example 1, we: + * + * - wait 3 seconds; + * - push a 4-second job; + * - wait 10 seconds; + * - push a 3-second job. + * + * So neither the execution time, nor the + * arrival instant, nor the amount of jobs + * are fixed to the eyes of the scheduler. + * Thus, if we were to deal with this job + * directly with EDF, it would be hard + * to ensure a predictable execution. + */ + job_t *job1 = create_job('A', 0, 4 * U); + job_t *job2 = create_job('B', 1, 3 * U); + + k_sleep(K_MSEC(3 * U)); + k_cbs_push_job(&cbs_1, job_function, job1, K_FOREVER); + trace(job1->id, job1->counter, TRIGGER); + + k_sleep(K_MSEC(10 * U)); + k_cbs_push_job(&cbs_1, job_function, job2, K_FOREVER); + trace(job2->id, job2->counter, TRIGGER); + + destroy_job(job1); + destroy_job(job2); + +#elif EXAMPLE == 2 + k_thread_start(task1); + k_thread_start(task2); + /* + * In example 2 we push the same job, but + * at a slightly different timing: + * + * - wait 2 seconds; + * - push a 3-second job; + * - wait 10 seconds; + * - push a 3-second job; + * - wait 8 seconds; + * - push a 1-second job. + * + * The values for both examples were + * tailored to showcase the CBS behavior + * under different circumstances. + */ + job_t *job1 = create_job('A', 0, 3 * U); + job_t *job2 = create_job('B', 1, 3 * U); + job_t *job3 = create_job('C', 2, 1 * U); + + k_sleep(K_MSEC(2 * U)); + k_cbs_push_job(&cbs_1, job_function, job1, K_FOREVER); + trace(job1->id, job1->counter, TRIGGER); + + k_sleep(K_MSEC(10 * U)); + k_cbs_push_job(&cbs_1, job_function, job2, K_FOREVER); + trace(job2->id, job2->counter, TRIGGER); + + k_sleep(K_MSEC(8 * U)); + k_cbs_push_job(&cbs_1, job_function, job3, K_FOREVER); + trace(job3->id, job3->counter, TRIGGER); + + destroy_job(job1); + destroy_job(job2); + destroy_job(job3); +#endif + + return 0; +} diff --git a/samples/cbs/simple/CMakeLists.txt b/samples/cbs/simple/CMakeLists.txt new file mode 100644 index 0000000000000..51ede44f269d9 --- /dev/null +++ b/samples/cbs/simple/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(cbs_sample) + +target_sources(app PRIVATE src/main.c) diff --git a/samples/cbs/simple/README.rst b/samples/cbs/simple/README.rst new file mode 100644 index 0000000000000..05a91055537a2 --- /dev/null +++ b/samples/cbs/simple/README.rst @@ -0,0 +1,168 @@ +.. zephyr:code-sample:: cbs_simple + :name: CBS simple + + A sample to understand the CBS lifecycle and main events. + +Overview +******** + +A sample that creates one Constant Bandwidth Server (CBS) and pushes +a simple job to the server queue three times every two seconds. In +this example, the job simply prints some information in the console +and increments a counter. The main thread regularly pushes some jobs +to the CBS queue and sleeps for 2 seconds, starting over afterwards. + +Event logging +============= + +The logging feature is enabled by default, which allows users to see +the key events of a CBS in the console as they happen. The events are: + +.. list-table:: Constant Bandwidth Server (CBS) events + :widths: 10 90 + :header-rows: 1 + + * - Event + - Description + * - J_PUSH + - a job has been pushed to the CBS job queue. + * - J_COMP + - a job has been completed. + * - B_COND + - the kernel verified that the available (budget, deadline) pair yielded + a higher bandwidth than what's configured for that CBS. This condition + is only checked right after J_PUSH happens. When true, the deadline is + recalculated as (t + period) and the budget is replenished. + * - B_ROUT + - the budget ran out mid-execution and was replenished. The deadline is + postponed in (period) units (i.e. deadline += period). + * - SWT_TO + - the CBS thread entered the CPU to start or resume the execution of a job. + * - SWT_AY + - the CBS thread left the CPU due to preemption or completing a job execution. + +The server deadline recalculates whenever the budget is restored - that is, when +either the condition is met (B_COND) or it simply runs out (B_ROUT). In the first +case, the deadline will be set as job arrival instant + CBS period, whereas in the +latter it will be simply postponed by the CBS period. + +Building and Running +******************** + +This application can be built and executed on QEMU as follows: + +.. zephyr-app-commands:: + :zephyr-app: samples/cbs/simple + :host-os: unix + :board: qemu_riscv32 + :goals: run + :compact: + +To build and run on a physical target (i.e. XIAO ESP32-C3) instead, +run the following: + +.. zephyr-app-commands:: + :zephyr-app: samples/cbs/simple + :board: xiao_esp32c3 + :goals: build flash + :compact: + +Sample Output +============= + +The sample is built with the CBS configured with a budget of +80 milliseconds and a period of 800 milliseconds, as follows: + +.. code-block:: c + + /* remember: (name, budget, period, static_priority) */ + K_CBS_DEFINE(cbs_1, K_MSEC(80), K_MSEC(800), EDF_PRIORITY); + +This yields an utilization factor (bandwidth) of 80/800 = 10% +of the CPU at most, leaving the remaining 90% for whatever other +threads and CBSs the user may want to implement. The main function +simply pushes the same job three times every 2 seconds, as follows: + +.. code-block:: c + + int main(void){ + for(;;){ + printf("\n"); + k_cbs_push_job(&cbs_1, job_function, job1, K_NO_WAIT); + k_cbs_push_job(&cbs_1, job_function, job1, K_NO_WAIT); + k_cbs_push_job(&cbs_1, job_function, job1, K_NO_WAIT); + k_sleep(K_SECONDS(2)); + } + } + +the job itself is nothing but a function that prints some +information and busy waits for 10 milliseconds: + +.. code-block:: c + + void job_function(void *arg){ + /* prints some info and increments the counter */ + printf("%s %s, %d\n\n", job->msg, CONFIG_BOARD_TARGET, job->counter); + job->counter++; + + /* busy waits for (approximately) 10 milliseconds */ + k_busy_wait(10000); + } + +So if each job takes slightly over 10 milliseconds and the +server has an 80-millisecond budget, it is expected that all +three jobs will be served without exhausting the budget. In +other words, this would be the logging result: + +.. code-block:: console + + [job] j1 on xiao_esp32c3/esp32c3, 0 + + [job] j1 on xiao_esp32c3/esp32c3, 1 + + [job] j1 on xiao_esp32c3/esp32c3, 2 + + [00:00:02.047,000] CBS: cbs_1 J_PUSH 1280000 + [00:00:02.047,000] CBS: cbs_1 B_COND 1280000 //CBS was idle. deadline = now + period + [00:00:02.047,000] CBS: cbs_1 J_PUSH 1280000 + [00:00:02.047,000] CBS: cbs_1 J_PUSH 1280000 + [00:00:02.047,000] CBS: cbs_1 SWT_TO 1280000 //CBS enters the CPU to execute the jobs + [00:00:02.060,000] CBS: cbs_1 J_COMP 1086468 + [00:00:02.072,000] CBS: cbs_1 J_COMP 891390 + [00:00:02.084,000] CBS: cbs_1 J_COMP 694073 + [00:00:02.084,000] CBS: cbs_1 SWT_AY 694073 //all jobs completed. CBS leaves the CPU + +.. note:: + The CBS thread does an underlying conversion from timeout + units passed on :c:macro:`K_CBS_DEFINE` (e.g. :c:macro:`K_MSEC`) + to ensure units compatibility with :c:func:`k_thread_deadline_set`, + which currently accepts only hardware cycles. That's why, + in this example, K_MSEC(80) translates to 1280000 hardware + cycles. Systems with different clock speeds will + showcase different values. + +Now what if we decrease the budget to, say, 30 milliseconds? +This would reduce the maximum CPU utilization to 30/800 = 37.5%, +but now the jobs will likely exhaust the budget and trigger a +deadline recalculation eventually. And it happens indeed: + +.. code-block:: console + + [job] j1 on xiao_esp32c3/esp32c3, 0 + + [job] j1 on xiao_esp32c3/esp32c3, 1 + + [job] j1 on xiao_esp32c3/esp32c3, 2 + + [00:00:02.053,000] CBS: cbs_1 J_PUSH 480000 + [00:00:02.053,000] CBS: cbs_1 B_COND 480000 //CBS was idle. deadline = now + period + [00:00:02.053,000] CBS: cbs_1 J_PUSH 480000 + [00:00:02.053,000] CBS: cbs_1 J_PUSH 480000 + [00:00:02.053,000] CBS: cbs_1 SWT_TO 480000 + [00:00:02.066,000] CBS: cbs_1 J_COMP 274543 + [00:00:02.078,000] CBS: cbs_1 J_COMP 81781 + [00:00:02.083,000] CBS: cbs_1 B_ROUT 480000 //CBS ran out. deadline += period + [00:00:02.083,000] CBS: cbs_1 SWT_AY 479556 //yields for possible other threads with earlier deadline + [00:00:02.083,000] CBS: cbs_1 SWT_TO 479556 //no other thread with earlier deadline, gets back to the CPU + [00:00:02.090,000] CBS: cbs_1 J_COMP 370333 + [00:00:02.090,000] CBS: cbs_1 SWT_AY 370333 diff --git a/samples/cbs/simple/prj.conf b/samples/cbs/simple/prj.conf new file mode 100644 index 0000000000000..0f3984ac829f7 --- /dev/null +++ b/samples/cbs/simple/prj.conf @@ -0,0 +1,11 @@ +CONFIG_SYS_CLOCK_TICKS_PER_SEC=10000 +CONFIG_HEAP_MEM_POOL_SIZE=256 +CONFIG_CBS=y +CONFIG_CBS_LOG=y + +############################################################################### +### Uncomment the following if using QEMU targets. ############################ +############################################################################### + +CONFIG_QEMU_ICOUNT=n +CONFIG_STDOUT_CONSOLE=y diff --git a/samples/cbs/simple/sample.yaml b/samples/cbs/simple/sample.yaml new file mode 100644 index 0000000000000..478a2bacb5653 --- /dev/null +++ b/samples/cbs/simple/sample.yaml @@ -0,0 +1,15 @@ +sample: + description: A basic Constant Bandwidth Server sample + name: basic CBS sample +common: + tags: + - CBS +tests: + sample.cbs.simple: + build_only: true + platform_allow: + - qemu_riscv32 + - qemu_cortex_a53 + integration_platforms: + - qemu_riscv32 + tags: cbs diff --git a/samples/cbs/simple/src/lib/helper.h b/samples/cbs/simple/src/lib/helper.h new file mode 100644 index 0000000000000..7a151fbced5b0 --- /dev/null +++ b/samples/cbs/simple/src/lib/helper.h @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024 Instituto Superior de Engenharia do Porto (ISEP). + * + * SPDX-License-Identifier: Apache-2.0 + */ +#ifndef _APP_HELPER_H_ +#define _APP_HELPER_H_ + +typedef struct { + char msg[50]; + uint32_t counter; +} job_t; + +void report_cbs_settings(void) +{ + printf("\n/////////////////////////////////////////////////////////////////////////////////" + "/////\n"); + printf("\nBoard:\t\t%s\n", CONFIG_BOARD_TARGET); +#ifdef CONFIG_CBS + printf("[CBS]\t\tCBS enabled.\n"); +#ifdef CONFIG_CBS_LOG + printf("[CBS]\t\tCBS events logging: enabled.\n"); +#else + printf("[CBS]\t\tCBS events logging: disabled.\n"); +#endif +#ifdef CONFIG_TIMEOUT_64BIT + printf("[CBS]\t\tSYS 64-bit timeouts: supported.\n"); +#else + printf("[CBS]\t\tSYS 64-bit timeouts: not supported. using 32-bit API instead.\n"); +#endif +#else + printf("[CBS]\t\tCBS disabled.\n"); +#endif + printf("\n/////////////////////////////////////////////////////////////////////////////////" + "/////\n\n"); +} + +#endif diff --git a/samples/cbs/simple/src/main.c b/samples/cbs/simple/src/main.c new file mode 100644 index 0000000000000..af67a1604afd0 --- /dev/null +++ b/samples/cbs/simple/src/main.c @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2024 Instituto Superior de Engenharia do Porto (ISEP). + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include "lib/helper.h" + +/* + * Remember that for "pure" EDF results, all user + * threads must have the same static priority. + * That happens because EDF will be a tie-breaker + * among two or more ready tasks of the same static + * priority. An arbitrary positive number is chosen here. + */ +#define EDF_PRIORITY 5 + +job_t *create_job(char *msg, int counter) +{ + job_t *job = (job_t *)k_malloc(sizeof(job_t)); + + strcpy((char *)job->msg, msg); + job->counter = counter; + return job; +} + +void destroy_job(job_t *job) +{ + k_free(job); +} + +/* + * The CBS job must return void and receive a void* + * argument as parameter. The actual argument type is + * up to you, to be unveiled and used within the + * function. + */ +void job_function(void *arg) +{ + job_t *job = (job_t *)arg; + + /* prints some info and increments the counter */ + printf("%s %s, %d\n\n", job->msg, CONFIG_BOARD_TARGET, job->counter); + job->counter++; + + /* busy waits for (approximately) 10 milliseconds */ + k_busy_wait(10000); +} + +/* + * In essence, the CBS is a thread that automatically + * recalculates its own deadline whenever it executes + * a job for longer than its allowed time slice (budget). + * These are the key points of how it works: + * + * 1. when a running job consumes its budget entirely, + * the kernel postpones the deadline (deadline += CBS period) + * and replenishes the budget. + * + * 2. when a job is pushed and the CBS is idle, the + * kernel checks if the current (budget, deadline) pair + * yields a higher bandwidth (CPU utilization) than + * configured. This is not allowed so, if true, the + * kernel also intervenes. In this case, the budget is + * replenished and the new deadline is (now + CBS period). + + * The job_function above prints stuff and busy waits + * for approximately 10 milliseconds, so you can see + * the CBS in action by tweaking the values below. 3 + * jobs are pushed at once and will execute sequentially, + * taking roughly 30 milliseconds to complete. So what + *happens when you drop the budget to lower than K_MSEC(30)? + */ + +/* remember: (name, budget, period, static_priority) */ +K_CBS_DEFINE(cbs_1, K_MSEC(80), K_MSEC(800), EDF_PRIORITY); + +int main(void) +{ + k_sleep(K_SECONDS(2)); + report_cbs_settings(); + + job_t *job1 = create_job("[job]\t\tj1 on", 0); + + for (;;) { + printf("\n"); + k_cbs_push_job(&cbs_1, job_function, job1, K_NO_WAIT); + k_cbs_push_job(&cbs_1, job_function, job1, K_NO_WAIT); + k_cbs_push_job(&cbs_1, job_function, job1, K_NO_WAIT); + k_sleep(K_SECONDS(2)); + } + + destroy_job(job1); + return 0; +}