0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

浅谈鸿蒙内核代码调度队列

鸿蒙系统HarmonyOS 来源:oschina 作者:鸿蒙内核发烧友 2020-10-23 11:00 次阅读

为何单独讲调度队列?

鸿蒙内核代码中有两个源文件是关于队列的,一个是用于调度的队列,另一个是用于线程间通讯的IPC队列。

本文详细讲述调度队列,详见代码: kernel_liteos_a/kernel/base/sched/sched_sq/los_priqueue.c

IPC队列后续有专门的博文讲述,这两个队列的数据结构实现采用的都是双向循环链表,LOS_DL_LIST实在是太重要了,是理解鸿蒙内核的关键,说是最重要的代码一点也不为过,源码出现在 sched_sq模块,说明是用于任务的调度的,sched_sq模块只有两个文件,另一个los_sched.c就是调度代码。

涉及函数

功能分类 接口 描述
创建队列 OsPriQueueInit 创建了32个就绪队列
获取最高优先级队列 OsPriQueueTop 查最高优先级任务
从头部入队列 OsPriQueueEnqueueHead 从头部插入某个就绪队列
从尾部入队列 OsPriQueueEnqueue 默认是从尾部插入某个就绪队列
出队列 OsPriQueueDequeue 从最高优先级的就绪队列中删除
OsPriQueueProcessDequeue 从进程队列中删除
OsPriQueueProcessSize 用进程查队列中元素个数
OsPriQueueSize 用任务查队列中元素个数
OsTaskPriQueueTop 查最高优先级任务
OsDequeEmptySchedMap 进程出列
OsGetTopTask 获取被调度选择的task

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

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

位图调度器

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

#ifndef CLZ
#define CLZ(value)                                  (__clz(value)) //汇编指令
#endif

LITE_OS_SEC_BSS LOS_DL_LIST *g_priQueueList = NULL; //所有的队列 原始指针
LITE_OS_SEC_BSS UINT32 g_priQueueBitmap; // 位图调度
// 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.

队列初始化做了哪些工作?详细看代码

#define OS_PRIORITY_QUEUE_NUM 32

UINT32 OsPriQueueInit(VOID)
{
    UINT32 priority;

    /* system resident resource */
    g_priQueueList = (LOS_DL_LIST *)LOS_MemAlloc(m_aucSysMem0, (OS_PRIORITY_QUEUE_NUM * sizeof(LOS_DL_LIST)));
    if (g_priQueueList == NULL) {
        return LOS_NOK;
    }

    for (priority = 0; priority < OS_PRIORITY_QUEUE_NUM; ++priority) {
        LOS_ListInit(&g_priQueueList[priority]);
    }
    return LOS_OK;
}

因TASK 有32个优先级,在初始化时内核一次性创建了32个双向循环链表,每种优先级都有一个队列来记录就绪状态的tasks的位置,g_priQueueList分配的是一个连续的内存块,存放了32个LOS_DL_LIST,再看一下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;

几个常用函数

还是看入队和出队的源码吧,注意bitmap的变化!

从代码中可以知道,调用了LOS_ListTailInsert(&priQueueList[priority], priqueueItem); 注意是从循环链表的尾部插入的,也就是同等优先级的TASK被排在了最后一个执行,只要每次都是从尾部插入,就形成了一个按顺序执行的队列。鸿蒙内核的设计可谓非常巧妙,用极少的代码,极高的效率实现了队列功能。

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

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

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

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

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

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

VOID OsPriQueueDequeue(LOS_DL_LIST *priQueueList, UINT32 *bitMap, LOS_DL_LIST *priqueueItem)
{
    LosTaskCB *task = NULL;
    LOS_ListDelete(priqueueItem);

    task = LOS_DL_LIST_ENTRY(priqueueItem, LosTaskCB, pendList);
    if (LOS_ListEmpty(&priQueueList[task->priority])) {
        *bitMap &= ~(PRIQUEUE_PRIOR0_BIT >> task->priority);//队列空了,对应优先级位 置0
    }
}

同一个进程下的线程的优先级可以不一样吗?

请先想一下这个问题。

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

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

进程状态迁移说明:

Init→Ready:

进程创建或fork时,拿到该进程控制块后进入Init状态,处于进程初始化阶段,当进程初始化完成将进程插入调度队列,此时进程进入就绪状态。

Ready→Running:

进程创建后进入就绪态,发生进程切换时,就绪列表中最高优先级的进程被执行,从而进入运行态。若此时该进程中已无其它线程处于就绪态,则该进程从就绪列表删除,只处于运行态;若此时该进程中还有其它线程处于就绪态,则该进程依旧在就绪队列,此时进程的就绪态和运行态共存。

Running→Pend:

进程内所有的线程均处于阻塞态时,进程在最后一个线程转为阻塞态时,同步进入阻塞态,然后发生进程切换。

Pend→Ready / Pend→Running:

阻塞进程内的任意线程恢复就绪态时,进程被加入到就绪队列,同步转为就绪态,若此时发生进程切换,则进程状态由就绪态转为运行态。

Ready→Pend:

进程内的最后一个就绪态线程处于阻塞态时,进程从就绪列表中删除,进程由就绪态转为阻塞态。

Running→Ready:

进程由运行态转为就绪态的情况有以下两种:

有更高优先级的进程创建或者恢复后,会发生进程调度,此刻就绪列表中最高优先级进程变为运行态,那么原先运行的进程由运行态变为就绪态。

若进程的调度策略为SCHED_RR,且存在同一优先级的另一个进程处于就绪态,则该进程的时间片消耗光之后,该进程由运行态转为就绪态,另一个同优先级的进程由就绪态转为运行态。

Running→Zombies:

当进程的主线程或所有线程运行结束后,进程由运行态转为僵尸态,等待父进程回收资源。

注意看上面红色的部分,一个进程竟然可以两种状态共存!

UINT16 processStatus; /**< [15:4] process Status; [3:0] The number of threads currently

                                                            running in the process */

    processCB->processStatus &= ~(status | OS_PROCESS_STATUS_PEND);//取反后的与位运算
    processCB->processStatus |= OS_PROCESS_STATUS_READY;//或位运算

一个变量存两种状态,怎么做到的?答案还是按位保存啊。还记得上面的位图调度g_priQueueBitmap吗,那可是存了32种状态的。其实这在任何一个系统的内核源码中都很常见,类似的还有左移 <<,右移 >>等等

继续说进程和线程的关系,线程的优先级必须和进程一样吗?他们可以不一样吗?答案是:可以不一样,否则怎么会有设置task优先级的函数。

线程调度器

真正让CPU工作的是线程,进程只是个装线程的容器,线程有任务栈空间,是独立运行于内核空间,而进程只有用户空间,具体在后续的内存篇会讲,这里不展开说,但进程结构体LosProcessCB有一个这样的定义。看名字就知道了,那是跟调度相关的。

    UINT32               threadScheduleMap;            /**< The scheduling bitmap table for the thread group of the
                                                            process */
    LOS_DL_LIST          threadPriQueueList[OS_PRIORITY_QUEUE_NUM]; /**< The process's thread group schedules the
                                                                         priority hash table */

咋一看怎么进程的结构体里也有32个队列,其实这就是线程的就绪状态队列。threadScheduleMap就是进程自己的位图调度器。具体看进程入队和出队的源码。调度过程是先去进程就绪队列里找最高优先级的进程,然后去该进程找最高优先级的线程来调度。具体看笔者认为的内核最美函数OsGetTopTask,能欣赏到他的美就读懂了就绪队列是怎么管理的。

LITE_OS_SEC_TEXT_MINOR LosTaskCB *OsGetTopTask(VOID)
{
    UINT32 priority, processPriority;
    UINT32 bitmap;
    UINT32 processBitmap;
    LosTaskCB *newTask = NULL;
#if (LOSCFG_KERNEL_SMP == YES)
    UINT32 cpuid = ArchCurrCpuid();
#endif
    LosProcessCB *processCB = NULL;
    processBitmap = g_priQueueBitmap;
    while (processBitmap) {
        processPriority = CLZ(processBitmap);
        LOS_DL_LIST_FOR_EACH_ENTRY(processCB, &g_priQueueList[processPriority], LosProcessCB, pendList) {
            bitmap = processCB->threadScheduleMap;
            while (bitmap) {
                priority = CLZ(bitmap);
                LOS_DL_LIST_FOR_EACH_ENTRY(newTask, &processCB->threadPriQueueList[priority], LosTaskCB, pendList) {
#if (LOSCFG_KERNEL_SMP == YES)
                    if (newTask->cpuAffiMask & (1U << cpuid)) {
#endif
                        newTask->taskStatus &= ~OS_TASK_STATUS_READY;
                        OsPriQueueDequeue(processCB->threadPriQueueList,
                                          &processCB->threadScheduleMap,
                                          &newTask->pendList);
                        OsDequeEmptySchedMap(processCB);
                        goto OUT;
#if (LOSCFG_KERNEL_SMP == YES)
                    }
#endif
                }
                bitmap &= ~(1U << (OS_PRIORITY_QUEUE_NUM - priority - 1));
            }
        }
        processBitmap &= ~(1U << (OS_PRIORITY_QUEUE_NUM - processPriority - 1));
    }

OUT:
    return newTask;
}
映射张大爷的故事:张大爷喊到张全蛋时进场时表演时,张全蛋要决定自己的哪个节目先表演,也要查下他的清单上优先级,它同样也有个张大爷同款记分牌,就这么简单。
编辑:hfy
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 调度器
    +关注

    关注

    0

    文章

    98

    浏览量

    5230
  • 鸿蒙系统
    +关注

    关注

    183

    文章

    2633

    浏览量

    66119
收藏 人收藏

    评论

    相关推荐

    鸿蒙内核源码Task/线程技术分析

    、使用内存空间等系统资源,并独立于其它线程运行。 鸿蒙内核每个进程内的线程独立运行、独立调度,当前进程内线程的调度不受其它进程内线程的影响。 鸿蒙
    的头像 发表于 10-18 10:42 2158次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b>源码Task/线程技术分析

    (转)HarmonyOS(鸿蒙OS)发布,聊聊操作系统的调度

    内核,但不是这篇。 本文想再谈谈关于人机交互操作系统本身以及微内核调度等操作系统比较核心的问题。 也许,鸿蒙内核确实对
    发表于 08-20 08:00

    【HarmonyOS】鸿蒙内核源码分析(调度机制篇)

    看task从哪些渠道产生:渠道很多,可能是shell 的一个命令,也可能由内核创建,更多的是大家编写应用程序new出来的一个线程。调度的内容已经有了,那他们如何有序的被调度?答案:是32个进程和线程就绪
    发表于 10-14 14:00

    鸿蒙源码分析系列(总目录) | 给HarmonyOS源码逐行加上中文注释

    |-鸿蒙内核源码分析(调度机制篇) | 任务是如何被调度执行的|-鸿蒙内核源码分析(
    发表于 11-20 11:24

    鸿蒙内核源码分析(调度机制篇):Task是如何被调度执行的

    本文分析任务调度机制源码 详见:代码库建议先阅读阅读之前建议先读本系列其他文章,进入鸿蒙系统源码分析(总目录),以便对本文任务调度机制的理解。为什么学一个东西要学那么多的概念?
    发表于 11-23 10:53

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

    为何单独讲调度队列鸿蒙内核代码中有两个源文件是关于队列的,一个是用于
    发表于 11-23 11:09

    鸿蒙内核源码分析(Task管理篇):task是内核调度的单元

    独立运行、独立调度,当前进程内线程的调度不受其它进程内线程的影响。鸿蒙内核中的线程采用抢占式调度机制,同时支持时间片轮转
    发表于 11-23 14:01

    鸿蒙内核源码分析(Task管理篇):task是内核调度的单元

    )代码 ,这是怎么回事?其实在鸿蒙内核中, task就是线程, 初学者完全可以这么理解,但二者还是有区别,否则干嘛要分两个词描述。到底有什么区别?是管理上的区别,task是调度层面的概
    发表于 11-24 10:24

    VxWorks实时内核调度的研究分析

    VxWorks实时内核调度的研究分析论述了0S中调度的概念、类型、调度队列模型,并着重对VxWorks实时
    发表于 12-16 14:07 13次下载

    Vx Works实时内核调度的研究分析

    论述了OS 中调度的概念、类型、调度队列模型,并着重对VxWorks 实时内核进行了分析。关键词:嵌入式实时操作系统(RTOS) ;VxWorks ;
    发表于 03-25 10:36 33次下载

    VxWorks实时内核调度的研究分析

    论述了0S中调度的概念、类型、调度队列模型,并着重对VxWorks实时内核进行了分析。
    发表于 11-27 16:22 16次下载

    浅谈鸿蒙内核源码的栈

    上面的代码鸿蒙内核用栈方式一样,都采用了递减满栈的方式, 什么是递减满栈?
    的头像 发表于 04-24 11:21 1406次阅读
    <b class='flag-5'>浅谈</b><b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b>源码的栈

    鸿蒙内核源码:谁来触发调度工作?

    鸿蒙内核中 Task 和 线程 在广义上可以理解为是一个东西,但狭义上肯定会有区别,区别在于管理体系的不同,Task是调度层面的概念,线程是进程层面概念。
    的头像 发表于 04-24 10:50 1460次阅读
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b>源码:谁来触发<b class='flag-5'>调度</b>工作?

    鸿蒙内核源码分析:task是内核调度的单元

    从系统的角度看,线程是竞争系统资源的最小运行单元。线程可以使用或等待CPU、使用内存空间等系统资源,并独立于其它线程运行。 鸿蒙内核每个进程内的线程独立运行、独立调度,当前进程内线程的调度
    发表于 11-23 15:51 22次下载
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b>源码分析:task是<b class='flag-5'>内核</b><b class='flag-5'>调度</b>的单元

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

    鸿蒙内核代码中有两个源文件是关于队列的,一个是用于调度队列,另一个是用于线程间通讯的IPC
    发表于 11-23 15:48 31次下载
    <b class='flag-5'>鸿蒙</b><b class='flag-5'>内核</b>源码分析:进程和Task的就绪<b class='flag-5'>队列</b>对<b class='flag-5'>调度</b>的作用