【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 在根本接触不到物理地址,接触到的都是虚拟地址!


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


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

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

相关文章
|
2月前
|
资源调度 Linux 调度
Linux c/c++之进程基础
这篇文章主要介绍了Linux下C/C++进程的基本概念、组成、模式、运行和状态,以及如何使用系统调用创建和管理进程。
39 0
|
4月前
|
网络协议 Linux
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
684 2
|
25天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
96 4
linux进程管理万字详解!!!
|
16天前
|
存储 运维 监控
深入Linux基础:文件系统与进程管理详解
深入Linux基础:文件系统与进程管理详解
58 8
|
13天前
|
Linux
如何在 Linux 系统中查看进程占用的内存?
如何在 Linux 系统中查看进程占用的内存?
|
25天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
66 4
|
26天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
27天前
|
消息中间件 存储 Linux
|
2月前
|
运维 Linux
Linux查找占用的端口,并杀死进程的简单方法
通过上述步骤和命令,您能够迅速识别并根据实际情况管理Linux系统中占用特定端口的进程。为了获得更全面的服务器管理技巧和解决方案,提供了丰富的资源和专业服务,是您提升运维技能的理想选择。
48 1
|
2月前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
【10月更文挑战第9天】本文将深入浅出地介绍Linux系统中的进程管理机制,包括进程的概念、状态、调度以及如何在Linux环境下进行进程控制。我们将通过直观的语言和生动的比喻,让读者轻松掌握这一核心概念。文章不仅适合初学者构建基础,也能帮助有经验的用户加深对进程管理的理解。
26 1