linux操作系统采用虚拟内存管理技术,把内存空间分为用户空间和内核空间,用户空间由用户进程使用,用户进程无法直接访问内核空间,只能通过系统调用(软中断)或者硬中断间接访问。对于32位linux系统来说,系统物理内存最大寻址范围是2^32=4GB
,用户空间分配的大小是3GB,地址范围是0x0——0xbfffffff;内核空间是1GB,地址范围是0xc0000000——0xffffffff。
对于用户来说,直接访问到的是虚拟内存空间,这种虚拟地址的方式是现代操作系统中几乎都应用的,这样的好处是不言而喻的,不仅通过内存地址转换解决了多个进程访问内存冲突的问题,还提高系统的安全性。
内存完整性,每个进程占用的虚拟内存是比实际物理内存要大(比如32位linux是3G),由于虚拟内存对进程的”欺骗”,每个进程都认为自己获取的内存是一块连续的地址,这样在编写应用程序时无需担心大块内存分配失败问题。
安全性:进程访问内存时,通过通过页表来寻址,对于页表访问由操作系统管理各类访问权限,实现内存的权限控制。
内存mmap,利用虚拟内存,可以将文件、设备物理地址映射到用户地址空间,用户进程可以直接访问,省去了用户空间和内核空间之间内存拷贝过程。
内存swap,linux 系统引入swap分区,在可用物理内存不足时,将暂时不用的内存数据拷贝到存储介质(磁盘)上,让出内存让出给优先进程使用,进程使用完,再将原数据从磁盘加载到内存中。通过这种swap技术,linux系统可以让进程有“无限”内存可以用。
mmap就是将文件映射到进程虚拟内存空间,用户程序操作这段内存空间达到读写文件的目的,而且提高访问效率。linux思想是一切接文件,因此系统的驱动设备的物理空间同样可以映射到用户内存空间。
无论是普通文件还是设备文件吗,都是基于系统的虚拟文件系统接口,普通文件为了保护磁盘,避免频繁读写,还引入带缓冲页机制,通过read/write/ioctl
访问文件时,都需经历“用户到内核”的内存拷贝过程,然后才将文件内容写入磁盘。如图,单个进程访问文件时,需经历2个拷贝过程。进程通信(IPC)机制,如管道、消息队列等,同样需经历4个拷贝过程才能实现两个进程间通信,共享内存除外,所以共享内存也是最快的IPC方式。
通过mmap方法,将文件(包括设备文件)映射到用户进程虚拟内存空间,代替read/write/ioctl
的访问方式,此时内存拷贝过程只有“用户空间到虚拟内存空间”,省去了“用户到内核”的拷贝过程,在数据量大的情况下能显著提升读写效率。因此,mmap也称为“零拷贝”(zero copy)技术。此时与IPC中的“共享内存”机制是比较类似的。
注:
一般涉及到大数据,非频繁读写的情况考虑使用mmap;频繁读写且是零碎内容的,对磁盘有一定损伤。
mmap
的实现是,系统首先先分配一段空闲的进程空间,先建立一个vm_area_struct
的数据结构,表示虚拟进程空间地址,然后将新建的虚拟区结构vm_area_struct
加入进程的虚拟地址区域链表或树中,内核调用内核态mmap
函数把磁盘文件区域和虚拟进程空间地址映射起来。此时,只是建立了映射关系,实际物理内存空间没有内容的,需要经过MMU将“虚拟空间”和“物理空间”建立映射。进程在发起访问映射虚拟进程空间操作时,由于实际内存的物理页面还没有放置到页框中,内核会产生一个缺页中断从而进入实际页框分配过程,然后根据mmap()
函数建立的vm_area_struct
将磁盘文件加载到物理内存中。之后进程可以正常访问这段映射内存了。
linux内存采用的是页式管理机制,页是内存的最小粒度,大小通常是4K字节。因此,映射的文件大小不足一页时,实际分配的虚拟进程空间也是按整数页分配的。
mmap主要有两种常用用法
注
linux驱动设备也是“文件”,但字符设备(LED、GPIO、串口)等一些“字节流”式的设备一般不支持mmap方法,因为字符设备和块设备的缓冲同步策略不一样。
函数原型
void* mmap (caddr_t addr, size_t len, int prot, int flags, off_t offset)
标识 | 函数 |
---|---|
PROT_READ | 映射区可读 |
PROT_WRITE | 映射区可写 |
PROT_EXEC | 映射区可执行 |
PROT_NONE | 映射区不可访问 |
MAP_SHARED
,对映射区写数据同时会写入文件,且允许其他映射该文件的进程共享MAP_PRIVATE
,对映射区的写入操作会产生一个私有映射区的复制(copy-on-write), 对该区域的修改不会写入原文MAP_ANONYMOUS
,表示创建匿名映射,此时会忽略参数fd,不涉及文件,而且映射区只用于父子进程共享MAP_DENYWRITE
,只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝MAP_LOCKED
,表示锁定映射区,该区域不会被内存交互(swap)open
时的返回值;如果是匿名映射(MAP_ANONYMOUS
),fd设为-1error
中注:
建立映射后,即使文件关闭,映射依然存在。因为映射的是磁盘的地址,不是文件本体,与文件描述符无关。同时,由于映射机制是按页映射的,可用于进程间通信的有效地址空间是容纳映射文件大小的整数页。
函数原型
int munmap(void *addr, size_t len)
error
中 文件映射到进程空间后,用户进程即可通过访问内存的方式间接访问文件,适用于常用的内存操作函数memcpy、memset
以及字符操作函数strncpy、sprintf
等。关于访问范围,有几个注意的点:
mmap()
函数len
参数决定以下图为例,映射进程空间范围是page0——page3,实际映射文件大小page0、page1及一部分page2,因此进程可访问范围也就是page0——page2。访问超出文件大小范围(page2超出文件映射部分),是无法修改文件内容的;访问page3将导致总线错误,访问非映射范围(page3之后)会导致进程崩溃(段错误)。如果,此时文件大小扩张,并且在page3范围,有效访问空间也扩张。因此,使用mmap时需注意映射空间有效访问范围。
进程在映射空间的对共享内容的修改不会实时同步写回到磁盘文件中,只有调用munmap()
函数释放映射后才会执行同步操作。mmap机制提供msync()
函数,用于手动同步修改内容到磁盘源文件。
int msync ( void * addr, size_t len, int flags);
MS_ASYNC
,异步操作,函数调用后立即返回,不需等待同步完成MS_SYNC
,同步操作,等待同步完成函数才返回MS_INVALIDATE
,通知其他共享映射区域数据变动,进程映射已失效,需重新建立映射获取最新数据内容error
中编写一个例子实现:
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char **argv)
{
int fd = 0, fsize = 1024;
char *mmap_mem = NULL;
struct stat ft;
if (argc <= 1)
{
printf("%s: file path error\n", argv[0]); /* argv[1] 为文件名称 */
exit(-1);
}
fd = open(argv[1], O_RDWR);
if (fd < 0)
{
perror ("open");
}
/* 获取文件属性 */
if ((fstat (fd, &ft)) == -1)
{
perror ("fstat");
}
/* 将文件映射到进程虚拟空间 */
mmap_mem = (char *) mmap (NULL, ft.st_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (mmap_mem == (void *) -1)
{
perror ("mmap");
exit(-1);
}
/* 映射后, 即使关闭文件可以操作文件 */
close (fd);
/* 打印输出文件 */
printf("before file:%s\n", mmap_mem);
/* 修改文件并立即同步 */
mmap_mem[0] = 'h';
if ((msync ((void *) mmap_mem, ft.st_size, MS_SYNC)) == -1)
{
perror ("msync");
}
printf("after file:%s\n", mmap_mem);
/* 释放映射区 */
munmap(mmap_mem, ft.st_size);
return 0;
}
手动创建一个“file_test”文件,并输入“Hello word”内容,然后保存。在Ubuntu16 64位系统下编译上述程序并执行。
编写一个例子实现:
#include
#include
#include
#include
#include
#define MMAP_MEM_SIZE 1024
int main (int argc, char **argv)
{
char *mmap_mem = NULL;
int pid = 0;
pthread_mutex_t mutex; /* 互斥锁 */
pthread_mutexattr_t mutexattr; /* 互斥锁属性 */
/* 创建互斥锁 */
pthread_mutexattr_init(&mutexattr); /* 初始化 mutex 属性 */
pthread_mutexattr_setpshared(&mutexattr, PTHREAD_PROCESS_SHARED); /* 修改属性为进程间共享 */
pthread_mutex_init(&mutex, &mutexattr);
/* 创建内存匿名映射, 用于父子进程间 */
mmap_mem = (char *) mmap (NULL, MMAP_MEM_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (mmap_mem == (void *) -1)
{
perror ("mmap");
exit(-1);
}
pid = fork();
if (pid == 0)
{
sleep(1); /* 让父进程先执行 */
pthread_mutex_lock(&mutex);
printf ("child process read mem: %s\n", mmap_mem); /* 获取父进程内容 */
sprintf (mmap_mem, "%s", "child process content"); /* 修改映射区 */
pthread_mutex_unlock(&mutex);
munmap (mmap_mem, MMAP_MEM_SIZE);
exit (0);
}
else if (pid > 0)
{
pthread_mutex_lock(&mutex);
sprintf(mmap_mem, "%s", "parent process content"); /* 修改映射区 */
pthread_mutex_unlock(&mutex);
sleep(2); /* 主动挂起父进程,让子进程访问 */
printf ("parent process read mem: %s\n", mmap_mem); /* 获取子进程内容 */
}
else
{
perror("fork");
}
return 0;
}
【1】【Linux】Linux的虚拟内存详解(MMU、页表结构)
【2】 细话mmap