除了虚拟化CPU和内存,另外一个是持久存储,永久存储信息。持久存储设备与内存不同,内存在断电时内容会丢失,而持久存储设备会保持这些数据不变。
1. 文件和目录
文件就是一个线性字节数组,每个字节都可以读取或者写入。每个文件都有某种低级名称,通常是某种数字,由于历史原因,文件的低级名称通常称为inode号,即inode number。
文件系统的责任仅仅是将这些数据永久存储在磁盘上,并确保你再次请求数据时,得到你原来放在那里的内容,但是要实现这个并不简单。
目录就像一个文件一样,也有一个低级名字,即inode号,但是它的内容非常具体:它包含一个对的列表。
目录层次结构从根目录开始,并使用某种分隔符来命名后续子目录,直到命名所需的文件或目录。
文件名通常包含两部分:一部分是任意名称,一部分是指示文件类型的。但是这通常只是一个惯例
2. 文件系统接口
包含创建、访问和删除文件的基础
3. 创建文件
创建文件可以通过open系统调用完成,通过调用open() 并传入O_CREAT标志,程序可以创建一个新文件,如下:
int fd = open("foo", O_CREAT | O_WRONLY | O_TRUNC);
O_CREAT:创建文件
O_WRONLY:只能写入
O_TRUNC: 如果该文件已经存在,首先将其截断为零字节大小,删除现有内容
open的一个重要方面是它的返回值:文件描述符,它是一个整数,每个进程是私有的,文件描述符可以理解成一种权限,即一个不透明的句柄,它也可以让你执行某些操作;文件描述符也可以看作指向文件类型对象的指针。
4. 读写文件
跟踪程序系统调用的工具
- Linux上的strace
- MacOS上的dtruss或truss
每个正在运行的进程已经打开了三个文件:
- 标准输入
- 标准输出
- 标准错误
系统调用read的原型为:
ssize_t read(int fd, void *buf, size_t count);
- 第一个参数fd是文件描述符,告诉系统读取哪个文件
- 第二个参数buf指向一个用于放置read()结果的缓冲区
- 第三个参数是缓冲区大小
- read的返回值是它读取的字节数
系统调用write原型
ssize_t write(int fd, const void *buf, size_t count);
5. 读取和写入,但不按顺序
如果你在文本文件上构建了索引并利用它来查找特定单词,最终可能从文件中的某些随机偏移中读取数据,为此,我们可以使用lseek系统调用
off_t lseek(int fildes, off_t offset, int whence);
- whence=SEEK_SET时,offset为偏移字节数
- whence=SEEK_CUR时,offset为当前位置加上字节数
- whence=SEEK_END时,offset为整个文件大小加上字节数
lseek()调用只是在OS内存中更改一个变量该变量跟踪特定进程的下一个读取或者写入开始的偏移量。
6. 用fsync()立即写入
当程序调用write时,它只是告诉文件系统:请在将来的某个时刻,将此数据写入持久存储,由于性能原因,文件系统会将这些写入在内存缓冲一段时间后将写入实际发送到存储设备。只有在极少数情况下数据会丢失,比如写入磁盘之前机器崩溃。
但是在某些场景要求会更高,为了支持这些类型的应用程序,大多数文件系统都提供了一些额外的控制API,在UNIX中,提供给应用程序的接口称为fsync(int fd)。当进程针对特定文件描述符调用fsync()时,文件系统通过强制将所有脏数据写入磁盘来响应,一旦所有这些写入完成,fsync例程就会返回。
7. 文件重命名
系统调用rename
rename(char *old, char *new);
old: 文件原来名称
new: 新名称
rename提供了一个保证:它通常是一个原子调用,无论系统是否崩溃
8.获取文件信息
文件系统存储了每个文件大量的信息,这些数据通常称为文件元数据,要查看特定文件的元数据,我们可以使用stat或者fstat系统调用。
每个文件系统通常将这种类型的信息保存在一个名为inode的结构中。
9. 删除文件
删除文件相关的系统调用是unlink(),成功时返回零
10. 创建目录
你永远不能直接写入目录,因为目录的格式被视为文件系统元数据,所以你只能简介更新目录,例如,通过在其中创建文件、目录或其他对象类型。
创建目录的系统调用是mkdir()
目录创建时,虽然看起来是空的,但是它至少有两个条目:一个引用自身的条目,一个引用其父目录的条目。
11. 读取目录
相关的系统调用有opendir(), readdir()和closedir()
12. 删除目录
相关系统调用是rmdir,但注意该系统调用要求被删除之前目录是空的,否则调用会失败。
13. 硬连接
系统调用link()有两个参数:一个旧路径和一个新路径。当你将一个新的文件名链接到一个旧的文件名时,实际上创建了另一种引用同一个文件的方法。
你可以看到实际上已完成的链接,只是对inode号创建了新的引用。
创建文件时实际上做了两件事:
- 创建一个结构,它将跟踪几何所有关于文件的信息,包括其大小、文件块在磁盘上的位置等
- 将人类可读的名称链接到该文件,并将该链接放入目录中
调用unlink会删除人类可读的名称和给定inode号之间的链接,并减少引用计数,只有当引用计数达到零时,文件系统才会释放inode和相关数据块,从而真正删除该文件。
14. 符号链接
还有一种非常有用的链接类型,称为符号链接,有时称为软链接,事实表明,硬连接有点局限,你不能创建目录的硬连接。你不能硬连接到其他磁盘分区中的文件,所以人们又创建了软链接。
符号链接和硬链接实际上完全不同:第一个区别是符号链接本身是一个不同类型的文件;(除了文件和目录外的第三种类型);由创建符号链接的方式,由可能造成所谓的悬空引用(符号链接和硬链接完全不同,删除名为file的原始文件会导致符号链接指向不再存在的路径名)
15. 创建并挂载文件系统
创建文件系统的工具mkfs
挂载文件系统使用mount程序,它使用同名系统调用:以现有目录作为目标挂载点,本质上是将新的文件系统粘贴到目录树的这个点上。
mount的好处是:它将所有文件系统一到一棵树上,而不是有用多个独立的文件系统,这样让命名更加统一。
17. 小结
UNIX系统中文件系统接口看似非常基本,但是要想掌握它,还是需要不断的练习。