Blocking tasks

The simple scheduler implemented so far provides only two states for the tasks, TASK_READY and TASK_RUNNING. A third state can be implemented to define a task that does not actually need to be resumed, because it is blocked waiting for an event or a timeout. A task can be waiting for a system event of some type, such as:

  • Interrupt events from an input/output device in use by the task
  • Communication from another task, such as the TCP/IP stack
  • Synchronization mechanisms, such as a mutex or a semaphore, to access a shared resource in the system that is currently unavailable
  • Timeout events

To manage the different states, the scheduler may implement two or more lists, to separate the tasks currently running, or ready to run, from those waiting for an event. The scheduler then selects the next task among those in the TASK_READY state, and ignores the ones in the list of the blocked tasks:

State machine describing the task's execution states

This second version of the scheduler keeps track of the current running task using a global pointer, instead of the index of the array, and organizes the tasks into two lists:

  • tasklist_active: Containing the task block for the running task and all the tasks in the TASK_READY state, waiting to be scheduled
  • tasklist_waiting: Containing the task block of the tasks currently blocked

The easiest showcase to implement for this new mechanism is a sleep_ms function, which can be used by tasks to temporarily switch to a waiting state and set up a resume point in the future to be scheduled again. Providing this kind of facility allows our tasks to sleep in between LED toggle actions, instead of running a busy-loop, repeatedly checking for the timer expired. These new tasks are not only more efficient, because they do not waste CPU cycles in a busy-loop, but also more readable:

void task_test0(void *arg)
{
blue_led_on();
while(1) {
sleep_ms(500);
blue_led_toggle();
}
}

void task_test1(void *arg)
{
red_led_on();
while(1) {
sleep_ms(125);
red_led_toggle();
}
}

To arrange the task blocks into lists, a pointer to the next element is added to the structure, and the two lists are populated at runtime. To manage the sleep_ms function, a new field is added to keep track of the system time when the task is supposed to be put in the active list to be resumed:

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

The lists can be managed with two simple functions to the insert/delete elements:

struct task_block *tasklist_active = NULL;
struct task_block *tasklist_waiting = NULL;

static void tasklist_add(struct task_block **list,struct task_block *el)
{
el->next = *list;
*list = el;
}

static int tasklist_del(struct task_block **list, struct task_block *delme)
{
struct task_block *t = *list;
struct task_block *p = NULL;
while (t) {
if (t == delme) {
if (p == NULL)
*list = t->next;
else
p->next = t->next;
return 0;
}
p = t;
t = t->next;
}
return -1;
}

Two additional functions are added to move the tasks from the active list to the waiting list and vice versa, which additionally change the state of the task itself:

static void task_waiting(struct task_block *t)
{
if (tasklist_del(&tasklist_active, t) == 0) {
tasklist_add(&tasklist_waiting, t);
t->state = TASK_WAITING;
}
}
static void task_ready(struct task_block *t)
{
if (tasklist_del(&tasklist_waiting, t) == 0) {
tasklist_add(&tasklist_active, t);
t->state = TASK_READY;
}
}

The sleep_ms function sets the resume time and moves the task to the waiting state, then activates the scheduler so that the task is preempted:

void sleep_ms(int ms)
{
if (ms < TASK_TIMESLICE)
return;
t_cur->wakeup_time = jiffies + ms;
task_waiting(t_cur);
schedule();
}

The new PendSV handler selects the next task to run from the active list, which is assumed to always contain at least one task, as the kernel main task is never put in the waiting state. The new thread is selected through the tasklist_next_ready function, which also ensures that if the current task has been moved from the active list, or is the last in line, the head of the active list is selected for the next timeslice:

static inline struct task_block *tasklist_next_ready(struct task_block *t)
{
if ((t->next == NULL) || (t->next->state != TASK_READY))
return tasklist_active;
return t->next;
}

This small function is the core of the new scheduler based on the double list, and is invoked in the middle of each context switch to select the next active task in PendSV:

void __attribute__((naked)) isr_pendsv(void)
{
store_context();
asm volatile("mrs %0, msp" : "=r"(t_cur->sp));
if (t_cur->state == TASK_RUNNING) {
t_cur->state = TASK_READY;
}
t_cur = tasklist_next_ready(t_cur);
t_cur->state = TASK_RUNNING;
asm volatile("msr msp, %0" ::"r"(t_cur->sp));
restore_context();
asm volatile("mov lr, %0" ::"r"(0xFFFFFFF9));
asm volatile("bx lr");
}

Finally, in order to check for the wake-up time of each sleeping task, the kernel visits the list of waiting tasks, and moves the task blocks back to the active list whenever the wake-up time has elapsed. The kernel initialization now includes a few extra steps to ensure that the kernel task itself is put in the list of running tasks at boot:

void main(void) {
clock_pll_on(0);
led_setup();
button_setup();
systick_enable();
kernel.name[0] = 0;
kernel.id = 0;
kernel.state = TASK_RUNNING;
kernel.wakeup_time = 0;
tasklist_add(&tasklist_active, &kernel);
task_create("test0",task_test0, NULL);
task_create("test1",task_test1, NULL);
task_create("test2",task_test2, NULL);
while(1) {
struct task_block *t = tasklist_waiting;
while (t) {
if (t->wakeup_time && (t->wakeup_time < jiffies)) {
t->wakeup_time = 0;
task_ready(t);
break;
}
t = t->next;
}
WFI();
}
}
..................Content has been hidden....................

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