[Linux](8)进程地址空间

本文介绍了C语言中地址空间的概念,包括代码区、全局变量、堆和栈的分布。通过示例展示了堆栈地址的变化规律,以及父子进程间全局变量的共享和写时拷贝原理。同时探讨了虚拟地址的重要性,它为每个进程提供了独立的内存视图,保护内存并简化内存管理。
摘要由CSDN通过智能技术生成

验证地址空间

学C语言时我们应该见过这样的图:

进程地址空间

如何理解它呢?

根据这张图,在 Linux 下我们可以按由低到高的顺序,把各个分区的变量的地址打印出来:

#include <stdio.h>                
#include <stdlib.h>               
                                  
int un_g_val;                     
int g_val = 100;                  
                                  
int main(int argc, char* argv[], char* env[])  
{                                 
    printf("code addr:          %p\n", main);
    printf("init global addr:   %p\n", &g_val);
    printf("uninit global addr: %p\n", &un_g_val);
    char* m1 = (char*)malloc(100);
    printf("heap addr:          %p\n", m1);
    printf("stack addr:         %p\n", &m1);
    for (int i = 0; i < argc; ++i)
    {
        printf("argv addr:          %p\n", argv[i]);
    }
    for (int i = 0; env[i]; ++i)
    {
        printf("env addr:           %p\n", env[i]);
    }
    return 0;
} 

结果:

[CegghnnoR@VM-4-13-centos 2022_8_15]$ ./mytest
code addr:          0x40057d
init global addr:   0x60103c
uninit global addr: 0x601044
heap addr:          0xd50010
stack addr:         0x7ffe2043a400
argv addr:          0x7ffe2043a76a
env addr:           0x7ffe2043a773
env addr:           0x7ffe2043a789
env addr:           0x7ffe2043a7a1
#以下均为环境变量地址:略。。。

👆地址确实是依次增大的。并且在堆和栈之间出现了一个非常大的断层。

堆区向上增长,栈区向下增长

不断申请堆区空间

char* m1 = (char*)malloc(100);
char* m2 = (char*)malloc(100);
char* m3 = (char*)malloc(100);
char* m4 = (char*)malloc(100);
printf("heap addr1:          %p\n", m1);
printf("heap addr2:          %p\n", m2);
printf("heap addr3:          %p\n", m3);
printf("heap addr4:          %p\n", m4);
printf("stack addr1:         %p\n", &m1);
printf("stack addr2:         %p\n", &m2);
printf("stack addr3:         %p\n", &m3);
printf("stack addr4:         %p\n", &m4);

堆区空间的地址是逐渐增大的:

heap addr1:          0x215c010
heap addr2:          0x215c080
heap addr3:          0x215c0f0
heap addr4:          0x215c160

栈区变量的地址是逐渐减小的:

stack addr1:         0x7ffeecce37a0
stack addr2:         0x7ffeecce3798
stack addr3:         0x7ffeecce3790
stack addr4:         0x7ffeecce3788

堆栈相对而生,我们在C函数中定义的变量,通常在栈上保存,那么先定义的一定是地址比较高的。


函数内定义 static 变量,本质是编译器会把该变量编译进全局数据区。

static int a = 3;
printf("static addr: %p\n", &a);
static addr: 0x601040

地址空间的存在

让父子进程分别打印全局变量的值和地址

#include <stdio.h>  
#include <stdlib.h>  
#include <unistd.h>  
#include <sys/types.h>

int g_val = 100;

int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        //child
        while (1)
        {
            printf("我是子进程:%d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
            sleep(1);
        }
    }
    else 
    {
        //parent
        while (1)
        {
            printf("我是父进程:%d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
            sleep(2);
        }                                                                                             
    }                                                                                                 
    return 0;                                                                                         
} 
[CegghnnoR@VM-4-13-centos 2022_8_15]$ ./mytest
我是父进程:2840, ppid: 14100, g_val: 100, &g_val: 0x601054
我是子进程:2841, ppid: 2840, g_val: 100, &g_val: 0x601054

父子进程打印的全局变量值和地址是一致的,由此可以得出:

当父子进程没有修改全局数据的时候,父子是共享该数据的。


下面对子进程部分进行修改,让它 5 秒后修改全局变量:

//child
int flag = 0;
while (1)
{
    printf("我是子进程:%d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
    sleep(1);
    ++flag;
    if (flag == 5)
    {
        g_val = 200;
        printf("我是子进程,我已修改全局数据\n");
    }  
}
#修改前同上。。。
我是子进程,我已修改全局数据
我是子进程:5081, ppid: 5080, g_val: 200, &g_val: 0x60105c
我是父进程:5080, ppid: 14100, g_val: 100, &g_val: 0x60105c

现象:子进程的 g_val 确实改成了200,父进程的没改,但是他们的地址却都一样。

也就是说,父子进程读取的是同一个变量,但是在后续有修改的情况下,父子进程读取到的内容却不一样。

结论我们在 C/C++ 中使用的地址,不是物理地址

它其实是虚拟地址,也叫线性地址,逻辑地址

虚拟地址可以通过页表映射到物理地址。

概念

每一个进程在启动的时候,都会让操作系统为其创建一个地址空间,该地址空间就是进程地址空间

同样的,操作系统也要管理这些进程地址空间,所以它其实是内核的一个数据结构 struct mm_struct

为了维护进程的独立性,进程地址空间让每个进程都认为自己是独占系统中的所有资源的。

所谓进程地址空间,其实就是OS通过软件的方式,给进程提供一份软件视角,认为自己会独占系统的所有资源(主要是内存资源)。

在内核里的具体实现是,task_struct(PCB)内有指针指向 mm_struct(进程地址空间),mm_struct 内有指针指向一个链表,链表的每一个结点有 startend 表示一块分区,还有一个指针,指向页表。


❓程序是如何变成进程的?

  • 代码被编译出来,还没有被加载进内存的时候,程序内部就已经有地址和分区了。readelf -S [可执行程序] 显示的就是程序内部的各种区域。

  • 这种地址是一种相对地址,当加载到内存里的时候,利用它在内存里的第一个位置和相对位置偏移量就可以转化成虚拟地址,并建立页表。

❓为什么父子进程全局变量的地址一样,读取到的内容却不一样?

  • 在没有修改的时候,确实是共享同一块空间。当有进程要修改全局变量时,操作系统会重新开辟一段空间,并替换页表中的物理地址,使原来的虚拟地址映射到一个新的物理地址。

  • 也就是说,页表只有物理地址改了,虚拟地址不变,所以我们看到的地址是一样的,但实际上两个进程通过各自的页表映射到了不同的物理空间,读取的值也就不一样了。

  • 操作系统给修改的一方重新开辟空间,并把原来的数据拷贝到新的空间的行为叫做写时拷贝

pid_t id = fortk() 中,同一个 id 变量,为什么会有不同的值?

  • pid_t id 是属于父进程栈空间中定义的变量,fork 内部会创建一个进程,程序运行到 return 前就已经有两个进程了,所以 return 会被执行两次,而 return 的本质,就是通过寄存器将返回值写入到接受返回值的变量中。当 id = fork() 的时候,谁先返回,谁就发生写时拷贝。最后大家的虚拟地址是一样的,但是对应的物理地址不一样,也就有了不同的值。

❓为什么要有虚拟地址空间?

  1. 保护内存。内存是硬件,本身并没有分区,虚拟地址空间则是在程序和硬件之间添加了一层软硬件层,对非法的访问可以直接拦截。
  2. 管理内存。通过地址空间,进行功能模块的解耦。
  3. 让进程或程序可以以统一的视角看待内存。维护进程独立性。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

世真

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值