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()。
          如果你的代碼配置爲模塊方式,那麼必須確保:源文件中使用的所有接口必須是已經輸出的符號,否則導致在裝載時鏈接錯誤

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章