第一章
本教程修改自趙磊的網上的一系列教程.本人覺得該系列教程寫的非常不錯.以風趣幽默的語言將塊驅動寫的非常詳細,對於入門教程,應該屬於一份經典了. 本人在這對此係列教程對細微的修改,僅針對Linux 2.6.36版本.並編譯運行成功. 該教程所有版權仍歸作者趙磊所有,本人只做適當修改.
第1章
+---------------------------------------------------+
| 寫一個塊設備驅動
+---------------------------------------------------+
| 作者:趙磊
| 網名:OstrichFly、飛翔的鴕鳥
| email: [email protected]
+---------------------------------------------------+
| 文章版權歸原作者所有。
| 大家可以自由轉載這篇文章,但原版權信息必須保留。
| 如需用於商業用途,請務必與原作者聯繫,若因未取得
| 授權而收起的版權爭議,由侵權者自行負責。
+---------------------------------------------------+
同樣是讀書,讀小說可以行雲流水,讀完後心情舒暢,意猶未盡;讀電腦書卻舉步艱難,讀完後目光呆滯,也是意猶未盡,只不過未盡的是痛苦的回憶。
研究證明,痛苦的記憶比快樂的更難忘記,因此電腦書中的內容比小說記得持久。
而這套教程的目的是要打破這種狀況,以至於讀者在忘記小說內容忘記本文。
在這套教程中,我們通過寫一個建立在內存中的塊設備驅動,來學習linux內核和相關設備驅動知識。
選擇寫塊設備驅動的原因是:
1:容易上手
2:可以牽連出更多的內核知識
3:像本文這樣的塊設備驅動教程不多,所以需要一個
好吧,扯淡到此結束,我們開始寫了。
本章的目的用盡可能最簡單的方法寫出一個能用的塊設備驅動。
所謂的能用,是指我們可以對這個驅動生成的塊設備進行mkfs,mount和讀寫文件。
爲了儘可能簡單,這個驅動的規模不是1000行,也不是500行,而是100行以內。
這裏插一句,我們不打算在這裏介紹如何寫模塊,理由是介紹的文章已經滿天飛舞了。
如果你能看得懂、並且成功地編譯、運行了這段代碼,我們認爲你已經達到了本教程的入學資格,
當然,如果你不幸的卡在這段代碼中,那麼請等到搞定它以後再往下看:
mod.c:
static int __init initialization_function(void)
{
return 0;
}
static void __exit cleanup_function(void)
{
}
//註冊模塊加載卸載函數
module_init(initialization_function); //指定模塊加載函數
module_exit(cleanup_function); //指定模塊卸載函數
//模塊信息及許可證
MODULE_AUTHOR("LvApp"); //作者
MODULE_LICENSE("Dual BSD/GPL"); //許可證
MODULE_DESCRIPTION("A simple block module"); //描述
MODULE_ALIAS("block"); //別名
Makefile:
#your use kernel_path
KERNEL_PATH = "/LvApp/linux-2.6.36.2-v1.05"
#kernel modules
obj-m += 文件名
#Specify flags for the module compilation.
#EXTRA_CFLAGS=-g -O0
buile:kernel_modules
kernel_modules:
make -C $(KERNEL_PATH) M=$(shell pwd) modules
clean:
make -C $(KERNEL_PATH) M=$(shell pwd) clean
好了,這裏我們假定你已經搞定上面的最簡單的模塊了,懂得什麼是看模塊,以及簡單模塊的編寫、編譯、加載和卸載。
還有就是,什麼是塊設備,什麼是塊設備驅動,這個也請自行google吧,因爲我們已經迫不及待要寫完程序下課。
爲了建立一個可用的塊設備,我們需要做......1件事情:
1:用add_disk()函數向系統中添加這個塊設備
添加一個全局的
static struct gendisk *g_blkdev_disk;
然後申明模塊的入口和出口:
module_init(initialization_function); //指定模塊加載函數
module_exit(cleanup_function); //指定模塊卸載函數
然後在入口處添加這個設備、出口處私房這個設備:
static int __init initialization_function(void)
{
add_disk(g_blkdev_disk);
return 0;
}
static void __exit cleanup_function(void)
{
del_gendisk(g_blkdev_disk); //->add_disk
}
當然,在添加設備之前我們需要申請這個設備的資源,這用到了alloc_disk()函數,因此模塊入口函數initialization_function(void)應該是:
static int __init initialization_function(void)
{
g_blkdev_disk = alloc_disk(1);
if(NULL == g_blkdev_disk){
ret = -ENOMEM;
goto err_alloc_disk;
}
add_disk(g_blkdev_disk);
return 0;
err_alloc_disk:
return ret;
}
還有別忘了在卸載模塊的代碼中也加一個行清理函數:
put_disk(g_blkdev_disk);
還有就是,設備有關的屬性也是需要設置的,因此在alloc_disk()和add_disk()之間我們需要:
strcpy(g_blkdev_disk->disk_name, BLK_DISK_NAME);
g_blkdev_disk->major = ?1;
g_blkdev_disk->first_minor = 0;
g_blkdev_disk->fops = ?2;
g_blkdev_disk->queue = ?3;
set_capacity(g_blkdev_disk, ?4);
BLK_DISK_NAME其實是這個塊設備的名稱,爲了紳士一些,我們把它定義成宏了:
#define BLK_DISK_NAME "block_name"
這裏又引出了4個問號。(天哪,是不是有種受騙的感覺,像是陪老婆去做頭髮)
第1個問號:
每個設備需要對應的主、從驅動號。
我們的設備當然也需要,但很明顯我不是腦科醫生,因此跟寫linux的那幫瘋子不熟,得不到預先爲我保留的設備號。
還有一種方法是使用動態分配的設備號,但在這一章中我們希望儘可能做得簡單,因此也不採用這種方法。(在最後的代碼中,採用了動態分配.詳細請參閱代碼)
那麼我們採用的是:搶別人的設備號。
我們手頭沒有AK47,因此不敢幹的太轟轟烈烈,而偷偷摸摸的事情倒是可以考慮的。
柿子要撿軟的捏,而我們試圖找出一個不怎麼用得上的設備,然後搶他的ID。
打開linux/include/linux/major.h,把所有的設備一個個看下來,我們覺得最勝任被搶設備號的傢伙非COMPAQ_SMART2_XXX莫屬。
第一因爲它不強勢,基本不會被用到,因此也不會造成衝突;第二因爲它有錢,從COMPAQ_SMART2_MAJOR到COMPAQ_SMART2_MAJOR7有那8個之多的設備號可以被搶,不過癮的話還有它妹妹:COMPAQ_CISS_MAJOR~COMPAQ_CISS_MAJOR7。
爲了讓搶劫顯得紳士一些,我們在外面又定義一個宏:
#define SIMP_BLKDEV_DEVICEMAJOR COMPAQ_SMART2_MAJOR
然後在?1的位置填上SIMP_BLKDEV_DEVICEMAJOR。
第2個問號:
gendisk結構需要設置fops指針,雖然我們用不到,但該設還是要設的。
好吧,就設個空得給它:
在全局部分添加:
struct block_device_operations fop = {
.owner = THIS_MODULE,
};
然後把?2的位置填上&fop。
第3個問號:
這個比較麻煩一些。
首先介紹請求隊列的概念。對大多數塊設備來說,系統會把對塊設備的訪問需求用bio和bio_vec表示,然後提交給通用塊層。
通用塊層爲了減少塊設備在尋道時損失的時間,使用I/O調度器對這些訪問需求進行排序,以儘可能提高塊設備效率。
關於I/O調度器在本章中不打算進行深入的講解,但我們必須知道的是:
1:I/O調度器把排序後的訪問需求通過request_queue結構傳遞給塊設備驅動程序處理
2:我們的驅動程序需要設置一個request_queue結構
申請request_queue結構的函數是blk_init_queue(),而調用blk_init_queue()函數時需要傳入一個函數的地址,這個函數擔負着處理對塊設備數據的請求。
因此我們需要做的就是:
1:實現一個static void blkdev_do_request(struct request_queue *q)函數。
2:加入一個全局變量,指向塊設備需要的請求隊列:
static struct request_queue *g_blkdev_queue;
3:在加載模塊時用blkdev_do_request函數的地址作參數調用blk_init_queue()初始化一個請求隊列:
g_blkdev_queue = blk_init_queue(blkdev_do_request,NULL);
if(NULL == g_blkdev_queue){
ret = -ENOMEM;
goto err_init_queue;
}
4:卸載模塊時把simp_blkdev_queue還回去:
blk_cleanup_queue(g_blkdev_queue);
5:在?3的位置填上g_blkdev_queue。
第4個問號:
這個還好,比前面的簡單多了,這裏需要設置塊設備的大小。
塊設備的大小使用扇區作爲單位設置,而扇區的大小默認是512字節。
當然,在把字節爲單位的大小轉換爲以扇區爲單位時,我們需要除以512,或者右移9位可能更快一些。
同樣,我們試圖把這一步也做得紳士一些,因此使用宏定義了塊設備的大小,目前我們定爲16M:
#define SIMP_BLKDEV_BYTES (16*1024*1024)
然後在?4的位置填上SIMP_BLKDEV_BYTES>>9。
看到這裏,是不是有種身陷茫茫大海的無助感?並且一波未平,一波又起,在搞定這4個問號的同時,居然又引入了blkdev_do_request函數!
當然,如果在身陷茫茫波濤中時你認爲到處都是海,因此絕望,那麼恭喜你可以不必捱到65歲再退休;
反之,如果你認爲到處都是沒有三聚氰胺鮮魚,並且隨便哪個方向都是岸時,那麼也恭喜你,你可以活着回來繼續享受身爲納稅人的榮譽。
爲了理清思路,我們把目前爲止涉及到的代碼整理出來:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/genhd.h> //add_disk
#include <linux/blkdev.h> //struct block_device_operations
#define _DEBUG_
#define BLK_DISK_NAME "block_name"
#define SIMP_BLKDEV_DEVICEMAJOR COMPAQ_SMART2_MAJOR
#define SIMP_BLKDEV_BYTES (16*1024*1024)
static struct gendisk *g_blkdev_disk;
static struct request_queue *g_blkdev_queue;
struct block_device_operations fop = {
.owner = THIS_MODULE,
};
static void blkdev_do_request(struct request_queue *q)
{
}
static int __init initialization_function(void)
{
int ret = 0;
printk(KERN_WARNING "blk_init_queue\n");
g_blkdev_queue = blk_init_queue(blkdev_do_request,NULL);
if(NULL == g_blkdev_queue){
ret = -ENOMEM;
goto err_init_queue;
}
printk(KERN_WARNING "alloc_disk\n");
g_blkdev_disk = alloc_disk(1);
if(NULL == g_blkdev_disk){
ret = -ENOMEM;
goto err_alloc_disk;
}
strcpy(g_blkdev_disk->disk_name,BLK_DISK_NAME);
g_blkdev_disk->major = SIMP_BLKDEV_DEVICEMAJOR;
g_blkdev_disk->first_minor = 0;
g_blkdev_disk->fops = &fop;
g_blkdev_disk->queue = g_blkdev_queue;
add_disk(g_blkdev_disk);
#ifdef _DEBUG_
printk(KERN_WARNING "ok\n");
#endif
return ret;
err_alloc_disk:
blk_cleanup_queue(g_blkdev_queue);
return ret;
}
static void __exit cleanup_function(void)
{
del_gendisk(g_blkdev_disk); //->add_disk
put_disk(g_blkdev_disk); //->alloc_disk
blk_cleanup_queue(g_blkdev_queue); //->blk_init_queue
}
//註冊模塊加載卸載函數
module_init(initialization_function); //指定模塊加載函數
module_exit(cleanup_function); //指定模塊卸載函數
//模塊信息及許可證
MODULE_AUTHOR("LvApp"); //作者
MODULE_LICENSE("Dual BSD/GPL"); //許可證
MODULE_DESCRIPTION("A simple block module"); //描述
MODULE_ALIAS("block"); //別名
剩下部分的不多了,真的不多了。請相信我,因爲我不在質監局上班。
我寫的文章誠實可靠,並且不拿你納稅的錢。
我們還有一個最重要的函數需要實現,就是負責處理塊設備請求的blkdev_do_requestuest()。
首先我們看看究竟把塊設備的數據以什麼方式放在內存中。
畢竟這是在第1章,因此我們將使用最simple的方式實現,也就是,數組。
我們在全局代碼中定義:
unsigned char blkdev_data[SIMP_BLKDEV_BYTES];
對驅動程序來說,這個數組看起來大了一些,如果不幸被懂行的人看到,將100%遭到最無情、最嚴重的鄙視。
而我們卻從極少數公僕那裏學到了最有效的應對之策,那就是:無視他,然後把他定爲成“不明真相的羣衆”。
然後我們着手實現blkdev_do_requestuest。
這裏介紹blk_fetch_request()函數,原型是:
struct request *blk_fetch_request(struct request_queue *q)
用來從一個請求隊列中拿出一條請求(其實嚴格來說,拿出的可能是請求中的一段)。
隨後的處理請求本質上是根據rq_data_dir(req)返回的該請求的方向(讀/寫),把塊設備中的數據裝入req->buffer、或是把req->buffer中的數據寫入塊設備。
剛纔已經提及了與request結構相關的rq_data_dir()宏和.buffer成員,其他幾個相關的結構成員和函數是:
blk_rq_pos(req):請求的開始磁道
blk_rq_cur_sectors(req):請求磁道數
__blk_end_request_cur(req, err):結束一個請求,第2個參數表示請求處理結果,成功時設定爲1,失敗時設置爲0或者錯誤號。
因此我們的blkdev_do_requestuest()函數爲:
static void blkdev_do_request(struct request_queue *q)
{
struct request *req;
req = blk_fetch_request(q);
while ( NULL != req ) {
int err = 0;
if ((blk_rq_pos(req) + blk_rq_cur_sectors(req)) << 9
> SIMP_BLKDEV_BYTES) {
printk(KERN_ERR BLK_DISK_NAME
": bad request: block=%llu, count=%u\n",
(unsigned long long)blk_rq_pos(req),
blk_rq_cur_sectors(req));
err = -EIO;
goto done;
}
switch (rq_data_dir(req)) {
case READ:
memcpy(req->buffer,
blkdev_data + (blk_rq_pos(req) << 9),
blk_rq_cur_sectors(req) << 9);
break;
case WRITE:
memcpy(blkdev_data + (blk_rq_pos(req) << 9),
req->buffer,
blk_rq_cur_sectors(req) << 9);
break;
default:
break;
}
done:
if(!__blk_end_request_cur(req, err))
req = blk_fetch_request(q);
}
}
函數使用blk_fetch_request(q)遍歷struct request_queue *q中使用struct request *req表示的每一段,首先判斷這個請求是否超過了我們的塊設備的最大容量,
然後根據請求的方向rq_data_dir(req)進行相應的請求處理。由於我們使用的是指簡單的數組,因此請求處理僅僅是2條memcpy。
memcpy中也牽涉到了扇區號到線性地址的轉換操作,我想對堅持到這裏的讀者來說,這個操作應該不需要進一步解釋了。
編碼到此結束,然後我們試試這個程序:
首先編譯:
# make
make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step1 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'
CC [M] /root/test/simp_blkdev/simp_blkdev_step1/simp_blkdev.o
Building modules, stage 2.
MODPOST
CC /root/test/simp_blkdev/simp_blkdev_step1/simp_blkdev.mod.o
LD [M] /root/test/simp_blkdev/simp_blkdev_step1/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'
#
加載模塊
# insmod simp_blkdev.ko
#
用lsmod看看。
這裏我們注意到,該模塊的Used by爲0,因爲它既沒有被其他模塊使用,也沒有被mount。
# lsmod
Module Size Used by
simp_blkdev 16784008 0
...
#
如果當前系統支持udev,在調用add_disk()函數時即插即用機制會自動爲我們在/dev/目錄下建立設備文件。
設備文件的名稱爲我們在gendisk.disk_name中設置的simp_blkdev,主、從設備號也是我們在程序中設定的72和0。
如果當前系統不支持udev,那麼很不幸,你需要自己用mknod /dev/simp_blkdev b 72 0來創建設備文件了。
# ls -l /dev/simp_blkdev
brw-r----- 1 root disk 72, 0 11-10 18:13 /dev/simp_blkdev
#
在塊設備中創建文件系統,這裏我們創建常用的ext3。
當然,作爲通用的塊設備,創建其他類型的文件系統也沒問題。
# mkfs.ext3 /dev/simp_blkdev
mke2fs 1.39 (29-May-2006)
Filesystem label=
OS type: Linux
Block size=1024 (log=0)
Fragment size=1024 (log=0)
4096 inodes, 16384 blocks
819 blocks (5.00%) reserved for the super user
First data block=1
Maximum filesystem blocks=16777216
2 block groups
8192 blocks per group, 8192 fragments per group
2048 inodes per group
Superblock backups stored on blocks:
8193
Writing inode tables: done
Creating journal (1024 blocks): done
Writing superblocks and filesystem accounting information: done
This filesystem will be automatically checked every 38 mounts or
180 days, whichever comes first. Use tune2fs -c or -i to override.
#
如果這是第一次使用,建議創建一個目錄用來mount這個設備中的文件系統。
當然,這不是必需的。如果你對mount之類的用法很熟,你完全能夠自己決定在這裏幹什麼,甚至把這個設備mount成root。
# mkdir -p /mnt/temp1
#
把建立好文件系統的塊設備mount到剛纔建立的目錄中
# mount /dev/simp_blkdev /mnt/temp1
#
看看現在的mount表
# mount
...
/dev/simp_blkdev on /mnt/temp1 type ext3 (rw)
#
看看現在的模塊引用計數,從剛纔的0變成1了,
原因是我們mount了。
# lsmod
Module Size Used by
simp_blkdev 16784008 1
...
#
看看文件系統的內容,有個mkfs時自動建立的lost+found目錄。
# ls /mnt/temp1
lost+found
#
隨便拷點東西進去
# cp /etc/init.d/* /mnt/temp1
#
再看看
# ls /mnt/temp1
acpid conman functions irqbalance mdmpd NetworkManagerDispatcher rdisc sendmail winbind
anacron cpuspeed gpm kdump messagebus nfs readahead_early setroubleshoot wpa_supplicant
bluetooth frecord irda mdmonitor NetworkManager psacct saslauthd vncserver
#
現在這個塊設備的使用情況是
# df
文件系統 1K-塊 已用 可用 已用% 掛載點
...
/dev/simp_blkdev 15863 1440 13604 10% /mnt/temp1
#
再全刪了玩玩
# rm -rf /mnt/temp1/*
#
看看刪完了沒有
# ls /mnt/temp1
#
好了,大概玩夠了,我們把文件系統umount掉
# umount /mnt/temp1
#
模塊的引用計數應該還原成0了吧
# lsmod
Module Size Used by
simp_blkdev 16784008 0
...
#
最後一步,移除模塊
# rmmod simp_blkdev
#
這是這部教程的第1章,不好意思的是,內容比預期還是難了一些。
當初還有一種考慮是在本章中僅僅實現一個寫了就丟的塊設備驅動,也就是說,對這個塊設備的操作只能到mkfs這一部,而不能繼續mount,因爲剛纔寫的數據全被扔了。
或者更簡單些,僅僅寫一個hello world的模塊。
但最後還是寫成了現在這樣沒,因爲我覺得拿出一個真正可用的塊設備驅動程序對讀者來說更有成就感。
無論如何,本章是一個開始,而你,已經跨入了學習塊設備驅動教室的大門,或者通俗來說,上了賊船。
而在後續的章節中,我們將陸續完善對這個程序,通過追加或者強化這個程序,來學習與塊設備有關、或與塊設備無關但與linux有關的方方面面。
總之,我希望通過這部教程,起碼讓讀者學到有用的知識,或者更進一步,引導讀者對linux的興趣,甚至領悟學習一切科學所需要的鑽研精神。
作爲第一章的結尾,引用我在另一篇文章中的序言:
謹以此文向讀者示範什麼叫做嚴謹的研究。
呼喚踏實的治學態度,反對浮躁的論壇風氣。
--OstrichFly
<未完,待續>
附上完整代碼:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/genhd.h> //add_disk
#include <linux/blkdev.h> //struct block_device_operations
#define _DEBUG_
#define BLK_DISK_NAME "block_name"
#define SIMP_BLKDEV_DEVICEMAJOR COMPAQ_SMART2_MAJOR
#define SIMP_BLKDEV_BYTES (16*1024*1024)
static int MAJOR_NR = 0;
static struct gendisk *g_blkdev_disk;
static struct request_queue *g_blkdev_queue;
unsigned char blkdev_data[SIMP_BLKDEV_BYTES];
struct block_device_operations fop = {
.owner = THIS_MODULE,
};
static void blkdev_do_request(struct request_queue *q)
{
struct request *req;
req = blk_fetch_request(q);
while ( NULL != req ) {
int err = 0;
if ((blk_rq_pos(req) + blk_rq_cur_sectors(req)) << 9
> SIMP_BLKDEV_BYTES) {
printk(KERN_ERR BLK_DISK_NAME
": bad request: block=%llu, count=%u\n",
(unsigned long long)blk_rq_pos(req),
blk_rq_cur_sectors(req));
err = -EIO;
goto done;
}
switch (rq_data_dir(req)) {
case READ:
memcpy(req->buffer,
blkdev_data + (blk_rq_pos(req) << 9),
blk_rq_cur_sectors(req) << 9);
break;
case WRITE:
memcpy(blkdev_data + (blk_rq_pos(req) << 9),
req->buffer,
blk_rq_cur_sectors(req) << 9);
break;
default:
break;
}
done:
if(!__blk_end_request_cur(req, err))
req = blk_fetch_request(q);
}
}
static int __init initialization_function(void)
{
int ret = 0;
printk(KERN_WARNING "register_blkdev\n");
MAJOR_NR = register_blkdev(0, BLK_DISK_NAME);
if(MAJOR_NR < 0)
{
return -1;
}
printk(KERN_WARNING "blk_init_queue\n");
g_blkdev_queue = blk_init_queue(blkdev_do_request,NULL);
if(NULL == g_blkdev_queue){
ret = -ENOMEM;
goto err_init_queue;
}
printk(KERN_WARNING "alloc_disk\n");
g_blkdev_disk = alloc_disk(1);
if(NULL == g_blkdev_disk){
ret = -ENOMEM;
goto err_alloc_disk;
}
strcpy(g_blkdev_disk->disk_name,BLK_DISK_NAME);
g_blkdev_disk->major = MAJOR_NR;
g_blkdev_disk->first_minor = 0;
g_blkdev_disk->fops = &fop;
g_blkdev_disk->queue = g_blkdev_queue;
set_capacity(g_blkdev_disk, SIMP_BLKDEV_BYTES>>9);
add_disk(g_blkdev_disk);
#ifdef _DEBUG_
printk(KERN_WARNING "ok\n");
#endif
return ret;
err_alloc_disk:
blk_cleanup_queue(g_blkdev_queue);
err_init_queue:
unregister_blkdev(MAJOR_NR, BLK_DISK_NAME);
return ret;
}
static void __exit cleanup_function(void)
{
del_gendisk(g_blkdev_disk); //->add_disk
put_disk(g_blkdev_disk); //->alloc_disk
blk_cleanup_queue(g_blkdev_queue); //->blk_init_queue
unregister_blkdev(MAJOR_NR, BLK_DISK_NAME); //->register_blkdev
}
//註冊模塊加載卸載函數
module_init(initialization_function); //指定模塊加載函數
module_exit(cleanup_function); //指定模塊卸載函數
//模塊信息及許可證
MODULE_AUTHOR("LvApp"); //作者
MODULE_LICENSE("Dual BSD/GPL"); //許可證
MODULE_DESCRIPTION("A simple block module"); //描述
MODULE_ALIAS("block"); //別名
本人是在參考教程之後修改的教程內容.如有不同.可能有遺漏沒有修改.造成對讀者的迷惑,在此致歉~~