引言
MIT 6.S081 2020 操作系统
本文为MIT 6.S081课程第八章教材内容翻译加整理。
本课程前置知识主要涉及:
- C语言(建议阅读C程序语言设计—第二版)
- RISC-V汇编
- 推荐阅读: 程序员的自我修养-装载,链接与库
索引结点层
术语inode(即索引结点)可以具有两种相关含义之一。它可能是指包含文件大小和数据块编号列表的磁盘上的数据结构。或者“inode”可能指内存中的inode,它包含磁盘上inode的副本以及内核中所需的额外信息。
磁盘上的inode都被打包到一个称为inode块的连续磁盘区域中。每个inode的大小都相同,因此在给定数字n的情况下,很容易在磁盘上找到第n个inode。事实上,这个编号n,称为inode number或i-number,是在具体实现中标识inode的方式。
磁盘上的inode由struct dinode
(kernel/fs.h:32)定义:
- 字段
type
区分文件、目录和特殊文件(设备)。type
为零表示磁盘inode是空闲的。 - 字段
nlink
统计引用此inode的目录条目数,以便识别何时应释放磁盘上的inode及其数据块。 - 字段
size
记录文件中内容的字节数。 -
addrs
数组记录保存文件内容的磁盘块的块号。
// 直接块数量
#define NDIRECT 12
// 间接块数量
#define NINDIRECT (BSIZE / sizeof(uint))
// 最大的文件大小
#define MAXFILE (NDIRECT + NINDIRECT)
// On-disk inode structure
struct dinode {
short type; // File type
short major; // Major device number (T_DEVICE only)
short minor; // Minor device number (T_DEVICE only)
short nlink; // Number of links to inode in file system
uint size; // Size of file (bytes)
uint addrs[NDIRECT+1]; // Data block addresses
};
内核将活动的inode集合保存在内存中;struct inode
(kernel/file.h:17)是磁盘上struct dinode
的内存副本。只有当有C指针引用某个inode时,内核才会在内存中存储该inode。ref
字段统计引用内存中inode的C指针的数量,如果引用计数降至零,内核将从内存中丢弃该inode:
// in-memory copy of an inode
struct inode {
uint dev; // Device number
uint inum; // Inode number
int ref; // Reference count
struct sleeplock lock; // protects everything below here
int valid; // inode has been read from disk?
short type; // copy of disk inode
short major;
short minor;
short nlink;
uint size;
uint addrs[NDIRECT+1];
};
iget
和iput
函数分别获取和释放指向inode的指针,修改引用计数。指向inode的指针可以来自文件描述符、当前工作目录和如exec
的瞬态内核代码。
xv6的inode代码中有四种锁或类似锁的机制。icache.lock
保护以下两个不变量:
- inode最多在缓存中出现一次;
- 缓存inode的
ref
字段记录指向缓存inode的内存指针数量。
struct {
struct spinlock lock;
struct inode inode[NINODE];
} icache;
每个内存中的inode都有一个包含睡眠锁的lock
字段,它确保以独占方式访问inode的字段(如文件长度)以及inode的文件或目录内容块。如果inode的ref
大于零,则会导致系统在cache中维护inode,而不会对其他inode重用此缓存项。
最后,每个inode都包含一个nlink
字段(在磁盘上,如果已缓存则复制到内存中),该字段统计引用文件的目录项的数量;如果inode的链接计数大于零,xv6将不会释放inode。
iget()
返回的struct inode
指针在相应的iput()
调用之前保证有效:
- inode不会被删除,指针引用的内存也不会被其他inode重用。
iget()
提供对inode的非独占访问,因此可以有许多指向同一inode的指针。文件系统代码的许多部分都依赖于iget()
的这种行为,既可以保存对inode的长期引用(如打开的文件和当前目录),也可以防止争用,同时避免操纵多个inode(如路径名查找)的代码产生死锁。
iget
返回的struct inode
可能没有任何有用的内容。为了确保它保存磁盘inode的副本,代码必须调用ilock
。这将锁定inode(以便没有其他进程可以对其进行ilock
),并从磁盘读取尚未读取的inode。iunlock
释放inode上的锁。将inode指针的获取与锁定分离有助于在某些情况下避免死锁,例如在目录查找期间。多个进程可以持有指向iget
返回的inode的C指针,但一次只能有一个进程锁定inode。
inode缓存只缓存内核代码或数据结构持有C指针的inode。它的主要工作实际上是同步多个进程的访问;缓存是次要的。如果经常使用inode,在inode缓存不保留它的情况下buffer cache可能会将其保留在内存中。inode缓存是直写的,这意味着修改已缓存inode的代码必须立即使用iupdate
将其写入磁盘。
一旦内存中的inode被修改,必须立即将修改同步到磁盘上的dinode中
代码:Inodes
为了分配新的inode(例如,在创建文件时),xv6调用ialloc
(kernel/fs.c:196)。Ialloc
类似于balloc
:它一次一个块地遍历磁盘上的索引节点结构体,查找标记为空闲的一个。当它找到一个时,它通过将新type
写入磁盘来声明它,然后末尾通过调用iget
(kernel/fs.c:210)从inode缓存返回一个条目。ialloc
的正确操作取决于这样一个事实:一次只有一个进程可以保存对bp
的引用:ialloc
可以确保其他进程不会同时看到inode可用并尝试声明它。
// Inodes per block. --> 一个block可以塞下多少个inode信息
#define IPB (BSIZE / sizeof(struct dinode))
// Block containing inode i --> 计算当前inode所在的block编号
#define IBLOCK(i, sb) ((i) / IPB + sb.inodestart)
// Allocate an inode on device dev.
// Mark it as allocated by giving it type type.
// Returns an unlocked but allocated and referenced inode.
struct inode*
ialloc(uint dev, short type)
{
int inum;
struct buf *bp;
struct dinode *dip;
// 遍历所有dinodes
for(inum = 1; inum < sb.ninodes; inum++){
// 获取当前dinode所在的block
bp = bread(dev, IBLOCK(inum, sb));
// 获取block上存储的dinode结构体信息
dip = (struct dinode*)bp->data + inum%IPB;
// 空闲inode
if(dip->type == 0){ // a free inode
// 清空当前dip结构体数据
memset(dip, 0, sizeof(*dip));
// 设置当前inode的type
dip->type = type;
// 当前block块标记为脏块加入log block日志记录数组中
log_write(bp); // mark it allocated on the disk
brelse(bp);
// 返回当前dinode在inode cache的映射
return iget(dev, inum);
}
brelse(bp);
}
panic("ialloc: no inodes");
}
Iget
(kernel/fs.c:243)在inode缓存中查找具有所需设备和inode编号的活动条目(ip->ref > 0
)。如果找到一个,它将返回对该incode的新引用(kernel/fs.c:252-256)。在iget
扫描时,它会记录第一个空槽(kernel/fs.c:257-258)的位置,如果需要分配缓存项,它会使用这个槽。
// Find the inode with number inum on device dev
// and return the in-memory copy. Does not lock
// the inode and does not read it from disk.
static struct inode*
iget(uint dev, uint inum)
{
struct inode *ip, *empty;
acquire(&icache.lock);
// Is the inode already cached?
empty = 0;
// 检查当前inode cache中是否已经缓存当前inode,如果遍历完发现没有缓存
// 那么empty会拿到一个空的entry用于存放当前我们要查找的inode
for(ip = &icache.inode[0]; ip < &icache.inode[NINODE]; ip++){
if(ip->ref > 0 && ip->dev == dev && ip->inum == inum){
ip->ref++;
release(&icache.lock);
return ip;
}
// 记录第一个出现的空槽
if(empty == 0 && ip->ref == 0) // Remember empty slot.
empty = ip;
}
// Recycle an inode cache entry.
// 说明inode cache爆满--抛出异常
if(empty == 0)
panic("iget: no inodes");
// 设置valid字段为0,表示用到的时候需要从磁盘加载inode的具体数据
ip = empty;
ip->dev = dev;
ip->inum = inum;
ip->ref = 1;
ip->valid = 0;
release(&icache.lock);
return ip;
}
在读取或写入inode的元数据或内容之前,代码必须使用ilock
锁定inode。Ilock
(kernel/fs.c:289)为此使用睡眠锁。一旦ilock
以独占方式访问inode,它将根据需要从磁盘(更可能是buffer cache)读取inode。函数iunlock
(kernel/fs.c:317)释放睡眠锁,这可能会导致任何睡眠进程被唤醒。
// Lock the given inode.
// Reads the inode from disk if necessary.
void
ilock(struct inode *ip)
{
struct buf *bp;
struct dinode *dip;
// inode状态异常
if(ip == 0 || ip->ref < 1)
panic("ilock");
acquiresleep(&ip->lock);
// 如果当前inode状态为无效,那么先从磁盘加载该inode信息
if(ip->valid == 0){
// 从磁盘读取该inode所在的block
bp = bread(ip->dev, IBLOCK(ip->inum, sb));
// 获取磁盘上的dinode信息,赋值给inode
dip = (struct dinode*)bp->data + ip->inum%IPB;
ip->type = dip->type;
ip->major = dip->major;
ip->minor = dip->minor;
ip->nlink = dip->nlink;
ip->size = dip->size;
memmove(ip->addrs, dip->addrs, sizeof(ip->addrs));
brelse(bp);
// 设置当前inode为有效状态
ip->valid = 1;
if(ip->type == 0)
panic("ilock: no type");
}
}
Iput
(kernel/fs.c:333)通过减少引用计数(kernel/fs.c:356)释放指向inode的C指针。如果这是最后一次引用,inode缓存中该inode的槽现在将是空闲的,可以重用于其他inode。
如果iput
发现没有指向inode的C指针引用,并且inode没有指向它的链接(发生于无目录),则必须释放inode及其数据块。Iput
调用itrunc
将文件截断为零字节,释放数据块;将索引节点类型设置为0(未分配);并将inode写入磁盘(kernel/fs.c:338)。
// Drop a reference to an in-memory inode.
// If that was the last reference, the inode cache entry can
// be recycled.
// If that was the last reference and the inode has no links
// to it, free the inode (and its content) on disk.
// All calls to iput() must be inside a transaction in
// case it has to free the inode.
//.
void
iput(struct inode *ip)
{
acquire(&icache.lock);
// 如果当前Inode的引用计数为1,并且inode有效,且nlink数为0,则释放当前inode,及其绑定的data block
if(ip->ref == 1 && ip->valid && ip->nlink == 0){
// inode has no links and no other references: truncate and free.
// ip->ref == 1 means no other process can have ip locked,
// so this acquiresleep() won't block (or deadlock).
acquiresleep(&ip->lock);
release(&icache.lock);
// 释放数据块
itrunc(ip);
ip->type = 0;
iupdate(ip);
ip->valid = 0;
releasesleep(&ip->lock);
acquire(&icache.lock);
}
ip->ref--;
release(&icache.lock);
}
// Truncate inode (discard contents).
// Caller must hold ip->lock.
// 释放当前Inode绑定的所有data block
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])
// 修改位图block信息即可
bfree(ip->dev, a[j]);
}
brelse(bp);
// 释放完所有间接块,再释放存放间接块指针的当前block
bfree(ip->dev, ip->addrs[NDIRECT]);
ip->addrs[NDIRECT] = 0;
}
ip->size = 0;
iupdate(ip);
}
// Copy a modified in-memory inode to disk.
// Must be called after every change to an ip->xxx field
// that lives on disk, since i-node cache is write-through.
// Caller must hold ip->lock.
// 将内存中修改后的inode信息持久化到磁盘中存储
void
iupdate(struct inode *ip)
{
struct buf *bp;
struct dinode *dip;
// 读取当前inode对应的block,将内存中inode信息赋值给磁盘上dinode结构体
bp = bread(ip->dev, IBLOCK(ip->inum, sb));
dip = (struct dinode*)bp->data + ip->inum%IPB;
dip->type = ip->type;
dip->major = ip->major;
dip->minor = ip->minor;
dip->nlink = ip->nlink;
dip->size = ip->size;
memmove(dip->addrs, ip->addrs, sizeof(ip->addrs));
// 记录当前修改后的block到日志中去
log_write(bp);
brelse(bp);
}
iput
中释放inode的锁定协议值得仔细研究:
- 一个危险是并发线程可能正在
ilock
中等待使用该inode(例如,读取文件或列出目录),并且不会做好该inode已不再被分配的准备。这不可能发生,因为如果缓存的inode没有链接,并且ip->ref
为1,那么系统调用就无法获取指向该inode的指针。那一个引用是调用iput
的线程所拥有的引用。的确,iput
在icache.lock
的临界区域之外检查引用计数是否为1,但此时已知链接计数为零,因此没有线程会尝试获取新引用。
iput用于释放当前inode,释放条件是引用计数为1,并且link链接数为0,因为链接数为0,所以此时不可能存在其他线程等待使用该inode.
- 另一个主要危险是,对
ialloc
的并发调用可能会选择iput
正在释放的同一个inode。这只能在iupdate
写入磁盘以使inode的type
为零后发生。这个争用是良性的:分配线程将客气地等待获取inode的睡眠锁,然后再读取或写入inode,此时iput
已完成。
iput()
可以写入磁盘。这意味着任何使用文件系统的系统调用都可能写入磁盘,因为系统调用可能是最后一个引用该文件的系统调用。即使像read()
这样看起来是只读的调用,也可能最终调用iput()
。这反过来意味着,即使是只读系统调用,如果它们使用文件系统,也必须在事务中进行包装。
iput()
和崩溃之间存在一种具有挑战性的交互。iput()
不会在文件的链接计数降至零时立即截断文件,因为某些进程可能仍在内存中保留对inode的引用:
- 进程可能仍在读取和写入该文件,因为它已成功打开该文件。
虽然某个inode的link数为0了,但是考虑到ref数大于1,可能会存在其他进程通过文件描述符进行数据读取
但是,如果在最后一个进程关闭该文件的文件描述符之前发生崩溃,则该文件将被标记为已在磁盘上分配,但没有目录项指向它。
当我们进行link操作时,会在当前文件的父目录下创建一个新的目录项<新文件名,inode>,因此如果最后一个进程unlink这个文件后,但是在iput该inode时奔溃,会导致文件已分配(bitmap还未修改),但是没有目录项指向他。
文件系统以两种方式之一处理这种情况。
- 简单的解决方案用于恢复时:重新启动后,文件系统会扫描整个文件系统,以查找标记为已分配但没有指向它们的目录项的文件。如果存在任何此类文件,接下来可以将其释放。
- 第二种解决方案不需要扫描文件系统。在此解决方案中,文件系统在磁盘(例如在超级块中)上记录链接计数降至零但引用计数不为零的文件的i-number。如果文件系统在其引用计数达到0时删除该文件,则会通过从列表中删除该inode来更新磁盘列表。恢复时,文件系统将释放列表中的任何文件。
Xv6没有实现这两种解决方案,这意味着inode可能被标记为已在磁盘上分配,即使它们不再使用。这意味着随着时间的推移,xv6可能会面临磁盘空间不足的风险。
代码: Inode包含内容
磁盘上的inode结构体struct dinode
包含一个size
和一个块号数组(见图8.3)。inode数据可以在dinode
的addrs
数组列出的块中找到。前面的NDIRECT
个数据块被列在数组中的前NDIRECT
个元素中;这些块称为直接块(direct blocks)。接下来的NINDIRECT
个数据块不在inode中列出,而是在称为间接块(indirect block)的数据块中列出。addrs
数组中的最后一个元素给出了间接块的地址。因此,可以从inode中列出的块加载文件的前12 kB(NDIRECT x BSIZE
)字节,而只有在查阅间接块后才能加载下一个256 kB(NINDIRECT x BSIZE
)字节。这是一个很好的磁盘表示,但对于客户端来说较复杂。函数bmap
管理这种表示,以便实现我们将很快看到的如readi
和writei
这样的更高级例程。bmap(struct inode *ip, uint bn)
返回索引结点ip
的第bn
个数据块的磁盘块号。如果ip
还没有这样的块,bmap
会分配一个。
函数bmap
(kernel/fs.c:378)从简单的情况开始:前面的NDIRECT
个块在inode本身中列出(kernel/fs.c:383-387)中。下面NINDIRECT
个块在ip->addrs[NDIRECT]
的间接块中列出。Bmap
读取间接块(kernel/fs.c:394),然后从块内的正确位置(kernel/fs.c:395)读取块号。如果块号超过NDIRECT+NINDIRECT
,则bmap
调用panic
崩溃;writei
包含防止这种情况发生的检查(kernel/fs.c:490)。
Bmap
根据需要分配块。ip->addrs[]
或间接块中条目为零表示未分配块。当bmap
遇到零时,它会用按需分配的新块(kernel/fs.c:384-385)(kernel/fs.c:392-393)替换它们。
// Return the disk block address of the nth block in inode ip.
// If there is no such block, bmap allocates one.
// 返回inode中第n个block在磁盘中的块号,如果还没分配就进行分配
static uint
bmap(struct inode *ip, uint bn)
{
uint addr, *a;
struct buf *bp;
// 如果第n个block属于直接块范畴
if(bn < NDIRECT){
// 该block还未分配空间
if((addr = ip->addrs[bn]) == 0)
// 分配一个新的磁盘块,并返回block number
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)
// 分配新的磁盘块,并返回对应的block number
ip->addrs[NDIRECT] = addr = balloc(ip->dev);
// 从磁盘读取该block
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
// 查看间接块第n个条目对应的block是否已经分配了
// 如果分配了,那么就进行分配
if((addr = a[bn]) == 0){
a[bn] = addr = balloc(ip->dev);
// 因为修改当前间接块的内容,所以需要将当前block记录到日志中去
log_write(bp);
}
brelse(bp);
return addr;
}
panic("bmap: out of range");
}
itrunc
释放文件的块,将inode的size
重置为零。Itrunc
(kernel/fs.c:410)首先释放直接块(kernel/fs.c:416-421),然后释放间接块中列出的块(kernel/fs.c:426-429),最后释放间接块本身(kernel/fs.c:431-432)。
// 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;
// 更新inode块到磁盘
iupdate(ip);
}
Bmap
使readi
和writei
很容易获取inode的数据。Readi
(kernel/fs.c:456)首先确保偏移量和计数不超过文件的末尾。开始于超过文件末尾的地方读取将返回错误(kernel/fs.c:461-462),而从文件末尾开始或穿过文件末尾的读取返回的字节数少于请求的字节数(kernel/fs.c:463-464)。主循环处理文件的每个块,将数据从缓冲区复制到dst
(kernel/fs.c:466-474)。
// Read data from inode.
// Caller must hold ip->lock.
// If user_dst==1, then dst is a user virtual address;
// otherwise, dst is a kernel address.
// 从block中读取数据到用户缓冲区,参数二表示是否为用户态虚拟地址
int
readi(struct inode *ip, int user_dst, uint64 dst, uint off, uint n)
{
uint tot, m;
struct buf *bp;
// 偏移量是否超过文件大小
if(off > ip->size || off + n < off)
return 0;
// 如果期望读取的数据量比文件off偏移量起始的剩余字节数还多,那么缩减期望读取的数据量大小
if(off + n > ip->size)
n = ip->size - off;
// tot表示当前已经读取的字节数,off是读取偏移指针,dst是用户态缓存区偏移指针
for(tot=0; tot<n; tot+=m, off+=m, dst+=m){
// 从磁盘读取当前偏移量所在的block块
bp = bread(ip->dev, bmap(ip, off/BSIZE));
// min(剩余未读取字节数,当前block剩余空间)
m = min(n - tot, BSIZE - off%BSIZE);
// 从block拷贝数据到用户态缓冲区地址,数据长度为n
if(either_copyout(user_dst, dst, bp->data + (off % BSIZE), m) == -1) {
brelse(bp);
tot = -1;
break;
}
brelse(bp);
}
return tot;
}
writei
(kernel/fs.c:483)与readi
相同,但有三个例外:从文件末尾开始或穿过文件末尾的写操作会使文件增长到最大文件大小(kernel/fs.c:490-491);循环将数据复制到缓冲区而不是输出(kernel/fs.c:36);如果写入扩展了文件,writei
必须更新其大小(kernel/fs.c:504-511)。
// Write data to inode.
// Caller must hold ip->lock.
// If user_src==1, then src is a user virtual address;
// otherwise, src is a kernel address.
int
writei(struct inode *ip, int user_src, uint64 src, uint off, uint n)
{
uint tot, m;
struct buf *bp;
// 偏移量有效性检测
if(off > ip->size || off + n < off)
return -1;
// 写入的数据量是否超过文件最大大小的限制
if(off + n > MAXFILE*BSIZE)
return -1;
// tot代表写入数据量,off是文件写入偏移指针,src代表用户态缓冲区写入偏移指针
for(tot=0; tot<n; tot+=m, off+=m, src+=m){
// 从磁盘读取待写入的block到buf cache
bp = bread(ip->dev, bmap(ip, off/BSIZE));
// min(剩余待写入数据量,当前block剩余空闲空间)
m = min(n - tot, BSIZE - off%BSIZE);
// 从用户缓冲区向block写入数据
if(either_copyin(bp->data + (off % BSIZE), user_src, src, m) == -1) {
brelse(bp);
n = -1;
break;
}
// 当前block被修改了,需要记录日志
log_write(bp);
brelse(bp);
}
if(n > 0){
if(off > ip->size)
// 更新当前文件写入偏移量
ip->size = off;
// write the i-node back to disk even if the size didn't change
// because the loop above might have called bmap() and added a new
// block to ip->addrs[].
// 更新inode所在block
iupdate(ip);
}
return n;
}
readi
和writei
都是从检查ip->type == T_DEV
开始的。这种情况处理的是数据不在文件系统中的特殊设备;我们将在文件描述符层返回到这种情况。
函数stati
(kernel/fs.c:442)将inode元数据复制到stat
结构体中,该结构通过stat
系统调用向用户程序公开。
// Copy stat information from inode.
// Caller must hold ip->lock.
void
stati(struct inode *ip, struct stat *st)
{
st->dev = ip->dev;
st->ino = ip->inum;
st->type = ip->type;
st->nlink = ip->nlink;
st->size = ip->size;
}
代码:目录层
目录的内部实现很像文件。其inode的type
为T_DIR
,其数据是一系列目录条目(directory entries)。每个条目(entry)都是一个struct dirent
(kernel/fs.h:56),其中包含一个名称name
和一个inode编号inum
。名称最多为DIRSIZ
(14)个字符;如果较短,则以NUL
(0)字节终止。inode编号为零的条目是空的。
// Directory is a file containing a sequence of dirent structures.
#define DIRSIZ 14
//目录条目
struct dirent {
ushort inum;
char name[DIRSIZ];
};
函数dirlookup
(kernel/fs.c:527)在目录中搜索具有给定名称的条目。如果找到一个,它将返回一个指向相应inode的指针,解开锁定,并将*poff
设置为目录中条目的字节偏移量,以满足调用方希望对其进行编辑的情形。如果dirlookup
找到具有正确名称的条目,它将更新*poff
并返回通过iget
获得的未锁定的inode。Dirlookup
是iget
返回未锁定indoe的原因。调用者已锁定dp
,因此,如果对.
,当前目录的别名,进行查找,则在返回之前尝试锁定indoe将导致重新锁定dp
并产生死锁(还有更复杂的死锁场景,涉及多个进程和..
,父目录的别名。.
不是唯一的问题。)调用者可以解锁dp
,然后锁定ip
,确保它一次只持有一个锁 :
// Look for a directory entry in a directory.
// If found, set *poff to byte offset of entry.
struct inode*
dirlookup(struct inode *dp, char *name, uint *poff)
{
uint off, inum;
struct dirent de;
if(dp->type != T_DIR)
panic("dirlookup not DIR");
// 遍历当前目录文件下每个目录项
for(off = 0; off < dp->size; off += sizeof(de)){
// 从当前目录下读取出目录项信息到de中
if(readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("dirlookup read");
// 当前目录项还没有分配,直接返回
if(de.inum == 0)
continue;
// 比较和目标目录项是否一致
if(namecmp(name, de.name) == 0){
// entry matches path element
// 是需要寻找的目标目录项
if(poff)
// 设置poff的值为off
*poff = off;
// 记录目录项指向的inode numbder
inum = de.inum;
// 从磁盘读取中对应的inode
return iget(dp->dev, inum);
}
}
// 没有对应的目录项
return 0;
}
函数dirlink
(kernel/fs.c:554)将给定名称和inode编号的新目录条目写入目录dp
。如果名称已经存在,dirlink
将返回一个错误(kernel/fs.c:560-564)。主循环读取目录条目,查找未分配的条目。当找到一个时,它会提前停止循环(kernel/fs.c:538-539),并将off
设置为可用条目的偏移量。否则,循环结束时会将off
设置为dp->size
。无论哪种方式,dirlink
都会通过在偏移off
处写入(kernel/fs.c:574-577)来向目录添加一个新条目。
// Write a new directory entry (name, inum) into the directory dp.
int
dirlink(struct inode *dp, char *name, uint inum)
{
int off;
struct dirent de;
struct inode *ip;
// Check that name is not present.
// 如果目录项存在,返回-1
if((ip = dirlookup(dp, name, 0)) != 0){
// iget增加了引用计数,这里iput将引用计数进行递减
iput(ip);
return -1;
}
// Look for an empty dirent.
// 遍历每个目录项,寻找一个空的
for(off = 0; off < dp->size; off += sizeof(de)){
if(readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("dirlink read");
if(de.inum == 0)
break;
}
// 找到一个空的目录项
strncpy(de.name, name, DIRSIZ);
de.inum = inum;
// 将新的目录项写入到当前目录下
if(writei(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("dirlink");
return 0;
}
代码:路径名
路径名查找涉及一系列对dirlookup
的调用,每个路径组件调用一个。Namei
(kernel/fs.c:661)计算path
并返回相应的inode。函数nameiparent
是一个变体:它在最后一个元素之前停止,返回父目录的inode并将最后一个元素复制到name
中。两者都调用通用函数namex
来完成实际工作。
struct inode*
namei(char *path)
{
char name[DIRSIZ];
return namex(path, 0, name);
}
struct inode*
nameiparent(char *path, char *name)
{
return namex(path, 1, name);
}
Namex
(kernel/fs.c:626)首先决定路径计算的开始位置:
- 如果路径以斜线开始,则计算从根目录开始;
- 否则,从当前目录开始(kernel/fs.c:630-633)。
- 然后,它使用
skipelem
依次考察路径的每个元素(kernel/fs.c:635)。 - 循环的每次迭代都必须在当前索引结点
ip
中查找name
。 - 迭代首先给
ip
上锁并检查它是否是一个目录。 - 如果不是,则查找失败(kernel/fs.c:636-640)(锁定
ip
是必要的,不是因为ip->type
可以被更改,而是因为在ilock
运行之前,ip->type
不能保证已从磁盘加载。) - 如果调用是
nameiparent
,并且这是最后一个路径元素,则根据nameiparent
的定义,循环会提前停止; - 最后一个路径元素已经复制到
name
中,因此namex
只需返回解锁的ip
(kernel/fs.c:641-645)。 - 最后,循环将使用
dirlookup
查找路径元素,并通过设置ip = next
(kernel/fs.c:646-651)为下一次迭代做准备。 - 当循环用完路径元素时,它返回
ip
。
namex
过程可能需要很长时间才能完成:它可能涉及多个磁盘操作来读取路径名中所遍历目录的索引节点和目录块(如果它们不在buffer cache中)。Xv6经过精心设计,如果一个内核线程对namex
的调用在磁盘I/O上阻塞,另一个查找不同路径名的内核线程可以同时进行。Namex
分别锁定路径中的每个目录,以便在不同目录中进行并行查找。
// Look up and return the inode for a path name.
// If parent != 0, return the inode for the parent and copy the final
// path element into name, which must have room for DIRSIZ bytes.
// Must be called inside a transaction since it calls iput().
static struct inode*
namex(char *path, int nameiparent, char *name)
{
struct inode *ip, *next;
// 如果查询以'/'开头,则加载根目录的inode
if(*path == '/')
ip = iget(ROOTDEV, ROOTINO);
else
// 加载当前工作目录的inode
ip = idup(myproc()->cwd);
// 每次循环处理路径的一个元素 --> /a/b/c -> a,b,c
while((path = skipelem(path, name)) != 0){
// 锁住当前inode
ilock(ip);
// 检查当前inode的类型是否为目录,如果不是目录,则解锁并释放当前inode,并返回0。
if(ip->type != T_DIR){
iunlockput(ip);
return 0;
}
// 如果nameiparent为真且path已经到达末尾(即最后一个路径元素),则提前结束循环,解锁当前inode,并返回当前inode
if(nameiparent && *path == '0'){
// Stop one level early.
iunlock(ip);
return ip;
}
// 通过调用dirlookup函数,在当前目录中查找给定名称的目录项,如果找不到,则解锁并释放当前inode,并返回0
if((next = dirlookup(ip, name, 0)) == 0){
iunlockput(ip);
return 0;
}
iunlockput(ip);
ip = next;
}
// 如果nameiparent为真,则说明要返回父目录的inode,此时释放当前inode,并返回0
if(nameiparent){
iput(ip);
return 0;
}
//否则,返回最终的inode
return ip;
}
// Copy the next path element from path into name.
// Return a pointer to the element following the copied one.
// The returned path has no leading slashes,
// so the caller can check *path=='\0' to see if the name is the last one.
// If no name to remove, return 0.
//
// Examples:
// skipelem("a/bb/c", name) = "bb/c", setting name = "a"
// skipelem("///a//bb", name) = "bb", setting name = "a"
// skipelem("a", name) = "", setting name = "a"
// skipelem("", name) = skipelem("", name) = 0
//
static char*
skipelem(char *path, char *name)
{
char *s;
int len;
while(*path == '/')
path++;
if(*path == 0)
return 0;
s = path;
while(*path != '/' && *path != 0)
path++;
len = path - s;
if(len >= DIRSIZ)
memmove(name, s, DIRSIZ);
else {
memmove(name, s, len);
name[len] = 0;
}
while(*path == '/')
path++;
return path;
}
namx函数使用举例如下:
// 函数将返回 "file1.txt" 的inode,并将名称 "file1.txt" 存储在 'name' 缓冲区中。
char* path = "/dir1/file1.txt";
char name[DIRSIZ];
struct inode* file_inode = namex(path, 0, name);
// 函数将返回 "dir1" 的inode,并将名称 "dir1" 存储在 'name' 缓冲区中。
char* path = "/dir1";
char name[DIRSIZ];
struct inode* dir_inode = namex(path, 0, name);
// 函数将返回父目录 "dir1" 的inode,并将名称 "file1.txt" 存储在 'name' 缓冲区中。
char* path = "/dir1/file1.txt";
char name[DIRSIZ];
struct inode* parent_inode = namex(path, 1, name);
这种并发性带来了一些挑战。例如,当一个内核线程正在查找路径名时,另一个内核线程可能正在通过取消目录链接来更改目录树。一个潜在的风险是,查找可能正在搜索已被另一个内核线程删除且其块已被重新用于另一个目录或文件的目录。
Xv6避免了这种竞争。例如,在namex
中执行dirlookup
时,lookup线程持有目录上的锁,dirlookup
返回使用iget
获得的inode。Iget
增加索引节点的引用计数。只有在从dirlookup
接收inode之后,namex
才会释放目录上的锁。现在,另一个线程可以从目录中取消inode的链接,但是xv6还不会删除inode,因为inode的引用计数仍然大于零。
另一个风险是死锁。例如,查找“.
”时,next
指向与ip
相同的inode。在释放ip
上的锁之前锁定next
将导致死锁。为了避免这种死锁,namex
在获得下一个目录的锁之前解锁该目录。这里我们再次看到为什么iget
和ilock
之间的分离很重要。
文件描述符层
Unix界面的一个很酷的方面是,Unix中的大多数资源都表示为文件,包括控制台、管道等设备,当然还有真实文件。文件描述符层是实现这种一致性的层。
正如我们在第1章中看到的,Xv6为每个进程提供了自己的打开文件表或文件描述符。每个打开的文件都由一个struct file
(kernel/file.h:1)表示,它是inode或管道的封装,加上一个I/O偏移量。每次调用open
都会创建一个新的打开文件(一个新的struct file
):
- 如果多个进程独立地打开同一个文件,那么不同的实例将具有不同的I/O偏移量。
- 另一方面,单个打开的文件(同一个
struct file
)可以多次出现在一个进程的文件表中,也可以出现在多个进程的文件表中。 - 如果一个进程使用
open
打开文件,然后使用dup
创建别名,或使用fork
与子进程共享,就会发生这种情况。 - 引用计数跟踪对特定打开文件的引用数。可以打开文件进行读取或写入,也可以同时进行读取和写入。
readable
和writable
字段可跟踪此操作。
struct file {
enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE } type;
int ref; // reference count
char readable;
char writable;
struct pipe *pipe; // FD_PIPE
struct inode *ip; // FD_INODE and FD_DEVICE
uint off; // FD_INODE
short major; // FD_DEVICE
};
系统中所有打开的文件都保存在全局文件表ftable
中。文件表具有分配文件(filealloc
)、创建重复引用(filedup
)、释放引用(fileclose
)以及读取和写入数据(fileread
和filewrite
)的函数。
struct {
struct spinlock lock;
struct file file[NFILE];
} ftable;
前三个函数遵循现在熟悉的形式。
-
Filealloc
(kernel/file.c:30)扫描文件表以查找未引用的文件(f->ref == 0
),并返回一个新的引用;
// Allocate a file structure.
struct file*
filealloc(void)
{
struct file *f;
acquire(&ftable.lock);
for(f = ftable.file; f < ftable.file + NFILE; f++){
if(f->ref == 0){
f->ref = 1;
release(&ftable.lock);
return f;
}
}
release(&ftable.lock);
return 0;
}
-
filedup
(kernel/file.c:48)增加引用计数;
// Increment ref count for file f.
struct file*
filedup(struct file *f)
{
acquire(&ftable.lock);
if(f->ref < 1)
panic("filedup");
f->ref++;
release(&ftable.lock);
return f;
}
-
fileclose
(kernel/file.c:60)将其递减。当文件的引用计数达到零时,fileclose
会根据type
释放底层管道或inode。
// Close file f. (Decrement ref count, close when reaches 0.)
void
fileclose(struct file *f)
{
struct file ff;
acquire(&ftable.lock);
if(f->ref < 1)
panic("fileclose");
if(--f->ref > 0){
release(&ftable.lock);
return;
}
ff = *f;
f->ref = 0;
f->type = FD_NONE;
release(&ftable.lock);
if(ff.type == FD_PIPE){
pipeclose(ff.pipe, ff.writable);
} else if(ff.type == FD_INODE || ff.type == FD_DEVICE){
begin_op();
iput(ff.ip);
end_op();
}
}
函数filestat
、fileread
和filewrite
实现对文件的stat
、read
和write
操作:
-
Filestat
(kernel/file.c:88)只允许在inode上操作并且调用了stati
。
// Get metadata about file f.
// addr is a user virtual address, pointing to a struct stat.
int
filestat(struct file *f, uint64 addr)
{
struct proc *p = myproc();
struct stat st;
if(f->type == FD_INODE || f->type == FD_DEVICE){
ilock(f->ip);
stati(f->ip, &st);
iunlock(f->ip);
if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
return -1;
return 0;
}
return -1;
}
-
Fileread
和filewrite
检查打开模式是否允许该操作,然后将调用传递给管道或inode的实现。
// Read from file f.
// addr is a user virtual address.
int
fileread(struct file *f, uint64 addr, int n)
{
int r = 0;
// 检查文件是否可读
if(f->readable == 0)
return -1;
if(f->type == FD_PIPE){
r = piperead(f->pipe, addr, n);
} else if(f->type == FD_DEVICE){
if(f->major < 0 || f->major >= NDEV || !devsw[f->major].read)
return -1;
r = devsw[f->major].read(1, addr, n);
} else if(f->type == FD_INODE){
ilock(f->ip);
// 借助inode文件元数据信息读取数据,从file->off偏移量开始读取
if((r = readi(f->ip, 1, addr, f->off, n)) > 0)
// 文件读取偏移量+r
f->off += r;
iunlock(f->ip);
} else {
panic("fileread");
}
return r;
}
// Write to file f.
// addr is a user virtual address.
// 向文件中写入数据,数据来源于用户空间某个虚拟地址,数据长度为n
int
filewrite(struct file *f, uint64 addr, int n)
{
int r, ret = 0;
// 文件是否可写判断
if(f->writable == 0)
return -1;
// 文件类型是管道
if(f->type == FD_PIPE){
ret = pipewrite(f->pipe, addr, n);
}
// 文件类型是设备类型
else if(f->type == FD_DEVICE){
if(f->major < 0 || f->major >= NDEV || !devsw[f->major].write)
return -1;
ret = devsw[f->major].write(1, addr, n);
}
// 要写入的文件是INODE类型
else if(f->type == FD_INODE){
// write a few blocks at a time to avoid exceeding
// the maximum log transaction size, including
// i-node, indirect block, allocation blocks,
// and 2 blocks of slop for non-aligned writes.
// this really belongs lower down, since writei()
// might be writing a device like the console.
// 每一次写入的最大数据量限制
int max = ((MAXOPBLOCKS-1-1-2) / 2) * BSIZE;
int i = 0;
// 由于单次写入数据量可能会超过单批次最大写入限制,因此会被拆分为多批次写入
while(i < n){
// 计算要写入的数据长度
int n1 = n - i;
// 判断是否超过单批次数据写入限制,如果超过,那么本次写入数据量变更为max
if(n1 > max)
n1 = max;
// 开启日志事务操作记录
begin_op();
// 获取当前文件inode的lock
ilock(f->ip);
// 将数据写入inode,各个参数含义: 当前文件对应的inode,数据源地址是否为用户空间虚拟地址,源数据地址,写入文件的偏移量,写入数据长度
// 返回值是实际写入的数据量
if ((r = writei(f->ip, 1, addr + i, f->off, n1)) > 0)
// 文件偏移量增加r
f->off += r;
// 释放文件inode的lock
iunlock(f->ip);
// 提交日志事务操作记录
end_op();
// 出现错误
if(r < 0)
break;
// 实际写入的数据量与预期不符,则抛出异常
if(r != n1)
panic("short filewrite");
// 写入数据总量+r
i += r;
}
// 如果最终写入的数据量与预期不符,返回-1
ret = (i == n ? n : -1);
} else {
panic("filewrite");
}
return ret;
}
如果文件表示inode,fileread
和filewrite
使用I/O偏移量作为操作的偏移量,然后将文件指针前进该偏移量(kernel/file.c:122-123)(kernel/file.c:153-154)。
管道没有偏移的概念。回想一下,inode的函数要求调用方处理锁(kernel/file.c:94-96)(kernel/file.c:121-124)(kernel/file.c:163-166)。inode锁定有一个方便的副作用,即读取和写入偏移量以原子方式更新,因此,对同一文件的同时多次写入不能覆盖彼此的数据,尽管他们的写入最终可能是交错的。
代码:系统调用
通过使用底层提供的函数,大多数系统调用的实现都很简单(请参阅kernel/sysfile.c)。有几个调用值得仔细看看。
函数sys_link
和sys_unlink
编辑目录,创建或删除索引节点的引用。它们是使用事务能力的另一个很好的例子。sys_link
(kernel/sysfile.c:120)从获取其参数开始,两个字符串分别是old
和new
(kernel/sysfile.c:125)。假设old
存在并且不是一个目录(kernel/sysfile.c:129-132),sys_link
会增加其ip->nlink
计数。然后sys_link
调用nameiparent
来查找new
(kernel/sysfile.c:145)的父目录和最终路径元素,并创建一个指向old
的inode(kernel/sysfile.c:148)的新目录条目。new
的父目录必须存在并且与现有inode位于同一设备上:inode编号在一个磁盘上只有唯一的含义。如果出现这样的错误,sys_link
必须返回并减少ip->nlink
。
// Create the path new as a link to the same inode as old.
uint64
sys_link(void)
{
char name[DIRSIZ], new[MAXPATH], old[MAXPATH];
struct inode *dp, *ip;
// 获取要link的已经存在的路径名,和link产生的新路径名
if(argstr(0, old, MAXPATH) < 0 || argstr(1, new, MAXPATH) < 0)
return -1;
// 开启日志事务
begin_op();
// namei返回已经存在的文件的inode
if((ip = namei(old)) == 0){
end_op();
return -1;
}
// 锁住当前old inode
ilock(ip);
// 如果当前要link的是目录,则直接返回,不进行操作
if(ip->type == T_DIR){
iunlockput(ip);
end_op();
return -1;
}
// 增加当前inode的link数量
ip->nlink++;
// 更新磁盘上当前Inode所在Block信息
iupdate(ip);
iunlock(ip);
// 获取当前new文件的父目录,如果不存在则有问题
if((dp = nameiparent(new, name)) == 0)
goto bad;
ilock(dp);
// 父目录设备号必须和子文件一样,在当前目录下新增一个目录项,名字为新的文件名,inode编号和old文件一样
if(dp->dev != ip->dev || dirlink(dp, name, ip->inum) < 0){
iunlockput(dp);
goto bad;
}
// 递减dp和ip两个inode的引用计数
iunlockput(dp);
iput(ip);
// 日志事务修改落盘
end_op();
return 0;
bad:
ilock(ip);
ip->nlink--;
iupdate(ip);
iunlockput(ip);
end_op();
return -1;
}
事务简化了实现,因为它需要更新多个磁盘块,但我们不必担心更新的顺序。他们要么全部成功,要么什么都不做。例如在没有事务的情况下,在创建一个链接之前更新ip->nlink
会使文件系统暂时处于不安全状态,而在这两者之间发生的崩溃可能会造成严重破坏。对于事务,我们不必担心这一点
Sys_link
为现有inode创建一个新名称。
link增加了对应inode的link数,同时在对应父目录下新增一条目录项
函数create
(kernel/sysfile.c:242)为新inode创建一个新名称。它是三个文件创建系统调用的泛化:带有O_CREATE
标志的open
生成一个新的普通文件,mkdir
生成一个新目录,mkdev
生成一个新的设备文件。
与sys_link
一样,create
从调用nameiparent
开始,以获取父目录的inode。然后调用dirlookup
检查名称是否已经存在(kernel/sysfile.c:252)。如果名称确实存在,create
的行为取决于它用于哪个系统调用:
-
open
的语义与mkdir
和mkdev
不同。 - 如果
create
是代表open
(type == T_FILE
)使用的,并且存在的名称本身是一个常规文件,那么open
会将其视为成功,create
也会这样做(kernel/sysfile.c:256)。否则,这是一个错误(kernel/sysfile.c:257-258)。 - 如果名称不存在,
create
现在将使用ialloc
(kernel/sysfile.c:261)分配一个新的inode。如果新inode是目录,create
将使用.
和..
条目对它进行初始化。 - 最后,既然数据已正确初始化,
create
可以将其链接到父目录(kernel/sysfile.c:274)。 -
Create
与sys_link
一样,同时持有两个inode锁:ip
和dp
。不存在死锁的可能性,因为索引结点ip
是新分配的:系统中没有其他进程会持有ip
的锁,然后尝试锁定dp
。
static struct inode*
create(char *path, short type, short major, short minor)
{
struct inode *ip, *dp;
char name[DIRSIZ];
// path=/home/dhy.txt --> name被赋值为dhy.txt,同时dp= home inode
if((dp = nameiparent(path, name)) == 0)
return 0;
ilock(dp);
// 在当前目录下寻找名为dhy.txt的目录项
if((ip = dirlookup(dp, name, 0)) != 0){
iunlockput(dp);
ilock(ip);
// 如果存在,并且type类型为文件,同时inode的类型为文件或者设备,直接返回当前inode
if(type == T_FILE && (ip->type == T_FILE || ip->type == T_DEVICE))
return ip;
iunlockput(ip);
// 否则返回0
return 0;
}
// 如果当前目录下不存在名为dhy.txt的目录项,则分配一个新的inode
if((ip = ialloc(dp->dev, type)) == 0)
panic("create: ialloc");
ilock(ip);
ip->major = major;
ip->minor = minor;
ip->nlink = 1;
iupdate(ip);
// 如果要创建的是目录
if(type == T_DIR){ // Create . and .. entries.
dp->nlink++; // for ".."
iupdate(dp);
// No ip->nlink++ for ".": avoid cyclic ref count.
// 目录下永远存在两个目录项: .和..
if(dirlink(ip, ".", ip->inum) < 0 || dirlink(ip, "..", dp->inum) < 0)
panic("create dots");
}
// 添加一个目标目录项<name,inum>
if(dirlink(dp, name, ip->inum) < 0)
panic("create: dirlink");
iunlockput(dp);
return ip;
}
使用create
,很容易实现sys_open
、sys_mkdir
和sys_mknod
。Sys_open
(kernel/sysfile.c:287)是最复杂的,因为创建一个新文件只是它能做的一小部分。如果open
被传递了O_CREATE
标志,它将调用create
(kernel/sysfile.c:301)。否则,它将调用namei
(kernel/sysfile.c:307)。Create
返回一个锁定的inode,但namei
不锁定,因此sys_open
必须锁定inode本身。这提供了一个方便的地方来检查目录是否仅为读取打开,而不是写入。假设inode是以某种方式获得的,sys_open
分配一个文件和一个文件描述符(kernel/sysfile.c:325),然后填充该文件(kernel/sysfile.c:337-342)。请注意,没有其他进程可以访问部分初始化的文件,因为它仅位于当前进程的表中。
uint64
sys_open(void)
{
char path[MAXPATH];
int fd, omode;
struct file *f;
struct inode *ip;
int n;
// 获取要打开的文件的路径和打开的权限模式
if((n = argstr(0, path, MAXPATH)) < 0 || argint(1, &omode) < 0)
return -1;
begin_op();
// 文件不存在则进行创建
if(omode & O_CREATE){
// 调用create在文件存在时直接返回,不存在时创建文件
ip = create(path, T_FILE, 0, 0);
if(ip == 0){
end_op();
return -1;
}
} else {
// 如果打开模式不包含O_CREATE标志,表示要打开已存在的文件
// 调用namei函数根据路径找到文件的inode(索引节点)
if((ip = namei(path)) == 0){
end_op();
return -1;
}
ilock(ip);
// 如果文件类型是目录并且打开模式不是只读的,直接返回
// 在文件系统中,目录是一种特殊类型的文件,它包含其他文件和子目录的条目。
// 通常情况下,只有以只读模式打开目录文件才是合理的,因为目录的结构和内容需要保持一致性。
if(ip->type == T_DIR && omode != O_RDONLY){
iunlockput(ip);
end_op();
return -1;
}
}
// 如果文件类型是设备且设备的主设备号无效(小于0或大于等于NDEV)
if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){
iunlockput(ip);
end_op();
return -1;
}
// 从文件列表中分配一个新的file槽位,fd同理
if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){
if(f)
fileclose(f);
iunlockput(ip);
end_op();
return -1;
}
// 文件类型是设备
if(ip->type == T_DEVICE){
f->type = FD_DEVICE;
f->major = ip->major;
} else {
// 文件类型是INODE
f->type = FD_INODE;
f->off = 0;
}
f->ip = ip;
f->readable = !(omode & O_WRONLY);
f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
// 是否需要将当前文件数据进行截断
if((omode & O_TRUNC) && ip->type == T_FILE){
itrunc(ip);
}
iunlock(ip);
end_op();
return fd;
}
// Allocate a file structure.
struct file*
filealloc(void)
{
struct file *f;
acquire(&ftable.lock);
for(f = ftable.file; f < ftable.file + NFILE; f++){
if(f->ref == 0){
f->ref = 1;
release(&ftable.lock);
return f;
}
}
release(&ftable.lock);
return 0;
}
// Allocate a file descriptor for the given file.
// Takes over file reference from caller on success.
static int
fdalloc(struct file *f)
{
int fd;
struct proc *p = myproc();
for(fd = 0; fd < NOFILE; fd++){
if(p->ofile[fd] == 0){
p->ofile[fd] = f;
return fd;
}
}
return -1;
}
// 大家可以改造让mkdir系统调用支持-p参数
uint64
sys_mkdir(void)
{
char path[MAXPATH];
struct inode *ip;
begin_op();
// 获取路径参数并尝试去创建目录
if(argstr(0, path, MAXPATH) < 0 || (ip = create(path, T_DIR, 0, 0)) == 0){
end_op();
return -1;
}
iunlockput(ip);
end_op();
return 0;
}
// 创建设备文件
uint64
sys_mknod(void)
{
struct inode *ip;
char path[MAXPATH];
int major, minor;
begin_op();
// 路径,主设备号和次设备号
if((argstr(0, path, MAXPATH)) < 0 ||
argint(1, &major) < 0 ||
argint(2, &minor) < 0 ||
// 创建设备文件
(ip = create(path, T_DEVICE, major, minor)) == 0){
end_op();
return -1;
}
iunlockput(ip);
end_op();
return 0;
}
在我们还没有文件系统之前,第7章就研究了管道的实现。函数sys_pipe
通过提供创建管道对的方法将该实现连接到文件系统。它的参数是一个指向两个整数的指针,它将在其中记录两个新的文件描述符。然后分配管道并安装文件描述符。
uint64
sys_pipe(void)
{
uint64 fdarray; // user pointer to array of two integers
struct file *rf, *wf;
int fd0, fd1;
struct proc *p = myproc();
// 获取存放管道文件描述符的数组
if(argaddr(0, &fdarray) < 0)
return -1;
// 分配管道读文件和写文件
if(pipealloc(&rf, &wf) < 0)
return -1;
fd0 = -1;
// 分配读文件描述符和写文件描述符号
if((fd0 = fdalloc(rf)) < 0 || (fd1 = fdalloc(wf)) < 0){
if(fd0 >= 0)
p->ofile[fd0] = 0;
fileclose(rf);
fileclose(wf);
return -1;
}
// 将两个管道文件描述符copy到用户指定的fdarray中去
if(copyout(p->pagetable, fdarray, (char*)&fd0, sizeof(fd0)) < 0 ||
copyout(p->pagetable, fdarray+sizeof(fd0), (char *)&fd1, sizeof(fd1)) < 0){
p->ofile[fd0] = 0;
p->ofile[fd1] = 0;
fileclose(rf);
fileclose(wf);
return -1;
}
return 0;
}
int
pipealloc(struct file **f0, struct file **f1)
{
struct pipe *pi;
pi = 0;
*f0 = *f1 = 0;
if((*f0 = filealloc()) == 0 || (*f1 = filealloc()) == 0)
goto bad;
if((pi = (struct pipe*)kalloc()) == 0)
goto bad;
pi->readopen = 1;
pi->writeopen = 1;
pi->nwrite = 0;
pi->nread = 0;
initlock(&pi->lock, "pipe");
(*f0)->type = FD_PIPE;
(*f0)->readable = 1;
(*f0)->writable = 0;
(*f0)->pipe = pi;
(*f1)->type = FD_PIPE;
(*f1)->readable = 0;
(*f1)->writable = 1;
(*f1)->pipe = pi;
return 0;
bad:
if(pi)
kfree((char*)pi);
if(*f0)
fileclose(*f0);
if(*f1)
fileclose(*f1);
return -1;
}
真实世界
实际操作系统中的buffer cache比xv6复杂得多,但它有两个相同的用途:
- 缓存和同步对磁盘的访问。
与UNIX V6一样,Xv6的buffer cache使用简单的最近最少使用(LRU)替换策略;有许多更复杂的策略可以实现,每种策略都适用于某些工作场景,而不适用于其他工作场景。更高效的LRU缓存将消除链表,而改为使用哈希表进行查找,并使用堆进行LRU替换。现代buffer cache通常与虚拟内存系统集成,以支持内存映射文件。
Xv6的日志系统效率低下。提交不能与文件系统调用同时发生。系统记录整个块,即使一个块中只有几个字节被更改。它执行同步日志写入,每次写入一个块,每个块可能需要整个磁盘旋转时间。真正的日志系统解决了所有这些问题。
日志记录不是提供崩溃恢复的唯一方法。早期的文件系统在重新启动期间使用了一个清道夫程序(例如,UNIX的fsck
程序)来检查每个文件和目录以及块和索引节点空闲列表,查找并解决不一致的问题。清理大型文件系统可能需要数小时的时间,而且在某些情况下,无法以导致原始系统调用原子化的方式解决不一致问题。从日志中恢复要快得多,并且在崩溃时会导致系统调用原子化。
Xv6使用的索引节点和目录的基础磁盘布局与早期UNIX相同;这一方案多年来经久不衰。BSD的UFS/FFS和Linux的ext2/ext3使用基本相同的数据结构。文件系统布局中最低效的部分是目录,它要求在每次查找期间对所有磁盘块进行线性扫描。当目录只有几个磁盘块时,这是合理的,但对于包含许多文件的目录来说,开销巨大。Microsoft Windows的NTFS、Mac OS X的HFS和Solaris的ZFS(仅举几例)将目录实现为磁盘上块的平衡树。这很复杂,但可以保证目录查找在对数时间内完成(即时间复杂度为O(logn))。
Xv6对于磁盘故障的解决很初级:如果磁盘操作失败,Xv6就会调用panic
。这是否合理取决于硬件:如果操作系统位于使用冗余屏蔽磁盘故障的特殊硬件之上,那么操作系统可能很少看到故障,因此panic
是可以的。另一方面,使用普通磁盘的操作系统应该预料到会出现故障,并能更优雅地处理它们,这样一个文件中的块丢失不会影响文件系统其余部分的使用。
Xv6要求文件系统安装在单个磁盘设备上,且大小不变。随着大型数据库和多媒体文件对存储的要求越来越高,操作系统正在开发各种方法来消除“每个文件系统一个磁盘”的瓶颈。基本方法是将多个物理磁盘组合成一个逻辑磁盘。RAID等硬件解决方案仍然是最流行的,但当前的趋势是在软件中尽可能多地实现这种逻辑。这些软件实现通常允许通过动态添加或删除磁盘来扩展或缩小逻辑设备等丰富功能。当然,一个能够动态增长或收缩的存储层需要一个能够做到这一点的文件系统:xv6使用的固定大小的inode块阵列在这样的环境中无法正常工作。将磁盘管理与文件系统分离可能是最干净的设计,但两者之间复杂的接口导致了一些系统(如Sun的ZFS)将它们结合起来。
Xv6的文件系统缺少现代文件系统的许多其他功能;例如,它缺乏对快照和增量备份的支持。文章来源:https://www.toymoban.com/news/detail-544945.html
现代Unix系统允许使用与磁盘存储相同的系统调用访问多种资源:命名管道、网络连接、远程访问的网络文件系统以及监视和控制接口,如/proc
(注:Linux 内核提供了一种通过/proc
文件系统,在运行时访问内核内部数据结构、改变内核设置的机制。proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口。)。不同于xv6中fileread
和filewrite
的if
语句,这些系统通常为每个打开的文件提供一个函数指针表,每个操作一个,并通过函数指针来援引inode的调用实现。网络文件系统和用户级文件系统提供了将这些调用转换为网络RPC并在返回之前等待响应的函数。文章来源地址https://www.toymoban.com/news/detail-544945.html
练习
- 为什么要在
balloc
中panic
?xv6可以恢复吗? - 为什么要在
ialloc
中panic
?xv6可以恢复吗? - 当文件用完时,
filealloc
为什么不panic
?为什么这更常见,因此值得处理? - 假设在
sys_link
调用iunlock(ip)
和dirlink
之间,与ip
对应的文件被另一个进程解除链接。链接是否正确创建?为什么? -
create
需要四个函数调用都成功(一次调用ialloc
,三次调用dirlink
)。如果未成功,create
调用panic
。为什么这是可以接受的?为什么这四个调用都不能失败? -
sys_chdir
在iput(cp->cwd)
之前调用iunlock(ip)
,这可能会尝试锁定cp->cwd
,但将iunlock(ip)
延迟到iput
之后不会导致死锁。为什么不这样做? - 实现
lseek
系统调用。支持lseek
还需要修改filewrite
,以便在lseek
设置off
超过f->ip->size
时,用零填充文件中的空缺。 - 将
O_TRUNC
和O_APPEND
添加到open
,以便>
和>>
操作符在shell中工作。 - 修改文件系统以支持符号链接。
- 修改文件系统以支持命名管道。
- 修改文件和VM系统以支持内存映射文件。
到了这里,关于MIT 6.S081 教材第八章内容 -- 文件系统 -- 02的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!