前言
我们经常使用mmap函数,它到底是怎么实现的呢?今天就来说说。。。
读者朋友是不是有这样的疑问:
1.在调用mmap()后,它的返回值到底是什么?又是怎么来的呢?
2.我们在驱动中实现static int xxx_drv_mmap(struct file *filp, struct vm_area_struct *vma)的参数为什么是这样的?它跟我们的mmap()函数参数完全不沾边啊。
3.在linux底层驱动的实例中是怎么实现的?做了哪些事情呢?
函数使用
映射
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
On success, mmap() returns a pointer to the mapped area. On error, the value MAP_FAILED (that is, (void *) -1) is returned, and errno is set to indicate the cause of the error.
作用:在进程的虚拟地址空间建立一个新的映射,实质是建立物理内存和虚拟地址间的映射。
返回:成功执行时,mmap()返回被映射区的指针(虚拟地址)。失败时,mmap()返回MAP_FAILED[其值为(void *)-1]。
参数:
addr:期望在进程线性地址(虚拟地址)映射区的开始地址
length:映射区的长度
prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起
PROT_EXEC :页内容可以被执行
PROT_READ :页内容可以被读取
PROT_WRITE :页可以被写入
PROT_NONE :页不可访问
flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体
MAP_FIXED //使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。
MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直到msync()或者munmap()被调用,文件实际上不会被更新。
MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
...
fd:有效的文件描述符。如果MAP_ANONYMOUS被设定,为了兼容问题,其值应为-1
offset:被映射文件对象内容的偏移位置
取消映射
int munmap(void *addr, size_t length);
成功执行时,munmap()返回0。失败时,munmap返回-1,error返回标志和mmap一致;
该调用在进程地址空间中解除一个映射关系,addr是调用mmap()时返回的地址,len是映射区的大小;
当映射关系解除后,对原来映射地址的访问将导致段错误发生。
mmap优势
1、对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。
怎么理解?因为用户空间地址直接对内存进行了映射,可以直接访问。
2、实现了用户空间和内核空间的高效交互方式。两空间的各自修改操作可以直接反映在映射的区域内,从而被对方空间及时捕捉。
3、提供进程间共享内存及相互通信的方式。不管是父子进程还是无亲缘关系的进程,都可以将自身用户空间映射到同一个文件或匿名映射到同一片区域。从而通过各自对映射区域的改动,达到进程间通信和进程间共享的目的。
同时,如果进程A和进程B都映射了区域C,当A第一次读取C时通过缺页从磁盘复制文件页到内存中;但当B再读C的相同页面时,虽然也会产生缺页异常,但是不再需要从磁盘中复制文件过来,而可直接使用已经保存在内存中的文件数据。
4、可用于实现高效的大规模数据传输。内存空间不足,是制约大数据操作的一个方面,解决方案往往是借助硬盘空间协助操作,补充内存的不足。但是进一步会造成大量的文件I/O操作,极大影响效率。这个问题可以通过mmap映射很好的解决。换句话说,但凡是需要用磁盘空间代替内存的时候,mmap都可以发挥其功效。
数据结构
struct vm_area_struct {
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;
...
struct rb_node vm_rb;
struct mm_struct *vm_mm; /* The address space we belong to. */
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
unsigned long vm_flags; /* Flags, see mm.h. */
/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;
...
}
函数分析
调用的过程图示
mmap()系统调用过程
unsigned long do_mmap(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flags, vm_flags_t vm_flags,
unsigned long pgoff, unsigned long *populate,
struct list_head *uf)
{
struct mm_struct *mm = current->mm;
/* Careful about overflows.. */
len = PAGE_ALIGN(len);
/* 获取需要映射到的地址,并验证它是一个有效的地址空间 */
addr = get_unmapped_area(file, addr, len, pgoff, flags);
/* 实际的映射函数 */
addr = mmap_region(file, addr, len, vm_flags, pgoff, uf);
...
return addr;
}
在上面的这个函数中,它的参数和我们用户空间的调用的mmap()函数差不多。
get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
unsigned long pgoff, unsigned long flags)
{
unsigned long (*get_area)(struct file *, unsigned long,
unsigned long, unsigned long, unsigned long);
unsigned long error = arch_mmap_check(addr, len, flags);
/* 1.确定get_area 的匹配函数 */
get_area = current->mm->get_unmapped_area;
if (file) {
if (file->f_op->get_unmapped_area)
get_area = file->f_op->get_unmapped_area;
} else if (flags & MAP_SHARED) {
get_area = shmem_get_unmapped_area;
}
/* 2.调用获取地址空间的函数 */
addr = get_area(file, addr, len, pgoff, flags);
return addr;
}
在上面的这个函数中,获得了可以映射的虚拟地址空间。
unsigned long mmap_region(struct file *file, unsigned long addr,
unsigned long len, vm_flags_t vm_flags, unsigned long pgoff,
struct list_head *uf)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma, *prev;
int error;
struct rb_node **rb_link, *rb_parent;
unsigned long charged = 0;
//检查前一个线性区能否合并新的线性区
vma = vma_merge(mm, prev, addr, addr + len, vm_flags,
NULL, file, pgoff, NULL, NULL_VM_UFFD_CTX);
/* 为新mva分配内存 */
vma = kmem_cache_zalloc(vm_area_cachep, GFP_KERNEL);
/* 初始化赋值 */
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags;
vma->vm_page_prot = vm_get_page_prot(vm_flags);
vma->vm_pgoff = pgoff;
if (file) {
vma->vm_file = get_file(file);
/* 调用驱动中对应的映射函数 */
error = call_mmap(file, vma);
/* 返回的虚拟地址在这里赋值 */
addr = vma->vm_start;
vm_flags = vma->vm_flags;
} else if (vm_flags & VM_SHARED) {
//线性区是一个匿名区,调用此函数进行初始化,共享匿名区主要用于进程间通讯
error = shmem_zero_setup(vma);
}
//把新线性区插入到线性区链表和红-黑树中
vma_link(mm, vma, prev, rb_link, rb_parent);
file = vma->vm_file;
return error;
}
上面这个函数是实际的核心操作函数,它分配了一个新的vma ,然后调用的相应的map函数。其中addr = vma->vm_start的addr将作为返回值,最终会传递到用户空间的mmap()函数的返回值。
而error = call_mmap(file, vma)也解释了为什么在驱动中mmap()实现的函数的参数是下面这样的?
static int xxx_drv_mmap(struct file *filp, struct vm_area_struct *vma)
V4L2驱动中的实现
在V4L2中底层中,带通过DMA分配内存后,需要将内存映射到用户空间。
在videobuf2-dma-contig.c源码中:
const struct vb2_mem_ops vb2_dma_contig_memops = {
...
.mmap = vb2_dc_mmap,
...
};
具体的实现函数如下:
static int vb2_dc_mmap(void *buf_priv, struct vm_area_struct *vma)
{
struct vb2_dc_buf *buf = buf_priv;
int ret;
ret = dma_mmap_attrs(buf->dev, vma, buf->cookie,buf->dma_addr, buf->size, buf->attrs);
vma->vm_flags |= VM_DONTEXPAND | VM_DONTDUMP;
vma->vm_private_data = &buf->handler;
vma->vm_ops = &vb2_common_vm_ops;
vma->vm_ops->open(vma);
return 0;
}
调用了中间包裹函数。
static inline int dma_mmap_attrs(struct device *dev, struct vm_area_struct *vma, void *cpu_addr,
dma_addr_t dma_addr, size_t size, unsigned long attrs)
{
const struct dma_map_ops *ops = get_dma_ops(dev);
if (ops->mmap){
/* 调用实际的映射函数 */
return ops->mmap(dev, vma, cpu_addr, dma_addr, size, attrs);
}
}
我们在使用dma的api时候,例如dma_alloc_coherent等,这些API中都会调用
const struct dma_map_ops *ops = get_dma_ops(dev)得到ops,在arm64 因此DMA API的底层实现主要需要实现dma_map_ops 这个结构体,目前在ARM64 中有三种方式实现。
在arch/arm64/mm/dma-mapping.c源码中:
方式一:
static struct dma_map_ops dummy_dma_ops
方式二:
static struct dma_map_ops iommu_dma_ops
...
方式三:
static struct dma_map_ops swiotlb_dma_ops = {
...
.mmap = __swiotlb_mmap,
...
};
其中,我们用到的函数:
static int __swiotlb_mmap(struct device *dev,
struct vm_area_struct *vma,
void *cpu_addr, dma_addr_t dma_addr, size_t size,
unsigned long attrs)
{
int ret;
unsigned long pfn = dma_to_phys(dev, dma_addr) >> PAGE_SHIFT;
vma->vm_page_prot = __get_dma_pgprot(attrs, vma->vm_page_prot, is_device_dma_coherent(dev));
if (dma_mmap_from_dev_coherent(dev, vma, cpu_addr, size, &ret))
return ret;
/* 实际是调用这个函数进行映射的 */
return __swiotlb_mmap_pfn(vma, pfn, size);
}
函数实现如下:
static int __swiotlb_mmap_pfn(struct vm_area_struct *vma, unsigned long pfn, size_t size)
{
unsigned long nr_vma_pages = (vma->vm_end - vma->vm_start) >>PAGE_SHIFT;
unsigned long nr_pages = PAGE_ALIGN(size) >> PAGE_SHIFT;
unsigned long off = vma->vm_pgoff;
if (off < nr_pages && nr_vma_pages <= (nr_pages - off)) {
/* 将内核态的物理内存映射到用户空间 */
ret = remap_pfn_range(vma, vma->vm_start,
pfn + off,
vma->vm_end - vma->vm_start,
vma->vm_page_prot);
}
}
下面的函数将内核态的物理内存映射到用户空间 ,涉及到MMU和页表映射,具体的实现这里就不分析了。
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
unsigned long pfn, unsigned long size, pgprot_t prot)
{
pgd_t *pgd;
unsigned long next;
unsigned long end = addr + PAGE_ALIGN(size);
struct mm_struct *mm = vma->vm_mm;
unsigned long remap_pfn = pfn;
int err;
if (is_cow_mapping(vma->vm_flags)) {
if (addr != vma->vm_start || end != vma->vm_end)
return -EINVAL;
vma->vm_pgoff = pfn;
}
err = track_pfn_remap(vma, &prot, remap_pfn, addr, PAGE_ALIGN(size));
if (err)
return -EINVAL;
vma->vm_flags |= VM_IO | VM_PFNMAP | VM_DONTEXPAND | VM_DONTDUMP;
BUG_ON(addr >= end);
pfn -= addr >> PAGE_SHIFT;
pgd = pgd_offset(mm, addr);
flush_cache_range(vma, addr, end);
do {
next = pgd_addr_end(addr, end);
err = remap_p4d_range(mm, pgd, addr, next,
pfn + (addr >> PAGE_SHIFT), prot);
if (err)
break;
} while (pgd++, addr = next, addr != end);
if (err)
untrack_pfn(vma, remap_pfn, PAGE_ALIGN(size));
return err;
}
小结
通过上面的几段代码,我们知道了mmap()在内核中主要实现过程,也了解了在驱动中的实现方式。
相关内容阅读
链接: V4L2框架
链接: V4L2之设备注册
链接: V4L2之mmap()函数
链接: V4L2之events
链接: V4L2之buffer分配和映射