<Linux> 文件系统

目录

前言:

一、 磁盘

(一)磁盘的物理结构

(二)磁盘的物理存储结构

1. 数据存储

2. 存储结构

二、磁盘的逻辑抽象

三、磁盘信息

(一)具体结构

(二)重新认识目录 

四、理解文件系统中的增删查改

五、软硬链接

(一)软链接

 (二)硬链接

(三)二者区别

实现原理: 

(四)取消链接

(五)ACM时间

六、动静态库

(一)什么是库

1. 库的作用

(二)制作一个静态库

 (三)静态库的使用

1. 通过指定路径使用静态库

2. 将头文件和静态库文件安装至系统目录中

(四)制作一个动态库

(五)动态库的使用

2. 建立软链接

3. 更改配置文件

七、动静态库的加载

(一)静态库的加载

(二)动态库的加载

1. 加载过程

2. 动态库地址的理解

八、动态库知识补充

总结:


前言:

文件分为 内存文件 磁盘文件。磁盘文件,这是一个特殊的存在,因为它不属于冯诺依曼体系,而是位于专门的存储设备中,因此 磁盘文件 存在的意义是将文件更好的存储起来,以便后续对文件进行访问。在高效存储 磁盘文件 这件事上,前辈们研究出了十分巧妙的管理手段及操作方法,而这些手段和方法共同构成了我们今天所谈的 文件系统。

文件系统是操作系统中负责管理持久数据的子系统,简单点就是负责把用户的文件存到磁盘硬件中,因为即使计算机断电了,磁盘里的数据并不会丢失,所以可以持久化的保存文件。

文件系统的基本数据单位是文件,它的目的是对磁盘上的文件进行组织管理,那组织的方式不同,就会形成不同的文件系统。

一、 磁盘

(一)磁盘的物理结构

现在市面上的磁盘主要分为 机械硬盘 和 固态硬盘,前者读取速度慢,但便宜、稳定;后者读取速度快,但价格高昂且数据易损,两者各有其应用场景,本文主要介绍的是 机械硬盘。磁盘的是计算机中唯一一个机械设备,同时它还是一个外设,其图片如下:

根据 冯诺依曼体系结构机械硬盘 在速度上远远慢于 CPU 和 内存 

举例机械硬盘有多慢

  • 假设 CPU 运行速度是纳秒级,那么内存就是微秒级,而机械硬盘只不是是毫秒级

为何 机械硬盘 如此慢?这与它的结构有很大关系

其主要的核心物理结构有三个:

  • 磁盘片:磁盘片是硬盘中承载数据存储的介制,磁盘是由多个盘片叠加在一起,互相之间由垫圈隔开的。
  • 盘面:一片磁盘片是由两面的,每一面被称为盘面,磁盘片的两面都可以存储数据的。每一个盘面都有对应的磁头,也就是说一个磁盘片有两个磁头的。
  • 磁头:磁头是向磁盘读取数据的媒介,其通过磁性原理读取磁性介质上数据。所以磁头不与盘面接触,磁头悬浮在盘面上面或下面。其中还有重要的一点是:磁头是共进退的
  • ....

机械设备 控制是需要时间的,因此导致 机械硬盘 读写数据速度相对于 CPU 和 内存 来说比较慢

(二)磁盘的物理存储结构

1. 数据存储

总所周知,数据是以 0 和 1 的方式进行存储的,常见的存储介质有:强信号与弱信号高电平与低电平波峰与波谷南极与北极 等,而盘面上比较适合的是 南极与北极。

当磁头移动到指定位置时

  • 向磁盘写入数据:N->S
  • 删除磁盘中的数据:S->N

磁盘中读写的本质:更改基本元素的南北极、读取南北极

注意: 磁头并非与盘面进行直接接触,而是以 15 纳米的超低距离进行磁场更改

        这个距离相当于一架波音747距离地面1米进行超低空飞行,所以如果磁头制作工艺不够精湛,可能会导致磁头在写入/读取数据时,与盘面发生摩擦(高速旋转)发热,从而导致磁场消失,该扇区失效,数据丢失。

        所以机械硬盘 不能在其运行时随意移动,因为角度的偏转也有可能导致发生摩擦,造成数据丢失,更不能用力拍打 机械硬盘

2. 存储结构

在磁盘的盘面上,磁盘被一个个的同心圆以及射线进行分割,从而出现了:磁道, 扇面扇区

  • 扇区: 被一个个的同心圆以及射线进行分割出的一个个扇形区域。
  • 扇面: 两条相邻的射线之间夹的所有扇区构成扇面。
  • 磁道:盘面上半径相同的扇区构成一个磁道。

  • 柱面:由于现实世界中磁盘的立体结构,所以把空间中所有半径相同的磁道定义为一个柱面

其中扇区是存储的基本单元, 每个扇区其大小为:512 byte或 4kb ,一般来说都是512 byte(下面我们讨论时也是以512 byte为准)

由于扇区是最小的存储单元所以在硬件的角度:一个文件(内容 + 属性)无非是占用一个或多个扇区进行数据的存储。

那么在硬件上磁盘是怎么定位一个扇区的呢? —— 答案是CHS定位法! cylinder柱面 head磁头 sector扇区

  1. 磁盘中的磁头是有编号的,我们首先根据扇区所在的盘面先确定使用几号磁头。
  2. 每个扇区都有自己所在的磁道,根据扇区所在的磁道就可以确定磁头的偏移位置。
  3. 每一个扇区在所在的扇面上都已经被编好了号码,磁头最后根据扇面所在的号码确定扇区。

我们既然能够通过CHS定位一个扇区,那么也能定位多个扇区,从而将文件能够从硬件的角度进行读取和写入!

二、磁盘的逻辑抽象

有了上面的知识我们知道能够通过CHS去定位一个文件的基本单元,但是操作系统是不是采用这种方式去定位磁盘中的数据呢?答案是不是! 

这主要有以下两点原因:

  1. 操作系统是软件,磁盘是硬件,硬件通过CHS定位扇区,操作系统如果采用和硬件一样的定位方式就会和硬件之间产生很大的耦合关系,如果我们的硬件变了(例如:机械磁盘变为固态硬盘),那么操作系统也要进行变化,这并不是一个好的情况。
  2. 扇区的大小是512 byte ,以扇区为单位进行IO时的数据量太小了,在进行大量IO时这会极大的影响到运行的速度。

操作系统实际进行IO时,是以4kb为单位的(这个大小是可以调整的)4kb = 512 * 8 byte 
因此将8个扇区定义为一个块,操作系统按照一个块的单位进行IO。

磁盘片的物理结构是一个圆环型结构,假设我们能够将每一个盘面按照磁道进行拉伸展开(就像使用胶布一样),那不就变成了一个线性结构了吗?

展开以后对于每一个磁道里面都有许多扇区,这些扇区组合起来便可以被抽象为一个数组!

但是这个数组太大了,而且每一个单位的数据量也有点太小了,我们还要对其进行抽象,我们将8个扇区组成一个块,这样数组的长度就缩短了8倍,经过这一层抽象后,由原来的一个扇区数组变为块数组。

其中逻辑块的数组下标被定义为逻辑块地址 (LBA地址) ,现在 OS 想访问具体的扇区时,只需通过 起始扇区的地址 + 偏移量 就可以获取 LBA 地址,然后通过特定手段转为 CHS 地址,交给外设进行访问即可 LBA和CHS转换,这个操作的原理类似于指针的解引用。

于是操作系统通过LBA地址进行访问存储的数据,这就是操作系统对磁盘等存储硬件的逻辑抽象。

因此对于外设中文件的管理,经过 先描述,再组织 后,变成了对数组的管理,这个数据就是 task_struct 中的 struct block。

最后我们就能理解为什么 IO 的基本单位是 4 kb 了,因为直接读取一个数据块(4 kb),这样可以提高 IO 效率(内存对齐)。

三、磁盘信息

(一)具体结构

经过上面的抽象我们操作系统便拿到了一个逻辑块的大数组,但是这个数组太大了,我们对这个大数组直接管理还是太过于困难了,于是我们操作系统就可以对这个大数组进行分区管理(类似于windows的分盘),当我们管理好了一个分区就可以将一个分区的管理方法复制到其他的分区中,从而实现了全局的数据管理。

但是每个分区的数据还是太大了,操作系统还要对每一个区进行分组,通过分组再次降低管理的难度,其中每个分区其内部的结构如下图: 

  • Boot Block: 里面存放的是与操作系统启动相关的内容,诸如:分区表,操作系统镜像的地址等,一般这个区域被放在磁盘的0号磁头,0号磁道,1号扇区中,如果这个区域的内容受到破坏,那么操作系统将无法启动!
  • 超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck inode的总量,未使用的bolck inode的数量,一个bolck inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。
  • GDT,Group Descriptor Table:块组描述符,描述块组属性信息。
  • 块位图(Block Bitmap):Block Bitmap中记录着Data Blocks中哪个数据块已经被占用,哪个数据块没有被占用。
  • inode位图(inode Bitmap):每个bit位表示对应的inode是否空闲可用。
  • i节点表(inode Table):一般来说,一个文件内部的所有属性被保存在一个inode节点中(一个inode节点的大小一般是128字节),一个分区会存在大量的文件,所以一个分区中会有大量的inode节点,每一个inode节点都有自己的编号,这个编号也属于文件属性。为了更好的管理inode节点,就要有一个专门的区域存放所有的inode节点,这个区域就是inodeTable,其内部可以看成一个数组结构。
  • 数据区(Date blocks):里面是大量的数据块,每一个数据块都可以用来存放文件内容。

细节注意要点:

  1. 每一个块组都有Block Bitmap inode Bitmap inode Table Date blocks,其他的部分每个块组里面可能没有。
  2. Super Block 在每一个块组里都可能存在,也可能不存在但至少要有一个块组存在超级块!而且每一个存在超级块的块组里面的超级块是一样的,并且所有存在超级块的块组其里面的超级块是联动更新的。
  3. 超级块存在多份的意义是:万一其中一个超级块损坏,还有其他超级块可以使用,并且可以利用其他完好的超级块去修复已损坏的超级块。不至于一个超级块损坏导致整个分区的文件系统直接损坏。
  4. inode节点中有一个数组,这个数组里面存放了对应文件使用的数据块的编号。
  5. inode编号不能跨分区使用,每一个inode编号在一个分区内唯一有效!
  6. 根据inode可以确定一个分区内的分组

(二)重新认识目录 

 在Linux的命令行中,我们可以使用ls -il命令可以看到文件的inode编号:

其实在Linux中系统对于文件只认识inode编号,并不认识文件名,那你可能会很好奇:我们平时一直使用的都是文件名,没有使用过inode编号为什么我们还能够操作文件呢?

这其实和目录有关!我们的打开任意一个文件都是在一个目录里面打开的,而且目录本身也是文件,目录也有inode编号!其里面也有内容,也需要数据块,其里面的内容是:该目录下文件名与该文件的inode映射关系。

因此当我们使用文件名时,目录会自动帮我们找到对应的inode编号,完成相应的要求。

例如我们在Linux下使用cat xxx.xx 命令,其大致的执行过程是:

  1. 在目录下找到log.txt 的inode编号。
  2. 利用inode编号在inode Table中找到inode
  3. 根据inode找到 xxx.xx文件 使用的数据块的编号。
  4. 根据数据块的编号找到数据块,将内容提取出来并刷新到显示器上面。
     

四、理解文件系统中的增删查改

:见上面的cat xxx.xx 文件的例子 

删 :

  1. 根据当前要删除的文件名到目录中找到对应的inode编号。
  2. 根据inode编号到inode Table中找到inode节点。
  3. 根据inode节点中的内容找到该文件对应的Block Bitmap,然后将相应的bit进行置0表示内容的删除。
  4. 根据inode编号将inode bitmap 对应的bit进行置0表示属性的删除。
  5. 将当前目录中inode 编号与文件名的映射关系进行删除。
     

:(创建一个内容为空的文件)

  1. 操作系统在inode bitmap 中从低向高依次扫描,将找到的第一个bit0的位置置成1
  2. 然后在inode Table 中的对应位置写入新的属性。
  3. 然后向当前目录中增加新的inode 编号与文件名的映射关系。 

改:

  1. 根据当前的文件名到目录中找到对应的inode 编号。
  2. 根据inode编号到inode Table中找到inode节点。
  3. 计算需要的数据块的个数,在Block bitmap中找到未使用的数据块,并将相应的bit0置成1
  4. 将分配给文件的数据块的编号填入inode中。
  5. 将数据写入到数据块中
     

补充细节

  1. 如果文件被误删了,该怎么办?数据应该怎么被恢复?(这里我们只讨论大致的原理)
    答案是:最好什么都不做,因为Block bitmap被置为0以后,相应的数据块已经不受保护了,此时再创建新文件就有可能覆盖原来的文件。

  2. 数据恢复的原理是:Linux系统为我们提供了一个日志,这个日志里面的数据会根据时间定期刷新,所有被删除的文件的inode 编号在这里都有记录,通过被删除文件的inode 编号,先把inode bitmap相应的位置的bit0置成1然后根据inode 编号到inode Table中找到对应的数据块编号,然后到Block bitmap中将相应位置的bit0置成1

  3. 上面我们说的分区,分组,填写系统属性是谁在做,什么时候做的呢?
    答案是:是操作系统在做!是在格式化的时候做的!在我们操作系统分区完成以后,为了能让分区能够正常使用,需要对分区进行格式化,格式化的本质就是:操作系统向分区内写入文件系统管理属性的信息!

  4. inode里面只是用数组来与数据块进行单纯的一 一映射吗?
    答案是:并不是的,如果一个inode 里面存放数据块的编号的数组大小是15,如果只是单纯的一 一映射 ,那么一个文件只能存储15 * 4KB = 60 KB的内容。这显然是不合理的!

    所以inode里面存放数据块的编号的数组被规定它的前几个下标是直接索引,中间几个是二级索引,后面几个的三级索引, …

    直接索引:直接指向数据块。
    二级索引:指向一个数据块,这个数据块里面的内容是直接索引。
    三级索引:指向一个数据块,这个数据块里面的内容是二级索引。

    二级索引对应的数据量单位:4KB / 4 * 4KB = 4 MB
    三级索引对应的数据单位:(4KB / 4 )^2 * 4KB = 4G 

  5. 有没有一种可能一个分组,数据块没用完,inode没了,或者inode没用完,数据块用完了?
    答案是:有可能的,如果我们一直创建空文件,就可能导致inode 使用完毕,而数据块没有使用完,如果我们的所有内容都放在一个文件中,就可能导致inode 没有使用完,而数据块使用完了。

五、软硬链接

(一)软链接

当我们有一个文件在一个很深的目录时,我们是不方便去使用这个文件的,那有没有一种方式能够让我们能够轻松的找到并使用这个文件呢,有的,那便是软链接。软连接非常类似于windows中的快捷方式。

我们可以在当前的目录里面建立一个软连接,其中软链接文件名可以自定义,来让我们能够更加方便的去使用mytest 可执行程序

ln -s 文件名 软链接名

可以看到,执行软链接跟执行源可执行程序没有任何差别:

 (二)硬链接

生成硬链接文件就更简单了,对文件 mytest 进行硬连接,生成硬连接文件 my-hard-link ,其中硬链接文件名也可以自定义:

ln 文件名 硬连接名 // 不带参数默认是硬链接

可以看到,执行软链接跟执行源可执行程序没有任何差别: 


 

(三)二者区别

当我们进行创捷一个文件时,在文件权限后面会有一个数字,这个数字就是硬链接数。我们查看一下他们的inode编号:

我们可以发现它们的编号并不相同,源程序跟硬链接编号一样,但软链接就不一样。

区别一:

  • 软链接文件的 inode 编号与源文件不同(独立存在),软连接文件比源文件小得多,软连接的内容就是自己所指向的文件的路径
  • 硬链接文件与源文件共用一个 inode 编号(对源文件其别名),硬链接文件与源文件一样大,并且硬链接文件与源文件的链接数变成了 2

我们再给 mytest 创建一个硬链接,并且可以发现源文件的硬链接数+1了,同时也再次证实了硬链接与源文件 inode 一样:


实现原理: 

 那为什么源文件跟硬链接inode编号一样并且源文件硬链接数会+1捏?与实现原理有关:

当我们创建硬链接时,操作系统在当前目录里面建立新的映射关系,操作系统把原文件的inode编号与硬链接建立映射关系,此时一个inode编号就有了两个文件名,同时在inode节点中会有一个引用计数的变量ref_count,当我们建立一个硬链接时,这个引用计数的变量就会自增一下,表示硬链接数目加一:

软链接又称为符号链接,它是一个单独存在的文件,拥有属于自己的 inode 属性及相应的文件内容,不过在软连接的 Data block 中存放的是源文件的地址,因此软连接很小,并且非常依赖于源文件。

我们分别删除掉源文件,看看软硬链接有什么区别:

 区别二:

  • 当我们将源文件删除后,软连接失效,因为软链接文件依赖于源文件
  • 当我们将源文件删除后,硬链接仍然有效,因为硬链接文件是源文件的别名

 原理:

假设只是单纯的删除软连接文件,那么对源文件的内容没有丝毫影响,就好比 windows 桌面上的快捷方式,有的人以为将快捷方式(软链接)文件删除了,就是在 “卸载” 软件,其实不是,如果想卸载软件,直接将其源文件相关文件夹全部删除即可。

当我们删除一个文件时,目录会正常帮我们删除文件名与inode的映射关系,但是操作系统不一定会为我们删除原文件,操作系统会将该文件对应的inode节点中的引用计算变量也会自减一下,如果减完之后等于0就删除文件,否则只是修改了引用计数变量。 


这也就解释了为什么删除源文件后,硬链接文件不受任何影响,仅仅只是 硬链接数 - 1,同理,删除硬链接文件,也不会影响源文件。

为什么新建目录的硬链接数为 2

  • 因为一个目录在新建后,其中默认存在两个隐藏文件:. 与 ..
  • 其中 . 表示当前目录,.. 表示上级目录

 Linux 中的目录结构为多叉树,即当前节点(目录)需要与父节点(上级目录)、子节点(下级目录)建立链接关系,并且还得知道当目录的地址,否则就会导致切换目录时出现错误。

为了避免因用户的误操作而导致的目录环状问题,规定用户不能手动给目录建立硬链接关系,只能由 OS 自动建立硬链接,比如新目录后,默认与上级目录和当前目录建立硬链接文件,在当目录下创建新目录后,当前目录的硬链接数 + 1:

操作系统拒绝了我们的请求,操作系统不允许给一个目录建立硬链接,因为给目录建立硬链接可能导致环路路径问题 。


一般来说,一个目录文件的硬链接数 -2 就是该目录下的目录个数。 

(四)取消链接

取消链接的方式有两种:

  • 直接删除链接文件
  • 通过 unlink 取消链接关系

(五)ACM时间

每一个文件都有三个时间:访问 Access、修改属性 Change、修改内容 Modify,简称为 ACM 时间

可以通过 stat 查看指定文件的 ACM 时间信息

这三个时间的刷新策略如下:

  • Access:最近一次查看内容的时间,具体实现取决于系统
  • Change:最近一次修改属性的时间
  • Modify:最近一次修改内容的时间(内容更改后,属性也会跟着修改)

Access是高频操作,如果每次查看都更新的话,会导致 IO 效率变低,因此 实际变化取决于刷新策略:查看 N 后次刷新

注意: 修改内容一定会导致属性时间被修改,但不一定会导致访问时间被修改,因为可以不打开文件,对文件进行操作,比如直接重定向到文件:echo "...." xxx.xx

六、动静态库

(一)什么是库

简单来说:库是一些可重定向的二进制文件,这些文件在链接时可以与其他的可重定向的二进制文件一起链接形成可执行程序。

一般来说库被分为静态库动态库,他们是有不同的后缀来进行区分的。

系统平台 静态库 动态库
Windows .lib .dll
Linux .a .so

另外对于C/C++来说其库的名称也是有规范要求的,例如在Linux下:一般要求是 lib + 库的真实名称 +(版本号)+ .so /.a + (版本号),版本号是可以省略不写的。

  • 比如 libstdc++.so.6 ,去掉前缀跟后缀,最终库名为 stdc++

  • libc-2.17.so,去掉前缀跟后缀,最终库名为 c

有了上面的一点基础知识以后我们就能够去见一见库了,Linux系统在安装时已经为我们预装了C&C++的头文件和库文件。

对于C/C++头文件在Linux里面一般在/usr/include目录下面存放我们的头文件:


 对于C/C++的库文件,一般在 /usr/lib64 和 /lib64 里面,/lib64里面给的是root和内核所需so或者a之类的库文件,而 /usr/lib64 是普通用户能够使用的。

1. 库的作用

提高开发效率

系统已经预装了 C/C++ 的头文件和库文件,头文件提供说明,库文件提供方法的实现

  1. 头文件提供方法说明,库提供方法的实现,头和库是有对应关系的,是要组合在一起使用的
  2. 头文件是在预处理阶段就引入的,程序在链接时链接的本质其实就是链接库!

如果没有库文件,那么你在开发时,需要自己手动将 printf 等高频函数编写出来,因此库文件可以提高我们的开发效率,比如 Python 中就有很多现成的库函数可以使用,效率很高。

  1. 我们在使用像vs2019这样的编译器时要下载并安装开发环境,这其中是在下载什么?安装编译器软件,安装要开发的语言配套的库和头文件。
  2. 我们在使用编译器,都会有语法的自动提醒功能,但是都需要先包含头文件,这时为什么呢?
    语法提醒本质是编译器或者编辑器,它会自动的将用户输入的内容,不断的在被包含的头文件中进行搜索,自动提醒功能是依赖头文件而来的!
  3. 我们在写代码的时候,我们的环境怎么知道我们的代码中有哪些地方有语法报错,哪些地方定义变量有问题?
    不要小看编译器,编译器有命令行的模式,还有其他自动化的模式,编辑器或集成开发环境可以在后台不断的帮我们调用编译器检查语法而不生成可执行文件,从而达到语法检查的效果。

(二)制作一个静态库

库的使用能够提高我们的开发效率,接下来我们来制作一个库!

//myadd.h
#pragma once
int myadd(int x, int y);
 
//myadd.c
#include "myadd.h"
int myadd(int x int y)
{
    return x + y;
}
 
//mysub.h
#pragma once
int mysub(int x, int y);
 
//mysub.c
#include "mysub.h"
int mysub(int x, int y)
{
    return x - y;
}

//main.c
#include <stdio.h>
#include "myadd.h"
#include "mysub.h"

int main()
{
  int a = 10, b = 20;

  printf("%d + %d = %d\n", a, b, myadd(a, b));
  printf("%d - %d = %d\n", a, b, mysub(a, b));

  return 0;
}


我们将库的头文件与库的实现文件放在了mylib 文件夹里面了,将 main.c 放在了 otherPerson 里面,此时 main.c 与库的头文件以及实现文件不在一起,此时编译会报错。

提示我们找不到头文件,就算我们将头文件移过去也会有链接错误。如果我们想让其他人调用自己程序的一些功能,但是不想把源代码交给其他人,则可以把自己的程序经过预处理、编译、汇编,生成 .o 文件,即可重定位目标二进制文件,交给别人使用。

gcc -c myadd.c mysub.c main.c

再把实现功能的源代码与二进制文件、使用功能的程序分别分离到 mylib otherPerson 目录中,把 main.c 文件也使用指令 gcc -c 生成 .o  文件,再与其他 .o 文件进行链接,最后生成可执行文件:

同样执行成功。

上面的整个过程就是我们制作静态库的基本流程,当然这样的制作其实还是有缺陷的,当我们的项目文件过于庞大时,我们要给一个.c文件十几个这样的.o文件,而且文件过于分散了,不利于管理,于是我们就需要将多个这样的.o文件打成一个包,我们将这个包直接给别人,别人就能直接使用了。


打包的命令是:

ar -rc [lib库名.a] [*.o]
  • ar 命令用于建立或修改备存文件,或是从备存文件中抽取文件。可集合许多文件,成为单一的备存文件,在备存文件中,所有成员文件皆保有原来的属性与权限。
  • r :如果打包好的 xxx.a 库中没有 xxx.o 那么就会把模块 xxx.o 添加到库的末尾,如果有的话就会替换之(位置还是原来的位置)。
  • c :建立备存文件。

 (三)静态库的使用

1. 通过指定路径使用静态库

在我们实际使用库时,我们一般将头文件放在一个目录里面,将库放到另外一个文件里面,这样便于我们进行分类管理。我们也按照这种标准化的做法,来整理一下我们的目录结构。

我们 libmymath.a 静态库不是C的标准库,编译器不认识第三方库(需要提供第三方库的路径及库名),所以gcc不会在进行编译时去链接我们自己写的静态库,所以我们还要给gcc添加一些参数用来指明我们要链接的静态库。 

 正确写法,加上需链接的库:

(其中 -I - L -l ,其后面传递的内容可以加空格进行分割,也可以不加空格)

  • -I: 指明我们要包含的头文件路径,此处为 ../include
  • -L :指明我们包含的库的路径,此处为 ../mylib
  • -l:指明我们要包含的库文件名(这里的库文件名是指真实名称),库名称为 mymath

为什么编译 C/C++ 代码时,不需要指定路径?

  • 因为这些库都是系统级的,gcc/g++ 默认找的就是 stdc/stdc++ 库

2. 将头文件和静态库文件安装至系统目录中

除了这种比较麻烦的指定路径编译外,我们还可以将头文件与动态库文件直接安装在系统目录中,直接使用,无需指定路径(需要指定静态库名

所谓的安装软件,就是将自己的文件安装到系统目录下

sudo cp ./include/*.h /usr/include/
sudo cp ./mylib/*.a /lib64/

注意: 将自己写的文件安装到系统目录下是一件危险的事(导致系统环境被污染),用完后记得手动删除 。

总结:第三方库的使用

  1. 需要指定的头文件,和库文件。
  2. 如果没有默认安装到系统gcc、g++默认的搜索路径下,用户必须指明对应的选项,告知编译器: a.头文件在哪里 b.库文件在哪里 c.库文件具体是谁。
  3. 将我们下载下来的库和头文件,拷贝到系统默认路径下,在Linux下就是安装库! 那么卸载呢?对任何软件而言,安装和卸载的本质就是拷贝到系统特定的路径下!
  4. 如果我们安装的库是第三方的库,我们要正常使用,即便是已经全部安装到了系统中,gcc g++必须用 -l 指明具体库的名称!
  5. 无论我们是从网络中直接下载的库,还是源代码(编译方法)。都会提供一个 make install 安装的命令,这个命令所做的就是安装到系统中的工作。我们安装大部分指令、库等等都是需要 sudo 提权的。

(四)制作一个动态库

动态库:动态库不同于静态库,动态库中的函数代码不需要加载到源文件中,而是通过 与位置无关码 ,对指定函数进行链接使用。

动态库的打包也同样分为两步:

  1. 编译源文件,生成二进制可链接文件,此时需要加上 -fPIC 与位置无关码(下文会详细解释)
  2. 通过 gcc/g++ 直接目标程序(此时不需要使用 ar 归档工具)

将源文件编译为 .o 二进制文件,此时需要带上 -fPIC 与位置无关码

gcc -c -fPIC *.c

将所有的 .o 文件打包为动态库(借助 gcc/g++) 

gcc -o libmycalc.so *.o -shared

当我们有了动态库以后,我们是可以删除可重定向的二进制文件的,但是动态库不能够删除,动态库删除的话,依赖此动态库的程序也将不能够运行!

(五)动态库的使用

下面我们尝试用动态库去链接形成可执行程序:

注意:我们自己写的库是属于第三方库,我们要编译时要指明:头文件路径,库文件路径,库文件名(真实名称)。 

发生了错误,系统提示我们程序运行时,没有办法找到动态库,这是为什么呢?

这就和动态库的特性有关了,由于采用动态库的程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码,所以运行的程序必须要知道去哪里链接我们的库,即对于动态库在编译期间我们要告诉编译器去哪里链接库进行编译,在运行期间要告诉操作系统去哪里链接库进行运行。

静态库不需要链接是因为:静态库在编译链接期间将用户使用的二进制代码直接拷贝到目标可执行程序中,编译后的程序是一个完整的程序,不需要再运行时再使用静态库了。

操作系统查找动态库的方法有三种:

  1. 设置环境变量:LD_LIBRARY_PATH。
  2. 在系统指定路径下建立软链接,指向对应的库。
  3. 配置文件。

在我们Linux下有一个环境变量:LD_LIBRARY_PATH,操作系统会去这个环境变量下的路径去搜索动态库,我们可以将我们的第三方库加入到这个环境变量中,然后我们再运行我们的可执行程序就能成功了。

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/yrj/Linux/file/FileSystem/mylib_static_dynamic/mylib

注意: 更改环境变量只是临时方案,因为重新登录后会失效,需重新添加才能成功执行

2. 建立软链接

我们知道Linux中C/C++的默认库路径是/usr/lib64/lib64,这也是系统搜索库的默认路径,我们可以将我们的第三方库在二者之中下面建立一个软连接(不推荐直接将第三方库拷贝到默认库路径/usr/lib64/lib64下面),这样我们也能够正常使用了。

sudo ln -s /home/yrj/Linux/file/FileSystem/mylib_static_dynamic/mylib/libmymath.so /lib64/libmymath.so

通过 ldd 查看程序链接情况: 

因为软链接是一个正常的文件,永远保存在磁盘上,所以我们退出后再次登录时,程序依然可以正常运行。

3. 更改配置文件

在我们的Linux系统中有一个配置文件目录/etc/ld.so.conf.d,在这个目录里面我们可以创建一个文件,文件里面写上动态库的路径,这样我们系统在搜索动态库时也会搜索到该路径。

 现在我们自己在此目录下 touch 一个配置文件,并在该配置文件内输入所需库的路径:

 更改完配置文件后,需要让该配置文件生效。采用指令:

ldconfig

​​​​​​​

注意: 后两种方法都可以做到永久生效(因为存入了系统目录中),但在使用完后最好删除,避免污染系统环境 

七、动静态库的加载

(一)静态库的加载

在形成可执行程序的链接期间,静态库中的代码会被直接拷贝一份进入可执行程序内。所以在程序运行期间静态库可以理解为不会被加载,或者说静态库和程序一起被加载。

但是由于是静态库,当多个进程包含相同的静态库时这会导致内存中存在大量的重复代码,导致内存资源的浪费。

(二)动态库的加载

1. 加载过程

当使用动态库编译好了一个可执行文件后,该可执行文件存储在磁盘当中,并在运行时加载到内存里。

我们知道,程序被加载到内存后就变成了进程,OS会在内存中创建对应的 task_struct 、 mm_struct 、 页表 。用户在执行程序中的代码时,正常执行。当需要执行动态库内的代码时,OS会先在内存中搜寻动态库是否存在,如果存在,就直接将动态库通过页表进行映射到进程的进程地址空间中的共享区中,否则就会将磁盘中的动态库加载进入内存,然后再通过页表进行映射,映射到虚拟地址空间的共享区中。这些动作都是由OS自动完成的。

可执行文件在被编译完成时,就已经具备了对应的虚拟地址。以上动作完成后,再执行动态库内的代码,OS会自动识别,并跳转到虚拟地址的共享区部分,通过页表的映射关系,执行内存中对应的动态库代码,动态库代码执行完毕后,再回到虚拟地址的代码区部分,继续执行下面的其他代码。

换句话说,只要把库加载到内存,映射到进程的地址空间后,进程执行库中的方法,就依旧还是在自己的地址空间内进行函数跳转即可。

2. 动态库地址的理解

在程序编译链接形成可执行程序的时候,可执行程序内部就已经有地址了,地址一共有两类,分别是绝对编址相对编址。我们知道被编译好的程序内部是有地址的!动态库内部的地址并不是绝对地址,而是偏移量!(相对地址)

动态库必定面临一个问题:不同的进程,运行程度不同,需要使用的第三方库是不同的,这就注定了每一个进程的共享区中的空闲位置是不确定的。如果采用了绝对编址,在一个进程使用了多个库时就有可能照成地址冲突!因此,动态库中函数的地址,绝对不能使用绝对编址,动态库中的所有地址都是偏移量,默认从 0 开始。简单来说,库中的函数只需要记录自己在该库中的偏移量,即相对地址就可以了。

当一个库真正的被映射到进程地址空间时,他的起始地址才能真正的确定,并且被OS管理起来。OS本身管理库,所以OS知道我们调用库中函数时,使用的是哪一个库,这个库的起始地址是什么。当需要执行库中的函数时,只需要拿到库的起始地址,加上对应函数在该库中的偏移量,就能够调用对应函数了。

借助函数在库中的相对地址,无论库被加载到了共享区的哪一个位置,都不影响我们准确的找到对应函数,也不会与其他库产生冲突了! 所以这种库被称为动态库,动态库中地址偏移量被称为与位置无关码。

八、动态库知识补充

当同时拥有 静态库动态库 时,默认采用动态链接;

如果想要使用静态链接,则需要在编译时加上 -static 命令选项。 

 如果只有静态库,但又不指定静态链接,会发生什么?

会默认使用动态链接。而对于其他的,比如C库等等,依然默认使用动态链接。 

可以看看以上三种方式生成的可执行程序大小:

静态链接生成的程序比动态链接大得多,并且内含静态库的动态链接程序,也比纯粹的动态链接程序大,说明程序不是 非静即动,可以同时使用动态库与静态库 。

总结:

关于动静态库的优缺点可以看看下面这个表格

区别 动态库 静态库
调用方式 通过函数位置进行调用 直接将需要的函数拷贝至程序中
依赖性(运行时) 需要依赖于动态库 可以独立于静态库运行
空间占用 共享动态库中的代码,空间占用少 拷贝代码会占用大量空间
加载速度 调用函数,加载速度慢 直接运行,加载速度快

相关推荐

  1. Linux虚拟文件系统

    2023-12-09 17:04:02       36 阅读
  2. linux 文件系统

    2023-12-09 17:04:02       32 阅读

最近更新

  1. TCP协议是安全的吗?

    2023-12-09 17:04:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2023-12-09 17:04:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2023-12-09 17:04:02       19 阅读
  4. 通过文章id递归查询所有评论(xml)

    2023-12-09 17:04:02       20 阅读

热门阅读

  1. 四、C#笔记

    2023-12-09 17:04:02       31 阅读
  2. 【自学篇】Python篇-第一天温度转换

    2023-12-09 17:04:02       41 阅读
  3. 力扣98. 验证二叉搜索树

    2023-12-09 17:04:02       36 阅读
  4. 胶囊网络实现手写数字分类

    2023-12-09 17:04:02       37 阅读
  5. git修改commit信息

    2023-12-09 17:04:02       36 阅读
  6. 传世SUN引擎如何安装

    2023-12-09 17:04:02       31 阅读
  7. CoreDNS实战(八)-递归服务器

    2023-12-09 17:04:02       43 阅读
  8. Linux常用命令详解与示例

    2023-12-09 17:04:02       38 阅读
  9. WPF DataGrid 里面的ToggleButton点击不生效

    2023-12-09 17:04:02       41 阅读