Binder driver梳理
第1章 binder driver概述
进程间通信(IPC)或者远程调用过程(RPC)包含三个要素:
- 源:即调用者(Client)
- 目的:即提供服务者(Server)
- 数据:Cilent端想要调用Server端的某一个方法,需要传入什么参数,返回什么结果,这些数据都放在一个buffer中,由Cilent传到Server端。
但是,由于Client端和Server端是两个独立的进程,无法直接通信,因此,它们只能借助binder 驱动来完成交互。简单说,当用户控件调用open()时,最终会调用binder 驱动中的binder_open()方法,其他的mmap()/ioctl()方法也都是一样。
第2章 binder driver 基础数据结构
-
binder proc是描述进程上下文信息的,每一个用户空间的进程都对应一个binder proc结构体。binder_node是Binder实体对应的结构体,它是Server在Binder驱动中的体现。binder_ref是Binder引|用对应的结构体,它是Client在Binder驱动中的体现。
-
2.1 binder_proc
binder_proc 用来描述binder进程上下文信息的结构体,binder驱动的文件节点是”/dev/binder”,每当一个程序打开该文件节点时,binder驱动中都会新建一个binder_proc对象来保存该进程的上下文信息。
binder_proc中有四棵红黑树,分别是:threads,nodes,refs_by_desc,refs_by_nod,
其数据结构体为:
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;
…
};
- proc_node:将该binder_proc添加到”全局哈希表binder_procs中;
- threads:包含该进程处理用户请求的所有线程的红黑树,threads成员和binder_thread->rb_node关联到红黑树,将binder_proc和binder_thread关联起来;
- node:包含该进程内的所有binder实体所组成的红黑树。nodes成员和binder_node->rb_node关联到红黑树,从而将binder_proc和binder_node关联起来;
- rb_node_desc:包行进程内的所有binder引用所组成的红黑树。refs_by_desc成员和binder_ref->rb_node_desc关联到红黑树,将binder_proc和binder_ref关联起来;
- rb_node_node:包行进程内的所有binder引用所组成的红黑树。refs_by_node成员和binder_ref->rb_node_node关联到红黑树,将binder_proc和binder_ref关联起来;
- waiting_threads:关联到该binder引用实体的binder_node->refs哈希表中。
2.2 binder_node
binder_node是描述binder实体相关信息的结构体,在binder驱动中,会为每一个Server都创建一个Binder实体,即会为每个Server都创建一个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;
…
};
- debuf_id:每个binder_node创建时的debugid用于调试;
- binder_work:binder引用和binder_proc->refs_by_desc红黑树相关联;
- rb_node:如果该实体还有引用,则通过rb_node将节点链接到proc->nodes红黑树;
- dead_node:如果该实体引用都为0,则通过dead_node链接到全局哈希列表。
- proc:保存该binder引用所属的进程;
- refs:该binder实体所有引用组成的链表;
2.3 binder_ref
binder_ref是内核中用来描述binder引用相关信息的,在binder驱动中,会为每个binder node都创建对应的binder引用,即会为每个binder node创建binder ref对象。
其数据结构体为:
struct binder_ref {
struct binder_ref_data data;
struct rb_node rb_node_desc;
struct rb_node rb_node_node;
struct hlist_node node_entry;
struct binder_proc *proc;
struct binder_node *node;
struct binder_ref_death *death;
};
- binder_ref_data:包含debugid、handle、引用计数等信息;
- rb_node_desc:binder引用和binder_proc->refs_by_desc红黑树相关联;
- rb_node_node:binder引用和binder_proc->refs_by_node红黑树相关联;
- node_entry:关联到该binder引用实体的binder_node->refs哈希表中。
- binder_proc:该binder引用所属的进程;
- binder_node:该binder引用所引用的binder实体;
- binder_ref_death:引用死亡通知;
2.4 binder_thread
binder_thread是描述binder线程相关信息的结构体。
其数据结构体为:
struct binder_thread {
struct binder_proc *proc;
struct rb_node rb_node;
struct list_head waiting_thread_node;
int pid;
int looper; /* only modified by this thread */
bool looper_need_return; /* can be written by other thread */
struct binder_transaction *transaction_stack;
struct list_head todo;
bool process_todo;
struct binder_error return_error;
struct binder_error reply_error;
wait_queue_head_t wait;
struct binder_stats stats;
atomic_t tmp_ref;
bool is_dead;
struct task_struct *task;
};
- proc:该线程所属的binder进程;
- rb_node:将该线程添加到进程的threads红黑树中进行管理
- waiting_thread_node:
- pid:进程id
- looper:线程状态
- transaction_stack:线程正在处理的事务栈
- todo:线程待处理的事务链表
- return_error:write失败,返回错误码
- reply_error:reply失败,返回错误码
- wait:线程的等待队列,空闲线程挂在等待队列中被唤醒
- stats:保存一些线程的统计信息,如请求注册线程,便于调试
第3章 binder 驱动初始化
binder驱动是通过binder_init来初始化的,在binder_init中主要做了以下三件事:
- 创建一系列binder相关的文件,储存binder相关的信息
- init_binder_device注册binder设备
- init_binderfs初始化binder文件系统
3.1创建binder相关文件
- 概述
在binder_init函数中,会首先创建/binder目录,然后在binder目录下创建binder相关的几个文件以及proc目录。在设备中可以通过/sys/kernel/debug/binder目录查看state、stats、transactions、transaction_log、failed_transaction_log文件,以及proc目录。
- /sys/kernel/debug/binder/state:整体以及各个进程的thread/node/ref/buffer的状态信息,如有deadnode也会打印
- /sys/kernel/debug/binder/stats:整体以及各个进程的线程数,事务个数等的统计信息
- /sys/kernel/debug/binder/failed_transaction_log:记录32条最近的传输失败事件
- /sys/kernel/debug/binder/transaction_log:记录32条最近的传输事件
- /sys/kernel/debug/binder/transactions:遍历所有进程的buffer分配情况
proc目录中都是进程号,观察其中的进程都是注册在驱动的进程,其中包括servicemanager。可以通过命令cat /sys/kernel/debug/binder/proc/进程号查看对应进程的binder信息
- 代码示例
binder_debugfs_dir_entry_root = debugfs_create_dir("binder", NULL);
if (binder_debugfs_dir_entry_root) {
const struct binder_debugfs_entry *db_entry;
binder_for_each_debugfs_entry(db_entry)
debugfs_create_file(db_entry->name,
db_entry->mode,
binder_debugfs_dir_entry_root,
db_entry->data,
db_entry->fops);
binder_debugfs_dir_entry_proc = debugfs_create_dir("proc",
binder_debugfs_dir_entry_root);
}
3.2 注册binder设备
- 概述
Linux内核把所有的misc设备组织在一起,构成一个子系统,进行统一管理。在这个子系统里的所有misc类型的设备共享一个主设备号,但它们次设备号不同。
binder设备也是通过misc_register(&binder_device->miscdev);这个接口注册的,其中binder_device->miscdev即miscdevice表示一个misc设备,它记录了设备的名称msicdev.name、次设备号miscdev.minor以及支持的系统调用操作miscdev.fops等。 binder_device->miscdev.fops即结构体file_operations。它是把系统调用和驱动程序关联起来的关键结构。这个结构的每一个成员都对应着一个系统调用,Linux系统调用通过调用file_operations中相应的函数指针,接着把控制权转交给函数,从而完成Linux设备驱动程序的工作。
- 代码示例
binder_device->miscdev.fops = &binder_fops;
binder_device->miscdev.minor = MISC_DYNAMIC_MINOR;
binder_device->miscdev.name = name;
…
ret = misc_register(&binder_device->miscdev);
// binder_fops定义如下:
const struct file_operations binder_fops = {
.owner = THIS_MODULE,
.poll = binder_poll,
.unlocked_ioctl = binder_ioctl,
.compat_ioctl = compat_ptr_ioctl,
.mmap = binder_mmap,
.open = binder_open,
.flush = binder_flush,
.release = binder_release,
};
这就意味着当binder驱动执行系统调用时,如果是系统调用ioctl(),最终会调用binder_ioctl(),其他调用如open()和mmap()也都是如此。
3.3 初始化binder文件系统
- 概述
在驱动注册成功之后,必须通过register_filesystem()向VFS注册binder文件系统,并且将binder文件系统挂载到VFS。每个注册的文件系统都用一个类型为file_system_type的对象表示,file_system_type主要记录文件系统的类型相关信息,比如名称、上下文初始化函数指针等。binder_fs_type指明将要挂载的Binder文件系统名为binder。Binder文件系统挂载到VFS的时机在init进程启动的时候。相关挂载指令在system/core/rootdir/init.rc中。
- 代码示例
static struct file_system_type binder_fs_type = {
.name = "binder",
.init_fs_context = binderfs_init_fs_context,
.parameters = binderfs_fs_parameters,
.kill_sb = kill_litter_super,
.fs_flags = FS_USERNS_MOUNT,
};
int __init init_binderfs(void)
{
…
int ret;
ret = register_filesystem(&binder_fs_type);
return ret;
…
}
# Mount binderfs
mkdir /dev/binderfs
mount binder binder /dev/binderfs stats=global
chmod 0755 /dev/binderfs
symlink /dev/binderfs/binder /dev/binder
symlink /dev/binderfs/hwbinder /dev/hwbinder
symlink /dev/binderfs/vndbinder /dev/vndbinder
第4章 binder 内存管理
4.1 mmap
- 概述
当Client端想要发送数据到Server端时,常规的方式是先从Client端用户空间拷贝数据到内核空间,再从内核空间拷贝数据到Server端的用户空间,在这个过程中,发生了两次拷贝。
而在binder驱动中,Server端进程会先调用mmap将Server端的用户控件的一段虚拟内存和内核空间的一段虚拟内存映射到同一物理内存上。这段物理内存将作为一个缓冲区,接收来自不同Client端的数据。当Client端发送事务数据时,内核会通过copy_from_user将数据放入缓冲区,然后通知Server端,由于Server端用户空间和内核空间共享这块物理内存缓冲区,所以,Server端可以直接访问缓冲区数据,无需再次通过copy_to_user将数据拷贝到用户空间。
- mmap的实现
当从缓冲区分配一块处理事务数据的 buffer 时,就会为 buffer分配对应的物理页,buffer可以简单理解为一段连续的用户空间虚拟地址。为其分配物理页的时候,vma_insert_page()函数会逐页绑定到对应的虚拟地址上,随后kmap() 函数再将物理页映射到内核空间,至此,用户空间虚拟地址、内核空间虚拟地址就映射到了同一个物理页。内核通过内核空间虚拟地址调用 copy_from_user 把数据从客户端拷贝到 buffer 的物理页后,内核空间虚拟地址就用不上,可以通过 kunmap 取消物理页面与内核空间的映射。当服务端通过 buffer 的用户空间虚拟地址,就可以访问到对应物理页上的数据。
- 代码示例
binder_alloc_copy_user_to_buffer(struct binder_alloc *alloc,
struct binder_buffer *buffer,
binder_size_t buffer_offset,
const void __user *from,
size_t bytes)
{
if (!check_buffer(alloc, buffer, buffer_offset, bytes))
return bytes;
while (bytes) {
unsigned long size;
unsigned long ret;
struct page *page;
pgoff_t pgoff;
void *kptr;
page = binder_alloc_get_page(alloc, buffer,
buffer_offset, &pgoff);
size = min_t(size_t, bytes, PAGE_SIZE - pgoff);
kptr = kmap(page) + pgoff;
ret = copy_from_user(kptr, from, size);
kunmap(page);
if (ret)
return bytes - size + ret;
bytes -= size;
from += size;
buffer_offset += size;
}
return 0;
}
4.2 内存管理
binder的内存管理指的就是管理mmap映射的这块缓冲区。这其中涉及了缓冲区、缓冲区大小、缓冲区分配/释放、和缓冲区的初始化,分配/释放物理内存页。
4.2.1 缓冲区
binder驱动用binder_buffer来描述缓冲区。其定义如下:
struct binder_buffer {
struct list_head entry; /* free and allocated entries by address */
struct rb_node rb_node; /* free entry by size or allocated entry */
/* by address */
unsigned free:1;
unsigned clear_on_free:1;
unsigned allow_user_free:1;
unsigned async_transaction:1;
unsigned oneway_spam_suspect:1;
unsigned debug_id:27;
struct binder_transaction *transaction;
struct binder_node *target_node;
size_t data_size;
size_t offsets_size;
size_t extra_buffers_size;
void __user *user_data;
int pid;
};
- entry:表示链表中的一个节点。将 binder_buffer 插入 binder_alloc 的 buffers 链表时,就是将该
entry 插入链表中。遍历链表时,拿到该 entry,即可通过 container_of() 获取对应的 binder_buffer。 - rb_node:表示红黑树中的一个节点,和 entry 类似,用于在 binder_alloc 的 free_buffers 和
allocated_buffers 中表示该 binder_buffer。 - free - debug_id:free、clear_on_free、async_transaction 和
oneway_spam_suspect 这几个字段,主要是一些标志位。free 标识该 binder_buffer 是空闲的,:1
表示该字段是位字段,只占 1 个比特位。 - debug_id 是用于调试的唯一ID.
- transaction:指向与该缓冲区关联的 binder_transaction。
- target_node:指向与该缓冲区关联的 binder_node。
- data_size:transaction 数据的大小。
- offsets_size:offsets 数组的大小。
- extra_buffers_size:其他对象(如 sg 列表)的空间大小。
- user_data:用户空间的虚拟地址,指向该缓冲区的起始位置。
- pid:所属进程的进程 ID。
4.2.2 缓冲区大小
缓冲区的大小等于 data_size + offsets_size + extra_buffers_size 。
4.2.3 缓冲区初始化
Binder驱动最开始是不会为缓冲区分配对应的物理内存的,只是先分配了一段用户空间的虚拟地址。
- 代码示例:
int binder_alloc_mmap_handler(struct binder_alloc *alloc,
struct vm_area_struct *vma)
{
int ret;
const char *failure_string;
struct binder_buffer *buffer;
//设置buffer大小,限制最大为4m
alloc->buffer_size = min_t(unsigned long, vma->vm_end - vma->vm_start,
SZ_4M);
mutex_unlock(&binder_alloc_mmap_lock);
// 缓冲区的起始地址是vma->vm_start即为用户空间缓冲区的起始地址
alloc->buffer = (void __user *)vma->vm_start;
alloc->pages = kcalloc(alloc->buffer_size / PAGE_SIZE, //分配物理页的指针数组
sizeof(alloc->pages[0]),
GFP_KERNEL);
// 为binder_buffer对象分配内存
buffer = kzalloc(sizeof(*buffer), GFP_KERNEL);
buffer->user_data = alloc->buffer;
// 将新创建的binder_buffer加入到的buffers双向链表中
list_add(&buffer->entry, &alloc->buffers);
buffer->free = 1;
// 将新创建的buffer插入到free_buffers红黑树中
binder_insert_free_buffer(alloc, buffer);
… …
return 0;
}
4.2.4 分配/释放缓冲区
每个使用binder通信的进程,都会事先建立一个缓冲区,用binder_alloc缓冲区分配器来管理内存分配,binder_alloc部分定义如下:
struct binder_alloc {
struct mutex mutex;
struct vm_area_struct *vma;
struct mm_struct *vma_vm_mm;
void __user *buffer;
struct list_head buffers;
struct rb_root free_buffers;
struct rb_root allocated_buffers;
size_t free_async_space;
struct binder_lru_page *pages;
size_t buffer_size;
uint32_t buffer_free;
int pid;
size_t pages_high;
};
- vma:描述了一块用户空间的虚拟内存,包括起始地址、结束地址等 ; vma_vm_mm:mm_struct
- 描述的是该进程的用户空间及相关信息; buffer:vm_area_struct 的起始地址;
- buffers:一个双向循环链表,管理着从缓冲区里划分的所有 binder_buffer,按 buffer 的起始的用户空间虚拟地址排序;
- free_buffers:一棵红黑树,管理着所有可分配的 binder_buffer,树里按 buffer 的大小排序;
- allocated_buffers:一棵红黑树,管理着所有已分配的 binder_buffer,树里按 buffer
- 的起始的用户空间虚拟地址排序; pages:一个数组,每个元素都是一个指针,指向一个
- binder_lru_page。binder_lru_page 对应一个物理页; buffer_size:描述整个缓冲区的大小;
- pid:所属进程的进程 ID; pages_high:描述物理页面个数;
在处理事务的时候,会从缓冲区划分一块 binder_buffer 出来处理事务。这时候,才会为这块小缓冲区分配物理内存。
为事务分配缓冲区,有两种情况:
1.查询 free_buffers 红黑树,找不到大小刚好合适的缓冲区,就找大于事务数据大小的最小空闲缓冲区进行切割:
2. 查询 free_buffers 红黑树,刚好找到合适的缓冲区,直接使用它。 其流程如下:
处理完事务之后,就会释放为事务分配的缓冲区。其流程如下:
在分配/释放缓冲区时的buffer内存变化如下:
4.2.5 分配/释放物理内存页
结构体page就是用来描述一个物理内存页的,分配和释放物理内存页都是以页为单位,遍历binder_buffer中的所有页。在分配物理内存页时需要判断,如果之前已经为该页分配物理内存页,就直接使用,只需要从binder_alloc_lru链表中移除即可,若没有为该页分配过物理内存页,就先通过alloc_page()函数分配,然后用vm_insert_page()函数建立映射。
其流程如下:
4.2.6 内存缩减器
内存缩减器是一种用于回收未使用的内存页面的机制,当系统内存紧张时被调用回收最近最久未使用的空闲物理内存页。Binder驱动初始化时,会通过binder_alloc_shrinker_init()注册binder内存缩减器。
首先,在binder_alloc_shrinker_init函数中通过list_lru_init函数注册一个binder_alloc_lru链表。其中,binder_alloc_lru 是一个 lru 链表,主要用于内存不足时,回收物理内存页,所有用户进程共享这个 lru 链表。结构体 binder_lru_page 描述了该 lru 链表的一个结点,记录着对应物理内存页的信息。
接着通过register_shrinker函数注册binder_shrinker,binder_shrinker定义了binder驱动的内存缩减器,并声明了缩减器的两个回调函数binder_shrink_count和binder_shrink_scan,
binder_shrink_scan接口通过调用binder_alloc_free_page()函数回收最近最久未使用的空闲物理内存页。
第5章 binder transaction
5.1 binder 命令
Binder命令就像网络请求中的 Http 报文一样,在进程间传递的 Binder 消息,也遵循特定的 Binder 协议。Binder 协议是 Android Binder 的底层通信协议,主要在 Android 用户空间和内核空间之间进行通信。Binder 协议定义了一组消息码,每个消息码都对应 Binder 系统中的一个操作或事件。
Binder 的消息通信基于这组消息码。消息的具体格式由 Binder 协议定义,通常包括消息码和数据两部分:
- 消息码:一个整数值,表示要执行的操作或事件。消息码分为两大类:BC 、BR。BC 是由客户端/服务端发送给Binder 驱动的消息,BR
是 由 Binder 驱动处理过后,回复给客户端/服务端的消息。 - 数据:与消息码相关的数据,紧跟在消息码后面。例如,如果消息码是
BC_TRANSACTION,则数据部分会包含事务的详细信息,包含调用的代码、调用参数等。
5.2 ioctl 命令
Binder 消息,都是通过系统调用 ioctl() 来实现的。Binder 驱动的 ioctl 实现就是 binder_ioctl()。
Binder 定义了一组 ioctl 命令,每个命令,都对应 Binder 系统中的一个操作。类似 Binder 消息,ioctl 命令通常也包括命令码和数据两部分。如:
- BINDER_WRITE_READ:读写操作。Binder 消息就是通过这个 ioctl 命令发送的。该命令码后面,紧跟着一个 binder_write_read 。
- BINDER_SET_MAX_THREADS:设置线程池的最大线程数。该命令码后面紧跟一个无符号的32位整数,即我们设置的最大线程数。
Binder消息都是通过BINDER_WRITE_READ 这个 ioctl 命令发送/读取的。一个 BINDER_WRITE_READ 里可以发送/读取多个 Binder 消息。在发送/读取消息时将数据保存在binder_write_read结构体中,其定义如下:
struct binder_write_read {
binder_size_t write_size; /* bytes to write */
binder_size_t write_consumed; /* bytes consumed by driver */
binder_uintptr_t write_buffer;
binder_size_t read_size; /* bytes to read */
binder_size_t read_consumed; /* bytes consumed by driver */
binder_uintptr_t read_buffer;
};
- write_size:用户空间希望写入驱动的字节数。
- write_consumed:驱动实际消耗的字节数。
- write_buffer:用户空间写入的数据的起始地址。
- read_size:用户空间希望从驱动中读取的字节数。
- read_consumed:驱动实际返回的字节数。
- read_buffer:用户空间从驱动中读取数据的缓冲区的起始地址。
还没写完,未完待续…