Chinaunix首页 | 论坛 | 博客
  • 博客访问: 152165
  • 博文数量: 116
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 20
  • 用 户 组: 普通用户
  • 注册时间: 2017-08-21 15:04
文章分类

全部博文(116)

文章存档

2014年(1)

2013年(13)

2012年(27)

2011年(49)

2010年(26)

分类: 嵌入式

2010-07-02 10:20:11

  就整个kernel运行在单个保护域而言,Linux kernel 成为了“单内核”,但是Linux kernel是组件模式的,在运行时允许“代码” 动态地插入到kernel和从kernel中移除。

模块就是相关的一些子程序,数据,入口点和出口点共同组合成的一个单一的二进制映像,即一个可装载的 kernel目标文件。模块的支持使得系统可以拥有一个最小的基本的内核映像,

并且通过模块的方式支持一些可选的特征和驱动程序。模块对内核代码动态地插 入到kernel中和从kernel中移除提供了一种简便方法;它有助于调试内核程序(例如,当我们在

板子上调试一个驱动程序时,可以采用模块的方式,也 可以采用静态的编译到内核当中,如果采用后者,每次修改驱动程序的时候,都必须重新烧写内核;如果采用前者,只需把驱动

程序下载到板子,然后动态插入到内 核);当有新的设备“热插”到的系统时,可以通过模块方式按需装载新的驱动程序。在这章中,我们将学习模块的基本知识和实现模块的基本原理

以及如何写自己 的模块。

1. Hello World

   模块的开发很像写一个应用程序,它有自己的入口点,出口点和自己的“生活空间”。下面我们一起来写“Hello World”内核模块,借此学习写内核模块的一般步骤。
    /*
 * hello.c  Hello, World! As a Kernel Module
 */
#include 
#include 
#include 

/*
 * hello_init  the init function, called when the module is loaded.
 * Returns zero if successfully loaded, nonzero otherwise.
 */
static int hello_init(void)
{
        printk(KERN_ALERT "I bear a charmed life.\n" ) ;
        return 0;
}

/*
 * hello_exit  the exit function, called when the module is removed.
 */
static void hello_exit(void)
{
        printk(KERN_ALERT "Out, out, brief candle!\n" ) ;
}

module_init(hello_init) ;
module_exit(hello_exit) ;

MODULE_LICENSE("GPL" ) ;
MODULE_AUTHOR("Shakespeare" ) ;
    通 过module_init()把hello_init()函数注册为这个模块的入口点。当装载模块的时候,内核调用hello_init()函数。 module_init()是一个宏,它的任务是把它
的惟一参数作为相应模块的初始化函数。所有初始化函数必须有下列形 式:int my_init(void);
由于初始化函数不会被外部的代码直接调用,因此你不必export这个函数,如果把这个函数标志为static,
将会更加合理。初始化函数返回一个int型整数,如果初始化成功,返回零,如果初始化失败,返回非零。这里的初始化函数仅仅打印了一句话,然后返回零。在实际开发的模块里,
初始化函数一般是注册资源,为数据结构分配内存等等。如果我们选择把这个文件静态地编译到内核的image内,那么也会保留初始化函数,并且在内核启动的时候运行。
    通 过module_exit()把hello_exit()函数注册为这个模块的出口点。当模块从内存中移除的时候,内核调用hello_exit()。退 出函数在返回之前,必须清除模块占用的资源,
确保硬件处于一致状态等等。退出函数返回之后,模块从内核中移除。
    退出函数必须有下列形式:void my_exit(void); 似于初始化函数,你把这个函数标志为static,将会更加合理。
    如果这个文件编译到静态的内核image内,退出函数不会包含在内核image之内,它也不会被调用。因为,如果不是以模块方式插入内核,代码永远不会从内存中删除。
    宏 MODULE_LICENSE()用于指定这个文件的copyright license。如果装载一个non-GPL模块到内存,从而导致在内核中设置 了tainted flag。这个flag仅仅为开发者
提供说明信息的目的。如果设置了tainted flag,许多内核开发者会给出bug报告。进一 步说,non-GPL模块不能调用GPL-only 符号。
    最后,宏MDULE_AUTHOR()指定了这个文件的作者。宏的值完全是为了提供说明信息的目的。
2. Building Modules
   在编译模块之前,我们必须决定模块的源码放在哪里。你可以把模块的源码增加到内核源码的一个合适的地方,既可以把你的文件作为一个”patch”,最终也可以把你的代码合到官方的源码树内。另外一种选择是在源码树之外维护和编译你自己的模块。
2.1 At Home in the Source Tree
    从理想的角度出发,你的模块是官方Linux的一部分,这样,模块的代码可以生存在内核的源码树内。如果你的工作彻底地放入内核之中,那么首先你需要对模块代码要更多的维护观念,这也是通常首选的方案。
    第 一步是决定你的模块将要生活在内核源码树的哪个地方。假设我们要写一个通过USB线把手机当作PC机的一台联机小电脑的驱动程序,那么这个驱动程序放在内 核源码树的哪个
地方比较合适呢?我们进入到内核源码树根目录下的drivers/目录下看一看,各个子目录的组织是按照驱动程序的class, type 来实现的。我们很容易发现有个usb/子目录,
在usb/子目录之内,我们又可以找到gadget/子目录,在gadget/子目录内可以发现许多驱动程 序,有的用来实现通过USB线把手机当作u盘,有的用来实现通过USB线把手机当作modem,等等,
物以类聚,把手机当作PC机的一台联机小电脑的驱动 程序生活在gadget/子目录里看来很合适,但是由于实现这个驱动程序需要许多文件,因此,我们想在gadget/子目录
下再创建online/子目 录,让我们新创建的相关文件存放这个目录下。
    在gadget/子目录下创建online/子目录之后,我们必须在gadget/的Makefile文件中添加一行:                 
obj-m += online/
    目 的在于编译模块的时候,告诉build系统找到online/子目录。一般情况下,为了控制是不是需要编译驱动程序,我们的使用一个特定的配置选项来达到 这个目的,
例如,使用CONFIG_USB_GADGET_ONLINE(在第五节会讲述如何添加一个新的配置选项)。因此我们可以把上面添加的一行修改 为:
obj-$(CONFIG_USB_GADGET_ONLINE) += online/
    最后,我们在online/下创建新文件Makefile,并且添加下面一行到其内。
obj-m += netmeeting.o
    到这一步,build系统能够沿着源码树往下找到online/子目录,并且根据netmeeting.c文件建立netmeeting.ko模块。虽然在Makefile文件中netmeeting.o的扩展名是.o,但是编译后模块的扩展名是.ko。
    一般情况下,为了控制是不是需要编译该驱动程序,我们的使用一个特定的配置选项来达到这个目的,例如:
obj-$(CONFIG_USB_GADGET_ONLINE) += netmeeting.o
    当然,实现手机当作PC机的一台联机小电脑的驱动程序需要许多文件,那么Makefile又该如何写呢?
obj-$(CONFIG_USB_GADGET_ONLINE) += netmeeting.o
netmeeting-objs := one.o two.o three.o four.o
    这样,netmeeting.ko 由one.c, two.c, three.c 和four.c四个文件编译连接而成。
    最后,如果你想为这些文件指定额外的gcc编译选项,在Makefile文件中添加类似于以下一行:
EXTRA_CFLAGS += -ONLINE_NETMEETING
    如果我们不需要创建一个子目录来存放新添加的文件,而是放新添加的文件在gadget/目录下,那么我们把在online/下的Makefile内的所有内容移到gadget/目录下的Makefile文件中。
    如 果模块是否编译由配置选项来控制,然而你又想编译此模块,那么在编译之前,必须把配置选项打开。在源代码树kernel/目录下,运行

make menuconfig,在drivers/usb/gadget下并没有发现我们添加的online目录,这究竟什么回事?我们学了第五节如何 添加一个新的配置选项后,问题就会迎刃而解。
2.2 Living Externally
   如果我们在源码树之外维护和建立模块,那么在自己的源码目录下创建Makefile文件,并且添加:
obj-m := netmeeting.o
   这样会把netmeeting.c编译成netmeeting.ko。 如果有多个源文件,那么在Makefile文件中添加:
obj-m := netmeeting.o
netmeeting-objs := one.o two.o three.o four.o
   这样,one.c, two.c, three.c, four.c都编译到netmeeting.ko之内。
   与在内核源码树内添加新文件相比,主要的区别在于build过程。在内核源码树之外编译模块,需要使用命令make 去找到内核源码文件和基本的Makefile文件。
按照下面的方法做就行了。
make –C /kernel/source/location SUBDIRS=$PWD modules
   这里/kernel/source/location是已经配置过的内核源码树的位置。你最好不要把内核源码树放在/usr/src/linux之下,这个目录下存放的是系统安装时的内核源码树,
把你的内核源码树放到容易找的地方,比如home目录中。
2.2 Installing Modules
    编译后的模块要放在/lib/modules/version/kernel/。下面的命令把所有编译后的模块放到相应的目录之下 (需要root权限)。
make modules_install
2.3 Generating Module Dependencies
    Linux 模块实用工具能够理解模块间的依赖性。这意味着,如果模块chum依赖于模块bait,那么当装载模块chum时,模块bait会自动装载。这种依赖性信 息不是凭空存在的,
而是事先要建立起来。大多数发行的Linux能够自动产生这种映射关系,并且在每次启动的时候,建立最新的依赖关系。
为了建立模块间的 依赖信息,在root权限下运行如下命令:
depmod
    为了快速更新模块间的依赖信息,即如果有新模块,重新建立依赖信息;如果没有新模块,会保留原来的依赖信息。在root权限下运行如下命令:
depmod –A
    模块间的依赖信息存放在文件/lib/modules/version/modules.dep之中。
2.4 Loading Modules
    用insmod装载模块是一种最简单的方法。它请求kernel装载指定的模块。insmod不会检查模块间的依赖关系,也不会执行是否有错误的检查。用法很简单。在root权限下,运行下列命令:
insmod module
    这里module是模块名。
    同样,移除模块,使用rmmod。在root权限下,运行下列命令:
rmmod module
    这两个实用工具,虽然用法简单,但是缺乏智能性。实用工具modprobe提供了依赖关系的解决方案,智能的错误检查和汇报,等等。装载模块的时候,是我们的首选。
    用modprobe装载模块,在root权限下,运行下列命令:
modprobe module { module parameters }
    这里module是模块名。模块的参数(请看第七节)是要传递到模块的参数。有点像DOS下执行程序的时候附带几个参数。
    modprobe命令不仅试图装载写在其后的模块,还试图装载它依赖的所有模块。因此,我们要首选它。
    modprobe也可以用于从kernel中移除模块,在root权限下,运行下列命令:
modprobe r modules
    这里modules指的是要移除的模块名,可以是多个模块名。不像rmmod仅移除指定的模块,modprobe还移除它依赖的并且不在使用中的其它模块。
2.5 Managing Configuration Options
    在 2.6 kernel中,由于有了新的 “kbuild”系统,因此增添一项新的配置是相当容易的。就是在Kconfig文件中增添一项新的配置内容。 Kconfig文件用于衔接整个kernel源码树。
对于驱动程序而言,Kconfig文件在驱动程序源码的同级目录。如果你的驱动程序在 drivers/usb/gadget/下,那么你用的是driversusb/gadget/Kconfig。
    如果你创建了一个新目录,那么要在其下创建新的Kconfig文件,并且从一个已存在的Kconfig中“source”它。即在已存在的Kconfig(例如,drivers/usb/gadget/Kconfig)中添加类似下面的一行:
source “drivers/usb/gadget/online/Kconfig”
    然后在新建的Kconfig中添加类似以下的内容:
config USB_GADGET_ONLINE
  tristate “ Gadget Netmeeting support”
  default n
  help 
If you say Y here, netmeeting driver will be compiled into the kernel. You can also say M here and the driver will be built as a module named netmeeting.ko
If unsure, say N.
第一行定义了配置选项。事先已经假定存在有CONFIG_前缀,因此用不着我们写。

第二行说明了这个配置选项是三态的,即有三种选择方式,第一种是选择Y,表示相对应的程序编译到kernel之内;
第二种是选择M,表示相对应的程序编译成 模块,第一种是选择N,表示不编译相对应的程序.如果没有编译成模块这个选项,
可以用bool代替tristate。指令tristate后带引号的文本 是配置选项名,用于各种配置实用程序的选项显示。

第三行为这个配置选项指定一个默认值,这里是表示选择了n。
第四行help指令表示其后面是帮助文本。有助于用户和开发者理解相应的程序和建立自己的内核。

还有其他另外一些指令。比如depends指令用于指定要使这个配置选项有效,则必须先要设置其它配置选项有效。如果这种依赖关系不能满足,那么配置选项就会失效。例如,如果你添加了如下的指令:
depends on PXA27X
要使CONFIG_GADGET_NETMEETING这个配置选项有效,必须首先要使CONFIG_PXA27X这个配置选项有效。
指令select与depends有些类似,不同点在于:如果选了当前配置选项,那么select后的配置选项也会选中。由于指令select会自动“打开”其他配置选项,因此它不如depends常用。用法如下:
select PXA27X
如果打开CONFIG_GADGET_NETMEETING配置选项,那么自动打开CONFIG_PXA27X配置选项。
对于select和depends指令,使用&&,||和!组合多个配置选项。例如:
depends on DUMB_DRIVERS && !ONE_DRIVER
意思是仅当CONFIG_DUMB_DRIVERS配置选项打开而CONFIG_ONE_DRIVER配置选项关闭时,CONFIG_GADGET_NETMEETING配置选项才打开。

指令if可以跟在指令tristate和bool之后,这样为配置选项设置了一个条件选项。如果条件不符合,不仅关闭了配置选项甚至在配置实用工具内也不会出现这个配置选项。例如:

bool “Deep Sea Mode” if OCEAN
其表示只有配置选项CONFIG_OCEAN打开之后,配置实用工具才会显示配置选项名Deep Sea Mode,而且也会打开CONFIG_GADGET_NETMEETING配置选项。

指令if也可以跟在指令default之后,其意思是仅当条件成立时,才会赋默认值。
    为了更容易地建立配置,配置系统提供了几个meta-options。当且仅当用户希望打开设计用来禁止关键特性(比如在嵌入式系统上保留精确的内存)的选 项时,那么打开配置选项 CONFIG_ EMBEDDED( This option allows certain base kernel options and settings to be disabled or tweaked. This is for

specialized environments which can tolerate a “non- standard” kernel. Only use this if you really know what you are doing)。
配 置系统CONFIG_BROKEN_ON_SMP是用于指定一个驱动程序不是SMP-safe。通常这个选项是没有打开的,这样强制用户清晰地认识到一个 驱动程序在SMP环境下具有“破坏性”。
当然,新开发的驱动程序,不要使用这个选项。
   最后,CONFIG_EXPERIMENTAL配置选项用于标志一个驱动程序是处于试验阶段或者beta版本阶段,这个选项默认是关闭的,这使得用户在使用驱动程序之前清晰地认识到其中的风险性。
2.6 Module Parameters
    对于如何向模块传递参数,Linux kernel 提供了一个简单的框架。其允许驱动程序声明参数,并且用户在系统启动或模块装载时为参数指定相应值,在 驱动程序里,
参数的用法如同全局变量。这些模块参数也能够在sysfs中显示出来。结果,有许许多多的方法用来创建和管理模块参数。
<1> 通过宏module_param()定义一个模块参数:
module_param(name, type, perm);
    这 里,name既是用户看到的参数名,又是模块内接受参数的变量;
type表示参数的数据类型,是下列之一:byte, short, ushort, int, uint, long, ulong, charp, bool, invbool。
这些类型分是:
a byte, a short integer,an unsigned short integer, an integer, an unsigned integer, a long integer, an unsigned longinteger, a pointer to a char,
a Boolean,a Boolean whose value is inverted from what the user specifies.The byte type is stored in a single char and the Boolean types
are stored in variables of type int. The rest are stored in the corresponding primitive C types.
最 后,perm指定了在sysfs中相应文件的访问权限。访问权限用通常的八进制格式来表示,例如,用0644(表示ower具有读写权限,group和 everyone只读权限),
或者用通常的S_Ifoo定义,例如,S_IRUGO | S_IWUSR (表示everyone具有读权限,用户具有 写权限)。用0表示完全关闭在sysfs中相对应的项。
   其实宏不会声明变量,因此在使用宏之前,必须声明变量。所以,典型地用法如下:
static unsigned int use_acm = 0; 
module_param(use_acm, uint, S_IRUGO);
这些必须写在模块源文件的开头部分。即use_acm是全局的。

<2>我们也可以使模块源文件内部的变量名与外部的参数名有不同的名字。这通过宏module_param_named()定义。
module_param_named(name, variable, type, perm);
这里name是外部可见的参数名,variable是源文件内部的全局变量名。
例如:
static unsigned int max_test = 9;
module_param_name(maximum_line_test, max_test, int, 0);

<3>如果模块参数是一个字符串时,通常使用charp类型定义这个模块参数。内核复制用户提供的字符串到内存,并且相对应的变量指向这个字符串。
例如:static char *name;
module_param(name, charp, 0);

<4>另一种方法是通过宏module_param_string()让内核把字符串直接复制到程序中的字符数组内。
module_param_string(name, string, len, perm);
这里,name是外部的参数名,string是内部的变量名,len是以string命名的buffer大小(可以小于buffer的大小,但是没有意义),perm表示sysfs的访问权限(或者perm是零,表示完全关闭相对应的sysfs项)。
例如:
static char species[BUF_LEN];
module_param_string(specifies, species, BUF_LEN, 0);

<5>上面说得只是给模块传入一个参数的情况,如果给模块传入多个参数,那该怎么办呢?可以通过宏module_param_array()给模块传入多个参数。
用法如下:
module_param_array(name, type, nump, perm);
    这里,name既是外部模块的参数名又是程序内部的变量名,type是数据类型,perm是sysfs的访问权限。指针nump指向一个整数,其值表示有多少个参数存放在数组name中。
值得注意是name数组必须静态分配。
例如:static int finsh[MAX_FISH];
static int nr_fish;
module_param_array(fish, int, &nr_fish, 0444);

<6>通过宏module_param_array_named()使得内部的数组名与外部的参数名有不同的名字。
例如:module_param_array_named(name, array, type, nump, perm);
这里的参数意义与其它宏一样。

<7>最后,通过宏MODULE_PARM_DESC()对参数进行说明:
static unsigned short size = 1;
module_param(size, ushort, 0644);
MODULE_PARM_DESC(size, “The size in inches of the fishing pole” \“
                 connected to this computer.” );
使用这些宏时需要包含头文件

2.7 Exported Symbols

    当装载模块的时候,模块动态地链接入内核之中。动态链接的二进制代码只能调用外部函数,然而,外部函数必须明确地输出,在内核中,通过
EXPORT_SYMBOL()和EXPORT_SYMBOL_GPL来达到这个目的。输出的函数可以被其它模块调用。没有输出过的函数不能被其它模块调用。模块比核心内核映像代码具有更严格的链接和调用规则。
因为所有核心源文件链接成一个单一的作为基础的映像,因此在内核中核心代码可以调用任何非静态的接口。当然,输出符号也必须是非静态属性。一套输出的内核符号称之为输出的内核接口,甚至称之为kernel API。
输出一个内核符号是举手之劳之事。当函数声明之时,在其后EXPORT_SYMBOL()把函数输出。
例如:
/* it will receive control requests including set_configuration(), which enables non-control requests. 
*/
int usb_gadget_register_driver(struct usb_gadget_driver *driver)
{

}
EXPORT_SYMBOL(usb_gadget_register_driver) ;
    从此以后,任何模块都可以调用函usb_gadget_register_driver(),只要在源文件中包含声明这个函数的头文件,或者extern这个函数的声明。
    有些开发者希望他们的接口只让遵从GPL的模块调用。通过MODULE_LICENSE()的使用,内核链接器能够强制保证做到这点。如果你希望前面的函数仅被标有GPL许可证的模块访问,那么你可以用如下方式输出符号:
EXPORT_SYMBOL_GPL(usb_gadget_register_driver);
    如果你的代码配置为模块方式,那么必须确保:源文件中使用的所有接口必须是已经输出的符号,否则导致在装载时链接错误。
2.8 Wrapping Up Modules
    这章我们学习了如何写模块,建立模块,装载和卸载模块。我们讨论了什么是模块,Linux如何动态装载模块代码,而且我们还讨论了模块的参数和输出符号。

阅读(699) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~