背景
近期要將算法部署到一個機羣的虛擬主機(Debian 9.1 gcc 6.3.0)上,採用的是Java + JNI + shared library的方式來完成底層算法能力的部署。
其中需要用到各種第三方庫,有從源碼編譯的,也有直接下載的so,包括OpenCV相關、TensorFlow相關、MKL以OpenMP相關的動態庫。
遇到一個問題,libmklml_intel.so 這個庫只能放在 LD_LIBRARY_PATH中進行加載,而不能通過java.library.path完成加載,所以有必要搞清楚這兩個路徑究竟有什麼區別。
java.library.path
官方文檔的定義是:List of paths to search when loading libraries
從定義我們可以發現,首先是一個list,也就是說可以包括多個地址,然後這些地址是用來幫助jvm搜索需要加載的庫文件的。
設置java.library.path
最簡單的辦法就是在啓動jvm前通過java -Djava.library.path=path-to-your-libs
設置這個全局變量。
作用
那麼這個地址具體是如何被使用的呢?
當我們調用System.loadLibrary(libname)
時,會調用Runtime.loadLibary
,然後調用java/lang/ClassLoader.loadLibrary
。在ClassLoader.loadLibrary中,系統屬性java.library.path
將會被獲取,並用來生成需要加載的庫的絕對路徑,然後將這個絕對路徑傳給本地方法來調用dlopen/dlsym
並最終加載這個庫。
如果加載失敗,會根據實際情況返回三個異常值:
SecurityException − if a security manager exists and its checkLink method doesn’t allow loading of the specified dynamic library
UnsatisfiedLinkError − if the library does not exist
NullPointerException − if libname is null
可以參考OpenJDK的倉庫:
static void loadLibrary(Class fromClass, String name,
boolean isAbsolute) {
ClassLoader loader =
(fromClass == null) ? null : fromClass.getClassLoader();
if (sys_paths == null) {
usr_paths = initializePath("java.library.path");
sys_paths = initializePath("sun.boot.library.path");
}
if (isAbsolute) {
if (loadLibrary0(fromClass, new File(name))) {
return;
}
throw new UnsatisfiedLinkError("Can't load library: " + name);
}
if (loader != null) {
String libfilename = loader.findLibrary(name);
if (libfilename != null) {
File libfile = new File(libfilename);
if (!libfile.isAbsolute()) {
throw new UnsatisfiedLinkError(
"ClassLoader.findLibrary failed to return an absolute path: " + libfilename);
}
if (loadLibrary0(fromClass, libfile)) {
return;
}
throw new UnsatisfiedLinkError("Can't load " + libfilename);
}
}
for (int i = 0 ; i < sys_paths.length ; i++) {
File libfile = new File(sys_paths[i], System.mapLibraryName(name));
if (loadLibrary0(fromClass, libfile)) {
return;
}
}
if (loader != null) {
for (int i = 0 ; i < usr_paths.length ; i++) {
File libfile = new File(usr_paths[i],
System.mapLibraryName(name));
if (loadLibrary0(fromClass, libfile)) {
return;
}
}
}
// Oops, it failed
throw new UnsatisfiedLinkError("no " + name + " in java.library.path");
}
LD_LIBRARY_PATH
爲了搞清楚這個變量的作用,我們先說明一下Unix系統是如何加載動態庫的,然後自然就明白爲什麼要有LD_LIBRARY_PATH以及如何使用了。
動態庫如何加載?
在基於GNU glibc的系統上,包括所有的linux系統,啓動一個ELF格式的二進制可執行文件會自動調用加載器加載必要的動態鏈接庫,一個最簡單的可執行文件一般也會包含一些系統的動態庫比如libc.so等。在Linux系統中,這個加載器叫做/lib/ld-linux.so.X
,這個X指的是加載器的版本號。加載器然後查找並加載所需的動態庫。
加載器在什麼路徑中搜索和加載動態庫呢——/etc/ld.so.conf
,這個文件會包括/etc/ld.so.conf.d/*.conf
這些文件夾中所有的.conf文件,而具體的動態庫搜索路徑,就包含在每個.conf文件中,比如/etc/ld.so.conf.d/libc.conf
,它是libc的默認的搜索路徑/usr/local/lib
,這也是爲什麼我們不需要顯示聲明使用系統庫卻能自動完成加載的原因,也是爲什麼不同的系統編出來的庫無法通用的可見原因之一,因爲不同系統的/usr/local/lib
目錄下的動態庫並不一致。
如果每次啓動都去查找所有的目錄,那樣顯然是比較笨的做法,所以使用/etc/ld.so.cache
來緩存路徑,並通過ldconfig來更新這個緩存路徑,有興趣的可以自行查看一下這個緩存文件。實際上,這個緩存路徑也很長了,基本上包含了系統可能存放動態庫的路徑。
爲什麼有LD_LIBRARY_PATH?
上面我們說到可以通過cache和ldconfig來簡化搜索和加載動態庫的流程,但是還有兩個問題沒有考慮到,一是還沒有將編出來的庫放到系統目錄中去,二是依賴庫數量很少,不需要經過這麼複雜的查找。
LD_LIBRARY_PATH
就是用來滿足這個需要,它也指定一個搜索路徑,且ld-linux.so會優先在這個路徑下搜索需要的動態庫,如果沒找到,再去ld.so.conf中指定的目錄尋找。
使用
export LD_LIBRARY_PATH=paths-to-libs
需要注意的一點是,多個目錄是通過
:
隔開的
區別
前面分別介紹了java.library.path 和 LD_LIBRARY_PATH,都是爲了加載所需的動態庫,有什麼區別呢?
- 前者是在java環境中調用,在jvm啓動前設置生效;後者也是在啓動前,但是是在Unix環境中使用
- 前者是通過修改property來設置路徑;後者是直接增加了ld-linux.so的搜索路徑
- 對於JNI直接調用的庫,最好使用前者,對於有多重依賴關係的庫,最好使用LD_LIBRARY_PATH
參考
HowTo: How to configure library path for JNI dependent libraries
https://zauner.nllk.net/post/0013-jni-and-the-java-library-path/
https://docs.oracle.com/javase/8/docs/api/java/lang/System.html#getProperties–
https://www.tutorialspoint.com/java/lang/runtime_loadlibrary.htm
https://stackoverflow.com/questions/27945268/difference-between-using-java-library-path-and-ld-library-path
Linux關於動態庫的文檔