内存管理

本文涉及的产品
公网NAT网关,每月750个小时 15CU
简介: 其中介绍了虚拟内存的机制以及mmap系统调用的实现。mmap允许直接将设备内存映射到用户进程的地址空间中。物理内存的管理,包括缓存的分配及回收,请页机制,交换空间等。   1)交换模块(swap) 这个模块负责控制内存内容的换入换出,它通过替换机制,使得物理内存的页框(RAM页)中保留有效的逻辑页,即从主存中淘汰最近没被访问的逻辑页,保存近来访问过的逻辑页。

其中介绍了虚拟内存的机制以及mmap系统调用的实现。mmap允许直接将设备内存映射到用户进程的地址空间中。物理内存的管理,包括缓存的分配及回收,请页机制,交换空间等。

 

1)交换模块(swap)

这个模块负责控制内存内容的换入换出,它通过替换机制,使得物理内存的页框(RAM页)中保留有效的逻辑页,即从主存中淘汰最近没被访问的逻辑页,保存近来访问过的逻辑页。该模块实现的源程序分别是:

  • page_io.c的主要函数功能是读写交换文件。
  • swap_state.c的主要函数功能是修改交换高速缓存(swap cache)。
  • swapfile.c的主要函数功能是完成换入换出系统调用(sys_swapin、sys_swapon)。
  • swap.c的主要函数功能是定义交换使用的数据结构和常量,如free_page_low、free_page_high。
  • kswapd是一个周期性地处理交换内存和外村的守护进程(内核线程)

2)核心内存管理模块

这个模块负责核心内存管理功能,即对页的管理,这些功能将被别的内核子系统(如文件系统)所使用。该模块实现的源程序分别是:

  • page_alloc.c主要函数功能是处理页的释放、回收和分配
  • memory.c只要包含了请页机制的相关函数。

3)结构特定的模块

这个模块负责给各种硬件平台提供通用接口,这个模块通过执行命令来改变硬件MMU的虚拟地址映射,并在发生页错误时,提供公用的方法来通知别的内核子系统。这个模块是实现虚拟内存的物理基础。

首先内存管理程序通过映射机制把用户程序的逻辑地址映射到物理地址,在用户程序运行时如果发现程序要用的虚地址没有对应的物理内存时,就发出了请求也要求1;如果有空闲的内存可供分配,就请求分配内存2(于是用到了内存的分配二号回收),并把正在使用的物理页记录在页缓存中3(使用了缓存机制)。如果没有足够的内存可供分配,那么就调用交换机制,腾出一部分内存4,5.另外在地址映射中要通过TLB(转换后备缓冲区)来寻找物理页8,交换机制中也要用到交换缓存6,并且把物理页内容交换到交换文件中后也要修改页表来映射文件地址7.

1.1 内存地址类型和内存保护

1.1.1 地址类型

Linux是一个使用虚拟内存的系统,虚拟内存使运行在系统上的程序可以分配到比可用物理内存更多的空间。虚拟内存还能在进程地址空间上使用很多技巧,如映射设备的内存。

Linux系统使用几种类型的地址,下面列出了Linux用到的地址类型概念。

1)用户虚拟地址

该地址是用户空间的程序使用的地址。根据硬件体系结构的不同,用户地址可以是32位或64位,且每个进程拥有自己独立的虚拟地址空间。

2)物理地址

该地址是物理内存的地址,在处理器和系统内存之间使用。物理地址是32或者64位。

3)总线地址

该地址是指总线的寄存器的地址,在外设总线和内存之间使用。

4)内核逻辑地址

内核逻辑地址组成了常规的内核地址空间,这些地址映射了大部分乃至所有的主内存,并被视为物理内存使用。在大多数的体系结构中,逻辑地址及其所关联的物理地址之间的区别,仅仅在于一个常数的偏移量。逻辑地址使用硬件特有的指针大小,所以,在配置有大量内存的32位系统上,仅通过逻辑地址可以无法寻址所有的物理内存。在内核中,逻辑地址通常保存在unsigned long或者void*这样的变量中。由kmalloc返回的内存就是逻辑地址。

通过宏可将逻辑地址与物理地址相互转换,通过定义在<asm/page.h>中的宏__pa()返回与其关联的物理地址,也可以使用__va()宏将物理地址映射回逻辑地址,但只能用于低端内存页。

5)内存虚拟地址

由函数vmalloc分配的内存是虚拟地址,kmap函数也返回虚拟地址。这种地址却不一定能直接映射到物理内存。它需通过一系列处理,如分配内存、地址转换等,才能与逻辑地址联系起来。虚拟地址通常保存在指针变量中。

6)低端内存和高端内存

低端内存和高端内存都是指物理内存。

  • 低端内存。低端内存代表存在于内存空间的逻辑地址的物理内存。大家遇到的几乎都是低端内存。
  • 高端内存。高端内存是那些不存在逻辑地址的内存,高端内存常常是保留给用户空间的进程页。

在i386系统上,低端内存和高端内存的之间的界限通常设置为1GB。它是内核本身设置的限制,用于将32位地址空间分割为内核空间和用户空间。

用户空间可访问4GB的虚拟线性内存空间。其中,0到3GB的虚拟内存地址是用户空间,用户进程可以直接对其进行访问。从3GB到4GB的虚拟内存地址为内核空间,存放仅用于内核访问的代码和数据,是所有进程共享的,用户不能对它进行操作

所有的进程从3GB到4GB的虚拟空间都是一样的,有同样的页目录项和同样的页表,对应的同样的物理内存段。这样,内核态进程就共享代码段和数据段。

1.1.2 内存保护

1)不同任务之间的保护

在80386上,通过赋予每个任务不同的虚拟—物理地址转换映射,把每个任何放置在不同的虚拟地址完成。在30386上,每个任务都有自己的段表和页表。

这种保护操作系统的方法是把操作系统存储在虚拟地址空间的一个公共区域,然后,使每个任务按此区域分配一个同样的虚拟地址空间,并进行同样的虚拟-物理地址映射。各个任务公用的这部分虚拟地址空间被称为全局地址空间。

只有一个任务占有的虚拟地址空间部分,即不被任何其他任务共享的虚拟地址部分,称为局部地址空间。

在不同的任务中,对同一虚拟地址的访问,实际上转换为不同的物理地址。这就使得操作系统对每个任务的存储器可以赋予相同的虚拟地址,仍然保证任务的隔离。另一方面,对全局地址空间中同一虚拟地址的访问,在所有任务中都转换为同样的物理地址,从而支持公共的代码及数据的共享,例如,对操作系统的共享。

2)同一任务的保护

在一个任务之内,定义了四种执行特权级别,用来限制对任务中段进行访问。在0级是操作系统内核,处理I/O,内存管理及其他关键的操作;在1级是系统调用处理程序,用户程序可以通过调用这里的过程执行系统调用,但是,只有一些特定的和受保护的过程可以被调用;2级是库过程;最后用户程序运行在级别3上,受到的保护最少。

1.2 80386的段页式管理机制

80386以两级虚拟-物理地址转换,即使用了分段机制和分页机制来实现两级地址转换的。第一级把包含段地址和段内偏移量的虚拟地址,转换为一个线性地址。第二级把线性地址转换为物理地址。

Linux内核中在物理内存中为每个进程维护一个页表,页表驻留在内存,不能被交换到磁盘。但是,内核使用一个独立的页表来管理页表映射内核段,与用户空间每个进程一个页表不同,该页表属于整个内核,而且与当前正在运行的进程无关。

1.3 进程的内存组织

1.3.1 内存管理的数据结构

内存管理中有三个重要的数据结构struc vm_area_struct,struct page和struct mm_struct,它们用于表示进程的内存使用,它们与进程的关系如图4.10所示。

每个进程都有一个struct mm_struct结构体,用来描述一个进程的虚拟内存。在结构体中包含了进程的页表和许多其他的大量信息。

vm_area_struct结构描述进程的一个虚拟内存地址区域。一个VMA(虚拟内存区域)是对页错误处理有同一规则的进程虚拟内存空间的部分,如共享库、运行区域等,代表进程空间的一块单独连续的地址空间。

page结构用来描述一个物理页,系统中每个物理页有一个页结构来保护跟踪。

1.3.2 VMA在/proc文件系统中的显示

VMA是Virtual Memory Aera的缩写,即内存虚拟区域。在每个进程中,一般分为这几个虚拟内存区域:程序的代码区域(即text段);每种类型的数据对应一个区域,其中包括初始化数据(在执行之处已经明确赋值的数据);未初始化的数据(BSS);程序栈等。

vma在程序运行中,不断地由申请、清除、查找、分割、融合等对vma的管理操作,所有vma存在一个双向链表中,另外还用AVL树进行管理,以加速查找。

一个进程的内存区域可以从/proc/pid/maps中看到,/proc/*/maps中的每项都与一个vm_area_struct结构成员对应,可以用一个列表对每个字段进行描述。

例如:

每行中的字段说明如下:

  • start-end 虚拟内存区域的起始和结束地址
  • perm虚拟内存区域的读、写和执行许可的位掩码。最后一个字符p表示是私有,s表示共享。
  • offset虚拟内存区域在被映射文件中的偏移(以页为单位)。
  • major:minor主设备号和次设备号。
  • Image被映射的文件名字

VMA的应用是在页面错误时产生调页或换页操作的。进程的所有VMA以一个排序的双向链表来连接,按虚地址的下降顺序来排列的,每个VMA对应一个相邻的地址空间范围。

1.4 虚拟内存管理

1.4.1 大容量对象缓存

内核提供了kmalloc和kfree,分配真实地址已知的实际物理内存块,还提供vmalloc和vfree,用于对内核使用的虚拟内存进行分配和释放。由kmalloc返回的内存更适合类似设备驱动的程序来使用。

当一个结构或表需要建立大容量对象实例时,常用vmalloc函数进行申请。

kmalloc分配的地址范围一般在3G~high_memory,所以分配的物理地址与虚拟地址只有一个PAGE_OFFSET偏移,不需要为地址段修改页表。而且分配的虚拟逻辑地址和物理地址都是连续的。

其中kmalloc分配的是物理地址,而返回的则是虚拟地址(通过一定的偏移量将物理地址转换为虚拟地址)。

 

vmalloc函数分配的虚拟空间在3G+high_memory+VMALLOC_OFFSET以上高端,由vmlist链表管理。3GB是内核态赖以访问物理内存的地址,high_memory是安装在计算机实际可用的物理内存的最高地址。VMALLOC_OFFSET则为长度为8MB的“隔离带”,起着越界保护作用。

vmalloc函数分配的虚拟空间管理结构列出如下:

在mm/vmalloc.c中有vmlist链表的全局变量申明struct vm_struct *vmlist,在vmlist链表中,每个虚拟内存块之间都有个4KB大小的“隔离带”,用来检测访问指针的越界错误。第一个节点的地址就是VMALLOC_START。

函数vmalloc的功能是分配与size相匹配页面大小的连续虚拟内存,但对应的物理内存仍需经缺页中断后,由缺页中断服务程序分配,分配的物理页是不连续的。函数vmalloc申请大块缓冲区,但当前不用的内容不会调用入到物理内存中。

函数vmalloc列出如下:

void *vmalloc(unsigned long size)
{
    return __vmalloc(size,GFP_KERNEL|__GFP_HIGHMEM,PAGE_KERNEL);
}

函数__vmalloc分配足够的页数与size相配,把它们映射进连续的内核虚拟空间,但分配的内存块不一定连续。在函数中第一步是在vmlist中寻找一个大小合适的虚拟内存块(get_vm_area(size))。第二步检查这个虚拟块是否可用(空闲),建立页目录,找到空闲(虚拟块映射内)分配给调用进程(get_free_page());如果虚拟块是不可用的,那么必须要释放掉这个虚拟块(vfree)。

1.4.2 内存映射

内存映射的介绍

运行可执行文件时,先被映射到进程的虚拟地址空间中,形成vm_area_struct结构链表,接着程序的一部分被操作系统装入到物理内存。这种将映像链接到进程虚拟地址空间的访问称为“内存映射”。通过内存映射,文件的内容被直接链接到进程的虚拟地址空间。

随着vm_area_struct结构的生成,这些结构所描述的虚拟内存区域上的标准操作函数也由Linux初始化。在逻辑地址和物理地址之间相互转换的工作,是由内核和硬件内存管理单元(MMU)共同完成的,MMU是CPU的一个部分。内核告诉MMU如何为每个进程把逻辑严密映射到某特定物理页面,而MMU在进程提出内存请求时完成实际的转换工作。为了减少开销,最近被执行的地址转换结果将被存储在MMU的转换后备缓存(TLB)内。除了由于内核的操作致使TLB无效偶尔会通知CPU外,Linux不会明确管理TLB。

内核维护了一个或者更多由struct page项构成的数组,它们跟踪系统上所有的物理内存。

一些函数和宏可用来在struct page和虚拟地址之间进行转换:

  • struct page *virt_to_page(void *kaddr):这个宏接受一个内核逻辑地址,并返回与其关联的struct page指针。因为它需要一个逻辑地址,它对vmalloc返回的内存和高端内存无效。
  • void *page_address(struct page *page):如果这个地址存在,则返回该页的内核虚拟地址。对于高端内存,仅在该页已经被映射的情况下,其地址才存在。

在移动或修改页表时,应该持有该page_table_lock自旋锁。

与内存映射相关的几个文件在mm目录下,其中,mmap.c文件中主要函数do_mmap的功能是:把文件中的逻辑地址映射成虚存的线性地址,即把从文件结构中得到的逻辑地址转换为vm_area_struct结构所需的地址。mremap.c文件中主要函数sys_mremap的功能是:扩张或缩小现存的虚拟内存空间。filemap.c文件中的主要函数功能是:处理内存映射和也高速缓存器,即把线性地址空间映射到内存且修改页高速缓存。这部分含有从磁盘读写的I/O操作。

sys_brk系统调用

sys_brk提供支持C语言的malloc和free函数低级操作。C库函数malloc通过系统调用sys_brk向内核申请了一段虚地址空间vma来建立地址映射,vma全部建立起与内存页的映射,C库函数free也通过这个系统调用告诉内存需要收回这段地址空间,取消已建立的映射。sys_brk系统调用可以对用户进程的堆的大小进行操作,使堆扩展或者缩小。

sys_brk中调用do_brk函数,它是一个简单的do_mmap函数,它仅处理匿名映射,并将vma全部映射到内存页,而不像do_mmap函数是由缺页来引起调页的。

mmap系统调用

一个进程可通过系统调用mmap(),将一个已经打开文件的内容映射到它的用户空间。它直接调用do_mmap()函数来完成映射,在这个函数中,参数file为映射的文件,参数addr为映射的地址,参数len为VMA长度,长度prot指定该vma段的访问权限,参数flag为vma段的属性。

1.4.3 虚拟内存的加锁和保护

Linux可对虚拟内存段中的任一部分加锁或保护。对进程的虚拟地址加锁,其实质就是对vma段的vm_flags属性“或”上VM_LOCKED。在虚存加锁后,它对应的物理页面驻留内存,不再被页面置换程序换出。除非调用mlock的进程终止或者调用exec执行其他程序,这部分被锁住的源码才被释放。通过fork()调用所创建子进程不能够继承由父进程调用mlock锁住的页面。加锁操作有四个系统调用函数:mlock、munlock、mlockall和munlockall。

1.5 物理内存管理

NUMA(非一致内存访问体系)系统保持了物理上分散、而逻辑上同一的内存模式,是高性能服务器的主流体系结构。

1.5.1 伙伴系统

伙伴系统的概述

        Linux内核内存管理的一项重要工作就是如何在频繁申请释放内存的情况下,避免碎片的产生。Linux采用伙伴系统解决外部碎片的问题,采用slab解决内部碎片的问题,在这里我们先讨论外部碎片问题。避免外部碎片的方法有两种:一种是之前介绍过的利用非连续内存的分配;另外一种则是用一种有效的方法来监视内存,保证在内核只要申请一小块内存的情况下,不会从大块的连续空闲内存中截取一段过来,从而保证了大块内存的连续性和完整性。显然,前者不能成为解决问题的普遍方法,一来用来映射非连续内存线性地址空间有限,二来每次映射都要改写内核的页表,进而就要刷新TLB,这使得分配的速度大打折扣,这对于要频繁申请内存的内核显然是无法忍受的。因此Linux采用后者来解决外部碎片的问题,也就是著名的伙伴系统。

什么是伙伴系统?

      伙伴系统的宗旨就是用最小的内存块来满足内核的对于内存的请求。在最初,只有一个块,也就是整个内存,假如为1M大小,而允许的最小块为64K,那么当我们申请一块200K大小的内存时,就要先将1M的块分裂成两等分,各为512K,这两分之间的关系就称为伙伴,然后再将第一个512K的内存块分裂成两等分,各位256K,将第一个256K的内存块分配给内存,这样就是一个分配的过程。

物理页面的分配和回收

Linux使用伙伴算法来有效地分配与回收页面块。页面分配时,内存分配器在free_area数组中寻找一个与请求大小相同的空闲块。分配算法首先搜寻满足请求大小的页面。它从free_area数据结构的list域着手沿着链来搜索空闲页面。如果没有以这样的方式请求大小的空闲页面,则它搜索两倍于请求大小的内存块。这个过程一直将持续到free_area被搜索完或找到满足要求的内存块为止。

如果找到的页面大于请求的块,则把它分成两块:一个与请求块匹配,另一个事空闲块。每块大小都是2的N次幂。空闲块被链接进相应大小的队列,而另一个页面块被分配给调用者。

页面回收时,内存分配器将检查是否有相同大小的相邻或者伙伴内存块存在。如果有,则将它们结合起来形成一个大小为原来两倍的新空闲块。每次结合完之后,代码还要检查是否可以继续合并成更大的页面。

_get_free_page函数是申请空闲页面的上层接口。free_pages函数释放内存页。

 

下面我们结合示意图来了解伙伴系统分配和回收内存块的过程。

 

1 初始化时,系统拥有1M的连续内存,允许的最小的内存块为64K,图中白色的部分为空闲的内存块,着色的代表分配出去了得内存块。

2 程序A申请一块大小为34K的内存,对应的order为0,即2^0=1个最小内存块

   2.1 系统中不存在order 0(64K)的内存块,因此order 4(1M)的内存块分裂成两个order 3的内存块(512K)

   2.2 仍然没有order 0的内存块,因此order 3的内存块分裂成两个order 2的内存块(256K)

   2.3 仍然没有order 0的内存块,因此order 2的内存块分裂成两个order 1的内存块(128K)

   2.4 仍然没有order 0的内存块,因此order 1的内存块分裂成两个order 0的内存块(64K)

   2.5 找到了order 0的内存块,将其中的一个分配给程序A,现在伙伴系统的内存为一个order 0的内存块,一个order

                1的内存块,一个order 2的内存块以及一个order 3的内存块

3 程序B申请一块大小为66K的内存,对应的order为1,即2^1=2个最小内存块,由于系统中正好存在一个order 1的内

     存块,所以直接用来分配

4 程序C申请一块大小为35K的内存,对应的order为0,同样由于系统中正好存在一个order 0的内存块,直接用来分  

   配

5 程序D申请一块大小为67K的内存,对应的order为1

   5.1 系统中不存在order 1的内存块,于是将order 2的内存块分裂成两块order 1的内存块

   5.2 找到order 1的内存块,进行分配

6 程序B释放了它申请的内存,即一个order 1的内存块

7 程序D释放了它申请的内存

   7.1 一个order 1的内存块回收到内存当中

   7.2由于该内存块的伙伴也是空闲的,因此两个order 1的内存块合并成一个order 2的内存块

8 程序A释放了它申请的内存,即一个order 0的内存块

9 程序C释放了它申请的内存

   9.1 一个order 0的内存块被释放

   9.2 两个order 0伙伴块都是空闲的,进行合并,生成一个order 1的内存块m

   9.3 两个order 1伙伴块都是空闲的,进行合并,生成一个order 2的内存块

   9.4 两个order 2伙伴块都是空闲的,进行合并,生成一个order 3的内存块

   9.5 两个order 3伙伴块都是空闲的,进行合并,生成一个order 4的内存块

1.5.2 缓存及slab

缓存的结构

一个对象的所有实例都存在同一缓存区中。不同的对象,放在不同的缓存区中。每一缓存区有若干个slab,按照满、半满、空的顺序排列。整个内核内存可看做是按照这种缓存区组织的。

在内核中缓存的全局变量malloc_size[]中定义了可分配对象的大小,每次分配对象时,只能分配成cache_sizes结构描述大小的对象。

slab结构

slab块是内核内存分配与页面级分配的接口。一种对象的所有实例在同一个缓冲区,每个缓冲区存在若干个slab块,按满、半满和空闲的顺序。每个slab块的大小为页面大小的整数倍,存有若干个对象。

slab算法基于对象缓存,就是保留对象初始化状态的不变部分。当新建一个对象时,如果在缓冲区中有空闲的这个对象位置时,就获得此对象,不必初始化。当释放对象时,只是在缓存中将相应位置标为空闲,而不做析构。只是在系统资源不足时,slab算法才将一部分未使用的缓存空间释放。这样减少了大量常用对象的初始化和析构部分的费时过程。

1.5.3 交换空间

内存管理系统需要将暂时不用的内存数据转储到外存中,Linux采用两种方式保存换出的页面:一种用整个设备,如硬盘的一个分区,称为变换设备;另一种用文件系统中固定长度的文件,称为交换文件。它们统称为交换空间。这两种交换方式的内部格式是一致的。前4096字节是一个以字符口串“SWAP-SPACE”结尾的位图。位图的每一位对应一个交换空间的页面,置位表示对应的页面可用于换页操作。每4096字节之后才是真正存放换出页面的空间。这样,每个交换空间最多可容纳(4096-10)*8-1=32687个页面。

交换设备比交换文件有效很多。在交换设备中,属于同一页面的数据块总是连续存放的。第一个数据块一经确定,后续的数据块可以按顺序读出或写入。而在交换文件中,属于同一页面的数据虽然在逻辑上是连续的(在交换文件的位图看来),但数据块的实际位置可能是零散的,需要通过交换文件的inode检索,这决定于拥有交换文件的文件系统。

交换文件是在物理磁盘上的,因此将所有的交换页面按簇为单位存储,每簇内的页面连续存放。

1.5.4 请页机制

页故障错误的产生有三种原因:

  • 程序出现错误,虚拟内存无效(还没有映射,即还没有映射到磁盘上),Linux将向进程发送SIGSEGV信号并终止进程的运行;
  • 虚拟内存有效,但其所有对应的页当前不在物理内存中(但是存在磁盘中),即缺页错误,这时,操作系统必须从磁盘映像(页未分配或属于共享库)或交换文件(此页被换出)中将其装入物理内存。
  • 最后一种情况是,要访问的虚地址被写访问(对只读区域进行写操作),即保护错误。

对有效的虚拟地址,即已分配的虚拟地址,如果“缺页”错误的话,Linux必须区分页所在的位置,即判断页是在交换文件中,还是在可执行映像中。Linux通过页表项中的信息区分页面所在的位置。如果该页的页表项是无效的,但非空,则说明该页处于交换文件中,操作系统要从交换文件装入页,如果都不在,则把虚拟地址映射到物理内存。

1.5.5 守护进程kswapd

当物理内存出现不足时,Linux内存管理子系统需要释放部分物理内存页。这一任务由内核的交换守护进程kswapd完成,该内核守护进程实际是一个内核线程,它在内核初始化时启动,并周期性地运行。它的任务就是保证系统中具有足够的空闲页,从而使内存管理子系统能够有效运行。

1.5.6 内存管理相关的高速缓存

 

相关文章
|
1月前
|
存储 程序员 编译器
C++:内存管理
C++:内存管理
43 1
|
2月前
|
存储 C语言 C++
C++:C/C++内存管理
C++:C/C++内存管理
33 0
|
1月前
|
存储 编译器 程序员
【C++】内存管理
【C++】内存管理
|
4月前
|
存储 Java C语言
c++内存管理
c++内存管理
38 0
|
6月前
|
存储 C语言 C++
【C++】C&C++内存管理(上)
【C++】C&C++内存管理(上)
|
9月前
|
算法
操作系统概论——内存管理
操作系统概论——内存管理
44 0
|
9月前
|
存储 编译器 C++
c/c++内存管理:
c/c++内存管理:
46 1
|
10月前
|
存储 算法 编译器
C++之内存管理
C++之内存管理
101 0
|
10月前
|
安全 Linux C语言
|
11月前
|
存储 编译器 C语言
【C++】C/C++内存管理(二)
【C++】C/C++内存管理(二)
69 0