【Linux】程序地址空间 | 写时拷贝(五)

目录

前言:

一、初步认识程序地址空间

1.虚拟地址

2.mm_struct区域划分 

二、写时拷贝

三、页表(宏观认识)

1.isexists标志位

四、关于mm_struct地址空间初始化

1.ELF格式文件

五、为什么存在虚拟地址空间? 

六、重谈写时拷贝

七、命令总结:

总结:


前言:

我们已经知道了进程状态,进程调度,OS管理,环境变量,还是否记得我们当时说过folk有两个返回值,我们当时只是说了个大概,这里我们就要来详细介绍到底是为什么。

一、初步认识程序地址空间

我们使用fork创建一个子进程,定义一个全局变量gval,子进程修改该全局变量,父进程只读取,每个进程都打印出gval的地址。观察结果:

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

int gval = 100;

int main()
{
    printf("我是一个进程, pid: %d, ppid: %d\n", getpid(), getppid());

    int id = fork();
    if (id == 0)
    {
        //字进程
        while(1)
        {
            printf("我是子进程, pid: %d, ppid: %d, gval: %d, &gval: %p\n", getpid(), getppid(), gval, &gval);
            gval++;
            sleep(1);
        }
    }
    else 
    {
        //父进程
        while(1)
        {
            
            printf("我是父进程, pid: %d, ppid: %d, gval: %d, &gval: %p\n", getpid(), getppid(), gval, &gval);
            sleep(1);
        }
    }
    return 0;
}

所以可以证明,该地址绝对不是物理地址! 

所以此处是虚拟地址/线性地址。

1.虚拟地址

对于C/C++程序的地址空间,大家应该都不陌生:

首先我们要了解两个概念:进程和程序。这两个不是同一个概念。

程序:是一组计算机能识别和执行的指令,通常存储在磁盘等介质上,以文件形式存在。例如.exe可执行文件,是一个静态的指令集合。

进程:是程序在计算机中的一次执行过程。当你启动了一个可执行程序的时候,OS会为这个程序创建一个进程,进程是一个动态的概念。

区别:程序是静态的指令集合,而进程是动态的执行过程。一个程序可以对应多个进程。

我们平时总会讲起C的地址空间,但是其实这并不是C程序的地址空间,而是启动C程序后的进程地址空间。 

这里我们还是用一个小故事来举例:

在美国有一个大富翁,有10亿美金。他有一群子女,这一群子女看到这10亿美金就会想着如何占有,每人都想据为己有,这样大富翁压力就会很大。

在一个平行宇宙中,这个大富翁吸取了这个大富翁的教训,不同的是这个大富翁私生活比较混乱,有一群私生子。私生子1在读博士,大富翁说你加油,学得好10亿归你。私生子2从商,大富翁说你加油,做得好10亿归你等等。此时大富翁就在对这些私生子进行画饼,而每个私生子都认为自己有10亿美金,私生子1说项出国留学,向大富翁要1w美金,大富翁肯定会给;私生子2说要100亿,大富翁说你要干嘛?肯定不给!

此时大富翁就在给这些私生子们画饼,但是大富翁也要管理好这些饼。所以大家可以把大富翁理解为OS,私生子理解为进程;这些饼理解为进程地址空间。

对这些饼的管理也是先描述再组织

让每一个进程都认为自己是独占系统物理内存大小,进程彼此之间不知道,不关心对方的存在,从而实现一定程度的隔离。所以所谓的进程虚拟地址空间,本质是一个内核数据结构对象(类似PCB)。

这里我们来看看源代码:

2.mm_struct区域划分 

而这个mm_struct有时如何进行区域划分的呢? 

假设此时你设计了一个能进行区域划分的结构体,你应该会这样写:

struct destoproom
{
    int start_part1, end_part1;
    int start_part2, end_part2;
};

所以mm_struct也是这样进行区域划分的。

理解地址空间上的地址(记录的是虚拟地址)。

地址的本质就是一个数字,可以使用unsigned long(4字节)保存,空间范围内的地址,我们可以随意使用,暂时不需要记录它的地址。

二、写时拷贝

在虚拟地址和物理地址会使用页表进行映射(这里不会把页表解释的很清楚,但是大家要大致知道原理是什么)。

在我们没有创建子进程之前,时间上是这样的:

虚拟地址空间和页表就是Linux对虚拟内存的管理方案。 

当创建子进程时,会以父进程为模版,把PCB(会修改一些必要属性)拷贝,页表拷贝,也就是浅拷贝。刚开始子进程没有自己的代码和数据,只能由父进程继承。

此时父子进程映射到同样的内存区域,所以父子代码共享。但是如果子进程此时修改了数据,而进程具有独立性,所以会发生写实拷贝(OS自主完成,是写时拷贝机制)。

所以我们就可以在相同的地址空间看到存储的值不同了。这也就是fork有两个返回值的原因,因为不同进程使用不同的页表查看,映射的物理内存不一样。

其实变量名就是地址。

所以再次理解进程:

进程 = 内核数据结构(task_struct/mm_struct/页表) + 自己的代码和数据

内核数据结构各自一份,代码和数据也是独立的。

三、页表(宏观认识)

页表可以理解为map这个数据结构。

页表中其实还有很多标记位,比如权限标志位和isexists标志位。

我们之前会写这样的代码:

char *str = "hello bit";
*str = 'H'

这是拿着栈区的指针指向放在常量区的数据,此时有要修改常量区数据,如果我们运行程序就会崩溃。这是OS做的事情,因为这段区域的权限是'r',而我们要进行'w',所以OS就杀死了该进程。

这个代码能编译过,所以编译器是发不现的,只有在运行的时候OS会发现,所以一般建议加上const,这样编译器就会发现。

1.isexists标志位

还是否记得,我们在加载一个程序的时候,是先加载PCB之后加载代码和数据。

此时有一个1GB的程序运行,我们先加载其PCB,关于物理地址我们先不管。此时这个程序会直接整个塞到内存中吗?如果真的是这样,后面的程序就很难跑了。

而前期我们只用前半部分,后面几乎不用;而之后前半部分跑完了,执行后半部分,前面的代码就几乎不会被执行了。所以在页表中有一个isexists标记位,表示目标数据是否在内存中,如果在内存中,就直接映射访问。否则发现目标地址无法访问,就会从外设加载到内存,所以会进行分批加载和挂起等操作。

也就是说当一个进程创建时,PCB先创建,页表虚拟地址创建,可能物理地址都没有,当访问时,发现物理地址没有,isexists标志位为false,就从磁盘中加载到内存,这样就建立好了映射关系。

这就是为什么我们内存8GB能玩128GB的游戏。

四、关于mm_struct地址空间初始化

是结构体就需要初始化,每个程序的大小都不一样,所以每个代码的正文代码大小都不一样,它应该是从哪来的?

readelf是一个用于查看 ELF(Executable and Linkable Format)文件信息的工具(这部分内容设计编译原理,比较难理解,目前作者也只能讲个大概,不过绝对可以帮助你理解一些问题)。

1.ELF格式文件

ELF 是一种用于可执行文件、目标文件、共享库等在 Linux 和许多类 Unix 系统中的标准文件格式。

readelf可以显示ELF文件的各种详细信息。我们使用readelf -S envtest来查看envtest程序信息。

这是一个可执行程序形成的分段:

可执行程序在编译的时候,各个区域的大小区域就已经知道了。所以mm_struct各个区域大小从可执行程序来的,可执行程序会进行分段,当加载程序时,可以不加载代码和数据,但是会维护这个可执行程序的属性,比如其代码区有多大等都会维护,初始化mm_struct各个区域的大小。

我们可以通过可执行程序初始化页表的权限标志位,就是通过flags完成的。

可执行程序在编译的时候,各个区域的大小区域就已经知道了,在编译后的文件中声明。但是堆区和栈区这些内容并不存在,之后在加载(运行)进程被OS创建的。堆区在申请时,其实也就是改变之前的end_part1改变而已。

注意刚开始申请的空间可能并没有物理内存,因为刚申请并不一定要使用,当使用时申请物理内存。所以堆区开辟内存本质就是把虚拟内存空间扩大一点点。

五、为什么存在虚拟地址空间? 

为什么存在虚拟地址空间?可以保护内存。什么又叫野指针呢?因为指向的是虚拟内存,页表查看权限标记或者映射关系,不对就会崩溃。

进程管理和内存管理在系统层面进行解耦合了,就是分开了。让进程以统一的视角看待物理内存,可执行程序的代码和数据可以加载到物理内存的任意地址处。也就是无序变有序。

这些以上操作都是有OS自动完成的,只要把进程管理好,地址空间就管理好了。

全局变量,字符常量的全局性,在程序运行期间都会有效,因为在他们在地址空间中,随着进程一直存在,全局变量的虚拟地址,大家都看得到。

六、重谈写时拷贝

系统怎么知道哪个地方要发生写时拷贝?

当父进程创建子进程时,父进程的页表权限全部改为只读。子进程创建后,复制父进程页表,当子进程修改数据时,页表可以识别对只读的区域进行写入,触发系统错误(缺页中断)。系统检测修改的区域是代码区还是数据区,OS判定发生写时拷贝。OS就会申请内存,发生拷贝,修改页表,恢复权限。

大家有没有想过,为什么一定要对修改的数据先要进行拷贝?因为我们的写入操作 != 要对目标区域进行覆盖才做。比如:count++ 

fork失败的原因:系统中有太多进程或者实际用户的进程数超过了限制。

七、命令总结:

readelf:可以显示ELF文件的各种详细信息。

-S:项用于显示 ELF 文件中的节头表(Section Headers)信息。

总结:

我们大致了解了进程地址空间,而且比较详细的了解了写时拷贝的机制,和页表这个玩意儿,那么接下来的文章将更加劲爆!进程替换和进程退出码我们都是需要知道的,这样你对操作系统的很多操作就会醍醐灌顶,记得追剧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值