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

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

相关推荐
Hello.Reader2 分钟前
Ubuntu 上正确安装 Kali 虚拟机、Docker 与 kail 工具指南
linux·ubuntu·docker
WarPigs6 分钟前
Windows IIS开启和配置服务器
运维·服务器
IT猿手6 分钟前
SCI一区:章鱼优化算法(Octopus Optimization Algorithm, OOA)求解23个测试函数,出图丰富,提供完整MATLAB代码
开发语言·算法·matlab
superior tigre7 分钟前
739 每日温度
算法·leetcode·职场和发展
原来是猿9 分钟前
Linux UDP Socket 编程入门:Echo Server/Client实现
linux·运维·udp
忡黑梨11 分钟前
eNSP_从直连到BGP全网互通
c语言·网络·数据结构·python·算法·网络安全
中微子12 分钟前
突然爆火的Warp 终端,开源1天破 4w Stars
linux·人工智能·开源
Run_Teenage26 分钟前
算法:离散化模板
算法
乐迪信息27 分钟前
乐迪信息:实时预警,秒级响应:船舶AI异常行为检测算法
大数据·人工智能·算法·安全·目标跟踪
6Hzlia29 分钟前
【Hot 100 刷题计划】 LeetCode 15. 三数之和 | C++ 排序+双指针
c++·算法·leetcode