在Linux设备驱动中,mmap
操作允许用户空间程序直接将设备内存映射到其地址空间,从而绕过内核缓冲区的复制,提升I/O效率。以下是实现mmap
设备操作的详细步骤和代码示例:
1. 驱动中实现mmap
的核心步骤
(1) 分配设备内存
为设备分配物理连续的内存(如使用dma_alloc_coherent
或kmalloc
):
static char *kernel_buffer;
static dma_addr_t phys_addr;
// 模块初始化时分配内存
static int __init mydev_init(void) {
kernel_buffer = dma_alloc_coherent(NULL, BUFFER_SIZE, &phys_addr, GFP_KERNEL);
if (!kernel_buffer)
return -ENOMEM;
return 0;
}
// 模块退出时释放内存
static void __exit mydev_exit(void) {
dma_free_coherent(NULL, BUFFER_SIZE, kernel_buffer, phys_addr);
}
(2) 实现mmap
方法
在file_operations
中定义mmap
函数指针:
static int mydev_mmap(struct file *filp, struct vm_area_struct *vma) {
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT; // 计算字节偏移
unsigned long pfn = phys_to_pfn(phys_addr + offset); // 物理地址转PFN
unsigned long size = vma->vm_end - vma->vm_start;
// 检查映射范围是否合法
if (offset + size > BUFFER_SIZE)
return -EINVAL;
// 设置非缓存属性
vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);
// 映射物理内存到用户空间
if (remap_pfn_range(vma, vma->vm_start, pfn, size, vma->vm_page_prot))
return -EAGAIN;
return 0;
}
static struct file_operations mydev_fops = {
.owner = THIS_MODULE,
.mmap = mydev_mmap,
// 其他操作(open/release等)...
};
(3) 处理用户空间偏移和权限
- 偏移量处理:用户空间的
mmap
调用通过offset
参数选择不同内存区域。 - 权限设置:禁用缓存以确保实时访问:
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot); // 完全非缓存 // 或 vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot); // 写合并(适用于帧缓冲区)
2. 用户空间测试程序
用户程序通过mmap
访问设备内存:
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#define BUFFER_SIZE 4096
int main() {
int fd = open("/dev/mydevice", O_RDWR);
if (fd < 0) {
perror("open failed");
return -1;
}
// 映射设备内存
char *user_buffer = mmap(NULL, BUFFER_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (user_buffer == MAP_FAILED) {
perror("mmap failed");
close(fd);
return -1;
}
// 读写操作
sprintf(user_buffer, "Hello from userspace!");
printf("Read from device: %s\n", user_buffer);
// 解除映射
munmap(user_buffer, BUFFER_SIZE);
close(fd);
return 0;
}
3. 关键注意事项
(1) 内存类型选择
- DMA内存:使用
dma_alloc_coherent
分配,适用于需要与设备DMA操作共享的内存。 - 内核常规内存:使用
kmalloc
分配(物理连续),但需确保GFP_DMA
标志以兼容DMA。
(2) 物理地址转换
- PFN计算:通过
phys_to_pfn
将物理地址转换为页帧号。 - High Memory处理:若设备内存位于高端内存(
HIGHMEM
),需使用kmap
临时映射,但remap_pfn_range
已自动处理。
(3) 安全性检查
- 偏移和长度校验:防止用户空间映射超出设备内存范围。
- 并发访问控制:使用互斥锁保护共享资源:
static DEFINE_MUTEX(mmap_mutex); static int mydev_mmap(...) { mutex_lock(&mmap_mutex); // 临界区操作... mutex_unlock(&mmap_mutex); }
4. 高级场景:非连续内存映射
若设备内存非连续(如通过vmalloc
分配),需逐页映射:
static int mydev_mmap(struct file *filp, struct vm_area_struct *vma) {
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
unsigned long size = vma->vm_end - vma->vm_start;
int i;
for (i = 0; i < (size >> PAGE_SHIFT); i++) {
unsigned long pfn = vmalloc_to_pfn(kernel_buffer + offset + i*PAGE_SIZE);
if (vm_insert_page(vma, vma->vm_start + i*PAGE_SIZE, pfn_to_page(pfn)))
return -EAGAIN;
}
return 0;
}
5. 调试技巧
- 查看映射状态:
cat /proc/<pid>/maps # 查看进程的内存映射
- 内核日志调试:
printk(KERN_DEBUG "mmap: offset=0x%lx, size=%lu\n", offset, size);
- 硬件断点:使用JTAG调试器或QEMU监视设备内存访问。
总结
通过实现mmap
方法,设备驱动能够:
- 提升性能:避免用户空间与内核间的数据拷贝。
- 灵活管理内存:支持直接访问DMA缓冲区、硬件寄存器等。
- 简化用户程序:用户空间可直接操作设备内存。
关键点包括正确分配内存、处理物理地址转换、设置页面属性及严格的边界检查。通过合理设计,mmap
成为高效设备驱动开发的核心技术之一。
参考: