目录
一、程序地址空间
二、fork返回值问题理解
一、程序地址空间
研究背景 kernel 2.6.32 32位平台
程序地址空间回顾
1.地址空间描述的基本空间大小单位是字节
2.32位下-->2^32次方个地址
3.一共有2^32个地址,每个地址标识一个字节,所以能够表示的地址空间范围是2^32*1字节=4GB字节
4.每一个字节都要有唯一的地址,也就需要2^32个地址,即可以用32位的数据表示(00000000~FFFFFFFF)。
区域划分
uint32_t 无符号整型,大小正好32bit
malloc申请一段mm_struct大小的空间,再将这段空间划分
区域调整
代码区、已初始化、未初始化、全局数据区是固定的,不会变化。但是堆区、栈区是变化的,需要被调整。
进程1和进程2都认为自己有2^32个进程地址空间大小,操作系统对进程申请空间有限制。如果进程申请空间超过了限制,操作系统也不允许进程申请。
为什么会存在地址空间:
1. 不安全,如果让进程直接访问物理内存,万一进程越界非法操作呢?肯定会导致数据丢失,或者程序异常。进程直接访问物理内存,可以扫描其他空间存的重要数据。导致机密数据泄露。
2.地址空间的存在,可以更方便的进行进程和进程的数据代码的解耦,保证了进程独立性这样的特征。
3.让进程以统一的视角,来看待进程对应的代码和数据等各个区域,方便使用。编译器也以统一的视角来进行编译代码
每一个进程因为有页表的存在,页表只会映射到合法内存中。就再也不担心物理内存被写坏、恶意读写。
思考:
我们写的代码出现野指针并不会影响其他程序。
来段代码感受一下
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
5 int g_val = 100;
7 int main()
8 {
9 pid_t id = fork();
11 if(id < 0)
12 {
13 perror("fork error\n");
14 return 1;
15 }
17 else if(id == 0)
18 { //child
19 int cnt = 0;
20 while(1)
21 {
22 printf("我是子进程,pid: %-5d, ppid: %-5d | g_val: %d &g_val: %p \n", getpid(),getppid(), g_val,&g_val);
23 sleep(2);
30 }
31 }
33 else
34 { //parent
35 while(1)
36 {
37 printf("我是父进程,pid: %-5d, ppid: %-5d | g_val: %d &g_val: %p \n", getpid(),getppid(), g_val,&g_val);
38 sleep(2);
39 }
40 }
42 return 0;
43 }
输出
我们发现,输出出来的变量值和地址是一模一样的,很好理解呀,因为子进程按照父进程为模版,父子并没有对变量进行进行任何修改。可是将代码稍加改动:
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <stdlib.h>
5 int g_val = 100;
7 int main()
8 {
9 pid_t id = fork();
11 if(id < 0)
12 {
13 perror("fork error\n");
14 return 1;
15 }
17 else if(id == 0)
18 { //child
19 int cnt = 0;
20 while(1)
21 {
22 printf("我是子进程,pid: %-5d, ppid: %-5d | g_val: %d &g_val: %p \n", getpid(),getppid(), g_val,&g_val);
23 sleep(2);
24 cnt++;
25 if(cnt == 10)
26 {
27 g_val = 300;
28 printf("子进程已经更改了全局的变量--------------------\n");
29 }
30 }
31 }
33 else
34 { //parent
35 while(1)
36 {
37 printf("我是父进程,pid: %-5d, ppid: %-5d | g_val: %d &g_val: %p \n", getpid(),getppid(), g_val,&g_val);
38 sleep(2);
39 }
40 }
42 return 0;
43 }
输出结果:
我们发现,父子进程,输出地址是一致的,但是变量内容不一样!为什么会这样呢?看图:
进程的地址都是虚拟地址,子进程按照父进程为模版所以地址和父进程一样。当子或者父进程任何一方尝试向共享数据做写入操作,此时操作系统会在物理内存上重新开辟一块空间并将数据拷贝到新空间,修改做操作一方进程的页表的映射关系让它指向新空间。注意,以上图子进程为例修改的是子进程页表左侧,右侧无变化所以我们看到的输出结果&g_val的值没有变。
写时拷贝:
任何一方尝试写入,OS先进行数据拷贝在更改页表映射,然后再让进程进行修改。
进程的独立性
• 独立的内核数据结构
• 写时拷贝--->不同进程的数据进行分离
• 操作系统,为了保证进程的独立性,做了很多的工作!!--->通过地址空间,通过页表让不同进程映射到不同的物理内存处。
分页&虚拟地址空间
说明:
上面的图就足矣说名问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!
进一步理解地址空间
让进程以统一的视角,来看待进程对应的代码和数据等各个区域,方便使用。编译器也以统一的视角来进行编译代码
现有一可执行程序my.exe,由于编译器以统一的视角来进行编译代码,现在程序内部有一套逻辑地址(0x2222、0x1234、0x1122)。———— >程序加载到内存,具备了物理地址。————>操作系统根据逻辑地址和物理地址做映射(页表)。当进程被调度时cpu拿到逻辑地址开始跑,查页表根据映射关系找到物理地址,找到mian函数开始执行。
二、fork返回值问题理解
1. 如何理解fork函数有两个返回值问题?
我们知道,当一个函数准备return的时候,核心代码已经执行完了。子进程早已经被创建好了,并且可能在OS的运行队列中,准备被调度了。此时已经有父子进程两个执行流,父进程和子进程各自执行return的。所以有两个返回值。
2.如何理解fork返回之后,给父进程返回子进程pid,给子进程返回0?
父亲:孩子=1:n,n是大于等于1的。一个孩子只有一个父亲,孩子找父亲具有唯一性。fork之后子进程不需要获取父进程的id值。一个父亲有多个孩子,那么怎么确定是哪个孩子呢?所以需要pid来标识子进程。
3.如果理解同一个id值,怎么可能会保存两个不同的值,让if else if同时执行?
返回的本质:就是写入! 所以,父子进程谁先返回,谁就先写入id,因为进程具有独立性,谁先返回,谁就写时拷贝。