ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android Binder线程池启动全解析:从Zygote到驱动扩容

Android Binder线程池启动全解析:从Zygote到驱动扩容 有段时间我一直在琢磨一个问题为什么每个Android进程里都悄悄趴着一群名字里带binder的线程用ps -T一看新起的进程刚跑起来还没干什么活呢后面就已经跟着几个binder:xxx线程了。这些线程不是App自己创建的也不是Looper线程它们来自Android系统最核心的跨进程通信机制——Binder。这篇文章就把Android里Binder线程池的启动过程从源码层面完整过一遍搞清楚一个进程从无到有Binder线程池是怎么被拉起来的以及驱动是怎么按需扩容的。这篇文章适合正在看Android系统源码、做Framework开发、或者排查跨进程通信相关问题的同学。文中涉及的文件主要在frameworks/native/libs/binder/下对应内核驱动是drivers/android/binder.c。我会把启动链路上的关键函数逐个拆开讲有些地方补充了我在实际调试中遇到过的坑。1. 理解线程池之前先看Binder的线程模型分配1.1 一次跨进程调用谁来执行服务端逻辑Binder作为Android整个系统IPC的主动脉设计思想是代理调用客户端持有服务端的代理对象BpBinder通过驱动把事务发到真正的服务端BBinder上。问题是服务端进程不可能用主线程一直死等Binder消息因为主线程还要跑UI、跑消息循环也不可能每个Binder请求临时起一个线程那线程开销早就把IPC的延迟优势抵消了。所以Binder在用户态设计了一个线程池模型每个进程打开/dev/binder并映射内存后维护一组专门用来收发Binder事务的线程。这组线程平时全部阻塞在内核态的ioctl(BINDER_WRITE_READ)上也就是等驱动的read一旦有事务到来驱动唤醒其中一个空闲线程让它去执行服务端的业务逻辑。线程池的规模不需要太精确驱动会根据繁忙程度自动请求用户态创建新线程这就是后面要讲的BR_SPAWN_LOOPER机制。1.2 TLS单例ProcessState和IPCThreadState各自管什么分析启动过程前必须先分清两个容易搞混的类类作用域职责ProcessState进程级单例打开binder驱动、mmap映射、记录线程池上限和当前线程数、负责spawn新线程IPCThreadState线程级单例TLS管理当前线程进出Binder循环、与驱动交换数据的收发缓冲、执行具体命令对应的代码分别在ProcessState.cpp和IPCThreadState.cpp。理解了这个分工线程池的启动逻辑就清晰了进程级状态由ProcessState把关具体某个线程要开始服务Binder了由IPCThreadState的joinThreadPool负责。1.3 启动入口比你想的更早很多App开发者以为Binder线程池是某个Java方法触发启动的其实不是。对于普通的App进程和系统服务进程Binder线程池的启动被安排在Zygote fork出进程之后、Java代码还没真正开始跑业务之前的过渡阶段。这个入口在app_main.cpp的AppRuntime::onZygoteInit()里是native层主动触发的。换句话说Binder线程池是进程级基础设施比四大组件、Application都更早就绪。2. 启动源头从AppRuntime到ProcessState单例2.1 Java进程与native层的接驳点一个App进程被Zygote fork出来后执行链大致是Zygote fork - 子进程进入 AndroidRuntime::start - AppRuntime::onZygoteInit()AppRuntime是AndroidRuntime的子类定义在app_main.cpp里。它的onZygoteInit()实现非常简单virtual void onZygoteInit() { spProcessState proc ProcessState::self(); ALOGV(App process: starting thread pool.\n); proc-startThreadPool(); }就这两步拿进程级单例ProcessState::self()然后调startThreadPool()。接下来Java层才去执行ActivityThread.main()、跑主线程Looper。所以Binder基础设施的初始化确实比Application的onCreate早了十万八千里。2.2 ProcessState::self()打开驱动与mmap的全部真相ProcessState::self()是一个经典的懒加载单例spProcessState ProcessState::self() { Mutex::Autolock _l(gProcessMutex); if (gProcess ! NULL) { return gProcess; } gProcess new ProcessState(/dev/binder); return gProcess; }构造函数里做的事情基本决定了Binder通信的地基ProcessState::ProcessState(const char *driver) : mDriverName(driver) , mDriverFD(-1) , mVMStart(MAP_FAILED) , mMaxThreads(DEFAULT_MAX_BINDER_THREADS) { base::Resultint opened open_driver(); ... if (mDriverFD 0) { mVMStart mmap(nullptr, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0); } }这里有两个关键点。第一open_driver()不只是open(/dev/binder, O_RDWR | O_CLOEXEC)还要做版本协商static base::Resultint open_driver() { int fd open(driver, O_RDWR | O_CLOEXEC); ... BinderVersion version; ioctl(fd, BINDER_VERSION, version); if (version.protocol_version ! BINDER_CURRENT_PROTOCOL) { // 版本不匹配则直接关闭 } return fd; }第二mmap映射的大小是BINDER_VM_SIZE定义很有意思#define BINDER_VM_SIZE ((1 * 1024 * 1024) - (4096 * 2))也就是1MB - 8KB。为什么是1MB - 8KBBinder驱动在内核侧管理这块映射区时需要保留两个page作为保护页防止数据越界所以用户态映射时主动留出两个page的空间合起来正好凑够内核侧的完整映射区域。这里还有一个不少人不注意的细节mmap的权限是PROT_READ只读、不可写。用户态不能直接往这块共享区写数据所有写入都必须通过驱动完成也就是走ioctl的BC_TRANSACTION等命令。映射区在Binder里的角色是接收缓冲区驱动把事务数据拷贝到这里用户态再从这块区域读出来天然保证了单向数据流的安全性。2.3 为什么线程池上限默认是15ProcessState里有一个常量DEFAULT_MAX_BINDER_THREADS默认值是15static constexpr int DEFAULT_MAX_BINDER_THREADS 15;这是经过长期实践经验沉淀出来的一个值。Binder线程每个都要占用1MB映射区的一部分作为内核缓冲区上下文同时每个线程都是一个阻塞在ioctl上的内核任务线程太多会造成无谓的内存和调度开销太少又会在高并发服务场景下处理不过来。15这个值对绝大多数进程都够用而且驱动会在繁忙时动态申请增加所以默认值不太会影响实际吞吐。另外一些低内存设备上ProcessState构造时会把这个值调成4算是谷歌针对低端机的差异化策略。2.4 startThreadPool做了什么startThreadPool()的代码很短void ProcessState::startThreadPool() { AutoMutex _l(mLock); if (!mThreadPoolStarted) { mThreadPoolStarted true; spawnPooledThread(true); } }它通过mThreadPoolStarted标志做了幂等保护保证一个进程只有第一次调用会真的创建线程。注意传参是true代表这是主线程里的Binder线程。这一步做完进程就有了第一个专门服务Binder的线程线程池被正式激活。很多Native层自己写的服务进程如果忘掉这步就会出现服务注册上了、客户端调用超时的诡异现象后面单独讲。3. 线程诞生记spawnPooledThread到joinThreadPool3.1 PoolThread与线程名spawnPooledThread的实现Android 8以后void ProcessState::spawnPooledThread(bool isMain) { if (mThreadPoolStarted) { String8 name String8::format(binder:%d, isMain ? 0 : getpid()); spThread t new PoolThread(isMain); t-name name; t-run(); } }这里有一个容易踩坑的细节新线程的名字由isMain参数决定。如果是主线程名字是binder:0如果是后续驱动请求创建的非主线程名字是binder:pid。但注意Android后来对线程名做了截断处理pthread_setname_np最多只能接受16个字符含结尾的\0所以实际在看ps -T时会出现binder:12345、Binder:1234_1这类名字奇怪的截断方式就是这行代码导致的。PoolThread是一个极简的Thread子类class PoolThread : public Thread { public: explicit PoolThread(bool isMain) : mIsMain(isMain) {} protected: virtual bool threadLoop() { IPCThreadState::self()-joinThreadPool(mIsMain); return false; } private: bool mIsMain; };threadLoop()里没有循环因为真正的循环在joinThreadPool()里。注意threadLoop返回的是false这是逻辑兜底假如joinThreadPool真的返回了这个线程就结束。3.2 主线程和非主线程进入循环的方式不同IPCThreadState::joinThreadPool是整个启动过程里承上启下的核心函数简化后的逻辑大致是void IPCThreadState::joinThreadPool(bool isMain) { mOut.writeInt32(isMain ? BC_ENTER_LOOPER : BC_REGISTER_LOOPER); set_sched_policy(0, SP_BACKGROUND); status_t result; do { processPendingDatalock(); result talkWithDriver(); if (result NO_ERROR) { size_t IN mIn.dataAvail(); if (IN 4) continue; int32_t cmd mIn.readInt32(); result executeCommand(cmd); } } while (result ! -ECONNREFUSED result ! -EBADF result ! -ESRCH); mOut.writeInt32(BC_EXIT_LOOPER); talkWithDriver(); }注意这里写出去的命令主线程发BC_ENTER_LOOPER非主线程发BC_REGISTER_LOOPER。这两个命令在驱动里的含义完全不同BC_ENTER_LOOPER表示这个线程是Binder进程里第一个进入循环的主线程驱动会把它标记为BINDER_LOOPER_STATE_ENTEREDBC_REGISTER_LOOPER表示这是后续动态创建的薄记线程标记为BINDER_LOOPER_STATE_REGISTERED。这两个状态直接影响驱动判断当前有没有空闲线程也就是后面扩容逻辑的输入。3.3 talkWithDriver真正的死等循环talkWithDriver()干的事情说白了就是把mOut缓冲里积压的命令/事务交给驱动然后阻塞等待驱动把新的事务填充到mIn缓冲status_t IPCThreadState::talkWithDriver(bool doReceive) { binder_write_read bwr; bwr.write_size mOut.dataSize(); bwr.write_buffer (uintptr_t)mOut.data(); if (doReceive) { bwr.read_size mIn.dataCapacity(); bwr.read_buffer (uintptr_t)mIn.data(); } ... ioctl(mProcess-mDriverFD, BINDER_WRITE_READ, bwr); ... }驱动就是靠所有Binder线程都阻塞在ioctl的binder_thread_read上来实现有活干活、没活睡觉的线程池效果。注意mOut和mIn是复用缓冲executeCommand处理完一条命令后mIn直接清空重来所以Binder线程池不会像普通线程池那样有任务队列它更像是驱动侧有队列线程只负责消费。3.4 收到BR_TRANSACTION之后怎么执行到Java层的executeCommand遇到BR_TRANSACTION时会从mIn里解析出事务数据找到对应的BBinder调用它的transactcase BR_TRANSACTION: { const binder_transaction_data tr ...; ... spBBinder b ...; b-transact(tr.code, buffer, reply, tr.flags); }Java层进程里每一个Java实体比如ApplicationThread、各种XxxManager的服务端Stub最终都对应一个JavaBBinder它的transact会通过JNI调到Java的Binder.execTransact。所以Binder线程池里的线程就是这个JNI回调链的执行线程。这也是为什么你在Android Studio的debug线程列表里能看到很多名为Binder:xxx的线程恰好停在Binder.execTransact的native方法上。4. 驱动侧的扩容指令BR_SPAWN_LOOPER的生成条件4.1 什么时候驱动会申请新线程一个进程的Binder线程池不会一开始就创建15个线程而是按需增长的。增长指令完全由内核驱动发出。驱动在binder_transaction()里发现service端进程没有空闲线程可用时会生成一个BR_SPAWN_LOOPER命令塞到当前发起事务线程的响应里让它带回用户态。简化后的逻辑可以理解成进程当前所有Binder线程都处于繁忙状态正在执行某个事务没有阻塞在读上等待新任务。新到了一个需要服务端处理的事务。驱动统计发现已经请求创建的线程数还没达到max_threads上限。这三个条件同时满足时驱动就往客户端线程的响应里追加一个BR_SPAWN_LOOPER。用户态收到后走到case BR_SPAWN_LOOPER: mProcess-spawnPooledThread(false); break;于是新线程诞生进入joinThreadPool(false)然后注册为BC_REGISTER_LOOPER。整个过程不需要应用层主动干预完全是驱动反馈驱动的自动扩缩容。4.2 requested_threads、ready_threads、max_threads的关系驱动里每个binder进程binder_proc维护了几个关键计数器字段含义requested_threads驱动已经向用户态请求创建、但可能还没完全就绪的线程数ready_threads已经就绪、正阻塞等待新事务的空闲线程数max_threads允许的Binder线程数上限由用户态通过BC_SET_MAX_THREADS设置用户态通过ProcessState::setThreadPoolMaxThreadCount()可以下发这个上限对应Binder驱动协议里的BC_SET_MAX_THREADS命令。默认情况下ProcessState::self()初始化的mMaxThreads就是15然后在合适时机同步给驱动。这也是为什么高并发Binder服务场景下你需要确认是不是真的把上限改上去了而不是改了个用户态变量就以为生效了。4.3 Java层的线程池上限调整在Java层对应的入口是android_util_Binder.cpp暴露的native方法Framework层一般通过BinderInternal.setMaxThreads之类的方式暴露。SystemServer这类核心进程会在一些特定阶段调大上限。比如系统开机过程中系统服务集中注册、频繁跨进程通信的阶段如果保持默认15可能在某些机型上导致binder调用排队。我自己就遇到过类似案例一个系统服务刚启动时大量对外提供跨进程查询在choreographer节奏正常的情况下日志里出现偶发TransactionTooLargeException/ binder调用超时最后把该进程的Binder最大线程数调高才缓解。需要提醒的是Binder线程数不是越大越好。每个binder线程如果处于读等待状态驱动侧关联的内核缓冲区虽然不占满但线程自身的栈空间、调度实体都是成本而且线程间本来就有锁竞争过多线程可能造成不必要的调度抖动。建议修改前先抓取线程池的实际繁忙度再决定是否调整。5. 验证线程池是否正常命名规律与常见排查5.1 用ps -T观察真实状态线程池启动是否成功其实用最土的办法就能验证。随便找一个正在运行的App进程adb shell ps -T -p pid正常情况下能看到类似这样的输出binder:12345主binder线程以及后续扩容产生的多个Binder:12345_2之类的线程Java层Binder线程的名字通常还带一个下划线序号。如果这个进程只作为客户端、从没接收过任何跨进程调用可能只有一个主binder线程。但是如果连一个都没有那就要怀疑进程是否真的调用了startThreadPool()。但注意ps -T看到的线程名可能会有截断binder:12345和Binder:1234_2这种后缀差异不代表是两个不同的线程池只是命名来源和截断规则不同。Nativ底下binder线程名由spawnPooledThread里的String8::format(binder:%d, ...)决定Java侧Binder线程执行时会再被Binder.setThreadName重命名成带下划线的格式。5.2 Native服务进程不启动线程池的典型症状我踩过最经典的坑是写一个纯Native层系统服务时忘了调用ProcessState::self()-startThreadPool()。当时表现是服务用defaultServiceManager()-addService注册成功客户端getService也能拿到Binder但一调用跨进程方法就直接超时系统日志里看不到任何服务端执行痕迹。原因很简单addService只是把binder节点发布到ServiceManager它不要求服务进程具备接收事务的能力。客户端发起事务后驱动发现这个进程没有一个空闲的Binder线程在读等待事务就一直挂着直到超时返回。所以Native服务进程的模板代码必须在初始化阶段显式调用spProcessState proc(ProcessState::self()); proc-startThreadPool();有些早期的Native代码习惯用IPCThreadState::self()-joinThreadPool()或创建spawnPooledThread效果等同。这个步骤省略掉后果就是服务形同虚设。5.3 线程数持续增长的排查思路正常情况下一个进程的Binder线程池规模是稳定的进程启动早期因为服务注册和初始化调用会增长到某个水平之后基本不动。如果发现binder线程数量一直在涨甚至稳定状态下还在往上涨优先排查以下方向是否有大量跨进程调用同时到达且每个调用耗时过长导致驱动不断申请新线程。是否有Binder调用形成了互相等待的环线程全部卡死在业务代码里迟迟不回到读等待状态。是否有线程因为异常在业务逻辑里退出驱动侧的统计和用户态的mCurrentThreads出现偏差反复扩容。实际操作中抓一下debuggerd的栈看看这些binder线程到底停在什么函数里比猜要快得多。大部分时候线程停在你自己的业务代码里就说明问题不在线程池而在锁或耗时逻辑。5.4 调整线程池上限的正确姿势如果确认是并发需求导致线程不够用调整手段分两层。Native层可以直接ProcessState::self()-setThreadPoolMaxThreadCount(32);Java层在App进程里虽然一般不建议去动这个值但系统进程可以通过调用native方法实现。调整之后驱动侧的max_threads会同步更新之后驱动在繁忙时会继续把线程数涨到新上限。这里有一个我自己验证过的小细节setThreadPoolMaxThreadCount要在线程池真正开始接收高并发事务之前调用如果在运行中调整新增线程的生效会有一定延迟因为驱动必须在下一个BR_SPAWN_LOOPER时机才会按照新上限请求线程。说句实在话Binder线程池这套机制最精妙的地方是它把线程池应该开多少个线程这个动态难题交给了内核驱动。驱动掌握着所有线程的实时状态知道哪些在忙、哪些空闲、新事务的紧迫程度然后通过BR_SPAWN_LOOPER反向请求用户态补线程。用户态只需要负责创建线程、进入循环、以及处理命令完全不需要感知业务高峰。真正遇到问题的时候大部分精力也应该放在业务代码本身的耗时和锁竞争上线程池本身反而很少是瓶颈。希望这篇文章能帮你把这条启动链路彻底梳理清楚下次ps -T再看到那些binder线程时你知道它是一个多么精密的运转体系。
返回列表