进程地址空间
进程的地址空间是进程可用于寻址内存的地址集合, 包括进程的物理地址空间和虚拟地址空间。操作系统有虚拟内存和物理内存的概念, 但是在很久之前, 操作系统只有物理内存的概念, 程序寻址都是用的物理地址, 寻址空间的大小取决于 cpu 地址线条数, 在 32 位的操作系统上, 寻址的范围是固定的(最多 4G).。也就是说, 每次运行一个程序, 都会给进程分配 4G 的物理内存, 这样就带来了很多麻烦:
- 内存浪费: 每个进程固定分配 4G 物理内存, 对内存要求太高; 并且物理内存的分配是连续的且随机的, 不能充分利用物理内存, 进而造成内存浪费
- 效率低下: 内存得不到充分利用, 就会产生过多的进程等待, 等待其他进程 结束后再被载入内存, 操作频繁, 效率低下
- 安全性低: 进程中的指令都是直接访问物理内存的, 这就可能影响其他进程 甚至破坏内核空间, 安全性低
虚拟地址空间
-
虚拟地址空间概念:
虚拟地址空间就是操作系统为进程所描述的一个假的地址空间,目的是为了让进程认为自己拥有一块连续的线性的完整的地址空间。但是实际上一个进程使用的内存并不是连续存储的,而是通过页表映射了虚拟地址与物理地址之间的关系。让进程通过页表获取物理地址,进而实现数据的离散式存储。
-
虚拟地址空间组成:
虚拟内存空间包括内核空间和用户空间, 在 32 位的 Linux 操作系统上, 1G(高地址)是内核 空间, 3G 是用户空间。
-
虚拟地址空间作用:
①:提高内存利用率(物理内存的离散存储)
②:保证了进程的独立性(每个进程都只能访问自己虚拟地址映射的物理内存)
③:页表可以进行内存访问控制(页表可以对每个虚拟地址进行权限标记)
虚拟内存工作流程
- 1.创建一个进程得到PCB(进程控制块(即task_struct这个结构体))
- 2.在 task_struct 中可以找到 内存描述符( mm_struct )。mm_struct 是用来描述进程地址空间的, 每一个进程都有唯一的进程地址空间, 即每个进程拥有唯一的 mm_struct 结构体,通过 mm_struct 将可执行程序映射到虚拟内存空间。
- 3.建立虚拟内存和物理内存的映射
写时拷贝技术
- 父进程创建一个子进程,父子进程分别打印自己的pid、value值、value地址。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main() {
int var = 1;
pid_t pid = fork();
if (pid < 0) {
perror("fork error");
exit(-1);
} else if (pid == 0) {
printf("child &var = %p\n", &var);
printf("child var = %d\n", var);
} else {
printf("parent &var = %p\n", &var);
printf("parent var = %d\n", var);
}
return 0;
}
程序运行如下:
结果发现父子进程打印的变量值和地址都相同,因为子进程并未对变量进行任何修改,我们在修改上述代码,在子进程中将 var 值改为 2,再次运行:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int var = 1;
pid_t pid = fork();
if (pid < 0) {
perror("fork error");
exit(-1);
} else if (pid == 0) {
var = 2;
printf("child &var = %p\n", &var);
printf("child var = %d\n", var);
} else {
printf("parent &var = %p\n", &var);
printf("parent var = %d\n", var);
}
return 0;
}
程序运行结果:
这时候,父子进程输出地址是一致的,但是变量内容不一样,可以说:
变量内容不一样,所以父子进程输出的变量绝对不是同一个变量 ,但地址值是一样的,说明该地址绝对不是物理地址! 在Linux地址下,这种地址叫做 虚拟地址 ,我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理
所以有父子进程”代码共享,数据独有“:
- 代码共享:由于代码是只读的,不能被修改(只读的访问控制由页表来控制),子进程和父进程代码会一直映射同一块物理内存,所以这就是代码共享。
- 数据独有:但是在子进程中修改变量的值, 却不影响父进程中变量的值,子进程中变量发生改变时,此时才会新开辟物理空间给子进程使用,并且改变页表到物理地址的映射关系。
为了理解 “代码共享,数据独有” ,需要理解 写时拷贝技术:
写时拷贝故名思意:是在写的时候(即改变字符串的时候)才会真正的开辟空间拷贝(深拷贝),如果只是对数据的读时,只会对数据进行浅拷贝。
写时拷贝:引用计数器的浅拷贝,又称延时拷贝。
:写时拷贝技术是通过"引用计数"实现的,在分配空间的时候多分配4个字节,用来记录有多少个指针指向块空间,当有新的指针指向这块空间时,引用计数加一,当要释放这块空间时,引用计数减一(假装释放),直到引用计数减为0时才真的释放掉这块空间。当有的指针要改变这块空间的值时,再为这个指针分配自己的空间(注意这时引用计数的变化,旧的空间的引用计数减一,新分配的空间引用计数加一)。
写时拷贝是一种可以推迟甚至避免拷贝数据的技术。子进程拷贝了父进程的PCB和页表,当子进程中变量发生改变时,此时才会新开辟物理空间给子进程使用,并且改变页表到物理地址的映射关系(虚拟地址没变,但映射的物理地址已经变化)
当子进程中改变变量 var 的值的时候,通过写实拷贝技术开辟一块新的物理空间给子进程使用,并将变量a的值拷贝到新的物理空间并改变其值为2,并且改变页表到物理地址的映射关系:
创建子进程的过程:
①:fork( )通过复制调用进程(就是复制了PCB),创建一个新的进程(子进程)
②:子进程拷贝父进程PCB中的数据(拷贝后子进程与父进程有相同的虚拟地址空间,相同的页表)
③:父子进程一开始映射同一块物理内存
④:等到物理内存内容修改的时候,系统才会为子进程重新开辟一块物理内存,拷贝数据过来。