Linux内存管理架构之四(mmap内存映射机制)

目录

1.是什么

2. 映射类型

2.1 文件映射和匿名映射

2.2私有映射和共享映射

2.3 brk的实现

3.实例

3.1 实现文件映射

3.2实现进程共享

2.3 实现内核驱动和进程共享 

4.mmap的调用流程

5.反向映射

·匿名映射的反向映射:

文件映射的反向映射:

6. 相关问题

 7.参考


1.是什么

        mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。

        常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。说白了,mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同,数据不通的繁琐过程。因此mmap效率更高。对文件的读取操作跨过了页缓存,减少了数据的拷贝次数,用内存读写取代I/O读写,提高了文件读取效率。

        mmap在用户空间映射调用系统中作用很大。我们平时使用的malloc/free 在申请小块内存的时候使用的是提前初始化好的内存池,一旦要申请大块的内存还是会使用mmap去做映射。

Linux内存管理架构之四(mmap内存映射机制)_第1张图片

        mmap函数主要用途有三个(应用和内核/驱动交互,进程间交互,大规模数据传输/大文件读写)​​​​​​​

        ​​​​​​​        ​​​​​​​        ​​​​​​​        ​​​​​​​Linux内存管理架构之四(mmap内存映射机制)_第2张图片 

2. 映射类型

2.1 文件映射和匿名映射

  文件映射就是映射具体的文件到内存区处理,但是和read、write不同,他不能扩大文件的大小; 匿名映射的不是具体文件,而是一块普通内存,用完释放,类似于malloc。

2.2私有映射和共享映射

Linux内存管理架构之四(mmap内存映射机制)_第3张图片

如上,进程的空间内,各种不同快的映射一共分成四种: 

·文件私有: 每个进程对其读写都是私有的,但是不会写入到磁盘,而是在各自的内存空间,比如不同进程使用同样的代码段, 数据段,数据是进程私有的。

·文件共享,基于文件的进程间通信

·匿名私有,一块内存的私有,用于单个进程内部使用,malloc使用的brk就是这种映射方式

·匿名共享:基于内存的进程间通信

mmap区四种映射类型都支持。

2.3 brk的实现

Linux内存管理架构之四(mmap内存映射机制)_第4张图片

malloc分配使用的heap堆内存其实也是通过mmap来实现的一段匿名映射空间!

SYSCALL_DEFINE1(brk, unsigned long, brk)
{
	unsigned long rlim, retval;
	unsigned long newbrk, oldbrk;
	struct mm_struct *mm = current->mm;
	unsigned long min_brk;
	down_write(&mm->mmap_sem);
	min_brk = mm->start_brk;

	newbrk = PAGE_ALIGN(brk);   // 新的brk地址
	oldbrk = PAGE_ALIGN(mm->brk);   //线程原来的brk地址
	if (oldbrk == newbrk)
		goto set_brk;

	/* Always allow shrinking brk. 新的比原来的还小,说明需要释放掉一些区域 */
	if (brk <= mm->brk) {
		if (!do_munmap(mm, newbrk, oldbrk-newbrk))
			goto set_brk;
		goto out;
	}

	/* Check against existing mmap mappings. 查找已经映射过了的的mmap内存空间 */
	if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))
		goto out;

	/* Ok, looks good - let it rip. 
     * 必须重新映射 申请虚拟内存,然后更新brk的位置,
     * 但是不分配物理内存,要等到实际访问才会产生缺页异常来分配 */
	if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk)
		goto out;
set_brk:
	mm->brk = brk;
out:
	retval = mm->brk;
	up_write(&mm->mmap_sem);
	return retval;
}

3.实例

3.1 实现文件映射

#include
#include
#include
#include
#include
#include
#include

int main(int argc, char* argv[])
{
    int fd = open("map.txt",O_RDWR);
    if(fd == -1){
        printf("文件打开失败\n");
        return -1;
    }

    // 创建映射 大小1024字节 可读写  可共享
    char* buf = mmap(NULL,1024,PROT_WRITE|PROT_READ,MAP_SHARED,fd,0);
    if(buf == MAP_FAILED){
        printf("映射失败\n");
        return 0;
    }
    // 写入
    printf("输出:%s\n",buf);
    sprintf(buf,"aaa:%d",10);
    printf("输出:%s\n",buf);
    // munmap 写入内容同步到磁盘文件
    int ret = munmap(buf,1024);
    if(ret == -1)    printf("删除失败\n");
    
    close(fd);

    return 0;
}

3.2实现进程共享

注意这里的fork会返回两次,因此if else两个发呢只都会被执行到!其实两个分支是被父子进程分别执行的,具体原因可见详解fork()函数的两个返回值_是桃萌萌鸭~的博客-CSDN博客_fork函数返回值

#include
#include
#include
#include
#include
#include
#include
#include
#include

#define MAX_SIZE 1024

int main()
{
    int fd = open("map.txt",O_RDWR|O_CREAT|O_TRUNC,0664);
    if(fd == -1){
        printf("文件打开失败\n");
        return -1;
    }
    ftruncate(fd,MAX_SIZE);
    // 映射
    char *buf = mmap(NULL,MAX_SIZE,PROT_WRITE|PROT_READ,MAP_SHARED,fd,0);
    if(buf == MAP_FAILED){
        printf("映射失败\n");
        return -1;
    }
    // 映射完成 文件描述符可以关闭
    close(fd);
    
    // 创建子进程
    pid_t pid = fork();
    if(pid == 0)    // 子进程
    {
        printf("子进程开始写\n");
        int i = 0;
        char *p = buf;
        for(i; i<10; i++)
        {
            sprintf(p,"DAI %2d\n",i);
            p+=8;
        }
        sleep(2);
        printf("子进程查看父进程最后的修改:%s\n",p);
        return 0;   // 子进程退出
    }
    else if(pid > 0)     // 父进程
    {
        sleep(1);
        printf("父进程开始读\n");
        char name[10] = {0};
        int id = 0, i = 0;
        for(i; i< 10; i++)
        {
            sscanf(buf+i*8,"%s %2d",name,&id);
            printf("读取到的是: %s %2d\n",name,id);
        }
        sprintf(buf+i*8,"我读完了\n");
        wait(NULL); // 回收子进程
    }


    int ret = munmap(buf,MAX_SIZE);
    if(ret == -1){
        printf("取消文件映射失败\n");
        return -1;
    }
    
    return 0;
}

2.3 实现内核驱动和进程共享 

... ...

4.mmap的调用流程

系统调用SYSCALL_DEFINE6(mmap_pgoff) ----> do_mmap_pgoff(获取文件节点inode信息并把文件和地址进行关联) -----> get_unmapped_area(从用户映射去获取可以用来map的虚拟地址addr)----> get_area = current->mm->get_unmapped_area (优先使用文件系统自己定义的接口 其次使用进程定义的接口来获取空闲虚拟地址)---->

SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,
		unsigned long, prot, unsigned long, flags,
		unsigned long, fd, unsigned long, pgoff)
{
	struct file *file = NULL;
	unsigned long retval = -EBADF;

	if (!(flags & MAP_ANONYMOUS)) { // 非匿名页映射
		audit_mmap_fd(fd, flags);
		if (unlikely(flags & MAP_HUGETLB))
			return -EINVAL;
		file = fget(fd);			//根据文件描述符fd获取传入文件的控制结构体
		if (!file)
			goto out;
	} else if (flags & MAP_HUGETLB) {	// 大页映射
        ...........
	}

	flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE);

	down_write(¤t->mm->mmap_sem);
	retval = do_mmap_pgoff(file, addr, len, prot, flags, pgoff);
	up_write(¤t->mm->mmap_sem);


}

static unsigned long do_mmap_pgoff(struct file *file, unsigned long addr,
			unsigned long len, unsigned long prot,
			unsigned long flags, unsigned long pgoff)
{
	struct mm_struct * mm = current->mm;
	struct inode *inode;
	vm_flags_t vm_flags;
	int error;
	unsigned long reqprot = prot;

	
	/* Obtain the address to map to. we verify (or select) it and ensure
	 * that it represents a valid section of the address space.
     * 这个函数从 找到没有map过的空余虚拟地址 并且确保这块地址有效
	 */
	addr = get_unmapped_area(file, addr, len, pgoff, flags);
	if (addr & ~PAGE_MASK)
		return addr;

	/* Do simple checking here so the lower-level routines won't have
	 * to. we assume access permissions have been handled by the open
	 * of the memory object, so we don't do any here.
	 */
	vm_flags = calc_vm_prot_bits(prot) | calc_vm_flag_bits(flags) |
			mm->def_flags | VM_MAYREAD | VM_MAYWRITE | VM_MAYEXEC;

    // 获取文件的inode节点信息
	inode = file ? file->f_path.dentry->d_inode : NULL;

    /* ..........*/
    //为获取到的虚拟地址创建地址映射
	return mmap_region(file, addr, len, flags, vm_flags, pgoff);
}

unsigned long get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,		unsigned long pgoff, unsigned long flags)
{
	unsigned long (*get_area)(struct file *, unsigned long,
				  unsigned long, unsigned long, unsigned long);

	// 如果当前进程结构体定义了这个函数 且不是文件映射 则使用进程自己定义的接口
	get_area = current->mm->get_unmapped_area;
	if (file && file->f_op && file->f_op->get_unmapped_area)
		get_area = file->f_op->get_unmapped_area;//优先使用文件系统定义的获取函数
	addr = get_area(file, addr, len, pgoff, flags);
	if (IS_ERR_VALUE(addr))
		return addr;

	return arch_rebalance_pgtables(addr, len);
}

5.反向映射

        通过虚拟地址查找物理地址叫做正向映射;通过物理空间直接查找虚拟地址(VMA)叫做反向映射。应用场景:内存回收,页面迁移;碎片整理,CMA回收,巨型页。

·匿名映射的反向映射:

Linux内存管理架构之四(mmap内存映射机制)_第5张图片

        在进程分配VMA地址空间,产生缺页异常之后,会使用anon_vma结构体来管理映射在所有在物理页上的VMA。

文件映射的反向映射:

文件映射的反向映射和匿名映射同理,是通过address_space结构体来实现反向查找,它和进程无关,

Linux内存管理架构之四(mmap内存映射机制)_第6张图片

 

6. 相关问题

mmap几问:

 ·如果更改mem变量的地址, 释放的时候munmap, 传入mem还能成功吗?

答 : 不能.

·如果对mem越界操作会怎么样?

答 : 文件答大小对映区操作有影响. 虽然当文件原内容大于申请范围,但是越界数小于文件原内容大小,不会出现问题. 但是不建议这样做.尽量使文件和申请大小相同并且在申请的大小内操作.

·如果文件偏移量随便填写会怎么样?

答 : 创建失败 偏移量offset必须是4K的整数倍

·如果文件描述符先关闭,对mmap映射有没有影响?

答 : 没有影响.

·open的时候可以创建一个文件来创建映射区吗?

答 : 可以,但是不能使用大小为0的文件. 可以使用ftruncate设置大小.

·open文件选选择O_WRONLY, 可以吗?

答 : 不可以. 因为加载的时候需要读一次.

·当选择MAT_SHARED的时候,open选择O_RDONLY, prot可以选择PROT_READ | PROT_WRITE 吗?

答 : 不可以 映射区权限 <= open文件权限

·如果不判断返回值会怎样?

答 : 会死的很难看

务必判断返回值
 

 7.参考

mmap内存映射在应用和内核/驱动交互,进程间交互,大规模数据传输/大文件读写中的使用_王道泼的博客-CSDN博客

你可能感兴趣的:(Linux内核结构学习,linux,开发语言,系统架构)