计算机与信息学院
操作系统 实验报告
实验一 实验环境的使用
实验目的
熟悉操作系统集成实验环境OS Lab的基本使用方法。练习编译、调试EOS操作系统内核以及EOS应用程序。
实验内容
(1)新建一个Windows控制台应用程序项目的步骤如下:
① 在“文件”菜单中选择“新建”,然后单击“项目”;
② 在“新建项目”对话框中,选择项目模板“控制台应用程序 (c)”;
③ 在“名称”中输入新项目使用的文件夹名称“oslab”;
④ 在“位置”中输入新项目保存在磁盘上的位置“C:\test”;
⑤点击“确定”按钮。
(2)生成项目
使用“生成项目”功能可以将程序的源代码文件编译为可执行的二进制文件,方法十分简单:在“生成”菜单中选择“生成项目”。 在项目生成过程中,“输出”窗口会实时显示生成的进度和结果。如果源代码中不包含语法错误,会在最后提示生成成功,成功生成Windows控制台应用程序项目后的“输出”窗口
如果源代码中存在语法错误,“输出”窗口会输出相应的错误信息(包括错误所在文件的路径,错误在文件中的位置,以及错误原因),并在最后提示生成失败。此时在“输出”窗口中双击错误信息所在的行,OS Lab会使用源代码编辑器打开错误所在的文件,并自动定位到错误对应的代码行。可以在源代码文件中故意输入一些错误的代码(例如删除一个代码行结尾的分号),然后再次生成项目,然后在“输出”窗口中双击错误信息来定位存在错误的代码行,将代码修改正确后再生成项目。
(3)执行项目
在OS Lab中选择“调试”菜单中的“开始执行(不调试)”,可以执行刚刚生成的Windows控制台应用程序。启动执行后会弹出一个Windows控制台窗口,显示控制台应用程序输出的内容。按任意键即可关闭此Windows控制台窗口。
(4)调试项目
按照下面的步骤练习使用“逐过程”功能:
1. 在OS Lab的“调试”菜单中选择“逐过程”,“逐过程”功能会执行黄色箭头当前指向的代码行,并将黄色箭头指向下一个要执行的代码行。
2. 按F10(“逐过程”功能的快捷键),黄色箭头就指向了调用printf函数的代码行。查看控制台应用程序窗口,仍然没有任何输出。
3. 再次按F10执行printf函数,查看控制台应用程序窗口,可以看到已经打印出了内容。
4. 在“调试”菜单中选择“停止调试”,结束此次调试。
按照下面的步骤练习使用“逐语句”功能和“跳出”功能:
1. 按F5(“启动调试”功能的快捷键),仍然会在之前设置的断点处中断。
2. 按F10逐过程调试,此时黄色箭头指向了调用函数Func的代码行。
3. 在“调试”菜单中选择“逐语句”,可以发现黄色箭头指向了函数Func中,说明“逐语句”功能可以进入函数,从而调试函数中的语句。
4. 选择“调试”菜单中的“跳出”,会跳出Func函数,返回到上级函数中继续调试(此时Func函数已经执行完毕)。
5. 按Shift+F5(“停止调试”功能的快捷键),结束此次调试。
在调试的过程中,OS Lab提供了三种查看变量值的方法,按照下面的步骤练习这些方法:
1. 按F5启动调试,仍然会在之前设置的断点处中断。
2. 将鼠标移动到源代码编辑器中变量n的名称上,此时会弹出一个窗口显示出变量n当前的值(由于此时还没有给变量n赋值,所以是一个随机值)。
3. 在源代码编辑器中变量n的名称上点击鼠标右键,在弹出的快捷菜单中选择“快速监视”,可以使用“快速监视”对话框查看变量n的值。然后,可以点击“关闭”按钮关闭“快速监视”对话框。
4. 在源代码编辑器中变量n的名称上点击鼠标右键,在弹出的快捷菜单中选择“添加监视”,变量n就被添加到了“监视”窗口中。使用“监视”窗口可以随时查看变量的值和类型。
使用“调用堆栈”窗口可以在调试的过程中查看当前堆栈上的函数,还可以帮助理解函数的调用层次和调用过程。按照下面的步骤练习使用“调用堆栈”窗口:
1. 按F5启动调试,仍然会在之前设置的断点处中断。
2. 选择“调试”菜单“窗口”中的“调用堆栈”,激活“调用堆栈”窗口。可以看到当前“调用堆栈”窗口中只有一个main函数(显示的内容还包括了参数值和函数地址)。
3. 按F11(“逐语句”功能的快捷键)调试,直到进入Func函数,查看“调用堆栈”窗口可以发现在堆栈上有两个函数Func和main。其中当前正在调试的Func函数在栈顶位置,main函数在栈底位置。说明是在main函数中调用了Func函数。
4. 在“调用堆栈”窗口中双击main函数所在的行,会有一个绿色箭头指向main函数所在的行,表示此函数是当前调用堆栈中的活动函数。同时,会将main函数所在的源代码文件打开,并也使用一个绿色箭头指向Func函数返回后的位置。
5. 在“调用堆栈”窗口中双击Func函数所在的行,可以重新激活此堆栈帧,并显示对应的源代码。
6. 反复双击“调用堆栈”窗口中Func函数和main函数所在的行,查看“监视”窗口中变量n的值,可以看到在不同的堆栈帧被激活时,OS Lab调试器会自动更新“监视”窗口中的数据,显示出对应于当前活动堆栈帧的信息。
实验感想
通过这次实验,让我熟悉了OS Lab的使用方法和操作流程,比如怎么生成OS内核、新建APP项目,以及保存EOS内核项目等操作,了解了EOS的应用程序,对实验环境(OS Lab)有了一定的了解,为下面的实验打下了的基础,方便以后的实验。
实验二 进程的创建
实验目的
练习使用EOS API函数CreateProcess创建一个进程,掌握创建进程的方法,理解进程和程序的区别。调试跟踪CreateProcess函数的执行过程,了解进程的创建过程,理解进程是资源分配的单位。
实验内容
(1)练习使用控制台命令创建EOS应用程序进程的具体步骤如下:
1. 在EOS应用程序项目的“项目管理器”窗口中双击Floppy.img文件,使用FloppyImageEditor工具打开此软盘镜像文件。
2. 将本实验文件夹中的Hello.exe文件拖动到FloppyImageEditor工具窗口的文件列表中释放,Hello.exe文件即被添加到软盘镜像文件中。Hello.exe一个EOS应用程序,其源代码可以参见本实验文件夹中的Hello.c源文件。
3. 在FloppyImageEditor中选择“文件”菜单中的“保存”后关闭FloppyImageEditor。
4. 按F7生成EOS应用项目。
5. 按F5启动调试。OS Lab会弹出一个调试异常对话框,并中断应用程序的执行。
6. 在调试异常对话框中选择“否”,忽略异常继续执行应用程序。
7. 激活虚拟机窗口,待该应用程序执行完毕后,在EOS的控制台中输入命令“A:\Hello.exe”后回车。
8. Hello.exe应用程序开始执行,观察其输出如图11-1。
9. 待Hello.exe执行完毕后可以重复第7步,或者结束此次调试。
(2)按照下面的步骤查看应用程序创建另一个应用程序的进程的执行结果:
1. 使用NewProc.c文件中的源代码替换之前创建的EOS应用程序项目中的EOSApp.c文件内的源代码。
2. 按F7生成修改后的EOS应用程序项目。
3. 按F5启动调试。OS Lab会首先弹出一个调试异常对话框。
4. 在调试异常对话框中选择“否”,继续执行。
5. 激活虚拟机窗口查看应用程序输出的内容,如图11-2。结合图11-1,可以看到父进程(EOSApp.exe)首先开始执行并输出内容,父进程创建了子进程(Hello.exe)后,子进程开始执行并输出内容,待子进程结束后父进程再继续执行。
6. 结束此次调试。
(3)按照下面的步骤调试CreateProcess函数创建进程的过程:
1. 按F5启动调试EOS应用程序,OS Lab会首先弹出一个调试异常对话框。
2. 选择“是”调试异常,调试会中断。
3. 在main函数中调用CreateProcess函数的代码行(第57行)添加一个断点。
4. 按F5继续调试,在断点处中断。
5. 按F11调试进入CreateProcess函数。
可以按照下面的步骤来分别验证应用程序和操作系统内核在进程的4G虚拟地址空间中所处的位置:
1. 由于此时在内核的CreateProcess函数内中断执行,所以在“调试”菜单的“窗口”中选择“反汇编”,会在“反汇编”窗口中显示CreateProcess函数的指令对应的反汇编代码。“反汇编”窗口的左侧显示的是指令所在的虚拟地址。可以看到所有指令的虚拟地址都大于0x80000000,说明内核(kernel.dll)处于高2G的虚拟地址空间中。
2. 在“调用堆栈”窗口中双击main函数项,设置main函数的调用堆栈帧为活动的。在“反汇编”窗口中查看main函数的指令所在的虚拟地址都是小于0x80000000,说明应用程序(eosapp.exe)处于低2G的虚拟地址空间中。
3. 在“调用堆栈”窗口中双击CreateProcess函数项,重新设置CreateProcess函数的调用堆栈帧为活动的。关闭“反汇编”窗口。
(4)调试PspCreateProcessEnvironment函数的步骤如下:
1. 在PsCreateProcess函数中找到调用PspCreateProcessEnvironment函数的代码行(create.c文件的第163行),并在此行添加一个断点。
EOS 操作系统实验教程 北京海西慧学科技有限公司 http://www.tevation.com 133
2. 按F5继续调试,到此断点处中断。
3. 按F11调试进入PspCreateProcessEnvironment函数。
由于PspCreateProcessEnvironment函数的主要功能是创建进程控制块并初始化其中的部分信息,所以在此函数的开始,定义了一个进程控制块的指针变量NewProcess。在此函数中查找到创建进程控制块的代码行(create.c文件的第418行) Status = ObCreateObject( PspProcessType, NULL, sizeof(PROCESS) + ImageNameSize +CmdLineSize, 0, (PVOID*)&NewProcess ); 这里的ObCreateObject函数会在由EOS内核管理的内存中创建了一个新的进程控制块(也就是分配了一块内存),并由NewProcess返回进程控制块的指针(也就是所分配内存的起始地址)。
按照下面的步骤调试进程控制块的创建过程:
1. 在调用ObCreateObject函数的代码行(create.c文件的第418行)添加一个断点。
2. 按F5继续调试,到此断点处中断。
3. 按F10执行此函数后中断。
4. 此时为了查看进程控制块中的信息,将表达式*NewProcess添加到“监视”窗口中。
5. 将鼠标移动到“监视”窗口中此表达式的“值”属性上,会弹出一个临时窗口,在临时窗口中会按照进程控制块的结构显示各个成员变量的值(可以参考PROCESS结构体的定义)。由于只是新建了进程控制块,还没有初始化其中成员变量,所以值都为0。
接下来调试初始化进程控制块中各个成员变量的过程:
1. 首先创建进程的地址空间,即4G虚拟地址空间。在代码行(create.c文件的第437行) NewProcess->Pas = MmCreateProcessAddressSpace(); 添加一个断点。
2. 按F5继续调试,到此断点处中断。
3. 按F10执行此行代码后中断。
4. 在“监视”窗口中查看进程控制块的成员变量Pas的值已经不再是0。说明已经初始化了进程的4G虚拟地址空间。
5. 使用F10一步步调试PspCreateProcessEnvironment函数中后面的代码,在调试的过程中根据执行的源代码,查看“监视”窗口中*NewProcess表达式的值,观察进程控制块中哪些成员变量是被哪些代码初始化的,哪些成员变量还没有被初始化。
6. 当从PspCreateProcessEnvironment函数返回到PsCreateProcess函数后,停止按F10。此时“监视”窗口中已经不能再显示表达式*NewProcess的值了,在PsCreateProcess函数中是使用ProcessObject指针指向进程控制块的,所以将表达式*ProcessObject添加到“监视”窗口中就可以继续观察新建进程控制块中的信息。
7. 接下来继续使用F10一步步调试PsCreateProcess函数中的代码,同样要注意观察执行后的代码修改了进程控制块中的哪些成员变量。当调试到PsCreateProcess函数的最后一行代码时,查看进程控制块中的信息,此时所有的成员变量都已经被初始化了(注意观察成员ImageName的值)。
8. 按F5继续执行,EOS内核会为刚刚初始化完毕的进程控制块新建一个进程。激活虚拟机窗口查看新建进程执行的结果。
9. 在OS Lab中选择“调试”菜单中的“停止调试”结束此次调试。
10. 选择“调试”菜单中的“删除所有断点”。
实验感想
通过本次实验,让我了解了程序和进程之间的关系,熟悉进程控制块结构体以及进程创建的过程,观察了进程的创建和进程控制块的工作原理。虽然实验步骤比较繁琐、内容比较晦涩,但是仔细阅读源代码后是可以理解实验原理的。
实验三 进程的同步
实验目的
使用EOS的信号量,编程解决生产者—消费者问题,理解进程同步的意义。调试跟踪EOS信号量的工作过程,理解进程同步的原理。修改EOS的信号量算法,使之支持等待超时唤醒功能(有限等待),加深理解进程同步的原理。
实验内容
(1)按照下面的步骤查看生产者-消费者同步执行的过程:
1. 使用pc.c文件中的源代码,替换之前创建的EOS应用程序项目中EOSApp.c文件内的源代码。
2. 按F7生成修改后的EOS应用程序项目。
3. 按F5启动调试。OS Lab会首先弹出一个调试异常对话框。
4. 在调试异常对话框中选择“否”,继续执行。
5. 立即激活虚拟机窗口查看生产者-消费者同步执行的过程,如图13-2。
6. 待应用程序执行完毕后,结束此次调试。
(2)调试EOS信号量的工作过程
①创建信号量
信号量结构体(SEMAPHORE)中的各个成员变量是由API函数CreateSemaphore的对应参数初始化的,查看main函数中创建Empty和Full信号量使用的参数有哪些不同,又有哪些相同,思考其中的原因。
按照下面的步骤调试信号量创建的过程:
1. 按F5启动调试EOS应用项目。OS Lab会首先弹出一个调试异常对话框。
2. 在调试异常对话框中选择“是”,调试会中断。
3. 在main函数中创建Empty信号量的代码行EmptySemaphoreHandle = CreateSemaphore(BUFFER_SIZE, BUFFER_SIZE, NULL); 添加一个断点。
4. 按F5继续调试,到此断点处中断。
5. 按F11调试进入CreateSemaphore函数。可以看到此API函数只是调用了EOS内核中的PsCreateSemaphoreObject函数来创建信号量对象。
6. 按F11调试进入semaphore.c文件中的PsCreateSemaphoreObject函数。在此函数中,会在EOS内核管理的内存中创建一个信号量对象(分配一块内存),而初始化信号量对象中各个成员的操作是在PsInitializeSemaphore函数中完成的。
7. 在semaphore.c文件的顶部查找到PsInitializeSemaphore函数的定义(第19行),在此函数的第一行代码处添加一个断点。
8. 按F5继续调试,到断点处中断。观察PsInitializeSemaphore函数中用来初始化信号量结构体成员的值,应该和传入CreateSemaphore函数的参数值是一致的。
9. 按F10单步调试PsInitializeSemaphore函数执行的过程,查看信号量结构体被初始化的过程。打开“调用堆栈”窗口,查看函数的调用层次。
② 等待信号量(不阻塞)
生产者和消费者刚开始执行时,用来放产品的缓冲区都是空的,所以生产者在第一次调用WaitForSingleObject函数等待Empty信号量时,应该不需要阻塞就可以立即返回。按照下面的步骤调试:
1. 删除所有的断点(防止有些断点影响后面的调试)。
2. 在eosapp.c文件的Producer函数中,等待Empty信号量的代码行WaitForSingleObject(EmptySemaphoreHandle, INFINITE); 添加一个断点。
3. 按F5继续调试,到断点处中断。
4. WaitForSingleObject 函数最终会调用内核中的PsWaitForSemaphore函数完成等待操作。所以,在semaphore.c文件中PsWaitForSemaphore函数的第一行添加一个断点。
5. 按F5继续调试,到断点处中断。
6. 按F10单步调试,直到完成PsWaitForSemaphore函数中的所有操作。可以看到此次执行并没有进行等待,只是将Empty信号量的计数减少了1就返回了。
③ 释放信号量(不唤醒)
1. 删除所有的断点(防止有些断点影响后面的调试)。
2. 在eosapp.c文件的Producer函数中,释放Full信号量的代码行添加一个断点。
3. 按F5继续调试,到断点处中断。
4. 按F11调试进入ReleaseSemaphore函数。
5. 继续按F11调试进入PsReleaseSemaphoreObject函数。
6. 先使用F10单步调试,当黄色箭头指向第269行时使用F11单步调试,进入PsReleaseSemaphore函数。
7. 按F10单步调试,直到完成PsReleaseSemaphore函数中的所有操作。可以看到此次执行没有唤醒其它线程(因为此时没有线程在Full信号量上被阻塞),只是将Full信号量的计数增加了1(由0变为了1)。
生产者线程通过等待Empty信号量使空缓冲区数量减少了1,通过释放Full信号量使满缓冲区数量增加了1,这样就表示生产者线程生产了一个产品并占用了一个缓冲区。
④ 等待信号量(阻塞)
由于开始时生产者线程生产产品的速度较快,而消费者线程消费产品的速度较慢,所以当缓冲池中所有的缓冲区都被产品占用时,生产者在生产新的产品时就会被阻塞,下面调试这种情况。
1. 结束之前的调试。
2. 删除所有的断点。
3. 按F5重新启动调试。OS Lab会首先弹出一个调试异常对话框。
4. 在调试异常对话框中选择“是”,调试会中断。
5. 在semaphore.c文件中的pswaitforsemaphore函数的 pspwait代码行(第78行)添加一个断点。
6. 按F5继续调试,并立即激活虚拟机窗口查看输出。开始时生产者、消费者都不会被信号量阻塞,同步执行一段时间后才在断点处中断。
7. 中断后,查看“调用堆栈”窗口,有Producer函数对应的堆栈帧,说明此次调用是从生产者线程函数进入的。
8. 在“调用堆栈”窗口中双击Producer函数所在的堆栈帧,绿色箭头指向等待Empty信号量的代码行,查看Producer函数中变量i的值为14,表示生产者线程正在尝试生产14号产品。
9. 在“调用堆栈”窗口中双击PsWaitForSemaphore函数的堆栈帧,查看Empty信号量计数(Semaphore->Count)的值为-1,所以会调用PspWait函数将生产者线程放入Empty信号量的等待队列中进行等待(让出CPU)。
10. 激活虚拟机窗口查看输出的结果。生产了从0到13的14个产品,但是只消费了从0到3的4个产品,所以缓冲池中的10个缓冲区就都被占用了,这与之前调试的结果是一致的。
⑤ 释放信号量(唤醒)
只有当消费者线程从缓冲池中消费了一个产品,从而产生一个空缓冲区后,生产者线程才会被唤醒并继续生产14号产品。可以按照下面的步骤调试:
1. 删除所有断点。
2. 在eosapp.c文件的Consumer函数中,释放Empty信号量的代码行(第180行) ReleaseSemaphore(EmptySemaphoreHandle, 1, NULL); 添加一个断点。
3. 按F5继续调试,到断点处中断。
4. 查看Consumer函数中变量i的值为4,说明已经消费了4号产品。
5. 按照3.3.2.2中的方法使用F10和F11调试进入PsReleaseSemaphore函数。
6. 查看PsReleaseSemaphore函数中Empty信号量计数(Semaphore->Count)的值为-1,和生产者线程被阻塞时的值是一致的。
7. 按F10单步调试PsReleaseSemaphore函数,直到在代码行(第132行) PspWakeThread(&Semaphore->WaitListHead, STATUS_SUCCESS); 处中断。此时Empty信号量计数的值已经由-1增加为了0,需要调用PspWakeThread函数唤醒阻塞在Empty信号量等待队列中的生产者线程(放入就绪队列中),然后调用PspSchedule函数执行调度,这样生产者线程就得以继续执行。
按照下面的步骤验证生产者线程被唤醒后,是从之前被阻塞时的状态继续执行的:
1. 在semaphore.c文件中PsWaitForSemaphore函数的最后一行(第83行)代码处添加一个断点。
2. 按F5继续调试,在断点处中断。
3. 查看PsWaitForSemaphore函数中Empty信号量计数(Semaphore->Count)的值为0,和生产者线程被唤醒时的值是一致的。
4. 在“调用堆栈”窗口中可以看到是由Producer函数进入的。激活Producer函数的堆栈帧,查看Producer函数中变量i的值为14,表明之前被阻塞的、正在尝试生产14号产品的生产者线程已经从PspWait函数返回并继续执行了。
5. 结束此次调试。
(3)修改EOS的信号量算法
修改后的代码:
STATUS
PsWaitForSemaphore(
IN PSEMAPHORE Semaphore,
IN ULONG Milliseconds
)
/*++
功能描述:
信号量的 Wait 操作(P 操作)。
参数:
Semaphore -- Wait 操作的信号量对象。
Milliseconds -- 等待超时上限,单位毫秒。
返回值:
STATUS_SUCCESS。
当你修改信号量使之支持超时唤醒功能后,如果等待超时,应该返回 STATUS_TIMEOUT。
--*/
{
STATUS Status;
BOOL IntState;
ASSERT(KeGetIntNesting() == 0); // 中断环境下不能调用此函数。
IntState = KeEnableInterrupts(FALSE); // 开始原子操作,禁止中断。
if (Semaphore->Count > 0) {
Semaphore->Count--;
Status = STATUS_SUCCESS;
}
else {
Status = PspWait(&Semaphore->WaitListHead, Milliseconds);
}
KeEnableInterrupts(IntState); // 原子操作完成,恢复中断。
return STATUS_SUCCESS;
}
STATUS
PsReleaseSemaphore(
IN PSEMAPHORE Semaphore,
IN LONG ReleaseCount,
OUT PLONG PreviousCount
)
/*++
功能描述:
信号量的 Signal 操作(V 操作)。
参数:
Semaphore -- Wait 操作的信号量对象。
ReleaseCount -- 信号量计数增加的数量。当前只能为 1。当你修改信号量使之支持
超时唤醒功能后,此参数的值能够大于等于 1。
PreviousCount -- 返回信号量计数在增加之前的值。
返回值:
如果成功释放信号量,返回 STATUS_SUCCESS。
--*/
{
STATUS Status;
BOOL IntState;
IntState = KeEnableInterrupts(FALSE); // 开始原子操作,禁止中断。
if (Semaphore->Count + ReleaseCount > Semaphore->MaximumCount) {
Status = STATUS_SEMAPHORE_LIMIT_EXCEEDED;
} else {
//
// 记录当前的信号量的值。
//
if (NULL != PreviousCount) {
*PreviousCount = Semaphore->Count;
}
for (Semaphore->Count += ReleaseCount;
Semaphore->Count > 0 && !ListIsEmpty(&Semaphore->WaitListHead );
Semaphore->Count--){
PspWakeThread(&Semaphore->WaitListHead, STATUS_SUCCESS);
}
//
// 可能有线程被唤醒,执行线程调度。
//
PspThreadSchedule();
Status = STATUS_SUCCESS;
}
KeEnableInterrupts(IntState); // 原子操作完成,恢复中断。
return Status;
}
实验感想
通过本次实验,对于信号量以及临界资源的利用有了更深的认识,p操作中,只要信号量不小于0,那么仍然可以分配资源,但是信号量一旦小于0,就代表了资源已经分配完毕,那么新的线程没有办法得到资源,只好阻塞,传入的参数中就有Milliseconds,代表了线程可以等待的最多的时间,在V操作中,由于是批量释放资源,所以采用了for循环的方法只要等待队列不为空,只要资源量大于0,就可以一直释放资源。对于修改代码,对我来说有一定的难度,但是在同学的帮助下理解了原理,在进行修改就简单的多了。
实验四 物理存储器与进程逻辑地址空间的管理
实验目的
通过查看物理存储器的使用情况,并练习分配和回收物理内存,从而掌握物理存储器的管理方法。通过查看进程逻辑地址空间的使用情况,并练习分配和回收虚拟内存,从而掌握进程逻辑地址空间的管理方法。
实验内容
(1)阅读ke/sysproc.c文件中第1059行的ConsoleCmdPhysicalMemory函数,学习“pm”命令是如何统计并输出物理存储器信息的。在阅读的过程中需要注意下面几点:
? 在统计输出物理存储器信息之前要关闭中断,之后要打开中断,这样可以防止在命令执行的过程中有其它线程分配或者释放物理页。
? 全局变量MiTotalPageFrameCount保存了物理页的总数。每个物理页的大小是4KB,由宏PAGE_SIZE定义。
? 全局变量MiZeroedPageCount和MiFreePageCount分别保存了零页和空闲页的数量。
? 计算已用物理页数量的方法是:物理页总数减去零页数量,再减去空闲页数量。
按照下面的步骤执行控制台命令“pm”,查看物理存储器的信息:
1. 按F7生成在本实验3.1中创建的EOS Kernel项目。
2. 按F5启动调试。
3. 待EOS启动完毕,在EOS控制台中输入命令“pm”后按回车。
(2)分配物理页和释放物理页
1. 使用OS Lab打开本实验文件夹中的pm.c文件(将文件拖动到OS Lab窗口中释放即可打开)。此文件中有一个修改后的ConsoleCmdPhysicalMemory函数,主要是在原有代码的后面增加了分配物理页和释放物理页的代码。
2. 使用pm.c文件中ConsoleCmdPhysicalMemory函数的函数体替换ke/sysproc.c文件中ConsoleCmdPhysicalMemory函数的函数体。
3. 按F7生成修改后的EOS Kernel项目。
4. 按F5启动调试。
5. 待EOS启动完毕,在EOS控制台中输入命令“pm”后按回车。
按照下面的步骤调试分配物理页和释放物理页的过程:
1. 结束之前的调试。
2. 在ke/sysproc.c文件的ConsoleCmdPhysicalMemory函数中,在调用MiAllocateAnyPages函数的代码行(第1103行)添加一个断点,在调用MiFreePages函数的代码行(第1115行)添加一个断点。
3. 按F5启动调试。
4. 待EOS启动完毕,在EOS控制台中输入命令“pm”后按回车。
5. pm命令开始执行后,会在调用MiAllocateAnyPages函数的代码行处中断,按F11调试进入MiAllocateAnyPages函数。
(3)阅读ke/sysproc.c文件中第959行的ConsoleCmdVM函数,学习“vm”命令是如何统计并输出进程的虚拟地址描述符信息的。在阅读的过程中需要注意下面几点:
? 与“pm”命令输出的是整个系统的物理存储器的使用情况不同,“vm”命令输出的是某个进程的虚拟地址描述符信息,所以“vm”命令使用了一个参数——进程ID,用来指定一个进程。这个进程既可以是系统进程,也可以是用户进程。
? 在统计输出指定进程的虚拟地址描述符信息之前要关闭中断,之后要打开中断,这样可以防止在命令执行的过程中有其它线程分配或者释放虚拟页。
? EOS操作系统的进程有4G的虚拟地址空间,但并不是所有的虚拟地址空间都使用虚拟地址描述符来管理,有一些地址空间是静态的,还有一些地址空间由其他的动态方式来管理(例如系统内存池)。
? 进程4G虚拟地址空间中由虚拟地址描述符所管理空间的低地址和高地址是固定的,在这段地址空间中,如果有虚拟页被占用,就会使用虚拟地址描述符来标识,并放入链表中管理。
按照下面的步骤执行控制台命令“vm”,查看系统进程的虚拟地址描述符信息:
1. 按F5启动调试。
2. 待EOS启动完毕,在EOS控制台中输入命令“pt”后按回车。“pt”命令可以输出当前系统中的进程列表,其中系统进程的ID为1。
3. 在EOS控制台中输入命令“vm 1”后按回车。
可以按照下面的步骤执行控制台命令“vm”,查看当创建了一个应用程序进程后,系统进程和应用程序进程中虚拟地址描述符的信息:
1. 在“项目管理器”窗口中双击Floppy.img文件,使用FloppyImageEditor工具打开此软盘镜像。
2. 将本实验文件夹中的LoopApp.exe文件添加到软盘镜像的根目录中(将LoopApp.exe文件拖动到FloppyImageEditor窗口中释放即可)。EOS应用程序LoopApp.exe的源代码可以参考本实验文件夹中的LoopApp.c文件。
3. 点击FloppyImageEditor工具栏上的保存按钮,关闭该工具。
4. 按F5启动调试。
5. 待EOS启动完毕,在EOS控制台中输入命令“A:\LoopApp.exe”后按回车。此时就使用EOS应用程序文件LoopApp.exe创建了一个应用程序进程,由于此进程执行了一个死循环,所以此进程不会结束执行,除非关闭虚拟机。
6. 此时按Ctrl+F2切换到“Console-2”,然后输入命令“pt”后按回车。输出的信息如图15-4所示。其中ID为31的进程就是应用程序进程,ID为33的线程就是应用程序进程的主线程。
7. 输入命令“vm 1”后按回车,可以查看系统进程中虚拟地址描述符的信息。输出的信息如图15-5所示。与图15-3比较可知,3号描述符所包含的一个虚拟页即为应用程序进程的句柄表,13号
描述符所包含的两个虚拟页即为应用程序进程主线程的堆栈。
8. 输入命令“vm 31”后按回车,可以查看应用程序进程中虚拟地址描述符的信息.
(4)在系统进程中分配虚拟页和释放虚拟页
接下来,在vm命令函数中添加分配虚拟页和释放虚拟页的代码,单步调试管理虚拟页的方法。首先,按照下面的步骤修改vm命令的源代码:
1. 使用OS Lab打开本实验文件夹中的vm.c文件(将文件拖动到OS Lab窗口中释放即可打开)。此文件中有一个修改后的ConsoleCmdVM函数,主要是在原有代码的后面增加了分配虚拟页和释放物理页的代码。
2. 使用vm.c文件中ConsoleCmdVM函数的函数体替换ke/sysproc.c文件中ConsoleCmdVM函数的函数体。
3. 按F7生成修改后的EOS Kernel项目。
4. 按F5启动调试。
5. 待EOS启动完毕,在EOS控制台中输入命令“vm 1”后按回车。
命令执行的结果会同时转储在“输出”窗口中,内容如图15-7所示。尝试说明分配虚拟页或者释放虚拟页后虚拟地址描述符以及物理存储器的变化情况。
按照下面的步骤调试分配虚拟页和释放虚拟页的过程:
1. 在ke/sysproc.c文件的ConsoleCmdVM函数中,在调用MmAllocateVirtualMemory函数的代码行添加一个断点,在调用MmFreeVirtualMemory函数的代码行添加一个断点。
2. 按F5启动调试。
3. 待EOS启动完毕,在EOS控制台中输入命令“vm 1”后按回车。
4. vm命令开始执行后,会在调用MmAllocateVirtualMemory函数的代码行处中断。此时要注意参数BaseAddress和RegionSize初始化的值。按F11调试进入MmAllocateVirtualMemory函数。
实验中的习题
1. 尝试在调用MmAllocateVirtualMemory函数时将RegionSize参数的值设置为PAGE_SIZE+1或者PAGE_SIZE*2+1。观察“输出”窗口中转储的信息,并说明申请虚拟内存的大小与实际分配的大小之间的关系,以及分配的虚拟内存大小会对分配的虚拟地址产生什么样的影响。将“输出”窗口中转储的信息保存在文本文件中。
输出结果:
1# Vad Include 1 Vpn From 655360 to 655360. (0xA0000000 - 0xA0000FFF)
2# Vad Include 2 Vpn From 655361 to 655362. (0xA0001000 - 0xA0002FFF)
3# Vad Include 2 Vpn From 655365 to 655366. (0xA0005000 - 0xA0006FFF)
4# Vad Include 2 Vpn From 655367 to 655368. (0xA0007000 - 0xA0008FFF)
5# Vad Include 2 Vpn From 655369 to 655370. (0xA0009000 - 0xA000AFFF)
6# Vad Include 2 Vpn From 655371 to 655372. (0xA000B000 - 0xA000CFFF)
7# Vad Include 2 Vpn From 655373 to 655374. (0xA000D000 - 0xA000EFFF)
8# Vad Include 2 Vpn From 655375 to 655376. (0xA000F000 - 0xA0010FFF)
9# Vad Include 2 Vpn From 655377 to 655378. (0xA0011000 - 0xA0012FFF)
10# Vad Include 2 Vpn From 655379 to 655380. (0xA0013000 - 0xA0014FFF)
11# Vad Include 2 Vpn From 655381 to 655382. (0xA0015000 - 0xA0016FFF)
Total Vpn Count: 2048.
Allocated Vpn Count: 21.
Free Vpn Count: 2027.
Zeroed Physical Page Count: 0.
Free Physical Page Count: 7126.
New VM's base address: 0xA0017000. Size: 0x3000.
1# Vad Include 1 Vpn From 655360 to 655360. (0xA0000000 - 0xA0000FFF)
2# Vad Include 2 Vpn From 655361 to 655362. (0xA0001000 - 0xA0002FFF)
3# Vad Include 2 Vpn From 655365 to 655366. (0xA0005000 - 0xA0006FFF)
4# Vad Include 2 Vpn From 655367 to 655368. (0xA0007000 - 0xA0008FFF)
5# Vad Include 2 Vpn From 655369 to 655370. (0xA0009000 - 0xA000AFFF)
6# Vad Include 2 Vpn From 655371 to 655372. (0xA000B000 - 0xA000CFFF)
7# Vad Include 2 Vpn From 655373 to 655374. (0xA000D000 - 0xA000EFFF)
8# Vad Include 2 Vpn From 655375 to 655376. (0xA000F000 - 0xA0010FFF)
9# Vad Include 2 Vpn From 655377 to 655378. (0xA0011000 - 0xA0012FFF)
10# Vad Include 2 Vpn From 655379 to 655380. (0xA0013000 - 0xA0014FFF)
11# Vad Include 2 Vpn From 655381 to 655382. (0xA0015000 - 0xA0016FFF)
12# Vad Include 3 Vpn From 655383 to 655385. (0xA0017000 - 0xA0019FFF)
Allocated Vpn Count: 24.
Free Vpn Count: 2024.
Zeroed Physical Page Count: 0.
Free Physical Page Count: 7126.
Free VM's base address: 0xA0017000. Size: 0x3000.
1# Vad Include 1 Vpn From 655360 to 655360. (0xA0000000 - 0xA0000FFF)
2# Vad Include 2 Vpn From 655361 to 655362. (0xA0001000 - 0xA0002FFF)
3# Vad Include 2 Vpn From 655365 to 655366. (0xA0005000 - 0xA0006FFF)
4# Vad Include 2 Vpn From 655367 to 655368. (0xA0007000 - 0xA0008FFF)
5# Vad Include 2 Vpn From 655369 to 655370. (0xA0009000 - 0xA000AFFF)
6# Vad Include 2 Vpn From 655371 to 655372. (0xA000B000 - 0xA000CFFF)
7# Vad Include 2 Vpn From 655373 to 655374. (0xA000D000 - 0xA000EFFF)
8# Vad Include 2 Vpn From 655375 to 655376. (0xA000F000 - 0xA0010FFF)
9# Vad Include 2 Vpn From 655377 to 655378. (0xA0011000 - 0xA0012FFF)
10# Vad Include 2 Vpn From 655379 to 655380. (0xA0013000 - 0xA0014FFF)
11# Vad Include 2 Vpn From 655381 to 655382. (0xA0015000 - 0xA0016FFF)
Allocated Vpn Count: 21.
Free Vpn Count: 2027.
Zeroed Physical Page Count: 0.
Free Physical Page Count: 7126.
2. 尝试在调用MmAllocateVirtualMemory函数时将BaseAddress参数的值设置为已经被占用的虚拟内存,例如0xA0000000,观察“输出”窗口中转储的信息。将“输出”窗口中转储的信息保存在文本文件中。
结果:
Total Vpn from 655360 to 657407. (0xA0000000 - 0xA07FFFFF)
1# Vad Include 1 Vpn From 655360 to 655360. (0xA0000000 - 0xA0000FFF)
2# Vad Include 2 Vpn From 655361 to 655362. (0xA0001000 - 0xA0002FFF)
3# Vad Include 2 Vpn From 655365 to 655366. (0xA0005000 - 0xA0006FFF)
4# Vad Include 2 Vpn From 655367 to 655368. (0xA0007000 - 0xA0008FFF)
5# Vad Include 2 Vpn From 655369 to 655370. (0xA0009000 - 0xA000AFFF)
6# Vad Include 2 Vpn From 655371 to 655372. (0xA000B000 - 0xA000CFFF)
7# Vad Include 2 Vpn From 655373 to 655374. (0xA000D000 - 0xA000EFFF)
8# Vad Include 2 Vpn From 655375 to 655376. (0xA000F000 - 0xA0010FFF)
9# Vad Include 2 Vpn From 655377 to 655378. (0xA0011000 - 0xA0012FFF)
10# Vad Include 2 Vpn From 655379 to 655380. (0xA0013000 - 0xA0014FFF)
11# Vad Include 2 Vpn From 655381 to 655382. (0xA0015000 - 0xA0016FFF)
Total Vpn Count: 2048.
Allocated Vpn Count: 21.
Free Vpn Count: 2027.
Zeroed Physical Page Count: 0.
Free Physical Page Count: 7126.
3. 尝试在调用MmAllocateVirtualMemory函数时将RegionSize参数的值设置为PAGE_SIZE*2,将BaseAddress参数的值设置为0xA0017004,观察“输出”窗口中转储的信息,并说明申请虚拟内存的大小与实际分配的大小之间的关系,以及申请的虚拟地址会对分配的虚拟内存大小产生什么样的影响。将“输出”窗口中转储的信息保存在文本文件中。
结果:
Total Vpn from 655360 to 657407. (0xA0000000 - 0xA07FFFFF)
1# Vad Include 1 Vpn From 655360 to 655360. (0xA0000000 - 0xA0000FFF)
2# Vad Include 2 Vpn From 655361 to 655362. (0xA0001000 - 0xA0002FFF)
3# Vad Include 2 Vpn From 655365 to 655366. (0xA0005000 - 0xA0006FFF)
4# Vad Include 2 Vpn From 655367 to 655368. (0xA0007000 - 0xA0008FFF)
5# Vad Include 2 Vpn From 655369 to 655370. (0xA0009000 - 0xA000AFFF)
6# Vad Include 2 Vpn From 655371 to 655372. (0xA000B000 - 0xA000CFFF)
7# Vad Include 2 Vpn From 655373 to 655374. (0xA000D000 - 0xA000EFFF)
8# Vad Include 2 Vpn From 655375 to 655376. (0xA000F000 - 0xA0010FFF)
9# Vad Include 2 Vpn From 655377 to 655378. (0xA0011000 - 0xA0012FFF)
10# Vad Include 2 Vpn From 655379 to 655380. (0xA0013000 - 0xA0014FFF)
11# Vad Include 2 Vpn From 655381 to 655382. (0xA0015000 - 0xA0016FFF)
Total Vpn Count: 2048.
Allocated Vpn Count: 21.
Free Vpn Count: 2027.
Zeroed Physical Page Count: 0.
Free Physical Page Count: 7126.
New VM's base address: 0xA0017000. Size: 0x3000.
1# Vad Include 1 Vpn From 655360 to 655360. (0xA0000000 - 0xA0000FFF)
2# Vad Include 2 Vpn From 655361 to 655362. (0xA0001000 - 0xA0002FFF)
3# Vad Include 2 Vpn From 655365 to 655366. (0xA0005000 - 0xA0006FFF)
4# Vad Include 2 Vpn From 655367 to 655368. (0xA0007000 - 0xA0008FFF)
5# Vad Include 2 Vpn From 655369 to 655370. (0xA0009000 - 0xA000AFFF)
6# Vad Include 2 Vpn From 655371 to 655372. (0xA000B000 - 0xA000CFFF)
7# Vad Include 2 Vpn From 655373 to 655374. (0xA000D000 - 0xA000EFFF)
8# Vad Include 2 Vpn From 655375 to 655376. (0xA000F000 - 0xA0010FFF)
9# Vad Include 2 Vpn From 655377 to 655378. (0xA0011000 - 0xA0012FFF)
10# Vad Include 2 Vpn From 655379 to 655380. (0xA0013000 - 0xA0014FFF)
11# Vad Include 2 Vpn From 655381 to 655382. (0xA0015000 - 0xA0016FFF)
12# Vad Include 3 Vpn From 655383 to 655385. (0xA0017000 - 0xA0019FFF)
Allocated Vpn Count: 24.
Free Vpn Count: 2024.
Zeroed Physical Page Count: 0.
Free Physical Page Count: 7126.
Free VM's base address: 0xA0017000. Size: 0x3000.
1# Vad Include 1 Vpn From 655360 to 655360. (0xA0000000 - 0xA0000FFF)
2# Vad Include 2 Vpn From 655361 to 655362. (0xA0001000 - 0xA0002FFF)
3# Vad Include 2 Vpn From 655365 to 655366. (0xA0005000 - 0xA0006FFF)
4# Vad Include 2 Vpn From 655367 to 655368. (0xA0007000 - 0xA0008FFF)
5# Vad Include 2 Vpn From 655369 to 655370. (0xA0009000 - 0xA000AFFF)
6# Vad Include 2 Vpn From 655371 to 655372. (0xA000B000 - 0xA000CFFF)
7# Vad Include 2 Vpn From 655373 to 655374. (0xA000D000 - 0xA000EFFF)
8# Vad Include 2 Vpn From 655375 to 655376. (0xA000F000 - 0xA0010FFF)
9# Vad Include 2 Vpn From 655377 to 655378. (0xA0011000 - 0xA0012FFF)
10# Vad Include 2 Vpn From 655379 to 655380. (0xA0013000 - 0xA0014FFF)
11# Vad Include 2 Vpn From 655381 to 655382. (0xA0015000 - 0xA0016FFF)
Allocated Vpn Count: 21.
Free Vpn Count: 2027.
Zeroed Physical Page Count: 0.
Free Physical Page Count: 7126.
实验感想
通过本次试验,我熟悉了如何分配和回收物理内存,懂得物理存储器的管理方法,通过查看进程逻辑地址空间的使用情况来练习分配和回收虚拟内存,让我掌握进程逻辑地址空间的管理方法。
实验五 扫描FAT12文件系统管理的软盘
实验目的
通过查看FAT12文件系统的扫描数据,并调试扫描的过程,理解FAT12文件系统管理软盘的方式。通过改进FAT12文件系统的扫描功能,加深对FAT12文件系统的理解。?
实验内容
(1)阅读控制台命令“sd”相关的源代码,并查看其执行的结果
阅读ke/sysproc.c文件中第1321行的ConsoleCmdScanDisk函数,学习“sd”命令是如何扫描软盘上的FAT12文件系统的。在阅读的过程中需要注意下面几点:
? 在开始扫描软盘之前要关闭中断,之后要打开中断,这样可以防止在命令执行的过程中有其它线程修改软盘上的数据。
? 以软盘的盘符“A:”做为ObpLookupObjectByName函数的参数,就可以获得FAT12文件系统设备对象的指针。
? FAT12文件系统设备对象的扩展块(FatDevice->DeviceExtension)是一个卷控制块(VCB,在文件io/driver/fat12.h的第115行定义),从其中可以获得文件系统的重要参数,并可以扫描FAT表。
? FatGetFatEntryValue函数可以根据第二个参数所指定的簇号,返回簇在FAT表中对应项的值,在扫描FAT表时通过调用此函数来统计空闲簇的数量(FreeClusterCount)。
按照下面的步骤执行控制台命令“sd”,查看扫描的结果:
1. 按F7生成在本实验3.1中创建的EOS Kernel项目。
2. 按F5启动调试。
3. 待EOS启动完毕,在EOS控制台中输入命令“sd”后按回车。
观察命令执行的结果,如图19-1所示,可以了解FAT12文件系统的信息。
(2)根据BPB中的信息计算出其他信息
修改“sd”命令函数ConsoleCmdScanDisk的源代码,在输出BPB中保存的信息后,不再通过pVcb->FirstRootDirSector等变量的值进行打印输出,而是通过BPB中保存的信息重新计算出下列信息,并打印输出:
①计算并打印输出根目录的起始扇区号,即pVcb->FirstRootDirSector的值。
②计算并打印输出根目录的大小,即pVcb->RootDirSize的值。
③计算并打印输出数据区的起始扇区号,即pVcb->FirstDataSector的值。
④计算并打印输出数据区中簇的数量,即pVcb->NumberOfClusters的值。
(3)阅读控制台命令“dir”相关的源代码,并查看其执行的结果
阅读ke/sysproc.c文件中第1227行的ConsoleCmdDir函数,学习“dir”命令是如何扫描软盘的根目录并输出根目录中的文件信息的。在阅读的过程中需要注意下面几点:
? 在开始扫描根目录之前要关闭中断,之后要打开中断,这样可以防止在命令执行的过程中有其它线程修改软盘上的数据。
? 以软盘的盘符“A:”做为ObpLookupObjectByName函数的参数,就可以获得FAT12文件系统设备对象的指针。
? FAT12文件系统设备对象的扩展块(FatDevice->DeviceExtension)是一个卷控制块(VCB,在文件io/driver/fat12.h的第115行定义),从其中可以获得文件系统的重要参数,可用于扫描根目录。
? 由于根目录的数据在软盘上,所以调用MmAllocateVirtualMemory函数分配了一块与根目录大小相同的缓冲区,然后调用IopReadWriteSector函数将根目录占用的扇区依次读入了缓冲区。注意在命令执行的最后需要调用MmFreeVirtualMemory函数释放缓冲区。
? 在扫描缓冲区中的目录项时,跳过了未使用的目录项和已经被删除的目录项,而只输出当前使用的目录项(文件)信息,包括文件名、文件大小和最后改写时间。
按照下面的步骤执行控制台命令“dir”,查看扫描的结果:
1. 按F7生成在本实验3.1中创建的EOS Kernel项目。
2. 按F5启动调试。
3. 待EOS启动完毕,在EOS控制台中输入命令“dir”后按回车。
观察命令执行的结果,如图19-2所示,可以看到当前软盘中存储的文件的信息。
(4)输出每个文件所占用的磁盘空间的大小
1. ConsoleCmdDir函数的源代码修改完毕后,按F7生成项目。
2. 在“项目管理器”窗口中双击Floppy.img文件,使用FloppyImageEditor工具打开此软盘镜像。
3. 将本实验文件夹中的void.txt文件(大小为0)添加到软盘镜像的根目录中(将void.txt文件拖动到FloppyImageEditor窗口中释放即可)。
4. 点击FloppyImageEditor工具栏上的保存按钮,关闭该工具。
5. 按F5启动调试。
6. 待EOS启动完毕,在EOS控制台中输入命令“dir”后按回车。
输出的内容应该与图19-3所示的内容相同,或者可以在“项目管理器”窗口中双击Floppy.img文件,使用FloppyImageEditor查看文件的相关信息,检验输出的结果是否正确。
实验感想
通过本次实验,我了解到FAT12文件系统使用开始的若干个扇区做为系统区,而将随后的扇区做为数据区。每个文件的存储位置和属性等信息保存在系统区中,而每个文件的实际数据都保存在数据区中。在此基础上,文件系统增加了簇等逻辑概念,来进一步优化对磁盘物理区域的管理。