内存与MMU缓存dCache与iCache每个CPU会有两个L1 Cache,分别是dCache和iCache,操作缓存的最小单元是缓存行(cache line)而不是字节缓存一致性iCache一致性一般来说iCache是只读的 不会存在一致性问题 但是我们调试的时候可能需要修改指令如果我们的指令只是写入了D-Cache但是I-Cache还是老指令 就可能引起崩溃你dCache一致性写回:write-back仅更新Cache的对应缓存行并标记为脏 仅当线路被逐出或明确清楚才更新内存写透:write-through同时更新dCache和内存 不标记为脏多核缓存一致性通过MESI协议保证一个核心的修改是其它核心可见的为了进一步优化性能,还在Cache与CPU之间引入了store_buffer,而这也带来的新的问题。具体可以看这篇文章:https://blog.csdn.net/wll1228/article/details/107775976DMA缓存一致性是指DMA传输数据到内存后,可能CPU的缓存还有传输前的数据或者CPU想要发起DMA传输 但此时CPU只是把数据写进了缓存 DMA传输旧数据配置DMA对应的内存为nocacheDMA传输之前/之后都对DMA buffer地址范围内的缓存clean(这种方法当DMA传输时又读/写DMA是不可预测发生什么的)要保证DMA的地址对齐cacheline,大小也要对齐cacheline否则会出现问题内存排序内存排序问题来自于CPU的乱序执行CPU虽然在指令指令阶段是乱序执行的,但最终落到寄存器层面还是顺序的CPU流水线(每条指令怎么执行): 取指-译码-执行-访存-写回(这里写回是寄存器)为什么要乱序执行?如果CPU只是乖乖的顺序指令,在很多时候会损失不少性能ADD R1,R2,R3//将 R2 和 R3 的值相加结果存入 R1SUB R4,R1,R5//用 R1 的值减去 R5结果存入 R4)MUL R6,R7,R8 ADD R9,R10,R11在这里 下条指令是依赖于上条指令的计算结果的,对于顺序执行CPU来说,如果要保证正确性,必须要等待上条指令完全执行完,此时CPU流水线就会停顿,插入很多NOP。那如果下面指令跟这几个寄存器无关,提前执行下面的指令 然后再执行第二条呗顺序提交虽然指令可以通过乱序执行最大化CPU的利用率,但是最终提交依然是顺序的,为什么—假设指令3不依赖指令1,2先执行了 结果指令1,2触发异常了,此时的现场该怎么保存,怎么恢复,是不是头大(QAQ)所以虽然乱序执行,但是对外的可见性上,依旧是R6,R7等寄存器的更新是在R1,R2,R3等寄存器更新完之后的当然寄存器层面是这样,再加上缓存和写入内存的时机可就不一定了,假如R1是变量a,R9是变量b,完全有可能变量b先于a落入内存。而这就是为什么要有内存屏障指令的地方。ARM下的内存类型有两种 Normal和DeviceNormal是弱排序的 处理器可能进行重排序/推测性访问等操作Device:不允许重排序 有弱到强四种规则内存屏障三条内存屏障指令主要是防止CPU的乱序执行,而无法应对编译器的指令重排ISB指令确保所有在ISB指令之后的指令都从指令高速缓存或内存中重新预取。它刷新流水线flush pipeline和预取缓冲区后才会从指令高速缓存或者内存中预取ISB指令之后的指令。例如系统级别的寄存器被改了之后/上下文切换(ASID更改 TLb维护)DSB指令仅当所有在它前面的内存访问指令都执行完毕后,才会执行在它后面的指令,即任何指令都要等待DSB指令前面的内存访问指令完成。DMB指令DMB指令保证的是DMB指令之前的所有内存访问指令和DMB指令之后的所有内存访问指令的执行顺序。也就是说DMB指令之后的内存访问指令不会被处理器重排到DMB指令的前面。// 核心A执行a1;b2;// 核心B执行while(b0)continue;assert(a1);// 可能失败失败的原因主要在于,虽然前面乱序执行后通过ROB保证了寄存器层面的顺序可见性,但由于store_buffer和缓存的存在,其他核心看见的不一定能先看见a1。对于这种情况,加上DMB()指令就能保证。换句话说,DMB指令保证了这俩变量落入L1缓存的顺序,从而保证对其他CPU可见性和程序中一致(是缓存,不是内存!)DMB只关心内存指令 假如它的下一条是ADD指令且不存在依赖关系,那就乱序吧例如,在linux的无锁环形缓冲区kfifo中,就是用了smp_mb()保证了顺序,否则对于其他CPU,优先看到fifo-out更新了 以为可以写入了,实际上此时还没有memcpy读完,就会出现问题unsignedint__kfifo_get(structkfifo*fifo,unsignedchar*buffer,unsignedintlen){unsignedintl;lenmin(len,fifo-in-fifo-out);/* first get the data from fifo-out until the end of the buffer */lmin(len,fifo-size-(fifo-out(fifo-size-1)));memcpy(buffer,fifo-buffer(fifo-out(fifo-size-1)),l);/* then get the rest (if any) from the beginning of the buffer */memcpy(bufferl,fifo-buffer,len-l);/* * Ensure that we remove the bytes from the kfifo -before- * we update the fifo-out index. */smp_mb();fifo-outlen;returnlen;}额外:编译器的指令重排序实际上编译器为了提高运行性能,也可能对于指令顺序进行重排,这个时候上述的指令就无能为力了,因为编译器层面不归它们管,它们看到的指令顺序就和程序里的不一样。编译器进行重排序时必须遵守一条不可动摇的底线即 as-if-serial 语义。这个原则规定无论指令如何重排在单线程环境下程序的最终执行结果必须与按源代码顺序执行的结果完全一致。同样下面的场景,就算CPU顺序执行了,编译器重排了还是可能出问题,不过这个好排查,看看汇编就行,如果担心就是用__asm__volatile(“” ::: “memory”);命令// 核心A执行a1;b2;// 核心B执行while(b0)continue;assert(a1);// 可能失败MMUMMU算是MPU的加强版,主要作用有两个:虚拟地址转换和内存保护,不过这两个统一到页表管理,页表里的每一项不仅记录了地址转换关系,还有对应的访问权限。每个进程所用到的页表都需要手动分配一块物理内存,记录进程对应的虚拟地址和物理地址转换关系ARM下的内存类型有两种 Normal和DeviceNormal是弱排序的 处理器可能进行重排序/推测性访问等操作Device:不允许重排序 有弱到强四种规则TLB因为每次都从MMU里进行地址查找和转换太慢了,所以就给MMU增加了缓存–TLB,CPU可以先通过TLB查询,查询不到再去MMU查询TLB本质就是缓存(SRAM) 也会遇到缓存一致性的问题 比如这个核心CPU修改了页表部分映射但是此时别的CPU核心的TLB还有的老映射 就可能出现问题(TLB shutdwon)或者是CPU修改了页表的映射信息 但是目前还在缓存中 或者就算写回页表了 但是TLB还没刷新都会有问题ARMV7 架构下提供了TLB缓存失效的TLBI 指令 用来保障部分场景下数据的一致性TLB shutdown可以参考linux的方案:https://www.zyxy.net/archives/30253RTT-SMART 目前没看到明白怎么解决的RTT下相关的数据结构struct aspace:内存管理数据结构核心typedefstructrt_aspace{void*start;/*地址空间起始的虚拟地址*/rt_size_tsize;void*page_table;/*页表 保存物理地址虚拟地址转换*/mm_spinlock_tpgtbl_lock;/*保护页表的自旋锁(SMP下对页表的访问)*/struct_aspace_treetree;/*二叉搜索树 管理所有的VMA*/structrt_mutexbst_lock;structrt_mem_obj*private_object;/*记录的VMA操作的函数指针 比如read_page/write_page*//*默认绑定static struct rt_private_ctx _priv_obj的memobject变量 每次都是深拷贝*/#ifdefARCH_USING_ASIDrt_uint64_tasid;#endif/* ARCH_USING_ASID */}*rt_aspace_t;struct rt_varea:对于内存区域的描述进程中包含 数据段/代码段/堆/栈 每一段的权限是不一样的,所以抽象出一个struct rt_varea描述某个特定的内存区域typedefstructrt_varea{void*start;/*起始虚拟地址*/rt_size_tsize;rt_size_toffset;/*记录了物理页的偏移 暂时没看到用途*/rt_size_tattr;/*页表里的权限 MMU_MAP_U_RWCB / MMU_MAP_U_RO*/rt_size_tflag;/*什么类型 比如MMF_MAP_PRIVATE*/structrt_aspace*aspace;structrt_mem_obj*mem_obj;/*具体的内存管理者*/struct_aspace_nodenode;void*data;}*rt_varea_t;struct rt_mem_obj:内存的管理者本质上来说是内存 read/write/open等函数指针,主要有三个全局结构体rt_mm_dummy_mapper: 空白的内存管理者 在最开始初始化页表的时候,绑定了这个structrt_mem_objrt_mm_dummy_mapper{.get_nameget_name,.on_page_faulton_page_fault,/*分配空白内存物理页*/.hint_freeNULL,.on_varea_openon_varea_open,.on_varea_closeon_varea_close,.on_varea_shrinkon_varea_shrink,.on_varea_spliton_varea_split,.on_varea_expandon_varea_expand,.on_varea_mergeon_varea_merge,.page_writepage_write,.page_readpage_read,};rt_aspace_map_static(rt_kernel_space,mdesc-varea,vaddr,length,mdesc-attr,MMF_MAP_FIXED,rt_mm_dummy_mapper,0);_priv_obj-mem_obj:管理私有内存 主要就是进程自己可见的内存(backup_aspace)COW技术实现的关键/*绝大部分的varea 都绑定的这个anon_page_fault*/staticstructrt_private_ctx_priv_obj{.mem_obj.get_name_anon_get_name,.mem_obj.on_page_fault_anon_page_fault,.mem_obj.hint_freeNULL,.mem_obj.on_varea_open_anon_varea_open,.mem_obj.on_varea_close_anon_varea_close,.mem_obj.on_varea_shrink_anon_varea_shrink,.mem_obj.on_varea_split_anon_varea_split,.mem_obj.on_varea_expand_anon_varea_expand,.mem_obj.on_varea_merge_anon_varea_merge,.mem_obj.page_read_anon_page_read,.mem_obj.page_write_anon_page_write,};_null_object:mmap2和stack对应的内存的管理者staticstructrt_mem_obj_null_object{.get_name_null_get_name,.hint_freeRT_NULL,.on_page_fault_null_page_fault,.page_read_null_page_read,.page_write_null_page_write,.on_varea_expand_null_expand,.on_varea_shrink_null_shrink,.on_varea_split_null_split,};在lwp_mmap2函数中,有void*lwp_mmap2(structrt_lwp*lwp,void*addr,size_tlength,intprot,intflags,intfd,off_tpgoffset){/*其它代码*/uspacelwp-aspace;lengthRT_ALIGN(length,ARCH_PAGE_SIZE);mem_obj_get_mmap_obj(lwp);/*null_object*/rcrt_aspace_map(uspace,addr,length,k_attr,k_flags,mem_obj,k_offset);}/*同样的 栈空间的varea也归它管*/stk_addr(void*)USER_STACK_VSTART;errrt_aspace_map(lwp-aspace,stk_addr,USER_STACK_VEND-USER_STACK_VSTART,MMU_MAP_U_RWCB,flags,STACK_OBJ,0);struct rt_mm_va_hint:在二叉树进行搜索typedefstructrt_mm_va_hint{void*limit_start;/*起始搜索范围*/rt_size_tlimit_range_size;void*prefer;/*偏好地址*/constrt_size_tmap_size;mm_flag_tflags;/*可读/可写*/}*rt_mm_va_hint_t;struct rt_priavte_ctx:实际上aspace真正是通过private_object offset_of绑定了这个结构体typedefstructrt_private_ctx{structrt_mem_objmem_obj;rt_aspace_tbackup_aspace;/*专门用来实现COW/内存交换的aspace 记录了一些特殊的虚拟地址的页表信息*//* both varea and aspace can holds a reference */rt_atomic_treference;/*引用计数*//* readonly private is shared object */longreadonly;/*是否只读 COW的关键*/}*rt_private_ctx_t;这个呢struct mem_desc:对内存的描述,主要用于初始化MMU的Table#defineDEVICE_MEM(SHARED|AP_RW|DOMAIN0|SHAREDEVICE|DESC_SEC|XN)#defineNORMAL_MEM(SHARED|AP_RW|DOMAIN0|MEMWBWA|DESC_SEC)#defineSTRONG_ORDER_MEM(SHARED|AP_RO|XN|DESC_SEC)structmem_desc{rt_uint32_tvaddr_start;rt_uint32_tvaddr_end;rt_uint32_tpaddr_start;rt_uint32_tattr;structrt_vareavarea;/*没用到*/};MMU的初始化配置rt_uint32_tmmutable_p0;/*这个函数主要是对页表MMUTable下的虚拟地址0xf0000000起始的大小为0x10000000做了以下参数检查 确保当前还未使用 PV_OFFSET就是启动过程提到的offset*/rt_hw_mmu_map_init(rt_kernel_space,(void*)0xf0000000,0x10000000,MMUTable,PV_OFFSET);/* struct mem_desc platform_mem_desc[] { {KERNEL_VADDR_START, KERNEL_VADDR_START 0x10000000, (rt_size_t)ARCH_MAP_FAILED, NORMAL_MEM} };*//*根据传入的参数 把页表和rt_kernel_space下的成员变量初始化好*//*ARCH_MAP_FAILED的意思就是虚拟地址和物理地址之间的偏移量是PV_OFFSET*//*内核空间的起始地址是:USER_VADDR_TOP*//*把内核空间的内存 绑定到rt_mm_dummy_mapper对应的内存管理器上 一旦在这个空间访问虚拟地址不存在 就利用它来管理*/rt_hw_init_mmu_table(platform_mem_desc,platform_mem_desc_size);/*得到真实的页表的物理地址 MMUTable的地址是链接地址*/mmutable_p(rt_uint32_t)MMUTable(rt_uint32_t)PV_OFFSET;rt_hw_mmu_switch((void*)mmutable_p);/*内存管理的基本单位是页 这里做的就是初始化页内存管理器 管理之前预留的page空间*/rt_page_init(init_page_region);rt_hw_mmu_ioremap_init(rt_kernel_space,(void*)0xf0000000,0x10000000);arch_kuser_init(rt_kernel_space,(void*)0xffff0000);RTT缺页异常的触发与梳理如果MMU查询虚拟地址没有查到对应物理地址,或者是发现物理地址的权限不对就会触发DATA_ABORT/PREFETCH_ABORT异常具体来说,重写了这个异常处理函数,最终会调用__do_page_fault函数来进行物理页面分配vector_dabt:/*data_abort的处理*//*现场的保存*/bl rt_hw_trap_dabtvoidrt_hw_trap_dabt(structrt_hw_exp_stack*regs){/*调用check_data_abort*/if(check_data_abort(regs)){}}intcheck_data_abort(structrt_hw_exp_stack*regs){structrt_lwp*lwp;void*dfarRT_NULL;rt_base_tdfsrRT_NULL;__asm__volatile(mrc p15, 0, %0, c6, c0, 0:r(dfar));__asm__volatile(mrc p15, 0, %0, c5, c0, 0:r(dfsr));structrt_aspace_fault_msgmsg{.fault_opMM_FAULT_OP_WRITE,.fault_typeMM_FAULT_TYPE_PAGE_FAULT,.fault_vaddrdfar,};lwplwp_self();/*尝试修复*/if(lwprt_aspace_fault_try_fix(lwp-aspace,msg)){regs-pc-8;return1;}return0;}具体来说,在try_fix函数里做了如下工作首先校验虚拟地址的合法性 struct rt_varea挂靠在aspace下用来保存进程的各个段varea _aspace_bst_search(aspace, msg-fault_vaddr);查询硬件页表 看看这个错误是不是已经被处理了_determine_precise_fault_type(msg, (rt_ubase_t)pa, varea);处理 读/写/异常switch(msg-fault_op){caseMM_FAULT_OP_READ:err_read_fault(varea,pa,msg);break;caseMM_FAULT_OP_WRITE:err_write_fault(varea,pa,msg);break;caseMM_FAULT_OP_EXECUTE:err_exec_fault(varea,pa,msg);break;default:}RTT内存管理用户态内存管理与其说是内存管理,倒不如说是怎么对rt_varea进行管理brk系统调用和mmap调用的区别主要在于,两者的内存管理对象mem-obj不同brk系统调用在linux里 malloc分配小块内存的时候就是通过brk系统调用进行操作的,具体来说做了如下两件事找到堆内存对应的rt_varea,并进行扩展这里很有意思,虽然传入的FLAG里是LWP_MAP_FLAG_PREFETCH,并且也通过_flags_to_aspace_flag函数转换成了MMF_PREFETCH表示需要直接分配物理页,但是在最终调用rt_aspace_map的时候,又把MMF_PREFETCH这个FLAG给去了…更新最新的堆内存顶的end_heap变量rt_base_tlwp_brk(void*addr){/*一些条件判断*/elseif((size_t)addrUSER_HEAP_VEND){sizeRT_ALIGN((size_t)addr-lwp-end_heap,ARCH_PAGE_SIZE);varealwp_map_user_varea_ext(lwp,(void*)lwp-end_heap,size,LWP_MAP_FLAG_PREFETCH);if(varea){lwp-end_heap(long)(varea-startvarea-size);retlwp-end_heap;}}returnret;}最终的调用是_lwp_map_user_vareaattr_flags_to_attr(flags);mm_flags_flags_to_aspace_flag(flags);retrt_aspace_map_private(lwp-aspace,va,map_size,attr,mm_flags);堆内存肯定是进程私有的内存啦,所以要通过priv_obj对应的mem_obj进行分配intrt_aspace_map_private(rt_aspace_taspace,void**addr,rt_size_tlength,rt_size_tattr,mm_flag_tflags){/*标志位判断*/else{priv_obj_get_private_obj(aspace);if(priv_obj){flags|MMF_MAP_PRIVATE;flags~MMF_PREFETCH;/*最终会扩展用户态堆内存对应的varea*/rcrt_aspace_map(aspace,addr,length,attr,flags,priv_obj,0);}else{rc-RT_ENOMEM;}}returnrc;}mmap2系统调用参数检查规范传进来的参数if(rc0){/* 规范化参数如果没有文件描述符或显式指定了匿名映射 */if(fd-1||flagsMAP_ANONYMOUS){fd-1;// 强制设为 -1/* 匿名映射不支持共享在 RTT 中通常处理为私有清除共享位 */flags~MAP_SHARED;/* 强制设为私有且匿名的标志 */flags|MAP_PRIVATE|MAP_ANONYMOUS;}/* 调用内核层逻辑 lwp_mmap2 */rc(sysret_t)lwp_mmap2(lwp_self(),addr,length,prot,flags,fd,pgoffset);}调用lwp_mmap2对匿名映射(fd -1)和非匿名映射要分开处理匿名映射:调用rt_aspace_mapuspacelwp-aspace;lengthRT_ALIGN(length,ARCH_PAGE_SIZE);mem_obj_get_mmap_obj(lwp);/*就是前面说的null_object*//*对于mmap 每一次调用都会生成一个对应的rt_varea*/rcrt_aspace_map(uspace,addr,length,k_attr,k_flags,mem_obj,k_offset);调用_mm_aspace_map更新varea: _varea_install如果有要求,立马映射物理内存if(errRT_EOK){varea*pvarea;if(MMF_TEST_CNTL(flags,MMF_PREFETCH)){/* do the MMU TLB business */err_do_prefetch(aspace,varea,varea-start,varea-size);}}文件映射:调用文件系统驱动的mmap函数内核态内存管理堆内存堆内存主要用在rt_malloc() / rt_calloc()等函数,是从之前内存布局提到的heap_start到heap_end进行分配如果所定义的宏不同 那么管理这块区域的内存算法也不同#define(RT_USING_SMALL_MEM_AS_HEAP)#define(RT_USING_MEMHEAP_AS_HEAP)// 遍历空闲链表 first fitdefine(RT_USING_SLAB_AS_HEAP)// 内存池page_alloc这是管理页内存区域的 前面提到的mem_obj 最后肯定涉及到内存分配释放 都是在这个区域的这里用的是伙伴算法(buddy),通过加锁保证多线程下的正确性