<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
	<channel>
		<atom:link href="https://www.gentoo-zh.org/extern.php?action=feed&amp;tid=470&amp;type=rss" rel="self" type="application/rss+xml" />
		<title><![CDATA[Gentoo中文社区 / linux源码解读（二十六）：android binder原理和实现方式]]></title>
		<link>https://www.gentoo-zh.org/viewtopic.php?id=470</link>
		<description><![CDATA[linux源码解读（二十六）：android binder原理和实现方式 最近发表的帖子。]]></description>
		<lastBuildDate>Tue, 11 Oct 2022 07:03:23 +0000</lastBuildDate>
		<generator>FluxBB</generator>
		<item>
			<title><![CDATA[linux源码解读（二十六）：android binder原理和实现方式]]></title>
			<link>https://www.gentoo-zh.org/viewtopic.php?pid=477#p477</link>
			<description><![CDATA[<p>1、linux提供了好几种IPC的机制：共享内存、管道、消息队列、信号量等，所有IPC机制的核心或本质就是在内核开辟一块空间，通信双方都从这块空间读写数据，整个流程图示如下：<br /><span class="postimg"><img src="https://img2022.cnblogs.com/blog/2052730/202202/2052730-20220223173811918-1508177042.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160; &#160; 这种通信方式天生的缺陷看出来了么? A进程把数据拷贝到内核，B进程从内核再拷贝走，同一份数据可能在内存存放了3份，同时还复制了2次，感觉和app通过read、write等方法从磁盘读数据的效率一样低啊！既然app读写文件数据能通过mmap提升效率，IPC是不是也能通过mmap提升效率了？答案也是肯定的，这就是android中binder诞生的背景！相比传统的IPC，binder只需要拷贝1次，整个原理和流程如下图所示：</p><p><span class="postimg"><img src="https://img2022.cnblogs.com/blog/2052730/202202/2052730-20220223180854847-984964398.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160; &#160; &#160;A进程还是把数据从用户空间写到内核缓存区，也就是生产者的流程或方式没变！改动最大的就是消费端了：先是内核建立数据接受缓存区，但这个缓存区和内核缓存区建立了映射，换句话说用的是同一块物理地址！接着是接收端进程空间和内核的数据接受缓存区页也建立映射，换句话说用的也是同一块物理地址，这样一来内核实际只用了1块物理地址，但是这块物理地址映射了2个虚拟地址（感觉好鸡贼.....）！</p><p>&#160; 2、原理和本质其实也很简单：通信双方不就是共用一块物理内存么？既然双方都要依靠这块内存，那么对这块内存的管理就尤为重要了！围绕这块内存的各种操作，也就是binder的各种操作，都定义在了binder_fops 结构体中，如下：</p><div class="codebox"><pre><code>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,
};</code></pre></div><p>&#160; 从名字就能看出来，分别是poll、io控制、mmap、打开、刷新和释放，通过这些接口完全能够操作共用的物理内存了！老规矩，在分析具体的实现方法前，先看看有哪些重要的结构体，从这些结构体的属性字段就能管中窥豹，看出具体怎么落地实现，结构体主要字段和关联关系如下：<br /><span class="postimg"><img src="https://img2022.cnblogs.com/blog/2052730/202202/2052730-20220224143853804-134590380.png" alt="FluxBB bbcode 测试" /></span></p><p>&#160; &#160;（2）要想实现IPC，第一个就要在内核开辟内存空间，否则交换个毛的数据啊！看上面的结构体，交换数据的空间在binder_buffer中，和binder_proc中的buffers的字段是关联的，所以binder驱动先生成binder_proc实例（本质是用来管理交换数据的公共内存），初始化后加入链表队列，整个过程在binder_open方法内部实现的，如下：</p><div class="codebox"><pre class="vscroll"><code>/*1、新生成、初始化binder_proc实例，并加入binder_procs全局队列
  2、初始化todo队列和等待队列
*/
static int binder_open(struct inode *nodp, struct file *filp)
{
    struct binder_proc *proc;

    binder_debug(BINDER_DEBUG_OPEN_CLOSE, &quot;binder_open: %d:%d\n&quot;,
             current-&gt;group_leader-&gt;pid, current-&gt;pid);
    // 分配 binder_proc 数据结构内存
    proc = kzalloc(sizeof(*proc), GFP_KERNEL);
    if (proc == NULL)
        return -ENOMEM;
    //增加task结构体的引用计数
    get_task_struct(current);
    proc-&gt;tsk = current;
    proc-&gt;vma_vm_mm = current-&gt;mm;
    INIT_LIST_HEAD(&amp;proc-&gt;todo);//初始化待处理事件队列头
    init_waitqueue_head(&amp;proc-&gt;wait);//初始化等待队列头
    proc-&gt;default_priority = task_nice(current);
    //锁定临界区
    binder_lock(__func__);
    // 增加BINDER_STAT_PROC的对象计数
    binder_stats_created(BINDER_STAT_PROC);
    /*添加新生成的proc_node到 binder_procs全局队列中，
    这样任何进程就可以访问到其他进程的 binder_proc 对象了*/
    hlist_add_head(&amp;proc-&gt;proc_node, &amp;binder_procs);
    proc-&gt;pid = current-&gt;group_leader-&gt;pid;
    INIT_LIST_HEAD(&amp;proc-&gt;delivered_death);
    filp-&gt;private_data = proc;
    //释放临界区的锁
    binder_unlock(__func__);

    if (binder_debugfs_dir_entry_proc) {
        char strbuf[11];

        snprintf(strbuf, sizeof(strbuf), &quot;%u&quot;, proc-&gt;pid);
        proc-&gt;debugfs_entry = debugfs_create_file(strbuf, S_IRUGO,
            binder_debugfs_dir_entry_proc, proc, &amp;binder_proc_fops);
    }

    return 0;
}</code></pre></div><p>&#160; 既然binder_open是生成binder_proc实例，用完后也需要释放和回收，避免内存泄漏，binder驱动提供了binder_release方法，如下:</p><div class="codebox"><pre><code>static int binder_release(struct inode *nodp, struct file *filp)
{
    struct binder_proc *proc = filp-&gt;private_data;

    debugfs_remove(proc-&gt;debugfs_entry);
    binder_defer_work(proc, BINDER_DEFERRED_RELEASE);

    return 0;
}
static void
binder_defer_work(struct binder_proc *proc, enum binder_deferred_state defer)
{
    mutex_lock(&amp;binder_deferred_lock);
    proc-&gt;deferred_work |= defer;
    if (hlist_unhashed(&amp;proc-&gt;deferred_work_node)) {
        //binder_proc实例添加到释放队列
        hlist_add_head(&amp;proc-&gt;deferred_work_node,
                &amp;binder_deferred_list);
        schedule_work(&amp;binder_deferred_work);
    }
    mutex_unlock(&amp;binder_deferred_lock);
}</code></pre></div><p>&#160; (3) binder_proc本质上是用来管理共用内存的结构体，这个实例化后就需要开始最重要的一步了：在进程虚拟地址申请内存，然后映射到内核的物理地址，这个过程是在binder_map中实现的，代码如下：</p><div class="codebox"><pre class="vscroll"><code>/*把内核的物理内存映射到用户进程地址空间中，这样就可以像操作用户内存那样操作内核内存*/
static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
    int ret;
    struct vm_struct *area;
    //获取proc实例，这里有通信双方的共用内存
    struct binder_proc *proc = filp-&gt;private_data;
    const char *failure_string;
    //通信双方的共用内存
    struct binder_buffer *buffer;

    if (proc-&gt;tsk != current)
        return -EINVAL;
    //共用内存最多4m，不能再多了
    if ((vma-&gt;vm_end - vma-&gt;vm_start) &gt; SZ_4M)
        vma-&gt;vm_end = vma-&gt;vm_start + SZ_4M;

    binder_debug(BINDER_DEBUG_OPEN_CLOSE,
             &quot;binder_mmap: %d %lx-%lx (%ld K) vma %lx pagep %lx\n&quot;,
             proc-&gt;pid, vma-&gt;vm_start, vma-&gt;vm_end,
             (vma-&gt;vm_end - vma-&gt;vm_start) / SZ_1K, vma-&gt;vm_flags,
             (unsigned long)pgprot_val(vma-&gt;vm_page_prot));
    //看看flags是否合法
    if (vma-&gt;vm_flags &amp; FORBIDDEN_MMAP_FLAGS) {
        ret = -EPERM;
        failure_string = &quot;bad vm_flags&quot;;
        goto err_bad_arg;
    }
    vma-&gt;vm_flags = (vma-&gt;vm_flags | VM_DONTCOPY) &amp; ~VM_MAYWRITE;
    //锁定临界区，便于在进程之间互斥，避免不同的进程同时申请虚拟内存
    mutex_lock(&amp;binder_mmap_lock);
    if (proc-&gt;buffer) {//这块内存已经映射了
        ret = -EBUSY;
        failure_string = &quot;already mapped&quot;;
        goto err_already_mapped;
    }
    /*从/proc/self/maps查找未使用的虚拟内存，并申请内核虚拟内存空间
    注意：这里是进程的虚拟地址空间
    */
    area = get_vm_area(vma-&gt;vm_end - vma-&gt;vm_start, VM_IOREMAP);
    if (area == NULL) {
        ret = -ENOMEM;
        failure_string = &quot;get_vm_area&quot;;
        goto err_get_vm_area_failed;
    }
    // 将申请到的内存地址保存到 binder_proc 对象中
    proc-&gt;buffer = area-&gt;addr;
    proc-&gt;user_buffer_offset = vma-&gt;vm_start - (uintptr_t)proc-&gt;buffer;
    mutex_unlock(&amp;binder_mmap_lock);

#ifdef CONFIG_CPU_CACHE_VIPT
    if (cache_is_vipt_aliasing()) {
        while (CACHE_COLOUR((vma-&gt;vm_start ^ (uint32_t)proc-&gt;buffer))) {
            pr_info(&quot;binder_mmap: %d %lx-%lx maps %p bad alignment\n&quot;, proc-&gt;pid, vma-&gt;vm_start, vma-&gt;vm_end, proc-&gt;buffer);
            vma-&gt;vm_start += PAGE_SIZE;
        }
    }
#endif
    //根据请求到的内存空间大小，分配给binder_proc对象的pages， 用于保存指向物理页的指针
    proc-&gt;pages = kzalloc(sizeof(proc-&gt;pages[0]) * ((vma-&gt;vm_end - vma-&gt;vm_start) / PAGE_SIZE), GFP_KERNEL);
    if (proc-&gt;pages == NULL) {
        ret = -ENOMEM;
        failure_string = &quot;alloc page array&quot;;
        goto err_alloc_pages_failed;
    }
    //共用内存大小，不超过4M
    proc-&gt;buffer_size = vma-&gt;vm_end - vma-&gt;vm_start;

    vma-&gt;vm_ops = &amp;binder_vm_ops;
    vma-&gt;vm_private_data = proc;
    //分配一个物理页
    if (binder_update_page_range(proc, 1, proc-&gt;buffer, proc-&gt;buffer + PAGE_SIZE, vma)) {
        ret = -ENOMEM;
        failure_string = &quot;alloc small buf&quot;;
        goto err_alloc_small_buf_failed;
    }
    buffer = proc-&gt;buffer;
    INIT_LIST_HEAD(&amp;proc-&gt;buffers);
    //将binder_buffer对象放入到proc-&gt;buffers链表中，便于统一管理
    list_add(&amp;buffer-&gt;entry, &amp;proc-&gt;buffers);
    buffer-&gt;free = 1;
    /*新生成的buffer加入红黑树管理*/
    binder_insert_free_buffer(proc, buffer);
    proc-&gt;free_async_space = proc-&gt;buffer_size / 2;
    //内存屏障，防止乱序
    barrier();
    proc-&gt;files = get_files_struct(current);
    proc-&gt;vma = vma;
    proc-&gt;vma_vm_mm = vma-&gt;vm_mm;

    /*pr_info(&quot;binder_mmap: %d %lx-%lx maps %p\n&quot;,
         proc-&gt;pid, vma-&gt;vm_start, vma-&gt;vm_end, proc-&gt;buffer);*/
    return 0;

err_alloc_small_buf_failed:
    kfree(proc-&gt;pages);
    proc-&gt;pages = NULL;
err_alloc_pages_failed:
    mutex_lock(&amp;binder_mmap_lock);
    vfree(proc-&gt;buffer);
    proc-&gt;buffer = NULL;
err_get_vm_area_failed:
err_already_mapped:
    mutex_unlock(&amp;binder_mmap_lock);
err_bad_arg:
    pr_err(&quot;binder_mmap: %d %lx-%lx %s failed %d\n&quot;,
           proc-&gt;pid, vma-&gt;vm_start, vma-&gt;vm_end, failure_string, ret);
    return ret;
}</code></pre></div><p>&#160; 这里面最核心的函数就是binder_update_page_range了，这个函数要么释放物理页，要么分配物理页；如果分配物理页，还会建立和进程虚拟地址空间的映射关系！其中真正建立虚拟内存和物理页映射关系的当属map_kernel_range_noflush和vm_insert_page函数了：前者是内核虚拟内存和物理页建立映射，后者是进程虚拟内存和物理页建立映射。整个代码如下：</p><div class="codebox"><pre class="vscroll"><code>/*分配和释放物理页；如果是分配，同时建立和进程虚拟地址空间的映射*/
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 page **page;
    struct mm_struct *mm;

    binder_debug(BINDER_DEBUG_BUFFER_ALLOC,
             &quot;%d: %s pages %p-%p\n&quot;, proc-&gt;pid,
             allocate ? &quot;allocate&quot; : &quot;free&quot;, start, end);

    if (end &lt;= start)
        return 0;

    trace_binder_update_page_range(proc, allocate, start, end);

    if (vma)
        mm = NULL;
    else
        /* 读取进程的内存描述符（mm_struct）, 
         * 并增加内存描述符(mm_struct)中的mm_users用户计数，防止mm_struct被释放*/
        mm = get_task_mm(proc-&gt;tsk);

    if (mm) { 
        /*获取写锁*/
        down_write(&amp;mm-&gt;mmap_sem);
        vma = proc-&gt;vma;
        if (vma &amp;&amp; mm != proc-&gt;vma_vm_mm) {
            pr_err(&quot;%d: vma mm and task mm mismatch\n&quot;,
                proc-&gt;pid);
            vma = NULL;
        }
    }
    //如果传入的allocate是0，就是释放物理页
    if (allocate == 0)
        goto free_range;

    if (vma == NULL) {
        pr_err(&quot;%d: binder_alloc_buf failed to map pages in userspace, no vma\n&quot;,
            proc-&gt;pid);
        goto err_no_vma;
    }
    /* 开始循环分配物理页，并建立映射，每次循环分配1个页*/
    for (page_addr = start; page_addr &lt; end; page_addr += PAGE_SIZE) {
        int ret;
        /* 确定页所存放的数组的位置，按内核虚拟地址由小到大排列*/
        page = &amp;proc-&gt;pages[(page_addr - proc-&gt;buffer) / PAGE_SIZE];

        BUG_ON(*page);
        //最核心的地方；分配物理页
        *page = alloc_page(GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO);
        if (*page == NULL) {
            pr_err(&quot;%d: binder_alloc_buf failed for page at %p\n&quot;,
                proc-&gt;pid, page_addr);
            goto err_alloc_page_failed;
        }
        /*将内核虚拟地址与该物理页建立映射关系,最终调用的是
        mm\vmalloc.c的vmap_page_range_noflush，然后依次递进调用
        vmap_pud_range、vmap_pmd_range、vmap_pte_range、set_pte_at设置页表的4级映射关系；
        注意：这里映射的是内核虚拟地址空间，用户进程的虚拟地址空间映射在后面，
        调用的是vm_insert_page方法*/
        ret = map_kernel_range_noflush((unsigned long)page_addr,
                    PAGE_SIZE, PAGE_KERNEL, page);
        //页表更新后刷新cpu的TLB缓存
        flush_cache_vmap((unsigned long)page_addr,
                (unsigned long)page_addr + PAGE_SIZE);
        if (ret != 1) {
            pr_err(&quot;%d: binder_alloc_buf failed to map page at %p in kernel\n&quot;,
                   proc-&gt;pid, page_addr);
            goto err_map_kernel_failed;
        }
        /*计算用户态虚地址*/
        user_page_addr =
            (uintptr_t)page_addr + proc-&gt;user_buffer_offset;
        /*将用户虚拟地址与该物理页建立映射关系*/
        ret = vm_insert_page(vma, user_page_addr, page[0]);
        if (ret) {
            pr_err(&quot;%d: binder_alloc_buf failed to map page at %lx in userspace\n&quot;,
                   proc-&gt;pid, user_page_addr);
            goto err_vm_insert_page_failed;
        }
        /* vm_insert_page does not seem to increment the refcount */
    }
    if (mm) {
        //释放写锁
        up_write(&amp;mm-&gt;mmap_sem);
        /*减少内存描述符(mm_struct)中的mm_users用户计数*/
        mmput(mm);
    }
    return 0;

free_range:
    for (page_addr = end - PAGE_SIZE; page_addr &gt;= start;
         page_addr -= PAGE_SIZE) {
        page = &amp;proc-&gt;pages[(page_addr - proc-&gt;buffer) / PAGE_SIZE];
        if (vma)
            zap_page_range(vma, (uintptr_t)page_addr +
                proc-&gt;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(&amp;mm-&gt;mmap_sem);
        mmput(mm);
    }
    return -ENOMEM;
}</code></pre></div><p>&#160; (4)内存映射完毕后，万事俱备，只欠数据了！通信双方都可以从内核缓存区读写数据，调用链条比较长，是这样的：binder_ioctl() -&gt; binder_get_thread() -&gt; binder_ioctl_write_read() -&gt; binder_thread_write()/binder_thread_read()！最终执行的就是binder_thread_write和binder_thread_read方法了，这两个方法的核心思路也很简单：循环读取cmd，根据不同的cmd才取不同的动作；具体读写数据时，因为涉及到内核和用户进程之间的拷贝，最终调用的还是copy_from_user和copy_to_user。因为代码太长，我这里只截取少量核心代码展示：</p><div class="codebox"><pre class="vscroll"><code>/*循环读取cmd，根据不同的cmd才取不同的动作；具体读写数据时，因为涉及到内核和用户进程之间
的拷贝，最终调用的还是copy_from_user和copy_to_user*/
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)
{
    uint32_t cmd;
    void __user *buffer = (void __user *)(uintptr_t)binder_buffer;
    //数据起始地址
    void __user *ptr = buffer + *consumed;
    //数据结束地址
    void __user *end = buffer + size;
    //可能有多个命令或数据要处理，需要循环
    while (ptr &lt; end &amp;&amp; thread-&gt;return_error == BR_OK) {
        //读取一个cmd命令
        if (get_user(cmd, (uint32_t __user *)ptr))
            return -EFAULT;
        //起始地址条过cmd命令占用的空间
        ptr += sizeof(uint32_t);
                 .................
                switch (cmd) {
        case BC_INCREFS:
        case BC_ACQUIRE:
        case BC_RELEASE:
                case BC_REPLY: {
            struct binder_transaction_data tr;
            //从3环用户态写数据到内核态，离不开copy_from_user和copy_to_user，任何情况都不例外
            if (copy_from_user(&amp;tr, ptr, sizeof(tr)))
                return -EFAULT;
            ptr += sizeof(tr);
            binder_transaction(proc, thread, &amp;tr, cmd == BC_REPLY);
            break;
        }
}</code></pre></div><p>&#160; binder_thread_read核心思路: 检查当前线程是否被唤醒。如果是，核心功能就是执行copy_to_user让3环的进程把数据拷贝走！如果不是就继续retry循环，核心代码如下：有个while死循环，在循环中探查是否有数据；如果没数据就回到retry继续等待唤醒！</p><div class="codebox"><pre class="vscroll"><code>retry:
    // 获取将要处理的任务
    wait_for_proc_work = thread-&gt;transaction_stack == NULL &amp;&amp;
                list_empty(&amp;thread-&gt;todo);
    if (wait_for_proc_work) {
        if (!(thread-&gt;looper &amp; (BINDER_LOOPER_STATE_REGISTERED |
                    BINDER_LOOPER_STATE_ENTERED))) {
            binder_user_error(&quot;binder: %d:%d ERROR: Thread waiting &quot;
                &quot;for process work before calling BC_REGISTER_&quot;
                &quot;LOOPER or BC_ENTER_LOOPER (state %x)\n&quot;,
                proc-&gt;pid, thread-&gt;pid, thread-&gt;looper);
            wait_event_interruptible(binder_user_error_wait,
                         binder_stop_on_user_error &lt; 2);
        }
        binder_set_nice(proc-&gt;default_priority);
        if (non_block) {
            // 非阻塞且没有数据则返回 EAGAIN
            if (!binder_has_proc_work(proc, thread))
                ret = -EAGAIN;
        } else
            // 阻塞则进入睡眠状态，等待可操作的任务
            ret = wait_event_freezable_exclusive(proc-&gt;wait, binder_has_proc_work(proc, thread));
    } else {
        if (non_block) {
            if (!binder_has_thread_work(thread))
                ret = -EAGAIN;
        } else
            ret = wait_event_freezable(thread-&gt;wait, binder_has_thread_work(thread));
    }

    binder_lock(__func__);

    if (wait_for_proc_work)
        proc-&gt;ready_threads--;
    thread-&gt;looper &amp;= ~BINDER_LOOPER_STATE_WAITING;

    if (ret)
        return ret;

    while (1) {
        uint32_t cmd;
        struct binder_transaction_data tr;
        struct binder_work *w;
        struct binder_transaction *t = NULL;

        // 获取 binder_work 对象
        if (!list_empty(&amp;thread-&gt;todo))
            w = list_first_entry(&amp;thread-&gt;todo, struct binder_work, entry);
        else if (!list_empty(&amp;proc-&gt;todo) &amp;&amp; wait_for_proc_work)
            w = list_first_entry(&amp;proc-&gt;todo, struct binder_work, entry);
        else {
            if (ptr - buffer == 4 &amp;&amp; !(thread-&gt;looper &amp; BINDER_LOOPER_STATE_NEED_RETURN)) /* no data added没有数据就回到retry继续等 */
                goto retry;
            break;
        }     ..................    }</code></pre></div><p>&#160; &#160; （5）上述通过读写公共内存通信的方式原理很简单，实现的时候还有点需要注意：记得介绍linux IPC时的poll么？ binder面临同样的问题：读取数据的进程是怎么知道公共内存有数据的了？这个问题是在binder_transaction解决的：binder_thread_write函数执行完后会调用binder_transaction，最后两行代码就是唤醒目标线程了，刚好和binder_thread_read中的循环探查数据完美闭环！</p><br /><p>if (target_wait)<br />&#160; &#160; &#160; &#160; // 唤醒目标线程<br />&#160; &#160; &#160; &#160; wake_up_interruptible(target_wait);</p><p> </p><p> </p><p>总结:</p><p>1、整个过程本质就是更改页表，让不同的虚拟地址映射到同一个物理地址，和windows下用shadow&#160; walker过PG保护的原理一模一样！</p><p>2、3环进程和内核之间拷贝数据用的还是copy_from_user和copy_to_user，一万年都不变的！</p><p> </p><p>参考：</p><p>1、https://www.bilibili.com/video/BV1Kf4y1z7kT?p=3&amp;spm_id_from=pageDriver&#160; android为什么选binder</p><p>2、https://zhuanlan.zhihu.com/p/35519585&#160; binder原理剖析</p><p>3、https://github.com/xdtianyu/SourceAnalysis/blob/master/Binder%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90.md binder源码分析</p><p>4、https://www.bilibili.com/video/BV1Ef4y127nU&#160; &#160;手写binder进程通信框架</p><p>&#160; &#160; &#160;<a href="https://www.bilibili.com/video/BV1Zp4y1Q7tZ/?spm_id_from=333.788.recommend_more_video.1" rel="nofollow">https://www.bilibili.com/video/BV1Zp4y1 … re_video.1</a></p><p>5、https://wangkuiwu.github.io/2014/09/02/Binder-Datastruct/&#160; binder中的数据结构</p><p>6、https://blog.csdn.net/zhanshenwu/article/details/106458188&#160; binder地址映射全解</p><p>7、https://blog.csdn.net/ljt326053002/article/details/105328384&#160; binder源码分析</p>]]></description>
			<author><![CDATA[dummy@example.com (batsom)]]></author>
			<pubDate>Tue, 11 Oct 2022 07:03:23 +0000</pubDate>
			<guid>https://www.gentoo-zh.org/viewtopic.php?pid=477#p477</guid>
		</item>
	</channel>
</rss>
