hnu操作系统-lab6

Lab 6 :任务调度

任务调度是操作系统的核心功能之一。 UniProton实现的是一个==单进程支持多线程==的操作系统。在UniProton中,一个任务表示一个线程。UniProton中的任务为==抢占式调度机制==,而非时间片轮转调度方式。高优先级的任务可打断低优先级任务,低优先级任务必须在高优先级任务挂起或阻塞后才能得到调度。

基础数据结构:双向链表

双向链表结构在 src/include/list_types.h 中定义。

1
2
3
4
5
6
7
8
9
#ifndef _LIST_TYPES_H
#define _LIST_TYPES_H

struct TagListObject {
    struct TagListObject *prev;
    struct TagListObject *next;
};

#endif  /* end _LIST_TYPES_H */

此外,在 src/include/prt_list_external.h 中定义了链表各种相关操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
#ifndef PRT_LIST_EXTERNAL_H
#define PRT_LIST_EXTERNAL_H

#include "prt_typedef.h"
#include "list_types.h"

//初始化链表对象的宏,将next和prev指针都指向自身
#define LIST_OBJECT_INIT(object) { \
        &(object), &(object)       \
    }
//另一种初始化链表对象的宏,使用do-while确保安全
#define INIT_LIST_OBJECT(object)   \
    do {                           \
        (object)->next = (object); \
        (object)->prev = (object); \
    } while (0)


#define LIST_LAST(object) ((object)->prev)//获取链表最后一个元素
#define LIST_FIRST(object) ((object)->next)//获取链表第一个元素
#define OS_LIST_FIRST(object) ((object)->next)//兼容性保留

/* 将newNode插入到prev和next之间 */
OS_SEC_ALW_INLINE INLINE void ListLowLevelAdd(struct TagListObject *newNode, struct TagListObject *prev,
                                            struct TagListObject *next)
{
    newNode->next = next;
    newNode->prev = prev;
    next->prev = newNode;
    prev->next = newNode;
}

/* 在链表头部添加节点,将newNode添加到listObject之后 */
OS_SEC_ALW_INLINE INLINE void ListAdd(struct TagListObject *newNode, struct TagListObject *listObject)
{
    ListLowLevelAdd(newNode, listObject, listObject->next);
}

/* 在链表尾部添加节点,将newNode添加到listObject之前 */
OS_SEC_ALW_INLINE INLINE void ListTailAdd(struct TagListObject *newNode, struct TagListObject *listObject)
{
    ListLowLevelAdd(newNode, listObject->prev, listObject);
}

/* 底层链表删除函数,连接prevNode和nextNode */
OS_SEC_ALW_INLINE INLINE void ListLowLevelDelete(struct TagListObject *prevNode, struct TagListObject *nextNode)
{
    nextNode->prev = prevNode;
    prevNode->next = nextNode;
}

/* 删除链表节点,并清空其指针 */
OS_SEC_ALW_INLINE INLINE void ListDelete(struct TagListObject *node)
{
    ListLowLevelDelete(node->prev, node->next);

    node->next = NULL;
    node->prev = NULL;
}

/* 检查链表是否为空(只有头节点) */
OS_SEC_ALW_INLINE INLINE bool ListEmpty(const struct TagListObject *listObject)
{
    return (bool)((listObject->next == listObject) && (listObject->prev == listObject));
}

//计算结构体type中field的偏移量
#define OFFSET_OF_FIELD(type, field) ((uintptr_t)((uintptr_t)(&((type *)0x10)->field) - (uintptr_t)0x10))
//通过field成员指针获取包含该成员的type结构体指针
#define COMPLEX_OF(ptr, type, field) ((type *)((uintptr_t)(ptr) - OFFSET_OF_FIELD(type, field)))

/* 根据ptr成员地址得到type控制块首地址, ptr成员地址, type控制块结构, field成员名 */
#define LIST_COMPONENT(ptrOfList, typeOfList, fieldOfList) COMPLEX_OF(ptrOfList, typeOfList, fieldOfList)

//列表节点遍历宏
#define LIST_FOR_EACH(posOfList, listObject, typeOfList, field)                                                    \
    for ((posOfList) = LIST_COMPONENT((listObject)->next, typeOfList, field); &(posOfList)->field != (listObject); \
        (posOfList) = LIST_COMPONENT((posOfList)->field.next, typeOfList, field))

#define LIST_FOR_EACH_SAFE(posOfList, listObject, typeOfList, field)                \
    for ((posOfList) = LIST_COMPONENT((listObject)->next, typeOfList, field);       \
        (&(posOfList)->field != (listObject))&&((posOfList)->field.next != NULL);  \
        (posOfList) = LIST_COMPONENT((posOfList)->field.next, typeOfList, field))

#endif /* PRT_LIST_EXTERNAL_H */

这里面最有意思的是 LIST_COMPONENT 宏,其作用是根据成员地址得到控制块首地址, ptr成员地址, type控制块结构, field成员名。

LIST_FOR_EACH 和 LIST_FOR_EACH_SAFE 用于遍历链表,主要是简化代码编写。

[!NOTE] 例子
struct Person {
int age;
struct list_node node; // 链表节点
};

// 通过节点指针找到包含它的Person结构
struct list_node* someNode = …;
struct Person* person = LIST_COMPONENT(someNode, struct Person, node);

任务控制块

在 include目录下创建 src/include/prt_task.h  src/include/prt_task_external.h

prt_task.h 中定义了任务创建时参数传递的结构体: struct TskInitParam。
prt_task_external.h 中定义了任务调度中最重要的数据结构——任务控制块 struct TagTskCb。

将任务运行队列结构 TagOsRunQue 直接定义为双向链表 TagListObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#define TagOsRunQue TagListObject //简单实现任务运行队列

/*
* 任务线程及进程控制块的结构体统一定义。
*/
struct TagTskCb {
    /* 当前任务的SP */
    void *stackPointer;
    /* 任务状态,后续内部全改成U32 */
    U32 taskStatus;
    /* 任务的运行优先级 */
    TskPrior priority;
    /* 任务栈配置标记 */
    U16 stackCfgFlg;
    /* 任务栈大小 */
    U32 stackSize;
    TskHandle taskPid;

    /* 任务栈顶 */
    uintptr_t topOfStack;

    /* 任务入口函数 */
    TskEntryFunc taskEntry;
    /* 任务Pend的信号量指针 */
    void *taskSem;

    /* 任务的参数 */
    uintptr_t args[4];
#if (defined(OS_OPTION_TASK_INFO))
    /* 存放任务名 */
    char name[OS_TSK_NAME_LEN];
#endif
    /* 信号量链表指针 */
    struct TagListObject pendList;
    /* 任务延时链表指针 */
    struct TagListObject timerList;
    /* 持有互斥信号量链表 */
    struct TagListObject semBList;
    /* 记录条件变量的等待线程 */
    struct TagListObject condNode;
#if defined(OS_OPTION_LINUX)
    /* 等待队列指针 */
    struct TagListObject waitList;
#endif
#if defined(OS_OPTION_EVENT)
    /* 任务事件 */
    U32 event;
    /* 任务事件掩码 */
    U32 eventMask;
#endif
    /* 任务记录的最后一个错误码 */
    U32 lastErr;
    /* 任务恢复的时间点(单位Tick) */
    U64 expirationTick;

#if defined(OS_OPTION_NUTTX_VFS)
    struct filelist tskFileList;
#if defined(CONFIG_FILE_STREAM)
    struct streamlist ta_streamlist;
#endif
#endif
};

创建 src/include/prt_module.h src/include/prt_errno.h

 prt_module.h中主要是一些模块ID的定义,而 prt_errno.h 主要是错误类型的相关定义,引入这两个头文件主要是为了保持接口与原版 UniProton 相一致。

创建 src/include/prt_amp_task_internal.h

定义了三个内联函数,将任务控制块加入运行队列或者从运行运行队列中移除任务控制块,用到了我们在上面定义的双向链表的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#ifndef PRT_AMP_TASK_INTERNAL_H
#define PRT_AMP_TASK_INTERNAL_H

#include "prt_task_external.h"
#include "prt_list_external.h"

// 定义任务入队宏,将任务添加到运行队列尾部
#define OS_TSK_EN_QUE(runQue, tsk, flags)
OsEnqueueTaskAmp((runQue), (tsk))

//定义任务头部入队宏,将任务添加到运行队列头部
#define OS_TSK_EN_QUE_HEAD(runQue, tsk, flags) OsEnqueueTaskHeadAmp((runQue), (tsk))

//定义任务出队宏,将任务从运行队列中移除
#define OS_TSK_DE_QUE(runQue, tsk, flags) OsDequeueTaskAmp((runQue), (tsk))

//外部函数声明
extern U32 OsTskAMPInit(void);//声明任务调度器初始化函数
extern U32 OsIdleTskAMPCreate(void);//声明空闲任务创建函数

//将任务添加到运行队列尾部
OS_SEC_ALW_INLINE INLINE void OsEnqueueTaskAmp(struct TagOsRunQue *runQue, struct TagTskCb *tsk)
{
ListTailAdd(&tsk->pendList, runQue);
return;
}

//将任务添加到运行队列头部
OS_SEC_ALW_INLINE INLINE void OsEnqueueTaskHeadAmp(struct TagOsRunQue *runQue, struct TagTskCb *tsk)
{
ListAdd(&tsk->pendList, runQue);
return;
}

//将任务从运行队列中移除
OS_SEC_ALW_INLINE INLINE void OsDequeueTaskAmp(struct TagOsRunQue *runQue, struct TagTskCb *tsk)
{
ListDelete(&tsk->pendList);
return;
}

#endif /* PRT_AMP_TASK_INTERNAL_H */

任务创建

以下代码若无代码块外的特殊声明,都是写在 src/kernel/task/prt_task_init.c 中

相关变量与函数声明

首先是引入必要的头文件。

然后声明了 1 个全局双向链表g_tskCbFreeList,并通过 LIST_OBJECT_INIT 宏进行初始化。 g_tskCbFreeList 链表是空闲的任务控制块链表。

最后声明了3个外部函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "list_types.h"
#include "os_attr_armv8_external.h"
#include "prt_list_external.h"
#include "prt_task.h"
#include "prt_task_external.h"
#include "prt_asm_cpu_external.h"
#include "os_cpu_armv8_external.h"
#include "prt_config.h"

/* Unused TCBs and ECBs that can be allocated. 声明全局双向链表*/
OS_SEC_DATA struct TagListObject g_tskCbFreeList = LIST_OBJECT_INIT(g_tskCbFreeList);

//声明三个外部函数
extern U32 OsTskAMPInit(void);//AMP任务初始化
extern U32 OsIdleTskAMPCreate(void);//ilde任务创建
extern void OsFirstTimeSwitch(void);//系统启动时的首次任务调度

其中头文件 src/include/prt_asm_cpu_external.h [下载] 包含内核相关的一些状态定义。

极简内存空间管理

内核运行过程中需要动态分配内存。我们实现了一种极简的内存管理,该内存管理方法仅支持4K大小,最多256字节对齐空间的分配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
//简单实现OsMemAllocAlign
/*
* 描述:分配任务栈空间
* 仅支持4K大小,最多256字节对齐空间的分配
*/
uint8_t stackMem[20][4096] __attribute__((aligned(256))); // 256 字节对齐,20 个 4K 大小的空间
uint8_t stackMemUsed[20] = {0}; // 记录对应 4K 空间是否已被分配
OS_SEC_TEXT void *OsMemAllocAlign(U32 mid, U8 ptNo, U32 size, U8 alignPow)
{
    // 最多支持256字节对齐
    if (alignPow > 8)
        return NULL;
    if (size != 4096)
        return NULL;
    for(int i = 0; i < 20; i++){
        if (stackMemUsed[i] == 0){
            stackMemUsed[i] = 1; // 记录对应 4K 空间已被分配
            return &(stackMem[i][0]); // 返回 4K 空间起始地址
        }
    }
    return NULL;
}

/*
* 描述:分配任务栈空间
*/
OS_SEC_L4_TEXT void *OsTskMemAlloc(U32 size)
{
    void *stackAddr = NULL;
        stackAddr = OsMemAllocAlign((U32)OS_MID_TSK, (U8)0, size,
                                /* 内存已按16字节大小对齐 */
                                OS_TSK_STACK_SIZE_ALLOC_ALIGN);
    return stackAddr;
}

[!info]

OsMemAllocAlign 函数用于分配任务栈空间。它接受四个参数:

mid:用于标识内存的类型。
ptNo:保留参数,未被使用。
size:期望的分配大小,这里固定为 4096 字节(4K)。
alignPow:对齐的幂次,最多支持 256 字节对齐,即对齐幂次为8。

OsTskMemAlloc 函数是一个简单的封装函数,用于分配任务栈空间。它调用OsMemAllocAlign 函数来获取任务栈空间,其中 OS_TSK_STACK_SIZE_ALLOC_ALIGN是一个
预定义的值,代表已经按照 16 字节大小对齐。返回分配空间的起始地址

任务栈初始化

我们手工制造一个任务栈就可以了。下面代码中 stack->x01 到 stack->x29 被初始化成很有标志性意义的值,其他他们的值不重要。比较重要的是 stack->x30 和 stack->spsr 等处的值。

struct TskContext 表示任务上下文,放在 src/bsp/os_cpu_armv8.h 中定义。在我们的实现上它与中断上下文 struct ExcRegInfo (在 src/bsp/os_exc_armv8.h 中定义)没有区别。在UniProton中,它们的定义有一些差别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/*
* 描述: 初始化任务栈的上下文
*/
void *OsTskContextInit(U32 taskID, U32 stackSize, uintptr_t *topStack, uintptr_t funcTskEntry)
{
    (void)taskID;
    struct TskContext *stack = (struct TskContext *)((uintptr_t)topStack + stackSize);//从栈顶加上栈大小得到栈底地址

    stack -= 1; // 指针减,减去一个TskContext大小

//初始化arm架构的通用寄存器x0-x30
//x30设置为任务入口地址
//xzr始终为0
    stack->x00 = 0;
    stack->x01 = 0x01010101;
    stack->x02 = 0x02020202;
    stack->x03 = 0x03030303;
    stack->x04 = 0x04040404;
    stack->x05 = 0x05050505;
    stack->x06 = 0x06060606;
    stack->x07 = 0x07070707;
    stack->x08 = 0x08080808;
    stack->x09 = 0x09090909;
    stack->x10 = 0x10101010;
    stack->x11 = 0x11111111;
    stack->x12 = 0x12121212;
    stack->x13 = 0x13131313;
    stack->x14 = 0x14141414;
    stack->x15 = 0x15151515;
    stack->x16 = 0x16161616;
    stack->x17 = 0x17171717;
    stack->x18 = 0x18181818;
    stack->x19 = 0x19191919;
    stack->x20 = 0x20202020;
    stack->x21 = 0x21212121;
    stack->x22 = 0x22222222;
    stack->x23 = 0x23232323;
    stack->x24 = 0x24242424;
    stack->x25 = 0x25252525;
    stack->x26 = 0x26262626;
    stack->x27 = 0x27272727;
    stack->x28 = 0x28282828;
    stack->x29 = 0x29292929;
    stack->x30 = funcTskEntry;   // x30: lr(link register)
    stack->xzr = 0;

    stack->elr = funcTskEntry;//异常返回时跳转到任务入口地址
    stack->esr = 0;//异常状态寄存器,异常的类型和原因
    stack->far = 0;//错误地址寄存器,确定哪个地址导致了异常
    //spsr设置为此值,表示使用el1的sp1栈指针,用于保存处理器在异常发生时的程序状态
    //禁用调试、SError、IRQ和FIQ中断
    stack->spsr = 0x305;    // EL1_SP1 | D | A | I | F
    return stack;

}

[!info]
void *OsTskContextInit(U32 taskID, U32 stackSize, uintptr_t *topStack, uintptr_t funcTskEntry)

  • ​taskID​​: 任务ID(当前未使用)
  • ​stackSize​​: 栈大小
  • ​topStack​​: 栈顶指针
  • ​funcTskEntry​​: 任务入口函数地址
  • ​返回值​​: 初始化后的栈指针位置
  • 此处重要的是寄存器x30将传入的funcTskEntry设置为该任务入口地址,spsr寄存器(异常发生时的程序状态))

在 src/bsp/os_cpu_armv8.h 中加入 struct TskContext 定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/*
* 任务上下文的结构体定义。
*/
struct TskContext {
    /* *< 当前物理寄存器R0-R12 */
    uintptr_t elr;               // 返回地址
    uintptr_t spsr;
    uintptr_t far;
    uintptr_t esr;
    uintptr_t xzr;
    uintptr_t x30;
    uintptr_t x29;
    uintptr_t x28;
    uintptr_t x27;
    uintptr_t x26;
    uintptr_t x25;
    uintptr_t x24;
    uintptr_t x23;
    uintptr_t x22;
    uintptr_t x21;
    uintptr_t x20;
    uintptr_t x19;
    uintptr_t x18;
    uintptr_t x17;
    uintptr_t x16;
    uintptr_t x15;
    uintptr_t x14;
    uintptr_t x13;
    uintptr_t x12;
    uintptr_t x11;
    uintptr_t x10;
    uintptr_t x09;
    uintptr_t x08;
    uintptr_t x07;
    uintptr_t x06;
    uintptr_t x05;
    uintptr_t x04;
    uintptr_t x03;
    uintptr_t x02;
    uintptr_t x01;
    uintptr_t x00;
};

任务入口函数

OsTskEntry 所有任务的入口函数

这个函数有几个有趣的地方。
(1)==你找不到类似 OsTskEntry(taskId); 这样的对 OsTskEntry 的函数调用。这实际上是在通过 OsTskContextInit 函数进行栈初始化时传入的,也就意味着当任务第一次就绪运行时会进入 OsTskEntry 执行。==
(2)用户指定的 taskcb->taskEntry 不一定要求是 4 参数的,可以是 0~4 参数之间任意选定,这个需要你在汇编层面去理解。

采用 OsTskEntry 的好处是在用户提供的 taskCb->taskEntry 函数的基础上进行了一层封装,比如可以确保调用taskCb->taskEntry执行完后调用 OsTaskExit,回收系统资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
* 描述:所有任务入口
*/
OS_SEC_L4_TEXT void OsTskEntry(TskHandle taskId)
{
    struct TagTskCb *taskCb;//指向任务控制块
    uintptr_t intSave;//保存中断状态

    (void)taskId;//忽略未使用的参数,避免编译器警告

    taskCb = RUNNING_TASK;//获取当前运行任务

//调用任务实际入口函数
    taskCb->taskEntry(taskCb->args[OS_TSK_PARA_0], taskCb->args[OS_TSK_PARA_1], taskCb->args[OS_TSK_PARA_2],
                    taskCb->args[OS_TSK_PARA_3]);

    // 调度结束后会开中断,所以不需要自己添加开中断
    intSave = OsIntLock();//保存当前中断状态并禁用中断

    OS_TASK_LOCK_DATA = 0; //重置任务锁

    /* PRT_TaskDelete不能关中断操作,否则可能会导致它核发SGI等待本核响应时死等 */
    OsIntRestore(intSave); //恢复之前保存的中断状态

    OsTaskExit(taskCb);

}

[!info]

  1. ​任务执行流程​​:

    • 所有任务都通过这个统一入口启动
    • 实际任务代码通过taskEntry指针调用
    • 支持最多4个参数传递
  2. ​中断处理​​:

    • 使用OsIntLock/OsIntRestore保护临界区
    • 注释指出调度器会处理中断状态,所以这里只是确保OS_TASK_LOCK_DATA的原子修改

创建任务

接口函数 PRT_TaskCreate

接口函数 PRT_TaskCreate 函数根据传入的 initParam 参数创建任务返回任务句柄 taskPid。

1
2
3
4
5
6
7
/*
* 描述:创建一个任务但不进行激活
*/
OS_SEC_L4_TEXT U32 PRT_TaskCreate(TskHandle *taskPid, struct TskInitParam *initParam)
{
    return OsTaskCreateOnly(taskPid, initParam);
}

PRT_TaskCreate 函数会直接调用 OsTaskCreateOnly 函数实际进行任务创建。

OsTaskCreateOnly 任务创建但不直接运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
OS_SEC_L4_TEXT U32 OsTaskCreateOnly(TskHandle *taskPid, struct TskInitParam *initParam)
{
    U32 ret;
    U32 taskId;
    uintptr_t intSave;
    uintptr_t *topStack = NULL;
    void *stackPtr = NULL;
    struct TagTskCb *taskCb = NULL;
    uintptr_t curStackSize = 0;


    intSave = OsIntLock();//保存中断状态并禁止中断,保证创建任务的过程是原子操作
    // 获取一个空闲的任务控制块
    ret = OsTaskCreateChkAndGetTcb(&taskCb);//获取一个空闲的任务控制块(tcb)
    if (ret != OS_OK) {//如果分配失败
        OsIntRestore(intSave);//恢复中断状态
        return ret;//返回错误码
    }


    taskId = taskCb->taskPid;//获取tcb记录的pid
    // 分配堆栈空间资源
    ret = OsTaskCreateRsrcInit(taskId, initParam, taskCb, &topStack, &curStackSize);
    if (ret != OS_OK) {
        ListAdd(&taskCb->pendList, &g_tskCbFreeList);//如果失败将tcb放回freelist
        OsIntRestore(intSave);//恢复中断状态
        return ret;//返回错误码
    }
    // 任务上下文栈初始化,就像刚发生过中断一样 设置任务的入口点为OsTskEntry
    stackPtr = OsTskContextInit(taskId, curStackSize, topStack, (uintptr_t)OsTskEntry);
    // 任务控制块初始化
    OsTskCreateTcbInit((uintptr_t)stackPtr, initParam, (uintptr_t)topStack, curStackSize, taskCb);


    //设置任务状态为挂起和状态使用
    taskCb->taskStatus = OS_TSK_SUSPEND | OS_TSK_INUSE;
   
    // 出参ID传出
    *taskPid = taskId; //通过taskPID 返回任务ID
    OsIntRestore(intSave);//恢复中断状态
    return OS_OK;//返回成功码

}

OsTaskCreateOnly 函数将:

  • 通过 OsTaskCreateChkAndGetTcb 函数从空闲链表 g_tskCbFreeList 中取一个任务控制块;

  • 在 OsTaskCreateRsrcInit 函数中,如果用户未提供堆栈空间,则通过 OsTskMemAlloc 为新建的任务分配堆栈空间;

  • OsTskContextInit 函数负责将栈初始化成刚刚发生过中断一样;

  • OsTskCreateTcbInit 函数负责用 initParam 参数等初始化任务控制块,包括栈指针、入口函数、优先级和参数等;

  • 最后将任务的状态设置为挂起 Suspend 状态。这意味着 PRT_TaskCreate 创建任务后处于 Suspend 状态,而不是就绪状态。

OsTaskCreateChkAndGetTcb 从tcb空闲列表中获取一个任务控制块

1
2
3
4
5
6
7
8
9
10
11
12
13
OS_SEC_ALW_INLINE INLINE U32 OsTaskCreateChkAndGetTcb(struct TagTskCb **taskCb)
{
    if (ListEmpty(&g_tskCbFreeList)) {
        return OS_ERRNO_TSK_TCB_UNAVAILABLE;
    }

    // 先获取到该控制块
    *taskCb = GET_TCB_PEND(OS_LIST_FIRST(&g_tskCbFreeList));
    // 成功,从空闲列表中移除
    ListDelete(OS_LIST_FIRST(&g_tskCbFreeList));

    return OS_OK;
}

OsTaskCreateRsrcInit 为任务分配堆栈空间

initParam 结构体用于传参,指定该任务创建的参数
topStackOut 指向一个指针的指针,用于输出任务栈的栈顶地址
curStackSize 指向一个无符号整型变量的指针,用于输出当前任务栈的大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
 OS_SEC_L4_TEXT U32 OsTaskCreateRsrcInit(U32 taskId, struct TskInitParam *initParam, struct TagTskCb *taskCb,uintptr_t **topStackOut, uintptr_t *curStackSize)
{
    U32 ret = OS_OK;
    uintptr_t *topStack = NULL;

    /* 查看用户是否配置了任务栈,如没有,则进行内存申请,并标记为系统配置,如有,则标记为用户配置。 */
    if (initParam->stackAddr != 0) { //用户配置了任务栈
        topStack = (void *)(initParam->stackAddr);
        taskCb->stackCfgFlg = OS_TSK_STACK_CFG_BY_USER;//标记为用户配置
    } else { //用户没有配置任务栈
        topStack = OsTskMemAlloc(initParam->stackSize);//分配栈
        if (topStack == NULL) {
            ret = OS_ERRNO_TSK_NO_MEMORY;//空间不够,返回错误码
        } else {
            taskCb->stackCfgFlg = OS_TSK_STACK_CFG_BY_SYS;//标记为系统配置
        }
    }
    *curStackSize = initParam->stackSize;//返回栈大小
    if (ret != OS_OK) {
        return ret;
    }

    *topStackOut = topStack;//返回栈指针
    return OS_OK;

}

OsTskContextInit 初始化任务栈的上下文

函数解释在任务栈初始化,这里不再赘述

OsTskCreateTcbInit 负责用 initParam 参数等初始化tcb

stackPtr 指向任务栈的指针
initParam 指向初始化参数的指针
topStackAddr 任务栈顶地址
curStackSize 当前任务栈大小
taskCb 指向任务tcb的指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
OS_SEC_L4_TEXT void OsTskCreateTcbInit(uintptr_t stackPtr, struct TskInitParam *initParam,
    uintptr_t topStackAddr, uintptr_t curStackSize, struct TagTskCb *taskCb)
{
    /* Initialize the task's stack */
    taskCb->stackPointer = (void *)stackPtr;//设置任务栈指针
    //初始化任务参数
    taskCb->args[OS_TSK_PARA_0] = (uintptr_t)initParam->args[OS_TSK_PARA_0];
    taskCb->args[OS_TSK_PARA_1] = (uintptr_t)initParam->args[OS_TSK_PARA_1];
    taskCb->args[OS_TSK_PARA_2] = (uintptr_t)initParam->args[OS_TSK_PARA_2];
    taskCb->args[OS_TSK_PARA_3] = (uintptr_t)initParam->args[OS_TSK_PARA_3];
    taskCb->topOfStack = topStackAddr;
    taskCb->stackSize = curStackSize;
    taskCb->taskSem = NULL;//任务pend的信号量指针
    taskCb->priority = initParam->taskPrio;//任务的运行优先级
    taskCb->taskEntry = initParam->taskEntry;//任务的入口函数
#if defined(OS_OPTION_EVENT)
    taskCb->event = 0;//任务事件
    taskCb->eventMask = 0;//任务事件掩码
#endif
    taskCb->lastErr = 0;//任务记录的最后一个错误码
//初始化持有互斥信号量链表,信号量链表,任务延时链表
    INIT_LIST_OBJECT(&taskCb->semBList);
    INIT_LIST_OBJECT(&taskCb->pendList);
    INIT_LIST_OBJECT(&taskCb->timerList);
    return;
}

解挂任务

PRT_TaskResume 函数 负责解挂任务

负责解挂任务,即将 Suspend 状态的任务转换到就绪状态。
PRT_TaskResume 首先检查当前任务是否已创建且处于 Suspend 状态,如果处于 Suspend 状态,则清除 Suspend 位,然后调用 OsMoveTaskToReady 将任务控制块移到就绪队列中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
OS_SEC_L2_TEXT U32 PRT_TaskResume(TskHandle taskPid)
{
    uintptr_t intSave;
    struct TagTskCb *taskCb = NULL;

    // 获取 taskPid 对应的任务控制块
    taskCb = GET_TCB_HANDLE(taskPid);

    intSave = OsIntLock();//中断保护

    //如果任务未被创建,恢复中断状态并返回错误码
    if (TSK_IS_UNUSED(taskCb)) {
        OsIntRestore(intSave);
        return OS_ERRNO_TSK_NOT_CREATED;
    }

    //检查任务是否正在运行且全局任务锁(g_uniTaskLock)被锁定
    if (((OS_TSK_RUNNING & taskCb->taskStatus) != 0) && (g_uniTaskLock != 0)) {
        OsIntRestore(intSave);
        return OS_ERRNO_TSK_ACTIVE_FAILED;
    }

    //检查任务是否既没有被挂起(OS_TSK_SUSPEND)也不处于可中断的延迟状态
    if (((OS_TSK_SUSPEND | OS_TSK_DELAY_INTERRUPTIBLE) & taskCb->taskStatus) == 0) {
        OsIntRestore(intSave);
        return OS_ERRNO_TSK_NOT_SUSPENDED;
    }
    //清除挂起状态标志位
    TSK_STATUS_CLEAR(taskCb, OS_TSK_SUSPEND);

    //将任务移到就绪队列
    OsMoveTaskToReady(taskCb);
    OsIntRestore(intSave);//恢复中断状态

    return OS_OK;

}

OsMoveTaskToReady 将任务加入就绪队列 g_runQueue

然后通过 OsTskSchedule 进行任务调度和切换(稍后描述)。 由于有新的任务就绪,所以需要通过OsTskSchedule 进行调度。这个位置一般称为调度点。对于优先级调度来说,找到所有的调度点并进行调度非常重要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
OS_SEC_ALW_INLINE INLINE void OsMoveTaskToReady(struct TagTskCb *taskCb)
{
    //检查任务是否处于可中断的延迟状态
    if (TSK_STATUS_TST(taskCb, OS_TSK_DELAY_INTERRUPTIBLE)) {
        /* 可中断delay, 属于定时等待的任务时候,去掉其定时等待标志位*/
        //如果任务同时有超时标志(OS_TSK_TIMEOUT)
        if (TSK_STATUS_TST(taskCb, OS_TSK_TIMEOUT)) {
            //调用OS_TSK_DELAY_LOCKED_DETACH将其从延迟队列中移除
            OS_TSK_DELAY_LOCKED_DETACH(taskCb);
        }
        //清除状态标志
        TSK_STATUS_CLEAR(taskCb, OS_TSK_TIMEOUT | OS_TSK_DELAY_INTERRUPTIBLE);
    }

    //如果任务没有被阻塞
    if ((taskCb->taskStatus & OS_TSK_BLOCK) == 0) {
        OsTskReadyAdd(taskCb);//加入就绪队列
        //检查后台活动标志,如果设置了就调度
        if ((OS_FLG_BGD_ACTIVE & UNI_FLAG) != 0) {
            OsTskSchedule();
            return;
        }
    }
}

任务管理系统初始化与启动

OsTskInit 函数通过调用 OsTskAMPInit 函数完成任务管理系统的初始化。主要包括:

  • 为任务控制块分配空间,由于我们只实现了简单的内存分配算法,所以支持的任务控制块数目为:4096 / sizeof(struct TagTskCb) - 2; 减去2是因为预留了 1 个空闲任务, 1 个无效任务。

  • 将所有分配的任务控制块加入空闲任务控制块链表 g_tskCbFreeList, 并对所有控制块进行初始化。

  • 任务就绪链表 g_runQueue 通过 INIT_LIST_OBJECT 初始化为空。

  • RUNNING_TASK 目前指向无效任务。

OsActivate 启动多任务系统。

  • 首先通过 OsIdleTskAMPCreate 函数创建空闲任务,这样当系统中没有其他任务就绪时就可以执行空闲任务了。

  • OsTskHighestSet 函数在就绪队列中查找最高优先级任务并将 g_highestTask 指针指向该任务。

  • UNI_FLAG 设置好内核状态

  • OsFirstTimeSwitch 函数将会加载 g_highestTask 的上下文后执行(稍后描述)。


在 prt_config.h 中加入空闲任务优先级定义。

1
#define OS_TSK_PRIORITY_LOWEST 63

​函数功能分析与总结​

​1. OsTskAMPInit()

  • ​功能​​:
    初始化 AMP(Asymmetric Multi-Processing,非对称多处理)任务管理系统,包括任务控制块(TCB)数组、空闲任务和无效任务的管理。

  • ​关键操作​​:

    • ​分配 TCB 数组​​:
      • 使用 OsMemAllocAlign 分配 4096 字节的内存,用于存储 OS_MAX_TCB_NUM 个任务控制块。
      • 如果分配失败,返回 OS_ERRNO_TSK_NO_MEMORY
    • ​计算最大任务数​​:
      • g_tskMaxNum = 4096 / sizeof(struct TagTskCb) - 2,预留 2 个 TCB(1 个空闲任务,1 个无效任务)。
    • ​初始化 TCB 数组​​:
      • 清零所有 TCB,并设置初始状态为 OS_TSK_UNUSED
      • 为每个 TCB 分配任务 ID(taskPid = idx + g_tskBaseId)。
      • 将所有 TCB 加入 g_tskCbFreeList 链表(空闲任务链表)。
    • ​初始化运行队列​​:
      • INIT_LIST_OBJECT(&g_runQueue) 初始化就绪队列。
    • ​设置当前运行任务​​:
      • RUNNING_TASK 初始化为无效任务(OS_PST_ZOMBIE_TASK)。
      • 设置 RUNNING_TASK 的状态为 OS_TSK_INUSE | OS_TSK_RUNNING,优先级为 OS_TSK_PRIORITY_LOWEST + 1
  • ​返回值​​:

    • OS_OK 表示初始化成功,否则返回错误码。

​2. OsTskInit()

  • ​功能​​:
    任务管理系统的总初始化函数,调用 OsTskAMPInit() 完成 TCB 数组的初始化。

  • ​关键操作​​:

    • 调用 OsTskAMPInit() 初始化任务控制块数组。
    • 如果失败,直接返回错误码。
  • ​返回值​​:

    • OS_OK 表示成功,否则返回 OsTskAMPInit() 的错误码。

​3. OsTskIdleBgd()

  • ​功能​​:
    空闲任务(Idle Task)的执行函数,通常是一个无限循环,用于在无其他任务运行时占用 CPU。

  • ​关键操作​​:

    • while (TRUE); 无限循环,不做任何实际工作。
  • ​用途​​:

    • 确保 CPU 不会进入空转状态,同时可以用于低功耗管理(如 WFI 指令)。

​4. OsIdleTskAMPCreate()

  • ​功能​​:
    创建空闲任务(Idle Task),并使其进入就绪状态。

  • ​关键操作​​:

    • ​初始化任务参数​​:
      • taskEntry = OsTskIdleBgd(任务入口函数)。
      • stackSize = 4096(栈大小)。
      • taskPrio = OS_TSK_PRIORITY_LOWEST(最低优先级)。
    • ​创建任务​​:
      • 调用 PRT_TaskCreate() 创建任务。
      • 如果失败,返回错误码。
    • ​恢复任务​​:
      • 调用 PRT_TaskResume() 使任务进入就绪状态。
    • ​记录空闲任务 ID​​:
      • IDLE_TASK_ID = taskHdl
  • ​返回值​​:

    • OS_OK 表示成功,否则返回 PRT_TaskCreate() 或 PRT_TaskResume() 的错误码。

​5. OsActivate()

  • ​功能​​:
    激活任务管理系统,启动多任务调度。

  • ​关键操作​​:

    • ​创建空闲任务​​:
      • 调用 OsIdleTskAMPCreate() 创建 Idle Task。
      • 如果失败,返回错误码。
    • ​设置最高优先级任务​​:
      • OsTskHighestSet() 设置当前最高优先级任务。
    • ​设置系统标志​​:
      • UNI_FLAG |= OS_FLG_BGD_ACTIVE | OS_FLG_TSK_REQ,表示后台任务已激活,并请求任务调度。
    • ​启动任务调度​​:
      • OsFirstTimeSwitch() 加载 g_highestTask 的上下文,并开始执行任务。
      • 正常情况下,此函数不会返回(因为任务调度接管 CPU)。
      • 如果返回,说明任务激活失败,返回 OS_ERRNO_TSK_ACTIVE_FAILED
  • ​返回值​​:

    • OS_OK 表示成功(理论上不会返回)。
    • 失败时返回 OsIdleTskAMPCreate() 的错误码或 OS_ERRNO_TSK_ACTIVE_FAILED

任务状态切换

在 src/kernel/task/prt_task.c 中,

  • 声明了运行队列 g_runQueue, 注意我们之前已经将其定义为双向队列。

  • 提供了将任务添加到就绪队列的 OsTskReadyAdd 函数和从就绪队列中移除就绪队列的 OsTskReadyDel 函数。

    • OsTskReadyAdd 会设置任务为就绪态

      • 首先获取全局运行队列g_runQueue,然后设置任务的状态为就绪 (OS_TSK_READY),并把任务添加到运行队列中,最后调用OsTskHighestSet()将g_highestTask 指针指向最高优先级任务(每当就绪队列中的任务发生变化时,要重新找到当前最高优先级的任务)。
    • OsTskReadyDel 会清除任务的就绪态

      • 首先获取全局运行队列g_runQueue,然后清除任务的就绪状态 (OS_TSK_READY),并从运行队列中移除该任务。最后,它同样调用OsTskHighestSet()将g_highestTask 指针指向最高优先级任务。
  • 提供了任务结束退出 OsTaskExit 函数,注意 OsTskEntry 中会调用 OsTaskExit 函数。由于任务退出,因此需要进行调度,即存在调度点,所以调用 OsTskSchedule 函数。

    • 其首先锁定中断(防止退出过程引发中断),然后调用OsTskReadyDel()将任务从就绪队列中移除,最后调用OsTskSchedule()进行任务调度(因为一个任务运行结束之后,需要陷入操作系统来引发调度),最后恢复中断。

其中,OS_TSK_EN_QUE 和 OS_TSK_DE_QUE 宏在 src/include/prt_amp_task_internal.h 定义。

调度与切换

src/kernel/sched/prt_sched_single.c

OsTskSchedule 任务调度,切换到最高优先级任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
OS_SEC_TEXT void OsTskSchedule(void)
{
    /* 外层已经关中断 */
    /* Find the highest task */
    OsTskHighestSet();//设置 g_highestTask 为当前最高优先级任务

    //如果当前运行的任务不是最高优先级任务且任务调度未被锁定
    if ((g_highestTask != RUNNING_TASK) && (g_uniTaskLock == 0)) {
        UNI_FLAG |= OS_FLG_TSK_REQ;//设置任务调度请求标志

        /* only if there is not HWI or TICK the trap */
        if (OS_INT_INACTIVE) { // 不在中断上下文中,否则应该在中断返回时切换
            OsTaskTrap();//触发任务切换
            return;
        }
    }

    return;
}

OsMainSchedule 调度的主入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
OS_SEC_L0_TEXT void OsMainSchedule(void)
{
    struct TagTskCb *prevTsk;
    if ((UNI_FLAG & OS_FLG_TSK_REQ) != 0) {//检查是否有调度请求
        prevTsk = RUNNING_TASK;//保存当前运行的任务,用于上下文保存

        /* 清除OS_FLG_TSK_REQ标记位 */
        UNI_FLAG &= ~OS_FLG_TSK_REQ;//​​OS_FLG_TSK_REQ​​:如果设置了该标志,说明需要调度。

        RUNNING_TASK->taskStatus &= ~OS_TSK_RUNNING;// 清除当前任务的运行状态
        g_highestTask->taskStatus |= OS_TSK_RUNNING;// 设置新任务的运行状态

        RUNNING_TASK = g_highestTask;// 更新当前运行任务
    }
    // 如果中断没有驱动一个任务ready,直接回到被打断的任务
    OsTskContextLoad((uintptr_t)RUNNING_TASK);// 加载新任务的上下文
}

OsFirstTimeSwitch 系统启动时的首次任务调度

1
2
3
4
5
6
7
8
9
OS_SEC_L4_TEXT void OsFirstTimeSwitch(void)
{
    OsTskHighestSet(); // 设置 g_highestTask 为最高优先级任务
    RUNNING_TASK = g_highestTask;// 设置当前运行任务未最高优先级任务
    TSK_STATUS_SET(RUNNING_TASK, OS_TSK_RUNNING);// 设置任务状态为运行中
    OsTskContextLoad((uintptr_t)RUNNING_TASK);// 加载任务上下文
    // never get here
    return;
}

OsTskHighestSet 设置 g_highestTask 为最高优先级任务

在 src/include/prt_task_external.h 中被定义为内联函数,提高性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
OS_SEC_ALW_INLINE INLINE void OsTskHighestSet(void)
{
    struct TagTskCb *taskCb = NULL;
    struct TagTskCb *savedTaskCb = NULL;

    // 遍历g_runQueue队列,查找优先级最高的任务
    LIST_FOR_EACH(taskCb, &g_runQueue, struct TagTskCb, pendList) {
        // 第一个任务,直接保存到savedTaskCb
        if(savedTaskCb == NULL) {
            savedTaskCb = taskCb;
            continue;
        }
        // 比较优先级,值越小优先级越高
        if(taskCb->priority < savedTaskCb->priority){
            savedTaskCb = taskCb;
        }
    }

    g_highestTask = savedTaskCb;
}

实现 OsTskContextLoad,OsContextLoad 和 OsTaskTrap

在 src/bsp/prt_vector.S 实现 OsTskContextLoad,OsContextLoad 和 OsTaskTrap。

OsTskContextLoad 从任务的栈指针(stackPointer)恢复任务上下文,并执行 eret 返回任务。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* 描述: void OsTskContextLoad(uintptr_t stackPointer)
*/
.globl OsTskContextLoad //声明为全局符号,可被外部调用。
.type OsTskContextLoad, @function
.align 4
OsTskContextLoad:
ldr X0, [X0] //从传入的参数加载任务栈指针
mov SP, X0 // X0 is stackPointer

OsContextLoad:
ldp x2, x3, [sp],#16 // 从栈加载 x2=ELR_EL1(返回地址),x3=SPSR_EL1(状态寄存器
//加载后栈顶弹出16字节
add sp, sp, #16 // 跳过far, esr, HCR_EL2.TRVM==1的时候,EL1不能写far, esr
msr spsr_el1, x3 // 恢复 SPSR_EL1(任务状态)
msr elr_el1, x2// 恢复 ELR_EL1(任务返回地址)
dsb sy// 数据同步屏障(确保内存操作完成)
isb// 指令同步屏障(确保指令流正确)

RESTORE_EXC_REGS // 恢复上下文

eret //从异常返回

OsTaskTrap 保存当前任务上下文,并跳转到 OsMainSchedule 进行任务切换
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
* 描述: Task调度处理函数。 X0 is g_runningTask
*/
.globl OsTaskTrap
.type OsTaskTrap, @function
.align 4

OsTaskTrap:
LDR x1, =g_runningTask /* OsTaskTrap是函数调用过来,x0 x1寄存器是caller save,此处能直接使用 x*/
LDR x0, [x1] /* x0 is the &g_pRunningTask->sp x0为当前任务栈指针地址 */

SAVE_EXC_REGS //保存所有通用寄存器

/* TskTrap需要保存CPSR,由于不能直接访问,需要拼接获取当前CPSR入栈 */
mrs x3, DAIF /* CPSR:DAIF 4种事件的mask, bits[9:6] */
mrs x2, NZCV /* NZCV:Condition flags, bits[31:28] */
orr x3, x3, x2
orr x3, x3, #(0x1U << 2) /* 当前的 exception level,bits[3:2] 00:EL0,01:El1,10:El2,11:EL3 */
orr x3, x3, #(0x1U) /* 当前栈的选择,bits[0] 0:SP_EL0,1:SP_ELX */
//合并 `DAIF` 和 `NZCV`,构造 `SPSR_EL1`。

mov x2, x30 // 用返回地址x30作为现场恢复点
sub sp, sp, #16 // 跳过esr_el1, far_el1, 异常时才有用
stp x2, x3, [sp,#-16]! // 压栈 x2=LR(返回地址),x3=SPSR_EL1(状态)

// 存入SP指针到g_pRunningTask->sp
mov x1, sp
str x1, [x0] // x0 is the &g_pRunningTask->sp 更新任务栈指针

B OsMainSchedule//跳转到调度器
loop1:
B loop1
剩下的一点7788

在 src/bsp/os_cpu_armv8_external.h 加入 OsTaskTrap 和 OsTskContextLoad 的声明和关于栈地址和大小对齐宏。

1
2
3
4
5
6
7
8
9


#define OS_TSK_STACK_SIZE_ALIGN  16U
#define OS_TSK_STACK_SIZE_ALLOC_ALIGN 4U //按2的幂对齐,即2^4=16字节
#define OS_TSK_STACK_ADDR_ALIGN  16U

extern void OsTaskTrap(void);
extern void OsTskContextLoad(uintptr_t stackPointer);

最后在 src/kernel/task/prt_sys.c 定义了内核的各种全局数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "prt_typedef.h"
#include "os_attr_armv8_external.h"
#include "prt_task.h"

OS_SEC_L4_BSS U32 g_threadNum;

/* Tick计数 */
// OS_SEC_BSS U64 g_uniTicks; // 把 lab5 中在 src/kernel/tick/prt_tick.c 定义的 g_uniTicks 移到此处则取消此行的注释


/* 系统状态标志位 */
OS_SEC_DATA U32 g_uniFlag = 0;

OS_SEC_DATA struct TagTskCb *g_runningTask = NULL;


// src/core/kernel/task/prt_task_global.c
OS_SEC_BSS TskEntryFunc g_tskIdleEntry;


OS_SEC_BSS U32 g_tskMaxNum;
OS_SEC_BSS struct TagTskCb *g_tskCbArray;
OS_SEC_BSS U32 g_tskBaseId;

OS_SEC_BSS TskHandle g_idleTaskId;
OS_SEC_BSS U16 g_uniTaskLock;
OS_SEC_BSS struct TagTskCb *g_highestTask;

任务调度测试

运行测试程序

作业

实现分时调度

时间片轮转调度

  • 修改任务调度函数OsTskSchedule,将OsTskHighestSet()改为OsTskRR(),fifo对就绪队列进行处理,最高优先级任务设为队首元素

  • OsTskRR() prt_task_external.h
    这里写的比较笨,大概意思就是用fifo策略更新队列,调度队首元素

  • 调度点:一定的tick过后触发调度,显然我们可以在时钟中断处理函数中加一些操作
    我们在OsTickDispatcher中新增OsTimerInterrupt();

  • 新增定时器中断处理函数OsTimerInterrupt(); prt_fifo.c
    每当我们的tick增加一个时间片长度,就进行进程切换,这里要注意,在任务队列为空时,我们不执行任务调度,否则会异常。

    定义时间片长度

  • 修改main.c使得效果更加明显

    Delay函数用于在指定的毫秒数内阻塞当前任务。它通过一个while循环和一个计时器实现,直到达到指定的延迟时间。

  • 输出如下

    相当不错呀~