天天看點

Linux裝置驅動Hello World程式介紹

by Valerie Henson

07/05/2007

(譯者注:本文的例子是隻能在linux的2.6核心下使用的,2.6以上的核心,譯者沒有做過實驗,2.4是要修改make檔案才能運作。)

一個核心子產品kernel module是一段能被核心動态加載和解除安裝的核心代碼,因為核心子產品程式是核心的一個部分,并且和核心緊密的互動,是以核心子產品不可能脫離核心編譯環境,至少,它需要核心的頭檔案和用于加載的配置資訊。編譯核心子產品同樣需要相關的開發工具,比如說編譯器。為了簡化,本文隻簡要讨論如何在Debian、Fedora和其他以.tar.gz形式提供的原版linux核心下進行核子產品的編譯。在這種情況下,你必須根據你正在運作核心相對應的核心源代碼來編譯你的核心子產品kernel module(當你的核心子產品一旦被裝載到你核心中時,核心就将執行該子產品的代碼)

必須要注意核心源代碼的位置,權限:核心程式通常在/usr/src/linux目錄下,并且屬主是root。如今,推薦的方式是将核心程式放在一個非root使用者的home目錄下。本文中所有指令都運作在非root的使用者下,隻有在必要的時候,才使用sudo來獲得臨時的root權限。配置和使用sudo可以man sudo(8) visudo(8) 和sudoers(5)。或者切換到root使用者下執行相關的指令。不管什麼方式,你都需要root權限才能執行本文中的一些指令。

在Debian下編譯核心子產品的準備

使用如下的指令安裝和配置用于在Debian編譯核心子產品的module-assitant包

 $sudoapt-getinstallmodule-assistant

Fedora的kernel-devel包包含了你編譯Fedora核心子產品的所有必要核心頭檔案和工具。你可以通過如下指令得到這個包。

 $sudoyuminstallkernel-devel

一般Linux 核心源代碼和配置

(譯者注,下面的編譯很複雜,如果你的Linux不是上面的系統,你可以使用REHL AS4系統,這個系統的核心就是2.6的核心,并且可以通過安裝直接安裝核心編譯支援環境,進而就省下了如下的步驟。而且下面的步驟比較複雜,建議在虛拟機安裝Linux進行實驗。)

如果你選擇使用一般的Linux核心源代嗎,你必須,配置,編譯,安裝和重新開機的你編譯核心。這個過程非常複雜,并且本文隻會讨論使用一般核心源代碼的基本概念。

linux的著名的核心源代碼在http://kernel.org上都可以找到。最近新釋出的穩定版本的代碼在首頁上。下載下傳全版本的源代碼,不要下載下傳更新檔代碼。例如,目前釋出穩定版本在url: http://kernel.org/pub/linux/kernel/v2.6/linux-2.6.21.5.tar.bz2上。如果需要更快速的下載下傳,從htpp://kernel.org/mirrors上找到最近的鏡像進行下載下傳。最簡單獲得源代碼的方式是以斷點續傳的方式使用wget。如今的http很少發生中斷,但是如果你在下載下傳過程中發生了中斷,這個指令将幫助你繼續下載下傳剩下的部分。

解包核心源代碼

$tarxjvf linux-<version>.tar.bz2

現在你的核心源代碼位于linux-/目錄下。轉到這個目錄下,并配置它:

$cdlinux-<version>

$ make menuconfig

一些非常易用的編譯目标make targets提供了多種編譯安裝核心的形式:Debian 包,RPM包,gzip後的tar檔案 等等,使用如下指令檢視所有可以編譯的目标形式

 $makehelp

一個可以工作在任何linux的目标是:(譯者注:REHL AS4上沒有tar-pkg這個目标,你可以任選一個rpm編譯,編譯完後再上層目錄可以看到有一個linux-.tar.gz可以使用)

 $maketar-pkg

當編譯完成後,可以調用如下指令安裝你的核心

 $sudotar-C / -xvf linux-<version>.tar

在标準位置建立的到核心源代碼的連結

 $sudoln-s <location oftop-levelsourcedirectory> /lib/modules/'uname-r'/build

現在已經核心源代碼已經可以用于編譯核心子產品了,重新開機你的機器以使得你根據新核心程式編譯的核心可以被裝載。

我們的第一個核心子產品,我們将以一個在核心中使用函數printk()列印”Hello world”的核心子產品為開始。printk是核心中的printf函數。printk的輸出列印在核心的消息緩存kernel message buffer并拷貝到/var/log/messages(關于拷貝的變化依賴于如何配置syslogd)

 $tarxzvf hello_printk.tar.gz

這個包中包含兩個檔案:Makefile,裡面包含如何建立核心子產品的指令和一個包含核心子產品源代碼的hello_printk.c檔案。首先,我們将簡要的過一下這個Makefile 檔案。

 obj-m := hello_printk.o

obj-m指出将要編譯成的核心子產品清單。.o格式檔案會自動地有相應的.c檔案生成(不需要顯示的羅列所有源代碼檔案)

 KDIR  := /lib/modules/$(shelluname-r)/build

KDIR表示是核心源代碼的位置。在目前标準情況是連結到包含着正在使用核心對應源代碼的目錄樹位置。

 PWD := $(shellpwd)

PWD訓示了目前工作目錄并且是我們自己核心子產品的源代碼位置

 default:

     $(MAKE) -C $(KDIR) M=$(PWD) modules

default是預設的編譯連接配接目标;即,make将預設執行本條規則編譯目标,除非程式員顯示的指明編譯其他目标。這裡的的編譯規則的意思是,在包含核心源代碼位置的地方進行make,然後之編譯$(PWD)(目前)目錄下的modules。這裡允許我們使用所有定義在核心源代碼樹下的所有規則來編譯我們的核心子產品。

現在我們來看看hello_printk.c這個檔案

 1.#include

2.    <linux/init.h>

3.#include

4.    <linux/module.h>

這裡包含了核心提供的所有核心子產品都需要的頭檔案。這個檔案中包含了類似module_init()宏的定義,這個宏稍後我們将用到

 1.staticint__init

2.hello_init(void){

3.    printk("Hello, world!n");

4.    return 0;

5.}

這是核心子產品的初始化函數,這個函數在核心子產品初始化被裝載的時候調用。__init關鍵字告訴核心這個代碼隻會被運作一次,而且是在核心裝載的時候。printk()函數這一行将列印一個”Hello, world”到核心消息緩存。printk參數的形式在大多數情況和printf(3)一模一樣。

 1.module_init(hello_init);

2.module_init()

宏告訴核心當核心子產品第一次運作時哪一個函數将被運作。任何在核心子產品中其他部分都會受到核心子產品初始化函數的影響。

 1.staticvoid__exit

2.hello_exit(void){

3.    printk("Goodbye, world!n");

4.}

5.module_exit(hello_exit);

同樣地,退出函數也隻在核心子產品被解除安裝的時候會運作一次,module_exit()宏标示了退出函數。__exit關鍵字告訴核心這段代碼隻在核心子產品被解除安裝的時候運作一次。

 1.MODULE_LICENSE("GPL");

2.MODULE_AUTHOR("Valerie Henson [email protected]");

3.MODULE_DESCRIPTION("Hello, world!" minimal module");

4.MODULE_VERSION("printk");

5.MODULE_LICENSE()

宏告訴核心,核心子產品代碼在什麼樣的license之下,這将影響主那些符号(函數和變量,等等)可以通路主核心。GPLv2 下的子產品(如同本例子中)能通路所有的符号。某些核心子產品license将會損害核心開源的特性,這些license訓示核心将裝載一些非公開或不受信的代碼。如果核心子產品不使用MODULE_LICENSE()宏,就被假定為非GPLv2的,這會損害核心的開源特性,并且大部分Linux核心開發人員都會忽略來自受損核心的bug報告,因為他們無法通路所有的源代碼,這使得調試變得更加困難。剩下的MODULE_*()這些宏以标準格式提供有用的标示該核心子產品的資訊(譯者注:這裡意思是,你必須使用GPLv2的license,否則你的驅動程式很有可能得不到Linux社群的開發者的支援 :))

現在,開始編譯和運作代碼。轉到相應的目錄下,編譯核心子產品

$cdhello_printk

$ make

接着,裝載核心子產品,使用insmod指令,并且通過dmesg來檢查列印出的資訊,dmesg是列印核心消息緩存的程式。

$sudoinsmod ./hello_printk.ko

$ dmesg | tail

你将從dmesg的螢幕輸出中看見”Hello world!”資訊。現在解除安裝使用rmmod解除安裝核心子產品,并檢查退出資訊。

$sudormmod hello_printk

到此,你就成功地完成了對核心子產品的編譯和安裝!

一種使用者程式和核心通訊最簡單和流行的方式是通過使用/proc下檔案系統進行通訊。/proc是一個僞檔案系統,從這裡的檔案讀取的資料是由核心傳回的資料,并且寫入到這裡面的資料将會被核心讀取和處理。在使用/proc方式之前,所用使用者和核心之間的通訊都不得不使用系統調用來完成。使用系統調用意味着你将在要在查找已經具有你需要的行為方式的系統調用(一般不會出現這種情況),或者建立一種新的系統調用來滿足你的需求(這樣就要求對核心全局做修改,并增加系統調用的數量,這是通常是非常不好的做法),或者使用ioctl這個萬能系統調用,這就要求要建立一個新檔案類型供ioctl操作(這也是非常複雜而且bug比較多的方式,同樣是非常繁瑣的)。/proc提供了一個簡單的,無需定義的方式在使用者空間和核心之間傳遞資料,這種方式不僅可以滿足核心使用,同樣也提供足夠的自由度給核心子產品做他們需要做的事情。

 1.#include <linux/init.h>

2.#include <linux/module.h>

3.#include <linux/proc_fs.h>

這次,我們将增加一個proc_fs頭檔案,這個頭檔案包括驅動注冊到/proc檔案系統的支援。當另外一個程序調用read()時,下一個函數将會被調用。這個函數的實作比一個完整的普通核心驅動的read系統調用實作要簡單的多,因為我們僅做了讓”Hello world”這個字元串緩存被一次讀完。

 1.staticint

2.hello_read_proc(char *buffer, char **start,off_t offset,

3.                int size, int *eof, void *data)

4.{

這個函數的參數值得明确的解釋一下。buffer是指向核心緩存的指針,我們将把read輸出的内容寫到這個buffer中。start參數多用更複雜的/proc檔案;我們在這裡将忽略這個參數;并且我隻明确的允許offset這個的值為0。size是指buffer中包含多位元組數;我們必須檢查這個參數已避免出現記憶體越界的情況,eof參數一個EOF的簡寫,用于傳回檔案是否已經讀到結束,而不需要通過調用read傳回0來判斷檔案是否結束。這裡我們不讨論依靠更複雜的/proc檔案傳輸資料的方法。這個函數方法體羅列如下:

 01.    char*hello_str ="Hello, world!\n";

02.    int len = strlen(hello_str); /* Don't include the null byte. */

03.    /*     * We only support reading the whole string at once.     */

04.    if (size < len)

05.        return< -EINVAL;

06.    /*     * If file position is non-zero, then assume the string has

07.    * been read and indicate there is no more data to be read.

08.    */

09.    if (offset != 0)

10.        return 0;

11.    /*     * We know the buffer is big enough to hold the string.     */

12.    strcpy(buffer, hello_str);

13.    /*     * Signal EOF.     */

14.    *eof = 1;

15.    return len;

16.}

下面,我們需将核心子產品在初始化函數注冊在/proc 子系統中。

 01.staticint__init

02.hello_init(void){

03.    /*

04.    * Create an entry in /proc named "hello_world" that calls

05.    * hello_read_proc() when the file is read.

06.    */

07.    if (create_proc_read_entry("hello_world", 0,

08.                        NULL, hello_read_proc, NULL) == 0) {

09.        printk(KERN_ERR

10.        "Unable to register "Hello, world!" proc filen");

11.        return -ENOMEM;

12.    }

13.    return 0;

14.}

15.module_init(hello_init);

當核心子產品解除安裝時,需要在/proc移出注冊的資訊(如果我們不這樣做的,當一個程序試圖去通路/proc/hello_world,/proc檔案系統将會試着執行一個已經不存在的功能,這樣将會導緻核心崩潰)

01.static void __exit

02.hello_exit(void){

03.    remove_proc_entry("hello_world", NULL);

04.}

05.module_exit(hello_exit);

06.MODULE_LICENSE("GPL");

07.MODULE_AUTHOR("Valerie Henson [email protected]");

08.MODULE_DESCRIPTION(""Hello, world!" minimal module");

09.MODULE_VERSION("proc");

下面我們将準備編譯和裝載模組

 cdhello_proc

$ sudo insmod ./hello_proc.ko

現在,将會有一個稱為/proc/hello_world的檔案,并且讀這個檔案的,将會傳回一個”Hello world”字元串。

$cat/proc/hello_world

Hello, world!

現在我們将使用在/dev目錄下的一個裝置檔案/dev/hello_world實作”Hello,world!” 。追述以前的日子,裝置檔案是通過MAKEDEV腳本調用mknod指令在/dev目錄下産生的一個特定的檔案,這個檔案和裝置是否運作在改機器上無關。到後來裝置檔案使用了devfs,devfs在裝置第一被通路的時候建立/dev檔案,這樣将會導緻很多有趣的加鎖問題和多次打開裝置檔案的檢查裝置是否存在的重試問題。目前的/dev版本支援被稱為udev,因為他将在使用者程式空間建立到/dev的符号連接配接。當核心子產品注冊裝置時,他們将出現在sysfs檔案系統中,并mount在/sys下。一個使用者空間的程式,udev,注意到/sys下的改變将會根據在/etc/udev/下的一些規則在/dev下建立相關的檔案項。

1.#include <linux/fs.h>

2.#include <linux/init.h>

3.#include <linux/miscdevice.h>

4.#include <linux/module.h>

5.#include <asm/uaccess.h>

正如我們看到的必須的頭檔案外,建立一個新裝置還需要更多的核心頭檔案支援。fs.sh包含所有檔案操作的結構,這些結構将由裝置驅動程式來填值,并關聯到我們相關的/dev檔案。miscdevice.h頭檔案包含了對通用miscellaneous裝置檔案注冊的支援。 asm/uaccess.h包含了測試我們是否違背通路權限讀寫使用者記憶體空間的函數。hello_read将在其他程序在/dev/hello調用read()函數被調用的是一個函數。他将輸出”Hello world!”到由read()傳入的緩存。

01.static ssize_t hello_read(struct file * file, char * buf, size_t count, loff_t *ppos)

02.{

03.    char *hello_str = "Hello, world!n";

04.    int len = strlen(hello_str); /* Don't include the null byte. */

05.    /*     * We only support reading the whole string at once.     */

06.    if (count < len)

07.        return -EINVAL;

08.    /*

09.    * If file position is non-zero, then assume the string has

10.    * been read and indicate there is no more data to be read.

11.    */

12.    if (*ppos != 0)

13.        return 0;

14.    /*

15.    * Besides copying the string to the user provided buffer,

16.    * this function also checks that the user has permission to

17.    * write to the buffer, that it is mapped, etc.

18.    */

19.    if (copy_to_user(buf, hello_str, len))

20.        return -EINVAL;

21.    /*

22.    * Tell the user how much data we wrote.

23.    */

24.    *ppos = len;

25.    return len;

26.}

下一步,我們建立一個檔案操作結構file operations struct,并用這個結構來定義當檔案被通路時執行什麼動作。在我們的例子中我們唯一關注的檔案操作就是read。

1.static const struct file_operations hello_fops = {

2.    .owner        = THIS_MODULE,

3.    .read        = hello_read,

4.};

現在,我們将建立一個結構,這個結構包含有用于在核心注冊一個通用miscellaneous驅動程式的資訊。

01.static struct miscdevice hello_dev = {

02.    /*

03.    * We don't care what minor number we end up with, so tell the

04.    * kernel to just pick one.

05.    */

06.    MISC_DYNAMIC_MINOR,

07.    /*

08.    * Name ourselves /dev/hello.

09.    */

10.    "hello",

11.    /*

12.    * What functions to call when a program performs file

13.    * operations on the device.

14.    */

15.    &hello_fops

16.};

在通常情況下,我們在init中注冊裝置

01.static int __init

03.    int ret;

04.    /*

05.    * Create the "hello" device in the /sys/class/misc directory.

06.    * Udev will automatically create the /dev/hello device using

07.    * the default rules.

09.    ret = misc_register(&hello_dev);

10.    if (ret)

11.        printk(KERN_ERR

12.            "Unable to register "Hello, world!" misc devicen");

13.    return ret;

接下是在解除安裝時的退出函數

03.    misc_deregister(&hello_dev);

07.MODULE_AUTHOR("Valerie Henson [email protected]>");

09.MODULE_VERSION("dev");

編譯并加載子產品:

$ cd hello_dev

$ sudo insmod ./hello_dev.ko

現在我們将有一個稱為/dev/hello的裝置檔案,并且這個裝置檔案被root通路時将會産生一個”Hello, world!”

$ sudo cat /dev/hello

 Hello, world!

但是我們不能使用普通使用者通路他:

$ cat /dev/hello

cat:/dev/hello: Permission denied  

$ ls -l

/dev/hello crw-rw---- 1 root root 10, 61 2007-06-20 14:31 /dev/hello

這是有預設的udev規則導緻的,這個條規将标明當一個普通裝置出現時,他的名字将會是/dev/,并且預設的通路權限是0660(使用者群組讀寫通路,其他使用者無法通路)。我們在真實情況中可能會希望建立一個被普通使用者通路的裝置驅動程式,并且給這個裝置起一個相應的連接配接名。為達到這個目的,我們将編寫一條udev規則。

udev規則必須做兩件事情:第一建立一個符号連接配接,第二修改裝置的通路權限。

下面這條規則可以達到這個目的:

KERNEL=="hello", SYMLINK+="hello_world", MODE="0444"

我們将詳細的分解這條規則,并解釋每一個部分。KERNEL==”hello” 标示下面的的規則将作用于/sys中裝置名字”hello”的裝置(==是比較符)。hello 裝置是我們通過調用misc_register()并傳遞了一個包含裝置名為”hello”的檔案操作結構file_operations為參數而達到的。你可以自己通過如下的指令在/sys下檢視

$ ls -d /sys/class/misc/hello//sys/class/misc/hello/

SYMLINK+=”hello_world” 的意思是在符号連結清單中增加 (+= 符号的意思着追加)一個hello_world ,這個符号連接配接在裝置出現時建立。在我們場景下,我們知道我們的清單的中的隻有這個符号連接配接,但是其他裝置驅動程式可能會存在多個不同的符号連接配接,是以使用将裝置追加入到符号清單中,而不是覆寫清單将會是更好的實踐中的做法。

MODE=”0444″的意思是原始的裝置的通路權限是0444,這個權限允許使用者,組,和其他使用者可以通路。

通常,使用正确的操作符号(==, +=, or =)是非常重要的,否則将會出現不可預知的情況。

現在我們了解這個規則是怎麼工作的,讓我們将其安裝在/etc/udev目錄下。udev規則檔案以和System V初始腳本目錄命名的同種方式的目錄下,/etc/udeve/rules.d這個目錄,并以字母/數字的順序。和System V的初始化腳本一樣,/etc/udev/rules.d下的目錄通常符号連接配接到真正的檔案,通過使用符号連接配接名,将使得規則檔案已正确的次序得到執行。

使用如下的指令,拷貝hello.rules檔案從/hello_dev目錄到/etc/udev目錄下,并建立一一個最先被執行的規則檔案連結在/etc/udev/rules.d目錄下。

$ sudo cp hello.rules /etc/udev/

$ sudo ln -s ../hello.rules /etc/udev/rules.d/010_hello.rules

現在我們重新裝載驅動程式,并觀察新的驅動程式項

$ sudo rmmod hello_dev

$ ls -l /dev/hello*

cr--r--r-- 1 root root 10, 61 2007-06-19 21:21 /dev/hello

lrwxrwxrwx 1 root root      5 2007-06-19 21:21 /dev/hello_world -> hello

最後,檢查你可以使用普通使用者通路/dev/hello_world裝置.

$ cat /dev/hello_world

Hello, world!  

本文轉自 haoel 51CTO部落格,原文連結:http://blog.51cto.com/haoel/158981,如需轉載請自行聯系原作者

繼續閱讀