3. 文件描述符操作.md 6.9 KB

3 文件描述符操作

3.1 基本概念

Linux中"一切皆文件"——普通文件、目录、设备、管道、socket都通过文件描述符(fd)操作。除了基本的 open/close/read/write,还有几个重要的文件描述符操作函数:dup/dup2(复制fd)和 fcntl(文件控制)。

核心概念:

  • 文件描述符是进程级的,不同进程可以有不同的fd指向同一个打开文件
  • dup/dup2 创建新的fd指向同一个file struct(共享文件偏移量和状态标志)
  • fcntl 可以修改fd的属性(如添加O_NONBLOCK标志)

3.2 核心API详解

3.2.1 dup / dup2 — 复制文件描述符

#include <unistd.h>

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

dup: 复制oldfd,返回一个新的文件描述符(当前未使用的最小值)

  • 新旧fd共享同一个file struct(文件偏移、打开标志等完全共享)
  • 修改一个fd的文件偏移会影响另一个

dup2: 将oldfd复制到newfd

  • 如果newfd已打开,先关闭newfd
  • 如果oldfd == newfd,dup2返回newfd但不关闭(不执行任何操作)
  • 返回newfd(成功)或-1(失败)

典型用途 — 重定向:

// 将标准输出重定向到文件
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO);  // STDOUT_FILENO(1) 指向 output.txt
close(fd);                 // 关闭原始fd(可选,因为dup2的newfd已经指向同一个文件)
printf("这句话会写入文件而不是终端\n");

注意: dup2 是原子操作(关闭newfd和复制oldfd在同一个系统调用中完成),而手动 close(newfd); dup(oldfd); 在多线程中有竞态条件。

3.2.2 fcntl — 文件控制

#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */);

常用cmd:

cmd 含义 参数
F_DUPFD 复制fd,返回>=arg的最小可用fd int arg
F_GETFD 获取文件描述符标志(如FD_CLOEXEC)
F_SETFD 设置文件描述符标志 int arg
F_GETFL 获取文件状态标志(如O_NONBLOCK)
F_SETFL 设置文件状态标志 int arg
F_GETLK 获取文件锁信息 struct flock*
F_SETLK 设置文件锁(非阻塞) struct flock*
F_SETLKW 设置文件锁(阻塞) struct flock*

常见用法 — 添加非阻塞标志:

#include <fcntl.h>

int flags = fcntl(fd, F_GETFL);  // 获取当前标志
if (flags < 0) {
    perror("fcntl F_GETFL");
    return -1;
}
flags |= O_NONBLOCK;             // 添加非阻塞标志
if (fcntl(fd, F_SETFL, flags) < 0) {
    perror("fcntl F_SETFL");
    return -1;
}

为什么需要fcntl设置O_NONBLOCK? open时没有指定O_NONBLOCK,后续想改为非阻塞,只能用fcntl。这是IO多路复用(epoll)中的常见操作。

3.2.3 文件锁 — flock / fcntl

#include <sys/file.h>

int flock(int fd, int operation);

operation:

  • LOCK_SH:共享锁(读锁),多个进程可以同时持有
  • LOCK_EX:排他锁(写锁),同一时刻只有一个进程持有
  • LOCK_UN:解锁
  • LOCK_NB:非阻塞(与前三个组合使用,如 LOCK_EX | LOCK_NB

示例 — 进程间文件锁:

#include <stdio.h>
#include <fcntl.h>
#include <sys/file.h>
#include <unistd.h>
#include <errno.h>

int main() {
    int fd = open("shared.dat", O_RDWR | O_CREAT, 0644);
    if (fd < 0) {
        perror("open");
        return 1;
    }

    // 尝试获取排他锁
    if (flock(fd, LOCK_EX | LOCK_NB) < 0) {
        if (errno == EWOULDBLOCK) {
            printf("文件已被其他进程锁定,等待中...\n");
            flock(fd, LOCK_EX); // 阻塞等待
        } else {
            perror("flock");
            close(fd);
            return 1;
        }
    }

    printf("获取锁成功,开始操作文件\n");
    // ... 文件操作 ...

    flock(fd, LOCK_UN); // 解锁
    close(fd);
    printf("操作完成,已解锁\n");
    return 0;
}

3.3 完整代码示例

示例:用dup2实现简单的输出重定向

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

int main() {
    // 保存原始stdout
    int saved_stdout = dup(STDOUT_FILENO);

    // 打开日志文件
    int log_fd = open("app.log", O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (log_fd < 0) {
        perror("open log");
        return 1;
    }

    // 重定向stdout到日志文件
    dup2(log_fd, STDOUT_FILENO);
    close(log_fd);

    // 这些printf会写入app.log而不是终端
    printf("这条消息在日志文件中\n");
    printf("当前进程PID: %d\n", getpid());

    // 恢复stdout到终端
    dup2(saved_stdout, STDOUT_FILENO);
    close(saved_stdout);

    // 这些printf会显示在终端
    printf("这条消息在终端上\n");
    printf("日志已写入app.log\n");

    return 0;
}

编译:gcc -o redirect redirect.c

3.4 注意事项与易错点

序号 坑点 说明
1 dup2原子操作 不要用close+dup替代dup2,多线程中不安全
2 dup共享file struct dup出的fd和原fd共享文件偏移量,写入一个会影响另一个
3 fcntl F_SETFL只能改O_APPEND/O_NONBLOCK/O_ASYNC 不能用fcntl修改O_RDONLY/O_WRONLY/O_RDWR(这些在open时确定)
4 flock是建议锁 只对同样使用flock的进程有效,不阻止其他进程直接read/write
5 FD_CLOEXEC标志 进程exec时自动关闭fd,多线程程序中重要(防止fd泄漏到子进程)

3.5 面试要点

Q: dup2和手动close+dup有什么区别? A: dup2是原子操作,在一个系统调用中完成"关闭newfd+复制oldfd"。手动close+dup在多线程中可能有竞态:close之后、dup之前,另一个线程可能open得到同一个newfd,导致dup覆盖了不该关闭的fd。

Q: 为什么epoll需要配合O_NONBLOCK? A: 如果socket是阻塞模式,read在没有数据时会阻塞,导致整个线程卡住,epoll就无法处理其他fd。非阻塞模式下read返回EAGAIN,线程可以继续处理其他事件。

Q: 文件锁有哪些类型? A: flock提供共享锁(LOCK_SH,读锁)和排他锁(LOCK_EX,写锁)。多个进程可以同时持有共享锁,但排他锁是互斥的。fcntl也提供更细粒度的锁(可以锁定文件的某个区域)。