内存映射介绍
内存映射,简而言之就是将用户空间的一段内存区域映射到内核空间,映射成功后,用户对这段内存区域
的修改可以直接反映到内核空间,相反,内核空间对这段区域的修改也直接反映用户空间。那么对于内核空间
<---->用户空间两者之间需要大量数据传输等操作的话效率是非常高的。
首先,驱动程序先分配好一段内存,接着用户进程通过库函数 mmap()来告诉内核要将多大的内存映射到内
核空间,内核经过一系列函数调用后调用对应的驱动程序的 file_operation 中的 mmap 函数,在该函数中调用
remap_pfn_range()来建立映射关系。直白一点就是:驱动程序在 mmap()中利用 remap_pfn_range()函数将内核空
间的一段内存与用户空间的一段内存建立映射关系。
用户空间函数
caddr_t mmap(caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset);
addr:指定文件应被映射到用户空间的起始地址,这样,选择起始地址的任务将由内核完成,而函数的返回值就是映射到用户空间的地址。其类型 caddr_t 实际上就是 void *。通常为 NULL(由内核来指定)
len:映射区的长度.长度单位是以内存页为单位 PAGE_ALIGN()。它从被映射文件开头 offset 个字节开始算
起,offset 参数一般设为 0,表示从文件头开始映射。
prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过 or 运算合理地组合在一起。mprotect(caddr_t addr, size_t len, int prot)函数可以修改。
PROT_EXEC //页内容可以被执行
PROT_READ //页内容可以被读取
PROT_WRITE //页可以被写入
PROT_NONE //页不可访问
flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体。
MAP_FIXED //使用指定的映射起始地址,如果由 start 和 len 参数指定的内存区重叠于现存的映射空间,重
叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。
MAP_SHARED //与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。直
到 msync()或者 munmap()被调用,文件实际上不会被更新。
MAP_PRIVATE //建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。
MAP_DENYWRITE //这个标志被忽略。
MAP_EXECUTABLE //同上
MAP_NORESERVE //不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。
当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。
MAP_LOCKED //锁定映射区的页面,从而防止页面被交换出内存。
MAP_GROWSDOWN //用于堆栈,告诉内核 VM 系统,映射区可以向下扩展。
MAP_ANONYMOUS //匿名映射,映射区不与任何文件关联。
MAP_ANON //MAP_ANONYMOUS 的别称,不再被使用。
MAP_FILE //兼容标志,被忽略。
MAP_32BIT //将映射区放在进程地址空间的低 2GB,MAP_FIXED 指定时会被忽略。当前这个标志只在 x86-
64 平台上得到支持。
MAP_POPULATE //为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。
MAP_NONBLOCK //仅和 MAP_POPULATE 一起使用时才有意义。不执行预读,只为已存在于内存中的页
面建立页表入口。
其中,MAP_SHARED , MAP_PRIVATE 必选其一,而 MAP_FIXED 则不推荐使用。
fd:有效的文件描述词。一般是由 open()函数返回,其值也可以设置为-1,此时需要指定 flags 参数中的
MAP_ANON,表明进行的是匿名映射。
offset:被映射对象内容的起点。一般设为 0,表示从文件头开始映射。
函数的返回值为最后文件映射到进程空间的地址,进程可以直接操作起始地址为该值的有效地址。系统调
用 mmap 用于共享内存时有下面两种常用的方式:
1) 使用普通文件提供的内存映射:适用于任何进程之间。此时,需要打开或创建一个文件,然后再调用
mmap,这种方式有许多特点和要注意的地方,我们在后面或举例子说明。
2) 使用特殊文件提供匿名内存映射:适用于具有亲缘关系的进程之间。由于父子进程特殊的亲缘关系,在
父进程中调用 mmap,然后调用 fork。那么在调用 fork 之后,子进程急促继承父进程匿名映射后的地
址空间,同样也继承 mmap 返回的地址,这样,父子进程就可以通过映射区进行通信了。注意,mmap
返回的地址,需要由父进程共同维护。
munmap(caddr_t addr, size_t len) //用来删除内存映射
成功执行时,mmap()返回被映射区的指针,munmap()返回 0。失败时,mmap()返回 MAP_FAILED[其值为
(void *)-1],munmap 返回-1。errno 被设为以下的某个值
EACCES:访问出错
EAGAIN:文件已被锁定,或者太多的内存已被锁定
EBADF:fd 不是有效的文件描述词
EINVAL:一个或者多个参数无效
ENFILE:已达到系统对打开文件的限制
ENODEV:指定文件所在的文件系统不支持内存映射
ENOMEM:内存不足,或者进程已超出最大内存映射数量
EPERM:权能不足,操作不允许
ETXTBSY:已写的方式打开文件,同时指定 MAP_DENYWRITE 标志
SIGSEGV:试着向只读区写入
SIGBUS:试着访问不属于进程的内存区
一般来说,进程在映射空间对共享内容的改变并不直接写回到磁盘文件中,往往在调用 munmap 后才执行该
操作.可以通过调用 msync 实现磁盘上文件内容与共享内存区的内容一致。该函数的原型如下:
int msync ( void * addr, size_t len, int flags);
addr:文件映射到进程空间的地址;
len:映射空间的大小;
flags:刷新的参数设置,可以取值 MS_ASYNC/MS_SYNC/MS_INVALIDATE
1) 取值为 MS_ASYNC(异步)时,调用会立即返回,不等到更新的完成;
2) 取值为 MS_SYNC(同步)时,调用会等到更新完成之后返回;
3) 取 MS_INVALIDATE(通知使用该共享区域的进程,数据已经改变)时,在共享内容更改之后,使得
文件的其他映射失效,从而使得共享该文件的其他进程去重新获取最新值
内核空间函数
注意:当用户调用 mmap()的时候,内核会进行如下的处理:
① 在进程的虚拟空间查找一块 VMA。
② 将这块 VMA 进行映射。
③ 如果设备驱动程序或者 VFS 文件系统的 file_operations 定义了 mmap()操作,则调用它。
④ 将这个 VMA 插入进程的 VMA 链表中。
内核调用流程:mmap.c
sys_mmap()->sys_mmap_pgoff()->vm_mmap_pgoff()->do_mmap_pgoff()->do_mmap()->file->f_op->mmap()
VFS 文件操作函数集中的 mmap 声明如下:
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
unsigned int (*poll) (struct file *, struct poll_table_struct *);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
int (*release) (struct inode *, struct file *);
int (*fsync) (struct file *, loff_t, loff_t, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long,
unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *,
size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *,
size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **, void **);
long (*fallocate)(struct file *file, int mode, loff_t offset,
loff_t len);
void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
unsigned (*mmap_capabilities)(struct file *);
#endif
ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
loff_t, size_t, unsigned int);
int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,u64);
ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,
u64);
} __randomize_layout;
vma 包含了用于访问设备的虚拟地址的信息,因此大量的工作由内核完成。为了执行 mmap,驱动程序只
需要为该地址返回建立合适的页表,并将 vma->vm_ops 替换为一系列的新操作就可以了。
有两种建立页表的方法:使用 remap_pfn_range 函数一次全部建立;或者通过 nopage VMA 方法每次建立一
个页表。本文只考虑第一种情况。
remap_pfn_range 负责为一段物理地址映射到进程的虚拟空间,它有如下的原型:
int remap_pfn_range(struct vm_area_struct *vma, unsigned long from, unsigned long
to, unsigned long size, pgprot_t prot)
vma:虚拟内存区域,在一定范围内的页将被映射到该区域内。
from:表示内存映射开始处的虚拟地址。
to:虚拟地址应该映射到的物理地址的页帧号由物理地址右移 PAGE_SHIFT 得到。
size:以字节为单位,被重新映射的区域大小。
prot:新页所要求的保护属性。