2. Binder设备内存映射
任务目标
本案例实现Binder驱动中的mmap文件操作接口,完成内核缓冲区分配与用户态地址映射,并验证用户态与内核态确实共享同一物理内存页。通过本案例,你将理解Binder一次拷贝通信机制的内存基础。
环境依赖
- Linux内核源码树(建议5.10+)
- 已配置的内核编译环境(
make,gcc) - 开发机为x86_64架构(简化页表操作)
核心实现
以下代码实现了一个最小Binder字符设备驱动,核心是binder_mmap函数:分配物理页、建立页表映射,并记录映射信息。用户态通过mmap系统调用获得内核缓冲区的直接访问地址。
// binder_mmap_demo.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/mm.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
#include <linux/highmem.h>
#define DEVICE_NAME "binder_mmap_demo"
#define BINDER_BUFFER_SIZE (4 * PAGE_SIZE) // 4页内核缓冲区
static dev_t dev_num;
static struct cdev binder_cdev;
static struct class *binder_class;
struct binder_buffer_info {
struct page **pages; // 物理页数组
int page_count;
size_t size;
};
static struct binder_buffer_info buffer_info;
static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
unsigned long size = vma->vm_end - vma->vm_start;
unsigned long addr;
int i, ret;
// 仅支持私有映射(Binder实际使用场景)
if (vma->vm_flags & VM_SHARED) {
pr_err("binder_mmap: only private mapping supported\n");
return -EINVAL;
}
// 限制映射大小不超过预分配缓冲区
if (size > buffer_info.size) {
pr_err("binder_mmap: request size %lu exceeds buffer %zu\n",
size, buffer_info.size);
return -EINVAL;
}
// 设置VMA标志:禁止交换、禁止扩展
vma->vm_flags |= VM_DONTEXPAND | VM_DONTDUMP;
vma->vm_flags &= ~VM_MAYWRITE; // 内核缓冲区只读映射(Binder语义)
// 为VMA建立页表,逐页映射到预分配的物理页
for (i = 0, addr = vma->vm_start; addr < vma->vm_end;
addr += PAGE_SIZE, i++) {
ret = vm_insert_page(vma, addr, buffer_info.pages[i]);
if (ret) {
pr_err("binder_mmap: vm_insert_page failed at %lu, ret=%d\n",
addr, ret);
return ret;
}
}
filp->private_data = vma; // 保存映射信息(演示用)
pr_info("binder_mmap: mapped %lu bytes (%d pages) at vma [%lx-%lx]\n",
size, i, vma->vm_start, vma->vm_end);
return 0;
}
static int binder_open(struct inode *inode, struct file *filp)
{
pr_info("binder_open: device opened\n");
return 0;
}
static int binder_release(struct inode *inode, struct file *filp)
{
pr_info("binder_release: device closed\n");
return 0;
}
static const struct file_operations binder_fops = {
.owner = THIS_MODULE,
.open = binder_open,
.release = binder_release,
.mmap = binder_mmap,
};
static int __init binder_mmap_init(void)
{
int i, ret;
// 分配物理页(演示用,实际Binder使用连续kmalloc缓存)
buffer_info.page_count = BINDER_BUFFER_SIZE / PAGE_SIZE;
buffer_info.size = BINDER_BUFFER_SIZE;
buffer_info.pages = kcalloc(buffer_info.page_count,
sizeof(struct page *), GFP_KERNEL);
if (!buffer_info.pages)
return -ENOMEM;
for (i = 0; i < buffer_info.page_count; i++) {
buffer_info.pages[i] = alloc_page(GFP_KERNEL | __GFP_ZERO);
if (!buffer_info.pages[i])
goto err_free_pages;
}
// 注册字符设备
ret = alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
if (ret)
goto err_free_pages;
cdev_init(&binder_cdev, &binder_fops);
ret = cdev_add(&binder_cdev, dev_num, 1);
if (ret)
goto err_unregister;
binder_class = class_create(THIS_MODULE, DEVICE_NAME);
if (IS_ERR(binder_class)) {
ret = PTR_ERR(binder_class);
goto err_cdev_del;
}
device_create(binder_class, NULL, dev_num, NULL, DEVICE_NAME);
pr_info("binder_mmap_demo: initialized, dev major=%d minor=%d\n",
MAJOR(dev_num), MINOR(dev_num));
return 0;
err_cdev_del:
cdev_del(&binder_cdev);
err_unregister:
unregister_chrdev_region(dev_num, 1);
err_free_pages:
for (i = 0; i < buffer_info.page_count; i++)
if (buffer_info.pages[i])
__free_page(buffer_info.pages[i]);
kfree(buffer_info.pages);
return ret;
}
static void __exit binder_mmap_exit(void)
{
int i;
device_destroy(binder_class, dev_num);
class_destroy(binder_class);
cdev_del(&binder_cdev);
unregister_chrdev_region(dev_num, 1);
for (i = 0; i < buffer_info.page_count; i++)
__free_page(buffer_info.pages[i]);
kfree(buffer_info.pages);
pr_info("binder_mmap_demo: exited\n");
}
module_init(binder_mmap_init);
module_exit(binder_mmap_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Binder mmap demo - kernel buffer shared mapping");
用户态验证程序
编译以下程序,通过mmap获取内核缓冲区地址,写入数据后由内核模块读取验证共享性。
// test_mmap_user.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#define DEVICE_PATH "/dev/binder_mmap_demo"
#define MAP_SIZE (4 * 4096)
int main(void)
{
int fd;
char *addr;
const char *msg = "Hello from userspace!";
fd = open(DEVICE_PATH, O_RDWR);
if (fd < 0) {
perror("open");
return 1;
}
addr = mmap(NULL, MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);
if (addr == MAP_FAILED) {
perror("mmap");
close(fd);
return 1;
}
// 尝试写入(应触发段错误,因为映射为只读)
// 此处仅演示读取验证,实际Binder通过ioctl切换读写权限
printf("mmap succeeded at %p\n", addr);
printf("First 4 bytes (should be zero): 0x%08x\n", *(unsigned int *)addr);
// 通过proc查看映射
printf("Check /proc/self/maps for %lx\n", (unsigned long)addr);
munmap(addr, MAP_SIZE);
close(fd);
return 0;
}
编译与运行
# 编译内核模块
make -C /lib/modules/$(uname -r)/build M=$(pwd) modules
# 编译用户态测试程序
gcc -o test_mmap_user test_mmap_user.c
# 加载模块
sudo insmod binder_mmap_demo.ko
# 查看内核日志确认初始化
dmesg | tail -5
# 运行用户态测试
./test_mmap_user
# 验证映射(观察用户态地址与内核日志中的vma范围)
cat /proc/self/maps | grep binder
# 卸载模块
sudo rmmod binder_mmap_demo
验证方法与判定标准
- 模块加载成功:
dmesg显示binder_mmap_demo: initialized,且/dev/binder_mmap_demo节点存在。 - mmap调用成功:用户态程序输出
mmap succeeded at 0x...,且读取前4字节为0(因__GFP_ZERO分配)。 - 映射范围一致:内核日志中
binder_mmap: mapped ... at vma [start-end]的地址范围与用户态/proc/self/maps中对应行一致,证明同一VMA。 - 共享性验证:在模块中增加调试节点(如
/proc/binder_demo_buffer)读取同一物理页内容,与用户态写入值比对。本演示因只读映射,用户态写入会触发段错误,符合预期——实际Binder通过ioctl临时切换页表权限实现写入。
关键机制说明
vm_insert_page将预分配的物理页插入用户VMA的页表,不产生数据拷贝。用户态地址与内核buffer_info.pages[i]指向同一struct page,即同一物理内存。Binder一次拷贝的核心正是:进程A写入数据到内核缓冲区(通过ioctl切换的临时写权限),进程B直接读取同一缓冲区,无需二次拷贝。
上图展示了本案例的内存映射关系:用户态VMA通过页表直接指向内核预分配的物理页,vm_insert_page建立映射后,用户态与内核态共享同一物理内存,无需拷贝即可互相访问数据。
常见问题
- insmod失败:确认内核头文件已安装,且当前内核允许加载模块(
CONFIG_MODULES启用)。 - mmap返回EINVAL:检查是否以
MAP_SHARED方式调用——本驱动仅支持MAP_PRIVATE。 - 用户态写入段错误:符合预期,因VMA设置了只读标志。实际Binder通过ioctl临时切换页表写权限实现数据写入。