当前位置: 首页> 科技> 数码 > 京东网站建设哪家好_网站制作流程_天津seo排名公司_市场调研的方法

京东网站建设哪家好_网站制作流程_天津seo排名公司_市场调研的方法

时间:2025/7/13 15:18:01来源:https://blog.csdn.net/2302_80245587/article/details/147021271 浏览次数:0次
京东网站建设哪家好_网站制作流程_天津seo排名公司_市场调研的方法

程序地址空间是什么?

 

        讲这个问题之前,我们先来看一段熟悉的代码,以前学习C语言或者C++语言时,就听说过程序内存分布,堆区,栈区,静态区,常量区,共享区,代码段,初始化/未初始化全局变量区等概念

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_unval;
int g_val = 100;
int main(int argc, char *argv[], char *env[])
{const char *str = "helloworld";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;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 static 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;}

 

我们运行一下代码

        我们观察一下运行结果,发现,我们的程序在内存中的分布是非常有规律的,栈是向下增长的,堆区是向上增长的,栈的地址要大于堆的地址,堆区和栈区之间还有一块共享区,环境变量和命令行参数的地址是最大的,最小的地址是代码段,然后是常量区,其次是初始化未初始化全局变量。

就像下面的图片一样

我们发现这个程序在内存里面的分布的非常有规律,我们以32位操作系统来举例,内核空间占内存1G,剩下的3G被称为用户空间,这是因为32位操作系统最大能寻址4096MB的内存 这时后会有一

个疑问,那么一个程序在内存中的空间分布是这样的,那么如果有多个程序呢,如果都按照这样分布,不就有点不合理了吗,程序与程序直接的内存空间不会打架吗,接下来我们再看一个小实验

来看看下面的代码

# include<stdio.h>
# include<unistd.h>
# include<sys/types.h>int val = 5;
int main()
{pid_t id = fork();if(id == 0){printf("子进程创建pid == %d ppid = %d\n",getpid(),getppid());while(val){printf("子进程,val的地址: %p val的值== %d \n",&val,val);val--;}}else {printf("我是父进程 pid == %d ,val的地址%p \n",getpid(),&val );printf("父进程val的值 == %d",val);}return 0;
}

这里我们让子进程里面的val依次-- 并且打印地址,同时查看父进程val的值和地址 看看结果

这里我们发现子进程修改val的值和父进程val的值的地址居然一样!! 

我们要知道一个概念:

进程具有独立性:

  • 每一个进程的task_struct内核数据结构独立。
  • 内存中的代码和数据独立。

根据上面的概念,这显然是不合理的,这如果是物理地址肯定就说不过去,那么这是什么? 这是虚拟地址

下面我们直接说结论:

  • 一个进程一个虚拟地址空间
  • 一个进程一套页表

  根据上面的结论,再结合我们给的代码例子,我们再简单的谈谈页表和虚拟地址空间

页表是什么

  页表是操作系统维护的数据结构,他本质也是一个数据结构,用于记录虚拟地址到物理地址的映射关系。每个进程有独立的页表,简单的来讲页表就是虚拟地址映射到物理地址的一个工具,页表里面存放的是虚拟地址空间和物理地址空间。

有了这个概念之后,我们知道我们的程序地址空间是虚拟的,不是物理的,那么为什么我们上面举的例子他们子进程和父进程的虚拟地址还是一样的?

我们来画图说明一下

        这里的虚拟地址空间的位宽是1字节,那我们的int是4字节,那是如何处理的?我们有int这个类型,我们这里存放的地址其实是int四字节里面地址最小的那一个字节,在访问变量时,知道开始地址,再知道偏移量就行了。

当我们的子进程想要修改val的值时,我们的操作系统就会介入,(因为要保证进程的独立性) 拷贝一个val的值到新的物理内存中,再更改子进程对应的页表映射,映射到新的物理地址上面,供子进程使用,并且不影响到父进程,这就是写时拷贝!

        一个进程对应一个页表,一个虚拟地址空间

        所以我们打印出来的地址都是一样的了,那么为什么虚拟地址不用改呢,那是因为子进程和父进程的页表里面的内容互不冲突,只要保证自己页表里面的地址不冲突就行了,有了以上的映射关系之后,我们在物理内存里面就可以随便存放了

        我们用户是看不到物理内存的,物理内存由操作系统统一管理,所以我们在调试代码的时候看到的内存全是虚拟地址。

        页表的映射是由谁完成的,笼统的来说就是由操作系统完成的,本质上来说是由mmu的内存管理单元的一个硬件设备完成的

那么为什么要这样设计呢,我们下面来说说

怎么办?虚拟地址和程序地址空间

我们看看下面的图片

 每个进程会认为自己独占4G内存空间,进程之间是相互独立的,进程认为我得到的是物理地址,其实是操作系统分配的虚拟地址,而这些虚拟地址由操作系统统一管理,怎么管理?先描述再组织!而虚拟地址空间本质就是一个名为mm_struct的数据结构!!而这个mm_struct存放task_struct里面

而mm_struct就是描述一个程序地址空间的结构体!

那他是怎么描述的呢,其实很简单

我们举个例子:

我们要在一个桌子上面划分区域,这个桌子长一米

在mm_struct里面也是如此,就像下面这样

struct mm_struct
{
long code_start;
long code_end;
long init_data_start;
long init_data_end;
............}

而调整区域就做相应区域的+=,-=就可以了。只需要确认区域的开始和结束即可!每个进程都会有自己的mm_struct当进程少时就用链表这个数据结构来组织mm_struct,当mm_struct多时就用红黑树

我们来看看源码

struct mm_struct
{/*...*/struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */ struct rb_root mm_rb; /* red_black树 */ unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的⼤⼩*/ /*...*/// 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。 unsigned long start_code, end_code, start_data, end_data;unsigned long start_brk, brk, start_stack;unsigned long arg_start, arg_end, env_start, env_end;/*...*/}

这个vm_area_struct *mmap又是什么呢?

我们想一个问题,像栈区可以用++,--实现空间的改变,栈在虚拟内存中只存在一份,但是堆呢,堆肯定不止一份吧,那他又是怎么存储的呢,我们看看下面的图片

        这里的vm_area_struct也是一个结构体,他里面描述的是一块的空间,看上面的图片一个vm_area描述一个栈的空间,一个vm_area描述一个栈堆的空间,如果有两个堆在来一个vm_area描述,然后在把这个vm_area用一个数据结构组织起来,这样就解决了堆不止一个的问题,我们来看看源代码

struct vm_area_struct {
unsigned long vm_start; //虚存区起始
unsigned long vm_end; //虚存区结束
struct vm_area_struct *vm_next, *vm_prev; //前后指针
struct rb_node vm_rb; //红⿊树中的位置
unsigned long rb_subtree_gap;
struct mm_struct *vm_mm; //所属的 mm_struct
pgprot_t vm_page_prot;
unsigned long vm_flags; //标志位
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
struct list_head anon_vma_chain;
struct anon_vma *anon_vma;
const struct vm_operations_struct *vm_ops; //vma对应的实际操作
unsigned long vm_pgoff; //⽂件映射偏移量
struct file * vm_file; //映射的⽂件
void * vm_private_data; //私有数据
atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;

这里在补充几点

 

        操作系统在加载进程时,先建立虚拟内存映射,再按需加载数据到物理内存(通过缺页中断机制)。这是现代操作系统(如Linux、Windows)的主流实现方式,目的是提高内存利用率和启动效率。

        缺页中断又是什么,缺页中断就是在页表里面,有我们的代码和数据的虚拟地址空间,但是在物理内存中没有,此时操作系统就会把对应的代码从硬盘上加载到物理内存中,补齐对应的页表映射关系。

为什么要这么设计

  • 将无序的内存,变成有序的,方便管理
  • 地址转换过程中,可以对访问的地址进行合法判断,防止不可恢复的错误发生

·       我们来想一个问题,我们定义的常量字符串,如果我们从代码上面修改,这个代码肯定是能编过的,但我们执行时,操作系统就会不让我们执行,就会崩溃,我们知道常量字符串是被编写到代码段的,代码段是不可被修改的,那么操作系统是怎么判断的呢,就是通过页表对应地址的权限来判断的,查找页表的时候,权限被拦截了!(页表出了物理虚拟内存的映射条目,还有权限条目)

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

        为什么这么说,我们的进程和内存之间通过页表进行联系,这个结构就是一个高内聚低耦合的一个设计,进程与内存之间没有直接的关联,我们谈一谈进程挂起,我们知道我们进程被挂起了,代表内存不够用了,操作系统就会把对应代码和数据的内存释放掉,但保留这个进程的

        task_struct,这个task_struct里面就会有页表,页表里面对应的虚拟地址还在,但物理地址被操作系统给释放掉了,当下一次要调度这个进程时,就可以把对应的代码和数据重新建立映射关系,这样操作系统对内存的管理就和进程没什么太大的影响。所以让进程管理和内存管理进行一定的解耦合。

我们再说说野指针问题

当一个指针的指向的物理地址被释放了,对应页表的虚拟地址也要被释放,所以映射关系就要去掉,如果我们此时再操作指针去操作这个地址时,查找页表就会失败,失败就会可能导致进程崩溃,所以有了野指针之后,进程可能崩溃。

 

我们再总结一下

为什么要有虚拟地址空间

这个问题其实可以转化为:如果程序直接可以操作物理内存会造成什么问题? 在早期的计算机中,要运⾏⼀个程序,会把这些程序全都装⼊内存,程序都是直接运⾏在内存上的, 也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运⾏多个程序时,必须保证 这些程序⽤到的内存总量要⼩于计算机实际物理内存的⼤⼩。
那当程序同时运⾏多个程序时,操作系统是如何为这些程序分配内存的呢?例如某台计算机总的内存 ⼤⼩是128M,现在同时运⾏两个程序A和B,A需占⽤内存10M,B需占⽤内存110。计算机在给程序分 配内存时会采取这样的⽅法:先将内存中的前10M分配给程序A,接着再从内存中剩余的118M中划分 出110M分配给程序B。 这种分配⽅法可以保证程序A和程序B都能运⾏,但是这种简单的内存分配策略问题很多。
安全风险
每个进程都可以访问任意的内存空间,这也就意味着任意⼀个进程都能够去读写系关内
存区域,如果是⼀个⽊⻢病毒,那么他就能随意的修改内存空间,让设备直接瘫痪。
地址不确定
众所周知,编译完成后的程序是存放在硬盘上的,当运⾏的时候,需要将程序搬到内存当中 去运⾏,如果直接使⽤物理地址的话,我们⽆法确定内存现在使⽤到哪⾥了,也就是说拷贝 的实际内存地址每⼀次运⾏都是不确定的,⽐如:第⼀次执⾏a.out时候,内存当中⼀个进程 都没有运⾏,所以搬移到内存地址是0x00000000,但是第⼆次的时候,内存已经有10个进程 在运⾏了,那执⾏a.out的时候,内存地址就不⼀定了
效率低下
如果直接使⽤物理内存的话,⼀个进程就是作为⼀个整体(内存块)操作的,如果出现物理
内存不够⽤的时候,我们⼀般的办法是将不常⽤的进程拷贝到磁盘的交换分区中,好腾出内
存,但是如果是物理地址的话,就需要将整个进程⼀起拷⾛,这样,在内存和磁盘之间拷贝
时间太⻓,效率较低。
存在这么多问题,有了虚拟地址空间和分页机制就能解决了吗?当然!
地址空间和页表是OS创建并维护的!是不是也就意味着,凡是想使⽤地址空间和页表进⾏映射, 也⼀定要在OS的监管之下来进⾏访问!!也顺便 ,包括各个 进程以及内核的相关有效数据!
保护了物理内存中的所有的合法数据
因为有地址空间的存在和页表的映射的存在,我们的物理内存中可以对未来的数据进⾏任意位置 的加载!物理内存的分配 和 进程的管理就可以做到没有关系, 。
进程管理模块和内存管理模块就完
成了解耦合
因为有地址空间的存在,所以我们在C、C++语⾔上new, malloc空间的时候,其实是在地址 空间上申请的,物理内存可以甚⾄⼀个字节都不给你。⽽当你真正进⾏对物理地址空间访问 的时候,才执⾏内存的相关管理算法,帮你申请内存,构建页表映射关系(延迟分配),这 是由操作系统⾃动完成,⽤⼾包括进程完全0感知!!
因为页表的映射的存在,程序在物理内存中理论上就可以任意位置加载。它可以将地址空间上的
虚拟地址和物理地址进⾏映射,在 进程视角所有的内存分布都可以是有序 的。

 

 

 

 

关键字:京东网站建设哪家好_网站制作流程_天津seo排名公司_市场调研的方法

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

责任编辑: