MCU單晶片韌體設計

2015年3月23日 星期一

在Raspberry Pi & Pi2 上面學 Linux 驅動程式開發 (二) -- Hello World --

實驗名稱: Hello World Code

實驗目的: 編譯 Linux Module 並成功掛載進 Linux Kernel 系統之中。

使用材料及設備:

    硬體: Raspberry Pi B+

    軟體: 可編譯 Linux Module 的 Raspbian 系統,Hello.c code。

原理介紹:

    不知道為何,Hello World一直是學習程式設計開發時的第一個範例,在這裡也不例外。我們用一個只是印出 "Hello World!" 字串到 Linux 系統訊息裡的簡單程式,來做為 Linux Driver 學習的切入點。然後試圖由最簡單的例子開始一點一滴地描述 Linux 系統的運作方式,以降低初學者進入這一門領域的門檻。


實驗解說/功能說明:


掛載之後會在Linux 系統訊息空間裡面印出一行內容為 "Hello World." 的系統訊息。

卸載之時會在Linux 系統訊息空間裡面印出一行內容為 "Goodbye World." 的系統訊息。

程式架構說明:


前兩行 include了兩個基本的 .h 檔,提供了 Linux Kernel 一些基本的必要指令。沒有這兩行的話compile會說找不到這一個程式裡面所用到的所有巨集和函式。

MODULE_LICENCE("Dual BSD/GPL"): 在第四行的指令本身沒做什麼,就只是設定該 module 的版權為 "Dual BSD/GPL"。Linux kernel module 裡面,這類的宣告其實還蠻重要的。

這樣的宣告不單會影響之後的版權與法律問題,常常還會影響之後一些特定函式的使用(有些函式要求你要將版權宣告為 GPL 才會讓你用...神奇吧...)

static int hello_init(void){...}: 程式的第六行宣告了一個函式。這個函式前面的修飾字 static 會讓這個函式的名稱(符號名稱)不會被其他的 module 看到,這樣子做可以防止其他掛載的 module 也宣告了一個一樣名稱的函式,造成兩個 module 因為重復定義函式名稱衝突而不能同時存在的問題。

也因此這個函式的名稱可以自行設定,只要不要和被include的library裡面的函式名稱有相同的就行。在這裡我們就先取名為"hello_init"。

雖然而這個 hello_init 函式的名稱可以自行設定,但是其傳入和回傳的參數形式(參數介面)必須是按照 Linux 系統裡面的設定,是固定的。

而這個參數介面的資訊就要去查才會知道。為了說明這個,我們先跳到程式的第17行。

module_init(hello_init): 第17行可以看到使用了一個函式 module_init(hello_init),其中這裡傳入了hello_init的 function 記憶體位址。

所以可以知道我們只要去查 module_init 這個系統內附的函式所傳入的function pointer的參數介面形式就可以知道hello_init的參數介面要設定為傳入void, 回傳 int。

而 module_init 的作用是將指定的 function pointer 註冊到 kernel 裡面並且告知 kernel 說,該 function pointer 的位置是該 module 掛載的時候要執行的第一個 function。然後 kernel 就會在知道這件事情,並且在 "適當" 的時候去執行這段程式碼。

上面這段話聽起來有點奇怪,直接說執行指令 suod insmod hello.ko 的時候就會去 run 這個 hello_init這個function不就好了?(打那麼多廢話是出來騙錢的嗎?) 其實是因為,Linux Module 很多函式的行為就和上一段說所說的一樣,都是 "註冊特定的 function 給 kernel 在相對應特定的時機呼叫使用。"

所以換言之(發文灌水大絕招),被 module_init 所註冊到 kernel 裡面的 function , kernel 的執行時機點就是 "一開始",感覺有點像是一般程式的 main function 的開始的部份。所以通常來說這一段程式碼會拿來進行一些程式執行初期宣告記憶體和初始化的動作。

而一個程式有進入點,就有出口點,所以接下來我們看程式第18行。

module_exit(hello_exit): 第18行,相對於先前所說的module的進入點,這裡設定的是程式結束是要做的事情。

一般來說就是把之前在進入點初始化所宣告用的記憶體給 release 掉,然後做一些moudle 結束時要做的善後動作。

所以換言之(灌水大絕招連發)被 module_exit 所註冊到 kernel 裡面的 function (這裡就是指 hello_exit) kernel 的執行時機點就是 "卸載的時候" (執行指令 sudo rmmod hello.ko的時候),通常來說這一段程式碼會拿來進行一些釋放先前宣告占用的記憶體或其他善後的動作。

而在這裡因為先前沒有搞出有的沒的東西,所以印個 "Goodbye, world" 然後,就沒有然後了。

接下來我們來看裡面用到的一個 function : printk():

printk(): 這個 function 會把引入的字串給印出來。聽起來不就是 C 標準函式庫裡面的 printf 嗎,為何不用 printf 而用 printk ? 這是因為 Linux Kernel 裡面並不支援使用 C 標準函式庫。因為在開發 Linux Kernel Module 的時候用不到 C 標準函式庫提供的那麼多功能,所以 Linux kernel 只內附了一些必要的基本功能,像是 printk 這類的。所以在寫Linux Driver Module 的時候,不會看到有人 include 像是 stdio.h、stdlib.h 這類 C 標準函式庫。 而是使用 linux kernel 提供的函式庫,大部份集中在 source code 裡的 include/ 目錄底下。

一般來說 module 會需要傳達出去的訊息都算是系統核心訊息,所以 printk() 就是用來傳達系統訊息而且不會輸出到標準輸出介面 stdin 之上,而是直接輸出到 kernel message 的空間裡面,所以自訂函式 hello_init 裡面的 printk("Hello World\n"); 就只是在 kernel message 的空間裡面印出"Hello World" 這個訊息。

執行結果:


這個程式會在被載入的時候印出一個 "Hello World." 字串,然後就沒有任何反應,就只是個 Hello World moudle...。

不過初學 Linux 系統的人會發覺,螢幕什麼也看不到...然後以為我在騙錢。其實事情是這樣的,Linux Driver Module 的角色通常就是Linux Kernel的一部份,而 Linux Kernel 所發出的訊息都會和系統內部運作有關,而這類的訊息一般都會在顯示並存放在 Linux Kernel 系統裡面。要看到這些系統訊息可以使用 dmesg 這個指令在螢幕上顯示出來。




但是在印出來之後,這個 Hello World module 就結束了嗎? 不,這個程式還是會存在 Linux Kernel 裡面並沒有被結束掉,可以用指令 lsmod 看目前掛載在 kernel 裡面的 module 有哪些,裡面就應當看到 hello (就是 hello.ko 去掉副檔名) 在表當中。




因為 Module 程式要真的結束,需要使用指令 remod 將其卸載之後程式才會被從 Kernel 的記憶體空間裡面被結束掉。



底下是 hello.c 的內容






沒有留言 :

張貼留言