背景
- 在项目中遇到需要讲明白 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
- 添加到基础知识点中,丰富知识树
网友评论