【知识讲解】 Linux文件系统底层认识


目录

前言

[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与缓冲区时序问题

示例代码

cpp 复制代码
int 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之前,手动强制刷新缓冲区

cpp 复制代码
printf("---\n");
fflush(stdout); // 强制把stdout缓冲区全部刷入内核
close(fd);

原理:printf属于C库,写在用户缓冲区;如果不fflush,缓冲区数据还在用户内存,关闭文件描述符之后,无法完成写入。


Part5. fork + 缓冲区

cpp 复制代码
printf("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相关的知识,敬请期待~
最后,祝大家可以:春风得意马蹄疾,一日看尽长安花!

最后的最后,要是觉得本文还可以的话,可以点点赞,关注小编一波,谢谢大家!~

相关推荐
Chenyu_3104 小时前
DPDK、TC 和 XDP:Linux 高性能网络到底在优化什么
linux·服务器·网络·缓存
陈让然5 小时前
WSL 桥接设置静态IP(快速)
linux·网络·ubuntu
乔旭伟5 小时前
Linux 服务器备份实战:一致性快照、日志归档与恢复演练
linux·运维·服务器
乔旭伟6 小时前
Linux 服务器安全加固实战:SSH、防火墙、补丁与入侵排查
linux·服务器·安全
刘胡子大叔7 小时前
备份包只有 1.5K?
linux·服务器·网络
无足鸟ICT7 小时前
【RHCA+】RPM包验证和数字证书
linux·运维·服务器
erzong2317 小时前
Linux操作系统:线程概念与控制
linux·算法
星恒随风7 小时前
Linux线程概念详解:从分页存储、页表、MMU与TLB,深入理解线程和进程的区别与联系
linux·运维·笔记·学习·状态模式
小范的技术工坊7 小时前
Linux应用开发(文件IO)
linux·stm32·单片机
小范的技术工坊7 小时前
Linux应用开发(文件属性)
linux·运维·服务器