大家好我是沐曦希💕
文章目录
一、前言
1.重新谈论文件
空文件,也要在磁盘占据空间,因为文件也有属性,属性也属于数据,需要空间进行存储。所以文件包括内容和属性
所以对文件操作就是对内容或者对属性进行操作,或者对内容和属性进行操作。
文件具有唯一性,所以在标定一个文件时候,必须使用文件路径加上文件名。如果没有指明对应的文件路径,默认是该文件在当前路径,从而进行文件访问。
当我们把fopen,fclose,fread,fwrite等接口写完之后,代码编译之后,形成二进制可执行程序之后,但是没有运行,文件对应的操作就没有被执行,所以对文件的操作本质是进程对文件的操作!
一个文件如果没有被打开,不可以直接进行文件访问,一个文件要被访问,就必须先被打卡开(由用户进程+OS打开)
注意:不是所有磁盘的文件都可以被打开
文件分为被打开的文件和没有被打开的文件,而文件由进程打开,所以文件操作的本质:进程和被打开文件的关系
2.重新谈论文件操作
不同语言有不同文件操作接口,但底层都一样。
因为文件存储在磁盘中,而磁盘属于硬件,而OS管理软硬件,所以所有人想要访问磁盘不能绕过OS,必须使用OS提供的文件级别的系统调用接口来访问磁盘,但是操作系统始终只有一个。
所以上层语言无论如何变化:库函数底层必须调用系统调用接口,库函数可以千变万化,但是底层不变。
二、回归C文件接口
1.打开和关闭
对于C语言文件操作,首先要打开文件
FILE * fopen (constchar* filename,constchar* mode );
mode参数是以mode形式打开文件,mode取值:
打开失败将会返回NULL ,而打开成功则返回文件的指针(FILE*)。最后我们则需要关闭(fclose)文件。
int fclose ( FILE * stream );
stream – 这是指向 FILE 对象的指针,该 FILE 对象指定了要被关闭的流。
关闭成功,返回0,否则返回EOF
2.读写文件
通过fgets和fputs以字符串形式进行读写,也可以通过fprint和fscanf进行格式化读写。
int fputs (constchar* str, FILE * stream );char* fgets (char* str,int num, FILE * stream );int fprintf ( FILE * stream,constchar* format,...);int fscanf ( FILE * stream,constchar* format,...);
当前路径:一个进程运行起来的时候,每个进程都会去记录自己当前所处的工作路径。
所有当前路径也就是当前进程的工作路径,可以被修改,所以每个进程都有自己的当前路径
文件操作中r和w分别代表读和写,r+(读写)代表不存在则出错,w+(读写)代表不存在则创建,a(append)进行追加,追加也是写入,a+()也是读写,写是追加。r+b是以二进制形式进行读,r+w是以二进制写入。
- fprintf+"w"
- fgets+"r" fgets会给字符串结尾添加\0
buff[strlen(buff)-1]=0
以上代码目的是在读取的时候按行打印,把\n多读了,处理一下\n;
- fprintf+"a"
3.扩展
以w方式打开文件,C会自动清空文件内部的数据
- 比特位传递选项
在C语言中,传标记位一个整数标记位一般传一个整数,而一个整数有32个比特位,所以我们可以通过比特位来传递选项。每个宏对应的数值,只有一个比特位是1,批次位置不会重复。下面使用比特位来传递选项,一个比特位一个选项,比特位位置不能重复:
通过|传递参数,这样就能传递多个标志位了。
- stdin & stdout & stderr
C默认会打开三个输入输出流,分别是stdin, stdout, stderr
仔细观察发现,这三个流的类型都是FILE*, fopen返回值类型,文件指针
三、系统文件
操作文件,除了上述C接口(当然,C++也有接口,其他语言也有),我们还可以采用系统接口来进行文件访问
1.open和close
- open fopen的底层就是调用open系统接口
mode参数含义是:文件权限,普通文件默认创建的是0664:一个文件形成的时候有默认文件的野码umask,普通文件创建的时候默认的起始权限是0666,在形成文件的时候0666&~umask。umask的默认值是0002。
不传mode的后果是生成的文件是乱码
pathname是文件路径
函数调用成功返回文件的描述符,调用失败返回-1
#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>intopen(constchar*pathname,int flags);//文件已经存在intopen(constchar*pathname,int flags, mode_t mode);//文件不存在//pathname:打开文件名//flags:标志位。O_RDONLY:只读 O_WRONLY:只写 O_RDWR:读写
O_CREAT:文件不存在,则只能由我们去创建它,也要传递mode,来指明该新文件的权限。
注意:O_CREAT是一个建议选项,文件存在还是不存在都可以使用
- close
fclose底层是调用close系统接口
注意:使用open并不会帮我们创建文件,而C语言的文件操作函数fopen会自动创建文件是因为它封装了会帮我们自动创建,但是对于系统接口我们需要加上O_CREAT(文件不存在自动创建).最终成功帮我们自动创建成功!但是权限是乱的,但是文件默认以什么权限创建?我们默认情况下目录以777,普通文件以666开始,这些都是通过open的第三个参数mode选项设置权限的,设定创建默认文件的权限。
other的权限是只读,因为有掩码umask,我们可以通过在进程中设置umask来改变文件的最终权限
但是此时父进程shell的umask结果还是0002,我们改变的是子进程的文件权限,因为进程具有独立性,并不会影响父进程的umask。
2.write和read
- write
fd:所写的文件的文件描述符
buf:缓冲区数据,参数是void*,之前所说,文件读取分为文本类和二进制类,这是对于语言提供给我们的文件读取的分类。但是在操作系统看来,都是void*,也就是二进制!
count:缓冲区所写的字节个数
返回值:返回写入的字节数,在这里我们并不太需要关注返回值
#include<stdio.h>#include<string.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h>#define FILE_NAME "log.txt"intmain(){umask(0);int fd =open(FILE_NAME,O_WRONLY | O_CREAT,0666);if(fd<0){perror("open");return1;}int cnt =5;char outBuffer[64];while(cnt){sprintf(outBuffer,"%s:%d\n","helloworld",cnt--);write(fd,outBuffer,strlen(outBuffer));}close(fd);}
如果string+1了,则会出现乱码的问题:
- 清空问题
#include<stdio.h>#include<string.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h>#define FILE_NAME "log.txt" intmain(){umask(0);int fd =open(FILE_NAME,O_WRONLY | O_CREAT | O_TRUNC,0666);if(fd<0){perror("open");return1;}close(fd);}
可以发现之前的hello world并没有被清空,而C语言文件操作函数fwrite每次写入都会清空文件中所有内容,这是因为C语言对write进行了封装,而在Linux下实现自动清空内容,需要我们自己添加选项内容O_TRUNC:
所以C语言简单的一个"w",底层就需要封装write和O_WRONLY(写入) | O_CREAT(不存在则创建 | O_TRUNC(清空),以及传入属性
- 追加O_APPEND "a"在系统层面上是封装了write,O_WRONLY | O_CREAT | O_APPEND(追加)
- read 从一个文件描述符中读取文件
//头文件#include<unistd.h>//返回值ssize_t系统定制类型
ssize_t read(int fd,void*buf, size_t count);
读文件需要用到选项O_RDONLY
成功返回读取到多少个字节,0代表读到文件结尾。
读文件的前提是文件已经是存在的了。
#include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<assert.h>#define FILE_NAME "log.txt"intmain(){int fd =open(FILE_NAME, O_RDONLY);assert(fd >0);char buffer[1024];
ssize_t num =read(fd, buffer,sizeof(buffer)-1);if(num >0)
buffer[num]=0;// 字符串结束标识符'\0'printf("%s",buffer);close(fd);return0;}
3.总结
系统调用接口:open/close/write/read/lseek必须用文件描述符
而C语言库函数(libc)接口:fopen/fclose/fwrite/fread/fseek是封装了系统调用接口
四、理解文件
文件操作的本质:进程和被打开文件的关系
进程可以打开多个文件,那么系统中一定存在大量的被打开的文件,而被打开的文件,要被OS管理起来。而管理的本质是先描述,再组织,所以操作系统为了管理对应的打开文件,必定要为文件创建对应的内核数据结构进行标识文件:struct file{};(与C语言的FILE没有关系) 其中包含了文件的大部分属性
进程和被打开的文件如何关联,也就是说进程和被打开文件的关系是如何维护的?通过文件打开(open)的返回值和文件描述符进行联系。
#include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<assert.h>#define FILE_NAME(number) "log.txt"#numberintmain(){int fd1 =open(FILE_NAME(1), O_WRONLY | O_CREAT | O_APPEND,0666);int fd2 =open(FILE_NAME(2), O_WRONLY | O_CREAT | O_APPEND,0666);int fd3 =open(FILE_NAME(3), O_WRONLY | O_CREAT | O_APPEND,0666);int fd4 =open(FILE_NAME(4), O_WRONLY | O_CREAT | O_APPEND,0666);printf("fd:%d", fd1);printf("fd:%d", fd2);printf("fd:%d", fd3);printf("fd:%d", fd4);close(fd1);close(fd2);close(fd3);close(fd4);return0;}
1.为什么从3开始,0,1,2呢?
2.3,4,5,6是连续的小整数,就像数组下标一样
五、文件描述符
在C语言阶段我们知道C程序会默认打开三个标准输入输出流:stdin(键盘),stdout(显示器),stderr(显示器)
FILE实际上是一个结构体!访问文件时,底层open必须采用系统调用,而系统调用接口访问文件必须用文件描述符,而在C语言用的并不是文件描述符,而是FILE,所以这个FILE结构体必定有一个文件描述符的字段。所以C语言不仅在接口上有封装,连数据类型都有封装。
这就很好的解答了为什么open的返回值是从3开始的问题!因为0,1,2默认被占用,我们的C语言封装了接口,同时也封装了操作系统内的文件描述符。
1.理解
文件描述符的本质是数组的下标
而现在知道,文件描述符就是从0开始的小整数。当我们打开文件时,操作系统在内存中要创建相应的数据结构来描述目标文件。于是就有了file结构体。表示一个已经打开的文件对象。而进程执行open系统调用,所以必须让进
程和文件关联起来。每个进程都有一个指针*files, 指向一张表files_struct,该表最重要的部分就是包含一个指针数组,每个元素都是一个指向打开文件的指针!所以,本质上,文件描述符就是该数组的下标。所以,只要拿着文件
描述符,就可以找到对应的文件。
这也就是为什么文件操作系统读到的数是整数,而且是连续的,因为文件操作系统内标记进程和文件之间的关系就是文件描述符表,用数组标定文件内容!通过文件描述符来访问文件!
2.分配规则
文件描述都是从最小未被使用的下标开始分配的,那么分别关掉0,1,2会发生什么?
- 关掉0 当我们把0关掉时候,该描述符没有被占用,当我们调用系统接口来创建一个新文件时候,该文件占用了0下标的文件描述符。
如果在创建一个文件对象,会在自己的文件描述符表从小到大按照顺序寻找最小的且没有被占用的fd.
文件描述符的分配规则:在files_struct数组当中,找到当前没有被使用的
最小的一个下标,作为新的文件描述符
- 关掉1
当我们关闭1时,此时1不在指向标准输出(显示器),不在向显示器打印,当我们打开文件的时候,系统会存在文件对象,然后在把文件的地址在files_struct找一个最小的没有被使用的文件描述符,此时是1,此时就把文件的地址填入1的下标里,在把1号文件描述符返回给上层,此时fd就拿到返回值1。
这里没有显示结果,根据前面推测,那么打印的内容应该打印在log.txt上。
此时打印的结果并没有在新打开的文件里,这是因为缓冲区的问题,没有被显示出来
刷新缓冲区看看:
#include<stdio.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<unistd.h>intmain(){close(1);umask(0);int fd =open("log.txt", O_WRONLY | O_CREAT | O_TRUNC,0666);if(fd <0){perror("open");return1;}printf("open fd:%d\n", fd);// printf->stdout fprintf(stdout,"open fd:%d\n", fd);fflush(stdout);close(fd);return0;}
我们发现,本来应该输出到显示器上的内容,输出到了文件 log.txt当中,其中,fd=1。这种特性叫做输出重定向。
六、重定向
常见的重定向有:>(输出重定向), >>(追加), <(输入重定向)
重定向的本质是:上层用的fd不变,在内核中更改fd对应的struct file*的地址
1.接口
- dup2
dup2的作用是两个文件描述符之间进行拷贝,是把fd里面的内容拷贝
dup2一旦重定向之后,最终剩下的都是oldfd
代码实现:
#include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<string.h>#define FILE_NAME "log.txt" intmain(){int fd =open(FILE_NAME, O_WRONLY | O_APPEND | O_CREAT,0666);if(fd <0){perror("open");return1;}dup2(fd,1);// 输出重定向 constchar* msg ="hello world";write(1, msg,strlen(msg));printf("\n");return0;}
把本该显示在显示器的内容打印在文件上
- 输入重定向
当然重定向到这还未结束
七、Linux下一切皆文件
在冯诺依曼体系中,我们知道硬件有键盘、显示器、磁盘、网卡等外设,在IO过程中,外设任何的数据处理都需要把数据读到内存,处理完毕之后将内存中的数据刷新到外设当中。因为软硬件资源多,所以操作系统需要对其先描述,再组织。所以这些外设都有对应的结构体,存储着对应着属性信息:
例如(只是一个例子,并非真实):
struct keyboard{};struct tty{};struct disk{};struct netcard{};
同时,对应着自己的IO函数,例如(只是一个例子,并非真实):
intkeyboardRead(){}intkeyboardWrite(){NULL}intttyRead(){}intttyRead(){}...
每种硬件访问方法是不一样的
具体硬件的读写方法都在应用匹配的驱动程序里。Linux一切皆文件是这样体现的:任何一个被打开的文件结构体对象struct file{ //各种文件的属性 }对象
struct file
{//各种文件属性int type;int status;int(*readp)();int(*write)();...};
不同的文件对应的读写方法不一样,struct file对象里面可以有很多的(*readp)()、(*writep)()函数指针,通过函数指针指向具体的读写方法。
站在struct file上层看来,所有的设备和文件,统一都是struct file->,就可以调用到具体设备的方法。即Linux下一切皆文件
上层调用不同的文件,底层调用不同的方法。站在上层,只需要调用对应的统一的文件,使用struct file,访问不同的文件,这是C语言实现多态的特征。这里struct file称为在操作系统层面上虚拟出来的文件对象vfs(虚拟文件系统)
PCB指向被打开的文件:
我们所谓的关闭文件只是在表明用户给OS说已经不需要使用了,由OS决定,OS把引用计数减到0时,才被OS真正删除掉。
四、写在最后
版权归原作者 沐曦希 所有, 如有侵权,请联系我们删除。