怎么理解Task
1. 官方文档是怎么描述线程基本概念
从系统的角度看,线程是竞争系统资源的最小运行单元。线程可以使用或等待CPU、使用内存空间等系统资源,并独立于其它线程运行。
鸿蒙内核每个进程内的线程独立运行、独立调度,当前进程内线程的调度不受其它进程内线程的影响。
鸿蒙内核中的线程采用抢占式调度机制,同时支持时间片轮转调度和FIFO调度方式。
鸿蒙内核的线程一共有32个优先级(0-31),最高优先级为0,最低优先级为31。
当前进程内高优先级的线程可抢占当前进程内低优先级线程,当前进程内低优先级线程必须在当前进程内高优先级线程阻塞或结束后才能得到调度。
线程状态说明:
初始化(Init):该线程正在被创建。
就绪(Ready):该线程在就绪列表中,等待CPU调度。
运行(Running):该线程正在运行。
阻塞(Blocked):该线程被阻塞挂起。Blocked状态包括:pend(因为锁、事件、信号量等阻塞)、suspend(主动pend)、delay(延时阻塞)、pendtime(因为锁、事件、信号量时间等超时等待)。
退出(Exit):该线程运行结束,等待父线程回收其控制块资源。
图 1 线程状态迁移示意图
注意官方文档说的是线程,没有提到task(任务),但内核源码中却有大量 task代码,很少有线程(thread)代码 ,这是怎么回事?
其实在鸿蒙内核中, task就是线程, 初学者完全可以这么理解,但二者还是有区别,否则干嘛要分两个词描述。
到底有什么区别?是管理上的区别,task是调度层面的概念,线程是进程层面的概念。 就像同一个人在不同的管理体系中会有不同的身份一样,一个男人既可以是 孩子,爸爸,丈夫,或者程序员,视角不同功能也会不同。 如何证明是一个东西,继续再往下看。
2. 执行task命令task命令 查出每个任务在生命周期内的运行情况,它运行的内存空间,优先级,时间片,入口执行函数,进程ID,状态等等信息,非常的复杂。这么复杂的信息就需要一个结构体来承载。而这个结构体就是 LosTaskCB(任务控制块)
对应张大爷的故事:task就是一个用户的节目清单里的一个节目,用户总清单就是一个进程,所以上面会有很多的节目。
3. task长得什么样子?说LosTaskCB之前先说下官方文档任务状态对应的 define,可以看出task和线程是一个东西。
- #define OS_TASK_STATUS_INIT 0x0001U
- #define OS_TASK_STATUS_READY 0x0002U
- #define OS_TASK_STATUS_RUNNING 0x0004U
- #define OS_TASK_STATUS_SUSPEND 0x0008U
- #define OS_TASK_STATUS_PEND 0x0010U
- #define OS_TASK_STATUS_DELAY 0x0020U
- #define OS_TASK_STATUS_TIMEOUT 0x0040U
- #define OS_TASK_STATUS_PEND_TIME 0x0080U
- #define OS_TASK_STATUS_EXIT 0x0100U
复制代码
LosTaskCB长什么样?抱歉,它确实有点长,但还是要全部贴出全貌。
- typedef struct {
- VOID *stackPointer; /**< Task stack pointer */
- UINT16 taskStatus; /**< Task status */
- UINT16 priority; /**< Task priority */
- UINT16 policy;
- UINT16 timeSlice; /**< Remaining time slice */
- UINT32 stackSize; /**< Task stack size */
- UINTPTR topOfStack; /**< Task stack top */
- UINT32 taskID; /**< Task ID */
- TSK_ENTRY_FUNC taskEntry; /**< Task entrance function */
- VOID *joinRetval; /**< pthread adaption */
- VOID *taskSem; /**< Task-held semaphore */
- VOID *taskMux; /**< Task-held mutex */
- VOID *taskEvent; /**< Task-held event */
- UINTPTR args[4]; /**< Parameter, of which the maximum number is 4 */
- CHAR taskName[OS_TCB_NAME_LEN]; /**< Task name */
- LOS_DL_LIST pendList; /**< Task pend node */
- LOS_DL_LIST threadList; /**< thread list */
- SortLinkList sortList; /**< Task sortlink node */
- UINT32 eventMask; /**< Event mask */
- UINT32 eventMode; /**< Event mode */
- UINT32 priBitMap; /**< BitMap for recording the change of task priority,
- the priority can not be greater than 31 */
- INT32 errorNo; /**< Error Num */
- UINT32 signal; /**< Task signal */
- sig_cb sig;
- #if (LOSCFG_KERNEL_SMP == YES)
- UINT16 currCpu; /**< CPU core number of this task is running on */
- UINT16 lastCpu; /**< CPU core number of this task is running on last time */
- UINT16 cpuAffiMask; /**< CPU affinity mask, support up to 16 cores */
- UINT32 timerCpu; /**< CPU core number of this task is delayed or pended */
- #if (LOSCFG_KERNEL_SMP_TASK_SYNC == YES)
- UINT32 syncSignal; /**< Synchronization for signal handling */
- #endif
- #if (LOSCFG_KERNEL_SMP_LOCKDEP == YES)
- LockDep lockDep;
- #endif
- #if (LOSCFG_KERNEL_SCHED_STATISTICS == YES)
- SchedStat schedStat; /**< Schedule statistics */
- #endif
- #endif
- UINTPTR userArea;
- UINTPTR userMapBase;
- UINT32 userMapSize; /**< user thread stack size ,real size : userMapSize + USER_STACK_MIN_SIZE */
- UINT32 processID; /**< Which belong process */
- FutexNode futex;
- LOS_DL_LIST joinList; /**< join list */
- LOS_DL_LIST lockList; /**< Hold the lock list */
- UINT32 waitID; /**< Wait for the PID or GID of the child process */
- UINT16 waitFlag; /**< The type of child process that is waiting, belonging to a group or parent,
- a specific child process, or any child process */
- #if (LOSCFG_KERNEL_LITEIPC == YES)
- UINT32 ipcStatus;
- LOS_DL_LIST msgListHead;
- BOOL accessMap[LOSCFG_BASE_CORE_TSK_LIMIT];
- #endif
- } LosTaskCB;
复制代码
结构体LosTaskCB内容很多,各代表什么含义?
LosTaskCB相当于任务在内核中的身份证,它反映出每个任务在生命周期内的运行情况。既然是周期就会有状态,要运行就需要内存空间,就需要被内核算法调度,被选中CPU就去执行代码段指令,CPU要执行就需要告诉它从哪里开始执行,因为是多线程,但只有一个CPU就需要不断的切换任务,那执行会被中断,也需要再恢复后继续执行,又如何保证恢复的任务执行不会出错,这些问题都需要说明白。
Task怎么管理
1.什么是任务池?前面已经说了任务是内核调度层面的概念,调度算法保证了task有序的执行,调度机制详见其他姊妹篇的介绍。
如此多的任务怎么管理和执行?管理靠任务池和就绪队列,执行靠调度算法。
代码如下(OsTaskInit):
- LITE_OS_SEC_TEXT_INIT UINT32 OsTaskInit(VOID)
- {
-
- UINT32 index;
- UINT32 ret;
- UINT32 size;
-
- g_taskMaxNum = LOSCFG_BASE_CORE_TSK_LIMIT;//任务池中最多默认128个,可谓铁打的任务池流水的线程
- size = (g_taskMaxNum + 1) * sizeof(LosTaskCB);
- /* * This memory is resident memory and is used to save the system resources * of task control block and will not be freed. */
- g_taskCBArray = (LosTaskCB *)LOS_MemAlloc(m_aucSysMem0, size);//任务池 常驻内存,不被释放
- if (g_taskCBArray == NULL) {
-
- return LOS_ERRNO_TSK_NO_MEMORY;
- }
- (VOID)memset_s(g_taskCBArray, size, 0, size);
-
- LOS_ListInit(&g_losFreeTask);//空闲任务链表
- LOS_ListInit(&g_taskRecyleList);//需回收任务链表
- for (index = 0; index < g_taskMaxNum; index++) {
-
- g_taskCBArray[index].taskStatus = OS_TASK_STATUS_UNUSED;
- g_taskCBArray[index].taskID = index;//任务ID最大默认127
- LOS_ListTailInsert(&g_losFreeTask, &g_taskCBArray[index].pendList);//都插入空闲任务链表
- }
-
- ret = OsPriQueueInit();//创建32个任务优先级队列,即32个双向循环链表
- if (ret != LOS_OK) {
-
- return LOS_ERRNO_TSK_NO_MEMORY;
- }
-
- /* init sortlink for each core */
- for (index = 0; index < LOSCFG_KERNEL_CORE_NUM; index++) {
-
- ret = OsSortLinkInit(&g_percpu[index].taskSortLink);//每个CPU内核都有一个执行任务链表
- if (ret != LOS_OK) {
-
- return LOS_ERRNO_TSK_NO_MEMORY;
- }
- }
- return LOS_OK;
- }
复制代码
g_taskCBArray 就是个任务池,默认创建128个任务,常驻内存,不被释放。
g_losFreeTask是空闲任务链表,想创建任务时来这里申请一个空闲任务,用完了就回收掉,继续给后面的申请使用。
g_taskRecyleList是回收任务链表,专用来回收exit 任务,任务所占资源被确认归还后被彻底删除,就像员工离职一样,得有个离职队列和流程,要归还电脑,邮箱,有没有借钱要还的 等操作。
对应张大爷的故事:用户要来场馆领取表格填节目单,场馆只准备了128张表格,领完就没有了,但是节目表演完了会回收表格,这样多了一张表格就可以给其他人领取了,这128张表格对应鸿蒙内核这就是任务池,简单吧。
2.就绪队列是怎么回事CPU执行速度是很快的,鸿蒙内核默认一个时间片是 10ms, 资源有限,需要在众多任务中来回的切换,所以绝不能让CPU等待任务,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;
- }
复制代码
注意看g_priQueueList 的内存分配,就是32个LOS_DL_LIST,还记得LOS_DL_LIST的妙用吗,不清楚的去 鸿蒙系统源码分析(总目录)里面翻。
对应张大爷的故事:就是门口那些排队的都是至少有一个节目单是符合表演标准的,资源都到位了,没有的连排队的资格都木有,就慢慢等吧。
3.任务栈是怎么回事每个任务都是独立开的,任务之间也相互独立,之间通讯通过IPC,这里的“独立”指的是每个任务都有自己的运行环境 —— 栈空间,称为任务栈,栈空间里保存的信息包含局部变量、寄存器、函数参数、函数返回地址等等
但系统中只有一个CPU,任务又是独立的,调度的本质就是CPU执行一个新task,老task在什么地方被中断谁也不清楚,是随机的。那如何保证老任务被再次调度选中时还能从上次被中断的地方继续玩下去呢?
答案是:任务上下文,CPU内有一堆的寄存器,CPU运行本质的就是这些寄存器的值不断的变化,只要切换时把这些值保存起来,再还原回去就能保证task的连续执行,让用户毫无感知。鸿蒙内核给一个任务执行的时间是 20ms ,也就是说有多任务竞争的情况下,一秒钟内最多要来回切换50次。
对应张大爷的故事:就是碰到节目没有表演完就必须打断的情况下,需要把当时的情况记录下来,比如小朋友在演躲猫猫的游戏,一半不演了,张三正在树上,李四正在厕所躲,都记录下来,下次再回来你们上次在哪就会哪呆着去,就位了继续表演。这样就接上了,观众就木有感觉了。
任务上下文(TaskContext)是怎样的呢?还是直接看源码
- /* The size of this structure must be smaller than or equal to the size specified by OS_TSK_STACK_ALIGN (16 bytes). */
- typedef struct {
-
- #if !defined(LOSCFG_ARCH_FPU_DISABLE)
- UINT64 D[FP_REGS_NUM]; /* D0-D31 */
- UINT32 regFPSCR; /* FPSCR */
- UINT32 regFPEXC; /* FPEXC */
- #endif
- UINT32 resved; /* It's stack 8 aligned */
- UINT32 regPSR;
- UINT32 R[GEN_REGS_NUM]; /* R0-R12 */
- UINT32 SP; /* R13 */
- UINT32 LR; /* R14 */
- UINT32 PC; /* R15 */
- } TaskContext;
复制代码
发现基本都是CPU寄存器的恢复现场值, 具体各寄存器有什么作用大家可以去网上详查,后续也有专门的文章来介绍。这里说其中的三个寄存器 SP, LR, PC。
LR
用途有二,一是保存子程序返回地址,当调用BL、BX、BLX等跳转指令时会自动保存返回地址到LR;二是保存异常发生的异常返回地址。
PC(Program Counter)
为程序计数器,用于保存程序的执行地址,在ARM的三级流水线架构中,程序流水线包括取址、译码和执行三个阶段,PC指向的是当前取址的程序地址,所以32位ARM中,译码地址(正在解析还未执行的程序)为PC-4,执行地址(当前正在执行的程序地址)为PC-8, 当突然发生中断的时候,保存的是PC的地址。
SP
每一种异常模式都有其自己独立的r13,它通常指向异常模式所专用的堆栈,当ARM进入异常模式的时候,程序就可以把一般通用寄存器压入堆栈,返回时再出栈,保证了各种模式下程序的状态的完整性。
4.任务栈初始化任务栈的初始化就是任务上下文的初始化,因为任务没开始执行,里面除了上下文不会有其他内容,注意上下文存放的位置在栈的底部。
Task函数集- LITE_OS_SEC_TEXT_INIT VOID *OsTaskStackInit(UINT32 taskID, UINT32 stackSize, VOID *topStack, BOOL initFlag)
- {
-
- UINT32 index = 1;
- TaskContext *taskContext = NULL;
-
- if (initFlag == TRUE) {
-
- OsStackInit(topStack, stackSize);
- }
- taskContext = (TaskContext *)(((UINTPTR)topStack + stackSize) - sizeof(TaskContext));//注意看上下文将存放在栈的底部
-
- /* initialize the task context */
- #ifdef LOSCFG_GDB
- taskContext->PC = (UINTPTR)OsTaskEntrySetupLoopFrame;
- #else
- taskContext->PC = (UINTPTR)OsTaskEntry;//程序计数器,CPU首次执行task时跑的第一条指令位置
- #endif
- taskContext->LR = (UINTPTR)OsTaskExit; /* LR should be kept, to distinguish it's THUMB or ARM instruction */
- taskContext->resved = 0x0;
- taskContext->R[0] = taskID; /* R0 */
- taskContext->R[index++] = 0x01010101; /* R1, 0x01010101 : reg initialed magic word */
- for (; index < GEN_REGS_NUM; index++) {
- //R2 - R12的初始化很有意思,为什么要这么做?
- taskContext->R[index] = taskContext->R[index - 1] + taskContext->R[1]; /* R2 - R12 */
- }
-
- #ifdef LOSCFG_INTERWORK_THUMB // 16位模式
- taskContext->regPSR = PSR_MODE_SVC_THUMB; /* CPSR (Enable IRQ and FIQ interrupts, THUMNB-mode) */
- #else
- taskContext->regPSR = PSR_MODE_SVC_ARM; /* CPSR (Enable IRQ and FIQ interrupts, ARM-mode) */
- #endif
-
- #if !defined(LOSCFG_ARCH_FPU_DISABLE)
- /* 0xAAA0000000000000LL : float reg initialed magic word */
- for (index = 0; index < FP_REGS_NUM; index++) {
-
- taskContext->D[index] = 0xAAA0000000000000LL + index; /* D0 - D31 */
- }
- taskContext->regFPSCR = 0;
- taskContext->regFPEXC = FP_EN;
- #endif
-
- return (VOID *)taskContext;
- }
复制代码
1.使用场景和功能任务创建后,内核可以执行锁任务调度,解锁任务调度,挂起,恢复,延时等操作,同时也可以设置任务优先级,获取任务优先级。任务结束的时候,则进行当前任务自删除操作。
Huawei LiteOS 系统中的任务管理模块为用户提供下面几种功能。
功能分类 | 接口名 | 描述 |
任务的创建和删除 | LOS_TaskCreateOnly | 创建任务,并使该任务进入suspend状态,并不调度。 |
| LOS_TaskCreate | 创建任务,并使该任务进入ready状态,并调度。 |
| LOS_TaskDelete | 删除指定的任务。 |
任务状态控制 | LOS_TaskResume | 恢复挂起的任务。 |
| LOS_TaskSuspend | 挂起指定的任务。 |
| LOS_TaskDelay | 任务延时等待。 |
| LOS_TaskYield | 显式放权,调整指定优先级的任务调度顺序。 |
任务调度的控制 | LOS_TaskLock | 锁任务调度。 |
| LOS_TaskUnlock | 解锁任务调度。 |
任务优先级的控制 | LOS_CurTaskPriSet | 设置当前任务的优先级。 |
| LOS_TaskPriSet | 设置指定任务的优先级。 |
| LOS_TaskPriGet | 获取指定任务的优先级。 |
任务信息获取 | LOS_CurTaskIDGet | 获取当前任务的ID。 |
| LOS_TaskInfoGet | 设置指定任务的优先级。 |
| LOS_TaskPriGet | 获取指定任务的信息。 |
| LOS_TaskStatusGet | 获取指定任务的状态。 |
| LOS_TaskNameGet | 获取指定任务的名称。 |
| LOS_TaskInfoMonitor | 监控所有任务,获取所有任务的信息。 |
| LOS_NextTaskIDGet | 获取即将被调度的任务的ID。 |
2.创建任务的过程创建任务之前先了解另一个结构体 tagTskInitParam
- typedef struct tagTskInitParam {
-
- TSK_ENTRY_FUNC pfnTaskEntry; /**< Task entrance function */
- UINT16 usTaskPrio; /**< Task priority */
- UINT16 policy; /**< Task policy */
- UINTPTR auwArgs[4]; /**< Task parameters, of which the maximum number is four */
- UINT32 uwStackSize; /**< Task stack size */
- CHAR *pcName; /**< Task name */
- #if (LOSCFG_KERNEL_SMP == YES)
- UINT16 usCpuAffiMask; /**< Task cpu affinity mask */
- #endif
- UINT32 uwResved; /**< It is automatically deleted if set to LOS_TASK_STATUS_DETACHED. It is unable to be deleted if set to 0. */
- UINT16 consoleID; /**< The console id of task belongs */
- UINT32 processID;
- UserTaskParam userParam;
- } TSK_INIT_PARAM_S;
复制代码
发现ping的调度优先级是8,比shell 还高,那shell的是多少?答案是:看源码是 9
- u32_t osShellPing(int argc, const char **argv)
- {
-
- int ret;
- u32_t i = 0;
- u32_t count = 0;
- int count_set = 0;
- u32_t interval = 1000; /* default ping interval */
- u32_t data_len = 48; /* default data length */
- ip4_addr_t dst_ipaddr;
- TSK_INIT_PARAM_S stPingTask;
- // ...省去一些中间代码
- /* start one task if ping forever or ping count greater than 60 */
- if (count == 0 || count > LWIP_SHELL_CMD_PING_RETRY_TIMES) {
-
- if (ping_taskid > 0) {
-
- PRINTK("Ping task already running and only support one nown");
- return LOS_NOK;
- }
- stPingTask.pfnTaskEntry = (TSK_ENTRY_FUNC)ping_cmd;//线程的执行函数
- stPingTask.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;//0x4000 = 16K
- stPingTask.pcName = "ping_task";
- stPingTask.usTaskPrio = 8; /* higher than shell 优先级高于10,属于内核态线程*/
- stPingTask.uwResved = LOS_TASK_STATUS_DETACHED;
- stPingTask.auwArgs[0] = dst_ipaddr.addr; /* network order */
- stPingTask.auwArgs[1] = count;
- stPingTask.auwArgs[2] = interval;
- stPingTask.auwArgs[3] = data_len;
- ret = LOS_TaskCreate((UINT32 *)(&ping_taskid), &stPingTask);
- }
- // ...
- return LOS_OK;
- ping_error:
- lwip_ping_usage();
- return LOS_NOK;
- }
复制代码
关于shell后续会详细介绍,请持续关注。
前置条件了解清楚后,具体看任务是如何一步步创建的,如何和进程绑定,加入调度就绪队列,还是继续看源码
- //创建Task
- LITE_OS_SEC_TEXT_INIT UINT32 LOS_TaskCreate(UINT32 *taskID, TSK_INIT_PARAM_S *initParam)
- {
- UINT32 ret;
- UINT32 intSave;
- LosTaskCB *taskCB = NULL;
-
- if (initParam == NULL) {
- return LOS_ERRNO_TSK_PTR_NULL;
- }
-
- if (OS_INT_ACTIVE) {
- return LOS_ERRNO_TSK_YIELD_IN_INT;
- }
-
- if (initParam->uwResved & OS_TASK_FLAG_IDLEFLAG) {//OS_TASK_FLAG_IDLEFLAG 是属于内核 idle进程专用的
- initParam->processID = OsGetIdleProcessID();//获取空闲进程
- } else if (OsProcessIsUserMode(OsCurrProcessGet())) {//当前进程是否为用户模式
- initParam->processID = OsGetKernelInitProcessID();//不是就取"Kernel"进程
- } else {
- initParam->processID = OsCurrProcessGet()->processID;//获取当前进程 ID赋值
- }
- initParam->uwResved &= ~OS_TASK_FLAG_IDLEFLAG;//不能是 OS_TASK_FLAG_IDLEFLAG
- initParam->uwResved &= ~OS_TASK_FLAG_PTHREAD_JOIN;//不能是 OS_TASK_FLAG_PTHREAD_JOIN
- if (initParam->uwResved & LOS_TASK_STATUS_DETACHED) {//是否设置了自动删除
- initParam->uwResved = OS_TASK_FLAG_DETACHED;//自动删除,注意这里是 = ,也就是说只有 OS_TASK_FLAG_DETACHED 一个标签了
- }
-
- ret = LOS_TaskCreateOnly(taskID, initParam);//创建一个任务,这是任务创建的实体,前面都只是前期准备工作
- if (ret != LOS_OK) {
- return ret;
- }
- taskCB = OS_TCB_FROM_TID(*taskID);//通过ID拿到task实体
-
- SCHEDULER_LOCK(intSave);
- taskCB->taskStatus &= ~OS_TASK_STATUS_INIT;//任务不再是初始化
- OS_TASK_SCHED_QUEUE_ENQUEUE(taskCB, 0);//进入调度就绪队列,新任务是直接进入就绪队列的
- SCHEDULER_UNLOCK(intSave);
-
- /* in case created task not running on this core,
- schedule or not depends on other schedulers status. */
- LOS_MpSchedule(OS_MP_CPU_ALL);//如果创建的任务没有在这个核心上运行,是否调度取决于其他调度程序的状态。
- if (OS_SCHEDULER_ACTIVE) {//当前CPU核处于可调度状态
- LOS_Schedule();//发起调度
- }
-
- return LOS_OK;
- }
复制代码
对应张大爷的故事:就是节目单要怎么填,按格式来,从哪里开始演,要多大的空间,王场馆好协调好现场的环境。这里注意 在同一个节目单只要节目没演完,王场馆申请场地的空间就不能给别人用,这个场地空间对应的就是鸿蒙任务的栈空间,除非整个节目单都完了,就回收了。把整个场地干干净净的留给下一个人的节目单来表演。
至此的创建已经完成,已各就各位,源码最后还申请了一次LOS_Schedule();因为鸿蒙的调度方式是抢占式的,如何本次task的任务优先级高于其他就绪队列,那么接下来要执行的任务就是它了!
3.Task部分还有哪些重要内容没讲到?内存怎么分配的,线程之间怎么通讯,运行期间堆栈是怎么执行的,等在内存,IPC有详细的描述
鸿蒙内核源码注释中文版 进入>>【 Gitee仓】阅读,代码仓库加注同步更新.... 文章来源:图解鸿蒙源码逐行注释分析