软硬链接与动静态库
1、软硬链接
(1)软链接
①如何建立
代码:ln -s 原文件名 软链接文件名
②特征
可以发现软链接文件与原文件的inode编号是不同的,这说明两者是两个不同的文件。软链接的内容其实是目标文件所对应的路径字符串,类似于windows下的快捷方式。
③使用场景
在项目打包时,一般分为bin、conf、log等目录,bin目录下一般是存放可执行程序。如果我们要执行bin目录下的可执行程序,那么就要如下写代码:
我们的做法是在proc项目目录下,创建一个软链接:
所以说,软链接其实就是一种快捷方式,其内部存的是目标文件的路径字符串。
(2)硬链接
①如何建立
用代码: ln 目标文件 硬链接文件名
②特征
观察硬链接和目标文件的inode编号,发现其是一样的,说明硬链接和目标文件是同一个inode编号映射的不同文件名。而在权限属性后有一个数字,表示的是硬链接数。当创建一个硬链接后,目标文件和硬链接文件的硬链接数都变为2了。
③目录文件的硬链接数
硬链接数其实是一个inode内部的引用计数,当该引用计数为0时系统则会删除该文件。当创建一个空目录时,其默认硬链接数是2.(示例为newdir)
这是因为在目录内部,默认会有目录的硬链接".";而在创建新目录后,上级目录的硬链接数会加一,因为新目录内部有上级目录的硬链接“..”。根据该特性,我们可以借此数当前目录的子目录个数=硬链接数-2。
系统禁止我们对目录创建硬链接,目的是为了避免形成路径环绕。(系统的. ..会形成路径环绕,但其本身能够处理,所以不会造成影响)
④使用场景
以上的. ..就是使用场景之一,是为了方便用户进行路径切换;
第二个使用场景就是文件备份。例如在某公司的网盘系统,用户A上传一个视频,该视频被很多用户已经上传过了,那么再上传时,系统就不会再将A上传的视频再下载一份,而是直接在A的网盘系统中用一个硬链接将本来已有的视频备份,这就叫快速上传功能。
2、动静态库
(1)静态库
①本质
静态库本质就是将库中的源代码编译成为.o目标文件,并打包成一个库文件。静态库的在linux系统中的后缀是.a(在windows中后缀是.dll)。静态库在编译时直接被复制到文件中。
②实验
先将多个.c文件编译成.o文件:
再将.o文件打包成静态库:
注意这里的名字取得不好,所以后面进行重命名了,命名为libmyc.a;而在编译时只用中间的myc就行了。
我们的做法是,构建一个mylib的目录,该目录的结构如下:
此时编译就可以通过了:
运行a.out:
注意test.c的代码:
#include"mystdio.h"
#include "mymath.h"
//双引号表示从当前路径下找
//#include<mystdio.h>
//#include <mymath.h>
//尖括号表示头文件从系统中找
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
int main()
{
myFILE* fd=my_fopen("log.txt","w");
const char* buffer="test text:";
my_fwrite(fd,buffer,strlen(buffer));
my_fflush(fd);
my_fclose(fd);
int x=1,y=2;
fprintf(stdout,"%d + %d = %d\n",x,y,Add(x,y));
return 0;
}
③代码解释
a、-I(大写i) 指定用户自定义的头文件路径;头文件是一个手册,告诉用户怎么使用函数,是函数的声明。包含头文件时用<>表示从系统路径中找;用""则表示从当前路径下找,如果-I不写的话,那么一定要写成""的形式,否则在系统路径中找不到自己下载或书写的库。
b、-L指定用户自定义库文件路径;.o文件提供实现。
c、-l(小写L)指定确定的第三方库名称
为什么平时使用gcc编译时,没有加这些选项?
因为默认gcc是可以认识C标准库的,而我们自己写的或者下载别人的库叫第三方库,gcc默认是不认识的。
用ldd查看可执行程序a.out依赖的库,发现并不是自己打包的静态库,而是系统自带的动态库。原因是因为这些依赖的库都只是动态库,而静态库已经把自己的代码拷贝到可执行程序里面了。这里该可执行程序既依赖了动态库,又依赖了静态库,一般来说,可执行程序依赖的库有动态链接的优先动态链接,如果特定的库只提供静态库会把静态库拷贝到可执行程序,也就是说一个可执行程序依赖的库可以是动静态库混杂的现象。gcc默认是动态链接的,但有些库只提供了静态库,那么就只能静态链接(将静态库中内容拷贝到程序中);有静态库有动态库时。默认动态链接,除非在编译时加上-static 强制静态链接。
④作用
静态库的使用是为了提高开发效率。(动态库也是这个作用)
(2)动态库
①本质
动态库本质也是将库中的源代码编译成为.o目标文件,并打包成一个库文件。动态库的在linux系统中的后缀是.so(在windows中后缀是 .lib)。动态库会在程序运行时加载到内存中。
②实验
编译成.o文件:(要带上fPIC)
将.o文件打包成动态库:
将动态库放入mylib目录下
编译、运行,发现a.out正常编译但无法正常运行,且其依赖的动态库找不到:
③代码解释及动态库的使用方式
解释:上述编译的时候,指定了编译器编译时寻找动态库的路径,所以编译成功了;而动态库是在进程启动后动态加载到内存中的,而在运行时,并未指定寻找动态库的路径,系统则会从默认的搜索路径 /lib64 去寻找, 所以找不到所依赖的动态库。
动态库如何使用?
方法一:将打包形成的动态库安装到系统中(放到系统的 /lib64/ 下)
此时可正常运行:
方法二:在系统路径 /lib64/ 下建立一个指向自己的动态库的软链接
结果a.out还是运行不了,这是因为命名少了一个.so
方法三:更改环境变量LD_LIBARARY_PATH
这种方法是内存级的,重启系统后会丢失该环境变量信息。
方法四:修改.bashrc配置文件,让环境变量永远生效,这里不演示了。
方法五:新增动态库搜索的配置文件 /etc/ld.so.conf.d
配置文件查看,并新建一个配置文件:
在配置文件中加入路径(这里是/home/zwh/mylib/mylib/lib):
再加载一下
此时重新进入该系统也能正常运行。
3、虚拟地址空间详述与动态库加载
(1)可执行程序与虚拟地址空间
.c等文件在编译成功后形成可执行程序,是ELF格式的,ELF是可执行程序的头部,涵盖了可执行程序的属性,也就是说该格式可以获取可执行程序的各属性。编译后的可执行程序,其内部有很多行汇编语句,每条汇编语句都有他的地址。
编译时设定地址采用的是平坦模式,也就是按照0000...000~FFFF...FFF,从基地址为0开始编址。而这时的地址,可以理解成虚拟地址,(本质是逻辑地址,但含义类似)。这里的编址方式是相对编址。
当可执行程序运行时,系统为可执行程序创建进程。进程=内核树结构+代码与数据。该创建过程,是先建构内核数据结构,再将代码与数据加载到内存中。内核数据结构有 task_struct,虚拟地址空间mm_struct,以及页表。因为可执行程序的格式是ELF,所以很容易通过ELF+加载器,将程序的各部分起始地址、结束地址拿到,用这些地址来建构虚拟地址空间mm_struct,并将页表的虚拟地址部分填好。与此同时,在CPU内部有一个寄存器pc指针,其中存储的是下一次执行代码的虚拟地址,在这个过程中会将main函数的入口地址放入pc指针。
然后将程序的代码和数据加载到内存中,此时就获取到了他们在内存中的物理地址,然后将这些物理地址填入页表。
当cpu开始运行之后,通过pc中的虚拟地址,结合页表找到物理地址,从而开始执行代码。
以上的过程可以看出,虚拟地址空间的概念,不是OS独有的,而是OS、编译器、加载器共有且统一的。
(2)动态库加载
在上面内存中的代码执行时,遇到动态库中的函数时,则会先判断库有没有被加载,如果没有,则会从磁盘中将动态库加载到内存。动态库是.o文件的集合,其内部是已经编译过的代码,也就有其虚拟地址,这里的地址也是相对地址(相对于某个起始值的偏移)。加载到内存之后,也就有了其物理地址。
此时将其虚拟的相对地址,放入虚拟地址空间mm_struct中的共享区部分,此时其在mm_struct中的虚拟地址是起始位置+相对地址(偏移量)。然后将此时的虚拟地址填入页表左侧,而在内存中的真实物理地址填入右侧。在可执行程序的代码中,调用动态库中的函数,就是通过偏移量call到共享区里去寻找对应的方法,然后当函数执行完返回的时候又会回到代码区执行后续代码。这样的函数调用就是代码区与共享区指令来回执行的过程。
所以在编译动态库时,我们加上-fPIC选项,其中fPIC叫做位置无关码。
系统是如何判断动态库是否加载进来的?
对每个加载在内存中的动态库先描述再组织,形成一个个的结构体,这些结构体彼此相连形成链表;判断需要的动态库是否需要加载只要查询这个链表就可以做到。