当前位置 博文首页 > IT先森:Android Binder通信一次拷贝你真的理解了吗?

    IT先森:Android Binder通信一次拷贝你真的理解了吗?

    作者:[db:作者] 时间:2021-08-25 15:45

    ????Android Binder通信一次拷贝你真的理解了吗?


    Android Binder框架实现目录:

    Android Binder框架实现之Binder的设计思想
    Android Binder框架实现之何为匿名/实名Binder
    Android Binder框架实现之Binder中的数据结构
    Android Binder框架实现之Binder相关的接口和类
    Android Binder框架实现之Parcel详解之基本数据的读写
    Android Binder框架实现之Parcel read/writeStrongBinder实现
    Android Binder通信一次拷贝你真的理解了吗?
    Android Binder框架实现之servicemanager守护进程
    Android Binder框架实现之defaultServiceManager()的实现
    Android Binder框架实现之Native层addService详解之请求的发送
    Android Binder框架实现之Native层addService详解之请求的处理
    Android Binder框架实现之Native层addService详解之请求的反馈
    Android Binder框架实现之Binder服务的消息循环
    Android Binder框架实现之Native层getService详解之请求的发送
    Android Binder框架实现之Native层getService详解之请求的处理
    Android Binder框架实现之Native层getService详解之请求的反馈
    Android Binder框架实现之Binder Native Service的Java调用流程
    Android Binder框架实现之Java层Binder整体框架设计
    Android Binder框架实现之Framework层Binder服务注册过程源码分析
    Android Binder框架实现之Java层Binder服务跨进程调用源码分析
    Android Binder框架实现之Java层获取Binder服务源码分析



    引言

    最近有读者在询问一个关于Binder通信"一次拷贝"的问题,说在学习Binder驱动的实现中看到有多次调用了copy_from_user和copy_to_user来进行数据的跨用户空间和内核空间的拷贝,但是为啥Android官方和绝大部分的博客还是说只进行了一次拷贝呢!这就是本篇博客的由来!

    ??对于从事Android开发的coder来说Binder应该不会陌生了(如果对它概念都没有的话,那估计大概率是个假的开发者了)!按照现在比较流行的段位排序来说,青铜选手(初级开发者)肯定知道Binder是Android提供的可以进行跨进程的IPC通信机制,而白银选手(中级开发者)肯定应该知道Binder跨进程IPC通信原理是通过内存映射实现的,而这也是Binder相对于其他传统进程间通信方式的优点之一(即我们总说的Binder只需要做“一次拷贝”,而其他传统方式需要“两次拷贝”的核心了)!

    对于这里的排位没有歧视或者任何其它的意思啊(仅仅是为了文字的描述),闻道有先后术有专攻而已,而已啊!

    不对啊,王者排序还没有结束啊!是的,这不我们的Binder的进阶打怪不是也没有结束吗,所以不要着急!,对于想继续进阶的王者段位(高阶开发者来说)肯定会再进一步思考深入,一定会遇到如下几个绕不开的的关于Binder的问题:

    • 这所谓的“一次拷贝”的实现机理到底是什么(是Android捣鼓出来了黑科技还是其它)
    • 这所谓的“一次拷贝”到底是发生在什么地方?

    这个问题,对于有研究过Binder驱动源码的读者来说一定会有体会,因为在Binder驱动的源码中有多次copy_from_user和copy_to_user的调用,根本不止"一次拷贝",但是很多的书籍包括Android官方都多宣称只有一次拷贝,这是为什么呢,是它们错了,还是我们理解不到位呢!

    • 这所谓的"一次拷贝“的到底是什么东西?

    而很不幸的是绝大部分介绍Android的Binder的文章会重点强调“一次拷贝”是其优点之一,但对上面的几个问题要么一笔带过,要么就是回答的并不完全正确,从而给读者造成一些理解上的混乱(感觉被欺骗了的感觉)。所以本篇文章会从两个维度来阐述这个问题:

    • 第一个维度:对于青铜开发者,将带领读者了解为啥Binder跨进程IPC通信只需要"一次拷贝",而传统的IPC通信为啥需要两次拷贝(当然想深入吗,可以先从我上面的Android Binder框架实现目录开启,不是打广告!)
    • 第二个维度:对于想继续进阶的开发者,我将带领大伙解决上面列举的后两个疑问点(这个阶段就需要读者对Binder驱动的工作过程和Binder驱动源码有一个大致的了解,如果没有那就只能是解决第一阶段目标了),对这个所谓的“一次拷贝”来个庖丁解牛式的解读

    注意,本篇博客的源码是基于Android 7.xx版本来进行的,其中后续分析涉及的源码路径如下:

    --- kernel/drivers/staging/android/binder.c
    --- kernel/include/linux/list.h
    --- kernel/drivers/staging/android/uapi/binder.h
    --- external/kernel-headers/original/uapi/linux/android/binder.h
    --- framework/native/cmds/servicemanager/binder.c
    --- frameworks/native/cmds/servicemanager/service_manager.c
    --- frameworks/native/include/binder/Parcel.h
    --- frameworks/native/include/binder/IPCThreadState.h
    --- frameworks/native/libs/binder/IPCThreadState.cpp
    --- frameworks/native/include/binder/ProcessState.h
    --- frameworks/native/libs/binder/ProcessState.cpp
    



    一.前期知识准备

    注意,注意,注意重要的事情说三篇!
    这一大章节主要是针对青铜读者进行入门使用的,如果你已经是白银段位或者王者段位的,这个章节可以跳过,直接进入下一环节!

    ??Android的Binder是一个跨多种技术的集大成者(特别是Linux相关的技术),所以在回答分析今天的博客标题所提出来的问题之前,我们非常有必要了解科普一些相关的知识,特别是Linux中传统的跨进程IPC通信的(注意这里的措辞是科普和了解,因为想要深入这块的知识,不是本篇博客也不是一两篇博客能做到的,而且说实话读者本人也就了解个大概,了解)!


    1.1 Linux系统中进程模型

    ??我们知道Linux中的进程是被隔离不能直接进行通信的,而Android的应用程序作为特殊的Linxu进程也遵循了这一原则。所以这里我们要必要先了解一下Linux中的进程模型,然后扩散到涉及的一些基本概念,再然后逐步展开,让大家先行掌握传统LInux进程通信,然后发散到Android的Binder IPC进程通信!

    啥也不说了,翠花上酸菜!我们先看下Linux中进程模型图,如下:


    在这里插入图片描述


    上面的图示向我们传递了Linux进程模型中几个非常重要的概念(这个几个概念和后续的Linux跨进程通信息息相关):

    • 进程隔离
    • 用户空间(User space)
    • 内核空间(Kernel space)
    • 系统调用(System call)

    下面让我们对上述几个概念一一介绍,各个了解,走起约起!


    1.2 Linux进程隔离

    提到进程隔离让我莫名的想到了种群隔离,生殖隔离,跑题了啊言归正传!我们从如下几个维度来简单说说进程隔离:

    • 进程隔离的概念:进程隔离简单的说就是Linux操作系统设计的一种机制使进程之间不能共享数据,保持各自数据的独立性即A进程不能访问B进程数据,同理B进程也不能访问A进程数据
    • 进程隔离的实现原理:进程隔离的实现使用到了虚拟内存技术(这个后面简单说下)
    • 进程隔离的目的:当然是通过虚拟内存技术,达到Linux进程中数据不能共享,从而保持独立的功能

    关于进程隔离的基本概念吗,类比一下种群隔离,生殖隔离就很容易理解了。就是在通常情况下进程之间不能进行直接的数据交换,必须借助一定的手段,打破这种隔离实现世界的平等。

    正是由于上述提到的进程隔离,从而导致Linux进程之间要进行数据交互就得采用特殊的通信机制进程间IPC通信!


    1.3 Linux虚拟内存

    虚拟内存顾名思义就是一种实际上并不存在的内存,是虚拟出来的。它是Linux操作系统为了进行内存管理而设计的一种内存管理机制。总之就是虚拟内存是被虚拟出来的,是为了实现更好的内存管理而创建出来的最后它会通过一定的机制和物理内存映射起来的,并且这个映射的过程对应用程序来说是透明的!

    感觉有点整不下去了,因为这个涉及到操作系统的相关原理了。!读者如果有兴趣的请参阅博客Linux虚拟内存和物理内存的理解和Linux虚拟内存,网上有很多关于这方面的知识,这里我就先撤了。


    1.4 Linux进程空间(内核空间和用户空间)

    ??我们前面提到了虚拟内存的概念,而Linux进程空间就是通过虚拟内存来实现的。我们知道现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操心系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核,保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
    针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。


    在这里插入图片描述


    关于进程空间有如下几个点需要注意:
    1.内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟内存中的(当然肯定会在实际使用中映射到实际的物理内存上面去的)。
    2.为了保证系统的安全,用户空间和内核空间是天然隔离的
    3.内核空间是被所有的进程所共享的,这个从最上面的进程模型也可以看出来
    4.为啥要将进程空间划分为用户空间和内核空间呢,且之间有隔离,用户空间不能随意操作内核空间呢,这个最最主要的原因是为了安全方面考虑的,因为内核拥有对底层设备的所有访问权限,为了安全用户进程是不能直接访问内核进程的


    1.5 Linux进程空间系统调用

    ??虽然操作系统从逻辑上进行了用户空间和内核空间的划分,但不可避免的用户空间需要访问内核资源,比如文件操作、访问网络等等。为了突破隔离限制,就需要借助系统调用来实现。系统调用是用户空间访问内核空间的唯一方式,保证了所有的资源访问都是在内核的控制下进行的,避免了用户程序对系统资源的越权访问,提升了系统安全性和稳定性。

    而在实际的操作中所有的系统资源管理都是在内核空间中完成的。比如读写磁盘文件,分配回收内存,从网络接口读写数据等等。我们的应用程序是无法直接进行这样的操作的。但是我们可以通过内核提供的接口来完成这样的任务这就是所谓的系统调用了。比如应用程序要读取磁盘上的一个文件,它可以向内核发起一个 “系统调用” 告诉内核:“我要读取磁盘上的某某文件”。其实就是通过一个特殊的指令让进程从用户态进入到内核态(到了内核空间),在内核空间中,CPU 可以执行任何的指令,当然也包括从磁盘上读取数据。具体过程是先把数据读取到内核空间中,然后再把数据拷贝到用户空间并从内核态切换到用户态。此时应用程序已经从系统调用中返回并且拿到了想要的数据,可以开开心心的往下执行了。其切换的示意图如下:


    在这里插入图片描述


    这里简单说下什么是内核态与用户态:
    (1)当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。
    (2)当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。
    并且Linux使用两级保护机制:0级供系统内核使用,3级供用户程序使用


    1.6 Linux进程空间跨进程通信常用的系统调用函数

    ??到这里不容易啊,概念性的东西就是这么枯燥且乏味(当然深入了,那就是另外一说了啊)!而我们今天的博客重点两字就是"通信",这里我们先重点看下Linux进程空间是重点通过那几个函数实现用户空间和内核空间的数据交互的。

    1.6.1 用户空间向内核空间传递数据常用函数

    这里主要用到了两个函数,分别是copy_from_user()和get_user(),它们拷贝的数据流向如下:


    在这里插入图片描述



    1.6.2 用户空间向内核空间传递数据常用函数

    这里主要用到了两个函数,分别是copy_to_user()和put_user(),它们拷贝的数据流向如下:


    在这里插入图片描述


    这里关于上述四个函数(宏)就不一一列举出来了啊,可以详见博客copy_to_user 、copy_from_user函数实现内核空间数据与用户空间数据的相互访问。


    1.7 Linux内存映射概念

    前面一口气介绍了这么多的Linux相关概念,这还没有完,还有一个非常重要重要的知识点没有介绍完毕,那就是Linux的内存映射概念。这里读者可以先缓缓,先理解理解前面的概念,心里有个谱,然后继续征战!

    内存映射是系统调用函数mmap()的中文翻译,其本质是一种进程虚拟内存的映射方法,它可以将一个文件、一段物理内存或者其它对象(也包括内核空间)映射到进程的虚拟内存地址空间。实现这样的映射关系后,进程就可以采用指针的方式来读写操作这一段内存,进而完成对文件(或者其它被映射的对象)的操作,而不必再调用 read/write 等系统调用函数了。所以正是因为有如上的特点内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。当映射成功以后,两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。也正因为如此,内存映射能够提供对进程间通信的支持。其模型如下所示:


    在这里插入图片描述


    看到上述的描述可以减少拷贝次数,估计读者眼前一亮了!难不成Binder通信只拷贝依次的秘密武器就是它,是的Binder通信少拷贝一次的秘密武器就是它。这里我们先不带入Binder通信的概念,只重点了解一下内存映射的原理!

    关于这一块的具体底层原理读者可以想见博客Android-内存映射mmap和Linux操作系统原理—内存—mmap进程虚拟内存映射这两篇博客。


    1.8 Linux动态内核加载模块技术与Binder驱动简介

    1.8.1 Linux动态内核加载模块技术

    ??通过前面的理论知识的补充,我们知道了Linux进程空间中的用户空间和内核空间在绝大部分情况下是隔离的,它们之间的通信是可以通过系统调用来实现的。而传统的进程IPC通信模式都是如下接着内核来实现的譬如如管道,socket,消息队列等,而它们都已经作为Linux的内核一部分了,所以Linux是天然支持如上几种IPC通信模式的。

    但是我们的Android中引入的Binder通信概念中的Binder驱动并不是Linux系统标准内核的一部分,那怎么实现加载和使用呢?这就得益于Linux的动态内核可加载模块(Loadable Kernel Module,LKM)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。

    1.8.2 Binder驱动简介

    在介绍Binder驱动之前我们先了解一下驱动的基本概念,驱动是一种使用实现对硬件进行相关操作,并屏蔽硬件特性的一种软件技术!并且Linux将驱动分为三大类:字符设备驱动,块设备驱动以及网络设备驱动。
    这里对于驱动的概念就不做过多的介绍了,关于驱动的详细介绍可以参见博客Linux驱动简介及分类。

    好了上面我们对驱动有了一定的了解了,这里我们简单介绍一下Binder驱动,Binder驱动是一种虚拟的字符设备(重点它是虚拟出来的,实际上Binder驱动并没有操作相关的硬件),注册在/dev/binder中,如下所示:


    在这里插入图片描述


    其定义了一套Binder通信协议,负责建立进程间的Binder通信,提供了数据包在进程之间传递的一系列底层支持。当然Binder驱动是Android特有的,所以它不是Linux标准内核携带的必须通过动态内核加载进行加载的。既然Binder驱动属于内核层所以应用层对于它访问也是通过系统调用实现的。




    二.Linux传统IPC通信原理和Binder通信原理

    这个章节主要是从理论层面解释Linux传统IPC通信原理和Binder通信原理关于拷贝次数差异的原因所在,让读者从可以瞬间从青铜进阶成白银(如果这关读者已经打通,可以直接跳过进入下一章节了)。至于想变成最后的王者呗,那必须是待最后的分析了!

    有了前面知识的铺垫,是时候来点真家伙了!让我们一起来揭开Binder IPC通信所谓"一次拷贝"和其它IPC通信“两次拷贝“的真实面纱!


    2.1 Linux传统跨进程IPC通信原理

    Linux传统进程IPC通信由于Linux进程设计的原因,通常需要如下的步骤进行跨进程IPC通信(共享内存除外):

    • 对于消息的发送端进程:

      • 通常传统的IPC通信(Socket,管道,消息队列)首先将消息发送方将要发送的数据存放在内存缓存区中
      • 然后通过前面所说系统调用进入内核态。然后操作系统为Linux发送方进程在内核空间分配内存,开辟一块内核缓存区,调用 copy_from_user()函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中
    • 对于消息的接收端进程:

      • 首先在进行接收数据时在自己的用户空间开辟一块内存缓存区
      • 然后操作系统为Linux接收方进程在内核空间中调用 copy_to_user() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信

    Linux传统跨进程IPC通信的模型可以使用如下的示意图来完整表示,如下:


    在这里插入图片描述


    好吗,说到这里不说下Linux传统跨进程IPC通信模型的缺点好像过意不去,必须吐槽一下(注意,此处仅仅就IPC通信数据传输效率,以及资源占用角度出发):

    1.传统IPC通信模型传输效率比较低,拷贝次数过多需要两次(这个仅是对于Binder和匿名共享内存而言),第一次是从发送方用户空间拷贝到内核缓存区,第二次是从内核缓存区拷贝到接收方用户空间。

    2.接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用API接收消息头来获取消息体的大小,这两种做法不是浪费空间就是浪费时间。



    2.2 Binder跨进程IPC通信原理

    ??传统Linux进程的跨进程IPC通信原理介绍完毕,是时候来介绍Binder跨进程IPC通信原理了!前面我们知道了传统的IPC通信采用的是发送端Linux进程用户空间内存-区–>内核空间—>接收端Linux进程用户空间内存区的"两次拷贝"方式,那么Binder跨进程IPC通信是怎么做到了节约一次内存拷贝只需要一次的呢?

    细心的的读者肯定想到了博主在前面知识储备章节说到的内存映射mmap方式了,并且博主在介绍mmap的时候有强调了一个关键点就是内存映射不仅可以将文件映射到进程的用户空间(从而减少数据的拷贝次数,用内存读写取代I/O读写,提高文件读取效率),而且也可以将内核空间的一块区域映射到进程的用户空间。而我们这里的Binder跨进程IPC通信正是借助了内存映射的方法,在内核空间和接收方用户空间的数据缓存区之间做了一层内存映射。这样一来,从发送方用户空间拷贝到内核空间缓存区的数据,就相当于直接拷贝到了接收方用户空间的数据缓存区,从而减少了一次数据拷贝(在这里不得不说声设计的真的巧妙啊)。

    一次完整的 Binder IPC 通信过程通常如下所示:

    • 对于Binder服务端进程而言:
      • Binder服务端(Service)在启动之后,通过系统调用Binde驱动在内核空间创建一个数据接收缓存区(调用binder_oepn方法执行相关的操作)
      • 接着Binder服务端进程空间的内核接收到系统调用的指令,进而调用binder_mmap函数进行对应的处理。首先申请一块物理内存,然后建立Binder服务端(Service端)的用户空间和内核空间一块区域的映射关系(这样上述两块区域就映射在一起了)
    • 对于请求端(Client)进程而言:
      • Client向服务端发送通信发送请求,这个请求数据打包完毕之后通过系统调用先到驱动中,然后在驱动中通过copy_from_user()将数据从用户空间拷贝到内核空间的缓存区中(注意这块内核空间和Binder服务端的用户空间存在映射关系)
      • 由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间(只需要进行一定的偏移操作即可),这样便完成了一次进程间的通信,从而达到了省去一次拷贝的操作

    Binder跨进程IPC通信(数据传输)的模型可以使用如下的示意图来完整表示,如下:


    在这里插入图片描述


    这里Binder就数据传入效率和资源包占用角度来说,相关传统的IPC通信有如下的优点:

    1.减少了数据的拷贝次数,用内存读写取代 I/O 读写,提高了文件读取效率

    2.作为Binder服务端开辟的数据接收区大小是固定的(对于普通的BInder服务端和servicemanager端有一定的区别,都是都没有大于1M的空间)




    三.Binder通信"一次拷贝"源码大揭秘

    ??通过前面的不懈努力,为我们的源码事业贡献无数青春和汗水的情况下(有点夸张啊)终于我们将Linux传统跨进程IPC通信原理和Binder跨进程IPC通信原理给弄清楚了,可喜可贺啊!但是如果想要真的掌握好了Binder通信"一次拷贝"的真正精髓,那还得深入研究一番,而这个章节的博客的目的正是如此带领读者从白银选手走向终极王者巅峰!在这大章节中我们将要重点掌握的是如下两个的关键知识点:

    • 内核中服务端内存映射的建立
    • 在Binder驱动源码中有多次调用了copy_from_user()和copy_to_user()这两个函数(注意这里强调的是多次调用),这里我们将要带领读者搞清楚每次调用都是在拷贝些什么东西,拷贝到哪里去了
    • 既然上面存在了多次拷贝,那为啥Binder通信又铺天盖地的说只进行了“一次拷贝”呢,这个是不是有问题呢,或者是我们的理解有问题呢?
    • 发送端数据通过一次拷贝到内核,怎么和接收服务端的映射空间建立连接

    在后续源码的分析中,我们会主要集中火力在和Binder实现的内存操作相关的源码分析中,Binder其它的相关逻辑就忽略带过了。并且这一章节对读者的Binder知识有一定高度的要求,如果读者没有相关方面的知识储备可以从本篇博客最开始的Binder框架目录中的文章开始!


    3.1 Binder预备知识准备

    前途是光明的,道路是曲折的!所以如果想要深入探究Binder通信"一次拷贝"的真正原理,在开始相关的源码分析前还是有必要梳理梳理一下将要涉及的相关Binder知识,来点储备。走起,梳理起:

    • 由于在后续的博客分析中会牵涉到Binder通信中数据的封装和打包,这里我们有必要了解一下Binder传入数据协议的的封装逻辑,具体的可以参见博客Android Binder框架实现之Binder中的数据结构和Android Binder框架实现之Native层addService详解之请求的发送这两篇博客,其Binder传入数据的打包封装基本模型如下所示:

    在这里插入图片描述


    在这里插入图片描述


    • 参与Binder通讯的进程,无论是请求端还是服务器端,他们都会通过调用ProcessState::self()函数来建立自己的初步映射(这也是为什么说Android进程是天生就支持Binder通信的所在),而我们本章节博客重点也是从此处开始

    对于上述流程还有不是很清楚的读者请参见如下博客:
    Android Binder框架实现之servicemanager守护进程
    Android Binder框架实现之defaultServiceManager()的实现
    Android Binder框架实现之Native层addService详解之请求的发送


    3.2 Binder服务端(接收)进程建立内存映射

    其实这个标题,应该是Binder进程建立内存映射,但是这里为了形成一个整体的链路所以特意写的是服务端(接收)进程。

    ??前面我们知道了Binder内存映射建立是从ProcessState::self()函数开始,该函数使用典型的单列模式:如已创建该实例则直接返回,如果没有创建,则创建返回这个实例,这里需要锁来防止创建两个同类型的实例,该函数还是static类型的,所以可以在系统的任何地方调用。先上源码,一起燥起来!

    //[ProcessState.cpp]
    sp<ProcessState> ProcessState::self()
    {
        Mutex::Autolock _l(gProcessMutex);
        if (gProcess != NULL) {
            return gProcess;
        }
        gProcess = new ProcessState;
        return gProcess;
    }
    

    上述源码逻辑没有啥好说的(主要就是判断是否已经初始化过了gProcess而已),我们接着看ProcessState的构造方法,如下:

    //[ProcessState.cpp]
    #