一篇围绕虚拟文件系统与 POSIX 文件系统 API 展开的学习笔记。


前面的几篇已经分别讨论了文件系统的抽象、硬件基础和保护机制。接下来需要回答两个更靠近程序员的问题:操作系统怎样把多种文件系统统一成同一套访问模型,以及用户态最常用的文件系统接口究竟围绕哪些对象展开。

这篇文章先讨论操作系统对文件系统的抽象,即虚拟文件系统(VFSVirtual File System); 再按数据类型和对象层级整理最常见的一组文件系统 POSIX API。

虚拟文件系统

1. 概述

现代操作系统往往同时支持多种文件系统:本地磁盘文件系统、可移动介质文件系统、网络文件系统,甚至内存文件系统。 如果每种文件系统都直接暴露自己独立的接口,那么上层程序就必须为每种文件系统进行适配 —— 这显然不现实。

因此,操作系统会在应用程序和具体文件系统实现之间插入一层统一抽象:

text
应用程序 -> VFS -> 具体文件系统实现

VFS 的核心作用有两点:

作用含义
统一接口上层程序只看到 open/read/write/close 这类通用操作
隔离实现具体文件系统自己负责这些操作在本地磁盘或远程文件系统上的真正实现

也就是说,应用程序访问的是"文件系统接口",而不是某个具体文件系统的内部方法。

2. VFS 的四类对象

Linux 的 VFS 一般围绕四类对象组织:

对象含义
superblock object表示一个已经挂载的文件系统
dentry object表示单个目录项,即文件名到 inode object 的映射
inode object表示一个文件对象的元数据
file object表示一个已经打开的文件实例

其中最容易混淆的是 inode objectfile object

对象关注点
inode object文件本身的属性,如大小、权限、块位置
file object一次打开实例的状态,如当前偏移、打开方式

file object 是一个瞬时对象,在文件打开时建立,关闭时销毁。 同一个文件可以被多个进程同时打开,因此对于同一个文件,可以创建多个file object,并具有相互独立的offset

除此之外,文件名只是目录中用于索引 inode 的可读名称,而并不是文件元数据或数据的一部分。

3. 打开文件表

用户进程只能够访问文件描述符表(fd表),但内核还维护内核文件打开表:

层次含义
进程 fd每个进程自己的文件描述符表
内核文件打开表记录真正的打开文件状态

fd表项往往指向内核文件打开表中的某个条目,与此同时,内核打开文件表维护引用计数,以确定是否有进程使用此文件。

这样的好处在于,多个进程先后打开同一个文件,无需反复进行磁盘寻址,只需要修改fd表,使其指向内核打开文件表的对应条目即可。

二者关系可以表述为:

text
进程 -> fd表项 -> 内核打开文件表条目 -> inode / dentry / superblock

这也是为什么两个进程都能打开同一个文件,但各自的 fd 号仍然不同:fd 是进程私有索引,而不是文件对象本身。

除此之外,操作系统还会维护缓存层。传统上,文件系统缓存和内存映射 I/O 可能分开管理; 而现代 Unix 系统一般倾向于使用统一页面缓存(unified page cache),避免在内存中同时维护两份文件映像。

4. 数据与操作

如果把 POSIX 文件系统接口按操作对象分类,可以得到下面这张表:

层级操作对象句柄常见 API
文件系统superblock objectchar* pathmountumountstatfs
目录dentry objectDIR* dirmkdiropendirreaddirclosedir
文件元数据inode objectint fdstatchmodtruncateumask
文件数据file objectint fdopenreadwritelseekfsync

因此,文件系统POSIX API实际上是针对不同数据的特定操作。本文也将按照这个层级,对文件系统POSIX API进行分类整理。

本篇最常出现的头文件如下:

c
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <dirent.h>
#include <sys/ioctl.h>

文件系统接口的返回值风格大体如下:

接口类型成功返回失败返回
生成 fd 的接口非负文件描述符-1,并设置 errno
一般操作接口0 或非负结果-1,并设置 errno
读取接口实际读取字节数-1,并设置 errno

因此,对这类接口最常见的判断模式是:

c
int fd = open("a.txt", O_RDONLY);
if (fd < 0) {
    /* error */
}

文件系统 API

1. mount、umount

mountumount分别用于挂载与取消挂载文件系统

c
#include <sys/mount.h>

int mount(const char *source, const char *target,
          const char *filesystemtype, unsigned long mountflags,
          const void *data);
int umount(const char *target);
int umount2(const char *target, int flags);

mount 将一个文件系统挂载到指定目录:

含义
source源设备路径,如 "/dev/sda1";对于 tmpfsproc 等虚拟文件系统可为 NULL
target挂载点目录路径
filesystemtype文件系统类型名称,如 "ext4""nfs""tmpfs"
mountflags挂载选项,按位或组合
data文件系统特有数据,通常传 NULL
成功返回0
失败返回-1,并设置 errno

常用 mountflags

标志含义
MS_RDONLY只读挂载
MS_NOSUID忽略 set-user-ID 和 set-group-ID 位
MS_NODEV禁止访问该文件系统上的设备文件
MS_NOEXEC禁止执行该文件系统上的程序
MS_SYNCHRONOUS所有写入立即同步到磁盘
MS_REMOUNT重新挂载已挂载的文件系统
MS_BIND绑定挂载,将已挂载点镜像到另一处

umount 卸载已挂载的文件系统:

含义
target挂载点路径
成功返回0
失败返回-1,并设置 errno

umount2 额外支持 flags 参数,常用 MNT_FORCE(强制卸载)、MNT_DETACH(延迟卸载)。umount 要求目标没有进程正在使用,否则返回 EBUSY

2. statfs、fstatfs

statfsfstatfs用于获取文件系统属性

c
#include <sys/statfs.h>

int statfs(const char *path, struct statfs *buf);
int fstatfs(int fd, struct statfs *buf);

statfs 获取路径所在文件系统的统计信息;fstatfs 通过已打开的 fd 获取。参数与返回值:

接口第一参数第二参数成功返回失败返回
statfspath 路径buf 接收文件系统统计信息的结构体指针0-1,并设置 errno
fstatfsfd 描述符同上0-1,并设置 errno

struct statfs 常用字段:

字段类型含义
f_typelong文件系统类型 ID
f_bsizelong最优传输块大小(字节)
f_blocksfsblkcnt_t文件系统总数据块数
f_bfreefsblkcnt_t空闲块数
f_bavailfsblkcnt_t非特权用户可用空闲块数
f_filesfsfilcnt_t文件节点总数(inode)
f_ffreefsfilcnt_t空闲文件节点数
f_namelenlong最大文件名长度

statfs 常用于查询磁盘剩余空间:

c
#include <sys/statfs.h>
#include <stdio.h>

int main(void) {
    struct statfs stfs;
    if (statfs("/", &stfs) != 0) {
        perror("statfs");
        return 1;
    }
    unsigned long long free_bytes = (unsigned long long)stfs.f_bsize * stfs.f_bavail;
    printf("available: %llu bytes\n", free_bytes);
    return 0;
}

目录 API

1. mkdir、rmdir、rename

mkdirrmdir分别用于创建和删除目录,rename用于修改目录表项中文件名

c
#include <sys/stat.h>
#include <unistd.h>

int mkdir(const char *path, mode_t mode);
int rmdir(const char *path);
int rename(const char *oldpath, const char *newpath);

mkdir 的参数与返回值:

含义
path新目录的路径名
mode初始权限位,同样受 umask 影响
成功返回0
失败返回-1,并设置 errno

rmdir 的参数与返回值:

含义
path要删除的目录路径;目录必须为空
成功返回0
失败返回-1,并设置 errno

rename 的参数与返回值:

含义
oldpath旧路径名
newpath新路径名
成功返回0
失败返回-1,并设置 errno

几个注意事项:

  • rename 操作的是目录项,而不是"修改 inode 内部名字"。Unix 文件对象本身并不直接把"文件名"存在 inode 里,名字存在目录项中。
  • rename 在同一个文件系统内是原子操作——无论中间是否发生崩溃,目录结构不会出现"新旧路径各有一份"或"两个路径都消失"的中间态。
  • 如果 newpath 已存在且是文件,rename 会原子地替换它(旧 newpath 被删除)。
  • rmdir 要求目录为空(只含 ...),这与 unlink 不能删除目录形成对称。

2. opendir、readdir、closedir

opendirreaddirclosedir用于操作目录

c
#include <dirent.h>

DIR *opendir(const char *name);
struct dirent *readdir(DIR *dirp);
int closedir(DIR *dirp);

opendir 的参数与返回值:

含义
name目录路径
成功返回DIR * 目录流句柄
失败返回NULL,并设置 errno

readdir 的参数与返回值:

含义
dirpopendir 返回的目录流句柄
成功返回指向下一个目录项的 struct dirent *;到达目录末尾时返回 NULL,但不设置 errno
出错返回NULL,并设置 errno

因此,区分"读完"与"出错"的方法是:调用前把 errno 置 0,调用后检查 errno

closedir 的参数与返回值:

含义
dirp要关闭的目录流句柄
成功返回0
失败返回-1,并设置 errno

struct dirent 常用字段:

字段类型含义
d_inoino_tinode 编号
d_namechar[]目录项名称(文件名)
d_typeunsigned char文件类型(非 POSIX 标准,但 Linux/BSD 普遍支持)

opendir / readdir / closedir 示例:

c
#include <dirent.h>
#include <errno.h>
#include <stdio.h>

int main(void) {
    DIR *dir = opendir("/tmp");
    if (dir == NULL) {
        perror("opendir");
        return 1;
    }

    struct dirent *entry;
    errno = 0;
    while ((entry = readdir(dir)) != NULL) {
        printf("%s (inode: %lu)\n", entry->d_name, (unsigned long)entry->d_ino);
        errno = 0;
    }
    if (errno != 0) {
        perror("readdir");
    }

    closedir(dir);
    return 0;
}

目录虽然在文件系统层面是一种特殊文件,但在 POSIX 用户态接口里通常通过 DIR * 这一层包装访问,而不是直接用 read(fd, ...) 解析目录格式。

文件元数据 API

1. stat、fstat、lstat

statfstatlstat用于读取文件元数据

c
#include <sys/stat.h>

int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);

参数与返回值:

接口第一参数第二参数成功返回失败返回
statpath 路径buf 接收元数据的结构体指针0-1,并设置 errno
fstatfd 描述符同上0-1,并设置 errno
lstatpath 路径同上0-1,并设置 errno

三者的区别:

接口根据什么定位对象符号链接是否跟随
stat路径是,返回目标文件的信息
fstat已打开的 fd不涉及路径解析
lstat路径否,返回符号链接自身信息

struct stat 常用字段:

字段类型含义
st_devdev_t所在设备的设备号
st_inoino_tinode 编号
st_modemode_t文件类型与权限位
st_nlinknlink_t硬链接计数
st_uiduid_t所有者用户 ID
st_gidgid_t所有者组 ID
st_sizeoff_t文件大小(字节)
st_blksizeblksize_t首选 I/O 块大小
st_blocksblkcnt_t已分配的 512B 块数
st_atimetime_t最后访问时间
st_mtimetime_t最后修改时间
st_ctimetime_t最后状态变更时间

st_mode 既编码文件类型,也编码权限位。判断文件类型应使用 POSIX 宏而非直接位运算:

含义
S_ISREG(mode)是否为普通文件
S_ISDIR(mode)是否为目录
S_ISLNK(mode)是否为符号链接
S_ISFIFO(mode)是否为命名管道
S_ISSOCK(mode)是否为 socket
S_ISBLK(mode)是否为块设备
S_ISCHR(mode)是否为字符设备

stat 示例:

c
#include <sys/stat.h>
#include <stdio.h>

int main(void) {
    struct stat st;
    if (stat("/tmp/os_demo.txt", &st) != 0) {
        perror("stat");
        return 1;
    }
    printf("size: %lld, inode: %llu, nlink: %lu\n",
           (long long)st.st_size, (unsigned long long)st.st_ino,
           (unsigned long)st.st_nlink);
    return 0;
}

2. chmod、fchmod、umask

chmodfchmod用于修改文件权限位,umask用于修改进程的屏蔽位

c
#include <sys/stat.h>

int chmod(const char *path, mode_t mode);
int fchmod(int fd, mode_t mode);
mode_t umask(mode_t cmask);

chmod 的参数与返回值:

含义
path目标文件路径
mode新的权限位,如 0644
成功返回0
失败返回-1,并设置 errno

fchmod 的参数与返回值:

含义
fd已打开的文件描述符,用于代替 path 定位文件
modechmod
成功返回0
失败返回-1,并设置 errno

umask 的参数与返回值:

含义
cmask要设置的权限屏蔽位,如 022
返回值调用前的旧 umask 值(无论成功与否,umask 不会失败)

值得注意的是,umask作用域是进程,一个进程只有一个 umask,与此同时:

  • 同一进程中的所有线程共享 umask
  • 子进程会继承父进程的 umask
  • exec 不会重置 umask

文件创建时的最终权限不是 open(..., mode) 直接给出的值,而是:

c
final_mode = requested_mode & ~umask

也就是说,对于已经屏蔽的位,将会始终填入0

umask022open(..., 0666) 创建出的文件实际权限是 0644

umask 示例:

c
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

int main(void) {
    mode_t old = umask(0);             // 暂不屏蔽任何权限位
    int fd = open("/tmp/os_umask_test", O_CREAT | O_WRONLY, 0666);
    umask(old);                        // 恢复原 umask
    close(fd);

    struct stat st;
    stat("/tmp/os_umask_test", &st);
    printf("mode: %o\n", st.st_mode & 0777);  // 输出 666
    unlink("/tmp/os_umask_test");
    return 0;
}

3. truncate、ftruncate

truncateftruncate用于修改文件大小

c
#include <unistd.h>

int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);

参数与返回值:

接口第一参数第二参数成功返回失败返回
truncatepath 路径length 目标文件大小(字节)0-1,并设置 errno
ftruncatefd 已打开描述符同上0-1,并设置 errno

两者都可缩短文件,也可把文件扩展到更大长度。扩展时新增区域填 \0(形成空洞文件);缩短时超出部分被丢弃。

ftruncate 不改变当前文件偏移。此外,ftruncate 也用于为 POSIX 共享内存对象设定大小——shm_open 返回的 fd 传给 ftruncate 即可。

文件数据 API

1. open、creat、close

open用于打开/创建文件,creat用于创建文件,close用于关闭文件

c
#include <fcntl.h>
#include <unistd.h>

int open(const char *path, int oflag, ...);
int creat(const char *path, mode_t mode);
int close(int fd);

open 的参数与返回值:

含义
path路径名
oflag打开方式与附加标志,由一组 O_* 按位或组合
mode仅在 oflagO_CREAT 时有效,指定新文件的初始权限
成功返回当前进程可用的最小非负 fd
失败返回-1,并设置 errno

open 最常见的标志位:

标志含义
O_RDONLY只读打开
O_WRONLY只写打开
O_RDWR读写打开
O_CREAT文件不存在则创建,此时必须提供第三个参数 mode
O_EXCLO_CREAT 配合,若文件已存在则失败
O_TRUNC打开时截断为 0 长度
O_APPEND每次 write 前自动将偏移移到文件末尾
O_NONBLOCK以非阻塞方式打开
O_SYNC每次 write 等待数据与元数据落盘

creat 的参数与返回值:

含义
path路径名
mode文件初始权限位
成功返回当前进程可用的最小非负 fd,且以只写方式打开
失败返回-1,并设置 errno

creat(path, mode) 等价于 open(path, O_WRONLY | O_CREAT | O_TRUNC, mode)。它是早期 Unix 遗留下来的接口,新代码建议直接用 open

close 的参数与返回值:

含义
fd要关闭的文件描述符
成功返回0
失败返回-1,并设置 errno

close(fd) 关闭的是当前进程对该打开实例的一个引用。当最后一个引用消失后,内核才真正回收对应的打开文件表条目。

注意 close 的返回值在实际代码中经常被忽略,但如果 fd 对应的是网络文件系统或有写缓存的场景,close 失败可能意味着数据未成功落盘。

open / close 最小示例:

c
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main(void) {
    int fd = open("/tmp/os_demo.txt", O_CREAT | O_RDWR | O_TRUNC, 0644);
    if (fd < 0) {
        perror("open");
        return 1;
    }
    write(fd, "hello", 5);
    close(fd);
    return 0;
}

2. read、write、pread、pwrite

readwritepreadpwrite用于文件的读写

c
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t pread(int fd, void *buf, size_t count, off_t offset);
ssize_t pwrite(int fd, const void *buf, size_t count, off_t offset);

read 的参数与返回值:

含义
fd已打开的文件描述符
buf接收数据的缓冲区
count期望读取的最大字节数
返回 >0实际读取的字节数;可能小于 count(短读取)
返回 0读到文件末尾(EOF
返回 -1出错,并设置 errno

write 的参数与返回值:

含义
fd已打开的文件描述符
buf待写出数据的缓冲区
count期望写出的字节数
返回 >0实际写出的字节数;可能小于 count(短写入)
返回 -1出错,并设置 errno

pread / pwriteread / write 基础上增加了一个显式偏移参数 offset

含义
offset从文件的哪个字节偏移开始读/写;不依赖也不修改 fd 的当前偏移

四者的区别如下:

接口使用当前文件偏移是否原子化"寻址 + I/O"
read / write
pread / pwrite

pread / pwrite 的意义不只是"多一个参数":它把"读取哪个位置"从打开实例状态中剥离出来,同时保证"定位 + I/O" 是原子的,适合多线程并发读写同一 fd 的场景。

这里有两个容易踩的边界:

  • 短读取 / 短写入readwrite 不保证一次调用就读满或写满 count 字节。读到多少取决于内核缓冲区状态;写入多少取决于剩余空间。循环读写直到达到目标字节数是常见做法。
  • O_APPEND 下的 pwrite:若 fdO_APPEND 打开,pwriteoffset 参数会被忽略,数据仍然追加到文件末尾。

read / writepread / pwrite 对比示例:

c
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main(void) {
    int fd = open("/tmp/os_demo.txt", O_CREAT | O_RDWR | O_TRUNC, 0644);

    // write 写入后偏移前移
    write(fd, "AAAA", 4);          // offset → 4

    // pwrite 在指定位置写,不改变当前偏移
    pwrite(fd, "BB", 2, 0);        // offset 仍为 4

    // pread 在指定位置读,不改变当前偏移
    char buf[8] = {0};
    pread(fd, buf, 4, 0);          // buf = "BBAA",offset 仍为 4
    printf("pread: %s\n", buf);

    // read 从当前偏移继续读
    lseek(fd, 0, SEEK_SET);        // offset → 0
    read(fd, buf, 4);               // buf = "BBAA",offset → 4
    printf("read:  %s\n", buf);

    close(fd);
    unlink("/tmp/os_demo.txt");
    return 0;
}

3. lseek、fsync、fdatasync

lseek用于修改文件当前偏移,fsyncfdatasync用于将修改冲洗到存储设备

c
#include <unistd.h>

off_t lseek(int fd, off_t offset, int whence);
int fsync(int fd);
int fdatasync(int fd);

lseek 的参数与返回值:

含义
fd已打开的文件描述符
offset相对基准的偏移量,可为负(取决于 whence
whence偏移基准,取 SEEK_SETSEEK_CURSEEK_END
成功返回新的文件偏移(距文件开头的字节数)
失败返回(off_t)-1,并设置 errno

whence 的取值:

whence含义
SEEK_SET以文件开头为基准
SEEK_CUR以当前偏移为基准
SEEK_END以文件末尾为基准

fsyncfdatasync 的参数与返回值:

含义
fd已打开的文件描述符
成功返回0
失败返回-1,并设置 errno

两者的区别:

接口作用
fsync将数据与所有元数据(大小、时间戳等)刷到存储设备
fdatasync只刷数据与必要元数据(保证后续可读的最小元数据集),不强制刷新非关键元数据

如果只调用 write(),数据可能仍停留在页面缓存里;只有调用 fsync() / fdatasync() 后才真正要求内核把相关修改提交到存储设备。 fdatasyncfsync 开销更低,适合不需要更新 inode 元数据的场景(如追加写入数据库日志)。

c
int fd = open("/tmp/os_demo.txt", O_CREAT | O_WRONLY, 0644);
write(fd, "data", 4);
fsync(fd);   // 确保数据落盘后再做后续操作
close(fd);

链接 API

linkunlink用于在目录中添加/删除inode的引用(也称创建/删除硬链接)

除此之外,unlink也是删除文件的系统调用,因为inode引用计数为0时,文件本体将被删除。

c
#include <unistd.h>

int link(const char *existing, const char *newpath);
int unlink(const char *path);

link 的参数与返回值:

含义
existing现有文件的路径名
newpath新目录项的路径名
成功返回0
失败返回-1,并设置 errno

link 为现有文件增加一个新的硬链接目录项。两个路径名指向同一个 inode,共享一个链接计数。不允许跨文件系统创建硬链接,也不允许对目录创建硬链接(除 root 外)。

unlink 的参数与返回值:

含义
path要删除的目录项路径
成功返回0
失败返回-1,并设置 errno

unlink 删除的不是"文件内容",而是名字到文件对象之间的一条链接。只有当 inode 的链接计数降到 0,且没有进程再持有打开引用时,文件对象才真正可回收。

这也是"删除一个正在被进程打开的文件仍然可以继续读写"的原因:unlink 只移除目录项,数据块仍在。

link / unlink 示例:

c
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <stdio.h>

int main(void) {
    int fd = open("/tmp/os_link_orig", O_CREAT | O_WRONLY, 0644);
    write(fd, "data", 4);
    close(fd);

    link("/tmp/os_link_orig", "/tmp/os_link_copy");  // 创建硬链接

    struct stat st;
    stat("/tmp/os_link_orig", &st);
    printf("nlink after link: %lu\n", (unsigned long)st.st_nlink);  // 2

    unlink("/tmp/os_link_copy");                     // 删除链接

    stat("/tmp/os_link_orig", &st);
    printf("nlink after unlink: %lu\n", (unsigned long)st.st_nlink); // 1

    unlink("/tmp/os_link_orig");
    return 0;
}

symlinkreadlink用于创建/读取软链接;软链接本身是文件,其删除可以使用unlink

c
#include <unistd.h>

int symlink(const char *target, const char *linkpath);
ssize_t readlink(const char *path, char *buf, size_t bufsiz);

symlink 的参数与返回值:

含义
target符号链接指向的目标路径
linkpath新符号链接的路径名
成功返回0
失败返回-1,并设置 errno

readlink 的参数与返回值:

含义
path符号链接的路径
buf接收目标路径内容的缓冲区
bufsiz缓冲区大小
返回 >0写入 buf 的字节数(不含 \0
返回 -1出错,并设置 errno

symlinklink 的关键区别:

维度硬链接 (link)符号链接 (symlink)
本质新增目录项,指向同一个 inode创建独立的文件对象,内容为目标路径字符串
跨文件系统不支持支持
指向目录不支持(root 除外)支持
目标删除后数据仍在,可通过剩余链接访问链接变成悬空链接(broken link)
inode与目标共享独立,不与目标共享

readlink 不会在 buf 末尾自动追加 \0,调用者需要根据返回值手动添加。

symlink / readlink 示例:

c
#include <unistd.h>
#include <sys/stat.h>
#include <stdio.h>

int main(void) {
    symlink("/tmp/os_link_orig", "/tmp/os_symlink");

    char buf[256] = {0};
    ssize_t n = readlink("/tmp/os_symlink", buf, sizeof(buf) - 1);
    if (n != -1) {
        buf[n] = '\0';
        printf("symlink target: %s\n", buf);
    }

    struct stat st;
    lstat("/tmp/os_symlink", &st);              // lstat 获取符号链接自身信息
    printf("is symlink: %d\n", S_ISLNK(st.st_mode));

    unlink("/tmp/os_symlink");
    return 0;
}

描述符控制 API

1. dup、dup2、dup3

dupdup2dup3用于在同一个进程中拷贝fd表项

c
#include <unistd.h>

int dup(int oldfd);
int dup2(int oldfd, int newfd);
int dup3(int oldfd, int newfd, int flags);

dup 的参数与返回值:

含义
oldfd要复制的已有描述符
成功返回当前进程可用的最小非负 fd,与 oldfd 指向同一打开实例
失败返回-1,并设置 errno

dup2 的参数与返回值:

含义
oldfd要复制的已有描述符
newfd期望分配的描述符号;若 newfd 已打开,会先原子地关闭它再复制
成功返回newfd
失败返回-1,并设置 errno

dup3dup2 基础上增加 flags 参数:

含义
flags目前只支持 O_CLOEXECexec 时自动关闭此 fd),传 0 则行为同 dup2

复制后的多个 fd 共享同一个内核打开文件表条目,因此共享文件偏移和打开状态。

dup / dup2 示例:验证共享偏移:

c
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main(void) {
    int fd = open("/tmp/os_dup_test", O_CREAT | O_RDWR | O_TRUNC, 0644);
    int fd2 = dup(fd);

    write(fd, "A", 1);        // fd offset → 1; fd2 共享同一打开实例,offset 也为 1
    write(fd2, "B", 1);       // 接着写,文件内容 "AB"

    char buf[8] = {0};
    lseek(fd, 0, SEEK_SET);
    read(fd, buf, 2);
    printf("content: %s\n", buf);  // "AB"

    close(fd2);
    close(fd);
    unlink("/tmp/os_dup_test");
    return 0;
}

dup2 最常见的用途是实现 I/O 重定向:

c
int fd = open("output.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO);  // stdout → output.txt
close(fd);
printf("this goes to file\n");

2. fcntl、ioctl

fcntl是用于控制“文件描述符行为”的通用接口,ioctl是I/O设备控制的通用接口

c
#include <fcntl.h>
#include <sys/ioctl.h>

int fcntl(int fd, int cmd, ...);
int ioctl(int fd, unsigned long request, ...);

fcntl 的参数与返回值:

含义
fd目标文件描述符
cmd命令,决定 fcntl 做什么以及第三参数的类型
...第三参数,类型取决于 cmd:可能是 intstruct flock *
返回值取决于 cmd:多数命令成功返回 0(或非负值),失败返回 -1 并设置 errnoF_DUPFD 返回新 fdF_GETFL 返回标志集

fcntl 常用命令:

命令作用第三参数
F_DUPFD / F_DUPFD_CLOEXEC复制描述符,可指定最小 fdint 最小 fd
F_GETFD获取描述符标志
F_SETFD设置描述符标志int 新标志(目前仅 FD_CLOEXEC
F_GETFL获取文件状态标志
F_SETFL设置文件状态标志int 新标志集
F_GETLK检查文件锁struct flock *
F_SETLK设置文件锁(非阻塞)struct flock *
F_SETLKW设置文件锁(阻塞等待)struct flock *

F_GETFL 可获取的标志包括 O_RDONLYO_WRONLYO_RDWRO_APPENDO_NONBLOCKO_SYNC 等。F_SETFL 只能修改 O_APPENDO_NONBLOCKO_SYNC等状态标志,不能修改访问方式(O_RDONLY / O_WRONLY / O_RDWR)。

ioctl 的参数与返回值:

含义
fd目标文件描述符
request设备相关的请求码,定义在对应设备头文件中
...第三参数,类型取决于 request,通常为指针
成功返回0 或非负值,含义取决于 request
失败返回-1,并设置 errno

ioctl 用于"标准读写不够表达"的设备控制场景(如终端设置、磁盘参数配置等)。

fcntl 示例:获取和修改文件状态标志

c
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main(void) {
    int fd = open("/tmp/os_fcntl_test", O_CREAT | O_WRONLY, 0644);

    int flags = fcntl(fd, F_GETFL);
    printf("access mode: %d\n", flags & O_ACCMODE);  // O_WRONLY

    // 追加 O_APPEND
    fcntl(fd, F_SETFL, flags | O_APPEND);

    write(fd, "hello", 5);  // 追加到末尾而非当前位置

    close(fd);
    unlink("/tmp/os_fcntl_test");
    return 0;
}

小结

本篇围绕文件系统的数据结构及其相应操作介绍了POSIX API:

文件系统:

操作API
挂载mountumount
属性statfsstatfs

目录:

操作API
创建mkdir
修改rename
读取opendirreaddirclosedir
删除rmdir

文件元数据:

操作API
读取statfstatlstat
修改chmodfchmodumasktruncateftruncate

文件数据:

操作API
创建/打开opencreat
寻址lseek
修改writewritefsyncfdatasync
读取readpread
关闭close

同时,对于特殊的链接操作与特殊的链接文件,补充介绍了如下API

数据操作
硬链接(目录表项中inode引用)linkunlink
软链接(一类特殊文件)symlinkreadlink

在结尾,补充了对于fd控制与通用设备控制的API

操作对象操作
fd复制dupdup2dup3
fd通用控制fcntl
设备ioctl