用户内存空间分布和mmap
# 整体布局
参考:https://blog.csdn.net/faxiang1230/article/details/106242380 (opens new window)
白色区域表示gap,为了防止无意间的访问越界,对该区域的任何读写操作都会触发MMU异常进而收到SIGSEGV信号。
地址空间底部依次是text,data,bss段,程序编译链接的时候就已经确定他们的size,可能加载的时候进行了地址随机化,但是相对地址是固定的.
heap段也就是堆区域,平时我们经常把堆和栈混为一谈,其实他们是分开的,在bss段上方有一个随机的gap区域,之后就是heap的基地址,随着程序的运行他动态向上增长.
mapping区域在在堆和栈中间,它是用来加载依赖动态库的,最顶部的动态库地址和栈基地址有一个间隔,为栈动态增长预留的 用户空间最上方是栈区域,栈的基地址是内核空间之下的地址,可能会有随机的偏移,随着函数的调用嵌套是向下增长的
# 用户栈
我们通过ulimit -a看到系统对当前进程的栈size进行了限制,大部分是8M的限制。用户可以通过ulimit -s来修改栈的大小,可以扩充到100M的大小
# bss段
参考:https://www.zhihu.com/question/23059602/answer/23486779 (opens new window)
早期的计算机存储设备是很贵的,而很多时候,数据段里的全局变量都是0(或者没有初始值),那么存储这么多的0到目标文件里其实是没有必要的。所以为了节约空间,在生成目标文件的时候,就把没有初始值(实际就是0)的数据段里的变量都放到BSS段里,这样目标文件就不需要那么大的体积里(节约磁盘空间)。只有当目标文件被载入的时候,加载器负责把BSS段清零(一个循环就可以搞定)。 之后,这个规则慢慢的成为一个标准配置,大多数编译器也就都支持了BSS段。
bss段通常用来存放程序中未初始化的或初始化为0的全局变量和静态变量,负责将BSS段清零的工作一般是由加载器完成的,当一个可执行文件被加载的时候,加载器(可以简单的理解为操作系统)负责把BSS段的内存清零。有初始化值的全局变量和静态变量在data段。
#include <stdio.h>
int a = 0;
int b;
int c = 5;
int main()
{
printf("data: c,f; bss:a,b,d,e\n");
static int d = 0;
static int e;
static int f = 5;
printf("a-bss : %p\n", &a);//00404014
printf("b-bss : %p\n", &b);//00404010
printf("c-data: %p\n", &c);//00402000
printf("d-bss : %p\n", &d);//00404018
printf("e-bss : %p\n", &e);//00404028
printf("f-data: %p\n", &f);//00402004
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# mmap
mmap()可以指定起始地址,如果该地址不可用,则系统会分配一个随机地址。返回值为为实际分配的地址。
mmap()分配的最小长度为pagesize,在shell中可以用getconf PAGE_SIZE
获取,一般默认是4096。
代码中可以通过int pagesize = sysconf(_SC_PAGE_SIZE);
获取页大小。
#include <sys/mman.h>
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
2
3
4
- 指定map的地址。为NULL则由kernel分配地址。不为NULL时,该地址仅供kernel参考(除非指定MAP_FIXED)。
- prot为读写执行权限
- flags为打开方式,
MAP_SHARED
表示共享方式映射内存。如果指定了MAP_FIXED
,则会映射到指定的地址。如果该地址之前已经被映射,则会抛弃前面的映射。anonymous
匿名的,不关联文件。 - fd, 映射文件的fd
- offset, 映射到映射文件的offset,必须为内存页大小的整数倍
RETURN VALUE On success, mmap() returns a pointer to the mapped area. On error, the value MAP_FAILED (that is, (void *) -1) is returned, and errno is set to indicate the cause of the error.
On success, munmap() returns 0. On failure, it returns -1, and errno is set to indicate the cause of the error
(probably to EINVAL).
- open()和shm_open() 参考https://bbs.csdn.net/topics/390598198/ (opens new window)
普通的open()会打开一个文件,而shm_open()会在/dev/shm目录上生成一个文件,而且会校验该目录下是不是挂载了tmpfs文件系统,如果不是也不能正常打开。因为这个文件存在tmpfs文件系统下,在不用的情况系统会自动删除掉。
所以如果需要保存文件,则open()普通文件,然后用mmap映射。如果只是ipc通信,不需要保存文件内容,则用shm_open()更方便,打开的临时文件不用了会自动删除。
// #define _GNU_SOURCE
#include <sys/types.h>
#include <sys/mman.h>
#include <string>
#include <iostream>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <error.h>
using namespace std;
//页大小一般为4096字节,读每页开头一个字节,从而建立tlb表项。注意要防止编译器优化。
void touch_page_tlb(void *addr, size_t len) {
for(int i=0; i<len; i+=4096) {
uint8_t *a = (uint8_t *)((char *)addr+i);
__asm__ __volatile__("" : : "m"(*a) : );
}
}
//典型用法, 不指定映射地址, map长度为len字节, 文件偏移offset=0
int main()
{
const char *filename = "/tmp/mmap_test";
int len = 8192;
int fd = open(filename, O_CREAT | O_RDWR, 0666);
if(fd < 0) {
std::cerr << "file open failed: " << strerror(errno) << std::endl;
return 1;
}
if(ftruncate(fd, len)) { //更改文件大小
std::cerr << "ftruncate failed: " << strerror(errno) << std::endl;
close(fd);
return 2;
}
uint8_t *addr1 = (uint8_t *)mmap(NULL, len, PROT_READ | PROT_WRITE , MAP_SHARED , fd, 0);
// close(fd);
if(addr1==MAP_FAILED){
std::cerr << "mmap failed: " << strerror(errno) << std::endl;
return -1;
}
printf("mmap succeed, addr1=%p\n", addr1);
uint8_t *addr2 = (uint8_t *)mmap(NULL, len, PROT_READ | PROT_WRITE , MAP_SHARED , fd, 0);
if(addr2==MAP_FAILED){
std::cerr << "mmap 2 failed: " << strerror(errno) << std::endl;
return -1;
}
else printf("mmap succeed, addr2=%p\n", addr2);
// 内存预读取
// madvise(addr1, len, MADV_WILLNEED | MADV_ACCESS_MANY); //建议内核该内存区域即将使用,使用方式为多进程随机读取
// _mm_prefetch(addr1, len)//预读到cache,一般不需要
touch_page_tlb(addr1, len);
printf("main end\n");
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
下面是映射固定地址的用法
int test_fixed_mmap()
{
//指定映射地址为0x400000, map长度为4096字节, MAP_ANONYMOUS不关联文件(通常初始化为0) fd=-1 文件偏移offset=0
uint8_t *mem1 = (uint8_t *)mmap((void *)0x400000, 4096, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_SHARED | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
uint8_t *mem2 = (uint8_t *)mmap((void *)0x401000, 64, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_SHARED | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
//测试mem1和mem2之间的关系,它们都处于本进程的同一用户地址空间,二者距离为0x1000(4096),可通过偏移互相访问
printf("mem1=0x%p\n", mem1);
printf("mem2=0x%p\n", mem2);
int *mem1_0 = (int *)mem1;
int *mem1_0x1000 = (int *)(mem1 + 0x2000 - 4);
int *mem2_0 = (int *)mem2;
printf("mem1_0x1000=0x%p\n", mem1_0x1000);
printf("mem2_0=0x%p\n", mem2_0);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22