Bootstrap

【Linux】inode 软硬链接和动静态库

一、inode

  由之前的学习,我们知道文件=内容+属性。 要理解 inode 是什么,首先得知道文件的内容和属性是分开存储的。文件数据(内容)储存在硬盘的数据块中,那么还得必须找到一个区域来存储文件的元信息(属性),这种存储文件元信息的区域就叫做 inode(索引节点)

inode 通常包含的一些关键信息:

  • 文件的大小(字节数)
  • 文件拥有者和所属组 id
  • 文件的类型,及其读写执行权限
  • 指向数据块的指针,找到数据块的位置
  • 硬链接数,指向该 inode 的硬链接数量,当计数变为0时,文件数据被释放
  • 文件的时间戳,ctime 指 inode 上一次修改时间,mtime 指文件内容上一次修改时间,atime 指文件最后一次被访问的时间

  我们经常通过文件名来对文件进行操作,但真正找到磁盘上文件的并不是文件名,而是 inode, 在 linux 中可以让多个文件名对应于同一个 inode。下面是一些查看 inode 相关的命令

  • ls -i 命令:查看文件对应的 inode 号码
  • stat 命令:查看某个文件的 inode 信息例,是 ls 命令的增强版,提供了更详尽的文件信息
  • df 命令:查看每个硬盘分区的 inode 总数和已经使用的数量

二、软硬链接

2.1 硬链接

方法:ln 源文件 目标文件
  一个文件名只能指向一个 inode 编号,但是 Linux 系统允许多个文件名指向同一个 inode ,通过 inode 链接来产生的新的文件名,而不是产生新的文件,称为硬链接。这就意味着,可以用不同的文件名访问同样的内容。删除一个文件名,不影响另一个文件名访问。因为 inode 信息中有一项叫做链接数,即指向该 inode 的文件总数,删除一个文件名时,链接数会减一。变为 0 时,表明没有文件指向这个 inode 编号,系统才会回收这个 inode 号码与文件数据块区,它的实现方式类似引用计数。
  默认创建的目录硬链接数是2,是因为目录名(假设为dir)和 inode 建立了映射关系,同时自己目录下的 . (表示当前目录,其实就是 dir,只不过名字不同罢了)也和 inode 建立了映射关系,所以硬链接数为2。当在这个 dir 目录下再创建一个新目录 dir1 时,dir 的硬链接数会加一,因为 dir1 目录下的 .. 会和上级目录 dir 映射的 inode 也建立一个映射,所以我们可以得出:目录的硬链接数 - 2 = 该目录下的其它目录数量
  注意不能对目录做硬链接,这是为了防止目录树循环:例如,如果目录A通过硬链接指向目录B,而目录B又通过硬链接指向目录A,这将导致无限循环。虽然 ... 对目录形成了硬链接,但它们是文件系统自动维护的,并且有明确定义的行为,它们不会导致目录树中的循环问题。

2.2 软链接

方法:ln -s 源文件或目录 目标文件或目录
  软链接就是再创建一个独立的文件,即目标文件。它有自己独立的 inode ,它的内容是源文件的路径,类似于 Windows 的新建快捷方式,可以快速找到目标文件或目录。
  硬链接的源文件和目标文件具有相同的 inode 与文件块区,而软链接的源文件和目标文件都具有属于自己的 inode 与文件块区,它们的 inode 是独立的。当目标文件删除时,源文件的 inode 链接数不会因此发生变化。而目标文件依赖于源文件而存在,若删除了源文件,打开目标文件就会报错。

小结:

  • 硬链接数本质是一个引用计数
  • 存储文件元信息的区域就叫做 inode(索引节点)
  • 软链接是一个独立的文件,相当与创建了一个快捷方式
  • 硬链接就是同一文件的不同文件名,创建硬链接就是在指定目录下建立了不同文件名和指定inode的映射关系仅此而已

三、动静态库

3.1 库的重要性和优势

为什么要用库:
  使用他人编写的库可以极大提升软件开发的效率和稳健性。顶尖工程师所开发的成熟库往往经过了严格的测试和实际应用的检验,这意味着它们在功能实现上不仅高效,而且在异常处理和稳定性方面也非常可靠。当我们站在这些巨人的肩膀上进行开发时,可以专注于创新和业务逻辑的实现,而不是基础功能的构建,从而加快开发速度并减少错误发生的概率。此外,这些库通常拥有活跃的社区支持,为开发者提供了宝贵的资源和即时的帮助,进一步降低了开发和维护的成本。

  实际上,无论是静态库还是动态库,它们的核心优势在于保护源代码的同时提供给他人使用的功能。当我们创建一个库时,我们通常不希望公开源代码,但需要让其他开发者了解如何使用库中的功能。为此,我们会公开头文件,这些头文件包含了库中方法的声明,使开发者能够了解每个方法的用途和调用方式。然而,仅有头文件是不够的,因为它们只包含方法的声明,并不包含实现细节。为了让库的方法能够在其他项目中被实际调用,我们需要将实现这些方法的源代码编译,然后打包成库文件。这样,其他开发者就可以在不接触源代码的情况下,通过链接我们的库来使用其中的功能,实现功能的共享和代码的重用

3.2 静态库制作及其使用

静态库:
  为了让他人能够方便地使用我们提供的方法,我们可以将每个方法编译成单独的.o目标文件。随后,通过提供相应的.h头文件和这些.o文件,其他开发者就能调用这些方法。但是,当存在大量.o文件时,管理和使用它们会变得繁琐且效率低下,因为每个文件都需要单独链接。
  为了避免这种情况,我们可以使用ar -rc 目标文件名 源文件列表 命令将所有的.o文件整合打包成一个静态库。 rc 表示 replace and create,表示如果要生成的库中已经包含了对应的 .o 文件,就将其替换,否则就创建 。制作静态库就是将这些.o 文件打包的过程。

  • 静态库以lib为前缀 .a 作为文件后缀
  • 程序在编译链接的时候,将静态库的代码拷贝到可执行文件中,因此程序运行的时候将不再依赖库文件

使用静态库:gcc 源文件 -o 目标文件 -I 头文件路径 -L 库文件路径 -l库文件名(去掉lib前缀和.a后缀)
  由上面可知,要使用别人的方法,我们需要对应的头文件和库文件。将库拷贝到系统的默认路径下,这个过程叫作库的安装。如下图所示,在 main.c 中我们要参考 mymath.h 头文件的说明,使用 libmymath.a 库中的方法。

通过下面参见的静态库错误使用演示,我们来学习如何正确使用静态库

  从上面可以看出,我们在使用第三方库时,需要指定头文件路径和库文件路径。那为什么 C/C++ 在编译的时候,从来没有明显的使用过 -I -L -l 等选项呢?这是因为 gcc/g++ 编译器默认会在当前目录以及系统的 /usr/include 路径下搜索所需的头文件。同样,对于库文件,编译器也会在标准路径如 /lib64/usr/lib64 下进行查找。对于标准库,gcc/g++ 早已知道如何找到并链接它们,因此不需要额外的 -l 选项。这样可以简化编译过程,开发者不需要为每个标准库函数指定链接选项。如果第三方库的头文件和库文件位于这些默认路径中,编译器就能够自动找到它们。

3.3 动态库制作及其使用

动态库:
  生成动态库的过程开始于编译源代码文件,这一步骤与创建静态库相似,但有一些关键的差异。我们 gcc -fPIC -c $^ -o $@指令 将源文件编译成.o目标文件,在编译时加上 -fPIC(Position Independent Code)选项,产生位置无关码,确保生成的代码可以在内存中的任何位置执行,而不会产生地址冲突。
  接下来,我们 gcc -shared -o $@ $^指令 将这些编译后的 .o 文件链接成一个共享对象文件, -shared 选项告诉编译器你想要创建的是一个动态库,而不是一个静态库或可执行文件。生成的 .so 文件就是动态库,它可以被多个程序共享,并且在程序运行时被动态加载

  • shared:生成共享库格式
  • fPIC:产生位置无关码(position independent code)
  • 动态库以 .so 作为文件后缀
  • 程序在运行的时候才去链接动态库的代码,多个程序间共享同一份库代码
  • 一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
  • 在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接
  • gcc 编译默认是动态库,但只有静态库的时候就只能是静态库。如果动静态库都有,而非要使用静态库就加上 -static选项,摒弃默认优先使用动态库的原则,直接使用静态库的方案

使用动态库:gcc 源文件 -o 目标文件 -I 头文件路径 -L 库文件路径 -l库文件名(去掉lib前缀和.so后缀)

通过下面分析我们来学习如何正确使用动态库

  其实上面说的很清楚了,程序在运行的时候才去链接动态库的代码。但是我们只是告诉 gcc 动态库的名字及其所在的路径了,形成可执行程序后,gcc 就与之后的事无关了。此时系统是知道库名字的,却不知道库的路径!当我们修改环境变量或者系统配置文件时,就不需要再提供库名字了,只需要提供库的路径

对此我们有以下几种方法,让系统找到动态库:

  • 将库文件添加到系统/lib64或/usr/lib64路径下
  • 在系统/lib64或/usr/lib64路径中创建要使用的动态库文件的软链接
  • 将库文件添加到环境变量LD_LIBRARY_PATH中,LD_LIBRARY_PATH 是一个关键的环境变量,它指定了动态链接器 ld.so 在搜索共享库文件时应当遍历的目录。当一个程序需要加载共享库时,动态链接器会遵循特定的搜索顺序:首先,它会查找程序内部指定的路径;如果内部路径无效,它会转向 LD_LIBRARY_PATH 变量中列出的目录;如果这些路径同样无法满足需求,动态链接器将退回到系统预定义的默认路径
    export 命令来修改该环境变量:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path(自己的动态库路径)
  • 配置系统文件,当我们重新启动xshell的时候,会发现我们之前追加的环境变量信息不见了。这是因为每次重启的时候,bash 都会从配置文件中读取数据重新生成环境变量。为了让动态库永久有效,我们可以尝试修改系统配置文件。在系统 /etc/ld.so.conf.d 路径下,新增一个自己的.conf文件,文件内容就是动态库路径
    先创建conf文件,添加动态库路径sudo vim /etc/ld.so.conf.d/my.conf(自己的conf文件名)
    再更新配置文件sudo ldconfig

  操作系统在加载程序时,会为每个程序分配一个独立的进程地址空间。程序的代码、数据和堆栈等分别位于地址空间的不同区域。主体代码在代码区,程序按顺序执行,当运行到需要调用动态库中的函数时,动态库中通过页表映射加载到共享区中。然后平滑地跳转到共享区执行动态库中的函数,完成后再返回到原代码区继续执行。这使得程序能够在自己的地址空间内,无缝地执行包括动态库代码在内的所有指令。
  不同进程可以通过相同的页表映射机制,将同一个动态库加载到各自的共享区中。这样,动态库就可以在多个进程之间实现共享,这也是动态库有时被称为共享库的原因。

;