Linux 内存管理:内存映射

之前讲了那么多内存的东西,那么都离不开内存映射,不论虚拟地址到物理地址,还是用户空间地址到内核空间。关于映射用户空间最常用的是mmap来映射设备的io空间,直接访问,来提高io效率。内核的有ioremap映射设备io地址空间以供内核访问,kmap映射申请的高端内存,

还有DMA ,dma主要用的多的是网卡驱动里ring buffer机制.

下面就说说mmap:

函数原型:void* mmap ( void * start , size_t len , int prot , int flags , int fd , off_t offset )

参数说明:

  • start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址。
  • length:映射区的长度。//长度单位是 以字节为单位,不足一内存页按一内存页处理
  • prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起
  • 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_HUGETLB (since Linux 2.6.32)
  • Allocate the mapping using “huge pages.” See the kernel source file Documentation/vm/hugetlbpage.txt for further information.
  • fd:有效的文件描述词。一般是由open()函数返回,其值也可以设置为-1,此时需要指定flags参数中的MAP_ANON,表明进行的是匿名映射。
  • off_t offset:被映射对象内容的起点。文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍

返回值:

成功执行时,mmap()返回被映射区的指针,失败时,mmap()返回MAP_FAILED[其值为(void *)-1],

上边只是对mmap的基本参数做了说明,我们知道用户空间都是文件访问,即file_operations 中有函数指针mmap,那么调用的mmap 的时候一般需要传递fd。

在include/linux/fs.h:

通过上面的结构我们知道mmap系统调用最后调用文件操作指针函数mmap.
那么需要看一下mmap系统调用的实现:mm/mmap.c:

在说之前,需要补充一下知识,第一用户空间的内存布局,和结构体struct vm_area_struct

其实之前文章已经说过这个布局。
我们看include/linux/mm_types.h:

Struct vm_area_struct用红黑树来管理。不是和vmalloc里一些结构很相似?但是别搞混了.
内核中每一个这样的对象都表示用户进程地址空间的一段区域。
当linux 运行一个应用程序时,系统调用exec通过load_elf_binary函数把elf加载到用户虚拟空间。前面我们已经说了栈和堆。Text不用多解释。

那么基本流程就是:
1. 用户调用mmap系统调用
2. 内核在用户空间mmap区域分配一个空闲的vm_area_struct对象。
3. 然后修改页目录表项把对象的地址和设备的内存对应起来

那么在用户空间,mmap系统调用函数原型为:
Void *mmap(void *start,size_t length,int prot ,int flags,int fd, off_t offset);

它能够起作用的前提是打开的设备文件的驱动里实现了mmap。

看看mmap系统调用内核实现,
1.找到fd对应的struct file;
2 do_mmap_pgoff完成映射的工作。

细说do_mmap_pgoff函数
(1) 调用get_unmapped_area获得未使用的vm_area_struct
(2) 后续是mmap_region
(3) 调用到驱动file->mmap的具体实现
(4) 具体驱动层mmap的实现

在具体实现驱动层的mmap前,linux内核已经实现了页表映射的接口api供我们使用。

Remap_pfn_range (memory.c)也有其他延伸接口

Mmap是可以忽略fd参数的:MAP_ANONYMOUS建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享
参数fd:要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有
些系统不支持匿名内存映射,则可以使用fopen打开/dev/zero文件,然后对该文件进行映射,可以同样达到匿名内存映射的效果。
MAP_HUGETLB是内核2.6.32引入的一个mmap flags, 用于使用huge pages分配共享内存.

使用大页面的好处是在大内存的管理上减少CPU的开销。Linux对大页面内存的引入对减少TLB的失效效果不错,特别是内存大而密集型的程序,比如说在数据库中的使用

显然正常的mmap调用流程会走人第一个if语句获取file指针.

接着调用了:

获取互斥锁,调用do_mmap_pgoff

首先调用get_unmapped_area在用户内存空间map区里分配一个空闲区。 然后调用mmap_region具体的映射.
在mmap_region中:

这个函数有两个关键的地方,第一就是申请了vma并初始化,然后调用 file->f_op->mmap(file, vma);
这样整个流程就清晰了,驱动开发人员只需要关注设备驱动里file操作中mmap实现就可以了。

关于可执行文件的映射我们可以参考几个图:

那么对应每个进程都有个一个mm_struct:

在mm_struct中有
struct vm_area_struct * mmap; /* list of VMAs */

它保存了进程所有映射的区域,之前我们提到过每个vma(即结构vm_area_struct都代表用户空间的一个映射)。那么它在这里连接起来。

 

我们在mmap_region中看到这样一行代码:

vma_link(mm, vma, prev, rb_link, rb_parent); 即把申请的vma加入管理中.

这里需要说明库文件的map和设备驱动的映射不太一样,前者不要求物理地址连续,但是后者要求,因为设备io空间默认是连续的.

对于任何一个普通文件,对于的file *中的mmap操作是什么呢?

这个跟fs有关系:
.mmap=generic_file_mmap // filemap.c

我们也可以通过proc来查看:
#cat /proc/pid/maps

而查看静态的bin可以通过nm和objdump,Nm查看bin的符号,objdump可以查看elf信息,也可以通过file 和readelf查看

这里就说说mmap支持的功能:

1. mmap共享内存:

(1)使用普通文件提供的内存映射:
适用于任何进程之间。此时,需要打开或创建一个文件,然后再调用mmap()

典型调用代码如下:
fd=open(name, flag, mode); if(fd<0) …
ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0);
通过mmap()实现共享内存的通信方式有许多特点和要注意的地方,可以参看UNIX网络编程第二卷。

(2)使用特殊文件提供匿名内存映射:
适用于具有亲缘关系的进程之间。由于父子进程特殊的亲缘关系,在父进程中先调用mmap(),然后调用 fork()。那么在调用fork()之后,子进程继承父进程匿名映射后的地址空间,同样也继承mmap()返回的地址,这样,父子进程就可以通过映射区 域进行通信了。注意,这里不是一般的继承关系。一般来说,子进程单独维护从父进程继承下来的一些变量。而mmap()返回的地址,却由父子进程共同维护。 对于具有亲缘关系的进程实现共享内存最好的方式应该是采用匿名内存映射的方式。此时,不必指定具体的文件,只要设置相应的标志即可。

2. 提高文件访问效率

3. 映射设备

实现映射设备的函数mmap的时候,需要用到remap_pfn_range
remap_pfn_range不能映射常规内存,只存取保留页和在物理内存顶之上的物理地址。因为保留页和在物理
内存顶之上的物理地址内存管理系统的各个子模块管理不到。640 KB 和 1MB 是保留页可能映射,设备I/O
内存也可以映射。如果想把kmalloc()申请的内存映射到用户空间,则可以通过mem_map_reserve()把相应
的内存设置为保留后就可以。

remap_pfn_range常用于设备内存映射,而nopage()常用于RAM映射
调用mmap()时就决定了映射大小,不能再增加。换句话说,映射不能改变文件的大小。反过来,由文件被映射部分,而不是由文件大小来决定进程可访问内存空间范围(映射时,指定offset最好是内存页面大小的整数倍)。

通常使用mmap()的三种情况.提高I/O效率、匿名内存映射、共享内存进程通信。

在kernel里,通常有3种申请内存的方式:vmalloc, kmalloc, alloc_pages。kmalloc与alloc_pages类似,均是申请连续的地址空间。而vmalloc则可以申请一段不连续的物理地址空间,并将其映射到连续的线性地址上。每次vmalloc之后,内核会创建一个vm_struct,用以映射分配到的不连续的内存区域。vm_struct类似vma,但是又不是一回事。vma是将物理内存映射到进程的虚拟地址空间。而vm_struct是将物理内存映射到内核的线性地址空间。  既然vmalloc拿到的不是连续的物理内存,那么将这些内存映射到vma时,就不能直接利用remap_pfn_range()了。此时可以采用两种方法,一种是实现vm_operations_struct的fault()方法,用以在缺页时再映射需要的页。此方法操作起来较为麻烦。另一种方法是直接使用remap_vmalloc_range()函数。该函数的原型为:

int remap_vmalloc_range(struct vm_area_struct *vma, void *addr,

unsigned long pgoff)

其中参数vma是mmap使用调用传下来的,addr即为vmalloc()所分配内存的起始地址。而pgoff则为mmap()系统调用里的偏移参数,可以通过vma->vm_pgoff获得。该函数成功执行后,返回值为0。如果返回值为负数,则说明出错了。通常是由于所传的参数不正确。

需要注意的是,需要映射到用户空间的内存段,不能直接利用vmalloc()分配,而应该使用vmalloc_user()函数。该函数除了分配内存之外,还会将相应的vm_struct结构标记为VM_USERMAP。否则,remap_vmalloc_range将返回错误。

下面附上自己设备映射的测试代码(由于是测试只映射内核内存,用了两种方式一种是kmalloc 一种是vmalloc,而映射设备的时候直接传递设备io地址)

用户空间程序:

内核模块代码:

Makfile:
obj-m:=hello.o

编译:
make -C /usr/src/linux M=pwd modules // /usr/src/linux是内核路径或者内核头文件路径
安装 insmod hello.ko // 还需要自己查询设备号来创建设备文件.

1 5 收藏 1 评论

相关文章

可能感兴趣的话题



直接登录
最新评论
跳到底部
返回顶部