diff --git a/doc/introduction/index.rst b/doc/introduction/index.rst index af67331aa299..3d24e4e5601a 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 cb56ccd3673b..4504a4fb178a 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 000000000000..87e5b02a4430 --- /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 000000000000..c2e622c2a4e3 --- /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 000000000000..32ef06b6a1b0 --- /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 000000000000..80f484844d8a --- /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
A 
A 
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 036bef82c662..d1e7cd22082c 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 \ diff --git a/include/zephyr/kernel/thread.h b/include/zephyr/kernel/thread.h index fd8e4c02f235..319a1e76521d 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 000000000000..2e8b90a243ec --- /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 8ba95f6c5708..2f9194a3718e 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 28177a1d76a6..cb4b18d9cd4b 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 000000000000..d9916cf0870d --- /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 03c4a991bb08..0dcea67a5032 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 000000000000..24fe0dd53aaa --- /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 000000000000..dbb40af7ee88 --- /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 d4de81b19d7c..9db27ef6c5a3 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 000000000000..f0674378f55d --- /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 000000000000..f3690560863a --- /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 184343a70de6..e9d2dff4d065 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 diff --git a/samples/cbs/cbs.rst b/samples/cbs/cbs.rst new file mode 100644 index 000000000000..bcf20b7ff4d3 --- /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 000000000000..51ede44f269d --- /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 000000000000..d477fc04f2e6 --- /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 000000000000..8f179c5669cb --- /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 000000000000..eb457dd545dc --- /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 000000000000..a23af76e6991 --- /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 000000000000..96341d36266e --- /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 000000000000..a0a7dd4aac26 --- /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 000000000000..870ae2edd1e2 --- /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 000000000000..7af8df67d2c2 --- /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 000000000000..51ede44f269d --- /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 000000000000..05a91055537a --- /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 000000000000..0f3984ac829f --- /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 000000000000..478a2bacb565 --- /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 000000000000..7a151fbced5b --- /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 000000000000..af67a1604afd --- /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; +}