[文章]

鸿蒙内核源码分析(进程管理篇):进程是内核的资源管理单元

2020-11-24 11:23:44  321 鸿蒙系统 内核 源码
分享
0
基本概念(先看官方文档一定要读)
从系统的角度看,进程是资源管理单元。进程可以使用或等待CPU、使用内存空间等系统资源,并独立于其它进程运行。

OpenHarmony内核的进程模块可以给用户提供多个进程,实现了进程之间的切换和通信,帮助用户管理业务程序流程。这样用户可以将更多的精力投入到业务功能的实现中。

OpenHarmony内核中的进程采用抢占式调度机制,支持时间片轮转调度方式和FIFO调度机制。

OpenHarmony内核的进程一共有32个优先级(0-31),用户进程可配置的优先级有22个(10-31),最高优先级为10,最低优先级为31。

高优先级的进程可抢占低优先级进程,低优先级进程必须在高优先级进程阻塞或结束后才能得到调度。

每一个用户态进程均拥有自己独立的进程空间,相互之间不可见,实现进程间隔离。
用户态根进程init由内核态创建,其它用户态进程均由init进程fork而来。

进程状态说明:
  • 初始化(Init):该进程正在被创建。
  • 就绪(Ready):该进程在就绪列表中,等待CPU调度。
  • 运行(Running):该进程正在运行。
  • 阻塞(Pend):该进程被阻塞挂起。本进程内所有的线程均被阻塞时,进程被阻塞挂起。
  • 僵尸态(Zombies):该进程运行结束,等待父进程回收其控制块资源。
图 1 进程状态迁移示意图
进程状态迁移说明:
  • Init→Ready:
    进程创建或fork时,拿到该进程控制块后进入Init状态,处于进程初始化阶段,当进程初始化完成将进程插入调度队列,此时进程进入就绪状态。
  • Ready→Running:
    进程创建后进入就绪态,发生进程切换时,就绪列表中最高优先级的进程被执行,从而进入运行态。若此时该进程中已无其它线程处于就绪态,则该进程从就绪列表删除,只处于运行态;若此时该进程中还有其它线程处于就绪态,则该进程依旧在就绪队列,此时进程的就绪态和运行态共存。
  • Running→Pend:
    进程内所有的线程均处于阻塞态时,进程在最后一个线程转为阻塞态时,同步进入阻塞态,然后发生进程切换。
  • Pend→Ready / Pend→Running:
    阻塞进程内的任意线程恢复就绪态时,进程被加入到就绪队列,同步转为就绪态,若此时发生进程切换,则进程状态由就绪态转为运行态。
  • Ready→Pend:
    进程内的最后一个就绪态线程处于阻塞态时,进程从就绪列表中删除,进程由就绪态转为阻塞态。
  • Running→Ready:
    进程由运行态转为就绪态的情况有以下两种:
    • 有更高优先级的进程创建或者恢复后,会发生进程调度,此刻就绪列表中最高优先级进程变为运行态,那么原先运行的进程由运行态变为就绪态。
    • 若进程的调度策略为SCHED_RR,且存在同一优先级的另一个进程处于就绪态,则该进程的时间片消耗光之后,该进程由运行态转为就绪态,另一个同优先级的进程由就绪态转为运行态。
  • Running→Zombies:
    当进程的主线程或所有线程运行结束后,进程由运行态转为僵尸态,等待父进程回收资源。
使用场景
进程创建后,用户只能操作自己进程空间的资源,无法操作其它进程的资源(共享资源除外)。 用户态允许进程挂起,恢复,延时等操作,同时也可以设置用户态进程调度优先级和调度策略,获取进程调度优先级和调度策略。进程结束的时候,进程会主动释放持有的进程资源,但持有的进程pid资源需要父进程通过wait/waitpid或父进程退出时回收。
开始正式分析
对应张大爷的故事,进程就是那些在场馆外32个队列里排队的,那些队列就是进程的就绪队列。
请注意上面标注的红色的字 进程是资源管理单元 ,而非调度单元,调度单元是谁?是 Task ,看下官方对应状态的define
  1. #define OS_PROCESS_STATUS_INIT           0x0010U
  2. #define OS_PROCESS_STATUS_READY          0x0020U
  3. #define OS_PROCESS_STATUS_RUNNING        0x0040U
  4. #define OS_PROCESS_STATUS_PEND           0x0080U
  5. #define OS_PROCESS_STATUS_ZOMBIES        0x100U
复制代码
一个进程从创建到消亡过程,在内核肯定是极其复杂的。为了方便理解进程,整个系列文章笔者会用张大爷的故事打比方,从生活中的例子来将神秘的系统内核外化解剖出来给大家看。一件这么复杂的事情肯定会有个复杂的结构体来承载,它就是LosProcessCB(进程控制块),代码很长但必须全部拿出来,长是长了点,忍忍吧!
  1. LITE_OS_SEC_BSS LosProcessCB *g_runProcess[LOSCFG_KERNEL_CORE_NUM]; //*kyf 用一个指针数组记录进程运行,LOSCFG_KERNEL_CORE_NUM 为 CPU的核数
  2. LITE_OS_SEC_BSS LosProcessCB *g_processCBArray = NULL;//*kyf 进程池,最大进程数为 64个
  3. LITE_OS_SEC_DATA_INIT STAtiC LOS_DL_LIST g_freeProcess;//*kyf 记录空闲的进程链表
  4. LITE_OS_SEC_DATA_INIT STATIC LOS_DL_LIST g_processRecyleList;//*kyf 记录回收的进程列表

  5. typedef struct ProcessCB {
  6.     CHAR                 processName[OS_PCB_NAME_LEN]; /**< Process name */
  7.     UINT32               processID;                    /**< process ID = leader thread ID */
  8.     UINT16               processStatus;                /**< [15:4] process Status; [3:0] The number of threads currently
  9.                                                             running in the process */
  10.     UINT16               priority;                     /**< process priority */
  11.     UINT16               policy;                       /**< process policy */
  12.     UINT16               timeSlice;                    /**< Remaining time slice */
  13.     UINT16               consoleID;                    /**< The console id of task belongs  */
  14.     UINT16               processMode;                  /**< Kernel Mode:0; User Mode:1; */
  15.     UINT32               parentProcessID;              /**< Parent process ID */
  16.     UINT32               exitCode;                     /**< process exit status */
  17.     LOS_DL_LIST          pendList;                     /**< Block list to which the process belongs */
  18.     LOS_DL_LIST          childrenList;                 /**< my children process list */
  19.     LOS_DL_LIST          exitChildList;                /**< my exit children process list */
  20.     LOS_DL_LIST          siblingList;                  /**< linkage in my parent's children list */
  21.     ProcessGroup         *group;                       /**< Process group to which a process belongs */
  22.     LOS_DL_LIST          subordinateGroupList;         /**< linkage in my group list */
  23.     UINT32               threadGroupID;                /**< Which thread group , is the main thread ID of the process */
  24.     UINT32               threadScheduleMap;            /**< The scheduling bitmap table for the thread group of the
  25.                                                             process */
  26.     LOS_DL_LIST          threadSiblingList;            /**< List of threads under this process */
  27.     LOS_DL_LIST          threadPriQueueList[OS_PRIORITY_QUEUE_NUM]; /**< The process's thread group schedules the
  28.                                                                          priority hash table */
  29.     volatile UINT32      threadNumber; /**< Number of threads alive under this process */
  30.     UINT32               threadCount;  /**< Total number of threads created under this process */
  31.     LOS_DL_LIST          waitList;     /**< The process holds the waitLits to support wait/waitpid */
  32. #if (LOSCFG_KERNEL_SMP == YES)
  33.     UINT32               timerCpu;     /**< CPU core number of this task is delayed or pended */
  34. #endif
  35.     UINTPTR              sigHandler;   /**< signal handler */
  36.     sigset_t             sigShare;     /**< signal share bit */
  37. #if (LOSCFG_KERNEL_LITEIPC == YES)
  38.     ProcIpcInfo         ipcInfo;       /**< memory pool for lite ipc */
  39. #endif
  40.     LosVmSpace          *vmSpace;       /**< VMM space for processes */
  41. #ifdef LOSCFG_FS_VFS
  42.     struct files_struct *files;        /**< Files held by the process */
  43. #endif
  44.     timer_t             timerID;       /**< iTimer */

  45. #ifdef LOSCFG_SECURITY_CAPABILITY
  46.     User                *user;
  47.     UINT32              capability;
  48. #endif
  49. #ifdef LOSCFG_SECURITY_VID
  50.     TimerIdMap          timerIdMap;
  51. #endif
  52. #ifdef LOSCFG_DRIVERS_TZDRIVER
  53.     struct file         *execFile;     /**< Exec bin of the process */
  54. #endif
  55.     mode_t umask;
  56. } LosProcessCB;
复制代码

进程的模式有两种,内核态和用户态,能想到main函数中肯定会创建一个内核态的最高优先级进程,他就是 KProcess
调用过程如下
通过task命令查看任务运行状态,可以看到 KProcess 进程 ,看名字就知道是一个内核进程,在系统启动时创建,图中可以看到 KProcess 的task运行情况,从表里可以看到KProcess内有 10几个task
进程初始化
  1. //单核CPU只有并发(Concurrent),多核才会有并行(Parallel) LITE_OS_SEC_BSS 和 LITE_OS_SEC_DATA_INIT 是告诉编译器这些全局变量放在哪个数据段
  2. LITE_OS_SEC_BSS LosProcessCB *g_runProcess[LOSCFG_KERNEL_CORE_NUM];// CPU内核个数,这才是真正的并行
  3. LITE_OS_SEC_BSS LosProcessCB *g_processCBArray = NULL; // 进程池数组
  4. LITE_OS_SEC_DATA_INIT STATIC LOS_DL_LIST g_freeProcess;// 空闲状态下可供分配的进程,此时进程白纸一张
  5. LITE_OS_SEC_DATA_INIT STATIC LOS_DL_LIST g_processRecyleList;// 需要回收的进程列表
  6. LITE_OS_SEC_BSS UINT32 g_userInitProcess = OS_INVALID_VALUE;// 用户态的初始init进程,用户态下其他进程由它 fork
  7. LITE_OS_SEC_BSS UINT32 g_kernelInitProcess = OS_INVALID_VALUE;// 内核态初始Kprocess进程,内核态下其他进程由它 fork
  8. LITE_OS_SEC_BSS UINT32 g_kernelIdleProcess = OS_INVALID_VALUE;// 内核态idle进程,由Kprocess fork
  9. LITE_OS_SEC_BSS UINT32 g_processMaxNum;// 进程最大数量
  10. LITE_OS_SEC_BSS ProcessGroup *g_processGroup = NULL;// 进程组
复制代码


以上是进程模块全局变量。注释是笔者添加的,鸿蒙内核的注释很少,查看更多注释前往以下仓库

鸿蒙内核源码注释中文版 进入>>【CSDN仓】阅读,代码仓库正加注同步更新中....

KProcess 在张大爷的故事里相当于场馆的工作人员,他们也要接受张大爷的调度排队进场,但他们的优先级是最高的0级,他们进场后需完成场馆的准备工作,再开门做生意。如果需要多个工作人员怎么办,就是通过fork,简单说就是复制一个,复制的前提是需要有一个,鸿蒙里就是KProcess,其他工作人员都是通过它fork的。那用户怎么来的呢?就是真正要排队的人也是一样,先创建一个用户爸爸,其他用户就用爸爸fork来的,注意是fork 不是那个什么K,哈哈,你懂得。

还是直接看代码吧
  1. /**
  2. * @ingroup los_config
  3. * Maximum supported number of process rather than the number of usable processes.
  4. */
  5. #ifndef LOSCFG_BASE_CORE_PROCESS_LIMIT
  6. #define LOSCFG_BASE_CORE_PROCESS_LIMIT 64
  7. #endif

  8. //进程模块初始化,被编译放在代码段 .init 中
  9. LITE_OS_SEC_TEXT_INIT UINT32 OsProcessInit(VOID)
  10. {
  11.     UINT32 index;
  12.     UINT32 size;

  13.     g_processMaxNum = LOSCFG_BASE_CORE_PROCESS_LIMIT;//默认支持64个进程
  14.     size = g_processMaxNum * sizeof(LosProcessCB);//算出总大小

  15.     g_processCBArray = (LosProcessCB *)LOS_MemAlloc(m_aucSysMem1, size);// 进程池,占用内核堆,内存池分配
  16.     if (g_processCBArray == NULL) {
  17.         return LOS_NOK;
  18.     }
  19.     (VOID)memset_s(g_processCBArray, size, 0, size);//安全方式重置清0

  20.     LOS_ListInit(&g_freeProcess);//进程空闲链表初始化,创建一个进程时从g_freeProcess中申请一个进程描述符使用
  21.     LOS_ListInit(&g_processRecyleList);//进程回收链表初始化,回收完成后进入g_freeProcess等待再次被申请使用
复制代码
代码已经很清楚,创建了一个进程池,默认64个进程,也就是不改宏LOSCFG_BASE_CORE_PROCESS_LIMIT的情况下 系统最多是64个进程,但有两个进程先被占用,用户态和内核态各一个,他们是后续创建进程的爹,所以最多留给外面的只有 62个进程可创建,代码的最后两个爸爸的task阻塞链表被清空了,因为没有阻塞任务当然要清空。

创建内核态Kprocess的过程
创建核心态进程,也就是线程池中的 [2] 号进程,task 命令中 Kprocess PID = 2, 参数是 内核态和 最高优先级 0
OsKernelInitProcess
OsCreateIdleProcess
代码的把kprocess 设为当前进程,并且fork了一个 KIdle(内核态的空闲进程)
  1. //创建一个名叫"KIdle"的进程,给CPU空闲的时候使用
  2. STATIC UINT32 OsCreateIdleProcess(VOID)
  3. {
  4.     UINT32 ret;
  5.     CHAR *idleName = "Idle";
  6.     LosProcessCB *idleProcess = NULL;
  7.     Percpu *perCpu = OsPercpuGet();
  8.     UINT32 *idleTaskID = &perCpu->idleTaskID;//得到CPU的idle task

  9.     ret = OsCreateResourceFreeTask();// 创建一个资源回收任务,优先级为5 用于回收进程退出时的各种资源
  10.     if (ret != LOS_OK) {
  11.         return ret;
  12.     }
  13.         //创建一个名叫"KIdle"的进程,并创建一个idle task,CPU空闲的时候就待在 idle task中等待被唤醒
  14.     ret = LOS_Fork(CLONE_FILES, "KIdle", (TSK_ENTRY_FUNC)OsIdleTask, LOSCFG_BASE_CORE_TSK_IDLE_STACK_SIZE);
  15.     if (ret < 0) {
  16.         return LOS_NOK;
  17.     }
  18.     g_kernelIdleProcess = (UINT32)ret;//返回进程ID

  19.     idleProcess = OS_PCB_FROM_PID(g_kernelIdleProcess);//通过ID拿到进程实体
  20.     *idleTaskID = idleProcess->threadGroupID;//绑定CPU的IdleTask,或者说改变CPU现有的idle任务
  21.     OS_TCB_FROM_TID(*idleTaskID)->taskStatus |= OS_TASK_FLAG_SYSTEM_TASK;//设定Idle task 为一个系统任务
  22. #if (LOSCFG_KERNEL_SMP == YES)
  23.     OS_TCB_FROM_TID(*idleTaskID)->cpuAffiMask = CPUID_TO_AFFI_MASK(ArchCurrCpuid());//多核CPU的任务指定,防止乱串了,注意多核才会有并行处理
  24. #endif
  25.     (VOID)memset_s(OS_TCB_FROM_TID(*idleTaskID)->taskName, OS_TCB_NAME_LEN, 0, OS_TCB_NAME_LEN);//task 名字先清0
  26.     (VOID)memcpy_s(OS_TCB_FROM_TID(*idleTaskID)->taskName, OS_TCB_NAME_LEN, idleName, strlen(idleName));//task 名字叫 idle
  27.     return LOS_OK;
  28. }
复制代码

OsIdleTask
CPU空闲时是跑在idleTask中的,这里是CPU休息的地方,进入低电量模式,等待被事件唤醒上班
  1. //空闲任务 注意 #define WEAK       __attribute__((weak)) 是用于防止crash的
  2. LITE_OS_SEC_TEXT WEAK VOID OsIdleTask(VOID)
  3. {
  4.     while (1) {//只有一个死循环
  5. #ifdef LOSCFG_KERNEL_TICKLESS
  6.         if (OsTickIrqFlagGet()) {
  7.             OsTickIrqFlagSet(0);
  8.             OsTicklessStart();
  9.         }
  10. #endif
  11.         Wfi();
  12.     }
  13. }
  14. VOID Wfi(VOID)//WFI指令:arm core 立即进入low-power standby state,直到有WFI Wakeup events发生
  15. {
  16.     __asm__ __volatile__ ("wfi" : : : "memory");//一般用于cpuidle
  17. }
复制代码

创建用户态进程的过程是怎样的?看代码
  1. /**
  2. * @ingroup los_process
  3. * User state root process default priority
  4. */
  5. #define OS_PROCESS_USERINIT_PRIORITY     28

  6. //所有的用户进程都是使用同一个用户代码段描述符和用户数据段描述符,它们是__USER_CS和__USER_DS,也就是每个进程处于用户态时,它们的CS寄存器和DS寄存器中的值是相同的。当任何进程或者中断异常进入内核后,都是使用相同的内核代码段描述符和内核数据段描述符,它们是__KERNEL_CS和__KERNEL_DS。这里要明确记得,内核数据段实际上就是内核态堆栈段。
  7. LITE_OS_SEC_TEXT_INIT UINT32 OsUserInitProcess(VOID)
  8. {
  9.     INT32 ret;
  10.     UINT32 size;
  11.     TSK_INIT_PARAM_S param = { 0 };
  12.     VOID *stack = NULL;
  13.     VOID *userText = NULL;
  14.     CHAR *userInitTextStart = (CHAR *)&__user_init_entry;//代码区开始位置 ,所有进程
  15.     CHAR *userInitBssStart = (CHAR *)&__user_init_bss;// 未初始化数据区(BSS)。在运行时改变其值
  16.     CHAR *userInitEnd = (CHAR *)&__user_init_end;// 结束地址
  17.     UINT32 initBssSize = userInitEnd - userInitBssStart;
  18.     UINT32 initSize = userInitEnd - userInitTextStart;

  19.     LosProcessCB *processCB = OS_PCB_FROM_PID(g_userInitProcess);
  20.     ret = OsProcessCreateInit(processCB, OS_USER_MODE, "Init", OS_PROCESS_USERINIT_PRIORITY);// 初始化用户进程,它将是所有应用程序的父进程
  21.     if (ret != LOS_OK) {
  22.         return ret;
  23.     }

  24.     userText = LOS_PhysPagesAllocContiguous(initSize >> PAGE_SHIFT);// 分配连续的物理页
  25.     if (userText == NULL) {
  26.         ret = LOS_NOK;
  27.         goto ERROR;
  28.     }

  29.     (VOID)memcpy_s(userText, initSize, (VOID *)&__user_init_load_addr, initSize);// 安全copy 经加载器load的结果 __user_init_load_addr -> userText
  30.     ret = LOS_VaddrToPaddrMmap(processCB->vmSpace, (VADDR_T)(UINTPTR)userInitTextStart, LOS_PaddrQuery(userText),
  31.                                initSize, VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE |
  32.                                VM_MAP_REGION_FLAG_PERM_EXECUTE | VM_MAP_REGION_FLAG_PERM_USER);// 虚拟地址与物理地址的映射
  33.     if (ret < 0) {
  34.         goto ERROR;
  35.     }

  36.     (VOID)memset_s((VOID *)((UINTPTR)userText + userInitBssStart - userInitTextStart), initBssSize, 0, initBssSize);// 除了代码段,其余都清0

  37.     stack = OsUserInitStackAlloc(g_userInitProcess, &size);// 初始化堆栈区
  38.     if (stack == NULL) {
  39.         PRINTK("user init process malloc user stack faiLED!\n");
  40.         ret = LOS_NOK;
  41.         goto ERROR;
  42.     }

  43.     param.pfnTaskEntry = (TSK_ENTRY_FUNC)userInitTextStart;// 从代码区开始执行,也就是应用程序main 函数的位置
  44.     param.userParam.userSP = (UINTPTR)stack + size;// 指向栈顶
  45.     param.userParam.userMapBase = (UINTPTR)stack;// 栈底
  46.     param.userParam.userMapSize = size;// 栈大小
  47.     param.uwResved = OS_TASK_FLAG_PTHREAD_JOIN;// 可结合的(joinable)能够被其他线程收回其资源和杀死
  48.     ret = OsUserInitProcessStart(g_userInitProcess, ¶m);// 创建一个任务,来运行main函数
  49.     if (ret != LOS_OK) {
  50.         (VOID)OsUnMMap(processCB->vmSpace, param.userParam.userMapBase, param.userParam.userMapSize);
  51.         goto ERROR;
  52.     }

  53.     return LOS_OK;

  54. ERROR:
  55.     (VOID)LOS_PhysPagesFreeContiguous(userText, initSize >> PAGE_SHIFT);//释放物理内存块
  56.     OsDeInitPCB(processCB);//删除PCB块
  57.     return ret;
  58. }
复制代码

发现用户态init 和 创建的过程类似 内核态,并且用户态爸爸进程的优先级是 28,好低啊。

对应张大爷的故事:工作人员和用户的工作环境是不一样的,工作人员可以全场活动,但用户不能去管理处溜达,你只能在属于你表演的场地活动,这个场地就是用户空间,这涉及到内存的管理,非常复杂, 具体去系列篇看怎么管理内存的文章。

留下两个小问题请大家思考
OsUserInitProcess里有些关于内存的代码,源码中找不到值,比如:
  1.     CHAR *userInitTextStart = (CHAR *)&__user_init_entry;
  2.     CHAR *userInitBssStart = (CHAR *)&__user_init_bss;
  3.     CHAR *userInitEnd = (CHAR *)&__user_init_end;
复制代码
为什么会这样?
另外两个爸爸对应的PID是 1和2,那进程池里的0号进程又去哪里了呢?

全部文章进入 >> 鸿蒙系统源码分析(总目录) 查看
鸿蒙内核源码注释中文版 进入>>【Gitee仓】阅读,代码仓库加注同步更新....
文章来源:图解鸿蒙源码逐行注释分析

评论

您需要登录后才可以回帖 登录 | 注册

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容图片侵权或者其他问题,请联系本站作侵删。 侵权投诉
发文章