赞
踩
进程地址空间排布图:
验证地址空间排布:
int main(int argc, char** argv, char** env){
//代码区
cout << "code addr:" << (void*)main << endl;
cout << "const string addr:" << (void*)"hello" << endl;
cout << endl;
//初始化数据区
cout << "init global addr:" << &g_init_val << endl;
static int s_init_val = 20;
cout << "init static addr:" << &s_init_val << endl;
//未初始化数据区
static int s_uninit_val;
cout << "uninit global addr:" << &g_uninit_val << endl;
cout << "uninit static addr:" << &s_uninit_val << endl;
cout << endl;
//堆区和栈区
int* pheap1 = new int;
int* pheap2 = new int;
int* pheap3 = new int;
cout << "heap addr:" << pheap1 << endl;
cout << "heap addr:" << pheap2 << endl;
cout << "heap addr:" << pheap3 << endl;
cout << "stack addr:" << &pheap1 << endl;
cout << "stack addr:" << &pheap2 << endl;
cout << "stack addr:" << &pheap3 << endl;
cout << endl;
//命令行参数和环境变量
cout << "argv addr:" << (void*)argv[0] << endl;
cout << "env addr:" << (void*)env[0] << endl;
return 0;
}
测试结果:
说明:
内核空间
在32位系统下,一个进程通常会被分配4GB的虚拟内存空间。这是因为32位系统使用32位的地址空间,每个地址可以表示2^32个不同的内存位置,即4GB。地址范围是0x00000000~0xFFFFFFFF。
在这4GB的虚拟内存空间中有大约1GB的内核空间会被操作系统保留,用于存储操作系统本身的代码和数据。剩下的大约3GB空间才是该进程的用户空间。
注意:以上所有的测试结果及结论均只在Linux系统中有效,Windows系统有自己的更复杂的内存机制因此得到的结果可能有所不同,但其底层原理是相同的。
先观察一个现象:
int g_val = 100;
void Test1(){
pid_t id = fork();
if(id == 0)
{
//child:
int cnt = 0;
while(1)
{
cout << "I'm child process "<< "pid:" << getpid() << " ppid:" << getppid();
cout << "g_val:" << g_val << " &g_val:" << &g_val << endl;
sleep(1);
++cnt;
if(cnt == 3) {
g_val = 200;
cout << "child change g_val 100->200 success!" << endl;
}
}
}
else{
//father:
while(1)
{
cout << "I'm father process "<< "pid:" << getpid() << " ppid:" << getppid();
cout << "g_val:" << g_val << " &g_val:" << &g_val << endl;
sleep(1);
}
}
}
运行结果:
为什么父子进程g_val的地址相同但值不同?
提示:
上面我们研究的内存布局,实际上指的是虚拟内存。
下文中的地址空间指的就是进程的虚拟地址空间、虚拟内存,这里我做了简化。
在Linux系统中所谓的逻辑地址,虚拟地址,线性地址实际是一回事,只不过是在不同阶段的不同称谓罢了。
struct mm_struct *mm, *active_mm;
。也就是说可以通过PCB找到进程对应的地址空间。完善进程的概念:
一个进程包括两大部分:
- 内核数据结构:进程PCB(task_struct),虚拟内存地址空间(mm_struct),映射页表
- 内存块:用于存储代码和数据
为什么父子进程g_val的地址相同但值不同?
fork();函数为什么会有两个返回值?一个变量怎么会同时保存不同的值呢?
答:在fork()
函数的return
语句之前,子进程就已经被成功创建了。所以return
会被执行两次,函数返回实际就是在对外部接收变量进行写入,此时物理内存发生写时拷贝。所以父子进程其实已经有属于各自的物理内存了,只不过在用户层面,在同一份代码中用同一个变量,同一个虚拟地址对其进行标识罢了。
想要访问物理内存必须通过地址空间和映射页表,而他们又是操作系统创建并维护的。也就是说必须要在操作系统的监管之下访问物理内存。虚拟内存技术的作用如下:
内存保护:凡是非法访问,操作系统都能够识别到,并终止这个进程。
内存隔离:保证进程之间互不干扰,实现了进程的独立性。
每个进程都认为自己独占整个系统的4GB内存(32),即虚拟地址范围从0x00000000到0xFFFFFFFF。
每个进程的页表映射的是物理内存的不同区域,因此进程之间能做到互不干扰。
每个进程都不知道其他进程的存在
内存管理:地址空间和页表的存在使得进程的内存分布有序化
物理内存不存在区块划分,也就是说物理内存中的代码和数据是乱序的。
但是映射页表可以将有序的虚拟地址空间映射到分散无序的物理内存上。
这就使得进程的内存分布有序化,便于进行内存管理。
地址空间的存在使得内存管理和进程管理模块完成了解耦合
内存管理模块负责将进程的虚拟地址映射到物理内存中的页框,而进程管理模块负责管理进程的创建、调度和终止等操作。
通过地址空间的存在,内存管理模块可以独立于进程管理模块进行工作。内存管理模块只需要关注虚拟地址和物理地址之间的映射关系,而不需要关心具体的进程信息。同样地,进程管理模块也不需要关心内存管理的具体实现细节。
这种解耦合的设计使得内存管理和进程管理模块可以独立开发、测试和维护,提高了系统的可扩展性和可维护性。同时,也使得操作系统能够更好地支持多任务和虚拟内存的功能。
举例说明:
- 当我们在代码中使用malloc, new申请内存空间时,实际上是在申请虚拟地址空间。(进程管理)
- 操作系统采用延迟内存分配的策略,来提高内存的使用效率:如果申请的地址空间不立马使用,操作系统就不会为其马上分配物理内存;只有当进程真正对物理内存进行访问的时候,才会执行内存的相关管理算法申请物理内存空间,构建页表映射关系。(内存管理)
- 对物理内存的分配和管理,完全是独立于进程之外的另一个模块。用户包括进程对内存管理完全0感知。
可执行程序文件内部存在虚拟地址吗?
提示:在Linux系统中所谓的逻辑地址,虚拟地址,线性地址实际是一回事,只不过是在不同阶段的不同称谓罢了。
重新理解进程的创建状态和挂起状态
分批换入换出内存
操作系统通常使用虚拟内存技术,将程序的代码和数据分为多个页面(或者多个段),并根据需要将这些页面从磁盘加载到内存中。
虚拟内存允许操作系统将程序的部分代码和数据加载到内存中,而将其他部分保留在磁盘上。当程序需要访问未加载到内存的页面时,操作系统会将相应的页面从磁盘加载到内存中,并将不再使用的页面换出内存上。
页表不仅可以映射内存地址,还可以直接映射磁盘等外设上的位置。所以程序的换入实际上是将页表上记录的磁盘位置下的代码和数据加载到内存中,并更新页表的映射关系使其指向内存中的位置。而程序的换出实际上是直接将内存中相应的代码和数据释放,并更新页表映射关系使其重新指向磁盘中的位置。
这种分批换入换出的方式使得程序可以使用比实际可用内存更大的虚拟地址空间,从而允许运行更大的程序。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。