xv6(14) 文件系统:创建

2023-12-07 16:29:45 浏览数 (1)

文件系统:创建

文件系统的创建在原理上并不复杂,就是创建文件系统所需要的元信息,比如说超级块的位置和大小,日志区的位置和大小,$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),这参数明显不可能小于两个,所以如果小于两个,打印错误消息然后退出

代码语言:c复制
  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$ 区的位置和大小等等,将这些基本信息写入磁盘相应的地方就是所谓的创建文件系统了。

好了本节就这样吧,有什么问题还请批评指正,也欢迎大家来同我讨论交流学习进步。

0 人点赞