本书老套路,介绍文件系统的实现之前,先介绍一下相关的api和使用。
文件和目录的几个核心点
- 文件可以认为包含2种级别的定义。一个是低级别的inode number,可以认为是id。另一个是用户级别的,也就是filename
- 文件的后缀其实只是一种惯例。这个其实熟悉Linux的都知道,可执行文件不像windows一样一定要有exe,在shell中,只要权限是可执行,就可以直接执行。后缀只是方便操作系统和用户而已
- 目录是树状组织的一系列节点
文中篇幅非常长,介绍了许多API,这边总结一下其中提到的一些概念以及贯穿文件系统API的一些思想
使用文件描述符来操作文件
文件系统中,在程序级别,使用open打开某个文件,会返回该文件的描述符,一个和类似id的东西,然后通过该描述符进行操作,我们看下cat foo指令的系统调用
prompt> strace cat foo
...
open("foo", O_RDONLY|O_LARGEFILE) = 3
read(3, "hello\n", 4096) = 6
write(1, "hello\n", 6) = 6
hello
read(3, "", 4096) = 0
close(3) = 0
...
prompt>
open返回foo的文件描述符3,read也是用了3去读取内容。
几个问题:
- 为什么文件描述符是3?因为1和2会预留并分配给标准输入和标准输出
- 为什么write写到的目标描述符是1?这就很明显了,结合到第一个问题,write这里是把foo中读到的数据写入到了标准输出
- 为什么还要read一次?再read一次发现没数据后,关闭文件描述符。结束读取
随机读写文件
这里主要为了引出操作系统中文件大致的结构
struct file {
int ref; // 引用计数
char readable; // 可读标志
char writable; // 可写标志
struct inode *ip; // 指向 inode 结构体的指针
uint off; // 偏移量
};
上述的结构表示了当前操作系统所有打开的文件,所有这些结构组成的就是open file table,打开文件表,可以看到,通过其中偏移量的直接操作,我们就可以完成随机读写了
父子进、线程之间分享文件描述符
int main(int argc, char *argv[]) {
// Open the file "README" in read-only mode
int fd = open("README", O_RDONLY);
assert(fd >= 0);
// Duplicate the file descriptor
int fd2 = dup(fd);
// Now fd and fd2 can be used interchangeably
return 0;
}
使用dup可以使得父子之间对同一个文件描述符进行复制,复制的主要区别就是:他们使用的是文件打开表中的同一个节点,所以他们拥有相同的偏移量
fsync()确保同步写
按照我们之前描述的操作系统和磁盘间的一些交互可以了解到,普通的write并不代表着就已经写到硬盘了再返回,而是可能通过丢个任务给DMA,并且还需要再等待磁盘的调度合并才会真正落盘。
fsync()则是一个为了提供一致性的显式强制写调用,它只在确保数据已经写入到磁盘以后才会返回,它会比write慢非常多,但是可以保证数据的安全。
重命名
这里介绍了一个rename系统调用,主要就是负责原子性的将文件改名。大部分编辑器通过rename来保存你的修改。步骤如下:
- 将临时文件.tmp,使用fsync持久化到磁盘
- 将临时文件.tmp,使用rename原子性的修改名字为原文件的文件名
- 删除原文件
文件元信息
使用stat可以查看
struct stat {
dev_t st_dev; // ID of device containing file
ino_t st_ino; // inode number
mode_t st_mode; // protection
nlink_t st_nlink; // number of hard links
uid_t st_uid; // user ID of owner
gid_t st_gid; // group ID of owner
dev_t st_rdev; // device ID (if special file)
off_t st_size; // total size, in bytes
blksize_t st_blksize; // blocksize for filesystem I/O
blkcnt_t st_blocks; // number of blocks allocated
time_t st_atime; // time of last access
time_t st_mtime; // time of last modification
time_t st_ctime; // time of last status change
};
这个保存文件信息的结构就是前面说的inode,在后续介绍实现时会细讲
目录directory
我们在创建一个目录后,每个目录都会自动创建2个目录,分别是./和../,使用ls -la就可以看到
目录本身就是一个树状结构,包含的信息也不算多
删除文件——以及软硬链接
结合前面的inode概念,我们就可以更好的理解软链接以及硬链接了
硬链接
硬链接使用系统调用link(),创建一个硬链接,硬链接的本质是:创建一个文件,将这个文件指向同一个inode。
所以你会发现,硬链接创建后,2个文件的inode是相同的,下面的第一列就是inode
prompt> ls -i file file2
67158084 file
67158084 file2
prompt>
在我们删除某个文件时,本质上是对硬链接做一个unlink,所以我们使用rm等操作时,对应的系统调用时unlink,断开这个文件和inode的链接
所以我们删除文件后,文件并不会立马消失,而是只是删除掉了inode的一个link,这个inode什么时候彻底被清除,就等待操作系统后续的决策了(或许是使用lazy load都可以,当发现再需要一个的时候,找一个废弃的重新去初始化,都不需要定时释放)
那么理所应当的,当创建了1个硬链接后,删除其中一个文件,另一个硬链接的文件不会消失,并且,修改其中一个文件,另一个文件也会被修改
延伸一下:
在cp去copy一个文件的时候,我们显然可以使用一种方式来节省资源:
- cp时,创建一个新文件,并且使用硬链接链接到原来的文件
- 在修改时,才根据Copy on Write机制,去真正复制这个文件并保存新的inode
软链接
软链接那么就刚好相反了,它只是一个符号链接,相当于创建了一个文件名,链接到了对应的文件名上,而不是inode上。
我们可以通过一个图来直观看下这个引用
所以,在软连接删除file1时,其实就意味着inode没了,并且file2成为了悬空指针
文件的权限控制
文件的权限就是我们经常看到的:
先说说2个root,分别表示了这个文件的所有者是root,文件所属组的组名也是root
上图中第一列,它一共有10位,由几部分组成
第一位
第一位是描述了文件的类型,有3种:
d:表示目录
-:表示常规文件
l:表示是符号链接(短链接)
后九位
后九位分三组,每组三位。这三组每组的三位分别表示(文件可读r,文件可写w,文件可执行r)
这三组分别表示:
文件所有者对该文件的权限
文件所属组对该文件的权限
其他人对该文件的权限
比如图中的第一行,drwxr-xr-x就表示了:
d:该文件是个目录
rwx:文件所有者对该文件可读可写可执行(执行后就跳到上级目录了呗)
r-x:文件所属组对该文件可读并且可执行
r-x:其他人对该文件也可读可执行
如何修改权限?
熟悉linux的都知道,常用一般都是chmod 755,或者777。其中改的含义其实就是修改rwx,只不过是使用二进制模式表示,777的三个7分别表示修改3个组的权限,单个的7是rwx这3位数字的二进制表示,比如rwx=111=7,rw-=110=6,r--=100=4
如何创建一个文件系统——挂载
挂载就是mount,在linux里,外部存储都是依靠挂载到系统的某个节点,以此来让操作系统为它创建整个目录树的。
mount所做的很简单,就是将一个现有目录作为目标挂载点,并将一个新的文件系统粘贴到该点的目录树中。
假设/dev/sda1有a,b这2个目录,挂载到系统中一个指定目录的话
mount -t ext3 /dev/sda1 /home/users
此时整个目录树就复制过来了
prompt> ls /home/users/
a b
总结
文中就有总结,这里直接贴文中的英文总结吧,然后翻译由gpt倾情完成
KEY FILE SYSTEM TERMS
文件系统关键术语
1. File: An array of bytes that can be created, read, written, and deleted. It has a low-level name (i.e., a number) that uniquely refers to it. This low-level name is often called an i-number.
文件:一个字节数组,可以创建、读取、写入和删除。它具有一个低级名称(即一个数字),唯一地标识它。这个低级名称通常称为 i-number(i号)。
2. Directory: A collection of tuples, each containing a human-readable name and a low-level name to which it maps. Each entry refers either to another directory or to a file. Each directory has a low-level name (i.e., an i-number) and two special entries:
• .: Refers to the directory itself.
• ..: Refers to its parent directory.
目录:一个元组的集合,每个元组包含一个可读的名称和一个低级名称,表示映射到的对象。每个条目要么指向另一个目录,要么指向一个文件。每个目录也有一个低级名称(即i号)和两个特殊条目:
• .:指向目录自身。
• ..:指向其父目录。
3. Directory Tree: Also known as a directory hierarchy, it organizes all files and directories into a large tree structure, starting at the root.
目录树:也称为 目录层级结构,它将所有文件和目录组织成一个大型树形结构,从 根 开始。
4. File Access: To access a file, a process must use a system call (usually open()) to request permission from the operating system. If permission is granted, the OS returns a file descriptor, which can be used for read or write access, depending on permissions and intent.
文件访问:要访问一个文件,进程必须使用系统调用(通常是 open())向操作系统请求权限。如果权限被授予,操作系统会返回一个 文件描述符,根据权限和意图,可以用于读取或写入访问。
5. File Descriptor: A private, per-process entity that refers to an entry in the open file table. This entry tracks which file the access refers to, the current offset of the file (i.e., which part of the file the next read or write will access), and other relevant information.
文件描述符:一个每进程私有的实体,指向 打开文件表 中的一个条目。这个条目跟踪访问指向的文件、文件的 当前偏移量(即下一个读取或写入将访问文件的哪个部分)和其他相关信息。
6. Current Offset: Calls to read() and write() naturally update the current offset. Processes can also use lseek() to change its value, enabling random access to different parts of the file.
当前偏移量:对 read() 和 write() 的调用自然会更新当前偏移量。进程还可以使用 lseek() 来更改其值,从而实现对文件不同部分的随机访问。
7. Force Updates: To force updates to persistent media, a process must use fsync() or related calls. Doing so correctly while maintaining high performance can be challenging, so think carefully when using these calls.
强制更新:要强制更新持久媒体,进程必须使用 fsync() 或相关调用。正确地做到这一点,同时保持高性能可能具有挑战性,因此在使用这些调用时要仔细考虑。
8. Hard Links and Symbolic Links: To have multiple human-readable names in the file system refer to the same underlying file, use hard links or symbolic links. Each has its own strengths and weaknesses, so consider their appropriate usage.
硬链接和符号链接:要在文件系统中让多个可读名称指向同一个基础文件,可以使用硬链接或符号链接。每种链接都有其优缺点,因此要考虑它们的适当使用。
9. Deleting Files: Deleting a file is essentially performing one last unlink() operation from the directory hierarchy.
删除文件:删除一个文件本质上是在目录层级结构中执行最后一次 unlink() 操作。
10. File Sharing Controls: Most file systems have mechanisms to enable and disable sharing. Basic controls are provided by permissions bits, while more sophisticated access control lists allow for precise control over who can access and manipulate information.
文件共享控制:大多数文件系统都有启用和禁用共享的机制。基本控制由 权限位 提供,而更复杂的 访问控制列表 则允许对谁可以访问和操作信息进行精确控制。