您好,登錄后才能下訂單哦!
這篇文章主要介紹了Java離Linux內核有多遠的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Java離Linux內核有多遠文章都會有所收獲,下面我們一起來看看吧。
測試環境版本信息:
Ubuntu(lsb_release -a) | Distributor ID: UbuntuDescription: Ubuntu 19.10Release: 19.10 |
---|---|
Linux(uname -a) | Linux yahua 5.5.5 #1 SMP … x86_64 x86_64 x86_64 GNU/Linux |
Java | Openjdk jdk14 |
玩內核的人怎么也懂 Java
?這主要得益于我學校的 Java
課程和畢業那會在華為做 Android
手機的經歷,幾個模塊從 APP/Framework/Service/HAL/Driver
掃過一遍,自然對 Java
有所了解。
每次提起 Java
,我都會想到一段有趣的經歷。剛畢業到部門報到第一個星期,部門領導(在華為算是 Manager)安排我們熟悉 Android
。我花了幾天寫了個 Android
游戲,有些類似連連看那種。開周會的時候,領導看到我的演示后,一臉不悅,質疑我的直接領導(在華為叫 PL,Project Leader)沒有給我們講明白部門的方向。
emm,我當時確實沒明白所謂的熟悉 Android
是該干啥,后來 PL 說,是要熟悉 xxx 模塊,APP 只是其中一部分。話說如果當時得到的是肯定,也許我現在就是一枚 Java
工程師了(哈哈手動狗頭)。
世界上最遠的距離,是咱倆坐隔壁,我在看底層協議,而你在研究 spring……如果想拉近咱倆的距離,先下載 openjdk
源碼(openjdk),然后下載 glibc
(glibc),再下載內核源碼
(kernel)。
Java
程序到 JVM
,這個大家肯定比我熟悉,就不班門弄斧了。
我們就從 JVM
的入口為例,分析 JVM
到內核的流程,入口就是 main
函數了(java.base/share/native/launcher/main.c):
JNIEXPORT int main(int argc, char **argv) { //中間省略一萬行參數處理代碼 return JLI_Launch(margc, margv, jargc, (const char**) jargv, 0, NULL, VERSION_STRING, DOT_VERSION, (const_progname != NULL) ? const_progname : *margv, (const_launcher != NULL) ? const_launcher : *margv, jargc > 0, const_cpwildcard, const_javaw, 0); }
JLI_Launch
做了三件我們關心的事。
首先,調用 CreateExecutionEnvironment
查找設置環境變量,比如 JVM
的路徑(下面的變量 jvmpath
),以我的平臺為例,就是 /usr/lib/jvm/java-14-openjdk-amd64/lib/server/libjvm.so
,window
平臺可能就是 libjvm.dll
。
其次,調用 LoadJavaVM
加載 JVM
,就是 libjvm.so
文件,然后找到創建 JVM
的函數賦值給 InvocationFunctions
的對應字段:
jboolean LoadJavaVM(const char *jvmpath, InvocationFunctions *ifn) { void *libjvm; //省略出錯處理 libjvm = dlopen(jvmpath, RTLD_NOW + RTLD_GLOBAL); ifn->CreateJavaVM = (CreateJavaVM_t) dlsym(libjvm, "JNI_CreateJavaVM"); ifn->GetDefaultJavaVMInitArgs = (GetDefaultJavaVMInitArgs_t) dlsym(libjvm, "JNI_GetDefaultJavaVMInitArgs"); ifn->GetCreatedJavaVMs = (GetCreatedJavaVMs_t) dlsym(libjvm, "JNI_GetCreatedJavaVMs"); return JNI_TRUE; }
dlopen
和 dlsym
涉及動態鏈接,簡單理解就是 libjvm.so
包含 JNI_CreateJavaVM
、JNI_GetDefaultJavaVMInitArgs
和 JNI_GetCreatedJavaVMs
的定義,動態鏈接完成后,ifn->CreateJavaVM
、ifn->GetDefaultJavaVMInitArgs
和 ifn->GetCreatedJavaVMs
就是這些函數的地址。
不妨確認下 libjvm.so
有這三個函數。
objdump -D /usr/lib/jvm/java-14-openjdk-amd64/lib/server/libjvm.so | grep -E "CreateJavaVM|GetDefaultJavaVMInitArgs|GetCreatedJavaVMs" | grep ":$" 00000000008fa9d0 <JNI_GetDefaultJavaVMInitArgs@@SUNWprivate_1.1>: 00000000008faa20 <JNI_GetCreatedJavaVMs@@SUNWprivate_1.1>: 00000000009098e0 <JNI_CreateJavaVM@@SUNWprivate_1.1>:
openjdk
源碼里有這些實現的(hotspot/share/prims/下),有興趣的同學可以繼續鉆研。
最后,調用 JVMInit
初始化 JVM
,load Java
程序。
JVMInit
調用 ContinueInNewThread
,后者調用 CallJavaMainInNewThread
。插一句,我是真的不喜歡按照函數調用的方式講述問題,a 調用 b,b 又調用 c,簡直是在浪費篇幅,但是有些地方跨度太大又怕引起誤會(尤其對初學者而言)。相信我,注水,是真沒有,我不需要經驗+3 哈哈。
CallJavaMainInNewThread
的主要邏輯如下:
int CallJavaMainInNewThread(jlong stack_size, void* args) { int rslt; pthread_t tid; pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); if (stack_size > 0) { pthread_attr_setstacksize(&attr, stack_size); } pthread_attr_setguardsize(&attr, 0); // no pthread guard page on java threads if (pthread_create(&tid, &attr, ThreadJavaMain, args) == 0) { void* tmp; pthread_join(tid, &tmp); rslt = (int)(intptr_t)tmp; } else { rslt = JavaMain(args); } pthread_attr_destroy(&attr); return rslt; }
看到 pthread_create
了吧,破案了,Java
的線程就是通過 pthread
實現的。此處就可以進入內核了,但是我們還是先繼續看看 JVM
。ThreadJavaMain
直接調用了 JavaMain
,所以這里的邏輯就是,如果創建線程成功,就由新線程執行 JavaMain
,否則就知道在當前進程執行JavaMain
。
JavaMain
是我們關注的重點,核心邏輯如下:
int JavaMain(void* _args) { JavaMainArgs *args = (JavaMainArgs *)_args; int argc = args->argc; char **argv = args->argv; int mode = args->mode; char *what = args->what; InvocationFunctions ifn = args->ifn; JavaVM *vm = 0; JNIEnv *env = 0; jclass mainClass = NULL; jclass appClass = NULL; // actual application class being launched jmethodID mainID; jobjectArray mainArgs; int ret = 0; jlong start, end; /* Initialize the virtual machine */ if (!InitializeJVM(&vm, &env, &ifn)) { //1 JLI_ReportErrorMessage(JVM_ERROR1); exit(1); } mainClass = LoadMainClass(env, mode, what); //2 CHECK_EXCEPTION_NULL_LEAVE(mainClass); mainArgs = CreateApplicationArgs(env, argv, argc); CHECK_EXCEPTION_NULL_LEAVE(mainArgs); mainID = (*env)->GetStaticMethodID(env, mainClass, "main", "([Ljava/lang/String;)V"); //3 CHECK_EXCEPTION_NULL_LEAVE(mainID); /* Invoke main method. */ (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs); //4 ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1; LEAVE(); }
第 1 步,調用 InitializeJVM
初始化 JVM
。InitializeJVM
會調用 ifn->CreateJavaVM
,也就是libjvm.so
中的 JNI_CreateJavaVM
。
第 2 步,LoadMainClass
,最終調用的是 JVM_FindClassFromBootLoader
,也是通過動態鏈接找到函數(定義在 hotspot/share/prims/ 下),然后調用它。
第 3 和第 4 步,Java
的同學應該知道,這就是調用 main
函數。
有點跑題了……我們繼續以 pthread_create
為例看看內核吧。
其實,pthread_create
離內核還有一小段距離,就是 glibc
(nptl/pthread_create.c
)。創建線程最終是通過 clone
系統調用實現的,我們不關心 glibc
的細節(否則又跑偏了),就看看它跟直接 clone
的不同。
(推薦微課:Java微課)
以下關于線程的討論從書里摘抄過來。
const int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM | CLONE_SIGHAND | CLONE_THREAD | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | 0); __clone (&start_thread, stackaddr, clone_flags, pd, &pd->tid, tp, &pd->tid);
各個標志的說明如下表(這句話不是摘抄的。。。)。
標志 | 描述 |
---|---|
CLONE_VM | 與當前進程共享VM |
CLONE_FS | 共享文件系統信息 |
CLONE_FILES | 共享打開的文件 |
CLONE_PARENT | 與當前進程共有同樣的父進程 |
CLONE_THREAD | 與當前進程同屬一個線程組,也意味著創建的是線程 |
CLONE_SYSVSEM | 共享sem_undo_list |
…… | …… |
與當前進程共享 VM、共享文件系統信息、共享打開的文件……看到這些我們就懂了,所謂的線程是這么回事。
Linux
實際上并沒有從本質上將進程和線程分開,線程又被稱為輕量級進程(Low Weight Process, LWP),區別就在于線程與創建它的進程(線程)共享內存、文件等資源。
完整的段落如下(雙引號擴起來的幾個段落),有興趣的同學可以詳細閱讀:
“ fork
傳遞至 _do_fork
的 clone_flags
參數是固定的,所以它只能用來創建進程,內核提供了另一個系統調用 clone
,clone
最終也調用 _do_fork
實現,與 fork
不同的是用戶可以根據需要確定 clone_flags
,我們可以使用它創建線程,如下(不同平臺下 clone
的參數可能不同):
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp, int __user *, parent_tidptr, int, tls_val, int __user *, child_tidptr) { return _do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr); }
Linux
將線程當作輕量級進程,但線程的特性并不是由 Linux
隨意決定的,應該盡量與其他操作系統兼容,為此它遵循 POSIX
標準對線程的要求。所以,要創建線程,傳遞給 clone
系統調用的參數也應該是基本固定的。
創建線程的參數比較復雜,慶幸的是 pthread
(POSIX thread)為我們提供了函數,調用pthread_create
即可,函數原型(用戶空間)如下。
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
第一個參數 thread
是一個輸出參數,線程創建成功后,線程的 id
存入其中,第二個參數用來定制新線程的屬性。新線程創建成功會執行 start_routine
指向的函數,傳遞至該函數的參數就是arg
。
pthread_create
究竟如何調用 clone
的呢,大致如下:
//來源: glibc const int clone_flags = (CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SYSVSEM | CLONE_SIGHAND | CLONE_THREAD | CLONE_SETTLS | CLONE_PARENT_SETTID | CLONE_CHILD_CLEARTID | 0); __clone (&start_thread, stackaddr, clone_flags, pd, &pd->tid, tp, &pd->tid);
clone_flags
置位的標志較多,前幾個標志表示線程與當前進程(有可能也是線程)共享資源,CLONE_THREAD
意味著新線程和當前進程并不是父子關系。
clone
系統調用最終也通過 _do_fork
實現,所以它與創建進程的 fork
的區別僅限于因參數不同而導致的差異,有以下兩個疑問需要解釋。
首先,vfork
置位了 CLONE_VM
標志,導致新進程對局部變量的修改會影響當前進程。那么同樣置位了 CLONE_VM
的 clone
,也存在這個隱患嗎?答案是沒有,因為新線程指定了自己的用戶棧,由 stackaddr
指定。copy_thread
函數的 sp
參數就是 stackaddr
,childregs->sp = sp
修改了新線程的 pt_regs
,所以新線程在用戶空間執行的時候,使用的棧與當前進程的不同,不會造成干擾。那為什么 vfork
不這么做,請參考 vfork
的設計意圖。
其次,fork
返回了兩次,clone
也是一樣,但它們都是返回到系統調用后開始執行,pthread_create
如何讓新線程執行 start_routine
的?start_routine
是由 start_thread
函數間接執行的,所以我們只需要清楚 start_thread
是如何被調用的。start_thread
并沒有傳遞給 clone
系統調用,所以它的調用與內核無關,答案就在 __clone
函數中。
(推薦教程:Linux教程)
為了徹底明白新進程是如何使用它的用戶棧和 start_thread
的調用過程,有必要分析 __clone
函數了,即使它是平臺相關的,而且還是由匯編語言寫的。
/*i386*/ ENTRY (__clone) movl $-EINVAL,%eax movl FUNC(%esp),%ecx /* no NULL function pointers */ testl %ecx,%ecx jz SYSCALL_ERROR_LABEL movl STACK(%esp),%ecx /* no NULL stack pointers */ //1 testl %ecx,%ecx jz SYSCALL_ERROR_LABEL andl $0xfffffff0, %ecx /*對齊*/ //2 subl $28,%ecx movl ARG(%esp),%eax /* no negative argument counts */ movl %eax,12(%ecx) movl FUNC(%esp),%eax movl %eax,8(%ecx) movl $0,4(%ecx) pushl %ebx //3 pushl %esi pushl %edi movl TLS+12(%esp),%esi //4 movl PTID+12(%esp),%edx movl FLAGS+12(%esp),%ebx movl CTID+12(%esp),%edi movl $SYS_ify(clone),%eax movl %ebx, (%ecx) //5 int $0x80 //6 popl %edi //7 popl %esi popl %ebx test %eax,%eax //8 jl SYSCALL_ERROR_LABEL jz L(thread_start) ret //9 L(thread_start): //10 movl %esi,%ebp /* terminate the stack frame */ testl $CLONE_VM, %edi je L(newpid) L(haspid): call *%ebx /*…*/
以 __clone
(&start_thread, stackaddr, clone_flags, pd, &pd->tid, tp, &pd->tid) 為例,
FUNC(%esp)
對應 &start_thread
,
STACK(%esp)
對應 stackaddr
,
ARG(%esp)
對應 pd
(新進程傳遞給 start_thread
的參數)。
第 1 步,將新進程的棧 stackaddr
賦值給 ecx
,確保它的值不為 0。
第 2 步,將 pd
、&start_thread
和 0 存入新線程的棧,對當前進程的棧無影響。
第 3 步,將當前進程的三個寄存器的值入棧,esp
寄存器的值相應減12。
第 4 步,準備系統調用,其中將 FLAGS+12(%esp)
存入 ebx
,對應 clone_flags
,將clone
的系統調用號存入 eax。
第 5 步,將 clone_flags
存入新進程的棧中。
第 6 步,使用 int
指令發起系統調用,交給內核創建新線程。截止到此處,所有的代碼都是當前進程執行的,新線程并沒有執行。
從第 7 步開始的代碼,當前進程和新線程都會執行。對當前進程而言,程序將它第 3 步入棧的寄存器出棧。但對新線程而言,它是從內核的 ret_from_fork
執行的,切換到用戶態后,它的棧已經成為 stackaddr
了,所以它的 edi
等于 clone_flags
,esi
等于 0,ebx
等于&start_thread
。
系統調用的結果由 eax
返回,第 8 步判斷 clone
系統調用的結果,對當前進程而言,clone
系統調用如果成功返回的是新線程在它的 pid namespace
中的 id
,大于 0,所以它執行 ret
退出__clone
函數。對新線程而言,clone
系統調用的返回值等于 0,所以它執行L(thread_start)
處的代碼。clone_flags
的 CLONE_VM
標志被置位的情況下,會執行 call *%ebx
,ebx
等于&start_thread
,至此 start_thread
得到了執行,它又調用了提供給pthread_create
的 start_routine
,結束。”
如此看來,Java
→ JVM
→ glibc
→ 內核
,好像也沒有多遠。
關于“Java離Linux內核有多遠”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“Java離Linux內核有多遠”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。