分类: LINUX
2015-01-16 15:43:39
原文地址:MINIX文件系统分析 作者:windguy
---------从硬盘上建立分区到使用MINIX文件系统
第一部分 认识硬盘
该部分内容转帖自:
让我们再来复习一下 硬盘 里面有什么东西值得我们来讨论的!
· 物理组成:
就物理组成来说,硬盘是由多个硬盘盘所组成的,而每一个硬盘盘上面都有个磁头( Head )在进行该硬盘盘上面的读写工作,而当磁头固定不动,硬盘盘转一圈所画出来的圆就是所谓的磁道( Track ),全部硬盘盘上面同一个磁道组成了磁柱( Cylinder ),这个磁柱也是磁盘分割( partition )时,最小的单位了!另外,由圆心向半径方向划直线,而每个 Track 上面细分成一个一个的扇区( Sector )那就是最小的磁盘储存物理量了,一个Sector 通常为 512 Bytes 。这就是整个硬盘的构造啰,那么如果以硬盘的 filesystem 来做为检视呢?你可以简单的这样想啦,一个 Partition 当中就具有一个 filesystem ( 档案系统 ) 啰!那么一个
partition 当中能不能具有两个
filesystem 呢?理论上应该是不行的!因为每个档案系统都有其独特的支持方式,例如 Linux 的 ext3 就无法被 Windows 系统所读取!而你将一个 partition 格式化的时候,总不能格式化为 ext3 也同时格式化为 fat32 吧?!那是不可能的啊!底下来谈一谈关于档案系统的相关功能吧!
· Filesystem
在进行磁盘格式化或者是分割的时候,都会需要知道磁盘的一些基本的名词定义,主要有:
o 主要开机扇区( Master
Boot Recorder, MBR ):主要开机扇区可以说是一颗硬盘里面最重要的地方了,怎么说呢?因为他记录了所有硬盘的分割信息,以及开机的时候可以进行开机管理程序的写入等等,如果
一颗硬盘的 MBR 死掉了,那么这颗硬盘几乎就可以说是寿终正寝了~那么 MBR 有什么限制呢?他最大的限制来自于他的大小不够大到储存所有的信息,因此,他仅提供最多四个 partition 的记忆,这就是所谓的 Primary (P)与 Extended (E)扇区最多相加只能有四个的原因了。所以说,如果你预计分割超过 4个partition 的话,那么势必需要使用 3P + 1E ,并且将所有的剩余空间都拨给 Extended 才行( 记得呦! Extended 最多只能有一个 ),否则只要 3P + E 之后还有剩下的空间,那么那些容量将成为废物而浪费了?所以结论就是『如果您要分割硬盘时,并且已经预计规划使用掉 MBR 所提供的 4 个 partition ( 3P + E 或 4P )那么磁盘的全部容量需要使用光,否则剩下的容量也不能再被使用』。不过,如果您仅是分割出 1P + 1E 的话,那么剩下的空间就还能再分割两个 partition !
o 区块( Block ):档案在磁盘当中会被储存在一个固定的大小区块中,那就是 Block ,而 Block 的大小通常为 2 的次方,其中,由于磁盘的扇区物理量通常为 0.5K ( 512
Bytes ),所以 Block 通常规划为 0.5 K 的倍数,例如 EXT2 预设 Block 为 4K 即是一例!其中需要特别留意的地方是,一个 Block 最多仅能容纳一个档案,所以当一个档案不足 4K 时,例如大小仅为 1K ,那么剩下的 3 K 容量将会被浪费掉,这与 inode 也有一定的相关性!因此,在规划您的磁盘时,需要留意到您主机的用途来进行规划较佳!与此同时,若 Block 规划的太小,则磁盘的 Block 数目会大增,而造成 inode 在指向 block 时候的一些搜寻时间的增加,又会造成大档案读写方面的效率较差,这也与 inode 有一定程度的相关性呢!需要特别留意的是:『当系统读取了某一个档案,则该档案所在的区块资料会被加载到内存当中,所以该磁盘区块就会被放置在主存储器的
缓冲快取区中,若这些区块的资料被改变时,刚开始资料仅有主存储器的区块资料会被改变,而且在缓冲区当中的区块资料会被标记为” Dirty “,这个时候磁盘实体区块尚未被修正!所以亦即表示,这些” Dirty “区块的数据必需写到磁盘当中,以维持磁盘实体区块上的数据与主存储器中的区块资料的一致性。』这也是为什么当 Linux 系统不正常关机,或者是突发性的跳电时,总是会造成系统在磁盘检验上面大花时间的原因了!
o Superblock:当我们在进行磁盘分割( partition )时,每个磁盘分割槽( partition )就是一个档案系统( filesystem ),而每个档案系统开始的位置的那个 block 就称为 superblock ,superblock的作用是储存像是档案系统的大小、空的和填满的区块,以及他各自的总数和其它诸如此类的信息等等,这也就是说,当您要使用这一个磁
盘分割槽( 或者说是档案系统 )来进行资料存取的时候,第一个要经过的就是 superblock 这个区块了,所以啰, superblock 坏了,您的这个磁盘槽大概也就回天乏术了!
o Inode: 在 Linux 档案与目录管理 当中,我们提到了很多的 inode 概念,这里在加重补强一下说明:对于档案系统而言一个inode是在inode tables
中的一个项目。Inode包含了所有档案有关的信息例如名称、大小、连接的数量、资料建立之日期,修改及存取的时间。它也包含了磁盘区块的档案指向 (pointer)。pointer是用来记录档案被储存在何处。对于 inode 的规划方面,我们可以使用 mke2fs 来进行。
大致上硬盘就有这些东西,您还得了解每一个 filesystem 就是一个磁盘分割槽,免得到时候底下讲的东西太混乱时,造成您的困扰,那就不好意思啦!
查看硬盘或目录的容量
好了!那么在文字接口底下有什么方法可以查看目前的磁盘最大容许容量、已经使用掉的容量、目前所在目录的已使用容量?还有还有,怎么知道目前目录底下使用掉的硬盘容量呢?以及如何查询目前的 inodes 数目?呵呵!底下我们就来谈一谈主要的两个指令:
df 查看以挂载磁盘的总容量、使用容量与 inode 等等
du 查看档案使用掉的容量有多少?
· df
语法:
[root @tsai /root ]# df -[ikm] 参数说明: -i: 使用 i-nodes 显示结果 -k: 使用 KBytes 显示结果 -m: 使用 MBytes 显示结果 说明:
这是用来显示 目前磁盘空间的指令!需要注意的是,由于我们的档案或者是外挂的磁盘都是加在『 / 』底下,所以当根目录没有空间的时后,嘿嘿!你的 Linux 系统大概就是挂了吧~(说个笑话!当初我们系上有个研究生在管理 Sun 的工作站,是别研究室的,他的硬盘明明有好几 GB ,但是就是没有办法将几 MB
的资料 copy 进去,他就去跟老板讲说机器坏了!嘿!明明才来维护过几天而已为何会坏了!结果老板将维护商叫来骂了 2 小时左右吧!后来,维护商发现原来硬盘的『总空间』还有很多,只是某个扇区填满了,偏偏该研究生就是要将资料 copy 去那个扇区!呵呵!后来那个研究生就被命令『再也不许碰 Sun 主机』了~~)当然啰!你可以将你的资料放置在加挂的硬盘中,那么如何知道目前哪一个磁盘还有多少空间呢?!
这里要请大家再 复习一下,我们的硬盘扇区规划中, primary 扇区每一颗硬盘只允许 4 个,其它的就放置在 Extended 扇区中了!而,硬盘的代号与 IDE 的插槽是有关系的!如果忘记了,那就回去安装 Linux 那一章复习一下吧!好了!假设我只有一棵硬盘,且放在 IDE 的 mater ,那么我的硬盘就是 /dev/hda 啰!而在这颗硬盘中的分割扇区就可以由 /dev/hda1 开始向上加!
OK,那 么使用 df -k 之后,出现的上面的资料中,可以知道我的硬盘更分为 /dev/hda1, /dev/hda2, /dev/hda3, /dev/hda5 与 /dev/hda6 ,咦! /dev/hda4 跑去哪里了!呵呵!其实 /dev/hda4 通常就是 Extended 扇区啦!而后面的 /dev/hda5, /dev/hda6 等扇区就是由 /dev/hda4 所切出来的!所以 /dev/hda5 + /dev/hda6 =
/dev/hda4 !当然,如果还有没有显示出来的,例如 Swap ,则 /dev/hda4 还有可能更大呦!
那么来解释一下上面的资料吧!
· Filesystem:说的是扇区啦!另外,如果你有加挂软盘的话,那么就会出现如上表中最后一行啰!
· 1k-blocks:说明底下的数字单位是 1KB 呦!如果你使用 df -m 则这一栏会出现
· Used :顾名思义,就是使用掉的硬盘空间啦!
· Available :也就是剩下的磁盘空间大小;
· Use% :就是磁盘的使用率啦!如果使用率高达 90% 以上时,最好需要注意一下了,免得容量不足造成系统问题(例如最容易被灌爆的
/var/spool/mail 这个放置邮件的磁盘!)。
· Mounted on
:就是磁盘挂载的目录所在啦!例如 /dev/hda5 是挂载在 /proxy1 底下,而 /var 是 /dev/hda2 这个扇区啰!
另外,需要注意的是,有的时后某些系统会出现 /proc 这个扇区,但是里面的东西都是 0 ,不要紧张! /proc 的东西都是 Linux 系统所需要加载的系统资料,而且是挂载在『内存当中』的,有点像是以前
DOS 年代的虚拟硬盘啦!所以当然没有占任何的硬盘空间啰!
· du
语法:
[root @test /root ]# du [-abckms] [目录名称] 参数说明: [目录名称] 可以省略,如果省略的话,表示要统计目前所在目录的档案容量 -a :全部的档案与目录都列出来!默认值是指列出目录的值! -b :列出的值以 bytes 输出 -c :最后加总 total ! -k :列出的值以 KB 输出 -m :列出的值以 MB 输出 -s :只列出最后加总的值!
第二部分 制作MINIX文件系统
本部分内容为转帖自:
http://huqingyu.cnblogs.com/archive/2005/02/18/105589.html
我在其中加上了一些注解。
一、格式化硬盘
我们要将创建的256MB硬盘Image文件hdc.img进行分区并创建MINIX文件系统,将在这个Image文件中创建1个分区,并且建立成MINIX文件系统。
(1)将hdc.img做为从盘挂到Bochs下已有的模拟系统中(例如SLS Linux):
编辑SLS Linux系统的Bochs配置文件bochsrc.bxrc。在ata0-master一行下加入我们的硬盘Image文件的配置参数行:
ata0-slave:type=disk, path=..\Linux-0.11\hdc.img,
cylinders=520, heads=16, spt=63
(2)在Bochs下运行SLS Linux
(3)利用fdisk命令在hdc.img文件中建立1个分区。
由于SLS Linux默认建立的分区类型是支持MINIX2.0文件系统的81类型(Linux/MINIX),因此需要使用fdisk的t命令把类型修改成80(Old MINIX)类型。 这里请注意,我们已经把hdc.img挂接成SLS Linux系统下的第2个硬盘。按照Linux 0.11对硬盘的命名规则,该硬盘整体的设备名应为/dev/hd5(参见下面表)。但是从Linux 0.95版开始硬盘的命名规则已经修改成目前使用的规则,因此在SLS Linux下第2个硬盘整体的设备名称是/dev/hdb
硬盘逻辑设备号 逻辑设备号 对应设备文件
0x300 /dev/hd0 代表整个第1个硬盘
0x301 /dev/hd1 表示第1个硬盘的第1个分区
0x302 /dev/hd2 表示第1个硬盘的第2个分区
0x303 /dev/hd3 表示第1个硬盘的第3个分区
0x304 /dev/hd4 表示第1个硬盘的第4个分区
0x305 /dev/hd5 代表整个第2个硬盘
0x306 /dev/hd6 表示第2个硬盘的第1个分区
0x307 /dev/hd7 表示第2个硬盘的第2个分区
0x308 /dev/hd8 表示第2个硬盘的第3个分区
0x309 /dev/hd9 表示第2个硬盘的第4个分区
5. 请记住该分区中数据块数大小(这里是65015),在创建文件系统时会使用到这个值。当分区建立好后,按照通常的做法需要重新启动一次系统,以让SLS Linux系统内核能正确识别这个新加的分区。
6. 再次进入SLS Linux模拟系统后,我们使用mkfs命令在刚建立的第1个分区上创建MINIX文件系统。命令与信息如下所示。这里创建了具有64000个数据块的分区(一个数据块为1KB字节)。
mkfs /dev/hdb1 64000
至此,我们完成了在hdc.img文件的第1个分区中创建文件系统的工作。
注解:分区建立好后,在hdc.img这个硬盘上就有了如下的结构(第一个分区):
Boot块 SUPER块 i节点位图块 逻辑块位图块 i节点块 数据块
|
|
|
|
|
|
其它分区也具有相同的结构。其中
Boot块用一个盘块,逻辑块号为0。
SUPER块用一个盘块,逻辑块号为1。
i节点位图块不多于8个逻辑块,块数为超级块字段中s_imap_blocks的值,逻辑块号从2开始。
逻辑块位图块不多于8个逻辑块,块数为超级块字段中s_zmap_blocks的值,逻辑块号紧跟i节点位图块号。
i节点所使用的块为超级块字段中s_ninodes/ INODES_PER_BLOCK的值(INODES_PER_BLOCK为每个块中所占有的i节点个数)。
数据块为文件存放数据的地方,又称为数据逻辑块,一个逻辑块为一个盘块,一个盘块占有两个扇区,也就是1024个字节。
如果硬盘仅作为根文件系统的盘,且文件系统放在第一个分区,则该块硬盘的硬盘分区表就放在引导分区中的0x1BE(446)个字节处。该分区表的读取我们在后面讲解,而第510字节 != 0x55 或者第511字节!= 0xAA,则说明该分区表是坏的。也就是第一个分区中的引导扇区的最后两个字节要满足这个要求。
FDISK只是做了一件事情,就是在硬盘分区表中记录了各个分区的起始扇区号和结束扇区号,这个想法是否正确可能需要fdisk SOURCE CODE的验证。mkfs /dev/hdb1 64000 才是真的在第一个分区上建立我上面所说的MINIX的文件系统的结构。 我想同时还应该建立了第一个目录---根目录和其所对应的i节点。目录和节点的关系在后面讲解。
二、将软盘内容复制到硬盘
(1)用软盘启动:
先放入boot盘,再放入root盘
(2)若还想格式化hdc.img:
mkfs /dev/hd1 64000
(3)把软盘上的根文件系统复制到硬盘上
[/mnt]#for i in bin dev etc usr tmp
> do
> cp +recursive +verbose /$i $i
done
注解: 这个时候我们就可以mount我们的hdc.img了,然后把上面的文件拷贝到我们的硬盘上,在拷贝的时候同时建立了硬盘上的文件和i节点的对应关系,一个i节点是否可用又是由i节点位图对应的位来确定的,此处我们需要知道的就是在我们的硬盘上文件和i节点的已经建立对应关系。
三、使用硬盘Image上的根文件系统
一旦你在硬盘Image文件上建立好文件系统,就可以让Linux
0.11以它作为根文件系统启动。这通过修改引导盘bootimage-0.11文件的第509、510字节的内容就可以实现。请按照以下步骤来进行。
1. 首先复制bootimage-0.11和bochsrc.bxrc两个文件,产生bootimage-0.11-hd和bochsrc-hd.bxrc文件。
2. 编辑bochsrc-hd.bxrc配置文件。把其中的'floppya:'上的文件名修改成'bootimage-0.11-hd',并存盘。
3. 用UltraEdit或任何其它可修改二进制文件的编辑器(winhex等)编辑bootimage-0.11-hd二进制文件。修改第509、510字 节(原值应该是00、00)为01、03,表示根文件系统设备在硬盘Image的第1个分区上。然后存盘退出。如果把文件系统安装在了别的分区上,那么需 要修改前1个字节以对应到你的分区上。
注解: 内核就是放在引导盘bootimage-0.11中的,该盘启动的时候,通过BIOS会读取引导盘上的部分内容。例如引导扇区(512字节)的代码先被BIOS读到内存绝对地址0x
在内核代码中是这样实现的
#define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)
ROOT_DEV = ORIG_ROOT_DEV;
这样我们就取得了根文件系统所在的分区为ROOT_DEV=0x0301.也就是第一块硬盘的第一个分区。
下面内容可作为参考,但是本文下面要讲述的是第三部分的内容,也就是MINIX文件系统的启动。
四、在Linux 0.11系统上编译0.11内核
目前作者已经重新组建了一个带有gcc 1.40编译环境的Linux
0.11系统软件包。该系统设置成在Bochs仿真系统下运行,并且已经配置好相应的bochs配置文件。该软件包可从下面地址得到。
http://oldlinux.org/Linux.old/bochs/linux-0.11-devel-040329.zip
在/usr/src/linux目录下键入'make'命令即可编译Linux 0.11内核源代码,并生成引导启动映象文件Image。若需要输出这个Image文件,可以首先备份bootimage-0.11-hd文件,然后使用
下面命令就会把bootimage-0.11-hd替换成新的引导启动文件。直接重新启动Bochs即可使用该新编译生成的bootimage-0.11 -hd来引导系统。
[/usr/src/linux]# make
[/usr/src/linux]# dd bs=8192 if=Image of=/dev/fd0
14.1内核引导启动+根文件系统组成的集成盘
本节内容主要说明制作由内核引导启动映像文件和根文件系统组合成的集成盘映像文件的制作原理和方法。主要目的是了解Linux
0.11内核内存虚拟盘工作原理,并进一步理解引导盘和根文件系统盘的概念。加深对kernel/blk_drv/ramdisk.c程序运行方式的理
解。在制作这个集成盘之前,我们需要首先下载或准备好以下实验软件:
http://oldlinux.org/Linux.old/bochs/linux-0.11-devel-040923.zip
http://oldlinux.org/Linux.old/images/rootimage-0.11-for-orig
linux-0.11-devel是运行在Bochs下的带开发环境的Linux 0.11系统,rootimage-0.11是1.44MB软盘映像文件中的Linux 0.11根文件系统。后缀'for-orig'是指该根文件系统适用于未经修改的Linux 0.11内核源代码编译出的内核引导启动映像文件。当然这里所说的“未经修改”是指没有对内核作过什么大的改动,因为我们还是要修改编译配置文件 Makefile,以编译生成含有内存虚拟盘的内核代码来。
通常我们使用软盘启动Linux 0.11系统时需要两张盘(这里“盘”均指对应软盘的Image文件):一张是内核引导启动盘,一张是基本的根文件系统盘。这样必须使用两张盘才能引导启
动系统来正常运行一个基本的Linux系统,并且在运行过程中根文件系统盘必须一直保持在软盘驱动器中。而我们这里描述的集成盘是指把内核引导启动盘和一
个基本的根文件系统盘的内容合成制作在一张盘上。这样我们使用一张集成盘就能引导启动Linux 0.11系统到命令提示符状态。集成盘实际上就是一张含有根文件系统的内核引导盘。
为了能运行集成盘系统,该盘上的内核代码中需要开启内存虚拟盘(RAMDISK)的功能。这样集成盘上的根文件系统就能被加载到内存中的虚拟盘中,从而系统上的两个软盘驱动器就能腾出来用于加载(mount)其他文件系统盘或派其他用途。下面我们再详细介绍一下在一张1.44MB盘上制作成集成盘的原理和步骤。
14.1.1.1引导过程原理
Linux 0.11的内核在初始化时会根据编译时设置的RAMDISK选项判断在系统物理内存是否要开辟虚拟盘区域。如果没有设置RAMDISK(即其长度为0)则 内核会根据ROOT_DEV所设置的根文件系统所在设备号,从软盘或硬盘上加载根文件系统,执行无虚拟盘时的一般启动过程。
如果在编译Linux 0.11内核源代码时,在其linux/Makefile配置文件中定义了RAMDISK的大小,则内核代码在引导并初始化RAMDISK区域后就会首先
尝试检测启动盘上的第256磁盘块(每个磁盘块为1KB,即2个扇区)开始处是否存在一个根文件系统。检测方法是判断第257磁盘块中是否存在一个有效的
文件系统超级块信息。如果有,则将该文件系统加载到RAMDISK区域中,并将其作为根文件系统使用。从而我们就可以使用一张集成了根文件系统的启动盘来
引导系统到shell命令提示符状态。若启动盘上指定磁盘块位置(第256磁盘块)上没有存放一个有效的根文件系统,那么内核就会提示插入根文件系统盘。
在用户按下回车键确认后,内核就把处于独立盘上的根文件系统整个地读入到内存的虚拟盘区域中去执行。这个检测和加载过程见图14-6所示。
图略
图14-7 集成盘上代码结构
对于Linux 0.1x内核,其代码加数据段的长度很小,大约在120KB左右。在开发Linux系统初始阶段,即使考虑到内核的扩展,Linus还是认为内核的长度不
会超过256KB,因此在1.44MB的盘上可以把一个基本的根文件系统放在启动盘的第256个磁盘块开始的地方,组合形成一个集成盘片。一个添加了基本 根文件系统的引导盘(即集成盘)的结构示意图见图14-7所示。其中文件系统的详细结构请参见文件系统一章中的说明。
如上所述,集成盘上根文件系统放置的位置和大小主要与内核的长度和定义的RAMDISK区域的大小有关。Linus在ramdisk.c程序中默 认地定义了这个根文件系统的开始放置位置为第256磁盘块开始的地方。对于Linux 0.11内核来讲,编译产生的内核Image文件(即引导启动盘Image文件)的长度在120KB左右,因此把根文件系统放在盘的第256磁盘块开始的 地方肯定没有问题,只是稍许浪费了一点磁盘空间。还剩下共有1440 - 256 = 1184 KB空间可用来存放根文件系统。当然我们也可以根据具体编译出的内核大小来调整存放根文件系统的开始磁盘块位置。例如我们可以修改ramdisk.c第 75行block的值为130把存放根文件系统的开始位置往前挪动一些以腾出更多的磁盘空间供盘上的根文件系统使用。
在不改动内核程序ramdisk.c中默认定义的根文件系统开始存放磁盘块位置的情况下,我们假设需要制作集成盘上的根文件系统的容量为 1024KB(最大不超过1184KB)。制作集成盘的主要思路是首先建立一个1.44MB的空的Image盘文件,然后将新编译出的开启了 RAMDISK功能的内核Image文件复制到该盘的开始处。再把定制的大小不超过1024KB的文件系统复制到该盘的第256磁盘块开始处。具体制作步 骤如下所示。
14.1.2.1重新编译内核
重新编译带有RAMDISK定义的内核Image文件,假定RAMDISK区域设置为2048KB。方法如下:
在Bochs系统中运行linux-0.11-devel系统。编辑其中的/usr/src/linux/Makefile文件,修改以下设置行:
RAMDISK = -DRAMDISK = 2048
ROOT_DEV = FLOPPY
然后重新编译内核源代码生成新的内核Image文件。
make clean; make
14.1.2.2制作临时根文件系统
制作大小为1024KB的根文件系统Image文件,假定其文件名为rootram.img。制作方法如下:
(1) 利用本章前面介绍的方法制作一张大小为1024KB的空Image文件。假定该文件的名称是rootram.img。可使用在现在的Linux系统下执行下面命令生成:
dd bs=1024 if=/dev/zero of=rootram.img count=1024
(2) 在Bochs系统中运行linux-0.11-devel系统。然后在Bochs主窗口上把驱动盘分别配置成:A盘为rootimage-0.11-orign;B盘为rootram.img。
(3) 使用下面命令在rootram-0.11盘上创建大小为1024KB的空文件系统。然后使用下列命令分别把A盘和B盘加载到/mnt和/mnt1目录上。若目录/mnt1不存在,可以建立一个。
mkfs /dev/fd1 1024
mkdir /mnt1
mount /dev/fd0 /mnt
mount /dev/fd1 /mnt1
(4) 使用cp命令有选择性地复制/mnt上rootimage-0.11-orign中的文件到/mnt1目录中,在/mnt1中制作出一个根文件系统。若遇 到出错信息,那么通常是容量已经超过了1024KB了。利用下面的命令或使用本章前面介绍的方法来建立根文件系统。
首先精简/mnt/中的文件,以满足容量不要超过1024KB的要求。我们可以删除一些/bin和/usr/bin下的文件来达到这个要求。关于容量可以使用df命令来查看。例如我选择保留的文件是以下一些:
[/bin]# ll
total 495
-rwx--x--x 1 root root 29700
Apr 29 20:15 mkfs
-rwx--x--x 1 root root 21508
Apr 29 20:15 mknod
-rwx--x--x 1 root root 25564
Apr 29 20:07 mount
-rwxr-xr-x 1 root root 283652 Sep 28
10:11 sh
-rwx--x--x 1 root root 25646
Apr 29 20:08 umount
-rwxr-xr-x 1 root 4096 116479 Mar 3
2004 vi
[/bin]#[/bin]# cd /usr/bin
[/usr/bin]# ll
total 364
-rwxr-xr-x 1 root root 29700
Jan 15 1992 cat
-rwxr-xr-x 1 root root 29700
Mar 4 2004 chmod
-rwxr-xr-x 1 root root 33796
Mar 4 2004 chown
-rwxr-xr-x 1 root root 37892
Mar 4 2004 cp
-rwxr-xr-x 1 root root 29700
Mar 4 2004 dd
-rwx--x--x 1 root 4096 36125
Mar 4 2004 df
-rwx--x--x 1 root root 46084
Sep 28 10:39 ls
-rwxr-xr-x 1 root root 29700
Jan 15 1992 mkdir
-rwxr-xr-x 1 root root 33796
Jan 15 1992 mv
-rwxr-xr-x 1 root root 29700
Jan 15 1992 rm
-rwxr-xr-x 1 root root 25604
Jan 15 1992 rmdir
[/usr/bin]#
然后利用下列命令复制文件。另外,可以按照自己的需要修改一下/etc/fstab和/etc/rc文件中的内容。
cd /user
for i in bin dev etc usr tmp
do
cp +recursive +verbose /$i $i
done
sync
(5) 使用umount命令卸载/dev/fd0和/dev/fd1上的文件系统,然后使用dd命令把/dev/fd1中的文件系统复制到Linux-0.11-devel系统中,建立一个名称为rootram-0.11的根文件系统Image文件:
dd bs=1024 if=/dev/fd1 of=rootram-0.11 count=1024
此时在Linux-0.11-devel系统中我们已经有了新编译出的内核Image文件/usr/src/linux/Image和一个简单的容量不超过1024KB的根文件系统映像文件rootram-0.11。
14.1.2.3建立集成盘
组合上述两个映像文件,建立集成盘。修改Bochs主窗口A盘配置,将其设置为前面准备好的1.44MB名称为bootroot-0.11的映像文件。然后执行命令:
dd bs=8192 if=/usr/src/linux/Image of=/dev/fd0
dd bs=1024 if=rootram-0.11 of=/dev/fd0 seek=256
sync;sync;sync;
其中选项bs=1024 表示定义缓冲的大小为1KB。seek=256 表示写输出文件时跳过前面的256个磁盘块。然后退出Bochs系统。此时我们在主机的当前目录下就得到了一张可以运行的集成盘映像文件bootroot-0.11
14.1.3运行集成盘系统
先为集成盘制作一个简单的Bochs配置文件bootroot-0.11.bxrc。其中主要设置是:
floppya: 1_44=bootroot-0.11
然后用鼠标双击该配置文件运行Bochs系统。此时应有如图14-8所示显示结果。
为了方便大家做实验,也可以从下面网址下载已经做好并能立刻运行的集成盘软件:
第三部分
MINIX文件系统的启动
如果要很好的理解这部分内容,请参考《LINUX内核完全注释》。
1、系统启动时安装根文件系统
内核在启动时把所有的初始化都完成后,就启动第一个进程init,在该函数中通过系统调用setup进入内核态。
void init(void)
{
setup((void *)
&drive_info);
}
参数drive_info是内存地址0x90080处的内容,为boot/setup.s中的程序在执行的时候通过BIOS中断把硬盘的相关信息存放所在的位置。
#define DRIVE_INFO (*(struct drive_info *)0x90080)
drive_info = DRIVE_INFO;
2、安装函数的系统调用的实现在kernel/blk_drv的hd.c文件中
一个硬盘必须知道要有多少个柱面,多少个磁头,每磁道多少个扇区等信息,
那么首先我们就要读取一个硬盘的这些信息。然后我们再读硬盘的分区表,填充我们的硬盘分区结构,首先我们读硬盘的第0个逻辑块,如果是第一块硬盘则设备号为0x300,如果是第二块硬盘则设备号为0x305,我们把硬盘上的1K的(盘块大小)内容读到内存中的高速缓冲区中(1K大小内存中),这个缓冲区由一个struct buffer_head的结构管理,我们通过结构struct partition *p 把第0个逻辑块中偏移0x1BE处的数据读入,这样我们就可以把每个分区的开始扇区和扇区大小赋予我们的分区结构数组struct hd_struct hd中每个元素的字段的值了。
int sys_setup(void
* BIOS)
{
static int callable = 1;
int i,drive;
unsigned char cmos_disks;
struct partition *p;
struct buffer_head * bh;
if (!callable)
return -1;
callable = 0;
#ifndef HD_TYPE
for (drive=0 ; drive<2 ; drive++) {
hd_info[drive].cyl = *(unsigned
short *) BIOS;
hd_info[drive].head = *(unsigned
char *) (2+BIOS);
hd_info[drive].wpcom = *(unsigned
short *) (5+BIOS);
hd_info[drive].ctl = *(unsigned
char *) (8+BIOS);
hd_info[drive].lzone = *(unsigned
short *) (12+BIOS);
hd_info[drive].sect = *(unsigned
char *) (14+BIOS);
BIOS += 16;
}
if (hd_info[1].cyl)
NR_HD=2;
else
NR_HD=1;
#endif
for (i=0 ; i
hd[i*5].start_sect = 0;
hd[i*5].nr_sects =
hd_info[i].head*
hd_info[i].sect*hd_info[i].cyl;
}
if ((cmos_disks = CMOS_READ(0x12)) &
0xf0)
if (cmos_disks & 0x
NR_HD = 2;
else
NR_HD = 1;
else
NR_HD = 0;
for (i = NR_HD ; i < 2 ; i++) {
hd[i*5].start_sect = 0;
hd[i*5].nr_sects = 0;
}
for (drive=0 ; drive
if (!(bh = bread(0x300 +
drive*5,0))) {
printk("Unable to read
partition table of drive %d\n\r",
drive);
panic("");
}
if (bh->b_data[510] != 0x55 ||
(unsigned char)
bh->b_data[511] != 0xAA) {
printk("Bad partition
table on drive %d\n\r",drive);
panic("");
}//填充硬盘的分区表
p = 0x1BE + (void *)bh->b_data;
for (i=1;i<5;i++,p++) {//p++下一个分区结构,跳过32个字节。
hd[i+5*drive].start_sect =
p->start_sect;
hd[i+5*drive].nr_sects =
p->nr_sects;
}
brelse(bh);
}
if (NR_HD)
printk("Partition table%s
ok.\n\r",(NR_HD>1)?"s":"");
rd_load();
mount_root();
return (0);
}
上面hd[0]和hd[5]中是整个硬盘的分区信息,把这些硬盘信息读取后,则调用mount_root函数,开始加载根文件系统
3、函数mount_root在fs/super.c文件中
首先初始化文件表数组和超级块数组,接着读设备号为0x301这个分区的超级块信息,然后读根目录的i节点,根目录的节点号为1,成功读取根目录的i节点后把0x301这个分区的超级块的安装点、根目录、当前目录的i节点都指向刚读取的i节点。
然后我们通过超级块结构取得逻辑块数和i节点数,然后我们根据逻辑块位图和i节点位图判断相应的位是否被占用了,如果该位为0,则表示位图对应的逻辑块或i节点空闲。逻辑块位图0号逻辑块和i节点位图的0号i节点都没有使用。所以对应的位置为1。
逻辑块位图表示的是数据逻辑块的使用情况,也就是第1位指示的是数据逻辑块的第一个块的使用情况!
set_bit应该理解为test_bit,函数原型为:
参数addr为测试比特位操作的起始地址,参数bitnr是比特位偏移值。
#define set_bit(bitnr,addr) ({ \
register int __res __asm__("ax"); \
__asm__("bt %2,%3;setb %%al":"=a"
(__res):"a" (0),"r" (bitnr),"m" (*(addr))); \
__res; })
功能:测试指定位偏移处的比特位的值(0或1),并返回该比特值。
void mount_root(void)
{
int i,free;
struct super_block * p;
struct m_inode * mi;
if (32 != sizeof (struct d_inode))
panic("bad i-node
size");
for(i=0;i
file_table[i].f_count=0;
if (MAJOR(ROOT_DEV) == 2) {
printk("Insert root floppy
and press ENTER");
wait_for_keypress();
}
for(p = &super_block[0] ; p <
&super_block[NR_SUPER] ; p++) {
p->s_dev = 0;
p->s_lock = 0;
p->s_wait = NULL;
}
if (!(p=read_super(ROOT_DEV)))
panic("Unable to mount
root");
if (!(mi=iget(ROOT_DEV,ROOT_INO)))
panic("Unable to read root
i-node");
mi->i_count += 3 ; /* NOTE! it is logically used 4 times, not
1 */
p->s_isup = p->s_imount = mi;
current->pwd = mi;
current->root = mi;
free=0;
i=p->s_nzones;
while (-- i >= 0)
if
(!set_bit(i&8191,p->s_zmap[i>>13]->b_data))
free++;
printk("%d/%d free
blocks\n\r",free,p->s_nzones);
free=0;
i=p->s_ninodes+1;
while (-- i >= 0)
if
(!set_bit(i&8191,p->s_imap[i>>13]->b_data))
free++;
printk("%d/%d free
inodes\n\r",free,p->s_ninodes);
}
下面这段代码的意思是首先从超级块中取得总共有多少逻辑块,然后对每一个块,看他落在位图s_zmap数组的哪一个区间,因为一个盘块有1K,也就是8K(=8192=1<<13)位。也就是在一个s_zmap区间中可以表示8192个逻辑块。所以我们要算我们的块号是在那个位图区间中,然后i&8191(表示0..8191为8192个值)表示相对与这个位图区间的起始地址的偏移位数。相应的位为0则表示还没有使用。
i=p->s_nzones;
while (-- i >= 0)
if (!set_bit(i&8191,p->s_zmap[i>>13]->b_data))
free++;
下面我们来算一下i节点要占有多少个逻辑块,我们的硬盘是
i节点在磁盘(disk)上的结构为:
struct d_inode {
unsigned
short i_mode;
unsigned
short i_uid;
unsigned
long i_size;
unsigned
long i_time;
unsigned
char i_gid;
unsigned
char i_nlinks;
unsigned
short i_zone[9];
};
一个逻辑块所能存储的d_inode数量为INODES_PER_BLOCK
=BLOCK_SIZE/sizeof(struct d_inode)=1024/32=32。假设i节点位图占用了3个逻辑块,那么i节点位图所能表示的最大i节点数为3*(8*1024)=24576个i节点,所以i节点区所占用的逻辑块数为24576/32=768。
那么我们也就可以算出我们的第一个数据逻辑块的块号了=2(引导块+超级块)+3(i节点位图所占的块)+8(逻辑块位图所占用的块)+768(i节点区所占用的逻辑块)=781。
超级块结构中显示i节点数量的字段为s_ninodes。
我们把超级块读入内存后(超级块数组中的一个元素),则MINIX文件系统的启动加载就算是完成了。
关于s_ninodes的个数在分区的时候就已经确定了,但是我还是没法知道是怎么计算出来的。
补充:在oldlinux论坛上找到了对mkfs.c注释的原代码,代码中有信息说明是如何知道总i节点数目的,感谢hooligan版主。附件中哟有注释了的代码!!
下面我们来说几个比较低层一点的函数:
一、硬盘数据在文件系统的管理下怎么读到内存中的?
使用的函数是bread,通过设备号和逻辑块号,读取相应的逻辑块的内容到内存中的同样块大小(1K)的地方,内存中的块是由一个buffer_head结构管理的,这个结构除了一个指向内存中管理的1K地址的pointer外,还有其它的字段,描述该块的使用情况。
结构为:truct
buffer_head {
char *
b_data; /* pointer to
data block (1024 bytes) */
unsigned
long b_blocknr; /* block number */
unsigned
short b_dev; /* device (0 = free)
*/
unsigned
char b_uptodate;
unsigned
char b_dirt; /* 0-clean,1-dirty
*/
unsigned
char b_count; /* users using
this block */
unsigned
char b_lock; /* 0 - ok, 1 -locked
*/
struct
task_struct * b_wait;
struct
buffer_head * b_prev;
struct
buffer_head * b_next;
struct
buffer_head * b_prev_free;
struct
buffer_head * b_next_free;
};
b_blocknr和b_dev是组成哈希表的时候要用到的,b_prev和b_next是在HASH表链中要用到的pointer。
b_dirt,b_count,b_lock描述该块的使用情况。
b_prev_free,b_next_free是所有buffer_head结构连接起来的指针。
内核模块(
|
|
|
|
|
|
缓冲块 |
|
|
|
|
头结构
在buffer_init(fs/buffer.c)初始化的时候,高速缓冲区的开始部分为buffer_head结构,1K大小的内存块从高速缓冲区的末端往前划分。最后在头结构和缓冲块之间不足1K空间的弃之不用。
每一个缓冲头结构通过b_prev_free,b_next_free连接成空闲缓冲块双向链表结构。
free_list指向头结构的第一个块:
|
|
|
|
哈希表
上面的连到hash表上的表示相应的逻辑块已经读入了内存中,free_list指向的表示内存中还没有使用的块的头结构。在空闲的链表中是b_prev_free,b_next_free字段连接的。
而哈希表是通过b_prev和b_next字段连接的。
有了上面的知识,那么在理解文件管理系统把硬盘上的数据读入高速缓冲块的时候就容易了。
struct buffer_head * bread(int dev,int block)
{
struct
buffer_head * bh;
if
(!(bh=getblk(dev,block)))
panic("bread:
getblk returned NULL\n");
if
(bh->b_uptodate)
return
bh;
ll_rw_block(READ,bh);
wait_on_buffer(bh);
if
(bh->b_uptodate)
return
bh;
brelse(bh);
return
NULL;
}
上面的if
(bh->b_uptodate)检查该块是否有效,如果getblk是新申请的空闲块,则我们要调用ll_rw_block(READ,bh)通过硬盘的驱动程序从硬盘上读数据了,这就是驱动程序和高速缓冲交互的事了。
如果getblk是哈希表中的取得的缓冲头结构,则表示硬盘上该块的内容已经读入高速缓冲中,所以可以直接返回。
Getblk就是根据设备号和块号首先搜索HASH表,搜索成功就直接返回。否则就在free_list所连接的空闲队列中找到一个缓冲头结构,然后重新安排该缓冲头结构在HASH表和free_list所连接的空闲列表中的位置。
一、通过i节点号怎么找到i节点的?
通过i节点号找到 i节点使用iget(fs/inode.c)函数完成的。
该函数首先从i节点表中申请一个空闲的i节点,不用麻烦的申请内存了(struct
m_inode inode_table[NR_INODE]={{0,},};在启动的时候就会分配内存的)。
然后我们还是要在i节点表中扫描看我们需要的i节点是否已经读入到i节点表中了,如果已经存在了,就做一些检查比如设备号是否匹配。
如果该i节点还是某个文件系统的安装点,则我们要扫描超级块表,找到对应的超级块,并从安装在此i节点的超级块上取设备号,并令i节点号为1,然后从新搜索该i节点表。
如果该i节点不是文件系统的安装点,则释放申请的空闲i节点,可以返回找到的i节点了。
如果在i节点表中没有找到指定的i节点,则利用前面申请的空闲i节点在i节点表中建立该节点。并且该i节点的设备字段和i节点号字段被赋值。
然后还要从设备上读取相应的节点号的i节点信息,并返回该i节点。
struct m_inode *
iget(int dev,int nr)
{
struct m_inode * inode, * empty;
if (!dev)
panic("iget with
dev==0");
empty = get_empty_inode();//取一个空闲的,备用
inode = inode_table;
while (inode < NR_INODE+inode_table) {
if (inode->i_dev != dev ||
inode->i_num != nr) {
inode++;
continue;
}
wait_on_inode(inode);
if (inode->i_dev != dev ||
inode->i_num != nr) {
inode = inode_table;
continue;
}
inode->i_count++;
if (inode->i_mount) {
int i;
for (i = 0 ; i
if
(super_block[i].s_imount==inode)
break;
if (i >= NR_SUPER) {
printk("Mounted
inode hasn't got sb\n");
if (empty)
iput(empty);
return inode;
}
iput(inode);
dev = super_block[i].s_dev;
nr = ROOT_INO;
inode = inode_table;
continue;
}
if (empty)
iput(empty);
return inode;
}
if (!empty)
return (NULL);
inode=empty;
inode->i_dev = dev;
inode->i_num = nr;
read_inode(inode);//从磁盘上读信息
return inode;
}
下面我们有必要说说read_inode这个函数:
static void
read_inode(struct m_inode * inode)
{
struct super_block * sb;
struct buffer_head * bh;
int block;
lock_inode(inode);
if (!(sb=get_super(inode->i_dev)))
panic("trying to read inode
without dev");
block = 2 + sb->s_imap_blocks +
sb->s_zmap_blocks +
(inode->i_num-1)/INODES_PER_BLOCK;
if (!(bh=bread(inode->i_dev,block)))
panic("unable to read i-node
block");
*(struct d_inode *)inode =
((struct d_inode *)bh->b_data)
[(inode->i_num-1)%INODES_PER_BLOCK];
brelse(bh);
unlock_inode(inode);
}
首先要取得所在设备的超级块的信息,我们要计算i节点位图和逻辑块位图占用的逻辑块。
然后计算该i节点号所在的逻辑块block = 2 + sb->s_imap_blocks +
sb->s_zmap_blocks +(inode->i_num-1)/INODES_PER_BLOCK,然后我们就可以通过上面介绍的bread函数从硬盘上读取相应逻辑块的内容到高速缓冲块中了。
我们还要通过i节点号计算该i节点在块中的便移位置,把那一段内存转化成一个存储在磁盘上的d_inode结构。并赋给我们的内存inode的前面部分的数值也就是d_inode部分。
这样我们就从硬盘上找到了相应i节点号的信息了。
*(struct d_inode
*)inode =((struct d_inode
*)bh->b_data)[(inode->i_num-1)%INODES_PER_BLOCK];
而(struct d_inode *)bh->b_data转化为struct d_inode类型的数组,后面的[ ]下标就是一个d_inode结构了,而不是指针。
例如:
Char arr[16];
Long b1=((long
*)arr)[1];
转化为long数组后,就表示arr是4个字节为一个元素了,所以b1共占用了arr中的4个单元,范围是arr[4..7]。
用指针也可以理解这种转化,上面的(long *)arr可以说是转化成long指针了,arr指向第一个地址,那么arr+1当然就是第二个long单元的值了。
C语言这东西果然是很强大,很难相信什么程度的人能说把它搞精通了。
二、从用户的角度说,通过文件名是怎么找到i节点的?
你得相信一个文件名(或者目录名)就对应一个i节点,只有通过i节点,你才能找到你的文件在硬盘上数据区的内容。
那么是MINIX文件系统是怎么实现的呢?是用namei这个函数实现的,含义也就是通过名字找到i节点。
首先我们通过路径名找到文件所在的最顶层目录,如果我们给的就是一个目录,则直接返回该目录的i节点就行。
接着我们在该目录下面查找是否含有该名字的目录项,并同时返回该名字所
在的目录项结构和目录项所在的缓冲头结构。
然后我们用目录项的设备号和i节点号通过iget函数找到对应文件名的i节点,返回该i节点。
struct m_inode * namei(const char * pathname)
{
const char
* basename;
int
inr,dev,namelen;
struct
m_inode * dir;
struct
buffer_head * bh;
struct
dir_entry * de;
// dir_namei为该名字所在目录的i节点,如果路径名为/usr/bin/,
//则返回的就是bin这个目录的i节点,如果路径名是/usr/bin,则返回的是usr这个目录的i节点。
if (!(dir =
dir_namei(pathname,&namelen,&basename)))
return
NULL;
if
(!namelen) /* special case:
'/usr/' etc */
return
dir;
bh =
find_entry(&dir,basename,namelen,&de);
if (!bh) {
iput(dir);
return
NULL;
}
inr =
de->inode;
dev =
dir->i_dev;
brelse(bh);
iput(dir);
dir=iget(dev,inr);
if (dir) {
dir->i_atime=CURRENT_TIME;
dir->i_dirt=1;
}
return dir;
}
函数dir_namei首先查找该路径的顶层目录的i节点,然后处理最后的文件名。
static struct m_inode * dir_namei(const char *
pathname,
int *
namelen, const char ** name)
{
char c;
const char
* basename;
struct
m_inode * dir;
//顶层目录的查找函数
if (!(dir =
get_dir(pathname)))
return
NULL;
basename =
pathname;
while
(c=get_fs_byte(pathname++))
if
(c=='/')
basename=pathname;
*namelen =
pathname-basename-1;
*name =
basename;
return dir;
}
下面看看最终是怎么取得所在目录的顶层目录的i节点的。
比如/usr/home/test,如果test是一个文件名,则返回test所在目录的i节点。
首先我们要判断当前进程的根和进程的当前工作目录是否存在,且必须被使用,因为我们的路径搜索要从这两者之一的其中一个开始。如果是绝对路径则搜索从当前进程的根;如果路径最前面没有/ 则是相对路径,搜索从当前工作目录开始。
然后我们通过一个循环一级一级的查找目录,如果我们在当前搜索的i节点中找到了下一级目录的目录项,然后我们就要使用目录项的设备号和i节点号找到下一级目录所在的i节点。然后把下一级目录所在的i节点作为当前搜索的i节点继续搜索,
直到找到文件所在的目录了,这是通过
for(namelen=0;(c=get_fs_byte(pathname++))&&(c!='/');namelen++)
/*
nothing */ ;
if
(!c)
return
inode;
循环实现的,循环过后,如果pathname走到最后了,则不可能有‘/’字符返回,所以返回的c一定是NULL,也即是pathname走到最后了。
static struct m_inode * get_dir(const char * pathname)
{
char c;
const char
* thisname;
struct
m_inode * inode;
struct
buffer_head * bh;
int
namelen,inr,idev;
struct
dir_entry * de;
if
(!current->root || !current->root->i_count)
panic("No
root inode");
if
(!current->pwd || !current->pwd->i_count)
panic("No
cwd inode");
if
((c=get_fs_byte(pathname))=='/') {
inode
= current->root;
pathname++;
} else if
(c)
inode
= current->pwd;
else
return
NULL; /* empty name is bad */
inode->i_count++;
//循环查找路径名的各个目录的i节点
while (1) {
thisname
= pathname;
if
(!S_ISDIR(inode->i_mode) || !permission(inode,MAY_EXEC)) {
iput(inode);
return
NULL;
}
//计算下一级目录的名字
for(namelen=0;(c=get_fs_byte(pathname++))&&(c!='/');namelen++)
/*
nothing */ ;
if
(!c)
return
inode;
//在当前搜索目录中查找下一级目录名是否存在
if
(!(bh = find_entry(&inode,thisname,namelen,&de))) {
iput(inode);
return
NULL;
}
//使用下一级目录名所在的i节点继续搜索
inr
= de->inode;
idev
= inode->i_dev;
brelse(bh);
iput(inode);
if
(!(inode = iget(idev,inr)))
return
NULL;
}
}
查找目录下的某个文件名或者目录名是否存在的函数,
参数dir为目录的i节点;
Name为需要搜索的文件名或者目录名;
Res_dir为返回文件名或者目录名所在的目录项。
如果文件名为’..’,则我们有两种特殊情况要处理,其一 当前搜索目录就是当前进程的根目录,则简单的将文件长度变为1,也就是文件名为”.”; 其二 当前搜索目录的i节点号为1,说明是文件系统的根节点,在一个安装点上的’..’,将导致目录切换到安装到文件系统的目录i节点,然后我们让当前搜索目录指向被安装到的i节点,然后对被安装到的i节点进行处理.
然后我们取得i节点所指向的逻辑块号block = (*dir)->i_zone[0],把该块读入内存,依次比较每一个目录项的名字,如果匹配成功则返回该目录项和目录项所在的高速缓冲头结构,否则循环读取下一个块中的目录项,直到比较的目录项数量超过了该i节点所包含的目录项数目.
static struct buffer_head * find_entry(struct m_inode
** dir,
const char
* name, int namelen, struct dir_entry ** res_dir)
{
int
entries;
int
block,i;
struct
buffer_head * bh;
struct
dir_entry * de;
struct
super_block * sb;
#ifdef NO_TRUNCATE
if (namelen
> NAME_LEN)
return
NULL;
#else
if (namelen
> NAME_LEN)
namelen
= NAME_LEN;
#endif
//该目录下目录项的数目
entries =
(*dir)->i_size / (sizeof (struct dir_entry));
*res_dir =
NULL;
if
(!namelen)
return
NULL;
/* check for '..', as we might have to do some
"magic" for it */
if
(namelen==2 && get_fs_byte(name)=='.' &&
get_fs_byte(name+1)=='.') {
/* '..' in a pseudo-root results in a faked '.' (just
change namelen) */
if
((*dir) == current->root)
namelen=1;
else
if ((*dir)->i_num == ROOT_INO) {
/* '..' over a mount-point results in 'dir' being
exchanged for the mounted
directory-inode. NOTE! We set mounted, so that we can iput the new dir
*/
sb=get_super((*dir)->i_dev);
if
(sb->s_imount) {
iput(*dir);
(*dir)=sb->s_imount;
(*dir)->i_count++;
}
}
}
//首先读该i节点的第一个逻辑块到高速缓冲块中,
if (!(block
= (*dir)->i_zone[0]))
return
NULL;
if (!(bh =
bread((*dir)->i_dev,block)))
return
NULL;
i = 0;
de =
(struct dir_entry *) bh->b_data;
//搜索完所有的目录项
while (i
< entries) { //首先搜索一个块的所有目录项结构,并匹配文件名.
//目录项地址超过了该块的地址,则读下一个逻辑块进行搜索
if
((char *)de >= BLOCK_SIZE+bh->b_data) {
brelse(bh);
bh
= NULL;
// 其中i/DIR_ENTRIES_PER_BLOCK表示属于第几个文件块,然后找到存
//储在该文件块中对应的逻辑块号. 并读该逻辑块到高速缓冲中.
if
(!(block = bmap(*dir,i/DIR_ENTRIES_PER_BLOCK)) ||
!(bh = bread((*dir)->i_dev,block))) {
i
+= DIR_ENTRIES_PER_BLOCK;
continue;
}
de
= (struct dir_entry *) bh->b_data;
}
//匹配成功则返回目录项结构和目录项所在的高速缓冲头结构.
if
(match(namelen,name,de)) {
*res_dir
= de;
return
bh;
}
de++;
i++;
}
brelse(bh);
return
NULL;
}
通过i节点的文件块,找到该块对应的逻辑块.
Bmap也就是create为0的情况,只查找而不创建.
下面我们来说说m_inode的结构:
struct m_inode {
unsigned
short i_mode;
unsigned
short i_uid;
unsigned
long i_size;
unsigned
long i_mtime;
unsigned
char i_gid;
unsigned
char i_nlinks;
unsigned short i_zone[9];
/* these are in memory also */
struct
task_struct * i_wait;
unsigned
long i_atime;
unsigned
long i_ctime;
unsigned
short i_dev;
unsigned
short i_num;
unsigned
short i_count;
unsigned
char i_lock;
unsigned
char i_dirt;
unsigned
char i_pipe;
unsigned
char i_mount;
unsigned
char i_seek;
unsigned
char i_update;
};
关系到下面函数要用的主要是i_zone[9]这个字段,所以我们就来说说它的意思: i_zone[0].. i_zone[6]存放的是对应文件的直接逻辑块, i_zone[7]用于存放一次间接逻辑块号,
i_zone[8]用于存放二次间接逻辑块号.
所以如果文件块数为0..6的时候,逻辑块号就取i_zone[0].. i_zone[6]中的内容,但是如果文件块数为7到518时,则用i_zone[7]中存放的一次间接逻辑块号
读出该块到高速缓冲中,然后用(文件块数-7)做为下标在高速缓冲块中找到其中的一项,取出该项的值即是该文件块所对应的逻辑块号.注意518-7=511<512,则一个块的索引是从0..511的也就是512项.
但是如果文件块数大于518, 则用i_zone[8]中存放的一次间接逻辑块号
读出该块到高速缓冲中,然后用(文件块数/512=文件块数>>9)做为下标在高缓冲块中找到其中的一项,取出该项的值作为二次间接逻辑块号, 由二次间接逻辑块号读出该块到高速缓冲中,然后用(文件块数%512=文件块数&512)做为下标在高缓冲块中找到其中的一项, 取出该项的值即是该文件块所对应的逻辑块号,注意文件块数/512表示落在哪一个区间,文件块数&512表示在该块的偏移.
所以文件块的最大数为=7+512+512*512=262663,所以一个i节点可以表示最大262663文件块.
static int _bmap(struct m_inode * inode,int block,int
create)
{
struct
buffer_head * bh;
int i;
if
(block<0)
panic("_bmap:
block<0");
if (block
>= 7+512+512*512)
panic("_bmap:
block>big");
if
(block<7) {
if
(create && !inode->i_zone[block])
if
(inode->i_zone[block]=new_block(inode->i_dev)) {
inode->i_ctime=CURRENT_TIME;
inode->i_dirt=1;
}
return
inode->i_zone[block];
}
block -= 7;
if
(block<512) {
if
(create && !inode->i_zone[7])
if
(inode->i_zone[7]=new_block(inode->i_dev)) {
inode->i_dirt=1;
inode->i_ctime=CURRENT_TIME;
}
if
(!inode->i_zone[7])
return
0;
if
(!(bh = bread(inode->i_dev,inode->i_zone[7])))
return
0;
i =
((unsigned short *) (bh->b_data))[block];
if
(create && !i)
if
(i=new_block(inode->i_dev)) {
((unsigned
short *) (bh->b_data))[block]=i;
bh->b_dirt=1;
}
brelse(bh);
return
i;
}
block -=
512;
if (create
&& !inode->i_zone[8])
if
(inode->i_zone[8]=new_block(inode->i_dev)) {
inode->i_dirt=1;
inode->i_ctime=CURRENT_TIME;
}
if
(!inode->i_zone[8])
return
0;
if
(!(bh=bread(inode->i_dev,inode->i_zone[8])))
return
0;
i =
((unsigned short *)bh->b_data)[block>>9];
if (create
&& !i)
if
(i=new_block(inode->i_dev)) {
((unsigned
short *) (bh->b_data))[block>>9]=i;
bh->b_dirt=1;
}
brelse(bh);
if (!i)
return
0;
if
(!(bh=bread(inode->i_dev,i)))
return
0;
i =
((unsigned short *)bh->b_data)[block&511];
if (create
&& !i)
if
(i=new_block(inode->i_dev)) {
((unsigned
short *) (bh->b_data))[block&511]=i;
bh->b_dirt=1;
}
brelse(bh);
return i;
}
三、怎么申请硬盘上的一个空闲逻辑块,并返回该逻辑块的块号?
通过函数new_block(fs/bitmap.c)实现。
该函数首先取得设备号上的超级块,通过函数get_super(dev)直接在内存超级块表中查找,所以该设备号的超级块之前没有从硬盘上读入内存,则此时不与理会,返回为0。
我们通过超级块的逻辑块位图的缓冲头结构直接操作硬盘上的逻辑块位图所占用的硬盘上的逻辑块。这是在安装超级块的时候也已经完成的工作,否则,该每一个缓冲头结构都可能没有在内存中分配空间(也就是分配内存中高速缓冲区的一个头结构了)。
然后我们寻找首个0比特位,根据0比特位的偏移计算空闲块的块号。因为逻辑块位图映射的是数据逻辑块部分,所以返回的逻辑块号应该是: 逻辑块位图中的偏移+第一个数据逻辑块号。
然后我们把该块读入内存,清除块上的数据,然后设置缓冲头结构的b_uptodate(更新),b_dirt(脏)字段为1。
释放指定的缓冲区,brelse函数等待该缓冲区解锁,引用计数减1,唤醒等待空闲缓冲区的进程。
缓冲头结构的b_count为0表示缓冲区是空闲的。
int new_block(int dev)
{
struct buffer_head
* bh;
struct
super_block * sb;
int i,j;
//在超级块表中读取对应设备号的超级块
if (!(sb =
get_super(dev)))
panic("trying
to get new block from nonexistant device");
j = 8192;
for (i=0 ;
i<8 ; i++)
if
(bh=sb->s_zmap[i])
if
((j=find_first_zero(bh->b_data))<8192)
break;
if (i>=8
|| !bh || j>=8192)
return
0;
if
(set_bit(j,bh->b_data))
panic("new_block:
bit already set");
bh->b_dirt
= 1;
//计算逻辑块号,块位图偏移还需要加上第一个数据逻辑块号
j += i*8192
+ sb->s_firstdatazone-1;
if (j >=
sb->s_nzones)
return
0;
if (!(bh=getblk(dev,j)))
panic("new_block:
cannot get block");
if
(bh->b_count != 1)
panic("new
block: count is != 1");
clear_block(bh->b_data);
bh->b_uptodate
= 1;
bh->b_dirt
= 1;
//释放缓冲区,也就是缓冲区引用计数减1,并唤醒等待缓冲区的进程
brelse(bh);
return j;
}
第四部分
简单的硬盘设备驱动
为什么要把这一部分也写入了,我觉得有必要了解一下硬件上面的低层操作,假设我们的操作系统不需要文件系统,直接从硬盘上读取数据就行了,这虽然有点笨,但是也不是没有的事情,这种操作系统的应用场合就是LINUX的嵌入式操作系统的应用。
首先我们要知道我们的文件系统在ll_rw_block的时候是把缓冲区加到了一个请求队列中了,我们要新建一个请求时,我们就从下面的请求数组中找到一个空的项(不用去申请内存了),然后把我们缓冲区的相关信息填充到请求的相应字段中。接着根据我们的主设备号比如硬盘是3,则在blk_dev块设备数组表中找到对应的其中一项,然后找到该块设备字段current_request,把我们的新请求加入到current_request所指示的请求链表中。
struct request request[NR_REQUEST];
struct blk_dev_struct {
void
(*request_fn)(void);
struct
request * current_request;
};
struct blk_dev_struct blk_dev[NR_BLK_DEV] = {
{ NULL,
NULL }, /* no_dev */
{ NULL,
NULL }, /* dev mem */
{ NULL,
NULL }, /* dev fd */
{ NULL,
NULL }, /* dev hd */
{ NULL,
NULL }, /* dev ttyx */
{ NULL,
NULL }, /* dev tty */
{ NULL,
NULL } /* dev lp */
};
图为:Blk_dev[3].current_requset的当前请求指向Request 数组中的其中一项,然后Request的指针又会指向下一项,这样就行成了一个请求队列。
Blk_dev[3].current_requset
Request 数组
|
|
|
|
|
|
|
我们的blk_dev有一个字段request_fn,对于硬盘是do_hd_request,即请求处理函数。所以我们新加入了一个请求后,我们要调用我们的请求处理函数对请求队列进行处理。
硬盘读写数据是通过硬盘控制器来进行,也就是对端口的读写了,我们的读写数据是放在硬盘控制器的缓冲区中的,首先我们要定位我们所要读写的数据在硬盘上的位置,即是读写扇区个数,哪一个磁头,哪一个柱面,命令(READ or WRITE)和中断返回后的处理函数。通过调用hd_out函数完成,然后我们就可以把数据写到硬盘控制器的缓冲区中了,调用
port_write(HD_DATA,CURRENT->buffer,256);
把数据写入。
下面函数的参数:drive 哪一块硬盘,0第一块,1第二块;
nsect:扇区个数;sect:某个磁道的起始扇区;
head:哪一个磁头;cyl:哪一个柱面;
cmd:命令;intr_addr:中断返回处理函数
static void hd_out(unsigned int drive,unsigned int nsect,unsigned int sect,
unsigned int head,unsigned int cyl,unsigned int cmd,
void (*intr_addr)(void))
{
register int port asm("dx");
if (drive>1 || head>15)
panic("Trying to write bad sector");
if (!controller_ready())
panic("HD controller not ready");
do_hd = intr_addr;
outb_p(hd_info[drive].ctl,HD_CMD);//写入控制字
port=HD_DATA;
outb_p(hd_info[drive].wpcom>>2,++port);//写入预补偿柱面除以4
outb_p(nsect,++port); //写入扇区个数
outb_p(sect,++port); //磁道的起始扇区,
outb_p(cyl,++port); //柱面的低8位
outb_p(cyl>>8,++port); //柱面的高8位
outb_p(0xA0|(drive<<4)|head,++port); //驱动器号+磁头号
outb(cmd,++port); //命令
}
上面这个函数在哪里被调用了,在请求处理函数do_hd_request中,该函数既然是处理请求的,当然需要做很多工作了,首先它处理的是当前的请求,也就是blk_dev[3]. current_request所指向的请求。
从我们的请求结构中,我们知道我们需要读写的开始扇区号,还需加上该次设备号所在分区的起始扇区号,得到在硬盘上的绝对扇区号,
hd_info[dev].sect表示硬盘上每一个磁道所含有的扇区数,block为在硬盘上的绝对扇区号,下面汇编的意思就是:输入EAX寄存器中放入绝对扇区号,EDX放入0,%4这个寄存器放入磁道所含有的扇区数。
_asm__("divl %4":"=a" (block),"=d" (sec):"0" (block),"1" (0),
"r" (hd_info[dev].sect));
除法的意思是用EAX的值除以%4的值,EAX为商,EDX为余数;
所以block为磁道数,sec为下一磁道的开始扇区数;
,下面汇编的意思就是:输入EAX寄存器中放入磁道数,EDX放入0,%4这个寄存器放入硬盘所含有磁头数。
__asm__("divl %4":"=a" (cyl),"=d" (head):"0" (block),"1" (0),
"r" (hd_info[dev].head));
所以cyl为该扇区所在柱面数,head为该扇区所在磁头;
柱面是相对于几个盘片的,磁道是相对于一个盘片的;几个盘片的同心圆就够成了一个柱面。
对于这个除法可以这样理解,因为一个柱面有15个磁头,也就有了15个磁道,所以
磁道数除以磁头数就是柱面数,余数就是下一个柱面从第一个盘片往下数到该扇区所在盘片的数量*2(一个盘片有2面),也就是磁头数;
例如:如果当前硬盘有9个磁头,8个柱面,每磁道10个扇区;
则我们请求硬盘的第14扇区的位置为:对于第一个除法后,该扇区所在磁道数为14/10=1,下一磁道的开始扇区号是14%10=4;
对于第二个除法后,该扇区所在柱面为1/9=0,该扇区所在磁头1%9=1;
因为磁道是在一个柱面上从第一个盘片往下数的,实际上该扇区应该在第0个盘片的第二面上也是磁头为1号,柱面为0。
当然对于这第二个除法还有一种算法就是用磁道数除以硬盘的柱面数,商为磁头数,余数为该扇区所在柱面数;但是这种算法对于上面给出的例子就是另外一种结果了。
void do_hd_request(void)
{
int i,r;
unsigned int block,dev;
unsigned int sec,head,cyl;
unsigned int nsect;
INIT_REQUEST;
dev = MINOR(CURRENT->dev);
block = CURRENT->sector;
if (dev >= 5*NR_HD || block+2 > hd[dev].nr_sects) {
end_request(0);
goto repeat;
}
block += hd[dev].start_sect;
dev /= 5; //大于等于5表示第二个硬盘
//计算硬盘定位的相关参数
__asm__("divl %4":"=a" (block),"=d" (sec):"0" (block),"1" (0),
"r" (hd_info[dev].sect));
__asm__("divl %4":"=a" (cyl),"=d" (head):"0" (block),"1" (0),
"r" (hd_info[dev].head));
sec++;
nsect = CURRENT->nr_sectors;
if (reset) {
reset = 0;
recalibrate = 1;
reset_hd(CURRENT_DEV);
return;
}
if (recalibrate) {
recalibrate = 0;
hd_out(dev,hd_info[CURRENT_DEV].sect,0,0,0,
WIN_RESTORE,&recal_intr);
return;
}
if (CURRENT->cmd == WRITE) {//写数据到硬盘上,先定位,再写数据
hd_out(dev,nsect,sec,head,cyl,WIN_WRITE,&write_intr);
for(i=0 ; i<3000 && !(r=inb_p(HD_STATUS)&DRQ_STAT) ; i++)
/* nothing */ ;
if (!r) {
bad_rw_intr();
goto repeat;
}
port_write(HD_DATA,CURRENT->buffer,256);
} else if (CURRENT->cmd == READ) {
hd_out(dev,nsect,sec,head,cyl,WIN_READ,&read_intr);
} else
panic("unknown hd-command");
}
在写中断返回处理函数中,我们判断扇区数是不是已经为0,如果还有扇区要写,则重新把中断返回处理函数赋给do_hd(在kernel/system_call.s中被调用),中断调用后do_hd被清0;所以要重新赋值。如果已经发送完毕,则调用end_request把当前请求从请求队列中删除。
static void write_intr(void)
{
if (win_result()) {
bad_rw_intr();
do_hd_request();
return;
}
if (--CURRENT->nr_sectors) {
CURRENT->sector++;
CURRENT->buffer += 512;
do_hd = &write_intr;//该函数是以字为单位写入的
port_write(HD_DATA,CURRENT->buffer,256);
return;
}
end_request(1);
do_hd_request();
}
结束请求处理函数,需要唤醒使用该缓冲区的进程和等待该缓冲区的进程,并且释放当前请求。这样就把文件系统层和设备驱动层联结起来了。
extern inline void end_request(int uptodate)
{
DEVICE_OFF(CURRENT->dev);
if (CURRENT->bh) {
CURRENT->bh->b_uptodate = uptodate;
unlock_buffer(CURRENT->bh);
}
if (!uptodate) {
printk(DEVICE_NAME " I/O error\n\r");
printk("dev %04x, block %d\n\r",CURRENT->dev,
CURRENT->bh->b_blocknr);
}
wake_up(&CURRENT->waiting);
wake_up(&wait_for_request);
CURRENT->dev = -1;
CURRENT = CURRENT->next;
}
结束语:MINIX文件系统是最基本的文件系统,只要搞懂了MINIX文件系统的基础知识,那么理解JFFS2和EXT2等文件系统也不是很难的事!
|
|