LINUX kernel development之添加内核模块并加入选项菜单


一.LINUX Kernel Module

             LINUX Kernel是组件模式的,所谓组件模式是指:LINUX Kernel在运行时,允许“代码”动态的插入或者移出Kernel。
    所谓模块是指:相关的一些子程序,数据、入口点和出口点共同组合成的一个单一的二进制映像,也就是一个可装载的Kernel目标文件。
    模块的支持,使得系统可以拥有一个最小的内核映像,并且通过模块的方式支持一些可选的特征和驱动程序。
    模块可动态的插入Kernel和从Kernel中移除,提供了一种调试内核程序的简便方法。模块的加载方式分为两种:静态加载和动态加载。
    静态加载是将模块直接编译入内核,若模块需要修改和升级,我们就得重新编译整个内核,并且必须重新烧写内核,工作量加大。
    动态加载是需要时,加载入内核,不需要时,从内核卸载。不需对内核进行编译和烧写,就可方便的对模块进行修改和升级,大大减小了工作量,也省去了我们很多的麻烦.

二.实践

1>.Hello World:

模块的开发就像写一个应用程序,它有自己的入口点,出口点,生命周期.
   /*
   *hello.c Hello,World! As a Kernel Module
   */
   #include <linux/init.h>
   #include <linux/module.h>
   #include <linux/kernel.h>
  
   /*
   *hello_init the init function , called when the module is loaded.
   *Return zero if successfully loaded, nozero otherwise.
   */
   static int hello_init(void)
   {
         printk(KERN_ALERT" Hello,World!\n");
         return 0;
   }

   /*
   *hello_exit the exit function ,called when the module is removed.
   */
   static void hello_exit(void)
   {
         printk(KERN_ALERT"Good,Bye!\n");
   }
  
   module_init(hello_init);
   module_exit(hello_exit);

   MODULE_LICENSE("GPL");
   MODULE_AUTHOR("HuG");


   注释:
   module_init()和module_exit()都是宏。
   module_init()把hello_init()函数注册为这个模块的入口点。当模块被装载时,内核调用hello_init()函数。
   module_init()的任务是把它的唯一参数作为相应模块的初始化函数。
   初始化函数必须有如下的形式:int my_init(void)
   由于初始化函数不会被外部的代码直接调用,所以,不必export这个函数。所以将初始化函数标志为static会更加的合理。
   初始化函数的返回值:如果初始化成功,返回0;否则返回非零。
   此处的初始化函数仅仅是打印出一句话。实际开发的模块中,初始化函数一般完成的工作是:注册资源,为数据结构分配内存等等。
   同理,module_exit()是把hello_exit()函数注册为这个模块的出口点。当模块从内核中移除时,内核调用这个函数。
   退出函数在返回之前,必须清除模块所占的资源,确保硬件处于一致状态等等。
   退出函数必须有如下形式:void my_exit(void);同上,将函数标志为static会更合理。
注意:若采取静态加载的方式,将模块编译进内核,则内核启动时,调用static int my_init(void);但是退出函数static void my_exit(void);不会包含在内核的映像内,它也不会被调用。因为静态的加载方式,是将模块当作内核的一部分编译进内核中,所以代码永远不会从内核中删除。
   宏MODULE_LICENSE()用于指定这个文件的版权许可。
   MODULE_AUTHOR()用于指定本文件的作者。宏的值完全是为了提供说明信息。

2>.building Modules

在编译模块,让模块开始工作之前,我们必须确定模块的源码放置位置:

有两种方式:
   一、把模块的源码增加到内核源码的一个合适的地方,即可以把文件作为一个"patch”,最终也可以将代码合到官方的源码树内。
   二、在源码树之外维护和编译模块。

2.1>.在源码树内添加

           我们通常的选择是将模块放入源码树之内,作为LINUX的一部分。这样模块可以生存在内核的源码树内。
  如果,我们的模块是一个和USB相关的一个驱动,我们可将其放入drivers/usb/目录下。我们进入drivers/usb/gadget/目录下,我们发现gadget/目录下有很多驱动程序,都是些和USB相关的驱动。因此,我们将模块放在此目录下。
   新建目录drivers/usb/gadget/hhtest/,将模块的所有文件放于此目录下(test.c+Makefile+Kconfig)

A>.test.c见hello world程序;

B>.Makefile中需写入:

obj-$(CONFIG_USB_GADGET_TEST) += test.o

C>.Kconfig写入:

config USB_GADGET_TEST
tristate "Hello Driver added by hh"      //在menuconfig界面显示的配置选项名称
default n
help
   test for adding driver to menuconfig.


/×××××××××××××××××××××格式说明××××××××××××××××××××××××××××××

config USB_GADGET_TEST
        tristate "Gadget Netmeeting support"
        default n
        help
     If you say Y here,netmeeting driver will be compiles 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指令表示其后面是帮助文本。有助于用户和开发人员理解相应的程序和建立自己的内核。
还有一些其它的指令。
    

//Kconfig文件用于衔接整个Kernel源码树;

×××××××××××××××××××××××××××××××××××××××××××××××××××/


进入新建目录hhtest的 父目录gadget/,分别修改其下的Makefile文件:

D>.在Makefile中添加:obj-$(CONFIG_USB_GADGET_TEST) += hhtest/

//添加目的:使build系统能够沿着源码树往下找到hhtest/子目录

E>.其次要在一个已经存在的Kconfig文件中添加如下一行(一般在新建目录的父目录的Kconfig添加):

     source "drivers/usb/gadget/hhtest/Kconfig"
     source命令的作用是,让后面带的文件或者目录下的文件生效。相当于,让文件执行一下,让修改生效。省去了重启电脑的麻烦。
     由于我们的Kconfig是新建的,所以,它并没有生效。
     所以,我们在一个已经存在的Kconfig文件中,调用source命令 , 让我们新建的Kconfig文件生效。


==================================================================================================================

         源码树内添加方法{多文件}:    

         如果新加模块包含多个文件,则可以将Makefile(gadget/hhtest/下)写为:

obj-$(CONFIG_USB_GADGET_ONLINE) += test.o
    test-objs :=one.o two.o three.o four.o


    这样,test.ko由one.c,two.c,three.c,four.c四个文件编译链接而成。
    如果,想要为这些文件指定额外的gcc编译选项,在Makefile文件中添加类似如下的一行:

    EXTRA_CFLAGS += -ONLY_TEST

    如果我们将模块的所有文件放置在目录gadget/下,则将hhtest/目录下的Makefile中的内容,添加到目录gadget/目录下的Makefile中即可;


===================================================================================================================

2.2>.置于源码树之外
    如果将模块源码置于源码树之外,那么在自己的源码目录下创建Makefile文件,并且添加:

   obj-m := test.o


    这样会把netmeeting.c编译成netmeeting.ko。如果有多个源文件,那么在Makefile文件中添加:

     obj-m := test.o
     test-objs :=one.o two.o three.o four.o

     放置于源码树之外和放置于源码树之内,主要区别在于build过程
     放置于源码树之外,编译模块时,需要使用命令make去找到内核源码文件和基本的Makefile文件。如下所示:

     make -C /kernel/source/location SUBDIRS=$PWD modules

     /keinel/source/location是已经配置过的内核源码树位置。


3、安装模块(installing Modules)
     编译后的模块要放在目录/lib/modules/version/kernel/下。一般是在Makefile中添加:

     modules_install:
                     cp netmeeting.ko /lib/modules/version/kernel/
     .PHONY:
            modules_install

    此步操作,需要root权限。

4.生成模块依赖(Generating Modules Dependencies)
     LINUX模块实用工具能够理解模块间的依赖性。
     也就是说如果模块chen依赖于模块sbb,那么在装载模块chen时,模块sbb会自动的装载。
     也就是LINUX下常说的依赖性编译:文件A的编译依赖于B,B的编译依赖于文件C。
    

      在root权限下,运行如下命令来建立模块间的依赖信息。

      sudo  depmod 或 sudo depmod -n   //-n:show process

----------------------------------------------------------------------------------------------------------------------------------------

     该命令详细的使用方法和规则,可通过下面命令来查看帮助:

     depmod --help

----------------------------------------------------------------------------------------------------------------------------------------

     模块间的依赖信息存放在文件/lib/modules/version/modules.dep中.

----------------------------------------------------------------------------------------------------------------------------------------

5、装载模块(loading Modules)
     用命令insmod装载模块是最简单的一种方法。它请求内核装载指定的模块。
     命令insmod不会检查模块间的依赖关系,也不会执行是否有错误的检查。
     加载模块:

      insmod (模块名)


     卸载模块:

      rmmod (模块名)

      这两个工具很简单,实用,但是缺乏智能性。

    ××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××××
     而实用工具modprobe提供了依赖性关系的解决方案,智能的错误检查和报告等。装载模块时,是我们的首选。

     root权限下,执行命令:
     加载模块:


     modprobe (模块名) (模块参数)             //模块参数见下一节6.

     modprobe命令,不仅试图装载写在其后的模块,还试图装载它依赖的所有模块。因此,是首选。
     卸载模块:

     modprobe r (模块名)

     这里的modprobe可以移除多个模块,还可以移除它依赖的并且不在使用中的其它模块。


6、模块参数(Module Parameters)
     对于如何向模块传递参数,LINUX Kernel提供了一个简单的框架。其允许驱动程序声明参数,并且用户在系统启动或模块装载时为参数指定相应值,在驱动程序中,参数的用法如同全局变量。这些模块参数也能够在sysfs中显示出来(sysfs暂不清楚是什么)。结果,有许多办法来创建和管理模块参数。
     通过宏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 long integer, 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中相应文件的访问权限。
      访问权限用通常的八进制格式来表示或者通常的S_Ifoo定义。
      八进制格式的使用和操作系统下是一致的。
      0755:表示所有制是读写、执行的权限。所在组是读和执行的权限。其他用户是读和执行的权限。
      S_Ifoo下:例如:
      S_IRUGO|S_IWUSR(表示其它用户具有读权限,用户具有写权限)。用0表示完全关闭在sysfs中相对应的项。
    
      因为宏是不能声明变量的,所以,在使用宏之前,必须先声明变量。典型的用法如下:

      static unsigned int use_acm = 0;
      module_param(use_acm,uint,S_IRUGO);

      这些变量的声明是放在模块源文件的开头部分。即use_acm是全局变量。
      我们可以使用宏module_param_named()使模块源文件内部的变量名与外部的参数名有不同的名字。
可以理解为,给变量名加了个引用。

      module_param_named(name,variable,type,perm);
      name:外部可见的参数名
      variable:源文件内部的全局变量

      例如:

      static unsigned int max_test = 9;
      module_param_named(maximum,max_test,int,0);

      如果模块参数是一个字符串时,通常使用charp类型定义这个模块参数。内核复制用户提供的字符串到内存,并且相对应的变量指向这个字符串。例如:

      static char *name;
      module_param(name,charp,0);

      另一种方法是通过宏
      module_param_string()让内核把字符串直接复制到程序中的字符数组内。

      module_param_string(name,string,len,perm);
      name:外部的参数名
      string:内部的变量名
      len:以string命名的buffer大小(len可以小于buffer的大小,但是没有意义)
      perm:sysfs的访问权限(或者perm为零,表示完全关闭相对应的sysfs项)。

      以上的都是只能传递一个参数给模块。如果要给模块传递多个参数,可以通过宏

      module_param_array(name,type,nump,perm);
      name:既是外部模块的参数名又是程序内部的变量名。name数组必须静态分配。
      type:是数据类型。
      perm:sysfs的访问权限。
      nump:是一个指针。其值表示有多少个参数存放在数组name中。

      例如:

      static int finish[MAX_FISH];
      static int nr_fish;
      module_param_array(fish,int,&nr_fish,0444);

     
      我们可以通过宏module_param_array_named(name,array,type,nump,perm);
      参数的意义和宏module_param_named()是一样的。

      最后,用宏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");

     
      使用这些宏,需要包含头文件:<linux/moduleparam.h>


===================================================================================================================

7.输出符号(Exported Symbols)
      当装载模块的时候,模块是动态的链接入内核之中。
      然而,动态链接的二进制代码只能调用外部函数,所以外部函数必须明确的输出,才能被模块调用。
      在内核中,通过EXPORT_SYMBOL()和EXPORT_SYMBOL_GPL()来达到目的。
      输出的函数,可以被其它的模块调用。
      没有输出的函数,不能被其它模块调用。
      模块比核心内核映像代码具有更严格的链接和调用规则。因为所有核心源文件链接成一个单一的作为基础的映像,因此在内核中核心代码可以调用任何非静态的接口。
      当然,输出符号也必须是非静态属性。
      一套输出的内核符号称之为输出的内核接口,也称之为Kernel API。
      当函数声明时,用EXPORT_SYMBOL()把函数输出。
      例如:


      int usb_gadget_register_driver(struct usb_gadget_driver *driver)
      {
       .......
      }
      EXPORT_SYMBOL(usb_gadget_register_driver);

      这样,任何模块都可以调用函数usb_gadget_register_driver(),只要在源文件中包含了声明这个函数的头文件,或者extern这个函数的声明(这点同C语言)。
      若你希望你的接口让只遵守GPL的模块调用。那么通过MODULE_LICENSE()的使用,内核链接器能够保证做到这一点。
             
EXPORT_SYMBOL_GPL(usb_gadget_register_driver);只允许标有GPL许可证的模块访问函数usb_gadget_register_driver()。
          如果你的代码配置为模块方式,那么必须确保:源文件中使用的所有接口必须是已经输出的符号,否则导致在装载时链接错误

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值