为什么需要内存管理
- 分段和分页
- 内存分段
- 内存分页
- 分页情况下,虚拟内存如何映射到物理地址
- 页表原理
- 多级页表
- TLB快表
- 段页式内存管理
- 需要为什么进程地址空间
- Linux的进程虚拟地址空间
- 管理进程地址空间
- 如何分配虚拟内存
- 虚拟内存的管理
- 程序编译后的二进制文件如何映射到虚拟内存空间中
- 内核的虚拟地址
- 虚拟内核空间进行动态映射
- 物理内存地址
- cpu读取内存的数据
- Linux的物理和虚拟内存的页都是4KB的大小
在计算机中运行的进程,都是二进制代码&数据&内核的数据结构组成,这些内容都需要物理内存进行保存。许多的进程直接使用这些物理内存,操作系统是非常难对指向物理内存进行管理的,如果一个进出现了问题,就有可能会影响其他的进程。所以有了进程地址空间,而且是每一个进程独有的且和其他的进程地址空间隔离,即使该进程出现了问题也不会造成其他进程的问题。在Linux中,如果是32位的相同,进程地址空间可以表示的区域有4GB的大小。其中1GB的内核空间是所有进程共享的。有了进程地址空间,相当于为每个进程提供了一个自己独立的内存区域,且每个区域都是4GB的大小,就可以访问到比真实的物理地址大的空间。
分段和分页
在Linux中。
- 分段:可以为每一个每一个进程分配不同的线性地址空间,不同的段有着不同的功能,有代码段,数据段,堆区栈区。每一段都是连续的空间。
- 分页:将整个虚拟和物理内存的空间划分成许多连续的小块空间,这样的小块空间就是页,在物理内存中为页框,在Linux中,每一个页的大小的空间为4KB。
内存分段
分段机制下的虚拟地址有两部分组成,段的选择因子和段内的偏移量。
- 段的选择因子:段的选择因子有段号,段号,段寄存器通过段号找到对应的段表,段表有对应的段描述符,段描述符有段的完整信息,段的基地址、段的界限和特权等级等。
- 段内的偏移量:偏移量在0和段的界限之间,段基地址加上偏移量再通过虚拟到物理地址的转化,就可以访问到物理地址了。
分段会产生内存碎片问题,产生的原因是,申请段大小的空间,申请多少就会有多少,然后不同的段大小是不一样的,这就会导致有些区域空间无法申请成功,操作系统就会再其他可以申请的区域申请,就会导致无法申请的区域浪费掉,这就是外部的内存碎片问题。
内存分页
分段能连续分配空间,但因为有些空间不足,无法申请,但是这种情况又无法避免,只能减少。内存不足的情况下,如果进程需要物理内存,操作系统会将一部分的内存数据交换出去,让当前的进程有一定的物理空间使用。然而数据的交互是需要耗时的,这时候我们就需要减少内存的交换,面对这种情况,
- 我们可以将虚拟和物理的内存进行分页,页与页之间是紧密排列的,所以不会有外部碎片。
- 页与页之间是紧密排列的,所以不会有外部碎片。有了分页,内存需要置换的话,不需要把一整个段置换,只需要将需要的页进行置换即可,可以提高效率。
分页情况下,虚拟内存如何映射到物理地址
虚拟和物理地址的转化,是由一个页表来进行的。提供虚拟和物理地址的映射。虚拟地址由页号和偏移量。
- 页号:页号作为页表的索引,提高页号找到对应物理地址。
- 偏移量:通过偏移量,更加页号找到的物理地址进行偏移量的计算来获取目标的地址。
页表原理
页表的存在,让虚拟和物理地址有了联系。一个页或页框是4KB的大小,虚拟和物理地址是4GB的大小,就会有4 * 1024 * 1024 / 4个页或页框(一百多万的页),一个页表的大小就要4MB的大小,如果进程过多的话,采用这样的页表方式,将会浪费物理内存。
多级页表
采用多级页表就可以解决页表占用空间过多的问题。第一个页表大小为4KB,有1024个页表项,而页表项不存储虚拟地址,而是存储二级页表的页号,二级页表也是一个4KB大小的页表,也有1024个页表项,在极端场景下就能表示4GB的空间大小。32位情况下,前10比特位表示一级页表,中间10位表示二级页表,后12位表示物理地址的偏移量。
一级页表4KB+二级页表4MB的情况 > 一个页表4MB:这让看,会发现使用二级页表会比直接使用一个页表还多4KB,极端情况是这样的。但一个进程在运行的时候,并不是一定需要4GB的空间大小,有些进程还不一定把他的全部数据加载到物理内存当中。这时候,一级页表创建的时候,有些页表项不会被使用到,二级页表就不会存在,如果需要的话,再创建二级页表。如果只使用了20%的二级的页表,一级加二级页表所占的内存只需要0.804MB,远比只使用一个页表更节省空间。
TLB快表
如果每次都虚拟地址都通过页表找到对应的物理地址,效率会很慢,所以计算机科学家在CPU芯片中加入了一个Cache,就是TLB快表,用于存放将虚拟地址映射至物理地址的标签页表条目。有了TLB的缓存,避免每次都要查询页表项,每次CPU访问某个地址的时候,现在TLB缓存中查询,如果查询到有对应的虚拟到物理的映射,则直接通过MMU内存管理单元进行虚拟到物理的访问,减少了页表查询的消耗。
- 首先,CPU进行虚拟地址寻址时,首先会在MMU的TLB快表缓存当中寻找是否有虚拟地址到物理地址的页表条目,如果存在,直接通过MMU访问物理地址。
- 如果TLB缓存没有,则会发生硬件中断,将外设的资源加载到内存当中,页表重新映射虚拟地址到物理地址的页表条目,然后再将该页表条目缓存在TLB,方便下次的快速查找。
段页式内存管理
分段可以让内存空间分为多个有目的逻辑段,不同的数据存放在不同的段空间,分页将多个分段的空间划分为大小一致的许多连续的空间,可以在磁盘和物理内存进行交换的数据减少,提高效率。就可以使用段号,段内页号,页号偏移进行虚拟到物理地址的映射。
- 访问段表查询段号,得到段内的页表地址。
- 通过页表找到物理页号。
- 在通过偏移量的条件下,得到物理地址。
需要为什么进程地址空间
- 进程的地址空间其实并没有进程需要的数据,数据而是在物理内存当中,所以进程地址空间的内存也是虚拟内存。
- 如果所有的进程都直接使用物理内存,如果其中一个进程修改其他进程的物理内存的数据,就会有可能导致进程的崩溃,所以,为了不同的进程安全,则使用进程地址空间作为间接的访问真实的物理内存,并通过进程地址空间和物理空间的转化,来对内存进行访问。进程之间互不干涉。在操作系统中,通过CPU的MMU对虚拟地址对物理地址的转化来找到真实的物理内存。
- 如果可执行二进制代码运行多个进程,地址都是物理地址的话,就会指向同一块物理内存,导致程序出错或崩溃。如果重新为每一个进程直接分配物理内存,那会非常的复杂,调试的时候,程序员就难以区分地址。
- 使用了虚拟内存,直接可以让程序员看到的是连续的地址,虽然底层物理的内存是不连续的。每个进程的地址空间互相独立,互补干扰,其中的一个进程崩溃,不会影响其他的进程,即使虚拟地址一样,通过虚拟转物理的技术,不同进程的物理地址是不会发生冲突的。
Linux的进程虚拟地址空间
- 在Linux中,进程地址空间不是一开始就是在最小地址初开始的,而是在0x0804 8000 地址开始。在0x0000 0000 到 0x0804 8000是一段不可访问的空间。因为数值较小,通常会被认为是一个不合法的地址,比如C语言的无效指针NULL,指向的就是这块区域。
- 代码区:包括二进制可执行代码;
- 数据区:包括已初始化的静态常量和全局变量;
- BSS:未初始化的数据区;
- 堆区:内核使用start_brk标识堆的起始位置,brk标识堆的结束位置。当申请新的内存空间时,只需要将brk指针增加到对应的大小,回收时减少对应大小即可。
- 共享映射区:包括动态库,共享内存,堆空间申请;
- 栈:存放局部变量的数据,和函数调用的上下文。有固定的大小,可以调整。在内核中,使用start_stack标识栈的起始地址,RSP寄存器保存栈顶指针,RBP寄存器保存栈基地址;
- 内核空间:每一个进程都共用同一个1GB的内核地址空间;
管理进程地址空间
Linux中的进程使用一个结构体mm_struct来描述进程地址空间的,mm_struct是进程控制块task_struct的一个结构。每个进程的mm_struct都是独立的互不干扰。task_size其实是用户态可以访问的空间,task_struct就划分了用户态和内核态了。
struct task_struct{
unsigned long task_size;//可以访问的大小 0xc0000000
struct mm_struct *mm;
}
如何分配虚拟内存
根据mm_struct不同的定义进行划分。
- start_code和end_code标识代码段开始和结尾,就是存放二进制代码的区域。
- start_data和start_data是定义了的数据段的开始和结束的区域,后边紧跟着BSS未初始化的数据。
- start_brk就是动态申请空间的堆区域的开始,brk标识申请的空间的结束位置,低地址往高地址增加。
- mmap就是内存的映射区的开始,高地址往低地址增加。运行时所依赖的动态链接库就是加载到该内存区域。
- start_stack是栈的起始位置,栈使用的空间是由高地址往低地址使用,结束的位置的至就在寄存器的栈顶指针。
虚拟内存的管理
mm_struct对虚拟空间进行划分,现在使用新的结构体vm_area_struct对这些区域进行管理。每个vm_area_struct对应一个划分的区域。然后使用类似链表的结构进行组织。
struct mm_struct{
struct vm_area_struct *mmap; /* list of VMAs */
}
struct vm_area_struct {
struct mm_struct * vm_mm; /* The address space we belong to. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next;
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units, *not* PAGE_CACHE_SIZE */
struct file * vm_file; /* File we map to (can be NULL). */
}
- vm_mm标识该虚拟内存管理属于哪个空间,进行指针的回指到属于那块虚拟内存区域;
- vm_start标识虚拟划分空间的起始地址,vm_end标识结束地址;
- vm_next标识下一个vm_area_struct 的划分,内存的划分管理使用链表进行管理;
- anon_vma标识的是匿名映射区,当进程动态申请的空间大于128kb的时候,则会在该区域申请空间,而不是通过堆区域的brk的指针往上增长,而是调用mmap申请空间。当mmap为文件申请空间的时候,vm_file属性就用来关联被映射的文件,这样虚拟内存就可以和映射的文件关联起来。vm_pgoff则标识映射到虚拟内存中的文件内容的偏移量。
程序编译后的二进制文件如何映射到虚拟内存空间中
- 编译后的文件是elf格式的可执行文件,当执行elf格式的文件时,会将二进制代码加载到内存当中。
- 通过内核函数load_elf_binary将elf的文件映射到虚拟地址空间。当fork创建子进程,exec进行进程替换的时候,执行二进制程序的时候,建立虚拟内存映射。初始化虚拟内存所需要的数据。
内核的虚拟地址
在32位的计算机下,虚拟内核空间在0xc000000到0xffffffff的区域,每个进程都是共有这块虚拟内存空间的。在内核低地址的896MB的空间,是一块直接映射的区域。直接映射到物理内存的0~896MB的区域。虽然是直接映射的区域,但虚拟内核空间还是使用了页表进行虚拟到物理的映射。只是说每次的映射的物理内存的地址是不会改变的。
- 在这段896MB的物理内存当中,第1MB在系统启动的时候就已经被占用,1MB之后存放的内核的代码段,数据段,BSS段等。内核的elf格式的代码在启动的时候加载到内存当中。
- 高端内存:在剩下的4GB- 896MB的3200MB的物理内存就是高端内存,是无法直接映射到内核虚拟空间剩下的128MB的空间的。所以只能动态的映射到虚拟内存的128内存当中,哪些使用先映射,使用完毕的话,可以让其他要映射的覆盖上去。
虚拟内核空间进行动态映射
虚拟内存采用函数vmalloc调用申请空间的,申请的虚拟空间是连续的,但物理空间不一定是连续的。然后通过页表的映射来进行虚拟到物理的转化。
malloc和vmalloc的区别
- malloc用于用户申请堆上空间,是C标准库的函数。vmalloc是Linux内核提供的函数,用于内核中分配虚拟地址连续但物理地址不连续的空间。
- malloc申请的大小没有特别的限制,但受到系统可用内存的限制。vmalloc申请的空间也没有大小的限制,适用于较大内存的场景。
- malloc申请的的内存在虚拟地址是连续的,但物理地址是否连续取决内存管理器的实现和系统内存碎片的情况。malloc和vmalloc申请的空间都不会自动的初始化。
- vmalloc在内核中执行,可能会阻塞,在调用vmalloc时一般不会被中断,系统调用的执行被视为一个原子操作,即在执行期间不会被中断。这是为了确保在系统调用服务例程执行期间对内核数据结构的一致性和完整性。系统调用执行的时间相对较短,内核会采取一些机制来防止在其执行期间被中断。
- malloc调用是C标准库的函数调用,如果申请的空间大于堆可用的空间,malloc就会调用brk()或mmap()系统调用。用户态->内核态,如果申请的空间在堆区域满足,则直接在用户态就可以申请到,不需要到内核态。
物理内存地址
内存也叫随机访问存储器(RAM),分为静态SRAM和动态DRMA。
- SRAM用于CPU的三级缓存的高速缓存,越靠近CPU的SRAM运行速率越快,但相应的容量也会越少。
- DRAM常用于主存上,运行速率相对高速缓存是慢的,但容量也比高速缓存大。内存是由多个存储器模块组成,存储器模块又包含着8个DRAM芯片。每一个DRAM芯片是由一个二维矩阵组成的。矩阵中每个元素称为supercell超单元,大小位一个字节,每个单元都有坐标。提高行列坐标进行寻找。存储控制器将物理地址转化位DRAM芯片的坐标,然后找到对应的位置将数据发到存储控制器。
cpu读取内存的数据
- 1:CPU获取到虚拟地址,通过MMU内存管理单元对虚拟地址进行物理地址的转化,将物理地址作为地址信号放在系统总线上传输,随后在I/O桥将系统总线上的地址喜欢转化为存储中小的电子信号。
- 2:主存的存储控制器收到电子信号的物理地址,存储控制器找到对应存储模块的DRAM地址,然后获取数据。
- 3:存储控制器读取的数据放到存储总线上,存储总线通过I/O桥,I/O将数据信号转化为系统总线的数据信号进行传递。
- 4:CPU 芯片感受到系统总线上的数据信号,将数据从系统总线上读取出来并拷贝到寄存器中。
Linux的物理和虚拟内存的页都是4KB的大小
页的大小规定为2的整数次幂,因为有利于计算机的位运算,提高运行的效率。如果内存不足的情况下,有些进程需要使用到内存,操作系统就会将一些不太使用的物理页进行换入换出,还有内存和磁盘文件都会有交互,交互都是要消耗时间的。如果内存和磁盘直接传输小块数据是速度比较快的。所以内核默认采用4KB的大小。
- 页表过小:虽然传输的效率会变快,内部的碎片也会减少,但需要的页表项就会增多,如果要页表的换入换出,就会涉及过多的页表,频繁的换入换出会更耗时,降低效率。
- 页表过大:大的页表会减少页表一定的内存消耗,但会导致内部碎片增多。页大小过大,搜索页表会加快一定速率,但搜索页内的内容,要比页小的耗时。大的页和磁盘的每次交互或者换入换出的消耗更多的时间。综合来看4KB的大小更合理。
Linux中,使用struct page对物理页进行管理内核如何描述物理内存页。