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_threadreturn_error字段经常被利用。攻击者可以通过构造特殊的Binder请求,让驱动把错误码写入这个字段,从而绕过安全检查。嗯,这个我们后面讲漏洞案例时会详细展开。

5.3 工作队列与等待队列:驱动层的调度核心

好,接下来是重头戏——工作队列和等待队列。这两个队列是Binder驱动实现线程调度的核心数据结构。

先看一张我画的流程图,帮你理清它们的关系:

Binder驱动层:工作队列与等待队列调度流程 进程A(客户端) 进程B(服务端) BC_TRANSACTION Binder驱动 binder_proc.proc_todo 工作队列 (todo list) 等待队列 (wait queue) Binder线程池(最多16个线程) 每个线程有自己的 binder_thread.todo 和 binder_thread.wait BR_TRANSACTION 流程:客户端发送请求 → 驱动放入工作队列 → 唤醒等待线程 → 线程从todo取出工作项 → 处理请求

这张图里,我特意把工作队列等待队列分开画了。实际上,在驱动代码里,它们是这样配合的:

  1. 工作队列(todo list):每个binder_proc(进程)和binder_thread(线程)都有自己的todo链表。驱动收到请求后,先把工作项挂到目标进程的proc_todo上,然后尝试找一个空闲线程,把工作项迁移到线程的thread_todo上。
  2. 等待队列(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结构、工作队列和等待队列,这三块内容你吃透了,后面分析具体的漏洞案例就会轻松很多。