除了libc和libm庫,UNIX系統庫沒有其他標准的命名規范。Linux上的一些系統庫可能和UNIX平台上庫的名稱不同,這就需要知道Linux上各庫所包含和支持的功能。表3-2根據所支持的功能列舉了一些Linux系統庫。
下面列出了GNU libc庫所包含的庫文件以及對應的描述(注釋5):
- ld.so,為使用了共享庫的可執行程序提供的一個輔助程序;
- libBrokenLocal.[a,so],Mozilla等應用程序用以解決被破壞的locale的庫文件;
- libSegFault.so,段錯誤信號處理器,它試圖捕獲段錯誤信號。
- libanl.[a,so],異步的名稱查詢庫。
- libbsd-compat.a,在Linux上運行BSD程序時需要的庫。
- libc.[a,so],最主要的C庫(常用的C函數的集合)。
- libcrypt.[a,so],加密庫。
- libdl.[a,so],動態鏈接接口庫。
- libg.a,g++運行時庫。
- libieee.a,IEEE浮點運算庫。
- libm.[a,so],數學庫。
- libmcheck.a,包含啟動時運行的代碼。
- libmemusage.so,memusage用來收集應用程序內存使用情況的庫。
- libnsl.a,網絡服務庫。
- libnss_comkpat.so,libnss_dns.so, libnss_files.so, libnss_hesiod.so, libnss_nis.so, libnss_nisplus.so,NSS(Name Service Switch)庫,包含解析主機名、用戶名、組名、別名、服務、協議等的函數。
- libpcprofile.so,包含一些跟蹤統計代碼行消耗CPU時間的概要分析(profiling)函數。
- libpthread.[a,so],POSIX線程庫。
- libresolv.[a,so],包含為網絡域名服務器創建、發送、解釋網絡包的函數。
- librpcsvc.a,包含提供各種RPC服務的函數。
- librt.[a,so],包含POSIX1.b實時擴展所定義的大部分接口函數。
- libthread_db.so,包含開發多線程程序調試器的函數。
- libutil.[a,so],包含常用的UNIX工具使用的“標准”函數。
上面這些庫大多位於/usr/lib目錄,也有一些在/lib目錄下,例如libSegFault.so.
3.1.1 glibc遵循的標准
GNU glibc發布了一個描述其所遵循的標准的報告(注釋6)。該報告同時也列出了GNU libc需要改進的地方。寫作本書時,該報告顯示GNU libc通過了FIPS POSIX90、POSIX96、UNIX98、ANSI、C89/99,和ISO9899標准的頭文件一致性檢查。所有主要Linux發行版的glibc也都遵循LSB規范。
3.2 GNU科學庫
把進行高性能計算的應用程序移植到Linux上需要一個支持庫,該庫要與UNIX平台上的科學庫非常匹配。Linux上類似的庫叫GNU科學庫(GNU Scientific Library,簡寫作GSL)。GSL是一系列數學運算例程的集合。這些例程是用C語言重新編寫的,並且給編程人員提供了一個新式的API模式---允許編程人員為各高級語言編寫包裹函數(wrapper)。這些源代碼使用的是GPL發布許可。
GNU科學庫包含了數學運算領域的很多內容。表3-3列出了GNU科學庫提供的例程。
這些例程的用法在GSL手冊(注釋7)中有詳盡的描述,包括函數的定義、示例程序,以及函數實現的算法所引用的論文。
3.3 共享庫
我們所移植過的大多數應用程序都使用了共享庫。然而,不同的操作系統在創建和命名共享庫時卻不盡相同。Linux上,共享庫可以有不同的文件擴展名,例如,共享庫可以以.so或.so.1.0結束。以.so.x.x(x為數字)結尾的共享庫叫版本化庫。第一個數字代表大版本號,第二個數字代表小版本號。有些情況下,共享庫的擴展名還可以是.so.x.x.x(x為數字)的形式,這裡最後一個數字代表發布號,並且是可選的。下面給出了共享庫文件名的格式:
(代碼)p58 第11行,lib.so...
大版本號、小版本號,以及發布號的變化反映了對共享庫所作的不同類型的修改。下面是對增大大版本號、小版本號和發布號的一些指導:
- 當對共享庫提供的接口做了與以前版本不兼容的改變時,需要增大大版本號。這個大的改變意味著依賴該庫先前大版本的應用程序需要作相應修改才能使用大版本更新後的庫。
- 當共享庫增加了新的接口同時也保留了原來的接口時,增大小版本號。
- 當作了與以前兼容的修改又沒有增加新接口時,增大發布號。這通常是對一些實現做了改動以提高性能和擴展性。
要在Linux上創建共享庫,使用-shared編譯參數;該參數告訴GNU ld創建一個共享庫而不是應用程序。下面是這樣一個例子:
(代碼)p58 最後一行 $ gcc –o libfoo.so –shared –fpic foo.c
3.4 庫版本化
在共享庫和應用程序之間維護二進制級的兼容性或ABI是很重要的。共享庫的ABI是應用程序依賴的運行時接口;如果每次發布時共享庫的ABI都與以前的兼容,那麼在其中某一個版本的共享庫上編譯的應用程序不需要任何改動就可以在後續版本上運行。庫版本化就是Linux以及同期的其他操作系統實現二進制兼容性的方法。
我們以前移植過的一些應用程序需要庫版本化的支持。各UNIX平台也都實現了庫版本化,但實現的方法不盡相同。Linux提供了兩種不同的技術來實現庫版本化:外部庫版本化和符號版本化。
3.4.1 外部庫版本化
鏈接過程中,鏈接器(ld)會查找以.so結尾的共享庫文件。以.so結尾的庫文件叫鏈接器名稱,這是由他們在Linux上的使用方式決定的。當編譯一個依賴某一共享庫的應用程序時,僅僅是該共享庫的soname(不是共享庫的文件名)作為依賴關系被記錄在應用程序的二進制代碼中。運行時鏈接器就是使用共享庫的soname來查找和裝載該庫的。共享庫的soname只包含有大版本號(例如,libfoo.so.1)
當修改後的共享庫與以前版本不兼容時,新的共享庫必須有一個新的外部版本名稱。也就是說,該庫的soname必須改變。這些不兼容的修改包括:刪除一個符號,去掉某函數的一個參數,改變了某函數的語義屬性以致與以前的定義不再一致並且與老版本二進制不兼容等等。我們來看下面的例子。(見pdf附件 341.pdf)
3.4.2 符號版本化
就像前面所提到的,當對共享庫所作的修改能夠向前兼容時,我們只增大小版本號。這種修改包括增加一些新的接口同時又不改變已有的接口。但是,即使只做這種小版本的修改,也會出現一個很重要的問題:一個在某一小版本的共享庫上編譯的應用程序並不一定能夠在以前小版本的庫上運行。這是因為該應用程序可能使用了新增加的、以前小版本的庫中沒有的接口。為了解決這個問題,引入了符號版本化。符號版本化允許共享庫記錄下每個小版本都新增了什麼內容。
在Linux上,GNU ld可以使用-version-script連接器選項來創建符號版本化的共享庫。編譯器選項-Wl,--version-script=mapfile告訴鏈接器哪些符號要從生成的共享庫中輸出出來。每個符號分屬global(被輸出)和local(不被輸出)兩類中的一種。來看下面的例子。foo.c包含一個函數foo1,該文件用來創建1.1版本的共享庫。(見附件 示例代碼.pdf)
可以看到,這次main只引用了版本化庫的LX_1.1。
GNU ld還允許在定義符號的源文件中把符號綁定到某一版本中,而不僅僅是在腳本文件中指定。另外,GNU ld還允許同一函數的多個版本出現在同一個共享庫中。更多詳細信息,請參考GNU ld手冊(注釋13)和Ulrich Drepper的文章“How to Write Shared Libraries”。
從2.1版本開始,glibc就已經實現了符號版本化。符號版本化同時也是LSB規范1.2及更高版本的一部分。
3.5 動態鏈接器(運行時鏈接器)
Linux動態鏈接器(/lib/ld.so.1或/lib64/ld64.so.1)查找和裝載應用程序所需的共享庫,准備應用程序的運行,然後運行應用程序。除非編譯時為ld指明-static選項,否則Linux二進制程序都是動態鏈接的。
在所有現代UNIX操作系統上,都有一些環境變量可以影響動態鏈接器的運行。例如AIX上的環境變量LIBPATH可以改變動態鏈接器的搜索路徑。以下環境變量可以影響到Linux上動態鏈接器的運行:
- LD_LIBRARY_PATH,以冒號分開的目錄列表,運行時會在這些目錄中查找需要的庫。
- LD_PRELOAD,以空格分開的庫列表,這些庫會在其他所有庫之前裝載。這常常用來有選擇的覆蓋某些共享庫中的函數。
- LD_BIND_NOW,如果該環境變量設置成非空字符串,動態鏈接器會在程序啟動時解析所有符號,而不是首次引用時才解析符號(也就是常說的“延遲綁定”)。這在使用調試器時非常有用。
- LD_TRACE_LOADED_OBJECTS,如果該環境變量設置成非空字符串,程序會列出它所依賴的共享庫,就像運行ldd命令一樣,而不是正常的執行。
Linux動態鏈接器采用廣度優先(breadth first)的方式解決庫的依賴關系。也就是說,首先是可執行程序所依賴的庫按照動態節(dynamic section)列出的順序被裝載進來,然後是“第一個被依賴的庫”所依賴的庫按照同樣的方法裝載進來,以此類推,直到所有的依賴關系都被解決。
在命令行運行下面的命令,會得到更多關於Linux動態鏈接器的信息:
(代碼)(P69第最後一行)
$ info ld.so
3.5.1 編程接口
Linux提供了一套API來動態裝載庫。下面列出了這些API:
- dlopen,打開一個庫,並為使用該庫做些准備。
- dlsym,在打開的庫中查找符號的值。
- dlclose,關閉庫。
- dlerror,返回一個描述最後一次調用dlopen、dlsym,或dlclose的錯誤信息的字符串。
C語言用戶需要包含頭文件dlfcn.h才能使用上述API。glibc還增加了兩個POSIX標准中沒有的API:
- dladdr,從函數指針解析符號名稱和所在的文件。
- dlvsym,與dlsym類似,只是多了一個版本字符串參數。
在Linux上,使用動態鏈接的應用程序需要和庫libdl.so一起鏈接,也就是使用選項-ldl。但是,編譯時不需要和動態裝載的庫一起鏈接。程序3-1是一個在Linux上使用dl*例程的簡單示例。
(代碼)(P70-73)
編譯該程序:
(代碼)(P73第5行)
$ make
運行程序:
(代碼)(P73第15行)
$ ./main
用ldd命令檢查可執行程序:
$ ldd ./main
(代碼)(P73第19行)
可以看到,可執行程序main沒有引用動態裝載的庫。
3.5.2 延遲重定位(Lazy Relocation)
延遲重定位/裝載是一個允許符號只在需要時才重定位的特性。這常在各UNIX系統上解析函數調用時用到。當一個和共享庫一起鏈接的應用程序幾乎不會用到該共享庫中的函數時,該特性被證明是非常有用的。這種情況下,只有庫中的函數被應用程序調用時,共享庫才會被裝載,否則不會裝載,因此會節約一些系統資源。但是如果把環境變量LD_BIND_NOW設置成一個非空值,所有的重定位操作都會在程序啟動時進行。也可以在鏈接器命令行通過使用-z now鏈接器選項使延遲綁定對某個特定的共享庫失效。需要注意的是,除非重新鏈接該共享庫,否則對該共享庫的這種設置會一直有效。
3.5.3 初始化(initializing)和終止化(finalizing)函數
有時候,以前的代碼可能用到了兩個特殊的函數:_init和_fini。_init和_fini函數用在裝載和卸載某個模塊(注釋14)時分別控制該模塊的構造器和析構器(或構造函數和析構函數)。他們的C語言原型如下:
(代碼)(P74第8行)
void _init(void);
void _fini(void);
當一個庫通過dlopen()動態打開或以共享庫的形式打開時,如果_init在該庫中存在且被輸出出來,則_init函數會被調用(注釋15)。如果一個庫通過dlclose()動態關閉或因為沒有應用程序引用其符號而被卸載時,_fini函數會在庫卸載前被調用。當使用你自己的_init和_fini函數時,需要注意不要與系統啟動文件一起鏈接。可以使用GCC選項-nostartfiles做到這一點。
但是,使用上面的函數或GCC的-nostartfiles選項並不是很好的習慣,因為這可能會產生一些意外的結果。相反,庫應該使用__attribute__((constructor))和__attribute__((destructor))函數屬性來輸出它的構造函數和析構函數。如下所示:
(代碼)(P74第21行)
void __attribute__((constructor)) x_init(void)
void __attribute__((destructor)) x_fini(void)
構造函數會在dlopen()返回前或庫被裝載時(注釋16)調用。析構函數會在這樣幾種情況下被調用:dlclose()返回前,或main()返回後,或裝載庫過程中exit()被調用時。
3.6 系統調用
系統調用是用戶程序請求內核為調用線程或進程提供具體服務的接口。因為UNIX平台上的一些系統調用是與操作系統密切相關的,因此在Linux上可能不存在類似的系統調用。這種情況下,就需要在Linux上實現一個包裹函數(wrapper)。
Linux上系統調用的列表位於/usr/include/asm/unistd.h中。本書的附錄部分還對Linux和UNIX系統(如Solaris,HP-UX等)進行了並列比較。
3.7 大頁面支持
大頁面的應用主要是用來提高應用程序的性能,該類應用程序需要分配大塊內存並且頻繁訪問該內存。性能的提高主要是通過減少地址轉換緩沖器 (Translation Lookaside Buffer,簡寫作TLB,一塊虛擬地址到物理地址轉換的緩沖區)的未命中次數來實現的。當TLB能夠映射更大的虛擬內存范圍時,即可減少TLB的未命中次數。因為大部分現代的體系結構支持多種頁面大小,上述方法也就可以實現了。例如,Intel 32位架構支持4KB和4MB(PAE模式時為2MB)的頁面;Itanium支持多種頁面大小:4K,8K,64K,256K,1M,4M,16M和256M;SUN UltraSPARC支持8K,64K,512K和4M的頁面;64位PowerPC(ppc64)支持4K,64K,16M和64G的頁面。本節內容將告訴應用程序開發人員如何使用Linux內核提供的大頁面支持功能。
Linux 2.6內核包含有內建的對hugetlbpage(Linux社區稱呼大頁面的專用術語)的支持。內核配置成支持hugetlbpage時,命令cat /proc/meminfo的輸出會顯示出關於hugetlbpage的信息,如下例:
(代碼)(P75倒數第8行)
HugePages_Total : 20
HugePages_Free : 20
Hugepagesize: 16384 KB
一種類型為hugetlbfs的文件系統也應該會出現在/proc/filesystems中。在用戶空間的應用程序能夠使用hugetlbpage支持前,管理員應該先在內核中分配這些大頁面。/proc/sys/vm/nr_hugepages的內容顯示的是內核中當前配置的大頁面的個數。如果要在系統上配置10個大頁面,可以用下面的命令:
(代碼)(P76第1行)
echo 10 > /proc/sys/vm/nr_hugepages
只有當系統中存在足夠的連續物理內存時,分配請求才會成功;只有存在足夠多的能夠轉回到正常內存池的空閒大頁面時,釋放請求才會成功。用作hugetlbpage的頁面在內核中作為保留頁面而不能用作其他用途。
應用程序開發人員有兩種方法可以使用hugetlbpage支持:
1. 系統V共享內存系統調用(shmget,shmat)
2. mmap系統調用
同一個應用程序也可以兩者都使用。
下面的示例程序中,我們給出了如何使用上述系統調用來獲得hugetlbpage支持。這些程序來源於/usr/src/linux/Documentation/vm/hugetlbpage.txt。
示例3-2中,應用程序使用系統V共享內存系統調用來申請由大頁面保留的256M內存。shmget系統調用使用SHM_HUGETLB標志告訴內核申請的是大頁面。
(代碼)(P76-78)
對ia86架構,內核為大頁面保留了一個特定的內存區域。也就是說,調用進程必須指定某一個固定的地址。但對i386,x86_64,和ppc64不需要一個固定的地址。
你也可能需要把每個共享內存段的最大大小增大到256MB。這可以用下面的命令實現:
(代碼)(P78第26行)
echo 268435456 > /proc/sys/kernel/shmmax
還需要關注的另一個限制是/proc/sys/kernel/shmall,它顯示的是系統中可以創建的共享內存的總頁數。
mmap system call
這種情況下,需要管理員首先掛載一個hugetlbfs類型的文件系統,然後在該掛載點上創建的所有文件都保存在大頁面上。
(代碼)(P78第33行)
mount none /mnt/huge –t hugetlbfs –o uid=1000,gid=100
上述命令在目錄/mnt/huge上掛載一個hugetlbfs類型的文件系統,並把該文件系統的根目錄的所有者和組分別設置成1000和100。程序3-3給出了一個使用mmap系統調用申請由大頁面保留的256MB內存的示例。
(代碼)(P79-80)
注意,對hugetlbfs文件系統上的文件,read和write系統調用是不支持的。通常的chown,chgrp和chmod(如果有權限的話)可以用來改變hugetlbfs文件系統上文件的屬性。