2021SC@SDUSC
1, Scheduler
* * scheduler is a program module that uses relevant scheduling algorithms to determine the current process to be executed** All schedulers have a common feature: the scheduler can distinguish between ready processes and suspended processes. The scheduler can select one of all ready processes and then activate the process by executing it. The difference between different schedulers lies in the different scheduling algorithms. From the bottom, the scheduler is actually a timer interrupt service program shared by multiple different processes.
The core of embedded real-time operating system is scheduler and process switching
1. Cooperative scheduler
The cooperative scheduler executes the corresponding processes according to the user's set time or cycle. These processes do not support preemption during execution, that is, they are not allowed to preempt the CPU during process execution, but only allow the process to voluntarily give up the control of the CPU
2. Preemptive scheduler
In practical application, because different processes need different response times, when a process with long execution time and no emergency is ahead of a process with short execution time and emergency, the emergency process cannot be executed in time, resulting in reduced efficiency. At this time, preemptive scheduler is necessary** If preemptive scheduling is adopted: * * when a process with higher priority than the current process enters the ready state, the currently executing process will be deprived of CPU control and handed over to the process with higher priority.
The most important advantage of using preemptive scheduler is that the response time of processes can be optimized. The scheduler of TencentOS Tiny, which we will talk about below, also uses preemptive scheduler between processes with different priorities
3. Time slice scheduler
In small embedded RTOS, the most commonly used time slice scheduling algorithm is round robin scheduling algorithm. This scheduling algorithm can be used in preemptive or cooperative multi processes. Time slice scheduling is suitable for scenarios that do not require real-time response of processes.
To implement the round robin scheduling algorithm, you need to assign a special list to the processes with the same priority to record the currently ready processes, and assign a time slice to each process. The time slice is the time when the process executes on the CPU at one time, and the completion of the process needs to be executed on the CPU many times, When a process runs out of time in one execution, the slice will hand over CPU control to the next process.
Current embOS, FreeRTOS, μ Both COS-III and RTX support round robin scheduling algorithm.
2, Scheduler for TencentOS Tiny
In TencentOS Tiny, schedulers are used in two ways:
- For scheduling between processes with different priorities: the scheduler adopts priority based full preemptive scheduling. During system operation, when there is a ready process with higher priority than the current process, the current process will be cut out immediately, and the high priority process will preempt the processor to run
- For the scheduling between processes with the same priority: the scheduler adopts the time slice rotation mode for scheduling
Let's analyze the scheduler workflow of TencentOS Tiny through the source code:
Start scheduler
The scheduler is started by the cpu_sched_start function, which will be TOS_ knl_ The start function is called. This function mainly does two things, first through readyqueue_ highest_ ready_ task_ The get function obtains the highest priority ready process in the current system and assigns it to the pointer K to the current process control block_ curr_ Task, and then set the system status to running KNL_STATE_RUNNING.
Function cpu_sched_start is written in assembly code. TencentOS Tiny supports a variety of core chips, including M3/M4/M7. Different chips have different functions for CPU_ sched_ The implementation of start is different. Here, take M4 as an example:
__API__ k_err_t tos_knl_start(void) { if (tos_knl_is_running()) { return K_ERR_KNL_RUNNING; } k_next_task = readyqueue_highest_ready_task_get(); k_curr_task = k_next_task; k_knl_state = KNL_STATE_RUNNING; cpu_sched_start(); return K_ERR_NONE; }
In the above function, readyqueue_ highest_ ready_ task_ The result of the get () function is to get the process with the highest priority among the ready processes, and then return it to k_next_task and assign it to the pointer K of the current process block_ curr_ Task, modify the kernel running state to running state
port_sched_start CPSID I ; set pendsv priority lowest ; otherwise trigger pendsv in port_irq_context_switch will cause a context swich in irq ; that would be a disaster MOV32 R0, NVIC_SYSPRI14 MOV32 R1, NVIC_PENDSV_PRI STRB R1, [R0] LDR R0, =SCB_VTOR LDR R0, [R0] LDR R0, [R0] MSR MSP, R0 ; k_curr_task = k_next_task MOV32 R0, k_curr_task MOV32 R1, k_next_task LDR R2, [R1] STR R2, [R0] ; sp = k_next_task->sp LDR R0, [R2] ; PSP = sp MSR PSP, R0 ; using PSP MRS R0, CONTROL ORR R0, R0, #2 MSR CONTROL, R0 ISB ; restore r4-11 from new process stack LDMFD SP!, {R4 - R11} IF {FPU} != "SoftVFP" ; ignore EXC_RETURN the first switch LDMFD SP!, {R0} ENDIF ; restore r0, r3 LDMFD SP!, {R0 - R3} ; load R12 and LR LDMFD SP!, {R12, LR} ; load PC and discard xPSR LDMFD SP!, {R1, R2} CPSIE I BX R1
In the process of starting the kernel scheduler, you need to configure the interrupt priority of PendSV to be the lowest, that is, to NVIC_ Write syspri14 (0xE000ED22) address to NVIC_PENDSV_PRI(0xFF). Because PendSV involves system scheduling, the priority of system scheduling is lower than that of other hardware interrupts of the system, that is, it gives priority to responding to external hardware interrupts in the system. Therefore, the interrupt priority of PendSV should be configured as the lowest, otherwise it is likely to generate process scheduling in the interrupt context.
PendSV exception will automatically delay the request for context switching until other ISRs are processed. To implement this mechanism, you need to program PendSV as the lowest priority exception. If the OS detects that an ISR is active, it will suspend a PendSV exception to suspend the context switch. That is, as long as the priority of PendSV is set to the lowest, even if systick interrupts the IRQ, it will not immediately perform context switching. Instead, the PendSV service routine will not start executing until the ISR is executed, and the context switching will be performed inside. Then get the address of MSP main stack pointer. In Cortex-M, 0xE000ED08 is SCB_ The address of the vtor register, which stores the starting address of the vector table. Load K_ next_ The process control block pointed to by task is to R2, where R2 is equal to the stack top pointer.
Load R2 to R0, and then update the stack top pointer R0 to psp. The stack pointer used during process execution is psp.
With R0 as the base address, load the contents of 8 words growing upward in the stack into the CPU registers R4~R11. At the same time, R0 will increase automatically. Then, you need to load R0 ~ R3, R12, LR, PC and xPSR into the CPU register group. The PC pointer points to the thread to be run, and the LR register points to the exit of the process. Because this is the first time to start a process, you need to manually pop all the registers on the process stack into the hardware to enter the context of the first process. At the beginning, there is no context environment for the first process to run, and you need to save it above when entering PendSV, so you need to manually create the process context environment (load these registers into the CPU register group)
Initialization of process stack
Let's look at how to initialize the process stack in the source code:
__KERNEL__ k_stack_t *cpu_task_stk_init(void *entry, void *arg, void *exit, k_stack_t *stk_base, size_t stk_size) { cpu_data_t *sp; sp = (cpu_data_t *)&stk_base[stk_size]; sp = (cpu_data_t *)((cpu_addr_t)(sp) & 0xFFFFFFF8); /* auto-saved on exception(pendSV) by hardware */ *--sp = (cpu_data_t)0x01000000u; /* xPSR */ *--sp = (cpu_data_t)entry; /* entry */ *--sp = (cpu_data_t)exit; /* R14 (LR) */ *--sp = (cpu_data_t)0x12121212u; /* R12 */ *--sp = (cpu_data_t)0x03030303u; /* R3 */ *--sp = (cpu_data_t)0x02020202u; /* R2 */ *--sp = (cpu_data_t)0x01010101u; /* R1 */ *--sp = (cpu_data_t)arg; /* R0: arg */ /* Remaining registers saved on process stack */ /* EXC_RETURN = 0xFFFFFFFDL Initial state: Thread mode + non-floating-point state + PSP 31 - 28 : EXC_RETURN flag, 0xF 27 - 5 : reserved, 0xFFFFFE 4 : 1, basic stack frame; 0, extended stack frame 3 : 1, return to Thread mode; 0, return to Handler mode 2 : 1, return to PSP; 0, return to MSP 1 : reserved, 0 0 : reserved, 1 */ #if defined (TOS_CFG_CPU_ARM_FPU_EN) && (TOS_CFG_CPU_ARM_FPU_EN == 1U) *--sp = (cpu_data_t)0xFFFFFFFDL; #endif *--sp = (cpu_data_t)0x11111111u; /* R11 */ *--sp = (cpu_data_t)0x10101010u; /* R10 */ *--sp = (cpu_data_t)0x09090909u; /* R9 */ *--sp = (cpu_data_t)0x08080808u; /* R8 */ *--sp = (cpu_data_t)0x07070707u; /* R7 */ *--sp = (cpu_data_t)0x06060606u; /* R6 */ *--sp = (cpu_data_t)0x05050505u; /* R5 */ *--sp = (cpu_data_t)0x04040404u; /* R4 */ return (k_stack_t *)sp; }
The source code is explained below:
- Get the stack top pointer as stk_base[stk_size] with high address, the stack of Cortex-M kernel grows downward.
- Bit 24 of R0, R1, R2, R3, R12, R14, R15 and xPSR will be automatically loaded and saved by the CPU.
- bit24 of xPSR must be set to 1, i.e. 0x01000000.
- Entry is the entry address of the process, that is, the PC
- R14 (LR) is the exit address of the process, so the process is generally an endless loop without return
- R0: arg is a formal parameter of the process body
- When initializing the stack, the sp pointer decreases automatically
That's all for this week. Next week, we will analyze the source code of how the scheduler finds the highest priority process and how to switch processes after finding the highest priority process