[文章]

鸿蒙内核源码分析(调度队列篇):进程和Task的就绪队列对调度的作用

2020-11-23 11:09:38  363 鸿蒙系统 内核 代码
分享
3
为何单独讲调度队列?
鸿蒙内核代码中有两个源文件是关于队列的,一个是用于调度的队列,另一个是用于线程间通讯的IPC队列。
本文详细讲述调度队列:详见代码
IPC队列后续有专门的博文讲述,这两个队列的数据结构实现采用的都是双向循环链表,LOS_DL_LIST实在是太重要了,是理解鸿蒙内核的关键,说是最重要的代码一点也不为过,源码出现在 sched_sq模块,说明是用于任务的调度的,sched_sq模块只有两个文件,另一个los_sched.c就是调度代码。
涉及函数[td]
功能分类
描述
创建队列
OsPriQueueInit
创建了32个就绪队列
获取最高优先级队列
OsPriQueueTop
查最高优先级任务
从头部入队列
OsPriQueueEnqueueHead
从头部插入某个就绪队列
从尾部入队列
OsPriQueueEnqueue
默认是从尾部插入某个就绪队列
出队列
OsPriQueueDequeue
从最高优先级的就绪队列中删除

OsPriQueueProcessDequeue
从进程队列中删除

OsPriQueueProcessSize
用进程查队列中元素个数
OsPriQueueSize用任务查队列中元素个数

OsTaskPriQueueTop查最高优先级任务
OsDequeEmptySchedMap进程出列
OsGetTopTask获取被调度选择的task

鸿蒙内核进程和线程各有32个就绪队列,进程队列用全局变量存放,创建进程时入队,任务队列放在进程的threadPriQueueList中。

映射张大爷的故事:就绪队列就是在外面排队的32个通道,按优先级0-31依次排好,张大爷的办公室有个牌子,类似打篮球的记分牌,一共32个,一字排开,队列里有人时对应的牌就是1,没有就是0 ,这样张大爷每次从0位开始看,看到的第一个1那就是最高优先级的那个人。办公室里的记分牌就是位图调度器。

位图调度器
  1. //*kfy 0x80000000U = 10000000000000000000000000000000(32位,1是用于移位的,设计之精妙,点赞)
  2. #define PRIQUEUE_PRIOR0_BIT   0x80000000U

  3. #ifndef CLZ
  4. #define CLZ(value)                                  (__clz(value)) //汇编指令
  5. #endif

  6. LITE_OS_SEC_BSS LOS_DL_LIST *g_priQueueList = NULL; //所有的队列 原始指针
  7. LITE_OS_SEC_BSS UINT32 g_priQueueBitmap; // 位图调度
  8. // priority = CLZ(bitmap); // 获取最高优先级任务队列 调度位
复制代码

整个los_priqueue.c就只有两个全部变量,一个是 LOS_DL_LIST *g_priQueueList 是32个进程就绪队列的头指针,在就绪队列中会讲另一个UINT32 g_priQueueBitmap  估计很多人会陌生,是一个32位的变量,叫位图调度器。怎么理解它呢?

鸿蒙系统的调度是抢占式的,task分成32个优先级,如何快速的知道哪个队列是空的,哪个队列里有任务需要一个标识,而且要极高效的实现?答案是:位图调度器。

简单说就是一个变量的位来标记对应队列中是否有任务,在位图调度下,任务优先级的值越小则代表具有越高的优先级,每当需要进行调度时,从最低位向最高位查找出第一个置 1 的位的所在位置,即为当前最高优先级,然后从对应优先级就绪队列获得相应的任务控制块,整个调度器的实现复杂度是 O(1),即无论任务多少,其调度时间是固定的。

进程就绪队列机制
CPU执行速度是很快的,其运算速度和内存的读写速度是数量级的差异,与硬盘的读写更是指数级。 鸿蒙内核默认一个时间片是 10ms,  资源很宝贵,它不断在众多任务中来回的切换,所以绝不能让CPU等待任务,CPU时间很宝贵,没准备好的任务不要放进来。这就是进程和线程就绪队列的机制,一共有32个任务就绪队列,因为线程的优先级是默认32个, 每个队列中放同等优先级的task。

队列初始化做了哪些工作?详细看代码
  1. #define OS_PRIORITY_QUEUE_NUM 32

  2. //内部队列初始化
  3. UINT32 OsPriQueueInit(VOID)
  4. {
  5.     UINT32 priority;

  6.     /* system resident resource *///常驻内存
  7.     g_priQueueList = (LOS_DL_LIST *)LOS_MemAlloc(m_aucSysMem0, (OS_PRIORITY_QUEUE_NUM * sizeof(LOS_DL_LIST)));//分配32个队列头节点
  8.     if (g_priQueueList == NULL) {
  9.         return LOS_NOK;
  10.     }

  11.     for (priority = 0; priority < OS_PRIORITY_QUEUE_NUM; ++priority) {
  12.         LOS_Listinit(&g_priQueueList[priority]);//队列初始化,前后指针指向自己
  13.     }
  14.     return LOS_OK;
  15. }
复制代码

因TASK 有32个优先级,在初始化时内核一次性创建了32个双向循环链表,每种优先级都有一个队列来记录就绪状态的tasks的位置,g_priQueueList分配的是一个连续的内存块,存放了32个LOS_DL_LIST,再看一下LOS_DL_LIST结构体,因为它太重要了!越简单越灵活
  1. typedef struct LOS_DL_LIST {
  2.     struct LOS_DL_LIST *pstPrev; /**< Current node's pointer to the previous node */
  3.     struct LOS_DL_LIST *pstNext; /**< Current node's pointer to the next node */
  4. } LOS_DL_LIST;
复制代码

几个常用函数
还是看入队和出队的源码吧,注意bitmap的变化!
从代码中可以知道,调用了LOS_ListTailInsert(&priQueueList[priority], priqueueItem); 注意是从循环链表的尾部插入的,也就是同等优先级的TASK被排在了最后一个执行,只要每次都是从尾部插入,就形成了一个按顺序执行的队列。鸿蒙内核的设计可谓非常巧妙,用极少的代码,极高的效率实现了队列功能。
  1. VOID OsPriQueueEnqueue(LOS_DL_LIST *priQueueList, UINT32 *bitMap, LOS_DL_LIST *priqueueItem, UINT32 priority)
  2. {
  3.     /*
  4.      * Task control blocks are inited as zero. And when task is deleted,
  5.      * and at the same time would be deleted from priority queue or
  6.      * other lists, task pend node will restored as zero.
  7.      */
  8.     LOS_ASSERT(priqueueItem->pstNext == NULL);

  9.     if (LOS_ListEmpty(&priQueueList[priority])) {
  10.         *bitMap |= PRIQUEUE_PRIOR0_BIT >> priority;//对应优先级位 置1
  11.     }

  12.     LOS_ListTailInsert(&priQueueList[priority], priqueueItem);
  13. }

  14. VOID OsPriQueueEnqueueHead(LOS_DL_LIST *priQueueList, UINT32 *bitMap, LOS_DL_LIST *priqueueItem, UINT32 priority)
  15. {
  16.     /*
  17.      * Task control blocks are inited as zero. And when task is deleted,
  18.      * and at the same time would be deleted from priority queue or
  19.      * other lists, task pend node will restored as zero.
  20.      */
  21.     LOS_ASSERT(priqueueItem->pstNext == NULL);

  22.     if (LOS_ListEmpty(&priQueueList[priority])) {
  23.         *bitMap |= PRIQUEUE_PRIOR0_BIT >> priority;//对应优先级位 置1
  24.     }

  25.     LOS_ListHeadInsert(&priQueueList[priority], priqueueItem);
  26. }

  27. VOID OsPriQueueDequeue(LOS_DL_LIST *priQueueList, UINT32 *bitMap, LOS_DL_LIST *priqueueItem)
  28. {
  29.     LosTaskCB *task = NULL;
  30.     LOS_ListDelete(priqueueItem);

  31.     task = LOS_DL_LIST_ENTRY(priqueueItem, LosTaskCB, pendList);
  32.     if (LOS_ListEmpty(&priQueueList[task->priority])) {
  33.         *bitMap &= ~(PRIQUEUE_PRIOR0_BIT >> task->priority);//队列空了,对应优先级位 置0
  34.     }
  35. }
复制代码

同一个进程下的线程的优先级可以不一样吗?
请先想一下这个问题。

进程和线程是一对多的父子关系,内核调度的单元是任务(线程),鸿蒙内核中任务和线程是一个东西,只是不同的身份。一个进程可以有多个线程,线程又有各自独立的状态,那进程状态该怎么界定?例如:ProcessA 有 TaskA(阻塞状态) ,TaskB(就绪状态) 两个线程,ProcessA是属于阻塞状态还是就绪状态呢?

先看官方文档的说明后再看源码。

进程状态迁移说明:
  • Init→Ready:
    进程创建或fork时,拿到该进程控制块后进入Init状态,处于进程初始化阶段,当进程初始化完成将进程插入调度队列,此时进程进入就绪状态。
  • Ready→Running:
    进程创建后进入就绪态,发生进程切换时,就绪列表中最高优先级的进程被执行,从而进入运行态。若此时该进程中已无其它线程处于就绪态,则该进程从就绪列表删除,只处于运行态;若此时该进程中还有其它线程处于就绪态,则该进程依旧在就绪队列,此时进程的就绪态和运行态共存。
  • Running→Pend:
    进程内所有的线程均处于阻塞态时,进程在最后一个线程转为阻塞态时,同步进入阻塞态,然后发生进程切换。
  • Pend→Ready / Pend→Running:
    阻塞进程内的任意线程恢复就绪态时,进程被加入到就绪队列,同步转为就绪态,若此时发生进程切换,则进程状态由就绪态转为运行态。
  • Ready→Pend:
    进程内的最后一个就绪态线程处于阻塞态时,进程从就绪列表中删除,进程由就绪态转为阻塞态。
  • Running→Ready:
    进程由运行态转为就绪态的情况有以下两种:
    • 有更高优先级的进程创建或者恢复后,会发生进程调度,此刻就绪列表中最高优先级进程变为运行态,那么原先运行的进程由运行态变为就绪态。
    • 若进程的调度策略为SCHED_RR,且存在同一优先级的另一个进程处于就绪态,则该进程的时间片消耗光之后,该进程由运行态转为就绪态,另一个同优先级的进程由就绪态转为运行态。
  • Running→Zombies:
    当进程的主线程或所有线程运行结束后,进程由运行态转为僵尸态,等待父进程回收资源。
注意看上面红色的部分,一个进程竟然可以两种状态共存!
  1.     UINT16               processStatus;                /**< [15:4] process Status; [3:0] The number of threads currently
  2.                                                             running in the process */

  3.     processCB->processStatus &= ~(status | OS_PROCESS_STATUS_PEND);//取反后的与位运算
  4.     processCB->processStatus |= OS_PROCESS_STATUS_READY;//或位运算
复制代码

一个变量存两种状态,怎么做到的?答案还是 按位保存啊。还记得上面的位图调度 g_priQueueBitmap吗,那可是存了32种状态的。其实这在任何一个系统的内核源码中都很常见,类似的还有 左移 <<,右移 >>等等
继续说进程和线程的关系,线程的优先级必须和进程一样吗?他们可以不一样吗?答案是:可以不一样,否则怎么会有设置task优先级的函数。其实在调度过程中如果遇到阻塞,内核往往会提高持有锁的task的优先级,让它能以最大概率被下一轮调度选中而快速释放锁资源。

线程调度器
真正让CPU工作的是线程,进程只是个装线程的容器,线程有任务栈空间,是独立运行于内核空间,而进程只有用户空间,具体在后续的内存篇会讲,这里不展开说,但进程结构体LosProcessCB 有一个这样的定义。看名字就知道了,那是跟调度相关的。
  1.     UINT32               threadScheduleMap;            /**< The scheduling bitmap table for the thread group of the
  2.                                                             process */
  3.     LOS_DL_LIST          threadPriQueueList[OS_PRIORITY_QUEUE_NUM]; /**< The process's thread group schedules the
  4.                                                                          priority hash table */
复制代码
咋一看怎么进程的结构体里也有32个队列,其实这就是线程的就绪状态队列。threadScheduleMap就是进程自己的位图调度器。具体看进程入队和出队的源码。调度过程是先去进程就绪队列里找最高优先级的进程,然后去该进程找最高优先级的线程来调度。具体看笔者认为的内核最美函数OsGetTopTask,能欣赏到他的美就读懂了就绪队列是怎么管理的。
  1. LITE_OS_SEC_TEXT_MINOR LosTaskCB *OsGetTopTask(VOID)
  2. {
  3.     UINT32 priority, processPriority;
  4.     UINT32 bitmap;
  5.     UINT32 processBitmap;
  6.     LosTaskCB *newTask = NULL;
  7. #if (LOSCFG_KERNEL_SMP == YES)
  8.     UINT32 cpuid = ArchCurrCpuid();
  9. #endif
  10.     LosProcessCB *processCB = NULL;
  11.     processBitmap = g_priQueueBitmap;
  12.     while (processBitmap) {
  13.         processPriority = CLZ(processBitmap);
  14.         LOS_DL_LIST_FOR_EACH_ENTRY(processCB, &g_priQueueList[processPriority], LosProcessCB, pendList) {
  15.             bitmap = processCB->threadScheduleMap;
  16.             while (bitmap) {
  17.                 priority = CLZ(bitmap);
  18.                 LOS_DL_LIST_FOR_EACH_ENTRY(newTask, &processCB->threadPriQueueList[priority], LosTaskCB, pendList) {
  19. #if (LOSCFG_KERNEL_SMP == YES)
  20.                     if (newTask->cpuAffiMask & (1U << cpuid)) {
  21. #endif
  22.                         newTask->taskStatus &= ~OS_TASK_STATUS_READY;
  23.                         OsPriQueueDequeue(processCB->threadPriQueueList,
  24.                                           &processCB->threadScheduleMap,
  25.                                           &newTask->pendList);
  26.                         OsDequeEmptySchedMap(processCB);
  27.                         goto OUT;
  28. #if (LOSCFG_KERNEL_SMP == YES)
  29.                     }
  30. #endif
  31.                 }
  32.                 bitmap &= ~(1U << (OS_PRIORITY_QUEUE_NUM - priority - 1));
  33.             }
  34.         }
  35.         processBitmap &= ~(1U << (OS_PRIORITY_QUEUE_NUM - processPriority - 1));
  36.     }

  37. OUT:
  38.     return newTask;
  39. }
复制代码

映射张大爷的故事:张大爷喊到张全蛋时进场时表演时,张全蛋要决定自己的哪个节目先表演,也要查下他的清单上优先级,它同样也有个张大爷同款记分牌,就这么简单。
喜欢的记得点赞分享,更多文章去 鸿蒙系统源码分析(总目录)查看
文章来自:图解鸿蒙源码逐行注释分析

评论

您需要登录后才可以回帖 登录 | 注册

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容图片侵权或者其他问题,请联系本站作侵删。 侵权投诉
发文章