【Linux】进程周边006之进程地址空间

这篇具有很好参考价值的文章主要介绍了【Linux】进程周边006之进程地址空间。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

 【Linux】进程周边006之进程地址空间,Linux,linux,运维,服务器

👀樊梓慕:个人主页

 🎥个人专栏:《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C++》《Linux》

🌝每一个不曾起舞的日子,都是对生命的辜负


目录

前言

1.程序地址空间

1.1验证地址空间的排布

 1.2利用fork函数观察当子进程修改某个共享变量时父子进程分别读取到的值和地址

2.进程地址空间

2.1操作系统是如何建立起进程与物理内存之间的联系的呢? 

2.2什么是进程地址空间?

2.3为什么有进程地址空间和页表

2.4malloc和new开辟空间的原理

2.5页表与写时拷贝的更多细节


前言

在之前学习进程概念时我们提到过fork函数,了解了如何创建进程,并且知道了fork之后的父子进程代码共享,当父子对共享的变量做修改时会拷贝一份到自己这再做修改(写时拷贝),但当时对于一个变量为什么能有两个值我们的讲解仍然十分局限,今天在学习完进程地址空间后,我想你就会明白原因所在。

 欢迎大家📂收藏📂以便未来做题时可以快速找到思路,巧妙的方法可以事半功倍。

=========================================================================

GITEE相关代码:🌟fanfei_c的仓库🌟

========================================================================= 


1.程序地址空间

在之前学习内存管理时我相信你一定见过这张图:

【Linux】进程周边006之进程地址空间,Linux,linux,运维,服务器


 当时我们说这是底层物理内存的分布,那今天我可能要告诉你他其实并不是,而只是操作系统创造出来的一个虚拟的结构,而真实的物理内存分布其实并不是如此。

但正式开始之前:我们还是来验证一下数据是不是按如图所示进行排列的呢?


1.1验证地址空间的排布

int g_unval;
int g_val = 100;

int main(int argc, char *argv[], char *env[])
{
    printf("code addr:\t%p\n", main);//验证正文代码
    printf("init data addr:\t%p\n", &g_val);//验证初始化数据(全局)
    printf("uninit data addr: %p\n", &g_unval);//验证未初始化数据(全局)

    char *heap = (char*)malloc(20);//如图先创建的动态内存应该在堆底
    char *heap1 = (char*)malloc(20);//所以heap的地址应为最小
    char *heap2 = (char*)malloc(20);//heap3的地址应为最大
    char *heap3 = (char*)malloc(20);//一会观察是否是这样

    printf("heap addr: %p\n", heap);//验证堆区(动态内存)
    printf("heap1 addr: %p\n", heap1);
    printf("heap2 addr: %p\n", heap2);
    printf("heap3 addr: %p\n", heap3);

    printf("stack addr: %p\n", &heap);//验证栈区(指针变量)
    printf("stack addr: %p\n", &heap1);//如图先创建的heap指针应该在栈空间中地址最大
    printf("stack addr: %p\n", &heap2);//所以&heap应为最大
    printf("stack addr: %p\n", &heap3);//&heap3应为最小

    for(int i = 0; argv[i]; 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;
}

 打印出来看看是不是这样呢?

【Linux】进程周边006之进程地址空间,Linux,linux,运维,服务器

补充知识:当一个变量被定义为static变量时,其实该变量的地址就被放到了全局变量的区域,他在某种意义上来讲就是全局变量,但是由于编译器的原因会对他进行语法上的检查等,才呈现出了静态变量的特性。 


 1.2利用fork函数观察当子进程修改某个共享变量时父子进程分别读取到的值和地址

既然我们之前在进程概念的学习中创建过子进程,那我们刚好可以观察一下当子进程修改某一共享变量时,父子进程读取到的该变量的值是否会发生改变,该变量的地址又呈现出什么样的内容?

int g_val = 100;

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        int cnt = 0;
        //子进程
        while(1)
        {
            printf("child, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", 
                    getpid(), getppid(), 
                    g_val, &g_val);//获取子进程信息以及变量g_val的值与地址
            sleep(1);
            cnt++;
            if(cnt == 2)//2s后修改全局变量g_val的值为200
            {
                g_val = 200;
                printf("child change g_val: 100->200\n");
            }
        }
    }
    else
    {
        while(1)
        {
            printf("father, pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", 
                    getpid(), getppid(), 
                    g_val, &g_val);//获取父进程信息以及变量g_val的值与地址
            sleep(1);
        }
    }
}

解析代码:2秒之前父子进程读取变量g_val的值,2秒后子进程对该变量进行修改,观察修改之后父子进程读取该变量的值如何变化,并且是否符合我们之前所讲的写时拷贝,是否会拷贝一份给自己再修改?

【Linux】进程周边006之进程地址空间,Linux,linux,运维,服务器


我们发现确实,当子进程对变量进行修改时,子进程对应的g_val发生了改变,而父进程没有改变,进程之间确实具有独立性。

可是最令人费解的是,父子进程读取该变量的地址竟然相同!?

这也就证实了之前我们所学习的所谓的内存分布图是假的,打印出来的地址也是假的,因为如果是物理内存地址,同一物理地址是不可能存放两个值的!!

结论:

  • 我们所有用到的语言上的地址,都不是物理地址,而是虚拟地址(线性地址)
  • 此图不是物理内存分布图,而是进程地址空间分布图。

2.进程地址空间

现在你就知道了文章开头给出的图片根本不是什么物理内存分布图,而是进程地址空间分布图。

完了,我们之前所学被颠覆了,那物理内存到底在哪里啊,进程是如何访问到物理内存的?

所以我们继续往下看:


2.1操作系统是如何建立起进程与物理内存之间的联系的呢? 

首先:每一个进程都会存在一个进程地址空间,操作系统如何管理这些进程地址空间呢?

先描述,再组织。

所以进程地址空间本质上就是一种数据结构,PCB中会有一个指针指向该数据结构,该数据结构中存储的就是对应的虚拟地址,所以操作系统对进程地址空间的管理也就变成了对该数据结构的管理。

另外操作系统会为我们维护一张映射表:页表

  • 该表中存储的就是虚拟地址与物理地址,通过虚拟地址就可以找到物理地址,也就建立起来了进程与物理内存的联系。

【Linux】进程周边006之进程地址空间,Linux,linux,运维,服务器 当创建子进程时,子进程会继承父进程的进程地址空间、页表等

所以我们说父子进程代码共享,数据共享,是因为他们的页表是相同的。

但对共享的变量进行修改时,会发生写时拷贝,拷贝到的代码和数据也是新开辟在物理内存上的,此时子进程只需要修改页表,虚拟地址不变,而物理地址则是新开辟的物理地址

【Linux】进程周边006之进程地址空间,Linux,linux,运维,服务器

所以才会出现虚拟地址相同,而物理地址不同的情况。


2.2什么是进程地址空间?

每一个进程都会存在一个进程地址空间,在32位操作系统下,该空间的大小为[0,4]GB。 

上面说到:进程地址空间其实就是一个数据结构,那该数据结构中都存在有哪些内容呢?

查看Linux内核源码:

【Linux】进程周边006之进程地址空间,Linux,linux,运维,服务器


我们找到mm_struct的定义:

struct mm_struct
{
    struct vm_area_struct* mmap;    
    struct rb_root mm_rb;           
    struct vm_area_struct* mmap_cache;    

    //....

    unsingned long start_code, end_code, start_data, end_data;  
    //代码段的开始start_code ,结束end_code,数据段的开始start_data,结束end_data

    unsigned long start_brk, brk, start_stack;    
    //start_brk和brk记录有关堆的信息,
    //start_brk是用户虚拟地址空间初始化,
    //brk是当前堆的结束地址,
    //start_stack是栈的起始地址

    unsigned long arg_start, arg_end, env_start, env_end;     
    //参数段的开始arg_start,结束arg_end,
    //环境段的开始env_start,结束env_end

}

 那么如何理解各个数据存放的区域呢,如上面的源码所示:就是利用首尾的位置信息。

通过这些信息我们就可以:

  • 判断是否越界
  • 可以进行扩大和缩小范围 

区域划分的本质就是区域内的地址我们可以使用。 

可是我们又知道进程地址空间是不具备保存实际的代码和数据的能力的。

这些代码和数据实际是放置在物理内存上的。

所以就需要页表的存在来将虚拟地址转化为实际的物理内存地址

那转化的工作是谁来做呢?

  • 粗浅的说是CPU,在转化的过程中,CPU中的CR3寄存器会记录页表的地址(注意:CR3中存储的地址一定是真实的物理地址,如果是虚拟地址,那CPU还不知道页表在哪,那怎么通过映射关系找到CR3中虚拟地址映射到实际的物理地址呢),当CPU开始执行正文代码时,假设遇到了a++这样的指令,那么CPU就会根据CR3寄存器中页表的地址进行查表,从而就得到了物理内存地址,也就找到了a的值。
  • 准确的说,这个转化工作是由CPU中的硬件单元MMU(内存管理单元)完成的。

2.3为什么有进程地址空间和页表

  • 因为有了进程地址空间和页表,物理内存空间上不连续、无序的空间就可以通过页表这一映射关系联系在一起,让进程以统一的视角看待内存。
  • 有了进程地址空间和页表后,每个进程都认为自己在独占内存,这样能更好的保障进程的独立性以及合理使用内存空间(当实际需要使用内存空间的时候再在内存进行开辟),并能将进程管理与内存管理进行解耦合。
  • 地址空间+页表的设计是保护内存安全的重要手段!

2.4malloc和new开辟空间的原理

在之前的学习中,我们不知道进程地址空间的概念,所以malloc和new开辟空间我们总是默认为内存上的操作,而学习完进程地址空间后,你会发现并不是如此。

当代码执行到malloc和new时,OS不一定会直接将实际的物理内存分配给你,因为该进程可能不会立即使用该块内存,也就造成了内存浪费,OS一定要确保效率和资源使用率,所以OS给你分配的实际上是进程地址空间,地址也是虚拟地址,而且并不会在页表上建立有效的映射关系。

当检测到该进程实际要使用该块空间时(写入修改之类的操作,读取不算),会发生缺页中断然后立即在页表中建立映射关系,此时该进程需要的物理内存空间才被申请。

这样做有什么好处呢?

  • 充分保证内存的使用率,不会造成空转;
  • 提升new或malloc的速度(因为没有实际在内存上开辟空间)。

2.5页表与写时拷贝的更多细节

 页表其实不光存放虚拟地址和物理内存地址,还有其他的属性,比如会存放权限属性。

【Linux】进程周边006之进程地址空间,Linux,linux,运维,服务器

什么意思呢?

我们平时写代码时常量不可修改究竟是谁决定的?

  • 其实就是操作系统在页表中该数据的权限属性上放置的是'r',当你要对该数据进行修改时(写入)时,首先需要进行虚拟地址与物理地址的转化,转化的过程中操作系统发现权限为只读,所以才不可修改不可写入。

那const修饰的数据是不是也是由页表决定的呢?

  • 不是const与系统没有任何关系,const是编译器检查前后语法的问题。const的意义是将可能在未来运行时出现的错误提前在编译阶段发现并报错。所以我们说const能加则加,是一种好的编程习惯,防御性编程。

你知道操作系统是如何知道什么时候进行写时拷贝的呢?

在父进程创建子进程时,按之前所学子进程会继承父进程的进程地址空间和页表。

并且操作系统还会将父子进程的页表中数据对应的权限属性修改为只读!

当父或子进程修改(写入)该数据时,会发生缺页中断,但其实缺页中断做的工作不仅会在物理内存上开辟空间建立映射关系,还会对我们的访问操作做判断:

  • 操作系统会判断,页表权限为只读,但数据所在的进程地址空间属于可读可写的数据区,操作系统明白了,这是要写时拷贝啊!

所以这就是操作系统判断什么时候进行写时拷贝的原理,根据这个方法,操作系统就能实现按需拷贝!

谁要使用(写入)给谁开辟新的物理空间,否则就不拷贝,共用物理内存空间。 


=========================================================================文章来源地址https://www.toymoban.com/news/detail-761889.html

如果你对该系列文章有兴趣的话,欢迎持续关注博主动态,博主会持续输出优质内容

🍎博主很需要大家的支持,你的支持是我创作的不竭动力🍎

🌟~ 点赞收藏+关注 ~🌟

=========================================================================

到了这里,关于【Linux】进程周边006之进程地址空间的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 浅析Linux进程地址空间

    现代处理器基本都支持虚拟内存管理,在开启虚存管理时,程序只能访问到虚拟地址,处理器的内存管理单元(MMU)会自动完成虚拟地址到物理地址的转换。基于虚拟内存机制,操作系统可以为每个运行中的进程创建独享的虚拟地址空间,在这个空间中执行的程序,无法感知

    2024年01月20日
    浏览(39)
  • Linux--进程地址空间

    1.线程地址空间 所谓进程地址空间(process address space),就是从进程的视角看到的地址空间,是进程运行时所用到的虚拟地址的集合。 简单地说,进程就是内核数据结构和代码和本身的代码和数据,进程本身不能访问物理地址,之时候就需要有一个中间媒介,就是地址空间,

    2024年02月11日
    浏览(44)
  • 【Linux】理解进程地址空间

    🍎 作者: 阿润菜菜 📖 专栏: Linux系统编程 ​我们在学习C语言的时候,都学过内存区域的划分如栈、堆、代码区、数据区这些。但我们其实并不真正理解内存 — 我们之前一直说的内存是物理上的内存吗? 我们先看一段测试代码: 运行结果: 我们可以注意到子进程的变量

    2024年02月02日
    浏览(53)
  • 『 Linux 』进程地址空间概念

    在c/C++中存在一种 内存 的概念; 一般来说一个内存的空间分布包括 栈区 , 堆区 , 代码段 等等; 且内存是 自底向上 (由 0x00000000 至 0xFFFFFFFF ); 以该图为例: 该图即为常见的内存分布图; 正文代码段 正文代码段所存放的数据 一般为函数体的二进制代码 ; 已初始化数据区 已初始化

    2024年02月03日
    浏览(45)
  • 【Linux】—— 进程地址空间

    序言: 在上篇中,我们讲解了关于进程优先级的概念。本期,我将给大家介绍的是关于进程地址空间的话题。 目录 (一)程序地址空间回顾 (二)代码演示 (三)进程地址空间的引入 总结 我们在学习C/C++语言的时候,大家可能都见过这样的空间布局图: 一个程序有哪些

    2024年02月15日
    浏览(41)
  • Linux:进程地址空间

    目录 1.程序地址空间  2.进程地址空间 我们在讲C/C++语言的时候,32位平台下,我们见过这样的空间布局图 我们来验证一下这张图的正确性: 运行结果: 通过观察静态变量的位置,可以认为静态变量就是全局变量,只是静态变量只初始化一次,有作用域的限制。 这里栈区还

    2024年02月04日
    浏览(40)
  • 【Linux】深挖进程地址空间

    作者简介:დ旧言~,目前大二,现在学习Java,c,c++,Python等 座右铭:松树千年终是朽,槿花一日自为荣。 目标:熟悉【Linux】进程地址空间 毒鸡汤:也许有一天,你发觉日子特别的艰难,那可能是这次的收获特别的巨大。 望小伙伴们点赞👍收藏✨加关注哟💕💕      

    2024年02月03日
    浏览(38)
  • 【Linux】进程理解与学习Ⅳ-进程地址空间

    环境:centos7.6,腾讯云服务器 Linux文章都放在了专栏:【 Linux 】欢迎支持订阅 🌹 相关文章推荐: 【Linux】冯.诺依曼体系结构与操作系统 【Linux】进程理解与学习Ⅰ-进程概念 浅谈Linux下的shell--BASH 【Linux】进程理解与学习Ⅱ-进程状态 【Linux】进程理解与学习Ⅲ-环境变量 在

    2023年04月14日
    浏览(81)
  • 【Linux进行时】进程地址空间

    我们在讲C语言的时候,老师给大家画过这样的空间布局图,但是我们对它不了解 我们写一个代码来验证Linux进程地址空间 这里没什么问题,就是他们的g_valule 和其地址都是一样的, 我们将代码调整一下,让子进程的g_value++ 我们可以发现子进程的g_value变了,但是父进程没有

    2024年02月08日
    浏览(41)
  • 【Linux取经路】初探进程地址空间

    之前在介绍 fork 函数的时候说过该函数返回了两次,至于为什么会返回两次,以及 fork 函数是如何做到返回两次的,在【Linux取经路】揭秘进程的父与子一文中已经做了详细的解释,忘了小伙伴可以点回去看看。在解释一个变量怎么会有两个不同值的时候,当时的说法是由于

    2024年01月21日
    浏览(50)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包