1. 软链接

1.1. 概念

  1. 软链接概念:也称为符号链接,是一种特殊类型的独立文件,它不直接包含文件的实际内容,而是包含了指向目标文件或目录的路径,类似于Windows中的快捷方式。当访问链接时,系统会根据链接中存储的路径找到并访问目标文件或目录。

ln -s 目标路径 链接路径


在这里插入图片描述

1.2. 特点

  1. 存储内容:软链接文件不直接包含文件的实际内容,而是包含了指向目标文件或目录的路径字符串。

  2. 独立文件:软链接文件在文件系统中是一个独立文件,拥有自己的inode编号和属性。

  3. 访问方式:通过软链接访问文件或目录时,系统首先读取软链接文件中的内容(即:路径信息),然后根据该路径找到并访问实际的目标文件或目录。

  4. 跨文件系统:软链接可以跨文件系统使用,即:可以链接到不同文件系统中的文件或目录。

  5. 删除与移动:如果目标文件被删除或移动,软链接将变为无效(即:死链接),但删除软链接本身,不会影响目标文件或目录。

1.3. 应用场景

  1. 快速访问文件:当某个文件位于深层次的目录中,通过创建一个指向该文件的软链接,就可以快速访问到该文件,简化了文件访问过程。这类似于Windows中的快捷方式。

  2. 动态库版本管理:对于共享库,可以使用软链接来管理不同版本动态库之间的切换。如:创建一个指向最新版本动态库的软链接,当库升级时,仅需要更新软链接指向新版本的动态库。

2. 硬链接

2.1. 概念

  1. 硬链接概念:指多个文件名指向同一个物理文件的链接关系。这些链接在文件系统中具有相同的inode编号,但可以位于不同的目录中。

建立硬链接,并没有新建文件,而是为文件创建了新的文件名,硬链接不是一个独立的文件,因为inode与目标文件的inode相同。

  1. 硬链接本质是在指定目录下,插入新的文件名和inode之间的映射关系,并让inode中的硬链接计数++。

ln 源路径 硬链接路径

2.2. 硬链计数

一、硬链接计数的概念

  1. 概念:在linux中,每个inodo中都有一个硬链接计数,它表示有多少个文件名指向这个inode。

二、硬链接计数的变化场景

  1. 创建文件:当创建一个新文件时,这个文件的硬件计数默认为1,因为此时只要一个文件名指向这个inode。

  2. 创建硬链接:当使用ln创建一个文件的硬链接,不会创建一个新的文件,只会为该文件创建一个新的文件名,并增加inode中的硬链接计数。

  3. 删除文件:当删除一个文件时,会删除指向该文件的文件名,并减少inode中的硬链接计数,如果此时硬链接计数等于0,文件才会被删除,系统回收该inode及其所占的磁盘空间。

三、硬链接计数的限制

  1. 不能跨文件系统:硬链接只能在同一文件系统内创建,不能跨文件系统或分区,因为inode只在分区内唯一。

  2. 不支持目录:在linux中,默认情况下,不能对目录创建硬链接,否则会导致环路问题。

问:为什么目录链接计数默认不为1?

  • Linux中,每个目录都包含两个特殊的目录 “.“和”. .”,".“代表当前目录,”. ."代表上级目录,它们在目录被创建时就自动生成,并作为硬链接存在。

💡Tips:一个目录下,子目录数 = 主目录硬链接计数 - 2。

2.3. 特点

  1. 共享inode:硬链接与原始文件共享同一个inode编号,意味着它们指向同一个物理文件的数据块,对物理文件所做的任何更改都将反映在所有硬链接上。

  2. 增加访问路径:创建硬链接实际上是为文件增加了一个新的访问路径或文件名。

  3. 无差别访问:无论通过哪个文件名访问文件,都指向同一个inode,即:指向同一个文件内容。因此硬链接与原始文件无差别,二者等价。

  4. 节省空间:由于硬链接共享相同的inode和数据块,因此它们不会占用额外都磁盘空间。

  5. 删除与移动:如果删除目标文件或硬链接,只要inode中的硬链接计数不为0,文件仍可以被硬链接访问,直到硬链接计数=0,文件才被真正删除。

  6. 只能在同一文件系统(分区)内创建,不允许对目录创建硬链接。

2.4. 应用场景

硬链接应用场景如下:数据备份、数据共享、数据恢复、版本控制、自动同步等。

3. 动静态库

  1. 库文件:将多个对象文件(.o文件)打包在一起的文件。

如果我希望其他人能够使用我编写的方法,通常的方法是提供一组同名方法.o文件(预编译的对象文件),然后将所有.o文件打包形成一个库文件,和相应的同名方法.h头文件。即:库文件 + 头文件。

3.1 库存在的原因

  1. 提高开发效率。

库底层封装了重复的代码和常用的功能,使得开发者在编写项目时可以直接使用这些现成的代码,而无需从头开始编写,这大大减少了开发的时间,提高了开发效率。

  1. 隐藏源代码。

这意味着用户只能看到库提供的接口(通过头文件.h),而无法直接访问或修改库中的源代码,这有助于保护开发者的知识产权和代码实现。

3.2. 静态库制作与使用

#include"my_add.h"

int my_add(int a, int b)
{
    return a + b;                                                                    
}
#include"my_sub.h"

int my_sub(int a, int b)
{
    return a - b;                                                                    
}

3.2.1 打包

ar [options] archive-file object-files

  1. archive-file是静态库名(lib库名.a);object-files是要添加到库中的对象名(.o文件)。

  2. 常见的选项

-c:创建库文件,如果库已存在,则会被覆盖。

-r:向库文件中添加.o文件,如果.o文件已在库中存在,则会被替换。

-t:列出库文件中包含的.o文件列表。

-v:在执行过程中显示详细的信息。

第一步:编译形成 .o 文件。

第二步:使用ar命令,将所有.o文件进行打包,形成静态库文件。

第三步:将库进行标准化。

  1. 库的标准化:通常将头文件或库文件分别放在不同的文件夹中,可以提高项目的可维护性和组织性。

libmyc.a:my_add.o my_sub.o  //第二步:使用ar命令,将所有.o文件进行打包,形成静态库文件
    ar -rc $@ $^

%.o:%.c  //第一步:编译形成 .o 文件
    gcc -c $<
 
.PHONY:clean  
clean:
    rm -rf *.a *.o mylib mylib.tgz
  
.PHONY:output  //第三步:将库进行标准化
output:   
    mkdir -p mylib/include
    mkdir -p mylib/lib 
    cp -rf *.h mylib/include/
    cp -rf *.a mylib/lib/
    tar czf mylib.tgz mylib   

3.2.2. 使用

#include"my_add.h"
#include"my_sub.h"
 
int main()
{
    int a = 4, b = 3;
    printf("%d + %d = %d\n", a, b, my_add(a, b));
    printf("%d - %d = %d\n", a, b , my_sub(a, b));
    
    return 0;                                                                            
}
  1. -I(i的大写):用来指定编译器搜索头文件的额外路径。

当编译器在编译过程中遇到#include指令时,它先会在标准的位置(当前目录或系统默认的头文件路径)来查找指定的头文件,如果查找不到,编译器就会使用-I指定的路径进行搜索。

  1. -L:用来指定链接器搜索库文件的额外路径。

当链接器在链接中需要找到某个库文件(.so、.a),它先会在标准的位置(系统默认的库路径)中查找,如果查找不到,链接器就会使用-L指定的路径进行搜索。

  1. -l(L的小写):用来指定链接器在链接过程中要链接的库。

补充:头文件的搜索路径:当前目录、系统默认的头文件路径(/usr/include、/usr/local/include)、gcc内置的标准头文件路径、命令行中通过-l选项指定的头文件路径。 库文件的搜索路径:系统默认的库文件路径(/usr/lib、/usr/local/lib)、gcc内置的库文件路径、命令行中通过-L选项指定的库文件路径、环境变量LIBARY_PATH中指定的路径。

这些选项分别用于控制编译和链接过程中的头文件、库文件的搜索路径和库文件的选择。

3.3. 动态库制作与使用

3.3.1. 打包

第一步:使用-fPIC选项,编译形成 .o 文件。

  • -fPIC:用于指示编译器生成与位置无关的代码,无论代码被加载到内存的哪个位置,它都能正确运行,而不依赖于它在编译或加载时的具体地址。这种特性通过使用相对寻址,而不是绝对寻址来实现的。这对于创建共享库是至关重要的,因为共享库可以在进程地址空间的任何位置被加载。

第二步:使用-shared,将所有.o文件进行打包,形成动态库文件。

  • -shared:告诉编译器gcc生成一个共享库(.so或.dll文件)。

第三步:将库进行标准化。

libmyc.so:my_add.o my_sub.o  //第二步:使用-shared,将所有.o文件进行打包,形成动态库文件
    gcc -shared -o $@ $^

%.o:%.c  //第一步:使用-fPIC选项,编译形成 .o 文件
    gcc -c -fPIC $<
 
.PHONY:clean  
clean:
    rm -rf *.so *.o mylib mylib.tgz
  
.PHONY:output  //第三步:将库进行标准化
output:   
    mkdir -p mylib/include
    mkdir -p mylib/lib 
    cp -rf *.h mylib/include/
    cp -rf *.so mylib/lib/
    tar czf mylib.tgz mylib   

3.3.2. 使用

现象:程序运行时,OS加载动态库文件myc失败,即:系统找不到这个文件。

分析如下:a. 静态库在编译阶段会被链入到可执行程序中,即:库的代码和数据会被直接拷贝到最终可执行程序中;程序在运行时不在需要静态库文件。 b.动态库在编译时只需要找到头文件,包含了对库中函数和数据的引用(通常是符号名和地址偏移量);程序在运行时,OS需要找到动态库文件,以便将库中的代码和数据加载到内存中。如果OS找不到动态库文件,程序无法加载,从而导致运行时错误。

4. 解决’动态库查不到’的4种方法

4.1. 库安装

  1. 将库或其他软件安装到系统中,本质是把对应的文件,拷贝到系统默认的搜索路径中。

  2. 在64位系统,系统中库默认的搜索路径为/lib64、/usr/lib64;在32系统,系统中库默认的搜索路径为/lib、/usr/lib。

4.2. 软链接

4.3. /etc/ld.so.conf.d配置文件

  1. /etc/ld.so.conf.d目录下的配置文件,用来指定额外的库文件的搜索路径,以便动态链接器能够在运行时找到并加载这些库文件。

4.4. LD_LIBRARY_PATH环境变量

  1. LD_LIBRARY_PATH:是一个环境变量,在linux中,为动态链接器指定额外的库搜索路径。

5. 动静态链接的选择

  1. 动静库同时存在:gcc默认会使用动态库进行动态链接;若想要进行静态链接,必须加上-static选项。

  1. 只存在静态库:可执行程序只能进行静态链接,程序不一定整体是静态链接的。

可执行程序只能进行静态链接,指的是对于特定库的链接方式: 当系统中只存在某个库的静态版本,任何尝试链接到这个库的程序都必须使用使用静态链接,因为动态链接器在运行时无法找到这个库的动态版本。

程序不一定整体是静态链接的,取决于它所依赖的所有库,以及编译链接时的选项: 一个程序可能依赖于多个库,此外,即使程序使用了静态链接来链接某些库时,它仍可能依赖于动态链接的库(C、C++标准库或系统库),这些库在OS级别上提供,且以动态链接的形式存在,以便在多个程序之间共享。

  1. 只存在动态库:只能进行动态链接,若非得进行静态链接,编译器或链接器会报错。

6. 理解动态库加载

6.1. 站在OS角度理解

一、动态库概念

  1. 动态库:也称为共享库,是一种包含代码和数据,可以在多个程序之间共享的文件,存放在磁盘上。

与静态库不同,静态库在程序编译时会被完全复制到可执行文件中,而共享库则在程序运行时被加载到内存中,如果多个程序使用同一个共享库,OS会让这些进程共享内存中的同一份库代码和数据,即:动态库的代码和数据在内存中只存在一份。

  1. 管理:系统中可以同时存在多个已经被加载的库,OS需要管理它们,先描述(包含了加载地址等信息)、再组织。

二、动态库加载的过程

  1. 检查依赖:程序启动时,动态链接器会检查该程序依赖的所有动态库。

  2. 搜索路径:动态链接器会在预设的库搜索路径中查找所需的动态库文件。

  3. 加载与映射:第一次加载、后续加载。

第一次加载:如果动态库尚未被加载到内存中,动态链接器会将该库加载到内存中,并映射到进程地址空间的共享区中。

后续加载:如果其他进程也需要共享这个库,动态链接器会检查内存中是否已存在该库;如果已存在,只需修改地址空间中共享区的映射关系,指向已存在的库副本;如果不存在,则重复第一次加载的过程。

  1. 优点:节省内存、易于更新、提高了程序的性能和安全性。

6.2. 编址

  1. 可执行程序在编译和链接完成后就已经具有了明确的格式信息,且在文件中按照类别划分了不同的区域 (如:代码段、未初始化数据段(BBS段)、初始化数据段),每个区域都有其对应的地址信息,通常是相对地址(虚拟地址)。

  2. 编址:在编译和链接阶段,为程序和库中的符号(变量、函数)分配地址的过程,主要有绝对编址、相对编址两种方式。

可编址的范围:32位平台,[0, 2^32] -> [0, 4GB] ,64位平台,[0, 2^64] -> [0, 16GB]。

  1. 绝对编址:在编译和链接过程中,符号的地址是固定的,即:已经确定了符号的实际的物理内存地址。这种方式要求程序运行时,必须加载到特定的物理地址处,否则无法正确的运行。

绝对编址中的地址 == 实际的物理内存地址。

在计算机早期,程序使用绝对编址,因为当时系统比较简单,程序通常使用直接映射到物理内存的绝对编址,而没有复杂的内存管理和保护机制,如:虚拟内存、页表等。

  1. 相对编制:也成为逻辑地址、虚拟地址。在编译和链接过程中,符号的地址是不固定的,而是相对于某个基地址的偏移量。这种方式允许程序在加载时动态确定实际地址,从而实现位置无关代码。

符号地址 = 基地址 + 偏移量。基地址在编译链接阶段是未知的,通常是由OS在程序加载时分配的虚拟地址,是在地址空间内的一个起始地址,如:0x400000。

现代计算机系统广泛使用相对编址,因为这提供了更好的灵活性和安全性。

问:地址空间、页表中的数据来自哪里?

  • 当基地址确定后,OS会使用可执行程序中各个区域的虚拟地址,来初始化进程地址空间、页表。每个区域在进程地址空间都有一个相对应的虚拟地址范围。在程序被加载到内存中,OS会自动分配物理内存,并构建页表来建立虚拟地址和物理地址之间的关系。

每个可执行程序大小不同,说明了每个程序中各个区域虚拟地址范围也会不同。相应地,当这些程序被加载到内存变为进程时,则每个进程地址空间中各个区域的虚拟地址的范围也是不同的。

6.3. 理解动态链接和加载问题

6.3.1. 一般程序的加载

一、一般程序加载的过程

  1. 读取可执行文件,并解析文件中各个段的信息:OS先从存储介质(如硬盘)中读取可执行文件(程序代码、数据以及依赖的库),然后OS会解析可执行文件中的各个段(如代码段、数据段、堆栈段等)的信息。这些段包含了程序运行所需的所有指令和数据。

  2. 分配虚拟地址空间、并确定基地址。

  3. 加载程序段、初始化地址空间:OS将可执行文件中的各个段加载到地址空间的相应位置。

4.重定位:在加载过程中,OS会进行重定位操作。这包括解析程序中的符号引用(如函数名、变量名等),并将它们转换为实际的物理内存地址。

  1. 初始化数据段和未初始化数据段、构建页表。

  2. 设置程序计数器(PC指针):在程序开始执行之前,OS会将PC指针设置为程序入口点的地址(main函数的地址),这是程序执行的第一条指令的虚拟地址。

  3. 执行程序:CPU会根据程序计数器中的地址从内存中读取指令,并执行它。

二、地址空间的构建和管理,需要由CPU、编译器、OS三者共同配合完成。

  1. CPU提供硬件支持,cr3寄存器存储页表的指针,MMU负责将虚拟地址转化为物理地址。

  2. 编译器负责将源代码编译成机器码,并生成可执行文件,这个文件包含了各个段的信息; 编译器在编译过程将程序中的符号地址编译成相对于基地址的偏移量;

  3. OS负责创建地址空间,并选择一个基地址; OS负责加载程序的各个段来初始化地址空间,并进行重定位来确定实际物理内存; OS负责创建和初始化页表,将虚拟地址转化为物理地址; OS处理缺页中断,动态分配物理内存。

三、CPU执行程序的过程

OS读取可执行程序表头中的入口地址(main),把它交到CPU,CPU的程序计数器指向main函数的虚拟地址0x401020,CPU从这个地址开始执行指令:取指令(虚拟地址) -> 地址转换 -> 分析指令 -> 执行指令 -> 更新PC指针(虚拟地址)。

CPU从当前PC指向的虚拟地址处读取指令,即:取指令 -> CPU中MMU使用页表将虚拟地址0x401020转化为物理地址0x112233,即:地址转换 -> CPU从物理地址0x112233处读取指令,并解码指令,即:分析指令 -> CPU根据解码的指令执行相应的操作,即:执行指令 -> 更新程序计数器,使其指向下一个指令的地址。

6.3.2. 动态库的加载

对于库的数据和方法的访问,都是可以通过库在地址空间的起始地址+程序内部的偏移量来实现。

Logo

助力广东及东莞地区开发者,代码托管、在线学习与竞赛、技术交流与分享、资源共享、职业发展,成为松山湖开发者首选的工作与学习平台

更多推荐