3. Binder驱动基础:设备节点、打开与mmap操作、Binder协议概述
Binder IPC 的核心实现位于 Linux 内核驱动层。用户空间的进程通过标准的文件操作接口与 Binder 驱动交互,驱动则负责管理进程间通信的底层细节。本章将深入剖析 Binder 驱动的三大基础:设备节点 /dev/binder、打开与内存映射(mmap)操作,以及 Binder 协议的基本框架。
3.1 设备节点 /dev/binder
在 Android 系统中,Binder 驱动被注册为一个标准的 misc 设备(杂项设备)。其主设备号为 10,次设备号动态分配,并在文件系统中创建名为 /dev/binder 的设备节点。
设备节点的创建过程发生在内核初始化阶段。Binder 驱动调用 misc_register() 函数向内核注册自己,该函数会自动在 /dev 目录下创建设备文件。用户空间的服务(如 servicemanager)通过打开此设备文件来获取与 Binder 驱动通信的句柄。
| 属性 | 值 | 说明 |
|---|---|---|
| 设备类型 | misc (杂项) | 主设备号 10,次设备号动态分配 |
| 设备路径 | /dev/binder | 用户空间访问 Binder 驱动的入口 |
| 文件操作 | binder_fops | 定义 open、release、mmap、ioctl 等回调函数 |
| 权限 | 0666 或更严格 | 允许系统服务与普通应用进程访问 |
3.2 打开操作:binder_open
当用户空间进程调用 open("/dev/binder", O_RDWR) 时,内核会执行 Binder 驱动注册的 binder_open 函数。这个函数是每个进程与 Binder 驱动建立联系的起点。
binder_open 的核心工作是为当前进程分配并初始化一个 binder_proc 结构体。该结构体是 Binder 驱动视角下的“进程”抽象,用于管理该进程的所有 Binder 相关资源。
struct binder_proc 是 Binder 驱动中最重要的结构体之一,它包含了进程的线程池、待处理事务、已映射内存区域、节点引用等信息。
binder_open 函数的主要步骤:
- 分配 binder_proc 结构体:使用
kzalloc在堆上分配内存并清零。 - 初始化链表头:初始化进程内的线程列表、待处理事务列表、节点列表等。
- 设置进程 PID:记录当前进程的 PID,用于调试和统计。
- 关联文件描述符:将
binder_proc指针存入file->private_data,以便后续操作(如 mmap、ioctl)能够快速获取该结构体。 - 将进程加入全局链表:将新创建的
binder_proc添加到 Binder 驱动的全局进程链表中。
// 简化后的 binder_open 核心逻辑
static int binder_open(struct inode *nodp, struct file *filp)
{
struct binder_proc *proc;
// 1. 分配并初始化 binder_proc 结构体
proc = kzalloc(sizeof(*proc), GFP_KERNEL);
if (proc == NULL)
return -ENOMEM;
// 2. 初始化各种链表头
INIT_LIST_HEAD(&proc->todo);
INIT_LIST_HEAD(&proc->nodes);
INIT_LIST_HEAD(&proc->refs_by_desc);
INIT_LIST_HEAD(&proc->refs_by_node);
INIT_LIST_HEAD(&proc->threads);
// 3. 记录进程 PID
proc->pid = current->pid;
// 4. 将 proc 关联到文件描述符
filp->private_data = proc;
// 5. 将进程加入全局链表
binder_lock(__func__);
hlist_add_head(&proc->proc_node, &binder_procs);
binder_unlock(__func__);
return 0;
}
3.3 mmap 操作:内存映射与缓冲区管理
Binder IPC 的高效性很大程度上得益于其内存映射机制。在打开设备文件后,用户空间进程必须调用 mmap 来分配用于数据传输的缓冲区。这是 Binder 驱动初始化的关键步骤。
mmap 操作的核心目的是在内核空间分配一块物理内存,并将其同时映射到内核空间和用户空间。这样,当 Binder 驱动需要将数据从一个进程传输到另一个进程时,只需在内核空间进行一次拷贝,而无需在用户空间和内核空间之间进行两次拷贝(一次从发送进程的用户空间拷贝到内核,一次从内核拷贝到接收进程的用户空间)。
binder_mmap 函数的主要步骤:
- 检查映射大小:确保映射大小不超过允许的最大值(通常为 4 页)。
- 分配物理页面:调用
alloc_page分配所需的物理页。 - 建立页表映射:使用
remap_pfn_range或vm_insert_page将分配的物理页映射到用户空间的虚拟地址。 - 初始化缓冲区管理结构:创建
binder_buffer链表,用于管理已映射内存中的空闲块和已分配块。 - 记录映射信息:将映射的虚拟地址、大小等信息存入
binder_proc结构体。
3.4 Binder 协议概述
Binder 协议定义了用户空间进程与 Binder 驱动之间通信的规范。它主要基于 ioctl 系统调用实现,核心命令是 BINDER_WRITE_READ。该命令允许进程在一次系统调用中同时发送数据(write)和接收数据(read)。
Binder 协议的数据单元称为“事务”(Transaction)。一个事务包含一个 binder_transaction_data 结构体,其中封装了目标对象、代码、数据负载等信息。
| 协议命令 | 方向 | 功能描述 |
|---|---|---|
| BINDER_WRITE_READ | 双向 | 核心命令,同时执行写入和读取操作 |
| BINDER_SET_MAX_THREADS | 用户→驱动 | 通知驱动该进程允许的最大线程数 |
| BINDER_SET_CONTEXT_MGR | 用户→驱动 | 将当前进程注册为 ServiceManager |
| BINDER_THREAD_EXIT | 用户→驱动 | 通知驱动某个 Binder 线程即将退出 |
| BINDER_VERSION | 用户→驱动 | 获取 Binder 驱动版本号 |
一个典型的 Binder 事务流程如下:
- 发送方:填充
binder_transaction_data结构体,指定目标句柄、事务代码和数据。 - ioctl 调用:发送方调用
ioctl(fd, BINDER_WRITE_READ, &bwr),其中bwr包含写入缓冲区和读取缓冲区。 - 驱动处理:Binder 驱动解析写入缓冲区,找到目标进程,并将事务数据拷贝到目标进程的 mmap 缓冲区中。
- 唤醒接收方:驱动将事务添加到目标进程的待处理队列,并唤醒目标进程中等待的 Binder 线程。
- 接收方:被唤醒的线程通过
ioctl读取返回的数据,完成一次 IPC 调用。