dev_mem的mmap分析

当用户空间需要实现驱动程序时, 需要操作底层的寄存器. 使用/dev/mem映射io内存空间就是一种常用的方式. /dev/mem 一般都是使用其 mmap 函数. 将底层的寄存器映射到用户空间地址. 下面对 /dev/memmmap函数进行一下简单的分析.
/dev/mem设备文件是由drivers/char/mem.c驱动创建. 该设备文件的file_operations

1
2
3
4
5
6
7
8
9
10
11
static const struct file_operations __maybe_unused mem_fops = {
.llseek = memory_lseek,
.read = read_mem,
.write = write_mem,
.mmap = mmap_mem,
.open = open_mem,
#ifndef CONFIG_MMU
.get_unmapped_area = get_unmapped_area_mem,
.mmap_capabilities = memory_mmap_capabilities,
#endif
};

mmap_mem定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
static int mmap_mem(struct file *file, struct vm_area_struct *vma)
{
size_t size = vma->vm_end - vma->vm_start;

if (!valid_mmap_phys_addr_range(vma->vm_pgoff, size))
return -EINVAL;

if (!private_mapping_ok(vma))
return -ENOSYS;

if (!range_is_allowed(vma->vm_pgoff, size))
return -EPERM;

if (!phys_mem_access_prot_allowed(file, vma->vm_pgoff, size,
&vma->vm_page_prot))
return -EINVAL;

vma->vm_page_prot = phys_mem_access_prot(file, vma->vm_pgoff,
size,
vma->vm_page_prot);

vma->vm_ops = &mmap_mem_ops;

/* Remap-pfn-range will mark the range VM_IO */
if (remap_pfn_range(vma,
vma->vm_start,
vma->vm_pgoff,
size,
vma->vm_page_prot)) {
return -EAGAIN;
}
return 0;
}

valid_mmap_phys_addr_range函数始终返回1.
private_mapping_ok函数在由MMU的情况下始终返回1.
range_is_allowed函数不配置CONFIG_STRICT_DEVMEM时始终返回1, 在配置CONFIG_STRICT_DEVMEM时, 齐定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static inline int range_is_allowed(unsigned long pfn, unsigned long size)
{
u64 from = ((u64)pfn) << PAGE_SHIFT;
u64 to = from + size;
u64 cursor = from;

while (cursor < to) {
if (!devmem_is_allowed(pfn))
return 0;
cursor += PAGE_SIZE;
pfn++;
}
return 1;
}

在这个函数中, 会对需要映射的地址段进行检查, 在x86平台上, devmem_is_allowed定义为:

1
2
3
4
5
6
7
8
9
10
int devmem_is_allowed(unsigned long pagenr)
{
if (pagenr < 256)
return 1;
if (iomem_is_exclusive(pagenr << PAGE_SHIFT))
return 0;
if (!page_is_ram(pagenr))
return 1;
return 0;
}

在arm平台上, dev_mem_allowed定义为:

1
2
3
4
5
6
7
8
int devmem_is_allowed(unsigned long pfn)
{
if (iomem_is_exclusive(pfn << PAGE_SHIFT))
return 0;
if (!page_is_ram(pfn))
return 1;
return 0;j
}

在x86上, 前1M空间是预留给BIOS和一些其他X等应用使用的空间, 这段空间是允许映射的.
iomem_is_exclusive定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/*
* check if an address is reserved in the iomem resource tree
* returns 1 if reserved, 0 if not reserved.
*/
int iomem_is_exclusive(u64 addr)
{
struct resource *p = &iomem_resource;
int err = 0;
loff_t l;
int size = PAGE_SIZE;

if (!strict_iomem_checks)
return 0;

addr = addr & PAGE_MASK;

read_lock(&resource_lock);
for (p = p->child; p ; p = r_next(NULL, p, &l)) {
/*
* We can probably skip the resources without
* IORESOURCE_IO attribute?
*/
if (p->start >= addr + size)
break;
if (p->end < addr)
continue;
/*
* A resource is exclusive if IORESOURCE_EXCLUSIVE is set
* or CONFIG_IO_STRICT_DEVMEM is enabled and the
* resource is busy.
*/
if ((p->flags & IORESOURCE_BUSY) == 0)
continue;
if (IS_ENABLED(CONFIG_IO_STRICT_DEVMEM)
|| p->flags & IORESOURCE_EXCLUSIVE) {
err = 1;
break;
}
}
read_unlock(&resource_lock);

return err;
}

如果这段地址对应的resource标记为IORESOURCE_BUSY, 那么将映射失败.
如果这段地址在ram中, 同样不允许映射.
可以看出在不配置CONFIG_STRICT_DEVMEM时, /dev/mem的限制是最小的.
phys_mem_access_prot_allowed函数返回1
检查各种限制条件通过后, 接着调用phys_mem_access_prot(file, vma->vm_pgoff, size, vma->vm_page_prot);来改变被映射的内存区的的页面属性.
对于arm64, 定义如下:

1
2
3
4
5
6
7
8
9
10
pgprot_t phys_mem_access_prot(struct file *file, unsigned long pfn,
unsigned long size, pgprot_t vma_prot)
{
if (!pfn_valid(pfn))
return pgprot_noncached(vma_prot);
else if (file->f_flags & O_SYNC)
return pgprot_writecombine(vma_prot);
return vma_prot;
}
EXPORT_SYMBOL(phys_mem_access_prot);

对于非内存空间, 使用nocached, 对于内存空间如果file打开了O_SYNC标志就使用write_combine.
最后调用remap_pfn_range来分配页表, 讲用户空间的一段线性地址空间指向映射的区域.
!