深入理解Linux2.6内核中sys_mmap系统调用

在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文件系统的实现

ext2文件系统file的operations函数指针的实现

 

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挺轻松的。

最后,如果本帖对您有一定的帮助,希望能点赞+关注+收藏!您的支持是给我最大的动力,后续会一直更新各种框架的使用和框架的源码解读~!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员李哈

创作不易,希望能给与支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值