Linux C语言Shared Library共享库细节探究

发布时间 2023-10-12 16:22:17作者: 秋来叶黄

开发中遇到一个问题,比如有一个类库A,被类库B引用,类库B和类库A都被程序C引用。类库A中有一个全局变量G,要求同一个进程中使用的是同一个全局变量G。

虽然看起来很简单,但是实际探究下来还有不少坑。

如果不是类库

如果A B都不是类库,而是直接引入源码编译,理论上比较方便解决。

示例一

pre.h

定义了全局变量a

#ifndef UNTITLED11_PRE_H
#define UNTITLED11_PRE_H
int a;
#endif //UNTITLED11_PRE_H

test.h

#ifndef UNTITLED11_TEST_H
#define UNTITLED11_TEST_H
#include <stdlib.h>

void ftest();
#endif //UNTITLED11_TEST_H

test.c

相当于另一个模块引用了全局变量

#include <stdio.h>
#include "pre.h"
#include "test.h"
#include <stdlib.h>
void ftest()
{
    a = 333;
}

main.c

#include <stdio.h>
#include <stdint.h>
#include "test.h"
#include "pre.h"
int main()
{
    a = 222;
    printf("main[%d]\n", a);
    ftest();
    printf("main[%d]\n", a);
    return 0;
}

输出结果

main[222]
main[333]

可以看出不同模块虽然都引入了pre.h,都有int a,但是使用的是同一个。

示例二

如果把上面pre.h中的int a;改成int a = 0;,编译就会报错

multiple definition of `a';

这是为什么呢?实际上示例一只是用了一个投巧的办法。因为int a;不能确定是声明还是定义,当遇到第一个赋值是,比如a = 222;,这时候才知道int a;是声明,只是告诉大家有这么一个变量,但是还没有为其分配空间,a = 222;才真正创建了这个变量,后面的a = 333;是赋值。所以使用的是同一个。

示例一的用法与使用extern是一致的,如下

示例三

把pre.h中的语句改成extern int a;
在main.c的main函数前添加int a = 0;

这个结果与示例一是一致的。就是大家都引用pre.h,但是extern int a;表示只是声明了这个变量,并没有实例化,a的实例化到其他文件找,我们在main.c中调用int a = 0;进行了实例化,所以当test.c中使用a的时候也会到其他文件找,也找到的是main.c中的a,所以是同一个。

使用类库

如果A B是类库,就比较麻烦了,因为类库与一个单独的程序没有区别,是独立的,所以就会遇到下面的问题。

示例一

在A的头文件定义全局变量,不管是int g_val;还是int g_val = 0;编译链接是都会遇到multiple definition问题,为什么呢?

那是因为,当编译A时,不管是如何写,A中肯定用到了g_val,当第一次用g_val时,g_val就已经定义了,同样B中也是一样。所以当B再次链接A或者C链接A和B时,就会有两个定义的g_val,所以就出现了multiple definition

示例二

在A的头文件定义静态变量。static int g_val;
这样虽然不会报错了,但是有另一个问题,就是大家的都相互独立,也就是说使用的是多份g_val。

我们直到static除了设置静态变量,让数据可以一直存在,还有一个作用,就是限制作用范围到当前文件。所以当编译完A后,A中有一个g_val,编译完B,B中有一个g_val,同理C中也有一个g_val,并且是独立的。这样虽然编译成功,但是没有达到我们的目的,我们要求是使用同一个g_val。

示例三

在A的头文件声明extern int g_val;
在A的源码定义int g_val = 0;
A编译成共享库,其他类库也是共享库。
这样就可以解决我们的问题。

因为共享库只会在第一次调用的时候加载一次,同一个进程,也只会在第一次加载的时候映射一次静态变量和全局变量。也就是说,虽然A B C都有全局变量G,但是只会在第一次调用的时候创建一个G,后续都使用的是同一个,这个与一开始直接把所有文件一起编译一样,只不过不是在main.c中定义了int a = 0;而是为pre.h创建一个pre.c,在pre.c中定义了int a = 0;

shared library共享库与static library静态库区别

提到shared library,我们都会对比static library。

主要的区别实际上可以从其初衷来看:

  • static library最早被发明出来,因为很多模块并不经常修改,每次都编译很麻烦,可以先把这些模块编译成static library,再编译其他模块时,如果需要,就把static library相关的代码加入到其他模块中。
  • shared library是为了解决,有写模块,经常被使用,或者要给其他人用,可以编译成shared library,当程序运行时,如果执行到shared library的代码,就把shared library加载进来,调用对应的二进制模块。节省了硬盘和内存空间。
  • static library相当于一个文件包,把其他模块编译生成的.o文件打包成一个.a(static library),当使用是,就相当于编译过程中把.o编译成一个二进制一样。
  • 由于static library是编译到其他模块中的,如果接口没变,但是实现方式做了修改(比如修复bug),那么所有使用到这个static library的模块都需要重新编译。
  • shared library只会把一些查找对应接口的信息放到程序中,并不会把整个库放入到链接库/程序中。当系统中第一个程序使用某个share library时,系统才把这个shared library从硬盘加载到内存。并且当有第二个程序使用这个shared library时,会使用内存中的同一份数据,节省了内存。
  • 共享库都是编译一份,放到指定路径,不会存在链接程序中,节省了硬盘。
  • 虽然多个进程使用同一份内存中的共享库,但是其静态数据和全局数据是相互独立的,会重新映射。这也是合理的,不然大家都可以相互修改数据,简直乱套了。
  • 由于链接程序只保存了共享库的接口信息,并没有载入其全部实现的二机制数据,所以如果接口信息没有变更,可以随时替换共享库,而链接程序不需要重新编译。
    The Linux Programming Interface