原文: http://nullwy.me/2018/01/java...
若是以爲個人文章對你有用,請隨意讚揚
前段時間開發的時候,遇到一個問題,就是如何用 Java 實現 chdir
?網上搜索一番,發現了 JNR-POSIX
項目 [stackoverflow ]。俗話說,好記性不如爛筆頭。如今將涉及到的相關知識點總結成筆記。html
其實針對 Java 實現 chdir
問題,官方 20 多年前就存在對應的 bug,即 JDK-4045688 'Add chdir or equivalent notion of changing working directory'。這個 bug 在 1997.04 建立,目前的狀態是 Won't Fix
(不予解決),理由大體是,若實現與操做系統同樣的進程級別的 chdir
,將影響 JVM 上的所有線程,這樣引入了可變(mutable)的全局狀態,這與 Java 的安全性優先原則衝突,如今添加全局可變的進程狀態,已經太遲了,對不變性(immutability)的支持纔是 Java 要實現的特性。java
chdir
是平臺相關的操做系統接口,POSIX 下對應的 API 爲 int chdir(const char *path);
,而 Windows 下對應的 API 爲 BOOL WINAPI SetCurrentDirectory(_In_ LPCTSTR lpPathName);
,另外 Windows 下也可使用 MSVCRT 中 API 的 int _chdir(const char *dirname);
(MSVCRT 下內部實現其實就是調用 SetCurrentDirectory
[reactos ] )。python
Java 設計理念是跨平臺,"write once, run anywhere"。很平臺相關的 API,雖然各個平臺都有本身的相似的實現,但存在會差別。除了多數常見功能,Java 並無對所有操做系統接口提供完整支持,好比不少 POSIX API。除了 chdir
,另一個典型的例子是,在 Java 9 之前 JDK 獲取進程 id 一直沒有簡潔的方法 [stackoverflow ],最新發布的 Java 9 中的 JEP 102(Process API Updates)才加強了進程 API。獲取進程 id 可使用如下方式 [javadoc ]:react
long pid = ProcessHandle.current().pid();
相比其餘語言,Pyhon 和 Ruby,對操做系統相關的接口都有更多的原生支持。Pyhon 和 Ruby 實現的相關 API 基本上都帶有 POSIX 風格。好比上文提到,chdir
和 getpid
,在 Pyhon 和 Ruby 下對應的 API 爲:Pyhon 的 os 模塊 os.chdir(path) 和 os.getpid();Ruby 的 Dir 類的 [Dir.chdir( [ string] )](https://ruby-doc.org/core-2.2... 類方法和 Process 類的 Process.pid 類屬性。Python 解釋器的 chdir
對應源碼爲 posixmodule.c#L2611,Ruby 解釋器的 chdir
對應源碼爲 dir.c#L848 和 win32.c#L6741。git
Java 下要想實現本地方法調用,須要經過 JNI。關於 JNI 的介紹,能夠參閱「Java核心技術,卷II:高級特性,第9版2013」的「第12章 本地方法」,或者讀當年 Sun 公司 JNI 設計者 Sheng Liang(梁勝)寫的「Java Native Interface: Programmer's Guide and Specification」。本文只給出實現 getpid
的一個簡單示例。程序員
首先使用 Maven 建立一個簡單的腳手架:github
mvn archetype:generate \ -DgroupId=com.test \ -DartifactId=jni-jnr \ -DpackageName=com.test \ -DinteractiveMode=false
在 com.test
包下添加 GetPidJni
類:web
package com.test; public class GetPidJni { public static native long getpid(); static { System.loadLibrary("getpidjni"); } public static void main(String[] args) { System.out.println(getpid()); } }
用 javac
編譯代碼 GetPidJNI.java
,而後用 javah
生成 JNI 頭文件:c#
$ mkdir -p target/classes $ javac src/main/java/com/test/GetPidJni.java -d "target/classes" $ javah -cp "target/classes" com.test.GetPidJni
生成的 JNI 頭文件 com_test_GetPidJni.h
,內容以下:windows
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_test_GetPidJni */ #ifndef _Included_com_test_GetPidJni #define _Included_com_test_GetPidJni #ifdef __cplusplus extern "C" { #endif /* * Class: com_test_GetPidJni * Method: getpid * Signature: ()J */ JNIEXPORT jlong JNICALL Java_com_test_GetPidJni_getpid (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif
如今有了頭文件聲明,但尚未實現,手動敲入 com_test_GetPidJni.c
:
#include "com_test_GetPidJni.h" JNIEXPORT jlong JNICALL Java_com_test_GetPidJni_getpid (JNIEnv * env, jclass c) { return getpid(); }
編譯 com_test_GetPidJni.c
,生成 libgetpidjni.dylib
:
$ gcc -I $JAVA_HOME/include -I $JAVA_HOME/include/darwin -dynamiclib -o libgetpidjni.dylib com_test_GetPidJni.c
生成的 libgetpidjni.dylib
,就是 GetPidJni.java
代碼中的 System.loadLibrary("getpidjni");
,須要加載的 lib。
如今運行 GetPidJni
類,就能正確獲取 pid:
$ java -Djava.library.path=`pwd` -cp "target/classes" com.test.GetPidJni
JNI 的問題是,膠水代碼(黏合 Java 和 C 庫的代碼)須要程序員手動書寫,對不熟悉 C/C++ 的同窗是很大的挑戰。
JNA(Java Native Access, wiki, github, javadoc, mvn),提供了相對 JNI 更加簡潔的調用本地方法的方式。除了 Java 代碼外,再也不須要額外的膠水代碼。這個項目最先能夠追溯到 Sun 公司 JNI 設計者 Sheng Liang 在 1999 年 JavaOne 上的分享。2006 年 11月,Todd Fast (也來自 Sun 公司) 首次將 JNA 發佈到 dev.java.net 上。Todd Fast 在發佈時提到,本身在這個項目上已經斷斷續續開發並完善了 6-7 年時間,項目剛剛在 JDK 5 上重構和重設計過,還可能有不少缺陷或缺點,但願其餘人能瀏覽代碼並參與進來。Timothy Wall 在 2007 年 2 月重啓了這項目,引入了不少重要功能,添加了 Linux 和 OSX 支持(本來只在 Win32 上測試過),增強了 lib 的可用性(而非僅僅基本功能可用) [ref ]。
看下示例代碼:
import com.sun.jna.Library; import com.sun.jna.Native; public class GetPidJNA { public interface LibC extends Library { long getpid(); } public static void main(String[] args) { LibC libc = Native.loadLibrary("c", LibC.class); System.out.println(libc.getpid()); } }
最初,JRuby 的核心開發者 Charles Nutter 在實現 Ruby 的 POSIX 集成時就使用了 JNA [ref ]。但過了一段時候後,開始開發 JNR(Java Native Runtime, github, mvn) 替代 JNA。Charles Nutter 在介紹 JNR 的 slides 中闡述了緣由:
Why Not JNA? - Preprocessor constants? - Standard API sets out of the box - C callbacks? - Performance?!?
即,(1) 預處理器的常量支持(經過 jnr-constants 解決);(2) 開箱即用的標準 API(做者實現了 jnr-posix, jnr-x86asm, jnr-enxio, jnr-unixsocket);(3) C 回調 callback 支持;(4) 性能(提高 8-10 倍)。
使用 JNR-FFI(github, mvn)實現 getpid
,示例代碼:
import jnr.ffi.LibraryLoader; public class GetPidJnr { public interface LibC { long getpid(); } public static void main(String[] args) { LibC libc = LibraryLoader.create(LibC.class).load("c"); System.out.println(libc.getpid()); } }
使用 JNR-POSIX(github, mvn)實現 chdir
和 getpid
,示例代碼:
import jnr.posix.POSIX; import jnr.posix.POSIXFactory; public class GetPidJnrPosix { private static POSIX posix = POSIXFactory.getPOSIX(); public static void main(String[] args) { System.out.println(posix.getcwd()); posix.chdir(".."); System.out.println(posix.getcwd()); System.out.println(posix.getpid()); } }
性能測試代碼爲 BenchmarkFFI.java
(github),測試結果以下:
# JMH version: 1.19 # VM version: JDK 1.8.0_144, VM 25.144-b01 Benchmark Mode Cnt Score Error Units BenchmarkFFI.testGetPidJna thrpt 10 8225.209 ± 206.829 ops/ms BenchmarkFFI.testGetPidJnaDirect thrpt 10 10257.505 ± 736.135 ops/ms BenchmarkFFI.testGetPidJni thrpt 10 77852.899 ± 3167.101 ops/ms BenchmarkFFI.testGetPidJnr thrpt 10 58261.657 ± 5187.550 ops/ms
即:JNI > JNR > JNA (Direct Mapping) > JNA (Interface Mapping)。相對 JNI 的實現性能,其餘三種方式,從大到小的性能百分比依次爲:74.8% (JNR), 13.2% (JnaDirect), 10.6% (JNA)。在博主電腦上測試,JNR 相比 JNA 將近快了 6-7 倍(JNR 做者 Charles Nutter 針對 getpid
的測試結果是 JNR 比 JNA 快 8-10 倍 [twitter slides ])。
先來看下 JNA,JNA 官方文檔 FunctionalDescription.md,對其實現原理有很好的闡述。這裏將從源碼角度分析實現的核心邏輯。
回顧下代碼,咱們現實定義了接口 LibC
,而後經過 Native.loadLibrary("c", LibC.class)
獲取了接口實現。這一步是怎麼作到的呢?翻下源碼 Native.java#L547 就知道,實際上是經過動態代理(dynamic proxy)實現的。使用動態代理須要實現 InvocationHandler 接口,這個接口的實如今 JNA 源碼中是類 com.sun.jna.Library.Handler。示例中的 LibC
接口定義的所有方法,將所有分派到 Handler 的 invoke 方法下。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
而後根據返回參數的不一樣,分派到 Native 類的,invokeXxx 本地方法:
/** * Call the native function. * * @param function Present to prevent the GC to collect the Function object * prematurely * @param fp function pointer * @param callFlags calling convention to be used * @param args Arguments to pass to the native function * * @return The value returned by the target native function */ static native int invokeInt(Function function, long fp, int callFlags, Object[] args); static native long invokeLong(Function function, long fp, int callFlags, Object[] args); static native Object invokeObject(Function function, long fp, int callFlags, Object[] args); ...
好比,long getpid()
會被分派到 invokeLong,而 int chmod(String filename, int mode)
會被分派到 invokeInt
。invokeXxx 本地方法參數:
Function function
,記錄了 lib 信息、函數名稱、函數指針地址、調用慣例等元信息;long fp
,即函數指針地址,函數指針地址經過 Native#findSymbol()得到(底層是 Linux API dlsym 或 Windows API GetProcAddress )。int callFlags
,即調用約定,對應 cdecl 或 stdcall。int callFlags
,即函數入參,若無參數,args 大小爲 0,如有多個參數,本來的入參被從左到右依次保存到 args 數組中。再來看下 invokeXxx
本地方法的實現 dispatch.c#L2122(invokeInt
或 invokeLong
實現源碼相似):
/* * Class: com_sun_jna_Native * Method: invokeInt * Signature: (Lcom/sun/jna/Function;JI[Ljava/lang/Object;)I */ JNIEXPORT jint JNICALL Java_com_sun_jna_Native_invokeInt(JNIEnv *env, jclass UNUSED(cls), jobject UNUSED(function), jlong fp, jint callconv, jobjectArray arr) { ffi_arg result; dispatch(env, L2A(fp), callconv, arr, &ffi_type_sint32, &result); return (jint)result; }
即,所有 invokeXxx
本地方法統一被分派到 dispatch
函數 dispatch.c#L439:
static void dispatch(JNIEnv *env, void* func, jint flags, jobjectArray args, ffi_type *return_type, void *presult)
這個 dispatch
函數是所有邏輯的核心,實現最終的本地函數調用。
咱們知道,發起函數調用,須要構造一個棧幀(stack frame)。構造棧幀,涉及到參數壓棧次序(參數從左到右壓入仍是從右到左壓入)和清理棧幀(調用者清理仍是被調用者清理)等實現細節問題。不一樣的編譯器在不一樣的 CPU 架構下有不一樣的選擇。構造棧幀的具體實現細節的選擇,被稱爲調用慣例(calling convention)。按照調用慣例構造整個棧幀,這個過程由編譯器在編譯階段完成的。好比要想發起 sum(2, 3)
這個函數調用,編譯器可能會生成以下等價彙編代碼:
; 調用者清理堆棧(caller clean-up),參數從右到左壓入棧 push 3 push 2 call _sum ; 將返回地址壓入棧, 同時 sum 的地址裝入 eip add esp, 8 ; 清理堆棧, 兩個參數佔用 8 字節
dispatch
函數是,須要調用的函數指針地址、輸入參數和返回參數,所有是運行時肯定。要想完成這個函數調用邏輯,就要運行時構造棧幀,生成參數壓棧和清理堆棧的工做。JNA 3.0 以前,實現運行時構造棧幀的邏輯的對應代碼 dispatch_i386.c、dispatch_ppc.c 和 dispatch_sparc.s,分別實現 Intel x8六、PowerPC 和 Sparc 三種 CPU 架構。
運行時函數調用,這個問題實際上是一個通常性的通用問題。早在 1996 年 10 月,Cygnus Solutions 的工程師 Anthony Green 等人就開發了 libffi(home, wiki, github, doc),解決的正是這個問題。目前,libffi 幾乎支持所有常見的 CPU 架構。因而,從 JNA 3.0 開始,摒棄了原先手動構造棧幀的作法,把 libffi 集成進了 JNA。
直接映射(Direct Mapping)
https://docs.oracle.com/javas...
http://www.chiark.greenend.or...
JNR 底層一樣也是依賴 libffi,參見 jffi。但 JNR 相比 JNA 性能更好,作了頗有優化。比較重要的點是,JNA 使用動態代理生成實現類,而 JNR 使用 ASM 字節碼操做庫生成直接實現類,去除了每次調用本地方法時額外的動態代理的邏輯。使用 ASM 生成實現類,對應的代碼爲 AsmLibraryLoader.java。其餘細節,限於文檔不全,本人精力有限,再也不展開。
Java 9 之前 JDK 獲取進程 id 沒有簡潔的方法,最新發布的 Java 9 中的 JEP 102(Process API Updates)加強了進程 API。進程 id 可使用如下方式 [javadoc ]
long pid = ProcessHandle.current().pid();
翻閱實現源碼,能夠看到對應的實現就是 JNI 調用:
jdk/src/java.base/share/classes/java/lang/ProcessHandleImpl [src ]
/** * Return the pid of the current process. * * @return the pid of the current process */ private static native long getCurrentPid0();
*nix 平臺下實現爲:
jdk/src/java.base/unix/native/libjava/ProcessHandleImpl_unix.c [src ]
/* * Class: java_lang_ProcessHandleImpl * Method: getCurrentPid0 * Signature: ()J */ JNIEXPORT jlong JNICALL Java_java_lang_ProcessHandleImpl_getCurrentPid0(JNIEnv *env, jclass clazz) { pid_t pid = getpid(); return (jlong) pid; }
Windows 平臺下實現爲:
jdk/src/java.base/windows/native/libjava/ProcessHandleImpl_win.c [src ]
/* * Returns the pid of the caller. * * Class: java_lang_ProcessHandleImpl * Method: getCurrentPid0 * Signature: ()J */ JNIEXPORT jlong JNICALL Java_java_lang_ProcessHandleImpl_getCurrentPid0(JNIEnv *env, jclass clazz) { DWORD pid = GetCurrentProcessId(); return (jlong)pid; }