目录
一、文件理解:
二、C语言的文件操作:
1、fopen:
什么是当前路径:
2、fclose:
3、fwrite:
4、默认打开的三个流:
三、系统文件:
1、open:
2、close:
3、write:
O_TRUNC:
O_APPEND:
四、文件描述符与FILE:
文件描述符:
FILE:
一、文件理解:
通过几个问题来理解文件:
文件是由什么构成的,是在哪里存放的
文件 = 文件内容+文件属性
文件分为已被使用的文件和未使用的文件
已使用的文件存放在磁盘,未使用的文件存放在内存(CPU只和内存打交道)
对文件进行操作本质是什么
对文件的操作可以是在语言方面的,也可以是在系统方面的
对于语言方面的文件操作就是通过库函数的调用而对文件进行读写
对于系统层面的文件操作就是通过先描述再组织的方式对文件进行管理操作
操作系统是怎么对各个正在使用的文件进行区分,管理的
操作系统对文件进行区分管理就 类似于管理进程 通过对文件进行描述(使用struct file 结构体对文件属性进行管理)再组织(每一个struct file结构体中有着指向下一个结构体的指针)
二、C语言的文件操作:
接下来回顾一下C语言中的对文件的操作接口:
可以看看以前写的文章,但是对于接口还有更多的补充
【C语言】文件操作-CSDN博客https://blog.csdn.net/2303_80828380/article/details/139933028?spm=1001.2014.3001.5501
1、fopen:
这个C语言中的标准库函数的作用是打开一个文件,如果没有找到文件就创建一个文件,再打开
在C语言中更多的是:
FILE* fopen(const char* pathname, const char* mode );
解析:
第一个参数:
可以写成要操作的文件名,如果在文件名前面不加路径,那就是在当前路径下打开,创建文件,如果加上了绝对路径那么就在指定的路径进行打开,创建文件
第二个参数:
是一个字符串,有几个选项可以选择的:
r : 只读模式,文件必须存在
r+: 读写模式,文件必须存在
w : 写模式,如果文件存在则文件长度清为0,即文件内容会消失,如果文件不存在则创建该文件
w+读写模式,如果文件存在则文件长度清为0,即文件内容会消失,如果文件不存在创建立该文件
a: 追加模式,如果文件存在,写入的数据会被加到文件尾后,如果文件不存在,则创建文件
a+: 追加模式,如果文件存在,写入的数据会被加到文件尾后,如果文件不存在,则创建文件
其中:a,附加写方式打开,不可读;a+,附加读写方式打开
如上,这就是在当前路径以写的模式打开 log.txt 文件
返回值:
返回类型是一个指向FILE对象的指针,FILE又是C库中自己封装的结构体,里面封装了文件描述符若文件打开失败,会返回NULL,
文件描述符:(这是一个非负整数)
这个可以理解为:每个文件,操作系统进行设计的时候都需要有一个下标对应一个文件,可以理解为每个数组下标就对应了一个文件,通过这个数组下标就能访问操作文件 这个数组下标就被称为文件描述符,每个进程都有一个文件描述符表,用于跟踪进程打开的文件和I/O资源
什么是当前路径:
通过上述知识可以知道,当要在当前路径下以写模式打开文件时,如果没有找到该文件,就在当前路径下创造一个文件,那么系统是怎么找到当前路径的呢?当前路径又是什么呢?
#include<stdio.h>
#include<unistd.h>
#include<string.h>int main()
{printf("pid:%d\n",getpid());FILE* fp = fopen("log.txt","w");if(fp == NULL){perror("fopen fail");return 1;}fclose(fp);sleep(1000);return 0;
}
如上代码,这就是在当前路径下以读的形式打开一个文件,如果不存在就在当前路径下创建一个log.txt文件再打开
可以通过下述的指令查看到进程的当前路径cwd(current working directory)
如上,当进程执行的时候,可以在/proc目录下查看该进程的数据,里面有一个cwd,操作系统就是通过查看这个来作为进程的当前路径
2、fclose:
打开、关闭文件类似于动态开辟空间(开辟好一个空间后要将其释放),当我们打开一个文件后,在使用后也要记得关闭文件,这个时候就使用fclose即可,
参数就是将该文件的文件指针传入fclose函数即可,fclose函数如果关闭文件成功会返回0
最后当关闭后及时将文件指针置空防止野指针
fclose(pf);//关闭文件
pf = NULL;//及时置空
3、fwrite:
这是一个文件写入方式,有四个参数
解析:
第一个参数:
代表着要写入数据的起始地址
第二个参数:
代表着要拷贝数据的大小
第三个参数:
代表着要拷贝数据的个数
第四个参数:
代表着要数据要到写到哪儿去
#include<stdio.h>
#include<unistd.h>
#include<string.h>int main()
{FILE* fp = fopen("log.txt","w");if(fp == NULL){perror("fopen fail");return 1;}const char* sum = "abcdefghijklmn\n";fwrite(sum,sizeof(char),6,fp);fclose(fp);return 0;
}
如上代码,就是将sun字符串,6个大小为char的数据写入到fp指针指向的 log.txt 中,
这样打开log.txt文件就可以看到已经写了6个char大小的字符
如果想一次全部写入可以修改为
fwrite(sun,strlen(sum),1,fp);
这样,再打开log.txt文件就可以看到把sum所有字符串都写入
4、默认打开的三个流:
在Linux下可以看做一切皆文件,所以我们电脑的显示器,键盘也可以看作是文件,
比如当在显示器上能够看到数据,其实就是往显示器文件中写入了数据
键盘能够输入数据,其实就是CPU从键盘文件中读入数据
那么当进程启动的时候我们为什么不用在代码中打开键盘文件,显示器文件呢?
其实在进程启动的时候,就默认打开了三个流:标准输入流,标准输出流,标准错误流
中三个在C语言中对应的分别就是stdin,stdout,stderr,我们在man手册中可以看到这三者的类型都是FILE*,这也就相当于打开了三个文件
比如我们也可以直接向stdout流里面写数据,这样的话就可以在显示器中看到了
三、系统文件:
首先要知道,对文件进行操作上述讲的是在语言方面的接口,操作系统还有一套系统接口来对文件进行访问的,实际上,语言方面的接口就是对系统接口进行封装的,
如下:语言方面的接口就在用户操作接口地方,系统接口就在system call处,系统接口更接近底层
文件是在磁盘上存储的,磁盘又属于硬件,平时我们在IO的时候访问文件本质上就是和硬件打交道,通过前面的知识我们了解到,用户如果想访问硬件是不能够直接访问的,必须要经过操作系统的,又因为操作系统不相信任何人,所以操作系统提供了系统调用接口供用户使用而访问底层硬件
1、open:
这个就是一个系统调用接口,man手册中的初步介绍如下:
解析:
第一个参数:
这是一个待操作文件名,其实和fopen中的一样,
如果以文件名的方式给出,就是在当前路径下进行文件操作
如果以路径+文件名的方式给出,就是在所给路径下进行文件操作
第二个参数:
这是一个打开文件的方式,
第三个参数:
这是代表着创建一个文件时,这个文件的默认权限是什么,起始权限为(0666)
扩展:
对于一个整形来说,有32个比特位,就可以看做有32个标志位。而这种标志位就是flags,flags利用了这种比特位级别的标志方式
接下来看看下面代码,这就是类似于想看哪里的标志位,就直接show(ONE)之类的
#define ONE (1<<0)//1
#define TWO (1<<1)//2
#define THREE (1<<2)//4
#define FOUR (1<<3)//8void show(int flag)
{if(flag & ONE) printf("hello one\n");if(flag & TWO) printf("hello two\n");if(flag & THREE) printf("hello three\n");if(flag & FOUR) printf("hello four\n");
}int main()
{printf("-----------------------------\n");show(ONE);printf("-----------------------------\n");show(TWO);printf("-----------------------------\n");show(FOUR|THREE);printf("-----------------------------\n");show(ONE|TWO|THREE);return 0;
}
运行结果:
所以,在open函数的内部就类似于上述的方法,定义宏,然后在open函数内部进行传参,然后通过“按位与”运算来进行判断
返回值:
open函数的返回值是返回的文件描述符,
接下来我们使用open函数,并且查看它的返回值,这里第二个参数采用的是以读的形式打开,如果在当前路径下没有找到文件,就创建文件,并且默认权限为0666
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>int main()
{int fp1 = open("log1.txt",O_WRONLY|O_CREAT,0666);int fp2 = open("log2.txt",O_WRONLY|O_CREAT,0666);int fp3 = open("log3.txt",O_WRONLY|O_CREAT,0666);int fp4 = open("log4.txt",O_WRONLY|O_CREAT,0666);int fp5 = open("log5.txt",O_WRONLY|O_CREAT,0666);int fp6 = open("log6.txt",O_WRONLY|O_CREAT,0666);printf("fp1 : %d\n",fp1);printf("fp2 : %d\n",fp2);printf("fp3 : %d\n",fp3);printf("fp4 : %d\n",fp4);printf("fp5 : %d\n",fp5);printf("fp6 : %d\n",fp6);return 0;
}
如上的运行结果如下,返回的这些整数就是文件描述符,那么为什么是从3开始而不是从0开始的呢?
这当然是因为进程启动的时候就已经默认打开了三个文件流,stdin,stdout,stderr,这三个文件占领了0,1,2,所以后面打开的文件就依次从3开始
接下来看看这些已经创建的文件的权限:
可以看到权限对应的是0664,但是我们传的明明是0666,这是为什么呢?
很简单,在前面的学习中我们了解到了文件的真正的权限等于默认权限 & (~umask),系统中默认的umask为0002,所以默认权限0666变成真正权限就是0664
当然,我们也可以在代码中进行umask的修改,将umask置为0
这样,文件的真正权限就变为0666
2、close:
在打开文件后要记得关闭文件,关闭文件成功返回0,关闭文件失败返回-1
3、write:
这个系统调用接口是向文件中写入数据,
解析:
第一个参数:
文件描述符,也就是打开文件时的返回值(open的返回值)
第二个参数:
写入的数据来源
第三个参数:
写入数据的字节数
如下就是一个先打开一个log.txt的文件,然后再向文件中写入字符串
int main()
{int fd = open("log.txt",O_WRONLY|O_CREAT,0666);if(fd < 0){perror("open fail");return 1;}const char* message = "abcdefghijkl\n";write(fd,message,strlen(message));close(fd);return 0;
}
那么运行后再查看log.txt就可以看到我们已经把字符串写进去了
O_TRUNC:
这个宏作为第二个参数中,是打开文件后清空当前文件在写入
当如果是没有O_TRUNC宏的时候,对已经存在数据如下,
在进行写入数据的时候就会从开始写入,本来存在的数据并不会被清除
如果想清楚本来的数据,只需在open的第二个参数加上O_TRUNC这个宏即可
O_APPEND:
如果不想进行清空写入,也不想在最开始写入,我要在最后面追加写入,那么就在open的第二个参数加上宏O_APPEND即可
如下,就是在open上加上宏O_APPEND,然后在最后追加aaaaaaaaaaaaaaa
如下,一开始log.txt就是三行abcdefg...,然后在运行程序后,就可以看到在最后追加了一串a
四、文件描述符与FILE:
文件描述符:
当启动进程的时候内存会加载一个PCB(Linux中是task_struct)
这个task_struct结构体里面肯定有一个指针struct file_struct* file指向一个叫做struct file_struct的结构体
这个结构体里面有一个struct file* fd_array[ ]的指针数组,这个数组的下标就是文件描述符
这个指针数组里面存放的是struct file*的指针,每个指针指向struct file的结构体
这个struct file的结构体里边就是对文件属性进行管理
当我们打开一个文件的时候,会生成一个描述文件的结构体,然后进程会在struct file* fd_array[](文件指针数组)里面找一个空位置保存刚刚创建描述文件的结构体的地址,然后再将这个数组的下标返回给用户,这个返回值就是open的返回值,最终,进程就可以根据这一张文件描述符表,就可以把我们打开的文件找到了
FILE:
FILE是C库中封装的一个结构体,因为Linux访问文件只看文件描述符,所以FILE这个结构体里面肯定封装了文件描述符,如下,stdin,stdout,stderr是FILE*类型的,所以里这三个结构体里面肯定分别封装了fd = 0,fd = 1,fd = 2
如果将stdout这个文件关闭了,那么就看不到输出在显示器上的数据了
但是可以继续往stderr里面输入,这个时候也可以看到了
这是因为stdout和stderr都是指向显示器文件,显示器文件有引用计数(事实上struct file结构体里面都有引用计数)
所以关闭stdout文件不会彻底关闭显示器文件
关闭文件的本质就是让struct file里面的引用计数-1,并且把文件描述符表里面指向这个struct file的下标置为空
当一个文件的引用计数减为0时,操作系统就会关闭该文件的文件描述符,但文件本身在文件系统中仍然存在,直到被删除