V4L2之mmap()函数

10 篇文章 2 订阅
5 篇文章 17 订阅

前言

我们经常使用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函数的调用过程

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分配和映射

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值