扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
2.2.3 内核模块
整个内核并不需要同时装入内存。应该确认,为保证系统能够正常运行,一些特定的内核必须总是驻留在内存中,例如,进程调度代码就必须常驻内存。但是内核其他部分,例如大部分的设备驱动就应该仅在内核需要的时候才装载,而在其他情况下则无需占用内存。
举例来说,只有在内核真正和CD-ROM通讯时才需要使用完成内核与CD-ROM通讯的设备驱动程序,因此内核可以被设置为在和设备通讯之前才装载相应代码。内核完成和设备的通讯之后可以将这部分代码丢弃。也就是说,一旦代码不再需要,就可以从内存中移走。系统运行过程中可以增减的这部分内核称为内核模块。
内核模块的优点是可以简化内核自身的开发。假设你购买了一个新的高速CD-ROM驱动器,但是现有的CD-ROM驱动程序并不支持该设备。你自然就希望增加对这种高速模式的支持以提高系统光驱设备的性能。如果作为内核模块来编译驱动程序,你的工作将会方便得多:编译驱动程序、加载到内核、测试、卸载驱动程序、修改驱动程序、再次加载驱动程序到内核、测试,如此周而复始。如果你的驱动程序是直接编辑在内核中的,那么你就必须重新编译整个内核并且在每次修改驱动程序之后重新启动机器。这样慢得很多。
自然,你也必须留意内核模块。对于指明其他内核模块在磁盘上的驻留位置的那些模块,一定不能从内存中卸载,否则,内核将只能通过访问磁盘来装载处理磁盘访问的内核模块,这是不可能实现的。这也是我们要选择把部分内核作为模块编译还是直接编译进内核使其常驻内存的又一个原因。知道自己系统的设置方式,因而也就可以选择正确使用的方式(如果为了确保安全,可以简单的忽略内核模块系统的优点,而把所有的内容都编译到内核里面)。
内核模块会带来一些速度上的损失,这是因为一些必需的代码现在并不在RAM中,必需要从磁盘读入。但是整个系统的性能通常会有所提高,这主要是因为通过丢弃暂时不使用的模块可以释放出额外的RAM供应用程序使用。如果这部分内存被内核所占用,应用程序将只能更加频繁地进行磁盘交换,而这种磁盘交换会显著地降低应用程序的性能(磁盘交换将在第8章中讨论)。
内核模块还会带来因复杂度的增加所造成的开销,这是因为在系统运行的过程中,移进移出部分内核需要额外的代码。然而,复杂度的开销是可以管理的。通过使用外部程序来代理一些必需的工作还可以更进一步降低复杂度的开销(更为确切的说法是,这样做不是减少了复杂度的开销,而是把复杂度的开销重新分配了一下)。这是对内核模块原理的一个小小的扩展:即使是内核的支持模块,对于内核来说也只是外部的、部分可用的,只有在需要的时候才被装入内存。
通常用于这种目的程序称为modprobe。有关的modprobe代码超出了本书的范围,但是在Linux的每个发行版本中都包含有它。本节的剩余部分将讨论同modprobe协同工作,以装载内核模块的内核代码。
1. request_module
24432:作为函数说明之前的注释,request_module是一个函数。内核的其他模块在需要装载其他内核模块的时候,都必须调用这个函数。就像内核处理其他工作一样,这种调用也是为当前运行的进程进行的。从进程的角度来看,这种调用的请求通常是隐含的—正在执行进程其他请求的内核可能会发现,必须调入一个模块才能够完成该请求。例如,请参见10070行,这里是一些将在第7章中讨论的代码。
24446:以内核中的一个独立进程的形式执行exec_modprobe函数(24384行)。这并不能只通过函数的简单调用实现,因为exec_modprobe要继续调用exec来执行一个程序。因此,对函数exec_modprobe的简单调用将永远不会有返回。
这和使用fork以准备exec调用十分类似,你可以认为kernel_thread对内核来说就是较低版本的fork,虽然两者有很大不同。fork是从指定函数开始执行新的进程,而不是从调用者的当前位置开始运行。正如fork一样,kernel_thread返回的值是新进程的进程号。
24448:和fork一样,从kernel_thread返回的负值表示内部错误。
24455:正如函数中论述的一样,大部分的信号将因当前进程而被暂时阻塞。
24462:等待exec_modprobe执行完毕,同时指出所需要的模块是已经成功装入内存,还是装载失败了。
24465:结束运行,恢复信号。如果exec_modprobe返回错误代码,则打印错误消息。
2. exec_modprobe
24384:exec_modprobe运行为内核增加内核模块的程序。这里的模块名是一个void*的指针,而不是char*的指针。原因简单说来就是kernel_thread 产生的函数通常都使用void*指针参数。
24386:设置modprobe的参数列表和环境。modprobe_path(24363行)用来定位modprobe程序的位置。它可以通过内核的sysctl特性来修改,这一点将在第11章中介绍(参见30388行)。这意味着root可以动态选择不同于/sbin/modprobe的程序来运行,以适应当modprobe被安装到其他地方或者使用修改过的modprobe替换掉了原有的modprobe之类的情况。
24400:(正如代码中描述的一样)出于安全性考虑,丢弃所有挂起的信号和信号句柄(handl-ers)。这里最重要的部分是对flush_signal_handlers的调用(28041行),它使用内核默认的信号句柄代替所有用户定义的信号句柄。如果在此时有信号被传送到内核,它将获得默认响应—通常是忽略信号或杀死进程。但是不管怎样都不会引起安全风险。由于该函数从触发它的进程中分离出来(如前所述),所以,不管原始进程在此处是否改变其原来分配的信号,句柄都不会产生任何影响。
24405:关闭调用进程打开的所有文件。最重要的是,这意味着modprobe程序不再从调用进程中继承标准输入输出和标准错误。这很有可能会引起安全漏洞(这可能是在替代modprobe的程序中引起的问题,但是modprobe本身实际上并不关心这个差异)。
24413:modprobe程序作为root运行,它拥有root所拥有的所有权限。和整个内核中其他地方一样,请注意root使用用户ID号0的假定在这里已经被写入程序。用户ID号和权能系统(capability system,在接下来的几行中会用到)将在第7章中介绍。
24421:试图执行modprobe程序。如果尝试失败,内核将使用printk打印错误消息并返回错误代码。这里是可能产生printk的缓冲器过载的地点之一。module_name的长度并没有明确限制,就我们对该调用的看法而言,它可能长达一百万个字符。为防止printk缓冲器过载,你必需遍历所有对于该函数的调用(实际上是对request_module的调用),以保证每个调用者使用足够短的、不会为printk造成麻烦的模块名。
24427:当execve成功执行时,它不会返回任何结果,因此本处是不可能执行到的。但是编译器却并不知道这一点,因此,此处使用了return语句以保证gcc不出错。
对于内核的进一步讨论将超出本章的既定范围,因此在这个问题上我们到此为止。然而本书中也包括了其他必需的内核代码。在读完第4章和第5章之后,也许你会希望再次仔细研读一下这部分内容。有关这个问题的两个文件是include/linux/module.h(从15529行开始)和/kernel/module.c(从24476行开始)。和sys_create_module(24586行)、sys_init_module(24637行)、sys_delete_module(24860行)和sys_query_module(25148行)四个函数需要特别注意一样,struct module(15581行)也要特别引起注意。这些函数实现了modprobe及insmod、lsmod和rmmod所使用的系统调用,以完成模块的装载、定位和卸载。
内核触发直接回调内核程序的现象看起来很令人奇怪。但是,实际上进行的工作不止于此。例如,modprobe必须实际访问磁盘以搜寻要装载的模块。而且更为重要的一点是,这种方法赋予root对内核模块系统更多的控制能力。这主要是因为root也可以运行modprobe及相关程序。因此,root既可以手工装载、查询、卸载模块,也可以由内核自动完成。
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者