1.进程的地址空间
在讲程序地址空间,我们先看一段代码和现象:
#include <stdio.h>
#include <unistd.h>
int gval = 100;
int main()
{
printf("我是一个进程,pid: %d,ppid: %d\n", getpid(), getppid());
pid_t id = fork();
if (id == 0)
{
// child
while (1)
{
printf("我是子进程,pid: %d,ppid: %d,gval: %d,&gval: %p\n", getpid(), getppid(),gval,&gval);
gval++;
sleep(1);
}
}
else
{
//parent
while(1)
{
printf("我是父进程,pid: %d,ppid: %d,gval: %d,&gval: %p\n", getpid(), getppid(),gval,&gval);
sleep(1);
}
}
return 0;
}
一个全局变量,子进程对其进行++,父进程不进行操作,分别对其取地址,最后发现都指向相同的地址空间,但是怎么可能对相同的地址空间进行读取,父进程读的数不变,子进程一直改变,这种情况是不可能产生的。
- 变量内容不⼀样,所以⽗⼦进程输出的变量绝对不是同⼀个变量
- 但地址值是⼀样的,说明,该地址绝对不是物理地址!
1.虚拟地址/线性地址
由于父子进程对相同地址读取,值不一样,我们可以进行推断,这个地址肯定不是物理地址,
如果是物理地址,就不可能不同的进程,一个查出来值不变,一个查出来值在进行改变,因为两地址是一模一样的,所有此处一定不是物理地址。
由此,就可以引出了一个概念:虚拟地址/线性地址,此处打印的地址为虚拟地址。
1.是什么?
在Linux中,会存在一个虚拟地址,本质就是一个内核数据结构对象(类似PCB),
在Linux中以mm_struct命名,就是该虚拟地址空间的内核数据对象,
在虚拟地址中有各个区域,那么这些区域是怎么进行划分维护的呢?
本质就是空间区域的划分!!也就是说,只需要告诉开头与结尾就可以确定一个空间范围。
在mm_struct中对其进行了维护:
start表示开头,end表示结尾,这样就可以划分好区域。
怎么理解地址空间上的地址?
- 地址本来就是一个数子,可以被保存在unsigned long (4字节)中
- 空间范围内的地址,可以随便使用,暂时不需要记录下来
虚拟地址被维护在进程PCB中,当我们创建一个进程时,不仅要创建PCB,还要创建他的虚拟地址空间。
在OS中还存在一张页表,他是其映射作用,虚拟地址通过页表进行映射可以访问到物理地址空间。
再来看上面代码,由于子进程拷贝父进程代码数据,同时也会拷贝task_struct,mm_struct,页表。
由于拷贝,所以子进程与父进程虚拟地址相同,所以此时子进程与父进程指向同一个区域gval,
当子进程要对这个区域的gval进行修改,又不能影响父进程的值,此时就有了写时拷贝,当OS发现子进程要修改的区域与父进程冲突后,才会会进行写时拷贝,在物理内存上新开辟一个空间,如后把gval值拷贝过来,就可以对其进行修改,同时又不影响父进程。那么此时只需要修改子进程通过虚拟地址映射的物理地址即可,这样,在上层观察到父子进程打印的虚拟地址相同,但底层取指向不同的地址空间。
写实拷贝的意义?
因为每次创建子进程,都会拷贝父进程代码数据,消耗比较大,所以对于一些只读的代码就可以不进行拷贝,父子进行代码共享,当子进程或父进程想要对其进行修改,这时OS就会自主进行写时拷贝,达到有效节省空间的目的。
所以 ,进程=内核数据结构(task_struct/mm_struct/页表)+代码和数据
关于页表:
可以理解成跟哈希表一样的结构,可以通过映射来找数据。
页表有很多标记位,有个涉及rwx权限方面,一个是表示所指向的内存空间是否存在。
通过权限进行限定行为,
这段代码为什么不能对其进行修改,是因为这个代码区域被设置为了只读区域,当进行写入时,OS发现违法操作,就会对其直接杀掉。
当进程被创建,是先创建页表,还是先把可执行程序从磁盘加载到物理内存中?
先创建页表,后进行加载程序。在程序加载到内存之前,需要先建立好页表,以便在程序运行时能够准确地将程序中的虚拟地址转换为物理地址。如果先加载程序而没有页表,处理器在执行程序时就无法确定虚拟地址对应的物理地址,程序也就无法正确运行。
另一个标记位isexists,表明当前虚拟地址,物理地址的映射所对应的空间是否在内存中存在,为什么会这样说呢?因为假设一个很庞大的程序,他不可能全不加载进内存,如果全部加载进内存,就会发生浪费,因为半天运行不到后面代码,此时后面代码所站空间就会发生浪费,所以当一个程序加载进内存不是全部一次性加载。
所以这时就需要判断所对应的内存是否存在,如果不存在,要么是还没加载进来,要么是被切换出去了,但是如果要进行访问了,会把数据重新从外设做换入动作。
这个标记位支撑着:1.分配加载,2.挂起等操作。
关于地址空间:
指令readelf ,可以查看可执行程序的信息,
虚拟地址空间也是结构体,也需要初始化,那么怎么初始化呢?
可执行程序进行编译的时候,就已经拿到各个区域大小的信息,mm_struct初始化,从可执行程序来的。可执行程序包括:1.区域分段,2.包含属性,所以操作系统(进程管理),编译原理,可执行程序相互之间有关系。
2.为什么?
为什么存在虚拟地址空间和页表?
1.保护内存
比如为什么叫做野指针,为什么野指针程序就崩溃了?
根本原因,就是野指针,在C/C++中,用的地址是虚拟地址,在转换为物理地址的时候,要么权限不允许,要么不存在对应的映射,所以OS就会杀死这个进程。起到保护作用!!
2.有虚拟地址空间和页表的存在,使进程管理与内存管理在系统层面上进行了解耦合,如果没有其存在,耦合度太高,不好分别进行管理
3.让进程以统一视角进行看待物理内存,可执行程序代码数据加载到物理内存的任意位置处,因为有页表进行映射,就可以让无序变成有序,统一看待物理内存以及各个区域。
3.怎么办
地址空间本质是一个struct mm_struct,所有内容都是OS自主进行完成的,所以只要把进程管理好,地址空间就管理好了。
全局变量,字符常量------具有全局性,在程序运行期间一直存在有效----->为什么?--->在地址空间中,随着进程,一直存在,全局变量的虚拟地址,一直可以被看见!!!
标签:虚拟地址,gval,空间,地址,页表,进程,内存 From: https://blog.csdn.net/2302_80652761/article/details/143820934