精华内容
下载资源
问答
  • 系列的组件组成,包括 Client、Server、ServiceManager、Binder 驱动。其中 Client、Server、Service Manager 运行在用户空间,Binder 驱动运行在内核空间。其中 Service Manager 和 Binder 驱动由系统提供,而 ...
    因为Android是基于Linux系统内核的,所以在学习Binder一次拷贝原理之前必须先学习一下Linux操作系统的基本知识。
    

    用户空间和内核空间

    • linux系统运行模式分为两层:高优先级模式(特权模式),低优先级模式(普通模式)。在 CPU 的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如清内存、设置时钟等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加。所以,CPU 将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通应用程序只能使用那些不会造成灾难的指令。
    • linux系统在高优先级模式中运行系统内核代码以及与硬件密切相关的代码。 低优先级运行营运程序与硬件无关部分。应用程序不能直接操控硬件或者调用内核函数,需借助一系列接口函数申请让系统调用相关代码在内核空间运行,获取代码运行权限。
    • Linux 操作系统和驱动程序运行在内核空间,应用程序运行在用户空间。每一个系统进程都拥有自己私有的地址空间和数据,用户空间造成的进程错误会被局部化,而不会影响到内核或者其他进程。每个应用程序或者进程都会有自己特定的地址、私有数据空间,程序之间一般不会相互影响。
    • 内核态与用户态:当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。
      在内核态下,进程运行在内核地址空间中,此时 CPU 可以执行任何指令。运行的代码也不受任何的限制,可以自由地访问任何有效地址,也可以直接进行端口的访问。在用户态下,进程运行在用户地址空间中,被执行的代码要受到 CPU 的诸多检查,它们只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址,且只能对任务状态段(TSS)中 I/O 许可位图(I/O Permission Bitmap)中规定的可访问端口进行直接访问。

    内核地址空间划分

    一、32位Linux系统虚拟地址空间
    32位Linux内核虚拟地址空间划分0-3G为用户空间,3~4G为内核空间(注意,内核可以使用的线性地址只有1G)。注意这里是32位内核地址空间划分,64位内核地址空间划分是不同的(64位Linux内核不存在高端内存,因为64位内核可以支持超过512GB内存。若机器安装的物理内存超过内核地址空间范围,就会存在高端内存)。如下图所示:用户空间内存分为3G,内核空间内存分为1G。
    在这里插入图片描述
    二、逻辑内存地址与物理内存映射(内存条物理地址)
    当内核模块代码或线程访问内存时,代码中的内存地址都为逻辑地址,而对应到真正的物理内存地址,需要地址一对一的映射;
    假如内核逻辑地址空间访问为0xc0000000 ~ 0xffffffff,那么对应的物理内存范围就为0×0 ~ 0×40000000,即只能访问1G物理内存。若机器中安装8G物理内存,那么内核就只能访问前1G物理内存,后面7G物理内存将会无法访问,因为内核的地址空间已经全部映射到物理内存地址范围0×0 ~ 0×40000000。

    三、Linux内核高端内存
    上面提到了内核就只能访问前1G物理内存,后面7G物理内存将会无法访问,所以不能将内核地址空间0xc0000000 ~ 0xfffffff全部用来简单的地址映射。x86架构中将内核地址空间划分三部分:ZONE_DMAZONE_NORMALZONE_HIGHMEMZONE_HIGHMEM即为高端内存,这就是内存高端内存概念的由来。
    在x86结构中,三种类型的区域(从3G~4G计算)如下:

    • ZONE_DMA: 内存开始的16MB
    • ZONE_NORMAL :16MB~896MB,880M
    • ZONE_HIGHMEM :896MB ~ 结束(1G),128M
      在这里插入图片描述

    高端内存HIGH_MEM地址空间范围为 0xF8000000 ~ 0xFFFFFFFF(896MB~1024MB)。那么如内核是如何借助128MB高端内存地址空间是如何实现访问可以所有物理内存?

    当内核想访问高于896MB物理地址内存时,从0xF8000000 ~ 0xFFFFFFFF地址空间范围内找一段相应大小空闲的逻辑地址空间,借用一会。借用这段逻辑地址空间,建立映射到想访问的那段物理内存(即填充内核PTE页面表),临时用一会,用完后归还。
    所以高端内存的最基本思想:借一段地址空间,建立临时地址映射,用完后释放,达到这段地址空间可以循环使用,从而访问所有物理内存。

    四、Linux内核高端内存的划分
    内核将高端内存划分为3部分:VMALLOC_START ~ VMALLOC_END、KMAP_BASE ~ FIXADDR_START 和 FIXADDR_START ~ 4G
    在这里插入图片描述
    对 于高端内存,可以通过 alloc_page() 或者其它函数获得对应的 page,但是要想访问实际物理内存,还得把 page 转为线性地址才行,也就是说,我们需要为高端内存对应的 page 找一个线性空间,这个过程称为高端内存映射。
    对应高端内存的3部分,高端内存映射有三种方式:

    • 映射到”内核动态映射空间”(noncontiguous memory allocation)
      这种方式很简单,因为通过 vmalloc() ,在”内核动态映射空间”申请内存的时候,就可能从高端内存获得页面(参看 vmalloc 的实现),因此说高端内存有可能映射到”内核动态映射空间”中。

    • 持久内核映射(permanent kernel mapping)
      如果是通过 alloc_page() 获得了高端内存对应的 page,如何给它找个线性空间?内核专门为此留出一块线性空间,从 PKMAP_BASE 到 FIXADDR_START,用于映射高端内存。在 2.6内核上,这个地址范围是 4G-8M 到 4G-4M 之间。这个空间起叫”内核永久映射空间”或者”永久内核映射空间”。这个空间和其它空间使用同样的页目录表,对于内核来说,就是 swapper_pg_dir,对普通进程来说,通过 CR3 寄存器指向。通常情况下,这个空间是 4M 大小,因此仅仅需要一个页表即可,内核通过来 pkmap_page_table 寻找这个页表。通过 kmap(),可以把一个 page 映射到这个空间来。由于这个空间是 4M 大小,最多能同时映射 1024 个 page。因此,对于不使用的的 page,及应该时从这个空间释放掉(也就是解除映射关系),通过 kunmap() ,可以把一个 page 对应的线性地址从这个空间释放出来。

    • 临时映射(temporary kernel mapping)
      内核在 FIXADDR_START 到 FIXADDR_TOP 之间保留了一些线性空间用于特殊需求。这个空间称为”固定映射空间”在这个空间中,有一部分用于高端内存的临时映射。这块空间具有如下特点:
      (1)每个 CPU 占用一块空间
      (2)在每个 CPU 占用的那块空间中,又分为多个小空间,每个小空间大小是 1 个 page,每个小空间用于一个目的,这些目的定义在 kmap_types.h 中的 km_type 中。
      当要进行一次临时映射的时候,需要指定映射的目的,根据映射目的,可以找到对应的小空间,然后把这个空间的地址作为映射地址。这意味着一次临时映射会导致以前的映射被覆盖。通过 kmap_atomic() 可实现临时映射。

    原文:https://blog.csdn.net/tommy_wxie/article/details/17122923/

    用户空间与内核空间交互

    一、用户空间与内核空间是如何交互的?
    所有的系统资源管理都是在内核空间中完成的。比如读写磁盘文件,分配回收内存,从网络接口读写数据等等。应用程序是无法直接进行这样的操作的。但是可以通过内核提供的接口来完成这样的任务。
    比如应用程序要读取磁盘上的一个文件,它可以向内核发起一个 “系统调用” 告诉内核:“我要读取磁盘上的某某文件”。其实就是通过一个特殊的指令让进程从用户态进入到内核态(到了内核空间),在内核空间中,CPU 可以执行任何的指令,当然也包括从磁盘上读取数据。具体过程是先把数据读取到内核空间中,然后再把数据拷贝到用户空间并从内核态切换到用户态。此时应用程序已经从系统调用中返回并且拿到了想要的数据。
    在这里插入图片描述

    二、从内核空间和用户空间的角度看一看整个 Linux 系统的结构
    它大体可以分为三个部分,从下往上依次为:硬件 -> 内核空间 -> 用户空间。如下图所示:用户空间在硬件之上,内核空间中的代码控制了硬件资源的使用权,用户空间中的代码只有通过内核暴露的系统调用接口(System Call Interface)才能使用到系统中的硬件资源。
    在这里插入图片描述
    原文:https://www.cnblogs.com/sparkdev/p/8410350.html

    Binder一次拷贝原理

    一、常规进程间通信(两次Copy)
    Linux中两个进程之间是相互隔离,无法之间通信,需要通过内核空间进行传输数据。通常的做法是消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copy_from_user() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。同样的,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copy_to_user() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信,其中经过了两次数据Copy。
    在这里插入图片描述
    而Binder方式的IPC,首先也是需要把发送方的数据从用户空间拷贝到内核空间,但是后面,Binder驱动程序在内核空间的虚拟内存地址和接收方的虚拟内存地之间做了一个映射,这样接收方就可以直接去读取这个地址上的数据了,减少了一次拷贝。
    在这里插入图片描述
    原文:https://ke.qq.com/user/index/index.html#/plan/cid=2203778&term_id=102306334

    二、 Binder一次拷贝原理-mmap
    1、Binder 是基于 C/S 架构的。由一系列的组件组成,包括 Client、Server、ServiceManager、Binder 驱动。其中 Client、Server、Service Manager 运行在用户空间,Binder 驱动运行在内核空间。其中 Service Manager 和 Binder 驱动由系统提供,而 Client、Server 由应用程序来实现。Client、Server 和 ServiceManager 均是通过系统调用函数 open、mmap 和 ioctl 来访问设备文件 /dev/binder,从而实现与 Binder 驱动的交互来间接的实现跨进程通信。其中mmap在其中起到了数据一次copy传输的作用(这里只做原理学习,后面再进行源码分析)。

    2、mmap函数属于系统调用,mmap会从当前进程中获取用户态可用的虚拟地址空间(vm_area_struct *vma),并在mmap_region中真正获取vma,然后调用file->f_op->mmap(file, vma),进入驱动处理,之后就会在内存中分配一块连续的虚拟地址空间,并预先分配好页表、已使用的与未使用的标识、初始地址、与用户空间的偏移等等,通过这一步之后,就能把Binder在内核空间的数据直接通过指针地址映射到用户空间,供进程在用户空间使用,这是一次拷贝的基础。
    3、当数据从进程A的用户空间拷贝到内核空间的时候,是直从当前进程A的用户空间接拷贝到目标进程B的内核空间,这个过程是在请求端线程中处理的,操作对象是目标进程B的内核空间。看源码如下:

    static void binder_transaction(struct binder_proc *proc,
    			       struct binder_thread *thread,
    			       struct binder_transaction_data *tr, int reply){
    			       ...
    		// 在通过进行binder事物的传递时,如果一个binder事物(用struct binder_transaction结构体表示)需要使用到内存,
    		// 就会调用binder_alloc_buf函数分配此次binder事物需要的内存空间。
    		// 需要注意的是:这里是从目标进程的binder内存空间分配所需的内存
    		//从target进程的binder内存空间分配所需的内存大小,这也是一次拷贝,完成通信的关键,直接拷贝到目标进程的内核空间
    		//由于用户空间跟内核空间仅仅存在一个偏移地址,所以也算拷贝到用户空间
    		t->buffer = binder_alloc_buf(target_proc, tr->data_size,
    			tr->offsets_size, !reply && (t->flags & TF_ONE_WAY));
    		t->buffer->allow_user_free = 0;
    		t->buffer->debug_id = t->debug_id;
    		//该binder_buffer对应的事务    
    		t->buffer->transaction = t;
    		//该事物对应的目标binder实体 ,因为目标进程中可能不仅仅有一个Binder实体
    		t->buffer->target_node = target_node;
    		trace_binder_transaction_alloc_buf(t->buffer);
    		if (target_node)
    			binder_inc_node(target_node, 1, 0, NULL);
    		// 计算出存放flat_binder_object结构体偏移数组的起始地址,4字节对齐。
    		offp = (size_t *)(t->buffer->data + ALIGN(tr->data_size, sizeof(void *)));
    		   // struct flat_binder_object是binder在进程之间传输的表示方式 //
    	       // 这里就是完成binder通讯单边时候在用户进程同内核buffer之间的一次拷贝动作 //
    		  // 这里的数据拷贝,其实是拷贝到目标进程中去,因为t本身就是在目标进程的内核空间中分配的,
    		if (copy_from_user(t->buffer->data, tr->data.ptr.buffer, tr->data_size)) {
    			binder_user_error("binder: %d:%d got transaction with invalid "
    				"data ptr\n", proc->pid, thread->pid);
    			return_error = BR_FAILED_REPLY;
    			goto err_copy_data_failed;
    		}
    
    

    可以看到binder_alloc_buf(target_proc, tr->data_size,tr->offsets_size, !reply && (t->flags & TF_ONE_WAY))函数在申请内存的时候,是从target_proc进程空间中去申请的,这样在做数据拷贝的时候copy_from_user(t->buffer->data, tr->data.ptr.buffer, tr->data_size)),就会直接拷贝target_proc的内核空间,而由于Binder内核空间的数据能直接映射到用户空间,这里就不在需要拷贝到用户空间。这就是一次拷贝的原理。内核空间的数据映射到用户空间其实就是添加一个偏移地址,并且将数据的首地址、数据的大小都复制到一个用户空间的Parcel结构体,具体可以参考Parcel.cpp的Parcel::ipcSetDataReference函数。
    在这里插入图片描述
    原文:https://blog.csdn.net/happylishang/article/details/62234127

    展开全文
  • Binder (一) mmap与一次拷贝原理

    千次阅读 2019-11-29 15:10:41
    Binder机制 跨进程通信IPC 远程过程调用手段RPC 4个角色进行粘合,Client、Server、Service Manager和Binder驱动程序 Binder Driver misc设备 /dev/binder static struct file_operations binder_fops = { ....

    Binder机制

    • 跨进程通信IPC
    • 远程过程调用手段RPC
    • 4个角色进行粘合,Client、Server、Service Manager和Binder驱动程序
    • 整个过程只需要一次拷贝

    Binder Driver misc设备

    /dev/binder

    //没有read,write, ioctl实现
    static struct file_operations binder_fops = {
    	.owner = THIS_MODULE,
    	.poll = binder_poll,
    	.unlocked_ioctl = binder_ioctl,
    	.mmap = binder_mmap,
    	.open = binder_open,
    	.flush = binder_flush,
    	.release = binder_release,
    };
     
    static struct miscdevice binder_miscdev = {
    	.minor = MISC_DYNAMIC_MINOR,
    	.name = "binder",
    	.fops = &binder_fops
    };
     
    static int __init binder_init(void)
    {
    	int ret;
     
    	binder_proc_dir_entry_root = proc_mkdir("binder", NULL);
    	if (binder_proc_dir_entry_root)
    		binder_proc_dir_entry_proc = proc_mkdir("proc", binder_proc_dir_entry_root);
    	ret = misc_register(&binder_miscdev);
    	if (binder_proc_dir_entry_root) {
    		create_proc_read_entry("state", S_IRUGO, binder_proc_dir_entry_root, binder_read_proc_state, NULL);
    		create_proc_read_entry("stats", S_IRUGO, binder_proc_dir_entry_root, binder_read_proc_stats, NULL);
    		create_proc_read_entry("transactions", S_IRUGO, binder_proc_dir_entry_root, binder_read_proc_transactions, NULL);
    		create_proc_read_entry("transaction_log", S_IRUGO, binder_proc_dir_entry_root, binder_read_proc_transaction_log, &binder_transaction_log);
    		create_proc_read_entry("failed_transaction_log", S_IRUGO, binder_proc_dir_entry_root, binder_read_proc_transaction_log, &binder_transaction_log_failed);
    	}
    	return ret;
    }
     
    device_initcall(binder_init);
    
    
    binder_open

    创建一个struct binder_proc数据结构来保存打开设备文件/dev/binder的进程的上下文信息
    并且将这个进程上下文信息保存在打开文件结构struct file的私有数据成员变量private_data

    static int binder_open(struct inode *nodp, struct file *filp)
    {
    	struct binder_proc *proc;            //proc
     
    	if (binder_debug_mask & BINDER_DEBUG_OPEN_CLOSE)
    		printk(KERN_INFO "binder_open: %d:%d\n", current->group_leader->pid, current->pid);
     
    	proc = kzalloc(sizeof(*proc), GFP_KERNEL);
    	if (proc == NULL)
    		return -ENOMEM;
    	get_task_struct(current);
    	INIT_LIST_HEAD(&proc->todo);
    	init_waitqueue_head(&proc->wait);
    	proc->default_priority = task_nice(current);
    	mutex_lock(&binder_lock);
    	binder_stats.obj_created[BINDER_STAT_PROC]++;
    	hlist_add_head(&proc->proc_node, &binder_procs);
    	proc->pid = current->group_leader->pid;
    	INIT_LIST_HEAD(&proc->delivered_death);
    
    	filp->private_data = proc;                    //private_data
    	mutex_unlock(&binder_lock);
    ...
    }
    
    binder_mmap

    对打开的设备文件进行内存映射操作mmap
    重点:同一物理地址,当内核地址为kernel_addr,则进程地址为proc_addr = kernel_addr + user_buffer_offset

    bs->mapped = mmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, bs->fd, 0);
    
    static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
    {
            int ret;
            struct vm_struct *area;
            //private_data得到在打开设备文件/dev/binder时创建的struct binder_proc结构
            struct binder_proc *proc = filp->private_data;
            const char *failure_string;
            struct binder_buffer *buffer;
     //struct vm_area_struct用户空间内存映射信息,连续的虚拟地址空间
     //mapsize 最大4M
            if ((vma->vm_end - vma->vm_start) > SZ_4M)
                    vma->vm_end = vma->vm_start + SZ_4M;
    
            binder_debug(BINDER_DEBUG_OPEN_CLOSE,
                         "binder_mmap: %d %lx-%lx (%ld K) vma %lx pagep %lx\n",
                         proc->pid, vma->vm_start, vma->vm_end,
                         (vma->vm_end - vma->vm_start) / SZ_1K, vma->vm_flags,
                         (unsigned long)pgprot_val(vma->vm_page_prot));
    
            if (vma->vm_flags & FORBIDDEN_MMAP_FLAGS) {
                    ret = -EPERM;
                    failure_string = "bad vm_flags";
                    goto err_bad_arg;
            }
            vma->vm_flags = (vma->vm_flags | VM_DONTCOPY) & ~VM_MAYWRITE;
    
            mutex_lock(&binder_mmap_lock);
            if (proc->buffer) {
                    ret = -EBUSY;
                    failure_string = "already mapped";
                    goto err_already_mapped;
            }
      //一次拷贝的原理
      //分配一个连续的内核虚拟空间,与进程虚拟空间大小一致	
      //struct vm_struct内核空间内存映射信息,(3G+896M + 8M) ~ 4G 之间 (120M)
      //同一个物理页面,一方映射到进程虚拟地址空间,一方面映射到内核虚拟地址空间
      //只需要把Client进程空间的数据拷贝一次到内核空间,然后Server与内核共享这个数据
      //get_vm_area获得size字节大小的内核虚拟空间 ,物理页面是需要时才去申请和映射
            area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);
            if (area == NULL) {
                    ret = -ENOMEM;
                    failure_string = "get_vm_area";
                    goto err_get_vm_area_failed;
            }
            //proc->buffer指向内核虚拟空间的起始地址,buffer_size大小
            proc->buffer = area->addr;
    		 //地址偏移量 = 用户虚拟地址空间 - 内核虚拟地址空间
            proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;
            mutex_unlock(&binder_mmap_lock);
    
    #ifdef CONFIG_CPU_CACHE_VIPT
            if (cache_is_vipt_aliasing()) {
                    while (CACHE_COLOUR((vma->vm_start ^ (uint32_t)proc->buffer))) {
                            printk(KERN_INFO "binder_mmap: %d %lx-%lx maps %p bad alignment\n", proc->pid, vma->vm_start, vma->vm_end, proc->buffer);
                            vma->vm_start += PAGE_SIZE;
                    }
            }
    #endif
    		//分配物理页的指针数组,数组大小为vma的等效page个数;
            proc->pages = kzalloc(sizeof(proc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE), GFP_KERNEL);
            if (proc->pages == NULL) {
                    ret = -ENOMEM;
                    failure_string = "alloc page array";
                    goto err_alloc_pages_failed;
            }
            proc->buffer_size = vma->vm_end - vma->vm_start;
    
            vma->vm_ops = &binder_vm_ops;
            vma->vm_private_data = proc;
     //先分配1个物理页PAGE_SIZE,并将其分别映射到内核线性地址和用户态虚拟地址上
            if (binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma)) {
                    ret = -ENOMEM;
                    failure_string = "alloc small buf";
                    goto err_alloc_small_buf_failed;
            }
            //binder_buffer对象 指向proc的buffer起始地址
            buffer = proc->buffer;
             //初始化进程binder_buffer链表头, binder_buffer,在将来transaction时会用到
            INIT_LIST_HEAD(&proc->buffers);
            //buffers添加第一个节点buffer
            list_add(&buffer->entry, &proc->buffers);
            buffer->free = 1;
            //将空闲buffer放入proc->free_buffers中
            binder_insert_free_buffer(proc, buffer);
            proc->free_async_space = proc->buffer_size / 2;
            barrier();
            proc->files = get_files_struct(proc->tsk);
            proc->vma = vma;
            proc->vma_vm_mm = vma->vm_mm;
    
            /*printk(KERN_INFO "binder_mmap: %d %lx-%lx maps %p\n",
                     proc->pid, vma->vm_start, vma->vm_end, proc->buffer);*/
            return 0;
    
    err_alloc_small_buf_failed:
            kfree(proc->pages);
            proc->pages = NULL;
    err_alloc_pages_failed:
            mutex_lock(&binder_mmap_lock);
            vfree(proc->buffer);
            proc->buffer = NULL;
    err_get_vm_area_failed:
    err_already_mapped:
            mutex_unlock(&binder_mmap_lock);
    err_bad_arg:
            printk(KERN_ERR "binder_mmap: %d %lx-%lx %s failed %d\n",
                   proc->pid, vma->vm_start, vma->vm_end, failure_string, ret);
            return ret;
    
    
    static int binder_update_page_range(struct binder_proc *proc, int allocate,
    	void *start, void *end, struct vm_area_struct *vma)
    {
    	void *page_addr;
    	unsigned long user_page_addr;
    	struct vm_struct tmp_area;
    	struct page **page;
    	struct mm_struct *mm;
     
    	if (binder_debug_mask & BINDER_DEBUG_BUFFER_ALLOC)
    		printk(KERN_INFO "binder: %d: %s pages %p-%p\n",
    		       proc->pid, allocate ? "allocate" : "free", start, end);
     
    	if (end <= start)
    		return 0;
     
    	if (vma)
    		mm = NULL;
    	else
    		mm = get_task_mm(proc->tsk);
     
    	if (mm) {
    		down_write(&mm->mmap_sem);
    		vma = proc->vma;
    	}
     //allocate 申请/释放
    	if (allocate == 0)
    		goto free_range;
     
    	if (vma == NULL) {
    		printk(KERN_ERR "binder: %d: binder_alloc_buf failed to "
    		       "map pages in userspace, no vma\n", proc->pid);
    		goto err_no_vma;
    	}
      //开始循环分配物理页
    	for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
    		int ret;
    		struct page **page_array_ptr;
    		page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];
     
    		BUG_ON(*page);
    		//分配1个page
    		*page = alloc_page(GFP_KERNEL | __GFP_ZERO);
    		if (*page == NULL) {
    			printk(KERN_ERR "binder: %d: binder_alloc_buf failed "
    			       "for page at %p\n", proc->pid, page_addr);
    			goto err_alloc_page_failed;
    		}
    		tmp_area.addr = page_addr;
    		tmp_area.size = PAGE_SIZE + PAGE_SIZE /* guard page? */;
    		page_array_ptr = page;
    		//内存映射到内核空间
    		ret = map_vm_area(&tmp_area, PAGE_KERNEL, &page_array_ptr);
    		if (ret) {
    			printk(KERN_ERR "binder: %d: binder_alloc_buf failed "
    			       "to map page at %p in kernel\n",
    			       proc->pid, page_addr);
    			goto err_map_kernel_failed;
    		}
    		// 内存映射到用户空间
    		user_page_addr =
    			(uintptr_t)page_addr + proc->user_buffer_offset;
    		ret = vm_insert_page(vma, user_page_addr, page[0]);
    		if (ret) {
    			printk(KERN_ERR "binder: %d: binder_alloc_buf failed "
    			       "to map page at %lx in userspace\n",
    			       proc->pid, user_page_addr);
    			goto err_vm_insert_page_failed;
    		}
    		/* vm_insert_page does not seem to increment the refcount */
    	}
    	if (mm) {
    		up_write(&mm->mmap_sem);
    		mmput(mm);
    	}
    	return 0;
     
    free_range:
    	for (page_addr = end - PAGE_SIZE; page_addr >= start;
    	     page_addr -= PAGE_SIZE) {
    		page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];
    		if (vma)
    			zap_page_range(vma, (uintptr_t)page_addr +
    				proc->user_buffer_offset, PAGE_SIZE, NULL);
    err_vm_insert_page_failed:
    		unmap_kernel_range((unsigned long)page_addr, PAGE_SIZE);
    err_map_kernel_failed:
    		__free_page(*page);
    		*page = NULL;
    err_alloc_page_failed:
    		;
    	}
    err_no_vma:
    	if (mm) {
    		up_write(&mm->mmap_sem);
    		mmput(mm);
    	}
    	return -ENOMEM;
    }
    //分配2的n此方个页面
    //struct page用于表示一个内存物理页
    struct page * alloc_pages(unsigned int gfp_mask, unsigned int order);
    //vm_area_struct表示用户空间的一段虚拟内存区域, 0~3G的空间中一段连续的虚拟地址空间
    //即使mmap系统调用所返回的地址,就是vm_start
    struct vm_area_struct
    {
         struct mm_struct *vm_mm;       //进程的mm_struct结构体
    
         unsigned  long vm_start;       //虚拟内存区域起始地址
         unsigned  long vm_end;         //虚拟内存区域结束地址
    
         //....
    } ;
    //将一个page指针指向的物理页映射到用户虚拟地址空间
    //vma:用户虚拟地址空间
    //addr:用户空间的虚拟地址
    int vm_insert_page(struct vm_area_struct *vma, unsigned long addr, struct page *page);
    
    //内核空间的一段连续的虚拟内存区域, 除去那896M用于连续物理内存(主要DMA作用),其他的部分1G - 896 -8(安全)=120M 可以用于不连续物理页面,next的链表
    struct vm_struct {  
        struct vm_struct    *next;       //指向下一个vm区域,所有的vm组成一个链表
        void                *addr;       //虚拟内存区域的起始地址
        unsigned long        size;       //该块内存区的大小
        struct page        **pages;	//vm所映射的page
        phys_addr_t        phys_addr;//对应起始的物理地址和addr相对应
        //... 
    };
    //向内核空间申请一段虚拟的内存区域
    struct vm_struct *get_vm_area(unsigned long size, unsigned long flags);
    //将内核地址空间的和内存物理页面映射
    int map_vm_area(struct vm_struct *area, pgprot_t prot, struct page ***pages);
    
    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 allow_user_free:1;
            unsigned async_transaction:1;
            unsigned debug_id:29;
    
            struct binder_transaction *transaction;
    
            struct binder_node *target_node;
            size_t data_size;
            size_t offsets_size;
            uint8_t data[0];             //0数组
    };
    
    

    在这里插入图片描述

    shell
     ps  | grep system_server 
     cat /proc/xxxx/maps | grep "/dev/binder"  
                   
    b3d42000-b3e40000 r--p 00000000 00:0c 2210       /dev/binder
    

    应用 open ,mmap

    frameworks/base/cmds/servicemanager目录下,主要是由binder.h、binder.c和service_manager.c
    int main(int argc, char **argv)
    {
        struct binder_state *bs;
        void *svcmgr = BINDER_SERVICE_MANAGER;
     
        bs = binder_open(128*1024);
     
        if (binder_become_context_manager(bs)) {
            LOGE("cannot become context manager (%s)\n", strerror(errno));
            return -1;
        }
     
        svcmgr_handle = svcmgr;
        binder_loop(bs, svcmgr_handler);
        return 0;
    }
    
    struct binder_state *binder_open(unsigned mapsize)
    {
        struct binder_state *bs;
     
        bs = malloc(sizeof(*bs));
        if (!bs) {
            errno = ENOMEM;
            return 0;
        }
     
        bs->fd = open("/dev/binder", O_RDWR);
        if (bs->fd < 0) {
            fprintf(stderr,"binder: cannot open device (%s)\n",
                    strerror(errno));
            goto fail_open;
        }
     
        bs->mapsize = mapsize;
        bs->mapped = mmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, bs->fd, 0);
        if (bs->mapped == MAP_FAILED) {
            fprintf(stderr,"binder: cannot map device (%s)\n",
                    strerror(errno));
            goto fail_map;
        }
     
            /* TODO: check version */
     
        return bs;
     
    fail_map:
        close(bs->fd);
    fail_open:
        free(bs);
        return 0;
    }
    
    展开全文
  • 面试官: 谈一谈Binder原理和实现一次拷贝的流程 心理分析:能问出该问题,面试官对binder的理解是非常深入的。想问求职者对Android底层有没有深入理解 求职者:应该从linux进程通信原理的两次拷贝说起,然后引申...

    面试官: 谈一谈Binder的原理和实现一次拷贝的流程

    心理分析:能问出该问题,面试官对binder的理解是非常深入的。想问求职者对Android底层有没有深入理解

    求职者:应该从linux进程通信原理的两次拷贝说起,然后引申为什么binder却只有一次拷贝 ,最后阐述内核空间 与用户空间的定义

    1 Linux 下传统的进程间通信原理

    了解 Linux IPC 相关的概念和原理有助于我们理解 Binder 通信原理。因此,在介绍 Binder 跨进程通信原理之前,我们先聊聊 Linux 系统下传统的进程间通信是如何实现。

    1.1 基本概念介绍

    这里我们先从 Linux 中进程间通信涉及的一些基本概念开始介绍,然后逐步展开,向大家说明传统的进程间通信的原理。

     

    上图展示了 Liunx 中跨进程通信涉及到的一些基本概念:

    进程隔离
    进程空间划分:用户空间(User Space)/内核空间(Kernel Space)
    系统调用:用户态/内核态
    进程隔离

    简单的说就是操作系统中,进程与进程间内存是不共享的。两个进程就像两个平行的世界,A 进程没法直接访问 B 进程的数据,这就是进程隔离的通俗解释。A 进程和 B 进程之间要进行数据交互就得采用特殊的通信机制:进程间通信(IPC)。

    进程空间划分:用户空间(User Space)/内核空间(Kernel Space)

    现在操作系统都是采用的虚拟存储器,对于 32 位系统而言,它的寻址空间(虚拟存储空间)就是 2 的 32 次方,也就是 4GB。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也可以访问底层硬件设备的权限。为了保护用户进程不能直接操作内核,保证内核的安全,操作系统从逻辑上将虚拟空间划分为用户空间(User Space)和内核空间(Kernel Space)。针对 Linux 操作系统而言,将最高的 1GB 字节供内核使用,称为内核空间;较低的 3GB 字节供各进程使用,称为用户空间。

    简单的说就是,内核空间(Kernel)是系统内核运行的空间,用户空间(User Space)是用户程序运行的空间。为了保证安全性,它们之间是隔离的。

     

    系统调用:用户态与内核态

    虽然从逻辑上进行了用户空间和内核空间的划分,但不可避免的用户空间需要访问内核资源,比如文件操作、访问网络等等。为了突破隔离限制,就需要借助系统调用来实现。系统调用是用户空间访问内核空间的唯一方式,保证了所有的资源访问都是在内核的控制下进行的,避免了用户程序对系统资源的越权访问,提升了系统安全性和稳定性。

    Linux 使用两级保护机制:0 级供系统内核使用,3 级供用户程序使用。

    当一个任务(进程)执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。

    当进程在执行用户自己的代码的时候,我们称其处于用户运行态(用户态)。此时处理器在特权级最低的(3级)用户代码中运行。

    系统调用主要通过如下两个函数来实现:

    copy_from_user() //将数据从用户空间拷贝到内核空间
    copy_to_user() //将数据从内核空间拷贝到用户空间
    2.2 Linux 下的传统 IPC 通信原理

    理解了上面的几个概念,我们再来看看传统的 IPC 方式中,进程之间是如何实现通信的。

    通常的做法是消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copyfromuser() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。同样的,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copytouser() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信。如下图:

     

    这种传统的 IPC 通信方式有两个问题:

    性能低下,一次数据传递需要经历:内存缓存区 --> 内核缓存区 --> 内存缓存区,需要 2 次数据拷贝;
    接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用 API 接收消息头来获取消息体的大小,这两种做法不是浪费空间就是浪费时间。
    2. Binder 跨进程通信原理

    理解了 Linux IPC 相关概念和通信原理,接下来我们正式介绍下 Binder IPC 的原理。

    2.1 动态内核可加载模块 && 内存映射

    正如前面所说,跨进程通信是需要内核空间做支持的。传统的 IPC 机制如管道、Socket 都是内核的一部分,因此通过内核支持来实现进程间通信自然是没问题的。但是 Binder 并不是 Linux 系统内核的一部分,那怎么办呢?这就得益于 Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。

    在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder 驱动(Binder Dirver)。

    那么在 Android 系统中用户进程之间是如何通过这个内核模块(Binder 驱动)来实现通信的呢?难道是和前面说的传统 IPC 机制一样,先将数据从发送方进程拷贝到内核缓存区,然后再将数据从内核缓存区拷贝到接收方进程,通过两次拷贝来实现吗?显然不是,否则也不会有开篇所说的 Binder 在性能方面的优势了。

    这就不得不通道 Linux 下的另一个概念:内存映射。

    Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。

    内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。也正因为如此,内存映射能够提供对进程间通信的支持。

    2.2 Binder IPC 实现原理

    Binder IPC 正是基于内存映射(mmap)来实现的,但是 mmap() 通常是用在有物理介质的文件系统上的。

    比如进程中的用户区域是不能直接和物理设备打交道的,如果想要把磁盘上的数据读取到进程的用户区域,需要两次拷贝(磁盘-->内核空间-->用户空间);通常在这种场景下 mmap() 就能发挥作用,通过在物理介质和用户空间之间建立映射,减少数据的拷贝次数,用内存读写取代I/O读写,提高文件读取效率。

    而 Binder 并不存在物理介质,因此 Binder 驱动使用 mmap() 并不是为了在物理介质和用户空间之间建立映射,而是用来在内核空间创建数据接收的缓存空间。

    一次完整的 Binder IPC 通信过程通常是这样:

    首先 Binder 驱动在内核空间创建一个数据接收缓存区;
    接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
    发送方进程通过系统调用 copyfromuser() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。
    如下图:

     

    3. Binder 通信模型

    介绍完 Binder IPC 的底层通信原理,接下来我们看看实现层面是如何设计的。

    一次完整的进程间通信必然至少包含两个进程,通常我们称通信的双方分别为客户端进程(Client)和服务端进程(Server),由于进程隔离机制的存在,通信双方必然需要借助 Binder 来实现。

    3.1 Client/Server/ServiceManager/驱动

    前面我们介绍过,Binder 是基于 C/S 架构的。由一系列的组件组成,包括 Client、Server、ServiceManager、Binder 驱动。其中 Client、Server、Service Manager 运行在用户空间,Binder 驱动运行在内核空间。其中 Service Manager 和 Binder 驱动由系统提供,而 Client、Server 由应用程序来实现。Client、Server 和 ServiceManager 均是通过系统调用 open、mmap 和 ioctl 来访问设备文件 /dev/binder,从而实现与 Binder 驱动的交互来间接的实现跨进程通信。

     

    Client、Server、ServiceManager、Binder 驱动这几个组件在通信过程中扮演的角色就如同互联网中服务器(Server)、客户端(Client)、DNS域名服务器(ServiceManager)以及路由器(Binder 驱动)之前的关系。

    通常我们访问一个网页的步骤是这样的:首先在浏览器输入一个地址,如 http://www.google.com 然后按下回车键。但是并没有办法通过域名地址直接找到我们要访问的服务器,因此需要首先访问 DNS 域名服务器,域名服务器中保存了 http://www.google.com 对应的 ip 地址 10.249.23.13,然后通过这个 ip 地址才能放到到 http://www.google.com 对应的服务器。

     

    Android Binder 设计与实现一文中对 Client、Server、ServiceManager、Binder 驱动有很详细的描述,以下是部分摘录:

    Binder 驱动 Binder 驱动就如同路由器一样,是整个通信的核心;驱动负责进程之间 Binder 通信的建立,Binder 在进程之间的传递,Binder 引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。

    ServiceManager 与实名 Binder ServiceManager 和 DNS 类似,作用是将字符形式的 Binder 名字转化成 Client 中对该 Binder 的引用,使得 Client 能够通过 Binder 的名字获得对 Binder 实体的引用。注册了名字的 Binder 叫实名 Binder,就像网站一样除了除了有 IP 地址意外还有自己的网址。Server 创建了 Binder,并为它起一个字符形式,可读易记得名字,将这个 Binder 实体连同名字一起以数据包的形式通过 Binder 驱动发送给 ServiceManager ,通知 ServiceManager 注册一个名为“张三”的 Binder,它位于某个 Server 中。驱动为这个穿越进程边界的 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager。ServiceManger 收到数据后从中取出名字和引用填入查找表。

    细心的读者可能会发现,ServierManager 是一个进程,Server 是另一个进程,Server 向 ServiceManager 中注册 Binder 必然涉及到进程间通信。当前实现进程间通信又要用到进程间通信,这就好像蛋可以孵出鸡的前提却是要先找只鸡下蛋!Binder 的实现比较巧妙,就是预先创造一只鸡来下蛋。ServiceManager 和其他进程同样采用 Bidner 通信,ServiceManager 是 Server 端,有自己的 Binder 实体,其他进程都是 Client,需要通过这个 Binder 的引用来实现 Binder 的注册,查询和获取。ServiceManager 提供的 Binder 比较特殊,它没有名字也不需要注册。当一个进程使用 BINDERSETCONTEXT_MGR 命令将自己注册成 ServiceManager 时 Binder 驱动会自动为它创建 Binder 实体(这就是那只预先造好的那只鸡)。其次这个 Binder 实体的引用在所有 Client 中都固定为 0 而无需通过其它手段获得。也就是说,一个 Server 想要向 ServiceManager 注册自己的 Binder 就必须通过这个 0 号引用和 ServiceManager 的 Binder 通信。类比互联网,0 号引用就好比是域名服务器的地址,你必须预先动态或者手工配置好。要注意的是,这里说的 Client 是相对于 ServiceManager 而言的,一个进程或者应用程序可能是提供服务的 Server,但对于 ServiceManager 来说它仍然是个 Client。

    Client 获得实名 Binder 的引用 Server 向 ServiceManager 中注册了 Binder 以后, Client 就能通过名字获得 Binder 的引用了。Client 也利用保留的 0 号引用向 ServiceManager 请求访问某个 Binder: 我申请访问名字叫张三的 Binder 引用。ServiceManager 收到这个请求后从请求数据包中取出 Binder 名称,在查找表里找到对应的条目,取出对应的 Binder 引用作为回复发送给发起请求的 Client。从面向对象的角度看,Server 中的 Binder 实体现在有两个引用:一个位于 ServiceManager 中,一个位于发起请求的 Client 中。如果接下来有更多的 Client 请求该 Binder,系统中就会有更多的引用指向该 Binder ,就像 Java 中一个对象有多个引用一样。

    3.2 Binder 通信过程

    至此,我们大致能总结出 Binder 通信过程:

    首先,一个进程使用 BINDERSETCONTEXT_MGR 命令通过 Binder 驱动将自己注册成为 ServiceManager;
    Server 通过驱动向 ServiceManager 中注册 Binder(Server 中的 Binder 实体),表明可以对外提供服务。驱动为这个 Binder 创建位于内核中的实体节点以及 ServiceManager 对实体的引用,将名字以及新建的引用打包传给 ServiceManager,ServiceManger 将其填入查找表。
    Client 通过名字,在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,通过这个引用就能实现和 Server 进程的通信。
    我们看到整个通信过程都需要 Binder 驱动的接入。下图能更加直观的展现整个通信过程(为了进一步抽象通信过程以及呈现上的方便,下图我们忽略了 Binder 实体及其引用的概念):

     

    4 Binder 通信中的代理模式

    我们已经解释清楚 Client、Server 借助 Binder 驱动完成跨进程通信的实现机制了,但是还有个问题会让我们困惑。A 进程想要 B 进程中某个对象(object)是如何实现的呢?毕竟它们分属不同的进程,A 进程 没法直接使用 B 进程中的 object。

    前面我们介绍过跨进程通信的过程都有 Binder 驱动的参与,因此在数据流经 Binder 驱动的时候驱动会对数据做一层转换。当 A 进程想要获取 B 进程中的 object 时,驱动并不会真的把 object 返回给 A,而是返回了一个跟 object 看起来一模一样的代理对象 objectProxy,这个 objectProxy 具有和 object 一摸一样的方法,但是这些方法并没有 B 进程中 object 对象那些方法的能力,这些方法只需要把把请求参数交给驱动即可。对于 A 进程来说和直接调用 object 中的方法是一样的。

    当 Binder 驱动接收到 A 进程的消息后,发现这是个 objectProxy 就去查询自己维护的表单,一查发现这是 B 进程 object 的代理对象。于是就会去通知 B 进程调用 object 的方法,并要求 B 进程把返回结果发给自己。当驱动拿到 B 进程的返回结果后就会转发给 A 进程,一次通信就完成了。

     

    5 Binder 的完整定义

    现在我们可以对 Binder 做个更加全面的定义了:

    从进程间通信的角度看,Binder 是一种进程间通信的机制;
    从 Server 进程的角度看,Binder 指的是 Server 中的 Binder 实体对象;
    从 Client 进程的角度看,Binder 指的是对 Binder 代理对象,是 Binder 实体对象的一个远程代理
    从传输过程的角度看,Binder 是一个可以跨进程传输的对象;Binder 驱动会对这个跨越进程边界的对象对一点点特殊处理,自动完成代理对象和本地对象之间的转换。
    6. 手动编码实现跨进程调用

    通常我们在做开发时,实现进程间通信用的最多的就是 AIDL。当我们定义好 AIDL 文件,在编译时编译器会帮我们生成代码实现 IPC 通信。借助 AIDL 编译以后的代码能帮助我们进一步理解 Binder IPC 的通信原理。

    但是无论是从可读性还是可理解性上来看,编译器生成的代码对开发者并不友好。比如一个 BookManager.aidl 文件对应会生成一个 BookManager.java 文件,这个 java 文件包含了一个 BookManager 接口、一个 Stub 静态的抽象类和一个 Proxy 静态类。Proxy 是 Stub 的静态内部类,Stub 又是 BookManager 的静态内部类,这就造成了可读性和可理解性的问题。

    Android 之所以这样设计其实是有道理的,因为当有多个 AIDL 文件的时候把 BookManager、Stub、Proxy 放在同一个文件里能有效避免 Stub 和 Proxy 重名的问题。

    因此便于大家理解,下面我们来手动编写代码来实现跨进程调用。

    6.1 各 Java 类职责描述

    在正式编码实现跨进程调用之前,先介绍下实现过程中用到的一些类。了解了这些类的职责,有助于我们更好的理解和实现跨进程通信。

    IBinder : IBinder 是一个接口,代表了一种跨进程通信的能力。只要实现了这个借口,这个对象就能跨进程传输。
    IInterface : IInterface 代表的就是 Server 进程对象具备什么样的能力(能提供哪些方法,其实对应的就是 AIDL 文件中定义的接口)
    Binder : Java 层的 Binder 类,代表的其实就是 Binder 本地对象。BinderProxy 类是 Binder 类的一个内部类,它代表远程进程的 Binder 对象的本地代理;这两个类都继承自 IBinder, 因而都具有跨进程传输的能力;实际上,在跨越进程的时候,Binder 驱动会自动完成这两个对象的转换。
    Stub : AIDL 的时候,编译工具会给我们生成一个名为 Stub 的静态内部类;这个类继承了 Binder, 说明它是一个 Binder 本地对象,它实现了 IInterface 接口,表明它具有 Server 承诺给 Client 的能力;Stub 是一个抽象类,具体的 IInterface 的相关实现需要开发者自己实现。
    6.2 实现过程讲解

    一次跨进程通信必然会涉及到两个进程,在这个例子中 RemoteService 作为服务端进程,提供服务;ClientActivity 作为客户端进程,使用 RemoteService 提供的服务。如下图:

     

    那么服务端进程具备什么样的能力?能为客户端提供什么样的服务呢?还记得我们前面介绍过的 IInterface 吗,它代表的就是服务端进程具体什么样的能力。因此我们需要定义一个 BookManager 接口,BookManager 继承自 IIterface,表明服务端具备什么样的能力。

    /**
     * 这个类用来定义服务端 RemoteService 具备什么样的能力
     */
    public interface BookManager extends IInterface {
     void addBook(Book book) throws RemoteException;
    }
    只定义服务端具备什么样的能力是不够的,既然是跨进程调用,那么接下来我们得实现一个跨进程调用对象 Stub。Stub 继承 Binder, 说明它是一个 Binder 本地对象;实现 IInterface 接口,表明具有 Server 承诺给 Client 的能力;Stub 是一个抽象类,具体的 IInterface 的相关实现需要调用方自己实现。

    public abstract class Stub extends Binder implements BookManager {
     ...
     public static BookManager asInterface(IBinder binder) {
     if (binder == null)
     return null;
     IInterface iin = binder.queryLocalInterface(DESCRIPTOR);
     if (iin != null && iin instanceof BookManager)
     return (BookManager) iin;
     return new Proxy(binder);
     }
     ...
     @Override
     protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
     switch (code) {
     case INTERFACE_TRANSACTION:
     reply.writeString(DESCRIPTOR);
     return true;
     case TRANSAVTION_addBook:
     data.enforceInterface(DESCRIPTOR);
     Book arg0 = null;
     if (data.readInt() != 0) {
     arg0 = Book.CREATOR.createFromParcel(data);
     }
     this.addBook(arg0);
     reply.writeNoException();
     return true;
     }
     return super.onTransact(code, data, reply, flags);
     }
     ...
    }
    Stub 类中我们重点介绍下 asInterface 和 onTransact。

    先说说 asInterface,当 Client 端在创建和服务端的连接,调用 bindService 时需要创建一个 ServiceConnection 对象作为入参。在 ServiceConnection 的回调方法 onServiceConnected 中 会通过这个 asInterface(IBinder binder) 拿到 BookManager 对象,这个 IBinder 类型的入参 binder 是驱动传给我们的,正如你在代码中看到的一样,方法中会去调用 binder.queryLocalInterface() 去查找 Binder 本地对象,如果找到了就说明 Client 和 Server 在同一进程,那么这个 binder 本身就是 Binder 本地对象,可以直接使用。否则说明是 binder 是个远程对象,也就是 BinderProxy。因此需要我们创建一个代理对象 Proxy,通过这个代理对象来是实现远程访问。

    接下来我们就要实现这个代理类 Proxy 了,既然是代理类自然需要实现 BookManager 接口。

    public class Proxy implements BookManager {
     ...
     public Proxy(IBinder remote) {
     this.remote = remote;
     }
     @Override
     public void addBook(Book book) throws RemoteException {
     Parcel data = Parcel.obtain();
     Parcel replay = Parcel.obtain();
     try {
     data.writeInterfaceToken(DESCRIPTOR);
     if (book != null) {
     data.writeInt(1);
     book.writeToParcel(data, 0);
     } else {
     data.writeInt(0);
     }
     remote.transact(Stub.TRANSAVTION_addBook, data, replay, 0);
     replay.readException();
     } finally {
     replay.recycle();
     data.recycle();
     }
     }
     ...
    }
    我们看看 addBook() 的实现;在 Stub 类中,addBook(Book book) 是一个抽象方法,Server 端需要去实现它。

    如果 Client 和 Server 在同一个进程,那么直接就是调用这个方法。
    如果是远程调用,Client 想要调用 Server 的方法就需要通过 Binder 代理来完成,也就是上面的 Proxy。
    在 Proxy 中的 addBook() 方法中首先通过 Parcel 将数据序列化,然后调用 remote.transact()。正如前文所述 Proxy 是在 Stub 的 asInterface 中创建,能走到创建 Proxy 这一步就说明 Proxy 构造函数的入参是 BinderProxy,即这里的 remote 是个 BinderProxy 对象。最终通过一系列的函数调用,Client 进程通过系统调用陷入内核态,Client 进程中执行 addBook() 的线程挂起等待返回;驱动完成一系列的操作之后唤醒 Server 进程,调用 Server 进程本地对象的 onTransact()。最终又走到了 Stub 中的 onTransact() 中,onTransact() 根据函数编号调用相关函数(在 Stub 类中为 BookManager 接口中的每个函数中定义了一个编号,只不过上面的源码中我们简化掉了;在跨进程调用的时候,不会传递函数而是传递编号来指明要调用哪个函数);我们这个例子里面,调用了 Binder 本地对象的 addBook() 并将结果返回给驱动,驱动唤醒 Client 进程里刚刚挂起的线程并将结果返回。

    这样一次跨进程调用就完成了。

     

     

    展开全文
  • 三 APP 三 Server进程 前面主要从源码的角度看了下APP进程...通过个Native发送过来的BINDER_WRITE_READ指令来看下个简单的流程: (1)在ServiceManager进程中调用到framework/native/cmds/servicemanag...

    目录

    前言

    一 概述

    二 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接收消息头来获取消息体的大小,浪费时间或空间。

    展开全文
  • Android Binder通信一次拷贝你真的理解了吗?

    千次阅读 多人点赞 2021-01-13 20:24:47
    最近有读者在询问一个关于Binder通信"一次拷贝"的问题,说在学习Binder驱动的实现中看到有多次调用了copy_from_user和copy_to_user来进行数据的跨用户空间和内核空间的拷贝,但是为啥Android官方和绝大部分的博客...
  • 面试官: 谈一谈Binder原理和实现一次拷贝的流程 心理分析:能问出该问题,面试官对binder的理解是非常深入的。想问求职者对Android底层有没有深入理解 求职者:应该从linux进程通信原理的两次拷贝说起,然后引申...
  • 面试官: 谈一谈Binder原理和实现一次拷贝的流程 心理分析:能问出该问题,面试官对binder的理解是非常深入的。想问求职者对Android底层有没有深入理解 **求职者:**应该从linux进程通信原理的两次拷贝说起,...
  • public class Proxy implements BookManager { ... public Proxy(IBinder remote) { this.remote = remote; } @Override public void addBook(Book book) throws RemoteException { ...
  • 但是 Binder 并不是 Linux 系统内核的部分,那怎么办呢?这就得益于 Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在...
  • 在谈Android的跨进程通信问题上时,总会问到Android的IPC机制,是指两个进程之间进行数据交换的过程...在谈IPC机制时候核心的就是Binder的运作原理,本文将从以下几点来讲Android额Binder原理。 什么是Binder ...
  • Binder原理

    2018-04-21 19:26:46
    在阅读本篇文章之前,建议先阅读一下这篇文章。 、概述 本篇博文的目的是学习整理,以备忘。...共享内存虽然不需要数据拷贝,但是控制复杂。 Socket作为款通用接口,其传输效率低,开销大...
  • Android跨进程通信:图文详解 Binder机制 原理

    万次阅读 多人点赞 2017-06-22 10:31:24
    虽然 网上有很多介绍 Binder的文章,可是存在一些问题:浅显的讨论Binder机制 或 一味讲解 Binder源码、逻辑不清楚,最终导致的是读者们还是无法形成个完整的Binder概念 本文采用 清晰的图文讲解方式,按照 大角度...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 3,659
精华内容 1,463
关键字:

binder一次拷贝原理