建立和运行一个简单的模块
1 源代码
hw.c文件
- #include <linux/kernel.h>
- #include <linux/module.h>
- MODULE_LICENSE("Dual BSD/GPL");
- static int __init init_hw(void)
- {
- printk(KERN_ALERT "Hello, I'm Octagram.\n");
- return 0;
- }
- static void __exit exit_hw(void)
- {
- printk(KERN_ALERT "Goodbye!\n");
- }
- module_init(init_hw);
- module_exit(exit_hw);
Makefile文件
- KERNELDIR ?= /lib/modules/`uname -r`/build
- PWD := $(shell pwd)
- CROSS_COMPILE ?=
- CC ?= $(CROSS_COMPILE)gcc
- obj-m := hw.o
- all:
- $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
- install: all
- clean:
- rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions *.markers *.symvers *.order
- .PHONY: modules modules_install clean
编译方法
make
编译结束后,会在文件夹下产生一个hw.ko文件。
2 模块的加载与卸载
使用insmod命令能在内核中插入模块,与其对应的是rmmod,能删除内核中模块。
$ sudo insmod hw.ko
$ sudo rmmod hw.ko
$ dmesg
打印出来的信息:
Hello, I'm Octagram.
Goodbye!
3 C源码注释
3.1 printk函数
printk 函数在 Linux 内核中定义并且对模块可用;它与标准C库函数printf 的行为相似。内核需要它自己的打印函数,因为它靠自己运行,没有C库的帮助。模块能够调用printk是因为在insmod命令加载了它之后,模块被连接到内核并且可存取内核的公用符号,而printk就是这样一个公用符号。
【原型】
int printk(const char * fmt, …);
该函数在“内核根目录/include/linux/printk.h”中被定义,其参数只有一个字符串,在上述源代码中使用了符号“KERN_ALERT”,这个符号也在“内核根目录/include/linux/printk.h”中被定义:
- #define KERN_EMERG "<0>" /* system is unusable */
- #define KERN_ALERT "<1>" /* action must be taken immediately */
- #define KERN_CRIT "<2>" /* critical conditions */
- #define KERN_ERR "<3>" /* error conditions */
- #define KERN_WARNING "<4>" /* warning conditions */
- #define KERN_NOTICE "<5>" /* normal but significant */
- #define KERN_INFO "<6>" /* informational */
- #define KERN_DEBUG "<7>" /* debug-level messages */
这些符号在打印信息时不会被显示出来,被称为日志级别,内核中共提供了八种不同的日志级别,未指定日志级别的printk()采用的默认级别是DEFAULT_MESSAGE_LOGLEVEL,这个宏在“内核根目录/kernel/printk.c”中被定义为整数 4,即对应KERN_WARNING。
通过读写“/proc/sys/kernel/printk”文件可读取和修改控制台的日志级别。查看这个文件的方法如下:
$ sudo cat /proc/sys/kernel/printk
6 4 1 7
显示的四个数分别表示当前控制台日志级别、未明确指定日志级别的默认消息日志级别(即上文中提到的DEFAULT_MESSAGE_LOGLEVEL)、最小(最高)允许设置的控制台日志级别(数字越小,级别越高)、引导时默认的日志级别。当printk()中的消息日志级别小于当前控制台日志级别时,printk的信息就会在控制台上显示。但无论当前控制台日志级别是何值,通过“/proc/kmsg”(或使用dmesg命令)总能查看。另外,如果配置好并运行了syslogd或klogd,没有在控制台上显示的printk的信息也会追加到“/var/log/messages.log”中。
可用下面的命令设置当前日志级别:
$ sudo echo 8 > /proc/sys/kernel/printk
$ sudo cat /proc/sys/kernel/printk
8 4 1 7
这样,所有的系统日志信息都将被打印到终端。
3.2 头文件
头文件“内核源码目录/include/linux/kernel.h”中包含了大量与内核有关的头文件,其中与printk函数有关的头文件printk.h就包含在其中。“内核源码目录/include/linux/module.h”则与建立模块有关。
3.3 MODULE_LICENSE("Dual BSD/GPL")
这句代码声明模块的许可证,不是严格要求的。内核认识的特定许可有:GPL(GNU通用公共许可的任何版本)、GPL v2、GPL and additional rights、Dual BSD/GPL、Dual MPL/GPL和Proprietary等等。除非在模块明确标识一个自由许可,否则内核就会假定它是私有的,内核在私有模块加载时会被“弄污浊”。内核开发者不会热心帮助在加载了私有模块后遇到问题的用户。
3.4 初始化函数
在C源码中,使用了一个函数:module_init(init_hw),该函数的参数是另外一个函数的指针,这个函数将告诉内核,模块初始化时该调用哪个函数。当使用命令insmod插入模块时,内核将自动调用指定的初始话函数,因此才会在日志信息中打印出“Hello, I'm Octagram.”,初始化函数的声明应该是:
- static int __init initialization_function(void)
- {
- /* Initialization code here */
- return 0;
- }
“__init”表示这个函数是初始化函数,将被放到内存内核空间的初始化段。这个符号在Linux内核源码中有定义成:
//“内核目录/include/linux/init.h”中定义
#define __init __section(.init.text) __cold notrace
相关宏的定义:
//“内核目录/include/linux/compiler.h”中定义
#define __section(S) __attribute__ ((__section__(#S)))
这是gcc编译器特有的语法,表示函数具有“.init.text”段的属性,其中“.text”表示代码段。之后由连接器ld会按照内核lds脚本提供的信息,将具有相同段属性的代码放在一起,形成一个连续的内存区域。
“__attribute__”是gcc的一个关键字,用于告诉gcc,代码或数据具有怎么样的属性,“__section__(#S)”表示代码或数据在哪一个段,连接器ld连接程序时就会将这些代码或数据放到特定的位置,“#S”是一种不常用的C语言语法,表示将S代表的内容转换为字符串,例如#.init.text等价于".init.text"。
//“内核目录/include/linux/compiler-gcc4.h”中定义
#define __cold __attribute__((__cold__))
“__cold__”同样是gcc的一个关键字,有关内容在“内核目录/include/linux/compiler-gcc4.h”有一段描述:
Mark functions as cold. gcc will assume any path leading to a call to them will be unlikely. This means a lot of manual unlikely()s are unnecessary now for any paths leading to the usual suspects like BUG(), printk(), panic() etc. [but let's keep them for now for older compilers.
Early snapshots of gcc 4.3 don't support this and we can't detect this in the preprocessor, but we can live with this because they're unreleased. Maketime probing would be overkill here. gcc also has a __attribute__((__hot__)) to move hot functions into a special section, but I don't see any sense in this right now in the kernel context.
//“内核目录/include/linux/compiler.h”中定义
#define notrace __attribute__((no_instrument_function))
“no_instrument_function”在gcc的手册中有解释:
A function may be given the attribute no_instrument_function, in which case this instrumentation will not be done. This can be used, for example, for the profiling functions listed above, high-priority interrupt routines, and any functions from which the profiling functions cannot safely be called (perhaps signal handlers, if the profiling routines generate output or allocate memory).
3.5 清理函数
类似初始化函数,模块从内核中删除时需要将调用清理函数完成清理工作,由module_exit()函数指定模块的清理函数。当使用命令rmmod清除模块时,内核会自动调用模块的清理函数,因此才能在日志中看到“Goodbye!”,清理函数的声明应该是:
static void __exit cleanup_function(void)
{
/*Cleanup code here */
}
相关关宏的定义:
“__exit”和“__init”类似,在内核中被定义成:
//“内核目录/include/linux/init.h”中定义
#define __exit __section(.exit.text) __exitused __cold
表示函数具有段属性“.exit”,该段同样是位于内核空间的一段内存中,在这段内存中集中了所有用于卸载模块的函数,如果模块直接建立在内核里,或者内核配置成不允许模块卸载,这个函数可以省略,如果模块没有定义一个清理函数,内核不会允许它被卸载。一个标识__exit的函数只在模块卸载或者系统停止时调用,任何别的情况,调用都是错误的。
//“内核目录/include/linux/init.h”中定义
#ifdef MODULE
#define __exitused
#else
#define __exitused __used
#endif
关于“__used”的说明在“内核目录/include/linux/compiler.h”中有一段:
Allow us to avoid 'defined but not used' warnings on functions and data, as well as force them to be emitted to the assembly file. As of gcc 3.4, static functions that are not marked with attribute((used)) may be elided from the assembly file. As of gcc 3.4, static data not so marked will not be elided, but this may change in a future gcc version. NOTE: Because distributions shipped with a backported unit-at-a-time compiler in gcc 3.3, we must define __used to be __attribute__((used)) for gcc >=3.3 instead of 3.4. In prior versions of gcc, such functions and data would be emitted, but would be warned about except with attribute((unused)). Mark functions that are referenced only in inline assembly as __used so the code is emitted even though it appears to be unreferenced.
这个宏是防止gcc提出“定义而未使用”的警告的。
4 Makefile注释
(1) $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
“$(MAKE)”表示再一次执行make指令,但是这次执行将通过选项“-C”切换到目录“$(KERNELDIR)”,也即是内核源代码目录下去执行,这样读取的就会是内核的Makefile,执行的目标是“modules”,但是如果就这样开始执行了,生成的目标文件就会跑到内核目录下,因此还需要只用“M=$(PWD)”选项切换到当前目录再输出。
(2) CROSS_COMPILE ?=
指定交叉编译的工具链前缀,如果这项不为空,make会调用对应的工具链来编译,产生非PC端的程序。
(3) obj-m := hw.o
在内核的Makefile中,规定:如果要编译一个模块,需要将模块对应的obj文件放在obj-m中,如果是在编译内核时要将模块直接编译进内核中,需要将模块对应的obj文件放在obj-y中。如果一个模块名为 module.ko,来自2个源文件(姑且称之为, file1.c 和 file2.c)正确的书写应当是:
obj-m := module.o
module-objs := file1.o file2.o
5 如何产生ARM端的模块程序
5.1 修改源码
上面的程序由于使用了ubuntu的内核目录,所以产生的是PC的程序,但是我们需要产生ARM端的程序,这个只需要修改Makefile就可以了。新的Makefile如下:
Makefile文件
- KERNELDIR ?= /home/octagram/workspace/ARM/OMAP3530/linux-2.6.38.8
- PWD := $(shell pwd)
- CROSS_COMPILE ?= /home/octagram/softwares/ARMTools/ocotonix/ca.8.3.0/ bin/arm-octagram-linux-gnueabi-
- CC ?= $(CROSS_COMPILE)gcc
- obj-m := hw.o
- all:
- $(MAKE) -C $(KERNELDIR) M=$(PWD) modules
- install: all
- clean:
- rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions *.markers *.symvers *.order
- .PHONY: modules modules_install clean
(1) KERNELDIR ?=
重新指定内核目录的路径,因为这回要使用ARM的内核源码而非PC的,上面的这个路径是为OMAP3530移植的内核目录。
(2) CROSS_COMPILE ?=
指定交叉编译工具链的路径和前缀,因为ARM和PC是完全不同的体系,因此同一个程序产生的二进制机器码也是不同的,所以需要对应的编译工具。PC端的为空是因为PC的工具链存放在系统路径下,能被自动搜索到,而且PC的工具链没有前缀。
5.2 开始编译
在模块源代码目录下执行下面命令:
$ make
(1) 第一次编译出现以下错误:
ERROR: Kernel configuration is invalid.
include/generated/autoconf.h or include/config/auto.conf are missing.
Run 'make oldconfig && make prepare' on kernel src to fix it.
根据提示在内核目录下(而非模块源代码目录)执行以下命令可解决:
$ make octagram-ocsom3530_defconfig
$ make prepare
这里没有执行oldconfig是因为那不是我的开发板的配置文件,这个配置文件需要使用make menuconfig命令,然后按照开发板的情况进行配置,会自动生成隐藏文件.config,最后将配置文件复制到arch/arm/configs目录下,改名做备份,这个属于内核移植时的工作。
(2) 第二次编译出现以下错误:
MODPOST 1 modules
/bin/sh: scripts/mod/modpost: not found
这是因为没有编译scripts造成的。在内核目录下(而非模块源代码目录)执行下面命令:
$ make scripts
(3) 还有一个警告信息
WARNING: Symbol version dump /home/octagram/workspace/ARM/OMAP3530/linux-2.6.36.3/Module.symvers
is missing; modules will have no dependencies and modversions.
这是因为没有编译内核造成的。在内核目录下(而非模块源代码目录)执行下面命令:
$ make