文件的初步理解
C语言中我们了解过一些有关文件处理的函数,如:fopen、fclose、fread……这些函数其实都是封装了系统调用接口,从而利于我们直接使用。
认识文件
了解文件之前我们必须知道:文件=内容+属性。文件的属性标识着文件的时间、权限、大小、拥有者…… 而对于文件的内容就是文件中的存储数据。其实归结到一起,文件的内容和属性其实都是数据。
文件访问与管理
就拿我们磁盘上的文件来说,我们要想访问一个文件的话,就要先将文件双击也就是打开文件,然后再操作文件。所以我们要知道:访问文件前要先打开文件。而C语言中我们可以通过fopen函数来打开一个文件,但是访问结束别忘了关闭文件:fclose。对于我们用C语言去调用fopen打开文件的过程其实是进程通过操作系统帮助我们打开的这个文件(操作系统为我们提供了系统调用接口),因为只有当我们的程序加载到内存中运行起来才会调用fopen函数去打开文件。所以说我们访问文件本质上其实是进程通过操作系统帮我们访问文件的。
我们的文件创建了以后其实是存在磁盘上的,而操作系统是对文件能够进行管理的,此时由冯诺依曼体系结构,我们知道这之中必定要借助内存,所以当我们进行文件访问时,被访问的文件是通过操作系统从磁盘加载到内存中,然后被操作系统进行管理,而管理的本质是:先描述再组织。所以就像操作系统管理进程时会为每一个进程创建PCB一样,管理打开的文件时也会在内核中创建一个struct file的结构,其中存储文件的所有数据。但是管理文件却有一点不同的,因为基于上段内容我们知道是进程来访问文件的。每一个进程访问的有可能是多个文件,所以综上我们可以知道:操作系统可以借助进程PCB去管理该进程所访问的文件。
所以我们基于上面的分析就又可以推测出进程PCB中应该也存放着一个叫做:struct file* array[]的指针数组,将每个进程所打开的文件管理起来。
C文件接口的使用
int main()
{FILE* pf = fopen("bite.txt","w");//w方式打开,不存在就创建文件,且清空数据if(NULL==pf){cout<<"文件打开失败"<<endl;return 1;}fputs("Linux so uneasy\n",pf);fputs("Linux so uneasy\n",pf);fclose(pf);return 0;
}
这里想说的就是,当我们实现好makefile以后,执行完make指令之后是不会创建bite.txt这个文件的,此时只是会生成exe可执行程序,并没有执行该程序,也就是说该程序还没有变成进程,所以我们无法创建文件,也就是说,本质上是进程通过操作系统帮助我们打开这个文件。
此时打开的方式我们要知道常用的两种:w && a
w和a在打卡不存在的文时都会创建文件,但是 w方式就是在每次打开文件的时候都会对文件所有内容进行自动的清除,而a方式并不会清楚文件内容而是进行追加方式打开文件。
而且注意w打开方式其实对应的就是输出重定向(>);而a的打开方式对应的是追加重定向(>>)。
还有一些其他的打开方式:
r:只读的方式打开文件。(文件必须存在)
r+:读写的方式打开文件,可以写文件,打开文件后进行写入操作时,会覆盖原有位置的内容。(文件必须存在)
w+:读写的方式打开文件,其余就像w一样。
a+:以可读写的方式打开文本文件,其他与a一样。
C语言的函数其实都是封装了系统调用接口的,因为进程是通过操作系统来打开文件的,操作系统是进行软硬件管理的一款软件,而且操作系统是不允许我们直接对硬件资源进行访问操作的,所以操作系统会为我们提供系统调用接口供我们进行硬件访问。
认识系统调用接口
文件打开:
以上是文档,以下是文档的内容解释说明:
此时系统调用open的第一个参数是指文件名,而第二个参数就是文件打开方式,但是不同于C接口。这里的flag是一个整数,但是其中包含了32个比特位的数据的,所以实质上是通过对应比特位来标志着不同的打开文件方式的。其实底层就是多个if语句分别进行判断该比特位上数据是否为1,为1则表明数据有效,满足该打开方式。
但是当我们新建文件时,最好给该文件赋予权限,否则的话就可能会是乱码式的权限。别忘了umask权限掩码,但是可以直接函数调用umask(num)函数,设置权限掩码为num。
int main()
{int fd = open("test.txt",O_WRONLY|O_CREAT,0666); if(fd==-1){cout<<"调用失败"<<endl;}close(fd); return 0;
}
其实类比上C接口
:w文件打开方式就相当于 open("test.txt",O_WRONLY|O_CREAT|O_,TRUNC0666);
:a文件打开方式就相当于是open("test.txt",O_WRONLY|O_APPEND,0666);
文件写入:
对于像文件中写入数据,我们有write系统调用接口。
int main()
{int fd = open("test.txt",O_WRONLY|O_CREAT,0666); if(fd==-1){cout<<"调用失败"<<endl;}write(fd,"haha",4);close(fd); return 0;
}
此时需要注意的就是,这个count参数是你的指针buf的长度-1(不算上\0)其实\0是C语言规定的,但是我们操作的是文本文件内容,所以不需要关注\0。
文件打开的返回值:(文件描述符的认识)
int main()
{int fd1 = open("test.txt",O_WRONLY|O_CREAT,0666);int fd2 = open("test.txt",O_WRONLY|O_CREAT,0666);int fd3 = open("test.txt",O_WRONLY|O_CREAT,0666);cout<<fd1<<" "<<fd2<<" "<<fd3<<endl;close(fd1); close(fd2); close(fd3); return 0;
}
首先我们知道文件打开的返回值是整型,而且经过验证发现文件依次打开后的返回值是有顺序的,而且在上文中对文件的访问与管理有了初步的认识,文件是被操作系统管理起来的,而管理自然就会创建文件的描述结构体对象,而每个进程通过操作系统打开的不仅仅只有一个文件,就像我们这里打开了三个文件(虽然都是同一个文件),而这些文件的返回值不就恰恰说明了文件存放顺序嘛,而存放在哪里呢,是一个文件类型的指针数组,而返回值就是下标。其实操作系统访问文件时访问的就是下标。
进程会默认打开文件 :
但是为什么创建的文件不是对应着0、1、2描述符的呢,其实每个进程在运行的时候都会默认打开标准输入stdin(键盘)、标准输出stdout(显示器)、标准错误stderr(显示器)。
这里C语言提供的FILE实际上是一个结构体,其中就封装了文件描述符的值,因为文件描述符就是文件的标识,操作系统对指定文件的操作实际上就是根据文件描述符进行区分文件的。
文件描述符的理解
分配文件描述符
我们前面在连续创建文件时,可以看出文件描述符是依次从3开始进行分配的。其中的本质就是进程文件描述表的下标位置,该表中存放的就是指向文件的指针。但是如果我们进行文件关闭,并再次创建文件,此时的文件描述符是如何呢?
test_1:
int main() {int fd1=open("test.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);cout<<"fd1 = "<<fd1<<endl;close(fd1);int fd2=open("test.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);cout<<"fd2 = "<<fd2<<endl;close(fd2);return 0; }
test_2:
int main() {close(0);//关闭stdin文件int fd2=open("test.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);cout<<"fd2 = "<<fd2<<endl;close(fd2);return 0; }
所以就可以说明文件描述符指向的文件被关闭以后,该文件所存在文件描述符表中的数据也被清除了,而下一次再次创建文件时,新文件的文件描述符就从最低位开始依次向空的位置处填充。
认识read系统调用
该系统调用函数是从文件描述符fd中读取文件数据并放到buf中,而count就是buf指针内部的空间大小,返回值是实际读取数据的个数。
int main()
{char buf[501];ssize_t sz=read(0,buf,500);//sz是实际字符个数buf[sz]=0;//末尾\0用于打印if(sz>0){cout<<buf<<endl;}return 0;
}
我们的这段代码就是从标准输入中读取数据,然后打印出来,标准输入对应的就是键盘,所以我们从键盘中输入什么数据,最终就会打印什么数据,别忘了最后还有一个回车键。(如果不想要最后的回车键就直接将 buf[sz-1]=0;进行设置)
回顾write系统调用
这里write系统调用需要我们注意的就是,文件中存的字符信息和我们语言中的字符串是不同的,我们语言中都会在字符串结尾+\0但是文件并不需要。以至于文件的写入都会将字符串的长度传过去。
int main()
{char buf[501];ssize_t sz=read(0,buf,500);buf[sz]=0;if(sz>0){// cout<<buf<<endl;write(1,buf,strlen(buf));//1->标准输出(显示器)}return 0;
}
数据读写本质
在我们进行数据的读写时,操作系统都会将文件数据从磁盘加载到内存的文件缓冲区中。所以我们读写本质就是将内核的缓冲区数据拷贝(拷回)到当前临时对象,也就是进行数据的来回的拷贝。
重定向
重定向主要讲讲输入输出两种,对应着键盘和显示器,所以自然就和文件联系起来了,而我们对具体文件的操作本质上都是通过文件描述符表的指向的的而不是C语言中的FILE*指针变量名的。
输出重定向
int main()
{close(1);//stdoutopen("test.txt",O_WRONLY|O_TRUNC|O_CREAT,0666);//printf("haha\n");return 0;
}
对于以上现象其实也挺好解释的,我们知道进程运行时会默认打开3个文件,所以我们C语言调用printf函数时就是默认向显示器中进行打印数据,但是我们代码中关闭了该文件,且紧接着创建test.txt这个文件,所以根据文件描述符分配规则:寻找最小的且,没有被使用的位置所以,此时自然文件描述符1对应的就是test.txt这个文件,而文件的本质区分就是文件描述符,所以此时打印的数据就进入了test.txt当中。
分析图如下:
输出重定向
了解了输入重定向以后,输出重定向就好理解多了。
int main()
{close(0);//stdinopen("test.txt",O_RDONLY,0666);//char buf[30];ssize_t sz = read(0,buf,30);buf[sz]=0;cout<<buf<<endl;return 0;
}
所以基于上面的测试可以得出:重定向本质其实就是修改文件描述符表特定数组下标里的内容。
重定向系统调用接口
对于以上的两种重定向方式都属于原理层的使用,实际上,我们是可以直接调用接口实现重定向的。dmp2
翻译理解后就是:oldfd才是重定向之后的文件,也就是保留下来的文件 其功能就是将文件描述符表中oldfd下标里的内容拷贝到newfd里。也就是说明此时有两个下标里的内容指向的是同一个文件,所以对于这同一个文件会采用引用计数的方式避免关闭文件时会同时关闭的情况。
int main()
{int fd = open("a.txt",O_RDWR|O_CREAT|O_TRUNC,0666);int fdd = open("b.txt",O_RDWR|O_CREAT|O_TRUNC,0666);write(fdd,"abcabc",6);dup2(fd,1);//fd输出重定向dup2(fdd,0);//fdd输入重定向//将fdd中的数据输出到fd中char buf[1000];rewind(stdin);//将位置指针移动到文件开头//fseek(fp, 0, SEEK_SET);//将位置指针fp移动到文件开头ssize_t sz = read(0,buf,1000);//从sdin中读取数据buf[sz]=0;cout<<buf<<endl;//输出到stdout里close(fd);close(fdd);return 0;
}
以上代码是通过系统调用接口函数同时实现输入与输出重定向,其实也没什么,但是有一点就是:文件每当进行一次读写后,该指针自动指向下一次读写的位置。 当文件刚打开或创建时,该指针指向文件的开始位置。!!!!!!!!!!!
也就是说如果没有rewind函数将文件位置指针移到文件开头的位置的话,那么后面的read函数始终无法从0号标识符下读取数据,因为此时的位置指针已经指向文件最后的位置,所以实际读取到的有效数据始终为0,因此必须将位置指针回位。
认知stderr标准错误
stderr标准错误对应的也是显示器,就是向显示器里打印,但是我们已经有了标准输出stdout为什么还要标准错误stderr呢,stdout也是向显示器中打印的啊。
int main()
{fprintf(stdout,"hello stdout\n");fprintf(stderr,"hello stderr\n");return 0;
}
这里我们分别打印数据到标准输出和标准错误中,对应的也就是显示器,所以执行可执行程序的时候就会向显示器中都打印出来。
我们可以重定向输出:
我们知道 > 是输出重定向,就是将本该在标准输出里的内容输出到文件tmp.txt中,但是我们此时的标准错误并没有重定向,所以stderr打印到了显示器上,而我们打印tmp.txt中的文件就是:
当我们想将标准输入和标准输出同时重定向的话就可以:
其中的2文件描述符对应的就是标准错误,此时的1(标准错误)已经重定向成了tmp.txt了,所以stderr也重定向到了tmp.txt了
所以基于上面的内容我们可以了解到标准输出和标准错误虽然都是对应的显示器,但是我们可以将两者重定向打印到不同的文件中进行区分。