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++ 只是把它包装成了 virtualoverride 这些语法糖。


二、内核级缓冲区的本质:另类的共享内存

1. 笔记里的那句话

将数据的一块加载入内存,建立于共享区的一个映射!Mmap!就像共享内存一样!
内核级缓冲区这样的本质其实和共享内存一模一样!内核级缓冲区就是另类的共享内存!
这样优化了 IO 效率!

2. 为什么说它"和共享内存一模一样"

回忆共享内存(System V IPC)的三要素:

  1. 一块物理内存,不属于任何进程;
  2. 通过**映射(挂接)**进入各个进程自己的地址空间(共享区);
  3. 各进程读写同一块物理内存,省去拷贝

再看内核级缓冲区(页缓存 page cache):

  1. 一块物理内存——文件数据的一块被加载进内存后驻留在页缓存里;
  2. 通过映射机制关联到访问它的进程——进程读写这个"文件",实际是在操作这块内存;
  3. 多个进程访问同一个文件时,命中同一份页缓存,避免反复读盘

对比一下:进程私有数据被多个进程访问时要靠写时拷贝各存一份;而文件数据天然是公共资源,OS 让所有人映射同一份物理页——这就是"另类的共享内存"。

3. 它优化 IO 效率的三个角度

角度一:读的优化——预读与命中

第一次读文件某个块,要从磁盘搬到内存(慢);之后再读同一个块,直接命中页缓存,磁盘 IO 变成了内存访问,快几个数量级。而且内核会顺便把相邻的块也预读进来,顺序读几乎全程命中。

角度二:写的优化——延迟刷盘

进程 write 写文件,数据先进内核级缓冲区就返回了,不必等磁盘。真正的落盘由内核线程负责(之前笔记里的"约 30 秒周期")。把多次小写入攒成一次大写入,减少磁盘寻道。

角度三:共享的优化——零拷贝

多个进程读同一个文件,磁盘数据只需要从磁盘加载一次;进程 A 和进程 B 的"文件视图"都落在同一块物理页上。这和之前写动态库为什么能共享是同一个道理:加载一次,各进程映射,天然共享

4. 和 mmap 的关系

笔记里提到的 mmap 是把这套机制"显式化"的接口:

  • 普通文件读写:进程发 read/write 系统调用 → 内核在页缓存和用户缓冲区之间拷贝
  • mmap 映射文件:直接把文件的页缓存映射进进程地址空间,进程读写指针就是在读写页缓存本体——连内核态和用户态之间那次拷贝都省了

之前共享内存那篇提过 mmap 的挂载机制,这里正好闭环:共享内存 = 没有文件身份的页缓存;页缓存 = 有文件身份的共享内存。同一套物理页管理,两种使用姿势。


三、小结

这两页笔记各一句话:

  1. C 语言多态 = 结构体嵌套(偏移量为 0 的"继承")+ 函数指针表(运行期按类型查表)——VFS 的 f_op 就是现成的教科书;
  2. 内核级缓冲区 = 另类的共享内存——同一块物理页被映射给所有访问者,用"少拷贝、晚落盘、可共享"三个角度优化 IO。

学到这里的感悟是:操作系统的很多"高级机制",剥开来看都是最朴素的数据结构(结构体、指针、数组、映射)的组合。看懂一层封装,就少一分神秘感。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐