一、学习C语言时我们所认为的内存
如何验证下图?
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
int un_glo;
int init_glo = 100;
int main()
{
printf("code addr: %p\n", main); //代码区
const char *str = "hello linux";
printf("read only char addr: %p\n", str); //字符常量区
printf("init global addr: %p\n", &init_glo); //已初始化全局数据区
printf("uninit global addr: %p\n", &un_glo); //未初始化全局数据区
char *heap1 = (char*)malloc(100);
char *heap2 = (char*)malloc(100);
char *heap3 = (char*)malloc(100);
char *heap4 = (char*)malloc(100);
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("stack1 addr: %p\n", &str); //栈区
printf("stack2 addr: %p\n", &heap1);
printf("stack3 addr: %p\n", &heap2);
printf("stack4 addr: %p\n", &heap3);
return 0;
}
运行结果如下:
我们发现打印结果地址大小与图中相符。
规定向上为高地址,其中堆区向上增长,栈区向下增长。
>int a[10]与struct b{x,y,z}中的元素地址大小?
>int a变量的地址?
C/C++定义任何类型,开辟空间整体向下增长,以开辟地址的最低地址作为起始地址,使用时局部性向上使用。
>用static修饰a变量,a就是全局变量
static int a = 10;
printf("a addr: %p\n", &a);
所谓的静态区就在已初始化全局数据区。
>字符常量区不可被修改
由于字符常量区离代码区比较近,本质上它俩是编在一块的,系统保护代码区不可修改的方式就是保护字符常量区的方式。
>实际上,栈区上还有命令行参数和环境变量
验证一下:
二、上述内容不是内存,而是进程地址空间
进程地址空间通过页表与物理内存进行地址映射。
之所以g_val值不同,地址却相同是因为发生了写实拷贝,当子进程想要修改共同指向的数据时,OS会现在物理内存中开辟一块空间,并把原来空间的值拷贝一份,这块空间有新的物理地址。
若子进程先进行修改就把子进程的页表映射关系修改为新的物理地址,此时子进程指向新的空间并修改新空间的数据。哪个进程先改,哪个进程修改页表。
栈区堆区相对而生就是通过调整下图中的heap_start, heap_end, stack_start, stack_end实现的。
页表除了上述内容外还有权限字段,进程地址空间还要再通过权限字段才能访问内存
上面提到字符常量区不能被修改是因为虚拟地址通过页表映射物理地址时,发现要进行写操作,但要访问的物理地址处只有读权限,所以不能修改。
CPU中存在名为CR3的寄存器,存储页表的物理地址。
进程地址空间的好处:
1.让进程以统一的视角看待内存,所以任意一个进程可以通过地址空间+页表将乱序的内存数据变为有序。
2.存在虚拟(进程)地址空间可以有效进行进程访问内存的安全检查。
3.将进程管理和内存管理进行解耦。
3.1.通过页表可以将进程映射到不同的物理内存处,从而实现进程的独立性。
缺页中断:
页表中还有一个字段表示内存是否分配以及是否有内容。
当执行代码的时候,读取访问到一个虚拟地址,查页表发现对应的内存和代码都没有,此时OS会暂停访问请求,接着在内存中开辟空间,把可执行程序需要执行的对应虚拟地址的代码加载到内存里,把对应的物理地址填充到页表中,把标志位该为11,最后让暂停的代码继续访问。这个过程叫做缺页中断。