

❤️@燃于AC之乐 来自重庆 计算机专业的一枚大学生
✨专注 C/C++ Linux 数据结构 算法竞赛 AI
🏞️志同道合的人会看见同一片风景!
👇点击进入作者专栏:
🌟《算法画解》算法相关题目点击即可进入实操🌟
感兴趣的可以先收藏起来,请多多支持,还有大家有相关问题都可以给我留言咨询,希望希望共同交流心得,一起进步,你我陪伴,学习路上不孤单!
文章目录
- [📖 前言](#📖 前言)
- [📁 一、深入理解文件](#📁 一、深入理解文件)
-
- [1.1 文件的独特认识](#1.1 文件的独特认识)
- [1.2 文件操作的认识](#1.2 文件操作的认识)
- [💻 二、C文件接口使用](#💻 二、C文件接口使用)
-
- [2.1 文件的打开与路径](#2.1 文件的打开与路径)
- [2.2 文件的写入操作](#2.2 文件的写入操作)
- [2.3 文件的读取操作](#2.3 文件的读取操作)
- [2.4 输出信息到显示器的多种方法](#2.4 输出信息到显示器的多种方法)
- [2.5 标准输入输出流](#2.5 标准输入输出流)
- [🚗 三、系统文件I/O](#🚗 三、系统文件I/O)
-
- [3.1 标志位传递方法](#3.1 标志位传递方法)
- [3.2 系统级文件写操作](#3.2 系统级文件写操作)
- [3.3 系统级文件读操作](#3.3 系统级文件读操作)
- [3.4 系统文件I/O接口详解](#3.4 系统文件I/O接口详解)
- [3.5 open函数返回值与文件描述符](#3.5 open函数返回值与文件描述符)
- [3.6 综合示例:文件复制工具](#3.6 综合示例:文件复制工具)
- [🎓 总结](#🎓 总结)
📖 前言
在计算机科学的核心领域,文件系统扮演着承上启下的关键角色------它既是应用程序数据存储的物理载体,也是操作系统资源管理的抽象接口。当我们谈及文件操作时,很多人会停留在C语言库函数的表面应用,却鲜少深入探究其背后的系统机制。从"一切皆文件"的Linux设计哲学,到文件描述符的精妙抽象,再到系统调用与库函数的本质区别,文件I/O的世界远比表面看起来更加深邃。本文将从底层原理到实践应用,层层剖析文件系统的奥秘,带你穿越抽象屏障,直抵操作系统内核,建立对文件I/O的完整认知体系,为后续深入系统编程奠定坚实基础。
📁 一、深入理解文件
1.1 文件的独特认识
1.狭义:
文件在磁盘中;磁盘是永久性的存储介质,从而文件在磁盘上的存储是永久的;磁盘是外设(输入/输出设备);对磁盘上文件的操作本质是对文件的操作,也都是对外设的输入输出:IO。
2,广义:
"Linux下一切皆文件"是核心抽象设计: 将硬件、进程、网络等资源统一抽象为具有路径、可通过标准文件操作(open/read/write/close)访问的对象。
主要体现: 设备文件(/dev/):磁盘(/dev/sda)、终端(/dev/tty)、输入设备等。
进程信息(/proc/):每个进程以目录形式暴露其状态、内存、命令行参数。
系统控制(/sys/):内核参数、设备属性可通过文件读写动态调整。
实现原理: 内核通过虚拟文件系统(VFS) 层统一接口,驱动或子系统实现 read、write 等标准操作,将底层操作转化为文件语义。
1.2 文件操作的认识
文件 = 属性 + 内容 。
因此,所有对文件的操作本质是对文件内容操作 和文件属性操作 。
从系统角度看,对文件的操作本质是进程 对文件的操作;而磁盘的管理者是操作系统 。
文件的读写本质不是通过C语言/C++的库函数(语言层)来实现操作的,而是通过通过文件相关的系统调用接口 来实现的,库函数 是为了用户提供方便。
库函数也都封装了底层的文件系统调用;并且OS要把被打开的文件管理起来。
💻 二、C文件接口使用
2.1 文件的打开与路径
示例代码:打开文件
c
#include <stdio.h>
int main()
{
FILE *fp = fopen("myfile", "w");
if(!fp){
printf("fopen error!\n");
}
while(1); // 为了让进程持续运行,方便查看信息
fclose(fp);
return 0;
}
问题:打开的myfile文件在哪个路径下?
答案: 在程序的当前工作目录下
如何查看程序的当前路径?
在Linux系统中,可以通过查看/proc/[进程id]目录来获取进程的相关信息:
首先获取进程ID:
bash
ps aux | grep hello
查看进程信息:
bash
ls /proc/[进程id] -l
关键符号链接:
cwd:指向当前进程运行目录的符号链接(current working directory)
exe:指向启动当前进程的可执行文件的完整路径
理解:
当进程创建时,它会继承父进程的当前工作目录。当进程使用相对路径打开文件时,操作系统会将该相对路径与进程的当前工作目录拼接,形成完整的文件路径。
2.2 文件的写入操作
示例代码:hello.c写文件
c
#include <stdio.h>
#include <string.h>
int main()
{
// 以写入模式("w")打开文件,如果文件不存在则创建,存在则清空
FILE *fp = fopen("myfile", "w");
if(!fp){
printf("fopen error!\n");
return 1;
}
const char *msg = "hello bit!\n";
int count = 5;
// 循环写入5次
while(count--){
// fwrite参数:数据指针,每个元素大小,元素个数,文件指针
fwrite(msg, strlen(msg), 1, fp);
}
fclose(fp);
return 0;
}
关键点:
"w"模式:如果文件存在则清空内容,不存在则创建
fwrite()函数将数据写入文件
写入完成后必须调用fclose()关闭文件,释放资源
2.3 文件的读取操作
示例代码:hello.c读文件
c
#include <stdio.h>
#include <string.h>
int main()
{
// 以只读模式("r")打开文件
FILE *fp = fopen("myfile", "r");
if(!fp){
printf("fopen error!\n");
return 1;
}
char buf[1024];
const char *msg = "hello bit!\n";
while(1){
// fread参数:缓冲区,每个元素大小,元素个数,文件指针
// 注意:这里使用strlen(msg)作为读取大小,可能不是最优选择
ssize_t s = fread(buf, 1, strlen(msg), fp);
if(s > 0){
buf[s] = '\0'; // 添加字符串结束符
printf("%s", buf);
}
// 检查是否到达文件末尾
if(feof(fp)){
break;
}
}
fclose(fp);
return 0;
}
改进版:实现简单cat命令
c
#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){
printf("fopen error!\n");
return 2;
}
char buf[1024];
while(1){
// 改进:使用sizeof(buf)确保读取完整缓冲区
int s = fread(buf, 1, sizeof(buf), fp);
if(s > 0){
buf[s] = '\0';
printf("%s", buf);
}
if(feof(fp)){
break;
}
}
fclose(fp);
return 0;
}
2.4 输出信息到显示器的多种方法
c
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg = "hello fwrite\n";
// 方法1:使用fwrite向stdout写入
fwrite(msg, strlen(msg), 1, stdout);
// 方法2:使用printf函数
printf("hello printf\n");
// 方法3:使用fprintf向stdout写入
fprintf(stdout, "hello fprintf\n");
// 方法4:使用puts函数(自动添加换行符)
puts("hello puts");
// 方法5:使用putchar逐个字符输出
const char *str = "hello putchar\n";
while(*str){
putchar(*str++);
}
return 0;
}
2.5 标准输入输出流
标准流的声明
c
#include <stdio.h>
// 这三个是C语言默认打开的标准流
extern FILE *stdin; // 标准输入流(默认连接到键盘)
extern FILE *stdout; // 标准输出流(默认连接到显示器)
extern FILE *stderr; // 标准错误流(默认连接到显示器)
标准流的特点 自动打开:程序启动时自动打开,无需手动fopen
不可关闭:尝试关闭这些流可能导致未定义行为
可重定向:可以通过shell重定向到文件或其他设备
bash
./program > output.txt # 标准输出重定向到文件
./program 2> error.txt # 标准错误重定向到文件
./program < input.txt # 标准输入重定向从文件读取
| 模式 | 描述 | 文件存在 | 文件不存在 | 读写位置 |
|---|---|---|---|---|
| "r" | 只读 | 打开成功 | 打开失败 | 文件开头 |
| "r+" | 读写 | 打开成功 | 打开失败 | 文件开头 |
| "w" | 只写 | 清空内容 | 创建新文件 | 文件开头 |
| "w+" | 读写 | 清空内容 | 创建新文件 | 文件开头 |
| "a" | 追加 | 打开成功 | 创建新文件 | 文件末尾 |
| "a+" | 读和追加 | 打开成功 | 创建新文件 | 读-开头,写-末尾 |
文件位置操作函数:
c
#include <stdio.h>
int main()
{
FILE *fp = fopen("test.txt", "r+");
if(!fp) return 1;
// ftell:获取当前文件位置
long pos = ftell(fp);
printf("Current position: %ld\n", pos);
// fseek:移动文件位置指针
// SEEK_SET:从文件开头
// SEEK_CUR:从当前位置
// SEEK_END:从文件末尾
fseek(fp, 10, SEEK_SET); // 移动到离文件开头10字节处
fseek(fp, -5, SEEK_END); // 移动到离文件末尾5字节处
// rewind:将文件位置指针重置到文件开头
rewind(fp);
fclose(fp);
return 0;
}
🚗 三、系统文件I/O
3.1 标志位传递方法
在深入系统文件I/O之前,我们需要先理解一种在系统编程中常用的技巧:通过位运算传递多个标志位。这种方法在系统文件IO接口中被广泛使用。
c
#include <stdio.h>
// 使用二进制位定义标志位(注意:这里应该使用16进制或8进制)
#define ONE 0x01 // 0000 0001
#define TWO 0x02 // 0000 0010
#define FOUR 0x04 // 0000 0100
#define EIGHT 0x08 // 0000 1000
void func(int flags) {
// 使用位与(&)运算检查特定标志位是否被设置
if (flags & ONE) printf("flags has ONE! ");
if (flags & TWO) printf("flags has TWO! ");
if (flags & FOUR) printf("flags has FOUR! ");
if (flags & EIGHT) printf("flags has EIGHT! ");
printf("\n");
}
int main() {
// 测试单一标志位
func(ONE);
// 输出: flags has ONE!
// 测试另一个单一标志位
func(FOUR);
// 输出: flags has FOUR!
// 测试多个标志位组合(使用位或|运算)
func(ONE | TWO);
// 输出: flags has ONE! flags has TWO!
// 测试三个标志位的组合
func(ONE | FOUR | EIGHT);
// 输出: flags has ONE! flags has FOUR! flags has EIGHT!
return 0;
}
关键点解析:
使用二进制位表示不同的标志位,每个位代表一个独立的选项
通过|(位或)运算组合多个标志位
通过&(位与)运算检查特定标志位是否被设置
这种方法可以高效地传递多个布尔选项,节省内存和参数传递开销
3.2 系统级文件写操作
c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int main()
{
// 设置文件创建时的权限掩码
// umask(0)表示不清除任何权限位
umask(0);
// 打开文件:O_WRONLY只写模式 | O_CREAT如果不存在则创建
// 第三个参数0644表示文件权限:rw-r--r--
int fd = open("myfile", O_WRONLY | O_CREAT, 0644);
if(fd < 0){
perror("open"); // 打印系统错误信息
return 1;
}
int count = 5;
const char *msg = "hello bit!\n";
int len = strlen(msg);
// 循环写入5次
while(count--){
// write系统调用:
// fd: 文件描述符(后面详细介绍)
// msg: 要写入数据的缓冲区首地址
// len: 期望写入的字节数
// 返回值: 实际写入的字节数(可能小于len)
ssize_t ret = write(fd, msg, len);
if(ret < 0){
perror("write");
break;
}
}
close(fd); // 关闭文件描述符
return 0;
}
3.3 系统级文件读操作
c
#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("myfile", O_RDONLY);
if(fd < 0){
perror("open");
return 1;
}
const char *msg = "hello bit!\n";
char buf[1024];
while(1){
// read系统调用:
// fd: 文件描述符
// buf: 读取数据的缓冲区
// strlen(msg): 期望读取的字节数
// 返回值: 实际读取的字节数(0表示文件末尾,-1表示错误)
ssize_t s = read(fd, buf, strlen(msg));
if(s > 0){
buf[s] = '\0'; // 确保字符串正确结束
printf("%s", buf);
} else if(s == 0) {
// 到达文件末尾
break;
} else {
// 读取错误
perror("read");
break;
}
}
close(fd);
return 0;
}
3.4 系统文件I/O接口详解
open函数
c
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
// open函数的两种形式
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
参数解析:
pathname: 要打开或创建的目标文件路径
flags: 打开文件的标志位(使用位或运算组合)
常用flags标志位:
c
// 基本访问模式(必须且只能指定一个)
O_RDONLY // 只读打开
O_WRONLY // 只写打开
O_RDWR // 读写打开
// 可选标志位(可以组合使用)
O_CREAT // 文件不存在时创建
O_APPEND // 追加模式(总是在文件末尾写入)
O_TRUNC // 如果文件存在且为普通文件,将其长度截断为0
O_EXCL // 与O_CREAT一起使用,如果文件存在则失败
O_NONBLOCK // 非阻塞模式
O_SYNC // 同步写入(数据立即写入磁盘)
mode: 当使用O_CREAT创建新文件时,指定文件的访问权限
权限模式说明:
权限使用8进制表示,如0644
0644表示:所有者rw-(6),组用户r--(4),其他用户r--(4)
实际权限 = mode & ~umask(umask为权限掩码)
返回值:
成功:返回新的文件描述符(非负整数)
失败:返回-1,并设置errno
其他系统文件I/O接口
c
#include <unistd.h>
// 写入数据
ssize_t write(int fd, const void *buf, size_t count);
// 读取数据
ssize_t read(int fd, void *buf, size_t count);
// 关闭文件描述符
int close(int fd);
// 移动文件读写位置
off_t lseek(int fd, off_t offset, int whence);
// whence取值:
// SEEK_SET:从文件开头偏移
// SEEK_CUR:从当前位置偏移
// SEEK_END:从文件末尾偏移
3.5 open函数返回值与文件描述符
系统调用 vs 库函数:
| 特性 | 系统调用) | 库函数) |
|---|---|---|
| 定义 | 操作系统内核提供的接口 | 编程语言标准库提供的函数 |
| 示例 | open, read, write, close | fopen, fread, fwrite, fclose |
| 位置 | 位于操作系统内核空间 | 位于用户空间库中 |
| 开销 | 较大(需要上下文切换) | 较小(在用户空间执行) |
| 封装关系 | 库函数通常封装系统调用 | 系统调用是更底层的接口 |
文件描述符(File Descriptor)
c
int main() {
// 打开一个文件,返回文件描述符
int fd = open("test.txt", O_RDONLY);
printf("File descriptor: %d\n", fd); // 通常为3
// 标准文件描述符(始终存在)
printf("stdin fd: %d\n", STDIN_FILENO); // 0
printf("stdout fd: %d\n", STDOUT_FILENO); // 1
printf("stderr fd: %d\n", STDERR_FILENO); // 2
close(fd);
return 0;
}
文件描述符的本质:
数组索引:文件描述符是进程文件描述符表(file descriptor table)的索引
进程私有:每个进程有自己的文件描述符表
从0开始:0,1,2已被标准输入、输出、错误占用
资源管理:操作系统通过文件描述符管理打开的文件
文件描述符表结构:
c
// 伪代码表示进程的文件描述符表
struct process_fd_table {
struct file_descriptor *entries[1024]; // 通常1024个条目
int next_free_fd; // 下一个可用的文件描述符
};
// 当open成功时
int open_file(const char *pathname, int flags) {
// 1. 在内核中创建或找到对应的文件对象
// 2. 在进程的文件描述符表中分配一个空闲位置
// 3. 将文件对象指针存储到该位置
// 4. 返回该位置的索引(文件描述符)
}
3.6 综合示例:文件复制工具
c
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 4096
int main(int argc, char *argv[]) {
if (argc != 3) {
fprintf(stderr, "Usage: %s <source> <destination>\n", argv[0]);
return 1;
}
// 打开源文件(只读)
int src_fd = open(argv[1], O_RDONLY);
if (src_fd < 0) {
perror("open source file");
return 1;
}
// 打开目标文件(创建或截断,读写权限)
int dst_fd = open(argv[2], O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (dst_fd < 0) {
perror("open destination file");
close(src_fd);
return 1;
}
char buffer[BUFFER_SIZE];
ssize_t bytes_read, bytes_written;
// 循环读取和写入
while ((bytes_read = read(src_fd, buffer, sizeof(buffer))) > 0) {
bytes_written = write(dst_fd, buffer, bytes_read);
if (bytes_written != bytes_read) {
perror("write");
close(src_fd);
close(dst_fd);
return 1;
}
}
if (bytes_read < 0) {
perror("read");
}
// 关闭文件描述符
close(src_fd);
close(dst_fd);
printf("File copied successfully!\n");
return 0;
}
🎓 总结
本文系统性地构建了对Linux文件操作的全景认知。从"一切皆文件"的核心抽象哲学出发,揭示了文件不仅是数据存储单元,更是操作系统统一管理各类资源的接口。通过对比C语言标准库函数与系统调用,我们深入理解了文件操作从用户空间到内核空间的完整流程。文件描述符机制作为进程与文件之间的桥梁,完美体现了操作系统的资源管理智慧。无论是基础的读写操作,还是高级的标志位组合使用,系统文件I/O都展现出比语言层接口更强大的灵活性和控制力。掌握这些知识,不仅是学习系统编程的基础,更是理解操作系统工作原理的关键一步。

加油!志同道合的人会看到同一片风景。
看到这里请点个赞 ,关注 ,如果觉得有用就收藏一下吧。后续还会持续更新的。 创作不易,还请多多支持!