【Linux】程序地址空间(是什么?为什么?)

一、程序地址空间回顾

我们在学习C语言时,都接触过这样的空间布局图

我们对其不是特别理解,我们可以先对其各区域进行分布验证:

cpp 复制代码
#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);
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("test static addr: %p\n", &test); //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;
}
cpp 复制代码
code addr: 0x400416
init global addr: 0x403020
uninit global addr: 0x40302c
heap addr: 0x171e96b0
heap addr: 0x171e96d0
heap addr: 0x171e96f0
heap addr: 0x171e9710
test static addr: 0x403024
stack addr: 0x7ffc76d2b388
stack addr: 0x7ffc76d2b380
stack addr: 0x7ffc76d2b378
stack addr: 0x7ffc76d2b370
read only string addr: 0x40185c
argv[0]: 0x7ffc76d2d580
env[0]: 0x7ffc76d2d587
env[1]: 0x7ffc76d2d597
env[2]: 0x7ffc76d2d5ae
env[3]: 0x7ffc76d2d5ca
env[4]: 0x7ffc76d2d5d8
env[5]: 0x7ffc76d2d5ef
env[6]: 0x7ffc76d2d5fb
env[7]: 0x7ffc76d2d610
env[8]: 0x7ffc76d2d61f
env[9]: 0x7ffc76d2d62e
env[10]: 0x7ffc76d2d63f
env[11]: 0x7ffc76d2dd58
env[12]: 0x7ffc76d2dd87
env[13]: 0x7ffc76d2dd9e
env[14]: 0x7ffc76d2dda9
env[15]: 0x7ffc76d2ddcc
env[16]: 0x7ffc76d2ddd5
env[17]: 0x7ffc76d2ddfd
env[18]: 0x7ffc76d2de05
env[19]: 0x7ffc76d2de1a
env[20]: 0x7ffc76d2de36
env[21]: 0x7ffc76d2de58
env[22]: 0x7ffc76d2de71
env[23]: 0x7ffc76d2dee6
env[24]: 0x7ffc76d2df19
env[25]: 0x7ffc76d2df33
env[26]: 0x7ffc76d2df46
env[27]: 0x7ffc76d2df57
env[28]: 0x7ffc76d2dfe8

二、虚拟地址

我们之前在进程概念 的学习中创建过子进程,那我们刚好可以观察一下当子进程修改某一共享变量时,父子进程读取到的该变量的值是否会发生改变,该变量的地址又呈现出什么样的内容?

cpp 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
    pid_t id = fork();
    if(id < 0)
    {
    perror("fork");
    return 0;
    }
    else if(id == 0)
    {
     //child
    printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }
    else{ //parent
    printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
    }
    sleep(1);
    return 0;
}

我们发现,输出出来的变量值和地址是⼀模⼀样的,很好理解呀,因为⼦进程按照⽗进程为模版,⽗ ⼦并没有对变量进⾏进⾏任何修改。可是将代码稍加改动:

cpp 复制代码
int main()
{
  //子进程修改value值的情况
          pid_t id=fork();
          if(id<0)
          {
                  perror("fork()");
          }
          else if(id==0)
          {
                  g_val=100;
                  printf("child[%d]:%d:%p\n",getpid(),g_val,&g_val);      
          }
          else
          {
                  printf("parent[%d]:%d:%p\n",getpid(),g_val,&g_val);
          }
  
  
          return 0;
  }

我们发现,⽗⼦进程,输出地址是⼀致的,但是变量内容不⼀样!

变量内容不一样,说明父子进程输出的变量一定不是同一个变量 ,但地址是一样的,说明此地址一定不是物理地址,在Linux地址下,这种地址是虚拟地址

我们在⽤C/C++语⾔所看到的地址,全部都是虚拟地址 !物理地址,⽤⼾⼀概看不到,由OS统⼀ 管理,OS必须负责将 虚拟地址转化成 物理地址

三、进程地址空间

所以我们意识到开头最先看的图片不是物理内存分布图,而是进程空间分布图。

3.1分页、虚拟地址空间

这个图足矣说明问题,同一个变量,地址相同,其实都是虚拟地址相同,内容不同其实是因为被映射到不同的物理地址。

虚拟地址空间是个结构体变量!!页表承担着虚拟地址和物理地址映射的作用!

3.2写时拷贝

这里还有个写时拷贝的现象,子进程继承了父进程的g_val变量,如果子进程不修改这个变量值,那么它们共享这个变量,g_val在同一个物理位置,如果子进程要改变变量的值,这时会在物理内存上拷贝一份g_val变量供子进程使用,这就是写时拷贝现象!

通常,⽗⼦代码共享,⽗⼦再不写⼊时,数据也是共享的,当任意⼀⽅试图写⼊,便以写时拷⻉的方式各⾃⼀份副本。如图:

因为有写时拷贝技术的存在,所以父子进程得以彻底分离离!完成了进程独立性的技术保证!​ 写时拷贝,是一种延时申请技术,可以提高整机内存的使用率。

四、虚拟内存管理

linux下进程的地址空间的所有信息的结构体是mm_struct(内存描述符),每个进程只有一个

mm_struct,在每个进程的task_struct结构中,有一个指向mm_struct的结构体指针。

倘若我们遇到堆区的动态开辟空间呢?可能一个程序在堆区上会malloc很多次,地址不同,区域不同,会被分成很多份,这时候一个start和end指针能管理过来吗?

其实也无需担心,linux内核会使用vm_area_struct结构来表示一个独立的虚拟内存区域(VMA),再采用单链表或者红黑树的方式(vm_area_struct结构)来连接各个VMA,方便进程快速访问。

五、为什么要有虚拟地址空间呢?

1.要保护物理内存,如果每个进程都可以访问任意的内存空间,这也就意味着任意进程都可去读写系统相关内存区域,这是高危的行为!

2.因为有地址空间的存在和页表的映射的存在,我们的物理内存中可以对未来的数据进行任意位置的加载,物理内存的分配和进程的管理就没有关系了,进程管理模块内存管理模块就完成了解耦合

注:所以我们之前在new和malloc空间时,其实是在虚拟地址空间上申请的,物理内存可以甚至一个字节都不给你,而当你真正需要对物理地址空间进行访问时,才执行相关的管理算法来帮你申请内存,构建页表映射关系,做到延时分配,而这个操作是由操作系统自动完成的,用户0感知!

缺页中断时,其实就是在动态内存分配

3.因为有页表的映射存在,程序在物理内存中理论上可以在任意位置加载,它可以将地址空间上的虚拟地址和物理地址进行映射,在进程视角所有的内存分布都可以是有序的

这里再补充一个进程挂起的小知识,进程挂起其实就是找到页表,把页表右侧的物理地址清除,然后把代码+数据换出到磁盘的指定区域。

相关推荐
橙子也要努力变强2 小时前
Linux I/O 缓冲区、inode、软硬链接与磁盘结构全解析
linux·c++·操作系统
setmoon2142 小时前
C++与量子计算模拟
开发语言·c++·算法
异步的告白2 小时前
嵌入式Linux学习-默认规则
linux
实心儿儿2 小时前
算法6:相交链表
数据结构·算法·链表
nglff2 小时前
蓝桥杯抱佛脚第二天|简单枚举,前缀和
算法·职场和发展·蓝桥杯
2301_793804692 小时前
C++安全编程指南
开发语言·c++·算法
新缸中之脑2 小时前
cmux多智能体管理工具
运维·服务器
m0_518019482 小时前
分布式系统安全通信
开发语言·c++·算法
linxinglu2 小时前
DeepMind:解开智能之谜与「科学发现」的终极自动化杠杆
运维·人工智能·自动化