摘要:CPUP(Central Processing Unit Percentage,CPU占用率)分为零碎CPU占用率和工作CPU占用率。用户通过零碎级的CPU占用率,判断以后零碎负载是否超出设计规格。通过零碎中各个工作的CPU占用状况,判断各个工作的CPU占用率是否合乎设计的预期。
本文分享自华为云社区《鸿蒙轻内核M核源码剖析系列十五 CPU使用率CPUP (1)》,作者:zhushy。
CPUP(Central Processing Unit Percentage,CPU占用率)分为零碎CPU占用率和工作CPU占用率。用户通过零碎级的CPU占用率,判断以后零碎负载是否超出设计规格。通过零碎中各个工作的CPU占用状况,判断各个工作的CPU占用率是否合乎设计的预期。
零碎CPU占用率是指周期时间内零碎的CPU占用率,用于示意零碎一段时间内的闲忙水平,也示意CPU的负载状况。零碎CPU占用率的无效示意范畴为0~100,其精度(可通过配置调整)为百分比。100示意零碎满负荷运行。
工作CPU占用率指单个工作的CPU占用率,用于示意单个工作在一段时间内的闲忙水平。工作CPU占用率的无效示意范畴为0~100,其精度(可通过配置调整)为百分比。100示意在一段时间内零碎始终在运行该工作。
本文通过剖析鸿蒙轻内核CPUP扩大模块的源码。本文中所波及的源码,以OpenHarmony LiteOS-M内核为例,均能够在开源站点https://gitee.com/openharmony... 获取。
CPUP模块用工作级记录的形式,在工作切换时,记录工作启动工夫,工作切出或者退出工夫,每次当工作退出时,零碎会累加整个工作的占用工夫。接下来,咱们看下CPUP模块反对的常见操作的源代码。
1、CPUP构造体定义和罕用宏定义
1.1 CPUP构造体定义
在文件components\cpup\los_cpup.h定义的CPUP管制块构造体为OsCpupCB,构造体源代码如下,allTime记录该工作自系统启动以来运行的cycle数,startTime记录工作开始运行的工夫,historyTime[]历史运行工夫数组的10个元素记录最近10秒中每一秒中每个工作自系统启动以来运行的cycle数,其余构造体成员的解释见正文局部。
typedef struct { UINT32 cpupID; /**< 工作编号 */ UINT16 status; /**< 工作状态 */ UINT64 allTime; /**< 总共运行的工夫 */ UINT64 startTime; /**< 工作开始工夫 */ UINT64 historyTime[OS_CPUP_HISTORY_RECORD_NUM]; /**< 历史运行工夫数组,其中OS_CPUP_HISTORY_RECORD_NUM为10 */} OsCpupCB;
另外,还定义了一个构造体CPUP_INFO_S,如下:
typedef struct tagCpupInfo { UINT16 usStatus; /**< 保留以后运行工作状态 */ UINT32 uwUsage; /**< 应用状况,值范畴为 [0,1000]. */} CPUP_INFO_S;
1.2 CPUP枚举定义
CPUP头文件components\cpup\los_cpup.h中还提供了相干的枚举,CPUP占用率类型CPUP_TYPE_E,及CPUP统计工夫距离模式CPUP_MODE_E。
typedef enum { SYS_CPU_USAGE = 0, /* 零碎CPUP */ TASK_CPU_USAGE, /* 工作CPUP */} CPUP_TYPE_E;typedef enum { CPUP_IN_10S = 0, /* CPUP统计周期10s */ CPUP_IN_1S, /* CPUP统计周期1s */ CPUP_LESS_THAN_1S, /* CPUP统计周期<1s */} CPUP_MODE_E;
2、CPUP初始化
CPUP默认敞开,用户能够通过宏LOSCFG_BASE_CORE_CPUP进行开启。开启CPUP的状况下,在系统启动时,在kernel\src\los_init.c中调用OsCpupInit()进行CPUP模块初始化。上面,咱们剖析下CPUP初始化的代码。
⑴处计算CPUP构造体池须要的内存大小,而后为CPUP申请内存,如果申请失败,则返回谬误。⑵处初始化胜利后,设置初始化标记g_cpupInitFlg。
LITE_OS_SEC_TEXT_INIT UINT32 OsCpupInit(){ UINT32 size; size = g_taskMaxNum * sizeof(OsCpupCB);⑴ g_cpup = (OsCpupCB *)LOS_MemAlloc(m_aucSysMem0, size); if (g_cpup == NULL) { return LOS_ERRNO_CPUP_NO_MEMORY; } (VOID)memset_s(g_cpup, size, 0, size);⑵ g_cpupInitFlg = 1; return LOS_OK;}
3、CPUP罕用操作
3.1 CPUP外部接口
咱们先剖析下外部接口,这些接口会被LOS_结尾的内部接口调用。
3.1.1 OsTskCycleStart记录工作开始工夫
CPUP模块对外接口执行前期会调用该外部接口,设置下一个工作的开始运行工夫。
⑴处先判断CPUP是否曾经初始化,如果没有初始化过,退出该函数的执行。⑵处获取新工作的工作编号。⑶处设置该工作对应的CPUP构造体的工作编号和开始工夫。
LITE_OS_SEC_TEXT_MINOR VOID OsTskCycleStart(VOID){ UINT32 taskID;⑴ if (g_cpupInitFlg == 0) { return; }⑵ taskID = g_losTask.newTask->taskID;⑶ g_cpup[taskID].cpupID = taskID; g_cpup[taskID].startTime = LOS_SysCycleGet(); return;}
3.1.2 OsTskCycleEnd记录工作完结工夫
CPUP模块对外接口执行后期会调用该外部接口,获取当前任务的完结工夫,并统计当前任务的运行总工夫。
⑴处先判断CPUP是否曾经初始化,如果没有初始化过,退出该函数的执行。⑵处获取当前任务的工作编号。⑶处如果该工作的开始工夫为0,退出函数执行。⑷处获取零碎的以后cycle数。⑸如果获取的小于工作CPUP开始工夫,则把获取的cycle数加上每个tick的cycle数。⑹处计算当前任务的运行的总工夫,而后把开始工夫置0。
LITE_OS_SEC_TEXT_MINOR VOID OsTskCycleEnd(VOID){ UINT32 taskID; UINT64 cpuCycle;⑴ if (g_cpupInitFlg == 0) { return; }⑵ taskID = g_losTask.runTask->taskID;⑶ if (g_cpup[taskID].startTime == 0) { return; }⑷ cpuCycle = LOS_SysCycleGet();⑸ if (cpuCycle < g_cpup[taskID].startTime) { cpuCycle += g_cyclesPerTick; }⑹ g_cpup[taskID].allTime += (cpuCycle - g_cpup[taskID].startTime); g_cpup[taskID].startTime = 0; return;}
3.1.3 OsTskCycleEndStart工作切换时更新工作历史运行工夫
该函数在任务调度切换时会被执行,计算以后运行工作的运行总工夫,记录新工作的开始工夫,并更新所有工作的历史运行工夫。函数的示意图如下:
⑴处先判断CPUP是否曾经初始化,如果没有初始化过,退出该函数的执行。⑵处获取当前任务的工作编号,而后获取零碎的以后cycle数。⑶处如果当前任务的开始工夫不为0,则计算当前任务的运行的总工夫,而后把开始工夫置0。
⑷处获取新工作的工作编号,⑸处设置该工作对应的CPUP构造体的工作编号和开始工夫。⑹处如果记录距离大于零碎时钟(即每秒的cycle数),更新上次记录时间。这意味着每个工作的historyTime[]数组中的每个元素示意1s多的周期内该工作的运行cycle数量,并不是十分准确的。而后执行⑺,记录每一个工作对应的CPUP的历史运行工夫。⑻处更新历史运行工夫数组的以后索引值。
LITE_OS_SEC_TEXT_MINOR VOID OsTskCycleEndStart(VOID){ UINT32 taskID; UINT64 cpuCycle; UINT16 loopNum;⑴ if (g_cpupInitFlg == 0) { return; }⑵ taskID = g_losTask.runTask->taskID; cpuCycle = LOS_SysCycleGet();⑶ if (g_cpup[taskID].startTime != 0) { if (cpuCycle < g_cpup[taskID].startTime) { cpuCycle += g_cyclesPerTick; } g_cpup[taskID].allTime += (cpuCycle - g_cpup[taskID].startTime); g_cpup[taskID].startTime = 0; }⑷ taskID = g_losTask.newTask->taskID;⑸ g_cpup[taskID].cpupID = taskID; g_cpup[taskID].startTime = cpuCycle;⑹ if ((cpuCycle - g_lastRecordTime) > OS_CPUP_RECORD_PERIOD) { g_lastRecordTime = cpuCycle; for (loopNum = 0; loopNum < g_taskMaxNum; loopNum++) {⑺ g_cpup[loopNum].historyTime[g_hisPos] = g_cpup[loopNum].allTime; }⑻ if (g_hisPos == (OS_CPUP_HISTORY_RECORD_NUM - 1)) { g_hisPos = 0; } else { g_hisPos++; } } return;}
3.1.4 OsGetPrePos获取历史运行工夫数组上一索引地位
代码比较简单,如果传入参数curPos为0,则返回数组的最初一个索引地位OS_CPUP_HISTORY_RECORD_NUM - 1。否则返回减1返回。
LITE_OS_SEC_TEXT_MINOR static inline UINT16 OsGetPrePos(UINT16 curPos){ return (curPos == 0) ? (OS_CPUP_HISTORY_RECORD_NUM - 1) : (curPos - 1);}
3.1.5 OsGetPositions获取历史运行工夫数组的以后及上一索引地位
依据CPUP统计工夫距离模式,获取历史运行工夫数组的以后及上一索引地位。
⑴处获取历史运行工夫数组的以后索引地位,⑵如果工夫距离模式为1秒,以后索引curPos地位为g_hisPos的上一索引地位,上一索引地位prePos须要持续上前一位。⑶如果工夫距离模式小于1秒,以后索引curPos地位为g_hisPos的上一索引地位,上一索引地位prePos为0。如果工夫距离模式是10秒,以后索引curPos地位就等于g_hisPos,上一索引地位prePos为0。⑷处设置传出参数。
LITE_OS_SEC_TEXT_MINOR static VOID OsGetPositions(UINT16 mode, UINT16* curPosAddr, UINT16* prePosAddr){ UINT16 curPos; UINT16 prePos = 0;⑴ curPos = g_hisPos;⑵ if (mode == CPUP_IN_1S) { curPos = OsGetPrePos(curPos); prePos = OsGetPrePos(curPos);⑶ } else if (mode == CPUP_LESS_THAN_1S) { curPos = OsGetPrePos(curPos); }⑷ *curPosAddr = curPos; *prePosAddr = prePos;}
3.2 CPUP对外接口
咱们先剖析下内部接口,接口阐明如下:
3.2.1 LOS_SysCpuUsage
该函数会统计以后零碎CPU占用率,返回值基于千分率计算,取值范畴为[0,1000]。函数的示意图如下:
⑴处先判断CPUP是否曾经初始化,如果没有初始化过,返回错误码。⑵处调用函数OsTskCycleEnd()获取当前任务的完结工夫,并计算出运行总工夫。⑶处统计所有工作的运行总工夫,如果总工夫不为0,执行⑷计算出零碎的工作CPU占用率。⑸处调用函数OsTskCycleStart()设置新工作的CPUP统计的开始工夫。
LITE_OS_SEC_TEXT_MINOR UINT32 LOS_SysCpuUsage(VOID){ UINT64 cpuCycleAll = 0; UINT32 cpupRet = 0; UINT16 loopNum; UINT32 intSave;⑴ if (g_cpupInitFlg == 0) { return LOS_ERRNO_CPUP_NO_INIT; } intSave = LOS_IntLock();⑵ OsTskCycleEnd();⑶ for (loopNum = 0; loopNum < g_taskMaxNum; loopNum++) { cpuCycleAll += g_cpup[loopNum].allTime; }⑷ if (cpuCycleAll) { cpupRet = LOS_CPUP_PRECISION - (UINT32)((LOS_CPUP_PRECISION * g_cpup[g_idleTaskID].allTime) / cpuCycleAll); }⑸ OsTskCycleStart(); LOS_IntRestore(intSave); return cpupRet;}
3.2.2 LOS_HistorySysCpuUsage
该函数获取零碎历史CPU占用率,对于历史CPU占用率,须要传入工夫距离模式参数,反对10秒、1秒、小于1秒三种。
⑴处先判断CPUP是否曾经初始化,如果没有初始化过,返回错误码。⑵处调用函数OsTskCycleEnd()获取当前任务的完结工夫,并计算出运行总工夫。⑶处调用函数OsGetPositions()计算出历史运行工夫数组索引地位。⑷处计算出各个工作的周期内运行总工夫,如果工夫距离模式为1秒,取值两个历史运行工夫之差,即为1秒内工作的运行工夫数。对于工夫距离模式为10秒,historyTime[curPos]示意10秒前的自系统启动以来的工作运行的工夫数,计算出来的差值即为10秒内工作的运行工夫数。对于工夫距离模式为小于1秒,historyTime[curPos]示意上一秒前的自系统启动以来的工作运行的工夫数,计算出来的差值即为小于1秒内工作的运行工夫数。⑸处计算闲暇工作周期内运行总工夫。⑹处如果总工夫不为0,计算出零碎的工作历史CPU占用率。最初,调用函数OsTskCycleStart()设置新工作的CPUP统计的开始工夫。能够参考示意图进行了解:
LITE_OS_SEC_TEXT_MINOR UINT32 LOS_HistorySysCpuUsage(UINT16 mode){ UINT64 cpuCycleAll = 0; UINT64 idleCycleAll = 0; UINT32 cpupRet = 0; UINT16 loopNum; UINT16 curPos; UINT16 prePos = 0; UINT32 intSave;⑴ if (g_cpupInitFlg == 0) { return LOS_ERRNO_CPUP_NO_INIT; } // get end time of current task intSave = LOS_IntLock();⑵ OsTskCycleEnd();⑶ OsGetPositions(mode, &curPos, &prePos); for (loopNum = 0; loopNum < g_taskMaxNum; loopNum++) {⑷ if (mode == CPUP_IN_1S) { cpuCycleAll += g_cpup[loopNum].historyTime[curPos] - g_cpup[loopNum].historyTime[prePos]; } else { cpuCycleAll += g_cpup[loopNum].allTime - g_cpup[loopNum].historyTime[curPos]; } }⑸ if (mode == CPUP_IN_1S) { idleCycleAll += g_cpup[g_idleTaskID].historyTime[curPos] - g_cpup[g_idleTaskID].historyTime[prePos]; } else { idleCycleAll += g_cpup[g_idleTaskID].allTime - g_cpup[g_idleTaskID].historyTime[curPos]; }⑹ if (cpuCycleAll) { cpupRet = (LOS_CPUP_PRECISION - (UINT32)((LOS_CPUP_PRECISION * idleCycleAll) / cpuCycleAll)); } OsTskCycleStart(); LOS_IntRestore(intSave); return cpupRet;}
3.2.3 LOS_TaskCpuUsage
该函数会统计指定工作的CPU占用率,和函数LOS_SysCpuUsage()代码类似度高,能够参考上文对该函数的解说。
LITE_OS_SEC_TEXT_MINOR UINT32 LOS_TaskCpuUsage(UINT32 taskID){ UINT64 cpuCycleAll = 0; UINT16 loopNum; UINT32 intSave; UINT32 cpupRet = 0; if (g_cpupInitFlg == 0) { return LOS_ERRNO_CPUP_NO_INIT; } if (OS_TSK_GET_INDEX(taskID) >= g_taskMaxNum) { return LOS_ERRNO_CPUP_TSK_ID_INVALID; } if (g_cpup[taskID].cpupID != taskID) { return LOS_ERRNO_CPUP_THREAD_NO_CREATED; } if ((g_cpup[taskID].status & OS_TASK_STATUS_UNUSED) || (g_cpup[taskID].status == 0)) { return LOS_ERRNO_CPUP_THREAD_NO_CREATED; } intSave = LOS_IntLock(); OsTskCycleEnd(); for (loopNum = 0; loopNum < g_taskMaxNum; loopNum++) { if ((g_cpup[loopNum].status & OS_TASK_STATUS_UNUSED) || (g_cpup[loopNum].status == 0)) { continue; } cpuCycleAll += g_cpup[loopNum].allTime; } if (cpuCycleAll) { cpupRet = (UINT32)((LOS_CPUP_PRECISION * g_cpup[taskID].allTime) / cpuCycleAll); } OsTskCycleStart(); LOS_IntRestore(intSave); return cpupRet;}
3.2.4 LOS_HistoryTaskCpuUsage
该函数获取指定工作的历史CPU占用率,和函数LOS_HistorySysCpuUsage()代码类似度高,能够参考上文对该函数的解说。
LITE_OS_SEC_TEXT_MINOR UINT32 LOS_HistoryTaskCpuUsage(UINT32 taskID, UINT16 mode){ UINT64 cpuCycleAll = 0; UINT64 cpuCycleCurTsk = 0; UINT16 loopNum, curPos; UINT16 prePos = 0; UINT32 intSave; UINT32 cpupRet = 0; if (g_cpupInitFlg == 0) { return LOS_ERRNO_CPUP_NO_INIT; } if (OS_TSK_GET_INDEX(taskID) >= g_taskMaxNum) { return LOS_ERRNO_CPUP_TSK_ID_INVALID; } if (g_cpup[taskID].cpupID != taskID) { return LOS_ERRNO_CPUP_THREAD_NO_CREATED; } if ((g_cpup[taskID].status & OS_TASK_STATUS_UNUSED) || (g_cpup[taskID].status == 0)) { return LOS_ERRNO_CPUP_THREAD_NO_CREATED; } intSave = LOS_IntLock(); OsTskCycleEnd(); OsGetPositions(mode, &curPos, &prePos); for (loopNum = 0; loopNum < g_taskMaxNum; loopNum++) { if ((g_cpup[loopNum].status & OS_TASK_STATUS_UNUSED) || (g_cpup[loopNum].status == 0)) { continue; } if (mode == CPUP_IN_1S) { cpuCycleAll += g_cpup[loopNum].historyTime[curPos] - g_cpup[loopNum].historyTime[prePos]; } else { cpuCycleAll += g_cpup[loopNum].allTime - g_cpup[loopNum].historyTime[curPos]; } } if (mode == CPUP_IN_1S) { cpuCycleCurTsk += g_cpup[taskID].historyTime[curPos] - g_cpup[taskID].historyTime[prePos]; } else { cpuCycleCurTsk += g_cpup[taskID].allTime - g_cpup[taskID].historyTime[curPos]; } if (cpuCycleAll) { cpupRet = (UINT32)((LOS_CPUP_PRECISION * cpuCycleCurTsk) / cpuCycleAll); } OsTskCycleStart(); LOS_IntRestore(intSave); return cpupRet;}
3.2.5 LOS_AllTaskCpuUsage
该函数获取全副工作的CPU占用率,获取的CPU占用率信息保留在传出参数构造体CPUP_INFO_S cpupInfo指向的内存区域里,须要留神这个内存区域的大小须要等于sizeof(CPUP_INFO_S) g_taskMaxNum。还须要传入工夫距离模式参数,反对10秒、1秒、小于1秒三种。
⑴处先判断CPUP是否曾经初始化,如果没有初始化过,返回错误码。传出参数cpupInfo指针不能为空,否则返回错误码。⑵处调用函数OsTskCycleEnd()获取当前任务的完结工夫,并计算出运行总工夫。⑶处调用函数OsGetPositions()计算出历史运行工夫数组索引地位。⑷处计算出各个工作的周期内运行总工夫,如果工夫距离模式为1秒,取值两个历史运行工夫之差,否则取值XX。⑸处设置每一个工作的状态,而后计算出每一个工作的CPU占用率。最初,调用函数OsTskCycleStart()设置新工作的CPUP统计的开始工夫。
LITE_OS_SEC_TEXT_MINOR UINT32 LOS_AllTaskCpuUsage(CPUP_INFO_S *cpupInfo, UINT16 mode){ UINT16 loopNum; UINT16 curPos; UINT16 prePos = 0; UINT32 intSave; UINT64 cpuCycleAll = 0; UINT64 cpuCycleCurTsk = 0;⑴ if (g_cpupInitFlg == 0) { return LOS_ERRNO_CPUP_NO_INIT; } if (cpupInfo == NULL) { return LOS_ERRNO_CPUP_TASK_PTR_NULL; } intSave = LOS_IntLock();⑵ OsTskCycleEnd();⑶ OsGetPositions(mode, &curPos, &prePos); for (loopNum = 0; loopNum < g_taskMaxNum; loopNum++) { if ((g_cpup[loopNum].status & OS_TASK_STATUS_UNUSED) || (g_cpup[loopNum].status == 0)) { continue; } if (mode == CPUP_IN_1S) { cpuCycleAll += g_cpup[loopNum].historyTime[curPos] - g_cpup[loopNum].historyTime[prePos]; } else { cpuCycleAll += g_cpup[loopNum].allTime - g_cpup[loopNum].historyTime[curPos]; } }⑷ for (loopNum = 0; loopNum < g_taskMaxNum; loopNum++) { if ((g_cpup[loopNum].status & OS_TASK_STATUS_UNUSED) || (g_cpup[loopNum].status == 0)) { continue; } if (mode == CPUP_IN_1S) { cpuCycleCurTsk += g_cpup[loopNum].historyTime[curPos] - g_cpup[loopNum].historyTime[prePos]; } else { cpuCycleCurTsk += g_cpup[loopNum].allTime - g_cpup[loopNum].historyTime[curPos]; }⑸ cpupInfo[loopNum].usStatus = g_cpup[loopNum].status; if (cpuCycleAll) { cpupInfo[loopNum].uwUsage = (UINT32)((LOS_CPUP_PRECISION * cpuCycleCurTsk) / cpuCycleAll); } cpuCycleCurTsk = 0; } OsTskCycleStart(); LOS_IntRestore(intSave); return LOS_OK;}
3.2.6 LOS_CpupUsageMonitor
该函数获取历史CPU占用率并打印输出,传入参数有三个:CPU占用率类型,CPUP工夫周期模式,指定的工作编号。对于工作CPU占用率,才须要指定无效的工作编号。
⑴处解决CPU占用率类型为零碎CPU占用率的状况,⑵处打印应用的CPUP工夫周期模式。⑶处通过调用函数LOS_HistorySysCpuUsage()获取零碎历史CPU占用率,而后执行⑷打印输出CPU占用率后果,输入后果范畴为[0,100]。
⑸处解决CPU占用率类型为指定工作CPU占用率的状况,首先判断下工作编号的有效性,校验工作是否创立等。⑹处打印应用的CPUP工夫周期模式。⑺处通过调用函数LOS_HistoryTaskCpuUsage()获取指定工作的历史CPU占用率,而后执行⑻打印输出CPU占用率后果,输入后果范畴为[0,100]。
LITE_OS_SEC_TEXT_MINOR UINT32 LOS_CpupUsageMonitor(CPUP_TYPE_E type, CPUP_MODE_E mode, UINT32 taskID){ UINT32 ret; LosTaskCB *taskCB = NULL; switch (type) {⑴ case SYS_CPU_USAGE:⑵ if (mode == CPUP_IN_10S) { PRINTK("\nSysCpuUsage in 10s: "); } else if (mode == CPUP_IN_1S) { PRINTK("\nSysCpuUsage in 1s: "); } else { PRINTK("\nSysCpuUsage in <1s: "); }⑶ ret = LOS_HistorySysCpuUsage(mode);⑷ PRINTK("%d.%d", ret / LOS_CPUP_PRECISION_MULT, ret % LOS_CPUP_PRECISION_MULT); break;⑸ case TASK_CPU_USAGE: if (taskID > LOSCFG_BASE_CORE_TSK_LIMIT) { PRINT_ERR("\nThe taskid is invalid.\n"); return OS_ERROR; } taskCB = OS_TCB_FROM_TID(taskID); if ((taskCB->taskStatus & OS_TASK_STATUS_UNUSED)) { PRINT_ERR("\nThe taskid is invalid.\n"); return OS_ERROR; }⑹ if (mode == CPUP_IN_10S) { PRINTK("\nCPUusage of taskID %d in 10s: ", taskID); } else if (mode == CPUP_IN_1S) { PRINTK("\nCPUusage of taskID %d in 1s: ", taskID); } else { PRINTK("\nCPUusage of taskID %d in <1s: ", taskID); }⑺ ret = LOS_HistoryTaskCpuUsage(taskID, mode);⑻ PRINTK("%u.%u", ret / LOS_CPUP_PRECISION_MULT, ret % LOS_CPUP_PRECISION_MULT); break; default: PRINT_ERR("\nThe type is invalid.\n"); return OS_ERROR; } return LOS_OK;}
小结
本文率领大家一起分析了鸿蒙轻内核的CPUP扩大模块的源代码。感激浏览,如有任何问题、倡议,都能够博客下留言给我,谢谢。
点击关注,第一工夫理解华为云陈腐技术~