Binder(四):mmap 原理:Binder 一次拷贝的底层机制

文章内容

一、mmap 是什么

mmap 内存映射:虚拟地址经 MMU 映射到物理页/文件

1.1 内存映射的定义

mmap 是 Linux 提供的一个系统调用(用户态程序请求内核代为执行某项工作的入口),作用是把进程的一段虚拟内存区域和一个具体的对象关联起来。这个对象可以是磁盘上的文件,也可以是内核分配的一批物理内存。关联建立之后,用文件的内容初始化这块虚拟内存区域,进程读写这段地址就等于读写那个对象。

这里的虚拟地址是进程眼里的地址编号,不装数据;数据躺在物理内存里,也就是内存条上真实的存储单元。两者由 MMU 查页表连接。memory mapping 这个名字说的就是这层连接由谁指定。

1.2 为什么需要 mmap

平时进程根本管不了这层连接。malloc 出来一段内存,虚拟地址落到哪几页物理内存上由内核自行安排,进程既看不到也改不了,而且系统默认不会让两段不同的虚拟地址指向同一块物理内存——地址空间隔离本来就是保护机制的一部分。

想要打破这个默认,就得显式地说明映射目标:这段虚拟内存,我要它对应这个文件,或者对应这批物理页。mmap 就是提这个要求的 API。Binder 需要的「内核和某个进程看到同一份内存」,只能从这条路走。

二、Binder 怎么用 mmap

2.1 binder_mmap:驱动层实现共享映射

binder_mmap:内核与接收方用户空间映射到同一批物理页

Binder 的共享映射发生在驱动内部的 binder_mmap 方法里——它是 Binder 驱动为 mmap 系统调用注册的处理函数,用户态调一次 mmap,内核最终执行的就是它。驱动在这里为该进程开辟一块缓冲区,让内核地址空间和进程用户空间同时指向这批物理页。

有个容易想歪的地方:这不是「用户态调两次 mmap,一次给内核一次给自己」。驱动本身就跑在内核里,两侧的页表它都能操作,一次调用就把两边的映射一起建好了。用户态只需要拿到返回的那个地址。

2.2 谁调用:服务端注册时调用一次

调用方是接收数据的一侧,也就是服务端。像 AMS(ActivityManagerService,管理四大组件与进程生命周期的系统服务)这种常驻服务,在启动向 Binder 注册的时候会打开设备并调用一次 mmap,此后整个进程生命周期里复用这块缓冲区,不会每次通信都重新映射。

2.3 调用链:从应用层到驱动

应用层写的 mmap 并不是内核函数,而是 Libc(C 标准库,Android 上是 Bionic)提供的封装。这层封装把参数整理好,触发一次陷入内核的系统调用,内核按文件描述符找到对应的驱动,最后落到 binder_mmap 上。

简单来说:应用层 mmap → Libc 封装 → 系统调用陷入内核 → Binder 驱动的 binder_mmap 建立共享映射。面试里如果被追问「你调的 mmap 和驱动里的 binder_mmap 什么关系」,答的就是这条链路。

三、文件读写案例:两次拷贝变成一次

3.1 传统文件读写的两次拷贝

文件读写:两次拷贝 vs mmap 一次写入直达

用户空间没法直接碰文件。写一个字符串进磁盘文件,常规路径是调 write,数据先从进程的用户空间拷进内核的页缓存,内核再择机把页缓存刷到文件——用户视角上白白多了一趟搬运。

3.2 mmap 优化:写虚拟内存即写文件

换成 mmap 之后,进程先让一段虚拟内存和这个文件关联,拿到一个普通的字符指针。往这个指针里写数据,写的就是文件对应的那批物理页,内核负责把它们回写到磁盘。中间那次用户态到内核态的拷贝没有了。

3.3 代码演示

下面这段是最小可运行的验证:全程没有调用 write,只用 memcpy 往内存里写,文件里就出现了内容。它把「虚拟内存与对象关联」这句抽象的话变成可以 cat 出来的结果。

c
#include <stdio.h>                                    // perror#include <string.h>                                   // memcpy strlen#include <fcntl.h>                                    // open O_RDWR#include <unistd.h>                                   // ftruncate close#include <sys/mman.h>                                 // mmap msync munmap int main(void) {                                      // 程序入口    const char *path = "/tmp/mmap_demo.txt";          // 目标文件路径    int fd = open(path, O_RDWR | O_CREAT, 0644);      // 打开文件,不存在就创建,拿到文件描述符 fd    if (fd < 0) { perror("open"); return 1; }         // fd 是内核为打开的文件分配的整数编号     size_t len = 4096;                                // 映射长度,取一页 4KB    if (ftruncate(fd, len) != 0) return 1;            // 把文件撑到 4KB,否则映射区超出文件会触发 SIGBUS     char *addr = mmap(NULL, len,                      // 由内核挑一段空闲虚拟地址来映射                      PROT_READ | PROT_WRITE,         // 这段虚拟内存可读可写                      MAP_SHARED,                     // 共享映射:写入对文件和其他映射者都可见                      fd, 0);                         // 关联到 fd 指向的文件,从偏移 0 开始    if (addr == MAP_FAILED) { perror("mmap"); return 1; }  // 映射失败直接退出     const char *msg = "written via mmap, no write()\n";     // 准备写入的内容    memcpy(addr, msg, strlen(msg) + 1);               // 直接写虚拟内存,这就是唯一一次数据搬运     if (msync(addr, len, MS_SYNC) != 0) return 1;     // 主动把脏页刷回磁盘,保证立刻可见    munmap(addr, len);                                // 解除映射,归还这段虚拟地址    close(fd);                                        // 关闭文件描述符    printf("check: cat %s\n", path);                  // 提示去文件系统里验证    return 0;                                         // 结束}

编译运行后 cat /tmp/mmap_demo.txt,字符串已经在文件里。整个过程只有 memcpy 这一次内存写入,没有出现用户态到内核态的第二次拷贝——数据落在哪一批物理页上,是映射建立那一刻就定好的。

四、Binder 驱动的一次拷贝

Binder 驱动一次拷贝:copy_from_user 进共享区,接收方直接读

4.1 两块虚拟内存指向同一块物理内存

把上面例子里的「文件」换成「一块物理内存」,就是 Binder 的做法。binder_mmap 分配一批物理页,一头挂到内核地址空间的虚拟地址上,另一头挂到接收方进程用户空间的虚拟地址上。两个地址值完全不同,页表翻译之后落到同一批页。

4.2 copy_from_user 一次拷贝进共享区

发送方发起事务时,驱动调用 copy_from_user——内核从用户态地址读取数据的标准接口,内核不能直接解引用用户传来的指针,必须走这个带校验的入口——把数据拷进那块共享物理内存。写入的位置正好在接收方的映射范围内。

拷贝到此结束。驱动唤醒接收方,给它的是缓冲区内的一个偏移;接收方按这个偏移读自己的用户空间,读到的就是刚写进去的那份数据,没有第二段搬运。

4.3 零拷贝还是拷贝一次

写 mmap 出来的内存看着像零拷贝,因为用户代码里只出现了一次 memcpy 或者根本看不到拷贝动作。但 Binder 这条链路上,copy_from_user 是实打实的一次内存复制,把发送方的数据搬进了共享区。所以准确的说法是一次拷贝,而不是零拷贝——省掉的是接收侧那一次。

五、总结

5.1 mmap 做的事

mmap 把一段虚拟内存和一个对象绑在一起,让读写这段地址直接作用于对象本身,中转环节因此消失。对象是文件时,写内存就是写文件;对象是一批物理页、并且内核和某个进程都映射了它时,两边就共享了同一份数据。

Binder 用的是第二种。发送方那一次 copy_from_user 无法省,接收方那一次因为映射早就建好而根本不需要,这就是「跨进程只拷贝一次」的全部内容。

5.2 可验证的动作与延伸

3.3 的代码可以直接编译运行,把 MAP_SHARED 换成 MAP_PRIVATE 再跑一遍,文件不会有任何变化——私有映射走写时复制,改动只留在进程自己的副本里,这个对照能确认共享靠的是标志位而不是 mmap 本身。

进程跑起来后 cat /proc/<pid>/maps,能看到映射区间和它后面跟的文件名;Android 上任取一个应用进程执行同样的命令,会看到 /dev/binder 对应的那段映射,那就是 binder_mmap 建立的接收缓冲区。


本文由 AgentPlanFlow 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Binder(四):mmap 原理:Binder 一次拷贝的底层机制 · Yinchen 的博客