目录
一、虚拟地址
父进程和子进程之间,代码共享,而数据可能会发生修改,所以当其中一个进程要写入数据时,则发生写时拷贝,各自私有一份。
现在有源文件内容如下所示。
int glob_val = 10;
int main()
{
pid_t id = fork();
if(id == 0)
{
int cnt = 0;
while(1)
{
printf("child : pid:%d, ppid:%d, glob_val:%d, &glob_val:%p\n",getpid(),getppid(),glob_val,&glob_val);
sleep(1);
++cnt;
if(cnt == 5)
{
glob_val = 20;
printf("child change glob_val:10->20\n");
}
}
}
else
{
while(1)
{
printf("child : pid:%d, ppid:%d, glob_val:%d, &glob_val:%p\n",getpid(),getppid(),glob_val,&glob_val);
sleep(1);
}
}
return 0;
}
预期运行结果是,该程序运行五秒之内,父子进程打印的全局变量的值和地址均相同,五秒之后,子进程修改了值,则发生写时拷贝,父子进程打印的全局变量的值和地址均不同。
实际运行结果:
[euto@VM-4-13-centos 24926]$ ./myaddr
child : pid:10513, ppid:31055, glob_val:10, &glob_val:0x60105c
child : pid:10514, ppid:10513, glob_val:10, &glob_val:0x60105c
child : pid:10513, ppid:31055, glob_val:10, &glob_val:0x60105c
child : pid:10514, ppid:10513, glob_val:10, &glob_val:0x60105c
child : pid:10513, ppid:31055, glob_val:10, &glob_val:0x60105c
child : pid:10514, ppid:10513, glob_val:10, &glob_val:0x60105c
child : pid:10513, ppid:31055, glob_val:10, &glob_val:0x60105c
child : pid:10514, ppid:10513, glob_val:10, &glob_val:0x60105c
child : pid:10513, ppid:31055, glob_val:10, &glob_val:0x60105c
child : pid:10514, ppid:10513, glob_val:10, &glob_val:0x60105c
child : pid:10513, ppid:31055, glob_val:10, &glob_val:0x60105c
child change glob_val:10->20
child : pid:10514, ppid:10513, glob_val:20, &glob_val:0x60105c
child : pid:10513, ppid:31055, glob_val:10, &glob_val:0x60105c
child : pid:10514, ppid:10513, glob_val:20, &glob_val:0x60105c
child : pid:10513, ppid:31055, glob_val:10, &glob_val:0x60105c
child : pid:10514, ppid:10513, glob_val:20, &glob_val:0x60105c
child : pid:10513, ppid:31055, glob_val:10, &glob_val:0x60105c
child : pid:10514, ppid:10513, glob_val:20, &glob_val:0x60105c
child : pid:10514, ppid:10513, glob_val:20, &glob_val:0x60105c
child : pid:10513, ppid:31055, glob_val:10, &glob_val:0x60105c
为什么五秒之后全局变量的地址是相同的。
预期结果应该是两个完全不相关的变量,各自有值和地址。其实,这个地址是虚拟地址,并不是实际的物理地址。
而虚拟地址并不止在父子进程的数据拷贝上出现,在程序员这一层,所见到的都是由操作系统提供的虚拟地址,而非实际的物理地址。
划分分区的也是虚拟地址。
虚拟地址可以理解为逻辑地址,在底层其实是有物理地址的,只不过逻辑地址和物理地址之间的函数关系是由操作系统处理的。
当子进程做了写时拷贝后,其实发生的变化不在虚拟地址上,而是在物理地址上。
二、进程地址空间
每一个进程,都存在进程地址空间。
- 操作系统对进程地址空间的管理
先面向对象,再数据结构。
- 进程和进程地址空间
PCB内有一个结构体指针,这个指针指向地址空间这个结构体。
- 区域划分
即虚拟地址空间的区域划分,用代码来实现,本质就是进程地址空间这个结构体的成员变量,进程地址空间的结构体一定有类似下面这样的字眼。
struct 进程地址空间
{
int code_start,code_end;
int init_start,init_end;
int heap_start,heap_end;
int stack_start,stack_end;
·····
}
- 区域划分的本质
所谓的区域划分,本质就是操控特定的几个变量,这些变量实现了区域划分的条件:
1.可以判断是否越界。
2.可以扩大或者缩小范围。
划分好的区域,在区域内的所有地址,是可以被当前进程所使用的。
- 进程管理和内存管理是独立、互不影响的。
代码、数据本质上还是保存在物理内存上面,而虚拟地址是操作系统逻辑上提供给用户的地址,将逻辑地址映射为物理地址的数据结构叫做页表。
可执行程序加载到内存后,进程创建PCB即task_struct、PCB又创建进程地址空间即mm_struct。
内存管理:可执行程序的代码、数据都保存在物理内存中。
进程管理:进程获取代码、数据是在进程地址空间,即获取到的是虚拟地址。
二者之间用页表映射。
- CPU完成页表的映射操作
CPU获取到虚拟地址,CPU内部的寄存器CR3保存着页表的起始地址,CPU内部有一个部件MMU(memory mange unit)将虚拟地址通过页表映射到物理地址。
注意,CR3内部保存的页表的起始地址一定是物理地址。
- 进程地址空间和页表这种设计的意义
1.内存管理的时候,代码和数据的内存块都是碎片化的,处于无序的状态,而进程地址空间是有序的,页表将无序的物理内存转换为有序的进程地址空间,进程操作内存空间会更方便。
2.内存管理和进程管理互相独立、不影响,这种解耦合的做法会大大减轻设计操作系统的负担。
3.进程地址空间+页表的设计某种意义上也保护了物理内存。
- 总结
1.这张图,解释了本文刚开始的问题,为什么地址相同的变量会有不同的值,这是不可能的事情,本质上还是不同的物理空间,只是中间多了一层虚拟地址。
2.也能清楚的说明为什么fork的返回值会有两个,本质上还是虚拟地址通过页表映射的两个物理地址。
3.进程之间是独立互不影响的。通过这张图,进程=PCB+代码数据,PCB、进程地址空间、页表等都是每一个进程各自有一份独立的,而代码内容是只读的,可以共享,数据内容通过写时拷贝各自私有一份,故此有了结论:进程是独立的。
4.malloc、new申请空间的实际过程:
malloc、new申请空间后,用户不一定会立即使用这个空间做写入操作。所以,操作系统考虑到有限的内存资源,让用户层在malloc、new后不一定是拿到了物理地址。
malloc、new的地址是进程地址空间上的虚拟地址,在堆区。
当用户层对这个空间要做写入操作时,操作系统会先中断这个申请写入的操作,然后在物理内存上开辟对应的空间,并且建立页表的映射关系。这个中断操作称为缺页中断。
操作系统将申请空间的操作分开做处理,保证了物理内存的使用率,不会空转,也提升了某种场景下malloc、new的申请速度。