二、动态库和静态库
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。
一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码
在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接(dynamic linking)
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
库我们应该很熟悉了,因为我们写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\n", a, b, add(a, b)); a = 100; b = 20; printf("sub(%d,%d)=%d\n", 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下的管道。