文章目录


前言

看到本章内容进程地址空间大家可以没有太了解或者听到过,但是学过C语言的我们都知道程序地址空间(C/C++程序地址空间),因此我们先复习一下程序地址空间!


​正文开始​

一、程序地址空间回顾

1.1 空间布局图

相比大家在学习C语言的时候都见过这份图,但是具体的内容我们并不是很了解

[ Linux ] 进程地址空间_linux


首先有一个问题,我们曾经学过的程序地址空间是内存吗???

答案是 它不是内存!!!

程序地址空间的说法都不太准确,应该叫做进程地址空间!

所以对于进程地址空间是属于操作系统的概念!我们当时学C语言的时候对这部分只是有了一个浅浅的认识,方便我们的理解!

其实程序地址空间其实叫做进程地址空间,而进程是操作系统上的概念。因此,进程地址空间分为如上图所示的几个部分:

  • 栈区(stack): 在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。 栈区主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。
  • 堆区(heap): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由 OS 回收 。分配方式类似于链表。
  • 数据段(静态区) ( static ) :存放全局变量、静态数据。程序结束后由系统释放。
  • 代码段: 存放函数体(类成员函数和全局函数)的二进制代码。

1.2接下来我们在Linux下进行地址空间验证

1 #include<stdio.h>
2
3 int un_g_val;
4 int g_val=100;
E> 5 int main(int argv,char* argv[],char* env[])
6 {
7 printf("code addr :%p\n",main);
8 printf("init global addr :%p\n",&g_val);
9 printf("uninit addr :%p\n",&un_g_val);
E> 10 char *m1=(char*)malloc(100);
11 printf("heap addr :%p\n",m1);
12
13 printf("stack addr :%p\n",&m1);
E> 14 for(int i=0;i<argc;i++)
15 {
E> 16 printf("argv addr :%p\n",argv[i]);
17 }
18 for(int i=0;env[i]!=NULL;i++)
19 {
20 printf("env addr :%p\n",env[i]);
21 }
22 return 0;
23 }

[ Linux ] 进程地址空间_运维_02


按照我们上面的图片地址是依次增大的,也就对应了我们程序地址空间的图片!

我们也能看到堆和栈中间出现了非常大的地址镂空!

[ Linux ] 进程地址空间_linux_03


接下来我们代码进行增加后测验

[ Linux ] 进程地址空间_linux_04


我们可以看出堆区地址在不断的向上增长!!!我们再来测试一下栈

[ Linux ] 进程地址空间_地址空间_05


[ Linux ] 进程地址空间_运维_06


上图我们可以看出栈空间的地址是向下增长(减少)!

所以我们得出结论:堆,栈相对而生!!!

我们一般在C语言函数中定义的变量,通常在栈上保存,那么先定义的一定是地址比较高的!

1.3 如何理解static变量

[ Linux ] 进程地址空间_运维_07

[ Linux ] 进程地址空间_地址空间_08


函数内定义的变量用static修饰,本质是编译前会把该变量编译进全局数据区!

所以我们也能理解经常在C语言中说道静态变量的生命周期是在程序结束后释放!

二、感知地址空间的存在

我们使用一段代码来感知地址空间

1 #include<stdio.h>                                                                                                                                                                        
2 #include<unistd.h>
3 #include<stdlib.h>
4
5 int g_val=100;
6
7 int main()
8 {
9 pid_t id=fork();
10 if(id==0)
11 {
12 //child
13 while(1)
14 {
15 printf("我是子进程:%d, ppid: %d,g_val: %d,&g_val: %p\n\n",getpid(),getppid(),g_val,&g_val);
16 sleep(1);
17 }
18 }
19 else
20 {
21 //parent
22
23 while(1)
24 {
25 printf("我是父进程:%d, ppid: %d,g_val: %d,&g_val: %p\n\n",getpid(),getppid(),g_val,&g_val);
26 sleep(2);
27 }
28 }
29 return 0;
30 }

[ Linux ] 进程地址空间_运维_09


到目前为止,父子进程没有修改全局数据的时候,父子是共享该数据的!

当我们尝试去写入,我们在子进程中的g_val的值修改成200,会发生什么事情???

[ Linux ] 进程地址空间_地址空间_10


我们发现了一个惊人的一面!!!

父子进程读取同一个变量(因为地址一样!),但是后续没有人修改的情况下,父子进程读到的内容却不一样!!!

所以我们可以得出一个结论:我们在C/C++中使用的地址,绝对不可能是物理地址!!!

因为如果是物理地址,上图的情况是不可能产生的!!!

那是什么呢???
给出结论:虚拟地址,线性地址,逻辑地址,这三者在Linux下是相同的!

为什么操作系统不让我直接看到物理内存呢?

内存就是一个硬件,不能阻拦访问你访问!只能被动地进行读取和写入!
因此如果我们能够直接访问甚至修改物理内存,将会造成不可预料的后果,因此为了安全起见,操作系统不会让我们直接访问物理内存。

三、进程地址空间

3.1 概念

每一个进程在启动的时候,都会让操作系统给它创建一个地址空间,该进程地址空间就是进程地址空间

[ Linux ] 进程地址空间_linux_11


每一个进程都会有一个自己的进程地址空间!!

那么操作系统要不要管理这些进程地址空间呢?

那么怎么去管理呢?

进程地址空间,其实是内核的一个数据结构,操作系统肯定会先描述在组织这些数据结构,mm_struct(稍后看!)

3.2 理解进程地址空间

所以我们验证说程序地址空间是不准确的,准确的应该说成进程地址空间,那么我们应该怎么来理解呢?

进程地址空间在逻辑上是一个抽象的概念,我们在谈这个概念之前我们需要引入一个进程独立性。

  • 进程独立性:多进程运行,需要独享各种资源,多进程运行期间互不干扰!

所谓的地址空间:其实就是OS通过软件的方式,给进程提供一个软件视角,认为自己会独占系统的所有资源(内存)

[ Linux ] 进程地址空间_地址空间_12


上面的图就足矣说名问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址!

什么叫做区域?

在mm_struct中,我们知道有不同的区域,栈区、堆区、全局数据区等等,在内核中是以下面这段代码所示存储的

struct mm_struct
{
long code_start;
long code_end;

long init_start;
long init_end;

long uninit_start;
long uninit_end;

long heap_start;
long heap_end;

long stack_start;
long stack_end;
...
}

因此地址空间就可以被划分为不同的区域,每一个区域范围之内都可以有一套地址作为页表中的虚拟地址和物理地址进行映射。页表映射是将程序加载到内存有内程序变成进程之后,由OS给每个进程构建一个页表结构。

查看内核源码

[ Linux ] 进程地址空间_运维_13

3.3 程序是如何变成进程的

抛出问题:程序被编译出来,没有加载的时候,程序内部有地址吗??

答:有的。链接过程是把已有程序和库当中的代码产生关联,没有地址怎么进行调用呢?

程序被编译出来,没有加载的时候,程序内部有没有区域??

答:有的。我们可以在Linux下使用指令进行查看可执行程序,可以验证每一个可执行程序是有地址的,一个程序没有被加载的时候是有地址的。

readelf -S test

[ Linux ] 进程地址空间_linux_14


当父进程或者子进程在写入的时候,由于进程具有独立性,如果子进程将同一个变量修改时,操作系统会重新给子进程重新开辟一段空间,建立映射关系,因此,最终我们查看时,虽然地址一样,但是所指的数据已经不同了。这种我们也叫做写时拷贝。

fork有两个返回值,同一个变量怎么会有两个返回值

在之前,我们知道fork有两个返回值,而同一个变量怎么会有两个返回值,这时我们就可以理解pid_t id是属于父进程栈空间中定义的变量,fork内部,return会被执行两次,return的本质,就是通过寄存器将返回值写入到结构返回值的变量中!!当id=fork()的时候,谁先返回,谁就要发生写时拷贝,所以,同一个变量会有不同的内容值,本质是因为大家的虚拟地址是一样的,但是大家对应的物理地址是不一样的!

为什么要有虚拟地址空间?

安全性:

在前面说过如果只有物理内存,没有虚拟地址时,直接让进程访问物理内存是不安全的。那为什么有虚拟地址就是安全的呢?因为通过页表转换,如果是一个不安全或者非法的指针,访问物理内存时,通过页表进行转换过程进行审核,非法的访问就可以直接拦截了。

进程管理:

用多少开多少,什么时候用什么时候开!通过地址空间,进行功能模块的解耦!!

让进程或者程序可以以一种统一的视角看待内存:

方便以统一的方式来编译和加载所有的可执行程序方便简化进程本身的设计和实现。