内核很多模块都使用了位图,我们将具体分析进程调度相关的位图管理。我们知道鸿蒙内核进程和线程都是32个优先级,而之所以是32个优先级,主要就是因为他们的优先级是由位图管理的,BitMap是UINT32的变量,所以进程和线程都是32个优先级,一个位一个级别,最高位优先级最低。
UINT32 priBitMap; /**< BitMap for recording the change of task priority,
the priority can not be greater than 31 */
进程和线程在执行过程中优先级会经常变化,变量priBitMap就是用来记录所有曾经变化过的优先级(曾经有过的所有优先级历史记录),例如 0x0000004B = 0b0000 0000 0000 0000 0000 0000 0100 1011就代表该Task曾经有过 0,1,3,6 这几种优先级(0代表最高优先级)。我们还可以通过位图管理器函数,得到该Task有过的最高优先级(1出现的最低位置)和最低优先级(1出现的最高位置)。
UINT32 queueBitmap; //位图调度器,每一位对应一个优先级,用于标识对应优先级的就绪队列是否有就绪任务
除此之外,还可用位图表示任务状态,Lite_OS定义了六种任务状态。用每一位来表示一种不同的状态,1表示是,0表示不是。
#define OS_TASK_STATUS_INIT 0x0001U //初始化状态
#define OS_TASK_STATUS_READY 0x0002U //就绪状态的任务都将插入就绪队列,注意就绪队列 的本质是个双向链表
#define OS_TASK_STATUS_RUNNING 0x0004U //运行状态
#define OS_TASK_STATUS_SUSPENDED 0x0008U
#define OS_TASK_STATUS_PENDING 0x0010U //阻塞状态
#define OS_TASK_STATUS_PEND_TIME 0x0080U
#define OS_TASK_STATUS_DELAY 0x0020U //延期状态
#define OS_TASK_STATUS_TIMEOUT 0x0040U //任务超时
采用这种数据形式,可以允许多种标签同时存在,比如 0x07 = 0b00000111,对应以上定义就是任务有三个标签(初始,就绪,和运行),进程和线程在运行期间是允许多种标签同时存在的。
对位的管理/运算需要有个专门的管理器:位图管理器 (见源码 los_bitmap.c )。位操作提供了4个API,进行置1、清0、获取为1的最高、最低位等操作,如下:
接口名 | 描述 |
---|---|
LOS_BitmapSet | 对状态字的某一标志位进行置1操作 |
LOS_BitmapClr | 对状态字的某一标志位进行清0操作 |
LOS_HighBitGet | 获取状态字中为1的最高位 |
LOS_LowBitGet | 获取状态字中为1的最低位 |
更详细分析可参考LiteOS内核源码分析系列五 LiteOS内核位操作模块
双向链表是指含有往前和往后两个方向的链表,即每个结点中除存放下一个节点指针外,还增加一个指向其前一个节点的指针。其头指针 head 是唯 一确定的。从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点,这种数据结构形式使得双向链表在查找时更加方便, 特别是大量数据的遍历。由于双向链表具有对称性,能方便地完成各种插入、删除等操作。LOS_DL_LIST结构很简单,如下所示:
typedef struct LOS_DL_LIST {//双向链表,内核最重要结构体
struct LOS_DL_LIST *pstPrev; /**< Current node's pointer to the previous node *///前驱节点(左手)
struct LOS_DL_LIST *pstNext; /**< Current node's pointer to the next node *///后继节点(右手)
} LOS_DL_LIST;
在创建双向链表时,可以先创建一个节点作为Head头节点,链表的节点从HEAD节点开始挂载。从head节点的依次遍历下一个节点,最后一个不等于Head节点的节点称之为Tail尾节点。这个Tail节点也是Head节点的前驱。从Head向前查找,可以更快的找到Tail节点。
内核的各个模块都能看到双向链表的身影。理解双向链表LOS_DL_LIST 及相关函数是读懂鸿蒙内核的关键,它是鸿蒙内核最重要的结构体。关于双向链表LOS_DL_LIST的初始化以及对双向链表的插入删除等操作定义在 kernel_liteos_a-master\kernel_liteos_a-master\kernel\include\los_list.h 文件中。下面是截取的相关接口及其功能描述:
接口名 | 功能描述 |
---|---|
LOS_ListInit | 对链表进行初始化 |
LOSListAdd | 将新节点添加到链表中 |
LOS_ListTailInsert | 将节点插入到双向链表尾部 |
LOS_ListHeadInsert | 将节点插入到双向链表头部 |
LOS_ListDelete | 将指定的节点从链表中删除 |
LOS_ListEmpty | 判断链表是否为空 |
LOS_ListDelInit | 将指定的节点从链表中删除使用该节点初始化链表 |
LOS_ListTailInsertList | 将链表插入到双向链表尾部 |
LOS_ListHeadInsertList | 将链表插入到双向链表头部 |
LOS_DL_LIST 是复杂结构体的最爱,在进程控制块定义的时候会用到双向链表描述一个进程的所有信息,在SortLinkList排序链表的定义使用上也起到了很重要的作用,在任务调度切换函数OsSchedTaskSwicth()中就有调用到一个排序链表插入函数OsAdd2SortLink() ,具体定义如下:
LITE_OS_SEC_TEXT VOID OsAdd2SortLink(const SortLinkAttribute *sortLinkHeader, SortLinkList *sortList)
{
SortLinkList *listSorted = NULL;
LOS_DL_LIST *listObject = NULL;
⑴ if (sortList->idxRollNum > OS_TSK_MAX_ROLLNUM) {
SET_SORTLIST_VALUE(sortList, OS_TSK_MAX_ROLLNUM);
}
listObject = sortLinkHeader->sortLink;
⑵ if (listObject->pstNext == listObject) {
LOS_ListTailInsert(listObject, &sortList->sortLinkNode);
} else {
⑶ listSorted = LOS_DL_LIST_ENTRY(listObject->pstNext, SortLinkList, sortLinkNode);
do {
⑷ if (ROLLNUM(listSorted->idxRollNum) <= ROLLNUM(sortList->idxRollNum)) {
ROLLNUM_SUB(sortList->idxRollNum, listSorted->idxRollNum);
} else {
⑸ ROLLNUM_SUB(listSorted->idxRollNum, sortList->idxRollNum);
break;
}
⑹ listSorted = LOS_DL_LIST_ENTRY(listSorted->sortLinkNode.pstNext, SortLinkList, sortLinkNode);
} while (&listSorted->sortLinkNode != listObject);
⑺ LOS_ListTailInsert(&listSorted->sortLinkNode, &sortList->sortLinkNode);
}
}
分析函数定义,包含2个参数,第一个参数sortLinkHeader用于指定排序链表的头结点,第二个参数sortList是待插入的链表节点,此时该节点的滚动数等于对应阻塞任务或定时器的超时时间。⑴处代码处理滚动数超大的场景,如果滚动数大于OS_TSK_MAX_ROLLNUM,则设置滚动数等于OS_TSK_MAX_ROLLNUM。⑵处代码,如果排序链表为空, 则把链表节点尾部插入。如果排序链表不为空,则执行⑶处代码,获取排序链表上的下一个节点SortLinkList *listSorted。⑷、⑸ 处代码,如果待插入节点的滚动数大于排序链表的下一个节点的滚动数,则把待插入节点的滚动数减去下一个节点的滚动数,并继续执行⑹处代码,继续与下下一个节点进行比较。否则,如果待插入节点的滚动数小于排序链表的下一个节点的滚动数,则把下一个节点的滚动数减去待插入节点的滚动数,然后跳出循环,继续执行⑺处代码,完成待插入节点的插入。
参考:LiteOS:盘点那些重要的数据结构
在任务调度模块,就绪队列是个重要的数据结构,就绪队列需要支持初始化,出入队列,从队列获取最高优先级任务等操作。鸿蒙内核进程和线程各有32个就绪队列,进程队列用全局变量存放, 创建进程时入队。队列初始化可参考OsSchedInit()函数。
线程队列中放同等优先级的task,在初始化时内核一次性创建了32个双向循环链表,每种优先级都有一个队列来记录就绪状态的tasks的位置,g_sched分配的是一个连续的内存块,存放了32个双向链表。
//* 0x80000000U = 10000000000000000000000000000000(32位,1是用于移位的)
#define OS_PRIORITY_QUEUE_NUM 32 //优先级级别数
#define PRIQUEUE_PRIOR0_BIT 0x80000000U //第一优先级位图,所有优先级位图由该位图右移对应优先级得到
/* 任务调度队列集合 */
typedef struct {
LOS_DL_LIST priQueueList[OS_PRIORITY_QUEUE_NUM]; //各优先级任务就绪队列,默认32级
UINT32 readyTasks[OS_PRIORITY_QUEUE_NUM]; //各优先级任务队列中就绪任务个数
UINT32 queueBitmap; //位图调度器,每一位对应一个优先级,用于标识对应优先级的就绪队列是否有就绪任务
} SchedQueue;
/* 进程调度队列 */
typedef struct {
SchedQueue queueList[OS_PRIORITY_QUEUE_NUM]; //进程优先级调度队列,默认32级
UINT32 queueBitmap; //位图调度器,每一位对应一个优先级,用于标识对应优先级是否有就绪进程
SchedScan taskScan; //函数指针,扫描任务
SchedScan swtmrScan; //函数指针,扫描定时器
} Sched;
STATIC Sched *g_sched = NULL; //全局调度器
队列数据结构如下图所示:
鸿蒙系统的调度是抢占式的,task分成32个优先级,位图调度器就是一个32位的变量,它的每一位来标记对应队列中是否有任务,在位图调度下,任务优先级的值越小则代表具有越高的优先级,每当需要进行调度时,从最低位向最高位查找出第一个置 1 的位的所在位置,即为当前最高优先级,然后从 对应优先级就绪队列获得相应的任务控制块,整个调度器的实现复杂度是 O(1),即无论任务多少,其调度时间是固定的。
Init→Ready: 进程创建或fork时,拿到该进程控制块后进入Init状态,处于进程初始化阶段,当进程初始化完成将进程插入调度队列,此时进程进入就绪状 态。
Ready→Running: 进程创建后进入就绪态,发生进程切换时,就绪列表中最高优先级的进程被执行,从而进入运行态。若此时该进程中已无其它线程处于就绪态, 则该进程从就绪列表删除,只处于运行态;若此时该进程中还有其它线程处于就绪态,则该进程依旧在就绪队列,此时进程的就绪态和运行态共 存。
Running→Pend: 进程内所有的线程均处于阻塞态时,进程在最后一个线程转为阻塞态时,同步进入阻塞态,然后发生进程切换。
Pend→Ready / Pend→Running: 阻塞进程内的任意线程恢复就绪态时,进程被加入到就绪队列,同步转为就绪态,若此时发生进程切换,则进程状态由就绪态转为运行态。
Ready→Pend: 进程内的最后一个就绪态线程处于阻塞态时,进程从就绪列表中删除,进程由就绪态转为阻塞态。
Running→Ready: 进程由运行态转为就绪态的情况有以下两种: 有更高优先级的进程创建或者恢复后,会发生进程调度,此刻就绪列表中最高优先级进程变为运行态,那么原先运行的进程由运行态变为就 绪态。 若进程的调度策略为SCHED_RR,且存在同一优先级的另一个进程处于就绪态,则该进程的时间片消耗光之后,该进程由运行态转为就绪 态,另一个同优先级的进程由就绪态转为运行态。 Running→Zombies: 当进程的主线程或所有线程运行结束后,进程由运行态转为僵尸态,等待父进程回收资源。
参考文章HarmonyOS内核进程调度相关函数
本篇文章内容为操作系统兴趣小组成员共同学习成果总结,同时参考了其他作者的总结分析文章,具体参考都已在文中标明。