Java离Linux内核有多远
这篇文章主要介绍了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 |
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
工程师了(哈哈手动狗头)。
从 launcher 说起
世界上最远的距离,是咱俩坐隔壁,我在看底层协议,而你在研究 spring……如果想拉近咱俩的距离,先下载 openjdk
源码(openjdk),然后下载 glibc
(glibc),再下载内核源码
(kernel)。
Java
程序到 JVM
,这个大家肯定比我熟悉,就不班门弄斧了。
我们就从 JVM
的入口为例,分析 JVM
到内核的流程,入口就是 main
函数了(java.base/share/native/launcher/main.c):
JNIEXPORT intmain(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; 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); (*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
的呢,大致如下:
//来源: glibcconst 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
函数了,即使它是平台相关的,而且还是由汇编语言写的。
ENTRY (__clone)movl $-EINVAL,%eaxmovl FUNC(%esp),%ecx testl %ecx,%ecxjz SYSCALL_ERROR_LABELmovl STACK(%esp),%ecx //1testl %ecx,%ecxjz SYSCALL_ERROR_LABELandl $0xfffffff0, %ecx //2subl $28,%ecxmovl ARG(%esp),%eax movl %eax,12(%ecx)movl FUNC(%esp),%eaxmovl %eax,8(%ecx)movl $0,4(%ecx)pushl %ebx //3pushl %esipushl %edimovl TLS+12(%esp),%esi //4movl PTID+12(%esp),%edxmovl FLAGS+12(%esp),%ebxmovl CTID+12(%esp),%edimovl $SYS_ify(clone),%eaxmovl %ebx, (%ecx) //5int $0x80 //6popl %edi //7popl %esipopl %ebxtest %eax,%eax //8jl SYSCALL_ERROR_LABELjz L(thread_start)ret //9L(thread_start): //10movl %esi,%ebp testl $CLONE_VM, %edije 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内核有多远”知识都有一定的了解,大家如果还想学习更多知识,欢迎关注编程网行业资讯频道。
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341