Task Services

Concepts

A task is a preemptible thread of execution that implements a portion of an application’s processing. It is normally used for processing that is too lengthy or too 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. For each microkernel task, the following properties must be specified:

  • A memory region to be used for stack and execution context information.
  • A function to be invoked when the task starts executing.
  • The priority to be 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, at the 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 to be 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 coordinate 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 no fibers or ISRs are available to execute; fiber and ISR executions always take precedence.

When prompted for a context switch to a different task, fiber, or ISR, the kernel automatically saves the current task’s CPU register values; these values get restored when the task later resumes execution.

Task State

Each microkernel task has an associated state that determines whether or not it can be scheduled for execution. The state records factors that could prevent the task from executing, such as:

  • The task has not been started.
  • The task is waiting (for a semaphore, for 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 CONFIG_NUM_TASK_PRIORITIES configuration option.

An application task can have any priority from 0 (highest priority) down to CONFIG_NUM_TASK_PRIORITIES-2. The lowest priority level, CONFIG_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. When multiple executable tasks of the same 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 calls task_yield() after the scheduler’s time slice expired.
  • The task is supplanted by an equal or lower-priority task that is ready to execute because the current task called a kernel API that blocked its own execution. For example, the task attempted to take a semaphore that was 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, where slices are measured in system clock ticks. The time slice size is specified with the CONFIG_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; this gives any other task 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 tasks of equal priority are ready to execute, the current task remains the current task, and it continues to execute.

Tasks with a priority higher than that specified by the CONFIG_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 belongs 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 group designations listed below are pre-defined by the kernel; additional task groups can be defined by the application.

EXE
This task group is started automatically by the kernel during system intialization.
SYS
This task group is a set of system tasks that continues to execute during system debugging.
FPU
This task group is a set of tasks that requires the kernel to save x87 FPU and MMX floating point context information during context switches.
SSE
This task group is a set of tasks that requires the kernel to save SSE floating point context information during context switches. (Tasks with this group designation 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 bitwise 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

All of the following Microkernel APIs are provided by microkernel.h.

APIs Affecting the Currently-Executing Task

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.

APIs Affecting a Specified Task

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

APIs Affecting Multiple Tasks

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.