C语言多态与内核缓冲区的本质-VFS的两个底层设计
C 语言实现多态与内核级缓冲区的本质:两个"设计巧思"的收尾(7-31)
我的github:(https://github.com/xcx55/ubuntu-linux-project)
感谢各位大佬参观我的github!!
7 月 31 号的笔记只有两页,但都是"点睛"性质的:一篇回答「C 语言没有 class,Linux 内核的 VFS 凭什么玩多态」,另一篇把前面学过的内核级缓冲区和共享内存彻底打通。这两页很短,但串起来的东西不少,正好写成一篇收尾。
一、C 语言实现多态:结构体嵌套 + 函数指针
1. 笔记里的核心图
笔记原文只有三行,却把原理说全了:
嵌套结构属性,外面是函数指针!
本质:依据类型查符号表
确定偏移量,改变指针类型即可!
拆开讲就是三板斧:
第一板斧:结构体嵌套——"继承"的 C 语言翻译
C++ 里子类继承父类,翻译成 C 就是:把"父类结构体"作为"子类结构体的第一个成员"。
// 父类
struct file {
const struct file_operations *f_op; // 函数指针表
// ...其他公共属性
};
// "子类":普通文件
struct regular_file {
struct file base; // 第一个成员放父类
// 普通文件自己的属性
};
// "子类":管道文件
struct pipe_file {
struct file base;
struct pipe_inode_info *info;
// 管道自己的属性
};
关键在于:任何子类对象的地址,强制转换成父类指针后,偏移量都是 0——因为父类就在开头。这就是"确定偏移量,改变指针类型即可"的含义。
第二板斧:函数指针表——"虚函数表"的 C 语言翻译
C++ 的虚表放在对象里,C 语言直接把所有操作函数收进一张表:
struct file_operations {
int (*read)(struct file *, char *, size_t);
int (*write)(struct file *, const char *, size_t);
int (*open)(struct inode *, struct file *);
// ...
};
每个"子类"各准备一份自己的表:普通文件的 read 走磁盘 IO,管道文件的 read 走内核级缓冲区,socket 的 read 走网卡——接口名相同,实现各异。
第三板斧:依据类型查表调用——多态的发生时刻
上层代码只认父类指针:
struct file *f = ...; // 不管你是什么文件
f->f_op->read(f, buf, n); // 到底执行谁的 read?
这一句在运行期发生的事是:顺着 f 找到它的 f_op 指针 → 查到这张表里 read 槽位上记录的函数地址 → 跳转执行。"依据类型查符号表、确定偏移量"说的就是这条链路——对象里存了什么类型,就查到哪张表。
2. 回扣之前学过的三个场景
这套机制我们其实已经见过三次了:
| 场景 | 父类 | "子类"们的差异 |
|---|---|---|
| VFS 一切皆文件 | struct file + f_op | 普通文件/管道/socket 各自实现 read/write |
| 进程间通信 | 同一套 file→inode 架构 | 管道文件数据不落盘,走内核级缓冲区 |
| C++ 虚函数 | 基类 + 虚表 | 派生类重写虚函数,运行期查虚表 |
所以 C++ 的多态不是魔法,内核早就在用纯 C 写同样的东西。C++ 只是把它包装成了 virtual、override 这些语法糖。
二、内核级缓冲区的本质:另类的共享内存
1. 笔记里的那句话
将数据的一块加载入内存,建立于共享区的一个映射!Mmap!就像共享内存一样!
内核级缓冲区这样的本质其实和共享内存一模一样!内核级缓冲区就是另类的共享内存!
这样优化了 IO 效率!
2. 为什么说它"和共享内存一模一样"
回忆共享内存(System V IPC)的三要素:
- 一块物理内存,不属于任何进程;
- 通过**映射(挂接)**进入各个进程自己的地址空间(共享区);
- 各进程读写同一块物理内存,省去拷贝。
再看内核级缓冲区(页缓存 page cache):
- 一块物理内存——文件数据的一块被加载进内存后驻留在页缓存里;
- 通过映射机制关联到访问它的进程——进程读写这个"文件",实际是在操作这块内存;
- 多个进程访问同一个文件时,命中同一份页缓存,避免反复读盘。
对比一下:进程私有数据被多个进程访问时要靠写时拷贝各存一份;而文件数据天然是公共资源,OS 让所有人映射同一份物理页——这就是"另类的共享内存"。
3. 它优化 IO 效率的三个角度
角度一:读的优化——预读与命中
第一次读文件某个块,要从磁盘搬到内存(慢);之后再读同一个块,直接命中页缓存,磁盘 IO 变成了内存访问,快几个数量级。而且内核会顺便把相邻的块也预读进来,顺序读几乎全程命中。
角度二:写的优化——延迟刷盘
进程 write 写文件,数据先进内核级缓冲区就返回了,不必等磁盘。真正的落盘由内核线程负责(之前笔记里的"约 30 秒周期")。把多次小写入攒成一次大写入,减少磁盘寻道。
角度三:共享的优化——零拷贝
多个进程读同一个文件,磁盘数据只需要从磁盘加载一次;进程 A 和进程 B 的"文件视图"都落在同一块物理页上。这和之前写动态库为什么能共享是同一个道理:加载一次,各进程映射,天然共享。
4. 和 mmap 的关系
笔记里提到的 mmap 是把这套机制"显式化"的接口:
- 普通文件读写:进程发
read/write系统调用 → 内核在页缓存和用户缓冲区之间拷贝; mmap映射文件:直接把文件的页缓存映射进进程地址空间,进程读写指针就是在读写页缓存本体——连内核态和用户态之间那次拷贝都省了。
之前共享内存那篇提过 mmap 的挂载机制,这里正好闭环:共享内存 = 没有文件身份的页缓存;页缓存 = 有文件身份的共享内存。同一套物理页管理,两种使用姿势。
三、小结
这两页笔记各一句话:
- C 语言多态 = 结构体嵌套(偏移量为 0 的"继承")+ 函数指针表(运行期按类型查表)——VFS 的
f_op就是现成的教科书; - 内核级缓冲区 = 另类的共享内存——同一块物理页被映射给所有访问者,用"少拷贝、晚落盘、可共享"三个角度优化 IO。
学到这里的感悟是:操作系统的很多"高级机制",剥开来看都是最朴素的数据结构(结构体、指针、数组、映射)的组合。看懂一层封装,就少一分神秘感。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)