物理地址映射到虚拟地址空间
- mmp 会在物理地址里找一块页框,然后在用虚拟地址空间选一块虚拟(线性地址)地址,然后将这两个内容分别填到页表里,就建立起来里他们之间的映射关系。
- 用户态先通过虚拟地址找到页表,然后再找到对应的物理地址,然后在访问其中的内存,mmp这这个系统调用就是来建立这种关系的
文件映射到虚拟地址空间
- 除了将物理地址映射到线性(虚拟)地址空间,还可以将文件映射到虚拟地址空间,那么用户在虚拟地址空间里(也就相当于用户在内存里面),直接对文件的操作可以同步到文件当中。这样就可以一次映射一大块文件,不用再使用read和write这样的系统调用。节省了系统的开销。
- 建立mmp映射时候,如果不对建立的这块虚拟内存进访问,是不会把磁盘的内容加载到物理地址空间。只要访问时,才会把文件块的内容加载到物理地址的页框当中。
- 包括加载共享库,也是用的mmp机制。
mmp
- mmp 返回一个地址,void 的意思就是不对返回地址对应的地址空间进行访问,需要访问时再用不同的访问方式去访问这块内存。(将void*改成int *, char *…再使用)
- port 指定映射区域的访问权限,虚拟地址映射到物理地址是以页(Page)为单位的,通常一页为4K。前三个可以进行或运算,PORT_NONE只能单独使用(对页不进行任何访问)
- MAP_SHARED: 可以把一块物理内存映射到两个进程的不同的虚拟地址空间当中,这时一个进程对映射的虚拟地址空间中的内容进行修改,那在另一个进程中也可见。文件映射时,修改也会同步到文件当中。
- MAP_ANONYMOUS 把物理地址映射到虚拟地址空间,但是这个物理地址是没有名字的。
- offset 是4k的整数倍
munmap
代码示例
1. 将一块物理地址映射到进程的虚拟地址空间
t_stdio.h
#ifndef T_STDIO_H_
#define T_STDIO_H_
#include <stdio.h>
#define E_MSG(STRING, VAL) do{perror(STRING); return(VAL);}while(0)
#endif
mmap.c
#include "t_stdio.h"
#include <sys/mman.h>
#include <string.h>
// void *mmap(void *addr, size_t length, int prot, int flags,
// int fd, off_t offset);
// int munmap(void *addr, size_t length);
int main(void){
// 权限是可读可写
int prot = PROT_READ | PROT_WRITE;
// 不需要同步到底层文件, 所以采用私有。
// 并且是物理地址映射到虚拟地址空间, 所以采用匿名
int flags = MAP_PRIVATE | MAP_ANONYMOUS;
// 将物理地址映射到进程的虚拟地址空间
// NULL让内核去选择一块虚拟空间
// 指定映射区域长度1024, 那么munmap的映射区域长度也就是1024
// 映射区域可读可写
// flags 映射方式 私有加匿名
// 因为是匿名映射fd=-1, offset=0
void *p = mmap(NULL, 1024, prot, flags, -1, 0);
if (p == MAP_FAILED) E_MSG("mmap", -1);
// 到这里已经将物理地址映射到了进程的虚拟地址空间
// 再这里去使用p就不会出现段错误了,因为*p这个虚拟地址有对应的物理地址
// strcpy 可以给void* 类型的地址空间里拷贝数据吗? 但是void类型的指针是不能直接使用的。好好想想
strcpy(p, "hello beijing");
printf("%s\n", (char *)p);
// 解除映射
munmap(p, 1024);
return 0;
}
```shell
$ gcc mmap.c
$ ./a.out
hello beijing
2. 将一个文件映射到进程的虚拟地址空间(内存), 对内存中的内容进行修改,直接反映到文件中
t_file.h
#ifndef T_FILE_H
#define T_FILE_H
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#endif
mmap_file.c
#include "t_file.h"
#include "t_stdio.h"
#include <sys/mman.h>
int main(int argc, char *argv[]) {
// 以读写的方式打开文件
int fd=open(argv[1], O_RDWR);
if(fd==-1)E_MSG("open", -1);
// 权限和上面的文件打开方式保持一致
int prot = PROT_READ | PROT_WRITE;
// 映射方式选择MAP_SHARED
int flags = MAP_SHARED;
// 建立文件到虚拟地址的映射
// 映射时不要超过文件的长度, 不然会错误
void *p = mmap(NULL, 6, prot, flags, fd, 0);
if (p==MAP_FAILED)E_MSG("mmap", -1);
// 关闭文件描述符, 不影响已经建立的映射区域
close(fd);
//映射已经建立
// void * 类型指针不能直接使用,转为int * 类型再使用
// 前面的0x前缀表示十六进制。十六进制下每2位恰好为二进制下的8位即8bit。我们把8bit定义为1byte,所以十六进制数每2位即是1byte。
// 0x30313233 是16进制的整数,4个字节, 以整型的方式去修改进程虚拟地址空间
// 0x31 -> 0011 0001 -> 是整数49 -> ASSIC中 字符'1' 对应 整数49
// 字符在内存中以整数的方式保存
*((int *)p) = 0x30313233;
// 解除映射
munmap(p,6);
return 0;
}
// 查看文件内容
// 上面是accic码
$ touch hello; echo hello > hello
$ gcc mmap_file.c
## od -tx1 -tc 查看文件内容
## 下面是字符, 上面是assic码
## 实际上 hello 字符会被转换成assic码( 68 65 6c 6c 6f ) 存放在文件当中
$ od -tx1 -tc hello
0000000 68 65 6c 6c 6f 0a
h e l l o \n
0000006
$ a.out hello
## 以整型的方式去修改, 一次修改4个字节
$ od -tx1 -tc hello
0000000 33 32 31 30 6f 0a
3 2 1 0 o \n
0000006
## 如果 *((int *)p) = 0x3031;
## 还是修改4个字节
$ od -tx1 -tc hello
0000000 31 30 00 00 6f 0a
1 0 \0 \0 o \n
0000006
# 30 在高地址,属于高位字节放在高地址中,是小端
补充知识(字节序)
- short val = 0x0001; 十六进制数每2位即是1byte, 短整型val占两个字节, 00是高位字节, 01是低位字节, 内存增长方向为: 左边是低地址,右边是高地址。
- 大端:如果把00高位字节放到低地址当中, 01低位字节就必须放到高地址
- 小端:如果把00高位字节放到高地址中,01低位字节就必须放到低地址中
代码示例
s_b.c
#include <stdio.h>
//short类型占两个字节, char类型占一个字节
// 所以这个联合类型的变量占两个字节, c 和 v的地址是一样的
typedef union {
short v;
char c;
}u_t;
int main(void){
// 把0x0001存到v里,查看c里是00还是01, 如果01说明是小端(低位字节放低地址)
u_t t;
t.v = 0x0001;
if(t.c)
printf("small\n");
else
printf("big\n");
return 0;
}
$ gcc s_m.c
$ ./a.out
small