-10 +

virtual memory

一个例子

char msg[] = "Hallo, world";    // mov %edi, $224cc
msg[1] += 4;                    // add (%edi), $4

上述例子中我们看到了编译器使用了一个绝对地址,考虑以下问题:

如果第二个程序输出是 Hillo, world 那么显然不符合预期,如果希望输出 Hello, world,就得满足两个条件:

Virtual memory 就是为了回答上述问题来设计的。

Virtual memory

page table

现代虚拟内存是基于 page 地址来管理的,所有的物理内存会分割为最小 page 单元(4k in x86)。 进程会以 [BASE;BASE+PAGESIZE) 映射到单独的一页。多个页的管理叫做 page table。 现代 CPU 提供大页管理,这个可以支持 M 甚至 G 大小。根据上面的例子,linux kernel 将设置进程的虚拟内存, 并且 copy 数据到物理内存中:

当第二个进程启动后,新的进程会分配单独的虚拟地址,所以数据会被单独 copy 到新的物理地址。但是使用的还是之前的地址。 当进程调度到 CPU,这时 page table 地址会被单独的存储到特定寄存器(如 CR3 on x86)。 所有地址将会被 CPU Memory Management Unit 单元翻译。

segments

一个进程中虚拟地址,是以组的形式来管理的,这些组被叫做 segments

当调用 execve() 后,进程的虚拟地址就被创建出来了,新创建的虚拟地址有 4 个特定的 segments

进程的参数和环境变量会被 push 到 stack 上。

我们可以尝试使用 malloc() 来分配内存,标准 C lib 库使用 brk() 或者 sbrk() 系统调用。 程序也可以使用 mmap() 来映射文件到内存中, 如果没有传递文件参数给 mmap(),那么会创建一个叫做 anonymous memory 的特殊 segment。 这样的内存可用于内存分配器,这个是于程序无关的。

在 linux 咱们可以使用 pmap 或者 /proc/PID/mapping 文件系统来查看进程的虚拟地址。

address space struct

linux 使用 mm_struct 结构体来管理虚拟地址:

vm_area_struct 表一个 segment,其中 vm_start 代表 segment 开始位置, vm_end 代表 segment 结束位置。 kernel 主要维护了两个主要的 segment 列表:

segment 也可能映射文件,所有会有一个 non-NULL 值在 vm_file 上,指向一个 file。 一个 file 会有 address_space 包含一个文件的所有 pagesaddress_space 对象上的 page_tree 属性上。 这个对象还通过线性和非线性列表来应用文件的 inode,因此这个文件的所有映射都可以共享。 另外一个映射方式 anonymous memory,这个数据保存在 anon_vma 结构中, 每一个 segment 都存在一个 vm_mm 指针引用 mm_struct

mm_struct 还包含其他有用的信息,例如:

linux 可以保存缓存内存的统计信息,在进程的 mm_struct 中的 rss_stat 属性上。

Page fault

如上所述,当一个进程访问内存时,memory management unit 获取一个地址,从 page table 找到这个地址的入口,并且获取到物理地址。 但是这个地址可能不存在。这时 CPU 将触发一个错误,这个错误叫做 page fault, page faults 会发生在以下三种场景:

Page faults 是影响性能的,因为这个会中断进程的执行,所有设计了各种系统调用, mlock(), madvise() 这些允许为内存区域设置 flag 来减少 memory faults。 例如:mlock() 应该保证内存的分配,所以这个内存区域不会引发 minor fault。 如果 page faults 出现在 kernel 地址空间,这个将导致 kernel panic。

Kernel allocator

Virtual memory 在 kernel 和 application 之间的分配子系统叫做 kernel allocator。 这个可能会被用到应用程序或者 kernel 内部,如: ethernet packets,block input-output buffers 等。

底层级的 kernel allocator 是一个 page allocator。这个维护了一个 free pages 列表, cache pages 是 cache 文件系统数据,这个可能会很容易被驱逐。used pages 会被保留,因为这个需要被刷入磁盘中。

对于 kernel 对象而言,单页(4k 或者 8k)通常太大了,因为这些结构通常比较小。 另外,实现一个经典的堆内存分配其不是非常有效,kernel 需要经常分配相同大小的对象。

参考

关于我

85 后程序员, 比较熟悉 Java,JVM,Golang 相关技术栈, 关注 Liunx kernel,目前痴迷于分布式系统的设计和实践。 研究包括但不限于 Docker Kubernetes eBPF 等相关技术。

Blog

Code

Life

Archive