欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Linux 进程(二) 进程地址空间

程序员文章站 2022-06-12 10:12:48
...

上一节我们提到过父子进程的一个概念:父子进程代码共享,数据各自开辟空间。

因为子进程从父进程的PCB中拷贝了数据,所以它的代码、数据以及运行的位置,都与父进程一模一样。但是为什么这个代码是无法修改的?为什么又需要再各自开辟空间呢?Linux是如何实现权限控制以及空间映射的呢?

我们用这段代码进行试验

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>

int val = 0;

int main()
{
        pid_t id = fork();
        if(id == 0)
        {
                val = 100;
                //子进程
                printf("子进程pid:[%d]  val:[%d] val地址:[%p]\n", getpid(), val, &val);
        }
        else if(id > 0)
        {
                //父进程
                sleep(3);
                printf("父进程pid:[%d]  val:[%d] val地址:[%p]\n", getpid(), val, &val);

        }
        return 0;

}

我们利用一个全局变量val,看看修改子进程中的变量val,父进程会不会发生变化,他们的地址又是否相同。

因为子进程运行的位置和父进程一样,所以先让父进程睡眠一会,让子进程先修改
Linux 进程(二) 进程地址空间
奇怪的事情发生了,明明子进程已经修改了val,但是父进程的却没变,同时明明父子进程中全局变量val的大小都不一样,发生了变化,但是他们的地址确还是一样的,这就有些不符合逻辑了,因为一个地址中不可能有两个同名的变量。

这里就让我们确定了一件事情,我们在代码中所看到的地址,并不是真正的地址。

这就引入了程序地址空间的概念。


进程地址空间

程序是不占用内存的,它只是一个没有生命的实体, 只有运行起来的程序(进程)才会被加载到内存中,这才会占用内存。

地址:地址就是对内存单元的编号,通过这个编号来访问数据。

从上面的例子我们发现,代码中看到地址并不是真正的内存地址,而是虚拟内存地址

为什么要创建这样一个虚拟的内存地址呢?

操作系统为了不让进程直接访问物理内存,通过mm_struct结构体来为进程描述了一个虚拟的,连续的,完整的地址空间(只有编号,无法存储),也就是我们所说的虚拟地址空间

为什么不让进程直接访问物理内存呢?

Linux 进程(二) 进程地址空间
假设我们内存中有6m的空间,其中已经存入了3m,这时我们想再存入一个3m,但是问题来了,因为物理空间还剩下的3m是不连续,所以这时会再找一个连续的空间来存储这个3m。这样就造成了内存的大量浪费

还有这样一种情况。
当几个进程同时访问物理内存时,各进程的操作可能会产生冲突,可能会产生无法预料的后果。缺乏访问控制的内存是非常不安全的。


页表

操作系统再引入虚拟地址空间的时候还引入了一种东西,叫做页表

通过页表来映射虚拟地址和物理地址的关系。
Linux 进程(二) 进程地址空间

  • 通过在虚拟地址来使数据进行连续的存储,然后再通过页表映射到物理内存上,来实现离散式的存储,提高了内存的利用率。

  • 同时页表可以针对某个地址设置访问权限,让某个地址设置为只读,通过这种方法来实现内存的访问控制。

  • 为了能够使进程具有独立性,彼此之间不会相互干预,每一个进程都会有它自己的页表和虚拟地址空间。


回到最开始的问题。

为什么父子进程的代码相同,且无法修改?
:因为通过页表将代码段的权限设置为只读,所以无法修改。

为什么父子进程数据各自开辟空间?
:其实父子进程一开始物理地址和虚拟地址都是相同的,但是当任意一个进程中数据发生变化的时候,这个时候操作系统会找到另外一块物理空间,将数据全部拷贝过去给发生修改的进程使用,并且修改原来的物理空间的权限,使原来的物理空间给另一个进程使用。