我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Java离Linux内核有多远

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

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
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 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.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;        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 初始化 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 的呢,大致如下:

//来源: 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_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 函数了,即使它是平台相关的,而且还是由汇编语言写的。

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_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内核有多远”知识都有一定的了解,大家如果还想学习更多知识,欢迎关注编程网行业资讯频道。

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Java离Linux内核有多远

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Java离Linux内核有多远

这篇文章主要介绍了Java离Linux内核有多远的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇Java离Linux内核有多远文章都会有所收获,下面我们一起来看看吧。Java 离内核有多远?测试环境版本信息:Ub
2023-06-27

Linux内核如何实现多核模式

这篇文章主要介绍Linux内核如何实现多核模式,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!在微软Windows 7大行其道的今天,你是否还坚持应用Linux操作系统。如果你是Linux操作系统的老用户。 这里为你讲
2023-06-17

Linux内核 vs Windows内核有什么区别

这篇文章给大家分享的是有关Linux内核 vs Windows内核有什么区别的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。Windows 和 Linux 可以说是我们比较常见的两款操作系统的。Windows 基本
2023-06-15

linux有没有内核级线程

本文小编为大家详细介绍“linux有没有内核级线程”,内容详细,步骤清晰,细节处理妥当,希望这篇“linux有没有内核级线程”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。linux有内核级线程,linux支持内核
2023-07-04

linux内核有哪些作用

本文小编为大家详细介绍“linux内核有哪些作用”,内容详细,步骤清晰,细节处理妥当,希望这篇“linux内核有哪些作用”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。linux内核的作用:1、负责进程的创建和销毁
2023-06-29

linux内核有没有中断函数

这篇文章主要介绍“linux内核有没有中断函数”,在日常操作中,相信很多人在linux内核有没有中断函数问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”linux内核有没有中断函数”的疑惑有所帮助!接下来,请跟
2023-07-04

linux内核是否有main函数

本篇内容主要讲解“linux内核是否有main函数”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“linux内核是否有main函数”吧!linux内核有main函数;main函数是程序的入口,ma
2023-07-05

linux内核的作用有哪些

这篇文章主要介绍了linux内核的作用有哪些的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇linux内核的作用有哪些文章都会有所收获,下面我们一起来看看吧。linux内核的功能:1、管理进程,内核负责创建和销毁
2023-06-30

Linux内核的功能有哪些

这篇文章主要介绍“Linux内核的功能有哪些”,在日常操作中,相信很多人在Linux内核的功能有哪些问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Linux内核的功能有哪些”的疑惑有所帮助!接下来,请跟着小编
2023-06-15

Java+Linux内核源码之如何理解多线程之进程

这篇文章主要讲解了“Java+Linux内核源码之如何理解多线程之进程”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Java+Linux内核源码之如何理解多线程之进程”吧!Linux 内核如
2023-06-15

​Linux内核中的container_of有什么用

container_of是用于在数据结构中获取包含该数据结构的容器的指针的宏。在Linux内核中,container_of通常用于实现内核中的链表、队列或其他数据结构。通常情况下,内核中的数据结构会在其内部包含一个指向容器的指针。当我们需
2023-10-22

Linux内核里的DebugFS有什么用

本篇内容主要讲解“Linux内核里的DebugFS有什么用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Linux内核里的DebugFS有什么用”吧!DebugFS,顾名思义,是一种用于内核调试
2023-06-16

linux的内核有哪些子系统

这篇文章主要介绍“linux的内核有哪些子系统”,在日常操作中,相信很多人在linux的内核有哪些子系统问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”linux的内核有哪些子系统”的疑惑有所帮助!接下来,请跟
2023-06-21

Linux内核的新特性有哪些

这篇文章主要介绍“Linux内核的新特性有哪些”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Linux内核的新特性有哪些”文章能帮助大家解决问题。准备工作要更新内核,你先要确定自己使用的系统是32位
2023-06-28

linux有没有内核文件操作函数

这篇文章主要介绍“linux有没有内核文件操作函数”,在日常操作中,相信很多人在linux有没有内核文件操作函数问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”linux有没有内核文件操作函数”的疑惑有所帮助!
2023-03-02

linux内核和linux系统的区别有哪些

本篇内容主要讲解“linux内核和linux系统的区别有哪些”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“linux内核和linux系统的区别有哪些”吧!linux内核和linux系统的区别:1
2023-07-02

Linux内核移植的方法有哪些

这篇文章主要介绍“Linux内核移植的方法有哪些”,在日常操作中,相信很多人在Linux内核移植的方法有哪些问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Linux内核移植的方法有哪些”的疑惑有所帮助!接下来
2023-06-27

编程热搜

目录