我试图了解使用 Linux 内核模块的缺点。我了解使用它们的好处:能够将代码动态插入到正在运行的系统中,而无需重新编译和重新启动基本系统。鉴于这种强大的优势,我猜测大多数内核代码应该位于内核模块中,而不是作为基本内核的一部分,但情况似乎并非如此——大量的核心子系统(如内存管理)仍然进入基础内核。
我能想到的原因之一是内核模块在启动过程中加载得很晚,因此核心功能必须进入基础内核。我阅读的另一个原因是关于碎片化。
我不太明白为什么内核模块会导致内存碎片,有人可以解释一下吗?使用内核模块还有其他缺点吗?
答案1
是的,基本组件(例如mm)不能成为可加载模块的原因是因为它们是必不可少的——没有它们内核将无法工作。
我找不到任何引用声称内存碎片对可加载模块的影响是显着的,但是这部分LLKM 操作指南对您来说可能会很有趣。
我认为这个问题实际上是内存碎片问题的重要组成部分,通常发生在两个层面上:内核 mm 子系统管理的实际内存碎片,以及非常大的应用程序可能发生的虚拟地址空间碎片(哪个我猜想主要是它们设计和编译方式的结果)。
关于实际内存的碎片,我认为这在比页面大小 (4 KB) 更精细的粒度下是不可能的。因此,如果您正在读取 1 MB 的虚拟连续空间,该空间实际上 100% 碎片化为 1024 个页面,那么或许涉及 1000 次额外的小手术。在这部分操作指南中,我们读到:
基本内核在其珍贵的连续域中包含一大片可重用内存——kmalloc 池。在某些版本的 Linux 中,模块加载器首先尝试从该池中获取连续的内存来加载 LKM,并且仅当没有足够大的空间可用时,才会转到 vmalloc 空间。 Andi Kleen 于 2002 年 10 月提交了在 Linux 2.5 中执行此操作的代码。他声称差异在百分之几的范围内。
这里,vmalloc 空间(用户空间应用程序所在的位置)可能很容易碎片化为页面。这就是当代操作系统的现实(它们都通过虚拟寻址来管理内存)。我们可以由此推断,虚拟寻址也可能在用户空间中造成“几个百分点”的性能损失,但就虚拟寻址而言必要且不可避免的在用户领域,它仅与完全理论上的事物有关。
进程的虚拟地址空间(而不是其背后的实际内存)的碎片有可能进一步复合碎片,但这永远不会适用于内核模块(而最后一段显然可以)。
如果你想听我的意见,那不值得深思。请记住,即使使用高度模块化的内核,最常用的组件(文件系统、网络等)也往往会很早就加载并保持加载状态,因此它们肯定会位于实际内存的连续区域中,因为它是什么值得(这可能是不这样做的一个原因无意义地加载和卸载模块)。
答案2
对于单片内核,理论上可以为内核分配单个连续的内存块。如果按需加载(和卸载)模块,那么所有内核内存不可能都是连续的,因此根据定义,它将是碎片化的。代价是模块化内核通常会比单片内核使用更少的内存。对于默认发行版整体内核来说确实如此,它可能有许多未使用的驱动程序,但如果您构建自己的整体内核,情况就不那么正确了。
有些子系统根本不适合模块化,内存管理显然是一个,SMP 是另一个。进程调度不是模块化的,但 I/O 调度策略是模块化的。其他子系统可能由于复杂的相互依赖性而不是模块化的,例如 TCP/IP。
模块的另一个问题是,如果它们驱动您需要启动的设备,它们就不太好,解决方案如initrd
解决此问题。
最后一个考虑因素是安全性,允许可加载模块是一个潜在的风险,例如内核可加载 rootkit 等克纳克。看http://www.la-samhna.de/library/rootkits/index.html和http://www.sans.org/security-resources/malwarefaq/Ptrace.php。您可以通过强制执行签名模块(从内核 3.7 开始)、加载封锁模块最后,或其他硬化。
答案3
我不知道将代码编译为可加载内核模块而不是直接编译到内核映像中可能会有什么其他缺点,但要使用您的特定示例,假设内存管理曾是作为内核模块构建,而不是构建到内核二进制映像本身中。
- 内核如何为初始 RAM 磁盘分配内存?
- 内核如何为挂载文件系统所需的结构分配内存?
- 内核如何分配内存来加载内存管理模块?
- 内存管理模块如何知道内核的其他部分(可能是访问内存管理模块所需的其他可加载内核模块)已经声明了哪些内存?
正如您所看到的,这立即打开了整个潜在的蠕虫罐头。任务调度是另一个类似的核心内核概念。
从另一个角度来看,如果没有内存管理模块,内核还能做什么有用的工作呢?它不像硬件驱动程序,如果系统上未安装硬件,则可以简单地禁用它;这是内核本身甚至需要引导的基本功能。
虽然真正的微内核从关注点分离和代码可读性/可理解性的角度来看具有一定的好处,但即使它们需要内核本身内部的某些东西根本无法工作。内存和任务处理是多任务操作系统内核的核心概念之一 - 正如上面的列表所示,它们适用于任何工作所需的所有意图和目的。如果没有别的办法,尝试将其分离成单独加载的组件会增加大量的复杂性,而绝对没有任何好处(因为无论如何每个人都会加载该模块)。
答案4
Linux 内核模块代码是使用vmalloc()
. vmalloc()
没有碎片问题,因为它是虚拟内存。内存被分为页,通常为 4Kb,虚拟内存允许将碎片物理页重新映射为连续的虚拟块。
另一个角落是 vmalloc(),它在单独的虚拟地址空间中分配虚拟连续(但物理上分散)的页面。 vmalloc() 相对较慢,但它可以执行看起来与内核连续的大型分配。例如,它用于为可加载模块中的代码分配空间。