文章目录
- 前言
- 开发环境配置之内核升级
- 为什么升级内核
- 内核升级
- Hello world 驱动程序
- 驱动模块的组成
- Hello World模块
- 编译Hello World模块
- 模块的操作
- Hello World模块加载后文件系统的变化
- 模块参数和模块之间通信
- 模块参数
- 模块的文件格式ELF
- 模块之间的通信
- 模块之间的通信实例
- 将模块加入内核
- 向内核添加模块
- Kconfig
- Kconfig语法
- 应用实例:在内核中新增加add_sub模块
- 对add_sub模块进行配置
- 小结
前言
开发板以STM32MP157为例进行实验
。
万事开头难,写驱动程序也是一样,本章将构建第一个驱动程序。驱动程序和模块的关系非常密切,所以这里将详细讲解模块的相关知识。而模块编程成败与否的先决条件是要有统一的内核版本,所以这里将讲解怎样升级内核版本。最后为了提高程序员的编程效率,这里将介绍两种集成开发环境。
开发环境配置之内核升级
构建正确的开发环境,对于写驱动程序非常重要。错误的开发环境,编写的驱动程序不能正确运行。特别是关于内核版本的问题,内核版本不匹配,会使驱动程序不能在系统中运行,所以需要对啮合进行升级。实际以STM32MP157为例
,参考教程以``Fedora Core9为例进行内核升级注意区分,首先要说明为什么要升级内核。
为什么升级内核
内核时一个提供硬件抽象层、磁盘及文件系统控制、多任务等功能的系统软件。
根据内核是否被修改过,可以将内核分为标准内核和厂商内核
两类,如下图
标准内核源码和标准内核
标准内核源码是指从kernel.org
官方网站下载的标准代码。其是Linux内核开发者经过严格测试所构建的内核代码。标准内核是将标准内核源码编译后得到的二进制映象文件,如上图左半部所示。
厂商内核源码和厂商内核
在某些情况下,发行版厂商会对标准内核源码进行适当的修改,以优化内核的性能。这种经过修改后的标准内核源码,就是厂商修改过的内核源码。将厂商修改过的内核源码编译后,会形成厂商发行版内核。所以,厂商发行版内核是对标准内核的修改和优化。这里,需要注意的是,厂商发行版内核和标准内核对于驱动程序是不兼容的,根据不同内核源码编译的驱动程序是不能互用的。
两者兼容性问题
构建驱动程序模块时,必须考虑驱动程序与内核的兼容性。使用标准内核源码构建内核模块,就是标准内核模块,其不能在厂商内核中使用。使用厂商修改过的内核源码构建的内核模块,就是厂商的内核模块,其不能再标准内核中使用。这里,需要注意的是,即使模块代码相同,标准内核模块和特定厂商的内核模块,其模块格式也是不相同的。
可以通过uname -r
命令查看使用的内核版本。
内核升级
尽管不少的开发板中可以使用"软件包管理器工具"对内核进行升级,但毕竟是开发厂商编译的内核有其局限性,里面添加了很多驱动开发系统不需要的模块,而驱动开发需要的模块却没有开启。因此,学会自己手动编译升级也是很有必要的。例如将内核升级为linux 2.6.39.4
内核升级的步骤如下所示:
- 从
http://www.kernel.org/pub/linux/kernel
上下载linux-2.6.29.4.tar.bz2
内核源码包。 - 使用
mkdir linux-2.6.29.4
在根目录中建立一个目录 - 将压缩包复制到创建的目录中
- 进入目录解压源码包
- 进入第二层源码目录
- 执行
make menuconfig
配置内核并保存。——详细需要参考厂商或者开发板特性 - 编译内核
make
命令 - 编译内核模块
make modules
- 安装内核模块
make modules_install
- 安装内核
make install
- 使用
reboot
重启计算机,选择新内核启动系统。
内核升级成功后,可以通过命令uname -r
来查看内核版本。
为了节省时间,可以编写一个shell程序。代码如下:
#! /bin/sh
cd /
mkdir linux-2.6.29.4
cp linux-2.6.29.4.tar.bz2 /linux-2.6.29.4/
cd linux-2.6.29.4
tar -xjvf linux-2.6.29.4.tar.bz2
cd linux-2.6.29.4
make menuconfig
make
make modules
make modules_install
make install
reboot
该shell文件没有可执行权限,需要使用命令chmod
让shell文件具有可执行权限,命令如下:
chmod a+x install-new-core
然后执行该shell文件,使升级内核自动进行,使用如下命令:
./install-new-core
注意:对内核的升级并不会破坏现有的内核,也不会破坏系统上的文件等资源。内核升级以后,除了性能上的改变外,对用户来说就像什么也没有发生一样
。
Hello world 驱动程序
本节将带领读者编写第一个驱动模块,该驱动模块的功能是在加载时,输出"Hello World";在卸载时,输出"Goodbye World"。这个驱动程序模块虽然简单,但是也包含了驱动模块的重要组成部分。在本节的开始,将先对模块的重要组成部分进行介绍。
驱动模块的组成
一个驱动模块主要由如下几部分组成:
图中的顺序也是源文件中的顺序。不按照这个顺序来编写驱动模块也不会出错,只是大多数开发人员都喜欢这样的顺序规范。
下面对这些结构进行说明。
1、头文件(必须)
驱动模块会使用内核中的许多函数,所以需要包含必要的头文件。有两个头文件是所有驱动模块都必须包含的
。
#inlcude <linux/module.h>
#inlcude <linux/init.h>
module.h
文件包含了加载模块时需要使用的大量符号和函数定义。init.h
包含了模块加载函数和释放函数的宏定义。
2、模块参数(可选)
模块参数是驱动模块加载时,需要传递给驱动模块的参数。如果一个驱动模块需要完成两种功能,那么就可以通过模块参数选择使用哪一种功能。
3、模块加载函数(必须)
模块加载函数是模块加载时,需要执行的函数。
4、模块卸载函数(必须)
模块卸载函数是模块卸载时,需要执行的函数
5、模块许可声明(必须)
模块许可声明表示模块受内核支持的程度。有许可权的模块会更受到开发人员的重视。需要使用MODULE_LICENSE
表示该模块的许可权限。内核可以识别的许可权限如下:
MODULE_LICENSE("GPL"); /*任一版本的GNU公告许可权*/
MODULE_LICENSE("GPL v2"); /* GPL版本2许可权*/
MODULE_LICENSE("GPL and additional rights"); /*GPL及其附加许可权*/
MODULE_LICENSE("Dual BSD/GPL"); /*BSD/GPL双重许可权*/
MODULE_LICENSE("Dual MPL/GPL"); /*MPL/GPL双重许可权*/
MODULE_LICENSE("Proprietary"); /*专有许可权*/
如果一个模块没有包含任何许可权,那么就会认为是不符合规范的。这时,内核加载这种模块时,会收到内核加载一个非标准模块的警告。开发人员不喜欢维护这种没有遵循许可权标准的内核模块。
以GPL为例,说明许可权的意义。GPL是General Public License的缩写,表示通用公共许可证。GNU通用公共许可证可以保证你有发布自由软件的自由;保证你能收到源程序或者在你需要时能得到它;保证你能修改软件或将它的一部分用于新的自由软件。
Hello World模块
任何一本关于编程的书,几乎都以"Hello World"开始。现在,来看一下最简单的一个驱动模块。
#include <linux/init.h> /*定义了一些相关的宏*/
#include <linux/module.h> /*定义了模块需要的*/
static int hello_init(void)
{
printk(KERN_ALERT "Hello World\n"); /*打印 Hello World*/
return 0;
}
static void hello_exit(void)
{
printk(KERNEL_ALERT "Goodbye World\n"); /*打印Goodbye World*/
}
module_init(hello_init); /*指定加载模块函数*/
module_exit(hello_exit); /*指定模块卸载函数*/
MODULE_LICENSE("Dual BSD/GPL"); /*指定许可权为Dual BSD/GPL*/
源码解析:
- 1~2行是两个必须的头文件
- 3~7行是该模块的加载函数,当使用
insmod
命令加载模块时,会调用该函数。 - 8~11行是该模块的释放函数,当使用
rmmod
命令卸载模块时,会调用该函数 - 12行,
module_init
是内核模块的一个宏。其用来声明模块的加载函数,也就是使用insmod
命令加载模块时,调用的函数hello_init()
。 - 13行,
module_exit
也是内核模块的一个宏。用来声明模块的释放函数,也就是使用rmmod
命令卸载模块时,调用的函数hello_exit()
。 - 14行,使用
MODULE_LICENSE()
表示代码遵循的规范,该模块代码遵循BSD和GPL双重规范。这些规范定义了模块在传播过程中的版权问题。
编译Hello World模块
在对Hello World模块进行编译时,需要满足一定的条件。
1、编译内核模块的条件
正确的编译内核模块满足下面一些重要的先决条件:
I.读者应该确保使用正确版本的编译工具、模块工具和其他必要的工具。不同版本的内核需要不同版本的编译工具。
II.应该有一份内核源码,该源码的版本应该和系统目前使用的内核版本一致。
III.内核源码应该至少被编译过一次,也就是执行过make
命令。
2、Makefile文件
编译Hello World模块需要编写一个Makefile
文件。首先来看一下一个完整的Makefile文件,以便对该文件有整体的认识。
ifeq ($(KERNELRELEASE),)
KERNELDIR ?= /linux-2.6.29.4/linux-2.6.29.4
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M= $(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M= $(PWD) modules_install
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
else
obj-m := hello.o
endif
Makefile解析
- 1行,判断
KERNELRELEASE
变量是否为空,该变量是描述内核版本字符串。只有执行make
命令的当前目录为内核源代码目录时,该变量才不为空字符。 - 2~3行定义了
KERNELDIR
和PWD
变量。KERNELDIR
是内核路径变量,PWD
是由执行pwd
命令得到的当前模块路径。 - 4行是一个标识,以冒号结尾,标识Makefile文件的一个功能选项。
- 5行
make
的语法是"Make -C 内核路径 M=模块路径 modules"。该语句会执行内核模块的编译 - 6行和4行标识同样的意思。
- 7行是模块安装到模块对应的路径中,当在命令执行
make modules_install
时,执行该命令,其他时候不执行。 - 8行是删除多余文件标识
- 9行是删除编译过程的中间文件的命令
- 11行,
意思是将hello.o编译成hello.ko
模块。如果要编译其他模块时,只要将hello.o
中的hello
改为模块的文件名就可以了。
3、Makefile文件的执行过程
Makefile文件的执行过程有些复杂,为了使读者对该文件的执行过程有个清晰的了解,结合下图进行分析。
执行make
命令后,将进入Makefile文件。此时KERNELRELEASE
变量为空,此时是第一个进入Makefile文件。当执行完2,3行代码后,会根据make
命令的参数执行不同的逻辑,如下:
make modules_install
命令,将执行6,7行将模块安装到操作系统中。make clean
命令,会删除目录中的所有临时文件make
命令,会执行4,5行编译模块。首先$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
中的-C $(KERNELDIR)
选项,会使编译进入内核源码目录/linux-2.6.29.4/linux-2.6.29.4
,读取Makefile文件,并从中得到一些信息,例如变量KERNELRELEASE
将在这里被赋值。当内核源码目录中的Makefile文件读取完成后,编译器会根据选项M=$(PWD)
第二次进入模块所在的目录,并再次执行Makefile文件。当第二个执行Makefile文件时,变量KERNELRELEASE
的值为内核发布版本信息,也就是不为空,此时会执行10、11、12行代码。这里的代码指明了模块源码中各文件的依赖关系,以及要生成的目标模块名,这里就正式编译模块了。
模块的操作
Linux为用户提供了modutils
工具,用来操作模块。这个工具集主要包括:
insmod
命令加载模块。使用insmod hello.ko
可以加载hello.ko
模块。模块加载后会自动调用hello_init()
函数。该函数会打印"Hello World"信息。如果在终端没有看见信息,则这条信息被发送到了/var/log/message
文件中。可以使用demsg | tail
命令查看文件的最后几行。如果模块带有参数,那么使用下面的格式可以传递参数给模块:insmod 模块.ko 参数1=值1 参数2=值2 参数3=值3 /*参数之间没有逗号*/
rmmod
命令卸载模块。如果模块没有被使用,那么执行rmmod hello.ko
就可以卸载hello.ko
模块。modprobe
命令是比较高级的加载和删除模块命令,其可以解决模块之间的依赖性问题。将在后面介绍。lsmod
命令列出已经加载的模块信息。在insmod hello.ko
之前后分别执行该命令就可以知道hello.ko
模块是否被加载。modinfo
命令用于查询模块相关的信息,比如作者、版权等。
Hello World模块加载后文件系统的变化
当使用insmod hello.ko
加载模块后文件系统会发生什么样的变化呢?文件系统存储着有关模块的属性信息。程序员可以从这些属性信息中了解目前模块在系统中的状态,这些状态对开发调试非常重要。
/proc/modules发生变化,在modules文件中会增加如下一行
:
这几个字段的信息分别是模块名、使用的内存、引用计数、分隔符、活动状态和加载到内核中的地址。
lsmod
命令就是通过读取/proc/modules
文件列出内核当前已经加载的模块信息的。lsmod
去掉了部分信息,并使显示时更为整齐。执行lsmod
命令的结构如下:
/proc/devices文件没有变化,因为hello.ko模块并不是一个设备模块
。
在/sys/module/目录会增加hello这个模块的基本信息
在/sys/module/目录下会增加一个hello目录。该目录中包含了一些以层次结构组织的内核模块的属性信息。使用tree -a hello目录可以得到下面的目录结构
。
模块参数和模块之间通信
为了增加模块的灵活性,可以给模块添加参数。模块参数可以控制模块的内部逻辑。从而使模块可以在不同的情况下,完成不同的功能,下面首先对模块参数进行介绍。
模块参数
用于空间的应用程序可以接受用户的参数,设备驱动程序有时候也需要接受参数。例如一个模块可以实现两种相似的功能,这时可以传递一个参数到驱动模块,以决定其使用哪一种功能。参数需要在加载模块时指定,例如insmod xxx.ko param=1
可以使用"module_param(参数名,参数数据类型,参数读写权限)"来为模块定义参数。例如下面代码定义了一个长整型和整型参数:
static long a = 1;
static int b = 1;
module_param(a, long, S_IRUGO);
module_param(b, int , S_IRUGO);
参数数据类型可以是byte、short、ushort、int、uint、long、ulong、bool、charp(字符指针类型)
。细心的读者可以看出,模块参数的类型中没有浮点类型。这是因为,内核并不能完美地支持浮点数操作。在内核中使用浮点数时,除了要人工保存和恢复浮点数寄存器外还有一些琐碎的事情要做。为例避免麻烦通常不在内核中使用浮点数。除此之外printk()
函数也不支持浮点类型。
模块的文件格式ELF
了解模块以何种格式存储在硬盘中,对于理解模块间怎样通信是非常有必要的。使用file
命令可以知道hello.ko
模块使用的是ELF
文件格式,命令如下:
此命令在linux驱动开发中经常使用,请读者留意其使用方法。
下图描述的是ELF目标文件的总体结构。图中省去了ELF一些繁琐的结构,把最主要的结构提取出来,形成了下图所示的ELF文件基本结构图。
ELF Header
头位于文件的最前部。其包含了描述整个文件的基本属性,例如ELF文件版本、目标机器型号、程序入口地址等。.text
表示代码段,存放文件的代码部分。.data
表示数据段,存放已经初始化的数据等。.Section Table
表描述了ELF文件包含的所有段的信息,例如每个段的段名,段的长度、在文件中的偏移、读写权限及段的其他属性。.symtab
表示符号表。符号表是一种映射函数到真实内存地址的数据结构。其就像一个字典,其记录了在编译阶段,无法确定地址的函数。该符号表将在模块文件加载阶段,由系统赋予真实的内存地址。
模块之间的通信
模块是为了完成某种特定任务而设计的。其功能比较单一,为了丰富系统的功能,所以模块之间常常进行通信。它们之间可以共享变量、数据结构,也可以调用对方提供的功能函数。
以下图所示来进行分析模块1是如何调用模块2的功能函数的。为了讲清楚这个过程需要从模块2的加载讲起。
模块2的加载过程如下:
(1)使用insmod 模块2.ko
加载磨块2
(2)内核为模块2分配空间,然后将模块的代码和数据装入分配的内存中。
(3)内核发现符号表中有函数1、函数2可以导出,于是将其内存地址记录在内核符号表中
。
模块1在加载进内核时,系统会执行以下操作:
(1)insmod
命令会为模块分配空间,然后将模块的代码和数据装入内存中。
(2)内核在模块1的符号表(symtab
)中发现一些未解析的函数。上图中这些为解析的函数是“函数1”、“函数2”,这些函数位于模块2的代码中。所以模块1会通过内核符号表,查到相应的函数,并将函数地址填到模块1的符号表中。
通过模块1加载的过程后,模块1就可以使用模块2提供的“函数1”和“函数2”了。
模块之间的通信实例
本实例通过两个模块介绍模块之间的通信。模块add_sub
提供了两个导出函数add_integer()
和sub_integer()
,分别完成两个数字的加法和减法。模块test
用来调试模块add_sub
提供的两个方法,完成加法或减法的操作。
1、add_sub模块
模块add_sub中提供了一个加法函数和一个减法函数,其add_sub.c文件如下:
#inlcude <linux/init.h>
#include <linux/module.h>
#include "add_sub.h"
long add_integer(int a, int b)
{
return a+b;
}
long sub_integer(int a, int b)
{
return a-b;
}
EXPORT_SYMBOL(add_integer); /*导出加法函数*/
EXPORT_SYMBOL(sub_integer); /*导出减法函数*/
MODULE_LICENSE("Dual BSD/GPL");
该文件定义了一个加法和减法函数,这两个函数需要导出到内核符号表
,才能够被其他模块所调用。EXPORT_SYMBOL
就是导出宏。该宏的功能就是让内核知道其定义的函数可以被其他函数使用。
使用EXPORT_SYMBOL
使函数变为导出函数是很方便的,但是不能随便使用。一个Linux内核源码中有几百万行代码,函数数以万计,模块中很有可能出现同名函数。幸运的是编译器认为模块中的函数都是私有的,不同模块出现相同的函数名,并不会对编译产生影响,前提是不能使用EXPORT_SYMBOL
导出符号。
为了测试模块add_sub的功能,这里建立了另一个test模块。test模块需要知道add_sub模块提供了那些功能函数,所以定义了一个add_sub.h
头文件,代码如下:
#ifndef _ADD_SUB_H_
#define _ADD_SUB_H_
long add_integer(int a, int b);
long sub_integer(int a, int b);
#endif
2、test模块
test模块用来测试add_sub模块提供的两个方法,同时test模块也可以接受一个AddOrSub
,用来决定是调用add_integer()
函数还是sub_integer()
函数。当AddOrSub
为1时,调用add_integer()
函数;当AddOrSub
不为1时,调用sub_integer()
函数。test模块的代码如下:
#include <linux/init.h>
#include <linux/module.h>
#include "add_sub.h" /*不要使用<>包含文件,否则找不到该文件*/
/*定义模块传递的参数a, b*/
static long a = 1;
static long b = 1;
static int AddOrSub = 1;
static int test_init(void) /*模块加载函数*/
{
long result = 0;
printk(KERN_ALERT "test init\n");
if(1==AddOrSub)
{
result=add_integer(a, b);
}
else
{
result=sun_integer(a, b);
}
printk(KERN_ALERT "The %s result is %ld", AddOrSub==1?"Add":
"Sub", result);
return 0;
}
static void test_exit(void) /*模块卸载函数*/
{
printk(KERN_ALERT "test exit\n");
}
module_init(test_init);
module_exit(test_exit);
module_param(a, long, S_IRUGO);
module_param(b, long, S_IRUGO);
module_param(AddOrSub, int, S_IRUGO);
/*描述信息*/
MODULE_LICENSE("Dual BSD/GPL");
MODULE_AUTHOR("Jacky Hu");
MODULE_DESCRIPTION("A module for testing module params and EXPORT_SYMBOL");
MODULE_VERSION("V1.0");
3、编译模块
分别对两个模块进行编译,得到两个模块文件。add_sub
模块的Makefile文件与Hello World模块的Makefile文件有所不同。在add_sub
模块的Makefile文件中,变量PRINT_INC
表示add_sub.h
文件所在的目录,该文件声明了add_integer()函数和sub_integer()函数的原型。EXTRA_CFLAGS
变量表示在编译模块时,需要添加的目录。编译器会从这些目录中找到需要的头文件。add_sub
模块的Makefile如下:
ifeq($(KERNELRELEASE),)
KERNELDIR ?=/linux-2.6.29.4/linux-2.6.29.4
PWD := $(shell pwd)
PRINT_INC = $(PWD)/../include
EXTRA_CFLAGS += -I $(PRINT_INC)
modules:
$(MAKE) -I $(PRINT_INC) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_version
.PHYONY:modules modules_install clean
else
#called from kernel build system: just declare what our modules are
obj-m := add_sub.o
endif
test模块的Makefile文件如下代码所示。SYMBOL_INC
是包含目录,该目录包含了add_sub.h
头文件。该文件中定义了两个在test模块中调用的函数。KBUILD_EXTRA_SYMBOLS
包含了在编译add_sub
模块时,产生的符号表文件Module.symvers
,这个文件中列出了add_sub
模块中函数的地址。在编译test模块时,需要这个符号表。
obj-m := test.o
KERNELDIR ?= /linux-2.6.29.4/linux-2.6.29.4
PWD := $(shell pwd)
SYMBOL_INC = $(obj)/../include
EXTRA_CFLAGS += -I $(SYMBOL_INC)
KBUILD_EXTRA_SYMBOL=$(obj)/../print/Module.symvers
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
modules_install:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
.PHONY:modules modules_install clean
4、测试模块
在加载test模块之前,需要先加载add_sub模块,test模块才能访问add_sub模块提供的导出函数,命令如下:
使用insmod
加载模块,并传递参数到模块中。参数AddOrSub=2
表示执行a-b
在/sys/module/
目录下会创建一个test目录,其中可以清楚地看到paramters下有3个文件,分别表示3个参数。
将模块加入内核
当编译了模块后,如果希望模块随着系统一起启动,那么需要将模块静态编译进内核
。将模块静态编译入内核,需要完成一些必要的步骤。
向内核添加模块
向Linux内核中添加驱动模块,需要完成3个工作:
(1)编写驱动程序文件
(2)将驱动程序文件放到Linux内核源码的相应目录中,如果没有合适的目录,可以自己建立一个目录存放驱动程序文件。
(3)在目录的Kconfig
文件中添加新驱动程序对应的项目编译选择。
(4)在目录的Makefile
文件中添加新驱动的编译语句。
Kconfig
内核源码树的目录下都有两个文件Kconfig
和Makefile
。分布到各目录的Kconfig
文件构成了一个分布式的内核配置数据库
,每个Kconfig文件分别描述了所属目录源文档相关的内核配置菜单
。在内核配置make menuconfig(或xconfig等)
时,从Kconfig
中读出菜单,用户选择后保存到.config
这个内核配置文档中。在内核编译时,主目录中的Makefile
调用这个.config
文件,就知道用户的选择。
上面的内容说明了,Kconfig就是对应着内核的配置菜单。如果想要添加新的驱动到内核源码中,就需要修改Kconfig
文件。
为了使读者对Kconfig
文件有一个直观的认识,这里举一个简单的例子,这个例子是IIC驱动。在linux-2.6.29.4/drivers/i2c目录中包含了I2c设备驱动的源代码,其目录结构如下:
该目录中包含了一个Kconfig文件,该文件中包含I2C_CHARDEV
配置选项。
上述Kconfig
文件的这段脚本配置了I2C_CHARDEV
选项。这个选项tristate
是一个三态配置选项,它意味着模块要么编译为内核,要么编译为内核模块
。当选项为Y时,表示编译入内核;当选项为M时,表示编译为模块;当选项为N时,表示不编译。如下图所示,“I2C device interface”选项设置为M,表示编译为内核模块。help
后面的内容为帮助信息,在单击“快捷键?”时,会显示帮助信息。
Kconfig语法
Kconfig语法较为简单,其语法在Documentation/kbuild/kconfig-language.txt
文件中做了介绍。归纳起来Kconfig的语法主要包括以下几个方面。
1、主要语法总览
Kconfig配置文件描述了一系列的菜单入口。除了帮助信息之外,每一行都以一个关键字开始,这些关键字如下:
config
menuconfig
choice/endchoice
comment
menu/endmenu
if/endif
前5个关键字都定义了一个菜单选项,if/endif
是一个条件选项。下面对常用的一些菜单语法进行说明。
2、菜单入口(config)
大多数内核配置选项都对应Kconfig中的一个菜单,该菜单可以在make menuconfig
中显示,写法如下:
config MODVERSIONS
bool "Set version infomation on all module symbols"
depends on MODULES
help
Usually, modules have to be recompiled whenever you switch to a new kernel...
每一行都以关键字开始,并可以有多个参数。config
关键字定义一个新的配置选项,之后几行定义了该配置选项的属性。属性可以有类型、输入提示(input prompt)、依赖关系、帮助信息和默认值等。
可以出现两个相同的配置选项,但每个选项只能有一个输入提示并且类型还不能冲突。
每个配置选项都必须指定一种类型,包括bool、tristate、string、hex和int
,其中tristate和string
是两种基本类型,其他类型都是基于这两种类型的。如下定义的是一个bool类型:
类型定义后面紧跟输入提示,这些提示将显示在配置菜单中。下面的两种方法可以用来输入提示:
方式1:
bool "Neworking support"
方式2:
bool
prompt "Networking support"
输入提示的一般语法如下:
prompt <prompt> ["if" <expr>]
其中prompt
是关键字,表示一个输入提示。<prompt>
是一个提示信息。可选项if
用来表示该提示的依赖关系。
默认值的语法如下:
default <expr> [if <expr>]
一个配置选项可以有多个默认值,但是只有第一个默认值是有效的。只有config
选项可能配置默认值。
依赖关系的如下:
depends on <expr>
如果定义了多个依赖关系,那么可以用"&&"来连接,表示与的关系。依赖关系可以应用到菜单的所有其他选择中,下面两个例子是等价的。
例子1:
bool "foo" if BAR #如果定义BAR选项,那么就使能foo选项
default y if BAR #如果定义BAR选项,那么foo的默认值就是y,表示编译入内核
例子2:
depends on BAR # foo选项的可配置与否,依赖于BAR选项
bool "foo"
default y
depends
能够限定一个config
选项的能力,即如果A依赖于B,则在B被配置为Y的情况下,A可以为Y、M、N;在B被配置为M的情况下,A可以为M,N;在B被配置为N的情况下,A只能为N,表示禁用该功能。
help(或者---help---)
begin
...
end
可以用“help”或者“—help—”定义帮助信息。帮助信息可以在开发人员配置内核时给出提示。
3、菜单结构(menu)
菜单结构一般作为菜单入口的父菜单。菜单入口在菜单结构中的位置可由两种方式决定。第一种方式如下:
menu "Network device support"
depends on NET
config NETDEVICES
...
endmenu
menu
和endmenu
为菜单结构关键字,处于其中的config
选项是菜单入口。菜单入口NETDEVICES
是菜单结构Network device support
的子菜单。depends on NET
是主菜单menu
的依赖项,只有在配置NET
的情况下,才可以配置Network device support
菜单项。而且,所有子菜单选项都会继承父菜单的依赖关系,例如,Network device support
对NET
的依赖将被加到配置选项NETDEVICES
的依赖关系中。
第二种方式是通过分析依赖关系生成菜单结构。如果一个菜单选项在一定程度上依赖另一个菜单选项,那么它就成为该选项的子菜单。如果父菜单选项为Y或者M,那么子菜单可见;如果父菜单为N,那么子菜单就不可见。例如:
config MODULES
bool "Enable loadable module support"
config MODVERSIONS
bool "Set version information on all module symbols"
depends on MODULES
comment "module support disabled"
depends on !MODULES
由语句“depends on MODULES”可知,MODVERSIONS
直接依赖于MODULES
,所以MODVERSIONS
是MODULES
的子菜单。如果MODULES
不为N,那么MODVERSIONS
是可见的。
4、选择菜单(choice)
选择菜单定义一组选项。此选项的类型只能是boolean
或tristate
型。该选项的语法如下:
"choice"
<choice options>
<choice block>
"endchoice"
在一个硬件有多个驱动的情况下可以使用choice
菜单,使用choice
菜单可以实现最终只有一个驱动被编译进内核中。choice
菜单可以接受的另一个选项是optional
,这样选项被设置为N,表示内核没有被选中。
5、注释菜单(comment)
注释菜单定义了配置过程中显示给用户的注释。此注释也可以被输出到文件中,以备查看。注释的语法如下:
comment <prompt>
<comment options>
在注释中唯一可以定义的属性是依赖关系,其他的属性不可以定义。
应用实例:在内核中新增加add_sub模块
下面讲解一个综合实例,假设我们将要在内核中添加一个add_sub
模块。考虑add_sub
模块的功能,决定将该模块加到内核源码的drivers
目录中。在drivers
目录中增加一个add_sub_Kconfig
子目录。add_sub
模块的源码目录add_sub_Kconfig
如下:
在内核中增加了子目录,需要为相应的目录创建Kconfig
和Makefile
文件,才能对模块进行配置和编译。同时子目录的父目录中的Kconfig
和Makefile
文件也需要修改,以使子目录中的Kconfig
和Makefile
文件能够被引用。
在新增加的add_sub_Kconfig
目录中,应该包含如下的Kconfig
文件:
#
# add_sub configuration
#
menu "ADD_SUB" #主菜单
comment "ADD_SUB"
config CONFIG_ADD_SUB #子菜单,添加add_sub模块的功能
boolean "ADD_SUB support"
default y
#子菜单,添加test模块的功能,只有配置CONFIG_ADD_SUB选项时,该菜单才会显示
config CONFIG_TEST
tristate "ADD_SUB test support"
depends on CONFIG_ADD_SUB #依赖CONFIG_ADD_SUB
default y
endmenu
由于ADD_SUB
对于内核来说是新功能,所以首先需要创建一个ADD_SUB
菜单;然后用comment
显示ADD_SUB
,等待用户选中;接下来判断用户是否选择了ADD_SUB,如果选择了ADD_SUB,那么将显示ADD_SUB support,该选项默认设置为Y,表示编译入内核。接下来,如果ADD_SUB support被配置为Y,即变量CONFIG_ADD_SUB=y,那么将显示ADD_SUB test support,此选项依赖于CONFIG_ADD_SUB。由于CONFIG_TEST可以被编译入内核,也可以编译为内核模块,所以这里的选项类型设置为tristate
。
为了使这个Kconfig
文件能起作用,需要修改linux-2.6.29.4/drivers/Kconfig文件,在文件的末尾增加以下内容:
source "drivers/add_sub_Kconfig/Kconfig"
脚本中的source
表示引用新的Kconfig文件,参数为Kconfig文件的相对路径名。同时为了使add_sub
和test
模块能够被编译,需要在add_sub_Kconfig
目录中增加一个Makefile文件,该Makefile文件如下:
obj-$(CONFIG_ADD_SUB) +=add_sub.o
obj-$(CONFIG_TEST) +=test.o
变量CONFIG_ADD_SUB
和CONFIG_TEST
就是Kconfig
文件中定义的变量。该脚本根据配置变量的取值构建obj-*列表
。例如obj-$(CONFIG_ADD_SUB)
等于obj-y
时,表示构建add_sub.o
模块,并编译入内核中;当obj-$(CONFIG_ADD_SUB)
等于obj-n
时,表示不构建add_sub.o
模块;当obj-$(CONFIG_ADD_SUB)
等于obj-m
时,表示单独编译模块,不放入内核中。
为了使整个add_sub_Kconfig
目录能够引起编译器的注意,add_sub_Kconfig
的父目录drivers中的Makefile也需要增加如下脚本:
obj-$(ADD_SUB) +=add_sub_Kconfig/
在linux-2.6.29.4/drivers/Makefile中添加obj-$(ADD_SUB)+=add_sub_Kconfig/,使得用于在进行内核编译时能够进入add_sub_Kconfig
目录中。增加了Kconfig和Makefile文件之后的新的add_sub_Kconfig树形目录如下:
对add_sub模块进行配置
当将add_sub模块的源文件加入内核源代码中后,需要对其进行配置,才能编译模块。配置的步骤如下:
(1)在内核源代码目录中执行make menuconfig
命令。
(2)选择Device Drivers
选项,再选择Select
选项。
(3)在进入的界面中选择ADD_SUB选项,该选项就是Kconfig文件中menu
菜单定义的,再选择Select选项。
(4)如下图所示,有ADD_SUB support和ADD_SUB test support两个选项可以选择。其中ADD_SUB support
是ADD_SUB test support
的父选项,只有在ADD_SUB support
选中时,才能对ADD_SUB test support
进行选中。图中*
表示选中的意思;如果为空,表示不选中。
小结
本篇博客主要讲解了怎样构建一个驱动程序。首先讲解了为什么要升级内核。然后对Hello World程序进行了简单介绍。在这个基础上,又详细地讲解了模块之间的通信,这些都是驱动程序开发的基础。最后讲解了怎样将模块加入到内核中,让模块运行起来。