Linux C語言Shared Library共享庫細節探究

開發中遇到一個問題,比如有一個類庫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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章