目录
[Part1. Shell 标准输出、标准错误](#Part1. Shell 标准输出、标准错误)
[Part2. VFS 虚拟文件系统](#Part2. VFS 虚拟文件系统)
[Part3. 缓冲区原理](#Part3. 缓冲区原理)
[Part4. close与缓冲区时序问题](#Part4. close与缓冲区时序问题)
[Part5. fork + 缓冲区](#Part5. fork + 缓冲区)
[Part6. C库FILE* 封装实现思路](#Part6. C库FILE* 封装实现思路)
[Part7. 结语](#Part7. 结语)
前言
Linux下一切皆文件,对于文件底层的认识是非常重要的,接下来来跟随小编的视角来看看吧。
let's go!!!!!!!!
Part1. Shell 标准输出、标准错误
基础回顾
进程默认3个文件描述符:
fd 名称 C++对象 作用
0 stdin 标准输入 std::cin 读取输入
1 stdout 标准输出 std::cout 普通程序输出
2 stderr 标准错误 std::cerr 错误信息输出核心特点:stdout 和 stderr 默认都打印到显示器,但二者是两套独立的文件流。
std::cerr 默认不带缓冲区,直接输出;std::cout 默认带行缓冲。
重定向语法
cpp//fd=1(标准输出)重定向写入 log.txt,覆盖原有内容 ./a.out 1> log.txt # 简写,> 默认就是fd=1,等价上面 ./a.out > log.txt //fd=2(标准错误)重定向到 err.txt ./a.out 2> log.err //输出和错误分开保存 ./a.out 1> log.txt 2> log.err //标准错误重定向到标准输出(二者合并写入同一个文件) ./a.out > log.txt 2>&1 //>> 追加模式,不会清空文件 ./a.out >> log.txt 2>> log.txt
执行命令:
./a.out > log.txt
• std::cout 输出走fd=1,被重定向,写入log.txt• std::cerr 走fd=2,不受>影响,仍然直接打印到终端屏幕
这就是分开stdout/stderr的意义:即使正常输出被重定向保存到文件,错误提示依然可以直接展示给用户。
Part2. VFS 虚拟文件系统
Linux内核使用VFS抽象层,实现一套统一文件接口,管理所有外设。
不管是普通磁盘文件、键盘、显示器、管道、socket,在内核都统一抽象成struct file。
核心结构
1. 所有外部设备,在内核都抽象成统一结构体,包含:type设备类型、status状态、读写操作函数指针。
2. struct file 内部存放函数指针:read、write等。
◦ 读显示器、读磁盘文件,上层调用接口完全一致。
◦ 内核会根据设备类型,调用对应设备底层读写函数。
这就是面向对象多态思想:C语言通过函数指针实现多态。上层代码不用关心底层是什么硬件。
Linux「一切皆文件」
磁盘、终端、管道、socket都可以通过open/read/write访问,统一文件描述符机制。
VFS就是这套抽象的核心,隔离上层系统调用和底层硬件驱动。
Part3. 缓冲区原理
分为两层:用户态缓冲区 和 内核缓冲区。
手写笔记重点:fopen/fprintf/cout带的缓冲区,属于用户缓冲区,在应用程序内存里;open/write系统调用没有用户缓冲区,直接进入内核缓冲区。
为什么要有用户缓冲区?
系统调用(进入内核)开销很大。频繁调用write会不断陷入内核,性能差。
用户缓冲区就是一块内存:先把数据攒在用户内存,攒够条件,一次性调用write写入内核缓冲区,减少系统调用次数,提升IO效率。
缓冲区刷新触发条件(满足任意一条就会刷入内核)
1. 行缓冲:遇到换行符\n刷新(stdout默认是行缓冲)
2. 缓冲区写满,自动刷新
3. 主动调用fflush强制刷新 fflush(stdout)
4. 程序正常退出,自动刷新缓冲区
区分:
• stdout:行缓冲,终端场景遇到\n就刷;重定向到文件时,变为全缓冲。
• stderr:默认无缓冲,写入立刻调用系统调用,不缓存。
Part4. close与缓冲区时序问题
示例代码
cppint fd = open("log.txt", O_CREAT | O_WRONLY | O_TRUNC, 0666); close(1); // 关闭标准输出fd=1 dup2(fd, 1); // 将fd=1重定向到log.txt printf("---\n"); // printf使用C库stdout缓冲区 write(fd, "aaa", 3); // 系统调用,无用户缓冲区 close(fd);
现象:
1. close(1) + dup2,把stdout重定向到文件
2. write是系统调用,直接写入内核缓冲,立刻写入文件
3. printf的字符串存在C库stdout用户缓冲区
4. 紧接着close(fd)关闭文件描述符
问题:缓冲区还没刷新!程序还没触发缓冲区刷新,文件fd已经被关闭。缓冲区里残留的数据无法写入文件,printf内容丢失!
解决方案:在printf之后,close之前,手动强制刷新缓冲区
cppprintf("---\n"); fflush(stdout); // 强制把stdout缓冲区全部刷入内核 close(fd);原理:printf属于C库,写在用户缓冲区;如果不fflush,缓冲区数据还在用户内存,关闭文件描述符之后,无法完成写入。
Part5. fork + 缓冲区
cppprintf("hhh"); fork();输出结果:终端运行输出1次;重定向到文件运行,输出2次hhh。
原理
1. 终端环境:stdout是行缓冲,字符串hhh没有换行,但程序执行fork之前,终端缓冲区检测,或者说终端模式下,函数调用结束会刷,只打印一次。
2. 重定向到文件:stdout切换为全缓冲。printf("hhh")把数据写到用户缓冲区,没有触发刷新。
3. fork()创建子进程,子进程会拷贝父进程的用户内存,连带缓冲区一起复制。
4. 父子进程退出时,各自刷新自己缓冲区,于是文件里面出现两次hhh。
也就是fork会拷贝进程地址空间,包含未刷新的用户缓冲区。
规避办法:fork前调用fflush(stdout)清空缓冲区。
Part6. C库FILE* 封装实现思路
FILE*是C标准库封装的结构体,内部包含:
• fd:底层内核文件描述符
• 状态标记
• 用户缓冲区(char数组),以及有效数据长度
• 函数指针:flush、read、write等接口
简易实现逻辑:
1. fopen:底层调用系统调用open拿到fd,创建FILE结构体,分配用户缓冲区,根据传入模式r/w/a翻译成open对应的flags。
2. fwrite/fprintf:使用memcpy把数据拷贝进用户缓冲区;判断缓冲区是否写满,满了就调用底层write系统调用,刷入内核。
3. fflush:主动把缓冲区数据调用write写入内核。
4. fclose:先fflush刷新缓冲区,再调用close(fd)关闭内核文件。
底层:fopen/fclose/fread/fwrite全部是用户态库代码,最终调用open/close/read/write系统调用。
Part7. 结语
这篇文章我们知道了文件系统的底层,接下来,小编还会带来更多Linux相关的知识,敬请期待~
最后,祝大家可以:春风得意马蹄疾,一日看尽长安花!最后的最后,要是觉得本文还可以的话,可以点点赞,关注小编一波,谢谢大家!~
