目录
前言
在我们学习文件之前得先明白文件是什么?
——文件 = 内容 + 属性——
文件是谁打开的呢?
——文件由进程调度打开,当然前提是文件必须先加载入内存中~我们在访问文件之前必须得先打开,修改文件就是通过执行代码的方式进行修改——
文件都是呈现被进程打开的状态吗?
——虽然一个进程可以打开很多个文件,但并不是会全部打开,不去访问的文件会存储在磁盘中等待——
一. C文件接口
我们先来进行一个简单的文件写入操作~
#include <stdio.h> int main() { FILE* fp = fopen("./log.txt","w"); if(fp==NULL) { perror("fopen"); return 1; } //写入操作 const char* str = "hello\n"; fputs(str,fp); fclose(fp); return 0; }
我们以写方式打开文件~若文件不存在则会在原路径下创建文件,用fputs函数把写入的内容传到文件中~
如果我们尝试注释掉写入操作,那么原来的文件内容还会存在吗?
之前的文件内容消失了~明明我们已经往里面写入内容了且文件没有被删除,为什么下一次打开内容会消失呢?
总结:以W方式打开的文件每一次打开里面的内容都会自动清空~
我们接着换追加的方式进行打开文件~
#include <stdio.h> int main() { FILE* fp = fopen("./log.txt","a"); if(fp==NULL) { perror("fopen"); return 1; } //写入操作 const char* str = "hello\n"; fputs(str,fp); fclose(fp); return 0; }
这时候就可以发现我们每一次打开文件都可以保留上一次的内容了~
ps:echo " " > 文件名 与echo " " >> 文件名这两条指令本质就是以w方式打开并写入与以a方式打开并写入~
下面我们再来学习一个写入函数fwrite
#include <stdio.h> #include <string.h> #define FILENAME "log.txt" int main() { FILE* fp = fopen(FILENAME,"w"); if(fp==NULL) { perror("fopen"); return 1; } //写入操作 const char* str = "hello\n"; int cnt = 5; while(cnt) { int n = fwrite(str,strlen(str),1,fp); printf("write %d block\n",n); cnt--; } fclose(fp); return 0; }
fwrite写入成功时的返回值为1;而传递的参数分别为:
写入的内容;多少个基本单位;基本单位的大小(字节);写入的方向~
我们利用fget函数来读取在文件中的内容~
#include <stdio.h> #include <string.h> #define FILENAME "log.txt" int main() { FILE* fp = fopen(FILENAME,"r"); if(fp==NULL) { perror("fopen"); return 1; } //读取操作 char buffer[64]; while(1) { char * r = fgets(buffer,sizeof(buffer),fp); if(!r) break; printf("%s", buffer); } return 0; }
下面为大家介绍一下程序默认打开的三个文件流:
- stdin 标准输入 键盘设备
- stdout 标准输出 显示器设备
- stderr 标准错误 显示器设备
难道我们就没有怀疑过吗?我们从键盘上获取输入的信息,从显示器上读取输出的信息本质上都是去访问硬件设备的~而这些设备一定会有对应的文件来方便操作系统进行管理~
所以每次程序启动的时候这三个文件系统会帮我们打开,节省我们的时间成本~我们总不能每写一个程序都要手动去打开这三个必须要执行的文件吧?
#include <stdio.h> #include <string.h> #define FILENAME "log.txt" int main() { printf("hello printf\n"); fputs("hello fputs\n",stdout); const char* str = "hello,fwrite\n"; fwrite(str,strlen(str),1,stdout); fprintf(stdout,"hello fprintf\n"); return 0; }
#include <stdio.h> #include <string.h> #define FILENAME "log.txt" int main() { char buffer[64]; fscanf(stdin,"%s", buffer); printf("%s", buffer); return 0; }
二. 文件系统调用接口
访问文件不仅仅有c语言上的文件接口,OS也必须提供对应的访问文件的系统调用
open接口
我们先来了解一下参数flags的使用~
#define ONE 1 #define TWO (1<<1) #define THREE (1<<2) #define FOUR (1<<3) #define FIVE (1<<4) void Print(int flag) { if(flag & ONE) printf("1\n"); if(flag & TWO) printf("2\n"); if(flag & THREE) printf("3\n"); if(flag & FOUR) printf("4\n"); if(flag & FIVE) printf("5\n"); } int main() { Print(ONE); printf("----------------------\n"); Print(TWO); printf("----------------------\n"); Print(ONE|TWO); printf("----------------------\n"); Print(THREE|FOUR|FIVE); printf("----------------------\n"); Print(ONE|TWO|THREE|FOUR|FIVE); }
flag可以传递多个参数,不再局限于参数的数量~ 本质上传递的参数为宏~
#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("log.txt",O_WRONLY|O_CREAT); if(fd==-1) { perror("open"); return 1; } }
我们使用系统调用接口open对文件进行写操作方式打开,如果没有文件则在该路径下创建文件~
我们发现创建好的文件权限出现了问题?而这时候就需要我们的另一个参数mode上场了~
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() { umask(0); int fd = open("log.txt",O_WRONLY|O_CREAT,0666); if(fd==-1) { perror("open"); return 1; } }
我们手工给文件赋予权限~
下面我们来介绍一下open返回值fd(文件描述符)的特点~
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> int main() { umask(0); int fd = open("log.txt",O_WRONLY|O_CREAT,0666); if(fd==-1) { perror("open"); return 1; } const char* str = "hello\n"; write(fd,str,strlen(str)); close(fd); return 0; }
使用系统调用接口时,我们进行写入操作(write)需要fd ,关闭文件(close)也需要fd。
我们再把字符串改为aa且不带换行~
这时候发现新的内容直接和旧内容重叠了~
我们新添加的O_TRUNC宏定义可以在每次打开文件时清空原有的内容~这样就相当于c语言当中的w方式了
若我们想要进行追加内容,可以用宏定义:O_APPEND,这样整体就相当于c语言中的a方式了
我们回归主题来继续讨论返回值fd的用途~
#include <stdio.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <string.h> 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; }
我们发现每个文件返回的fd都不一样的,并且还是以递增的顺序~那么最前面的三个又是谁呢?
int main() { printf("%d\n", stdin->_fileno); printf("%d\n", stdout->_fileno); printf("%d\n", stderr->_fileno); return 0; }
其实就是系统默认帮我们打开的文件流~
它们的fd被c语言封装到_fileno的成员变量内~
通过这张图我们可以看到所接触的c语言接口其实本质都是封装了系统调用
而在c语言接口中之所以用FILE* 而不是fd的原因就是因为fd这个类型也被c语言封装了~
之所以封装都是为了可移植性,如果我们用系统调用那就得考虑是在Linux环境下还是Windows环境下,但如果是用c标准库这些封装系统调用接口的函数就会去自行匹配环境差异,我们去交给操作系统干~
为什么fd文件描述符是数字呢?这其中还代表了什么含义吗?
打开多个文件需要管理,因此有了操作系统的文件管理~为了让进程管理与文件管理之间出现低耦合,我们在它们之间添加了一个结构体媒介~该媒介专门存储对应文件管理的结构体指针并放到数组内进行标号,而进程则存储指向该媒介的指针间接去管理这些文件~
所以fd本质上就是数组的标号,它代表着相对应文件的存放顺序~
#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("log.txt", O_RDONLY); if(fd == -1) { perror("open"); return 1; } char buffer[1024]; ssize_t s = read(fd, buffer, sizeof(buffer)); if(s>0) { buffer[s] = 0; printf("%s", buffer); } close(fd); }
我们用read系统调用来读取文件内容~
我们拿系统调用举例,当我们获取到该文件的fd后操作系统就可以通过PCB中找到文件描述表,再通过获取的fd下标找到关于该文件的对象描述,后面无论是对文件的修改还是访问都可以正常去执行与管理~
拓展:
一切皆为文件,都可以被操作系统所管理~
我们在对描述文件的结构体中存储读写的函数指针,这些函数指针分别指向对应文件的读写系统调用,这样我们面对各种文件不同的操作方法时就有了消除访问硬件的差异性,实现对所以文件的管理。可以看到,即使是硬件设备我们也可以通过文件管理的方式进行整合,不用亲自去访问硬件,而是间接去访问~所以才有一切皆文件的说法~
文件fd的分配规则
int main() { close(0); int fd = open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666); printf("fd: %d\n", fd); }
当我们关闭fd为0的文件时(关闭stdin),我们新创建的文件会占领前面空缺的下标位置~
说明:最小的没有被使用的数组下标,会分配给最新打开的文件~
利用规则实现重定向
如果我们尝试关闭文件(stdout:显示器)就会发生有趣的事情~ 本来应该打印在显示器上的内容结果打印到我们新创建的文件里面去了~
首先我们得知道close函数是系统调用的,可是printf不知道啊,它位于上层是C语言标准库里的,Printf默认就是向fd为1即显示器打印的,不关他的事~
可是新创建的文件已经是下标为1了,那么printf本该向显示器写入的变成向文件写入了~而这就是输出重定向~
我们再来测试一组~
int main() { close(0); int fd = open("log.txt",O_RDONLY,0666); int a = 0; scanf("%d", &a);// stdin->_fileno = 0 printf("%d\n",a); }
这一次我们把stdin(键盘)文件关闭了,一开始执行程序时我们已经无法通过键盘来获取内容了,当我们在新创建的log文件中写入内容,再执行一次发现获取到最新写入的a了~
把本该键盘获取的内容转为在新文件中获取,这是输入重定向~
这些C语言接口都会出现上述的情况~
当然如果我们想要实现重定向也不用这么麻烦,这里有专门的函数供我们调用~
dup2的功能就是把newfd拷贝成oldfd,最后只会剩下oldfd
最后我们再来探讨一下指令>,>>与open文件之间的关系~
int main() { //int fd = open("log.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666); //int fd = open("log.txt", O_WRONLY|O_CREAT|O_APPEND, 0666); int fd = open("log.txt", O_RDONLY); //dup2(fd, 0); char buffer[1024]; while(1) { // 默认情况,stdin键盘中读取,默认是要阻塞 char * s = fgets(buffer, sizeof(buffer), stdin); // stdin -> FILE * -> _fileno if(s == NULL) break; printf("file content: %s", buffer); } }
如果我们不使用dup2,那么该进程就会等待我们键盘的输入~
若是使用dup2,那么进程就会直接获取到log文件的内容并向显示器输出打印~
其实关于重定向的指令本质上也是属于进程~而指令后面的指定文件名就是其要打开的文件,至于打开的方式就取决与是>指令还是>>指令,对应的指令会有对应的打开方式,它们会用自己的打开方式来进行进程替换~