目录
一、文件理解:
通过几个问题来理解文件:
文件是由什么构成的,是在哪里存放的
文件 = 文件内容+文件属性
文件分为已被使用的文件 和未使用的文件
已使用的文件存放在磁盘,未使用的文件存放在内存(CPU只和内存打交道)
对文件进行操作本质是什么
对文件的操作可以是在语言方面 的,也可以是在系统方面 的
对于语言方面 的文件操作就是通过库函数的调用而对文件进行读写
对于系统层面 的文件操作就是通过先描述再组织的方式对文件进行管理操作
操作系统是怎么对各个正在使用的文件进行区分,管理的
操作系统对文件进行区分管理就 类似于管理进程 通过对文件进行描述(使用struct file 结构体对文件属性进行管理)再组织(每一个struct file结构体中有着指向下一个结构体的指针)
二、C语言的文件操作:
接下来回顾一下C语言中的对文件的操作接口:
可以看看以前写的文章,但是对于接口还有更多的补充
1、fopen:
这个C语言中的标准库函数的作用是**打开一个文件,**如果没有找到文件就创建一个文件,再打开
在C语言中更多的是:
cpp
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资源
什么是当前路径:
通过上述知识可以知道,当要在当前路径下以写模式打开文件时,如果没有找到该文件,就在当前路径下创造一个文件,那么系统是怎么找到当前路径的呢?当前路径又是什么呢?
cpp
#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
最后当关闭后及时将文件指针置空防止野指针
cpp
fclose(pf);//关闭文件
pf = NULL;//及时置空
3、fwrite:
这是一个文件写入方式,有四个参数
解析:
第一个参数:
代表着要写入数据的起始地址
第二个参数:
代表着要拷贝数据的大小
第三个参数:
代表着要拷贝数据的个数
第四个参数:
代表着要数据要到写到哪儿去
cpp
#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大小的字符
如果想一次全部写入可以修改为
cpp
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)之类的
cpp
#define ONE (1<<0)//1
#define TWO (1<<1)//2
#define THREE (1<<2)//4
#define FOUR (1<<3)//8
void 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
cpp
#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的文件,然后再向文件中写入字符串
cpp
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时,操作系统就会关闭该文件的文件描述符,但文件本身在文件系统中仍然存在,直到被删除