这篇文章主要介绍了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进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。