深入理解Linux虚拟内存管理(一)2

简介: 深入理解Linux虚拟内存管理(一)

深入理解Linux虚拟内存管理(一)1:https://developer.aliyun.com/article/1597676

第3章 页表管理

3.1 描述页目录

    可参考 ==> 1.1 两级页表结构

   每个进程都有一个指向其自己 PGD 的指针(mn_struct→pgd),它其实就是一个物理页面帧。该帧包括了一个 pgd_t 类型的数组,但 pgd_t 因不同的体系结构也有所不同,它的定义在  文件中。不同的结构中载入页表的方式也有所不同。在 x86 结构中,进程页表的载入是通过把 mm_struct→pgd 复制到 cr3 寄存器完成,这种方式对 TLB 的刷新有副作用。事实上,这种方式体现了在不同的结构中 __flush_tlb() 的实现情况。


   PGD 表中每个有效的项都指向一个页面帧,此页面帧包含着一个 pmd_t 类型的 PMD 项数组,每一个 pmd_t 又指指向另外的页面帧,这些页面帧由很多个 pte_t 类型的 PTE 构成,而 pte_t 最终指向包含真正用户数据的页面。当页面被交换到后援设备时,存储在 PTE 里的将是交换项,这个交换项在系统发生页面错误时在调用 do_swap_page 时作为查找页面数据的依据。页表的布局如图 3.1 所示。

    为了能在这三个不同的页表层里产生不同的偏移量以及在实际的页内产生偏移量,任何一个给定的线性地址将会被划分成几个部分。为了有助于将线性地址划分成几个部分,每个页表层均提供了 3 个宏来完成此工作,它 们是 SHIFTSIZEMASK 宏。SHIFT 宏主要用于指定在页面每层映射的长度,以位为单位计算,如图 3.2 所示。

3.2 描述页表项

   如前所述,三层页表中的每一个项 PTE,PMD,PGD 分别由 pte_t,pmd_t,pgd_t 描述。它们实际上都是无符号的整型数据,之所以定义成结构,是出于两个原因。第一是为了起到类型保护的作用,以使得它们不会被滥用。第二是为了满足某些特性,如在支持 PAE 的 x86 中,将有额外的 4 位用于对大于 4 GB 内存的寻址。为了存储用于保护的位,内核中使用 pgprot_t 定义了一些相关的标志位,它们一般被放在页表项的低位。


   出于类型转换的考虑,内核在 asm/page.h 文件中定义了 4 个宏,这些宏管理先前讨论的类型并返回数据结构中相关的部分。它们是 pte_val(), pmd_val(), pgd_val() 和 pgprot_val() 。为了能反向转换,内核又提供了另外 4 个宏 __pte(),__pmd(),__pgd() 和 __pgprot()。


3.3 页表项的使用

   定义在  文件中的一些宏,对于页表项的定位及检查是很重要的。为了定位一个页目录,这里提供了 3 个宏,用以把线性地址分成了 3 个不同的部分。pgd_offset() 通过对一个线性地址和 mm_struct 结构的操作覆盖所需地址的 PGD 项。pmd_offset() 通过对一个 PGD 项和一个线性地址的操作返回相应的 PMD。pte_offset() 通过对一个 PMD 的操作返回相应的 PTE。线性地址剩下的部分就是在此页面内的偏移量部分。这些字段之间的关系如图 3.1 所示。


   第 2 组宏决定了一个页表项是否存在或者是否正在使用中。


pte_none(), pmd_none() 和 pgd_none() 在相应的项不存在时返回 1。

pte_present(), pmd_present() 和 pgd_present() 在相应的页表项的 PRESENT 位被置位时返回 1。

pte_clear(),pmd_clear() 和 pgd_clear() 将相应的页表项清空。

pmd_bad() 和 pgd_bad() 用于在页表项作为函数的参数传递时并且这个函数有可能改变这个表项的值时,对页表项进行检查。它们是否返回 1 需要看某些体系结构是如何定义这些宏。即便是很明确地定义了这些宏,也应确保页面项被设置为存在并被访问。

3.4 页表项的转换和设置

   下面的函数和宏用于映射地址和页面到 PTE,并设置个别的项。


   宏 mk_pte() 用于把一个 struct page 和一些保护位合成一个 pte_t,以便插入到页表当中。另一个宏 mk_pte_phys() 也具有类似的功能,它将一个物理页面的地址作为参数。


   宏 pte_page() 返回一个与 PTE 项相对应的 struct page。pmd_page() 则返回包含页表项集的 struct page。


   宏 set_pte() 把诸如从 mk_pte 返回的一个 pte_t 写到进程的页表里。pte_clear() 则是反向操作。此外还有一个函数 ptep_get_and_clear() 用于清空进程页表的一个项并返回 pte_t。不论是对 PTE 的保护还是 struct page 本身,一旦需要修改它们时这个工作则是很重要的。


3.5 页表的分配和释放

   最后一组函数用于对页表进行分配和释放。如前所述,页表是一些包含一个项数组的物理页面。而分配和释放物理页面的工作,相对而言代价很高,这不仅体现在时间上,还体现在页面分配时是关中断的。无论在 3 级的哪一级,分配已经被删除页表的操作都是非常频繁的,所以要求这些操作尽可能地快很重要。


   于是,这些物理页面被缓存在许多被称作快速队列的不同队列里。不同的结构实现它们虽都有所不同,但原理相同。例如,并不是所有的系统都会缓存 PGD,因为对它们的分配和释放只发生在进程创建和退出的时候。因为这些操作花费的代价较大,其他页面的分配就显得微不足道了。


   PGD,PMD 和 PTE 有两组不同的函数分别用作分配页表和释放页表。分配的函数有 pgd_alloc(),pmd_alloc() 和 pte_alloc(),相对应的释放函数有 pgd_free(),pmd_free() 和 pte_free()。


   具体而言,存在三种不同的用作缓冲的高速缓存,分别称为 pgd_quicklist, pmd_quicklist 以及 pte_quicklist。不同的结构实现它们的方式虽都有所不同,但都使用了一种叫做后入先出(LIFO)的结构。一般而言,一个页表项包含着很多指向其他包括页表或数据的页面的指针。当队列被缓存时,队列里的第一个元素将指向下一个空闲的页表。在分配时,这个队列里最后进入的元素将被弹出来,而在释放时,一个元素将被放入这个队列中作为新的队列首部。

使用一个计数器对这个高速缓存中所包含的页面数量进行计数。


   虽然可以选用 get_pgd_fast() 作为 pgd_quicklist 上快速分配函数的名称,但 Linux 并未独立于体系结构对它进行显式的定义。PMD 和 PTE 的缓存分配函数明确地定义为 pmd_alloc_one_fast() 和 pte_alloc_one_fast()。


   如果高速缓存中没有多余的页面,页面的分配将通过物理页面分配器(见第 6 章)完成。分别对应 3 级的函数为 get_pgd_slow(),pmd_alloc_one() 以及 pte_alloc_one()。


   显然,在这些高速缓存中可能有大量的页面存在,所以应当有一种机制来管理高速缓存的空间。每当高速缓存增大或收缩时,就通过一个计数器增大或减小来计数,并且该计数器有最大值和最小值。在两个地方中可以调用 check_pgt_cache() 检查这些极值。当计数器达到最大值时,系统就会释放一些高速缓存里的项,直到它重新到达最小值。在调用 clear_page_tables() 后,在可能有大量的页表到达时,系统就会调用 check_pgt_cache(),这个函数同时也可以被系统的空闲任务所调用。

3.6 内核页表

3.6.1 引导初始化

  在 arch/i386/kernel/head.S 的汇编程序中的函数 startup_32() 主要用于开启页面单元。由于所有在 vmlinuz 中的普通内核代码都编译成以 PAGE_OFFSET+1 MB 为起始地址,实际上系统将内核装载到以第一个 1MB(0x00100000) 为起始地址的物理空间中。第一个 1MB 的地址常在一些设备用作和 BIOS 进行通讯的地方自行跳过。该文件中的引导初始化代码总是把虚拟地址减去 __PAGE_OFFSET,从而能获得以 1 M 为起始地址的物理地址。所以在开启换页单元以前,必须首先建立相应的页表映射,从而将 8 MB 的物理空间转换为虚拟地址 PAGE_OFFSET。


   初始化工作在编译过程中开始进行,它先静态地定义一个称为 swapper_pg_dir 的数组,使用链接器指示在地址 0x00101000。然后分别为两个页面 pg0 和 pg1 创建页表项。如果处理器支持页面大小拓展(PSE)位,那么该位将被设置,使得调动的页面大小是 4 MB,而不是通常的 4 KB。第一组指向 pg0 和 pg1 的指针被放在能覆盖 1~9 MB 内存空间的位置;而第二组则被放置在 PAGE_OFFSET+1 MB 的位置。这样一旦开启换页,在上述页表及页表项指针建立后系统可以保证,在内核映象中不论是采用物理地址还是采用虚拟地址,页面之间的映射关系都是正确的。


   映射建立后,系统通过对 CR0 寄存器某一位置位开启换页单元,接着通过一个跳跃指令保证指令指针(EIP 寄存器)的正确性。

3.6.2 收尾工作

    用于完成页表收尾工作的对应函数是 paging_init()x86 上该函数的调用图如图 3.4 所示。


   系统首先调用 pagetable_init() 初始化对应于 ZONE_DMA 和 ZONE_NORMAL 的所有物理内存所必要的页表。注意在 ZONE_HIGHMEM 中的高端内存不能被直接引用,对其的映射都是临时建立起来的。对于内核所使用的每一个 pgd_t,系统会调用引导内存分配器(见第 5 章)来分配一个页面给 PGD。若可以使用 4 MB 的 TLB 项替换 4 KB,则 PSE 位将被设置。如果系统不支持 PSE 位,那么系统会为每个 pmd_t 分配一个针对 PTE 的页面。若 CPU 支持 PGE 标志位,系统也会设置该标志以使得页表项是全局性的,以对所有进程可见。


   接下来,pagetable_init() 函数调用 fixrange_init() 建立固定大小地址空间,以映射从虚拟地址空间的尾部即起始于 FIXADDR_START 的空间。映射用于局部高级编程中断控制器(APIC),以及在 FIX_KMAP_BEGIN 和 FIX_KMAP_END 之间 kmap_atomic() 的原子映射。最后,函数调用 fixrange_init() 初始化高端内存映射中 kmap() 函数所需要的页表项。


   在 pagetable_init() 函数返回后,内核空间的页表则完成了初始化,此时静态 PGD(swapper_pg_dir)被载入 CR3 寄存器中,换页单元可以使用静态表。


   paging_init() 接下来的工作是调用 kmap_init() 初始化带有 PAGE_KERNEL 标志位的每个 PTE。最终的工作是调用 zone_sizes_init(),用于初始化所有被使用的管理区结构。

3.7 地址和struct page之间的映射

   对 Linux 而言,必须有一种快速的方法把虚拟地址映射到物理地址或把 struct page 映射到它们的物理地址。Linux 为实现该机制,在虚拟和物理内存使用了一个 mem_map 的全局数组,因为这个数组包含着指向系统物理内存所有 struct page 的指针。所有的结构都采用了非常相似的机制,为了表述简单,在这里我们只详细讨论 x86 结构中的情况。本节我们首先讨论物理地址如何被映射到内核虚拟地址,以及又是如何利用 mem_map 数组的。

3.7.1 物理和虚拟内核地址之间的映射

   正如在 3.6 节看到的,在 x86 中 Linux 把从 0 开始的物理地址直接映射成从 PAGE_OFFSET 即 3GB 开始的虚拟地址。这意味着在 x86 上,可以简单地将任意一个虚拟地址减去 PAGE_OFFSET 而获得其物理地址,就像函数 virt_to_phys() 和宏 __pa() 所做的那样:

/* from <asm-i386/page.h> */
# define __pa(x)((unsigned long)(x)-PAGE_OFFSET)

/* from <asm-i386/io.h */
static inline unsigned long virt_to_phys(volatile void * address)
{
  return __pa(address);
}

   很明显,逆操作只需简单加上 PAGE_OFFSET 即可,它通过 phys_to_virt() 和宏 __va() 完成。接下来我们将看到内核如何利用这些功能将 struct pages 映射成物理地址。


   有一个例外,就是 virt_to_phys() 不能用于将虚拟地址转换成物理地址。尤其是在 PPC 和 ARM 的结构中,virt_to_phys 不能转换由 consistent_alloc() 函数返回的地址。在 PPC 和 ARM 结构中,使用 consistent_alloc() 函数从无缓冲的 DMA 返回内存。

3.7.2 struct page和物理地址间的映射

   正如在 3.6.1 所看到的一样,系统将内核映象装载到 1 MB 物理地址起始位置,当然,这个物理地址就是虚拟地址 PAGE_OFFSET + 0x00100000。此外,物理内存为内核映象预留了 8 MB 的虚拟空间,这个空间可以被 2 个 PGD 所访问到。这好像意味着第一个有效的内存空间应在 0xC0800000 开始的地方,但事实并非如此。Linux 还为 ZONE_DMA 预留了 16 MB 的内存空间,所以真正能被内核分配使用的内存起始位置应在 0xC1000000,这个位置即为全局量 mem_map 所在的位置。虽然 Linux 还使用 ZONE_DMA,但是只会在非常有必要的情况下使用。


   通过把物理地址作为 mem_map 里的一个下标,从而将其转换成对应的 struct pages。通过把物理地址位右移 PAGE_SHIFT 位,从而将右移后的物理地址作为从物理地址 0 开始的页面帧号 (PFN),它同样也是 mem_map 数组的一个下标 。 正如宏 virt_to_page() 所做的那样,其声明在  中如下:

#define virt_to_page(kaddr)(mem_map +(__pa(kaddr)>> PAGE_SHIFT))

    宏 virt_to_page() 通过 __pa() 把虚拟地址 kaddr 转换成物理地址,然后再通过右移 PAGE_SHIFT 位转换成 mem_map 数组的一个下标,接着通过简单的加法操作就可以在 mem_map 中查找它们。Linux 中不存在将页面转换成物理地址的宏,但你应该知道如何计算。

3.8 转换后援缓冲区(TLB)

   最初,当处理器需要映射一个虚拟地址到一个物理地址时,需要遍历整个页面目录以搜索相关的 PTE。通常这表现为每个引用内存的汇编指令实际上需要多个页表截断的相分离的内存引用 [Tan01]。为了避免这种情况的过度出现,许多体系结构中都利用了这样一个事实,就是大多数的进程都是采用局部引用,或者,换句话说,少量的页面却使用了大量的内存引用。它们提供一个转换后援缓冲区(TLB)来利用这种引用的局部性原理,这个高速缓存是一个联合内存,用来缓存虚拟到物理页表转换的中间结果。


   Linux 假设大多数的体系结构都是支持 TLB 的,即便独立于体系结构的代码并不关心它如何工作。相反,与体系结构相关的钩子都分散在 VM 的代码中,大家知道,一些带有 TLB 的硬件是需要提供一个 TLB 相关的操作的。 例如,在页表更新后,诸如在一个页面错误发生时,处理器可能需要更新 TLB 以满足虚拟地址的映射要求。


   不是所有的体系结构都需要这种类型的操作,但是,因为有些体系结构是需要的,所以 Linux 中就需要存在钩子。 如果某个体系结构并不需要诸如此类的操作,那么在这个体系结构中完成 TLB 操作的函数就是一个空函数,这在编译时就进行过优化。


   大部分关于 TLB 的 API 钩子列表都在  中声明,如表 3.2 和表 3.3 所列,在内核源码的 Documentation/cachetlb.txt[Miloo] 文件已写明了这些 API。在某些情况下可能仅只有一个 TLB 刷新操作,但由于 TLB 刷新操作和 TLB 填充工作都是开销非常大的操作,所以应尽可能避免不必要的 TLB 刷新操作。例如,切换上下文时,Linux 会使用延迟 TLB 刷新以避免载入新的页表,这将在 4.3 节作进一步的讨论。

3.9 一级CPU高速缓存管理

3.10 2.6中有哪些新特性

  • MMU-less 体系结构的支持
  • 反向映射
  • 基于对象的反向映射
  • 高端内存中的 PTE
  • 大型 TLB 文件系统
  • 高速缓存刷新管理

第4章 进程地址空间

4.1 线性地址空间

   从用户的角度来看,地址空间是一个平坦的线性地址空间,但从内核的角度来看却大不一样。地址空间分为两个部分:一个是随上下文切换而改变的用户空间部分,一个是保持不变的内核空间部分。两者的分界点由 PAGE_OFFSET 决定,在 x86 中它的值是 0xC0000000(3G)。这意味着有 3 GB 的空间可供用户使用,与此同时,内核可以映射剩余的 1 GB 空间。内核角度的线性虚拟地址空间如图 4.1 所示。


   系统为了载入内核映象,需要保留从 PAGE_OFFSET 开始的 8 MB (两个 PGD 定位的内存大小)空间,这 8 MB 只是为载入内核而保留的合适空间。如 3.6.1 小节所述,内核映象在内核页表初始化时被放置到此保留的 8 MB 空间内,紧随其后的是供 UMA 体系结构使用的 mem_map 数组,这已经在第 2 章 中讨论过。该数组通常位于标记为 16 MB 的位置,但为避免用到 ZONE_DMA,也不是经常这样。对于 NUMA 体系结构,虚拟 mem_map 各部分分散在该区域内,各部分所在具体位置由不同的体系结构决定。

// include/asm-i386/pgtable.h
#define VMALLOC_OFFSET  (8*1024*1024)
#define VMALLOC_START (((unsigned long) high_memory + 2*VMALLOC_OFFSET-1) & \
            ~(VMALLOC_OFFSET-1))
#define VMALLOC_VMADDR(x) ((unsigned long)(x))
#define VMALLOC_END (FIXADDR_START)

// include/asm-i386/fixmap.h
#define FIXADDR_TOP (0xffffe000UL)
#define FIXADDR_SIZE  (__end_of_fixed_addresses << PAGE_SHIFT)
#define FIXADDR_START (FIXADDR_TOP - FIXADDR_SIZE)

// include/asm-i386/page.h
#define PAGE_SHIFT  12
#define __PAGE_OFFSET   (0xC0000000)

// arch/i386/kernel/setup.c
/*
 * 128MB for vmalloc and initrd
 */
#define VMALLOC_RESERVE (unsigned long)(128 << 20)

   从 PAGE_OFFSET 到 VMALLOC_START - VMALLOC_OFFSET 是物理内存映射的部分。这个区域的大小由可用 RAM 的大小决定。正如我们将在 4.6 节所看到的,它通过页表项把物理内存映射到 PAGE_OFFSET 开始的虚拟地址。为防止边界错识,在物理内存映射和 vmalloc 地址空间之间存在一个大小为 VMALLOC_OFFSET 的空隙。在 x86 上,这个空间大小为 8 MB。例如,在一个 RAM 大小为 32 MB 的 x86 系统上,VMALLOC_START 等于 PAGE_OFFSET + 0x02000000(32M) + 0x00800000(8M)。


   在小内存的系统中,为了能使 vmalloc() 在一个连续的虚拟地址空间里表示一个非连续的内存分配情况,余下的虚拟地址空间减去 2 个页面空隙的大小将全部用于 vmalloc()。而在大内存系统中,vmalloc 的区域则扩大到 PKMAP_BASE 减去 2 个页面空隙的大小,此外还引入 2 个区域。第 1 个是从 PKMAP_BASE 开始的区域,这部分保留给 kmap() 使用,而 kmap() 的作用是把高端内存页面映射到低端内存,如第 9 章所述。第 2 个区域是从 FXADDR_STAT 至 FIXADDR_TOP 的固定虚拟地址映射区域,这个区域供在编译时需要知道虚拟地址的子系统使用,例如高级可编程的中断控制器(APIC)。FIXADDR_TOP 在 x86 中静态地定义为 0xFFFFE000,这个位置在虚拟地址空间结束的前一页上。固定映射区域的大小通过在编译时的 __FIXADDR_SIZE 变量计算,再从 FIXADDR_TOP 向后索引 __FIXADDR_SIZE 大小,从而标识 FIXADDR_START 区域的起始地址。


   vmalloc(),kmap() 以及固定映射区域所需的区域大小限制了 ZONE_NORMAL 的大小。由于运行中的内核需要这些函数,所以在地址空间的顶端至少需要保留 VMALLOC_RESERVE 大小的区域。VMALLOC_RESERVE 在每个体系结构中都有所不同,在 x86 中它是128 MB。这正是 ZONE_NORMAL 大小通常只有 896 MB 的原因。vmalloc 区域由线性地址空间上端 1 GB 空间大小减去保留的 128 MB 区域所得。

4.2 地址空间的管理

   进程可使用的地址空间由 mm_struct 管理,它类似于 BSD 中的 vmspace 结构[McK96]。


   每个进程地址空间中都包含许多使用中的页面对齐的内存区域。它们不会相互重叠,而

且表示了一个地址的集合,这个集合包含那些出于保护或其他目的而相互关联的页面。这些区域由 struct vm_area_struct 管理,它们类似于 ESD 中的 vm_map_entry 结构。具体而言,一个区域可能表示 malloc() 所使用的进程堆,或是一个内存映射文件(例如共享库),又或是一块由 mmap() 分配的匿名内存区域。这些区域中的页面可能还未被分配,或已分配,或常驻内存中又或已被交换出去。


   如果一个区域是一个文件的映象,那么它的 vm_file 字段将被设置。通过查看 vm_file→f_dentry→d_inode→i_mapping 可以获得这段区域所代表的地址空间内容。这个地址空间包含所有与文件系统相关的特定信息,这些信息都是为了实现在磁盘上进行基于页面的操作。


   图 4.2 中图示了各种地址空间相关结构之间的关联。表 4.1 则列举了许多影响地址空间

和区域的系统调用。



4.3 进程地址空间描述符

   进程地址空间由 mm_struct 结构描述,这意味着一个进程只有一个 mm_struct 结构,且该结构在进程用户空间中由多个线程共享。事实上,线程正是通过任务链表里的任务是否指

向同一个 mm_struct 来判定的。


   内核线程不需要 mm_struct,因为它们永远不会发生缺页中断或访问用户空间。惟一的例外是 vmalloc 空间的缺页中断。缺页中断的处理代码认为该例外是一种特殊情况,并借助主页表中的信息更新当前页表。由于内核线程不需要 mm_struct,故 task_struct->mm 字段总为 NULL。对某些任务如引导空闲任务 ,mm_struct 永远不会被设置,但对于内核线程而言,调用 daemonize() 也会调用 exit_mm() 以减少对它的使用计数。


   由于 TLB 的刷新需要很大的开销,特别是像在 PPC 这样的体系结构中,由于地址空间的内核部分对所有进程可见,那些未访问用户空间的进程所做的 TLB 刷新操作就是无效的,而 Linux 采用了一种叫 “延迟 TLB” 的技术避免了这仲刷新操作。Linux 通过借用前个任务的 mm_struct,并放入 task_struct->active_mm 中,避免了调用 switch_mm() 刷新 TLB。这种技术在上下文切换次数上取得了很大的进步。


   进入延迟 TLB 时,在对称多处理机(Symmetric Multiprocessing,SMP)上,系统会调用 enter_lazy_tlb() 函数以确保 mm_struct 不会被 SMP 的处理器所共享。而在 UP 机器上这是一个空操作。第二次用到延迟 TLB 是在进程退出时,系统会在该进程等待被父进程回收时,调用 start_lazy_tlb() 函数。


   该结构有两个引用计数,分别是 mm_users 和 mm_count。mm_users 描述存取这个 mm_struct 用户空间的进程数,存取的内容包括页表、文件的映象等。例如,线程以及 swap_out() 代码会增加这个计数以确保 mm_struct 不会被过早地释放。当这个计数值减为 0 时,exit_mmap() 会删除所有的映象并释放页表,然后减小 mm_count 值。


   mm_count 是对 mm_sturct 匿名用户的计数。初始化为 1 则表示该结构的真实用户。匿名用户不用关心用户空间的内容,它们只是借用 mm_struct 的用户。例子用户是使用延迟 TLB 转换的核心线程。当这个计数减为 0 时,就可安全释放掉 mm_struct。存在两种计数是因为匿名用户需要 mm_struct,即便 mm_struct 中的用户映象已经被释放。但它不会延迟页表的释放操作。

// include/linux/sched.h
struct mm_struct {
  struct vm_area_struct * mmap;   /* list of VMAs */
  rb_root_t mm_rb;
  struct vm_area_struct * mmap_cache; /* last find_vma result */
  pgd_t * pgd;
  atomic_t mm_users;      /* How many users with user space? */
  atomic_t mm_count;      /* How many references to "struct mm_struct" (users count as 1) */
  int map_count;        /* number of VMAs */
  struct rw_semaphore mmap_sem;
  spinlock_t page_table_lock;   /* Protects task page tables and mm->rss */

  struct list_head mmlist;    /* List of all active mm's.  These are globally strung
             * together off init_mm.mmlist, and are protected
             * by mmlist_lock
             */

  unsigned long start_code, end_code, start_data, end_data;
  unsigned long start_brk, brk, start_stack;
  unsigned long arg_start, arg_end, env_start, env_end;
  unsigned long rss, total_vm, locked_vm;
  unsigned long def_flags;
  unsigned long cpu_vm_mask;
  unsigned long swap_address;

  unsigned dumpable:1;

  /* Architecture-specific MM context */
  mm_context_t context;
};

该结构中各个字段的含义如下。


mmap VMA:地址空间中所有 VMA 的链表首部。

mm_rb VMA:VMA 都排列在一个链表中,且存放在一个红黑树中以加快查找速度。该字段表示树的根部。

mmap_cache:最后一次通过 find_vma() 找到的 VMA 存放处,前提假设是该区可能会被再次用到。

pgd:全局目录表的起始地址。

mm_users:访问用户空间部分的用户计数值,本节已经介绍过。

mm_count:匿名用户计数值。访问 mm_struct 的匿名用户技术值。本节已经介绍过。数值 1 针对真实用户。

map_count:正被使用中的 vma 的数量。

mmap_sem:这是一个读写保护锁并长期有效。因为用户需要这种锁作长时间的操作或者可能进入睡眠,所以不能用自旋锁。一个读操作通过 down_read() 来获得这个信号量。如果需要进行写操作,则通过 down_write() 获得该信号量,并在 VMA 链表更新后,获得 page_table_lock 锁。

page_table_lock:该锁用于保护 mm_struct 中大部分字段,与页表类似,它防止驻留集大小(Resident Set Size,RSS)(见 rss)计数和 VMA 被修改。

mmlist:所有的 mm_struct 结构通过它链接在一起。

start_code,end_code:代码段的起始地址和结束地址。

start_data,end_data:数据段的起始地址和结束地址。

start_brk,brk:堆的起始和结束地址。

start_stack:栈的起始地址。

arg_start,arg_end:命令行参数的起始地址和结束地址。

env_start,env_end:环境变量区域的起始和结束地址。

rss:驻留集的大小是该进程常驻内存的页面数,注意,全局零页面不包括在 RSS 计数之内。

total_vm:进程中所有 vma 区域的内存空间总和。

locked_vm:内存中被锁住的常驻页面数。

def_flags:只有一种可能值,VM_LOCKED。它用于指定在默认情况下将来所有的映射是上锁还是未锁。

cpu_vm_mask:代表 SMP 系统中所有 CPU 的掩码值,内部处理器中断(IPI)用这个值来判定一个处理器是否应执行一个特殊的函数。这对于每一个 CPU 的 TLB 刷新很重要。

swap_address:当换出整个进程时,页换出进程记录最后一次被换出的地址。

dumpable:由 prctl() 设置,只有在跟踪一个进程时,这个字段才有用。

context:跟体系结构相关的 MMU 上下文。

 对 mm_struct 结构进行操作的函数描述如表 4.2 所列。

4.3.1 分配一个描述符

   系统有两个函数用于分配 mm_struct 结 构。它们本质上相同,但有一个重要的区别。allocate_mm() 只是一个预处理宏,它从 slab allocator (见第 8 章) 中分配一个 mm_struct。而 mm_alloc() 从 slab 中分配,然后调用 mm_init() 函数对其初始化。

4.3.2 初始化一个描述符

    系统中第一个 mm_struct 通过 init_mm() 初始化。因为后继的子 mm_struct 都通过复制进行设置,所以第 1 个 mm_struct 在编译时静态设置,通过宏 INIT_MM 完成设置。

#define INIT_MM(name) \
{             \
  mm_rb:    RB_ROOT,      \
  pgd:    swapper_pg_dir,     \
  mm_users: ATOMIC_INIT(2),     \
  mm_count: ATOMIC_INIT(1),     \
  mmap_sem: __RWSEM_INITIALIZER(name.mmap_sem), \
  page_table_lock: SPIN_LOCK_UNLOCKED,    \
  mmlist:   LIST_HEAD_INIT(name.mmlist),  \
}

    第 1mm_struct 创建后,系统将该 mm_struct 作为一个模板来创建新的 mm_structcopy_mm() 函数完成复制操作,它调用 init_mm() 初始化与具体进程相关的字段。

4.3.3 销毁一个描述符

  新的用户通过 atomic_inc(&mm->mm_users) 增加使用计数,同时通过 mmput() 减少该计数。如果 mm_users 变成 0,所有的映射区域通过 exit_mmap() 释放,同时释放页表,因为已经没有用户使用这个用户空间。mm_count 之所以通过 mmdrop() 减 1,是因为所有页表和 VMA 的使用者都被看成是一个 mm_struct 用户。在 mm_count 变成 0 时,mm_struct 会被释放。

// kernel/fork.c
void mmput(struct mm_struct *mm)
{
  if (atomic_dec_and_lock(&mm->mm_users, &mmlist_lock)) {
    list_del(&mm->mmlist);
    spin_unlock(&mmlist_lock);
    exit_mmap(mm);
    mmdrop(mm);
  }
}

深入理解Linux虚拟内存管理(一)3:https://developer.aliyun.com/article/1597689


目录
相关文章
|
3月前
|
安全 Linux Shell
Linux上执行内存中的脚本和程序
【9月更文挑战第3天】在 Linux 系统中,可以通过多种方式执行内存中的脚本和程序:一是使用 `eval` 命令直接执行内存中的脚本内容;二是利用管道将脚本内容传递给 `bash` 解释器执行;三是将编译好的程序复制到 `/dev/shm` 并执行。这些方法虽便捷,但也需谨慎操作以避免安全风险。
195 6
|
13天前
|
缓存 Java Linux
如何解决 Linux 系统中内存使用量耗尽的问题?
如何解决 Linux 系统中内存使用量耗尽的问题?
|
13天前
|
缓存 Linux
如何检查 Linux 内存使用量是否耗尽?
何检查 Linux 内存使用量是否耗尽?
|
22天前
|
算法 Linux 开发者
深入探究Linux内核中的内存管理机制
本文旨在对Linux操作系统的内存管理机制进行深入分析,探讨其如何通过高效的内存分配和回收策略来优化系统性能。文章将详细介绍Linux内核中内存管理的关键技术点,包括物理内存与虚拟内存的映射、页面置换算法、以及内存碎片的处理方法等。通过对这些技术点的解析,本文旨在为读者提供一个清晰的Linux内存管理框架,帮助理解其在现代计算环境中的重要性和应用。
|
5天前
|
存储 算法 安全
深入理解Linux内核的内存管理机制
本文旨在深入探讨Linux操作系统内核的内存管理机制,包括其设计理念、实现方式以及优化策略。通过详细分析Linux内核如何处理物理内存和虚拟内存,揭示了其在高效利用系统资源方面的卓越性能。文章还讨论了内存管理中的关键概念如分页、交换空间和内存映射等,并解释了这些机制如何协同工作以提供稳定可靠的内存服务。此外,本文也探讨了最新的Linux版本中引入的一些内存管理改进,以及它们对系统性能的影响。
|
28天前
|
存储 缓存 监控
|
2月前
|
存储 缓存 监控
Linux中内存和性能问题
【10月更文挑战第5天】
40 4
|
2月前
|
算法 Linux
Linux中内存问题
【10月更文挑战第6天】
58 2
|
25天前
|
缓存 算法 Linux
Linux内核中的内存管理机制深度剖析####
【10月更文挑战第28天】 本文深入探讨了Linux操作系统的心脏——内核,聚焦其内存管理机制的奥秘。不同于传统摘要的概述方式,本文将以一次虚拟的内存分配请求为引子,逐步揭开Linux如何高效、安全地管理着从微小嵌入式设备到庞大数据中心数以千计程序的内存需求。通过这段旅程,读者将直观感受到Linux内存管理的精妙设计与强大能力,以及它是如何在复杂多变的环境中保持系统稳定与性能优化的。 ####
31 0
|
2月前
|
存储 缓存 固态存储