【Linux】基础 I/O 详解:从 fopen 到 open,终于搞懂 fd 为什么从 3 开始

🔥个人主页:爱和冰阔乐
📚专栏传送门:《数据结构与算法》 、C++
🐶学习方向:C++方向学习爱好者
⭐人生格言:得知坦然 ,失之淡然

🏠博主简介
文章目录
前言
本文主要整理 Linux 基础 I/O 的前半部分内容,从文件的基本概念讲起,依次回顾 C 语言文件接口、标准输入输出错误、文件打开方式,再进入系统调用层面的 open、write、read、close。最后结合实验解释为什么 fd 通常从 3 开始,以及文件描述符本质上为什么是进程文件描述符表的数组下标。
一、先理解什么是“文件”
在磁盘上新建一个空文件,文件大小显示为 0KB,那么这个文件到底占不占空间?
这里不能简单理解成“有内容才叫文件”。一个文件可以先粗略理解成:
文件 = 文件内容 + 文件属性
所谓文件属性,也可以叫元数据,比如文件名、权限、拥有者、修改时间、inode 信息等。
所以空文件虽然没有真正的文件内容,但是它仍然需要被文件系统记录和管理。从严格角度说,空文件一般不会分配存放内容的数据块,但是它需要 inode、目录项等元数据来描述。也就是说,它不是“什么都没有”。
这也是学习文件系统时第一个要转过来的点:
文件不只是内容,文件还有属性。
1.1 狭义理解文件
从最普通的角度看,文件一般存放在磁盘中。
磁盘属于永久性存储介质,断电后数据不会像内存一样丢失。同时,磁盘也是一种外设,程序想读磁盘内容,或者把数据写入磁盘,本质上都是在和外设进行输入输出操作,也就是 I/O。
所以我们平时说的文件读写,底层可以理解成:
进程和磁盘设备之间进行数据交换。
1.2 从系统角度理解文件操作
我们平时写 C 语言时,经常使用 fopen、fclose、fread、fwrite 这些函数。
但是要注意:
真正能访问硬件的不是 C 语言库函数,而是操作系统。
用户程序不能直接去操作磁盘、显示器、键盘这些硬件。硬件是由操作系统管理的,用户程序想访问硬件,只能通过操作系统提供的系统调用接口。
所以 C 语言文件接口的底层,一定会封装对应的系统调用。
还有一个问题:代码里写了 fopen,文件就已经打开了吗?
答案是没有。
代码躺在磁盘里时,它只是一个可执行文件,还没有真正运行。只有程序被加载到内存中运行起来,变成进程,并且执行到 fopen 或 open 那一行时,文件才真正被打开。
因此,对文件的操作,本质上是:
进程对文件进行操作。
从是否被打开的角度看,文件可以简单分成两类:
| 类型 | 含义 |
|---|---|
| 磁盘级文件 | 文件存在磁盘中,但当前没有被进程打开 |
| 内存级文件 | 文件已经被进程打开,操作系统需要在内核中维护它 |
二、回顾 C 语言文件接口
在进入系统调用之前,先回顾一下 C 语言文件接口。因为我们最开始接触文件操作时,通常都是从 fopen 这一套开始的。
2.1 以写的方式打开文件
fopen 可以使用不同方式打开文件,比如读、写、追加等。
如果打开失败,返回 NULL;如果打开成功,返回一个 FILE* 类型的文件指针。
fopen 接口说明:不同打开方式对应不同文件操作权限。

fopen 打开成功后返回 FILE*,后续读写都围绕这个文件流进行。

下面是一段简单的写文件代码:
#include <stdio.h>
#include <string.h>
int main()
{
// 不带路径时,文件会创建在当前进程的工作目录下
FILE *fp = fopen("log.txt", "w");
if (fp == NULL)
{
perror("fopen");
return 1;
}
int cnt = 1;
const char *msg = "hello world";
while (cnt <= 10)
{
char buffer[1024];
snprintf(buffer, sizeof(buffer), "%s%d\n", msg, cnt++);
fwrite(buffer, strlen(buffer), 1, fp);
}
fclose(fp);
return 0;
}
实验结果:以 w 方式写入后,log.txt 中生成了对应内容。

这里有一个细节:如果 fopen 只传文件名,不传路径,文件会创建在哪里?
每个进程都有自己的当前工作目录。可以使用下面的命令查看正在运行进程的信息:
ls /proc/[进程id] -l
其中比较常见的两个字段是:
| 字段 | 含义 |
|---|---|
cwd |
当前进程运行目录的符号链接 |
exe |
当前进程可执行程序完整路径的符号链接 |
通过 /proc 查看进程信息,可以看到 cwd 和 exe 这两个关键链接。

所以,不带路径打开文件时,操作系统会根据当前进程的工作目录 cwd 来决定文件放在哪里。
2.2 以读的方式打开文件
下面写一个简单的读文件程序:
#include <stdio.h>
#include <string.h>
int main()
{
FILE *fp = fopen("log.txt", "r");
if (fp == NULL)
{
perror("fopen");
return 1;
}
char buf[1024];
while (1)
{
size_t n = fread(buf, 1, sizeof(buf) - 1, fp);
if (n > 0)
{
buf[n] = '\0';
printf("%s", buf);
}
if (feof(fp))
{
break;
}
}
fclose(fp);
return 0;
}
这里要注意一点:如果我们想把读出来的内容当字符串打印,就要给数组预留一个位置放 \0。
文件本身只是一堆字节,不会自动帮你加字符串结束符。
2.3 简单实现一个 cat 命令
稍微改一下,就能实现一个简单版 cat:
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
if (argc != 2)
{
printf("Usage: %s <filename>\n", argv[0]);
return 1;
}
FILE *fp = fopen(argv[1], "r");
if (fp == NULL)
{
perror("fopen");
return 2;
}
char buf[1024];
memset(buf, 0, sizeof(buf));
while (1)
{
size_t n = fread(buf, 1, sizeof(buf) - 1, fp);
if (n > 0)
{
buf[n] = '\0';
printf("%s", buf);
}
if (feof(fp))
{
break;
}
}
fclose(fp);
return 0;
}
运行时直接传文件名:
./mycat log.txt
实验结果:传入文件名后,程序可以像简化版 cat 一样读取文件内容。

这样程序就不用每次修改源码了。想读哪个文件,运行时传不同参数即可。
三、stdin、stdout、stderr 到底是什么?
我们写程序时,经常直接使用:
printf("hello world\n");
scanf("%d", &x);
fprintf(stderr, "error\n");
这里就有一个问题:我们并没有手动打开键盘,也没有手动打开显示器,为什么可以直接输入输出?
因为一个程序运行起来时,通常已经拥有三个默认的标准文件描述符:
| 名称 | 含义 | 默认对应设备 |
|---|---|---|
stdin |
标准输入 | 键盘 |
stdout |
标准输出 | 显示器 |
stderr |
标准错误 | 显示器 |
在 C 语言层面,它们是 FILE* 类型:
#include <stdio.h>
extern FILE *stdin;
extern FILE *stdout;
extern FILE *stderr;
这就解释了为什么 fprintf(stdout, ...) 可以直接向屏幕打印。因为 stdout 本身就代表标准输出流。
3.1 输出到显示器的几种方法
输出到显示器不只有 printf 一种写法,下面这几种本质上都是把内容写到标准输出:
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg = "hello fwrite\n";
fwrite(msg, strlen(msg), 1, stdout);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}
如果暂时不看 C 库函数,直接站在系统调用角度,也可以用 read 和 write 操作默认的 0、1、2:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main()
{
char buf[1024];
ssize_t s = read(0, buf, sizeof(buf) - 1); // 从标准输入读取
if (s > 0)
{
buf[s] = 0;
write(1, buf, strlen(buf)); // 写到标准输出
write(2, buf, strlen(buf)); // 写到标准错误
}
return 0;
}
这里就能看出:stdin/stdout/stderr 是 C 语言层面的文件流,而 0/1/2 是系统层面的文件描述符。
直接向标准输出打印,本质上就是向标准输出对应的 fd 写入内容。

需要注意的是,严格来说,底层的 0、1、2 文件描述符通常是进程从父进程(比如 shell)继承来的,C 标准库再把它们包装成 stdin、stdout、stderr 这三个 FILE* 流。
所以可以这样理解:
系统层只认 fd,C 语言层把 fd 包装成了 FILE*,让我们用起来更方便。
四、文件打开方式的细节
不同的打开方式,会直接影响文件原来的内容。
4.1 w 方式:清空写入
如果用 w 方式打开一个已经存在的文件,但是不写任何内容:
#include <stdio.h>
int main()
{
FILE *fp = fopen("log.txt", "w");
if (fp == NULL)
{
perror("fopen");
return 1;
}
fclose(fp);
return 0;
}
运行后会发现,log.txt 原来的内容被清空了。
实验结果:w 方式打开已有文件,即使不写入内容,也会清空原文件。

所以 w 的特点是:
文件不存在就创建;文件存在就先清空,再从开头写入。
这也能解释为什么输出重定向 > 会清空原文件。因为它打开目标文件时,使用的就是类似“清空写入”的逻辑。
输出重定向
>会先清空目标文件,再写入新的输出内容。

4.2 a 方式:追加写入
如果使用 a 方式打开文件:
a 方式表示 append,也就是追加写入。

FILE *fp = fopen("log.txt", "a");
if (fp == NULL)
{
perror("fopen");
return 1;
}
fprintf(fp, "hello append\n");
fclose(fp);
它不会清空原文件,而是把新内容追加到文件末尾。
实验结果:a 方式会把新内容继续追加到原文件末尾。

这对应 Linux 中的追加重定向:
./a.out >> log.txt
追加重定向
>>的底层逻辑,也可以理解成追加方式打开文件。

4.3 r+、w+、a+ 的区别
这几个模式都带 +,表示以读写方式打开,但是它们的细节不一样:
| 打开方式 | 文件不存在时 | 文件存在时 | 特点 |
|---|---|---|---|
r+ |
打开失败 | 保留原内容 | 从开头读写 |
w+ |
创建新文件 | 先清空原内容 | 从开头读写 |
a+ |
创建新文件 | 保留原内容 | 写入永远追加到末尾 |
还有一个容易踩坑的地方:
用 w+ 这种读写方式打开文件后,刚写进去的内容,不能马上直接读出来。
原因是写入数据后,文件的读写位置已经移动到了内容末尾。此时直接读,读到的就是文件末尾,自然读不出刚才写入的内容。
如果想重新从头读,可以使用:
rewind(fp);
相关接口还有:
| 接口 | 作用 |
|---|---|
fseek |
调整文件读写位置 |
ftell |
获取当前文件读写位置 |
rewind |
将文件读写位置移动到开头 |
读写指针相关接口:fseek、ftell、rewind。

4.4 写字符串时要不要写入 \0?
答案是:不需要。
\0 是 C 语言用来标识字符串结尾的规则,和文件系统本身没有关系。
文件只关心字节数据。如果把 \0 也强行写入文本文件,用某些工具打开时可能会看到奇怪的显示效果。
五、系统文件 I/O:open、write、read、close
C 语言接口用起来比较方便,但它不是最底层。Linux 系统层提供的是系统调用接口。
5.1 open:系统层面的文件打开
open 的常见形式如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags, mode_t mode);
open 系统调用接口:返回值是文件描述符 fd。

open 的 flags 用来描述打开方式,mode 用来描述新文件权限。

返回值:
| 返回值 | 含义 |
|---|---|
>= 0 |
打开成功,返回文件描述符 fd |
-1 |
打开失败,并设置错误码 |
比如创建并以写入方式打开文件:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("log.txt", O_CREAT | O_WRONLY, 0666);
if (fd < 0)
{
perror("open");
return 1;
}
close(fd);
return 0;
}
实验结果:open 创建文件后,如果没有正确设置权限,显示效果可能不符合预期。

这里的 O_CREAT | O_WRONLY 就是通过位图传递多个标志位。
5.2 库函数和系统调用的关系
前面用到的 fopen、fclose、fread、fwrite 属于 C 标准库函数;这一节用到的 open、close、read、write、lseek 属于系统调用接口。
可以先这样理解:
| 层次 | 常见接口 | 特点 |
|---|---|---|
| C 标准库 | fopen/fread/fwrite/fclose |
用起来方便,内部封装了更多细节 |
| 系统调用 | open/read/write/close/lseek |
操作系统直接提供的接口,更贴近底层 |
所以,fopen 这一类函数不是直接操作硬件,而是对系统调用又封装了一层。这样做的好处是让我们写代码更方便,也能屏蔽一部分系统差异。
这也是为什么我们后面一定要把 FILE* 和 fd 放在一起理解:
FILE*是 C 库层的文件流,fd是系统层真正用来定位打开文件的整数。
补充图:C 库函数和系统调用的关系。

5.3 为什么系统调用用位图传标志位?
如果一个函数有很多选项,难道每个选项都传一个参数吗?
这样函数参数会非常多,不好维护。
Linux 中常用位图的方式传递标志位。也就是用一个整数的不同二进制位,表示不同选项是否打开。
举个简单例子:
#include <stdio.h>
#define ONE_FLAG (1 << 0)
#define TWO_FLAG (1 << 1)
#define THREE_FLAG (1 << 2)
#define FOUR_FLAG (1 << 3)
void Print(int flag)
{
if (flag & ONE_FLAG)
printf("One!\n");
if (flag & TWO_FLAG)
printf("Two!\n");
if (flag & THREE_FLAG)
printf("Three!\n");
if (flag & FOUR_FLAG)
printf("Four!\n");
}
int main()
{
Print(ONE_FLAG);
Print(ONE_FLAG | TWO_FLAG);
Print(ONE_FLAG | TWO_FLAG | THREE_FLAG);
return 0;
}
open 的 flags 也是类似思想。
常见标志位如下:
| 标志位 | 含义 |
|---|---|
O_RDONLY |
只读打开 |
O_WRONLY |
只写打开 |
O_RDWR |
读写打开 |
O_CREAT |
文件不存在则创建 |
O_TRUNC |
打开时清空原文件内容 |
O_APPEND |
追加写入 |
5.4 mode 和 umask
如果使用 O_CREAT 创建新文件,一定要传第三个参数 mode,比如:
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
这里的 0666 表示普通文件的默认权限:拥有者、所属组、其他用户都可以读写。
但是最终看到的权限可能不是 666,而是 644。这是因为系统还有一个权限掩码 umask。
实际权限大致可以理解为:
最终权限 = mode & (~umask)
如果临时把 umask 设置为 0:
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
umask(0);
return 0;
}
那么创建文件时就不会被默认掩码屏蔽权限。
设置 mode 为 0666 后,最终权限还会受到 umask 影响。

5.5 close:关闭文件
系统调用层关闭文件使用 close:
#include <unistd.h>
close(fd);
它的参数就是 open 返回的文件描述符。
close 使用 fd 关闭对应的打开文件。

5.6 write:系统层面的写入
write 的常见形式如下:
#include <unistd.h>
ssize_t write(int fd, const void *buf, size_t count);
参数含义:
| 参数 | 含义 |
|---|---|
fd |
要写入的文件描述符 |
buf |
要写入的数据起始地址 |
count |
想写入的字节数 |
返回值:
| 返回值 | 含义 |
|---|---|
> 0 |
实际写入的字节数 |
-1 |
写入失败 |
write 接口说明:第一个参数是 fd,返回值是实际写入的字节数。

示例:
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
if (fd < 0)
{
perror("open");
return 1;
}
const char *msg = "hello world\n";
write(fd, msg, strlen(msg));
close(fd);
return 0;
}
这里写入字符串时,仍然不需要把 \0 写入文件,所以使用 strlen(msg),不要写成 strlen(msg) + 1。
5.7 O_TRUNC 与 O_APPEND
如果打开一个已经有内容的文件,只使用:
open("log.txt", O_CREAT | O_WRONLY, 0666);
它不会自动清空原文件。
实验现象:只用 O_WRONLY 打开时,不会自动清空文件内容。

如果想打开时清空文件,必须加 O_TRUNC:
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
如果想追加写入,使用 O_APPEND:
int fd = open("log.txt", O_CREAT | O_WRONLY | O_APPEND, 0666);
所以 C 语言的打开方式和系统调用大致可以对应为:
| C 语言方式 | 系统调用标志位 |
|---|---|
"w" |
`O_WRONLY |
"a" |
`O_WRONLY |
"r" |
O_RDONLY |
这也说明:
C 语言的文件接口,本质上是对系统调用的封装。
5.8 文本写入和二进制写入
看下面这段代码:
int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int a = 1234567;
write(fd, &a, sizeof(a));
close(fd);
实验结果:直接写入 int 的二进制数据,文本方式打开会看到不可读内容。

如果用普通文本方式打开 log.txt,可能会看到乱码。
原因是这里写入的不是字符串 "1234567",而是变量 a 在内存中的二进制数据。int 通常占 4 个字节,所以文件中写入的是这 4 个字节的二进制形式。
如果想让文件中看到 1234567,需要先把整数格式化成字符串:
char buf[32];
snprintf(buf, sizeof(buf), "%d", a);
write(fd, buf, strlen(buf));
从系统调用角度看,write 并不关心你写的是文本还是二进制。它只认一段地址和一段字节数。
所谓文本写入、二进制写入,更多是语言层和用户理解上的区分。
语言层可以使用 fputs 进行文本写入。

fwrite 更偏向按字节块写入,也常用于二进制数据写入。

5.9 read:系统层面的读取
read 的常见形式如下:
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);
参数含义:
| 参数 | 含义 |
|---|---|
fd |
从哪个文件描述符读取 |
buf |
读取到的数据放在哪里 |
count |
最多读取多少字节 |
返回值:
| 返回值 | 含义 |
|---|---|
> 0 |
实际读到的字节数 |
0 |
读到文件末尾 |
-1 |
读取失败 |
read 接口说明:从指定 fd 读取数据到用户提供的空间。

read 的返回值需要重点关注:大于 0、等于 0、小于 0 含义不同。

示例代码:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main()
{
int fd = open("log.txt", O_RDONLY);
if (fd < 0)
{
perror("open");
return 1;
}
while (1)
{
char buffer[64];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = '\0';
printf("%s", buffer);
}
else if (n == 0)
{
break;
}
else
{
perror("read");
break;
}
}
close(fd);
return 0;
}
这里同样要注意,如果把读出来的内容当字符串打印,就要手动加 \0。
六、fd 到底是什么?为什么从 3 开始?
现在问题来了:
open 返回的只是一个整数,为什么拿着这个整数就能读写文件?
我们先做个实验:
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main()
{
int fd1 = open("log1.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int fd2 = open("log2.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int fd3 = open("log3.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
int fd4 = open("log4.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
printf("fd1: %d\n", fd1);
printf("fd2: %d\n", fd2);
printf("fd3: %d\n", fd3);
printf("fd4: %d\n", fd4);
close(fd1);
close(fd2);
close(fd3);
close(fd4);
return 0;
}
正常情况下,你会看到类似结果:
fd1: 3
fd2: 4
fd3: 5
fd4: 6
实验结果:连续打开普通文件,fd 一般从 3、4、5、6 开始分配。

那 0、1、2 去哪里了?
它们已经被标准输入、标准输出、标准错误占用了:
| fd | 含义 | 默认设备 |
|---|---|---|
| 0 | 标准输入 | 键盘 |
| 1 | 标准输出 | 显示器 |
| 2 | 标准错误 | 显示器 |
可以用 fileno 验证:
#include <stdio.h>
int main()
{
printf("stdin fd: %d\n", fileno(stdin));
printf("stdout fd: %d\n", fileno(stdout));
printf("stderr fd: %d\n", fileno(stderr));
return 0;
}
输出一般是:
stdin fd: 0
stdout fd: 1
stderr fd: 2
实验结果:stdin、stdout、stderr 底层分别对应 fd 0、1、2。

所以新打开的普通文件,一般就从 3 开始分配。
这就能得出一个很重要的结论:
C 语言的 FILE* 结构体中,底层一定封装了对应的文件描述符 fd。
语言层用 FILE*,系统层用 fd。不同语言的文件接口可能不一样,但到了操作系统层面,最终都要落到文件描述符上。
七、操作系统如何管理被打开的文件?
为什么 fd 是一个整数?
因为它本质上可以理解成数组下标。
7.1 先描述,再组织
操作系统管理任何东西,一般都有一个思想:
先描述,再组织。
进程要被管理,操作系统会用 task_struct 描述进程。
文件被打开后,也需要被管理,操作系统会在内核中创建对应的文件对象,比如 struct file。
一个进程可以打开多个文件,那么进程怎么知道自己打开了哪些文件?
在内核中,每个进程都有自己的文件描述符表。简单理解,它里面有一个数组:
struct file *fd_array[];
这个数组中的每个元素,存放的是一个已经打开文件对象的地址。
当进程调用 open 打开新文件时,内核大致做这些事:
- 在内核中创建一个
struct file对象,描述这个被打开的文件。 - 在当前进程的文件描述符表中,找到一个最小的空闲位置。
- 把
struct file的地址填入这个位置。 - 把这个位置的数组下标返回给用户。
进程、文件描述符表和 struct file 之间的关系,可以先按这个结构理解。

这个数组下标,就是 fd。
所以:
文件描述符 fd 的本质,就是当前进程文件描述符表的数组下标。
如果想从内核源码里验证这条线,也可以顺着下面几个结构去看:
| 结构体 | 大致作用 | 常见位置 |
|---|---|---|
task_struct |
描述进程,里面会关联文件描述符表 | include/linux/sched.h |
files_struct |
描述当前进程打开文件的表 | include/linux/fdtable.h |
struct file |
描述一个已经打开的文件对象 | include/linux/fs.h |
不同系统的内核版本路径可能不一样,可以先用 uname -a 看自己的内核版本,再到对应源码目录中查找。这里不需要把源码全部背下来,只要抓住这条关系即可:
task_struct -> files_struct -> fd_array[] -> struct file
补充图:fd 本质是当前进程文件描述符表的数组下标。

也就是说,进程不是直接“拿着文件名”去读写,而是先通过自己的 fd 表找到对应的打开文件对象。
7.2 read/write 的基本过程
当用户调用:
read(fd, buffer, size);
操作系统会根据当前进程找到它的文件描述符表,再用 fd 作为下标找到对应的 struct file,然后把读到的数据放到用户传入的 buffer 中。
当用户调用:
write(fd, buffer, size);
操作系统同样会先根据 fd 找到对应的文件对象,再把用户传入的数据写入目标文件。
所以 read/write 的主线可以先这样理解:
进程传入 fd,内核根据 fd 找到文件对象,再完成对应读写操作。
八、全文总结
本篇主要整理了 Linux 基础 I/O 的前半部分内容。我们先从文件的概念讲起,知道了文件不仅有内容,还有权限、时间、inode 等属性信息。接着回顾了 C 语言中的 fopen、fread、fwrite、fclose,并理解了 stdin、stdout、stderr 这三个默认打开的标准流。
后面进入系统调用层,学习了 open、write、read、close 等接口。和 C 库函数不同,open 成功后返回的是文件描述符 fd。通过实验可以发现,普通文件的 fd 一般从 3 开始,因为 0、1、2 已经分别被标准输入、标准输出、标准错误占用了。
最后要记住一句话:
FILE* 是 C 语言层的文件流,fd 是系统层定位文件的整数,本质上就是进程文件描述符表的数组下标。
理解了 fd 的本质,后面学习重定向、dup2 和 MiniShell 重定向实现就会轻松很多。
文件描述符表。
只要抓住这条线,Linux 文件 I/O 的底层逻辑就清楚了。
资源分享
【Linux进程控制】从exec程序替换到手写简易Shell:fork、execvp、环境变量与内建命令
【Linux系统篇】从 fork 到 WNOHANG:进程创建与等待机制详解
【Linux进程】程序地址空间详解:虚拟地址、页表、写时拷贝与mm_struct
更多推荐




所有评论(0)