Android 跨进程通信-(五)Binder机制之一次拷贝的原理

目录

前言

一 概述

二 ServiceManager进程接收到指令

1.ServiceManager进程循环等待接收指令

2.Kernel层binder_ioctl()匹配BINDER_WRITE_READ指令

三 ServiceManager进程binder_parse()解析BR_TRANSACTION指令

1.ServiceManager进程binder_send_reply()将BC_REPLY指令发给Binder驱动

2.Kernel中的binder_ioctl()处理BINDER_WRITE_READ指令

四 总结


前言

Android 跨进程通信-(二)Binder机制之ServiceManager

Android 跨进程通信-(三)Binder机制之Client

Android 跨进程通信-(四)Binder机制之Server

前面的三篇主要是总结了在Binder通信中的四个组成部分在用户空间的ServiceManager进程、Client进程、Server进程是怎么创建出来的,那么三者是怎么通信的呢?

一 概述

ServiceManager进程、Client进程、Server进程运行在用户空间,三者之间是不能相互通信的,都必须通过内核空间的Binder驱动进行中转。简单用一个图来表示如下:

二 ServiceManager进程接收到指令

当Server进程发出注册Service的请求的时候,同样要先将请求发送给Binder驱动,然后该Server进程进入休眠,然后等到目标进程处理完请求之后,给予响应。这个过程以后在去研究,这次重点想了解下目标进程是怎么处理请求的。

1.ServiceManager进程循环等待接收指令

ServiceManager进程主要通过binder_loop()事件循环机制,在for循环中不停等待Binder驱动发送的指令,然后通过binder_parse()解析Binder驱动发过来的指令进行处理,具体代码对应在frameworks/native/cmds/servicemanager/builder.c中的binder_loop(),大体代码如下:

void binder_loop(struct binder_state *bs, binder_handler func)
{   
     //......   
     for (;;) {  
        bwr.read_size = sizeof(readbuf);
        bwr.read_consumed = 0;
        bwr.read_buffer = (uintptr_t) readbuf;

        res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);
        //......
        res = binder_parse(bs, reply, (uintptr_t) readbuf, bwr.read_consumed, 0);
     //......
}

当Binder驱动接收到Client或者Server发出查询、注册等请求的时候,此时就会返回到ServiceManager进程的该循环,通过binder_parse()来解析Binder驱动发过来的事件。

当ServiceManager进程发送BINDER_WRITE_READ指令的时候,最终通过系统调用就会调用到kernel的build_ioctl()。

2.Kernel层binder_ioctl()匹配BINDER_WRITE_READ指令

在Kernel的代码具体实现对应kernel/goldfish/drivers/staging/android/binder.c的binder_ioctl()方法,流程如下:

static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg)
{
    int ret;
    //1.从传入的文件描述获取到包含该进程信息的结构体
    struct binder_proc *proc = filp->private_data;
    struct binder_thread *thread;
    //从用户空间传过来的要写入的信息
     void __user *ubuf = (void __user *)arg;

    ......
    //2.获取描述当前线程的结构体binder_thread
    thread = binder_get_thread(proc);
   ......
    //3.各种ioctl指令的匹配
    switch (cmd) {
	    case BINDER_WRITE_READ:
		    struct binder_write_read bwr;
		   //3.将数据从用户空间拷贝到内核空间的bwr结构体的变量中
		if (copy_from_user(&bwr, ubuf, sizeof(bwr))) {
			ret = -EFAULT;
			goto err;
		}
	 ......
        //4.判断结构体中的write_size>0,则进行写操作
		if (bwr.write_size > 0) {
			ret = binder_thread_write(proc, thread, (void __user *)bwr.write_buffer, bwr.write_size, &bwr.write_consumed);
			if (ret < 0) {
				bwr.read_consumed = 0;
				if (copy_to_user(ubuf, &bwr, sizeof(bwr)))
					ret = -EFAULT;
				goto err;
			}
		}
        //4.判断结构体中的read_size>0,则进行读操作
		if (bwr.read_size > 0) {
			ret = binder_thread_read(proc, thread, (void __user *)bwr.read_buffer, bwr.read_size, &bwr.read_consumed, filp->f_flags & O_NONBLOCK);
			if (!list_empty(&proc->todo))
				wake_up_interruptible(&proc->wait);
			if (ret < 0) {
				if (copy_to_user(ubuf, &bwr, sizeof(bwr)))
					ret = -EFAULT;
				goto err;
			}
		}
	 ......
        //5.将处理完的数据拷贝到用户空间的变量中
		if (copy_to_user(ubuf, &bwr, sizeof(bwr))) {
			ret = -EFAULT;
			goto err;
		}
		break;
    }
   //......

    }
    ret = 0;
    return ret;
   //......
}

该binder_ioctl()中的主要流程如下:

  • (1)将Native传入的binder_write_read结构体保存到ubuf;
  • (2)通过copy_from_user()将ubuf的内容拷贝到binder_write_read结构体bwr中(此时从用户空间拷贝到内核空间);
  • (3)根据binder_write_read结构体中的write_size>0还是read_size>0来决定执行写操作还是读操作。
  • (4)通过copy_to_user()将bwr中的内容拷贝到用户空间的ubuf中(此时从内核空间拷贝到用户空间)

遗留问题:之前在去了解内存映射的时候,进程可以直接读写内存,而不需要任何数据的拷贝,但是为什么这里还要执行copy_from_user()/过copy_to_user()??这个怎么理解呢?

解答:我觉得这里可能仅仅是copy_from_user()这里应该指的仅是将目前的内核空间的这个变量进行赋值;而copy_to_user()是对用户空间的变量进行更新。

从ServiceManager进程发出的结构体中 bwr.read_size>0,所以调用到binder_thread_read(),从而完成Binder驱动将会BR_TRANSACTION指令发给到ServiceManager进程。

三 ServiceManager进程binder_parse()解析BR_TRANSACTION指令

ServiceManager进程通过binder_parse()匹配到对应的BR_TRANSACTION,最后调用到binder_send_reply()将Server进程需要的数据返回给Binder驱动。

int binder_parse(struct binder_state *bs, struct binder_io *bio,
                 uintptr_t ptr, size_t size, binder_handler func)
{
     ........   
        case BR_TRANSACTION: {
            struct binder_transaction_data *txn = (struct binder_transaction_data *) ptr;
            .......
            if (func) {
                .......
                else {
                    binder_send_reply(bs, &reply, txn->data.ptr.buffer, res);
                }
            }
            ptr += sizeof(*txn);
            break;
        }
    ........ 
 } 

1.ServiceManager进程binder_send_reply()将BC_REPLY指令发给Binder驱动

从代码中可以看出,ServiceManager进程最终将BC_REPLY指令通过binder_write()发给Binder驱动。

void binder_send_reply(struct binder_state *bs,
                       struct binder_io *reply,
                       binder_uintptr_t buffer_to_free,
                       int status)
{
.......
    data.cmd_free = BC_FREE_BUFFER;
    data.buffer = buffer_to_free;
    data.cmd_reply = BC_REPLY;
.......
    binder_write(bs, &data, sizeof(data));
}

int binder_write(struct binder_state *bs, void *data, size_t len)
{
    struct binder_write_read bwr;
    int res;
    //设置write_size长度,说明这是一个写操作
    bwr.write_size = len;
    bwr.write_consumed = 0;
    bwr.write_buffer = (uintptr_t) data;
    bwr.read_size = 0;
    bwr.read_consumed = 0;
    bwr.read_buffer = 0;
    //将创建的"/dev/binder"文件描述符以及binder_write_read 都传入了kernel的ioctl()
    res = ioctl(bs->fd, BINDER_WRITE_READ, &bwr);
    if (res < 0) {
        fprintf(stderr,"binder_write: ioctl failed (%s)\n",
                strerror(errno));
    }
    return res;
}

此时又通过ioctl(),Binder驱动BINDER_WRITE_READ指令,这就进入到Binder机制中的一次拷贝原理的地方。

2.Kernel中的binder_ioctl()处理BINDER_WRITE_READ指令

通过switch/case的匹配,最终调用binder_thread_write()来实现写操作。在binder_thread_write()真正的实现了从一个进程到另外一个进程的数据拷贝。在binder_thread_write()中根据不同的case匹配到BC_REPRY:

int binder_thread_write(
    struct binder_proc *proc, 
    struct binder_thread *thread,
    void __user *buffer, 
    int size, 
    signed long *consumed
){
// .......
    swtich(cmd){
        case BC_TRANSACTION:

        case BC_REPLY: {
            struct binder_transaction_data tr;
            if (copy_from_user(&tr, ptr, sizeof(tr)))
                return -EFAULT;
            ptr += sizeof(tr);
//调用binder_transaction来实现具体的数据拷贝
            binder_transaction(proc, thread, &tr, cmd == BC_REPLY);
            break;
        }
    }
}

在匹配到BC_REPLY的时候,首先通过copy_from_user()将数据从发送进程在用户空间的虚拟内存拷贝到在内核空间中对应的虚拟内存中。

一个小点:

在通过Binder驱动传递数据的时候,需要通过Binder驱动的内存空间来传递数据。(在内核中传递数据是通过内存来传递的。)

或者在详细一点这个逻辑:上一步将数据从发送进程的虚拟内存空间(也就是用户空间)拷贝到内核的虚拟内存空间之后,在用Binder驱动传递数据的时候,需要从该发送进程对应的内核的虚拟内存空间拷贝到Binder驱动分配的内存,才可以完成数据的传递。

经过上面将数据从发送进程在用户空间的虚拟内存拷贝到对应的内核空间的虚拟内存之后,还需要将数据拷贝到Binder驱动分配的内存空间,该过程主要通过binder_transaction()来实现内核的数据拷贝,进入到binder_transaction()看下几个主要逻辑:

//binder_transaction(proc, thread, &tr, cmd == BC_REPLY);
static void binder_transaction(
        struct binder_transaction_data *tr, 
        int reply
    ){
        //1.根据穿进入到handle来看下是其他进程还是ServiceManager进程,然后找到对应的目标进程
        if (tr->target.handle) {
        //if传进来的handle不是0,在当前进程下,根据handle找到binder_ref结构体
            struct binder_ref *ref;
            ref = binder_get_ref(proc, tr->target.handle);//查找binder_ref结构体
            target_node = ref->node;//通过ref找到对应的node节点
        } else {
        //handle是0表示是service_manager
            target_node = binder_context_mgr_node;//这个是特殊的进程,在binder_ioctl中创建
        }
        target_prc = target_node->proc;//拿到目标进程的结构体描述
        //2.从目标进程里面分配内存给t->buffer
        t->buffer = binder_alloc_buf(target_proc, tr->data_size,
        tr->offsets_size, !reply && (t->flags & TF_ONE_WAY));
        //3.将data和offset都复制拷贝到了目的进程
        copy_from_user(t->buffer->data, tr->data.ptr.buffer, tr->data_size);
        //tr->data.ptr.offsets是flat_binder_object的指针数组,就是binder_io前面那四个字节的数据
}

这个方法的主要流程就是

  • 1)根据传入的handle去对应的目标进程:如果handle不为0,则是其他进程;如果为0,就是ServiceMananger进程;
  • 2)最重要的就是 binder_alloc_buf(target_proc,...)用来分配Binder驱动的内存空间,详细说明下:从目标进程对应的内核的虚拟内存地址(在创建目标进程时不仅会分配进程的虚拟内存空间,还会在内核空间的对应分配虚拟内存空间的地址)开始分配内存给t->buffer,由于内核的虚拟内存空间和用户空间的虚拟内存空间有内存映射关系,所以在执行copy_from_user的时候,其实就是之间把发送进程的数据直接拷贝到了目标进程的内存中
  • 3)剩下的就是一些链表信息了。

四 总结

从上面的源码分析中可以看到,所谓的Binder的一次拷贝数据指的是:在两个进程通过Binder驱动传输数据的时候,首先需要将数据从发送进程的用户空间拷贝到内核空间,在内核之间传递数据的时候,由于Binder驱动直接从目标进程的内核对应的虚拟内存空间开始分配的内存空间&&目标进程的内核空间和进程的虚拟内存空间的内存映射关系,所以从发送进程对应的内核的虚拟内存空间拷贝到Binder驱动的内存空间,相当于直接拷贝到了目标进程的用户空间中。这里就是由于内存映射Binder机制中的一次拷贝的真正所在。简单的用图表示下这个过程:

简单描述下“从发送进程到目标进程的数据传输”这个过程示意图:

  • 1.在数据传输之前,发送进程和目标进程都会在用户空间分配虚拟内存,在内核空间也会对应分配虚拟内存,并且都会映射到一块物理内存(即/proc/binder/proc/进程ID)
  • 2.数据传输过程:
  • (1)将数据从发送进程在用户空间的虚拟内存拷贝到内核空间对应的虚拟内存
  • (2)通过Binder驱动传递数据(在内核中通过内存传递数据),需要将数据从发送进程在内核空间对应的虚拟内存拷贝到Binder驱动的内存
  • (3)由于Binder驱动的分配的内存是从目标进程在内核空间对应的虚拟内存开始的,所以(2)过程相当于直接拷贝到目标进程在内核空间对应的虚拟内存中;
  • (4)而目标进程在用户空间的虚拟内存在内核空间对应的虚拟内存存在内存映射,所以Binder驱动在传输数据的时候,相当于直接将数据发送进程在内核空间对应的虚拟内存空间拷贝到目的进程的用户空间的虚拟内存空间中。

经过上面几步就完成了Binder机制所谓的一次拷贝,其实我觉得应该指的是Binder驱动的一次拷贝。

像其他的Linux的IPC,如管道和信号基本上都是四次拷贝:

  • (1)从发送进程在用户空间对应虚拟内存拷贝到在内核空间对应的虚拟内存
  • (2)内核将数据拷贝到管道或信号的内存中;
  • (3)从管道或信号的内存拷贝到目标进程在内核空间对应的虚拟内存中;
  • (4)从目标进程在内核空间对应的虚拟内存拷贝到在用户空间对应的虚拟内存中。

另外在Binder驱动的内存开辟内存的空间大小的时候,也是以发送数据的大小来开辟内存空间。相对于Linux通常的IPC的接收数据的缓冲区都是由接收进程自己创建的,由于接收进程并不知道需要多大的空间来存放将要传过来的数据,因此只能开辟尽可能大的内存空间或者先调用API接收消息头来获取消息体的大小,浪费时间或空间。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值