Appearance
[English version](/posts/c教程/en-US/07-Working Close to the System/063-File Descriptors and openreadwrite)
现在您可以创建和管理进程了,让我们探讨一下这些进程如何通过文件描述符与文件、设备甚至彼此之间进行通信。
文件描述符 (FD) 是 Unix 和 C 中最简单但最强大的抽象之一。一切,文件、管道、套接字、终端,都由一个小整数句柄表示。一旦了解了如何打开、读取、写入和关闭文件描述符,您就可以与 Unix 机器上的任何 I/O 系统进行交互。
步骤 1. 什么是文件描述符?
文件描述符是一个整数,用于标识进程中的打开资源。默认情况下,每个进程都以三个打开描述符开始:
| FD | 符号名称 | 描述 |
|---|---|---|
| 0 | STDIN_FILENO | 标准输入(键盘) |
| 1 | STDOUT_FILENO | 标准输出(屏幕) |
| 2 | STDERR_FILENO | 标准错误(屏幕) |
每次打开文件、套接字或管道时,内核都会为您提供最低的未使用 FD。
步骤 2. 使用 open() 打开文件
可以直接使用系统调用层打开文件,而不用fopen()来自工作室。
#include <fcntl.h> // open
#include <unistd.h> // close, read, write
#include <stdio.h> // perror
int main(void) {
int fd = open("data.txt", O_RDONLY);
if (fd == -1) {
perror("open failed");
return 1;
}
printf("File descriptor: %d\n", fd);
close(fd);
}编译并运行:
gcc open_demo.c -o open_demo
./open_demo输出示例:
File descriptor: 3步骤 3. 从文件中读取
read(fd, buffer, size)将原始字节读入内存缓冲区。
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void) {
int fd = open("data.txt", O_RDONLY);
if (fd == -1) return 1;
char buf[64];
ssize_t n = read(fd, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
printf("Read %zd bytes: %s\n", n, buf);
}
close(fd);
}输出:
Read 12 bytes: Hello world步骤 4. 写入文件
write(fd, buffer, size)将原始字节从内存写入文件描述符。
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main(void) {
int fd = open("out.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
const char msg[] = "Writing from C using write()\n";
write(fd, msg, strlen(msg));
close(fd);
}这会覆盖out.txt与您的留言。标志:
-O_WRONLY→ 只写 -O_CREAT→ 如果不存在则创建 -O_TRUNC→ 截断(清除)现有内容
最后的论点0644设置 Unix 权限:
- 所有者可以读/写,
- 团体/其他人可以阅读。
步骤 5. 追加和非阻塞模式
您可以使用按位或组合标志:
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0644);O_APPEND每次写入之前将文件偏移量移至末尾,非常适合日志。
您还可以以非阻塞方式打开文件:
int fd = open("pipe", O_RDONLY | O_NONBLOCK);对于套接字或命名管道上的 I/O 很有用。
步骤 6. 复制描述符
您可以使用复制 FDdup()或者dup2()。这就是重定向的工作原理(>在贝壳中)。
例子:
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
int main(void) {
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO); // redirect stdout to file
close(fd);
printf("This goes into output.txt!\n");
}后dup2,打印到标准输出的所有内容都会转到output.txt.
运行并检查:
./redir_demo
cat output.txt输出:
This goes into output.txt!步骤 7. 偏移量和 lseek()
您可以使用以下命令在文件内移动lseek(fd, offset, whence).
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void) {
int fd = open("data.txt", O_RDONLY);
lseek(fd, 5, SEEK_SET); // move to byte 5
char buf[16];
read(fd, buf, 10);
buf[10] = '\0';
printf("Chunk: %s\n", buf);
close(fd);
}whence可以是:
-SEEK_SET(从开始) -SEEK_CUR(从当前) -SEEK_END(从末尾开始)
步骤 8. 错误检查和返回值
所有系统调用都会返回:
- 非负值→成功 -
-1→ 错误(检查errno)
例子:
#include <errno.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main(void) {
int fd = open("missing.txt", O_RDONLY);
if (fd == -1)
fprintf(stderr, "Error: %s\n", strerror(errno));
}输出:
Error: No such file or directory小代码:使用系统调用复制文件
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main(void) {
int src = open("source.txt", O_RDONLY);
int dst = open("copy.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (src == -1 || dst == -1) {
perror("open failed");
return 1;
}
char buf[256];
ssize_t n;
while ((n = read(src, buf, sizeof(buf))) > 0)
write(dst, buf, n);
close(src);
close(dst);
return 0;
}编译并运行:
gcc copy.c -o copy
./copy现在copy.txt与source.txt,使用纯系统调用。
步骤 9. 从 STDIN 读取并写入 STDOUT
您可以使用read(0, ...)和write(1, ...)直接用于控制台 I/O。
#include <unistd.h>
int main(void) {
char buf[64];
ssize_t n = read(STDIN_FILENO, buf, sizeof(buf));
write(STDOUT_FILENO, buf, n);
}跑步:
./echo_demo
hello world输出:
hello world这是每个 shell 命令的本质。
第 10 步:为什么它很重要
文件描述符统一了所有内容的 I/O:
- 常规文件
- 管道和插座
- 设备和终端
它们可以让您准确控制数据如何流入和流出您的程序,这是系统工具、服务器和操作系统级编程的基础。
一旦理解了这些原语,您就可以构建自己的工具版本,例如cat,tee,甚至是简单的贝壳。
自己尝试一下
1.写一个迷你cat克隆使用read()和write(). 2. 使用dup2()将 stdout 和 stderr 重定向到文件。 3.使用添加错误消息perror()并处理EINTR. 4. 使用lseek()在打印之前跳过文件的前 N 个字节。 5. 实现一个简单的文件追加器O_APPEND.
接下来,您将使用这些文件描述符来使进程进行通信、构建管道和重定向,这与 shell 用于连接命令的机制相同ls | grep.
