【xv6操作系统】文件系统与内存映射及实验设计

文件系统

  • 文件系统相关理论知识结合 【操作系统】文件系统 进行对比学习,也可以在复习文件系统后,或完成修改大文件后复习以下内容会有更深的理解记忆。

xv6文件系统结构

xv6文件系统层次: 从上至下分别为 文件描述符、路径名、目标、索引节点、日志、缓冲区高速缓存、磁盘

  • 磁盘层: 读取和写入virtio硬盘上的块
  • 缓冲区高速缓存层: 缓存磁盘块并同步对它们的访问,确保每次只有一个内核进程可以修改存储在任何特定块中的数据
  • 日志记录层: 允许更高层在一次事务(transaction)中将更新包装到多个块,并确保在遇到崩溃时自动更新这些块(即,所有块都已更新或无更新)
  • 索引结点层: 提供单独的文件,每个文件表示为一个索引结点,其中包含唯一的索引号(i-number)和一些保存文件数据的块。
  • 目录层:每个目录实现为一种特殊的索引结点,其内容是一系列目录项,每个目录项包含一个文件名和索引号。
  • 路径名层: 提供了分层路径名,如/usr/rtm/xv6/fs.c,并通过递归查找来解析它们。
  • 文件描述符层: 使用文件系统接口抽象了许多Unix资源(比如管道、设备、文件等),简化了程序员的工作

xv6文件系统结构

  • 文件系统不使用块0(它保存引导扇区)
  • 块1称为超级块: 它包含有关文件系统的元数据(文件系统大小(以块为单位)、数据块数、索引节点数和日志中的块数)。超级块由一个名为 mkfs 的单独的程序填充,该程序构建初始文件系统
  • 从2开始的块保存日志
  • 日志之后是索引节点,每个块有多个索引节点然后是位图块跟踪正在使用的数据块
  • 其余的块是数据块:每个都要么在位图块中标记为空闲,要么保存文件或目录的内容

缓冲区高速缓存层(Buffer cache)

Buffer cache有两个任务:

  • 缓存常用块,以便不需要从慢速磁盘重新读取它们
  • 同步对磁盘块的访问,以确保磁盘块在内存中只有一个副本,并且一次只有一个内核线程使用该副本

Buffer cache层导出的主接口主要是 breadbwrite

  • bread 获取一个buf,其中包含一个可以在内存中读取或修改的块的副本
  • bwrite 将修改后的缓冲区写入磁盘上的相应块

  内核线程必须通过调用 brelse 释放缓冲区。Buffer cache每个缓冲区使用一个睡眠锁,以确保每个缓冲区(每个磁盘块)每次只被一个线程使用;bread 返回一个上锁的缓冲区,brelse 释放该锁。

  Buffer cache中保存磁盘块的缓冲区数量固定,这意味着如果文件系统请求还未存放在缓存中的块,Buffer cache必须回收当前保存其他块内容的缓冲区。Buffer cache为新块回收最近使用最少的缓冲区。这样做的原因是认为最近使用最少的缓冲区是最不可能近期再次使用的缓冲区。

  Buffer cache是以 双链表 表示的缓冲区。对Buffercache的所有其他访问都通过 bcache.head 引用链表,而不是 buf 数组。

  缓冲区有两个与之关联的状态字段。字段 valid 表示缓冲区是否包含块的副本。字段 disk 表示缓冲区内容是否已交给磁盘,这可能会更改缓冲区(例如,将数据从磁盘写入 data)

索引节点 inode

inode是一个64字节的数据结构,包含:

  • type字段: 文件类型,表明inode是文件还是目录

  • nlink字段:,也就是link计数器,用来跟踪究竟有多少文件名指向了当前的inode

  • size字段: 文件大小。表明了文件数据有多少个字节

  • 12个直接块编号,这些直接块编号直接指向文件的前 12 个磁盘块。当文件较小时,直接块可以满足文件存储的需求,每个块的大小在 xv6 中是 1KB,因此使用 12 个直接块意味着最多可以存储 12*1KB=12KB 的数据

  • 1个间接块编号指向一个间接块,间接块本身是一个磁盘块,其中包含了 256 个条目,每个条目存储一个数据块编号,这些编号依次指向文件的数据块


因此XV6中最大文件尺寸:(256+12)*1KB=268KB,1个block是1KB,1个块编号是4字节,1024 / 4 = 256

inode有两种含义: 磁盘上的数据结构inode,以及内存中的inode,包含磁盘inode的副本以及内核中所需的额外信息

  • 磁盘inode(struct dinode):inode 在磁盘上被存储为固定大小的结构,并被连续存储在一个区域中。struct dinode (kernel/fs.h)是磁盘 inode 的数据结构,字段 type 表示 inode 类型(文件、目录、特殊文件),字段 nlink 记录引用该 inode 的目录条目的数量,减为零时释放该 inode 和其占用的数据块,字段 size 记录文件内容的字节数,字段 addrs 为一个数组,记录了保存文件内容的磁盘块号。

  • 内存inode: 当文件被访问(如打开或修改)时,系统会将磁盘上的 inode 加载到内存中,并在内存中维护它的一个副本。 struct inode(kernel/file.h)是磁盘 inode 的内存副本,并包含额外字段来支持内核的操作。只有当某个进程需要访问文件时,系统才会将磁盘inode 加载到内存,并创建 struct inode 。ref 字段统计引用内存中inode的C指针的数量,如果引用计数降至零,内核将从内存中丢弃该inode。iget 和 iput函数分别获取和释放指向 inode 的指针,修改引用计数。

内存inode中的锁机制:

  • icache.lock:保护全局 inode 缓存(icache),确保每个inode 在缓存中最多只有一个副本,保护 inode中 ref 字段的正确性,即记录指向该 inode 的内存指针的正确数量
  • inode的 struct sleeplock lock 字段:每个 inode 的独立锁,用于对该 inode 的独占访问,确保对 inode 的元数据字段和其内容块的修改是线程安全的
  • inode的 ref 字段:管理内存 inode 的生命周期,记录指向该 inode 的C 指针数量。如果 ref 减少到 0,表示内存中已没有任何代码持有该 inode 的引用,缓存可以移除该inode,但 inode 在磁盘上仍然存在
  • inode的 nlink 字段:管理磁盘 inode 的生命周期,记录指向该inode的目录项(创建硬链接)。当nlink 减少到 0,表示没有任何目录项指向该 inode,如果 inode 的内容已经不在内存中(即 ref ==0),该 inode 将被完全释放,包括inode 本身和它占用的所有数据块

实验1:大文件

  • 添加二级链接块,实现超大文件存储

1. 修改索引数据类型

1.1 原本的XV6文件系统中,inode的结构如下:

//kernel/fs.h
#define NDIRECT 12
#define NINDIRECT (BSIZE / sizeof(uint))
#define MAXFILE (NDIRECT + NINDIRECT)

// On-disk inode structure
struct dinode {
  short type;           	// 文件类型
  short major;          	// 主设备号
  short minor;          	// 次设备号
  short nlink;          	// 链接数
  uint size;            	// 文件大小
  uint addrs[NDIRECT+1];   // 数据块地址
};
  • addrs 字段用来索引记录数据的所在盘块号。
  • 每个文件所占用的 前 12 个盘块(add[0-11]) 的盘块号是直接记录在 inode 中的(每个盘块 1024 字节),前 12 KB 数据,都可以通过访问 inode 直接得到盘块号
  • 大于 12 个盘块的部分,会分配一个额外的一级索引表(addr[11],1024Byte),用于存储这部分数据的所在盘块号。一个索引项是4字节,一级索引表可以包含 BSIZE(1024) / 4 = 256 个盘块号
  • 加上 inode 中的 12 个盘块号,一个文件最多可以使用 12+256=268 个盘块,也就是 268KB

1.2 修改 inode 的结构:,修改成11个直接块,1个一级间接块,1个二级间接块。注意这是磁盘inode的结构体:

//kernel/fs.h
#define NDIRECT 11                          // 间接块
#define NINDIRECT (BSIZE / sizeof(uint))    // 一级索引块 256
#define MAXFILE (NDIRECT + NINDIRECT + NINDIRECT * NINDIRECT) // 11 + 256 + 256 * 256 = 65804

// On-disk inode structure
struct dinode {
  short type;           	// 文件类型
  short major;          	// 主设备号
  short minor;          	// 次设备号
  short nlink;          	// 链接数
  uint size;            	// 文件大小
  // 0- 10:直接索引    11:间接索引    12: 二级间接索引
  uint addrs[NDIRECT+2];   // 数据块地址
};

修改对比图:

1.3:还需要修改内存中inode的副本结构体, 直接块腾一个位置出来给二级间接块,kernel/file.h

2. 修改逻辑块号与物理块号的映射

2.1 原xv6文件系统中逻辑块号与物理块号的映射关系: 先判断逻辑块号与直接映射的物理块号来确定是否为直接映射,若否则建立间接映射

// Return the disk block address of the nth block in inode ip.
// If there is no such block, bmap allocates one.
// kernel/fs.c
static uint bmap(struct inode *ip, uint bn)
{
	uint addr, *a;
	struct buf *bp;
	
	// bt小于直接块  直接映射
	if(bn < NDIRECT){
		if((addr = ip->addrs[bn]) == 0)
			ip->addrs[bn] = addr = balloc(ip->dev);
		return addr;
	}
	bn -= NDIRECT;
	
	// 间接块   减去直接块的数量得到间接块的逻辑号
	if(bn < NINDIRECT)
	{
		// 间接块还没分完 分配一个
		// Load indirect block, allocating if necessary.
		if((addr = ip->addrs[NDIRECT]) == 0) 	// 0~NDIRECT-1是直接块,NDIRECT 是间接块, NDIRECT是二级间接块
			ip->addrs[NDIRECT] = addr = balloc(ip->dev);	// 可以映射256个块
			
		// 获取刚分配的缓存块,检查bn对应的块,如果为则没有分配,建立映射  
		bp = bread(ip->dev, addr);
		a = (uint*)bp->data;
		if((addr = a[bn]) == 0){
			a[bn] = addr = balloc(ip->dev);
			log_write(bp);
		}
		 
		// 建立映射结束,释放掉缓存块(可以通过ip->dev和bn找到这个块了)  
		brelse(bp);
		return addr;
	}
	
	panic("bmap: out of range");
}

2.2 理解了间接映射原理,增加的二级映射就很好修改了: 先判断是否为直接映射,若否则判断是否为间接映射,若否则进入二级间接映射,注意二级间接映射要寻址两次(相当于确定一个数值在二维矩阵中的位置)

// Return the disk block address of the nth block in inode ip.
// If there is no such block, bmap allocates one.
static uint bmap(struct inode *ip, uint bn)
{
	uint addr, *a;
	struct buf *bp;
	
	// bt小于直接块  直接映射
	if(bn < NDIRECT){
		if((addr = ip->addrs[bn]) == 0)
			ip->addrs[bn] = addr = balloc(ip->dev);
		return addr;
	}
	bn -= NDIRECT;
	
	// 间接块   减去直接块的数量得到间接块的逻辑号
	if(bn < NINDIRECT)
	{
		// 间接块还没分完 分配一个
		// Load indirect block, allocating if necessary.
		if((addr = ip->addrs[NDIRECT]) == 0)            // 0~NDIRECT-1是直接块,NDIRECT 是间接块, NDIRECT是二级间接块
			ip->addrs[NDIRECT] = addr = balloc(ip->dev);  // 可以映射256个块
		
		// 获取刚分配的缓存块,检查bn对应的块,如果为则没有分配,建立映射  
		bp = bread(ip->dev, addr);
		a = (uint*)bp->data;
		if((addr = a[bn]) == 0){
			a[bn] = addr = balloc(ip->dev);
			log_write(bp);
		}
	
		// 建立映射结束,释放掉缓存块(可以通过ip->dev和bn找到这个块了)  
		brelse(bp);
		return addr;
	}
	
	bn -= NINDIRECT;
	// 减去间接块,减去一级间接块的数量得到二级件结块的逻辑块号
	if(bn < NINDIRECT * NINDIRECT)
	{
		// 二级间接块还没分完 分配一个
		// Load indirect block, allocating if necessary.
		if((addr = ip->addrs[NDIRECT + 1]) == 0)              // 0~NDIRECT-1是直接块,NDIRECT 是间接块, NDIRECT是二级间接块
			ip->addrs[NDIRECT] = addr = balloc(ip->dev);        // 可以映射256个块
		bp = bread(ip->dev, addr);
		a = (uint*)bp->data;
	
		// bn处在二级索引中间级的第 bn/NINDIRECT 个索引处
		if((addr = a[bn / NDIRECT]) == 0){
			a[bn] = addr = balloc(ip->dev);
			log_write(bp);
		}
		brelse(bp);
	
		// 最后一级索引 相当于确定了行,再确定列
		bn %= NDIRECT;
		bp = bread(ip->dev, addr);
		a = (uint*)bp->data;
		if((addr = a[bn]) == 0){
			a[bn] = addr = balloc(ip->dev);
			log_write(bp);
		}
		
		brelse(bp);
		return addr;
	}
	
	panic("bmap: out of range");
}

3. 释放节点映射的所有数据块

3.1 原xv6文件系统中释放该inode节点映射的所有数据块: 原理类似于分配,直接循环释放所有数据块

// kernel/fs.c
// Truncate inode (discard contents).
// Caller must hold ip->lock.
void
itrunc(struct inode *ip)
{
  int i, j;
  struct buf *bp;
  uint *a;

  // 释放直接块的映射
  for(i = 0; i < NDIRECT; i++){
    if(ip->addrs[i]){
      bfree(ip->dev, ip->addrs[i]);
      ip->addrs[i] = 0;
    }
  }

  // 释放一级间接块的映射
  if(ip->addrs[NDIRECT]){
    bp = bread(ip->dev, ip->addrs[NDIRECT]);
    a = (uint*)bp->data;
    for(j = 0; j < NINDIRECT; j++){
      if(a[j])
        bfree(ip->dev, a[j]);
    }
    brelse(bp);
    bfree(ip->dev, ip->addrs[NDIRECT]);
    ip->addrs[NDIRECT] = 0;
  }

  ip->size = 0;
  iupdate(ip);
}

3.2 添加释放二级间接块映射的所有数据块: 原理类似于释放一级间接块,循环遍历释放即可

// kernel/fs.c
// Truncate inode (discard contents).
// Caller must hold ip->lock.
void itrunc(struct inode *ip)
{
	int i, j;
	struct buf *bp;
	uint *a;
	
	// 释放直接块的映射
	for(i = 0; i < NDIRECT; i++){
		if(ip->addrs[i]){
			bfree(ip->dev, ip->addrs[i]);
			ip->addrs[i] = 0;
		}
	}
	
	// 释放一级间接块的映射
	if(ip->addrs[NDIRECT])
	{
		bp = bread(ip->dev, ip->addrs[NDIRECT]);
		a = (uint*)bp->data;
		for(j = 0; j < NINDIRECT; j++){
			if(a[j])
			bfree(ip->dev, a[j]);
		}
		brelse(bp);
		bfree(ip->dev, ip->addrs[NDIRECT]);
		ip->addrs[NDIRECT] = 0;
	}
	
	
	// 添加释放二级间接块的映射
	if(ip->addrs[NDIRECT + 1])
	{
		bp = bread(ip->dev, ip->addrs[NDIRECT + 1]);
		a = (uint*)bp->data;
		for (int i = 0; i < NINDIRECT; i++)
		{
			if(a[i])
			{
				struct buf* bp2 = bread(ip->dev, a[i]);
				uint* a2 = (uint*)bp2->data;
				for(j = 0; j < NINDIRECT; j++){
					if(a2[j])
					bfree(ip->dev, a2[j]);
				}
				brelse(bp2);
				bfree(ip->dev, a[i]);
			}
		}
		brelse(bp);
		bfree(ip->dev, ip->addrs[NDIRECT + 1]);
		ip->addrs[NDIRECT + 1] = 0;
	}
	
	ip->size = 0;
	iupdate(ip);
}

实验2:软链接

1. 添加系统调用与软软连接相关标志位

  • 该实验需要实现软链接机制,实现一个软链接系统调用函数symlink,系统调用过程参考系统调用与traps机制章节,这里已轻车熟路,不再赘述。

  • 根据提示,添加新的文件类型 T_SYMLINK ,表示文件为软链接文件:

    // kernel/stat.h
    #define T_DIR     1   // Directory
    #define T_FILE    2   // File
    #define T_DEVICE  3   // Device
    #define T_SYMLINK  4   // 软链接
    
  • 添加新的文件标志位 O_NOFOLLOW,该标志可用于 open 系统调用

    // kernel/fcntl.h
    #define O_RDONLY  0x000
    #define O_WRONLY  0x001
    #define O_RDWR    0x002
    #define O_CREATE  0x200
    #define O_TRUNC   0x400
    #define O_NOFOLLOW   0x800
    

2. sys_symlink系统调用具体实现

  实现具体的 sys_symlink: 使用 create 创建一个加了锁的指向源文件的inode,再将链接目标文件地址通过 writei 写入inode,操作完之后使用 iunlockput 解锁创建的inode并使其引用计数-1,表示创建软链接的操作结束,释放锁。

//kernel/sysfile.c
uint64 sys_symlink(void)
{
  char src[MAXPATH];
  char dst[MAXPATH];
  struct inode* ip;

  if(argstr(0, src, MAXPATH) < 0 || argstr(1, dst, MAXPATH) < 0)
    return -1;

  begin_op();
  
  // 创建一个新的inode,类型为T SYMLINK,指向path文件
  ip = create(dst, T_SYMLINK, 0, 0);
  if(ip == 0) {
    end_op();
    return -1;
  }
  
  // 数据进入新的inode
  if(writei(ip, 0, (uint64)src, 0, MAXPATH) != MAXPATH){
    iunlockput(ip);
    end_op();
    return -1;
  } 

  iunlockput(ip);
  end_op();
  return 0;
}

   create 在文件系统中创建一个新的文件,设置其类型是 T_SYMLINK, 使用一个新的inode关联这个文件,inode 的初始状态为空,没有任何数据。create 还在目录中为这个新文件创建了一个目录项,使得路径名 path 可以找到这个inode。使用 writei 函数将符号链接的目标路径(dst)作为数据,写入到新创建的 inode 的数据块中,文件的内容就变成了目标路径 dst。

3. sys_open打开文件判断是否为软链接

  修改 sys_open 函数,通过循环调用 namei 获取对应的inode,如果指向的仍是软链接,就继续循环读取,直到找到真正指向的文件,或者超过了一定的链接深度

// kernel/sysfile.c
uint64
sys_open(void)
{
	...
	if(omode & O_CREATE){
		ip = create(path, T_FILE, 0, 0);
		if(ip == 0){
			end_op();
			return -1;
		}
	} 
	// 软链接
	else 
	{
		int symlink_depth = 0;
		while (1)
		{
			// 解析路径,获取对应的inode
			if((ip = namei(path)) == 0){
				end_op();
				return -1;
			}
		
			ilock(ip);
			//如果当前指向的仍是软链接,则继续循环
			if(ip->type == T_SYMLINK && (omode & O_NOFOLLOW) == 0)
			{
				// 链接层数超过10层就退出
				if(++symlink_depth > 10) {
					iunlockput(ip);
					end_op();
					return -1;
				}
				
				// 读取链接的目标路径
				if(readi(ip, 0, (uint64)path, 0, MAXPATH) < 0) {
					iunlockput(ip);
					end_op();
					return -1;
				}
				iunlockput(ip);
			}
			else
				break;
		}
		
		if(ip->type == T_DIR && omode != O_RDONLY){
			iunlockput(ip);
			end_op();
			return -1;
		}
	}
	
	if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){
		iunlockput(ip);
		end_op();
		return -1;
	}
	...
}
### xv6 文件系统理论概念 xv6 文件系统设计简单而有效,旨在帮助学生理解掌握基本的操作系统文件管理机制。该文件系统支持创建、删除、读取写入文件以及目录结构的维护。 #### 文件系统概述 文件系统操作系统的一部分,负责管理存储文件数据。在 xv6 中,文件系统基于 FAT (File Allocation Table) 类似的设计理念[^1]。这种设计理念使得文件系统的实现既直观又易于理解。 #### 数据结构 为了有效地管理磁盘上的文件xv6 使用了几种核心的数据结构- **超级块 (Superblock)**:记录整个文件系统的元信息,如可用 inode block 数量等。 - **i节点 (Inode)**:每个 i 节点对应一个特定的文件或目录,并保存有关此对象的关键属性(大小、权限等),还包括指向实际数据块位置的信息。 - **索引表 (Directory Entry)**:用于表示单个条目(即文件名到其相应 i-node 编号之间的映射关系)。这些条目被组织成树形结构来形成完整的路径层次。 ```c struct superblock { uint size; // Size of file system image (blocks) uint nblocks; // Number of data blocks uint ninodes; // Number of inodes. }; struct dinode { short type; // File type short major; // Major device number (T_DEV only) short minor; // Minor device number (T_DEV only) short nlink; // Number of links to inode in file system uint size; // Size of file (bytes) uint addrs[N]; // Data block addresses }; ``` #### 操作流程 当执行诸如 `open` 或者 `create` 这样的系统调用时,内核会按照如下方式处理请求: 1. 查找指定路径对应的 i-node; 2. 如果需要,则分配新的资源给新建立的对象; 3. 更新相应的元数据并同步至磁盘上持久化存储区域; 4. 返回描述符或其他形式的结果给应用程序层。 通过这种方式,xv6 提供了一个简化版却功能完备的 POSIX 风格 API 接口集,使开发者能够专注于学习底层原理而不必担心复杂度过高的细节问题[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不会编程的小江江

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值