装载与动态链接——装载与进程(一)


相关文章:
静态链接——编译和链接(一)
静态链接——目标文件(二)
静态链接——静态链接(三)
装载与动态链接——装载与进程(一)
装载与动态链接——动态链接(二)

本文主要是《程序员的自我修养》一书的内容摘要和梳理,如有需要并且没有被本文涵盖的内容,建议读者自行观看原书。

前面主要写了 3 篇文章来介绍书中对程序的静态链接的相关内容,接下来系列开启对动态链接的学习。

在介绍动态链接的内容之前要先介绍可执行文件的装载,看看可执行文件的装载的本质是什么。

进程虚拟地址空间

程序与进程
这个算是比较基础的概念区别,程序是一个静态的概念,它是一些预先编译好的指令和数据集合的一个文件;进程则是一个动态的概念,它是程序运行时的一个过程。

程序和进程概念跟做菜相比较的话,那么程序就是菜谱,计算机的 CPU 就是人,相关的厨具则是计算机的其他硬件,整个炒菜的过程就是一个进程。计算机按照程序的指令把输入数据加工成输出数据,就好像菜谱指导人把原料做成美味可口的菜肴。

虚拟地址空间
程序被运行起来以后,都会有自己独立的虚拟地址空间。这个地址空间的大小由计算机硬件平台决定的,如 32 位 CPU 理论有 4GB 的虚拟空间。

以 32 位 CPU 为例,一般整个 4GB 会被操作系统本身用去一部分,Linux 平台操作系统会分配 1GB, Windows 则是会分配 2GB。

从硬件层面,32 位的地址线最多只能访问 4GB 的物理内存,程序使用的空间能不能超过 4GB 呢?

这个问题应该从两个方面来看,首先,“空间”如果是指虚拟地址空间,答案是“否”,如果“空间”指计算机的内存空间,答案是“是”。

操作系统会修改地址映射算法来访问更多内存,但解决之法还是使用 64 位处理器。

装载的方式

程序执行时所需要的指令都必须在内存中才能够正常运行,最简单的方法就是把程序所需要的指令和数据全部都装进内存中,但是很多情况下程序所需要的内存数量大于物理内存的数量。

后来研究发现,程序运行时是有局部性原理的,所以我们可以将程序最常用的部分驻留在内存中,不太常用的数据放在磁盘里,这就是动态装入的基本原理。

覆盖装入

覆盖装入是早期还没有虚拟内存时代的产物,其把内存分配的任务给到了程序员,程序员在编写代码的时候必须手工把程序分割成若干块,然后编写一个小的辅助代码来管理这些模块如何驻留在内存中而何时应该被替换掉。
在这里插入图片描述
上图简单的例子,程序有 “main”,“A”,“B”,三个模块,其大小分别是 1024 字节,512 字节,256 字节。“main” 调用 “A”,“B” 两个模块,但是 “A”,“B” 之间没有调用关系,理论上需要 1024+512+256=1792 字节,但是如图采用覆盖载入的方法,“main” 用到 “A” 时把 “A” 加载到内存,用到 “B” 时,由于“A” 不会被用到,就把 “B” 装入原来 “A” 所占的内存空间,这样整个程序运行就只要 1536 字节。

实际情况往往比这个复杂,在多个模块的情况下,程序员需要手工将模块按照它们的调用依赖关系组织成树状结构。
在这里插入图片描述

  • 这个树状结构从任何一个模块到树的根模块都叫调用路径。当该模块被调用时,调用路径上的模块都必须在内存中。
  • 禁止跨树间调用。如 C 不能调用D,B,E,F。因为覆盖管理器不能保证跨树间的模块能够存在于内存中。
页映射

页映射是目前可执行文件装载的方式,把程序的数据和指令都按照页(Page)为单位划分,装载和操作的单位就是

不同页的选取有很多算法,就是操作系统中常介绍的,例如 FIFO 先进先出算法,LUR 最少使用算法等。

操作系统角度加载可执行文件

进程建立

进程最关键的特征是拥有独立的虚拟地址空间,操作系统创建一个进程,然后加载可执行文件并执行,分为以下三步:

  • 创建一个独立的虚拟地址空间:分配一个页目录(建立虚拟地址到物理地址的映射函数需要的结构)
  • 读取可执行文件头,建立虚拟空间与可执行文件的映射关系虚拟地址与可执行文件的映射关系,实际是创建映射函数所需要的数据结构。
  • 将 CPU 的指令寄存器设置成可执行文件的入口地址,启动运行:跳转到 ELF 文件头保存的入口地址
页错误

上述进程建立后,程序的指令和数据并没有装入内存,而是建立起了可执行文件 <——> 虚拟内存 之间的映射关系。当 CPU 打算执行程序时,发现虚拟内存里是一个空页,就会认为是一个页错误(Page Fault),然后 CPU 把控制权给操作系统,操作系统会查找映射关系,找到空页对应的 VMA ,计算出相应页在可执行文件中的偏移,然后在物理内存上分配一个物理页面,将进程中该虚拟页与分配的物理页之间建立映射关系,然后把控制权再还给进程,从页错误的地方重新开始执行。

进程虚存空间分布

段 Segment 与 Section

前面介绍的是一个段的情况,每个可执行文件会包含很多的段,如果加载的段的长度不是页的整数倍就会造成内存的浪费了。从操作系统的角度,它并不关心每个段的实际内容,它更关系的是段的操作权限,且操作权限情况有限:

  • 可读可执行:代码段
  • 可读可写:数据段
  • 只可读:只读数据段

所以操作系统在加载时,对于相同权限的段,会把它们合并到一起当作一个段映射,这里的段对应应为“segment”(合并前的段对应“section”)。

堆和栈

前面合并后每一个 Segment 都有一个 VMA 的结构来对这些段进行内存管理,进程在运行的时候还可能会需要栈(stack)和堆(heap),它们在进程的虚拟空间也是以 VMA 的形式存在的。

所以操作系统是通过给进程划分出一个个的 VMA 来管理进程的虚拟空间,一个进程可能包含下面几种 VMA 区域:

  • 代码 VMA :权限只读、可执行
  • 数据 VMA :权限可读写,可执行
  • 堆 VMA :权限可读写、可执行
  • 栈 VMA: 权限可读写、不可执行
段地址对齐

这里主要还是对每个段的长度不是页长度的整数倍引起的内存浪费进行进一步优化,有一些 UNIX 操作系统是让各个段临接的部分可以共享一个物理页面的,然后将该物理页面分别映射两次,所以一个物理页面可能包含很多段。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值