嘿,各位技术潮人!好久不见甚是想念。生活就像一场奇妙冒险,而编程就是那把超酷的万能钥匙。此刻,阳光洒在键盘上,灵感在指尖跳跃,让我们抛开一切束缚,给平淡日子加点料,注入满满的
passion。准备好和我一起冲进代码的奇幻宇宙了吗?Let's go!
我的博客:yuanManGan
我的专栏:C++入门小馆 C言雅韵集 数据结构漫游记 闲言碎语小记坊 进阶数据结构 走进Linux的世界 题山采玉 领略算法真谛

程序地址空间回顾
我相信大家对这张图不陌生了。

写个代码测试一下
cpp
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 int g_unval;
5 int g_val = 100;
6 int main(int argc, char *argv[], char *env[])
7 {
8 const char *str = "helloworld";
9 printf("code addr: %p\n", main);
10 printf("init global addr: %p\n", &g_val);
11 printf("uninit global addr: %p\n", &g_unval);
12 static int test = 10;
13 char *heap_mem = (char*)malloc(10);
14 char *heap_mem1 = (char*)malloc(10);
15 char *heap_mem2 = (char*)malloc(10);
16 char *heap_mem3 = (char*)malloc(10);
17 printf("heap addr: %p\n", heap_mem); //heap_mem(0), &heap_mem(1)
18 printf("heap addr: %p\n", heap_mem1); //heap_mem(0), &heap_mem(1)
19 printf("heap addr: %p\n", heap_mem2); //heap_mem(0), &heap_mem(1)
20 printf("heap addr: %p\n", heap_mem3); //heap_mem(0), &heap_mem(1)
21
22 printf("test static addr: %p\n", &test); //heap_mem(0), &heap_mem(1)
23 printf("stack addr: %p\n", &heap_mem); //heap_mem(0), &heap_mem(1)
24 printf("stack addr: %p\n", &heap_mem1); //heap_mem(0), &heap_mem(1)
25 printf("stack addr: %p\n", &heap_mem2); //heap_mem(0), &heap_mem(1)
26 printf("stack addr: %p\n", &heap_mem3); //heap_mem(0), &heap_mem(1)
27
28 printf("read only string addr: %p\n", str);
29 for(int i = 0 ;i < argc; i++)
30 {
31 printf("argv[%d]: %p\n", i, argv[i]);
32 }
33 for(int i = 0; env[i]; i++)
34 {
35 printf("env[%d]: %p\n", i, env[i]);
36 }
37
38 return 0;
39 }

我们的栈是向下生长,而堆却是向上生长 ,但我们上一篇博客讲到,我们的
一个变量的地址是它的众多地址中最小的那个即:

虚拟地址
来段代码感受⼀下
cpp
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 int g_val = 0;
5 int main()
6 {
7 pid_t id = fork();
8 if(id < 0)
9 {
10 perror("fork");
11 return 0;
12 }
13 else if(id == 0)
14 {
15 //child
16 printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
17 }
18 else
19 {
20 //parent
21 printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
22 }
23 sleep(1);
24 return 0;
25 }

我们发现父子进程对应的地址是一样的,这就说明父子进程共用一份代码和数据。
这就好比我们之前遇到过的浅拷贝一样的概念
我们改一下代码:
cpp
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
4 int g_val = 0;
5 int main()
6 {
7 pid_t id = fork();
8 if(id < 0)
9 {
10 perror("fork");
11 return 0;
12 }
13 else if(id == 0)
14 {
15 //child
16 int cnt = 5;
17 while(cnt--)
18 {
19 printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
20 g_val++;
21 sleep(1);
22 }
23 }
24 else
25 {
26 //parent
27 int cnt = 5;
28 while(cnt--)
29 {
30 printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
31 sleep(1);
32 }
33 }
34 return 0;
35 }

咦为什么,我们的地址没有改变,子进程改了g_val,但父进程的g_val没有改呢?
这就说明这个地址不是物理地址,而是虚拟地址,真正的物理地址我们看不到,被OS隐藏起来了,目的是保护内存。
要回答上面的问题,就要真正的了解虚拟地址了

这个是虚拟内存真正的样子,存在一个页表将虚拟内存映射到物理内存。
子进程不仅回继承父进程的代码和数据,还会继承这个页表。

当我们要修改g_val的值的时候,我们就会发生写时拷贝,将g_val在物理内存中拷贝一份,再让你修改,这样就使得我们在看到g_val的地址是一样的,但更改g_val的值互相不会有影响。

那Linux是怎样管理整个虚拟内存的呢?
在每个进程里面都有一个描述地址空间的结构体mm_struct
cpp
1 struct task_struct
2 {
3 /*...*/
4 struct mm_struct *mm; //对于普通的⽤⼾进程来说该字段指向他的
虚拟地址空间的⽤⼾空间部分,对于内核线程来说这部分为NULL。
5 struct mm_struct *active_mm; // 该字段是内核线程使⽤的。当该
进程是内核线程时,它的mm字段为NULL,表⽰没有内存地址空间,可也并不是真正的没有,这是因为所
有进程关于内核的映射都是⼀样的,内核线程可以使⽤任意进程的地址空间。
6
7 /*...*/
8 }```
```cpp
1 struct mm_struct
2 {
3 /*...*/
4 struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */
5 struct rb_root mm_rb; /* red_black树 */
6 unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的⼤⼩*/
7 /*...*/
8 // 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。
9 unsigned long start_code, end_code, start_data, end_data;
10 unsigned long start_brk, brk, start_stack;
11 unsigned long arg_start, arg_end, env_start, env_end;
12 /*...*/
13}
这里面就要描述一个区域开始的结束的成员变量。


那我们的常量数据段是怎么做到不让你修改这个数据的呢?
在页表的它不止单单有一个映射关系,它还有权限位。


