目录
- 程序地址空间回顾
- 进程地址空间
- 什么是进程地址空间?
- 进程地址空间与PCB、物理内存、页表和磁盘之间的关系
- 为什么要存在虚拟地址空间?
- 重新理解地址空间
程序地址空间回顾
了解进程的运行:
1 #include <stdio.h>
2 #include <unistd.h>
3
4 int main()
5 {
6 pid_t id = fork();
7 if(id<0)
8 {
9 printf("fork error\n");
10 return 1;
11 }
12 else if(id==0)
13 {
14 printf("子进程\n");
15 }
16 else
17 {
18 printf("父进程\n");
19 }
20 return 0;
21 }
运行结果:我们会发现这打印的结果乱七八糟,因为它也不知道什么时候该干什么
我们让代码睡眠1秒:打印的结果就正常了
以前我们学习的内存管理(程序地址空间):
为了验证上面虚拟地址,我们运行下面代码:
int global_value = 100;
int main()
{
pid_t id = fork();
if(id < 0)
{
printf("fork error\n");
return 1;
}
else if(id == 0)
{
int cnt = 0;
while(1)
{
printf("我是子进程, pid: %d, ppid: %d | global_value: %d, &global_value: %p\n", getpid(), getppid(), global_value, &global_value);
sleep(1);
cnt++;
if(cnt == 10)
{
global_value = 300;
printf("子进程已经更改了全局的变量啦..........\n");
}
}
}
else
{
while(1)
{
printf("我是父进程, pid: %d, ppid: %d | global_value: %d, &global_value: %p\n", getpid(), getppid(), global_value, &global_value);
sleep(2);
}
}
sleep(1);
}
(这种问题出现的原因在下面的为什么要存在虚拟地址空间有讲述)
多进程在读取同一地址的时候,怎么可能出现不同的结果?
- 变量内容不一样,所以父子进程输出的变量绝对不是同一个变量
- 这里地址没变化,说明这里的地址一定不是物理地址,我们以前学习的语音的基本地址(指针)不是对应的物理地址,而是虚拟地址(线性地址)(逻辑地址),这三种说法在Linux之中是相等的,但是在别的地方它们是不同的概念。
- 在Linux地址下,这种地址叫做 虚拟地址,我们在用C/C++语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理
打印出来的地址空间排布,全部都是虚拟地址
感性理解虚拟地址空间:
我们将进程地址空间类比 “大饼” ,而这个“大饼”就是操作系统给进程画的,进程它认为自己是独占系统资源(事实上并不是)。假如我是老板(操作系统),我要给我的员工(进程)画饼,员工(进程)要被管理,每个员工的大饼不尽相同,那我们给员工画的大饼(地址空间)也要被管理,进程要被管理-先描述再组织。所以说,地址空间的本质:是内核的一种数据结构(mm_struct)(PCB也是内核的一种数据结构)
进程地址空间
所以之前说‘程序的地址空间’是不准确的,准确的应该说成 进程地址空间
什么是进程地址空间?
在操作系统中,进程地址空间是进程可以访问的虚拟地址范围。它是操作系统分配给进程的连续虚拟地址块。进程地址空间用于存储进程的代码、数据和堆栈。
每个进程都有自己的专用地址空间,这意味着没有两个进程可以访问同一个虚拟地址。这样做是为了确保进程不会相互干扰。操作系统使用称为内存保护的机制来强制实施此分离。
进程地址空间分为多个区域,每个区域都有不同的用途。例如,代码区域包含进程的可执行代码,数据区域包含进程的数据,堆栈区域包含进程的调用堆栈。
操作系统通过将虚拟地址映射到物理地址来管理进程地址空间。这是使用称为页表的数据结构完成的。页表是将每个虚拟地址映射到物理地址的表。当进程尝试访问虚拟地址时,操作系统会在页表中查找该地址以查找相应的物理地址。
进程地址空间是操作系统中的一个关键概念。它允许操作系统将进程彼此隔离并管理系统的内存资源。
有关进程地址空间的一些其他注意事项:
- 进程地址空间的大小通常受系统上可用的物理内存量的限制。
- 操作系统可以使用各种技术来管理进程地址空间,例如分页和交换。
- 进程地址空间可以使用共享内存和进程间通信 (IPC) 等机制在进程之间共享。
如何使用进程地址空间的一些示例:
- 创建进程时,操作系统会为其分配新的进程地址空间。
- 从文件加载进程时,操作系统会将文件的内容映射到进程的地址空间。
- 当进程进行系统调用时,操作系统会将进程的虚拟地址转换为物理地址,然后执行系统调用。
- 当进程终止时,操作系统将释放进程的地址空间。
如何理解区域划分:
- 地址空间描述的基本空间大小是字节
- 32位下,2^32次方个地址 、2^32 * 1字节 = 4GB空间“范围”
- 每一个字节都要有唯一的地址
- 2^32个地址是虚拟地址,只要保证唯一性即可
实例出mm_struct的对象:
定义局部变量,malloc new堆空间,是扩大栈区或堆区
函数调用完毕,free,是缩小栈区或堆区
这个mm_struct就是操作系统给进程画的“大饼”,告诉进程这4G空间全是给你的。
进程地址空间与PCB、物理内存、页表和磁盘之间的关系
进程地址空间是进程内存的逻辑视图。它是操作系统分配给进程的连续虚拟地址范围。进程地址空间用于存储进程的代码、数据和堆栈。
PCB(过程控制块)是一种数据结构,其中包含有关进程的信息,例如其进程ID,父进程ID,状态,优先级和地址空间。(Linux操作系统下的PCB是: task_struct)
物理内存是计算机的实际内存。它用于存储当前正在运行的所有进程的代码,数据和堆栈。
页表是将虚拟地址映射到物理地址的数据结构。操作系统使用它来将进程使用的虚拟地址转换为硬件可以访问的物理地址。
磁盘是辅助存储设备。它用于存储当前未运行的代码,数据和进程堆栈。
进程地址空间与PCB、物理内存、页表、磁盘的关系如下:
- PCB包含一个指向进程地址空间的指针。
- 进程地址空间由操作系统使用页表映射到物理内存。
- 如果没有足够的物理内存可用,操作系统可以将页从进程地址空间交换到磁盘。
-
创建进程时,操作系统会为其分配新的进程地址空间。进程地址空间最初为空。然后,操作系统将进程的代码和数据从磁盘加载到进程地址空间中。
-
当进程运行时,操作系统使用页表将进程地址空间映射到物理内存。这允许进程访问其代码、数据和堆栈。
-
当进程未运行时,操作系统可能会将页从其进程地址空间交换到磁盘。这将为其他进程释放物理内存。
-
恢复进程时,操作系统使用页表将页从其进程地址空间加载回物理内存。这允许进程从中断的位置继续运行。
进程地址空间、PCB、物理内存、页表和磁盘都是现代操作系统的重要组成部分。它们协同工作以允许多个进程在一台计算机上同时运行。
Linux内核代码:
这个就是存放起始值:
磁盘与内存的IO,进程虚拟地址的工作原理:
操作系统给每个进程画的“大饼”:就是每个地址空间都是2^32。实际上OS每次只会给你申请那么多,它也知道你其实也差不多就用那么多,你要是申请多了,就不让你申请成功,而不是真的给每个进程都4GB空间。
为什么要存在虚拟地址空间?
- 如果让进程直接访问物理内存,万一进程非法越界操作呢?
- 地址空间的存在,可以更方便的进行进程和进程的数据代码的解耦,保证了进程独立性这样的特征
我们分析一下上面出现的这种问题:
子进程是由父进程为模板创建的,把父进程的PCB拷贝给子进程,把父进程的地址空间也拷贝给子进程,因为是拷贝过来的,所以在同样的地址有这个global_value。
因为进程具有独立性,一个进程对被共享的数据做修改的时候,如果影响了其他的进程,那么就不能称为独立了。所以,当子进程要修改global_value的值的时候,在物理内存中会首先将它的值拷贝一份在另外的空间,然后将子进程的页表的映射更改为这个拷贝后的空间的地址,然后再更改它的值,整个过程不会影响到父进程的值,同时也没有影响虚拟地址空间的地址,所以我们父子进程打印出来的&global_value的地址没有变换。
上面的这种方式叫做写时拷贝,他是操作系统自动做的,对不同进程的数据进行分离。操作系统为了保证进程的独立性,做了很多工作,通过地址空间,通过页表,让不同的进程,映射到不同的物理内存处。
进程 = 内核数据结构 + 进程对应的代码和数据 (两个都是独立的,所以说进程具有独立性)
- 让进程以统一的视角,来看待对应的代码和数据等各个区域,方便使用编译器也已统一的视角来进程编译代码(规则是一样的,编完即可直接使用)
重新理解地址空间
我们写一个调用函数:
int a = 10;
fun()
{
use a;(仅代表使用a)
}
main()
{
fun();
}
且看下面调用逻辑:
总结:
- 我们的可执行程序在没有加载到内存的时候在内部已经有地址了,是在代码的编译期间按照2^32(32位机器)空间进行编址,属于是逻辑地址。所以我们平时说的在32位机器下编译,在64位下编译,差别就是编址空间不同。
- 虚拟地址空间,不要只认为操作系统会遵循对应的规则,编译器也要遵守,编译器在编译代码的时候,就是按照虚拟地址空间的方式对我们的代码和数据进行编址的。
- 我们将磁盘的代码加载到物理内存,就天然具备了一个物理地址。此刻我们有两套地址,一个是标识物理存在中的代码和数据的地址,一个是程序内部互相跳转的时候的地址-虚拟地址
- debug运行起来CPU内部使用的都是虚拟地址
程序运转:
- 我们首次运行不是将main函数的物理地址加载进入CPU,而是靠mm_struct里面已经对应好的start将其虚拟地址加载入CPU
- main函数的虚拟地址加入后我们通过地址空间,然后页表映射到物理内存上,我们找到main函数后向下执行到调用fun(),然后是将fun();这个指令读取到CPU内部,而不是直接加载物理地址,这个指令内部就有地址(虚拟地址),然后重复上述运行。
- 整个过程中CPU都没有见到物理内存
感性的理解上述虚拟地址、页表、物理地址关系:
我们将虚拟地址认为是你的学号,页表为你的班长,你的宿舍号就是物理地址。你住进学校宿舍就天然就有了一个宿舍号(物理地址),你的辅导员(CPU)使用学号(虚拟地址)需要找到你,就通过你的班长(页表),来找到你的宿舍号最后找到你,你的辅导员(CPU),不关心你的宿舍号(物理地址)是什么,只关心你的本人学号(虚拟地址)。
进程地址空间:
如有错误或者不清楚的地方欢迎私信或者评论指出🚀🚀