1. 程式人生 > >linux下的塊裝置驅動(一)

linux下的塊裝置驅動(一)

塊裝置的驅動比字符裝置的難,這是因為塊裝置的驅動和核心的聯絡進一步增大,但是同時塊裝置的訪問的幾個基本結構和字元還是有相似之處的。

有一句話必須記住:對於儲存裝置(硬碟~~帶有機械的操作)而言,調整讀寫的順序作用巨大,因為讀寫連續的扇區比分離的扇區快。

但是同時: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;
}