实验六 任务调度

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

基础数据结构:双向链表

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

1#ifndef _LIST_TYPES_H
2#define _LIST_TYPES_H
3
4struct TagListObject {
5    struct TagListObject *prev;
6    struct TagListObject *next;
7};
8
9#endif  /* end _LIST_TYPES_H */

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

 1#ifndef PRT_LIST_EXTERNAL_H
 2#define PRT_LIST_EXTERNAL_H
 3
 4#include "prt_typedef.h"
 5#include "list_types.h"
 6
 7#define LIST_OBJECT_INIT(object) { \
 8        &(object), &(object)       \
 9    }
10
11#define INIT_LIST_OBJECT(object)   \
12    do {                           \
13        (object)->next = (object); \
14        (object)->prev = (object); \
15    } while (0)
16
17#define LIST_LAST(object) ((object)->prev)
18#define LIST_FIRST(object) ((object)->next)
19#define OS_LIST_FIRST(object) ((object)->next)
20
21/* list action low level add */
22OS_SEC_ALW_INLINE INLINE void ListLowLevelAdd(struct TagListObject *newNode, struct TagListObject *prev,
23                                            struct TagListObject *next)
24{
25    newNode->next = next;
26    newNode->prev = prev;
27    next->prev = newNode;
28    prev->next = newNode;
29}
30
31/* list action add */
32OS_SEC_ALW_INLINE INLINE void ListAdd(struct TagListObject *newNode, struct TagListObject *listObject)
33{
34    ListLowLevelAdd(newNode, listObject, listObject->next);
35}
36
37/* list action tail add */
38OS_SEC_ALW_INLINE INLINE void ListTailAdd(struct TagListObject *newNode, struct TagListObject *listObject)
39{
40    ListLowLevelAdd(newNode, listObject->prev, listObject);
41}
42
43/* list action lowel delete */
44OS_SEC_ALW_INLINE INLINE void ListLowLevelDelete(struct TagListObject *prevNode, struct TagListObject *nextNode)
45{
46    nextNode->prev = prevNode;
47    prevNode->next = nextNode;
48}
49
50/* list action delete */
51OS_SEC_ALW_INLINE INLINE void ListDelete(struct TagListObject *node)
52{
53    ListLowLevelDelete(node->prev, node->next);
54
55    node->next = NULL;
56    node->prev = NULL;
57}
58
59/* list action empty */
60OS_SEC_ALW_INLINE INLINE bool ListEmpty(const struct TagListObject *listObject)
61{
62    return (bool)((listObject->next == listObject) && (listObject->prev == listObject));
63}
64
65#define OFFSET_OF_FIELD(type, field) ((uintptr_t)((uintptr_t)(&((type *)0x10)->field) - (uintptr_t)0x10))
66
67#define COMPLEX_OF(ptr, type, field) ((type *)((uintptr_t)(ptr) - OFFSET_OF_FIELD(type, field)))
68
69/* 根据成员地址得到控制块首地址, ptr成员地址, type控制块结构, field成员名 */
70#define LIST_COMPONENT(ptrOfList, typeOfList, fieldOfList) COMPLEX_OF(ptrOfList, typeOfList, fieldOfList)
71
72#define LIST_FOR_EACH(posOfList, listObject, typeOfList, field)                                                    \
73    for ((posOfList) = LIST_COMPONENT((listObject)->next, typeOfList, field); &(posOfList)->field != (listObject); \
74        (posOfList) = LIST_COMPONENT((posOfList)->field.next, typeOfList, field))
75
76#define LIST_FOR_EACH_SAFE(posOfList, listObject, typeOfList, field)                \
77    for ((posOfList) = LIST_COMPONENT((listObject)->next, typeOfList, field);       \
78        (&(posOfList)->field != (listObject))&&((posOfList)->field.next != NULL);  \
79        (posOfList) = LIST_COMPONENT((posOfList)->field.next, typeOfList, field))
80
81#endif /* PRT_LIST_EXTERNAL_H */

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

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

任务控制块

任务相关的头文件主要包括 src/include/prt_task.h [下载] 和 src/include/prt_task_external.h [下载]两个头文件。此外还会用到 src/include/prt_module.h [下载] 和 src/include/prt_errno.h [下载] 两个头文件。 prt_module.h中主要是一些模块ID的定义,而 prt_errno.h 主要是错误类型的相关定义,引入这两个头文件主要是为了保持接口与原版 UniProton 相一致。

prt_task.h 中除了一些相关宏定义外,还定义了任务创建时参数传递的结构体: struct TskInitParam。

 1/*
 2* 任务创建参数的结构体定义。
 3*
 4* 传递任务创建时指定的参数信息。
 5*/
 6struct TskInitParam {
 7    /* 任务入口函数 */
 8    TskEntryFunc taskEntry;
 9    /* 任务优先级 */
10    TskPrior taskPrio;
11    U16 reserved;
12    /* 任务参数,最多4个 */
13    uintptr_t args[4];
14    /* 任务栈的大小 */
15    U32 stackSize;
16    /* 任务名 */
17    char *name;
18    /*
19    * 本任务的任务栈独立配置起始地址,用户必须对该成员进行初始化,
20    * 若配置为0表示从系统内部空间分配,否则用户指定栈起始地址
21    */
22    uintptr_t stackAddr;
23};

prt_task_external.h 中定义了任务调度中最重要的数据结构——任务控制块 struct TagTskCb。

 1#define TagOsRunQue TagListObject //简单实现
 2
 3/*
 4* 任务线程及进程控制块的结构体统一定义。
 5*/
 6struct TagTskCb {
 7    /* 当前任务的SP */
 8    void *stackPointer;
 9    /* 任务状态,后续内部全改成U32 */
10    U32 taskStatus;
11    /* 任务的运行优先级 */
12    TskPrior priority;
13    /* 任务栈配置标记 */
14    U16 stackCfgFlg;
15    /* 任务栈大小 */
16    U32 stackSize;
17    TskHandle taskPid;
18
19    /* 任务栈顶 */
20    uintptr_t topOfStack;
21
22    /* 任务入口函数 */
23    TskEntryFunc taskEntry;
24    /* 任务Pend的信号量指针 */
25    void *taskSem;
26
27    /* 任务的参数 */
28    uintptr_t args[4];
29#if (defined(OS_OPTION_TASK_INFO))
30    /* 存放任务名 */
31    char name[OS_TSK_NAME_LEN];
32#endif
33    /* 信号量链表指针 */
34    struct TagListObject pendList;
35    /* 任务延时链表指针 */
36    struct TagListObject timerList;
37    /* 持有互斥信号量链表 */
38    struct TagListObject semBList;
39    /* 记录条件变量的等待线程 */
40    struct TagListObject condNode;
41#if defined(OS_OPTION_LINUX)
42    /* 等待队列指针 */
43    struct TagListObject waitList;
44#endif
45#if defined(OS_OPTION_EVENT)
46    /* 任务事件 */
47    U32 event;
48    /* 任务事件掩码 */
49    U32 eventMask;
50#endif
51    /* 任务记录的最后一个错误码 */
52    U32 lastErr;
53    /* 任务恢复的时间点(单位Tick) */
54    U64 expirationTick;
55
56#if defined(OS_OPTION_NUTTX_VFS)
57    struct filelist tskFileList;
58#if defined(CONFIG_FILE_STREAM)
59    struct streamlist ta_streamlist;
60#endif
61#endif
62};

简单起见,我们还将任务运行队列结构 TagOsRunQue 直接定义为双向链表 TagListObject(见上面代码)。

最后我们引入了 src/include/prt_amp_task_internal.h

 1#ifndef PRT_AMP_TASK_INTERNAL_H
 2#define PRT_AMP_TASK_INTERNAL_H
 3
 4#include "prt_task_external.h"
 5#include "prt_list_external.h"
 6
 7#define OS_TSK_EN_QUE(runQue, tsk, flags) OsEnqueueTaskAmp((runQue), (tsk))
 8#define OS_TSK_EN_QUE_HEAD(runQue, tsk, flags) OsEnqueueTaskHeadAmp((runQue), (tsk))
 9#define OS_TSK_DE_QUE(runQue, tsk, flags) OsDequeueTaskAmp((runQue), (tsk))
10
11extern U32 OsTskAMPInit(void);
12extern U32 OsIdleTskAMPCreate(void);
13
14OS_SEC_ALW_INLINE INLINE void OsEnqueueTaskAmp(struct TagOsRunQue *runQue, struct TagTskCb *tsk)
15{
16    ListTailAdd(&tsk->pendList, runQue);
17    return;
18}
19
20OS_SEC_ALW_INLINE INLINE void OsEnqueueTaskHeadAmp(struct TagOsRunQue *runQue, struct TagTskCb *tsk)
21{
22    ListAdd(&tsk->pendList, runQue);
23    return;
24}
25
26OS_SEC_ALW_INLINE INLINE void OsDequeueTaskAmp(struct TagOsRunQue *runQue, struct TagTskCb *tsk)
27{
28    ListDelete(&tsk->pendList);
29    return;
30}
31
32#endif /* PRT_AMP_TASK_INTERNAL_H */

该头文件中主要是定义了三个内联函数,用于将任务控制块加入运行队列或从运行队列中移除任务控制块。

任务创建

任务创建代码主要在 src/kernel/task/prt_task_init.c 中。 代码比较多,我们分几个部分分别介绍。

相关变量与函数声明

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

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

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

 1#include "list_types.h"
 2#include "os_attr_armv8_external.h"
 3#include "prt_list_external.h"
 4#include "prt_task.h"
 5#include "prt_task_external.h"
 6#include "prt_asm_cpu_external.h"
 7#include "os_cpu_armv8_external.h"
 8#include "prt_config.h"
 9
10
11/* Unused TCBs and ECBs that can be allocated. */
12OS_SEC_DATA struct TagListObject g_tskCbFreeList = LIST_OBJECT_INIT(g_tskCbFreeList);
13
14extern U32 OsTskAMPInit(void);
15extern U32 OsIdleTskAMPCreate(void);
16extern void OsFirstTimeSwitch(void);

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

极简内存空间管理

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

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

任务栈初始化

在理论课程中,我们知道当发生任务切换时会首先保存前一个任务的上下文到栈里,然后从栈中恢复下一个将运行任务的上下文。可是当任务第一次运行的时候怎么恢复上下文,之前从来没有保存过上下文?

答案就是我们手工制造一个就可以了。下面代码中 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*/
 4void *OsTskContextInit(U32 taskID, U32 stackSize, uintptr_t *topStack, uintptr_t funcTskEntry)
 5{
 6    (void)taskID;
 7    struct TskContext *stack = (struct TskContext *)((uintptr_t)topStack + stackSize);
 8
 9    stack -= 1; // 指针减,减去一个TskContext大小
10
11    stack->x00 = 0;
12    stack->x01 = 0x01010101;
13    stack->x02 = 0x02020202;
14    stack->x03 = 0x03030303;
15    stack->x04 = 0x04040404;
16    stack->x05 = 0x05050505;
17    stack->x06 = 0x06060606;
18    stack->x07 = 0x07070707;
19    stack->x08 = 0x08080808;
20    stack->x09 = 0x09090909;
21    stack->x10 = 0x10101010;
22    stack->x11 = 0x11111111;
23    stack->x12 = 0x12121212;
24    stack->x13 = 0x13131313;
25    stack->x14 = 0x14141414;
26    stack->x15 = 0x15151515;
27    stack->x16 = 0x16161616;
28    stack->x17 = 0x17171717;
29    stack->x18 = 0x18181818;
30    stack->x19 = 0x19191919;
31    stack->x20 = 0x20202020;
32    stack->x21 = 0x21212121;
33    stack->x22 = 0x22222222;
34    stack->x23 = 0x23232323;
35    stack->x24 = 0x24242424;
36    stack->x25 = 0x25252525;
37    stack->x26 = 0x26262626;
38    stack->x27 = 0x27272727;
39    stack->x28 = 0x28282828;
40    stack->x29 = 0x29292929;
41    stack->x30 = funcTskEntry;   // x30: lr(link register)
42    stack->xzr = 0;
43
44    stack->elr = funcTskEntry;
45    stack->esr = 0;
46    stack->far = 0;
47    stack->spsr = 0x305;    // EL1_SP1 | D | A | I | F
48    return stack;
49}

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

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

任务入口函数

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

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

 1/*
 2* 描述:所有任务入口
 3*/
 4OS_SEC_L4_TEXT void OsTskEntry(TskHandle taskId)
 5{
 6    struct TagTskCb *taskCb;
 7    uintptr_t intSave;
 8
 9    (void)taskId;
10
11    taskCb = RUNNING_TASK;
12
13    taskCb->taskEntry(taskCb->args[OS_TSK_PARA_0], taskCb->args[OS_TSK_PARA_1], taskCb->args[OS_TSK_PARA_2],
14                    taskCb->args[OS_TSK_PARA_3]);
15
16    // 调度结束后会开中断,所以不需要自己添加开中断
17    intSave = OsIntLock();
18
19    OS_TASK_LOCK_DATA = 0;
20
21    /* PRT_TaskDelete不能关中断操作,否则可能会导致它核发SGI等待本核响应时死等 */
22    OsIntRestore(intSave);
23
24    OsTaskExit(taskCb);
25}

创建任务

创建任务的代码看上去还是比较多,但已经不是很复杂了。我们从后面的代码往前面看,首先是接口函数 PRT_TaskCreate 函数根据传入的 initParam 参数创建任务返回任务句柄 taskPid。

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

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

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

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

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

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

  1// src/core/kernel/task/prt_task_internal.h
  2OS_SEC_ALW_INLINE INLINE U32 OsTaskCreateChkAndGetTcb(struct TagTskCb **taskCb)
  3{
  4    if (ListEmpty(&g_tskCbFreeList)) {
  5        return OS_ERRNO_TSK_TCB_UNAVAILABLE;
  6    }
  7
  8    // 先获取到该控制块
  9    *taskCb = GET_TCB_PEND(OS_LIST_FIRST(&g_tskCbFreeList));
 10    // 成功,从空闲列表中移除
 11    ListDelete(OS_LIST_FIRST(&g_tskCbFreeList));
 12
 13    return OS_OK;
 14}
 15
 16OS_SEC_ALW_INLINE INLINE bool OsCheckAddrOffsetOverflow(uintptr_t base, size_t size)
 17{
 18    return (base + size) < base;
 19}
 20
 21OS_SEC_L4_TEXT U32 OsTaskCreateRsrcInit(U32 taskId, struct TskInitParam *initParam, struct TagTskCb *taskCb,
 22                                                uintptr_t **topStackOut, uintptr_t *curStackSize)
 23{
 24    U32 ret = OS_OK;
 25    uintptr_t *topStack = NULL;
 26
 27    /* 查看用户是否配置了任务栈,如没有,则进行内存申请,并标记为系统配置,如有,则标记为用户配置。 */
 28    if (initParam->stackAddr != 0) {
 29        topStack = (void *)(initParam->stackAddr);
 30        taskCb->stackCfgFlg = OS_TSK_STACK_CFG_BY_USER;
 31    } else {
 32        topStack = OsTskMemAlloc(initParam->stackSize);
 33        if (topStack == NULL) {
 34            ret = OS_ERRNO_TSK_NO_MEMORY;
 35        } else {
 36            taskCb->stackCfgFlg = OS_TSK_STACK_CFG_BY_SYS;
 37        }
 38    }
 39    *curStackSize = initParam->stackSize;
 40    if (ret != OS_OK) {
 41        return ret;
 42    }
 43
 44    *topStackOut = topStack;
 45    return OS_OK;
 46}
 47
 48OS_SEC_L4_TEXT void OsTskCreateTcbInit(uintptr_t stackPtr, struct TskInitParam *initParam,
 49    uintptr_t topStackAddr, uintptr_t curStackSize, struct TagTskCb *taskCb)
 50{
 51    /* Initialize the task's stack */
 52    taskCb->stackPointer = (void *)stackPtr;
 53    taskCb->args[OS_TSK_PARA_0] = (uintptr_t)initParam->args[OS_TSK_PARA_0];
 54    taskCb->args[OS_TSK_PARA_1] = (uintptr_t)initParam->args[OS_TSK_PARA_1];
 55    taskCb->args[OS_TSK_PARA_2] = (uintptr_t)initParam->args[OS_TSK_PARA_2];
 56    taskCb->args[OS_TSK_PARA_3] = (uintptr_t)initParam->args[OS_TSK_PARA_3];
 57    taskCb->topOfStack = topStackAddr;
 58    taskCb->stackSize = curStackSize;
 59    taskCb->taskSem = NULL;
 60    taskCb->priority = initParam->taskPrio;
 61    taskCb->taskEntry = initParam->taskEntry;
 62#if defined(OS_OPTION_EVENT)
 63    taskCb->event = 0;
 64    taskCb->eventMask = 0;
 65#endif
 66    taskCb->lastErr = 0;
 67
 68    INIT_LIST_OBJECT(&taskCb->semBList);
 69    INIT_LIST_OBJECT(&taskCb->pendList);
 70    INIT_LIST_OBJECT(&taskCb->timerList);
 71
 72    return;
 73}
 74
 75/*
 76* 描述:创建一个任务但不进行激活
 77*/
 78OS_SEC_L4_TEXT U32 OsTaskCreateOnly(TskHandle *taskPid, struct TskInitParam *initParam)
 79{
 80    U32 ret;
 81    U32 taskId;
 82    uintptr_t intSave;
 83    uintptr_t *topStack = NULL;
 84    void *stackPtr = NULL;
 85    struct TagTskCb *taskCb = NULL;
 86    uintptr_t curStackSize = 0;
 87
 88    intSave = OsIntLock();
 89    // 获取一个空闲的任务控制块
 90    ret = OsTaskCreateChkAndGetTcb(&taskCb);
 91    if (ret != OS_OK) {
 92        OsIntRestore(intSave);
 93        return ret;
 94    }
 95
 96    taskId = taskCb->taskPid;
 97    // 分配堆栈空间资源
 98    ret = OsTaskCreateRsrcInit(taskId, initParam, taskCb, &topStack, &curStackSize);
 99    if (ret != OS_OK) {
100        ListAdd(&taskCb->pendList, &g_tskCbFreeList);
101        OsIntRestore(intSave);
102        return ret;
103    }
104    // 栈初始化,就像刚发生过中断一样
105    stackPtr = OsTskContextInit(taskId, curStackSize, topStack, (uintptr_t)OsTskEntry);
106    // 任务控制块初始化
107    OsTskCreateTcbInit((uintptr_t)stackPtr, initParam, (uintptr_t)topStack, curStackSize, taskCb);
108
109    taskCb->taskStatus = OS_TSK_SUSPEND | OS_TSK_INUSE;
110    // 出参ID传出
111    *taskPid = taskId;
112    OsIntRestore(intSave);
113    return OS_OK;
114}
115
116/*
117* 描述:创建一个任务但不进行激活
118*/
119OS_SEC_L4_TEXT U32 PRT_TaskCreate(TskHandle *taskPid, struct TskInitParam *initParam)
120{
121    return OsTaskCreateOnly(taskPid, initParam);
122}

解挂任务

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

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

 1// src/core/kernel/task/prt_task_internal.h
 2OS_SEC_ALW_INLINE INLINE void OsMoveTaskToReady(struct TagTskCb *taskCb)
 3{
 4    if (TSK_STATUS_TST(taskCb, OS_TSK_DELAY_INTERRUPTIBLE)) {
 5        /* 可中断delay, 属于定时等待的任务时候,去掉其定时等待标志位*/
 6        if (TSK_STATUS_TST(taskCb, OS_TSK_TIMEOUT)) {
 7            OS_TSK_DELAY_LOCKED_DETACH(taskCb);
 8        }
 9        TSK_STATUS_CLEAR(taskCb, OS_TSK_TIMEOUT | OS_TSK_DELAY_INTERRUPTIBLE);
10    }
11
12    /* If task is not blocked then move it to ready list */
13    if ((taskCb->taskStatus & OS_TSK_BLOCK) == 0) {
14        OsTskReadyAdd(taskCb);
15
16        if ((OS_FLG_BGD_ACTIVE & UNI_FLAG) != 0) {
17            OsTskSchedule();
18            return;
19        }
20    }
21}
22
23/*
24* 描述解挂任务
25*/
26OS_SEC_L2_TEXT U32 PRT_TaskResume(TskHandle taskPid)
27{
28    uintptr_t intSave;
29    struct TagTskCb *taskCb = NULL;
30
31    // 获取 taskPid 对应的任务控制块
32    taskCb = GET_TCB_HANDLE(taskPid);
33
34    intSave = OsIntLock();
35
36    if (TSK_IS_UNUSED(taskCb)) {
37        OsIntRestore(intSave);
38        return OS_ERRNO_TSK_NOT_CREATED;
39    }
40
41    if (((OS_TSK_RUNNING & taskCb->taskStatus) != 0) && (g_uniTaskLock != 0)) {
42        OsIntRestore(intSave);
43        return OS_ERRNO_TSK_ACTIVE_FAILED;
44    }
45
46    /* If task is not suspended and not in interruptible delay then return */
47    if (((OS_TSK_SUSPEND | OS_TSK_DELAY_INTERRUPTIBLE) & taskCb->taskStatus) == 0) {
48        OsIntRestore(intSave);
49        return OS_ERRNO_TSK_NOT_SUSPENDED;
50    }
51
52    TSK_STATUS_CLEAR(taskCb, OS_TSK_SUSPEND);
53
54    /* If task is not blocked then move it to ready list */
55    OsMoveTaskToReady(taskCb);
56    OsIntRestore(intSave);
57
58    return OS_OK;
59}

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

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 的上下文后执行(稍后描述)。

  1/*
  2* 描述:AMP任务初始化
  3*/
  4extern U32 g_threadNum;
  5extern void *OsMemAllocAlign(U32 mid, U8 ptNo, U32 size, U8 alignPow);
  6OS_SEC_L4_TEXT U32 OsTskAMPInit(void)
  7{
  8    uintptr_t size;
  9    U32 idx;
 10
 11    // 简单处理,分配4096,存OS_MAX_TCB_NUM个任务。#define OS_MAX_TCB_NUM  (g_tskMaxNum + 1 + 1)  // 1个IDLE,1个无效任务
 12    g_tskCbArray = (struct TagTskCb *)OsMemAllocAlign((U32)OS_MID_TSK, 0,
 13                                                    4096, OS_TSK_STACK_SIZE_ALLOC_ALIGN);
 14    if (g_tskCbArray == NULL) {
 15        return OS_ERRNO_TSK_NO_MEMORY;
 16    }
 17
 18    g_tskMaxNum = 4096 / sizeof(struct TagTskCb) - 2;
 19
 20
 21    // 1为Idle任务
 22    g_threadNum += (g_tskMaxNum + 1);
 23
 24    // 初始化为全0
 25    for(int i = 0; i < OS_MAX_TCB_NUM - 1; i++)
 26        g_tskCbArray[i] = {0};
 27
 28    g_tskBaseId = 0;
 29
 30    // 将所有控制块加入g_tskCbFreeList链表,且设置控制块的初始状态和任务id
 31    INIT_LIST_OBJECT(&g_tskCbFreeList);
 32    for (idx = 0; idx < OS_MAX_TCB_NUM - 1; idx++) {
 33        g_tskCbArray[idx].taskStatus = OS_TSK_UNUSED;
 34        g_tskCbArray[idx].taskPid = (idx + g_tskBaseId);
 35        ListTailAdd(&g_tskCbArray[idx].pendList, &g_tskCbFreeList);
 36    }
 37
 38    /* 在初始化时给RUNNING_TASK的PID赋一个合法的无效值,放置在Trace使用时出现异常 */
 39    RUNNING_TASK = OS_PST_ZOMBIE_TASK;
 40
 41    /* 在初始化时给RUNNING_TASK的PID赋一个合法的无效值,放置在Trace使用时出现异常 */
 42    RUNNING_TASK->taskPid = idx + g_tskBaseId;
 43
 44    INIT_LIST_OBJECT(&g_runQueue);
 45
 46    /* 增加OS_TSK_INUSE状态,使得在Trace记录的第一条信息状态为OS_TSK_INUSE(创建状态) */
 47    RUNNING_TASK->taskStatus = (OS_TSK_INUSE | OS_TSK_RUNNING);
 48    RUNNING_TASK->priority = OS_TSK_PRIORITY_LOWEST + 1;
 49
 50    return OS_OK;
 51}
 52
 53/*
 54* 描述:任务初始化
 55*/
 56OS_SEC_L4_TEXT U32 OsTskInit(void)
 57{
 58    U32 ret;
 59    ret = OsTskAMPInit();
 60    if (ret != OS_OK) {
 61        return ret;
 62    }
 63
 64    return OS_OK;
 65}
 66
 67/*
 68* 描述:Idle背景任务
 69*/
 70OS_SEC_TEXT void OsTskIdleBgd(void)
 71{
 72    while (TRUE);
 73}
 74
 75/*
 76* 描述:ilde任务创建.
 77*/
 78OS_SEC_L4_TEXT U32 OsIdleTskAMPCreate(void)
 79{
 80    U32 ret;
 81    TskHandle taskHdl;
 82    struct TskInitParam taskInitParam = {0};
 83    char tskName[OS_TSK_NAME_LEN] = "IdleTask";
 84
 85    /* Create background task. */
 86    taskInitParam.taskEntry = (TskEntryFunc)OsTskIdleBgd;
 87    taskInitParam.stackSize = 4096;
 88    // taskInitParam.name = tskName;
 89    taskInitParam.taskPrio = OS_TSK_PRIORITY_LOWEST;
 90    taskInitParam.stackAddr = 0;
 91
 92    /* 任务调度的必要条件就是有背景任务,此时背景任务还没有创建,因此不会发生任务切换 */
 93    ret = PRT_TaskCreate(&taskHdl, &taskInitParam);
 94    if (ret != OS_OK) {
 95        return ret;
 96    }
 97    ret = PRT_TaskResume(taskHdl);
 98    if (ret != OS_OK) {
 99        return ret;
100    }
101    IDLE_TASK_ID = taskHdl;
102
103    return ret;
104}
105
106/*
107* 描述:激活任务管理
108*/
109OS_SEC_L4_TEXT U32 OsActivate(void)
110{
111    U32 ret;
112    ret = OsIdleTskAMPCreate();
113    if (ret != OS_OK) {
114        return ret;
115    }
116
117    OsTskHighestSet();
118
119    /* Indicate that background task is running. */
120    UNI_FLAG |= OS_FLG_BGD_ACTIVE | OS_FLG_TSK_REQ;
121
122    /* Start Multitasking. */
123    OsFirstTimeSwitch();
124    // 正常情况不应执行到此
125    return OS_ERRNO_TSK_ACTIVE_FAILED;
126}

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

1#define OS_TSK_PRIORITY_LOWEST 63

任务状态转换

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

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

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

    • OsTskReadyDel 会清除任务的就绪态

  • 提供了任务结束退出 OsTaskExit 函数,注意 OsTskEntry 中会调用 OsTaskExit 函数。由于任务退出,因此需要进行调度,即存在调度点,所以调用 OsTskSchedule 函数。

 1#include "prt_task_external.h"
 2#include "prt_typedef.h"
 3#include "os_attr_armv8_external.h"
 4#include "prt_asm_cpu_external.h"
 5#include "os_cpu_armv8_external.h"
 6#include "prt_amp_task_internal.h"
 7
 8OS_SEC_BSS struct TagOsRunQue g_runQueue;  // 核的局部运行队列
 9
10/*
11* 描述:将任务添加到就绪队列, 调用者确保不会换核,并锁上rq
12*/
13OS_SEC_L0_TEXT void OsTskReadyAdd(struct TagTskCb *task)
14{
15    struct TagOsRunQue *rq = &g_runQueue;
16    TSK_STATUS_SET(task, OS_TSK_READY);
17
18    OS_TSK_EN_QUE(rq, task, 0);
19    OsTskHighestSet();
20
21    return;
22}
23
24/*
25* 描述:将任务从就绪队列中移除,关中断外部保证
26*/
27OS_SEC_L0_TEXT void OsTskReadyDel(struct TagTskCb *taskCb)
28{
29    struct TagOsRunQue *runQue = &g_runQueue;
30    TSK_STATUS_CLEAR(taskCb, OS_TSK_READY);
31
32    OS_TSK_DE_QUE(runQue, taskCb, 0);
33    OsTskHighestSet();
34
35    return;
36}
37
38// src/core/kernel/task/prt_task_del.c
39/*
40* 描述:任务结束退出
41*/
42OS_SEC_L4_TEXT void OsTaskExit(struct TagTskCb *tsk)
43{
44
45    uintptr_t intSave = OsIntLock();
46
47    OsTskReadyDel(tsk);
48    OsTskSchedule();
49
50    OsIntRestore(intSave);
51
52}

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

调度与切换

src/kernel/sched/prt_sched_single.c

 1#include "prt_task_external.h"
 2#include "os_attr_armv8_external.h"
 3#include "prt_asm_cpu_external.h"
 4#include "os_cpu_armv8_external.h"
 5
 6/*
 7* 描述:任务调度,切换到最高优先级任务
 8*/
 9OS_SEC_TEXT void OsTskSchedule(void)
10{
11    /* 外层已经关中断 */
12    /* Find the highest task */
13    OsTskHighestSet();
14
15    /* In case that running is not highest then reschedule */
16    if ((g_highestTask != RUNNING_TASK) && (g_uniTaskLock == 0)) {
17        UNI_FLAG |= OS_FLG_TSK_REQ;
18
19        /* only if there is not HWI or TICK the trap */
20        if (OS_INT_INACTIVE) { // 不在中断上下文中,否则应该在中断返回时切换
21            OsTaskTrap();
22            return;
23        }
24    }
25
26    return;
27}
28
29/*
30* 描述: 调度的主入口
31* 备注: NA
32*/
33OS_SEC_L0_TEXT void OsMainSchedule(void)
34{
35    struct TagTskCb *prevTsk;
36    if ((UNI_FLAG & OS_FLG_TSK_REQ) != 0) {
37        prevTsk = RUNNING_TASK;
38
39        /* 清除OS_FLG_TSK_REQ标记位 */
40        UNI_FLAG &= ~OS_FLG_TSK_REQ;
41
42        RUNNING_TASK->taskStatus &= ~OS_TSK_RUNNING;
43        g_highestTask->taskStatus |= OS_TSK_RUNNING;
44
45        RUNNING_TASK = g_highestTask;
46    }
47    // 如果中断没有驱动一个任务ready,直接回到被打断的任务
48    OsTskContextLoad((uintptr_t)RUNNING_TASK);
49}
50
51/*
52* 描述: 系统启动时的首次任务调度
53* 备注: NA
54*/
55OS_SEC_L4_TEXT void OsFirstTimeSwitch(void)
56{
57    OsTskHighestSet();
58    RUNNING_TASK = g_highestTask;
59    TSK_STATUS_SET(RUNNING_TASK, OS_TSK_RUNNING);
60    OsTskContextLoad((uintptr_t)RUNNING_TASK);
61    // never get here
62    return;
63}

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

 1/*
 2* 模块内内联函数定义
 3*/
 4OS_SEC_ALW_INLINE INLINE void OsTskHighestSet(void)
 5{
 6    struct TagTskCb *taskCb = NULL;
 7    struct TagTskCb *savedTaskCb = NULL;
 8
 9    // 遍历g_runQueue队列,查找优先级最高的任务
10    LIST_FOR_EACH(taskCb, &g_runQueue, struct TagTskCb, pendList) {
11        // 第一个任务,直接保存到savedTaskCb
12        if(savedTaskCb == NULL) {
13            savedTaskCb = taskCb;
14            continue;
15        }
16        // 比较优先级,值越小优先级越高
17        if(taskCb->priority < savedTaskCb->priority){
18            savedTaskCb = taskCb;
19        }
20    }
21
22    g_highestTask = savedTaskCb;
23}

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

 1/*
 2* 描述: void OsTskContextLoad(uintptr_t stackPointer)
 3*/
 4    .globl OsTskContextLoad
 5    .type OsTskContextLoad, @function
 6    .align 4
 7OsTskContextLoad:
 8    ldr    X0, [X0]
 9    mov    SP, X0            // X0 is stackPointer
10
11OsContextLoad:
12    ldp    x2, x3, [sp],#16
13    add    sp, sp, #16        // 跳过far, esr, HCR_EL2.TRVM==1的时候,EL1不能写far, esr
14    msr    spsr_el1, x3
15    msr    elr_el1, x2
16    dsb    sy
17    isb
18
19    RESTORE_EXC_REGS // 恢复上下文
20
21    eret //从异常返回
22
23
24/*
25* 描述: Task调度处理函数。 X0 is g_runningTask
26*/
27    .globl OsTaskTrap
28    .type OsTaskTrap, @function
29    .align 4
30
31OsTaskTrap:
32    LDR    x1, =g_runningTask /* OsTaskTrap是函数调用过来,x0 x1寄存器是caller save,此处能直接使用 */
33    LDR    x0, [x1] /* x0 is the &g_pRunningTask->sp */
34
35    SAVE_EXC_REGS
36
37    /* TskTrap需要保存CPSR,由于不能直接访问,需要拼接获取当前CPSR入栈 */
38    mrs    x3, DAIF /* CPSR:DAIF 4种事件的mask, bits[9:6] */
39    mrs    x2, NZCV /* NZCV:Condition flags, bits[31:28] */
40    orr    x3, x3, x2
41    orr    x3, x3, #(0x1U << 2) /* 当前的 exception level,bits[3:2] 00:EL0,01:El1,10:El2,11:EL3 */
42    orr    x3, x3, #(0x1U) /* 当前栈的选择,bits[0] 0:SP_EL0,1:SP_ELX */
43
44    mov    x2, x30    // 用返回地址x30作为现场恢复点
45    sub    sp, sp, #16  // 跳过esr_el1, far_el1, 异常时才有用
46    stp    x2, x3, [sp,#-16]!
47
48    // 存入SP指针到g_pRunningTask->sp
49    mov    x1, sp
50    str    x1, [x0]   // x0 is the &g_pRunningTask->sp
51
52    B      OsMainSchedule
53loop1:
54    B      loop1

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

1#define OS_TSK_STACK_SIZE_ALIGN  16U
2#define OS_TSK_STACK_SIZE_ALLOC_ALIGN 4U //按2的幂对齐,即2^4=16字节
3#define OS_TSK_STACK_ADDR_ALIGN  16U
4
5extern void OsTaskTrap(void);
6extern void OsTskContextLoad(uintptr_t stackPointer);

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

 1#include "prt_typedef.h"
 2#include "os_attr_armv8_external.h"
 3#include "prt_task.h"
 4
 5OS_SEC_L4_BSS U32 g_threadNum;
 6
 7/* Tick计数 */
 8// OS_SEC_BSS U64 g_uniTicks; // 把 lab5 中在 src/kernel/tick/prt_tick.c 定义的 g_uniTicks 移到此处则取消此行的注释
 9
10/* 系统状态标志位 */
11OS_SEC_DATA U32 g_uniFlag = 0;
12
13OS_SEC_DATA struct TagTskCb *g_runningTask = NULL;
14
15// src/core/kernel/task/prt_task_global.c
16OS_SEC_BSS TskEntryFunc g_tskIdleEntry;
17
18
19OS_SEC_BSS U32 g_tskMaxNum;
20OS_SEC_BSS struct TagTskCb *g_tskCbArray;
21OS_SEC_BSS U32 g_tskBaseId;
22
23OS_SEC_BSS TskHandle g_idleTaskId;
24OS_SEC_BSS U16 g_uniTaskLock;
25OS_SEC_BSS struct TagTskCb *g_highestTask;

任务调度测试

  1#include "prt_typedef.h"
  2#include "prt_tick.h"
  3#include "prt_task.h"
  4
  5extern U32 PRT_Printf(const char *format, ...);
  6extern void PRT_UartInit(void);
  7extern void CoreTimerInit(void);
  8extern U32 OsHwiInit(void);
  9extern U32 OsActivate(void);
 10extern U32 OsTskInit(void);
 11
 12void Test1TaskEntry()
 13{
 14    PRT_Printf("task 1 run ...\n");
 15
 16    U32 cnt = 5;
 17    while (cnt > 0) {
 18        // PRT_TaskDelay(200);
 19        PRT_Printf("task 1 run ...\n");
 20        cnt--;
 21    }
 22}
 23
 24void Test2TaskEntry()
 25{
 26    PRT_Printf("task 2 run ...\n");
 27
 28    U32 cnt = 5;
 29    while (cnt > 0) {
 30        // PRT_TaskDelay(100);
 31        PRT_Printf("task 2 run ...\n");
 32        cnt--;
 33    }
 34}
 35
 36S32 main(void)
 37{
 38
 39
 40    // 初始化GIC
 41    OsHwiInit();
 42    // 启用Timer
 43    CoreTimerInit();
 44    // 任务系统初始化
 45    OsTskInit();
 46
 47    PRT_UartInit();
 48
 49    PRT_Printf("            _       _ _____      _             _             _   _ _   _ _   _           \n");
 50    PRT_Printf("  _ __ ___ (_)_ __ (_) ____|   _| | ___ _ __  | |__  _   _  | | | | \\ | | | | | ___ _ __ \n");
 51    PRT_Printf(" | '_ ` _ \\| | '_ \\| |  _|| | | | |/ _ \\ '__| | '_ \\| | | | | |_| |  \\| | | | |/ _ \\ '__|\n");
 52    PRT_Printf(" | | | | | | | | | | | |__| |_| | |  __/ |    | |_) | |_| | |  _  | |\\  | |_| |  __/ |   \n");
 53    PRT_Printf(" |_| |_| |_|_|_| |_|_|_____\\__,_|_|\\___|_|    |_.__/ \\__, | |_| |_|_| \\_|\\___/ \\___|_|   \n");
 54    PRT_Printf("                                                     |___/                               \n");
 55
 56    PRT_Printf("ctr-a h: print help of qemu emulator. ctr-a x: quit emulator.\n\n");
 57
 58    U32 ret;
 59    struct TskInitParam param = {0};
 60
 61    // task 1
 62    // param.stackAddr = 0;
 63    param.taskEntry = (TskEntryFunc)Test1TaskEntry;
 64    param.taskPrio = 35;
 65    // param.name = "Test1Task";
 66    param.stackSize = 0x1000; //固定4096,参见prt_task_init.c的OsMemAllocAlign
 67
 68    TskHandle tskHandle1;
 69    ret = PRT_TaskCreate(&tskHandle1, &param);
 70    if (ret) {
 71        return ret;
 72    }
 73
 74    ret = PRT_TaskResume(tskHandle1);
 75    if (ret) {
 76        return ret;
 77    }
 78
 79    // task 2
 80    // param.stackAddr = 0;
 81    param.taskEntry = (TskEntryFunc)Test2TaskEntry;
 82    param.taskPrio = 30;
 83    // param.name = "Test2Task";
 84    param.stackSize = 0x1000; //固定4096,参见prt_task_init.c的OsMemAllocAlign
 85
 86    TskHandle tskHandle2;
 87    ret = PRT_TaskCreate(&tskHandle2, &param);
 88    if (ret) {
 89        return ret;
 90    }
 91
 92    ret = PRT_TaskResume(tskHandle2);
 93    if (ret) {
 94        return ret;
 95    }
 96
 97    // 启动调度
 98    OsActivate();
 99
100    // while(1);
101    return 0;
102
103}

提示

将新建文件加入构建系统

lab6 作业

作业1

实现分时调度。

提示

分时调度的调度点存在于时钟Tick中断、任务结束等处。