文章目录
前言
本文将从文件的基本概念出发,先回顾 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 file
结构体对象的管理,它们被组织成一个双链表,保存所有当前打开的文件。
- 文件描述符表(
files_struct
)
- 每个进程都有一个
struct files_struct
类型的对象,它记录了该进程所打开的所有文件的信息。 struct files_struct
中有一个文件描述符表,维护了一个struct file*
类型的数组。数组的下标就是文件描述符,指向进程打开的文件的struct file
结构体对象。
- 文件描述符的分配规则
- 操作系统会为进程打开的新文件分配一个文件描述符,分配从 3 开始(因为标准输入、输出、错误流占用文件描述符 0、1、2)。
- 新打开的文件将从进程的文件描述符表中找到最小的未使用下标,作为文件描述符。
FILE
类型在 C 语言中的作用
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)
操作系统会在程序启动时自动打开标准输入(文件描述符 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前进的动力!