目录
内核模块简介
内核模块编写
源码分析
Makefile文件
hello模块的具体实现
ARM端调试
ARM端调试遇到问题
Linux 内核模块是一种可以被内核动态加载和卸载的可执行程序,通过内核模块可以扩展内核的功能,通常内核模块被用于设备驱动、文件系统等。如果没有内核模块,需要向内核添加功能就需要修改代码、重新编译内核、安装新内核等步骤,不仅繁琐,而且容易出错,不易于调试。
Linux内核是一个整体结构,可以把内核想象成一个巨大的程序,各种功能结合在一起。当修改和添加新功能的时候,需要重新生成内核,效率极低。为了弥补整体式内核的缺点,Linux内核的开发者设计了内核模块机制。从代码角度看, 内核模块是一组可以完成某种功能的函数集合。从执行角度看,内核模块是一个已经编译但是没有连接的程序。
对于内核来说 ,模块包括了在运行时可以连接的代码。模块的代码可以被连接带内核,作为内核的一部分,因此被称为内核模块。从用户角度看,内核模块是一个外挂组件,在需要的时可以被删除。内核模块给开发者提供了动态扩充内核功能的途径。
内核模块是一个应用程序,但是与普通的应用程序有所不同,区别在于:
运行环境不同。内核模块运行在内核空间,可以访问系统的几乎所有的软硬件资源,因此在编程内核模块时要格外注意,一点小错误就会导致系统崩溃;
功能定位不同。普通应用程序是为了完成某个特定目标,功能定位明确;内核模块是为了其他的内核模块以及应用程序服务的,通常提供的是通用的功能;
函数调用方式不同。内核模块只是调用内核提供的函数,访问其他的函数会导致运行异常;普通应用程序可以调用自身以外的函数,只要正确连接就行了;
内核模块使用的是物理内存,这点与应用程序不同。应用程序使用虚拟内存,有一个巨大的地址空间, 在应用程序中可以分配大块的内存。内核模块可以使用的内存非常小,在编写内核模块代码的时候要注意内存的分配和使用。
#include
#include
#include
static __init int hello_init(void)
{
printk(KERN_ALERT "Hello, This is my first driver program!\n");
return 0;
}
static __exit void hello_exit(void)
{
printk(KERN_ALERT "Good Luck, see you!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_AUTHOR("TJincheng ");
MODULE_DESCRIPTION("Linux Kernel hello module (c)");
MODULE_LICENSE("Dual BSD/GPL");
1、加载和卸载
从内核模块的动态加载特性可以看出,内核模块至少支持加载和卸载两种操作,因此一个内核模块至少包括加载和卸载两个函数。
module_init(hello_init); //模块加载
module_exit(hello_exit); //模块卸载
2、头文件
先看看头文件, 下面这两个头文件是模块的必备头文件
#include //包含了init初始化函数和exit清理函数
#include //包含了大量加载模块需要的函数和符号的定义
然后是包含内核打印函数printk的头文件
#include //中包含了内核打印函数 printk函数 等
3、宏定义
__init、__exit(__双下划线)等宏是在linux-3.0/include/linux/init.h 中:
#define __init __section(.init.text) __cold notrace
#define __exit __section(.exit.text) __exitused __cold notrace
#define module_init(x) __initcall(x);
#define module_exit(x) __exitcall(x);
#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn) __define_initcall("6",fn,6)
typedef int (*initcall_t)(void);
#define __define_initcall(level,fn,id)
static initcall_t __initcall_##fn##id __used
__attribute__((__section__(".initcall" level ".init"))) = fn
static __init int hello_init(void)
宏就会被展开为
static __section(.init.text) __cold notrace inthello_init(void)
static __exit int hello_exit(void)
宏就会被展开为
__section(.exit.text) __exitused __cold notrace int hello_exit
这里的 __section 为gcc的链接选项,他表示把该函数链接到Linux内核映像文件的相应段中,这样hello_init将会被链接进.init.text段中,而hello_exit将会被链接进.exit.text段中。被链接进这两个段中的函数代码在调用完成之后,内核将会自动释放他们所占用的内存资源。因为这些函数只需要初始化或退出一次,所以hello_init()和hello_exit()函数最好在前面加上__init 和 __exit。
module_init(hello_init)
宏就会被展开为:
static int (*initcall_t)(void) __initcall_hello_init6 __used __attribute__((__section__(".initcall" "6" ".init"))) = hello_init;
这段代码也就是定义了一个叫 __initcall_hello_init6的函数指针,他指向 hello_init 这个函数, gcc的链接选项 attribute 和 section 将该指针变量链接到linux内核映像的 .initcall 段中。linux系统在启动时,完成CPU和板级初始化之后,就会从该段中读入所有的模块初始化函数就执行。每一个Linux内核模块都需要使用module_init()和module_exit()宏来修饰,这样系统启动时才能自动调用并初始化他们。
4、printk函数简介
printk 函数在 Linux 内核中定义并且对模块可用,它与标准 C 库函数 printf 的行为相似。内核需要它自己的打印函数, 因为它靠自己运行, 没有 C 库的帮助. 模块能够调用 printk 是因为在insmod 加载了它之后, 模块被链接到内核并且可存取内核的公用符号. 字串 KERN_ALERT 是消息的优先级。printk支持分级别打印调试,这些级别定义在linux-3.0/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 condition 公告信息 */
#define KERN_INFO "<6>" /* informational 提示信息,通常用于打印启动过程或者某个硬件的状态*/
#define KERN_DEBUG "<7>" /* debug-level messages 调试消息 */
Linux内核中printk()的语句是否打印到串口终端上,与u-boot里的bootargs参数中的loglelve=7 相关,只有低于loglevel级别的信息才会打印到控制终端上,否则不会在控制终端上输出。这时我们只能通过dmesg命令查看。 linux下的dmesg命令的可以查看linux内核所有的打印信息,它们记录在/var/log/messages系统日志文件中。linux内核中的打印信息很多,我们可以使用 dmesg -c命令清除之前的打印信息。
5、模块相关信息的声明
声明可以写在模块的任何地方(但必须在函数外面),但是惯例是写在模块最后。
MODULE_AUTHOR("xxx"); //作者信息
MODULE_DESCRIPTION("xxxxxxx") //简单描述
MODULE_LICENSE("xxxxx"); //指定它的代码使用哪个许可
其他的声明方式
MODULE_VERSION //声明版本
MODULE_ALIAS // 别名
MODULE_DEVICE_TABLE // 来告知用户空间, 模块支持那些设备
1、PC端Makefile
KERNAL_DIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
obj-m := hello.o
modules:
$(MAKE) -C $(KERNAL_DIR) M=$(PWD) modules
@make clear
clear:
@rm -f *.o *.cmd *.mod.c
@rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
@rm -f .*ko.cmd .*.o.cmd .*.o.d
@rm -f *.unsigned
clean:
@rm -f hello.ko
2、ARM端Makefile
LINUX_SRC = ${shell pwd}/../linux/linux-3.0/
CROSS_COMPILE=/opt/xtools/arm920t/bin/arm-linux-
INST_PATH=/tftp
PWD := $(shell pwd)
EXTRA_CFLAGS+=-DMODULE
obj-m += hello.o
modules:
@echo ${LINUX_SRC}
@make -C $(LINUX_SRC) M=$(PWD) modules
@make clear
uninstall:
rm -f ${INST_PATH}/*.ko
install: uninstall
cp -af *.ko ${INST_PATH}
clear:
@rm -f *.o *.cmd *.mod.c
@rm -rf *~ core .depend .tmp_versions Module.symvers modules.order -f
@rm -f .*ko.cmd .*.o.cmd .*.o.d
clean: clear
@rm -f *.ko
1、make先编译源文件
make
2、加载模块
sudo insmod kernel_hello.ko
3、查看已加载模块
lsmod | grep ker*
4、查看内核打印消息
sudo dmesg -c
5、模块卸载
sudo rmmod kernel_hello
6、PC端调试出现问题–PC端内核版本不匹配
大家可以参考我之前写的博客:博客链接
我们通过tftpd32小程序(注意关PC防火墙和pc端的IP地址与ARM板子的Ip地址在同一网段内)将PC端使用ARM的模块传到开发板上:
tftp -gr hello.ko 192.168.1.2 //通过tftp获取自己在虚拟机编译好的hello模块
1、在ARM板上挂载模块:
insmod hello.ko
2、查看模块:
lsmod
3、删除模块:
rmmod hello.ko
du -sh hello.ko
命令来查看大小