1 mmap驱动要做的事情
确定物理地址
确定属性:是否使用 cache、 buffer
建立映射关系
参考 Linux 驱动源文件代码:
我们要验证mmap功能,在驱动程序中申请一个 8K 的 buffer,让 APP 通过 mmap 能直接访问。
2 mmap驱动代码示例分析
linux内核中常用的内存申请方式:
函数名 | 说明 |
---|---|
kmalloc | 分配到的内存物理地址是连续的 |
kzalloc | 分配到的内存物理地址是连续的,内容清 0 |
vmalloc | 分配到的内存物理地址不保证是连续的 |
vzalloc | vzalloc 分配到的内存物理地址不保证是连续的,内容清 0 |
我们在 mmap 时应该使用 kmalloc 或 kzalloc,这样得到的内存物理地址是连续的,mmap后 APP 才可以使用同一个基地址去访问这块内存。 (如果物理地址不连续,就要执行多次 mmap 了)
点击查看代码
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/miscdevice.h>
#include <linux/kernel.h>
#include <linux/major.h>
#include <linux/mutex.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>
#include <linux/stat.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/tty.h>
#include <linux/kmod.h>
#include <linux/gfp.h>
#include <asm/pgtable.h>
#include <linux/mm.h>
#include <linux/slab.h>
static int major = 0;
static char *kernel_buf;
static struct class *hello_class;
static int bufsiz = 1024*8;
#define MIN(a, b) (a < b ? a : b)
static ssize_t hello_drv_read (struct file *file, char __user *buf, size_t size, loff_t *offset){
int err;
err = copy_to_user(buf, kernel_buf, MIN(bufsiz, size));
return MIN(bufsiz, size);
}
static ssize_t hello_drv_write (struct file *file, const char __user *buf, size_t size, loff_t *offset)
{
int err;
err = copy_from_user(kernel_buf, buf, MIN(1024, size));
return MIN(1024, size);
}
static int hello_drv_mmap(struct file *file, struct vm_area_struct *vma)
{
/* 获得物理地址 */
unsigned long phy = virt_to_phys(kernel_buf);
/* 设置属性: cache, buffer */
vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);
/* map */
if (remap_pfn_range(vma, vma->vm_start, phy >> PAGE_SHIFT,
vma->vm_end - vma->vm_start, vma->vm_page_prot)) {
printk("mmap remap_pfn_range failed\n");
return -ENOBUFS;
}
return 0;
}
static int hello_drv_open (struct inode *node, struct file *file){
return 0;
}
static int hello_drv_close (struct inode *node, struct file *file){
return 0;
}
static struct file_operations hello_drv = {
.owner = THIS_MODULE,
.open = hello_drv_open,
.read = hello_drv_read,
.write = hello_drv_write,
.release = hello_drv_close,
.mmap = hello_drv_mmap,
};
static int __init hello_init(void)
{
int err;
kernel_buf = kmalloc(bufsiz, GFP_KERNEL);
strcpy(kernel_buf, "old");
major = register_chrdev(0, "hello", &hello_drv);
hello_class = class_create(THIS_MODULE, "hello_class");
err = PTR_ERR(hello_class);
if (IS_ERR(hello_class)) {
printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
unregister_chrdev(major, "hello");
return -1;
}
device_create(hello_class, NULL, MKDEV(major, 0), NULL, "hello");
return 0;
}
static void __exit hello_exit(void)
{
device_destroy(hello_class, MKDEV(major, 0));
class_destroy(hello_class);
unregister_chrdev(major, "hello");
kfree(kernel_buf);
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
分析:init时,驱动使用kmalloc分配8K空间(物理地址连续), 初始化为”old“
字符串。实现read,write函数。mmap函数中:
/* 获得物理地址 */
unsigned long phy = virt_to_phys(kernel_buf);
/* 设置属性: cache, buffer */
vma->vm_page_prot = pgprot_writecombine(vma->vm_page_prot);
/*映射*/
remap_pfn_range(vma, vma->vm_start, phy >> PAGE_SHIFT, vma->vm_end - vma->vm_start, vma->vm_page_prot);
pgprot_writecombine
设置属性为Non-cached buffered (NCB)
#include <asm/pgtable.h>
#define pgprot_noncached(prot) \
__pgprot_modify(prot, PTE_ATTRINDX_MASK, PTE_ATTRINDX(MT_DEVICE_nGnRnE) | PTE_PXN | PTE_UXN)
#define pgprot_writecombine(prot) \
__pgprot_modify(prot, PTE_ATTRINDX_MASK, PTE_ATTRINDX(MT_NORMAL_NC) | PTE_PXN | PTE_UXN)
#define pgprot_device(prot) \
__pgprot_modify(prot, PTE_ATTRINDX_MASK, PTE_ATTRINDX(MT_DEVICE_nGnRE) | PTE_PXN | PTE_UXN)
注意:remap_pfn_range
中,pfn
的意思是“ Page Frame Number”
。在 Linux 中,整个物理地址空间可以分为第 0 页、第 1 页、第 2 页,诸如此类,这就是 pfn。假设每页大小是 4K,那么给定物理地址 phy
,它的 pfn = phy / 4096 = phy >> 12
。内核的 page 一般是 4K,但是也可以配置内核修改 page的大小。所以为了通用, pfn = phy >> PAGE_SHIFT
#include <linux/mm.h>
int remap_pfn_range(struct vm_area_struct *, unsigned long addr,
unsigned long pfn, unsigned long size, pgprot_t);
如果我们的buf不是用kmalloc
, 而是vmalloc
,那么需要映射多次,每次映射一个page 4k
.(MMU过程中内存以page为单位作为连续内存单元)
3 mmap应用代码示例与分析
点击查看代码
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
/*
* ./hello_drv_test
*/
int main(int argc, char **argv){
int fd;
char *buf;
int len;
char str[1024];
/* 1. 打开文件 */
fd = open("/dev/hello", O_RDWR);
if (fd == -1)
{
printf("can not open file /dev/hello\n");
return -1;
}
/* 2. mmap
* MAP_SHARED : 多个APP都调用mmap映射同一块内存时, 对内存的修改大家都可以看到。
* 就是说多个APP、驱动程序实际上访问的都是同一块内存
* MAP_PRIVATE : 创建一个copy on write的私有映射。
* 当APP对该内存进行修改时,其他程序是看不到这些修改的。
* 就是当APP写内存时, 内核会先创建一个拷贝给这个APP,
* 这个拷贝是这个APP私有的, 其他APP、驱动无法访问。
*/
buf = mmap(NULL, 1024*8, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (buf == MAP_FAILED){
printf("can not mmap file /dev/hello\n");
return -1;
}
printf("mmap address = 0x%x\n", buf);
printf("buf origin data = %s\n", buf); /* old */
/* 3. write */
strcpy(buf, "new");
/* 4. read & compare */
/* 对于MAP_SHARED映射: str = "new"
* 对于MAP_PRIVATE映射: str = "old"
*/
read(fd, str, 1024);
if (strcmp(buf, str) == 0)
{
/* 对于MAP_SHARED映射,APP写的数据驱动可见
* APP和驱动访问的是同一个内存块
*/
printf("compare ok!\n");
} else {
/* 对于MAP_PRIVATE映射,APP写数据时, 是写入原来内存块的"拷贝"
*/
printf("compare err!\n");
printf("str = %s!\n", str); /* old */
printf("buf = %s!\n", buf); /* new */
}
while (1){
sleep(10); /* cat /proc/pid/maps */
}
munmap(buf, 1024*8);
close(fd);
return 0;
}
3.1 共享映射与私有映射
/*
* MAP_SHARED : 多个APP都调用mmap映射同一块内存时, 对内存的修改大家都可以看到。
* 就是说多个APP、驱动程序实际上访问的都是同一块内存
* MAP_PRIVATE : 创建一个copy on write的私有映射。
* 当APP对该内存进行修改时,其他程序是看不到这些修改的。
* 就是当APP写内存时, 内核会先创建一个拷贝给这个APP,
* 这个拷贝是这个APP私有的, 其他APP、驱动无法访问。
*/
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);
3.1.1 copy on write
① mmap时使用MAP_PRIVATE
②③ 当写入mmap内存时,会copy这块内存
④写入新数据,会将数据写入新copy的内存
⑤读数据还是从旧的那块映射内存去读,因此这时会与buf中的数据不一样
根据上面的mmap应用示例来分析和验证MAP_SHARED
和MAP_PRIVATE
的差异:
先用MAP_PRIVATE
,执行测试程序:
再用MAP_SHARED
,执行测试程序: