自己动手操作系统一书的总结
Orange 总结
Orange分为Boot部分,load部分和内核kernel部分:
具体内存分布如下图所示:
系统启动过程:bios加载boot部分(512byte)于内存7c00h处并跳转到7c00h处开始执行,boot部分代码负责将load部分加载于内存9000h处并跳转到那里。Load部分代码加载kernel然后控制权交给kernel。
每个部分的具体细节:
Boot部分:通过在软盘中找到load.bin所在位置,并且将其拷贝到9000h处,最后跳转到那里(具体技术包括对fat12文件系统结构的理解和对磁盘读写方法);
Load部分:
- 通过在软盘中找到kernel.bin所在位置并将其拷贝到80000h处;
- 然后开启保护模式和分页机制;
- 然后又将位于80000h处的kernel.bin拷贝到30000h处,并且设置kernel.bin
的入口为30400h(这里需要了解elf文件的结构);
Kernel 部分
1.由于以前的堆栈和gdt在load部分定义,故需要将其切换到内核中;(由cstart函数完成)
2.添加中断处理:(不可屏蔽中断和8259的几个外部中断),先初始化8259,比如设置8259主从片对应的中断号等等,建立idt,将中断处理程序与idt联系起来;(由cstart中的init_prot函数完成)
3.多进程处理(kernel_main函数):进程准备工作有:tss和gdt在gdt里面的初始化,加载tss,初始化进程结构体(包括进程的cs,ip,ss,sp等进程对应的ldt的内容及其属性的初始化,等等)进程开始运行时已经将进程结构体末端地址保存于tss中;
进程的运行和调度:初始化时钟中断,进程调度函数是时钟中断处理函数,进程切换函数是restart;从用户程序进入中断(也就是将要发生进程调度):先从tss中获取sp(为进程结构体的末端),然后将ss,sp,cs,eflags,ip等压栈(也就是压入进程结构体中),进入save函数保存进程上下文(保存在进程结构体中),然后切换堆栈到内核栈,进入时钟中断函数,从时钟中断函数返回后又进入进程切换函数restart,最后完成了进程的切换。
- 系统调用添加(类似于进程调度函数):将一个中断对应于系统调用,找到通用系统调用处理函数(进入中断),然后跟上面一样保存相应的进程上下文信息,根据系统调用号进入相应系统调用内核程序,最后返回到用户进程(返回值存放于eax中)。
- 输入输出系统:处理键盘中断(进扫描码存入缓冲区),从缓冲区读出键盘扫描码并解析,最后打印出来,对显示器
- 进程间通信(间接实现微内核模式),就是通过sendrec这个系统调用实现,然后通过msg_send和msg_receive这两个内核api完成了内核的通信,在进程表中有所有发送消息给此进程的链表队列,然后将一个任务进程(rpl=1)的处于无限循环中来接受用户进程的消息,然后作相应的处理后返回相应的信息(这就是系统调用的过程)。
- 文件系统:
磁盘驱动:发送命令(hd_cmd_out),然后等待磁盘准备好的中断interrupt_wait,然后在磁盘端口进行相应的读写操作
文件系统结构:
特征:仅有一个文件夹(‘/’),root区即该文件的内容,每个文件所占扇区大小确定,而且是连续扇区
boot sector:第0个扇区,引导扇区
superblock:超级快,第1个扇区,超级快记录了inode和sector(扇区)个数,
inode map区 :第2个扇区,记录inode个情况,第一位保留,可以记录的inode=512*8-1
sector map区:记录扇区的使用情况,第1为保留,所占的扇区数=总扇区数/(512*8)+1.(注意sector开始从root区记录扇区的使用情况,而且第一位保留)
inode区:记录文件大小,起始扇区,等等信息(本文件系统中每个文件最大大小确定,而且文件存储在连续的扇区中)inode所占的扇区大小=512*8*sizeof(inode)/512=8*sizeof(inode)
数据区:
root区:也就是文件夹’/’的内容,跟一个普通文件一样,占固定大小扇区,内容就是一个个文件的目录项(包括inode号和文件名)
其他文件存储区:其他普通文件存储区域
读取一个文件内容的过程:提取文件的文件名(没有前缀),从root区找到文件的inode号,从inode读取文件信息(起始扇区,文件大小等等),然后根据文件信息读取文件内容.
文件系统中几个全局变量:
struct file_desc
{
int fd_mode; /**< R or W */
int fd_pos; /**< Current position for R/W. */
struct inode* fd_inode; /**< Ptr to the i-node */
};
进程表中指向打开文件的指针数组struct file_desc* flip[];全局变量struct file_desc
f_desc_table[],每个数组成员代表一个打开文件,则其fd_inode不为空指针。而flip的每个数组成员正好指向f_desc_table的一个成员;还有个全局变量inode_table[],而f_desc_table中的fd_inode正好指向这个inode_table数组成员。
打开文件过程可以更具体描述为:根据文件名找到inode号,根据inode号从inode_table中查找,如果inode_table中有,则不需要从硬盘中读取,否则需要从硬盘中读取inode,
然后将上面几个数据结构建立关系,这样就一个文件打开了。
- 内存管理:(fork和exec两个系统调用的实现)
Fork:完全复制父进程的所有,具体过程如下:
进程表初始化的修改:修改init进程地址空间(以前是4g,现在修改成小于1mb的合适大小的),将所有的进程表元素(尽管有些进程表元素标记为未用)的全局描述符初始化(就是在全局描述符中填充相应进程的局部描述符的信息);
- 从进程表数组中找到一个空闲的元素,分配给子进程,对子进程表中的相关数据初始化(比如父进程id);
- 获取父进程的代码段和数据段的信息(包括段的开始地址,段界限,粒度)
- 将其完全进行拷贝(先在一个给定地址开始分配1mb内存给新进程),然后将其拷贝到新的大小为1mb地址处(仅需要拷贝1次,因为代码段和数据段占用相同的地址空间)
- 由于子进程的地址空间已经改变,故需要重新填写其相应局部选择子
- 具体怎么让程序分别跳到父进程和子进程运行,对于父进程,由于父进程先发
Send后发receive消息,而do_fork函数返回后像父进程发送了send消息,故父进程可以继续运行,但返回值是0,而对于子进程,由于完全复制父进程,所以其就是将执行receive消息(因为send时已经进入中断,父进程上下文被保护,然后do_fork函数被调用,然后就是子进程被创建进入就绪状态);最后do_fork函数最后向子进程发送消息(包含子进程的id).
进程地址空间切换要注意的问题:由于只有init以及其子进程所占用的进程地址空间大小为1mb,所以一段程序进入中断后,其进程地址空间就是原来内核的进程地址空间(也就是4gb大小的).
Exec系统调用实现:将文件(可执行文件,且有_start标识符开始,这样以便编译器识别)存取在硬盘指定扇区位置,然后操作系统启动时读取文件,然后以文件系统格式存放在硬盘中,然后根据文件名将文件内容读出来覆盖原有的进程地址空间,这样可执行文件就被拷贝到相应的进程地址空间中,然后设定相应的堆栈地址和入口地址(设置进程表的寄存器值),一旦进程被调度,则程序就可以运行起来.