个人Linux操作系统学习笔记12 - 虚拟地址空间
验证地址空间
#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;
}
打印结果:
code addr: 0x40055d
read only string addr: 0x400800
init global addr: 0x601034
uninit global addr: 0x601040
heap addr: 0x8e2010
heap addr: 0x8e2030
heap addr: 0x8e2050
heap addr: 0x8e2070 # 堆向上(高地址)增长
test static addr: 0x601038
stack addr: 0x7ffdf55f13d8
stack addr: 0x7ffdf55f13d0
stack addr: 0x7ffdf55f13c8
stack addr: 0x7ffdf55f13c0 # 栈向下(低地址)增长
argv[0]: 0x7ffdf55f17d0
env[0]: 0x7ffdf55f17d8
env[1]: 0x7ffdf55f17ed
env[2]: 0x7ffdf55f180e
env[3]: 0x7ffdf55f1819
env[4]: 0x7ffdf55f1829
env[5]: 0x7ffdf55f1837
env[6]: 0x7ffdf55f1859
env[7]: 0x7ffdf55f186e
env[8]: 0x7ffdf55f1881
env[9]: 0x7ffdf55f188e
env[10]: 0x7ffdf55f18d5
env[11]: 0x7ffdf55f1e71
env[12]: 0x7ffdf55f1e8e
env[13]: 0x7ffdf55f1ef0
env[14]: 0x7ffdf55f1f12
env[15]: 0x7ffdf55f1f23
env[16]: 0x7ffdf55f1f3a
env[17]: 0x7ffdf55f1f42
env[18]: 0x7ffdf55f1f55
env[19]: 0x7ffdf55f1f65
env[20]: 0x7ffdf55f1f71
env[21]: 0x7ffdf55f1fa4
env[22]: 0x7ffdf55f1fc7
env[23]: 0x7ffdf55f1fe6
注意点:
-
从上到下的布局:
[高地址] 内核空间 命令行参数环境变量 栈 ↓ 共享区 ↑ 堆 未初始化数据 初始化数据 (字符常量区) 正文代码 [低地址] -
栈向下(低地址)增长,堆向上(高地址)增长
堆与栈相对而生
-
在栈中:整体的空间利用是从高到低,但是每一个变量内部是从低到高
因为每一个变量的
&取地址都是取最低的地址,因此在变量中都是先利用最小的地址。数组内部元素同理。下标小的在低地址,下标大的在高地址
以上讲的不是真正的内存,而是 “进程地址空间/虚拟地址空间”,这是操作系统的概念!
不叫 “程序地址空间”!!!
#include <stdio.h>
#include <unistd.h>
int g_val = 100;
int main()
{
printf("g_val: %d, &g_val: %p\n", g_val, &g_val);
pid_t id = fork();
if(id == 0)
{
while(1)
{
printf("child process: pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
++g_val;
}
}
else if(id > 0)
{
while(1)
{
printf("parent process: pid: %d, ppid: %d, g_val: %d, &g_val: %p\n", getpid(), getppid(), g_val, &g_val);
sleep(1);
}
}
else
{
printf("fork ERROR!\n");
return 1;
}
return 0;
}
运行结果:
g_val: 100, &g_val: 0x601054
parent process: pid: 12501, ppid: 29548, g_val: 100, &g_val: 0x601054
child process: pid: 12502, ppid: 12501, g_val: 100, &g_val: 0x601054
parent process: pid: 12501, ppid: 29548, g_val: 100, &g_val: 0x601054
child process: pid: 12502, ppid: 12501, g_val: 101, &g_val: 0x601054
parent process: pid: 12501, ppid: 29548, g_val: 100, &g_val: 0x601054
child process: pid: 12502, ppid: 12501, g_val: 102, &g_val: 0x601054
parent process: pid: 12501, ppid: 29548, g_val: 100, &g_val: 0x601054
child process: pid: 12502, ppid: 12501, g_val: 103, &g_val: 0x601054
parent process: pid: 12501, ppid: 29548, g_val: 100, &g_val: 0x601054
child process: pid: 12502, ppid: 12501, g_val: 104, &g_val: 0x601054
parent process: pid: 12501, ppid: 29548, g_val: 100, &g_val: 0x601054
child process: pid: 12502, ppid: 12501, g_val: 105, &g_val: 0x601054
parent process: pid: 12501, ppid: 29548, g_val: 100, &g_val: 0x601054
child process: pid: 12502, ppid: 12501, g_val: 106, &g_val: 0x601054
^C
- 子进程试图修改变量时,会发生写时拷贝——进程具有独立性
为什么两个进程打印的 g_val 地址相同 ?
说明,该地址不是物理地址!
曾经在编程语言中学的指针也不是物理地址!
以前学的地址都叫虚拟地址!
计算机中存在两套地址:物理地址与虚拟地址空间的虚拟地址!
曾经学的 000…00 至 FFF…FF 都是虚拟地址
操作系统创建进程时,会为进程创建一个页表——保存虚拟地址对物理地址的映射
初始化进程时也要初始化页表
用户使用的地址是虚拟地址,对某个地址进行修改时,内核需要找出对应的物理地址后再进行修改
子进程在创建时,是以父进程为模板
进程 = 内核数据结构 + 代码与数据
-
虚拟地址空间和页表,每个进程各有一套
-
fork之后,父子共享代码和数据
——子进程会拷贝父进程的页表,类似发生了浅拷贝
-
旧结论:进程具有独立性
如何保证进程独立性:
-
代码为只读,父子不会互相影响
-
如果数据被修改,会导致进程不独立
OS规定:父子中,任何一个进程尝试对共享的变量进行修改时,不能直接修改,而是发生写时拷贝!
此时试图修改共享变量的进程的页表中会获得新的物理地址——类比深拷贝
代码和数据都是独立的!
-
物理地址被操作系统隐藏,用户无法访问——可以保护内存数据
虚拟地址空间如何管理?
先描述再组织!
类比进程管理,linux系统中有一个mm_struct的结构体用于管理每一个进程的虚拟地址空间
mm_struct 可以在 task_struct 中找到
如何理解虚拟地址空间中的区域划分?
struct v_memory
{
int task1_start;
int task1_end;
int task2_start;
int task2_end;
// ...
}
void test()
{
struct v_memory vm(1, 50, 50, 100);
}
区域内的任何位置,对应的进程可以使用
当需要调整内存中某个区域的大小时,可修改结构体变量中成员变量的值
什么是页表?查表工作谁来做?为什么要有虚拟地址空间?其它子问题
-
页表基础理解
一个多列表格,内容包括虚拟地址、物理地址、标志位(例如记录rwx权限)
虚拟地址 物理地址 权限 是否存在内存中 其它标志位 001234 001111 r 1 … … … r/w/x三种权限 1表示存在于物理内存;0表示不存在于物理内存,数据被挂起至swap分区 … -
查表工作由
MMU(Memory Management Unit 内存管理单元,CPU的一个硬件)完成 -
为什么全局变量、static变量的生命周期是全局的?
因为这两种变量存于初始化数据/未初始化数据区,本质是生命周期随进程
-
常量字符串和代码为什么是只读的?
页表给进程提供的限制——不允许进程写入
-
其它语言如java/py,都有虚拟地址,java虚拟机由cpp写成,py解释器由cpp写成
-
为什么要有虚拟地址空间?
-
控制进程的行为,拦截进程的非法行为,保护物理内存!
-
有了虚拟地址空间和页表,原则上:
- 数据可以存在于物理内存的任意位置,而不用严格按照虚拟地址空间中的布局
因此可以做到:将内存布局无序变有序!
-
在内核层面上实现进程管理与内存管理解耦合!
-
在Linux系统中,创建一个进程时,先创建内核数据结构,再加载代码和数据
-
由于上面这个机制,可以实现——懒加载机制!
在不急着执行该进程时,可以先只创建内核数据结构,暂不加载代码和数据;等到需要运行时再加载代码和数据。
使得物理内存能够得到更好的利用!
-
例如:在Cpp程序中,使用malloc申请的空间不一定会马上使用
因此本质是在虚拟地址空间开辟空间,在填写了页表的虚拟地址栏,但是物理地址栏留空,等待需要使用时再进行动态申请!
该过程名为——缺页中断引起内存二次申请!
-
-
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)