Sealessland logo Sealessland
OS

操作系统对象与文件系统

详解文件描述符、文件系统层级(FHS)、管道与Socket通信机制

OS

文件描述符(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)自然也就是独立的,互不干扰。

fd

文件系统与设备

文件系统层级标准 (FHS) 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]
}
数据竞争

这是单管道复用时的典型错误。

由于父进程的执行速度通常快于子进程的调度唤醒,发生以下时序问题:

  1. 父进程执行 write,数据存入内核缓冲区。

  2. 父进程立即执行 read

  3. 此时子进程尚未运行,父进程读走了自己刚才写入的数据。

  4. 结果:父进程获取了错误数据,子进程因管道为空而永久阻塞。

僵尸进程

代码在结束前未调用 wait()waitpid()

  • 当子进程退出时,内核会保留其进程描述符以便父进程获取退出状态。

  • 若父进程不进行回收,这些残留的进程描述符将占用系统资源表,成为僵尸进程。

Tip

  1. 双管道隔离:创建两个管道分别用于 ParentChildParent \rightarrow ChildChildParentChild \rightarrow Parent 的单向通信。

  2. 关闭文件描述符fork 后,立即在各自进程中关闭不使用的读端或写端。

  3. 协议定义:避免发送裸字符串,使用定长结构体或消息头处理数据边界。

套接字 (Socket)

内核中的通信端点,本质上仍是一个文件描述符 (fd),但提供了更强大的跨网络通信能力。

核心特性:

  • 数据传输:可以使用标准的 read()write(),但更常用 recv()send() 以支持额外的标志位控制。
  • 连接管理:使用 close() 断开连接并释放内核资源。
  • 多路复用:因为 Socket 提供了 fd 作为 handler,所以它可以被放入 selectpollepoll 中统一管理,实现高效的事件驱动 I/O。