1、回顾C语言文件接口
目录
1.1、文件打开
FILE * fopen ( const char * filename, const char * mode );
通过文件名以指定打开方式,打开文件
打开方式(参数2)
- w 只写,如果文件不存在,会新建,文件写入前,会先清空内容
- a 追加,在文件末尾,对文件进行追加写入,追加前不会清空内容
- r 只读,打开已存在的文件进行读取,若文件不存在,会打开失败
- w+、a+、r+ 读写兼具,区别在于是否会新建文件,只有 r+ 不会新建
//打开文件进行操作
//在当前目录中打开文件 log.txt
//注意:同一个文件,可以同时多次打开
FILE* fp1 = fopen("log.txt", "w"); //只读
FILE* fp2 = fopen("log.txt", "a"); //追加
FILE* fp3 = fopen("log.txt", "r"); //只写,文件不存在会打开失败
FILE* fp4 = fopen("log.txt", "w+"); //可读可写
FILE* fp5 = fopen("log.txt", "a+"); //可读可追加
FILE* fp6 = fopen("log.txt", "r+"); //可读可写,文件不存在会打开失败
若文件打开失败,会返回空 NULL
,可以在打开后判断是否成功 。
如果文件不存在,会创建新的文件,文件创建的位置和进程当前工作目录一样。如果在程序中使用了chdir(path)函数修改了进程的当前工作目录,那么创建的文件所处路径会和进程工作目录一样。
进程相关的路径:
1. 运行时路径
定义:运行时路径指的是可执行文件的存储位置,也就是程序启动时操作系统加载该程序的路径。它是程序的固定存储路径。
是否可以修改:不可修改。一旦程序启动,运行时路径由操作系统确定,并且是固定的。如果需要修改,只能通过移动可执行文件到新的位置并重新启动程序。
2. 当前工作目录
定义:当前工作目录是进程的默认目录,用于解析相对路径。也就是说,当进程打开文件时,如果没有提供完整路径,操作系统会默认从当前工作目录开始查找文件。
是否可以修改:可以修改。进程可以通过调用
chdir()
系统调用动态修改当前工作目录。修改后,文件操作将基于新的工作目录。chdir()修改并不会影响环境变量路径。3. 可执行文件路径
定义:可执行文件路径是指程序存储在硬盘上的位置,即程序的文件系统路径。它表示当前执行的程序的具体位置。
是否可以修改:不可修改。程序的可执行文件路径在程序启动时由操作系统确定,并且通常不允许在运行时更改。如果需要修改路径,只能通过移动程序文件并重新启动。
4. 环境变量路径
定义:环境变量路径通常指的是与系统或进程相关的路径信息,如
PATH
(系统可执行文件的查找路径)和HOME
(用户的家目录)。这些路径会影响程序在运行时的行为和资源查找。是否可以修改:可以修改。环境变量可以在进程运行时通过
setenv()
或putenv()
等函数进行修改,影响程序的运行行为。例如,修改PATH
环境变量可以改变程序查找命令的路径。5. 相对路径
定义:相对路径是相对于当前工作目录的路径。比如,如果当前工作目录是
/home/user
,文件data.txt
的相对路径就是data.txt
,它会被解析为/home/user/data.txt
。是否可以修改:依赖于工作目录的变化。相对路径本身不能直接修改,但它依赖于当前工作目录的变化。工作目录一旦改变,相对路径的解析结果就会变化。
6. 绝对路径
定义:绝对路径是从文件系统的根目录开始的完整路径,不依赖于当前工作目录。例如,
/home/user/data.txt
是一个绝对路径。是否可以修改:不可修改。绝对路径表示文件系统中固定的文件或目录位置,只有文件或目录本身可以移动或修改,而路径本身是固定的。
1.2 文件关闭
文件打开并使用后需要关闭,就像动态内存申请后需要释放一样
int fclose ( FILE * stream );
关闭已打开文件,只需通过 FILE*
指针进行操作即可
//对上面打开的文件进行关闭
//无论以哪种方式打开,关闭方法都一样
fclose(fp1);
fclose(fp2);
fclose(fp3);
fclose(fp4);
fclose(fp5);
fclose(fp6);
注意: 只能对已打开的文件进行关闭,若文件不存在,会报错。
1.3、文件写入
C语言
对于文件写入有这几种方式:fputc
、fputs
、fwrite
、fprintf
和 snprintf
int fputc ( int character, FILE * stream );
int fputs ( const char * str, FILE * stream );
size_t fwrite ( const void * ptr, size_t size, size_t count, FILE * stream );
//返回值是写入成功的 元素个数,即它成功写入的对象的数量。如果返回值小于 count,表示有部分数据写入失败。
int snprintf ( char * s, size_t n, const char * format, ... );
前几种方式比较简单,无非就是 逐字符写入、逐行写入 与 格式化写入,这里主要来介绍一下 snprintf
snprintf 是 sprintf 的优化版,增加了读取字符长度控制,更加安全
参数1:缓冲区,常写做 buffer 数组
参数2:缓冲区的大小
参数3:格式化输入,比如 "%d\n", 10
使用 snprintf 函数写入数据至缓冲区后,可以再次通过 fputs 函数,将缓冲区中的数据真正写入文件中。
#include <stdio.h>
#include <stdlib.h>
#define LOG "log.txt" //日志文件
#define SIZE 32
int main()
{
FILE* fp = fopen(LOG, "w");
if(!fp)
{
perror("fopen file fail!"); //报错
exit(-1); //终止进程
}
char buffer[SIZE]; //缓冲区
int cnt = 5;
while(cnt--)
{
snprintf(buffer, SIZE, "%s\n", "Hello File!"); //写入数据至缓冲区
fputs(buffer, fp); //将缓冲区中的内容写入文件中
}
fclose(fp);
fp = NULL;
return 0;
}
得益于格式化控制,可以灵活地向日志文件中写入内容 。
2.文件操作的本质
文件存储在磁盘当中,而磁盘属于外部设备,根据高级语言操作的本质,用户不能直接越过操作系统向外部设备进行读写操作,C语言中使用的fopen,fwrite,fclose,实际上封了系统调用接口,通过操作系统封装的接口,用户才能向文件读写。操作系统为进程创建PCB结构体,进行管理,同理也为文件创建了struct_file结构体,其中包含了文件的各种信息。
1. 文件存储和硬件交互
文件最终是存储在磁盘中,磁盘作为外部设备,无法直接进行读写操作。程序(尤其是用户程序)不能直接访问硬件,这样做会带来很多问题,如安全性、并发性问题、文件访问冲突等。
因此,操作系统作为硬件的管理者,提供了文件访问的机制,确保每个程序能够在不直接接触硬件的情况下进行文件的读写操作。
2. 操作系统对硬件的管理
操作系统通过设备驱动程序(如硬盘驱动、文件系统驱动)与硬件交互,屏蔽了底层硬件的细节。用户程序通过系统调用接口(如
open
、read
、write
)与操作系统进行通信,从而间接与磁盘等外设进行文件操作。操作系统负责调度和管理文件访问,并且通过 文件系统 (如 ext4、NTFS、FAT32 等)提供统一的文件存储与访问接口,文件系统通过操作系统的文件控制结构来管理文件元数据、存储位置和文件描述符等。
3. C 语言中的文件操作
在 C 语言中,文件操作是通过标准库函数(如
fopen
、fwrite
、fclose
等)来完成的。这些函数实际上是对操作系统系统调用接口的封装,程序员通过这些高层接口与操作系统交互,进而实现文件读写。
fopen()
:打开文件并返回文件指针,操作系统会为该文件创建一个文件控制结构(struct file
)。
fwrite()
:向文件写入数据,操作系统通过相应的系统调用(如write
)将数据写入磁盘。
fclose()
:关闭文件,并释放相关资源,操作系统会更新文件控制信息(如偏移量、文件描述符等)并释放相应的文件控制结构。在这个过程中,操作系统对底层磁盘操作进行了抽象和封装,程序员不需要关心底层的文件存储机制,只需使用提供的高层 API。
4. 操作系统如何管理进程与文件
操作系统不仅管理进程(通过 PCB),也管理文件。每个打开的文件都会在操作系统中创建一个 文件控制块(struct file) 来记录该文件的各种信息,常见的文件信息包括:
文件描述符:每个打开的文件都会分配一个唯一的文件描述符,用于标识文件。每个进程会有自己的文件描述符表。
文件指针:文件的当前位置指针,用于记录文件的当前读写位置。
文件访问模式:文件是以读、写、追加等模式打开的,文件控制结构中包含了这个信息。
文件状态:文件的锁定状态、是否已经打开、是否需要缓存等。
struct file
是操作系统内部的数据结构,它帮助操作系统管理文件访问,确保文件操作的安全性和一致性。5. 系统调用与文件操作的关系
操作系统通过系统调用为用户程序提供访问硬件的接口。用户程序使用这些系统调用来进行文件操作,如打开文件、读取数据、写入数据等。对于文件操作,系统调用通常包括以下几个步骤:
打开文件:系统调用
open
或fopen
会创建一个文件描述符,操作系统会检查文件是否存在,权限是否满足,返回文件描述符。读取文件:系统调用
read
或fread
会从文件的当前位置读取数据。操作系统将数据从磁盘加载到内存中,然后返回给程序。写入文件:系统调用
write
或fwrite
会将数据写入文件。操作系统会将数据从内存写入磁盘,并更新文件的当前位置。关闭文件:系统调用
close
或fclose
会关闭文件描述符,操作系统释放相关资源,并更新文件控制信息。6. 文件系统与文件操作
操作系统通过文件系统来管理磁盘上的文件。文件系统将文件数据存储在硬盘的特定区域,并管理文件的目录结构、文件的元数据(如大小、权限、创建时间等)以及文件的访问权限。
常见的文件系统有:
EXT4:Linux 常用的文件系统,支持文件访问权限、数据冗余、日志功能等。
NTFS:Windows 操作系统使用的文件系统,支持更强的文件权限控制和文件加密。
FAT32:较老的文件系统,广泛用于移动存储设备(如U盘)。
每种文件系统有自己的存储方式和访问协议,操作系统通过文件系统提供的接口来管理文件的存取。
7. 系统调用与标准库函数的区别
系统调用:操作系统提供的直接访问硬件的接口,如
open
、read
、write
、close
等。标准库函数:例如 C 语言中的
fopen
、fread
、fwrite
、fclose
等,它们是对系统调用的封装,提供了更高层次的抽象和易用性。通过这种封装,程序员可以更加简便地进行文件操作,而不需要直接处理系统调用的底层细节。
3.系统级文件操作
系统调用可能用到的头文件
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<string.h>
#include<unistd.h>
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); //可以修改权限
函数理解
返回值:不同于 FILE*,系统级文件打开函数返回类型为 int,即 文件描述符( file descriptor ),文件打开失败返回 -1
文件描述符很重要,将在下篇文章 《重定向本质》 中讲解
参数1:pathname 待操作文件名,和 fopen 一样
参数2:flags 打开选项,open 使用的标记位的方式传递选项信号,用一个 int 至多可以表示 32 个选项
参数3:mode 权限设置,文件起始权限为 0666
主要就是参数2有点复杂,使用了 位图 的方式进行多参数传递。
可以利用这个特性,写一个关于位图的小demo
#include <stdio.h>
#include <stdlib.h>
#define ONE 0x1
#define TWO 0x2
#define THREE 0x4
void Test(int flags)
{
//模拟实现三种选项传递
if(flags & ONE)
printf("This is one\n");
if(flags & TWO)
printf("This is two\n");
if(flags & THREE)
printf("This is three\n");
}
int main()
{
Test(ONE | TWO | THREE);
printf("-----------------------------------\n");
Test(ONE); //位图使得选项传递更加灵活
return 0;
}
函数 open
中的参数2正是位图,其参数有很多个,这里列举部分
O_RDONLY //只读
O_WRONLY //只写
O_APPEND //追加
O_CREAT //新建
O_TRUNC //清空
实际使用时,可以按照位图demo中的方式进行参数传递
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h> //write 的头文件
#define LOG "log.txt" //日志文件
#define SIZE 32
int main()
{
//三种参数组合,就构成了 fopen 中的 w
int fd = open(LOG, O_WRONLY | O_CREAT | O_TRUNC, 0666); //权限最好设置
if(fd == -1)
{
perror("open file fail1");
exit(-1);
}
const char* ps = "Hello System Call!\n";
int cnt = 5;
while(cnt--)
write(fd, ps, strlen(ps)); //不能将 '\0' 写入文件中
close(fd);
return 0;
}
注意:
- 假若文件不存在,open 中的参数3最好进行设置,否则创建出来的文件权限为随机值
- 继承环境变量表后,umask 默认为 0002,当然也可以自定义
- 通过系统级函数 write 写入字符串时,不要刻意加上 '\0',因为对于系统来说,这也只是一个普通的字符('\0' 作为字符串结尾只是 C语言 的规定)
C语言 中的 fopen 调用 open 函数,其中的选项对应关系如下
- w -> O_WRONLY | O_CREAT | O_TRUNC
- a -> O_WRONLY | O_CREAT | O_APPEND
- r -> O_RDONLY
- ……
所以只要我们想,使用 open 时,也能做到 只读方式 打开 不存在的文件,也不会报错,加个 O_CREAT 参数即可
3.2、关闭 close
close 函数根据文件描述符关闭文件
#include <unistd.h>
int close(int fildes);
Linux 下一切皆文件
包括这三个标准流: stdin、stdout、stderr
它们的文件描述符依次为:0、1、2,也可以通过 close(1) 的方式,关闭标准流
3.3、写入 write
write 函数的返回值类型有点特殊,但使用方法与 fwrite 基本一致
#include <unistd.h>
ssize_t write(int fildes, const void *buf, size_t nbyte);
向文件中写入字符串,前面已经演示过了~
高级语言文件操作的本质
只要是在 Linux 平台中编写的程序,无论是 Java、Python、PHP 还是其他语言,在进行文件相关操作时,其文件操作函数都有对系统级函数进行封装,也就是说,要想与硬件(磁盘)打交道,必须经过 系统调用 -> OS -> 驱动 这条路线,无法直接与硬件进行交互