检察院加强网站建设,陕西省建设资质是哪个网站,app开发制作一般多少钱,ps软件下载绘画#x1f57a;作者#xff1a; 主页 我的专栏C语言从0到1探秘C数据结构从0到1探秘Linux菜鸟刷题集 #x1f618;欢迎关注#xff1a;#x1f44d;点赞#x1f64c;收藏✍️留言 #x1f3c7;码字不易#xff0c;你的#x1f44d;点赞#x1f64c;收藏❤️关注对我真的… 作者 主页 我的专栏C语言从0到1探秘C数据结构从0到1探秘Linux菜鸟刷题集 欢迎关注点赞收藏✍️留言 码字不易你的点赞收藏❤️关注对我真的很重要有问题可在评论区提出感谢阅读 文章目录 前言程序地址空间回顾示例一示例二原因 什么是地址空间小故事历史上的地址 VS 现在的地址虚拟地址1.什么是虚拟地址2. 什么是页表3. 遗留问题4. 当我们的程序编译的时候形成可执行程序的时候还没有被加载到内存中的时候请问我们程序内部有地址吗5. 程序从编译到执行的过程中步骤是什么样的 地址空间的概念 为什么要有地址空间1. 隔离和保护2. 资源管理3. 内存分布有序化 重新理解什么是挂起 前言
当你在电脑上运行一个程序时你可能想知道它是如何在内存中存储和管理数据的。有没有一种方法可以使不同的程序在内存中有自己的专属空间相互之间不会相互干扰呢
今天我们将探索一个令人着迷的概念——进程地址空间。进程地址空间是计算机系统中一项至关重要的技术它为每个正在运行的程序提供了独立的内存空间用于存储代码、数据和堆栈等信息。
想象一下当你同时打开多个应用程序如浏览器、音乐播放器和游戏时它们能够在内存中各自存在而不相互干扰。这得益于进程地址空间的隔离和管理。通过进程地址空间不同的程序拥有自己的内存区域彼此之间不会相互干扰从而确保了计算环境的稳定性和安全性。
了解进程地址空间的概念将帮助你更好地理解程序的执行过程、内存管理以及如何避免程序之间的相互影响。它在操作系统、编程和软件开发中都有着重要的应用。通过本次学习我们将揭开进程地址空间的神秘面纱探索其在计算机系统中的重要性和实际应用。
现在让我们一起深入了解进程地址空间探索程序运行背后的奥秘吧
研究背景5.14.0-344.el9.x86_64
程序地址空间回顾
示例一
在我们之前学习c语言的时候老师可能给大家讲过这样的空间布局图。 我们可以看到。命令行参数环境变量高于栈的地址、高于堆的地址、高于未初始化数据的地址、高于初始化数据的地址、高于正文代码的地址。事实是否真的如此呢
我们来验证一下。
测试代码如下
#include stdio.h
#include unistd.h
#include stdlib.hint g_unval;
int g_val 100;int main(int argc, char *argv[], char *env[])
{// int a 10;//字面常量const char *str helloworld;// 10;// a;printf(code addr: %p\n, main);printf(init global addr: %p\n, g_val);printf(uninit global addr: %p\n, g_unval);static int test 10;//static的本质是将该变量开辟在全局区域。char *heap_mem (char*)malloc(10);char *heap_mem1 (char*)malloc(10);char *heap_mem2 (char*)malloc(10);char *heap_mem3 (char*)malloc(10);printf(heap addr: %p\n, heap_mem); //heap_mem(0), heap_mem(1)printf(heap addr: %p\n, heap_mem1); //heap_mem(0), heap_mem(1)printf(heap addr: %p\n, heap_mem2); //heap_mem(0), heap_mem(1)printf(heap addr: %p\n, heap_mem3); //heap_mem(0), heap_mem(1)printf(test stack addr: %p\n, test); //heap_mem(0), heap_mem(1)printf(stack addr: %p\n, heap_mem); //heap_mem(0), heap_mem(1)printf(stack addr: %p\n, heap_mem1); //heap_mem(0), heap_mem(1)printf(stack addr: %p\n, heap_mem2); //heap_mem(0), heap_mem(1)printf(stack addr: %p\n, heap_mem3); //heap_mem(0), heap_mem(1)printf(read only string addr: %p\n, str);for(int i 0 ;i argc; i){printf(argv[%d]: %p\n, i, argv[i]);}for(int i 0; env[i]; i){printf(env[%d]: %p\n, i, env[i]);}return 0;
}
结果如下
我们还能看到栈向下增长所以后面的变量地址就越小而堆向上增长后面的变量地址就越大。堆和栈是相对而生的因为他们之间有相当大的一块空间是共享的。
但是我们知道free是在堆上申请空间的free时只传入堆起始地址怎么知道要删除几个字节呢其实在malloc时会把它的属性数据存起来包括大小、地址等。
我们还需要了解到。用户空间是0~3GB内核空间是3GB到4GB。 示例二
这里还要讲述一个例子一开始有一个进程后来 fork 成了两个我们在子进程中修改全局变量的值。我们会发现最后子进程的值和父进程的值不相同但是地址相同为什么怎么可能同一个地址同时读取的时候出现了不同的值呢
测试代码如下
#include stdio.h
#include unistd.hint g_val 100;int main()
{pid_t id fork();if(id 0){int cnt 0;//childwhile(1){printf(I am child, pid: %d, ppid: %d, g_val: %d, g_val: %p\n,\getpid(), getppid(), g_val, g_val);sleep(1);cnt;if(cnt 5){g_val 200;printf(child chage g_val 100 - 200 success\n);}}}else {//fatherwhile(1){printf(I am father, pid: %d, ppid: %d, g_val: %d, g_val: %p\n,\getpid(), getppid(), g_val, g_val);sleep(1);}}
}
测试结果如下
我们发现父子进程输出地址是一致的但是变量内容不一样能得出如下结论:
变量内容不一样,所以父子进程输出的变量绝对不是同一个变量但地址值是一样的说明该地址绝对不是物理地址在Linux地址下这种地址叫做 虚拟地址我们在用C/C语言所看到的地址全部都是虚拟地址物理地址用户一概看不到由OS统一管理
原因
这里原因也和虚拟地址有关。当我们子进程没有改变父进程的那个全局变量的时候他们在同一块内存中但是如果子进程改变了全局变量的值就会发生写时拷贝他就到了另一块物理内存上了但是这两块物理内存所对应的虚拟地址是同一个也就造成了这样的现象。
在Linux和windows上面验证上面的代码可能会跑出不一样的结果我们上面的结论默认只在Linux上有效。
什么是地址空间
小故事
我们通过一个故事来学习它。
从前有一个大富翁他有三个孩子。这个大富翁非常富有拥有很多钱和财产。为了激励他的孩子们他给每个孩子画了一个大饼。这个大饼代表了10亿美元的财富。每个孩子都认为自己拥有这10亿美元。
大富翁告诉大女儿她将来会继承家族的产业成为家族的掌门人。大女儿非常高兴她开始努力学习为将来的责任做准备。
大富翁告诉二女儿她将来会接管他的生意成为一位成功的商人。二女儿也非常兴奋她开始学习商业知识努力提升自己的能力。
大富翁告诉小儿子他将来会成为街上最亮眼的人。小儿子非常开心他开始锻炼自己的才艺努力成为一个出色的表演者。
每个孩子都根据自己拥有10亿美元的想法制定了自己的计划。大女儿想要投资房地产二女儿想要扩大生意小儿子想要在表演领域取得成功。
然而事实上这10亿美元只是大富翁给孩子们画的一个饼他并没有真正给他们这笔钱。但是每当孩子们向大富翁要钱时大富翁通常会给他们一些零花钱。因此每一个人都认为自己是富翁的合法继承人。但是站在上帝视角我知道他们彼此的存在。我也知道他们并不是唯一的合法继承人。但是这个老爹给儿子画的饼我们就叫做地址空间。那么我们现实之中的饼相当于谁呢他是看得见摸得着的它叫做物理内存。
我们前面讲述操作系统的时候讲过先描述再组织那这里的地址空间就是我们所说的描述但是他在将来也一定是一种数据结构要和一个特定的进程关联起来。
历史上的地址 VS 现在的地址
其实在一开始的时候并没有地址空间这个概念地址访问的直接就是物理内存这样的话内存本身是随时可以被读写的他特别不安全假如有野指针等问题就很麻烦。
但是现代计算机提出了下面这种方式
他引入了虚拟地址的概念。将虚拟地址与物理地址映射起来要访问物理内存就需要先映射有人可能会说最终还是会访问物理内存的万一我的虚拟地址是一个非法地址呢
不要急这时候操作系统会禁止映射。就像小时候。你的父母可能会让你把你的压岁钱上交说存在他那里怕你弄丢了等你什么时候要用的时候再给你但是当你想买玩具的时候想买很多玩具的时候你妈觉得那不是必要的就不会同意不管你怎么撒泼打滚是一个道理。
虚拟地址
1.什么是虚拟地址
其实如硬盘网卡等外设也有寄存器我们想把内存的存储空间和外设的寄存器统一编制成当做内存看但每一个硬件的本身是不同的所以就需要引入一个虚拟地址空间的东西它将不同的硬件对应的设备进行编制所以实际上我们访问的某些硬件是它和虚拟地址就是这样一个关系。
几乎所有的语言如果他有地址的概念这个地址一定不是物理地址而是虚拟地址一般我们碰不到物理地址操作系统设计者怕我们误操作导致系统崩溃。 我们看上面的地址空间。它划分有栈区、共享区、堆区、被初始化数据区等区域。但是我们不用看也知道为了有效管理内存资源、提供程序运行所需的运行环境并确保不同程序之间的互相隔离和安全性他肯定会划分成各个区域的。
事实也的确如此我们看源码
我们会发现它是有通过start或者end的标记值来划分区域。所谓的区域划分本质就是在一个范围里定义start和end。
所以到现在我们应该知道地址空间它一定是一种数据结构而且它里面要有各个区域的划分。我们要知道每个进程都有他自己的地址空间。该地址空间包含了各种内存区域如代码段、数据段、堆、栈等。这些区域在进程运行期间可以动态地进行分配和释放。
需要注意的是不同进程的地址空间可以具有相同的布局即相同的内存区域类型和相似的地址段但它们在逻辑上是独立的相互之间不会干扰或共享内部数据。每个进程都有自己的独立地址空间使得进程可以在同一台计算机上独立地运行并与其他进程隔离开来。
2. 什么是页表
页表是处理地址空间和物理内存对应的一种数据结构是地址空间对象的重要组成部分用于将进程的虚拟地址映射到物理内存。页表中的每个表项记录了虚拟页和物理页之间的映射关系。当进程访问虚拟地址时操作系统会根据页表的映射关系将虚拟地址转化为物理地址从而访问对应的物理内存。
3. 遗留问题
在前面的文章中我们讲述到fork之后return会被执行两次它的本质就是对ID进行写入此时发生了写时拷贝所以父子进程各自其实在物理内存中有属于自己的变量空间只不过在我们看到的它是用同一个变量也就是说同一个虚拟地址来标识了。
在fork函数中父进程和子进程都会执行fork函数内部的return语句。这是因为return语句是一条语句它在执行时会被两个指令分别执行。所以当fork函数内部的return语句被执行时它会被父进程和子进程各自执行一次。
这样的执行机制导致了fork函数的返回值有两个分别是父进程的返回值和子进程的返回值。父进程的返回值是子进程的进程ID而子进程的返回值是0。
需要注意的是当执行return语句时实际上是对ID进行写入操作。尽管这个ID只有一个是属于父进程的但是当fork成功之后当执行return语句时会发生写入操作写入之后父进程和子进程都会执行if和else判断。父进程在判断时使用的是自己的ID而子进程在判断时使用的是自己的ID。这是因为在fork函数内部的return语句执行时对ID的写入操作发生了写时拷贝所以父进程和子进程各自拥有自己的ID。
4. 当我们的程序编译的时候形成可执行程序的时候还没有被加载到内存中的时候请问我们程序内部有地址吗
答案是已经有地址了。地址空间不仅仅理解成为是操作系统要遵守的其实编译器也要遵守在编译器编译代码的时候就已经给我们形成了各个区域代码区数据区的并且采用了和Linux内核中一样的编址方式给每一个变量每一行代码都进行了编址故程序在编译的时候每一个字段早已经具有了一个虚拟地址。
在编译器的视角里在一个程序内部的各种地址关系其实这个地址不叫做虚拟地址它叫做逻辑地址。只不过在 Linux 下逻辑地址、线性地址和物理地址其实是一模一样的因为我们的 Linux 认为所有的那么起始地址全部都是从 0 开始的。
可执行程序在编译阶段就已经具有地址。在编写C或C代码时编译器会为每个函数和变量分配对应的地址。这些地址是相对于程序内部的可以理解为虚拟地址。编译器会根据程序的结构和代码逻辑为每个函数和变量分配合适的地址。
当程序被加载到内存中时操作系统会为其分配一块地址空间这个地址空间是操作系统给进程分配的也被称为虚拟地址空间。在这个虚拟地址空间中程序的代码、数据和其他区域会被映射到对应的物理地址上。
因此可以说在程序编译阶段程序内部已经具有地址这些地址是相对于程序内部的虚拟地址。当程序加载到内存后操作系统会将虚拟地址映射到物理地址上使得程序可以在内存中正确执行。
5. 程序从编译到执行的过程中步骤是什么样的
a. 编译将每个函数和变量分配对应的地址这个地址是虚拟地址 b. 加载程序被加载到内存中操作系统会为其分配一块地址空间这个地址空间是操作系统给进程分配的也被称为虚拟地址空间。程序会把自己编译时分配的虚拟地址与地址空间的虚拟地址相对应在这个虚拟地址空间中程序的代码、数据和其他区域会被映射到对应的物理地址上。 c. 运行task_struct会被加载到CPU中CPU在运行过程中需要调用哪一个函数就会通过虚拟地址找到对应的物理地址然后调用该函数调用该函数后也会通过页表、地址空间、PCB来返回对应的地址往复循环。
地址空间的概念
地址空间是操作系统为进程专门设计的一种内核数据结构。它是进程在运行时所使用的虚拟内存空间的抽象表示。在计算机系统中每个进程都有自己独立的地址空间用于存储程序的指令、数据和堆栈等信息。
地址空间的定义包括了进程的线性区域的划分每个区域由起始地址和结束地址来标定。这些区域可以包括代码段、数据段、堆和栈等。每个区域内的地址可以不连续但在指定的范围内都可以被访问。 地址空间的设计目的是为了提供一种抽象的方式来管理进程的内存使用。它使得每个进程都可以拥有自己独立的地址空间而不会相互干扰。通过地址空间的映射机制操作系统可以将进程的虚拟地址转化为物理内存地址从而避免了进程对内存的直接访问。 总之地址空间是操作系统为进程提供的一种抽象概念用于管理进程的内存使用。它定义了进程的虚拟内存空间的划分和映射机制确保每个进程都可以独立地访问自己的内存空间保证了进程之间的隔离和安全性。
为什么要有地址空间
1. 隔离和保护 凡是非法的访问或者映射操作系统都会识别到并终止你这个进程它可以有效的保护物理内存因为地址空间和页表是操作系统创建并维护的也就意味着凡是想使用地址空间和页表进行映射也一定要在操作系统的监管之下来进行访问他还保护了物理内存中所有的合法数据包括各个进程以及内核的相关有效数据。 2. 资源管理 因为有地址空间的存在也因为有页表的映射的存在我们的物理内存中是不是可以对未来的数据进行任意位置的加载 当然可以物理内存的分配可以做到和进程的管理没有关系。这样就实现了内存管理模块和进程管理模块的解耦合。 所以我们在C、C语言上new和malloc空间的时候本质是在虚拟地址空间上申请的。试想一下如果我申请了物理空间但是我不立马使用是不是就会造成了空间的浪费了当然会。 而本质上因为有地址空间的存在上层申请空间其实是在地址空间上申请的物理内存可以甚至一个字节都不给你而当你真正进行对物理地址空间访问的时候才执行内存的相关管理算法帮你申请内存构建列表映射关系。然后再让你进行内存的访问。而对物理地址空间访问是由操作系统自动完成的用户包括进程完全灵感知。这样也就是延迟分配的策略它可以提高整机的效率它使内存的有效使用几乎为100%。有了地址空间的存在我们可以以有序化的视角去看待进程的代码和数据在物理内存中的分布情况。这样可以更好地确定代码和数据的起始地址并且便于操作系统进行统一管理。同时有序化的内存分布也使得我们能够更好地检测越界访问等问题。 3. 内存分布有序化 因为在物理内存中理论上可以任意位置的加载那是不是物理内存中的几乎所有的数据和代码在内存中是乱序的呢 确实如此因为有页表的存在它可以将地址空间上的虚拟地址和物理地址进行映射。 他就可以做到说所有的内存分布有序化。我们知道地址空间是操作系统给进程画的大饼。 进程的独立性可以通过地址空间加页表的方式实现结合上一条来说进程要访问的物理内存中的数据和代码可能目前并没有在物理内存中存在也可以让不同的进程映射到不同的物理内存这样就很容易实现了进程的独立性。 因为有地址空间的存在每一个进程都认为自己拥有4GB空间并且各个区域是有序的。进而可以通过列表映射到不同的区域来实现进程的独立性每一个进程不知道也不需要知道其他进程的存在。 重新理解什么是挂起 加载的本质就是创建进程那么是不是非得立马把所有的程序的代码和数据加载到内存中并创建内核数据结构建立映射关系 不是这样的在最极端情况下只有内核结构被创建出来了是什么情况的此时他并没有被调用的时候就是这种情况这个状态也就是新建状态理论上我们是可以实现对程序的分批加载的既然可以分批加载当然也可以分批换出。进程的数据和代码被换出了就叫做挂起了。
页表映射的时候不仅仅映射的是内存磁盘中的位置也可以映射。
比如说加载一些大型的游戏如果一次性全部加载物理内存肯定是不够的然而我们分批加载加载了一部分又换出一部分如此往复便实现了游戏的加载。还有就是当我们加载游戏时。某个进程可能需要等待其他资源甚至这个进程短时间不会再被执行了他此时的状态就是阻塞了他此时可能就会把物理内存唤出到磁盘中等待下一次的唤入。