进程的地址空间

发布于:2024-08-16 ⋅ 阅读:(78) ⋅ 点赞:(0)

进程的地址空间

引例:先看一下效果

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

int g_val = 100;

int main()
{
    printf("father is running,pid:%d,ppid:%d\n",getpid(),getppid());
    pid_t id = fork();
    if(id == 0)
    {
        int cnt = 0;
        while(1)
        {
            printf("I am child process,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 = 300;
                printf("I am child process,change %d -> %d\n",100,300);
            }
        }
    }
    else
    {
        while(1)
        {
            printf("I am father process,pid:%d,ppid:%d,g_val:%d,&g_val=%p\n",getpid(),getppid(),g_val,&g_val);
            sleep(1);
        }
    }
    return 0;
}

运行发现g_val的值不一样,但地址是一样的
在这里插入图片描述
首先我们要知道,编译成二进制的时候,函数名和变量名都不存在。语言上的函数名和变量名也仅仅只是方便我们写代码
在这里插入图片描述
32位和64位的地址空间是不一样的,后面都是以32位为例
在这里插入图片描述

每个进程都要有自己的地址空间。
创建一个进程,不仅仅创建一个地址空间。
子进程会把父进程的很多内核数据结构拷贝一份,所以页表(本质也是结构体对象)也会拷贝一份(与C语言中的浅拷贝类似)
父子进程所指向的都是一个g_val
在这里插入图片描述
如果子进程直接对g_val修改,会被父进程看到。
并且子进程修改了,如果父进程需要用到g_val肯定会影响父进程。
所以当子进程修改的时候,会在物理内存重新开辟一个空间,把老的数据重新拷贝到新的空间当中,把新的地址填入子进程的页表当中,重新构成映射(由OS自主完成写实拷贝)
在这里插入图片描述

如果父子进程都不写(修改)g_val,默认父子进程共享g_val
为什么在创建子进程的时候全把数据全拷贝一份?
答:不能保证所有的数据都用到,省空间省时间。
写实拷贝–>按需申请–>通过拷贝的时间顺序(有需则拷贝,哪个先需要就先拷贝哪个),达到有效节省空间的目的

如何理解地址空间

什么是划分区域

在这里插入图片描述

地址空间本质是内核的一个struct结构体,内部有很多的属性都表示栈、堆等[start,end]的范围
源代码:
在这里插入图片描述

地址空间的理解

故事:
钟离,有10个亿摩拉。
某一天,跟胡桃说好好干,要是干不下去了可以找他要钱,有10个亿
偶遇魈在除魔:好好干,工资少不了你的,少啥装备找我要,有10个亿
又在群玉阁遇到了凝光:努力工作,需要啥材料跟我提,我有10个亿
在这里插入图片描述
钟离给每一个人,画了一个大饼[0,10亿],让每个人以为都可以使用这10亿
大饼画多了,要不要把饼管理起来呢?
要的,比如你老板昨天跟你说,干得好,双倍工资。今天告诉你,干得好,让你干小组长。露馅了
其中钟离(操作系统),10亿(物理内存),胡桃魈…(进程),画的大饼(进程地址空间)

为什么要有地址空间

如果进程直接指向物理内存:堆和栈都放在不同的空间,如果再多放几个进程,物理内存内部很乱,管理起来复杂,容易产生越界
在这里插入图片描述
加了地址空间和页表:这样无论物理内存怎么乱,都可以通过页表映射找到
在这里插入图片描述
地址空间和页表的第一个好处:将无序变成有序,让进程以统一的视角看待物理内存以及自己运行的各个区域
两个例子:

在这里插入图片描述

第二个好处:进程管理模块和内存管理模块进行解耦。提高内存效率
第三个好处:拦截非法请求
在这里插入图片描述
通俗一点:张三同学的压岁钱被父母报关,怕其乱花钱
在这里插入图片描述
所以拦截非法请求–>对物理内存进行保护

进一步理解页表和写实拷贝

页表也不简单:
在这里插入图片描述
以进程挂起为例:
在这里插入图片描述
关于页表中的rwx权限有一个典型的例子:
为什么字符常量字符串不能被修改?
在这里插入图片描述
写实拷贝:
回到最开始的代码:
在没有子进程之前,g_val的权限是rw(可读可写的)
创建子进程后,OS把权限调整成r
在这里插入图片描述
当父子进程识别到有写入g_val的时候,OS识别到错误
要进行判断:
1.是不是数据不在物理内存–>缺页中断(虚拟地址存在合法,物理内存不在,是否标记为为0,重新开辟内存进行映射)
2.是不是数据需要写实拷贝–>发生写时拷贝
3.如果都不是,进行异常处理

如何理解虚拟地址

浅提一个问题:
在最开始的时候,地址空间和页表里面的数据从哪里来?
程序没有加载到内存里面之前,单纯变成二进制的时候,程序中本身就有地址
查看反汇编:

objdump -S [文件名]

在这里插入图片描述
那程序里面的地址是什么地址?
就是虚拟地址(逻辑地址)
在形成可执行程序的时候
就已经天然的形成一个个段了
每一个段都有起始地址
在对整个代码编译时,每一个代码已经被编过址了
编址就是采用的虚拟地址
每一个区域就有了它天然的虚拟地址
所以进程每一次加载到内存里面,都可能是不同的位置
但内部使用的地址是不变的
所以地址空间和页表直接从数据读来
这种编译模式叫做平坦模式

fork的原理:

经典的问题:fork为什么会有两个返回值?
测试代码:

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

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        while(1)
        {
            printf("I am child,%d,%p\n",id,&id);
            sleep(1);
        }
    }
    else if(id > 0)
    {
        while(1)
        {
            printf("I am father,%d,%p\n",id,&id);
            sleep(1);
        }
    }
    return 0;
}

地址一样,典型的虚拟地址
在这里插入图片描述
在这里插入图片描述