【Linux篇】第八篇——进程地址空间

简介: 【Linux篇】第八篇——进程地址空间

程序地址空间


之前在学习C语言期间,经常听到提及这些区域,我问大家一个问题:这里的地址空间是内存嘛?答案:并不是,这里的地址空间是进程地址空间(后面详细讲)。

image.png

我们通过代码来证明上面的地址空间分布图:

image.png

运行结果:这段空间中自下而上,地址是增长的,栈是向地址减小方向增长(栈式先使用高地址),而堆是向地址增长方向增长(堆是先使用低地址),堆栈之间的共享区,主要是用来加载动态库的。

image.png

更深一步了解,做了如下测试:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
int a=5;
int main()
{
     pid_t ret=fork();
    if(ret==0)
    {
      while(1)
      {
        printf("I am child:pid:%d  %d:%p\n",getpid(),a,&a);
        sleep(1);
      }
    }
    else{
      while(1)                                                                                                                                                                                                  
      {
        printf("I am parent:ppid:%d  %d:%p\n",getppid(),a,&a);
        sleep(1);
      }
    }
    return 0;
  }

代码运行结果如下:

image.png

父子进程中对于同一个变量打印的地址是一样的,这是因为子进程以父进程为模板,因为都没有对数据进行修改,所以这里变量地址也是一样的。

对代码进行修改如下:

image.png

可以发现,子进程对数据进行修改后,父子进程打印a的值不一样的,但是地址却是一样的。

分析结果得出

在创建子进程那里说过,父子进程共享代码和数据,但是如果有一方队数据做出了修改,那么修改方将堆被修改的这一份数据写实拷贝,这一份数据各自私有,所以根据我们现有的知识可以推断出变量a在父子进程中的地址不一样的,但是测试发现他们的这个变量地址是一样的,但是内容却不一样。

难道对于同一块空间可以有两份不同的数据吗?

答案是否定的。所以我们可以得出结论,我们上面看到的地址绝对不是物理地址。事实上,在Linux地址下,这种地址叫做虚拟地址(下面讨论)。且平时我们在C/C++语言打印所看到的地址都是虚拟地址,物理地址对于我们用户是看不到的,由OS管理。

进程地址空间


任何学过的语言,里面的地址都不会是物理地址,虚拟地址是由操作系统提供给我们的,操作系统如何给我们提供?既然这种地址是虚拟地址,那么一定有某种途径将虚拟地址转化为物理地址,因为数据和代码是在物理内存上,因为冯诺依曼规定任何数据在启动时必须加载到物理内存,所以肯定需要将虚拟地址转化为物理地址,这里的转化是由操作系统完成,所有的程序都必须运行起来,运行起来之后,该程序立即变成了进程,那么刚刚打印的虚拟地址大概率和进程有某种关系。


举个例子:上学期间,经常会和同桌去画三八线,比如一张课桌是100cm,我们用一把尺子去划分,女孩的区域是[0,50],男孩的区域是[50,100],那么我们在计算机中去叙述这个事情?定义如下:

struct area
{
  unsigned long start;
  unsigned long end;
};
struct area girl={0,50};
struct area boy={50,100};

此时我们划分了区域,这时不管是男孩还是女孩,大脑里都有了这样的区域:

image.png

当女孩觉得自己活动范围不够,想扩大自己的区域时,就可以调整自己认为的[start,end],划分三八线的过程,就是划分区域的过程,调整区域的过程,本质就是调整自己认为的[start,end]


其中我们将桌子认为是物理内存,男孩和女孩认为是每一个进程,而男孩和女孩本质上都认为自己有一把尺子(脑海里的尺子),这把尺子就是进程地址空间,男孩想放自己的书包、铅笔等物品时,男孩就在自己的进程地址空间再划分区域放自己的物品。


那么如何划分进程地址空间的区域呢?在Linux当中,进程地址空间本质上是一种数据结构,是多个区域的集合。


在Linux内核中,有这样一个结构体:struct mm_struct,其中对虚拟地址每个区域进行了对应的划分(从0x00000000到0xffffffff)它也是被放在PCB中管理起来的,可以通过PCB找到对应的进程地址空间。每个进程都要一个进程地址空间,也就是每个进程都认为自己独享内存资源。所以说这个进程地址空间其实是一个虚拟地址空间。如何在这个结构体去表示我们开始说的一个一个的区域呢?这样去表示:

struct mm_struct
{
    unsigned long code_start;//代码区
    unsigned long code_end;
    unsigned long init_start;//初始化区
    unsigned long init_end;
    unsigned long uninit_start;//未初始化区
    unsigned long uninit_end;
    unsigned long heap_start;//堆区
    unsigned long heap_end;
    unsigned long stack_start;//栈区
    unsigned long stack_end;
    //...等等
}

在上面的例子中,男孩脑海里有一把尺子,想着自己拥有桌子的一半,女孩脑海里也有一把尺子,想着也拥有桌子的一半,而此时我们改变了:男孩和女孩关系比较好,不进行划分三八线,男孩脑海里有一把尺子,想着自己拥有0-100cm的桌子,女孩脑海里有一把尺子,想着自己也拥有0-100cm的桌子,他们在放东西的时候,只要记住了尺子的刻度就可以了。


我们的进程地址空间就相当于那把尺子,而尺子是有刻度的,进程地址空间也是从0x00000000到0xffffffff,可以在上面进行区域划分:比如代码区[code_start,code_end];那么区间的每一个地址单位就称为虚拟地址。


总结:

进程地址空间本质是进程看待内存的方式,抽象出来的一个概念,内核:struct mm_struct,这样的每个进程,都认为自己独占系统内存资源(每个私生子都认为自己独占10亿家产),地址空间区域划分本质:将线性地址空间划分成为一个一个的area,[start,end]。虚拟地址本质,在[start,end]之间的各个地址叫做虚拟地址

虚拟地址空间每个进程存放的是一个虚拟地址空间,OS会将虚拟地址通过某种映射关系映射到对应的物理地址空间,从而得到自己的那一份数据。不同进程的虚拟地址可以完全一样嘛?答案是可以完全一样,因为每个进程都有各自的页表,每个进程都是独立的进行通过各自页表中虚拟地址和物理内存的映射关系去找代码和数据:

image.png

父子进程各种有一份虚拟空间地址,在子进程刚被创建时,父子进程代码和数据共享,所以此时虚拟地址空间的内容是基本一样的(当然有部分数据不同,比如各子的id等),且映射关系也是一样的,但是当子进程对数据进行修改时,子进程对那份数据进行写时拷贝,所以物理空间地址发生了变化,但是虚拟地址还是没有发生变化,只是改变了子进程的页表中那份虚拟地址的映射关系而已,所以两个相同的虚拟地址在父子进程分别看到了不同的物理地址空间。

 总结:虚拟地址和物理空间之间是通过页表完成的映射关系

为什么要存在地址空间


为什么进程不直接访问物理内存?这样不行吗?为什么要多此一举,存在虚拟地址空间去映射找到物理内存?

举个例子:小时候每当过年,我们都会收到压岁钱,妈妈都会要帮我们收起来,要买东西的时候再给我们,然而为什么不直接在我这,买的时候更方便囊?其实,在我们这有可能我们会被骗,会乱花钱等问题。

保护物理内存不受任何进程地址的直接访问,在虚拟地址到物理地址的转化过程中方便进行合法校验。

如果进程直接访问物理内存,那么看到的地址就是物理地址,而语言中有指针,如果指针越界了,一个进程的指针指向了另一个进程的代码和数据,那么进程的独立性,便无法保证,因为物理内存暴露,其中就有可能有恶意程序直接通过物理地址,进行内存数据的篡改,如果里面的数据有账号密码就可以改密码,即使不让改,也可以读取。

  • 将内存申请和内存使用的概念在时间上划分清楚,通过虚拟地址空间来屏蔽底层内存申请的过程(进程也不再关心该过程),从而达到进程和OS进行内存管理操作,进行进程调度和内存管理进行解耦

如果没有进程地址空间,进程直接访问物理内存,当进程退出时,内存管理需要尽快将该进程回收,在这个过程当中必须得保证内存管理得知道某个进程退出了,并且内存管理也得知道某个进程开始了,这样才能给他们及时得分配资源和回收资源,这就意味着内存管理和进程管理模块时强耦合得,也就是说内存管理和进程管理关系比较大,通过我们上面得理解,如果有了进程地址空间,当一个进程需要资源的时候,通过页表映射去要就可以了,内存管理就只需要知道哪些内存区域是无效的,哪些是有效的(被页表映射的就是有效的,没有被页表映射的就是无效的),当一个进程退出时,它的映射关系也就没了,此时没有了映射关系,物理内存这里就将该进程的数据设置为无效,所以第二个好处就是将内存管理和进程管理进行解耦,内存管理是怎么知道有效还是无效的?比如说在一块物理内存区域设置一个计数器count,当页表中有映射到这块区域时,count就++,当一个映射去掉时,就将count--,内存管理只需要检测这个count是不是0,如果是0,说明它是没人用的。

  • 虚拟地址空间可以将空间连续化,降低了异常越界访问的概率

磁盘上的可执行程序分区域的每个大小单位为4KB,每个这个大小的数据称为页帧,在物理内存中的每个大小单位也为4KB,每个这个大小的数据称为页框,那么为什么要分区域呢?因为方便生成可执行程序,在这之后其中有一个链接库的过程,如果可执行程序是乱的,那么这个链接过程非常困难,所以需要分好区域,由此进程地址空间才有了区域划分这样的概念,进程的地址空间连续化,也让顺序语句的执行成为了可能:当前语句的起始地址+当前代码的长度就等于下一条语句的地址。如果没有进程地址空间,因为物理内存空余的地方不一定是连续的空间,可能是零散的,那么将可执行程序的数据加载到内存当中时,那么这些数据就是零散的放在各个位置,而这些位置我们又是不确定的,此时很难找到代码和数据的位置了,进程地址空间的存在,进程地址空间又是进行区域划分的,通过页表的映射关系可以很好的找到物理内存,所以这也是存在地址空间的一个理由:让每个进程,以同样的方式(虚拟地址),明确程序运行的地址

  • 让每个进程,以同样的方式(虚拟地址),看待代码和数据,明确程序运行的地址

有了虚拟地址空间,每个进程认为自己独享整个内存资源。每个进程将以相同的方式看待内存,这样就大大地提高了操作系统的工作效率。举例:CPU在对每个进程中的代码执行时,要找到代码的起始地址,且只需要查找固定的虚拟地址,因为对于不同的进程地址空间有不同的映射关系,所以这个固定的虚拟地址在不同的进程中会映射到不同的物理地址中,找到相关代码和数据,所以CPU可以很快地查找到程序运行的起始位置。

  • 站在CPU和应用层角度,看待内存的方式是统一的,且每个空间区域的相对位置是比较确定的。

回顾:为什么父进程和子进程的数据不一样,这个我们不意外,因为数据是私有的,但是地址却也是相同的,这是什么原因呢?到达这里我想这个问题已经显而易见了:

image.png

此时g_val的虚拟地址没有变化,而子进程的g_val的虚拟地址对物理内存地址的映射已经发生了变化,指向的数据区的g_val已经变为了100。


相关文章
|
8月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
301 67
|
7月前
|
Web App开发 Linux 程序员
获取和理解Linux进程以及其PID的基础知识。
总的来说,理解Linux进程及其PID需要我们明白,进程就如同汽车,负责执行任务,而PID则是独特的车牌号,为我们提供了管理的便利。知道这个,我们就可以更好地理解和操作Linux系统,甚至通过对进程的有效管理,让系统运行得更加顺畅。
215 16
|
7月前
|
Unix Linux
对于Linux的进程概念以及进程状态的理解和解析
现在,我们已经了解了Linux进程的基础知识和进程状态的理解了。这就像我们理解了城市中行人的行走和行为模式!希望这个形象的例子能帮助我们更好地理解这个重要的概念,并在实际应用中发挥作用。
143 20
|
6月前
|
监控 Shell Linux
Linux进程控制(详细讲解)
进程等待是系统通过调用特定的接口(如waitwaitpid)来实现的。来进行对子进程状态检测与回收的功能。
131 0
|
6月前
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
200 0
|
6月前
|
存储 Linux Shell
Linux进程概念-详细版(二)
在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。
133 0
|
6月前
|
Linux 调度 C语言
Linux进程概念-详细版(一)
子进程与父进程代码共享,其子进程直接用父进程的代码,其自己本身无代码,所以子进程无法改动代码,平时所说的修改是修改的数据。为什么要创建子进程:为了让其父子进程执行不同的代码块。子进程的数据相对于父进程是会进行写时拷贝(COW)。
179 0
|
8月前
|
JavaScript Linux Python
在Linux服务器中遇到的立即重启后的绑定错误:地址已被使用问题解决
总的来说,解决"地址已被使用"的问题需要理解Linux的网络资源管理机制,选择合适的套接字选项,以及合适的时间点进行服务重启。以上就是对“立即重启后的绑定错误:地址已被使用问题”的全面解答。希望可以帮你解决问题。
422 20
|
9月前
|
存储 Linux 调度
【Linux】进程概念和进程状态
本文详细介绍了Linux系统中进程的核心概念与管理机制。从进程的定义出发,阐述了其作为操作系统资源管理的基本单位的重要性,并深入解析了task_struct结构体的内容及其在进程管理中的作用。同时,文章讲解了进程的基本操作(如获取PID、查看进程信息等)、父进程与子进程的关系(重点分析fork函数)、以及进程的三种主要状态(运行、阻塞、挂起)。此外,还探讨了Linux特有的进程状态表示和孤儿进程的处理方式。通过学习这些内容,读者可以更好地理解Linux进程的运行原理并优化系统性能。
345 4
|
9月前
|
Linux Shell
Linux 进程前台后台切换与作业控制
进程前台/后台切换及作业控制简介: 在 Shell 中,启动的程序默认为前台进程,会占用终端直到执行完毕。例如,执行 `./shella.sh` 时,终端会被占用。为避免不便,可将命令放到后台运行,如 `./shella.sh &`,此时终端命令行立即返回,可继续输入其他命令。 常用作业控制命令: - `fg %1`:将后台作业切换到前台。 - `Ctrl + Z`:暂停前台作业并放到后台。 - `bg %1`:让暂停的后台作业继续执行。 - `kill %1`:终止后台作业。 优先级调整:
695 5

热门文章

最新文章