目录
背景
一、什么是内存映射?
二、mman函数
1.权限问题
2.总线错误
3.内存权限
4.读文件内容
5.映射与文件
6.非法参数错误
7.偏移量大小
8.映射内存大小
8.1 申请6k,访问5k
8.2 申请2k,访问3k
8.3 返回值检查
三、内存映射实现
四、匿名内存映射
五、munmap函数
背景
在传统的文件访问中,进程通常使用系统调用(例如open()
,read()
,write()
,close()
等)来操作文件。这些系统调用允许进程打开文件、从文件中读取数据、向文件中写入数据以及关闭文件。这种方法需要通过系统调用在用户空间和内核空间之间进行数据传输,因此可能会产生一定的性能开销。
而使用内存映射技术,即将文件的内容映射到进程的虚拟内存空间中,使得进程可以直接访问内存而无需通过系统调用来读取或写入文件的内容。这样做可以提高文件访问的效率,因为读写文件的操作直接转化为对内存的操作,避免了频繁的系统调用。
内存映射是一种将磁盘文件的内容映射到进程的虚拟内存空间中的技术。这意味着文件的内容被映射到进程的地址空间中,使得进程可以直接通过访问内存的方式来读取或写入文件的内容,而无需调用传统的read和write系统调用。这种方式能够提高对文件的访问效率,并且简化了对文件的操作。
内存映射的主要优点包括:
-
性能优势:由于文件内容直接映射到了内存中,因此读取文件的操作可以直接通过内存访问完成,避免了频繁的磁盘IO操作,从而提高了读取文件的速度。
-
简化操作:通过内存映射,文件被映射到了进程的地址空间中,进程可以像操作内存一样对文件进行读写,这样就简化了对文件的操作流程。
-
共享内存:多个进程可以共享同一个文件的内存映射,这样可以实现进程间的数据共享,而无需进行显式的数据传输。
一、什么是内存映射?
使一个磁盘文件与内存中的一个缓冲区相映射,进程可以像访问普通内存一样对文件进行访问,不必再调用read,write。
二、mman函数
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
功能:创建共享内存映射
函数返回值:成功返回创建的映射区首地址,失败返回MAP_FAILED( ((void *) -1) ),设置errno值。
参数说明:
addr:指定要映射的内存地址,一般设置为 NULL 让操作系统自动选择合适的内存地址。
length:必须>0。映射地址空间的字节数,它从被映射文件开头 offset 个字节开始算起。
prot:指定共享内存的访问权限。
可取如下几个值的可选:
PROT_READ(可读),
PROT_WRITE(可写),
PROT_EXEC(可执行),
PROT_NONE(不可访问)。
flags:由以下几个常值指定:
MAP_SHARED(共享的),
MAP_PRIVATE(私有的),
MAP_ANONYMOUS(匿名映射,用于血缘关系进程间通信)
MAP_FIXED(表示必须使用 start 参数作为开始地址,如果失败不进行修正),
其中,MAP_SHARED , MAP_PRIVATE必选其一,而 MAP_FIXED 则不推荐使用。
fd:表示要映射的文件句柄。如果匿名映射写-1。
offset:表示映射文件的偏移量,一般设置为 0 表示从文件头部开始映射。
注意事项:
1.权限问题
#include<stdio.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/mman.h>
#include<unistd.h>
int main()
{
void* addr;
int fd;
fd = open("test",O_RDONLY);
if(fd < 0)
{
perror("open");
return 0;
}
// int len = lseek(fd,0,SEEK_END);
addr = mmap(NULL,2048,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(addr == MAP_FAILED)
{
perror("mmap");
return 0;
}
return 0;
}
文件的打开权限是只读,运行结果为:Permission denied
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ gcc -o mman mman.c
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ ./mman
mmap: Permission denied
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ cat mman.c
文件打开权限修改为可读可写即可,因为PROT_READ | PROT_WRITE为可读可写,要求:
当MAP_SHARED时,要求:映射区的权限应 <=文件打开的权限(出于对映射区的保护),如果不满足报非法参数(Invalid argument)错误。
2.总线错误
用于映射的文件大小必须>0,当映射文件大小为0时,指定非0大小创建映射区,访问映射地址会报总线错误。
#include<stdio.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/mman.h>
#include<unistd.h>
int main()
{
void* addr;
int fd;
fd = open("test",O_RDWR);
if(fd < 0)
{
perror("open");
return 0;
}
// int len = lseek(fd,0,SEEK_END);
addr = mmap(NULL,2048,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(addr == MAP_FAILED)
{
perror("mmap");
return 0;
}
memcpy(addr,"abcdefg",7);
return 0;
}
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ gcc -o mman mman.c
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ ./mman
Bus error (core dumped)
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$
test文件里为空, 不满足:用于映射的文件大小必须>0。直接在test里加个空格都行。
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ cat test
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ vim test
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ cat test
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ ./mman
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ cat test
ablinux@linux:/mnt/hgfs/linuxshare/linux_code/mman$
文件足够大,就可以显示出abcdef:
ablinux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ vim test
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ ls -l test
-rwxrwxrwx 1 root root 104 Mar 8 00:44 test
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ ./mman
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ cat test
abcdefg
3.内存权限
addr = mmap(NULL,2048,PROT_READ | PROT_WRITE,MAP_PRIVATE,fd,0);
当MAP_PRIVATE时候,mmap中的权限是对内存的限制,只需要文件有读权限即可,操作只在内存有效,不会写到物理磁盘,且不能在进程间共享。
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ gcc -o mman mman.c
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ ./mman
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ cat test
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ vim test
4.读文件内容
注意进行强制类型转换,不然会有警告。
printf("read = %s\n",(char*)addr);
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ vim mman.c
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ gcc -o mman mman.c
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ ./mman
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ cat test
abcdefg
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ vim mman.c
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ gcc -o mman mman.c
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ ./mman
read = abcdefg
5.映射与文件
映射区的释放与文件关闭无关,只要映射建立成功,文件可以立即关闭。
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ vim mman.c
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ gcc -o mman mman.c
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ ./mman
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ cat test
999999999
int main()
{
void* addr;
int fd;
fd = open("test",O_RDWR);
if(fd < 0)
{
perror("open");
return 0;
}
// int len = lseek(fd,0,SEEK_END);
addr = mmap(NULL,2048,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(addr == MAP_FAILED)
{
perror("mmap");
return 0;
}
close(fd);
memcpy(addr,"999999999",9);
// printf("read = %s\n",(char*)addr);
return 0;
}
6.非法参数错误
指定0大小创建映射区,报非法参数错误(Invalid argument)
addr = mmap(NULL,0,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ vim mman.c
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ gcc -o mman mman.c
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ ./mman
mmap: Invalid argument
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$
可以通过lseek取文件大小
int len = lseek(fd,0,SEEK_END);
printf("%d\n",len);
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ ls -l test
-rwxrwxrwx 1 root root 51 Mar 8 01:20 test
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ ./mman
51
read = 999999999
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$
#include <sys/types.h>
#include <unistd.h>
off_t lseek(int filedes, off_t offset, int whence) ;
返回值:成功:返回文件新的偏移量(成功)
lessk()函数的参数说明:
第一个参数是文件描述符;
第二个参数是偏移量,
参数 offset可正可负,负数时向文件开头偏移,正数相对于文件末尾偏移
第三个参数是有三个选项:
1.SEEK_SET:将文件指针偏移到传入字节数处(文件头开始偏移)
2.SEEK_CUR:将文件指针偏移到当前位置加上传入字节数处;((当前位置开始偏移)
3.SEEK_END:将文件指针偏移到文件末尾加上传入字节数处(作为拓展作用,必须再执行一次写操作)
7.偏移量大小
文件偏移量必须为0或者4K的整数倍(不是,会报非法参数Invalid argument错误)
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
最后一个参数是偏移量
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ vim mman.c
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ gcc -o mman mman.c
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ ./mman
mmap: Invalid argument
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$
addr = mmap(NULL,len,PROT_READ | PROT_WRITE,MAP_SHARED,fd,1);
8.映射内存大小
映射大小可以大于文件大小,但只能访问文件page的内存地址,否则报总线错误 ,超出映射的内存大小报段错误。
实际是根据文件大小分配空间的,文件大小是51。你申请2k的空间,实际分配的是4k的空间,而你申请6k,实际分配的还是4k空间,所以报总线错误。
8.1 申请6k,访问5k
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ vim mman.c
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ gcc -o mman mman.c
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ ./mman
Bus error (core dumped)
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$
addr = mmap(NULL,6000,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
memcpy((addr+5000),"999999999",9);
8.2 申请2k,访问3k
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ vim mman.c
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ gcc -o mman mman.c
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ ./mman
read = 999999999
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$
addr = mmap(NULL,2000,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
memcpy((addr+3000),"999999999",9);
8.3 返回值检查
mmap创建映射区出错概率非常高,一定要检查返回值,确保映射区建立成功再进行后续操作。
void* addr;
addr = mmap(NULL,2048,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(addr == MAP_FAILED)
{
perror("mmap");
return 0;
}
三、内存映射实现
mmanw.c
#include<stdio.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/mman.h>
#include<unistd.h>
int main()
{
void* addr;
int fd;
fd = open("test",O_RDWR);
if(fd < 0)
{
perror("open");
return 0;
}
int len = lseek(fd,0,SEEK_END);
addr = mmap(NULL,2048,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(addr == MAP_FAILED)
{
perror("mmap");
return 0;
}
close(fd);
int i = 0;
while(i<2048)
{
memcpy((addr+i),"a",1);
i++;
sleep(1);
}
return 0;
}
mmanr.c
#include<stdio.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/mman.h>
#include<unistd.h>
int main()
{
void* addr;
int fd;
fd = open("test",O_RDWR);
if(fd < 0)
{
perror("open");
return 0;
}
int len = lseek(fd,0,SEEK_END);
addr = mmap(NULL,2048,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(addr == MAP_FAILED)
{
perror("mmap");
return 0;
}
close(fd);
while(1)
{
printf("read = %s\n",(char*)(addr));
sleep(1);
}
return 0;
}
四、匿名内存映射
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
fd:表示要映射的文件句柄。如果匿名映射写-1。
匿名映射适用于具有亲缘关系的进程之间。
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ gcc -o mmap_n mmap_n.c
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ ./mmap_n.c
./mmap_n.c: line 13: syntax error near unexpected token `('
./mmap_n.c: line 13: `int main()'
linux@linux:/mnt/hgfs/linuxshare/linux_code/mman$ ./mmap_n
read father value = 1234567890
#include<stdio.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<sys/mman.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
int main()
{
void* addr;
//匿名映射不需要文件操作
addr = mmap(NULL,2048,PROT_READ | PROT_WRITE,MAP_SHARED | MAP_ANONYMOUS,-1,0);
if(addr == MAP_FAILED)
{
perror("mmap");
return 0;
}
pid_t pid;
pid = fork();
if(pid < 0)
{
perror("pid");
return 0;
}
else if(pid > 0)
{
memcpy(addr,"1234567890",10);
wait(NULL);
}
else
{
sleep(1);
printf("read father value = %s\n",(char*)addr);
}
return 0;
}
使用mmap
函数创建一个大小为2048字节的共享内存区域。mmap
的第一个参数是指定的映射地址(这里为NULL表示由系统决定),然后指定映射的大小为2048字节,设置读写权限为可读可写,使用MAP_SHARED
标志来创建一个共享映射,MAP_ANONYMOUS
标志表示不与任何文件关联 。
使用fork
函数创建一个子进程。如果fork
返回负值,则表示创建失败,父进程会输出错误信息并退出。如果fork
返回0,则表示当前代码运行在子进程中,子进程将等待1秒后,从共享内存中读取数据并打印输出。如果fork
返回正值,则表示当前代码运行在父进程中,父进程使用memcpy
将字符串"1234567890"复制到共享内存中,然后等待子进程结束。
为什么?
父进程在写入数据后,可能会导致子进程读取数据,
而如果父进程提前退出,操作系统会清理共享内存,
这可能导致子进程读取到的数据不完整或者无效。
通过调用wait(NULL),父进程等待子进程执行完毕,
确保子进程能够正常读取共享内存中的数据。
只有在子进程执行完毕后,父进程才会继续执行后续的操作,包括退出程序,
这样就不会导致共享内存被提前清理。
五、munmap函数
int munmap(void *addr, size_t length);
返回值:成功返回0,失败返回-1,并设置errno值。
函数参数:
addr:调用mmap函数成功返回的映射区首地址
length:映射区大小(即:mmap函数的第二个参数)