鸿蒙内核源码分析,鸿蒙内核源码分析(调度机制篇)|解读鸿蒙源码

提示:本文基于开源鸿蒙内核分析,官方源码【kernel_liteos_a】,官方文档【docs】

本文作者:鸿蒙内核发烧友,将持续研究鸿蒙内核,更新博文,敬请关注。内容仅代表个人观点,错误之处,欢迎大家指正完善。

本文分析任务调度机制源码 详见:../kernel/base/sched/sched_sq/los_sched.c

目录

建议先阅读

先说几个概念

进程和线程的状态迁移图

调度是如何触发的?

调度过程

OsGetTopTask()

鸿蒙内核源码分析系列其他文章

建议先阅读

阅读之前建议先读本系列以下几篇文章

鸿蒙内核源码分析(Task/线程管理篇)

鸿蒙内核源码分析(进程管理篇)

鸿蒙内核源码分析(调度队列篇)

以便对本文任务调度机制的理解。

先说几个概念

鸿蒙的内核中 Task 和 线程 在广义上可以理解为是一个东西,区别在于管理体系的不同,Task是调度层面,线程是进程层面。但狭义上肯定会有区别,比如 main() 函数中首个函数 OsSetMainTask(); 就是设置启动任务,但此时啥都还没开始呢,Kprocess 进程都没创建,怎么会有大家普通意义上所理解的线程呢。狭义上的后续有 鸿蒙内核源码分析(启动过程篇) 来说明。不知道大家有没有这种体会,学一个东西的过程中要接触很多新概念,尤其像 Java/android 的生态,概念贼多,很多同学都被绕在概念中出不来,痛苦不堪。那问题是为什么需要这么多的概念?

举个例子就明白了:

假如您去深圳参加一个面试老板问你哪里人?你会说是 江西人,湖南人... 而不会说是张家村二组的张全蛋,这样还谁敢要你。但如果你参加同乡会别人问你同样问题,你也不会说是来自东北那旮沓的。明白了吗?张全蛋还是那个张全蛋,但因为场景变了,您的说法就得必须跟着变,否则没法愉快的聊天。

那程序设计就是源于生活,归于生活,大家对程序的理解就是要用生活中的场景去打比方,更好的理解概念。你说呢?

那在内核的调度层面,咱们就说task, task是内核调度的单元,调度就是围着它转。

进程和线程的状态迁移图

先看看task从哪些渠道产生:

渠道很多,可能是shell 的一个命令,也可能由内核创建,更多的是大家编写应用程序new出来的一个线程。

调度的内容已经有了,那他们如何有序的被调度?答案是:靠32个进程就绪队列。为什么是32个,系列文章里有详细说明,自己去翻。

这张进程状态迁移示意图一定要看明白,线程的状态迁移大家去官方文档看,不一一列出来,太多了占地方。

注意只有就绪状态才有队列,因为就绪就意味着工作都准备好了就等着CPU来执行了。有三种情况会加入就绪队列

Init→Ready:

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

Pend→Ready / Pend→Running:

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

Running→Ready:

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

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

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

调度是如何触发的?

调度程序让task各就各位,在其生命周期内不停的进度状态流转,那是什么让调度去工作的,它是如何触发的?

笔者能想到的触发方式是以下三个

Tick(时钟管理),类似于JAVA的定时任务,时间到了就触发。系统定时器是内核时间机制中最重要的一部分,它提供了一种周期性触发中断机制,即系统定时器以HZ(时钟节拍率)为频率自行触发时钟中断。当时钟中断发生时,内核就通过时钟中断处理程序timer_interrupt()对其进行处理。鸿蒙内核默认是10ms触发一次,执行以下函数:

/*

* Description : Tick interruption handler

*/

LITE_OS_SEC_TEXT VOID OsTickHandler(VOID)

{

UINT32 intSave;

TICK_LOCK(intSave);

g_tickCount[ArchCurrCpuid()]++;

TICK_UNLOCK(intSave);

#ifdef LOSCFG_KERNEL_VDSO

OsUpdateVdsoTimeval();

#endif

#ifdef LOSCFG_KERNEL_TICKLESS

OsTickIrqFlagSet(OsTicklessFlagGet());

#endif

#if (LOSCFG_BASE_CORE_TICK_HW_TIME == YES)

HalClockIrqClear(); /* diff from every platform */

#endif

OsTimesliceCheck();

OsTaskScan(); /* task timeout scan *///*kyf 任务扫描,发起调度

#if (LOSCFG_BASE_CORE_SWTMR == YES)

OsSwtmrScan();

#endif

}

里面对任务进行了扫描,调用任务调度

第二个是各种软硬中断,如何USB插拔,键盘,鼠标这些外设引起的中断。

第三个是程序主动中断,比如运行过程中需要申请其他资源,资源被占用的时候触发的中断

最后一个是创建一个新任务后主动发起的抢占式调度

这是申请调用调度的地方

这里提下 OsCopyProcess(), 这是fork进程的主体函数,可以看出fork之后立即申请了一次调度。

LITE_OS_SEC_TEXT INT32 LOS_Fork(UINT32 flags, const CHAR *name, const TSK_ENTRY_FUNC entry, UINT32 stackSize)

{

UINT32 cloneFlag = CLONE_PARENT | CLONE_THREAD | CLONE_VFORK | CLONE_FILES;

if (flags & (~cloneFlag)) {

PRINT_WARN("Clone dont support some flags!\n");

}

flags |= CLONE_FILES;

return OsCopyProcess(cloneFlag & flags, name, (UINTPTR)entry, stackSize);

}

调度过程

以上是需要提前了解的信息,接下来直接上源码看调度过程吧,文件就三个函数。

VOID OsSchedResched(VOID)

{

LOS_ASSERT(LOS_SpinHeld(&g_taskSpin));//*kyf 调度过程要上锁

newTask = OsGetTopTask(); //*kyf 获取最高优先级任务

OsSchedSwitchProcess(runProcess, newProcess);//*kyf 切换运行的进程

(VOID)OsTaskSwitchCheck(runTask, newTask);

OsCurrTaskSet((VOID*)newTask);//*kyf 设置当前任务

if (OsProcessIsUserMode(newProcess)) {

OsCurrUserTaskSet(newTask->userArea);//*kyf 运行空间

}

/* do the task context switch */

OsTaskSchedule(newTask, runTask); //*kyf 切换任务上下文

}

函数有点长,笔者留了最重要的几行,看这几行就够了,流程如下:

调度过程要自旋锁,不允许任何中断发生,没错,说的是任何事是不能去打断它,否则后果太严重了,这可是在切换进程和线程的操作啊。

在就绪队列里找个最高优先级的task

切换进程,就是task/线程 归属的那个进程为当前进程

设置它为当前任务

用户模式需要设置运行空间,因为每个进程的空间是不一样的

是最重要的,切换任务上下文,参数是新老两个任务,一个要保存现场,一个要恢复线程。

什么是任务上下文?看鸿蒙内核分析系列其他文章,有专门的介绍。这里要说明的是 在CPU的层面,它只认任务上下文!

这里看不到任何代码了,因为这是跟CPU相关的,不同的CPU需要去适配不同的汇编代码,所以这些汇编代码不会出现在一个通用工程中。请留意后续 鸿蒙内核源码分析(汇编指令篇)。

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;

}

#ifdef__cplusplus

#if__cplusplus

}

本篇就先写这么多吧,鸿蒙内核源码虽然文件不多,但关系及其复杂,拆解源码是件辛苦也是快乐的事,编写成文分享给大家更是件痛并快乐着的事,喜欢的就点个赞吧

你可能感兴趣的:(鸿蒙内核源码分析)