CSE_lecture3:File System 2

File System API and Disk I/O

implement the file system API

API有以下几个:

  • CHDIR(即cd), MKDIR
  • CREAT, LINK, UNLINK, RENAME
  • SYMLINK
  • MOUNT, UNMOUNT(读取super block)
  • OPEN, READ, WRITE, APPEND, CLOSE
  • SYNC(将内存数据放到磁盘上)

这些API通过system call暴露给用户

区分一下open()fopen()

  • open是来自操作系统的system call,而fopen是c语言规范中libc的一个API
  • open返回一个fd,而fopen返回一个指向文件的指针
  • 只有fopen在windows和linux上都同名,而open不见得
  • fopen具备更好的性能,因为fopen使用了一个buffer I/O,可能并没有直接写入磁盘

inode是file的metadata,里面除了上节课讲的几个之外,还有:

  • owner id: 用于权限控制
  • types of permission: 谁有权限,有哪些权限(mode: read, write, execute)
  • time stamps: access time(管理read), modify time(管理write), change of time(管理link)
1
2
3
4
5
6
7
8
9
10
11
struct inode
integer block_nums[N]
integer size
integer type
integer refcnt
integer userid
integer groupid
integer mode
integer atime
integer mtime
integer ctime

open时的过程为:

  • 通过id和mode检查是否有打开的权限
  • 更新atime
  • 返回fd

每个进程默认有三个fd,分别管理stdin(0), stdout(1), stderr(2),fd还可以通知一些device,这是因为这些device本质都是文件

为什么使用fd而不是inode pointer?

  • 为了安全不让你了解kernel的数据结构,实现os和应用的隔离
  • 同时让文件修改全权交给kernel,实现non-bypassability
  • 对于同一个文件,不同的操作对应的fd不一样

fd要求返回当前可用fd中最小的那个,而这就导致了当open过多时要加锁,导致性能下降

文件被打开时,file cursor默认指向0的位置,而read和write时,cursor向前移动,同时read和write也是从当前cursor开始的,使用SEEK来任意移动file cursor

sharing file cursor: 父进程将fd传给子进程,在写log时有用;not sharing file cursor: 两个进程打开同一个文件

使用fd_table和file_table维护file cursor(这些都是in memory的),其中file_table全局,而file_table里面的refcnt与inode里面的区分,而是表明有多少进程共享同一个index,而当refcnt = 0时回收资源

1
2
3
4
5
6
7
8
9
10
11
12
13
procedure OPEN(string filename, integer flags, integer mod) -> integer
inode_number <- PATH_TO_INODE_NUMBER(filename, wd)
if inode_number = FAILURE and flags = O_CREATE then # 创建文件?
inode_number <- CREATE(filename, mode)
if inode_number = FAILURE then
return FAILURE
inode <- INODE_NUMBER_TO_INODE(inode_number)
if PERMITTED(inode, flags) then # 是否具备权限?
file_index <- INSERT(file_table, inode_number)
fd <- FIND_UNUSED_ENTRY(fd_table) # 在fd_table寻找最小空闲fd
fd_table[fd] <- file_index # 将fd和file index关联起来
return fd
else return FAILURE
1
2
3
4
5
6
7
8
9
10
11
12
13
procedure READ(integer fd, character[] &buf, integer n) -> integer
file_index <- fd_table[fd]
cursor <- file_table[file_index].cursor
inode <- INODE_NUMBER_TO_INODE(file_table[file_index].inode_number)
m <- MINIMUM(inode.size - cursor, n)
atime of inode <- now()
if m = 0 then return END_OF_FILE
for i from 0 to m - 1 do
b <- INODE_NUMBER_TO_BLOCK(cursor + i, inode_number)
COPY(b, buf, MINIMUN(m - i, BLOCKSIZE))
i <- i + MINIMUM(m - i, BLOCKSIZE)
file_table[file_index].cursor <- cursor + m
return m

每次OPEN, READ, CREATION时到底调用了多少次read和write?见以下两张图

每次READ时都要在inode里面write,很麻烦,因此添加参数-no-atime,并在close的时候再一并更新atime

注意在create到bar inode时要先read再write,这是因为write的粒度都是4KB,而inode只有1KB,因此只能先读上来4KB;而create最后foo inode因为先前已经read过了,所以就只用write了;像bar inode[0]就直接write了,因为直接write 4KB就行

以下三个写操作顺序,哪个最合理?检查什么时候断电,权衡后选择第一个,但也不完美

  • update block bitmap, write new data, update inode(size and pointer): 1后断电,浪费一个block;2后断电,白写了data,还是浪费一个block
  • update block bitmap, update inode(size and pointer), write new data: 2后断电,等价于访问旧的数据,非常危险
  • update inode(size and pointer), update block bitmap, write new data: 1后断电,inode里面的pointer指向一个还没覆写的block,有可能读到旧的数据,非常危险

SYNC将cache写入磁盘,避免断电时数据丢失

delete after open but before close: inode在close前不会回收即可

rename有一个更好的方案:

  1. LINK(from_name, to_name): 注意这里是to_name的inode num被修改成from_name的,然后refcnt变为2
  2. UNLINK(from_name): refcnt变回1

在1和2之间断电,至少保证了from_name和to_name指向同一个文件

other file systems(not inode)

  • 方法1:使用连续的block存储文件
  • 方法2:使用linked list,即FAT(file allcation table):
    • FAT为一个linked list,和block一一对应,这导致FAT的容错性差
    • file number为list起始位置得索引
    • 将空的block视为一个大文件,对应一个free list
    • 写入文件时,从free list中获取空闲的block,并将它们link起来
    • FAT的目录包含了file name, file number, next,因此也不支持硬链接,根目录在sector 0

CSE_lecture3:File System 2
http://example.com/2025/09/23/CSE-lecture3-File-System-2/
作者
jietiDdd
发布于
2025年9月23日
许可协议