(二)Linux設備驅動的模塊化編程

本系列導航
(一)初識Linux驅動
(二)Linux設備驅動的模塊化編程
(三)寫一個完整的Linux驅動程序訪問硬件並寫應用程序進行測試
(四)Linux設備驅動之多個同類設備共用一套驅動
(五)Linux設備驅動模型介紹
(六)Linux驅動子系統-I2C子系統
(七)Linux驅動子系統-SPI子系統
(八)Linux驅動子系統-PWM子系統
(九)Linux驅動子系統-Light子系統
(十)Linux驅動子系統-背光子系統
(十一)Linux驅動-觸摸屏驅動

我們剛開始學習驅動,都是以模塊的形式來編寫驅動程序。

1. 什麼是模塊?

官方定義: 可在運行時添加到內核中的代碼被稱爲“模塊”。
釋義: Linux設備驅動只有在Linux內核(可百度搜索釋義)中才能工作,內核是驅動運行所依賴的環境(Linux內核中有驅動運行所需要的庫等),所以驅動編譯、運行有兩種方式:一個是直接將驅動代碼放入內核中,作爲內核的一部分進行編譯,然後Linux內核啓動的時候,驅動也即運行;第二個是將驅動單獨編譯成一個模塊,當Linux內核運行起來後,需要某個驅動的時候,再將對應的驅動模塊添加到當前的Linux內核中,當不需要某個驅動的時候,可以從內核中將對應的驅動模塊卸載掉。

2. 模塊化編程有什麼好處?

1)可以減小內核鏡像的體積,因爲模塊本身不被編譯到內核鏡像裏面。
2)可以在內核中添加或刪除功能(模塊化的形式)而不用重新編譯內核(每一次從新編譯內核很耗時):
非模塊化驅動編程過程: 編寫驅動->編譯內核(驅動放入內核代碼中一起編譯)->生成鏡像燒寫到硬件->如果驅動出現問題則從新回到第一步修改然後開始直到成功。
模塊化驅動編程過程: 編寫驅動->單獨將驅動編譯成一個模塊->將模塊下載到正在運行的硬件上並插入到內核中->如果有問題則回到步驟一從新開始,整個過程無需重新編譯和燒寫內核。

3. 寫驅動模塊和寫普通的Linux應用程序有什麼區別?

許多同學在剛開始寫Linux驅動程序的時候不知道該怎麼寫,上來就是int main() {},下面我就分析下我們要寫的驅動模塊和Linux應用程序的區別:

Linux模塊 Linux應用程序
(1)運行空間 內核空間 用戶空間
(2)入口函數 模塊加載函數 main函數
(3)庫 內核源碼庫(內核include目錄) 用戶空間的庫/usr/lib
(4)釋放 必須釋放(裝載和卸載) 要求釋放
(5)段錯誤危害 可能導致整個系統崩潰 危害小,不會影響系統

4. 如何寫驅動模塊?

模塊的三要素:
1)版本聲明

MODULE_LICENSE("GPL");

GPL:GNU通用公共許可證,如果不加版本聲明,編譯的時候會報錯,關於這個聲明的具體作用,可自行上網百度。
2)模塊的加載函數也即模塊的入口函數 – 相當於應用程序的main函數
模塊的加載函數有兩種寫法,第一種寫法,又叫缺省寫法:

int init_module(void) {}

當加載這個模塊的時候,會調用到這個模塊的init_module函數執行。
缺點:當大家都採用這種寫法時,內核中將會有太多的init_module, 使閱讀時難以區分。
第二種寫法,稱爲自定義寫法,顧名思義,就是入口函數名字可以自己定義,如 :

static int hello_init(void) {}

但是加載的時候,系統如何知道你這個自定義的函數就是入口函數呢?所以需要聲明這個自定義函數就是入庫函數,如下:

module_init(hello_init);

我們大多也都採用這種寫法。
3)模塊的卸載函數也即模塊的出口函數
這個函數通常要做的是釋放入口函數裏面申請的資源。同上,也有兩種寫法,第一種缺省寫法:

void cleanup_module(void) {}

第二種自定義函數寫法,也就是我們常用的寫法:

static void hello_exit(void) {}module_exit(hello_exit);

用sourceinsight或者vim編輯一個最簡單的模塊hello.c:

    #include <linux/kernel.h>
    #include <linux/module.h>
    
    static int hello_init(void)
    {
        printk("%s : %d\n", __func__, __LINE__);
    
        return 0;
    }
    
    static void hello_exit(void)
    {
        printk("%s : %d\n", __func__, __LINE__);
    }
    
    
    MODULE_LICENSE("GPL");
    module_init(hello_init);
    module_exit(hello_exit);

其中,包含的頭文件是內核源碼庫裏面的,這個在前面講過。在模塊的加載函數和卸載函數中我們加入了打印,通過打印我們能知道代碼的執行過程。但是,注意這裏用的打印是內核裏面的printk,而不是用戶空間的printf,printk時內核源碼庫提供的,這段代碼最終也是運行在內核空間的。
如何編譯這個模塊:
因爲有些同學想了解編譯模塊的Makefile具體是如何實現的,所以在這裏我就給大家詳解一下,如果不想了解,直接cp我的Makefile使用即可。
參考內核源碼文檔-> Documentation/kbuild/modules.txt,其中第二章,How to Build External Modules是我們要仔細閱讀的:

To build external modules, you must have a prebuilt kernel available that contains the configuration and header files used in the build. Also, the kernel must have been built with modules enabled. If you are using a distribution kernel, there will be a package for the kernel you are running provided by your distribution.

大概意思就是說如果你想編譯一個外部模塊,那麼你必須要有一個提前編譯過的並且有效的內核,這個內核裏面包括編譯模塊所需要的頭文件等,並且這個內核中的module屬性選項被enable(Make menuconfig可以配置的)。或者如果你用的是一個工作的內核,比如你在一個ubuntu系統(Linux內核+圖形庫等的組合)上面編譯模塊,那麼這個系統會提供一些package,我們剛開始學習,不涉及具體的硬件,就先在一個Ubuntu的內核中編譯運行我們的模塊。
Makefile 語法:

Command Syntax
The command to build an external module is:
       $ make -C <path_to_kernel_src> M=$PWD            
       //-C <path_to_kernel_src>因爲編譯需要內核庫,所以這個選擇指定內核源碼路徑,編譯的時候就知道去哪找那些依賴的庫。
       //-M 指定你當前要編譯的源碼所在的路徑
    
build against the running kernel use:
       $ make -C /lib/modules/`uname -r`/build M=$PWD  
       //這個就是我們說的,當我們要依靠一個正在運行的Ubuntu的內核進行編譯和運行,那麼這個kernel路徑該如何選擇呢?這就是ubuntu中內核庫的絕對路徑
    
Creating a Kbuild File for an External Module
       obj-m := <module_name>.o                        
       //obj-m指定你最終要生成的產物是module模塊,<module_name>.o表示你要生成<module_name>這個模塊需要依賴<module_name>.o, 中間的編譯環節有內核的kbuild系統來完成的。

所以最終的Makefile寫法如下(#爲註釋):

#定義一些變量,增加代碼的閱讀性和擴展性
#`uname -r`這種寫法就是執行uname -r這個shell命令,從而構造這個絕對路徑,因爲每個人的計算機的絕對路徑都不一樣,這樣提高通用性,對於我的主機,這個路徑相當於/lib/modules/4.4.0-31-generic/build
KERNEL_PATH := /lib/modules/`uname -r`/build
PWD := $(shell pwd)

#這個名字表示:要生產的模塊的名字,最終會生成hello.ko
MODULE_NAME := hello
   
#表示要生成hello.ko要依靠中間文件hello.o  kbuild系統會將源碼hello.c編譯成hello.o
obj-m := $(MODULE_NAME).o
   
#當執行make命令默認會尋找第一個目標,即all
all:
	$(MAKE) -C $(KERNEL_PATH) M=$(PWD)
   
#執行make clean要執行的操作,將編譯生成的中間文件刪掉
clean:
	rm -rf .*.cmd *.o *.mod.c *.order *.symvers  *.ko

保存爲Makefile文件,最後將Makefile文件和hello.c放入同一個文件夾,然後執行make進行編譯會生成最終的模塊hello.ko。

5. 如何驗證這個模塊? – 模塊相關的命令

插入或者加載一個模塊

sudo insmod hello.ko

在包含有hello.ko的目錄執行這個命令,就會將這個hello模塊插入到你當前運行的內核(ubuntu系統或者開發板)中,並且執行你的入口函數hello_init,這時有些同學會發現,執行完插入模塊的命令後,沒有執行printk打印,其實是執行了,只不過是打印到了緩存裏面,我們需要用下面的命令查看打印信息:
查看printk打印信息:
在這裏插入圖片描述 dmesg //查看系統從開機到當前時刻由printk輸出到緩存的所有log
sudo dmesg -c //查看顯示log信息,並將整個緩存清除掉
sudo dmesg -C //不顯示log信息,將整個緩存清除掉
看到log信息後,如何確認模塊是否真正插入成功?
查詢內核中插入的所有模塊:
lsmod //如何顯示的模塊太多,我們可以通過lsmod | grep hello 這個命令來查看是否有hello這個模塊。
在這裏插入圖片描述
當我不需要這個模塊時,如何從內核中將這個模塊卸載掉?
卸載模塊:
sudo rmmod hello //注意,模塊名字時hello,那麼執行這個卸載命令的時候,就會執行我們的卸載函數hello_exit,通過dmesg可以看到對應的log,通過lsmod可以發現沒有hello這個模塊了。
在這裏插入圖片描述
有同學看到打印信息後會有個疑問,爲什麼卸載模塊的時候,竟然有hello_init的log,這個地方我解釋下,前面說過,printk是把打印信息輸出到緩存中,也就是說,每次執行dmesg的時候,是將整個緩存的log都打印出來了,所以hello_init這個log是執行insmod加載模塊的時候打印的。
查看模塊信息:
modinfo hello.ko
在這裏插入圖片描述
最後再說一個細節,如果把再我電腦上編譯的hello.ko拿到你的Ubuntu下能運行嗎?這個是肯定不行的,通過modinfo命令,我們可以看到這個模塊所依賴的內核版本信息,一個模塊只能運行在編譯這個模塊對應的那個內核環境中。

6. 模塊傳遞參數

顧名思義,就是在系統啓動或者加載模塊的時候,爲參數指定相應的值,在驅動程序裏,參數的用法如同全局變量,這樣可以使模塊具有更大的靈活性以及擴展性。
例如下面的例子:
給xxx.ko對應的驅動程序裏面的path傳遞"/lib/module/firmware/xxx.bin"這個字符串

insmod xxx.ko   path="/lib/module/firmware/xxx.bin"

驅動程序需按照以下步驟實現:
(1)定義接收參數的變量,並初始化

static int intarg = 100

(2)module_param(參數名,參數類型,參數讀/寫權限)來聲明intarg這個參數可以用來接收外部傳入

module_param(intarg, int ,0600)

(3)可選:

MODULE_PARM_DESC (intarg, "A integer");

這樣聲明後,通過modinfo可查看相應的信息
(4)執行insmod命令時傳遞參數

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