【Linux】基础IO-----软硬链接与动静态库
目录
一、软硬链接初步理解:
1、软链接:
2、硬链接:
二、软硬链接深层理解:
1、软链接:
2、硬链接:
三、软硬链接应用场景:
1、软链接:
2、硬链接:
四、动静态库:
1、静态库的实现与应用:
2、动态库的实现与应用:
3、动态库是怎么被加载的:
五、再谈进程地址空间:
1、程序没有加载前的地址:
2、程序加载后的地址(进程):
3、动态库的地址:
一、软硬链接初步理解:
1、软链接:
首先,我们对一个普通文件进行软连接,
在命令行中,指令是:ln -s XXX YYY
ln是进行链接,如果后面不加上-s则为硬链接,这里加上-s为软连接
XXX为待链接的文件,也可以叫源文件(当前目录中已经存在的文件)
YYY为生成的软连接文件(在当前目录中即将生成的文件)
如上,这就是生成了一个软连接文件soft_link指向log.txt
如上,发现软连接后的文件是独立的,新的文件,它和它所连接的文件不是同一个(inode不同)
2、硬链接:
对一个普通文件进行硬链接
在命令行中,指令和软连接差不多,就是少一个-s,指令:ln XXX YYY
XXX为待链接的文件,也可以叫源文件(当前目录中已经存在的文件)
YYY为生成的硬连接文件(在当前目录中即将生成的文件)
如上就是生成了一个硬链接文件hard_link指向源文件test.txt
但是发现链接的两个文件的inode是一样的,
所以得出结论:硬链接不是一个独立的文件,二者是同一个文件(inode是一样的)
并且我们可以看到,在权限后面的数字从1变为2了,这个其实是引用计数器,也可以叫做硬链接数
所以可以得出结论:软链接文件依赖于源文件,而硬链接文件是源文件的别名
所以,我们将软硬链接的源文件都删除,可以发现:软连接文件失效了,但是硬链接文件依然有效,但是硬链接数变为1了
二、软硬链接深层理解:
1、软链接:
软链接到底是个什么东西呢?这既然是一个文件,并且不是源文件,那么这个文件里面有什么呢?
如上,我们先创建一个log.txt文件,然后在向这里面追加写入5行 hello_solf
然后在对其进行软链接
最后cat源文件和软链接文件,发现这两个里面都是同样的数据
这是为什么呢?-------- 其实软链接可以看做是源文件的一种快捷方式
例如在Windows中就是桌面图标,这其实就是软链接,真正的文件在比较深的目录下,但是当我们启动程序的时候并不是在目录里面找到该文件程序,而是在桌面上使用快捷方式直接打开的
那么回到我们的Linux中,软链接文件里面的数据块保存的是指向源文件的路径,所以软链接文件的大小一般是不会太大的,里面不包含目标文件的内容,通常只有几个字节
2、硬链接:
硬链接中,硬链接是和它所链接的源文件的inode是一样的,但是文件名不一样
我们又知道:文件是在放在目录中的,而目录文件中的文件块里面保存的就是文件名和inode之间的映射关系
所以硬链接其实就是在目录块里面,创建一个新文件名和inode之间的映射关系,每多存在一个映射关系,硬链接数(引用计数器)就+1,
硬链接中,无论文件名怎么变,但是都是映射到同一个inode也就是同一个文件,
任意一个文件,无论是目录,还是普通文件,都有inode,每一个inode内部都有一个引用计数器,这个引用计数器:也就是有多少个文件指向我
当我们进行删除的时候,其实就是让inode里面的引用计数器减1,当inode减为0的时候就真的进行删除了------根据inode编号找到对应的区和组,然后在根据inode找到,对应的block num删除释放,再将block bitmap和inodebitmap的对应位置置为0
三、软硬链接应用场景:
1、软链接:
既然在Windows中软链接可以使用于一个快捷方式,那么在xshell中是不是也可以通过软链接进行一个快捷方式呢?
如上,如果要在一个很深目录下的程序进行执行,但是如果不想cd进去,那么就可以在当前目录下建立软链接,然后就可以在当前目录下直接进行执行
如上,指令就是ln -s 源文件(前面可加目录) 软链接文件
这样就可以在当前目录下执行软链接文件,这就可以看做是一个快捷方式了
当然,还有一个更加方便的快捷方式:
如上,进行sudo提权,在/usr/bin目录下进行软链接,这样只需要如下,直接输入这个软链接即可,就不需要带上路径了
在当前目录下也可以看到
但是不建议在系统目录下瞎搞,所以就需要将链接删除,
方法就是unlink 软硬链接名
如上,这就是将mybin软链接文件删除,这样就不能使用mybin了
2、硬链接:
硬链接可以用来给重要的源文件起别名并使用,一旦发生删除等不可逆行为时,确保源文件的安全
这个上面也说到了,是给普通文件起别名,让这个文件inode中的引用计数+1,当删除的时候引用计数-1,当减为0的时候就真正删除该文件
当然,上述都是对普通文件进行硬链接的,那么能不能对目录文件进行硬链接呢?------答案是不能
但是我们却看到如下的硬链接数量却不是简单的1,2,这是为什么呢?
要想理解上述问题,我们就需要更加深入理解 ./ 和 ../
如上,红框框里面的当前文件也就是进入d1目录下的上级文件,就是相同的文件,可以通过inode来看
所以,如上,为什么528615这个文件的硬链接数为3-------在silence目录下有一个文件link对应着这个inode,在这个目录文件中有一个 . 文件,这是第二个,在这个目录文件中有一个d1目录文件,这个d1目录文件中有一个 ../ 作为528615的第三个文件名,所以528615这个文件的硬链接数为3
所以,我们可以在当前目录中在创建一个目录,这样528615这个文件的硬链接数就又+1了(新创建的一个目录中存在 .. 对应着528615这个inode)
那么可以进入dd看看dd的上级目录也会是528615
但是我们用户却不能够自主创建硬链接对应的目录,只能由OS自动创建目录的硬链接,这是为什么呢?
因为用户可能会创建出目录环状问题
我们知道,在目录中,是多叉树的形状
那么,如果进行硬链接,如上,当进行查找的时候,当查找到红色的目录的时候,就又会回到根目录重新找,这样就会进入一个死循环,所以不能自主创建硬链接对应的目录
四、动静态库:
无论是动态库还是静态库,都是库文件,常见的库文件有stdio.h,string.h,vector.h等等
这些库文件都是由.o文件链接打包而成的二进制文件,.o文件又是一些函数方法接口,如printf,scanf等等,
在Linux中,静态库的前缀是lib,后缀是.a
,动态库的前缀是lib,后缀是.so
所以动静态库的命名是,libXXX.a或者libYYY.so
对应库的名字应该是去掉前缀lib和后缀.a或者是.so后剩余的部分
静态库是将所需要的函数代码拷贝到源文件中直接使用
动态库是通过动态链接的方式,进行链接使用
1、静态库的实现与应用:
比如上述,左边是mymath.h的各种函数声明,右边是mymath.c对应函数的实现,其中myerrno是错误码,比如在除零错误中就可以将原本的0修改为1,这样用户使用的时候就知道了是除零错误了
在进行编码完成后,需要进行链接,发布等操作,如下在Makefile文件中,依次进行分析:
第一个的话是定义了一个变量 lib,其值为 libmymath.a
第二个的话表示一个目标规则,意思是生成 libmymath.a(通过变量 $(lib) 引用)所需的依赖文件是 mymath.o,这里通过变量而不是直接libmymath.a:mymath.o,这是因为一般做大项目时候,可以重用代码,减少维护成本,要不然,后面libmymath.a改个名字,下面都要改
接下来ar是生成静态库,就是将编译好的.o文件打包成一个包
加上-rc选项,这串命令的意思就变成了将.o文件放到.a的库里面
如果这个.a文件不存在就创建,如果有内容就进行替换
在下来就是正常编译到.o文件,伪目标clean,伪目标output方便进行发布(建立目录,拷贝到对应目录中)
接下来就可以在main函数中使用了,只需包含我们所写的静态库
如下,但是我们正常编译却报错,找不到头文件
这是为什么呢?-------因为我们虽然在代码中包含了库,但是编译器却不知道这个库的头文件,库文件在哪里,所以就会报错
所以首先要告诉编译器库的依赖头文件在哪里在gcc后加上-I(大写的i)后面空格在跟上所依赖头文件的目录
但是依然会报错 还要告诉编译器所依赖的库文件在哪里-L 后面空格在跟上所依赖的库文件的目录
但是还是解决不好,因为还要告诉编译器所依赖的库文件的名称-l(小写L)后面一般紧跟所依赖的名称(去掉前缀和后缀)
这样就可以编译成功了,需要告诉编译器所依赖库的名称而不需要告诉编译器所依赖头文件的名称是因为在代码中已经告诉编译器头文件的名称了
但是我们一般编译正常.c文件的时候却不用这么麻烦,直接gcc/g++后面加上文件名即可
这是因为编译器默认搜索路径就能够找到C/C++的库,它们是在系统中存在的,并且编译器也认识,并且在默认搜索路径中也能找到对应的动静态库,所以就不用像上面那么麻烦
当进行运行的时候发现除零错误,这就是代码中myerrno所对应的错误标识符
将头文件和静态库文件安装至系统目录中,就不用这么麻烦地编译了
在系统默认搜索路径中在/usr/include和/lib64分别对应着依赖的头文件和库文件,所以将我们所写的库文件和头文件拷贝到这两个路径下就不用这么麻烦了
但是在编译的时候还是要记得加上所依赖库的名称
但是一般是不建议把自己所写的库随便就放到系统的默认搜索路径下,这样会污染环境的
所以除了上述的方法还有方法就是建立软链接
在默认include路径下建立软连接
在默认lib64路径下建立软连接
如上,这就是在系统默认搜索路径下和我们自己写的库文件和头文件建立软链接,并且注意,后面跟上库名称的就是自己新的软连接名称(去前缀和后缀)但是如下,当进行编译的时候却失败了,这是为什么呢
上述是找不到头文件,那么就是我们的软链接没有被触发,因为我们代码中的头文件只是mymath.h是找不到软链接的,需要带上软链接文件mylog
这样就可以了
2、动态库的实现与应用:
在学习动态库之前,了解指令ldd,ldd后跟上程序,这样的话就能够看到该程序所依赖的动态库
想要链接成动态库,和静态库差不多,都需要首先编译成.o文件,再将这些.o文件链接成动态库
首先创建两组.c文件和.h文件
在里面分别写上几行代码
然后在命令行中将其编译成.o文件,这里和之前有区别,要加上-fPIC位置无关码
因为链接库是通过各种.o文件进行链接生成的
然后再将这些.o文件打包起来生成对应的动态库
注意,这里打包生成动态库是不需要使用ar指令的,用gcc/g++即可,因为生成默认库的时候是动态库,使用的时候默认也是动态库,除非带上-static选项指定静态库,所以gcc就不需要依靠别的指令,自己就可以完成动态库的链接
如上,在使用的时候和正常编译出可执行程序的差不多,这里还需要加上选项-shared
这样就生成了动态库链接后的文件 他是默认带了可执行选项的 所以我们可以重新理解一下可执行
其实可执行权限就是指的是当前文件是否可以 以可执行的形式加载到内存中,静态库就不会被加载到内存中,而动态库要被加载到内存中的,所以动态库就有可执行权限,也就是说动态库是能够被执行的,但是它自己不能够被执行,它有自己对应的方法,需要别人对其进行调用的
了解上述了我们来完善我们的Makefile文件,来一次性生成动静态库
其中第四行的意思是:
makefile中第一个遇到的目标始终被执行,也就是说,make就是执行第一个遇到的目标 ,这里是all 也.PHONY是添加伪目标,也就是说,不管是否有更新,all这个目标始终都会被执行
所以如果这个伪目标的话,就只会执行第7行$(static-lib):mymath.o这个和第8行它的依赖方法,然后$(static-lib)又需要mymath.o这个依赖文件,所以9,10行也会执行,但是此时后面的第12行这个目标不是第一个目标了就不会执行,也就是不会生成动态库了,为了解决这问题,就搞个伪目标all,这个all有两个依赖文件,所以后面两个依赖文件就都会被执行了
这样就能够将动静态库同时加载了
然后将我们的动静态库编译,发布出去
这样,在lib目录下,就能够看到我们的库文件和头文件了
接着就可以使用了
如上,这就是将这个main.c文件编译好生成a.out文件
但是我们执行这个可执行文件a.out却执行不了,这是为什么呢?
因为我们只告诉了编译器我们的库文件和头文件在哪,却没有告诉加载器我们的库文件和头文件,如下,使用ldd指令查看没有看到所依赖的动态库文件
为了解决上述问题,这里有4中方法
1、直接将动态库拷贝到默认库路径/lib下(注意这个默认库路径是要根据自己的Linux版本,比如在CentOS下是在lib64或者/usr/lib64下,但是在Ubuntu下是在/lib下)
2、在系统默认库路径下建立软链接
3、通过环境变量调整用户的库的默认搜索路径的,LD_LIBRARY_PATH
4、在/etc/ld.so.conf.d路径下创建一个名为mylib.conf(这个名字可以随便取)
然后在这个文件里面将动态库所在的绝对路径放在这个文件里面
接着在ldconfig一下就可以了
如上,虽然有4种方法,第一种就已经是最常用的
3、动态库是怎么被加载的:
当我们进行ldd的时候,发现只看得到动态库的加载,动态库在哪,但是看不到静态库在哪,
首先我们要知道,当我们编译形成可执行程序的时候,静态库是已经拷贝到可执行程序里面去了,那么就和外面的静态库没有关系了,如果别的程序还要使用这个静态库,那么就继续拷贝即可
但是我们的动态库是所有可执行程序共享的,所以动态库也叫做共享库,并且动态库也会被加载到内存中,这之后就会被所有进程共享,这样就能够比静态库更加大大节省内存
接下来看看一个程序是如何加载到内存的
如上,这就是将一个test.c文件加载到物理内存中,然后创建一个虚拟地址空间,在通过页表进行映射到虚拟地址空间中的代码区和数据区,
那么接下来看看,如果这个程序调用一个动态库中的函数,那么这个动态库是如何加载的呢
如上,在Linux下一切皆文件,在磁盘中的动态库文件首先被加载到物理内存中,然后也是通过页表映射到栈的堆之间的共享区,当在虚拟地址空间中的代码执行到对应的库函数的时候就在共享区中找到对应的函数即可
并且,在系统中,动态库有很多种,所以OS要对其进行管理就需要先描述在组织,这样就能将系统中的所有动态库管理起来,所以这个库有没有被调用,有没有加载到内存中,OS都能够知道了
但是,这个时候就出现了一个问题,既然多个进程在共享区中共享一个动态库,在上述介绍动态库的时候,我们写了一个全局变量myerrno,如果一个进程修改这个变量,那么其他进程中,这个myerrno会不会也被修改了呢?
答案很显然是不会的,这是为什么呢?
因为会发生写时拷贝,写时拷贝的本质:
有多个进程的页表映射到了物理内存上面的同一块区域,并且发生了修改,这时就会发生写时拷贝
五、再谈进程地址空间:
首先,来问几个问题------什么是虚拟地址,什么是物理地址,在上面加载动态库的时候选项 -fPIC 这个与地址无关码怎么理解
这个时候,我们重新谈谈地址
1、程序没有加载前的地址:
当程序编译好之后,内部有没有地址的概念呢?
很显然是有的,当我们形成可执行程序编译后,其就已经有了这个地址的概念
如上,在一个可执行程序中,当它编译完成后,尽管没有加载到内存,此时依然形成了地址,当main函数里面的func函数被编译后就形成了0x1,因为func定义在0x1处,不仅仅是函数,数据变量等在编译后就已经编好址了,这样函数名,变量名就已经不重要了
接下来,我们写一个简单的代码来看看反汇编
执行上述代码就能够看看该程序的反汇编
如上,每一个代码都可以看到对应的地址,并且这些地址之间的大小是不一样的,这是因为每一行指令的大小是不一样的,有的1字节,有的几字节不等,
所以当我们执行指令的时候就能知道当前指令占了多大的空间,那么只需知道入口地址就能够把所有指令都执行
上述还有一种指令,其里面包含着对应的函数调用,如上述的call 1050,这就是调用1050处位置的函数,如下,来实现对应的打印操作
所以,CPU是怎样执行指令的呢?
我们讲一个故事:一个很小的孩子,他还没有了解世界上语言的组织,但是他在学习各种各样的词语,比如玩具,拿起来等等,当大人们和他交流的时候,就可以说“拿起来“,”玩具”,这样当听到关键词的时候就能够做出相应的动作,所以CPU也是类似,每一个汇编语言如push,mov,call等都是一个个“小词语”,CPU提前内置了能够理解这些指令的二进制,当把这些词语组合起来就能够完成计算机的各种任务
2、程序加载后的地址(进程):
如上,当可执行程序以块的形式从磁盘加载到物理内存,这个时候就会有其对应的物理地址了,并且这些指令在其内部也有逻辑地址,比如上述的call指令有它自己的逻辑地址,当它加载到物理内存中后又会有其对应的物理地址
那么CPU是怎么执行我们的代码的呢?
当我们程序编译形成可执行程序的时候会生成一个入口地址entry,这个entry里面放着入口地址,这里的入口地址是是逻辑地址并不是物理地址,毕竟它还是编译过后,并没有加载到内存中,也就自然而然没有物理地址的概念。
然后CPU中有一个EIP/PC指针,当这个可执行程序加载到内存中,这个时候就会将入口地址entry加载到这个EIP寄存器中,然后这个CPU就开始执行了,
首先从这个entry地址找到对应的虚拟地址,然后通过页表映射到对应的物理地址处,对应物理地址处,不一定会全部加载,可能会只加载一部分,然后如果在页表中虚拟与物理地址相互转化的时候,发现没有物理地址对应的映射,这个时候就会发生缺页中断,再把可执行程序中对应的代码以页为单位加载到物理内存中,这个时候就有对应的物理地址了,这个时候就有了对应的映射,然后就会按顺序执行,每执行一个指令,这个指令的长度也就会知道,然后就会在EIP指针中加上对应的长度,这个时候就能够执行每一行指令了
当CPU读到了函数调用指令call的时候,此时CPU读到的内容可能有数据,也有地址,这个指令内部用到的地址就是虚拟地址,然后再在正文代码中找到对应的虚拟地址,然后在通过页表映射到对应的物理地址(不存在物理地址就缺页中断即可)
所以我们整个过程从读取程序当中的地址,到内部分析处理,到重新二次继续访问程序整个过程凡是读到的地址全部都是虚拟地址,然后执行的时候通过页表转化成物理地址执行指令,若读到数据就继续执行,若读到地址就在正文代码中找到对应的虚拟地址,在通过页表映射到对应的物理地址,继续执行,这样整个过程就循环起来了
3、动态库的地址:
当可执行程序中读到了动态库中所需的函数,这个时候就会将动态库从磁盘中加载到物理内存了,然后在通过页表映射到对应的共享区中
但是共享区很大,并且我们程序的代码中只保存了libc.so的逻辑地址,那么我们的动态库要映射到共享区的哪里呢?比如c库中的printf的地址是0x1122,那么是不是就映射到0x1122呢?这显然不是的,因为如果这次映射到0x1122,那么后面的库如果也是加载到0x1122,那么就会出问题,所以,动态库被加载到固定地址空间的位置是不可能的,
所以库必须设计成可以在虚拟内存共享区中任意位置加载,所以库在形成的时候让自己内部函数不要采用绝对编址,只表示每个函数在库中的偏移量即可
所以库就可以在地址空间的共享区中随便放了,只需最终记住每一个库在地址空间共享区中的起始地址即可,又因为OS要对库做管理,所以一个库要被加载到地址空间的什么地址,OS也是要知道的,然后找对应的库函数只需改库在共享区中的起始地址+偏移量即可找到了,然后返回继续执行
fPIC这个与位置无关码,就是直接用偏移量对库中函数进行编址,保证这个库在虚拟内存中的任意内存加载,这就叫做与位置无关码
那么最后问一下,静态库为什么没有加载?没有与位置无关?
因为静态库会直接拷贝到可执行程序里,所以就不谈加载的问题,所以也可以将静态库中的方法看做是我自己的方法,那么也就不需要用偏移量编码,直接按照绝对地址编码即可,也就是说既然把库方法拷贝到可执行程序里,其编的时候就是按照虚拟地址来编的,所以库方法在虚拟地址当中的什么位置就确定了