Task Services
Concepts
A task is a preemptible thread of execution that implements a portion of an application’s processing. It is is normally used to perform processing that is too lengthy or complex to be performed by a fiber or an ISR.
A microkernel application can define any number of application tasks. Each task has a name that uniquely identifies it, allowing it to be directly referenced by other tasks. Other properties that must be specified when a task is defined include:
- a memory region that is used for its stack and for other execution context information,
- a function that is invoked when the task starts executing, and
- a priority that is used by the microkernel scheduler.
A task’s entry point function takes no arguments, so there is no need to define any argument values for it.
The microkernel automatically defines a system task, known as the idle task, which has lowest priority. This task is used during system initialization, and subsequently executes only when there is no other work for the system to do. The idle task is anonymous and must not be referenced by application tasks.
Note
A nanokernel application can define only a single application task, known as the background task, which is very different from the microkernel tasks described in this section. For more information see Nanokernel Task Services.
Task Lifecycle
The kernel automatically starts a task during system initialization if the task
belongs to the EXE
task group; see Task Groups below.
A task that is not started automatically must be started by another task
using task_start()
.
Once a task is started it normally executes forever. A task may terminate gracefully by simply returning from its entry point function. If it does, it is the task’s responsibility to release any system resources it may own (such as mutexes and dynamically allocated memory blocks) prior to returning, since the kernel does not attempt to reclaim them so they can be reused.
A task may also terminate non-gracefully by aborting. The kernel
automatically aborts a task when it generates a fatal error condition,
such as dereferencing a null pointer. A task can also be explicitly aborted
using task_abort()
. As with graceful task termination,
the kernel does not attempt to reclaim system resources owned by the task.
A task may optionally register an abort handler function that is invoked by the kernel when the task terminates (including during graceful termination). The abort handler can be used to record information about the terminating task or to assist in reclaiming system resources owned by the task. The abort handler function is invoked by the microkernel server fiber, so it cannot directly call kernel APIs that must be invoked by a task; instead, it must co-ordindate with another task to invoke such APIs indirectly.
Note
The kernel does not currently make any claims regarding an application’s ability to restart a terminated task.
Task Scheduling
The microkernel’s scheduler selects which of the system’s tasks is allowed to execute; this task is known as the current task. The nanokernel’s scheduler permits the current task to execute only when there is no fiber or ISR that needs to execute, since fiber and ISR execution takes precedence.
The kernel automatically takes care of saving the current task’s CPU register values when it performs a context switch to a different task, a fiber, or an ISR, and restores these values when the task later resumes execution.
Task State
A microkernel task has an associated state that determines whether or not it can be scheduled for execution. The state records all factors that can prevent the task from executing, such as:
- the task has not been started
- the task is waiting for it needs (e.g. a semaphore, a timeout, ...)
- the task has been suspended
- the task has terminated
A task whose state has no factors that prevent its execution is said to be executable.
Task Priorities
A microkernel application can be configured to support any number of task
priority levels using the NUM_TASK_PRIORITIES
configuration option.
An application task can have any priority from 0 (highest priority) down to
NUM_TASK_PRIORITIES
-2. The lowest priority level,
NUM_TASK_PRIORITIES
-1, is reserved for the microkernel’s idle task.
A task’s original priority can be altered up or down after the task has been started.
Scheduling Algorithm
The microkernel’s scheduler always selects the highest priority executable task to be the current task. If multiple executable tasks of that priority are available the scheduler chooses the one that has been waiting longest.
Once a task becomes the current task it remains scheduled for execution by the microkernel until one of the following occurs:
- The task is supplanted by a higher priority task that becomes ready to execute.
- The task is supplanted by an equal priority task that is ready to execute,
either because the current task explicitly calls
task_yield()
or because the kernel implicitly callstask_yield()
because the scheduler’s time slice has expired. - The task is supplanted by an equal or lower priority task that is ready to execute, because the current task calls a kernel API that blocks its own execution. (For example, the task attempts to take a semaphore that is unavailable.)
- The task terminates itself by returning from its entry point function.
- The task aborts itself by performing an operation that causes a fatal error,
or by calling
task_abort()
.
Time Slicing
The microkernel’s scheduler supports an optional time slicing capability that prevents a task from monopolizing the CPU when other tasks of the same priority are ready to execute.
The scheduler divides time into a series of time slices, whose size is
measured in system clock ticks. The time slice size is specified by
the TIMESLICE_SIZE
configuration option, but this size can also
be changed dynamically while the application is running.
At the end of every time slice the scheduler implicitly invokes
task_yield()
on behalf of the current task, thereby giving
all other tasks of that priority the opportunity to execute before the
current task can once again be scheduled. If one or more equal priority
tasks are ready to execute, the current task is preempted to allow those
tasks to execute. If no equal priority tasks are ready to execute,
the current task remains the current task, and continues to execute.
Tasks having a priority higher than that specified by the
TIMESLICE_PRIORITY
configuration option are exempt from time
slicing, and are never preempted by a task of equal priority. This
capability allows an application to use time slicing only for lower
priority tasks that are less time-sensitive.
Note
The microkernel’s time slicing algorithm does not ensure that a set of equal priority tasks will receive an equitable amount of CPU time, since it does not measure the amount of time a task actually gets to execute. For example, a task may become the current task just before the end of a time slice and then immediately have to yield the CPU. On the other hand, the microkernel’s scheduler does ensure that a task never executes for longer than a single time slice without being required to yield.
Task Suspension
The microkernel allows a task to be suspended, which prevents the task
from executing for an indefinite period of time. The task_suspend()
API allows an application task to suspend any other task, including itself.
Suspending a task that is already suspended has no additional effect.
Once suspended, a task cannot be scheduled until another task calls
task_resume()
to remove the suspension.
Note
A task can prevent itself from executing for a specified period of time
using task_sleep()
. However, this is different from suspending
a task since a sleeping task becomes executable automatically when the
time limit is reached.
Task Groups
The kernel allows a set of related tasks, known as a task group, to be manipulated as a single unit, rather than individually. This simplifies the work required to start related tasks, to suspend and resume them, or to abort them.
The kernel supports a maximum of 32 distinct task groups. Each task group has a name that uniquely identifies it, allowing it to be directly referenced by tasks.
The task groups a task belong to are specified when the task is defined. A task may belong to a single task group, to multiple task groups, or to no task group. A task’s group memberships can also be changed dynamically while the application is running.
The task groups listed below are pre-defined by the kernel; additional task groups can be defined by the application.
EXE
- The set of tasks which are started automatically by the kernel during system intialization.
SYS
- The set of system tasks which continue executing during system debugging.
FPU
- The set of tasks that require the kernel to save x87 FPU and MMX floating point context information during context switches.
SSE
- The set of tasks that require the kernel to save SSE floating point context information during context switches. (Tasks in this group are implicitly members of the
FPU
task group too.)
Usage
Defining a Task
The following parameters must be defined:
- name
- This specifies a unique name for the task.
- priority
- This specifies the scheduling priority of the task.
- entry_point
This specifies the name of the task’s entry point function, which should have the following form:
void <entry_point>(void) { /* task mainline processing */ ... /* (optional) normal task termination */ return; }- stack_size
- This specifies the size of the memory region used for the task’s stack and for other execution context information, in bytes.
- groups
- This specifies the task groups the task belongs to.
Public Task
Define the task in the application’s MDEF using the following syntax:
TASK name priority entry_point stack_size groups
The task groups are specified using a comma-separated list of task group names
enclosed in square brackets, with no embedded spaces. If the task does not
belong to any task group specify an empty list; i.e. []
.
For example, the file projName.mdef
defines a system comprised
of six tasks as follows:
% TASK NAME PRIO ENTRY STACK GROUPS
% ===================================================================
TASK MAIN_TASK 6 keypad_main 1024 [KEYPAD_TASKS,EXE]
TASK PROBE_TASK 2 probe_main 400 []
TASK SCREEN1_TASK 8 screen_1_main 4096 [VIDEO_TASKS]
TASK SCREEN2_TASK 8 screen_2_main 4096 [VIDEO_TASKS]
TASK SPEAKER1_TASK 10 speaker_1_main 1024 [AUDIO_TASKS]
TASK SPEAKER2_TASK 10 speaker_2_main 1024 [AUDIO_TASKS]
A public task can be referenced by name from any source file that includes
the file zephyr.h
.
Private Task
Define the task in a source file using the following syntax:
DEFINE_TASK(PRIV_TASK, priority, entry, stack_size, groups);
The task groups are specified using a list of task group names separated by
|
; i.e. the logical OR operator. If the task does not belong to any
task group specify NULL.
For example, the following code can be used to define a private task named
PRIV_TASK
.
DEFINE_TASK(PRIV_TASK, 10, priv_task_main, 800, EXE);
To utilize this task from a different source file use the following syntax:
extern const ktask_t PRIV_TASK;
Defining a Task Group
The following parameters must be defined:
- name
- This specifies a unique name for the task group.
Public Task Group
Define the task group in the application’s .MDEF file using the following syntax:
TASKGROUP name
For example, the file projName.mdef
defines three new task groups
as follows:
% TASKGROUP NAME
% ========================
TASKGROUP VIDEO_TASKS
TASKGROUP AUDIO_TASKS
TASKGROUP KEYPAD_TASKS
A public task group can be referenced by name from any source file that
includes the file zephyr.h
.
Note
Private task groups are not supported by the Zephyr kernel.
Example: Starting a Task from Another Task
This code shows how the currently executing task can start another task.
void keypad_main(void)
{
/* begin system initialization */
...
/* start task to monitor temperature */
task_start(PROBE_TASK);
/* continue to bring up and operate system */
...
}
Example: Suspending and Resuming a Set of Tasks
This code shows how the currently executing task can temporarily suspend the execution of all tasks belonging to the designated task groups.
void probe_main(void)
{
int was_overheated = 0;
/* continuously monitor temperature */
while (1) {
now_overheated = overheating_update();
/* suspend non-essential tasks when overheating is detected */
if (now_overheated && !was_overheated) {
task_group_suspend(VIDEO_TASKS
AUDIO_TASKS);
was_overheated = 1;
}
/* resume non-essential tasks when overheating abates */
if (!now_overheated && was_overheated) {
task_group_resume(VIDEO_TASKS
AUDIO_TASKS);
was_overheated = 0;
}
/* wait 10 ticks of system clock before checking again */
task_sleep(10);
}
}
Example: Offloading Work to the Microkernel Server Fiber
This code shows how the currently executing task can perform critical section processing by offloading it to the microkernel server. Since the critical section function is being executed by a fiber, once the function begins executing it cannot be interrupted by any other fiber or task that wants to log an alarm.
/* alarm logging subsystem */
#define MAX_ALARMS 100
struct alarm_info alarm_log[MAX_ALARMS];
int num_alarms = 0;
int log_an_alarm(struct alarm_info *new_alarm)
{
/* ensure alarm log isn't full */
if (num_alarms == MAX_ALARMS) {
return 0;
}
/* add new alarm to alarm log */
alarm_info[num_alarms] = *new_alarm;
num_alarms++;
/* pass back alarm identifier to indicate successful logging */
return num_alarms;
}
/* task that generates an alarm */
void XXX_main(void)
{
struct alarm_info my_alarm = { ... };
...
/* record alarm in system's database */
if (task_offload_to_fiber(log_an_alarm, &my_alarm) == 0) {
printf("Unable to log alarm!");
}
...
}
APIs
The following APIs affecting the currently executing task
are provided by microkernel.h
:
task_id_get()
- Gets the task’s ID.
isr_task_id_get()
- Gets the task’s ID from an ISR.
task_priority_get()
- Gets the task’s priority.
isr_task_priority_get()
- Gets the task’s priority from an ISR.
task_group_mask_get()
- Gets the task’s group memberships.
isr_task_group_mask_get()
- Gets the task’s group memberships from an ISR.
task_abort_handler_set()
- Installs the task’s abort handler.
task_yield()
- Yields CPU to equal-priority tasks.
task_sleep()
- Yields CPU for a specified time period.
task_offload_to_fiber()
- Instructs the microkernel server fiber to execute a function.
The following APIs affecting a specified task
are provided by microkernel.h
:
task_priority_set()
- Sets a task’s priority.
task_entry_set()
- Sets a task’s entry point.
task_start()
- Starts execution of a task.
task_suspend()
- Suspends execution of a task.
task_resume()
- Resumes execution of a task.
task_abort()
- Aborts execution of a task.
task_group_join()
- Adds a task to the specified task group(s).
task_group_leave()
- Removes a task from the specified task group(s).
The following APIs affecting multiple tasks
are provided by microkernel.h
:
sys_scheduler_time_slice_set()
- Sets the time slice period used in round-robin task scheduling.
task_group_start()
- Starts execution of all tasks in the specified task groups.
task_group_suspend()
- Suspends execution of all tasks in the specified task groups.
task_group_resume()
- Resumes execution of all tasks in the specified task groups.
task_group_abort()
- Aborts execution of all tasks in the specified task groups.