linux内存映射文件
1. linux内核中虚拟内存是怎样映射到物理内存
当程序在运行的时候,会检测到数据在虚拟内存中,并没在物理内存中,这时候会产生一个缺页中断,
有缺页中断来映射。
2. Linux关于地址空间和MMAP映射有何特点
Linux采用
虚拟
内存技术,系统中的所有进程之间以虚拟方式共享内存。对每个进程来说,它们好像都可以访问整个系统的所有物理内存。更重要的是,即使单独一个进程,它拥有的地址空间也可以远远大于系统物理内存。
进程地址空间由每个进程中的线性地址区组成,每个进程都有一个32位或64位的平坦(flat)空间,空间的具体大小取决于体系结构。“平坦”指地址空间范围是一个独立的连续区间。通常情况下,每个进程都有唯一的这种平坦空间,而且每个进程的地址空间之间彼此互不相干。两个不同的进程可以在它们各自地址空间的相同地址内存存放不同的数据。但是进程之间也可以选择共享地址空间,我们称这样的进程为线程。
在地址空间中,我们更为关心的是进程有权访问的虚拟内存地址区间,比如08048000~0804c000。这些可被访问的合法地址区间被成为内存区域(memory area),通过内核,进程可以给自己的地址空间动态地添加或减少内存区域。
进程只能访问有效范围内的内存地址。每个内存区域也具有相应进程必须遵循的特定访问属性,如只读、只写、可执行等属性。如果一个进程访问了不在有效范围中的地址,或以不正确的方式访问有效地址,那么内核就会终止该进程,并返回“段错误”信息。
?
内存区域可以包含各种内存对象,如下:
?
可执行文件代码的内存映射,成为代码段(text section)。
?
可执行文件的已初始化全局变量的内存映射,成为数据段(data section)。
?
包含未初始化全局变量的零页(也就是bss段)的内存映射。零页是指页面中的数据全部为0。
?
用于进程用户空间栈的零页的内存映射。
?
每一个诸如C库或动态链接程序等共享库的代码段、数据段和bss也会被载入进程的地址空间。
?
任何内存映射文件。
?
任何共享内存段。
?
任何匿名的内存映射,比如由malloc()分配的内存。
进程地址空间的任何有效地址都只能位于唯一的区域,这些内存区域不能相互覆盖。可以看到,在执行的进程中,每个不同的内存片断都对应一个独立的内存区域:栈、对象代码、全局变量、被映射的文件等等。
内核使用内存描述符表示进程的地址空间。内存描述符由mm_struct结构体表示,定义在文件中,该结构包含了和进程地址空间有关的全部信息。
VMA
内存区域由vm_area_struct结构体描述,定义在文件中,内存区域在内核中也经常被称作虚拟内存区域或者VMA。
VMA标志是一种位标志,它定义在vm_area_struct结构中(该结构中的vm_flags子域)。和物理页的访问权限不同,VMA标志反映了内核处理页面索需要遵守的行为准则,而不是硬件要求。VM_IO标志内存区域中包含对设备I/O空间的映射。该标志通常在设备驱动程序执行 mmap()函数进行I/O空间映射时才被设置,同时该标志也表示该内存区域不能被包含在任何进程的存放转存(core mp)中。VM_RESERVED标志内存区域不能被换出,它也是在设备驱动程序进行映射时被设置。
vm_area_struct结构体中的vm_ops域指向与指定内存区域相关的操作函数表,内核使用表中的方法操作VMA。
mmap()和do_mmap():创建地址区间
内核使用do_mmap()函数创建一个新的线性地址区间。但是说给函数创建一个新VMA并不非常准确,因为如果创建的地址区间和一个已经存在的地址区间相邻,并且它们具有相同的访问权限的话,那么两个区间将合并为一个。如果不能合并,那么就确实需要创建一个新的VMA了。但无论哪种情况,do_mmap()函数都会将一个地址区间加入到进程的地址空间中——无论是扩展已经存在的内存区域还是创建一个新的区域。
do_mmap()函数声明在文件中,原型如下:
unsigned long do_mmap(struct file *file, unsigned long addr,
unsigned long len, unsigned long prot,
unsigned long flag, unsigned long offset)
在用户空间可以通过mmap()函数调用获取内核函数do_mmap()的功能。mmap()系统调用原型如下:
void *mmap2(void *start, size_t length,
int prot, int flags,
int fd, off_t pgoff)
do_munmap()函数从特定的进程地址空间中删除指定地址区间,该函数在文件中声明:
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
系统调用munmap()给用户空间程序提供了一种从自身地址空间中删除指定地址区间的方法,它和系统调用mmap()的作用相反:
int munmap(void *start, size_t length)
mmap设备操作
对于驱动程序来说,内存映射可以提供给用户程序直接访问设备内存的能力。映射一个设备,意味着使用户空间的一段地址关联到设备内存上。无论何时,只要程序在分配的地址范围内进行读取或者写入,实际上就是对设备的访问。
并不是所有的设备都能进行mmap抽象。例如,串口设备和其他面向流的设备就无法实现这种抽象。mmap的另一个限制是映射都是以 PAGE_SIZE为单位的。内核只能在页表一级处理虚拟地址;因此,被映射的区域必须是PAGE_SIZE的整数倍,而且必须位于起始于 PAGE_SIZE整数倍地址的物理内存内。如果区域的大小不是页大小的整数倍,内核就通过生成一个稍微大一些的区域来容纳它。
mmap方法是file_operations结构中的一员,并且在执行mmap系统调用时就会调用该方法。在调用实际方法之前,内核会完成很多工作,而且该方法的原型与系统调用的原型由很大区别。关于Linux命令的介绍,看看《linux就该这么学》,具体关于这一章地址3w(dot)linuxprobe/chapter-02(dot)html
文件操作声明如下:
int (*mmap) (struct file * filp, struct vm_area_struct *vma);
其中vma参数包含了用于访问设备的虚拟地址区间的信息。大部分工作已经由内核完成了,要实现mmap,驱动程序只要为这一地址范围构造合适的页表即可,如果需要的话,就用一个新的操作集替换vma->vm_ops。
有两种建立页表的方法:使用remap_page_range函数可一次建立所有的页表,或者通过nopage VMA方法每次建立一个页表。
构造用于映射一段物理地址的新页表的工作是由remap_page_range完
3. 嵌入式 linux 的 内存映射 是个什么过程具体什么意思什么功能一定采纳
我的理解:
将A内存 隐射到B内存。
那么对对B的读写,将直接反馈到A上。
我所接触到的,Linux平台下,对设备寄存器的读写,用到内存映射。
4. linux中使用了什么内存管理方法,为什么
“事实胜于雄辩”,我们用一个小例子(原形取自《User-Level Memory Management》)来展示上面所讲的各种内存区的差别与位置。
进程的地址空间对应的描述结构是“内存描述符结构”,它表示进程的全部地址空间,——包含了和进程地址空间有关的全部信息,其中当然包含进程的内存区域。
进程内存的分配与回收
创建进程fork()、程序载入execve()、映射文件mmap()、动态内存分配malloc()/brk()等进程相关操作都需要分配内存给进程。不过这时进程申请和获得的还不是实际内存,而是虚拟内存,准确的说是“内存区域”。进程对内存区域的分配最终都会归结到do_mmap()函数上来(brk调用被单独以系统调用实现,不用do_mmap()),
内核使用do_mmap()函数创建一个新的线性地址区间。但是说该函数创建了一个新VMA并不非常准确,因为如果创建的地址区间和一个已经存在的地址区间相邻,并且它们具有相同的访问权限的话,那么两个区间将合并为一个。如果不能合并,那么就确实需要创建一个新的VMA了。但无论哪种情况,do_mmap()函数都会将一个地址区间加入到进程的地址空间中--无论是扩展已存在的内存区域还是创建一个新的区域。
同样,释放一个内存区域应使用函数do_ummap(),它会销毁对应的内存区域。
如何由虚变实!
从上面已经看到进程所能直接操作的地址都为虚拟地址。当进程需要内存时,从内核获得的仅仅是虚拟的内存区域,而不是实际的物理地址,进程并没有获得物理内存(物理页面——页的概念请大家参考硬件基础一章),获得的仅仅是对一个新的线性地址区间的使用权。实际的物理内存只有当进程真的去访问新获取的虚拟地址时,才会由“请求页机制”产生“缺页”异常,从而进入分配实际页面的例程。
该异常是虚拟内存机制赖以存在的基本保证——它会告诉内核去真正为进程分配物理页,并建立对应的页表,这之后虚拟地址才实实在在地映射到了系统的物理内存上。(当然,如果页被换出到磁盘,也会产生缺页异常,不过这时不用再建立页表了)
这种请求页机制把页面的分配推迟到不能再推迟为止,并不急于把所有的事情都一次做完(这种思想有点像设计模式中的代理模式(proxy))。之所以能这么做是利用了内存访问的“局部性原理”,请求页带来的好处是节约了空闲内存,提高了系统的吞吐率。要想更清楚地了解请求页机制,可以看看《深入理解linux内核》一书。
这里我们需要说明在内存区域结构上的nopage操作。当访问的进程虚拟内存并未真正分配页面时,该操作便被调用来分配实际的物理页,并为该页建立页表项。在最后的例子中我们会演示如何使用该方法。
系统物理内存管理
虽然应用程序操作的对象是映射到物理内存之上的虚拟内存,但是处理器直接操作的却是物理内存。所以当应用程序访问一个虚拟地址时,首先必须将虚拟地址转化成物理地址,然后处理器才能解析地址访问请求。地址的转换工作需要通过查询页表才能完成,概括地讲,地址转换需要将虚拟地址分段,使每段虚地址都作为一个索引指向页表,而页表项则指向下一级别的页表或者指向最终的物理页面。
每个进程都有自己的页表。进程描述符的pgd域指向的就是进程的页全局目录。下面我们借用《linux设备驱动程序》中的一幅图大致看看进程地址空间到物理页之间的转换关系。
上面的过程说起来简单,做起来难呀。因为在虚拟地址映射到页之前必须先分配物理页——也就是说必须先从内核中获取空闲页,并建立页表。下面我们介绍一下内核管理物理内存的机制。
物理内存管理(页管理)
Linux内核管理物理内存是通过分页机制实现的,它将整个内存划分成无数个4k(在i386体系结构中)大小的页,从而分配和回收内存的基本单位便是内存页了。利用分页管理有助于灵活分配内存地址,因为分配时不必要求必须有大块的连续内存[3],系统可以东一页、西一页的凑出所需要的内存供进程使用。虽然如此,但是实际上系统使用内存时还是倾向于分配连续的内存块,因为分配连续内存时,页表不需要更改,因此能降低TLB的刷新率(频繁刷新会在很大程度上降低访问速度)。
鉴于上述需求,内核分配物理页面时为了尽量减少不连续情况,采用了“伙伴”关系来管理空闲页面。伙伴关系分配算法大家应该不陌生——几乎所有操作系统方面的书都会提到,我们不去详细说它了,如果不明白可以参看有关资料。这里只需要大家明白Linux中空闲页面的组织和管理利用了伙伴关系,因此空闲页面分配时也需要遵循伙伴关系,最小单位只能是2的幂倍页面大小。内核中分配空闲页面的基本函数是get_free_page/get_free_pages,它们或是分配单页或是分配指定的页面(2、4、8…512页)。
注意:get_free_page是在内核中分配内存,不同于malloc在用户空间中分配,malloc利用堆动态分配,实际上是调用brk()系统调用,该调用的作用是扩大或缩小进程堆空间(它会修改进程的brk域)。如果现有的内存区域不够容纳堆空间,则会以页面大小的倍数为单位,扩张或收缩对应的内存区域,但brk值并非以页面大小为倍数修改,而是按实际请求修改。因此Malloc在用户空间分配内存可以以字节为单位分配,但内核在内部仍然会是以页为单位分配的。
另外,需要提及的是,物理页在系统中由页结构structpage描述,系统中所有的页面都存储在数组mem_map[]中,可以通过该数组找到系统中的每一页(空闲或非空闲)。而其中的空闲页面则可由上述提到的以伙伴关系组织的空闲页链表(free_area[MAX_ORDER])来索引。
内核内存使用
Slab
所谓尺有所长,寸有所短。以页为最小单位分配内存对于内核管理系统中的物理内存来说的确比较方便,但内核自身最常使用的内存却往往是很小(远远小于一页)的内存块——比如存放文件描述符、进程描述符、虚拟内存区域描述符等行为所需的内存都不足一页。这些用来存放描述符的内存相比页面而言,就好比是面包屑与面包。一个整页中可以聚集多个这些小块内存;而且这些小块内存块也和面包屑一样频繁地生成/销毁。
为了满足内核对这种小内存块的需要,Linux系统采用了一种被称为slab分配器的技术。Slab分配器的实现相当复杂,但原理不难,其核心思想就是“存储池[4]”的运用。内存片段(小块内存)被看作对象,当被使用完后,并不直接释放而是被缓存到“存储池”里,留做下次使用,这无疑避免了频繁创建与销毁对象所带来的额外负载。
Slab技术不但避免了内存内部分片(下文将解释)带来的不便(引入Slab分配器的主要目的是为了减少对伙伴系统分配算法的调用次数——频繁分配和回收必然会导致内存碎片——难以找到大块连续的可用内存),而且可以很好地利用硬件缓存提高访问速度。
Slab并非是脱离伙伴关系而独立存在的一种内存分配方式,slab仍然是建立在页面基础之上,换句话说,Slab将页面(来自于伙伴关系管理的空闲页面链表)撕碎成众多小内存块以供分配,slab中的对象分配和销毁使用kmem_cache_alloc与kmem_cache_free。
Kmalloc
Slab分配器不仅仅只用来存放内核专用的结构体,它还被用来处理内核对小块内存的请求。当然鉴于Slab分配器的特点,一般来说内核程序中对小于一页的小块内存的请求才通过Slab分配器提供的接口Kmalloc来完成(虽然它可分配32到131072字节的内存)。从内核内存分配的角度来讲,kmalloc可被看成是get_free_page(s)的一个有效补充,内存分配粒度更灵活了。
有兴趣的话,可以到/proc/slabinfo中找到内核执行现场使用的各种slab信息统计,其中你会看到系统中所有slab的使用信息。从信息中可以看到系统中除了专用结构体使用的slab外,还存在大量为Kmalloc而准备的Slab(其中有些为dma准备的)。
内核非连续内存分配(Vmalloc)
伙伴关系也好、slab技术也好,从内存管理理论角度而言目的基本是一致的,它们都是为了防止“分片”,不过分片又分为外部分片和内部分片之说,所谓内部分片是说系统为了满足一小段内存区(连续)的需要,不得不分配了一大区域连续内存给它,从而造成了空间浪费;外部分片是指系统虽有足够的内存,但却是分散的碎片,无法满足对大块“连续内存”的需求。无论何种分片都是系统有效利用内存的障碍。slab分配器使得一个页面内包含的众多小块内存可独立被分配使用,避免了内部分片,节约了空闲内存。伙伴关系把内存块按大小分组管理,一定程度上减轻了外部分片的危害,因为页框分配不在盲目,而是按照大小依次有序进行,不过伙伴关系只是减轻了外部分片,但并未彻底消除。你自己比划一下多次分配页面后,空闲内存的剩余情况吧。
所以避免外部分片的最终思路还是落到了如何利用不连续的内存块组合成“看起来很大的内存块”——这里的情况很类似于用户空间分配虚拟内存,内存逻辑上连续,其实映射到并不一定连续的物理内存上。Linux内核借用了这个技术,允许内核程序在内核地址空间中分配虚拟地址,同样也利用页表(内核页表)将虚拟地址映射到分散的内存页上。以此完美地解决了内核内存使用中的外部分片问题。内核提供vmalloc函数分配内核虚拟内存,该函数不同于kmalloc,它可以分配较Kmalloc大得多的内存空间(可远大于128K,但必须是页大小的倍数),但相比Kmalloc来说,Vmalloc需要对内核虚拟地址进行重映射,必须更新内核页表,因此分配效率上要低一些(用空间换时间)
与用户进程相似,内核也有一个名为init_mm的mm_strcut结构来描述内核地址空间,其中页表项pdg=swapper_pg_dir包含了系统内核空间(3G-4G)的映射关系。因此vmalloc分配内核虚拟地址必须更新内核页表,而kmalloc或get_free_page由于分配的连续内存,所以不需要更新内核页表。
vmalloc分配的内核虚拟内存与kmalloc/get_free_page分配的内核虚拟内存位于不同的区间,不会重叠。因为内核虚拟空间被分区管理,各司其职。进程空间地址分布从0到3G(其实是到PAGE_OFFSET,在0x86中它等于0xC0000000),从3G到vmalloc_start这段地址是物理内存映射区域(该区域中包含了内核镜像、物理页面表mem_map等等)比如我使用的系统内存是64M(可以用free看到),那么(3G——3G+64M)这片内存就应该映射到物理内存,而vmalloc_start位置应在3G+64M附近(说"附近"因为是在物理内存映射区与vmalloc_start期间还会存在一个8M大小的gap来防止跃界),vmalloc_end的位置接近4G(说"接近"是因为最后位置系统会保留一片128k大小的区域用于专用页面映射,还有可能会有高端内存映射区,这些都是细节,这里我们不做纠缠)。
上图是内存分布的模糊轮廓
由get_free_page或Kmalloc函数所分配的连续内存都陷于物理映射区域,所以它们返回的内核虚拟地址和实际物理地址仅仅是相差一个偏移量(PAGE_OFFSET),你可以很方便的将其转化为物理内存地址,同时内核也提供了virt_to_phys()函数将内核虚拟空间中的物理映射区地址转化为物理地址。要知道,物理内存映射区中的地址与内核页表是有序对应的,系统中的每个物理页面都可以找到它对应的内核虚拟地址(在物理内存映射区中的)。
而vmalloc分配的地址则限于vmalloc_start与vmalloc_end之间。每一块vmalloc分配的内核虚拟内存都对应一个vm_struct结构体(可别和vm_area_struct搞混,那可是进程虚拟内存区域的结构),不同的内核虚拟地址被4k大小的空闲区间隔,以防止越界——见下图)。与进程虚拟地址的特性一样,这些虚拟地址与物理内存没有简单的位移关系,必须通过内核页表才可转换为物理地址或物理页。它们有可能尚未被映射,在发生缺页时才真正分配物理页面。
这里给出一个小程序帮助大家认清上面几种分配函数所对应的区域。
#include<linux/mole.h>
#include<linux/slab.h>
#include<linux/vmalloc.h>
unsignedchar*pagemem;
unsignedchar*kmallocmem;
unsignedchar*vmallocmem;
intinit_mole(void)
{
pagemem = get_free_page(0);
printk("<1>pagemem=%s",pagemem);
kmallocmem = kmalloc(100,0);
printk("<1>kmallocmem=%s",kmallocmem);
vmallocmem = vmalloc(1000000);
printk("<1>vmallocmem=%s",vmallocmem);
}
voidcleanup_mole(void)
{
free_page(pagemem);
kfree(kmallocmem);
vfree(vmallocmem);
}
实例
内存映射(mmap)是Linux操作系统的一个很大特色,它可以将系统内存映射到一个文件(设备)上,以便可以通过访问文件内容来达到访问内存的目的。这样做的最大好处是提高了内存访问速度,并且可以利用文件系统的接口编程(设备在Linux中作为特殊文件处理)访问内存,降低了开发难度。许多设备驱动程序便是利用内存映射功能将用户空间的一段地址关联到设备内存上,无论何时,只要内存在分配的地址范围内进行读写,实际上就是对设备内存的访问。同时对设备文件的访问也等同于对内存区域的访问,也就是说,通过文件操作接口可以访问内存。Linux中的X服务器就是一个利用内存映射达到直接高速访问视频卡内存的例子。
熟悉文件操作的朋友一定会知道file_operations结构中有mmap方法,在用户执行mmap系统调用时,便会调用该方法来通过文件访问内存——不过在调用文件系统mmap方法前,内核还需要处理分配内存区域(vma_struct)、建立页表等工作。对于具体映射细节不作介绍了,需要强调的是,建立页表可以采用remap_page_range方法一次建立起所有映射区的页表,或利用vma_struct的nopage方法在缺页时现场一页一页的建立页表。第一种方法相比第二种方法简单方便、速度快,但是灵活性不高。一次调用所有页表便定型了,不适用于那些需要现场建立页表的场合——比如映射区需要扩展或下面我们例子中的情况。
我们这里的实例希望利用内存映射,将系统内核中的一部分虚拟内存映射到用户空间,以供应用程序读取——你可利用它进行内核空间到用户空间的大规模信息传输。因此我们将试图写一个虚拟字符设备驱动程序,通过它将系统内核空间映射到用户空间——将内核虚拟内存映射到用户虚拟地址。从上一节已经看到Linux内核空间中包含两种虚拟地址:一种是物理和逻辑都连续的物理内存映射虚拟地址;另一种是逻辑连续但非物理连续的vmalloc分配的内存虚拟地址。我们的例子程序将演示把vmalloc分配的内核虚拟地址映射到用户地址空间的全过程。
程序里主要应解决两个问题:
第一是如何将vmalloc分配的内核虚拟内存正确地转化成物理地址?
因为内存映射先要获得被映射的物理地址,然后才能将其映射到要求的用户虚拟地址上。我们已经看到内核物理内存映射区域中的地址可以被内核函数virt_to_phys转换成实际的物理内存地址,但对于vmalloc分配的内核虚拟地址无法直接转化成物理地址,所以我们必须对这部分虚拟内存格外“照顾”——先将其转化成内核物理内存映射区域中的地址,然后在用virt_to_phys变为物理地址。
转化工作需要进行如下步骤:
找到vmalloc虚拟内存对应的页表,并寻找到对应的页表项。
获取页表项对应的页面指针
通过页面得到对应的内核物理内存映射区域地址。
如下图所示:
第二是当访问vmalloc分配区时,如果发现虚拟内存尚未被映射到物理页,则需要处理“缺页异常”。因此需要我们实现内存区域中的nopaga操作,以能返回被映射的物理页面指针,在我们的实例中就是返回上面过程中的内核物理内存映射区域中的地址。由于vmalloc分配的虚拟地址与物理地址的对应关系并非分配时就可确定,必须在缺页现场建立页表,因此这里不能使用remap_page_range方法,只能用vma的nopage方法一页一页的建立。
程序组成
map_driver.c,它是以模块形式加载的虚拟字符驱动程序。该驱动负责将一定长的内核虚拟地址(vmalloc分配的)映射到设备文件上。其中主要的函数有——vaddress_to_kaddress()负责对vmalloc分配的地址进行页表解析,以找到对应的内核物理映射地址(kmalloc分配的地址);map_nopage()负责在进程访问一个当前并不存在的VMA页时,寻找该地址对应的物理页,并返回该页的指针。
test.c它利用上述驱动模块对应的设备文件在用户空间读取读取内核内存。结果可以看到内核虚拟地址的内容(ok!),被显示在了屏幕上。
执行步骤
编译map_driver.c为map_driver.o模块,具体参数见Makefile
加载模块:insmodmap_driver.o
生成对应的设备文件
1在/proc/devices下找到map_driver对应的设备命和设备号:grepmapdrv/proc/devices
2建立设备文件mknodmapfilec 254 0(在我的系统里设备号为254)
利用maptest读取mapfile文件,将取自内核的信息打印到屏幕上。
5. Linux I/O内存静态映射
在将Linux移植到目标电路板的过程中,有得会建立外设IO内存物理地址到虚拟地址的静态映射,这个映射通过在与电路板对应的map_desc结构体数组中添加新的成员来完成。iotable_init()是最终建立页映射的函数,它通过ACHINE_START、MACHINE_END宏赋值给电路板的map_io())函数。将Linux操作系统移植到特定平台上,MACHINE_START(或者DT_MACHINE_START)、MACHINE_END宏之间的定义针对特定电路板而设计,其中的map_io ()成员函数完成IO内存的静态映射。在一个已经移植好操作系统的内核中,驱动工程师可以对非常规内存区域的IO内存(外设控制器寄存器、MCU内部集成的外设控制器寄存器等)依照电路板的资源使用情况添加到map_desc数组中,但是目前该方法已经不值得推荐。
Cache和DMA本身似乎是两个毫不相关的事物。Cache被用作CPU针对内存的缓存,利用程序的空间局部性和时间局部性原理,达到较高的命中率,从而避免CPU每次都必须要与相对慢速的内存交互数据来提高数据的访问速率。DMA可以作为内存与外设之间传输数据的方式,在这种传输方式之下,数据并不需要经过CPU中转。
假设DMA针对内存的目的地址与Cache缓存的对象没有重叠区域,DMA和Cache之间将相安无事。但是,如果DMA的目的地址与Cache所缓存的内存地址访问有重叠,经过DMA操作,与Cache缓存对应的内存中的数据已经被修改,而CPU本身并不知道,它仍然认为Cache中的数据就是内存中的数据,那在以后访问Cache映射的内存时,它仍然使用陈旧的Cache数据。这样就会发生Cache与内存之间数据“不一致性”的错误。
6. LINUX内存映射问题
Linux的内存模型,一般为:
地址
作用
说明
>=0xc000 0000
内核虚拟存储器
用户代码不可见区域
<0xc000 0000
Stack(用户栈)
ESP指向栈顶
↓
↑
空闲内存
>=0x4000 0000
文件映射区
<0x4000 0000
↑
空闲内存
Heap(运行时堆)
通过brk/sbrk系统调用扩大堆,向上增长。
.data、.bss(读写段)
从可执行文件中加载
>=0x0804 8000(0x00008000 for arm linux)
.init、.text、.rodata(只读段)
从可执行文件中加载
<0x0804 8000(0x00008000 for arm linux)
保留区域
运行一个测试程序,观察其结果:
#include <stdio.h>
int main(int argc, char* argv[])
{
int first = 0;
int* p0 = malloc(1024);
int* p1 = malloc(1024 * 1024);
int* p2 = malloc(512 * 1024 * 1024 );
int* p3 = malloc(1024 * 1024 * 1024 );
printf("main=%p print=%p\n", main, printf);
printf("first=%p\n", &first);
printf("p0=%p p1=%p p2=%p p3=%p\n", p0, p1, p2, p3);
getchar();
return 0;
}
运行后,输出结果为:
main=0x8048404 print=0x8048324
first=0xbfcd1264
p0=0x9253008 p1=0xb7ec0008 p2=0x97ebf008 p3=0x57ebe008
my pc (fc5)输出结果如下:
main=0x80483f4 print=0x8048324
first=0xbf848660
p0=0x9ab2008 p1=0xb7e38008 p2=0x97e37008 p3=(nil)
arm-linux输出如下结果:
main=0x8528 print=0x8404
first=0xbec9fe10
p0=0x11008 p1=0x4005a008 p2=(nil) p3=(nil)
main和print两个函数是代码段(.text)的,其地址符合表一的描述。
first是第一个临时变量,由于在first之前还有一些环境变量,它的值并非0xbfffffff,而是0xbfcd1264,这是正常的。
p0是在堆中分配的,其地址小于0x4000 0000,这也是正常的。
但p1和p2也是在堆中分配的,而其地址竟大于0x4000 0000,与表一描述不符。
原因在于:运行时堆的位置与内存管理算法相关,也就是与malloc的实现相关。关于内存管理算法的问题,我们在后继文章中有详细描述,这里只作简要说明。在glibc实现的内存管理算法中,Malloc小块内存是在小于0x4000 0000的内存中分配的,通过brk/sbrk不断向上扩展,而分配大块内存,malloc直接通过系统调用mmap实现,分配得到的地址在文件映射区,所以其地址大于0x4000 0000。
7. Linux将设备地址映射到用户空间内存映射与VMA
一般情况下,用户空间是不可能也不应该直接访问设备的,但是,设备驱动程序中可实现mmap ()函数,这个函数可使得用户空间能直接访问设备的物理地址。实际上,mmap ()实现了这样的一个映射过程:它将用户空间的一段内存与设备内存关联,当用户访问用户空间的这段地址范围时,实际上会转化为对设备的访问。
这种能力对于显示适配器一类的设备非常有意义,如果用户空间可直接通过内存映射访问显存的话,屏幕帧的各点像素将不再需要一个从用户空间到内核空间的复制的过程。
mmap ()必须以PAGE_SIZE为单位进行映射,实际上,内存只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行页对齐,强行以PAGE_SIZE的倍数大小进行映射。
从file_operations文件操作结构体可以看出,驱动中mmap ()函数的原型如下:
int ( *mmap)(struct file *, struct vm_area_struct* ) ;
驱动中的mmap () 函数将在用户进行mmap ()系统调用时最终被调用,mmap ()系统调用的原型与file_operations中mmap ()的原型区别很大,如下所示:
caddr_t mmap (caddr_t addr,size_t len,int prot,int flags,int fd,off_t offset);
参数fd为文件描述符,一般由open ()返回,fd也可以指定为-1,此时需指定flags参数中的MAP_ANON,表明进行的是匿名映射。
len是映射到调用用户空间的字节数,它从被映射文件开头offset个字节开始算起,offset参数一般设为0,表示从文件头开始映射。
prot参数指定访问权限,可取如下几个值的“或”:PROT_READ(可读)、PROT_WRITE(可写)、PROT_EXEC(可执行)和PROT_NONE(不可访问)。
参数addr指定文件应被映射到用户空间的起始地址,一般被指定为NULL,这样,选择起始地址的任务将由内核完成,而函数的返回值就是映射到用户空间的地址。其类型caddr_t实际上就是void*。
当用户调用mmap ())的时候,内核会进行如下处理。
1)在进程的虚拟空间查找一块VMA。
2)将这块VMA进行映射。
3)如果设备驱动程序或者文件系统的file_operations定义了mmap ()操作,则调用它。
4)将这个VMA插入进程的VMA链表中。
file_operations中mmap ()函数的第一个参数就是步骤1)找到的VMA。
由mmap ()系统调用映射的内存可由munmap ()解除映射,这个函数的原型如下:
int munmap(caddr_t addr, size_t len ) ;
驱动程序中mmap ()的实现机制是建立页表,并填充VMA结构体中vm_operations_struct指针。
8. 一个文本文件用内存映射打开后,如何一行一行地进行读取 Linux下C语言
//mbuf为文件的内存映射地址,buf为你要保存字符串的地址。
char *readline(const char *mbuf, char *buf, int maxlen)
{
int len=0;
while(len<maxlen-1 && *(mbuf+len)!='\n')
{
*(buf+len)=*(mbuf+len);
len++;
}
*(buf+len)='\0';
return buf;
}
9. 关于linux系统下的物理内存映射
内存是一种存储设备,现在一般是ddr
sdram,地址是用来标记内存的数据的。在操作系统中物理内存指实际的ddr
sdram,而虚拟内存指的是在硬盘中的缓存,windows中是页面文件,linux中是swap分区。cpu产生的地址是虚拟地址也可以称作有效地址,而在cpu外地址线上的信号称作实际地址或物理地址。这2类地址有某种对应关系,由操作系统管理。如果是x86架构的话,物理地址和虚拟地址中间还有线性地址的概念。