文件 FD
向硬件中写入文件本质需要通过系统,向系统中写入文件本质就是在调用系统提供的相应的接口间接来进行写入,用户没有权利直接写入
文件的打开和关闭,本质上是 CPU 在执行代码,通过进程来打开和关闭文件,一个进程可以打开多个文件,而系统中有大量的进程,因此也可能会有大量被打开的文件,因此需要类似于进程 PCB 一样的描述文件属性的结构体来对这些文件进行组织
C 语言中的文件操作
FILE* fopen("test.txt","w");//以写入方式打开test.txt文件,如果文件不存在,则创建文件
//如果存在,则清空文件,然后写入
可以通过>来将左边的内容插入的文件中,和以写入方式打开文件类似,都是能够创建和清空文件的
echo "abc">test.txt
open 函数:
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
int creat(const char *pathname, mode_t mode);
//成功则返回文件描述符,失败返回-1
open 的本质工作
- 创建 struct_file
- 开辟文件缓冲区的空间,加载文件数据
- 查找文件描述符表,找到当前没有被使用的最小的一个下标,作为新的文件描述符
- file 地址填入对应的表下标中
- 返回下标
flags 本质是一个位图,可以通过 | 操作进行增加,如:
int fp=open("./log.txt",O_WRONLY|O_CREAT);
这里的 O_WRONLY 和 O_CREAT 分别表示以方式进行打开和如果文件不存在则进行创建
还有其他的 flag 如下:
O_RDONLY:以读方式打开
O_WRONLY:以写方式打开
O_RDWR:以读写的方式打开
O_TRUNC:若文件中原来有内容,则将其清空
O_APPEND:以追加的方式进行打开,不清空内容
mode 是指文件的权限,以数字的方式进行修改,如 0777,但是系统中有个默认的 umask,会与 0777 进行与操作,导致想要的修改与实际不一致,因此可以在程序中修改 umask,方式如下:
mode_t umask(mode_t mask);
通过修改 mask 的值来进行修改 umask
write 函数:
ssize_t write(int fd, const void *buf, size_t count)
//fd即原来open的文件,buf即缓冲区的内容,count为要存入的个数例:
const char* t="11fasd6\n";write(fp,t,strlen(t));
无论读写,都必须在合适的时候让操作系统把文件的内容读取到文件缓冲区中
读写函数的本质就是拷贝
read 函数:将数据从内核缓冲区拷贝到用户缓冲区
- 系统调用:用户空间的read函数调用会触发一个系统调用,将控制权转移到操作系统内核。
- 文件描述符验证:内核会验证提供的文件描述符是否有效,并确定它指向哪个文件或设备。
- 权限检查:内核会检查当前进程是否有权限从指定的文件或设备读取数据。
- 数据读取:如果权限检查通过,内核会从文件系统的内部数据结构(如inode)或设备驱动程序中读取数据。这些数据首先被加载到内核的一个临时缓冲区中。
- 数据拷贝:然后,内核将这个缓冲区中的数据拷贝到用户提供的缓冲区中。这一步是读取操作的核心,但它并不是将数据从文件缓冲区拷贝到内核缓冲区,而是从内核缓冲区(或文件系统/设备驱动程序的内部缓冲区)拷贝到用户缓冲区。
- 更新文件偏移量:对于常规文件,读取操作通常会导致文件的当前偏移量(文件指针)向前移动读取的字节数。
- 返回结果:最后,内核会将实际读取的字节数返回给用户空间。如果读取过程中发生错误,系统调用将失败,并返回一个错误代码。
write 函数:将数据从用户缓冲区拷贝到内核缓冲区
- 系统调用:用户空间的write函数调用会触发一个系统调用,将控制权转移到操作系统内核。
- 数据拷贝:内核会首先将用户缓冲区中的数据拷贝到内核缓冲区中。这一步是write函数操作的核心部分。
- 写入操作:然后,内核会根据文件描述符找到对应的文件或设备,并将内核缓冲区中的数据写入到其中。
- 更新文件偏移量:对于常规文件,写入操作通常会导致文件的当前偏移量(文件指针)向前移动写入的字节数。
- 返回结果:最后,内核会将实际写入的字节数返回给用户空间。如果写入过程中发生错误,系统调用将失败,并返回一个错误代码。
文件描述符
Linux进程默认情况下会有3个缺省打开的文件描述符,分别是标准输入0, 标准输出1, 标准错误2.
0,1,2对应的物理设备一般是:键盘,显示器,显示器,因此文件描述符是从 3 开始的
每个进程都有一张表,来指向对应的 file_struct
对文件的修改是需要先导入到内存中,然后才能刷新到磁盘里
文件描述符本质上是一个结构体数组中文件映射关系的数组下标
通过找到各个硬件设备的 struct_file 来找到各自的输入输出方法,这个方法是存放在各个硬件设备的驱动程序中的,每个 struct_file 里面存放着一个指针用于寻找
文件的 struct_file 中都会有其对应的操作方法,当使用不同的下标时,将会使用不同的方法,类似于多态,每一个设备都有对应的 struct_file,通过下标来进行查找
open 系统调用通过路径和文件名得到对应的文件,然后内核会为该文件创建对应的 stuct_file,然后找到当前没有被使用的最小的一个下标,作为新的文件描述符,将其指向新的 stuct_file 对象,然后后续操作就可以通过文件 fd 来进行了,因此如果将 0 关闭了,然后打开一个新文件,那么这个新文件就会被分配为 0
struct_file 中存放着 read 和 write 方法,分别指向各自文件自己提供的 read 和 write 方法,当进程调用 write和 read 时,通过传入文件 fd 找到对应的 struct_file,然后 struct_file 调用自己的 read 和 write,read 和 write 再指向驱动程序的 read 和 write,由于 Linux 中一切皆文件,因此可以通过驱动程序操作设备,实现对所有设备和其他文件的操作
虚拟文件系统 VFS
struct_file 就是一个虚拟文件系统,为各种文件系统提供了统一的接口
作用:
- 屏蔽底层差异:VFS屏蔽了不同文件系统之间的差异,为上层应用程序提供了一个统一的接口,使得应用程序能够以一致的方式访问各种不同类型的文件系统。
- 提供公共功能:在VFS中实现了一些公共的功能,如inode缓存、页缓存等,这些功能提高了文件系统的性能和效率。
- 支持多种文件系统:VFS支持多种文件系统类型,包括本地文件系统(如ext4、XFS等)、网络文件系统(如NFS)以及特殊文件系统(如procfs、sysfs等)。
重定向 _ 缓冲区
重定向:
重定向的本质就是在内核中改变文件描述符下标位置,和上层无关
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
int stat(const char* path,struct stat *buf);//通过指定路径获取stat结构体
int fstat(int fd,struct stat *buf)//通过文件描述符获取文件的属性
int lstat(const char*path,struct stat *buf);
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include<unistd.h>
#include <stdlib.h>
int main()
{close(1);//标准输出的文件描述符为1,这里将其关闭了,因此执行后续操作时,操作系统会将1,//即最小的可分配的文件描述符分配给myfile文件,//后续的printf默认会将内容输出到1,而1已经不是标准输出了//因此会直接输入到myfile文件中int fd = open("myfile", O_WRONLY|O_CREAT, 0644);if(fd < 0){perror("open");return 1;}printf("fd: %d\n", fd);fprintf(stdout,"fprintf,%d\n",fd);fflush(stdout);//fflush将内容刷新到语言的缓冲区,//正常结束时操作系统会将缓冲区中的内容刷新到内核//但是这里因为提前使用close将文件关闭了,//因此操作系统刷新时无法将内容刷新到文件中,//因而如果不加上fflush文件中就会没有内容close(fd);//也可以将close去掉,让程序结束后操作系统自己将语言缓冲区中的内容刷新到文件中exit(0);
}
stat 函数用于填充 struct stat 结构体
struct stat
结构体包含了文件的多种属性,包括但不限于:
- 文件类型(如普通文件、目录、符号链接等)
- 文件大小(以字节为单位)
- 文件权限(读、写、执行权限,以及特殊权限位如SUID、SGID和粘滞位)
- 文件的硬链接数
- 文件的所有者和所属组
- 文件的最后访问时间、最后修改时间和最后状态改变时间(通常表示为自Epoch(1970年1月1日)以来的秒数)
- 文件所在的设备ID和inode号
close 函数是一个系统调用,用于将文件描述符关闭
- 在调用
fork
函数后,父进程和子进程将共享打开的文件描述符。但是,每个进程都有自己独立的文件描述符表,因此关闭一个进程中的文件描述符不会影响另一个进程中的相应文件描述符。
重定向是本质就是更改 struct_file[] 的下标,让指针更改指向位置,将本来要输出的位置更改到另一个位置,如将本来要输出到显示器文件的内容输出到硬盘文件中
dup2
可以通过 dup2 系统调用来进行重定向
#include <unistd.h>
int dup(int oldfd);
int dup2(int oldfd, int newfd);
当调用 dup 函数时,内核在进程中创建一个新的文件描述符,此描述符是当前可用文件描述符的最小数值,这个文件描述符指向 oldfd 所拥有的文件表项。
dup2 函数是将 oldfd 里的参数拷贝给 newfd,最后两个都变成 oldfd,让 new 成为 old 的一份拷贝,把本应该在 newfd 打开的文件在 oldfd 打开
dup2 和 dup 的区别就是可以用newfd参数指定新描述符的数值,如果 newfd 已经打开,则先将其关闭。如果 newfd 等于 oldfd,则 dup2 返回 newfd, 而不关闭它。dup2 函数返回的新文件描述符同样与参数 oldfd 共享同一文件表项
void test_dup()
{int fd=open("test.txt",O_CREAT|O_WRONLY|O_APPEND,0666);if(fd==NULL){return -1;}dup2(fd,1);//把输入到1中的内容重定向到fd中printf("test111");fflush(stdout);close(fd);
}
缓冲区
当 fprint 向外输出时,会先输出到语言层面的缓冲区 stdout 中,要通过 flush 才能将语言中的缓冲区的内容刷新到内核的缓冲区,从而输出到外设中
缓冲区能够解耦和提高效率
解耦
- 异步处理(时间解耦):生产者将数据存入缓冲区后即可继续后续操作,无需等待消费者处理完毕;消费者则按自身节奏从缓冲区获取数据,双方无需同步时间,独立运行
- 流量控制(速率解耦):缓冲区平滑处理速率差异。当生产者速度 > 消费者速度时,数据暂存于缓冲区;反之,消费者从缓冲区获取历史数据,避免了系统因瞬时负载崩溃
- 并发与空间解耦:多生产者/消费者可并发操作缓冲区(需线程安全),彼此无需感知对方的存在,扩展性强,组件可独立增减
- 错误隔离:缓冲区作为中间层,隔离生产者和消费者的故障,提升了系统容错性,局部故障不影响整体运行
- 接口简化:生产者和消费者仅需与缓冲区交互(如
push
/pop
操作),无需了解对方实现细节,降低组件间耦合度,便于独立升级或替换
提高效率
- 批量处理降低开销:缓冲区本质就是一段内存空间,能够将许多个小的请求合并成一个较大的请求,以减少 I/O 的访问次数,从而提高刷新 IO 效率,间接提高整体效率
- 利用内存等高速存储暂存数据,替代直接访问低速设备
刷新策略
- 立即刷新:有文件进入就刷新,虽然能够确保数据不会丢失,但是性能差,会频繁访问 I/O
- 行刷新:当数据流中检测到换行符(如
\n
)时,立即将当前缓冲区的内容刷新到目标位置(如文件、网络、屏幕等) - 全缓冲:缓冲区满或显式调用
flush()
-
- stdout:行缓冲,数据在换行时才输出
- stderr:无缓冲,信息会立即显示
void test_flush()
{int fd=open("test",O_CREAT|O_WRONLY|O_APPEND,0666);dup2(fd,1);printf("printf\n");fprintf(stdout,"fprintf\n");const char* msg="write\n";write(1,msg,strlen(msg));fork();//由于print和fprintf都是语言层面的,因此打印时不会直接打印到文件中,而是进入到缓冲区//但是write是系统调用,因此会直接在调用完成后立即将数据输出到文件中//在fork后,父子进程都会进行一次退出,因此都会刷新一次缓冲区,//进而文件中会有两个,printf和fprintf,而write只有一个
}
特殊情况:
进程退出自动刷新
强制刷新
“>”: 标准输出重定向更改 1 号 fd 中的内容
“>>”:追加重定向符,将命令的输出追加到指定文件的末尾,而不是覆盖文件内容。
“<”:输入重定向,将文件内容作为命令的标准输入(而非通过键盘输入)
“2>”:错误重定向符,将命令执行过程中的错误信息输出到指定文件。
“&>”:合并重定向符,将两个文件描述符指向的内容重定向到同一个文件中
进程的程序替换,不会影响进程关联或打开的文件
原因:进程的替换只会替换进程的代码和数据,不会修改进程的文件描述符
标准错误 2 stderr 就是打印错误信息,本质是向 2 打印,而标准输出是向 1 打印
ls 1>ok.log 2>err.log
这个实质上是让1指向ok.log,2指向err.log
2>&1
这个重定向其实是把1中的内容放到2里面,这样2就指向1的文件了,然后,1也指向自己的文件
这样就实现了把两个文件描述符指向的内容重定向到同一个文件中了
c++中打印错误信息通过 cerr 进行,用法和 cout 一样