💖作者:小树苗渴望变成参天大树🎈
🎉作者宣言:认真写好每一篇博客💤
🎊作者gitee:gitee✨
💞作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法🎄
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!
文章目录
- 前言
- 一、以c语言为例,认识一下之前的接口
- 二、过度到系统,再谈文件系统调用
- 三、访问文件的本质
- 四、重定向的原理介绍
- 五、理解Linux下一切皆文件。
- 六、缓冲区
- 七、总结
前言
今天博主来讲一篇大家以前很害怕的但又不得不说的一个的一个知识点–文件管理,再我们c语言的时候,博主就给大家介绍过文件操作,那时候我们使用过很多函数去对文件操作,而我们又不怎么去使用这些函数,所以又熟悉又陌生,今天我们又把文件拿出来讲,远远不至于之前的哪些内容,博主讲深入系统层面来给大家进行讲解,让大家明白我们之前的函数是怎么去操作的,会解决大家很多疑惑,话不多说我们开始进入正文讲解。
共识原理
- 文件=内容+属性
- 文件分为打开的文件和没打开的文件
- 打开的文件,是谁打开的?? 答案是进程,所以我们后面讨论的是进程和文件的关系
- 没打开的文件,在哪放着的?答案是磁盘上。没打开的文件最关注的问题是什么? 答案是没打开的文件很多,这样就要我们把文件分类好,管理起来,就必须先描述在组织才能方便我们后面对文件的增删查改操作,所以在系统里面必须又关于文件管理的属性指针,来指向这样文件,方便管理。
- 打开的文件是由进程加载到内存的,一个进程可以打开多个文件所以是1:n的关系,此时进程也需要把他打开的文件管理起来,才知道哪些文件是他自己打开的,最后才有办法关闭。
针对4,5点的管理我们一会再正文部分给大家具体介绍,并画出详细的图。上面的我相信大家应该都可以理解,可以理解为人(进程),快递(文件),驿站(磁盘)的关系。
一、以c语言为例,认识一下之前的接口
我们只看写文件,因为读文件没有什么好看的。再进行一个文件操作的时候,需要先打开一个文件,我们来回顾一下c语言的打开文件操作
第一个参数是写入的内容字符串,第二个参数是内容多大,第三个参数是写几个这样的,第四个是要传打开的文件
w方式:
#include<stdio.h>
#include<string.h>
int main()
{
FILE* fp=fopen("login.txt","w");
if(fp==NULL)
{
perror("fopen");
return -1;
}
const char*msg="hello,file";
fwrite(msg,strlen(msg),1,fp);
fclose(fp);
return 0;
}
我们看到效果和我们文档里面是一样的,如果没有的文件会在当前路径自动创建一个,而每次打开文件都会先清空文件,再进行写入,通过第三个图我们也发现和写不写内容没有关系,只要每次重新打开就会情空。
a方式:
#include<stdio.h>
#include<string.h>
int main()
{
FILE* fp=fopen("login1.txt","a");
if(fp==NULL)
{
perror("fopen");
return -1;
}
const char*msg="hello,file";
fwrite(msg,strlen(msg),1,fp);
fclose(fp);
return 0;
}
结果和文档的效果是一样的,没有文件就会在当前路径创建新的文件,然后每次打开就会再文件的末尾追加内容。
解决疑惑
(1)在fwrite的时候我们传的strlen(msg),此时需不需要加+1.就是这个\0的长度要不要加上去,我们加一下看看结果:
我们打开文件发现会出现乱码,说明/0确实被写进去了,因为我们的vim是文本解析器,所以会出现乱码,那我们到底要不要加1呢,答案是不要的,这一看就是不对了,注意:\0是c语言层面的为了给字符串作为结束标志的,而写入文件只需要把有效字符写进去就行了,这个\0和文件是没有关系的,所以不用写进去
(2)fopen的两种方式中博主都提到了当前路径,是怎么做到的,修改当前路径会不会在其他路径下创建,来看演示:
我们来使用一个系统函数改变当前路径
博主讲当前路径改成上一级路径,我们发现创建的文件就跑到上一级文件了。可以通过这样的方式来改变当前路径。
---------------------------------------------------------------------------------------------------------------------------------
通过上面介绍的两种方式,我们还能想起来什么??
之前是不是说过重定向,一个输出重定向,一个追加重定向,都是往文件里面写内容,那么可以这样理解,我们的重定向的第一步肯定是要打开文件,输出和追加的区别就是打开方式的不同,但肯定是类似于上面的方式去实现的,再这里大家心里面现有一个印象,这个后面我会再重点谈的。
铺垫:
我们的c语言会默认打开三个FILE类型的文件
他们在stdio.h里面,这也是我们一开始学习c语言就要包含这个文件的原因
Linux下一切皆文件,所以这三个分别对应的是:键盘文件,显示器文件,显示器文件
我们来使用函数往这些文件里面写点东西
#include<stdio.h>
#include<string.h>
#include<unistd.h>
int main()
{
const char*msg="hello,file";
fwrite(msg,strlen(msg),1,stdout);
return 0;
}
#include<stdio.h>
#include<string.h>
#include<unistd.h>
int main()
{
const char*msg="hello,file";
fprintf(stderr, "%s: %d\n", msg, 1234);
return 0;
}
stdin是键盘文件,不好演示的,可以从键盘读取数据,一会回过头来给大家演示。
这三个默认打开的文件我一会再回头过来讲。
二、过度到系统,再谈文件系统调用
再一开始的共识中,博主就说到文件没被打开时候是在磁盘上的,磁盘是外部设备,也就是硬件,所以访问磁盘文件就是再访问硬件,我们的用户通过库函数去打开文件就比如先经过系统调用接口,去让操作系统去访问硬件资源,操作系统不信任任何用户,而os又是管理着硬件资源,所以必须经过os这一关,几乎所有的库只要访问硬件设备,必定要封装系统调用 我们之前学的printf/fprintf/fscanf/fwrite/fread/fgetc/fopen/fclose都需要封装系统调用接口。
(1)打开文件的系统调用接口open
这个函数在二号手册,所以属于系统调用函数,调用失败返回-1
我们的第一个参数是打开的文件,第二个参数类似于位图操作,接下来使用一个例子带大家看看这个是什么:
#define ONE (1<<0) // 1
#define TWO (1<<1) // 2
#define THREE (1<<2) // 4
#define FOUR (1<<3) // 8
void show(int flags)
{
if(flags&ONE) printf("hello function1\n");
if(flags&TWO) printf("hello function2\n");
if(flags&THREE) printf("hello function3\n");
if(flags&FOUR) printf("hello function4\n");
}
int main()
{
printf("-----------------------------\n");
show(ONE);
printf("-----------------------------\n");
show(TWO);
printf("-----------------------------\n");
show(ONE|TWO);
printf("-----------------------------\n");
show(ONE|TWO|THREE);
printf("-----------------------------\n");
show(ONE|THREE);
printf("-----------------------------\n");
show(THREE|FOUR);
printf("-----------------------------\n");
}
博主在文档中圈出来的一些宏定义就是表示功能通过声明也大致可以看出大致含义.
第三个参数是类型,给文件设置什么样的权限,默认是666,但是有掩码,我们看到的就是664,如果我们就像让文件变成默认的666,那么就要修改掩码,但是不要修改其他文件的默认,所以需要在打开这个文件的进程里面修改,使用umask函数就可以了。
(2)再来看看写入的函数:
这个函数其实和库函数有点像,一会看使用,具体参数博主就不做具体介绍了
来具体看看库函数是怎么使用的:
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
umask(0);//设置掩码为0
//系统的掩码和这里的掩码程序因该听谁的,答案是谁进听谁的,不会更改系统的掩码。
int fd=open("login2.txt",O_WRONLY,0666);
if(fd<0)
{
perror("open");
return 0;
}
char*msg="hello,fd\n";
write(fd,msg,strlen(msg));//这个同样不需要加1
return 0;
}
为什么没有打开成功呢,原因是打开文件的使用的功能就是O_WRONLY,必须要在加一个功能
int fd=open("login2.txt",O_WRONLY|O_CREAT,0666);
(1)实现w的效果
int fd=open("login2.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
(2)实现a的效果
O_APPEND这是追加功能,所以不能和O_TRUNC同时使用,这两个是冲突的。
int fd=open("login2.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
我们讲系统调用接口给大家演示了,大家也知道了我们系统调用接口的功能更加的全面,但是此时我不在给大家介绍内容了,我先讲下面一个主题
三、访问文件的本质
我们刚才使用了open这个函数,他的返回值fd,在write里面也需要传fd,那么fd到底是什么,答案是文件描述符(数组的下标)
接下来通过一幅图给大家介绍
接下来博主在进程里面再多创建几个文件,看看打印出来的fd是什么,和我们预期画图是不是一个效果:
int main()
{
umask(0);//设置掩码为0
int fd=open("login2.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
int fd1=open("login3.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
int fd2=open("login4.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
int fd3=open("login5.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
int fd4=open("login6.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
if(fd<0)
{
perror("open");
return 0;
}
printf("fd:%d\n",fd);
printf("fd1:%d\n",fd1);
printf("fd2:%d\n",fd2);
printf("fd3:%d\n",fd3);
printf("fd4:%d\n",fd4);
return 0;
}
umask(0);//设置掩码为0
close(0);//这个一会再说
int fd=open("login2.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
int fd1=open("login3.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
int fd2=open("login4.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
int fd3=open("login5.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
printf("fd:%d\n",fd);
printf("fd1:%d\n",fd1);
printf("fd2:%d\n",fd2);
printf("fd3:%d\n",fd3);
return 0;
确实和我们分析的一样。会再数组里面找没有使用并且下标最小的位置讲打开的文件信息填进去
提出疑问:回顾铺垫
我们的0 1 2这三个下标呢??大家还记不记得上面的铺垫,我们的c语言程序默认打开第三个文件,那这三个下标会不会就对应着三个文件呢,我们来通过实验演示一下,我们猜想他们是按照的文档的方式先后打开的。
stdin(0),stdout(1),stderr(2)
stdin(0)
int main()
{
char buffer[1024];//0下标,我们默认是stdin
ssize_t s = read(0, buffer, sizeof(buffer));//从键盘文件上只能读取数据,所以使用系统接口read,
//虽然没有介绍过,但是大家看一下文档应该就会使用
if(s < 0) return 1;
buffer[s] = '\0';//因为从键盘读取到的只是有效字符,所以需要手动给字符数组加一个结束标志
printf("echo : %s\n", buffer);
return 0;
}
stdout(1),stderr(2)
这两个都指向显示器文件,就一起展示了
const char *msg = "hello Linux\n";
write(1, msg, strlen(msg));
write(2, msg, strlen(msg));
结论出来了,那我们的c程序默认打开的三个文件是c语言的特性吗,肯定不是啊,这是os的特性,C语言拿来用了而已,**那为什么要默认打开这三个文件呢??**原因是我们的掉闹再一打开对应的键盘显示器就被加载了,这样程序员才能进行写代码,这是天然决定的
既然我们的c语言要支持这样默认打开的文件,那他必定要对fd进行封装,因为系统调用接口都是通过fd来对文件进行操作的,所以我们看到的FILE他是什么??是不是就是我们C语言里面的结构体,我们通过->来访问一下fd:
printf("stdin:%d\n",stdin->_fileno);
printf("stdout:%d\n",stdout->_fileno);
printf("stderr:%d\n",stderr->_fileno);
看到这里大家对默认打开的文件是不是有了更深的了解了,大家可以自己下去测试关掉着三个默认的文件,还可不可以从键盘或者显示器进行操作,博主就不演示了,博主接下来带大家回过头来分析那幅图,我么再测试的默认打开的文件的时候,有两个文件都是显示器,那我们再关闭文件的时候,是怎么操作的:
我们的文件结构体里面有一个引用计数,他每次关闭,对应的文件的引用计数就会减减,到0此会真的关闭。我给大家测试一下:
const char *msg = "hello Linux\n";
write(1, msg, strlen(msg));
close(1);//关闭显示器文件
write(2, msg, strlen(msg));
结果即使关闭了1指向的显示器文件,但是显示器文件还是可以用的,原因就是close并没有关闭,只是将引用计数减减了。
四、重定向的原理介绍
大家如果从上面一直看下来应该知道我提到过重定向的内容,再铺垫之前就提到过,我们演示过w和a方式之后,往文件里面写东西,和重定向的效果是一样的,但是这节博主将带大家看看原理,再将这个之前,我先给大家讲解一下fd的分配规则,这个再上面也提到过
#include<stdio.h>
#include<unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd=open("log.txt",O_WRONLY|O_CREAT,0666);
if(fd<0)
{
perror("open");
return -1;
}
printf("fd=%d",fd);
close(fd);
return 0;
}
int main()
{
close(2);
int fd=open("log.txt",O_WRONLY|O_CREAT,0666);
if(fd<0)
{
perror("open");
return -1;
}
printf("fd=%d",fd);
close(fd);
return 0;
}
int main()
{
close(1);
int fd=open("log.txt",O_WRONLY|O_CREAT,0666);
if(fd<0)
{
perror("open");
return -1;
}
printf("fd=%d",fd);
close(fd);
return 0;
}
通过前两个案例,我们可以看出,fd的分配规则是找文件描述符表里面下标最小并且为空的位置放我们先打开的文件,我们的fd是1或者2都是指向显示器文件,为什么第三个案例和第二个案例却结果不太,第三个案例没有打印出fd的数值,原因是printf他的功能是往fd=1这个文件上打印文件的,而关闭1了就显示不了。
我们再来写一个例子:
int main()
{
close(1);
int fd=open("log.txt",O_WRONLY|O_CREAT,0666);
if(fd<0)
{
perror("open");
return -1;
}
const char*msg1="hello normal file\n";
int cnt=5;
while(cnt--)
{
write(1,msg1,strlen(msg1));
}
close(fd);
return 0;
}
我们看到我们打开log文件,万一往显示器上写内容,但是最后结果没有在显示器上显示,而是写道文件里面的,这是为什么??
来看图解:
这就是输出重定向
int main()
{
close(0);
int fd=open("log.txt",O_RDONLY|O_CREAT,0666);
if(fd<0)
{
perror("open");
return -1;
}
char inbuffer[1024];
ssize_t s = read(0, inbuffer, sizeof(inbuffer)-1);
if(s>0)
{
inbuffer[s] = '\0';
printf("echo# %s\n", inbuffer);
}
close(fd);
return 0;
}
本来是从键盘上读取文件,结果却从log.txt里面读取了,这就是输入重定向。
**特点:**上面两种重定向演示的特点都是,打log.txt文件,想在默认打开的三个文件进行写操作和读操作,但是结果都最后都是操作到log.txt上,原因就是充重定向。上面的办法,先关闭一个文件这样太不好看了,有没有系统函数专门做这样的事情呢??
dup2函数
int main()
{
int fd=open("log.txt",O_RDONLY|O_CREAT,0666);
if(fd<0)
{
perror("open");
return -1;
}
dup2(fd,0);
char inbuffer[1024];
ssize_t s = read(0, inbuffer, sizeof(inbuffer)-1);
if(s>0)
{
inbuffer[s] = '\0';
printf("echo# %s\n", inbuffer);
}
close(fd);
return 0;
}
也达到了相同的效果,接下来具体介绍一下函数的构成
这个参数的设计有点不和人情。
将oldfd拷贝到newfd上面,这里的oldfd就是新建的文件log.txt,newfd是你要拷贝的文件,最后再文件描述符表中会有两块位置的值是相同的,都是oldfd,这里面的拷贝不是下标拷贝,下标不可能有两个一样的,是将下标里面的内容拷贝,说这个参数的名字不好的原因是,新建的log.txt按道理比默认的文件后创建,他的fd应该是newold,结果看这个参数就跟搞反了一样。但也没办法,我们适应这个函数之后可以把oldfd给关闭,也可以不关闭,看你个人,这个函数的使用就比之前的方法好很多,一眼就能看出来你要干什么,博主会再下篇给大家写一个简单的shell给大家演示怎么默认实现命令行上面的重定向功能。
---------------------------------------------------------------------------------------------------------------------------------
我想在给大家解决一下心中的小疑惑,我们的os再启动的时候就会默认打开三个文件,但是其中有两个fd指向显示器文件,stdout(1),stderr(2),现在要解决的就是1 & 2的区别是什么??
博主先来写一段代码:
int main()
{
const char*msg1="hello normal file\n";
write(1,msg1,strlen(msg1));
write(1,msg1,strlen(msg1));
write(1,msg1,strlen(msg1));
write(1,msg1,strlen(msg1));
write(1,msg1,strlen(msg1));
const char*msg2="hell err file\n";
write(2,msg2,strlen(msg2));
write(2,msg2,strlen(msg2));
write(2,msg2,strlen(msg2));
write(2,msg2,strlen(msg2));
write(2,msg2,strlen(msg2));
return 0;
}
我们看到两个效果是一样的
我们使用了重定向将1显示器文件和normal.log文件进行重定向,所以本来输出在显示器的内容重定向到normal.log文件里面了,而strerr是2显示器文件,和我没关系,默认的是把1显示器和别的文件进行重定向。下面的和上面的一样。
我们可以实现日志的分开写入,其实在本质上两者没有区别,但是为了分流去干一些是,就必须弄两个,方便分流操作。如果不想实现分流,就使用下面的方法:
先执行./mytest > normal.log 因为已经重定向了1号下标位置的值就是normal.log的内容,在通过&1就是把1号的内容给二号位置,此时如果一个文件都没有关闭,而且先创建normal.log文件的的话,那么1 2 3下标的内容都是3号下标里面normal.log,这样就可以写在同一个文件里面了。
至此我们的重定向就讲解完毕了,大家到时候来支持一下博主的手写shell程序吧。
五、理解Linux下一切皆文件。
大家好好的理解一下这个图,接下来给大家讲解缓冲区
六、缓冲区
相信这个名词大家在好久之前都听到过,但是只闻其人,未见其人,所以我们对缓冲区的概念还是非常模糊的,接下来我就带大家来看看这个缓冲区是什么。
提出问题:
什么是缓冲区?
为什么要有缓冲区??
缓冲区是怎么去工作的??
缓冲区在哪里?
这些问题我们之前都是不知道的,但是这节会给大家解决的。
首先我们来看一段代码:
#include<stdio.h>
#include<string.h>
int main()
{
const char *fstr = "hello fwrite\n";
const char *str = "hello write\n";
//c语言的接口
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
fwrite(fstr, strlen(fstr), 1, stdout);
//系统调用的接口
write(1, str, strlen(str));
return 0;
}
#include<stdio.h>
#include<string.h>
#include<unistd.h>
int main()
{
const char *fstr = "hello fwrite\n";
const char *str = "hello write\n";
//c语言的接口
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
fwrite(fstr, strlen(fstr), 1, stdout);
//系统调用接口
write(1, str, strlen(str));
fork();
return 0;
}
通过着两个测试用例,大家看出来差别了吗??四个写入函数,在第一个代码中打印出来的效果来看,我相信大家没啥疑惑,但是不代表大家懂了为什么会出现这样的结果,但是第二个代码,我们在执行完上面四个代码之后再进行fork的,但是往显示器上打印还是四个,而往文件打印就成了7个,细心的通过会发现,只有c语言调用的函数接口都打印了两次,而系统调用函数接口只打印了一次,而且肯定和fork有关系,但是这个结果很让人理解。,但是这节讲的是缓冲区,所以和缓冲区肯定有关系
再来看一个案例:
因为再第一次测试的时候,两次代码只有c语言函数的接口函数出现了不同的变化,所以这次的案例,为了测试一下c语言调用函数接口,我们发现,我们的三个函数都是往显示器上打印,而我的close是再三个函数执行完毕后再关闭的,按道理往屏幕上打印是不受影响的,而两次代码的差距就在于打印的字符串有没有带\n,通过结果也可以发现,我们的第三个图没有带close,结果显示出来了,说明再执行close之前,三个函数打印的数据肯定放在其他地方了(缓冲区),而通过第二个图来看,如果1号被关闭打印不出来很正常,但是重定向到文件当中,应该把数据放进去文件,结果却是文件里面没有,原因是我么们先执行程序,显示器的数据先执行完再重定向到文件里面而1号被关闭了,就重定向不了了,所以得出1号文件没有被关闭的时候,再程序退出的时候,就会通过其他位置(缓冲区)把数据通过1号文件打印出来,如果1号被关闭,那么这些数据就被自动清除了。
七、总结
我们上面学完了稳健操作,大家对文件的恐惧感应该减少不少了,而且以前经常出现的概念今天也很好的解释了一遍,理解上面不难,主要的还是大家要贯穿,每个知识点都是连着的,跳着看就会有地方不理解,所以需要一步步,有逻辑性的去学习,那今天的只是就分享到这里了,我们下篇讲解一下文件系统。