虚拟地址空间

虚拟地址空间

一、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 交换分区 机制,操作系统可以把暂时闲置、暂时不用的进程数据,从物理内存转移到硬盘当中。当进程需要使用时再调回物理内存。虚拟地址空间的大小可以远大于实际物理内存大小,让系统能够同时运行远超物理内存承载能力的多个大型程序,解决物理内存不足无法运行大程序的痛点。

相关推荐
Ha_To2 小时前
26.5.19 未授权漏洞
linux·服务器·网络
ZGUIZ2 小时前
Ubuntu 25.10 蓝牙Wifi不可用解决流程
linux·运维·ubuntu
rising start3 小时前
Linux入门及相关命令
linux·运维·服务器
kyle~3 小时前
机器人感知 --- 多相机传感时间误差分析
linux·c++·数码相机·机器人·ros2·传感器
minji...3 小时前
Linux 网络基础之传输层协议TCP(九)从内核源码的角度打通系统与网络之间的关系,套接字多态的体现
linux·运维·服务器·网络·网络协议·tcp/ip·http
corpse20103 小时前
CentOS Linux release 8.5.2111下的CVE-2026-31431 Linux内核提权漏洞处置
linux·运维·centos
楼兰公子3 小时前
《深入理解Linux网络技术内幕》全套学习资料合集
linux·网络·应用·驱动
想唱rap3 小时前
IO多路转接之epoll
linux·运维·服务器·数据库·网络协议·算法·http
舰长1153 小时前
polkit服务没起来,导致防火墙命令卡住
linux·运维·服务器