Post

访问操作系统对象;文件描述符

访问操作系统对象;文件描述符

Everything is a file

文件系统可以用于构建任何信息系统,我们可以把“file” 当作一个可以顺序读写的字节流对象,而不仅仅是磁盘上的文件

FHS

Filesystem Hierarchy Standard FHS: enables software and user to predict the location of installed files and directories

路径含义
/bin基本命令
/etc配置
/home用户目录
/dev设备
/proc进程信息(虚拟文件系统!)

文件描述符:访问操作系统对象的 “指针”

我们可以这样简单地去想象一个file:

1
2
3
4
struct FILE { 
    char *data;
    size_t offset;
}

我们可以类比成在 CrazyOS 里允许多个 buffer

  • open: f = malloc(sizeof(struct FILE));
  • close: free(f);
  • read/write: *(f->data++);
  • lseek: f-> += offset;
  • dup: f_new = f; 👉 dup = 创建一个新的 fd,但指向同一个 file(打开实例)【共享offset和flags】

内核里实际上差不多长这样:

1
2
3
4
5
struct file {
    inode *inode;
    off_t offset;
    int flags; // in which way to operate this file
}
1
int fd = open("a.txt", flags, mode);

fd(file descriptor)实际上不是指针,而是一个整数索引!(当然,我们可以用指针去类比)

在 Unix 和类 Unix 操作系统中,文件描述符是一个非负整数,用于表示一个打开的文件、管道、网络连接或其他类似的资源。当一个程序打开一个文件或创建一个数据流时,操作系统会返回一个文件描述符,程序可以通过这个描述符来读取、写入或操作对应的文件或资源。

1
2
3
4
5
6
fd table (per-process)

fd=0 ──→ file*
fd=1 ──→ file*
fd=2 ──→ file*
fd=3 ──→ file*

所以,read(3, buf, 100);就可以想象成是:fd=3 → 找到 file* → 操作它

当我想要去访问一个file的时候:

1
2
3
4
5
6
7
user: open("a.txt")
↓
kernel: 创建 file 对象
↓
kernel: 找一个 fd(比如 3)
↓
返回 fd=3 给用户

“The open() system call opens the file specified by pathname.”

总结一下,我们获得了这个模型:

1
2
3
4
5
6
7
8
9
10
11
fd(用户态整数)
 ↓
fd table(进程私有) // 也就是同一个fd,在不同进程里会对应不同file
 ↓
file*(内核对象:状态 + offset)
 ↓
具体资源(inode / socket / pipe) // 统一接口,支持不同类型资源
 ↓
数据来源(page cache / buffer / device)
 ↓
byte stream

fd = handle(句柄)

如果没有fd,而是直接拿到了struct file *f;,那就完蛋了:

  • 可以随便改内核数据
  • 可以伪造指针
  • 进程之间完全不隔离

file = open instance(打开实例)

file是“一次 open 调用产生的上下文对象”,里面包含:

  • 当前读写位置(offset)
  • 打开模式(读/写)
  • 指向资源的指针(inode/socket)

那为什么不能 fd → resource呢,这是因为“打开”本身是有状态的!“读文件”不是一次性行为,而是一个持续过程(有进度),失去了结构体维护的信息,要么从头读,要么用户自己管理offset!所以offset就是这个“状态”的关键!

stateful就是:输入 + 当前状态 → 输出 + 新状态

fd = open(“a.txt”);,本质上就是在创建一个“读取状态机”(file)!

resource = actual data source(真实资源)

resource是对各种资源的一个抽象,否则将会有很多很多API

另一个 “地址空间”

  • 0 (stdin), 1 (stdout), 2 (stderr), …【每个进程一启动,就已经“自带”了 3 个打开的 fd】

    • fd名字含义
      0stdin标准输入
      1stdout标准输出
      2stderr标准错误
      1
      2
      3
      4
      5
      
      fd table:
          
      0 ──→ file(stdin)  ──→ 键盘 / 输入流
      1 ──→ file(stdout) ──→ 终端(屏幕)
      2 ──→ file(stderr) ──→ 终端(屏幕)
      
  • open() 总是分配最小的未使用描述符

    • 新打开的文件从 3 开始分配
    • 文件关闭后,编号可以重复利用

fork()

没错,又是fork()!如果我们考虑到fork(),事情就复杂起来了!假设我先fork(),然后再write(),那么到底会运行出一个什么样的结果呢?

为了防止各种诡异的结果出现,设计者选择了浅拷贝:子进程复制 fd table,但 fd 指向的 file(打开实例)是共享的,包括offset!而我们之前说了,offset是决定状态的关键要素,因此,当我们执行:

1
2
3
4
5
6
7
8
9
int fd = open("a.txt", O_RDONLY);

if (fork() == 0) {
    // 子进程
    read(fd, buf, 5);
} else {
    // 父进程
    read(fd, buf, 5);
}

实际上的结果是,两个进程共享offset,谁先读,谁推进offset

操作系统的真正复杂性

API 之间会产生互相影响
  • fork() 以后,offset 会发生什么?
    • 这就是 fork() 看似优雅,实际复杂的地方
    • 软件系统的每一处设计都要小心考虑和其他部分的交互
Windows Handle API
  • 默认 handle 是不继承的 (和 UNIX 默认继承相反)
    • 可以在创建时设置 bInheritHandles,或者运行时修改
    • “最小权限原则”
  • lpStartupInfo 用于配置 stdin, stdout, stderr
    • Linux 引入了 O_CLOEXEC

管道

1
fd → file(pipe) → kernel buffer → file(pipe) → fd

截屏2026-03-27 20.41.58

管道 = 内核中的一个“带缓冲区的文件对象”,提供了进程之间通信的机制

  • 两边各有一个 file(open instance)
  • 中间是同一个 buffer(资源)

互相等待(blocking / 阻塞)与同步

写者等待

1
2
3
4
5
6
7
int fd[2];
pipe(fd);

// 父进程写
while (1) {
    write(fd[1], buf, 4096);
}
  • 如果缓冲区满了,write 会阻塞
  • 阻塞时间 = 直到有读端读取数据

读者等待

1
2
3
4
5
int fd[2];
pipe(fd);

// 子进程读
read(fd[0], buf, 1024);
  • 如果缓冲区为空,read 阻塞
  • 阻塞时间 = 直到写端写入数据 所有写端关闭

写者遇到无读者

1
write(fd[1], ...)
  • 如果管道的读端全都关闭:
    • 内核触发 SIGPIPE
    • write 返回错误 EPIPE
  • 这是 Unix 的设计保证:不允许写“无人接收的数据”

读者遇到无写者

1
read(fd[0], ...)
  • 如果缓冲区空且写端都关闭:
    • read 返回 0(EOF)
  • 阻塞不会永远等待

普通文件 fork 后的竞争问题

怎么又是你?

1
2
3
4
5
int fd = open("a.txt", O_RDONLY);  // 内容:"ABCDEFGHIJ"
fork();

// 子进程:read(fd, 5) → "ABCDE", offset 变为 5
// 父进程:read(fd, 5) → "FGHIJ" (因为 offset 已是 5)

问题:读写顺序取决于进程调度,结果不确定

管道的本质

特性普通文件管道
offset 共享✅ 是❌ 无 offset 概念
数据流向双向单向
数据消费可重复读读一次即删除
用途持久化存储进程间通信 (IPC)
1
2
父进程 fd[1] (写)  →  内核缓冲区  →  子进程 fd[0] (读)
                         (数据读完即消失)

因此我们就知道了:

问题答案
管道数据会回流到父进程吗?❌ 不会,单向流动
管道解决文件共享竞争吗?❌ 不是,它是 IPC 机制
fork 后 fd 共享导致什么问题?offset 竞争,结果不确定
管道如何避免这个问题?无 offset 概念,数据单向流动
This post is licensed under CC BY 4.0 by the author.

Trending Tags