v44.04 鸿蒙内核源码分析(中断管理篇) | 江湖从此不再怕中断 | 百篇博客分析OpenHarmony源码

原创
2021/03/23 16:46
阅读数 1.4W

“唐棣之华,偏其反而。岂不尔思?室是远而。”子曰:“未之思也,夫何远之有?” 《论语》:子罕篇

在这里插入图片描述

百篇博客系列篇.本篇为:

v44.xx 鸿蒙内核源码分析(中断管理篇) | 江湖从此不再怕中断

硬件架构相关篇为:

关于中断部分系列篇将用三篇详细说明整个过程.

  • 中断概念篇 中断概念很多,比如中断控制器,中断源,中断向量,中断共享,中断处理程序等等.本篇做一次整理.先了解透概念才好理解中断过程.用海公公打比方说明白中断各个概念.可前往查看.

  • 中断管理篇(本篇) 从中断初始化HalIrqInit开始,到注册中断的LOS_HwiCreate函数,到消费中断函数的 HalIrqHandler,剖析鸿蒙内核实现中断的过程,很像设计模式中的观察者模式.

  • 中断切换篇 用自下而上的方式,从中断源头纯汇编代码往上跟踪代码细节.说清楚保存和恢复中断现场TaskIrqContext过程.

编译开关

系列篇编译平台为 hi3516dv300,整个工程可前往查看. 预编译处理过程会自动生成编译开关 menuconfig.h ,供编译阶段选择编译,可前往查看.

//....
#define LOSCFG_ARCH_ARM_VER "armv7-a"
#define LOSCFG_ARCH_CPU "cortex-a7"
#define LOSCFG_PLATFORM "hi3516dv300"
#define LOSCFG_PLATFORM_BSP_GIC_V2 1
#define LOSCFG_PLATFORM_ROOTFS 1
#define LOSCFG_KERNEL_CPPSUPPORT 1
#define LOSCFG_HW_RANDOM_ENABLE 1
#define LOSCFG_ARCH_CORTEX_A7 1
#define LOSCFG_DRIVERS_HDF_PLATFORM_RTC 1
#define LOSCFG_DRIVERS_HDF_PLATFORM_UART 1

中断初始化

hi3516dv300 中断控制器选择了 LOSCFG_PLATFORM_BSP_GIC_V2 ,对应代码为 gic_v2.c GIC(Generic Interrupt Controller)是ARM公司提供的一个通用的中断控制器. 看这种代码因为涉及硬件部分,需要对照ARM中断控制器 gic_v2.pdf文档看.可前往地址下载查看.

//硬件中断初始化
VOID HalIrqInit(VOID)
{
    UINT32 i;

    /* set externel interrupts to be level triggered, active low. */	//将外部中断设置为电平触发,低电平激活
    for (i = 32; i < OS_HWI_MAX_NUM; i += 16) {
        GIC_REG_32(GICD_ICFGR(i / 16)) = 0;
    }

    /* set externel interrupts to CPU 0 */	//将外部中断设置为CPU 0
    for (i = 32; i < OS_HWI_MAX_NUM; i += 4) {
        GIC_REG_32(GICD_ITARGETSR(i / 4)) = 0x01010101;
    }

    /* set priority on all interrupts */	//设置所有中断的优先级
    for (i = 0; i < OS_HWI_MAX_NUM; i += 4) {
        GIC_REG_32(GICD_IPRIORITYR(i / 4)) = GICD_INT_DEF_PRI_X4;
    }

    /* disable all interrupts. */			//禁用所有中断。
    for (i = 0; i < OS_HWI_MAX_NUM; i += 32) {
        GIC_REG_32(GICD_ICENABLER(i / 32)) = ~0;
    }

    HalIrqInitPercpu();//初始化当前CPU中断信息

    /* enable gic distributor control */
    GIC_REG_32(GICD_CTLR) = 1; //使能分发中断寄存器,该寄存器作用是允许给CPU发送中断信号

#if (LOSCFG_KERNEL_SMP == YES)
    /* register inter-processor interrupt *///注册核间中断,啥意思?就是CPU各核直接可以发送中断信号
    //处理器间中断允许一个CPU向系统其他的CPU发送中断信号,处理器间中断(IPI)不是通过IRQ线传输的,而是作为信号直接放在连接所有CPU本地APIC的总线上。
    LOS_HwiCreate(LOS_MP_IPI_WAKEUP, 0xa0, 0, OsMpWakeHandler, 0);//注册唤醒CPU的中断处理函数
    LOS_HwiCreate(LOS_MP_IPI_SCHEDULE, 0xa0, 0, OsMpScheduleHandler, 0);//注册调度CPU的中断处理函数
    LOS_HwiCreate(LOS_MP_IPI_HALT, 0xa0, 0, OsMpScheduleHandler, 0);//注册停止CPU的中断处理函数
#endif
}
//给每个CPU core初始化硬件中断
VOID HalIrqInitPercpu(VOID)
{
    /* unmask interrupts */	//取消中断屏蔽
    GIC_REG_32(GICC_PMR) = 0xFF;

    /* enable gic cpu interface */	//启用gic cpu接口
    GIC_REG_32(GICC_CTLR) = 1;
}

解读

  • 上来四个循环,是对中断控制器寄存器组的初始化,也就是驱动程序,驱动程序是配置硬件寄存器的过程.寄存器分通用和专用寄存器.下图为 gic_v2 的寄存器功能 ,这里对照代码和datasheet重点说下中断配置寄存器(GICD_ICFGRn) 在这里插入图片描述

  • 以下是GICD_ICFGRn的介绍

    The GICD_ICFGRs provide a 2-bit Int_config field for each interrupt supported by the GIC. This field identifies whether the corresponding interrupt is edge-triggered or level-sensitive

    GICD_ICFGRs为GIC支持的每个中断提供一个2位配置字段。此字段标识相应的中断是边缘触发的还是电平触发的

    0xC00 - 0xCFC GICD_ICFGRn RW IMPLEMENTATION DEFINED Interrupt Configuration Registers
    #define GICD_ICFGR(n)                   (GICD_OFFSET + 0xc00 + (n) * 4) /* Interrupt Configuration Registers */		//中断配置寄存器
    

    如此一个32位寄存器可以记录16个中断的信息,这也是代码中出现 GIC_REG_32(GICD_ICFGR(i / 16))的原因.

  • GIC-v2支持三种类型的中断

    • PPI:私有外设中断(Private Peripheral Interrupt),是每个CPU私有的中断。最多支持16个PPI中断,硬件中断号从ID16~ID31。PPI通常会送达到指定的CPU上,应用场景有CPU本地时钟。
    • SPI:公用外设中断(Shared Peripheral Interrupt),最多可以支持988个外设中断,硬件中断号从ID32~ID1019。
    • SGI:软件触发中断(Software Generated Interrupt)通常用于多核间通讯,最多支持16个SGI中断,硬件中断号从ID0~ID15。SGI通常在内核中被用作 IPI 中断(inter-processor interrupts),并会送达到系统指定的CPU上,函数的最后就注册了三个核间中断的函数.
      typedef enum {//核间中断
          LOS_MP_IPI_WAKEUP,	//唤醒CPU
          LOS_MP_IPI_SCHEDULE,//调度CPU
          LOS_MP_IPI_HALT,	//停止CPU
      } MP_IPI_TYPE;
      

中断相关的结构体

size_t g_intCount[LOSCFG_KERNEL_CORE_NUM] = {0};//记录每个CPUcore的中断数量 
HwiHandleForm g_hwiForm[OS_HWI_MAX_NUM];//中断注册表 @note_why 用 form 来表示?有种写 HTML的感觉 :P
STATIC CHAR *g_hwiFormName[OS_HWI_MAX_NUM] = {0};//记录每个硬中断的名称 
STATIC UINT32 g_hwiFormCnt[OS_HWI_MAX_NUM] = {0};//记录每个硬中断的总数量
STATIC UINT32 g_curIrqNum = 0; //记录当前中断号
typedef VOID (*HWI_PROC_FUNC)(VOID); //中断函数指针
typedef struct tagHwiHandleForm {	
    HWI_PROC_FUNC pfnHook;	//中断处理函数
    HWI_ARG_T uwParam;		//中断处理函数参数
    struct tagHwiHandleForm *pstNext;	//节点,指向下一个中断,用于共享中断的情况
} HwiHandleForm;

typedef struct tagIrqParam {	//中断参数
    int swIrq;		//	软件中断
    VOID *pDevId;	//	设备ID
    const CHAR *pName;	//名称
} HwiIrqParam;

注册硬中断

/******************************************************************************
 创建一个硬中断
 中断创建,注册中断号、中断触发模式、中断优先级、中断处理程序。中断被触发时,
 handleIrq会调用该中断处理程序
******************************************************************************/
LITE_OS_SEC_TEXT_INIT UINT32 LOS_HwiCreate(HWI_HANDLE_T hwiNum,	//硬中断句柄编号 默认范围[0-127]
                                           HWI_PRIOR_T hwiPrio,		//硬中断优先级	
                                           HWI_MODE_T hwiMode,		//硬中断模式 共享和非共享
                                           HWI_PROC_FUNC hwiHandler,//硬中断处理函数
                                           HwiIrqParam *irqParam)	//硬中断处理函数参数
{
    UINT32 ret;

    (VOID)hwiPrio;
    if (hwiHandler == NULL) {//中断处理函数不能为NULL
        return OS_ERRNO_HWI_PROC_FUNC_NULL;
    }
    if ((hwiNum > OS_USER_HWI_MAX) || ((INT32)hwiNum < OS_USER_HWI_MIN)) {//中断数区间限制 [32,96]
        return OS_ERRNO_HWI_NUM_INVALID;
    }

#ifdef LOSCFG_NO_SHARED_IRQ	//不支持共享中断
    ret = OsHwiCreateNoShared(hwiNum, hwiMode, hwiHandler, irqParam);
#else
    ret = OsHwiCreateShared(hwiNum, hwiMode, hwiHandler, irqParam);
#endif
    return ret;
}
//创建一个共享硬件中断,共享中断就是一个中断能触发多个响应函数
STATIC UINT32 OsHwiCreateShared(HWI_HANDLE_T hwiNum, HWI_MODE_T hwiMode,
                                HWI_PROC_FUNC hwiHandler, const HwiIrqParam *irqParam)
{
    UINT32 intSave;
    HwiHandleForm *hwiFormNode = NULL;
    HwiHandleForm *hwiForm = NULL;
    HwiIrqParam *hwiParam = NULL;
    HWI_MODE_T modeResult = hwiMode & IRQF_SHARED;

    if (modeResult && ((irqParam == NULL) || (irqParam->pDevId == NULL))) {
        return OS_ERRNO_HWI_SHARED_ERROR;
    }

    HWI_LOCK(intSave);//中断自旋锁

    hwiForm = &g_hwiForm[hwiNum];//获取中断处理项
    if ((hwiForm->pstNext != NULL) && ((modeResult == 0) || (!(hwiForm->uwParam & IRQF_SHARED)))) {
        HWI_UNLOCK(intSave);
        return OS_ERRNO_HWI_SHARED_ERROR;
    }

    while (hwiForm->pstNext != NULL) {//pstNext指向 共享中断的各处理函数节点,此处一直撸到最后一个
        hwiForm = hwiForm->pstNext;//找下一个中断
        hwiParam = (HwiIrqParam *)(hwiForm->uwParam);//获取中断参数,用于检测该设备ID是否已经有中断处理函数
        if (hwiParam->pDevId == irqParam->pDevId) {//设备ID一致时,说明设备对应的中断处理函数已经存在了.
            HWI_UNLOCK(intSave);
            return OS_ERRNO_HWI_ALREADY_CREATED;
        }
    }

    hwiFormNode = (HwiHandleForm *)LOS_MemAlloc(m_aucSysMem0, sizeof(HwiHandleForm));//创建一个中断处理节点
    if (hwiFormNode == NULL) {
        HWI_UNLOCK(intSave);
        return OS_ERRNO_HWI_NO_MEMORY;
    }

    hwiFormNode->uwParam = OsHwiCpIrqParam(irqParam);//获取中断处理函数的参数
    if (hwiFormNode->uwParam == LOS_NOK) {
        HWI_UNLOCK(intSave);
        (VOID)LOS_MemFree(m_aucSysMem0, hwiFormNode);
        return OS_ERRNO_HWI_NO_MEMORY;
    }

    hwiFormNode->pfnHook = hwiHandler;//绑定中断处理函数
    hwiFormNode->pstNext = (struct tagHwiHandleForm *)NULL;//指定下一个中断为NULL,用于后续遍历找到最后一个中断项(见于以上 while (hwiForm->pstNext != NULL)处)
    hwiForm->pstNext = hwiFormNode;//共享中断

    if ((irqParam != NULL) && (irqParam->pName != NULL)) {
        g_hwiFormName[hwiNum] = (CHAR *)irqParam->pName;
    }

    g_hwiForm[hwiNum].uwParam = modeResult;

    HWI_UNLOCK(intSave);
    return LOS_OK;
}

解读

  • 内核将硬中断进行编号,如:
      #define NUM_HAL_INTERRUPT_TIMER0        33
      #define NUM_HAL_INTERRUPT_TIMER1        33
      #define NUM_HAL_INTERRUPT_TIMER2        34
      #define NUM_HAL_INTERRUPT_TIMER3        34
      #define NUM_HAL_INTERRUPT_TIMER4        35
      #define NUM_HAL_INTERRUPT_TIMER5        35
      #define NUM_HAL_INTERRUPT_TIMER6        36
      #define NUM_HAL_INTERRUPT_TIMER7        36
      #define NUM_HAL_INTERRUPT_DMAC          60
      #define NUM_HAL_INTERRUPT_UART0         38
      #define NUM_HAL_INTERRUPT_UART1         39
      #define NUM_HAL_INTERRUPT_UART2         40
      #define NUM_HAL_INTERRUPT_UART3         41
      #define NUM_HAL_INTERRUPT_UART4         42
      #define NUM_HAL_INTERRUPT_TIMER         NUM_HAL_INTERRUPT_TIMER4
    
    例如:时钟节拍处理函数 OsTickHandler 就是在 HalClockInit中注册的
    //硬时钟初始化
    VOID HalClockInit(VOID)
    {
      // ...
      (void)LOS_HwiCreate(NUM_HAL_INTERRUPT_TIMER, 0xa0, 0, OsTickHandler, 0);//注册OsTickHandler到中断向量表 
    }
    //节拍中断处理函数 ,鸿蒙默认10ms触发一次
      LITE_OS_SEC_TEXT VOID OsTickHandler(VOID)
      {
          UINT32 intSave;
          TICK_LOCK(intSave);//tick自旋锁
          g_tickCount[ArchCurrCpuid()]++;// 累加当前CPU核tick数
          TICK_UNLOCK(intSave);
          OsTimesliceCheck();//时间片检查
          OsTaskScan(); /* task timeout scan *///扫描超时任务 例如:delay(300)
          #if (LOSCFG_BASE_CORE_SWTMR == YES)
              OsSwtmrScan();//扫描定时器,查看是否有超时定时器,加入队列
          #endif
      } 
    
  • 鸿蒙是支持中断共享的,在OsHwiCreateShared中,将函数注册到g_hwiForm中.中断向量完成注册后,就是如何触发和回调的问题.触发在 鸿蒙内核源码分析(总目录)中断切换篇中已经讲清楚,触发是从底层汇编向上调用,调用的C函数就是HalIrqHandler

中断怎么触发的?

分两种情况:

  • 通过硬件触发,比如按键,USB的插拔这些中断源向中断控制器发送电信号(高低电平触发或是上升/下降沿触发),中断控制器经过过滤后将信号发给对应的CPU处理,通过硬件改变PC和CPSR寄存值,直接跳转到中断向量(固定地址)执行.

      b   reset_vector            @开机代码
      b   _osExceptUndefInstrHdl 	@异常处理之CPU碰到不认识的指令
      b   _osExceptSwiHdl			@异常处理之:软中断
      b   _osExceptPrefetchAbortHdl	@异常处理之:取指异常
      b   _osExceptDataAbortHdl		@异常处理之:数据异常
      b   _osExceptAddrAbortHdl		@异常处理之:地址异常
      b   OsIrqHandler				@异常处理之:硬中断
      b   _osExceptFiqHdl				@异常处理之:快中断
    
  • 通过软件触发,常见于核间中断的情况, 核间中断指的是几个CPU之间相互通讯的过程.以下为某一个CPU向其他CPU(可以是多个)发起让这些CPU重新调度LOS_MpSchedule的中断请求信号.最终是写了中断控制器的GICD_SGIR寄存器,这是一个由软件触发中断的寄存器.中断控制器会将请求分发给对应的CPU处理中断,即触发了OsIrqHandler.

    //给参数CPU发送调度信号
      VOID LOS_MpSchedule(UINT32 target)//target每位对应CPU core 
      {
          UINT32 cpuid = ArchCurrCpuid();
          target &= ~(1U << cpuid);//获取除了自身之外的其他CPU
          HalIrqSendIpi(target, LOS_MP_IPI_SCHEDULE);//向目标CPU发送调度信号,核间中断(Inter-Processor Interrupts),IPI
      }
      //SGI软件触发中断(Software Generated Interrupt)通常用于多核间通讯
      STATIC VOID GicWriteSgi(UINT32 vector, UINT32 cpuMask, UINT32 filter)
      {
          UINT32 val = ((filter & 0x3) << 24) | ((cpuMask & 0xFF) << 16) |
                      (vector & 0xF);
    
          GIC_REG_32(GICD_SGIR) = val;//写SGI寄存器
      }
      //向指定核发送核间中断
      VOID HalIrqSendIpi(UINT32 target, UINT32 ipi)
      {
          GicWriteSgi(ipi, target, 0);
      }
    

中断统一处理入口函数 HalIrqHandler

//硬中断统一处理函数,这里由硬件触发,调用见于 ..\arch\arm\arm\src\los_dispatch.S
VOID HalIrqHandler(VOID)
{
    UINT32 iar = GIC_REG_32(GICC_IAR);//从中断确认寄存器获取中断ID号
    UINT32 vector = iar & 0x3FFU;//计算中断向量号
    /*
     * invalid irq number, mainly the spurious interrupts 0x3ff,
     * gicv2 valid irq ranges from 0~1019, we use OS_HWI_MAX_NUM
     * to do the checking.
     */
    if (vector >= OS_HWI_MAX_NUM) {
        return;
    }
    g_curIrqNum = vector;//记录当前中断ID号
    OsInterrupt(vector);//调用上层中断处理函数
    /* use orignal iar to do the EOI */
    GIC_REG_32(GICC_EOIR) = iar;//更新中断结束寄存器
}
VOID OsInterrupt(UINT32 intNum)//中断实际处理函数
{
    HwiHandleForm *hwiForm = NULL;
    UINT32 *intCnt = NULL;

    intCnt = &g_intCount[ArchCurrCpuid()];//当前CPU的中断总数量 ++
    *intCnt = *intCnt + 1;//@note_why 这里没看明白为什么要 +1

#ifdef LOSCFG_CPUP_INCLUDE_IRQ //开启查询系统CPU的占用率的中断
    OsCpupIrqStart();//记录本次中断处理开始时间
#endif

#ifdef LOSCFG_KERNEL_TICKLESS
    OsTicklessUpdate(intNum);
#endif
    hwiForm = (&g_hwiForm[intNum]);//获取对应中断的实体
#ifndef LOSCFG_NO_SHARED_IRQ	//如果没有定义不共享中断 ,意思就是如果是共享中断
    while (hwiForm->pstNext != NULL) { //一直撸到最后
        hwiForm = hwiForm->pstNext;//下一个继续撸
#endif
        if (hwiForm->uwParam) {//有参数的情况
            HWI_PROC_FUNC2 func = (HWI_PROC_FUNC2)hwiForm->pfnHook;//获取回调函数
            if (func != NULL) {
                UINTPTR *param = (UINTPTR *)(hwiForm->uwParam);
                func((INT32)(*param), (VOID *)(*(param + 1)));//运行带参数的回调函数
            }
        } else {//木有参数的情况
            HWI_PROC_FUNC0 func = (HWI_PROC_FUNC0)hwiForm->pfnHook;//获取回调函数
            if (func != NULL) {
                func();//运行回调函数
            }
        }
#ifndef LOSCFG_NO_SHARED_IRQ
    }
#endif
    ++g_hwiFormCnt[intNum];//中断号计数器总数累加

    *intCnt = *intCnt - 1;	//@note_why 这里没看明白为什么要 -1 
#ifdef LOSCFG_CPUP_INCLUDE_IRQ	//开启查询系统CPU的占用率的中断
    OsCpupIrqEnd(intNum);//记录中断处理时间完成时间
#endif
}

解读 统一中断处理函数是一个通过一个中断号去找到注册函数的过程,分四步走:

  • 第一步:取号,这号是由中断控制器的 GICC_IAR寄存器提供的,这是一个专门保存当前中断号的寄存器.
  • 第二步:从注册表g_hwiForm中查询注册函数,同时取出参数.
  • 第三步:执行函数,也就是回调注册函数,分有参和无参两种情况 func(...),在中断共享的情况,注册函数会指向 next 注册函数pstNext,依次执行回调函数,这是中断共享的实现细节.
    typedef struct tagHwiHandleForm {	
          HWI_PROC_FUNC pfnHook;	//中断处理函数
          HWI_ARG_T uwParam;		//中断处理函数参数
          struct tagHwiHandleForm *pstNext;	//节点,指向next中断,用于共享中断的情况
    } HwiHandleForm;
    
  • 第四步:销号,本次中断完成了就需要消除记录,中断控制器也有专门的销号寄存器GICC_EOIR
  • 另外的是一些统一数据,每次中断号处理内核都会记录次数,和耗时,以便定位/跟踪/诊断问题.

百篇博客分析.深挖内核地基

  • 给鸿蒙内核源码加注释过程中,整理出以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切.确实有难度,自不量力,但已经出发,回头已是不可能的了。 😛
  • 与代码有bug需不断debug一样,文章和注解内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。

按功能模块:

基础工具 加载运行 进程管理 编译构建
双向链表 位图管理 用栈方式 定时器 原子操作 时间管理 ELF格式 ELF解析 静态链接 重定位 进程映像 进程管理 进程概念 Fork 特殊进程 进程回收 信号生产 信号消费 Shell编辑 Shell解析 编译环境 编译过程 环境脚本 构建工具 gn应用 忍者ninja
进程通讯 内存管理 前因后果 任务管理
自旋锁 互斥锁 进程通讯 信号量 事件控制 消息队列 内存分配 内存管理 内存汇编 内存映射 内存规则 物理内存 总目录 调度故事 内存主奴 源码注释 源码结构 静态站点 时钟任务 任务调度 任务管理 调度队列 调度机制 线程概念 并发并行 CPU 系统调用 任务切换
文件系统 硬件架构
文件概念 文件系统 索引节点 挂载目录 根文件系统 字符设备 VFS 文件句柄 管道文件 汇编基础 汇编传参 工作模式 寄存器 异常接管 汇编汇总 中断切换 中断概念 中断管理

百万汉字注解.精读内核源码

WeHarmony/kernel_liteos_a_note

鸿蒙研究站 | 每天死磕一点点,原创不易,欢迎转载,但请注明出处。

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
1 收藏
1
分享
返回顶部
顶部