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

2.1 Linux内核整体结构

在Linux内核的结构中使用了很多“约定俗成”的约定,这些约定大部分被C语言开发者所熟知,并被广泛应用到日常使用C语言的开发中。

Linux内核在层次定义中一般使用面向对象的结构体抽象方式,这种方式首先定义一个结构体(包含各种函数指针并且管理其列表),下层通过生成这样一个被定义的结构体,来将操作函数赋值给该结构体的对应域;然后调用上层的注册函数,将信息注册到上层,这样上层就可以用统一的函数调用不同的下层接口。这种方式使用了C语言,同时融入了面向对象的编程思路。一个典型的例子如下。

img
img

当上层调用一个下层的函数时,一般有3种返回值的方式:第1种,通过语言规定的函数返回值;第2种,通过传递指向结果指针的方式;第3种,使用回调函数,回调函数通常是由调用者提供的。

内核系统按照功能分类可以分成几个大型的子系统,其通过上下层通信的方式将内核各个子系统有效地区分了层级关系,使得系统架构更清晰。虽然这个方法并不一定是Linux首创的,但大量的C语言程序都受到这种风格的影响。

内核中的系统大体分为纵向系统、横向系统和独立子系统。

(1)纵向系统是指具体的功能模块,这些模块总体像一片森林,不同的功能树下面有很多的层次。例如一个用户端对USB文件的操作,要走完内核中的很多个层次(有文件系统层、缓存层、通用块层、SCSI层、USB层),每个层次的内部又分为多个子层次。但Linux内核尽量将一个层次内部的子层次数量控制在3个以内:为上层提供统一接口的接口层,实现主要逻辑的功能中间层,以及统一为下层不同驱动提供编程接口的驱动层。不同层次有不同的分工,按照顺序完成工作,一整条链路下来就是Linux的一个纵向的子系统。

(2)横向系统是被各个子系统所使用的、以统一的方式对外提供服务的组件。横向系统主要有固定点钩子和文件系统两种:固定点钩子如audit、trace、netfilter等;文件系统如group、proc、sys等。

(3)独立子系统是内核中功能相对独立的模块。比较大型的独立子系统主要包括调度、锁、信号、中断、DMA、时钟、内存管理、sysrq、驱动子系统、安全子系统、进程间通信等;还包括一些常用的比较小型的组件,主要包括workqueue、tasklet、打印等。

2.1.1 内核模块

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

有很多公司可以不必遵守GPL协议。例如,做NTFS文件系统内核模块驱动的Tuxera公司,其最著名的产品是用户端开源的NTFS文件系统驱动ntfs-3g,然而这个驱动的工作效率并不高,另外该公司还提供闭源的NTFS内核模块驱动,如果要使用该闭源驱动则需要购买。

模块的执行原理与其他功能组件类似,都是由内核约定好要调用的函数,再由模块开发者填充实现这些函数,在添加、关闭模块的时候内核模块调度系统就会执行用户注册的自己实现的函数。在模块开发中比较典型的是初始化函数和退出函数,模块代码示例如下。

img
img

可以看出module_init和module_exit就是注册约定函数的调用。模块内部定义的钩子函数是static,目的是只内部可见。__init和__exit是GCC的特性,回收明确表示无用的代码。标记为__init的函数会被放入.init.text代码段,这个代码段不会再被用到,在模块加载完后会被回收,以节省内存。上面代码的最后三行是三个宏,如果缺少MODULE_LICENSE宏的版权声明,那么在加载的时候内核就会告警“Warning: loading hello.ko will taint the kernel: no license”。

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

原则上内核模块在被使用的过程中不可以被卸载,但可以被强制卸载,或者找到所有使用它的单位按照顺序将其关闭或卸载,使模块的被引用计数变为0。而在加载的时候必须保证模块与运行中的内核相容。insmod和rmmod是用户端加载和卸载模块的常用命令。

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

模块签名有两层含义:一层是版本号;另一层是哈希签名。xor内核模块的modinfo输出如图2-1所示。

img

图2-1 xor内核模块的modinfo输出

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

xor内核模块是在压缩过程中会用到的一个内核模块,这个内核模块包含一个core_initcall的初始化顺序指定特性,对外只导出了一个xor_blocks函数。下面列举一个完整的内核模块,具体代码如下。

img
img
img
img

以上整个模块被声明为GPL,唯一的导出函数是xor_blocks(),其属性也是GPL。结尾的core_initcall()是模块的初始化函数,当模块加载的时候,需要先运行这个函数。当模块被内联编译进内核的时候,还需要指定模块被初始化的顺序,这个顺序就是由core_initcall()系列函数组成的,具体代码如下。

img
img

当模块之间存在依赖关系,为了一个模块必须在另外模块之前完成初始化,就需要使用带初始化顺序的初始化方法。这个初始化顺序也是Linux内核在启动时的内部初始化的顺序。

整个xor模块的原理就是先通过初始化函数选择一个计算函数,再通过暴露出来的xor_blocks()函数直接调用在初始化时选择的计算函数进行计算。

2.1.2 内核符号表

内核符号表是内核内部各个功能模块之间互相调用的纽带,各个模块之间依赖函数调用进行通信。加载的模块所导出的函数通过导出操作就可以被其他模块定位并调用,代码示例如下。

img
img

以上是摘自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的区别在于每个模块都可以声明自己遵守的协议。比如遵守GPL协议的模块,可以在自己的模块代码中添加:MODULE_LICENSE("GPL"),之后就能用另外一个模块调用被本模块封装之后的函数。只有设置了遵守GPL协议的模块才可以被EXPORT_SYMBOL_GPL定义导出的系统调用。

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

img

图2-2 内核当前符号表 xAtdhuPI+D+51B7qysyar10hL5u88x4Syu79oWZGhGIpqBvbn/+C2Mbqj9ujSbve

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