您可以添加到网摘 让更多人关注此文章:
简介:本文通过一个基于X86的小型中文操作系统的建立过程,说明了一个小型操作系统的设计原理,编码过程和实现方法。作为一个开放源代码的系统,本文通过对源代码的剖析,较为详细地说明了内存管理,进程结构等的具体实现,并最后介绍了在开发过程中对产生的系统内核的调试,对操作系统的实践具有一定的指导意义。
一 引言
操作系统是计算机的软件基础。在进行一些系统编程实验的时候,我们需要对一些系统程序进行试验,并对这些程序进行量化评测。现有的大型操作系统,如Windows和Linux过于复杂,不适合进行试验。并且由于系统本身的开销比较大,对程序的量化评测要扣除系统(如进程调度)的开销,不易把握。因此我们自行设计开发了一个基于X86的小型中文操作系统。本文介绍了在开发中采用的一些方法和技巧,探讨小型操作系统的实作过程。
二 设计
1. 微内核
微内核是内核的一种形式。在微内核中,各个独立的模块被分离出来,作为独立的实体存在(进程)。在系统运行过程中,各个模块的进程独立运行,各进程通过消息通讯机制进行通讯。由于微内核系统具有良好的结构和可移植性,且易于调试,所以在设计中我们采用了微内核作为我们的内核结构。
2. 内存布局/管理
在X86提供的保护模式(Protected Mode)中,可以采用分段和分页两种方法来对内存进行管理[1]。我们可以通过对GDT或是LDT的相应的描述符表项来对内存的相应段进行设置,如段边界、段大小、特权级等属性[2]。在一个小型的系统中,可以通过将系统的整个内存空间全部设为一个段,即一个平的内存段,来达到简化系统的目的。这样做的缺点是不能有效利用到保护模式的一些优点,如段数据的保护。在NASM中,我们通过以下的代码对GDT进行段的设置:
_gdt:
dw 0, 0, 0, 0 ; (0)
; kernel cs 0x08 (1)
dw 0x3FFF ; base: 0, limit: 64M
dw 0x0000
dw 0x9A00
dw 0x00C0
; kernel ds 0x10 (2)
dw 0x3FFF ; base: 0, limit: 64M
dw 0x0000
dw 0x9200
dw 0x00C0
; user cs 0x1b (3)
dw 0x3FFF ; base: 0, limit: 64M
dw 0x0000
dw 0xFA00
dw 0x00C0
; user ds 0x23 (4)
dw 0x3FFF ; base: 0, limit: 64M
dw 0x0000
dw 0xF200
dw 0x00C0
3. 进程结构
多任务是一个现代操作系统所必须的一部分。在一个任务切换到另一个任务的过程中,我们必须保存现在正在运行的这个进程的上下文,以便在下次调入运行时能够在现在断下的地方继续运行,然后再调入下一个应该运行的进程的上下文,开始下一个进程的运行。在我们的系统中,简单地保存了下面的这些信息:
struct proc_struct
{
int pid; // 进程id
int parent;
struct proc_struct* next_proc; // 下一个进程
struct proc_struct* prev_proc; // 上一个进程
// 进程状态
long eip;
long eax;
long ebx;
long ecx;
long edx;
long esp;
long ebp;
long edi;
long esi;
long eflags;
// 段寄存器
short cs;
short ds;
short es;
short ss;
short fs;
short gs;
// 进程已经运行时间
int total_tick;
unsigned char stack[STACK_NUM]; // 堆栈段
};
其中pid字段记录了这个进程的ID,parent记录了父进程的ID,next_proc是指向下一个要运行的进程的上下文的结构指针,以便系统调入下一个进程运行时进行快速定位,prev_proc记录了在这个进程之前运行的进程结构地址。由此可见,在我们的实现在,我们采用了双向链表对进程进行管理,既简单,又对进程的增加、删除、调序带来了方便。对于当前正在运行的进程,系统用struct proc_struct* p_proc这个指针来指向其进程结构。
在进行了以上的设置以后,我们就可以实现简单的进程管理系统了。process_schedule函数在进程双向链表中循环切换,在一个进程运行完一定的时间后,直接载入下一个进程的上下文,然后跳到下一个进程的执行点,进行执行。
int process_schedule(void)
{
// 进行一些进进程管理
if(p_proc->next_proc == NULL)
{
// 只有这一个进程运行,跳回去继续运行
jump_to_proc(p_proc);
}
p_proc = p_proc->next_proc; // 转到下一个进程
jump_to_proc(p_proc); // 切换过去
return 1;
}
其中,jump_to_proc这个函数在取得下一个进程的上下文后,将上下文装入CPU各寄存器中,然后运行jmp指令跳到进程的执行点中进行执行。
void jump_to_proc(struct proc_struct* p)
{
// 跳到指定的进程中去运行
temp_ebp = p->ebp;
temp_eip = p->eip;
temp_eax = p->eax;
temp_ebx = p->ebx;
temp_ecx = p->ecx;
temp_edx = p->edx;
temp_esp = p->esp;
temp_edi = p->edi;
temp_esi = p->esi;
temp_eflags = p->eflags;
__asm__("movl %0,%%ebp\n\t"
"movl %2,%%ebx\n\t"
"movl %3,%%ecx\n\t"
"movl %4,%%edx\n\t"
"movl %5,%%esp\n\t"
"movl %6,%%edi\n\t"
"movl %7,%%esi\n\t"
"pushl %8\n\t"
"popfl\n\t"
"movl %1,%%eax\n\t"
"sti\n\t"
::"m"(temp_ebp),
"m"(temp_eax),
"m"(temp_ebx),
"m"(temp_ecx),
"m"(temp_edx),
"m"(temp_esp),
"m"(temp_edi),
"m"(temp_esi),
"m"(temp_eflags));
switching = 0;
__asm__("jmp *%0\n\t"
::"m"(temp_eip));
// 此函数不应被返回
}
4. 文件系统
在FAT16,FAT32,EXT2等文件系统中,FAT32由于其实用性和简单性,我们选用它作为实现在文件系统。在硬盘驱动的基础上,我们简单地实现了FAT32文件系统的读功能,这样在后续的开发中,我们可以将各个功能模块作成单独的文件调入执行,增大系统的灵活性。
5. 启动设计
X86系统加电后BIOS执行完自检,将启动设备的第一个扇区读入到物理地址为0x7C00的内存单元中,然后跳到0x7C00去运行。由此可见,操作系统与BIOS的接口就在于启动设备(软驱,硬盘驱动器或是光盘驱动器)的第一个扇区。一般在这第一个扇区完成操作系统的初步载入。但我们在实践中发现,在一个扇区这么小的空间内(512个字节),将整个系统载入比较困难,且如果安装在PC上会与已经存在的操作系统发生冲突。
在这种情况下,我们想到GNU的GRUB启动装载器。GRUB能够支持多个操作系统的共存,通常我们将系统内核编译成GRUB启动装载器能够识别的文件格式,GRUB便能将系统加载到指定的内存地址中去,然后跳到我们的系统中去运行。
|