fd和缓冲区
1. 文件描述符fd
1.1. 概念与本质
- 定义:是用于标识打开文件的非负整数。
- 文件描述符的本质,就是数组下标。
1.2. 打开文件的管理
问:为什么访问文件的系统调用接口,都必需使用文件描述符fd?
- 当我们打开一个文件时,OS会在内存中创建一个file结构体,用来描述被打开的文件,这个结构体包含了文件的当前读写位置、文件描述符、文件路径等相关信息。
structfile{structpath f_path;// 文件路径structfile_operations*f_op;// 文件操作指针int _fileno;//文件描述符loff_t f_pos;// 当前文件读写位置...};structfile_operations{int(*open)(structinode*,structfile*);int(*release)(structinode*,structfile*);ssize_t(*read)(structfile*,char __user *,size_t,loff_t*);ssize_t(*write)(structfile*,constchar __user *,size_t,loff_t*);loff_t(*llseek)(structfile*,loff_t,int);...};
- 因为open函数是由进程来执行的,所以必须让进程和文件关联起来。在每一个task_struct结构体中都包含了一个struct file_struct*类型的指针,它指向了一个包含文件描述符表的file_struct结构体。
structtask_struct{...
files_struct *files;// 文件描述符表...};
- 文件描述符表是一个file*类型的指针数组,每个元素都指向一个打开的文件,而文件描述符就是此数组的下标,所以只要拿到了文件描述符表,就可以索引到对应的文件。
structfiles_struct{...structfile*fd[FD_SETSIZE];// 文件描述符数组...};
打开文件本质:在内核中,把文件在磁盘上找到、内容和属性加载,在内存中创建file结构体,属性、方法、缓冲区都初始化,然后把结构体链入到系统管理文件的链表中,并且在指针数组中找到一个数据下标,再把它的地址填充进来,最后把数组下标(fd)返回给上层用户,应用层得到fd值。
a. 把数据写到文件中write:
- 因为write是由进程通过系统调用来执行的,而系统能够识别出是哪个进程在请求服务,即:OS可以找到进程(task_struct);
- 因为write需要访问文件,所以通过fd,直接在数组中进行索引,从而找到文件。再把用户空间中的缓冲区buf中的数据,拷贝到内核空间中的文件结构体对象的缓冲区中,最后让OS把缓冲区的刷新到磁盘文件中。
b. 从文件中读取数据read:
- 因为read是由进程通过系统调用来执行的,而系统能够识别出是哪个进程在请求服务,即:OS可以找到进程(task_struct);
- 因为read需要访问文件,所以通过fd,直接在数组中进行索引,从而找到文件。如果文件结构体对象的缓冲区中有内容,就直接读取到用户空间的缓冲区buf中,反之,就让OS把磁盘文件中的数据导入到内存中。
c. 关闭文件close:
- 因为close是由进程通过系统调用来执行的,而系统能够识别出是哪个进程在请求服务,即:OS可以找到进程(task_struct);
- 因为close需要访问文件,所以通过fd,直接在数组中进行索引,从而找到文件。OS再将文件结构体对象进行释放。
1.3. 一切皆文件的理解
一、文件系统的抽象和VFS
- VFS(Virtual File System):虚拟文件系统,是Linux内核的一个软件层,它提供了一套统一的接口来访问各种类型的文件系统和硬件设备。
这种设计使得用户和应用程序能够通过调用相同的系统调用(如open、write、read等)来操作不同的文件系统,而无需关心底层文件系统的具体实现细节。
- 文件操作结构体file_operations:在linux内核中,每个打开的文件都有一个指向file_operations结构体的指针,它包含一系列函数指针,即:它定义了文件的各种操作(如:读、写、打开、关闭),其内包含的函数指针指向具体的实现方法。
不同的文件系统或硬件的驱动程序会提供这些函数的具体实现,但这些函数的参数类、返回值类型、函数名,必须与定义在file_operations结构体中的函数指针相匹配。
structfile{structinode*f_inode;// 文件的inodestructfile_operations*f_op;// 文件操作函数指针unsignedlong f_flags;// 文件标志loff_t f_pos;// 当前文件位置// 其他信息...};structfile_operations{//文件操作函数int(*open)(structinode*,structfile*);int(*release)(structinode*,structfile*);ssize_t(*read)(structfile*,char __user *,size_t,loff_t*);ssize_t(*write)(structfile*,constchar __user *,size_t,loff_t*);loff_t(*llseek)(structfile*,loff_t,int);// 其他操作...};
- VFS的核心思想是通过抽象和封装来屏蔽底层硬件的差异。
二、面向对象编程的类比
- 多态性:在面向对象编程中,多态性允许我们使用统一的接口来调用不同的实现。在VFS中,是通过函数指针来实现多态性,不同的文件系统的具体实现方法不同,但上层应用程序只使用统一的函数指针接口。
- 封装:VFS屏蔽了文件系统和硬件设备的差异,即:隐藏了底层的细节;使得上层应用程序可以使用统一的函数指针接口来访问文件。
上层代码无需关心底层操作的实现,只需按照统一接口的规范进行操作。
不能从显示器读数据,平时在显示器输入的东西,显示器也显示了,不是通过显示器把数据交给了你的程序,而是从键盘中输入数据,你的程序先从键盘中读到的,为了让用户看到你输入的数据,程序就把数据同步的给显示器拷贝了一份。
1.4. 分配规则
- fd分配规则:最小未被使用的数组下标,会被分配给最新打开的文件。
#include<stdio.h>#include<sys/types.h>#include<fcntl.h>#include<unistd.h>#include<sys/stat.h>#include<string.h>intmain(){close(1);//关闭标准输出流int fd1 =open("log.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);printf("fd1: %d\n", fd1);//printf默认向stdin—>fd = 1打印printf("hello world\n");//输出重定向:本来应该把内容向显示器文件进行写入,更改为向磁盘文件进行写入return0;}
#include<stdio.h>#include<sys/types.h>#include<fcntl.h>#include<unistd.h>#include<sys/stat.h>#include<string.h>intmain(){close(0);//关闭标入输出流 int fd2 =open("data.txt", O_RDWR);printf("fd2: %d\n", fd2);char buff[64];fgets(buff,64,stdin);//输入重定向:本来应该从键盘文件中读取的内容,更改为从磁盘文件中读取printf("%s\n", buff);return0;}
1.5. 重定向的本质
- 重定向的本质:更改文件描述符表的内容,即:更改文件描述符(stdin、stdou、stderr)的指向,使得原本要写入到标准输出的数据,被重定向到其他文件、或者原本要从标准输入中读取的数据,重定向到来自于其他文件。
1.5.1. dup2
int dup2(int oldfd,int newfd);
- 功能:将stdin、stdout、stderr重定向到文件或其他设备。
- 参数:oldfd:要被复制的文件描述符;newfd:目标文件的描述符。
- 返回值:成功,返回新的文件描述符(即:newfd)。出错时,返回 -1,并设置 errno以指示错误。
#include<stdio.h>#include<sys/types.h>#include<fcntl.h>#include<unistd.h>#include<sys/stat.h>intmain(){int fd =open("log.txt", O_WRONLY|O_CREAT|O_TRUNC,0666);dup2(fd,1);printf("hello zzx\n");return0;}
2. FILE中的缓冲区
2.1. 概念
- 概念:本质是一块内存区域,用于暂时存放数据,以便更高效地处理输入、输出操作。
💡此处的缓冲区(如:进度条中的缓冲区等),不是内存中的缓冲区,它是语言层面的缓冲区,即:C语言自带的缓冲区,由C语言标准库提供。
- 缓冲区也会为格式化输入、输出操作提高场所。
printf函数工作原理:它会将其他类型的数据(如整数、浮点数等)转换为字符数据(即字符串),转化后的数据会被写入到FILE结构体维护的缓冲区中,根据条件刷新缓冲区。
scanf函数工作原理:scanf会从输入流中读取字符数据,将读取的数据转化为相应的格式化数据,格式化的数据会被存放到FILE结构体维护的缓冲区中,最终被存放到变量中。
2.2. 存在的原因
- 提高使用者的效率
减少了C接口的使用时间,从而减少了用户的等待时间,提高了使用者的效率:调用C接口时,只要将数据交给了缓冲区,就可立即返回,无需等待实际的写入操作完成,意味这用户可以更快地继续执行其他任务。
- 提高计算机整体的拷贝效率。
调用系统调用接口,都是有成本的,有时间和空间的开销。
减少调用系统调用的次数,提高了计算机整体的拷贝效率:缓冲区可以聚集大量数据,直到缓冲区满了,再调用一次系统调用进行实际的数据写入,即:进行一次拷贝。
故事理解:张三给李四送生日礼物,只需要将礼物交给附近的菜鸟驿站,就可立即其他活动,无需亲自送到的李四那,即:提高了使用者的效率。菜鸟驿站不是每次只处理一个包裹,而是收集多个包裹,直到它们填满整个运输车辆,然后再一次性运送到目的地,即:聚集数据,一次拷贝,提高了计算机的整体效率。—— 菜鸟驿站就相当于缓冲区。
2.3. 类型(刷新方案)
一、无缓冲、无刷新
- 无缓冲:无刷新,意味着数据不会暂存在缓冲区中,而是立即被写入到目标设备中。
- 适用场景:需要立即看到结果、实时性要求很高的场景,如:实时系统、设备驱动程序。
- 优点:保证了数据的即时可见性。
- 缺点:性能下降,频繁的使用系统调用会增加开销。
二、全缓冲、全刷新
- 全缓冲:全刷新,缓冲区满了或者关闭文件时,缓冲区的数据才会被刷新到目的设备中。
- 适用场景:文件的读写操作,尤其是大文件。
- 优点:减少了系统调用的次数,提高了性能。
- 缺点:可能会丢失数据,如:在缓冲区的数据未被刷新前,发生崩溃,则这部分的数据就会丢失。
三、行缓冲、行刷新
- 行缓冲:行刷新,意味着遇到换行符\n,缓冲区的数据就会被立即刷新到目的设备中。
- 适用场景:标准输入输出(显示器)。
💡当调用c语言接口fflush(),进行强制刷新; 进程退出时,或文件关闭时,自动刷新。
2.4. 存放的位置
- 缓冲区存放在FILE结构体中,即:缓冲区是被FILE结构来维护的。
- 每个通过标准C库函数打开的文件,都拥有自己的缓冲区。
fwrite等标准库函数,会先将数据拷贝到缓冲区中,然后根据一定的条件,调用系统调用接口进行刷新。
文件操作的系统调用接口,其实是个拷贝函数,它将数据从语言层的缓冲区拷贝到内存的缓冲区。
typedefstruct_IO_FILE FILE;struct_IO_FILE{int _flags;/* High-order word is _IO_MAGIC; rest is flags. */#define_IO_file_flags_flags//缓冲区相关/* The following pointers correspond to the C++ streambuf protocol. *//* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */char* _IO_read_ptr;/* Current read pointer */char* _IO_read_end;/* End of get area. */char* _IO_read_base;/* Start of putback+get area. */char* _IO_write_base;/* Start of put area. */char* _IO_write_ptr;/* Current put pointer. */char* _IO_write_end;/* End of put area. */char* _IO_buf_base;/* Start of reserve area. */char* _IO_buf_end;/* End of reserve area. *//* The following fields are used to support backing up and undo. */char*_IO_save_base;/* Pointer to start of non-current get area. */char*_IO_backup_base;/* Pointer to first valid character of backup area */char*_IO_save_end;/* Pointer to end of non-current get area. */struct_IO_marker*_markers;struct_IO_FILE*_chain;int _fileno;//封装的文件描述符#if0int _blksize;#elseint _flags2;#endif
_IO_off_t _old_offset;/* This used to be _offset but it's too small. */#define__HAVE_COLUMN/* temporary *//* 1+column number of pbase(); 0 is unknown. */unsignedshort _cur_column;signedchar _vtable_offset;char _shortbuf[1];/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;#ifdef_IO_USE_OLD_IO_FILE};
2.4.1. 代码证明、现象解释
#include<stdio.h>#include<unistd.h>#include<string.h>intmain(){constchar* s1 ="hello write\n";write(1, s1,strlen(s1));//调用系统调用,直接将数据写入到内核中//fprintf、fwrite为库函数,向显示器进行写入,行刷新(遇到换行符)constchar* s2 ="hello fprintf\n";fprintf(stdout,"%s", s2);constchar* s3 ="hello fwrite\n";fwrite(s3,strlen(s3),1,stdout);fork();//在创建子进程之前,缓冲区中的数据全部被刷新到内核中了return0;}
现象1解释:write()为系统调用接口,直接将数据写入到内核中;fprintf、fwrite为库函数,先将数据写入到缓冲区中,因为它们都是向显示器进行写入,而写入显示器是行刷新(遇到换行符\n,进行刷新),所以fork创建子进程前缓冲区中的数据全部被刷新到内核中了。
Tips:刷新到内核的数据,不属于进程的数据;存放在缓冲区中的数据,属于进程的数据。
现象2解释:重定向到普通文件时,数据刷新缓冲区的方式,由行缓存变为全缓冲,C语言接口自带缓冲区,所以它会将数据写入到缓冲区中,就不会立即刷新。fork创建子进程,父子共享缓冲区的数据,但是进程退出后,统一进行刷新。刷新缓冲区,是清空缓冲区,是修改数据的一种方式,所以父子进程的数据会发生写时拷贝,父子进程分别刷新各自的缓冲区,随即产生两份数据。write是系统调用接口,直接将数据写入到内核中,不存在所谓的缓冲区。
- 一般C库函数写入文件时,是全缓冲; 写入到显示器时,是行缓冲。
- 重定向到普通文件时,数据刷新缓冲区的方式,由行缓存变为全缓冲。
- 刷新缓冲区,是清空缓冲区,是修改数据的一种方式。
2.5. 模拟C标准库中的方法
#pragmaonce //防止头文件重复包含#include<stdio.h>#include<unistd.h>#include<sys/types.h>#include<sys/stat.h>#include<fcntl.h>#include<string.h>#include<stdlib.h>#defineSIZE4094//定义缓冲区的类型#defineNone_Flush1#defineFull_Flush(1<<1)#defineLine_Flush(1<<2)//自定义file结构体typedefstructmyfile{int fileno;//文件描述符int pos;//当前读写位置int cap;//缓冲区容量int flush_mode;//缓冲区类型char buff[SIZE];//输出缓冲区}myfile;
myfile*my_fopen(constchar* path,constchar* mode);//打开文件voidmy_fclose(myfile* fp);//关闭文件intmy_fwrite(myfile* fp,constchar* str,int size);//读文件voidmy_fflush(myfile* fp);//刷新缓冲区 voidprint_buff(myfile* fp);//打印file结构体的内容,便于测试
#include"mystdio.h"constchar*To_string(int flush_mode)//将整形转化为字符串{if(flush_mode & None_Flush)return"Nono Flush";elseif(flush_mode & Line_Flush)return"Line_Flush";elseif(flush_mode & Full_Flush)return"Full_Flush";}voidprint_buff(myfile* fp)//打印file结构体的内容{printf("fd: %d\n", fp->fileno);printf("fd: %d\n", fp->pos);printf("buff: %s\n", fp->buff);printf("flush_mode: %s\n",To_string(fp->flush_mode));}
myfile*my_fopen(constchar* path,constchar* mode)//打开文件{int flag =-1;//确认它是以何种方式打开文件if(strcmp(mode,"r")==0)
flag = O_RDONLY;elseif(strcmp(mode,"w")==0)
flag = O_WRONLY|O_CREAT|O_TRUNC;elseif(strcmp(mode,"a")==0)
flag = O_WRONLY|O_CREAT|O_APPEND;elsereturnNULL;//底层调用系统调用接口open打开文件int fd =-1;if(flag & O_RDONLY)//读不需要创建新的文件
fd =open(path, flag);else//写、追加,都需要创建新的文件,并且需要设置文件权限{umask(0); fd =open(path, flag,0666);}if(fd <0)//调用open失败returnNULL;//为打开的文件创建一个file类型的结构体,用来记录描述打开文件的信息
myfile* fp =(myfile*)malloc(sizeof(myfile));if(fp ==NULL)//malloc调用1失败returnNULL;//file结构体对象构建成功,进行初始化
fp->fileno = fd;
fp->pos =0;
fp->cap = SIZE;
fp->flush_mode = Line_Flush;return fp;}voidmy_fflush(myfile* fp)//刷新缓冲区{if(fp->pos ==0)return;//底层调用系统调用接口writewrite(fp->fileno, fp->buff, fp->pos);//清空缓冲区的内容
fp->pos =0;}voidmy_fclose(myfile* fp)//关闭文件{my_fflush(fp);//文件关闭,自动刷新缓冲区close(fp->fileno);free(fp);}intmy_fwrite(myfile* fp,constchar* str,int size)//向文件写内容{//将数据先拷贝到用户层的缓冲区内memcpy(fp->buff + fp->pos, str, size);
fp->pos += size;//判断是否需要刷新if(fp->flush_mode == Line_Flush && fp->buff[fp->pos -1]=='\n')//行刷新my_fflush(fp);elseif(fp->flush_mode == Full_Flush && fp->pos == fp->cap)//全刷新my_fflush(fp);return0;}
intmain(){
myfile* fp =my_fopen("data.txt","w");if(fp ==NULL)return1;char buf[SIZE];int cnt =5;while(cnt--){snprintf(buf, SIZE,"helloworld: %d :", cnt);//字符串的拼接my_fwrite(fp, buf,strlen(buf));print_buff(fp);sleep(1);}my_fclose(fp);return0;}
版权归原作者 奶芙c 所有, 如有侵权,请联系我们删除。