系統程式設計師成長計畫 演算法與容器 三

2021-04-26 01:21:16 字數 3164 閱讀 1770

前面我們通過容器介面抽象了雙向鍊錶和動態陣列,這樣佇列的實現就不依賴於具體的容器了。但是作為佇列的使用者,它仍然要在編譯時決定使用哪個容器。佇列的測試程式就是佇列的使用者之一,它的實現**如下:

queue* queue = queue_create(linear_container_dlist_create(null, null)); 

for(i = 0; i < n; i++)

...

這裡必須明確指定是 linear_container_dlist_create還是 linear_container_darray_create,假設使用者想要換一種容器,那還是要修改**並重新編譯才行。現在我們思考另外乙個問 題,如何讓使用者(如這裡的測試程式)想換一種容器時,不需要修改**也不需要重新編譯。

在繼續學習之前,我們先介紹幾個概念:

靜態庫:在linux下,靜態庫的擴充套件名為.a,a代表archive的意思。正常情況下乙個c原始檔編譯之後生成乙個目標檔案(.o),目標檔案 裡存放的是程式的機器指令和資料,它不包含執行時的資訊,所以不能直接執行。用命令ar把多個目標檔案打包成乙個檔案就生成了所謂的靜態庫。可執行檔案鏈 接靜態庫時,會把用到的函式和資料拷貝過去。多個可執行檔案鏈結同乙個靜態庫時,所用到的函式和資料就會拷貝多次,那就存在不少空間浪費。

共享庫:顧名思義,共享庫可以在多個可執行檔案之間共享,鏈結時不用拷貝函式或資料,只是建立乙個函式鏈結表(plt),在執行時通過這個表來確定 具體呼叫的函式。共享庫可以有效的避免空間浪費,但它也不是免費的午餐,它在載入時存在額外的開銷,在鏈結多個共享庫時會比較明顯。不過目前出現的 prelink和gnu hash等技術,有效的緩解了這個問題。共享庫另外乙個好處就是可以單獨公升級,理論上,修改某個共享庫不需要重新編譯依賴它的應用程式(不過實際操作時與 它的介面變化和資訊隱藏程度有關)。

可執行檔案:linux下加x屬性的檔案都是可執行檔案,這裡可執行檔案特指elf(executable and linking format)格式的可執行檔案。 可執行檔案在編譯時連線靜態庫,會把所用到的函式會被拷貝過來,執行時不再依賴於靜態庫。在編譯時鏈結共享庫,它不拷貝函式和資料,但在執行時會依賴於其 鏈結的共享庫。

回到前面的問題:我們發現在編譯時,無論是鏈結靜態庫還是共享庫,它都繫結了呼叫者與使用者之間的關係。真正要在執行時決定而不是編譯時決定使用哪種容器,那就不能包含具體容器的標頭檔案,鏈結具體容器所在的庫了。今天我們要學習一種新的技術:在執行時動態載入共享庫。

除了一些嵌入式環境下使用的實時作業系統(rtos)外,現代作業系統都支援在執行時動態載入共享庫的機制。linux下有dlopen系列函式,windows下有loadlibrary系列函式,其它平台也有類似的函式。下面是使用dlopen的簡單示例:

#include 

#include

#include

int main(int argc, char **argv)

由於這些函式在每個平台的名稱和引數都有所不同,直接使用這些函式會帶來可移植性的問題,為此有必要對它們進行包裝。不同平台有不同的函式,也就是 說存在多種不同的實現,那這是否意味著要用介面呢?答案是不用。原因是同乙個平台只一種實現,而且不會出現潛在的變化。我們要做的只是加乙個適配層,用它 來隔離不同平台就好了。

我們把這個對載入函式的適配層稱為module,宣告(module.h)如下:

struct _module;

typedef struct _module module;

typedef enum _moduleflags

moduleflags;

module* module_create(const char* file_name, moduleflags flags);

void* module_sym(module* thiz, const char* func_name);

void module_destroy(module* thiz);

這裡它們的實現只是對dl系列函式做個簡單包裝。由於不同平台有不同的實現,為了維護方便,我們把不同的實現放在不同的檔案中,比如linux的實現放在module_linux.c裡。

module* module_create(const char* file_name, moduleflags flags)

} return thiz;

}

我們再看看佇列的測試程式怎麼寫:

#include "linear_container.h" 

typedef linearcontainer* (*linearcontainerdarraycreatefunc)(datadestroyfunc data_destroy, void* ctx);

int main(int argc, char* argv)

module = module_create(argv[1], 0);

return_val_if_fail(module != null, 0);

linear_container_create = (linearcontainerdarraycreatefunc)module_sym(module, argv[2]);

return_val_if_fail(linear_container_create != null, 0);

queue = queue_create(linear_container_create(null, null));

...}

說明:

o 標頭檔案只包含linear_container.h,而不包含linear_container_dlist.h。

o 通過module_sym獲取容器的建立函式,而不是直接呼叫linear_container_dlist_create。

編譯時不再鏈結容器所在的共享庫:

gcc -wall -g module_linux.c queue.c queue_test.c -ldl -o queue_dynamic_test

執行決定要使用的容器:

./queue_dynamic_test ./libcontainer.so linear_container_dlist_create

這樣一來,容器的呼叫者和使用者完全分隔開了。介面+動態載入的方式也稱為外掛程式式設計,它是軟體可擴充套件性的基礎,像作業系統、辦公軟體、瀏覽器和web伺服器等大型軟體都無一例外的使用了這類技術。

本節示例**請到這裡

系統程式設計師成長計畫 演算法與容器 三 上

系統程式設計師成長計畫 演算法與容器 三 上 作者 李先靜 前面我們通過容器介面抽象了雙向鍊錶和動態陣列,這樣佇列的實現就不依賴於具體的容器了。但是作為佇列的使用者,它仍然要在編譯時決定使用哪個容器。佇列的測試程式就是佇列的使用者之一,它的實現 如下 queue queue queue create...

系統程式設計師成長計畫005

1.這個變成大寫的函式,就不需要用函式指標來給foreach做引數了。因為他沒有什麼其他變種,不像print那樣,既要print int又要print str。函式指標,或者說 函式,別瞎用!2.書裡的寫法 dlist foreach dlist,str toupper,null 看來還是堅持了 函...

系統程式設計師成長計畫 併發 五

文章出處 作者 李先靜 無鎖 lock free 資料結構 多執行緒併發執行時,雖然有共享資料,如果所有執行緒只是讀取共享資料而不修改它,也是不用加鎖的,比如 段就是共享的 資料 每個執行緒都會讀取,但是不用加鎖。排除所有這些情況,多執行緒之間有共享資料,有的執行緒要修改這些共享資料,有的執行緒要讀...