第四章
本教程修改自趙磊的網上的一系列教程.本人覺得該系列教程寫的非常不錯.以風趣幽默的語言將塊驅動寫的非常詳細,對於入門教程,應該屬於一份經典了. 本人在這對此係列教程最後附上對Linux 2.6.36版本的代碼.並編譯運行成功. 該教程所有版權仍歸作者趙磊所有,本人只做附錄代碼的添加,併爲對原文修改.有不懂的地方,可以聯繫我 [email protected] 或者給我留言.
+---------------------------------------------------+
| 寫一個塊設備驅動
+---------------------------------------------------+
| 作者:趙磊
| email: [email protected]
+---------------------------------------------------+
| 文章版權歸原作者所有。
| 大家可以自由轉載這篇文章,但原版權信息必須保留。
| 如需用於商業用途,請務必與原作者聯繫,若因未取得
| 授權而收起的版權爭議,由侵權者自行負責。
+---------------------------------------------------+
上一章結束時說過,本章會準備一些不需要動腦子的內容,現在我們開始履行諾言。
看上去簡單的事情實際上往往會被弄得很複雜,比如取消公僕們的招待費用問題;
看上去複雜的事情真正做起來也可能很簡單,比如本章中要讓我們的塊設備支持分區操作。
談到分區,不懂電腦的人想到了去找“專家”幫忙;電腦入門者想到了“高手”這個名詞;
漸入佳境者想到了fdisk;資深級玩家想到了dm;紅點玩家想到了隱藏的系統恢復區;
程序員想到了分區表;病毒製造者想到了把分區表清空......
作爲塊設備驅動程序的設計者,我們似乎需要想的比他們更多一些,
我們大概需要在驅動程序開始識別塊設備時訪問設備上的分區表,讀出裏面的數據進行分析,
找出這個塊設備中包含哪一類的分區(奇怪吧,但真相是分區表確實有很多種,只是我們經常遇到的大概只有ibm類型罷了)、
幾個分區,每個分區在塊設備上的區域等信息,再在驅動程序中對每個分區進行註冊、創建其管理信息......
讀到這裏,正在繫鞋帶準備溜之大吉的同學們請稍等片刻聽我說完,
雖然實際上作者也鼓勵同學們多作嘗試,甚至是這種無謂的嘗試,但本章中的做法卻比上述的內容簡單得多。
因爲這一回linux居然幫了我們的忙,並且不是I/O調度器的那種倒忙。
打開linux代碼,我們會在fs/partitions/目錄中發現一些文件,這些友好的文件將會默默無聞地幫我們的大忙。
而我們需要做的居然如此簡單,還記得alloc_disk()函數嗎?
我們一直用1作參數來調用它的,但現在,我們換成64,這意味着設定塊設備最大支持63個分區。
然後......不要問然後,因爲已經做完了。
當然,如果要讓代碼看起來漂亮一些的話,我們可以考慮用一個宏來定義最大分區數。
也就是,在文件的頭部增加:
/* usable partitions is SIMP_BLKDEV_MAXPARTITIONS - 1 */
#define SIMP_BLKDEV_MAXPARTITIONS (64)
然後把
simp_blkdev_disk = alloc_disk(1);
改成
simp_blkdev_disk = alloc_disk(SIMP_BLKDEV_MAXPARTITIONS);
好了,真的改好了。
上一章那樣改得太多看起來會讓讀者不爽,那麼這裏改得太少,是不是也同樣不爽?
大概有關部門深信老百姓接受不了有害物質含量過少的食品,因此制定了食品中三聚氰胺含量的標準。
於是,今後我們大概會制定出一系列標準,比如插入多深才能叫強姦什麼的。
爲了達到所謂的標準,我們破例補充介紹一下alloc_disk()函數:
這個函數的原型爲:
struct gendisk *alloc_disk(int minors);
用於申請一個gendisk結構,並做好一些初始化工作。
minors用於指定這個設備使用的次設備號數量,因爲第一個次設備號已經用於表示整個塊設備了,
因此餘下的minors-1個設備號用於表示塊設備中的分區,這就限制了這個塊設備中的最大可訪問分區數。
我們注意“最大可訪問分區數”這個詞:
“最大”雖然指的是上限,但並不意味這是唯一的上限。
極端情況下如果這個塊設備只有2個磁道,那麼無論minors多大,塊設備本身充其量也只能建立2個分區。
這時再談minors值能到達多少簡直就是扯淡,就像腐敗不根除,建多少經濟適用房都是白搭一樣。
“可訪問”指的是通過驅動程序可以訪問的分區數量,這是因爲我們只有那麼多次設備號。
但這個數字並不妨礙用戶在塊設備上面建多少個區。比如我們把minors設定爲4,那麼最大可訪問的分區數量是3,
足夠變態的用戶完全可以在塊設備上建立幾十個分區,只不過結果是隻能使用前3個分區而已。
現在我們可以試試這個程序了。
與以往相同的是,我們編譯和加載這個模塊:
# make
make -C /lib/modules/2.6.18-53.el5/build SUBDIRS=/root/test/simp_blkdev/simp_blkdev_step04 modules
make[1]: Entering directory `/usr/src/kernels/2.6.18-53.el5-i686'
CC [M] /root/test/simp_blkdev/simp_blkdev_step04/simp_blkdev.o
Building modules, stage 2.
MODPOST
CC /root/test/simp_blkdev/simp_blkdev_step04/simp_blkdev.mod.o
LD [M] /root/test/simp_blkdev/simp_blkdev_step04/simp_blkdev.ko
make[1]: Leaving directory `/usr/src/kernels/2.6.18-53.el5-i686'
# insmod simp_blkdev.ko
#
與以往不同的是,這一次加載完模塊後,我們並不直接在塊設備上創建文件系統,而是進行分區:
# fdisk /dev/simp_blkdev
Device contains neither a valid DOS partition table, nor Sun, SGI or OSF disklabel
Building a new DOS disklabel. Changes will remain in memory only,
until you decide to write them. After that, of course, the previous
content won't be recoverable.
Warning: invalid flag 0x0000 of partition table 4 will be corrected by w(rite)
Command (m for help):
關於fdisk我們不打算在這裏介紹,因爲我們試圖讓這篇文檔看起來專家一些。
使用n命令創建第一個主分區:
Command (m for help): n
Command action
e extended
p primary partition (1-4)
p
Partition number (1-4): 1
First cylinder (1-2, default 1): 1
Last cylinder or +size or +sizeM or +sizeK (1-2, default 2): 1
Command (m for help):
如果細心一些的話,在這裏可以看出一個小麻煩,就是:這塊磁盤一共只有2個磁道。
因此,我們只好指定第一個分區僅佔用1個磁道。畢竟,還要爲第2個分區留一些空間。
然後建立第二個分區:
Command (m for help): n
Command action
e extended
p primary partition (1-4)
p
Partition number (1-4): 2
First cylinder (2-2, default 2): 2
Command (m for help):
這一步中由於只剩下1個磁道,fdisk便不再問我們Last cylinder,而是自作主張地把最後一個磁道分配給新的分區。
這時我們的分區情況是:
Command (m for help): p
Disk /dev/simp_blkdev: 16 MB, 16777216 bytes
255 heads, 63 sectors/track, 2 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes
Device Boot Start End Blocks Id System
/dev/simp_blkdev1 1 1 8001 83 Linux
/dev/simp_blkdev2 2 2 8032+ 83 Linux
Command (m for help):
寫入分區,退出fdisk:
Command (m for help): w
The partition table has been altered!
Calling ioctl() to re-read partition table.
Syncing disks.
#
然後我們在這兩個分區中創建文件系統
# mkfs.ext3 /dev/simp_blkdev1
mke2fs 1.39 (29-May-2006)
Filesystem label=
OS type: Linux
Block size=1024 (log=0)
Fragment size=1024 (log=0)
2000 inodes, 8000 blocks
400 blocks (5.00%) reserved for the super user
First data block=1
Maximum filesystem blocks=8388608
1 block group
8192 blocks per group, 8192 fragments per group
2000 inodes per group
Writing inode tables: done
Creating journal (1024 blocks): done
Writing superblocks and filesystem accounting information: done
This filesystem will be automatically checked every 27 mounts or
180 days, whichever comes first. Use tune2fs -c or -i to override.
# mkfs.ext3 /dev/simp_blkdev2
mke2fs 1.39 (29-May-2006)
Filesystem label=
OS type: Linux
Block size=1024 (log=0)
Fragment size=1024 (log=0)
2008 inodes, 8032 blocks
401 blocks (4.99%) reserved for the super user
First data block=1
Maximum filesystem blocks=8388608
1 block group
8192 blocks per group, 8192 fragments per group
2008 inodes per group
Writing inode tables: done
Creating journal (1024 blocks): done
Writing superblocks and filesystem accounting information: done
This filesystem will be automatically checked every 23 mounts or
180 days, whichever comes first. Use tune2fs -c or -i to override.
#
然後mount設兩個設備:
# mount /dev/simp_blkdev1 /mnt/temp1
# mount /dev/simp_blkdev2 /mnt/temp2
#
看看結果:
# mount
/dev/hda1 on / type ext3 (rw)
proc on /proc type proc (rw)
sysfs on /sys type sysfs (rw)
devpts on /dev/pts type devpts (rw,gid=5,mode=620)
tmpfs on /dev/shm type tmpfs (rw)
none on /proc/sys/fs/binfmt_misc type binfmt_misc (rw)
/dev/simp_blkdev1 on /mnt/temp1 type ext3 (rw)
/dev/simp_blkdev2 on /mnt/temp2 type ext3 (rw)
#
然後讀/寫:
# cp /etc/init.d/* /mnt/temp1/
# cp /etc/passwd /mnt/temp2
# ls /mnt/temp1/
NetworkManager avahi-dnsconfd dund ipmi lost+found netfs portmap rpcsvcgssd vncserver
NetworkManagerDispatcher bluetooth firstboot iptables lvm2-monitor netplugd psacct saslauthd winbind
acpid capi functions irda mcstrans network rdisc sendmail wpa_supplicant
anacron conman
# ls /mnt/temp2
lost+found passwd
#
收尾工作:
# umount /dev/temp1
# umount /dev/temp2
# rmmod simp_blkdev
#
看起來本章應該結束了,但爲了耽誤大家更多的時間,我們來回憶一下剛纔出現的小麻煩。
我們發現這塊磁盤只有2個磁道,由於分區是以磁道爲邊界的,因此最大隻能創建2個分區。
不過謝天謝地,好歹我們能夠證明我們的程序是支持“多個”分區的......儘管只有2個。
那麼爲什麼系統會認爲我們的塊設備只有2個磁道呢?其實這不怪系統,因爲我們根本沒有告訴系統我們的磁盤究竟有多少個磁道。
因此係統只好去猜、猜、猜,結果就猜成2個磁道了。
好吧,說的細節一些,傳統的磁盤使用8個位表示盤面數、6個位表示每磁道扇區數、10個位表示磁道數,因此盤面、每磁道扇區、磁道的最大數值分別爲255、63和1023。
這也是傳說中啓動操作系統時的1024柱面(磁道)和硬盤容量8G限制的根源。
現代磁盤採用線性尋址方式突破了這一限制,從本質上說,如果你的機器還沒生鏽,那麼你的硬盤無論是內部結構還是訪問方式都與常識中的盤面、每磁道扇區、磁道無關。
但爲了與原先的理解兼容,對於現代磁盤,我們在訪問時還是假設它具有傳統的結構。目前比較通用的假設是:所有磁盤具有最大數目的(也就是恆定的)盤面和每磁道扇區數,而磁盤大小與磁道數與成正比。
因此,對於一塊80G的硬盤,根據假設,這塊磁盤的盤面和每磁道扇區數肯定是255和63,磁道數爲:80*1024*1024*1024/512(字節每扇區)/255(盤面數)/63(每磁道扇區數)=10043(小數部分看作不完整的磁道被丟棄)。
話歸原題,在驅動程序中我們指定了磁盤大小爲16M,共包含16*1024*1024/512=32768個扇區。假設這塊磁盤具有最大盤面和每磁道扇區數後,它的磁道數就是:32768/255/63=2。
我們看起開應該很happy,因爲系統太看得起我們了,竟然把我們的塊設備看成現代磁盤進行磁道數的換算處理。
不過我們也可能unhappy,因爲這造成磁盤最大隻能被分成2個區。(至於爲什麼分區以磁道作爲邊界,可以想象一下磁盤的結構)
但我們的磁盤只有區區16M啊,所以最好還是告訴系統我們的磁盤沒有那麼多的盤面數和每磁道扇區數,這將讓磁道數來得多一些。
在下一章中,我們打算搞定這個問題。
<未完,待續>
#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 BLKDEV_DEVICEMAJOR COMPAQ_SMART2_MAJOR
#define BLKDEV_BYTES (16*1024*1024)
#define MAX_PARTITIONS 64
static int MAJOR_NR = 0;
static struct gendisk *g_blkdev_disk;
static struct request_queue *g_blkdev_queue;
unsigned char blkdev_data[BLKDEV_BYTES];
static int blkdev_make_request(struct request_queue *q, struct bio *bio)
{
struct bio_vec *bvec;
int i;
void *dsk_mem;
if ((bio->bi_sector << 9) + bio->bi_size > BLKDEV_BYTES) {
printk(KERN_ERR BLK_DISK_NAME
": bad request: block=%llu, count=%u\n",
(unsigned long long)bio->bi_sector, bio->bi_size);
bio_endio(bio, -EIO);
return 0;
}
dsk_mem = blkdev_data + (bio->bi_sector << 9);
bio_for_each_segment(bvec, bio, i) {
void *iovec_mem;
switch (bio_rw(bio)) {
case READ:
case READA:
iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
memcpy(iovec_mem, dsk_mem, bvec->bv_len);
kunmap(bvec->bv_page);
break;
case WRITE:
iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
memcpy(dsk_mem, iovec_mem, bvec->bv_len);
kunmap(bvec->bv_page);
break;
default:
printk(KERN_ERR BLK_DISK_NAME
": unknown value of bio_rw: %lu\n",
bio_rw(bio));
bio_endio(bio, -EIO);
return 0;
}
dsk_mem += bvec->bv_len;
}
bio_endio(bio, 0);
return 0;
}
/*
struct hd_geometry {
unsigned char heads; //磁頭數255 //有幾個磁頭,也叫盤面數
unsigned char sectors; //扇區數64 //一個柱面有幾個扇區
unsigned short cylinders; //柱面 //有多少個磁道的數量
unsigned long start;
};
*/
int gendisk_getgeo(struct block_device *pblk_dev, struct hd_geometry *phd_geo)
{
/*
* capacity heads sectors cylinders
* 0~16M 1 1 0~32768
* 16M~512M 1 32 1024~32768
* 512M~16G 32 32 1024~32768
* 16G~... 255 63 2088~...
*/
if (BLKDEV_BYTES < 16 * 1024 * 1024) {
phd_geo->heads = 1;
phd_geo->sectors = 1;
} else if (BLKDEV_BYTES < 512 * 1024 * 1024) {
phd_geo->heads = 1;
phd_geo->sectors = 32;
} else if (BLKDEV_BYTES < 16ULL * 1024 * 1024 * 1024) {
phd_geo->heads = 32;
phd_geo->sectors = 32;
} else {
phd_geo->heads = 255;
phd_geo->sectors = 63;
}
phd_geo->cylinders = BLKDEV_BYTES >> 9 / phd_geo->heads / phd_geo->sectors;
return 0;
}
struct block_device_operations fop = {
.owner = THIS_MODULE,
.getgeo = gendisk_getgeo,
};
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_alloc_queue\n");
g_blkdev_queue = blk_alloc_queue(GFP_KERNEL);
if(NULL == g_blkdev_queue){
ret = -ENOMEM;
goto err_alloc_queue;
}
blk_queue_make_request(g_blkdev_queue, blkdev_make_request);
printk(KERN_WARNING "alloc_disk\n");
// g_blkdev_disk = alloc_disk(1);
g_blkdev_disk = alloc_disk(MAX_PARTITIONS);
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, 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_alloc_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);
}
//註冊模塊加載卸載函數
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"); //別名
本人是在參考教程之後修改的教程內容.如有不同.可能有遺漏沒有修改.造成對讀者的迷惑,在此致歉~~