Binder 框架——整体架构与跨进程调用

文章内容

为了方便解释 Android Binder 框架运行逻辑,将整个 binder 单独抽出来,使其可以独立编译、分析和运行。

一、为什么 Android 选了 Binder

Linux 上做跨进程通信有不少现成方案:管道、Unix domain socket、共享内存、System V IPC。Android 选择 Binder,主要原因有两个。

  • 安全性。传统 Unix IPC 不要求调用方自证身份:一个进程往 socket 里写数据,接收方无从可靠知道对方的 PID 和 UID,除非调用方自己在数据包里写明(可以伪造)。Binder 把身份校验做进了内核驱动:每次事务,驱动自动附带上调用方的 pid 和 uid,服务端通过 IPCThreadState::getCallingPid() / getCallingUid() 拿到的是内核保证的值,无法伪造。

  • 面向对象语义。Binder 给用户态提供跨进程方法调用模型。客户端拿到的是一个本地代理对象(BpBinder),调用它的方法和调用本地对象语法一致。内核把方法调用翻译成事务、投递到目标进程、再把返回值传回来。对象可以从一个进程传到另一个进程,驱动维护跨进程引用计数。

二、整体架构与阅读主线

从调用方代码到内核驱动,Binder 可以分成五层:AIDL 生成的业务接口负责把方法签名固定下来,Bp* 代理把参数写进 Parcel,BpBinder 把类型化调用压成 handle、事务号和 flags,IPCThreadState 把事务写成 BC_TRANSACTION 并通过 ioctl(BINDER_WRITE_READ) 交给驱动,驱动再把事务投递给目标进程里的 BBinder/Bn*。ProcessState 在进程级提供底座:打开 /dev/binder、建立 mmap 事务缓冲区、缓存 handle 对应的代理对象、管理 binder 线程池。

Binder 五层调用栈

驱动侧维护 binder_proc、binder_thread、binder_node 和 handle 映射。客户端写入 BC_TRANSACTION,驱动定位目标对象,唤醒服务端线程并返回 BR_TRANSACTION;同步调用处理完后,服务端写 BC_REPLY,客户端等到 BR_REPLY 后继续执行。调用方 pid/uid、Binder 引用、fd 复制和死亡通知都在这条路径上完成。

Binder 调用驱动与事务返回流程

三、项目概览:宿主机上的 Android 12 Binder

3.1 工程边界

工程里的 Binder 框架来自 AOSP android-12。external/android/frameworks/native/libs/binder 是 libbinder,external/android/frameworks/native/cmds/servicemanager 是注册中心。Android 设备上依赖的 SELinux、VINTF、log 等能力在 compat/ 和 host 构建配置里做了适配;服务接口由项目内置的 tools/bin/aidl 生成 C++ 代码。README 解释它直接使用内核 binder_linux 驱动,响应体通过 ParcelFileDescriptor 传 fd,几百 MB 和几百字节下载占用相同量级的 Binder 事务流量。

3.2 四个进程

运行后最少有四个用户态进程:servicemanager 占用 handle 0,维护服务名到 Binder 对象的映射;http.service 负责 HTTP/1.1、TLS、Range 和 socket 读写;download.service 负责 .part 文件、断点续传、原子改名和进度聚合;download_client 只知道 download.service,不直接依赖 HTTP 代码。这个边界在 client/download.cpp 文件头也写得很清楚:客户端调用 enqueue,下载服务再调用 fetchToFd,进度沿原路 oneway 回来(client/download.cpp:1-14)。标题里的“三次跨进程调用”可以按观察点理解:启动期服务向 servicemanager 注册和查询,运行期 client 调 download,download 再调 http;如果把回调也纳入时序,方向会反过来形成一条 http -> download -> client 的 oneway 链。

这个项目适合从 main 入口读起,原因在于它把 Android 系统里分散在 init、servicemanager、系统服务和应用进程里的动作压缩到一个可执行文件周围。启动器负责创建进程和等待注册,servicemanager 负责名字空间,两个服务负责业务对象,客户端负责发起请求。每一层都短,进程边界又真实存在,读代码时能把“谁持有本地对象、谁只拿远端代理、哪一步进入驱动”分清楚。后文的图也按这个顺序展开:先看四进程拓扑,再看 libbinder 类关系,再看 launcher 总时序,接着拆 servicemanager 和下载链路。这样读还有一个好处:注册、查询、业务调用、回调、fd 传递都能在同一组日志里复现,不需要在完整 Android 系统里追多个无关服务。

四进程拓扑与内核驱动

这张拓扑图应突出两个方向:同步调用方向是 download_client -> download.service -> http.service;回调方向是 http.service -> download.service -> download_client。servicemanager 只参与注册和查找,不转发后续业务事务。

四、Binder 框架总览:代理、Parcel、驱动事务

4.1 用户态五层

这套项目沿用 Android C++ Binder 的常规分层。

/dev/binder 是内核入口,用户态通过 ioctl(BINDER_WRITE_READ) 写 BC_* 命令、读 BR_* 命令。ProcessState 是进程级单例,打开驱动、mmap 事务缓冲区、维护 handle 到 BpBinder 的缓存、管理线程池;初始化只执行一次(ProcessState.cpp:91-122),mmap 在构造函数里完成(ProcessState.cpp:436-465)。IPCThreadState 是线程级状态机,transact 发送 BC_TRANSACTION,waitForResponse 等待 BR_REPLY 或 BR_TRANSACTION_COMPLETE(IPCThreadState.cpp:700-755、IPCThreadState.cpp:875-973)。BpBinder 和 BBinder 形成代理/桩模型;AIDL 生成的 Bp* 负责写 Parcel,Bn* 负责 onTransact 分发。Parcel 承载参数、返回值、Binder 引用和 fd。

4.2 类关系

libbinder 核心类关系

图里可以把 IInterface 放在业务接口上方:IHttpService 派生出 BpHttpService 和 BnHttpService,HttpService 继承 BnHttpService;IDownloadService 同理。客户端拿到的 sp<IDownloadService> 实际包装了远端 BpBinder;服务端收到事务后进入 BBinder::transact,再由 AIDL 生成的 Bn*::onTransact 调业务方法。ProcessState 提供进程内 handle 缓存,IPCThreadState 提供每个线程的发送、等待和分发循环。

AIDL 生成的事务号体现了“方法调用变成 code”的过程。下面这个片段说明 fetchToFd 是 IHttpService 的第三个业务事务,最终会作为 binder_transaction_data.code 进入驱动。

cpp
// newRequestId 对应第一个业务事务号static constexpr uint32_t TRANSACTION_newRequestId = ::android::IBinder::FIRST_CALL_TRANSACTION + 0;// getString 对应第二个业务事务号static constexpr uint32_t TRANSACTION_getString = ::android::IBinder::FIRST_CALL_TRANSACTION + 1;// fetchToFd 对应第三个业务事务号static constexpr uint32_t TRANSACTION_fetchToFd = ::android::IBinder::FIRST_CALL_TRANSACTION + 2;// cancel 被声明为 oneway,但仍然有自己的事务号static constexpr uint32_t TRANSACTION_cancel = ::android::IBinder::FIRST_CALL_TRANSACTION + 3;

4.3 事务的生命周期

发起方代理把参数写进 Parcel,再调用 remote()->transact(code, data, reply, flags)。IPCThreadState::transact 把它封装成 BC_TRANSACTION 写入 mOut(IPCThreadState.cpp:700-704);talkWithDriver 用 BINDER_WRITE_READ 把命令交给驱动(IPCThreadState.cpp:975-1029)。驱动根据 handle 找到目标进程里的 binder_node,把事务投递为 BR_TRANSACTION。目标线程在 executeCommand 里读出 binder_transaction_data,把数据区交给 Parcel,再调用 BBinder::transact 或 context object 的 transact(IPCThreadState.cpp:1204-1279)。同步事务处理完后发 BC_REPLY,调用方等到 BR_REPLY 后继续执行。

handle 和对象的映射由驱动维护。用户态只看到本进程内的整数 handle;handle 0 是注册中心,ProcessState::getStrongProxyForHandle(0) 会先发 PING_TRANSACTION,没有 context manager 时返回 null(ProcessState.cpp:274-324)。

五、main 入口

5.1 参数分发

service/main.cpp 编译为 service_manager。它可以当启动器,也可以变成单个服务进程。

cpp
// 没有参数时进入 all 模式const char* mode = argc > 1 ? argv[1] : "all";// all 模式负责拉起完整服务树if (strcmp(mode, "all") == 0) return runManager();// http 模式只运行 http.serviceif (strcmp(mode, "http") == 0) return httpsvc::runService();// download 模式只运行 download.serviceif (strcmp(mode, "download") == 0) return downloadsvc::runService();// servicemanager 模式 exec 真正的 servicemanager 二进制if (strcmp(mode, "servicemanager") == 0) return execServiceManager();

5.2 launcher 启动顺序

runManager 的启动顺序是 /dev/binder 探测、确保 servicemanager 存在、启动 http.service、启动 download.service、打印注册表、进入守护循环。binderIsReady 只检查 /dev/binder 是否存在(service/main.cpp:148-153)。ensureServiceManager 先用 ProcessState::self()->getContextObject(nullptr) 探测 handle 0;没起来就找 servicemanager 二进制,fork 后每 250ms 轮询一次,最多 20 次(service/main.cpp:163-200)。

cpp
// handle 0 已经可 ping 通时直接复用if (serviceManagerIsUp()) {// 已存在的 servicemanager 归外部所有    return true;// 结束已有实例分支}// 在可执行文件附近查找 servicemanagerconst std::string path = findBinary("servicemanager");// fork 并 exec servicemanagergServiceManager.pid = spawn(path, nullptr);// 标记由当前 launcher 负责清理gOwnServiceManager = true;// 每 250ms 轮询一次,总计最多 5sfor (int i = 0; i < 20; ++i) {// 等待子进程进入 context manager 状态    usleep(250 * 1000);// handle 0 可用后启动完成    if (serviceManagerIsUp()) return true;// 继续轮询}

两个服务的启动由 startService 完成。它用 /proc/self/exe 找到自己,fork 后给子进程传入 http 或 download 参数,再用 checkService 等待服务名注册成功(service/main.cpp:204-231)。runManager 先启动 http.service,再启动 download.service。

launcher 启动三个进程的总时序

这张总时序图应覆盖完整启动:launcher ping handle 0,必要时 spawn servicemanager;然后 spawn 自身成为 http.service,等待 http.service 注册;再 spawn 自身成为 download.service,等待 download.service 注册;随后 listServices 打印 download.service、http.service、manager(service/main.cpp:252-257)。

5.3 进程守护与退出

cleanup 的顺序是 download、http、servicemanager(service/main.cpp:137-140)。如果 servicemanager 是外部已有实例,gOwnServiceManager 为 false,则launcher 不会进行销毁。SIGINT 和 SIGTERM 统一走 cleanup(service/main.cpp:143-145、service/main.cpp:236-237)。守护循环里只要任一子进程退出,就记录退出者并关闭剩余服务树(service/main.cpp:260-284)。这能避免只剩半棵服务树继续持有旧 Binder 对象。

六、servicemanager:handle 0 的注册与查找

6.1 启动成为 context manager

service_manager servicemanager 最终执行的是 AOSP 的 servicemanager 二进制。它用 ProcessState::initWithDriver("/dev/binder") 初始化驱动,设置最大 binder 线程数为 0,创建 ServiceManager 对象,把 "manager" 自注册,然后把该对象设置为 context object 并调用 becomeContextManager。BinderCallback 把 binder fd 加进 Looper,有输入事件时调用 IPCThreadState::handlePolledCommands(同文件:39-60、132-139)。所以 servicemanager 的事件循环是 Looper 驱动的单线程分发。

becomeContextManager 里会尝试 BINDER_SET_CONTEXT_MGR_EXT,在无 SELinux 的 host 上有本地补丁先探测 /sys/fs/selinux,必要时走旧 ioctl(ProcessState.cpp:148-194)。这个适配让普通 Linux 内核上的 /dev/binder 能稳定接受后续事务。

6.2 addService 链路

服务端注册时调用 defaultServiceManager()->addService(name, service)。在 IServiceManager.cpp 里,老 C++ 接口由 ServiceManagerShim 包装到 AIDL android.os.IServiceManager,addService 直接调用真实 AIDL 代理(IServiceManager.cpp:64-75、269-275)。事务到达 servicemanager 进程后进入 ServiceManager::addService,做权限、服务名、VINTF 检查,并把 (name, binder) 写入 mNameToService。

cpp
// 用服务名作为 map key,覆盖旧条目mNameToService[name] = Service {// 保存服务端传来的 Binder 引用    .binder = binder,// 保存是否允许 isolated 进程查询    .allowIsolated = allowIsolated,// 保存 dumpsys 优先级    .dumpPriority = dumpPriority,// 保存注册方调试 pid    .debugPid = ctx.debugPid,// 结束 Service 聚合初始化};

片段来自 external/android/frameworks/native/cmds/servicemanager/ServiceManager.cpp:281-287。它说明注册中心最终就是一张服务名到 Binder 引用的表。真正的跨进程部分发生在到达这里之前:服务对象作为 sp<IBinder> 被写入 Parcel,驱动把本地 BBinder 转成其他进程可持有的 handle。

6.3 checkService 链路

查找服务走 ServiceManager::checkService,它调用 tryGetService(name, false);找不到就返回 null,不尝试懒启动(ServiceManager.cpp:189-228)。launcher、client、download service 都用这条路径:launcher 用它等待两个服务注册完成(service/main.cpp:214-223);client 用它拿 download.service(client/download.cpp:299-306);download service 用它轮询 http.service(DownloadService.cpp:196-208)。

servicemanager 注册与查找时序

这张分时序图适合拆两条线:addService 从服务端进程进入 handle 0,把 binder 引用存入 mNameToService;checkService 从客户端进程进入 handle 0,返回服务 binder 引用。拿到返回值后,后续业务调用直接发给目标服务进程,不再绕回 servicemanager。

七、服务端进程:注册与线程模型

7.1 http.service

http.service 的入口是 httpsvc::runService。它拿到 ProcessState,把最大 binder 线程数设为 8,创建 HttpService,用 AIDL 常量 IHttpService::SERVICE_NAME() 得到 "http.service",注册到 servicemanager,然后启动线程池并让主线程加入池(HttpService.cpp:258-280)。fetchToFd 会在一个 binder 线程上阻塞完整传输周期,所以线程池大小直接限制并发下载数;这点在头文件注释里也写明了(HttpService.h:4-11)。

下面的片段说明 http 服务的注册和线程池形态。

cpp
// 每个 fetchToFd 会占住一个 binder 线程ps->setThreadPoolMaxThreadCount(8);// 创建本地 BBinder 服务对象sp<HttpService> service = sp<HttpService>::make();// 使用 AIDL 常量作为服务名const String16 name = IHttpService::SERVICE_NAME();// 注册到 handle 0 对应的 servicemanagerconst auto status = android::defaultServiceManager()->addService(name, service);// 启动 binder 线程池ps->startThreadPool();// 主线程进入 binder 分发循环IPCThreadState::self()->joinThreadPool();

7.2 download.service

download.service 的 runService 也设置 8 个 binder 线程,但它先 startThreadPool 再 addService。服务进入注册表后事务随时可能到达,池要先准备好(DownloadService.cpp:480-492)。enqueue 收到客户端请求后创建 Task,记录 url、目标路径、回调 binder 和状态,再用 IPCThreadState::getCallingPid/getCallingUid 打印调用者身份,随后起 detached worker 跑 runTask(DownloadService.cpp:356-389)。这些 pid/uid 来自驱动填充的 binder_transaction_data.sender_pid/sender_euid,IPCThreadState::executeCommand 在分发前把它们写入线程状态(IPCThreadState.cpp:1244-1247)。

cpp
// 取当前 binder 事务的调用方身份IPCThreadState* ipc = IPCThreadState::self();// 打印内核提供的 pid 和 uidprintf("[download] pid=%d uid=%d enqueue(#%d, \"%s\" -> \"%s\")\n", ipc->getCallingPid(),// 继续打印 uid、任务 id、url 和目标路径       ipc->getCallingUid(), task->id, url.c_str(), destPath.c_str());// 下载任务脱离 binder 线程执行std::thread([this, task] { runTask(task); }).detach();

enqueue 只是接收任务,耗时下载不占用处理客户端请求的 binder 线程。

八、下载的完整调用链

8.1 总链路

一次下载的完整跨进程调用链

全链路分成五段。客户端从 handle 0 查 download.service,拿到 IDownloadService 代理;客户端把 IDownloadCallback 作为 Binder 引用传给 enqueue;download 的 worker 查 http.service,创建 ProgressRelay,把 .part fd 包进 ParcelFileDescriptor 传给 fetchToFd;http 进程一边读 socket 一边写 fd;http 的进度 oneway 回到 download,download 聚合后再 oneway 回到 client,完成、失败、取消也沿 IDownloadCallback 回去。

8.2 客户端:只依赖 download.service

客户端启动后先 ProcessState::self(),再 startThreadPool,因为进度回调会打回客户端进程,没有 binder 线程池就没人处理(client/download.cpp:285-288)。随后调用 defaultServiceManager(),用 checkService("download.service") 查找服务,interface_cast<IDownloadService> 转成类型化代理,linkToDeath 注册死亡通知,然后调用 enqueue(url, destPath, callback)(client/download.cpp:290-324)。Callback::await 通过条件变量等终态回调(client/download.cpp:196-201、343-354)。

cpp
// 启动线程池,接收服务端回调ps->startThreadPool();// 获取 handle 0 上的 servicemanager 代理auto sm = android::defaultServiceManager();// 使用 AIDL 常量查找 download.servicesp<IBinder> binder = sm->checkService(serviceName);// 把裸 IBinder 转成类型化 IDownloadService 代理sp<IDownloadService> service = android::interface_cast<IDownloadService>(binder);// 注册死亡通知,服务进程死亡时唤醒等待binder->linkToDeath(recipient);// 把客户端回调 Binder 引用传入服务端Status status = service->enqueue(options.url, options.destPath, callback, &downloadId);

8.3 download.service:服务调服务

runTask 先确保目标目录存在,读取 <destPath>.part 的长度作为 resumeFrom,打开 .part 时没有 O_TRUNC,这就是续传的落点(DownloadService.cpp:245-263)。接着通过 httpService() 查找并缓存 IHttpService 代理;缓存失效时用 isBinderAlive() 检查,查找失败时最多轮询 10s(DownloadService.cpp:188-213)。拿到 http 代理后先 newRequestId,再创建 ProgressRelay,对 fd 做一次 dup 包进 ParcelFileDescriptor,最后调用 fetchToFd。注释写明这个调用会阻塞完整传输,所以每个下载都有自己的 worker 线程(DownloadService.cpp:271-305)。

cpp
// .part 文件长度就是续传起点const int64_t resumeFrom = fileSize(task->partPath);// 打开 .part,不截断已有内容unique_fd fd(open(task->partPath.c_str(), O_WRONLY | O_CREAT | O_CLOEXEC, 0644));// 查找并缓存 http.service 代理sp<IHttpService> http = httpService(&error);// 复制 fd 给 Parcel 使用ParcelFileDescriptor sink{unique_fd(dup(fd.get()))};// 同步调用 http.service,直到传输结束status = http->fetchToFd(requestId, task->url, sink, resumeFrom, relay, &written);

下载服务在 Binder 拓扑里同时是服务端和客户端:对 download_client 是服务端,对 http.service 是客户端。

8.4 http.service:fd passing 与进度回调

IHttpService.aidl 直接声明 fetchToFd 的第三个参数是 in ParcelFileDescriptor sink,回调是可空的 IHttpProgressCallback(IHttpService.aidl:54-72)。生成的代理代码会写入 requestId、url、Parcelable fd、resumeFrom 和 callback binder,再同步 transact(TRANSACTION_fetchToFd)(IHttpService.cpp:107-137)。服务端 onTransact 读回 ParcelFileDescriptor 和 callback,然后调用业务实现(IHttpService.cpp:289-321)。

HttpService::fetchToFd 开头有一句很关键的注释:fd 在 Parcel 被读取时已由内核 dup 到当前进程,本次调用期间有效(HttpService.cpp:130-137)。随后它按 HTTP 响应的 startOffset 对 fd lseek,如果服务器拒绝 Range 且从 0 返回,就截断旧 .part;写入过程中定期调用 IHttpProgressCallback;成功后返回本次写入字节数(HttpService.cpp:159-226)。

cpp
// 读取 ParcelFileDescriptor 中的进程内 fdconst int fd = sink.get();// 按服务端实际返回的起点定位文件if (lseek(fd, info.startOffset, SEEK_SET) < 0) {// 记录定位失败,停止传输    writeError = std::string("lseek: ") + strerror(errno);// 通知 HTTP 客户端停止    return false;// 结束错误分支}// 进度回调存在且达到阈值时上报callback->onBytesReceived(requestId, written, total);

片段来自 service/http/HttpService.cpp:135-197。fd 的序列化细节可以从 Parcel::writeDupParcelFileDescriptor 和 readUniqueParcelFileDescriptor 看到:写侧先 fcntl(..., F_DUPFD_CLOEXEC),读侧再 fcntl 复制为接收进程自己的 fd(Parcel.cpp:1183-1193、Parcel.cpp:1912-1924)。

8.5 回调链与 oneway

IHttpProgressCallback.aidl 和 IDownloadCallback.aidl 都把接口声明为 oneway(IHttpProgressCallback.aidl:9-12、IDownloadCallback.aidl:7-12)。生成代码里,onResponseStart、onBytesReceived、onStarted、onProgress、onCompleted 等方法都以 IBinder::FLAG_ONEWAY 调用 remote()->transact(IHttpProgressCallback.cpp:62-96、IDownloadCallback.cpp:58-202)。这意味着 http 线程发出进度事务后不等对端业务方法执行完;download 转发给 client 也是同样模式。AIDL 注释还写明发往同一 binder 对象的 oneway 事务按调用顺序投递(IDownloadCallback.aidl:7-10)。

download 的 ProgressRelay 把 http 侧“本次响应写了多少”转换成“整个下载已完成多少”。onResponseStart 记录 resumedFrom、total、downloaded,并回调客户端 onStarted;onBytesReceived 叠加 resumed 前缀、限频、计算速度,再回调客户端 onProgress(DownloadService.cpp:95-147)。终态由 runTask 在 rename 成功后调用 onCompleted,失败和取消分别调用 onFailed、onCanceled(DownloadService.cpp:223-243、330-349)。

8.6 响应体为什么不走 Parcel

IHttpService.aidl 的注释写得很明确:响应体不放进 Parcel,fetchToFd 写调用方传入的 fd;Binder 事务缓冲区每进程约 1MB,所有在途事务共享(IHttpService.aidl:12-16)。把文件内容塞进 Parcel 会碰到缓冲区大小、拷贝和并发占用问题。fd passing 只把“写到哪里”的能力跨进程传过去,真正的大数据由 http 进程从 socket 读出后写入内核文件对象;Binder 只承载 requestId、url、fd、offset、回调引用和小型状态码。

这也是本项目最适合作为 Binder 学习材料的地方:一次下载同时覆盖服务注册/发现、服务调服务、同步事务、Binder 引用传递、oneway、fd passing、调用者身份、死亡通知和 service-specific error。README 的能力表把这些点逐项列出(README.md:161-174)。

九、ProcessState:mmap 与线程池

9.1 进程级入口

前面从 main.cpp 看到了四个进程的边界。每个进程第一次接触 Binder 时,都会走到 ProcessState::self() 或 initWithDriver()。ProcessState::init 用 [[clang::no_destroy]] 保存进程级单例,用 std::call_once 保证初始化只跑一遍;如果传入的驱动不可访问,会退回 /dev/binder(ProcessState.cpp:91-122)。这解释了为什么 download_client、http.service、download.service 各自都有一份 Binder 进程状态:单例只在进程内唯一,跨进程没有共享 C++ 对象。

看 mmap 这一小段,可以确认 Binder 事务缓冲区从哪里来。

cpp
// 每个进程映射约 1MB,预留两个页#define BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2)// 构造函数先打开 binder 驱动, mDriverFD(open_driver(driver))// fd 有效时才建立事务缓冲区映射if (mDriverFD >= 0) {// 把 binder 驱动映射成只读用户态地址区    mVMStart = mmap(nullptr, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE, mDriverFD, 0);// mmap 失败后关闭驱动 fd    if (mVMStart == MAP_FAILED) {// 进程不再持有可用 binder 驱动        mDriverFD = -1;// 结束失败分支    }// 结束 fd 有效分支}

片段来自 external/android/frameworks/native/libs/binder/ProcessState.cpp:44、ProcessState.cpp:436-459。从这里能看到事务数据有一块进程私有的用户态映射区,内核驱动通过这块区域投递 binder_transaction_data 指向的数据和对象偏移表。

9.2 handle 缓存与线程池

handle 是进程内的远端对象编号。getStrongProxyForHandle 先通过 lookupHandleLocked 找到 handle_entry,已有 BpBinder 且弱引用可提升时复用;没有缓存或旧对象已经回收时创建新的 BpBinder(ProcessState.cpp:274-341)。handle 0 有特殊路径:创建代理前先向 handle 0 发 PING_TRANSACTION,返回 DEAD_OBJECT 就说明 context manager 不在(ProcessState.cpp:292-324)。这正好对应 launcher 的 serviceManagerIsUp() 探测。

线程池是服务端处理外来事务的工作队列。startThreadPool 只把 mThreadPoolStarted 置位并创建一个主 binder 线程(ProcessState.cpp:139-145);spawnPooledThread 创建 PoolThread 并运行,线程名带进程 pid 和序号(ProcessState.cpp:355-370)。线程真正等事务的地方在 IPCThreadState::joinThreadPool。http.service 把上限设成 8,因为 fetchToFd 会占住 binder 线程;download.service 的传输在 worker 线程里跑,binder 池更多承担客户端调用和回调落点。设计上,ProcessState 管“进程能接多少 Binder 活”,业务线程只负责自己的阻塞工作,二者不要混在一起。

这个设计把资源边界放在进程级:mmap 缓冲区按进程分配,handle 表按进程缓存,线程池上限也按进程告诉驱动。客户端进程只需要少量线程接回调;HTTP 服务进程要为慢网络保留多个 binder 线程;下载服务把长任务挪到 detached worker,避免 enqueue 的 Binder 入口被下载耗时拖住。读本文的三次跨进程调用时,可以把 ProcessState 看作每个进程的 Binder 底座:它不理解业务接口,却决定了这个进程能否打开驱动、能缓存哪些远端代理、能同时处理多少外来事务。

十、IPCThreadState:talkWithDriver 与命令循环

10.1 发起事务

IPCThreadState 是线程局部对象。AIDL 代理最终会调用 IPCThreadState::transact(handle, code, data, reply, flags),它把 handle、事务号、Parcel 数据写成 BC_TRANSACTION,同步调用随后进入 waitForResponse(IPCThreadState.cpp:700-755)。waitForResponse 循环调用 talkWithDriver,遇到 BR_REPLY 就把驱动返回的 buffer 挂到 reply Parcel;遇到 BR_TRANSACTION_COMPLETE、BR_DEAD_REPLY、BR_FAILED_REPLY 分别处理 oneway 完成、远端死亡和失败(IPCThreadState.cpp:875-973)。

看 talkWithDriver 可以看到一次系统调用同时包含写命令和读结果。

cpp
// 写缓冲区大小来自 mOut 中累积的 BC_* 命令bwr.write_size = outAvail;// 写缓冲区指向 mOut 的底层数据bwr.write_buffer = (uintptr_t)mOut.data();// 需要读时把 mIn 的容量交给驱动bwr.read_size = mIn.dataCapacity();// 读缓冲区指向 mIn 的底层数据bwr.read_buffer = (uintptr_t)mIn.data();// 通过 BINDER_WRITE_READ 与 binder 驱动交换命令if (ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) >= 0)// ioctl 成功后把状态记为 NO_ERROR    err = NO_ERROR;

片段来自 external/android/frameworks/native/libs/binder/IPCThreadState.cpp:991-1029。它说明用户态没有单独的“发送系统调用”和“接收系统调用”,一次 BINDER_WRITE_READ 就能把 BC_TRANSACTION 送下去,并把可能已经到达的 BR_* 命令带回来。

10.2 接收事务

服务端线程在 joinThreadPool 中先写入 BC_ENTER_LOOPER 或 BC_REGISTER_LOOPER,然后反复调用 getAndExecuteCommand(IPCThreadState.cpp:617-648)。getAndExecuteCommand 读驱动命令、增加执行线程计数、调用 executeCommand,执行完再降低计数;线程池饥饿超过 100ms 会打日志(IPCThreadState.cpp:519-564)。这就是 http.service 多并发下载时为什么要增大线程池上限:每个阻塞的 fetchToFd 都算一个正在执行的 binder 线程。

当 BR_TRANSACTION 到达,调用者身份会在分发前写进当前线程状态。

cpp
// 驱动返回的事务数据挂到临时 Parcelbuffer.ipcSetDataReference(reinterpret_cast<const uint8_t*>(tr.data.ptr.buffer), tr.data_size,// 对象偏移表也挂到同一个 Parcel                            reinterpret_cast<const binder_size_t*>(tr.data.ptr.offsets), tr.offsets_size/sizeof(binder_size_t), freeBuffer);// 调用方 pid 来自驱动填充的 sender_pidmCallingPid = tr.sender_pid;// 调用方 uid 来自驱动填充的 sender_euidmCallingUid = tr.sender_euid;// 普通服务对象走 BBinder::transacterror = reinterpret_cast<BBinder*>(tr.cookie)->transact(tr.code, buffer, &reply, tr.flags);

DownloadService::enqueue 里打印的 pid/uid 来自驱动在事务投递时附带的调用者身份,客户端无法靠 Parcel 字段伪造。

IPCThreadState 的关键取舍是把“发起调用”和“服务调用”放进同一个线程对象里。客户端线程发 enqueue 时,它用同一个 talkWithDriver 等 reply;服务端池线程收到 BR_TRANSACTION 时,也用同一个对象读命令、设置调用方身份、执行 BBinder::transact、发送 reply。这样同步调用可以自然嵌套:download worker 调 http 时,它自己成为 http 的客户端;http 回调 download 时,download 进程里的 binder 线程又成为服务端。跨进程调用看起来像普通 C++ 方法,靠的就是这层状态机把 BC/BR 协议折叠成阻塞调用和事件分发。

还有一个细节会影响调试判断:mIn 里已有未处理命令时,talkWithDriver 会先消化旧数据,再决定是否继续读驱动。这让一个线程可以连续处理驱动塞进来的多条 BR 命令,减少系统调用次数,也避免回调、死亡通知、reply 混在一起时丢掉顺序。

十一、BpBinder 与 BBinder:handle 转译

11.1 BpBinder:远端对象的本地代理

BpBinder 只持有远端对象的句柄。构造函数把 BinderHandle 存进 mHandle,binderHandle() 返回里面的整数(BpBinder.cpp:150-186)。这个整数只在当前进程有意义;同一个服务对象在不同客户端进程里可能对应不同 handle。ProcessState 负责缓存 handle -> BpBinder,BpBinder 负责把方法调用继续交给 IPCThreadState。

下面这段是代理转发的关键。

cpp
// 先判断是否为 RPC binderif (CC_UNLIKELY(isRpcBinder())) {// RPC binder 走 RpcSession,本文不分析这条路径    status = rpcSession()->transact(rpcAddress(), code, data, reply, flags);// 普通内核 binder 进入这个分支} else {// binderHandle() 是当前进程里的远端对象编号    status = IPCThreadState::self()->transact(binderHandle(), code, data, reply, flags);// 结束普通 binder 分支}// DEAD_OBJECT 后代理永久标记为不可用if (status == DEAD_OBJECT) mAlive = 0;

片段来自 external/android/frameworks/native/libs/binder/BpBinder.cpp:274-281。也就是说,IDownloadService::enqueue、IHttpService::fetchToFd 这些类型化调用,最终都会退化成“handle + code + Parcel + flags”。

11.2 BBinder:服务端桩与死亡通知

服务端对象继承 BBinder。驱动把事务投给目标进程后,IPCThreadState::executeCommand 从 tr.cookie 找回本地 BBinder*,再调用它的 transact(IPCThreadState.cpp:1265-1272)。BBinder::transact 处理少数系统事务,如 PING_TRANSACTION、DEBUG_PID_TRANSACTION,其余交给虚函数 onTransact;AIDL 生成的 BnHttpService::onTransact、BnDownloadService::onTransact 就接在这里。

cpp
// 每次事务从 Parcel 起点开始读data.setDataPosition(0);// 默认没有错误status_t err = NO_ERROR;// 按事务号分发switch (code) {// 普通业务事务落到 onTransact    default:// AIDL 生成的 Bn* 覆盖这个虚函数        err = onTransact(code, data, reply, flags);// 结束 default 分支        break;// 结束 switch}

片段来自 external/android/frameworks/native/libs/binder/Binder.cpp:182-204。它说明服务端桩的入口非常薄,真正的参数反序列化和业务方法调用在 AIDL 生成层完成。

死亡通知也是代理侧维护的。BpBinder::linkToDeath 第一次注册时会创建 mObituaries,向 IPCThreadState 请求死亡通知并 flush 命令(BpBinder.cpp:290-323)。当驱动报告远端死亡,sendObituary 把 mAlive 置 0,并逐个调用 DeathRecipient::binderDied(BpBinder.cpp:364-406)。第八章客户端在 enqueue 前对 download.service 的 IBinder 调 linkToDeath,正是靠这条链路避免服务进程死后 await() 永远等不到终态。

handle 转译让对象边界变得清晰。本地服务对象只在拥有它的进程里是 BBinder*;一旦穿过驱动,另一端看到的就是 BpBinder(handle)。驱动维护 handle 与 binder_node 的关系,用户态不需要知道目标进程的地址。这个规则同样适用于回调:客户端传入的 Callback 在 client 进程里是 BnDownloadCallback,在 download 进程里变成远端代理。download 保存它并调用 onProgress 时,调用方向反过来,模型没有变化。

这套模型也解释了为什么 interface_cast 只做类型化包装,不会把远端对象“下载”到本进程。checkService 返回的是一个 IBinder,interface_cast<IDownloadService> 得到的是 BpDownloadService;它的方法体仍然写 Parcel、发事务、等 reply。服务端实现始终留在原进程里,跨进程边界由 handle 保持。

所以排查 Binder 问题时,要先分清手里拿的是本地桩还是远端代理。

十二、Parcel:序列化协议

12.1 数据区与对象偏移表

Parcel 有自己的内部结构。普通整数、字符串等写进 mData,Binder 对象和 fd 还会在 mObjects 里记录偏移。writeInplace 会按 pad_size 对齐写入位置,必要时扩容(Parcel.cpp:779-823);writeObject 把 flat_binder_object 写入数据区,并把该对象在数据区里的偏移放进 mObjects,供驱动扫描和修正(Parcel.cpp:1310-1352)。这就是为什么 Binder 能在同一份 Parcel 里混放基础类型、对象引用和 fd。

Binder 引用写入时会按本地/远端方向选择不同类型。

cpp
// 本进程没有本地 BBinder,说明传的是远端代理if (!local) {// 远端代理写成 BINDER_TYPE_HANDLE    obj.hdr.type = BINDER_TYPE_HANDLE;// handle 是当前进程里的远端对象编号    obj.handle = handle;// 本地对象写成 BINDER_TYPE_BINDER} else {// 本地 BBinder 暴露为 binder node    obj.hdr.type = BINDER_TYPE_BINDER;// cookie 保存本地对象指针,目标进程不会直接使用这个地址    obj.cookie = reinterpret_cast<uintptr_t>(local);// 结束本地对象分支}

片段来自 external/android/frameworks/native/libs/binder/Parcel.cpp:224-258。这段代码也印证了:同一个 sp<IBinder> 写进 Parcel 时,会根据它在当前进程是本地对象还是远端代理,生成不同的 flat_binder_object。

12.2 反序列化与 fd passing

读回 Binder 引用时,unflattenBinder 反向处理:BINDER_TYPE_BINDER 用 cookie 找本进程里的本地对象;BINDER_TYPE_HANDLE 通过 ProcessState::self()->getStrongProxyForHandle(flat->handle) 创建或复用 BpBinder(Parcel.cpp:273-310)。这一步和第九章的 handle 缓存连在一起。客户端把 IDownloadCallback 传给 download service 时,客户端写出的是本地 BnDownloadCallback;download 进程读到后得到的是一个代理,后续回调就能跨进程打回客户端。

fd 的路径更直接,本文的下载响应体就依赖它。

cpp
// 写侧先复制一份 fd,避免把调用方原 fd 所有权交出去int dupFd = fcntl(fd, F_DUPFD_CLOEXEC, 0);// 把复制出来的 fd 写成 ParcelFileDescriptorstatus_t err = writeParcelFileDescriptor(dupFd, true /*takeOwnership*/);// 读侧先从 ParcelFileDescriptor 读到驱动传来的 fdint got = readParcelFileDescriptor();// 再复制成当前进程拥有的 unique_fdval->reset(fcntl(got, F_DUPFD_CLOEXEC, 0));

片段来自 external/android/frameworks/native/libs/binder/Parcel.cpp:1183-1193、Parcel.cpp:1912-1924。它说明 fd passing 传递的是内核文件描述对象的引用能力,不传文件内容。第八章说响应体不走 Parcel,落点就在这里:Binder 事务缓冲区约 1MB,适合控制参数和对象引用,不适合承载下载 body。

Parcel 的设计目标是让驱动能识别“普通字节”和“需要内核介入的对象”。基础类型按顺序读写,调用双方依赖 AIDL 生成代码保持顺序一致;对象偏移表告诉驱动哪里有 Binder 引用或 fd,需要做引用计数、handle 分配、fd 复制。下载链路里,url、requestId、resumeFrom 是普通数据,callback 是 Binder 引用,sink 是 fd。三类数据进入同一个 Parcel,但内核处理方式完全不同。理解这一点后,fetchToFd 的参数设计就很直观:控制面走 Parcel,数据面走 fd。

十三、ServiceManager:注册与发现

13.1 defaultServiceManager 与查询策略

defaultServiceManager() 用 std::call_once 缓存全局代理;内部循环调用 ProcessState::self()->getContextObject(nullptr),拿不到 handle 0 就每秒 sleep 后重试(IServiceManager.cpp:94-110)。这解释了第五章 launcher 为什么不用它探测 servicemanager 是否存在:默认入口会等到成功,适合业务进程依赖注册中心,无法表达“现在没起来,我去拉起它”。

getService 和 checkService 都进入 tryGetService,差别在第二个参数:getService(name) 传 startIfNotFound=true,找不到时会尝试懒启动;checkService(name) 传 false,只查表并立即返回(ServiceManager.cpp:183-228)。本项目的 launcher 和 download service 都选择 checkService,原因一致:它们需要有界等待和明确失败,避免把线程交给无限等待。

查表逻辑可以看这一段。

cpp
// getService 允许找不到时触发懒启动*outBinder = tryGetService(name, true);// checkService 只查当前注册表*outBinder = tryGetService(name, false);// 命中服务名后拿到 Service 结构if (auto it = mNameToService.find(name); it != mNameToService.end()) {// 返回注册时保存的 binder 引用    out = service->binder;// 结束命中分支}// 找不到且允许启动时尝试 lazy serviceif (!out && startIfNotFound) {// Android 系统上通过属性触发启动    tryStartService(name);// 结束懒启动分支}

片段来自 external/android/frameworks/native/cmds/servicemanager/ServiceManager.cpp:183-228。注意 checkService("http.service") 的 null 返回是正常控制流,不代表 Binder 事务失败。

13.2 注册、等待与清理

addService 的实现先取调用者上下文,拒绝 app uid 注册服务,检查访问控制、空 binder、服务名合法性和 VINTF 声明;如果注册的是远端 binder,还会对它 linkToDeath(ServiceManager.cpp:246-279)。检查通过后写入 mNameToService[name],并通知此前注册的回调(ServiceManager.cpp:281-298)。第六章的注册链路到这里才算真正落表。

waitForService 是比 checkService 更重的路径。它先 getService,如果没拿到,就创建 BnServiceCallback,向 servicemanager 注册通知;回调里保存 binder、flush commands、唤醒条件变量;等待循环每秒醒一次并重新请求服务,处理 lazy service 的竞态(IServiceManager.cpp:292-368)。这个机制适合系统组件等待长期依赖。本文的 download 链路没有用它,是因为下载请求希望在 10s 内明确告诉客户端 http.service 未注册。

服务死亡后的清理也在 servicemanager 内部完成。addService 已经对远端服务 binder 注册了死亡通知;binderDied 收到通知后遍历 mNameToService,把死亡 binder 对应的条目 erase,同时清理注册回调和客户端回调(ServiceManager.cpp:461-477)。注册表会随服务生死变化,并跟驱动维护的对象生命周期联动。服务进程退出后,后续 checkService 会返回 null,客户端的旧代理也会在使用或死亡通知中感知 DEAD_OBJECT。

放回本文的启动流程看,servicemanager 只负责名字到对象的首次连接。launcher 等待两个服务注册,是为了确保 client 后续能查到 download.service,download worker 能查到 http.service。一旦查找返回 Binder 引用,业务事务就直接在调用方和服务方之间流动。注册中心不会参与 enqueue、fetchToFd、进度回调,也不会看到 fd 里的响应体。这个边界让 ServiceManager 保持轻量,适合单线程 Looper 模型。

十四、示例总结

从 main.cpp 往下看,进程边界就是 Binder 边界。http.service 和 download.service 由同一个二进制 fork/exec 出来,但它们有不同地址空间,download 调 http 会真实进入驱动事务。

一次下载由两段同步 Binder 串起来:download_client -> download.service 的 enqueue 接受任务,download.service -> http.service 的 fetchToFd 执行传输。进度和终态沿两个 oneway 回调对象反向返回。

大响应体不进 Parcel。ParcelFileDescriptor 让内核把 fd 复制到 http 进程,Binder 事务只负责传递控制信息和对象引用。这条设计让下载大小不决定 Binder 缓冲区压力。


本文由 AgentPlanFlow 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Binder 框架——整体架构与跨进程调用 · Yinchen 的博客