首页 文章 精选 留言 我的

精选列表

搜索[HarmonyOS SDK],共10000篇文章
优秀的个人博客,低调大师

鸿蒙内核源码分析(信号消费篇) | 用户栈到内核栈的两次切换 | 百篇博客分析HarmonyOS源码 | v49.02

百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< oschina | 51cto | csdn | harmony > 信号消费 本篇为信号消费篇,读之前建议先阅读信号生产篇,信号部分姊妹篇如下: v48.xx (信号生产篇) | 如何安装和发送信号? < csdn | 51cto | harmony > v49.xx (信号消费篇) | 用户栈到内核栈的两次切换 < csdn | 51cto | harmony > 本篇有相当的难度,涉及用户栈和内核栈的两次切换,寄存器改值,将围绕下图来说明. 解读 为本篇理解方便,把图做简化标签说明: user:用户空间, kernel:内核空间, source:源函数 sighandle:信号处理函数,syscall:任意系统调用号-N, syscall:信号处理完成的系统调用号-sigreturn 系列篇已多次说过,用户态的任务有两个运行栈,一个是用户栈,一个是内核栈.栈空间分别来自用户空间和内核空间.两种空间是有严格的地址划分的,通过虚拟地址的大小就能判断出是用户空间还是内核空间.系统调用本质上是软中断,它使CPU执行指令的场地由用户栈变成内核栈.怎么变的并不复杂,就是改变(sp和cpsr寄存器的值).sp指向哪个栈就代表在哪个栈运行, 当cpu在用户栈运行时是不能访问内核空间的,但内核态任务可以访问整个空间,而且内核态任务没有用户栈. 理解了上面的说明,再来说下正常系统调用流程是这样的: user.source() -> kernel.syscall(N) - > user.source() ,想要回到user.source()继续运行,就必须保存用户栈现场各寄存器的值.这些值保存在内核栈中,恢复也是从内核栈恢复. 信号消费的过程的上图可简化表示为: user.source() -> kernel.syscall(N) ->user.sighandle() ->kernel.syscall(sigreturn) -> user.source() 在原本要回到user.source()的中间插入了信号处理函数的调用. 这正是本篇要通过代码来说清楚的核心问题. 顺着这个思路可以推到以下几点,实际也是这么做的: kernel.syscall(N) 中必须要再次保存user.source()的上下文sig_switch_context,为什么已经保存了一遍还要再保存一次? 因为第一次是保存在内核栈中,而内核栈的数据会因回到用户态user.sighandle()运行而被清空数据.第二次保存在任务结构体中,任务来源于任务池,是内核全局变量,常驻内存的. typedef struct { // ... sig_cb sig;//信号控制块,用于异步通信 } LosTaskCB; 还必须要改变原有PC/R0/R1寄存器的值.因为要执行user.sighandle(),PC寄存器就必须指向它,而R0,R1就是它的参数. 信号处理完成后须回到内核态,怎么再次陷入内核态? 答案是:__NR_sigreturn,这也是个系统调用.回来后还原sig_switch_context,即还原user.source()被打断时SP/PC等寄存器的值,使其跳回到用户栈从user.source()打断处继续执行. 有了这三个推论,再理解下面的代码就是吹灰之力了,涉及三个关键函数 OsArmA32SyscallHandle,OsSaveSignalContext,OsRestorSignalContext本篇一一解读,彻底挖透.先看信号上下文结构体sig_switch_context. sig_switch_context //任务中断上下文 #define TASK_IRQ_CONTEXT \ unsigned int R0; \ unsigned int R1; \ unsigned int R2; \ unsigned int R3; \ unsigned int R12; \ unsigned int USP; \ unsigned int ULR; \ unsigned int CPSR; \ unsigned int PC; typedef struct {//信号切换上下文 TASK_IRQ_CONTEXT unsigned int R7; //存放系统调用的ID unsigned int count; //记录是否保存了信号上下文 } sig_switch_context; 这是保存用户栈的结构体,USP,ULR代表用户栈指针和返回地址. 其他寄存器没有保存的原因是系统调用不会用到那些寄存器,所以不需要保存. R7是在系统调用发生时用于记录系统调用号,在信号处理过程中,R0将获得信号编号,作为user.sighandle()的第一个参数. count记录是否保存了信号上下文 OsArmA32SyscallHandle 系统调用总入口 /* The SYSCALL ID is in R7 on entry. Parameters follow in R0..R6 */ /****************************************************************** 由汇编调用,见于 los_hw_exc.s / BLX OsArmA32SyscallHandle SYSCALL是产生系统调用时触发的信号,R7寄存器存放具体的系统调用ID,也叫系统调用号 regs:参数就是所有寄存器 注意:本函数在用户态和内核态下都可能被调用到 //MOV R0, SP @获取SP值,R0将作为OsArmA32SyscallHandle的参数 ******************************************************************/ LITE_OS_SEC_TEXT UINT32 *OsArmA32SyscallHandle(UINT32 *regs) { UINT32 ret; UINT8 nArgs; UINTPTR handle; UINT32 cmd = regs[REG_R7];//C7寄存器记录了触发了具体哪个系统调用 if (cmd >= SYS_CALL_NUM) {//系统调用的总数 PRINT_ERR("Syscall ID: error %d !!!\n", cmd); return regs; } //用户进程信号处理函数完成后的系统调用 svc 119 #__NR_sigreturn if (cmd == __NR_sigreturn) { OsRestorSignalContext(regs);//恢复信号上下文,回到用户栈运行. return regs; } handle = g_syscallHandle[cmd];//拿到系统调用的注册函数,类似 SysRead nArgs = g_syscallNArgs[cmd / NARG_PER_BYTE]; /* 4bit per nargs */ nArgs = (cmd & 1) ? (nArgs >> NARG_BITS) : (nArgs & NARG_MASK);//获取参数个数 if ((handle == 0) || (nArgs > ARG_NUM_7)) {//系统调用必须有参数且参数不能大于8个 PRINT_ERR("Unsupport syscall ID: %d nArgs: %d\n", cmd, nArgs); regs[REG_R0] = -ENOSYS; return regs; } //regs[0-6] 记录系统调用的参数,这也是由R7寄存器保存系统调用号的原因 switch (nArgs) {//参数的个数 case ARG_NUM_0: case ARG_NUM_1: ret = (*(SyscallFun1)handle)(regs[REG_R0]);//执行系统调用,类似 SysUnlink(pathname); break; case ARG_NUM_2://如何是两个参数的系统调用,这里传三个参数也没有问题,因被调用函数不会去取用R2值 case ARG_NUM_3: ret = (*(SyscallFun3)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2]);//类似 SysExecve(fileName, argv, envp); break; case ARG_NUM_4: case ARG_NUM_5: ret = (*(SyscallFun5)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3], regs[REG_R4]); break; default: //7个参数的情况 ret = (*(SyscallFun7)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3], regs[REG_R4], regs[REG_R5], regs[REG_R6]); } regs[REG_R0] = ret;//R0保存系统调用返回值 OsSaveSignalContext(regs);//如果有信号要处理,将改写pc,r0,r1寄存器,改变返回正常用户态路径,而先去执行信号处理程序. /* Return the last value of curent_regs. This supports context switches on return from the exception. * That capability is only used with the SYS_context_switch system call. */ return regs;//返回寄存器的值 } 解读 这是系统调用的总入口,所有的系统调用都要跑这里要统一处理.通过系统号(保存在R7),找到注册函数并回调.完成系统调用过程. 关于系统调用可查看 v37.xx (系统调用篇) | 系统调用到底经历了什么 < csdn | 51cto | harmony > 本篇不详细说系统调用过程,只说跟信号相关的部分. OsArmA32SyscallHandle总体理解起来是被信号的保存和还原两个函数给包夹了.注意要在运行过程中去理解调用两个函数的过程,对于同一个任务来说,一定是先执行OsSaveSignalContext,第二次进入OsArmA32SyscallHandle后再执行OsRestorSignalContext. 看OsSaveSignalContext,由它负责保存user.source() 的上下文,其中改变了sp,r0/r1寄存器值,切到信号处理函数user.sighandle()运行. 在函数的开头,碰到系统调用号__NR_sigreturn,直接恢复信号上下文就退出了,因为这是要切回user.source()继续运行的操作. //用户进程信号处理函数完成后的系统调用 svc 119 #__NR_sigreturn if (cmd == __NR_sigreturn) { OsRestorSignalContext(regs);//恢复信号上下文,回到用户栈运行. return regs; } OsSaveSignalContext 保存信号上下文 有了上面的铺垫,就不难理解这个函数的作用. /********************************************** 产生系统调用时,也就是软中断时,保存用户栈寄存器现场信息 改写PC寄存器的值 **********************************************/ void OsSaveSignalContext(unsigned int *sp) { UINTPTR sigHandler; UINT32 intSave; LosTaskCB *task = NULL; LosProcessCB *process = NULL; sig_cb *sigcb = NULL; unsigned long cpsr; OS_RETURN_IF_VOID(sp == NULL); cpsr = OS_SYSCALL_GET_CPSR(sp);//获取系统调用时的 CPSR值 OS_RETURN_IF_VOID(((cpsr & CPSR_MASK_MODE) != CPSR_USER_MODE));//必须工作在CPU的用户模式下,注意CPSR_USER_MODE(cpu层面)和OS_USER_MODE(系统层面)是两码事. SCHEDULER_LOCK(intSave);//如有不明白前往 https://my.oschina.net/weharmony 翻看工作模式/信号分发/信号处理篇 task = OsCurrTaskGet(); process = OsCurrProcessGet(); sigcb = &task->sig;//获取任务的信号控制块 //1.未保存任务上下文任务 //2.任何的信号标签集不为空或者进程有信号要处理 if ((sigcb->context.count == 0) && ((sigcb->sigFlag != 0) || (process->sigShare != 0))) { sigHandler = OsGetSigHandler();//获取信号处理函数 if (sigHandler == 0) {//信号没有注册 sigcb->sigFlag = 0; process->sigShare = 0; SCHEDULER_UNLOCK(intSave); PRINT_ERR("The signal processing function for the current process pid =%d is NULL!\n", task->processID); return; } /* One pthread do the share signal */ sigcb->sigFlag |= process->sigShare;//扩展任务的信号标签集 unsigned int signo = (unsigned int)FindFirstSetedBit(sigcb->sigFlag) + 1; OsProcessExitCodeSignalSet(process, signo);//设置进程退出信号 sigcb->context.CPSR = cpsr; //保存状态寄存器 sigcb->context.PC = sp[REG_PC]; //获取被打断现场寄存器的值 sigcb->context.USP = sp[REG_SP];//用户栈顶位置,以便能从内核栈切回用户栈 sigcb->context.ULR = sp[REG_LR];//用户栈返回地址 sigcb->context.R0 = sp[REG_R0]; //系统调用的返回值 sigcb->context.R1 = sp[REG_R1]; sigcb->context.R2 = sp[REG_R2]; sigcb->context.R3 = sp[REG_R3]; sigcb->context.R7 = sp[REG_R7];//为何参数不用传R7,是因为系统调用发生时 R7始终保存的是系统调用号. sigcb->context.R12 = sp[REG_R12];//详见 https://my.oschina.net/weharmony/blog/4967613 sp[REG_PC] = sigHandler;//指定信号执行函数,注意此处改变保存任务上下文中PC寄存器的值,恢复上下文时将执行这个函数. sp[REG_R0] = signo; //参数1,信号ID sp[REG_R1] = (unsigned int)(UINTPTR)(sigcb->sigunbinfo.si_value.sival_ptr); //参数2 /* sig No bits 00000100 present sig No 3, but 1<< 3 = 00001000, so signo needs minus 1 */ sigcb->sigFlag ^= 1ULL << (signo - 1); sigcb->context.count++; //代表已保存 } SCHEDULER_UNLOCK(intSave); } 解读 先是判断执行条件,确实是有信号需要处理,有处理函数.自定义处理函数是由用户进程安装进来的,所有进程旗下的任务都共用,参数就是信号signo,注意可不是系统调用号,有区别的.信号编号长这样. #define SIGHUP 1 //终端挂起或者控制进程终止 #define SIGINT 2 //键盘中断(ctrl + c) #define SIGQUIT 3 //键盘的退出键被按下 #define SIGILL 4 //非法指令 #define SIGTRAP 5 //跟踪陷阱(trace trap),启动进程,跟踪代码的执行 #define SIGABRT 6 //由abort(3)发出的退出指令 #define SIGIOT SIGABRT //abort发出的信号 #define SIGBUS 7 //总线错误 #define SIGFPE 8 //浮点异常 #define SIGKILL 9 //常用的命令 kill 9 123 | 不能被忽略、处理和阻塞 系统调用号长这样,是不是看到一些很熟悉的函数. #define __NR_restart_syscall 0 #define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 #define __NR_write 4 #define __NR_open 5 #define __NR_close 6 #define __NR_waitpid 7 #define __NR_creat 8 #define __NR_link 9 #define __NR_unlink 10 #define __NR_execve 11 #define __NR_chdir 12 #define __NR_time 13 #define __NR_mknod 14 #define __NR_chmod 15 #define __NR_lchown 16 #define __NR_break 17 最后是最最最关键的代码,改变pc寄存器的值,此值一变,在_osExceptSwiHdl中恢复上下文后,cpu跳到用户空间的代码段 user.sighandle(R0,R1) 开始执行,即执行信号处理函数. sp[REG_PC] = sigHandler;//指定信号执行函数,注意此处改变保存任务上下文中PC寄存器的值,恢复上下文时将执行这个函数. sp[REG_R0] = signo; //参数1,信号ID sp[REG_R1] = (unsigned int)(UINTPTR)(sigcb->sigunbinfo.si_value.sival_ptr); //参数2 OsRestorSignalContext 恢复信号上下文 /**************************************************** 恢复信号上下文,由系统调用之__NR_sigreturn产生,这是一个内部产生的系统调用. 为什么要恢复呢? 因为系统调用的执行由任务内核态完成,使用的栈也是内核栈,CPU相关寄存器记录的都是内核栈的内容, 而系统调用完成后,需返回任务的用户栈执行,这时需将CPU各寄存器回到用户态现场 所以函数的功能就变成了还原寄存器的值 ****************************************************/ void OsRestorSignalContext(unsigned int *sp) { LosTaskCB *task = NULL; /* Do not adjust this statement */ LosProcessCB *process = NULL; sig_cb *sigcb = NULL; UINT32 intSave; SCHEDULER_LOCK(intSave); task = OsCurrTaskGet(); sigcb = &task->sig;//获取当前任务信号控制块 if (sigcb->context.count != 1) {//必须之前保存过,才能被恢复 SCHEDULER_UNLOCK(intSave); PRINT_ERR("sig error count : %d\n", sigcb->context.count); return; } process = OsCurrProcessGet();//获取当前进程 sp[REG_PC] = sigcb->context.PC;//指令寄存器 OS_SYSCALL_SET_CPSR(sp, sigcb->context.CPSR);//重置程序状态寄存器 sp[REG_SP] = sigcb->context.USP;//用户栈堆栈指针, USP指的是 用户态的堆栈,即将回到用户栈继续运行 sp[REG_LR] = sigcb->context.ULR;//返回用户栈代码执行位置 sp[REG_R0] = sigcb->context.R0; sp[REG_R1] = sigcb->context.R1; sp[REG_R2] = sigcb->context.R2; sp[REG_R3] = sigcb->context.R3; sp[REG_R7] = sigcb->context.R7; sp[REG_R12] = sigcb->context.R12; sigcb->context.count--; //信号上下文的数量回到减少 process->sigShare = 0; //回到用户态,信号共享清0 OsProcessExitCodeSignalClear(process);//清空进程退出码 SCHEDULER_UNLOCK(intSave); } 解读 在信号处理函数完成之后,内核会触发一个__NR_sigreturn的系统调用,又陷入内核态,回到了OsArmA32SyscallHandle. 恢复的过程很简单,把之前保存的信号上下文恢复到内核栈sp开始位置,数据在栈中的保存顺序可查看 用栈方式篇 ,最重要的看这几句. sp[REG_PC] = sigcb->context.PC;//指令寄存器 sp[REG_SP] = sigcb->context.USP;//用户栈堆栈指针, USP指的是 用户态的堆栈,即将回到用户栈继续运行 sp[REG_LR] = sigcb->context.ULR;//返回用户栈代码执行位置 注意这里还不是真正的切换上下文,只是改变内核栈中现有的数据.这些数据将还原给寄存器.USP和ULR指向的是用户栈的位置.一旦PC,USP,ULR从栈中弹出赋给寄存器.才真正完成了内核栈到用户栈的切换.回到了user.source()继续运行. 真正的切换汇编代码如下,都已添加注释,在保存和恢复上下文中夹着OsArmA32SyscallHandle @ Description: Software interrupt exception handler _osExceptSwiHdl: @软中断异常处理,注意此时已在内核栈运行 @保存任务上下文(TaskContext) 开始... 一定要对照TaskContext来理解 SUB SP, SP, #(4 * 16) @先申请16个栈空间单元用于处理本次软中断 STMIA SP, {R0-R12} @TaskContext.R[GEN_REGS_NUM] STMIA从左到右执行,先放R0 .. R12 MRS R3, SPSR @读取本模式下的SPSR值 MOV R4, LR @保存回跳寄存器LR AND R1, R3, #CPSR_MASK_MODE @ Interrupted mode 获取中断模式 CMP R1, #CPSR_USER_MODE @ User mode 是否为用户模式 BNE OsKernelSVCHandler @ Branch if not user mode 非用户模式下跳转 @ 当为用户模式时,获取SP和LR寄出去值 @ we enter from user mode, we need get the values of USER mode r13(sp) and r14(lr). @ stmia with ^ will return the user mode registers (provided that r15 is not in the register list). MOV R0, SP @获取SP值,R0将作为OsArmA32SyscallHandle的参数 STMFD SP!, {R3} @ Save the CPSR 入栈保存CPSR值 => TaskContext.regPSR ADD R3, SP, #(4 * 17) @ Offset to pc/cpsr storage 跳到PC/CPSR存储位置 STMFD R3!, {R4} @ Save the CPSR and r15(pc) 保存LR寄存器 => TaskContext.PC STMFD R3, {R13, R14}^ @ Save user mode r13(sp) and r14(lr) 从右向左 保存 => TaskContext.LR和SP SUB SP, SP, #4 @ => TaskContext.resved PUSH_FPU_REGS R1 @保存中断模式(用户模式) @保存任务上下文(TaskContext) 结束 MOV FP, #0 @ Init frame pointer CPSIE I @开中断,表明在系统调用期间可响应中断 BLX OsArmA32SyscallHandle /*交给C语言处理系统调用,参数为R0,指向TaskContext的开始位置*/ CPSID I @执行后续指令前必须先关中断 @恢复任务上下文(TaskContext) 开始 POP_FPU_REGS R1 @弹出FPU值给R1 ADD SP, SP,#4 @ 定位到保存旧SPSR值的位置 LDMFD SP!, {R3} @ Fetch the return SPSR 弹出旧SPSR值 MSR SPSR_cxsf, R3 @ Set the return mode SPSR 恢复该模式下的SPSR值 @ we are leaving to user mode, we need to restore the values of USER mode r13(sp) and r14(lr). @ ldmia with ^ will return the user mode registers (provided that r15 is not in the register list) LDMFD SP!, {R0-R12} @恢复R0-R12寄存器 LDMFD SP, {R13, R14}^ @ Restore user mode R13/R14 恢复用户模式的R13/R14寄存器 ADD SP, SP, #(2 * 4) @定位到保存旧PC值的位置 LDMFD SP!, {PC}^ @ Return to user 切回用户模式运行 @恢复任务上下文(TaskContext) 结束 具体也可看这两篇: v42.xx (中断切换篇) | 中断切换到底在切换什么? < csdn | 51cto | harmony > v41.xx (任务切换篇) | 汇编告诉任务到底在切换什么 < csdn | 51cto | harmony > 鸿蒙源码百篇博客 往期回顾 在给 鸿蒙内核源码加中文注释 过程中,整理出以下文章.内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感.百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思.更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了.:P 写文章比写代码累多了,越深入研究,越觉得没写好,所以文章和注解会反复修正, .xx代表修改的次数, 将持续完善源码注解和文档内容,精雕细琢,言简意赅, 尽全力打磨精品内容. v49.xx (信号消费篇) | 用户栈到内核栈的两次切换 < csdn | 51cto | harmony > v48.xx (信号生产篇) | 如何安装和发送信号? < csdn | 51cto | harmony > v47.xx (进程回收篇) | 进程在临终前如何向老祖宗托孤 < csdn | 51cto | harmony > v46.xx (特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 < csdn | 51cto | harmony > v45.xx (fork篇) | fork是如何做到调用一次,返回两次的 ? < csdn | 51cto | harmony > v44.xx (中断管理篇) | 硬中断的实现<>观察者模式 < csdn | 51cto | harmony > v43.xx (中断概念篇) | 外人眼中权势滔天的当红海公公 < csdn | 51cto | harmony > v42.xx (中断切换篇) | 中断切换到底在切换什么? < csdn | 51cto | harmony > v41.xx (任务切换篇) | 汇编告诉任务到底在切换什么 < csdn | 51cto | harmony > v40.xx (汇编汇总篇) | 所有的汇编代码都在这里 < csdn | 51cto | harmony > v39.xx (异常接管篇) | 社会很单纯,复杂的是人 < csdn | 51cto | harmony > v38.xx (寄存器篇) | arm所有寄存器一网打尽,不再神秘 < csdn | 51cto | harmony > v37.xx (系统调用篇) | 系统调用到底经历了什么 < csdn | 51cto | harmony > v36.xx (工作模式篇) | cpu是韦小宝,有哪七个老婆? < csdn | 51cto | harmony > v35.xx (时间管理篇) | tick是操作系统的基本时间单位 < csdn | 51cto | harmony > v34.xx (原子操作篇) | 是谁在为原子操作保驾护航? < csdn | 51cto | harmony > v33.xx (消息队列篇) | 进程间如何异步解耦传递大数据 ? < csdn | 51cto | harmony > v32.xx (cpu篇) | 整个内核就是一个死循环 < csdn | 51cto | harmony > v31.xx (定时器篇) | 内核最高优先级任务是谁? < csdn | 51cto | harmony > v30.xx (事件控制篇) | 任务间多对多的同步方案 < csdn | 51cto | harmony > v29.xx (信号量篇) | 信号量解决任务同步问题 < csdn | 51cto | harmony > v28.xx (进程通讯篇) | 进程间通讯有哪九大方式? < csdn | 51cto | harmony > v27.xx (互斥锁篇) | 互斥锁比自旋锁可丰满许多 < csdn | 51cto | harmony > v26.xx (自旋锁篇) | 真的好想为自旋锁立贞节牌坊! < csdn | 51cto | harmony > v25.xx (并发并行篇) | 怎么记住并发并行的区别? < csdn | 51cto | harmony > v24.xx (进程概念篇) | 进程在管理哪些资源? < csdn | 51cto | harmony > v23.xx (汇编传参篇) | 汇编如何传递复杂的参数? < csdn | 51cto | harmony > v22.xx (汇编基础篇) | cpu在哪里打卡上班? < csdn | 51cto | harmony > v21.xx (线程概念篇) | 是谁在不断的折腾cpu? < csdn | 51cto | harmony > v20.xx (用栈方式篇) | 栈是构建底层运行的基础 < csdn | 51cto | harmony > v19.xx (位图管理篇) | 为何进程和线程优先级都是32个? < csdn | 51cto | harmony > v18.xx (源码结构篇) | 梳理内核源文件的作用和含义 < csdn | 51cto | harmony > v17.xx (物理内存篇) | 这样记伙伴算法永远不会忘 < csdn | 51cto | harmony > v16.xx (内存规则篇) | 内存管理到底在管什么? < csdn | 51cto | harmony > v15.xx (内存映射篇) | 什么是内存最重要的实现基础 ? < csdn | 51cto | harmony > v14.xx (内存汇编篇) | 什么是虚拟内存的实现基础? < csdn | 51cto | harmony > v13.xx (源码注释篇) | 鸿蒙必须成功,也必然成功 < csdn | 51cto | harmony > v12.xx (内存管理篇) | 虚拟内存全景图是怎样的? < csdn | 51cto | harmony > v11.xx (内存分配篇) | 内存有哪些分配方式? < csdn | 51cto | harmony > v10.xx (内存主奴篇) | 紫禁城的主子和奴才如何相处? < csdn | 51cto | harmony > v09.xx (调度故事篇) | 用故事说内核调度过程 < csdn | 51cto | harmony > v08.xx (总目录) | 百万汉字注解 百篇博客分析 < csdn | 51cto | harmony > v07.xx (调度机制篇) | 任务是如何被调度执行的? < csdn | 51cto | harmony > v06.xx (调度队列篇) | 内核有多少个调度队列? < csdn | 51cto | harmony > v05.xx (任务管理篇) | 任务池是如何管理的? < csdn | 51cto | harmony > v04.xx (任务调度篇) | 任务是内核调度的单元 < csdn | 51cto | harmony > v03.xx (时钟任务篇) | 调度最大的源动力来自哪里? < csdn | 51cto | harmony > v02.xx (进程管理篇) | 进程是内核资源管理单元 < csdn | 51cto | harmony > v01.xx (双向链表篇) | 谁是内核最重要结构体? < csdn | 51cto | harmony > 进入 >> oschina | csdn | 51cto | 掘金 | harmony 参与贡献 Fork 本仓库 >> 新建 Feat_xxx 分支 >> 提交代码注解 >> 新建 Pull Request 新建 Issue 喜欢请「点赞+关注+收藏」 各大站点搜 「鸿蒙内核源码分析」.欢迎转载,请注明出处.

优秀的个人博客,低调大师

鸿蒙内核源码分析(信号消费篇) | 用户栈到内核栈的两次切换 | 百篇博客分析HarmonyOS源码 | v49.03

百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< oschina | 51cto | csdn | harmony > 信号消费 本篇为信号消费篇,读之前建议先阅读信号生产篇,信号部分姊妹篇如下: v48.xx (信号生产篇) | 如何安装和发送信号? < csdn | 51cto | harmony > v49.xx (信号消费篇) | 用户栈到内核栈的两次切换 < csdn | 51cto | harmony > 本篇有相当的难度,涉及用户栈和内核栈的两次切换,寄存器改值,将围绕下图来说明. 解读 为本篇理解方便,把图做简化标签说明: user:用户空间 kernel:内核空间 source(...):源函数 sighandle(...):信号处理函数, syscall(...):系统调用,参数为系统调用号,如sigreturn,N(表任意) user.source():表示在用户空间运行的源函数 系列篇已多次说过,用户态的任务有两个运行栈,一个是用户栈,一个是内核栈.栈空间分别来自用户空间和内核空间.两种空间是有严格的地址划分的,通过虚拟地址的大小就能判断出是用户空间还是内核空间.系统调用本质上是软中断,它使CPU执行指令的场地由用户栈变成内核栈.怎么变的并不复杂,就是改变(sp和cpsr寄存器的值).sp指向哪个栈就代表在哪个栈运行, 当cpu在用户栈运行时是不能访问内核空间的,但内核态任务可以访问整个空间,而且内核态任务没有用户栈. 理解了上面的说明,再来说下正常系统调用流程是这样的: user.source() -> kernel.syscall(N) - > user.source() ,想要回到user.source()继续运行,就必须保存用户栈现场各寄存器的值.这些值保存在内核栈中,恢复也是从内核栈恢复. 信号消费的过程的上图可简化表示为: user.source() -> kernel.syscall(N) ->user.sighandle() ->kernel.syscall(sigreturn) -> user.source() 在原本要回到user.source()的中间插入了信号处理函数的调用. 这正是本篇要通过代码来说清楚的核心问题. 顺着这个思路可以推到以下几点,实际也是这么做的: kernel.syscall(N) 中必须要再次保存user.source()的上下文sig_switch_context,为何已经保存了一次还要再保存一次? 因为第一次是保存在内核栈中,而内核栈这部分数据会因回到用户态user.sighandle()运行而被恢复现场出栈了.保存现场/恢复现场是成双出队的好基友,注意有些文章说会把整个内核栈清空,这是不对的. 第二次保存在任务结构体中,任务来源于任务池,是内核全局变量,常驻内存的.两次保存的都是user.source()运行时现场信息,再回顾下相关的结构体.关键是sig_switch_context typedef struct { // ... sig_cb sig;//信号控制块,用于异步通信 } LosTaskCB; typedef struct {//信号控制块(描述符) sigset_t sigFlag; //不屏蔽的信号集 sigset_t sigPendFlag; //信号阻塞标签集,记录那些信号来过,任务依然阻塞的集合.即:这些信号不能唤醒任务 sigset_t sigprocmask; /* Signals that are blocked */ //任务屏蔽了哪些信号 sq_queue_t sigactionq; //信号捕捉队列 LOS_DL_LIST waitList; //等待链表,上面挂的是等待信号到来的任务, 请查找 OsTaskWait(&sigcb->waitList, timeout, TRUE) 理解 sigset_t sigwaitmask; /* Waiting for pending signals */ //任务在等待哪些信号的到来 siginfo_t sigunbinfo; /* Signal info when task unblocked */ //任务解锁时的信号信息 sig_switch_context context; //信号切换上下文, 用于保存切换现场, 比如发生系统调用时的返回,涉及同一个任务的两个栈进行切换 } sig_cb; 还必须要改变原有PC/R0/R1寄存器的值.想要执行user.sighandle(),PC寄存器就必须指向它,而R0,R1就是它的参数. 信号处理完成后须回到内核态,怎么再次陷入内核态? 答案是:__NR_sigreturn,这也是个系统调用.回来后还原sig_switch_context,即还原user.source()被打断时SP/PC等寄存器的值,使其跳回到用户栈从user.source()的被打断处继续执行. 有了这三个推论,再理解下面的代码就是吹灰之力了,涉及三个关键函数 OsArmA32SyscallHandle,OsSaveSignalContext,OsRestorSignalContext本篇一一解读,彻底挖透.先看信号上下文结构体sig_switch_context. sig_switch_context //任务中断上下文 #define TASK_IRQ_CONTEXT \ unsigned int R0; \ unsigned int R1; \ unsigned int R2; \ unsigned int R3; \ unsigned int R12; \ unsigned int USP; \ unsigned int ULR; \ unsigned int CPSR; \ unsigned int PC; typedef struct {//信号切换上下文 TASK_IRQ_CONTEXT unsigned int R7; //存放系统调用的ID unsigned int count; //记录是否保存了信号上下文 } sig_switch_context; 保存user.source()现场的结构体,USP,ULR代表用户栈指针和返回地址. CPSR寄存器用于设置CPU的工作模式,CPU有7种工作模式,具体可前往翻看 v36.xx (工作模式篇) | cpu是韦小宝,有哪七个老婆? < csdn | 51cto | harmony > 谈论的用户态(usr普通用户)和内核态(sys超级用户)对应的只是其中的两种.二者都共用相同的寄存器.还原它就是告诉CPU内核已切到普通用户模式运行. 其他寄存器没有保存的原因是系统调用不会用到它们,所以不需要保存. R7是在系统调用发生时用于记录系统调用号,在信号处理过程中,R0将获得信号编号,作为user.sighandle()的第一个参数. count记录是否保存了信号上下文 OsArmA32SyscallHandle 系统调用总入口 /* The SYSCALL ID is in R7 on entry. Parameters follow in R0..R6 */ /****************************************************************** 由汇编调用,见于 los_hw_exc.s / BLX OsArmA32SyscallHandle SYSCALL是产生系统调用时触发的信号,R7寄存器存放具体的系统调用ID,也叫系统调用号 regs:参数就是所有寄存器 注意:本函数在用户态和内核态下都可能被调用到 //MOV R0, SP @获取SP值,R0将作为OsArmA32SyscallHandle的参数 ******************************************************************/ LITE_OS_SEC_TEXT UINT32 *OsArmA32SyscallHandle(UINT32 *regs) { UINT32 ret; UINT8 nArgs; UINTPTR handle; UINT32 cmd = regs[REG_R7];//C7寄存器记录了触发了具体哪个系统调用 if (cmd >= SYS_CALL_NUM) {//系统调用的总数 PRINT_ERR("Syscall ID: error %d !!!\n", cmd); return regs; } //用户进程信号处理函数完成后的系统调用 svc 119 #__NR_sigreturn if (cmd == __NR_sigreturn) { OsRestorSignalContext(regs);//恢复信号上下文,回到用户栈运行. return regs; } handle = g_syscallHandle[cmd];//拿到系统调用的注册函数,类似 SysRead nArgs = g_syscallNArgs[cmd / NARG_PER_BYTE]; /* 4bit per nargs */ nArgs = (cmd & 1) ? (nArgs >> NARG_BITS) : (nArgs & NARG_MASK);//获取参数个数 if ((handle == 0) || (nArgs > ARG_NUM_7)) {//系统调用必须有参数且参数不能大于8个 PRINT_ERR("Unsupport syscall ID: %d nArgs: %d\n", cmd, nArgs); regs[REG_R0] = -ENOSYS; return regs; } //regs[0-6] 记录系统调用的参数,这也是由R7寄存器保存系统调用号的原因 switch (nArgs) {//参数的个数 case ARG_NUM_0: case ARG_NUM_1: ret = (*(SyscallFun1)handle)(regs[REG_R0]);//执行系统调用,类似 SysUnlink(pathname); break; case ARG_NUM_2://如何是两个参数的系统调用,这里传三个参数也没有问题,因被调用函数不会去取用R2值 case ARG_NUM_3: ret = (*(SyscallFun3)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2]);//类似 SysExecve(fileName, argv, envp); break; case ARG_NUM_4: case ARG_NUM_5: ret = (*(SyscallFun5)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3], regs[REG_R4]); break; default: //7个参数的情况 ret = (*(SyscallFun7)handle)(regs[REG_R0], regs[REG_R1], regs[REG_R2], regs[REG_R3], regs[REG_R4], regs[REG_R5], regs[REG_R6]); } regs[REG_R0] = ret;//R0保存系统调用返回值 OsSaveSignalContext(regs);//如果有信号要处理,将改写pc,r0,r1寄存器,改变返回正常用户态路径,而先去执行信号处理程序. /* Return the last value of curent_regs. This supports context switches on return from the exception. * That capability is only used with the SYS_context_switch system call. */ return regs;//返回寄存器的值 } 解读 这是系统调用的总入口,所有的系统调用都要跑这里要统一处理.通过系统号(保存在R7),找到注册函数并回调.完成系统调用过程. 关于系统调用可查看 v37.xx (系统调用篇) | 系统调用到底经历了什么 < csdn | 51cto | harmony > 本篇不详细说系统调用过程,只说跟信号相关的部分. OsArmA32SyscallHandle总体理解起来是被信号的保存和还原两个函数给包夹了.注意要在运行过程中去理解调用两个函数的过程,对于同一个任务来说,一定是先执行OsSaveSignalContext,第二次进入OsArmA32SyscallHandle后再执行OsRestorSignalContext. 看OsSaveSignalContext,由它负责保存user.source() 的上下文,其中改变了sp,r0/r1寄存器值,切到信号处理函数user.sighandle()运行. 在函数的开头,碰到系统调用号__NR_sigreturn,直接恢复信号上下文就退出了,因为这是要切回user.source()继续运行的操作. //用户进程信号处理函数完成后的系统调用 svc 119 #__NR_sigreturn if (cmd == __NR_sigreturn) { OsRestorSignalContext(regs);//恢复信号上下文,回到用户栈运行. return regs; } OsSaveSignalContext 保存信号上下文 有了上面的铺垫,就不难理解这个函数的作用. /********************************************** 产生系统调用时,也就是软中断时,保存用户栈寄存器现场信息 改写PC寄存器的值 **********************************************/ void OsSaveSignalContext(unsigned int *sp) { UINTPTR sigHandler; UINT32 intSave; LosTaskCB *task = NULL; LosProcessCB *process = NULL; sig_cb *sigcb = NULL; unsigned long cpsr; OS_RETURN_IF_VOID(sp == NULL); cpsr = OS_SYSCALL_GET_CPSR(sp);//获取系统调用时的 CPSR值 OS_RETURN_IF_VOID(((cpsr & CPSR_MASK_MODE) != CPSR_USER_MODE));//必须工作在CPU的用户模式下,注意CPSR_USER_MODE(cpu层面)和OS_USER_MODE(系统层面)是两码事. SCHEDULER_LOCK(intSave);//如有不明白前往 https://my.oschina.net/weharmony 翻看工作模式/信号分发/信号处理篇 task = OsCurrTaskGet(); process = OsCurrProcessGet(); sigcb = &task->sig;//获取任务的信号控制块 //1.未保存任务上下文任务 //2.任何的信号标签集不为空或者进程有信号要处理 if ((sigcb->context.count == 0) && ((sigcb->sigFlag != 0) || (process->sigShare != 0))) { sigHandler = OsGetSigHandler();//获取信号处理函数 if (sigHandler == 0) {//信号没有注册 sigcb->sigFlag = 0; process->sigShare = 0; SCHEDULER_UNLOCK(intSave); PRINT_ERR("The signal processing function for the current process pid =%d is NULL!\n", task->processID); return; } /* One pthread do the share signal */ sigcb->sigFlag |= process->sigShare;//扩展任务的信号标签集 unsigned int signo = (unsigned int)FindFirstSetedBit(sigcb->sigFlag) + 1; OsProcessExitCodeSignalSet(process, signo);//设置进程退出信号 sigcb->context.CPSR = cpsr; //保存状态寄存器 sigcb->context.PC = sp[REG_PC]; //获取被打断现场寄存器的值 sigcb->context.USP = sp[REG_SP];//用户栈顶位置,以便能从内核栈切回用户栈 sigcb->context.ULR = sp[REG_LR];//用户栈返回地址 sigcb->context.R0 = sp[REG_R0]; //系统调用的返回值 sigcb->context.R1 = sp[REG_R1]; sigcb->context.R2 = sp[REG_R2]; sigcb->context.R3 = sp[REG_R3]; sigcb->context.R7 = sp[REG_R7];//为何参数不用传R7,是因为系统调用发生时 R7始终保存的是系统调用号. sigcb->context.R12 = sp[REG_R12];//详见 https://my.oschina.net/weharmony/blog/4967613 sp[REG_PC] = sigHandler;//指定信号执行函数,注意此处改变保存任务上下文中PC寄存器的值,恢复上下文时将执行这个函数. sp[REG_R0] = signo; //参数1,信号ID sp[REG_R1] = (unsigned int)(UINTPTR)(sigcb->sigunbinfo.si_value.sival_ptr); //参数2 /* sig No bits 00000100 present sig No 3, but 1<< 3 = 00001000, so signo needs minus 1 */ sigcb->sigFlag ^= 1ULL << (signo - 1); sigcb->context.count++; //代表已保存 } SCHEDULER_UNLOCK(intSave); } 解读 先是判断执行条件,确实是有信号需要处理,有处理函数.自定义处理函数是由用户进程安装进来的,所有进程旗下的任务都共用,参数就是信号signo,注意可不是系统调用号,有区别的.信号编号长这样. #define SIGHUP 1 //终端挂起或者控制进程终止 #define SIGINT 2 //键盘中断(ctrl + c) #define SIGQUIT 3 //键盘的退出键被按下 #define SIGILL 4 //非法指令 #define SIGTRAP 5 //跟踪陷阱(trace trap),启动进程,跟踪代码的执行 #define SIGABRT 6 //由abort(3)发出的退出指令 #define SIGIOT SIGABRT //abort发出的信号 #define SIGBUS 7 //总线错误 #define SIGFPE 8 //浮点异常 #define SIGKILL 9 //常用的命令 kill 9 123 | 不能被忽略、处理和阻塞 系统调用号长这样,是不是看到一些很熟悉的函数. #define __NR_restart_syscall 0 #define __NR_exit 1 #define __NR_fork 2 #define __NR_read 3 #define __NR_write 4 #define __NR_open 5 #define __NR_close 6 #define __NR_waitpid 7 #define __NR_creat 8 #define __NR_link 9 #define __NR_unlink 10 #define __NR_execve 11 #define __NR_chdir 12 #define __NR_time 13 #define __NR_mknod 14 #define __NR_chmod 15 #define __NR_lchown 16 #define __NR_break 17 最后是最最最关键的代码,改变pc寄存器的值,此值一变,在_osExceptSwiHdl中恢复上下文后,cpu跳到用户空间的代码段 user.sighandle(R0,R1) 开始执行,即执行信号处理函数. sp[REG_PC] = sigHandler;//指定信号执行函数,注意此处改变保存任务上下文中PC寄存器的值,恢复上下文时将执行这个函数. sp[REG_R0] = signo; //参数1,信号ID sp[REG_R1] = (unsigned int)(UINTPTR)(sigcb->sigunbinfo.si_value.sival_ptr); //参数2 OsRestorSignalContext 恢复信号上下文 /**************************************************** 恢复信号上下文,由系统调用之__NR_sigreturn产生,这是一个内部产生的系统调用. 为什么要恢复呢? 因为系统调用的执行由任务内核态完成,使用的栈也是内核栈,CPU相关寄存器记录的都是内核栈的内容, 而系统调用完成后,需返回任务的用户栈执行,这时需将CPU各寄存器回到用户态现场 所以函数的功能就变成了还原寄存器的值 ****************************************************/ void OsRestorSignalContext(unsigned int *sp) { LosTaskCB *task = NULL; /* Do not adjust this statement */ LosProcessCB *process = NULL; sig_cb *sigcb = NULL; UINT32 intSave; SCHEDULER_LOCK(intSave); task = OsCurrTaskGet(); sigcb = &task->sig;//获取当前任务信号控制块 if (sigcb->context.count != 1) {//必须之前保存过,才能被恢复 SCHEDULER_UNLOCK(intSave); PRINT_ERR("sig error count : %d\n", sigcb->context.count); return; } process = OsCurrProcessGet();//获取当前进程 sp[REG_PC] = sigcb->context.PC;//指令寄存器 OS_SYSCALL_SET_CPSR(sp, sigcb->context.CPSR);//重置程序状态寄存器 sp[REG_SP] = sigcb->context.USP;//用户栈堆栈指针, USP指的是 用户态的堆栈,即将回到用户栈继续运行 sp[REG_LR] = sigcb->context.ULR;//返回用户栈代码执行位置 sp[REG_R0] = sigcb->context.R0; sp[REG_R1] = sigcb->context.R1; sp[REG_R2] = sigcb->context.R2; sp[REG_R3] = sigcb->context.R3; sp[REG_R7] = sigcb->context.R7; sp[REG_R12] = sigcb->context.R12; sigcb->context.count--; //信号上下文的数量回到减少 process->sigShare = 0; //回到用户态,信号共享清0 OsProcessExitCodeSignalClear(process);//清空进程退出码 SCHEDULER_UNLOCK(intSave); } 解读 在信号处理函数完成之后,内核会触发一个__NR_sigreturn的系统调用,又陷入内核态,回到了OsArmA32SyscallHandle. 恢复的过程很简单,把之前保存的信号上下文恢复到内核栈sp开始位置,数据在栈中的保存顺序可查看 用栈方式篇 ,最重要的看这几句. sp[REG_PC] = sigcb->context.PC;//指令寄存器 sp[REG_SP] = sigcb->context.USP;//用户栈堆栈指针, USP指的是 用户态的堆栈,即将回到用户栈继续运行 sp[REG_LR] = sigcb->context.ULR;//返回用户栈代码执行位置 注意这里还不是真正的切换上下文,只是改变内核栈中现有的数据.这些数据将还原给寄存器.USP和ULR指向的是用户栈的位置.一旦PC,USP,ULR从栈中弹出赋给寄存器.才真正完成了内核栈到用户栈的切换.回到了user.source()继续运行. 真正的切换汇编代码如下,都已添加注释,在保存和恢复上下文中夹着OsArmA32SyscallHandle @ Description: Software interrupt exception handler _osExceptSwiHdl: @软中断异常处理,注意此时已在内核栈运行 @保存任务上下文(TaskContext) 开始... 一定要对照TaskContext来理解 SUB SP, SP, #(4 * 16) @先申请16个栈空间单元用于处理本次软中断 STMIA SP, {R0-R12} @TaskContext.R[GEN_REGS_NUM] STMIA从左到右执行,先放R0 .. R12 MRS R3, SPSR @读取本模式下的SPSR值 MOV R4, LR @保存回跳寄存器LR AND R1, R3, #CPSR_MASK_MODE @ Interrupted mode 获取中断模式 CMP R1, #CPSR_USER_MODE @ User mode 是否为用户模式 BNE OsKernelSVCHandler @ Branch if not user mode 非用户模式下跳转 @ 当为用户模式时,获取SP和LR寄出去值 @ we enter from user mode, we need get the values of USER mode r13(sp) and r14(lr). @ stmia with ^ will return the user mode registers (provided that r15 is not in the register list). MOV R0, SP @获取SP值,R0将作为OsArmA32SyscallHandle的参数 STMFD SP!, {R3} @ Save the CPSR 入栈保存CPSR值 => TaskContext.regPSR ADD R3, SP, #(4 * 17) @ Offset to pc/cpsr storage 跳到PC/CPSR存储位置 STMFD R3!, {R4} @ Save the CPSR and r15(pc) 保存LR寄存器 => TaskContext.PC STMFD R3, {R13, R14}^ @ Save user mode r13(sp) and r14(lr) 从右向左 保存 => TaskContext.LR和SP SUB SP, SP, #4 @ => TaskContext.resved PUSH_FPU_REGS R1 @保存中断模式(用户模式) @保存任务上下文(TaskContext) 结束 MOV FP, #0 @ Init frame pointer CPSIE I @开中断,表明在系统调用期间可响应中断 BLX OsArmA32SyscallHandle /*交给C语言处理系统调用,参数为R0,指向TaskContext的开始位置*/ CPSID I @执行后续指令前必须先关中断 @恢复任务上下文(TaskContext) 开始 POP_FPU_REGS R1 @弹出FPU值给R1 ADD SP, SP,#4 @ 定位到保存旧SPSR值的位置 LDMFD SP!, {R3} @ Fetch the return SPSR 弹出旧SPSR值 MSR SPSR_cxsf, R3 @ Set the return mode SPSR 恢复该模式下的SPSR值 @ we are leaving to user mode, we need to restore the values of USER mode r13(sp) and r14(lr). @ ldmia with ^ will return the user mode registers (provided that r15 is not in the register list) LDMFD SP!, {R0-R12} @恢复R0-R12寄存器 LDMFD SP, {R13, R14}^ @ Restore user mode R13/R14 恢复用户模式的R13/R14寄存器 ADD SP, SP, #(2 * 4) @定位到保存旧PC值的位置 LDMFD SP!, {PC}^ @ Return to user 切回用户模式运行 @恢复任务上下文(TaskContext) 结束 具体也可看这两篇: v42.xx (中断切换篇) | 中断切换到底在切换什么? < csdn | 51cto | harmony > v41.xx (任务切换篇) | 汇编告诉任务到底在切换什么 < csdn | 51cto | harmony > 鸿蒙源码百篇博客 往期回顾 在给 鸿蒙内核源码加中文注释 过程中,整理出以下文章.内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感.百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思.更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了.:P 写文章比写代码累多了,越深入研究,越觉得没写好,所以文章和注解会反复修正, .xx代表修改的次数, 将持续完善源码注解和文档内容,精雕细琢,言简意赅, 尽全力打磨精品内容. v49.xx (信号消费篇) | 用户栈到内核栈的两次切换 < csdn | 51cto | harmony > v48.xx (信号生产篇) | 如何安装和发送信号? < csdn | 51cto | harmony > v47.xx (进程回收篇) | 进程在临终前如何向老祖宗托孤 < csdn | 51cto | harmony > v46.xx (特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 < csdn | 51cto | harmony > v45.xx (fork篇) | fork是如何做到调用一次,返回两次的 ? < csdn | 51cto | harmony > v44.xx (中断管理篇) | 硬中断的实现<>观察者模式 < csdn | 51cto | harmony > v43.xx (中断概念篇) | 外人眼中权势滔天的当红海公公 < csdn | 51cto | harmony > v42.xx (中断切换篇) | 中断切换到底在切换什么? < csdn | 51cto | harmony > v41.xx (任务切换篇) | 汇编告诉任务到底在切换什么 < csdn | 51cto | harmony > v40.xx (汇编汇总篇) | 所有的汇编代码都在这里 < csdn | 51cto | harmony > v39.xx (异常接管篇) | 社会很单纯,复杂的是人 < csdn | 51cto | harmony > v38.xx (寄存器篇) | arm所有寄存器一网打尽,不再神秘 < csdn | 51cto | harmony > v37.xx (系统调用篇) | 系统调用到底经历了什么 < csdn | 51cto | harmony > v36.xx (工作模式篇) | cpu是韦小宝,有哪七个老婆? < csdn | 51cto | harmony > v35.xx (时间管理篇) | tick是操作系统的基本时间单位 < csdn | 51cto | harmony > v34.xx (原子操作篇) | 是谁在为原子操作保驾护航? < csdn | 51cto | harmony > v33.xx (消息队列篇) | 进程间如何异步解耦传递大数据 ? < csdn | 51cto | harmony > v32.xx (cpu篇) | 整个内核就是一个死循环 < csdn | 51cto | harmony > v31.xx (定时器篇) | 内核最高优先级任务是谁? < csdn | 51cto | harmony > v30.xx (事件控制篇) | 任务间多对多的同步方案 < csdn | 51cto | harmony > v29.xx (信号量篇) | 信号量解决任务同步问题 < csdn | 51cto | harmony > v28.xx (进程通讯篇) | 九种进程间通讯方式速揽 < csdn | 51cto | harmony > v27.xx (互斥锁篇) | 比自旋锁丰满许多的互斥锁 < csdn | 51cto | harmony > v26.xx (自旋锁篇) | 自旋锁当立贞节牌坊! < csdn | 51cto | harmony > v25.xx (并发并行篇) | 怎么记住并发并行的区别? < csdn | 51cto | harmony > v24.xx (进程概念篇) | 进程在管理哪些资源? < csdn | 51cto | harmony > v23.xx (汇编传参篇) | 汇编如何传递复杂的参数? < csdn | 51cto | harmony > v22.xx (汇编基础篇) | cpu在哪里打卡上班? < csdn | 51cto | harmony > v21.xx (线程概念篇) | 是谁在不断的折腾cpu? < csdn | 51cto | harmony > v20.xx (用栈方式篇) | 栈是构建底层运行的基础 < csdn | 51cto | harmony > v19.xx (位图管理篇) | 为何进程和线程优先级都是32个? < csdn | 51cto | harmony > v18.xx (源码结构篇) | 梳理内核源文件的作用和含义 < csdn | 51cto | harmony > v17.xx (物理内存篇) | 这样记伙伴算法永远不会忘 < csdn | 51cto | harmony > v16.xx (内存规则篇) | 内存管理到底在管什么? < csdn | 51cto | harmony > v15.xx (内存映射篇) | 什么是内存最重要的实现基础 ? < csdn | 51cto | harmony > v14.xx (内存汇编篇) | 什么是虚拟内存的实现基础? < csdn | 51cto | harmony > v13.xx (源码注释篇) | 鸿蒙必须成功,也必然成功 < csdn | 51cto | harmony > v12.xx (内存管理篇) | 虚拟内存全景图是怎样的? < csdn | 51cto | harmony > v11.xx (内存分配篇) | 内存有哪些分配方式? < csdn | 51cto | harmony > v10.xx (内存主奴篇) | 紫禁城的主子和奴才如何相处? < csdn | 51cto | harmony > v09.xx (调度故事篇) | 用故事说内核调度过程 < csdn | 51cto | harmony > v08.xx (总目录) | 百万汉字注解 百篇博客分析 < csdn | 51cto | harmony > v07.xx (调度机制篇) | 任务是如何被调度执行的? < csdn | 51cto | harmony > v06.xx (调度队列篇) | 内核有多少个调度队列? < csdn | 51cto | harmony > v05.xx (任务管理篇) | 任务池是如何管理的? < csdn | 51cto | harmony > v04.xx (任务调度篇) | 任务是内核调度的单元 < csdn | 51cto | harmony > v03.xx (时钟任务篇) | 调度最大的源动力来自哪里? < csdn | 51cto | harmony > v02.xx (进程管理篇) | 进程是内核资源管理单元 < csdn | 51cto | harmony > v01.xx (双向链表篇) | 谁是内核最重要结构体? < csdn | 51cto | harmony > 进入 >> oschina | csdn | 51cto | 掘金 | harmony 参与贡献 Fork 本仓库 >> 新建 Feat_xxx 分支 >> 提交代码注解 >> 新建 Pull Request 新建 Issue 喜欢请「点赞+关注+收藏」 各大站点搜 「鸿蒙内核源码分析」.欢迎转载,请注明出处.

优秀的个人博客,低调大师

| 百篇博客分析HarmonyOS源码 | v47.01

百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< oschina | 51cto | csdn | harmony > 进程关系链 进程是家族式管理的,父子关系,兄弟关系,朋友关系,子女关系,甚至陌生人关系(等待你消亡)在一个进程的生命周期中都会记录下来.用什么来记录呢?当然是内核最重要的胶水结构体LOS_DL_LIST,进程控制块(以下简称PCB)用了8个双向链表来记录进程家族的基因关系和运行时关系.如下: typedef struct ProcessCB { //...此处省略其他变量 LOS_DL_LIST pendList; /**< Block list to which the process belongs */ //进程所属的阻塞列表,如果因拿锁失败,就由此节点挂到等锁链表上 LOS_DL_LIST childrenList; /**< my children process list */ //孩子进程都挂到这里,形成双循环链表 LOS_DL_LIST exitChildList; /**< my exit children process list */ //那些要退出孩子进程挂到这里,白发人送黑发人。 LOS_DL_LIST siblingList; /**< linkage in my parent's children list */ //兄弟进程链表, 56个民族是一家,来自同一个父进程. LOS_DL_LIST subordinateGroupList; /**< linkage in my group list */ //进程是组长时,有哪些组员进程 LOS_DL_LIST threadSiblingList; /**< List of threads under this process *///进程的线程(任务)列表 LOS_DL_LIST threadPriQueueList[OS_PRIORITY_QUEUE_NUM]; /**< The process's thread group schedules thepriority hash table */ //进程的线程组调度优先级哈希表 LOS_DL_LIST waitList; /**< The process holds the waitLits to support wait/waitpid *///进程持有等待链表以支持wait/waitpid } LosProcessCB; 解读 pendList 个人认为它是鸿蒙内核功能最多的一个链表,它远不止字面意思阻塞链表这么简单,只有深入解读源码后才能体会它真的是太会来事了,一般把它理解为阻塞链表就行.上面挂的是处于阻塞状态的进程. childrenList孩子链表,所有由它fork出来的进程都挂到这个链表上.上面的孩子进程在死亡前会将自己从上面摘出去,转而挂到exitChildList链表上. exitChildList退出孩子链表,进入死亡程序的进程要挂到这个链表上,一个进程的死亡是件挺麻烦的事,进程池的数量有限,需要及时回收进程资源,但家族管理关系复杂,要去很多地方消除痕迹.尤其还有其他进程在看你笑话,等你死亡(wait/waitpid)了通知它们一声. siblingList兄弟链表,和你同一个父亲的进程都挂到了这个链表上. subordinateGroupList 朋友圈链表,里面是因为兴趣爱好(进程组)而挂在一起的进程,它们可以不是一个父亲,不是一个祖父,但一定是同一个老祖宗(用户态和内核态根进程). threadSiblingList线程链表,上面挂的是进程ID都是这个进程的线程(任务),进程和线程的关系是1:N的关系,一个线程只能属于一个进程.这里要注意任务在其生命周期中是不能改所属进程的. threadPriQueueList线程的调度队列数组,一共32个,任务和进程一样有32个优先级,调度算法的过程是先找到优先级最高的进程,在从该进程的任务队列里去最高的优先级任务运行. waitList 是等待子进程消亡的任务链表,注意上面挂的是任务.任务是通过系统调用 pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options); 将任务挂到waitList上.鸿蒙waitpid系统调用为SysWait,稍后会讲. 进程正常死亡过程 一个进程的自然消亡过程如下 //一个进程的自然消亡过程,参数是当前运行的任务 STATIC VOID OsProcessNaturalExit(LosTaskCB *runTask, UINT32 status) { LosProcessCB *processCB = OS_PCB_FROM_PID(runTask->processID);//通过task找到所属PCB LosProcessCB *parentCB = NULL; LOS_ASSERT(!(processCB->threadScheduleMap != 0));//断言没有任务需要调度了,当前task是最后一个了 LOS_ASSERT(processCB->processStatus & OS_PROCESS_STATUS_RUNNING);//断言必须为正在运行的进程 OsChildProcessResourcesFree(processCB);//释放孩子进程的资源 #ifdef LOSCFG_KERNEL_CPUP OsCpupClean(processCB->processID); #endif /* is a child process */ if (processCB->parentProcessID != OS_INVALID_VALUE) {//判断是否有父进程 parentCB = OS_PCB_FROM_PID(processCB->parentProcessID);//获取父进程实体 LOS_ListDelete(&processCB->siblingList);//将自己从兄弟链表中摘除,家人们,永别了! if (!OsProcessExitCodeSignalIsSet(processCB)) {//是否设置了退出码? OsProcessExitCodeSet(processCB, status);//将进程状态设为退出码 } LOS_ListTailInsert(&parentCB->exitChildList, &processCB->siblingList);//挂到父进程的孩子消亡链表,家人中,永别的可不止我一个. LOS_ListDelete(&processCB->subordinateGroupList);//和志同道合的朋友们永别了,注意家里可不一定是朋友的,所有各有链表. LOS_ListTailInsert(&processCB->group->exitProcessList, &processCB->subordinateGroupList);//挂到进程组消亡链表,朋友中,永别的可不止我一个. OsWaitCheckAndWakeParentProcess(parentCB, processCB);//检查父进程的等待任务并唤醒任务,此处将会切换到其他任务运行. OsDealAliveChildProcess(processCB);//老父亲临终向各自的祖宗托孤 processCB->processStatus |= OS_PROCESS_STATUS_ZOMBIES;//贴上僵死进程的标签 (VOID)OsKill(processCB->parentProcessID, SIGCHLD, OS_KERNEL_KILL_PERMISSION);//以内核权限发送SIGCHLD(子进程退出)信号. LOS_ListHeadInsert(&g_processRecyleList, &processCB->pendList);//将进程通过其阻塞节点挂入全局进程回收链表 OsRunTaskToDelete(runTask);//删除正在运行的任务 return; } LOS_Panic("pid : %u is the root process exit!\n", processCB->processID); return; } 解读 退群,向兄弟姐妹siblingList告别,向朋友圈(进程组)告别subordinateGroupList. 留下你的死亡记录,老父亲记录到exitChildList,朋友圈记录到exitProcessList中. 告诉后人死亡原因OsProcessExitCodeSet,因为waitList上挂的任务在等待你的死亡信息. 向老祖宗托孤,用户态和内核态进程都有自己的祖宗进程(1和2号进程),老祖宗身子硬朗,最后死.所有的短命鬼进程都可以把自己的孩子委托给老祖宗照顾,老祖宗会一视同仁. 将自己变成了OS_PROCESS_STATUS_ZOMBIES僵尸进程. 老父亲跑到村口广播这个孩子已经死亡的信号OsKill. 将自己挂入进程回收链表,等待回收任务ResourcesTask回收资源. 最后删除这个正在运行的任务,很明显其中一定会发生一次调度OsSchedResched. //删除一个正在运行的任务 LITE_OS_SEC_TEXT VOID OsRunTaskToDelete(LosTaskCB *taskCB) { LosProcessCB *processCB = OS_PCB_FROM_PID(taskCB->processID);//拿到task所属进程 OsTaskReleaseHoldLock(processCB, taskCB);//task还锁 OsTaskStatusUnusedSet(taskCB);//task重置为未使用状态,等待回收 LOS_ListDelete(&taskCB->threadList);//从进程的线程链表中将自己摘除 processCB->threadNumber--;//进程的活动task --,注意进程还有一个记录总task的变量 processCB->threadCount LOS_ListTailInsert(&g_taskRecyleList, &taskCB->pendList);//将task插入回收链表,等待回收资源再利用 OsEventWriteUnsafe(&g_resourceEvent, OS_RESOURCE_EVENT_FREE, FALSE, NULL);//发送释放资源的事件,事件由 OsResourceRecoveryTask 消费 OsSchedResched();//申请调度 return; } 但这是一个自然死亡的进程,还有很多非正常死亡在其他篇幅中已有说明.请自行翻看.非正常死亡的会产生僵尸进程.这种进程需要别的进程通过 waitpid来回收. 孤儿进程 一般情况下往往是白发人送黑发人,子进程的生命周期是要短于父进程.但因为fork之后,进程之间相互独立,调度算法一视同仁,父子之间是弱的关系力,就什么情况都可能发生了.内核是允许老父亲先走的,如果父进程退出而它的一个或多个子进程还在运行,那么这些子进程就被称为孤儿进程,孤儿进程最终将被两位老祖宗(用户态和内核态)所收养,并由老祖宗完成对它们的状态收集工作。 //当一个进程自然退出的时候,它的孩子进程由两位老祖宗收养 STATIC VOID OsDealAliveChildProcess(LosProcessCB *processCB) { UINT32 parentID; LosProcessCB *childCB = NULL; LosProcessCB *parentCB = NULL; LOS_DL_LIST *nextList = NULL; LOS_DL_LIST *childHead = NULL; if (!LOS_ListEmpty(&processCB->childrenList)) {//如果存在孩子进程 childHead = processCB->childrenList.pstNext;//获取孩子链表 LOS_ListDelete(&(processCB->childrenList));//清空自己的孩子链表 if (OsProcessIsUserMode(processCB)) {//是用户态进程 parentID = g_userInitProcess;//用户态进程老祖宗 } else { parentID = g_kernelInitProcess;//内核态进程老祖宗 } for (nextList = childHead; ;) {//遍历孩子链表 childCB = OS_PCB_FROM_SIBLIST(nextList);//找到孩子的真身 childCB->parentProcessID = parentID;//孩子磕头认老祖宗为爸爸 nextList = nextList->pstNext;//找下一个孩子进程 if (nextList == childHead) {//一圈下来,孩子们都磕完头了 break; } } parentCB = OS_PCB_FROM_PID(parentID);//找个老祖宗的真身 LOS_ListTailInsertList(&parentCB->childrenList, childHead);//挂到老祖宗的孩子链表上 } return; } 解读 函数很简单,都一一注释了,老父亲临终托付后事,请各自的老祖宗照顾孩子. 从这里也可以看出进程的家族管理模式,两个家族从进程的出生到死亡负责到底. 僵尸进程 一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。 如果一个进程已经终止,但是它的父进程尚未调用wait或waitpid对它进行清理,这时的进程状态称为僵尸(Zombie)进程,即 Z 进程.任何进程在刚终止时都是僵尸进程,正常情况下,僵尸进程都立刻被父进程清理了. 不正常情况下就需要手动waitpid清理了. waitpid 在鸿蒙系统中,一个进程结束了,但是它的父进程没有等待(调用wait waitpid)它,那么它将变成一个僵尸进程。通过系统调用 waitpid可以彻底的清理掉子进程.归还pcb.最终调用到SysWait #include <sys/wait.h> #include "syscall.h" pid_t waitpid(pid_t pid, int *status, int options) { return syscall_cp(SYS_wait4, pid, status, options, 0); } //等待子进程结束 int SysWait(int pid, USER int *status, int options, void *rusage) { (void)rusage; return LOS_Wait(pid, status, (unsigned int)options, NULL); } //返回已经终止的子进程的进程ID号,并清除僵死进程。 LITE_OS_SEC_TEXT INT32 LOS_Wait(INT32 pid, USER INT32 *status, UINT32 options, VOID *rusage) { (VOID)rusage; UINT32 ret; UINT32 intSave; LosProcessCB *childCB = NULL; LosProcessCB *processCB = NULL; LosTaskCB *runTask = NULL; ret = OsWaitOptionsCheck(options);//参数检查,只支持LOS_WAIT_WNOHANG if (ret != LOS_OK) { return -ret; } SCHEDULER_LOCK(intSave); processCB = OsCurrProcessGet(); //获取当前进程 runTask = OsCurrTaskGet(); //获取当前任务 ret = OsWaitChildProcessCheck(processCB, pid, &childCB);//先检查下看能不能找到参数要求的退出子进程 if (ret != LOS_OK) { pid = -ret; goto ERROR; } if (childCB != NULL) {//找到了进程 return OsWaitRecycleChildPorcess(childCB, intSave, status);//回收进程 } //没有找到,看是否要返回还是去做个登记 if ((options & LOS_WAIT_WNOHANG) != 0) {//有LOS_WAIT_WNOHANG标签 runTask->waitFlag = 0;//等待标识置0 pid = 0;//这里置0,是为了 return 0 goto ERROR; } //等待孩子进程退出 OsWaitInsertWaitListInOrder(runTask, processCB);//将当前任务挂入进程waitList链表 //发起调度的目的是为了让出CPU,让其他进程/任务运行 OsSchedResched();//发起调度 runTask->waitFlag = 0; if (runTask->waitID == OS_INVALID_VALUE) { pid = -LOS_ECHILD;//没有此子进程 goto ERROR; } childCB = OS_PCB_FROM_PID(runTask->waitID);//获取当前任务的等待子进程ID if (!(childCB->processStatus & OS_PROCESS_STATUS_ZOMBIES)) {//子进程非僵死进程 pid = -LOS_ESRCH;//没有此进程 goto ERROR; } //回收僵死进程 return OsWaitRecycleChildPorcess(childCB, intSave, status); ERROR: SCHEDULER_UNLOCK(intSave); return pid; } 解读 pid是数据参数,根据不同的参数代表不同的含义,含义如下: 参数值 说明 pid<-1 等待进程组号为pid绝对值的任何子进程。 pid=-1 等待任何子进程,此时的waitpid()函数就退化成了普通的wait()函数。 pid=0 等待进程组号与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程。 pid>0 等待进程号为pid的子进程。 pid不同值代表的真正含义可以看这个函数OsWaitSetFlag. //设置等待子进程退出方式方法 STATIC UINT32 OsWaitSetFlag(const LosProcessCB *processCB, INT32 pid, LosProcessCB **child) { LosProcessCB *childCB = NULL; ProcessGroup *group = NULL; LosTaskCB *runTask = OsCurrTaskGet(); UINT32 ret; if (pid > 0) {//等待进程号为pid的子进程结束 /* Wait for the child process whose process number is pid. */ childCB = OsFindExitChildProcess(processCB, pid);//看能否从退出的孩子链表中找到PID if (childCB != NULL) {//找到了,确实有一个已经退出的PID,注意一个进程退出时会挂到父进程的exitChildList上 goto WAIT_BACK;//直接成功返回 } ret = OsFindChildProcess(processCB, pid);//看能否从现有的孩子链表中找到PID if (ret != LOS_OK) { return LOS_ECHILD;//参数进程并没有这个PID孩子,返回孩子进程失败. } runTask->waitFlag = OS_PROCESS_WAIT_PRO;//设置当前任务的等待类型 runTask->waitID = pid; //当前任务要等待进程ID结束 } else if (pid == 0) {//等待同一进程组中的任何子进程 /* Wait for any child process in the same process group */ childCB = OsFindGroupExitProcess(processCB->group, OS_INVALID_VALUE);//看能否从退出的孩子链表中找到PID if (childCB != NULL) {//找到了,确实有一个已经退出的PID goto WAIT_BACK;//直接成功返回 } runTask->waitID = processCB->group->groupID;//等待进程组的任意一个子进程结束 runTask->waitFlag = OS_PROCESS_WAIT_GID;//设置当前任务的等待类型 } else if (pid == -1) {//等待任意子进程 /* Wait for any child process */ childCB = OsFindExitChildProcess(processCB, OS_INVALID_VALUE);//看能否从退出的孩子链表中找到PID if (childCB != NULL) {//找到了,确实有一个已经退出的PID goto WAIT_BACK; } runTask->waitID = pid;//等待PID,这个PID可以和当前进程没有任何关系 runTask->waitFlag = OS_PROCESS_WAIT_ANY;//设置当前任务的等待类型 } else { /* pid < -1 */ //等待指定进程组内为|pid|的所有子进程 /* Wait for any child process whose group number is the pid absolute value. */ group = OsFindProcessGroup(-pid);//先通过PID找到进程组 if (group == NULL) { return LOS_ECHILD; } childCB = OsFindGroupExitProcess(group, OS_INVALID_VALUE);//在进程组里任意一个已经退出的子进程 if (childCB != NULL) { goto WAIT_BACK; } runTask->waitID = -pid;//此处用负数是为了和(pid == 0)以示区别,因为二者的waitFlag都一样. runTask->waitFlag = OS_PROCESS_WAIT_GID;//设置当前任务的等待类型 } WAIT_BACK: *child = childCB; return LOS_OK; } status带走进程退出码,exitCode分成了三个部分格式如下 /* * Process exit code * 31 15 8 7 0 * | | exit code | core dump | signal | */ #define OS_PRO_EXIT_OK 0 //进程正常退出 //置进程退出码第七位为1 STATIC INLINE VOID OsProcessExitCodeCoreDumpSet(LosProcessCB *processCB) { processCB->exitCode |= 0x80U;// 0b10000000 } //设置进程退出信号(0 ~ 7) STATIC INLINE VOID OsProcessExitCodeSignalSet(LosProcessCB *processCB, UINT32 signal) { processCB->exitCode |= signal & 0x7FU;//0b01111111 } //清除进程退出信号(0 ~ 7) STATIC INLINE VOID OsProcessExitCodeSignalClear(LosProcessCB *processCB) { processCB->exitCode &= (~0x7FU);//低7位全部清0 } //进程退出码是否被设置过,默认是 0 ,如果 & 0x7FU 还是 0 ,说明没有被设置过. STATIC INLINE BOOL OsProcessExitCodeSignalIsSet(LosProcessCB *processCB) { return (processCB->exitCode) & 0x7FU; } //设置进程退出号(8 ~ 15) STATIC INLINE VOID OsProcessExitCodeSet(LosProcessCB *processCB, UINT32 code) { processCB->exitCode |= ((code & 0x000000FFU) << 8U) & 0x0000FF00U; /* 8: Move 8 bits to the left, exitCode */ } 0 - 7为信号位,信号处理有专门的篇幅,此处不做详细介绍,请自行翻看,这里仅列出部分信号含义. #define SIGHUP 1 //终端挂起或者控制进程终止 #define SIGINT 2 //键盘中断(如break键被按下) #define SIGQUIT 3 //键盘的退出键被按下 #define SIGILL 4 //非法指令 #define SIGTRAP 5 //跟踪陷阱(trace trap),启动进程,跟踪代码的执行 #define SIGABRT 6 //由abort(3)发出的退出指令 #define SIGIOT SIGABRT //abort发出的信号 #define SIGBUS 7 //总线错误 #define SIGFPE 8 //浮点异常 #define SIGKILL 9 //常用的命令 kill 9 123 | 不能被忽略、处理和阻塞 #define SIGUSR1 10 //用户自定义信号1 #define SIGSEGV 11 //无效的内存引用, 段违例(segmentation violation),进程试图去访问其虚地址空间以外的位置 #define SIGUSR2 12 //用户自定义信号2 #define SIGPIPE 13 //向某个非读管道中写入数据 #define SIGALRM 14 //由alarm(2)发出的信号,默认行为为进程终止 #define SIGTERM 15 //终止信号 #define SIGSTKFLT 16 //栈溢出 #define SIGCHLD 17 //子进程结束信号 #define SIGCONT 18 //进程继续(曾被停止的进程) #define SIGSTOP 19 //终止进程 | 不能被忽略、处理和阻塞 #define SIGTSTP 20 //控制终端(tty)上 按下停止键 #define SIGTTIN 21 //进程停止,后台进程企图从控制终端读 #define SIGTTOU 22 //进程停止,后台进程企图从控制终端写 #define SIGURG 23 //I/O有紧急数据到达当前进程 #define SIGXCPU 24 //进程的CPU时间片到期 #define SIGXFSZ 25 //文件大小的超出上限 #define SIGVTALRM 26 //虚拟时钟超时 #define SIGPROF 27 //profile时钟超时 #define SIGWINCH 28 //窗口大小改变 #define SIGIO 29 //I/O相关 #define SIGPOLL 29 // #define SIGPWR 30 //电源故障,关机 #define SIGSYS 31 //系统调用中参数错,如系统调用号非法 #define SIGUNUSED SIGSYS //系统调用异常 options是行为参数,提供了一些另外的选项来控制waitpid()函数的行为。 参数值 鸿蒙支持 说明 LOS_WAIT_WNOHANG 支持 如果没有孩子进程退出,则立即返回,而不是阻塞在这个函数上等待;如果结束了,则返回该子进程的进程号。 LOS_WAIT_WUNTRACED 不支持 报告终止或停止的子进程的状态 LOS_WAIT_WCONTINUED 不支持 鸿蒙目前只支持了LOS_WAIT_WNOHANG模式,内核源码中虽有LOS_WAIT_WUNTRACED和LOS_WAIT_WCONTINUED的实现痕迹,但是整体阅读下来比较乱,应该是没有写好. 鸿蒙源码百篇博客 往期回顾 v47.xx (进程回收篇) | 老父亲如何向老祖宗临终托孤 ? < csdn | 51cto | harmony > v46.xx (特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 < csdn | 51cto | harmony > v45.xx (fork篇) | fork是如何做到调用一次,返回两次的 ? < csdn | 51cto | harmony > v44.xx (中断管理篇) | 硬中断的实现<>观察者模式 < csdn | 51cto | harmony > v43.xx (中断概念篇) | 外人眼中权势滔天的当红海公公 < csdn | 51cto | harmony > v42.xx (中断切换篇) | 中断切换到底在切换什么? < csdn | 51cto | harmony > v41.xx (任务切换篇) | 汇编逐行注解分析任务上下文 < csdn | 51cto | harmony > v40.xx (汇编汇总篇) | 所有的汇编代码都在这里 < csdn | 51cto | harmony > v39.xx (异常接管篇) | 社会很单纯,复杂的是人 < csdn | 51cto | harmony > v38.xx (寄存器篇) | ARM所有寄存器一网打尽,不再神秘 < csdn | 51cto | harmony > v37.xx (系统调用篇) | 全盘解剖系统调用实现过程 < csdn | 51cto | harmony > v36.xx (工作模式篇) | CPU是韦小宝,有哪七个老婆? < csdn | 51cto | harmony > v35.xx (时间管理篇) | Tick是操作系统的基本时间单位 < csdn | 51cto | harmony > v34.xx (原子操作篇) | 是谁在为原子操作保驾护航? < csdn | 51cto | harmony > v33.xx (消息队列篇) | 进程间如何异步解耦传递大数据 ? < csdn | 51cto | harmony > v32.xx (CPU篇) | 内核是如何描述CPU的? < csdn | 51cto | harmony > v31.xx (定时器篇) | 内核最高优先级任务是谁? < csdn | 51cto | harmony > v30.xx (事件控制篇) | 任务间多对多的同步方案 < csdn | 51cto | harmony > v29.xx (信号量篇) | 信号量解决任务同步问题 < csdn | 51cto | harmony > v28.xx (进程通讯篇) | 进程间通讯有哪九大方式? < csdn | 51cto | harmony > v27.xx (互斥锁篇) | 互斥锁比自旋锁可丰满许多 < csdn | 51cto | harmony > v26.xx (自旋锁篇) | 真的好想为自旋锁立贞节牌坊! < csdn | 51cto | harmony > v25.xx (并发并行篇) | 怎么记住并发并行的区别? < csdn | 51cto | harmony > v24.xx (进程概念篇) | 进程在管理哪些资源? < csdn | 51cto | harmony > v23.xx (汇编传参篇) | 汇编如何传递复杂的参数? < csdn | 51cto | harmony > v22.xx (汇编基础篇) | CPU在哪里打卡上班? < csdn | 51cto | harmony > v21.xx (线程概念篇) | 是谁在不断的折腾CPU? < csdn | 51cto | harmony > v20.xx (用栈方式篇) | 栈是构建底层运行的基础 < csdn | 51cto | harmony > v19.xx (位图管理篇) | 为何进程和线程优先级都是32个? < csdn | 51cto | harmony > v18.xx (源码结构篇) | 内核500问你能答对多少? < csdn | 51cto | harmony > v17.xx (物理内存篇) | 这样记伙伴算法永远不会忘 < csdn | 51cto | harmony > v16.xx (内存规则篇) | 内存管理到底在管什么? < csdn | 51cto | harmony > v15.xx (内存映射篇) | 什么是内存最重要的实现基础 ? < csdn | 51cto | harmony > v14.xx (内存汇编篇) | 什么是虚拟内存的实现基础? < csdn | 51cto | harmony > v13.xx (源码注释篇) | 热爱是所有的理由和答案 < csdn | 51cto | harmony > v12.xx (内存管理篇) | 虚拟内存全景图是怎样的? < csdn | 51cto | harmony > v11.xx (内存分配篇) | 内存有哪些分配方式? < csdn | 51cto | harmony > v10.xx (内存主奴篇) | 紫禁城的主子和奴才如何相处? < csdn | 51cto | harmony > v09.xx (调度故事篇) | 用故事说内核调度 < csdn | 51cto | harmony > v08.xx (总目录) | 百万汉字注解 百篇博客分析 < csdn | 51cto | harmony > v07.xx (调度机制篇) | 任务是如何被调度执行的? < csdn | 51cto | harmony > v06.xx (调度队列篇) | 就绪队列对调度的作用 < csdn | 51cto | harmony > v05.xx (任务管理篇) | 谁在让CPU忙忙碌碌? < csdn | 51cto | harmony > v04.xx (任务调度篇) | 任务是内核调度的单元 < csdn | 51cto | harmony > v03.xx (时钟任务篇) | 触发调度最大的动力来自哪里? < csdn | 51cto | harmony > v02.xx (进程管理篇) | 进程是内核资源管理单元 < csdn | 51cto | harmony > v01.xx (双向链表篇) | 谁是内核最重要结构体? < csdn | 51cto | harmony > 参与贡献 访问注解仓库地址 Fork 本仓库 >> 新建 Feat_xxx 分支 >> 提交代码注解 >> 新建 Pull Request 新建 Issue 喜欢请「点赞+关注+收藏」 关注「鸿蒙内核源码分析」公众号 各大站点搜 「鸿蒙内核源码分析」.欢迎转载,请注明出处. 进入 >> oschina | csdn | 51cto | 简书 | 掘金 | harmony

优秀的个人博客,低调大师

鸿蒙内核源码分析(汇编汇总篇) | HarmonyOS所有汇编代码全都在这,拿走不谢 | 百篇博客分析鸿蒙源码 | v40.03

百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< oschina | csdn | weharmony > 汇编其实很可爱 绝大部分IT从业人员终生不用触碰到的汇编,它听着像上古时代遥远的呼唤,总觉得远却又能听到声,汇编再往下就真的是01110011了,汇编指令基本是一一对应了机器指令. 所谓内核是对硬件的驱动,对驱动之后资源的良序管理,这里说的资源是CPU(单核/多核),内存,磁盘,i/o设备.层层封装,步步遮蔽,到了应用层,不知有汉,无论魏晋才好.好是好,但有句话,其实哪有什么岁月静好,只是有人替你负重前行.难道就不想知道别人是怎么负重前行的? 越高级的语言是越接近人思维模式的,越低级的语言就是越贴近逻辑与非门的高低电平的起伏.汇编是贴着硬件飞行的,要研究内核就绕不过汇编,觉得神秘是来源于不了解,恐惧是来自于没接近. 其实深入分析内核源码之后就会发现,汇编其实很可爱,很容易,比c/c++/java容易太多了,真的是很傻很单纯. 鸿蒙内核源码分析系列篇至少已经有五篇涉及到了汇编,请自行翻看,但还是远远不够,要写十五篇,彻底摸透,现在才刚刚开始,本篇先整理鸿蒙内核所有汇编文件和大概说明文件的作用,后续一块一块来剥,不把这些汇编剥个精光不罢休. 汇编目录 鸿蒙所有汇编文件如下: 直接点击可以查看注解源码,有些站点会把链接去除,没办法,可直接去各大站点搜"鸿蒙内核源码分析",找到源码注解. \arch\arm\arm\src startup 启动相关 reset_vector_mp.S 多核CPU下启动代码,大文件 reset_vector_up.S 单核CPU下启动代码,大文件 armv7a cache.S 缓存相关的两个函数 los_dispatch.S 异常分发处理,大文件. los_hw_exc.S 硬件异常相关,大文件. los_hw_runstop.S OsSRSaveRegister 和 OsSRRestoreRegister 汇编实现 jmp.S 两个简单的跳转函数 hw_user_get.S 拷贝用户空间数据到内核空间 hw_user_put.S 拷贝内核空间数据到用户空间 hw_user_get.S 将用户空间数据src 拷贝到内核空间 dst // errno_t _arm_get_user(void *dst, const void *src, size_t dstTypeLen, size_t srcTypeLen) FUNCTION(_arm_get_user) stmdb sp!, {r0, r1, r2, r3, lr} @四个参数入栈,保存LR cmp r2, #0 @r2 和 0比较 beq .Lget_user_return @相等 跳到Lget_user_return 直接返回 cmp r2, r3 @r2 和 r3比较 bne .Lget_user_err @不等,说明函数要返回错误 cmp r2, #1 @r2 和 1比较 bhi .Lget_user_half @if(dstTypeLen>1) 跳转到Lget_user_half .Lget_user_byte: @按字节拷贝数据 0: ldrbt r3, [r1], #0 @r3=*r1 1: strb r3, [r0], #0 @*r0=r3 b .Lget_user_return .Lget_user_half: cmp r2, #2 @r2 和 2比较 bhi .Lget_user_word @if(dstTypeLen>2) Lget_user_word 2: ldrht r3, [r1], #0 @完成最后一个字节的拷贝 3: strh r3, [r0], #0 @完成最后一个字节的拷贝 b .Lget_user_return .Lget_user_word: cmp r2, #4 @r2 和 4比较 bhi .Lget_user_err @if(dstTypeLen>4) 跳转到Lget_user_err 4: ldrt r3, [r1], #0 5: str r3, [r0], #0 .Lget_user_return: @返回锚点 ldmia sp!, {r0, r1, r2, r3, lr} @保存的内容出栈,恢复各寄存器值 mov r0, 0 @r0保存返回值为0 bx lr @跳回调用函数继续执行,_arm_get_user到此结束! .Lget_user_err: ldmia sp!, {r0, r1, r2, r3, lr} @保存的内容出栈,恢复各寄存器值 mov r0, #-14 @r0保存返回值为-14 bx lr @跳回调用函数继续执行,_arm_get_user到此结束! .pushsection __exc_table, "a" .long 0b, .Lget_user_err .long 1b, .Lget_user_err .long 2b, .Lget_user_err .long 3b, .Lget_user_err .long 4b, .Lget_user_err .long 5b, .Lget_user_err .popsection 解读 用户空间和内核空间的数据为什么需要拷贝? 这是个经典问题,看了网上的一些回答,没毛病: 内核不能信任任何用户空间的指针。必须对用户空间的指针指向的数据进行验证。如果只做验证不做拷贝的话,那么在随后的运行中要随时受到其它进/线程可能修改用户空间数据的威胁。所以必须做拷贝。 在内存系列篇中已经反复的说过,每个用户进程都有自己独立的用户空间,但这个用户空间是通过MMU映射出来的,是表面上繁花似锦,背后都共用着真正的物理内存,所以在高频率的任务切换过程中,原有的用户空间地址内容很容易被覆盖掉.举个例子说明下: 用户A有个美女西施放在万聪酒店21号房说要献给内核大佬,如果内核不直接把美女接回家,而仅仅是做个记录,写着西施在万聪酒店21号房,内核大佬立马跑去过,还不会错能拿对人,但如果被其他事给耽搁了呢? 耽搁的这回功夫,调度算法把万聪酒店21号房给了用户B使用,当然用户B使用之前,酒店管理人员会把西施置换个地方(以至于用户A再回到酒店时,原来的东西该怎样还咋样还原). 等21号房空出来了,B肯定不知道原来的房间是A在用,而且里面曾经还过有个美女西施,更不可能晓得A把西施献给内核大佬这回事了.因为B的业务需要,很可能往21号房整了个东施进来. 此时如果内核大佬事忙完了,想起用户A献美女的事了,是时候了.因为只记录了地址,直接去万聪酒店21号房抓人,可这会抓出来那是咱东施小姐呀.这可不把事给搞砸啦. 所以需要跨空间拷贝,直接把美女接回家找个地方关起来先. reset_vector_mp.S 和 reset_vector_up.S 鸿蒙开机代码根据 CPU多核还是单核分成了两个独立文件处理. mp就是多处理器(multiprocessing)的意思: 多CPU核的操作系统3种处理模式(SMP+AMP+BMP) 鸿蒙实现的是 SMP 的方式 非对称多处理(Asymmetric multiprocessing,AMP)每个CPU内核 运行一个独立的操作系统或同一操作系统的独立实例(instantiation)。 对称多处理(Symmetric multiprocessing,SMP)一个操作系统的实例 可以同时管理所有CPU内核,且应用并不绑定某一个内核。 混合多处理(Bound multiprocessing,BMP)一个操作系统的实例可以 同时管理所有CPU内核,但每个应用被锁定于某个指定的核心。 up(unit processing )的意思,单个CPU,虽然没mp的复杂,但文件也很大 500行汇编,一小节讲不完,需要单独的一篇专讲 reset_vector 这里只列出up情况下的开机代码 reset_vector: @鸿蒙单核cpu 开机代码 /* do some early cpu setup: i/d cache disable, mmu disabled */ mrc p15, 0, r0, c1, c0, 0 bic r0, #(1<<12) bic r0, #(1<<2 | 1<<0) mcr p15, 0, r0, c1, c0, 0 /* r11: delta of physical address and virtual address */ adr r11, pa_va_offset ldr r0, [r11] sub r11, r11, r0 /* if we need to relocate to proper location or not */ adr r4, __exception_handlers /* r4: base of load address */ ldr r5, =SYS_MEM_BASE /* r5: base of physical address */ subs r12, r4, r5 /* r12: delta of load address and physical address */ beq reloc_img_to_bottom_done /* if we load image at the bottom of physical address */ /* we need to relocate image at the bottom of physical address */ ldr r7, =__exception_handlers /* r7: base of linked address (or vm address) */ ldr r6, =__bss_start /* r6: end of linked address (or vm address) */ sub r6, r7 /* r6: delta of linked address (or vm address) */ add r6, r4 /* r6: end of load address */ los_dispatch.S 和 los_hw_exc.S 异常模式处理入口和统一分发现实,之前也有提到过,很复杂,1000多行,后续单独细说实现过程. jmp.S 两个简单的函数longjmp setjmp 的实现,加注解部分请前往 鸿蒙内核源码注解分析 查看 FUNCTION(longjmp) ldmfd r0,{r4-r12} add r0,#(4 * 9) ldr r13,[r0] add r0,#4 ldr r14,[r0] cmp r1,#0 moveq r1,#1 mov r0,r1 mov pc,lr FUNCTION(setjmp) stmea r0,{r4-r12} add r0,#(4 * 9) str r13,[r0] add r0,#4 str r14,[r0] mov r0,#0 mov pc,lr los_hw_runstop.S .global OsSRSaveRegister .global OsSRRestoreRegister 两个函数的汇编现实,有点复杂,后续单独说明. cache.S 这是缓存部分的两个函数实现,此处没有加注解,试着看明白这两个函数的实现.加注解部分请前往 鸿蒙内核源码注解分析 查看 .macro DCACHE_LINE_SIZE, reg, tmp mrc p15, 0, \tmp, c0, c0, 1 lsr \tmp, \tmp, #16 and \tmp, \tmp, #0xf mov \reg, #4 mov \reg, \reg, lsl \tmp .endm FUNCTION(arm_inv_cache_range) push {r2, r3} DCACHE_LINE_SIZE r2, r3 sub r3, r2, #1 tst r0, r3 bic r0, r0, r3 mcrne p15, 0, r0, c7, c14, 1 tst r1, r3 bic r1, r1, r3 mcrne p15, 0, r1, c7, c14, 1 1: mcr p15, 0, r0, c7, c6, 1 add r0, r0, r2 cmp r0, r1 blo 1b dsb pop {r2, r3} mov pc, lr FUNCTION(arm_clean_cache_range) push {r2, r3} DCACHE_LINE_SIZE r2, r3 sub r3, r2, #1 bic r0, r0, r3 1: mcr p15, 0, r0, c7, c10, 1 add r0, r0, r2 cmp r0, r1 blo 1b dsb pop {r2, r3} mov pc, lr 参与贡献 访问注解仓库地址 Fork 本仓库 >> 新建 Feat_xxx 分支 >> 提交代码注解 >> 新建 Pull Request 新建 Issue 喜欢请大方 点赞+关注+收藏 吧 各大站点搜 "鸿蒙内核源码分析" .欢迎转载,请注明出处. 进入 >> 百万汉字注解 百篇博客分析 精读鸿蒙源码 深挖地基工程 < gitee | csdn | oschina >

优秀的个人博客,低调大师

| 中文注解HarmonyOS源码 | v27.01

鸿蒙内核源码中文注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆, 四大源码仓每日同步更新< Gitee | Github | CSDN | Coding > 鸿蒙内核源码分析博客 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新< OSCHINA | CSDN | WeHarmony > 本篇说清楚互斥锁 读本篇之前建议先读鸿蒙内核源码分析(总目录)之自旋锁篇. 内核中哪些模块会用到互斥锁?看图: 图中是内核有关模块对互斥锁初始化,有文件,有内存,用消息队列等等,使用面非常的广.其实在给内核源码加注的过程中,会看到大量的自旋锁和互斥锁,它们的存在有序的保证了内核和应用程序的正常运行.是非常基础和重要的功能. 概述 自旋锁 和 互斥锁 虽都是锁,但解决的问题不同, 自旋锁解决用于CPU核间共享内存的竞争,而互斥锁解决线程(任务)间共享内存的竞争. 自旋锁的特点是死守共享资源,拿不到锁,CPU选择睡眠,等待其他CPU释放资源.所以共享代码段不能太复杂,否则容易死锁,休克. 互斥锁的特点是拿不到锁往往原任务阻塞,切换到新任务运行.CPU是会一直跑的.这样很容易会想到几个问题: 第一:会出现很多任务在等同一把锁的情况出现,因为切换新任务也可能因要同一把锁而被阻塞,CPU又被调去跑新新任务了.这样就会出现一个等锁的链表. 第二:持有锁的一方再申请同一把锁时还能成功吗? 答案是可以的,这种锁叫递归锁,是鸿蒙内核默认方式. 第三:当优先级很高的A任务要锁失败,主动让出CPU进入睡眠,而如果持有锁的B任务优先级很低, 迟迟等不到调度不到B任务运行,无法释放锁怎么办? 答案是会临时调整B任务的优先级,调到A一样高,这样B能很快的被调度到,等B释放锁后其优先级又会被打回原形.所以一个任务的优先级会看情况时高时低. 第四:B任务释放锁之后要主动唤醒等锁的任务链表,使他们能加入就绪队列,等待被调度.调度算法是一视同仁的,它只看优先级. 带着这些问题,进入鸿蒙内核互斥锁的实现代码,本篇代码量较大, 每行代码都一一注解说明. 互斥锁长什么样? enum { LOS_MUX_PRIO_NONE = 0, //线程的优先级和调度不会受到互斥锁影响,先来后到,普通排队. LOS_MUX_PRIO_INHERIT = 1, //当高优先级的等待低优先级的线程释放锁时,低优先级的线程以高优先级线程的优先级运行。 //当线程解锁互斥量时,线程的优先级自动被将到它原来的优先级 LOS_MUX_PRIO_PROTECT = 2 //详见:OsMuxPendOp中的注解,详细说明了LOS_MUX_PRIO_PROTECT的含义 }; enum { LOS_MUX_NORMAL = 0, //非递归锁 只有[0.1]两个状态,不做任何特殊的错误检,不进行deadlock detection(死锁检测) LOS_MUX_RECURSIVE = 1, //递归锁 允许同一线程在互斥量解锁前对该互斥量进行多次加锁。递归互斥量维护锁的计数,在解锁次数和加锁次数不相同的情况下,不会释放锁,别的线程就无法加锁此互斥量。 LOS_MUX_ERRORCHECK = 2, //进行错误检查,如果一个线程企图对一个已经锁住的mutex进行relock或对未加锁的unlock,将返回一个错误。 LOS_MUX_DEFAULT = LOS_MUX_RECURSIVE //鸿蒙系统默认使用递归锁 }; typedef struct { //互斥锁的属性 UINT8 protocol; //协议 UINT8 prioceiling; //优先级上限 UINT8 type; //类型属性 UINT8 reserved; //保留字段 } LosMuxAttr; typedef struct OsMux { //互斥锁结构体 UINT32 magic; /**< magic number */ //魔法数字 LosMuxAttr attr; /**< Mutex attribute */ //互斥锁属性 LOS_DL_LIST holdList; /**< The task holding the lock change */ //当有任务拿到本锁时,通过holdList节点把锁挂到该任务的锁链表上 LOS_DL_LIST muxList; /**< Mutex linked list */ //等这个锁的任务链表,上面挂的都是任务,注意和holdList的区别. VOID *owner; /**< The current thread that is locking a mutex */ //当前拥有这把锁的任务 UINT16 muxCount; /**< Times of locking a mutex */ //锁定互斥体的次数,递归锁允许多次 } LosMux; 这互斥锁长的明显的比自旋锁丰满多啦,还记得自旋锁的样子吗,就一个变量,单薄到令人心疼. 初始化 LITE_OS_SEC_TEXT UINT32 LOS_MuxInit(LosMux *mutex, const LosMuxAttr *attr) { //... SCHEDULER_LOCK(intSave); //拿到调度自旋锁 mutex->muxCount = 0; //锁定互斥量的次数 mutex->owner = NULL; //持有该锁的任务 LOS_ListInit(&mutex->muxList); //初始化等待该锁的任务链表 mutex->magic = OS_MUX_MAGIC; //固定标识,互斥锁的魔法数字 SCHEDULER_UNLOCK(intSave); //释放调度自旋锁 return LOS_OK; } 留意mutex->muxList,这又是一个双向链表, 双向链表是内核最重要的结构体,不仅仅是鸿蒙内核,在linux内核中(list_head)又何尝不是,牢牢的寄生在宿主结构体上.muxList上挂的是未来所有等待这把锁的任务. 三种申请模式 申请互斥锁有三种模式:无阻塞模式、永久阻塞模式、定时阻塞模式。 无阻塞模式:即任务申请互斥锁时,入参timeout等于0。若当前没有任务持有该互斥锁,或者持有该互斥锁的任务和申请该互斥锁的任务为同一个任务,则申请成功,否则立即返回申请失败。 永久阻塞模式:即任务申请互斥锁时,入参timeout等于0xFFFFFFFF。若当前没有任务持有该互斥锁,则申请成功。否则,任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,直到有其他任务释放该互斥锁,阻塞任务才会重新得以执行。 定时阻塞模式:即任务申请互斥锁时,0<timeout<0xFFFFFFFF。若当前没有任务持有该互斥锁,则申请成功。否则该任务进入阻塞态,系统切换到就绪任务中优先级最高者继续执行。任务进入阻塞态后,超时前如果有其他任务释放该互斥锁,则该任务可成功获取互斥锁继续执行,若超时前未获取到该互斥锁,接口将返回超时错误码。 如果有任务阻塞于该互斥锁,则唤醒被阻塞任务中优先级最高的,该任务进入就绪态,并进行任务调度。 如果没有任务阻塞于该互斥锁,则互斥锁释放成功。 申请互斥锁主函数 OsMuxPendOp //互斥锁的主体函数,由OsMuxlockUnsafe调用,互斥锁模块最重要的几个函数之一 //最坏情况就是拿锁失败,让出CPU,变成阻塞任务,等别的任务释放锁后排到自己了接着执行. STATIC UINT32 OsMuxPendOp(LosTaskCB *runTask, LosMux *mutex, UINT32 timeout) { UINT32 ret; LOS_DL_LIST *node = NULL; LosTaskCB *owner = NULL; if ((mutex->muxList.pstPrev == NULL) || (mutex->muxList.pstNext == NULL)) {//列表为空时的处理 /* This is for mutex macro initialization. */ mutex->muxCount = 0;//锁计数器清0 mutex->owner = NULL;//锁没有归属任务 LOS_ListInit(&mutex->muxList);//初始化锁的任务链表,后续申请这把锁任务都会挂上去 } if (mutex->muxCount == 0) {//无task用锁时,肯定能拿到锁了.在里面返回 mutex->muxCount++; //互斥锁计数器加1 mutex->owner = (VOID *)runTask; //当前任务拿到锁 LOS_ListTailInsert(&runTask->lockList, &mutex->holdList);//持有锁的任务改变了,节点挂到当前task的锁链表 if ((runTask->priority > mutex->attr.prioceiling) && (mutex->attr.protocol == LOS_MUX_PRIO_PROTECT)) {//看保护协议的做法是怎样的? LOS_BitmapSet(&runTask->priBitMap, runTask->priority);//1.priBitMap是记录任务优先级变化的位图,这里把任务当前的优先级记录在priBitMap OsTaskPriModify(runTask, mutex->attr.prioceiling);//2.把高优先级的mutex->attr.prioceiling设为当前任务的优先级. }//注意任务优先级有32个, 是0最高,31最低!!!这里等于提高了任务的优先级,目的是让其在下次调度中继续提高被选中的概率,从而快速的释放锁. return LOS_OK; } //递归锁muxCount>0 如果是递归锁就要处理两种情况 1.runtask持有锁 2.锁被别的任务拿走了 if (((LosTaskCB *)mutex->owner == runTask) && (mutex->attr.type == LOS_MUX_RECURSIVE)) {//第一种情况 runtask是锁持有方 mutex->muxCount++; //递归锁计数器加1,递归锁的目的是防止死锁,鸿蒙默认用的就是递归锁(LOS_MUX_DEFAULT = LOS_MUX_RECURSIVE) return LOS_OK; //成功退出 } //到了这里说明锁在别的任务那里,当前任务只能被阻塞了. if (!timeout) {//参数timeout表示等待多久再来拿锁 return LOS_EINVAL;//timeout = 0表示不等了,没拿到锁就返回不纠结,返回错误.见于LOS_MuxTrylock } //自己要被阻塞,只能申请调度,让出CPU core 让别的任务上 if (!OsPreemptableInSched()) {//不能申请调度 (不能调度的原因是因为没有持有调度任务自旋锁) return LOS_EDEADLK;//返回错误,自旋锁被别的CPU core 持有 } OsMuxBitmapSet(mutex, runTask, (LosTaskCB *)mutex->owner);//设置锁位图,尽可能的提高锁持有任务的优先级 owner = (LosTaskCB *)mutex->owner; //记录持有锁的任务 runTask->taskMux = (VOID *)mutex; //记下当前任务在等待这把锁 node = OsMuxPendFindPos(runTask, mutex);//在等锁链表中找到一个优先级比当前任务更低的任务 ret = OsTaskWait(node, timeout, TRUE);//task陷入等待状态 TRUE代表需要调度 if (ret == LOS_ERRNO_TSK_TIMEOUT) {//这行代码虽和OsTaskWait挨在一起,但要过很久才会执行到,因为在OsTaskWait中CPU切换了任务上下文 runTask->taskMux = NULL;// 所以重新回到这里时可能已经超时了 ret = LOS_ETIMEDOUT;//返回超时 } if (timeout != LOS_WAIT_FOREVER) {//不是永远等待的情况 OsMuxBitmapRestore(mutex, runTask, owner);//恢复锁的位图 } return ret; } 释放锁的主体函数 OsMuxPostOp //是否有其他任务持有互斥锁而处于阻塞状,如果是就要唤醒它,注意唤醒一个任务的操作是由别的任务完成的 //OsMuxPostOp只由OsMuxUnlockUnsafe,参数任务归还锁了,自然就会遇到锁要给谁用的问题, 因为很多任务在申请锁,由OsMuxPostOp来回答这个问题 STATIC UINT32 OsMuxPostOp(LosTaskCB *taskCB, LosMux *mutex, BOOL *needSched) { LosTaskCB *resumedTask = NULL; if (LOS_ListEmpty(&mutex->muxList)) {//如果互斥锁列表为空 LOS_ListDelete(&mutex->holdList);//把持有互斥锁的节点摘掉 mutex->owner = NULL; return LOS_OK; } resumedTask = OS_TCB_FROM_PENDLIST(LOS_DL_LIST_FIRST(&(mutex->muxList)));//拿到等待互斥锁链表的第一个任务实体,接下来要唤醒任务 if (mutex->attr.protocol == LOS_MUX_PRIO_INHERIT) {//互斥锁属性协议是继承会怎么操作? if (resumedTask->priority > taskCB->priority) {//拿到锁的任务优先级低于参数任务优先级 if (LOS_HighBitGet(taskCB->priBitMap) != resumedTask->priority) {//参数任务bitmap中最低的优先级不等于等待锁的任务优先级 LOS_BitmapClr(&taskCB->priBitMap, resumedTask->priority);//把等待任务锁的任务的优先级记录在参数任务的bitmap中 } } else if (taskCB->priBitMap != 0) {//如果bitmap不等于0说明参数任务至少有任务调度的优先级 OsMuxPostOpSub(taskCB, mutex);// } } mutex->muxCount = 1;//互斥锁数量为1 mutex->owner = (VOID *)resumedTask;//互斥锁的持有人换了 resumedTask->taskMux = NULL;//resumedTask不再等锁了 LOS_ListDelete(&mutex->holdList);//自然要从等锁链表中把自己摘出去 LOS_ListTailInsert(&resumedTask->lockList, &mutex->holdList);//把锁挂到恢复任务的锁链表上,lockList是任务持有的所有锁记录 OsTaskWake(resumedTask);//resumedTask有了锁就唤醒它,因为当初在没有拿到锁时处于了pend状态 if (needSched != NULL) {//如果不为空 *needSched = TRUE;//就走起再次调度流程 } return LOS_OK; } 总结 1.互斥锁解决的是任务间竞争共享内存的问题. 2.申请锁失败的任务会进入睡眠OsTaskWait,内核会比较持有锁的任务和申请锁任务的优先级,把持有锁的任务优先级调到尽可能的高,以便更快的被调度执行,早日释放锁. 3.释放锁的任务会在等锁链表中找一个高优先级任务,通过OsTaskWake唤醒它,并向调度算法申请调度.但要注意,调度算法只是按优先级来调度,并不保证调度后的任务一定是要唤醒的任务. 4.互斥锁篇关键是看懂 OsMuxPendOp 和 OsMuxPostOp 两个函数. 喜欢就请收藏吧 各大站点搜 "鸿蒙内核源码分析" ,快速找到组织. 鸿蒙内核源码中文注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆, 四大源码仓每日同步更新< Gitee | Github | CSDN | Coding > 鸿蒙内核源码分析博客 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点每日同步更新< OSCHINA | CSDN | WeHarmony >

优秀的个人博客,低调大师

【短视频SDK - 参数解析】对焦模式、裁剪模式、视频质量、分辨率、视频比例、帧率、关键帧间隔等参数解析

1.参数简析 参数名称 简介 影响 裁剪模式 分为填充模式和裁剪模式 影响图像画面的展示细节 视频质量 是指生成的视频的输出参数,是一组参数决定的数值 视频清晰度和文件大小 分辨率 图像分辨率则是单位英寸中所包含的像素点数,分辨率影响图像大小,与图像大小成正比:分辨率越高,图像越大;分辨率越低,图像越小。 影响视频文件大小和画面大小 视频比例 视频预览和输出的比例,常用1:1,4:3,3:4,16:9,9:16 根据需求不同展示不同的比例,影响视频大小 帧率 默认:25fps 影响视频的连贯度,通俗的来讲就是过小就会显得卡顿 关键帧间隔 单位:fps, 跟帧率设置有关,设置值最小为50fps,建议值125 影响视频清晰度 对焦模式 录制时可以设置手动对焦或者是追焦模式 视频清晰度和需要对焦的点 2.详细解析 2.1 裁剪模式 裁剪模式在导入编辑、导入裁剪模块都有用到,主要是渲染视频或

优秀的个人博客,低调大师

鸿蒙内核源码分析(进程回收篇) | 进程在临终前如何向老祖宗托孤 | 百篇博客分析HarmonyOS源码 | v47.02

百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< oschina | 51cto | csdn | harmony > 进程关系链 进程是家族式管理的,父子关系,兄弟关系,朋友关系,子女关系,甚至陌生人关系(等待你消亡)在一个进程的生命周期中都会记录下来.用什么来记录呢?当然是内核最重要的胶水结构体LOS_DL_LIST,进程控制块(以下简称PCB)用了8个双向链表来记录进程家族的基因关系和运行时关系.如下: typedef struct ProcessCB { //...此处省略其他变量 LOS_DL_LIST pendList; /**< Block list to which the process belongs */ //进程所属的阻塞列表,如果因拿锁失败,就由此节点挂到等锁链表上 LOS_DL_LIST childrenList; /**< my children process list */ //孩子进程都挂到这里,形成双循环链表 LOS_DL_LIST exitChildList; /**< my exit children process list */ //那些要退出孩子进程挂到这里,白发人送黑发人。 LOS_DL_LIST siblingList; /**< linkage in my parent's children list */ //兄弟进程链表, 56个民族是一家,来自同一个父进程. LOS_DL_LIST subordinateGroupList; /**< linkage in my group list */ //进程是组长时,有哪些组员进程 LOS_DL_LIST threadSiblingList; /**< List of threads under this process *///进程的线程(任务)列表 LOS_DL_LIST threadPriQueueList[OS_PRIORITY_QUEUE_NUM]; /**< The process's thread group schedules thepriority hash table */ //进程的线程组调度优先级哈希表 LOS_DL_LIST waitList; /**< The process holds the waitLits to support wait/waitpid *///进程持有等待链表以支持wait/waitpid } LosProcessCB; 解读 pendList 个人认为它是鸿蒙内核功能最多的一个链表,它远不止字面意思阻塞链表这么简单,只有深入解读源码后才能体会它真的是太会来事了,一般把它理解为阻塞链表就行.上面挂的是处于阻塞状态的进程. childrenList孩子链表,所有由它fork出来的进程都挂到这个链表上.上面的孩子进程在死亡前会将自己从上面摘出去,转而挂到exitChildList链表上. exitChildList退出孩子链表,进入死亡程序的进程要挂到这个链表上,一个进程的死亡是件挺麻烦的事,进程池的数量有限,需要及时回收进程资源,但家族管理关系复杂,要去很多地方消除痕迹.尤其还有其他进程在看你笑话,等你死亡(wait/waitpid)了通知它们一声. siblingList兄弟链表,和你同一个父亲的进程都挂到了这个链表上. subordinateGroupList 朋友圈链表,里面是因为兴趣爱好(进程组)而挂在一起的进程,它们可以不是一个父亲,不是一个祖父,但一定是同一个老祖宗(用户态和内核态根进程). threadSiblingList线程链表,上面挂的是进程ID都是这个进程的线程(任务),进程和线程的关系是1:N的关系,一个线程只能属于一个进程.这里要注意任务在其生命周期中是不能改所属进程的. threadPriQueueList线程的调度队列数组,一共32个,任务和进程一样有32个优先级,调度算法的过程是先找到优先级最高的进程,在从该进程的任务队列里去最高的优先级任务运行. waitList 是等待子进程消亡的任务链表,注意上面挂的是任务.任务是通过系统调用 pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options); 将任务挂到waitList上.鸿蒙waitpid系统调用为SysWait,稍后会讲. 进程正常死亡过程 一个进程的自然消亡过程如下 //一个进程的自然消亡过程,参数是当前运行的任务 STATIC VOID OsProcessNaturalExit(LosTaskCB *runTask, UINT32 status) { LosProcessCB *processCB = OS_PCB_FROM_PID(runTask->processID);//通过task找到所属PCB LosProcessCB *parentCB = NULL; LOS_ASSERT(!(processCB->threadScheduleMap != 0));//断言没有任务需要调度了,当前task是最后一个了 LOS_ASSERT(processCB->processStatus & OS_PROCESS_STATUS_RUNNING);//断言必须为正在运行的进程 OsChildProcessResourcesFree(processCB);//释放孩子进程的资源 #ifdef LOSCFG_KERNEL_CPUP OsCpupClean(processCB->processID); #endif /* is a child process */ if (processCB->parentProcessID != OS_INVALID_VALUE) {//判断是否有父进程 parentCB = OS_PCB_FROM_PID(processCB->parentProcessID);//获取父进程实体 LOS_ListDelete(&processCB->siblingList);//将自己从兄弟链表中摘除,家人们,永别了! if (!OsProcessExitCodeSignalIsSet(processCB)) {//是否设置了退出码? OsProcessExitCodeSet(processCB, status);//将进程状态设为退出码 } LOS_ListTailInsert(&parentCB->exitChildList, &processCB->siblingList);//挂到父进程的孩子消亡链表,家人中,永别的可不止我一个. LOS_ListDelete(&processCB->subordinateGroupList);//和志同道合的朋友们永别了,注意家里可不一定是朋友的,所有各有链表. LOS_ListTailInsert(&processCB->group->exitProcessList, &processCB->subordinateGroupList);//挂到进程组消亡链表,朋友中,永别的可不止我一个. OsWaitCheckAndWakeParentProcess(parentCB, processCB);//检查父进程的等待任务并唤醒任务,此处将会切换到其他任务运行. OsDealAliveChildProcess(processCB);//老父亲临终向各自的祖宗托孤 processCB->processStatus |= OS_PROCESS_STATUS_ZOMBIES;//贴上僵死进程的标签 (VOID)OsKill(processCB->parentProcessID, SIGCHLD, OS_KERNEL_KILL_PERMISSION);//以内核权限发送SIGCHLD(子进程退出)信号. LOS_ListHeadInsert(&g_processRecyleList, &processCB->pendList);//将进程通过其阻塞节点挂入全局进程回收链表 OsRunTaskToDelete(runTask);//删除正在运行的任务 return; } LOS_Panic("pid : %u is the root process exit!\n", processCB->processID); return; } 解读 退群,向兄弟姐妹siblingList告别,向朋友圈(进程组)告别subordinateGroupList. 留下你的死亡记录,老父亲记录到exitChildList,朋友圈记录到exitProcessList中. 告诉后人死亡原因OsProcessExitCodeSet,因为waitList上挂的任务在等待你的死亡信息. 向老祖宗托孤,用户态和内核态进程都有自己的祖宗进程(1和2号进程),老祖宗身子硬朗,最后死.所有的短命鬼进程都可以把自己的孩子委托给老祖宗照顾,老祖宗会一视同仁. 将自己变成了OS_PROCESS_STATUS_ZOMBIES僵尸进程. 老父亲跑到村口广播这个孩子已经死亡的信号OsKill. 将自己挂入进程回收链表,等待回收任务ResourcesTask回收资源. 最后删除这个正在运行的任务,很明显其中一定会发生一次调度OsSchedResched. //删除一个正在运行的任务 LITE_OS_SEC_TEXT VOID OsRunTaskToDelete(LosTaskCB *taskCB) { LosProcessCB *processCB = OS_PCB_FROM_PID(taskCB->processID);//拿到task所属进程 OsTaskReleaseHoldLock(processCB, taskCB);//task还锁 OsTaskStatusUnusedSet(taskCB);//task重置为未使用状态,等待回收 LOS_ListDelete(&taskCB->threadList);//从进程的线程链表中将自己摘除 processCB->threadNumber--;//进程的活动task --,注意进程还有一个记录总task的变量 processCB->threadCount LOS_ListTailInsert(&g_taskRecyleList, &taskCB->pendList);//将task插入回收链表,等待回收资源再利用 OsEventWriteUnsafe(&g_resourceEvent, OS_RESOURCE_EVENT_FREE, FALSE, NULL);//发送释放资源的事件,事件由 OsResourceRecoveryTask 消费 OsSchedResched();//申请调度 return; } 但这是一个自然死亡的进程,还有很多非正常死亡在其他篇幅中已有说明.请自行翻看.非正常死亡的会产生僵尸进程.这种进程需要别的进程通过 waitpid来回收. 孤儿进程 一般情况下往往是白发人送黑发人,子进程的生命周期是要短于父进程.但因为fork之后,进程之间相互独立,调度算法一视同仁,父子之间是弱的关系力,就什么情况都可能发生了.内核是允许老父亲先走的,如果父进程退出而它的一个或多个子进程还在运行,那么这些子进程就被称为孤儿进程,孤儿进程最终将被两位老祖宗(用户态和内核态)所收养,并由老祖宗完成对它们的状态收集工作。 //当一个进程自然退出的时候,它的孩子进程由两位老祖宗收养 STATIC VOID OsDealAliveChildProcess(LosProcessCB *processCB) { UINT32 parentID; LosProcessCB *childCB = NULL; LosProcessCB *parentCB = NULL; LOS_DL_LIST *nextList = NULL; LOS_DL_LIST *childHead = NULL; if (!LOS_ListEmpty(&processCB->childrenList)) {//如果存在孩子进程 childHead = processCB->childrenList.pstNext;//获取孩子链表 LOS_ListDelete(&(processCB->childrenList));//清空自己的孩子链表 if (OsProcessIsUserMode(processCB)) {//是用户态进程 parentID = g_userInitProcess;//用户态进程老祖宗 } else { parentID = g_kernelInitProcess;//内核态进程老祖宗 } for (nextList = childHead; ;) {//遍历孩子链表 childCB = OS_PCB_FROM_SIBLIST(nextList);//找到孩子的真身 childCB->parentProcessID = parentID;//孩子磕头认老祖宗为爸爸 nextList = nextList->pstNext;//找下一个孩子进程 if (nextList == childHead) {//一圈下来,孩子们都磕完头了 break; } } parentCB = OS_PCB_FROM_PID(parentID);//找个老祖宗的真身 LOS_ListTailInsertList(&parentCB->childrenList, childHead);//挂到老祖宗的孩子链表上 } return; } 解读 函数很简单,都一一注释了,老父亲临终托付后事,请各自的老祖宗照顾孩子. 从这里也可以看出进程的家族管理模式,两个家族从进程的出生到死亡负责到底. 僵尸进程 一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。 如果一个进程已经终止,但是它的父进程尚未调用wait或waitpid对它进行清理,这时的进程状态称为僵尸(Zombie)进程,即 Z 进程.任何进程在刚终止时都是僵尸进程,正常情况下,僵尸进程都立刻被父进程清理了. 不正常情况下就需要手动waitpid清理了. waitpid 在鸿蒙系统中,一个进程结束了,但是它的父进程没有等待(调用wait waitpid)它,那么它将变成一个僵尸进程。通过系统调用 waitpid可以彻底的清理掉子进程.归还pcb.最终调用到SysWait #include <sys/wait.h> #include "syscall.h" pid_t waitpid(pid_t pid, int *status, int options) { return syscall_cp(SYS_wait4, pid, status, options, 0); } //等待子进程结束 int SysWait(int pid, USER int *status, int options, void *rusage) { (void)rusage; return LOS_Wait(pid, status, (unsigned int)options, NULL); } //返回已经终止的子进程的进程ID号,并清除僵死进程。 LITE_OS_SEC_TEXT INT32 LOS_Wait(INT32 pid, USER INT32 *status, UINT32 options, VOID *rusage) { (VOID)rusage; UINT32 ret; UINT32 intSave; LosProcessCB *childCB = NULL; LosProcessCB *processCB = NULL; LosTaskCB *runTask = NULL; ret = OsWaitOptionsCheck(options);//参数检查,只支持LOS_WAIT_WNOHANG if (ret != LOS_OK) { return -ret; } SCHEDULER_LOCK(intSave); processCB = OsCurrProcessGet(); //获取当前进程 runTask = OsCurrTaskGet(); //获取当前任务 ret = OsWaitChildProcessCheck(processCB, pid, &childCB);//先检查下看能不能找到参数要求的退出子进程 if (ret != LOS_OK) { pid = -ret; goto ERROR; } if (childCB != NULL) {//找到了进程 return OsWaitRecycleChildPorcess(childCB, intSave, status);//回收进程 } //没有找到,看是否要返回还是去做个登记 if ((options & LOS_WAIT_WNOHANG) != 0) {//有LOS_WAIT_WNOHANG标签 runTask->waitFlag = 0;//等待标识置0 pid = 0;//这里置0,是为了 return 0 goto ERROR; } //等待孩子进程退出 OsWaitInsertWaitListInOrder(runTask, processCB);//将当前任务挂入进程waitList链表 //发起调度的目的是为了让出CPU,让其他进程/任务运行 OsSchedResched();//发起调度 runTask->waitFlag = 0; if (runTask->waitID == OS_INVALID_VALUE) { pid = -LOS_ECHILD;//没有此子进程 goto ERROR; } childCB = OS_PCB_FROM_PID(runTask->waitID);//获取当前任务的等待子进程ID if (!(childCB->processStatus & OS_PROCESS_STATUS_ZOMBIES)) {//子进程非僵死进程 pid = -LOS_ESRCH;//没有此进程 goto ERROR; } //回收僵死进程 return OsWaitRecycleChildPorcess(childCB, intSave, status); ERROR: SCHEDULER_UNLOCK(intSave); return pid; } 解读 pid是数据参数,根据不同的参数代表不同的含义,含义如下: 参数值 说明 pid<-1 等待进程组号为pid绝对值的任何子进程。 pid=-1 等待任何子进程,此时的waitpid()函数就退化成了普通的wait()函数。 pid=0 等待进程组号与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程。 pid>0 等待进程号为pid的子进程。 pid不同值代表的真正含义可以看这个函数OsWaitSetFlag. //设置等待子进程退出方式方法 STATIC UINT32 OsWaitSetFlag(const LosProcessCB *processCB, INT32 pid, LosProcessCB **child) { LosProcessCB *childCB = NULL; ProcessGroup *group = NULL; LosTaskCB *runTask = OsCurrTaskGet(); UINT32 ret; if (pid > 0) {//等待进程号为pid的子进程结束 /* Wait for the child process whose process number is pid. */ childCB = OsFindExitChildProcess(processCB, pid);//看能否从退出的孩子链表中找到PID if (childCB != NULL) {//找到了,确实有一个已经退出的PID,注意一个进程退出时会挂到父进程的exitChildList上 goto WAIT_BACK;//直接成功返回 } ret = OsFindChildProcess(processCB, pid);//看能否从现有的孩子链表中找到PID if (ret != LOS_OK) { return LOS_ECHILD;//参数进程并没有这个PID孩子,返回孩子进程失败. } runTask->waitFlag = OS_PROCESS_WAIT_PRO;//设置当前任务的等待类型 runTask->waitID = pid; //当前任务要等待进程ID结束 } else if (pid == 0) {//等待同一进程组中的任何子进程 /* Wait for any child process in the same process group */ childCB = OsFindGroupExitProcess(processCB->group, OS_INVALID_VALUE);//看能否从退出的孩子链表中找到PID if (childCB != NULL) {//找到了,确实有一个已经退出的PID goto WAIT_BACK;//直接成功返回 } runTask->waitID = processCB->group->groupID;//等待进程组的任意一个子进程结束 runTask->waitFlag = OS_PROCESS_WAIT_GID;//设置当前任务的等待类型 } else if (pid == -1) {//等待任意子进程 /* Wait for any child process */ childCB = OsFindExitChildProcess(processCB, OS_INVALID_VALUE);//看能否从退出的孩子链表中找到PID if (childCB != NULL) {//找到了,确实有一个已经退出的PID goto WAIT_BACK; } runTask->waitID = pid;//等待PID,这个PID可以和当前进程没有任何关系 runTask->waitFlag = OS_PROCESS_WAIT_ANY;//设置当前任务的等待类型 } else { /* pid < -1 */ //等待指定进程组内为|pid|的所有子进程 /* Wait for any child process whose group number is the pid absolute value. */ group = OsFindProcessGroup(-pid);//先通过PID找到进程组 if (group == NULL) { return LOS_ECHILD; } childCB = OsFindGroupExitProcess(group, OS_INVALID_VALUE);//在进程组里任意一个已经退出的子进程 if (childCB != NULL) { goto WAIT_BACK; } runTask->waitID = -pid;//此处用负数是为了和(pid == 0)以示区别,因为二者的waitFlag都一样. runTask->waitFlag = OS_PROCESS_WAIT_GID;//设置当前任务的等待类型 } WAIT_BACK: *child = childCB; return LOS_OK; } status带走进程退出码,exitCode分成了三个部分格式如下 /* * Process exit code * 31 15 8 7 0 * | | exit code | core dump | signal | */ #define OS_PRO_EXIT_OK 0 //进程正常退出 //置进程退出码第七位为1 STATIC INLINE VOID OsProcessExitCodeCoreDumpSet(LosProcessCB *processCB) { processCB->exitCode |= 0x80U;// 0b10000000 } //设置进程退出信号(0 ~ 7) STATIC INLINE VOID OsProcessExitCodeSignalSet(LosProcessCB *processCB, UINT32 signal) { processCB->exitCode |= signal & 0x7FU;//0b01111111 } //清除进程退出信号(0 ~ 7) STATIC INLINE VOID OsProcessExitCodeSignalClear(LosProcessCB *processCB) { processCB->exitCode &= (~0x7FU);//低7位全部清0 } //进程退出码是否被设置过,默认是 0 ,如果 & 0x7FU 还是 0 ,说明没有被设置过. STATIC INLINE BOOL OsProcessExitCodeSignalIsSet(LosProcessCB *processCB) { return (processCB->exitCode) & 0x7FU; } //设置进程退出号(8 ~ 15) STATIC INLINE VOID OsProcessExitCodeSet(LosProcessCB *processCB, UINT32 code) { processCB->exitCode |= ((code & 0x000000FFU) << 8U) & 0x0000FF00U; /* 8: Move 8 bits to the left, exitCode */ } 0 - 7为信号位,信号处理有专门的篇幅,此处不做详细介绍,请自行翻看,这里仅列出部分信号含义. #define SIGHUP 1 //终端挂起或者控制进程终止 #define SIGINT 2 //键盘中断(如break键被按下) #define SIGQUIT 3 //键盘的退出键被按下 #define SIGILL 4 //非法指令 #define SIGTRAP 5 //跟踪陷阱(trace trap),启动进程,跟踪代码的执行 #define SIGABRT 6 //由abort(3)发出的退出指令 #define SIGIOT SIGABRT //abort发出的信号 #define SIGBUS 7 //总线错误 #define SIGFPE 8 //浮点异常 #define SIGKILL 9 //常用的命令 kill 9 123 | 不能被忽略、处理和阻塞 #define SIGUSR1 10 //用户自定义信号1 #define SIGSEGV 11 //无效的内存引用, 段违例(segmentation violation),进程试图去访问其虚地址空间以外的位置 #define SIGUSR2 12 //用户自定义信号2 #define SIGPIPE 13 //向某个非读管道中写入数据 #define SIGALRM 14 //由alarm(2)发出的信号,默认行为为进程终止 #define SIGTERM 15 //终止信号 #define SIGSTKFLT 16 //栈溢出 #define SIGCHLD 17 //子进程结束信号 #define SIGCONT 18 //进程继续(曾被停止的进程) #define SIGSTOP 19 //终止进程 | 不能被忽略、处理和阻塞 #define SIGTSTP 20 //控制终端(tty)上 按下停止键 #define SIGTTIN 21 //进程停止,后台进程企图从控制终端读 #define SIGTTOU 22 //进程停止,后台进程企图从控制终端写 #define SIGURG 23 //I/O有紧急数据到达当前进程 #define SIGXCPU 24 //进程的CPU时间片到期 #define SIGXFSZ 25 //文件大小的超出上限 #define SIGVTALRM 26 //虚拟时钟超时 #define SIGPROF 27 //profile时钟超时 #define SIGWINCH 28 //窗口大小改变 #define SIGIO 29 //I/O相关 #define SIGPOLL 29 // #define SIGPWR 30 //电源故障,关机 #define SIGSYS 31 //系统调用中参数错,如系统调用号非法 #define SIGUNUSED SIGSYS //系统调用异常 options是行为参数,提供了一些另外的选项来控制waitpid()函数的行为。 参数值 鸿蒙支持 说明 LOS_WAIT_WNOHANG 支持 如果没有孩子进程退出,则立即返回,而不是阻塞在这个函数上等待;如果结束了,则返回该子进程的进程号。 LOS_WAIT_WUNTRACED 不支持 报告终止或停止的子进程的状态 LOS_WAIT_WCONTINUED 不支持 鸿蒙目前只支持了LOS_WAIT_WNOHANG模式,内核源码中虽有LOS_WAIT_WUNTRACED和LOS_WAIT_WCONTINUED的实现痕迹,但是整体阅读下来比较乱,应该是没有写好. 鸿蒙源码百篇博客 往期回顾 v47.xx (进程回收篇) | 进程在临终前如何向老祖宗托孤 < csdn | 51cto | harmony > v46.xx (特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 < csdn | 51cto | harmony > v45.xx (fork篇) | fork是如何做到调用一次,返回两次的 ? < csdn | 51cto | harmony > v44.xx (中断管理篇) | 硬中断的实现<>观察者模式 < csdn | 51cto | harmony > v43.xx (中断概念篇) | 外人眼中权势滔天的当红海公公 < csdn | 51cto | harmony > v42.xx (中断切换篇) | 中断切换到底在切换什么? < csdn | 51cto | harmony > v41.xx (任务切换篇) | 汇编逐行注解分析任务上下文 < csdn | 51cto | harmony > v40.xx (汇编汇总篇) | 所有的汇编代码都在这里 < csdn | 51cto | harmony > v39.xx (异常接管篇) | 社会很单纯,复杂的是人 < csdn | 51cto | harmony > v38.xx (寄存器篇) | ARM所有寄存器一网打尽,不再神秘 < csdn | 51cto | harmony > v37.xx (系统调用篇) | 全盘解剖系统调用实现过程 < csdn | 51cto | harmony > v36.xx (工作模式篇) | CPU是韦小宝,有哪七个老婆? < csdn | 51cto | harmony > v35.xx (时间管理篇) | Tick是操作系统的基本时间单位 < csdn | 51cto | harmony > v34.xx (原子操作篇) | 是谁在为原子操作保驾护航? < csdn | 51cto | harmony > v33.xx (消息队列篇) | 进程间如何异步解耦传递大数据 ? < csdn | 51cto | harmony > v32.xx (CPU篇) | 内核是如何描述CPU的? < csdn | 51cto | harmony > v31.xx (定时器篇) | 内核最高优先级任务是谁? < csdn | 51cto | harmony > v30.xx (事件控制篇) | 任务间多对多的同步方案 < csdn | 51cto | harmony > v29.xx (信号量篇) | 信号量解决任务同步问题 < csdn | 51cto | harmony > v28.xx (进程通讯篇) | 进程间通讯有哪九大方式? < csdn | 51cto | harmony > v27.xx (互斥锁篇) | 互斥锁比自旋锁可丰满许多 < csdn | 51cto | harmony > v26.xx (自旋锁篇) | 真的好想为自旋锁立贞节牌坊! < csdn | 51cto | harmony > v25.xx (并发并行篇) | 怎么记住并发并行的区别? < csdn | 51cto | harmony > v24.xx (进程概念篇) | 进程在管理哪些资源? < csdn | 51cto | harmony > v23.xx (汇编传参篇) | 汇编如何传递复杂的参数? < csdn | 51cto | harmony > v22.xx (汇编基础篇) | CPU在哪里打卡上班? < csdn | 51cto | harmony > v21.xx (线程概念篇) | 是谁在不断的折腾CPU? < csdn | 51cto | harmony > v20.xx (用栈方式篇) | 栈是构建底层运行的基础 < csdn | 51cto | harmony > v19.xx (位图管理篇) | 为何进程和线程优先级都是32个? < csdn | 51cto | harmony > v18.xx (源码结构篇) | 内核500问你能答对多少? < csdn | 51cto | harmony > v17.xx (物理内存篇) | 这样记伙伴算法永远不会忘 < csdn | 51cto | harmony > v16.xx (内存规则篇) | 内存管理到底在管什么? < csdn | 51cto | harmony > v15.xx (内存映射篇) | 什么是内存最重要的实现基础 ? < csdn | 51cto | harmony > v14.xx (内存汇编篇) | 什么是虚拟内存的实现基础? < csdn | 51cto | harmony > v13.xx (源码注释篇) | 热爱是所有的理由和答案 < csdn | 51cto | harmony > v12.xx (内存管理篇) | 虚拟内存全景图是怎样的? < csdn | 51cto | harmony > v11.xx (内存分配篇) | 内存有哪些分配方式? < csdn | 51cto | harmony > v10.xx (内存主奴篇) | 紫禁城的主子和奴才如何相处? < csdn | 51cto | harmony > v09.xx (调度故事篇) | 用故事说内核调度 < csdn | 51cto | harmony > v08.xx (总目录) | 百万汉字注解 百篇博客分析 < csdn | 51cto | harmony > v07.xx (调度机制篇) | 任务是如何被调度执行的? < csdn | 51cto | harmony > v06.xx (调度队列篇) | 就绪队列对调度的作用 < csdn | 51cto | harmony > v05.xx (任务管理篇) | 谁在让CPU忙忙碌碌? < csdn | 51cto | harmony > v04.xx (任务调度篇) | 任务是内核调度的单元 < csdn | 51cto | harmony > v03.xx (时钟任务篇) | 触发调度最大的动力来自哪里? < csdn | 51cto | harmony > v02.xx (进程管理篇) | 进程是内核资源管理单元 < csdn | 51cto | harmony > v01.xx (双向链表篇) | 谁是内核最重要结构体? < csdn | 51cto | harmony > 参与贡献 Fork 本仓库 >> 新建 Feat_xxx 分支 >> 提交代码注解 >> 新建 Pull Request 新建 Issue 喜欢请「点赞+关注+收藏」 关注「鸿蒙内核源码分析」公众号 各大站点搜 「鸿蒙内核源码分析」.欢迎转载,请注明出处. 进入 >> oschina | csdn | 51cto | 简书 | 掘金 | harmony

优秀的个人博客,低调大师

鸿蒙内核源码分析(特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 | 百篇博客分析HarmonyOS源码 | v46.02

百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< oschina | csdn | harmony > 三个进程 鸿蒙有三个特殊的进程,创建顺序如下: 2号进程,KProcess,为内核态根进程.启动过程中创建. 0号进程,KIdle为内核态第二个进程,它是通过KProcess fork 而来的.这有点难理解. 1号进程,init,为用户态根进程.由任务SystemInit创建. 发现没有在图中看不到0号进程,在看完本篇之后请想想为什么? 家族式管理 进程(process)是家族式管理,总体分为两大家族,用户态家族和内核态家族. 用户态的进程是平民阶层,干着各行各业的活,权利有限,人数众多,活动范围有限.中南海肯定不能随便进出.这个阶层有个共同的老祖宗g_userInitProcess (1号进程). g_userInitProcess = 1; /* 1: The root process ID of the user-mode process is fixed at 1 *///用户态的根进程 //获取用户态进程的根进程,所有用户进程都是g_processCBArray[g_userInitProcess] fork来的 LITE_OS_SEC_TEXT UINT32 OsGetUserInitProcessID(VOID) { return g_userInitProcess; } 内核态的进程是贵族阶层,管理平民阶层的,维持平民生活秩序的,拥有超级权限,人数不多.这个阶层老祖宗是 g_kernelInitProcess(2号进程). g_kernelInitProcess = 2; /* 2: The root process ID of the kernel-mode process is fixed at 2 *///内核态的根进程 //获取内核态进程的根进程,所有内核进程都是g_processCBArray[g_kernelInitProcess] fork来的,包括g_processCBArray[g_kernelIdleProcess]进程 LITE_OS_SEC_TEXT UINT32 OsGetKernelInitProcessID(VOID) { return g_kernelInitProcess; } 这两个阶层可以相互流动吗,有没有可以通过高考改变命运的机会? 答案是: 绝对不可能!!! 龙生龙,凤生凤,老鼠生儿会打洞.从老祖宗创建的那一刻起就被刻在基因里了,抹不掉了. 因为所有的进程都是由这两位老同志克隆(clone)来的,继承了这份基因.LosProcessCB有专门的标签来processMode区分这两个阶层.整个鸿蒙内核源码并没有提供改变命运机会的set函数. #define OS_KERNEL_MODE 0x0U //内核态 #define OS_USER_MODE 0x1U //用户态 STATIC INLINE BOOL OsProcessIsUserMode(const LosProcessCB *processCB)//用户模式进程 { return (processCB->processMode == OS_USER_MODE); } typedef struct ProcessCB { // ... UINT16 processMode; /**< Kernel Mode:0; User Mode:1; */ //模式指定为内核还是用户进程 } LosProcessCB; 2号进程 KProcess 2号进程为内核态的老祖宗,也是内核创建的首个进程,源码过程如下,省略了不相干的代码. bl main @带LR的子程序跳转, LR = pc - 4 ,执行C层main函数 /****************************************************************************** 内核入口函数,由汇编调用,见于reset_vector_up.S 和 reset_vector_mp.S up指单核CPU, mp指多核CPU bl main ******************************************************************************/ LITE_OS_SEC_TEXT_INIT INT32 main(VOID)//由主CPU执行,默认0号CPU 为主CPU { // ... 省略 uwRet = OsMain();// 内核各模块初始化 } LITE_OS_SEC_TEXT_INIT INT32 OsMain(VOID) { // ... ret = OsKernelInitProcess();// 创建内核态根进程 // ... ret = OsSystemInit(); //中间创建了用户态根进程 } //初始化 2号进程,即内核态进程的老祖宗 LITE_OS_SEC_TEXT_INIT UINT32 OsKernelInitProcess(VOID) { LosProcessCB *processCB = NULL; UINT32 ret; ret = OsProcessInit();// 初始化进程模块全部变量,创建各循环双向链表 if (ret != LOS_OK) { return ret; } processCB = OS_PCB_FROM_PID(g_kernelInitProcess);// 以PID方式得到一个进程 ret = OsProcessCreateInit(processCB, OS_KERNEL_MODE, "KProcess", 0);// 初始化进程,最高优先级0,鸿蒙进程一共有32个优先级(0-31) 其中0-9级为内核进程,用户进程可配置的优先级有22个(10-31) if (ret != LOS_OK) { return ret; } processCB->processStatus &= ~OS_PROCESS_STATUS_INIT;// 进程初始化位 置1 g_processGroup = processCB->group;//全局进程组指向了KProcess所在的进程组 LOS_ListInit(&g_processGroup->groupList);// 进程组链表初始化 OsCurrProcessSet(processCB);// 设置为当前进程 return OsCreateIdleProcess();// 创建一个空闲状态的进程 } 解读 main函数在系列篇中会单独讲,请留意自行翻看,它是在开机之初在SVC模式下创建的. 内核态老祖宗的名字叫 KProcess,优先级为最高 0 级."KProcess"进程是长期活跃的,很多重要的任务都会跑在其之下.例如: Swt_Task oom_task system_wq tcpip_thread SendToSer SendToTelnet eth_irq_task TouchEventHandler USB_GIANT_Task 此处不细讲这些任务,在其他篇幅有介绍,但光看名字也能猜个八九,请自行翻看. 紧接着KProcess 以CLONE_FILES的方式 fork了一个 名为KIdle的子进程(0号进程). 内核态的所有进程都来自2号进程这位老同志,子子孙孙,代代相传,形成一颗家族树,和人类的传承所不同的是,它们往往是白发人送黑发人,子孙进程往往都是短命鬼,老祖宗最能活,子孙都死绝了它还在,有些收尸的工作要交给它干. 0 号进程 KIdle 0号进程是内核创建的第二个进程,在OsKernelInitProcess的末尾将2号进程设为当前进程后,紧接着就fork了0号进程.为什么一定要先设置当前进程,因为fork需要一个父进程,而此时系统处于启动阶段,并没有当前进程. 是的,您没有看错.进程是操作系统为方便管理资源而衍生出来的概念,系统并不是非要进程,任务才能运行的. 开机阶段就是啥都没有,默认跑在svc模式下,默认起始地址reset_vector都是由硬件上电后规定的. 进程,线程都是跑起来后慢慢赋予的意义,OsCurrProcessSet是从软件层面赋予了此为当前进程的这个概念.KProcess是内核设置的第一个当前进程. //创建一个名叫"KIdle"的0号进程,给CPU空闲的时候使用 STATIC UINT32 OsCreateIdleProcess(VOID) { UINT32 ret; CHAR *idleName = "Idle"; LosProcessCB *idleProcess = NULL; Percpu *perCpu = OsPercpuGet(); UINT32 *idleTaskID = &perCpu->idleTaskID;//得到CPU的idle task ret = OsCreateResourceFreeTask();// 创建一个资源回收任务,优先级为5 用于回收进程退出时的各种资源 if (ret != LOS_OK) { return ret; } //创建一个名叫"KIdle"的进程,并创建一个idle task,CPU空闲的时候就待在 idle task中等待被唤醒 ret = LOS_Fork(CLONE_FILES, "KIdle", (TSK_ENTRY_FUNC)OsIdleTask, LOSCFG_BASE_CORE_TSK_IDLE_STACK_SIZE); if (ret < 0) {//内核进程的fork并不会一次调用,返回两次,此子进程执行的开始位置是参数OsIdleTask return LOS_NOK; } g_kernelIdleProcess = (UINT32)ret;//返回 0号进程 idleProcess = OS_PCB_FROM_PID(g_kernelIdleProcess);//通过ID拿到进程实体 *idleTaskID = idleProcess->threadGroupID;//绑定CPU的IdleTask,或者说改变CPU现有的idle任务 OS_TCB_FROM_TID(*idleTaskID)->taskStatus |= OS_TASK_FLAG_SYSTEM_TASK;//设定Idle task 为一个系统任务 #if (LOSCFG_KERNEL_SMP == YES) OS_TCB_FROM_TID(*idleTaskID)->cpuAffiMask = CPUID_TO_AFFI_MASK(ArchCurrCpuid());//多核CPU的任务指定,防止乱串了,注意多核才会有并行处理 #endif (VOID)memset_s(OS_TCB_FROM_TID(*idleTaskID)->taskName, OS_TCB_NAME_LEN, 0, OS_TCB_NAME_LEN);//task 名字先清0 (VOID)memcpy_s(OS_TCB_FROM_TID(*idleTaskID)->taskName, OS_TCB_NAME_LEN, idleName, strlen(idleName));//task 名字叫 idle return LOS_OK; } 解读 看过fork篇的可能发现了一个参数, KIdle被创建的方式和通过系统调用创建的方式不一样,一个用的是CLONE_FILES,一个是 CLONE_SIGHAND 具体的创建方式如下: #define CLONE_VM 0x00000100 //子进程与父进程运行于相同的内存空间 #define CLONE_FS 0x00000200 //子进程与父进程共享相同的文件系统,包括root、当前目录、umask #define CLONE_FILES 0x00000400 //子进程与父进程共享相同的文件描述符(file descriptor)表 #define CLONE_SIGHAND 0x00000800 //子进程与父进程共享相同的信号处理(signal handler)表 #define CLONE_PTRACE 0x00002000 //若父进程被trace,子进程也被trace #define CLONE_VFORK 0x00004000 //父进程被挂起,直至子进程释放虚拟内存资源 #define CLONE_PARENT 0x00008000 //创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子” #define CLONE_THREAD 0x00010000 //Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群 KIdle创建了一个名为Idle的任务,任务的入口函数为OsIdleTask,这是个空闲任务,啥也不干的.专门用来给cpu休息的,cpu空闲时就待在这个任务里等活干. LITE_OS_SEC_TEXT WEAK VOID OsIdleTask(VOID) { while (1) {//只有一个死循环 #ifdef LOSCFG_KERNEL_TICKLESS //低功耗模式开关, idle task 中关闭tick if (OsTickIrqFlagGet()) { OsTickIrqFlagSet(0); OsTicklessStart(); } #endif Wfi();//WFI指令:arm core 立即进入low-power standby state,进入休眠模式,等待中断. } } fork 内核态进程和fork 用户态进程有个地方会不一样,就是SP寄存器的值.fork用户态的进程 一次调用两次返回(父子进程各一次),返回的位置一样(是因为拷贝了父进程陷入内核时的上下文).所以只能通过返回值来判断是父还是子返回.这个在fork篇中有详细的描述.请自行翻看. 但fork内核态进程虽也有两次返回,但是返回的位置却不一样,子进程的返回位置是由内核指定的OsIdleTask,即Idle任务的入口函数.详见代码: //任务初始化时拷贝任务信息 STATIC VOID OsInitCopyTaskParam(LosProcessCB *childProcessCB, const CHAR *name, UINTPTR entry, UINT32 size, TSK_INIT_PARAM_S *childPara) { LosTaskCB *mainThread = NULL; UINT32 intSave; SCHEDULER_LOCK(intSave); mainThread = OsCurrTaskGet();//获取当前task,注意变量名从这里也可以看出 thread 和 task 是一个概念,只是内核常说task,上层应用说thread ,概念的映射. if (OsProcessIsUserMode(childProcessCB)) {//用户态进程 childPara->pfnTaskEntry = mainThread->taskEntry;//拷贝当前任务入口地址 childPara->uwStackSize = mainThread->stackSize; //栈空间大小 childPara->userParam.userArea = mainThread->userArea; //用户态栈区栈顶位置 childPara->userParam.userMapBase = mainThread->userMapBase; //用户态栈底 childPara->userParam.userMapSize = mainThread->userMapSize; //用户态栈大小 } else {//注意内核态进程创建任务的入口由外界指定,例如 OsCreateIdleProcess 指定了OsIdleTask childPara->pfnTaskEntry = (TSK_ENTRY_FUNC)entry;//参数(sp)为内核态入口地址 childPara->uwStackSize = size;//参数(size)为内核态栈大小 } childPara->pcName = (CHAR *)name; //拷贝进程名字 childPara->policy = mainThread->policy; //拷贝调度模式 childPara->usTaskPrio = mainThread->priority; //拷贝优先级 childPara->processID = childProcessCB->processID; //拷贝进程ID if (mainThread->taskStatus & OS_TASK_FLAG_PTHREAD_JOIN) { childPara->uwResved = OS_TASK_FLAG_PTHREAD_JOIN; } else if (mainThread->taskStatus & OS_TASK_FLAG_DETACHED) { childPara->uwResved = OS_TASK_FLAG_DETACHED; } SCHEDULER_UNLOCK(intSave); } 结论是创建0号进程中的OsCreateIdleProcess调用LOS_Fork后只会有一次返回.而且返回值为0,因为 g_freeProcess中0号进程还没有被分配.详见代码,注意看最后的注释: //进程模块初始化,被编译放在代码段 .init 中 LITE_OS_SEC_TEXT_INIT UINT32 OsProcessInit(VOID) { UINT32 index; UINT32 size; g_processMaxNum = LOSCFG_BASE_CORE_PROCESS_LIMIT;//默认支持64个进程 size = g_processMaxNum * sizeof(LosProcessCB);//算出总大小 g_processCBArray = (LosProcessCB *)LOS_MemAlloc(m_aucSysMem1, size);// 进程池,占用内核堆,内存池分配 if (g_processCBArray == NULL) { return LOS_NOK; } (VOID)memset_s(g_processCBArray, size, 0, size);//安全方式重置清0 LOS_ListInit(&g_freeProcess);//进程空闲链表初始化,创建一个进程时从g_freeProcess中申请一个进程描述符使用 LOS_ListInit(&g_processRecyleList);//进程回收链表初始化,回收完成后进入g_freeProcess等待再次被申请使用 for (index = 0; index < g_processMaxNum; index++) {//进程池循环创建 g_processCBArray[index].processID = index;//进程ID[0-g_processMaxNum-1]赋值 g_processCBArray[index].processStatus = OS_PROCESS_FLAG_UNUSED;// 默认都是白纸一张,贴上未使用标签 LOS_ListTailInsert(&g_freeProcess, &g_processCBArray[index].pendList);//注意g_freeProcess挂的是pendList节点,所以使用要通过OS_PCB_FROM_PENDLIST找到进程实体. } g_userInitProcess = 1; /* 1: The root process ID of the user-mode process is fixed at 1 *///用户态的根进程 LOS_ListDelete(&g_processCBArray[g_userInitProcess].pendList);// 将1号进程从空闲链表上摘出去 g_kernelInitProcess = 2; /* 2: The root process ID of the kernel-mode process is fixed at 2 *///内核态的根进程 LOS_ListDelete(&g_processCBArray[g_kernelInitProcess].pendList);// 将2号进程从空闲链表上摘出去 //注意:这波骚操作之后,g_freeProcess链表上还有,0,3,4,...g_processMaxNum-1号进程.创建进程是从g_freeProcess上申请 //即下次申请到的将是0号进程,而 OsCreateIdleProcess 将占有0号进程. return LOS_OK; } 1号进程 init 1号进程为用户态的老祖宗.创建过程如下, 省略了不相干的代码. LITE_OS_SEC_TEXT_INIT INT32 OsMain(VOID) { // ... ret = OsKernelInitProcess();// 创建内核态根进程 // ... ret = OsSystemInit(); //中间创建了用户态根进程 } UINT32 OsSystemInit(VOID) { //.. ret = OsSystemInitTaskCreate();//创建了一个系统任务, } STATIC UINT32 OsSystemInitTaskCreate(VOID) { UINT32 taskID; TSK_INIT_PARAM_S sysTask; (VOID)memset_s(&sysTask, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S)); sysTask.pfnTaskEntry = (TSK_ENTRY_FUNC)SystemInit;//任务的入口函数,这个函数实现由外部提供 sysTask.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;//16K sysTask.pcName = "SystemInit";//任务的名称 sysTask.usTaskPrio = LOSCFG_BASE_CORE_TSK_DEFAULT_PRIO;// 内核默认优先级为10 sysTask.uwResved = LOS_TASK_STATUS_DETACHED;//任务分离模式 #if (LOSCFG_KERNEL_SMP == YES) sysTask.usCpuAffiMask = CPUID_TO_AFFI_MASK(ArchCurrCpuid());//cpu 亲和性设置,记录执行过任务的CPU,尽量确保由同一个CPU完成任务周期 #endif return LOS_TaskCreate(&taskID, &sysTask);//创建任务并加入就绪队列,并立即参与调度 } //SystemInit的实现由由外部提供 比如..\vendor\hi3516dv300\module_init\src\system_init.c void SystemInit(void) { // ... if (OsUserInitProcess()) {//创建用户态进程的老祖宗 PRINT_ERR("Create user init process faialed!\n"); return; } } //用户态根进程的创建过程 LITE_OS_SEC_TEXT_INIT UINT32 OsUserInitProcess(VOID) { INT32 ret; UINT32 size; TSK_INIT_PARAM_S param = { 0 }; VOID *stack = NULL; VOID *userText = NULL; CHAR *userInitTextStart = (CHAR *)&__user_init_entry;//代码区开始位置 ,对应 LITE_USER_SEC_ENTRY CHAR *userInitBssStart = (CHAR *)&__user_init_bss;// 未初始化数据区(BSS)。在运行时改变其值 对应 LITE_USER_SEC_BSS CHAR *userInitEnd = (CHAR *)&__user_init_end;// 结束地址 UINT32 initBssSize = userInitEnd - userInitBssStart; UINT32 initSize = userInitEnd - userInitTextStart; LosProcessCB *processCB = OS_PCB_FROM_PID(g_userInitProcess);//"Init进程的优先级是 28" ret = OsProcessCreateInit(processCB, OS_USER_MODE, "Init", OS_PROCESS_USERINIT_PRIORITY);// 初始化用户进程,它将是所有应用程序的父进程 if (ret != LOS_OK) { return ret; } userText = LOS_PhysPagesAllocContiguous(initSize >> PAGE_SHIFT);// 分配连续的物理页 if (userText == NULL) { ret = LOS_NOK; goto ERROR; } (VOID)memcpy_s(userText, initSize, (VOID *)&__user_init_load_addr, initSize);// 安全copy 经加载器load的结果 __user_init_load_addr -> userText ret = LOS_VaddrToPaddrMmap(processCB->vmSpace, (VADDR_T)(UINTPTR)userInitTextStart, LOS_PaddrQuery(userText), initSize, VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE | VM_MAP_REGION_FLAG_PERM_EXECUTE | VM_MAP_REGION_FLAG_PERM_USER);// 虚拟地址与物理地址的映射 if (ret < 0) { goto ERROR; } (VOID)memset_s((VOID *)((UINTPTR)userText + userInitBssStart - userInitTextStart), initBssSize, 0, initBssSize);// 除了代码段,其余都清0 stack = OsUserInitStackAlloc(g_userInitProcess, &size);//分配任务在用户态下的运行栈,大小为1M if (stack == NULL) { PRINTK("user init process malloc user stack failed!\n"); ret = LOS_NOK; goto ERROR; } param.pfnTaskEntry = (TSK_ENTRY_FUNC)userInitTextStart;// 从代码区开始执行,也就是应用程序main 函数的位置 param.userParam.userSP = (UINTPTR)stack + size;// 用户态栈底 param.userParam.userMapBase = (UINTPTR)stack;// 用户态栈顶 param.userParam.userMapSize = size;// 用户态栈大小 param.uwResved = OS_TASK_FLAG_PTHREAD_JOIN;// 可结合的(joinable)能够被其他线程收回其资源和杀死 ret = OsUserInitProcessStart(g_userInitProcess, &param);// 创建一个任务,来运行main函数 if (ret != LOS_OK) { (VOID)OsUnMMap(processCB->vmSpace, param.userParam.userMapBase, param.userParam.userMapSize); goto ERROR; } return LOS_OK; ERROR: (VOID)LOS_PhysPagesFreeContiguous(userText, initSize >> PAGE_SHIFT);//释放物理内存块 OsDeInitPCB(processCB);//删除PCB块 return ret; } 解读 从代码中可以看出用户态的老祖宗创建过程有点意思,首先它的源头和内核态老祖宗一样都在OsMain. 通过创建一个分离模式,优先级为10的系统任务 SystemInit,来完成.任务的入口函数 SystemInit()的实现由平台集成商来指定. 本篇采用了hi3516dv300的实现.也就是说用户态祖宗的创建是在 sysTask.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;//16K 栈中完成的.这个任务归属于内核进程KProcess. 用户态老祖宗的名字叫 Init,优先级为28级. 用户态的每个进程有独立的虚拟进程空间vmSpace,拥有独立的内存映射表(L1,L2表),申请的内存需要重新映射,映射过程在内存系列篇中有详细的说明. init创建了一个任务,任务的入口地址为 __user_init_entry,由编译器指定. 用户态进程是指应有程序运行的进程,通过动态加载ELF文件的方式启动.具体加载流程系列篇有讲解,不细说.用户态进程运行在用户空间,但通过系统调用可陷入内核空间.具体看这张图: 鸿蒙源码百篇博客 往期回顾 v46.xx (特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 < csdn | harmony > v45.xx (fork篇) | fork是如何做到调用一次,返回两次的 ? < csdn | harmony > v44.xx (中断管理篇) | 硬中断的实现<>观察者模式 < csdn | harmony > v43.xx (中断概念篇) | 外人眼中权势滔天的当红海公公 < csdn | harmony > v42.xx (中断切换篇) | 中断切换到底在切换什么? < csdn | harmony > v41.xx (任务切换篇) | 汇编逐行注解分析任务上下文 < csdn | harmony > v40.xx (汇编汇总篇) | 所有的汇编代码都在这里 < csdn | harmony > v39.xx (异常接管篇) | 社会很单纯,复杂的是人 < csdn | harmony > v38.xx (寄存器篇) | ARM所有寄存器一网打尽,不再神秘 < csdn | harmony > v37.xx (系统调用篇) | 全盘解剖系统调用实现过程 < csdn | harmony > v36.xx (工作模式篇) | CPU是韦小宝,有哪七个老婆? < csdn | harmony > v35.xx (时间管理篇) | Tick是操作系统的基本时间单位 < csdn | harmony > v34.xx (原子操作篇) | 是谁在为原子操作保驾护航? < csdn | harmony > v33.xx (消息队列篇) | 进程间如何异步解耦传递大数据 ? < csdn | harmony > v32.xx (CPU篇) | 内核是如何描述CPU的? < csdn | harmony > v31.xx (定时器篇) | 内核最高优先级任务是谁? < csdn | harmony > v30.xx (事件控制篇) | 任务间多对多的同步方案 < csdn | harmony > v29.xx (信号量篇) | 信号量解决任务同步问题 < csdn | harmony > v28.xx (进程通讯篇) | 进程间通讯有哪九大方式? < csdn | harmony > v27.xx (互斥锁篇) | 互斥锁比自旋锁可丰满许多 < csdn | harmony > v26.xx (自旋锁篇) | 真的好想为自旋锁立贞节牌坊! < csdn | harmony > v25.xx (并发并行篇) | 怎么记住并发并行的区别? < csdn | harmony > v24.xx (进程概念篇) | 进程在管理哪些资源? < csdn | harmony > v23.xx (汇编传参篇) | 汇编如何传递复杂的参数? < csdn | harmony > v22.xx (汇编基础篇) | CPU在哪里打卡上班? < csdn | harmony > v21.xx (线程概念篇) | 是谁在不断的折腾CPU? < csdn | harmony > v20.xx (用栈方式篇) | 栈是构建底层运行的基础 < csdn | harmony > v19.xx (位图管理篇) | 为何进程和线程优先级都是32个? < csdn | harmony > v18.xx (源码结构篇) | 内核500问你能答对多少? < csdn | harmony > v17.xx (物理内存篇) | 这样记伙伴算法永远不会忘 < csdn | harmony > v16.xx (内存规则篇) | 内存管理到底在管什么? < csdn | harmony > v15.xx (内存映射篇) | 什么是内存最重要的实现基础 ? < csdn | harmony > v14.xx (内存汇编篇) | 什么是虚拟内存的实现基础? < csdn | harmony > v13.xx (源码注释篇) | 热爱是所有的理由和答案 < csdn | harmony > v12.xx (内存管理篇) | 虚拟内存全景图是怎样的? < csdn | harmony > v11.xx (内存分配篇) | 内存有哪些分配方式? < csdn | harmony > v10.xx (内存主奴篇) | 紫禁城的主子和奴才如何相处? < csdn | harmony > v09.xx (调度故事篇) | 用故事说内核调度 < csdn | harmony > v08.xx (总目录) | 百万汉字注解 百篇博客分析 < csdn | harmony > v07.xx (调度机制篇) | 任务是如何被调度执行的? < csdn | harmony > v06.xx (调度队列篇) | 就绪队列对调度的作用 < csdn | harmony > v05.xx (任务管理篇) | 谁在让CPU忙忙碌碌? < csdn | harmony > v04.xx (任务调度篇) | 任务是内核调度的单元 < csdn | harmony > v03.xx (时钟任务篇) | 触发调度最大的动力来自哪里? < csdn | harmony > v02.xx (进程管理篇) | 进程是内核资源管理单元 < csdn | harmony > v01.xx (双向链表篇) | 谁是内核最重要结构体? < csdn | harmony > 参与贡献 访问注解仓库地址 Fork 本仓库 >> 新建 Feat_xxx 分支 >> 提交代码注解 >> 新建 Pull Request 新建 Issue 喜欢请「点赞+关注+收藏」 关注「鸿蒙内核源码分析」公众号,百万汉字注解 + 百篇博客分析 => 深挖鸿蒙内核源码 各大站点搜 「鸿蒙内核源码分析」 .欢迎转载,请注明出处. oschina | csdn | harmony | 简书 | 掘金

优秀的个人博客,低调大师

鸿蒙内核源码分析(特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 | 百篇博客分析HarmonyOS源码 | v46.01

百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< oschina | csdn | harmony > 三个进程 鸿蒙有三个特殊的进程,创建顺序如下: 2号进程,KProcess,为内核态根进程.启动过程中创建. 0号进程,KIdle为内核态第二个进程,它是通过KProcess fork 而来的.这有点难理解. 1号进程,init,为用户态根进程.由任务SystemInit创建. 发现没有在图中看不到0号进程,在看完本篇之后请想想为什么? 家族式管理 进程(process)是家族式管理,总体分为两大家族,用户态家族和内核态家族. 用户态的进程是平民阶层,干着各行各业的活,权利有限,人数众多,活动范围有限.中南海肯定不能随便进出.这个阶层有个共同的老祖宗g_userInitProcess (1号进程). g_userInitProcess = 1; /* 1: The root process ID of the user-mode process is fixed at 1 *///用户态的根进程 //获取用户态进程的根进程,所有用户进程都是g_processCBArray[g_userInitProcess] fork来的 LITE_OS_SEC_TEXT UINT32 OsGetUserInitProcessID(VOID) { return g_userInitProcess; } 内核态的进程是贵族阶层,管理平民阶层的,维持平民生活秩序的,拥有超级权限,人数不多.这个阶层老祖宗是 g_kernelInitProcess(2号进程). g_kernelInitProcess = 2; /* 2: The root process ID of the kernel-mode process is fixed at 2 *///内核态的根进程 //获取内核态进程的根进程,所有内核进程都是g_processCBArray[g_kernelInitProcess] fork来的,包括g_processCBArray[g_kernelIdleProcess]进程 LITE_OS_SEC_TEXT UINT32 OsGetKernelInitProcessID(VOID) { return g_kernelInitProcess; } 这两个阶层可以相互流动吗,有没有可以通过高考改变命运的机会? 答案是: 绝对不可能!!! 龙生龙,凤生凤,老鼠生儿会打洞.从老祖宗创建的那一刻起就被刻在基因里了,抹不掉了. 因为所有的进程都是由这两位老同志克隆(clone)来的,继承了这份基因.LosProcessCB有专门的标签来processMode区分这两个阶层.整个鸿蒙内核源码并没有提供改变命运机会的set函数. #define OS_KERNEL_MODE 0x0U //内核态 #define OS_USER_MODE 0x1U //用户态 STATIC INLINE BOOL OsProcessIsUserMode(const LosProcessCB *processCB)//用户模式进程 { return (processCB->processMode == OS_USER_MODE); } typedef struct ProcessCB { // ... UINT16 processMode; /**< Kernel Mode:0; User Mode:1; */ //模式指定为内核还是用户进程 } LosProcessCB; 2号进程 KProcess 2号进程为内核态的老祖宗,也是内核创建的第一个进程,源码过程如下,省略了不相干的代码. bl main @带LR的子程序跳转, LR = pc - 4 ,执行C层main函数 /****************************************************************************** 内核入口函数,由汇编调用,见于reset_vector_up.S 和 reset_vector_mp.S up指单核CPU, mp指多核CPU bl main ******************************************************************************/ LITE_OS_SEC_TEXT_INIT INT32 main(VOID)//由主CPU执行,默认0号CPU 为主CPU { // ... 省略 uwRet = OsMain();// 内核各模块初始化 } LITE_OS_SEC_TEXT_INIT INT32 OsMain(VOID) { // ... ret = OsKernelInitProcess();// 创建内核态根进程 // ... ret = OsSystemInit(); //中间创建了用户态根进程 } //初始化 2号进程,即内核态进程的老祖宗 LITE_OS_SEC_TEXT_INIT UINT32 OsKernelInitProcess(VOID) { LosProcessCB *processCB = NULL; UINT32 ret; ret = OsProcessInit();// 初始化进程模块全部变量,创建各循环双向链表 if (ret != LOS_OK) { return ret; } processCB = OS_PCB_FROM_PID(g_kernelInitProcess);// 以PID方式得到一个进程 ret = OsProcessCreateInit(processCB, OS_KERNEL_MODE, "KProcess", 0);// 初始化进程,最高优先级0,鸿蒙进程一共有32个优先级(0-31) 其中0-9级为内核进程,用户进程可配置的优先级有22个(10-31) if (ret != LOS_OK) { return ret; } processCB->processStatus &= ~OS_PROCESS_STATUS_INIT;// 进程初始化位 置1 g_processGroup = processCB->group;//全局进程组指向了KProcess所在的进程组 LOS_ListInit(&g_processGroup->groupList);// 进程组链表初始化 OsCurrProcessSet(processCB);// 设置为当前进程 return OsCreateIdleProcess();// 创建一个空闲状态的进程 } 解读 main函数在系列篇中会单独讲,请留意自行翻看,它是在开机之初在SVC模式下创建的. 内核态老祖宗的名字叫 KProcess,优先级为最高 0 级."KProcess"进程是长期活跃的,很多重要的任务都会跑在其之下.例如: Swt_Task oom_task system_wq tcpip_thread SendToSer SendToTelnet eth_irq_task TouchEventHandler USB_GIANT_Task 此处不细讲这些任务,在其他篇幅有介绍,但光看名字也能猜个八九,请自行翻看. 紧接着KProcess 以CLONE_FILES的方式 fork了一个 名为"KIdle"的子进程. 内核态的所有进程都来自2号进程这位老同志,子子孙孙,代代相传,形成一颗家族树,和人类的传承所不同的是,它们往往是白发人送黑发人,子孙进程往往都是短命鬼,老祖宗最能活,子孙都死绝了它还在,有些收尸的工作要交给它干. 0 号进程 KIdle 0号进程是内核创建的第一个进程,在OsKernelInitProcess的末尾将2号进程设为当前进程后,紧接着就fork了0号进程.为什么一定要先设置当前进程,因为fork需要一个父进程,而此时系统处于启动阶段,并没有当前进程. 是的,你没有看错.进程是操作系统为方便管理资源而衍生出来的概念,系统并不是非要进程,任务才能运行的. 开机阶段就是啥都没有,默认跑在svc模式下,默认指定了入口地址reset_vector都是由硬件上电后规定的. 进程,线程都是跑起来后慢慢赋予的含义,OsCurrProcessSet是从软件层面赋予了此为当前进程的这个概念.此处是内核设置的第一个当前进程. //创建一个名叫"KIdle"的0号进程,给CPU空闲的时候使用 STATIC UINT32 OsCreateIdleProcess(VOID) { UINT32 ret; CHAR *idleName = "Idle"; LosProcessCB *idleProcess = NULL; Percpu *perCpu = OsPercpuGet(); UINT32 *idleTaskID = &perCpu->idleTaskID;//得到CPU的idle task ret = OsCreateResourceFreeTask();// 创建一个资源回收任务,优先级为5 用于回收进程退出时的各种资源 if (ret != LOS_OK) { return ret; } //创建一个名叫"KIdle"的进程,并创建一个idle task,CPU空闲的时候就待在 idle task中等待被唤醒 ret = LOS_Fork(CLONE_FILES, "KIdle", (TSK_ENTRY_FUNC)OsIdleTask, LOSCFG_BASE_CORE_TSK_IDLE_STACK_SIZE); if (ret < 0) {//内核进程的fork并不会一次调用,返回两次,此子进程执行的开始位置是参数OsIdleTask return LOS_NOK; } g_kernelIdleProcess = (UINT32)ret;//返回 0号进程 idleProcess = OS_PCB_FROM_PID(g_kernelIdleProcess);//通过ID拿到进程实体 *idleTaskID = idleProcess->threadGroupID;//绑定CPU的IdleTask,或者说改变CPU现有的idle任务 OS_TCB_FROM_TID(*idleTaskID)->taskStatus |= OS_TASK_FLAG_SYSTEM_TASK;//设定Idle task 为一个系统任务 #if (LOSCFG_KERNEL_SMP == YES) OS_TCB_FROM_TID(*idleTaskID)->cpuAffiMask = CPUID_TO_AFFI_MASK(ArchCurrCpuid());//多核CPU的任务指定,防止乱串了,注意多核才会有并行处理 #endif (VOID)memset_s(OS_TCB_FROM_TID(*idleTaskID)->taskName, OS_TCB_NAME_LEN, 0, OS_TCB_NAME_LEN);//task 名字先清0 (VOID)memcpy_s(OS_TCB_FROM_TID(*idleTaskID)->taskName, OS_TCB_NAME_LEN, idleName, strlen(idleName));//task 名字叫 idle return LOS_OK; } 解读 看过fork篇的可能发现了一个参数, KIdle被创建的方式和通过系统调用创建的方式不一样,一个用的是CLONE_FILES,一个是 CLONE_SIGHAND 具体的创建方式如下: #define CLONE_VM 0x00000100 //子进程与父进程运行于相同的内存空间 #define CLONE_FS 0x00000200 //子进程与父进程共享相同的文件系统,包括root、当前目录、umask #define CLONE_FILES 0x00000400 //子进程与父进程共享相同的文件描述符(file descriptor)表 #define CLONE_SIGHAND 0x00000800 //子进程与父进程共享相同的信号处理(signal handler)表 #define CLONE_PTRACE 0x00002000 //若父进程被trace,子进程也被trace #define CLONE_VFORK 0x00004000 //父进程被挂起,直至子进程释放虚拟内存资源 #define CLONE_PARENT 0x00008000 //创建的子进程的父进程是调用者的父进程,新进程与创建它的进程成了“兄弟”而不是“父子” #define CLONE_THREAD 0x00010000 //Linux 2.4中增加以支持POSIX线程标准,子进程与父进程共享相同的线程群 KIdle创建了一个名为Idle的任务,任务的入口函数为OsIdleTask,这是个空闲任务,啥也不干的.专门用来给cpu休息的,cpu空闲时就待在这个任务里等活干. LITE_OS_SEC_TEXT WEAK VOID OsIdleTask(VOID) { while (1) {//只有一个死循环 #ifdef LOSCFG_KERNEL_TICKLESS //低功耗模式开关, idle task 中关闭tick if (OsTickIrqFlagGet()) { OsTickIrqFlagSet(0); OsTicklessStart(); } #endif Wfi();//WFI指令:arm core 立即进入low-power standby state,等待中断,进入休眠模式。 } } fork 内核态进程和fork 用户态进程有个地方会不一样,就是SP寄存器的值.fork用户态的进程 一次调用两次返回(父子进程各一次),返回的位置一样(是因为拷贝了父进程陷入内核时的上下文).所以只能通过返回值来判断是父还是子返回.这个在fork篇中有详细的描述.请自行翻看. 但fork内核态进程虽也有两次返回,但是返回的位置却不一样,子进程的返回位置是由内核指定的.例如:OsIdleTask就是入口函数.详见代码: //任务初始化时拷贝任务信息 STATIC VOID OsInitCopyTaskParam(LosProcessCB *childProcessCB, const CHAR *name, UINTPTR entry, UINT32 size, TSK_INIT_PARAM_S *childPara) { LosTaskCB *mainThread = NULL; UINT32 intSave; SCHEDULER_LOCK(intSave); mainThread = OsCurrTaskGet();//获取当前task,注意变量名从这里也可以看出 thread 和 task 是一个概念,只是内核常说task,上层应用说thread ,概念的映射. if (OsProcessIsUserMode(childProcessCB)) {//用户态进程 childPara->pfnTaskEntry = mainThread->taskEntry;//拷贝当前任务入口地址 childPara->uwStackSize = mainThread->stackSize; //栈空间大小 childPara->userParam.userArea = mainThread->userArea; //用户态栈区栈顶位置 childPara->userParam.userMapBase = mainThread->userMapBase; //用户态栈底 childPara->userParam.userMapSize = mainThread->userMapSize; //用户态栈大小 } else {//注意内核态进程创建任务的入口由外界指定,例如 OsCreateIdleProcess 指定了OsIdleTask childPara->pfnTaskEntry = (TSK_ENTRY_FUNC)entry;//参数(sp)为内核态入口地址 childPara->uwStackSize = size;//参数(size)为内核态栈大小 } childPara->pcName = (CHAR *)name; //拷贝进程名字 childPara->policy = mainThread->policy; //拷贝调度模式 childPara->usTaskPrio = mainThread->priority; //拷贝优先级 childPara->processID = childProcessCB->processID; //拷贝进程ID if (mainThread->taskStatus & OS_TASK_FLAG_PTHREAD_JOIN) { childPara->uwResved = OS_TASK_FLAG_PTHREAD_JOIN; } else if (mainThread->taskStatus & OS_TASK_FLAG_DETACHED) { childPara->uwResved = OS_TASK_FLAG_DETACHED; } SCHEDULER_UNLOCK(intSave); } 结论是创建0号进程中的OsCreateIdleProcess调用LOS_Fork后只会有一次返回.而且返回值为0,因为 g_freeProcess中0号进程还没有被分配.详见代码,注意看最后的注释: //进程模块初始化,被编译放在代码段 .init 中 LITE_OS_SEC_TEXT_INIT UINT32 OsProcessInit(VOID) { UINT32 index; UINT32 size; g_processMaxNum = LOSCFG_BASE_CORE_PROCESS_LIMIT;//默认支持64个进程 size = g_processMaxNum * sizeof(LosProcessCB);//算出总大小 g_processCBArray = (LosProcessCB *)LOS_MemAlloc(m_aucSysMem1, size);// 进程池,占用内核堆,内存池分配 if (g_processCBArray == NULL) { return LOS_NOK; } (VOID)memset_s(g_processCBArray, size, 0, size);//安全方式重置清0 LOS_ListInit(&g_freeProcess);//进程空闲链表初始化,创建一个进程时从g_freeProcess中申请一个进程描述符使用 LOS_ListInit(&g_processRecyleList);//进程回收链表初始化,回收完成后进入g_freeProcess等待再次被申请使用 for (index = 0; index < g_processMaxNum; index++) {//进程池循环创建 g_processCBArray[index].processID = index;//进程ID[0-g_processMaxNum-1]赋值 g_processCBArray[index].processStatus = OS_PROCESS_FLAG_UNUSED;// 默认都是白纸一张,贴上未使用标签 LOS_ListTailInsert(&g_freeProcess, &g_processCBArray[index].pendList);//注意g_freeProcess挂的是pendList节点,所以使用要通过OS_PCB_FROM_PENDLIST找到进程实体. } g_userInitProcess = 1; /* 1: The root process ID of the user-mode process is fixed at 1 *///用户态的根进程 LOS_ListDelete(&g_processCBArray[g_userInitProcess].pendList);// 将1号进程从空闲链表上摘出去 g_kernelInitProcess = 2; /* 2: The root process ID of the kernel-mode process is fixed at 2 *///内核态的根进程 LOS_ListDelete(&g_processCBArray[g_kernelInitProcess].pendList);// 将2号进程从空闲链表上摘出去 //注意:这波骚操作之后,g_freeProcess链表上还有,0,3,4,...g_processMaxNum-1号进程.创建进程是从g_freeProcess上申请 //即下次申请到的将是0号进程,而 OsCreateIdleProcess 将占有0号进程. return LOS_OK; } 1号进程 init 1号进程为用户态的老祖宗源码过程如下, 省略了不相干的代码. LITE_OS_SEC_TEXT_INIT INT32 OsMain(VOID) { // ... ret = OsKernelInitProcess();// 创建内核态根进程 // ... ret = OsSystemInit(); //中间创建了用户态根进程 } UINT32 OsSystemInit(VOID) { //.. ret = OsSystemInitTaskCreate();//创建了一个系统任务, } STATIC UINT32 OsSystemInitTaskCreate(VOID) { UINT32 taskID; TSK_INIT_PARAM_S sysTask; (VOID)memset_s(&sysTask, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S)); sysTask.pfnTaskEntry = (TSK_ENTRY_FUNC)SystemInit;//任务的入口函数,这个函数实现由外部提供 sysTask.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;//16K sysTask.pcName = "SystemInit";//任务的名称 sysTask.usTaskPrio = LOSCFG_BASE_CORE_TSK_DEFAULT_PRIO;// 内核默认优先级为10 sysTask.uwResved = LOS_TASK_STATUS_DETACHED;//任务分离模式 #if (LOSCFG_KERNEL_SMP == YES) sysTask.usCpuAffiMask = CPUID_TO_AFFI_MASK(ArchCurrCpuid());//cpu 亲和性设置,记录执行过任务的CPU,尽量确保由同一个CPU完成任务周期 #endif return LOS_TaskCreate(&taskID, &sysTask);//创建任务并加入就绪队列,并立即参与调度 } //SystemInit的实现由由外部提供 比如..\vendor\hi3516dv300\module_init\src\system_init.c void SystemInit(void) { // ... if (OsUserInitProcess()) {//创建用户态进程的老祖宗 PRINT_ERR("Create user init process faialed!\n"); return; } } //用户态根进程的创建过程 LITE_OS_SEC_TEXT_INIT UINT32 OsUserInitProcess(VOID) { INT32 ret; UINT32 size; TSK_INIT_PARAM_S param = { 0 }; VOID *stack = NULL; VOID *userText = NULL; CHAR *userInitTextStart = (CHAR *)&__user_init_entry;//代码区开始位置 ,对应 LITE_USER_SEC_ENTRY CHAR *userInitBssStart = (CHAR *)&__user_init_bss;// 未初始化数据区(BSS)。在运行时改变其值 对应 LITE_USER_SEC_BSS CHAR *userInitEnd = (CHAR *)&__user_init_end;// 结束地址 UINT32 initBssSize = userInitEnd - userInitBssStart; UINT32 initSize = userInitEnd - userInitTextStart; LosProcessCB *processCB = OS_PCB_FROM_PID(g_userInitProcess);//"Init进程的优先级是 28" ret = OsProcessCreateInit(processCB, OS_USER_MODE, "Init", OS_PROCESS_USERINIT_PRIORITY);// 初始化用户进程,它将是所有应用程序的父进程 if (ret != LOS_OK) { return ret; } userText = LOS_PhysPagesAllocContiguous(initSize >> PAGE_SHIFT);// 分配连续的物理页 if (userText == NULL) { ret = LOS_NOK; goto ERROR; } (VOID)memcpy_s(userText, initSize, (VOID *)&__user_init_load_addr, initSize);// 安全copy 经加载器load的结果 __user_init_load_addr -> userText ret = LOS_VaddrToPaddrMmap(processCB->vmSpace, (VADDR_T)(UINTPTR)userInitTextStart, LOS_PaddrQuery(userText), initSize, VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE | VM_MAP_REGION_FLAG_PERM_EXECUTE | VM_MAP_REGION_FLAG_PERM_USER);// 虚拟地址与物理地址的映射 if (ret < 0) { goto ERROR; } (VOID)memset_s((VOID *)((UINTPTR)userText + userInitBssStart - userInitTextStart), initBssSize, 0, initBssSize);// 除了代码段,其余都清0 stack = OsUserInitStackAlloc(g_userInitProcess, &size);//分配任务在用户态下的运行栈,大小为1M if (stack == NULL) { PRINTK("user init process malloc user stack failed!\n"); ret = LOS_NOK; goto ERROR; } param.pfnTaskEntry = (TSK_ENTRY_FUNC)userInitTextStart;// 从代码区开始执行,也就是应用程序main 函数的位置 param.userParam.userSP = (UINTPTR)stack + size;// 用户态栈底 param.userParam.userMapBase = (UINTPTR)stack;// 用户态栈顶 param.userParam.userMapSize = size;// 用户态栈大小 param.uwResved = OS_TASK_FLAG_PTHREAD_JOIN;// 可结合的(joinable)能够被其他线程收回其资源和杀死 ret = OsUserInitProcessStart(g_userInitProcess, &param);// 创建一个任务,来运行main函数 if (ret != LOS_OK) { (VOID)OsUnMMap(processCB->vmSpace, param.userParam.userMapBase, param.userParam.userMapSize); goto ERROR; } return LOS_OK; ERROR: (VOID)LOS_PhysPagesFreeContiguous(userText, initSize >> PAGE_SHIFT);//释放物理内存块 OsDeInitPCB(processCB);//删除PCB块 return ret; } 解读 从代码中可以看出用户态的老祖宗创建过程有点意思,首先它的源头和内核态老祖宗一样都在OsMain. 通过创建一个分离模式,优先级为10的系统任务 SystemInit,来完成.任务的入口函数 SystemInit()的实现由平台集成商来指定. 本篇采用了hi3516dv300的实现.也就是说用户态祖宗的创建是在 sysTask.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE;//16K 栈中完成的.这个任务归属于内核进程KProcess. 用户态老祖宗的名字叫 Init,优先级为28级. 用户态的每个进程有独立的虚拟进程空间vmSpace,拥有独立的内存映射表(L1,L2表),申请的内存需要重新映射,映射过程在内存系列篇中有详细的说明. init创建了一个任务,任务的入口地址为 __user_init_entry,由编译器指定. 用户态进程是指应有程序运行的进程,通过动态加载ELF文件的方式启动.具体加载流程系列篇有讲解,不细说.用户态进程运行在用户空间,但通过系统调用可以陷入内核空间.具体看这张图: 鸿蒙源码百篇博客 往期回顾 v46.xx (特殊进程篇) | 龙生龙,凤生凤,老鼠生儿会打洞 < csdn | harmony > v45.xx (fork篇) | fork是如何做到调用一次,返回两次的 ? < csdn | harmony > v44.xx (中断管理篇) | 硬中断的实现<>观察者模式 < csdn | harmony > v43.xx (中断概念篇) | 外人眼中权势滔天的当红海公公 < csdn | harmony > v42.xx (中断切换篇) | 中断切换到底在切换什么? < csdn | harmony > v41.xx (任务切换篇) | 汇编逐行注解分析任务上下文 < csdn | harmony > v40.xx (汇编汇总篇) | 所有的汇编代码都在这里 < csdn | harmony > v39.xx (异常接管篇) | 社会很单纯,复杂的是人 < csdn | harmony > v38.xx (寄存器篇) | ARM所有寄存器一网打尽,不再神秘 < csdn | harmony > v37.xx (系统调用篇) | 全盘解剖系统调用实现过程 < csdn | harmony > v36.xx (工作模式篇) | CPU是韦小宝,有哪七个老婆? < csdn | harmony > v35.xx (时间管理篇) | Tick是操作系统的基本时间单位 < csdn | harmony > v34.xx (原子操作篇) | 是谁在为原子操作保驾护航? < csdn | harmony > v33.xx (消息队列篇) | 进程间如何异步解耦传递大数据 ? < csdn | harmony > v32.xx (CPU篇) | 内核是如何描述CPU的? < csdn | harmony > v31.xx (定时器篇) | 内核最高优先级任务是谁? < csdn | harmony > v30.xx (事件控制篇) | 任务间多对多的同步方案 < csdn | harmony > v29.xx (信号量篇) | 信号量解决任务同步问题 < csdn | harmony > v28.xx (进程通讯篇) | 进程间通讯有哪九大方式? < csdn | harmony > v27.xx (互斥锁篇) | 互斥锁比自旋锁可丰满许多 < csdn | harmony > v26.xx (自旋锁篇) | 真的好想为自旋锁立贞节牌坊! < csdn | harmony > v25.xx (并发并行篇) | 怎么记住并发并行的区别? < csdn | harmony > v24.xx (进程概念篇) | 进程在管理哪些资源? < csdn | harmony > v23.xx (汇编传参篇) | 汇编如何传递复杂的参数? < csdn | harmony > v22.xx (汇编基础篇) | CPU在哪里打卡上班? < csdn | harmony > v21.xx (线程概念篇) | 是谁在不断的折腾CPU? < csdn | harmony > v20.xx (用栈方式篇) | 栈是构建底层运行的基础 < csdn | harmony > v19.xx (位图管理篇) | 为何进程和线程优先级都是32个? < csdn | harmony > v18.xx (源码结构篇) | 内核500问你能答对多少? < csdn | harmony > v17.xx (物理内存篇) | 这样记伙伴算法永远不会忘 < csdn | harmony > v16.xx (内存规则篇) | 内存管理到底在管什么? < csdn | harmony > v15.xx (内存映射篇) | 什么是内存最重要的实现基础 ? < csdn | harmony > v14.xx (内存汇编篇) | 什么是虚拟内存的实现基础? < csdn | harmony > v13.xx (源码注释篇) | 热爱是所有的理由和答案 < csdn | harmony > v12.xx (内存管理篇) | 虚拟内存全景图是怎样的? < csdn | harmony > v11.xx (内存分配篇) | 内存有哪些分配方式? < csdn | harmony > v10.xx (内存主奴篇) | 紫禁城的主子和奴才如何相处? < csdn | harmony > v09.xx (调度故事篇) | 用故事说内核调度 < csdn | harmony > v08.xx (总目录) | 百万汉字注解 百篇博客分析 < csdn | harmony > v07.xx (调度机制篇) | 任务是如何被调度执行的? < csdn | harmony > v06.xx (调度队列篇) | 就绪队列对调度的作用 < csdn | harmony > v05.xx (任务管理篇) | 谁在让CPU忙忙碌碌? < csdn | harmony > v04.xx (任务调度篇) | 任务是内核调度的单元 < csdn | harmony > v03.xx (时钟任务篇) | 触发调度最大的动力来自哪里? < csdn | harmony > v02.xx (进程管理篇) | 进程是内核资源管理单元 < csdn | harmony > v01.xx (双向链表篇) | 谁是内核最重要结构体? < csdn | harmony > 参与贡献 访问注解仓库地址 Fork 本仓库 >> 新建 Feat_xxx 分支 >> 提交代码注解 >> 新建 Pull Request 新建 Issue 喜欢请「点赞+关注+收藏」 关注「鸿蒙内核源码分析」公众号,百万汉字注解 + 百篇博客分析 => 深挖鸿蒙内核源码 各大站点搜 「鸿蒙内核源码分析」 .欢迎转载,请注明出处.

优秀的个人博客,低调大师

鸿蒙内核源码分析(事件控制篇) | 任务间一对多和多对多的同步方案 | 中文注解HarmonyOS源码 | v30.01

百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee | Github | CSDN | Coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< OSCHINA | CSDN | WeHarmony > 本篇说清楚事件(Event) 读本篇之前建议先读鸿蒙内核源码分析(总目录)其他篇. 官方概述 先看官方对事件的描述. 事件(Event)是一种任务间通信的机制,可用于任务间的同步。 多任务环境下,任务之间往往需要同步操作,一个等待即是一个同步。事件可以提供一对多、多对多的同步操作。 一对多同步模型:一个任务等待多个事件的触发。可以是任意一个事件发生时唤醒任务处理事件,也可以是几个事件都发生后才唤醒任务处理事件。 多对多同步模型:多个任务等待多个事件的触发。 鸿蒙提供的事件具有如下特点: 任务通过创建事件控制块来触发事件或等待事件。 事件间相互独立,内部实现为一个32位无符号整型,每一位标识一种事件类型。第25位不可用,因此最多可支持31种事件类型。 事件仅用于任务间的同步,不提供数据传输功能。 多次向事件控制块写入同一事件类型,在被清零前等效于只写入一次。 多个任务可以对同一事件进行读写操作。 支持事件读写超时机制。 再看事件图 注意图中提到了三个概念 事件控制块 事件 任务 接下来结合代码来理解事件模块的实现. 事件控制块长什么样? typedef struct tagEvent { UINT32 uwEventID; /**< Event mask in the event control block,//标识发生的事件类型位,事件ID,每一位标识一种事件类型 indicating the event that has been logically processed. */ LOS_DL_LIST stEventList; /**< Event control block linked list *///读取事件任务链表 } EVENT_CB_S, *PEVENT_CB_S; 简单是简单,就两个变量,如下: uwEventID:用于标识该任务发生的事件类型,其中每一位表示一种事件类型(0表示该事件类型未发生、1表示该事件类型已经发生),一共31种事件类型,第25位系统保留。 stEventList,这又是一个双向链表, 双向链表是内核最重要的结构体, 可前往 鸿蒙内核源码分析(总目录) 查看双向链表篇. LOS_DL_LIST像狗皮膏药一样牢牢的寄生在宿主结构体上stEventList上挂的是所有等待这个事件的任务. 事件控制块<>事件<>任务 三者关系 一定要搞明白这三者的关系,否则搞不懂事件模块是如何运作的. 任务是事件的生产者,通过 LOS_EventWrite,向外部广播发生了XX事件,并唤醒此前已在事件控制块中登记过的要等待XX事件发生的XX任务. 事件控制块EVENT_CB_S 是记录者,只干两件事件: 1.uwEventID按位记录哪些事件发生了,它只是记录,怎么消费它不管的. 2.stEventList记录哪些任务在等待事件,但任务究竟在等待哪些事件它也是不记录的 任务也是消费者,通过 LOS_EventRead消费,只有任务自己清楚要以什么样的方式,消费什么样的事件. 先回顾下任务结构体 LosTaskCB 对事件部分的描述如下: typedef struct { //...去掉不相关的部分 VOID *taskEvent; //和任务发生关系的事件控制块 UINT32 eventMask; //对哪些事件进行屏蔽 UINT32 eventMode; //事件三种模式(LOS_WAITMODE_AND,LOS_WAITMODE_OR,LOS_WAITMODE_CLR) } LosTaskCB; taskEvent 指向的就是 EVENT_CB_S eventMask 屏蔽掉 事件控制块 中的哪些事件 eventMode 以什么样的方式去消费事件,三种读取模式 #define LOS_WAITMODE_AND 4U #define LOS_WAITMODE_OR 2U #define LOS_WAITMODE_CLR 1U 所有事件(LOS_WAITMODE_AND):逻辑与,基于接口传入的事件类型掩码eventMask,只有这些事件都已经发生才能读取成功,否则该任务将阻塞等待或者返回错误码。 任一事件(LOS_WAITMODE_OR):逻辑或,基于接口传入的事件类型掩码eventMask,只要这些事件中有任一种事件发生就可以读取成功,否则该任务将阻塞等待或者返回错误码。 清除事件(LOS_WAITMODE_CLR):这是一种附加读取模式,需要与所有事件模式或任一事件模式结合使用(LOS_WAITMODE_AND | LOS_WAITMODE_CLR或 LOS_WAITMODE_OR | LOS_WAITMODE_CLR)。在这种模式下,当设置的所有事件模式或任一事件模式读取成功后,会自动清除事件控制块中对应的事件类型位。 生成和消费同一个事件的多个任务,可以没有任何关系! 函数列表 事件可应用于多种任务同步场景,在某些同步场景下可替代信号量。 其中读懂 OsEventWrite 和 OsEventRead 就明白了事件模块的实现. 事件初始化 -> LOS_EventInit //初始化一个事件控制块 LITE_OS_SEC_TEXT_INIT UINT32 LOS_EventInit(PEVENT_CB_S eventCB) { UINT32 intSave; intSave = LOS_IntLock();//锁中断 eventCB->uwEventID = 0; //其中每一位表示一种事件类型(0表示该事件类型未发生、1表示该事件类型已经发生) LOS_ListInit(&eventCB->stEventList);//事件链表初始化 LOS_IntRestore(intSave);//恢复中断 return LOS_OK; } 代码解读: 事件是共享资源,所以操作期间不能产生中断. 初始化两个记录者 uwEventID stEventList 事件生产过程 -> OsEventWrite LITE_OS_SEC_TEXT VOID OsEventWriteUnsafe(PEVENT_CB_S eventCB, UINT32 events, BOOL once, UINT8 *exitFlag) { LosTaskCB *resumedTask = NULL; LosTaskCB *nextTask = NULL; BOOL schedFlag = FALSE; eventCB->uwEventID |= events;//对应位贴上标签 if (!LOS_ListEmpty(&eventCB->stEventList)) {//等待事件链表判断,处理等待事件的任务 for (resumedTask = LOS_DL_LIST_ENTRY((&eventCB->stEventList)->pstNext, LosTaskCB, pendList); &resumedTask->pendList != &eventCB->stEventList;) {//循环获取任务链表 nextTask = LOS_DL_LIST_ENTRY(resumedTask->pendList.pstNext, LosTaskCB, pendList);//获取任务实体 if (OsEventResume(resumedTask, eventCB, events)) {//是否恢复任务 schedFlag = TRUE;//任务已加至就绪队列,申请发生一次调度 } if (once == TRUE) {//是否只处理一次任务 break;//退出循环 } resumedTask = nextTask;//检查链表中下一个任务 } } if ((exitFlag != NULL) && (schedFlag == TRUE)) {//是否让外面调度 *exitFlag = 1; } } //写入事件 LITE_OS_SEC_TEXT STATIC UINT32 OsEventWrite(PEVENT_CB_S eventCB, UINT32 events, BOOL once) { UINT32 intSave; UINT8 exitFlag = 0; SCHEDULER_LOCK(intSave); //禁止调度 OsEventWriteUnsafe(eventCB, events, once, &exitFlag);//写入事件 SCHEDULER_UNLOCK(intSave); //允许调度 if (exitFlag == 1) { //需要发生调度 LOS_MpSchedule(OS_MP_CPU_ALL);//通知所有CPU调度 LOS_Schedule();//执行调度 } return LOS_OK; } 代码解读: 给对应位贴上事件标签,eventCB->uwEventID |= events; 注意uwEventID是按位管理的.每个位代表一个事件是否写入,例如 uwEventID = 00010010 代表产生了 1,4 事件 循环从stEventList链表中取出等待这个事件的任务判断是否唤醒任务. OsEventResume //事件恢复,判断是否唤醒任务 LITE_OS_SEC_TEXT STATIC UINT8 OsEventResume(LosTaskCB *resumedTask, const PEVENT_CB_S eventCB, UINT32 events) { UINT8 exitFlag = 0;//是否唤醒 if (((resumedTask->eventMode & LOS_WAITMODE_OR) && ((resumedTask->eventMask & events) != 0)) || ((resumedTask->eventMode & LOS_WAITMODE_AND) && ((resumedTask->eventMask & eventCB->uwEventID) == resumedTask->eventMask))) {//逻辑与 和 逻辑或 的处理 exitFlag = 1; resumedTask->taskEvent = NULL; OsTaskWake(resumedTask);//唤醒任务,加入就绪队列 } return exitFlag; } 3.唤醒任务OsTaskWake只是将任务重新加入就绪队列,需要立即申请一次调度 LOS_Schedule . 事件消费过程 -> OsEventRead LITE_OS_SEC_TEXT STATIC UINT32 OsEventRead(PEVENT_CB_S eventCB, UINT32 eventMask, UINT32 mode, UINT32 timeout, BOOL once) { UINT32 ret; UINT32 intSave; SCHEDULER_LOCK(intSave); ret = OsEventReadImp(eventCB, eventMask, mode, timeout, once);//读事件实现函数 SCHEDULER_UNLOCK(intSave); return ret; } //读取指定事件类型的实现函数,超时时间为相对时间:单位为Tick LITE_OS_SEC_TEXT STATIC UINT32 OsEventReadImp(PEVENT_CB_S eventCB, UINT32 eventMask, UINT32 mode, UINT32 timeout, BOOL once) { UINT32 ret = 0; LosTaskCB *runTask = OsCurrTaskGet(); runTask->eventMask = eventMask; runTask->eventMode = mode; runTask->taskEvent = eventCB;//事件控制块 ret = OsTaskWait(&eventCB->stEventList, timeout, TRUE);//任务进入等待状态,挂入阻塞链表 if (ret == LOS_ERRNO_TSK_TIMEOUT) {//如果返回超时 runTask->taskEvent = NULL; return LOS_ERRNO_EVENT_READ_TIMEOUT; } ret = OsEventPoll(&eventCB->uwEventID, eventMask, mode);//检测事件是否符合预期 return ret; } 代码解读: 事件控制块是给任务使用的, 任务给出读取一个事件的条件 eventMask 告诉系统屏蔽掉这些事件,对屏蔽的事件不感冒. eventMode 已什么样的方式去消费事件,是必须都满足给的条件,还是只满足一个就响应. 条件给完后,自己进入等待状态 OsTaskWait,等待多久 timeout决定,任务自己说了算. OsEventPoll检测事件是否符合预期,啥意思?看下它的代码就知道了 //根据用户传入的事件值、事件掩码及校验模式,返回用户传入的事件是否符合预期 LITE_OS_SEC_TEXT UINT32 OsEventPoll(UINT32 *eventID, UINT32 eventMask, UINT32 mode) { UINT32 ret = 0;//事件是否发生了 LOS_ASSERT(OsIntLocked());//断言不允许中断了 LOS_ASSERT(LOS_SpinHeld(&g_taskSpin));//任务自旋锁 if (mode & LOS_WAITMODE_OR) {//如果模式是读取掩码中任意事件 if ((*eventID & eventMask) != 0) { ret = *eventID & eventMask; //发生了 } } else {//等待全部事件发生 if ((eventMask != 0) && (eventMask == (*eventID & eventMask))) {//必须满足全部事件发生 ret = *eventID & eventMask; //发生了 } } if (ret && (mode & LOS_WAITMODE_CLR)) {//是否清除事件 *eventID = *eventID & ~ret; } return ret; } 编程实例 本实例实现如下流程。 示例中,任务Example_TaskEntry创建一个任务Example_Event,Example_Event读事件阻塞,Example_TaskEntry向该任务写事件。可以通过示例日志中打印的先后顺序理解事件操作时伴随的任务切换。 在任务Example_TaskEntry创建任务Example_Event,其中任务Example_Event优先级高于Example_TaskEntry。 在任务Example_Event中读事件0x00000001,阻塞,发生任务切换,执行任务Example_TaskEntry。 在任务Example_TaskEntry向任务Example_Event写事件0x00000001,发生任务切换,执行任务Example_Event。 Example_Event得以执行,直到任务结束。 Example_TaskEntry得以执行,直到任务结束。 #include "los_event.h" #include "los_task.h" #include "securec.h" /* 任务ID */ UINT32 g_testTaskId; /* 事件控制结构体 */ EVENT_CB_S g_exampleEvent; /* 等待的事件类型 */ #define EVENT_WAIT 0x00000001 /* 用例任务入口函数 */ VOID Example_Event(VOID) { UINT32 ret; UINT32 event; /* 超时等待方式读事件,超时时间为100 ticks, 若100 ticks后未读取到指定事件,读事件超时,任务直接唤醒 */ printf("Example_Event wait event 0x%x \n", EVENT_WAIT); event = LOS_EventRead(&g_exampleEvent, EVENT_WAIT, LOS_WAITMODE_AND, 100); if (event == EVENT_WAIT) { printf("Example_Event,read event :0x%x\n", event); } else { printf("Example_Event,read event timeout\n"); } } UINT32 Example_TaskEntry(VOID) { UINT32 ret; TSK_INIT_PARAM_S task1; /* 事件初始化 */ ret = LOS_EventInit(&g_exampleEvent); if (ret != LOS_OK) { printf("init event failed .\n"); return -1; } /* 创建任务 */ (VOID)memset_s(&task1, sizeof(TSK_INIT_PARAM_S), 0, sizeof(TSK_INIT_PARAM_S)); task1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Event; task1.pcName = "EventTsk1"; task1.uwStackSize = OS_TSK_DEFAULT_STACK_SIZE; task1.usTaskPrio = 5; ret = LOS_TaskCreate(&g_testTaskId, &task1); if (ret != LOS_OK) { printf("task create failed .\n"); return LOS_NOK; } /* 写g_testTaskId 等待事件 */ printf("Example_TaskEntry write event .\n"); ret = LOS_EventWrite(&g_exampleEvent, EVENT_WAIT); if (ret != LOS_OK) { printf("event write failed .\n"); return LOS_NOK; } /* 清标志位 */ printf("EventMask:%d\n", g_exampleEvent.uwEventID); LOS_EventClear(&g_exampleEvent, ~g_exampleEvent.uwEventID); printf("EventMask:%d\n", g_exampleEvent.uwEventID); /* 删除任务 */ ret = LOS_TaskDelete(g_testTaskId); if (ret != LOS_OK) { printf("task delete failed .\n"); return LOS_NOK; } return LOS_OK; } 运行结果 Example_Event wait event 0x1 Example_TaskEntry write event . Example_Event,read event :0x1 EventMask:1 EventMask:0 喜欢就请收藏吧 各大站点搜 "鸿蒙内核源码分析" ,快速找到组织. 百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee | Github | CSDN | Coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< OSCHINA | CSDN | WeHarmony >

优秀的个人博客,低调大师

| 中文注解HarmonyOS源码 | v34.01

百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee | Github | CSDN | Coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< OSCHINA | CSDN | WeHarmony > 本篇说清楚原子操作 读本篇之前建议先读鸿蒙内核源码分析(总目录)系列篇. 基本概念 在支持多任务的操作系统中,修改一块内存区域的数据需要“读取-修改-写入”三个步骤。然而同一内存区域的数据可能同时被多个任务访问,如果在修改数据的过程中被其他任务打断,就会造成该操作的执行结果无法预知。 使用开关中断的方法固然可以保证多任务执行结果符合预期,但这种方法显然会影响系统性能。 ARMv6架构引入了LDREX和STREX指令,以支持对共享存储器更缜密的非阻塞同步。由此实现的原子操作能确保对同一数据的“读取-修改-写入”操作在它的执行期间不会被打断,即操作的原子性。 有多个任务对同一个内存数据进行加减或交换操作时,使用原子操作保证结果的可预知性。 看过鸿蒙内核源码分析(总目录)自旋锁篇的应该对LDREX和STREX指令不陌生的,自旋锁的本质就是对某个变量的原子操作,而且一定要通过汇编代码实现,也就是说LDREX和STREX指令保证了原子操作的底层实现. 回顾下自旋锁申请和释放锁的汇编代码. ArchSpinLock 申请锁代码 FUNCTION(ArchSpinLock) @死守,非要拿到锁 mov r1, #1 @r1=1 1: @循环的作用,因SEV是广播事件.不一定lock->rawLock的值已经改变了 ldrex r2, [r0] @r0 = &lock->rawLock, 即 r2 = lock->rawLock cmp r2, #0 @r2和0比较 wfene @不相等时,说明资源被占用,CPU核进入睡眠状态 strexeq r2, r1, [r0]@此时CPU被重新唤醒,尝试令lock->rawLock=1,成功写入则r2=0 cmpeq r2, #0 @再来比较r2是否等于0,如果相等则获取到了锁 bne 1b @如果不相等,继续进入循环 dmb @用DMB指令来隔离,以保证缓冲中的数据已经落实到RAM中 bx lr @此时是一定拿到锁了,跳回调用ArchSpinLock函数 ArchSpinUnlock 释放锁代码 FUNCTION(ArchSpinUnlock) @释放锁 mov r1, #0 @r1=0 dmb @数据存储隔离,以保证缓冲中的数据已经落实到RAM中 str r1, [r0] @令lock->rawLock = 0 dsb @数据同步隔离 sev @给各CPU广播事件,唤醒沉睡的CPU们 bx lr @跳回调用ArchSpinLock函数 运作机制 鸿蒙通过对ARMv6架构中的LDREX和STREX进行封装,向用户提供了一套原子操作接口。 LDREX Rx, [Ry] 读取内存中的值,并标记对该段内存为独占访问: 读取寄存器Ry指向的4字节内存数据,保存到Rx寄存器中。 对Ry指向的内存区域添加独占访问标记。 STREX Rf, Rx, [Ry] 检查内存是否有独占访问标记,如果有则更新内存值并清空标记,否则不更新内存: 有独占访问标记 将寄存器Rx中的值更新到寄存器Ry指向的内存。 标志寄存器Rf置为0。 没有独占访问标记 不更新内存。 标志寄存器Rf置为1。 判断标志寄存器 标志寄存器为0时,退出循环,原子操作结束。 标志寄存器为1时,继续循环,重新进行原子操作。 功能列表 原子数据包含两种类型Atomic(有符号32位数)与 Atomic64(有符号64位数)。原子操作模块为用户提供下面几种功能,接口详细信息可以查看源码。 此处讲述 LOS_AtomicAdd , LOS_AtomicSub,LOS_AtomicRead,LOS_AtomicSet 理解了函数的汇编代码是理解的原子操作的关键. LOS_AtomicAdd //对内存数据做加法 STATIC INLINE INT32 LOS_AtomicAdd(Atomic *v, INT32 addVal) { INT32 val; UINT32 status; do { __asm__ __volatile__("ldrex %1, [%2]\n" "add %1, %1, %3\n" "strex %0, %1, [%2]" : "=&r"(status), "=&r"(val) : "r"(v), "r"(addVal) : "cc"); } while (__builtin_expect(status != 0, 0)); return val; } 这是一段C语言内嵌汇编,逐一解读 先将 val status v addVal的值交由通用寄存器(R0~R3)接管. %2代表了入参v,[%2]代表的是参数v指向地址的值,也就是 *v ,函数要独占的就是它 %0 ~ %3 对应 val status v addVal ldrex %1, [%2] 表示 val = *v ; add %1, %1, %3 表示 val = val + addVal; strex %0, %1, [%2] 表示 *v = val; status 表示是否更新成功,成功了置0,不成功则为 1 __builtin_expect是结束循环的判断语句,将最有可能执行的分支告诉编译器。 这个指令的写法为:__builtin_expect(EXP, N)。 意思是:EXP==N 的概率很大。 综合理解__builtin_expect(status != 0, 0) 说的是status = 1失败的可能性很大,不成功就重新来一遍,直到strex更新成(status == 0)为止. "=&r"(val) 被修饰的操作符作为输出,即将寄存器的值回给val,val为函数的返回值 "cc"向GCC编译器声明以上信息. LOS_AtomicSub //对内存数据做减法 STATIC INLINE INT32 LOS_AtomicSub(Atomic *v, INT32 subVal) { INT32 val; UINT32 status; do { __asm__ __volatile__("ldrex %1, [%2]\n" "sub %1, %1, %3\n" "strex %0, %1, [%2]" : "=&r"(status), "=&r"(val) : "r"(v), "r"(subVal) : "cc"); } while (__builtin_expect(status != 0, 0)); return val; } 解读 同 LOS_AtomicAdd解读 volatile 这里要重点说下volatile,volatile 提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都要直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。 //读取内存数据 STATIC INLINE INT32 LOS_AtomicRead(const Atomic *v) { return *(volatile INT32 *)v; } //写入内存数据 STATIC INLINE VOID LOS_AtomicSet(Atomic *v, INT32 setVal) { *(volatile INT32 *)v = setVal; } 编程实例 调用原子操作相关接口,观察结果: 1.创建两个任务 任务一用LOS_AtomicAdd对全局变量加100次。 任务二用LOS_AtomicSub对全局变量减100次。 2.子任务结束后在主任务中打印全局变量的值。 #include "los_hwi.h" #include "los_atomic.h" #include "los_task.h" UINT32 g_testTaskId01; UINT32 g_testTaskId02; Atomic g_sum; Atomic g_count; UINT32 Example_Atomic01(VOID) { int i = 0; for(i = 0; i < 100; ++i) { LOS_AtomicAdd(&g_sum,1); } LOS_AtomicAdd(&g_count,1); return LOS_OK; } UINT32 Example_Atomic02(VOID) { int i = 0; for(i = 0; i < 100; ++i) { LOS_AtomicSub(&g_sum,1); } LOS_AtomicAdd(&g_count,1); return LOS_OK; } UINT32 Example_TaskEntry(VOID) { TSK_INIT_PARAM_S stTask1={0}; stTask1.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Atomic01; stTask1.pcName = "TestAtomicTsk1"; stTask1.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE; stTask1.usTaskPrio = 4; stTask1.uwResved = LOS_TASK_STATUS_DETACHED; TSK_INIT_PARAM_S stTask2={0}; stTask2.pfnTaskEntry = (TSK_ENTRY_FUNC)Example_Atomic02; stTask2.pcName = "TestAtomicTsk2"; stTask2.uwStackSize = LOSCFG_BASE_CORE_TSK_DEFAULT_STACK_SIZE; stTask2.usTaskPrio = 4; stTask2.uwResved = LOS_TASK_STATUS_DETACHED; LOS_TaskLock(); LOS_TaskCreate(&g_testTaskId01, &stTask1); LOS_TaskCreate(&g_testTaskId02, &stTask2); LOS_TaskUnlock(); while(LOS_AtomicRead(&g_count) != 2); dprintf("g_sum = %d\n", g_sum); return LOS_OK; } 结果验证 g_sum = 0 喜欢就大方 点赞+关注+收藏 吧 各大站点搜 "鸿蒙内核源码分析" ,快速找到组织. 百万汉字注解 >> 精读内核源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< Gitee | Github | CSDN | Coding > 百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,多站点每日同步更新< OSCHINA | CSDN | WeHarmony >

优秀的个人博客,低调大师

开源日报 | 下一个AI之城;ChromeOS“安卓化”;HarmonyOS超越iOS;Stable Diffusion 3 Medium开源;计算机专业还值得报考吗?

欢迎阅读 OSCHINA 编辑部出品的开源日报,每天更新一期。 # 2024.6.13 今日要闻 Stable Diffusion 3 Medium 正式开源 Stability AI宣布Stable Diffusion 3 Medium 现已开源,是 Stable Diffusion 3 系列中最新、最先进的文本生成图像 AI 模型 —— 官方声称是 “迄今为止最先进的开源模型”,其性能甚至超过了 Midjourney 6。 SD3 Medium 可以根据用户输入的文本描述,重点克服了文生图模型中手部和脸部的挑战,生成足以乱真的的图像。SD3 Medium 还利用其底层的 Diffusion Transformer 架构,高精度地整合了文字元素。 专为手机设计的最强高速推理引擎 PowerInfer-2 PowerInfer-2 是专为智能手机设计的高度优化的推理框架。PowerInfer-2 最多支持 Mixtral 47B MoE 模型,实现每秒 11.68 个令牌的惊人速度,比其它最先进的框架快 22 倍。即使对于 7B 型号,仅将 FFN 权重的 50% 放置在手机上,PowerInfer-2 仍然保持最先进的速度! ChromeOS 逐渐“安卓化” 谷歌在博客透露称,ChromeOS 底层将更广泛地与 Android 共用同样的技术栈。 为了继续以更快、更大规模的方式向用户推出新的 Google AI 功能,我们将采用部分 Android 堆栈(例如 Android Linux 内核和 Android 框架)作为 ChromeOS 基础的一部分。 将基于 Android 的技术栈引入 ChromeOS 将使我们能够加快 ChromeOS 核心的 AI 创新步伐,简化工程工作,并帮助手机和配件等不同设备更好地与 Chromebook 配合使用。 华为鸿蒙OS一季度首次超越苹果iOS,成中国第二大手机操作系统 根据研究机构Counterpoint Research发布的最新数据,2024年第一季度,华为鸿蒙OS在中国市场超越苹果iOS,这意味着,鸿蒙OS已成中国第二大操作系统。从全球手机系统市场份额来看,安卓和iOS同比均下降1%,而鸿蒙OS的全球份额从2%翻了一番,达到4%。 今日观察 社交观察 计算机专业还值得报考吗 如果简单的回答是:值得报。但更长的答案是:不是值不值得报的问题,而是考上了应该怎么学的问题。 因为在AI时代,掌握计算机理论肯定没有坏处。但是如果在计算机专业只学计算机就意义不大了(当然,如果是天才选手,目标是要把transformer架构拉下马的除外),但是在计算机的基础上,再修一个行业技能专业意义很大。因为AI的价值,未来一定在应用落地。计算机+传统行业。 计算机专业以后更多将是一种语言能力,而语言需要在具体的交流场景中(也就是某个行业),才能更大发挥作用,每个人都应该努力成为一个双语者。 - 微博高飞 Apple Intelligence 背后的模型分成三层 苹果对于 Apple Intelligence 背后技术的介绍,他们的模型分成三层: 1. 本地模型 这部分包含语言模型和扩散模型,另外还有用来做 RAG 的 Semantic Index (语义索引)和 App Intents Toolbox(应用意图工具箱)。 其中语言模型是 3B 的小模型,并且针对一系列任务微调过,比如说摘要、润色、回复邮件等,在这些微调模型权重之上有个适配器,可以根据任务来选择最合适的权重,所以虽然只有 3B,但是能力不弱。 2. 私有云计算(Private Cloud Compute) 对于本地模型满足不了要求的,会传到云端运算。并且苹果在安全性上做了很多工作,对于每一次请求,用户的设备会和私有云计算集群建立一个端到端加密的连接。只有被选择的集群才能解密这些请求数据,数据在返回响应后不会被保留,Apple 也无法访问。 以苹果的口碑,对于这个安全架构我还是相信的。 3. 第三方 LLM 这个视频中没有提及,综合一些其他信息,像一些 Siri 的知识问答、聊天,是接入的外部模型,比如目前接入的有 OpenAI 的 GPT-4o,但未来肯定会接入更多家,并且不同国家地区的 LLM 服务商可能有所不能,比如国内肯定不会用 ChatGPT,至于哪一家还没有宣布。 - 微博宝玉xp iPhone的安卓味越来越浓 其实12年前iPhone4还是4s时代就可以通话录音了,只要越狱后在Cydia里搜索到AudioRecorder就可以通话录音了,只不过不越狱的IOS没这个功能。 相比之下,当然是安卓更方便,毕竟开源,各家开启的方式都很方便,像魅族Flyme是可以自动开启录音,还可以指定联系人才开启录音,iPhone现在有通话录音了,问题居然还会通知对方,那多尴尬啊??? - 即刻科技数码秀 导出微信聊天记录的开源工具 这个memotrace太好用了,我刚才试了一下,备份一个1.4G的微信群聊,四分钟,导出所有文本、图片、表情包和系统信息后只有18M,然后RAR压缩后只有2M!就是说其他99.8%的微信空间都是垃圾冗余。而且导出之后的HTML是按照页面总数和月份分类好的,占用内存非常小,可以在任何浏览器上读取。 ​​​ github.com/LC044/WeChatMsg/releases - 微博失眠狸 那个团子和Stable Diffusion的1000天 如果你昨夜看了如期开源的Stable Diffusion3 Medium,会发现Model Card上3个名字,有2个似曾相识于中文区。Dango233和huoju都是亲身参与SD3训练的开发者,他们从Disco Diffusion时期(2021)开始参与图像生成,1000多天,见证社区一路波澜壮阔。 SD3能顺利开放的波折和他们的力争此刻按下不表,总之二位是开源真正的热爱和实践者。我们的Diffuseum也非常感谢他们作为创立成员,给我们以及中文社区提供的技术支持和信心。 - 微信Latent Cat 媒体观察 吴恩达开源了一个机器翻译智能体项目 在研究团队有限的测试中,吴恩达团队开源的翻译智能体有时能够与领先的商业提供商进行同等水平的竞争,有时则不如它们。但它仍提供了一个高度可控的翻译系统,只需简单更改 prompt,使用者就可以指定语气(正式 / 非正式)、地区变体(例如:使用者想要在西班牙本地说的西班牙语,还是在拉丁美洲说的),并确保术语的翻译一致性(通过提供词汇表)。这个应用程序虽然目前仍稍显稚嫩,但鉴于反思工作流已经展现出不错的成果,吴恩达认为智能体翻译仍有很大的提升空间。 - 机器之心 被指全军覆没?中国软件行业遭遇「季度寒冬」 | BUG 快牛云科技创始人周海鹏总结道:“需求侧本身能力还驾驭不了数字化,人才还需要升级。供给侧本身很多时候半懂不懂,为了大单不顾一切,反过来伤害了客户,加上现在甲方资金紧张。”这些因素的综合,成为当下国内头部软件厂商业绩惨淡的关键。 - 新浪科技 电商之外,阿里的另一场反击! 被马云反复强调的AI,正悄然开启阿里巴巴的新篇章。这不但体现在它正加速电商业务的AI变革,也体现在它对中国AI初创公司拔尖者包圆式的投资。业内甚至有句玩笑话: “如果你想投资中国AI初创公司,最好的办法就是去买阿里巴巴的股票。” - 华商韬略 周鸿祎被AI创作者索赔1元 使用“AI重绘”素材算不算侵权? “对于360此次图片使用之争,我希望该判例能够形成一个国内相关的指导意见。落实到个体上,如何在现有法律框架下保护原创,尊重知识产权,同时又不影响人工智能行业发展,是一个非常难的课题。” - 新京报 AI与“粉红诱惑” 声音打通的是无数个场,游戏的、带货的、教育的等等,最后抵达的是用户。在大模型时代,把声音功能前置是一种典型的产品思维,这意味着更低的门槛、更高效的交互方式以及更庞大的用户群体。 - 光子星球 硅谷之后,谁是下一个AI之城? 已经很少有一个机会能让全球众多城市都跃跃欲试了。到底哪个城市能抓住机会?谁能抓到最大的机会?我们的初步调查表明,在这种全球影响力竞争中,区域竞争其实比全球竞争更激烈。在竞争全球用户之前,每个城市及其公司都需要先角逐人才。而在人才流动上,全球化仍然要让位于区域化。其次,对于学术资源深厚的城市,每个大厂都想去设办公室、抢夺人才,但这对当地城市不见得是好事。最后,基础技术不是一切,产业和文化具有多样性,才能支撑更繁荣的机会。 - 第一财经YiMagazine 美图不想在文生视频领域和巨头们“硬碰硬” 美图试图在投入和盈利之间寻求平衡。“搞模型军备竞赛不是美图的核心战略,我们会在保证应用竞争力的前提下,控制模型的投入。”他还强调,过去很长时间美图都是常年亏损,所以会更看重落地场景和商业模式。” - 界面 今日推荐 开源项目 skylot/jadx https://github.com/skylot/jadx JADX 是一个Dex 到 Java 的反编译器,用于从 Android Dex 和 Apk 文件生成 Java 源代码。JADX 有命令行和 GUI 两个版本。 每日一博 Java 开发必读,谈谈对 Spring IOC 与 AOP 的理解 IOC 和 AOP 是 Spring 中的两个核心的概念。 IOC(Inverse of Control)控制反转,也可以称为依赖倒置。所谓依赖,从程序的角度看,就是比如 A 要调用 B 的方法,那么 A 就依赖于 B,反正 A 要用到 B,则 A 依赖于 B。所谓倒置,你必须理解如果不倒置,会怎么着,因为 A 必须要有 B,才可以调用 B,如果不倒置,意思就是 A 主动获取 B 的实例:B b = new B (),这就是最简单的获取 B 实例的方法(当然还有各种设计模式可以帮助你去获得 B 的实例,比如工厂、Locator 等等),然后你就可以调用 b 对象了。所以,不倒置,意味着 A 要主动获取 B,才能使用 B;到了这里,就应该明白了倒置的意思了。倒置就是 A 要调用 B 的话,A 并不需要主动获取 B,而是由其它人自动将 B 送上门来。 AOP (面向切面编程)的目标就是分离关注点。什么是关注点呢?就是你要做的事,就是关注点。假如你是个公子哥,没啥人生目标,天天就是衣来伸手,饭来张口,整天只知道玩一件事!那么,每天你一睁眼,就光想着吃完饭就去玩(你必须要做的事),但是在玩之前,你还需要穿衣服、穿鞋子、叠好被子、做饭等等等等事情,这些事情就是你的关注点,但是你只想吃饭然后玩,那么怎么办呢?这些事情通通交给别人去干。在你走到饭桌之前,有一个专门的仆人 A 帮你穿衣服,仆人 B 帮你穿鞋子,仆人 C 帮你叠好被子,仆人 D 帮你做饭,然后你就开始吃饭、去玩(这就是你一天的正事),你干完你的正事之后,回来,然后一系列仆人又开始帮你干这个干那个,然后一天就结束了! 开源之声 用户观点 ChromeOS逐渐“安卓化” 观点:1:宁愿折腾那玩意儿也不做去完善 android pc 观点 2:这个是好事,慢慢这个桌面化了。 arm windows 没它啥事了。 话说windows 是该灭亡了。 观点 3:不能用 apk 文件,什么都是扯淡 观点 4:开发者模式开出来就行了 观点 5:我在ChromeOS用Android Studio安装apk 有的架构还是不兼容 观点 6:这系统竟然还有人用 全球销量6000万的开发板:“树莓派”上市了——曾坐马斯克的大火箭上太空 观点 1:可惜现在树莓派既不价格低廉,也不开源开放 观点 2:社区不错,学生党做做毕设合适,就是经销商那副你不买有的是人买的嘴脸,对公采购就算了。 观点 3:原来普通的arm都能上太空 观点 4:现在的树莓派,高不成低不就的。在国产替代的大环境下,媒体要有作为的忽视。 观点 5:用别人的玩儿过,如果做硬件开发折腾和学习,价格不划算。如果用于桌面电脑,看高清视频卡顿,性能又跟不上。总之想玩儿一玩儿不如买个华强北山寨版orange派 观点 6:现在国产开发版一大把,价格低性能还更强 观点 7:老哥给推荐一个国产的,我整一个玩玩 观点 8:orangepi 观点 9:为啥看到留言都是吐槽的?我最近折腾了一个,感觉挺好的,Debian几乎就是原生 Debian,还没遇到软件问题 观点 10:以前:这个便宜,买这个。现在:这个性能好,买这个。 观点 11:树莓派以前还能夸两句,现在价格完全配不上,一开始还搞什么公益不公益的,现在还上市算是装都不装了。 苹果称 Swift 是 C++ 的最佳继任者 观点 1:swift和rust为何不合并? 观点 2:swift支持Linux,那么就可以支持Android native了吗? ---END--- 最后,欢迎扫码下载「开源中国 APP」,阅读海量技术报告、程序员极客分享!

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册