Linux——软硬链接与动静态库
软硬链接与动静态库
文章目录
- 软硬链接与动静态库
- 1. 软硬链接
- 1.1 软链接(符号链接)
- 1.1.1 如何建立软链接
- 1.1.2 软链接的特点
- 1.2 硬链接
- 1.2.1 如何建立硬链接
- 1.2.2 硬链接的特点
- 1.2.2.1 引用计数
- 1.2.2.2 环路问题
- 1.3 删除链接
- 2. 动静态库
- 2.1 库的必要性
- 2.2 库名
- 2.3 静态库
- 2.3.1 制作静态库
- 2.3.2 使用静态库
- 2.4 动态库
- 2.4.1 动态库的制作
- 2.4.2 使用动态库
- 2.5 库的查找以及头文件的查找
- 2.5.1 动静态链接
- 2.5.2 库的查找
- 2.5.3 头文件的查找
- 2.6 动静态库的优先级
- 2.7 动态库的加载
1. 软硬链接
1.1 软链接(符号链接)
1.1.1 如何建立软链接
建立软链接的命令:
ln -s [被链接的文件] [软链接的名字]
- 其中,被链接的文件可以是一个文件,也可以是一个目录
例如
给一个文件建立软链接:
给一个目录建立软链接:
1.1.2 软链接的特点
我们以给一个文件建立的软链接为例,看看这个软链接和被建立链接的文件有什么不同:
可以发现,当我们查看二者的inode时,它们的inode编号是不一样的,这说明了以下几点:
- 软链接
mybin.so
拥有独立的inode编号,而我们在Linux——磁盘文件中提到过,inode编号
用于唯一的标识一个文件 - 因此,软链接
mybin.so
不同于被链接的文件mybin
,它是一个独立的文件
那么,软链接存放的究竟是什么呢?
对于上面链接了一个可执行程序的软链接
mybin.so
,我么可以试着./
运行一下:对于上面链接了一个目录的软链接
test1.so
,我们也可以cd
一下:可以发现,使用软链接和被链接的文件的方法几乎没有区别
这一点其实很像我们Windows系统的快捷方式,只要我们运行某个快捷方式,这个快捷方式对应的可执行程序也就被运行起来了
我们可以点击快捷方式的属性,来查看它存放的信息:
可以看到,它存放的就是目标文件所在的路径
通过上面的分析,可以得出结论:
软链接存放的就是被链接的文件的路径
同时,我们发现,上面的快捷方式指向的文件实际是在E盘
,而一般来说,放在电脑桌面的快捷方式是存放在C盘
的,而同样在Linux——磁盘文件中提到过,一个分区就是一个文件系统,因此又可以得出一个结论:
软链接是可以跨文件系统进行链接的
- 而之所以能够实现这一功能,就得益于它拥有独立的inode编号和数据块,是一个独立的文件
1.2 硬链接
1.2.1 如何建立硬链接
建立硬链接的命令:
ln [被链接的文件] [硬链接的名字]
- 需要注意,只能给一个具体的文件建立硬链接,不能给目录建立硬链接
例如:
给一个可执行程序
mybin
建立硬链接mybin.hard
给一个目录建立硬链接:
1.2.2 硬链接的特点
同样,我们来查看硬链接和被链接文件的inode编号
:
可以看到,硬链接和被链接的文件的inode
编号是一样的,这也i就说明了:硬链接并不是一个独立的文件
那硬链接到底是什么?实际上,对一个文件建立硬链接其实就是新建一个文件名,并将这个文件名与被链接的文件的inode编号
建立映射关系,并使该文件的引用计数加一。
1.2.2.1 引用计数
引用计数其实就是文件权限后面的那个数字,有多少个文件名与该文件的inode建立了映射关系,这个引用计数就是几:
现在我们删除源文件mybin
,看硬链接mybin.hard
是否还能使用:
可以看到:
- 尽管我们删除了源文件,但这只是导致了对应的
inode编号
的引用计数减一,文件的内容并没有删除,链接它的硬链接也可以正常使用。而之所以有这样的现象,就是因为系统操作文件时只认文件的inode编号,因为硬链接和源文件共享inode编号,因此可以通过硬链接找到源文件的内容- 同时我们可以推论:只有当
inode编号
的引用计数减小到0时,其对应的文件内容(数据块)才会被置为空闲
我们都知道目录也是一个文件,因此目录也有它对应的引用计数,那么目录的引用计数有什么特点呢?
我么可以先建立一个空目录进行观察:
可以看到这个空目录的引用计数为
2
,这是因为:
- 目录的目录名
test1
与inode
建立了映射关系- 目录底下隐藏文件
./
表示当前目录,也与inode
建立了映射关系我们在这个空目录
test1
底下建一个目录test2
:可以看到,该目录的引用计数变为了
3
,这是因为:
test1
目录底下的test2
底下,有一个隐藏文件../
,该文件与test2
的上级目录也就是test1
的inode建立了映射关系,因此test1
的inode的引用计数要加一通过上面的分析,我们可以通过一个目录的引用计数来快速得到这个目录底下包含有多少个目录
目录包含的目录个数 = 该目录的引用计数 - 2
- 减去的2,即自己的文件名,和该目录底下的隐藏目录
./
- 剩下的就是包含的每个目录的隐藏目录
../
,也就可以得到包含的目录个数了
1.2.2.2 环路问题
我们之前提到硬链接不能给目录创建,就是因为如果给目录创建硬链接,就会导致环路问题
例如:
假设我们在
/home/TEST
下给根目录/
建立硬链接ln / dir
,那么未来就会出现这样的情况:
- 如果未来我们要在所有路径下查找一个文件,那么由于硬链接指向的是一个目录,因此系统也会进入这个目录进行查找
- 由于硬链接指向的是根目录
/
,因此又会进行相同的查找- 循环往复
可以看到,如果发生环路问题,就会导致系统查找文件的逻辑无法正常的终止,这就会持续消耗系统的资源,甚至导致系统崩溃
1.3 删除链接
有两种方式删除链接:
rm <链接名>
unlink <链接名>
2. 动静态库
2.1 库的必要性
考虑这样一种情况:
未来我们编写的程序实现了一个算法,并且我们想让其他人也能使用这个算法,但是我们并不希望使用这个算法的用户能够看到这个算法的源代码,我们该如何做到?
要解决这个问题,我们就需要知道一个代码文件编译的全过程:预处理、编译、汇编、链接
- 预处理:进行换替换、头文件展开等操作,生成
.i
文件- 编译:对与处理过后的文件进行语法分析和词义分析,如果没有错误,生成
.s
文件- 汇编:将生成的
.s
文件转换为汇编代码,生成.o
目标文件- 链接:将目标文件与对应的库文件进行动静态链接,形成最终的可执行程序
注:以上四个过程在程序的翻译环境和运行环境、Linux——编译器gcc/g++有较为详细的阐述
知道了上述四个过程后,我们就可以想到一种解决方案:
将自己编写的算法代码编译形成.o
目标文件,最后将这个.o
目标文件和对应的头文件交给用户即可
例如:
我们设计了一个实现简单加减乘除的功能代码
math.cc
,提供给user
使用:首先将
math.cc
编译成一个目标文件math.o
:g++ -c -o math.o math.cc -std=c++11
将对应的头文件和目标文件提交给
user
:cp math.hpp math.o user
最终用户就可以利用这个目标文件
.o
来实现自己的功能了:g++ -c -o main.o main.cc g++ -o mybin main.o math.o ###################################### g++ -o mybin main.cc math.o -std=c++11 #也可以简写为这种形式
在上面的例子中,我们就可以把math.o
看作是一个原始的库文件,在链接阶段就会把main.o
与math.o
进行链接,形成最终的可执行程序mybin
现在问题又来了:
如果我们设计了很多个算法(几十个甚至上百个),这些算法又都实现在不同的文件里,那么如果我们想让其他人使用这些算法,我们总不能将这些算法文件都编译成
.o
文件,然后让用户一个个的进行链接吧?
这就需要用到我们的库来解决问题了,我们可以将对应的.o
目标文件和头文件封装成一个动态库或者静态库,再将这个动静态库发送给用户,用户就可以方便的通过库来使用我们编写的算法了
总的来说,库有以下两大优点:
- 隐藏源文件
- 提高开发效率
2.2 库名
我们可以利用命令:ldd [filename]
来查看一个可执行程序依赖的库文件,例如:
可以看到,上面的可执行程序mybin
依赖了libstdc++.so.6、libc.so.6、libm.so.6
等库,其中:
.6
是版本后缀.so
表示动态库的后缀,动态库的后缀还可以是.sl、.sa
,但是.so
最常用.a
表示静态库的后缀lib
是所有库的前缀- 去掉库的前缀和后缀才是正真的库名,例如上面的
stdc++、c、m
2.3 静态库
2.3.1 制作静态库
命令;
ar -rc [lib库名.a] [被打包的.o文件]
例如:
如上图,我们就成功制作出了一个静态库——libmymath.a
2.3.2 使用静态库
命令:
gcc/g++ -o [生成的可执行程序] [主程序] -l[去掉前后缀的库名] -L [所依赖库的路径] -I [所依赖头文件的路径]
- 如果头文件就在当前路径,可以不指定所依赖头文件的路径
例如:
2.4 动态库
2.4.1 动态库的制作
被用来打包动态库的不能是普通的.o
文件,其还需要一个位置无关码 PIC
命令:
gcc/g++ -c -fPIC [被编译的文件] -o [生成的.o文件]
-fPIC
就是指在生成目标文件的同时还要生成一个位置无关码PIC
得到了具有位置无关吗的.o
目标文件后,就可以将他们打包为一个动态库了:
命令:
gcc/g++ -shared -o [lib库名.so] [被打包的.o文件]
-shared
即创建一个共享库(动态库)
例如:
2.4.2 使用动态库
和静态库的使用一致,命令:
gcc/g++ -o [生成的可执行程序] [主程序] -l[去掉前后缀的库名] -L [所依赖库的路径] -I [所依赖头文件的路径]
例如:
可以看到,我们成功的使用动态库来编译了一个文件并生成了一个可执行程序,但是我们又看到了一个奇怪的现象:我们执行可执行程序时发生错误:系统找不到我们的动态库libmymath.so
。这是为什么?
接下来我们进行分析:
2.5 库的查找以及头文件的查找
我们需要知道,Linux系统中,库的默认查找路径是/lib、/usr/lib
等目录
同时我们还需要知道静态链接与动态链接:
2.5.1 动静态链接
静态链接:
即如果一个程序在编译时链接到的是一个静态库,那么就会和这个静态库进行静态链接
静态链接是在编译时就完成的
静态链接会将静态库的内容加载到可执行程序中
因此在执行最终生成的可执行程序时便不需要再查找静态库所在的路径了,即这个可执行程序不依赖外部库文件
动态链接:
即如果一个程序在编译时链接到的是一个动态库,那么系统就会发生如下的操作:
编译时链接(静态绑定/早期绑定):
- 对于那些系统库或其他共享库的函数,编译器或链接器并不会直接将他们替换为原本的内容,而是会生成对这些函数的引用表。这样可执行程序并不会包含这些函数的具体实现,而是只会保留对他们的引用
运行时链接(动态链接)
- 在运行可执行程序时,会发生动态链接。操作系统会将可执行程序加载到内存并解析其中对动态库的引用,之后会发生两个重要的步骤:
- 加载动态库:操作系统会将被依赖的动态库加载到内存中
- 重定位:对于可执行程序中部分函数对动态库的引用,操作系统会根据库在内存中的实际地址,将这些引用进行重定位,使其指向正确的函数或数据的位置
因此,由于生成的可执行程序并没有包含使用库函数的实际内容,而是在运行时对动态库的地址进行查找,从而找到函数的实现,因此在运行可执行程序时仍然需要找到所依赖的动态库的地址,这也就是上面的例子出错的原因
关于动静态链接的总结:
静态链接:
- 优点:
- 使用静态链接生成的可执行程序不再依赖外部的库文件,因此独立性较好
- 依赖静态库的可执行程序在运行时不需要像动态库那样还需要进行库加载、重定位等操作,因此启动速度更快
- 缺点:
- 静态链接会将静态库的内容加载到可执行程序中,因此依赖静态库的可执行程序往往较大,不利于节省空间资源
- 由于生成的可执行程序不依赖静态库,因此如果对静态库进行了跟新,就需要对整个程序进行重新编译,因此可维护性较差
动态链接:
- 优点:
- 动态链接不会将动态库的内容直接加载到可执行程序中,而是生成引用表,因此占用空间小,节省空间资源
- 依赖动态库的可执行程序不直接包含动态库的内容,而是对动态库的引用(地址),在运行时会进行动态库的查找,因此如果对动态库进行了更新,不需要对可执行程序重新编译,即依赖动态库的程序可维护性好,利于更新
- 缺点:
- 由于生成的可执行程序仍依赖动态库,因此独立性较差,如果动态库丢失,则程序无法运行
- 由于在执行可执行程序时还需要执行运行时链接(加载动态库、重定位等操作),因此程序的启动速度较慢
回到上面的问题,总而言之就是由于:
可执行程序mybin
依赖的是动态库,因此会在运行时对动态库进行查找,但是我们自己创建的动态库并不在系统的默认查找路径中,从而导致了运行错误
因此,我们需要让系统能够查找到库,有如下方法:
2.5.2 库的查找
方法一:将可执行程序依赖的动态库直接下载到系统的默认查找路径下
例如,我们将动态库libmymath.so
下载到默认路径/lib
下:
方法二: 可以在系统的默认查找路径下, 建立一个链接到动态库的软链接
注: 软链接的名称必须与动态库的全名(前缀 + 名称 + 后缀)相同, 否则系统还是会找不到
例如:
方法三: 修改环境变量LD_LIBRARY_PATH
环境变量
LD_LIBRARY_PATH
的作用是告诉系统: 找库时, 除了要查找默认查找路径, 还要在设置的路径地下进行查找注:
如果系统没有环境变量
LD_LIBRARY_PATH
, 可以通过export
命令进行添加
LD_LIBRARY_PATH
的主要作用是在程序运行的时候,告知系统动态库的搜索路径,并不会在编译时起作用。因此尽管修改了该环境变量,在编译时还是需要用-L <lib_path>
选项。
例如:
同时也需要注意, 由于环境变量是内存级的, 因此每一次关闭终端, 环境变量都会变为初始值, 如果想要让这个环境变量永久生效 , 我们需要修改文件~/.bashrc
, 即将命令export LD_LIBRARY_PATH=/home/lwj/user:$LD_LIBRARY_PATH
添加到~/.bashrc
文件中:
总结:
通过上面的诸如将自己编写的动态库下载到系统的默认查找路径、在默认查找路径建立软链接、修改环境变量的方式,就可以在不告诉系统库路径的情况下,让系统自己去查找程序依赖的库,而不会发生错误。
例如,通过在系统的默认查找路径下下载动态库或者建立与动态库的软链接的方式,编译时就不需要添加
-L <lib_path>
选项了住:为保证系统的稳定性,一般不建议修改系统目录
2.5.3 头文件的查找
头文件的默认查找路径为:当前路径和/usr/include
目录
因此,如果你要编译的程序所依赖的头文件在这两个系统默认查找的路径下,就不需要添加-I <include_path>
选项了
而如果所依赖的头文件不在这两个路径下,又不想使用-I <include_path>
指定路径,下面三种方法同样可以可以让系统找到程序所依赖的头文件
方法一:将头文件下载到/usr/include
下
例如:
方法二:在/usr/include
下建立与所依赖头文件的软链接
例如:
方法三:修改环境变量
在 C 和 C++ 编程中:
可以使用
C_INCLUDE_PATH
(用于 C 头文件)和CPLUS_INCLUDE_PATH
(用于 C++ 头文件)环境变量来指定额外的头文件搜索路径。
例如:
2.6 动静态库的优先级
编译程序时,如果存在同名的动静态库,那么程序会优先链接到动态库
注:如果想要使用静态库进行编译,在g++/gcc命令的末尾处加上-static
选项即可
例如
2.7 动态库的加载
如果一个程序依赖的是静态库,编译时静态库的内容会直接下载到程序中,因此运行时静态库并不会再次加载入内存中,即静态库不存在加载问题
接下来我们来讨论动态库的加载
如果一个可执行程序依赖了动态库,那么当运行这个可执行程序时,程序和其所依赖的动态库都会加载到物理内存中
加载到内存中的可执行程序会形成一个进程,并生成他的进程地址空间,并通过页表构建与物理地址的映射关系
由于程序依赖了动态库的方法,因此为了在物理内存中找到动态库,也需要通过页表来建立task_struct到物理内存中动态库的映射关系(系统会将动态库通过页表映射到虚拟地址空间的共享区中)
如果又有另外的可执行程序依赖了同样的动态库,并将这个可执行程序加载到了内存中形成了进程,那么由于动态库之前已经被加载到物理内存了,此时只需要建立这个进程的虚拟地址空间与动态库的映射关系就可以了
可以看到,两个不同的进程依赖了相同的被加载到内存中的动态库,因此动态库也叫做共享库,被共享的内存也被称为共享内存
从而也可以得出,动态库的使用可以大大节省内存资源
本篇完