【Linux】四、Linux 进程概念(四)

简介: 目录十、进程地址空间10.1 回顾C/C++ 地址空间10.2 测试10.3 感性理解虚拟地址空间10.4 如何画大饼?10.5 如何理解区域划分和区域调整10.6 虚拟地址空间、页表和物理地址10.7 为什么存在地址空间10.7.1 保证物理内存的安全性10.7.2 保证进程的独立性10.7.3 保证进程的统一性(难点)

目录

十、进程地址空间

10.1 回顾C/C++ 地址空间

10.2 测试

10.3 感性理解虚拟地址空间

10.4 如何画大饼?

10.5 如何理解区域划分和区域调整

10.6 虚拟地址空间、页表和物理地址

10.7 为什么存在地址空间

10.7.1 保证物理内存的安全性

10.7.2 保证进程的独立性

10.7.3 保证进程的统一性(难点)


十、进程地址空间

10.1 回顾C/C++ 地址空间

C/C++ 地址空间基本是下面这样子的,以 32 位的平台为例

image.png

这里的地址空间是什么?是物理地址吗?下面解释

10.2 测试

测试代码

#include<stdio.h>                                                                                                                                                          #include<unistd.h>intglobal_value=1;
intmain()
{
pid_tid=fork();
if(id<0)
    {
printf("fork error\n");
    }
elseif(id==0)
    {
while(1)
        {
printf("I am son process, pid:%d, ppid:%d | golbal_value:%d, &global_value:%p\n", getpid(), getppid(), global_value, &global_value);
sleep(1);
        }
    }
else    {
while(1)
        {
printf("I am parent process, pid:%d, ppid:%d | golbal_value:%d, &global_value:%p\n", getpid(), getppid(), global_value, &global_value);
sleep(2);
        }
    }    
return0;    
}

运行结果,global_value 的地址都相同,没毛病

image.png

下面稍微修改一下程序

测试代码

#include<stdio.h>                                                                                                                                        #include<unistd.h>                                                                                                                                       intglobal_value=1;                                                                                                                                    
intmain()                                                                                                                                               
{                                                                                                                                                        
pid_tid=fork();                                                                                                                                   
if(id<0)                                                                                                                                           
    {                                                                                                                                                    
printf("fork error\n");                                                                                                                          
    }                                                                                                                                                    
elseif(id==0)                                                                                                                                     
    {                                                                                                                                                    
intcnt=0;                                                                                                                                     
while(1)                                                                                                                                         
        {                                                                                                                                                
printf("I am son process, pid:%d, ppid:%d    | golbal_value:%d, &global_value:%p\n", getpid(), getppid(), global_value, &global_value);      
sleep(1);                                                                                                                                    
if(cnt==5)                                                                                                                                 
            {                                                                                                                                            
global_value=100;                                                                                                                      
printf("global_value 已发生改变\n");                                                                                                                       
            }                                                            
cnt++;    
        }    
    }       
else    {               
while(1)    
        {
printf("I am parent process, pid:%d, ppid:%d | golbal_value:%d, &global_value:%p\n", getpid(), getppid(), global_value, &global_value);
sleep(2);
        }
    }
return0;                                                                                                                                                              
}

运行,观察变化

image.png

我们观察发现,同一个地址,居然打出了两个不同的值,这是什么情况??

       如果说我们是在同一个物理地址处获取的值,那必定是相同的,而现在在同一个地址处获取到的值却不同,这只能说明我们打印出来的地址绝对不是物理地址

       以此推导,我们在学习各种语言所遇到的地址,并不是对应的物理地址,包括指针

那这个地址是什么地址?

       我们在学习各种语言中所遇到的地址叫做虚拟地址,虚拟地址不是物理地址,虚拟地址也叫线性地址和逻辑地址


学习各种语言所看到的地址,全部都是虚拟地址!物理地址,用户一概看不到,由OS统一管理OS必须负责将 虚拟地址 转化成 物理地址

10.3 感性理解虚拟地址空间

理念:进程会认为自己是独占系统资源的,事实上并不是

举个栗子:

一个富翁Peter,他有百亿美金,他在外面有三个私生子,son1、son2和 son3,三个私生子彼此之间都不知道各自的存在,son1、son2和 son3都认为他的父亲只有一个儿子,就是他们自己

Peter 对儿子们画了一个超级大饼:

Peter 对 son1 说:你管理好这个工厂,等我不在了,你就继承我的百亿美金

Peter 继续对 son2 说:你当好这个金融公司的CEO,等我不在了,你就继承我的百亿美金

Peter 又对 son3 说:你好好念书,等我不在了,你就继承我的百亿美金

这三个儿子非常高兴,son1 认为自己独占他爸的资产,son2 也认为自己独占他爸的资产,son3 也认为自己独占他爸的资产

儿子们想他老爸要钱,不会说:爸,你给 百亿美金我用用。即使儿子是这么说,他爸也不可能给的,儿子要钱了,Peter 只会给几百万,几千万,Peter不可能说把全部的资产全给儿子


这里的大富翁就相当于操作系统;他的百亿美金就相当于内存;给儿子们画的百亿美金大饼就相当于地址空间,准确的说是进程地址空间,它就是我们C/C++ 学习是所划分的地址空间(栈区、堆区...),而进程地址空间就是操作系统给进程画的大饼;儿子就相当于进程,儿子向老爸要的钱就相当于我们向内存申请内存或对象空间


儿子们向他老爸要百亿美金,他老爸可以直接拒绝,就好比你的内存有16G,进程A 一上来就申请个16G内存,操作系统直接拒绝了进程的请求,申请内存一般每次都是申请一点点:10kb,20kb,不可能说上来就直接申请16G的内存,操作系统也不会给你这么干

10.4 如何画大饼?

       大富翁给儿子们画饼,儿子们肯定要记住是谁给他们画的饼,画的饼是什么样子的,儿子们脑中都有一个蓝图结构体,这个结构体里面包含了是谁给他们画的饼,画的饼是什么...等等

struct蓝图{
char*who;
char*when;
char*money;
//岗位//...}

画饼的本质:在大脑中构建一个蓝图,这种蓝图实际上是一种数据结构对象

再举个栗子:

假设某个公司的老板,他给公司内的500个员工画饼:你们好好干,等公司上市了,一人给...

员工是要被管理的,老板给员工画的饼也要被管理

这500个员工就相当于500个进程,老板给员工画饼就相当于操作系统给进程画的饼:进程地址空间;500个进程也要被管理, 进程地址空间也要被管理,如何管理?先描述,再组织


地址空间的本质:是内核的一种数据结构,这个数据结构叫 mm_struct

继续谈如何画饼

继续看 C/C++ 的地址空间,假设是在 32位平台下

image.png

 地址空间描述的基本空间大小的单位是字节,32位下就有 2^32 个地址空间(字节),这些空间都是虚拟地址空间,这个虚拟地址空间就是进程地址空间, 2^32 个地址空间(字节) = 4GB的空间范围,每个字节都要有唯一的地址,这样下来给每个字节对应一个唯一的地址,这样地址就有了 2^32 个地址,地址最大的意义只要保证唯一性即可,怎么表示 2^32 个地址,32位的数据即可表示:32bit(unsigned int )

10.5 如何理解区域划分和区域调整

地址空间中有栈区、堆区、代码区...等等,那它们是如何划分的呢?

下面继续举栗子:

小明和小红是一年级的同学,他们互相是同桌,他们的课桌假设只有100cm,小红嫌小明老是占用她的课桌位置,于是与小明一起对课桌进行了区域划分,两人个占一半,小明的活动范围是 [0 ~ 50]cm,小红的活动范围是 [51 ~ 100]cm

image.png

如何描述小明和小红所划分的区域?如图

image.png

小明老是越过分割线,小明老是挨揍,于是小明就对小红说能不能重新划分一下区域,我们各自留下 5cm的缓冲空间,小红说可以,你可以适当在缓冲空间活动,但是不能超过我的分割线

image.png

多次对桌子分配的区域所进行的调整,就可以看成不断改变结构体的内部成员变量大小的过程

structDestopd= {0, 50, 51, 100};//一开始structDestopd_new= {0, 45, 55, 100};//区域发生改变structDestopd_new1= {0, 30, 31, 100};//区域再次发生改变

       小明和小红进行的划分区域和区域调整,就相当于地址空间的区域划分和区域调整,小明和小红是一个具体的 mm_struct 结构体

       虚拟地址空间的区域划分,实际上也就是相当于小明和小红进行区域划分,这个划分的结构体就是 struct mm_struct,它里面包含了对各个区进行划分的数据,进行划分的空间大小为 2^32 个字节,也就是 4GB 的空间大小(32位下)

structmm_struct{
uint32_tcode_start, code_end;//代码区uint32_tdata_start, data_end;//数据区uint32_theap_start, heap_end;//堆区uint32_tstack_start, stack_end;//栈区//....//...}

      划分好之后,小红再次对区域进行调整,这就相当于再次对虚拟地址空间划分进行调整,堆区和栈区的虚拟地址空间大小是可以被改变的,也就是再次进行空间调整,进行调整只需要改变 start和end 的范围。比如我们进行 malloc 或 new 开辟空间,实际上就是对 堆区或栈区 进行调整,增大堆区或栈区的空间范围,当我们 free 掉空间的时候,也就对应调整缩小 栈区或堆区的空间范围

我们看一眼 Linux 的部分 mm_struct 内核数据结构

这里面确实对各个区域进行了划分

这也证明了,我们之前一直所谈的C/C++地址空间这个叫法是个错误的,其实际上是进程的地址空间

10.6 虚拟地址空间、页表和物理地址

我们已经知道了 struct mm_struct *mm,这个 *mm指针就是指向 mm_struct 这个结构体

image.png

这是我们的内存,可执行程序要执行就先要加载到内存里,那么我们通常所说的物理地址也就是内存与磁盘经常会产生联系,即数据在内存与磁盘间传输的过程我们称为IO,IO的单位是 4KB,那么我们就将内存中 4KB的大小空间看成一个 page页因此对于内存的数据来说,如果内存为4GB,那么我们可以把内存分割成 4GB/4KB 个 page页,即我们可以将内存想象为一个结构体数组:struct page mem[4GB/4KB],通过偏移量就可以访问内存中所有的page页,也就可以访问到内存的所有数据

image.png

进程地址空间 和 物理地址之间的关联

       而对于这些虚拟的地址实际上作为数据来说,也需要存放在物理地址的某一个位置,因此这就会与内存产生关联。而虚拟地址与物理地址产生关联的媒介就这样产生了,我们将这个媒介称之为页表。(由于页表的内容过于复杂,在这里仅仅是引出这么个名词方便后续解释)

       每一个虚拟地址都需要通过页表需要映射到物理内存上,一个页表中有两个地址(一个是虚拟地址,另一个是物理地址)也就是8个字节需要存储,那么储存这个页表所需要的空间为:2^32*8 = 32GB,内存压根存不下,内存只有 4GB,所以页表的存储形式不简单,而且极为复杂,因此关于页表的知识这里不谈,后续会讲

image.png

       假设一个程序它定义了 int a = 100,我们对 a 进行取地址 &a,取到的地址就是虚拟地址,假设 a 的虚拟地址是 0x1234 5678,这个虚拟地址通过页表的映射,映射找到相应的物理地址,假设物理地址是 0x1111 2222

image.png

我们做的就是把可执行程序加载到内存,通过页表映射到内存等其他的所有工作,都是由操作系统自动帮你完成

image.png

多个进程运行,每个进程都认为自己占用 2^32 个地址 = 4GB,实际上操作系统并不允许任何一个进程完全占用所有的内存空间,而且进程是看不到物理内存的,只能通过页表取间接访问

10.7 为什么存在地址空间

10.7.1 保证物理内存的安全性

      如果直接让进程访问物理内存,这是非常不安全的。比如,万一进程越界非法操作呢?有一个恶意进程扫描你的物理内存,读取你的隐私数据,账号密码...等等,所以进程直接访问物理内存是不安全的。

       所以就需要一个虚拟地址空间,给进程啥闹腾,非法操作,野指针...随便让进程弄,这些非法进程非法访问物理内存或非法进行映射的时候,页表可以直接拦截你的非法操作,这个识别恶意进程和终止恶意进程都是由操作系统做的

      至于怎么识别和怎么做,后面篇章会讲

10.7.2 保证进程的独立性

解释 10.2 的测试现象,同一个地址,打出了两个不同的值

image.png

相同地址下父进程和子进程的数值为什么不同?

我们都知道了子进程是以父进程为模板创建出来的

image.png

  程序运行时,global_value = 100 被存放在了物理内存中,父进程和子进程都需要访问 global_value,于是 global_value 的虚拟地址空间中的地址就会通过页表映射到物理内存中,于是父进程和子进程就可以通过虚拟地址空间中的地址去访问 global_value,并且打印时父进程和子进程对应的 global_value 对应的虚拟地址也是相同的,因此开始时我们能看到父进程和子进程对应的 global_value 的数值和地址都相同

image.png

当子进程要改变 global_value 的值时,涉及到了写时拷贝和进程的独立性

       进程是具有独立性的,一个进程对被共享的数据修改,如果影响了其他进程,就不能称之为独立性了,所以一个进程对被共享的数据修改不能影响其他进程


       操作系统为了保证进程的独立性,操作系统做了很多工作:通过地址空间,通过页表,让不同的进程映射到不同的物理内存处


写时拷贝:

       任何一方尝试写入数据,操作系统先进行数据拷贝,更改页表映射,然后再让进程进行修改。写时拷贝用于不同进程的数据进行分离,比如两个进程共享一个数据,其中一个进程要对共享的数据进行修改,一个进程仍然指向原有的物理地址,而修改共享数据的另一个进程则发生写时拷贝

       所以,当子进程要改变 global_value 的值时,子进程会发生写时拷贝,操作系统就会将子进程页表与内存的物理地址之间的联系断开,并在物理内存的另一个位置将原来物理地址的数据拷贝过来,拷贝后再进行对值的修改,子进程页表与内存的物理地址之间的联系也将被修改,指向拷贝后的地址。

       所以,当子进程要改变 global_value 的值,并不会影响到父进程的 global_value 的值,这个操作与虚拟地址也没有任何关系,因此我们所看到的子进程与父进程的虚拟地址仍是相同的地址,发生改变的只是物理地址

image.png

进程 = 内核数据结构 + 进程对应的代码和数据,内核数据结构是独立的,进程对应的代码和数据也是独立的,因此进程就是独立的

image.png

      所以,进程地址空间的存在,可以更方便的进行 进程和进程的数据代码的解耦,从而保证了进程独立性的这种特征

10.7.3 保证进程的统一性(难点)

当我们写了一个可执行程序,当它加载到内存的时候,这个可执行程序的内部有地址吗?

       答案是肯定有的,程序编译的过程为:预处理、编译、汇编、链接,程序在第二步编译的时候已经有了地址(在调试模式下,反汇编可以查看),最后一步链接才是生成可执行程序,所以在生成可执行程序的时候,可执行程序的内部已经有的地址。

       这个地址叫逻辑地址,在Linux下,虚拟地址和逻辑地址是一样的,下面为了方便都叫虚拟地址

虚拟地址空间的规则只有操作系统会遵守吗?

       当然不是,不仅操作系统需要遵守,编译器同样需要遵守!编译器在编译你的代码的时候,就是按照虚拟地址空间的方式进行对代码和数据进行编址的

       上面说的地址,是我们程序内使用的地址

        假设在32位平台下,也就是按照32位地址空间进行编址,假设磁盘中有一个可执行程序 my.exe,它里面有一个main 函数,还有一个func 函数,还有一个变量 a,main 函数调用这个 func 函数,func 函数里面使用了变量a,my.exe 是一个可执行程序,它的内部已经有了虚拟地址(逻辑地址),假设 a的地址是 0x1122,func() 函数的地址是 0x1111,main() 函数的地址是 0x2222

image.png

在编译时,main() 里面的 fun() 会通过虚拟地址跳转到定义的 fun()函数,当可执行程序加载到物理内存时,这个虚拟地址仍然存在,也就是程序内部使用的地址在加载到物理内存中时仍然存在。

      执行程序加载到物理内存时,天然具备了一个外部的物理地址,可执行程序的内部也有一套虚拟地址,这样相当有了两套地址

       也就是说,可执行程序加载到物理内存时,可执行程序内部有一套虚拟地址,外部有一套物理地址!!一套是程序内部互相跳转的虚拟地址,另一套是标识物理存在中代码和数据的地址

image.png

       当 CPU 执行这段代码的指令的时候,程序内部的虚拟地址空间就被加载出来,*mm 指向这块虚拟空间 mm_struct,这个空间就有着这个程序的虚拟地址

image.png

       CPU 读进来的指令,指令内部就有地址(虚拟地址)

那么当CPU的寄存器,比如 pc指针通过指令读取此代码时,指令内读出来的是物理地址还是虚拟地址呢?

一定是虚拟地址!因为指令的内部使用的那一套地址就是虚拟地址,虚拟地址再通过页表映射找到物理地址

image.png

        当CPU再次读取指令,从main函数中出来再次调用fun()函数,出来的是物理地址还是虚拟地址呢?答案当然还是虚拟地址!原因与上述的理解相同

       过上述的物理内存的映射与寄存器的读取,整个代码跳转的逻辑就那么一点点的转起来了,读取虚拟地址,再通过页表找到物理地址,这个过程确实很抽象

       在这个过程中我们也发现 CPU 在根本接触不到物理地址,接触到的都是虚拟地址!


        所以,地址空间的存在,可以让进程以统一的视角来看待进程对应的代码和数据等各个区域,方便使用。编译器也以统一的视角来进行编译代码(使用和编译的 统一是指虚拟地址空间的统一,因为规则一样,所以虚拟地址编完即可使用)


----------------我是分割线---------------

文章到这里就结束了,进程概念这个篇章也完结了,下篇进入进程控制

相关文章
|
8月前
|
并行计算 Linux
Linux内核中的线程和进程实现详解
了解进程和线程如何工作,可以帮助我们更好地编写程序,充分利用多核CPU,实现并行计算,提高系统的响应速度和计算效能。记住,适当平衡进程和线程的使用,既要拥有独立空间的'兄弟',也需要在'家庭'中分享和并行的成员。对于这个世界,现在,你应该有一个全新的认识。
287 67
|
7月前
|
NoSQL Linux 编译器
GDB符号表概念和在Linux下获取符号表的方法
通过掌握这些关于GDB符号表的知识,你可以更好地管理和理解你的程序,希望这些知识可以帮助你更有效地进行调试工作。
313 16
|
7月前
|
Web App开发 Linux 程序员
获取和理解Linux进程以及其PID的基础知识。
总的来说,理解Linux进程及其PID需要我们明白,进程就如同汽车,负责执行任务,而PID则是独特的车牌号,为我们提供了管理的便利。知道这个,我们就可以更好地理解和操作Linux系统,甚至通过对进程的有效管理,让系统运行得更加顺畅。
204 16
|
7月前
|
Unix Linux
对于Linux的进程概念以及进程状态的理解和解析
现在,我们已经了解了Linux进程的基础知识和进程状态的理解了。这就像我们理解了城市中行人的行走和行为模式!希望这个形象的例子能帮助我们更好地理解这个重要的概念,并在实际应用中发挥作用。
141 20
|
6月前
|
监控 Shell Linux
Linux进程控制(详细讲解)
进程等待是系统通过调用特定的接口(如waitwaitpid)来实现的。来进行对子进程状态检测与回收的功能。
125 0
|
6月前
|
存储 负载均衡 算法
Linux2.6内核进程调度队列
本篇文章是Linux进程系列中的最后一篇文章,本来是想放在上一篇文章的结尾的,但是想了想还是单独写一篇文章吧,虽然说这部分内容是比较难的,所有一般来说是简单的提及带过的,但是为了让大家对进程有更深的理解与认识,还是看了一些别人的文章,然后学习了学习,然后对此做了总结,尽可能详细的介绍明白。最后推荐一篇文章Linux的进程优先级 NI 和 PR - 简书。
187 0
|
6月前
|
存储 Linux Shell
Linux进程概念-详细版(二)
在Linux进程概念-详细版(一)中我们解释了什么是进程,以及进程的各种状态,已经对进程有了一定的认识,那么这篇文章将会继续补全上篇文章剩余没有说到的,进程优先级,环境变量,程序地址空间,进程地址空间,以及调度队列。
127 0
|
6月前
|
Linux 调度 C语言
Linux进程概念-详细版(一)
子进程与父进程代码共享,其子进程直接用父进程的代码,其自己本身无代码,所以子进程无法改动代码,平时所说的修改是修改的数据。为什么要创建子进程:为了让其父子进程执行不同的代码块。子进程的数据相对于父进程是会进行写时拷贝(COW)。
165 0
|
9月前
|
存储 Linux 调度
【Linux】进程概念和进程状态
本文详细介绍了Linux系统中进程的核心概念与管理机制。从进程的定义出发,阐述了其作为操作系统资源管理的基本单位的重要性,并深入解析了task_struct结构体的内容及其在进程管理中的作用。同时,文章讲解了进程的基本操作(如获取PID、查看进程信息等)、父进程与子进程的关系(重点分析fork函数)、以及进程的三种主要状态(运行、阻塞、挂起)。此外,还探讨了Linux特有的进程状态表示和孤儿进程的处理方式。通过学习这些内容,读者可以更好地理解Linux进程的运行原理并优化系统性能。
332 4
|
9月前
|
Linux 数据库 Perl
【YashanDB 知识库】如何避免 yasdb 进程被 Linux OOM Killer 杀掉
本文来自YashanDB官网,探讨Linux系统中OOM Killer对数据库服务器的影响及解决方法。当内存接近耗尽时,OOM Killer会杀死占用最多内存的进程,这可能导致数据库主进程被误杀。为避免此问题,可采取两种方法:一是在OS层面关闭OOM Killer,通过修改`/etc/sysctl.conf`文件并重启生效;二是豁免数据库进程,由数据库实例用户借助`sudo`权限调整`oom_score_adj`值。这些措施有助于保护数据库进程免受系统内存管理机制的影响。