操作系统中分页管理内核的模拟与实现
现代操作系统大部分都采用了分页的方式对物理内存进行管理,Intel 从80386之后也在硬件上支持的分页管理,为操作系统的设计和实现带来了很多便利之处。
由于实际去实现一个操作系统的内存管理内核是一件相对比较困难的事情,因此我们今天将用模拟的方式来设计和实现一个操作系统的分页管理内核。由于我自己也是一个学生,因此有说的不妥甚至错误的地方,请各位看官不要吝啬您的意见建议甚至批评。如无特别说明,下文的物理内存地址一词均指模拟物理内存中的物理地址。
一切开始以前先来确定一些基准数据:
1. 物理页的大小:物理页的大小通常和硬件系统有关,
2. 比如Intel80386及以后的处理器都是按4KB大小来分页。这里我们也选择用4KB来作为分页的尺寸。
3. 逻辑页的大小:逻辑页的大小个人认为和物理页取同4. 样大小最好,5. 因为这样可以不6. 用担心页边界问题,7. 并且在设计分页变换机构时比较容易实现。因此也取为4KB。
8. 系统能有效管理的最大内存:作为一个模拟系统,9. 这里我取32MB作为最大内存管理上限,10. 太大了没什么实际意义,11. 再说模拟系统跑起来也会很占资源。
12. 页号地址范围:依照逻辑页尺寸和支持的最大内存,13. 系统中最多可有8192个逻辑页面,14. 则页面地址为0x0-0x1FFF
15. 页内偏移量范围:页尺寸取为4KB 则页内偏移地址为0x0-0x0fff.。
在分页管理系统中,地址的通常不止一种。在我们现在要做的系统中,地址分为3类,既分页地址,线性地址,虚拟地址。
线性地址:线性地址既存储器的物理地址,在系统内核未初始化和初始化时,都将使用线性地址操作内存。
虚拟地址:虚拟地址是在分页后产生的。其含义和线形地址相同,在系统中存在的各个进程,都使用虚拟地址访问内存,虚拟地址传入内核后,内核利用该进程的页表和自身的页地址变换机构将虚拟地址映射为某一个线性地址。
分页地址:分页地址由2部分组成,按照上文的基准数据设定,其结构如下:
BBBBBBBBBBBBB:BBBBBBBBBBBB
前一部分共13bit用来记录页号地址,后一部分共12bit用来记录页内偏移量。由于我们将物理页和逻辑页的尺寸统一了,因此分页地址所表示的整体意义和线性地址是一样的。
例如:分页地址0000000000001000000000001 是0x1001。作为分页地址,它表示物理内存中第二个物理页(注意,不是第一个物理页,因为0x0000也是有效地址,表示的是第一个物理页),页内偏移量为01。这个分页地址所表示的线性地址为:页号×页尺寸+页内偏移量,即:1×4096+1=4097 (0x1001)。可以看到,从整体上来看,分页地址和线性地址在意义上一样的,但是分页地址和线性地址所表示的自身含义则是完全不同的,这一点在逻辑页和物理页尺寸不统一的情况下尤为明显,并且在这种情况下,分页地址和线性地址的整体含义也就完全不同了。由此看出,逻辑页和物理页尺寸的统一给我们设计和实现内核的地址变换部分提供了很大的便利。
在正式开始设计内核前,先来看看现代操作系统的一个重要特征:硬件无关性。这个特征通常是提供给用户的。但是个人认为,在操作系统的底层设计中,也应该有一个接口部分是直接操作硬件,而其他部分在这个部分之上,通过这个接口去访问和控制硬件。这样做可以使得操作系统的实现可以在某种程度上脱离具体硬件规格,并且可以给移植带来方便。这种设计思路在Microsoft Windows NT家族的中得到了具体的体现,NT中使用了“硬件抽象层”(HAL)的概念来分离操作系统的相对高层部分和底层硬件。各个高层部分通过HAL层来访问和控制硬件,而不是将各种对硬件的访问和控制操作分散在系统的各个角落。这种设计思路将在我们今天即将开始设计的模拟内核中再一次得到体现。这就是“模拟硬件抽象层”(EmuHAL)。
EmuHAL的设计思路完全取自Windows NT的HAL。作为给这个模拟内核搭配的HAL层,EmuHAL将提供一个基本上完全真实的物理内存环境供我们来操作,你可以认为通过EmuHAL层进行操作和访问的是完全的一个物理硬件,而不必要将其想象成别的什么。在这里,EmuHAL层通过利用一个数组,来提供真实的物理内存操作环境。下面是其提供的接口函数:
DWORD APIENTRY Fn_HAL_Init(LPDWORD lpMemoary,
UDWORD NewMemoaryLength);
BYTE APIENTRY Fn_ReadByte(UDWORD Address);
VOID APIENTRY Fn_WriteByte(UDWORD Address,DWORD Value);
WORD APIENTRY Fn_ReadWord(UDWORD Address);
VOID APIENTRY Fn_WriteWord(UDWORD Address,DWORD Value);
DWORD APIENTRY Fn_ReadDword(UDWORD Address);
VOID APIENTRY Fn_WriteDword(UDWORD Address,DWORD Value);
这些函数都是对内存的原子操作的模拟。除去第一初始化函数外,从上到下一共3组,每组对应一个读写服务集,分别为:字节读/写集,字读/写集, 双字读/写集。EmuHAL层的实现源代码如下:
宏定义头文件::Base.h
//=================================================================================
//Name: Base.h
//Funcation: Base Type Define,CPU support,MyUnicode Support.
//Author: kakashi.R
//Last Update: 6.1.2005
//=================================================================================
#ifndef _BASE_H
#define _BASE_H
#define TRUE 1
#define FALSE 0
#define SUCCESS 1
#define FAILURE 0
#define ENABLE 1
#define DISABLE 0
#define HIGH 1
#define LOW 0
#define USED 1
#define UNUSE 0
#define NULL 0
#pragma message("")
#pragma message("This is A Header File Created by kakashir. :)")
#pragma message("")
#if (defined _IOSTREAM) //Incude iostream.h for I/O.
#pragma message("")
#pragma message("Macro _IOSTREAM actived Now.")
#pragma message("Now iostream.h included for I/O.")
#pragma message("")
#include <iostream.h>
#endif //#if (defined _IOSTREAM)
#if ( !(defined APIENTRY) && (defined _STDAPI) ) //__stdcall style support.
#pragma message("")
#pragma message("Macro _STDAPI actived Now.")
#pragma message("Funcations call style KeyWord APIENTRY Defined as __stdcall.")
#pragma message("")
#define APIENTRY __stdcall
#define CENTRY __cdecl
#define API APIENTRY
#define PASCAL API
#define CALLBACK PASCAL
#endif //#if ( !(defined APIENTRY) && (defined _STDAPI) )
#ifdef _I386 //Intel 80386 and later CPU support and typedefs.
#pragma message("")
#pragma message("Macro _I386 actived NOW.")
#pragma message("Data define Keywords defined for Intel 80386 and later Processer Family.")
#pragma message("")
#if !(defined _UNICODE)
typedef char CHAR;
#endif //#if !(defined _UNICODE )
typedef __int8 BYTE;
typedef __int16 WORD;
typedef __int32 DWORD;
typedef __int64 QWORD;
typedef float FLOAT;
typedef double DOUBLE;
typedef unsigned __int8 UBYTE;
typedef unsigned __int16 UWORD;
typedef unsigned __int32 UDWORD;
typedef unsigned __int64 UQWORD;
typedef void* LPVOID;
typedef WORD* LPWORD;
typedef BYTE* LPBYTE;
typedef DWORD* LPDWORD;
typedef QWORD* LPQWORD;
typedef FLOAT* LPFLOAT;
typedef DOUBLE* LPDOUBLE;
typedef UBYTE* LPUBYTE;
typedef UWORD* LPUWORD;
typedef UDWORD* LPUDWORD;
typedef UQWORD* LPUQWORD;
typedef bool BOOL;
typedef void VOID;
typedef union ___uint128 //128bit unsigned data support.
{
unsigned __int64 __128[2];
unsigned __int32 __32[4];
unsigned __int16 __16[8];
unsigned __int8 __8[16];
}uint128;
typedef union ___sint128 //128bit signed data support.
{
signed __int64 __128[2];
signed __int32 __32[4];
signed __int16 __16[8];
signed __int8 __8[16];
}sint128;
typedef UBYTE U8;
typedef short unsigned U16;
typedef long unsigned U32;
typedef UQWORD U64;
typedef uint128 U128;
typedef BYTE S8;
typedef short signed S16;
typedef long signed S32;
typedef QWORD S64;
typedef sint128 S128;
#endif //#ifdef _I386
#if (defined _UNICODE && defined _I386) // MyUNICODE support!! :)
#pragma message("")
#pragma message("Macro _UNICODE actived NOW.")
#pragma message("The Keyword CHAR(upcase) defined 16bit for support UNICODE now.")
#pragma message("Pleas use cout() to display UNICODE char or String.")
#pragma message("use __char to define Old ANSCII-Style Character :)")
#pragma message("")
typedef char __char;
typedef union ___wchar //Define MyUNICODE struct.
{
U16 Unicode16;
U8 Unicode8[2];
}__wchar;
typedef __wchar CHAR;
#include <UnicodeTable.h> //include MyUNICODE Table :)
#define __IOSTREAM
#include <iostream.h>
#if (defined _IOSTREAM)
void APIENTRY _cout(CHAR Word) //MyUnicode I/O Funcation for character Display
{
if(Word.Unicode8[1] == 0)
{
cout<<(__char)Word.Unicode8[0]<<flush;
}
else
{
if(Word.Unicode8[1] >=0 && Word.Unicode8[1] <= HIGH_WORD_MAX &&
Word.Unicode8[0] >=0 && Word.Unicode8[0] <= LOW_WORD_MAX)
{
_Buffer[0] = *(UnicodeTable[(Word.Unicode8[1]-1)] + (2*Word.Unicode8[0]) );
_Buffer[1] = *(UnicodeTable[(Word.Unicode8[1]-1)] + (2*Word.Unicode8[0])+1 );
_Buffer[2] = '/0';
cout<<_Buffer<<flush;
}
}
}
void APIENTRY _cout(CHAR* Word) //MyUnicode I/O Funcation for String display
{
U32 Length = sizeof(Word)/2;
U32 Tmp_a=0;
for(Tmp_a=0;Tmp_a<Length;Tmp_a++)
{
if((Word+Tmp_a)->Unicode8[1] == 0)
{
cout<<(__char)( (Word+Tmp_a)->Unicode8[0])<<flush;
}
else if ( ((Word+Tmp_a)->Unicode8[1]) >=0 && ((Word+Tmp_a)->Unicode8[1]) <= HIGH_WORD_MAX &&
((Word+Tmp_a)->Unicode8[0]) >=0 && ((Word+Tmp_a)->Unicode8[0]) <= LOW_WORD_MAX )
{
_Buffer[0] = *(UnicodeTable[ ( (Word+Tmp_a)->Unicode8[1] )-1 ] + 2*( (Word+Tmp_a)->Unicode8[0] ) );
_Buffer[1] = *(UnicodeTable[ ( (Word+Tmp_a)->Unicode8[1] )-1 ] + 2*( (Word+Tmp_a)->Unicode8[0] )+1);
_Buffer[2] = '/0';
cout<<_Buffer<<flush;
}
}
}
#define lpUnicode(var,value) ((var)->Unicode16) = value
#define Unicode(var,value) ((var).Unicode16) = value
#define cout(UnicodeString) _cout(UnicodeString)
#endif //#if (defined _IOSTREAM)
#endif //#if (define __UNICODE)
#endif //#ifndef _BASE_H
//===================================================================//
EmuHAL层源代码头文件 Hal_Define.h
#ifndef _HAL_DEFINE_H
#define _HAL_DEFINE_H
#define _STDAPI
#define _I386
#include <base.h>
#define MAX_MEMOARY_SUPPORT 0x02000000
DWORD APIENTRY Fn_HAL_Init(LPDWORD lpMemoary,UDWORD NewMemoaryLength);
BYTE APIENTRY Fn_ReadByte(UDWORD Address);
VOID APIENTRY Fn_WriteByte(UDWORD Address,DWORD Value);
WORD APIENTRY Fn_ReadWord(UDWORD Address);
VOID APIENTRY Fn_WriteWord(UDWORD Address,DWORD Value);
DWORD APIENTRY Fn_ReadDword(UDWORD Address);
VOID APIENTRY Fn_WriteDword(UDWORD Address,DWORD Value);
#endif //#ifndef _HAL_DEFINE_H
//===================================================================//
EmuHAL 层源代码实现文件:Hal.cpp
#ifndef _HAL_H
#define _HAL_H
#include "../include/HAL_Define.h"
static LPDWORD lpMemoary=NULL;
static UDWORD MemoarySize=NULL;
DWORD APIENTRY Fn_HAL_Init(LPDWORD Init_lpMemoary,UDWORD NewMemoaryLength)
{
DWORD Resault=SUCCESS;
_asm
{
mov EAX,[Init_lpMemoary]
cmp EAX,NULL
je ERROR
mov [lpMemoary],EAX
jmp EXIT
ERROR:
mov [Resault],FAILURE
EXIT:
}
MemoarySize = NewMemoaryLength;
_asm
{
cmp [MemoarySize],MAX_MEMOARY_SUPPORT
ja OverFlow
jmp OK
OverFlow:
mov [MemoarySize],MAX_MEMOARY_SUPPORT
OK:
}
return Resault;
}
BYTE APIENTRY Fn_ReadByte(UDWORD Address)
{
BYTE Resault=NULL;
_asm
{
push EBX
mov EBX,[Address]
mov EAX,[MemoarySize]
cmp EBX,EAX
jae EXIT
mov EBX,[lpMemoary]
add EBX,[Address]
mov BL ,BYTE ptr[EBX]
mov [Resault],BL
EXIT:
pop EBX
}
return Resault;
}
VOID APIENTRY Fn_WriteByte(UDWORD Address,DWORD Value)
{
_asm
{
push EBX
mov EBX,[Address]
mov EAX,[MemoarySize]
cmp EBX,EAX
jae EXIT
mov EBX,[lpMemoary]
mov EAX,[Value]
add EBX,[Address]
mov BYTE ptr[EBX],AL
EXIT:
pop EBX
}
}
WORD APIENTRY Fn_ReadWord(UDWORD Address)
{
WORD Resault=NULL;
_asm
{
push EBX
mov EBX,[Address]
mov EAX,[MemoarySize]
sub EAX,0x02
cmp EBX,EAX
ja EXIT
mov EBX,[lpMemoary]
add EBX,[Address]
mov BX ,WORD ptr[EBX]
mov [Resault],BX
EXIT:
pop EBX
}
return Resault;
}
VOID APIENTRY Fn_WriteWord(UDWORD Address,DWORD Value)
{
_asm
{
push EBX
mov EBX,[Address]
mov EAX,[MemoarySize]
sub EAX,0x02
cmp EBX,EAX
ja EXIT
mov EBX,[lpMemoary]
mov EAX,[Value]
add EBX,[Address]
mov WORD ptr[EBX],AX
EXIT:
pop EBX
}
}
DWORD APIENTRY Fn_ReadDword(UDWORD Address)
{
DWORD Resault=NULL;
_asm
{
push EBX
mov EBX,[Address]
mov EAX,[MemoarySize]
sub EAX,0x04
cmp EBX,EAX
ja EXIT
mov EBX,[lpMemoary]
add EBX,[Address]
mov EBX ,DWORD ptr[EBX]
mov [Resault],EBX
EXIT:
pop EBX
}
return Resault;
}
VOID APIENTRY Fn_WriteDword(UDWORD Address,DWORD Value)
{
_asm
{
push EBX
mov EBX,[Address]
mov EAX,[MemoarySize]
sub EAX,0x04
cmp EBX,EAX
ja EXIT
mov EBX,[lpMemoary]
mov EAX,[Value]
add EBX,[Address]
mov DWORD ptr[EBX],EAX
EXIT:
pop EBX
}
}
#endif //#ifndef _HAL_H
以上3个文件中,Base.h 是我个人给自己写的类型定义和宏定义文件。在后面设计实现内核时也会用到。从现在开始,我们正式进入内核的设计。
首先,我们要实现的模拟内核是内存分页管理内核,但是由于分页管理时有可能出现页表过长的情况(这在页尺寸较小但需要管理的内存总量很大时尤其明显),这时我们采用的方法是将页表分组,即多级页表的方式。那么我们现在正在设计的模拟内核也需要实现这样的功能吗?我个人给出的答案是否。并不是因为我们管理的最大内存只有32MB所以不需要这样的功能,而是由我们所设计的模拟内核在一个真实的操作系统所处的位置导致的。个人认为,在真实的操作系统中,内存管理内核应该并不是一个单独整体,而是由一个层次结构组成。大致上如下:
系统调用接口层
页表管理层(高级)
页表管理层(低级)
HAL层
其中“页表管理层(高级)”部分实现对二级或者多级页表的管理,而页表管理层(低级)部分则实现对于一级页表中的地址映射,变换操作,同时由于该部分是紧挨HAL层的,因此对物理内存的访问也将通过该层传