【Linux】进程虚拟地址空间详解
目录
一、引出虚拟地址空间
二、什么是虚拟地址空间
三、虚拟空间划分
3.1 如何理解进程是独立的
3.2 为什么全局变量全局有效
3.3 为什么字符串常量只读
四、为什么要虚拟地址空间
(1)安全风险
(2)无序变有序
(3)解耦合
正文:
一、引出虚拟地址空间
结论:我们历史所学的所有地址都是虚拟地址,我们是看不到真正的物理地址的。
在学习的过程中,大家在课堂上或网上可能经常看到以下内存的划分图:
进程具有独立性,所以进程在内存中的物理地址空间应该也具备独立性。
那么下面通过一个例子证明是否如我们所想这样:创建一个test.c文件把下面代码打进去再运行
查看结果发现gval已经从100加到101了,变量内容不⼀样,所以父子进程输出的变量绝对不是同⼀个变量,但地址值是⼀样的,说明,该地址绝对不是物理地址!
二、什么是虚拟地址空间
结论:通过页表映射到物理内存的位置进行访问。
多个可执行程序加载到内存,操作系统通过先描述后组织的手段管理进程,系统会自动生成描述该进程的结构体(PCB)、虚拟地址空间表和页表。
进程在访问内存时要先进行虚拟地址到物理地址的页表映射,找到物理内存才能访问数据。
三、虚拟空间划分
结论:虚拟地址空间的区域划分本质上是通过 vm_area_struct(VMA)来实现的
虚拟地址空间是操作系统为每个进程抽象出来的一种内存视图,它让每个进程都“认为”自己独占整个内存资源。有多个进程就有多个虚拟空间,这时操作系统就要通过先描述再组织的方法管理起来这些虚拟空间。
而我们所谈到的虚拟地址空间其实是一个内核数据结构(struct mm_struct)
虚拟地址空间的管理是通过 struct mm_struct这个关键数据结构实现的。每个进程都有一个独立的 struct mm_struct,它描述了该进程的整个虚拟内存布局,包括代码段、数据段、堆、栈、内存映射区域等。如下图所示:
struct mm_struc是一个内核数据结构,是一段线性空间,也就可以通过一段开始和结束地址表明一段范围即可。开始到结束之间的内容都可以被使用。
在 Linux 内核中,struct mm_struc 并不直接“划分”虚拟地址空间,而是通过管理 虚拟内存区域(vm_area_struct) 来描述进程地址空间的布局。每个 VMA(vm_area_struct)代表一段连续的虚拟内存范围,并记录该区域的权限、映射方式等属性。
所以虚拟地址空间的区域划分本质上是通过 vm_area_struct(VMA)来实现的。mm_struc 通过链表和红黑树组织这些 VMAs,从而实现对虚拟地址空间的动态管理。如下图所示:
可以理解为:虚拟地址空间就是操作系统给进程画的一张饼。
比如一个大富翁有一个亿,同时也有6个私生子,它对每个私生子都说未来一个亿都是给他的,而私生子之间并不知道其他私生子的存在,都认为未来会继承一个亿。
同理,操作系统为每个进程分配一个独立的虚拟地址空间(比如 32 位系统是 4GB),让进程“以为”自己独占整个内存。
只有进程真正访问这块内存时(触发缺页异常),操作系统才会分配物理内存,相当于“先答应给你,等你要用的时候再兑现”。
虚拟地址空间分为:用户空间VS内核空间
用户空间:进程可以直接通过虚拟地址访问用户态内存(如代码、堆、栈)
内核空间:必须通过系统调用(如 read、write)使用
3.1 如何理解进程是独立的
进程=内核数据结构(PCB、虚拟地址空间、页表)+进程的代码和数据
可执行程序加载到内存会自动创建PCB、虚拟地址空间、页表。父进程fork()创建子进程也会把PCB、虚拟地址空间、页表给子进程拷一份,这样父子都有独立的内核数据结构,子进程是继承父进程的,所以进程的代码和数据都指向父进程的(共用)。但是代码是只读的,数据也是只读的,如果子进程要对数据进行修改会进行写实拷贝(数据层面的分离)也就和父进程不再是同一个数据。内核数据结构、进程的代码和数据都不同所以进程是独立的,子进程销毁不影响父进程。
3.2 为什么全局变量全局有效
全局变量是在全局数据区的,地址空间只要存在,那么全局数据区就要存在,所以全局变量会一直存在,包括static静态变量。
3.3 为什么字符串常量只读
字符串常量是和代码编译在一起的,都是只读(因为代码就是只读)
而当你想修改常量值(w),必须用虚拟地址通过页表映射找到物理地址才能修改,在页表中会有专门权限限制你要访问的内容是可读还是可写(r/w)字符串常量区被页表映射时不让写入操作。
我们在定义字符串时前面会加const,const是约束编译器,让编译器进行写入检查,检查到就报错!
四、为什么要虚拟地址空间
(1)安全风险
有了虚拟地址就必须转换为物理地址才能访问。虚拟到物理之间的转换会进行安全审核,变相保护了物理内存安全,维护了独立性。
如果没有虚拟地址空间,用户直接在物理内存中用指针指来指去会很乱,可能改到其他数据造成不必要麻烦,缺乏独立性,进程之间反而会相互影响。
(2)无序变有序
物理内存的实际使用情况通常是杂乱无序!!!
我们可执行程序加载到内存,为它分配的物理内存可以是任意位置,但是虚拟地址空间分配是固定顺序的(哪一块是栈区,哪一块是堆区....已经标好)所以进程看自己代码全是有序的,不管你物理内存中怎么乱,都可以通过虚拟地址页表映射找到指定数据。
(3)解耦合
对进程管理和内存管理解耦合!使其在内存管理中增加空间不影响进程管理部分。
我们创建一个进程,要对其先描述后组织,那么一定要把进程的代码和数据立即加载进来吗?
不一定,可以一边执行一边加载!执行到某行代码发现内存空间内没有就开空间把数据加载进来。这种加载称为惰性加载,提高了内存使用率。上面提到的写实拷贝也是一种惰性申请,我们要改变数据了再开空间、拷数据然后建立映射关系。
完,期待下次的一起学习~