这是一系列的 Binder 文章,会从内核层到 Framework 层,再到应用层,浅显易懂,介绍整个 Binder 的规划。详见《图解 Binder:概述》。
本文依据 Android platform 分支 android-13.0.0_r1 和内核分支 common-android13-5.15 解析。
一些要害代码的链接,或许会因为源码的改变,发生方位偏移、丢失等现象。能够查找函数名,重新进行定位。
每个进程在运用 Binder 进程通讯之前,都会先经过 mmap 进行映射,用于树立接纳其他进程业务消息的缓冲区。mmap 是 Binder 完成进程通讯一次复制的要害。
内核也为缓冲区的高效分配、开释,规划了杂乱的数据结构。
在本文,咱们将深入探讨 Binder 的内存办理。这触及了虚拟内存、mmap、缓冲区分配和开释、物理内存页分配和开释,以及内存减缩器等机制。它们一起提升 Binder 通讯的性能。
虚拟内存
在深入了解 Binder 的内存办理之前,咱们需求先掌握一下虚拟内存的基本概念。
虚拟内存是核算机体系的一种内存办理技能,它的优势在于:
- 它能够让每个程序认为自己独占了全部的(虚拟)内存,简化了内存办理和数据保护。
- 它使得大型程序的开发变得更容易,程序员无需关怀内存物理地址的分配和办理,只需求处理逻辑地址。
- 它供给了有用的内存保护机制,每个进程运转在其自己独立的地址空间内,互不干扰。
- 经过将虚拟内存分页,物理内存也依照相同巨细分页,每个虚拟页面能够映射到任意物理页面,使得物理内存能够更加有用地运用。
虚拟寻址
在运用了虚拟内存的核算机体系里,进程拥有的是虚拟地址。在拜访物理内存之前,每个虚拟地址都需求先转换为对应的物理地址。地址翻译是经过 CPU 上的内存办理单元(MMU)完成的。MMU会运用一种称为页表(Page Table)的数据结构来完成这种转换。页表本质上是一种映射关系的表,它把虚拟地址映射到物理地址。
下图展示了地址翻译的过程:
页与页框
页:页是虚拟内存分配的基本单位,一般页的巨细是 2 的幂,如 4KB。
页框:即物理内存页。物理内存被区分为巨细持平的块,每一块被称为页框,其巨细与虚拟内存的页巨细相同。
页表会记载虚拟页和物理页框之间的映射关系。当程序拜访一个虚拟地址时,硬件会首先查找页表,确认这个虚拟地址归于哪一个虚拟页,然后从页表中找到对应的物理页框,然后再拜访这个页框中的实际数据。
虚拟地址是由页号和页内偏移量组成的。页号用于在页表中查找相应的页表条目,该条目包含了页框的地址。页内偏移量则用于在页框中找到具体的数据。
虚拟内存的区分
虚拟内存技能使得每个进程都有自己的虚拟地址空间。虚拟地址空间一般被分为两部分:用户空间和内核空间。
-
用户空间:这部分虚拟内存被分配给用户级的应用程序运用。应用程序的代码、数据和堆栈都坐落用户空间。
-
内核空间:这部分虚拟内存用于操作体系内核。内核代码和数据、页表、缓存以及驱动程序都在内核空间。用户空间程序不能直接拜访这部分内存。
这个分割一般是静态的。在 32 位的 Linux 体系中,一般最高的 1GB 虚拟内存是内核空间,而剩余的 3GB 是用户空间(这个比例在 64 位体系中会有不同,这儿只讨论 32 位体系)。
如上图,在 32 位的 Linux 体系中,一般的虚拟地址空间散布是这样的:
- 用户空间:虚拟地址范围是 0x00000000 – 0xBFFFFFFF,这部分占有了虚拟地址空间的 3GB(也便是 0-3GB ),供用户程序运用。
- 内核空间:虚拟地址范围是 0xC0000000 – 0xFFFFFFFF,这部分占有了虚拟地址空间的 1GB(也便是 3-4GB ),供内核运用。
每个进程都有自己的用户空间,彼此独立,而且一切的进程都同享同一个内核空间。如下图:
内核空间有自己的页表,不会随进程切换改变。而用户空间对应进程,每个进程都有自己的用户空间,都有自己的页表。所以每逢进程切换,用户空间也会切换。内核空间、不同用户空间的页表都由内核办理着。
用户空间、内核空间的彼此拜访
一切内存拜访,无论是用户空间还是内核空间,都是经过虚拟地址进行的。
但是用户空间即使拥有内核空间的虚拟地址,也不能直接拜访内核空间的内存,反之亦然。首要原因有两个:
- 安全性:假如用户程序能够直接拜访内核空间,那么恶意程序就或许会攻击内核,破坏体系的安全性。
- 稳定性:假如用户程序能够直接更改内核数据,那么它或许会破坏内核的稳定性,导致体系溃散。
操作体系经过在不同的权限级别上运转用户程序和内核来完成这种保护。
虽然内核有权限拜访用户空间,但直接拜访或许会带来一些问题,比方内核空间无法保证用户空间的数据始终有用和可用。用户进程或许会因为各种原因(如被其他进程杀死)导致其虚拟地址中的数据无效,假如内核在这种情况下直接拜访用户空间的虚拟地址,就或许会发生严峻的过错,如内核溃散。所以为了保护内核,内核一般会将用户空间的数据,复制到内核,再进行拜访。
内核里有两个函数是专门用来保证用户空间、内核空间彼此拜访的安全:
copy_from_user:将n个字节的用户空间内存从地址 from 复制到内核空间的地址 to 。
copy_to_user:将n个字节的内核空间内存从地址from复制到用户空间的地址to。
这两个函数的方针不仅仅是数据复制。它们的首要意图是在内核代码安全地拜访用户空间内存时,处理各种或许出现的问题,如无效地址、地址未对齐、页面未映射等问题。
mmap
前面咱们了解了虚拟内存相关的概念。现在咱们开始学习 mmap,正式开启 Binder 内存办理的旅程。
咱们先来了解一下传统 IO 的 write/read 和 mmap 的差异。
一次复制的 write/read
read 和 write 每次调用都会发生一次数据复制。以 read 体系调用为例:当用户进程读取磁盘文件时,它会调用 read。这时,文件数据会从磁盘读取到内核空间的一个缓冲区,再复制到用户空间。
“复制”一般指的是将数据从一个内存区域复制到另一个内存区域,这个过程会耗费 CPU 资源。
读取/写入文件、设备的时分,一般会采用一个叫 DMA(Direct Memory Access)的硬件技能。它允许设备(比方硬盘或网络接口)直接读取和写入主内存,无需经过 CPU,从而防止了传统意义上的“复制”。
因而,咱们不把 DMA 算作“复制”。因为它不触及 CPU,能够直接从一个当地传输数据到另一个当地。
零复制的 mmap
mmap 函数自身其实并不触及到数据的复制。mmap 是一种内存映射技能:它将用户空间的一段虚拟地址和内核空间的一段虚拟地址,映射到同一块物理内存上,即让用户空间、内核空间同享这块物理内存。
在读取磁盘文件时,咱们先用 mmap 完成映射,然后再读取文件到内核空间的缓冲区。因为用户空间和内核空间同享这块物理内存,所以,进程经过用户空间的虚拟地址,直接就能够读取到文件数据,防止了 copy_to_user,所以是“零复制”:
mmap 的优缺
当调用 mmap 时,mmap 一般会映射较大的一块物理内存。但操作体系一般不会立马分配对应的物理内存,而是创建一个映射表,将用户空间、内核空间的虚拟地址与物理地址相关联。当你真正拜访这个内存区域的时分,或许触发缺页(page fault),此刻操作体系才会分配对应的物理内存页,再从硬盘上把数据读入到物理内存。
相比之下,当调用 read 函数时,咱们需求的缓冲区一般是 4KB,也便是一个物理页,不会像 mmap 那样,发生很多的缺页和树立页表映射的开支。
但是,这并不意味着 read/write 比 mmap 更高效。比方 read,它每次都需求进行数据复制,而 mmap 只在第一次拜访时触发缺页,假如这个内存区域被频频拜访,那么 mmap 在长期运转下来或许更高效。此外,mmap 也支持多个进程同享同一个文件映射,这是 read 无法做到的。
总的来说,mmap 和 read/write 各有好坏,适用的场景也有所不同。在处理大文件的时分,mmap 显然更有优势。
Binder 的 mmap
Binder 进程通讯一次复制的隐秘
当客户端想要发送数据到服务端数据时,惯例方法便是先从客户端进程复制数据到内核,再从内核复制数据到服务端进程,发生了两次内存复制:
而在 Binder 驱动中,服务端进程会事前调用 mmap 将服务端的用户空间的一段虚拟内存和内核空间的一段虚拟内存,映射到同一块物理内存上。这段物理内存,将作为一个缓冲区,接纳来自不同客户端的数据。比方,客户端发送业务数据时,内核会经过 copy_from_user 将数据放进缓冲区,然后告诉服务端。服务端只需求经过用户空间相应的虚拟地址,就能直接拜访数据:
当 Android 的进程第一次经过 Binder 框架进行 IPC 时,就会触发 ProcessState 的初始化,调用 Binder 驱动的 mmap。
mVMStart = mmap(nullptr, BINDER_VM_SIZE, PROT_READ, MAP_PRIVATE | MAP_NORESERVE,
opened.value(), 0);
上面的代码,是调用 mmap 映射的内存巨细,巨细为BINDER_VM_SIZE,即 1MB – 2 Page:
#define BINDER_VM_SIZE ((1 * 1024 * 1024) - sysconf(_SC_PAGE_SIZE) * 2)
不设为 1MB 的原因,能够看 Android 的 git commit 记载:
Modify the binder to request 1M – 2 pages instead of 1M. The backing store in the kernel requires a guard page, so 1M allocations fragment memory very badly. Subtracting a couple of pages so that they fit in a power of two allows the kernel to make more efficient use of its virtual address space.
这一块内存,便是每个运用 Binder 通讯的进程都会事前树立的一块读写缓冲区。也是 Binder 驱动完成跨进程通讯只需求一次数据复制的要害所在。
Binder 的 mmap 完成
在 Linux 体系中,mmap 体系调用是经过虚拟文件体系(VFS)层和内核的内存办理子体系来完成的。
当进程执行 mmap 体系调用时,内核会查看方针文件的类型,并调用相应文件体系的 mmap 完成。对于普通的磁盘文件,这一般是文件体系驱动的 mmap 完成。对于设备文件,如 /dev/binder ,便是 Binder 驱动的 mmap 完成。
Binder 的 mmap 调用链路
下面是一个 Binder 驱动的 mmap 完成的调用链路:
-
mmap()
-
bionic/libc/SYSCALLS.TXT
-
__NR_mmap
-
__NR3264_mmap
-
sys_mmap()
-
ksys_mmap_pgoff()
-
vm_mmap_pgoff()
-
do_mmap()
- get_unmapped_area() // 从当时进程的用户空间中找到一片闲暇区域,回来这块区域的开始地址
-
mmap_region()
- vm_area_alloc() // 用 slab 给 vm_area_struct 实例分配内存
- get_file()
-
call_mmap() // 调用相应文件体系的 mmap 完成
-
binder_mmap()
- binder_alloc_mmap_handler()
-
binder_mmap()
-
do_mmap()
-
vm_mmap_pgoff()
-
ksys_mmap_pgoff()
-
sys_mmap()
-
__NR3264_mmap
-
__NR_mmap
-
bionic/libc/SYSCALLS.TXT
Binder 驱动的 mmap 完成,便是 binder_mmap()。
调用 mmap 后,会从用户空间区分一块虚拟内存,终究在调用 Binder 驱动的 binder_mmap 时,也会从内核空间区分一块虚拟内存出去。它们会映射到同一块物理内存(在 Binder 通讯时,才会正式分配需求的物理内存)。这块内存作为一块缓冲区,专门用来接纳、处理与该进程相关的 Binder 通讯数据。如下图:
- vm_area_struct:描绘一块用户空间的虚拟内存,包含开始地址、完毕地址等。
- vm_struct:描绘一块内核空间的虚拟内存,包含开始地址、内存区域巨细等。
依据 mmap 机制,服务端启动的时分,咱们会为它创建一个缓冲区,用户空间、内核空间都有一块虚拟内存映射到它,服务端进程和内核都能够拜访这个缓冲区,但是运用的是不同的虚拟地址(用户空间地址和内核空间地址)。
当内核接纳来自客户端的业务数据时,先从缓冲区中分配一块 buffer,然后经过 copy_from_user 将数据复制到该 buffer 指向的物理内存上。这时分,内核操作的是这块 buffer 在 vm_struct 里的地址,也便是内核空间的虚拟地址。终究内核运用偏移量核算出该 buffer 在 vm_area_struct 里的地址,也便是用户空间的虚拟地址,再告诉服务端接纳。这期间没有 copy_to_user,只需求一次复制。
用户空间、内核空间的虚拟地址切换
Binder 会核算并记载 vm_area_struct、vm_struct 两块虚拟内存的偏移,示例代码可参考 Android9 的 binder_alloc_mmap_handler():
int binder_alloc_mmap_handler(struct binder_alloc *alloc,
struct vm_area_struct *vma)
{
struct vm_struct *area;
// 从内核空间分配一块和 vma 巨细持平的虚拟内存
area = get_vm_area(vma->vm_end - vma->vm_start, VM_ALLOC);
// area->addr 便是 vma 的开始地址
alloc->buffer = area->addr;
// 核算 vma 和 vm 之间的偏移量,记载在 alloc->user_buffer_offset 里
alloc->user_buffer_offset = vma->vm_start - (uintptr_t)alloc->buffer;
}
这样,经过偏移量,咱们就能够经过用户空间的虚拟地址,快速取得对应的内核空间的虚拟地址:
void *kern_ptr;
kern_ptr = (void *)(user_ptr - alloc->user_buffer_offset);
或许经过内核空间的虚拟地址,快速取得对应的用户空间的虚拟地址:
unsigned long user_page_addr;
user_page_addr = (uintptr_t)page_addr + alloc->user_buffer_offset
新的 binder_mmap 完成
在 Android10 今后,binder_mmap 的完成有了比较大的改动,撤销了 vm_struct 的运用:
- binder_alloc_mmap_handler() 不再从内核空间分配一块和 vma 巨细持平的虚拟内存,也不再记载什么偏移量。
- 将客户端数据复制到缓冲区的时分,是逐一物理页处理的:先经过 kmap 树立内核空间虚拟地址和物理内存页的映射,然后经过 copy_from_user 按页复制,终究再 kunmap 撤销映射。详见 binder_alloc_copy_user_to_buffer()。
unsigned long
binder_alloc_copy_user_to_buffer(struct binder_alloc *alloc,
struct binder_buffer *buffer,
binder_size_t buffer_offset,
const void __user *from,
size_t bytes)
{
while (bytes) {
unsigned long size;
unsigned long ret;
struct page *page;
pgoff_t pgoff;
void *kptr;
// buffer 是从缓冲区里区分的一小块,用来接纳客户端数据
// buffer 现已分配物理内存,还经过 vm_insert_page() 将用户空间虚拟地址与物理页绑定
page = binder_alloc_get_page(alloc, buffer,
buffer_offset, &pgoff);
size = min_t(size_t, bytes, PAGE_SIZE - pgoff);
// 经过 kmap ,将物理页映射到内核空间,并回来该内存页的内核空间虚拟地址
kptr = kmap(page) + pgoff;
// 逐页复制
ret = copy_from_user(kptr, from, size);
// 撤销映射
kunmap(page);
if (ret)
return bytes - size + ret;
bytes -= size;
from += size;
buffer_offset += size;
}
return 0;
}
撤销 vm_struct 的运用,最大的优点是极大地防止了耗尽内核可用的地址空间。
但撤销了记载偏移量,又如何取得对应的用户空间的虚拟地址呢?
当从缓冲区分配一块处理业务数据的 buffer 时,就会为 buffer 分配对应的物理页。buffer 能够简略理解为一段连续的用户空间虚拟地址。为其分配物理页的时分,会逐页绑定到对应的虚拟地址上。这是发生在上面代码的 binder_alloc_get_page() 之前的。kmap 再将物理页临时映射到内核空间,那就相当于将用户空间虚拟地址、内核空间虚拟地址映射到了同一个物理页。内核经过内核空间虚拟地址调用 copy_from_user 把数据从客户端复制到 buffer 的物理页后,内核空间虚拟地址就用不上,能够经过 kunmap 撤销映射。后续,服务端经过 buffer 的用户空间虚拟地址,就能够拜访到对应物理页上的数据。
Binder 内存办理
Binder 的内存办理,指的便是办理 mmap 映射的这块缓冲区。Binder 驱动为这块缓冲区,规划了杂乱的数据结构,完成了高效查询、分配和。
缓冲区分配器:binder_alloc
每个运用 Binder 通讯的 Android 进程,都会事前树立一个缓冲区。它用 binder_alloc 来办理。
binder_alloc 的部分定义如下:
struct binder_alloc {
struct vm_area_struct *vma;
struct mm_struct *vma_vm_mm;
void __user *buffer;
struct list_head buffers;
struct rb_root free_buffers;
struct rb_root allocated_buffers;
struct binder_lru_page *pages;
size_t buffer_size;
};
- vma:一个指针,指向的 vm_area_struct便是 mmap 调用时分配的。vm_area_struct 描绘了一块用户空间的虚拟内存,包含开始地址、完毕地址等,也便是 Binder 的缓冲区。
- vma_vm_mm:mm_struct 描绘的是该进程的用户空间及相关信息。
- buffer:vm_area_struct 的开始地址。
- buffers:一个双向循环链表,办理着从缓冲区里区分的一切 binder_buffer,按 buffer 的开始的用户空间虚拟地址排序。一开始缓冲区没有被拆分,buffers 只要一个 binder_buffer,代表整个缓冲区。
- free_buffers:一棵红黑树,办理着一切能够分配的 binder_buffer,树里按 buffer 的巨细排序,意图是为了依据业务数据的巨细在 O(logn) 时刻杂乱度内找到能够接受业务数据的闲暇 binder_buffer。
- allocated_buffers:一棵红黑树,办理着一切已分配的 binder_buffer,树里按 buffer 的开始的用户空间虚拟地址排序,意图是为了依据 buffer 的开始地址在 O(logn) 时刻杂乱度内找到方针 binder_buffer,进行开释。
- pages:一个数组,每个元素都是一个指针,指向一个 binder_lru_page。binder_lru_page 对应一个物理页,被组织成一个双向循环链表,意图是在体系内存不足时,高效收回物理页。
- buffer_size:描绘整个缓冲区的巨细。
缓冲区:binder_buffer
Binder 驱动用 binder_buffer 来描绘缓冲区。
struct binder_buffer {
struct list_head entry;
struct rb_node rb_node;
unsigned free:1;
unsigned clear_on_free:1;
unsigned allow_user_free:1;
unsigned async_transaction:1;
unsigned oneway_spam_suspect:1;
unsigned debug_id:27;
struct binder_transaction *transaction;
struct binder_node *target_node;
size_t data_size;
size_t offsets_size;
size_t extra_buffers_size;
void __user *user_data;
int pid;
};
- entry:表明链表中的一个节点。将 binder_buffer 刺进 binder_alloc 的 buffers 链表时,便是将该 entry 刺进链表中。遍历链表时,拿到该 entry,即可经过 container_of() 获取对应的 binder_buffer。
- rb_node:表明红黑树中的一个节点,和 entry 类似,用于在 binder_alloc 的 free_buffers 和 allocated_buffers 中表明该 binder_buffer。
- free – debug_id:free、clear_on_free、async_transaction 和 oneway_spam_suspect 这几个字段,首要是一些标志位。free 标识该 binder_buffer 是闲暇的,:1 表明该字段是位字段,只占 1 个比特位。debug_id 是用于调试的仅有ID。
- transaction:指向与该缓冲区关联的 binder_transaction。
- target_node:指向与该缓冲区关联的 binder_node。
- data_size:transaction 数据的巨细。
- offsets_size:offsets 数组的巨细。
- extra_buffers_size:其他目标(如 sg 列表)的空间巨细。
- user_data:用户空间的虚拟地址,指向该缓冲区的开始方位。
- pid:所属进程的进程 ID。
binder_buffer 的巨细
结构体 binder_buffer 并没有记载它表明的缓冲区的巨细。大体上,缓冲区的巨细等于 data_size + offsets_size + extra_buffers_size 。但因为字节对齐,缓冲区的巨细或许会比这三个字段的总和稍大一点。Binder 驱动运用 buffers 链表,采用了比较巧妙的方法核算一个 binder_buffer 的巨细,详见 binder_alloc_buffer_size():
static size_t binder_alloc_buffer_size(struct binder_alloc *alloc,
struct binder_buffer *buffer)
{
// 该 binder_buffer 是链表的终究一个节点
if (list_is_last(&buffer->entry, &alloc->buffers))
// alloc->buffer + alloc->buffer_size 便是该进程的整个缓冲区的完毕地址
// 用整个缓冲区的完毕地址减去该 binder_buffer 开始地址,就能够得出它的巨细
return alloc->buffer + alloc->buffer_size - buffer->user_data;
// 该 binder_buffer 不是链表的终究一个节点
// 用后续节点表明的 binder_buffer 的完毕地址减去该 binder_buffer 开始地址,就能够得出它的巨细
return binder_buffer_next(buffer)->user_data - buffer->user_data;
}
缓冲区初始化
Binder 驱动其实一开始是不会为缓冲区分配对应的物理内存的,仅仅先分配了一段用户空间的虚拟地址,即仅仅先分配了虚拟内存。
注:allocated_buffers 红黑树此刻为空树。
初始化代码首要在 binder_alloc_mmap_handler() 里:
int binder_alloc_mmap_handler(struct binder_alloc *alloc,
struct vm_area_struct *vma)
{
struct binder_buffer *buffer;
// 缓冲区巨细最大不会超越 4MB
alloc->buffer_size = min_t(unsigned long, vma->vm_end - vma->vm_start,
SZ_4M);
// 缓冲区的开始地址便是 vma->vm_start
alloc->buffer = (void __user *)vma->vm_start;
alloc->pages = kcalloc(alloc->buffer_size / PAGE_SIZE,
sizeof(alloc->pages[0]),
GFP_KERNEL);
// 为一个 binder_buffer 目标分配内存
buffer = kzalloc(sizeof(*buffer), GFP_KERNEL);
buffer->user_data = alloc->buffer;
// 将新创建的 binder_buffer 刺进 buffers链表中
list_add(&buffer->entry, &alloc->buffers);
buffer->free = 1;
// 将新创建的 binder_buffer 刺进到 free_buffers 红黑树
binder_insert_free_buffer(alloc, buffer);
alloc->free_async_space = alloc->buffer_size / 2;
binder_alloc_set_vma(alloc, vma);
return 0;
}
缓冲区分配
在处理业务的时分,会从缓冲区区分一块 binder_buffer 出来处理业务。这时分,才会为这块小缓冲区分配物理内存。
为业务分配缓冲区,有两种情况:
- 查询 free_buffers 红黑树,找不到巨细刚好适宜的缓冲区,就找大于业务数据巨细的最小闲暇缓冲区进行切开:
- ① 查询了 free_buffers 红黑树,大于业务数据巨细的最小闲暇缓冲区便是 binder_buffer0。对其进行切开,如图,binder_buffer0 标识为已运用,切开出来的 binder_buffer1 标识为闲暇的。
- ② 将 binder_buffer1 刺进 buffers 链表,刺进到 binder_buffer0 后边
- ③ 从 free_buffers 红黑树中移除 binder_buffer0,并刺进 binder_buffer1。
- ④ 将 binder_buffer0 刺进到 allocated_buffers 红黑树中。
- 查询 free_buffers 红黑树,刚好找到适宜的缓冲区,直接运用它(下图的缓冲区是经过一段时刻运用构成的):
- ① 查询了 free_buffers 红黑树,刚好等于业务数据巨细的闲暇缓冲区便是 binder_buffer1。将binder_buffer1 标识为已运用。
- ② binder_buffer1 现已在 buffers 链表中,无需做任何更改。
- ③ 从 free_buffers 红黑树中移除 binder_buffer1
- ④ 将 binder_buffer1 刺进到 allocated_buffers 红黑树中。
缓冲区分配的代码首要在 binder_alloc_new_buf_locked() 中:
static struct binder_buffer *binder_alloc_new_buf_locked(
struct binder_alloc *alloc,
size_t data_size,
size_t offsets_size,
size_t extra_buffers_size,
int is_async,
int pid)
{
// ALIGN 宏用于将一个值向上对齐到指定的对齐鸿沟。以防止任何潜在的内存对齐问题。
// 在这儿,对齐鸿沟是 sizeof(void *),一般是 4 字节(32 位体系)或 8 字节(64 位体系)。
data_offsets_size = ALIGN(data_size, sizeof(void *)) +
ALIGN(offsets_size, sizeof(void *));
size = data_offsets_size + ALIGN(extra_buffers_size, sizeof(void *));
/* Pad 0-size buffers so they get assigned unique addresses */
size = max(size, sizeof(void *));
// 遍历红黑树,寻觅适宜的 buffer
while (n) {
buffer = rb_entry(n, struct binder_buffer, rb_node);
BUG_ON(!buffer->free);
buffer_size = binder_alloc_buffer_size(alloc, buffer);
// 比需求的size大,纳入考虑,但还是要继续寻觅有没有更适宜的,即巨细更挨近,遍历左子树
if (size < buffer_size) {
best_fit = n;
n = n->rb_left;
} else if (size > buffer_size) // 比需求的 size 小,不符合,遍历右子树
n = n->rb_right;
else {
// 刚好 size == buffer_size,不需求再找了
best_fit = n;
break;
}
}
// 在前面遍历红黑树的时分,size < buffer_size,会遍历左子树,
// 这时分,遍历完左子树,都或许没有找到比之前更适宜的,终究 n 会变为 NULL,
// 而 buffer 在遍历过程中,也会被赋值为左子树节点的值。
// 所以,这儿需求更新为之前找到的 best_fit 的值。
if (n == NULL) {
buffer = rb_entry(best_fit, struct binder_buffer, rb_node);
buffer_size = binder_alloc_buffer_size(alloc, buffer);
}
// 核算出的地址(has_page_addr)表明找到的 buffer 所占有的内存页的终究一个内存页的开始地址。
has_page_addr = (void __user *)
(((uintptr_t)buffer->user_data + buffer_size) & PAGE_MASK);
// 核算出的地址(end_page_addr)表明实际需求的 buffer 的内存页的终究一个内存页的完毕地址。
end_page_addr =
(void __user *)PAGE_ALIGN((uintptr_t)buffer->user_data + size);
if (end_page_addr > has_page_addr)
end_page_addr = has_page_addr;
// 为 buffer 分配物理内存页
ret = binder_update_page_range(alloc, 1, (void __user *)
PAGE_ALIGN((uintptr_t)buffer->user_data), end_page_addr);
// 分配的 buffer 比实际需求的小
if (buffer_size != size) {
struct binder_buffer *new_buffer;
new_buffer = kzalloc(sizeof(*buffer), GFP_KERNEL);
// 把多出来的内存放进一个新的 new_buffer 里
new_buffer->user_data = (u8 __user *)buffer->user_data + size;
// 刺进双向链表 buffers 中:刺进到本来的 buffer 后边
list_add(&new_buffer->entry, &buffer->entry);
// 标记为闲暇的
new_buffer->free = 1;
// 将 new_buffer 刺进到红黑树 free_buffers 中
binder_insert_free_buffer(alloc, new_buffer);
}
// 从红黑树 free_buffers 移除已分配的 buffer
rb_erase(best_fit, &alloc->free_buffers);
// 标记为已运用
buffer->free = 0;
buffer->allow_user_free = 0;
// 将已分配的 buffer 刺进到红黑树 allocated_buffers 中
binder_insert_allocated_buffer_locked(alloc, buffer);
buffer->data_size = data_size;
buffer->offsets_size = offsets_size;
buffer->async_transaction = is_async;
buffer->extra_buffers_size = extra_buffers_size;
buffer->pid = pid;
buffer->oneway_spam_suspect = false;
return buffer;
}
缓冲区开释
处理完业务之后,就会开释为业务分配的缓冲区:
-
① binder_buffer1 开释后,因为 binder_buffer1 的前后都是闲暇的 buffer,会进行兼并,即 binder_buffer0、binder_buffer1 和 binder_buffer2 兼并,只剩 binder_buffer0。
-
② buffers 链表处理:
- 发现 binder_buffer1 不是尾节点,而且后续节点 binder_binder2 是闲暇的,直接移除 binder_buffer2 节点。
- 发现 binder_buffer2 不是头节点,而且前驱节点 binder_binder0 是闲暇的,这时分,移除的是 不是 binder_binder0,而是 binder_buffer1 节点。
-
③ 在处理 buffers 链表的时分,会同步更新 free_buffers 红黑树,所以终究 free_buffers 红黑树只剩 binder_buffer0 节点。
-
④ 从 allocated_buffers 红黑树中移除 binder_buffer1。
缓冲区开释的代码首要在 binder_free_buf_locked():
static void binder_free_buf_locked(struct binder_alloc *alloc,
struct binder_buffer *buffer)
{
size_t size, buffer_size;
buffer_size = binder_alloc_buffer_size(alloc, buffer);
size = ALIGN(buffer->data_size, sizeof(void *)) +
ALIGN(buffer->offsets_size, sizeof(void *)) +
ALIGN(buffer->extra_buffers_size, sizeof(void *));
// 开释 buffer 对应的物理内存页
binder_update_page_range(alloc, 0,
(void __user *)PAGE_ALIGN((uintptr_t)buffer->user_data),
(void __user *)(((uintptr_t)
buffer->user_data + buffer_size) & PAGE_MASK));
// 将开释的 buffer 从红黑树 allocated_buffers 中移除
rb_erase(&buffer->rb_node, &alloc->allocated_buffers);
// 将 buffer 标识为闲暇的
buffer->free = 1;
// 假如该 buffer 不是链表中的终究一个节点,尝试和后续节点进行兼并
if (!list_is_last(&buffer->entry, &alloc->buffers)) {
struct binder_buffer *next = binder_buffer_next(buffer);
// 假如后续节点是未被运用的 buffer ,将这两个 buffer 兼并。
// 兼并方法很简略,将后续节点移除即可。
if (next->free) {
rb_erase(&next->rb_node, &alloc->free_buffers);
binder_delete_free_buffer(alloc, next);
}
}
// buffers 是个双向循环链表,当 alloc->buffers.next == buffer->entry 时,
// 开释的 buffer 便是链表里的仅有节点,prev、next都是指向它自己,不需求兼并
if (alloc->buffers.next != &buffer->entry) {
struct binder_buffer *prev = binder_buffer_prev(buffer);
// 假如前驱节点是未被运用的 buffer ,将这两个 buffer 兼并。
// 兼并方法不是将前驱节点移除,而是将代表开释 buffer 的节点移除
if (prev->free) {
binder_delete_free_buffer(alloc, buffer);
rb_erase(&prev->rb_node, &alloc->free_buffers);
// 将 buffer 更新为 prev
buffer = prev;
}
}
// 将开释的 buffer 刺进到红黑树 free_buffers 中
binder_insert_free_buffer(alloc, buffer);
}
分配/开释物理内存页
分配、开释一个 binder_buffer 的物理内存页,都是在 binder_update_page_range() 里进行的:
static int binder_update_page_range(struct binder_alloc *alloc, int allocate,
void __user *start, void __user *end)
{
void __user *page_addr;
unsigned long user_page_addr;
struct binder_lru_page *page;
struct vm_area_struct *vma = NULL;
struct mm_struct *mm = NULL;
bool need_mm = false;
// allocate :为 0 时是开释物理内存页,为 1 时是分配物理内存页
if (allocate == 0)
goto free_range;
......
// 以页为单位,遍历 binder_buffer 的一切页(page_addr 便是当时处理的页的用户空间虚拟地址)
for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
int ret;
bool on_lru;
size_t index;
index = (page_addr - alloc->buffer) / PAGE_SIZE;
page = &alloc->pages[index];
// page_ptr 不为空,之前已分配物理内存页,所以仅仅从 binder_alloc_lru 链表中移除即可
if (page->page_ptr) {
on_lru = list_lru_del(&binder_alloc_lru, &page->lru);
continue;
}
// alloc_page() 分配一个物理内存页,回来值是指向该物理内存页 page 的指针
page->page_ptr = alloc_page(GFP_KERNEL |
__GFP_HIGHMEM |
__GFP_ZERO);
page->alloc = alloc;
INIT_LIST_HEAD(&page->lru);
user_page_addr = (uintptr_t)page_addr;
// vm_insert_page() 将一个用户空间虚拟地址,绑定到指定的物理内存页上
ret = vm_insert_page(vma, user_page_addr, page[0].page_ptr);
if (index + 1 > alloc->pages_high)
alloc->pages_high = index + 1;
}
return 0;
// 开释 binder_buffer 的物理内存页
free_range:
for (page_addr = end - PAGE_SIZE; 1; page_addr -= PAGE_SIZE) {
bool ret;
size_t index;
index = (page_addr - alloc->buffer) / PAGE_SIZE;
page = &alloc->pages[index];
// 仅仅简略得把物理内存页增加至链表 binder_alloc_lru
ret = list_lru_add(&binder_alloc_lru, &page->lru);
if (page_addr == start)
break;
}
return vma ? -ENOMEM : -ESRCH;
}
-
分配:以页为单位,遍历 binder_buffer 的一切页(page_addr 便是当时处理的页的用户空间虚拟地址)
- 假如之前现已为该页分配物理内存页,就复用,只需求从 binder_alloc_lru 链表里移除即可。复用的优点是,减少了分配、映射的耗费。
- 假如没有为该页分配过物理内存页,就经过 alloc_page() 分配,然后用 vm_insert_page() 树立映射。
-
开释:以页为单位,遍历 binder_buffer 的一切页,不撤销映射、也不收回物理内存页,仅仅逐一地刺进到 binder_alloc_lru 的尾部。
结构体 page 便是用来描绘一个物理内存页(即页框)的。
binder_alloc_lru 是一个 lru 链表,首要用于内存不足时,收回物理内存页,一切用户进程同享这个 lru 链表。结构体 binder_lru_page 描绘了该 lru 链表的一个结点,记载着对应物理内存页的信息。
注意,虽然是多个进程一起运用 binder_alloc_lru。但是,由某个进程刺进到binder_alloc_lru的页,只能由它自己再次获取并运用,不能被其他进程获取到并运用。
内存减缩器
内存减缩器是一种用于收回未运用的内存页面的机制,它们在体系内存紧张时被调用,收回最近最久未运用的闲暇物理内存页。
Binder 驱动初始化时,即 binder_init() 被调用时,会经过 binder_alloc_shrinker_init() 注册 Binder 内存减缩器。
struct list_lru binder_alloc_lru;
// binder_shrinker定义了 Binder 驱动程序的内存减缩器。这儿声明晰减缩器的两个回调函数:
// count_objects 和 scan_objects
static struct shrinker binder_shrinker = {
.count_objects = binder_shrink_count,
.scan_objects = binder_shrink_scan,
.seeks = DEFAULT_SEEKS,
};
int binder_alloc_shrinker_init(void)
{
// 初始化 binder_alloc_lru 链表
int ret = list_lru_init(&binder_alloc_lru);
if (ret == 0) {
// 注册 Binder 驱动的内存减缩器
// 注册减缩器后,内核就能够在需求时调用其 scan_objects 和 count_objects 回调函数。
ret = register_shrinker(&binder_shrinker);
if (ret)
list_lru_destroy(&binder_alloc_lru);
}
return ret;
}
在体系内存不足的时分,binder_shrink_scan() 会被调用:
-
binder_shrink_scan()
-
list_lru_walk() // 遍历 binder_alloc_lru 链表
- binder_alloc_free_page() // 收回最近最久未运用的闲暇物理内存页
-
list_lru_walk() // 遍历 binder_alloc_lru 链表