Linux内存管理宏观篇(七)虚拟内存

简介: Linux内存管理宏观篇(七)虚拟内存

Linux内存管理宏观篇(七)虚拟内存

前面知道了物理内存,物理内存是实打实的,我只有这么多,用的时候你只能用这么多。

为了解决一些问题,产生虚拟内存,通过虚拟内存可以让我们每个进程都能拥有虚拟的3GB用户态地址空间,同时与硬件层屏蔽后还可以增加我们程序的移植性。以及众多好处,这里在前面认识内存的时候讲过,移植性,保护内存安全等等。

我觉得最重要的是我们可以通过地址虚拟赋予每个进程更大的内存使用空间,当然这就涉及到不断地映射与释放。而我们的进程基本上不会实实在在的得到3GB的物理内存。

咱们常用的malloc()是内存分配的接口

而mmap()是用户态用来建立文件映射或者匿名映射的函数。

知道了

  • 虚拟内存是什么?
  • 虚拟内存为什么?

那么对于虚拟内存是怎么管理的?

进程的地址空间管理,内核使用了struct vm_area_struct数据结构来描述(又是通过一个结构体,或者说是对象):VMA,中文名可以称为进程地址空间或者进程线性区。

因为进程的地址空间肯定是进程的元素,因此在进程的管理结构体 struct mm_struct也会有进程地址空间属性,这个属性就是管理VMA。

下面就来看看进程地址空间。

1、进程地址空间

1、是什么?

是进程可以寻址的虚拟空间地址

2、有多大?

32位的是4GB

3、都能操作?

不得行,只有3GB的用户地址可以,内核地址需要通过系统调用。而用户空间的进程地址空间则可以被合法访问,地址空间称为内存区域(memory area)。

4、怎么去操作?

进程可以通过内核的内存管理机制动态地添加和删除这些内存区域,这些内存区域在Linux内核采用VMA数据结构来抽象描述。

5、去了错的地方,做了错的事情怎么办?

不可能,内存的区域权限不同,每个进程有自己的相应的内存区域。任何不安全的行为都会报警。严重的会报告“Segment Fault”段错误并终止该进程

6、内存区域有什么?

代码段映射,可执行文件中包含只读并可执行的程序头,如代码段和init段等。

数据段映射,可执行文件中包含可读可写的程序头,如数据段和bss段等。

用户进程的栈。通常是在用户空间的最高地址,从上往下延伸。它包含栈帧,里面包含了局部变量和函数调用参数等。注意不要和内核栈混淆,进程的内核栈独立存在并有内核维护,主要用于上下文切换。

MMAP 映射区域。位于用户进程栈下面,主要用于 mmap 系统调用,比如映射一个文件的内容到进程地址空间等。

堆映射区域。malloc()函数分配的进程虚拟地址就是这段区域。

7、虚拟内存大家都是3GB,要是小红和小绿搞到一起怎么办?

不可能的,因为虚拟地址分配的值一样,但是每个进程都有自己的页表,页表是独一份,因此每个进程能访问的区域都是相互隔离的。各有不同。(你以为给了你全世界,实际上只是一个角)

下面来看看内存描述符是什么?

2、内存描述符 mm_struct

对于进程的内存区域和对应的页表映射,内核采用抽象一个数据结构来管理。然后在进程控制块(PCB)结构task_struct中有一个指针mm是指向这个mm_struct数据结构的。

task_struct(mm)–>mm_struct–>VMA

mm_struct数据结构定义在include/linux/mm_types.h文件中,下面是它的主要成员。

之前我们从进程的角度看过内存,这里可以结合一起看一下。

下面看看VMA

3、VMA管理

VMA数据结构定义在mm_types.h文件中。[include/linux/mm_types.h]

进一步看看mm_struct,struct mm_struct数据结构是描述进程内存管理的核心数据结构,该数据结构也提供了管理VMA所需要的信息,这些信息概况如下。

[include/linux/mm_types.h]
struct mm_struct {
struct vm_area_struct *mmap;
struct rb_root mm_rb;
};

每个VMA都要连接到mm_struct中的链表和红黑树中,以方便查找。mmap 形成一个单链表,进程中所有的 VMA 都链接到这个链表中,链表头是mm_struct->mmap。

mm_rb是红黑树的根节点,每个进程有一棵VMA的红黑树。

VMA按照起始地址以递增的方式插入mm_struct->mmap链表。当进程拥有大量的VMA时,扫描链表和查找特定的VMA是非常低效的操作,例如在云计算的机器中,所以内核中通常要靠红黑树来协助,以便提高查找速度。从VMA的角度来观察进程的内存管理,如图7.20所示。(当年特么的学的算法终于用上了)

1.查找VMA

通过虚拟地址addr来查找VMA是内核中常用的操作。内核提供一个API函数来实现这个查找操作。

struct vm_area_struct *find_vma(struct mm_struct *mm, unsigned long addr)
struct vm_area_struct *find_vma_prev(struct mm_struct *mm, unsigned long addr,struct vm_area_struct **pprev)
static inline struct vm_area_struct * find_vma_intersection(struct mm_struct * mm,unsigned long start_addr, unsigned long end_addr)

find_vma()函数根据给定地址addr查找满足如下条件之一的VMA,如图7.21所示。addr在VMA空间范围内,即 vma->vm_start <= addr < vma->vm_end。距离addr最近,并且VMA的结束地址大于addr的一个VMA。(说明了什么?)

2.插入VMA

insert_vm_struct()是内核提供的插入VMA的核心API函数。

int insert_vm_struct(struct mm_struct *mm, struct vm_area_struct *vma)

insert_vm_struct()函数向VMA链表和红黑树插入一个新的VMA。参数mm是进程的内存描述符,vma是要插入的线性区VMA。(这个链表和红黑树的关系是什么?)

3.合并VMA

在新的 VMA 被加入进程的地址空间时,内核会检查它是否可以与一个或多个现存的VMA进行合并。vma_merge()函数实现将一个新的VMA和附近的VMA合并。变成一个更大的连续的空间。

struct vm_area_struct *vma_merge(struct mm_struct *mm,struct vm_area_struct *prev, unsigned long addr,unsigned long end, unsigned long vm_flags,struct anon_vma *anon_vma, struct file *file,pgoff_t pgoff, struct mempolicy *policy)

vma_merge()函数参数多达9个。其中,mm是相关进程的struct mm_struct数据结构。prev是紧接着新VMA前继节点的VMA,一般通过find_vma_links()函数来获取。add和end是新VMA的起始地址和结束地址。vm_flags是新VMA的标志位。如果新VMA属于一个文件映射,则参数file指向该文件struct file数据结构。参数proff指定文件映射偏移量。参数anon_vma是匿名映射的struct anon_vma数据结构。

4、malloc背后的男人

malloc()函数是C语言中的内存分配函数。假设系统中有进程A和进程B,分别使用testA和testB函数分配内存。

void testA(void){
char * bufA = malloc(100);
*bufA = 100;
}
//进程B分配内存
void testB(void){
char * bufB = malloc(100);
mlock(bufB, 100);
}

其实在内存这方面的函数,经常会问到的就是这个是否会立即分配内存?这个分配的内存在哪里呢?

假设不考虑libc的因素,malloc分配100字节,那么实际上内核是为其分配100字节吗?(那不然呢?骗纸?)

假设使用printf打印指针bufA和bufB指向的地址是一样的,那么在内核中这两块虚拟内存是否“打架”了呢?(这打印的是虚拟地址还是物理地址诶?)

malloc()函数是C语言标准库里封装的一个核心函数。C标准库做一些处理后调用Linux内核系统去调用brk。也许读者并不太熟悉brk的系统调用,原因在于很少有人会直接使用系统调用 brk 向系统申请内存,而总是通过 malloc()之类的 C 标准库的 API 函数。如果把malloc()想象成零售,那么 brk 就是代理商。malloc 函数的实现为用户进程维护一个本地小仓库,当进程需要使用更多的内存时就向这个小仓库“要货”,小仓库存量不足时就通过代理商brk向内核“批发”。

(我不太认可这里的比喻,其实我们分配内存的申请一发生,我的映像是立马就深入到系统调用了,这里零售说明,我能屯点货,但是malloc最多就是个中间人,把你要给我的东西通过他转交给我,而没有这个缓存的属性。我觉得中间人更合适,但是这是因为我对malloc的认识很浅薄,继续保持疑问往下看。)

brk系统调用定义如下。

SYSCALL_DEFINE1(brk, unsigned long, brk)

在32位Linux内核中,每个用户进程拥有3GB的虚拟空间。内核如何为用户空间划分这3GB的虚拟空间呢?

3GB的用户空间怎么划分?

用户进程的可执行文件由代码段和数据段组成,数据段包括所有的静态分配的数据空间,例如全局变量和静态局部变量等。这些空间在可执行文件装载时,内核就为其分配好这些空间,包括虚拟地址和物理页面,并建立好二者的映射关系。

如图7.22所示,用户进程的用户栈从3GB虚拟空间的顶部开始,由顶向下延伸,而brk分配的空间是从数据段的顶部end_data到用户栈的底部。动态分配空间是从进程的end_data开始,每次分配一块空间,就把这个边界往上推进一段,同时内核和进程都会记录当前的边界的位置。

malloc函数其实是为用户空间分配进程地址空间的,用内核术语就是分配一块VMA,相当于一个空的纸箱子。因为VMA还没和具体物理地址挂钩呢。相当于一个空的纸箱子。

那什么时候才往纸箱子里装东西呢?

一是到了真正使用箱子时才往里面装东西;(声明未初始化)

二是分配箱子时就装了你想要的东西。(声明初始化)

进程A中的testA函数就是第一种情况。当使用这段内存时,CPU去查询页表,发现页表为空,CPU触发缺页异常,然后在缺页异常里一页一页地分配内存,需要一页给一页。(这个缺页异常据说很精彩)

进程B里面的testB函数是第二种情况,直接分配已装满的纸箱子,你要的虚拟内存都已经分配了物理内存并建立了页表映射。

假设不考虑C语言标准库的因素,malloc分配100字节,那么内核会分配多少字节呢?处理器的MMU硬件单元处理最小单元是页,所以内核分配内存、建立虚拟地址和物理地址映射关系都以页为单位,PAGE_ALIGN(addr)宏让地址addr按页面大小对齐。

那就是1024字节。

还有个问题就是malloc相同的虚拟地址会打架吗?

其实每个用户进程有自己的一份页表,mm_struct 数据结构中有一个pgd成员指向这个页表的基地址,在用fork()函数创建新进程时会初始化一份页表。每个进程有一个mm_struct数据结构,包含一个属于进程自己的页表、一个管理VMA的红黑树和链表。进程本身的VMA会挂入属于自己的红黑树和链表,所以即使进程A和进程B使用malloc分配内存返回的相同的虚拟地址,它们也是两个不同的VMA,分别被不同的两套页表来管理。

简言之:数据结构里有页表,页表基地址不同,还有各种不同,进程有很多自己独特的数据结构,因此最后的物理地址是不一样的。

所以即使进程A和进程B使用malloc分配内存返回的相同的虚拟地址,它们也是两个不同的VMA,分别被不同的两套页表来管理。

(得写写libc这个东西的作用)

6、mmap背后故事

mmap/munmap 接口函数是用户空间最常用的两个系统调用接口,无论是在用户程序中分配内存、读写大文件、链接动态库文件,还是多进程间共享内存,都可以看到mmap/munmap的身影。mmap/munmap函数声明如下。

#include <sys/mman.h>void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
int munmap(void *addr, size_t length);

最后根据文件关联性和映射区域是否共享等属性,mmap又可以分成如下4种情况,如表7.7所示。

1.私有匿名映射

当参数fd=−1且flags= MAP_ANONYMOUS | MAP_PRIVATE时,创建的mmap映射是私有匿名映射。私有匿名映射最常见的用途是在glibc分配大块的内存中,当需要分配的内存大于MMAP_THREASHOLD(128KB)时,glibc会默认使用mmap代替brk来分配内存。

2.共享匿名映射

当参数fd=−1且flags= MAP_ANONYMOUS | MAP_SHARED时,创建的mmap映射是共享匿名映射。共享匿名映射让相关进程共享一块内存区域,通常用于父子进程之间通信。

创建共享匿名映射有如下两种方式。

1)fd=−1 且 flags= MAP_ANONYMOUS | MAP_SHARED。在这种情况下,do_mmap_pgoff()->mmap_region()函数最终会调用shmem_zero_setup()打开一个“/dev/zero”特殊的设备文件。

2)直接打开“/dev/zero”设备文件,然后使用这个文件句柄来创建mmap。

上述两种方式最终都会调用到shmem模块来创建共享匿名映射。

3.私有文件映射

创建文件映射时,flags的标志位被设置为MAP_PRIVATE,此时就会创建私有文件映射。私有文件映射最常用的场景是加载动态共享库。

4.共享文件映射

创建文件映射时,flags的标志位被设置为MAP_SHARED,此时就会创建共享文件映射。如果prot参数指定了PROT_WRITE,那么打开文件时需要指定O_RDWR标志位。共享文件映射通常有如下两个场景。

1)读写文件。把文件内容映射到进程地址空间,同时对映射的内容做修改,内核的回写机制最终会把修改的内容同步到磁盘中。

2)进程间通信。进程之间的进程地址空间相互隔离,一个进程不能访问到另一个进程的地址空间。如果多个进程都同时映射到一个相同文件时,就实现了多进程间的共享内存通信。如果一个进程对映射内容做了修改,那么另一个进程是可以看到的。

mmap机制在Linux内核中实现的代码框架和brk机制非常类似,mmap机制如图7.24所示,其中有很多关于VMA的操作。另外,mmap机制和缺页异常机制结合在一起会变得复杂很多。

小结

这一章讲了一下虚拟内存,虚拟内存主要从进程的角度去将虚拟内存,因为虚拟内存的目的和出发点当时就是为了解决进程的地址问题。然后说了一下这里面涉及到的关键的几个数据结构。 mm_struct、vm_area_struct。然后讲了vma的管理(查找、插入、合并),搞清楚这些操作的用处在哪里很重要。我觉得vma这个部分的算法数据结构思想还是蛮不错的,需要的话可以研究,在数据库里也涉及到了红黑树,可见内核的算法和思想,对于软件的设计架构还是通用的。

最后介绍了malloc、mmap。这两个重要的函数的流程与功能。这里我也浅浅的粗略瞟了一眼,等后面需要了再来详细读。

待写

vma

libc

malloc

mmap

参考资料:

《奔跑吧 Linux内核》

目录
相关文章
|
3天前
|
算法 安全 Linux
探索Linux内核的虚拟内存管理
【5月更文挑战第20天】 在本文中,我们将深入探讨Linux操作系统的核心组成部分之一——虚拟内存管理。通过剖析其关键组件和运作机制,揭示虚拟内存如何提供高效的内存抽象,支持庞大的地址空间,以及实现内存保护和共享。文章将重点讨论分页机制、虚拟内存区域(VMAs)的管理、页面置换算法,并简要分析这些技术是如何支撑起现代操作系统复杂而多变的内存需求的。
|
1天前
|
消息中间件 存储 安全
【Linux 系统】进程间通信(共享内存、消息队列、信号量)(下)
【Linux 系统】进程间通信(共享内存、消息队列、信号量)(下)
|
1天前
|
消息中间件 算法 Linux
【Linux 系统】进程间通信(共享内存、消息队列、信号量)(上)
【Linux 系统】进程间通信(共享内存、消息队列、信号量)(上)
|
3天前
|
程序员 编译器 C语言
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free(下)
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free
8 0
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free(下)
|
3天前
|
C语言 C++
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free(中)
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free
9 0
|
3天前
|
编译器 数据库 C语言
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free(上)
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free
12 0
C语言进阶⑰(动态内存管理)四个动态内存函数+动态通讯录+柔性数组_malloc+free(上)
|
5天前
|
算法 Java Python
【Python 的内存管理机制专栏】Python 内存管理实战:性能优化与内存泄漏检测
【5月更文挑战第18天】Python内存管理关乎程序性能与稳定性。优化包括避免过多临时对象,如优化列表推导式减少对象创建。警惕循环引用造成的内存泄漏,如示例中的Node类。使用`gc`模块检测泄漏,通过`gc.set_debug(gc.DEBUG_LEAK)`和`gc.collect()`获取信息。实践中需持续分析内存使用,优化算法、数据结构和资源释放,以提升程序质量与效率。
【Python 的内存管理机制专栏】Python 内存管理实战:性能优化与内存泄漏检测
|
5天前
|
存储 Java 程序员
【Python 的内存管理机制专栏】深入解析 Python 的内存管理机制:从变量到垃圾回收
【5月更文挑战第18天】Python内存管理关乎程序性能与稳定性,包括变量存储和垃圾回收。变量存储时,如`x = 10`,`x`指向内存中值的引用。垃圾回收通过引用计数自动回收无引用对象,防止内存泄漏。了解此机制可优化内存使用,避免循环引用等问题,提升程序效率和稳定性。深入学习内存管理对成为优秀Python程序员至关重要。
【Python 的内存管理机制专栏】深入解析 Python 的内存管理机制:从变量到垃圾回收
|
8天前
|
存储 Linux 程序员
【操作系统原理】—— Linux内存管理
【操作系统原理】—— Linux内存管理