文件系统:创建
文件系统的创建在原理上并不复杂,就是创建文件系统所需要的元信息,比如说超级块的位置和大小,日志区的位置和大小,$inode$ 区的位置和大小等等,将这些基本信息写入磁盘相应的地方就是所谓的创建文件系统了。当然这只是基本原理,还有很多细节要处理,我们在 $xv6$ 创建文件系统的程序中再详细了解。
$xv6$ 运行在 $qemu$ 或者 $bochs$ 虚拟机上,磁盘是虚拟化的,是把主机磁盘上的一个文件当作自己的磁盘来使用。主机上有个文件叫做 $fs.img$,虚拟机将这个文件当作 $xv6$ 的磁盘,但是对于 $xv6$ 本身来说,它不知道这个 $fs.img$ 就是主机磁盘上的一个普通文件,对于 $xv6$ 来说,$fs.img$ 就是它的磁盘。虽然多了中间这层虚拟机,但这并不妨碍我们理解文件系统是如何创建的,诸位来看:
指定或计算各类信息
这创建文件系统的第一步就是指定或者计算各类元信息,指定是说自己自定义文件系统多大,$inode$ 日志区等等多大,而计算是指根据前面指定的一些基本信息算出额外的信息,比如说第一个空闲块在哪等等。不多说,直接来看 $xv6$ 如何操作的:
代码语言:c复制#define FSSIZE 1000 //文件系统大小:1000个块
#define BSIZE 512 //块大小:512字节
#define NINODES 200 //inode 200个
#define LOGSIZE (MAXOPBLOCKS*3) //日志区:30块
#define MAXOPBLOCKS 10 //每次文件系统调用最大能够操作的块数
上述主要是指定了一些文件系统的基本信息,比如说文件系统大小,块大小等等,下面来看看需要计算的一些信息:
代码语言:c复制int nbitmap = FSSIZE/(BSIZE*8) 1; //位图大小
int ninodeblocks = NINODES / IPB 1; //inode区大小
#define IPB (BSIZE / sizeof(struct dinode)) //一个块能存放多少个inode
int nlog = LOGSIZE; //日志区的大小
int nmeta = 2 nlog ninodeblocks nbitmap; //元数据的大小,2表示引导块和超级块
int nblocks = FSSIZE - nmeta; //数据区的大小
int freeblock = nmeta; //数据区的起始块号
上述的一些元信息的计算应该还是很简单的,不多解释,注意文件系统的基本单位是块,块数就是大小,块号就是位置即可。
相关功能函数
转化为小端模式
我学习的 $xv6$ 实现基于 $intel$ 的 $x86$ 架构,使用小端模式,$xv6$ 一般运行在虚拟机上,虚拟机又可能运行在各个平台,使用的大小端可能就不一样,这里全转化为小端模式。
代码语言:c复制/**转化为小端模式**/
ushort xshort(ushort x){
ushort y;
uchar *a = (uchar*)&y;
a[0] = x;
a[1] = x >> 8;
return y;
}
uint xint(uint x){
uint y;
uchar *a = (uchar*)&y;
a[0] = x;
a[1] = x >> 8;
a[2] = x >> 16;
a[3] = x >> 24;
return y;
}
运用移位运算来转化大小端,应该还是很简单的,不多说
读写扇区
代码语言:c复制void wsect(uint sec, void *buf) //将buf中的数据写到sec扇区
{
if(lseek(fsfd, sec * BSIZE, 0) != sec * BSIZE){ //定位到要写的位置
perror("lseek");
exit(1);
}
if(write(fsfd, buf, BSIZE) != BSIZE){ //将buf中的数据写进文件
perror("write");
exit(1);
}
}
void rsect(uint sec, void *buf) //从sec扇区中读取数据到buf中
{
if(lseek(fsfd, sec * BSIZE, 0) != sec * BSIZE){ //定位
perror("lseek");
exit(1);
}
if(read(fsfd, buf, BSIZE) != BSIZE){ //读
perror("read");
exit(1);
}
}
写磁盘扇区就变成写文件了,先调用 $lseek$ 系统调用定位到要写的位置 $sec times BSIZE$,读磁盘类似不再多说
读写 inode
代码语言:c复制void winode(uint inum, struct dinode *ip)
{
char buf[BSIZE];
uint bn;
struct dinode *dip;
bn = IBLOCK(inum, sb); //编号为inum的inode在哪个扇区
rsect(bn, buf); //读取这个扇区的数据到buf
dip = ((struct dinode*)buf) (inum % IPB); //inode的位置
*dip = *ip; //结构体赋值
wsect(bn, buf); //写回磁盘(文件)
}
void rinode(uint inum, struct dinode *ip)
{
char buf[BSIZE];
uint bn;
struct dinode *dip;
bn = IBLOCK(inum, sb); //编号为inum的inode在哪个扇区
rsect(bn, buf); //读取这个扇区的数据到buf
dip = ((struct dinode*)buf) (inum % IPB); //inode的位置
*ip = *dip; //结构体赋值
}
这两个函数来读写 $inode$,过程见注释
分配 inode
代码语言:c复制uint ialloc(ushort type)
{
uint inum = freeinode ; //分配inode编号
struct dinode din;
bzero(&din, sizeof(din)); //清0
din.type = xshort(type); //文件类型
din.nlink = xshort(1); //硬连接数1
din.size = xint(0); //文件大小0
winode(inum, &din); //写到磁盘
return inum; //返回inode编号
}
这个函数分配一个 $inode$,经过前面那么多篇文件系统的文章,这个函数应该是个小 case 了,只说一点,$freeinode$ 为第一个空闲的 $inode$ 编号,初始值为 1。其他的不多说,过程见注释。
分配块
代码语言:c复制void balloc(int used) //将used前面所有的块的位图置 1
{
uchar buf[BSIZE];
int i;
printf("balloc: first %d blocks have been allocatedn", used);
assert(used < BSIZE*8);
bzero(buf, BSIZE); //清 0
for(i = 0; i < used; i ){
buf[i/8] = buf[i/8] | (0x1 << (i%8)); //位图相应位置1表分配出去
}
printf("balloc: write bitmap block at sector %dn", sb.bmapstart);
wsect(sb.bmapstart, buf); //写回磁盘
}
$xv6$ 里面所有的块都有相应的位图管理,这个函数将块号为 $used$ 之前的所有块的位图置 1。
向文件末尾写数据
代码语言:c复制void iappend(uint inum, void *xp, int n) //将xp指向的数据写到inum指向的文件末尾,写n个字节
{
char *p = (char*)xp;
uint fbn, off, n1;
struct dinode din;
char buf[BSIZE];
uint indirect[NINDIRECT];
uint x;
/***获取 inum 指向的文件最后一个数据块的位置(块号)***/
rinode(inum, &din); //读取第inum个inode
off = xint(din.size); //文件大小,字节数
// printf("append inum %d at off %d sz %dn", inum, off, n);
while(n > 0){
fbn = off / BSIZE; //文件大小,块数,向下取整了,所以不是实际的块数
assert(fbn < MAXFILE); //不能超过支持的最大文件
if(fbn < NDIRECT){ //如果在直接索引范围类
if(xint(din.addrs[fbn]) == 0){ //如果未分配该数据块
din.addrs[fbn] = xint(freeblock ); //分配
}
x = xint(din.addrs[fbn]); //记录该块的地址(块号)
} else { //如果超出直接索引范围,在间接索引范围内
if(xint(din.addrs[NDIRECT]) == 0){ //如果间接索引块未分配
din.addrs[NDIRECT] = xint(freeblock ); //分配
}
rsect(xint(din.addrs[NDIRECT]), (char*)indirect); //读取间接索引块
if(indirect[fbn - NDIRECT] == 0){ //若间接索引块中的条目指向的数据块未分配
indirect[fbn - NDIRECT] = xint(freeblock ); //分配
wsect(xint(din.addrs[NDIRECT]), (char*)indirect); //同步到磁盘
}
x = xint(indirect[fbn-NDIRECT]); //记录该数据块的地址(块号)
}
/**计算要写的字节数***/
n1 = min(n, (fbn 1) * BSIZE - off); //计算一次性最多写的字节数
rsect(x, buf); //读取x扇区
bcopy(p, buf off - (fbn * BSIZE), n1); //计算从哪开始写,bcopy(src, dest, size)
wsect(x, buf); //写回x扇区
n -= n1; //更新还要写的字节数
off = n1; //更新dest位置(文件大小)
p = n1; //更新src位置
}
din.size = xint(off); //更新inode的文件大小属性
winode(inum, &din); //写回inode
}
这个函数向编号为 $inum$ 的 $inode$ 指向的文件末尾写数据,内核里面也有类似的函数,上面也有详细注释了,就不做具体说明。这类函数从哪开始写,写到哪儿,一次性能写多少,变量有些多,有点扰人,不过逻辑不复杂,耐心点应该没什么问题。
文件系统创建
这部分来看文件系统的创建
代码语言:c复制/*******Makefile*********/
./mkfs fs.img README $(UPROGS)
/********mkfs.c**********/
int main(int argc, char *argv[]){
/***********略**************/
if(argc < 2){ //参数小于两个
fprintf(stderr, "Usage: mkfs fs.img files...n");
exit(1); //退出
}
$Makefile$ 中有文件系统的创建命令 ./mkfs fs.img README $(UPROGS)
,这参数明显不可能小于两个,所以如果小于两个,打印错误消息然后退出
assert((BSIZE % sizeof(struct dinode)) == 0); //BSIZE是否是dinode大小整数倍
assert((BSIZE % sizeof(struct dirent)) == 0); //BSIZE是否是dirent大小整数倍
$xv6$ 的文件系统没有处理一些关键结构跨扇区的情况,因为 $BSIZE$ 是 $dinode$,$dirent$ 的整数倍,正常情况下不会出现跨扇区的情况。
打开磁盘文件
代码语言:c复制 fsfd = open(argv[1], O_RDWR|O_CREAT|O_TRUNC, 0666); //打开磁盘文件
if(fsfd < 0){ //如果打开失败
perror(argv[1]);
exit(1);
}
这部分就是打开磁盘文件,因为 $xv6$ 的磁盘对于主机来说就是个普通文件嘛,对文件操作之前肯定要先打开。
初始化超级块
代码语言:c复制 // 1 fs block = 1 disk sector
nmeta = 2 nlog ninodeblocks nbitmap; //元数据大小
nblocks = FSSIZE - nmeta; //数据区大小
sb.size = xint(FSSIZE); //文件系统大小
sb.nblocks = xint(nblocks); //数据区大小
sb.ninodes = xint(NINODES); //inode个数
sb.nlog = xint(nlog); //日志区大小
sb.logstart = xint(2); //日志区起始位置
sb.inodestart = xint(2 nlog); //inode区起始位置
sb.bmapstart = xint(2 nlog ninodeblocks); //位图区起始位置
这一部分配置超级块信息,就是将第一部分指定和计算的各类信息写到超级块
代码语言:c复制 freeblock = nmeta; //第一个能够分配的数据块
for(i = 0; i < FSSIZE; i ) //磁盘清零
wsect(i, zeroes);
memset(buf, 0, sizeof(buf)); //buf清0
memmove(buf, &sb, sizeof(sb)); //移动超级块信息到buf
wsect(1, buf); //将buf写到第一个扇区(第0个扇区是引导快)
这一部分写超级块,超级块位于第 1 个扇区,第 0 个扇区是引导块
根目录
代码语言:c复制 rootino = ialloc(T_DIR); //分配一个inode指向根目录文件
assert(rootino == ROOTINO); //根目录的inode编号是否为 1
这一部分为根目录文件分配 $inode$,根目录的 $inode$ 编号是 1,编号 0 用作判断该 $inode$ 是否空闲
代码语言:c复制 bzero(&de, sizeof(de)); //目录项de清0
de.inum = xshort(rootino); //根目录inode编号
strcpy(de.name, "."); //当前目录 . 目录项
iappend(rootino, &de, sizeof(de)); //向根目录文件末尾写目录项
bzero(&de, sizeof(de)); //目录项de清0
de.inum = xshort(rootino); //根目录inode编号
strcpy(de.name, ".."); //父目录 .. 目录项
iappend(rootino, &de, sizeof(de)); //向根目录文件末尾写目录项
这一部分创建根目录的 .
..
目录项,根目录的当前目录和父目录都是根目录
参数文件
代码语言:c复制 for(i = 2; i < argc; i ){ //从第2个参数可执行文件开始循环(0开始)
assert(index(argv[i], '/') == 0); //原本应是想判断该文件是否在根目录下
if((fd = open(argv[i], 0)) < 0){ //打开该文件
perror(argv[i]);
exit(1);
}
if(argv[i][0] == '_') //如果该文件名以_开头,跳过
argv[i];
inum = ialloc(T_FILE); //分配一个普通文件类型的inode
bzero(&de, sizeof(de)); //目录项清 0
de.inum = xshort(inum); //inode编号
strncpy(de.name, argv[i], DIRSIZ); //copy文件名
iappend(rootino, &de, sizeof(de)); //将该目录项添加到根目录文件末尾
while((cc = read(fd, buf, sizeof(buf))) > 0) //读取参数文件数据到buf
iappend(inum, buf, cc); //将buf写进inode指向的数据区
close(fd); //关闭该参数文件
}
这部分将主机上编译好的可执行文件写进磁盘文件的根目录,首先为每个可执行文件比如说 $cat$ 分配一个 $inode$,然后在根目录下安装目录项,最后将主机上的 $cat$ 这个可执行文件的数据写到 $inode$ 指向的数据区中。再次注意这时的操作都是在主机中进行的,对主机而言的磁盘文件对于 $xv6$ 来说就是磁盘。我初次接触 $xv6$ 的时候始终就有个疑惑,主机上的这些东西怎么跑去 $qemu$ 模拟出来的机器里面去了,在里面执行的各种命令不是在主机里面吗?为什么 $qemu$ 里的 $xv6$ 能使用,原因就在此处了。
代码语言:c复制 // fix size of root inode dir
rinode(rootino, &din); //读取根目录inode
off = xint(din.size); //原根目录大小
off = ((off/BSIZE) 1) * BSIZE; //新根目录大小,直接取整??
din.size = xint(off);
winode(rootino, &din); //写回磁盘
balloc(freeblock); //将分配出去的块相应的位图置1
exit(0); //退出
}
最后这部分更新根目录 $inode$ 信息,另外前面向磁盘写文件不是分配了那么多块出去吗?$xv6$ 里面的块都是有相应的位图标识,所以这里调用 $balloc$ 将第一个空闲块之前的位图全部置 1。因为 $xv6$ 的系统布局就是数据区前面全是不可分配的元数据,需要全部置 1。
这部分就是制作文件系统的过程,文件系统是指就是对分区组织和管理,创建文件系统所需要的元信息,比如说超级块的位置和大小,日志区的位置和大小,$inode$ 区的位置和大小等等,将这些基本信息写入磁盘相应的地方就是所谓的创建文件系统了。
好了本节就这样吧,有什么问题还请批评指正,也欢迎大家来同我讨论交流学习进步。