本文中我們將站在非內(nèi)核開發(fā)者的角度,給大家介紹應(yīng)用和系統(tǒng)工程師如何梳理 Linux 內(nèi)核代碼。希望大家讀完之后能有所收獲,也希望更多的開發(fā)者能夠關(guān)注到內(nèi)核開發(fā)領(lǐng)域,畢竟連祖師爺 Linus
都表示內(nèi)核維護(hù)者要后繼無人了呀!
Java 離內(nèi)核有多遠(yuǎn)?
測試環(huán)境版本信息:
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 |
玩內(nèi)核的人怎么也懂 Java
?這主要得益于我學(xué)校的 Java
課程和畢業(yè)那會在華為做 Android
手機(jī)的經(jīng)歷,幾個模塊從 APP/Framework/Service/HAL/Driver
掃過一遍,自然對 Java
有所了解。
每次提起 Java
,我都會想到一段有趣的經(jīng)歷。剛畢業(yè)到部門報到第一個星期,部門領(lǐng)導(dǎo)(在華為算是 Manager)安排我們熟悉 Android
。我花了幾天寫了個 Android
游戲,有些類似連連看那種。開周會的時候,領(lǐng)導(dǎo)看到我的演示后,一臉不悅,質(zhì)疑我的直接領(lǐng)導(dǎo)(在華為叫 PL,Project Leader)沒有給我們講明白部門的方向。
emm,我當(dāng)時確實沒明白所謂的熟悉 Android
是該干啥,后來 PL 說,是要熟悉 xxx 模塊,APP 只是其中一部分。話說如果當(dāng)時得到的是肯定,也許我現(xiàn)在就是一枚 Java
工程師了(哈哈手動狗頭)。
(推薦教程:Java教程)
從 launcher 說起
世界上最遠(yuǎn)的距離,是咱倆坐隔壁,我在看底層協(xié)議,而你在研究 spring……如果想拉近咱倆的距離,先下載 openjdk
源碼(openjdk),然后下載 glibc
(glibc),再下載內(nèi)核源碼
(kernel)。
Java
程序到 JVM
,這個大家肯定比我熟悉,就不班門弄斧了。
我們就從 JVM
的入口為例,分析 JVM
到內(nèi)核的流程,入口就是 main
函數(shù)了(java.base/share/native/launcher/main.c):
JNIEXPORT int
main(int argc, char **argv)
{
//中間省略一萬行參數(shù)處理代碼
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
做了三件我們關(guān)心的事。
首先,調(diào)用 CreateExecutionEnvironment
查找設(shè)置環(huán)境變量,比如 JVM
的路徑(下面的變量 jvmpath
),以我的平臺為例,就是 /usr/lib/jvm/java-14-openjdk-amd64/lib/server/libjvm.so
,window
平臺可能就是 libjvm.dll
。
其次,調(diào)用 LoadJavaVM
加載 JVM
,就是 libjvm.so
文件,然后找到創(chuàng)建 JVM
的函數(shù)賦值給 InvocationFunctions
的對應(yīng)字段:
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
涉及動態(tài)鏈接,簡單理解就是 libjvm.so
包含 JNI_CreateJavaVM
、JNI_GetDefaultJavaVMInitArgs
和 JNI_GetCreatedJavaVMs
的定義,動態(tài)鏈接完成后,ifn->CreateJavaVM
、ifn->GetDefaultJavaVMInitArgs
和 ifn->GetCreatedJavaVMs
就是這些函數(shù)的地址。
不妨確認(rèn)下 libjvm.so
有這三個函數(shù)。
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
源碼里有這些實現(xiàn)的(hotspot/share/prims/下),有興趣的同學(xué)可以繼續(xù)鉆研。
最后,調(diào)用 JVMInit
初始化 JVM
,load Java
程序。
JVMInit
調(diào)用 ContinueInNewThread
,后者調(diào)用 CallJavaMainInNewThread
。插一句,我是真的不喜歡按照函數(shù)調(diào)用的方式講述問題,a 調(diào)用 b,b 又調(diào)用 c,簡直是在浪費(fèi)篇幅,但是有些地方跨度太大又怕引起誤會(尤其對初學(xué)者而言)。相信我,注水,是真沒有,我不需要經(jīng)驗+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
實現(xiàn)的。此處就可以進(jìn)入內(nèi)核了,但是我們還是先繼續(xù)看看 JVM
。ThreadJavaMain
直接調(diào)用了 JavaMain
,所以這里的邏輯就是,如果創(chuàng)建線程成功,就由新線程執(zhí)行 JavaMain
,否則就知道在當(dāng)前進(jìn)程執(zhí)行JavaMain
。
JavaMain
是我們關(guān)注的重點,核心邏輯如下:
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 步,調(diào)用 InitializeJVM
初始化 JVM
。InitializeJVM
會調(diào)用 ifn->CreateJavaVM
,也就是libjvm.so
中的 JNI_CreateJavaVM
。
第 2 步,LoadMainClass
,最終調(diào)用的是 JVM_FindClassFromBootLoader
,也是通過動態(tài)鏈接找到函數(shù)(定義在 hotspot/share/prims/ 下),然后調(diào)用它。
第 3 和第 4 步,Java
的同學(xué)應(yīng)該知道,這就是調(diào)用 main
函數(shù)。
有點跑題了……我們繼續(xù)以 pthread_create
為例看看內(nèi)核吧。
其實,pthread_create
離內(nèi)核還有一小段距離,就是 glibc
(nptl/pthread_create.c
)。創(chuàng)建線程最終是通過 clone
系統(tǒng)調(diào)用實現(xiàn)的,我們不關(guān)心 glibc
的細(xì)節(jié)(否則又跑偏了),就看看它跟直接 clone
的不同。
(推薦微課:Java微課)
以下關(guān)于線程的討論從書里摘抄過來。
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);
各個標(biāo)志的說明如下表(這句話不是摘抄的。。。)。
標(biāo)志 | 描述 |
---|---|
CLONE_VM | 與當(dāng)前進(jìn)程共享VM |
CLONE_FS | 共享文件系統(tǒng)信息 |
CLONE_FILES | 共享打開的文件 |
CLONE_PARENT | 與當(dāng)前進(jìn)程共有同樣的父進(jìn)程 |
CLONE_THREAD | 與當(dāng)前進(jìn)程同屬一個線程組,也意味著創(chuàng)建的是線程 |
CLONE_SYSVSEM | 共享sem_undo_list |
…… | …… |
與當(dāng)前進(jìn)程共享 VM、共享文件系統(tǒng)信息、共享打開的文件……看到這些我們就懂了,所謂的線程是這么回事。
Linux
實際上并沒有從本質(zhì)上將進(jìn)程和線程分開,線程又被稱為輕量級進(jìn)程(Low Weight Process, LWP),區(qū)別就在于線程與創(chuàng)建它的進(jìn)程(線程)共享內(nèi)存、文件等資源。
完整的段落如下(雙引號擴(kuò)起來的幾個段落),有興趣的同學(xué)可以詳細(xì)閱讀:
“ fork
傳遞至 _do_fork
的 clone_flags
參數(shù)是固定的,所以它只能用來創(chuàng)建進(jìn)程,內(nèi)核提供了另一個系統(tǒng)調(diào)用 clone
,clone
最終也調(diào)用 _do_fork
實現(xiàn),與 fork
不同的是用戶可以根據(jù)需要確定 clone_flags
,我們可以使用它創(chuàng)建線程,如下(不同平臺下 clone
的參數(shù)可能不同):
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
將線程當(dāng)作輕量級進(jìn)程,但線程的特性并不是由 Linux
隨意決定的,應(yīng)該盡量與其他操作系統(tǒng)兼容,為此它遵循 POSIX
標(biāo)準(zhǔn)對線程的要求。所以,要創(chuàng)建線程,傳遞給 clone
系統(tǒng)調(diào)用的參數(shù)也應(yīng)該是基本固定的。
創(chuàng)建線程的參數(shù)比較復(fù)雜,慶幸的是 pthread
(POSIX thread)為我們提供了函數(shù),調(diào)用pthread_create
即可,函數(shù)原型(用戶空間)如下。
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
第一個參數(shù) thread
是一個輸出參數(shù),線程創(chuàng)建成功后,線程的 id
存入其中,第二個參數(shù)用來定制新線程的屬性。新線程創(chuàng)建成功會執(zhí)行 start_routine
指向的函數(shù),傳遞至該函數(shù)的參數(shù)就是arg
。
pthread_create
究竟如何調(diào)用 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
置位的標(biāo)志較多,前幾個標(biāo)志表示線程與當(dāng)前進(jìn)程(有可能也是線程)共享資源,CLONE_THREAD
意味著新線程和當(dāng)前進(jìn)程并不是父子關(guān)系。
clone
系統(tǒng)調(diào)用最終也通過 _do_fork
實現(xiàn),所以它與創(chuàng)建進(jìn)程的 fork
的區(qū)別僅限于因參數(shù)不同而導(dǎo)致的差異,有以下兩個疑問需要解釋。
首先,vfork
置位了 CLONE_VM
標(biāo)志,導(dǎo)致新進(jìn)程對局部變量的修改會影響當(dāng)前進(jìn)程。那么同樣置位了 CLONE_VM
的 clone
,也存在這個隱患嗎?答案是沒有,因為新線程指定了自己的用戶棧,由 stackaddr
指定。copy_thread
函數(shù)的 sp
參數(shù)就是 stackaddr
,childregs->sp = sp
修改了新線程的 pt_regs
,所以新線程在用戶空間執(zhí)行的時候,使用的棧與當(dāng)前進(jìn)程的不同,不會造成干擾。那為什么 vfork
不這么做,請參考 vfork
的設(shè)計意圖。
其次,fork
返回了兩次,clone
也是一樣,但它們都是返回到系統(tǒng)調(diào)用后開始執(zhí)行,pthread_create
如何讓新線程執(zhí)行 start_routine
的?start_routine
是由 start_thread
函數(shù)間接執(zhí)行的,所以我們只需要清楚 start_thread
是如何被調(diào)用的。start_thread
并沒有傳遞給 clone
系統(tǒng)調(diào)用,所以它的調(diào)用與內(nèi)核無關(guān),答案就在 __clone
函數(shù)中。
(推薦教程:Linux教程)
為了徹底明白新進(jìn)程是如何使用它的用戶棧和 start_thread
的調(diào)用過程,有必要分析 __clone
函數(shù)了,即使它是平臺相關(guān)的,而且還是由匯編語言寫的。
/*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)
對應(yīng) &start_thread
,
STACK(%esp)
對應(yīng) stackaddr
,
ARG(%esp)
對應(yīng) pd
(新進(jìn)程傳遞給 start_thread
的參數(shù))。
- 第 1 步,將新進(jìn)程的棧
stackaddr
賦值給ecx
,確保它的值不為 0。 - 第 2 步,將
pd
、&start_thread
和 0 存入新線程的棧,對當(dāng)前進(jìn)程的棧無影響。 - 第 3 步,將當(dāng)前進(jìn)程的三個寄存器的值入棧,
esp
寄存器的值相應(yīng)減12。 - 第 4 步,準(zhǔn)備系統(tǒng)調(diào)用,其中將
FLAGS+12(%esp)
存入ebx
,對應(yīng)clone_flags
,將clone
的系統(tǒng)調(diào)用號存入 eax。 - 第 5 步,將
clone_flags
存入新進(jìn)程的棧中。 - 第 6 步,使用
int
指令發(fā)起系統(tǒng)調(diào)用,交給內(nèi)核創(chuàng)建新線程。截止到此處,所有的代碼都是當(dāng)前進(jìn)程執(zhí)行的,新線程并沒有執(zhí)行。 - 從第 7 步開始的代碼,當(dāng)前進(jìn)程和新線程都會執(zhí)行。對當(dāng)前進(jìn)程而言,程序?qū)⑺?3 步入棧的寄存器出棧。但對新線程而言,它是從內(nèi)核的
ret_from_fork
執(zhí)行的,切換到用戶態(tài)后,它的棧已經(jīng)成為stackaddr
了,所以它的edi
等于clone_flags
,esi
等于 0,ebx
等于&start_thread
。 - 系統(tǒng)調(diào)用的結(jié)果由
eax
返回,第 8 步判斷clone
系統(tǒng)調(diào)用的結(jié)果,對當(dāng)前進(jìn)程而言,clone
系統(tǒng)調(diào)用如果成功返回的是新線程在它的pid namespace
中的id
,大于 0,所以它執(zhí)行ret
退出__clone
函數(shù)。對新線程而言,clone
系統(tǒng)調(diào)用的返回值等于 0,所以它執(zhí)行L(thread_start)
處的代碼。clone_flags
的CLONE_VM
標(biāo)志被置位的情況下,會執(zhí)行call *%ebx
,ebx
等于&start_thread
,至此start_thread
得到了執(zhí)行,它又調(diào)用了提供給pthread_create
的start_routine
,結(jié)束。”
如此看來,Java
→ JVM
→ glibc
→ 內(nèi)核
,好像也沒有多遠(yuǎn)。
(推薦微課:Linux微課)
以上就是關(guān)于Java
跟Linux
內(nèi)核距離有多遠(yuǎn)的相關(guān)介紹了,希望對大家有所幫助。