os-project4
实验目标
1.熟悉Minix操作系统的进程管理
2.学习Unix风格的内存管理
实验要求
修改Minix3.1.2a的进程管理器,改进brk系统调用的实现,使得分配给进程的数据段+栈段空间耗尽时,brk系统调用给该进程分配一个更大的内存空间,并将原来空间中的数据复制至新分配的内存空间,释放原来的内存空间,并通知内核映射新分配的内存段。
环境配置
这次的试验需要用到 Minix3.1.2
在配置3.1.2的过程中,我发现 VirtualBox 似乎没有修改硬件兼容性的设置,因此最后还是在VMware上进行开发。
启动虚拟机时,在setup步骤中,网卡选择AMD LANCE,否则无法获得IP地址,其他可选择默认设置。安装完之后,输入shutdown,然后输入boot d0p0重启,后续关机都需输入shutdown,否则可能导致磁盘错误。
输入mv /etc/rc.daemons.dist /etc/rc.daemons
。在网络模式为NAT模式时,这样在重启后可以看到IP地址(若无法连接ftp,检查该处是否修改)。
在虚拟机终端输入packman
可以安装额外的软件包(不能移除iso镜像),为了方便,可以选择全部安装(大概400MB)。安装完之后,open-ssh,vim等已经安装。输入passwd root为root用户创建密码,在重启后可用ftp连接虚拟机(仍然是NAT模式)
能获得ip之后,在虚拟机窗口中输入:
1 | ftp |
然后就可以在本机上通过ftp和ssh连接虚拟机了
注意事项
- PM是用户进程,printf结果显示在虚拟机下,可以不需要串口输出。
相关的函数
- alloc_mem 分配内存
- sys_abscopy 拷贝内存内容
- free_mem 释放内存
- sys_newmap 通知内核,注册内存段
- 用户调用brk函数针对的是虚拟地址,而minix最底层内存管理是物理地址,不能混淆。
- 需要小心处理clicks 和 bytes的单位换算和对齐。
本实验是修改brk系统调用的实现,所以不需要更改brk系统调用的接口,brk函数调用的参数和返回值请遵循原定义,请不要修改。
程序运行错误会可能产生体积很大的core文件,建议在磁盘空间较大的/home下做实验,否则可能会占满磁盘,导致无法开机。
如果break修改后导致程序的编译都无法正常进行(因为在程序编译的过程中可能需要brk系统调用)。在这种情况下,需从未修改过的内核中对程序进行编译。
CC编译器版本很老,语法检查很严格,编写C代码时不要引入非英文字符,或者使用高级语法。
本project使用的Minix版本号是3.1.2。
实验步骤
修改内存分配:
我们知道,Minix的内存管理非常简单: 存储管理器保存着一张按照内存地址排列的空洞列表。当由于执行系统调用FORK或者EXEC需要内存时,系统会使用 first-fit
的策略找出一个能放得下的hole。那么首先我们来看一下hole
的结构:
1 |
|
那么现在,我们需要修改/usr/src/servers/pm/alloc.c
中的alloc_mem
函数,把first-fit修改成best-fit,即分配内存之前,先遍历整个空闲内存块列表,找到最佳匹配的空闲块。而之前的策略,是找到第一个能放下的空闲块。
不妨先看一下原来的best-fit策略是怎么写的
1 | PUBLIC phys_clicks alloc_mem(clicks) |
1 | PUBLIC phys_clicks alloc_mem(clicks) |
修改/usr/src/servers/pm/break.c中的adjust函数,并增加了一个allocate_new_mem局部函数在adjust函数中调用。brk系统调用流程:
do_brk函数计算数据段新的边界,然后调用adjust函数,adjust函数计算程序当前的空闲空间是否足够分配:
若足够,则调整数据段指针,堆栈指针;通知内核程序的映像发生了变化,返回do_brk函数。
若不够,调用allocate_new_mem函数申请新的足够大的内存空间;将程序现有的数据段和堆栈段的内容分别拷贝至新内存区域的底部(bottom)和顶部(top);通知内核程序的映像发生了变化;返回do_brk函数。
```c
r = adjust(rmp, new_clicks, new_sp); // 如果成功,adjust会返回OK
/最后返回地址或者-1(报错)/
rmp->mp_reply.reply_ptr = (r == OK ? m_in.addr : (char*)-1);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
那么首先来看一下 `adjust` 函数的原理, 需要改动的地方其实不多。只是原本并没有不够用了就扩容的功能。当函数发现 `lower< gap_base` 的时候,说明栈段和数据存储段发生了冲突,这时候调用 `alloc_new_mem` 并判断是否申请成功。是则返回0,否则就返回 ENOMEM,表示没空间了。
```c
PUBLIC int adjust(rmp, data_clicks, sp) register struct mproc *rmp; /* whose memory is being adjusted? */
vir_clicks data_clicks; /* how big is data segment to become? */
vir_bytes sp; /* new value of sp */
{
/*
看栈段和数据段能否共存,有必要的话调整一下
需要注意的是,原版这里给出的注释是: 内存从未被分配或者是释放,它只是在数据段和栈段之间的空隙中加减罢了。如果gap的大小最终变为负数,说明这个调整失败了,此时就需要返回 ENOMEM
*/
register struct mem_map *mem_sp, *mem_dp;
vir_clicks sp_click, gap_base, lower, old_clicks;
int changed, r, ft;
long base_of_stack, delta; /* longs avoid certain problems */
int results;
mem_dp = &rmp->mp_seg[D]; /*数据段指针*/
mem_sp = &rmp->mp_seg[S]; /*栈段指针*/
changed = 0; /*一个 flag,用来放栈段/数据段是否已被修改*/
// 如果当前 栈段长度为0,说明栈为空,直接返回OK
if (mem_sp->mem_len == 0)
return (OK); /* don't bother init */
/* See if stack size has gone negative (i.e., sp too close to 0xFFFF...) */
/*
判断栈的大小是否已经为负
*/
//栈基为虚拟地址加栈长度
base_of_stack = (long)mem_sp->mem_vir + (long)mem_sp->mem_len;
sp_click = sp >> CLICK_SHIFT; /* click containing sp */
if (sp_click >= base_of_stack)
return (ENOMEM); /* sp too high */
/* 新旧栈指针的差 栈向下生长的距离,因为栈是向下生长的,所以要用当前的虚拟地址减*/
delta = (long)mem_sp->mem_vir/*旧*/ - (long)sp_click/*新*/;
//r
lower = (delta > 0 ? sp_click : mem_sp->mem_vir);
/* Add a safety margin for future stack growth. Impossible to do right. */
#define SAFETY_BYTES (384 * sizeof(char *))
#define SAFETY_CLICKS ((SAFETY_BYTES + CLICK_SIZE - 1) / CLICK_SIZE)
gap_base = mem_dp->mem_vir + data_clicks + SAFETY_CLICKS;
/*
origin:
说明栈段和数据存储段发生了冲突
if (lower < gap_base) return(ENOMEM); /* data and stack collided
new: 见下
*/
/* 现在,调用allocate_new_mem, 申请更大的内存空间 */
if (lower < gap_base)
{
/*add the length of stack to the virtual address of stack
to get the length of the memory space*/
results = allocate_new_mem(rmp, data_clicks, delta, (phys_clicks)(rmp->mp_seg[S].mem_vir - rmp->mp_seg[D].mem_vir + rmp->mp_seg[S].mem_len));
//判断是否申请成功
if (results == ENOMEM)
return (ENOMEM);
else if (results == OK)
return (OK);
}
/* 如果栈段数据段不冲突,那么就更新数据段的长度. */
old_clicks = mem_dp->mem_len;
if (data_clicks != mem_dp->mem_len)
{
mem_dp->mem_len = data_clicks;
//设置Flag
changed |= DATA_CHANGED;
}
/* 根据栈指针的变化修改栈的长度 */
if (delta > 0)
{
//更新栈指针的虚拟地址和物理地址,地址向下增长
mem_sp->mem_vir -= delta;
mem_sp->mem_phys -= delta;
//更新栈长度
mem_sp->mem_len += delta;
changed |= STACK_CHANGED;
}
/* 判断新的栈段、数据段大小是否适合地址空间 */
ft = (rmp->mp_flags & SEPARATE);
#if (CHIP == INTEL && _WORD_SIZE == 2)
r = size_ok(ft, rmp->mp_seg[T].mem_len, rmp->mp_seg[D].mem_len,
rmp->mp_seg[S].mem_len, rmp->mp_seg[D].mem_vir, rmp->mp_seg[S].mem_vir);
#else
r = (rmp->mp_seg[D].mem_vir + rmp->mp_seg[D].mem_len >
rmp->mp_seg[S].mem_vir)
? ENOMEM
: OK;
#endif
if (r == OK)
{
int r2;
if (changed && (r2 = sys_newmap(rmp->mp_endpoint, rmp->mp_seg)) != OK)
panic(__FILE__, "couldn't sys_newmap in adjust", r2);
free_mem(old_base, old_clicks);
printf("free the old memory space\n");
return (OK);
}
printf("size not ok!\n");
/* New sizes don't fit or require too many page/segment registers. Restore.*/
if (changed & DATA_CHANGED)
{
mem_dp->mem_len = cur_clicks;
mem_dp->mem_phys = cur_base;
}
if (changed & STACK_CHANGED)
{
printf("restore stack segment\n");
mem_sp->mem_vir = cur_v_base;
mem_sp->mem_phys = cur_s_base;
mem_sp->mem_len = cur_s_click;
}
return (ENOMEM);
}
那么 我们自己写的allocate_new_mem
的原理又如何呢?
1 |
|
- 编译Minix:
- 进入/usr/src/servers目录,输入make image, 等编译成功之后输入make install 安装新的PM程序。
- 进入/usr/src/tools目录,输入make hdboot, 成功之后再键入make install命令安装新的内核程序。
- 键入shutdown 命令关闭虚拟机,进入boot monitor界面。设置启动新内核的选项,在提示符键入:
newminix(5,start new kernel) {image=/boot/image/3.1.2ar1;boot;}
- 然后回车,键入save命令保存设置。 5为启动菜单中的选择内核版本的键(数字键,可选其他数字键),3.1.2ar1为在
/usr/src/tools
目录中输入make install
之后生成的内核版本号,请记得在/usr/src/tools
中执行make install
命令之后记录生成的新内核版本号。 - 输入
menu
命令,然后敲数字键(上一步骤中设置的数字)启动新内核,登录进minix 3
中测试。
测试用例
- 测试程序在编写时有特定要求,即将所有的变量申明放到main函数之外,让所有的变量变成全局变量,不允许使用局部变量。按照Minix3的规则全局变量保存在数据段中,不存放在堆栈中,所以可以通过测试。
- 测试程序一只是简单测试sbrk调用,不断的调整数据段的上界,并未对新分配的内存空间进行访问。
test1.c 代码:
1 |
|
修改前如下:
1 | incremented by 1, total 1 |
修改后如下:
1 | incremented by 1, total 1 |
- 测试程序二则对新分配的内存空间进行了访问。
test2.c 代码
1 |
|
修改前:
1 | incremented by: 1, total: 1 , result: 760 |
修改后:
1 | incremented by: 1, total: 1 , result: 760 |
- 建议先通过测试程序一,再尝试测试程序二。通过修改brk,使得二个测试程序可以分配比之前更多的内存。测试程序最后的输出结果和分配给minix操作系统的物理内存数量有关。
结果我们发现,通过修改 alloc_mem(分配策略) 和 adjust(调整策略) 之后,我们能申请到更多的内存空间了
实验结论
通过这次实验,我对分配策略有了更深刻的认识(下个匹配与最佳匹配等)。并更全面的从底层了解了堆调整的策略(比如sys_abscopy,alloc_mem函数,click和byte之间的转化等)
同时,对虚拟机的内核编译、代码调试也有了更多的经验。