毫米级剖析:Binder如何实现「一次拷贝」神话?

ps:本文内容较干,建议收藏后再读。
一、为什么是Binder
  • 移动端IPC的特殊需求:由于移动设备资源的稀缺性,对性能、安全、易用性要求比较高,Linux传统的IPC机制限制,对于Android中大量的服务跨进程调用不太友好。

    • 管道(Pipes) - 只能单向通信,功能有限
    • 消息队列(Message Queues)- 缺乏面向对象的抽象
    • 共享内存(Shared Memory)- 需要额外的同步机制,容易出错
    • 套接字(Sockets)- 开销较大,不适合频繁的本地通信
    • 信号(Signals) - 功能简单,只能传递简单信息
  • Binder的诞生与设计目标:OpenBinder 最初由 Be Inc. 开发,后来由 Palm Inc. 接手。当 OpenBinder 的主要开发者 Dianne Hackborn 加入 Google 后,这项技术也被带到了 Android 项目中。Android 团队在需要为这个新的移动操作系统选择一种 IPC 机制时,基于性能、稳定性和安全性等方面的考量 ,最终在OpenBinder 的基础上进行了改造和优化,发展出了如今 Android 系统中的 Binder 机制。

Android Binder 继承并精简了 OpenBinder 的核心设计理念,但侧重点有所不同:

  • 汲取核心思想:Android Binder 继承了 OpenBinder 中基于对象的通信方式 。
  • 专注本地 IPC:不同于 OpenBinder 旨在构建分布式系统的宏大目标 ,Android Binder 专注于单设备内的高效、安全 IPC。这种专注使得 Android Binder 的设计更加轻量和高效。
  • 关键技术创新:Android Binder 的实现依赖于一个名为 Binder 驱动 的 Linux 内核模块 。其通过 mmap 技术,实现了理论上仅需一次数据拷贝的进程间通信,显著提升了性能 。
  • 强化安全措施

    :Android Binder 在内核层面提供了身份标识鉴别机制,系统服务可以验证请求来源进程的 UID/PID,从而实施访问控制 。

二、Binder的总体架构与核心组件

C/S架构:主要涉及Client(服务调用进程),Server(服务提供进程),对于Android系统来说,系统服务众多,我们需要一个管理类方便我们查询这些服务,所以有了ServiceManager(服务大管家),Binder Driver(提供IPC底层传输能力),从宏观角度来看架构组成:

其中核心的IPC数据传输(内存拷贝)等操作依赖Binder驱动实现,那么Binder驱动是在内核层的,在类Linux系统中,进程空间分为用户空间和内核空间,用户空间的内存各进程独有,其他进程无法访问。而内核空间是共享的,可以通过统一的API与内核空间产生交互。正因为这一特性,各用户空间的进程才能间接地通过内核空间进行数据通信。后面章节会详细剖析,这里不再详述。对于Binder机制来说,Binder驱动程序就运行在内核空间中,利用Linux的LKM(Loadable kernel module,Linux内核动态可加载模块)机制加载到内核中。

Binder驱动被加载到内核空间后是如何发挥作用的呢?Binder驱动的内部数据结构图如下:

Binder驱动中关键数据结构是如何发挥作用的?主要涉及binder_proc、binder_node、binder_ref。

static HLIST_HEAD(binder_procs);

在binder.c驱动代码中定义了binder_procs这个哈希链表头,用来管理所用使用binder的进程相关信息。

当有一个进程通过系统调用open打开binder设备,最终被调用到binder_open函数:

static int binder_open(struct inode *nodp, struct file *filp)
{
  struct binder_proc *proc;
  binder_debug(BINDER_DEBUG_OPEN_CLOSE, "binder_open: %d:%d\n",
         current->group_leader->pid, current->pid);
// 根据结构体的定义分配内核内存
  proc = kzalloc(sizeof(*proc), GFP_KERNEL);
if (proc == NULL)
    return -ENOMEM;
  get_task_struct(current);
  proc->tsk = current;
  proc->vma_vm_mm = current->mm;
  INIT_LIST_HEAD(&proc->todo);
  init_waitqueue_head(&proc->wait);
  proc->default_priority = task_nice(current);
// 获取Binder驱动的主锁,确保下面对Binder驱动的全局变量的操作是线程安全的
  binder_lock(__func__);
  binder_stats_created(BINDER_STAT_PROC);
// 将当前进程所创建的binder_pro加入到全局的哈希链表当中
  hlist_add_head(&proc->proc_node, &binder_procs);
  proc->pid = current->group_leader->pid;
  INIT_LIST_HEAD(&proc->delivered_death);
  filp->private_data = proc;
// 释放获取的主锁
  binder_unlock(__func__);
if (binder_debugfs_dir_entry_proc) {
    char strbuf[11];
    snprintf(strbuf, sizeof(strbuf), "%u", proc->pid);
    proc->debugfs_entry = debugfs_create_file(strbuf, S_IRUGO,
      binder_debugfs_dir_entry_proc, proc, &binder_proc_fops);
  }
return0;
}

在binder_open函数中,会为当前进程创建一个binder_proc结构体,并根据结构体的定义分配内存一个内核的内存空间,然后将当前进程的相关信息存储到binder_proc结构体中,最后通过hlist_add_head(&proc->proc_node, &binder_procs)将binder_proc加入到binder_procs的哈希链表当中。

binder_proc结构体的定义:

struct binder_proc {
  struct hlist_node proc_node;
struct rb_root threads;
struct rb_root nodes;
struct rb_root refs_by_desc;
struct rb_root refs_by_node;
struct list_head waiting_threads;
int pid;
struct task_struct *tsk;
conststruct cred *cred;
struct hlist_node deferred_work_node;
int deferred_work;
int outstanding_txns;
bool is_dead;
bool is_frozen;
bool sync_recv;
bool async_recv;
wait_queue_head_t freeze_wait;
struct dbitmap dmap;
struct list_head todo;
struct binder_stats stats;
struct list_head delivered_death;
struct list_head delivered_freeze;
  u32 max_threads;
int requested_threads;
int requested_threads_started;
int tmp_ref;
struct binder_priority default_priority;
struct dentry *debugfs_entry;
struct binder_alloc alloc;
struct binder_context *context;
spinlock_t inner_lock;
spinlock_t outer_lock;
struct dentry *binderfs_entry;
bool oneway_spam_detection_enabled;
};

其成员变量struct rb_root nodes 一个用红黑树存储了当前进程所有的对外提供服务的binder_node(用户空间的binder服务实现)的集合,以SystemServer进程为例,其注册的服务包含AMS、WMS、PMS等等服务,而这些服务的所有binder实现记录在nodes这个红黑树中。那binder_node结构体又是什么样:

struct binder_node {
  int debug_id;
spinlock_t lock;
struct binder_work work;
union {
    struct rb_node rb_node;
    struct hlist_node dead_node;
  };
struct binder_proc *proc;
struct hlist_head refs;
int internal_strong_refs;
int local_weak_refs;
int local_strong_refs;
int tmp_refs;
binder_uintptr_t ptr;
binder_uintptr_t cookie;
struct {
    /*
     * bitfield elements protected by
     * proc inner_lock
     */
    u8 has_strong_ref:1;
    u8 pending_strong_ref:1;
    u8 has_weak_ref:1;
    u8 pending_weak_ref:1;
  };
struct {
    /*
     * invariant after initialization
     */
    u8 sched_policy:2;
    u8 inherit_rt:1;
    u8 accept_fds:1;
    u8 txn_security_ctx:1;
    u8 min_priority;
  };
bool has_async_transaction;
struct list_head async_todo;
};

其中binder_uintptr_t ptr 是指向用户空间binder服务对象的指针,也就是可以通过binder_node直接找到服务对象。

struct hlist_head refs refs会将指向该节点的多有引用存储到哈希链表中,这样当节点的服务停止工作时,可以方向通知其他引用方。

binder_proc除了存储了自己进程所能提供的binder服务(binder_node),也会存储和管理当前进程所引用到其它进程的服务

struct rb_root refs_by_desc;
struct rb_root refs_by_node;

这两个红黑树存储的都是binder_ref结构的集合,只是以不同索引方式排序,目的是可以提供更快速的查询能力。

refs_by_desc是按照描述符排序的引用红黑树,怎么理解描述符,它是定义在binder_ref的data中的binder_ref_data结构体中:

struct binder_ref {

struct binder_ref_data data;
  ....
struct binder_proc *proc;// 拥有此引用的进程
struct binder_node *node;// 被引用的服务节点

}

struct binder_ref_data {
uint32_t desc;
int strong; // 强引用计数
int weak; // 弱引用计数
}

描述符就是一个整型的数值(用户空间句柄)。当用户空间通过句柄调用服务时,通过这个红黑树查找。举例:

// 用户空间获取ActivityManager服务,内核就会根据句柄的值查找服务
serviceManager.getService("activity")

refs_by_node也是一样,只是排序是根据节点的指针值进行排序。

binder_proc (进程A)
├── refs_by_desc (红黑树)
│ ├── ref1 (desc=1) → ActivityManager节点
│ ├── ref2 (desc=2) → WindowManager节点
│ └── ref3 (desc=3) → PackageManager节点
└── refs_by_node (红黑树)
    ├── ref1 → ActivityManager节点
    ├── ref2 → WindowManager节点
    └── ref3 → PackageManager节点

binder_node (ActivityManager节点)
└── refs (哈希链表)
    ├── ref1 (来自进程A)
    ├── ref4 (来自进程B)
    └── ref7 (来自进程C)

binder_proc中管理本进程自己的服务是直接管理binder_node,为什么管理引用的外部进程服务不使用binder_node而中间加一层binder_ref结构呢?

首先,不同进程的用户空间是不能互相访问的,通过引用可以实现间接访问:

// 1. 应用进程请求ActivityManager服务
service = serviceManager.getService("activity");

// 2. 内核为应用进程创建对ActivityManager的引用
ref = binder_get_ref_for_node(app_proc, activity_manager_node);

// 3. 引用被添加到应用进程的refs_by_desc中
rb_insert_color(&ref->rb_node_desc, &app_proc->refs_by_desc);

另外加一个中间层可以加额外的加一些逻辑而不影响服务节点本身,比如在binder_ref的data中加本进程内的强引用计数和弱引用计数,不会干扰到引用同样节点的其他进程。当前进程退出时也只需要清理当前的引用计数,不影响其他进程。通过使用简单的整数句柄来代替复杂的节点指针地址,方便用户空间使用。还可以在中间层加一些权限验证,错误处理等。

回到binder的工作流中来,当Binder被加载到内核空间时,首先被调用的就是binder_init()函数,

  • 模块初始化 :在 binder_init() 函数中完成驱动的初始化工作,该函数通过 device_initcall(binder_init) 宏在内核启动时被调用。
  • 创建工作队列 :创建一个名为"binder"的单线程工作队列 binder_deferred_workqueue ,用于处理延迟工作。
  • 创建调试文件系统 :在debugfs中创建binder相关的调试目录和文件,用于运行时调试和状态查看。
  • 注册设备 :通过 misc_register(&binder_miscdev) 将Binder注册为混杂设备,设备名为"binder",主设备号为MISC_DYNAMIC_MINOR(255),次设备号动态分配。
  • 设置文件操作 :通过 binder_fops 结构体定义设备支持的操作,包括 open 、 mmap 、 ioctl 、 poll 等。

对于Android系统来说,当Binder驱动程序运行起来后,ServiceManager就可以通过系统调用Open调用Binder驱动获得一个文件描述符fD,通过fD就可以后续在Binder驱动中进行读写操作,当然ServiceManager同样通过系统调用mmap来进行内存映射,将内核中的一块物理内存映射到自己的虚拟地址上。当然由于ServiceManager的特殊性,他会在Binder驱动中将自己注册为Binder的上下文管理者(Context Manager),负责管理其他服务的注册与查询。核心过程如下图所示:

ServiceManager注册流程:

  1. open() → 打开/dev/binder设备文件
  2. 返回文件描述符
  3. ioctl(BINDER_SET_CONTEXT_MGR) → 申请成为上下文管理器
  4. Binder驱动设置ServiceManager为Context Manager
  5. ServiceManager进入binder_loop消息循环,等待请求

系统服务注册流程:

  1. 系统服务调用addService()注册服务
  2. 通过ioctl(BINDER_WRITE_READ)发送到Binder驱动
  3. Binder驱动将请求转发给Context Manager
  4. 唤醒ServiceManager的消息循环
  5. ServiceManager处理注册请求
  6. 在Binder驱动中创建对应的binder_node节点

应用查询服务流程:

  1. 应用进程调用getService()查询服务
  2. 通过ioctl发送查询请求到Binder驱动
  3. Binder驱动转发查询请求给Context Manager  
  4. 唤醒ServiceManager处理查询
  5. ServiceManager查找对应服务
  6. 返回服务的binder_ref引用给应用进程
三、核心设计思想剖析

我们已经基本弄清楚Binder的运行逻辑了,但有没有想过一些问题,Binder又没有对应的硬件,为什么搞出一个Binder驱动?都说Binder进行IPC效率高,只需一次内存拷贝,具体是怎么一次内存拷贝?

首先第一个问题,为什么叫Binder驱动。这是因为Android是基于Linux内核进行修改的,在Linux系统中有一句话“一切皆是文件”,为了与其他系统组件保持同样的设计逻辑;并且从开发便利的角度出发,它可以利用成熟的Linux驱动开发框架,重用现有的设备管理机制;与Linux内核的设备管理子系统可以无缝集成。大白话就是Linux中基于文件的成套的成熟API完全可以用在这里,省去很多不必要的重新设计的成本。

总而言之,

  1. 它符合驱动程序的本质特征 - 作为内核与用户空间的接口层  
  2. 它采用了标准的驱动程序架构 - 设备文件、file_operations等  
  3. 它提供了虚拟设备的功能 - 虽然没有硬件,但提供了IPC这个"虚拟设备"的服务
  4. 它利用了驱动框架的优势 - 权限控制、资源管理、安全隔离

一次内存拷贝究竟是什么?那么对比其他的IPC方式一般是两次内存拷贝。以消息队列为例:

当进程A传递消息到进程B时,第一次拷贝发生在将进程A中的用户空间的数据拷贝到内核空间的消息队列中(1次拷贝),然后再将内核空间的消息队列里的数据拷贝到进程B的用户空间中(2次拷贝)。

而Binder的一次内存拷贝依赖于mmap(内存映射),直接将内核空间的内存映射到用户空间的虚拟地址上,

所以关键还是使用mmap机制做了内存的映射,讲到mmap不得不再讲讲为什么Binder会通过这种创建一个设备文件(“/dev/binder”)的方式提供内核空间给用户空间访问的接口,Linux中从用户空间访问内核空间有多重方式,按照是否通过打开文件的操作分类大致可以这么分:

需要“打开文件”的方式:

  • 设备文件访问(/dev/*)
  • proc文件访问(/proc/*)
  • sysfs文件系统(/sys/*)
  • 普通文件操作

不需要“打开文件”的方式:

  • 直接系统调用(getpid(),fork(),exit()等)
  • 信号机制(signal(),kill())
  • 套接字创建(socket())
  • 内存管理(brk(), mmap()直接调用)

而Binder选择设备文件的方式的原因是:Binder需要复杂的双向通信,使用设备文件的方式可以利用现有的机制满足Binder复杂IPC过程的使用。

继续回到mmap()的过程,简单点讲就是Binder在内核区给每一个进程建一个内核缓冲区对应到的实际物理内存与进程用户空间的虚拟内存地址映射,这样当数据发送方发送数据时,内核将数据从用户空间拷贝到内核缓冲区,而接收进程能直接访问该内存。

核心目标:为每一个进程建立一块“与内核共享”的映射区域,用于手法事务数据与对象元数据,降低拷贝次数、加速IPC。

那mmap具体是如何做的呢(结合内核binder.c文件中的binder_mmap()函数):

四、一次Binder调用的全链路解析

1、对于系统服务来说,需要提供全局的查询能力,所以会将服务注册在ServiceManager当中,目的是方便全局的其他进程查询使用。上面的大bu是基于系统服务来分析的,其使用流程大致如下:

2、如果你是通过编写Java层AIDL来实现IPC,其底层确实也是通过Binder来实现传递数据,但是Java层的服务是不需要注册到ServiceManager当中的,但是Binder通信的原理是一样的,也是通过mmap机制实现一次内存拷贝。

新建一个ITestDemoInterface.aidl为例,来完整分析AIDL是怎么使用Binder机制来完成IPC的。

编译系统将.aidl文件编译生成对应的Java文件,并生产Stub抽象类以及Proxy代理类,服务进程Service继承Stub抽象类并实现具体的业务方法,在onBind函数中将IBinder对象返回。

客户端进程通过bindService函数调用去连接服务,触发服务Service去Binder驱动注册,生成服务对应的binder_node结构体并加入到服务所在进程的binder_proc的红黑树中,并将服务节点的句柄返回到客户端,客户端通过句柄可以查找到对应的服务引用以及最终的服务节点进行IPC调用。

一图胜千言,AIDL调用的简单时序图如下所示:

五、小结

Binder是Android专为移动端设计的IPC机制,相比传统Linux IPC(管道、消息队列等),它通过三大创新解决性能与安全问题:

  1. 单次内存拷贝:基于mmap将内核缓冲区映射到进程用户空间,数据发送方仅需一次拷贝到内核,接收方直接访问共享内存,大幅提升效率;
  2. 面向对象通信:以"Binder对象"为通信单元,支持跨进程调用服务方法,天然适配面向对象开发;
  3. 内核级安全控制:Binder驱动在内核层验证进程身份(UID/PID),服务端可实施精细权限管理。

架构核心采用C/S模式:

  • ServiceManager:作为"服务大管家"统一管理系统服务注册与查询;
  • Binder驱动:位于内核层(通过/dev/binder设备文件交互),管理关键数据结构:
    • binder_proc:记录进程的Binder资源(如服务节点树nodes、服务引用树refs_by_desc);
    • binder_node:对应服务端实体(如ActivityManagerService),可通过指针定位到用户空间服务;
    • binder_ref:存储外部进程被调用的服务的引用(含强/弱引用计数)。

BinderIPC调用依赖Binder驱动中转:客户端通过句柄定位服务引用,驱动转发请求至服务端进程,实现跨进程方法调用。Java层AIDL本质是Binder机制的封装应用。

点赞+关注,下一期更精彩!

本文分析源码基于最新的AOSP:https://cs.android.com/android/platform/superproject?hl=zh-cn

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值