文件系统

  • 公众号:Rand_cs

本文继续来看

x

v

6

xv6

xv6 的文件系统部分,

x

v

6

xv6

xv6 将文件系统的设计分为 7 层:

i

n

o

d

e

磁盘 \rightarrow 缓存区 \rightarrow 日志 \rightarrow inode \rightarrow 目录 \rightarrow 路径 \rightarrow 文件系统调用

磁盘→缓存区→日志→inode→目录→路径→文件系统调用 ,磁盘、缓存区、日志三个部分在前文已经说了,本文接着讲述

i

n

o

d

e

inode

inode,目录,路径三个层次。

这部分的理论知识可以参考前文:捋一捋文件系统。本文直接来看 xv6 的文件系统这部分是如何实现的。

文件系统布局

再来系统的看看 xv6 文件系统的布局图:

这个图与

x

v

6

xv6

xv6 文档给出的布局图有些不一样,主要是日志区的位置变化了。

x

v

6

xv6

xv6 文档给出的布局图日志区位于文件系统的末尾,但是根据源码来看日志区应该是位于超级块后面的。前文直接用的

x

v

6

xv6

xv6 文档中的图,应该是有误的,实在抱歉。我看了几个版本的

x

v

6

xv6

xv6 源码和文档,源码是日志区都是安排在超级块后面,而文档的布局图描述的是将日志区放在末尾。不过这不是重点,不影响咱们理解,不管位于哪儿,在超级块中做相应修改就行。

引导块、超级块

第 0 块是引导块,里面存放的启动程序也就是

b

o

o

t

b

l

o

c

k

bootblock

bootblock,详见前文:多处理器下的计算机启动

第 1 块是超级块,存有文件系统的元信息,相关结构体定义如下:

struct superblock {
uint size; // Size of file system image (blocks) 文件系统大小,也就是一共多少块
uint nblocks; // Number of data blocks 数据块数量
uint ninodes; // Number of inodes. //i结点数量
uint nlog; // Number of log blocks //日志块数量
uint logstart; // Block number of first log block //第一个日志块块号
uint inodestart; // Block number of first inode block //第一个i结点所在块号
uint bmapstart; // Block number of first free map block //第一个位图块块号
};

可以看出超级块实则就是文件系统布局的信息集合。在

m

k

f

s

.

c

mkfs.c

mkfs.c 中我们可以知道:

#define NINODES 200
#define MAXOPBLOCKS 10
#define LOGSIZE (MAXOPBLOCKS*3)
#define FSSIZE 1000
#define IPB (BSIZE / sizeof(struct dinode)) int nbitmap = FSSIZE/(BSIZE*8) + 1;
int ninodeblocks = NINODES / IPB + 1;
int nlog = LOGSIZE; int nmeta = 2 + nlog + ninodeblocks + nbitmap;
int nblocks = FSSIZE - nmeta; int logstart = 2;
int inodestart = 2 + nlog;
int bmapstart = 2 + nlog + ninodeblocks;

从上述代码可以看出,文件系统的各个部分从哪开始,到哪结束都是可以明确计算出来的,所以其实不管将日志区安排在哪,我们都可以从超级块中获取相应的位置大小信息。

数据区

紧接着超级块的区域应该是

i

n

o

d

e

inode

inode,但是

i

n

o

d

e

inode

inode 的内容有些多有些复杂,我们放在后面讲,先来看看数据区中数据块的组织与管理。

数据块的分配和释放由位图来管理,但位图管理的区域不止数据区,而是整个文件系统。有关位图的宏定义如下:

// Bitmap bits per block    每个块能有多少个bit
#define BPB (BSIZE*8)
// Block of free map containing bit for block b 块b在哪个位图块上
#define BBLOCK(b, sb) (b/BPB + sb.bmapstart)

分配回收

static uint balloc(uint dev)
{
int b, bi, m;
struct buf *bp; bp = 0;
for(b = 0; b < sb.size; b += BPB){
bp = bread(dev, BBLOCK(b, sb)); //读取位图信息
for(bi = 0; bi < BPB && b + bi < sb.size; bi++){
m = 1 << (bi % 8);
if((bp->data[bi/8] & m) == 0){ // Is block free? 如果该块空闲
bp->data[bi/8] |= m; // Mark block in use. 标记该块使用
log_write(bp);
brelse(bp); //释放锁
bzero(dev, b + bi); //将该块置0
return b + bi; //返回块号
}
}
brelse(bp); //释放锁
}
panic("balloc: out of blocks");
} static void bfree(int dev, uint b) //释放一个数据块,相应位图清零
{
struct buf *bp;
int bi, m; bp = bread(dev, BBLOCK(b, sb));
bi = b % BPB;
m = 1 << (bi % 8);
if((bp->data[bi/8] & m) == 0)
panic("freeing free block");
bp->data[bi/8] &= ~m;
log_write(bp);
brelse(bp);
}

位图块中每一位都代表着一块,该位置 1 表示相应的块正在使用,该位置 0 表示相应的块空闲。分配数据块就是在位图中寻找空闲位,然后将其置 1 就代表分配出去了。

上述代码涉及的都是比较简单的位运算,有详细的注释,就不说明了,释放一个数据块的操作就是分配的逆操作,也不再赘述。

inode

磁盘上的 dinode

紧接着超级块后面的区域是

i

n

o

d

e

s

inodes

inodes 区域,

x

v

6

xv6

xv6 定义的

i

n

o

d

e

inode

inode 共有 200 个,每个磁盘上的

d

i

n

o

d

e

dinode

dinode 定义如下:

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[NDIRECT+1]; // Data block addresses
};
#define NDIRECT 12

t

y

p

e

type

type 表示该

i

n

o

d

e

inode

inode 指向的文件的类型,在

x

v

6

xv6

xv6 里面就只有三种类型,普通文件,目录文件,设备文

n

l

i

n

k

nlink

nlink 表示该文件的链接数,链接分为硬链接和软连接,这里与链接数目直接相关的是硬链接,后面实现文件系统调用的时候我们会看到,

s

y

s

_

l

i

n

k

(

)

sys\_link()

sys_link() 系统调用会创建一个新目录项并且增加一个链接数。

s

y

s

_

u

n

l

i

n

k

(

)

sys\_unlink()

sys_unlink() 系统调用将链接数减 1,如果该文件在内存中的引用数和链接数都为 0 的话,则会删除该文件。这部分咱们后面再慢慢聊,本文只捎带提一下。

s

i

z

e

size

size 表示文件的大小,这里是以字节为单位。

L

i

n

u

x

Linux

Linux 里面使用

m

a

j

o

r

n

u

m

b

e

r

major\ number

major number 和

m

i

n

o

r

n

u

m

b

e

r

minor\ number

minor number 来区分设备,同一类设备有着相同的

m

a

j

o

r

n

u

m

b

e

r

major\ number

major number,

m

i

n

o

r

n

u

m

b

e

r

minor\ number

minor number 又表示该类设备中具体的某设备。后面我们会看到如果文件类型为设备,则读写的时候会根据

m

a

j

o

r

n

u

m

b

e

r

major\ number

major number 调用相应设备的读写程序。

每个

i

n

o

d

e

inode

inode 有 11 个一级指针,一个间接指针,用来指向数据块。这些都是可以改变的,

x

v

6

xv6

xv6 有个关于支持大文件的实现就是要增加一个间接索引来实现。

inode 缓存

磁盘上的

i

n

o

d

e

inode

inode 在内存中也有相应的缓存:

struct {
struct spinlock lock;
struct inode inode[NINODE];
} icache; //磁盘上i结点在内存中的缓存

内存中的

i

n

o

d

e

inode

inode 定义如下:

struct inode {
uint dev; // Device number 设备号
uint inum; // Inode number inode 号
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];
};

内存中的

i

n

o

d

e

inode

inode 比磁盘上的

i

n

o

d

e

inode

inode 多了几个属性,首先是设备号,

L

i

n

u

x

Linux

Linux 里面一切皆文件,设备也是文件,所以设备号来表示什么设备。但是 xv6 没这么复杂,这里主要就是来区分磁盘的主盘和从盘。

d

e

v

=

1

dev = 1

dev=1时为从盘,

d

e

v

=

0

dev = 0

dev=0 时为主盘,这个值在读写磁盘的时候用到,用它来设置磁盘的 device 寄存器来指定主盘从盘。详见:带你了解磁盘驱动程序

r

e

f

ref

ref 表示引用数,这个要与

l

i

n

k

link

link 链接数作区别,目前可以暂且理解为

l

i

n

k

link

link 为磁盘上文件之间的关系,而

r

e

f

ref

ref 主要用于内存中引用该文件的次数,比如

c

l

o

s

e

(

)

close()

close() 关闭文件使引用数减 1。这部分在文件系统调用的时候再作详细讲解。

v

a

l

i

d

valid

valid 表示是否有效,跟磁盘那里缓存块中的数据是否有效一个意思,如果缓存中的数据是从磁盘中读取过来的,则有效。通常无效是因为

i

n

o

d

e

inode

inode 刚分配,所以里面的数据无效

整个

i

n

o

d

e

inode

inode 缓存区有一把自旋锁,每个

i

n

o

d

e

inode

inode 缓存有把休眠锁,为什么如此,道理还是同磁盘和缓存块。首先它们都是公共资源,需要锁来避免竞争条件。再者

i

c

a

c

h

e

icache

icache 的作用就是组织管理

i

n

o

d

e

inode

inode,像是一个分配器,访问

i

c

a

c

h

e

icache

icache 的临界区的时间是很短的,使用自旋锁就行。而一个进程对某个

i

n

o

d

e

inode

inode 的使用时间可能很长,最好使用休眠锁,其他进程也想要获取该

i

n

o

d

e

inode

inode 的使用权时就休眠让出

C

P

U

CPU

CPU 提高性能。

分配 inode

对于磁盘上的

d

i

n

o

d

e

dinode

dinode,

x

v

6

xv6

xv6 并没有什么组织管理结构,分配空闲

d

i

n

o

d

e

dinode

dinode 的方法就是简单粗暴地从头至尾循环查找空闲

d

i

n

o

d

e

dinode

dinode。

struct inode*
ialloc(uint dev, short type)
{
int inum;
struct buf *bp;
struct dinode *dip; for(inum = 1; inum < sb.ninodes; inum++){
bp = bread(dev, IBLOCK(inum, sb)); //读取第inum个i结点所在的位置
dip = (struct dinode*)bp->data + inum%IPB; //该i结点地址
if(dip->type == 0){ // a free inode 找到空闲i结点
memset(dip, 0, sizeof(*dip));
dip->type = type;
log_write(bp); // mark it allocated on the disk
brelse(bp);
return iget(dev, inum); //以内存中的i结点形式返回
}
brelse(bp);
}
panic("ialloc: no inodes");
} #define IBLOCK(i, sb) ((i) / IPB + sb.inodestart)
#define IPB (BSIZE / sizeof(struct dinode))

先来看看下面两个宏定义,

I

P

B

IPB

IPB 表示一个块中能有几个

i

n

o

d

e

inode

inode,

I

B

L

O

C

K

(

i

,

s

b

)

IBLOCK(i, sb)

IBLOCK(i,sb) 表示第

i

i

i 个

i

n

o

d

e

inode

inode 在第几块。

分配数据块的时候有位图来组织管理,所以分配数据块的时候就“从头至尾”的查询空闲位,而

d

i

n

o

d

e

dinode

dinode 没有组织管理的机制,所以就直接从头至尾的查询

d

i

n

o

d

e

dinode

dinode 的使用情况。如果该

d

i

n

o

d

e

dinode

dinode 的

t

y

p

e

type

type 属性为 0 表示该

d

i

n

o

d

e

dinode

dinode 空闲,可以分配,反之正是用当中不可分配。

分配了之后将该

d

i

n

o

d

e

dinode

dinode 初始化,将其数据全部置 0,然后赋其

t

y

p

e

type

type 属性,表示该

d

i

n

o

d

e

dinode

dinode 指向一个

t

y

p

e

type

type 类型的文件。

分配了该

d

i

n

o

d

e

dinode

dinode 需要在磁盘上也将其标记为已分配,因为目前是在内存中操作的,得同步到磁盘上去,所以直接调用

l

o

g

_

w

r

i

t

e

(

)

log\_write()

log_write() 将该

d

i

n

o

d

e

dinode

dinode 所在的缓存块同步到磁盘。当然并未真正地直接写到磁盘了,只是在将该缓存数据标记为脏,关于日志,读写磁盘的操作本文不赘述了,可以参考前文:带你了解磁盘驱动程序

回到分配

d

i

n

o

d

e

dinode

dinode 的函数上来,磁盘上的

d

i

n

o

d

e

dinode

dinode 已分配,得到了

i

n

o

d

e

inode

inode 号,但是文件系统实际工作的时候使用的是内存中的

i

n

o

d

e

inode

inode 缓存,所以调用

i

g

e

t

(

)

iget()

iget() 来分配(获取)一个内存中的

i

n

o

d

e

inode

inode 来缓存

d

i

n

o

d

e

dinode

dinode 数据:

static struct inode* iget(uint dev, uint inum)
{
struct inode *ip, *empty; acquire(&icache.lock); // Is the inode already cached? 如果该dinode在内存中已缓存
empty = 0;
for(ip = &icache.inode[0]; ip < &icache.inode[NINODE]; ip++){
if(ip->ref > 0 && ip->dev == dev && ip->inum == inum){ //在缓存中找到该i结点
ip->ref++; //引用加1
release(&icache.lock);
return ip;
}
if(empty == 0 && ip->ref == 0) // Remember empty slot. 记录icache中空闲的inode
empty = ip;
} // Recycle an inode cache entry.
if(empty == 0)
panic("iget: no inodes");
//该dinode在内存中没有缓存,分配一个空闲inode empty
//根据参数,初始化该空闲inode,还没读入数据,valid设为0
ip = empty;
ip->dev = dev;
ip->inum = inum;
ip->ref = 1;
ip->valid = 0;
release(&icache.lock); return ip;
}

如果磁盘上的

d

i

n

o

d

e

dinode

dinode 在

i

c

a

c

h

e

icache

icache 中已有缓存,那么直接将该

i

n

o

d

e

inode

inode 的引用数加 1,再返回该

i

n

o

d

e

inode

inode 就行。如果没有缓存则分配一个空闲的

i

n

o

d

e

inode

inode,根据参数初始化

i

n

o

d

e

inode

inode,因为没有实际读入

d

i

n

o

d

e

dinode

dinode 的数据,所以

i

n

o

d

e

inode

inode 的

v

a

l

i

d

valid

valid 属性置 0 表示无效。

使用修改 inode

使用

i

n

o

d

e

inode

inode 之前需要加锁:

void ilock(struct inode *ip)
{
struct buf *bp;
struct dinode *dip; if(ip == 0 || ip->ref < 1) //空指针引用小于1都是错误的
panic("ilock"); acquiresleep(&ip->lock); //上锁 if(ip->valid == 0){ //有效位为0,从磁盘读入数据
bp = bread(ip->dev, IBLOCK(ip->inum, sb));
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);
ip->valid = 1;
if(ip->type == 0)
panic("ilock: no type");
}
}

分配

i

n

o

d

e

inode

inode 的时候并未从磁盘中

d

i

n

o

d

e

dinode

dinode 读入数据,只是将

i

n

o

d

e

inode

inode 的

v

a

l

i

d

valid

valid 置 0 表示数据无效,正式读入

d

i

n

o

d

e

dinode

dinode 数据在这加锁的时候进行。

对缓存中

i

n

o

d

e

inode

inode 的修改需要同步到磁盘上的

d

i

n

o

d

e

dinode

dinode:

void iupdate(struct inode *ip)
{
struct buf *bp;
struct dinode *dip; bp = bread(ip->dev, IBLOCK(ip->inum, sb)); //读取磁盘上的i结点
dip = (struct dinode*)bp->data + ip->inum%IPB;
dip->type = ip->type; //update
dip->major = ip->major;
dip->minor = ip->minor;
dip->nlink = ip->nlink;
dip->size = ip->size;
memmove(dip->addrs, ip->addrs, sizeof(ip->addrs));
log_write(bp);
brelse(bp);
}

用完

i

n

o

d

e

inode

inode 需要 “放下” 它:

void iput(struct inode *ip)
{
acquiresleep(&ip->lock); //取锁
if(ip->valid && ip->nlink == 0){
//获取该i结点的引用数
acquire(&icache.lock);
int r = ip->ref;
release(&icache.lock); if(r == 1){
// inode has no links and no other references: truncate and free.
itrunc(ip);
ip->type = 0;
iupdate(ip);
ip->valid = 0;
}
}
releasesleep(&ip->lock); acquire(&icache.lock);
ip->ref--;
release(&icache.lock);
}

该函数将

i

n

o

d

e

inode

inode 的引用数减 1,如果本身就是该

i

n

o

d

e

inode

inode 的最后一个引用,且链接数也为 0,那么调用

i

t

r

u

n

c

(

)

itrunc()

itrunc() 将该

i

n

o

d

e

inode

inode 指向的所有数据块全部释放,也就相当于删除了

i

n

o

d

e

inode

inode 指向的文件

i

p

t

y

p

e

=

0

ip \rightarrow type = 0

ip→type=0 表示该

i

n

o

d

e

inode

inode 已释放,被回收到了

i

c

a

c

h

e

icache

icache。将

i

n

o

d

e

inode

inode 信息更新到磁盘上的

d

i

n

o

d

e

dinode

dinode 之后将

v

a

l

i

d

valid

valid 置 0 表数据不再有效。

索引

i

n

o

d

e

inode

inode 的索引部分用来指向数据块

static uint bmap(struct inode *ip, uint bn)  //给i结点第bn个索引指向的地方分配块
{
uint addr, *a;
struct buf *bp; //bn为直接索引
if(bn < NDIRECT){
//如果第bn个索引指向的块还未分配,则分配,否则返回块号
if((addr = ip->addrs[bn]) == 0)
ip->addrs[bn] = addr = balloc(ip->dev);
return addr;
} bn -= NDIRECT; //bn为间接索引 if(bn < NINDIRECT){
// Load indirect block, allocating if necessary.
if((addr = ip->addrs[NDIRECT]) == 0) //如果间接索引所在的块还未分配,分配
ip->addrs[NDIRECT] = addr = balloc(ip->dev);
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; //返回索引bn指向的块的块号
} panic("bmap: out of range");
}

b

m

a

p

(

s

t

r

u

c

t

i

n

o

d

e

i

p

,

u

i

n

t

b

n

)

bmap(struct\ inode *ip, uint bn)

bmap(struct inode∗ip,uintbn) 返回索引

b

n

bn

bn 指向的数据块块号,如果该数据块未分配,则分配之后再返回该块块号。

static 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);
}

t

r

u

n

c

a

t

e

i

n

o

d

e

truncate\ inode

truncate inode,截断

i

n

o

d

e

inode

inode,不知怎样翻译,但这个函数的实际工作就是将

i

n

o

d

e

inode

inode 所指向的数据块全部释放,也就相当于删除文件了。具体的工作方式就是一个个的判断索引是否有效,如果有效就调用

b

f

r

e

e

(

)

bfree()

bfree() 释放,然后将该索引置为无效。基本上就与

b

m

a

p

(

)

bmap()

bmap() 做相反的工作。

读写数据

int readi(struct inode *ip, char *dst, uint off, uint n) //从inode读取数据
{
uint tot, m;
struct buf *bp; if(ip->type == T_DEV){ //如果该inode指向的是设备文件
if(ip->major < 0 || ip->major >= NDEV || !devsw[ip->major].read)
return -1;
return devsw[ip->major].read(ip, dst, n); //使用设备特有的读取方式
} if(off > ip->size || off + n < off) //如果开始读取的位置超过文件末尾,如果读取的字节数是负数
return -1;
if(off + n > ip->size) //如果从偏移量开始的n字节超过文件末尾
n = ip->size - off; //则只能够再读取这么多字节 for(tot=0; tot<n; tot+=m, off+=m, dst+=m){ //tol:目前总共已读的字节数,n:需要读取的字节数,off:从这开始读,dst:目的地
bp = bread(ip->dev, bmap(ip, off/BSIZE)); //读取off所在的数据块到缓存块
m = min(n - tot, BSIZE - off%BSIZE); //一次性最多读取m字节
memmove(dst, bp->data + off%BSIZE, m); //赋值数据到dst
brelse(bp); //释放缓存块
}
return n;
} int writei(struct inode *ip, char *src, uint off, uint n)
{
uint tot, m;
struct buf *bp; if(ip->type == T_DEV){
if(ip->major < 0 || ip->major >= NDEV || !devsw[ip->major].write)
return -1;
return devsw[ip->major].write(ip, src, n);
} if(off > ip->size || off + n < off)
return -1;
if(off + n > MAXFILE*BSIZE)
return -1; for(tot=0; tot<n; tot+=m, off+=m, src+=m){
bp = bread(ip->dev, bmap(ip, off/BSIZE));
m = min(n - tot, BSIZE - off%BSIZE);
memmove(bp->data + off%BSIZE, src, m);
log_write(bp);
brelse(bp);
}

r

e

a

d

i

(

s

t

r

u

c

t

i

n

o

d

e

i

p

,

c

h

a

r

d

s

t

,

u

i

n

t

o

f

f

,

u

i

n

t

n

)

readi(struct\ inode *ip, char *dst, uint\ off, uint\ n)

readi(struct inode∗ip,char∗dst,uint off,uint n) 函数用来读取数据,从 ip 指向的文件中,从

o

f

f

off

off 开始读,读取

n

n

n 字节到

d

s

t

dst

dst 中去。

如果

i

n

o

d

e

inode

inode 指向的文件类型是设备,那么读取数据要使用专门的读取方式,比如说控制台有着自己专门的读入数据方式,这些设备的读取方法集合在

d

e

v

s

w

devsw

devsw 数组当中。关于这部分我们后面的文章再详述,这里只说磁盘上的文件的读写。

紧接着判断了一下参数的合理性,比如

o

f

f

off

off 不应超过文件末尾,读取的字节数

n

n

n 不应该是负数,如果

o

f

f

+

n

>

i

p

s

i

z

e

off + n > ip \rightarrow size

off+n>ip→size 超过了文件末尾,那么最多只能读取

i

p

s

i

z

e

o

f

f

ip \rightarrow size - off

ip→size−off 个字节数,要修改

n

n

n 的值。

文件数据可能有多块,

o

f

f

/

B

S

I

Z

E

off/BSIZE

off/BSIZE 表示

o

f

f

off

off 所在的块数,这个块数是相对于该文件开头的块号,调用

b

m

a

p

(

)

bmap()

bmap() 获取

o

f

f

off

off所在的绝对块号。拿到

o

f

f

off

off 所在数据块的绝对块号之后调用

b

r

e

a

d

(

)

bread()

bread() 读取该数据块的数据到缓存块

b

p

bp

bp。

数据在内存中已经准备完毕,可以赋值移动数据了。但是每次读取的数据有上限,不能超过

n

t

o

t

n-tot

n−tot ,这是还需要读取的字节数,不能超过

B

S

I

Z

E

o

f

f

%

B

S

I

Z

E

BSIZE-off\%BSIZE

BSIZE−off%BSIZE,这是每次最多能够读取的字节数。不能超过这两者,所以两者之中取较小值。

w

r

i

t

e

i

(

s

t

r

u

c

t

i

n

o

d

e

i

p

,

c

h

a

r

s

r

c

,

u

i

n

t

o

f

f

,

u

i

n

t

n

)

writei(struct\ inode *ip, char *src, uint\ off, uint\ n)

writei(struct inode∗ip,char∗src,uint off,uint n),将数据源

s

r

c

src

src 中的数据写

n

n

n 字节到

i

p

ip

ip 指向的文件中,从偏移量为

o

f

f

off

off 的地方开始写。

基本上是与

r

e

a

d

i

(

)

readi()

readi()相反的操作,只是最后需要更新

i

n

o

d

e

inode

inode 信息,因为像文件中写数据,该文件会变大,

i

n

o

d

e

inode

inode 是文件的代表与文件一一对应,需要更新

i

n

o

d

e

inode

inode 的 size 属性,然后再调用

i

u

p

d

a

t

e

(

)

iupdate()

iupdate() 将

i

n

o

d

e

inode

inode 同步到磁盘上的

d

i

n

o

d

e

dinode

dinode。

目录

目录也是文件,只是它的数据有些特殊,其数据是一个个目录项,其主要属性有文件名,

i

n

o

d

e

inode

inode 编号。

i

n

o

d

e

inode

inode 是一个文件的代言人,一个文件与一个

i

n

o

d

e

inode

inode 一一对应,但是

i

n

o

d

e

inode

inode 的属性里面并没有文件名。文件名这个属性在目录项这指出,目录项的主要作用就是将

i

n

o

d

e

inode

inode 和文件名联系起来。

结构定义

struct dirent {            //目录项结构
ushort inum;
char name[DIRSIZ];
}; #define DIRSIZ 14

x

v

6

xv6

xv6 中目录项只由两项组成,文件名和

i

n

o

d

e

inode

inode 编号。

查找目录项

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)){
if(readi(dp, (char*)&de, off, sizeof(de)) != sizeof(de)) //读取dp指向的目录文件,每次读一个目录项
panic("dirlookup read");
if(de.inum == 0) //如果是根目录??????????????
continue;
if(namecmp(name, de.name) == 0){ //根据名字找文件
// entry matches path element
if(poff) //记录该目录项在目录中的偏移
*poff = off;
inum = de.inum; //name文件的inode编号
return iget(dp->dev, inum); //或取该inode
}
} return 0;
}

这个函数用来在

i

n

o

d

e

d

p

inode dp

inodedp 指向的目录文件下寻找名为

n

a

m

e

name

name 的目录项,将该目录项的偏移量记录在

p

o

f

f

poff

poff 中,最后返回名字为

n

a

m

e

name

name 的文件的

i

n

o

d

e

inode

inode。

因此根据文件名查找文件的是指就是在目录文件中查找目录项的过程,具体的查找方式就是一个个的比对目录项的名称和要查找的文件名是否相同,如果相同,则找到,反之说明该目录下并没有要查找的文件

添加目录项

int dirlink(struct inode *dp, char *name, uint inum)
{
int off;
struct dirent de;
struct inode *ip; // Check that name is not present.
if((ip = dirlookup(dp, name, 0)) != 0){
iput(ip); //dirlookup调用iget使引用数加1,所以调用iput使引用数减1
return -1; //name目录项已存在,返回-1
} // Look for an empty dirent.
for(off = 0; off < dp->size; off += sizeof(de)){
if(readi(dp, (char*)&de, off, sizeof(de)) != sizeof(de))
panic("dirlink read");
if(de.inum == 0) //找到一个空闲目录项
break;
} strncpy(de.name, name, DIRSIZ); //设置目录项的文件名字
de.inum = inum; //设置目录项的i结点编号
if(writei(dp, (char*)&de, off, sizeof(de)) != sizeof(de)) //将该目录项写进dp指向的目录文件中
panic("dirlink"); return 0;
}

此函数用来在

i

n

o

d

e

d

p

inode dp

inodedp 指向的目录文件中添加一个目录项,通常是创建了一个新文件,需要在该目录下添加这个新文件的信息

首先查找该目录项是否存在,如果不存在则找一个空闲目录项位置,将新文件的

i

n

o

d

e

inode

inode 和文件名写进去。

路径

路径,何为路径,比如常见的

/

a

/

b

/

c

/a/b/c

/a/b/c,仔细观察,会发现,路径实则是一个个文件名组成的,这个文件可能是目录文件,也可能是普通文件。一般最后一项是普通文件名,中间的都是目录文件名

另外像

/

a

/

b

/

c

/a/b/c

/a/b/c 这种路径以 '/' 开头表示绝对路径,

a

/

b

/

c

a/b/c

a/b/c 这种不以 '/' 开头的表示相对路径。这两种路径大家应该都很熟悉了,不再多做解释,或者可以参考文章捋一捋文件系统,其中详细的捋了捋文件系统的理论知识。

不论哪一种路径表示,都需要一个路径解析函数,将其中一个个文件名给提取出来:

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继续向后移,剥出最前面的目录名
path++;
len = path - s; //记录该目录名的长度
if(len >= DIRSIZ)
memmove(name, s, DIRSIZ); //将该目录名复制给name
else {
memmove(name, s, len);
name[len] = 0;
}
while(*path == '/') //继续跳过'/'
path++;
return path; //返回剩下的路径
}

s

k

i

p

e

l

e

m

(

)

skipelem()

skipelem() 调用一次解析一个头部的文件名放在

n

a

m

e

name

name 中,返回剩下的路径。

用源码注释中的例子来说明:

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 struct inode*
namex(char *path, int nameiparent, char *name)
{
struct inode *ip, *next; if(*path == '/') //绝对路径
ip = iget(ROOTDEV, ROOTINO); //读取根目录
else //相对路径
ip = idup(myproc()->cwd); //读取当前工作目录 while((path = skipelem(path, name)) != 0){
ilock(ip);
if(ip->type != T_DIR){
iunlockput(ip);
return 0;
}
if(nameiparent && *path == '\0'){ //如果是要返回父结点,并且剩下的路径已经为空,则当前结点就是i结点直接返回
// Stop one level early.
iunlock(ip);
return ip;
}
if((next = dirlookup(ip, name, 0)) == 0){ //查询下一个目录
iunlockput(ip);
return 0;
}
iunlockput(ip);
ip = next; //当前目录指向下一个,然后while循环,直到解析到最后
}
if(nameiparent){
iput(ip);
return 0;
}
return ip;
}

这个函数根据路径返回

i

n

o

d

e

inode

inode,比如说路径

/

a

/

b

/

c

/a/b/c

/a/b/c,如果

n

a

m

e

i

p

a

r

e

n

t

nameiparent

nameiparent 有效,则返回文件

b

b

b 的

i

n

o

d

e

inode

inode,如果

n

a

m

e

i

p

a

r

e

n

t

nameiparent

nameiparent 无效,则返回文件

c

c

c 的

i

n

o

d

e

inode

inode。

这个函数主要是对路径解析函数

s

k

i

p

e

l

e

m

(

)

skipelem()

skipelem() 和查找目录项函数

d

i

r

l

o

o

k

u

p

(

)

dirlookup()

dirlookup() 函数的运用,根据路径查找文件的步骤大致如下:

  • 获取当前目录的

    i

    n

    o

    d

    e

    inode

    inode

  • 根据

    i

    n

    o

    d

    e

    inode

    inode 获取目录文件

  • 在该目录文件下根据文件名查找文件/目录
  • 循环上述过程直到文件被找到

n

a

m

e

x

(

)

namex()

namex() 函数就是上述过程的实现,至于这个函数的具体怎么实现的,就不详细说明了,可以自己举个例子根据代码模拟一下过程就明白了。在模拟的过程中主要注意几个条件判断:

  • (path = skipelem(path, name)) != 0,当

    p

    a

    t

    h

    path

    path 为空字符串的才返回 0,也就是说 skipelem("", name) = 0。 path 指向一个空字符串,并不是说 path 本身为空

  • if(nameiparent && *path == '\0')

    p

    a

    t

    h

    path

    path 为空字符串的时候也就是 “” 的时候 *path = '\0'

另外如果是相对路径的话,当前目录需要从进程

P

C

B

PCB

PCB 中的

p

w

d

pwd

pwd 属性中获取,相关内容后面进程再详述。

本文主要介绍了

x

v

6

xv6

xv6 文件系统

i

n

o

d

e

inode

inode,目录,路径三个层次的设计与实现,应该对这三个概念会有个更深刻的认识。

i

n

o

d

e

inode

inode 是文件的代表,与文件一一对应,目录也是文件,只是其数据是其他文件的信息,也就是一个个目录项。目录项是其他文件的文件名和相应的

i

n

o

d

e

inode

inode 编号组合而成。而路径呢?就是一个个文件名组合在一起的字符串。可以使用路径解析函数将一个个文件名解析出来,然后根据一层层目录中的目录项从上至下地查找文件

好啦本文就到这里了,有什么错误还请批评指针,也欢迎大家来同我讨论交流一起学习进步。

xv6 文件系统的更多相关文章

  1. XV6文件系统

    文件系统 文件系统的目的是组织和存储数据,典型的文件系统支持用户和程序间的数据共享,并提供数据持久化的支持(即重启之后数据仍然可用). xv6 的文件系统中使用了类似 Unix 的文件,文件描述符,目 ...

  2. XV6源代码阅读-文件系统

    Exercise1 源代码阅读 文件系统部分 buf.h fcntl.h stat.h fs.h file.h ide.c bio.c log.c fs.c file.c sysfile.c exec ...

  3. XV6操作系统代码阅读心得(五):文件系统

    Unix文件系统 当今的Unix文件系统(Unix File System, UFS)起源于Berkeley Fast File System.和所有的文件系统一样,Unix文件系统是以块(Block ...

  4. xv6课本翻译之——第0章 操作系统接口

    Chapter 0 第0章 Operating system interfaces 操作系统接口 The job of an operating system is to share a comput ...

  5. XV6操作系统接口

    操作系统接口 操作系统的工作是(1)将计算机的资源在多个程序间共享,并且给程序提供一系列比硬件本身更有用的服务.(2)管理并抽象底层硬件,举例来说,一个文字处理软件(比如 word)不用去关心自己使用 ...

  6. 【翻译】XV6-DRAFT as of September 3,2014 第0章 操作系统接口

    操作系统接口 操作系统的任务是让多个程序共享计算机(资源),并且提供一系列基于计算机硬件的但更有用的服务.操作系统管理并且把底层的硬件抽象出来,举例来说,一个文字处理软件(例如word)不需要关心计算 ...

  7. book-rev8 Chapter 0 Operating system interfaces

    Chapter 0 第0章 Operating system interfaces 操作系统接口 The job of an operating system is to share a comput ...

  8. MIT 6.S081 Lab File System

    前言 打开自己的blog一看,居然三个月没更新了...回想一下前几个月,开题 + 实验室杂活貌似也没占非常多的时间,还是自己太懈怠了吧,掉线城和文明6真的是时间刹手( 不过好消息是把15445的所有l ...

  9. MIT6.828 La5 File system, Spawn and Shell

    Lab 5: File system, Spawn and Shell 1. File system preliminaries 在lab中我们要使用的文件系统比大多数"真实"文件 ...

  10. 9. Lab: file system

    https://pdos.csail.mit.edu/6.S081/2021/labs/fs.html 1. Large files (moderate) 1.1 要求 Modify bmap() s ...

随机推荐

  1. 技术人如何"结构化"高效思考?

    1 前言 你是否在解决问题时,常常觉得脑子很乱,很多零散的信息迸发出来,但就是无法形成解决方案. 之所以这样,原因就在于,我们的大脑处理不了太多零散而复杂的信息.人类大脑在处理信息的时候,有两个规律: ...

  2. vue 插件(swiper)使用

    两种方法: 1... 打开https://www.swiper.com.cn/download/index.html 下载css,js... 把js,css引入public里面的index.html文 ...

  3. 牛客网-SQL专项训练23

    ①假设创建新用户nkw,现在想对于任何IP的连接,仅拥有user数据库里面的select和insert权限,则列表语句中能够实现这一要求的语句是(B) 解析: 考察知识点-数据库授权命令: GRANT ...

  4. 基于开源PolarDB-X打造中正智能身份认证业务数据基座

    简介: 在10月25日由阿里云开发者社区.PolarDB开源社区.infoQ联合举办的「开源人说」第三期--<数据库PolarDB专场>沙龙上,中正智能科技有限公司平台软件部研发总监韩毅带 ...

  5. Koordinator v0.7: 为任务调度领域注入新活力

    简介: 在这个版本中着重建设了机器学习.大数据场景需要的任务调度能力,例如 Coscheduling.ElasticQuota 和精细化的 GPU 共享调度能力.并在调度问题诊断分析方面得到了增强,重 ...

  6. ZooKeeper 在阿里巴巴的服务形态演进

    简介: 本文将给大家介绍下 ZooKeeper 的最佳实践场景,归为了 3 类,分别是:微服务领域,代表的集成产品是 Dubbo/SpringCloud:大数据领域,代表的集成产品是 Flink/Hb ...

  7. 系列解读SMC-R:透明无感提升云上 TCP 应用网络性能(一)| 龙蜥技术

    ​简介:已有的应用若想使用RDMA技术改造成本高,那么有没有一种技术是不做任何改造就可以享受RDMA带来的性能优势? ​ 文/龙蜥社区高性能网络SIG 引言 Shared Memory Communi ...

  8. WPF 框架开发 调试和开发 XAML 构建过程的 PresentationBuildTasks 方法

    阅读本文,你可以了解如何编写开发和调试 XAML 构建为 Baml 和 g.cs 文件的过程和工具.本文也适合想要了解 WPF 的 XAML 构建过程的开发者阅读,本文提供了可以断点调试 WPF 的 ...

  9. notepad运行python代码的步骤

    notepad运行python代码的步骤: 1.用notepad++打开python文件.或者新建文件,保存为.py格式. 2.在菜单栏上面有一个运行,我们点击运行->运行,或者使用快捷键F5. ...

  10. EPAI手绘建模APP常用工具栏_1

    1.常用工具栏 图 1 常用工具栏 (1) 撤销 (2) 重做 (3) 删除 (4) 复制 ① 选中场景中的模型后,复制按钮变成可用状态,否则变成禁用状态.可以选择多个模型一起复制. (5) 变换 图 ...