目录
1.理解“文件”
1.1狭义理解
1.文件在磁盘里。
2.磁盘是永久性存储介质,因此文件在磁盘上的存储是永久性的。
3.磁盘是外设(即是输出设备也是输入设备)。
4.磁盘上的文件 本质是对文件的所有操作,都是对外设的输入和输出 简称IO。
1.2广义理解
Linux下一切皆文件(键盘,显示器,网卡,磁盘.....)
1.3文件操作的归类认知
1.对于0KB的空文件是占用磁盘空间的。
2.文件时文件属性(元数据)和文件内容的集合(文件=属性(元数据)+内容)。
3.所有的文件操作本质是文件内容操作和文件属性操作。
1.4系统角度
1.对文件的操作本质是 进程对文件的操作
2.磁盘的管理者是操作系统
3.文件的读写本质不是通过C语言/C++的库函数来操作的(这些库函数只是为用户提供方便),而是通过文件相关的系统调用接口来实现的。
2.回顾C文件接口
C语言文件操作函数汇总:

2.1打开文件

打开的myfile文件在当前路径下,那系统如何知道程序的当前路径?
通过此命令查看当前正在运行进程的信息
ls /proc/[进程id] -l

cwd:指向当前进程运行目录的一个符号链接
exe:指向启动当前进程的可执行文件(完整路径)的符号链接。
当前路径
所以说,当前路径并不是指我们可执行程序所处的路径,而是指 当这个可执行程序运行成为进程时,我们所处的路径,文件就默认创建在这个路径。(比如说你在可执行程序的上一级目录启动这个可执行文件,文件创建在可执行程序的上一级目录,而不是可执行程序那里)
打开文件,本质是进程打开,所以,进程知道自己在何处,即便文件不带路径,进程也知道。
由此OS也就能知道要创建的文件放在哪。
此外,如果文件已经存在,w方式打开文件会先清空文件内容。

那么如果未来想要清空文件,只需要写一段让文件w方式打开再关闭的代码就可以实现。
例如:main函数加上参数int argc,int *argv[]
当argc==2时,fopen(argv[1],"w");判断打开成功与否后直接fclose
这样我们在命令行输入 ./代码文件 文件名,就能将文件内容清空了
以a方式打开,不作清空,在文件结尾进行追加

其余打开方式总览

echo时以>写入指定文件,是以w方式打开
以>>写入指定文件,是以a方式打开
2.2写文件


2.3读文件


如此我们可以实现一个cat


2.4 stdin&&stdout&&stderr
输出信息到显示屏,你有哪些方法?
printf输出:
ps:向显示器写入12345或者给键盘输入12345,看上去是一个int类型,但输入的其实是1,2,3,4,5char类型,所以显示器和键盘也被叫做字符设备,printf("%d",x)为什么要有占位符,就是为了将输入的整型(比如12345 转换成 1,2,3,4,5 比如3.14转换成3,.,1,4,本质是通过查找ASCII码表转换)格式化输出,这也是为什么printf叫做格式化输出的原因。像这种需要格式化的都是文本文件,二进制文件就不需要格式化。这种区别是由文件本身属性导致的,不同属性的文件调用不同的函数接口,二进制就用fread,fwrite,文本就用fgets,fputs。
标准输入,输出,错误 流:

Linux中的一切东西都可以看作文件,显示器和键盘也可以看作文件。我们能看到显示屏上的数据,是因为我们向“显示屏文件”写入了数据,电脑能获取我们键盘上敲击的字符,也是因为电脑从“键盘文件”中读取了数据。
那么为什么我们向“显示屏文件”写入数据或者从“键盘文件”中读取数据,不需要我们先打开对应文件呢?值得注意的是,打开文件一定是进程运行时打开的,而任何进程在启动时会默认打开三个流:标准输入流,标准输出流,标准错误流。对应到C语言当中就是stdin,stdout,stderr,第一个对应“键盘”文件,后俩个对应“显示器文件”。
通过man手册也可以知道这三个流是FILE*类型的,也就是说stdin,stdout,stderr和我们自己打开文件得到的文件指针是同一概念。

当我们C程序运行起来时,操作系统就会默认使用C语言的相关接口将这三个流文件打开,然后我们就能使用 scanf/printf 函数对 键盘/显示器 进行输入输出操作。
ps:标准输入流,标准输出流,标准错误流,在C中是stdin,stdout,stderr,在C++中是cin,cout,cerr,在其他语言中都有类似概念,这种特性由操作系统支持,并不是语言特有的。

3.系统文件I/O
打开文件的方式不仅仅是fopen,ifstream等流式,语言层的方案。其实系统才是打开文件最底层的方案。语言的库函数只是对系统调用接口进行了封装。
3.1open
系统调用接口中使用open函数打开文件,open的函数原型如下:
int open(const char *pathname, int flags, mode_t mode);
参数介绍:
第一个参数:表示要打开或者创建的目标文件
- 若pathname以路径的方式给出,则当需要创建该文件时,就在pathname路径下进行创建。
- 若pathname以文件名的方式给出,则当需要创建该文件时,默认在当前路径下进行创建。(注意当前路径的含义)
第二个参数:表示打开文件的方式
常用选项如下,
如果要传入多个参数,参数间以“ | ”间隔开,比如要以写的方式打开文件,但要创建这个文件
O_WRONLY | O_CREAT

PS:系统接口open的第二个参数flags其实就是整型,所谓的参数选项不过是宏定义,flags共有32个比特位(标志位),特点是32个标志位只有一个标志位是1(除了O_RDONLY选项全是0,且为默认选项),如此一来,在open函数内部就能通过“与”运算判断是否设置了某一选项

int open(arg1, arg2, arg3){
if (arg2&O_RDONLY){
//设置了O_RDONLY选项
}
if (arg2&O_WRONLY){
//设置了O_WRONLY选项
}
if (arg2&O_RDWR){
//设置了O_RDWR选项
}
if (arg2&O_CREAT){
//设置了O_CREAT选项
}
//...
}
ps:open并不像C语言那样默认打开文件时清空内容,如果要打开前清空内容就要再传一个O_TRUNC,只传一个O_WRONLY只是覆盖式写。
所以说库里的函数都是对系统调用的封装


为什么语言要对系统调用进行封装?
首先肯定是系统调用麻烦,因为你用系统接口你还要知道这些宏啊,掩码啊之类的。
其次更重要的就是跨平台性,可移植性。说人话就是你的语言在别的平台也可以用,不同系统比如mac os,windows,Linux的系统调用接口都不一样,不同语言在不同平台下运行它语言的代码,就有对 对应平台系统调用接口 的封装,你封装了才能在这个系统下面使用你的语言,如果不行那这语言肯定没太多人用。文件相关的系统调用是所有语言文件操作的根,只要理解系统调用,语言的文件操作只需熟悉即可。
如何做到跨平台?
所以为啥语言更新一次都是几年一更新,就是因为发布前需要全部平台都兼容。。

第三个参数:表示创建文件的默认权限
例如,将Mode设置为0666,期望中文件创建出来的权限如下:后三个数字分别对应用户,组,其他的权限
![]()
但实际创建出的其实是0664,这是因为创建出来的文件的权限还会收到umask(文件默认掩码)的影响,实际创建出来文件的权限为:mode&(~umask)。
umask的默认值为0002,目的就是限制others随意修改文件。所以当我们设置mode是0666时,创建出的文件真实权限其实是0664.
如果想要创建出的文件权限值不受umask的影响,可以使用umask函数将文件默认掩码设置为0
umask(0); //将文件默认掩码设置为0
ps:不需要创建文件时,open的第三个可以不设置。
返回值:返回一个文件描述符
文件描述符本质就是一个指针数组的下标,数组中的每一个指针指向被打开文件的信息,open成功时数组指针个数就加1,所以当你一次打开多个文件然后打印他们的返回值时,会发现他们是以1递增的,而且是从3开始。如果打开失败,返回的是-1。而0,1,2其实对应的是标准输入流,标准输出流,标准错误流。这三个文件描述符是默认缺省打开的。
ps:当你手动将0,文件关闭后,你会发现新建的第一个文件描述符就是0了。所以文件描述符的分配规则:在files_struct数组中,找到当前没有被使用的最小的一个下标,作为新的文件描述符。
OS内一定是存在大量被打开文件的,操作系统当然需要对这些文件进行管理。何谓管理,先描述再组织,依据文件的属性一定是创建了struct file类似的结构体,然后不同结构体里通过指针如同双链表一般链接起来,如此对文件的管理转换成对具体数据结构的管理,这里都转换成了对链表的增删查改。

为了知道文件是哪些进程打开的,所以进程结构体PCB中肯定有一个struct file_struct*file来存储进程打开的文件。

源码:


所以说文件描述符本质是数组下标。这也是为啥write,close要传fd,就是为了方便OS识别打开的文件。我们有了文件描述符就知道要对哪些文件进行操作了。
3.2close
int close(int fd);
关闭某文件传入文件的文件描述符即可。文件关闭失败会返回-1.
3.3write
ssize_t write(int fd, const void *buf, size_t count);
解释:buf位置开始向后count字节的数据写入文件描述符为fd的文件当中。
- 如果数据写入成功,实际写入数据的字节个数被返回。
- 如果数据写入失败,-1被返回。
写入文件代码展示:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0){
perror("open");
return 1;
}
const char* msg = "hello syscall\n";
for (int i = 0; i < 5; i++){
write(fd, msg, strlen(msg));
}
close(fd);
return 0;
}

3.4read
ssize_t read(int fd, void *buf, size_t count);
从文件描述符为fd的文件中读取count字节内容到buf里
- 如果数据读取成功,实际读取数据的字节个数被返回。
- 如果数据读取失败,-1被返回。
读取操作演示:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd = open("log.txt", O_RDONLY);
if (fd < 0){
perror("open");
return 1;
}
char ch;
while (1){
ssize_t s = read(fd, &ch, 1);
if (s <= 0){
break;
}
write(1, &ch, 1); //向文件描述符为1的文件写入数据,即向显示器写入数据
}
close(fd);
return 0;
}

3.5重定向
重定向原理
在明确了文件描述符的概念及其分配规则后,现在我们已经具备理解重定向原理的能力了。重定向的本质就是修改文件描述符对应的struct file*的内容。
输出重定向就是,说人话就是,将我们本应该输出到一个文件的数据重定向输出到另一个文件中。
例如,如果我们想让本应该输出到“显示器文件”的数据输出到myfile文件当中,那么我们可以在打开myfile文件之前将文件描述符为1的文件关闭,也就是将“显示器文件”关闭,这样一来,当我们后续打开myfile文件时所分配到的文件描述符就是1。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
int main()
{
close(1);
int fd = open("myfile", O_WRONLY|O_CREAT, 00644);
if(fd < 0){
perror("open");
return 1;
}
printf("fd: %d\n", fd);
fflush(stdout);
close(fd);
exit(0);
}
运行后我们发现,本该printf到显示器上的内容输入到了myfile文件里,其fd=1,这种情况就叫做输出重定向。常见的重定向有:>, >> , <
解释:
printf函数是默认向stdout输出数据的,而stdout指向的是一个struct FILE类型的结构体,该结构体当中有一个存储文件描述符的变量,而stdout指向的FILE结构体中存储的文件描述符就是1,因此printf实际上就是向文件描述符为1的文件输出数据。
C语言的数据并不是立马写到了内存操作系统里面,而是写到了C语言的缓冲区当中,所以使用printf打印完后需要使用fflush将C语言缓冲区当中的数据刷新到文件中。
或者例如,如果我们想让本应该从“键盘文件”读取数据的scanf函数,改为从log.txt文件当中读取数据,那么我们可以在打开log.txt文件之前将文件描述符为0的文件关闭,也就是将“键盘文件”关闭,这样一来,当我们后续打开log.txt文件时所分配到的文件描述符就是0。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
close(0);
int fd = open("log.txt", O_RDONLY | O_CREAT, 0666);
if (fd < 0){
perror("open");
return 1;
}
char str[40];
while (scanf("%s", str) != EOF){
printf("%s\n", str);
}
close(fd);
return 0;
}
想要追加重定向的话就给open第二个参数再传O_APPEND,打开文件前close(1)即可。
dup2
要完成重定向,我们可以将fd_array数组中的元素拷贝,将我们的文件拷贝到指定下标位置即可。比如我们要将fd_aeeay[3]当中元素拷贝到fd_array[1]当中,因为C语言当中的stdout就是向文件描述符为1文件输出数据,那么此时我们就将输出重定向到了我们自己的文件中。
而在Linux操作系统中提供了系统接口dup2,我们可以使用该函数完成重定向。
int dup2(int oldfd, int newfd);
功能:看参数就很好理解,dup2会将fd_array[oldfd]内容拷贝到fd_array[newfd]当中,如果有必要的话我们需要先使用关闭文件描述符为newfd的文件。
返回值:调用成功返回newfd,否则返回-1.
使用dup2时,我们需要注意以下两点:
- 如果oldfd不是有效的文件描述符,则dup2调用失败,并且此时文件描述符为newfd的文件没有被关闭。
- 如果oldfd是一个有效的文件描述符,但是newfd和oldfd具有相同的值,则dup2不做任何操作,并返回newfd。
例如,我们将打开文件log.txt时获取到的文件描述符fd和1传入dup2函数,那么dup2将会把fd_arrya[fd]的内容拷贝到fd_array[1]中,本应该输出到显示器的数据就会重定向输出到log.txt文件当中。
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
int main()
{
int fd = open("log.txt", O_WRONLY | O_CREAT, 0666);
if (fd < 0){
perror("open");
return 1;
}
close(1);
dup2(fd, 1);
printf("hello printf\n");
fprintf(stdout, "hello fprintf\n");
return 0;
}

在minishell中添加重定向功能
添加重定向步骤大致如下:
1.对于获取到的命令进行判断,若命令当中包含重定向符号>、>>或是<,则该命令需要进行处理,即将符号左边解析为命令,右边解析为文件。
2.设置type变量,type为0表示命令当中包含输出重定向,type为1表示命令当中包含追加重定向,type为2表示命令当中包含输入重定向。
3.重定向符号后面的字段标识为目标文件名,若type值为0,则以写的方式打开目标文件;若type值为1,则以追加的方式打开目标文件;若type值为2,则以读的方式打开目标文件。
4.若type值为0或者1,则使用dup2接口实现目标文件与标准输出流的重定向;若type值为2,则使用dup2接口实现目标文件与标准输入流的重定向。
4.理解“一切皆文件”
简单来说,就是一些不是文件的外设设备(键盘,显示器,磁盘等)可以抽象为文件,你可以用访问文件的方式去访问他们的一切信息。如此以来,开发者仅需要使用一套API和开发工具,即可调取Linux系统中绝大部分资源。比如,Linux中几乎所有读操作(读文件,读系统状态)都能用read函数进行,几乎所有更改(更改文件,更改系统参数)的操作都可以用write函数进行。
之前我们说过,当打开一个文件时,操作系统为了管理所打开的文件,就会为这个文件创建一个file结构体,该结构体定义在/usr/src/kernels/3.10.0-1160.71.1.el7.x86_64/include/linux/fs.h下。值得关注的是,struct file中的f_op指针指向了一个file_operations结构体,这个结构体中的成员除了struct module* owner其余都是函数指针。该结构和struct file都在fs.h下,struct file中有指向该结构的指针。
所以系统调用后的流程是这样的:用户调用系统调用,内核根据fd找到struct file(task_struct->file找到文件描述符表,然后再通过fd索引找到struct file),利用file->f_op找到file_operation结构体,然后检查f_op->【对应系统调用名称】是否存在,存在就调用这个函数。
struct file_operations {
struct module *owner;
//指向拥有该模块的指针;
loff_t (*llseek) (struct file *, loff_t, int);
//llseek ⽅法⽤作改变⽂件中的当前读/写位置, 并且新位置作为(正的)返回值.
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
//⽤来从设备中获取数据. 在这个位置的⼀个空指针导致 read 系统调⽤以 -
EINVAL("Invalid argument") 失败. ⼀个⾮负返回值代表了成功读取的字节数( 返回值是⼀个
"signed size" 类型, 常常是⽬标平台本地的整数类型).
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
//发送数据给设备. 如果 NULL, -EINVAL 返回给调⽤ write 系统调⽤的程序. 如果⾮负,
返回值代表成功写的字节数.
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long,
loff_t);
//初始化⼀个异步读 -- 可能在函数返回前不结束的读操作.
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned
long, loff_t);
//初始化设备上的⼀个异步写.
int (*readdir) (struct file *, void *, filldir_t);
//对于设备⽂件这个成员应当为 NULL; 它⽤来读取⽬录, 并且仅对**⽂件系统**有⽤.
unsigned int (*poll) (struct file *, struct poll_table_struct *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
//mmap ⽤来请求将设备内存映射到进程的地址空间. 如果这个⽅法是 NULL, mmap 系统调⽤
返回 -ENODEV.
int (*open) (struct inode *, struct file *);
//打开⼀个⽂件
int (*flush) (struct file *, fl_owner_t id);
//flush 操作在进程关闭它的设备⽂件描述符的拷⻉时调⽤;
int (*release) (struct inode *, struct file *);
//在⽂件结构被释放时引⽤这个操作. 如同 open, release 可以为 NULL.
int (*fsync) (struct file *, struct dentry *, int datasync);
//⽤⼾调⽤来刷新任何挂着的数据.
int (*aio_fsync) (struct kiocb *, int datasync);
int (*fasync) (int, struct file *, int);
int (*lock) (struct file *, int, struct file_lock *);
//lock ⽅法⽤来实现⽂件加锁; 加锁对常规⽂件是必不可少的特性, 但是设备驱动⼏乎从不实
现它.
ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *,
int);
unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned
long, unsigned long, unsigned long);
int (*check_flags)(int);
int (*flock) (struct file *, int, struct file_lock *);
ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t
*, size_t, unsigned int);
ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *,
size_t, unsigned int);
int (*setlease)(struct file *, long, struct file_lock **);
};
file_operation就是把系统调用和驱动程序关联起来的关键数据结构,这个结构的每一个成员都对应着一个系统调用,读取file_operation中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。
每个设备都可以有自己的read,write,但一定是对应着不同的操作方法。比如我的显示器只需要实现写方法,读方法函数内部是空的。
通过struct file下file_operation中的各种函数回调,让我们开发者只用file便可调取Linux系统中绝大部分的资源,这便是Linux下一切皆文件的核心理解。

ps:其实这就是VFS虚拟文件系统:(给进程封装文件)
VFS(Virtual File System,虚拟文件系统) 是 Linux 内核中的一个核心抽象层,它通过统一接口屏蔽不同文件系统(如 ext4、NTFS、procfs)和硬件设备(如磁盘、USB、网络存储)的差异,让用户和应用程序能够以一致的方式访问所有文件资源。其核心思想是**“一切皆文件”**。
计算机里的一切问题,都可以通过添加一层软件层解决。这也是为啥进程和内存间需要有进程虚拟地址空间,如果进程直接对应内存就需要维护具体的物理地址,一旦代码和数据发生变化,那么程序对应的地址就会变化,很容易会造成野指针问题,而且自己维护加载到内存的具体物理地址会特别麻烦,所以进程和内存之间添加一层软件层,也就是进程虚拟地址空间,解耦进程与内存的直接依赖。
ps:另外我们还可以发现,这个文件和外设的关系就类似基类和派生类,指针指向什么就调用什么函数,这是多态,只不过Linux是用C语言写的,所以只能用函数指针。
5.缓冲区
打开文件,OS会创建一个strutc file ,其内容又分为三类:文件属性,操作方法集,文件缓冲区。磁盘中存储文件的属性和内容,属性加载到文件属性,内容加载到文件缓冲区,对应文件读写方法加载到操作方法集。未来调用read的时候,文件拷贝到文件缓冲区,拷贝成功后再调用操作方法集里的内容将数据内容刷新到外设。

5.1什么是缓冲区
缓冲区分为文件缓冲区(FILE缓冲区,也叫用户缓冲区),和内核缓冲区,缓冲区的本质其实就是一段内存空间。注意一下,下边提及到的“缓冲区”是指用户缓冲区。
缓冲区是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区依据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。
5.2为什么要引入缓冲区机制
读写文件时,如果不会开辟堆文件操作的缓冲区,直接通过系统调用对磁盘进行操作(读,写等),那么每次对文件进行一次读写操作时,都需要使用读写系统调用来处理此操作,即需要执行一次系统调用,执行一次系统调用将涉及到CPU状态的切换,即从用户空间切换到内核空间,实现进程上下文的切换,这将损耗一定的CPU时间,频繁的磁盘访问会对程序的执行效率造成很大影响。
为了减少使用系统调用的次数,提高效率,我们就可以采用缓冲机制。比如我们从磁盘里取信息,可以在磁盘文件进行操作时,可以一次从文件中读出大量的数据到缓冲区中,以后对这部分的访问就不需要再使用系统调用了,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。
又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们CPU就可以处理别的事。可以看出,缓冲区就是一块内存区,它用在输入输出设备和CPU之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU能够协调工作,避免低速的输入输出设备占用CPU,解放CPU!使其能够高效率的工作。
引入缓冲区还可以提高IO函数的效率,比如像printf把数据格式化后,拷贝到文件缓冲区后就可以立即返回,然后处理余下代码了。(printf流程:数据格式化->拷贝到文件缓冲区->检查是否刷新(因为默认是行缓冲区,检测是否有换行符))
5.3缓冲类型
标准I/O提供了3种类型的缓冲区
1.全缓冲区:这种缓冲方式要求填满整个缓冲区后才进行I/O系统调用操作。对于磁盘文件的操作通常使用全缓冲的方式访问。
2.行缓冲区:在行缓冲情况下,当在输入和输出中遇到换行符时,标准I/O库函数将会执行系统调用操作。当所操作的流涉及到一个终端时(例如标准输入和标准输出),使用行缓冲方式。(比如我们将命令打印到显示器上就是行缓冲)因为标准I/O库每行的缓冲区长度是固定的,所以只要填满了缓冲区,即使还没有遇到换行符,也会执行I/O系统调用操作,默认行缓冲区的大小为1024.
3.无缓冲区:无缓冲区是指标准I/O库不对字符进行缓存,直接调用系统调用。标准出错流stderr通常是不带缓冲区的,这使得出错信息能够尽快地显示出来。注意这里的不带缓冲区是指不带用户缓冲区(文件缓冲区,也就是FILE缓冲区),系统调用会直接将数据拷贝到内核缓冲区。
所谓库函数不过是封装了系统调用,所有刷新到用户缓冲区的内容最终都会刷新到内核缓冲区。
除了上述列举的默认刷新方式,下列特殊情况也会引发用户缓冲区的刷新:
1.缓冲区满时
2.执行flush语句(用户缓冲区刷新到内核缓冲区)
3.进程结束
ps:fsync(fd) 内核刷新到外设
示例如下:
之前我们也有一个问题就是sleep期间printf的数据去哪了,答案就是在缓冲区中,

#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 0;
}
printf("hello world: %d\n", fd);
close(fd);
return 0;
}
我们本想使用重定向思维,让本应该打印在显示屏上的内容写到“log.txt”文件中(printf函数默认向fd=1的文件输出),但我们发现,程序运行结束后,文件中并没有被写入内容:

这是由于我们将1号描述符重定向到磁盘文件后,缓冲区的刷新方式成为了全缓冲。而我们写入的内容并没有填满整个缓冲区,导致并不会将缓冲区的内容刷新到磁盘文件中。怎么办呢?可以使用fflush强制刷新下缓冲区。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
close(1);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 0;
}
printf("hello world: %d\n", fd);
fflush(stdout);
close(fd);
return 0;
}
还有一种方法,刚好可以解释stderr是无缓冲区的
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
close(2);
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0666);
if (fd < 0) {
perror("open");
return 0;
}
perror("hello world");
close(fd);
return 0;
}
这种方式便可以将2号文件描述符重定向至文件,由于stderr没有缓冲区,“hello world”不用fflush语句也可以写入文件
5.4FILE
上述提到的缓冲区都是FILE缓冲区(用户缓冲区),不是内核缓冲区。
因为IO相关函数与系统调用接口对应,并且库函数封装系统调用,所以本质上,访问文件都是通过fd访问的。所以C库当中的FILE结构体内部,必定封装了fd。
ps:struct FILE本质是一个结构体,里边封装了这些东西
我们研究一下下面这段代码:
#include <stdio.h>
#include <string.h>
int main()
{
const char *msg0="hello printf\n";
const char *msg1="hello fwrite\n";
const char *msg2="hello write\n";
printf("%s", msg0);
fwrite(msg1, strlen(msg0), 1, stdout);
write(1, msg2, strlen(msg2));
fork();
return 0;
}
运行结果
如果将输出重定向到我们的文件,再cat打开,会发现运行结果变成了这样

write(系统调用)运行一次,printf和fwrite(库函数)运行了两次。原因肯定和fork有关!
一般C库函数写入文件时是全缓冲的,而写入显示器是行缓冲。
printf fwrite库函数自己维护了一段用户缓冲区,当发生重定向到普通文件时,数据的缓冲方式由行缓冲(遇到\n刷新缓冲区)变成了全缓冲。(缓冲区满才刷新)(普通文件默认全缓冲)
而我们放在缓冲区中的数据,就不会被立即刷新,甚至fork之后也不立即刷新,但是进程退出后会统一刷新,写入文件当中。
但fork时,用户态缓冲区(由printf/fwrite维护)也会在fork()时复制给子进程,父进程和子进程各自退出后,都会刷新缓冲区内的内容导致数据被重复写入文件。
而write作为系统调用不会维护缓冲区,也就是无缓冲区模式。
综上:printf fwrite库函数都会自己维护缓冲区,而write系统调用则没有缓冲区。我们所说的缓冲区,都是用户级缓冲区(FILE缓冲区)。
其实为了提升整机性能,OS也会提供相关内核级缓冲区。当我们刷新用户缓冲区的数据时,并不是直接将用户缓冲区的数据刷新到磁盘或是显示器上,而是先将数据刷新到操作系统缓冲区,然后再由操作系统将数据刷新到磁盘或是显示器上。(操作系统有自己的刷新机制,我们不必关系操作系统缓冲区的刷新规则)
那么库函数的缓冲区是谁提供的?库函数在系统调用上层,库函数是系统调用的封装,但是系统调用没有缓冲区,库函数有缓冲区,所以库函数的缓冲区是二次加上的,又因为是C,所以由C标准库提供。
常说printf默认打到stdout里,其实像标准输入流,标准输出流都是FILE*类型的指针,他们自己都会维护缓冲区。
ps:标准错误流没有用户缓冲区,直接刷新到内核缓冲区,这是为了错误信息即使打印,以及正常输出和错误输出的分离。
ps:./a.out >ok.txt其实是./a.out 1>ok.txt的简写,因为>是 输出 重定向呗
ps:也可以 newfd>&oldfd 的形式重定向,将oldfd的内容拷贝到newfd。
未来debug时看error.txt就行了
FILE结构体中有一大段缓冲区信息,感兴趣可以看看
//缓冲区相关
/* 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. */
6.模拟简单实现libc库
mystdio.h
1 #ifndef __MYSTDIO_H__ 2 #define __MYSTDIO_H__ 3 4 #define FLUSH_NONE 1 5 #define FLUSH_LINE 2 6 #define FLUSH_FULL 4 7 8 #define SIZE 4096 9 #define UMASK 0666 10 11 #define FORCE 1 12 #define NORMAL 2 13 14 typedef struct _MY_IO_FILE 15 { 16 int fileno; 17 int flag; 18 char outbuffer[SIZE]; 19 int curr; 20 int cap; 21 }MyFILE; 22 23 MyFILE *my_fopen(const char*filename,const char*mode); 24 void my_fclose(MyFILE*fp); 25 int my_fwrite(const char*s,int size,MyFILE*fp); 26 void my_fflush(MyFILE*fp); 27 28 #endif
mystdio.c
1 #include"mystdio.h" 2 #include<stdlib.h> 3 #include<string.h> 4 #include<sys/types.h> 5 #include<sys/stat.h> 6 #include<fcntl.h> 7 #include<unistd.h> 8 MyFILE*my_fopen(const char*filename,const char*mode) 9 { 10 int fd=-1; 11 if(strcmp(mode,"w")==0) 12 { 13 fd=open(filename,O_CREAT|O_WRONLY|O_TRUNC,UMASK); 14 } 15 else if(strcmp(mode,"r")==0) 16 { 17 fd=open(filename,O_RDONLY); 18 } 19 else if(strcmp(mode,"a")==0) 20 { 21 fd=open(filename,O_CREAT|O_WRONLY|O_APPEND,UMASK); 22 } 23 else if(strcmp(mode,"a+")==0) 24 { 25 fd=open(filename,O_CREAT|O_RDWR|O_APPEND,UMASK); 26 } 27 else 28 { 29 //... 30 } 31 32 //打开文件失败 33 if(fd<0)return NULL; 34 MyFILE*fp=(MyFILE*)malloc(sizeof(MyFILE)); 35 if(!fp)return NULL; 36 37 //打开文件就是在创建FILE对象 38 fp->fileno=fd; 39 fp->flag=FLUSH_LINE; 40 fp->curr=0; 41 fp->cap=SIZE; 42 fp->outbuffer[0]=0; 43 44 return fp; 45 } 46 void my_fclose(MyFILE*fp) 47 { 48 if(fp->fileno>=0) 49 { 50 //强制刷新一下 51 my_fflush(fp); 52 fsync(fp->fileno); 53 close(fp->fileno); 54 free(fp); 55 } 56 57 } 58 59 static void my_fflush_core(MyFILE*fp,int force) 60 { 61 if(fp->curr<=0)return; 62 if(force==FORCE) 63 { 64 write(fp->fileno,fp->outbuffer,fp->curr); 65 fp->curr=0; 66 } 67 else 68 { 69 if( (fp->flag&FLUSH_LINE) && fp->outbuffer[fp->curr-1]=='\n') 70 { 71 //刷新,也就是系统调用 72 write(fp->fileno,fp->outbuffer,fp->curr); 73 fp->curr=0; 74 } 75 else if((fp->flag&FLUSH_FULL)&&fp->curr==fp->cap) 76 { 77 write(fp->fileno,fp->outbuffer,fp->curr); 78 fp->curr=0; 79 } 80 else 81 { 82 write(fp->fileno,fp->outbuffer,fp->curr); 83 fp->curr=0; 84 } 85 } 86 } 87 int my_fwrite(const char*s,int size,MyFILE*fp) 88 { 89 //fwrite的本质:拷贝 90 memcpy(fp->outbuffer+fp->curr,s,size); 91 fp->curr+=size; 92 93 //检测刷新,必须符合刷新条件 94 my_fflush_core(fp,NORMAL); 95 return size; 96 } 97 void my_fflush(MyFILE*fp) 98 { 99 my_fflush_core(fp,FORCE); 100 }
test.c
1 #include"mystdio.h" 2 #include<string.h> 3 int main() 4 { 5 MyFILE*fp=my_fopen("log.txt","w"); 6 if(fp==NULL) 7 { 8 return 1; 9 } 10 11 const char *s="hello myfile\n"; 12 int cnt=20; 13 while(cnt--) 14 { 15 my_fwrite(s,strlen(s),fp); 16 } 17 18 my_fclose(fp); 19 return 0; 20 }
此篇完


1401

被折叠的 条评论
为什么被折叠?



