Linux 进程(二) 进程地址空间
上一节我们提到过父子进程的一个概念:父子进程代码共享,数据各自开辟空间。
因为子进程从父进程的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,父进程会不会发生变化,他们的地址又是否相同。
因为子进程运行的位置和父进程一样,所以先让父进程睡眠一会,让子进程先修改
奇怪的事情发生了,明明子进程已经修改了val,但是父进程的却没变,同时明明父子进程中全局变量val的大小都不一样,发生了变化,但是他们的地址确还是一样的,这就有些不符合逻辑了,因为一个地址中不可能有两个同名的变量。
这里就让我们确定了一件事情,我们在代码中所看到的地址,并不是真正的地址。
这就引入了程序地址空间的概念。
进程地址空间
程序是不占用内存的,它只是一个没有生命的实体, 只有运行起来的程序(进程)才会被加载到内存中,这才会占用内存。
地址:地址就是对内存单元的编号,通过这个编号来访问数据。
从上面的例子我们发现,代码中看到地址并不是真正的内存地址,而是虚拟内存地址。
为什么要创建这样一个虚拟的内存地址呢?
操作系统为了不让进程直接访问物理内存,通过mm_struct结构体来为进程描述了一个虚拟的,连续的,完整的地址空间(只有编号,无法存储),也就是我们所说的虚拟地址空间。
为什么不让进程直接访问物理内存呢?
假设我们内存中有6m的空间,其中已经存入了3m,这时我们想再存入一个3m,但是问题来了,因为物理空间还剩下的3m是不连续,所以这时会再找一个连续的空间来存储这个3m。这样就造成了内存的大量浪费。
还有这样一种情况。
当几个进程同时访问物理内存时,各进程的操作可能会产生冲突,可能会产生无法预料的后果。缺乏访问控制的内存是非常不安全的。
页表
操作系统再引入虚拟地址空间的时候还引入了一种东西,叫做页表。
通过页表来映射虚拟地址和物理地址的关系。
-
通过在虚拟地址来使数据进行连续的存储,然后再通过页表映射到物理内存上,来实现离散式的存储,提高了内存的利用率。
-
同时页表可以针对某个地址设置访问权限,让某个地址设置为只读,通过这种方法来实现内存的访问控制。
-
为了能够使进程具有独立性,彼此之间不会相互干预,每一个进程都会有它自己的页表和虚拟地址空间。
回到最开始的问题。
为什么父子进程的代码相同,且无法修改?
:因为通过页表将代码段的权限设置为只读,所以无法修改。
为什么父子进程数据各自开辟空间?
:其实父子进程一开始物理地址和虚拟地址都是相同的,但是当任意一个进程中数据发生变化的时候,这个时候操作系统会找到另外一块物理空间,将数据全部拷贝过去给发生修改的进程使用,并且修改原来的物理空间的权限,使原来的物理空间给另一个进程使用。
上一篇: R语言编写、调用自定义函数
下一篇: 盘点中国最有可能是外星人的九个人!