5、Binder线程模型:Binder线程池的创建与管理、binder_thread结构、工作队列与等待队列
好,我们进入Binder机制里非常核心的一块——线程模型。说实话,很多做Android开发的朋友,对Binder的通信原理能说个七七八八,但一问到“Binder的线程到底是怎么创建和管理的?”就有点含糊了。嗯,这恰恰是系统服务稳定性和安全性的基石。你想想看,如果线程模型搞不清楚,权限提升漏洞怎么来的,你都没法彻底理解。
5.1 Binder线程池的创建与管理
Binder线程池,说白了就是一组专门用来处理Binder请求的工作线程。我个人习惯把它理解成一个“快递分拣中心”——每个快递员(线程)从传送带(驱动层的工作队列)上取包裹(Binder请求),然后送到对应的收件人(目标服务)手里。
这个线程池是在进程启动时,由ProcessState类负责初始化的。我们来看一下关键流程:
// frameworks/native/libs/binder/ProcessState.cpp
void ProcessState::startThreadPool() {
AutoMutex _l(mLock);
if (!mThreadPoolStarted) {
mThreadPoolStarted = true;
spawnPooledThread(true); // 创建第一个主线程
}
}
void ProcessState::spawnPooledThread(bool isMain) {
sp<Thread> t = new PoolThread(isMain);
t->run(name.string()); // 启动线程
}
这里有个细节:spawnPooledThread的第一个参数isMain,标记这个线程是不是“主线程”。主线程有什么特殊?它负责处理来自ServiceManager的注册和查询请求。我在项目中遇到过一个问题,某个系统服务在初始化时卡住了,排查了半天,发现就是主线程被一个死循环阻塞了,导致整个ServiceManager的通信都挂了。
核心要点:Binder线程池是懒加载的。默认情况下,一个进程最多可以创建16个Binder线程(由BINDER_MAX_THREADS定义)。当并发请求超过这个数,新的请求就会在驱动层排队等待。
线程池的管理策略是这样的:
- 按需创建:驱动发现没有空闲线程处理新请求时,会通知用户空间创建新线程
- 上限控制:最多16个线程,防止资源耗尽
- 空闲回收:线程空闲一段时间后会自动退出(不过这个机制在早期版本里有点bug)
避坑指南:我曾经在分析一个系统服务ANR问题时,发现它的Binder线程池被占满了。原因是某个第三方应用频繁调用一个耗时接口,每个请求都占着一个线程等I/O。解决方案很简单——把那个接口改成异步,或者增加线程池上限。但要注意,改上限要谨慎,线程太多反而会降低性能。
5.2 binder_thread结构:每个线程的“身份证”
在Binder驱动层,每个参与通信的线程都有一个对应的binder_thread结构体。你可以把它理解成线程在Binder世界里的“身份证”和“工作台”。
// kernel/drivers/android/binder.c
struct binder_thread {
struct binder_proc *proc; // 所属进程
struct rb_node rb_node; // 红黑树节点(用于查找)
struct list_head todo; // 待处理工作项链表
uint32_t return_error; // 错误码
enum binder_looper_state looper_state; // 线程状态
// ... 省略其他字段
};
这个结构体里,我最关注的是todo链表和looper_state状态。为什么?因为它们直接决定了线程怎么处理请求。
todo链表里存的是什么呢?就是驱动分配给这个线程的Binder工作项。每个工作项可能是一个BC_TRANSACTION(发送事务),也可能是一个BR_TRANSACTION(接收事务)。驱动通过把工作项挂到不同线程的todo链表上,实现了请求的分发。
而looper_state状态,标记了线程当前在干什么:
| 状态 | 含义 | 说明 |
|---|---|---|
| BINDER_LOOPER_STATE_REGISTERED | 已注册 | 线程已加入线程池,可以接收请求 |
| BINDER_LOOPER_STATE_ENTERED | 已进入 | 线程正在等待工作项 |
| BINDER_LOOPER_STATE_WAITING | 等待中 | 线程在等待队列上休眠 |
| BINDER_LOOPER_STATE_POLL | 轮询中 | 线程正在使用epoll等待 |
注意:在分析权限提升漏洞时,binder_thread的return_error字段经常被利用。攻击者可以通过构造特殊的Binder请求,让驱动把错误码写入这个字段,从而绕过安全检查。嗯,这个我们后面讲漏洞案例时会详细展开。
5.3 工作队列与等待队列:驱动层的调度核心
好,接下来是重头戏——工作队列和等待队列。这两个队列是Binder驱动实现线程调度的核心数据结构。
先看一张我画的流程图,帮你理清它们的关系:
这张图里,我特意把工作队列和等待队列分开画了。实际上,在驱动代码里,它们是这样配合的:
- 工作队列(todo list):每个
binder_proc(进程)和binder_thread(线程)都有自己的todo链表。驱动收到请求后,先把工作项挂到目标进程的proc_todo上,然后尝试找一个空闲线程,把工作项迁移到线程的thread_todo上。 - 等待队列(wait queue):当所有线程都在忙时,新的请求就只能在等待队列里排队。线程处理完当前工作后,会从等待队列里取下一个工作项。
关键机制:驱动在分配工作项时,会优先选择与请求来源线程在同一个CPU核心上的线程。这是为了利用CPU缓存局部性,减少缓存失效的开销。嗯,这个优化在Android 8.0之后才引入的,之前的老版本没有这个策略。
我们来看一段驱动里的核心调度代码:
// kernel/drivers/android/binder.c
static int binder_thread_read(struct binder_proc *proc,
struct binder_thread *thread,
binder_uintptr_t binder_buffer, size_t size,
binder_size_t *consumed, int non_block)
{
// 1. 先检查线程自己的todo链表
if (list_empty(&thread->todo)) {
// 2. 如果线程todo为空,检查进程todo
if (list_empty(&proc->todo)) {
// 3. 都为空,进入等待队列休眠
ret = wait_event_interruptible_exclusive(
thread->wait, binder_has_thread_work(thread));
} else {
// 4. 进程有工作,迁移到线程todo
binder_dequeue_work_from_proc(proc, thread);
}
}
// 5. 处理线程todo里的工作项
while (!list_empty(&thread->todo)) {
// 处理每个工作项...
}
}
这段代码的逻辑很清晰:线程先看自己有没有活干,没有就去进程的公共队列里拿,再没有就睡觉等活。我在分析一个死锁问题时,就发现是某个线程在wait_event_interruptible_exclusive这里卡住了——因为另一个线程持有了锁,而这个线程在等锁释放,但持有锁的线程又在等Binder回复,形成了循环等待。
个人经验:我曾经在调试一个系统服务时,发现它的Binder线程经常处于BINDER_LOOPER_STATE_WAITING状态。一开始以为是负载低,后来用ftrace跟踪才发现,是驱动层的工作队列分配策略有问题——所有请求都堆到了一个线程上,其他线程在空等。解决方案是调整了线程的优先级和CPU亲和性,让负载更均衡。
5.4 线程模型与安全:攻击面分析
讲完了线程模型,我们得聊聊它和安全的关系。说实话,Binder线程模型的设计初衷是性能和公平性,但攻击者往往能从这些机制里找到突破口。
常见的攻击面包括:
- 线程池耗尽攻击:攻击者发送大量并发请求,占满所有16个Binder线程,导致合法请求被阻塞。这其实是一种资源耗尽型DoS攻击。
- 工作队列投毒:通过构造特殊的Binder事务,让工作项被挂到目标线程的todo链表上,但内容却是恶意的。如果目标服务没有做好参数校验,就可能被利用。
- 等待队列欺骗:攻击者可以伪造一个Binder回复,唤醒一个正在等待的线程,让它处理一个本不该它处理的工作项。嗯,这个漏洞在CVE-2019-2215里就有体现。
安全建议:我个人建议,在开发系统服务时,一定要对Binder接口的调用频率做限流。另外,对于耗时操作,务必使用异步Binder调用(oneway),避免长时间占用线程池。我曾经见过一个服务,因为同步调用太多,导致整个系统的Binder通信都瘫痪了。
好了,关于Binder线程模型,我们就讲到这里。线程池的创建、binder_thread结构、工作队列和等待队列,这三块内容你吃透了,后面分析具体的漏洞案例就会轻松很多。