前言 Linux 是一个整体式的内核(Monolithic Kernel)结构,也就是说,整个内核是一个单独的、非常大的程序。
从实现机制来说,我们又把它划分为5个子系统(前一篇文章有介绍),内核的各个子系统都提供了内部接口(函数和变量),这些函数和变量可供内核所有子系统调用和使用。 
 
Linux 的整体式结构决定了要给内核增加新的成分也是非常困难,因此Linux 提供了一种全新的机制—可装入模块(Loadable Modules,以下简称模块):
用户可以根据自己的需要,在不需要对内核进行重新编译的条件下,模块能被动态地插入到内核或从内核中移走。 
 
模块的特点:
模块本身可以不被编译入内核映像,从而控制了内核的大小。 
模块一旦被加载,它就和内核中的其他部分完全一样。 
 
我们需要知道什么?
什么是模块?为什么要使用模块?
 
整体式内核or微内核 
linux 内核是一体化内核,就是说,在内核模式下,linux内核执行在一个单独的地址空间。
 
可以说linux是做出了各方面的权衡,典型的实用主义。实用主义又赢得了一些胜利。
 
什么是模块? 模块:
是内核的一部分(通常是设备驱动程序),但是并没有被编译到内核里面去。 
它们被分别编译并连接成一组目标文件,这些文件能被插入到正在运行的内核,或者从正在运行的内核中移走,进行这些操作可以使用insmod(插入模块)或rmmod(移走模块)命令,或者,在必要的时候,内核本身能请求内核守护进程(kerned)装入或卸下模块。 
 
这里列出在Linux 内核源程序中所包括的一些模块:
文件系统:minix,sysv,isofs,hpfs,smbfs,ext3,nfs,proc 等。 
所有的SCSI 高级驱动程序: disk, tape, cdrom, generic。 
大多数SCSI 驱动程序: aha1542, in2000等 
大多数以太网驱动程序:非常多,请看./Documentation/networking/net-modules.txt 
很多其他模块,例如:
binfmt_elf: elf 装入程序 
binfmt_java: java 装入程序 
serial: 串口(tty) 
 
 
 
为什么要使用模块? Linux内核的整体架构本就非常庞大,其包含的组件也非常多。如果把所有需要的功能都编译到Linux内核中。这会导致两个问题:
一是生成的内核会很大, 
二是如果我们要在现有的内核中新增或删除功能,将不得不重新编译内核。 
 
而使用模块机制可使得编译出的内核本身并不需要包含所有功能,而在这些功能需要被使用的时候,其对应的代码被动态地加载到内核。按需动态装入模块是非常吸引人的,因为这样可以保证内核达到最小并且使得内核非常灵活
挂载文件系统的例子 例如,当你可能偶尔使用 VFAT 文件系统,你只要安装(mount) VFAT,VFAT 文件系统就成为一个可装入模块,kerneld 通过自动装入VFAT 文件系统建立你的Linux 内核,当你卸下(unmount)VFAT 部分时,系统检测到你不再需要的FAT 系统模块,该模块自动地从内核中被移走。按需动态装入模块还意味着,你会有更多的内存给用户程序。
Linux 内核模块的优缺点 利用内核模块的动态装载性具有如下优点:
将内核映像的尺寸保持在最小,并具有最大的灵活性; 
便于检验新的内核代码,而不需重新编译内核并重新引导。 
 
内核模块的引入带来的缺点:
对系统性能和内存利用有负面影响; 
装入的内核模块和其他内核部分一样,具有相同的访问权限,因此,差的内核模块会导致系统崩溃 ; 
为了使内核模块访问所有内核资源,内核必须维护符号表,并在装入和卸载模块时修改这些符号表 有些模块要求利用其他模块的功能,因此,内核要维护模块之间的依赖性 ; 
内核必须能够在卸载模块时通知模块 ,并且要释放分配给模块的内存和中断等资源;内核版本和模块版本的不兼容,也可能导致系统崩溃,因此,严格的版本检查 是必需的。 
 
内核模块的Hello World 这个最简单的内核模块只包含内核模块加载函数、卸载函数和对GPL v2许可权限的声明以及一些描述信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include  <linux/init.h>  #include  <linux/module.h>  static  int  __init hello_init (void )    printk(KERN_INFO "Hello World enter\n" );  return  0 ; } module_init(hello_init);             static  void  __exit hello_exit (void )   printk(KERN_INFO "Hello World exit\n " ); } module_exit(hello_exit);             MODULE_AUTHOR("Barry Song <21cnbao@gmail.com>" );    MODULE_LICENSE("GPL v2" );                           MODULE_DESCRIPTION("A simple Hello World Module" );  MODULE_ALIAS("a simplest module" );                  
编译它会产生hello.ko目标文件,通过insmod ./hello.ko命令可以加载它,通过rmmod hello命令可以卸载它,加载时输出“Hello World enter”,卸载时输出“Hello World exit”。
说明: 
内核模块中用于输出的函数是内核空间的printk()而不是用户空间的printf()。printk()的用法和printf()基本相似,但前者可定义输出级别。printk()可作为一种最基本的内核调试手段, 
moudle_init 和 module_exit 这几行使用了特别的内核宏来指出模块加载函数和卸载函数的角色. 
 
内核是一个独特的环境, 它将它的要求强加于要和它接口的代码上.所以写内核代码需要遵循各种规范。
 
有几个文件对模块是特殊的, 必须出现在每一个可加载模块中. 因此, 几乎所有模块代码都有下面内容:
1 2 #include <linux/module.h> #include <linux/init.h> 
moudle.h 包含了大量加载模块需要的函数和符号的定义.init.h 来指定初始化和清理函数大部分模块还包含 moudleparam.h, 使得可以在模块加载时传递参数给模块. 
 
Linux内核模块程序结构 一个Linux内核模块主要由如下几个部分组成:
模块加载函数
当通过insmod或modprobe命令加载内核模块时,模块的加载函数会自动被内核执行,完成本模块的相关初始化工作。 
 
模块卸载函数
当通过rmmod命令卸载某模块时,模块的卸载函数会自动被内核执行,完成与模块卸载函数相反的功能。 
 
模块许可证声明
许可证(LICENSE)声明描述内核模块的许可权限,如果不声明LICENSE,模块被加载时,将收到内核被污染(Kernel Tainted)的警告。 
在Linux内核模块领域,可接受的LICENSE包括“GPL”、“GPL v2”、“GPL and additional rights”、“DualBSD/GPL”、“Dual MPL/GPL”和“Proprietary”(关于模块是否可以采用非GPL许可权,如“Proprietary”,这个在学术界和法律界都有争议)。 
大多数情况下,内核模块应遵循GPL兼容许可权。Linux内核模块最常见的是以MODULE_LICENSE(“GPL v2”)语句声明模块采用GPL v2。 
 
模块参数(可选)
模块参数是模块被加载的时候可以传递给它的值,它本身对应模块内部的全局变量。 
 
 
模块导出符号(可选)
内核模块可以导出的符号(symbol,对应于函数或变量),若导出,其他模块则可以使用本模块中的变量或函数。 
 
 
模块作者等信息声明(可选)
各种 MODULE_ 声明可以出现在你的源码文件的任何函数之外的地方. 但是惯例是把这些声明放在文件末尾。  
 
 
 
模块加载函数 Linux内核模块加载函数一般以__init标识声明,示例模板如下:
1 2 3 static  int  _ _init initialization_function (void ) module_init(initialization_function); 
模块加载函数以“module_init(函数名)”的形式被指定。它返回整型值,若初始化成功,应返回0。而在初始化失败时,应该返回错误编码。
总是返回相应的错误编码是种非常好的习惯,因为只有这样,用户程序才可以利用perror等方法把它们转换成有意义的错误信息字符串。
 
在Linux内核中,可以使用request_module(const char*fmt,…)函数加载内核模块,驱动开发人员可以通过调用下列代码灵活地加载其他内核模块:
1 request_module(module_name); 
模块卸载函数 Linux内核模块卸载函数一般以__exit标识声明,示例模板如下:
1 2 3 static  void  _ _exit cleanup_function (void ) module_exit(cleanup_function); 
模块卸载函数在模块卸载的时候执行,而不返回任何值,且必须以“module_exit(函数名)”的形式来指定。
用__exit来修饰模块卸载函数,可以告诉内核如果相关的模块被直接编译进内核(即built-in),则cleanup_function()函数会被省略,直接不链进最后的镜像。因为如果模块被内置进内核,就不可能卸载它了,卸载函数也就没有存在的必要了。
模块参数 可以用“module_param(参数名,参数类型,参数读/写权限)”为模块定义一个参数。例如下列代码定义了1个整型参数和1个字符指针参数:
1 2 3 4 static  char  *book_name = "dissecting Linux Device Driver" ;module_param(book_name, charp, S_IRUGO); static  int  book_num = 4000 ;module_param(book_num, int , S_IRUGO); 
在装载内核模块时,用户可以向模块传递参数,形式为insmode(或modprobe)模块名参数名=参数值,如果不传递,参数将使用模块内定义的缺省值。 
如果模块被内置,就无法insmod了,但是bootloader可以通过在bootargs里设置“模块名.参数名=值”的形式给该内置的模块传递参数 
 
导出符号 Linux的“/proc/kallsyms”文件对应着内核符号表,它记录了符号以及符号所在的内存地址。
1 2 EXPORT_SYMBOL(符号名); EXPORT_SYMBOL_GPL(符号名); 
导出的符号可以被其他模块使用,只需使用前声明一下即可。 
EXPORT_SYMBOL_GPL()只适用于包含GPL许可权的模块。 
 
下面给出了一个导出整数加、减运算函数符号的内核模块的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include  <linux/init.h>  #include  <linux/module.h>  int  add_integar (int  a, int  b) return  a + b;} EXPORT_SYMBOL_GPL(add_integar); int  sub_integar (int  a, int  b) return  a - b;} EXPORT_SYMBOL_GPL(sub_integar); MODULE_LICENSE("GPL v2" ); 
从“/proc/kallsyms”文件中找出add_integar、sub_integar的相关信息:
1 2 3 4 5 6 7 8 9 # grep integar /proc/kallsyms e679402c r __ksymtab_sub_integar [export_symb] e679403c r __kstrtab_sub_integar [export_symb] e6794038 r __kcrctab_sub_integar [export_symb] e6794024 r __ksymtab_add_integar [export_symb] e6794048 r __kstrtab_add_integar [export_symb] e6794034 r __kcrctab_add_integar [export_symb] e6793000 t add_integar [export_symb] e6793010 t sub_integar [export_symb] 
模块声明与描述 1 2 3 4 5 MODULE_AUTHOR(author);             MODULE_DESCRIPTION(description);   MODULE_VERSION(version_string);    MODULE_DEVICE_TABLE(table_info);   MODULE_ALIAS(alternate_name);      
对于USB、PCI等设备驱动,通常会创建一个MODULE_DEVICE_TABLE,以表明该驱动模块所支持的设备:
1 2 3 4 5 6 7 /* table of devices that work with this driver */ static struct usb_device_id skel_table [] = { 	{ USB_DEVICE(USB_SKEL_VENDOR_ID,USB_SKEL_PRODUCT_ID) }, 	{ } /* terminating enttry */ }; MODULE_DEVICE_TABLE (usb, skel_table); 
查询内核中已加载模块的信息 lsmod命令 lsmod命令格式 lsmod命令可以获得系统中已加载的所有模块以及模块间的依赖关系,例如:
1 2 3 4 5 6 7 8 $ lsmod Module         Size     Used    by hello           9  472       0  nls_iso8859_1  12  032       1  nls_cp437      13  696       1  vfat           18  816       1  fat            57  376       1     vfat ... 
Module :表示模块的名称。  Size:表示模块的大小。  Used:表示依赖模块的个数。  by:表示依赖模块的内容。 
 
lsmod命令原理 lsmod命令实际上是读取并分析/proc/modules文件,与上述lsmod命令结果对应的“/proc/modules”文件。如下:
1 2 3 4 5 $ cat /proc/modules hello 12393  0  - Live 0xe67a2000  (OF) nls_utf8 12493  1  - Live 0xe678e000  isofs 39596  1  - Live 0xe677f000  vboxsf 42561  2  - Live 0xe6767000  (OF)… 
/sys/module与tree -a 内核中已加载模块的信息也存在于/sys/module目录下:
加载hello.ko后,内核中将包含/sys/module/hello目录,该目录下又有一个refcnt文件和一个sections目录,在/sys/module/hello目录下运行tree –a可得到如下目录树: 
tree命令会以树状图的方式列出指定目录下的所有文件,包括目录里的文件,显示出指定目录的文件目录结构。 
1 2 3 computer:/sys/module /hello# tree -a  .├── coresize├── holders├── initsize├── initstate├── notes│ └── .note.gnu.build-id├── refcnt├── sections│ ├── .exit .text 3  directories, 15  files
modinfo命令 使用modinfo<模块名>命令可以获得模块的信息,包括模块作者、模块的说明、模块所支持的参数以及vermagic:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 # modinfo floppy filename:       /lib/modules/4.15 .0 -96 -generic/kernel/drivers/block/floppy.ko alias:          block-major-2 -* license:        GPL author:         Alain L. Knaff srcversion:     EECA1709167BA33BEFC75FD alias:          acpi*:PNP0700:* alias:          pnp:dPNP0700* depends:         retpoline:      Y intree:         Y name:           floppy vermagic:       4.15 .0 -96 -generic SMP mod_unload  signat:         PKCS#7  signer:          sig_key:         sig_hashalgo:   md4 parm:           floppy:charp parm:           FLOPPY_IRQ:int  parm:           FLOPPY_DMA:int  
加载和卸载模块 modprobe命令 modprobe命令:
可以加载模块和卸载模块 
比insmod命令要强大,它在加载某模块时,会同时加载该模块所依赖的其他模块。 
使用modprobe命令加载的模块若以“modprobe -r filename”的方式卸载,将同时卸载其依赖的模块。 
modprobe可载入指定的个别模块,或是载入一组相依的模块。modprobe会根据depmod所产生的相依关系,决定要载入哪些模块。若在载入过程中发生错误,在modprobe会卸载整组的模块。 
 
Linux modprobe命令-菜鸟教程 模块之间的依赖关系存放在根文件系统的/lib/modules/<kernel-version>/modules.dep文件中,实际上是在整体编译内核的时候由depmod工具生成的,它的格式非常简单:
1 2 3 4 5 6 7 kernel/lib/cpu-notifier-error-inject.ko: kernel/lib/notifier-error-inject.ko kernel/lib/pm-notifier-error-inject.ko: kernel/lib/notifier-error-inject.ko kernel/lib/lru_cache.ko: kernel/lib/cordic.ko: kernel/lib/rbtree_test.ko: kernel/lib/interval_tree_test.ko: updates/dkms/vboxvideo.ko: kernel/drivers/gpu/drm/drm.ko 
insmod命令 
insmod 实用程序必须找到请求装入的内核模块,请求装入的内核模块通常保存在/lib/modules/kernel-version/目录下。 
内核模块被连接成目标文件,与系统中其他程序不同的是,这种目标文件是可重定位的(它们是a.out 或ELF 格式的目标文件)。 
insmods 实用程序位于/sbin 目录下 
 
只有超级用户才能插入一个模块,其简单的命令如下:
1 insmod serial.o //serial.o 为串口的驱动程序。 
但是,这条命令执行以后可能会出现错误信息,诸如模块与内核版本不匹配、不认识的符号等。
查看/proc/ksyms,从中就可以发现内核移出的所有符号,插入模块时候出现不认识的符号,说明模块中的符号并未包含到内核中。这种情况通常说明模块有一些依赖的模块没有装载到内核。
 
怎么才能知道所依赖的模块呢?
除了从符号名判断外,更有效的方法是使用depmod 和 modprobe 命令来代替insmod 命令。传递参数给模块 通常情况下,当你插入模块时,还需要把参数传递给模块。
1 2 insmod ne.o io=0x400 irq=10 //这里装入的是NE2000 类的以太网适配器驱动程序,并告诉它以太网适配器的I/O 地址为0x400,其所产生的中断为IRQ 10。 
对于可装入模块,并没有标准的参数形式,也几乎没有什么约定。每个模块的编写者可以决定insmod 可以用什么样的参数。 
对于Linux 内核现已支持的模块,Linux HOWTO 文档给出了每种驱动程序的参数信息。 
 
rmmod命令 通过rmmod hello命令可以卸载hello模块
参考资料 《Linux设备驱动开发详解:基于最新的Linux4.0内核》