当前位置: 首页 > news >正文

【Linux】进程地址空间

本篇博客来认识一下linux下程序地址空间的概念

演示所用系统:CentOS 7.6

文章目录

  • 1.引入程序地址空间
    • 1.1 验证不同区域
    • 1.2 fork感知地址空间的存在
  • 2.简述程序地址空间
    • 2.1 程序地址空间和代码编译
    • 2.2 写时拷贝
      • fork两个返回值的解释
  • 3.程序地址空间的作用
  • 结语

1.引入程序地址空间

之前学习C/C++的时候,多少应该都听过栈区/堆区/静态区/全局区的概念,还有一张很经典的演示图,大部分讲解这几个内存区域的图片都和下图类似

image-20221007151039950

但是有一个问题,这里的程序地址空间,是我们的物理内存上的东西吗?

并不是!

  • 程序/进程地址空间是操作系统上的概念,它和我们物理内存本身不是一个东西

1.1 验证不同区域

用下面这个代码来简单验证一下不同区域上的区别

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

int un_global_val;//未初始化全局变量
int global_val=100;//已初始化全局变量
//main函数的参数
int main(int argc, char *argv[], char *env[])
{
    printf("code addr         : %p\n", main);
    printf("init global addr  : %p\n", &global_val);
    printf("uninit global addr: %p\n", &un_global_val);
    char *m1 = (char*)malloc(100);
    char *m2 = (char*)malloc(100);
    char *m3 = (char*)malloc(100);
    char *m4 = (char*)malloc(100);
    int a = 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("stack addr a      : %p\n", &a);
    printf("stack addr s      : %p\n", &s);
    printf("\n");
    for(int i = 0; i < argc; i++)
    {
        printf("argv addr         : %p\n", argv[i]);
    }
    printf("\n");
    for(int i =0 ; env[i];i++)
    {
        printf("env addr          : %p\n", env[i]);
    }
    return 0;
}

image-20221007151932639

通过上面的测试,可以看到其结果和文章最开始的那张图相同。这里解释一下向上/向下的含义

  • 向上增长:向地址增大的方向增长
  • 向下增长:向地址减小的方向增长

不过那个图片内部还少了一些东西,比如命令行参数和环境变量其实是存放在栈区之上的。补全之后的图片如下

image-20221007152623384

其中我们还可以发现,栈区和堆区之间有非常大的内存空隙

heap addr         : 0x1a140f0
heap addr         : 0x1a14160
stack addr        : 0x7ffe6671ec60
stack addr        : 0x7ffe6671ec58

因为在C/C++中定义的变量都是在上保存的,栈向下增长,先定义的变量地址较高!

    int a = 100;
    static int s = 100;

关于函数中static修饰的变量,可以看到其地址空间属于全局静态区。虽然在函数中用static修饰是限制其只能在该函数内访问,但是该变量的声明周期是跟随整个程序的!

stack addr a      : 0x7ffe6671ec44
stack addr s      : 0x601048

说了这么多,我们也没看看出来程序地址空间在哪儿啊?

1.2 fork感知地址空间的存在

下面可以用一个简单的fork代码来确认程序地址空间的存在!

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

int main()
{
    int test = 10;
    int ret = fork();
    if(ret == 0)
    {
        while(1)
        {
            printf("我是子进程%d,ppid:%d,test:%d,&test: %p\n\n",getpid(),getppid(),test,&test);
            sleep(1);
        }
    }
    else
    {    
        while(1)
        {
            printf("我是父进程%d,ppid:%d,test:%d,&test: %p\n\n",getpid(),getppid(),test,&test);
            sleep(1);
        }
    }       
    return 0;
}

依旧是最简单的一个fork代码,正常情况下,二者打印的结果应该是一样的!

image-20221007154111800

可如果我们在子进程中修改一下test呢?

image-20221007154330028

这时候就会发现一个离谱的现象:子进程和父进程打印的test值不一样,但是其地址却完全相同

如果我们在C/C++中使用的地址就是物理地址,是不可能出现这种情况的!怎么可能在物理内存的同一个地址访问出两个不同的结果呢?

就好比张三和李四在同一天的同一时间去了AA路30号这个地址,不可能会出现张三去了发现是超市,而李四去了发现是医院的情况

这便告诉我们了程序地址空间的存在,亦或者说,我们在编程中使用的地址都是虚拟地址

2.简述程序地址空间

每一个进程在启动的时候,都会让操作系统给其分配一个地址空间,这就是进程地址空间

  • 先描述再组织的理念,进程地址空间其实是操作系统内核的一个数据结构struct mm_struct
  • 之前提到过进程具有独立性,在多进程运行的时候,需要独享各种资源。而进程地址空间的作用,就是让进程认为自己是独占操作系统中的所有资源!

这个操作,其实就是操作系统给该进进程画了一个假的内存(虚拟地址)进程需要内存的时候,操作系统就会在页表里面画一个地址给他,再将该地址映射到物理内存上面

image-20221007203947913

在Linux源码中可以看到这玩意的存在,其中的struct vm_area_struct * mmap;就是一个我们的页表

image-20221007203759071

这里就能看到虚拟地址空间的start和end了!

image-20221007203918107

2.1 程序地址空间和代码编译

我们直到,C语言代码需要经过预处理-编译-链接-汇编这几个步骤

  • 程序编译出来,没有被加载的时候,程序内部有地址(如果没有地址,无法进行链接)
  • 程序编译出来,没有被加载的时候,程序内部有区域readelf -s 可执行文件可以查看区域)
[muxue@bt-7274:~/git/raspi/code/22-10-07_程序地址空间]$ readelf -S test
There are 30 section headers, starting at offset 0x19f8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298
       000000000000001c  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002b8  000002b8
       00000000000000c0  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000400378  00000378
       0000000000000059  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           00000000004003d2  000003d2
       0000000000000010  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          00000000004003e8  000003e8
       0000000000000020  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             0000000000400408  00000408
       0000000000000018  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000400420  00000420
       00000000000000a8  0000000000000018  AI       5    23     8
  [11] .init             PROGBITS         00000000004004c8  000004c8
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         00000000004004f0  000004f0
       0000000000000080  0000000000000010  AX       0     0     16
  [13] .text             PROGBITS         0000000000400570  00000570
       00000000000001e2  0000000000000000  AX       0     0     16
  [14] .fini             PROGBITS         0000000000400754  00000754
       0000000000000009  0000000000000000  AX       0     0     4
  [15] .rodata           PROGBITS         0000000000400760  00000760
       000000000000005e  0000000000000000   A       0     0     8
  [16] .eh_frame_hdr     PROGBITS         00000000004007c0  000007c0
       0000000000000034  0000000000000000   A       0     0     4
  [17] .eh_frame         PROGBITS         00000000004007f8  000007f8
       00000000000000f4  0000000000000000   A       0     0     8
  [18] .init_array       INIT_ARRAY       0000000000600e10  00000e10
       0000000000000008  0000000000000008  WA       0     0     8
  [19] .fini_array       FINI_ARRAY       0000000000600e18  00000e18
       0000000000000008  0000000000000008  WA       0     0     8
  [20] .jcr              PROGBITS         0000000000600e20  00000e20
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .dynamic          DYNAMIC          0000000000600e28  00000e28
       00000000000001d0  0000000000000010  WA       6     0     8
  [22] .got              PROGBITS         0000000000600ff8  00000ff8
       0000000000000008  0000000000000008  WA       0     0     8
  [23] .got.plt          PROGBITS         0000000000601000  00001000
       0000000000000050  0000000000000008  WA       0     0     8
  [24] .data             PROGBITS         0000000000601050  00001050
       0000000000000004  0000000000000000  WA       0     0     1
  [25] .bss              NOBITS           0000000000601054  00001054
       0000000000000004  0000000000000000  WA       0     0     1
  [26] .comment          PROGBITS         0000000000000000  00001054
       000000000000002d  0000000000000001  MS       0     0     1
  [27] .symtab           SYMTAB           0000000000000000  00001088
       0000000000000648  0000000000000018          28    46     8
  [28] .strtab           STRTAB           0000000000000000  000016d0
       000000000000021e  0000000000000000           0     0     1
  [29] .shstrtab         STRTAB           0000000000000000  000018ee
       0000000000000108  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

需要注意的是,程序内部的地址,和内存的地址没有关系

可以理解为,我们程序内部都存放的是一个相对地址。编译程序的时候,认为程序是按照0000~FFFF进行编址的。

当程序被加载到内存当中时,假设系统将该程序的代码从内存0x100开始加载,就可以依照程序编址的数据加上这个偏移量,从而存放在内存中。

比如程序中有一个代码段的位置是0x1F,这时候在加载程序的时候,就会把这个代码段加上偏移量来加载

代码地址虚拟地址
0x1f0x11f
0x200x120

大概就是这样,吧哩吧啦……

2.2 写时拷贝

现在就可以来解答一下1.2中出现的问题了

image-20221007200911768

当子进程尝试修改test变量的时候,操作系统就会开始一个写时拷贝,开辟一个新的空间,将对应的值考入该空间,再重新映射页表。

这时候,虽然页表左侧的虚拟地址没有变化,但是映射的物理地址已经不一样了!

image-20221007200928695

这样就能保证父子进程的独立性,谁修改变量都互不影响!

类似C++中实现的深拷贝

fork两个返回值的解释

pid_t id这个变量属于父进程栈空间中定义的变量,但是fork内部,return会被执行两次(return的本质是通过寄存器将返回值写入到接收返回值的变量中)

id = fork()的时候,谁先返回,谁就会发生一次写时拷贝。所以同一个变量有不同的内容值,本质上也是同一个虚拟地址,对应了不同物理地址的体现!

  • 打印fork的返回值,即可观察到和1.2中一样的情况,虚拟地址相同,但是ret的值不同

image-20221007201647347

3.程序地址空间的作用

需要注意的是,内存作为一个硬件,没有办法拒绝你的读写!内存是不带控制功能的!

直接让用户修改物理内存风险极大:

  • 野指针问题
  • 用户可能直接修改操作系统需要用到的内存地址,导致系统boom

程序地址空间让访问内存时添加了一层软硬件层,可以对转化过程进行审核,拦截非法的访问

  • 保护内存
  • 可以使用进程管理更好的对功能模块进行解耦(linux内存管理)
  • 让程序/进程可以用统一的方式/视角来看待内存,以统一的方式编译加载所有可执行程序,简化程序本身的设计和实现

同时,程序地址空间还可以延迟用户的内存使用。比如我们现在malloc了100个字节的空间,实际上操作系统并不会立马给你申请空间,而是操作你的mm_struct让进程以为自己已经申请成功了。当程序真正使用这个空间的时候,操作系统才会去物理内存中进行映射!

申请的时候,是通过linux的内存管理模块进行操作的。该模块只负责开辟内存,而不管开辟内存的用途

这种“延迟访问”,可以避免某些程序申请了内存而在一段时间内没有使用的问题!避免了内存资源的无效占用(也是一种浪费)

结语

关于这部分的理解其实并不算十分透彻,或许在日后的项目实践中能加深理解呢~

相关文章:

  • 【计算机组成原理】输入/输出系统(四)—— I/O方式
  • 让GPU跑的更快
  • 给课题组师弟师妹们的开荒手册
  • Java操作Excel - Easy Excel
  • 交通状态预测 | Python实现基于LSTM的客流量预测方法
  • 一条sql语句在MySQL的执行流程
  • 当遇到听不了的歌,Python程序员都是这么做的...
  • leetcode-289:生命游戏
  • C语言中的结构体应用详解及注意事项
  • 【2022】Elasticsearch-7.17.6集群部署
  • 计算器——位运算(c语言)
  • Maven 基础 5 第一个Maven 项目(IDEA 生成)
  • TypeScript算法题实战——哈希表篇
  • 嵌入式分享合集71
  • 又一巅峰神作 14年工作经验大佬手写“微服务项目下高并发的流量治理”,太牛了
  • Docker: 容器互访的三种方式
  • java多线程
  • Laravel Mix运行时关于es2015报错解决方案
  • MaxCompute访问TableStore(OTS) 数据
  • Median of Two Sorted Arrays
  • Vue 重置组件到初始状态
  • 给新手的新浪微博 SDK 集成教程【一】
  • 解决iview多表头动态更改列元素发生的错误
  • 小试R空间处理新库sf
  • 完善智慧办公建设,小熊U租获京东数千万元A+轮融资 ...
  • #pragma 指令
  • #经典论文 异质山坡的物理模型 2 有效导水率
  • $.ajax()参数及用法
  • ${ }的特别功能
  • (10)ATF MMU转换表
  • (Oracle)SQL优化技巧(一):分页查询
  • (第二周)效能测试
  • (附源码)计算机毕业设计ssm高校《大学语文》课程作业在线管理系统
  • (附源码)计算机毕业设计SSM基于健身房管理系统
  • (接口自动化)Python3操作MySQL数据库
  • (蓝桥杯每日一题)love
  • (译) 函数式 JS #1:简介
  • (转)Linux整合apache和tomcat构建Web服务器
  • .NET Core6.0 MVC+layui+SqlSugar 简单增删改查
  • .net 按比例显示图片的缩略图
  • .Net6 Api Swagger配置
  • .project文件
  • [Android Pro] AndroidX重构和映射
  • [C#]C#学习笔记-CIL和动态程序集
  • [C++] 多线程编程-thread::yield()-sleep_for()
  • [CQOI 2011]动态逆序对
  • [docker]docker网络-直接路由模式
  • [E链表] lc83. 删除排序链表中的重复元素(单链表+模拟)
  • [halcon案例2] 足球场的提取和射影变换
  • [IT生活推荐]大家一起来玩游戏喽,来的都进!
  • [kubernetes]控制平面ETCD
  • [leetcode top100] 0924 找到数组中消失的数,合并二叉树,比特位计数,汉明距离
  • [Linux] 常用命令--版本信息/关机重启/目录/文件操作
  • [Mac软件]Boxy SVG 4.20.0 矢量图形编辑器
  • [NOI2022] 众数 题解