回调函数执行过程分析

Posted 开宝特攻

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了回调函数执行过程分析相关的知识,希望对你有一定的参考价值。

回调函数执行过程分析

​ 学号:SA*****200

回调的过程

​ 将函数作为参数,传入某一外部函数,以便该外部函数可以调用此函数来完成某些任务,这样的函数就称为回调函数(CallBack,call-then-back)

​ 上图是一个示例,应用程序在调用某一库函数时,通过将函数地址作为参数传递的方式,指定了一个回调函数。库函数执行过程中,会调用这一函数,来完成应用程序的请求。一般回调函数都是和应用程序处于同一抽象层,这样就形成了一个:高层调用底层,底层再反过来调用高层的过程。

​ 事实上,回调不仅可以用于在应用程序和库函数之间,把库函数换成别的中间函数也行。

回调的优势

​ 回调函数为应用提供了很大的灵活性、可拓展性。

​ 一方面,同一个接口(即中间函数),根据起始函数传入的参数(即回调函数地址)不同,可以表现出不同的行为,这体现了多态的思想。

​ 另一方面,采用这种方法,也满足了开闭原则,即你可以在不修改中间函数源代码的情况下,仅通过修改或者添加回调函数,便可以变更接口的行为。

一个回调函数执行的例子

​ 下面是一个回调函数执行的例子(meun.c 中执行过程与此相仿)

​ 现有一个接口 getOddNum,可以给定任意整数,它返回一个奇数。至于得到奇数的方式,则由用户自定义,比如是采用 2K+1 还是 4k+1 的形式,由传入的回调函数确定。

​ main 函数以 doubleNum 为参数调用 getOddNum 时,会把其地址传入 rsi 寄存器:movl $_Z9doubleNumi, %esi,然后就调用接口, call _Z9getOddNumiPFiiE,开始接口的处理过程了。

​ 在 getOddNum 函数中,分配栈帧后,会 call *%rsi,也即调用 rsi 寄存器中的地址指向的函数,而 rsi 中存放的,正是在 main 函数中放入的 doubleNum 的入口地址。可见,确实是在 getOddNum 函数中调用的回调函数。

​ 进入 doubleNum 函数后,它会把 rdi 寄存器中的参数加上自身后返回,返回到 getOddNum 函数中,然后 getOddNum 函数对 rax 寄存器中的返回值加一,把 rsp 指向 main 函数的栈顶,然后返回。此时,main 函数便完成了一次对接口 getOddNum 的调用,传入3,得到了奇数 7。

​ 想以 4k+1 的方式得到奇数,也不需要改动接口 getOddNum 的代码,只需要添加一个回调函数 fourTimesNum 即可,调用的方式也不变,简单满足了开闭原则。

jvmti agent的加载与回调函数的执行分析

参考技术A        JVM agent 的加载流程基本都一致,最近在学习java debug ,本文就结合jvmti impl的实现 jdwp agent 分析一下agent 的加载与回调函数的执行流程。

      Agent 是在 Java 虚拟机启动之时加载的,这个加载处于虚拟机初始化的早期,在这个时间点上:

      在这个函数中,虚拟机传入了一个 JavaVM 指针,以及命令行的参数。通过 JavaVM,我们可以获得 JVMTI 的指针,并获得 JVMTI 函数的使用能力,所有的 JVMTI 函数都通过这个 jvmtiEnv 获取,不同的虚拟机实现提供的函数细节可能不一样,但是使用的方式是统一的。

      这里传入的版本信息参数很重要,不同的 JVMTI 环境所提供的功能以及处理方式都可能有所不同,不过它在同一个虚拟机中会保持不变。命令行参数事实上就是上面启动命令行中的 options 部分,在 Agent 实现中需要进行解析并完成后续处理工作。参数传入的字符串仅仅在 Agent_OnLoad 函数里有效。
      需要强调的是,这个时候由于虚拟机并未完成初始化工作,并不是所有的 JVMTI 函数都可以被使用。
       Agent 还可以在运行时加载。具体说来,虚拟机会在运行时监听并接受 Agent 的加载,在这个时候,它会使用 Agent 的:

      同样的在这个初始化阶段,不是所有的 JVMTI 的 Capability 参数都处于可操作状态,而且 options 这个 char 数组在这个函数运行之后就会被丢弃,如果需要,需要做好保留工作。
      Agent 的主要功能是通过一系列的在虚拟机上设置的回调(callback)函数完成的,一旦某些事件发生,Agent 所设置的回调函数就会被调用,来完成特定的需求。

      最后,Agent 完成任务,或者虚拟机关闭的时候,虚拟机都会调用一个类似于类析构函数的方法来完成最后的清理任务,注意这个函数和虚拟机自己的 VM_DEATH 事件是不同的。

LoadJavaVM -> JNI_CreateJavaVM -> Threads::create_vm

2、create_vm_init_agents() 函数主要功能是找到dll, 然后找到dll中的Agent_Onload方法,然后调用这个方法进行agent初始化

核心函数是lookup_agent_on_load。 (定义在hotspot/src/share/vm/runtime/thread.cpp)

里面的核心函数是 lookup_on_load 。主要是找到dll,然后找到里面的agent onload 方法。 定义如下:

Agent_OnLoad是一个外部的c函数,看一下。(定义位置: jdk7u/jdk/src/share/instrument/InvocationAdapter.c)

在方法Agent_OnLoad中创建一个新的 JPLISAgent(Java Programming Language Instrumentation Services Agent),初始化了类和包里的配置文件,并且同时从Vm环境中获取了 jvmtiEnv 的环境。

看一下核心 createNewJPLISAgent :

再看一下初始化JPLISAgent的函数 initializeJPLISAgent

VMInit的初始化函数 eventHandlerVMInit : (定义位置: jdk7u/jdk/src/share/instrument/InvocationAdapter.c)。callback support 分了两个阶段(主要在processJavaStart中做了):
(1)在OnLoad时, 安装VMInit handler
(2)当VMInit handler 跑起来时,移除VMInit handler 安装一个ClassFileLoadHook handler(里面有我们定义的具体执行的回调函数).

processJavaStart 里面执行了注册event的处理钩子。

setLivePhaseEventHandlers :

startJavaAgent

1、 JVMTI 和 Agent 实现

以上是关于回调函数执行过程分析的主要内容,如果未能解决你的问题,请参考以下文章

回调函数

回调函数

js中的回调函数

普通函数与回调函数的区别

js中的回调函数

回调函数详解