测试系统准备

  • 使用主流内核,供应商内核可能会修改内核API
  • 在系统中配置好内核树,2.6版本之前只需要头文件即可,

hello world模块

应该最少包含module_init module_exit和MODULE_LICENSE

内核模块对比应用程序

  • 应用程序从头至尾顺序处理,内核模块是提供了一些事件处理函数的集合,事件发生时调用模块中的函数
  • 退出函数不同,应用程序退出后会释放资源,内核模块在module_exit必须清理所有资源,不然会保留到系统重启
  • 应用程序可以使用其他库中的函数,而模块只能使用内核输出的函数
  • 处理错误,应用程序的段错误是无害的,内核错误至少会杀掉当前进程

内核空间和用户空间

  • CPU高级别操作运行在内核态,低级别操作运行在用户态
  • 用户空间到内核空间切换,当发生系统调用或者被硬件中断挂起。系统调用发生时仍然在调用进程的上下文中,而中断处理程序不和任何程序相关。
  • 模块是内核功能的扩展,运行在内核空间。驱动运行两种任务:系统调用和中断处理。

内核的并发

  • 来源:多进程调用,中断处理程序是异步的,定时器等异步程序,多处理器,支持并发的单处理器。
  • 原则:代码必须是可重入的,共享资源的保护,2.6代码中不能假定它在一段代码上持有处理器(时刻都会让出CPU)。

当前进程

  • 通过current全局项,有一个指针字段task_struct。
  • current不是一个真正的全局变量,他是一种快速访问当前进程的机制。
    几个别的细节
  • 堆栈,用户态堆栈很大,内核堆栈很小,可能只有一页,而且是所有内核空间调用连共享,因此不要声明大的自动变量,而在需要的时候动态分配。
  • 双下划线开头的函数是一个底层接口组件,谨慎使用。
  • 内核代码不能进行浮点运算,支持浮点运算将使每次进出内核空间时保存和恢复浮点计算器状态,额外的负担不值得

编译和加载

编译模块

准备工作:

  • 编译工具的版本要正确(Documentation/Changes中说明),太旧和太新都可能有问题,内核对编译器版本进行了假设
  • 建立源码树
  • makefile
    在这里插入图片描述

加载和卸载模块

  • insmod加载模块的数据段和代码段到内核,接着执行一个类似ld的操作,把模块中未解决的符号链接到内核的符号表中。但不是连接器,内核不修改模块的磁盘文件,而是内存内的拷贝。insmod接收很多命令行的选项,安排值给模块中的参数。加载时配置
  • modprobe发现模块引用了当前内核没有的符号,modprobe在当前模块搜索路径中搜索其他模块,找到之后将他们加载到内核。
  • rmmod从内核中移除模块,如果正在被使用则无法移除。
  • lsmod读取/proc/modules获取当前加载的模块列表,也可以在/sys/module的sys文件系统中找到

版本依赖

  • 模块是紧密结合到一个特定内核版本的数据结构和函数原型上的
  • 模块见到的内核接口可能在不同版本之间有很大差异
  • 修改上面makefile文件中的KERNELDIR变量

平台依赖

  • 更新的处理器(使用正确的模式)还能够处理36位(或者更大)的物理地址,从而允许处理器寻址高于4G的物理地址。处理器寻址过程是怎么样的,物理地址的范围?
  • 加载模块时,内核会检查处理器相关的配置选项以便确保模块匹配于运行中的内核。如果模块在不同选项下编译,则不会装在模块。
  • 编写一个驱动程序用于一般性的发布,则最好考虑如何支持可能的不同处理器变种。
    • 最好的办法是用GPL兼容许可证来发布自己的驱动程序,并将其贡献给内核主分支
    • 以源码的形式及一组用于编译的脚本
    • 以二进制的方式,则需要检查目标发行版提供的不同内核,并为每个内核提供模块的一的版本

内核符号表

  • insmod使用公共内核符号表来解析模块中未定义的符号。包含所有全局内核项(函数和变量)的地址。当模块被装入内核后,它所导出的任何符号都会变成内核符号表的一部分。
  • 模块层叠技术。目的是模块化及复用。
  • 在当前目录安装模块仍然只能使用insmod,modprobe只能从标准的已安装模块目录中搜索需要装入的模块。
  • 导出符号:
    + EXPORT_SYMBOL(name);
    + EXPORT_SYMBOL_GPL(name);_GPL版本是的要导出的符号只能被GPL许可证下的模块使用。
    + 符号必须在模块文件的全局部分导出,不能在函数中导出。这是因为上面的宏将被扩展为一个特殊变量的声明。
    + 该变量是全局的,该变量将在模块可执行文件的特殊部分中保存,装在时,内核通过这个段来寻找模块导出的变量。

预备知识

  • 可装在模块必须包含头文件
    #include <linux/module.h> 可装载模块需要的大量符号和函数定义
    #include <linux/init.h> 目的是指定初始化和清楚函数

  • 大部分模块还包含moduleparam.h,可以在装在模块时向模块传递参数。

  • 不是严格要求的MODULE_LICENSE行
    GPL,任意版本的GNU通用公共许可证
    GPLv2 GLP版本2
    GPL and additional rights GPL及附加权利
    Dual BSD/GPL GPL/BSD双许可证
    Dual MPL/GPL GPL/MPL双许可证
    Proprietary 专有
    没有显式设置则内核假定为专有,

  • 其他描述性定义
    MODULE_AUTHOR 作者
    MODULE_DESCRIPTION 简短描述
    MODULE_VERSION 修订号 ,管理参考 module.h中注释
    MODULE_ALIAS 别名
    MODULE_DEVICE_TABLE 高速用户空间模块所支持的设备

  • 可以出现在函数以外的任何地方,内核编码习惯是放在文件的最后

  • 初始化
    初始化函数应该被声明为static
    __init或__initdata标记可选,但应该使用,用来暗示内核,在初始化阶段使用,装在完成后丢掉,可以把占用内存释放,
    不要在初始化之后仍然使用的函数或数据结构上使用__init标记
    __devinit或__devinitdata,只有在内核未被配置为支持热插拔设备的情况下,这两个标记会被翻译为__init和__initdata
    模块可以注册不同类型设备,注册函数参数一般为用来描述设施和设施名称的数据结构指针,数据结构通常包含模块指针
    很多可注册的设施所支持的功能属于软件抽象范畴,而不与任何硬件直接相关

  • 清除函数
    __exit标记的函数只能在内核卸载或者系统关闭时被调用,如果模块内嵌到内核或者讷河的配置不允许卸载模块,则被标记为__exit的函数将被简单丢弃

  • 初始化过程中的错误处理
    注册失败后可以通过降低功能来继续运转,只要有可能模块应该继续向前并尽可能提供功能
    无法继续,则自行把出错之前的注册工作撤销掉,不撤销,内核处于不稳定状态
    通常使用goto撤销注册
    清除函数使用相反的注册顺序
    初始化函数中调用清除函数可避免代码重复,清除函数必须在撤销每项设施的注册之前检查它的状态,可以使用外部变量记录每个初始化步骤的成功
    如果在初始化函数中使用了清除函数__exit标记则不能使用

  • 模块装载竞争
    模块的初始化函数执行过程中,刚注册的设施可能会被立即使用,而不是等到初始化函数退出之后,因此,在支持某个设施的所有内部初始化函数完成之前,不要注册任何设施
    需要注意的问题,当初始化失败而内核的某些部分已经使用了模块所注册的某个设施时,不能直接撤销注册需要确认直到没有其他操作访问。

模块参数

  • modprobe可以从配置文件/ect/modprob.conf中读取参数
  • module_param三个参数:名称,类型,sysfs入口项访问许可掩码
  insmod hellop howmany=10 whom="Mon"
  static char *whom = "world"
    static int howmany = 1;
    module_param(howmany, int , S_IRUGO)
    module_param(whom, charp, S_IRUGO)

参数类型
bool true或者false
invbool 翻转其值
charp 字符指针值,
int long short uint ulong ushort
module_param_array(name,type,num,perm)
perm:0,sysfs不可见;S_IRUGO只读,S_IRUGO|S_IWUSR可读写,内核不会通知模块发生了修改,除非模块打算自己检测,通常不应该是可写的。

用户空间编写驱动程序

  • 好处:
    • 可以和整个C库连接。驱动程序就是用户程序,不需要再编写用户程序来实现策略(之前提到的方式:用户程序实现策略和内核驱动一起发布)
    • 使用通常的调试器调试
    • 如果用户空间驱动程序挂起,简单杀掉
    • 用户空间驱动使用的内存空间可以被换出,不经常使用也不会占用内存
    • 如果设计良好仍然支持对设备并发访问
    • 如果必须编写闭源代码,可以更容易避免因为修改内核接口导致的不明确许可问题
    • 例子:libusb gadgetfs
    • 实现:用户空间的驱动程序被实现为一个服务器进程,其任务是替代内核作为硬件控制的唯一代理。客户应用程序可连接到该服务器并和设备执行实际的通信。这就是X服务器的本质。
  • 缺点:
    • 中断在用户空间中不可用
    • 只有通过mmap映射/dev/mem才能直接访问内存,但只有特区额用户才可以执行这个操作
    • 只有在调用ioperm或iopl后才可以访问io端口,并不是所有平台都支持这两个系统调用,并且访问/dev/port可能非常缓慢,也需要特权用户
    • 响应时间很慢,数据传递需要上下文切换
    • 驱动程序可能会被患处内存,响应时间更慢。使用mlock可以解决,但由于连接其他库,需要占用更多内存,而且也需要特权用户
    • 不能处理重要设备,包括网络和块设备
  • 适用:准备处理一种新的不常见的硬件时,避免挂起整个系统,先在用户空间完成,后再封装到内核中。

如何构造安装自己的内核?

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐