进程虚拟地址空间

关键词:进程虚拟地址空间,进程描述符,页表,分段式,段页式。

在进入正式的内容之前,我们先了解一个重要的概念——进程描述符PCB。

在Linux操作系统中,描述进程的结构体叫做task_struct。Linux操作系统通过task_struct感知进程的存在。

task_struct结构体中的内容:

标识符:描述本进程的唯一标识符,又来区别本进程与其他进程。

状态:进程的状态,退出信号等等

优先级:相对于其它进程的优先级。

程序计数器:存储着程序中下一条将要执行的指令的地址。

内存指针:包括程序代码及进程相关数据的指针,还有其它进程共享的内存块的指针。

上下文数据:进程执行时处理器的寄存器中的数据。

IO状态信息:包括显示IO请求,分配给进程的IO设备和被进程使用的文件列表。

记账信息:可能包括处理器时间总和,使用的时钟总和,时间限制,记帐号等。

其他信息 

我们本篇所主要讨论的便是task_struct结构体的内存指针所指向的空间。

以32位操作系统为例,由之前所学的基础知识我们很容易知道,所谓的“位”指的是比特位,只有0与1两种状态,而32位,则代表着它有着32根地址线,每一根地址线都有着0和1两种状态,所以对应的,其地址可以由0x0000 0000表示到0xffff ffff,共计4G。 

 进程虚拟地址空间_第1张图片

以上便是task_struct结构体中内存指针所指向的进程地址空间

需要注意的是,对于每一个进程,操作系统都会给它们分配一个task_struct结构体,对应的,也会有一片进程地址空间。

然而,从硬件的角度讲,我们的CPU只有4G的内存……操作系统却给了每个进程4G的空间,这意味着操作系统给所有进程画的大饼实际上是n*4G的内存,这是远远大于我们真实可用的空间的。

那么,为什么在操作系统画的大饼远大于实际饼的大小的情况下,我们的电脑却能流畅的运行呢?

这就不得不提到进程地址空间的另一个名字了——进程虚拟地址空间

虚拟,就是假设的,不符合或不一定符合事实的。

操作系统给每个进程都画了一张4G的饼,但毕竟一个进程是几乎不可能一下子使用4G的内存,绝大多数进程它们就算拼尽全力,所使用的空间,也是远小于4G的,这就给了操作系统很大的操作空间,它通过自己的方式精细规划,合理地给进程分配真正可以使用的物理内存,从而使计算机可以同时运行多个进程。

操作系统所存储数据地物理内存的地址我们是不得而知的,我们通过程序所得到的地址往往是操作系统给进程分配的进程虚拟地址空间中的虚拟地址。

int main() {
    int i = 10;
     int fd = fork();
     if (fd > 0) {
         i = 5;
         sleep(1);                                                                                                                                                                      
         printf("pid:%d, &i:%p, i= %d\n", getpid(), &i, i);
     } else {
         printf("pid:%d, &i:%p, i= %d\n", getpid(), &i, i);
     }
     return 0;
 }

 图片为以上程序运行结果,很容易看到,在不同的进程中,i的地址是相同的,但是它的值却是不同的。由此可以佐证,我们通过程序获取的并不是变量真实存储的物理内存。

那么为什么操作系统要给每一个进程都虚拟出一个进程地址空间?而不让进程直接访问物理内存?

因为各个进程方位同一片物理地址空间,就会导致不可控。在有限的物理地址空间中,进程没有办法知道哪一片内存是空闲的,哪一片内存是被其它进程使用的,这种时候,如果冒昧的使用,就会导致多个进程在访问物理内存时出现混乱。所以,物理内存要由操作系统统一管理起来,采用预先直接分配内存的方式分配给进程。

 为什么要给每个进程都分配4G进程地址空间(32位操作系统)?

因为操作系统不清楚进程会使用多少内存,使用多久。所以,操作系统便给每个进程都分配了4G的虚拟地址,在进程真正要保存数据或者申请内存的时候,操作系统再给进程分配相应的物理空间。这样比较合理,也会节省很多的空间,谁用谁分配。每一个进程的虚拟地址对应的物理地址是操作系统在背后转换的。

那么操作系统究竟是怎样完成虚拟地址与物理地址间的转换的?

页表

操作系统将虚拟进程地址空间分为一页一页的小块(通常一页为4096字节)

将物理内存分成一块一块的小块,大小与虚拟地址空间的一页相同。

页表则是记录进程虚拟地址空间中的页与物理内存中的块的映射关系。

进程虚拟地址空间_第2张图片 

 虚拟地址相关计算

虚拟地址 = 页号 + 页内偏移

页号 = 虚拟地址 / 页的大小

页内偏移 = 虚拟地址 % 页的大小

 为什么内存分配的时候不采用连续的方式,而是一页一页离散的方式?

为了防止产生内存碎片。

假设总共有8M的内存,如果将内存以2M,4M,2M的方式分配给3个进程。如果两个占用2M内存的进程终止了,会释放出4M的空闲空间。如果此时再开始一个需要4M内存的进程,因为采用的是连续的分配方式,此时内存中并没有大小为4M连续空间,所以新的4M进程无法产生。此时我们可以认为那两块分明存在且无法申请的2M内存为内存碎片。

PS.个人语言解释如此。

其他虚拟地址转换物理地址方法。

分段式

虚拟地址 = 段号 + 段内偏移

段表 段号:段的起始地址

进程虚拟地址空间_第3张图片 

段页式

虚拟地址 = 段号 + 页号 + 页内偏移

段表 段号:段的起始地址

页表 页号:块号 

 进程虚拟地址空间_第4张图片

 通过段号,找到页表的起始位置,从而找到具体的页表。

通过页表中的页号,找到对应的块号

通过具体的物理内存块号,加上页内偏移,锁定具体的物理内存。

你可能感兴趣的:(笔记,Linux基础学习,linux)