虚拟地址空间
一、C语言中的空间开辟
在之前学C语言时,总会有一张这样的空间布局图

但是它是否是在内存中呢?接下来会逐步解答。有以下测试代码
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_unval;
int g_val = 100;
int main(int argc, char *argv[], char *env[]) {
// const char *str = "helloworld";
printf("code addr: %p\n", main);
printf("init global addr: %p\n", &g_val);
printf("uninit global addr: %p\n", &g_unval);
// printf("test static addr: %p\n", &test); //heap_mem(0), &heap_mem(1)
// static int test = 10;
char *heap_mem = (char*)malloc(10);
// char *heap_mem1 = (char*)malloc(10);
// char *heap_mem2 = (char*)malloc(10);
// char *heap_mem3 = (char*)malloc(10);
printf("heap addr: %p\n", heap_mem); //heap_mem(0), &heap_mem(1)
// printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1)
// printf("heap addr: %p\n", heap_mem2); //heap_mem(0), &heap_mem(1)
// printf("heap addr: %p\n", heap_mem3); //heap_mem(0), &heap_mem(1)
printf("stack addr: %p\n", &heap_mem); //heap_mem(0), &heap_mem(1)
// printf("stack addr: %p\n", &heap_mem1); //heap_mem(0), &heap_mem(1)
// printf("stack addr: %p\n", &heap_mem2); //heap_mem(0), &heap_mem(1)
// printf("stack addr: %p\n", &heap_mem3); //heap_mem(0), &heap_mem(1)
// printf("read only string addr: %p\n", str);
// for(int i = 0 ;i < argc; i++) {
// printf("argv[%d]: %p\n", i, argv[i]);
// }
// for(int i = 0; env[i]; i++) {
// printf("env[%d]: %p\n", i, env[i]);
// }
return 0;
}

对于栈的空间开辟比较特别 ,其中整体是按从高到低 的规则开辟,但是其中的任何变量都还是遵循变量地址都是最小地址 ,也就是个体变量的地址还是从低到高。
堆、栈相对而生
二、虚拟
刚刚描述的内存空间,并不属于C语言的范畴,而是操作系统中的概念。它不是内存,而是虚拟(程序)地址空间
1)父子进程数据变化测试
我们知道,父子进程的数据是共享的,只要不对数据做出改变,父子进程共用一套数据。
测试代码:
c
#include<stdio.h>
#include<unistd.h>
int g_val = 100;
int main() {
printf("g_val: %d, &g_val: %p\n", g_val, &g_val);
pid_t id = fork();
if (id == 0) {
while (1) {
printf("我是子进程,pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
}
}
else {
while (1) {
printf("我是父进程,pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
}
}
return 0;
}

当我们在子进程中将数据修改会怎么样?我们在子进程中修改++g_val如下测试:

由于进程具有独立性父子进程的g_val不同,但是我们发现!父子进程的g_val的地址竟然一样!不合理呀!
我们推理:这个地址绝对不会是物理地址,回想指针,说明指针也不就是物理地址。
那么我们称它叫:虚拟地址。
2)父子进程独立性原理
一个程序运行,从磁盘中加载数据到内存中,OS会将程序转化成进程,生成进程两件套:task_struct+代码数据;
此时代码数据会存在一个虚拟地址空间 中,这个空间会通过一个页表,把数据地址对应到物理内存上。

那我们再思考父子进程的行动流程:
子进程创建时,要以父进程为模板,包括task_struct+代码数据;也就是说,父子进程各自都有一套虚拟地址空间 和页表!
之所以说fork之后父子进程共享 代码数据,是因为子进程会拷贝父进程的页表,类似于**"浅拷贝"**!
那么我们又想到,进程之间具有独立性,那是怎么让它具有独立性的呢?
程序加载到内存,只有代码 和数据
- 代码是只读的,那么父子进程就不会相互影响;
- 但是程序运行时,大概率会对数据进行操作,导致进程不独立!
所以OS规定:父子进程任何一个,尝试对共享的数据进行修改时,要发生写实拷贝。
- 写实拷贝 :在内存中再开辟一块空间,存放新的修改数据,将子进程的页表对应的物理内存地址修改。
此时,两个进程的g_val的虚拟地址还是相同,但是映射到的物理地址不同了。(物理地址被操作系统隐藏,是不可见的;)

3)什么是虚拟地址空间?
想象成系统给进程画的饼 ,有多少个进程就有多少张饼,那么这个"饼"也需要被管理,所以内核中定义了:struct mm_struct(memory management);
这是我们继struct task_struct认识到的第二个内核结构体。
4)虚拟地址空间的划分原理
c
struct mm_struct
{
/*...*/
struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */
struct rb_root mm_rb; /* red_black树 */
unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的⼤⼩*/
/*...*/
// 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
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;
/*...*/
}
发现空间指向的类型怎么不是指针?其实我们用到的地址本质就是一串数字,这里就直接用long类型了;
当虚拟地址空间中的不同区域大小变化时,底层就是对应区域的start和end变化;

5)什么是页表?
页表除了有对应的虚拟-物理映射关系,还有标志位;以后还有更多的页表内容需要学习。

标志位中的权限,解释了为什么常量字符串和代码为什么是只读的;本质是不让进程写入。
- 那为什么全局变量、
static变量生命周期是全局的?
首先,当一个变量被static修饰之后,它就会被放进初始化全局数据 和未初始化全局数据之间,成为一个同全局变量一样的全局数据;那全局数据为什么生命周期为全局?
因为只要进程进行,虚拟地址空间就开辟,其中的全局数据就一直存在,所以本质就是全局数据的生命周期随进程!
三、虚拟地址空间存在的逻辑与意义
(1)精细化内存权限管理,筑牢安全防护
操作系统在建立地址映射页表时,可以为不同虚拟内存区域设置不同访问权限 ,比如代码段只读不可写、数据段可读写、内核区域禁止用户进程访问、栈区域限制访问范围等。能够有效阻止程序非法修改程序代码、越界读写内存、恶意入侵内核等违规操作,防范内存漏洞、恶意程序攻击。
(2)屏蔽物理内存差异,简化程序开发与移植
程序员编写代码、编译程序时,只需要使用统一规范的虚拟地址 ,完全不需要关心当前电脑物理内存大小、内存空闲位置、硬件内存布局等底层硬件细节。不管程序运行在什么配置的电脑上,进程的地址布局完全一致,程序不用针对不同硬件修改内存相关逻辑,极大降低开发难度,也让程序具备良好的跨平台、跨设备移植性。
(3)高效实现内存共享,节省系统物理资源
系统中大量进程会使用相同的系统库、动态链接库、公共程序代码。利用虚拟地址映射机制,多个进程的虚拟地址可以映射到同一块物理内存 ,只在物理内存中留存一份公共代码与数据,不用重复拷贝占用内存,极大节省物理内存空间,提升整机内存使用效率。
(4)实现进程地址隔离,保障系统稳定
操作系统为每一个运行的进程,都分配独立、完整、互不重叠的虚拟地址空间。每个进程都认为自己独占整块内存,无法直接访问其他进程的虚拟地址,也不能随意访问操作系统内核地址。一旦某个进程出现内存错误、崩溃、恶意篡改行为,只会影响自身运行,不会破坏其他进程与整个操作系统,从底层实现进程隔离,大幅提升系统稳定性与安全性。
(5)突破物理内存容量限制,实现内存扩容
统稳定
操作系统为每一个运行的进程,都分配独立、完整、互不重叠的虚拟地址空间。每个进程都认为自己独占整块内存,无法直接访问其他进程的虚拟地址,也不能随意访问操作系统内核地址。一旦某个进程出现内存错误、崩溃、恶意篡改行为,只会影响自身运行,不会破坏其他进程与整个操作系统,从底层实现进程隔离,大幅提升系统稳定性与安全性。
(5)突破物理内存容量限制,实现内存扩容
借助磁盘 swap 交换分区 机制,操作系统可以把暂时闲置、暂时不用的进程数据,从物理内存转移到硬盘当中。当进程需要使用时再调回物理内存。虚拟地址空间的大小可以远大于实际物理内存大小,让系统能够同时运行远超物理内存承载能力的多个大型程序,解决物理内存不足无法运行大程序的痛点。