ch02.构造和运行模块
模块的构造:
1 #include <linux/init.h> 2 #include <linux/module.h> 3 4 MODULE_LICENSE("GPL"); 5 6 static int hello_init(void) 7 { 8 printk("Hello,world\n"); 9 return 0; 10 } 11 12 static void hello_exit(void) 13 { 14 printk("Goodbye,cruel world\n"); 15 } 16 module_init(hello_init); 17 module_exit(hello_exit);
对于自己为模块创建的makefile文件:
obj-m :=hello.o表示有一个模块需要从目标文件hello.o中构造,而从该目标文件中构造的模块名称是hello.ko。
如果我们要构造的模块名称为module.ko,并由两个源文件生成(比如file1.c和file2.c),则正确的makefile可如下编写:
obj-m :=module.o
module-objs :=file1.o file2.o
构造模块的make命令应该是参照内核顶层目录的Makefiel文件指定的编译方法:
make -C $(KERNELDIR) M=$(PWD) modules
上述命令首先改变目录到-C选项指定的位置(即内核源码顶层目录),其中保存有内核的顶层makefile文件。M=选项让改makefile在构造modules目标之前返回到模块源代码目录。染回,modules目标指向obj-m变量中设定的模块。
完整的makefile:
1 ifeq ($(KERNELRELEASE),) //ifeq和括号之间需要有一个空格 2 //防止-C选项切回到内核后M=选项切回模 3 块所在目录重复执行相同语句 4 KERNELDIR = /lib/modules/$(shell uname -r)/build 5 PWD = $(shell pwd) 6 7 modules: 8 make -C $(KERNELDIR) M=$(PWD) modules 9 clean: 10 make -C $(KERNELDIR) M=$(PWD) clean 11 else 12 obj-m :=hello.o 13 endif
注:在一个典型的构造过程中,上述makefile将被读取两次。
当从命令行执行make时,KERNERRELEASE变量尚未设置,通过-C切回到内核顶层目录,调用modules目标,由M=选项指定模块所在目录。当第二次读取makefile文件时,KERNELRELEASE在内核sourcetree目录已经有了定义,所以执行else语句部分,调用obj-m :=hello.o生成对应的hello.ko文件。
装载和卸载模块
加载模块:insmod
卸载模块:rmmod
insmod和ld有些类似,将模块的代码和数据装入内核,然后使用内核的符号表解析模块中任何未解析的符号。insmod可以接受一些命令行选项,并且可以在模块链接到内核之前给模块中的整型和字符串型变量赋值。
注;insmod如何工作?实际上它依赖于定义在kernel/module.c中的一个系统调用。函数sys_init_module给模块分配内核内存(函数vmalloc负责分配内存)以便装载模块,然后,该系统调用将模块正文复制到内存区域,并通过内核符号表解析模块中的内核引用,最后调用模块的初始化函数。(内核源码中只有系统调用的名字前带有sys_前缀)
lsmod程序列出当前装载到内核中的所有模块,还提供了一些其他信息,比如其他模块是不是在使用某个特定模块等。lsmod通过读取/proc/modules虚拟文件来获得这些信息(也可以在sysfs虚拟文件系统中/sys/module下找到)
如果模块装载失败,可以查看系统日志文件(/var/log/messages或者系统配置使用的文件),将看到导致模块装载失败的具体原因。
内核符号表
当模块被装入内核后,它所导出的任何符号都会变成内核符号表的一部分。通常情况下,模块只需要实现自己的功能,而无需导出任何符号。但是,如果其他模块需要从某个模块中获得“好处”时,我们可以导出符号供其他模块使用。(模块层叠技术)
如果一个模块需要向其他模块导出符号,使用:
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name); 要导出的模块只能被GPL许可证下的模块使用。
同时,也需要在调用该符号的模块中使用export外部引用。
初始化和关闭
static int __init initialization_function(void)
{
/*初始化代码*/
}
module_init(initialization_function);
static:防止和内核中其他模块重名
__init:表明该函数仅在初始化期间使用,在模块装载完后,模块装载器将会把初始化函数扔掉,这样可将该函数占用的内存释放出来。
static void __exit cleanup_function(void)
{
/*清除代码*/
}
module_exit(cleanup_function);
清除函数没有返回值。
__exit:标记该代码仅用于模块卸载,如果不想只用于卸载函数,可以不加__exit。
初始化过程中的错误处理
当我们在内核中注册设施时,要时刻铭记注册可能会失败,即使是最简单的动作,都需要分配内存,而所需要的内存可能无法获得。因此模块代码必须始终检查返回值,并确保所请求的操作已真正成功。
如果模块的初始化出现错误之后,模块必须自行撤销已注册的设施。如果由于某种原因我们未能撤销已注册的设施,则内核会处于一种不稳定状态,这是因为内核中包含了一些指向并不存在的代码和内部指针。这种情况下,唯一有效的解决方法就是重新引导系统。因此,必须在初始化过程中出现错误时认真完成正确的工作。
goto语句进行错误处理:
不管初始化过程在什么时候失败,下面的例子(使用了虚构的注册和撤销注册函数)都能正确工作:
1 int __init my_init_function(void) 2 { 3 int err; 4 /*使用指针和名称注册*/ 5 err = register_this(ptr1,"skul1"); 6 if(err) goto fail_this; 7 err = register_that(ptr2,"skul2"); 8 if(err) goto fail_that; 9 err = register_those(ptr3,"skul3"); 10 if(err) goto fail_those; 11 return 0; /*成功(写在标号前)*/ 12 13 fail_those: unregister_that(ptr2,"skul2"); 14 fail_that: unregister_this(ptr1,"skul1"); 15 fail_this: return err; /*返回错误*/ 16 /*标号倒序*/ 17 }
这段代码准备注册三个(虚构的)设施。在出错的时候使用goto语句,它将只撤销出错时刻以前所成功注册的那些设施!(需要包含<linux/errno.h>)
列一种观点不支持goto的使用,而是记录任何成功注册的设施,然后在出错的时候调用模块的清除函数。清除函数将仅仅回滚以成功完成的步骤。然而这种替代方法需要更多的代码和cpu时间,因此在追求效率的代码中使用goto语句仍然是最好的错误恢复机制。
模块的清除函数需要撤销初始化函数所注册的所有设施,并且习惯上(但不是必须的)以相反于注册的顺序撤销设施:
1 void __exit my-cleanup_function(void) 2 { 3 unregister_those(ptr3,"skul3"); /*倒序注销*/ 4 unregister_that(ptr2,"skul2"); 5 unregister_this(ptr1,"skul1"); 6 return; 7 }
如果初始化和清除工作涉及到很多设施,则goto方法可能变得难以管理,因为所有用于清除设施的代码在初始化函数中重复,同时一些标号交织在一起。因此,有时候我们需要考虑重新构思代码结构。----每当发生错误时从初始化函数中调用清除函数,
这种方法将减少代码的重复并且使代码更清晰有条理。下面是这种方法的简单示例:
1 struct something *item1; 2 struct somethingelse *item2; 3 int stuff_ok; 4 5 void my_cleanup(void) /*此时卸载函数不能加__exit*/ 6 { 7 if(item1) 8 release_thing(item1); 9 if(item2) 10 release_thing2(item2); 11 if(stuff_ok) 12 unregister_stuff(); 13 return; 14 } 15 16 int __init my_init(void) 17 { 18 int err = -ENOMEM; /*err是一个负数*/ 19 20 item1 = allocate_thing(arguments); /*分配函数*/ 21 item2 = allocate_thing2(arguments2); 22 if(!item1 || !item2) 23 goto fail; 24 err = register_stuff(item1,item2); /*注册函数*/ 25 if(!err) 26 stuff_ok = 1; 27 else 28 goto fail; 29 return 0; /*成功*/ 30 31 fail: 32 my_cleanup(); 33 return err; 34 }
这种方式的初始化能够扩展到对大量设施的支持,因此比前面的技术更具优越性。需要注意的是,因为清除函数被非退出代码调用,因此不能将清除函数标记为__exit。
注:在注册完成之后,内核的某些部分可能会立即使用我们刚刚注册的任何设施。话句话说,在初始化函数还在运行的时候,内核就完全可能会调用我们的模块,因此,在首次注册完成之后,代码就应该准备好被内核的其他部分调用==》在用来支持某个设施的所有内部初始化完成之前,不要注册任何设施!
模块参数
模块的传参可在运行insmod或modprobe命令装载模块时传递。在insmod改变模块参数之前,模块必须让这些参数对insmod命令可见。参数必须使用module_param宏来声明(moduleparam.h)。
如:
static char *whom = "world";
static int howmany = 1;
module_param(howmany,int,S_IRUGO);
module_param(whom,char*,S_IRUGO);
参数1:变量的名称 变量2:类型 变量三:用于sysfs入口项的访问许可掩码 一般设置为0444