每一个不曾起舞的日子,都是对生命的辜负。
前言
在之前文件的学习中,一开始我们就提到了文件操作的本质是进程和被打开文件之间的关系,那没有没打开的文件呢?如果一个文件没有被打开,该如何被OS管理呢?
- 没有被打开的文件只能静静的放在磁盘上放着
磁盘上面有大量的文件,而其中的大部分都是处于未被打开的状态,一这些文件也需要被静态管理起来,方便我们随时找到并打开,操作系统对未打开文件的管理,称为文件系统。就比如快递点的快递没有被取走,但也有一一对应的取件码方便取走。那磁盘是如何管理的呢?
一.磁盘结构
磁盘是我们计算机中唯一的一个机械结构。要理解操作系统如何对磁盘上的未打开文件进行管理,首先我们需要对磁盘这个设备的物理结构、存储结构与逻辑结构进行理解,然后再在此基础上理解操作系统对磁盘的管理方法。
1.1 磁盘的物理结构
总体来说,硬盘结构包括: 盘片、磁头、盘片主轴、控制电机、磁头控制器、数据转换器、接口、缓存等几个部份。
盘片: 磁盘是按摞的,也就是说一个磁盘有很多个盘片。
盘面: 一个盘片有两个盘面。
磁头: 每一面都有一个磁头。也就是说假如磁盘有五片,那么就有十面,也就有十个磁头。磁头和盘面是没有接触的,是悬浮在盘面上的,一旦盘面高速旋转,磁头就会漂浮起来,因此磁盘必须防止抖动,否则磁头就会上下摆动,给盘面刮花,造成磁盘上的二进制数据丢失。
马达: 盘片加电之后,马达会使盘片逆时针告高速旋转,此时的磁头会来回的左右摆动。因此马达设备可以控制磁头摆动与盘片旋转。
可以发现,在磁盘内部也有自己的硬件电路,硬件电路有硬件逻辑,可以称之为磁盘本身的伺服系统,即可以通过硬件电路组成伺服系统,从而给磁盘发送二进制指令,让磁盘定位或者寻址某个特定的区域,从而读取磁盘上对应的数据。
我们的笔记本电脑装载的不是机械磁盘,而是SSD。磁盘是我们计算机中唯一的一个机械结构。相对于其他设备而言,由于磁盘是一个硬件结构+外设,所以硬盘访问相对较慢(只是相对的),因此操作系统就需要处理很多工作。虽然目前很少见到磁盘,但在企业端,磁盘依旧是主流,由于SSD成本太高,且有读写限制,容易被击穿,即便访问速度远超磁盘,但也不能将磁盘完全替代。
注意:磁盘封装性强,可以拆开,但是一旦拆开立即报废。
1.2 磁盘的存储结构
磁盘有很多片,每片都有两面,我们拿其中一面进行分析。
通过俯视观察,一个面有无数个同心圆,每圈都是一个磁道,磁道的每一段都会存储大量的数据。磁盘寻址时,基本单位不是bit,更不是byte,而是扇区,一般的磁盘一个扇区的大小为512byte。如图一个小段就是扇区。
对于同心圆来说,越向外的扇区面积越大,但他们都是扇区,也就是都是512byte,为了保持存储大小相同,因此越靠近圆心的扇区密度越大,越远离圆心的扇区密度越小,因此这样各个扇区存储的大小都是相同的。(当然存在扇区大小不同的磁盘,但这里不考虑)
数据存储在扇区上。那么在一个面中,我们怎么能够定位任意一个扇区呢?
确认扇区首先需要确定扇区所在的磁道。设计时根据磁道距离圆心的远近,将磁道进行了一一的编号,磁头的来回摆动,实际上就是寻找指定磁道的过程,一旦找到,磁头就会停在指定位置上不在摆动。确认磁道之后,盘片的高速旋转与静止的磁头形成了相对运动,此时盘片的旋转就是让磁头定位扇区的过程。盘片旋转越快,效率越高。
但实际上磁盘不是成面存在的,而是一摞磁盘。
可以看出,柱面就是每一个盘面上同心同大小的磁道。柱面这个概念同样重要,因为对于一摞磁盘,它的所有的磁头都是连在一起共进退的,因此在磁盘上查找数据时,磁头不是在一面上去找,而是拿着一摞磁头在所有面的磁道上去找。因此柱面这个概念就相当于确定了所有磁头所在的位置。
磁盘寻址的过程:
磁盘在进行 IO 时,首先需要进行寻址,而寻址的过程如下 :先定位在哪一个磁道(Track)/柱面(Cylinder)(此时磁头不用动了),再定位磁头Head(定位盘面),最后定位在哪一个扇区Sector。
可见,磁盘中定位任何一个扇区,采用的硬件基本定位方式:先柱面、再磁头、最后扇区,即:CHS定位法。
能定位任意一个,肯定也能定位任意多个,定位方式相同。
拓展知识:
在大型公司中,一个磁盘通常是几个T,里面保存着大量的用户数据,为了用户数据安全,企业需要要对不用/损坏的磁盘进行消磁;
常见的磁盘消磁方法有两种:一、加热,其缺点是销毁成本高,销毁的磁盘不能回收造成浪费,并且不能保证所有盘的数据全部消磁。二、向磁盘中写入垃圾数据或将磁盘格式化,其缺点是某些磁盘厂商有能力恢复之前的数据。
所以一般大公司都会和磁盘厂商进行协商,要求厂商提供磁盘深度清理的定制协议功能,比如企业通过向磁盘输入协议密文来对磁盘进行深度清理。
注:一般大型公司都有自己固定的磁盘供应厂商,该企业的大部分磁盘都由该厂商提供,但是企业选择磁盘是不会全部都选一家公司的盘的,一是防止杀熟,二是防止磁盘批量化故障。
1.3 磁盘的逻辑结构
理解逻辑结构
我们以磁带的结构来引出磁盘的逻辑结构,如图,磁带盒里面一共有两个齿轮,其中一个齿轮上面缠绕着一圈圈的磁带,当我们把磁带盒插入磁带录音机后,磁带里面的音频数据就会读取然后通过录音机播放出来;当我们把磁带盒拆开后,我们可以发现,磁带扯出来后其结构是线性的,也就是说,磁带里面的数据是按线性方式来读取的。
如果一摞磁盘有两片,即四个面,内存为500GB,根据线性的方式展开理解可以看成如下结构:
通过这样的展开,我们可以把这样的逻辑结构看成一个arr[n]数组,数组中每一个元素的大小都为512byte。此时如果我们对磁盘进行管理,那就可以直接对这个数组进行管理。这就是先描述,再组织。
那此时,如果想要找到指定的扇区,只要知道这个扇区的下标就可以定位磁盘指定的扇区。在操作系统内部,我们称这种地址为LBA地址(Logical Block Address),即逻辑块地址。
找到了LBA地址,我们需要确定CHS地址。LBA 地址转 CHS 定位例子:
假设一个磁盘有两个盘片,每个盘片有两个盘面,每个盘面有10个磁道,每个磁道有100个扇区;现在,某个扇区的LBA地址为1234,求该扇区在磁盘上的具体位置:
最后,为什么要将CHS进行抽象呢?直接CHS不行吗?有如下两个原因:
- 便于管理。
- 不想让OS的代码和硬件强耦合。(硬件变化时不会影响操作系统)
二.理解文件系统
2.1 对IO单位的优化
虽然对应的磁盘的访问的基本单位是512字节,但是依旧很小,从而导致磁盘的频繁IO。因此,OS内的文件系统定制了进行进行多个扇区的读取->1KB/2KB/4KB(常见)为基本单位,从而减少IO的过程,哪怕只读取或者修改1bit,也必须将4KB加载到内存,进行读取或修改,如果必要,再写回磁盘。
注:文件系统以 4KB 作为数据 IO 的单位,那么当读取的数据小于 4KB 时,我们仍然需要读取 4KB 数据,那么就有同学可能担心数据无用的问题,其实,计算机中的局部性原理已经很好的解决了这个问题。实际上这是一个空间换时间的过程。
2.2 磁盘分区与分组
对于一个GB级别比如500GB的磁盘,文件系统IO单位为4KB显然不够大,即便之前已经分区也不便于管理,因此还需要继续分组。
对于每次的分组来说,如果能将一组管理好,那么其他组用同样的方式也能管理好,因此采用分治的思想,最终只要能将这块5GB的磁盘空间管理好,那么整个磁盘都可以管理好。以上述方式,分组之后产生的需要管理的部分就变成了如下的结构:
每个分区的第一部分数据是 Boot Block 启动块,后面才是各个分组,它与计算机开机相关,我们不用关心。
现在,我们只需要管理好一个分组,然后管理模式复制到其他分组就可以管理好一个分区;再将一个分区的管理模式复制到其他分区就可以管理好整个磁盘了。其中,操作系统对一个分区的管理就被称为文件系统。
2.3 分组的管理方法
超级块(Super Block):
存放文件系统本身的结构的全部信息。记录的信息主要有:block 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。Super Block不放在分区的位置是为了能够进行备份,一旦其他Super Bloack出现异常,就可以将其他正常的SuperBlock拷贝过来。
Inode
- 文件 = 内容+属性。 Linux的文件属性和内容是分开存储的,文件属性通过Inode存储,Inode是固定大小的,一个文件一个Inode。需要注意的是文件的名字并不在Inode中存储。文件内容采用data block进行存储,它的大小随着应用类型的变化而不断变化。
Inode为了区分彼此,每一个Inode都有一个自己的ID,通过ls
的-i
选项即可查看:
Inode Table
Inode Table保存了分组内部所有可用的(已经使用+没有使用)Inode。Inode Table在划分组的时候大小就已经确定好了。创建文件时就需要在Inode Table中找到没有被使用过的Inode,把文件中的所有属性填充到这个Inode里面,这样文件的属性就都有了。
Data blocks
数据块,保存了分组内部所有文件的 data block 块,数据块的大小不是固定的,它随着应用类型的变化而变化,其中包含了一个文件的全部或者部分内容 (文件内容太多时需要使用多个 data block)。
Inode Bitmap
Inode对应的位图结构。无论是Inode Table还是Data blocks在创建文件时都必须有一个查找功能。对于Inode Table来说,Inode Bitmap通过比特位的方式,如果有100个文件,那就有100个比特位,位图中的比特位的位置和当前文件对应的inode的位置是一一对应的,如果是0就代表没被占用可以使用,1就代表已被占用不能使用。
Block Bitmap
数据块对应的位图结构。位图中的比特位位置和当前data block对应的数据块位置是一一对应的关系。哪一个块被占用就由0置1,哪一个块被释放就由1置0。
GDT (Group Descriptor Table)
块组描述符,描述块组属性信息。对应分组的宏观属性信息,比如一共有多少个Inode,一共有多少数据块,Inode备用了多少,数据块备用了多少等等。虽然这些数量可以计算,但是计算会消耗性能,所以GDT存在的意义就是存储这些数量以免计算。
2.4 文件操作
大部分上述已经谈到过,在这里总结一下。
新建文件
在了解了一个分组的具体组成之后,我们就知道新建文件时的对应操作了: 在 Inode bitmap 里面查找为0的比特位编号,置为1,然后将文件的所有属性填写到 inode table 对应编号下标的空间中;再在 block bitmap 中查找一个/多个为0的比特位编号,将其置为1,然后将文件的所有内容填写到 data blocks 对应编号下标的空间中;最后再修改 super block、GDT 等位置中的数据。同时,需要将新文件文件名与 inode 的映射关系写入到目录的 data block 中。
获取文件 inode
在 Linux 中,查找文件统一使用 inode 编号,但是我们平时只适用过文件名,从没有使用过 inode,那么操作系统是如何将文件名与 inode 一一对应的呢?答案是通过目录。目录和普通文件不同,目录的内容是下级目录或者普通文件,所以目录的 data block 里面存储的是当前目录下文件名与 inode 的映射关系。
所以当我们在某一个目录下使用文件名查找文件时,操作系统会读取目录 data block 里面的数据,找到文件名对应的 inode 编号,找不到就提示 文件不存在。而当我们在目录下新建文件/文件夹时,操作系统会向目录 data block 里面写入新文件与 inode 的映射关系。这也是为什么在目录下读取文件信息需要 r 权限,在目录下新建文件需要 w 权限的原因。
读取文件属性
先通过目录 data block 得到文件的 inode 编号,然后在 inode bitmap 查看对于编号比特位是否为1,检查 inode 有效性,然后从 inode table 中读取对应 inode 中的文件属性。
注:inode 编号可以跨分组,但不可以跨分区,即同一分区内 inode 是统一编号的。
读取文件内容
对文件的保存就是对内容+属性的保存,内容保存在数据块里,属性保存在Inode里。
当我们查找一个文件的时候,统一通过Inode编号查找。查找文件的内容时,其存储内容的所有数据块的地址都在Inode中的一个数组中存放,通过数组中每一个元素存储的值就可以指定找到这个文件的全部内容。当然,通过Inode数组中的元素找到的数据块,这个数据块存放的也有可能是其他数据块的地址。
struct inode {
int id;
mode_t mode;
int uid;
int gid;
int size;
//...
int blocks[15];//存放数据块地址的数组
};
注:一般来说,blocks 里面前12个元素存放的都是一个 block 编号,代表 data blocks 里面的一块空间,但是最后三个元素不同,虽然它们存放的也是一个 block 编号,但 data blocks 对应 block 编号中存放的内容却很特殊,blocks[12] 指向的 data block 中存放的是一级索引,即其中存放的也是一个类似于 blocks[15] 的数组,指向多个 data block;blocks[13] 指向的 data block 中存放的是二级索引,即其中存放的内容类似于 blocks[12];以此类推,blocks[14] 里面存放的是三级索引。这样,即使该文件很大,操作系统也能够成功读取文件的内容。
删除文件
只需要将 inode bitmap 和 block bitmap 里面对应比特位置为 0 即可,后面新文件的文件属性和文件内容直接覆盖原来已删除文件的属性和内容。
恢复文件
在理解了删除文件的原理之后,我们就明白文件删除之后是可以恢复的 – 操作系统包含了文件的日志信息,会将文件名与 inode 的映射关系保存在一个临时的日志文件里,我们通过这个日志文件找到误删文件的 inode,然后将 inode bitmap 里面对应的比特位重新置为1,再通过 inode 结构体中的 blocks 成员找到所有的数据块,再将 block bitmap 中对应比特位置为1即可;
不过这一切的前提是原文件的 inode 没有被新文件使用 – 如果新文件使用了原文件的 inode,那么对应的 inode table 以及 data block 里面的数据都会被覆盖,所以文件误删之后最好的做法就是什么都别做,避免新建文件将原文件的 inode 占用。
三.软硬链接
首先touch 一个myfile.txt
3.1理解硬链接
创建硬链接的指令:(hard_file.link就是myfile.txt的硬链接)
ln myfile.txt hard_file.link
创建myfile.txt的硬链接之后,各种属性与目标文件都相同,主要的是Inode相同。因此实际上这两个都是同一个文件,那为什么一个文件有两个名字?实际上在之前的分组管理方法中我们知道Inode,Inode作为文件的属性,其中包含着对应文件的数据块的地址,而给这个文件硬链接后,实际上就多了一个映射关系,不仅可以从myfile.txt找到对应的文件内容,通过hard_file.link同样可以找到,与此同时存在一个计数器的变量也会从1变成2(引用计数,也称硬链接数)。硬链接数默认是1的原因:每一个普通文件本身就有一个文件名和一个Inode,即一个映射关系。
建立硬链接做了什么
通过现象可以看出->随着不断输入,硬链接与目标文件大小同时发生变化,因为都储存到一个位置。因此也就解释了创建一个硬链接,根本没有新增文件,因为没有给硬链接分配独立的Inode。既然没有创建文件,硬链接本身也就没有自己的属性集合,用的也是目标文件的Inode和内容。
如果将myfile.txt删掉,通过硬链接仍然可以访问到该文件的内容,因为只是去掉了一个映射关系,计数器减1,还存在一个映射关系可以访问。因此可以看出,只有当硬链接计数器为0时,才算是将一个文件真正的删掉。
注意:这样操作与重命名的意义是不同的。
3.2 理解软链接
创建硬链接的指令:(soft_file.link就是myfile.txt的软链接)
ln -s myfile.txt soft_file.link
软链接和硬链接并不一样,不采用计数器的方式,因为软链接的文件与目标文件是两个文件,软链接虽然同样可以找到链接对应的文件,但一旦将myfile.txt删掉,软链接也会失效,这是因为软链接存的并不是文件的Inode,而是目标文件的路径。
所以软硬链接的区别实际上就是与目标文件的Inode是否相同
软链接的用法
我们将其他目录下的程序以软链接的方式能够更加方便的找到并运行起来。
可以看出,软链接就相当于Windows的快捷方式。注:删链接文件rm和unlink都可以。
3.3 理解.
我们发现,empty作为目录,它的硬链接数量初始值为2,这是有原因的,说明仍有一个文件与empty的Inode相同,构成硬链接。
通过查看,我们发现隐含的.实际上就是empty的硬链接,且.文件是自动生成的,所以目录初始的硬链接为2的原因就是因为.的硬链接。
所以在empty目录中,cd .
就是cd empty
3.4 理解…
如果在empty目录中有新建了一个目录dir,我们发现,empty的硬链接数又发生了变化,即从2变成了3。
那么说明一定又多了一个硬链接的文件:
可以发现,dir中的..
的Inode与empty的Inode相同,可见,从文件系统的层面,再次加深了对cd ..
的理解,cd ..
就是cd empty
。
如果想给目录主动建立硬链接呢?
我们发现,这是不被允许的,那Linux为什么不允许普通用户给目录硬链接呢?
如果目录要创建硬链接的话,那么它的子目录以及子文件都要创建相应的硬链接,这无疑带来了很大的局限, 如果一个目录下有着很多的内容,那么一个硬链接的创建将会是无法想象的,可以说对应目录下的每个内容都要与源目录下的每个内容创建相应的硬链接。
.
和..
的硬链接是OS自己建立的。
当然,目录可以建立软链接,因为只是一个快捷方式。
四.动态库和静态库
注:在Linux第六篇:gcc/g++及Makefile中提到过。
4.1 库的理解
对于我们所编写的文件
有三类:
- 主函数:main.c
- 功能函数:my_add.c、my_sub.c
- 头文件声明:my_add.h、my_sub.h
我们需要将功能函数通过gcc -c的方式形成二进制文件:my_add.o、my_sub.o,再将.h和.o的文件打包放在一起,不能将主函数放在包内,而打包后形成的.o文件,就被称为库。
4.2 什么是静态库
一、定义
静态库是将库中的代码拿到本地,即当程序运行的时候就不再需要链接静态库。即可以 ”离线访问“
静态库一般是libXXX.a
格式,XXX就对应文件名。因此对于上述文件,可以通过编写makefile的方式来生成静态库/打包成libmymath.a。
命令:ar -rc libmymath.a add.o sub.o
或者ar -rc $@ $^
统一打包成libmymath.a之后,就可以看到这个文件:
这就是打包生成的静态库。
二、Linux能够编写C语言的原因
那为什么我们能够在Linux中编写C语言,实际上也是同样的道理,我们发现Linux中同样存在着编写的库和相应的头文件,正是通过这些库,我们在C语言编写代码时通过#include<>
从c库获取信息才能够写出有效的代码。
三、静态库和静态链接
因此,在自己写静态库交付给别人的时候,我们同样需要将库(.a .so)和.h文件交付给用户,才能执行库中的功能。将库和.h文件打包在一起发布output,才能供给用户使用,因此这种操作我们应该在makefile文件中提前设置好:
生成压缩包之后,用户就可以下载这个压缩包从而使用我们所编写的库。
实际上,下载就是所谓的拷贝,即我们可以将这个压缩包copy到另一个路径和新的main.c进行链接,通过tar -xzf mylib.tgz
解压之后,就可以将我们所写的库引入到新的位置去链接了,此时利用gcc指令,需要注意的是,后面必须跟上我们自己定义的静态库和.h文件的路径,并且必须跟上相应静态库的名字(注意:名字是去掉前缀和后缀)。
这个时候问题来了,为什么平时我们写代码的时候不需要跟上静态库的位置呢?事实上对于gcc/g++来说,其默认已经跟上了对应的标准库,但由于现在的库使我们自定义的,因此gcc不会找到,所以我们需要主动添加上。
这样生成的mymath就可以直接执行,完成静态链接。
注意:
-
选项后面的空格带不带都是可行的。
-
形成一个可执行程序,可能不仅仅依赖一个库
-
gcc默认是动态链接的(建议行为)。对于具体的一个库,究竟是动态库还是静态库取决于你提供的是动态库还是静态库。
四、安装库
除了上面通过选项的方式找到对应的头文件及相应的静态库,还有一种方式可以不用这些选项->通过安装的方式,即把我们自定义的头文件和库拷贝到默认路径下:
这样,通过如下方式同样可以执行:
不过·,目前不太推荐安装的方法,因为正处于现学的过程,写的东西一般没什么用。
最后,别忘了删除刚才实验的文件,这个过程实际上就叫做卸载:
4.3 什么是动态库
一、定义
程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。即必须 ”在线访问“
静态库一般是libXXX.so
格式,XXX就对应文件名。拿my_sub.c举例,与静态库生成.o文件不同的是,动态库的.o文件需要gcc -c -fPIC my_sub.c
,即多了一个选项:-fPIC
【产生位置无关码(position independent code)】
想要将全部的.o文件打包,也与静态库有所不同,同样多了一个选项:shared
【表示生成共享库格式】即:gcc -shared -o libmymath.so my_sub.o my_add.o
可以看出,静态库通过ar形成,动态库通过shared形成。
二、动态库与动态链接
接下来,与上面一样,将动态库和.h文件打包,这次采用手动的形式:
拷贝到test中,可见,唯一不同的就是动态库后缀变成了.so
接下来仍然和静态库的执行一样,通过gcc 链接生成可执行文件:
但是出现了问题,找不到对应的文件!这与静态库就出现了差别,静态库这里明明可以直接运行,与静态库一样,在gcc的时候告诉了指定的头文件,库文件。
解决方式:
但是我们告诉的对象是gcc,实际上当编译完成就与gcc无关了。但对于动态库来说,其链接不仅仅在编译时需要,在运行时同样需要,因为动态库是在运行时才慢慢加载动态库的,因此在运行时,OS和Shell也需要知道库在哪里。所以还差一步,告诉OS和shell动态库在哪里,OS和shell一般加载库的时候,都在lib64系统目录下加载,而你的库不在系统目录下,因此解决办法: 将我们自己写的库拷贝到系统路径下,方便OS和shell找到。但上面的方法谁都能做,还有其他方式能够找到动态库。
方案二:环境变量的方式
将动态库添加到环境变量中,方便OS和Shell找到。
此时就能够找到对应的动态库:
最后也可以运行成功了:
但添加环境变量会存在一个问题,当我们把窗口关闭后,即再次登录时,我们自定义的环境变量就不复存在,因此再次运行,mymath同样会找不到对应的动态库,这种方式适合平时的测试。如果想一直保存,就需要配置系统的环境变量的文件,非常麻烦。因此如果想永久保存,还有两种其他方案:
方案三:配置文件的方式
首先:找到系统中的ld.so.conf.d路径,在其中任意创建个文件,但是格式必须为.conf。其次:将动态库的路径写到这个文件中。最后:通过ldconfig更新路径缓存。
通过这样的方式,OS和Shell就会找到动态库,并永久有效。
方案四:当前路径创建软链接的方式
这样同样可以。
方案五:系统路径lib64路径创建软链接的方式
这样同样可以,并能够永久保存。方案四和方案五实际上是同一种做法。
由于在实际操作中可能不止一个库,因此就有这么三种情况:
- 全是静态库:只能静态链接
- 全是动态库:只能动态链接
- 动静结合:取决于gcc,但只要有一个动态库,就是动态链接。
4.4 demo
对于第三方库,我们也可以下载这样的库。举个例子,可以下载一个图形化的界面库,即:ncurses库
普通用户安装方式:sudo yum install -y ncurses-devel
root用户就去掉sudo
通过这个库,就可以通过引入这个库的头文件通过代码的实现,从而执行一个简单的图形化界面。
代码实现:
通过命令:gcc main.c -lncurses
执行生成a.out文件
执行a.out:./a.out
按q退出。
五.动静态库的加载理解
回顾进程地址空间部分知识:
我们曾经讲过,程序不仅在运行时作为进程地址空间存在,在编译的时候就已经以虚拟地址的方式把我们的程序编译好,即程序在没有加载到内存的时候,就已经有了代码区、全局数据区,已初始化,未初始化,只读……(堆区栈区没有)
5.1 加载静态库
对于静态库来讲,静态库不需要加载,而程序需要加载。当静态库链接的时候,实际上是将代码(printf)拷贝进程序中,所以后面程序运行的时候就不再依赖于静态库。
而一旦有很多程序,静态库就会拷贝大量重复的代码分给不同的程序。通过进程地址空间的知识我们知道当静态库拷贝代码给程序时,实际上是把代码拷贝进了代码区。因此在程序运行形成进程地址空间时,静态库中的代码只能被映射到进程地址空间相应的代码区中,未来的这段代码,必须通过相对确定的地址位置进行访问。
5.2 加载动态库
具体分成以下步骤:
-
对于动态链接来说,可执行程序中存放的是动态库中某具体 .o 文件的地址,同时,由于组成动态库的可重定向文件是通过位置无关码 fPIC 生成的,所以这个地址并不是 .o 文件的真正地址,而是该 .o 文件在动态库中的偏移量;(与C++中的虚函数表一样)
-
然后就是程序运行的过程:操作系统会将磁盘中的可执行程序加载到物理内存中,然后创建 mm_struct,建立页表映射,然后开始执行代码,当执行到库函数时,操作系统发现该函数链接的是一个动态库的地址,且该地址是一个外部地址,操作系统就会暂停程序的运行,开始加载动态库;
-
加载动态库:操作系统会将磁盘中动态库加载到物理内存中,然后通过页表将其映射到该进程的地址空间的共享区中,然后立即确定该动态库在地址空间中的地址,即动态库的起始地址,然后继续执行代码;
-
此时操作系统就可以根据库函数中存放的地址,即 .o 文件在动态库中的偏移量,再加上动态库的起始地址得到 .o 文件的地址,然后跳转到共享区中执行函数,执行完毕后跳转回来继续执行代码段后面的代码。这就是完整的动态库的加载过程。
因此可以看出,动态库并不像静态库一样,不需要拷贝大量的相同代码,多个程序可以共享一份动态库中的代码。
转载:https://blog.csdn.net/NEFUT/article/details/129201913