GPU 驱动自升级及其原理

知其所以然

背景

GPU 驱动自升级,是在以下两个痛点下催生出来的:

  1. 换机频率降低,手机的生命周期越来越长
    智能手机已经不再是一个增量市场,变成一个存量市场。消费者换机频率逐年降低,手机生命周期变长。生命周期变长了以后,那么如何更好地维护老机型就变成一个难题
  2. 开发者需要升级 ROM 才能用到 GPU driver 的新功能
    一直以来,游戏开发者想要升级到最新的 GPU 驱动的渠道跟普通的用户一样,只能靠 OEM 厂商的推送系统更新,他们没有一个渠道能够直接拿到最新的 GPU 驱动,因此无法第一时间对驱动的新特性进行适配。如何建立一个渠道能够让游戏开发者能够及时获取到最新的 GPU 驱动也是一个难题

有痛点就会有需求,GPU 驱动自升级就是针对这两个痛点提出的解决方案。

不过,这个方案要落地还会遇到很多实际问题:首先,GPU 驱动跟整个操作系统依赖极大,影响的模块很多,如 Display,Camera,Media 等,稍有不慎整个系统就会不稳定甚至 crash。

同时,这个功能要真正落地,还涉及到多方:GPU 驱动来自 SoC 厂商;Android 的 OEM 厂商等。每一方里涉及到厂商的又十分众多。因此在确定方案的时候需要保证能够有足够的兼容性。

以上这么多难题注定了这个方案从提出到落地,需要花很长的时间,走很长的路。

而事实也是如此,从 2017 年开始,Android O 提出了 Project Treble,引入了 VNDK 和 linker namespace,那时候其实已经在对这个方案进行技术铺垫了;再到 2018 年的 Android P,谷歌在 framework 加了很多 dummy code,此时整个方案的框架已经基本完成;再到 2019 年的 Android Q,这个方案作为 Q 的核心功能跟大家见面了。而小米十周年旗舰——小米 10/小米 10 Pro 也成为国内第一款落地 GPU 驱动自升级功能的 Android 智能手机。

在 GPU 驱动自升级方案中,可升级的 GPU 驱动分为两种,一种称为 Game Driver,面向的普通用户,主要是针对游戏这种十分依赖 GPU 的场景进行特殊优化,稳定的 driver;另一种称为 Prerelease Driver,面向游戏开发者,目的是让游戏开发者能够提前获取到包含新特性的,不稳定的驱动,进行提前的适配和利用。这两种驱动分别封装在两个 apk,分别通过两个 property 来分区:ro.gfx.driver.1 指定的是 Game Driver 的包名,ro.gfx.driver.1 指定的是 Prerelease Driver 的包名。

需要说明的是,虽然称为 apk,但是实际上更多地可以理解为是两个盛放 driver 的容器,apk 本身不具备任何的代码,而之所以要将其打包为 apk,更多的是为了利用 Android 的签名机制以及利用 PMS 能够对所有安装的 apk 进行统一的管理。

将 apk 解包以后可以放在整个包的结构如下:

├── AndroidManifest.xml
├── assets
│   ├── sphal_libraries.txt
│   └── whitelist.txt
├── lib
│   ├── arm64-v8a
│   └── armeabi-v7a
└── res

分别说明一下各个结构的作用:

  • sphal_libraries.txt
    能够被 gfx driver 访问的, sphal 中的库列表,后面会详细讲解
  • whitelist.txt
    使用 Game Driver 的包名白名单
  • lib
    driver 本体,分为 32 位和 64 位,根据应用的 abi 选择加载对应的 driver

Game Driver 和 Prerelease Driver (后面统称为 updated driver)这两个 apk 的结构是一样的,只不过 lib 文件夹里面的驱动不一样而已。

updated driver 配置流程

这部分讲讲在 app 启动的时候,是怎么给 app 挑选对应的 GPU driver 的。

app 在启动的过程中,经过下面的流转(详细的流程省略,非本文主题):

ActivityThread::handleBindApplication()
   \__ActivityThread::setupGraphicsSupport()
        \__GraphicsEnvironment.setup()

最终走到核心代码:

public void setup(Context context, Bundle coreSettings) {
   final PackageManager pm = context.getPackageManager();
    final String packageName = context.getPackageName();
    
    ....
    
    if (!chooseDriver(context, coreSettings, pm, packageName)) {
        setGpuStats(SYSTEM_DRIVER_NAME, SYSTEM_DRIVER_VERSION_NAME, SYSTEM_DRIVER_VERSION_CODE,
                SystemProperties.getLong(PROPERTY_GFX_DRIVER_BUILD_TIME, 0), packageName,
                getVulkanVersion(pm));
    }
}

核心函数 chooseDriver()

private static boolean chooseDriver(
        Context context, Bundle coreSettings, PackageManager pm, String packageName) {
    // PART 1
    final String driverPackageName = chooseDriverInternal(context, coreSettings);
    if (driverPackageName == null) {
        return false;
    }

    ......
}

chooseDriverInternal()

chooseDriverInternal(),这个函数的主要作用是,根据系统的配置,为当前启动的 app 判断是否需要加载 Game/Prerelease Driver,如果需要那么返回对应的 driver apk 的包名;如果不需要,那么返回 null,表明当前启动的 app 只需要加载 system gpu driver。判断的逻辑:

private static String chooseDriverInternal(Context context, Bundle coreSettings) {
    // 判断 game driver 的 property 是否配置了
    final String gameDriver = SystemProperties.get(PROPERTY_GFX_DRIVER);
    final boolean hasGameDriver = gameDriver != null && !gameDriver.isEmpty();

    // 判断 prerelease driver 的 property 是否配置了
    final String prereleaseDriver = SystemProperties.get(PROPERTY_GFX_DRIVER_PRERELEASE);
    final boolean hasPrereleaseDriver = prereleaseDriver != null && !prereleaseDriver.isEmpty();

    // 两个 property 都没有配置,前面提到这两个 property 代表的是两个包名
    // 如果都没有配置,那么说明这项功能没有使能,使用 system gpu driver
    if (!hasGameDriver && !hasPrereleaseDriver) {
        if (DEBUG) Log.v(TAG, "Neither Game Driver nor prerelease driver is supported.");
        return null;
    }

    // To minimize risk of driver updates crippling the device beyond user repair, never use an
    // updated driver for privileged or non-updated system apps. Presumably pre-installed apps
    // were tested thoroughly with the pre-installed driver.
    // 不是所有的 app 都可以使用这个功能的,如果当前是预装应用,那么也不会对其使用该功能
    final ApplicationInfo ai = context.getApplicationInfo();
    if (ai.isPrivilegedApp() || (ai.isSystemApp() && !ai.isUpdatedSystemApp())) {
        if (DEBUG) Log.v(TAG, "Ignoring driver package for privileged/non-updated system app.");
        return null;
    }

    // 接下来看下面的流程图:

chooseDriverInternal

在判断完当前的 app 需要使用的 driver 类型以后,返回到 chooseDriver()

chooseDriver()

private static boolean chooseDriver(
        Context context, Bundle coreSettings, PackageManager pm, String packageName) {
    final String driverPackageName = chooseDriverInternal(context, coreSettings);
    if (driverPackageName == null) {
        return false;
    }
    
    // PART 2
    final PackageInfo driverPackageInfo;
    // 容纳 game driver 的 apk 必须是一个 system app,否则会直接跳过
    try {
        driverPackageInfo = pm.getPackageInfo(driverPackageName,
                PackageManager.MATCH_SYSTEM_ONLY | PackageManager.GET_META_DATA);
    } catch (PackageManager.NameNotFoundException e) {
        Log.w(TAG, "driver package '" + driverPackageName + "' not installed");
        return false;
    }

    // Android O 及以后,谷歌提出了 Project Triple,其中包括 VNDK,规定了 GPU driver 必须是在 sphal 的 linker space
    // 因此 driver apk 的 targetSdkVersion 必须大于等于 O
    // O drivers are restricted to the sphal linker namespace, so don't try to use
    // packages unless they declare they're compatible with that restriction.
    final ApplicationInfo driverAppInfo = driverPackageInfo.applicationInfo;
    if (driverAppInfo.targetSdkVersion < Build.VERSION_CODES.O) {
        if (DEBUG) {
            Log.w(TAG, "updated driver package is not known to be compatible with O");
        }
        return false;
    }

    // 判断当前运行 app 是 32 位的还是 64 位的,以便加载正确 abi 的 gpu driver
    final String abi = chooseAbi(driverAppInfo);
    if (abi == null) {
        if (DEBUG) {
            // This is the normal case for the pre-installed empty driver package, don't spam
            if (driverAppInfo.isUpdatedSystemApp()) {
                Log.w(TAG, "updated driver package has no compatible native libraries");
            }
        }
        return false;
    }

    final StringBuilder sb = new StringBuilder();
    sb.append(driverAppInfo.nativeLibraryDir)
      .append(File.pathSeparator);
    sb.append(driverAppInfo.sourceDir)
      .append("!/lib/")
      .append(abi);
    final String paths = sb.toString();
    // 读取 driver apk 里面的 sphal_libraries.txt 的内容
    final String sphalLibraries = getSphalLibraries(context, driverPackageName);
    if (DEBUG) {
        Log.v(TAG,
                "gfx driver package search path: " + paths
                        + ", required sphal libraries: " + sphalLibraries);
    }
    // 配置阶段最重要的一个函数,下面详解
    setDriverPathAndSphalLibraries(paths, sphalLibraries);

    ......
    return true;
}

其中,path 这个变量存放了当前 app 的 GPU Driver 的加载路径,类似于:

/system/app/xxx/lib/:/system/app/xxx/xxx.apk!/lib/armeabi-v7a/

包括两部分,使用 : 分隔,一部分是 driver apk 所在的 lib 路径,另一个的 driver apk 包内部的 lib 路径,请注意这里有一个特殊的符号:**!/**,这个符号的意义后面会详细说明。

在确定了启动的 apk 需要用到的 driver apk 以后,最终会走到一个最重要的函数:setDriverPathAndSphalLibraries(),通过这个函数,带上两个参数—— driver path 和 sphal_libraries.txt 里面的内容。查看这个函数的定义,其实这个是一个 JNI 函数,真正的实现在这里(frameworks/native/libs/graphicsenv/GraphicsEnv.cpp):

void GraphicsEnv::setDriverPathAndSphalLibraries(const std::string path,
                                                 const std::string sphalLibraries) {
    ......
    mDriverPath = path;
    mSphalLibraries = sphalLibraries;
}

这里就是简单地将这两个变量存起来。至此,updated driver 的配置流程就走完了,而要看这两个变量到底怎么用,得看下面的加载流程。

VNDK 和 linker namespace

在说明加载流程之前,需要说明一下 linker namespace,这个是 GPU 驱动自升级用到的核心技术,而这个技术是在 Google 在 Android O 的 Project Treble 引入的。要理解这个技术的作用先要理解一下 Project Treble。

Project Treble 将系统拆分解耦为两部分——system 和 vendor,诣在分离 android framework 和硬件驱动的耦合,system 分区只存放原生 android 相关的内容,vendor 分区存放厂商相关定制的内容。目的是为了达到在保持 vendor 分区不变的情况下,仅通过升级 system 分区即可升级到新版本的 Android。前面提到 system 和 vendor 要保持独立,不能相互依赖。但是也不可能完全独立,双方还是存在一些依赖的,。基于这样的需求,将 system 分区的 so 分为以下:

  • FMK-ONLY
    framework 专有库,称为 FMK-ONLY,vendor 无法访问
  • LLNDK
    API/ABI 稳定的,system 和 vendor 共享一套的库,称为 LLNDK
  • VNDK
    vendor 如果需要访问到一部分的 FMK-ONLY 的库,那么可以通过 VNDKVNDK 是专门给 vendor 模块维护的一部分 system 库,位于 /system/lib[64]/vndk-${version}/,在 FMK-ONLY 可以找到同名的库,但是两者的符号表是不一致,是 FMK-ONLY 的同名不同副本。如下图:
    VNDK
  • VNDK-SP
    VNDK-SP 属于 VNDK, 是 SP-HAL 维二能够依赖的模块(另一个是上面提到的 LLNDK)。而 SP-HAL 是 system 访问 vendor 的唯一途径,位于 vendor 分区。如果此时 system 和 vendor 需要涉及到相同的库,那么 vendor 调用的就是 VDNK-SP,路径是 /system/lib[64]/vndk-sp-${version}/
    这么说可能比较难以理解,举个例子:
    surfaceflinger 属于 system 的进程,但是当它要进行 GPU 合成的时候,需要访问位于 vendor 分区的 GPU 驱动,此时需要通过 SP-HAL—— libGLES_${chipset}.so,而此时 surfaceflinger 和 libGLES_${chipset}.so 都要依赖 libcutils.so,我们就称 libGLES_${chipset}.so 依赖的 libcutils.soVNDK-SP。如下图:VNDK-SP
    用一张图展示上面各个名词在各个分区的位置:

通过上面的例子可以看到,在 surfaceflinger 其实是有两个 libcutils.so 的副本的,两个库名字一样,但是符号表完全不一样,如何正确加载不引起冲突呢?
linker namespace 就是为解决这个问题而提出的技术。LLNDKVNDK-SPlibcutils.so 分别隶属于不同的 liner namespace,这样就可以避免冲突问题,各自加载各自的库。降维理解就是:linker namespace 相当于 shell 的 $PATH 环境变量。当我们在 shell 里面敲入 java 的时候,shell 会从 $PATH 里面一个一个寻找 java 可执行二进制文件。同样的, so 库在被加载的时候同样有一个类似环境变量的东西(称为“搜索路径”)告诉 linker 链接器去哪里找到这个 so,而 linker namespace 相当于就是通过创建两个隔离的空间,不同的空间有不同的搜索路径,不同的进程从不同的空间加载 so 库,从而避免了加载库的时候出现的名字一致的问题。详细的在后面的加载流程会具体说明。

driver 加载时机

在 Android Q 上,EGL/OpenGL driver 加载时机有两种,一种是 zygote 在启动的时候会先将他们进行预加载,另一种是 Activity 在启动的时候也会去进行加载,下面分别说明:

zygote 预加载 GPU driver

我们知道,所有的 Android 进程都是由 zygote 或者 zygote64 孵化出来的,而当 zygote 和 zygote64 在启动的时候,会去将 gpu driver 进行预加载(省略了 zygote 启动的完整流程,与主题无关):

frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
ZygoteInit::main()
  \__ ZygoteInit::preload()
        \__ZygoteInit:maybePreloadGraphicsDriver()
             \__ZygoteInit::nativePreloadGraphicsDriver()

然后通过 JNI 走到:frameworks/base/core/jni/com_android_internal_os_ZygoteInit.cppandroid_internal_os_ZygoteInit_nativePreloadGraphicsDriver

void android_internal_os_ZygoteInit_nativePreloadGraphicsDriver(JNIEnv* env, jclass) {
    ScopedSCSExit x;
    // 这里通过判断 debug.hwui.renderer 这个 property 的值:
    // skiavk: hwui 使用 Vulkan
    // skiagl: hwui 使用 OpenGL
    if (Properties::peekRenderPipelineType() == RenderPipelineType::SkiaGL) {
        eglGetDisplay(EGL_DEFAULT_DISPLAY);
    }
}

然后通过下面路径:

eglGetDisplay()
  \__egl_init_drivers()
       \__egl_init_drivers_locked()
            \__Loader::open()

走到最终的 driver 加载函数 Loader::open()。接下来看第二种:

activity 加载 GPU driver

当启动一个 Activity 的时候会调用 startActivity 的方法,这个方法会 Binder 调用到 system_server AMS,AMS 作为中转经过一系列处理后会 Binder 调用到目标进程 ApplicationThread(Activity 的启动这里就不展开了,与主题无关),最终会调用到 ActivityThreadhandleLaunchActivity 方法,而 gpu driver 就是在这里加载的:

ActivityThread::handleLaunchActivity()
  \__HardwareRenderer::preload()

这里的 preload() 这一个 JNI 方法,具体的实现流程:

android_view_ThreadedRenderer_preload()
  \__RenderProxy::preload()
       \__RenderThread::preload()

RenderThread::preload() 里面会去调用 eglGetDisplay()

void RenderThread::preload() {
    // EGL driver is always preloaded only if HWUI renders with GL.
    if (Properties::getRenderPipelineType() == RenderPipelineType::SkiaGL) {
        std::thread eglInitThread([]() { eglGetDisplay(EGL_DEFAULT_DISPLAY); });
        eglInitThread.detach();
    } else {
        requireVkContext();
    }
    HardwareBitmapUploader::initialize();
}

最终也会走到 Loader::open() 这个函数,在这个函数完成真正完成的加载流程:

void* Loader::open(egl_connection_t* cnx)

在说明加载流程之前,得先说明一下 egl_connection_t 这个结构体,整个加载过程就是对这个结构体的填充赋值,因此需要对这个结构体有深刻地理解:

egl_connection_t

struct egl_connection_t {
    enum {
        GLESv1_INDEX = 0,
        GLESv2_INDEX = 1
    };

    inline egl_connection_t() : dso(nullptr),
                                libEgl(nullptr),
                                libGles1(nullptr),
                                libGles2(nullptr),
                                systemDriverUnloaded(false) {
        ......
    }

    void *              dso;
    gl_hooks_t *        hooks[2];
    EGLint              major;
    EGLint              minor;
    EGLint              driverVersion;
    egl_t               egl;

    // Functions implemented or redirected by platform libraries
    platform_impl_t     platform;

    void*               libEgl;
    void*               libGles1;
    void*               libGles2;

    // 当需要使能 updated driver 的时候,需要将默认的 gpu driver unload,再加载 updated driver
    // 当 updated driver 加载完成以后,会将 systemDriverUnloaded 设置为 true
    bool                systemDriverUnloaded;
    ......
};

结构体中有几个重要的成员变量:

  • void * dso
    存放的其实是这个结构的地址:

     struct driver_t {
          explicit driver_t(void* gles);
          ~driver_t();
          // returns -errno
          int set(void* hnd, int32_t api);
          void* dso[3];
    };

    主要的是 void *dso[]dso[0] 存放了 libEGL.so 的句柄(dlopen 的返回值,后面一致);dso[1] 存放了 libGLESv1_CM.so 的句柄;dso[2] 存放了 libGLESv2.so 的句柄。作用是判断 GPU driver 是否已经加载了。

  • gl_hooks_t * hooks[2]
    hooks[0] 保存了所有 GLESv1_CM 函数的符号地址;hooks1 保存了所有 GLESv2 函数的符号地址

  • egl_t egl
    egl 保存了所有 EGL 函数的符号地址

  • void * libEgl/libGles1/libGles2
    保存了 libEGL.solibGLESv1_CM.solibGLESv2.so 这三个 wrapper 库的句柄

updated driver 加载流程

libEGL/libGLES(后面统称为 GPU driver) 这些库都是在 Loader::open() 里面完成的。这个函数分为两部分——首先判断是否需要 unload system GPU driver,如果需要会先执行 unload 操作;接着尝试加载 game/prerelease driver(这两种 driver 在这里走的流程是完全一致的,因此后面统称为 updated driver);如果失败会再尝试多次加载 system GPU driver。虽然是先 unload 再 load,但是如果不先说明 load 的流程,unload 会难以说明清楚,所以下面的内容会先说明 load 的流程,再说明 unload 的流程:

加载 updated driver

加载 updated driver 在 attempt_to_load_updated_driver() 这个函数中完成,这个函数用到了三个核心函数:

  • getDriverNamespace() 配置 linker namespace
  • load_updated_driver() 加载 GPU driver
  • initialize_api() 获取符号地址

getDriverNamespace() 配置 linker namespace

android_namespace_t* GraphicsEnv::getDriverNamespace() {
    std::lock_guard<std::mutex> lock(mNamespaceMutex);

    if (mDriverNamespace) {
        return mDriverNamespace;
    }

    if (mDriverPath.empty()) {
        return nullptr;
    }

    auto vndkNamespace = android_get_exported_namespace("vndk");
    if (!vndkNamespace) {
        return nullptr;
    }

    mDriverNamespace = android_create_namespace("gfx driver",
                                                mDriverPath.c_str(), // ld_library_path
                                                mDriverPath.c_str(), // default_library_path
                                                ANDROID_NAMESPACE_TYPE_ISOLATED,
                                                nullptr, // permitted_when_isolated_path
                                                nullptr);

    if (!linkDriverNamespaceLocked(vndkNamespace)) {
        mDriverNamespace = nullptr;
    }

    return mDriverNamespace;
}

getDriverNamespace() 这个函数主要做了以下的事情:

  1. 使用前面配置流程的最后一步,updated driver 的路径就是被存放在 mDriverPath 这个变量。而现在 mDriverPath 被当做参数传给 android_create_namespace()。这里就需要说明一下 android_create_namespace() 这个函数,它的作用是创建一个 linker namespace,各个参数:
android_create_namespace(
     const char *name, // 这个 link namespace 的名字
     const char *ld_library_path,
     const char *default_library_path,
     unint64_t type, 
     const char *permitted_when_isolated_path,
     android_namespace_t *parent);

参数中有很多个路径,这些表明了这个 linker namespace 中库的搜索路径,按照先 ld_library_pathdefault_library_path 的顺序,并且如果 namespace 的 typeANDROID_NAMESPACE_TYPE_ISOLATED,那么如果在前面的路径找不到,还会在 permitted_when_isolated_path 里面寻找。

这些路径最终会传到 create_namespace()

ns->set_ld_library_paths(std::move(ld_library_paths));
ns->set_default_library_paths(std::move(default_library_paths));
ns->set_permitted_paths(std::move(permitted_paths));

这里只是把路径保存在 app 的 gfx driver 的 linker namespace 里面。至于这些路径怎么用,后面加载部分会详细介绍。

因此这里创建里一个名为 gfx driver 的 linker namespace,并且指定 mDriverPath 为该 linker namespace 的库搜索路径。
2. 调用 linkDriverNamespaceLocked()

 bool GraphicsEnv::linkDriverNamespaceLocked(android_namespace_t* vndkNamespace) {
        const std::string llndkLibraries = getSystemNativeLibraries(NativeLibrary::LLNDK);
        if (llndkLibraries.empty()) {
            return false;
        }
        if (!android_link_namespaces(mDriverNamespace, nullptr, llndkLibraries.c_str())) {
            ALOGE("Failed to link default namespace[%s]", dlerror());
            return false;
        }
    
        const std::string vndkspLibraries = getSystemNativeLibraries(NativeLibrary::VNDKSP);
        if (vndkspLibraries.empty()) {
            return false;
        }
        if (!android_link_namespaces(mDriverNamespace, vndkNamespace, vndkspLibraries.c_str())) {
            ALOGE("Failed to link vndk namespace[%s]", dlerror());
            return false;
        }
    
        if (mSphalLibraries.empty()) {
            return true;
        }
    
        // Make additional libraries in sphal to be accessible
        auto sphalNamespace = android_get_exported_namespace("sphal");
        if (!sphalNamespace) {
            ALOGE("Depend on these libraries[%s] in sphal, but failed to get sphal namespace",
                  mSphalLibraries.c_str());
            return false;
        }
    
        if (!android_link_namespaces(mDriverNamespace, sphalNamespace, mSphalLibraries.c_str())) {
            ALOGE("Failed to link sphal namespace[%s]", dlerror());
            return false;
        }
    
        return true;
}

这个函数多次调用了 android_link_namespaces() 这个函数:

bool android_link_namespaces(struct android_namespace_t* from,
                                struct android_namespace_t* to,
                                const char* shared_libs_sonames);

作用是,from 这个 namespace 能够访问到 shared_libs_sonames(内容是一连串以 : 分隔开来的 so 的名字,例如:libc.so:libmath.so)的这些库,但是 shared_libs_sonames 却是从 to 这个 namespace 中加载的。
因此,linkDriverNamespaceLocked() 这个函数做了这几件事:

  • gfx driver 能够访问 LLNDK 的库
  • gfx driver 能够访问 VNDKSP 的库
  • gfx driver 能够从 sphal 这个 link namespace 访问到 mSphalLibraries 的库。前面提到,sphallibraries.txt 的内容被存放在 mSpahlLibraries 这个变量中。至此,我们终于能够理解 sphallibraries.txt 这个文件里面的内容的意义,就是能够被 gfx driver 访问的, sphal 中的库列表

通过 linkDriverNamespaceLocked()gfx driver 这个 linker namespace 变得非常完备以及真正可用。updated driver 能够正确地在这个新创建的 linker namespace 中被搜索和加载。

通过一张图总结上面的内容:
getDriverNamespace

getDriverNamespace() 创建,配置完 gfx driver 这个 linker namespace 之后,就开始真正的加载流程,主要都是在 load_updated_driver() 这个函数完成的:

load_updated_driver() 加载 GPU driver

static void* load_updated_driver(const char* kind, android_namespace_t* ns) {
    ATRACE_CALL();
    // 当 flags 为 ANDROID_DLEXT_USE_NAMESPACE 的时候,说明库是从 library_namespace 指定的 link namespace 加载
    const android_dlextinfo dlextinfo = {
        .flags = ANDROID_DLEXT_USE_NAMESPACE,
        .library_namespace = ns,
    };
    void* so = nullptr;
    // static const char* DRIVER_SUFFIX_PROPERTY = ;
    //
    // static const char* HAL_SUBNAME_KEY_PROPERTIES[2] = {
    //     "ro.hardware.egl"       // 在 8250 平台是 adreno
    //     "ro.board.platform",    // 在 8250 平台是 kona
    // };
    // 这里拼凑出 SoC 私有的 GPU driver 的名字,例如高通的 GPU driver 名字为:libGLESv2_adreno.so
    char prop[PROPERTY_VALUE_MAX + 1];
    for (auto key : HAL_SUBNAME_KEY_PROPERTIES) {
        if (property_get(key, prop, nullptr) <= 0) {
            continue;
        }
        std::string name = std::string("lib") + kind + "_" + prop + ".so";
        so = do_android_dlopen_ext(name.c_str(), RTLD_LOCAL | RTLD_NOW, &dlextinfo);
        if (so) {
            return so;
        }
    }
    return nullptr;
}

do_android_dlopen_ext 实际上调用的就是 android_dlopen_ext(const char* __filename, int __flags, const android_dlextinfo* __info),前两个参数的意义跟 dlopen() 是一致的(参见这里),另外的 __info 这个参数是 Android 自定义的,例如这里的 dlextinfo 就是表明当前这个 so 是从前面创建的 gfx driver 这个 link namespace 中加载。

这里需要注意 kind 有两种情况:在具体的实现中,GPU driver 要么全都集成在一个库:/vendor/lib/egl/libGLES.so,此时 kind 就是 GLES;要么是(最常见的)拆分成三个库:/vendor/lib/egl/libEGL.so|libGLESv1_CM.so|libGLESv2.so,此时 kind 总共有三类:EGLGLESv1_CMGLESv2,其中 GLESv1_CM 代表的是 OpenGL ES 1.1 的驱动,GLESv2 代表的是 OpenGL ES 2.0/3.0/3.1 的驱动。

加载函数 android_dlopen_ext() 经过下面的调用:

android_dlopen_ex()
 \_ _loader_android_dlopen_ext()
       \_ dlopen_ext()
           \_ do_dlopen()
               \_ find_library()
                   \_ find_libraries()
                       \_ find_library_internal()
                           \_ load_library()
                               \_ open_library()

open_library() 把存起来的路径传给 open_library_on_paths

int fd = open_library_on_paths(zip_archive_cache, name, file_offset, ns->get_ld_library_paths(), realpath);

这个函数把前面创建 link namespace 的时候保存的路径当做参数,然后会去检查 path 里面是否有 !/ 分隔符,如果有,那么就从 apk 这个 zip 包里面把 so 解压出来:

static int open_library_at_path(ZipArchiveCache* zip_archive_cache,
                             const char* path, off64_t* file_offset,
                             std::string* realpath) {
   int fd = -1;
   // const char* const kZipFileSeparator = "!/";
   if (strstr(path, kZipFileSeparator) != nullptr) {
      fd = open_library_in_zipfile(zip_archive_cache, path, file_offset, realpath);
   }

因此现在可以解答前面的疑问:path 里面带 !/ 的意义是:从 apk 的包里面解压拿到包里面 !/ 后面的路径代表的内容。,例如前面的 /system/app/xxx/xxx.apk!/lib/armeabi-v7a/ 意义就是从 /system/app/xxx/xxx.apk 这个 zip 包里面的 lib/armeabi-v7a/

同样用一张图来总结上面的内容:

load_updated_driver

initialize_api 获取符号地址

这个函数依据不同的类型(EGLGLESv1_CMGLESv2),获取符号地址的方式也不同:

  • EGL
    if (mask & EGL) {
        getProcAddress = (getProcAddressType)dlsym(dso, "eglGetProcAddress");
    
        ALOGE_IF(!getProcAddress,
                "can't find eglGetProcAddress() in EGL driver library");
    
        egl_t* egl = &cnx->egl;
        __eglMustCastToProperFunctionPointerType* curr =
            (__eglMustCastToProperFunctionPointerType*)egl;
        char const * const * api = egl_names;
        while (*api) {
            char const * name = *api;
            __eglMustCastToProperFunctionPointerType f =
                (__eglMustCastToProperFunctionPointerType)dlsym(dso, name);
            if (f == nullptr) {
                // couldn't find the entry-point, use eglGetProcAddress()
                f = getProcAddress(name);
                if (f == nullptr) {
                    f = (__eglMustCastToProperFunctionPointerType)nullptr;
                }
            }
            *curr++ = f;
            api++;
        }
    }
    这里首先获取到 eglGetProcAddress() 的符号地址并且保存在 getProcAddress 这个变量,后面会用到。eglGetProcAddress 这个函数的作用是返回一个 EGL/GL(ES) 的符号地址,作用跟后面的 dlsym() 类似。
    需要注意的是 egl_names
    char const * const egl_names[] = {
       #include "egl_entries.in"
       nullptr
    };
    elg_entries.in 的内容是一系列的 EGL 标准接口的名称字符串:
#define EGL_ENTRY(_r, _api, ...) #_api,
EGL_ENTRY(EGLDisplay, eglGetDisplay, NativeDisplayType)

因此 egl_names 的内容就是一个 EGL 接口函数的字符串数组,遍历该数组,并且从 load_updated_driver() 加载的库调用 dlsym() 获取接口函数的符号地址(前面提到 load_updated_driver() 调用的 android_dlopen_ext()dlopen() 作用类似),获取到的函数符号地址最终都保存在 cnx->egl

 struct egl_connection_t {
        ...
        egl_t               egl;
}
 struct egl_t {
         #include "EGL/egl_entries.in"
 };

神奇的事情来了,这里 struct egl_t 的定义和上面的 egl_names[] 用到了同样的 egl_entries.in,但是在 struct egl_tEGL_ENTRY 的定义不一样了,这里变成了函数指针:

#define EGL_ENTRY(_r, _api, ...) _r (*(_api))(__VA_ARGS__);

所以 cnx->egl 保存了所有 EGL 函数的符号地址。

  • GLESv1_CM/GLESv2
    GLESv1_CM 和 GLESv2 加载主要都在 init_api() 完成,函数的作用是将 GLESv1_CM 的所有函数符号地址保存到 cnx->hooks[0],GLESv2 保存在 cnx->hooks[1]
    void init_api(void* dso,
               char const * const * api,
               char const * const * ref_api,
               __eglMustCastToProperFunctionPointerType* curr,
               getProcAddressType getProcAddress);
    解释一下各个参数的意义:
  • api
    这个参数,GLESv1_CM 传的是 gl_names_1,GLESv2 传的是 gl_names 这两个跟前面的 egl_names 一样,同样是 #include 一个 in 文件(gl_names_1entries_gles1.ingl_namesentries.in),这些 in 文件跟 egl_entries.in 类似:
    GL_ENTRY(void, glActiveShaderProgram, GLuint pipeline, GLuint program)
    跟前面 EGL_ENTRY 一样,也是有两个定义,这里是将接口转成接口的函数声明字符串:
    #define GL_ENTRY(_r, _api, ...) #_api,
  • ref_api
    这个参数的作用是加速 GLESv1_CM 符号表地址的加载速度,在获取 GLESv1_CM 所有函数的符号表时,ref_api 赋值为 gl_names(也就是 GLESv2 的函数列表),然后在获取符号表的时候,如果当前的函数属于 GLESv2,那么就将地址赋值为 NULL 并且跳过。原理是 GLESv1_CM 是 GLESv2 的子集,而在 cnx->hook[2] 的声明中:
    struct egl_connection_t {
            ......
            gl_hooks_t *        hooks[2];
    };
    
    struct gl_hooks_t {
           struct gl_t {
               #include "entries.in"
           } gl;
           struct gl_ext_t {
               __eglMustCastToProperFunctionPointerType extensions[MAX_NUMBER_OF_GL_EXTENSIONS];
           } ext;
    };
    数组两个都是 gl_hooks_t,即 GLESv2 的长度。因此对于 GLESv1_CM 的库,如果函数名属于 GLESv2,那么可以直接跳过符号表获取的逻辑转而赋值为 NULL,从而加快 GLESv1_CM 的处理速度。
  • curr
    分别对应了上面提到的 cnx->hooks[],GLESv1_CM 对应 cnx->hooks[0],GLESv2 对应 cnx->hooks[1],代表这符号表地址保存的位置。
  • getProcAddress
    前面获取 EGL 库的符号地址的时候提到的,eglGetProcAddress 的符号地址。

接着,获取 GLESv1_CM/GLESv2 的符号地址按照下面的顺序进行:

  • 直接 dlsym() 尝试获取
  • 如果失败,使用 eglGetProcAddress() 尝试获取
  • 如果失败,尝试使用 dlsym() 加载非 OES 的版本。例如 glAlphaFuncxOES() 获取符号地址失败,那么尝试获取 glAlphaFuncx() 这个非 OES 的符号地址(注:假设 OpenGL 针对移动平台作了专门的修改或者优化,就会加上 OES)
  • 如果失败,尝试使用 dlsym() 加载 OES 的版本
  • 如果都失败,那么将当前函数的符号表赋值为 gl_unimplemented,意为没有实现。

总结

总结一下,updated driver 的加载主要在 attempt_to_load_system_driver() 中完成,主要分为三步:

  1. 创建/配置 link namespace,主要作用是将 app 加载 GPU driver 的路径转到 updated driver apk 所在的位置
  2. 从 updated driver apk 中依次将 libEGLlibGLESv1_CMlibGLESv2 打开获取 handle
  3. 从第二步获取的 handle 中获取各个 EGL, GLES 的函数的符号地址

其实看上去跟常规的加载库的步骤是一直的,只不过多了第一步的创建 link namespace 限定 so 的加载路径。

至此,整个 updated driver 的加载流程就完全讲完了。再用一张图总结前面所有的内容:
updated driver 总结

unload system driver

driver 加载时机那一节提到的那样,zygote[64] 在启动的时候会去将 system driver 预加载,而所有 Android 进程都是由 zygote[64] 孵化的,因此每一个进程默认 system driver 都是已经加载完毕的,因此对于那些需要 updated driver 的 app,就需要将 system driver unload。而是否需要 unload system driver 是通过判断 mDriverPath 是否为空(updated driver 的配置过程提到,这个变量的意义是 updated driver 的具体路径)。而 unload 的操作也非常简单,跟前面的加载恰恰相反,就是将存放 EGL 和 GLES 库的 handle 和函数符号地址的 hooks[]dso[] 这两个变量的内容都置为 nullptr,这里就不赘述了。

Author: simowce

Permalink: https://blog.simowce.com/all-about-updated-driver/

知识共享许可协议
本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可。