在SylixOS 中退出中断和内核都会系统调度任务,任务调度底层切换上下文,底层实现参考链接
在SyliOS默认优先级是0-255,数值越小优先级越高。在SylixOS 任务就绪队列是通过位图的方式去查找,如果当前优先级有任务则对应的位图位就会变成1。在查找到最后优先级后,从对应链表中取出任务控制块tcb,对比是否比当前任务优先级高,决定是否切换任务。
在arm中支持硬件实现前导零计数,所以能够快速的从位图中查找出来。
首先看几个重要的结构体
第一个结构体体是位图,由于32位并不能够满足0-255优先级的需求 ,所以这里使用了两级位图。
第二个结构体是对应优先级的队列。每个优先级都有一个对应的队列,如果要找此优先级的任务,就从这个队列中查找。
第三个结构体是上面两个结构的结合。将位图,链表结合在一起就实现了查找就绪任务时间复杂度为O(1)。
在内核退出的时候会调用_Schedule函数
/*********************************************************************************************************
** 函数名称: Schedule
** 功能描述: 内核退出时, 会调用此调度函数 (进入内核状态并关中断被调用)
** 输 入 : NONE
** 输 出 : 线程上下文返回值
** 全局变量:
** 调用模块:
*********************************************************************************************************/
INT _Schedule (VOID)
{
ULONG ulCPUId;
PLW_CLASS_CPU pcpuCur;
PLW_CLASS_TCB ptcbCur;
PLW_CLASS_TCB ptcbCand;
INT iRetVal = ERROR_NONE;
ulCPUId = LW_CPU_GET_CUR_ID(); /* 当前 CPUID */
pcpuCur = LW_CPU_GET(ulCPUId); /* 当前 CPU 控制块 */
ptcbCur = pcpuCur->CPU_ptcbTCBCur;
#if LW_CFG_SMP_EN > 0
if (LW_UNLIKELY(ptcbCur->TCB_plineStatusReqHeader)) { /* 请求当前任务改变状态 */
if (__LW_STATUS_CHANGE_EN(ptcbCur, pcpuCur)) { /* 是否可以进行状态切换 */
_ThreadStatusChangeCur(pcpuCur); /* 检查是否需要进行状态切换 */
}
}
#endif /* LW_CFG_SMP_EN */
ptcbCand = _SchedGetCand(pcpuCur, 1ul); /* 获得需要运行的线程 */
if (ptcbCand != ptcbCur) { /* 如果与当前运行的不同, 切换 */
__LW_SCHEDULER_BUG_TRACE(ptcbCand); /* 调度器 BUG 检测 */
#if LW_CFG_SMP_EN > 0 /* SMP 系统 */
__LW_SPINLOCK_BUG_TRACE(pcpuCur);
#endif /* LW_CFG_SMP_EN > 0 */
pcpuCur->CPU_bIsIntSwitch = LW_FALSE; /* 非中断调度 */
pcpuCur->CPU_ptcbTCBHigh = ptcbCand;
/*
* TASK CTX SAVE();
* SWITCH to SAFE stack if in SMP system;
* _SchedSwp();
* TASK CTX LOAD();
*/
archTaskCtxSwitch(pcpuCur); /* 线程切换,并释放内核自旋锁 */
#if !defined(__SYLIXOS_ARM_ARCH_M__) || (LW_CFG_CORTEX_M_SVC_SWITCH > 0)
LW_SPIN_KERN_LOCK_IGNIRQ(); /* 内核自旋锁重新加锁 */
#endif /* LW_CFG_CORTEX_M_SVC_SWITCH */
}
#if LW_CFG_SMP_EN > 0 /* SMP 系统 */
else {
__LW_SMP_NOTIFY(ulCPUId); /* SMP 调度通知 */
return (iRetVal);
}
#endif /* LW_CFG_SMP_EN */
LW_TCB_GET_CUR(ptcbCur); /* 获得新的当前 TCB */
iRetVal = ptcbCur->TCB_iSchedRet; /* 获得调度器信号的返回值 */
ptcbCur->TCB_iSchedRet = ERROR_NONE; /* 清空 */
return (iRetVal);
}
_Schedule函数首先判断当前任务的状态是否需要改变,如果需要切换任务状态。
然后调用_SchedGetCand函数查找需要运行的任务。
PLW_CLASS_TCB _SchedGetCand (PLW_CLASS_CPU pcpuCur, ULONG ulCurMaxLock)
{
if (!__COULD_SCHED(pcpuCur, ulCurMaxLock)) { /* 当前执行线程不能调度 */
return (pcpuCur->CPU_ptcbTCBCur);
} else { /* 可以执行线程切换 */
#if LW_CFG_SMP_EN > 0
LW_CPU_CLR_SCHED_IPI_PEND(pcpuCur);
#endif /* LW_CFG_SMP_EN > 0 */
if (LW_CAND_ROT(pcpuCur)) { /* 产生优先级卷绕 */
_CandTableUpdate(pcpuCur); /* 尝试更新候选表, 抢占调度 */
}
return (LW_CAND_TCB(pcpuCur));
}
}
此函数首先通过__COULD_SCHED判断是否能够调度任务,在中断嵌套,或者任务被锁住禁止调度等是不能够调度的。__COULD_SCHED函数这里就不详细展开。
如果是多核SMP 情况下会使用LW_CPU_CLR_SCHED_IPI_PEND 发送一个核间中断,通知其他核心进行调度。先不讨论核间中断的实现细节(不同处理器实现不同)。
_CandTableUpdate函数是实现找到最高优先级任务的函数,函数的内容如下:
VOID _CandTableUpdate (PLW_CLASS_CPU pcpu)
{
UINT8 ucPriority;
REGISTER PLW_CLASS_TCB ptcbCand;
PLW_CLASS_PCBBMAP ppcbbmap;
BOOL bNeedRotate = LW_FALSE;
#if LW_CFG_SMP_EN > 0 /* SMP 多核 */
PLW_CLASS_TCB ptcbNew;
#endif
if (!LW_CPU_IS_ACTIVE(pcpu)) { /* CPU 必须为激活状态 */
return;
}
ptcbCand = LW_CAND_TCB(pcpu);
if (ptcbCand == LW_NULL) { /* 当前没有候选线程 */
_CandTableFill(pcpu);
goto __update_done;
}
ppcbbmap = _CandTableSeek(pcpu, &ucPriority); /* 当前就绪表中最高优先级 */
if (ppcbbmap == LW_NULL) {
LW_CAND_ROT(pcpu) = LW_FALSE; /* 清除优先级卷绕标志 */
return;
}
#if LW_CFG_SMP_EN > 0
if (LW_CPU_ONLY_AFFINITY_GET(pcpu) && !ptcbCand->TCB_bCPULock) { /* 强制运行亲和度任务 */
bNeedRotate = LW_TRUE; /* 当前普通任务需要让出 CPU */
} else
#endif /* LW_CFG_SMP_EN > 0 */
{
if (ptcbCand->TCB_usSchedCounter == 0) { /* 已经没有时间片了 */
if (LW_PRIO_IS_HIGH_OR_EQU(ucPriority,
ptcbCand->TCB_ucPriority)) { /* 是否需要轮转 */
bNeedRotate = LW_TRUE;
}
} else {
if (LW_PRIO_IS_HIGH(ucPriority,
ptcbCand->TCB_ucPriority)) {
bNeedRotate = LW_TRUE;
}
}
}
if (bNeedRotate) { /* 存在更需要运行的线程 */
_CandTableEmpty(pcpu); /* 清空候选表 */
_CandTableResel(pcpu, ppcbbmap, ucPriority); /* 重新选择任务执行 */
#if LW_CFG_SMP_EN > 0 /* SMP 多核 */
ptcbNew = LW_CAND_TCB(pcpu);
if ((ptcbNew != ptcbCand) && !ptcbCand->TCB_bCPULock) { /* 是否需要尝试标记其他 CPU */
_CandTableNotify(pcpu, ptcbCand); /* 通知其他 CPU 进行调度查看 */
}
#endif /* LW_CFG_SMP_EN > 0 */
}
__update_done:
LW_CAND_ROT(pcpu) = LW_FALSE; /* 清除优先级卷绕标志 */
}
从上面的代码中可以看到在进行了一些情况判断后,调用了_CandTableSeek函数。_CandTableSeek函数实现的功能是在0-255优先级中那个有任务需要调度。如果存在有优先级比当前任务高的或者当前任务的时间片已经用完,那么从这个优先级对应的链表中选出任务。_CandTableResel函数就是实现选处任务的。
_CandTableSeek 是一个内联函数
static LW_INLINE PLW_CLASS_PCBBMAP _CandTableSeek (PLW_CLASS_CPU pcpu, UINT8 *pucPriority)
{
#if LW_CFG_SMP_EN > 0
REGISTER UINT8 ucGlobal;
if (_BitmapIsEmpty(LW_CPU_RDY_BMAP(pcpu))) {
if (LW_CPU_ONLY_AFFINITY_GET(pcpu) ||
_BitmapIsEmpty(LW_GLOBAL_RDY_BMAP())) { /* 就绪表为空 */
return (LW_NULL);
}
*pucPriority = _BitmapHigh(LW_GLOBAL_RDY_BMAP());
return (LW_GLOBAL_RDY_PCBBMAP()); /* 从全局就绪表选择 */
} else {
*pucPriority = _BitmapHigh(LW_CPU_RDY_BMAP(pcpu)); /* 本地就绪表最高优先级获取 */
if (LW_CPU_ONLY_AFFINITY_GET(pcpu) ||
_BitmapIsEmpty(LW_GLOBAL_RDY_BMAP())) {
return (LW_CPU_RDY_PCBBMAP(pcpu)); /* 选择本地就绪任务 */
}
ucGlobal = _BitmapHigh(LW_GLOBAL_RDY_BMAP());
if (LW_PRIO_IS_HIGH_OR_EQU(*pucPriority, ucGlobal)) { /* 同优先级, 优先执行 local */
return (LW_CPU_RDY_PCBBMAP(pcpu));
} else {
*pucPriority = ucGlobal;
return (LW_GLOBAL_RDY_PCBBMAP());
}
}
#else
if (_BitmapIsEmpty(LW_GLOBAL_RDY_BMAP())) { /* 就绪表中无任务 */
return (LW_NULL);
} else {
*pucPriority = _BitmapHigh(LW_GLOBAL_RDY_BMAP());
return (LW_GLOBAL_RDY_PCBBMAP());
}
#endif /* LW_CFG_SMP_EN > 0 */
}
因为一个soc上可能有多个核心,所以存在全局还是本地。全局指的是全部soc,本地指的是当前soc。
查找最高优先级是通过_BitmapHigh函数实现的。
UINT8 _BitmapHigh (PLW_CLASS_BMAP pbmap)
{
UINT32 uiHigh = (UINT32)archFindLsb((INT)pbmap->BMAP_uiMap);
UINT32 uiLow;
if (uiHigh == 0) {
return (0);
}
uiHigh--;
uiLow = (UINT32)archFindLsb((INT)pbmap->BMAP_uiSubMap[uiHigh]) - 1;
return ((UINT8)((uiHigh << 5) | uiLow));
}
此函数首先通过 archFindLsb查找位图的第一层。第一层的每一位对应的32位。查找到位图的第一层后,再次通过archFindLsb查找位图的第二层。最后将两个组合成最大优先级数值。
archFindLsb
FUNC_DEF(archFindLsb)
MOV W1 , #0
SUB W1 , W1 , W0
AND W0 , W1 , W0
MOV W1 , #32
CLZ W0 , W0
SUB W0 , W1 , W0
RET
FUNC_END()
在arm中有个CLZ指令,该指令由硬件实现计算最高位1前面的0个数。比如2前面有30个0,1前面有31个0。我们想找最低位为1的前面有多少个零,所以这里使用
MOV W1 , #0
SUB W1 , W1 , W0
AND W0 , W1 , W0
这三条命令让最低位1保留,其他全部变为0.加入W0传入进来为5,最后W0值是1.因为只保留最低位的1。
调用CLZ命令计算最低位1前面有多少个0.在用32减前面零的个数,就能快速计算出当前最低位1是在哪一位。因为在SylixOS中0-255 值越小优先级越大。所以这里使用的找到最低位为1的位置。
在_BitmapHigh函数中位图第一层已经通过上面方式找到,位图的第二层
uiLow = (UINT32)archFindLsb((INT)pbmap->BMAP_uiSubMap[uiHigh]) - 1;
可以看出也是调用archFindLsb函数查找到最低位1位置。
最后使用下面组合
((UINT8)((uiHigh << 5) | uiLow));
组合成当前最高优先级(也就是0-255当前存在最小值)。
此时查找最高优先级数值的任务就结束,继续回到_CandTableUpdate函数,下面需要从对应的优先级链表中找到任务控制块tcb,以下为_CandTableUpdate函数的部分
ppcbbmap = _CandTableSeek(pcpu, &ucPriority); /* 当前就绪表中最高优先级 */
if (ppcbbmap == LW_NULL) {
LW_CAND_ROT(pcpu) = LW_FALSE; /* 清除优先级卷绕标志 */
return;
}
#if LW_CFG_SMP_EN > 0
if (LW_CPU_ONLY_AFFINITY_GET(pcpu) && !ptcbCand->TCB_bCPULock) { /* 强制运行亲和度任务 */
bNeedRotate = LW_TRUE; /* 当前普通任务需要让出 CPU */
} else
#endif /* LW_CFG_SMP_EN > 0 */
{
if (ptcbCand->TCB_usSchedCounter == 0) { /* 已经没有时间片了 */
if (LW_PRIO_IS_HIGH_OR_EQU(ucPriority,
ptcbCand->TCB_ucPriority)) { /* 是否需要轮转 */
bNeedRotate = LW_TRUE;
}
} else {
if (LW_PRIO_IS_HIGH(ucPriority,
ptcbCand->TCB_ucPriority)) {
bNeedRotate = LW_TRUE;
}
}
}
if (bNeedRotate) { /* 存在更需要运行的线程 */
_CandTableEmpty(pcpu); /* 清空候选表 */
_CandTableResel(pcpu, ppcbbmap, ucPriority); /* 重新选择任务执行 */
#if LW_CFG_SMP_EN > 0 /* SMP 多核 */
ptcbNew = LW_CAND_TCB(pcpu);
if ((ptcbNew != ptcbCand) && !ptcbCand->TCB_bCPULock) { /* 是否需要尝试标记其他 CPU */
_CandTableNotify(pcpu, ptcbCand); /* 通知其他 CPU 进行调度查看 */
}
#endif /* LW_CFG_SMP_EN > 0 */
}
_CandTableSeek执行完毕后,就要查看找出的优先级是否大于当前任务的,还有当前任务的时间片是否到,满足其中一个条件就进行任务的切换。调用_CandTableResel函数找对应的任务控制块tcb。
/*********************************************************************************************************
** 函数名称: _CandTableResel
** 功能描述: 选择一个最该执行线程放入候选表.
** 输 入 : pcpu CPU 结构
** ppcbbmap 优先级位图
** ucPriority 需要运行任务的优先级
** 输 出 : NONE
** 全局变量:
** 调用模块:
*********************************************************************************************************/
static VOID _CandTableResel (PLW_CLASS_CPU pcpu, PLW_CLASS_PCBBMAP ppcbbmap, UINT8 ucPriority)
{
REGISTER PLW_CLASS_TCB ptcb;
REGISTER PLW_CLASS_PCB ppcb;
ptcb = _CandTableNext(ppcbbmap, ucPriority); /* 确定可以候选运行的线程 */
ppcb = &ppcbbmap->PCBM_pcb[ucPriority];
LW_CAND_TCB(pcpu) = ptcb; /* 保存新的可执行线程 */
ptcb->TCB_ulCPUId = LW_CPU_GET_ID(pcpu); /* 记录 CPU 号 */
ptcb->TCB_bIsCand = LW_TRUE; /* 进入候选运行表 */
_DelTCBFromReadyRing(ptcb, ppcb); /* 从就绪环中退出 */
if (_PcbIsEmpty(ppcb)) {
__DEL_RDY_MAP(ptcb); /* 从就绪表中删除 */
}
}
在此函数中首先调用了 _CandTableNext函数,函数是根据优先级,从队列中取出一个任务,函数内容如下:
static PLW_CLASS_TCB _CandTableNext (PLW_CLASS_PCBBMAP ppcbbmap, UINT8 ucPriority)
{
REGISTER PLW_CLASS_PCB ppcb;
REGISTER PLW_CLASS_TCB ptcb;
ppcb = &ppcbbmap->PCBM_pcb[ucPriority];
ptcb = _LIST_ENTRY(ppcb->PCB_pringReadyHeader,
LW_CLASS_TCB,
TCB_ringReady); /* 从就绪环中取出一个线程 */
if (ptcb->TCB_ucSchedPolicy == LW_OPTION_SCHED_FIFO) { /* 如果是 FIFO 直接运行 */
return (ptcb);
} else if (ptcb->TCB_usSchedCounter == 0) { /* 缺少时间片 */
ptcb->TCB_usSchedCounter = ptcb->TCB_usSchedSlice; /* 补充时间片 */
_list_ring_next(&ppcb->PCB_pringReadyHeader); /* 下一个 */
ptcb = _LIST_ENTRY(ppcb->PCB_pringReadyHeader,
LW_CLASS_TCB,
TCB_ringReady);
}
return (ptcb);
}
从就绪链表中取出一个任务后,首先判断调度方式,如果是先进先出则执行执行,如果不是判断是否时间片用完,时间片用完补充时间片,然后从优先级链表中在取出其下一个来执行。
在_CandTableNext执行完成后,最高优先级任务的tcb已经找到,然后在_CandTableResel函数中调用
LW_CAND_TCB(pcpu) = ptcb;
#define LW_CAND_TCB(pcpu) pcpu->CPU_cand.CAND_ptcbCand
此宏是将当前tcb加入到对用cpu的候选任务中。
然后在_CandTableResel函数中调用_DelTCBFromReadyRing函数,将任务从优先级队列中删除。最后调用如下函数
if (_PcbIsEmpty(ppcb)) {
__DEL_RDY_MAP(ptcb); /* 从就绪表中删除 */
}
功能是判断当前优先级队列是否为空,如果为空就将位图的对应优先级的位变为0.
在_CandTableUpdate函数最后,根据情况是否触发核间中断通知其他核心调度。_CandTableUpdate函数执行完毕后
在_SchedGetCand函数执行如下函数
if (LW_CAND_ROT(pcpuCur)) { /* 产生优先级卷绕 */
_CandTableUpdate(pcpuCur); /* 尝试更新候选表, 抢占调度 */
}
return (LW_CAND_TCB(pcpuCur));
最后返回了当前cpu的就绪任务,因为前面通过_CandTableResel函数查找到最高优先级任务加入到了对应cpu的就绪中。
如果找到的线程不是当前线程此时就会进行任务切换,切换函数如下
FUNC_DEF(archTaskCtxSwitch)
LDR X18, [X0] ;/* 获取当前 TCB 的 REG_CTX 地址*/
SAVE_SMALL_REG_CTX ;/* 保存小寄存器上下文 */
MOV X19 , X0 ;/* X19 暂存 X0 */
#if LW_CFG_SMP_EN > 0
BL _SchedSafeStack ;/* _SchedSafeStack(); */
MOV SP , X0 ;/* 设置 SP */
MOV X0 , X19 ;/* 恢复 X0 */
#endif
BL _SchedSwp ;/* _SchedSwp(); */
LDR X18, [X19] ;/* 获取当前 TCB 的 REG_CTX 地址*/
LDR X9 , [X18, #CTX_TYPE_OFFSET] ;/* 获得上下文类型 */
CMP X9 , #0
B.NE _RestoreSmallCtx
RESTORE_BIG_REG_CTX ;/* 恢复大寄存器上下文 */
LINE_LABEL(_RestoreSmallCtx)
RESTORE_SMALL_REG_CTX ;/* 恢复小寄存器上下文 */
FUNC_END()
首先保存上下文,然后获得调度栈,跳转到_SchedSwp执行,_SchedSwp函数最主要是如下一段代码
REGISTER PLW_CLASS_TCB ptcbCur = pcpuCur->CPU_ptcbTCBCur;
REGISTER PLW_CLASS_TCB ptcbHigh = pcpuCur->CPU_ptcbTCBHigh;
REGISTER LW_OBJECT_HANDLE ulCurId = ptcbCur->TCB_ulId;
REGISTER LW_OBJECT_HANDLE ulHighId = ptcbHigh->TCB_ulId;
BOOL bIsIntSwitch = pcpuCur->CPU_bIsIntSwitch;
............
省略
...........
pcpuCur->CPU_ptcbTCBCur = ptcbHigh; /* 切换任务 */
将pcpuCur->CPU_ptcbTCBHigh保存的任务保存到pcpuCur->CPU_ptcbTCBCur。在前面的函数将查找到高优先级的任务保存到了 pcpuCur->CPU_ptcbTCBHigh中。
执行完_SchedSwp 函数后,就是进行任务的切换。