Linux:文件

前言

  文件的原理是什么?我们是如何向文件中写入的?先提出一个问题,一个空文件放到磁盘上,它有大小吗?

1. 文件操作的初识

  先回答上面的问题,一个文件没有数据,它占据磁盘空间吗?答案是占据的。文件 = 内容 + 属性,属性也是数据,所以一个空文件在磁盘上也是占据空间的。
  内容是数据,属性也是数据,。存储文件其实是既存储内容数据,也存储属性数据。我们要访问一个文件的时候,需要先把这个文件打开。其中我们指的是谁,一个文件打开前是什么样的,打开后又是什么样的?

对文件的操作都是在程序中进行的,只有当程序在CPU运行变成进程时,才会发生对文件的操作,所以访问文件,实际上是进程在访问文件。而一个文件在没有被打开前,它还是在磁盘上的,它就是一个普通的磁盘文件。打开文件意味着它已经要被CPU执行了,而文件是在磁盘,根据冯诺依曼体系结构,CPU只与内存打交道,所以打开文件实际上就是将文件加载到内存。

  加载磁盘上的文件,就是要访问磁盘设备,访问硬件,这就是需要操作系统来处理。而一个进程又可以打开多个文件,多个进程就可能会打开很多的文件,那么这么多的文件是不是就需要被管理呢?那答案自然是需要的,那就我们先前学的,先描述再组织。所以我们可以认为在内存中是一个结构体来管理众多被打开文件的。
  所以文件按照是否被打开分为:被打开文件(在内存中),没有被打开文件(在磁盘中),所以对文件的操作就是以此为出发点的。我们研究文件操作的本质就是:进程和被打开文件之间的关系。
  关于文件的操作,可以参考我之前总结的C语言文件操作,里面有一些关于文件的写入写出。在这里插入图片描述

2. 文件操作

2.1 打开关闭文件

  其实说进程打开文件是不准确的,底层实际上是进程通过操作系统提供的系统调用来打开文件。而上面C语言打开文件的函数其底层一定是封装了系统调用。在这里插入图片描述在这里插入图片描述
  这就是系统调用接口,那我们就先来看看参数是什么意思。在这里插入图片描述
  第一个参数就是文件名的意思,我们主要是看第二个参数是什么意思。
在这里插入图片描述
  这里圈起来的就是第二个参数,意思就是只读、只写、可读可写、当文件不存在时创建一个文件等待含义,那么这么多参数要如何使用呢?这明明只有一个参数,而我们想要完成可读(O_RDONLY)并且当文件不存在时要创建一个文件(O_CREAT)这就有两个参数,这是如何传参的呢?其实它们都是一个个的宏,我通过例子来讲解它们的用法:
在这里插入图片描述在这里插入图片描述
  这个参数实际都只是使用一个bit位来表示,然后再通过或运算来实现两个不同的功能。我们来一起看看具体的写法:在这里插入图片描述
  这个函数的返回值是:如果打开失败返回 -1,如果成功返回一个文件描述符(后面会讲)。(以这种方式打开文件并不会清空文件,如果新写入一段数据,只会根据新写入数据的大小进行覆盖)

在这里插入图片描述
  我们发现我们创建出来的文件是红色的,这是因为一个文件被创建出来是要被指明这个文件的权限是什么样的,我们创建时并没有指明这个文件的权限,所以它的权限就是乱码。这时候就要拿出我们的第三个参数了mode。
  这个参数也就是我们之间在权限篇中修改文件权限的方式一样,使用八进制数字来传参。在这里插入图片描述
在这里插入图片描述
  我们传的是666,结果我们发现最后的权限是664,这是为什么呢?这是因为里面还有个掩码啊!!!当然我们也可以把掩码去掉:在这里插入图片描述在这里插入图片描述
  这下我们创建出来的文件的权限就是666了。

2.2 读写文件

在这里插入图片描述
  第一个就是文件标识符,第二个是写入数据的地址,第三个是写入数据的大小。
在这里插入图片描述在这里插入图片描述
  我们知道字符串后面是有一个斜杠零的,它需不需要被写入到文件中的,也就是写入数据的大小要不要写成strlen(ch) + 1呢?这是不需要的,因为斜杠零只是C语言层面上的规定,并不是文件上的说法,所以不需要。

O_TRUNC:打开文件之前会将文件清空。
O_APPEND:在文件结尾追加数据。

2.3 fd介绍

  看了上面的示例代码,我们可能会对打开文件的返回值有所疑惑,为什么返回值是一个整数,在写入的时候也是对一个整数进行写入,它到底是什么呢???
在这里插入图片描述在这里插入图片描述
  我们发现这是一连串的数字,这代表什么呢?前面我们说了操作系统是需要将被打开文件用一个结构体管理起来的,这个结构体叫做struct file,它用来管理诸多被打开的文件,但是我们又知道一个进程可以打开多个文件,那么操作系统是如何区分哪个文件是被哪个进程打开的呢?
  因此在每个进程的PCB中还需要记录自己打开的文件有那些,所以在进程PCB中会有一个struct file_struct *files;的字段,它是一个指针,指向一个专门用来管理自身进程打开的文件。在这个结构体中有一个指针数组,存放了每一个被打开文件的地址,而我们上面fd(文件描述符)返回值就是这个数组的下标,就是通过下标来准确找到被打开文件的。

在这里插入图片描述
  那么0、1、2是存放什么呢?进程在打开时会默认打开三个文件,标准输入(键盘,stdin),标准输入(显示器,stdout),标准错误(显示器,stderr)。它们三个分别占据0、1、2。在这里插入图片描述
  如何证明呢?它们的返回值都是FILE*,而不是一个整数,这分明不符合呀。其实FILE是一个结构体,而fd就在这个结构体中,在结构体中fd被命名为_fileno,有兴趣的小伙伴可以看源码噢,我们来看:在这里插入图片描述在这里插入图片描述

  而操作系统 / C语言为什么要默认将stdin、stdout、stderr打开呢? 当然就是为了让程序员默认进行输入输出的代码编写啦!!!就像我们写的第一份代码,hello world,如果操作系统不默认打开,我们这些当时的小白怎么写代码呢???
  而前面我们又说过一切皆文件,这又要如何理解呢?
在这里插入图片描述
  在此基础上看,也就是在我们的视角,只需要调用每个struct file的读写方法,以统一的方法调用各个硬件。在C++中,这是不是就是多态呢??? 上面就是基类,read就是虚函数,下面就是派生类,以此来完成通过调用相同的函数,来进行不同的功能!!!

2.4 struct file的简单理解

  在内核中,读写数据是如何进行的呢?
在这里插入图片描述

  那我们就来看看源码是什么样的:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.5 fd分配规则

  进程已经默认打开了0、1、2,我们可以直接使用0、1、2。
在这里插入图片描述在这里插入图片描述
  而如果我们把0号文件关闭了,我们这个文件的fd是会不会还是3呢?
在这里插入图片描述
在这里插入图片描述
  我们发现变成了0,也就是说新文件会从头开始扫描数组,找到第一个为空的地方进行填充。(寻找最小位置,分配给指定的文件)

  我们知道1是代表这stdout,如果我们把这个关闭会发生什么呢?
在这里插入图片描述
在这里插入图片描述
  我们发现将stdout文件关闭后,它就不会再向显示器上打印数据了,但是我们却发现在log.txt中出现了我们本应该向显示器打印的数据,这是为什么呢?
  我们知道文件描述符会找到最小的位置来分配,所以1位置的文件已经从stdout变成了log.txt。
在这里插入图片描述
  而这就是重定向,重定向的本质是其实就是修改特定文件fd的下表内容。

2.6 重定向

在这里插入图片描述
  我们上面已经向log.txt文件中写入了一些内容,现在我们将stdin文件关闭,那么新打开文件的fd就会变成0。此时我们再使用fread读数据,本来是从stdin(键盘)读取数据(也就是fd为0的文件),但是现在fd=0的文件已经从stdin变成了log.txt,所以现在读出的数据就不再是从键盘读了,而是变成了从log.txt中读。在这里插入图片描述
  所以重定向实际上就是文件描述符表级别的数据内存的拷贝。
  而这样的重定向每次都需要关闭文件才能实现,那有没有什么方式可以直接实现呢?
在这里插入图片描述
  自然是有的,那就是dup2系统调用。参数就是用oldfd覆盖掉newfd。dup2 (fd,1) 意思就是将新打开文件的fd覆盖掉下标为1(stdout)的地方。那这是不是就是有两个地方都存放了原fd呢?是的,在内核中会用引用计数的方式解决这个问题。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
  那么进程替换会影响重定向吗?答案是不会的。
在这里插入图片描述
  程序替换只是替换了数据和代码,对于PCB中的有些数据会进行修改,但是大部分还是会保留下来。

2.7 strerr

在这里插入图片描述
在这里插入图片描述

  为什么我们strerr还会输出到显示器上,而stdout是输出到文件中?
在这里插入图片描述
  这是因为stderr依旧是指向显示器的,当时会输出到显示器中。那么有没有什么办法能将它们都输出到文件中呢?
在这里插入图片描述
  2>&1的意思就是将1的内容给了2。
在这里插入图片描述
  其实重定向的完整的写法是:
在这里插入图片描述
  stderr有什么用呢?
在这里插入图片描述
  可以完成不同的重定向,将错误信息写入到指定文件中,与其他内容分离。

3. 缓冲区

  缓冲区到底是什么?它是用来干什么的?
  缓冲区实际上就是一块内存空间,它是用来提高操作系统的工作效率的。我们知道当使用键盘输入数据时,根据冯诺依曼体系结构我们知道,这是需要硬件与内存进行交互的,而它们的效率比较慢,因此就需要一个缓冲区来提高它们的传输效率。
  就好比,如果我们输入abcd,每输入一个字符,键盘就与内存交互一下数据。而另一种是先用一个缓冲区将abcd先存储起来,再一起传输到内存中。这两者之间孰优孰劣想必一目了然。
  而既然缓冲区能暂时存储数据,那么必然要有一定的刷新方式,也就是什么时候向内存中(也就是OS)传输数据。这个刷新策略一般分为两种:

  1. 无缓冲(立即刷新)
  2. 行缓冲(行刷新)
  3. 全缓冲(全刷新)

  上面说的只是一般策略,在特殊情况下:

  1. 强制刷新
  2. 进程退出时,一般都要刷新缓冲区

  而同时,一般对于显示器文件,也就是向显示器上打印数据的文件,一般使用行缓冲,对于磁盘上的文件,也就是向文件中写入的数据,一般使用全缓冲。

  我们来看一个样例:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
  注意下图中fork的位置以及最后的输出结果。
在这里插入图片描述
在这里插入图片描述
  我们发现fork明明是在下面写的,为什么在log.txt中会出现关于C语言的接口会出现两遍,而系统调用的接口只出现了一遍。 这是为什么呢???

  1. 当我们直接向显示器上打印时,使用的策略是行缓冲,而每一句打印最后都带有"\n",说明在fork之前所有的数据都已经刷新完毕,都已经被打印到显示器上了,缓冲区中已经没有内容了
  2. 而当我们打印数据从显示器转换到了磁盘文件当中,那么它的刷新策略已经变了,变成了全缓冲。
  3. 全缓冲意味缓冲区变大了,实际写入的数据无法将缓冲区写满,在fork执行的时候,数据依旧在缓冲区中。在这里插入图片描述也就是说,虽然fork是在下面,但是创建出来的子进程中的文件缓冲区中是保留下来了父进程缓冲区中的数据,所以会在最后进程结束时,需要被刷新缓冲区,所以父子进程都会打印一遍数据。
  4. 但是为什么只有C语言接口的数据被打印了两遍,而系统调用接口的数据只打印了一遍呢?从结果上看我们猜测出了C语言一定是有自己的缓冲区,而系统调用是不适用C语言维护的缓冲区的,是直接写入到文件缓冲区中的!!! (上面的图只是为了方便大家对缓冲区有个参考好理解,实际上对于解释第三点问题的答案是不符合的,仅仅只是为了引出第四点的内容,下面这个才是真正准确的图解)在这里插入图片描述而这个过程也就叫做刷新!!!将缓冲区中的内容一步步拷贝到文件缓冲区中。
  5. C/C++提供的缓冲区中的数据是用户自己的数据,那么也就是属于这个进程的,所以在创建子进程时会将缓冲区中的数据也拷贝下来。而系统调用的接口是直接将数据给了操作系统,已经不是这个进程的数据了,所以子进程是无法得到系统调用接口所传输的数据的。在这里插入图片描述(此时父子进程中的缓冲区其实是一份,也就是指向的是同一份数据)
  6. 当进程退出时,一般都要刷新缓冲区,而此时的刷新 操作是不是就是一种 “写入” 或者是 “清空” 操作呢?而由于指向的是同一份数据,再进行写入时就要发生写时拷贝,就相当于父子进程各有一份打印出来的数据,然后再进程结束时再刷新缓冲区,所以最后就会得到两份C语言接口打印的数据。

  printf fwrite 库函数会自带缓冲区,而 write 系统调用没有带缓冲区。另外,我们这里所说的缓冲区,都是用户级缓冲区。其实为了提升整机性能,OS也会提供相关内核级缓冲区,不过不再我们讨论范围之内。那这个缓冲区谁提供呢? printf fwrite 是库函数, write 是系统调用,库函数在系统调用的“上层”, 是对系统调用的“封装”,但是 write 没有缓冲区,而 printf fwrite 有,足以说明,该缓冲区是二次加上的,又因为是C,所以由C标准库提供。
  上面所讲的全部,实际上都是用户级缓冲区,也就是C语言维护的缓冲区。而操作系统中的文件缓冲区,叫做内核缓冲区。同时上面我们所说的也都是用户缓冲区的刷新策略,而内核缓冲区有它自己的刷新策略。

4. 文件系统

  我们前面谈的都是被打开文件,在文件被打开之前,它是存放在哪里呢?我们是如何准确的找到它呢?其实在我们的计算机中被打开的文件只是少数,更多的是未被打开的文件,那么需不需要被管理呢?对于这部分文件的核心工作又是什么呢?
  那必然是需要进行管理的,不然你每次打开文件的时候,如何能快速找到它呢?那么它管理的核心工作是什么呢?那就只有如何能快速定位(通过路径来找)到你需要打开的文件。
  所以对于文件的管理工作分为两部分:

  1. 被打开文件进行管理
  2. 没有被打开的文件也要在磁盘中被管理。(这也就是文件系统)

  文件系统一般都指未被打开的文件,有些书籍上也指所有的文件。
  对于未被打开的文件,我们都知道它是存储在磁盘上的,那么对磁盘上的文件又是如何管理的呢?
在这里插入图片描述
  对于磁盘,是有好几个面,上面我只画出了一个面,每个面又分为扇区,一个扇区又有许多磁道,到磁道就是磁盘存储数据的基本单位,一个磁道的大小一般是512字节。我们可以把磁盘由圆形展开为长方形:
在这里插入图片描述
  每一块空间就是磁盘上的一小圈,可以形象的看成二维数组。然后再将二维数组搞成一维数组的形状。
在这里插入图片描述
  这样对于磁盘数据的管理就变成了对一维数组的管理,而512字节太小了,所有一般以八个磁道为单位,分成块来对磁盘进行写入,八个磁道的大小就是4kb。
在这里插入图片描述
  每次写入数据时,先由磁头找到对应的磁道,然后再进行写入,这样就可以对磁盘数据进行管理了。
  虽然可以管理的,但是磁盘的非常大,比如有300GB,你这一次写入4kb,是不是有一种大海捞针的感觉。所以对于整个磁盘还要进行管理。
在这里插入图片描述
  我们可以将其进行划分,因此磁盘每个地方都是一模一样的,所以对第一块100GB大小的管理方式,是不是可以直接拷贝到第二块100GB大小的区域,以此类推。(分区,对每一个区的管理就是一个文件系统,各个区的文件系统实现可以不一样,只要实现管理能力即可)
  而100GB依旧很大,我们可以再将其划分,比如大小为2GB,那么同样的道理,对于2GB空间的管理方案可以直接套用到后面的空间上,也就是对2GB空间的管理,就是对100GB空间的管理,也就是对整个磁盘空间的管理。这里是分治思想。(分组)
  那么在局部上我们是如何进行管理的呢?
在这里插入图片描述
  上面这个管理方式就叫做文件系统。在局部就是以上面的方式管理起来的,那么这些东西都是什么呢?我们知道文件 = 内容 + 属性,内容和属性都是数据,都是要被管理起来的。那么相应的就需要有东西来管理它们,所以在文件中除了我们自己的文件数据,还有许多管理的数据。 比如有一个整形字段,为0就表示该处空间未被使用,为1表示该空间已被使用,这么这个字段是不是也是数据呢?
  所以在管理磁盘时,还需要将这个管理数据加载到文件系统中,这个过程就叫做格式化。 所以在文件中不仅存放了自己的文件信息,还存放了许多管理文件的数据。
  我们来具体看看上面的管理信息。当我们创建出一个文件后,输入ls -li选项,就会发现多出了一行数据:
在这里插入图片描述

  多出来的数据就叫做 inode 编号,一般情况,一个文件一个inode,基本上每个文件都要inode,并且它在整个分区上是具有唯一性的,在Linux内核中,识别文件和文件名无关,只和inode有关。
  inode table节点表中存放了文件的属性和文件的大小,所有者,最近修改时间等等。一个节点表对应一个文件,在inode table中有很多个节点表。一个节点表中在Linux中是128字节。
在这里插入图片描述
  在每个组中都有一个起始编号,每个文件通过使用自己的inode编号减去起始编号就可以得到自己处于inode Table中的哪个位置。
  Data blocks: 存放文件内容,里面都是4kb大小的内容数据。
在这里插入图片描述
  并将每一块进行编号,1、2、3……等等。那么这么多的块,一个文件怎么知道自己的数据在哪一块上呢?
  在struct inode中会有一个int block[N]数据,数组中存放1、3、5,那就说明这个文件的内容存放在块号为1、3、5的空间中。这个N一般为15,那么就有疑惑了,只有15个那空间也不大啊。起始并不是大家想的那么简单,比如前12个空间是直接映射文件的内容,第13个空间也是4kb,采用二级映射,也就是存储的是一级映射的内容,第14块就是三级映射,里面存放的是二级映射的内容。这样就可以存放很大的数据。
  块位图(Block Bitmap):Block Bitmap中记录着Data Block中哪个数据块已经被占用,哪个数据块没有被占用。
  GDT,Group Descriptor Table:块组描述符,描述块组属性信息。
  超级块(Super Block):存放文件系统本身的结构信息。记录的信息主要有:bolck 和 inode的总量,未使用的block和inode的数量,一个block和inode的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。所以它是存放了多次的。
  inode位图(inode Bitmap):每个bit表示一个inode是否空闲可用。

  那么问题来了,我们平时使用的都是文件名,而不是inode,那么这是如何管理的呢?相比大家已经猜出来了,文件名和它的inode之间一定是有映射关系的。
在这里插入图片描述
  我们发现目录也是有inode的,说明目录的管理方式和文件是一样的,那么目录存放的内容是什么呢?没错!!!就是文件名和它自己inode的映射关系!!!(eg:a.txt:795266)
  所以一个文件中是不允许出现名字相同的文件。(Linux中文件名并不属于文件的属性。)
  那么Linux中是如何找到一个文件的呢?首先找到这个文件的目录,通过查找这个目录中的内容,找到文件名与之对应的inode,再找到所属的分区,再找到分区中的组,再通过inode编号找到自己inode结构体所在的位置。问题是,目录也是文件,找目录的时候又要如何找呢?其实就是从根目录开始找,一直找到该文件(进程打开文件,进程PCB中有cwd记录着当前工作目录)。那么还有一个问题,inode编号在每个分区中有效, 也就是说不同分区可能出现相同的indoe,如何区分呢?
在这里插入图片描述

  其实里面有一个操作叫挂载,它可以将该文件与你所在的分区进行挂钩(通过一定的方式使它们关联起来),标志着你的文件属于这个分区。
  而找到inode结构体后,将其中的属性加载到内存,再用于struct file保存起来,再通过inode找到数据块,加载到内存中,形成文件缓冲区。当用户读写文件时,先通过文件描述符找到对应的struct file,再从其中读写数据。

5. 软硬链接

  我们先来看现象:
在这里插入图片描述
  我们发现软链接的文件inode是不同的,而硬链接的inode是相同的,这就说明软链接是一个独立的文件,硬链接不是一个独立的文件。

  • 什么是链接文件?
      一个文件被打开一定要先找到它,有时候它的路径可能会很深,用户不容易去找它,就可以使用一个方法将其之间反倒桌面上,我们平时电脑上的图标实际上就是一个个链接文件,可以让我们不用去搜索这个.exe文件的路径就可以打开它。
  • 什么是软链接,什么是硬链接?
      软链接的作用就是快速定位文件,比如你写了一个可执行程序,是被打包送给别人的,比如:在这里插入图片描述  bin目录是用来存放你的可执行程序,而log目录是用来存放的日志文件,但是你交付给别人的是proj,你要是想运行代码还必须得进入到bin目录下才能运行,现在你建立一个软链接就可以运行了。在这里插入图片描述  软链接中存储的就是目标文件的路径,所以才可以通过软链接运行我们的可执行程序。
      硬链接不是一个独立的文件,实际上它是指定目录内部的一组映射关系,文件名与inode的映射关系。在这里插入图片描述  从结果上看,是不是就只是将hello文件进行重命名了呢?是的, 同时它的数字也减少1,相信不少人看到这里就能反应过来,这就是引用计数。对于一个文件是否被删除,就要看这个引用计数是否为0了。 在这里插入图片描述  这个就是表示硬链接数,那些文件名指向同一个inode。在这里插入图片描述  对于新建的目录为什么它的硬链接数为2呢?我们知道一个目录下是有隐藏文件的:在这里插入图片描述  所以它的硬连接数为2。在这里插入图片描述  当我们再创建一个目录后就会发现newdir变成了3,这是因为再dir中的 . . 目录也是指向的也是newdir。
      我们无法对目录进行硬链接,因为这样在找一个文件时容易出现死循环问题:在这里插入图片描述

6.动静态库

  • 静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
  • 动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
  • 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
  • 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)。
  • 动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。

6.1 简易理解静态库的原理

  讲理不举例,犹如放马屁:
在这里插入图片描述
在这里插入图片描述
  有点小错误,更正一下代码(懒的再截图了,后面忘使用函数了,直接使用了加减乘……)。

  这是一个多文件实现了加减乘的功能,在这里,我们用加减乘的.h和.c文件来模拟成库函数,也就是我们要求在当前目录下,在不需要.h和.c的文件下,我们单凭一个Test.c文件来运行我们的程序。
在这里插入图片描述
  在我们将.c和.h文件移走后,我们发现由于找不到各个函数所以编译不通过。因为系统只会在指定的默认路径下找我们的头文件,在这里我只是将它随便移到了一个目录中,我们只需要使用 -I 指令来指定我们的路径就好了。 在这里插入图片描述  框起来的就是存放这些函数.h文件的路径,我是存放到了当前目录下的tmp目录中的include目录中了。
  但此时我们发现了链接错我,因为此时我们写的函数还没有打包成为一个库,所以暂时是找不到对应的函数的,我们只需要将我们写的函数打包成一个静态库就可以。我们继续往下看:
在这里插入图片描述
  首先我们知道预处理、编译、汇编、链接,而与库函数链接是链接过程的任务,而在此时我们的文件已经变成了.o文件,因此在这里我们需要先将我们写的函数生成.o文件,在打包成一个静态库。(也就是在库里面,实际上都是一个个的.o二进制文件)
  而其中的 ar -rc 指令就是将.o文件打包成一个第三方库的指令。
在这里插入图片描述
  我们就可以看到生成了一个库了, 如何证明它就是一个库呢?我们先将所有的文件都移除,只留下Test.c文件:
在这里插入图片描述
  我们发现还是不通过,因为这方面的问题与找头文件一样,系统都是在指定路径下找的,库函数也一样,所以此时我们还需要指明我们所打包的库文件的路径。
  不过在此之前还要再插入一个知识点,那就是关于库的名字。我们上面是将库打包成了libmymath.a,但是这并不是库文件的名字,它的真实名字需要去掉前缀和后缀,前缀就是lib,后缀就是.a,所以它的名字其实叫mymath。话不多说,我们直接来看最后的结果:
在这里插入图片描述
  其中 -l(小写L) 后面紧跟你的库名字,然后再使用 -L 指明库文件所在的路径。(我顺便将我们的路径也给展示出来了,就是图下面的那个)。
  由此在不需要.c和.h文件的情况下,我们单凭一个Test.c就完成了文件的运行,这与我们平时使用的库函数的过程是一样的,只不过我们不需要添加路径,系统会自己找到它本身库文件的所在位置。这里由于是自己打包的,并不在指定的默认路径下,所以系统找不到,得我们自己指明路径。(当然如果你将你写的头文件和库文件都放到系统的默认路径下,也不用自己指明路径了,这在后面的动态库中会讲)

6.2 静态库小结

  • 所以.a文件(库文件)实际上就是一堆.o文件的集合。
  • 在gcc后面加上 -static 可强制该文件只能使用静态库来链接。
  • gcc 默认是使用动态库链接的,但是某些库没有动态库的情况下,才会使用静态库。
  • 系统头文件的默认路径是 /usr/include
  • 系统库文件的默认路径是 /lib64/

6.3 动态库

  关于如何打包的问题,大题上与静态库差不多,只是在指令方面有所不一样:在这里插入图片描述
  不一样的部分我都画出来了,至于下面的output,其实就是上面将我们的头文件和库文件放到一个目录中,达到与我们的Test.c文件分开的一个效果。不必理会
在这里插入图片描述
  但是此时我们按照上面的步骤运行时,就发现报错了,说无法打开这个动态库,原因是找不到,这是为什么呢?所以在这里先来说一说动静态库的一点区别。
  其实对于静态库来说,它在编译时是直接加载到了你自己写的程序中,所以运行时相当于在你写的Test.c文件中就存在静态库中的所有内容。也就是说,如果有多个文件使用同一个静态库,那么多个文件中都会自己私有一份静态库中的数据,所以一般使用静态库时,文件的大小都比较大。
  而对于动态库而言,在编译时,它并没有加载到文件中,而是通过一种寻址的方式,在内存中找到动态库,然后通过找寻动态库的地址来访问动态库中的内容。也就是说动态库是需要被加载到内存中的,同时如果有多个文件使用同一个动态库,实际上使用的是同一个,并不会存在多份动态库。(在后面的动态库链接原理部分会详细讲解)

  那么这里要如何解决呢?

  • 方法一:直接放到系统的默认路径下在这里插入图片描述  直接拷贝到系统的默认搜索路径下,这样我们就可以使用了。(但是要表明你要使用的是那个库)在这里插入图片描述  我们就可以看到我们链接的动态库了。

  • 软链接在这里插入图片描述  通过软链接我们发现也可以完成,但是后面还需要加路径,这与前面的问题一样,我们只需要将其加到系统的默认搜索路径下就可以了。(想尝试的小伙伴,自己可以试试,与上面的过程一样,这里我就不演示了)

  • 环境变量在这里插入图片描述  我们可以在这个环境变量中添加自己的库的路径。当我们生成的可执行程序就可以找到库文件的路径。在这里插入图片描述

  • 直接更改系统关于动态库的配置文件在这里插入图片描述在这里插入图片描述  通过这种方式也可以让系统找到库文件的路径。(后两者的具体的场景再现方式为:以第一种方式为基础,当生成可执行程序时,将拷贝到系统中的库文件删除,不删除可执行程序。之后文件就会找不到对应的库,再使用后两种方法可以让系统重新找到所对应的库文件)

6.4 小结

  • 同一组方法,提供两种动静态库,gcc默认使用动态库。
  • 对于第三方库,使用时需要指明库文件的名称,对于系统自身的库文件则不需要。

6.5 动态库的加载

  在打包一个动态库时,有一个FPIC没有将,这里我们来谈一谈。FPIC是与位置无关码,这是什么意思呢?我们来看图:在这里插入图片描述
  其实在磁盘上文件数据的存放也是有一定的格式的,如上图一样。
在这里插入图片描述
  不知可执行程序要加载到内存,对于的动态库也要加载到内存。当使用到某些方法时,就到对于的库文件中通过寻址的方式来找。
  那么程序在没有被加载时,有地址吗? 从上图不难看出,程序没有被加载时,也是有地址的。存放的就是一个一个方法在对于库文件中的地址。
  变量名,函数名编译成为二进制,它们还存在吗? 不在了,都变成了一些二进制数据,但实际上它们是都变成了地址。
  编译的时候,对代码进行编译,基本遵循虚拟地址那一套,虚拟地址空间,不仅仅是OS中的概念,编译器编译的时候也会按照这种方式进行编译可执行程序,这样才能在加载的时候,进行磁盘文件到内存,再进行映射。在这里插入图片描述
  所有数据都是以基地址+偏移量的返回来存放,每一种类型的数据存放在固定的区域。这与虚拟地址是一样的,它也叫逻辑地址。
  绝对编址和相对编制: 绝对编址就是以0地址为参照进行编址,相对编址就是以某一个地址为参照进行编址。这有什么不同呢?在这里插入图片描述
  你现在距离木棍30米远,距离入口20米,当以入口为参照,木棍向后移动十米,你距离木棍就变成了40米,参照不动你就不动;当以木棍为参照,木棍移动十米,你也跟着移动十米,你们两个之间的距离并不会变。前者就是绝对编址,后者就是相对编址。这里可能不好理解,下面会举一个更好的例子。
在这里插入图片描述
在这里插入图片描述
  当可执行程序被运行时,由于它也是虚拟地址,与内存中的地址空间相似,会直接被加载在地址空间的相应位置中。在符号表的位置存放了使用的库文件的起始地址,而其中的一个个方法都被替换成了相对于库文件起始地址的偏移量(相对编址),不会因为加载到共享区时,由于加载的位置不同导致各个方法相对于库文件起始地址不同而发生改变。
  上面的解释中意思是:在物理空间中存放的其实是一个个的虚拟地址吗?
  是的,CPU执行的其实也是虚拟地址,在可执行程序中有一块空间专门存放一个程序的起始地址(main函数),然后交由CPU执行,CPU通过这个起始的虚拟地址,找到该进程,再找到该进程地址空间,再经过页表找到物理内存中的物理地址,然后执行main函数中的第一个语句(该物理地址中的内容)。
  也就是在程序还没开始运行时,页表中就已经存在了一些虚拟地址和物理地址的映射关系。

总结

  本章主要讲解了有关文件的一系列知识,以及有关动静态库的概念,并且在这里重新对虚拟地址进行了更近一层的讲解。
  如果大家发现有什么错误的地方,可以私信或者评论区指出喔。我会继续深入学习Linux,希望能与大家共同进步,那么本期就到此结束,让我们下期再见!!觉得不错可以点个赞以示鼓励!!

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不如小布.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值