文件操作的底层原理(文件描述符与缓冲区) | 您所在的位置:网站首页 › 计算机缓冲区是什么意思啊 › 文件操作的底层原理(文件描述符与缓冲区) |
文章目录
零.前言一、C/C++语言的文件操作回顾1.C语言文件操作2.C++文件操作
二、三种输入输出流1.读写的本质2.stdout和stderr区别
三、系统调用接口1.open和close的概念2.open和close的使用2.系统调用接口的参数和返回值
四、文件描述符1.文件描述符的值2.操作系统对文件的管理3.父子进程的文件关系4.不同外设的读写
五、文件描述符的分配规则以及输入重定向1.文件描述符的分配规则2.重定向(1)输出重定向(2)追加重定向(3)输入重定向(4)直接重定向
六、缓冲区1.缓冲区的分类2.缓冲区的刷新策略3.对缓冲区刷新的理解
七、总结
零.前言
在C语言和C++中都存在文件操作,通常是以读或者写的方式打开文件,然后进行读写,最后关闭文件。但其实文件操作的底层并没有这样简单。 文件操作的底层原理分为两部分,分别某一进程找到它打开的文件,某一进程对该文件进行操作,要理解这两部分,就需要理解文件描述符和缓冲区。 一、C/C++语言的文件操作回顾 1.C语言文件操作 #include #include int main() { FILE* fp1=fopen("./log.txt","w"); if(fp1==NULL) { perror("fopen"); return -1; } int cnt=10; while(cnt--) { const char* msg="hello file!\n"; fputs(msg,fp1);//写操作 } fclose(fp1); FILE* fp2=fopen("./log.txt","r"); char buffer[64]; while(fgets(buffer,sizeof(buffer),fp2)) { printf("%s\n",buffer);//读操作 } if(feof(fp2))//判断是否正常退出 { printf("fgets quit normal!\n"); } else { printf("fgets quit not normal"); } fclose(fp2); return 0; } 2.C++文件操作 #include #include #include using namespace std; int main() { ofstream out("./log.txt",std::ios::out|std::ios::binary); if(!out.is_open()) { std::cerr printf("open error!\n"); } else { close(fd); }此时以O_WRONLY|O_CREAT的方式创建了一个文件log.txt,表示的是以写的方式打开,如果文件不存在则创建它。 权限为0644,转换为二进制表示为110 100 100。 我们可以得知,当C语言使用fopen来对open进行封装时,没有让我们自己规定权限,说明在封装的过程中权限已经被规定了。 此时我们可以观察到log.txt的权限: 文件名没有什么可以介绍的,下面主要来介绍标志位: 通过man手册查询可知,flags的类型为int类型,而显然O_RDONLY,O_WRONLY等都是宏,因此,标志位是由int型所定义的宏。 在代码中使用或操作符O_WRONLY|O_CREAT来满足实现两者中的一种操作。因此我们可以大概可以猜到这些宏是怎么定义的,即:这些宏都是只有一个比特位为1的数据,并且不重复。 我们可以通过查询这些宏的定义来验证这一猜想: grep -ER 'O_CREAT|O_RDONLY|WRONLY' /usr/include/
每当操作系统打开一个文件的时候,会给他一个编号,这个编号就叫做文件描述符。当打开文件失败,则文件描述符为-1。 其中标准输入,标准输出,标准错误的文件描述符分别为0,1,2(因为在操作系统眼中,它们都是以文件的形式来存在的。) 首先我们可以来接收一下open的返回值,即文件描述符: int fd=open("./log.txt",O_WRONLY|O_CREAT,0644); printf("%d\n",fd); if(fd close(fd); }最终得到open的返回值,即文件描述符的值是:3,这是因为在C语言中0,1,2号文件是默认被打开的。 我们对文件进行操作就需要打开文件,而打开文件是由某一个进程来完成的,打开文件的本质是将文件信息加载到内存中,而一个进程可以打开很多个文件,如果有很多进程,那么内存中就存在很多的已经打开的文件的信息。因此操作系统是需要对这些打开的文件来进行管理的。 操作系统的管理方法是:先描述,再组织。而对文件的描述是在一个名为file的结构体中进行的。 一个文件包括内容和属性两个部分(比如创建一个空文件,在磁盘中也会占据空间的,这是因为需要存储该空文件的属性)。因此在file结构体中有文件的内容和属性两个内容。同时,操作系统将所有打开的文件通过数据结构组织了起来(即将各个file结构体组织了起来),每一个进程需要知道自己打开的文件在哪一个位置,因此在PCB中需要一个来描述该进程打开哪些文件的结构体files,而结构体files中存在一个指针数组array_file,它的每个元素指向的就是该进程打开的每一个文件对应的file,下面用一张图来说明几者之间的关系: 子进程在创建之初是父进程的拷贝,它的files结构体与父进程是相同的,因此父进程打开的文件子进程也会进行打开。而我们在创建进程打开的标准输入,标准输出以及标准错误其实是由bash打开的,由各个进程继承下来的。 4.不同外设的读写注意,系统调用接口只有一套,但是显然我们对不同外设的读写方式是不一样的,比如对磁盘,显示器的读写,对标准输入甚至不需要写操作。那么一套系统调用接口如何实现对不同外设的读写呢? 这和C++的多态有些相似,不同外设的读写方式被写入了对应的驱动中,但调用读写操作的时候,实际上调用的是该外设的驱动上的读写操作,从而完成读写的。外设都有I/O接口,但不一定都要被实现。 五、文件描述符的分配规则以及输入重定向 1.文件描述符的分配规则标准输入,标准输出以及标准错误的文件描述符分别为0,1,2,并且会在进程打开的时候自动进行打开,从而导致后序创建的文件的文件描述符的值为3,4,5…如果我们在进程中将标准输入或输出关闭呢?后序文件的文件描述符的值是否会发生变化呢? 答案是会的,我们可以通过下面的例子来总结一些规律: close(0); int fd=open("./log.txt",O_WRONLY|O_CREAT,0644); printf("%d\n",fd);通过close将标准输入关闭,此时我们运行程序会发现,log.txt的文件操作符变成了0。正好填补了空出的0号位置。
打印的结果是: 即将本来要打印在显示器上的内容打印在了文件log.txt中,它的原理就是先将显示器关闭,使得log.txt的文件描述符为1,然后调用系统调用函数来向描述符为1的文件,即log.txt进行写入。 注意输出重定向是将标准输出重定向,而不是标准错误。在标准错误打印的内容不会被重定向,因为它的文件描述符是2。 (2)追加重定向追加重定向指的是重定向的内容不覆盖文件中原有的内容。只是在open函数中的flag多加了一个APPEND的参数,其他和输出重定向相同。 (3)输入重定向输入重定向即将标准输入关闭,将某一个文件来作为输入。 close(0); int fd=open("./log.txt",O_RDONLY,0644); char line[128]; while(fgets(line,sizeof(line)-1,stdin)) { printf("%s\n",line); }此时fgets拿到的就是文件log.txt中的内容,而不是标准输入的,说明它本质是通过stdin的文件描述符来寻找文件的。 (4)直接重定向我们会发现,如果想完成重定向操作需要进行关闭标准输入输出等操作,是比较麻烦的,因此系统提供了一个dup2函数来直接进行重定向的操作: 此时将数组中的1下标中的元素替换为fd下标的元素,上层对文件描述符为1的文件进行操作就是对之前的fd对应的文件进行操作。 六、缓冲区 1.缓冲区的分类在C语言中我们在学习getchar函数的时候就提到过缓冲区,只不过只是浅尝则止。其实缓冲区分为两类,分别是用户缓冲区和系统缓冲区。 用户缓冲区的数据最终刷新到系统中,系统缓冲区的数据最终刷新到硬件上。在C语言中我们写入的数据会首先保存在用户缓冲区中,该用户缓冲区是由C语言提供的。 在C语言的结构体FILE中,不仅仅封装了文件描述符,还封装了缓冲区。 我们可以在usr/include/libio.h找到关于FILE的定义: 1.立即刷新(不缓冲) 2.行刷新(行缓冲):遇到换行操作,则进行缓冲区的刷新。比如向显示器的刷新。 3.全缓冲:缓冲区满了即进行刷新,比如向磁盘文件中写入,这样也解决了缓冲区的溢出问题。 3.对缓冲区刷新的理解首先我们来验证缓冲区的刷新策略。 const char* msg="hello 标准输出!\n"; write(1,msg,strlen(msg)); printf("hello printf!\n"); fprintf(stdout,"hello fprintf!\n"); fputs("hello fputs!\n",stdout);这段代码的打印结果很简单: 当我们在代码的末尾将文件1关闭的时候,再进行重定向: ./mytest1>log.txt此时我们在文件中看到的结果是:
当我们在代码结尾创建一个子进程的时候: 本文阐述了数据被写入硬件的完整过程,首先数据是在一个进程中完成写入操作的。通过该进程的PCB中的file指针,会找到该进程的files结构体,在files结构体中有一个数组,它的元素是该进程打开的文件的指针(指向用来描述文件的file结构体),它的下标是各个文件的文件描述符。 在用户层面,对某一个文件进行写入只需要知道它的文件描述符即可,在写入的过程中,如果是用户层面的写入需要先将内容写入用户层的缓冲区,然后根据要写入的不同硬件的刷新策略,将数据刷新到系统缓冲区,最终再写到硬件中。如果是系统层面的写入,则直接在系统缓冲区写入,然后再写入硬件。 总体来说,文件操作分为两个部分,分别是找文件和写文件,对应的分别为对文件操作符的理解,和对缓冲区的理解。 |
CopyRight 2018-2019 实验室设备网 版权所有 |