首先会在第2章对内核模块做一个宏观上的介绍, 并给出一个demo, 让我们可以快速上手体验一下如何编译使用内核模块.
接下来会在第3章对内核模块的细节做更多详细的分析, 以便我们能深入理解内核模块, 并编写出专业的内核模块. 这一章需要反复阅读理解. 可能过段时间或者遇到具体项目的时候, 还需要拿出来再重新阅读.
最后第4章是驱动设计指导规范, 它是一个提纲性质的, 提醒我们在编写内核模块时的主要注意事项. 我们在做具体项目的时候, 可以查询此章节获得一个快速指引, 如果你对快速指引的细节遗忘了, 则需要重新理解第3章的内容.
ARM板上Linux的启动顺序一般是boot->kernel image->filesystem.
Kernel image是一个文件, 它的名字一般叫做zImage或uImage.
我们是如何编译出kernel image的呢? 大致的步骤是: 首先获取内核源代码; 然后运行make menuconfig命令对源代码进行配置, 所谓配置就是对内核的功能进行定制, 比如需要LCD的功能, 就把LCD相关的代码配置为Y, 不需要音频播放的功能, 就把音频相关代码配置为N, 还有一类可以配置为M; 配置完成后, 执行make命令, 就会得到我们需要的kernel image文件.
在执行make的过程中, 所有配置为Y的代码都会编译链接到kernel image里面, 配置为Y的代码越多, 生成的image就越大; 所有配置为N的代码都不会编译; 所有配置为M的选项都会生成对应的.ko文件, 这个.ko文件就是内核模块.
内核模块与kernel image的地位是对等的, kernel image里面包含了内核的一些固定功能, 比如LCD 显示, 触摸等; 内核模块用来扩展内核的功能, 比如一个USB 蓝牙设备的lanya.ko文件. 内核模块可以被动态的加载到内核, 比如把lanya.ko加载到内核, 内核就具备了蓝牙的功能; 同样内核模块也可以被动态的从内核中卸载, 比如卸载lanya.ko, 内核就失去了蓝牙的功能.
那么内核模块以及模块的这种动态加载的机制有什么好处呢?
? 首先, 模块本身不被编译进kernel image, 从而控制了kernel image的大小
? 其次, 一旦模块被加载, 它就和kernel image中的其他部分完全一样, 从而扩展内核的功能
? 在调试驱动的时候, 动态加载的机制会很方便, 你只需要重新编译内核模块, 并重新加载它即可; 而不需要去重复 ”编译kernel image, 烧录kernel image, 重启kernel image” 这一漫长的过程.
除了2.1节中提到的在make menuconfig的时候配置为M的方式来编译内核模块(这种方式, 模块代码放置于内核源码的某个目录下), 还有一种方法可以编译模块.
我们可以编写一个单独的模块代码, 不把它放置在内核源码树下, 只需要在编译模块时候, 指定内核树的路径即可. 这种方式在内核已经运行在某个机器上的时候会很方便.
本小节的Demo就是采用的后面这种方式.
找一个运行Linux系统的机器, 比如运行ubuntu的PC, 或者运行debian的树莓派, 做如下操作
? 获取模块代码
git clone https://gitlab.com/study-linux/building_running_modules.git
? 编译模块
cd building_running_modules && make
? 装载模块
sudo insmod HelloWorld.ko
装载完毕后运行dmesg命令, 会在最后看到如下信息
? 卸载模块
sudo rmmod HelloWorld
卸载完毕之后运行dmesg命令, 会在最后看到如下信息:
本小节将更详细的介绍什么是内核模块, 主要是把内核模块和应用程序做对比.
先说明一下下面每个标题的设计思想, 标题的作用是给出大纲, 强调重点. 当我们细致的理解完这篇文章之后, 过一段时间, 有些细节可能就忘了. 当我想回想这些细节的时候, 需要把文章从头到尾在读一遍吗? 可以是可以, 不过太浪费时间了, 而且我们已经细致的理解过此文, 只需要有个大纲, 就能想起来里面的细节.
因此下面每一个小节的标题, 都代表着内核模块与应用程序的不同之处, 打开导航栏, 阅读这些标题, 能快速了解或者回想起一些东西. 有的标题有点长, 因为我们想更清楚的表达标题的意思, 把导航栏往右多拖一点, 这样你就能看清所有的标题.
这里简单介绍一下内核空间与用户空间, 更全面的解释会放在Linux核心子系统关于进程调度和内存管理的文章中. 内核代码和用户空间代码都运行在虚拟地址中, 32位的CPU, 虚拟地址总共有4G. 内核代码使用3G ~ 4G的空间, 称作内核空间; 应用程序使用0 ~ 3G的空间, 称作用户空间.
除了地址空间不一样, 优先权等级也不一样. 内核空间的代码运行在最高级别, 在这个级别中可以进行所有的操作; 而应用程序运行在最低级别, 在这个级别中, CPU控制着对硬件的直接访问以及对内存的非授权访问.
内核模块就是内核代码的一部分, 所以它也运行在内核空间.
应用程序运行在用户空间, 每当应用程序执行内核系统调用或者被硬件中断挂起时, 它就进入到内核空间. 实现系统调用功能的内核代码运行在进程上下文中, 它代表用户进程执行操作. 而处理硬件中断的内核代码和进程是异步的, 与一个特定的进程无关.
通常来讲, 一个内核模块(也叫驱动程序), 需要处理上述两类任务, 模块中的某些函数作为系统调用的一部分执行, 其他函数负责中断处理.
基于上面的概念, 我们也可以理解用户层的2个进程之间是不能交换数据的, 也就是不能通信. 因为每个进程都有一个0 ~ 3G的虚拟地址空间, 一个进程没有办法给另外一个进程传递一个地址从而让另一个进程从这个地址获取数据, 因为同一个虚拟地址在不同的进程中会被映射到不同的物理地址, 所以没办法交换数据. 如果两个进程要通信, 需要通过内核, 因为不同的进程看到的是同一个内核空间, 相同的内核空间虚拟地址会被映射到同一块内存;进程1通过系统调用进入内核空间, 在内核空间的某个地址放一个数据, 然后进程2也进入内核空间, 从这个地址拿走这个数据, 从而完成通信.
比如把每个城市想象为一个进程, 每个城市的地铁想象为该进程的虚拟地址. 规定普通城市地铁命名只能从1-10, 首都的地铁命名从10 – 15. 那么你在城市A的地铁2号线的某个座位上放一个包包, 让城市B的人去地铁2号线的那个座位上取这个包包, 等他跑到他的2号线上一看, 显然没有这个包. 要把包交给他, 你需要到10号线的某个位置放下这个包, 然后告诉他去10号线去拿. 当然, 只有进入首都, 才能看见10号线.
所以可能会有多个进程来调用我们的内核模块, 内核代码可以通过访问全局项current<在asm.current.h中定义>来获得当前进程. current是struct task_struct类型的指针, 通过current, 我们可以得到进程的详细信息, 比如命令名 current->comm, 进程ID current->pid.
什么叫事件型驱动? 比如一个小孩很懒, 早上起来坐着什么也不动, 他妈妈让他刷牙他就去刷牙, 让他吃早餐他就去吃早餐, 自己不会主动去做什么, 而是别人让他做什么他就做什么, 这种就叫事件型驱动. 代码的设计中也经常会遇到这种思路, 最常见的比如一个消息处理程序, 当有人往它的消息队列里面发送一个消息时, 他就取出这个消息, 判断是什么消息, 然后处理该消息. 这个程序跟那个小孩一样, 他们都具备做某些事情的能力, 但是不会主动做, 而是等待事件去驱动它做.
内核模块就是这样的, 在加载的时候, 它会告诉内核: ”我来了, 我可以处理XXX这些事情”, 然后它就不动了, 等内核需要用到它的某个功能时, 就会调用它提供的接口; 卸载的时候, 它会告诉内核: “我走了, 以后XXX这些事情不要在找我”.
大多数小规模及中规模的应用程序, 都不是事件驱动的, 它们从头到尾执行完所有的代码, 然后退出. 某些应用程序也会设计成事件驱动的方式, 但是它与内核模块任然有一个主要的不同: 应用程序在退出时, 可以不管理资源的释放或其它清除工作; 但内核模块的退出函数必须仔细撤销初始化函数所做的一切, 否则在系统重启之前这些东西就会残留在内核中.
作为程序员, 我们知道应用程序可以调用它并未定义的函数, 这是因为连接过程能够在某个库中找到这个程序. 例如, 定义在libc中的printf函数就可以被任何应用程序调用.
但是内核模块仅仅被链接到内核, 它只能调用内核导出的那些函数, 而不存在任何可以链接的函数库. 例如, printk就是内核导出的一个函数, 我们在编写内核代码时, 只能使用printk, 而不能使用printf.
要查看内核导出了哪些函数, 你可以通过cat /proc/kallsyms命令获取, 或者在内核源代码中, grep EXPORT_SYMBOL
内核编程与应用程序编程的另一个区别是, 内核编程必须时刻记住, 即使是最简单的内核模块, 都要考虑并发的问题.
除去多线程应用, 大部分应用程序都是顺序执行, 不需要关心因为其他一些事情的发生会改变它们的运行环境. 内核代码却不会在这样一个简单的世界中运行.
有几方面的原因促使内核编程必须考虑并发问题. 首先, 可能有多个并发的进程会同时使用我们的驱动程序; 其次, 大多数设备能够中断CPU, 中断处理程序是异步的, 可能驱动程序正在准备把变量A赋值为1时, 中断产生了, 中断系统调用该驱动程序的处理函数要求把A赋值为4; 还有在多处理器系统上, 可能同时不止一个CPU运行我们的驱动程序; 最后, 2.6中的内核代码已经是可抢占的, 意味着即使在单处理器上也存在类似多处理器系统中的并发问题.
所以, Linux内核代码(包括驱动程序代码)必须能够同时运行在多个上下文中, 内核数据结构需要仔细设计才能保证多个进程分开执行, 访问共享数据的代码也必须避免破坏共享数据.
对编写正确的内核代码来说, 优良的并发管理是必须的, 我们需要更全面的知识才能做好这件事情, 会用一篇单独的文章来专门讨论并发问题以及内核中用于并发管理的原语.
简单说一下堆和栈的区别, 局部变量保存在栈中, 函数调用时的参数, 某个函数里面调用另一个函数时本函数的上下文, 也是保存在栈中. 全局变量和静态变量是放在静态存储区的(它们的地址是编译期间决定的). malloc或者new出来的内存就是在堆里面, 需要程序员自己管理和清除.
应用程序在虚拟内存中布局, 并且有一块很大的栈空间. 内核的栈空间很小, 可能只和一个4096字节大小的页那样小, 我们的内核模块代码与整个内核空间代码一起共享这个栈, 因此需要留意对栈的使用. 一般大的数据结构, 最好用动态分配的方式.
内核的API或者说内核的导出函数中, 有很多以两个下划线(_ _)做为前缀的函数, 这种名称的函数通常是一些底层组件, 应当谨慎使用, 否则后果自负.
一般来说, _ _开头的函数, 都会有某个不带_ _前缀的函数封装它, 我们最好使用这些函数.
内核代码不能实现浮点运算. 如果打开了浮点支持, 在某些架构上, 需要在进入和退出内核空间时保存和恢复浮点处理器状态. 这种额外的开销没有任何价值, 内核代码也不需要浮点运算.
所以printf可以打印浮点数, 但是printk却不支持浮点数.
内核编程在许多方面区别于用户空间编程, 在后期学习的过程中, 我们将在此小节持续添加这些区别.
本文的开头快速体验了如何编译一个内核模块, 你只需要敲一个make命令, 然后就能得到结果. 但是实际上背后的事情不止这么简单, 本节会详细介绍模块编译的一些细节.
在开始编译模块之前, 你需要做一些准备工作.
首先, 检查编译器版本是否正确, 模块工具和其他必要的工具是否齐备, 内核文档目录Documentation/Changes文件列出了需要的工具版本. 不要使用老工具, 同样也不要使用更新版本的工具.
其次, 需要准备内核树并构造内核树.
分两种情况来讨论这个内核树:
第一种情况是我们在做板子的驱动开发时, 比如我们有一个树莓派的板子, 要编译出一个内核在板子上运行, 我们会从树莓派官方的github仓库clone内核源代码到某个目录, 进入该目录配置并编译这个内核. 这个源代码就是内核树, 编译的操作就是构造内核树.
第二种情况是我们已经有了一个运行Linux系统的机器, 比如运行ubuntu的PC, 如果该机器没有安装内核树, 我们就需要安装并构造内核树. 具体的步骤如下:
? 运行命令 uname –r , 结果形如 : 3.2.0-48-generic
? 查看 /lib/modules 目录下是否有对应的目录, 形如 : /lib/modules/3.2.0-48-generic . 如果存在, 证明内核数已经安装了, /lib/modules/3.2.0-48-generic/build文件是一个链接, 指向内核源码树所在的目录; 如果没有, 则需要按照下面的步骤来安装并构造内核数.
? 运行命令 sudo apt-cache search linux-source , 查看一下可下载的源码包
? 选择一个对应的源码包安装, 形如 : sudo apt-get install linux-source-3.2.0
? 下载完后, 会在 /usr/src 下出现一个 linux-source-3.2.0.tar.bz2的压缩包
? 解压该压缩包 : tar jxvf linux-source-3.2.0.tar.bz2
? 解压完成后, 会出现一个新的目录 : /usr/src/ linux-source-3.2.0
? 进入该目录: make oldconfig && make && make bzImage && make modules && make modules_install
? 执行完毕之后, 就会在/lib/modules下看见相应的目录了
准备工作完毕之后, 就可以开始编译自己的内核模块了.
假设你已经写好了一个很简单的hello.c, 想把它编译成一个模块
只需要在hello.c所在的目录下新建一个Makefile文件, 在里面加上一句 obj-m := hello.o
然后执行编译命令 make –C ~/kernel-2.6 M=`pwd` modules
上述命令首先改变目录到-C选项制定的位置(假设内核源码树在~/kernel-2.6), 该目录保持有内核的顶层Makefile文件, M=选项让顶层Makefile在构造modules目标之前, 返回到模块源码所在的目录, 然后, modules目标指向obj –m变量中设定的模块.
熟悉Makefile的人可能注意到内核模块的Makefile非常简单, 跟Makefile的常见形式不一样, 这是因为内核的构造系统处理了其余的问题. 内核的构造系统非常复杂, 如果需要了解全貌, 可以阅读Documentation/kbuild目录下的文件.
obj-m = hello.o 表明有一个模块需要从目标文件hello.o中构造, 而该模块的名称就是hello.ko
如果我们要构造的一个模块名称为module.ko, 并由两个源文件生成(file1.c , file2.c), 则正确的Makefile可如下编写:
obj-m := module.o
module-objs := file1.o file2.o
每次都要敲上面一长串make命令很烦人, 我们可以把Makefile稍微设计一下, 第一个目的是只用敲一个make命令就能编译出模块; 第二个目的是模块代码不管放在哪里, 都能编译, 所谓”不管放在哪里”, 是指不管你放在源码树目录下, 还是放在源码树外面, 都能编译. 该Makefile: https://gitlab.com/study-linux/building_running_modules/blob/master/Makefile
# If KERNELRELEASE is defined, we‘ve been invoked from the
# kernel build system and can use its language.
ifneq ($(KERNELRELEASE),)
obj-m := HelloWorld.o
# Otherwise we were called directly from the command
# line; invoke the kernel build system.
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
default:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
endif
clean:
rm -f *.o *.ko
distclean:
rm -f *.o *.ko *.mod.c modules.* Module.*
如果我们把模块代码放在内核数目录下, 在内核数根目录下执行make命令, 则KERNELRELEASE就不是空, 执行if部分生成模块; 如果我们把模块代码放在内核数目录外面, 这个Makefile会被读取2次, 第一次是我们敲了make命令之后, KERNELRELEASE为空, 会执行else部分, else部分设定KERNELDIR, PWD, 然后执行default下$(MAKE)命令(Makefile中make命令被参数化成了$(MAKE) ), 然后这个make命令会找到内核树, 然后第二次调用该Makefile, 这一次, KERNELRELEASE就不为空了, 执行if部分生成模块. KERNELRELEASE是在内核数顶层Makefile中初始化的.
模块构造成功之后, 下一步就是装载模块.
insmod可以用来完成这项工作, insmod程序和ld有些类似, 它将模块的代码和数据装入内核, 然后使用内核符号表解析模块中任何未解析的符合. 与链接器不同之处是, insmod不会修改内核的磁盘文件, 仅仅修改内种的副本, 意味着, 重启之后, 需要重新insmod.
insmod可以接受一些命令行选项(man insmod), 并且可以在模块链接到内核之前给模块中的整型和字符串型变量赋值. 也就是给模块传递参数, 我们会在后面详解讨论如何编写模块代码让它可以接受参数.
insmod依赖于定义在kernel/module.c中的一个系统调用, 函数sys_init_module给模块分配内核内存, 内核代码中有且只有系统调用的名字前带有sys_前缀. 如果有兴趣了解inmsod的细节, 可以去网上找一下, 这里不过多的讨论.
rmmod用于从内核中移除模块. 注意如果内核认为模块任然在使用状态, 或者内核被配置为禁止移除模块, 则无法移除该模块.
modprobe工具和insmod类似, 也可以用来装载模块, 不同之处在于modprobe会考虑要装载的模块是否引用了一些当前内核不存在的符号, 如果有, modprobe会在当前模块搜索路径中查找定义了这些符号的其他模块, 并同时装载这些模块. 也就是说modprobe会处理模块之间的依赖关系.
在使用modprobe时, 有以下注意事项:
modprobe是在/lib/module/`uname -r`下寻找加载的模块的, 并且modprobe需要一个最新的modules.dep文件, 这个modules.dep文件内容是各个模块之间的依赖等信息, 此文件是由depmod命令来更新的.
与insmod不同的是, modprobe后面跟的是模块的模块名, 而不是模块文件的名称, 也就是说不需要带.ko后缀.
modprobe使用的具体的步骤如下:
将编译好的模块放入/lib/module/`uname -r`下
用sudo depmod命令更新modules.dep文件
然后用sudo modprobe HelloWorld命令装载模块
lsmod列出当前装载到内核中的所有模块, 它是通过读取/proc/modules虚拟文件来获得这些信息的, 有关当前已装载模块的信息, 也可以在sysfs虚拟文件系统的/sys/module下找到.
了解完上面的信息, 我们可以开始自动动手编写内核模块了. 宏观上的思路是, 有一个c文件, 存放我们编写好的模块源代码, 有一个Makefile, 用于编译该c文件, 然后执行make命令编译出内核模块, 接下来就可以装载/卸载内核模块了.
本小节中, 我们会详解说明这个c文件该怎么编写, 内核是一个特定的环境, 对需要和它接口的内核模块代码有一些特殊的要求.
大部分内核代码中都要包含相当数量的头文件, 以便获得函数、数据类型和变量的定义. 有几个头文件是专门用于模块的, 因此必须出现在每个可装载的模块中. 因此, 所有的模块代码中都包含下面两行代码:
#include <linux/module.h>
#include <linux/init.h>
另外, 模块应该指定代码所使用的许可证. 因此我们需要包含MODULE_LICENSES行:
MODULE_LICENSES(“GPL”);
内核能够识别的许可证包括: “GPL”, “GPL v2”, “GPL and additional rights”, “Dual BSD/GPL”, “Dual MPL/GPL”, “Proprietary(专有)”. 如果一个模块没有显示地标记为上述内核可识别的许可证, 则会被假定是专有的, 模块被加载的时候, 将收到内核被污染(kernel tainted)的警告.
除了上述的LICENSES, 模块中也可选择性包含如下描述性定义:
MODULE_AUTHOR : 描述模块作者
MODULE_DESCRIPTION : 用来说明模块用途的简短描述
MODULE_VERSION : 代码修订号, 有关版本字符串的规则, 参考linux/module.h中的注释
MODULE_ALIAS : 模块的别名
MODULE_DEVICE_TABLE : 告诉用户空间模块所支持的设备
上述MODULE_申明可以出现在源文件中函数以为的任何地方, 一般的做法是把这些申明放在文件的最后
static int _ _init initialization_function(void)
{
/* Initialization code here */
}
module_init(initialization_function);
|
|
static |
C语言中static修饰函数时, 是隐藏的意思, 表示该函数只能在本文件中使用 |
int |
加载函数的返回值是整型, 0表示成功, 若失败, 应返回错误代码(linux/errno.h), 错误代码是一个负值, 如-ENOMEM |
_ _init |
它对内核是一种暗示, 表明该函数仅在初始化期间使用. 内核中, 所有_ _init标识的函数在链接时都放在.init.text这个区段. 此外, 所有的_ _init函数在区段.initcall.init中还保存了一份函数指针, 初始化时内核会通过这些函数指针调用这些_ _init函数, 并在初始化完成后释放init区段(包括.init.text, .initcall.init等). 模块被装载之后, 装载器就会丢掉加载函数, 以释放该函数占用的内存. 该选项是可选的, 不一定要加, 但是推荐加它, 以节省内存. 不过注意, 不要在结束初始化之后任要使用的函数或者数据结构(用 _ _initdata标记)上使用这个标记. lsmod可以查看模块占用的内存, 我们可以做个对比实验. |
module_init |
必须使用它把某个函数标记为加载函数 |
|
|
加载函数也被称为初始化函数, 我们在模块的初始化函数里面一般是向内核进行注册(调用内核提供的register_xxx函数), 以便实现某些功能. 就像告诉内核, 我有XXX这些功能, 实现这些功能的函数分别是YYY, 内核在需要的时候就会调用这些函数. 例如, 我们在模块代码中注册一个字符设备驱动, 随后该模块就能响应open, read等系统调用.
内核代码中大部分注册函数名字都带有register_前缀, 用grep register_可以方便的看到有哪些注册函数.
在编写模块初始化代码时, 要时刻铭记初始化过程可能会任意位置失败: 即使是最简单的内存分配, 都可能存在内存不足的情. 因此模块代码必须始终检查返回值.
如果遇到错误情况, 首先要判断模块是否可以继续初始化. 通常, 某个注册失败后可以通过降低功能来继续运转.
如果发生某些错误导致无法继续初始化, 则要将出错之前的任何注册工作撤销掉. 否则内核中会因为包含一些并不存在的代码的指针而不稳定.
错误恢复的处理有时用goto比较有效, 它可以让逻辑更清晰. 例如:
int _ _init my_init_function(void)
{
int err;
/* registration takes a pointer and a name */
err = register_this(ptr1, "skull");
if (err) goto fail_this;
err = register_that(ptr2, "skull");
if (err) goto fail_that;
err = register_those(ptr3, "skull");
if (err) goto fail_those;
return 0; /* success */
fail_those: unregister_that(ptr2, "skull");
fail_that: unregister_this(ptr1, "skull");
fail_this: return err; /* propagate the error */
}
另外一种方法是, 在初始化出错的时候调用模块的卸载函数, 在卸载函数中仅仅回滚初始化代码中已经成功完成的步骤. 这种方法需要更多的代码和CPU时间, 在追求效率的代码中推荐使用goto.
当初始化和清除工作涉及到很多设备时, goto用起来会让人有点头疼, 而且在增加/删除某一项注册工作时, 对goto的修改需要非常小心. 这个时候, 我们就可以考虑出错时调用模块卸载函数这种方法, 下面是一个例子:
struct something *item1;
struct somethingelse *item2;
int stuff_ok;
void my_cleanup(void)
{
if (item1)
release_thing(item1);
if (item2)
release_thing2(item2);
if (stuff_ok)
unregister_stuff( );
return;
}
int _ _init my_init(void)
{
int err = -ENOMEM;
item1 = allocate_thing(arguments);
item2 = allocate_thing2(arguments2);
if (!item2 || !item2)
goto fail;
err = register_stuff(item1, item2);
if (!err)
stuff_ok = 1;
else
goto fail;
return 0; /* success */
fail:
my_cleanup( );
return err;
}
同时我们需要留意所谓的装载竞争问题. 一旦我们调用内核注册函数注册了某个功能之后, 内核的某些部分可能会立即使用我们刚刚注册的功能. 换句话说, 模块的初始化函数还在运行的时候, 内核就可能会调用我们的模块. 因此, 必须铭记, 在某个功能的所有初始化动作完成之前, 不要注册这个功能.
打个比方, 我们想注册一个摄像头驱动, 假设我们先调用内核函数注册了这个驱动, 然后接着在去初始化摄像头的一些硬件状态, 这种逻辑就有问题, 你有可能发现你做的摄像头驱动有时候能用, 有时候不能用, 这个时候你就抓狂了…
另外需要注意, 如果我们已经成功了注册了某项功能, 但是在接下来的初始化代码中发生了错误, 内核可能已经在调用你注册成功的那些功能了. 我们应该尽量避免这种情况的出现, 万一出现了, 在处理错误情况的时候, 需要仔细处理内核其他部分可能正在进程的操作, 并等待这些操作的完成.
模块的卸载函数也是内核模块代码必须的, 卸载函数一般如下:
static void _ _exit cleanup_function(void)
{
/* Cleanup code here */
}
module_exit(cleanup_function);
解释一下上面代码片段中的几个重点:
|
|
static |
C语言中static修饰函数时, 是隐藏的意思, 表示该函数只能在本文件中使用 |
void |
卸载函数没有返回值 |
_ _exit |
修饰该函数仅用于模块卸载, 只能在模块被卸载或者系统关闭时调用. 如果模块被直接内嵌到内核, 或者内核的配置不允许卸载模块, 则被标记为_ _exit的函数将被简单的丢弃. 所以, 如果我们初始化过程的错误处理时调用到了卸载函数, 则不应该把它标记为 _ _exit. |
module_exit |
必须使用它把某个函数标记为卸载函数 |
|
|
通常来说, 卸载函数要完成与模块加载函数相反的功能, 例如:
若加载函数注册了XXX, 则卸载函数要注销XXX
若加载函数动态申请了内存, 则卸载函数要释放这些内存
若加载函数申请了硬件资源(中断, DMA通道, I/O端口等), 则卸载函数要释放这些资源
若加载函数开启了硬件, 则卸载函数一般要关闭硬件
一般卸载函数中, 清除/释放资源的代码, 最好保持与初始化函数中相反的顺序, 也就是说最后申请的资源, 最先释放. 虽然这不是强制的.
我们可以用”module_param(参数名, 参数类型, 参数读/写权限)”为模块定义一个参数, 或者用module_param_array(数组名, 数组类型, 数组长, 参数读/写权限).
例如下列代码定义了一个整型参数和一个字符指针参数:
#include <linux/moduleparam.h>
static char *whom = "world";
static int howmany = 1;
module_param(howmany, int, S_IRUGO);
module_param(whom, charp, S_IRUGO);
static int int_array[6] = {1, 2, 3, 4, 5, 6};
static int int_array_num;
module_param_array(int_array, int, &int_array_num, S_IRUGO);
解释一下模块参数的几个重点:
在编写内核模块代码时, 会为每个模块参数指定一个默认值, 该默认值可以在装载模块时通过命令insmod或者modprobe修改, 例如:
insmod hello.ko howmany=10 whom=”Mom” int_array=3,4
modprobe还可以从它的配置文件(/etc/modprob.conf)中读取参数值
当模块被装载之后, 如果模块参数”读/写权限”设置恰当, 我们也可以通过sysfs来获取或者修改参数值. 注意, 如果一个参数通过sysfs被修改, 这个修改动作会立即生效, 但是内核没有任何机制来通知模块某个参数被修改了.
“/proc/kallsyms” 文件对应着内核导出的符号表, 它记录了符号以及符号所在的内存地址.
内核模块可以调用内核导出的符号(也叫函数), 同时内核模块自己也可以导出符号供其他模块使用.
可以用如下宏导出符号到内核符号表:
只有在内核模块代码中声明了使用GPL LICENSES, 才能使用EXPORT_SYMBOL_GPL导出的符号
在Linux 2.6中, 模块使用计数一般会由设备模型系统自动处理, 主要目的是为了防止设备正在被使用时, 卸载此设备对应的模块.
比如我们编写了一个内核模块, 在模块代码里面注册了一个字符设备, 当系统开始使用这个字符设备时, 系统会自动调用try_module_get(dev->owner)去增加此设备对应模块的使用计数, 当不在使用此设备时, 内核使用module_put(dev->owner)减少此设备对应模块的使用计数. 当使用计数不为0时, 不能卸载模块.
这里只做简单介绍, 以后有需要在细说.
Linux内核的版本在不停的变化, 不同的版本, 导出的内核符号可能不一样, 内核符号的参数也可能不一样. 除了这些, 可能还有别的区别.
如果我们打算编写一个内核模块, 让它可以在不同版本的内核上编译, 则需要用到一些#ifdef来组织自己的代码.
linux/version.h中会定义内核的版本, 这个头文件会自动包含在linux/module.h中, 该头文件定义了下面这些宏:
|
|
UTS_RELEASE |
描述内核版本的字符串, 例如”2.6.10” |
LINUX_VERSION_CODE |
二进制数据, 版本发行号中的每一部分对应一个字节. 例如: 2.6.10对应的LINUX_VERSION_CODE是132618(即0x02060a) |
用于创建一个二进制的版本号, 例如, KERNEL_VERSION(2, 6, 10)代表132618. 这个宏在我们需要将当前版本和一个已知的版本比较时非常有用 |
通过上面的宏, 能够处理大部分的版本依赖问题. 但是不要在内核代码中胡乱使用#ifdef条件语句将整个驱动程序代码弄得杂乱无章. 最好的一个解决方法就是将所有相关的预处理条件语句集中存放在一个特定的头文件里面.
依赖于特定版本的代码应该隐藏在底层细节中, 高层代码可直接调用这些函数, 而无需关注底层细节. 用这种方式编写的代码便于阅读, 同时更为健壮.
每种CPU都有自己的独特特性, 内核设计者可以充分利用这些特性来达到目标平台上目标文件的最优性能.
这块主要是指内核代码可以根据不同的需求, 将CPU的某些寄存器指定为特殊用途, 从而发挥CPU的威力.
这一块目前不细说, 以后遇到了在来细讲.
如果遇到这种情况, 我们需要记住的是, 把这些底层细节进行隐藏, 提高程序的健壮性.
File |
Comment |
#include <linux/init.h> |
标示模块的初始化和清除函数的宏 module_init(init_function); module_exit(cleanup_function)
标示仅用于初始化/清除阶段的函数或数据 _ _init , _ _initdata _ _exit, _ _exitdata |
#include <linux/module.h> |
模块源代码中必须包含的头文件 MODULE_LICENSE MODULE_AUTHOR MODULE_DESCRIPTION MODULE_VERSION MODULE_DEVICE_TABLE MODULE_ALIAS |
#include <linux/sched.h> |
包含驱动程序使用的大部分内核API的定义, 包括睡眠函数以及各种变量申明 struct task_struct *current current代表当前进程 current->pid current->comm 当前进程ID和命令名 |
#include <linux/version.h> |
这个头文件会自动包含在linux/module.h中 内核版本信息的头文件 UTS_RELEASE LINUX_VERSION_CODE KERNEL_VERSION |
#include <linux/export.h> |
这个头文件会自动包含在linux/module.h中 EXPORT_SYMBOL(name); EXPORT_SYMBOL_GPL(name); |
#include <linux/moduleparam.h> |
这个头文件会自动包含在linux/module.h中 module_param(variable, type, perm) module_param_array(variable, type, num, perm) |
#include <linux/kernel.h> |
int printk(const char * fmt, …); |
vermagic.o |
内核源码树中的一个目标文件, 它描述了模块的构造环境 |
|
|
/sys/module /proc/modules |
包含当前已经装载模块信息的目录, 它是一个目录 这个文件是早期用法, 它是单个文件 |
/proc/kallsyms |
内核符号表文件 |
Tools |
Comment |
insmod |
加载模块, 可传递模块参数 |
rmmod |
卸载模块 |
加载模块, 同时会处理模块之间依赖关系. 可传递模块参数 需要将编译好的模块放入/lib/module/`uname -r`下, 并用depmod命令更新modules.dep文件 Modprobe后面跟的是模块的模块名, 而非模块文件名称 |
|
lsmod |
查看已加载的模块信息 |
modinfo |
modinfo hello.ko可以查看内核信息, 包括 模块作者 模块说明 模块所支持的参数 以及vermagic |
1. include相关头文件
2. 编写模块加载函数(必须)
3. 编写模块卸载函数(必须)
4. 模块LICENSES声明(必须)
5. 模块作者等其他信息, MODULE_AUTHOR等(推荐)
6. 模块参数(如果模块需要参数)
7. 模块导出符号(如果模块需要导出符号)
8. 考虑模块相关的版本依赖/平台依赖(如果需要考虑)
原文:https://www.cnblogs.com/jliuxin/p/14129346.html