前言:本节内容是基础IO部分的动静态库。 本节内容, 我们将站在实现者的角度上自己实现一下动静态库, 并且会站在使用者的角度上使用我们自己实现的库。过程中牵扯到许多新的知识, 最后我们会重谈一下我们的进程。 理解一下有了动静态库的进程到底是怎么样的。现在, 开始我们的学习。
ps:本节内容了解进程地址空间,页表即可观看。
目录
什么是动静态库?
要制作无论是静态库还是动态库, 首先我们得知道什么是库?那么, 什么是库呢?
其实, 库就是由一些.o文件打包而成的一堆二进制文件, 这就叫库。而其中构成库的.o文件其实就是一些各种各样的函数方法。比如我们写的printf, scanf等等都是一些方法, 这些方法是保存在某些.c文件中的, 但是这些.c文件后来被编译成为了.o文件。 由这些.o文件按照某种指令进行打包成为一个库文件, 这就叫做库。 ——也就是说, 库文件里面包含了许多可复用的, 已经写好的代码, 可以被我们使用。
我们的库有两种, 一种叫做静态库, 一种叫做动态库。 动态库一般以.so为结尾, 静态库一般以.a为结尾。
什么是库暂且先提这么多, 具体的细节我们下面的内容中再说。
静态库的实现与应用
静态库的实现
由上面的知识点我们知道, 库是由.c文件生成.o文件,这些.o文件再打包而成的。
上面这张图是我们自己实现的加减乘除的函数, 需要注意的是除法函数要判断一下除零错误, 然后我们设计一个myerrno来表示错误码, 用来标记错误信息。
把我们的源代码想办法打包成库, 然后我们提供库 + .h。 但是这里有一个问题——我们可不可以不提供.h文件呢?答案是不可以,为什么? 因为我们的.h文件里面是一份我们定义的方法的说明书。而且我们的库里面是一群二进制, 我们直接看是看不懂的!!!
静态库形成方法
如上图, 我们先创建makefile, 然后在makefile输入上面的代码——第一行说明要生成叫做libmymath.a的静态库(静态库的后缀为.a), 第三行就是说明我们生成库依赖的文件(也就是mymath.o), 第四行就是我们要将mymath.c生成mymath.o。
这里我们再重新了解一下什么是静态库?
那么假如有下面几个原文件
其中, main.c是我们自己写的, a.c, b.c, c.c, d.c是别人的。 如果别人直接将源代码给我们, 我们直接将这五个文件编译成.o文件, 然后链接就行了!!
那么, 了解了这个之后再理解静态库就非常的简单,因为静态库是什么?——上面的a.o, b.o, c.o, d.o打个包, 这个包就是所有的源代码的.o文件的集合,未来我们进行编译的时候, 只需要将main.c变成main.o, 然后再和我们的这个包进行链接, 就是程序了。 而这个包, 就叫做静态库!!!
然后我们再回到生成静态库的代码中,这里有个打包命令, 叫做ar
这个ar -rc中, ar是生成静态库的一个命令, 就是说将我们所有的.o打包成一个包。 然后-r就是replace的意思, -c就是creat的意思。 这两个连起来的意思就是说假如有这么一串命令:ar -rc librarymath.a.add.o, sub.o——那么这串命令的意思就变成了将后面两个.o文件放到.a的库里面。 而且, 如果这个.a文件不存在就创建, 如果有内容就进行替换。
这里补充好之后就是下图:
注意将clean要将.o, .a文件全部清理。
写好makefile之后, 我们make一下, 得到的结果如下图:
这里就生成了mymath.o、libmymath.a——这里其实就是先生成mymath.o, 然后将mymath.o打包生成libmymath.a。
那么, 现在我的库如果想要给别人使用, 想要发布出去, 怎么发布呢?——这里我们还要使用一下makefile
首先打开我们的makefile, 在makefile里面创建一个PHONY, 名字叫做output, 同时, 我们在lib下创建一个include, 还有一个mymathlib然后将所有的头文件拷贝到include里面, 将所有的库文件拷贝到mymathlib里面。 未来,我们的所有的自己写的静态库, 就能够将.h文件放到include里, 静态库放到mymathlib里面。
同时, 我们的clean, 删除也要跟着变一下, 要加上-r选项——目的是为了能够删除目录
然后执行make;make output后就能发布静态库了:
但是, 问题来了, 我们今天如果像下面这样写着, 包含了我们自己写的库的头文件mymath.h, 那么我们要如何才能编译这个程序呢?
我们如果直接编译, 很显然, 会发生错误, 如下图就发生了链接错误:
所以, 这里正确的编译指令不能直接使用gcc编译程序。 而正确的编译指令就是——gcc main.c -I ./lib/include/ -L ./lib/mymathlib/ -lmymath.
这里现在来讲解这条指令是什么意思。
首先-I的意思是指定包含的头文件的搜索路径, 也就是我们的mymath.h会在上图的./lib/inclde中去找。 然后-L的意思就是指定静态库的搜索路径。 但是后面也要跟一个-I + 静态库名称, 来指定使用哪个静态库(这个静态库的名称要去掉前缀和后缀。就比如上图中静态库本来是libmymath.a, 现在变成了mymath)。
但是我们知道, 我们在写c, c++的时候可没有这么麻烦, 只需要gcc/g++ 后面跟文件名就行了。 而这上面又是-I, 又是-L, 又是-l是为什么呢?
因为c, c++的头文件, 默认就会处在系统的默认搜索路径中,默认能被找到。 c, c++的库, 同样的处在默认的系统的搜索路径中, 并且c, c++默认就能够认识对应的c, c++的动静态库, 所以也就不需要指定。
那么我们也想要像c, c++的库一样, 不想像上面那么麻烦怎么办呢?——那么我们就可以将我们的头文件拷贝到系统的include。 将静态库, 安装到lib64里面。 (/usr/bin/include, 以及lib64分别是头文件和库的默认搜索路径。)但是这两个是系统目录, 需要使用root权限才行!!!
这一块的内容很多, 所以这里先不谈, 我们先谈两个小知识点——errno与 ldd
errno
看一下errno——为了区分我们的库函数有没有被正常使用, 一般我们的数据库都会创建一个全局变量, 就如同这里的errno。
这个errno怎么用呢?——这里假如我们写一个除零错误的函数:
ps:这里博主前面的除零错误函数写错了, 写到这里才看到, 改正一下:
我们运行一下上面的main.c程序:
我们可以看到错误码的错误标识, 这个错误标识我们还可以标识为1, 2, 3等等。 用来标识不同的错误信息。
ldd
ldd能够查看可执行程序连接的动态库。
为什么只能看到动态库, 而看不到我们自己写的mymath静态库呢? ——这是因为gcc默认就是动态链接,但是我们在链接的时候, 我们的lib里面没有动态库, 只有静态库, 所以我们只能静态链接。
另外, 未来我们链接库的时候, 如果我们什么都没有带, 默认就是动态链接。 如果我们想要静态链接, 就要加上static。(注意, 如果系统中需要链接多个库的话, gcc也可以链接多个库。只需要将需要连接的库依次往指令后面跟即可)
如何建立默认搜索路径
谈完上面的两个小知识点之后, 我们就来谈我们上面所提到的——如何像使用c/c++默认库一样不带有冗长的指令选项呢?
上面已经说过——我们只需要将我们的mymath.h, libmymath.a放到系统的默认路径下面!!!
注意, 系统的默认目录处于usr还有lib里面, 而这两个是系统级别目录, 需要使用root权限。(下面执行指令时已经变为root账户)
实行指令之后就可以看到我们已经把文件拷贝进去了!
上面的这两条指令, 其实就是库的安装!!!——可能有点友友会问, 这就安装好了?——是的, 安装其实并没有这么高大上, 我们下载一个库, 一定要有头文件, 一定要有库文件。而库的安装,本质上就是我们将头文件, 库文件安装在系统目录里面。我们一般的情况下, 其实安装的本质就是拷贝, 将程序拷贝到我们的系统目录下(但是不排除有些程序安装需要改变环境变量等等)。
静态库的使用
首先我们知道头文件我们安装到了默认的路径下面。 所以-I后面的头文件搜索路径就可以不要了。 并且,对于静态库来说, 我们将静态库安装到了默认路径下面,那么-L后面的静态库搜索路径就可以不要了,但是-l后面的指定静态库我们仍需要有。这是因为我们必须要知道.o文件要链接那些静态库。如果不指定静态库, 仍旧会发生报错:
我们指定准确的静态库之后, 就可以编译通过了:
那么问题来了, 我们自己编写的库一般不要放到系统目录下面, 因为我们写的可能不太好(如果大佬写的很好的话请忽略). 所以我们这个时候我们就不要将库直接放到系统目录下面, 而是使用另一种方法——软链接:
这里先删除之前我们安装的库
如下图, 红色框框是我们想要连接的头文件所在的目录, 蓝色框框是软链接文件的路径:
如下图, 红色框框是我们想要链接的静态库, 蓝色框框是将软链接文件放到的路径:
建立好了上面的东西之后, 注意, 我们的连接的头文件目录连接的是myincl->include,也就是说,我们如果包含的头文件是mymath.h, 那么系统不会触发myincl的软链接, 也就找不到mymath.h, 所以我们这里要包含头文件要写成:myincl/mymath.h。
此时编译就又能通过了:
动态库的实现与应用
动态库的实现
动态库我们再新建两组库, 一个mylog.c和头文件, 一个是myprintf.c和头文件。
在myprintf.h和mylog.h里面都写上#pragma once以及包含stdio头文件。
然后写上如下内容
myprintf.c写上下面第一张图的内容, mylog,c写上下面第二张图的内容
链接动态库要怎么链接呢?我们知道, 我们的文件只有到.o文件才会链接库, 所以, 生成动态库, 一定也是先将文件变成.o文件, 再生成动态库。 只不过, 生成动态库比生成静态库多了一些选项而已。
这里我们先生成两个.o文件, 要想生成动态库, 必须要生成可以生成动态库的.o文件。
然后再生成动态库, 注意:静态库生成要用的打包指令ar, 动态库不需要, 因为动态库算是默认的, 生成库的时候默认生成动态库, 使用库的时候默认使用动态库。 只有当指定静态库或者没有动态库的时候才会使用静态库。
动态库的后缀使用.so, 并且生成动态库需要使用-shared标识生成库文件。
生成动态库和生成可执行文件一样, 我们要使用-o选项标识生成可执行程序, 生成的文件后缀是.so。 然后我们并不是真的生成可执行程序, 所以加上-shared选项标识生成动态库。
然后, 为什么生成的动态库是一个绿色的可执行文件呢? (我们的静态库是不可执行的, 是白色的。)这是因为我们的程序在运行的时候, 静态库不会被加载到内存中, 而动态库联系着我们的程序, 要加载到内存中。 ——也就是说, 我们要如何理解可执行权限?那么什么叫做可执行权限呢?其实就是当前文件是否会以可执行程序的形式加载到内存中。这就叫做可执行权限。 ——而动态库, 就使用有着可执行权限。(虽然动态库没有main函数, 但是有对应的方法, 也就是说动态库不是不能执行, 而是不能自己单独执行。)
现在来看一下生成两个库的makefile怎么写:
下面是我们的makefile
保存退出后我们make;make output就能生成如下的目录了, 然后我们以后给别人提供库, 这里面就有三个头文件,可以查看文件的方法。 然后提供两个库, 一个动态, 一个静态。 根据方法的使用说明进行使用这两个库就行了。
动态库的使用
现在, 使用一下这两个库,先创建一个文件夹, 然后将我们的mylib文件夹放到mylib文件夹里面。
我们这一次包含两个头文件, 其中动态库里面有着myprintf.h的方法, 静态库有着mymath.h的方法。
接下来编译它, 编译的方式和之前是一样的, 就是利用-I选择搜索头文件的路径。 ——只用定位到路径, 不需要定位头文件的名称。并且使用-L定位搜索的库路径。——也是只需要定位到路径, 但是后面要跟上-I + 库名(去掉前缀和后缀)
那么, 只要未来我们需要使用别人的库, 就是用-I,-L以及后面跟-l。
但是, 又有问题了。 看我们运行我们的a.out, 就会发现, 运行不出结果, 并且下图的红字意思就是说动态库不能分享目标文件。
请问这是为什么呢?
我们如果使用a.out就会看到, 下面的我们自己创建的动态库, 它显示没有找到。
但是我们不是已经告诉gcc了吗?像什么-I, -L, -l这不就是在告诉gcc要去哪里寻找库吗?——我们也知道, 我们使用的指令是告诉gcc库在哪里, 而gcc是什么呢?gcc是编译器。 也就是说, 我们的指令是告诉编译器库在哪里放着。
但是, 我们也要告诉我们的加载器!!!那么我们如何告诉加载器呢?这里有四种方法:
- 其最简单的方法就是直接将动态库拷贝到lib64里面。 因为我们知道系统的库默认搜索路径就是lib4, 所以我们将库直接拷贝到lib64去是一定可以的。
- 建立软链接
建立好了之后, 就可以看到我们的加载器也能找到库了!
注意实验完成之后要unlink软链接。
- 第三种方法是调整环境变量——有一个环境变量是专门用来调整用户的库的默认搜索路径的。——这个环境变量是LD——LIBRARY——PATH, 就是下图:
现在我们使用export指令添加默认搜索路径就可以了!!!
注意实验完成之后需要重启服务器,就可以将LD_LIBRARY_PATH做的修改清空。
- 第四种方式我们可以先变成root用户, 然后进入etc目录下的ld.so.conf.d目录。 (这个目录文件干啥用的博主也不清楚, 但是可以解决问题)。
进入到这个路径我们只需要在这个路径创建一个文件, 这里取一个dynamic-9-4.conf文件名。
然后打开文件, 将路径拷贝到里面去:
完成之后ldconfig一下, 就饿可以看到a,out的动态库链接好了。所以综上, 第四种方式就是——/etc/ld.so.conf.d建立自己的动态库路径的配置文件, 然后重新ldconfig即可。
综上,就是令加载器知道指定动态库的四种方法, 实际上我们在日常工作中一般使用的都是别人的成熟的库, 都是采用直接安装的方式!!!
动态库是如何被加载的
通过上面的学习, 我们知道,动态库在运行的时候, 是要被加载的。 但是静态库不会被加载, 而且我们ldd的时候, 我们会看到ldd下面没有所谓的静态库:
这说明我们的程序默认是动态连接的, 只不过我们使用的mymath方法只提供了静态库, 只能让静态库提供静态链接, 我们生成可执行程序之后就说明我们的mymath的方法已经拷贝到可执行程序里面了。 已经和静态库没有关系了, 所以我们将静态库删除后, 程序仍可运行。 这就好比静态库卖的是二手电脑,而动态库是网吧。 静态库卖出去的电脑已经是我们自己的了, 而动态库我们只是暂时使用。
而且, 常见的动态库是被所有的可执行程序(有动态链接的)共享的, 所以也叫共享库。 所以动态库在系统中加载之后, 会被所有的进程所共享!!!——这就能大大的节省内存!!!而对于动态库来说, 性能只有一开始加载动态库的时候会降低效率, 之后就可以被所有人使用了!!!
现在我们来重新回顾一下我们的进程地址空间的内容:
将来我们运行一个可执行程序, 上图的1.exe。就是先将可执行程序的文件加载到内存中, 然后保存在内存的page里面, 并且创建一个进程和虚拟地址空间, 然后页表映射到这个程序的代码和数据。
这里有一个虚拟机的原理小知识点可以补充一下:就是我们以前是知道的, 进程间是独立的, 我们的1.exe挂掉, 不影响2.exe。
那么就有一个问题, 如果我们的1.exe是操作系统的代码呢?意思就是说如果我们的操作系统被当作一个可执行程序可以吗?——操作系统上面再跑一个操作系统, 而又因为进程间的独立性, 所以这个操作系统不影响我们原来的操作系统。 ——这就是内核级别的虚拟机的原理!!!
现在回归我们的正题, 就是动态库是如何被加载的呢?
我们知道, linux下一切皆文件, 那么, 动态库也是一个文件。 并且是一个可执行的文件, 既然是一个可执行的文件, 那么他就能够被加载到内存中。 如下图红框框就是一个动态库文件。
而加载过程就是如下:
- 一开始, 我们的1.exe加载到内存中, 形成了一个进程, 并且疯狂的运行。 可是, 当我们需要使用某个方法的时候, 我们当前的代码中没有所谓的方法的实现, 那么我们就会利用编译的时候所说的库的位置去寻找对应的库。 找到后, 就将对应的动态库加载到内存。
- 加载到内存后,很显然, 我们的进程并不能看到这个库, 因为没有也表映射, 所以我们就要将加载进来的库, 映射到我们虚拟地址空间中的共享区中!!!
- 最后, 以后我们的进程, 再想调用库里面的某种方法, 就直接向共享区里面找就可以了!
从上面我们就可以看到, 也可使说是下一个结论——就是动态库加载到内存, 建立映射关系后, 从此往后, 我们执行的任何代码, 都是在我们的进程的地址空间中进行执行!!!
并且, 因为系统中的动态库不可能只有一个, 会有很多个, 那么操作系统就势必会将这些动态库管理起来——先描述, 再组织——也就是说, 对于系统中的所有的库的各种情况, 操作系统都非常清楚!!!
那么问题来了, 我们说过, 动态库是共享库。 那么多个进程之间是是可能使用一个库的。 而这个库里面可能存在errno这种全局变量。而库在物理内存中只有一份, 那么当一个进程修改这个变量的时候, 会不会影响其他进程呢?
答案是当然不会, 为什么?
因为会发生写时拷贝——可能有的友友会问, 问什么会发生写时拷贝?写时拷贝不是发生在父子进程中的吗, 我们的多个进程可能不是父子进程啊,也会发生写时拷贝吗?——这里就牵扯到了发生写时拷贝的本质,而写时拷贝的本质就是有两个进程的页表映射到了内存上面的同一块区域, 并且发生了修改, 这就会发生写时拷贝!!!
————以上就是本篇的全部内容, 本篇内容到此就结束啦, 感谢友友的阅读, 下面是本节的笔记, 和正文几乎一样的, 觉得本节内容有用的话可以保存方便查阅哦。