Linux中"一切皆文件"——普通文件、目录、设备、管道、socket都通过文件描述符(fd)操作。除了基本的 open/close/read/write,还有几个重要的文件描述符操作函数:dup/dup2(复制fd)和 fcntl(文件控制)。
核心概念:
dup/dup2 创建新的fd指向同一个file struct(共享文件偏移量和状态标志)fcntl 可以修改fd的属性(如添加O_NONBLOCK标志)#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
dup: 复制oldfd,返回一个新的文件描述符(当前未使用的最小值)
dup2: 将oldfd复制到newfd
典型用途 — 重定向:
// 将标准输出重定向到文件
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); 在多线程中有竞态条件。
#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)中的常见操作。
#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;
}
#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
| 序号 | 坑点 | 说明 |
|---|---|---|
| 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泄漏到子进程) |
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也提供更细粒度的锁(可以锁定文件的某个区域)。