目录
一、验证进程地址空间
二、感知进程地址空间的存在
一、验证进程地址空间
我们之前学的 C/C++ 程序地址空间是物理内存吗?
答:不是物理内存,甚至叫做程序地址空间都不太准确,应该叫做进程地址空间,因此根本就不是 C/C++ 上的概念,而是操作系统上的概念、
在 Linux 操作系统下验证进程地址空间:
在 Winodws 操作系统下无法验证进程地址空间,原因:
1、与 Windows 操作系统本身的设置有关、
2、与 Windows 操作系统所使用的编译器也有关系,编译器会对代码地址空间做调整,防止代码被恶意猜测,比如:栈随机化策略、
[LCC@hjmlcc ~]$ ls
a.out hjm.c process.c test.c
[LCC@hjmlcc ~]$ ./a.out -a -b -c
Code addr :0x40057d
Init global addr :0x60103c
Uninit global addr :0x601044
Heap addr :0x812010
Stack addr :0x7ffc3f1db3c0
Argv addr :0x7ffc3f1db7c6
Argv addr :0x7ffc3f1db7ce
Argv addr :0x7ffc3f1db7d1
Argv addr :0x7ffc3f1db7d4
Env addr :0x7ffc3f1db7d7
Env addr :0x7ffc3f1db7ed
Env addr :0x7ffc3f1db7fd
Env addr :0x7ffc3f1db808
Env addr :0x7ffc3f1db818
Env addr :0x7ffc3f1dbfe6
[LCC@hjmlcc ~]$
验证堆区和栈区的增长方向的问题:
二、感知进程地址空间的存在
注意:使用 Linux 操作系统提供的系统调用接口 fork 创建出当前进程的子进程,若启动当前进程,那么当前进程和当前进程的子进程启动的先后顺序是不确定的,到底是谁先谁后启动,取决于 CPU 先运行了谁,通过实验可知,一般都是父进程先被启动,子进程再被启动,但并代表父进程一定会先被启动,记住即可、
#include<stdio.h>
#include<unistd.h>
int g_val=100;
int main()
{
pid_t id=fork();//不考虑创建当前进程子进程失败的情况、
if(id == 0)
{
//子进程
while(1)
{
printf("我是子进程:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
sleep(1);
}
}
else{
//父进程
while(1)
{
printf("我是父进程:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
sleep(2);
}
}
return 0;
}
注意:此时父子进程读取到的普通全局变量是 同一个 变量、
现做如下修改:
1 #include<stdio.h>
2 #include<unistd.h>
3 //普通全局变量、
4 int g_val=100;//以普通局部变量举例也是可以的、
5 int main()
6 {
7 pid_t id=fork();//不考虑创建当前进程子进程失败的情况、
8 if(id == 0)
9 {
10 //子进程
11 int flag=0;
12 while(1)
13 {
14 printf("我是子进程:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
15 sleep(1);
16 flag++;
17 if(flag==5)
18 {
19 g_val=200;
20 printf("我是子进程,普通全局变量的值已被修改,请注意\n");
21 }
22 }
23 }
24 else{
25 //父进程
26 while(1)
27 {
28 printf("我是父进程:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
29 sleep(2);
30 }
31 }
32 return 0;
33 }
由上图可知,父子进程读取到的普通全局变量的地址相同,但是读取到的普通全局变量的值不同,因此我们可知,此处普通全局变量的地址一定不是物理地址,不然肯定不会出现这种情况,虽然上述父子进程读取到的普通全局变量的地址相同,但父子进程各自读取到的普通全局变量并不是 同一个 变量,所以,此处的普通全局变量的地址一定是虚拟地址、
我们之前在 C/C++ 中所谓的地址,都不是物理地址,都是虚拟地址;虚拟地址在 Linux 操作系统下又被称为:线性地址、逻辑地址;这三个概念在 Linux 操作系统下是一样的概念,但是不在Linux 操作系统下,是三个完全不一样的概念,这和 Linux 操作系统本身的空间布局有关、
本节所讲的进程地址空间,本质上就是虚拟地址空间,因此本小结也可被称为:感知虚拟地址空间的存在、
操作系统不让我们直接看到或访问物理内存的原因:不安全;内存(物理内存,不存在虚拟内存的概念,物理地址对应物理内存)是一个硬件,不能阻拦我们进行访问,只能被动的进行读取和写入;由野指针或越界等原因造成程序崩溃的情况并不是由内存(物理内存)造成的,而是由操作系统在程序与内存(物理内存)之间添加的软件层造成的、
每一个进程在启动时,操作系统都会给其创建一个进程地址空间(虚拟地址空间),操作系统会对这若干个进程地址空间进行管理(先描述再组织);所谓的进程地址空间,其实本质上就是内核(操作系统)的一个数据结构,也存在于内存(物理内存)中,在 Linux 操作系统中,也是一个描述进程地址空间的 struct 结构体( struct mm_struct )、
所谓的进程地址空间其实就是操作系统通过软件的方式,给进程提供一个软件视角,使得每一个进程都认为其独占操作系统的所有资源(物理内存)、
前面我们知道进程是具有独立性的,体现在相关的内核数据结构是独立的和进程的代码和数据是独立的;类比于一位海王同时撩三个女的(广撒网,钓大鱼),并对每一个女的说我只中意你,且画大饼说以后对你怎么怎么好……,从而使得每个女的都天真的认为我是不可替代的那个人,这个例子中,海王充当的就是操作系统OS,三个女的就是三个进程,海王画的大饼就是进程地址空间,海王画大饼的原因在于为了维护这三个女(三个进程)的独立性,互补干扰,若有交集则必然乱套、
所谓的进程就是:进程等于加载到内存(物理内存)中的可执行程序(代码及数据)加上该进程对应的内核数据结构(该进程对应的描述该进程所使用的 struct 结构体(task_struct),其也存在于内存(物理内存)之中)的组合、
在 Linux 内核中,每个进程都有 task_struct 结构体,该结构体中有个指针指向一个结构mm_struct (进程地址空间),我们假设磁盘上的一个可执行程序被加载到物理内存,之后,操作系统会给每一个进程构建一个页表结构(映射表),我们需要将虚拟地址空间(进程地址空间)和物理内存之间建立映射关系,这种映射关系是通过页表(映射表)的结构完成的,如下图:
// Linux 内核中的进程地址空间、
struct mm_struct
{
struct vm_area_struct* mmap; // list of VMAs
...
...
}
strcut vm_area_struct
{
struct mm_struct* vm_mm;
unsigned long vm_start;
unsigned long vm_end;
...
...
}
磁盘中的可执行程序里是有地址的,因为,在链接阶段就是把多个目标文件和链接库通过地址链接起来,从而生成可执行程序的,因此可执行程序里面是由地址的,在该可执行程序中,包括代码区,已初始化全局数据区,未初始化全局数据区等等,这些区域在划分时,采用的不是在物理内存中的地址,采用的是相对地址(相对于整个可执行程序最开始的地址),当该可执行程序被加载到物理内存中(0x00000000 - 0xFFFFFFFF)时,
而堆区和栈区是等该可执行程序被加载到物理内存中才会存在的,
也是有区域的,在 Linux 命令行中输入:readelf -S a.out 即可查看、