版权声明:本文为CSDN博主「CQ062364」的原创文章,遵循CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/CQ062364/article/details/38499937
mmap是将一个文件中的连续部分映射到虚拟内存中的一块连续的区域,它做的映射是线性映射,如果需要将一个文件中的不同部分以不同的顺序映射到虚拟内存中的连续区域,则需要使用多个mmap映射,从消耗的资源来看这样做代价比较昂贵(因为分配的vm_area_struct多了)。内核提供了一个系统调用叫做remap_file_pages,通过这个函数可以实现非线性映射,同时不需要分配更多的vm_area_struct。下面先看一下这个系统调用是怎么用的,然后说一下非线性映射在内核中是怎么实现的。
该函数包含在头文件sys/mman.h中,函数原型如下:
int remap_file_pages(void *start, size_t size, int prot, ssize_t pgoff, int flags);
要创建非线性映射,必须执行以下步骤。
1.用函数mmap创建一个线性映射,该映射的flag必须为MAP_SHARED。
2.调用一次或者多次remap_file_pages,对已经映射了的文件上的位置和虚拟内存中的地址的映射关系进行重新排列。
remap_file_pages的作用是将文件上pgoff和size标识的区域移动到一个新的位置。
start: 标识移动的目标位置,因此该地址必须位于第一步中用mmap创建的映射范围中。
size: 要移动的区域的长度,以字节为单位。
pgoff: 制定了要移动区域在文件中的偏移量,以页为单位。
prot: 必须为0
flags: 和mmap中的flags的含义是一样的。
下面看一段示例代码,代码的作用是将一个文件text的第一页和第二页颠倒映射,text文件的前4096个字节全部为字母a,后4096个字节全部为字母b。
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <memory.h>
#define LEN 8192
int main(void)
{
int i, fd, ret;
char str[LEN] = {0};
char path[256] = {0};
char* start = NULL;
unsigned int sum = 0;
fd = open("text", O_RDWR);
start = mmap(NULL, LEN, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);//先执行线性映射,映射文件text的前两页。
if(start == MAP_FAILED) {
perror("mmap failed");
return;
}
memcpy(str, start, 8);
printf("%s\n", str);
memcpy(str, start + 4096, 8);
printf("%s\n", str);
memcpy(str, start + 4096 - 4, 8);
printf("%s\n", str);
ret = remap_file_pages(start, LEN >> 1, 0, 1, MAP_SHARED);//将文件的第二页重新映射到start位置
if (ret < 0) {
perror("remap file pages failed1");
munmap(start, LEN);
close(fd);
return 0;
}
ret = remap_file_pages(start + 4096, LEN >> 1, 0,
0, MAP_SHARED);//将文件的第一页重新映射到start+4096的位置。
if (ret < 0) {
perror("remap file pages failed2");
munmap(start, LEN);
close(fd);
return 0;
}
memcpy(str, start, 8);
printf("%s\n", str);
memcpy(str, start + 4096, 8);
printf("%s\n", str);
memcpy(str, start + 4096 - 4, 8);
printf("%s\n", str);
munmap(start, LEN);
close(fd);
return 0;
}
编译后执行效果如下,可以看出文件中的第一页和第二页确实被颠倒了,实现了非线性映射:
aaaaaaaa
bbbbbbbb
aaaabbbb
bbbbbbbb
aaaaaaaa
bbbbaaaa
下面讲讲在内核中这个系统调用的实现(在mm/fremap.c中),该系统调用的代码如下:。
SYSCALL_DEFINE5(remap_file_pages, unsigned long, start, unsigned long, size,
unsigned long, prot, unsigned long, pgoff, unsigned long, flags)
//SYSCALL_DEFINE5这是一个宏,表示该系统调用的参数有5个,系统调用服务函数的名字是sys_remap_file_pages,将该宏展开后的样子如下:
//long sys_remap_file_pages(unsigned long start, unsigned long size, unsigned long prot, unsigned long pgoff, unsigned long flags);
//参数的含义就不用解释了,和上面的基本一样。
{
struct mm_struct *mm = current->mm;
struct address_space *mapping;
unsigned long end = start + size;
struct vm_area_struct *vma;
int err = -EINVAL;
int has_write_lock = 0;
if (prot)//prot必须为0,否则出错:Invalid argument。
return err;
//将start和size按页大小对齐,因此传递这两个参数的时候最好是页大小的整数倍。
start = start & PAGE_MASK;
size = size & PAGE_MASK;
//一些检查,不涉及核心内容,省略。
/* We need down_write() to change vma->vm_flags. */
down_read(&mm->mmap_sem);
retry:
vma = find_vma(mm, start); //必须存在vma能够包含start地址,而且该vma的vm_flags中必须有VM_SHARED
if (!vma || !(vma->vm_flags & VM_SHARED))
goto out;
if (vma->vm_private_data && !(vma->vm_flags & VM_NONLINEAR))
goto out;
if (!(vma->vm_flags & VM_CAN_NONLINEAR))
goto out;
if (end <= start || start < vma->vm_start || end > vma->vm_end)//地址start必须在vma的映射范围内
goto out;
/* Must set VM_NONLINEAR before any pages are populated. */
if (!(vma->vm_flags & VM_NONLINEAR)) {//如果vma目前不是非线性映射,则要走这个分支。
/* Don't need a nonlinear mapping, exit success */
if (pgoff == linear_page_index(vma, start)) {//如果地址start在原来的映射中就是pgoff所映射的位置,就不需要做什么了
err = 0;
goto out;
}
if (!has_write_lock) {
up_read(&mm->mmap_sem);
down_write(&mm->mmap_sem);
has_write_lock = 1;
goto retry;
}
mapping = vma->vm_file->f_mapping;
/*
* page_mkclean doesn't work on nonlinear vmas, so if
* dirty pages need to be accounted, emulate with linear
* vmas.
*/
if (mapping_cap_account_dirty(mapping)) {//这个分支的条件没看懂,在我调试的时候,没走这个分支。
unsigned long addr;
struct file *file = vma->vm_file;
flags &= MAP_NONBLOCK;
get_file(file);
//我觉得如果走这个分支的话,会执行mmap_region函数,这个函数在前面介绍do_mmap的时候讲过了,但是在这里情况应该不一样了,首先start对应的地址已经被映射了,因此会使用do_munmap函数将该区域从原来的线性映射区域中分割出来,然后创建一个新的vma,用来映射pgoff对应的区域。这样就很像线性映射了。
addr = mmap_region(file, start, size,
flags, vma->vm_flags, pgoff);
fput(file);
if (IS_ERR_VALUE(addr)) {
err = addr;
} else {
BUG_ON(addr != start);
err = 0;
}
goto out;
}
spin_lock(&mapping->i_mmap_lock);
flush_dcache_mmap_lock(mapping);
vma->vm_flags |= VM_NONLINEAR;//设置非线性映射的标志,下次如果再有同一个vma区域中的非线性映射,就不会再走这个分支了,将会直接跳到下面去修改页表。
vma_prio_tree_remove(vma, &mapping->i_mmap);//将该vma从该文件的线性映射相关的数据结构(优先树)上删除
vma_nonlinear_insert(vma, &mapping->i_mmap_nonlinear);//将该vma插入到该文件的非线性映射链表中
flush_dcache_mmap_unlock(mapping);
spin_unlock(&mapping->i_mmap_lock);
}
if (vma->vm_flags & VM_LOCKED) {
/*
* drop PG_Mlocked flag for over-mapped range
*/
unsigned int saved_flags = vma->vm_flags;
munlock_vma_pages_range(vma, start, start + size);
vma->vm_flags = saved_flags;
}
mmu_notifier_invalidate_range_start(mm, start, start + size);
err = populate_range(mm, vma, start, size, pgoff);//修改页表,详情见下文
mmu_notifier_invalidate_range_end(mm, start, start + size);
if (!err && !(flags & MAP_NONBLOCK)) {
if (vma->vm_flags & VM_LOCKED) {
/*
* might be mapping previously unmapped range of file
*/
mlock_vma_pages_range(vma, start, start + size);
} else {
if (unlikely(has_write_lock)) {
downgrade_write(&mm->mmap_sem);
has_write_lock = 0;
}
make_pages_present(start, start+size);
}
}
/*
* We can't clear VM_NONLINEAR because we'd have to do
* it after ->populate completes, and that would prevent
* downgrading the lock. (Locks can't be upgraded).
*/
out:
if (likely(!has_write_lock))
up_read(&mm->mmap_sem);
else
up_write(&mm->mmap_sem);
return err;
}
修改页表的函数代码如下:
static int populate_range(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long addr, unsigned long size, pgoff_t pgoff)
{
int err;
do {//将虚拟地址范围(addr,addr+size)对应的页表修改,使其指向pgoff。
err = install_file_pte(mm, vma, addr, pgoff, vma->vm_page_prot);
if (err)
return err;
size -= PAGE_SIZE;
addr += PAGE_SIZE;
pgoff++;
} while (size);
return 0;
}
/*
* Install a file pte to a given virtual memory address, release any * previously existing mapping.
*/
static int install_file_pte(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long addr, unsigned long pgoff, pgprot_t prot)
{
int err = -ENOMEM;
pte_t *pte;
spinlock_t *ptl;
pte = get_locked_pte(mm, addr, &ptl);//获得原来的页表项。
if (!pte)
goto out;
if (!pte_none(*pte))
zap_pte(mm, vma, addr, pte);//删除原来的映射
//设置新的映射。由于非线性映射区域对应的页表项是一些特殊的填充项,这些页表项看起来像是对应不存在的页,但是其中所包含的附加信息将其标识为非线性映射页表项。访问此类页表项是会产生缺页异常,并读入正确的项。宏pgoff_to_pte用来将文件偏移量转换为非线性页表项。
set_pte_at(mm, addr, pte, pgoff_to_pte(pgoff));
/*
* We don't need to run update_mmu_cache() here because the "file pte"
* being installed by install_file_pte() is not a real pte - it's a
* non-present entry (like a swap entry), noting what file offset should
* be mapped there when there's a fault (in a non-linear vma where
* that's not obvious).
*/
pte_unmap_unlock(pte, ptl);
err = 0;
out:
return err;
}
好了,以上就是非线性映射的基本原理,说的不是很详细。可以看出,在一般情况下,非线性映射做的基本上就是在原来的线性映射中修改页表,已达到移动页的目的,这样会节省很多开支。
参考链接:http://man7.org/linux/man-pages/man2/remap_file_pages.2.html
————————————————
版权声明:本文为CSDN博主「CQ062364」的原创文章,遵循CC 4.0 BY-SA 版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/CQ062364/article/details/38499937