摘要:我們知道,發(fā)起函數(shù)調(diào)用,需要構(gòu)造一個(gè)棧幀。構(gòu)造棧幀的具體實(shí)現(xiàn)細(xì)節(jié)的選擇,被稱為調(diào)用慣例。要想完成這個(gè)函數(shù)調(diào)用邏輯,就要運(yùn)行時(shí)構(gòu)造棧幀,生成參數(shù)壓棧和清理堆棧的工作。目前,幾乎支持全部常見的架構(gòu)。
原文:http://nullwy.me/2018/01/java...遇到的問題
如果覺得我的文章對你有用,請隨意贊賞
前段時(shí)間開發(fā)的時(shí)候,遇到一個(gè)問題,就是如何用 Java 實(shí)現(xiàn) chdir?網(wǎng)上搜索一番,發(fā)現(xiàn)了 JNR-POSIX 項(xiàng)目 [stackoverflow ]。俗話說,好記性不如爛筆頭?,F(xiàn)在將涉及到的相關(guān)知識點(diǎn)總結(jié)成筆記。
其實(shí)針對 Java 實(shí)現(xiàn) chdir 問題,官方 20 多年前就存在對應(yīng)的 bug,即 JDK-4045688 "Add chdir or equivalent notion of changing working directory"。這個(gè) bug 在 1997.04 創(chuàng)建,目前的狀態(tài)是 Won"t Fix(不予解決),理由大致是,若實(shí)現(xiàn)與操作系統(tǒng)一樣的進(jìn)程級別的 chdir,將影響 JVM 上的全部線程,這樣引入了可變(mutable)的全局狀態(tài),這與 Java 的安全性優(yōu)先原則沖突,現(xiàn)在添加全局可變的進(jìn)程狀態(tài),已經(jīng)太遲了,對不變性(immutability)的支持才是 Java 要實(shí)現(xiàn)的特性。
chdir 是平臺相關(guān)的操作系統(tǒng)接口,POSIX 下對應(yīng)的 API 為 int chdir(const char *path);,而 Windows 下對應(yīng)的 API 為 BOOL WINAPI SetCurrentDirectory(_In_ LPCTSTR lpPathName);,另外 Windows 下也可以使用 MSVCRT 中 API 的 int _chdir(const char *dirname);(MSVCRT 下內(nèi)部實(shí)現(xiàn)其實(shí)就是調(diào)用 SetCurrentDirectory [reactos ] )。
Java 設(shè)計(jì)理念是跨平臺,"write once, run anywhere"。很平臺相關(guān)的 API,雖然各個(gè)平臺都有自己的類似的實(shí)現(xiàn),但存在會差異。除了多數(shù)常見功能,Java 并沒有對全部操作系統(tǒng)接口提供完整支持,比如很多 POSIX API。除了 chdir,另外一個(gè)典型的例子是,在 Java 9 以前 JDK 獲取進(jìn)程 id 一直沒有簡潔的方法 [stackoverflow ],最新發(fā)布的 Java 9 中的 JEP 102(Process API Updates)才增強(qiáng)了進(jìn)程 API。獲取進(jìn)程 id 可以使用以下方式 [javadoc ]:
long pid = ProcessHandle.current().pid();
相比其他語言,Pyhon 和 Ruby,對操作系統(tǒng)相關(guān)的接口都有更多的原生支持。Pyhon 和 Ruby 實(shí)現(xiàn)的相關(guān) API 基本上都帶有 POSIX 風(fēng)格。比如上文提到,chdir 和 getpid,在 Pyhon 和 Ruby 下對應(yīng)的 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 對應(yīng)源碼為 posixmodule.c#L2611,Ruby 解釋器的 chdir 對應(yīng)源碼為 dir.c#L848 和 win32.c#L6741。
JNI 實(shí)現(xiàn) getpidJava 下要想實(shí)現(xiàn)本地方法調(diào)用,需要通過 JNI。關(guān)于 JNI 的介紹,可以參閱“Java核心技術(shù),卷II:高級特性,第9版2013”的“第12章 本地方法”,或者讀當(dāng)年 Sun 公司 JNI 設(shè)計(jì)者 Sheng Liang(梁勝)寫的“Java Native Interface: Programmer"s Guide and Specification”。本文只給出實(shí)現(xiàn) getpid 的一個(gè)簡單示例。
首先使用 Maven 創(chuàng)建一個(gè)簡單的腳手架:
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,內(nèi)容如下:
/* 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
現(xiàn)在有了頭文件聲明,但還沒有實(shí)現(xiàn),手動(dòng)敲入 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。
現(xiàn)在運(yùn)行 GetPidJni 類,就能正確獲取 pid:
$ java -Djava.library.path=`pwd` -cp "target/classes" com.test.GetPidJni
JNI 的問題是,膠水代碼(黏合 Java 和 C 庫的代碼)需要程序員手動(dòng)書寫,對不熟悉 C/C++ 的同學(xué)是很大的挑戰(zhàn)。
JNA 實(shí)現(xiàn) getpidJNA(Java Native Access, wiki, github, javadoc, mvn),提供了相對 JNI 更加簡潔的調(diào)用本地方法的方式。除了 Java 代碼外,不再需要額外的膠水代碼。這個(gè)項(xiàng)目最早可以追溯到 Sun 公司 JNI 設(shè)計(jì)者 Sheng Liang 在 1999 年 JavaOne 上的分享。2006 年 11月,Todd Fast (也來自 Sun 公司) 首次將 JNA 發(fā)布到 dev.java.net 上。Todd Fast 在發(fā)布時(shí)提到,自己在這個(gè)項(xiàng)目上已經(jīng)斷斷續(xù)續(xù)開發(fā)并完善了 6-7 年時(shí)間,項(xiàng)目剛剛在 JDK 5 上重構(gòu)和重設(shè)計(jì)過,還可能有很多缺陷或缺點(diǎn),希望其他人能瀏覽代碼并參與進(jìn)來。Timothy Wall 在 2007 年 2 月重啟了這項(xiàng)目,引入了很多重要功能,添加了 Linux 和 OSX 支持(原本只在 Win32 上測試過),加強(qiáng)了 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 實(shí)現(xiàn) getpid
最初,JRuby 的核心開發(fā)者 Charles Nutter 在實(shí)現(xiàn) Ruby 的 POSIX 集成時(shí)就使用了 JNA [ref ]。但過了一段時(shí)候后,開始開發(fā) 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) 預(yù)處理器的常量支持(通過 jnr-constants 解決);(2) 開箱即用的標(biāo)準(zhǔn) API(作者實(shí)現(xiàn)了 jnr-posix, jnr-x86asm, jnr-enxio, jnr-unixsocket);(3) C 回調(diào) callback 支持;(4) 性能(提升 8-10 倍)。
使用 JNR-FFI(github, mvn)實(shí)現(xiàn) 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)實(shí)現(xiàn) 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),測試結(jié)果如下:
# 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 的實(shí)現(xiàn)性能,其他三種方式,從大到小的性能百分比依次為:74.8% (JNR), 13.2% (JnaDirect), 10.6% (JNA)。在博主電腦上測試,JNR 相比 JNA 將近快了 6-7 倍(JNR 作者 Charles Nutter 針對 getpid 的測試結(jié)果是 JNR 比 JNA 快 8-10 倍 [twitter slides ])。
實(shí)現(xiàn)原理 JNA 源碼簡析先來看下 JNA,JNA 官方文檔 FunctionalDescription.md,對其實(shí)現(xiàn)原理有很好的闡述。這里將從源碼角度分析實(shí)現(xiàn)的核心邏輯。
回顧下代碼,我們現(xiàn)實(shí)定義了接口 LibC,然后通過 Native.loadLibrary("c", LibC.class) 獲取了接口實(shí)現(xiàn)。這一步是怎么做到的呢?翻下源碼 Native.java#L547 就知道,其實(shí)是通過動(dòng)態(tài)代理(dynamic proxy)實(shí)現(xiàn)的。使用動(dòng)態(tài)代理需要實(shí)現(xiàn) InvocationHandler 接口,這個(gè)接口的實(shí)現(xiàn)在 JNA 源碼中是類 com.sun.jna.Library.Handler。示例中的 LibC 接口定義的全部方法,將全部分派到 Handler 的 invoke 方法下。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
然后根據(jù)返回參數(shù)的不同,分派到 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 本地方法參數(shù):
參數(shù) Function function,記錄了 lib 信息、函數(shù)名稱、函數(shù)指針地址、調(diào)用慣例等元信息;
參數(shù) long fp,即函數(shù)指針地址,函數(shù)指針地址通過 Native#findSymbol()獲得(底層是 Linux API dlsym 或 Windows API GetProcAddress )。
參數(shù) int callFlags,即調(diào)用約定,對應(yīng) cdecl 或 stdcall。
參數(shù) int callFlags,即函數(shù)入?yún)?,若無參數(shù),args 大小為 0,若有多個(gè)參數(shù),原本的入?yún)⒈粡淖蟮接乙来伪4娴?args 數(shù)組中。
再來看下 invokeXxx 本地方法的實(shí)現(xiàn) dispatch.c#L2122(invokeInt 或 invokeLong 實(shí)現(xiàn)源碼類似):
/* * 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 本地方法統(tǒng)一被分派到 dispatch 函數(shù) dispatch.c#L439:
static void dispatch(JNIEnv *env, void* func, jint flags, jobjectArray args, ffi_type *return_type, void *presult)
這個(gè) dispatch 函數(shù)是全部邏輯的核心,實(shí)現(xiàn)最終的本地函數(shù)調(diào)用。
我們知道,發(fā)起函數(shù)調(diào)用,需要構(gòu)造一個(gè)棧幀(stack frame)。構(gòu)造棧幀,涉及到參數(shù)壓棧次序(參數(shù)從左到右壓入還是從右到左壓入)和清理?xiàng)ㄕ{(diào)用者清理還是被調(diào)用者清理)等實(shí)現(xiàn)細(xì)節(jié)問題。不同的編譯器在不同的 CPU 架構(gòu)下有不同的選擇。構(gòu)造棧幀的具體實(shí)現(xiàn)細(xì)節(jié)的選擇,被稱為調(diào)用慣例(calling convention)。按照調(diào)用慣例構(gòu)造整個(gè)棧幀,這個(gè)過程由編譯器在編譯階段完成的。比如要想發(fā)起 sum(2, 3) 這個(gè)函數(shù)調(diào)用,編譯器可能會生成如下等價(jià)匯編代碼:
; 調(diào)用者清理堆棧(caller clean-up),參數(shù)從右到左壓入棧 push 3 push 2 call _sum ; 將返回地址壓入棧, 同時(shí) sum 的地址裝入 eip add esp, 8 ; 清理堆棧, 兩個(gè)參數(shù)占用 8 字節(jié)
dispatch 函數(shù)是,需要調(diào)用的函數(shù)指針地址、輸入?yún)?shù)和返回參數(shù),全部是運(yùn)行時(shí)確定。要想完成這個(gè)函數(shù)調(diào)用邏輯,就要運(yùn)行時(shí)構(gòu)造棧幀,生成參數(shù)壓棧和清理堆棧的工作。JNA 3.0 之前,實(shí)現(xiàn)運(yùn)行時(shí)構(gòu)造棧幀的邏輯的對應(yīng)代碼 dispatch_i386.c、dispatch_ppc.c 和 dispatch_sparc.s,分別實(shí)現(xiàn) Intel x86、PowerPC 和 Sparc 三種 CPU 架構(gòu)。
運(yùn)行時(shí)函數(shù)調(diào)用,這個(gè)問題其實(shí)是一個(gè)一般性的通用問題。早在 1996 年 10 月,Cygnus Solutions 的工程師 Anthony Green 等人就開發(fā)了 libffi(home, wiki, github, doc),解決的正是這個(gè)問題。目前,libffi 幾乎支持全部常見的 CPU 架構(gòu)。于是,從 JNA 3.0 開始,摒棄了原先手動(dòng)構(gòu)造棧幀的做法,把 libffi 集成進(jìn)了 JNA。
直接映射(Direct Mapping)
https://docs.oracle.com/javas...
http://www.chiark.greenend.or...
JNR 底層同樣也是依賴 libffi,參見 jffi。但 JNR 相比 JNA 性能更好,做了很有優(yōu)化。比較重要的點(diǎn)是,JNA 使用動(dòng)態(tài)代理生成實(shí)現(xiàn)類,而 JNR 使用 ASM 字節(jié)碼操作庫生成直接實(shí)現(xiàn)類,去除了每次調(diào)用本地方法時(shí)額外的動(dòng)態(tài)代理的邏輯。使用 ASM 生成實(shí)現(xiàn)類,對應(yīng)的代碼為 AsmLibraryLoader.java。其他細(xì)節(jié),限于文檔不全,本人精力有限,不再展開。
Java 9 的 getpid 實(shí)現(xiàn)Java 9 以前 JDK 獲取進(jìn)程 id 沒有簡潔的方法,最新發(fā)布的 Java 9 中的 JEP 102(Process API Updates)增強(qiáng)了進(jìn)程 API。進(jìn)程 id 可以使用以下方式 [javadoc ]
long pid = ProcessHandle.current().pid();
翻閱實(shí)現(xiàn)源碼,可以看到對應(yīng)的實(shí)現(xiàn)就是 JNI 調(diào)用:
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 平臺下實(shí)現(xiàn)為:
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 平臺下實(shí)現(xiàn)為:
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核心技術(shù),卷II:高級特性,第9版2013:第12章 本地方法,豆瓣
Java Native Interface: Programmer"s Guide and Specification, Sheng Liang (wiki,linkedin,msa), 1999,豆瓣:作者梁勝,中國科技大學(xué)少年班83級,并擁有耶魯大學(xué)計(jì)算機(jī)博士學(xué)位(1990-1996),目前 Rancher Labs 創(chuàng)始人兼 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 外部函數(shù)接口 http://www.infoq.com/cn/news/...
2005-08 Brian Goetz:用動(dòng)態(tài)代理進(jìn)行修飾 https://www.ibm.com/developer...
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/71846.html
摘要:序在里頭如何獲取硬盤的序列號呢,這里涉及了跨平臺的問題,不同的操作系統(tǒng)的查看命令不一樣,那么里頭如何去適配呢。這里使用了這個(gè)項(xiàng)目來獲取。使用的是的方式而不是的形式來進(jìn)行本地調(diào)用的。獲取方法,,,和之間的區(qū)別是什么,它們的調(diào)用效率怎么排名 序 在java里頭如何獲取硬盤的序列號呢,這里涉及了跨平臺的問題,不同的操作系統(tǒng)的查看命令不一樣,那么java里頭如何去適配呢。這里使用了oshi這個(gè)...
摘要:提供了這個(gè)技術(shù)來實(shí)現(xiàn)調(diào)用和程序,但實(shí)現(xiàn)起來比較麻煩,所以后來公司在的基礎(chǔ)上實(shí)現(xiàn)了一個(gè)框架使用這個(gè)框架可以減輕程序員的負(fù)擔(dān),使得調(diào)用和容易很多。 使用JAVA語言開發(fā)程序比較高效,但有時(shí)對于一些性能要求高的系統(tǒng),核心功能可能是用C或者C++語言編寫的,這時(shí)需要用到JAVA的跨語言調(diào)用功能。JAVA提供了JNI這個(gè)技術(shù)來實(shí)現(xiàn)調(diào)用C和C++程序,但JNI實(shí)現(xiàn)起來比較麻煩,所以后來SUN公司在...
摘要:我們找到了許多有趣的工具和組件用來檢測狀態(tài)的各個(gè)方面,其中一個(gè)就是在運(yùn)行期通過反射了解內(nèi)部機(jī)制。由于包含多種的實(shí)現(xiàn),就是供具體實(shí)現(xiàn)比如必須繼承的抽象類。調(diào)試器框架是可擴(kuò)展的,這意味著可以通過繼承這個(gè)抽象類來使用另一個(gè)調(diào)試器。 在日常工作中,我們都習(xí)慣直接使用或者通過框架使用反射。在沒有反射相關(guān)硬編碼知識的情況下,這是Java和Scala編程中使用的類庫與我們的代碼之間進(jìn)行交互的一種主要...
摘要:目錄創(chuàng)建創(chuàng)建項(xiàng)目與工具項(xiàng)目與工具步驟與代碼步驟與代碼使用調(diào)用使用調(diào)用項(xiàng)目與工具項(xiàng)目與工具步驟與代碼步驟與代碼實(shí)際效果實(shí)際效果參考鏈接參考鏈接創(chuàng)建項(xiàng)目與工具步驟與代碼使用創(chuàng)建動(dòng)態(tài)鏈接庫項(xiàng)目設(shè)置項(xiàng)目名與項(xiàng)目 目錄 1 C++創(chuàng)建dll 1.1 項(xiàng)目與工具 1.2 步驟與代碼 2 Java使用JN...
摘要:與動(dòng)態(tài)鏈接庫配套的,會有相應(yīng)的頭文件,來聲明動(dòng)態(tài)鏈接庫中對外暴露的方法。結(jié)構(gòu)體映射結(jié)構(gòu)體映射類編寫類,繼承,表示這個(gè)一個(gè)結(jié)構(gòu)體。聲明字段與,并且設(shè)置訪問屬性為。計(jì)算機(jī)狀態(tài)結(jié)構(gòu)體結(jié)構(gòu)體指針結(jié)構(gòu)體具體的值至此,功能完成。 問題描述 虛擬化項(xiàng)目,需要用到Java調(diào)用原生代碼的技術(shù),我們使用的是開源庫JNA(Java Native Access)。 Native(C/C++)代碼,編譯生成動(dòng)態(tài)...
閱讀 1687·2021-09-26 10:00
閱讀 2945·2021-09-06 15:00
閱讀 3554·2021-09-04 16:40
閱讀 2326·2019-08-30 15:44
閱讀 731·2019-08-30 10:59
閱讀 1902·2019-08-29 18:34
閱讀 3630·2019-08-29 15:42
閱讀 2308·2019-08-29 15:36