之前讲清楚了中断向量表,rtems中只实现了sp0和spx的irq中断。那么本文基于timer中断的入口handle来讲解中断是如何触发的
操作系统的ticker是基于硬件timer实现的,为了了解中断触发,timer是非常合适的一个例子,系统启动后,记录的每一个tick值都是一次timer的定时到期。
在arm64中,timer默认是读取cntfrq_el0寄存器,如下
其代码实现如下
void arm_generic_timer_get_config( uint32_t *frequency, uint32_t *irq ) { uint64_t val; __asm__ volatile ( "mrs %[val], cntfrq_el0" : [val] "=&r" (val) ); *frequency = val; #ifdef ARM_GENERIC_TIMER_USE_VIRTUAL *irq = BSP_TIMER_VIRT_PPI; #elif defined(AARCH64_GENERIC_TIMER_USE_PHYSICAL_SECURE) *irq = BSP_TIMER_PHYS_S_PPI; #else *irq = BSP_TIMER_PHYS_NS_PPI; #endif }
我们知道默认情况下使用的是no-secure el0,所以中断号设置是30,如下
#define BSP_TIMER_PHYS_NS_PPI 30
对于timer的使用,主要如下几个步骤
至此timer会倒计时发生中断,然后就是中断触发了,对于中断触发,其流程如下
关于中断等下会讲,现在先简单把timer说清楚
关于设置cntp_ctl,代码如下
void arm_gt_clock_set_control(uint32_t ctl) { __asm__ volatile ( #ifdef AARCH64_GENERIC_TIMER_USE_VIRTUAL "msr cntv_ctl_el0, %[ctl]" #elif defined(AARCH64_GENERIC_TIMER_USE_PHYSICAL_SECURE) "msr cntps_ctl_el1, %[ctl]" #else "msr cntp_ctl_el0, %[ctl]" #endif : : [ctl] "r" (ctl) ); }
关于设置timer的计数值,代码如下
void arm_gt_clock_set_compare_value(uint64_t cval) { __asm__ volatile ( #ifdef AARCH64_GENERIC_TIMER_USE_VIRTUAL "msr cntv_cval_el0, %[cval]" #elif defined(AARCH64_GENERIC_TIMER_USE_PHYSICAL_SECURE) "msr cntps_cval_el1, %[cval]" #else "msr cntp_cval_el0, %[cval]" #endif : : [cval] "r" (cval) ); }
至此,我们先简单把timer说清楚了。
对于一个中断,我们需要填充其ISR才能正常工作,所以为了让中断触发能够跳到自己设置的ISR,我们需要注册中断,RTEMS中注册中断的方式如下
static void arm_gt_clock_handler_install(rtems_interrupt_handler handler) { rtems_status_code sc; rtems_interrupt_entry_initialize( &arm_gt_interrupt_entry, handler, &arm_gt_clock_instance, "Clock" ); sc = rtems_interrupt_entry_install( arm_gt_clock_instance.irq, RTEMS_INTERRUPT_UNIQUE, &arm_gt_interrupt_entry ); if (sc != RTEMS_SUCCESSFUL) { bsp_fatal(BSP_ARM_FATAL_GENERIC_TIMER_CLOCK_IRQ_INSTALL); } }
这里的install动作我们关注如下调用
rtems_interrupt_entry_install bsp_interrupt_entry_install bsp_interrupt_entry_install_first
此函数的代码如下
static rtems_status_code bsp_interrupt_entry_install_first( rtems_vector_number vector, rtems_option options, rtems_interrupt_entry *entry ) { rtems_vector_number index; index = vector; bsp_interrupt_entry_store_release( bsp_interrupt_get_dispatch_table_slot( index ), entry ); bsp_interrupt_set_handler_unique( index, RTEMS_INTERRUPT_IS_UNIQUE( options ) ); bsp_interrupt_vector_enable( vector ); return RTEMS_SUCCESSFUL; }
可以发现其做了如下几个事情
至此,通过上述操作,中断会注册到向量表上,如下
&_Record_Interrupt_dispatch_table[ 30 ]; bsp_interrupt_dispatch_table[ 30 ]
上面两个地址是相等的
bsp_interrupt_dispatch_table[ i ] = &_Record_Interrupt_entry_table[ i ];
此时当中断发生时,可以通过此表找到对应的ISR。
对于timer中断,其通过curr_el_spx_irq触发,之前提过中断向量表了,这里我们只关心行为,那么首先要做的是JUMP_HANDLER
.macro JUMP_HANDLER /* Mask to use in BIC, lower 7 bits */ mov x0, #0x7f /* LR contains PC, mask off to the base of the current vector */ bic x0, lr, x0 /* Load address from the last word in the vector */ ldr x0, [x0, #0x78] /* * Branch and link to the address in x0. There is no reason to save the current * LR since it has already been saved and the current contents are junk. */ blr x0 /* Pop x0,lr from stack */ ldp x0, lr, [sp], #0x10 /* Return from exception */ eret nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop nop .endm
进入中断处理程序的代码是
ldr x0, [x0, #0x78]
根据RTEMS的中断管理之中断向量表
的分析,其入口函数是_AArch64_Exception_interrupt_no_nest
。
对于_AArch64_Exception_interrupt_no_nest
的代码,其实现如下
_AArch64_Exception_interrupt_no_nest: /* Execution template: Save volatile registers on thread stack(some x, all q, ELR, etc.) Switch to interrupt stack Execute interrupt handler Switch to thread stack Call thread dispatch Restore volatile registers from thread stack Return to embedded exception vector code */ /* Push interrupt context */ push_interrupt_context /* * Switch to interrupt stack, interrupt dispatch may enable interrupts causing * nesting */ msr spsel, #0 /* Jump into the handler */ bl .AArch64_Interrupt_Handler /* * Switch back to thread stack, interrupt dispatch should disable interrupts * before returning */ msr spsel, #1 /* * Check thread dispatch necessary, ISR dispatch disable and thread dispatch * disable level. */ cmp x0, #0 bne .Lno_need_thread_dispatch bl _AArch64_Exception_thread_dispatch .Lno_need_thread_dispatch: /* * SP should be where it was pre-handler (pointing at the exception frame) * or something has leaked stack space */ /* Pop interrupt context */ pop_interrupt_context /* Return to vector for final cleanup */ ret
可以看的保存中断上下文代码是push_interrupt_context
,那么其实现如下
.macro push_interrupt_context /* * Push x1-x21 on to the stack, need 19-21 because they're modified without * obeying PCS */ stp lr, x1, [sp, #-0x10]! stp x2, x3, [sp, #-0x10]! stp x4, x5, [sp, #-0x10]! stp x6, x7, [sp, #-0x10]! stp x8, x9, [sp, #-0x10]! stp x10, x11, [sp, #-0x10]! stp x12, x13, [sp, #-0x10]! stp x14, x15, [sp, #-0x10]! stp x16, x17, [sp, #-0x10]! stp x18, x19, [sp, #-0x10]! stp x20, x21, [sp, #-0x10]! /* * Push q0-q31 on to the stack, need everything because parts of every register * are volatile/corruptible */ stp q0, q1, [sp, #-0x20]! stp q2, q3, [sp, #-0x20]! stp q4, q5, [sp, #-0x20]! stp q6, q7, [sp, #-0x20]! stp q8, q9, [sp, #-0x20]! stp q10, q11, [sp, #-0x20]! stp q12, q13, [sp, #-0x20]! stp q14, q15, [sp, #-0x20]! stp q16, q17, [sp, #-0x20]! stp q18, q19, [sp, #-0x20]! stp q20, q21, [sp, #-0x20]! stp q22, q23, [sp, #-0x20]! stp q24, q25, [sp, #-0x20]! stp q26, q27, [sp, #-0x20]! stp q28, q29, [sp, #-0x20]! stp q30, q31, [sp, #-0x20]! /* Get exception LR for PC and spsr */ mrs x0, ELR_EL1 mrs x1, SPSR_EL1 /* Push pc and spsr */ stp x0, x1, [sp, #-0x10]! /* Get fpcr and fpsr */ mrs x0, FPSR mrs x1, FPCR /* Push fpcr and fpsr */ stp x0, x1, [sp, #-0x10]! .endm
可以看的,其操作如下
将spsel设置为0,这样栈跳到sp_el0上,然后执行AArch64_Interrupt_Handler
msr spsel, #0 bl .AArch64_Interrupt_Handler
我们先看AArch64_Interrupt_Handler做了哪些事情
.AArch64_Interrupt_Handler: /* Get per-CPU control of current processor */ GET_SELF_CPU_CONTROL SELF_CPU_CONTROL_GET_REG /* Increment interrupt nest and thread dispatch disable level */ ldr w2, [SELF_CPU_CONTROL, #PER_CPU_ISR_NEST_LEVEL] ldr w3, [SELF_CPU_CONTROL, #PER_CPU_THREAD_DISPATCH_DISABLE_LEVEL] add w2, w2, #1 add w3, w3, #1 str w2, [SELF_CPU_CONTROL, #PER_CPU_ISR_NEST_LEVEL] str w3, [SELF_CPU_CONTROL, #PER_CPU_THREAD_DISPATCH_DISABLE_LEVEL] /* Save LR */ mov x21, LR /* Call BSP dependent interrupt dispatcher */ bl bsp_interrupt_dispatch /* Restore LR */ mov LR, x21 /* Load some per-CPU variables */ ldr w0, [SELF_CPU_CONTROL, #PER_CPU_THREAD_DISPATCH_DISABLE_LEVEL] ldrb w1, [SELF_CPU_CONTROL, #PER_CPU_DISPATCH_NEEDED] ldr w2, [SELF_CPU_CONTROL, #PER_CPU_ISR_DISPATCH_DISABLE] ldr w3, [SELF_CPU_CONTROL, #PER_CPU_ISR_NEST_LEVEL] /* Decrement levels and determine thread dispatch state */ eor w1, w1, w0 sub w0, w0, #1 orr w1, w1, w0 orr w1, w1, w2 sub w3, w3, #1 /* Store thread dispatch disable and ISR nest levels */ str w0, [SELF_CPU_CONTROL, #PER_CPU_THREAD_DISPATCH_DISABLE_LEVEL] str w3, [SELF_CPU_CONTROL, #PER_CPU_ISR_NEST_LEVEL] /* Return should_skip_thread_dispatch in x0 */ mov x0, x1 /* Return from handler */ ret
主要做了如下事情,代码有注释,也很好理解
percpu结构体的四个变量如下
/** * This contains the current interrupt nesting level on this * CPU. */ uint32_t isr_nest_level; /** * @brief Indicates if an ISR thread dispatch is disabled. * * This flag is context switched with each thread. It indicates that this * thread has an interrupt stack frame on its stack. By using this flag, we * can avoid nesting more interrupt dispatching attempts on a previously * interrupted thread's stack. */ uint32_t isr_dispatch_disable; /** * @brief The thread dispatch critical section nesting counter which is used * to prevent context switches at inopportune moments. */ volatile uint32_t thread_dispatch_disable_level; /** * @brief This is set to true when this processor needs to run the thread * dispatcher. * * It is volatile since interrupts may alter this flag. * * This member is not protected by a lock and must be accessed only by this * processor. Code (e.g. scheduler and post-switch action requests) running * on another processors must use an inter-processor interrupt to set the * thread dispatch necessary indicator to true. * * @see _Thread_Get_heir_and_make_it_executing(). */ volatile bool dispatch_necessary;
根据上面的解析,此时中断会调整到bsp_interrupt_dispatch
中,然后寻找ISR程序运行,bsp_interrupt_dispatch
代码如下
void bsp_interrupt_dispatch(void) { while (true) { uint32_t icciar = READ_SR(ICC_IAR1); rtems_vector_number vector = GIC_CPUIF_ICCIAR_ACKINTID_GET(icciar); uint32_t status; if (!bsp_interrupt_is_valid_vector(vector)) { break; } status = arm_interrupt_enable_interrupts(); bsp_interrupt_handler_dispatch_unchecked(vector); arm_interrupt_restore_interrupts(status); WRITE_SR(ICC_EOIR1, icciar); } }
可以看的上述代码,它步骤如下
对于执行函数,如下
static inline void bsp_interrupt_dispatch_entries( const rtems_interrupt_entry *entry ) { do { ( *entry->handler )( entry->arg ); entry = bsp_interrupt_entry_load_acquire( &entry->next ); } while ( RTEMS_PREDICT_FALSE( entry != NULL ) ); }
值得注意的是,我们这里的handler就是之前注册的timer中断
Clock_driver_support_install_isr( Clock_isr );
也就是Clock_isr函数,这个函数也就是计算tick后重新激活timer。这里主要关心中断触发,接下来看中断返回
在AArch64_Interrupt_Handler
中,就介绍了中断返回的部分代码,这里简单复述一下
在AArch64_Interrupt_Handler
返回之后,还是回到中断向量表指定函数_AArch64_Exception_interrupt_no_nest
中了,这里继续做剩下的操作,如下
AArch64_Interrupt_Handler
返回之后,就直接恢复中断上下文,恢复的步骤就是将之前保存的寄存器恢复,函数是pop_interrupt_context
其实现如下
.macro pop_interrupt_context /* Pop fpcr and fpsr */ ldp x0, x1, [sp], #0x10 /* Restore fpcr and fpsr */ msr FPCR, x1 msr FPSR, x0 /* Pop pc and spsr */ ldp x0, x1, [sp], #0x10 /* Restore exception LR for PC and spsr */ msr SPSR_EL1, x1 msr ELR_EL1, x0 /* Pop q0-q31 */ ldp q30, q31, [sp], #0x20 ldp q28, q29, [sp], #0x20 ldp q26, q27, [sp], #0x20 ldp q24, q25, [sp], #0x20 ldp q22, q23, [sp], #0x20 ldp q20, q21, [sp], #0x20 ldp q18, q19, [sp], #0x20 ldp q16, q17, [sp], #0x20 ldp q14, q15, [sp], #0x20 ldp q12, q13, [sp], #0x20 ldp q10, q11, [sp], #0x20 ldp q8, q9, [sp], #0x20 ldp q6, q7, [sp], #0x20 ldp q4, q5, [sp], #0x20 ldp q2, q3, [sp], #0x20 ldp q0, q1, [sp], #0x20 /* Pop x1-x21 */ ldp x20, x21, [sp], #0x10 ldp x18, x19, [sp], #0x10 ldp x16, x17, [sp], #0x10 ldp x14, x15, [sp], #0x10 ldp x12, x13, [sp], #0x10 ldp x10, x11, [sp], #0x10 ldp x8, x9, [sp], #0x10 ldp x6, x7, [sp], #0x10 ldp x4, x5, [sp], #0x10 ldp x2, x3, [sp], #0x10 ldp lr, x1, [sp], #0x10 /* Must clear reservations here to ensure consistency with atomic operations */ clrex .endm
保存和恢复的步骤是差不多的,其步骤简单介绍如下
等到AArch64_Interrupt_Handler
返回之后,代码回到宏JUMP_HANDLER上,我们之前代码执行的blr,那么接下来做如下动作
blr x0 /* Pop x0,lr from stack */ ldp x0, lr, [sp], #0x10 /* Return from exception */ eret
这里从sp开始,去16个字节保存到x0和lr寄存器上,然后调用eret返回到中断前的状态,恢复ELR_EL1和SPSR_EL1寄存器。这里因为JUMP_HANDLER是宏,所以继续回溯到curr_el_spx_irq函数中来看ldp这条指令,如下
curr_el_spx_irq: stp x0, lr, [sp, #-0x10]! /* Push x0,lr on to the stack */ bl curr_el_spx_irq_get_pc /* Get current execution address */ curr_el_spx_irq_get_pc: /* The current PC is now in LR */ JUMP_HANDLER JUMP_TARGET_SPx .balign 0x80
可以看的,之前stp其实讲x0和lr的值保存在sp-0x10的位置,并修改了sp的值,所以eret之前做的就是恢复进入中断前的x0和lr寄存器。
至此,一次timer中断就完成返回到了中断开始之前的状态。
主动调用线程调度的函数是_AArch64_Exception_thread_dispatch
代码如下
_AArch64_Exception_thread_dispatch: /* Get per-CPU control of current processor */ GET_SELF_CPU_CONTROL SELF_CPU_CONTROL_GET_REG /* Thread dispatch */ mrs NON_VOLATILE_SCRATCH, DAIF .Ldo_thread_dispatch: /* Set ISR dispatch disable and thread dispatch disable level to one */ mov w0, #1 str w0, [SELF_CPU_CONTROL, #PER_CPU_ISR_DISPATCH_DISABLE] str w0, [SELF_CPU_CONTROL, #PER_CPU_THREAD_DISPATCH_DISABLE_LEVEL] /* Save LR */ mov x21, LR /* Call _Thread_Do_dispatch(), this function will enable interrupts */ mov x0, SELF_CPU_CONTROL mov x1, NON_VOLATILE_SCRATCH mov x2, #0x80 bic x1, x1, x2 bl _Thread_Do_dispatch /* Restore LR */ mov LR, x21 /* Disable interrupts */ msr DAIF, NON_VOLATILE_SCRATCH #ifdef RTEMS_SMP GET_SELF_CPU_CONTROL SELF_CPU_CONTROL_GET_REG #endif /* Check if we have to do the thread dispatch again */ ldrb w0, [SELF_CPU_CONTROL, #PER_CPU_DISPATCH_NEEDED] cmp w0, #0 bne .Ldo_thread_dispatch /* We are done with thread dispatching */ mov w0, #0 str w0, [SELF_CPU_CONTROL, #PER_CPU_ISR_DISPATCH_DISABLE] /* Return from thread dispatch */ ret
这里好像没什么好详细讲的了,其实就是主动调用_Thread_Do_dispatch
,主动让调度器开始下一个高优先级任务。
至此,本文详细的通过timer中断介绍了中断触发的详细过程。有助于了解RTEMS在aarch64上,以及gic-v3的中断管理流程
在《RTEMS初始化-bootcard调用流程》中就已经简单初始化中断向量表的过程,但其目的是为了梳理bootcard的调用流程,本文基于RTEMS的中断管理逻辑来分析RTEMS的中断管理功能,从而更清晰的了解RTEMS的中断管理
中断向量表的填充在aarch64-exception-default.S
中,里面会实现全局变量bsp_start_vector_table_begin的值,根据aarch64的定义中断有如下基本点
所以我们可以看到rtems填入中断的方式如下
curr_el_sp0_sync: .dword _AArch64_Exception_default .balign 0x80 curr_el_sp0_irq: JUMP_HANDLER JUMP_TARGET_SP0 .balign 0x80 curr_el_sp0_fiq: JUMP_HANDLER JUMP_TARGET_SP0 .balign 0x80 curr_el_sp0_serror: JUMP_HANDLER JUMP_TARGET_SP0 .balign 0x80 curr_el_spx_sync: .dword _AArch64_Exception_default .balign 0x80 curr_el_spx_irq: JUMP_HANDLER JUMP_TARGET_SPx .balign 0x80 curr_el_spx_fiq: JUMP_HANDLER JUMP_TARGET_SPx .balign 0x80 curr_el_spx_serror: JUMP_HANDLER JUMP_TARGET_SPx .balign 0x80 lower_el_aarch64_sync: JUMP_HANDLER JUMP_TARGET_SPx .balign 0x80 lower_el_aarch64_irq: JUMP_HANDLER JUMP_TARGET_SPx .balign 0x80 lower_el_aarch64_fiq: JUMP_HANDLER JUMP_TARGET_SPx .balign 0x80 lower_el_aarch64_serror: JUMP_HANDLER JUMP_TARGET_SPx .balign 0x80 lower_el_aarch32_sync: JUMP_HANDLER JUMP_TARGET_SPx .balign 0x80 lower_el_aarch32_irq: JUMP_HANDLER JUMP_TARGET_SPx .balign 0x80 lower_el_aarch32_fiq: JUMP_HANDLER JUMP_TARGET_SPx .balign 0x80 lower_el_aarch32_serror: JUMP_HANDLER JUMP_TARGET_SPx
可以看到,上述代码和arm64 spec描述完全一致
这部分内容在《RTEMS初始化-bootcard调用流程》提过,这里基于gic-v3再简单描述一下。
bsp_start bsp_interrupt_initialize bsp_interrupt_facility_initialize arm_interrupt_facility_set_exception_handler AArch64_set_exception_handler AArch64_get_vector_base_address char *vbar = VBAR_EL1 char *cvector_address = vbar + VECTOR_ENTRY_SIZE * exception + VECTOR_POINTER_OFFSET;
初始化共两个函数,我们逐步解析
gicv3_init_dist(ARM_GIC_DIST); gicv3_init_cpu_interface(_SMP_Get_current_processor());
对于init dist,如下
static void gicv3_init_dist(volatile gic_dist *dist) { uint32_t id_count = gicv3_get_id_count(dist); uint32_t id; dist->icddcr = GIC_DIST_ICDDCR_ARE_NS | GIC_DIST_ICDDCR_ARE_S | GIC_DIST_ICDDCR_ENABLE_GRP1S | GIC_DIST_ICDDCR_ENABLE_GRP1NS | GIC_DIST_ICDDCR_ENABLE_GRP0; for (id = 0; id < id_count; id += 32) { /* Disable all interrupts */ dist->icdicer[id / 32] = 0xffffffff; /* Set G1NS */ dist->icdigr[id / 32] = 0xffffffff; dist->icdigmr[id / 32] = 0; } for (id = 0; id < id_count; ++id) { gic_id_set_priority(dist, id, PRIORITY_DEFAULT); } for (id = 32; id < id_count; ++id) { gic_id_set_targets(dist, id, 0x01); } }
对于icddcr,对于GICD_CTLR寄存器,设置如下
dist->icddcr = GIC_DIST_ICDDCR_ARE_NS | GIC_DIST_ICDDCR_ARE_S | GIC_DIST_ICDDCR_ENABLE_GRP1S | GIC_DIST_ICDDCR_ENABLE_GRP1NS | GIC_DIST_ICDDCR_ENABLE_GRP0;
这里功能描述如下
这是distributor寄存器,其中开启了
也就是说这里开启了中断优先级路由和中断分发
对于dist->icdicer[id / 32] = 0xffffffff
这里对应寄存器GICD_ISENABLER,写1先禁用所有中断
对于dist->icdipr[id] = priority
这里对于寄存器GICD_IPRIORITYR,写入实际的优先级
对于dist->icdiptr[id] = targets;
这里对于寄存器GICD_ITARGETSR,写入中断处理目标寄存器(发给哪个CPU)
其他还有寄存器如下
volatile gic_redist *redist = gicv3_get_redist(cpu_index);
redistributor寄存器地址
在gdb中,我们可以查看中断向量表基地址为bsp_start_vector_table_begin,如下
0x6d000 <bsp_start_vector_table_begin>
对于中断的入口函数,其地址是entry + 0x78。 因为向量表的offset就是0x78,如下
#define VECTOR_POINTER_OFFSET 0x78
如果是sp0_irq,那么其地址是0x6dc98,0x6dc98是入口地址_AArch64_Exception_interrupt_nest
(gdb) x curr_el_sp0_irq + 0x78 0x6d0f8 <curr_el_sp0_irq_get_pc+112>: 0x000000000006dc98 (gdb) x 0x000000000006dc98 0x6dc98 <_AArch64_Exception_interrupt_nest>:
如果是spx_irq,那么其地址是0x6ddac,0x6ddac是入口地址_AArch64_Exception_interrupt_no_nest
(gdb) x curr_el_spx_irq + 0x78 0x6d2f8 <curr_el_spx_irq_get_pc+112>: 0x000000000006ddac (gdb) x 0x000000000006ddac 0x6ddac <_AArch64_Exception_interrupt_no_nest>:
对于未设置入口的sp0,其宏定义如下
.macro JUMP_TARGET_SP0 .dword .print_exception_dump_sp0 .endm
以curr_el_sp0_fiq为例也就是.print_exception_dump_sp0 其他类似
(gdb) x curr_el_sp0_fiq + 0x78 0x6d178 <curr_el_sp0_fiq_get_pc+112>: 0x000000000006d844 (gdb) x 0x000000000006d844 0x6d844 <.print_exception_dump_sp0>:
对于未设置入口的spx,其宏定义如下
.macro JUMP_TARGET_SPx .dword .print_exception_dump_spx .endm
以curr_el_spx_fiq为例也就是.print_exception_dump_spx,值得注意的是.print_exception_dump_spx的地址等于bsp_start_vector_table_end 其他类似
(gdb) x curr_el_spx_fiq + 0x78 0x6d378 <curr_el_spx_fiq_get_pc+112>: 0x000000000006d800 (gdb) x 0x000000000006d800 0x6d800 <bsp_start_vector_table_end>:
这里讲清楚了rtems中的中断向量表和gic-v3的中断初始化过程,接下来我们从中断触发的角度继续了解中断管理
关于内存泄漏这里就不说明了,我们知道在调试过程中,很多情况下并不算内存泄漏,例如分配的内存就是需要使用的,某些机制就需要申请内存,对象销毁才回收内存,那么它就不能算作内存泄漏,为了定位这种内存的使用情况,从而减少内存的使用情况,massif工具能够刨析内存的使用情况,然后结合代码解决这种问题。
安装很简单,我们需要具备valgrind和massif-visualizer两个工具,如下
apt install massif-visualizer valgrind
我们借助valgrind来加载massif工具,如下命令即可
valgrind --tool=massif ./ukui-tablet-desktop
当我们程序运行之后,可以将程序退出,那么本地会生成massif.out.pid的文件,根据这个文件就可以进行分析内存使用情况
分析通过massif-visualizer,命令举例如下
massif-visualizer massif.out.31446
此时我们可以看到界面如下
这里两个信息很重要
为了方便查看,我特地扩展了一下右侧的信息,这样就能够很方便的知道内存占用究竟是在哪里了。如下
这里拿峰值的72m为例,在72m的节点上,我们知道渲染使用了24M,QImage加载图片用了11M QImage构造用了2M。这都是实实在在的内存使用情况,展开就能看到函数调用。
massif能够查看内存占用,这些默认是基于上层代码函数的,如果是基于底层malloc, calloc, realloc, memalign, new等函数的,那么我们需要添加一个参数 --pages-as-heap=yes 即可。这样我们查看内存的使用情况关注点就来到了glibc上或自写的内存池上了。
为什么要这样查看呢,其实问题到这里的时候已经不是内存泄漏了,而是内存使用情况了,如果不去看底层内存管理机制,那么想降低内存的使用情况只能动复用内存或内存池或自行实现内存管理。所以才有--pages-as-heap=yes的出现。
那么演示如下:
首先我们收集日志如下
valgrind --tool=massif --pages-as-heap=yes ./ukui-tablet-desktop
退出程序之后,我们打开massif的日志界面 如下,这里我直接在峰值展开如下
这里我们就可以看到glibc内存管理的细节了,我们看到mmap arena tcache malloc free dl_map_segments 这类底层函数内存占用了。我们可以通过这些信息,为内存管理投其所好的进行内存优化,例如常用fastbin,大内存直接mmap等方式减少内存碎片。
了解glibc的简单原理可看我之前的文章。
并不是所有的内存问题都是泄漏,很多情况下其实是内存占用太大而已,实际还是需要使用的,那么这种情况下我们需要使用massif来查看内存布局,massif可以从两个角度调试,一个是应用角度,根据信息结合代码来进行初步调优,另一个是内存管理角度,根据信息结合glibc内存管理策略或自己写的内存管理程序策略来优化内存的分配,提高小块内存的利用率和减少大块内存的重复分配以及减少内存碎片。
这里介绍了massif,后续如果定位非内存泄漏问题会非常方便。
massif可以参考如下文章,原理性的东西我就不二转手了。
heaptrack是kde的内存调试工具,我们在定位内存泄漏的时候,为了方便定位可以安装此工具来排查,下面是heaptrack的使用方法
安装方式如下
apt install heaptrack heaptrack-gui libheaptrack
heaptrack不需要太多的参数支持,直接heaptrack追加需要监控的进程即可,如下
heaptrack ./ukui-tablet-desktop
因为我们调试内存泄漏的时候,大概是知道泄漏的点或者行为,此时进入程序触发泄漏行为即可。当泄漏出现的时候,我们关掉程序,可以在当前目录获得名字为heaptrack.name.pid.gz
类似的文件,此时直接加载此调试文件即可,如下
直接使用heaptrack_gui即可,如下
heaptrack_gui /home/kylin/heaptrack.ukui-tablet-desktop.29058.gz
此时我们可以看到图片如下
这里已经大概告诉你泄漏的函数原因了,可以继续追代码了。
如果我们需要看详细的统计数据,我们可以跳转到Flame Graph中,当然任何内存泄漏都有一定阈值,我们可以设置Cost Threshold即可,这里设置10%。如下所示
这里就直接看到超过10%的Cost的调用,直接就能看出原因。
虽然上面可以查看到简单的内存泄漏,但是内存泄漏是长期的,我们需要长时间盯着内存的消耗增长,那么我们可以挂机72小时后,在Consumed中查看其增长情况,如下。
此时如果是长时间挂机,内存不是趋于平稳,那么就说明内存泄漏比较明显了。否则我们只能说内存会被一定情况下被使用。
至此,我们基于heaptrack能够很方便的定位内存泄漏问题了,它不同于valgrind,大型的程序使用valgrind会导致性能十分低下,不方便排查问题。而像kde这种大型qt项目或其他,heaptrack是一个非常不错的选择。
之前把rtems上支持的所有的调度器介绍了一遍,但是一直都没有说上下文是如何切换的。其实上下文切换的逻辑就是保存线程TCB和通用寄存器,然后根据新的线程TCB信息进行运行。本文从上下文切换的角度来讲清楚RTEMS中是怎么完成上下文切换的
对于任何的任务,调用schedule就是根据当前任务的情况执行调度,那么主动调用如下函数
( *scheduler->Operations.schedule )( scheduler, the_thread );
这里不同的调度算法实现的回调不一样,为了方便介绍,这里以优先级调度为例,那么最后调用函数如下
static inline void _Scheduler_uniprocessor_Update_heir( Thread_Control *heir, Thread_Control *new_heir ) { _Assert( heir != new_heir ); #if defined(RTEMS_SMP) /* * We need this state only for _Thread_Get_CPU_time_used_locked(). Cannot * use _Scheduler_Thread_change_state() since THREAD_SCHEDULER_BLOCKED to * THREAD_SCHEDULER_BLOCKED state changes are illegal for the real SMP * schedulers. */ heir->Scheduler.state = THREAD_SCHEDULER_BLOCKED; new_heir->Scheduler.state = THREAD_SCHEDULER_SCHEDULED; #endif _Thread_Update_CPU_time_used( heir, _Thread_Get_CPU( heir ) ); _Thread_Heir = new_heir; _Thread_Dispatch_necessary = true; }
再设置完一些state之后,我们重点关注如下两个值的设置
_Thread_Heir = new_heir; _Thread_Dispatch_necessary = true;
这里的new_heir是即将要运行的任务,他是Thread_Control * 指针。而_Thread_Dispatch_necessary是Per_CPU_Control结构体的一个成员dispatch_necessary,我们通过dispatch_necessary来判断当前任务是否要scheduler。
我们当任务在合适的实际,我们可以设置dispatch enable,或者直接调用_Thread_Do_dispatch函数,这样线程就直接开始开始调度的实际行为。 我们看起核心代码如下
do { Thread_Control *heir; heir = _Thread_Get_heir_and_make_it_executing( cpu_self ); if ( heir == executing ) { break; } _ISR_Local_enable( level ); _Thread_Save_fp( executing ); _Context_Switch( &executing->Registers, &heir->Registers ); _Thread_Restore_fp( executing ); _User_extensions_Thread_switch( NULL, executing ); cpu_self = _Per_CPU_Get(); _ISR_Local_disable( level ); } while ( cpu_self->dispatch_necessary );
可以看到,只要dispatch_necessary设置为true,那么任务就会执行调度。执行调度的函数为_Context_Switch,它会根据每个架构的实现来运行上下文切换,我们基于armv8架构来介绍
这里主要是Arm64的实现,先看源码
DEFINE_FUNCTION_AARCH64(_CPU_Context_switch) .globl _CPU_Context_switch_no_return .set _CPU_Context_switch_no_return, _CPU_Context_switch #ifdef AARCH64_MULTILIB_ARCH_V8_ILP32 /* Sanitize inputs for ILP32 ABI */ mov w0, w0 mov w1, w1 #ifdef RTEMS_SMP #define reg_2 x2 #else #define reg_2 w2 #endif #else #define reg_2 x2 #endif /* Start saving context */ GET_SELF_CPU_CONTROL reg_2 ldr w3, [x2, #PER_CPU_ISR_DISPATCH_DISABLE] // 读取isr_dispatch_disable stp x19, x20, [x0] // 保存aapcs中规定的callee寄存器 也就是x19-x28 stp x21, x22, [x0, #0x10] stp x23, x24, [x0, #0x20] stp x25, x26, [x0, #0x30] stp x27, x28, [x0, #0x40] stp fp, lr, [x0, #0x50] // 保存fp和lr mov x4, sp // 保存sp寄存器 str x4, [x0, #0x60] #ifdef AARCH64_MULTILIB_VFP add x5, x0, #AARCH64_CONTEXT_CONTROL_D8_OFFSET stp d8, d9, [x5] // 保存浮点寄存器 stp d10, d11, [x5, #0x10] stp d12, d13, [x5, #0x20] stp d14, d15, [x5, #0x30] #endif str x3, [x0, #AARCH64_CONTEXT_CONTROL_ISR_DISPATCH_DISABLE] //将本线程的isr_dispatch_disable设置到x0 #ifdef RTEMS_SMP /* * The executing thread no longer executes on this processor. Switch * the stack to the temporary interrupt stack of this processor. Mark * the context of the executing thread as not executing. */ dmb SY //所有数据dmb add sp, x2, #(PER_CPU_INTERRUPT_FRAME_AREA + CPU_INTERRUPT_FRAME_SIZE) //分配一个栈 mov x3, #0 //将0写到is_executing中 strb w3, [x0, #AARCH64_CONTEXT_CONTROL_IS_EXECUTING_OFFSET] .L_check_is_executing: /* Check the is executing indicator of the heir context */ add x3, x1, #AARCH64_CONTEXT_CONTROL_IS_EXECUTING_OFFSET //获取is_executing的值 ldaxrb w4, [x3] //原子读取x3的内容到w4 cmp x4, #0 bne .L_get_potential_new_heir //如果已经其他cpu执行则跳到其他线程tcb上重新判断 /* Try to update the is executing indicator of the heir context */ mov x4, #1 stlxrb w5, w4, [x3] // 将is_executing设置回去,结果保存在w5 cmp x5, #0 bne .L_get_potential_new_heir // 如果设置失败,则跳到其他tcb上重新判断 dmb SY // dmb所有数据 #endif /* Start restoring context */ .L_restore: #if !defined(RTEMS_SMP) && defined(AARCH64_MULTILIB_HAS_LOAD_STORE_EXCLUSIVE) clrex // 清空独占监视器 #endif ldr x3, [x1, #AARCH64_CONTEXT_CONTROL_THREAD_ID_OFFSET] // 加载下一个线程的thread_id ldr x4, [x1, #AARCH64_CONTEXT_CONTROL_ISR_DISPATCH_DISABLE] // 加载下一个线程的isr_dispatch_disable #ifdef AARCH64_MULTILIB_VFP add x5, x1, #AARCH64_CONTEXT_CONTROL_D8_OFFSET // 加载下一个线程的浮点寄存器 ldp d8, d9, [x5] ldp d10, d11, [x5, #0x10] ldp d12, d13, [x5, #0x20] ldp d14, d15, [x5, #0x30] #endif msr TPIDR_EL0, x3 // 将x3设置到TPIDR_EL0 str w4, [x2, #PER_CPU_ISR_DISPATCH_DISABLE] // 更新isr_dispatch_disable ldp x19, x20, [x1] // 恢复下一个线程的callee寄存器 ldp x21, x22, [x1, #0x10] ldp x23, x24, [x1, #0x20] ldp x25, x26, [x1, #0x30] ldp x27, x28, [x1, #0x40] ldp fp, lr, [x1, #0x50] // 恢复fp和lr ldr x4, [x1, #0x60] mov sp, x4 //恢复sp ret
上面代码尽可能给出了注释,有些补充解释如下
我们知道_CPU_Context_switch的原型是:
_CPU_Context_switch( _executing, _heir )
所以x0 是当前线程tcb,x1是下一个线程的tcb
我们知道需要保存x19-x30 sp等寄存器,那么保存的地方在TPIDR_EL0上,TPIDR_EL0上存在的是Context_Control的指针,所以Context_Control结构体如下。
typedef struct { uint64_t register_x19; uint64_t register_x20; uint64_t register_x21; uint64_t register_x22; uint64_t register_x23; uint64_t register_x24; uint64_t register_x25; uint64_t register_x26; uint64_t register_x27; uint64_t register_x28; uint64_t register_fp; uint64_t register_lr; uint64_t register_sp; uint64_t isr_dispatch_disable; uint64_t thread_id; #ifdef AARCH64_MULTILIB_VFP uint64_t register_d8; uint64_t register_d9; uint64_t register_d10; uint64_t register_d11; uint64_t register_d12; uint64_t register_d13; uint64_t register_d14; uint64_t register_d15; #endif #ifdef RTEMS_SMP volatile bool is_executing; #endif } Context_Control;
dmb是数据内存同步,SY是对全系统进行刷新,SY的解释如下
Clear Exclusive access monitor,清空独占监视器,请问接下来要做独占访问,这里直接清空独占监视器
这两个都是独占访问内存和独占存储内存,
其中stlxrb会将"<Ws>"的值返回出来,如果是0则独占访问成功,如果非0则失败
至此,我们根据上下文切换的代码分析,清楚的知道了调度器在任务运行时的寄存器保存过程和sp分配过程等。相关演示操作在之前的文章有过体现,gdb能够很清楚的看到任务分配后的寄存器显示,这里就没有必要重复了。