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

验证方法与判定标准

  1. 模块加载成功:dmesg显示binder_mmap_demo: initialized,且/dev/binder_mmap_demo节点存在。
  2. mmap调用成功:用户态程序输出mmap succeeded at 0x...,且读取前4字节为0(因__GFP_ZERO分配)。
  3. 映射范围一致:内核日志中binder_mmap: mapped ... at vma [start-end]的地址范围与用户态/proc/self/maps中对应行一致,证明同一VMA。
  4. 共享性验证:在模块中增加调试节点(如/proc/binder_demo_buffer)读取同一物理页内容,与用户态写入值比对。本演示因只读映射,用户态写入会触发段错误,符合预期——实际Binder通过ioctl临时切换页表权限实现写入。

关键机制说明

vm_insert_page将预分配的物理页插入用户VMA的页表,不产生数据拷贝。用户态地址与内核buffer_info.pages[i]指向同一struct page,即同一物理内存。Binder一次拷贝的核心正是:进程A写入数据到内核缓冲区(通过ioctl切换的临时写权限),进程B直接读取同一缓冲区,无需二次拷贝。

用户态 内核态 用户虚拟地址 0x7f... (VMA) 页表 (PTE) vm_insert_page() 建立映射 struct binder_buffer_info pages[] 数组, page_count=4 物理内存页 (4 × PAGE_SIZE) Page 0 Page 1 Page 2 Page 3 mmap() PTE 指向同一物理页 同一物理内存,零拷贝共享 dmesg: mapped 16384 bytes (4 pages)

上图展示了本案例的内存映射关系:用户态VMA通过页表直接指向内核预分配的物理页,vm_insert_page建立映射后,用户态与内核态共享同一物理内存,无需拷贝即可互相访问数据。

常见问题

  • insmod失败:确认内核头文件已安装,且当前内核允许加载模块(CONFIG_MODULES启用)。
  • mmap返回EINVAL:检查是否以MAP_SHARED方式调用——本驱动仅支持MAP_PRIVATE。
  • 用户态写入段错误:符合预期,因VMA设置了只读标志。实际Binder通过ioctl临时切换页表写权限实现数据写入。