1. 系统文件I/O
打开⽂件的⽅式不仅仅是fopen,ifstream等流式,语⾔层的⽅案,其实系统才是打开⽂件最底层的方案。不过,在学习系统⽂件IO之前,先要了解下如何给函数传递标志位,该⽅法在系统⽂件IO接口中会使⽤到:
1.1 一种传递标志位的方法
cpp
1 #include<stdio.h>
2
3 #define ONE_FLAG (1<<0)
4 #define TWO_FLAG (1<<1)
5 #define THREE_FLAG (1<<2)
6 #define FOUR_FLAG (1<<3)
7
8 void Print(int flags)
9 {
10 if(flags & ONE_FLAG)
11 {
12 printf("one\n");
13 }
14 if(flags & TWO_FLAG)
15 {
16 printf("two\n");
17 }
18 if(flags & THREE_FLAG)
19 {
20 printf("THREE\n");
21 }
22 if(flags & FOUR_FLAG)
23 {
24 printf("FOUR\n");
25 }
26 }
27
28 int main()
29 {
30 Print(ONE_FLAG);
31 printf("\n");
32 Print(TWO_FLAG);
33 printf("\n");
34 Print(THREE_FLAG);
35 printf("\n");
36 Print(FOUR_FLAG);
37 printf("\n");
38 Print(ONE_FLAG | TWO_FLAG);
39 printf("\n");
40 Print(ONE_FLAG | TWO_FLAG | THREE_FLAG);
41 printf("\n");
42 return 0;
43 }

1.2 hello.c 写文件:
1.2.1 open

pathname
参数 :是一个指向以空字符结尾的字符串的指针,用于指定要打开或创建的文件路径。可以是绝对路径(例如/home/user/test.txt
),也可以是相对路径(相对于当前工作目录,例如test.txt
)。flags
参数 :是一个整数值,用于指定打开文件的方式和选项。- 必选的访问模式标志 :
O_RDONLY
:以只读方式打开文件,例如open("file.txt", O_RDONLY);
O_WRONLY
:以只写方式打开文件,例如open("file.txt", O_WRONLY);
O_RDWR
:以读写方式打开文件,例如open("file.txt", O_RDWR);
- 常用的可选标志 :
O_CREAT
:如果文件不存在,则创建该文件。当使用此标志时,需要提供第三个参数mode
来指定新文件的权限,例如open("newfile.txt", O_CREAT | O_WRONLY, 0644);
O_EXCL
:与O_CREAT
一起使用,如果要创建的文件已经存在,则open()
函数调用失败并返回 -1,避免意外覆盖已有文件,如open("newfile.txt", O_CREAT | O_EXCL | O_WRONLY, 0644);
O_TRUNC
:如果文件已经存在且以可写方式(O_WRONLY
或O_RDWR
)打开,会将文件的长度截断为 0,即清空文件原有内容,例如open("file.txt", O_WRONLY | O_TRUNC);
O_APPEND
:以追加模式打开文件,后续写入操作会将数据追加到文件末尾,防止覆盖原有数据,例如open("log.txt", O_WRONLY | O_APPEND);
O_NONBLOCK
:对于管道、套接字等特殊文件,以非阻塞方式打开。意味着在进行读写操作时,如果操作不能立即完成,函数不会阻塞进程,而是立即返回,例如open("/dev/ttyS0", O_RDONLY | O_NONBLOCK);
- 必选的访问模式标志 :
mode
参数 :当flags
中包含O_CREAT
标志时,需要提供该参数,用于指定新创建文件的权限。权限值是一个mode_t
类型的值,通常用八进制表示。例如0666
表示文件所有者、同组用户和其他用户都具有读写权限;0755
表示文件所有者具有读、写、执行权限,同组用户和其他用户具有读、执行权限 。
返回值:
open()
函数调用成功时,会返回一个非负整数,这个整数被称为文件描述符(file descriptor),它是一个索引值,用于标识进程中打开的文件。后续对该文件的读写等操作(如 read()
、write()
、lseek()
等函数)都要通过这个文件描述符来进行。
1.2.2 close

1.2.3 write

fd
参数 :是一个整数,表示要写入数据的文件描述符。文件描述符通常是由open()
函数打开文件或设备后返回的非负整数 。例如,int fd = open("test.txt", O_WRONLY | O_CREAT, 0644);
打开文件后,fd
就可以作为write()
函数的第一个参数。buf
参数 :是一个指向要写入数据缓冲区的指针,类型为const void *
。这意味着它可以指向任何类型的数据,如字符数组(用于写入文本数据)、结构体等 。例如,const char data[] = "Hello, world!";
,然后write(fd, data, sizeof(data));
就可以将data
数组中的内容写入到fd
对应的文件中。buf
参数 :是一个size_t
类型的值,表示要从buf
指向的缓冲区中写入的字节数 。size_t
是无符号整数类型,通常用于表示内存大小、数组长度等。
返回值:
write()
函数返回值类型为 ssize_t
,ssize_t
是有符号整数类型
cpp
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<sys/stat.h>
4 #include<fcntl.h>
5 #include<unistd.h>
6 #include<string.h>
7
8 int main()
9 {
10 umask(0);
11 int fd = open("log.txt", O_CREAT | O_WRONLY, 0666);
12 if(fd < 0)
13 {
14 perror("open");
15 return 1;
16 }
17
18 const char* msg = "hello linux!\n";
19 int cnt = 5;
20 while(cnt)
21 {
22 write(fd, msg, strlen(msg));
23 cnt--;
24 }
25
26 close(fd);
27 return 0;
28 }

1.3 hello.c 读文件

fd
:要读取数据的文件描述符,通常由open
等函数获取。buf
:指向用于存储读取数据的缓冲区的指针。count
:要读取的字节数。
返回值:
- 成功时,返回实际读取的字节数(可能小于
count
,比如文件已读到末尾等情况)。 - 失败时,返回 -1,并设置
errno
以指示错误原因。
cpp
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
int fd = open("log.txt", O_RDONLY);
if (fd < 0) {
perror("open");
return 1;
}
const char* msg = "hello linux!\n";
char buf[1024];
while (1) {
ssize_t s = read(fd, buf, strlen(msg));//类⽐write
if (s > 0) {
printf("%s", buf);
}
else {
break;
}
}
close(fd);
return 0;
}

1.4 文件描述符fd


文件描述符从 3
开始分配,是因为 0
、1
、2
这三个描述符被系统默认占用,用于进程与外界的基础交互:
0
:对应标准输入(STDIN_FILENO
),通常关联键盘输入。1
:对应标准输出(STDOUT_FILENO
),通常关联终端屏幕输出。2
:对应标准错误(STDERR_FILENO
),通常用于输出错误信息到终端。

1. 进程与文件的关联桥梁
每个进程都有一个 task_struct
结构体(进程控制块,用于管理进程的各种信息),其中有一个 *files
指针,它指向 files_struct
结构体。files_struct
是进程管理打开文件的核心结构,里面最重要的部分是一个指针数组 fd_array
(图中 file* fd_array[]
)。
2. 文件描述符的本质
文件描述符本质上就是 fd_array
这个指针数组的下标。比如,当我们说文件描述符为 3
时,它对应的就是 fd_array[3]
这个数组元素。
3. 与打开文件的关联
当进程通过 open
系统调用打开一个文件(或创建新文件)时,操作系统会在内核中创建一个 file
结构体。这个 file
结构体保存了文件相关的重要信息,比如文件的 inode 元信息(用于标识文件在文件系统中的属性、位置等)。然后,操作系统会在进程的 fd_array
中找到一个空闲的下标(即文件描述符),将该下标对应的数组元素(指针)指向这个 file
结构体。这样,进程后续就可以通过这个文件描述符来操作对应的文件了。
4. 标准文件描述符
系统默认会为每个进程预先打开三个标准文件:
- 标准输入:对应
fd_array[0]
,文件描述符为0
。 - 标准输出:对应
fd_array[1]
,文件描述符为1
。 - 标准错误:对应
fd_array[2]
,文件描述符为2
。当进程再打开新的文件时,会从3
开始分配文件描述符(因为0
、1
、2
已被标准文件占用)。
5. 文件描述符的分配规则
文件描述符的分配遵循 "最小未使用整数" 原则,具体表现为:
- 进程启动时,0(标准输入)、1(标准输出)、2(标准错误)被默认占用,新打开的文件从 3 开始分配。
- 不同进程的文件描述符相互独立,同一数字在不同进程中可能指向不同资源。
- 关闭后的文件描述符会被标记为可用,后续新打开文件时可能被复用。
重定向对分配的影响 :重定向(如 >
或 dup2
系统调用)会改变文件描述符与资源的关联关系,进而影响分配逻辑。例如:
- 使用
ls > output.txt
时,shell 会先关闭标准输出(fd=1),再打开output.txt
并将其分配到 fd=1,此时写入 fd=1 的数据会被定向到文件而非终端。 - 通过
dup2(oldfd, newfd)
可强制让newfd
指向oldfd
对应的资源:若newfd
已被占用,会先关闭它再完成绑定,此时newfd
的数值不变,但指向的资源被替换。
cpp
52 int main()
53 {
54 umask(0);
55 int fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666);
56 dup2(fd, 1);
57 close(fd);
58
59 printf("fd:%d\n", fd);
60 printf("hello linux!\n");
61 printf("hello linux!\n");
62 printf("hello linux!\n");
63 fprintf(stdout, "hello stdout\n");
64 fprintf(stdout, "hello stdout\n");
65 fprintf(stdout, "hello stdout\n");
66 fprintf(stdout, "hello stdout\n");
67 return 0;
68 }

这种机制使得进程无需修改代码,只需通过调整文件描述符的指向,即可灵活改变 I/O 流向。
2. 理解 "一切皆文件"
在 Linux 系统中,"一切皆文件" 是核心设计哲学之一,它并非指所有资源都是传统意义上的 "磁盘文件",而是指 Linux 用统一的 "文件接口" 来管理所有硬件设备、软件资源和 I/O 对象 ,让进程可以通过相同的一套系统调用(如 open
/read
/write
/close
)操作不同类型的资源,极大简化了系统设计和用户使用。
一、"一切皆文件" 的具体体现:哪些资源被视为 "文件"?
Linux 中几乎所有可操作的资源,都被抽象成 "文件" 的形式,常见类型包括:
- 普通文件 :传统的文本文件、二进制文件(如
test.txt
、a.out
),存储在磁盘上。 - 目录文件 :用于管理文件的 "文件夹",本质是记录子文件 / 子目录信息的特殊文件(如
/home
、/etc
)。 - 设备文件 :
- 块设备文件:按 "块" 读写的硬件(如硬盘、U 盘),对应
/dev/sda
、/dev/sdb1
; - 字符设备文件:按 "字符流" 读写的硬件(如键盘、鼠标、串口),对应
/dev/keyboard
、/dev/tty1
。
- 块设备文件:按 "块" 读写的硬件(如硬盘、U 盘),对应
- 管道文件(FIFO) :用于进程间通信的临时 "文件",数据在内存中传输,不落地磁盘(如
mkfifo pipe1
创建的管道)。 - 套接字文件(Socket) :用于网络通信或本地进程间通信的 "文件"(如
/var/run/docker.sock
是 Docker 的本地套接字)。 - 符号链接文件 :类似 "快捷方式",指向其他文件的特殊文件(如
ln -s test.txt link.txt
创建的链接)。
二、核心本质:统一的接口 + 抽象的 "文件结构"
Linux 之所以能实现 "一切皆文件",关键在于两点:
- 统一的系统调用接口无论操作的是普通文件、键盘还是网络,进程都用相同的一套函数:
- 用
open()
打开资源(获取文件描述符); - 用
read()
/write()
读写数据; - 用
close()
释放资源。
例如:
- 读普通文件:
fd = open("test.txt", O_RDONLY); read(fd, buf, 100);
- 读键盘输入(标准输入,对应
/dev/stdin
):read(0, buf, 100);
(0 是标准输入的文件描述符) - 写网络数据(通过套接字):
fd = socket(AF_INET, SOCK_STREAM, 0); write(fd, data, len);
- 内核的 "文件抽象层" 内核内部通过
struct file
结构体(文件对象)统一描述所有资源:

- 不管是磁盘文件还是键盘,内核都会为其创建一个
struct file
,记录资源的读写方式、当前位置、权限等信息; - 进程通过 "文件描述符"(fd)关联到
struct file
,无需关心资源的实际类型,只需通过 fd 调用接口即可。
三、为什么要设计 "一切皆文件"?
- 简化开发:开发者无需针对不同资源(如文件、键盘、网络)学习不同的操作接口,一套逻辑即可通用。
- 灵活扩展 :新增硬件或资源时,只需按 "文件接口" 实现驱动,无需修改内核核心逻辑(如新增打印机,只需创建对应的
/dev/lp0
设备文件)。 - 便于组合 :可通过 "重定向""管道" 等机制,将不同类型的 "文件" 串联使用。例如:
echo "hello" > /dev/printer
:将字符串写入打印机设备文件,实现打印;ls | grep txt
:将ls
的输出(标准输出,文件描述符 1)通过管道(特殊文件)传给grep
的输入(标准输入,文件描述符 0)。
四、一句话总结
"一切皆文件" 不是 "所有东西都是磁盘文件",而是 Linux 用 "文件" 的抽象,给所有资源套上了统一的 "操作外壳",让进程能以相同的方式与磁盘、硬件、网络等交互,是 Linux 简洁、灵活、可扩展的核心原因之一。