当前位置: 首页 > 科技观察

Linux内存寻址方式

时间:2023-03-13 01:15:46 科技观察

本文转载自微信公众号“Linux澡堂”,作者不感冒。转载本文请联系Linux澡堂公众号。为什么需要内存管理:早期的程序都是直接在物理地址上运行的,也就是说这个程序需要的空间如果不超过机器的物理内存就没有问题,但是在实际场景中,多任务而multi-process是的,这种为每个进程预留的物理地址是不可靠的。举个栗子:如果有3个程序a、b、c,a需要10M,b需要100M,c需要20M,总内存120M,按照前面的分配方式,前10M给a,10M-110M对于b来说,系统还剩10M,而c需要20M,显然剩余的内存不够给c。该怎么办?1、效率问题大家可能会认为,程序c运行时,程序b的数据写入磁盘,程序b运行时,数据又从磁盘写回。且不说程序b和c不能并行运行,就连频繁的io操作带来的耗时问题也是无法接受的。2、进程地址隔离的问题除了效率问题外,如果为进程保留的空间需要被其他进程访问,就会崩溃。比如进程a访问的空间是前10M,但是如果程序a中的一段代码访问了10-110M,可能会导致程序b崩溃,所以进程的地址空间需要和各个进程隔离其他。3.重定位问题在实际场景中,单个任务不可能运行在划分的内存中。当多个任务并行运行时,动态申请内存释放时,有可能在其他进程中申请地址。这时候就需要重新定位Navigate到新的地址。内存管理无非就是想解决以上三个问题。如何提高内存使用效率?如何隔离进程的地址空间?如何解决程序运行时的重定位问题?内存管理是如何从虚拟地址映射到物理地址的:内存管理从虚拟地址映射到物理地址的过程就是解决上面三个问题的过程。内存管理使用分段机制和分页机制分别解决以上三个问题。大致过程如下:分段机制:只要把程序分成段,整个段翻译到任意位置后,段内的地址是相对于段的基地址的,不管基地址是什么段的意义在于,只要给出段中的偏移地址,CPU就可以访问到正确的指令。所以在加载用户程序的时候,只要把整个段的内容复制到一个新的位置,然后把段基地址寄存器中的地址改成这个地址,程序就可以准确运行了,因为程序使用的是内偏移地址段,相对于新的段基地址,偏移地址处的内容还是一样的。可以看出,分段机制解决了进程间隔离和搬迁的问题。这个动作是在硬件中完成的,但是有些硬件没有分段机制。作为跨平台的linux,采用了更加通用的分页机制来解决线性地址到虚拟地址再到物理地址的转换。分页机制:可以参考《CPU是如何访问内存的?》了解一级页表的概念。linux为了兼容32位和64位,通常使用四级页表,页全局目录,页上层目录,页中间目录,页表:这里不详述linux如何转换线性地址(虚拟地址)通过四级页表到物理地址。网上有很多,推荐https://www.cnblogs.com/linhaostudy/p/10038100.html#autoid-2-2-0。进程切换时,根据task_struct找到mm_struct中的pgd字段,得到新进程的page全局目录,然后填入CR3寄存器,完成页面切换。看一下mmu分页寻址的过程:上面代码:#include#include#include#include#include#include#include#includeMODULE_DESCRIPTION("vitualaddresstophysicsaddress");staticintpid;staticunsignedlongva;module_param(pid,int,0644);//命令行传参(变量,类型,权限)module_param(va,ulong,0644);//va代表虚拟地址staticintfind_pgd_init(void){unsignedlongpa=0;//pa代表物理地址structtask_struct*pcb_tmp=NULL;pgd_t*pgd_tmp=NULL;pud_t*pud_tmp=NULL;pmd_t*pmd_tmp=NULL;pte_t*pte_tmp=NULL;printk(KERN_INFO"PAGE_OFFSET=0x%lx\n",PAGE_OFFSET);//页表中有多少项/*pud和pmd等占线性地址的多少位*/printk(KERN_INFO"PGDIR_SHIFT=%d\n",PGDIR_SHIFT);//注:PGD和PUD在32位系统同printk(KERN_INFO"PUD_SHIFT=%d\n",PUD_SHIFT);printk(KERN_INFO"PMD_SHIFT=%d\n",PMD_SHIFT);printk(KERN_INFO"PAGE_SHIFT=%d\n",PAGE_SHIFT);printk(KERN_INFO"PTRS_PER_PGD=%d\n",PTRS_PER_PGD);//有多少条PMD里有没有printk(KERN_INFO"PTRS_PER_PTE=%d\n",PTRS_PER_PTE);printk(KERN_INFO"PAGE_MASK=0x%lx\n",PAGE_MASK);//页面掩码structpid*p=NULL;p=find_vpid(pid);//通过进程pid号找到structpid结构pcb_tmp=pid_task(p,PIDTYPE_PID);//找到taskstructprintk(KERN_INFO"pgd=0x%p\n",pcb_tmp->mm->pgd);//判断给定地址va是否合法(vamm,va)){printk(KERN_INFO"virt_addr0x%lxnotavailable.\n",va);return0;}pgd_tmp=pgd_offset(pcb_tmp->mm,va);//返回线性地址va,page全局目录中entry对应的线性地址printk(KERN_INFO"pgd_tmp=0x%p\n",pgd_tmp);//pgd_val得到pgd_tmp指向的页面全局目录项//pgd_val打印出pgd_tmp中的值printk(KERN_INFO"pgd_val(*pgd_tmp)=0x%lx\n",pgd_val(*pgd_tmp));if(pgd_none(*pgd_tmp)){//判断pgd是否被映射printk(KERN_INFO"Notmappedinpgd.\n");return0;}pud_tmp=pud_offset(pgd_tmp,va);//返回va对应页的上层目录项的线性地址printk(KERN_INFO"pud_tmp=0x%p\n",pud_tmp);printk(KERN_INFO"pud_val(*pud_tmp)=0x%lx\n",pud_val(*pud_tmp));if(pud_none(*pud_tmp)){printk(KERN_INFO"Notmappedinpud.\n");return0;}pmd_tmp=pmd_offset(pud_tmp,va);//返回页面上va中间目录对应条目的线性地址printk(KERN_INFO"pmd_tmp=0x%p\n",pmd_tmp);printk(KERN_INFO"pmd_val(*pmd_tmp)=0x%lx\n",pmd_val(*pmd_tmp));if(pmd_none(*pmd_tmp)){printk(KERN_INFO"Notmappedinpmd.\n");return0;}//这里把原来的pte_offset_map()改成pte_offset_kernelpte_tmp=pte_offset_kernel(pmd_tmp,va);//pte指的是找表printk(KERN_INFO"pte_tmp=0x%p\n",pte_tmp);printk(KERN_INFO"pte_val(*pte_tmp)=0x%lx\n",pte_val(*pte_tmp));if(pte_none(*pte_tmp)){//判断是否存在映射printk(KERN_INFO"Notmappedinpte.\n");return0;}if(!pte_present(*pte_tmp)){printk(KERN_INFO"ptenotinRAM.\n");return0;}pa=(pte_val(*pte_tmp)&PAGE_MASK);//物理地址的计算方法printk(KERN_INFO"virt_addr0x%lxinRAMPageis0x%lx.\n",va,pa);//printk(KERN_INFO"contectin0x%lxis0x%lx\n",pa,*(unsignedlong*)((char*)pa+PAGE_OFFSET));return0;}staticvoid__exitfind_pgd_exit(void){printk(KERN_INFO"Goodbye!\n");}module_init(find_pgd_init);module_exit(find_pgd_exit);MODULE_LICENSE("GPL");可以看出虚拟机对应的物理地址地址ffff99b488d48000是800000进程也是mmu的进程。