CSE_lab1攻略

lab1: Basic Filesystem

在lab1中,我们需要实现一个单机inode-based filesystem,共有三层架构:

  • block layer: 包括block manager和block allocator
  • inode layer: 包括inode manager
  • filesystem layer: 包括create, write, read操作,同时对等directory和file

lab的每个部分正好对应了这三层

关于怎么获取lab和配置docker不再赘述,简单说明怎么编译和测试。在chfs目录下使用以下命令进行编译:

1
2
3
mkdir build
cd build
cmake ..

build目录下使用以下指令完成单元测试:

1
2
make build-tests -j
make test -j

要进行集成测试,首先在build目录下使用以下指令:

1
make fs -j

再进入scripts/lab1执行以下命令,请不要使用sudo

1
./integration_test.sh

接下来提供lab1的攻略,在实现lab时,阅读对应的头文件来使用可以简化代码的api

part 1: block layer

part 1A: block manager

在这一部分,我们需要实现block manager,来进行block的读写和清除

阅读manager.h和几个构造函数,我们得知block manager的结构为:

1
2
3
4
5
6
class BlockManager{
const usize block_sz = 4096 // block的大小,也就是4KB
u8 *block_data // 这里管理的是整个磁盘的所有data
usize block_cnt // 整个磁盘有多少block,因此在根据block id进行读写时,要进行判断
bool in_memory // lab1用不到
}

在实现write_block, write_partial_blockzero_block函数时,利用std::memcpy将data写入到block_data对应的位置即可(这里不用管block_data的状态),注意以下几点:

  • 判断block_id是否超出block_cnt
  • write_partial_block中,要额外判断offset + len是否超出block_sz
  • zero_block中,直接使用std::memset将block设置为全零即可(这里和上课讲的不太一样,上课时讲的是覆写block,而这里会进行清零)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
auto BlockManager::write_block(block_id_t block_id, const u8 *data)
-> ChfsNullResult {

// TODO: Implement this function.
// 检查是否越界
CHFS_ASSERT(block_id < this->block_cnt, "Invalid block id");
// data最多只写入block_sz个字节
std::memcpy(this->block_data + block_id * this->block_sz, data, this->block_sz);

return KNullOk;
}

auto BlockManager::write_partial_block(block_id_t block_id, const u8 *data,
usize offset, usize len)
-> ChfsNullResult {

// TODO: Implement this function.
CHFS_ASSERT(block_id < this->block_cnt, "Invalid block id");
// 检查offset和len是否合法
CHFS_ASSERT(offset + len <= this->block_sz, "Invalid offset or length");
std::memcpy(this->block_data + block_id * this->block_sz + offset, data, len);

return KNullOk;
}

auto BlockManager::zero_block(block_id_t block_id) -> ChfsNullResult {

// TODO: Implement this function.
CHFS_ASSERT(block_id < this->block_cnt, "Invalid block id");
// 全部置零
std::memset(this->block_data + block_id * this->block_sz, 0, this->block_sz);

return KNullOk;
}

read_block函数直接反向std::memcpy即可

1
2
3
4
5
6
7
8
auto BlockManager::read_block(block_id_t block_id, u8 *data) -> ChfsNullResult {

// TODO: Implement this function.
CHFS_ASSERT(block_id < this->block_cnt, "Invalid block id");
std::memcpy(data, this->block_data + block_id * this->block_sz, this->block_sz);

return KNullOk;
}

这一部分只需要几次ASSERT和几次std::memcpy即可实现,现在可以通过以下单元测试了:

  • BlockManagerTest.ReadWritePageTest
  • BlockManagerTest.ZeroTest
  • BlockManagerTest.InMemoryTest

part 1B: block allocator

在这一部分,我们需要实现block allocator,根据block bitmap来决定分配哪个block

还是阅读allocator.h,我们知道block bitmap被存储在多个连续的block里面,即区间[bitmap_block_id, bitmap_block_id + bitmap_block_cnt - 1],注意以下几点:

  • 由于是使用bit来存储block是否被使用,因此需要注意byte和bit的转换,即KBitsPerByte
  • bitmap的最后一个block不一定被使用完,需要分类讨论,并使用last_block_num

再进入bitmap.h,活用以下函数:

1
2
3
4
5
6
Bitmap(u8 *data, usize payload)                                     // 将读取进入buffer的block内容构造为一个bitmap,便于调用api
auto set(usize index) // 用于allocate
auto clear(usize index) // 用于deallocate
auto check(usize index) -> bool // 用于错误处理
auto find_first_free() -> std::optional<usize> // 用于allocate
auto find_first_free_w_bound(usize bits) -> std::optional<usize> // 用于allocate时的最后一个bitmap block

参考已经写好的代码,我们其实可以留意到如何进行block的读写,即使用一个buffer来存储内容,届时我们只需要处理buffer.data即可

1
2
3
std::vector<u8> buffer(bm->block_size());
bm->read_block(bid, buffer.data());
bm->write_block(bid, buffer.data());

接下来实现allocate函数,首先我们进行分类讨论,利用Bitmap类来寻找第一个空的bit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (i == this->bitmap_block_cnt - 1) {
// If current block is the last block of the bitmap.

// TODO: Find the first free bit of current bitmap block
// and store it in `res`.
// 对于最后一个bitmap block,只搜索last_block_num个bit,这里使用bitmap.h中的相关api
res = Bitmap(buffer.data(), bm->block_size())
.find_first_free_w_bound(this->last_block_num);

} else {

// TODO: Find the first free bit of current bitmap block
// and store it in `res`.
// 而其他bitmap block,搜索全部bit
res = Bitmap(buffer.data(), bm->block_size()).find_first_free();
}

考虑到我们通过一个循环来遍历bitmap的所有block,因此当res为真时,我们再进行分配,这里分为几步:

  • 将bitmap的对应位置设为1,使用前面提到的set函数
  • 将bitmap写回对应的block,怎么读的就怎么写
  • 计算出要返回的block id,这里就要注意bit和byte的转换了

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
// The block id of the allocated block.
block_id_t retval = static_cast<block_id_t>(0);

// TODO:
// 1. Set the free bit we found to 1 in the bitmap.
Bitmap bitmap(buffer.data(), bm->block_size());
bitmap.set(*res);
// 2. Flush the changed bitmap block back to the block manager.
bm->write_block(i + this->bitmap_block_id, buffer.data());
// 3. Calculate the value of `retval`.
retval = i * (bm->block_size() * KBitsPerByte) + *res;

return ChfsResult<block_id_t>(retval);

接下来实现deallocate函数,其实就是根据bid找到对应的bitmap block,进行clear即可,善用除数和余数定位,步骤如下:

  • 使用block_id / (bm->block_size() * KBitsPerByte)block_id % (bm->block_size() * KBitsPerByte)定位到是第几个bitmap block的第几个bit,并read_block,在这一部分我们需要使用check函数来判断是否invalid
  • 使用clear函数置零,再write_block

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// TODO: Implement this function.
// 1. According to `block_id`, zero the bit in the bitmap.
std::vector<u8> buffer(bm->block_size());
// 每个bitmap block管理block_sz * 8个数据块
bm->read_block(this->bitmap_block_id + block_id / (bm->block_size() * KBitsPerByte), buffer.data()).unwrap();
Bitmap bitmap(buffer.data(), bm->block_size());
// 如果该位已经是0了,说明已经被释放
if (!bitmap.check(block_id % (bm->block_size() * KBitsPerByte))) {
return ChfsNullResult(ErrorType::INVALID_ARG);
}
bitmap.clear(block_id % (bm->block_size() * KBitsPerByte));
// 2. Flush the changed bitmap block back to the block manager.
// unwrap()用于处理ChfsNullResult
bm->write_block(this->bitmap_block_id + block_id / (bm->block_size() * KBitsPerByte), buffer.data()).unwrap();
// 3. Return ChfsNullResult(ErrorType::INVALID_ARG)
// if you find `block_id` is invalid (e.g. already freed).

现在我们可以通过以下单元测试了:

  • BlockAllocatorTest.Allocation

part 2: inode layer

part 2A: inode and inode manager

首先分析一下inode层的架构:

1
| Super block | Inode Table | Inode allocation bitmap | Block allocation bitmap | Other data blocks |

其实和课上的区别就是inode table不再直接存储inode,而是存储inode id和block id的映射关系,也就是说,inode也被当作block处理了

这里注意inode table和inode bitmap都是存储的raw_id,而暴露给上层的都是logic_id,因此活用RAW_2_LOGICLOGIC_2_RAW即可,不过没搞清楚也无所谓,注释会告诉你的

几个架构的区间为:

  • inode table: [1, n_table_blocks],这是因为有一个super block
  • inode allocation bitmap: [1 + n_table_blocks, 1 + n_table_blocks + n_bitmap_blocks]

接下来就来实现几个函数,首先是allocate_inode,代码已经实现了遍历inode bitmap来找到free_idx了,同时也给inode分配好了bid,因此步骤如下:

  • 实现inode block的写入:创建一个Inode实例,利用构造函数将type刷进去,再使用flush_to_buffer函数将这个inode刷进buffer里面(详见inode.h),最后将buffer写入bid对应的block里面
  • 实现inode table的写入:使用set_table函数将raw_id存进去,raw_id的计算类似于block bitmap
  • 返回logic_id

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// TODO:
// 1. Initialize the inode with the given type.
// 使用buffer存储inode数据
std::vector<u8> buffer(bm->block_size());
Inode inode(type, bm->block_size());
inode.flush_to_buffer(buffer.data());
// 将buffer的内容写入到bid指定的block中
auto write_res = bm->write_block(bid, buffer.data());
if (write_res.is_err()) {
return ChfsResult<inode_id_t>(write_res.unwrap_error());
}
// 2. Setup the inode table.
// 先根据free_idx和count计算出inode id
inode_id_t raw_id = count * (bm->block_size() * KBitsPerByte) + free_idx.value();
// 根据inode id计算出inode index
inode_id_t logic_id = RAW_2_LOGIC(raw_id);
// 实现bid和inode index的映射关系
auto set_res = this->set_table(raw_id, bid);
if (set_res.is_err()) {
return ChfsResult<inode_id_t>(set_res.unwrap_error());
}
// 3. Return the id of the allocated inode.
return ChfsResult<inode_id_t>(logic_id);
// You may have to use the `RAW_2_LOGIC` macro
// to get the result inode id.

接下来是free_inode,这里提供的是logic_id,注意转换,步骤如下:

  • 清除inode table:将bid设置为KInvalidBlockID即可,参考manager.h
  • 清除inode bitmap:和1B部分的deallocate基本一致,遗憾的是inode manager没有提供block allocator的接口,所以重复这段代码,注意下区间定位即可

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// TODO:
// 1. Clear the inode table entry.
// You may have to use macro `LOGIC_2_RAW`
// to get the index of inode table from `id`.
inode_id_t raw_id = LOGIC_2_RAW(id);
// 直接将该entry设置为Invalid
auto set_res = this->set_table(raw_id, KInvalidBlockID);
if (set_res.is_err()) {
return ChfsNullResult(set_res.unwrap_error());
}
// 2. Clear the inode bitmap.
// 寻找在inode bitmap的第几个block的第几个bit
auto inode_bits_per_block = bm->block_size() * KBitsPerByte;
auto bitmap_block_idx = raw_id / inode_bits_per_block;
auto bit_idx = raw_id % inode_bits_per_block;
// 每次使用buffer管理读写操作
std::vector<u8> buffer(bm->block_size());
// 从1开始,因为0是super block
// inode bitmap从1 + n_table_blocks开始,到1 + n_table_blocks + n_bitmap_blocks结束
bm->read_block(1 + n_table_blocks + bitmap_block_idx, buffer.data()).unwrap();
Bitmap bitmap(buffer.data(), bm->block_size());
bitmap.clear(bit_idx);
bm->write_block(1 + n_table_blocks + bitmap_block_idx, buffer.data()).unwrap();

接下来是get函数,在inode table查找即可,这里的inode table为一个大数组,被存放在多个block当中,所以还是使用除数和余数方法定位,并将读上来的buffer类型转换为block_id_t数组(类型转换非常重要,后面还会出现),实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// TODO: Implement this function.
// Get the block id of inode whose id is `id`
// from the inode table. You may have to use
// the macro `LOGIC_2_RAW` to get the inode
// table index.
inode_id_t raw_id = LOGIC_2_RAW(id);
if (raw_id >= max_inode_supported - 1) {
return ChfsResult<block_id_t>(ErrorType::INVALID_ARG);
}
// 计算在inode table的第几个block的第几个entry
auto inode_per_block = bm->block_size() / sizeof(block_id_t);
auto block_idx = raw_id / inode_per_block;
auto entry_idx = raw_id % inode_per_block;
// 使用buffer存储读入的block数据
std::vector<u8> buffer(bm->block_size());
// 注意inode table从1开始,因为0是super block,以1 + n_table_blocks结尾
bm->read_block(1 + block_idx, buffer.data()).unwrap();
// 实现转换,保证数组的最小单位是block_id_t
block_id_t *table = reinterpret_cast<block_id_t *>(buffer.data());
res_block_id = table[entry_idx];

return ChfsResult<block_id_t>(res_block_id);

最后是set_table函数,和get函数非常相似,只不过从读table[entry_idx]变为table[entry_idx] = bid,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// TODO: Implement this function.
// Fill `bid` into the inode table entry
// whose index is `idx`.
// 类似于get函数,只不过是写入而不是读取
if (idx >= max_inode_supported - 1) {
return ChfsNullResult(ErrorType::INVALID_ARG);
}
// 计算在inode table的第几个block的第几个entry
auto inode_per_block = bm->block_size() / sizeof(block_id_t);
auto block_idx = idx / inode_per_block;
auto entry_idx = idx % inode_per_block;
// 使用buffer存储写入的block数据
std::vector<u8> buffer(bm->block_size());
bm->read_block(1 + block_idx, buffer.data()).unwrap();
// 实现转换,保证数组的最小单位是block_id_t
block_id_t *table = reinterpret_cast<block_id_t *>(buffer.data());
table[entry_idx] = bid;
bm->write_block(1 + block_idx, buffer.data()).unwrap();

这一部分实现后,即可通过以下测试:

  • InodeManagerTest.InitAndTable
  • InodeManagerTest.Allocation

part 3: filesystem layer

在filesystem层,我们可以尽情使用block_manager_, inode_manager_, block_allocator_的相关api了

本部分的错误处理过于冗余,因此提供的代码会进行简化

part 3A: create

本部分实现alloc_inode函数,按照以下步骤即可:

  • 为inode分配一个空闲block:使用block allocator的allocate函数获取bid即可,注意我们的数据都经过ChfsResult包装过,便于进行错误检查,要想获得里面的数据,使用unwrap()即可
  • 分配inode:使用inode manager的allocate_inode函数即可
  • 初始化inode block:这里其实在上一步就已经实现了

部分实现如下:

1
2
3
4
5
6
7
8
9
// TODO:
// 1. Allocate a block for the inode.
auto block_res = this->block_allocator_->allocate();
// unwrap用于获取Result的值,即获取bid,提供给allocate_inode使用
block_id_t bid = block_res.unwrap();
// 2. Allocate an inode.
inode_res = this->inode_manager_->allocate_inode(type, bid);
// 3. Initialize the inode block
// and write the block back to block manager.

现在能通过以下单元测试了:

  • BasicFileSystemTest.Init
  • FileSystemTest.CreateAndGetAttr

part 3B: read and write

这部分应该是整个lab最难的部分,因为需要同时考虑direct block和indirect block(幸运的是,有足够的证据证明每个inode只有一个indirect block),因此回到inode.h,寻找一些和这俩有关的api,后面会用到

为了克服难题,我们需要认真阅读两个函数中已经实现的部分

首先是read_file函数,该函数根据inode id干了这些事:

  • 读取inode,将其变成了inode_p指针,注意这一阶段分配了一个std::vector<u8> indirect_block(0),便于到时候我们缓存indirect block的内容
  • 遍历inode存放的所有block id,分类讨论是否是indirect block来准确获得存放file的bid
  • 根据bid进行read_file,追加到content中,注意这个content是一个std::vector<u8>,而不是一个std::string,在转换时注意类型转换

我们要实现的是第2、3步,分别来说一下,首先是获取bid这一步,分类讨论:

  • direct block:直接读取blocks数组的对应位置即可
  • indirect block:使用get_indirect_block_id函数将indirect读取进入indirect_block.data()这一缓冲区,将其转换成block_id_t数组,最后定位到数组对应位置即可,注意在定位时要减去get_direct_block_num

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Get current block id.
block_id_t bid = KInvalidBlockID;
if (inode_p->is_direct_block(read_sz / block_size)) {
// TODO: Implement the case of direct block.
// 直接从inode中获取block id
bid = inode_p->blocks[read_sz / block_size];
} else {
// TODO: Implement the case of indirect block.
// 先进入indirect block,再从indirect block中获取block id
block_manager_->read_block(inode_p->get_indirect_block_id(),
indirect_block.data());
// 将indirect_block转换为block_id_t数组
auto indirect_block_p =
reinterpret_cast<block_id_t *>(indirect_block.data());
// 在indirect block中,从direct block的数量开始找
bid =
indirect_block_p[read_sz / block_size - inode_p->get_direct_block_num()];
}

然后是读取文件到content这一步,比较简单,善用内置api即可:

1
2
3
4
5
// TODO: Read from current block and store to `content`.
auto read_res = this->block_manager_->read_block(bid, buffer.data());
// content用于存放file读取的内容
content.insert(content.end(), buffer.data(), buffer.data() + sz);
read_sz += sz;

其次是write_file函数,这个函数很冗长,干了这些事:

  • 依然是读取inode
  • 比较inode_p->get_size()content.size(),看是否需要分配新的block或者释放多余的block(从这里就能看出,write_file是直接进行覆写),当然这里又要分类讨论是否是indirect block了
  • 将content分block_size写入对应的block中,这里还是要分类讨论是否是indirect block
  • 更新inode,所以我们在前面直接作用于inode_p即可,不用对inode block进行读写,但是,indirect block在这里可没更新,因此在前面必须要进行indirect block的读写(你可以理解成除了inode block以外的block平等地需要在前面就读写)

我们要实现的依然是2、3步,注意分类讨论即可,由于复杂的错误处理逻辑,代码相当冗长,本攻略直接跳过了,交给代码补全吧

先说分配更多的block这一步,遍历inode里面的blocks数组的idx时:

  • 使用block allocator进行allocate,得到bid
  • 根据idx判断是否是indirect block
    • direct block: 直接set_block_direct
    • indirect block: 使用get_or_insert-indirect_block分配一个indirect block,然后read_block得到indirect block的block_id_t数组,修改对应位置后write_block

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// TODO: Implement the case of allocating more blocks.
// 1. Allocate a block.
auto res = this->block_allocator_->allocate();
block_id_t bid = res.unwrap();
// 2. Fill the allocated block id to the inode.
// You should pay attention to the case of indirect block.
// You may use function `get_or_insert_indirect_block`
// in the case of indirect block.
// 分类讨论是否是direct block
if (inode_p->is_direct_block(idx)) {
// Direct block
inode_p->set_block_direct(idx, bid);
} else {
// Indirect block
auto indirect_res =
inode_p->get_or_insert_indirect_block(this->block_allocator_);
// 读取indirect block内容到indirect_block
auto read_res = this->block_manager_->read_block(
inode_p->get_indirect_block_id(), indirect_block.data());
// 将indirect_block转换为block_id_t数组
auto indirect_block_p =
reinterpret_cast<block_id_t *>(indirect_block.data());
// 在indirect block中,从direct block的数量开始找
indirect_block_p[idx - inode_p->get_direct_block_num()] = bid;
// 将修改后的indirect block写回block manager
// 而inode本身暂时不用写回磁盘,等最后一起写回
auto write_res = this->block_manager_->write_block(
inode_p->get_indirect_block_id(), indirect_block.data());

}

释放多余的block就是将对应的位置变为KInvalidBlockID即可,变化不大,注意获取到bid进行deallocate即可。我们不用管indirect block本身释放的问题,因为代码已经实现了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (inode_p->is_direct_block(idx)) {

// TODO: Free the direct extra block.
block_id_t bid = inode_p->blocks[idx];
auto res = this->block_allocator_->deallocate(bid);
} else {

// TODO: Free the indirect extra block.
// 进入到indirect block,回收里面存放的block
auto indirect_block_id = inode_p->get_indirect_block_id();
auto read_res =
this->block_manager_->read_block(indirect_block_id, indirect_block.data());
auto indirect_block_p =
reinterpret_cast<block_id_t *>(indirect_block.data());
block_id_t bid = indirect_block_p[idx - inode_p->get_direct_block_num()];
auto res = this->block_allocator_->deallocate(bid);
// 既然之前有read_block,现在就要写回indirect block
indirect_block_p[idx - inode_p->get_direct_block_num()] = KInvalidBlockID;
auto write_res = this->block_manager_->write_block(indirect_block_id, indirect_block.data());
}

最后以block_size为单位将content写入block即可(由于已经std::memcpy过,故我们操作的是buffer.data()),分类讨论是否是indirect block即可,这里和read_file区别不大:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
std::vector<u8> buffer(block_size);
memcpy(buffer.data(), content.data() + write_sz, sz);

block_id_t bid = KInvalidBlockID;
if (inode_p->is_direct_block(block_idx)) {

// TODO: Implement getting block id of current direct block.
bid = inode_p->blocks[block_idx];

} else {

// TODO: Implement getting block id of current indirect block.
// 先进入到indirect block,再从indirect block中获取block id
block_manager_->read_block(inode_p->get_indirect_block_id(),
indirect_block.data());
// 将indirect_block转换为block_id_t数组
auto indirect_block_p =
reinterpret_cast<block_id_t *>(indirect_block.data());
// 在indirect block中,从direct block的数量开始找
bid = indirect_block_p[block_idx - inode_p->get_direct_block_num()];

}

// TODO: Write to current block.
auto write_res = this->block_manager_->write_block(bid, buffer.data());

write_sz += sz;
block_idx += 1;

实现复杂的逻辑后,我们可以通过以下测试了:

  • FileSystemTest.WriteLargeFile
  • FileSystemTest.SetAttr

part 3C: operations on directory entries

这一部分主要就是一些std::string操作,比较简单,但是一定要注意,content不是string类型,因此类型转换时要相当小心

关于parse_directory函数,它是dir_list_to_string的反函数(后面会用到),使用stream来分割字符串即可,注意std::stringinode_id_t的类型转换可以使用内置的string_to_inode_id函数即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void parse_directory(std::string &src, std::list<DirectoryEntry> &list) {

// TODO: Implement this function.
// 使用stream来分割字符串
std::stringstream ss(src);
std::string item;
while (std::getline(ss, item, '/')) {
auto pos = item.find(':');
if (pos != std::string::npos) {
DirectoryEntry entry;
entry.name = item.substr(0, pos);
std::string id_str = item.substr(pos + 1);
entry.id = string_to_inode_id(id_str);
list.push_back(entry);
}
}
}

关于read_directory函数,其使用read_file函数将directory抽象成的file的content.unwrap()读取上来,使用reinterpret_cast<const char*>进行类型转换,最后使用parse_directory函数将directory变成std::list<DirectoryEntry>即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
auto read_directory(FileOperation *fs, inode_id_t id,
std::list<DirectoryEntry> &list) -> ChfsNullResult {

// TODO: Implement this function.
// 根据inode id找到directory所在的file
auto content = fs->read_file(id);
if (content.is_err()) {
return ChfsNullResult(content.unwrap_error());
}
// 将存储的string转换成list<entry>
const auto &vec = content.unwrap();
std::string data(reinterpret_cast<const char*>(vec.data()), vec.size());
parse_directory(data, list);

return KNullOk;
}

append_to_directory主要就是直接操作std::string

1
2
3
4
5
6
7
8
9
10
11
12
13
auto append_to_directory(std::string src, std::string filename, inode_id_t id)
-> std::string {

// TODO: Implement this function.
// Append the new directory entry to `src`.
// 将'/'补上去
if (!src.empty()) {
src += '/';
}
src += filename + ':' + inode_id_to_string(id);

return src;
}

最后rm_from_directory将src遍历一次,将不是filename的部分写入到返回值即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
auto rm_from_directory(std::string src, std::string filename) -> std::string {

auto res = std::string("");

// TODO: Implement this function.
// Remove the directory entry from `src`.
// 使用stream来分割字符串,将不等于filename的部分重新拼接起来
std::stringstream ss(src);
std::string item;
bool first = true;
while (std::getline(ss, item, '/')) {
auto pos = item.find(':');
if (pos != std::string::npos) {
std::string name = item.substr(0, pos);
if (name != filename) {
if (!first) {
res += '/';
}
res += item;
first = false;
}
}
}

return res;
}

现在我们可以通过以下测试了:

  • FileSystemBase.Utilities
  • FileSystemBase.UtilitiesRemove
  • FileSystemTest.DirectOperationAdd

part 3D: combine directory and file together

这部分主要是操作directory抽象成的file,活用上一部分的几个函数即可,还是注意content.unwrap()的类型转换即可,本攻略依然忽略部分冗余的错误处理以简化逻辑

首先lookup函数要求我们使用read_directory函数获得std::list<DirectoryEntry>,遍历这个list即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
auto FileOperation::lookup(inode_id_t id, const char *name)
-> ChfsResult<inode_id_t> {
std::list<DirectoryEntry> list;

// TODO: Implement this function.
// 根据inode id找到directory所在的file
auto read_res = read_directory(this, id, list);
// 在list中寻找name对应的entry
auto it = std::find_if(list.begin(), list.end(),
[name](const DirectoryEntry &entry) {
return entry.name == name;
});
if (it != list.end()) {
return ChfsResult<inode_id_t>(it->id);
}

return ChfsResult<inode_id_t>(ErrorType::NotExist);
}

接下来mk_helper函数分为以下几步:

  • 使用lookup函数判断是否已经在里面
  • 为这个文件分配新的inode
  • 将新的filename <-> inode num映射关系写回directory即可(当然这里步骤重复了,你可以不使用lookup函数,这样可以少一次read_directory;或者使用append_to_directory简化代码也可以)

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
auto FileOperation::mk_helper(inode_id_t id, const char *name, InodeType type)
-> ChfsResult<inode_id_t> {

// TODO:
// 1. Check if `name` already exists in the parent.
// If already exist, return ErrorType::AlreadyExist.
// 直接使用lookup函数
auto lookup_res = lookup(id, name);
// 排除掉已存在和其他错误
if (lookup_res.is_ok()) {
return ChfsResult<inode_id_t>(ErrorType::AlreadyExist);
} else if (lookup_res.unwrap_error() != ErrorType::NotExist) {
return ChfsResult<inode_id_t>(lookup_res.unwrap_error());
}
// 2. Create the new inode.
auto alloc_res = this->alloc_inode(type);
inode_id_t new_inode_id = alloc_res.unwrap();
// 3. Append the new entry to the parent directory.
// 使用read directory函数得到list
auto list = std::list<DirectoryEntry>();
auto read_res = read_directory(this, id, list);
// 将新inode添加到目录中
list.push_back(DirectoryEntry{std::string(name), new_inode_id});
// 将list转换成string
std::string data = dir_list_to_string(list);
// 将string写回到文件中
std::vector<u8> new_content(data.begin(), data.end());
auto write_res = this->write_file(id, new_content);
return ChfsResult<inode_id_t>(new_inode_id);

// return ChfsResult<inode_id_t>(static_cast<inode_id_t>(0));
}

最后是unlink函数,步骤如下:

  • 使用lookup函数找到inode id来remove_file(参考operations.h
  • 使用read_directory得到list,删除filename对应的条目后写回文件即可(类似于mk_helper的最后一步,当然也可以使用rm_from_directory简化代码)

实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
auto FileOperation::unlink(inode_id_t parent, const char *name)
-> ChfsNullResult {

// TODO:
// 1. Remove the file, you can use the function `remove_file`
// 使用lookup找到对应的inode id
auto lookup_res = lookup(parent, name);
inode_id_t target_id = lookup_res.unwrap();
auto remove_res = this->remove_file(target_id);
// 2. Remove the entry from the directory.
// 使用read directory函数得到list
auto list = std::list<DirectoryEntry>();
auto read_res = read_directory(this, parent, list);
// 在list中删除对应的entry
list.remove_if([name](const DirectoryEntry &entry) {
return entry.name == name;
});
// 将list转换成string
std::string data = dir_list_to_string(list);
// 将string写回到文件中
std::vector<u8> new_content(data.begin(), data.end());
auto write_res = this->write_file(parent, new_content);
return KNullOk;
}

完成最后一部分后,可以通过以下单元测试:

  • FileSystemTest.mkdir

跑完所有单元测试后,跑跑集成测试即可

总结一下这个lab,难度不大,步骤注释都有写,根据头文件熟悉api即可,注意两个坑:

  • write_file一定要在读取indirect block后及时写回
  • std:vector<u8>转换成std::string时,一定要使用reinterpret_cast<const char*>,这样std::string才能正确解读u8 -> char

CSE_lab1攻略
http://example.com/2025/09/27/CSE-lab1攻略/
作者
jietiDdd
发布于
2025年9月27日
许可协议