finish
This commit is contained in:
parent
2c61ef07ff
commit
ec80779249
|
|
@ -224,7 +224,7 @@ MonoBehaviour:
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier:
|
m_EditorClassIdentifier:
|
||||||
m_Navigation:
|
m_Navigation:
|
||||||
m_Mode: 3
|
m_Mode: 0
|
||||||
m_WrapAround: 0
|
m_WrapAround: 0
|
||||||
m_SelectOnUp: {fileID: 0}
|
m_SelectOnUp: {fileID: 0}
|
||||||
m_SelectOnDown: {fileID: 0}
|
m_SelectOnDown: {fileID: 0}
|
||||||
|
|
|
||||||
|
|
@ -11955,7 +11955,7 @@ GameObject:
|
||||||
m_Icon: {fileID: 0}
|
m_Icon: {fileID: 0}
|
||||||
m_NavMeshLayer: 0
|
m_NavMeshLayer: 0
|
||||||
m_StaticEditorFlags: 0
|
m_StaticEditorFlags: 0
|
||||||
m_IsActive: 1
|
m_IsActive: 0
|
||||||
--- !u!224 &1887115188
|
--- !u!224 &1887115188
|
||||||
RectTransform:
|
RectTransform:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,10 @@ EditorBuildSettings:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Scenes:
|
m_Scenes:
|
||||||
- enabled: 1
|
- enabled: 0
|
||||||
path: Assets/Scenes/SampleScene.unity
|
path: Assets/Scenes/SampleScene.unity
|
||||||
guid: 2cda990e2423bbf4892e6590ba056729
|
guid: 2cda990e2423bbf4892e6590ba056729
|
||||||
|
- enabled: 1
|
||||||
|
path: Assets/Scenes/Main.unity
|
||||||
|
guid: 85bc7cc748291a344ba0188b5c2c378b
|
||||||
m_configObjects: {}
|
m_configObjects: {}
|
||||||
|
|
|
||||||
|
|
@ -268,7 +268,7 @@ InputManager:
|
||||||
negativeButton:
|
negativeButton:
|
||||||
positiveButton: enter
|
positiveButton: enter
|
||||||
altNegativeButton:
|
altNegativeButton:
|
||||||
altPositiveButton: space
|
altPositiveButton:
|
||||||
gravity: 1000
|
gravity: 1000
|
||||||
dead: 0.001
|
dead: 0.001
|
||||||
sensitivity: 1000
|
sensitivity: 1000
|
||||||
|
|
@ -485,3 +485,4 @@ InputManager:
|
||||||
type: 2
|
type: 2
|
||||||
axis: 5
|
axis: 5
|
||||||
joyNum: 0
|
joyNum: 0
|
||||||
|
m_UsePhysicalKeys: 1
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
# VMdemo
|
||||||
|
|
||||||
|
一个基于 Unity 的虚拟存储器地址转换可视化项目,用来演示从虚拟地址到物理地址的翻译过程,以及 TLB、二级页表、缺页中断与页面置换在一次内存访问中的作用。
|
||||||
|
|
||||||
|
项目目标不是做一个“黑盒计算器”,而是把一次地址访问拆成可观察、可暂停、可回放的步骤,方便课程讲解、实验演示和自学理解。
|
||||||
|
|
||||||
|
## 项目内容
|
||||||
|
|
||||||
|
当前项目围绕一次虚拟地址访问,提供以下可视化能力:
|
||||||
|
|
||||||
|
- 单步执行地址翻译流程
|
||||||
|
- 连续播放整段访问过程
|
||||||
|
- 展示 TLB、页表、FIFO 队列的实时状态
|
||||||
|
- 展示当前访问路径、关键决策原因和事件时间线
|
||||||
|
- 实时统计 TLB 命中率、页表命中率、缺页次数、平均访问开销
|
||||||
|
- 支持不同地址生成方式,观察局部性对命中率的影响
|
||||||
|
|
||||||
|
## 模拟规格
|
||||||
|
|
||||||
|
本项目当前采用固定核心规格:
|
||||||
|
|
||||||
|
| 项目 | 设定 |
|
||||||
|
| --- | --- |
|
||||||
|
| 机器位数 | 32-bit |
|
||||||
|
| 虚拟地址有效位 | 24-bit |
|
||||||
|
| 页大小 | 4 KB |
|
||||||
|
| 物理内存 | 1 MB |
|
||||||
|
| TLB | 8 项,全相联,LRU 替换 |
|
||||||
|
| 页表 | 二级页表,稀疏结构 |
|
||||||
|
| 页面置换 | FIFO |
|
||||||
|
|
||||||
|
可调参数主要包括:
|
||||||
|
|
||||||
|
- 访问次数 `N`
|
||||||
|
- 缺页惩罚 `pageFaultPenalty`
|
||||||
|
- 地址生成模式
|
||||||
|
- 顺序数组访问模式下的数组长度、元素大小、数组基址
|
||||||
|
|
||||||
|
目前支持两种地址生成模式:
|
||||||
|
|
||||||
|
- `RandomUniform`:在虚拟地址空间内均匀随机生成地址
|
||||||
|
- `SequentialArrayLoop`:按数组元素顺序循环访问,适合观察局部性
|
||||||
|
|
||||||
|
## 地址翻译流程
|
||||||
|
|
||||||
|
项目中的翻译引擎按以下步骤推进:
|
||||||
|
|
||||||
|
1. `GenerateVA`:生成本轮虚拟地址
|
||||||
|
2. `SplitVA`:拆分出 `VPN / offset / L1 / L2`
|
||||||
|
3. `LookupTLB`:查询 TLB
|
||||||
|
4. `LookupPageTable`:TLB 未命中后查询二级页表
|
||||||
|
5. `HandlePageFault`:页表未命中时分配页框并处理 FIFO 置换
|
||||||
|
6. `ComposePA`:合成物理地址
|
||||||
|
7. `Finalize`:记录统计信息并结束本轮访问
|
||||||
|
|
||||||
|
访问开销模型如下:
|
||||||
|
|
||||||
|
- TLB 命中:`+1`
|
||||||
|
- TLB miss + 页表命中:`+4`
|
||||||
|
- 缺页:`+4 + pageFaultPenalty`
|
||||||
|
|
||||||
|
## 可视化界面
|
||||||
|
|
||||||
|
主场景中会展示几类关键信息:
|
||||||
|
|
||||||
|
- 控制区:单步、连续播放、暂停、重置
|
||||||
|
- 配置区:访问次数、缺页惩罚、地址生成模式等输入
|
||||||
|
- 流程区:当前处于哪一个地址翻译步骤
|
||||||
|
- 表格区:TLB、页表关键项、FIFO 队列
|
||||||
|
- 追踪区:地址拆分、查询路径、当前决策原因
|
||||||
|
- 时间线区:每轮访问的事件日志
|
||||||
|
- 统计区:命中率、缺页次数、平均开销
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- Unity `2022.3.62f3c1`
|
||||||
|
- C#
|
||||||
|
- TextMesh Pro
|
||||||
|
- Unity UI
|
||||||
|
- Unity Test Framework
|
||||||
|
- DOTween
|
||||||
|
|
||||||
|
## 运行方式
|
||||||
|
|
||||||
|
### 1. 打开项目
|
||||||
|
|
||||||
|
使用 Unity Hub 安装并选择 `Unity 2022.3.62f3c1`,然后打开项目根目录:
|
||||||
|
|
||||||
|
```text
|
||||||
|
C:\UnityProjects\VMdemo
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 打开主场景
|
||||||
|
|
||||||
|
在 Unity 中打开:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Assets/Scenes/Main.unity
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 运行
|
||||||
|
|
||||||
|
点击 Play 后:
|
||||||
|
|
||||||
|
1. 根据需要调整访问次数、缺页惩罚和地址模式
|
||||||
|
2. 点击“单步”逐阶段观察地址翻译
|
||||||
|
3. 点击“连续播放”观察批量访问统计变化
|
||||||
|
4. 点击“暂停”或“重置”中断当前模拟
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
Assets/
|
||||||
|
Scenes/ 主场景
|
||||||
|
Scripts/
|
||||||
|
Core/ 配置、校验、地址拆分等基础模型
|
||||||
|
Simulation/ TLB、页表、物理内存、翻译引擎、统计模块
|
||||||
|
UI/ 界面控制、表格渲染、日志时间线、流程动画
|
||||||
|
Tests/
|
||||||
|
EditMode/ 核心逻辑的编辑器测试
|
||||||
|
doc/ 讲解材料、设计草稿与 TODO
|
||||||
|
Packages/ Unity 包依赖
|
||||||
|
ProjectSettings/ Unity 项目设置
|
||||||
|
```
|
||||||
|
|
||||||
|
## 关键脚本
|
||||||
|
|
||||||
|
- `Assets/Scripts/Core/SimulationConfig.cs`:模拟配置与固定规格定义
|
||||||
|
- `Assets/Scripts/Core/ConfigValidator.cs`:参数合法性校验与派生量计算
|
||||||
|
- `Assets/Scripts/Simulation/TranslatorEngine.cs`:地址翻译主流程状态机
|
||||||
|
- `Assets/Scripts/Simulation/TlbCache.cs`:TLB 查询与 LRU 替换
|
||||||
|
- `Assets/Scripts/Simulation/TwoLevelPageTable.cs`:二级页表实现
|
||||||
|
- `Assets/Scripts/Simulation/PhysicalMemoryManager.cs`:页框分配与 FIFO 管理
|
||||||
|
- `Assets/Scripts/Simulation/StatsCollector.cs`:统计指标计算
|
||||||
|
- `Assets/Scripts/UI/SimulationUIController.cs`:主界面交互与数据绑定
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
项目包含一组编辑器测试,覆盖核心逻辑:
|
||||||
|
|
||||||
|
- Step 1:参数与核心数据模型
|
||||||
|
- Step 2:地址拆分
|
||||||
|
- Step 3:TLB 与 LRU
|
||||||
|
- Step 4:二级页表与 FIFO 置换
|
||||||
|
- Step 5:翻译引擎状态机
|
||||||
|
- Step 6:统计模块
|
||||||
|
|
||||||
|
相关测试位于:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Assets/Tests/EditMode
|
||||||
|
```
|
||||||
|
|
||||||
|
可在 Unity Test Runner 中运行 EditMode Tests。
|
||||||
|
|
||||||
|
## 适用场景
|
||||||
|
|
||||||
|
这个项目适合用于:
|
||||||
|
|
||||||
|
- 操作系统课程中讲解虚拟存储器和地址翻译流程
|
||||||
|
- 展示 TLB 命中、页表命中、缺页之间的区别
|
||||||
|
- 对比随机访问与顺序访问对局部性的影响
|
||||||
|
- 作为后续扩展页面置换算法、页表结构或缓存模型的基础工程
|
||||||
|
|
||||||
|
## 后续可扩展方向
|
||||||
|
|
||||||
|
- 增加更多页面置换算法,例如 LRU / Clock
|
||||||
|
- 增加多种页表组织方式的切换
|
||||||
|
- 加入更细的访问耗时模型
|
||||||
|
- 增加截图、录屏或实验报告导出能力
|
||||||
|
- 增加更多教学案例预设
|
||||||
|
|
||||||
|
## 文档
|
||||||
|
|
||||||
|
仓库中的 `doc/` 目录保存了开发过程中的讲解资料与草稿,例如:
|
||||||
|
|
||||||
|
- `doc/MVP-TODO.md`
|
||||||
|
- `doc/dialog.md`
|
||||||
|
|
||||||
|
这些文档更偏向设计记录,正式使用说明以本 README 为准。
|
||||||
|
|
@ -0,0 +1,770 @@
|
||||||
|
## 课前准备(5分钟)
|
||||||
|
|
||||||
|
### 1. 环境检查脚本(`check_env.sh`)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
echo "========== WSL2 虚拟存储器演示环境 =========="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "系统架构与地址空间"
|
||||||
|
echo "CPU 架构: $(uname -m)"
|
||||||
|
echo "虚拟地址位数: $(getconf LONG_BIT) bit"
|
||||||
|
echo "用户空间地址范围: 0x0000000000000000 ~ 0x00007FFFFFFFFFFF (128TB)"
|
||||||
|
echo "理论可寻址空间: $((2**47 / 1024**4)) TB"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "物理资源限制"
|
||||||
|
echo "物理内存:"
|
||||||
|
free -h | grep Mem
|
||||||
|
echo ""
|
||||||
|
echo "Swap 空间:"
|
||||||
|
free -h | grep Swap
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "内核内存管理参数"
|
||||||
|
echo "Overcommit 策略: $(cat /proc/sys/vm/overcommit_memory) (0=启发式, 1=总是允许, 2=严格)"
|
||||||
|
echo "Overcommit 比例: $(cat /proc/sys/vm/overcommit_ratio)%"
|
||||||
|
echo "页大小: $(getconf PAGE_SIZE) bytes ($(($(getconf PAGE_SIZE)/1024)) KB)"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "WSL2 特有配置"
|
||||||
|
echo "WSL 版本:"
|
||||||
|
wsl.exe --status 2>/dev/null || echo "请在 Windows PowerShell 运行: wsl --status"
|
||||||
|
echo ""
|
||||||
|
echo "当前进程限制:"
|
||||||
|
ulimit -v # 虚拟内存限制
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
echo "准备演示程序"
|
||||||
|
ls -lh *.c 2>/dev/null || echo "请确保已编译所有演示程序"
|
||||||
|
```
|
||||||
|
|
||||||
|
**运行**:
|
||||||
|
```bash
|
||||||
|
chmod +x check_env.sh
|
||||||
|
./check_env.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 演示环节一:突破物理内存限制(10分钟)
|
||||||
|
|
||||||
|
### 核心程序(`demo1_overcommit.c`)
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <signal.h>
|
||||||
|
|
||||||
|
#define GB (1024ULL * 1024 * 1024)
|
||||||
|
|
||||||
|
volatile int stop = 0;
|
||||||
|
void handler(int sig) { stop = 1; }
|
||||||
|
|
||||||
|
void print_status(const char* label) {
|
||||||
|
printf("\n┌─ %s ──────────────────────┐\n", label);
|
||||||
|
char cmd[512];
|
||||||
|
snprintf(cmd, sizeof(cmd),
|
||||||
|
"echo \"│ 虚拟内存(VmSize):\" && cat /proc/%d/status | grep VmSize | awk '{print \"│ \" $2 \" \" $3}' && "
|
||||||
|
"echo \"│ 物理驻留(RSS):\" && cat /proc/%d/status | grep VmRSS | awk '{print \"│ \" $2 \" \" $3}' && "
|
||||||
|
"echo \"│ 交换空间(VmSwap):\" && cat /proc/%d/status | grep VmSwap | awk '{print \"│ \" $2 \" \" $3}'",
|
||||||
|
getpid(), getpid(), getpid());
|
||||||
|
system(cmd);
|
||||||
|
printf("└────────────────────────────────┘\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
signal(SIGINT, handler);
|
||||||
|
printf("【演示1】突破物理内存限制\n");
|
||||||
|
printf("PID: %d | 按 Ctrl+C 随时暂停\n\n", getpid());
|
||||||
|
|
||||||
|
// 获取物理内存
|
||||||
|
long pages = sysconf(_SC_PHYS_PAGES);
|
||||||
|
long page_size = sysconf(_SC_PAGE_SIZE);
|
||||||
|
double phys_gb = (double)pages * page_size / GB;
|
||||||
|
printf("► 系统物理内存: %.1f GB\n", phys_gb);
|
||||||
|
|
||||||
|
// 分配 3倍物理内存的虚拟空间
|
||||||
|
size_t alloc_size = (size_t)(phys_gb * 3 * GB);
|
||||||
|
printf("► 尝试分配: %.1f GB 虚拟内存 (3倍物理内存)\n", alloc_size / (double)GB);
|
||||||
|
printf(" 按回车继续...");
|
||||||
|
getchar();
|
||||||
|
|
||||||
|
void *mem = mmap(NULL, alloc_size, PROT_READ | PROT_WRITE,
|
||||||
|
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
|
||||||
|
if (mem == MAP_FAILED) {
|
||||||
|
perror("mmap 失败");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("✓ 分配成功! 虚拟地址: %p\n", mem);
|
||||||
|
print_status("分配后(未访问)");
|
||||||
|
printf(" → 注意: VmSize ≈ %.1f GB, RSS ≈ 0\n", alloc_size/(double)GB);
|
||||||
|
|
||||||
|
printf("\n► 开始稀疏访问(只使用 10%% 空间)...\n");
|
||||||
|
printf(" 按回车继续...");
|
||||||
|
getchar();
|
||||||
|
|
||||||
|
// 稀疏访问:每100页访问1页
|
||||||
|
long page_count = alloc_size / page_size;
|
||||||
|
for (int i = 0; i < page_count / 100 && !stop; i++) {
|
||||||
|
((char*)mem)[i * 100 * page_size] = 'A';
|
||||||
|
if (i % 1000 == 0) {
|
||||||
|
printf(" 已标记 %d 页 (%.2f MB)\r", i, i*100*page_size/(1024.0*1024));
|
||||||
|
fflush(stdout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print_status("稀疏访问后");
|
||||||
|
printf(" → RSS 应该很小(仅实际访问的页)\n");
|
||||||
|
|
||||||
|
printf("\n► 现在密集访问,直到触发 swap 或 OOM...\n");
|
||||||
|
printf(" 按回车继续(Ctrl+C 停止)...");
|
||||||
|
getchar();
|
||||||
|
|
||||||
|
size_t accessed = 0;
|
||||||
|
while (!stop && accessed < alloc_size) {
|
||||||
|
memset((char*)mem + accessed, 'X', 10 * 1024 * 1024); // 10MB 每步
|
||||||
|
accessed += 10 * 1024 * 1024;
|
||||||
|
if (accessed % (256 * 1024 * 1024) == 0) {
|
||||||
|
printf(" 已访问 %.0f MB\n", accessed/(1024.0*1024));
|
||||||
|
print_status("密集访问中");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print_status("最终状态");
|
||||||
|
munmap(mem, alloc_size);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**演示要点**:
|
||||||
|
1. **对比**:`VmSize` 立即达到 3GB,`RSS` 保持很小
|
||||||
|
2. **稀疏访问**:证明大地址空间 + 小物理内存 = 高效
|
||||||
|
3. **密集访问**:观察 `RSS` 增长 → `VmSwap` 增长 → 可能的 OOM
|
||||||
|
|
||||||
|
**配合监控**(另一个终端):
|
||||||
|
```bash
|
||||||
|
watch -n 0.3 'cat /proc/<PID>/status | grep -E "VmSize|VmRSS|VmSwap"'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 演示环节二:进程隔离与内存保护(8分钟)
|
||||||
|
|
||||||
|
### 核心程序(`demo2_isolation.c`)
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
int global = 100; // 数据段
|
||||||
|
|
||||||
|
void print_maps(const char* who) {
|
||||||
|
printf("\n【%s的内存映射】\n", who);
|
||||||
|
char cmd[256];
|
||||||
|
snprintf(cmd, sizeof(cmd),
|
||||||
|
"cat /proc/%d/maps | grep -E 'stack|heap|global' | head -5", getpid());
|
||||||
|
system(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
void print_vars(const char* who, int* local, int* dynamic) {
|
||||||
|
printf(" %s: global=%d @ %p, local=%d @ %p, dynamic=%d @ %p\n",
|
||||||
|
who, global, &global, *local, local, *dynamic, dynamic);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
printf("【演示2】进程隔离与内存保护\n");
|
||||||
|
printf("PID: %d\n\n", getpid());
|
||||||
|
|
||||||
|
int local = 200; // 栈
|
||||||
|
int *dynamic = malloc(sizeof(int));
|
||||||
|
*dynamic = 300; // 堆
|
||||||
|
|
||||||
|
printf("► 父进程初始状态:\n");
|
||||||
|
print_vars("父进程", &local, dynamic);
|
||||||
|
print_maps("父进程");
|
||||||
|
|
||||||
|
printf("\n► fork() 创建子进程...\n");
|
||||||
|
printf(" (子进程将复制父进程的虚拟地址空间,但物理页共享)\n");
|
||||||
|
printf(" 按回车继续...");
|
||||||
|
getchar();
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
// 子进程
|
||||||
|
printf("\n┌─ 子进程 (PID: %d) ─┐\n", getpid());
|
||||||
|
print_vars("子进程", &local, dynamic);
|
||||||
|
|
||||||
|
printf("\n► 子进程修改所有变量...\n");
|
||||||
|
global = 1000;
|
||||||
|
local = 2000;
|
||||||
|
*dynamic = 3000;
|
||||||
|
print_vars("修改后", &local, dynamic);
|
||||||
|
|
||||||
|
printf("\n► 查看 /proc/%d/maps(与父进程独立)\n", getpid());
|
||||||
|
char cmd[256];
|
||||||
|
snprintf(cmd, sizeof(cmd), "cat /proc/%d/maps | head -10", getpid());
|
||||||
|
system(cmd);
|
||||||
|
|
||||||
|
printf("\n 注意: 虚拟地址相同,但物理页已复制(COW)\n");
|
||||||
|
exit(0);
|
||||||
|
} else {
|
||||||
|
wait(NULL);
|
||||||
|
printf("\n└─ 父进程 (PID: %d) ─┘\n", getpid());
|
||||||
|
print_vars("父进程(子进程已退出)", &local, dynamic);
|
||||||
|
printf(" → 父进程变量未被修改!进程完全隔离\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
free(dynamic);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**进阶:验证 COW(写时复制)**
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 添加这段代码观察 COW 前后的页状态
|
||||||
|
#include <sys/resource.h>
|
||||||
|
|
||||||
|
void print_cow_stats(const char* label) {
|
||||||
|
struct rusage usage;
|
||||||
|
getrusage(RUSAGE_SELF, &usage);
|
||||||
|
printf(" %s: 次要缺页=%ld, 主要缺页=%ld\n",
|
||||||
|
label, usage.ru_minflt, usage.ru_majflt);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 演示环节三:内存共享机制(8分钟)
|
||||||
|
|
||||||
|
### 核心程序(`demo3_sharing.c`)
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
|
||||||
|
#define SIZE (100 * 1024 * 1024) // 100MB
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
printf("【演示3】内存共享机制\n\n");
|
||||||
|
|
||||||
|
// 创建共享内存对象
|
||||||
|
int fd = shm_open("/demo_shm", O_CREAT | O_RDWR, 0666);
|
||||||
|
ftruncate(fd, SIZE);
|
||||||
|
|
||||||
|
printf("► 创建共享内存: /demo_shm (%.1f MB)\n", SIZE/(1024.0*1024));
|
||||||
|
|
||||||
|
// 映射到虚拟地址空间
|
||||||
|
void *addr = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
|
||||||
|
printf("► 映射到虚拟地址: %p\n", addr);
|
||||||
|
|
||||||
|
// 填充数据
|
||||||
|
strcpy((char*)addr, "Hello from shared memory!");
|
||||||
|
|
||||||
|
printf("\n► fork() 创建子进程...\n");
|
||||||
|
printf(" 父子进程共享同一段物理内存!\n");
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
printf("\n┌─ 子进程 ─┐\n");
|
||||||
|
printf(" 读取共享内存: %s\n", (char*)addr);
|
||||||
|
printf(" 修改共享内存...");
|
||||||
|
strcpy((char*)addr, "Modified by child!");
|
||||||
|
printf("完成\n");
|
||||||
|
exit(0);
|
||||||
|
} else {
|
||||||
|
wait(NULL);
|
||||||
|
printf("\n└─ 父进程 ─┘\n");
|
||||||
|
printf(" 读取共享内存: %s\n", (char*)addr);
|
||||||
|
printf(" → 子进程的修改立即可见!\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对比:普通内存 vs 共享内存
|
||||||
|
printf("\n► 对比:普通堆内存 vs 共享内存\n");
|
||||||
|
char *heap_mem = malloc(1024);
|
||||||
|
strcpy(heap_mem, "Heap data");
|
||||||
|
|
||||||
|
printf(" 堆内存地址: %p (仅本进程可见)\n", (void*)heap_mem);
|
||||||
|
printf(" 共享内存地址: %p (多进程共享)\n", addr);
|
||||||
|
|
||||||
|
// 查看映射
|
||||||
|
printf("\n► 查看 /proc/%d/maps 中的共享内存\n", getpid());
|
||||||
|
char cmd[256];
|
||||||
|
snprintf(cmd, "cat /proc/%d/maps | grep demo_shm", getpid());
|
||||||
|
system(cmd);
|
||||||
|
|
||||||
|
munmap(addr, SIZE);
|
||||||
|
shm_unlink("/demo_shm");
|
||||||
|
free(heap_mem);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**编译需要**:
|
||||||
|
```bash
|
||||||
|
gcc -o demo3_sharing demo3_sharing.c -lrt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 演示环节四:内存保护(5分钟)
|
||||||
|
|
||||||
|
### 核心程序(`demo4_protection.c`)
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <setjmp.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
|
||||||
|
sigjmp_buf env;
|
||||||
|
|
||||||
|
void segfault_handler(int sig) {
|
||||||
|
printf("✓ 捕获到段错误 (SIGSEGV) - 内存保护生效!\n");
|
||||||
|
siglongjmp(env, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
printf("【演示4】内存保护机制\n\n");
|
||||||
|
|
||||||
|
signal(SIGSEGV, segfault_handler);
|
||||||
|
|
||||||
|
// 分配内存,设置不同权限
|
||||||
|
size_t size = 4096 * 3;
|
||||||
|
char *mem = mmap(NULL, size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
|
||||||
|
|
||||||
|
printf("► 分配 3 页内存,初始权限: PROT_NONE(完全不可访问)\n");
|
||||||
|
printf(" 虚拟地址: %p\n", (void*)mem);
|
||||||
|
|
||||||
|
if (sigsetjmp(env, 1) == 0) {
|
||||||
|
printf("\n► 尝试读取(应失败)...");
|
||||||
|
fflush(stdout);
|
||||||
|
char x = mem[0]; // 触发 SIGSEGV
|
||||||
|
printf("错误:未触发保护!\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改第一页为只读
|
||||||
|
printf("\n► 修改第一页为 PROT_READ(只读)\n");
|
||||||
|
mprotect(mem, 4096, PROT_READ);
|
||||||
|
|
||||||
|
if (sigsetjmp(env, 1) == 0) {
|
||||||
|
printf(" 尝试读取: %d ✓\n", mem[0]);
|
||||||
|
printf(" 尝试写入...");
|
||||||
|
fflush(stdout);
|
||||||
|
mem[0] = 'A'; // 应失败
|
||||||
|
printf("错误:未触发保护!\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改第二页为可读写
|
||||||
|
printf("\n► 修改第二页为 PROT_READ | PROT_WRITE(读写)\n");
|
||||||
|
mprotect(mem + 4096, 4096, PROT_READ | PROT_WRITE);
|
||||||
|
mem[4096] = 'X';
|
||||||
|
printf(" 读写成功: %c ✓\n", mem[4096]);
|
||||||
|
|
||||||
|
// 第三页保持 PROT_NONE
|
||||||
|
|
||||||
|
printf("\n► 查看权限设置:\n");
|
||||||
|
printf(" 页1 (%p): 只读\n", (void*)mem);
|
||||||
|
printf(" 页2 (%p): 读写\n", (void*)(mem + 4096));
|
||||||
|
printf(" 页3 (%p): 不可访问\n", (void*)(mem + 8192));
|
||||||
|
|
||||||
|
printf("\n► 验证 /proc/%d/maps:\n", getpid());
|
||||||
|
char cmd[256];
|
||||||
|
snprintf(cmd, "cat /proc/%d/maps | grep %p", getpid(), (void*)mem);
|
||||||
|
system(cmd);
|
||||||
|
|
||||||
|
munmap(mem, size);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 演示环节五:简化内存管理与屏蔽底层差异(4分钟)
|
||||||
|
|
||||||
|
### 快速演示(`demo5_management.c`)
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
// 展示虚拟内存如何简化编程:连续大数组,无需关心物理碎片
|
||||||
|
int main() {
|
||||||
|
printf("【演示5】简化内存管理 & 屏蔽底层差异\n\n");
|
||||||
|
|
||||||
|
printf("► 程序员视角:申请 1GB 连续数组\n");
|
||||||
|
size_t size = 1024 * 1024 * 1024;
|
||||||
|
int *big_array = malloc(size);
|
||||||
|
|
||||||
|
if (!big_array) {
|
||||||
|
printf(" 分配失败\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf(" ✓ 成功: big_array[0] @ %p\n", (void*)&big_array[0]);
|
||||||
|
printf(" ✓ 成功: big_array[262144] @ %p (中间)\n",
|
||||||
|
(void*)&big_array[262144]);
|
||||||
|
printf(" ✓ 成功: big_array[268435455] @ %p (末尾)\n",
|
||||||
|
(void*)&big_array[268435455]);
|
||||||
|
|
||||||
|
printf("\n► 物理现实(由内核处理):\n");
|
||||||
|
printf(" - 物理内存可能是碎片化的\n");
|
||||||
|
printf(" - 通过页表映射为'连续'的虚拟地址\n");
|
||||||
|
printf(" - 程序员无需关心物理地址、NUMA 节点、内存 bank\n");
|
||||||
|
|
||||||
|
printf("\n► 查看实际物理页分布:\n");
|
||||||
|
system("cat /proc/self/pagemap 2>/dev/null | head -5 || echo '需要 root 权限读取 pagemap'");
|
||||||
|
|
||||||
|
printf("\n► 跨平台一致性:\n");
|
||||||
|
printf(" 这段代码在以下环境表现一致:\n");
|
||||||
|
printf(" - WSL2 (Hyper-V 虚拟化)\n");
|
||||||
|
printf(" - 原生 Linux (x86_64)\n");
|
||||||
|
printf(" - 云服务器 (KVM/Xen)\n");
|
||||||
|
printf(" - 嵌入式 ARM\n");
|
||||||
|
printf(" 虚拟存储器屏蔽了硬件差异!\n");
|
||||||
|
|
||||||
|
free(big_array);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完整演示流程表
|
||||||
|
|
||||||
|
| 时间 | 环节 | 操作 | 可视化重点 |
|
||||||
|
|:---|:---|:---|:---|
|
||||||
|
| **0-5'** | 环境检查 | 运行 `check_env.sh` | 地址位数、物理内存、overcommit 策略 |
|
||||||
|
| **5-15'** | 突破物理限制 | 运行 `demo1` + `watch` 监控 | `VmSize` vs `RSS` 巨大差异,swap 增长 |
|
||||||
|
| **15-23'** | 进程隔离 | 运行 `demo2`,对比父子进程 | 相同虚拟地址,独立物理空间,COW 机制 |
|
||||||
|
| **23-31'** | 内存共享 | 运行 `demo3`,对比 `mmap` 共享 vs 堆内存 | 多进程看到相同数据,映射名称可见 |
|
||||||
|
| **31-36'** | 内存保护 | 运行 `demo4`,触发段错误 | `mprotect` 权限变化,`maps` 中 r/w/x 标志 |
|
||||||
|
| **36-40'** | 简化管理 | 运行 `demo5`,总结 | 连续虚拟地址 vs 物理碎片,跨平台一致性 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 课堂互动设计
|
||||||
|
|
||||||
|
### 预测-验证环节
|
||||||
|
在每个演示前,先让学生预测结果:
|
||||||
|
|
||||||
|
> "我要分配 3 倍物理内存,malloc 会成功吗?"
|
||||||
|
> - 预测 A:失败,内存不够
|
||||||
|
> - 预测 B:成功,但会立即占用大量物理内存
|
||||||
|
> - 预测 C:成功,且几乎不占用物理内存
|
||||||
|
>
|
||||||
|
> **验证**:运行代码,观察 `VmSize` 和 `RSS`
|
||||||
|
|
||||||
|
### 实时对比(双终端投影)
|
||||||
|
|
||||||
|
**左屏**:运行程序,显示输出
|
||||||
|
**右屏**:持续运行监控命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 推荐监控组合
|
||||||
|
watch -n 0.5 'clear; echo "=== 进程 ==="; cat /proc/<PID>/status | grep -E "Vm|Rss|Swap"; echo ""; echo "=== 系统 ==="; free -h'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键结论板书
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
┌─────────────────────────────────────────┐
|
||||||
|
│ 虚拟存储器 = 物理内存的"魔术师" │
|
||||||
|
├─────────────────────────────────────────┤
|
||||||
|
│ 1. 空间魔法: 小物理 → 大虚拟地址空间 │
|
||||||
|
│ 2. 隔离魔法: 多进程互不干扰 │
|
||||||
|
│ 3. 共享魔法: 相同物理页多视角映射 │
|
||||||
|
│ 4. 保护魔法: 硬件级权限控制 │
|
||||||
|
│ 5. 简化魔法: 连续地址屏蔽物理复杂性 │
|
||||||
|
└─────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# 进程内存隔离实时监控 - 纯外部版本 (Plan B)
|
||||||
|
# 通过 /proc 文件系统直接分析,无需修改目标程序
|
||||||
|
|
||||||
|
TARGET_PID=""
|
||||||
|
REFRESH_INTERVAL=0.5
|
||||||
|
|
||||||
|
# 清屏
|
||||||
|
clear_screen() {
|
||||||
|
printf "\033[2J\033[H"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 绘制标题
|
||||||
|
draw_header() {
|
||||||
|
local title=$1
|
||||||
|
printf "════════════════════════════════════════════════════════════════\n"
|
||||||
|
printf " %s\n" "$title"
|
||||||
|
printf "════════════════════════════════════════════════════════════════\n\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取进程基本信息
|
||||||
|
get_proc_basic() {
|
||||||
|
local pid=$1
|
||||||
|
if [ ! -d "/proc/$pid" ]; then
|
||||||
|
echo "进程不存在"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local name=$(cat /proc/$pid/comm 2>/dev/null)
|
||||||
|
local ppid=$(grep PPid /proc/$pid/status 2>/dev/null | awk '{print $2}')
|
||||||
|
local state=$(grep State /proc/$pid/status 2>/dev/null | awk '{print $2}')
|
||||||
|
printf "进程名: %-15s | PPID: %-6s | 状态: %s\n" "$name" "${ppid:-0}" "$state"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取内存统计
|
||||||
|
get_mem_stats() {
|
||||||
|
local pid=$1
|
||||||
|
if [ ! -f "/proc/$pid/status" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local vmsize=$(grep VmSize /proc/$pid/status 2>/dev/null | awk '{print $2}')
|
||||||
|
local vmrss=$(grep VmRSS /proc/$pid/status 2>/dev/null | awk '{print $2}')
|
||||||
|
local vmdata=$(grep VmData /proc/$pid/status 2>/dev/null | awk '{print $2}')
|
||||||
|
local vmstk=$(grep VmStk /proc/$pid/status 2>/dev/null | awk '{print $2}')
|
||||||
|
|
||||||
|
printf " 虚拟内存(VmSize): %8s KB (%6.2f MB)\n" "${vmsize:-0}" $(echo "scale=2; ${vmsize:-0}/1024" | bc 2>/dev/null || echo "0")
|
||||||
|
printf " 物理驻留(VmRSS): %8s KB (%6.2f MB)\n" "${vmrss:-0}" $(echo "scale=2; ${vmrss:-0}/1024" | bc 2>/dev/null || echo "0")
|
||||||
|
printf " 数据段(VmData): %8s KB\n" "${vmdata:-0}"
|
||||||
|
printf " 栈段(VmStk): %8s KB\n" "${vmstk:-0}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取关键内存映射(堆、栈、全局数据)
|
||||||
|
get_key_mappings() {
|
||||||
|
local pid=$1
|
||||||
|
if [ ! -f "/proc/$pid/maps" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf " %-20s %-18s %-10s %s\n" "区域" "虚拟地址范围" "权限" "大小"
|
||||||
|
printf " %-20s %-18s %-10s %s\n" "────────────────────" "──────────────────" "──────────" "────"
|
||||||
|
|
||||||
|
# 堆
|
||||||
|
local heap=$(grep "\\[heap\\]" /proc/$pid/maps 2>/dev/null | head -1)
|
||||||
|
if [ -n "$heap" ]; then
|
||||||
|
local range=$(echo "$heap" | awk '{print $1}')
|
||||||
|
local perm=$(echo "$heap" | awk '{print $2}')
|
||||||
|
local size_kb=$(echo "$heap" | awk '{print $3}')
|
||||||
|
printf " %-20s %-18s %-10s %s\n" "[堆] heap" "$range" "$perm" "${size_kb:-动态增长}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 栈
|
||||||
|
local stack=$(grep "\\[stack\\]" /proc/$pid/maps 2>/dev/null | head -1)
|
||||||
|
if [ -n "$stack" ]; then
|
||||||
|
local range=$(echo "$stack" | awk '{print $1}')
|
||||||
|
local perm=$(echo "$stack" | awk '{print $2}')
|
||||||
|
printf " %-20s %-18s %-10s %s\n" "[栈] stack" "$range" "$perm" "动态扩展"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 全局数据段(通过 maps 中的 data 段或程序段识别)
|
||||||
|
local data_seg=$(grep -E "\.data|rw-p.*[0-9a-f]+ [0-9a-f]+ [0-9a-f]+ .*$" /proc/$pid/maps 2>/dev/null | grep -v heap | head -1)
|
||||||
|
if [ -n "$data_seg" ]; then
|
||||||
|
local range=$(echo "$data_seg" | awk '{print $1}')
|
||||||
|
local perm=$(echo "$data_seg" | awk '{print $2}')
|
||||||
|
printf " %-20s %-18s %-10s %s\n" "[数据段] data" "$range" "$perm" "全局变量"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取 COW 相关信息(通过 smaps)
|
||||||
|
get_cow_info() {
|
||||||
|
local pid=$1
|
||||||
|
if [ ! -f "/proc/$pid/smaps_rollup" ]; then
|
||||||
|
printf " (内核不支持 smaps_rollup,跳过 COW 分析)\n"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local shared_clean=$(grep "Shared_Clean" /proc/$pid/smaps_rollup 2>/dev/null | awk '{print $2}')
|
||||||
|
local shared_dirty=$(grep "Shared_Dirty" /proc/$pid/smaps_rollup 2>/dev/null | awk '{print $2}')
|
||||||
|
local private_clean=$(grep "Private_Clean" /proc/$pid/smaps_rollup 2>/dev/null | awk '{print $2}')
|
||||||
|
local private_dirty=$(grep "Private_Dirty" /proc/$pid/smaps_rollup 2>/dev/null | awk '{print $2}')
|
||||||
|
|
||||||
|
printf " 共享内存(Shared): %8s KB (Clean: %s, Dirty: %s)\n" \
|
||||||
|
"$((${shared_clean:-0} + ${shared_dirty:-0}))" "${shared_clean:-0}" "${shared_dirty:-0}"
|
||||||
|
printf " 私有内存(Private): %8s KB (Clean: %s, Dirty: %s)\n" \
|
||||||
|
"$((${private_clean:-0} + ${private_dirty:-0}))" "${private_clean:-0}" "${private_dirty:-0}"
|
||||||
|
|
||||||
|
# COW 判断:Private_Dirty 增加表示发生了写时复制
|
||||||
|
if [ "${private_dirty:-0}" -gt 0 ]; then
|
||||||
|
printf " → Private_Dirty: %s KB (可能发生 COW 或写入操作)\n" "$private_dirty"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# 单进程监控视图
|
||||||
|
monitor_single() {
|
||||||
|
local pid=$1
|
||||||
|
local label=$2
|
||||||
|
|
||||||
|
clear_screen
|
||||||
|
draw_header "进程内存监控 - $label (PID: $pid)"
|
||||||
|
|
||||||
|
printf "【基本信息】\n"
|
||||||
|
get_proc_basic $pid
|
||||||
|
|
||||||
|
printf "\n【内存统计】\n"
|
||||||
|
get_mem_stats $pid
|
||||||
|
|
||||||
|
printf "\n【关键映射】\n"
|
||||||
|
get_key_mappings $pid
|
||||||
|
|
||||||
|
printf "\n【COW / 共享状态】\n"
|
||||||
|
get_cow_info $pid
|
||||||
|
|
||||||
|
printf "\n────────────────────────────────────────────────────────────────\n"
|
||||||
|
printf "提示: 按 Ctrl+C 退出 | 检测到 fork 将自动切换对比模式\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 双进程对比视图
|
||||||
|
monitor_compare() {
|
||||||
|
local parent_pid=$1
|
||||||
|
local child_pid=$2
|
||||||
|
|
||||||
|
clear_screen
|
||||||
|
draw_header "父子进程内存隔离对比"
|
||||||
|
|
||||||
|
# 检查进程是否存在
|
||||||
|
if [ ! -d "/proc/$parent_pid" ] || [ ! -d "/proc/$child_pid" ]; then
|
||||||
|
printf "错误: 父进程或子进程已退出\n"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 获取数据
|
||||||
|
local parent_vmrss=$(grep VmRSS /proc/$parent_pid/status 2>/dev/null | awk '{print $2}')
|
||||||
|
local child_vmrss=$(grep VmRSS /proc/$child_pid/status 2>/dev/null | awk '{print $2}')
|
||||||
|
local parent_private=$(grep Private_Dirty /proc/$parent_pid/smaps_rollup 2>/dev/null | awk '{print $2}')
|
||||||
|
local child_private=$(grep Private_Dirty /proc/$child_pid/smaps_rollup 2>/dev/null | awk '{print $2}')
|
||||||
|
|
||||||
|
# 计算指标
|
||||||
|
local total_rss=$(( (${parent_vmrss:-0} + ${child_vmrss:-0}) ))
|
||||||
|
local rss_diff=$(( ${child_vmrss:-0} - ${parent_vmrss:-0} ))
|
||||||
|
|
||||||
|
printf "┌────────────────────────────┬────────────────────────────┐\n"
|
||||||
|
printf "│ 父进程 (PID: %-11s)│ 子进程 (PID: %-11s)│\n" "$parent_pid" "$child_pid"
|
||||||
|
printf "├────────────────────────────┼────────────────────────────┤\n"
|
||||||
|
|
||||||
|
# 基本信息
|
||||||
|
local parent_name=$(cat /proc/$parent_pid/comm 2>/dev/null)
|
||||||
|
local child_name=$(cat /proc/$child_pid/comm 2>/dev/null)
|
||||||
|
printf "│ 名称: %-20s │ 名称: %-20s │\n" "$parent_name" "$child_name"
|
||||||
|
|
||||||
|
printf "├────────────────────────────┼────────────────────────────┤\n"
|
||||||
|
|
||||||
|
# 内存对比
|
||||||
|
printf "│【虚拟内存(VmSize)】 │【虚拟内存(VmSize)】 │\n"
|
||||||
|
local parent_vm=$(grep VmSize /proc/$parent_pid/status 2>/dev/null | awk '{print $2}')
|
||||||
|
local child_vm=$(grep VmSize /proc/$child_pid/status 2>/dev/null | awk '{print $2}')
|
||||||
|
printf "│ %8s KB (%6.2f MB) │ %8s KB (%6.2f MB) │\n" \
|
||||||
|
"${parent_vm:-0}" $(echo "scale=2; ${parent_vm:-0}/1024" | bc 2>/dev/null || echo "0") \
|
||||||
|
"${child_vm:-0}" $(echo "scale=2; ${child_vm:-0}/1024" | bc 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
printf "├────────────────────────────┼────────────────────────────┤\n"
|
||||||
|
printf "│【物理内存(VmRSS)】 │【物理内存(VmRSS)】 │\n"
|
||||||
|
printf "│ %8s KB (%6.2f MB) │ %8s KB (%6.2f MB) │\n" \
|
||||||
|
"${parent_vmrss:-0}" $(echo "scale=2; ${parent_vmrss:-0}/1024" | bc 2>/dev/null || echo "0") \
|
||||||
|
"${child_vmrss:-0}" $(echo "scale=2; ${child_vmrss:-0}/1024" | bc 2>/dev/null || echo "0")
|
||||||
|
|
||||||
|
printf "├────────────────────────────┼────────────────────────────┤\n"
|
||||||
|
printf "│【私有脏页(Private_Dirty)】 │【私有脏页(Private_Dirty)】 │\n"
|
||||||
|
printf "│ %8s KB │ %8s KB │\n" "${parent_private:-0}" "${child_private:-0}"
|
||||||
|
|
||||||
|
printf "├────────────────────────────┼────────────────────────────┤\n"
|
||||||
|
printf "│【虚拟地址空间】 │【虚拟地址空间】 │\n"
|
||||||
|
printf "│ 与 fork 前相同 │ 继承父进程(初始相同) │\n"
|
||||||
|
|
||||||
|
printf "├────────────────────────────┼────────────────────────────┤\n"
|
||||||
|
printf "│【物理页状态】 │【物理页状态】 │\n"
|
||||||
|
if [ "${child_private:-0}" -gt "${parent_private:-0}" ] && [ "${child_private:-0}" -gt 4 ]; then
|
||||||
|
printf "│ 部分共享 (COW 可能已触发) │ 已独立 (COW 已触发) │\n"
|
||||||
|
else
|
||||||
|
printf "│ 共享 (只读,等待写入) │ 共享 (只读,等待写入) │\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "└────────────────────────────┴────────────────────────────┘\n"
|
||||||
|
|
||||||
|
# COW 分析总结
|
||||||
|
printf "\n【COW (写时复制) 分析】\n"
|
||||||
|
printf " 父子 RSS 总和: %d KB (%.2f MB)\n" "$total_rss" $(echo "scale=2; $total_rss/1024" | bc 2>/dev/null || echo "0")
|
||||||
|
printf " 子进程 RSS 增量: %d KB\n" "${rss_diff:-0}"
|
||||||
|
|
||||||
|
if [ "${child_private:-0}" -gt 4 ]; then
|
||||||
|
printf " → 检测到子进程有 %s KB 私有脏页,COW 已触发!\n" "${child_private:-0}"
|
||||||
|
printf " → 子进程修改了共享页,内核已复制物理页框\n"
|
||||||
|
else
|
||||||
|
printf " → 子进程 Private_Dirty 较低,COW 尚未触发或修改较少\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "\n────────────────────────────────────────────────────────────────\n"
|
||||||
|
printf "提示: 按 Ctrl+C 退出 | 子进程退出将返回单进程视图\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 主监控循环
|
||||||
|
main() {
|
||||||
|
if [ $# -lt 1 ]; then
|
||||||
|
echo "用法: $0 <目标进程PID>"
|
||||||
|
echo ""
|
||||||
|
echo "功能:"
|
||||||
|
echo " 1. 监控单进程内存状态(堆、栈、数据段)"
|
||||||
|
echo " 2. 自动检测 fork() 创建的子进程"
|
||||||
|
echo " 3. 对比父子进程的 COW (写时复制) 状态"
|
||||||
|
echo ""
|
||||||
|
echo "示例:"
|
||||||
|
echo " 终端1: ./demo2_isolation"
|
||||||
|
echo " 终端2: $0 \$(pgrep demo2_isolation | head -1)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
local parent_pid=$1
|
||||||
|
local child_pid=""
|
||||||
|
|
||||||
|
# 验证初始进程存在
|
||||||
|
if [ ! -d "/proc/$parent_pid" ]; then
|
||||||
|
echo "错误: 进程 $parent_pid 不存在"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
trap 'clear_screen; echo "监控已退出"; exit 0' INT
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
# 检测子进程(通过 PPID 匹配)
|
||||||
|
if [ -z "$child_pid" ] || [ ! -d "/proc/$child_pid" ]; then
|
||||||
|
# 查找目标进程的子进程
|
||||||
|
child_pid=$(pgrep -P $parent_pid 2>/dev/null | head -1)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 如果父进程退出,结束监控
|
||||||
|
if [ ! -d "/proc/$parent_pid" ]; then
|
||||||
|
clear_screen
|
||||||
|
echo "目标进程已退出,监控结束"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 选择视图
|
||||||
|
if [ -n "$child_pid" ] && [ -d "/proc/$child_pid" ]; then
|
||||||
|
monitor_compare $parent_pid $child_pid
|
||||||
|
else
|
||||||
|
monitor_single $parent_pid "父进程"
|
||||||
|
fi
|
||||||
|
|
||||||
|
sleep $REFRESH_INTERVAL
|
||||||
|
done
|
||||||
|
}
|
||||||
|
```
|
||||||
Binary file not shown.
Reference in New Issue