在csdn逛了一圈发现并没有博主把sys_mmap讲的特别仔细,所以笔者觉得很有必要写一篇关于sys_mmap系统调用的文章。内核版本选自Linux2.6.0,CPU选自i386,32位机。
sys_mmap的作用
一言以蔽之,在虚拟内存指定的一段空间中开辟一段空间,此空间可以用来正常映射使用,也可以用来映射文件,所以如果是文件映射的话,文件数据就会映射在用户态空间。
sys_mmap源码讲解
流程图先放在这里。
用户态根据0x80中断向量走idt表映射到SYSTEM_CALL然后查系统调用表执行sys_mmap2最终陷入到内核态,这里的过程不过细讲直接看到sys_mmap2源码。
// addr 内核态想要的开始地址,实际会根据页对齐
// len 长度
// prot 拥有的操作
// flags 标志位
// fd 文件的下标
// pgoff 页全局表的偏移量
asmlinkage long sys_mmap2(unsigned long addr, unsigned long len,
unsigned long prot, unsigned long flags,
unsigned long fd, unsigned long pgoff)
{
return do_mmap2(addr, len, prot, flags, fd, pgoff);
}
do_mmap_pgoff代码量特别得多,笔者挑选比较重要的部分讲解。
因为mmap有2种选择,一种是正常映射,一种是文件映射。而在do_mmap2代码中就已经判断了是不是文件映射,如果是文件映射,就会通过fd找到对应file结构体。所以当我们看到在do_mmap_pgoff代码中看到有判断file结构体的if指令段都是在处理文件映射。
而我们需要理解一件事,不管是文件映射还是正常映射,对于mmap来说通用的部分就是会在虚拟地址中开辟一段空间,所以do_mmap_pgoff方法中会去映射一段虚拟地址抽象成一个vma_struct结构体来表示。具体逻辑在get_unmapped_area -> arch_get_unmapped_area方法中。
这里创建出一个vma后,重点看到do_mmap_pgoff方法中以下的代码中。
// 之前创建出vma
// 这里是vma的属性赋值。
vma->vm_mm = mm;
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_flags = vm_flags;
vma->vm_page_prot = protection_map[vm_flags & 0x0f];
// 这是公共部分,也就是除了文件映射,正常映射的vma->vm_ops是空的
// 而文件映射的vma->vm_ops在后续代码中会做设置回调操作。
vma->vm_ops = NULL;
vma->vm_pgoff = pgoff;
vma->vm_file = NULL;
vma->vm_private_data = NULL;
vma->vm_next = NULL;
INIT_LIST_HEAD(&vma->shared);
if (file) {
error = -EINVAL;
if (vm_flags & (VM_GROWSDOWN|VM_GROWSUP))
goto free_vma;
if (vm_flags & VM_DENYWRITE) {
error = deny_write_access(file);
if (error)
goto free_vma;
correct_wcount = 1;
}
vma->vm_file = file;
get_file(file);
// 函数指针,调用当前文件系统具体的实现。
error = file->f_op->mmap(file, vma);
if (error)
goto unmap_and_free_vma;
} else if (vm_flags & VM_SHARED) {
error = shmem_zero_setup(vma);
if (error)
goto free_vma;
}
对于当前mmap是文件映射来说,执行file->f_op->mmap(file, vma);所以看到ext2文件系统的实现
int generic_file_mmap(struct file * file, struct vm_area_struct * vma)
{
struct address_space *mapping = file->f_dentry->d_inode->i_mapping;
struct inode *inode = mapping->host;
if (!mapping->a_ops->readpage)
return -ENOEXEC;
update_atime(inode);
// 把当前开辟的vma中的operations的函数指针给实现
// 也就是说文件映射的mmap中开辟的vma对应的操作由文件系统实现
// 也就是说挂了钩子函数。
vma->vm_ops = &generic_file_vm_ops;
return 0;
}
static struct vm_operations_struct generic_file_vm_ops = {
// 缺页异常中回调函数
.nopage = filemap_nopage,
// 这个是vma文件映射大小改变回调的函数。
.populate = filemap_populate,
};
对于文件映射来说会给创建的vma挂上一个文件的回调梯子。而正常使用映射的vma的vm_ops是null,这点在后续的缺页异常中区分。
而我们要知道,mmap系统调用仅仅是在虚拟内存中创建一段空间的映射,所以以上代码就介绍完mmap系统调用了。
缺页异常的讲解
mmap仅仅是在虚拟内存中创建一片区域的映射,而用户态程序使用这片映射区域时,当MMU+OS去完成虚拟内存映射到物理内存的过程中,会发现当前虚拟内存并没有映射到物理页中。所以MMU就会给CPU发送一个PAGE FAULT(缺页异常),然后CPU查询idt表最终走到do_page_fault方法。
由于do_page_fault的代码量特别特别的大,还是老规矩看重点部分。但是看之前我们需要知道page fault的目的是什么,当虚拟地址映射到物理地址时,发现没有对应的物理页就会发生page fault,所以这个中断异常的目的是找到一个空闲的页,并且完成物理页跟虚拟地址的映射。
看到handler_mm_fault具体代码。
int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct * vma,
unsigned long address, int write_access)
{
pgd_t *pgd;
pmd_t *pmd;
__set_current_state(TASK_RUNNING);
// 找到全局页目录表
pgd = pgd_offset(mm, address);
inc_page_state(pgfault);
if (is_vm_hugetlb_page(vma))
return VM_FAULT_SIGBUS; /* mapping truncation does this. */
/*
* We need the page table lock to synchronize with kswapd
* and the SMP-safe atomic PTE updates.
*/
spin_lock(&mm->page_table_lock);
// 找到页中目录表
// 不存在页中目录就会去创建
pmd = pmd_alloc(mm, pgd, address);
// 通过页中目录表找到页表
if (pmd) {
// 通过页中目录找到页表。
// 如果页表不存在就会去创建
pte_t * pte = pte_alloc_map(mm, pmd, address);
if (pte)
// 根据页表来完成新创建的页的映射
return handle_pte_fault(mm, vma, address, write_access, pte, pmd);
}
spin_unlock(&mm->page_table_lock);
return VM_FAULT_OOM;
}
当缺页异常时,CPU会把错误码自动压栈,并且会把导致缺页异常的线性地址(高版本的内核中就可以理解为虚拟地址,因为没分段)给放入cr2寄存器中。而线性地址要参与到寻找页表的过程,因为把线性地址查分为几段,每一段表示不同页的具体偏移量(全局页、页中目录、页表、页帧)。然后再通过CR3寄存器找到页全局表的基址。
而上述代码通过CR2+CR3,从全局页表一层一层往下找,找到具体的页表描述符,再通过handler_pte_fault做具体的操作。
static inline int handle_pte_fault(struct mm_struct *mm,
struct vm_area_struct * vma, unsigned long address,
int write_access, pte_t *pte, pmd_t *pmd)
{
pte_t entry;
// 把地址转换为描述符数据。
entry = *pte;
// 这里能进来就代表当前的pte描述符的第1位和第8位为0
// 第1位为0也就是没映射。
if (!pte_present(entry)) {
/*
* If it truly wasn't present, we know that kswapd
* and the PTE updates will not touch it later. So
* drop the lock.
*/
// pte描述符都为0。
// 也就代表当前pte描述符没被页帧映射到。
if (pte_none(entry))
return do_no_page(mm, vma, address, write_access, pte, pmd);
if (pte_file(entry))
return do_file_page(mm, vma, address, write_access, pte, pmd);
return do_swap_page(mm, vma, address, pte, pmd, entry, write_access);
}
if (write_access) {
if (!pte_write(entry))
return do_wp_page(mm, vma, address, pte, pmd, entry);
entry = pte_mkdirty(entry);
}
entry = pte_mkyoung(entry);
establish_pte(vma, address, pte, entry);
pte_unmap(pte);
spin_unlock(&mm->page_table_lock);
return VM_FAULT_MINOR;
}
这里判断是不是没被映射过的pte页表项,没被映射就走到do_no_page。
static int
do_no_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, int write_access, pte_t *page_table, pmd_t *pmd)
{
struct page * new_page;
struct address_space *mapping = NULL;
pte_t entry;
struct pte_chain *pte_chain;
int sequence = 0;
int ret;
if (!vma->vm_ops || !vma->vm_ops->nopage)
return do_anonymous_page(mm, vma, page_table,
pmd, write_access, address);
pte_unmap(page_table);
spin_unlock(&mm->page_table_lock);
if (vma->vm_file) {
mapping = vma->vm_file->f_dentry->d_inode->i_mapping;
sequence = atomic_read(&mapping->truncate_count);
}
smp_rmb(); /* Prevent CPU from reordering lock-free ->nopage() */
retry:
new_page = vma->vm_ops->nopage(vma, address & PAGE_MASK, 0);
/* no page was available -- either SIGBUS or OOM */
if (new_page == NOPAGE_SIGBUS)
return VM_FAULT_SIGBUS;
if (new_page == NOPAGE_OOM)
return VM_FAULT_OOM;
pte_chain = pte_chain_alloc(GFP_KERNEL);
if (!pte_chain)
goto oom;
/*
* Should we do an early C-O-W break?
*/
if (write_access && !(vma->vm_flags & VM_SHARED)) {
struct page * page = alloc_page(GFP_HIGHUSER);
if (!page) {
page_cache_release(new_page);
goto oom;
}
copy_user_highpage(page, new_page, address);
page_cache_release(new_page);
lru_cache_add_active(page);
new_page = page;
}
spin_lock(&mm->page_table_lock);
/*
* For a file-backed vma, someone could have truncated or otherwise
* invalidated this page. If invalidate_mmap_range got called,
* retry getting the page.
*/
if (mapping &&
(unlikely(sequence != atomic_read(&mapping->truncate_count)))) {
sequence = atomic_read(&mapping->truncate_count);
spin_unlock(&mm->page_table_lock);
page_cache_release(new_page);
pte_chain_free(pte_chain);
goto retry;
}
page_table = pte_offset_map(pmd, address);
/*
* This silly early PAGE_DIRTY setting removes a race
* due to the bad i386 page protection. But it's valid
* for other architectures too.
*
* Note that if write_access is true, we either now have
* an exclusive copy of the page, or this is a shared mapping,
* so we can make it writable and dirty to avoid having to
* handle that later.
*/
/* Only go through if we didn't race with anybody else... */
if (pte_none(*page_table)) {
if (!PageReserved(new_page))
++mm->rss;
flush_icache_page(vma, new_page);
entry = mk_pte(new_page, vma->vm_page_prot);
if (write_access)
entry = pte_mkwrite(pte_mkdirty(entry));
set_pte(page_table, entry);
pte_chain = page_add_rmap(new_page, page_table, pte_chain);
pte_unmap(page_table);
} else {
/* One of our sibling threads was faster, back out. */
pte_unmap(page_table);
page_cache_release(new_page);
spin_unlock(&mm->page_table_lock);
ret = VM_FAULT_MINOR;
goto out;
}
/* no need to invalidate: a not-present page shouldn't be cached */
update_mmu_cache(vma, address, entry);
spin_unlock(&mm->page_table_lock);
ret = VM_FAULT_MAJOR;
goto out;
oom:
ret = VM_FAULT_OOM;
out:
pte_chain_free(pte_chain);
return ret;
}
if (!vma->vm_ops || !vma->vm_ops->nopage)
return do_anonymous_page(mm, vma, page_table,
pmd, write_access, address);看到这里,这是do_no_page最上层的一个if判断,回忆一下mmap系统调用中创建vma时候如果是文件就会走file->f_op->mmap(file, vma);,而此操作会把vma->vm_ops函数指针结构体给初始化。因为mmap非文件映射创建的vma->vm_ops = null,只有文件映射会赋值vm_ops。所以这里if也就是在判断当前创建页是文件映射还是普通正常映射。当这个if没过,下面的操作都是文件映射。
所以具体的创建页帧,页帧与页表项做映射的方法为:
正常映射:do_anonymous_page(mm, vma, page_table,pmd, write_access, address);
文件映射:new_page = vma->vm_ops->nopage(vma, address & PAGE_MASK, 0); 此函数指针对应的实现为filemap_nopage
总结
并不复杂,懂虚拟地址映射到物理地址的机制,分页的机制。并且懂中断机制其实看sys_mmap挺轻松的。
最后,如果本帖对您有一定的帮助,希望能点赞+关注+收藏!您的支持是给我最大的动力,后续会一直更新各种框架的使用和框架的源码解读~!