摘要:鸿蒙轻内核的任务排序链表,用于任务延迟到期/超时唤醒等业务场景,是一个非常重要、非常基础的数据结构。

本文会继续给读者介绍鸿蒙轻内核源码中重要的数据结构:任务排序链表TaskSortLinkAttr。鸿蒙轻内核的任务排序链表,用于任务延迟到期/超时唤醒等业务场景,是一个非常重要、非常基础的数据结构。本文中所涉及的源码,以OpenHarmony LiteOS-M内核为例,均可以在开源站点https://gitee.com/openharmony/kernel_liteos_m 获取。

1 任务排序链表

我们先看下任务排序链接的数据结构。任务排序链表是一个环状的双向链表数组,任务排序链表属性结构体TaskSortLinkAttr作为双向链表的头结点,指向双向链表数组的第一个元素,还维护游标信息,记录当前的位置信息。我们先看下排序链表属性的结构体的定义。

1.1 任务排序链表属性结构体定义

kernel\include\los_task.h头文件中定义了排序链表属性的结构体TaskSortLinkAttr。该结构体定义了排序链表的头节点LOS_DL_LIST *sortLink,游标UINT16 cursor,还有一个保留字段,暂时没有使用。

源码如下:

 
 typedef struct {
LOS_DL_LIST *sortLink;
UINT16 cursor;
UINT16 reserved;
} TaskSortLinkAttr;

在文件kernel\src\los_task.c中定义了排序链表属性结构体TaskSortLinkAttr类型的全局变量g_taskSortLink,该全局变量的成员变量sortLink作为排序链表的头结点,指向一个长度为32的环状的双向链表数组,成员变量cursor作为游标记录环状数组的当前游标位置。源代码如下。

​​​​​​​    LITE_OS_SEC_BSS  TaskSortLinkAttr                    g_taskSortLink;

我们使用示意图来讲述一下。任务排序链表是环状双向链表数组,长度为32,每一个元素是一个双向链表,挂载任务LosTaskCB的链表节点timerList。任务LosTaskCB的成员变量idxRollNum记录数组的索引和滚动数。全局变量g_taskSortLink的成员变量cursor记录当前游标位置,每过一个Tick,游标指向下一个位置,转一轮需要32 ticks。当运行到的数组位置,双向链表不为空,则把第一个节点维护的滚动数减1。这样的数据结构类似钟表表盘,也称为时间轮

我们举个例子来说明,基于时间轮实现的任务排序链表是如何管理任务延迟超时的。假如当前游标cursor为1,当一个任务需要延时72 ticks,72=2*32+8,表示排序索引sortIndex为8,滚动数rollNum为2。会把任务插入数组索引为sortIndex+cursor=9的双向链表位置,索要9处的双向链表维护节点的滚动为2。随着Tick时间的进行,从当前游标位置运行到数组索引位置9,历时8 ticks。运行到9时,如果滚动数大于0,则把滚动数减1。等再运行2轮,共需要72 ticks,任务就会延迟到期,可以从排序链表移除。每个数组元素对应的双向链表的第一个链表节点的滚动数表示需要转多少轮,节点任务才到期。第二个链表节点的滚动数需要加上第一个节点的滚动数,表示第二个节点需要转的轮数。依次类推。

示意图如下:

1.2 任务排序链表宏定义

OS_TSK_SORTLINK_LEN头文件中定义了一些和任务排序链表相关的宏定义。延迟任务双向链表数组的长度定义为32,高阶bit位位数为5,低阶bit位位数为27。对于任务的超时时间,取其高27位作为滚动数,低5位作为数组索引。

源码如下:

​​​​​​​      
 /**
* 延迟任务双向链表数组的数量(桶的数量):32
*/
#define OS_TSK_SORTLINK_LEN 32 /**
* 高阶bit位数目:5
*/
#define OS_TSK_HIGH_BITS 5U /**
* 低阶bit位数目:27
*/
#define OS_TSK_LOW_BITS (32U - OS_TSK_HIGH_BITS) /**
* 滚动数最大值:0xFFFF FFDF,1111 0111 1111 1111 1111 1111 1101 1111
*/
#define OS_TSK_MAX_ROLLNUM (0xFFFFFFFFU - OS_TSK_SORTLINK_LEN) /**
* 任务延迟时间数的位宽:5
*/
#define OS_TSK_SORTLINK_LOGLEN 5 /**
* 延迟任务的桶编号的掩码:31、0001 1111
*/
#define OS_TSK_SORTLINK_MASK (OS_TSK_SORTLINK_LEN - 1U) /**
* 滚动数的高阶掩码:1111 1000 0000 0000 0000 0000 0000 0000
*/
#define OS_TSK_HIGH_BITS_MASK (OS_TSK_SORTLINK_MASK << OS_TSK_LOW_BITS) /**
* 滚动数的低阶掩码:0000 0111 1111 1111 1111 1111 1111 1111
*/
#define OS_TSK_LOW_BITS_MASK (~OS_TSK_HIGH_BITS_MASK)

2 任务排序链表操作

我们分析下任务排序链表的操作,包含初始化,插入,删除,滚动数更新,获取下一个到期时间等。

2.1 初始化排序链表

在系系统内核初始化启动阶段,在函数UINT32 OsTaskInit(VOID)中初始化任务排序链表。该函数的调用关系如下,main.c:main() --> kernel\src\los_init.c:LOS_KernelInit() --> kernel\src\los_task.c:OsTaskInit()

初始化排序链表函数的源码如下:

​​​​​​​    
LITE_OS_SEC_TEXT_INIT UINT32 OsTaskInit(VOID)
{
UINT32 size;
UINT32 index;
LOS_DL_LIST *listObject = NULL;
......
⑴ size = sizeof(LOS_DL_LIST) * OS_TSK_SORTLINK_LEN;
listObject = (LOS_DL_LIST *)LOS_MemAlloc(m_aucSysMem0, size);
⑵ if (listObject == NULL) {
(VOID)LOS_MemFree(m_aucSysMem0, g_taskCBArray);
return LOS_ERRNO_TSK_NO_MEMORY;
} ⑶ (VOID)memset_s((VOID *)listObject, size, 0, size);
⑷ g_taskSortLink.sortLink = listObject;
g_taskSortLink.cursor = 0;
for (index = 0; index < OS_TSK_SORTLINK_LEN; index++, listObject++) {
⑸ LOS_ListInit(listObject);
} return LOS_OK;
}

⑴处代码计算需要申请的双向链表的内存大小,OS_TSK_SORTLINK_LEN为32,即需要为32个双向链表节点申请内存空间。然后申请内存,⑵处申请内存失败时返回相应错误码。⑶处初始化申请的内存区域为0等。⑷处把申请的双向链表节点赋值给g_taskSortLink的链表节点.sortLink,作为排序链表的头节点,游标.cursor初始化为0。然后⑸处的循环,调用LOS_ListInit()函数把双向链表数组每个元素都初始化为双向循环链表。

2.2 插入排序链表

插入排序链表的函数为OsTaskAdd2TimerList()。在任务等待互斥锁/信号量等资源时,都需要调用该函数将任务加入到对应的排序链表中。该函数包含两个入参,第一个参数LosTaskCB *taskCB用于指定要延迟的任务,第二个参数UINT32 timeout指定超时等待时间。

源码如下:

​​​​​​​  
  LITE_OS_SEC_TEXT VOID OsTaskAdd2TimerList(LosTaskCB *taskCB, UINT32 timeout)
{
LosTaskCB *taskDelay = NULL;
LOS_DL_LIST *listObject = NULL;
UINT32 sortIndex;
UINT32 rollNum; ⑴ sortIndex = timeout & OS_TSK_SORTLINK_MASK;
rollNum = (timeout >> OS_TSK_SORTLINK_LOGLEN);
⑵ (sortIndex > 0) ? 0 : (rollNum--);
⑶ EVALUATE_L(taskCB->idxRollNum, rollNum);
⑷ sortIndex = (sortIndex + g_taskSortLink.cursor);
sortIndex = sortIndex & OS_TSK_SORTLINK_MASK;
⑸ EVALUATE_H(taskCB->idxRollNum, sortIndex);
⑹ listObject = g_taskSortLink.sortLink + sortIndex;
⑺ if (listObject->pstNext == listObject) {
LOS_ListTailInsert(listObject, &taskCB->timerList);
} else {
⑻ taskDelay = LOS_DL_LIST_ENTRY((listObject)->pstNext, LosTaskCB, timerList);
do {
⑼ if (UWROLLNUM(taskDelay->idxRollNum) <= UWROLLNUM(taskCB->idxRollNum)) {
UWROLLNUMSUB(taskCB->idxRollNum, taskDelay->idxRollNum);
} else {
⑽ UWROLLNUMSUB(taskDelay->idxRollNum, taskCB->idxRollNum);
break;
} ⑾ taskDelay = LOS_DL_LIST_ENTRY(taskDelay->timerList.pstNext, LosTaskCB, timerList);
} while (&taskDelay->timerList != (listObject)); ⑿ LOS_ListTailInsert(&taskDelay->timerList, &taskCB->timerList);
}
}

⑴处代码计算等待时间timeout的低5位作为数组索引,高27位作为滚动数rollNum。这2行代码数学上的意义,就是把等待时间处于32得到的商作为滚动数,余数作为数组索引。⑵处代码,如果余数为0,可以整除时,滚动数减1。减1设计的原因是,在函数VOID OsTaskScan(VOID)中,每一个tick到来时,如果滚动数大于0,滚动数减1,并继续滚动一圈。后文会分析该函数VOID OsTaskScan(VOID)

⑶处代码把滚动数赋值给任务taskCB->idxRollNum的低27位。⑷处把数组索引加上游标,然后执行⑸赋值给任务taskCB->idxRollNum的高5位。⑹根据数组索引获取双向链表头结点,⑺如果此处双向链表为空,直接插入链表里。如果链表不为空,执行⑻获取第一个链表节点对应的任务taskDelay,然后遍历循环双向链表,把任务插入到合适的位置。⑼处如果待插入任务taskCB的滚动数大于等于当前链表节点对应任务的滚动数,则从待插入任务taskCB的滚动数中减去当前链表节点对应任务的滚动数,然后执行⑾获取下一个节点继续遍历。⑽处如果待插入任务taskCB的滚动数小于当前链表节点对应任务的滚动数,则从当前链表节点对应任务的滚动数中减去待插入任务taskCB的滚动数,然后跳出循环。执行⑿,完成任务插入。插入过程,可以结合上文的示意图进行理解。

2.3 从排序链表中删除

从排序链表中删除的函数为VOID OsTimerListDelete(LosTaskCB *taskCB)。在任务恢复/删除等场景中,需要调用该函数将任务从任务排序链表中删除。该函数包含一个参数LosTaskCB *taskCB,用于指定要从排序链表中删除的任务。

源码如下:

​​​​​​​  
 LITE_OS_SEC_TEXT VOID OsTimerListDelete(LosTaskCB *taskCB)
{
LOS_DL_LIST *listObject = NULL;
LosTaskCB *nextTask = NULL;
UINT32 sortIndex; ⑴ sortIndex = UWSORTINDEX(taskCB->idxRollNum);
⑵ listObject = g_taskSortLink.sortLink + sortIndex; ⑶ if (listObject != taskCB->timerList.pstNext) {
⑷ nextTask = LOS_DL_LIST_ENTRY(taskCB->timerList.pstNext, LosTaskCB, timerList);
UWROLLNUMADD(nextTask->idxRollNum, taskCB->idxRollNum);
} ⑸ LOS_ListDelete(&taskCB->timerList);
}

⑴处代码获取待从排序链表中删除的任务对应的数字索引。⑵处代码获取排序链表的头节点listObject。⑶处代码判断待删除节点是否是最后一个节点,如果不是最后一个节点,执行执行⑷处代码获取待删除节点的下一个节点对应的任务nextTask,在下一个节点的滚动数中增加待删除节点的滚动数,然后执行⑸处代码执行删除操作。如果是最后一个节点,直接执行⑸处代码删除该节点即可。

2.4 获取下一个超时到期时间

获取下一个超时到期时间的函数为OsTaskNextSwitchTimeGet(),我们分析下其代码。

源码如下:

​​​​​​​  
 UINT32 OsTaskNextSwitchTimeGet(VOID)
{
LosTaskCB *taskCB = NULL;
UINT32 taskSortLinkTick = LOS_WAIT_FOREVER;
LOS_DL_LIST *listObject = NULL;
UINT32 tempTicks;
UINT32 index; ⑴ for (index = 0; index < OS_TSK_SORTLINK_LEN; index++) {
⑵ listObject = g_taskSortLink.sortLink + ((g_taskSortLink.cursor + index) % OS_TSK_SORTLINK_LEN);
⑶ if (!LOS_ListEmpty(listObject)) {
⑷ taskCB = LOS_DL_LIST_ENTRY((listObject)->pstNext, LosTaskCB, timerList);
⑸ tempTicks = (index == 0) ? OS_TSK_SORTLINK_LEN : index;
⑹ tempTicks += (UINT32)(UWROLLNUM((UINT32)taskCB->idxRollNum) * OS_TSK_SORTLINK_LEN);
⑺ if (taskSortLinkTick > tempTicks) {
taskSortLinkTick = tempTicks;
}
}
}
return taskSortLinkTick;
}

⑴处代码循环遍历双向链表数组,⑵处代码从当前游标位置开始获取排序链表的头节点listObject。⑶处代码判断排序链表是否为空,如果排序链表为空,则继续遍历下一个数组。如果链表不为空,⑷处代码获取排序链表的第一个链表节点对应的任务。⑸处如果遍历的数字索引为0,tick数目使用32,否则使用具体的数字索引。⑹处获取任务的滚动数,计算出需要的等待时间,加上⑸处计算出的不足滚动一圈的时间。⑺处计算出需要等待的最小时间,即下一个最快到期的时间。

3 排序链表和Tick时间的关系

任务加入到排序链表后,时间一个tick一个tick的逝去,排序链表中的滚动数该如何更新呢?

时间每走过一个tick,系统就会调用Tick中断的处理函数OsTickHandler(),该函数在kernel\src\los_tick.c文件中实现。下面是该函数的代码片段,⑴处代码分别任务的超时到期情况。

​​​​​​​  
  LITE_OS_SEC_TEXT VOID OsTickHandler(VOID)
{
#if (LOSCFG_BASE_CORE_TICK_HW_TIME == 1)
platform_tick_handler();
#endif g_ullTickCount++; #if (LOSCFG_BASE_CORE_TIMESLICE == 1)
OsTimesliceCheck();
#endif ⑴ OsTaskScan(); // task timeout scan #if (LOSCFG_BASE_CORE_SWTMR == 1)
(VOID)OsSwtmrScan();
#endif
}

详细分析下函数OsTaskScan(),来了解排序链表和tick时间的关系。函数在kernel\base\los_task.c文件中实现,代码片段如下:

​​​​​​​ 
   LITE_OS_SEC_TEXT VOID OsTaskScan(VOID)
{
LosTaskCB *taskCB = NULL;
BOOL needSchedule = FALSE;
LOS_DL_LIST *listObject = NULL;
UINT16 tempStatus;
UINTPTR intSave;
intSave = LOS_IntLock(); ⑴ g_taskSortLink.cursor = (g_taskSortLink.cursor + 1) % OS_TSK_SORTLINK_LEN;
listObject = g_taskSortLink.sortLink + g_taskSortLink.cursor;
⑵ if (listObject->pstNext == listObject) {
LOS_IntRestore(intSave);
return;
} ⑶ for (taskCB = LOS_DL_LIST_ENTRY((listObject)->pstNext, LosTaskCB, timerList);
&taskCB->timerList != (listObject);) {
tempStatus = taskCB->taskStatus;
⑷ if (UWROLLNUM(taskCB->idxRollNum) > 0) {
UWROLLNUMDEC(taskCB->idxRollNum);
break;
} ⑸ LOS_ListDelete(&taskCB->timerList);
⑹ if (tempStatus & OS_TASK_STATUS_PEND) {
taskCB->taskStatus &= ~(OS_TASK_STATUS_PEND);
LOS_ListDelete(&taskCB->pendList);
taskCB->taskSem = NULL;
taskCB->taskMux = NULL;
}
⑺ else if (tempStatus & OS_TASK_STATUS_EVENT) {
taskCB->taskStatus &= ~(OS_TASK_STATUS_EVENT);
}
⑻ else if (tempStatus & OS_TASK_STATUS_PEND_QUEUE) {
LOS_ListDelete(&taskCB->pendList);
taskCB->taskStatus &= ~(OS_TASK_STATUS_PEND_QUEUE);
} else {
taskCB->taskStatus &= ~(OS_TASK_STATUS_DELAY);
} ⑼ if (!(tempStatus & OS_TASK_STATUS_SUSPEND)) {
taskCB->taskStatus |= OS_TASK_STATUS_READY;
OsHookCall(LOS_HOOK_TYPE_MOVEDTASKTOREADYSTATE, taskCB);
OsPriqueueEnqueue(&taskCB->pendList, taskCB->priority);
needSchedule = TRUE;
} if (listObject->pstNext == listObject) {
break;
} taskCB = LOS_DL_LIST_ENTRY(listObject->pstNext, LosTaskCB, timerList);
} LOS_IntRestore(intSave); ⑽ if (needSchedule) {
LOS_Schedule();
}
}

⑴处代码更新全局变量g_taskSortLink的游标,指向双向链表数组下一个位置,然后获取该位置的双向链表头结点listObject。⑵如果链表为空,则返回。如果双向链表不为空,则执行⑶循环遍历每一个链表节点。⑷处如果链表节点的滚动数大于0,则滚动数减1,说明任务还需要继续等待一轮。如果链表节点的滚动数等于0,说明任务超时到期,执行⑸从排序链表中删除。接下来需要根据任务状态分别处理,⑹处如果代码是阻塞状态,取消阻塞状态,并从阻塞链表中删除。⑺处如果任务阻塞在事件中,取消阻塞状态。⑻如果任务阻塞在队列,从阻塞链表中删除,取消阻塞状态,如果不是上述状态,取消延迟状态OS_TASK_STATUS_DELAY。⑼处如果代码是挂起状态,设置任务为就绪状态,加入任务就绪队列,设置需要重新调度标记。⑽如果设置需要重新调度,调用调度函数触发任务调度。

小结

掌握鸿蒙轻内核的排序链表TaskSortLinkAttr这一重要的数据结构,会给进一步学习、分析鸿蒙轻内核源代码打下了基础,让后续的学习更加容易。后续也会陆续推出更多的分享文章,敬请期待,也欢迎大家分享学习、使用鸿蒙轻内核的心得,有任何问题、建议,

都可以留言给我们: https://gitee.com/openharmony/kernel_liteos_m/issues 。为了更容易找到鸿蒙轻内核代码仓,建议访问 https://gitee.com/openharmony/kernel_liteos_m ,关注Watch、点赞Star、并Fork到自己账户下,谢谢。

点击关注,第一时间了解华为云新鲜技术~

鸿蒙轻内核M核源码分析:数据结构之任务排序链表的更多相关文章

  1. 鸿蒙轻内核M核源码分析:LibC实现之Musl LibC

    摘要:本文学习了LiteOS-M内核Musl LibC的实现,特别是文件系统和内存分配释放部分. 本文分享自华为云社区<鸿蒙轻内核M核源码分析系列十九 Musl LibC>,作者:zhus ...

  2. 深层剖析鸿蒙轻内核M核的动态内存如何支持多段非连续性内存

    摘要:鸿蒙轻内核M核新增支持了多段非连续性内存区域,把多个非连续性内存逻辑上合一,用户不感知底层的不同内存块. 本文分享自华为云社区<鸿蒙轻内核M核源码分析系列九 动态内存Dynamic Mem ...

  3. 鸿蒙轻内核M核的故障管家:Fault异常处理

    摘要:本文先简单介绍下Fault异常类型,向量表及其代码,异常处理C语言程序,然后详细分析下异常处理汇编函数实现代码. 本文分享自华为云社区<鸿蒙轻内核M核源码分析系列十八 Fault异常处理& ...

  4. Linux 内核调度器源码分析 - 初始化

    导语 上篇系列文 混部之殇-论云原生资源隔离技术之CPU隔离(一) 介绍了云原生混部场景中CPU资源隔离核心技术:内核调度器,本系列文章<Linux内核调度器源码分析>将从源码的角度剖析内 ...

  5. Solr4.8.0源码分析(6)之非排序查询

    Solr4.8.0源码分析(6)之非排序查询 上篇文章简单介绍了Solr的查询流程,本文开始将详细介绍下查询的细节.查询主要分为排序查询和非排序查询,由于两者走的是两个分支,所以本文先介绍下非排序的查 ...

  6. 【JUnit4.10源码分析】6.1 排序和过滤

    abstract class ParentRunner<T> extends Runner implements Filterable,Sortable 本节介绍排序和过滤. (尽管JUn ...

  7. ARMv8 Linux内核head.S源码分析

    ARMv8Linux内核head.S主要工作内容: 1. 从el2特权级退回到el1 2. 确认处理器类型 3. 计算内核镜像的起始物理地址及物理地址与虚拟地址之间的偏移 4. 验证设备树的地址是否有 ...

  8. Windows内核遍历驱动模块源码分析

    要获取windows 内核中所有驱动模块信息,调用 系统服务函数 NtQuerySystemInformation,参数SystemInformationClass 传入SystemModuleInf ...

  9. HashMap从源码分析数据结构

    1. HashMap在链表中存储的是键值对 2. 数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突.那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法 ...

  10. 鸿蒙轻内核源码分析:文件系统LittleFS

    摘要:本文先介绍下LFS文件系统结构体的结构体和全局变量,然后分析下LFS文件操作接口. 本文分享自华为云社区<# 鸿蒙轻内核M核源码分析系列二一 02 文件系统LittleFS>,作者: ...

随机推荐

  1. 《最新出炉》系列初窥篇-Python+Playwright自动化测试-21-处理鼠标拖拽-番外篇

    1.简介 前边宏哥拖拽有提到那个反爬虫机制,加了各种参数,以及加载js脚本文件还是有问题,偶尔宏哥好像发现了解决问题的办法,看到了黎明的曙光,宏哥就说试一下看看行不行,万一实现了.结果宏哥试了结果真的 ...

  2. KubeEdge-Ianvs v0.2 发布:终身学习支持非结构化场景

    本文分享自华为云社区<KubeEdge-Ianvs v0.2 发布:终身学习支持非结构化场景>,作者: 云容器大未来. 在边缘计算的浪潮中,AI是边缘云乃至分布式云中最重要的应用.随着边缘 ...

  3. 产品代码都给你看了,可别再说不会DDD(十):CQRS

    这是一个讲解DDD落地的文章系列,作者是<实现领域驱动设计>的译者滕云.本文章系列以一个真实的并已成功上线的软件项目--码如云(https://www.mryqr.com)为例,系统性地讲 ...

  4. STM32CUBEIDE 如何将变量定义到指定内存地址

    使用场景如下: 我需要将bootloader/APP的版本号和一些字段信息定义到指定FLASH地址. 在STM32CubeIDE中的方法: 截止当前STM32CubeIDE还没有提供图形化的针对FLA ...

  5. 如何正确执行 DORA 指标

    DevOps 研究与 DORA 评估指标可帮助我们深入了解软件开发和交付流程的性能和效率.这些指标包括部署频率.变更交付时间.变更失败率和平均恢复时间等方面.DORA 指标对于管理开发团队(从团队领导 ...

  6. Codeforces Round #736 (Div. 2). D - Integers Have Friends

    原题:D - Integers Have Friends 题意: 给定一个数组,求一个最长子数组满足\(a_i \,\, mod \,\, m \,\, = \,\, a_{i + 1} \,\, m ...

  7. 从管易云到MySQL通过接口配置打通数据

    从管易云到MySQL通过接口配置打通数据 数据源平台:管易云 管易云是金蝶旗下专注提供电商企业管理软件服务的子品牌,先后开发了C-ERP.EC-OMS.EC-WMS.E店管家.BBC.B2B.B2C商 ...

  8. 微盟&致远OA&聚水潭&YonSuite系统对接集成整体解决方案

    前言:大部分的企业都可能只用一套系统组织架构复杂,业务流程繁琐,内部同时有OA系统.BI系统.ERP系统......且各个系统都需要独立登陆,造成IT部门数据监管困难!如何在同一套中台系统上关联多管理 ...

  9. 如何利用Excel/WPS表格制作智能成绩查询系统?

    要利用Excel或WPS表格制作智能成绩查询系统,可以按照以下步骤进行: 1. 设计数据库结构:确定需要存储的学生信息和成绩数据,包括姓名.学号.科目.分数等字段. 2. 创建数据表:在Excel或W ...

  10. Datainside数据分析,基于大数据分析学生成绩综合评价

    Datainside是一种基于大数据分析的学生成绩综合评价方法,通过对海量学生成绩数据进行深度挖掘和分析,为学生的学习表现提供全面.客观的评价.以下是对Datainside数据分析学生成绩综合评价的详 ...