自古以來,學習一門新編程語言的第一步就是寫一個打印“hello world”的程序(可以看《hello world 集中營》這個帖子供羅列了300個“hello world”程序例子)在本文中,我們將用同樣的方式學習如何編寫一個簡單的linux內核模塊和設備驅動程序。我將學習到如何在內核模式下以三種不同的方式來打印hello world,這三種方式分別是: printk(),/proc文件,/dev下的設備文件。
一個內核模塊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包
$ sudo apt-get install module-assistant
以此你就可以開始編譯內核模塊,你可以在《Debian Linux Kernel Handbook》這本書中找到對Debian內核相關任務的更深度的討論。
Fedora的kernel-devel包包含了你編譯Fedora內核模塊的所有必要內核頭文件和工具。你可以通過如下命令得到這個包。
$ sudo yum install kernel-devel
有了這個包,你就可以編譯你的內核模塊kernel modules。關於Fedora編譯內核模塊的相關文檔你可以從Fedora release notes中找到。
一般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很少發生中斷,但是如果你在下載過程中發生了中斷,這個命令將幫助你繼續下載剩下的部分。
$ wget -c http://kernel.org/pub/linux/kernel/v2.6/linux-2.6.21.5.tar.bz2
解包內核源代碼
$ tar xjvf linux-<version>.tar.bz2
現在你的內核源代碼位於linux-/目錄下。轉到這個目錄下,並配置它:
$ cd linux-<version> $ make menuconfig
一些非常易用的編譯目標make targets提供了多種編譯安裝內核的形式:Debian 包,RPM包,gzip後的tar文件 等等,使用如下命令查看所有可以編譯的目標形式
$ make help
一個可以工作在任何linux的目標是:(譯者注:REHL AS4上沒有tar-pkg這個目標,你可以任選一個rpm編譯,編譯完後再上層目錄可以看到有一個linux-.tar.gz可以使用)
$ make tar-pkg
當編譯完成後,可以調用如下命令安裝你的內核
$ sudo tar -C / -xvf linux-<version>.tar
在標准位置建立的到內核源代碼的鏈接
$ sudo ln -s <location of top-level source directory> /lib/modules/'uname -r'/build
現在已經內核源代碼已經可以用於編譯內核模塊了,重啟你的機器以使得你根據新內核程序編譯的內核可以被裝載。
我們的第一個內核模塊,我們將以一個在內核中使用函數printk()打印”Hello world”的內核模塊為開始。printk是內核中的printf函數。printk的輸出打印在內核的消息緩存kernel message buffer並拷貝到/var/log/messages(關於拷貝的變化依賴於如何配置syslogd)
下載hello_printk 模塊的tar包 並解包:
$ tar xzvf hello_printk.tar.gz
這個包中包含兩個文件:Makefile,裡面包含如何創建內核模塊的指令和一個包含內核模塊源代碼的hello_printk.c文件。首先,我們將簡要的過一下這個Makefile 文件。
obj-m := hello_printk.o
obj-m指出將要編譯成的內核模塊列表。.o格式文件會自動地有相應的.c文件生成(不需要顯示的羅列所有源代碼文件)
KDIR := /lib/modules/$(shell uname -r)/build
KDIR表示是內核源代碼的位置。在當前標准情況是鏈接到包含著正在使用內核對應源代碼的目錄樹位置。
PWD := $(shell pwd)
PWD指示了當前工作目錄並且是我們自己內核模塊的源代碼位置
default: $(MAKE) -C $(KDIR) M=$(PWD) modules
default是默認的編譯連接目標;即,make將默認執行本條規則編譯目標,除非程序員顯示的指明編譯其他目標。這裡的的編譯規則的意思是,在包含內核源代碼位置的地方進行make,然後之編譯$(PWD)(當前)目錄下的modules。這裡允許我們使用所有定義在內核源代碼樹下的所有規則來編譯我們的內核模塊。
現在我們來看看hello_printk.c這個文件
#include <linux/init.h> #include <linux/module.h>
這裡包含了內核提供的所有內核模塊都需要的頭文件。這個文件中包含了類似module_init()宏的定義,這個宏稍後我們將用到
static int __init hello_init(void){ printk("Hello, world!n"); return 0; }
這是內核模塊的初始化函數,這個函數在內核模塊初始化被裝載的時候調用。__init關鍵字告訴內核這個代碼只會被運行一次,而且是在內核裝載的時候。printk()函數這一行將打印一個”Hello, world”到內核消息緩存。printk參數的形式在大多數情況和printf(3)一模一樣。
module_init(hello_init); module_init()
宏告訴內核當內核模塊第一次運行時哪一個函數將被運行。任何在內核模塊中其他部分都會受到內核模塊初始化函數的影響。
static void __exit hello_exit(void){ printk("Goodbye, world!n"); } module_exit(hello_exit);
同樣地,退出函數也只在內核模塊被卸載的時候會運行一次,module_exit()宏標示了退出函數。__exit關鍵字告訴內核這段代碼只在內核模塊被卸載的時候運行一次。
MODULE_LICENSE("GPL"); MODULE_AUTHOR("Valerie Henson [email protected]"); MODULE_DESCRIPTION("Hello, world!" minimal module"); MODULE_VERSION("printk"); MODULE_LICENSE()
宏告訴內核,內核模塊代碼在什麼樣的license之下,這將影響主那些符號(函數和變量,等等)可以訪問主內核。GPLv2 下的模塊(如同本例子中)能訪問所有的符號。某些內核模塊license將會損害內核開源的特性,這些license指示內核將裝載一些非公開或不受信的代碼。如果內核模塊不使用MODULE_LICENSE()宏,就被假定為非GPLv2的,這會損害內核的開源特性,並且大部分Linux內核開發人員都會忽略來自受損內核的bug報告,因為他們無法訪問所有的源代碼,這使得調試變得更加困難。剩下的MODULE_*()這些宏以標准格式提供有用的標示該內核模塊的信息(譯者注:這裡意思是,你必須使用GPLv2的license,否則你的驅動程序很有可能得不到Linux社區的開發者的支持 :))
現在,開始編譯和運行代碼。轉到相應的目錄下,編譯內核模塊
$ cd hello_printk $ make
接著,裝載內核模塊,使用insmod指令,並且通過dmesg來檢查打印出的信息,dmesg是打印內核消息緩存的程序。
$ sudo insmod ./hello_printk.ko $ dmesg | tail
你將從dmesg的屏幕輸出中看見”Hello world!”信息。現在卸載使用rmmod卸載內核模塊,並檢查退出信息。
$ sudo rmmod hello_printk $ dmesg | tail
到此,你就成功地完成了對內核模塊的編譯和安裝!
一種用戶程序和內核通訊最簡單和流行的方式是通過使用/proc下文件系統進行通訊。/proc是一個偽文件系統,從這裡的文件讀取的數據是由內核返回的數據,並且寫入到這裡面的數據將會被內核讀取和處理。在使用/proc方式之前,所用用戶和內核之間的通訊都不得不使用系統調用來完成。使用系統調用意味著你將在要在查找已經具有你需要的行為方式的系統調用(一般不會出現這種情況),或者創建一種新的系統調用來滿足你的需求(這樣就要求對內核全局做修改,並增加系統調用的數量,這是通常是非常不好的做法),或者使用ioctl這個萬能系統調用,這就要求要創建一個新文件類型供ioctl操作(這也是非常復雜而且bug比較多的方式,同樣是非常繁瑣的)。/proc提供了一個簡單的,無需定義的方式在用戶空間和內核之間傳遞數據,這種方式不僅可以滿足內核使用,同樣也提供足夠的自由度給內核模塊做他們需要做的事情。
為了滿足我們的要求,我們需要當我們讀在/proc下的某一個文件時將會返回一個“Hello world!”。我們將使用/proc/hello_world這個文件。下載並解開hello proc這個gzip的tar包後,我們將首先來看一下hello_proc.c這個文件
#include <linux/init.h> #include <linux/module.h> #include <linux/proc_fs.h>
這次,我們將增加一個proc_fs頭文件,這個頭文件包括驅動注冊到/proc文件系統的支持。當另外一個進程調用read()時,下一個函數將會被調用。這個函數的實現比一個完整的普通內核驅動的read系統調用實現要簡單的多,因為我們僅做了讓”Hello world”這個字符串緩存被一次讀完。
static int hello_read_proc(char *buffer, char **start,off_t offset, int size, int *eof, void *data) {
這個函數的參數值得明確的解釋一下。buffer是指向內核緩存的指針,我們將把read輸出的內容寫到這個buffer中。start參數多用更復雜的/proc文件;我們在這裡將忽略這個參數;並且我只明確的允許offset這個的值為0。size是指buffer中包含多字節數;我們必須檢查這個參數已避免出現內存越界的情況,eof參數一個EOF的簡寫,用於返回文件是否已經讀到結束,而不需要通過調用read返回0來判斷文件是否結束。這裡我們不討論依靠更復雜的/proc文件傳輸數據的方法。這個函數方法體羅列如下:
char *hello_str = "Hello, world!\n"; int len = strlen(hello_str); /* Don't include the null byte. */ /* * We only support reading the whole string at once. */ if (size < len) return< -EINVAL; /* * If file position is non-zero, then assume the string has * been read and indicate there is no more data to be read. */ if (offset != 0) return 0; /* * We know the buffer is big enough to hold the string. */ strcpy(buffer, hello_str); /* * Signal EOF. */ *eof = 1; return len; }
下面,我們需將內核模塊在初始化函數注冊在/proc 子系統中。
static int __init hello_init(void){ /* * Create an entry in /proc named "hello_world" that calls * hello_read_proc() when the file is read. */ if (create_proc_read_entry("hello_world", 0, NULL, hello_read_proc, NULL) == 0) { printk(KERN_ERR "Unable to register "Hello, world!" proc filen"); return -ENOMEM; } return 0; } module_init(hello_init);
當內核模塊卸載時,需要在/proc移出注冊的信息(如果我們不這樣做的,當一個進程試圖去訪問/proc/hello_world,/proc文件系統將會試著執行一個已經不存在的功能,這樣將會導致內核崩潰)
static void __exit hello_exit(void){ remove_proc_entry("hello_world", NULL); } module_exit(hello_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Valerie Henson [email protected]"); MODULE_DESCRIPTION(""Hello, world!" minimal module"); MODULE_VERSION("proc");
下面我們將准備編譯和裝載模組
$ cd hello_proc $ make $ sudo insmod ./hello_proc.ko
現在,將會有一個稱為/proc/hello_world的文件,並且讀這個文件的,將會返回一個”Hello world”字符串。
$ cat /proc/hello_world Hello, world!
你可以為為同一個驅動程序創建多個/proc文件,並增加相應寫/proc文件的函數,創建包含多個/proc文件的目錄,或者更多的其他操作。如果要寫比這個更復雜的驅動程序,可以使用seq_file函數集來編寫是更安全和容易的。關於這些更多的信息可以看《Driver porting: The seq_file interface》
現在我們將使用在/dev目錄下的一個設備文件/dev/hello_world實現”Hello,world!” 。追述以前的日子,設備文件是通過MAKEDEV腳本調用mknod命令在/dev目錄下產生的一個特定的文件,這個文件和設備是否運行在改機器上無關。到後來設備文件使用了devfs,devfs在設備第一被訪問的時候創建/dev文件,這樣將會導致很多有趣的加鎖問題和多次打開設備文件的檢查設備是否存在的重試問題。當前的/dev版本支持被稱為udev,因為他將在用戶程序空間創建到/dev的符號連接。當內核模塊注冊設備時,他們將出現在sysfs文件系統中,並mount在/sys下。一個用戶空間的程序,udev,注意到/sys下的改變將會根據在/etc/udev/下的一些規則在/dev下創建相關的文件項。
下載hello world內核模塊的gzip的tar包,我們將開始先看一下hello_dev.c這個源文件。
#include <linux/fs.h> #include <linux/init.h> #include <linux/miscdevice.h> #include <linux/module.h> #include <asm/uaccess.h>
正如我們看到的必須的頭文件外,創建一個新設備還需要更多的內核頭文件支持。fs.sh包含所有文件操作的結構,這些結構將由設備驅動程序來填值,並關聯到我們相關的/dev文件。miscdevice.h頭文件包含了對通用miscellaneous設備文件注冊的支持。 asm/uaccess.h包含了測試我們是否違背訪問權限讀寫用戶內存空間的函數。hello_read將在其他進程在/dev/hello調用read()函數被調用的是一個函數。他將輸出”Hello world!”到由read()傳入的緩存。
static ssize_t hello_read(struct file * file, char * buf, size_t count, loff_t *ppos) { char *hello_str = "Hello, world!n"; int len = strlen(hello_str); /* Don't include the null byte. */ /* * We only support reading the whole string at once. */ if (count < len) return -EINVAL; /* * If file position is non-zero, then assume the string has * been read and indicate there is no more data to be read. */ if (*ppos != 0) return 0; /* * Besides copying the string to the user provided buffer, * this function also checks that the user has permission to * write to the buffer, that it is mapped, etc. */ if (copy_to_user(buf, hello_str, len)) return -EINVAL; /* * Tell the user how much data we wrote. */ *ppos = len; return len; }
下一步,我們創建一個文件操作結構file operations struct,並用這個結構來定義當文件被訪問時執行什麼動作。在我們的例子中我們唯一關注的文件操作就是read。
static const struct file_operations hello_fops = { .owner = THIS_MODULE, .read = hello_read, };
現在,我們將創建一個結構,這個結構包含有用於在內核注冊一個通用miscellaneous驅動程序的信息。
static struct miscdevice hello_dev = { /* * We don't care what minor number we end up with, so tell the * kernel to just pick one. */ MISC_DYNAMIC_MINOR, /* * Name ourselves /dev/hello. */ "hello", /* * What functions to call when a program performs file * operations on the device. */ &hello_fops };
在通常情況下,我們在init中注冊設備
static int __init hello_init(void){ int ret; /* * Create the "hello" device in the /sys/class/misc directory. * Udev will automatically create the /dev/hello device using * the default rules. */ ret = misc_register(&hello_dev); if (ret) printk(KERN_ERR "Unable to register "Hello, world!" misc devicen"); return ret; } module_init(hello_init);
接下是在卸載時的退出函數
static void __exit hello_exit(void){ misc_deregister(&hello_dev); } module_exit(hello_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("Valerie Henson [email protected]>"); MODULE_DESCRIPTION(""Hello, world!" minimal module"); MODULE_VERSION("dev");
編譯並加載模塊:
$ cd hello_dev $ make $ 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 $ sudo insmod ./hello_dev.ko $ 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! $ cat /dev/hello Hello, world!