验证地址空间

#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;
}

打印结果:

code addr: 0x40055d
read only string addr: 0x400800
init global addr: 0x601034
uninit global addr: 0x601040
heap addr: 0x8e2010
heap addr: 0x8e2030
heap addr: 0x8e2050
heap addr: 0x8e2070 # 堆向上(高地址)增长
test static addr: 0x601038
stack addr: 0x7ffdf55f13d8
stack addr: 0x7ffdf55f13d0
stack addr: 0x7ffdf55f13c8
stack addr: 0x7ffdf55f13c0 # 栈向下(低地址)增长
argv[0]: 0x7ffdf55f17d0
env[0]: 0x7ffdf55f17d8
env[1]: 0x7ffdf55f17ed
env[2]: 0x7ffdf55f180e
env[3]: 0x7ffdf55f1819
env[4]: 0x7ffdf55f1829
env[5]: 0x7ffdf55f1837
env[6]: 0x7ffdf55f1859
env[7]: 0x7ffdf55f186e
env[8]: 0x7ffdf55f1881
env[9]: 0x7ffdf55f188e
env[10]: 0x7ffdf55f18d5
env[11]: 0x7ffdf55f1e71
env[12]: 0x7ffdf55f1e8e
env[13]: 0x7ffdf55f1ef0
env[14]: 0x7ffdf55f1f12
env[15]: 0x7ffdf55f1f23
env[16]: 0x7ffdf55f1f3a
env[17]: 0x7ffdf55f1f42
env[18]: 0x7ffdf55f1f55
env[19]: 0x7ffdf55f1f65
env[20]: 0x7ffdf55f1f71
env[21]: 0x7ffdf55f1fa4
env[22]: 0x7ffdf55f1fc7
env[23]: 0x7ffdf55f1fe6

注意点:

  • 从上到下的布局:

    [高地址]
    内核空间
    命令行参数环境变量
    栈
    ↓
    共享区
    ↑
    堆
    未初始化数据
    初始化数据
    (字符常量区)
    正文代码
    [低地址]
    
  • 栈向下(低地址)增长,堆向上(高地址)增长

    堆与栈相对而生

  • 在栈中:整体的空间利用是从高到低,但是每一个变量内部是从低到高

    因为每一个变量的&取地址都是取最低的地址,因此在变量中都是先利用最小的地址。数组内部元素同理。下标小的在低地址,下标大的在高地址

以上讲的不是真正的内存,而是 “进程地址空间/虚拟地址空间”,这是操作系统的概念!

不叫 “程序地址空间”!!!


#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("child process: pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
      sleep(1);
      ++g_val;
    }
  }
  else if(id > 0)
  {
    while(1)
    {
      printf("parent process: pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
      sleep(1);
    }
  }
  else
  {
    printf("fork ERROR!\n");
    return 1;
  }
  return 0;
}

运行结果:

g_val: 100, &g_val: 0x601054
parent process: pid: 12501, ppid: 29548, g_val: 100, &g_val: 0x601054
child process: pid: 12502, ppid: 12501, g_val: 100, &g_val: 0x601054
parent process: pid: 12501, ppid: 29548, g_val: 100, &g_val: 0x601054
child process: pid: 12502, ppid: 12501, g_val: 101, &g_val: 0x601054
parent process: pid: 12501, ppid: 29548, g_val: 100, &g_val: 0x601054
child process: pid: 12502, ppid: 12501, g_val: 102, &g_val: 0x601054
parent process: pid: 12501, ppid: 29548, g_val: 100, &g_val: 0x601054
child process: pid: 12502, ppid: 12501, g_val: 103, &g_val: 0x601054
parent process: pid: 12501, ppid: 29548, g_val: 100, &g_val: 0x601054
child process: pid: 12502, ppid: 12501, g_val: 104, &g_val: 0x601054
parent process: pid: 12501, ppid: 29548, g_val: 100, &g_val: 0x601054
child process: pid: 12502, ppid: 12501, g_val: 105, &g_val: 0x601054
parent process: pid: 12501, ppid: 29548, g_val: 100, &g_val: 0x601054
child process: pid: 12502, ppid: 12501, g_val: 106, &g_val: 0x601054
^C
  • 子进程试图修改变量时,会发生写时拷贝——进程具有独立性

为什么两个进程打印的 g_val 地址相同

说明,该地址不是物理地址!

曾经在编程语言中学的指针也不是物理地址!

以前学的地址都叫虚拟地址


计算机中存在两套地址:物理地址虚拟地址空间的虚拟地址

曾经学的 000…00 至 FFF…FF 都是虚拟地址

操作系统创建进程时,会为进程创建一个页表——保存虚拟地址对物理地址的映射

初始化进程时也要初始化页表

用户使用的地址是虚拟地址,对某个地址进行修改时,内核需要找出对应的物理地址后再进行修改


子进程在创建时,是以父进程为模板

进程 = 内核数据结构 + 代码与数据

  • 虚拟地址空间和页表,每个进程各有一套

  • fork之后,父子共享代码和数据

    ——子进程会拷贝父进程的页表,类似发生了浅拷贝

  • 旧结论:进程具有独立性

    如何保证进程独立性:

    • 代码为只读,父子不会互相影响

    • 如果数据被修改,会导致进程不独立

      OS规定:父子中,任何一个进程尝试对共享的变量进行修改时,不能直接修改,而是发生写时拷贝

      此时试图修改共享变量的进程的页表中会获得新的物理地址——类比深拷贝

    代码和数据都是独立的

物理地址被操作系统隐藏,用户无法访问——可以保护内存数据



虚拟地址空间如何管理?

先描述再组织!

类比进程管理,linux系统中有一个mm_struct的结构体用于管理每一个进程的虚拟地址空间

mm_struct 可以在 task_struct 中找到


如何理解虚拟地址空间中的区域划分?

struct v_memory
{
    int task1_start;
    int task1_end;
    int task2_start;
    int task2_end;
    // ...
}
void test()
{
    struct v_memory vm(1, 50, 50, 100);
}

区域内的任何位置,对应的进程可以使用

当需要调整内存中某个区域的大小时,可修改结构体变量中成员变量的值


什么是页表?查表工作谁来做?为什么要有虚拟地址空间?其它子问题

  • 页表基础理解

    一个多列表格,内容包括虚拟地址、物理地址、标志位(例如记录rwx权限)

    虚拟地址 物理地址 权限 是否存在内存中 其它标志位
    001234 001111 r 1
    r/w/x三种权限 1表示存在于物理内存;0表示不存在于物理内存,数据被挂起至swap分区
  • 查表工作由MMU(Memory Management Unit 内存管理单元,CPU的一个硬件)完成

  • 为什么全局变量、static变量的生命周期是全局的?

    因为这两种变量存于初始化数据/未初始化数据区,本质是生命周期随进程

  • 常量字符串和代码为什么是只读的?

    页表给进程提供的限制——不允许进程写入

  • 其它语言如java/py,都有虚拟地址,java虚拟机由cpp写成,py解释器由cpp写成

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

    1. 控制进程的行为,拦截进程的非法行为,保护物理内存

    2. 有了虚拟地址空间和页表,原则上:

      • 数据可以存在于物理内存的任意位置,而不用严格按照虚拟地址空间中的布局

      因此可以做到:将内存布局无序变有序

    3. 在内核层面上实现进程管理与内存管理解耦合

      • 在Linux系统中,创建一个进程时,先创建内核数据结构,再加载代码和数据

      • 由于上面这个机制,可以实现——懒加载机制

        在不急着执行该进程时,可以先只创建内核数据结构,暂不加载代码和数据;等到需要运行时再加载代码和数据。

        使得物理内存能够得到更好的利用!

      • 例如:在Cpp程序中,使用malloc申请的空间不一定会马上使用

        因此本质是在虚拟地址空间开辟空间,在填写了页表的虚拟地址栏,但是物理地址栏留空,等待需要使用时再进行动态申请!

        该过程名为——缺页中断引起内存二次申请

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐