购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.2 基础功能元素

2.2.1 模块支持

1.模块概述

模块是Linux支持动态功能扩展的最主要机制。内核代码中有很多模块,如果有了当前使用的内核的代码树,那么用户也可以编写外部的模块,动态添加到内核中执行即可。但是Linux内核的主要代码是遵守GPL(GNU General Public License)协议的,其内部暴露给模块使用。如果用户在内核模块进行编程时要使用内核内部定义的调用,就需要将自己完整的GPL公开,这就是GPL的传染机制。

有很多公司,例如做NTFS文件系统内核模块驱动的公司Tuxera,其最著名的产品是用户端开源的NTFS文件系统驱动ntfs-3g,然而这个驱动的效率并不高。不是公司没能力优化,而是不愿意公开,另外它们还提供闭源版的NTFS内核模块驱动,如果要使用则需要购买。再比如Kcodes公司的打印机模块,该模块可以识别和处理几乎所有的打印机。虽然该模块的功能非常强大,但是也是闭源的,也需要购买。

那么为什么这些公司可以闭源而不必遵守GPL协议呢?原因是它们大多数都采用两个模块的方式来规避,一个是LGPL模块,封装内核的GPL调用,但是自己本身是LGPL协议的,另一个模块则可以直接调用在第一个模块中封装过的,只有LGPL要求的系统这种方式严格的说也并不符合GPL协议的要求,但是在Linux内核中却是被允许的。GPL协议的传染性不能穿透二进制(否则用GCC写的所有程序都得开源)。

模块的执行原理与其他功能组件一样,都是模块开发者实现约定好功能的函数,然后使用规定的函数注册,在添加、关闭模块的时候内核模块调度系统就会执行用户注册的自己实现的函数。在模块开发中比较典型的是初始化和退出函数。

以上代码就构成了一个模块。可以看出module_init和module_exit就是注册约定函数的调用。模块内部定义的钩子函数是static的,目的是只内部可见。__init和__exit是GCC的特性,提供回收明确表示无用代码的能力。标记为__init的函数会被放入.init.text代码段,这个代码段在模块加载完后会被回收节省内存,因为不会再用到。最后的三个宏在很多版本上也可以编译和加载,但是一般尽量添加,尤其是在严肃的工程开发中。比如缺少MODULE_LICENSE宏的版权声明,在加载的时候内核就会告警:“Warning:loading hello.ko will taint the kernel:no license”。

2.内核符号表

内核符号表是内核内部各个功能模块之间互相调用的纽带,各个模块之间依赖这些函数调用进行通信。各个功能模块必须要导出符号表才能被模块使用。还有动态加载的模块的链接需求,在加载时符号表是对内核其他部分描述本模块的最好方式。加载的模块所导出的函数通过导出操作就可以被其他模块定位并调用。示例如下:

以上是摘自block/blk-core.c的3个函数,blk_old_get_request是内部使用的,blk_get_request使用了EXPORT_SYMBOL,可以被任何其他的模块和内核部分使用(仍然需要遵守GPL开源),而使用了EXPORT_SYMBOL_GPL的part_round_stats函数则是只能被声明自己是遵守GPL协议的模块使用。EXPORT_SYMBOL_GPL和EXPORT_SYMBOL的区别在于每个模块都可以声明自己模块遵守的Lisence。比如遵守GPL的模块,就可以在自己的模块代码中添加:MODULE_LICENSE("GPL"),或者是商业的模块可以MODULE_LICENSE(" proprietary"),之后就可以用另外一个模块调用本模块封装之后的函数,而另外的模块就不需要开源。只有设置了遵守GPL协议的模块才可以被EXPORT_SYMBOL_GPL定义导出的系统调用。

使用cat/proc/kallsyms命令会打印出包含了加载模块的内核当前的符号表,通过命令more/boot/System.map可以查看内核二进制符号列表。通过nm vmlinux也可以查看内核符号列表,可以显示所有在内核中的符号,模块中的符号要另行查看。通过nm module_name可以查看模块的符号列表,但是得到的是相对地址,只有加载后才会分配绝对地址,如图2-1所示。

图2-1

3.模块参数

模块可以在编程的时候指定其接受的参数,这个参数是给用户用的。模块加载之后,用户空间通过“echo-n ${value} >/sys/module/${modulename}/parameters/${parm}”就可以修改模块参数。

4.模块的加载和卸载

模块机制存在的意义就是动态加载和卸载,原则上内核模块在被使用的过程中不可以被卸载,但可以强制卸载,或者是找到所有使用单位按照顺序关闭或卸载,使模块的引用计数变为0。而加载的时候必须保证模块与运行中的内核相容。insmod、rmmod分别是约定的用户端加载和卸载模块的命令,但是也可以调用内核API来写其他名字的命令完成同样的操作。

5.模块签名

由于模块可以是外部代码,内核的版本又有很多个,所以内核必须确保该模块是使用当前内核代码编译出来的,否则执行时会出现错误。每个模块在编译时都会从内核目录中获得版本号写入编译的模块,运行中的内核在插入新的模块时会检测签名是否一致,若不一致就不会加载。

模块签名有两层含义,一层是版本号;另一层是哈希签名。使用modinfo就都可以发现,如图2-2所示。

图2-2

内核所关心的是在图2-2中显示的vermagic,这里没有对模块进行签名。如果签名了,则会在modinfo中多出signer、sig_key、sig_hashalgo这3个域。有的内核如果在编译的时候选择了CONFIG_MODULE_SIG_FORCE宏,那么没有签名的模块都是拒绝加载的。

2.2.2 模块编程可以使用的内核组件

1.workqueue

Linux下的工作队列是一种将工作推后执行的方式,其可以被睡眠、调度,与内核线程表现基本一致,但使用起来又比直接使用内核线程简单,一般用来处理任务内容比较动态的任务链。每个workqueue都可以添加多个work(使用queue_work函数)。

系统有默认的workqueue内核线程,然而用户可以自己定义workqueue,每一个workqueue都有对应的内核线程,但不是每一个workqueue都活跃到和其他workqueue所需要的资源一样的程度。再考虑到workqueue在使用过程中的一些其他问题,内核开发者实现了一个内核线程池,将后台承载的线程动态地绑定到workqueue上,这样就不需要每一个workqueue都创建自己的内核线程了,这个机制叫作cmwq(Concurrency Managed Workqueue)。示例如下:

以上是一个最简单的workqueue内核模块,除了可以使用create_singlethread_workqueue创建workqueue之外,还可以使用create_workqueue创建,此内核会为每一个CPU创建一个线程来执行workqueue。这两个是常用的“老式”的自己控制线程数的接口,cmwq使用alloc_workqueue来使用线程池创建workqueue。

2.中断系统和tasklet

Linux中的中断分为3个层次。最低的层次是在源代码arch目录下与各个平台相关的代码,一般位于平台代码下面的irq.c文件中,该部分代码直接与硬件相关,最后都要调用do_IRQ(__do_IRQ)进行执行。

do_IRQ就是中断系统的中层,其根据下层传来的中断号找到对应的中断处理函数,处理多CPU访问和中断重入问题,然后调用真实的中断处理函数,也就是中断的上层。但是这里内核做了区别,如果内核判断中断发生了嵌套(同时发生的中断很多)或者有其他的高时间成本的需求,则将中断处理函数以内核线程的形式(软中断)运行,否则直接运行。中断的最上层则与各个中断的具体功能相关。

tasklet一般专用于中断,因为中断不能阻塞,所以耗时较长的操作都交给tasklet在中断上下文之外调度执行,基于软中断实现。软中断被内核直接使用,但是如果用户模块想要直接使用则会非常难,因为需要考虑在不同CPU上的调度问题,所以软中断是锁密集型的机制。内核线程ksoftirqd专门用来调度软中断,而模块开发的时候希望使用这种软中断的延时执行机制,就可以调用内核封装好的tasklet装置。

中断系统是一个非常复杂的子系统,除非深度的内核开发者,例如比较细节的多CPU中断、中断亲和度、中断域等概念都是不太容易接触到的。其中中断亲和度常被运维人员用于锁定应用性能。

如图2-3所示是可能的中断号,在每一个中断号下面的文件都可以进行中断亲和度的绑定,将特定的进程绑定到特定的中断号上,这样进程不容易被抢占。

图2-3 YiDUFQI+HndHohDMuUiCFVCAstY6RAYDi7/2T7tmu7EOdOfMLa7jnkJwovnP5wPF

点击中间区域
呼出菜单
上一章
目录
下一章
×