目录
1.先认识文件
2.c语言中常用文件接口
fopen(打开文件)
3.系统接口操作文件
open
write
文件的返回值以及打开文件的本质
理解struct_file内核对象
了解文件描述符(fd)分配规则
重定向
dup接口
标准错误流
文件缓冲区
举例
总结:
简单接口的封装
1.先认识文件
首先了解文件之前,我们必须清楚的了解文件的本质:
1.文件=内容+属性,对文件的操作,就是对内容或者属性操作。
2.其实内容属于数据,属性也是属于数据的。存储文件,既要存储文件内容们也要存放文件属性。
3.我们(进程)在访问文件时,首先要打开这个文件,打开前他是存储在磁盘中的,打开后需要把文件加载都内存当中。
4.一个进程可打开多个文件,多个文件都会被加载到内存当中。
而打开文件这些工作也是操作系统干的事,因此操作系统要管理这些文件,就需要先描述在组织:
首先描述出文件的各个属性及内容,再将它组织成一个结构体,即文件的内核数据结构,通过链表的方式链接这些结构体,之后对文件实现增删查改就是操作链表。
因此打开一个文件,操作系统会在内核中形成一个被打开的文件对象来描述该文件。
5.因此一个打开的文件,与一个未被打开的文件两者此时是不一样的,没打开的文件就在磁盘里,打开的文件需要被加载到内存当中并形成对应的文件对象结构表示该文件,并用来管理该文件。
2.c语言中常用文件接口
fopen(打开文件)
先看第一个fopen参数第一个是文件路径,第二个是文件句柄(文件指针)。
1 #include<stdio.h>2 3 int main()4 {5 FILE *p=fopen("log.txt","w");//以下的方式在当前路径下打开一个log.txt文件6 if(p==NULL)7 {8 perror("fopen");//打开失败,打印错误信息9 return 1;10 }11 const char*str="hello linux\n";12 int cnt=10;13 while(cnt--)14 {15 fputs(str,p);//像文件里面写入十行字符串 16 }17 fclose(p);//关闭文件18 return 0;19 }
基本了解一下常用接口。
对于文件的打开方式,我们再看一下:
r:以读的方式打开文件,只能读。
r+:支持读文件,也支持写入,写入在文件末尾写。
对于w,以写方式打开的时候时截断的,即每次会先清空之前的内容,文件未创建会创建。即覆盖式打开文件。
而我们的重定向>文件,就是以w方式打开文件,当我们需要清空文件内容时,就是用重定向方式打开文件。
w+:与w方式一样,不过增加了读。
a:也是文件写入,只不过写入是从文件结尾写入,即追加写入。
我们也可以利用追加重定向的符号进行写入 >>文件。
a+:与a效果一样,增加了读。
3.系统接口操作文件
上述c语言操作文件的接口,在底层其实都是操作系统打开文件的指令被封装成c接口。
因此我们来尝试用系统接口操作文件:
open
首先对于open的返回值,如果打开成功,它会返回一个文件描述符(简称fd),失败会返回-1和错误信息。
第一个参数文件名,第二个参数flags,表示打开文件的标记位。
对于标志位,可能会右很多参数,对于linux下,传参方式是这样的:
这里的一个标志位,是可以传多个标志位的,
如果文件存在:
就是这里的O_RDONLY(以读方式打开),O_WRONLIY(以写的方式打开),O_RDWR(以读写的方式打开).
若果文件不存在:
标志位是下面这些,O_CREAT(创建文件),O_DIRCTORY(),O_NOCTTY().......
实际上这些标志位就是一个宏(整数),这里的每一个宏都是唯一的。
通过宏传参,我们可以实现对于满足宏条件的语句都可以执行,因此可以实现多个一起传入参数,且实现其中的多个功能。
如下一个简单的程序:
#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);//一些的方式打开,不存在就创建if(fd<0){perror("open failed");return 1;} close(fd);return 0;}
因为我们并没有提供文件属性设置的指令,因此这里会是乱码的。
因此我们可以看到对于函数open第二个是有三个参数,第三个参数就代表设置文件的权限:
这里的权限就是对应的8进制数:
当我们在打开时这样写:
int fd=open("log.txt",O_WRONLY|O_CREAT,0666);
但是这里的权限表示的是664,主要原因创建权限时,系统中会有权限掩码帮我们过滤我们设置的权限,可利用umask函数修改掩码。
//再open前加入
umask(0)//权限掩码设置为0
之后就可以看见都是6660了。
我们在一般使用的时候,就使用系统的umask。
write
打开晚间后,那么我们就可以操作文件了,最常用的就是文件写入了
向一个指定的文件描述符写入。
参数也不难看出是 fd文件描述符 buf写的字符串 count个数
char*str="hello,linux!";
write(fd,str,strlen(str));
其次,我们再写入时,如果之前有内容,并不会清空,而是从头开始覆盖式的写入,因此我们再写入之前如果想写入新的数据,需要添加新的标志位-(O_TRUNC),截断文件为0。
int fd=open("log.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);
若我们想要在文件结尾处写入,则式将这里的O_TRUNC换成0_QPPEND,追加写入。
int fd=open("log.txt",O_WRONLY|O_CREAT|O_APPEND,0666);
看到这里,我们就会发现如何去将系统库函数封装成c语言的接口。
文件的返回值以及打开文件的本质
当我们在代码创建了多个文件,将他们的文件描述符一一打印出来的时候,我们会发现他是从3开始,逐渐增加,且是连续的,逐渐增加我们明白,因为文件标识符不可能一样,通过++的方式确保每一个文件的文件描述符是唯一的,但是为什么是从3开始的呢?
首先和管理进程一样,在管理文件是操作系统要为每一个打开的文件创建对应的内核结构struct_fIle)管理该文件,结构体里面存放的就是描述文件的各种属性。
为了区分好文件结构体与进程的结构体,确定那个进程有哪些文件,文件被打开时,系统创建对应的文件结构体struct_file,同时会在该进程有一个数组里存放文件的地址,并将此时的下标返回给上级,通过此方式,哪些进程的哪些文件就可以被知道,且两者更好能区分。且这个下标就是我们这里的fd,进程标识符。
知道了这些,那么为什么不是从0开始的呢?前三个数组下标呢?
实际上,这个我们之前在学c语言的时候就会知道文件在打开时,会默认打开三个文件流:
标准输入流 stdin 键盘 0
标准输出流 stdout 显示器 1
标准错误流 stderr 显示器 2
这其实也就是前三个文件描述符。
我们知道c语言里FILE*来表示文件的开的返回值,那么是什么?
FILE* 其实是一个结构体,由c语言标准库提供,作为一个结构体,他其中必定封装有fd(文件描述符),其实对应的返回值与函数c语言库中都进行了封装。
int main() {printf("stdin->fd",stdin->_fileno);printf("stdout->fd",stdout->_fileno);printf("stderrer->fd",stderr->_fileno);FILE*p=fopen("log.txt","w");printf("FILE->fd",p->_fileno);fclose(p);}
了解了以上,那么为什么文件被打开时要默认打开这三个文件流?
默认打开这三个就是为了让用户直接读写数据,直接用函数接口操作文件。
其次在每一个文件架构体重都会有两个函数指针,red,wirte。对于键盘,显示器,这样的书输入输出设备,他们就会有对应的读写方法。
理解struct_file内核对象
我们可以大概猜到struct_file与进程的内个对象类似,里面存放了各种关于文件的信息(属性,内容), 对于结构体的创建,在内存当中被如何创建我们都可以类比进程。对于文件,我们想要读数据时,需要将文件加载到内存当中,其次写数据也是需要我们将数据先加载到内存当中,而文件数据加载到内存当中并不是直接加载到内存当中,而是先加载到文件缓冲区当中,那我们我们读写数据的本质就是在缓冲区里来回拷贝。
了解文件描述符(fd)分配规则
我们已经知道了fd本质就是文件结构体指针数组的下标,那么在实际使用时如何分配的呢?
我们继续了解一下read接口,向指定的文件标识符中读取数据。
既然默认打开三个流文件,我们来试试这
先看看输入流文件,我们知道它对应的文件标识符位0,我们直接通过read读取输入缓冲区的字符:
int main(){char buffer[100];//因为文件在打开时默认打开了三个流,因此我们可以直接做读写操作read(0,buffer,100);//给输入流读取buffer数组大小的字符串,及我们输入的字符串保存在bufferprintf("%s",buffer);return 0; }
通过打印侧面证明了我们可以访问输入这个文件。不用通过scanf来读字符。
int main(){char buffer[100];read(0,buffer,100);//给输入流读取字符串,并保存在bufferwrite(1,buffer,strlen(buffer));//给输出流写入字符串buffer return 0; }
所以fd的分配规则就是默认先打开这三个文件流,0,1,2.
且在分配的时候,从下开始找最小的,没有被使用的位置,再分配给打开的指定的文件。
当然我们也可以打开文件,再关闭这三个默认的文件流,例如close(1),那么我们想要打印出的数据就打印不出了。
重定向
基本了解以上内容之后,我们看看这个代码,是为什么呢?
int main(){close(1);//关闭输出流int fd=open(FILENAME,O_CREAT| O_WRONLY | O_TRUNC,0666);if(fd<0) { perror("open failed"); return 1; } printf("%d\n",fd); printf("stdout->%d\n",stdout->_fileno); fflush(stdout); close(fd); }
首首先我们关闭了输出流,运行程序之后,没有输出信息,没问题,关闭之后我们又打开了一个文件,该文件的输入输出错误流是打开的,此时我们进行打印之后,就刷新缓冲区,打印的信息,没出来,正常,但是当我们打开文件后,却发现写入显示器的内容到了文件里面,而这就是输出重定向。
那么为什么会是这样呢?
总结的来说就是,刚开始关闭了输入流,这里的结构体文件指针下标1对应没有指向(原本只想显示器),此时又打开了一个文件,(此时操作系统要看当前文件与此时打开的文件的关联,即打开我们.c文件时,下标0,2对应的指针又指向,1被关闭了,此时再次打开一个文件,则该文件地址要放到结构体指针数组,且要从最小的没被占用开始,所以该文件指向了下标1,即我们新打开的这个文件的描述符变成了1),因此打印进文件当中了。
因此,文件结构体数组下标没变,我们改变下标对应的内容,就可以实现重定向。
上述就是输入流:stdout->显示器转变stdout->文件
例如我们这里可以重新让他输入,而且这里我们使用fprintf接口,与printf差不多,只是多了我们需要的接口。
printf的stdout默认是向显示器里输出,利用fprintf我们可以决定向哪个地方输出,
将上述的代码的两个打印变成这样写:
fprintf(stdout,"%d\n",fd); fprintf(stdout,"stdout->%d\n",stdout->_fileno);
取消关闭输出流,在运行就可以看到会向显示器打入:
此时新打开的文件标识符位3,默认stdout的是1 。向显示器打印。此时数据1位置指向的是显示器。
再关闭,在运行,此时1位置对应的指向是文件,我们就发现他就是相对应文件里打印数据。
根据上述结论可以验证我们的想法。
打开文件的文件标识符为1,stdout也为1/
但是对于这里我们是通过刷新缓冲区测i可以,去掉刷新缓冲区,文件里也没写入,这与我们的文件缓冲区有关。
通过上述方式,我们很容易实现输出输入重定向。
我们可以从文件读数据,从键盘读数据,我们也可以向显示器打印数据,也可以向文件打印数据。
上层fd不变,指向的内容在改变。
dup接口
我们可以看到关于dup函数的功能,它会改变旧文件的文件描述符,即改变文件指针的指向。且dup会默认关闭旧的文件流(这里就是显示器)。
通过该种方式,我们也可以实现文件描述符的重定向,其中在文件结构体内部还定义了一个引用计数的fcount,有几个文件指向结构体数组的某个位置,对应fcount就为几,当fcount为0时就会释放掉。当然我们也可以自己再重定向之前关闭这个文件流,不关闭系统也会帮我们重新指向。
int main(){int fd=open(FILENAME,O_CREAT| O_WRONLY | O_TRUNC,0666);if(fd<0){perror("open failed");return 1;}//利用dup接口实现重定向,我们这里替换下标为1的输出流文件dup2(fd,1);//把原本文件的fd变成1,此时下标为1的地址指向的是文件fprintf(stdout,"file->%d",fd);fprintf(stdout,"stdout->%d",stdout->_fileno); return 0;
}
了解到重定向的本质,我们就可以通过命令行参数,来自己实现输入重定向符号>,追加重定向>>。
其次进程替换与重定向两者之间是没有影响的。程序替换只是地址映射发上了变换,并没有创建新的进程。,因此不影响重定向。
标准错误流
标准输入输出存在,我们很清楚,每次都要先打开这两个流我们也能理解,那么标准错误流,为什么每一次也要打开,错误流是干什么用的。
首先除了重定向输入流,输出流,我们也是可以重定向错误六,例如我们可以将错误流重定向到输出流。
1>log.txt 2>&1,通过连续的重定向我们可以实现,将多个文件的内容指向输出流1。通过该种方式我们就可以看到错误信息,方便我们排查。
文件缓冲区
当一个文件被访问时,无论读写都需要我们将它加载到文件缓冲区当中。即我们读写的数据都先事先拷贝到一个buffer里,再通过write接口将内容写到文件当中,且一个文件的缓冲刷新机制是全缓冲的。
那么什么是缓冲区,如何去理解缓冲区?
对于缓冲区,就是一部份内存,我们将数据拷贝其中,无论是C语言还是c++,我们都里理解为是malloc开辟出来的一块空间。
那为什么要有缓冲区呢?
缓冲区的作用是为了提高使用者的效率,我们不再把数据直接交给对方,而是先直接交给缓冲区,对于用户我们就直接完成了工作,缓冲区之后的工作让操作系统来搞。
因为有缓冲区,我们可以积累一部分派发数据,拥有不同的派发效果。当然积累数据的不同程度,缓冲区有不同的派发策略-----(无缓冲,行缓冲,全缓冲....)。其次也有特殊情况下的策略-------如强制刷新,进程退出(强制刷新)。
用过这种方式也提高了发送的效率。
举例
下来我们分析一下这段代码、
#include<stdio.h>2 #include<unistd.h>3 #include<string.h>4 int main()5 {6 7 fprintf(stdout,"hello linux1\n");8 fputs("hello linux2\n",stdout);9 printf("hello linux3\n");10 const char *str="hello write\n";11 write(1,str,strlen(str));12 13 fork();//注意在该位置创建的子进程 14 return 0;15 }
运行之后的输出,和重定向到log.txt的输出,为什么是这样子的?
对于write接口的数据先写到文件当中,其次是c的接口的写入,且写了两份?
首先第一次直接输出到显示器上的,没问题,创建的子进程的位置也是在最后,与他无关。
输入正常。
第二次重定向到文件之后,再打开里面的内容为什么是这样子的呢?
这里的fork是如何执行的呢?
首先我们的缓冲区刷新是行刷新,因为我们在每一句话后面都带\n了,即在fork之前,缓冲区都是刷新了的。
但是当我们通过输出重定向到log.txt,此时我们依然知道这里的文件结构一直真的数组下标1里面不是指向显示器的,而是指向log.txt的文件当中了,此时的缓冲区刷新由行刷新变成了全缓冲,
全缓冲意味着缓冲区变大,更加意味着,我们写入的简单数据不足以把缓冲区写满,因此fork在执行的时候,此时数据还在缓冲区中。
目前我们这里所说的缓冲区与操作系统无关,而是c语言的封装,因为我们的接口printf,fprintf底层都是write的封装,但是这里取有缓冲区的概念,系统接口本身是没有这的。因此这里的缓冲区就是与c语言本身自己有关。
而我们这里的c/c++提供的缓冲区所保存的数据,属不属于进程的数据呢?实际上该缓冲区的数据属于进程本身的数据。
但是对于文件里面的数据,他与进程是什么关系呢,文件里的数据与进程的此时来说因该是没有啥关系的,即文件里的数据不属于进程的数据,当我们把缓冲区的数据交给到操作系统时,此时的数据就与进程没什么关系,而是属于文件。
因此当进程退出的时候,一般要刷新缓冲区,而刷新缓冲区就是要把这里的数据写到文件当中。
而父子进程,数据共享,当一方写入到文件中时(对于这里来说就是任意一个进程退出,强制刷新缓冲区,像文件里写入),两方都发生写时拷贝,因此我们的数据写入了两份。
但是对于系统调用接口,他与缓冲区没有任何关系,他是直接写入的,因此系统接口只写一次。
而且系统接口先进行写入,之后缓冲区刷新,再写入缓冲区的数据。因此我们看到先写的write的内容,之后是两份拷贝。
总结:
对于系统的文件读写接口,直接接像文件写入的,但是c/c++等语言不仅对读写接口进行了封装,还提供了缓冲区,使得读写数据对缓冲区操作,缓冲区的数据属于进程,但当刷新写入文件当中时,此时的数据不属于进程,而属于操作系统(文件)。
对于c语言,我们的缓冲区就封装在FILE中(由许多指针构成的一块空间)。
简单接口的封装
.h文件
pragma once
#define MAXSIZE 1024
//三种刷新策略
#define Flushline 1
#define Flushall 2
#define Flushnone 3typedef struct _myFILE
{int fileno;//文件标识符char buffer[MAXSIZE];//缓冲区int end;//占用空间int flag;//标志
}myFILE;myFILE* my_fopen(const char *path,const char* mode);
int my_fclose(myFILE*fp);
int my_fputs(const char*s,int num,myFILE* stream);
int my_fflush(myFILE*stream);
.c文件
#include"mystdio.h"
#include<string.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<stdlib.h>
#include<errno.h>
#include<unistd.h>
#define permison 0666
//简单的封装io接口myFILE* my_fopen(const char *path,const char* mode)
{int fd=0;//文件标识符int flag=0;//标志位,模式//打开文件模式if(strcmp(mode,"r")==0){//以读的方式打开flag |=O_RDONLY;//只读}else if(strcmp(mode,"w")==0){//以写的方式打开flag|=(O_WRONLY | O_CREAT |O_TRUNC);}else if(strcmp(mode,"a")==0){//以追加的方式flag|=(O_WRONLY|O_CREAT|O_APPEND);}//打开文件if(flag & O_CREAT){//若文件不存在,创建文件fd=open(path,flag,permison);}else{fd=open(path,flag);}if(fd<0){errno=2;return NULL;}//打开成功,返回myFILE*p=(myFILE*)malloc(sizeof(myFILE));if(!p){//申请失败errno=3;return NULL;} p->flag=Flushline;p->fileno=fd;p->end=0;return p;
}
int my_fclose(myFILE*stream)
{my_fflush(stream);//把数据刷新到内核当中//fsync(stream->fileno);return close(stream->fileno);
}
int my_fputs(const char*s,int num,myFILE* stream)
{//把字符写到缓冲区当中memcpy(stream->buffer+stream->end,s,num);//从end位置处写入stream->end+=num;//判断\n ,遇到就要刷新if(stream->flag & Flushline&& stream->end>0 &&(stream->buffer[num-1]=='\n')){my_fflush(stream); //行刷新}return stream->end;}
int my_fflush(myFILE*stream)
{ //若缓冲区为空,就直接关闭if(stream->end==0){close(stream->fileno);}//刷新,写到文件中write(stream->fileno,stream->buffer,stream->end); stream->end=0;return 0;
}