【知识讲解】 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相关的知识,敬请期待~
最后,祝大家可以:春风得意马蹄疾,一日看尽长安花!

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

相关推荐
IT大白鼠1 小时前
彭大帅的AI运维助手——自然语言管理 Linux 集群与网络设备——第 1 篇 · 骨架:说人话,跑命令:自然语言 SSH 运维的骨架
linux·运维·人工智能
牢姐与蒯1 小时前
Linux信号(一).信号产生
linux·运维·服务器·ubuntu
雯宝7 小时前
mnt_init 函数
linux
律宏阔13 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔13 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴13 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千14 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei3714 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei3714 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化