需要云服务器等云产品来学习Linux的同学可以移步/-->腾讯云<--/-->阿里云<--/-->华为云<--/官网,轻量型云服务器低至112元/年,新用户首次下单享超低折扣。
目录
一、虚拟地址
先看一段父子进程共存的程序,由子进程对全局变量grobal_val进行修改:
#include <stdio.h>
#include <unistd.h>
int grobal_val=10;
int main()
{
pid_t id=fork();
if(id==0)
{
int cnt=0;
while(1)
{
printf("子进程:pid=%d,ppid=%d | grobal_val=%d,&grobal_val=%p\n",getpid(),getppid(),grobal_val,&grobal_val);
sleep(1);
++cnt;
if(cnt==10)
{
grobal_val=200;
printf("子进程已更改全局变量grobal_val\n");
}
}
}
else if(id>0)
{
while(1)
{
printf("父进程:pid=%d,ppid=%d | grobal_val=%d,&grobal_val=%p\n",getpid(),getppid(),grobal_val,&grobal_val);
sleep(1);
}
}
else
{
printf("fork error\n");
return 1;
}
return 0;
}
父子进程谁先执行不确定,由系统进行调度。
当子进程将全局变量grobal_val由10改为200,我们可以看到,父子进程的grobal_val的地址相同,但是父子进程从这个地址中获取的值却并不相同!
从同一块物理地址中取出的值是相同的,所以这个程序取出的地址(指针)并不是物理地址,而是虚拟地址(线性地址、逻辑地址)。注:逻辑地址指可执行程序编译完成后内部函数、变量的地址。逻辑地址有两种表示方法,一种是各个区域地址递增,另一种是每个区域的地址都从零偏移量开始(这种是比较老的表示方式)。
在Linux中的逻辑地址是第一种表示方式,所以Linux中逻辑地址就是虚拟地址。
之前学习的C/C++内存区域,是一块虚拟内存空间,每个进程有它自己的虚拟内存空间,即进程地址空间。所以上面的代码用fork创建子进程,因为子进程是父进程的拷贝,父子进程的grobal_val虽然虚拟地址一样,但会被映射到不同的物理地址上。
当grobal_val未被改变时,父子进程映射同一块grobal_val的物理地址,一旦父子进程的一方对共享数据进行修改,由于进程的独立性,操作系统会在物理内存中再开辟一块空间,并拷贝原数据,提出修改的进程的页表映射关系将会被改变,然后再让进程对数据进行修改,所以我们看到父子进程的数据并不一样。这种技术称为写时拷贝,对不同进程的数据进行分离。
二、对进程地址空间的理解
1、进程它自己会认为它独占CPU资源,但其实并不是。因为进程以时间片轮转的形式占用CPU资源,时间一到,马上从运行状态进入休眠状态,实质上是通过虚拟地址空间,让进程认为它独占CPU资源。
2、进程地址空间是操作系统给进程开辟的一块虚拟内存空间,这块空间用内核的一种数据结构来描述、组织。
操作系统给每个进程一块4GB的虚拟内存,进程每次想使用,按需申请即可,但不会全部给进程。(注意这里给的是虚拟内存,就像老板给员工画饼一样)
对Linux操作系统中进程的理解中提到过,进程使用进程控制块task_struct结构体进行管理,同样的,每个进程地址空间也需要被管理,管理进程地址空间的结构体叫mm_struct,task_struct中有一个指针指向自己的mm_struct。
mm_struct伪代码:
struct mm_struct
{
uint32_t code_start,code_end;
uint32_t data_start,data_end;
uint32_t heap_start,heap_end;
uint32_t stack_start,stack_end;
······//存储进程地址空间各区域的起始位置
};
三、32位下的进程地址空间
地址空间中的最小单元是字节,所以在32位系统下共有个地址空间,也就是4GB。
mm_struct结构体对象中存放各个区域的起始位置,栈区堆区的动态调整,本质上是修改各个区域的起始地址。
那么进程如何找到内存中的数据呢?
操作系统将进程中的虚拟地址通过页表映射到内存,找到对应的物理地址。
四、为什么要通过虚拟地址映射的方式访问物理地址
1、直接访问物理内存是非常不安全的,例如越界操作、恶意进程读取等。
2、页表会拦截不合理的请求,可以保护物理内存,防止恶意进程的访问 。
所以写代码出现野指针、内存越界等情况并不会造成操作系统的崩溃。
3、进程地址空间的存在,可以让进程和进程间的代码进行解耦(互不干扰),保证了进程独立性的特征。
4、进程和编译器均遵守进程地址空间这一套规则,编完即可使用。
编译器也遵守进程地址空间这一套规则:
我们的代码在磁盘时,程序的函数、变量等通过虚拟地址建立联系,满足程序间的互相跳转;
当程序由磁盘被加载到内存中时,就具备了物理地址。函数、变量等通过页表映射至虚拟地址。
根据可执行程序的虚拟地址初始化mm_struct结构体中每个虚拟内存中的边界。
当程序在CPU中跑起来时,CPU根据虚拟地址运行完程序后,通过页表映射至物理地址。