Understanding RTOS Priority Preemptive Scheduling

Follow+Star Public Number, don’t miss wonderful content

Understanding RTOS Priority Preemptive Scheduling

Source | McuLover666

1. Knowledge Point Consolidation

1.1. Knowledge Point Consolidation

The three major elements of a task: task control block, task stack, task entry function, here you can refer to the article:

  • How to write RTOS tasks?

1.2. Doubly Circular Linked List

A doubly linked list is a type of linked list, distinguished by the fact that each node has both a successor pointer and a predecessor pointer, the nodes of a doubly linked list look like this:Understanding RTOS Priority Preemptive Scheduling

If you are not familiar with the implementation and usage of doubly circular lists, please read this article first:

  • Implementation and Use of Doubly Circular Linked List in TencentOS-tiny

2. How Tasks Switch

In the RTOS kernel, the principle of switching from one task to the next is:

“Manually trigger the PendSV exception, and implement task switching in the PendSV exception service function”.

2.1. How to Trigger PendSV Exception

In STM32, set bit 28 of the Interrupt Control and State Register (ICSR) to 1 to manually trigger the PendSV exception, as shown:

Understanding RTOS Priority Preemptive Scheduling

The underlying function to trigger exceptions in tos is port_context_switch, implemented in arch\arm\arm-v7m\cortex-m4\armcc\port_s.S as follows:

    GLOBAL port_context_switch
port_context_switch
    LDR     R0, =NVIC_INT_CTRL
    LDR     R1, =NVIC_PENDSVSET
    STR     R1, [R0]
    BX      LR

At first glance, this assembly looks a bit difficult; let’s take a look at the specific values:

NVIC_INT_CTRL   EQU     0xE000ED04
NVIC_PENDSVSET  EQU     0x10000000

So the assembly code above is actually completing the operation of setting bit 28 of the ICSR register (which is NVIC_INT_CTRL in the code) to 1, right?

2.2. Implementing Task Switching in Exception Service

In STM32, the PendSV exception service function is named PendSV_Handler, and a weak definition is provided by default in stm32l4xx_it.c, so the implementation in tos can directly redefine this function, with the source code in arch\arm\arm-v7m\cortex-m4\armcc\port_s.S, the main steps are four:

“Disable global interrupts” (except NMI and HardFault) to prevent interruption during task switching:

CPSID   I

“Save the context”: Save the values of the current CPU register set and the value of the PSP stack pointer to the task stack;

“Load the context”: Load the values from the current task stack into the CPU register set and the PSP stack pointer;

“Enable global interrupts” to respond to all system interrupts in real time:

CPSIE   I

Just remember these four processes for task switching; delving into what each assembly instruction means is not very useful or helpful.

2.3. When the CPU Responds to PendSV Exception

We all know that “high-priority interrupts can interrupt low-priority interrupts”, which is an important guarantee of system real-time performance, so a question arises:

Compared to GPIO interrupts, timer interrupts, and serial interrupts, is the PendSV exception priority higher or lower?

Imagine this situation:

① The CPU is happily running task 1…

② At this moment, you press a button, generating a GPIO interrupt, and the CPU immediately runs to execute the interrupt handler…

③ During the processing, a PendSV exception occurs, and the CPU receives it and sarcastically says: “I just came from a normal task to handle an interrupt, and haven’t finished processing, now you want me to execute the next normal task, are you crazy?” After saying that, it continues to handle the interrupt…

Therefore, no matter how high the priority of the task is, it is not as high as an interrupt, “the priority of the system’s PendSV exception must be set to the lowest” to avoid task switching during external interrupt service functions.

The register to set the PendSV exception priority is as follows, the value can be from 0-255:Understanding RTOS Priority Preemptive Scheduling

In tos, the priority of the PendSV exception is set when starting the scheduler, with the source code as follows:

NVIC_SYSPRI14   EQU     0xE000ED22
NVIC_PENDSV_PRI EQU     0xFF

Similarly, the assembly code to set the PendSV exception priority to the lowest is as follows:

; set pendsv priority lowest
; otherwise trigger pendsv in port_irq_context_switch will cause a context switch in irq
; that would be a disaster
MOV32   R0, NVIC_SYSPRI14
MOV32   R1, NVIC_PENDSV_PRI
STRB    R1, [R0]

3. Ready List

3.1. What Does the Ready List Look Like?

The ready list is actually many doubly linked lists + a priority table, its type is defined in tos_sched.h, as follows:

typedef struct readyqueue_st
{
    k_list_t    task_list_head[TOS_CFG_TASK_PRIO_MAX];
    uint32_t    prio_mask[K_PRIO_TBL_SIZE];
    k_prio_t    highest_prio;
} readyqueue_t;

“Each priority is assigned a head node of a doubly linked list to mount the tasks of that priority”.

TOS_CFG_TASK_PRIO_MAX is the maximum task priority, configured in tos_config.h, the default is 10:

#define TOS_CFG_TASK_PRIO_MAX           10u

The node type k_list_t is a doubly linked list node type:

typedef struct k_list_node_st
{
    struct k_list_node_st *next;
    struct k_list_node_st *prev;
} k_list_t;

After all doubly linked list nodes are initialized, each doubly node’s next pointer points to itself (orange line), and the prev pointer also points to itself, as shown:Understanding RTOS Priority Preemptive Scheduling

“Used to indicate the priority currently used by the system”.

The size of the priority table is determined by the macro definition K_PRIO_TBL_SIZE:

#define K_PRIO_TBL_SIZE         ((TOS_CFG_TASK_PRIO_MAX + 31) / 32)

This definition is more particular; if the maximum priority does not exceed 32, the value of this macro will be 1, and a uint32_t type variable can be used, with each priority represented by one bit.

After initialization, the priority table looks like this:Understanding RTOS Priority Preemptive Scheduling

“Highest priority indicator member”.

The member highest_prio in the ready list is of type k_prio_t, which is actually of type uint8_t:

typedef uint8_t             k_prio_t;

This member indicates the highest priority of the tasks currently existing in the system, and the default is the maximum priority defined by the system.

3.2. How Many Ready Lists Are in the System?

That’s right, the answer is: “There is only one ready list”.

Declared in tos_global.h, convenient for use in all files of the entire kernel:

/* ready queue of tasks                         */
extern readyqueue_t         k_rdyq;

Defined in tos_global.c:

readyqueue_t        k_rdyq;

“Remember its name, it’s called k_rdyq”, where k stands for kernel and rdyq is an abbreviation for ready queue, it will appear frequently later.

3.3. Initializing the Ready List

Once you know what the ready list looks like, initializing the ready list becomes very simple, it’s all routine operations, implemented in tos_sched.c:

__KNL__ void readyqueue_init(void)
{
    uint8_t i;

    k_rdyq.highest_prio = TOS_CFG_TASK_PRIO_MAX;

    for (i = 0; i < TOS_CFG_TASK_PRIO_MAX; ++i) {
        tos_list_init(&k_rdyq.task_list_head[i]);
    }

    for (i = 0; i < K_PRIO_TBL_SIZE; ++i) {
        k_rdyq.prio_mask[i] = 0;
    }
}

Step ① is to set the initial value of the highest priority member to the highest priority configured for the system;

Step ② is to initialize each doubly linked list node;

Step ③ is to initialize all values of the priority table to 0.

4. How Tasks Are Mounted to the Ready List

At the end of the task creation API, the function readyqueue_add_tail is called to add the task to the ready list, so how is the task actually mounted?

The source code implementation of this function is as follows:

__KNL__ void readyqueue_add_tail(k_task_t *task)
{
    k_prio_t task_prio;
    k_list_t *task_list;

    task_prio = task->prio;
    task_list = &k_rdyq.task_list_head[task_prio];

    if (tos_list_empty(task_list)) {
        readyqueue_prio_mark(task_prio);
    }

    tos_list_add_tail(&task->pend_list, task_list);
}

“Get the head node corresponding to the priority of the task in the ready list”.

“Record this priority in the priority table”.

Determine whether this priority appears for the first time in the system; if so, set the corresponding bit in the priority table to 1 to indicate that a task of this priority exists in the system, and reassign the highest priority member in the ready list (note: the lower the priority value, the higher the priority):

__STATIC_INLINE__ void readyqueue_prio_insert(k_prio_t prio)
{
    k_rdyq.prio_mask[K_PRIO_NDX(prio)] |= K_PRIO_BIT(prio);
}

__STATIC_INLINE__ void readyqueue_prio_mark(k_prio_t prio)
{
    readyqueue_prio_insert(prio);

    if (prio < k_rdyq.highest_prio) {
        k_rdyq.highest_prio = prio;
    }
}

For example, when we create a task with priority 2, the priority table will look like this:Understanding RTOS Priority Preemptive Scheduling

“Mount the task control block’s pend list node to the tail of the doubly linked list obtained in step 1”.

The pend_list member in the task control block is also a doubly linked list node:

/**
 * task control block
 */
struct k_task_st {
 //……
 
    k_list_t pend_list; /**< when we are ready, our pend_list is in readyqueue; when pend, in a certain pend object's list. */

 //……
};

Using a doubly linked list to add this pend list node to the tail of the doubly linked list obtained in step 1, after adding it, as shown:Understanding RTOS Priority Preemptive Scheduling

5. Priority Preemptive Scheduling

5.1. Scheduling Rules

Understanding the above three sections, the priority preemptive scheduling becomes a natural conclusion.

Similarly, let’s first look at the source code of priority preemptive scheduling in tos_syc.c:

__KNL__ void knl_sched(void)
{
    TOS_CPU_CPSR_ALLOC();

    if (unlikely(!tos_knl_is_running())) {
        return;
    }

    if (knl_is_inirq()) {
        return;
    }

    if (knl_is_sched_locked()) {
        return;
    }

    TOS_CPU_INT_DISABLE();
    k_next_task = readyqueue_highest_ready_task_get();
    if (knl_is_self(k_next_task)) {
        TOS_CPU_INT_ENABLE();
        return;
    }

    cpu_context_switch();
    TOS_CPU_INT_ENABLE();
}

In the source code, it can be seen that priority preemptive scheduling is actually two steps:

① Get the pointer to the task control block of the highest priority task in the ready list;

② Start context switching;

To summarize, the rule of priority preemptive scheduling is:

“Whenever scheduling conditions are met, switch to the highest priority task in the ready list to start running”.

5.2. How to Get the Highest Priority Task

Don’t forget that there is a member in the ready list called highest_prio, which indicates the highest priority currently existing in the system, making it easy to obtain the linked list of the highest priority task, the function source code is as follows:

__KNL__ k_task_t *readyqueue_highest_ready_task_get(void)
{
    k_list_t *task_list;

    task_list = &k_rdyq.task_list_head[k_rdyq.highest_prio];
    return TOS_LIST_FIRST_ENTRY(task_list, k_task_t, pend_list);
}

However, it should be noted that what is mounted on the ready list is the pend list node in the task control block, as shown:Understanding RTOS Priority Preemptive Scheduling

Knowing the address of the pend list node in the task control block, how do we determine the base address of the task control block it belongs to?

Actually, it is obtained through the macro TOS_LIST_FIRST_ENTRY, for specific usage, please refer to the second article I mentioned at the beginning.

6. What is the Use of the Priority Table?

The purpose of the priority table is:

“To obtain the highest priority in the current ready list when removing a task from the ready list”.

The priority preemptive scheduler is indifferent to everything, it doesn’t care what the current state of the task is, it always looks for the highest priority task in the scheduling list.

So when a task needs to actively suspend, it must be removed from the ready list, the source code is as follows:

__API__ k_err_t tos_task_suspend(k_task_t *task)
{
    TOS_CPU_CPSR_ALLOC();

 //A bunch of parameter checks omitted

    TOS_CPU_INT_DISABLE();

    if (task_state_is_ready(task))
    { 
     // kill the good kid
        readyqueue_remove(task);
    }
    task_state_set_suspended(task);

    TOS_CPU_INT_ENABLE();
    knl_sched();

    return K_ERR_NONE;
}

The core function to remove from the ready list is readyqueue_remove, the source code is as follows:

__KNL__ void readyqueue_remove(k_task_t *task)
{
    k_prio_t task_prio;
    k_list_t *task_list;

    task_prio = task->prio;
    task_list = &k_rdyq.task_list_head[task_prio];

    tos_list_del(&task->pend_list);

    if (tos_list_empty(task_list)) {
        readyqueue_prio_remove(task_prio);
    }

    if (task_prio == k_rdyq.highest_prio) {
        k_rdyq.highest_prio = readyqueue_prio_highest_get();
    }
}

① Get the head node of the current priority in the ready list;

② Disconnect the task control block from that doubly linked list (but do not delete the task);

③ If the linked list becomes empty after disconnection, it indicates that there are no tasks of that priority in the ready list, and the corresponding bit in the priority table will be cleared;

“Re-obtain the highest priority in the ready list”.

This is when the role of the priority table is reflected; as mentioned earlier, the priority table records the priorities of the tasks currently existing in the ready list, so it can be used to traverse and find the highest priority, which is then assigned to the indicator member in the ready list.

The source code is as follows:

__STATIC__ k_prio_t readyqueue_prio_highest_get(void)
{
    uint32_t *tbl;
    k_prio_t prio;

    prio    = 0;
    tbl     = &k_rdyq.prio_mask[0];

    while (*tbl == 0) {
        prio += K_PRIO_TBL_SLOT_SIZE;
        ++tbl;
    }
    prio += tos_cpu_clz(*tbl);
    return prio;
}

7. Summary

After discussing so much content, it is very necessary to summarize the key points:

“The RTOS kernel starts a switch by manually triggering the PendSV exception, and task switching is implemented in the PendSV exception service function”.

“The priority of the PendSV exception in the RTOS kernel is set to the lowest to avoid task switching during external interrupt handler functions”.

“The so-called priority preemptive scheduling rule in the RTOS kernel is to always find the highest priority task from the ready queue to run”.

Of course, with the priority preemptive scheduling rule, it barely supports the physical body of an RTOS kernel, but when to schedule is the soul of an RTOS kernel.

———— END ————
Understanding RTOS Priority Preemptive Scheduling
● Column “Embedded Tools”
● Column “Embedded Development”
● Column “Keil Tutorial”
● Selected Tutorials from Embedded Column
Reply “Join Group” to join the technical exchange group according to the rules, reply “1024” to see more content.
Click “Read Original” to see more shares.

Leave a Comment

×