上次介绍了基础IO(二):Linux:基础IO(二.缓冲区、模拟一下缓冲区、详细讲解文件系统)
文章目录
- 1.软硬链接
- 1.1硬链接
- 1.2软链接
- 使用场景
- 2.动态库和静态库
- 1.1回顾
- 1.2静态库的制作和使用
- 为什么要有库
- 制作者角度
- ar指令—提取静态库文件
- gcc -l -L 来使用自己的静态库
- 1.3动态库制作和使用
- 形成`.o`文件与生成共享库
- 使用makefile文件整合操作
- 开始使用库
- 1.4完善动态库过程
- gcc -I
- 拷贝文件简化选项
- 解决运行找不到问题
- 3.理解动态库的加载
- 3.1系统角度
- 3.2编址
- 3.3一般程序的加载
- 3.4动态库的加载
1.软硬链接
1.1硬链接
硬链接(Hard Link)是Linux系统中的一种文件链接方式,它允许多个文件名指向同一个inode(索引节点),从而实现多个文件名指向同一个物理文件数据块。
- 硬链接与原始文件之间没有任何区别,它们共享相同的inode和数据块,因此对任意一个文件的修改都会影响其他所有硬链接指向的文件。
- 当您修改目标文件时,硬链接也会反映这些修改,因为它们实际上指向同一个数据块。同样,删除硬链接并不会影响目标文件的数据,只是删除了硬链接与inode号的映射关系
- 硬链接本质上是一个新的文件名,它与目标文件具有相同的inode编号,这种关系可以理解为新的文件名与目标文件的inode号之间的映射关系
- 硬链接本质就是在指定的目录下,插入新的文件名和目标文件的映射关系,并让inode的引用计数++
创建硬链接:要创建硬链接,可以使用ln
命令,语法如下:
ln <target> <link_name>
<target>
是要创建硬链接指向的目标文件的路径。<link_name>
是要创建的硬链接的名称。
上面的那个数字是:硬链接数
- 文件的硬链接数是指指向该文件的硬链接的数量。每个文件都会有一个链接计数(link count),用来记录指向该文件的硬链接数量。
当创建一个文件时,该文件的链接计数会初始化为1。当创建一个硬链接时,系统会增加该文件的链接计数。当删除一个硬链接时,系统会减少该文件的链接计数。只有当文件的链接计数减少到0时,系统才会真正删除该文件的数据块,并释放文件占用的空间。
- 我们知道文件都有一个与之相关联的inode结构体,inode中包含了文件的元数据信息,例如文件类型、大小、权限等,同时也包含了一个引用计数(reference count)字段。
引用计数记录了指向该inode的硬链接数量,也就是指向该文件的硬链接数。当创建一个硬链接时,系统会增加该文件对应inode的引用计数;当删除一个硬链接时,系统会减少该文件对应inode的引用计数
当文件的引用计数减少到0时,系统会执行以下操作:
- 将文件的inode标记为未使用,即将inode所在的数据结构标记为可重用。
- 将文件数据块的位图标记为未使用,表示这些数据块可以被其他文件使用。
- 将文件数据块内容清空,但并不立即释放磁盘空间,而是保留这些数据块的位置信息,以便之后写入新的数据时可以直接覆盖这些块,避免频繁的磁盘写入操作,提高性能。
这种方式称为延迟释放(delayed freeing)
1.2软链接
软链接(Symbolic Link)是Linux系统中用于创建文件链接的一种方式。它是一个特殊类型的文件,其中包含指向另一个文件或目录的路径。软链接与硬链接不同,软链接与原始文件之间是独立的,它们有不同的inode编号。
- 软链接(符号链接)是一个独立的文件,其中存储着指向目标文件的路径信息
- 当系统访问软链接文件时,实际上会根据软链接文件中存储的路径信息找到目标文件。软链接文件类似于Windows下的快捷方式,它提供了一种间接引用目标文件的方式
创建软链接:要创建软链接,可以使用ln -s
命令,ln
是Link
的缩写,-s
代表soft
:
ln -s <target> <link_name>
<target>
是要创建软链接指向的目标文件或目录的路径。<link_name>
是要创建的软链接的名称。
使用场景
- 软链接:软链接本身并不包含可执行代码,而是指向其他文件的路径。如果指向的文件是一个可执行文件,并且符号链接本身具有执行权限,那么可以通过符号链接执行目标文件。
可以在当前路径下建立一个软链接指向较深出的文件。方便我们快速找到
通过在当前路径下创建一个软链接,可以方便地访问位于其他较深路径的文件。这样可以简化文件路径的输入,提高操作效率,同时也可以避免频繁切换目录。
假设您经常需要访问一个深层次的文件
/path/to/deep/file.txt
,可以在当前路径下创建一个软链接,比如ln -s /path/to/deep/file.txt shortcut.txt
,这样您只需要在当前路径下使用./shortcut.txt
就可以访问目标文件了。软链接的特性使得它可以指向任意路径下的文件,包括目录。这种灵活性使得软链接成为管理文件系统中复杂结构的有用工具,提高了文件系统的可访问性和可操作性。
- 硬链接:一个目录下有多少个目录可以通过当前目录的硬链接个数减2
Linux中,不能给目录建立硬链接!
因为环路问题不允许给目录建立硬链接!
除非系统自己给目录建立硬链接:.
和..
2.动态库和静态库
1.1回顾
ldd
是一个Linux命令,用于打印出一个可执行文件或共享库的动态链接依赖关系。通过运行ldd
命令,您可以查看一个可执行文件或共享库所依赖的其他库文件,以及这些库文件的路径。使用
ldd
命令的基本语法:ldd <executable_file>
<executable_file>
是您要查看动态链接依赖关系的可执行文件的路径。
- 为什么需要库?
- 库是预先编译的可重用代码的集合,用于提供常用功能和服务。通过使用库,开发人员可以避免重复编写相同的代码,提高代码重用性和开发效率。此外,库还可帮助开发人员处理系统差异性,使得程序在不同系统上能够正确运行。
- 有哪些类型的库?
- 库分为动态库和静态库两种类型。动态库(Dynamic Link Libraries)在程序运行时加载到内存中,而静态库(Static Libraries)在编译时被链接到可执行文件中。在云服务器中,默认安装的是动态库。
- 云服务器是默认安装动态库的,没有安装静态库
- 如何查询程序的依赖关系?
- 使用
ldd
命令可以查询一个可执行文件所依赖的动态链接库。
- 使用
- 什么是静态链接?
- 静态链接是将库的代码和数据在编译时直接复制到可执行文件中的链接方式。通过在编译时静态链接库,可生成一个独立于系统环境的可执行文件。
- 默认编译程序时,使用的是动态编译。如果想要使用静态编译,需要加上
-static
选项 - 库的命名规则:
- 动态库通常以
libXXX.so
的形式命名,而静态库通常以libXXX.a
的形式命名 - 在库的真实名称中,通常会去除
lib
前缀和.so
或.a
后缀,只保留名称部分。剩下的就是真实名称
- 动态库通常以
1.2静态库的制作和使用
为什么要有库
- 提高代码的重用性和开发效率:库中包含了经过封装和优化的代码片段,可以提供常用功能和服务。通过使用库,开发人员可以避免重复编写相同的代码,提高代码的重用性和开发效率。
- 隐藏源代码
- 简化开发:库提供了现成的解决方案和功能模块,可以帮助开发人员快速构建应用程序,减少开发时间和工作量。
- 提高可靠性:经过广泛测试和验证的库通常具有较高的可靠性和稳定性,可以减少程序中的错误和bug,提高程序的质量。
- 处理系统差异性:不同操作系统和平台之间存在差异,库可以帮助开发人员处理这些差异性,使得程序能够在不同系统上正确运行。
- 提供标准接口:库通常提供了标准化的接口和API,开发人员可以通过这些接口与库进行交互,而不必关心底层实现细节。
制作者角度
这里我们写好了4个文件,分别是mymath.c
、mymath.h
、mystdio.c
、mystdio.h
使用:gcc -c
用于将源代码编译为目标文件(object file)。目标文件包含了机器代码,但它还不是一个完整的可执行程序,因为它还缺少一些信息,如启动代码、库函数的链接等。
-c
选项告诉 GCC 只进行编译阶段,不进行链接阶段。这样,你可以得到 .o
(object file)后缀的目标文件,而不是可执行文件。而且文件默认生成的名字与源文件相同,改下后缀
我们把二者进行打包,传给user(给这个user使用)
ar指令—提取静态库文件
ar命令是一个用于创建、修改和提取静态库文件的工具。静态库是编译后的程序代码集合,包含一组函数或其他对象文件,可以在链接时与可执行文件一起使用。通过将多个目标文件合并到一个静态库中,可以将其作为单个实体进行管理和分发,有助于减小可执行文件的大小和编译时间。
ar命令的基本语法如下:ar [参数选项] [归档文件名] [目标文件列表]
。
- c:创建归档文件。
- r:向归档文件中添加目标文件。
- d:从归档文件中删除目标文件。
- t:列出归档文件中包含的目标文件列表。
- x:从归档文件中提取目标文件。
- a:在库的一个已经存在的成员后面增加一个新的文件。
- b:在库的一个已经存在的成员前面增加一个新的文件。
- m:移动成员在库中的位置。
- u:替换或更新库中的成员。
- v:显示操作过程。
ar -rc libmyc.a *.o
写一份测试代码来使用一下这个库
#include "mymath.h"
#include "mystdio.h"
#include <stdio.h>
int main()
{
int res = myAdd(10, 20);
printf("%d+%d=%d\n", 10, 20, res);
myFILE *fp = my_fopen("log.txt", "w");
if(fp == NULL) return 1;
return 0;
}
gcc -l -L 来使用自己的静态库
在GCC编译器中,
-l
选项用于链接库文件。它后面跟着要链接的库的名称,不包括前缀“lib”和扩展名。例如,如果想链接一个名为libmath.so的数学库,可以使用-lmath
选项。GCC会在默认的库路径中搜索该库,并将其链接到生成的可执行文件中。此外,
-l
选项仅仅告诉GCC在链接阶段使用哪个库,但它并不指定库文件的搜索路径。如果需要指定库文件的搜索路径,可以使用
-L
选项。例如,-L/path/to/library
将告诉GCC在/path/to/library
目录下搜索库文件。
1.3动态库制作和使用
形成.o
文件与生成共享库
上面我讲解了,静态库的制作和使用,我们在形成.o
文件时,都是使用gcc -c code.c
==> code.o
。我们在打包时也是使用功能ar
现在我们使用:
- shared:
当我们在编译或链接一个库时,我们通常会指定它应该是一个共享库。这意味着该库的文件格式是为了与其他程序共享而设计的。在GCC或其他类似的编译器中,通常使用-shared
选项来指示应该生成一个共享库。(共享库就是动态链接库) - fPIC (Position Independent Code):
位置无关代码(PIC)是一种代码类型,它不依赖于它在内存中的具体地址来运行。这是共享库所需要的,因为共享库可以在程序的运行时被加载到任何内存地址。使用-fPIC
选项(在GCC中)告诉编译器生成这样的代码(产生与位置无关码)。 - 库名规则:libxxx.so:
在Linux系统中,共享库通常遵循特定的命名约定。它们通常以lib
开头,后跟库的名字(例如xxx
),并以.so
结尾。这里的.so
代表“shared object”,即共享对象
为了创建一个名为libmyc.so
的共享库,使用如下的命令:
gcc -shared -fPIC -o libmyc.so *.o
使用makefile文件整合操作
makefile文件内容如下
libmyc.so:mymath.o mystdio.o
gcc -shared -o $@ $^
mymath.o:mymath.c
gcc -c -fPIC $<
mystdio.o:mystdio.c
gcc -c -fPIC $<
.PHONY:clean
clean:
rm *.o libmyc.so -rf
过程与细节:
- 首先,Make 工具会查找当前目录下的 Makefile 文件,并读取其中的规则和指令。
- Make 工具会检查每个规则中的目标文件(target)和依赖文件(prerequisites)的时间戳,以确定哪些文件需要重新生成。
- 如果某个目标文件不存在,或者某个依赖文件的时间戳比目标文件的时间戳更新,那么 Make 工具会执行该规则中定义的命令来生成目标文件。
- 对于上述的 Makefile 文件,首先会检查
libmyc.so
文件是否存在,以及其依赖文件mymath.o
和mystdio.o
的时间戳情况。- 使用
make
指令时,Makefile 会按照默认规则执行第一个目标(在这里是libmyc.so
),并且只会执行第一个目标所依赖的规则。在这个例子中,libmyc.so
依赖于mymath.o
和mystdio.o
,因此会执行这两个规则来生成对应的目标文件
在 Makefile 中,
$<
是一个自动变量,用于表示当前规则中的第一个依赖文件(prerequisite)。当 Make 工具执行规则时,$<
会被替换为当前规则中的第一个依赖文件的文件名。使用
$^
会将所有的依赖文件传递给命令,而使用$<
只会传递当前规则中的第一个依赖文件。如果有多个依赖文件,$<
会在每个依赖文件上执行一次。因此,在 Makefile 中,如果有多个依赖文件,并且在命令中使用了
$<
,那么命令会在每个依赖文件上执行一次。
也能写成这样:
libmyc.so:mymath.o mystdio.o
gcc -shared -o $@ $^
%.o:%.c
gcc -c -fPIC $<
.PHONY:clean
clean:
rm *.o libmyc.so -rf
%.o: %.c
这个规则是一个模式规则(pattern rule),表示所有以
.o
结尾的目标文件依赖于对应的.c
源文件。在生成目标文件的命令中,使用了$<
。$<
表示当前规则中的第一个依赖文件,即对应的.c
源文件。因此,命令gcc -c -fPIC $<
会将对应的.c
源文件编译成目标文件。
开始使用库
首先把.h
文件交给使用者
cp ./*.h user
1.4完善动态库过程
首先我们完善一下makefile文件,添加能生成一个压缩包,里面分有include目录和lib目录,分别放有头文件和库
libmyc.so:mymath.o mystdio.o
gcc -shared -o $@ $^
mymath.o:mymath.c
gcc -c -fPIC $<
mystdio.o:mystdio.c
gcc -c -fPIC $<
.PHONY:clean
clean:
rm *.o libmyc.so -rf
.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
使用make output后,就能生成满足要求的压缩包
现在,如果直接进行编译会报错
gcc -I
在使用 gcc
编译器时,-I
选项用于指定头文件的搜索路径(在展开头文件时)。头文件通常包含在 #include
指令中,用于引入外部库或自定义的头文件。告诉编译器,可以在-1指定的路径下进行搜索头文件
gcc -I <include_path>
其中 <include_path>
是指定的头文件搜索路径。您可以在 -I
后面指定多个路径,用冒号或空格分隔。
现在要使用
gcc -I ./mylib/include -lmyc -L ./mylib/lib/libmyc.so main.c
-I ./mylib/include
:这个选项告诉编译器在./mylib/include
目录中查找头文件。在编译过程中,编译器会在指定的路径中搜索您在源代码中包含的头文件。
-lmyc
:这个选项告诉编译器链接名为libmyc.so
的库文件。通常,-l
选项后面跟着的是库文件的名称,编译器会在指定的库路径中查找该库文件。
-L ./mylib/lib
:这个选项告诉编译器在./mylib/lib
目录中查找库文件。编译器会在指定的路径中搜索您指定的库文件,以便在链接阶段正确地链接库文件。
拷贝文件简化选项
所谓的把库(其他软件)安装到系统中,本质就是把对应的文件,拷贝到系统指定的路径中(机器规定好的,一般都是root的目录)
所以经常需要我们普通用户使用
sudo
我们可以把自己的头文件与库分别拷贝到系统头文件目录下和库目录下
sudo mylib/include/* /usr/include
sudo mylib/lib/libmyc.so /lib64
这样后直接使用
gcc -lmyc main.c
就能实现编译链接
还是不建议大家把自己写的不是很成熟的代码,放到系统库里
解决运行找不到问题
链接生成可执行程序后,但在执行可执行文件时出现 “not found” 错误,通常是由于系统无法找到所需的动态库文件导致的。这种情况通常发生在链接的是动态库,并且操作系统无法实时找到该动态库的情况下。(所以对于动态库,编译器要能找到,OS也要能找到)
解决这个问题的方法可以采用以下几种方式:
-
默认路径拷贝:
- 将头文件(
.h
文件)和动态库文件(.so
文件)拷贝到系统默认路径,如/usr/include
和/lib/
目录中。
- 将头文件(
-
增加LD_LIBRARY_PATH环境变量:
-
使用环境变量
LD_LIBRARY_PATH
来指定动态库的路径:系统运行程序时,动态库查找的辅助路径 -
没有的话可以通过以下命令设置:
export LD_LIBRARY_PATH=/path/to/your/library:$LD_LIBRARY_PATH
-
本身就有的话,使用以下命令把自己的库的绝对路径添加到LD_LIBRARY_PATH里面
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:自己库的绝对路径
-
$LD_LIBRARY_PATH
:在 Shell 脚本或命令中,$LD_LIBRARY_PATH
表示引用环境变量LD_LIBRARY_PATH
的值。使用$
符号可以获取环境变量的值,而不是直接使用变量名。因此,$LD_LIBRARY_PATH
将会被替换为LD_LIBRARY_PATH
环境变量的当前值。 -
LD_LIBRARY_PATH
:LD_LIBRARY_PATH
是一个环境变量,用于指定动态链接库(共享库)的搜索路径。通过设置LD_LIBRARY_PATH
环境变量,您可以告诉系统在哪些路径中查找动态链接库。这个环境变量在编译和运行需要动态链接库的程序时非常有用。
-
-
-
使用软连接:
-
使用
ln -s
命令创建动态库文件的软链接到系统默认搜索路径中,例如:sudo ln -s /path/to/my/libmyc.so /lib64/libmyc.so
-
这样可以在默认搜索路径中建立软链接,使系统能够找到动态库文件。
-
-
修改配置文件:
- 在
/etc/ld.so.conf.d/
目录下新建文件,并在文件中加入自定义库的路径。 - 然后运行
sudo ldconfig
命令来更新系统的动态链接库配置,使系统能够实时找到动态库文件。
- 在
如果是官方的库的话,我们最好还是直接使用方法一。这样方便我们查找
自己的库的话,使用方法三就行
3.理解动态库的加载
动态库的加载是指在程序运行时,操作系统会将动态库加载到内存中,并将程序与动态库建立链接,以便程序能够调用动态库中的函数和资源。动态库的加载是延迟加载的,即在程序需要调用动态库中的函数时才会加载相应的库。
静态库在编译时会被整合到可执行文件中,因此在程序运行时不需要额外加载库文件。静态库的所有代码和数据都会被复制到可执行文件中,因此程序在运行时可以独立执行,不需要依赖外部的库文件。
总的来说,动态库的加载是指在程序运行时将库文件加载到内存中,并建立链接关系,使得程序能够调用库中的函数和资源。而静态库在编译时已经被整合到可执行文件中,因此在程序运行时不需要加载外部库文件。
动态库、共享库的本质就是:所有系统进程中公共的代码和数据,只需要存在一份!
3.1系统角度
在程序加载时,代码通常被加载到进程的虚拟地址空间中的代码段中。当程序执行到调用库函数的代码时,CPU会跳转到库函数的代码所在的内存地址,并开始执行库函数的代码
在动态库加载之后,动态库的代码和数据会被映射到进程的共享区中,使得进程可以直接访问和调用动态库中的函数和资源
当一个动态库已经加载到物理内存中,已有进程正在使用该库时,如果另一个进程也需要使用同一个动态库,操作系统会采取共享内存的方式,使新的进程的地址空间直接映射到已加载的动态库的内存处
谁来决定那些库加载了,哪些没加载?0S会自动完成
在Linux系统中,决定哪些库会加载,哪些库不会加载的主要责任在于动态链接器(dynamic linker)和运行时链接器(runtime linker)。这两个组件是操作系统的一部分,负责在程序运行时动态加载和链接库文件。
在操作系统中,可以同时存在大量的已加载库,这些库可能是系统自带的标准库、第三方库或用户自定义的库。操作系统需要管理这些库,以确保程序能够正确运行并提供良好的性能。
- 先描述,再组织
3.2编址
编址是指为内存中的每个存储单元分配一个唯一的地址,以便程序可以准确地访问和操作内存中的数据。在计算机系统中,编址是非常重要的,因为程序需要通过地址来定位和访问内存中的数据和指令。编址的过程涉及到操作系统、编译器以及可执行程序本身的格式信息。
尽管可执行程序还没有被加载到内存中,但它仍然具有地址的概念。在编译过程中,编译器会为程序中的各个变量、函数等符号分配地址,这些地址通常是相对地址或者符号表中的偏移量。这样,即使程序还没有被加载到内存中,各个符号仍然具有自己的地址。
此外,可执行程序在磁盘上已经被划分为不同的区域,这些区域通常包括代码段、数据段、符号表等。这些区域的划分通常是在编译器生成可执行文件时完成的,根据程序的结构和需要,编译器会将程序划分为不同的区域,并为每个区域分配相应的权限和访问属性。
因此,即使可执行程序还没有被加载到内存中,它仍然具有地址的概念,并且已经被划分为不同的区域。这些地址和区域的信息在程序加载到内存时会被操作系统读取,并根据这些信息将程序的各个部分映射到进程的虚拟地址空间中。
所以,我们之前讲解的进程地址空间里的虚拟地址,其实是由编译器在编译过程生成的。后来由操作系统读取,成为虚拟地址
编址方式有两个:绝对编址和相对编址/逻辑编址
- 绝对编址(平坦模式):地址都是连续的
- 相对编址/逻辑编址:会为每个不同的区域的开始处(start)分配一个地址后,其后地址为相对于start的偏移量(0、1、2····、n)
对于平坦模式Linux在使用,我们能认为可执行程序只有一个区域:地址=start+偏移量(现在start=0,偏移量就是本身的绝对编址)
3.3一般程序的加载
在CPU中,
CR3(Control Register 3
)是一种控制寄存器,用于存储页目录表的物理地址。在x86架构的处理器中,CR3寄存器用于指示当前进程的页目录表的物理地址,从而实现虚拟内存的地址转换。当处理器需要访问内存时,会使用CR3寄存器中存储的页目录表地址来查找对应的页表,进而将虚拟地址转换为物理地址。
PC指针(Program Counter)
是另一个重要的寄存器,用于存储当前正在执行的指令的地址或下一条将要执行的指令的地址。PC指针在程序执行过程中不断更新,指向当前指令或即将执行的下一条指令的地址。处理器根据PC指针中存储的地址来获取下一条指令的内容,并执行相应的操作。
地址空间既然是一个数据结构对象,谁来用什么数据初始化呢?——使用ELF格式的表头信息来进行初始化
在操作系统中,地址空间的初始化通常是由操作系统内核来完成的。当一个进程被创建时,操作系统会负责为该进程分配新的地址空间,并初始化这个地址空间。在这个过程中,操作系统可以使用可执行和可加载文件的头部信息(如ELF格式的表头信息)来进行初始化
ELF(Executable and Linkable Format)是一种常见的可执行文件格式,用于存储可执行程序的代码、数据和其他相关信息。ELF文件包含了各种表头信息,用于描述程序的布局、段的位置、大小、权限等信息。操作系统可以解析这些表头信息,根据其中的段表(Section Header Table)和程序头表(Program Header Table)等内容来初始化进程的地址空间。
所谓的地址空间,本质是由操作系统+ 编译器 + 计算机体系结构(CPU)三者共同配合完成的
- OS要能构建进程,进程地址空间,页表等
- 编译器要能在编译时产生逻辑地址和表头信息,供地址空间与页表的初始化
- CPU要有相关寄存器:
cr3
与pc指针等,还要能从虚拟地址转换为物理地址
3.4动态库的加载
动态库库的数据和方法的访问,都是可以通过库在地址空间
起始地址+我们程序内部的偏移量即可
现在IO基础也是全部讲完啦。下面开始进程间通讯了(这周是考试周好累啊)