Erlo

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

时间:2021-04-22   阅读:62次   来源:开源中国
页面报错
点赞

百万汉字注解 >> 精读鸿蒙源码,中文注解分析, 深挖地基工程,大脑永久记忆,四大码仓每日同步更新< gitee | github | csdn | coding >

百篇博客分析 >> 故事说内核,问答式导读,生活式比喻,表格化说明,图形化展示,主流站点定期更新中< oschina | 51cto | csdn | 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: %dn", 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 : %dn", 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;//返回用户栈代码执行位置
    

    注意这里还不是真正的切换上下文,只是改变内核栈中现有的数据.这些数据将还原给寄存器.USPULR指向的是用户栈的位置.一旦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 >

鸿蒙源码百篇博客 往期回顾

参与贡献

喜欢请「点赞+关注+收藏」

相关推荐

提交留言

评论留言

还没有评论留言,赶紧来抢楼吧~~

吐槽小黑屋()

* 这里是“吐槽小黑屋”,所有人可看,只保留当天信息。

  • Erlo吐槽

    Erlo.vip2021-07-29 14:00:38Hello、欢迎使用吐槽小黑屋,这就是个吐槽的地方。
  • 返回顶部

    给这篇文章打个标签吧~

    棒极了 糟糕透顶 好文章 PHP JAVA JS 小程序 Python SEO MySql 确认