Linux 17 程序地址空间

🔥个人主页: Milestone-里程碑

❄️个人专栏: <<力扣hot100>> <<C++>><<Linux>>

<<Git>><<MySQL>>

🌟心向往之行必能至

目录

[一. 以往知识回顾](#一. 以往知识回顾)

二.验证虚拟内存

三.进程地址空间

[3.1 解析地址相同,值不同](#3.1 解析地址相同,值不同)

[3.2 虚拟内存管理](#3.2 虚拟内存管理)

[3.2.1 描述进程](#3.2.1 描述进程)

[3.2.2 调整区域划分](#3.2.2 调整区域划分)

四.为什么要有虚拟内存空间

[4.1 将"无序"变"有序"](#4.1 将"无序"变"有序")

[4.2 保护物理内存](#4.2 保护物理内存)

五.问题


一. 以往知识回顾

想必之前在学C时,我们都见过这张图

可以看到上面即有内核空间,也有用户空间

但这里告示你个结论,这里你取到的所有地址,和以往你使用的指针都是虚拟内存,非物理内存

二.验证虚拟内存

bash 复制代码
[lcb@hcss-ecs-1cde 3]$ ./test
parent[876]: 0 : 0x601058
child[877]: 1 : 0x601058
parent[876]: 0 : 0x601058
parent[877]: 1 : 0x601058
child[878]: 2 : 0x601058
child[879]: 1 : 0x601058
parent[876]: 0 : 0x601058
parent[877]: 1 : 0x601058
parent[879]: 1 : 0x601058
parent[878]: 2 : 0x601058
child[880]: 1 : 0x601058
child[882]: 2 : 0x601058
child[881]: 2 : 0x601058
child[883]: 3 : 0x601058
parent[876]: 0 : 0x601058
parent[877]: 1 : 0x601058
parent[879]: 1 : 0x601058
parent[878]: 2 : 0x601058
parent[880]: 1 : 0x601058
parent[883]: 3 : 0x601058
parent[882]: 2 : 0x601058
child[885]: 2 : 0x601058
child[884]: 1 : 0x601058
parent[881]: 2 : 0x601058
child[886]: 2 : 0x601058
child[890]: 2 : 0x601058
child[893]: 3 : 0x601058
child[894]: 3 : 0x601058
child[887]: 3 : 0x601058
child[891]: 4 : 0x601058

源代码:

bash 复制代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
    pid_t id = fork();
    if(id < 0){
    perror("fork");
    return ;
else if(id == 0){ 
//child
    printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
else
{ //parent
    printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
}
    sleep(1);
    return 0;
}
bash 复制代码
parent[3767]: 0 : 0x601058
child[3768]: 0 : 0x601058

我们这里可以看到parent与child共享同一空间,其中原因我们在之前讲解fork已经说过,不再赘述

而我们知道,如果其中一个变量发生变化,对于父与子进程就会发生写实拷贝

那么如果他们不共享空间呢

源码

bash 复制代码
 1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4 int g_val = 0;
  5 int main()
  6 {
  7   while(1)
  8   {
  9   pid_t id = fork();
 10   if(id < 0){
 11   perror("fork");
 12   return 1;
 13   }
 14   else if(id == 0){ //child
 15     ++ g_val;
 16     printf( "child[%d]: %d : %p\n", getpid(), g_val, &g_val);
 17 }
 18 else{ //parent
 19   printf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);
 20 }
 21 
 22   sleep(1);
 23   }                                                                                                                                                                                                                              
 24   return 0;
 25 }

结果

bash 复制代码
[lcb@hcss-ecs-1cde 3]$ ./test
parent[876]: 0 : 0x601058
child[877]: 1 : 0x601058
parent[876]: 0 : 0x601058
parent[877]: 1 : 0x601058
child[878]: 2 : 0x601058
child[879]: 1 : 0x601058
parent[876]: 0 : 0x601058
parent[877]: 1 : 0x601058
parent[879]: 1 : 0x601058
parent[878]: 2 : 0x601058
child[880]: 1 : 0x601058
child[882]: 2 : 0x601058
child[881]: 2 : 0x601058
child[883]: 3 : 0x601058
parent[876]: 0 : 0x601058
parent[877]: 1 : 0x601058
parent[879]: 1 : 0x601058
parent[878]: 2 : 0x601058
parent[880]: 1 : 0x601058
parent[883]: 3 : 0x601058
parent[882]: 2 : 0x601058
child[885]: 2 : 0x601058
child[884]: 1 : 0x601058
parent[881]: 2 : 0x601058
child[886]: 2 : 0x601058
child[890]: 2 : 0x601058
child[893]: 3 : 0x601058
child[894]: 3 : 0x601058
child[887]: 3 : 0x601058
child[891]: 4 : 0x601058

我们能够发现每次执行 子进程与父进程的g_val不同,但输出的地址却是一样

如果上面存储的内存还为物理内存的话,又因为内存都有对应的值,那么就会出现问题了

因此:
变量内容不⼀样,所以⽗⼦进程输出的变量绝对不是同⼀个变量
但地址值是⼀样的,说明,该地址绝对不是物理地址!
在Linux地址下,这种地址叫做 虚拟地址
我们在⽤C/C++语⾔所看到的地址,全部都是虚拟地址!物理地址,⽤⼾⼀概看不到,由OS统⼀管理

OS则必须将虚拟地址转换为物理地址

三.进程地址空间

3.1 解析地址相同,值不同

那么该如何理解输出地址相同,但值不同呢?

原因就在于虚拟地址空间有页表

这里页表每行存储两个值,一个是虚拟空间的地址,一个是物理内存的地址,一一对应

在g_val发生变化之前,父子进程共用一个列表,但变化后,为了保证进程的独立性,系统先写实拷贝一份,子进程再进行修改

页表将虚拟地址(逻辑地址)映射到物理地址。每个进程拥有独立的页表,确保进程间的内存隔离,这也是为什么输出地址相同,而值不同

3.2 虚拟内存管理

与之前提到的OS管理一样,数据少时,直接管理也OK,但数据量大时,就效率过低了,此处一样要先描述再组织

描述linux下进程的地址空间的所有的信息的结构体是 mm_struct (内存描述符)。每个进程只有⼀ 个mm_struct结构,在每个进程的 task_struct 结构中,有⼀个指向该进程的mm_struct结构体指针。

先描述各个进程描述,再通过数据结构组织

3.2.1 描述进程

上面的地址是以一个字节为单位的,而每个区都有自己的空间大小(类似你小学与同桌的三八线),而要获取你们使用三八线的桌子空间大小很简单,即每个人的空间大小,就是末位置-起始位置=长度

虚拟内存同样,所有对进程描述,mm_struct会有 int begin int end,这就叫做区域划分了,只需要知道起始与末位置即可,而管理虚拟内存,即将这些描述通过链表组织起来

查看源码也确实如此

3.2.2 调整区域划分

虚拟地址以字节为单位,通过页表,实现了虚拟内存与物理内存的划分,但我们有可能会遇到两种情况

一:每次OS从硬盘读取的数据大小不同,虚拟内存如果分配?

其实OS会先在虚拟内存加载该部分空间(前面提到的预加载),然后再将硬盘的数据大小加载到页表,与虚拟内存一一对应

二:如果出现一个区域的内存不足,如何处理?

解决办法与你和你同桌类型,划定三八线后,你或许会觉得自己的空间太小,此时你就可以把三八线往你同桌那边挪动

此处类似,假设有 x y区域 ,对y进行扩张 ,即将 x_end-n y_start-n即可

问题:

mm-struct的初始化值从何来?

加载的时候,进行初始化

四.为什么要有虚拟内存空间

4.1 将"无序"变"有序"

我们之前提到OS时,说过task_struct中的代码和数据都是绑定在一起的,此处我们思考发现:

经过页表的映射,我们并不需要物理内存连续,绑定了,只需要映射后的虚拟内存连续即可,这样可以灵活我们代码与数据的分布

4.2 保护物理内存

其实页表还有一列权限列,与我们前面提到的文件 目录权限一样,它规定了可以进行的 读 写 执行操作

bash 复制代码
char * str ="hello";
str = "h";

如果在没学虚拟内存时,我们知道这个错,也只会说str是常量字符串,在创建时,只被给予了r -- 读的权限,因此不可修改

野指针问题:如果使用了野指针,很有可能会导致运行崩溃

那么页表就很好地解决了该问题,但释放了指针后,即将虚拟地址映射的物理空间释放了,找不到对应的值,避免了运行崩溃

让进程管理和内存管理进行一定的解耦合

五.问题

1.我们可以不加载代码和数据,只有task_struct mm_struct,页表

原因:如果读取到虚拟地址时,会发生缺页中断,去硬盘读取

2.创建进程,先有代码和数据,才会加载,才会有task_struct mm_sturct

先有struct

3.如何理解进程挂起?

在前面我们已经提到了进程挂起,即是在内存不足时,将一些优先级低的运行进程放回硬盘中,那么在这里,就是将一些优先级低的运行进程映射出的物理内存地址放回硬盘中,但保留虚拟空间地址,当需要时,通过该地址取出即可

4.通过malloc与new出来的空间在堆区,但不一定连续,一个strat end好像无法解决?

确实如此,但我们搜索可知,其实在定义时,定义了多组start,end,它们通过链表相连

  1. 上面的虚拟地址使用的地址是以一个字节为单位,那么int如何找呢?

先找到地址最小的位置,再往后找三个地址

进程具有独立性:

1.内核数据结构独立

2.加载进入的代码和数据独立

相关推荐
梵刹古音2 小时前
【C语言】 浮点型(实型)变量
c语言·开发语言·嵌入式
u0109272712 小时前
模板元编程调试方法
开发语言·c++·算法
CC.GG2 小时前
【Linux】进程控制(二)----进程程序替换、编写自主Shell命令行解释器(简易版)
linux·服务器·数据库
??(lxy)2 小时前
java高性能无锁队列——MpscLinkedQueue
java·开发语言
数研小生2 小时前
Full Analysis of Taobao Item Detail API taobao.item.get
java·服务器·前端
2401_838472512 小时前
C++图形编程(OpenGL)
开发语言·c++·算法
-dzk-2 小时前
【代码随想录】LC 203.移除链表元素
c语言·数据结构·c++·算法·链表
H Journey2 小时前
Linux 下添加用户相关
linux·运维·服务器·添加用户
齐落山大勇3 小时前
数据结构——栈与队列
数据结构