天天看点

Linux驱动程序开发 - Kbuild系统

一个简单的驱动

下面我们来编写第一个驱动程序,它很简单,在运行时会输出‘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来看到这些信息。

驱动基础

我们通过分析上面的代码来了解一个驱动程序的基本概念。

  • 头文件

就像你写C程序需要包含C库的头文件那样,Linux内核编程也需要包含Kernel头文件,大多的Linux驱动程序需要包含下面三个头文件:

#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);

同__init类似,如果驱动被编译进内核,则__exit宏会忽略清理函数,因为编译进内核的模块不需要做清理工作。显然,__init和__exit对动态加载的模块是无效的。

  • 版权信息

Linux内核是按照GPL发布的,同样Linux的驱动程序也要提供版权信息,否则当加载到内核中是系统会给出警告信息。Hello World例子中的版权信息是GPL。

MODULE_LICENSE("GPL");

二、Kbuild与Makefile

从Linux内核2.6开始,Linux内核的编译采用Kbuild系统,这同过去的编译系统有很大的不同,尤其对于Linux内核模块的编译。在 新的系统下,Linux编译系统会两次扫描Linux的Makefile:首先编译系统会读取Linux内核顶层的Makefile,然后根据读到的内容 第二次读取Kbuild的Makefile来编译Linux内核。

Linux内核Makefile分类

  • Kernel Makefile

Kernel Makefile位于Linux内核源代码的顶层目录,也叫 Top Makefile。它主要用于指定编译Linux Kernel目标文件(vmlinux)和模块(module)。这编译内核或模块是,这个文件会被首先读取,并根据读到的内容配置编译环境变量。对于内 核或驱动开发人员来说,这个文件几乎不用任何修改。

  • Kbuild Makefile

Kbuild系统使用Kbuild Makefile来编译内核或模块。当Kernel Makefile被解析完成后,Kbuild会读取相关的Kbuild Makefile进行内核或模块的编译。Kbuild Makefile有特定的语法指定哪些编译进内核中、哪些编译为模块、及对应的源文件是什么等。内核及驱动开发人员需要编写这个Kbuild Makefile文件。

  • ARCH Makefile

ARCH Makefile位于ARCH/$(ARCH)/Makefile,是系统对应平台的Makefile。Kernel Top Makefile会包含这个文件来指定平台相关信息。只有平台开发人员会关心这个文件。

Kbuild Makefile

Kbuild Makefile的文件名不一定是Makefile ,尽管推荐使用Makefile这个名字。大多的Kbuild文件的名字都是Makefile。为了与其他Makefile文件相区别,你也可以指定Kbuild Makefile的名字为Kbuild 。而且如果“Makefile”和“Kbuild”文件同时存在,则Kbuild系统会使用“Kbuild”文件。

  • 目标定义

Kbuild Makefile的一个最主要功能就是指定编译什么,这个功能是通过下面两个对象指定的obj-?和xxx-objs:

  • obj-?

obj-?指定编译什么,怎么编译?其中的“?”可能是“y”或“m”,“y”指定把对象编译进内核中,“m”指定把对象编译为模块。语法如下;

obj-? = $(target).o

target为编译对象的名字。如果没有指定xxx-objs,这编译这个对象需要的源文件就是$(target).c或$(target).s。如果指 定了$(target)-objs,则编译这个对象需要的源文件由$(target)-objs指定,并且不能有$(target).c 或$(target).s文件。

  • xxx-objs

xxx-objs指定了编译对象需要的文件,一般只有在源文件是多个时才需要它。 只要包含了这两行,Kbuild Makefile就应该可以工作了。

  • 嵌套编译

有时一个对象可能嵌入到另一个对象的目录下,那个如何编译子目录下的对象呢?其实很简单,只要指定obj_?的对象为子目录的名字就可以了:

obj-? = $(sub_target)/

其中“?”可以是“y”或“m”,$(sub_target)是子目录名字。

  • 编译器选项

尽管在大多数情况下不需要指定编译器选项,有时我们还是需要指定一些编译选项的。

  • ccflags-y, asflags-y and ldflags-y

这些编译选项用于指定cc、as和ld的编译选项 编译外部模块

有时候我们需要在内核源代码数的外面编译内核模块,编译的基本命令是:

make -C $(KERNEL_DIR) M=`pwd` modules

我们可以把这个命令集成到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可以给Kbuild系统添加外部系统头文件,

EXTRA_CFLAGS += $(ext_include_path)

一般外部头文件可能位于外部模块源文件的目录内,如何指定呢?这可以借助$(src)或$(obj)

  • $(src)/$(obj)

$(src)是一个相对路径,它就是Makefile/Kbuild文件所在的路径。同样$(obj)就是编译目标保存的路径,默认就是源代码所在路径。

因此,我们修改Kbuild文件添加 EXTRA_CFLAGS 来包含外部头文件尽管在这个驱动里没有引用外部系统头文件:

## Kbuild

MODULE_NAME = helloworld

$(MODULE_NAME)-objs := hello.o

EXTRA_CFLAGS := -I$(src)/include

obj-m := $(MODULE_NAME).o

继续阅读