摘要:我們知道,發起函數調用,需要構造一個棧幀。構造棧幀的具體實現細節的選擇,被稱為調用慣例。要想完成這個函數調用邏輯,就要運行時構造棧幀,生成參數壓棧和清理堆棧的工作。目前,幾乎支持全部常見的架構。
原文:http://nullwy.me/2018/01/java...遇到的問題
如果覺得我的文章對你有用,請隨意贊賞
前段時間開發的時候,遇到一個問題,就是如何用 Java 實現 chdir?網上搜索一番,發現了 JNR-POSIX 項目 [stackoverflow ]。俗話說,好記性不如爛筆頭?,F在將涉及到的相關知識點總結成筆記。
其實針對 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 要實現的特性。
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 ] )。
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 ]:
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。
JNI 實現 getpidJava 下要想實現本地方法調用,需要通過 JNI。關于 JNI 的介紹,可以參閱“Java核心技術,卷II:高級特性,第9版2013”的“第12章 本地方法”,或者讀當年 Sun 公司 JNI 設計者 Sheng Liang(梁勝)寫的“Java Native Interface: Programmer"s Guide and Specification”。本文只給出實現 getpid 的一個簡單示例。
首先使用 Maven 創建一個簡單的腳手架:
mvn archetype:generate -DgroupId=com.test -DartifactId=jni-jnr -DpackageName=com.test -DinteractiveMode=false
在 com.test 包下添加 GetPidJni 類:
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 頭文件:
$ 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,內容如下:
/* DO NOT EDIT THIS FILE - it is machine generated */ #include/* 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 實現 getpidJNA(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()); } }JNR 實現 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()); } }JMH 性能比較
性能測試代碼為 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,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 x86、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 的 getpid 實現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; }參考資料
Changing the current working directory in Java? https://stackoverflow.com/q/8...
How can a Java program get its own process ID? http://stackoverflow.com/q/35842
Java核心技術,卷II:高級特性,第9版2013:第12章 本地方法,豆瓣
Java Native Interface: Programmer"s Guide and Specification, Sheng Liang (wiki,linkedin,msa), 1999,豆瓣:作者梁勝,中國科技大學少年班83級,并擁有耶魯大學計算機博士學位(1990-1996),目前 Rancher Labs 創始人兼 CEO [ref ]
2013-07 Charles Nutter: Java Native Runtime http://www.oracle.com/technet...
JEP 191: Foreign Function Interface http://openjdk.java.net/jeps/191 作者是Charles Nutter
2014-03 Java 外部函數接口 http://www.infoq.com/cn/news/...
2005-08 Brian Goetz:用動態代理進行修飾 https://www.ibm.com/developer...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/71846.html
摘要:序在里頭如何獲取硬盤的序列號呢,這里涉及了跨平臺的問題,不同的操作系統的查看命令不一樣,那么里頭如何去適配呢。這里使用了這個項目來獲取。使用的是的方式而不是的形式來進行本地調用的。獲取方法,,,和之間的區別是什么,它們的調用效率怎么排名 序 在java里頭如何獲取硬盤的序列號呢,這里涉及了跨平臺的問題,不同的操作系統的查看命令不一樣,那么java里頭如何去適配呢。這里使用了oshi這個...
摘要:提供了這個技術來實現調用和程序,但實現起來比較麻煩,所以后來公司在的基礎上實現了一個框架使用這個框架可以減輕程序員的負擔,使得調用和容易很多。 使用JAVA語言開發程序比較高效,但有時對于一些性能要求高的系統,核心功能可能是用C或者C++語言編寫的,這時需要用到JAVA的跨語言調用功能。JAVA提供了JNI這個技術來實現調用C和C++程序,但JNI實現起來比較麻煩,所以后來SUN公司在...
摘要:我們找到了許多有趣的工具和組件用來檢測狀態的各個方面,其中一個就是在運行期通過反射了解內部機制。由于包含多種的實現,就是供具體實現比如必須繼承的抽象類。調試器框架是可擴展的,這意味著可以通過繼承這個抽象類來使用另一個調試器。 在日常工作中,我們都習慣直接使用或者通過框架使用反射。在沒有反射相關硬編碼知識的情況下,這是Java和Scala編程中使用的類庫與我們的代碼之間進行交互的一種主要...
摘要:目錄創建創建項目與工具項目與工具步驟與代碼步驟與代碼使用調用使用調用項目與工具項目與工具步驟與代碼步驟與代碼實際效果實際效果參考鏈接參考鏈接創建項目與工具步驟與代碼使用創建動態鏈接庫項目設置項目名與項目 目錄 1 C++創建dll 1.1 項目與工具 1.2 步驟與代碼 2 Java使用JN...
摘要:與動態鏈接庫配套的,會有相應的頭文件,來聲明動態鏈接庫中對外暴露的方法。結構體映射結構體映射類編寫類,繼承,表示這個一個結構體。聲明字段與,并且設置訪問屬性為。計算機狀態結構體結構體指針結構體具體的值至此,功能完成。 問題描述 虛擬化項目,需要用到Java調用原生代碼的技術,我們使用的是開源庫JNA(Java Native Access)。 Native(C/C++)代碼,編譯生成動態...
閱讀 1671·2021-09-26 10:00
閱讀 2935·2021-09-06 15:00
閱讀 3537·2021-09-04 16:40
閱讀 2297·2019-08-30 15:44
閱讀 714·2019-08-30 10:59
閱讀 1882·2019-08-29 18:34
閱讀 3615·2019-08-29 15:42
閱讀 2292·2019-08-29 15:36