中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java離Linux內核有多遠

發布時間:2022-01-26 15:30:52 來源:億速云 閱讀:126 作者:iii 欄目:開發技術

這篇文章主要介紹了Java離Linux內核有多遠的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Java離Linux內核有多遠文章都會有所收獲,下面我們一起來看看吧。

Java 離內核有多遠?

測試環境版本信息:

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
JavaOpenjdk jdk14

玩內核的人怎么也懂 Java?這主要得益于我學校的 Java 課程和畢業那會在華為做 Android 手機的經歷,幾個模塊從 APP/Framework/Service/HAL/Driver 掃過一遍,自然對 Java 有所了解。

每次提起 Java,我都會想到一段有趣的經歷。剛畢業到部門報到第一個星期,部門領導(在華為算是 Manager)安排我們熟悉 Android。我花了幾天寫了個 Android 游戲,有些類似連連看那種。開周會的時候,領導看到我的演示后,一臉不悅,質疑我的直接領導(在華為叫 PL,Project Leader)沒有給我們講明白部門的方向。

emm,我當時確實沒明白所謂的熟悉 Android 是該干啥,后來 PL 說,是要熟悉 xxx 模塊,APP 只是其中一部分。話說如果當時得到的是肯定,也許我現在就是一枚 Java 工程師了(哈哈手動狗頭)。

從 launcher 說起

世界上最遠的距離,是咱倆坐隔壁,我在看底層協議,而你在研究 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.sowindow 平臺可能就是 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;
}

dlopendlsym 涉及動態鏈接,簡單理解就是 libjvm.so 包含 JNI_CreateJavaVMJNI_GetDefaultJavaVMInitArgsJNI_GetCreatedJavaVMs 的定義,動態鏈接完成后,ifn->CreateJavaVMifn->GetDefaultJavaVMInitArgsifn->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 初始化 JVMload 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 實現的。此處就可以進入內核了,但是我們還是先繼續看看 JVMThreadJavaMain 直接調用了 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 初始化 JVMInitializeJVM 會調用 ifn->CreateJavaVM,也就是libjvm.so 中的 JNI_CreateJavaVM

第 2 步,LoadMainClass,最終調用的是 JVM_FindClassFromBootLoader,也是通過動態鏈接找到函數(定義在 hotspot/share/prims/ 下),然后調用它。

第 3 和第 4 步,Java 的同學應該知道,這就是調用 main 函數。

有點跑題了……我們繼續以 pthread_create 為例看看內核吧。

其實,pthread_create 離內核還有一小段距離,就是 glibcnptl/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_forkclone_flags 參數是固定的,所以它只能用來創建進程,內核提供了另一個系統調用 cloneclone 最終也調用 _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_VMclone,也存在這個隱患嗎?答案是沒有,因為新線程指定了自己的用戶棧,由 stackaddr 指定。copy_thread 函數的 sp參數就是 stackaddrchildregs->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_flagsesi 等于 0,ebx 等于&start_thread

  • 系統調用的結果由 eax 返回,第 8 步判斷 clone 系統調用的結果,對當前進程而言,clone 系統調用如果成功返回的是新線程在它的 pid namespace 中的 id,大于 0,所以它執行 ret 退出__clone 函數。對新線程而言,clone 系統調用的返回值等于 0,所以它執行L(thread_start) 處的代碼。clone_flagsCLONE_VM 標志被置位的情況下,會執行 call *%ebxebx 等于&start_thread,至此 start_thread 得到了執行,它又調用了提供給pthread_createstart_routine,結束。

如此看來,Java JVM glibc 內核,好像也沒有多遠。

關于“Java離Linux內核有多遠”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“Java離Linux內核有多遠”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

寻甸| 义马市| 凤山市| 长岛县| 利川市| 余江县| 柳河县| 合阳县| 桂林市| 金山区| 施甸县| 门头沟区| 新田县| 沽源县| 老河口市| 旬阳县| 广西| 呼和浩特市| 杭锦旗| 井冈山市| 治县。| 海丰县| 辉县市| 田林县| 西林县| 宁乡县| 嘉荫县| 南安市| 固阳县| 台北县| 南川市| 乌拉特中旗| 格尔木市| 紫金县| 丰台区| 新化县| 二连浩特市| 清徐县| 新晃| 长阳| 昌乐县|