| BC_EXIT_LOOPER
| 通知驱动,当前线程退出数据的发送和接收 |
| BC_REQUEST_DEATH_NOTYFICATION
| 通知驱动接收某个Binder服务
的死亡通知 |
| BC_CLEAR_DEATH_NOTYFICATION
| 通知驱动不再接收某个Binder服务
的死亡通知 |
| BC_DEAD_BINDER_DONE
| 回应BR_DEAD_BINDER
命令 |
驱动返回的命令列表
| 命令 | 说明 |
| — | — |
| BR_ERROR
| 驱动内部出错了 |
| BR_OK
| 命令成功 |
| BR_TRANSACTION
| Binder调用命令 |
| BR_REPLY
| 返回Binder调用的结果 |
| BR_ACQUIRE_RESULT
| 未使用 |
| BR_DEAD_REPLY
| 向驱动发送Binder调用
时,如果对方已经死亡,则驱动回应此命令 |
| BR_TRANSACTION_COMPLETE
| 回应BR_TRANSACTION
命令 |
| BR_INCREFS
| 要求增加Binder对象
的弱引用计数 |
| BR_ACQUIRE
| 要求增加Binder对象
的强引用计数 |
| BR_DECREFS
| 要求减少Binder对象
的弱引用计数 |
| BR_RELEASE
| 要求减少Binder对象
的强引用计数 |
| BR_ATTEMPT_ACQUIRE
| 要求将Binder对象
的弱引用变成强引用 |
| BR_NOOP
| 命令成功 |
| BR_SPAWN_LOOPER
| 通知创建Binder线程
|
| BR_FINISHED
| 未使用 |
| BR_DEAD_BINDER
| 通知关注的Binder
已经死亡 |
| BR_CLEAR_DEATH_NOTYFICATION_DONE
| 回应BC_CLEAR_DEATH_NOTYFICATION
|
| BR_FAILED_REPLY
| 如果Binder调用
的函数号
不正确,回复本消息 |
Binder消息序列图
前面列出的两个命令列表看上去指令蛮多的,其实可以分为四类:
-
Binder线程
管理相关 -
Binder方法调用
相关 -
Binder对象引用计数管理
相关 -
Binder对象死亡通知
相关
看下面两个序列图:
Binder调用
的消息序列图:
Binder对象引用计数管理
的消息序列图:
Binder驱动
中定义的操作函数如下:
static const struct file_operations binder_fops = {
.owner = THIS_MODULE,
.poll = binder_poll,
.unlocked_ioctl = binder_ioctl,
.compat_ioctl = binder_ioctl,
.mmap = binder_mmap,
.open = binder_open,
.flush = binder_flush,
.release = binder_release,
};
Binder驱动
中并没有实现常用的read
、write
操作。数据的传输都是通过ioctl
操作来完成的。
Binder驱动
中定义了很多的数据结构,一些主要的数据结构如下:
| 数据结构 | 说明 |
| — | — |
| binder_proc
| 每个使用open
打开Binder设备文件
的进程都会在驱动中创建一个biner_proc的
结构,用来记录该进程的各种信息和状态。例如:Binder节点表
、节点引用表
等 |
| binder_thread
| 每个Binder线程
在Binder驱动
中都有一个binder_thread
结构,记录线程相关的信息,例如要完成的任务等 |
| binder_node
| binder_proc
中有一张Binder节点对象表
,表项是binder_node
结构。代表进程中的BBinder对象,记录BBinder对象的指针、引用计数等数据 |
| binder_ref
| binder_proc
中还有一张节点引用表
,表项是binder_ref
结构。代表进程中的BpBinder对象
,保存所引用的对象binder_node
指针。BpBinder
中的mHandler
值,就是它在索引表中的位置 |
| binder_buffer
| 驱动通过mmap
的方式创建了一块大的缓存区,每次Binder
传输数据,会在缓存区分配一个binder_buffer
的结构来保存数据 |
在Binder驱动中,大量使用红黑树
来管理数据。Binder驱动
中是由内核提供的实现,具体表现是生命的一个全局变量binder_procs
:
static HLIST_HEAD(binder_procs);
所有进程的binder_proc结构体都将插入到这个变量代表的红黑树中。以binder_open的代码为例:
static int binder_open(struct inode *nodp, struct file *filp)
{
struct binder_proc *proc;
struct binder_device *binder_dev;
binder_debug(BINDER_DEBUG_OPEN_CLOSE, “%s: %d:%d\n”, func,
current->group_leader->pid, current->pid);
proc = kzalloc(sizeof(*proc), GFP_KERNEL);//分配空间给binder_proc结构体
//…
get_task_struct(current->group_leader);// 获取当前进程的task结构
proc->tsk = current->group_leader;
//…
INIT_LIST_HEAD(&proc->todo);// 初始化binder_porc中的todo队列
//…
filp->private_data = proc;// 将proc保存到文件结构中,供下次调用使用
mutex_lock(&binder_procs_lock);
// 将proc插入到全局变量binder_procs中
hlist_add_head(&proc->proc_node, &binder_procs);
mutex_unlock(&binder_procs_lock);
//…
return 0;
}
binder_open
函数主要功能是:
-
打开
Binder
驱动的设备文件 -
为当前进程创建和初始化
binder_proc
结构体proc
-
将proc插入到红黑树
binder_procs
中 -
将proc放入到file结构的
private_data
字段中 -
调用驱动的其他操作时可以从
file
结构中取出代表当前进程的binder_proc
前面介绍了,用户进程打开Binder
设备后,会调用mmap
在驱动中创建一块内存空间用于接收传递给本进程的Binder
数据。我们看下mmap
的代码(这部分是9.0的源码,和书中的结构有些不同):
static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
int ret;
struct binder_proc *proc = filp->private_data;
const char *failure_string;
if (proc->tsk != current->group_leader)
return -EINVAL;
// 检查要求分配的空间是否大于SZ_4M的定义
if ((vma->vm_end - vma->vm_start) > SZ_4M)
vma->vm_end = vma->vm_start + SZ_4M;
// 检查mmap的标记,不能带有FORBIDDEN_MMAP_FLAGS
if (vma->vm_flags & FORBIDDEN_MMAP_FLAGS) {
ret = -EPERM;
failure_string = “bad vm_flags”;
goto err_bad_arg;
}
//fork 的子进程无法复制映射空间,并且不允许修改属性
vma->vm_flags |= VM_DONTCOPY | VM_MIXEDMAP;
vma->vm_flags &= ~VM_MAYWRITE;
vma->vm_ops = &binder_vm_ops;
vma->vm_private_data = proc;
// 真正内存分配的逻辑在 binder_alloc_mmap_handler中
ret = binder_alloc_mmap_handler(&proc->alloc, vma);
//…
}
int binder_alloc_mmap_handler(struct binder_alloc *alloc,
struct vm_area_struct *vma)
{
int ret;
struct vm_struct *area;
const char *failure_string;
struct binder_buffer *buffer;
mutex_lock(&binder_alloc_mmap_lock);
//如果已分配缓存区,goto 错误
if (alloc->buffer) {
ret = -EBUSY;
failure_string = “already mapped”;
goto err_already_mapped;
}
//在用户进程分配一块内存作为缓冲区
area = get_vm_area(vma->vm_end - vma->vm_start, VM_ALLOC);
//如果内存分配失败,goto 对应错误
if (area == NULL) {
ret = -ENOMEM;
failure_string = “get_vm_area”;
goto err_get_vm_area_failed;
}
//把分配的缓冲区指针放在binder_proc的buffer字段
alloc->buffer = area->addr;
//重新配置下内存起始地址和偏移量
alloc->user_buffer_offset =
vma->vm_start - (uintptr_t)alloc->buffer;
mutex_unlock(&binder_alloc_mmap_lock);
//创建物理页结构体
alloc->pages = kzalloc(sizeof(alloc->pages[0]) *
((vma->vm_end - vma->vm_start) / PAGE_SIZE),
GFP_KERNEL);
alloc->buffer_size = vma->vm_end - vma->vm_start;
//在内核分配struct buffer的内存空间
buffer = kzalloc(sizeof(*buffer), GFP_KERNEL);
//这部分和书中着实不太一样,看的晕晕的
//目前只看出内存地址关联部分
buffer->data = alloc->buffer;
list_add(&buffer->entry, &alloc->buffers);
buffer->free = 1;
binder_insert_free_buffer(alloc, buffer);
// 异步传输将可用空间大小设置为映射大小的一半
alloc->free_async_space = alloc->buffer_size / 2;
barrier();
alloc->vma = vma;
alloc->vma_vm_mm = vma->vm_mm;
/* Same as mmgrab() in later kernel versions */
atomic_inc(&alloc->vma_vm_mm->mm_count);
return 0;
// …对应的错误信息
}
内存分配时:
-
首先调用get_vm_area在用户进程分配一块地址空间
-
接着在内核中也分配同样页数大小的空间
-
然后把它们的物理内存地址绑定在一起
-
这样应用和内核间就能共享一块空间了
当发生Binder调用时:
-
数据会从调用进程复制到内核空间中
-
驱动会在服务进程的缓冲区中寻找一块合适大小的空间来存放数据
-
因为服务进程的用户空间的缓冲区和内核空间的缓冲区是共享的
-
所以服务进程不需要将数据再从内核空间复制到用户空间
-
节省了一次复制过程
-
提高了Binder通信效率
ioctl
在驱动中的实现是binder_ioctl
函数,用来处理ioctl
操作的命令,这些命令最常见的就是BINDER_WRITE_READ
,用于Binder
调用,先看下这个命令的处理过程:
static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
int ret;
struct binder_proc *proc = filp->private_data;
struct binder_thread *thread;
unsigned int size = _IOC_SIZE(cmd);
void __user *ubuf = (void __user *)arg;
// 检查binder_stop_on_user_error的值是否小于2,这个值表示Binder中的错误数
// 大于2则挂起当前进程到binder_user_error_wait的等待队列上
ret = wait_event_interruptible(binder_user_error_wait, binder_stop_on_user_error < 2);
if (ret)
goto err_unlocked;
// 获取当前线程的数据结构
thread = binder_get_thread(proc);
if (thread == NULL) {
ret = -ENOMEM;
goto err;
}
switch (cmd) {
case BINDER_WRITE_READ:
//调用 binder_ioctl_write_read 方法
ret = binder_ioctl_write_read(filp, cmd, arg, thread);
break;
}
static int binder_ioctl_write_read(struct file *filp,
unsigned int cmd, unsigned long arg,
struct binder_thread *thread)
{
int ret = 0;
struct binder_proc *proc = filp->private_data;
unsigned int size = _IOC_SIZE(cmd);
void __user *ubuf = (void __user *)arg;
struct binder_write_read bwr;
if (size != sizeof(struct binder_write_read)) {
ret = -EINVAL;
goto out;
}
// 这里应该就是就是一次拷贝,把数据复制到内核中
if (copy_from_user(&bwr, ubuf, sizeof(bwr))) {
ret = -EFAULT;
goto out;
}
if (bwr.write_size > 0) {
// 如果命令时传递给驱动的数据,调用 binder_thread_write 处理
ret = binder_thread_write(proc, thread,
bwr.write_buffer,
bwr.write_size,
&bwr.write_consumed);
//省略异常处理和log打印
}
if (bwr.read_size > 0) {
// 如果命令时读取驱动的数据,调用 binder_thread_read 处理
ret = binder_thread_read(proc, thread, bwr.read_buffer,
bwr.read_size,
&bwr.read_consumed,
filp->f_flags & O_NONBLOCK);
trace_binder_read_done(ret);
binder_inner_proc_lock(proc);
// 如果进程的todo队列不为空,唤醒用户进程处理
if (!binder_worklist_empty_ilocked(&proc->todo))
binder_wakeup_proc_ilocked(proc);
binder_inner_proc_unlock(proc);
//省略异常处理和log打印
}
// 异常处理
}
BINDER_WRITE_READ
命令有可能是向驱动写数据,也可能是从驱动读数据
-
写数据,通过
binder_thread_write
方法 -
读数据,通过
binder_thread_read
方法 -
在下一节调用过程会细讲这两个方法
除了BINDER_WRITE_READ
命令外,还有像BINDER_SET_MAX_THREADS、BINDER_SET_CONTEXT_MGR
等辅助通信的命令,大家可以阅读下源码。
学习的重点还是看调用过程
的数据读写部分
Binder
调用是Binder
驱动中最核心的活动,整个过程几乎涉及了Binder
驱动所有的数据结构。我们先从调用的整体流程上来看下:
-
Binder
调用从客户进程中发起 -
通过
ioctl
操作来运行驱动的binder_ioctl
-
驱动接收到命令
BC_TRANSACTION
后 -
找到代表服务进程的binder_proc结构体
-
在服务进程的缓冲区分配一块内存来保存从客户进程传递的数据
-
ioctl回复消息BR_TRANSACTION_COMPLETED
-
---------到这里,本次ioctl调用结束---------
-
客户进程
收到回复消息后 -
再次调用ioctl进入等待,准备读取数据,直到调用结果返回
服务进程
无法知道何时会有Binder调用
到达,因此它至少由一个线程在ioctl
上等待。
-
驱动
会在有调用到达时唤醒等待的线程,并将数据传给服务进程
-
同一时刻可能有多个
Binder
调用到达 -
驱动
为每个调用创建一个binder_transaction
的结构体 -
将
结构体
中的work
字段插入到服务进程的todo队列
中 -
todo队列
是一个binder_work
结构的列表 -
每个进程都会有一个
todo队列
来接收需要完成的工作 -
当
服务端
的用户进程
对ioctl
执行读操作时 -
会循环执行
todo队列
中需要完成的任务 -
直到队列为空才挂起等待
客户进程的调用流程
上面刚刚讲到,客户进程
发送的Binder
调用消息会执行到函数binder_thread_write
,这个函数的功能就是处理所有用户进程
发送的消息,我们看下和Binder
调用有关的部分:
static int binder_thread_write(struct binder_proc *proc,
struct binder_thread *thread,
binder_uintptr_t binder_buffer, size_t size,
binder_size_t *consumed)
{
//…
case BC_TRANSACTION:
case BC_REPLY: {
struct binder_transaction_data tr;
// 包结构体tr的数据复制到内核
if (copy_from_user(&tr, ptr, sizeof(tr)))
return -EFAULT;
ptr += sizeof(tr);
// 调用 binder_transaction 函数处理
binder_transaction(proc, thread, &tr,
cmd == BC_REPLY, 0);
break;
}
//…
}
对BC_TRANSACTION
命令的处理就是调用了binder_transaction
函数,binder_transaction
函数既要处理BC_TRANSACTION
也会处理BC_REPLY
消息,我们先看读取的情况,也就是reply=false
的情况:
- 找到代表目标进程的节点:
if (tr->target.handle) {
struct binder_ref *ref;
//查找handle对应的引用项
ref = binder_get_ref_olocked(proc, tr->target.handle,
true);
if (ref) {
//引用项的node字段保存了Binder对象节点的指针
//复制给target_node
target_node = binder_get_node_refs_for_txn(
ref->node, &target_proc,
&return_error);
} else {
//…
}
//…
} else {
//把管理进程的节点名放到target_node中
target_node = context->binder_context_mgr_node;
//…
}
- 搜寻目标线程
if (!(tr->flags & TF_ONE_WAY) && thread->transaction_stack) {
struct binder_transaction *tmp;
tmp = thread->transaction_stack;
//…
while (tmp) {
struct binder_thread *from;
spin_lock(&tmp->lock);
from = tmp->from;
if (from && from->proc == target_proc) {
atomic_inc(&from->tmp_ref);
target_thread = from;
spin_unlock(&tmp->lock);
break;
}
spin_unlock(&tmp->lock);
tmp = tmp->from_parent;
}
}
上面这段代码的逻辑是:
-
如果本次调用不是异步调用,并且调用者线程中的
transaction_stack
不为NULL
,则在其中查找和本次调用具有相同目标进程的transaction_stack
,如果找到,则把它的目标线程设置为本次调用的目标线程。 -
transaction_stack
是一个列表,保存了本线程所有正在执行的Binder调用的binder_transaction
结构体。 -
Binder
驱动中用binder_transaction
来保存一次Binder调用
的所有数据,包括传递的数据、通信双发的进程、线程信息等。因为Binder调用涉及两个进程,还要向调用端传递返回值。 -
所以,驱动中用结构
binder_transaction
保存还没结束的Binder调用
。 -
通常情况下执行
Binder调用
时不会存在相同进程的Binder调用
,因此target_thread
的值大多为NULL
- 如果有目标线程,则使用目标线程中的
todo队列
,否则使用目标进程的todo队列
if (target_thread)
e->to_thread = target_thread->pid;
e->to_proc = target_proc->pid;
4.为当前的Binder调用
创建binder_transaction
结构,并用调用的数据填充它。
t = kzalloc(sizeof(*t), GFP_KERNEL);
//…
tcomplete = kzalloc(sizeof(*tcomplete), GFP_KERNEL);
//…
if (!reply && !(tr->flags & TF_ONE_WAY))
t->from = thread;
else
t->from = NULL;
t->sender_euid = task_euid(proc->tsk);
t->to_proc = target_proc;
t->to_thread = target_thread;
t->code = tr->code;
t->flags = tr->flags;
if (!(t->flags & TF_ONE_WAY) &&
binder_supported_policy(current->policy)) {
/* Inherit supported policies for synchronous transactions */
t->priority.sched_policy = current->policy;
t->priority.prio = current->normal_prio;
} else {
/* Otherwise, fall back to the default priority */
t->priority = target_proc->default_priority;
}
5.在目标进程
(也就是服务端
)的缓冲区分配空间,复制用户进程
的数据到内核
t->buffer = binder_alloc_new_buf(&target_proc->alloc, tr->data_size,
tr->offsets_size, extra_buffers_size,
!reply && (t->flags & TF_ONE_WAY));
//…
if (copy_from_user(t->buffer->data, (const void __user *)(uintptr_t)
tr->data.ptr.buffer, tr->data_size)) {
//…
}
if (copy_from_user(offp, (const void __user *)(uintptr_t)
tr->data.ptr.offsets, tr->offsets_size)) {
//…
}
- 这里复制的数据是
Binder调用
的参数数据,也是需要传递给服务进程
的数据,因此需要缓冲区。这个缓冲区是在目标进程
的大的缓冲区中分配的
- 处理传输中的
Binder对象
off_end = (void *)off_start + tr->offsets_size;
sg_bufp = (u8 *)(PTR_ALIGN(off_end, sizeof(void *)));
sg_buf_end = sg_bufp + extra_buffers_size;
off_min = 0;
for (; offp < off_end; offp++) {
//…
}
- 通过参数传递的
Binder对象
需要进行转换
,这里的for循环
就是进行转换操作,内容较多,会在下面处理传递的Binder对象
章节进行讲解
- 将本次调用的
binder_transaction
结构体链接到线程的binder_stack
列表中
if (!(t->flags & TF_ONE_WAY)) {
//…
t->need_reply = 1;
t->from_parent = thread->transaction_stack;
thread->transaction_stack = t;
// 书中下面注释部分的代码已经整合到 binder_proc_transaction 函数中了
// 首先,将结构 binder_transaction中的binder_work放到目标目标进程或线程的todo队列
// 然后,创建一个新的binder_work的结构体,并将它放到发送线程的todo队列
if (!binder_proc_transaction(t, target_proc, target_thread)) {
//… 失败处理
}
}
-
binder_transaction
结构中包含了一个binder_work
的结构体,因此它可以被放到todo队列
中 -
本地调用时
binder_work
的type
被设置为BINDER_WORK_TRANSACTION
后,插入了目标进程
或目标线程
的todo队列
中 -
为了使客户进程能收到回复消息,这里也会创建一个新的
binder_work
的结构体,并把它的type
设置成了BINDER_WORK_TRANSAXTION_COMPLETET
,并将它插入到当前线程的todo队列
中
前面介绍过,在binder_thread_write
函数执行完后,还会去判断是否需要执行binder_thread_read
函数:
-
由于调用端执行完
BC_TRANSACTION
后,会立刻执行ioctl的读指令 -
所以,在调用操作上,
binder_thread_read
函数算是紧接着binder_thread_write
函数后执行的
刚才新建的binder_work
的结构体已经插入todo队列
了,我们看下binder_thread_read
函数会进行哪些和todo队列
相关的操作:
while (1) {
//…
// 获得todo队列,获取失败则goto retry
if (!binder_worklist_empty_ilocked(&thread->todo))
list = &thread->todo;
else if (!binder_worklist_empty_ilocked(&proc->todo) &&
wait_for_proc_work)
list = &proc->todo;
else {
binder_inner_proc_unlock(proc);
/* no data added */
if (ptr - buffer == 4 && !thread->looper_need_return)
goto retry;
break;
}
//取出todo队列中的元素
w = binder_dequeue_work_head_ilocked(list);
//…
switch (w->type) {
//…省略大量case语句
case BINDER_WORK_TRANSACTION_COMPLETE: {
binder_inner_proc_unlock(proc);
cmd = BR_TRANSACTION_COMPLETE;
//把返回消息通过put_user放到用户空间的指针中
if (put_user(cmd, (uint32_t __user *)ptr))
return -EFAULT;
ptr += sizeof(uint32_t);
binder_stat_br(proc, thread, cmd);
kfree(w);
binder_stats_deleted(BINDER_STAT_TRANSACTION_COMPLETE);
} break;
//…省略大量case语句
}
//…
}
binder_thread_read
函数所做的处理就是把回复消息BR_TRANSACTION_COMPLETE
复制到用户空间
这样客户进程就可以收到回复消息了
- 到这里,
客户进程
的调用结束了。但是,Binder调用才完成一半,接下来,看看服务进程是如何调用数据的。
服务进程
的调用流程
服务进程
至少有一个线程会在ioctl上等待调用的到来。服务进
程调用ioctl时传递的是读数据的请求,所以最后调用的也是binder_thread_read
函数,我们看下binder_thread_read
函数完整的处理流程:
- 如果保存返回结果的缓冲区中还没有数据,先写入
BR_NOOP
消息:
if (*consumed == 0) {
if (put_user(BR_NOOP, (uint32_t __user *)ptr))
return -EFAULT;
ptr += sizeof(uint32_t);
}
- 进入循环处理所有
todo队列
中的工作
while (1) {
//…
}
- 读取线程或进程
todo队列
中需要完成的工作
struct binder_work *w = NULL;
//…
if (!binder_worklist_empty_ilocked(&thread->todo))
list = &thread->todo;
else if (!binder_worklist_empty_ilocked(&proc->todo) &&
wait_for_proc_work)
list = &proc->todo;
//…
w = binder_dequeue_work_head_ilocked(list);
- 用
switch
语句处理所有类型的工作
switch (w->type) {
case BINDER_WORK_TRANSACTION: {
binder_inner_proc_unlock(proc);
t = container_of(w, struct binder_transaction, work);
} break;
}
-
经过
客户进程的调用流程
后,此时的服务进程
中已经存在一个类型为BINDER_WORK_TRANSACTION
的工作需要处理 -
这里只是取出了和
binder_work
关联的binder_transaction
结构体指针 -
并保存到变量
t
中
- 调整线程的优先级
if (!t)
continue;
if (t->buffer->target_node) {
struct binder_node *target_node = t->buffer->target_node;
struct binder_priority node_prio;
tr.target.ptr = target_node->ptr;
tr.cookie = target_node->cookie;
node_prio.sched_policy = target_node->sched_policy;
node_prio.prio = target_node->min_priority;
binder_transaction_priority(current, t, node_prio,
target_node->inherit_rt);
cmd = BR_TRANSACTION;
}
- 这里复制的数据是
Binder调用
的参数数据,也是需要传递给服务进程
的数据,因此需要缓冲区。这个缓冲区是在目标进程
的大的缓冲区中分配的
- 处理传输中的
Binder对象
off_end = (void *)off_start + tr->offsets_size;
sg_bufp = (u8 *)(PTR_ALIGN(off_end, sizeof(void *)));
sg_buf_end = sg_bufp + extra_buffers_size;
off_min = 0;
for (; offp < off_end; offp++) {
//…
}
- 通过参数传递的
Binder对象
需要进行转换
,这里的for循环
就是进行转换操作,内容较多,会在下面处理传递的Binder对象
章节进行讲解
- 将本次调用的
binder_transaction
结构体链接到线程的binder_stack
列表中
if (!(t->flags & TF_ONE_WAY)) {
//…
t->need_reply = 1;
t->from_parent = thread->transaction_stack;
thread->transaction_stack = t;
// 书中下面注释部分的代码已经整合到 binder_proc_transaction 函数中了
// 首先,将结构 binder_transaction中的binder_work放到目标目标进程或线程的todo队列
// 然后,创建一个新的binder_work的结构体,并将它放到发送线程的todo队列
if (!binder_proc_transaction(t, target_proc, target_thread)) {
//… 失败处理
}
}
-
binder_transaction
结构中包含了一个binder_work
的结构体,因此它可以被放到todo队列
中 -
本地调用时
binder_work
的type
被设置为BINDER_WORK_TRANSACTION
后,插入了目标进程
或目标线程
的todo队列
中 -
为了使
客户进程
能收到回复消息,这里也会创建一个新的binder_work
的结构体,并把它的type
设置成了BINDER_WORK_TRANSAXTION_COMPLETET
,并将它插入到当前线程的todo队列
中
前面介绍过,在binder_thread_write
函数执行完后,还会去判断是否需要执行binder_thread_read
函数:
-
由于调用端执行完
BC_TRANSACTION
后,会立刻执行ioctl
的读
指令 -
所以,在调用操作上,
binder_thread_read
函数算是紧接着binder_thread_write
函数后执行的
刚才新建的binder_work
的结构体已经插入todo队列
了,我们看下binder_thread_read
函数会进行哪些和todo队列
相关的操作:
while (1) {
//…
// 获得todo队列,获取失败则goto retry
if (!binder_worklist_empty_ilocked(&thread->todo))
list = &thread->todo;
else if (!binder_worklist_empty_ilocked(&proc->todo) &&
wait_for_proc_work)
list = &proc->todo;
else {
binder_inner_proc_unlock(proc);
/* no data added */
if (ptr - buffer == 4 && !thread->looper_need_return)
goto retry;
break;
}
//取出todo队列中的元素
w = binder_dequeue_work_head_ilocked(list);
//…
switch (w->type) {
//…省略大量case语句
case BINDER_WORK_TRANSACTION_COMPLETE: {
binder_inner_proc_unlock(proc);
cmd = BR_TRANSACTION_COMPLETE;
//把返回消息通过put_user放到用户空间的指针中
if (put_user(cmd, (uint32_t __user *)ptr))
return -EFAULT;
ptr += sizeof(uint32_t);
binder_stat_br(proc, thread, cmd);
kfree(w);
binder_stats_deleted(BINDER_STAT_TRANSACTION_COMPLETE);
} break;
//…省略大量case语句
}
//…
}
-
binder_thread_read
函数所做的处理就是把回复消息BR_TRANSACTION_COMPLETE
复制到用户空间 -
这样
客户进程
就可以收到回复消息了
到这里,客户进程
的调用结束了。但是,Binder调用
才完成一半,接下来,看看服务进程
是如何调用数据的。
服务进程
的调用流程
服务进程
至少有一个线程会在ioctl上等待调用的到来。服务进程
调用ioctl时传递的是读数据的请求,所以最后调用的也是binder_thread_read
函数,我们看下binder_thread_read
函数完整的处理流程:
- 如果保存返回结果的缓冲区中还没有数据,先写入
BR_NOOP
消息:
if (*consumed == 0) {
if (put_user(BR_NOOP, (uint32_t __user *)ptr))
return -EFAULT;
ptr += sizeof(uint32_t);
}
- 进入循环处理所有
todo队列
中的工作
while (1) {
//…
}
- 读取线程或进程
todo队列
中需要完成的工作
struct binder_work *w = NULL;
//…
if (!binder_worklist_empty_ilocked(&thread->todo))
list = &thread->todo;
else if (!binder_worklist_empty_ilocked(&proc->todo) &&
wait_for_proc_work)
list = &proc->todo;
//…
w = binder_dequeue_work_head_ilocked(list);
- 用
switch语句
处理所有类型的工作
switch (w->type) {
case BINDER_WORK_TRANSACTION: {
binder_inner_proc_unlock(proc);
t = container_of(w, struct binder_transaction, work);
} break;
}
-
经过
客户进程的调用流程
后,此时的服务进程
中已经存在一个类型为BINDER_WORK_TRANSACTION
的工作需要处理 -
这里只是取出了和
binder_work
关联的binder_transaction
结构体指针 -
并保存到变量
t
中
- 调整线程的优先级
if (!t)
continue;
if (t->buffer->target_node) {
struct binder_node *target_node = t->buffer->target_node;
struct binder_priority node_prio;
tr.target.ptr = target_node->ptr;
tr.cookie = target_node->cookie;
node_prio.sched_policy = target_node->sched_policy;
node_prio.prio = target_node->min_priority;
binder_transaction_priority(current, t, node_prio,
target_node->inherit_rt);
cmd = BR_TRANSACTION;
}
-
如果t为NULL,继续循环
-
否则,开始准备返回的消息
BR_TRANSACTION
-
同时,设置线程的优先级
-
如果
调用线程
的优先级低于当前线程
指定的最低优先级
,则把当前线程
的优先级设为调用线程
的优先级 -
否则,把
当前线程
设为指定的最低优先级
-
这意味着
Binder线程
会以尽量低的优先级运行
- 准备返回的数据
tr.code = t->code;
tr.flags = t->flags;
tr.sender_euid = from_kuid(current_user_ns(), t->sender_euid);
//…
// 让tr中的data指针指向内核中保存的数据缓冲区
tr.data_size = t->buffer->data_size;
tr.offsets_size = t->buffer->offsets_size;
tr.data.ptr.buffer = (binder_uintptr_t)
((uintptr_t)t->buffer->data +
binder_alloc_get_user_buffer_offset(&proc->alloc));
tr.data.ptr.offsets = tr.data.ptr.buffer +
ALIGN(t->buffer->data_size,
sizeof(void *));
// 把 BR_TRANSACTION 消息复制到用户空间
if (put_user(cmd, (uint32_t __user *)ptr)) {
//…
return -EFAULT;
}
ptr += sizeof(uint32_t);
if (copy_to_user(ptr, &tr, sizeof(tr))) {
//把结构体tr数据复制到用户空间
//…
return -EFAULT;
}
ptr += sizeof(tr);
//…
break;//跳出while循环
这一段代码都是为消息BR_TRANSACTION
准备返回数据,要注意的是:
-
调用
copy_to_user
复制到用户空间
的只是结构体tr
的数据 -
服务进程
得到这个结构体之后,会直接读取它里面的data指针
的数据 -
数据准备完毕后,使用
break语句
跳出while循环
- 启动新线程
if (proc->requested_threads == 0 &&
list_empty(&thread->proc->waiting_threads) &&
proc->requested_threads_started < proc->max_threads &&
(thread->looper & (BINDER_LOOPER_STATE_REGISTERED |
BINDER_LOOPER_STATE_ENTERED)) /* the user-space code fails to */
/*spawn a new thread if we leave this out */) {
proc->requested_threads++;
//…
if (put_user(BR_SPAWN_LOOPER, (uint32_t __user *)buffer))
return -EFAULT;
}
当前线程要执行Binder调用
,新来的调用也需要线程来处理,因此:
-
函数结束前会检查进程中可用的线程数
-
如果需要创建新线程,则在返回的
buffer数据
中增加BR_SPAWN_LOOPER
消息,服务进程
收到这个消息会启动新的线程
完整的Binder调用过程还需要把回复消息传递给客户进程,这个过程使用的函数还是前面的这些,暂时不分析了。消化一下先
前面介绍了Binder对象
传递的原理和用户层的实现。(原理上整个人还是晕晕的),我们来看下Binder驱动
如何实现Binder对象
的传递的。
-
Binder驱动
中,代表每个进程的结构binde_proc
中有两个字段:nodes
和refs_by_node
-
这两个字段各指向两颗红黑树的头
-
nodes
:指向的是Binder节点对象表
,储存本进程中Binder实体对象
相关的数据 -
refs_by_node
:指向的是Binder引用对象表
,存储本进程的Binder引用对象
的数据以及对应的实体对象的节点指针
来个抽象点的图:
Binder驱动
中处理对象转换的代码位于函数binder_transaction
中
case BINDER_TYPE_BINDER:
case BINDER_TYPE_WEAK_BINDER: {
struct flat_binder_object *fp;
fp = to_flat_binder_object(hdr);
ret = binder_translate_binder(fp, t, thread);
} break;
static int binder_translate_binder(struct flat_binder_object *fp,
struct binder_transaction *t,
struct binder_thread *thread)
{
struct binder_node *node;
struct binder_proc *proc = thread->proc;
struct binder_proc *target_proc = t->to_proc;
struct binder_ref_data rdata;
int ret = 0;
//根据binder值查找Binder对象表中的节点
node = binder_get_node(proc, fp->binder);
if (!node) { //没有则新建一个节点
node = binder_new_node(proc, fp);
if (!node)
return -ENOMEM;
}
//…
if (security_binder_transfer_binder(proc->tsk, target_proc->tsk)) {
ret = -EPERM;
goto done;
}
// 在接收端进程中寻找节点的引用,找不到会创建一个新的引用
ret = binder_inc_ref_for_node(target_proc, node,
fp->hdr.type == BINDER_TYPE_BINDER,
&thread->todo, &rdata);
if (ret)
goto done;
// 将传递的binder数据结构的type的值改为 BINDER_TYPE_HANDLE
if (fp->hdr.type == BINDER_TYPE_BINDER)
fp->hdr.type = BINDER_TYPE_HANDLE;
else
fp->hdr.type = BINDER_TYPE_WEAK_HANDLE;
fp->binder = 0;
// rdata.desc存放的是节点引用表中的序号,赋值给handle
fp->handle = rdata.desc;
fp->cookie = 0;
//…
}
上面的流程是
-
通过
binder_get_node
函数在发送进程的Binder对象节点表
中查找节点 -
查找是通过比较数据中的
binder字段
和节点中对应的字段进行的 -
binder字段
中存放的是Binder对象
的弱引用指针 -
如果没找到
-
新建节点
-
把
binder字段
、cookie字段
的值保存到新节点 -
通过
binder_inc_ref_for_node
函数在目标进程查找Binder节点
的引用
。 -
没找到会新建一个
-
引用
指的是节点引用表
中的refs_by_node
节点,包含指向Binder节点
的指针 -
把数据的
type
字段的值改为BINDER_TYPE_HANDLE
或BINDER_TYPE_WEAK_HANDLE
-
把
handle
字段的值设为在节点引用表
的序号,这也是Binder引用对象
中handle值
的来历
下面再看看如何处理类型BINDER_TYPE_HANDLE
或BINDER_TYPE_WEAK_HANDLE
的
case BINDER_TYPE_HANDLE:
case BINDER_TYPE_WEAK_HANDLE: {
struct flat_binder_object *fp;
fp = to_flat_binder_object(hdr);
ret = binder_translate_handle(fp, t, thread);
} break;
static int binder_translate_handle(struct flat_binder_object *fp,
struct binder_transaction *t,
struct binder_thread *thread)
{
//…
// 通过handle值查找节点引用
node = binder_get_node_from_ref(proc, fp->handle,
fp->hdr.type == BINDER_TYPE_HANDLE, &src_rdata);
if (!node) {
//…
return -EINVAL;
}
//…
if (node->proc == target_proc) {
// 如果目标进程就是Binder对象的进程,开始转换
if (fp->hdr.type == BINDER_TYPE_HANDLE)
fp->hdr.type = BINDER_TYPE_BINDER;
else
fp->hdr.type = BINDER_TYPE_WEAK_BINDER;
fp->binder = node->ptr;
fp->cookie = node->cookie;
if (node->proc)
binder_inner_proc_lock(node->proc);
binder_inc_node_nilocked(node,
fp->hdr.type == BINDER_TYPE_BINDER,
0, NULL);
//…
} else {
//…
// 如果不是,则在目标进程新建一个Binder节点的引用
ret = binder_inc_ref_for_node(target_proc, node,
fp->hdr.type == BINDER_TYPE_HANDLE,
NULL, &dest_rdata);
//…
fp->handle = dest_rdata.desc;
fp->cookie = 0;
trace_binder_transaction_ref_to_ref(t, node, &src_rdata,
//…
}
}
这部分代码的流程是:
-
通过传输数据的
handle字段
在发送进程的节点引用表
中查找 -
正常情况下是可以找到,不能就错误返回
-
如果目标进程就是Binder对象所在的进程
-
开始进行转换,把数据的
type字段
转为BINDER_TYPE_BINDER
或BINDER_TYPE_WEAK_BINDER
BINDER_WORK_TRANSACTION -
把
binder
和cookie
字段设置为节点中保存的值 -
如果目标进程不是Binder对象所在的进程
-
在目标对象中建立一个节点对象的引用
到这里呢,Binder原理部分就差不多了,已经了解了包括:
-
客户端
调用
、接收
消息的过程 -
服务端
监听
、回复
消息的过 -
驱动
传输数据
、调度线程
的操作
现在,我们再来看最后的一小部分:ServiceManager
的作用
=================================================================================
关于ServiceManager
,先简单描述:
-
ServiceManager
是Binder架构
中用来解析Binder名称
的模块 -
ServiceManager
本身就是一个Binder服务
-
ServiceManager
并没有使用libbinder
来构建Binder服务
-
2020-09-04 同步了下项目代码,发现也替换成
libbinder
那一套了。。。。。 -
不确定是不是更换仓储了
-
要是这样就没意思了,简易版本对于加深
binder
理解还是很有帮助的 -
我们暂且按照老的版本来看下吧
-
ServiceManager
自己实现了一个简单的binder框架
来直接和驱动通信。。。
ServiceManager
源码路径在:frameworks/native/cmds/servicemanager
,主要包含两个文件:
-
binder.c
:用来实现简单的Binder通信功能 -
简单版本的
binder协议
实现 -
直接的
ioctl
操作与binder驱动
通信 -
不是本节的重点,感兴趣可以参照源码来学习啦
-
service_manager.c
:用来实现ServiceManager
的业务逻辑 -
重点是了解
ServiceManager
如何响应Binder服务
的注册和查询的 -
这么重要的服务要在什么时间启动呢?
-
这部分是在
system/core/rootdir/init.rc
中配置
on post-fs
…
start essential services
start logd
start servicemanager
start hwservicemanager
start vndservicemanager
-
hwservicemanager
用来支持HIDL
-
vndservicemanager
第三方厂商使用,应该是从Treble架构
中出现的
ServiceManager
的架构
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
最后
希望大家能有一个好心态,想进什么样的公司要想清楚,并不一定是大公司,我选的也不是特大厂。当然如果你不知道选或是没有规划,那就选大公司!希望我们能先选好想去的公司再投或内推,而不是有一个公司要我我就去!还有就是不要害怕,也不要有压力,平常心对待就行,但准备要充足。最后希望大家都能拿到一份满意的 offer !如果目前有一份工作也请好好珍惜好好努力,找工作其实挺累挺辛苦的。
这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
解析Binder名称
的模块
-
ServiceManager
本身就是一个Binder服务
-
ServiceManager
并没有使用libbinder
来构建Binder服务
-
2020-09-04 同步了下项目代码,发现也替换成
libbinder
那一套了。。。。。 -
不确定是不是更换仓储了
-
要是这样就没意思了,简易版本对于加深
binder
理解还是很有帮助的 -
我们暂且按照老的版本来看下吧
-
ServiceManager
自己实现了一个简单的binder框架
来直接和驱动通信。。。
ServiceManager
源码路径在:frameworks/native/cmds/servicemanager
,主要包含两个文件:
-
binder.c
:用来实现简单的Binder通信功能 -
简单版本的
binder协议
实现 -
直接的
ioctl
操作与binder驱动
通信 -
不是本节的重点,感兴趣可以参照源码来学习啦
-
service_manager.c
:用来实现ServiceManager
的业务逻辑 -
重点是了解
ServiceManager
如何响应Binder服务
的注册和查询的 -
这么重要的服务要在什么时间启动呢?
-
这部分是在
system/core/rootdir/init.rc
中配置
on post-fs
…
start essential services
start logd
start servicemanager
start hwservicemanager
start vndservicemanager
-
hwservicemanager
用来支持HIDL
-
vndservicemanager
第三方厂商使用,应该是从Treble架构
中出现的
ServiceManager
的架构
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-VwPHlZcC-1713678026394)]
[外链图片转存中…(img-Zqikp2xw-1713678026395)]
[外链图片转存中…(img-H06gtNRH-1713678026396)]
[外链图片转存中…(img-sTZCLGSn-1713678026397)]
[外链图片转存中…(img-YBQcOYHE-1713678026398)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
最后
希望大家能有一个好心态,想进什么样的公司要想清楚,并不一定是大公司,我选的也不是特大厂。当然如果你不知道选或是没有规划,那就选大公司!希望我们能先选好想去的公司再投或内推,而不是有一个公司要我我就去!还有就是不要害怕,也不要有压力,平常心对待就行,但准备要充足。最后希望大家都能拿到一份满意的 offer !如果目前有一份工作也请好好珍惜好好努力,找工作其实挺累挺辛苦的。
这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
由于篇幅有限,这里以图片的形式给大家展示一小部分。
[外链图片转存中…(img-0eBXr42K-1713678026400)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!