根据之前的了解,我们知道了rtems启动需要经过多个步骤,现在我们根据代码分析,记录一下初始化流程
总流程在函数rtems_initialize_executive中,其实现如下:
void rtems_initialize_executive(void) { const rtems_sysinit_item *item; /* Invoke the registered system initialization handlers */ RTEMS_LINKER_SET_FOREACH( _Sysinit, item ) { ( *item->handler )(); } _Syst_Malloc_Initializeem_state_Set( SYSTEM_STATE_UP ); _SMP_Request_start_multitasking(); _Thread_Start_multitasking(); /******************************************************************* ******************************************************************* ******************************************************************* ****** APPLICATION RUNS HERE ****** ****** THE FUNCTION NEVER RETURNS ****** ******************************************************************* ******************************************************************* *******************************************************************/ }
故分析可知,其主要调用如下:
_Workspace_Handler_initialization _Malloc_Initialize bsp_start zynq_uart_kernel_init _User_extensions_Handler_initialization rtems_initialize_data_structures _Scheduler_Ensure_exactly_one_processor _RTEMS_tasks_Manager_initialization _Thread_Create_idle bsp_r1_heap_extend rtems_libio_init rtems_filesystem_initialize _Console_simple_Initialize _RTEMS_tasks_Initialize_user_task rtems_libio_post_driver _SMP_Request_start_multitasking _Thread_Start_multitasking
初始化堆工作区
_Workspace_Handler_initialization _Workspace_Initialize_for_one_area _Heap_Initialize _Heap_Get_first_and_last_block
初始化malloc
_Malloc_Initialize _Malloc_Initialize_for_one_area _Heap_Initialize _Heap_Get_first_and_last_block
bsp主要初始化中断向量表和增加一致性cache区域,并初始化ecc
bsp_start bsp_interrupt_initialize bsp_interrupt_facility_initialize AArch64_set_exception_handler AArch64_get_vector_base_address VBAR_EL1 *vector_address = handler; /* Execution template: Save volatile regs on interrupt stack Execute irq handler Restore volatile regs from interrupt stack Return to embedded exception vector code */ _AArch64_Exception_interrupt_nest AArch64_Interrupt_Handler bsp_interrupt_dispatch bsp_interrupt_handler_dispatch_unchecked bsp_interrupt_dispatch_entries ( *entry->handler )( entry->arg ); /* 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 */ _AArch64_Exception_interrupt_no_nest AArch64_Interrupt_Handler _AArch64_Exception_thread_dispatch rtems_cache_coherent_add_area add_area _Heap_Initialize _Heap_Get_first_and_last_block zynqmp_ecc_init
串口初始化,这里后面的字符输出通过zynq_uart_kernel_output_char回调到每个字符的输出
zynq_uart_kernel_init zynq_uart_initialize zynq_uart_kernel_output_char
用户扩展的初始化handler,因为默认没有,故不分析
_User_extensions_Handler_initialization _Chain_Append_unprotected
这里完成cpu,thread,isr,scheduler,smp的初始化
rtems_initialize_data_structures _CPU_Initialize _Thread_Dispatch_initialization _ISR_Handler_initialization _Thread_Handler_initialization _Scheduler_Handler_initialization _Scheduler_priority_Initialize _Scheduler_priority_Ready_queue_initialize _SMP_Handler_initialize _CPU_SMP_Finalize_initialization rtems_interrupt_entry_install # IPI 为bsp_inter_processor_interrupt bsp_interrupt_entry_install bsp_interrupt_entry_install_first
这里只是断言了cpu为1个
_RTEMS_tasks_Manager_initialization _Thread_Initialize_information _User_extensions_Add_API_set _User_extensions_Add_set
创建idle线程
_Thread_Create_idle _Thread_Create_idle_for_CPU _Thread_Initialize _Thread_Try_initialize _User_extensions_Thread_create # 创建线程 cpu->executing = idle; _System_state_Set( SYSTEM_STATE_BEFORE_MULTITASKING );
扩展的heap
libio初始化
imfs的初始化,设置root
rtems_filesystem_initialize mount register_root_file_system mkdir mknod rtems_filesystem_mknod IMFS_mknod
添加/dev/console
_Console_simple_Initialize IMFS_add_node
设置Init函数
_RTEMS_tasks_Initialize_user_task # _RTEMS_tasks_User_task_table/Init rtems_task_create _RTEMS_tasks_Create _RTEMS_tasks_Allocate _Thread_Initialize
rtems_libio_post_driver open # /dev/console
等待状态PER_CPU_STATE_READY_TO_START_MULTITASKING
SYSTEM_STATE_UP _SMP_Request_start_multitasking _SMP_Wait_for_ready_to_start_multitasking _SMP_Try_to_process_message _SMP_Process_message SMP_MESSAGE_SHUTDOWN/SMP_MESSAGE_PERFORM_JOBS/SMP_MESSAGE_FORCE_PROCESSING
启动线程任务
_Thread_Start_multitasking _CPU_Start_multitasking _AArch64_Start_multitasking .L_check_is_executing .L_restore ldp fp, lr, [x1, #0x50] # _CPU_Context_switch_no_return _Thread_Handler _Thread_Entry_adaptor_numeric Init
至此,我们完成了rtems的全部的启动流程,可以看到,最后_Thread_Start_multitasking
会主动回调在rtems_task_start传入的adaptor函数指针,而这个指针指向Init函数。操作系统启动完成。
有些寄存器不太清晰,这里通过aarch64的芯片体系架构规范查询后留作记录,方便记忆
这里bit[2:3]判断当前处理elX。
系统控制寄存器,包括内存相关的控制
这里需要注意I,C,M位分别是
用于保持内核级的线程结构指针
内存屏障指令
获取cpu亲和性寄存器
这里bit[7:0]是PE下的亲和性寄存器值
此寄存器可以查询设置的物理地址范围,寄存器描述如下:
我们留意parange,如下解释
TCR寄存器是Translation Control Register,这里用来控制虚拟地址到物理地址转换的一些特性,本文介绍其中一些bit的含义
其寄存器描述如下:
这里TCR_EL1用作EL1下的虚拟地址到物理地址转换信息,能够设置虚拟地址大小,页表格式,缓存策略,访问权限等。下面详细介绍一些位
T0SZ用作设置ttbr0的size,顾名思义,具体解释如下
配置inner内存属性为 如果是1,则是write-back,read-allocate,write-allocate cacheable 。
orgn0 是配置outer 如果是1 则内存属性为 write-back,read-allocate,write-allocate cacheable 。
配置cache的模式,如果是11则为inner shareable
配置虚拟地址页面大小,如果是00,则是4k
配置物理地址大小,如果是0x101则 48bit 256TB
配置tlb miss后,是否遍历ttbrx,还是触发translation fault
aarch64中,有一个专门配置内存属性表的寄存器,为MAIR,本文基于MAIR寄存器讲解其作用
对于虚拟内存地址,我们知道其分布如下
这里我们只关注AttrIndx[2:0],可以知道描述如下:
这里stage 1就说ttbrx到pte的过程,这里的bits [4:2]指示的是MAIR寄存器中attr的索引。而MAIR寄存器如下 以el1为例
这里mair有8个attr,如下
这里的attr的值含义如下
其中熟悉可以分为设备内存和正常内存,如下
通过dd位区分,如下
这里我们看到GRE三个概念,解释如下
关于memory的shareable补充如下:
在TCR_EL1寄存器中,关注sh1 bit[29:28]如下:
这里可以看到
对于aarch64,cpu按照cluster划分,根据cluster的share规则如下
这里的inner指的是一个cluster内,outer指的非一个cluster内
而对于Normal Memory,通过0booooiiii来区分,如下解释
这里oooo是高位,iiii是低位。可以看到,这里对于Normal memory而言,我们看到了cacheable的如下属性
这里写通指的是内存写操作,直接更新到缓存和内存中,也就是缓存和内存数据是一致的
这里写回指的是内存写操作,只更新到缓存中,推迟对内存的更新,直到该缓存行被替换时才写回内存,这种情况下我们可以跟踪dirty bit,如果脏位是1,则缓存和内存的数据不一致
处理cacheable的write方式,还有transient 标志位
如果带有transient ,则表示内存使用时间很短,可以利用此标志来优先更新cache line
除了这些,我们还可以注意到RW标志,如下解释
这里的RW分别是 Read-allocate和Write-allocate,解释如下:
read的时候,如果出现miss,则从内存中读取数据,然后申请一个cache line,并记录
write的时候,如果出现miss,先写到内存中,然后申请一个cache line,并记录
如果都是0,则代表No allocate,解释如下
假设我设置的MAIR_EL1的值是0xffffffffff440400,这里计算如下
可以知道,dd也是00,便知道这里是如下:
也就是 设备内存,不执行合并,不执行指令重排,不执行提前写应答
可以知道,dd是01,便知道如下:
也就是 设备内存,不执行合并,不执行指令重排,可以提前写应答
0x44,对应如下
对于oooo如下:
对于iiii如下:
也就是 正常内存,cluster内和cluster之间 都是non-cacheable的,所以的读写操作都是没有cache的。
0xff,对于如下
对于oooo如下:
对于iiii如下:
这里rw也是11,如下解释
这里是 正常内存,非短暂内存,读 allocate, 写 allocate。 简单来说就是所有的内存带cache,读写操作都经过cache
rtems的内存初始化在bootcard解析的时候已经提到了,这里为了了解系统的内存申请情况,以c库的申请函数为例,探索内存的申请效果
回顾RTEMS初始化-bootcard调用流程可以知道,_Malloc_Initialize
会初始化内存,其调用如下:
void _Malloc_Initialize( void ) { RTEMS_Malloc_Heap = ( *_Workspace_Malloc_initializer )(); }
我们知道,这里赋值了一个函数指针_Workspace_Malloc_initializer
的返回值,跟踪函数可以查找到默认赋值为了_Workspace_Malloc_initialize_separate
,此函数的实现如下:
Heap_Control *_Workspace_Malloc_initialize_separate( void ) { return _Malloc_Initialize_for_one_area( &_Malloc_Heap ); }
我们留意static Heap_Control _Malloc_Heap
的全局变量,其在_Malloc_Initialize_for_one_area
中给了RTEMS_Malloc_Heap
,并调用了_Heap_Initialize
。_Heap_Initialize
已经分析过了,这里重点记住Heap_Control结构体如下
struct Heap_Control { Heap_Block free_list; uintptr_t page_size; uintptr_t min_block_size; uintptr_t area_begin; uintptr_t area_end; Heap_Block *first_block; Heap_Block *last_block; Heap_Statistics stats; };
对于每个malloc申请的内存,都对应一个Heap_Block,这里结构体如下:
struct Heap_Block { uintptr_t prev_size; uintptr_t size_and_flag; Heap_Block *next; Heap_Block *prev; };
可以发现,这个和glibc有点相似。但是是简化版本的。简单解释一下:
prev_size是前一个block的大小,如果size_and_flag的bit0为1(HEAP_PREV_BLOCK_USED),也就是正在使用则无效
size_and_flag是整个block大小和是否HEAP_PREV_BLOCK_USED的flag的判断
next是下一个block的地址,双向链表
prev是上一个block的地址,双向链表
为了测试malloc,需要如下测试程序
static void test_early_malloc( void ) { void *p; char *q; void *r; void *s; void *t; p = malloc( 1 ); rtems_test_assert( p != NULL ); free( p ); q = calloc( 1, 1 ); rtems_test_assert( q != NULL ); rtems_test_assert( p != q ); rtems_test_assert( q[0] == 0 ); free( q ); /* * This was added to address the following warning. * warning: pointer 'q' used after 'free' */ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wuse-after-free" r = realloc( q, 128 ); rtems_test_assert( r == q ); #pragma GCC diagnostic pop s = malloc( 1 ); rtems_test_assert( s != NULL ); free( s ); t = realloc( r, 256 ); rtems_test_assert( t != NULL ); rtems_test_assert( t != r ); free( t ); }
演示通过gdb,可以断点在函数test_early_malloc,此时我们先查看_Malloc_Heap 地址,如下
(gdb) p _Malloc_Heap $28 = {free_list = {prev_size = 0, size_and_flag = 0, next = 0x1342a0, prev = 0x1342a0}, page_size = 16, min_block_size = 32, area_begin = 1262239, area_end = 1072431104, first_block = 0x1342a0, last_block = 0x3febfff0, stats = {lifetime_allocated = 0, lifetime_freed = 0, size = 1071168848, free_size = 1071168848, min_free_size = 1071168848, free_blocks = 1, max_free_blocks = 1, used_blocks = 0, max_search = 0, searches = 0, allocs = 0, failed_allocs = 0, frees = 0, resizes = 0}} (gdb) p &_Malloc_Heap $30 = (Heap_Control *) 0x102c90 <_Malloc_Heap>
我们记住这个0x102c90和首个malloc地址0x1342b0。打印如下
(gdb) x/2g 0x1342a0 0x1342a0: 0x000000003fec0000 0x000000003fd8bd51
此时代码malloc(1)后,得到虚拟地址0x1342b0,对应p地址,我们查看p的block如下:
(gdb) x/4g p-0x10 0x1342a0: 0x000000003fec0000 0x0000000000000021 0x1342b0: 0x0000000000102c90 0x0000000000102c90
这里可以知道prev_size默认是area_end,size_and_flag是0x21,最低位1是HEAP_PREV_BLOCK_USED,代表正在使用,大小是32字节,next和prev相等则这是第一个内存。而0x102c90正好是默认heap的地址_Malloc_Heap
这里我们可以知道一个block的大小就是32字节。故malloc(1)默认是一个block的size。
接下来calloc(1,1)。获得了q的地址是0x1342d0,可以发现刚刚是0x1342b0 + 0x20。一切正常,此时打印q如下
(gdb) x/4g q-0x10 0x1342c0: 0x0000000000000000 0x0000000000000021 0x1342d0: 0x0000000000102c00 0x0000000000102c90
然后realloc( q, 128 ) 扩大q的大小,扩大之前我们知道size是32,扩大后如下
(gdb)x/4g r-0x10 0x1342c0: 0x0000000000000000 0x0000000000000091 0x1342d0: 0x0000000000100140 0x00000000001342b0
这里看到0x91,其中0x90是144,刚好是128+16。这里16正好是uintptr_t prev_size; + uintptr_t size_and_flag;的大小。一切正常
然后s = malloc( 1 );,这里理所应当是从0x1342d0+0x90,也就是0x134360。查看内存如下:
(gdb) x/4g s-0x10 0x134350: 0x0000000000000000 0x0000000000000021 0x134360: 0x0000000000102c90 0x0000000000102c90
完全没问题,最后t = realloc( r, 256 );将r的地址扩大256。因为r中间有一个s,所以这里t的地址发生改变,如下
(gdb) x/4g t-0x10 0x134370: 0x0000000000000000 0x0000000000000111 0x134380: 0x0000000000134360 0x00000000001342b0
因为是调用的realloc,所以链表的next指向了0x0000000000134360,prev是0x00000000001342b0,size是0x110也就是272,也就是256 + 16.一切正常。
至此,我们演示了rtmes通过malloc调用的内存行为,其行为是简化的glibc的分配行为。简单易懂。