linux驅動程序——將驅動程序編譯進內核
模塊的加載
通常來說,在驅動模塊的開發階段,一般是將模塊編譯成.ko文件,再使用
sudo insmod module.ko
或者
depmod -a
modprobe module
將模塊加載到內核,相對而言,modprobe要比insmod更加智能,它會檢查並自動處理模塊的依賴,而insmod出現依賴問題時僅僅是告訴你安裝失敗,自己想辦法吧。
將模塊編譯進內核
這一章節我們並不關注模塊的運行時加載,我們要討論的是將模塊編譯進內核。
在學習內核的Makefile規則的時候就可以知道,將驅動程序編譯成模塊時,只需要使用:
obj-m += module.o
指定相應的源代碼(源代碼為module.c)即可,所以很多朋友就簡單地得出結論:如果要將模塊編譯進內核,只要執行下面的的指令就可以了:
obj-y += module.o
事實上,這樣是行不通的,要明白怎么將驅動程序編譯進內核,我們還是得先了解linux源碼的編譯規則。
關於linux源碼的編譯規則和部分細節可以查看我的另一篇博客linux內核Makefile概覽
本篇博客的所有實驗基於arm平台,beagle bone開發板,內核版本為4.14.79
編譯平台
注:在以下的討論中,目標主機和本機指加載運行驅動程序的機器,是開發的對象。而開發機指只負責編譯的機器,一般指PC機。
本機編譯
在對驅動程序進行編譯時,一般會有兩種不同的做法:
- 直接在目標主機上編譯
- 在其他平台上構建交叉編譯環境,一般是在PC機上編譯出可在目標板上運行的驅動程序
直接在目標主機上編譯是比較方便的做法,本機編譯本機運行。
通常,本機系統中一般不會自帶linux內核源碼的頭文件,我們需要做的就是在系統中安裝頭文件:
sudo apt-get install linux-headers-$(uname -r)
$(uname -r)獲取當前主機運行的linux版本號。
有了頭文件,那么源代碼從哪里來呢?答案是並不需要源代碼,或者說是並不需要C文件形式的源代碼,而是直接引用當前運行的鏡像,在編譯時,將/boot/vmlinuz-$(version)鏡像當成庫文件進行鏈接即可。
值得注意的是,/boot/vmlinuz-$(version)是linux啟動時讀取的鏡像,但是在本機中進行驅動程序編譯的時候並不會影響到這個鏡像,換句話說,即使是指定了obj-y,驅動程序也不會編譯到/boot/vmlinuz-$(version)鏡像中,自然達不到將驅動編譯進內核的效果。
交叉編譯
本機(目標機)編譯是比較方便的,但是無法改變生成的鏡像,當然也可以將源碼下載到本機(目標機)中進行編譯,就可以生成相應的linux鏡像。
但是一般情況下,在嵌入式開發中,不論是網絡、內存還是執行速率,目標主機的性能一般不會太高,如果需要編譯完整的源碼時,用戶會更傾向於在PC端構建編譯環境以獲取更好的編譯性能。
選擇在開發機上編譯而不是本機編譯時,需要注意的一點就是:通常嵌入式開發都是基於arm、mips等嵌入式架構,而PC常用X86架構,在編譯時就不能使用開發機上的gcc編譯器,因為開發機上編譯器是針對開發平台(X86),而非運行平台(arm、mips),所以需要使用交叉編譯工具鏈,同時在編譯時指定運行的主機平台。
指令是這樣的:
make arch=arm CROSS_COMPILE=$COMPILE_PATH/$COMPILE_TOOL
也可以在makefile中給相應的arch和CROSS_COMPILE變量賦值,直接執行make指令即可。
顯然,這種交叉編譯方式是對linux內核源碼的完整編譯,主要生成這一些目標文件:
- 生成linux的可啟動鏡像,通常是zImage或者vmlinuz,這是一個可boot執行的壓縮文件
- 伴隨着的還有鏡像對應的map文件,這個文件對應鏡像中的編譯符號以及符號的地址信息
- 未編譯進內核的模塊,也就是在配置時被選為M的選項
- linux內核頭文件等等
在上文中有提到,目標主機中,linux的啟動鏡像放置在/boot目錄下,所以如果我們需要替換linux的鏡像,需要替換/boot目錄下的以下兩個文件:
- linux的可啟動鏡像,也就是生成的zImage或者vmlinuz
- .map文件
在主機中,模塊一般被放置在/lib/modules目錄中,如果交叉編譯出的版本與本機中模塊版本不一致,將無法識別,所以編譯出的模塊也需要替換。
驅動程序編譯進內核
根據上文,可以得出的結論是:在試圖將驅動程序編譯進內核時,我們需要編譯完整的linux內核源碼以生成相應的鏡像文件,然后將其替換到目標主機的/boot目錄下即可。
那么,怎樣將驅動的源碼C文件編譯進內核呢?
這個問題得從makefile的執行流程說起:
make的執行
首先,如果你有基本的linux內核編譯經驗,就知道在編譯linux源碼前,需要進行config(配置),以決定哪些部分編譯進內核、哪些部分編譯成模塊,
通常使用make menuconfig,不同的config方式通常只是選擇界面的不同,其中稍微特殊的make oldconfig則是沿用之前的配置。
在配置完成之后將生成一個.config文件,makefile根據.config文件選擇性地進入子目錄中執行編譯工作。
流程基本如上所說,但是我們需要知道更多的細節:
- make menuconfig執行的原理是什么?
- 頂層makefile是怎樣執行子目錄中的編譯工作的?
答案是這樣的:
- make menuconfig肯定是讀取某個配置文件來陳列出所有的配置選項,遞歸地進入到子目錄中,發現幾乎每個子目錄中都有一個名為Kconfig的文件,這個文件負責配置驅動在配置菜單中的顯示以及配置行為,且Kconfig遵循某種語法,make menuconfig就是讀取這些文件來顯示配置項。
- 遞歸地進入到每個子目錄中,發現其中都有一個makefile中,可以想到,makefile遞歸地進入子目錄,然后通過調用子目錄中的makefile來執行各級子目錄的編譯,最后統一鏈接。
整個linux內核的編譯都是采用一種分布式的思想,需要添加一個驅動到模塊中,我們需要做的事情就是:
-
將驅動源文件放在內核對應目錄中,一般的驅動文件放在drivers目錄下,字符設備放在drivers/char中,塊設備就放在drivers/blok中,文件的位置遵循這個規律,如果是單個的字符設備源文件,就直接放在drivers/char目錄下,如果內容較多足以構成一個模塊,則新建一個文件夾。
-
如果是新建文件夾,因為是分布式編譯,需要在文件夾下添加一個Makefile文件和Kconfig文件並修改成指定格式,如果是單個文件直接添加,則直接修改當前目錄下的Makefile和Kconfig文件將其添加進去即可。
-
如果是新建文件夾,需要修改上級目錄的Makefile和Kconfig,以將文件夾添加到整個源碼編譯樹中。
-
執行make menuconfig,執行make
-
將生成的新鏡像以及相應boot文件拷貝到目標主機中,測試。
beagle bone的啟動文件包括:vmlinuz、System.map,編譯出的模塊文件為modules
Kconfig的語法簡述
上文中提到,在添加源碼時,一般會需要一個Kconfig文件,這樣就可以在make menuconfig時對其進行配置選擇,在對一個源文件進行描述時,遵循相應的語法。
在這里介紹一些常用的語法選項:
source
source:相當於C語言中的include,表示包含並引用其他Kconfig文件
config
新建一個條目,用法:
source drivers/xxx/Kconfig
config TEST
bool "item name"
depends on NET
select NET
help
just for test
config是最常用的關鍵詞了,它負責新建一個條目,對應linux中的編譯模塊,條目前帶有選項。
config TEST:
config后面跟的標識會被當成名稱寫入到.config文件中,比如:當此項被選擇為[y],即編譯進內核時,最后會在.config文件中添加這樣一個條目:
CONFIG_TEST=y
CONFIG_TEST變量被傳遞給makefile進行編譯工作。
bool "item name":
其中bool表示選項支持的種類,bool表示兩種,編譯進內核或者是忽略,還有另一種選項就是tristate,它更常用,表示支持三種配置選項:編譯進內核、編譯成可加載模塊、忽略。而item name就是顯示在menu中的名稱。
depends on:
表示當前模塊需要依賴另一個選項,如果另一個選項沒有沒選擇編譯,當前條目選項不會出現在窗口中
select:
同樣是依賴相關的選項,表示當前選項需要另外其他選項的支持,如果選擇了當前選項,那么需要支持的那些選項就會被強制選擇編譯。
help:
允許添加一些提示信息
menu/menuend
用法:
menu "Test option"
...
endmenu
這一對關鍵詞創建一個選項目錄,該選項目錄不能被配置,選項目錄中可以包含多個選項
menuconfig
相當於menu+config,此選項創建一個選項目錄,而且當前選項目錄可配置。
編譯示例
梳理了整個添加的流程,接下來就以一個具體的示例來進行詳細的說明。
背景如下:
- 目標開發板為開源平台beagle bone,基於4.14.79內核版本,arm平台
- 需要添加的源代碼為字符設備驅動,名為cdev_test.c,新建一個目錄cdev_test
- 字符設備驅動實現的功能:在/dev目錄下生成一個basic_demo文件,用來檢測是否成功將源代碼編譯進內核
將驅動編譯進內核
放置目錄
鑒於是字符設備,所以將源文件目錄放置在$KERNEL_ROOT/drivers/char/下面。
如果是塊設備,就會被放置在block下面,但是這並不是絕對的,類似USB為字符設備,但是獨立了一個文件出來。
放置后目標文件位置為:$KERNEL_ROOT/drivers/char/cdev_test
Kconfig文件
在$KERNEL_ROOT/drivers/char/cdev_test目錄下創建一個Kconfig文件,並修改文件如下:
menu "cdev test dir"
config CDEV_TEST
bool "cdev test support"
default n
help
just for test ,hehe
endmenu
根據上文中對Kconfig文件的語法描述,可以看出,這個Kconfig文件的作用就是:
- 在menuconfig的菜單中,在Device Driver(對應drivers目錄) ---> Character devices(對應char目錄)菜單下創建一個名為"cdev test dir"的菜單選項,
執行效果是這樣的
- 在"cdev test dir"的菜單選項下創建一個"cdev test support"的條目,這個條目的選項只有兩個,[*]表示編譯進內核和[]表示不進行編譯,默認選擇n,不進行編譯。
執行效果是這樣的
- 選擇help可以查看相應的提示信息。
在上文中還提到,Kconfig分布式地存在於子目錄下,同時需要注意的是,在編譯時,配置工具並非無差別地進入每個子目錄,收集所有的Kconfig信息,而是遵循一定的規則遞歸進入。
那么,既然是新建的目錄,怎么讓編譯器知道要進入到這個子目錄下呢?答案是,在上級目錄的Kconfig中包含當前路徑下的Kconfig文件。
打開char目錄下的Kconfig文件,並且在文件的靠后位置添加:
source "drivers/char/xillybus/Kconfig"
就把新的Kconfig文件包含到系統中檢索目錄中了,那么drivers/char/又是怎么被檢索到的呢?
就是在drivers的Kconfig中添加drivers/char/目錄下的Kconfig索引,以此類推。
Makefile文件
在$KERNEL_ROOT/drivers/char/cdev_test目錄下創建一個Makefile文件,並且編譯Makefile文件如下:
obj-$(CONFIG_CDEV_TEST) += cdev_test.o
表示當前子目錄下Makefile的作用就是將cdev_test.c源文件編譯成cdev_test.o目標文件。
值得注意的是,這里的編譯選項中使用的是:
obj-$(CONFIG_CDEV_TEST)
而非
obj-y
如果確定要將驅動程序編譯進內核永遠不變,那么可以直接寫死,使用obj-y,如果需要進行靈活的定制,還是需要選擇第一種做法。
CONFIG_CDEV_TEST是怎么被配置的呢?在上文提到的Kconfig文件編寫時,有這么一行:
config CDEV_TEST
...
在Kconfig被添加到配置菜單中,且被選中編譯進內核時,就會在$KERNEL_ROOT/.config文件中添加一個變量:
CONFIG_CDEV_TEST=y
自動添加CONFIG_前綴,而名稱CDEV_TEST則是由Kconfig指定,看到這里,我想你應該明白了這是怎么回事了。
是不是這樣就已經將當前子目錄添加到內核編譯樹中了呢?其實並沒有,就像Kconfig一樣,Makefile也是分布式存在於整個源碼樹中,頂層makefile根據配置遞歸地進入到子目錄中,調用子目錄中的Makefile進行編譯。
同樣地,需要修改drivers/char/目錄下的Makefile文件,添加一行:
obj-$(CONFIG_CDEV_TEST) += cdev_test/
在編譯時,如果CONFIG_CDEV_TEST變量為y,cdev_test/Makefile就會被調用。
在make menuconfig中選中
生成配置的部分完成,就需要在menuconfig菜單中進行配置,執行:
make menuconfig
進入目錄選項Device Driver --> Character devices--->cdev test dir.
然后按'y'選中模塊cdev test support
保存退出,然后執行編譯:
make
拷貝到目標主機
將vmlinuz(zImage)、System.map拷貝到目標主機的/boot目錄下。
在編譯生成的modules拷貝到目標主機的/lib/modules目錄下。
需要注意的是:啟動文件也好,模塊也好,在目標板上很可能文件名為諸如vmlinuz-$version,會包含版本信息,需要將文件名修改成一致,不然無法啟動。對於模塊而言,就是相應模塊無法加載。
驗證
最后一步就是驗證自己的驅動程序是否被編譯進內核,如果被編譯進內核,驅動程序中的module_init()程序將被系統調用,完成一些開發者指定的操作。
這一部分的驗證操作就是各顯身手了。
好了,關於linux將驅動程序編譯進內核的討論就到此為止啦,如果朋友們對於這個有什么疑問或者發現有文章中有什么錯誤,歡迎留言
原創博客,轉載請注明出處!
祝各位早日實現項目叢中過,bug不沾身.