一个操作系统的实现(3)

文件系统

文件系统是建立在硬盘上的一个程序,所以由2部分组成:驱动和管理文件系统的进程FS。

首先看一下驱动是如何工作的:

通常,主板上面有2个IDE插槽,分别叫做IDE0/IDE1。每个IDE通道又可以接2个设备。驱动进程的目的就是要隐藏硬件细节,向FS进程提供统一的接口具体到这里,驱动为FS提供的接口就是打开,读取,写入,关闭等接口。下面是硬盘驱动程序:

Task_hd()

{

       Recv(any, msg) //接受任何进程传来的消息

       If(msg == DEV_OPEN)  //如果是打开设备消息,则对硬盘寄存器做一些操作,比如获取硬盘信息等。

       {

              Out_byte(***)   //向硬盘寄存器写入数据

              Recv(interrupt, msg) //阻塞等待硬盘中断发生(表示需要的数据已经得到)

       }

       Send(src, msg) //将获取的硬盘信息返回给发送者进程

}

上面的程序之添加了一个读取硬盘信息的消息,其他比如读取写入数据的消息和这个都是类似的。

有一点需要注意,驱动程序task_hd在读取硬盘信息或者写入数据到硬盘时候的操作其实比上面的情况略微复杂,下面来看一个完成的调用过程:

1 task_fs 文件系统进程 需要操作硬盘,给硬盘驱动进程task_hd() 发送一个msg。

2 假设这时,task_hd()处于空闲状态,即等待在recv(any,msg)处,则会接受这个消息,并向硬盘发送命令。

3 硬盘完成工作后,会触发中断, Recv(interrupt, msg)返回,并给task_fs 文件系统进程发送硬盘数据消息。

4 task_fs 文件系统进程 得到CPU时间时,会收到硬盘数据消息。

 

 

最简单的硬盘驱动看来是告一段落了。下面就可以在此基础上实现一个文件系统,首先需要明白一个文件系统的几个基本要素

1)  有地方存放metadata(一般为硬盘第二个扇区,因为第一个扇区为引导分区)

2)  有地方记录扇区使用情况(位图法)

3)  有地方记录任一文件信息,包括文件名,修改时间,占用哪些扇区等(一个i-node数组,每个元素包含了文件名,属性等信息。同时也需要一个位图来表示i-node数组的使用情况)

4)  文件索引

以后就按照这个方法来组织硬盘结构,创建/删除/写入/读取文件不过是按照这种格式来组织硬盘而已

 

为了实现多系统在硬盘上共存,必须将硬盘划分为多个分区。每个系统占用一个分区。通过一个硬盘的引导扇区可以设置硬盘分区,硬盘最多可以分为4个物理分区。其中每个分区还可以继续划分为多个逻辑分区。

一般Linux的设备分为主设备号和次设备号,主设备号表示不同的物理设备(硬盘,软盘);次设备号表示物理设备上的分区。归纳一下就是:主设备号告诉OS用哪个驱动程序来处理,次设备号告诉驱动程序这是哪个设备。

 

下面看一下硬盘驱动读取和写入数据的过程:

1 首先还是一样task_fs() 文件系统进程通过前面实现的IPC机制将读取(写入)硬盘数据消息发送(即复制)给task_hd()

2 task_hd()

{

       …

       Case:DEV_OPEN //如果某用户进程需要打开一个文件

              为文件内容分配扇区

              分配一个i-node

              分配inode-map

              分配sector-map

              创建文件索引

 

       Case: DEV_READ //读文件

              首先找到将要读取的文件的文件描述符和位置

              根据文件描述符通过驱动获取的该文件在硬盘的具体位置(即i-node)。

              将硬盘内容复制到缓存

}

这一部分写的不是很清楚,理一下思路,我的理解就是硬盘首先有一个文件系统结构(fat32,ntfs,ext2等等),相对于的就有一个硬盘驱动程序用来管理硬盘使用的文件系统并对上层提供文件操作的接口。上层用户需要读取硬盘数据的时候,首先将消息发送给FS,然后FS会将该消息发送给硬盘驱动程序,硬盘驱动程序根据硬盘所使用的文件格式来找到相应的数据并吧数据复制到OS提供的一个缓存中拱用户进程使用。

 

 

                                                        内存管理

到了现在,还没有完成了就是和内存相关的一些东西,首先看看如何创建一个进程(fork),每个shell就是一个子进程。

一个新的进程需要的条件有:

1 代码,数据,堆栈(从父进程复制过来)

2 在进程表中占据一个位置

3 在GDT中有一个位置,指向了该进程对应的LTD,也就是指向了进程的数据和堆栈等

 

看一下linux中fork用法:

First_proc()

{

       Int id = fork();

       If(pid != 0) //这是父进程

       Else 子进程运行

}

父子进程公用一个代码段

 

可以看到,内存管理主要需要在OS中增加2个进程,分别为所有用户的祖先进程(INIT0)和MM进程(memory Management process)。

Init0进程有些特殊,首先一般的进程的虚拟内存空间对用户来说是0-4GB,但是init0的内存在这里大致等于内核占用内存的大小。

Fork的大概步骤如下:

1 用户调用fork

2 通过消息机制发送FORK消息给MM

3 MM主循环完成创建进程的工作,主要流程为:找一个空闲的进程表项作为新进程的进程表项;读取父进程的表示内存占用的LDT,从中取出父进程的代码,数据和堆栈段内存首地址和范围。

3 根据父进程的内存占用情况,分配相同大小的内存给子进程。(分配内存需要注意的就是内存不要重复使用),然后将父进程的内存中的内容复制给子进程。

4 如果父进程有打开文件,要给FS进程发送一个消息来处理父子进程间的共享文件。(FS利用计数器实现)

 

销毁一个进程也差不多,唯一需要注意的就是MM在销毁一个进程的时候,需要首先销毁他的所有子进程。

 

 

 

接下来说明一下几个概念,大多是复制过来的:

CRT:一个应用程序只能调用2种东西:属于自己的函数,以及中断(系统调用就是软中断)。但是实际上,OS还为普通应用程序提供了一个CRT,这个库里面有已经编译好的库函数代码。用户应用程序可以很方便的使用CRT,而免去了很多中断调用。

 

       OS自带应用程序:每个OS都可以附带和安装很多应用程序,简单来说目的就是为了把应用程序从光盘中安装到OS认识的硬盘中并知道位置,这样在需要用时,OS可以自己找到该应用程序。例如,一个最简单的应用程序:

_start

Main(){printf(***);}

把他安装到OS中的步骤大致如下:

1 编译连接该应用程序并打包(设为inst.tar)

2 将打包的应用程序二进制文件写入OS安装盘的某扇区(设该扇区号为X)。

3 启动系统时,在mkfs()创建文件系统时建立一个新的文件cmd.tar,他对应的起始文件扇区号为X。

4 在init进程中,将cmd.tar解压,将其中包含的文件(即为inst.tar)存入文件系统。

 

 

Exec才等同于windows里面的CreartProcess.

First_proc()

{

       Int id = fork();

       If(pid != 0) //这是父进程

       Else 子进程运行

       {

              Exec()   //参数即为新的代码段

       }

}

 

Exec的执行流程:

检查参数个数并将它们依次存放到某连续内存处

向MM发送消息,消息体就是上面存放参数的连续内存。

3 MM消息接收到这个消息,将参数取出(由于exec所在进程和MM不是同一个进程,所有他们的虚拟地址不一样,MM需要通过物理地址复制的方式来获取消息)

根据exec传来的参数,MM将要执行文件复制到自己的缓冲区中

被执行文件是elf格式,MM根据格式将被执行文件的各个段放置到合适的空闲内存中。

建立栈

 

 

OK all done with simplest mode.. 

              2011.1.15 AM8:45  

        i love joliet - teddy_xiong  in ZNUFE

sylar MAIL: cug@live.cn
上一篇:[Unity XLua]热更新XLua入门(二)-俄罗斯方块实例篇


下一篇:《深入理解Android:卷III A》一一第3章 深入理解AudioService