Linux进程地址空间

一.进程地址空间

内存会分为几个区域:栈区、堆区、全局/静态区、代码区、字符常量区 …
如下空间布局图,请问这是物理内存吗?—— 不是,是进程地址空间。

两点结论

  • 进程地址空间不是物理内存!

  • 进程地址空间,会在进程的整个生命周期内一直存在,直到进程退出!

这也就解释了为什么全局/静态变量的生命周期是整个程序,因为全局/静态变量是随着进程一直存在的。

二. 验证地址空间的基本排布
 

#include <stdio.h>
#include <unistd.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);
   char *m2 = (char*)malloc(100);
   char *m3 = (char*)malloc(100);
   char *m4 = (char*)malloc(100);
   static int s = 100;
   printf("heap addr         : %p\n", m1);
   printf("heap addr         : %p\n", m2);
   printf("heap addr         : %p\n", m3);
   printf("heap addr         : %p\n", m4);

   printf("stack addr        : %p\n", &m1);
   printf("stack addr        : %p\n", &m2);
   printf("stack addr        : %p\n", &m3);
   printf("stack addr        : %p\n", &m4);
   printf("s stack addr        : %p\n", &s);
   for(int i = 0; i < argc; i++)
   {
       printf("argv addr         : %p\n", argv[i]); //argv/&argc?
   }

   for(int i =0 ; env[i];i++)
   {
       printf("env addr          : %p\n", env[i]);
   }
}

运行结果:

堆区向地址增大的方向增长,栈区选地址减少的方向增长,堆栈相对而生。
而是我们一般在c函数中定义的变量通常在栈保存,那么先定义的一定是地址比较高的。
如何理解static变量?函数类定义的变量用static修饰,本质是编译器会把该变量编译进全局数据区

三.虚拟地址

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int g_val=100;

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        int flag = 0;
        while(1)
        {
            printf("我是子进程:%d, ppid: %d, g_val: %d, &g_val: %p\n\n", getpid(), getppid(), g_val, &g_val);
            sleep(1);
            flag++;
            if(flag == 5)
            {
                g_val=200;
                printf("我是子进程,全局数据我已经改了,用户你注意查看!\n");
            }
        }
    }
    else 
    {
        //parent
        while(1)
        {
            printf("我是父进程:%d, ppid: %d, g_val: %d, &g_val: %p\n\n", getpid(), getppid(), g_val, &g_val);
            sleep(2);
        }
    }
}

 

观察发现,父子进程,打印的变量值是不一样的,但是变量地址竟然是一样的!
我们知道,父子进程代码共享,数据各自私有一份(写时拷贝)。

  • 思考1:运行结果中,变量值不一样,说明父子进程中的变量绝对不是同一个变量。
  • 思考2:运行结果中,打印的变量地址,绝对不可能是物理地址,因为同一物理地址处,不可能读取出来两个不一样的值。

所以,可以得出如下结论:

  • 我们曾经在 C/C++ 语言等其它语言中学到和看到的地址(比如取地址),全部都是「虚拟地址」,由操作系统统一管理。而物理地址,用户是一概看不到的。
  • 注意:程序的代码和数据一定是存在物理内存上的!因为想要运行程序必须先将代码和数据加载到物理内存中。所以一定需要操作系统负责将「虚拟地址」转化成「物理地址」。

同一个变量,打印的地址相同,其实是「虚拟地址」相同,内容不同其实是被映射到了不同的「物理地址」处

操作系统默认会给每个进程构建一个地址空间的概念(比如32位下,把物理内存资源抽象成了从 0x00000000 ~ 0xFFFFFFFF 共 4G 的一个「线性的虚拟地址空间」)

假设系统中有 10 个进程,每个进程都会认为自己有 4G 的物理内存资源。

每一个进程都会创建一个task_struct,每个进程都会维护一个mm_struct,都有相对应的区域,那么程序加载到内存时,程序会自己加载到物理内存的物理地址,CPU将虚拟地址和物理地址建立映射关系,最终进程访问的时候,它访问的是某个区域当中的地址的时候,它直接经过页表映射到物理内存,找到对应的代码。

当我们找到对应的代码和数据时,我们要将这个代码和数据加载到我们所对应的CPU里面,当我们把这个代码加载到CPU里的时候,这个代码里面也有地址的。那么这个代码里面的地址是什么地址呢?它早已经在我们加载的时候被转化成了线性地址或虚拟地址,所以CPU可以继续照着这个逻辑继续向后运行。

从同一块物理地址中取出的值是相同的,所以这个程序取出的地址(指针)并不是物理地址,而是虚拟地址(线性地址、逻辑地址)。注:逻辑地址指可执行程序编译完成后内部函数、变量的地址。

逻辑地址有两种表示方法,一种是各个区域地址递增,另一种是每个区域的地址都从零偏移量开始(这种是比较老的表示方式)。在Linux中的逻辑地址是第一种表示方式,所以Linux中逻辑地址就是虚拟地址。

之前学习的C/C++内存区域,是一块虚拟内存空间,每个进程有它自己的虚拟内存空间,即进程地址空间。所以上面的代码用fork创建子进程,因为子进程是父进程的拷贝,父子进程的grobal_val虽然虚拟地址一样,但会被映射到不同的物理地址上。

当g_val未被改变时,父子进程映射同一块g_val的物理地址,一旦父子进程的一方对共享数据进行修改,由于进程的独立性,操作系统会在物理内存中再开辟一块空间,并拷贝原数据,提出修改的进程的页表映射关系将会被改变,然后再让进程对数据进行修改,所以我们看到父子进程的数据并不一样。这种技术称为写时拷贝,对不同进程的数据进行分离。

四.认识地址空间

  • 在 Linux 中,「地址空间其实是内核中的一种数据结构」。
  • 在 Linux 中,OS 除了会为每个进程创建对应的 PCB(即 struct task_struct 结构体),还会创建对应的进程地址空间,即内核中的 struct mm_struct, 结构体task_struct中有一个指针指向自己的mm_struct
  • 进程它自己会认为它独占CPU资源,但其实并不是。因为进程以时间片轮转的形式占用CPU资源,时间一到,马上从运行状态进入休眠状态,实质上是通过虚拟地址空间,让进程认为它独占CPU资源。
  • 进程地址空间是操作系统给进程开辟的一块虚拟内存空间,这块空间用内核的一种数据结构来描述、组织。
  • 操作系统给每个进程一块4GB的虚拟内存,进程每次想使用,按需申请即可,但不会全部给进程。(注意这里给的是虚拟内存)
struct mm_struct
{
	uint32_t code_start,code_end;
	uint32_t data_start,data_end;
	uint32_t heap_start,heap_end;
	uint32_t stack_start,stack_end;
	······//存储进程地址空间各区域的起始位置
};

地址空间的本质:操作系统让「进程看待物理内存的方式」,这是抽象出来的一个概念。地址空间是内核中的一种数据结构,即 struct mm_struct 结构体。由 OS 给每个进程创建,这样每个进程都认为自己独占系统内存资源。

划分区域的本质:把「线性的地址空间」划分成了一个个的区域,通过设置结构体内的 start 和 end 的值,来表示区域的起始和结束。(比如栈区和堆区的增长)

为什么要进行区域划分呢?

可以通过 [start, end] 进行初步判断访问某个虚拟地址时,是否越界访问了。
因为可执行程序,在磁盘中是被划分成一个个的区域存储起来的,所以进程的地址空间才有了区域划分这样的概念,方便进程找到代码和数据。

虚拟地址的本质:每个区域 [start, end] 之间的各个地址就是虚拟地址,之间的虚拟地址是连续的

 

五.为什么要通过虚拟地址映射的方式访问物理地址

  • 直接访问物理内存是非常不安全的,例如越界操作、恶意进程读取等。
  • 页表会拦截不合理的请求,可以保护物理内存,防止恶意进程的访问 。所以写代码出现野指针、内存越界等情况并不会造成操作系统的崩溃。
  • 进程地址空间的存在,可以让进程和进程间的代码进行解耦(互不干扰),保证了进程独立性的特征。
  • 进程和编译器均遵守进程地址空间这一套规则,编完即可使用。

编译器也遵守进程地址空间这一套规则:

我们的代码在磁盘时,程序的函数、变量等通过虚拟地址建立联系,满足程序间的互相跳转;

当程序由磁盘被加载到内存中时,就具备了物理地址。函数、变量等通过页表映射至虚拟地址。

根据可执行程序的虚拟地址初始化mm_struct结构体中每个虚拟内存中的边界。

当程序在CPU中跑起来时,CPU根据虚拟地址运行完程序后,通过页表映射至物理地址。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值