模块

来源:互联网 发布:陈洁仪左右手 知乎 编辑:程序博客网 时间:2024/05/02 19:33

   Linux是单块内核(monolithic)的操作系统,整个系统内核都运行与一个单独的包含域中。Linux内核是模块化组成的,它允许内核在运行时动态地向其中插入或从中删除代码。这些代码包括相关的子例程,数据,函数入口和函数出口,被一并组合在一个单独的二进制镜像中,即所谓的可装载内核模块中,或简称为模块。

   支持模块的好处就是基本内核镜像可以尽可能的小,因为可选的功能和驱动程序可以利用模块形式再提供。模块允许我们方便的删除和重新载入内核代码,也方便了调试工作。而且当热插拔新设备时,可通过命令载入新的驱动程序。

  • Hello world!
  1. /*
  2.  *hello.c--hello world 简单内核模块程序
  3.  */
  4. #include<linux/init.h>
  5. #include<linux/module.h>
  6. #include<linux/kernel.h>
  7. static int hello_init()
  8. {
  9.   printk(KERN_ALERT"I bear a charmed life./n");
  10.   return 0;
  11. }
  12. static void hello_exit(void)
  13. {
  14.   printk(KERN_ALERT"out,out,brief candle!/n");
  15. }
  16. module_init(hello_init);
  17. module_exit(hello_exit);
  18. MODULE_LICENSE("GPL");
  19. MODULE_AUTHOR("Shakespeare");

   与大多数内核子系统不同,模块开发更接近编写新的应用系统,因为至少要在模块文件中具有入口点和出口点。

   模块的初始化函数(入口点)必须符合下面的形式:

  1. int my_init(void);       //如果初始化顺利完成,返回0,否则返回非零

   因为init函数通常不会被外部函数直接调用,所以不必导出该函数,它可以被标记为static类型。

   模块的初始化函数通过module_init()例程注册到系统中,在模块装载时被调用。调用module_init()实际上是一个宏调用,它惟一的参数是模块的初始化函数。

   模块的出口函数偶module_exit()例程注册到系统。在模块从内存卸载时,内核便会调用模块的出口函数。该函数可能

在返回前负责清理资源,以保证硬件处于一致状态,或者其他别的操作。在退出函数返回后,模块就被卸载了。退出函数的形式如下:

 

  1. void my_exit(void);

  如果上述文件被静态地编译到内核映像中,那么退出函数将不被包含,而且用于都不会被调用(如果不是编译成模块的话,代码就永远不需要从内核中卸载)。

  MODULE_LICENSE()宏用于指定模块的版权。如果载入非GPL模块到系统内存,则会在内核中设置被污染标志,这个标志起到记录信息的作用。不过如果开发者提交的bug报告中包含有被污染的标志,那么报告的信用会被减低。非GPL模块不能调用GPL-only符号。

    MODULE_AUTHOR()宏指定了代码作者,它完全是用于信息记录。

  • 构建模块

   在2.6内核中,采用了新的“kbuild”构建系统,构建的第一步是决定在哪里管理模块源码。可以把模块源码加入到内核源代码树中,或者作为一个补丁,或者最终把代码把代码最终合并到正式的内核代码树中;另一种可行的方式是在内核源代码树之外维护和构建模块源码。

 

1. 放在内核源码树中

    最理想的情况是你的模块正式称为Linux内核的一部分,这样就会被存放在内核源代码树中。

   设备驱动程序存放在内核源码树根目录deriver/的子目录下,在其内部设备驱动文件被进一步按照类别、类型和特殊驱动程序得呢个更有序的组织起来。字符设备存在与drivers/char/目录下,块设备存放在drivers/block/目录下,USB设备则存放在drivers/usb/目录下。

   假设我们的驱动程序名为Foo XL,如果在目录drivers/char/下建立了自己的代码子目录foo,要做的是向drivers/char/目录中的Makefile文件添加代码,不同的代码产生不同的编译效果:

  1. obj-m += foo/       //这行编译指令告诉模块构建系统在编译模块时需要进入foo/目录中
  2. obj-$(CONFIG_FOO) += foo/     //如果驱动程序的编译取决于一个特殊的配置选项,比如CONFIG_FISHING_POLE,这时要用这条编译指令
  3. /*驱动程序智能化,可以自动检测其他文件,如foo-xxx.c,这样foo-main.c和foo-xxx.c就一起被编译和连接到foo.ko模块内*/
  4. obj-$(CONFIG_FOO) += foo.o
  5. foo-objs :=foo-main.o foo-xxx.o
  6. //注意,如果需要在构建文件时需要额外的编译标记,添加如下指令
  7. EXTRA_CFLAGS += -DTITANIUM_POLE

 

     如果把源文件置于drivers/char/目录下,不建立新目录,就将原来处于drivers/char/foo/下你写的makefile中的代码都加入到drivers/char/下的Makefile中。

    编译,运行内核构建过程。如果模块编译取决于配置选项,比如有CONFIG_FOO约束,那么在编译前首先要确保选项被允许。

 

2. 放在内核代码外

   如要要脱离内核源代码以此来维护和构建自己的模块,要做的就是在自己的源代码目录中建立一个Makefile文件,它只需要一行指令:

  1. obj-m := foo.o

就可以把foo.c编译成foo.ko。如果有多个源文件,那么就用下列两行 :

 

  1. obj-m := foo.o
  2. foo-objs := foo-main.o foo-xxx.o

这样foo-main.c和foo-xxx.c就一起被编译和连接到foo.ko模块内。

   模块在内核内或内核外构建的最大区别在于构建过程,当模块在内核源码外围时,要告诉make如何找到内核源码文件和基础Makefile文件:

 

  1. /*  /kernel source location是已配置的内核源代码树。
  2.  *不要把要处理的内核源代码树放在/usr/src/linux下,而要移到你的home目录下的某个方便访问的地方
  3. */
  4. make -c /kernel source location  SUBDIRS-$PWD modules   
  •    安装模块

   编译后的模块将被装入到目录/lib/modules/version/kernel/下。比如,使用2.6.10版本内核,模块的源代码直接放在drivers/char/下,编译后的驱动程序的存放路径是:

/lib/modules/2.6.10/kernel/drivers/char/foo.ko

  1. //下面的构建命令用来安装编译的模块到合适的目录下,要以root权限运行
  2. make modules_install
  • 产生模块依赖性

  Linux模块之间存在依赖性。A模块依赖于B模块,那么当载入A模块时,B模块也会被自动载入。这里需要的依赖信息必须事先生成。若想产生内核依赖关系的信息,root用户可以运行命令depmod

  为了执行更快的更新操作,可以只为新模块生成依赖信息,而不是生成所有的依赖关系,这时root用户可以运行命令:

depmod -A

模块依赖关系信息存放在/lib/modules/version/modules.dep文件中。

  • 载入模块

  载入模块最简单的方法是通过insmod命令。这是个功能很有限的命令,它的作用是请求内核载入指定的模块。insmod程序不执行任何依赖性分析或进一步的错误检查。以root身份运行命令:

insmod  foo

  卸载一个模块:

rmmod foo

   系统提供了更先进的工具modprobe,它提供了模块依赖性分析,错误智能检查,错误报告以及其他功能和选项:

modprobe modulename [module parameter]

  参数将在模块加载时传入内核。

   modprobe命令不但会加载指定的模块,还会自动加载任何它所依赖的有关模块。它还可以用来从内核中卸载模块,以root身份运行:

modprobe -r modulename

   与rmmod命令不同,modprobe也会卸载给定模块所依赖的相关模块。其前提是这些相关模块没有被使用。

  • 管理配置选项

  前面设置了CONFIG_FOO配置选项,foo模块将被自动编译,若要添加一个新的配置选项还可以怎么做呢?

  向Kconfig文件中添加一项,用以对应内核源码树。

  对驱动程序而言,Kconfig一般与源代码处于同一目录,比如,foo程序在目录drivers/char/下,那么就存在drivers/char/Kconfig。如果新建了子目录存放foo,而且系统Kconfit文件存在于该目录,那么必须在一个已经存在的Kconfig文件(即drivers/char/Kconfig)中将它引入:

source "drivers/char/foo/Kconfig"

我们以Foo XL模块添加选项为例,在Kconfig文件中加入一个配置选项如下:

  1. config FOO
  2.        tristate "Foo XL support"
  3.        default n
  4. help
  5.        If you say Y here, support for the Foo XL with computer interface will
  6.        be complied into the kernel and accessible via device node. You can also 
  7.        say N here and the driver will be built as a module named foo.ko.
  8.        If unsure, say N. 

配置选项第1行定义了该选项所代表的配置目标。注意CONFIG_前缀不需要写上。

第2行声明选项类型为tristate,也就是说可以被编译进内核(Y),也可作为模块编译(M),或者干脆不编译它(N)。如果编译选项代表的是一个系统功能,而不是一个模块,那么编译选项将用bool指令代替tristate,这说明它不允许被编译成模块。处于指令之后的引号内的文字为该选项指定的名称。

第3行指定了该选项的默认选择,这里默认为不编译它。

第4行help指令的目的是为该选项提供帮助文档。

   除了上述选项,还有其他选项。

depends指令规定了在该选项被设置其,首先要设置的选项。假如依赖性不满足,那么该选项被禁止。比如:

  1. depends on FOO_DEPEND

加入到配置选项中,那么就意味着CONFIG_FOO_DEPEND被选择前,Foo XL模块是不能被使用的。

select指令与depends指令类似,不同之处在于,select指定了谁,它就会强行将被指定的选项打开。不能滥用该指令,因为它会自动的激活其他配置选项。比如:

  1. select BIT

 

意味着当CONFIG_FOO被激活时,配置选项CONFIG_BIT必然一起被激活。

   如果select和depends同时指定多个选项,需要通过&&指令来进行多选。使用depends还可以利用叹号!前缀来指明禁止某个选项。比如:

  1. depends on DUMB_DREVERS && !NO_FOO_ALLOWED
意味着指定驱动安装程序要求打开DUMB_DREVERS 选项,同时禁止NO_FOO_ALLOWED选项。

bool选项和tristate往往会结合if指令一起使用,表示某个选项取决于另一个配置选项。如果条件不满足,配置选项就会被禁止,甚至不会显示在配置工具中。比如:

  1. bool “Deep sea mode”if OCEAN

意味着"Deep sea mode"只有在CONFIG_OCEAN选项打开时才可见且有效。

If指令也可以与defualt指令结合使用,强制只有在条件满足时default选项才有效。

配置系统导出了一些元选项(meta-option)以简化生成的配置文件。

CONFIG_EMBEDDED是用于关闭那些用户想要禁止的关键功能。

CONFIG_BROKEN_ON_SMP用来表示驱动程序并非多处理器安全的。

CONFIG_EXPERIMENTAL用于说明某些功能尚在试验或处于beta版本阶段。

  • 模块参数

  Linux语序驱动程序声明参数,从而用户可以在系统启动或者模块装载时再指定参数值,这些参数对驱动程序来说属于全局变量。模块参数同时也将出现在sysfs文件系统中,这样一来,无论是生成模块参数,还是管理模块参数的方式都变得灵活多样了。

   定义一个模块参数可以通过宏module_param()完成:

  1. 在include/linux/moduleparam.h中
  2. /*第一个参数是用户可见参数名,也是模块中存放模块参数的变量名。
  3.  *第二个参数存放了参数的类型
  4.  *第三个参数制定了模块在sysfs文件系统下对应文件的权限,该值可以是八进制格式或是S_Ifoo的定义形式,比如S_IRUGO|S_IWUSR,如果是0则表示禁止所有的sysfs
  5. */
  6. #define module_param(name, type, perm)              /
  7.     module_param_named(name, name, type, perm)
  8. /* Helper functions: type is byte, short, ushort, int, uint, long,
  9.    ulong, charp, bool or invbool, or XXX if you define param_get_XXX,
  10.    param_set_XXX and param_check_XXX. */ name是外部可见参数名,value是参数对应的内部全局变量名称
  11. #define module_param_named(name, value, type, perm)            /
  12.     param_check_##type(name, &(value));                /
  13.     module_param_call(name, param_set_##type, param_get_##type, &value, perm); /
  14.     __MODULE_PARM_TYPE(name, #type)

   宏module_param()并没有定义变量,必须在使用该宏前进行变量定义。例如:

  1. static int allow_live_bait = 1;
  2. module_param(allow_live_bait,bool,0644);

  通常需要用一个charp类型来定义模块参数(一个字符串),内核将用户提供的这个字符串拷贝到内存,而且将你的变量指向该字符串。比如:

 

  1. static char *name;
  2. module_param(name,charp,0);

  有可能模块的外部参数名称与对应的内部参数名称,这时使用宏module_param_named()。比如:

  1. static unsigned int max_test = DEFAULT_MAX_LINE_TEST;
  2. module_para_named(maximum_line_test,max_test,int,0);

其他相关的宏:

  1. /* Actually copy string: maxlen param is usually sizeof(string). */
  2. #define module_param_string(name, string, len, perm)            / 
  3.     static struct kparam_string __param_string_##name       /
  4.         = { len, string };                  /
  5.     module_param_call(name, param_set_copystring, param_get_string, /
  6.            &__param_string_##name, perm);           /
  7.     __MODULE_PARM_TYPE(name, "string")
  8. /* Comma-separated array: *nump is set to number they actually specified. */
  9. #define module_param_array_named(name, array, type, nump, perm)     / 
  10.     static struct kparam_array __param_arr_##name           /
  11.     = { ARRAY_SIZE(array), nump, param_set_##type, param_get_##type,/
  12.         sizeof(array[0]), array };                  /
  13.     module_param_call(name, param_array_set, param_array_get,   /
  14.               &__param_arr_##name, perm);           /
  15.     __MODULE_PARM_TYPE(name, "array of " #type) 
  16. #define module_param_array(name, type, nump, perm)      / 
  17.     module_param_array_named(name, name, type, nump, perm)
  • 导出符号表

  模块被载入后,就会动态连接到内核。注意,它与用户空间中的动态连接库类型,只有当被显示导出后的外部函数,才可以被动态库调用。在内核中,导出内核函数需要使用特殊的指令:EXPORT_SYMBOL()和EXPORT_SYMBOL_GPL()。

  导出的内核函数可以被模块调用,而为导出的函数模块则无法被调用。模块代码的链接和调用规则相比核心内核镜像中的代码而言,更加严格。核心代码在内核中可以调用任意非静态接口,因为所有的核心源码文件都被链接成同一个镜像。当然,被导出的符号表所包含的函数必须是非静态的。

  导出的内核符号表被看做是导出的内核接口,甚至称为API。

  导出符号相对简单,在声明函数后,紧接着EXPORT_SYMBOL()指令就可以了。该函数如果定义在一个可访问的头文件中,那么任何模块现在都可以访问它。

  如果系统自己的接口仅仅对GPL兼容的模块科技,那么内核连接器使用MODULE_LICENCE()宏满足这个要求,如果系统函数仅仅对标记为GPL协议的模块可见,那么就要用EXPORT_SYMBOL_GPL()指令。

  如果你的代码被配置为模块,那么就必须确保当它被编译为模块时锁用的全部接口都已经被导出,否则会产生链接错误,模块不能成功编译。