Linux——程序地址空间|验证地址空间分布|地址空间|问题探讨fork()|一个变量怎么保存不同的值扩展 |为什么要有地址空间的三大理由|理由1 理由2 理由3

简介: 笔记

验证地址空间分布


栈向下增长,堆向上增长


1.png

这里的初始化和未初始化是指全局数据


这里的地址空间不是内存,以前指针的地址也不是内存


makefile里可使用$@ $^


2.png


$@:目标文件,这里就是hello


$^:这个代表冒号右侧所有文件


#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_unval;
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", &g_unval);
    char* heap_mem = (char*)malloc(10);
    printf("heap addr: %p\n", heap_mem); 
    printf("stack addr: %p\n", &heap_mem); 
    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;
}

3.png

在C语言中用malloc去申请空间,如果申请10个字节空间,可能会开辟20个,其中10个是开辟的空间,剩下10个自己存储的是该空间的信息如开辟时间等属性


ctrl+v 配合hjkl控制方向再输入I(大写)可进行注释删除与建立


static修饰的变量在正文代码区


C语言中如果直接写'a',"hello world"这些语句可以直接运行,因为这些是字面常量,int a=10;这不是字面常量,a是变量,10是字面常量,字面常量在正文代码区,正文代码区中有一个字符常量区,字面常量就在这个位置(字符常量区)


在32位机器下,一个进程的地址空间,取值范围是0x0000 0000~0xFFFF FFFF


[0,3GB]:用户空间,这个是按照上面所讲的空间排布的


[3GB,4GB]:内核空间


上面的验证代码,在windows下会跑出不一样的结果,上面的结论,默认只在Linux有效


地址空间


上面打印的全部是进程在打印,打印出来的地址是程序运行后打印的


内核中的地址空间,本质也一定是一种数据结构,要和一个特定的进程关联起来


当操作系统去直接访问物理内存,会存在一些问题,如进程1有野指针,操作系统访通过这个野指针问到了进程3,进程1可通过野指针直接修改进程3的数据,因此这种方式非常危险,后来被人们进行了改进


内存本身是随时可以被读写的

4.png

改进方法:每一个进程有自己的PCB,并且操作系统给每一个进程创建一个虚拟地址空间,这个地址空间叫虚拟/进程地址空间,还添加了一种映射机制,如果要访问物理内存,就要先进行映射


映射机制:虚拟地址->物理地址


5.png


每个进程都要有虚拟地址空间


虚拟地址空间区域划分,通过区域开头和区域尾部来划分区域大小,修改头部或尾部即可修改区域大小,本质是在一个范围里定义出eng和begin


struct space
{
int start;
int end;
}

地址空间是一种内核数据结构,它里面至少要有各个区域的划分


6.png


源代码;


struct mm_struct是内核中的一个结构


虚拟地址空间必须和进程一 一 关联起来


某进程的task_struct里面包含一个指针,这个指针指向该进程所对应的地址空间

7.png

地址空间的划分

8.png9.png


映射关系是通过页表实现的,地址空间和页表是每个进程都私有一份


问题探讨

#include <stdio.h>
#include <unistd.h>
int g_val = 100;
int main()
{
    pid_t id = fork();
    if (id == 0)
    {
        int cnt = 0;
        //child
        while (1)
        {
            printf("I am child, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",   getpid(), getppid(), g_val, &g_val);
            sleep(1);
            cnt++;
            if (cnt == 5)
            {
                g_val = 200;
                printf("child chage g_val 100 -> 200 success\n");
            }
        }
    }
    else
    {
        //father
        while (1)
        {
            printf("I am father, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n",  getpid(), getppid(), g_val, &g_val);
            sleep(1);
        }
    }
return 0;
}

1.png

我们发现改变后g_val的地址仍然一模一样,g_val的值却不同 ,这里怎么可能同一个地址,同时读取的时候,出现了不同的值?


这里的地址,不是我们以前所理解的物理内存的地址,而是虚拟地址(线性地址),几乎所有的语言,如果有地址的概念,这个地址一定不是物理地址


出现这种情况是因为,子进程继承了父进程的相关属性,但会对有些内容做修改如一些私有化的内容


刚开始是这样,子进程拷贝一份父进程的内容,把私有数据进行修改,因为是拷贝的所以页表一样,所以指向的物理内存也一样

2.png



但是当子进程修改数据的时候,为了保证进程的独立性(互不影响),操作系统识别到子进程修改变量时,会给子进程重新开辟一块空间,并把上面的值拷贝过来,然后修改一下子进程的映射关系,完成更改后这俩个的虚拟地址不受影响,但物理内存不一样,所以打出来的一样时因为打出来的是虚拟地址

3.png

写时拷贝:就是等到修改数据时才真正分配内存空间,这是对程序性能的优化,可以延迟甚至是避免内存拷贝,当然目的就是避免不必要的内存拷贝。上面就用到了写实拷贝。


fork ()一个变量怎么保存不同的值

4.png

程序return会被指向俩次


return的本质就是对id进行写入,发生了写时拷贝,所以父子进程各自起始在物理内存中有属于自己的变量空间,只不过在用户层用同一个变量(虚拟地址)来标识了


扩展

当我们的程序,在编译的时候,形成可执行程序的时候, 没有被加载到内存的时候,我们的程序内部是有地址的,可执行程序编译的时候,内部已经有地址了(虚拟)


操作系统不仅要遵守地址空间,编译器也要遵守地址空间,编译器编译代码的时候,就已经给我们形成了各个区域,如:代码区,数据区……并且,采用和Linux内核中一样的编址方式,给每一个变量,每一行代码都进行了编址,程序在编译的时候,每一个字段早已经具有了虚拟地址


程序内部的地址,依旧用的是编译器编译好的虚拟地址,当程序加载到内存的时候,每行代码,每个变量都有一个物理地址(外部地址)


这里CPU拿到的是虚拟地址

5.png

根据写好的代码长度确定虚拟地址的起始和结束


物理内存本身就有地址


CPU读到的是虚拟地址

6.png

为什么要有地址空间的三大理由


理由1

页表不是简单的映射关系,页表起始也是一种数据结构,其中也包含访问物理内存的权限


凡是非法的访问或者映射,OS都会识别到,并且终止这个进程。如:


char *str="1232";
*str='H';

7.png


字符串常量(在代码区中的字符常量区)不允许被修改,因为页表中有这段字符串的读和写权限,所以不能被修改,跟物理内存无关,物理内存可以被随便读写,但是有了页表之后,就不能了


所有的进程崩溃,本质就是进程退出,OS杀掉了这个子进程


地址空间有效的保护了物理内存,因为地址空间和页表是操作系统创建和维护的,凡是想使用地址空间和页表进行映射,也一定要在OS的监管之下来访问,这样保护了物理内存中的所有合法数据,包括各个进程,一级内核相关的数据


理由2

因为有地址空间的存在和页表映射的存在,我们的物理内存中,可以对未来数据进行任意位置的加载,就是把代码和数据从磁盘中加载到物理内存中的任意位置

8.png

物理内存的分配和进程的管理没有任何关系


内存管理模块和进程管理模块是解耦合(减少模块和模块的关联性)关系


9.png


我们在C/C++语言上,new或malloc空间的时候,本质是在虚拟地址空间上申请的


当我们申请了空间,如果我们不立马使用,则浪费了空间。


我们申请的地址空间本质是在虚拟内存空间上申请的,而物理内存甚至可以一个字节都不给我们(操作系统自动完成),当我们要真正访问对应的物理空间的时候,操作系统才执行内存的相关管理算法,帮我们在物理内存上申请内存,构建页表映射关系,让我们进行内存的访问。用户和进程对于这种情况,完全0感知。这种策略也叫延迟分配策略。


理由3

因为在物理内存中理论上可以任意位置加载,所以物理内存中的几乎所有的数据和代码都是乱序的


因为页表的存在,可以将地址空间上的虚拟地址和物理地址进行映射,在进程视角,所有的内存分布,都是有序的。地址空间+页表的存在可将内存分布有序化,这样方便管理。

10.png

地址空间是OS给进程画的大饼


进程要访问的物理内存中的数据和代码,可能目前并没有在物理内存中,同样的,也可以将不同的进程映射到不同的物理内存,这样更容易实现进程的进程独立性。所以进程的独立性,可以通过地址空间+页表的方式实现


因为有地址空间的存在,每一个进程都认为自己有4GB的空间(32),并且各个区域是有序的,进而可以通过页表映射到不同的区域,来实现进程的独立性,每个进程也不知道其它进程的存在。


再谈挂起


红颜色圈起来的都是进程

11.png

加载的本质就是创建进程,但是不需要立刻把所有的程序的代码和数据都加载到内存中,并创建内核数据结构建立映射关系。因为操作系统会帮我们创建进程,甚至极端情况下只有内核结构被创建了出来,连映射关系都没有,这种状态叫新建状态。这样可以实现对程序的分批加载。


既然可以分批加载(唤入),同样可以分批换出,如开机界面加载完就不再加载。甚至这个进程短时间不再被执行了,比如阻塞了。进程的数据和代码被换出了就叫做挂起。


页表映射的时候不止可以映射到物理内存中,还可以直接映射到磁盘中

相关文章
|
8天前
|
Linux 开发工具 C语言
Linux 安装 gcc 编译运行 C程序
Linux 安装 gcc 编译运行 C程序
31 0
|
8天前
|
Linux Android开发
测试程序之提供ioctl函数应用操作GPIO适用于Linux/Android
测试程序之提供ioctl函数应用操作GPIO适用于Linux/Android
11 0
|
5天前
|
存储 Linux Shell
Linux|Awk 变量、数字表达式和赋值运算符
Linux|Awk 变量、数字表达式和赋值运算符
12 2
|
7天前
|
Java Shell Linux
【linux进程控制(三)】进程程序替换--如何自己实现一个bash解释器?
【linux进程控制(三)】进程程序替换--如何自己实现一个bash解释器?
|
7天前
|
Shell Linux 程序员
【linux进程(六)】环境变量再理解&程序地址空间初认识
【linux进程(六)】环境变量再理解&程序地址空间初认识
|
7天前
|
算法 Linux Shell
【linux进程(二)】如何创建子进程?--fork函数深度剖析
【linux进程(二)】如何创建子进程?--fork函数深度剖析
|
21天前
|
NoSQL Linux PHP
php添加redis扩展 linux和windos图文详解 l
php添加redis扩展 linux和windos图文详解 l
3 0
|
29天前
|
监控 Java Linux
使用jvisualVM监控远程linux服务器上运行的jar程序
使用jvisualVM监控远程linux服务器上运行的jar程序
13 5
|
2月前
|
Linux Shell
【Linux】进程与可执行程序的关系&&fork创建子进程&&写实拷贝的理解
【Linux】进程与可执行程序的关系&&fork创建子进程&&写实拷贝的理解
|
2月前
|
算法 Linux 编译器
⭐⭐⭐⭐⭐Linux C++性能优化秘籍:从编译器到代码,探究高性能C++程序的实现之道
⭐⭐⭐⭐⭐Linux C++性能优化秘籍:从编译器到代码,探究高性能C++程序的实现之道
160 2