一个简单的驱动
下面我们来编写第一个驱动程序,它很简单,在运行时会输出‘Hello World’消息。
// hello.c #include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> static int __init hello_init(void) { printk(KERN_ALERT "Hello World!/n"); return 0; } static void __exit hello_exit(void) { printk(KERN_ALERT "Goodbye World!/n"); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE("GPL"); |
这就是一个简单的驱动程序,它什么也没做,仅仅是输出一些信息,不过对于我们来说这已经足够了。保存这个程序,命名为hello.c。在写一个Makefile文件用来编译它,Makefile和hello.c文件保存在同一个目录下。
##Makefile ifneq ($(KERNELRELEASE),) MODULE_NAME = helloworld $(MODULE_NAME)-objs := hello.o obj-m := $(MODULE_NAME).o else KERNEL_DIR = /lib/modules/`uname -r`/build MODULEDIR := $(shell pwd) .PHONY: modules default: modules modules: make -C $(KERNEL_DIR) M=$(MODULEDIR) modules clean distclean: rm -f *.o *.mod.c .*.*.cmd *.ko rm -rf .tmp_versions endif |
编译并运行这个模块:
//需要root权限来运行 make insmod helloworld.ko rmmod helloworld.ko |
尽管我们对它的一些细节还不够了解,它确实神奇的工作了,这个Hello World信息输出到了屏幕终端上(不是VT),或者系统的Kenrel log里(/var/log/messages),你可以通过运行dmesg来看到这些信息。
驱动基础
我们通过分析上面的代码来了解一个驱动程序的基本概念。
- 头文件
#include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> |
-
- init.h 定义了驱动的初始化和退出相关的函数,
- kernel.h 定义了经常用到的函数原型及宏定义
- module.h 定义了内核模块相关的函数、变量及宏。
- 初始化
typedef int (*initcall_t)(void); |
驱动程序是通过module_init宏来声明初始化函数的:
static int __init hello_init(void) { printk(KERN_ALERT "Hello World!/n"); return 0; } module_init(hello_init); |
__init 宏告诉编译器如果这个模块被编译到内核则把这个函数放到(.init.text)段,这样当函数初始化完成后这个区域可以被清除掉以节约系统内存。 Kenrel启动时看到的消息“Freeing unused kernel memory: xxxk freed”同它有关。
初始化函数是有返回值的,只有在初始化成功是才返回0,否则返回错误码(errno)。
- 卸载
typedef void (*exitcall_t)(void); |
驱动程序是通过module_exit宏来声明清理函数的:
static void __exit hello_exit(void) { printk(KERN_ALERT "Goodbye World!/n"); } module_exit(hello_exit); |
- 版权信息
MODULE_LICENSE("GPL"); |
二、Kbuild与Makefile
从Linux内核2.6开始,Linux内核的编译采用Kbuild系统,这同过去的编译系统有很大的不同,尤其对于Linux内核模块的编译。在 新的系统下,Linux编译系统会两次扫描Linux的Makefile:首先编译系统会读取Linux内核顶层的Makefile,然后根据读到的内容 第二次读取Kbuild的Makefile来编译Linux内核。
Linux内核Makefile分类
- Kernel Makefile
- Kbuild Makefile
- ARCH Makefile
Kbuild Makefile
Kbuild Makefile的文件名不一定是Makefile ,尽管推荐使用Makefile这个名字。大多的Kbuild文件的名字都是Makefile。为了与其他Makefile文件相区别,你也可以指定Kbuild Makefile的名字为Kbuild 。而且如果“Makefile”和“Kbuild”文件同时存在,则Kbuild系统会使用“Kbuild”文件。
- 目标定义
- obj-?
obj-? = $(target).o
target为编译对象的名字。如果没有指定xxx-objs,这编译这个对象需要的源文件就是$(target).c或$(target).s。如果指 定了$(target)-objs,则编译这个对象需要的源文件由$(target)-objs指定,并且不能有$(target).c 或$(target).s文件。
- xxx-objs
- 嵌套编译
- 编译器选项
- ccflags-y, asflags-y and ldflags-y
有时候我们需要在内核源代码数的外面编译内核模块,编译的基本命令是:
我们可以把这个命令集成到Makefile里,这样我们就可以只输入“make”命令就可以了。回想上一章的那个Makefile,它把Normal Makefile 和Kbuild Makefile集成到一个文件中了。为了区别Kbuild Makefile 和Normal Makefile,这样我们改写Makefile为如下形式,并且添加Kbuild Makefile - “Kbuild”。
##Makefile ifneq ($(KERNELRELEASE),) include "Kbuild" else KERNEL_DIR = /lib/modules/`uname -r`/build MODULEDIR := $(shell pwd) .PHONY: modules default: modules modules: make -C $(KERNEL_DIR) M=$(MODULEDIR) modules clean distclean: rm -f *.o *.mod.c .*.*.cmd *.ko rm -rf .tmp_versions endif |
## Kbuild MODULE_NAME = helloworld $(MODULE_NAME)-objs := hello.o obj-m := $(MODULE_NAME).o |
一般不需要在Makefile里包含如下代码,这样写完全是为了兼容老版本的Kbuild系统。KERNELRELEASE变量在Kernel Makefile里定义的,因此只有在第二次由Kbuild读取这个Makefile文件时才会解析到Kbuild的内容。
ifneq ($(KERNELRELEASE),) include "Kbuild" else ... endif |
外部头文件
有时需要连接内核源代码外部的系统头文件,但Kbuild系统默认的系统头文件都在内核源代码内部,如何使用外部的头文件呢?这个可以借助于Kbuild系统的特殊规则:
- EXTRA_CFLAGS
EXTRA_CFLAGS += $(ext_include_path)
一般外部头文件可能位于外部模块源文件的目录内,如何指定呢?这可以借助$(src)或$(obj)
- $(src)/$(obj)
因此,我们修改Kbuild文件添加 EXTRA_CFLAGS 来包含外部头文件尽管在这个驱动里没有引用外部系统头文件:
## Kbuild
MODULE_NAME = helloworld
$(MODULE_NAME)-objs := hello.o
EXTRA_CFLAGS := -I$(src)/include
obj-m := $(MODULE_NAME).o