第 15章 (最終章)
+---------------------------------------------------+
| 寫一個塊設備驅動 |
+---------------------------------------------------+
| 作者:趙磊 |
| email: [email protected] |
+---------------------------------------------------+
| 文章版權歸原作者所有。 |
| 大家可以自由轉載這篇文章,但原版權信息必須保留。 |
| 如需用於商業用途,請務必與原作者聯繫,若因未取得 |
| 授權而收起的版權爭議,由侵權者自行負責。 |
+---------------------------------------------------+
在上一章中我們對這個塊設備驅動所作的更改使它具備了動態申請內存的能力,
但實際上同時也埋下一個隱患,就是數據訪問衝突
這裏我們順便嘮叨一下內核開發中的同步問題
提到數據訪問同步,自然而然會使人想到多進程、多線程、加鎖、解鎖、
----------------------- Page 106-----------------------
信號量、synchronized關鍵字等東西,然後就很頭疼
對於用戶態程序,網上大量的解釋數據同步概念和方法的文章給人的印象大概是:
同步很危險,編程要謹慎,
處處有機關,問題很難找
對於第一次進行多線程時編程的人來說,感覺可能是以下兩種:
一種是覺得程序中處處都會有問題,任何一條訪問數據的指令都不安全,
恨不得把程序中所有的數據都加上鎖,甚至打算給鎖本身的數據再加個鎖,
另一種是沒覺得有什麼困難,根本不去理什麼都互斥不互斥,
就按原先的來,編出的程序居然也運行得很順
然後懷着這兩種想法人通過不斷的學習和實踐掌握了數據同步的知識後認識到,
數據同步其實並不像前一種想法那樣危險,也不像後一種想法那樣簡單
所幸的是對於不少用戶態程序來說,倒是可以不用考慮數據同步問題
至少當我們剛開始寫 HelloWorld時不用去理這個麻煩
而對於內核態代碼而言,很不幸,整個兒幾乎都相當於用戶態的多線程
其實事情也並非原本就是這麼糟的
在很久很久以前,山是青的,草是綠的,牛奶是能喝的,
見到老人摔跤是敢扶的,作者是純情的,電腦也是單 CPU的
那時的內核環境很靜,很美 除了中斷會時不時地搗搗亂,其餘的都挺詩意
代碼獨個兒在跑,就像是一輛汽車在荒漠上奔馳,因爲沒有其他妨礙,
幾乎可以毫無顧忌地訪問數據,而不用考慮什麼萬惡的訪問衝突
唯一要考慮的從天而降的中斷奧特曼,解決的方法倒也不難,禁用了中斷看你還能咋的
然後隨着作者的成長,目光從書本轉向了美眉,計算機也由單 CPU發展成了多 CPU
內核代碼的執行環境終於開始熱鬧起來,由於每個 CPU上都在執行任務,
這些任務進入到對應的內核態時會出現多條內核指令流同時執行,
這些指令流對全局數據的訪問很明顯就牽涉到了同步問題,這是開端
從那時起編程時要考慮其他CPU上的事情了
然後隨着作者的進一步成長,目光從美眉的臉轉向了胸,
CPU製造商爲了貫徹給程序員找麻煩的精神,搞出了亂序執行
這一創舉驚醒了多年來還在夢中的諸多程序員,原來,程序不是按程序執行的啊
正如林高官說的:“我是交通部派來的,級別和你們市長一樣高,敢跟我鬥,
你們這些人算個屁呀!”原來,無職無權的平民百姓就是屁啊
正當程序員從睡夢中驚醒還沒緩過神時,編譯器又跟着搗亂,
“你 CPU都能亂序了,憑什麼不讓我亂序 ?”
然後熱鬧了,好在我們還有mb()、rmb()、wmb()、barrier()這幾根救命稻草,
事情倒是沒變得太糟
----------------------- Page 107-----------------------
然後隨着作者的進一步成長,目光從美眉的胸轉向了臀,
內核也從一開始時被動的爲了適應多 CPU而不得已半推半就支持多任務並行,
轉向了主動掀起裙角管它一個還是幾個 CPU都去多任務了
從技術面解釋,這就是大名鼎鼎的內核搶佔
內核的程序員從此不僅要考慮其他CPU ,好要提妨自個兒的 CPU ,
因爲執行代碼的 CPU說不定什麼時候就莫名其妙的被調度執行別的任務了
如果以作者的成長曆程爲主線解釋內核的演化還不至於太混亂的話,
我們還可以考慮再介紹一下 spin_lock, mutex_lock, preempt_disable,
atomic_t和 rcu等函數,不過作者忍住了這一衝動,還是讓讀者去google吧
然後回到我們的代碼,現在的代碼是有問題的
比如 simp_blkdev_trans()函數中,假設 2個任務同時向塊設備的同一區域寫數據,
而這塊區域在這之前沒有被寫過,也就是說還沒有申請內存,那麼如果運氣夠好的話,
這兩個進程可能幾乎同時運行到:
this_first_page = radix_tree_lookup(&simp_blkdev_data,
(dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT);
這句,很明顯這兩個任務得到的 this_first_page都是 NULL ,然後它們爭先恐後的執行
if (!this_first_page)
判斷,從而進入之後的 alloc_pages ,隨後它們都會爲這個塊設備區域申請內存,並加入基樹結構
如果運氣爆發的話,這兩個任務 radix_tree_insert()的代碼中將有機會近乎同時越過
if (slot != NULL)
return -EEXIST;
的最後防線,先後將新申請的內存指針賦值給基樹結點
雖然 x86的多處理器對同一塊內存的寫操作是原子的,
這樣至少不會因爲這兩個任務同時賦值基樹指針造成指針指向莫名其妙的值,
但這仍然也解決不了我們的問題,後一個賦值操作將覆蓋前一個操作的結果,
基數節點最終將指向稍後一點執行賦值操作的任務
這兩個任務最終將運行到 radix_tree_insert()函數的結尾,而函數的返回值都是漂亮的
剩下的事情扳腳丫子大概也能想出來了,這兩個任務都將自欺欺人地認爲自己正確而成功地爲塊設備分
配了內存,
而真相是其中一個任務拿走的內存卻再也沒有機會拿回來了
至於解決方法嘛,當然是加鎖
只要我們讓“查找基數中有沒有這個節點”到“分配內存並插入這節點”的過程中沒有其他任務的打攪,
就自然的解決了這個問題
首先定義一個鎖,因爲是用來鎖simp_blkdev_data的,
就放在 static struct radix_tree_root simp_blkdev_data;後面吧:
DEFINE_MUTEX(simp_blkdev_datalock); /* protects the disk data op */
然後根據剛纔的思想給對 simp_blkdev_trans()函數中的 simp_blkdev_datalock的操作加鎖,
----------------------- Page 108-----------------------
也就是在
this_first_page = radix_tree_lookup(&simp_blkdev_data,
(dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT);
語句之前添加:
mutex_lock(&simp_blkdev_datalock);
操作結束後被忘了把鎖還回去,否則下次再操作時就成死鎖了,因此在
trans_done:
後面加上
mutex_unlock(&simp_blkdev_datalock);
這一行
完成了嗎?細心看看就知道還沒完
simp_blkdev_trans()函數中有一些判斷異常的代碼,這些代碼大多是扔出一條printk就直接
return的
這樣可不行,可千萬別讓它們臨走時把鎖也順回去了
這意味着我們要在 simp_blkdev_trans()函數中的 3個故障時return的代碼前完成鎖的釋放
因此 simp_blkdev_trans()函數最後就成了這樣:
static int simp_blkdev_trans(unsigned long long dsk_offset, void *buf,
unsigned int len, int dir)
{
unsigned int done_cnt;
struct page *this_first_page;
unsigned int this_off;
unsigned int this_cnt;
done_cnt = 0;
while (done_cnt < len) {
/* iterate each data segment */
this_off = (dsk_offset + done_cnt) & ~SIMP_BLKDEV_DATASEGMASK;
this_cnt = min(len - done_cnt,
(unsigned int)SIMP_BLKDEV_DATASEGSIZE - this_off);
mutex_lock(&simp_blkdev_datalock);
this_first_page = radix_tree_lookup(&simp_blkdev_data,
(dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT);
if (!this_first_page) {
if (!dir) {
memset(buf + done_cnt, 0, this_cnt);
goto trans_done;
}
----------------------- Page 109-----------------------
/* prepare new memory segment for write */
this_first_page = alloc_pages(
GFP_KERNEL | __GFP_ZERO | __GFP_HIGHMEM,
SIMP_BLKDEV_DATASEGORDER);
if (!this_first_page) {
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": allocate page failed\n");
mutex_unlock(&simp_blkdev_datalock);
return -ENOMEM;
}
this_first_page->index = (dsk_offset + done_cnt)
>> SIMP_BLKDEV_DATASEGSHIFT;
if (IS_ERR_VALUE(radix_tree_insert(&simp_blkdev_data,
this_first_page->index, this_first_page))) {
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": insert page to radix_tree failed"
" seg=%lu\n", this_first_page->index);
__free_pages(this_first_page,
SIMP_BLKDEV_DATASEGORDER);
mutex_unlock(&simp_blkdev_datalock);
return -EIO;
}
}
if (IS_ERR_VALUE(simp_blkdev_trans_oneseg(this_first_page,
this_off, buf + done_cnt, this_cnt, dir))) {
mutex_unlock(&simp_blkdev_datalock);
return -EIO;
}
trans_done:
mutex_unlock(&simp_blkdev_datalock);
done_cnt += this_cnt;
}
return 0;
}
這個函數差不多了
----------------------- Page 110-----------------------
我們再看看代碼中還有什麼地方也對 simp_blkdev_data進行操作來着,別漏掉了這些小王八蛋
查找一下代碼,我們發現free_diskmem()函數中也進行了操作
其實從理論上說,這裏不加鎖是不會產生問題的,因爲對內核在執行對塊設備設備時,
會鎖住這個設備對應的模塊 (天哪, 是鎖,這一章和鎖彪上了) ,
其結果是在 simp_blkdev_trans()函數操作 simp_blkdev_data的過程中,
該模塊無法卸載,從而無法不會運行到 free_diskmem()函數
那麼如果同時卸載這個模塊呢,回答是也沒有問題,英勇的模塊鎖也會搞掂這種情況
這一章由於沒有進行功能增加,就不列出修改後模塊的測試經過了,
不過作爲對讀者的安慰,我們將列出到目前爲止經歷了大大小小修改後的全部模塊代碼
看到這些代碼,我們能歷歷在目的回憶出讀這篇教程到現在爲止所經受的全部折磨和苦難
當然也能感受到堅持到現在所得到的知識和領悟
對於 Linux而言,甚至僅僅對於塊設備驅動程序而言,這部教程揭開的也僅僅是冰山一角
而更多的知識其實離我們很近,在google上,在代碼中,在心中
學習,是要用心,不斷地去想,同時要有恆心、耐心、要細心,
人應該越學越謙虛,問題應該越學越多,這大概就是作者通過這部教程最想告訴讀者的
#include <linux/module.h>
#include <linux/blkdev.h>
#include <linux/hdreg.h>
#include <linux/version.h>
/*
* A simple block device driver based on memory
*
* Copyright 2 8 -
* Zhaolei <[email protected]>
*
* Sample for using:
* Create device file (first time only):
* Note: If your system have udev, it can create device file for you in time
* of lsmod and fdisk automatically.
* Otherwise you need to create them yourself by following steps.
* mknod /dev/simp_blkdev b 72
* mknod /dev/simp_blkdev1 b 72 1
* mknod /dev/simp_blkdev2 b 72 2
*
* Create dirs for test (first time only):
* mkdir /mnt/temp1/ # first time only
* mkdir /mnt/temp2/ # first time only
----------------------- Page 111-----------------------
*
* Run it:
* make
* insmod simp_blkdev.ko
* # or insmod simp_blkdev.ko size=numK/M/G/T
* fdisk /dev/simp_blkdev # create 2 patitions
* mkfs.ext3 /dev/simp_blkdev1
* mkfs.ext3 /dev/simp_blkdev2
* mount /dev/simp_blkdev1 /mnt/temp1/
* mount /dev/simp_blkdev2 /mnt/temp2/
* # play in /mnt/temp1/ and /mnt/temp2/
* umount /mnt/temp1/
* umount /mnt/temp2/
* rmmod simp_blkdev.ko
*
*/
#define SIMP_BLKDEV_DEVICEMAJOR COMPAQ_SMART2_MAJOR
#define SIMP_BLKDEV_DISKNAME "simp_blkdev"
#define SIMP_BLKDEV_SECTORSHIFT (9)
#define SIMP_BLKDEV_SECTORSIZE (1ULL<<SIMP_BLKDEV_SECTORSHIFT)
#define SIMP_BLKDEV_SECTORMASK (~(SIMP_BLKDEV_SECTORSIZE-1))
/* usable partitions is SIMP_BLKDEV_MAXPARTITIONS - 1 */
#define SIMP_BLKDEV_MAXPARTITIONS (64)
#define SIMP_BLKDEV_DATASEGORDER (2)
#define SIMP_BLKDEV_DATASEGSHIFT (PAGE_SHIFT + SIMP_BLKDEV_DATASEGORDER)
#define SIMP_BLKDEV_DATASEGSIZE (PAGE_SIZE <<
SIMP_BLKDEV_DATASEGORDER)
#define SIMP_BLKDEV_DATASEGMASK (~(SIMP_BLKDEV_DATASEGSIZE-1))
static struct request_queue *simp_blkdev_queue;
static struct gendisk *simp_blkdev_disk;
static struct radix_tree_root simp_blkdev_data;
DEFINE_MUTEX(simp_blkdev_datalock); /* protects the disk data op */
static char *simp_blkdev_param_size = "16M";
module_param_named(size, simp_blkdev_param_size, charp, S_IRUGO);
static unsigned long long simp_blkdev_bytes;
----------------------- Page 112-----------------------
static int simp_blkdev_trans_oneseg(struct page *start_page,
unsigned long offset, void *buf, unsigned int len, int dir)
{
unsigned int done_cnt;
struct page *this_page;
unsigned int this_off;
unsigned int this_cnt;
void *dsk_mem;
done_cnt = 0;
while (done_cnt < len) {
/* iterate each page */
this_page = start_page + ((offset + done_cnt) >> PAGE_SHIFT);
this_off = (offset + done_cnt) & ~PAGE_MASK;
this_cnt = min(len - done_cnt, (unsigned int)PAGE_SIZE
- this_off);
dsk_mem = kmap(this_page);
if (!dsk_mem) {
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": map device page failed: %p\n", this_page);
return -ENOMEM;
}
dsk_mem += this_off;
if (!dir)
memcpy(buf + done_cnt, dsk_mem, this_cnt);
else
memcpy(dsk_mem, buf + done_cnt, this_cnt);
kunmap(this_page);
done_cnt += this_cnt;
}
return 0;
}
static int simp_blkdev_trans(unsigned long long dsk_offset, void *buf,
unsigned int len, int dir)
{
unsigned int done_cnt;
----------------------- Page 113-----------------------
struct page *this_first_page;
unsigned int this_off;
unsigned int this_cnt;
done_cnt = 0;
while (done_cnt < len) {
/* iterate each data segment */
this_off = (dsk_offset + done_cnt) & ~SIMP_BLKDEV_DATASEGMASK;
this_cnt = min(len - done_cnt,
(unsigned int)SIMP_BLKDEV_DATASEGSIZE - this_off);
mutex_lock(&simp_blkdev_datalock);
this_first_page = radix_tree_lookup(&simp_blkdev_data,
(dsk_offset + done_cnt) >> SIMP_BLKDEV_DATASEGSHIFT);
if (!this_first_page) {
if (!dir) {
memset(buf + done_cnt, 0, this_cnt);
goto trans_done;
}
/* prepare new memory segment for write */
this_first_page = alloc_pages(
GFP_KERNEL | __GFP_ZERO | __GFP_HIGHMEM,
SIMP_BLKDEV_DATASEGORDER);
if (!this_first_page) {
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": allocate page failed\n");
mutex_unlock(&simp_blkdev_datalock);
return -ENOMEM;
}
this_first_page->index = (dsk_offset + done_cnt)
>> SIMP_BLKDEV_DATASEGSHIFT;
if (IS_ERR_VALUE(radix_tree_insert(&simp_blkdev_data,
this_first_page->index, this_first_page))) {
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": insert page to radix_tree failed"
" seg=%lu\n", this_first_page->index);
__free_pages(this_first_page,
----------------------- Page 114-----------------------
SIMP_BLKDEV_DATASEGORDER);
mutex_unlock(&simp_blkdev_datalock);
return -EIO;
}
}
if (IS_ERR_VALUE(simp_blkdev_trans_oneseg(this_first_page,
this_off, buf + done_cnt, this_cnt, dir))) {
mutex_unlock(&simp_blkdev_datalock);
return -EIO;
}
trans_done:
mutex_unlock(&simp_blkdev_datalock);
done_cnt += this_cnt;
}
return 0;
}
static int simp_blkdev_make_request(struct request_queue *q, struct bio *bio)
{
int dir;
unsigned long long dsk_offset;
struct bio_vec *bvec;
int i;
void *iovec_mem;
switch (bio_rw(bio)) {
case READ:
case READA:
dir = 0;
break;
case WRITE:
dir = 1;
break;
default:
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": unknown value of bio_rw: %lu\n", bio_rw(bio));
goto bio_err;
}
if ((bio->bi_sector << SIMP_BLKDEV_SECTORSHIFT) + bio->bi_size
----------------------- Page 115-----------------------
> simp_blkdev_bytes) {
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": bad request: block=%llu, count=%u\n",
(unsigned long long)bio->bi_sector, bio->bi_size);
goto bio_err;
}
dsk_offset = bio->bi_sector << SIMP_BLKDEV_SECTORSHIFT;
bio_for_each_segment(bvec, bio, i) {
iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
if (!iovec_mem) {
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": map iovec page failed: %p\n", bvec->bv_page);
goto bio_err;
}
if (IS_ERR_VALUE(simp_blkdev_trans(dsk_offset, iovec_mem,
bvec->bv_len, dir)))
goto bio_err;
kunmap(bvec->bv_page);
dsk_offset += bvec->bv_len;
}
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, bio->bi_size, 0);
#else
bio_endio(bio, 0);
#endif
return 0;
bio_err:
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, 0, -EIO);
#else
bio_endio(bio, -EIO);
#endif
return 0;
}
----------------------- Page 116-----------------------
static int simp_blkdev_getgeo(struct block_device *bdev,
struct hd_geometry *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 (simp_blkdev_bytes < 16 * 1024 * 1024) {
geo->heads = 1;
geo->sectors = 1;
} else if (simp_blkdev_bytes < 512 * 1024 * 1024) {
geo->heads = 1;
geo->sectors = 32;
} else if (simp_blkdev_bytes < 16ULL * 1024 * 1024 * 1024) {
geo->heads = 32;
geo->sectors = 32;
} else {
geo->heads = 255;
geo->sectors = 63;
}
geo->cylinders = simp_blkdev_bytes >> SIMP_BLKDEV_SECTORSHIFT
/ geo->heads / geo->sectors;
return 0;
}
struct block_device_operations simp_blkdev_fops = {
.owner = THIS_MODULE,
.getgeo = simp_blkdev_getgeo,
};
void free_diskmem(void)
{
unsigned long long next_seg;
struct page *seglist[64];
int listcnt;
----------------------- Page 117-----------------------
int i;
next_seg = 0;
do {
listcnt = radix_tree_gang_lookup(&simp_blkdev_data,
(void **)seglist, next_seg, ARRAY_SIZE(seglist));
for (i = 0; i < listcnt; i++) {
next_seg = seglist[i]->index;
radix_tree_delete(&simp_blkdev_data, next_seg);
__free_pages(seglist[i], SIMP_BLKDEV_DATASEGORDER);
}
next_seg++;
} while (listcnt == ARRAY_SIZE(seglist));
}
int getparam(void)
{
char unit;
char tailc;
if (sscanf(simp_blkdev_param_size, "%llu%c%c", &simp_blkdev_bytes,
&unit, &tailc) != 2) {
return -EINVAL;
}
if (!simp_blkdev_bytes)
return -EINVAL;
switch (unit) {
case 'g':
case 'G':
simp_blkdev_bytes <<= 30;
break;
case 'm':
case 'M':
simp_blkdev_bytes <<= 20;
break;
case 'k':
case 'K':
simp_blkdev_bytes <<= 10;
----------------------- Page 118-----------------------
break;
case 'b':
case 'B':
break;
default:
return -EINVAL;
}
/* make simp_blkdev_bytes fits sector's size */
simp_blkdev_bytes = (simp_blkdev_bytes + SIMP_BLKDEV_SECTORSIZE - 1)
& SIMP_BLKDEV_SECTORMASK;
return 0;
}
static int __init simp_blkdev_init(void)
{
int ret;
ret = getparam();
if (IS_ERR_VALUE(ret))
goto err_getparam;
simp_blkdev_queue = blk_alloc_queue(GFP_KERNEL);
if (!simp_blkdev_queue) {
ret = -ENOMEM;
goto err_alloc_queue;
}
blk_queue_make_request(simp_blkdev_queue, simp_blkdev_make_request);
simp_blkdev_disk = alloc_disk(SIMP_BLKDEV_MAXPARTITIONS);
if (!simp_blkdev_disk) {
ret = -ENOMEM;
goto err_alloc_disk;
}
INIT_RADIX_TREE(&simp_blkdev_data, GFP_KERNEL);
strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME);
simp_blkdev_disk->major = SIMP_BLKDEV_DEVICEMAJOR;
simp_blkdev_disk->first_minor = 0;
simp_blkdev_disk->fops = &simp_blkdev_fops;
----------------------- Page 119-----------------------
simp_blkdev_disk->queue = simp_blkdev_queue;
set_capacity(simp_blkdev_disk,
simp_blkdev_bytes >> SIMP_BLKDEV_SECTORSHIFT);
add_disk(simp_blkdev_disk);
return 0;
err_alloc_disk:
blk_cleanup_queue(simp_blkdev_queue);
err_alloc_queue:
err_getparam:
return ret;
}
static void __exit simp_blkdev_exit(void)
{
del_gendisk(simp_blkdev_disk);
free_diskmem();
put_disk(simp_blkdev_disk);
blk_cleanup_queue(simp_blkdev_queue);
}
module_init(simp_blkdev_init);
module_exit(simp_blkdev_exit);
MODULE_LICENSE("GPL");
追記:偶然看到剛纔的代碼首部註釋,Copyright後面還是 2 8年
大概是從第一章開始一直這樣拷貝過來的
這部教程從2 8年11月斷斷續續的寫到了 2 9年3月,終於功德圓滿了
作爲作者寫的第一個如此長度篇幅的教程,炸一眼瞟過來,倒也還像個樣子,
看來寫教程並不是太難高攀的事情,因此如果讀者也時不時地有一些寫起來的衝動,
就不妨開始吧 : )
本章以塊設備驅動程序的代碼爲例,說明了內核中的同步概念,
當然,在不少情況下,程序員遇到的同步問題比這裏的要複雜的多,
內核中也採用了很多方法和技巧來處理同步,瞭解和學習這些知識,
收穫的不僅是數據同步本身的解決方法,更是一種思路,
這對於更一般的程序設計都是有很大幫助的,因此有空時google一下,
總能找到自己想了解的知識
<--全文完,趙磊出品,必屬精品-->