• 程序地址空间


    1.程序地址空间

    1.1 概念

    32位平台下程序地址空间的大小是2^32,指针是4个字节,原因也是因为刚好是32个比特位。

    注:正文代码的地址空间并不是最底层。

    image-20220827091805042

    第一个问题,我们以前学的程序地址空间是内存吗?程序地址空间不是物理内存!程序地址空间准确的来说应该是进程地址空间,它是操作系统上的概念!

    1.2 验证

    验证:我们发现地址是依次增加的,且堆和栈之间有很大的地址镂空!

    image-20220827094906521

    验证堆和栈的增长方向:堆区向高地址增长,栈区向低地址增长!栈区先定义的地址较高!

    image-20220827095346402

    1.3 static变量

    函数内定义的变量用static修饰本质是编译器把该变量编译进全局数据区,但作用域还是限制于该函数的代码块内。

    2. 感知地址空间的存在

    我们知道当父子进程没有人修改全局数据的时候,父子进程是共享该数据的!

    如果尝试写入呢?

    image-20220827113133543

    父子进程读取同一个变量(因为地址一样!),但是后续没有人修改的情况下,父子进程读取到的内容却不一样!!!

    image-20220827113522854

    结论:我们在C/C++中使用的地址,绝对不是物理地址。如果是物理地址,这种现象不可能产生!!!那就应该是虚拟地址

    在Linux中,虚拟地址也叫作线性地址或者逻辑地址。

    为什么我的操作系统不让我直接看到物理内存呢??因为不安全。

    内存就是一个硬件,不能阻拦你访问!只能被动的进行读取和写入!

    3. 虚拟进程地址空间

    3.1 引入

    每一个进程在启动的时候,都会让操作系统给他创建一个地址空间,该地址空间就是进程地址空间。

    每一个进程都会有一个自己的进程地址空间!!

    操作系统要不要管理这些进程地址空间呢??我们说了管理就是先描述再组织。实际上进程地址空间就是内核的一个数据结构,struct mm_struct。

    image-20220827115204084

    3.2 什么是进程地址空间

    我们已经说进程具有独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰!

    无非就是进程相关的数据结构是独立的,进程的代码和数据是独立的。

    假设身价10亿的富翁有3个私生子,为了不让3个儿子因为家产产生纠纷,富翁就骗每个儿子都是自己的独子,且为了维护3人之间的独立性,给每个儿子都制定了目标,只要达到目标就能继承10亿家产。

    那么这里的富翁就是操作系统,三个私生子就是进程,富翁给三个私生子画的大饼就是虚拟进程地址空间。

    所以进程地址空间是一个逻辑上抽象的概念:让每一个进程都认为自己是独占系统中所有资源的!!

    所谓的地址空间:其实就是OS通过软件的方式,给进程提供了一个软件视角,认为自己会独占系统的所有资源(主要是内存)

    每一个进程的PCB(task_struct)内都有一个mm_struct,内部就是我们所说的虚拟进程地址空间,所谓的栈区、堆区、数据段等区域都是通过链表来连接的,每个链表结构中有相应的虚拟地址编号(start、end)来划分栈、堆等区域。然后通过页表找到物理地址,最后对磁盘进行写入。

    页表是程序加载到内存变成进程后操作系统自动构建的,里面记录的是虚拟地址和物理地址之间的映射关系。

    所以看起来每个进程的不同区域的虚拟地址(栈,堆)都一样,实际上物理空间不一样。

    image-20220827125243976

    4. 程序是如何变成进程的?

    程序被编译出来,没有被加载的时候,程序内部有地址吗?有

    程序被编译出来,没有被加载的时候,程序内部有没有区域(堆、栈等)划分?有

    readelf -S 可执行程序
    
    • 1

    这条命令可以查看程序内部的区域划分

    我们的可执行程序编译好后,在磁盘就已经有自己的一套地址和区域划分了,只不过堆区、栈区加载在内存时才有。程序内部的地址是编译器给的(按照逻辑地址的形式),在Linux中逻辑地址 = 线性地址 = 虚拟地址。即从全0到全F编址。

    内存也是按照虚拟地址的形式编址,磁盘中程序的代码,全局变量等全部都是数据,加载到内存就有了物理地址,这个物理地址可以理解为"外部的"。就好比,你内部有一套地址,你坐进教室的座位上,座位上天然就有编号,你就自然又多了一套编号地址,用来找你在内存(教室)的什么位置。这个就叫物理地址。

    假设操作系统直接通过物理地址找到程序在内存的位置,读取到程序的代码,但是程序的代码本身用的是编译器给的地址,CPU不认识啊。所以不能这样。

    所以CPU执行程序的代码从内存中读取到的是程序内部的逻辑地址,就需要内核的虚拟进程地址空间通过页表来映射程序加载到内存的数据的物理地址。

    因为Linux和编译器使用的是同一套编址方式,所以在内存中看待程序就和磁盘中看待程序一样了。这样,只要页表映射好,只需要关心对应的资源在还是不在内存中,在就用,不在直接就能找到程序在磁盘的位置加载到内存中。

    image-20220827184712418

    5. 解答父子进程读取同一个变量数据不同的问题

    因为当父进程被创建,父进程有自己的task_struct,task_struct会指向虚拟进程地址,虚拟进程地址自动构建页表映射到物理地址。

    而fork创建子进程的这一套数据一般是继承父进程的数据,因为PCB是以父进程为模板。

    所以在全局变量没有写入的时候它们访问的变量是同一个,因为页表映射的物理地址是同一个。

    如果此时子进程把这个变量的数据改了,那么就有可能导致父进程出现问题,子进程就影响了父进程。

    我们知道进程具有独立性,当OS识别到子进程要修改这个全局变量的时候,操作系统会重新为子进程的这个变量重新开辟一段空间,所以子进程就不会指向父进程变量的地址了,而是指向新开辟的空间的地址,在此空间进行修改。

    所以在子进程修改变量时永远改变的是页表右侧(也就是物理地址),改变的是虚拟地址和物理地址的映射关系,而左侧不变。所以最终看到的虚拟地址一样,但是变量的数据却不一样!

    image-20220827193305893

    这个过程就叫作写时拷贝:当父子有任何一个进程尝试去修改对应的变量之后,修改的那一方就会发生写时拷贝,让它重新去拷贝一份物理内存,改变的只是映射关系(物理地址),虚拟地址并不会发生改变!

    通过页表,父子进程就可以通过写时拷贝分离数据,达到父子进程具有独立性的特点!

    6. fork有两个返回值,pid_t同一个变量怎么会有不同的值?

    pid_t id 是属于父进程栈空间中定义的变量,fork内部,return会被执行两次,return的本质就是通过寄存器将返回值写入到接受返回值的变量中!

    当id=fork()的时候,谁先返回,谁就要发生写时拷贝,所以同一个变量会有不同的内容值,本质是因为大家的虚拟地址是一样的,但是大家对应的物理地址是不一样的!!

    7. 为什么要有虚拟地址空间

    如果直接访问物理地址,多个程序加载到内存时,如果一个进程发生的野指针问题,那么就会影响另一个进程,这是不安全的,同时也不符合进程具有独立性的特点!

    如果通过虚拟地址的方式,访问内存添加了一层软硬件层,可以对转化过程进行审核,非法的访问,就可以直接拦截了!

    使用虚拟进程的优点:

    1. 保护内存!
    2. 进程管理:Linux内存管理通过程序地址空间进行功能模块的解耦!
    3. 让进程或者程序可以用一种统一的视角看待内存!方便以统一的方式来编译和加载所有的可执行程序,简化进程本身的设计与实现!
  • 相关阅读:
    vue 免费的每天不限次数的调用天气接口
    macos上安装配置emsdk的问题
    【20220901】What Happened When We All Stopped?
    轻松学习 Spring 事务
    C++_linux下_非阻塞键盘控制_程序暂停和继续
    win10 环境下Python 3.8按装fastapi paddlepaddle 进行身份证及营业执照的识别2
    maven离线模式及设置
    关于 Ceph 的一些维护工作总结
    Linux 下的 input 子系统开发框架
    关于 SAP UI5 所有控件的共同祖先 - sap.ui.base.ManagedObject
  • 原文地址:https://blog.csdn.net/iwkxi/article/details/126593754