美文网首页
我要理解 EGLContext TLS 的实现原理

我要理解 EGLContext TLS 的实现原理

作者: 1999c1b720cd | 来源:发表于2018-09-08 22:57 被阅读0次

背景

  • 在项目中遇到需要讲明白 EGLContext 类「为什么」需要在「创建线程」执行销毁操作问题。然后跟进这个问题理解 pthread 的实现原理,在这里做记录
  • 整理的目的是为了能改写程序

问题

  • 「为什么」EGLContext 实例需要在「创建线程」执行销毁操作?
  • pthread 涉及哪些数据结构和基本操作?
  • 一个进程有多少个栈?

数据结构

pthread 和 EGLContext

pthread 和 EGLContext 数据结构关系

基本执行流程如图

pthread 调用流程
  • 应用层 client.so 调用 C 标准库 libc.so 的 pthread API
  • C 标准库 libc.so 通过「中断」或者「特殊系统调用指令」调用内核 Kernel 函数
  • 用户态切换到内核态,CPU 寄存器 「CS:IP」 和 「SS:SP」 切换到「内核代码段」和「内核栈」
  • 系统调用期间 CPU 处于内核态,特权级别为 Ring0
  • 内核跟 CPU/Memory 等硬件打交道
  • CPU 负责执行指令、读写内存
  • 处理完成后内核切换回用户态,CPU 寄存器 「CS:IP」 和 「SS:SP」 切换到「用户代码段」和「用户栈」
  • pthread 接口在 Android 里面实现在 libc.so 中
  • 当前进程空间内的所有线程组成双向链表结构


    线程结构体用双向链表连接在一起
    • 全局变量 g_thread_list 指向表头节点
    • 每个 pthread 结构体内部包含 prev 和 next 指针
  • 单个 pthread 结构体的内存布局


    单个 pthread 内存布局
    • pthread_internal_t#mmap_size 变量记录该线程在用户空间占用内存大小 1024 * 1024 KB - 16 KB + 4 KB + sizeof(pthread_internal) ,大概 1 MiB 空间
    • 栈内存在低地址,结构体内存在高地址
    • 最低地址处是 1 页内存的保护区,用于触发栈溢出异常,通过 mprotect 系统调用修改页面属性为 NONE,不可读写、执行
    • 由于 pthread 结构体需要 16 字节地址对齐处开始,故最高地址处会留下一些 padding 空间
    • pthread_internal_t#tls 数组变量记录当前线程专用的 Thread Local Storage 空间,数组大小 BIONIC_TLS_SLOTS 枚举控制,共 9 个元素
    • tls 数组第 4 个元素记录 ogles_context_t 类型的指针
    • 通过改变这个地址的值,可以指向不同的 ogles_context_t 实例,这样 opengl 的代
      码就操作当前指向的实例
# 查看某个进程通过 pthread 创建的栈区域
generic_x86:/data/data/com.example.guangli.demo $ cat /proc/18442/maps | grep stack
# 用于栈溢出检查的 1 页内存。没有读写、执行属性,故栈溢出会段错误
cee83000-cee84000 ---p 00000000 00:00 0 [anon:thread stack guard page]
# 线程 18486 对应的栈。大小 0xFB000 字节,大约 1 MB 
cee85000-cef80000 rw-p 00000000 00:00 0 [stack:18486]
...
e3b87000-e3b88000 ---p 00000000 00:00 0 [anon:thread stack guard page]
e3b89000-e3c84000 rw-p 00000000 00:00 0 [stack:18448]

基本操作

eglMakeCurrent 时序图

eglMakeCurrent.png
  • 目的是让一个变量指向给定的 EGLContext 对象
  • 用户通过 android.opengl.EGL14#eglMakeCurrent 调用从 Java 层进入 JNI 层 com_google_android_gles_jni_EGLImpl.cpp#jni_eglMakeCurrent 函数
  • JNI 层调用 /frameworks/native/opengl/libs/EGL/eglApi.cpp#eglMakeCurrent 进入 Native 层
  • 查找到 pthread_internal#tls 数组的基址,并设置 OPENGL_API 下标指向 OpenGL ES 2.0 的函数指针列表
  • 通过 pthread_setspecific 设置 pthread_internal#key_data 数组变量中当前 EGLContext 对象

练习题

  • 阅读 pthread_internal 源码,添加上自己的理解和猜测
  • 根据源码画出内存区域模块图,理清三层结构的关系
  • 画出 pthread_internal 和 EGLContext 的类图
  • 画出 eglMakeCurrent 时序图,目的是串联起前面的数据结构
  • 绑定不同的 EGLContext 看数据是否是对的

答案

  • 「为什么」EGLContext 实例需要在「创建线程」执行销毁操作?
    • 首先执行 eglMakeCurrent 是设置当前 pthread_internal 结构体的 key_data 数组的某个下标指向不同的 EGLContext 对象
    • 其次 android.opengl.EGL14#eglDestroyContext 是销毁 java 层传递下来的一个 EGLContext 对象,这个对象并不是内部从 TLS 读取的
    • 最后销毁就是先判断对象的状态再销毁,并没有跟 TLS 有很强的绑定在一起
    • 所以一个 EGLContext 对象是可以在一个线程创建后传递到另一个线程使用,最后在某个线程销毁。这个问题有点迷惑性
  • pthread 涉及哪些数据结构和基本操作?
    • pthread_internal 结构体
    • 栈 + guard
    • 信号栈 + guard
  • 一个进程有多少个栈?
    • 一个进程包含多个线程,在 linux 中线程是 task
    • 内核每个线程和进程结构体一样,上层通过 clone 创建线程
    • 用户态栈
    • 用户态信号栈
    • 内核态 thread_info 2 页内存的栈

总结

  • 整个流程涉及到知识点较多,中间得带着问题跟踪信息处理的流程,不然会在代码的海洋中迷失自我
  • 理解 pthread 的数据结构、基本操作有助于理解当前进程空间的内存布局,知道哪里可以拿到某些信息,哪里可以改,哪里可以 hook
  • 添加到基础知识点中,丰富知识树

相关文章

网友评论

      本文标题:我要理解 EGLContext TLS 的实现原理

      本文链接:https://www.haomeiwen.com/subject/ayakwftx.html