❤️@燃于AC之乐 来自重庆 计算机专业的一枚大学生
✨专注 C/C++ Linux 数据结构 算法竞赛 AI
🏞️志同道合的人会看见同一片风景!
👇点击进入作者专栏:
《算法画解》✅
《linux系统编程》✅
《C++》✅
🌟《算法画解》算法相关题目点击即可进入实操🌟
感兴趣的可以先收藏起来,请多多支持,还有大家有相关问题都可以给我留言咨询,希望希望共同交流心得,一起进步,你我陪伴,学习路上不孤单!
文章目录
- 📖 前言
- 📁 一、深入理解文件
- 1.1 文件的独特认识
- 1.2 文件操作的认识
- 💻 二、C文件接口使用
- 2.1 文件的打开与路径
- 2.2 文件的写入操作
- 2.3 文件的读取操作
- 2.4 输出信息到显示器的多种方法
- 2.5 标准输入输出流
- 🚗 三、系统文件I/O
- 3.1 标志位传递方法
- 3.2 系统级文件写操作
- 3.3 系统级文件读操作
- 3.4 系统文件I/O接口详解
- 3.5 open函数返回值与文件描述符
- 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 文件的打开与路径
示例代码:打开文件
#include<stdio.h>intmain(){FILE*fp=fopen("myfile","w");if(!fp){printf("fopen error!\n");}while(1);// 为了让进程持续运行,方便查看信息fclose(fp);return0;}问题:打开的myfile文件在哪个路径下?
答案: 在程序的当前工作目录下
如何查看程序的当前路径?
在Linux系统中,可以通过查看/proc/[进程id]目录来获取进程的相关信息:
首先获取进程ID:
psaux|grephello 查看进程信息:ls/proc/[进程id]-l 关键符号链接:cwd:指向当前进程运行目录的符号链接(current working directory)
exe:指向启动当前进程的可执行文件的完整路径
理解:
当进程创建时,它会继承父进程的当前工作目录。当进程使用相对路径打开文件时,操作系统会将该相对路径与进程的当前工作目录拼接,形成完整的文件路径。
2.2 文件的写入操作
示例代码:hello.c写文件
#include<stdio.h>#include<string.h>intmain(){// 以写入模式("w")打开文件,如果文件不存在则创建,存在则清空FILE*fp=fopen("myfile","w");if(!fp){printf("fopen error!\n");return1;}constchar*msg="hello bit!\n";intcount=5;// 循环写入5次while(count--){// fwrite参数:数据指针,每个元素大小,元素个数,文件指针fwrite(msg,strlen(msg),1,fp);}fclose(fp);return0;}关键点:
"w"模式:如果文件存在则清空内容,不存在则创建
fwrite()函数将数据写入文件
写入完成后必须调用fclose()关闭文件,释放资源
2.3 文件的读取操作
示例代码:hello.c读文件
#include<stdio.h>#include<string.h>intmain(){// 以只读模式("r")打开文件FILE*fp=fopen("myfile","r");if(!fp){printf("fopen error!\n");return1;}charbuf[1024];constchar*msg="hello bit!\n";while(1){// fread参数:缓冲区,每个元素大小,元素个数,文件指针// 注意:这里使用strlen(msg)作为读取大小,可能不是最优选择ssize_ts=fread(buf,1,strlen(msg),fp);if(s>0){buf[s]='\0';// 添加字符串结束符printf("%s",buf);}// 检查是否到达文件末尾if(feof(fp)){break;}}fclose(fp);return0;}改进版:实现简单cat命令
#include<stdio.h>#include<string.h>intmain(intargc,char*argv[]){if(argc!=2){printf("Usage: %s <filename>\n",argv[0]);return1;}FILE*fp=fopen(argv[1],"r");if(!fp){printf("fopen error!\n");return2;}charbuf[1024];while(1){// 改进:使用sizeof(buf)确保读取完整缓冲区ints=fread(buf,1,sizeof(buf),fp);if(s>0){buf[s]='\0';printf("%s",buf);}if(feof(fp)){break;}}fclose(fp);return0;}2.4 输出信息到显示器的多种方法
#include<stdio.h>#include<string.h>intmain(){constchar*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逐个字符输出constchar*str="hello putchar\n";while(*str){putchar(*str++);}return0;}2.5 标准输入输出流
标准流的声明
#include<stdio.h>// 这三个是C语言默认打开的标准流externFILE*stdin;// 标准输入流(默认连接到键盘)externFILE*stdout;// 标准输出流(默认连接到显示器)externFILE*stderr;// 标准错误流(默认连接到显示器)标准流的特点 自动打开:程序启动时自动打开,无需手动fopen
不可关闭:尝试关闭这些流可能导致未定义行为
可重定向:可以通过shell重定向到文件或其他设备
./program>output.txt# 标准输出重定向到文件./program2>error.txt# 标准错误重定向到文件./program<input.txt# 标准输入重定向从文件读取| 模式 | 描述 | 文件存在 | 文件不存在 | 读写位置 |
|---|---|---|---|---|
| “r” | 只读 | 打开成功 | 打开失败 | 文件开头 |
| “r+” | 读写 | 打开成功 | 打开失败 | 文件开头 |
| “w” | 只写 | 清空内容 | 创建新文件 | 文件开头 |
| “w+” | 读写 | 清空内容 | 创建新文件 | 文件开头 |
| “a” | 追加 | 打开成功 | 创建新文件 | 文件末尾 |
| “a+” | 读和追加 | 打开成功 | 创建新文件 | 读-开头,写-末尾 |
文件位置操作函数:
#include<stdio.h>intmain(){FILE*fp=fopen("test.txt","r+");if(!fp)return1;// ftell:获取当前文件位置longpos=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);return0;}🚗 三、系统文件I/O
3.1 标志位传递方法
在深入系统文件I/O之前,我们需要先理解一种在系统编程中常用的技巧:通过位运算传递多个标志位。这种方法在系统文件IO接口中被广泛使用。
#include<stdio.h>// 使用二进制位定义标志位(注意:这里应该使用16进制或8进制)#defineONE0x01// 0000 0001#defineTWO0x02// 0000 0010#defineFOUR0x04// 0000 0100#defineEIGHT0x08// 0000 1000voidfunc(intflags){// 使用位与(&)运算检查特定标志位是否被设置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");}intmain(){// 测试单一标志位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!return0;}关键点解析:
使用二进制位表示不同的标志位,每个位代表一个独立的选项
通过|(位或)运算组合多个标志位
通过&(位与)运算检查特定标志位是否被设置
这种方法可以高效地传递多个布尔选项,节省内存和参数传递开销
3.2 系统级文件写操作
#include<stdio.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h>#include<string.h>intmain(){// 设置文件创建时的权限掩码// umask(0)表示不清除任何权限位umask(0);// 打开文件:O_WRONLY只写模式 | O_CREAT如果不存在则创建// 第三个参数0644表示文件权限:rw-r--r--intfd=open("myfile",O_WRONLY|O_CREAT,0644);if(fd<0){perror("open");// 打印系统错误信息return1;}intcount=5;constchar*msg="hello bit!\n";intlen=strlen(msg);// 循环写入5次while(count--){// write系统调用:// fd: 文件描述符(后面详细介绍)// msg: 要写入数据的缓冲区首地址// len: 期望写入的字节数// 返回值: 实际写入的字节数(可能小于len)ssize_tret=write(fd,msg,len);if(ret<0){perror("write");break;}}close(fd);// 关闭文件描述符return0;}3.3 系统级文件读操作
#include<stdio.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h>#include<string.h>intmain(){// 以只读模式打开文件intfd=open("myfile",O_RDONLY);if(fd<0){perror("open");return1;}constchar*msg="hello bit!\n";charbuf[1024];while(1){// read系统调用:// fd: 文件描述符// buf: 读取数据的缓冲区// strlen(msg): 期望读取的字节数// 返回值: 实际读取的字节数(0表示文件末尾,-1表示错误)ssize_ts=read(fd,buf,strlen(msg));if(s>0){buf[s]='\0';// 确保字符串正确结束printf("%s",buf);}elseif(s==0){// 到达文件末尾break;}else{// 读取错误perror("read");break;}}close(fd);return0;}3.4 系统文件I/O接口详解
open函数
#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>// open函数的两种形式intopen(constchar*pathname,intflags);intopen(constchar*pathname,intflags,mode_tmode);参数解析:
pathname: 要打开或创建的目标文件路径
flags: 打开文件的标志位(使用位或运算组合)
常用flags标志位:
// 基本访问模式(必须且只能指定一个)O_RDONLY// 只读打开O_WRONLY// 只写打开O_RDWR// 读写打开// 可选标志位(可以组合使用)O_CREAT// 文件不存在时创建O_APPEND// 追加模式(总是在文件末尾写入)O_TRUNC// 如果文件存在且为普通文件,将其长度截断为0O_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接口
#include<unistd.h>// 写入数据ssize_twrite(intfd,constvoid*buf,size_tcount);// 读取数据ssize_tread(intfd,void*buf,size_tcount);// 关闭文件描述符intclose(intfd);// 移动文件读写位置off_tlseek(intfd,off_toffset,intwhence);// whence取值:// SEEK_SET:从文件开头偏移// SEEK_CUR:从当前位置偏移// SEEK_END:从文件末尾偏移3.5 open函数返回值与文件描述符
系统调用 vs 库函数:
| 特性 | 系统调用) | 库函数) |
|---|---|---|
| 定义 | 操作系统内核提供的接口 | 编程语言标准库提供的函数 |
| 示例 | open, read, write, close | fopen, fread, fwrite, fclose |
| 位置 | 位于操作系统内核空间 | 位于用户空间库中 |
| 开销 | 较大(需要上下文切换) | 较小(在用户空间执行) |
| 封装关系 | 库函数通常封装系统调用 | 系统调用是更底层的接口 |
文件描述符(File Descriptor)
intmain(){// 打开一个文件,返回文件描述符intfd=open("test.txt",O_RDONLY);printf("File descriptor: %d\n",fd);// 通常为3// 标准文件描述符(始终存在)printf("stdin fd: %d\n",STDIN_FILENO);// 0printf("stdout fd: %d\n",STDOUT_FILENO);// 1printf("stderr fd: %d\n",STDERR_FILENO);// 2close(fd);return0;}文件描述符的本质:
数组索引:文件描述符是进程文件描述符表(file descriptor table)的索引
进程私有:每个进程有自己的文件描述符表
从0开始:0,1,2已被标准输入、输出、错误占用
资源管理:操作系统通过文件描述符管理打开的文件
文件描述符表结构:
// 伪代码表示进程的文件描述符表structprocess_fd_table{structfile_descriptor*entries[1024];// 通常1024个条目intnext_free_fd;// 下一个可用的文件描述符};// 当open成功时intopen_file(constchar*pathname,intflags){// 1. 在内核中创建或找到对应的文件对象// 2. 在进程的文件描述符表中分配一个空闲位置// 3. 将文件对象指针存储到该位置// 4. 返回该位置的索引(文件描述符)}3.6 综合示例:文件复制工具
#include<stdio.h>#include<stdlib.h>#include<fcntl.h>#include<unistd.h>#include<string.h>#defineBUFFER_SIZE4096intmain(intargc,char*argv[]){if(argc!=3){fprintf(stderr,"Usage: %s <source> <destination>\n",argv[0]);return1;}// 打开源文件(只读)intsrc_fd=open(argv[1],O_RDONLY);if(src_fd<0){perror("open source file");return1;}// 打开目标文件(创建或截断,读写权限)intdst_fd=open(argv[2],O_WRONLY|O_CREAT|O_TRUNC,0644);if(dst_fd<0){perror("open destination file");close(src_fd);return1;}charbuffer[BUFFER_SIZE];ssize_tbytes_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);return1;}}if(bytes_read<0){perror("read");}// 关闭文件描述符close(src_fd);close(dst_fd);printf("File copied successfully!\n");return0;}🎓 总结
本文系统性地构建了对Linux文件操作的全景认知。从“一切皆文件”的核心抽象哲学出发,揭示了文件不仅是数据存储单元,更是操作系统统一管理各类资源的接口。通过对比C语言标准库函数与系统调用,我们深入理解了文件操作从用户空间到内核空间的完整流程。文件描述符机制作为进程与文件之间的桥梁,完美体现了操作系统的资源管理智慧。无论是基础的读写操作,还是高级的标志位组合使用,系统文件I/O都展现出比语言层接口更强大的灵活性和控制力。掌握这些知识,不仅是学习系统编程的基础,更是理解操作系统工作原理的关键一步。
加油!志同道合的人会看到同一片风景。
看到这里请点个赞,关注,如果觉得有用就收藏一下吧。后续还会持续更新的。 创作不易,还请多多支持!