① 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指针。
② 预留一块16m的内存,在函数a中通过mmap映射预留的内存,并运行会导致溢出吗
mmap函数是unix/linux下的系统调用。
当存在客户-服务程序中复制文件时候,其数据流如下,要经历四次数据复制,开销很大。
-300x93.png
如果采用共享内存的方式,那么将大大优化IO操作,数据流变成了如下,数据只复制两次:
-300x108.png
映射文件或设备到内存中,取消映射就是munmap函数。
语法如下:
*mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);
int munmap(void *addr, size_t length);
该函数主要用途有三个:
l  将普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,用内存读写取代I/O读写,以获得较高的性能;
l  将特殊文件进行匿名内存映射,为关联进程提供共享内存空间;
l  为无关联的进程间的Posix共享内存(SystemV的共享内存操作是shmget/shmat)
我们来看下函数的入参选择:
ÃÂ 参数addr:
指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。
ÃÂ 参数length:
代表将文件中多大的部分映射到内存。
ÃÂ 参数prot:
映射区域的保护方式。可以为以下几种方式的组合:
PROT_EXEC 映射区域可被执行
PROT_READ 映射区域可被读取
PROT_WRITE 映射区域可被写入
PROT_NONE 映射区域不能存取
ÃÂ 参数flags:
影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此。
MAP_SHARED对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”( on write)对此区域作的任何修改都不会写回原来的文件内容。
MAP_ANONYMOUS建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。
MAP_DENYWRITE只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
MAP_LOCKED 将映射区域锁定住,这表示该区域不会被置换(swap)。
ÃÂ 参数fd:
要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。
ÃÂ 参数offset:
文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍。如下图内存映射文件的示例。
.png
1.1.1.1 共享映射
修改共享内存中的文件内容:
#include
#include
#include
#include
#include
#include
#include
int
main (int argc, char **argv)
{
int fd, nread, i;
struct stat sb;
char *mapped;
if ( argc <= 1 )
{
printf("%s: Need file path! \n",argv[0]);
exit(-1);
}
/* 打开文件 */
if ((fd = open (argv[1], O_RDWR)) < 0)
{
perror ("open");
}
/* 获取文件的属性 */
if ((fstat (fd, &sb)) == -1)
{
perror ("fstat");
}
/* 将文件映射至进程的地址空间 */
if ((mapped = (char *) mmap (NULL, sb.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)) == (void *) -1)
{
perror ("mmap");
}
/* 映射完后, 关闭文件也可以操纵内存 */
close (fd);
printf ("%s", mapped);
/* 修改一个字符,同步到磁盘文件 */
mapped[0] = '0';
if ((msync ((void *) mapped, sb.st_size, MS_SYNC)) == -1)
{
perror ("msync");
}
/* 释放存储映射区 */
if ((munmap ((void *) mapped, sb.st_size)) == -1)
{
perror ("munmap");
}
return 0;
}
编译后,即可执行,参数为当前目录的一个文件名字。
例如在当前目录下创建一个hello.txt,存入hello单词,然后执行./a.out hello.txt,完毕后发现文件中的第一个字母h变成了0。程序将文件映射到了内存中,并将第一个字符进行了修改并同步到了磁盘文件hello.txt中。
1.1.1.2 父子进程通信
使用fork创建进程,父子进程分别往共享内存中写入各自字符串,并读出。
#include
#include
#include
#include
#define BUF_SIZE 100
int
main (int argc, char **argv)
{
char *p_map;
/* 匿名映射,创建一块内存供父子进程通信 */
p_map = (char *) mmap (NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (fork () == 0)
{
sleep (1);
printf ("child got a message: %s\n", p_map);
sprintf (p_map, "%s", "from u son");
munmap (p_map, BUF_SIZE); //实际上,进程终止时,会自动解除映射。
exit (0);
}
sprintf (p_map, "%s", "from u father");
sleep (2);
printf ("parent got a message: %s\n", p_map);
return 0;
}
1.1.1.3 内存访问溢出
linux采用的是页式管理机制,使用mmap()映射普通文件后,进程会在自己的地址空间新增一块空间,空间大小由mmap()的len参数指定。但是,进程并不一定能够对全部新增空间都能进行有效访问。进程能够访问的有效地址大小取决于文件被映射部分的大小。决定进程能访问的大小是容纳文件被映射部分的最小页面数。如下图。
.png
#include
#include
#include
#include
#include
#include
int
main (int argc, char **argv)
{
int fd, i;
int pagesize, offset;
char *p_map;
struct stat sb;
/* 取得page size */
pagesize = sysconf (_SC_PAGESIZE);
printf ("pagesize is %d\n", pagesize);
/* 打开文件 */
fd = open (argv[1], O_RDWR, 00777);
fstat (fd, &sb);
printf ("file size is %zd\n", (size_t) sb.st_size);
offset = 0;
p_map = (char *) mmap (NULL, pagesize *2, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, offset);
close (fd);
// p_map[sb.st_size] = '9'; /* 导致总线错误 */
p_map[pagesize+1] = '9'; /* 导致段错误 */
munmap (p_map, pagesize * 2);
return 0;
③ 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完
④ linux共享内存和mmap的区别
共享内存允许两个或多个进程共享一给定的存储区,因为数据不需要来回复制,所内以是最快的一容种进程间通信机制。共享内存可以通过mmap()映射普通文件
(特殊情况下还可以采用匿名映射)机制实现,也可以通过系统V共享内存机制实现。应用接口和原理很简单,内部机制复杂。为了实现更安全通信,往往还与信号灯等同步机制共同使用。
mmap的机制如:就是在磁盘上建立一个文件,每个进程存储器里面,单独开辟一个空间来进行映射。如果多进程的话,那么不会对实际的物理存储器(主存)消耗太大。
shm的机制:每个进程的共享内存都直接映射到实际物理存储器里面。
1、mmap保存到实际硬盘,实际存储并没有反映到主存上。优点:储存量可以很大(多于主存);缺点:进程间读取和写入速度要比主存的要慢。
2、shm保存到物理存储器(主存),实际的储存量直接反映到主存上。优点,进程间访问速度(读写)比磁盘要快;缺点,储存量不能非常大(多于主存)
使用上看:如果分配的存储量不大,那么使用shm;如果存储量大,那么使用mmap。
⑤ 请教高手:如何在linux下用mmap映射超大文件
manpage里面的东西:来
void *mmap(void *start, size_t length, int prot, int flags,
int fd, off_t offset);
The mmap() function asks to map length bytes starting at offset offset from the file (or other object) specified by the file descriptor fd into memory,
就是源说,从offset位置开始,把文件fd的length字节映射到地址start上。
如果是64位的应用,4G是没有问题的,32位的应用不能。