网站业务怎么做的,什么是网络营销产品,asp网站图片,电视剧怎么做短视频网站文章目录 Linux进程内存布局图#xff1a;内存布局的验证 进程地址空间写时拷贝 Linux进程内存布局图#xff1a;
地址空间的范围#xff0c;在32位机器上是2^32比特位,也就是[0,4G]。
内存布局的验证
代码验证内存布局#xff1a; 验证代码#xff1a; #includes… 文章目录 Linux进程内存布局图内存布局的验证 进程地址空间写时拷贝 Linux进程内存布局图
地址空间的范围在32位机器上是2^32比特位,也就是[0,4G]。
内存布局的验证
代码验证内存布局 验证代码 #includestdio.h#includestdlib.h#includeunistd.h#includesys/types.hint init10;int uninit;int main(){printf(code addr:%p\n,main);printf(init addr:%p\n,init);printf(uninit addr:%p\n,uninit);char* heap (char* )malloc(20);printf(heap addr:%p\n,heap);printf(stack addr:%p\n,heap);return 0; }
运行结果及分析根据下图运行结果分析验证了上图的内存分布。
验证堆向上增长与栈向下增长
验证代码 char* heap1 (char* )malloc(20);char* heap2 (char* )malloc(20);char* heap3 (char* )malloc(20);char* heap4 (char* )malloc(20);char* heap5 (char* )malloc(20);printf(heap1 addr:%p\n,heap1);printf(heap2 addr:%p\n,heap2);printf(heap3 addr:%p\n,heap3);printf(heap4 addr:%p\n,heap4);printf(heap5 addr:%p\n,heap5);printf(stack1 addr:%p\n,heap1);printf(stack2 addr:%p\n,heap2);printf(stack3 addr:%p\n,heap3);printf(stack4 addr:%p\n,heap4);printf(stack5 addr:%p\n,heap5);
运行结果堆向上增长栈向下减小与内存分布图一样。 结论堆栈相对而生。 验证命令行参数与环境变量
验证代码 int main(int argc,char* argv[],char* env[]){for(int i 0;argv[i];i){printf(argv[%d]:%p \n,i,argvi);}for(int i 0;env[i];i){printf(env[%d]:%p \n,i,envi);}return 0;}
运行结果及分析环境变量与命令行参数这两张表(不是表指向的内容)比栈区大其中是先有命令行参数这张表才有环境变量这张表。 验证表指向的内容的地址存放 注意区分下面代码与上面代码的不同 验证代码 int main(int argc,char* argv[],char* env[]){for(int i 0;argv[i];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;}结果分析无论是表还是表指向的项目都在栈上部的。
验证静态变量在内存分布中的位置 这里就不验证了直接得出结论静态变量是存放在初始化数据与未初始化数据之间的。静态变量默认是会被初始化的哪怕用户定义出来没有赋值编译器也会初始化。例如int 类型的静态变量会被编译器初始化为0
看看一个这样的代码 代码 #includestdio.h#includestdlib.h#includeunistd.h#includesys/types.hint g_val 1000;int main(){pid_t id fork();if(id0){//子进程while(1){printf(child pid:%d ppid:%d g_val%d g_val:%p\n,getpid(),getppid(),g_val,g_val);sleep(1);}}//父进程 else{while(1){printf(father pid:%d ppid:%d g_val%d g_val:%p\n,getpid(),getppid(),g_val,g_val);sleep(1);}}return 0;}
运行结果符合我们预期的数据本来就是父子进程共享的除非要写入进程之间时具有独立性的写入的时候需要写时拷贝。
奇怪的现象
测试代码 #includestdio.h#includestdlib.h#includeunistd.h#includesys/types.hint g_val 1000;int main(){pid_t id fork();if(id0){//子进程int cnt 0;while(1){printf(child pid:%d ppid:%d g_val%d g_val:%p\n,getpid(),getppid(),g_val,g_val);sleep(1);cnt; if(cnt3){printf(child change g_val\n);g_val2000;}}//父进程else{while(1){ printf(father pid:%d ppid:%d g_val%d g_val:%p\n,getpid(),getppid(),g_val,g_val);sleep(1);}}return 0;}
运行结果分析奇怪的现象如下图同一个变量子进程尝试对g_val进行写入的时候会进行写时拷贝但是为什么地址一样但是值却不一样呢
解释上面的现象
地址一样却值不一样所以这个地址肯定不是物理地址。如果是物理地址绝对不可能在一个地址中存放的内容不一样。
这个地址叫做虚拟地址/线性地址。 结论我们平时用到的语言的地址全部都不是物理地址是虚拟地址。所以下面这个图的空间排布的情况不是物理内存它叫做进程地址空间。
进程地址空间
每一个进程都有一个task_structPCBPCB里面有该进程的进程地址空间进程地址空间和内存之间是用一张表叫做页表里面存放的是虚拟地址与物理地址建立关系的如下图页表对应一个映射关系是虚拟地址与物理地址之间的关系。根据虚拟地址可以找到对应的物理地址。下面的结构都是操作系统内部在维护的。 说明上面的图就足矣说名问题同一个变量地址相同其实是虚拟地址相同内容不同其实是被映射到了不同的物理地址
其中父进程创建子进程后子进程也会有一个这样的结构也会有进程地址空间页表并且父进程PCB的大部分属性都会被子进程继承下来页表也会被继承下来类似浅拷贝这时父子进程都指向同一个物理内存。以上面的示例分析当子进程尝试对g_val进行修改时操作系统会在内存中重新开一个空间将修改后的值放在这个空间里再改变页表中g_val的虚拟地址对应的物理地址注意改的是物理地址虚拟地址没有改变所以上面示例的结果打印出来的地址虚拟地址没有改变。如下图 根据上面的解释也能够很好的解释fork()返回值问题了
什么是进程地址空间?
进程地址空间是数据结构具体到进程中是有特定的数据结构对象。 如下图所示在进程的PCB中有一个指针指向自己的进程地址空间进程地址空间里面包含一个结构体结构体里面有很多start和end划分区域。 为什么要有地址空间和页表
在进程看来有了页表可以将物理内存从无徐变为有序因为页表是有序的。让进程以统一的视角看待内存将进程管理和内存管解耦合进程管理与内存管理互不干扰。地址空间页表是保护内存安全的重要手段拦截非法例如野指针越界问题。
内存申请问题malloc/new
申请内存本质是进程的地址空间中申请。 这样可以充分保证
内存使用率不会空转。提升new/malloc的速度。
写时拷贝
为什么需要写时拷贝 答进程之间要做到独立性。创建子进程的时候为什么不直接将父进程的代码和数据拷贝一份给子进程呢 答因为子进程并不是会对父进程的所有数据都要进行写入操作如果fork()创建子进程的时候直接拷贝一份代码和数据会降低fork()的效率。为什么是要拷贝呢只开空间不拷贝行不行 答因为子进程不一定是对这个数据直接进行覆盖式的写入可以只是对该数据进行局部修改或则是基于之前的值进行操作。
如何做到写时拷贝的
前面所说的页表不只是有虚拟地址与物理地址的转换的还可以带很多选项的如下图介绍其中一个权限 下图代码字符串hello Linux是具有常属性的不能被修改当我们尝试去修改的时候会报错运行报错。 是因为在页表有权限虚拟地址映射到物理地址的时候会做权限审核如下图所示当只有可读权限没有修改的权限的时候尝试去修改就会报错。
写时拷贝的细节
当要进行写时拷贝的时候会将父子进程页表里大部分内容的映射权限设置为只读权限当父子进程任何一方要去进行尝试写入的时候操作系统会进行判断如果是数据段对数据进行写入时合理的就会引发缺页中断操作系统会将权限改为读写然后写时拷贝后再把页表对应的条目改为读写。