分类: LINUX
2013-05-07 10:22:53
内核模块是Linux内核向外部提供的一个插口,其全称为动态可加载内核模块(Loadable Kernel Module,LKM),我们简称为模块。Linux内核之所以提供模块机制,是因为它本身是一个单内核(monolithic kernel)。单内核的最大优点是效率高,因为所有的内容都集成在一起,但其缺点是可扩展性和可维护性相对较差,模块机制就是为了弥补这一缺陷。
一、 什么是模块
模块是具有独立功能的程序,它可以被单独编译,但不能独立运行。它在运行时被链接到内核作为内核的一部分在内核空间运行,这与运行在用户空间的进程是不同的。模块通常由一组函数和数据结构组成,用来实现一种文件系统、一个驱动程序或其他内核上层的功能。
二、 编写一个简单的模块
模块和内核都在内核空间运行,模块编程在一定意义上说就是内核编程。因为内核版本的每次变化,其中的某些函数名也会相应地发生变化,因此模块编程与内核版本密切相关。以下例子针对2.6内核
1.程序举例
hellomod.c
001
// hello world driver for Linux 2.6
004 #include
009 static int __init lkp_init( void )
{
printk(“<1>Hello,World! from the kernel space…\n”);
return 0;
013 }
015 static void __exit lkp_cleanup( void )
{
printk(“<1>Goodbye, World! leaving kernel space…\n”);
018
}
020 module_init(lkp_init);
021 module_exit(lkp_cleanup);
022 MODULE_LICENSE(“GPL”);
.说明 第4行: 所有模块都要使用头文件module.h,此文件必须包含进来。 第5行: 头文件kernel.h包含了常用的内核函数。 第6行: 头文件init.h包含了宏_init和_exit,它们允许释放内核占用的内存。 建议浏览一下该文件中的代码和注释。 第9-12行: 这是模块的初始化函数,它必需包含诸如要编译的代码、初始化数据结构等内容。 第11行使用了printk()函数,该函数是由内核定义的,功能与C库中的printf()类似, 它把要打印的信息输出到终端或系统日志。字符串中的<1>是输出的级别, 表示立即在终端输出。 第15-18行: 这是模块的退出和清理函数。此处可以做所有终止该驱动程序时相关的清理工作。 第20行: 这是驱动程序初始化的入口点。对于内置模块,内核在引导时调用该入口点; 对于可加载模块则在该模块插入内核时才调用。 第21行: 对于可加载模块,内核在此处调用module_cleanup()函数,而对于内置的模块, 它什么都不做。 第22行: 提示可能没有GNU公共许可证。有几个宏是在2.4版的内核中才开发的(详情参见modules.h)。 函数module_init()和cleanup_exit()是模块编程中最基本也是必须的两个函数。 module_init()向内核注册模块所提供的新功能, 而cleanup_exit()注销由模块提供的所有功能。
模块编程属于内核编程,因此,除了对内核相关知识有所了解外,还需要了解与模块相关的知识。
1.应用程序与内核模块的比较
为了加深对内核模块的了解,表一给出应用程序与内核模块程序的比较。
表一 应用程序与内核模块程序的比较
C语言应用程序 内核模块程序 使用函数 Libc库 内核函数 运行空间 用户空间 内核空间 运行权限 普通用户 超级用户 入口函数 main() module_init() 出口函数 exit() module_exit() 编译 Gcc –c Makefile 连接 Gcc insmod 运行 直接运行 insmod 调试 Gdb kdbug, kdb,kgdb等 从表一我们可以看出,内核模块程序不能调用libc库中的函数,它运行在内核空间,且只有超级用户可以对其运行。另外,模块程序必须通过module_init()和module-exit()函数来告诉内核“我来了”和“我走了”。
2.内核符号表(如果对以下第2~4点理解上有困难,可以越过)
如 前所述,Linux内核是一个整体结构,像一个圆球,而模块是插入到内核中的插件。尽管内核不是一个可安装模块,但为了方便起见,Linux把内核也看作 一个“母”模块。那么模块与模块之间如何进行交互呢,一种常用的方法就是共享变量和函数。但并不是模块中的每个变量和函数都能被共享,内核只把各个模块中 主要的变量和函数放在一个特定的区段,这些变量和函数就统称为符号。到低哪些符号可以被共享? Linux内核有自己的规定。对于内核这个特殊的母模块,在kernel/ksyms.c中定义了从中可以“移出”的符号,例如进程管理子系统可以“移出”的符号定义如下:
/* 进程管理 */
EXPORT_SYMBOL(do_mmap_pgoff);
EXPORT_SYMBOL(do_munmap);
EXPORT_SYMBOL(do_brk);
EXPORT_SYMBOL(exit_mm);
…
EXPORT_SYMBOL(schedule);
EXPORT_SYMBOL(jiffies);
EXPORT_SYMBOL(xtime);
…
你可能对这些变量和函数已经很熟悉。其中宏定义EXPORT_SYMBOL()本身的含义是“移出符号”。为什么说是“移出”呢?因为这些符号本来是内核内部的符号,通过这个宏放在一个公开的地方,使得装入到内核中的其他模块可以引用它们。
实际上,仅仅知道这些符号的名字是不够的,还得知道它们在内核地址空间中的地址才有意义。因此,内核中定义了如下结构来描述模块的符号:
struct module_symbol
{
unsigned long value; /*符号在内核地址空间中的地址*/
const char *name; /*符号名*/
};
我们可以从/proc/ksyms文件中读取所有内核模块“移出”的符号,这所有符号就形成内核符号表,其格式如下:
内存地址 符号名 [所属模块]
在模块编程中,可以根据符号名从这个文件中检索出其对应的地址,然后直接访问该地址从而获得内核数据。第三列“所属模块”指符号所在的模块名,对于从内核这一母模块移出的符号,这一列为空。
模块加载后,2.4内核下可通过 /proc/ksyms、 2.6 内核下可通过/proc/kallsyms查看模块输出的内核符号
3.模块依赖
如前所述,内核符号表记录了所有模块可以访问的符号及相应的地址。当一个新的模块被装入内核后,它所申明的某些符号就会被登记到这个表中,而这些符号可能被其他模块所引用,这就引出了模块依赖这个问题。
一个模块A引用另一个模块B所移出的符号,我们就说模块B被模块A引用,或者说模块A依赖模块B。如果要链接模块A,必须先链接模块B。这种模块间相互依赖的关系就叫模块依赖。
4.模块引用计数器
为 了确保模块安全地卸载,每个模块都有一个引用计数器。当执行模块所涉及的操作时就递增计数器,在操作结束时就递减这个计数器;另外,当模块B被模块A引用 时,模块B的引用计数就递增,引用结束,计数器递减。什么时候可以卸载这个模块?当然只有这个计数器值为0的时候,例如,当一个文件系统还被安装在系统上 时就不能将其卸载,当这个文件系统不再被使用时,引用计数器就为0,于是可以卸载。
四.模块编译
Linux 中最重要的软件开发工具是 GCC。GCC 是 GNU 的 C 和 C++ 编译器。但是,在大型的开发项目中,通常有几十到上百个的源文件,如果每次均手工键入 gcc 命令进行编译的话,则会非常不方便。因此,人们通常利用make 工具来自动完成编译工作。利用这种自动编译可大大简化开发工作,避免不必要的重新编译。这些工作包括:如果仅修改了某几个源文件,则只重新编译这几个源文件;如果某个头文件被修改了,则重新编译所有包含该头文件的源文件。
1.编译工具make
实际上,make 工具通过一个称为 Makefile 的文件来完成并自动维护编译工作。Makefile 需要按照某种语法进行编写,其中说明了如何编译各个源文件并连接生成可执行文件,并定义了源文件之间的依赖关系。下面给出2.6 内核模块的Makefile模板(请参看Makefile的写法)
# Makefile2.6
obj-m += hellomod.o # 产生hellomod 模块的目标文件
CURRENT_PATH := $(shell pwd) #模块所在的当前路径
LINUX_KERNEL := $(shell uname -r) #Linux内核源代码的当前版本
LINUX_KERNEL_PATH := /usr/src/linux-headers-$(LINUX_KERNEL) #Linux内核源代码的绝对路径
all:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules #编译模块了
clean:
make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) clean #清理注意: 在每个命令前(例如make命令前)要键入一个制表符(按TAB键产生)
有了Makefile,执行make命令,会自动形成相关的后缀为.o和.ko文件。
到此,模块编译好了,该把它插入到内核了:
如:$insmod hellomod.ko当然,要以系统员的身份才能把模块插入。
成功插入后,可以通过dmesg命令查看,屏幕最后几行的输出就是你程序中输出的内容:Hello,World! from the kernel space…
当模块不再需要时,可以通过rmmod命令移去,例如$rmmod hellomod
modutils是管理内核模块的一个软件包。可以在任何获得内核源代码的地方获取Modutils(modutils-x.y.z.tar.gz)源代码,然后选择最高级别的patch.x.y.z等于或小于当前的内核版本,安装后在/sbin目录下就会有insomod、rmmod、ksyms、lsmod、modprobe等实用程序。当然,通常我们在加载Linux内核时,modutils已经被载入。
1.Insmod命令
调用insmod程序把需要插入的模块以目标代码的形式插入到内核中。在插入的时候,insmod自动调用init_module()函数运行。注意,只有超级用户才能使用这个命令,其命令格式为:
# insmod [path] modulename.c
2. rmmod命令
调用rmmod程序将已经插入内核的模块从内核中移出,rmmod会自动运行cleanup_module()函数,其命令格式为:
#rmmod [path] modulename.c
3.lsmod命令
调用lsmod程序将显示当前系统中正在使用的模块信息。实际上这个程序的功能就是读取/proc文件系统中的文件/proc/modules中的信息,其命令格式为:
#lsmod
4.ksyms命令
ksyms这个程序用来显示内核符号和模块符号表的信息。与lsmod相似,它的功能是读取/proc文件系统中的另一个文件/proc/kallsyms。
在此,我们将编写一个模块,其中有一个中断函数,当内核接收到某个 IRQ 上的一个中断时会调用它。先给出全部代码,读者自己调试,把对该程序的理解跟到本贴后面。
—————————————-
#include
#include
#includestatic int irq;
static char *interface;//MODULE_PARM_DESC(interface,”A network interface”); 2.4内核中该宏的用法
molule_parm(interface,charp,0644) //2.6内核中的宏
//MODULE_PARM_DESC(irq,”The IRQ of the network interface”);
module_param(irq,int,0644);static irqreturn_t myinterrupt(int irq, void *dev_id, struct pt_regs *regs)
{
static int mycount = 0;
if (mycount < 10) {
printk(“Interrupt!\n”);
mycount++;
}
return IRQ_NONE;
}
static int __init myirqtest_init(void)
{
printk (“My module worked!11111\n”);
if (request_irq(irq, &myinterrupt, SA_SHIRQ,interface, &irq)) {
printk(KERN_ERR “myirqtest: cannot register IRQ %d\n”, irq);
return -EIO;
}
printk(“%s Request on IRQ %d succeeded\n”,interface,irq);
return 0;
}
static void __exit myirqtest_exit(void)
{
printk (“Unloading my module.\n”);
free_irq(irq, &irq);
printk(“Freeing IRQ %d\n”, irq);
return;
}
module_init(myirqtest_init);
module_exit(myirqtest_exit);
MODULE_LICENSE(“GPL”);
—————————————-
这里要说明的是,在插入模块时,可以带两个参数,例如
insmod myirq.ko interface=eth0 irq=9
其中 具体网卡 irq的值可以查看 cat /proc/interrupts动手吧!以此为例,可以设计出各种各样有价值的内核模块,贴出来体验分享的快乐吧。
在上一部分“编写带有参数的中断模块”中,这个看似简单的程序,你调试并运行以后思考了哪些方面的问题?
(1)给模块传递参数,使得这个模块的扩展和应用有了空间,例如,在我的机器上查看/proc/interrupts
CPU0
0: 10655925 IO-APIC-edge timer
1: 9148 IO-APIC-edge i8042
6: 4 I O-APIC-edge floppy
7: 0 IO-APIC-edge parport0
8: 3 IO-APIC-edge rtc
9: 0 IO-APIC-fasteoi acpi
12: 41970 IO-APIC-edge i8042
15: 106157 IO-APIC-edge ide1
16: 57823 IO-APIC-fasteoi ioc0
17: 8090 IO-APIC-fasteoi eth0
18: 245 IO-APIC-fasteoi uhci_hcd:usb1, Ensoniq AudioPCI, usb
NMI: 0
LOC: 10249542
ERR: 0
MIS: 0然后,在插入模块时,你对每个中断都作为参数试运行一下,看看会出现什么问题?思考一下irq为0,3等值时,为什么插入失败?这就引出中断的共享和非共享问题,从而促使你分析Linux对共享的中断到底如何处理,共享同一个中断号的中断处理程序到底如何执行?
2. 对于myinterrupt()函数,可以进行怎样的改进,使得这个自定义的中断处理程序变得有实际意义?
static irqreturn_t myinterrupt(int irq, void *dev_id, struct pt_regs *regs)
{
static int mycount = 0;
if (mycount < 10) {
printk(“Interrupt!\n”);
mycount++;
}
return IRQ_NONE;
}比如,对于网卡中断,在此收集每一次中断发生时,从网卡接收到的数据,把其存入到文件中。以此思路,随你考虑应用场景了。
3. 模块机制给Linux内核的扩展和应用提供了方便的入口,在我们内核之旅 的电子杂志部分,针对内核相关的内容,每一部分都有相对比较实际的内核应用题目,感兴趣者可以去实践,前提是对内核相关内容的彻透理解。
在调试该程序的时候请保证调试了内的程序,并且对中断有了一定的学习。
#include
#include
#include
#includestatic int irq;
static char *interface;module_param(interface,charp,0644);
module_param(irq,int,0644);static int mycount = 0;
static long mytime = 0;static unsigned long data=0;
static struct tasklet_struct mytasklet;//定义小任务
//小任务函数
static void mylet(unsigned long data)
{
printk(“tasklet running.\n”);
if(mycount==0)
mytime=jiffies;if (mycount < 10)
{
mytime=jiffies-mytime;
printk(“Interrupt number %d –time %ld \n”,irq,mytime);
mytime=jiffies;
}
mycount++;return;
}
//中断服务程序
static irqreturn_t myinterrupt(int intno,void *dev_id)
{
tasklet_schedule(&mytasklet);//调度小任务,让它运行
return IRQ_NONE;
}static int __init mytasklet_init(void)
{
printk(“init…\n”);
tasklet_init(&mytasklet, mylet,data);//初始化小任务
tasklet_schedule(&mytasklet);if (request_irq(irq,&myinterrupt,IRQF_SHARED,interface,&irq))
{
printk(KERN_ERR “myirqtest: cannot register IRQ %d\n”, irq);
tasklet_kill(&mytasklet);//删除小任务
free_irq(irq,&irq);//释放中断
return -EIO;
}
printk(“%s Request on IRQ %d succeeded\n”,interface,irq);return 0;
}static void __exit mytasklet_exit(void)
{
tasklet_kill(&mytasklet);//删除小任务
free_irq(irq,&irq);//释放中断
printk(“Freeing IRQ %d\n”, irq);printk(“exit…\n”);
return;
}MODULE_AUTHOR(“Helight.Xu”);
MODULE_LICENSE(“GPL”);module_init(mytasklet_init);
module_exit(mytasklet_exit);
在调试该程序的时候请保证调试了内的程序,并且对中断有了一定的学习。
/*myirq.c*/
#include
#include
#includestatic int irq;
static char *interface;module_param(interface,charp,0644);
module_param(irq,int,0644);//static irq_handler_t myinterrupt(int irq, void *dev_id, struct pt_regs *regs)
static irqreturn_t myinterrupt(int irq, void *dev_id)
{
static int mycount = 0;
static long mytime = 0;
struct net_device *dev=(struct net_device *)dev_id;if(mycount==0){
mytime=jiffies;
}
//count the interval between two irqs
if (mycount < 10) {
mytime=jiffies-mytime;
printk(“Interrupt number %d — intterval(jiffies) %ld — jiffies:%ld \n”, irq,mytime, jiffies);
mytime=jiffies;
//printk(“Interrupt on %s —–%d \n”,dev->name,dev->irq);
}mycount++;
return IRQ_NONE;
}static int __init myirqtest_init(void)
{
printk (“My module worked!\n”);
//regist irq
//if (request_irq(irq,&myinterrupt,SA_SHIRQ,interface,&irq)) { //early than 2.6.23
if (request_irq(irq,&myinterrupt,IRQF_SHARED,interface,&irq)) { //later than 2.6.23
printk(KERN_ERR “myirqtest: cannot register IRQ %d\n”, irq);
return -EIO;
}
printk(“%s Request on IRQ %d succeeded\n”,interface,irq);return 0;
}static void __exit myirqtest_exit(void)
{
printk (“Unloading my module.\n”);
free_irq(irq, &irq); //release irq
printk(“Freeing IRQ %d\n”, irq);return;
}module_init(myirqtest_init);
module_exit(myirqtest_exit);MODULE_AUTHOR(“Helight.Xu”);
MODULE_LICENSE(“GPL”);编译使用该模块:
使用Makefile文件的内容如下
obj-m := myirq.o
KERNELDIR := /usr/src/kernels/linux-2.6.24/
all:
make -C $(KERNELDIR) M=$(PWD) modules
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions
在查看 /proc/interrupts文件后,确定要共享的中断号(应为该程序是共享中断号的),使用下面的命令插入模块。
insmod myirq.ko irq=2 interface=myirq
中断服务程序一般都是在中断请求关闭的条件下执行的,以避免嵌套而使中断控制复杂化。但是,中断是一个随机事件,它随时会到来,如果关中断的时间太长,CPU就不能及时响应其他的中断请求,从而造成中断的丢失。因此,内核的目标就是尽可能快的处理完中断请求,尽其所能把更多的处理向后推迟。例如,假设一个数据块已经达到了网线,当中断控制器接受到这个中断请求信号时,Linux内核只是简单地标志数据到来了,然后让处理器恢复到它以前运行的状态,其余的处理稍后再进行(如把数据移入一个缓冲区,接受数据的进程就可以在缓冲区找到数据)。因此,内核把中断处理分为两部分:上半部(top half)和下半部(bottom half),上半部(就是中断服务程序)内核立即执行,而下半部(就是一些内核函数)留着稍后处理:
首先,一个快速的“上半部”来处理硬件发出的请求,它必须在一个新的中断产生之前终止。通常,除了在设备和一些内存缓冲区(如果你的设备用到了DMA,就不止这些)之间移动或传送数据,确定硬件是否处于健全的状态之外,这一部分做的工作很少。
下半部运行时是允许中断请求的,而上半部运行时是关中断的,这是二者之间的主要区别。
但是,内核到底什时候执行下半部,以何种方式组织下半部?这就是我们要讨论的下半部实现机制,这种机制在内核的演变过程中不断得到改进,在以前的内核中,这个机制叫做bottom half(简称bh),在2.4以后的版本中有了新的发展和改进,改进的目标使下半部可以在多处理机上并行执行,并有助于驱动程序的开发者进行驱动程序的开发。下面主要介绍常用的小任务(Tasklet)机制及2.6内核中的工作队列机制。除此之外,还简要介绍2.4以前内核中的下半部和任务队列机制。
1 小任务机制
这里的小任务是指对要推迟执行的函数进行组织的一种机制。其数据结构为tasklet_struct,每个结构代表一个独立的小任务,其定义如下:
struct tasklet_struct {
struct tasklet_struct *next; /*指向链表中的下一个结构*/
unsigned long state; /* 小任务的状态 */
atomic_t count; /* 引用计数器 */
void (*func) (unsigned long); /* 要调用的函数 */
unsigned long data; /* 传递给函数的参数 */
};
结构中的func域就是下半部中要推迟执行的函数 ,data是它唯一的参数。
State域的取值为TASKLET_STATE_SCHED或TASKLET_STATE_RUN。TASKLET_STATE_SCHED表示小任务已被调度,正准备投入运行,TASKLET_STATE_RUN表示小任务正在运行。TASKLET_STATE_RUN只有在多处理器系统上才使用,单处理器系统什么时候都清楚一个小任务是不是正在运行(它要么就是当前正在执行的代码,要么不是)。
Count域是小任务的引用计数器。如果它不为0,则小任务被禁止,不允许执行;只有当它为零,小任务才被激活,并且在被设置为挂起时,小任务才能够执行。
2 声明和使用小任务
大多数情况下,为了控制一个寻常的硬件设备,小任务机制是实现下半部的最佳选择。小任务可以动态创建,使用方便,执行起来也比较快。
我们既可以静态地创建小任务,也可以动态地创建它。选择那种方式取决于到底是想要对小任务进行直接引用还是一个间接引用。如果准备静态地创建一个小任务(也就是对它直接引用),使用下面两个宏中的一个:
DECLARE_TASKLET(name, func, data)
DECLARE_TASKLET_DISABLED(name, func, data)
这两个宏都能根据给定的名字静态地创建一个tasklet_struct结构。当该小任务被调度以后,给定的函数func会被执行,它的参数由data给出。这两个宏之间的区别在于引用计数器的初始值设置不同。第一个宏把创建的小任务的引用计数器设置为0,因此,该小任务处于激活状态。另一个把引用计数器设置为1,所以该小任务处于禁止状态。例如:
DECLARE_TASKLET(my_tasklet, my_tasklet_handler, dev);
这行代码其实等价于
struct tasklet_struct my_tasklet = { NULL, 0, ATOMIC_INIT(0),
tasklet_handler, dev};
这样就创建了一个名为my_tasklet的小任务,其处理程序为tasklet_handler,并且已被激活。当处理程序被调用的时候,dev就会被传递给它。
3 编写自己的小任务处理程序
小任务处理程序必须符合如下的函数类型:
void tasklet_handler(unsigned long data)
由于小任务不能睡眠,因此不能在小任务中使用信号量或者其它产生阻塞的函数。但是小任务运行时可以响应中断。
4 调度自己的小任务
通过调用tasklet_schedule()函数并传递给它相应的tasklt_struct指针,该小任务就会被调度以便适当的时候执行:
tasklet_schedule(&my_tasklet); /*把 my_tasklet 标记为挂起 */
在小任务被调度以后,只要有机会它就会尽可能早的运行。在它还没有得到运行机会之前,如果一个相同的小任务又被调度了,那么它仍然只会运行一次。
可以调用tasklet_disable()函数来禁止某个指定的小任务。如果该小任务当前正在执行,这个函数会等到它执行完毕再返回。调用tasklet_enable()函数可以激活一个小任务,如果希望把以DECLARE_TASKLET_DISABLED()创建的小任务激活,也得调用这个函数,如:
tasklet_disable(&my_tasklet); /* 小任务现在被禁止,这个小任务不能运行 */
tasklet_enable(&my_tasklet); /* 小任务现在被激活 */
也可以调用tasklet_kill()函数从挂起的队列中去掉一个小任务。该函数的参数是一个指向某个小任务的tasklet_struct的长指针。在小任务重新调度它自身的时候,从挂起的队列中移去已调度的小任务会很有用。这个函数首先等待该小任务执行完毕,然后再将它移去。
5 tasklet的简单用法
下面是tasklet的一个简单应用, 以模块的形成加载。
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/kdev_t.h>
#include <linux/cdev.h>
#include <linux/kernel.h>
#include <linux/interrupt.h>
static struct tasklet_struct my_tasklet;
static void tasklet_handler (unsigned long data)
{
printk(KERN_ALERT "tasklet_handler is running.\n");
}
static int __init test_init(void)
{
tasklet_init(&my_tasklet, tasklet_handler, 0);
tasklet_schedule(&my_tasklet);
return 0;
}
static void __exit test_exit(void)
{
tasklet_kill(&my_tasklet);
printk(KERN_ALERT "test_exit running.\n");
}
MODULE_LICENSE("GPL");module_init
(test_init);
module_exit(test_exit);
module_init
从这个例子可以看出,所谓的小任务机制是为下半部函数的执行提供了一种执行机制,也就是说,推迟处理的事情是由tasklet_handler实现,何时执行,经由小任务机制封装后交给内核去处理。