操作系统对象与文件系统
详解文件描述符、文件系统层级(FHS)、管道与Socket通信机制
文件描述符(fd)
指向操作系统对象的指针。将硬件设备,信息通道,系统信息等资源抽象为统一的字节流。
可以使用同一套syscall实现交互。fd(file descriptor)作为一个非负的整数索引内核资源,系统调用通过这个指针 (fd) 确定进程希望访问操作系统中的哪个对象。通过open, close, read/write, lseek, dup 管理文件描述符。
- open
- p = malloc(sizeof(FileDescriptor));
- close
- delete(p);
- read/write
- *(p.data++);
- lseek
- p.data += offset;
- dup
- q = p;
| API | 作用对象 | 对内核的影响 |
|---|---|---|
open | 进程 + 系统 | 建立 fd -> file -> inode 的链路 |
read/write | 数据流 | 使用 file->f_pos 进行 I/O,并更新 |
lseek | 元数据 | 仅修改 file->f_pos |
dup | 进程表 | 让多个 fd 共享同一个 file 对象 |
close | 进程表 | 解除 fd 绑定,触发引用计数回收 |
作为一个进程视角的handler,fd的设计让进程可以只管理索引,内核只管理状态,fork之后的子进程继承了父进程管理的索引,所以复制了相同的fd表副本。文件偏移量是保存在文件实例中的,所以父子进程共享读写进度。dup也只是在同一个进程的fd表中复制了指针让两个fd指向同一个实例。open则是创建了一个新的实例,这无论是不同进程还是同一进程,只要调用 open,内核就会强制新建一个会话实例。因为实例是新的,所以它的读写状态(Offset)自然也就是独立的,互不干扰。

文件系统与设备
文件系统层级标准 (FHS) FHS

/bin:所有用户可用的基本命令。/etc:系统的配置文件。/var:经常变化的数据(日志、缓存)。
设备文件
真实设备:
/dev/sda(Block Device):代表你的第一块硬盘。/dev/tty(Character Device):代表当前的终端设备。
虚拟设备:
/dev/null:写给它的数据会被直接丢弃(用于禁止输出),读它会立即返回 EOF。/dev/urandom:随机数生成器。读它会产生无限的伪随机字节流。- procfs (
/proc):内核状态的窗口。全局可见的、基于文件路径的 Handler。
管道 (Pipe)
一个特殊的 “文件” (流)
- 由读者/写者共享
- 读口:支持 read
- 写口:支持 write
缺陷:
数据只能单向流动:从写入端流入,从读取端流出
无边界字节流:粘包问题
依赖fd:限制了通信的对象,命名管道可以解决但是引入文件路径依赖
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
int main() {
int fd[2];
// 缺陷 1:未初始化的缓冲区,且读取后未处理字符串结束符 \0
char buf[128]; // [!code warning]
if (pipe(fd) == -1) {
exit(1);
}
pid_t pid = fork();
if (pid == 0) {
// 子进程 (Child)
// 缺陷 2:端口管理混乱,未关闭不需要的写端 fd[1]
read(fd[0], buf, sizeof(buf));
// 模拟处理耗时,让竞争更明显
// sleep(1);
write(fd[1], "Pong", 4);
} else {
// 父进程 (Parent)
// 致命缺陷 3:自我竞争 (Self-Reading Race)
write(fd[1], "Ping", 4); // [!code error]
// 这里的 read 极大概率会读到上一行自己刚写入的 "Ping"
// 而不是子进程回复的 "Pong"
read(fd[0], buf, sizeof(buf)); // [!code error]
}
// 缺陷 4:父进程直接退出,未回收子进程
return 0; // [!code warning]
}
数据竞争
这是单管道复用时的典型错误。
由于父进程的执行速度通常快于子进程的调度唤醒,发生以下时序问题:
-
父进程执行 write,数据存入内核缓冲区。
-
父进程立即执行 read。
-
此时子进程尚未运行,父进程读走了自己刚才写入的数据。
-
结果:父进程获取了错误数据,子进程因管道为空而永久阻塞。
僵尸进程
代码在结束前未调用 wait() 或 waitpid()。
当子进程退出时,内核会保留其进程描述符以便父进程获取退出状态。
若父进程不进行回收,这些残留的进程描述符将占用系统资源表,成为僵尸进程。
Tip
双管道隔离:创建两个管道分别用于 和 的单向通信。
关闭文件描述符:
fork后,立即在各自进程中关闭不使用的读端或写端。协议定义:避免发送裸字符串,使用定长结构体或消息头处理数据边界。
套接字 (Socket)
内核中的通信端点,本质上仍是一个文件描述符 (fd),但提供了更强大的跨网络通信能力。
核心特性:
- 数据传输:可以使用标准的
read()和write(),但更常用recv()和send()以支持额外的标志位控制。 - 连接管理:使用
close()断开连接并释放内核资源。 - 多路复用:因为 Socket 提供了 fd 作为 handler,所以它可以被放入
select、poll、epoll中统一管理,实现高效的事件驱动 I/O。