143 / 163 · C11 · 约 8 分钟
插曲:文件与目录
本章用原创视角讲解操作系统如何把持久设备虚拟成文件与目录两层抽象,并展示UNIX风格的创建、读写与删除接口如何把内部inode编号隐藏在人类可读路径之后。
文件:内核眼中的持久字节流
对内核而言,每个文件只是一段可以随机读写的字节序列,其真实身份是一个整数inode编号。应用程序几乎从不直接使用这个编号,而是依靠目录把友好的名字翻译成编号。文件系统本身从不猜测这些字节代表图片、源码还是音乐,它只保证断电后数据仍然在。
目录如何拼出整棵命名树
目录的内容是一张简单的对照表:左边是用户看见的名字,右边是对应的inode。把目录再放进别的目录里,就能长出任意深度的树,根永远是单独的斜杠。一条绝对路径不过是从根出发、沿途查表的一串名字,因此同一棵树里可以同时存在两个都叫report.txt的文件,只要它们不在同一个目录。
open返回的小整数才是真正的能力
调用open并带上O_CREAT标志即可创建文件,返回值是一个仅对本进程有效的文件描述符。之后所有read、write、lseek都只认这个整数。进程退出或显式close后描述符失效。真正删掉名字的操作叫unlink,它只减少链接计数;数据要等到计数归零且无人打开才会释放。
统一命名带来的心智简化
一旦几乎所有东西——普通文件、设备、管道甚至某些进程信息——都出现在同一棵目录树下,程序员就只需记住一套open/read/write规则。这种“万物皆文件”的约定把复杂的底层差异藏了起来,也让脚本和工具能够用相同的方式处理完全不同的对象。
常见误区
- 把文件名里的点后缀当成内核强制的类型检查,其实只是约定
- 以为文件描述符可以像普通整数一样随便传给别的进程使用
- 以为调用unlink后磁盘块立刻被清零,忽略了硬链接和仍打开的描述符
运行一个例子
最低标准 C11 · 完整程序 · 下载 .c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void) {
const char *path = "ostep_ch39_demo.tmp";
int fd = open(path, O_CREAT | O_RDWR | O_TRUNC, 0644);
if (fd < 0) {
perror("open");
return 1;
}
const char msg[] = "hello from files\n";
ssize_t n = write(fd, msg, sizeof(msg) - 1);
if (n != (ssize_t)(sizeof(msg) - 1)) {
perror("write");
close(fd);
unlink(path);
return 1;
}
if (lseek(fd, 0, SEEK_SET) < 0) {
perror("lseek");
close(fd);
unlink(path);
return 1;
}
char buf[32];
ssize_t r = read(fd, buf, sizeof(buf) - 1);
if (r < 0) {
perror("read");
close(fd);
unlink(path);
return 1;
}
buf[r] = '\0';
printf("wrote %zd bytes, read back: %s", n, buf);
close(fd);
if (unlink(path) != 0) {
perror("unlink");
return 1;
}
printf("unlinked successfully\n");
return 0;
}
在本地编译
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror ostep-39-files-directories.c -o example && ./example预期结果
wrote 17 bytes, read back: hello from files
unlinked successfully
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
若两个不同目录项都指向同一个inode,执行一次unlink后文件内容是否立刻消失?请说明判断依据。
查看参考答案
不会立刻消失。unlink只删除其中一个名字并让链接计数减一。只要计数仍大于零,或者还有进程握着打开的文件描述符,数据块就会继续保留,另一个名字依然可以访问同一份内容。
继续查证
标准草案与官方章节会更新;版本标记只说明示例最低要求。