①管理方式:栈由编译器自动管理;堆由程序员控制,使用方便,但易产生内存泄露。
②生长方向:栈向低地址扩展(即”向下生长”),是连续的内存区域;堆向高地址扩展(即”向上生长”),是不连续的内存区域。这是由于系统用链表来存储空闲内存地址,自然不连续,而链表从低地址向高地址遍历。
③空间大小:栈顶地址和栈的最大容量由系统预先规定(通常默认2M或10M);堆的大小则受限于计算机系统中有效的虚拟内存,32位Linux系统中堆内存可达2.9G空间。
④存储内容:栈在函数调用时,首先压入主调函数中下条指令(函数调用语句的下条可执行语句)的地址,然后是函数实参,然后是被调函数的局部变量。本次调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的指令地址,程序由该点继续运行下条可执行语句。堆通常在头部用一个字节存放其大小,堆用于存储生存期与函数调用无关的数据,具体内容由程序员安排。
⑤分配方式:栈可静态分配或动态分配。静态分配由编译器完成,如局部变量的分配。动态分配由alloca函数在栈上申请空间,用完后自动释放。堆只能动态分配且手工释放。
⑥分配效率:栈由计算机底层提供支持:分配专门的寄存器存放栈地址,压栈出栈由专门的指令执行,因此效率较高。堆由函数库提供,机制复杂,效率比栈低得多。Windows系统中VirtualAlloc可直接在进程地址空间中分配一块内存,快速且灵活。
⑦分配后系统响应:只要栈剩余空间大于所申请空间,系统将为程序提供内存,否则报告异常提示栈溢出。
操作系统为堆维护一个记录空闲内存地址的链表。当系统收到程序的内存分配申请时,会遍历该链表寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点空间分配给程序。若无足够大小的空间(可能由于内存碎片太多),有可能调用系统功能去增加程序数据段的内存空间,以便有机会分到足够大小的内存,然后进行返回。,大多数系统会在该内存空间首地址处记录本次分配的内存大小,供后续的释放函数(如free/delete)正确释放本内存空间。
此外,由于找到的堆结点大小不一定正好等于申请的大小,系统会自动将多余的部分重新放入空闲链表中。
⑧碎片问题:栈不会存在碎片问题,因为栈是先进后出的队列,内存块弹出栈之前,在其上面的后进的栈内容已弹出。而频繁申请释放操作会造成堆内存空间的不连续,从而造成大量碎片,使程序效率降低。
可见,堆容易造成内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和内核态切换,内存申请的代价更为昂贵。所以栈在程序中应用最广泛,函数调用也利用栈来完成,调用过程中的参数、返回地址、栈基指针和局部变量等都采用栈的方式存放。所以,建议尽量使用栈,仅在分配大量或大块内存空间时使用堆。
使用栈和堆时应避免越界发生,否则可能程序崩溃或破坏程序堆、栈结构,产生意想不到的后果。
内存映射段(mmap)
此处,内核将硬盘文件的内容直接映射到内存, 任何应用程序都可通过Linux的mmap()系统调用或Windows的CreateFileMapping()/MapViewOfFile()请求这种映射。内存映射是一种方便高效的文件I/O方式, 因而被用于装载动态共享库。用户也可创建匿名内存映射,该映射没有对应的文件, 可用于存放程序数据。在 Linux中,若通过malloc()请求一大块内存,C运行库将创建一个匿名内存映射,而不使用堆内存。”大块” 意味着比阈值 MMAP_THRESHOLD还大,缺省为128KB,可通过mallopt()调整。
PS: 内存映射端在Linux 2.6.7以前是向上增长的,在2.6.7之后改为向下增长。更多可以查看《深入理解操作系统内核》4.2.1章节
栈(stack)
栈又称堆栈,由编译器自动分配释放,行为类似数据结构中的栈(先进后出)。堆栈主要有三个用途:
Linux中ulimit -s命令可查看和设置堆栈最大值,当程序使用的堆栈超过该值时, 发生栈溢出(Stack Overflow),程序收到一个段错误(Segmentation Fault)。注意,调高堆栈容量可能会增加内存开销和启动时间。
堆栈既可向下增长(向内存低地址)也可向上增长, 这依赖于具体的实现。本文所述堆栈向下增长。
堆栈的大小在运行时由内核动态调整。
内核地址空间直接映射区(896M)
所谓的直接映射区,就是这一块空间是连续的,和物理内存是非常简单的映射关系,其实就是虚拟内存地址减去 3G,就得到物理内存的位置。
__pa(vaddr) 返回与虚拟地址 vaddr 相关的物理地址;
__va(paddr) 则计算出对应于物理地址 paddr 的虚拟地址。
// PAGE_OFFSET => 3G 0x0c0000000
#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
#define __pa(x) __phys_addr((unsigned long)(x))
#define __phys_addr(x) __phys_addr_nodebug(x)
#define __phys_addr_nodebug(x) ((x) - PAGE_OFFSET)
这 896M 还需要仔细分解。在系统启动的时候,物理内存的前 1M 已经被占用了,从 1M 开始加载内核代码段,然后就是内核的全局变量、BSS 等,也是 ELF 里面涵盖的。这样内核的代码段,全局变量,BSS 也就会被映射到 3G 后的虚拟地址空间里面。具体的物理内存布局可以查看。
cat /proc/iomem
.....
00100000-bffdbfff : System RAM
01000000-01a02fff : Kernel code
01a03000-021241bf : Kernel data
02573000-02611fff : Kernel bss
25000000-34ffffff : Crash kernel
.....
在内核运行的过程中,如果碰到系统调用创建进程,会创建 task_struct 这样的实例,内核的进程管理代码会将实例创建在 3G 至 3G+896M 的虚拟空间中,当然也会被放在物理内存里面的前 896M 里面,相应的页表也会被创建。
在内核运行的过程中,会涉及内核栈的分配,内核的进程管理的代码会将内核栈创建在 3G 至 3G+896M 的虚拟空间中,当然也就会被放在物理内存里面的前 896M 里面,相应的页表也会被创建。
进程空间管理
高端内存 - HIGH_MEMORY
x86-32下特有的(x64下没有这个东西),因为内核虚拟空间只有1G无法管理全部的内存空间。
当内核想访问高于896MB物理地址内存时,从0xF8000000 ~ 0xFFFFFFFF地址空间范围内找一段相应大小空闲的逻辑地址空间,借用一会。借用这段逻辑地址空间,建立映射到想访问的那段物理内存(即填充内核PTE页面表),临时用一会,用完后归还。这样别人也可以借用这段地址空间访问其他物理内存,实现了使用有限的地址空间,访问所有所有物理内存。
从上面的描述,我们可以知道高端内存的最基本思想:借一段地址空间,建立临时地址映射,用完后释放,达到这段地址空间可以循环使用,访问所有物理内存。
看到这里,不禁有人会问:万一有内核进程或模块一直占用某段逻辑地址空间不释放,怎么办?若真的出现的这种情况,则内核的高端内存地址空间越来越紧张,若都被占用不释放,则没有建立映射到物理内存都无法访问了。
Linux内核高端内存
VMALLOC_OFFSET
系统会在low memory和VMALLOC区域留8M,防止访问越界。因此假如理论上vmalloc size有300M,实际可用的也是只有292M。
include/asm-x86/pgtable_32.h #define VMALLOC_OFFSET (8*1024*1024)
这个缺口可用作针对任何内核故障的保护措施。如果访问越界地址(即无意地访问物理上不存在的内存区),则访问失败并生成一个异常,报告该错误。如果vmalloc区域紧接着直接映射,那么访问将成功而不会注意到错误。在稳定运行的情况下,肯定不需要这个额外的保护措施,但它对开发尚未成熟的新内核特性是有用的。
VMALLOC
虚拟内存中连续、但物理内存中不连续的内存区,可以在vmalloc区域分配。该机制通常用于用户过程,内核自身会试图尽力避免非连续的物理地址。内核通常会成功,因为大部分大的内存块都在启动时分配给内核,那时内存的碎片尚不严重。但在已经运行了很长时间的系统上,在内核需要物理内存时,就可能出现可用空间不连续的情况。此类情况,主要出现在动态加载模块时。
include/asm-x86/pgtable_32.h
#define VMALLOC_START (((unsigned long) high_memory + \
2*VMALLOC_OFFSET-1) & ~(VMALLOC_OFFSET-1))
#ifdef CONFIG_HIGHMEM
#define VMALLOC_END (PKMAP_BASE-2*PAGE_SIZE)
#else
#define VMALLOC_END (FIXADDR_START-2*PAGE_SIZE)
#endif
vmalloc区域的起始地址,取决于在直接映射物理内存时,使用了多少虚拟地址空间内存(因此也依赖于上文的high_memory变量)。内核还考虑到下述事实,即两个区域之间有至少为VMALLOC_OFFSET的一个缺口,而且vmalloc区域从可被VMALLOC_OFFSET整除的地址开始。
VMALLOC_START 到 VMALLOC_END 之间称为内核动态映射空间,也即内核想像用户态进程一样 malloc 申请内存,在内核里面可以使用 vmalloc。假设物理内存里面,896M 到 1.5G 之间已经被用户态进程占用了,并且映射关系放在了进程的页表中,内核 vmalloc 的时候,只能从分配物理内存 1.5G 开始,就需要使用这一段的虚拟地址进行映射,映射关系放在专门给内核自己用的页表里面。
进程空间管理
持久映射
内核专门为此留出一块线性空间,从 PKMAP_BASE 到 FIXADDR_START ,用于映射高端内存。在 2.6内核上,这个地址范围是 4G-8M 到 4G-4M 之间。这个空间起叫”内核永久映射空间”或者”永久内核映射空间”。这个空间和其它空间使用同样的页目录表,对于内核来说,就是 swapper_pg_dir,对普通进程来说,通过 CR3 寄存器指向。通常情况下,这个空间是 4M 大小,因此仅仅需要一个页表即可,内核通过来 pkmap_page_table 寻找这个页表。通过 kmap(),可以把一个 page 映射到这个空间来。由于这个空间是 4M 大小,最多能同时映射 1024 个 page。因此,对于不使用的的 page,及应该时从这个空间释放掉(也就是解除映射关系),通过 kunmap() ,可以把一个 page 对应的线性地址从这个空间释放出来。
include/asm-x86/highmem.h
#define LAST_PKMAP 1024
#define PKMAP_BASE ( (FIXADDR_BOOT_START -PAGE_SIZE*(LAST_PKMAP + 1)) & PMD_MASK )
如果需要将高端页帧长期映射(作为持久映射)到内核地址空间中,必须使用kmap函数。需要映射的页用指向page的指针指定,作为该函数的参数。该函数在有必要时创建一个映射(即,如果该页确实是高端页),并返回数据的地址。
如果没有启用高端支持,该函数的任务就比较简单。在这种情况下,所有页都可以直接访问,因此只需要返回页的地址,无需显式创建一个映射。
如果确实存在高端页,情况会比较复杂。类似于vmalloc,内核首先必须建立高端页和所映射到的地址之间的关联。还必须在虚拟地址空间中分配一个区域以映射页帧,最后,内核必须记录该虚拟区域的哪些部分在使用中,哪些仍然是空闲的。
pkmap_count(在mm/highmem.c定义)是一容量为LAST_PKMAP的整数数组,其中每个元素都对应于一个持久映射页。它实际上是被映射页的一个使用计数器,语义不太常见。该计数器计算了内核使用该页的次数加1。如果计数器值为2,则内核中只有一处使用该映射页。计数器值为5表示有4处使用。一般地说,计数器值为n代表内核中有n-1处使用该页。
和通常的使用计数器一样,0意味着相关的页没有使用。计数器值1有特殊语义。这表示该位置关联的页已经映射,但由于CPU的TLB没有更新而无法使用,此时访问该页,或者失败,或者会访问到一个不正确的地址。
内核利用下列数据结构,来建立物理内存页的page实例与其在虚似内存区中位置之间的关联:
mm/highmem.c
struct page_address_map {
struct page *page;
void *virtual;
struct list_head list;
};
该结构用于建立page→virtual的映射(该结构由此得名)。page是一个指向全局mem_map数组中的page实例的指针,virtual指定了该页在内核虚拟地址空间中分配的位置。
刚才描述的kmap函数不能用于中断处理程序,因为它可能进入睡眠状态。如果pkmap数组中没有空闲位置,该函数会进入睡眠状态,直至情形有所改善。因此内核提供了一个备选的映射函数,其执行是原子的,逻辑上称为kmap_atomic。该函数的一个主要优点是它比普通的kmap快速。但它不能用于可能进入睡眠的代码。因此,它对于很快就需要一个临时页的简短代码,是非常理想的。kmap_atomic的定义在IA-32、PPC、Sparc32上是特定于体系结构的,但这3种实现只有非常细微的差别。其原型是相同的。
固定映射/临时映射
FIXADDR_START 到 FIXADDR_TOP(0xFFFF F000) 的空间,称为固定映射区域,主要用于满足特殊需求。
这块空间具有如下特点:
(1)每个 CPU 占用一块空间
(2)在每个 CPU 占用的那块空间中,又分为多个小空间,每个小空间大小是 1 个 page,每个小空间用于一个目的,这些目的定义在 kmap_types.h 中的 km_type 中。
当要进行一次固定映射的时候,需要指定映射的目的,根据映射目的,可以找到对应的小空间,然后把这个空间的地址作为映射地址。这意味着一次固定映射会导致以前的映射被覆盖。通过 kmap_atomic() 可实现固定映射。
固定映射是与物理地址空间中的固定页关联的虚拟地址空间项,但具体关联的页帧可以自由选择。它与通过固定公式与物理内存关联的直接映射页相反,虚拟固定映射地址与物理内存位置之间的关联可以自行定义,关联建立后内核总是会注意到的。
最后一个内存段由固定映射占据。这些地址指向物理内存中的随机位置。相对于内核空间起始处的线性映射,在该映射内部的虚拟地址和物理地址之间的关联不是预设的,而可以自由定义,但定义后不能改变。固定映射区域会一直延伸到虚拟地址空间顶端。
include/asm-x86/fixmap_32.h
#define __FIXADDR_TOP 0xfffff000
#define FIXADDR_TOP ((unsigned long)__FIXADDR_TOP)
#define __FIXADDR_SIZE (__end_of_permanent_fixed_addresses << PAGE_SHIFT)
#define FIXADDR_START (FIXADDR_TOP -__FIXADDR_SIZE)
固定映射地址的优点在于,在编译时对此类地址的处理类似于常数,内核一启动即为其分配了物理地址。此类地址的解引用比普通指针要快速。内核会确保在上下文切换期间,对应于固定映射的页表项不会从TLB刷出,因此在访问固定映射的内存时,总是通过TLB高速缓存取得对应的物理地址。
对每个固定映射地址都会创建一个常数,加入到fixed_addresses枚举值列表中。
include/asm-x86/fixmap_32.h
enum fixed_addresses {
FIX_HOLE,
FIX_VDSO,
FIX_DBGP_BASE,
FIX_EARLYCON_MEM_BASE,
#ifdef CONFIG_X86_LOCAL_APIC
FIX_APIC_BASE, /* 本地CPU APIC信息,在SMP系统上需要 */
#endif
...
#ifdef CONFIG_HIGHMEM
FIX_KMAP_BEGIN, /* 保留的页表项,用于临时内核映射 */
FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,
#endif
...
FIX_WP_TEST,
__end_of_fixed_addresses
};
内核提供了fix_to_virt函数,用于计算固定映射常数的虚拟地址。
include/asm-x86/fixmap_32.h
enum fixed_addresses {
FIX_HOLE,
FIX_VDSO,
FIX_DBGP_BASE,
FIX_EARLYCON_MEM_BASE,
#ifdef CONFIG_X86_LOCAL_APIC
FIX_APIC_BASE, /* 本地CPU APIC信息,在SMP系统上需要 */
#endif
...
#ifdef CONFIG_HIGHMEM
FIX_KMAP_BEGIN, /* 保留的页表项,用于临时内核映射 */
FIX_KMAP_END = FIX_KMAP_BEGIN+(KM_TYPE_NR*NR_CPUS)-1,
#endif
...
FIX_WP_TEST,
__end_of_fixed_addresses
};
编译器优化机制会完全消除if语句,因为该函数定义为内联函数,而且其参数都是常数。这样的优化是有必要的,否则固定映射地址实际上并不优于普通指针。形式上的检查确保了所需的固定映射地址在有效区域中。__end_of_fixed_adresses是fixed_addresses的最后一个成员,定义了最大的可能数字。如果内核访问的是无效地址,则调用伪函数__this_fixmap_does_not_exist(没有定义)。在内核链接时,这会导致错误信息,表明由于存在未定义符号而无法生成映像文件。因此,此种内核故障在编译时即可检测,而不会在运行时出现。
在引用有效的固定映射地址时,if语句中的比较总是会通过。由于比较的两个操作数都是常数,该条件判断语句实际上不会执行,在编译优化的过程中会直接消除。 __fix_to_virt定义为宏。由于fix_to_virt是内联函数,其实现代码会直接复制到查询固定映射地址的代码处。该宏定义如下:
include/asm-x86/fixmap_32.h
#define __fix_to_virt(x) (FIXADDR_TOP -((x) << PAGE_SHIFT))
从顶部开始(不是按照常理从底部开始),内核回退n页,以确定第n个固定映射项的虚拟地址。这个计算同样也只使用了常数,编译器能够在编译时计算结果。根据上文提到的内存划分,地址空间中对应的虚拟地址尚未用于其他用途。固定映射虚拟地址与物理内存页之间的关联是由set_fixmap(fixmap, page_nr)和set_fixmap_nocache建立的(未讨论后者的实现)。这两个函数只是将页表中的对应项与物理内存中的一页关联起来。不同于set_fixmap,set_fixmap_nocache在必要情况下,会停用所涉及页帧的硬件高速缓存。
在最后一个区域可以通过 kmap_atomic 实现临时内核映射。假设用户态的进程要映射一个文件到内存中,先要映射用户态进程空间的一段虚拟地址到物理内存,然后将文件内容写入这个物理内存供用户态进程访问。给用户态进程分配物理内存页可以通过 alloc_pages(),分配完毕后,按说将用户态进程虚拟地址和物理内存的映射关系放在用户态进程的页表中,就完事大吉了。这个时候,用户态进程可以通过用户态的虚拟地址,也即 0 至 3G 的部分,经过页表映射后访问物理内存,并不需要内核态的虚拟地址里面也划出一块来,映射到这个物理内存页。但是如果要把文件内容写入物理内存,这件事情要内核来干了,这就只好通过 kmap_atomic 做一个临时映射,写入物理内存完毕后,再 kunmap_atomic 来解映射即可。
物理内存
0~1M
前4 KiB是第一个页帧,一般会忽略,因为通常保留给BIOS使用。接下来的640 KiB原则上是可用的,但也不用于内核加载。其原因是,该区域之后紧邻的区域由系统保留,用于映射各种ROM(通常是系统BIOS和显卡ROM)。不可能向映射ROM的区域写入数据。但内核总是会装载到一个连续的内存区中,如果要从4 KiB处作为起始位置来装载内核映像,则要求内核必须小于640 KiB。
ZONE-DMA、ZONE_NORMAL、ZONE_HIGHMEM
在x86架构中内存有三种区域:ZONE_DMA,ONE_NORMAL,ZONE_HIGHMEM,不同类型的区域适合不同需要。在32位系统中结构中,1G(内核空间)/3G(用户空间) 地址空间划分时,三种类型的区域如下:
ZONE_DMA内存开始的16MB
ZONE_NORMAL16MB~896MB
ZONE_HIGHMEM896MB ~ 结束
临时内存页表、内核镜像
内核启动过程中,存在一个实模式保护模式的切换过程。在linux启动的最初阶段,内核刚刚被装入内存时,分页功能还未启用,此时是直接存取物理地址的(或者说线性地址就等于物理地址)。但初始化完成后,内核也需要有自己的虚拟地址空间(1个G大小),该虚拟地址空间的地址映射关系,会被作为模版拷贝到其他进程的内核地址空间中。
临时内核页表只用来映射物理地址的前8M空间内容。目的是允许CPU在实模式(直接存取物理地址)和保护模式(根据虚拟地址映射)之间切换的过程中,都能对这前8M的地址进行访问。(假如内核使用的全部内存可以存放在8M的空间里,因为一个页表可以映射4M的地址,所以8M的空间需要两个页表,也就是需要两个页目录项。这两张页表我们称为临时内核页表pg0和pg1。(页表的作用,参见地址映射))
Linux Kernel 有自己專屬的 Page Directory 及 Page Table
在系統初始化時,會先建立 2個 Page Table -- 包含 2048個 Page,共 8MB 的記憶體空間
這 8MB 是 Linux 開機最少需要的記憶體大小,而且保留給 Kernel 使用
Kernel Page Global Directory 是以變數 swapper_pg_dir 表示其資料結構可視為含有 1024 個元素的 pgd_t型態的陣列實體記憶體位址為 0x00101000
Kernel Page Table (第 0及第 1個 Table) 是以變數 pg0 及 pg1 表示其資料結構可視為含有 1024個元素的 pte_t 型態的陣列實體記憶體位址分別為 0x00102000 及 0x00103000
Linux Page 初始化的動作定義於 arch/i386/kernel/head.S因為開機時僅需要 8MB,所以只要初始化 2個 Page Table 便可,即 pg0 及 pg1其餘的 Page Table 均填入 0 的值
又為了讓這 2個 Page Table 可以被 Real Mode 及 Protect Mode 所存取Kernel Page Global Directory 中的第 0、第 768個 Entry以及第 1、第 769個 Entry分別會設為相同的 Page Table 的實體記憶體位址,如圖所示
初始化完成後,得到以下的結果:
swapper_pg_dir[0] = swapper_pg_dir[768] = 0x00102007
swapper_pg_dir[1] = swapper_pg_dir[769] = 0x00103007
pg0 加上 pg1 定址到實體記憶體 0x00000000 - 0x007FFFFF,共 8MB 的分頁
根據 pgd_t 及 pte_t 的欄位格式可得知:
bit 0-11 為 0x007,表示 Enable 旗號 Present、Read/Write、User/Supervisor(详见下面#页表描述符章节)bit 12-31 為 Base Address更多Linux内核源码高阶知识请加开发交流Q群篇【318652197】获取,进群免费获取相关资料,免费观看公开课技术分享,入群不亏,快来加入我们吧~
前100名进群领取,额外赠送一份价值699的内核资料包(含视频教程、电子书、实战项目及代码)
资源免费领
学习直通车
x86-64位虚拟地址空间
ZONE_DMA、ZONE_DMA32、ZONE_NORMAL
其他基础知识
页表描述符(page table descriptor)
64位页面描述符:
system.map
每次编译内核时,都生成一个文件System.map并保存在源代码目录下。除了所有其他(全局)变量、内核定义的函数和例程的地址,该文件还包括图3-11给出的常数的值。
x86-debian-10.10.0-System.map
x64-Debian-9-System.map
fanlv@debian:~$ head -10 /boot/System.map-4.19.0-17-686-pae
000001c9 A kexec_control_code_size
01000000 A phys_startup_32
c1000000 T _stext
c1000000 T _text
c1000000 T startup_32
c100009b W xen_entry
c10000a0 T start_cpu0
c10000b0 T startup_32_smp
c1000218 T verify_cpu
c1000314 T pvh_start_xen
符号类型:大写为全局符号,小写为局部符号
A:该符号的值是不能改变的,等于const
B:该符号来自于未初始化代码段bss段
C: 该符号是通用的,通用的符号指未初始化的数据。当链接时,多个通用符号可能对应一个名称,如果该符号在某一个位置定义,这个通用符号被当做未定义的引用。不明白,内核中也没有该类型的符号
D: 该符号位于初始化的数据段
G: 位于初始化数据段,专门对应小的数据对象,比如global int x,对应的大数据对象为 数组类型等
I: 到其他符号的间接引用,是对于a.out文件的GNU扩展,使用非常少
N:调试符号
R:只读代码段的符号
S:BSS段(未初始化数据段)的小对象符号
T:代码段符号,全局函数,t为局部函数
U:未定义的符号
V:该符号是一个weak object,当其连接到为定义的对象上上,该符号的值变为0
W: 类似于V
—: 该符号是a.out文件中的一个stabs symbol,获取调试信息
?: 未知类型的符号
(N)UMA 模型中的内存组织
有两种类型计算机,分别以不同的方法管理物理内存
UMA计算机(一致内存访问,uniform memory access)将可用内存以连续方式组织起来(可能有小的缺口)。SMP系统中的每个处理器访问各个内存区都是同样快。 NUMA计算机(非一致内存访问,non-uniform memory access)总是多处理器计算机。系统的各个CPU都有本地内存,可支持特别快速的访问。各个处理器之间通过总线连接起来,以支持对其他CPU的本地内存的访问,当然比访问本地内存慢些。
PS:还有个MPP(Massive Parallel Processing),这里不做讨论。
现在我们用的OS基本都是NUMA内存模式,可以通过lscpu 查看numa相关信息
dp-xxx(xxx@default:prod):message#lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Byte Order: Little Endian
CPU(s): 92
On-line CPU(s) list: 0,1
Off-line CPU(s) list: 2-91
Thread(s) per core: 0
Core(s) per socket: 23
Socket(s): 2
NUMA node(s): 2
Vendor ID: GenuineIntel
CPU family: 6
Model: 85
Model name: Intel(R) Xeon(R) Platinum 8260 CPU @ 2.40GHz
Stepping: 7
CPU MHz: 2399.998
BogoMIPS: 4799.99
Virtualization: VT-x
Hypervisor vendor: KVM
Virtualization type: full
L1d cache: 32K
L1i cache: 32K
L2 cache: 4096K
L3 cache: 16384K
NUMA node0 CPU(s): 0-45
NUMA node1 CPU(s): 46-91
和 numactl --hardware 查看
[root@n227-005-021 fanlv ]$ numactl --hardware
available: 1 nodes (0)
node 0 cpus: 0 1 2 3 4 5 6 7
node 0 size: 15787 MB
node 0 free: 2025 MB
node distances:
node 0
0: 10
NUMA特性禁用
一、检查OS是否开启NUMA
#numactl --hardware
二、Linux OS层面禁用NUMA
1、修改 grub.conf
#vi /boot/grub/grub.conf
#/* Copyright 2010, Oracle. All rights reserved. */
default=0
timeout=5
hiddenmenu
foreground=000000
background=ffffff
splashimage=(hd0,0)/boot/grub/oracle.xpm.gz
title Trying_C0D0_as_HD0
root (hd0,0)
kernel /boot/vmlinuz-2.6.18-128.1.16.0.1.el5 root=LABEL=DBSYS ro bootarea=dbsys rhgb quiet console=ttyS0,115200n8 console=tty1 crashkernel=128M@16M numa=off
initrd /boot/initrd-2.6.18-128.1.16.0.1.el5.img
2、重启Linux操作系统
#/sbin/reboot
3、确认OS层面禁用NUMA是否成功
#cat /proc/cmdline
root=LABEL=DBSYS ro bootarea=dbsys rhgb quiet console=ttyS0,115200n8 console=tty1 crashkernel=128M@16M numa=off
三种内存模型什么是内存模型?
这里的内存模型,是指Linux内核用怎么样的方式去管理物理内存,一个物理内存页(4k),内核会用一个page(64Byte)(类似物理页的meta)去记录Physics Page Number相关信息,下面几种内存模型是讲如何存储这个物理机的meta信息(page),保证内核能快速根据PFN/PPN找到Page,也可以根据Page快速算出PFN。
Linux内存模型发展经历了三个模式,分别为FLATMEM、DISCONTIGMEM、SPARSEMEM。
PS:这里说下PFN和PPN是一个东西。具体可以看这个PPT第4页
FLATMEM (flat memory model)
FLATMEM内存模型是Linux最早使用的内存模型,那时计算机的内存通常不大。Linux会使用一个struct page mem_map[x]的数组根据PFN去依次存放所有的strcut page,且mem_map也位于内核空间的线性映射区,所以根据PFN(页帧号)即可轻松的找到目标页帧的strcut page。
#define __pfn_to_page(pfn) (mem_map + ((pfn) - ARCH_PFN_OFFSET)) #define __page_to_pfn(page) ((unsigned long)((page) - mem_map) + \ARCH_PFN_OFFSET)
而对于FLATMEM来说,如果其管理的的物理内存本身是连续的还好说,如果不连续的话,那么中间一部分物理地址是没有对应的物理内存的,形象的说就像是一个个洞(hole),这会浪费mem_map数组本身占用的内存空间。
DISCONTIGMEM (discontiguous memory model)
对于物理地址空间不存在空洞(holes)的计算机来说,FLATMEM无疑是最优解。可物理地址中若是存在空洞的话,FLATMEM就显得格外的浪费内存,因为FLATMEM会在mem_map数组中为所有的物理地址都创建一个struct page,即使大块的物理地址是空洞,即不存在物理内存。可是为这些空洞这些struct page完全是没有必要的。
那什么情况下物理内存是不连续的?那就要说到后来出现的NUMA。为了有效的管理NUMA模式下的物理内存,一种被称为不连续内存模型(discontiguous memory model)的实现于1999年被引入Linux系统。
DISCONTIGMEM是个稍纵即逝的内存模型,在SPARSEMEM出现后即被完全替代,且当前的Linux kernel默认都是使用SPARSEMEM,所以介绍DISCONTIGMEM的意义不大。
//PS. 这段源码在Linux最新的代码中已经找不到
#define __pfn_to_page(pfn) \
({ unsigned long __pfn = (pfn); \
unsigned long __nid = arch_pfn_to_nid(__pfn); \
NODE_DATA(__nid)->node_mem_map + arch_local_page_offset(__pfn, __nid);\
})
#define __page_to_pfn(pg) \
({ const struct page *__pg = (pg); \
struct pglist_data *__pgdat = NODE_DATA(page_to_nid(__pg)); \
(unsigned long)(__pg - __pgdat->node_mem_map) + \
__pgdat->node_start_pfn; \
})
SPARSEMEM (sparse memory model)
稀疏内存模型是当前内核默认的选择,从2005年被提出后沿用至今,但中间经过几次优化,包括:CONFIG_SPARSEMEM_VMEMMAP和CONFIG_SPARSEMEM_EXTREME的引入,这两个配置通常是被打开的。
CONFIG_SPARSEMEM_VMEMMAP:
/* memmap is virtually contiguous. */
#define __pfn_to_page(pfn) (vmemmap + (pfn))
#define __page_to_pfn(page) (unsigned long)((page) - vmemmap)
// /linux/arch/x86/include/asm/pgtable_64.h
#define vmemmap ((struct page *)VMEMMAP_START)
PS:引入 vmemmap 的核心思想就是用空间(虚拟地址空间)换时间,2008年以后,SPARSEMEM_VMEMMAP 成为 x86-64 唯一支持的内存模型,因为它只比FLAGMEM开销稍微大一点点,但是比DISCONTIGMEM要高效的多。详见这里
转载请注明出处:无忧博主,如有疑问,请联系(762063026)。
本文地址:https://wuyouseo.com/it/4058.html