塊設備的驅動比字符設備的難,這是因爲塊設備的驅動和內核的聯繫進一步增大,但是同時塊設備的訪問的幾個基本結構和字符還是有相似之處的。
有一句話必須記住:對於存儲設備(硬盤~~帶有機械的操作)而言,調整讀寫的順序作用巨大,因爲讀寫連續的扇區比分離的扇區快。
但是同時:SD卡和U盤這類設備沒有機械上的限制,所以像上面說的進行連續扇區的調整顯得就沒有必要了。
先說一下對於硬盤這類設備的簡單的驅動。
在linux的內核中,使用gendisk結構來表示一個獨立的磁盤設備或者分區。這個結構中包含了磁盤的主設備號,次設備號以及設備名稱。
在國嵌給的歷程中,對gendisk這個結構體的填充是在simp_blkdev_init函數中完成的。在對gendisk這個結構填充之前要對其進行分配空間。具體代碼如下:
simp_blkdev_disk = alloc_disk(1);
if (!simp_blkdev_disk) {
ret = -ENOMEM;
goto err_alloc_disk;
}
這裏的alloc_disk函數是在內核中實現的,它後面的參數1代表的是使用次設備號的數量,這個數量是不能被修改的。
在分配好了關於gendisk的空間以後就開始對gendisk裏面的成員進行填充。具體代碼如下:
strcpy(simp_blkdev_disk->disk_name, SIMP_BLKDEV_DISKNAME); //宏定義simp_blkdev
simp_blkdev_disk->major = SIMP_BLKDEV_DEVICEMAJOR; //主設備號
simp_blkdev_disk->first_minor = 0; //次設備號
simp_blkdev_disk->fops = &simp_blkdev_fops; //主要結構
simp_blkdev_disk->queue = simp_blkdev_queue;
set_capacity(simp_blkdev_disk, SIMP_BLKDEV_BYTES>>9); //宏定義(16*1024*1024),實際上就是這個結構體。
在填充好gendisk這個結構以後向內核中 註冊這個磁盤設備。具體代碼如下:
add_disk(simp_blkdev_disk);
在LDD中說,想內核中註冊設備的必須在gendisk這個結構體已經填充好了以後,我們以前的字符設備的時候也是這麼做的,不知道爲什麼LDD在這裏強調了這個。
當不需要一個磁盤的時候要釋放gendisk,釋放部分的代碼在函數simp_blkdev_exit中實現的。具體的釋放代碼如下:
del_gendisk(simp_blkdev_disk);
在simp_blkdev_exit中同時還有put_disk(simp_blkdev_disk),這個是用來進行操作gendisk的引用計數。simp_blkdev_exit還實現了blk_cleanup_queue清除請求隊列的這個函數。終於說到請求隊列了。
在說等待隊列之前先要明確幾個概念:
①用戶希望對硬盤數據做的事情叫做請求,這個請求和IO請求是一樣的,所以IO請求來自於上層。
②每一個IO請求對應內核中的一個bio結構。
③IO調度算法可以將連續的bio(也就是用戶的對硬盤數據的相鄰簇的請求)合併成一個request。
④多個request就是一個請求隊列,這個請求隊列的作用就是驅動程序響應用戶的需求的隊列。
請求隊列在國嵌的程序中的simp_blkdev_queue
下面先說一下硬盤這類帶有機械的存儲設備的驅動。
這類驅動中用戶的IO請求對應於硬盤上的簇可能是連續的,可能是不連續的,連續的當然好,如果要是不連續的,那麼IO調度器就會對這些BIO進行排序(例如老謝說的電梯調度算法),合併成一個request,然後再接收請求,再合併成一個request,多個request之後那麼我們的請求隊列就形成了,然後就可以向驅動程序提交了。
在硬盤這類的存儲設備中,請求隊列的初始化代碼如下:
simp_blkdev_queue = blk_init_queue(simp_blkdev_do_request, NULL);
老謝說,在這種情況下首先調用的是內核中的make_requst函數,然後再調用自己定義的simp_blkdev_do_request。追了一下內核代碼,會發現make_requst內核代碼如下所示:
static int make_request(struct request_queue *q, struct bio * bio)
具體的就不貼了,不過可以知道這個make_request的作用就是使用IO調度器對多個bio的訪問順序進行了優化調整合併爲一個request。也就是在執行完成了這個函數之後纔去正式的執行內核的請求隊列。
合併後的request其實還是一個結構,這個結構用來表徵IO的請求,這個結構在內核中有具體的定義。
請求是一個結構,同時請求隊列也是一個結構,這個請求隊列在內核中的結構定義如下:
struct request_queue
{
/*
* Together with queue_head for cacheline sharing
*/
struct list_head queue_head;
struct request *last_merge;
struct elevator_queue *elevator;
/*
* the queue request freelist, one for reads and one for writes
*/
struct request_list rq;
request_fn_proc *request_fn;
make_request_fn *make_request_fn;
prep_rq_fn *prep_rq_fn;
unplug_fn *unplug_fn;
merge_bvec_fn *merge_bvec_fn;
prepare_flush_fn *prepare_flush_fn;
softirq_done_fn *softirq_done_fn;
rq_timed_out_fn *rq_timed_out_fn;
dma_drain_needed_fn *dma_drain_needed;
lld_busy_fn *lld_busy_fn;
/*
* Dispatch queue sorting
*/
sector_t end_sector;
struct request *boundary_rq;
/*
* Auto-unplugging state
*/
struct timer_list unplug_timer;
int unplug_thresh; /* After this many requests */
unsigned long unplug_delay; /* After this many jiffies */
struct work_struct unplug_work;
struct backing_dev_info backing_dev_info;
/*
* The queue owner gets to use this for whatever they like.
* ll_rw_blk doesn't touch it.
*/
void *queuedata;
/*
* queue needs bounce pages for pages above this limit
*/
gfp_t bounce_gfp;
/*
* various queue flags, see QUEUE_* below
*/
unsigned long queue_flags;
/*
* protects queue structures from reentrancy. ->__queue_lock should
* _never_ be used directly, it is queue private. always use
* ->queue_lock.
*/
spinlock_t __queue_lock;
spinlock_t *queue_lock;
/*
* queue kobject
*/
struct kobject kobj;
/*
* queue settings
*/
unsigned long nr_requests; /* Max # of requests */
unsigned int nr_congestion_on;
unsigned int nr_congestion_off;
unsigned int nr_batching;
void *dma_drain_buffer;
unsigned int dma_drain_size;
unsigned int dma_pad_mask;
unsigned int dma_alignment;
struct blk_queue_tag *queue_tags;
struct list_head tag_busy_list;
unsigned int nr_sorted;
unsigned int in_flight[2];
unsigned int rq_timeout;
struct timer_list timeout;
struct list_head timeout_list;
struct queue_limits limits;
/*
* sg stuff
*/
unsigned int sg_timeout;
unsigned int sg_reserved_size;
int node;
#ifdef CONFIG_BLK_DEV_IO_TRACE
struct blk_trace *blk_trace;
#endif
/*
* reserved for flush operations
*/
unsigned int ordered, next_ordered, ordseq;
int orderr, ordcolor;
struct request pre_flush_rq, bar_rq, post_flush_rq;
struct request *orig_bar_rq;
struct mutex sysfs_lock;
#if defined(CONFIG_BLK_DEV_BSG)
struct bsg_class_device bsg_dev;
#endif
};
LDD說,請求隊列實現了一個插入接口,這個接口允許使用多個IO調度器,大部分IO調度器批量累計IO請求,並將它們排列爲遞增或者遞減的順序提交給驅動。
多個連續的bio會合併成爲一個request,多個request就成爲了一個請求隊列,這樣bio的是直接的也是最基本的請求,bio這個結構的定義如下:
struct bio { sector_t bi_sector;
struct bio *bi_next; /* request queue link */
struct block_device *bi_bdev; /* target device */
unsigned long bi_flags; /* status, command, etc */ unsigned long bi_rw; /* low bits: r/w, high: priority */
unsigned int bi_vcnt; /* how may bio_vec's */
unsigned int bi_idx; /* current index into bio_vec array */
unsigned int bi_size; /* total size in bytes */
unsigned short bi_phys_segments; /* segments after physaddr coalesce*/ unsigned short bi_hw_segments; /* segments after DMA remapping */ unsigned int bi_max; /* max bio_vecs we can hold used as index into pool */ struct bio_vec *bi_io_vec; /* the actual vec list */
bio_end_io_t *bi_end_io; /* bi_end_io (bio) */
atomic_t bi_cnt; /* pin count: free when it hits zero */ void *bi_private;
bio_destructor_t *bi_destructor; /* bi_destructor (bio) */ };
需要注意的是,在bio這個結構中最重要的是bio.vec這個結構。同時還有許多操作bio的宏,這些都是內核給實現好了的。
請求隊列的實現:
首先使用 while ((req = elv_next_request(q)) != NULL)進行循環檢測,看看到底傳來的IO請求是個什麼。
然後進行讀寫區域的判定:
if ((req->sector + req->current_nr_sectors) << 9
> SIMP_BLKDEV_BYTES) {
printk(KERN_ERR SIMP_BLKDEV_DISKNAME
": bad request: block=%llu, count=%u\n",
(unsigned long long)req->sector,
req->current_nr_sectors);
//結束本次請求。
end_request(req, 0);
continue;
}
在進行讀寫區域的判定的時候涉及到了很多linux的編程習慣。
sector表示要訪問的第一個扇區。
current_nr_sectors表示預計訪問扇區的數目。
這裏的左移九位其實就是乘以512。
這樣((req->sector + req->current_nr_sectors) << 9就計算出可以預計要訪問的扇區的大小。進行了一次判斷。
如果上面的判斷沒有超出範圍,那麼就可以對請求的這一部分塊設備進行操作了。
simp_blkdev_disk = alloc_disk(1);
if (!simp_blkdev_disk) {
ret = -ENOMEM;
goto err_alloc_disk;
}
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;
simp_blkdev_disk->queue = simp_blkdev_queue;
set_capacity(simp_blkdev_disk, SIMP_BLKDEV_BYTES>>9);
add_disk(simp_blkdev_disk);
return 0;
關於gendisk結構的內存分配和成員的填充和硬盤類塊設備是一樣的。
由於SD卡和U盤屬於一類非機械類的設備,所以我們不需要那麼複雜的調度算法,也就是不需要把io請求進行排序,所以我們需要自己爲自己分配一個請求隊列。具體代碼如下:
simp_blkdev_queue = blk_alloc_queue(GFP_KERNEL);
需要注意一下的是在硬盤類塊設備的驅動中這個函數的原型是blk_init_queue(simp_blkdev_do_request, NULL);
在這種情況下其實並沒有調用內核的make_request(這個函數的功能上文說過),也就是說我們接下來要綁定的simp_blkdev_make_request這個函數和make_request是同一級別的函數。這裏就沒有涉及到算法調度的問題了。
然後需要進行的工作是:綁定製造請求函數和請求隊列。具體代碼如下:
blk_queue_make_request(simp_blkdev_queue, simp_blkdev_make_request);
把gendisk的結構成員都添加好了以後就可以執行add_disk(simp_blkdev_disk);這個函數把這塊分區添加進內核了。
整體列出製造請求部分的函數。
//這個條件是在判斷當前正在運行的內核版本。
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, 0, -EIO);
#else
bio_endio(bio, -EIO);
#endif
return 0;
}
dsk_mem = simp_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 SIMP_BLKDEV_DISKNAME
": unknown value of bio_rw: %lu\n",
bio_rw(bio));
#if LINUX_VERSION_CODE < KERNEL_VERSION(2, 6, 24)
bio_endio(bio, 0, -EIO);
#else
bio_endio(bio, -EIO);
#endif
return 0;
}
dsk_mem += 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;
}