文章目录
- 前言
- lesson 15_基础IO
-
- 一、共识原理
- 二、回顾C语言接口
-
- 2.1 文件的打开操作
- 2.2 文件的读取与写入操作
- 2.3 三个标准输入输出流
- 三、过渡到系统,认识文件系统调用
-
- 3.1 `open` 系统调用
-
- 1. 比特位标志位示例
- 3.2 `write` 系统调用
-
- 1. 模拟实现 `w` 选项
- 2. 模拟实现 `a` 选项
- 3.3 `read` 系统调用
- 四、访问文件的本质
- 结语
前言
本文将从文件的基本概念出发,先回顾 C 语言中文件操作的常用接口,再逐步过渡到 Linux 系统调用,解析文件描述符、文件打开对象、进程与文件的关系等关键概念。通过代码示例和原理分析,带你揭开 Linux 基础 IO 的神秘面纱,理解操作系统如何管理文件、进程如何与文件交互的底层逻辑。
lesson 15_基础IO
一、共识原理
-
文件 = 内容 + 属性。
-
文件分为 打开的文件 和 没打开的文件。
-
打开的文件:谁打开的?进程!—— 本质是研究进程和文件的关系。
-
没打开的文件:在哪里放着呢?在磁盘上。我们最关注的问题?没有被打开的文件非常多,文件如何被分门别类的放置好(如何存储) —— 我们要快速的进行增删查改 —— 快速找到文件。
-
文件被打开,必须先被加载到内存!
-
进程:打开的文件 = 1:n。
小结:操作系统内部,一定存在大量的别打开的文件!—— OS 要不要管理这些被打开的文件呢? —— 怎么管理???—— 先描述,在组织。—— 在内核中,一个被打开的文件都必须有自己的文件打开对象,包含文件的很多属性。struct XXX{文件属性;struct XXX *next};
二、回顾C语言接口
2.1 文件的打开操作
-
fopen 函数用于打开文件,格式为
- FILE *fopen(const char *path, const char *mode);
-
path: 文件路径或文件名。如果只有文件名,操作系统会在当前工作目录(cwd)下查找该文件。
-
mode: 文件打开模式。常见模式有:
- w: 如果文件已存在,先清空文件再写入。如果文件不存在,创建新文件。
- a: 以追加模式打开文件,在文件末尾添加内容。
-
当前路径 (cwd): 每个进程维护一个当前工作目录,操作系统会根据该目录来查找文件。如果路径没有指定,fopen 会使用进程的当前工作路径。
-
2.2 文件的读取与写入操作
-
fwrite 用于向文件写入数据。其函数声明为:
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);
- ptr: 指向要写入数据的指针。
- size: 每个写入对象的大小。
- nmemb: 要写入的对象个数。
- stream:文件流指针
-
举例使用:
int main()
{
FILE *fp = fopen("log.txt", "w");
if (fp == NULL)
{
perror("fopen");
return errno;
}
char* str = "Hello Linux!";
fwrite(str, strlen(str), 1, fp);
fclose(fp);
return 0;
}-
fwrite的第二个参数是指每个写入对象的大小,strlen函数返回的值是不包含字符串结束标识符,那么我们传参是加一还是不加一呢?加一就代表把\\0写入到文件中,那么我们是应该怎么选择呢?这里不妨试一试加一的结果:
注意 log.txt 文件中,字符串的末尾有一个^@,是什么意思呢?实际上这个字符组合是表示\\0的ASCII码,所以写入字符串时,使用 strlen 计算字符串长度时,不包括结束符 \\0。通常不需要将 \\0 写入文件,因为它是 C 语言中的结束标志,而在其它语言中读取文件时,可能不希望看到这些无关的字符。
-
2.3 三个标准输入输出流
C 程序启动时,会自动打开以下三个标准流:
- stdin: 标准输入流(通常与键盘连接)。
- stdout: 标准输出流(通常与显示器连接)。
- stderr: 标准错误流(通常与显示器连接)。
这三个流都由操作系统和 C 标准库提供,并用于处理程序与外部交互的基本输入输出。
-
三、过渡到系统,认识文件系统调用
文件其实是在磁盘上的,磁盘是外部设备,访问磁盘文件其实是访问硬件!几乎所有的库只要是访问硬件设备,必定要封装系统调用。
3.1 open 系统调用
open 是一个用于打开文件或创建文件的系统调用,其原型为:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
- 参数说明:
- pathname: 文件路径。
- flags: 打开文件时的标志,例如:
- O_RDONLY:只读打开。
- O_WRONLY:只写打开。
- O_RDWR:读写打开。
- O_CREAT:文件不存在时创建文件。
- O_TRUNC:打开文件时清空文件内容。
- O_APPEND:以追加模式打开文件。
- mode: 在使用 O_CREAT 时,需要指定新文件的访问权限。
- 返回值:成功返回文件描述符,失败返回 -1。
1. 比特位标志位示例
通过按位或(|)传递多个标志位,可以在同一次调用中同时指定多个选项。
代码示例:
#define ONE (1<<0) // 1
#define TWO (1<<1) // 2
#define FOUR (1<<2) // 4
#define EIGHT (1<<3) // 8
void show(int flags)
{
if(flags & ONE) printf("function1\\n");
if(flags & TWO) printf("function2\\n");
if(flags & FOUR) printf("function3\\n");
if(flags & EIGHT) printf("function4\\n");
return;
}
int main()
{
printf("————————————–\\n");
show(ONE);
printf("————————————–\\n");
show(ONE | TWO);
printf("————————————–\\n");
show(ONE | TWO | FOUR );
printf("————————————–\\n");
show(ONE | TWO | FOUR | EIGHT);
printf("————————————–\\n");
return 0;
}
输出示例:
3.2 write 系统调用
write 用于将数据写入文件,其原型为:
ssize_t write(int fd, const void *buf, size_t count);
- 参数说明:
- fd: 文件描述符。
- buf: 指向数据缓冲区的指针。
- count: 要写入的数据字节数。
- 返回值:实际写入的字节数。
1. 模拟实现 w 选项
模拟 fopen 的 w 模式(清空文件后写入):
int main()
{
umask(0); // 将权限掩码设置成0000
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if(fd < 0)
{
printf("open file error\\n");
return 1;
}
const char* str = "bbb";
ssize_t ret = write(fd, str, strlen(str));
close(fd);
return 0;
}
2. 模拟实现 a 选项
模拟 fopen 的 a 模式(追加写):
int main()
{
umask(0);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
if(fd < 0)
{
printf("open file error\\n");
return errno;
}
const char* str = "bbb";
ssize_t ret = write(fd, str, strlen(str));
close(fd);
return 0;
}
3.3 read 系统调用
read 用于从文件中读取数据,其原型为:
ssize_t read(int fd, void *buf, size_t count);
- 参数说明:
- fd: 文件描述符。
- buf: 存储读取数据的缓冲区。
- count: 缓冲区的大小。
- 返回值:实际读取的字节数。
四、访问文件的本质
- 当文件被打开时,操作系统为该文件创建一个 struct file 结构体对象,负责管理该文件的元数据和访问信息。
- 操作系统对文件的管理本质上就是对这些 struct file 结构体对象的管理,它们被组织成一个双链表,保存所有当前打开的文件。
- 每个进程都有一个 struct files_struct 类型的对象,它记录了该进程所打开的所有文件的信息。
- struct files_struct 中有一个文件描述符表,维护了一个 struct file* 类型的数组。数组的下标就是文件描述符,指向进程打开的文件的 struct file 结构体对象。
- 操作系统会为进程打开的新文件分配一个文件描述符,分配从 3 开始(因为标准输入、输出、错误流占用文件描述符 0、1、2)。
- 新打开的文件将从进程的文件描述符表中找到最小的未使用下标,作为文件描述符。
-
FILE 是 C 语言库中的封装类型,用于描述文件,它提供了更高层次的文件操作接口。
-
FILE 类型内部封装了文件描述符,_fileno 字段就是对应的文件描述符,可以通过它来访问底层的文件描述符。
int main()
{
umask(0);
int fd1 = open("log1.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd2 = open("log2.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd3 = open("log3.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
int fd4 = open("log4.txt", O_WRONLY | O_CREAT | O_APPEND, 0666);
printf("fd1: %d\\n", fd1);
printf("fd2: %d\\n", fd2);
printf("fd3: %d\\n", fd3);
printf("fd4: %d\\n", fd4);return 0;
}
- 文件可以被多个进程同时打开,struct file 中有一个 f_count 字段来记录文件的引用计数。
- 当进程关闭文件时,close 系统调用会将文件描述符表中对应位置的内容置为 NULL,减少文件的引用计数。如果引用计数为 0,操作系统会回收该文件对应的资源。
-
操作系统会在程序启动时自动打开标准输入(文件描述符 0)、标准输出(文件描述符 1)和标准错误(文件描述符 2)。
-
这三个文件描述符是预留的,程序中打开的新文件会从文件描述符 3 开始。
-
通过 close 系统调用关闭文件描述符后,进程无法再通过该文件描述符进行文件操作。例如,关闭标准输出(close(1))会导致后续的 printf 输出无法显示,但其他流如标准错误仍然有效。
int main()
{
close(1); // 将 stdout 关闭
int ret = printf("stdin->fd: %d\\n", stdin->_fileno);
printf("stdout->fd: %d\\n", stdout->_fileno);
printf("stderr->fd: %d\\n", stderr->_fileno);fprintf(stderr, "printf ret: %d\\n", ret);
return 0;
}
结语
IO 操作是操作系统的 “血脉”,理解其底层原理不仅能帮助我们写出更健壮的代码,还能为深入学习进程通信、网络编程等高级主题奠定基础。希望本文能成为你探索 Linux 系统编程的一块基石,在后续的学习中,你可以尝试结合实际项目,对比不同 IO 接口的性能差异,或深入分析内核源码中的文件管理逻辑,进一步提升技术深度。
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,17的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是17前进的动力!
评论前必须登录!
注册