【linux】如何加载进程
前言:在之前的博客介绍了进程地址空间,提到了地址空间是如何与物理内存相映射以及一些简单的运行机制,但在物理内存下,还存在与磁盘的交互,在磁盘中一个程序代码是如何存储的?又是怎么一步步加载到内存中?是谁在调度这一切?
虚拟地址与物理地址
虚拟地址是指计算机中程序所使用的地址空间,它是在程序执行过程中由程序产生的地址。在虚拟内存系统中,每个程序都有自己的虚拟地址空间,这使得每个程序都认为它是在独占地使用整个计算机内存。虚拟地址由操作系统和硬件共同管理,而程序中使用的地址都是虚拟地址
。在程序执行时,虚拟地址会被映射
到物理地址上。
物理地址是计算机内存中实际存储数据
的地方。它是计算机内存芯片上的唯一标识位置,是硬件直接访问的地址。物理地址空间是实际存在于计算机硬件上的内存空间。操作系统通过使用内存管理单元(MMU)
将虚拟地址映射到物理地址,从而实现虚拟内存的概念。
假设一个程序要访问地址0x00400000,这就是它的虚拟地址。在虚拟内存系统中,这个地址会被映射到计算机内存的另一个位置,比如物理地址0x0000A000。当程序试图访问虚拟地址0x00400000时,操作系统会通过MMU将它映射到对应的物理地址0x0000A000,从而实际上访问到了正确的内存位置。
这就是简单的虚拟与物理之间的映射,但在这之间还有些细节需要补充。
程序加载
加载前
首先需要明确的一点,程序在编译好后,就已经有地址
的概念了。
而且这里的地址指的是逻辑地址
。其实现在的逻辑地址和虚拟地址也没什么太大区别了。
当一个程序被存储到磁盘上时,它的内部分布通常是按照不同的段(例如代码段、数据段等)进行组织。其编址从0
开始,这种组织方式通常被称为平坦模式(Flat Model)。在这个阶段,程序的不同部分被分配不同的段,每个段可以有自己的基地址
。在编译阶段,程序中的符号(比如变量名、函数名等)被转换为相对于各自段基地址的偏移量
,形成了虚拟地址。
加载到内存
每个程序都有一个entry,也就是入口地址
。CPU通过拿到该程序的entry,就能够把程序加载到物理内存
中,虽然我这里先提到的是物理内存和磁盘间的数据交互,但其实在系统内,先完成的是对虚拟地址空间的描述和组织。
当需要执行某个程序时,操作系统会负责将程序从磁盘加载到内存中。在程序的首部通常就会含有entry。有了entry,CPU就可以从entry往下顺序执行指令(例如mov,push等,这些指令也会占据物理内存空间)。
虚拟地址空间和物理内存是通过页表
进行映射关联的。CPU执行某一个指令时,查询页表发现没有映射关系,就会引发缺页中断
,然后去物理内存中找到该地址。这是之前就讲到过的。
不过还是在这里补充一下:
页表和缺页中断: 虚拟地址空间和物理内存之间的映射是通过页表完成的。当CPU执行某个指令时,如果发现对应的虚拟地址没有映射到物理地址,就会触发缺页中断。操作系统会处理这个中断,将对应的页面从磁盘加载到内存,更新页表,然后重新执行导致缺页中断的指令。
当执行到例如call
指令时,就需要跳转到另一个地址接着执行,这个地址是编译时就形成
的虚拟地址,很明显和虚拟地址空间里的虚拟地址不是同一个。
但操作系统有自己的办法,当程序从磁盘中被加载到物理内存,每行指令就有了自己的物理地址
,只不过在内部存储了一个虚拟地址而已,通过其独有的物理地址,就能找到需要的虚拟地址(有时候也会是一个偏移量,下面会谈)。
动态库加载
静态库和动态库特点?
静态库在编译时被链接到程序,每个程序有独立副本
;动态库在运行时加载到内存
,多个程序可共享,节省内存,但需确保版本兼容性。
由上可知,静态库谈不上加载,是直接复制到程序中的,就算你删除了该静态库,先前被引入的程序也仍然可以使用该库的函数变量等,因为这些数据已经成为了该程序的一部分
。
而动态库不行,动态库是被多个程序所共享的。
动态库也被存储在磁盘上,当需要时,会被CPU调度进入物理内存,也同样的需要映射到一个虚拟内存地址上去。
但这里有一个问题:例如一个程序函数有printf,而这个函数依赖动态库liba,此时CPU执行到printf语句,就需要在这里通过该指令所蕴含的地址找到库里的printf函数实现。
例如该地址是0x1122,该放在虚拟内存的哪里呢?
之前讲过,代码段放在代码区,而动态库则放在共享区。而0x1122根据上文也可以推断,这是一个虚拟地址,CPU当然可以决定将这个printf实现放在0x1122,但如果这里被其他的数据占用了呢?
所以操作系统在这里并未将0x1122当做是一个虚拟地址,而是一个偏移量
,也就是说,CPU将liba映射到共享区中,然后通过printf里的偏移量,与liba首地址相加(通常是相加操作),就能得到正确的虚拟地址。