操作系统实验三:内存管理

实验目的

  1. 理解页面淘汰算法原理,编写程序演示页面淘汰算法。

  2. 验证Linux虚拟地址转化为物理地址的机制

  3. 理解和验证程序运行局部性的原理。

  4. 理解和验证缺页处理的流程。

实验内容

  1. Win/Linux编写二维数组遍历程序,理解局部性的原理。

  2. Windows/Linux模拟实现OPT或FIFO或LRU淘汰算法。

  3. 研读并修改Linux内核的缺页处理函数do_no_page(新版本应该是handle_mm_fault) 或页框分配函数get_free_page,并用printk打印调试信息。注意:需要编译内核。建议优麒麟或麒麟系统

  4. Linux下利用/proc/pid/pagemap技术计算某个变量或函数虚拟地址对应的物理地址等信息。建议优麒麟或麒麟系统

任务一:编写二维数组遍历程序,理解程序局部性原理

任务要求

  1. 数组尽可能开大一些,并尝试改变数组大小,改变内外重循环次序。例如从[2048] X[2048]变化到[10240] x [20480],观察它们的遍历效率。

  2. 在任务管理器或利用/proc中文件系统观察它们的缺页次数。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <iostream>
#include <sys/time.h>
#include <sys/resource.h>
#include <vector>
#include <iomanip>

using namespace std;

// 行优先遍历
void traverseRowMajor(vector<vector<int>> &arr)
{
int sum = 0;
for (size_t i = 0; i < arr.size(); i++)
{
for (size_t j = 0; j < arr[i].size(); j++)
{
sum += arr[i][j];
}
}
}

// 列优先遍历
void traverseColumnMajor(vector<vector<int>> &arr)
{
int sum = 0;
for (size_t j = 0; j < arr[0].size(); j++)
{
for (size_t i = 0; i < arr.size(); i++)
{
sum += arr[i][j];
}
}
}

int main()
{
int rows, cols;
cout << "输入矩阵的行数和列数(格式: x y): ";
cin >> rows >> cols;

vector<vector<int>> arr(rows, vector<int>(cols, 1));

rusage usage;
timeval start, end;

char choice;
cout << "选择遍历方式(r: 行优先,c: 列优先): ";

cin >> choice;

// 开始前获取进程的资源使用情况
getrusage(RUSAGE_SELF, &usage);
start = usage.ru_utime;

if (choice == 'r')
{
traverseRowMajor(arr);
}
else if (choice == 'c')
{
traverseColumnMajor(arr);
}

// 结束时获取进程的资源使用情况
getrusage(RUSAGE_SELF, &usage);
end = usage.ru_utime;

// 以毫秒输出时间
double time_taken = (end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000.0;

cout << fixed << setprecision(3) << "消耗时间: " << time_taken << " 毫秒\n";
cout << "页面错误: " << usage.ru_minflt << " 次\n";

return 0;
}

32位程序的编译

如果直接加上-m32编译,会报错:

32bit编译报错

因为缺少32位的库,需要安装32位的库:

1
2
sudo dpkg --add-architecture i386              # 添加32位架构
sudo apt-get install gcc-multilib g++-multilib # 安装32位库

32位和64位程序的运行结果

利用g++分别编译出32位和64位的可执行文件test_32和test_64运行结果如下:

64位运行结果

32位运行结果

现象观察和分析

  1. 执行时间:

    1. 在较小的矩阵(2048x2048)中,64位程序的执行时间略少于32位程序。
    2. 在较大的矩阵(10240x20480)中,64位程序在行优先遍历时明显快于32位程序,但在列优先遍历时,两者的时间差异不大。
  2. 页面错误:

    1. 对于两种程序(32位和64位),在相同的矩阵大小和遍历方式下,页面错误的数量几乎相同。

执行时间差异:

64位程序通常可以更有效地处理大型数据集,因为它们可以利用更大的地址空间和通常更高效的数据处理能力(例如,更大的寄存器和更优的内存管理)。这解释了为什么在处理较大的矩阵时,64位程序在行优先遍历中比32位程序更快。
对于列优先遍历,由于内存访问模式导致频繁的缓存未命中,这可能在一定程度上抵消了64位架构的优势,导致两者在大矩阵列优先遍历时的性能差异不大。

页面错误相似性:

页面错误的数量主要取决于程序的内存访问模式和操作系统的页面管理策略,而不是程序的位数。因此,即使是32位和64位程序,如果它们执行相同的内存访问操作,页面错误的数量也可能非常接近。感觉更多取决于内存访问模式和操作系统的内存管理。

但我其实有一个疑问在于,从直观的感觉来看,列有限遍历产生的页面错误应该比行优先遍历更多,因为列优先遍历时,每次访问的地址都是连续的,而行优先遍历时,每次访问的地址都是不连续的。但实际上,两者的页面错误数量几乎相同。

GPT的回答:

  • 内存分页和访问模式:

    • 现代计算机系统中,物理内存是分页管理的。如果一个矩阵的行或列跨越了多个内存页,那么无论是行优先还是列优先遍历,都可能导致页面错误。
    • 在行优先遍历中,如果矩阵的每行都跨越多个页面,那么即使访问是连续的,也可能在访问新行时遇到页面错误。
    • 在列优先遍历中,尽管每次跳跃到新列可能会触及新的内存页,但如果列数据较小并且能够在单个或少数几个内存页中被完全包含,页面错误的发生可能不会比行优先多很多。
  • 缓存和预取策略:

    • 现代处理器具有高级的缓存和预取机制。这些机制可能在某种程度上减少了预期中列优先遍历的页面错误数量。
    • 处理器可能能够预测接下来的内存访问模式,并提前将数据从主内存加载到缓存中,这样即使是列优先访问,也可能不会立即导致页面错误。
  • 操作系统的内存管理:

    • 操作系统在管理内存时可能采取一些策略,如延迟分配,这可能导致实际的页面错误数量与预期不同。
    • 操作系统可能会保持一定数量的空闲页面池,这样在初期的内存访问中,页面错误的发生率可能较低。
  • 矩阵的物理内存布局:

    • 矩阵在物理内存中的实际布局也可能影响页面错误的发生。例如,如果矩阵的某些部分已经由于其他原因被加载到内存中,这可能会减少特定遍历方式下的页面错误。
  • 系统的整体内存负载:

    • 如果系统上运行的其他进程占用了大量内存,这可能会影响到您的程序的页面错误行为。系统的整体内存负载和可用性可能会导致不同时间点上相同操作的不同页面错误表现。

任务二:模拟实现OPT或FIFO或LRU淘汰算法

任务要求

  1. 程序指令执行过程采用遍历数组的操作来模拟;

  2. 用1个较大数组A(例如2400个元素)模拟进程,数组里面放的是随机数,每个元素被访问时就使用printf将其打印出来,模仿指令的执行。数组A的大小必须是设定的页大小(例如10个元素或16个元素等等)的整数倍。

  3. 用3-8个小数组(例如数组B,数组C,数组D等)模拟分得的页框。小数组的大小等于页大小(例如10条指令的大小,即10的元素)。小数组里面复制的是大数组里面的相应页面的内容(自己另外构建页表,描述大数组的页与小数组序号的关系)。

  4. 利用不同的次序访问数组A,次序可以是:顺序,跳转,分支,循环,或随机,自己构建访问次序。不同的次序也一定程度反映程序局部性。

  5. 大数组的访问次序可以用rand()函数定义,模拟产生指令访问序列,对应到大数组A的访问次序。然后将指令序列变换成相应的页地址流,并针对不同的页面淘汰算法统计“缺页”情况。缺页即对应的“页面”没有装到小数组中(例如数组B,数组C,数组D等)。

  6. 实验中页面大小,页框数,访问次序,淘汰算法都应可调。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <limits.h>

#define ARRAY_SIZE 2400 // 进程虚拟空间大小
#define PAGE_SIZE 10 // 页框大小
#define NUM_PAGES (ARRAY_SIZE / PAGE_SIZE) // 虚拟内存的页数
#define NUM_FRAMES 5 // 物理内存的页数
#define JUMP_NUM_FRAMES 7 // 跳转的页数
#define LOOP_TIMES 3 // 循环的次数

int main_array[ARRAY_SIZE]; // 进程虚拟空间
int frames[NUM_FRAMES][PAGE_SIZE]; // 物理内存分割成页框
int page_table[NUM_PAGES]; // 页表项(虚拟页框号到物理页框号的映射)
int frame_table[NUM_FRAMES]; // 页框表(物理页框号到虚拟页框号的映射)
int page_faults = 0; // 缺页次数
int algo_choice = 0; // 页面替换算法

int frame_pointer = 0; // 用于FIFO
int frame_queue[NUM_FRAMES]; // 用于FIFO
int frame_timestamp[NUM_FRAMES]; // 用于LRU
int future_pages[ARRAY_SIZE]; // 用于OPT

char *algo_name[4] = {"随机替换", "OPT", "FIFO", "LRU"}; // 页面替换算法名称

// 初始化
void initialize()
{
srand(time(NULL));
// 填充随机数和初始化
for (int i = 0; i < ARRAY_SIZE; i++)
{
main_array[i] = rand() % 100000;
future_pages[i] = -1;
}
// 初始化页表
for (int i = 0; i < NUM_PAGES; i++)
{
page_table[i] = -1;
}
// 初始化页框队列和时间戳
for (int i = 0; i < NUM_FRAMES; i++)
{
frame_queue[i] = -1;
frame_timestamp[i] = -1;
frame_table[i] = -1;
}
}

int find_frame_opt(int current_time)
{
// 实现 OPT 算法
int frame = -1;
int max_future = -1;
// 找到未来最长时间不会使用的页框
for (int i = 0; i < NUM_FRAMES; i++)
{
// 如果找到空闲页框,直接返回
if (frame_table[i] == -1)
{
frame = i;
break;
}
int future = -1;

for (int j = current_time; j < ARRAY_SIZE; j++)
{
if (future_pages[j] == frame_table[i])
{
future = j;
break;
}
}
if (future > max_future)
{
max_future = future;
frame = i;
}
}
if (max_future == -1)
frame = rand() % NUM_FRAMES;
return frame;
}

int find_frame_fifo()
{
// 实现 FIFO 算法,依靠队列,先进先出
int frame = frame_pointer;
frame_pointer = (frame_pointer + 1) % NUM_FRAMES;
return frame;
}

int find_frame_lru()
{
// 实现 LRU 算法
int lru = INT_MAX;
int frame = -1;
// 找到最久未使用的页框
for (int i = 0; i < NUM_FRAMES; i++)
{
if (frame_timestamp[i] < lru)
{
lru = frame_timestamp[i];
frame = i;
}
}
return frame;
}

// 访问页面
void access_page(int page_number, int offset, int current_time)
{
// 缺页,采用对应的页面替换算法
if (page_table[page_number] == -1)
{
// 找到一个空闲或是应该替换的页框
int frame_number;
switch (algo_choice)
{
case 1:
frame_number = find_frame_opt(current_time);
break;
case 2:
frame_number = find_frame_fifo();
break;
case 3:
frame_number = find_frame_lru();
break;
default:
frame_number = rand() % NUM_FRAMES;
}
// 把之前的虚拟到物理的页表映射改为-1
if (frame_table[frame_number] != -1)
page_table[frame_table[frame_number]] = -1;

printf("\033[1;31m页面错误\033[0m,页面%3d 装入页框%2d,偏移%2d处数据为:", page_number, frame_number, offset);
page_table[page_number] = frame_number;
frame_table[frame_number] = page_number;
for (int i = 0; i < PAGE_SIZE; i++)
{
frames[frame_number][i] = main_array[page_number * PAGE_SIZE + i];
}
page_faults++;
frame_queue[frame_number] = page_number;
frame_timestamp[frame_number] = current_time;
}
else
{
printf("\033[1;32m页面命中\033[0m,页面%3d 位于页框%2d,偏移%2d处数据为:", page_number, page_table[page_number], offset);
frame_timestamp[page_table[page_number]] = current_time;
}
printf("%5d ", main_array[page_number * PAGE_SIZE + offset]);
printf("\n");
}

int main()
{
initialize();

// 提示用户选择页面替换算法
printf("请选择页面替换算法(1=OPT, 2=FIFO, 3=LRU),默认随机替换: ");
scanf("%d", &algo_choice);
if (algo_choice < 1 || algo_choice > 3)
{
printf("无效的页面替换算法选项,使用默认的替换方式:随机替换。\n");
algo_choice = 0;
}
printf("当前使用的页面替换算法: %s\n", algo_name[algo_choice]);

// 提示用户选择页面访问顺序
int access_order = 1;
if (algo_choice == 1)
{
// 提示用户选择,选择错误重选
do
{
if (access_order < 1 || access_order > 4)
printf("无效的访问顺序选项,请重新选择。\n");
printf("OPT算法需要知道未来的页面访问顺序,请选择页面访问顺序 (1=顺序, 2=跳转, 3=分支, 4=循环): ");
scanf("%d", &access_order);
} while (access_order < 1 || access_order > 4);
}
else
{
// 提示用户选择,选择错误重选
do
{
if (access_order < 1 || access_order > 5)
printf("无效的访问顺序选项,请重新选择。\n");
printf("请选择页面访问顺序 (1=顺序, 2=跳转, 3=分支, 4=循环, 5=随机): ");
scanf("%d", &access_order);
} while (access_order < 1 || access_order > 5);
}

// 根据选择的访问顺序访问页面
switch (access_order)
{
case 1: // 顺序访问
// 构造future_pages数组
for (int i = 0; i < ARRAY_SIZE; i++)
future_pages[i] = i / PAGE_SIZE;
for (int i = 0; i < ARRAY_SIZE; i++)
access_page(i / PAGE_SIZE, i % PAGE_SIZE, i);
break;
case 2: // 跳转访问
// 构造future_pages数组
for (int i = 0; i < ARRAY_SIZE; i++)
future_pages[i] = i * JUMP_NUM_FRAMES % ARRAY_SIZE / PAGE_SIZE;
// 间隔JUMP_NUM_FRAMES步访问
for (int i = 0; i < ARRAY_SIZE; i++)
access_page(i * JUMP_NUM_FRAMES % ARRAY_SIZE / PAGE_SIZE, i * JUMP_NUM_FRAMES % ARRAY_SIZE % PAGE_SIZE, i);
break;
case 3: // 分支访问
// 构造future_pages数组
for (int i = 0; i < ARRAY_SIZE; i++)
{
// 偶数页框顺序访问,奇数页框跳转访问
if (i % 2 == 0)
future_pages[i] = i / PAGE_SIZE;
else
future_pages[i] = i * JUMP_NUM_FRAMES % ARRAY_SIZE / PAGE_SIZE;
}
for (int i = 0; i < ARRAY_SIZE; i++) {
if (i % 2 == 0)
access_page(i / PAGE_SIZE, i % PAGE_SIZE, i);
else
access_page(i * JUMP_NUM_FRAMES % ARRAY_SIZE / PAGE_SIZE, i * JUMP_NUM_FRAMES % ARRAY_SIZE % PAGE_SIZE, i);
}
break;
case 4: // 循环访问
// 构造future_pages数组
for (int i = 0; i < ARRAY_SIZE; i++)
future_pages[i] = (i % (ARRAY_SIZE / LOOP_TIMES)) / PAGE_SIZE;
// 循环访问loop_times次
for (int i = 0; i < ARRAY_SIZE; i++)
access_page((i % (ARRAY_SIZE / LOOP_TIMES)) / PAGE_SIZE, (i % (ARRAY_SIZE / LOOP_TIMES)) % PAGE_SIZE, i);
break;
case 5: // 随机访问
for (int i = 0; i < ARRAY_SIZE; i++)
{
int ind = rand() % ARRAY_SIZE;
access_page(ind / PAGE_SIZE, ind % PAGE_SIZE, i);
}
break;
default:
for (int i = 0; i < ARRAY_SIZE; i++)
{
int ind = rand() % ARRAY_SIZE;
access_page(ind / PAGE_SIZE, ind % PAGE_SIZE, i);
}
}

/* 打印future_pages */
// for (int i = 0; i < NUM_PAGES; i++)
// {
// printf("%d ", future_pages[i]);
// }

// 输出缺页次数
printf("页面访问错误次数: %d,命中率: %.2f%%\n", page_faults, (1 - (float)page_faults / ARRAY_SIZE) * 100);

return 0;
}

程序运行结果

这是原本的输出示例,但为了更好的演示,我对程序进行了一些修改,注释掉了一些输出。

原本的输出

opt

opt运行结果

fifo

fifo运行结果

lru

lru运行结果

由于opt算法需要知道未来的页面访问顺序,而故选中opt算法之后,不存在5=随机访问这个选项。

任务三:修改Linux内核缺页处理函数,打印调试信息

任务要求

  1. 编写2个类似hello world或简单的for循环应用程序作为测试目标。

  2. 在Linux内核的缺页处理函数do_no_page( )或类似函数中(不同版本的此函数名有差异)采用printk方式添加调试信息,打印特定进程(以可程序的程序名为过滤条件)的缺页信息,统计缺页次数。

  3. 也可以在Linux内核的分配物理页框函数get_free_page( )或类似函数中(不同版本的此函数名有差异)采用printk方式添加调试信息,打印特定进程(以可程序的程序名为过滤条件)运行过程中分配新页框的信息,统计相关信息。

修改内核源代码

增加代码 ./mm/memory.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 增加两个系统调用用于控制程序,一个是设置目标程序名,一个是获取目标程序的缺页次数
static char target_program[256] = {0}; // target program name
static long page_fault_count = 0; // page fault counts

SYSCALL_DEFINE1(set_target_program_name, const char __user *, program_name) {
// copy name to kernel
if (copy_from_user(target_program,program_name, 255) != 0)
return -EFAULT; // if fail, return -EFAULT

target_program[255] = '\0';
return 0;
}

SYSCALL_DEFINE0(get_page_fault_count) {
printk(KERN_INFO "%s page fault count: %ld\n", target_program, page_fault_count);
page_fault_count = 0;
return page_fault_count;
}
1
2
3
// 在do_no_page函数中添加调试信息
if (strcmp(current->comm, target_program) == 0)
page_fault_count++;

系统调用函数声明 ./include/linux/syscalls.h

1
2
3
// 增加两个系统调用的声明
asmlinkage long sys_set_target_program_name(const char __user *name);
asmlinkage long sys_get_page_fault_count(void);

系统调用ID ./arch/x86/entry/syscalls/syscall_64.tbl

1
2
3
// 增加两个系统调用的系统调用号
448 64 set_target_program_name sys_set_target_program_name
449 64 get_page_fault_count sys_get_page_fault_count

系统调用ID声明./include/uapi/asm-generic/unistd.h

1
2
3
4
5
6
// 增加两个系统调用的声明
#define __NR_set_target_program_name 448
__SYSCALL(__NR_set_target_program_name, sys_set_target_program_name)

#define __NR_get_page_fault_count 449
__SYSCALL(__NR_get_page_fault_count, sys_get_page_fault_count)

编译内核完成后编写测试代码

编译的时候会报错如下:

报错

起初我不知道这个报错是什么意思,修改了很久就是没有效果,后来加了一个#include <linux/syscalls.h>就好了。也就是说,如果定义系统调用就必须要包含这个头文件。但是这个报错的指向性不是很明确,导致我耗费了很长时间。

下面是test1.c,test2.c只是去掉了新增加的系统调用。为了测试调试信息是可控的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <string.h>

// 假设这些是您的新系统调用的编号
#define SYS_SET_TARGET_PROGRAM_NAME 448
#define SYS_GET_PAGE_FAULT_COUNT 449

const int size = 1000;
int array[1000][1000];

int main() {
// 设置目标程序名
const char *target_program = "test";
syscall(SYS_SET_TARGET_PROGRAM_NAME, target_program);

// 执行可能触发页面错误的操作
int i, j;
for (i = 0; i < size; i++) {
for (j = 0; j < size; j++) {
array[i][j] = i + j;
}
}

// 获取并输出页面错误计数
syscall(SYS_GET_PAGE_FAULT_COUNT);

return 0;
}

程序运行结果

程序运行结果

如图,只有test1程序的页面错误计数被打印出来了,而test2程序的页面错误计数没有被打印出来。

任务四:Linux下利用/proc/pid/pagemap计算VA对应的PA

任务要求

  1. Linux的/proc/pid/pagemap文件允许用户查看当前进程虚拟页的物理地址等相关信息。每个虚拟页包含一个64位的值注意分析64位的信息

  2. 获取当前进程的pagemap文件的全名

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // ppt上的示例代码
    pid_t pid = getpid();
    sprintf(buf, "%d", pid);
    strcpy(filename, "/proc/");
    strcat(filename, buf);
    strcat(filename, "/pagemap");

    // 可以直接一步到位
    sprintf(filename, "/proc/%d/pagemap", pid);
  3. 可以输出进程中某个或多个全局变量或自定义函数的虚拟地址,所在页号,所在物理页框号,物理地址等信息。

思考:

(1) 如何扩充实验(写个通用函数)展示指定进程的指定虚拟地址对应的物理地址。

(2) 如何扩充实验验证不同进程的共享库(例如某个已知,被普遍调用的*.so库)具有同一物理地址。

代码实现

program.c实现了输出某个或多个全局变量或自定义函数的虚拟地址,所在页号,所在物理页框号,物理地址等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdint.h>
#include <sys/wait.h>

void printPhysicalAddress(const char *description, unsigned long vaddr)
{
int pageSize = getpagesize(); // 获取系统设定的页面大小
unsigned long v_pageIndex = vaddr / pageSize; // 计算此虚拟地址相对于0x0的经过的页面数
unsigned long v_offset = v_pageIndex * sizeof(uint64_t); // 计算在/proc/pid/page_map文件中的偏移量
unsigned long page_offset = vaddr % pageSize; // 计算虚拟地址在页面中的偏移量
uint64_t item = 0;

char buf[200];
sprintf(buf, "/proc/%d/pagemap", getpid());

int fd = open(buf, O_RDONLY);
if (fd == -1)
{
perror("Error opening pagemap");
return;
}

lseek(fd, v_offset, SEEK_SET); // 移动文件指针到文件相应位置获取item
if (read(fd, &item, sizeof(uint64_t)) != sizeof(uint64_t))
{
perror("Error reading pagemap");
close(fd);
return;
}

uint64_t phy_pageIndex = (((uint64_t)1 << 55) - 1) & item; // 物理页号,即取item的bit0-54
unsigned long paddr = (phy_pageIndex * pageSize) + page_offset; // 物理地址=物理页号*页面大小+页内偏移量
printf("【%s】pid = %d, 虚拟地址 = 0x%lx, 所在页号 = %lu, 物理地址 = 0x%lx, 所在物理页框号 = %lu\n",
description, getpid(), vaddr, v_pageIndex, paddr, phy_pageIndex);

close(fd);
}

const int a = 100; // 全局常量

int main()
{
int b = 100; // 局部变量
static int c = 100; // 局部静态变量
const int d = 100; // 局部常量
char *p = (char *)malloc(100); // 动态内存

printPhysicalAddress("全局常量", (unsigned long)&a);
printPhysicalAddress("局部变量", (unsigned long)&b);
printPhysicalAddress("局部静态变量", (unsigned long)&c);
printPhysicalAddress("局部常量", (unsigned long)&d);
printPhysicalAddress("动态内存", (unsigned long)p);
free(p);
exit(0);

return 0;
}

va2pa.c实现了输出指定进程的指定虚拟地址对应的物理地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdint.h>
#include <stdlib.h>

// 逻辑和上面的一样,只是pid和vaddr是参数
void printPhysicalAddressForProcess(pid_t pid, unsigned long vaddr)
{
int pageSize = getpagesize();
unsigned long v_pageIndex = vaddr / pageSize;
unsigned long v_offset = v_pageIndex * sizeof(uint64_t);
unsigned long page_offset = vaddr % pageSize;
uint64_t item = 0;

char pagemap_file[256];
sprintf(pagemap_file, "/proc/%d/pagemap", pid);

int fd = open(pagemap_file, O_RDONLY);
if (fd == -1)
{
perror("Error opening pagemap");
return;
}

lseek(fd, v_offset, SEEK_SET);
if (read(fd, &item, sizeof(uint64_t)) != sizeof(uint64_t))
{
perror("Error reading pagemap");
close(fd);
return;
}

uint64_t phy_pageIndex = (((uint64_t)1 << 55) - 1) & item;
unsigned long paddr = (phy_pageIndex * pageSize) + page_offset;
printf("pid = %d, 虚拟地址 = 0x%lx, 所在页号 = %lu, 物理地址 = 0x%lx, 所在物理页框号 = %lu\n",
pid, vaddr, v_pageIndex, paddr, phy_pageIndex);
close(fd);
}

int main(int argc, char *argv[])
{
if (argc != 3)
{
fprintf(stderr, "Usage: %s <pid> <virtual address>\n", argv[0]);
return 1;
}

pid_t pid = atoi(argv[1]);
unsigned long vaddr = strtoul(argv[2], NULL, 16);

printPhysicalAddressForProcess(pid, vaddr);

return 0;
}

test.c是一个测试程序,用于验证不同进程的共享库具有同一物理地址。如本程序中的printf函数,在libc.so共享库中

1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <unistd.h>

int main()
{
printf("PID: %d, Virtual address of printf: %p\n", getpid(), (void *)printf);
sleep(1000);
return 0;
}

程序运行结果

必须用sudo运行,否则这个文件应该是没有权限打开的

program.c

program.c运行结果

va2pa.c && test.c

启动两个test进程,分别输出printf函数的虚拟地址。va2pa.c分别计算两个进程的printf函数的虚拟地址对应的物理地址。

va2pa.c程序运行结果

右边两个红色的框线,打印当前运行的test程序的pid和printf函数的虚拟地址,然后把进程号和虚拟地址作为va2pa.c的参数,计算出物理地址。结果如左边两个框线所示,两个进程的printf函数的虚拟地址对应的物理地址(蓝色荧光笔标记)相同,证明了不同进程的共享库具有同一物理地址。