The task block

Tasks have their representation in the system in the form of a task block structure. This object contains all the information needed for the scheduler to keep track of the state of the task at all times, and is dependent on the design of the scheduler. Tasks might be defined at compile time and started after the kernel boots, or spawned and terminated while the system is running.

Each task block may contain a pointer to the start function, that defines the beginning of the code executed when the task is spawned, and a set of optional arguments. Memory is assigned for each task to use it as its private stack region. This way, the execution context of each thread and process is separated from all the others, and the values of the registers can be stored in a task-specific memory area when the task is interrupted. The task-specific stack pointer is stored in the task block structure, and it will be used to store the values of the CPU register upon context switches.

Running with separate stacks requires that some memory is reserved in advance, and associated with each task. In the simplest case, all tasks are using a stack of the same size, are created before the scheduler starts, and cannot be terminated. This way, the memory reserved to be associated with private stacks can be contiguous and associated with each new task. The memory region used for the stack areas can be defined in the linker script.

The reference platform has a separate core-coupled memory, mapped at 0x10000000. Among the many possibilities to arrange the memory sections, we decide to map the start of the stack space, used to associate stack areas to the threads, at the beginning of the CCRAM. The remaining CCRAM space is used as a stack for the kernel, which leaves all the SRAM, excluding the .data and .bss sections, for heap allocation. The pointers are exported by the linker script with the following PROVIDE instructions:

PROVIDE(_end_stack = ORIGIN(CCRAM) + LENGTH(CCRAM));
PROVIDE(stack_space = ORIGIN(CCRAM));
PROVIDE(_start_heap = _end);

In the kernel source, stack_space is declared as external, because it is exported by the linker script. We also declare the amount of space reserved for the execution stack of each task (expressed in number of four-bytes words):

extern uint32_t stack_space;
#define STACK_SIZE (256)

Every time a new task is created, the next kilobyte in the stack space is assigned as its execution stack, and the initial stack pointer will be set at the highest address in the area, as the execution stack grows backward:

Memory configuration used to provide separate execution stacks to tasks

A simple task block structure can then be declared, as follows:

#define TASK_WAITING 0
#define TASK_READY 1
#define TASK_RUNNING 2

#define TASK_NAME_MAXLEN 16;

struct task_block {
char name[TASK_NAME_MAXLEN];
int id;
int state;
void (*start)(void *arg);
void *arg;
uint32_t *sp;
};

A global array is defined to contain all the task blocks of the system. We use a global index to keep track of the tasks already created, to use the position in memory relative to the task identifier and the ID of the current running task:

#define MAX_TASKS 8
static struct task_block TASKS[MAX_TASKS];
static int n_tasks = 1;
static int running_task_id = 0;
#define kernel TASKS[0]

With this model, the task block is pre-allocated in the data section, and the fields are initialized in place, keeping track of the index. The first element of the array is reserved for the task block of the kernel, which is the current running process.

In our example, tasks are created by invoking the task_create function, providing a name, an entry point, and its argument. For a static configuration with a predefined number of tasks, this is done in the kernel initialization, but more advanced schedulers may allow us to allocate new control blocks to spawn new processes at runtime, while the scheduler is running:

struct task_block *task_create(char *name, void (*start)(void *arg), void *arg)
{
struct task_block *t;
int i;
if (n_tasks >= MAX_TASKS)
return NULL;
t = &TASKS[n_tasks];
t->id = n_tasks++;
for (i = 0; i < TASK_NAME_MAXLEN; i++) {
t->name[i] = name[i];
if (name[i] == 0)
break;
}
t->state = TASK_READY;
t->start = start;
t->arg = arg;
t->sp = ((&stack_space) + n_tasks * STACK_SIZE);
task_stack_init(t);
return t;
}

In order to implement the task_stack_init function, which initializes the values in the stack for the process to start running, we need to understand how the context switch works, and how new tasks are actually started when the scheduler is running.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset