您现在的位置是:首页 >技术交流 >【linux】:老师问什么是爱情,我说了句:软硬链接和动静态库网站首页技术交流
【linux】:老师问什么是爱情,我说了句:软硬链接和动静态库
前言
上一篇文章的最后我们讲解了文件的inode,那么文件名和inode有什么区别呢?区别就在于linux系统只认inode号,文件的inode属性中,并不存在文件名,而文件名其实是给用户用的。我们以前讲过linux文件目录,那么目录是文件吗?答案是是的,目录也是文件,并且目录也有inode,任何一个文件一定在目录里面,所以目录的内容是什么呢?首先目录的内部需要数据块,目录的数据块里面保存的是文件名和inode编号对应的映射关系,而且在目录内,文件名和inode互为key值,当我们访问一个文件的时候,我们是在特定目录下进行访问的,要找到inode需要先在当前目录下,找到文件对应的inode编号,一个目录也是一个文件,也一定属于一个分区,结合inode,在该分区中找到分组,在该分组的inode table中,找到文件的inode,通过inode和对应的datablock的映射关系,找到改文件的数据块,并加载到操作系统,并完成显示到显示器,下面我们进入本篇文章的主体
一、软硬链接
理解文件的增删查改
根据文件名找到inode -》number,从inode number到inode属性中的映射关系,设置block bitmap对应的比特位,然后将这个比特位置0即可。所以删除文件,只需要修改位图即可。
下面我们看一下inode和datablock的映射关系:
上图中我们只画出了直接索引,但其实并不仅仅是这样,因为直接索引能存放的数据太少了,而二级索引能存储的数据更多,二级索引所指向的数据块里面的内容,不是直接的数据,而是其他数据块的编号,二级索引上面还有三级索引,这些我们就不再介绍了 我们直接进入软硬链接的讲解:
我们先演示如何做一个软连接或硬链接:
建立软连接的命令是ln -s 文件名 软连接文件名
文件属性前面的l代表link,表示链接,下面我们看一下这个链接有什么:
我们发现这个链接指向刚刚那个原始文件,下面我们看看这个文件的inode:
软连接是一个独立的链接文件,有自己的inode number,必有自己的inode属性和内容。下面我们再演示一下硬链接:
硬链接就是刚刚的命令少掉-s选项即可,ln 文件名 硬链接文件名。
下面我们看看硬链接的文件和原始文件有什么关系:
我们发现硬链接和原始文件的inode一模一样,硬链接和目标文件共用一个inode number,意味着硬链接一定是和目标文件使用同一个inode的。
并且文件属性后面的数字都变成2了,这是什么意思呢?我们先讲原始文件删除看看有什么不一样的地方:
我们发现原来的2又变成1了但是inode依旧存在,我们看看可以读取吗
我们发现将原始文件删除后硬链接照样可以打印出原始文件的数据,而软连接则不可以并且显示文件不存在。那么硬链接是什么呢?硬链接其实就是一种引用计数,有相同的文件计数++,当我们删除文件的时候先--计数,只有当计数为0才会真正的将文件删除,如下图所示:
所以我们之前看到的文件属性后面的数字其实就是硬链接数,我们创建几个硬链接这个数字就会加几, 而软连接内部放的是自己所指向的文件的路径,而硬链接就是原始文件的引用。大家看到这里能联想到软连接是windows里面的什么吗?其实就是程序的快捷方式,我们将快捷方式删除是不影响真正的文件本身的,找到其文件路径照样可以打开软件,所以这就是软连接。上面的测试都是以普通文件做测试的,下面我们看看目录是什么样的:
为什么一个目录的硬链接数是2呢?通过我们刚刚的描述不难理解,是2的原因一定是因为有一个文件名和映射关系所指向了这个目录,首先目录名和自己的inode就是一个硬链接了,接下来我们进入目录里看看有什么文件指向这个目录:
我们发现这个空目录有隐藏文件,一个.和一个..,我们之前讲过.代表当前目录,..代表上级目录,并且最重要的是.这个文件的inode和目录的inode一模一样如下图所示:
那么..是否和.一个道理呢?答案是是的,..是上级目录的inode,如下图:
既然我们刚刚可以给文件加一个硬链接,那么目录是否也能呢,我们试试:
结果大家已经看到了,我们不能给目录加一个硬链接,这是为什么呢?为什么刚刚的. 和 .. 都能给目录建立硬链接,而我们却不行呢?因为操作系统不允许用户对目录建立硬链接,因为如果给目录建立硬链接容易造成环路的路径问题,如下图:
比如我们给上图中这个路径的107目录建立了硬链接,那么当我们用find进行查找路径的时候,找到了107中的硬链接,然后硬链接又将路径送回到107这样就永远找不到路径了,那么. 和 ..为什么可以避免这个问题呢?因为操作系统对这里做了特殊处理可以判断,而用户如果搞出这样的问题操作系统很难去判断是否造成环路如何解决。
文件的三个时间:
首先查看文件的时间的命令stat + 文件名:
change是对一个文件的属性做修改的时间如下图所示:
而modify是更改内容的时间,下面我们改一下看看:
不知道大家有没有发现,我们改内容后属性的时间也跟着变化了,这是因为更改文件内容会改变文件属性中的大小,所以属性也会变。Access是文件的访问时间,下面我们访问试试:
为什么我们访问了也没改呢?这是因为查看文件内容的比重很高,如果我们高频次的修改文件访问时间,就会高频次的访问磁盘将文件属性写入磁盘,这样会大大的消耗IO交互的成本,所以一般是访问多次修改一次时间。
二、动态库和静态库
库我们应该很熟悉了,因为我们写c/c++代码一直用着他们的标准库,下面我们看看linux下的库:
我们的系统已经预装了C/C++的头文件和库文件,头文件提供方法说明,库提供方法的实现,库和头是有对应的关系的,是要组合在一起使用的。头文件是在预处理阶段就引入的,链接本质其实就是链接库,所以我们在vs2019下安装开发环境实际上在安装编译器软件以及我们要开发的语言的库和头文件,如果我们在写代码的时候不包含头文件是没有语法的自动提醒功能的。
库的命名后面必须有.so(动态库)或者.a(静态库),比如我们现在有一个库的名字叫libstdc++.so.6,而一个库的真实名字必须去掉前缀lib和后缀.so,所以刚刚我们的那个库的真实名称应该是stdc++才对。在这里我们要说明一下,一般的云服务器,默认只会存在动态库,不存在静态库,静态库需要单独安装。
下面我们封装一个简单的库让大家了解如何使用库:
我们先创建加法减法的头文件和.c文件,接下来写一段简单的代码:
先完成add的头文件以及.c文件:
#ifndef __ADD_H__
#define __ADD_H__
int add(int a, int b);
#endif // __ADD_H__
#include "myadd.h"
int add(int a, int b)
{
return a + b;
}
头文件中用条件编译的方式防止头文件被包含,在.c文件中包含.h文件。
接着是sub的头文件以及.c文件:
#ifndef __SUB_H__
#define __SUB_H__
int sub(int a, int b);
#endif // __SUB_H__
#include "mysub.h"
int sub(int a, int b)
{
return a - b;
}
下面我们实现一下main函数:
#include <stdio.h>
#include "myadd.h"
#include "mysub.h"
int main( void )
{
int a = 10;
int b = 20;
printf("add(%d,%d)=%d
", a, b, add(a, b));
a = 100;
b = 20;
printf("sub(%d,%d)=%d
", a, b, sub(a, b));
}
下面我们用这三个.c文件生成一个可执行程序:
当然我们运行起来肯定也是没问题的,下一步是如何形成自己的库呢?进行这一步之前我们先用另一种方法给对方使用我们的库:创建两个文件夹,我们做的以下步骤都是不给对方源代码的情况下让对方使用我们的库:
接下来我们将main.c文件放入给他人使用的文件夹中:
下一步我们将.c文件和.h文件都放到mylib当中去:
下一步就是我们进入mylib路径下将这些文件打包:首先将.c文件经过预处理编译汇编形成.o文件,.o文件被叫做可重定位二进制目标文件,这个文件当前是无法运行的,但是已经是二进制了。
下一步以相同的步骤将mysub.c文件形成.o文件,如下图:
接下来将所有的.h文件拷贝到其他人那里去:
然后我们进入其他人的那个文件夹中:
到了这里其实想使用我们库的那个人已经可以使用了,他只需要在我们给他的文件夹中编译即可如下图:
我们先让main.c文件也形成一个.o文件,然后将这些.o文件链接到一起:
然后我们运行这个可执行程序发现正常使用我们的库。
下面我们正式的打包库,我们先打包一个静态库:
前面我们说过,库的前缀是lib,后缀是.a:
我们将所有的.o文件放进我们的math静态库中,可以看到静态库占用的空间非常大。下面我们将要给其他人的文件中除了main.c的文件其他的都删除:
删除后我们将.h和.a静态库拷贝到给其他人的文件中:
有了头文件和静态库那么其他人该怎么使用呢?我们直接gcc main.c就可以使用了。
这里报错是因为我们的编译器不认识这个库,所以我们需要用-l命令:
我们在形成可执行程序的时候,-L表示链接的意思L后面的.表示在当前路径查找我们的库,-l是在对应的路径下我要连哪个库,如上图所示成功运行。为什么不加前缀和后缀.a呢,因为我们前面说过了库的真正的名字是不包含前缀和后缀的。
经过我们前面所讲的,我们要想将我们的库分享给别人只需要把.a库文件放在一个文件夹里,把.h头文件放在一个头文件里,然后把这两个文件都发给对方即可。或者打包上传,对方想用解压即可。
第三方库的使用总结:
1.需要知道的库文件和头文件
2.如果没有默认安装到系统gcc g++默认的搜索路径下,用户必须指明对应的选项,告知编译器,1.头文件在哪里 2.库文件在哪里 3.库文件具体是谁
3.将我们下载下来的头文件和库文件,拷贝到系统默认路径下,在linux下安装库。而安装和卸载的本质就是拷贝到系统特定的路径下。
4.如果我们安装的库是第三方的(语言,操作系统,系统接口)库,我们要正常使用,即便已经全部安装到了系统中,gcc/g++必须用-l指明具体库的名称。
下面我们进行动态库的演示操作:
我们先将刚刚的.o和.a文件删掉
动态库直接使用gcc就可以了如下图所示:
-fPIC选项的意思是形成.o文件,接下来我们将.o文件打包:
动态库打包直接用gcc就可以了
我们要形成的库为mymath,记得加前缀和后缀,shared表示我们打的包是一个共享包
接下来创建两个给别人的文件夹,将.h都放入一个文件,将.so文件放到另一个文件
接下来我们用tar命令进行打包:
打包完成后将文件直接发给其他人:
这个时候其他人就可以直接解包用我们的库了:
在我们加载共享库的时候发现报错了,这是为什么呢?
这就说明在连接的时候动态库没有链接到我们的可执行程序里。其实这里报错的最主要的原因是我们在gcc命令中只是告诉了编译器我们的库在哪里,而操作系统并不知道库在哪里,运行的时候,因为我们的.so并没有在系统的默认路径下,所以操作系统依旧找不到库。那么为什么静态库可以找到呢?因为静态库的链接原则是将用户使用的二进制代码直接拷贝到目标可执行程序中,但是动态库却不会。
那么运行时操作系统如何查找动态库呢?我们有3种方法:
1.环境变量:LD_LIBRARY_PATH
下面我们演示如何将动态库添加到环境变量中:
这个时候我们再去查环境变量发现已经有了路径:
接下来我们将动态库链接到可执行程序中:
这次我们发现我们的可执行程序可以成功运行了。
当然这是一种临时方案,因为环境变量只在本次登录内有效当我们退出去重新登录后又不可以运行了。
2.软连接方案:
首先我们退出重新登录一下让刚刚的环境变量失效,然后在系统库中添加我们的库:
ln -s后面的是找到我们自己库的路径,后面的lib64是将我们的库的软连接添加到系统库中,
然后我们用ls -l命令查看该路径发现对应的软连接就是我们的库。接下来我们看看可以运行吗:
从上图中可以看到能成功运行并且不会像刚刚环境变量那样退出xshell后再登录不可以使用的问题了。
3.配置文件方案
我们先将刚刚的软连接方案解除
我们先看看linux下的配置文件是什么样的,然后我们也创建一个配置文件:
创建好我们自己的配置文件后我们直接把我们的库的路径放进去就可以了:
在配置文件中我们加上我们库的路径后,下一步就是加载对应的配置文件,加载对应的配置的文件的命令是ldconfig :注意我所使用的root本身就是超级用户,如果你们是普通用户必须前面加sudo提权。
并且此方法和第二个方案一样,即使退出xshell重新登录也可以继续使用库。
以上三种方法任意一种都可以查找到动态库,下面讲讲动静态库的加载问题。
静态链接形成的可执行程序本身就有静态库中对方方法的实现,但是静态库非常占用资源(磁盘,可执行程序体积变大加载占用内存,下载周期变长,占用网络资源)
上图中右边的大方块为磁盘,磁盘中有一个小方块就是我们刚刚写的静态库,而这个静态库会被多个人使用,并且这些人都会将静态库拷贝一份到自己的文件下面,而大家在使用运行的时候这些代码都会被拷贝到内存中,就如上图中的细长小方块就是内存,里面有4份代码并且都是重复的,如果这样的库在多份程序当中都被使用,并且每一个体积都很大,加载到内存当中就势必非常的占用资源,而占用的资源就是磁盘资源,内存资源,网络资源等。
下面我们看看动态库的加载问题:
最右边的大圆桶是磁盘,磁盘中用绿色方框圈出来的就是我们的动态库,而磁盘左边的是物理内存。当形成一个可执行程序时需要动态库里面的方法时,并不想静态库那样将代码直接拷贝到可执行程序里,而是将动态库中的方法的地址比如1234链接到可执行程序中。也就是说将可执行程序中的外部符号替换成为库中的具体地址。在运行可执行程序的时候会将可执行程序加载到内存,当程序变成进程不仅仅只是加载到内存,还要创建相应的PCB,所以就有了task_struct和进程地址空间和页表,在代码区有printf方法的虚拟地址,当执行这个方法的时候发现经过页表映射没有这个方法,这个时候操作系统就会检索动态库找到后将动态库通过页表映射到进程的共享区当中(共享区在堆区和栈区的中间)。通过我们的描述大家可以发现,动态库必定面临的一个问题就是:不同的进程,运行程度不同,需要使用的第三方库是不同的,注定了每一个进程的共享空间中的空闲位置是不确定的。而动态库中的所有地址,都是偏移量,默认从0地址开始。只有当动态库真正的被映射进地址空间的时候,它的起始地址才能真正确定。
总结
本篇文章相对较难,因为大部分概念都需要我们配合之前的知识去理解,并且这些问题都很抽象,需要大家自己动手画一遍逻辑图才能明白,对于我们给出的3种让操作系统去查找动态库的方法大家一定要动手尝试,因为这些方法在以后做项目的过程中一定会遇到!下一篇linux文章是进程间通信,到时候会详细的给大家介绍linux下的管道。