自古以來,學習一門新編程語言的第一步就是寫一個打印「hello world」的程序(能夠看《hello world 集中營》這個帖子供羅列了300個「hello world」程序例子)在本文中,咱們將用一樣的方式學習如何編寫一個簡單的linux內核模塊和設備驅動程序。我將學習到如何在內核模式下以三種不一樣的方式來打印hello world,這三種方式分別是: printk(),/proc文件,/dev下的設備文件。html
一個內核模塊kernel module是一段能被內核動態加載和卸載的內核代碼,由於內核模塊程序是內核的一個部分,而且和內核緊密的交互,因此內核模塊不可能脫離內核編譯環境,至少,它須要內核的頭文件和用於加載的配置信息。編譯內核模塊一樣須要相關的開發工具,好比說編譯器。爲了簡化,本文只簡要討論如何在Debian、Fedora和其餘以.tar.gz形式提供的原版linux內核下進行核模塊的編譯。在這種狀況下,你必須根據你正在運行內核相對應的內核源代碼來編譯你的內核模塊kernel module(當你的內核模塊一旦被裝載到你內核中時,內核就將執行該模塊的代碼)react
必需要注意內核源代碼的位置,權限:內核程序一般在/usr/src/linux目錄下,而且屬主是root。現在,推薦的方式是將內核程序放在一個非root用戶的home目錄下。本文中全部命令都運行在非root的用戶下,只有在必要的時候,才使用sudo來得到臨時的root權限。配置和使用sudo能夠man sudo(8) visudo(8) 和sudoers(5)。或者切換到root用戶下執行相關的命令。無論什麼方式,你都須要root權限才能執行本文中的一些命令。linux
在Debian下編譯內核模塊的準備程序員
使用以下的命令安裝和配置用於在Debian編譯內核模塊的module-assitant包shell
以此你就能夠開始編譯內核模塊,你能夠在《Debian Linux Kernel Handbook》這本書中找到對Debian內核相關任務的更深度的討論。編程
Fedora的kernel-devel包包含了你編譯Fedora內核模塊的全部必要內核頭文件和工具。你能夠經過以下命令獲得這個包。緩存
有了這個包,你就能夠編譯你的內核模塊kernel modules。關於Fedora編譯內核模塊的相關文檔你能夠從Fedora release notes中找到。安全
通常Linux 內核源代碼和配置app
(譯者注,下面的編譯很複雜,若是你的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不多發生中斷,可是若是你在下載過程當中發生了中斷,這個命令將幫助你繼續下載剩下的部分。
解包內核源代碼
如今你的內核源代碼位於linux-/目錄下。轉到這個目錄下,並配置它:
$
make
menuconfig
一些很是易用的編譯目標make targets提供了多種編譯安裝內核的形式:Debian 包,RPM包,gzip後的tar文件 等等,使用以下命令查看全部能夠編譯的目標形式
一個能夠工做在任何linux的目標是:(譯者注:REHL AS4上沒有tar-pkg這個目標,你能夠任選一個rpm編譯,編譯完後再上層目錄能夠看到有一個linux-.tar.gz可使用)
當編譯完成後,能夠調用以下命令安裝你的內核
在標準位置創建的到內核源代碼的連接
如今已經內核源代碼已經能夠用於編譯內核模塊了,重啓你的機器以使得你根據新內核程序編譯的內核能夠被裝載。
咱們的第一個內核模塊,咱們將以一個在內核中使用函數printk()打印」Hello world」的內核模塊爲開始。printk是內核中的printf函數。printk的輸出打印在內核的消息緩存kernel message buffer並拷貝到/var/log/messages(關於拷貝的變化依賴於如何配置syslogd)
下載hello_printk 模塊的tar包 並解包:
這個包中包含兩個文件:Makefile,裏面包含如何建立內核模塊的指令和一個包含內核模塊源代碼的hello_printk.c文件。首先,咱們將簡要的過一下這個Makefile 文件。
obj-m指出將要編譯成的內核模塊列表。.o格式文件會自動地有相應的.c文件生成(不須要顯示的羅列全部源代碼文件)
KDIR表示是內核源代碼的位置。在當前標準狀況是連接到包含着正在使用內核對應源代碼的目錄樹位置。
PWD指示了當前工做目錄而且是咱們本身內核模塊的源代碼位置
$(MAKE) -C $(KDIR) M=$(PWD) modules
default是默認的編譯鏈接目標;即,make將默認執行本條規則編譯目標,除非程序員顯示的指明編譯其餘目標。這裏的的編譯規則的意思是,在包含內核源代碼位置的地方進行make,而後之編譯$(PWD)(當前)目錄下的modules。這裏容許咱們使用全部定義在內核源代碼樹下的全部規則來編譯咱們的內核模塊。
如今咱們來看看hello_printk.c這個文件
2.
<linux/init.h>
3.
#include
4.
<linux/module.h>
這裏包含了內核提供的全部內核模塊都須要的頭文件。這個文件中包含了相似module_init()宏的定義,這個宏稍後咱們將用到
2.
hello_init(
void
){
3.
printk(
"Hello, world!n"
);
4.
return
0;
5.
}
這是內核模塊的初始化函數,這個函數在內核模塊初始化被裝載的時候調用。__init關鍵字告訴內核這個代碼只會被運行一次,並且是在內核裝載的時候。printk()函數這一行將打印一個」Hello, world」到內核消息緩存。printk參數的形式在大多數狀況和printf(3)如出一轍。
2.
module_init()
宏告訴內核當內核模塊第一次運行時哪個函數將被運行。任何在內核模塊中其餘部分都會受到內核模塊初始化函數的影響。
2.
hello_exit(
void
){
3.
printk(
"Goodbye, world!n"
);
4.
}
5.
module_exit(hello_exit);
一樣地,退出函數也只在內核模塊被卸載的時候會運行一次,module_exit()宏標示了退出函數。__exit關鍵字告訴內核這段代碼只在內核模塊被卸載的時候運行一次。
2.
MODULE_AUTHOR(
"Valerie Henson val@nmt.edu"
);
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社區的開發者的支持 :))
如今,開始編譯和運行代碼。轉到相應的目錄下,編譯內核模塊
$
make
接着,裝載內核模塊,使用insmod指令,而且經過dmesg來檢查打印出的信息,dmesg是打印內核消息緩存的程序。
$ dmesg |
tail
你將從dmesg的屏幕輸出中看見」Hello world!」信息。如今卸載使用rmmod卸載內核模塊,並檢查退出信息。
$ dmesg |
tail
到此,你就成功地完成了對內核模塊的編譯和安裝!
一種用戶程序和內核通信最簡單和流行的方式是經過使用/proc下文件系統進行通信。/proc是一個僞文件系統,從這裏的文件讀取的數據是由內核返回的數據,而且寫入到這裏面的數據將會被內核讀取和處理。在使用/proc方式以前,所用用戶和內核之間的通信都不得不使用系統調用來完成。使用系統調用意味着你將在要在查找已經具備你須要的行爲方式的系統調用(通常不會出現這種狀況),或者建立一種新的系統調用來知足你的需求(這樣就要求對內核全局作修改,並增長系統調用的數量,這是一般是很是很差的作法),或者使用ioctl這個萬能系統調用,這就要求要建立一個新文件類型供ioctl操做(這也是很是複雜並且bug比較多的方式,一樣是很是繁瑣的)。/proc提供了一個簡單的,無需定義的方式在用戶空間和內核之間傳遞數據,這種方式不只能夠知足內核使用,一樣也提供足夠的自由度給內核模塊作他們須要作的事情。
爲了知足咱們的要求,咱們須要當咱們讀在/proc下的某一個文件時將會返回一個「Hello world!」。咱們將使用/proc/hello_world這個文件。下載並解開hello proc這個gzip的tar包後,咱們將首先來看一下hello_proc.c這個文件
2.
#include <linux/module.h>
3.
#include <linux/proc_fs.h>
此次,咱們將增長一個proc_fs頭文件,這個頭文件包括驅動註冊到/proc文件系統的支持。當另一個進程調用read()時,下一個函數將會被調用。這個函數的實現比一個完整的普通內核驅動的read系統調用實現要簡單的多,由於咱們僅作了讓」Hello world」這個字符串緩存被一次讀完。
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文件傳輸數據的方法。這個函數方法體羅列以下:
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 子系統中。
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 val@nmt.edu"
);
08.
MODULE_DESCRIPTION(
""
Hello, world!
" minimal module"
);
09.
MODULE_VERSION(
"proc"
);
下面咱們將準備編譯和裝載模組
$
make
$
sudo
insmod ./hello_proc.ko
如今,將會有一個稱爲/proc/hello_world的文件,而且讀這個文件的,將會返回一個」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這個源文件。
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
02.
hello_init(
void
){
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.
08.
*/
09.
ret = misc_register(&hello_dev);
10.
if
(ret)
11.
printk(KERN_ERR
12.
"Unable to register "
Hello, world!
" misc devicen"
);
13.
return
ret;
14.
}
15.
module_init(hello_init);
接下是在卸載時的退出函數
01.
static
void
__exit
02.
hello_exit(
void
){
03.
misc_deregister(&hello_dev);
04.
}
05.
module_exit(hello_exit);
06.
MODULE_LICENSE(
"GPL"
);
07.
MODULE_AUTHOR(
"Valerie Henson val@nmt.edu>"
);
08.
MODULE_DESCRIPTION(
""
Hello, world!
" minimal module"
);
09.
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!
更多編寫udev規則的信息能夠在Daniel Drake的文章Writing udev rules中找到。