1.示例引入
运行如下代码那么运行结果如下图。
#include<stdio.h>
#include<unistd.h>int main()
{pid_t id =fork();if(id==-1){printf("创建进程错误!\n");return 1;}int size=0;if(id==0){//子进程while(1){printf("我是子进程,id为:%d,size=%d,size地址为%p\n",getpid(),size++,&size);sleep(1);}}else{//父进程while(1){printf("我是父进程,id为:%d,size=%d,size地址为%p\n",getpid(),size,&size);sleep(1);}}return 0;
}
结果会大吃一惊,相同地址的值竟然不同,这和C语言学习时的理论相违背,内存最小寻址单位是字节,给每个字节编号,指针指向其中某个字节编号。但是运行结果是相同指针,读取内存不一样。
2.虚拟地址空间
这是因为Linux操作系统没有采用让进程直接操作物理内存的方式,而是采用给进程一块虚拟空间,让进程间接访问物理内存,如下图。
这样就可以解释上述代码,虽然他们的虚拟地址相同,但是经过OS映射后的实际物理地址不同,所以才会出现地址相同而值不同的情况。
2.1 虚拟地址如何实现
在c语言中常常会涉及到如下内存概念,堆区,栈区,代码区,常量区等。
一个地址我们如何判断他在那个内存分区,一种方法是根据代码分析,malloc申请的在堆区,数组在栈区,还有一种方式就是打印堆区和栈区的变量地址,一般而言两者相差会十分大,看地址和那个更加接近,就处在那个区域。
#include<stdio.h>
#include<stdlib.h>int gval = 10;
int main()
{const char* p1 = "aaaa";int* p2 = (int*)malloc(12);int a = 10;int* p3 = &a;printf("常量区%p 全局变量区%p 堆区%p 栈区%p",p1,&gval,p2,p3);return 0;
}
实际上对于进程来说,只要我们知道堆区开始位置地址和结束位置地址就可以判断出当前变量处在那里了。我们管理不同分类的内存,只需要管理不同内存区域的开始与结束就行了。在Linux内核中就封装了mm_struct结构体管理进程内存,每个进程结构体内包含mm_struct结构体。
struct mm_struct {// 保护该结构体的自旋锁,用于并发访问控制spinlock_t mmap_lock;// 虚拟内存区域链表的头节点struct vm_area_struct *mmap;// 用于管理虚拟内存区域的红黑树的根节点struct rb_root mm_rb;// 虚拟内存区域的数量unsigned long map_count;// 代码段的起始地址unsigned long start_code, end_code;// 数据段的起始地址和结束地址unsigned long start_data, end_data;// 堆的起始地址unsigned long start_brk;// 堆的当前结束地址unsigned long brk;// 栈的起始地址unsigned long start_stack;// 命令行参数的起始地址unsigned long arg_start, arg_end;// 环境变量的起始地址和结束地址unsigned long env_start, env_end;// 页全局目录(Page Global Directory)指针pgd_t *pgd;// 内存管理上下文mm_context_t context;// 引用计数,记录有多少个地方引用了该 mm_structatomic_t mm_users;// 映射计数,记录有多少个进程映射了该内存空间atomic_t mm_count;// 锁,用于保护对该结构体的读写操作struct rw_semaphore mmap_sem;// 链表节点,用于将该 mm_struct 加入到全局的 mm_struct 链表中struct list_head mmlist;// 用于跟踪内存管理操作的统计信息struct mm_rss_stat rss_stat;
};
由此便可以划分出C语言的内存布局。
2.2 页表
页表其实就是虚拟地址和实际物理地址之间的桥梁,他类似于hash一样,如下图。
物理内存都是一样的,但是C语言划分的不同内存区域属性不同,例如代码区不可修改,栈区内存有限,这都要进程额外的维护起来。因此除了保留物理地址外,页表也会保留这块地址的属性,包括读,写,是否有效等信息。
由此便可以进行一定程度的保护,假如向一块只读内存写入数据,系统就会直接结束进程,弹出错误。所谓的野指针从页表的角度解读就是指定地址无效,或者是只读权限。
3.虚拟地址空间意义
3.1 保护内存安全
如果允许用户直接访问物理内存,那么便可以修改指定内存的数据,这极大可能会造成数据的错乱,程序运行崩溃,反观使用虚拟内存加页表的形式,可以最大程度的保护操作系统,用户在使用非法访问的时候直接禁止掉,不会对操作系统产生威胁。
3.2 解耦合
通过虚拟地址实现了进程管理与内存管理的分离,这样在进程使用内存的时候就不用关心内存够不够,是否可用的问题了,这样使管理方面变得简单。
3.3 有序
如果没有虚拟地址空间,多个进程在内存运行,那么每个进程申请的空间必定是七零八落的,东一块,西一块,管理起来十分的麻烦。但是经过虚拟地址空间之后,整个进程的空间布局变得明朗起来,分为栈区,堆区,静态区等,有利于学习与管理。