一、时间片原理介绍
1.1、什么是时间片?
时间片是操作系统分配给每个正在运行的进程(或线程)的一个固定时间段,在这个时间段内,进程可以独占CPU并执行其任务。时间片的主要目的是确保所有处于就绪状态的进程都能获得公平的CPU时间,避免某些进程长时间占用CPU。在时间片轮转调度中,系统会维护一个就绪队列,所有就绪进程按照到达的顺序排队等待执行。操作系统按照队列顺序依次分配时间片,每个进程在获得CPU后开始执行,直到其时间片用完。
时间片的长度通常由操作系统的调度器决定,这个长度可以根据系统的性能要求和进程的优先级进行调整。较短的时间片可以提供更快的响应时间,但会增加调度的开销;较长的时间片可以减少调度次数,但可能会降低系统的响应性。在OneOS中,默认一个时间片等于SysTick(滴答定时器)中断周期。
OneOS支持多个任务同时拥有一个优先级,在OneOS中允许一个任务运行一段时间(时间片)后让出CPU的使用权,让拥有同优先级的下一个任务运行,这种任务调度方法就是时间片轮转调度。下图展示了运行在同一优先级下的执行时间图,在优先级N下有3个就绪的任务,我们将时间片划分为3个时钟节拍。
(1) 任务3正在运行 (2) 任务3的时间片用完
(3) OneOS切换到任务1,任务1是优先级 N下的下一个就绪任务。
(4) 任务1连续运行至时间片用完。
(5) OneOS切换到任务2,任务2是优先级 N下的下一个就绪任务。
(6) 切换任务2。 (7) 任务2连续运行至时间片用完。
(8) OneOS切换到任务3,任务3是优先级 N下的下一个就绪任务。
(9) 切换任务3。 (10) 任务3运行
1.2、如何设置时间片?
工程目录下,右键打开OneOS-Cube并输入menuconfig。进入系统配置中修改Task time slice参数即可,OneOS默认值为10(表示有10个时间片)。影响时间片长度的还有Tick frequency(Hz),默认值为100(表示一个时间片长度为10ms)。
注意:systick设置为1000那么一个时间片的时间为1s/1000=1ms,若设置为100那么一个时间片的时间为1s/100=10ms。
1.3、时间片轮转调度原理
所有就绪任务被存放在就绪队列里面,如下图
上图中,
① BIT位为1时,优先级N就绪队列有就绪任务,否则没有。
② 相同优先级的就绪任务会连接在同一个队列上。
③ 发生调度时(例如更高优先级任务就绪或者当前任务退出时),通过查询变量中为1的最低BIT位,就可以得到最高优先级的就绪任务。
④ 任务创建时,会调用os_task_startup()这个函数把该任务挂载相应的就绪队列中。
时间片轮转调度是操作系统中实现多任务并发的一种基本方法,它简单、公平,并且能够提供合理的响应时间。然而,它可能不适用于所有类型的系统,特别是那些对实时性要求极高的系统。在这些系统中,可能需要更复杂的调度算法来保证任务的实时约束。
在实现上,调度器通常使用一个定时器中断来跟踪时间片的消耗。每次定时器中断发生时,如果当前进程的时间片已经用完,调度器就会进行上下文切换,将CPU分配给队列中的下一个进程。OneOS中的时钟节拍来源如下图:
时间片轮转调度提供了一种简单而公平的多任务处理机制,确保了系统中的所有就绪进程都能获得CPU时间,从而增强了操作系统的响应性和交互能力。通过为每个进程分配固定的时间片,它有效避免了高优先级进程长时间占用CPU资源,同时保证了低优先级进程也能获得执行的机会,这对于提升用户体验和系统整体性能至关重要。
二、时间片实验
2.1、实验目的
学习 os_task_set_time_slice ()函数的使用
2.2、实验设置
本实验设计两个任务: task1_task 和 task2_task ,这两个任务的任务功能如下:
task1_task:①串口输出任务1运行次数,
②调用Hal_Delay()函数延时10ms。
task2_task:①串口输出任务2运行次数,
②调用Hal_Delay()函数延时10ms
2.3、实验代码解释与分析
#include <board.h>
#include <os_task.h>
#include <os_types.h>
#define TASK1_PRIO 3 /*任务优先级*/
#define TASK1_STK 512 /*任务堆栈大小*/
os_task_dummy_t *task_cb1; /*任务1的控制块*/
void task1_task(void *parameter); /*任务1的任务函数*/
static os_task_id task1=0;
#define TASK2_PRIO 3
#define TASK2_STK 512
os_task_dummy_t *task_cb2;
static os_task_id task2=0;
void task2_task(void *parameter);
/*任务1的任务函数*/
void task1_task(void *parameter)
{
/*parameter = parameter;*/
int task1_num = 0;
while(1)
{
task1_num ++;
os_kprintf("task1 run :%d\r\n",task1_num);
HAL_Delay(10);
}
}
/*任务2的任务函数*/
void task2_task(void *parameter)
{
/*parameter = parameter;*/
int task2_num = 0;
while(1)
{
task2_num ++;
os_kprintf("task2 run :%d\r\n",task2_num);
HAL_Delay(10);
}
}
int main(void)
{
// os_task_id task1;
task1 = os_task_create(task_cb1, OS_NULL, TASK1_STK , "task1", task1_task, OS_NULL, TASK1_PRIO);
OS_ASSERT(task1);
os_task_startup(task1);
os_task_set_time_slice(task1,100);
// os_task_id task2;
task2 = os_task_create(task_cb2, OS_NULL, TASK2_STK , "task2", task2_task, OS_NULL, TASK2_PRIO);
OS_ASSERT(task2);
os_task_startup(task2);
os_task_set_time_slice(task2,50);
return 0;
}
任务创建流程就不解释了,直接看任务函数和main函数
task1_task中,无限循环地递增 task1_num 变量,打印一条消息到控制台,并调用 HAL_Delay(10) 函数使任务休眠10毫秒(任务2同理)。
在 main 函数中,首先创建任务1,使用 os_task_create 函数。这个函数接受任务控制块、堆栈内存、任务名称、任务函数和优先级等参数。然后使用 OS_ASSERT 宏来确保任务创建成功。接着调用 os_task_startup 函数启动任务,并使用 os_task_set_time_slice 函数设置任务1的时间片为100。任务2的时间片的值被设置为50
注意1:OneOS中一个时间片默认值是10ms,在这个实验中我们调整了滴答定时器的周期(默认值100,调整为1000),使得现在的一个时间片默认值为1ms(计算方式1.2节有讲)。
任务1和任务2不同之处在于它俩所占的时间片不同,任务1占有100个时间片,所以运行时长有100ms,但是由于每一次执行时都会被延时10ms, 所以如果任务1的执行时间忽略不计的话,任务1最多执行10次,任务2同理,最多执行5次。从实验结果来看,任务1在执行期间都执行9次,任务2则执行5次,这是合理的也是符合实验设置的。
注意2:HAL_Delay(10) 函数确保了任务1在输出后至少等待10毫秒。因此,即使输出操作本身不需要1毫秒,任务1的输出频率也被限制为每10毫秒一次。
注意3:实际的输出频率还可能受到操作系统调度策略和任务切换开销的影响。如果任务1的执行时间加上 HAL_Delay(10) 的时间超过了一个时间片,那么调度器可能会在 HAL_Delay(10) 期间切换到任务2,从而实现时间片轮转调度。如果任务1的执行时间非常短,那么它可能会在一个时间片内多次输出,直到时间片用完。