深入Android系统 Binder-4-驱动

  • offsets_size表示数据区IPC对象的大小

Binder消息则根据发送接收分为两套,从命名上也比较容易区分:

  • 发送类型的指的是消息从应用层驱动的过程,通常以BC_开头

  • 接收类型的指的是消息从驱动应用层的过程,通常以BR_开头

发送给驱动的Binder命令列表

| 命令 | 说明 |

| — | — |

| BC_TRANSACTION | 发送Binder调用的数据 |

| BC_REPLY | 返回Binder调用的返回值 |

| BC_ACQUIRE_RESULT | 回应BR_ATTEMPT_ACQUIRE命令 |

| BC_FREE_BUFFER | 释放通过mmap分配的内存块 |

| BC_INCREFS | 增加Binder对象的弱引用计数 |

| BC_ACQUIRE | 增加Binder对象的强引用计数 |

| BC_DECREFS | 减少Binder对象的弱引用计数 |

| BC_RELEASE | 减少Binder对象的强引用计数 |

| BC_INCREFS_DONE | 回应BC_INCREFS指令 |

| BC_ACQUIRE_DONE | 回应BC_ACQUIRE指令 |

| BC_ATTEMPT_ACQUIRE | 将Binder对象的弱引用升级为强引用 |

| BC_REGISTER_LOOPER | 把当前线程注册为线程池的主线程 |

| BC_ENTER_LOOPER | 通知驱动,线程可以进入数据的发送和接收 |

| 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驱动分析


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驱动中并没有实现常用的readwrite操作。数据的传输都是通过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的内存共享机制


前面介绍了,用户进程打开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操作


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驱动所有的数据结构。我们先从调用的整体流程上来看下:

  • 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的情况:

  1. 找到代表目标进程的节点:

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;

//…

}

  1. 搜寻目标线程

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

  1. 如果有目标线程,则使用目标线程中的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调用的参数数据,也是需要传递给服务进程的数据,因此需要缓冲区。这个缓冲区是在目标进程的大的缓冲区中分配的
  1. 处理传输中的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对象章节进行讲解
  1. 将本次调用的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_worktype被设置为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函数完整的处理流程:

  1. 如果保存返回结果的缓冲区中还没有数据,先写入BR_NOOP消息:

if (*consumed == 0) {

if (put_user(BR_NOOP, (uint32_t __user *)ptr))

return -EFAULT;

ptr += sizeof(uint32_t);

}

  1. 进入循环处理所有todo队列中的工作

while (1) {

//…

}

  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);

  1. 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

  1. 调整线程的优先级

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调用的参数数据,也是需要传递给服务进程的数据,因此需要缓冲区。这个缓冲区是在目标进程的大的缓冲区中分配的
  1. 处理传输中的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对象章节进行讲解
  1. 将本次调用的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_worktype被设置为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函数完整的处理流程:

  1. 如果保存返回结果的缓冲区中还没有数据,先写入BR_NOOP消息:

if (*consumed == 0) {

if (put_user(BR_NOOP, (uint32_t __user *)ptr))

return -EFAULT;

ptr += sizeof(uint32_t);

}

  1. 进入循环处理所有todo队列中的工作

while (1) {

//…

}

  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);

  1. 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

  1. 调整线程的优先级

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线程会以尽量低的优先级运行

  1. 准备返回的数据

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循环

  1. 启动新线程

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对象的传递的。

  • Binder驱动中,代表每个进程的结构binde_proc中有两个字段:nodesrefs_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;
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

学习福利

【Android 详细知识点思维脑图(技能树)】

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

r_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;
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-uEkNCtkv-1712015248385)]

[外链图片转存中…(img-Ig7kxSEU-1712015248386)]

[外链图片转存中…(img-p1JaY9Qh-1712015248387)]

[外链图片转存中…(img-L4QnAUMA-1712015248387)]

[外链图片转存中…(img-yzcFQz2a-1712015248388)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

学习福利

【Android 详细知识点思维脑图(技能树)】

[外链图片转存中…(img-zs4WXkgO-1712015248388)]

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,很多高级职位给的薪资真的特别高(钱多也不一定能找到合适的),所以努力让自己成为高级工程师才是最重要的。

这里附上上述的面试题相关的几十套字节跳动,京东,小米,腾讯、头条、阿里、美团等公司19年的面试题。把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。

由于篇幅有限,这里以图片的形式给大家展示一小部分。

[外链图片转存中…(img-vHBu3O3G-1712015248388)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 27
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值