一步一步走進塊驅動之第十五章

第十五章

本教程修改自趙磊的網上的一系列教程.本人覺得該系列教程寫的非常不錯.以風趣幽默的語言將塊驅動寫的非常詳細,對於入門教程,應該屬於一份經典了. 本人在這對此係列教程最後附上對Linux 2.6.36版本的代碼.並編譯運行成功. 該教程所有版權仍歸作者趙磊所有,本人只做附錄代碼的添加,併爲對原文修改.有不懂的地方,可以聯繫我 [email protected] 或者給我留言  

+---------------------------------------------------+

|                 寫一個塊設備驅動                  

+---------------------------------------------------+

作者:趙磊                                        

| email: [email protected]                      

+---------------------------------------------------+

文章版權歸原作者所有。                            

大家可以自由轉載這篇文章,但原版權信息必須保留。  

如需用於商業用途,請務必與原作者聯繫,若因未取得  

授權而收起的版權爭議,由侵權者自行負責。          

+---------------------------------------------------+

在上一章中我們對這個塊設備驅動所作的更改使它具備了動態申請內存的能力,
但實際上同時也埋下一個隱患,就是數據訪問衝突。

這裏我們順便嘮叨一下內核開發中的同步問題。
提到數據訪問同步,自然而然會使人想到多進程、多線程、加鎖、解鎖、
信號量、synchronized關鍵字等東西,然後就很頭疼。
對於用戶態程序,網上大量的解釋數據同步概念和方法的文章給人的印象大概是:
同步很危險,編程要謹慎,
處處有機關,問題很難找。

對於第一次進行多線程時編程的人來說,感覺可能是以下兩種:
一種是覺得程序中處處都會有問題,任何一條訪問數據的指令都不安全,
恨不得把程序中所有的數據都加上鎖,甚至打算給鎖本身的數據再加個鎖,
另一種是沒覺得有什麼困難,根本不去理什麼都互斥不互斥,
就按原先的來,編出的程序居然也運行得很順。
然後懷着這兩種想法人通過不斷的學習和實踐掌握了數據同步的知識後認識到,
數據同步其實並不像前一種想法那樣危險,也不像後一種想法那樣簡單。

所幸的是對於不少用戶態程序來說,倒是可以不用考慮數據同步問題。
至少當我們剛開始寫HelloWorld時不用去理這個麻煩。

而對於內核態代碼而言,很不幸,整個兒幾乎都相當於用戶態的多線程。
其實事情也並非原本就是這麼糟的。
在很久很久以前,山是青的,草是綠的,牛奶是能喝的,
見到老人摔跤是敢扶的,作者是純情的,電腦也是單CPU的。
那時的內核環境很靜,很美。除了中斷會時不時地搗搗亂,其餘的都挺詩意。
代碼獨個兒在跑,就像是一輛汽車在荒漠上奔馳,因爲沒有其他妨礙,
幾乎可以毫無顧忌地訪問數據,而不用考慮什麼萬惡的訪問衝突。
唯一要考慮的從天而降的中斷奧特曼,解決的方法倒也不難,禁用了中斷看你還能咋的。

然後隨着作者的成長,目光從書本轉向了美眉,計算機也由單CPU發展成了多CPU。
內核代碼的執行環境終於開始熱鬧起來,由於每個CPU上都在執行任務,
這些任務進入到對應的內核態時會出現多條內核指令流同時執行,
這些指令流對全局數據的訪問很明顯就牽涉到了同步問題,這是開端。
從那時起編程時要考慮其他CPU上的事情了。

然後隨着作者的進一步成長,目光從美眉的臉轉向了胸,
CPU製造商爲了貫徹給程序員找麻煩的精神,搞出了亂序執行。
這一創舉驚醒了多年來還在夢中的諸多程序員,原來,程序不是按程序執行的啊。
正如林高官說的:“我是交通部派來的,級別和你們市長一樣高,敢跟我鬥,
你們這些人算個屁呀!”原來,無職無權的平民百姓就是屁啊。
正當程序員從睡夢中驚醒還沒緩過神時,編譯器又跟着搗亂,
“你CPU都能亂序了,憑什麼不讓我亂序?”
然後熱鬧了,好在我們還有mb()、rmb()、wmb()、barrier()這幾根救命稻草,
事情倒是沒變得太糟。

然後隨着作者的進一步成長,目光從美眉的胸轉向了臀,
內核也從一開始時被動的爲了適應多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()函數的結尾,而函數的返回值都是漂亮的0。
剩下的事情扳腳丫子大概也能想出來了,這兩個任務都將自欺欺人地認爲自己正確而成功地爲塊設備分配了內存,
而真相是其中一個任務拿走的內存卻再也沒有機會拿回來了。

至於解決方法嘛,當然是加鎖。
只要我們讓“查找基數中有沒有這個節點”到“分配內存並插入這節點”的過程中沒有其他任務的打攪,
就自然的解決了這個問題。

首先定義一個鎖,因爲是用來鎖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的操作加鎖,
也就是在
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;
                        }


                        /* 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;
}

這個函數差不多了。
我們再看看代碼中還有什麼地方也對simp_blkdev_data進行操作來着,別漏掉了這些小王八蛋。
查找一下代碼,我們發現free_diskmem()函數中也進行了操作。

其實從理論上說,這裏不加鎖是不會產生問題的,因爲對內核在執行對塊設備設備時,
會鎖住這個設備對應的模塊(天哪,又是鎖,這一章和鎖彪上了),
其結果是在simp_blkdev_trans()函數操作simp_blkdev_data的過程中,
該模塊無法卸載,從而無法不會運行到free_diskmem()函數。

那麼如果同時卸載這個模塊呢,回答是也沒有問題,英勇的模塊鎖也會搞掂這種情況。

這一章由於沒有進行功能增加,就不列出修改後模塊的測試經過了,
不過作爲對讀者的安慰,我們將列出到目前爲止經歷了大大小小修改後的全部模塊代碼。
看到這些代碼,我們能歷歷在目的回憶出讀這篇教程到現在爲止所經受的全部折磨和苦難。
當然也能感受到堅持到現在所得到的知識和領悟。

對於Linux而言,甚至僅僅對於塊設備驅動程序而言,這部教程揭開的也僅僅是冰山一角。
而更多的知識其實離我們很近,在google上,在代碼中,在心中。
學習,是要用心,不斷地去想,同時要有恆心、耐心、要細心,
人應該越學越謙虛,問題應該越學越多,這大概就是作者通過這部教程最想告訴讀者的。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/genhd.h>		//add_disk
#include <linux/blkdev.h>		//struct block_device_operations
#include <linux/hdreg.h>

#define _DEBUG_

#define BLK_PAGE_ORDER			2
#define BLK_PAGE_SIZE			(PAGE_SIZE << BLK_PAGE_ORDER)
#define BLK_PAGE_SHIFT			(PAGE_SHIFT + BLK_PAGE_ORDER)
#define BLK_PAGE_MASK			(~(BLK_PAGE_SIZE - 1))

#define BLK_SECTOR_SHIFT		9
#define BLK_SECTOR_SIZE			(1ULL << BLK_SECTOR_SHIFT)
#define BLK_SECTOR_MASK			(~(BLK_SECTOR_SIZE - 1))

#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;

struct radix_tree_root blk_dev_data;
DEFINE_MUTEX(blk_radix_mutex);
static unsigned long long g_blk_size = 1;
static char*	defaule_size = "16M";

int getsize(void)
{
	char flag;
	char tail = 'N';

#ifdef _DEBUG_
	printk(KERN_WARNING "parameter = %s\n",defaule_size);
#endif		
	
	if(sscanf(defaule_size,"%llu%c%c",&g_blk_size,&flag,&tail) != 2){
		return -EINVAL;
	}
	
	if(!g_blk_size)
		return -EINVAL;
	
	switch(flag){
	case 'g':
	case 'G':
		g_blk_size <<= 30;
		break;
	case 'm':
	case 'M':
		g_blk_size <<= 20;
		break;
	case 'k':
	case 'K':
		g_blk_size <<= 10;
		break;
	}
	
	g_blk_size = (g_blk_size + BLK_SECTOR_SIZE - 1) & BLK_SECTOR_MASK;
	//此處爲字節對齊.(1<<9 - 1) = 255 = 0xFF
	
#ifdef _DEBUG_
	printk(KERN_WARNING "size = %llu tail = %c\n",g_blk_size,tail);
#endif			
	return 0;
}

static int disk_get_pages(struct page **ppage,unsigned int index)
{
	int ret = 0;
	struct page *page = NULL;
	page = (void*)alloc_pages(GFP_KERNEL | __GFP_ZERO | __GFP_HIGHMEM,BLK_PAGE_ORDER);
	if(NULL == page){
		ret = -ENOMEM;
		goto err_get_page;
	}
		
	page->index = index;
	ret = radix_tree_insert(&blk_dev_data,index,(void*)page);
	if(IS_ERR_VALUE(ret)){
		ret = -EIO;
		goto err_insert;
	}
	*ppage = page;
	return 0;

err_insert:
	__free_pages(page,BLK_PAGE_ORDER);
err_get_page:
	return ret;
}

/*
	function:	blk_make_once
	description:
		該函數完成一次數據操作,將數據讀/寫到指定的page頁面.
	parameter:	
		struct page *page,	:要讀/寫的頁面
		unsigned int offset,	:頁面的相對偏移長度
		void *iovec_men,		:真實的數據地址
		unsigned int len,		:數據長度
		int dir			:數據方向
	
*/
static int blk_make_once(struct page *page,unsigned int offset,void *iovec_men,unsigned int len,int dir)
{	
	void *dsk_mem = NULL;
	unsigned int count_current = 0;
	unsigned int count_done = 0;
	unsigned int count_offset = 0;
	struct page *this_page = NULL;
	
	while(count_done < len){
		count_offset = (offset + count_done) & ~PAGE_MASK;
		/*
			count_offset:計算當前地址到頁首的偏移
		*/
		count_current = min(len - count_done,
							(unsigned int)(PAGE_SIZE - count_offset));
		/*
			count_current:計算當前傳輸數據的長度,若跨頁,則取此頁到頁尾的長度
			(PAGE_SIZE - count_offset):計算當前偏移到頁尾的長度
		*/
		this_page = page + ((offset + count_done) >> PAGE_SHIFT);
		/*
			this_page:獲取片面,將頁面映射至高端內存
		*/
		dsk_mem = kmap(this_page);
		if(!dsk_mem){
			printk(KERN_ERR BLK_DISK_NAME
					": get memory page address failed: %p\n",
					page);
			return -EIO;
		}
		
		dsk_mem += count_offset;
		
		if(!dir){
			//read
			memcpy(iovec_men + count_done,dsk_mem,count_current);
		}else{
			//write
			memcpy(dsk_mem,iovec_men + count_done,count_current);
		}
		
		kunmap(this_page);
		
		count_done += count_current;
	}

	return 0;	
}


/*
	function:	blk_make_transition
	description:
		該函數完成一次映射好的becv數據操作
	parameter:	
		unsigned long long disk_offset,	:要讀寫的虛擬磁盤的地址
		void *iovec_men,				:映射好的bvec操作的數據地址
		unsigned int len,				:數據長度
		int dir					:數據方向
	
*/
static int blk_make_transition(unsigned long long disk_offset,void *iovec_men,unsigned int len,int dir)
{
	unsigned int count_current = 0;
	unsigned int count_done = 0;
	unsigned int count_offset = 0;
	struct page *page = NULL;
	count_done = 0;
	while(count_done < len){
		count_offset = (disk_offset + count_done) & ~BLK_PAGE_MASK;
		/*
			count_offset:表示當前頁對應頁對齊的地址的偏移長度
		*/
		count_current = min(len - count_done,
						(unsigned int)(BLK_PAGE_SIZE - count_offset));
		/*
			BLK_PAGE_SIZE - count_offset:表示當前頁最後能存儲的長度.
		*/
		
		mutex_lock(&blk_radix_mutex);
		/*
			採用互斥鎖,防止多進程同時訪問至此,該頁面未申請,導致多個進程同時申請同一個地址頁面
			導致頁面相互覆蓋,造成的頁面浪費.浪費的頁面無法釋放.
		*/
		page = (struct page*)radix_tree_lookup(&blk_dev_data,(disk_offset + count_done) >> BLK_PAGE_SHIFT);
		if(!page){
			if(!dir){
				memset(iovec_men + count_done,0,count_current);
				goto trans_done;
			}
			if(IS_ERR_VALUE(disk_get_pages(&page,(disk_offset + count_done) >> BLK_PAGE_SHIFT))){
				printk(KERN_ERR BLK_DISK_NAME
						": alloc page failed index: %llu\n",
						(disk_offset + count_done) >> BLK_PAGE_SHIFT);
				mutex_unlock(&blk_radix_mutex);
				return -EIO;
			}
		}
		//進行一次數據讀寫
		if(IS_ERR_VALUE(blk_make_once(page,count_offset,iovec_men + count_done,count_current,dir))){
			return -EIO;
		}
trans_done:		
		mutex_unlock(&blk_radix_mutex);
		count_done += count_current;
	}
	return 0;	
}

static int blkdev_make_request(struct request_queue *q, struct bio *bio)
{
	struct bio_vec *bvec;
	int i;
	int dir = 0;
	unsigned long long disk_offset = 0;

	if ((bio->bi_sector << BLK_SECTOR_SHIFT) + bio->bi_size > g_blk_size) {
		printk(KERN_ERR BLK_DISK_NAME
				": bad request: block=%llu, count=%u\n",
				(unsigned long long)bio->bi_sector, bio->bi_size);
		goto bio_err;
	}
	
	switch(bio_rw(bio)){
	case READ:
	case READA:
		dir = 0;
		break;
	case WRITE:
		dir = 1;
		break;
	}
	
	disk_offset = bio->bi_sector << BLK_SECTOR_SHIFT;
	
	bio_for_each_segment(bvec, bio, i) {
		void *iovec_mem;
		iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
		
		if(!iovec_mem){
			printk(KERN_ERR BLK_DISK_NAME
					": kmap page faile: %p\n",
                    bvec->bv_page);
            goto bio_err;    
		}
		//進行一次bvec的操作
		if(IS_ERR_VALUE(blk_make_transition(disk_offset,iovec_mem,bvec->bv_len,dir))){
			kunmap(bvec->bv_page);
			goto bio_err;
		}
		kunmap(bvec->bv_page);
		disk_offset += bvec->bv_len;
	}		
	
	bio_endio(bio, 0);
	return 0;
bio_err:
	bio_endio(bio, -EIO);
	return 0;
}

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 (g_blk_size < 16 * 1024 * 1024) {
		phd_geo->heads = 1;
		phd_geo->sectors = 1;
	} else if (g_blk_size < 512 * 1024 * 1024) {
		phd_geo->heads = 1;
		phd_geo->sectors = 32;
	} else if (g_blk_size < 16ULL * 1024 * 1024 * 1024) {
		phd_geo->heads = 32;
		phd_geo->sectors = 32;
	} else {
		phd_geo->heads = 255;
		phd_geo->sectors = 63;
	}

	phd_geo->cylinders = g_blk_size >> BLK_SECTOR_SHIFT / phd_geo->heads / phd_geo->sectors;
	
	return 0;
}

struct block_device_operations fop = {
	.owner = THIS_MODULE,
	.getgeo = gendisk_getgeo,
};

void delete_diskmem(void)
{
	int i = 0;
	int page_count = 0;
	int next_count = 0;
	struct page *pagelist[64] = {NULL};
	
	page_count = 0;
	next_count = 0;
	do{
		page_count = radix_tree_gang_lookup(&blk_dev_data, (void**)pagelist,next_count,ARRAY_SIZE(pagelist));
		for(i = 0;i < page_count;i++){
			next_count = pagelist[i]->index;
			radix_tree_delete(&blk_dev_data,next_count);
			__free_pages(pagelist[i],BLK_PAGE_ORDER);
		}
		next_count++;
	}while(page_count == ARRAY_SIZE(pagelist));
}

static int __init initialization_function(void)
{
	int ret = 0;
	
	getsize();
	
	MAJOR_NR = register_blkdev(0, BLK_DISK_NAME);
	if(MAJOR_NR < 0)
	{		
		return -1;
	}
	
	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);
	
	g_blkdev_disk = alloc_disk(MAX_PARTITIONS);
	if(NULL == g_blkdev_disk){
		ret = -ENOMEM;		
		goto err_alloc_disk;
	}
	
	INIT_RADIX_TREE(&blk_dev_data,GFP_KERNEL);
	
	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, g_blk_size>>BLK_SECTOR_SHIFT);
	
	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
	delete_diskmem();								//->alloc_diskmem
	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);						//指定模塊卸載函數

//到處函數.導出名size,對應變量defaule_size
module_param_named(size, defaule_size, charp, S_IRUGO);

//模塊信息及許可證
MODULE_AUTHOR("LvApp");								//作者
MODULE_LICENSE("Dual BSD/GPL");						//許可證
MODULE_DESCRIPTION("A simple block module");				//描述
MODULE_ALIAS("block");						   //別名

追記:偶然看到剛纔的代碼首部註釋,Copyright後面還是2008年。
大概是從第一章開始一直這樣拷貝過來的。
這部教程從2008年11月斷斷續續的寫到了2009年3月,終於功德圓滿了。
作爲作者寫的第一個如此長度篇幅的教程,炸一眼瞟過來,倒也還像個樣子,
看來寫教程並不是太難高攀的事情,因此如果讀者也時不時地有一些寫起來的衝動,
就不妨開始吧: )

本章以塊設備驅動程序的代碼爲例,說明了內核中的同步概念,
當然,在不少情況下,程序員遇到的同步問題比這裏的要複雜的多,
內核中也採用了很多方法和技巧來處理同步,瞭解和學習這些知識,
收穫的不僅是數據同步本身的解決方法,更是一種思路,
這對於更一般的程序設計都是有很大幫助的,因此有空時google一下,
總能找到自己想了解的知識。

<--全文完,趙磊出品,必屬精品-->

本人是在參考教程之後修改的教程內容.如有不同.可能有遺漏沒有修改.造成對讀者的迷惑,在此致歉~~ 


發佈了38 篇原創文章 · 獲贊 25 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章