Java 离 Linux 内核有多远?

发表于:2020-8-21 09:53

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:后端程序员面试汪    来源:今日头条

#
Java
  本期内容我们将站在非内核开发者的角度,给大家介绍应用和系统工程师如何梳理Linux内核代码。
  Java离内核有多远?
  测试环境版本信息:
  玩内核的人怎么也懂Java?这主要得益于我学校的Java课程和毕业那会在华为Android手机的经历,几个模块从APP/Framework/Service/HAL/Driver扫过一遍,自然对Java有所了解。
  每次提起Java,我都会想到一段有趣的经历。刚毕业到部门报到第一个星期,部门领导(在华为算是Manager)安排我们熟悉Android。我花了几天写了个Android游戏,有些类似连连看那种。开周会的时候,领导看到我的演示后,一脸不悦,质疑我的直接领导(在华为叫PL,ProjectLeader)没有给我们讲明白部门的方向。
  emm,我当时确实没明白所谓的熟悉Android是该干啥,后来PL说,是要熟悉xxx模块,APP只是其中一部分。话说如果当时得到的是肯定,也许我现在就是一枚Java工程师了。
  从launcher说起
  世界上最远的距离,是咱俩坐隔壁,我在看底层协议,而你在研究spring……如果想拉近咱俩的距离,先下载openjdk源码,然后下载glibc,再下载内核源码。
  Java程序到JVM,这个大家肯定比我熟悉,就不班门弄斧了。
  我们就从JVM的入口为例,分析JVM到内核的流程,入口就是main函数了(java.base/share/native/launcher/main.c):
  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的对应字段:
  dlopen和dlsym涉及动态链接,简单理解就是libjvm.so包含JNI_CreateJavaVM、JNI_GetDefaultJavaVMInitArgs和JNI_GetCreatedJavaVMs的定义,动态链接完成后,ifn->CreateJavaVM、ifn->GetDefaultJavaVMInitArgs和ifn->GetCreatedJavaVMs就是这些函数的地址。
  不妨确认下libjvm.so有这三个函数。
  openjdk源码里有这些实现的(hotspot/share/prims/下),有兴趣的同学可以继续钻研。
  最后,调用JVMInit初始化JVM,loadJava程序。
  JVMInit调用ContinueInNewThread,后者调用CallJavaMainInNewThread。插一句,我是真的不喜欢按照函数调用的方式讲述问题,a调用b,b又调用c,简直是在浪费篇幅,但是有些地方跨度太大又怕引起误会(尤其对初学者而言)。相信我,注水,是真没有,我不需要经验+3哈哈。
  CallJavaMainInNewThread的主要逻辑如下:
  看到pthread_create了吧,破案了,Java的线程就是通过pthread实现的。此处就可以进入内核了,但是我们还是先继续看看JVM。ThreadJavaMain直接调用了JavaMain,所以这里的逻辑就是,如果创建线程成功,就由新线程执行JavaMain,否则就知道在当前进程执行JavaMain。
  JavaMain是我们关注的重点,核心逻辑如下:
  第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的不同。
  以下关于线程的讨论从书里摘抄过来。
  各个标志的说明如下表:
  与当前进程共享VM、共享文件系统信息、共享打开的文件……看到这些我们就懂了,所谓的线程是这么回事。
  Linux实际上并没有从本质上将进程和线程分开,线程又被称为轻量级进程(LowWeightProcess,LWP),区别就在于线程与创建它的进程(线程)共享内存、文件等资源。
  完整的段落如下(双引号扩起来的几个段落),有兴趣的同学可以详细阅读:
  “fork传递至_do_fork的clone_flags参数是固定的,所以它只能用来创建进程,内核提供了另一个系统调用clone,clone最终也调用_do_fork实现,与fork不同的是用户可以根据需要确定clone_flags,我们可以使用它创建线程,如下(不同平台下clone的参数可能不同):
  Linux将线程当作轻量级进程,但线程的特性并不是由Linux随意决定的,应该尽量与其他操作系统兼容,为此它遵循POSIX标准对线程的要求。所以,要创建线程,传递给clone系统调用的参数也应该是基本固定的。
  创建线程的参数比较复杂,庆幸的是pthread(POSIXthread)为我们提供了函数,调用pthread_create即可,函数原型(用户空间)如下。
  第一个参数thread是一个输出参数,线程创建成功后,线程的id存入其中,第二个参数用来定制新线程的属性。新线程创建成功会执行start_routine指向的函数,传递至该函数的参数就是arg。
  pthread_create究竟如何调用clone的呢,大致如下:
  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函数中。
  为了彻底明白新进程是如何使用它的用户栈和start_thread的调用过程,有必要分析__clone函数了,即使它是平台相关的,而且还是由汇编语言写的。
  以__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系统调用如果成功返回的是新线程在它的pidnamespace中的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→内核,好像也没有多远。

  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系博为峰小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号