目录
1.基础IO
2.用户缓冲区
1.基础IO
在谈基础io之前,首先要来对文件要有一个基本认识:
共识原理:
1.文件=内容+属性;
2.文件分为打开的文件和没打开的文件
3.文件由进程打开,必须先被加载到内存
4.没打开的文件保存在磁盘上,打开的文件在内存中,OS先描述再组织管理多个文件,结构体为文件控制块
我们可以先来回忆一下C语言文件操作接口:
void test1()
{//打开文件的路径和文件名,默认在当前路径下新建一个文件//这个路径是如何获取的?当前路径就是进程的路径,保存在/proc/cwd中//如果我更改了当前路径,就可以改变文件的存储地点FILE* fp=fopen("log.txt","wb");//w打开文件,没有就创建,从头开始写入,清空之前所有内容,类似>//a打开文件,追加写入,类似>>if(fp==NULL){perror("fopen failed");exit(1);}const char* message="abcd";fwrite(message,sizeof(char)*(strlen(message)),1,fp);//这里注意不要strlen+1算上了\0,文件写入仅仅针对字节,\0写入变成乱码,没有意义fclose(fp);
}
当前路径就是进程的当前路径,保存在/proc/cwd中,如果没有指明路径,文件默认保存在这个路径下,w:写入,没有文件就创建,对文件进行清空;a: 写入,没有文件就创建,对文件进行追加写入。
程序启动默认打开三个流,C语言stdin(键盘文件)、stdout(显示器文件)、stderr(显示器文件),C++cin、cout、cerr
因为文件在磁盘上属于外部数据,程序不能直接获得文件数据,只能通过系统调用;常见的fwrite,fread,fscanf,fprintf都是库函数,上面是C语言中提供的关于文件操作的函数,下面来看看Linux系统调用:调用接口有open,write,close
- open:

参数一文件名,默认路径为当前路径;参数二提供这些选项:
O_RDONLY:以只读模式打开文件,
O_WRONLY:以只写模式打开文件,
O_RDWR:以读写模式打开文件,
与C++文件操作类似,多选使用按位或|,如果要实现C语言w模式,需要|O_CREAT|O_TRUNC,没有文件去创建以及清空已有内容,如果要实现C语言a模式,需要|O_CREAT|O_APPEND,没有文件去创建以及追加到已有内容
参数三权限设置0xxx,临时更改当前进程的权限掩码umask(0);
- read:

参数一文件描述符,参数二地址,参数三读取字节数;
- write:

参数一文件描述符,参数二地址,参数三写入字节数;
- close:
参数一文件描述符,传入fd即可;
void test2()
{//来看看open的系统调用//open的接口有两个,第一个接口不能指定权限,创建文件权限是随机值//第二个可以指定权限,但要注意权限掩码默认为0002//int fd=open("open.txt",O_WRONLY|O_CREAT|O_TRUNC);//注意,这样调用的open权限是随机值,如果要指定随机值,那么请调用第二种open函数umask(0);//在当前进程中临时改变权限掩码,可以解决下面的问题//int fd1=open("open.txt",O_WRONLY|O_CREAT|O_TRUNC,0666);//但是这样不会完全遵循0666,因为还存在着权限掩码的问题//close(fd1);int fd2=open("open.txt",O_WRONLY|O_CREAT|O_APPEND,0666);//但是这样不会完全遵循0666,因为还存在着权限掩码的问题if(fd2<0) perror("open failed\n");const char* str="ABCDEFGHIJKLMN";write(fd2,str,sizeof(str));write(fd2,"6666",4);//如果没有选择O_TRUNC就会导致写入没有清空,如果要追加,就带上O_APPEND
}
但是open返回值的作用并不理解,为什么是一个整形?
访问文件的本质:文件从磁盘中加载到内存中,先描述再组织,描述为file_struct,用双向链表组织,file_struct中保存的有文件的地址、基本属性权限大小读写位置等等,但是一个进程可能打开多个文件,怎么区分?其实PCB中有struct files_struct* file;file变量保存的是结构体地址,什么结构体?files_struct,其中保存的有struct file*类型的数组指针,数组中保存的是这个进程打开文件的file_struct地址,open返回的是对应数组下标,也就是文件描述符
可以来看看struct files_struct 以及 struct file的结构内容:
struct files_struct {atomic_t count; // 引用计数(用来关闭文件)bool resize_in_progress; // 是否正在调整大小wait_queue_head_t resize_wait; // 调整大小时的等待队列struct fdtable __rcu *fdt; // 当前文件描述符表struct fdtable fdtab; // 初始文件描述符表/* 用于执行时关闭的位图 */unsigned long close_on_exec_init[1];unsigned long open_fds_init[1];/* 用于执行时关闭的位图指针 */unsigned long *close_on_exec;unsigned long *open_fds;/* 最大文件描述符数量 */unsigned long max_fds;/* 位图的大小(以字为单位) */unsigned long max_fdset;/* 当前文件描述符的最大索引 */unsigned long next_fd;/* 用于共享检查的保护锁 */spinlock_t file_lock ____cacheline_aligned_in_smp;/* 执行 exec() 时关闭的文件描述符的列表 */struct list_head fdt_list;/* 支持非阻塞操作的文件描述符列表 */struct file __rcu **fd_array;
};struct file {union {struct llist_node fu_llist;struct rcu_head fu_rcuhead;} f_u;struct path f_path; /* 文件路径信息 */struct inode *f_inode; /* 关联的 inode */const struct file_operations *f_op; /* 文件操作函数集 *//* 保护 f_ep_links, f_flags. */spinlock_t f_lock;enum rw_hint f_write_hint;atomic_long_t f_count; /* 引用计数 */unsigned int f_flags; /* 文件标志 (O_RDONLY 等) */fmode_t f_mode; /* 文件模式 (读/写权限) */struct mutex f_pos_lock; /* 位置锁 */loff_t f_pos; /* 文件偏移量 (当前位置) */struct fown_struct f_owner; /* 进程所有权信息 */const struct cred *f_cred; /* 凭证信息 (权限) */struct file_ra_state f_ra; /* 预读状态 */unsigned long f_version; /* 版本号,用于检测变更 */#ifdef CONFIG_SECURITYvoid *f_security; /* 安全模块数据 */#endif/* 对于省内存的系统,删除以下成员 */#ifdef !CONFIG_SMALL_MEMORYvoid *private_data; /* 驱动私有数据 */#endif /* !CONFIG_SMALL_MEMORY */#ifdef CONFIG_EPOLL/* 用于 epoll 事件轮询 */struct list_head f_ep_links;struct list_head f_tfile_llink;#endif /* CONFIG_EPOLL *//* 预分配的辅助数据,可由文件系统使用 */struct address_space *f_mapping;/* 用于 NFS 等网络文件系统的锁 */#ifdef CONFIG_FSNOTIFYstruct fsnotify_mark_entry __rcu *f_fsnotify_marks;
#endif};
但是在你打开自己的文件之前操作系统已经打开了三个文件,下标0,1,2对应C语言中stdin,stdout,stderr三个文件,编程时你需要输入输出,这三个流(文件)打开是必然的,那C语言中的FILE结构体与file_struct有联系吗?
有,但不是同一个东西,FILE是C库自己封装的结构体,其中封装了文件描述符(_fileno),以及属于这一个文件的用户缓冲区,用来存放对这个文件进行C语言文件操作的数据,
拓展一下:
文件描述符的分配规则:从0下标开始,寻找最小的没有使用的数组位置,作为新文件的文件描述符
除了文件的打开和关闭,还有一个重要的内容是文件重定向:
先来介绍一下系统调用接口:

重定向:常用的只有dup2(int oldfd,int newfd)old要改变的文件描述符,new要打开的文件描述符,这个函数本质上是将oldfd下标对应的地址浅拷贝到了newfd下标内,完成了重定向,命令行中>/>>也是这样根据重定向实现的,这时对原来的newfd的读写就变成了对oldfd的读写
void test1()
{close(0);int pd=open(filename,O_WRONLY|O_CREAT|O_TRUNC,0666);const char* input="The file is opened in append mode. Before each write(2), the file offset is positioned at the end of the file, as if with lseek(2).";printf("文件描述符为:%d\n",pd);//这里测试说明,文件描述符的设置是按照线性遍历的,//哪个描述符空闲就使用哪个
}void test2()
{//自己来模拟一个重定向close(1);int pd=open(filename,O_WRONLY|O_CREAT|O_APPEND,0666);const char* input="The file is opened in append mode. Before each write(2), the file offset is positioned at the end of the file, as if with lseek(2).";write(pd,input,sizeof(char)*strlen(input));//这里可以看到原本要向stdout的文字写入了文件,这种改变了输入输出目的地就叫做重定向}void test3()
{//来介绍一下重定向的系统调用把!dup2int pd=open(filename,O_WRONLY|O_CREAT|O_APPEND,0666);if(pd<0) perror("open failed");const char* input="The file is opened in append mode. Before each write(2), the file offset is positioned at the end of the file, as if with lseek(2).\n";//这里重定向可以使用dup2(原来的fd,要打开的fd)//本质上是将oldfd的file_struct的地址拷贝到了newfd的位置dup2(pd,1);write(1,input,sizeof(char)*strlen(input));//这是输出重定向
}
重定向在命令行Bash中的运用:
#命令行:
./a.out 1>normal.txt 2>err.txt
#意思是将stdout重定向为nor,stderr重定向为err./a.out 1>normal.txt 2>&1
#意思是将stdout重定向为nor,stderr重定向为修改过的1(nor)
如果有外设接入,与文件系统又有哪些联系?
一切皆文件:外设先描述为struct device结构体,包含读写方法函数,再创建文件操作方法集:struct file_operations封装读写操作函数指针,再创建struct_file结构体,包含方法集指针,形成虚拟文件系统(virtual files system)
2.用户缓冲区
我们在学习语言的时候就了解到存在缓冲区这一概念:C中的库函数 printf,fprintf,fwrite 在不刷新缓冲区的前提下(不使用\n or fflush())都会先在缓冲区保存,(这个缓冲区是C语言的缓冲区,在内核之上),而系统调用 write函数时会写入内核缓冲区,调用 close 时,内核会强制将文件描述符对应的所有的内容从内核缓冲区刷新到磁盘,缓冲区存在多种刷新方式:
缓冲区的刷新分为 用户缓冲区(用户空间) 和 内核缓冲区(内核空间) 两个层面,两者的刷新触发条件和机制不同,在此先介绍用户缓冲区:
用户缓冲区(用户空间)的刷新方式:
用户缓冲区由应用程序或编程语言的标准库(如 C 的 stdio)管理,用于暂存待写入内核的数据(或暂存已读取但未被应用处理的数据)。其刷新(即数据从用户缓冲区传递到内核)的触发方式主要有以下几类:
1. 自动刷新(标准库自动触发)
标准库会根据缓冲区类型(全缓冲、行缓冲、无缓冲)自动决定何时刷新:
全缓冲:缓冲区填满时自动刷新。典型场景:对普通文件的写入(如 fopen 打开的文件)
行缓冲:遇到换行符(\n)时自动刷新。典型场景:终端输出(如 stdout)
无缓冲:数据直接传递给内核,无缓冲。典型场景:错误输出(如 stderr),或者使用fflush()
注:C缓冲区通过write系统调用写入到内核缓冲区,进程退出的时候也会刷新user-space
2. 手动刷新(应用程序主动触发)
调用 fflush 函数:显式刷新指定流的用户缓冲区。
程序正常终止(如 exit):调用 exit 时,标准库会遍历所有打开的流,调用 fflush 刷新用户缓冲区(_exit 不会触发此操作)。
void test1()
{const char* input1="hello fwrite";const char* input2="hello write";printf("hello printf\n");fprintf(stdout,"hello fprintf\n");fwrite(input1,strlen(input1),1,stdout);//以上是C语言提供的库函数,数据会与用户缓冲区接触!write(1,input2,strlen(input2));//没有必要+1哦!//这里来实验不同的缓冲方式,以及重定向和子进程创建对缓冲区的影响close(1);//这里需要了解的是,\n是行缓冲,通过write写入到内存缓冲区,//而重定向到文件之后,就会变成全缓冲,缓冲区满才会write写入内存。//write直接写入内核,close只会将内核数据刷新到磁盘,不管user-space
}
用户缓冲区存在的意义:
1.提高效率,操作系统的系统调用(如 write/read)是高成本操作:每次系统调用需要进行用户态与内核态的上下文切换(Context Switch),涉及寄存器保存、内存地址空间切换等操作,若应用程序每次写入少量数据(如 1 字节)就调用一次 write,会导致大量 CPU 资源消耗在上下文切换上,而非实际数据处理,通过用户缓冲区暂存数据,将多次小规模写入合并为一次大规模系统调用,提到了语言效率。
2.配合格式化,什么是格式化?格式化函数(如 C 的 printf)的核心功能是:将程序中的结构化数据(如整数、浮点数、字符串)转换为符合特定格式的字符流(如 %d → 十进制字符串,%f → 带小数点的字符串)。
void test2()
{//注意最后的return 0会刷新user-space,将数据通过内核写入到磁盘const char* input1="hello fwrite";const char* input2="hello write";printf("hello printf\n");fprintf(stdout,"hello fprintf\n");fwrite(input1,strlen(input1),1,stdout);write(1,input2,strlen(input2));//这里来实验不同的缓冲方式,以及重定向和子进程创建对缓冲区的影响fork();//这里你需要知道的是,fork创建子进程后对代码数据写时拷贝,user-space中的数据也一并复制,父子进程重定向到文件就会输出两段重复数据
}

进行重定向或者创建子进程时缓冲区发生的事:
1.重定向到文件,缓冲区刷新方式由行刷新→全刷新,其他视情况而定
2.创建子进程,代码数据发生写时拷贝(包括用户缓冲区),如果采用全缓冲写入到文件,最后进程结束文件中会有父子进程缓冲区的内容(*double)