1. 程式人生 > >塊裝置驅動程式之一

塊裝置驅動程式之一

一、塊裝置概述

linux支援的兩種重要的裝置型別分別是字元裝置和塊裝置,塊裝置可以隨機地以固定大小的塊傳送資料。與字元裝置相比,塊裝置有以下幾個特殊之處:
  1. 塊裝置可以從資料的任何位置進行訪問
  2. 塊資料總是以固定長度進行傳輸,即便請求的這是一個位元組
  3. 對塊裝置的訪問有大量的快取。當進行讀時,如果已經快取了,就直接使用快取中的資料,而不再讀裝置,對於寫也通過快取來進行延遲處理。
在塊系統中,資料塊指的是固定大小的資料,該固定大小由核心規定,通常是4096個位元組。與資料塊對應的是扇區,它是由裝置硬體所決定的一個塊,其大小取決於硬體,常見的硬體的扇區大多都是512個位元組。資料塊的大小都是扇區大小的整數倍。
由於檔案多儲存在塊裝置上,而且現代作業系統使用虛擬記憶體來工作,為了實現虛擬記憶體就需要將資料在記憶體和儲存裝置(塊裝置)之間進行交換。因而高效的塊驅動對系統性能是至關重要的,塊驅動的框架要比字元裝置複雜的多,其中引入了偽檔案系統bdev,快取,預讀演算法,IO排程器等,下圖是一個簡單的圖示:

struct block_device被核心用來表示塊裝置,它是塊裝置的基礎資料結構。系統中所有的塊裝置都被新增到了偽檔案系統bdev中,虛擬檔案系統VFS通過該檔案系統來訪問塊裝置。bdev使用的核心資料結構為:

struct bdev_inode {
	struct block_device bdev;
	struct inode vfs_inode;
};
由此可見,只要找到塊裝置在檔案系統中的inode(它被儲存在bdev_inode的vfs_inode域中),就可以找到該塊裝置對應的struct block_device,進而就可以對裝置進行操作了。從VFS到塊裝置的關聯關係就是通過偽檔案系統bdev建立起來的。
塊裝置的裝置檔案的file_operations結構中提供的是通用的塊讀寫函式,而不是驅動自己的讀寫。使用者發出的裝置讀寫是和緩衝區互動,也就是說使用者發出的讀寫請求讀寫的是緩衝區。塊框架負責(根據讀寫請求)完成緩衝區和物理裝置之間的資料交換。提交到裝置的操作都以請求的方式出現,所有的真實的裝置的讀寫都在請求被處理時被執行,因而塊裝置驅動最主要的工作就是完成請求的處理。
塊裝置框架提供了複雜的佇列功能用於對請求進行排隊,排程。請求佇列包含了一些基本的資訊,比如:

  • 指向該佇列排程器的指標,排程器用於管理該請求佇列上的請求
  • 為該請求佇列分配請求結構的記憶體池。該記憶體池用於為該裝置分配讀寫請求結構。
  • 請求佇列的設定資訊。
而請求則包含了本請求的基本資訊:
  • 比如該請求的gendisk和hd_struct
  • 該請求的bio資訊
  • 請求完成時的回撥函式指標 

二、註冊塊裝置

類似於字元裝置,對於塊裝置驅動來說,第一件事是向核心註冊自己。其對應的API為
int register_blkdev(unsigned int major, const char *name);
  • major:該裝置的主裝置號,如果為0,則該函式會為該裝置分配一個主裝置號。
  • name:該裝置的名字
如果註冊時指定了主裝置號,則成功返回0,否則返回負的錯誤碼
如果註冊時指定的主裝置號為0,則成功會返回系統自動分配的主裝置號,失敗返回負的錯誤碼。
如果不再使用裝置,可以用以下API解除註冊:
int unregister_blkdev(unsigned int major, const char *name);
引數和註冊時的要匹配。
對於塊裝置來說,註冊實際上就完成了兩個動作:
  1. 如果沒有指定主裝置號,則分配一個
  2. 將該裝置使用的裝置號資訊更新到資料庫major_names中,major_names中的資訊會出現在/proc/devices中
因此可以看出,註冊做的事情實際上非常少。註冊完成後,除了能夠在/proce/devices中看到裝置之外,不能對裝置做任何事情,裝置還無法使用。

三、塊裝置資料結構

將塊設備註冊到系統中並不能使得塊裝置可用,而且其中並沒有涉及到我們提到的塊裝置的請求佇列,排程器等內容。塊裝置的這些複雜特性是由大量複雜的資料結構來實現的。

3.1 塊裝置

塊裝置的核心資料結構是struct block_device,其定義如下:
struct block_device {
	dev_t			bd_dev;  /* not a kdev_t - it's a search key */
	int			bd_openers;
	struct inode *		bd_inode;	/* will die */
	struct super_block *	bd_super;
	struct mutex		bd_mutex;	/* open/close mutex */
	struct list_head	bd_inodes;
	void *			bd_claiming;
	void *			bd_holder;
	int			bd_holders;
	bool			bd_write_holder;
#ifdef CONFIG_SYSFS
	struct list_head	bd_holder_disks;
#endif
	struct block_device *	bd_contains;
	unsigned		bd_block_size;
	struct hd_struct *	bd_part;
	/* number of times partitions within this device have been opened. */
	unsigned		bd_part_count;
	int			bd_invalidated;
	struct gendisk *	bd_disk;
	struct request_queue *  bd_queue;
	struct list_head	bd_list;
	/*
	 * Private data.  You must have bd_claim'ed the block_device
	 * to use this.  NOTE:  bd_claim allows an owner to claim
	 * the same device multiple times, the owner must take special
	 * care to not mess up bd_private for that case.
	 */
	unsigned long		bd_private;


	/* The counter of freeze processes */
	int			bd_fsfreeze_count;
	/* Mutex for freeze */
	struct mutex		bd_fsfreeze_mutex;
};
  • bd_dev:裝置號
  • bd_openers:裝置開啟次數
  • bd_inode:該裝置的inode
  • bd_super:指向該裝置所在檔案系統的超級塊,即bdev的超級塊
  • bd_mutex:互斥鎖
  • bd_inodes:連結串列頭,該連結串列包含了表示該塊裝置的所有裝置檔案的inode
  • bd_claiming:申請獲取裝置者
  • bd_holder:當前持有裝置者
  • bd_holders:裝置有多少個持有者
  • bd_write_holder:是否是寫持有
  • bd_contains:該裝置所屬的塊裝置,如果該裝置就表示整個塊裝置,則它為NULL。
  • bd_block_size:該裝置的塊大小
  • bd_part:指向該塊裝置的hd_struct
  • bd_part_count:該塊裝置上的分割槽被引用的次數,如果不為0,則不能重新掃描分割槽,因為分割槽正被使用
  • bd_invalidated:該裝置上分割槽是否有效,1表示無效,如果分割槽無效,則下次開啟時會重新掃描分割槽表
  • bd_disk:指向該裝置所對應的gendisk
  • bd_queue:該裝置對應的請求佇列
  • bd_list:用於將所有的塊裝置新增到all_bdevs中
  • bd_private:給裝置的當前持有者使用的私有資料結構

3.2 通用磁碟和分割槽資料結構

struct block_device用於向驅動程式呈現一個塊裝置,而另外一個數據結構struct gendisk則表示整個磁碟,一個磁碟可能包括很多個struct block_device型別的塊裝置,其定義如下:
struct gendisk {
	/* major, first_minor and minors are input parameters only,
	 * don't use directly.  Use disk_devt() and disk_max_parts().
	 */
	int major;			/* major number of driver */
	int first_minor;
	int minors;                     /* maximum number of minors, =1 for disks that can't be partitioned. */


	char disk_name[DISK_NAME_LEN];	/* name of major driver */
	char *(*devnode)(struct gendisk *gd, umode_t *mode);


	unsigned int events;		/* supported events */
	unsigned int async_events;	/* async events, subset of all */


	/* Array of pointers to partitions indexed by partno.
	 * Protected with matching bdev lock but stat and other
	 * non-critical accesses use RCU.  Always access through
	 * helpers.
	 */
	struct disk_part_tbl __rcu *part_tbl;
	struct hd_struct part0;


	const struct block_device_operations *fops;
	struct request_queue *queue;
	void *private_data;


	int flags;
	struct device *driverfs_dev;  // FIXME: remove
	struct kobject *slave_dir;


	struct timer_rand_state *random;
	atomic_unchecked_t sync_io;	/* RAID */
	struct disk_events *ev;
#ifdef  CONFIG_BLK_DEV_INTEGRITY
	struct blk_integrity *integrity;
#endif
	int node_id;
};
  • major:磁碟的主裝置號
  • first_minor:磁碟的第一個次裝置號
  • minors:磁碟的最大次裝置號數目,如果為1,則磁碟不能分割槽
  • disk_name:磁碟名字
  • devnode:獲取裝置的devnode
  • events:支援的事件
  • part_tbl:一個數組,包含了該磁碟的所有分割槽
  • part0:該磁碟的第0個分割槽。實際上它不代表真正的分割槽,它代表整個磁碟(或許可以稱為主裝置),分割槽陣列的0號元素指向了它,當磁碟包含分割槽時,分割槽在分割槽陣列中的下標從1開始。
  • fops:該磁碟的操作函式集
  • queue:用於佇列管理
  • flags:磁碟的標誌,表示磁碟的狀態
  • private_data:磁碟的私有資料
  • driverfs_dev:表示該磁碟所屬的device
  • slave_dir:用於在sys檔案系統中建立一個該磁碟檔案的slaves目錄。
  • ev:用於檢測磁碟的事件
  • node_id:該資料結構所使用的NUMA節點
分割槽資料結構struct hd_struct定義如下
struct hd_struct {
	sector_t start_sect;
	sector_t nr_sects;
	sector_t alignment_offset;
	unsigned int discard_alignment;
	struct device __dev;
	struct kobject *holder_dir;
	int policy, partno;
	struct partition_meta_info *info;
#ifdef CONFIG_FAIL_MAKE_REQUEST
	int make_it_fail;
#endif
	unsigned long stamp;
	atomic_t in_flight[2];
#ifdef	CONFIG_SMP
	struct disk_stats __percpu *dkstats;
#else
	struct disk_stats dkstats;
#endif
	atomic_t ref;
	struct rcu_head rcu_head;
};
其中關鍵資料域的含義如下:
  • start_sect:起始扇區號
  • nr_sects:該分割槽扇區數目。分割槽0的該域儲存的是整個磁碟的扇區數目,也就是磁碟的容量。
  • __dev:該分割槽所對應的裝置資料結構
  • holder_dir:指向分割槽所在的父裝置的kobject
  • ref:分割槽的引用計數
struct gendisk的例項不能由驅動程式分配,必須使用API alloc_disk來分配,該API完成struct gendisk資料結構的分配和初始化,並呼叫裝置模型的API device_initialize完成裝置資料結構的初始化。在使用完後,必須使用del_gendisk來釋放它。

3.3 塊裝置、通用磁碟以及分割槽資料結構之間的關係

這三者的關係如圖所示


  • 對於一個磁碟其上的每一個分割槽都對應一個struct block_device資料結構,分割槽的struct block_device會通過bd_contains指向整個磁碟的struct block_device
  • 每一個磁碟只有唯一的一個struct gendisk結構
  • 磁碟上的所有的struct block_device都通過bd_disk指向表示磁碟的struct gendisk結構
  • 磁碟的struct gendisk的part指向一個struct hd_struct的指標陣列,每個陣列項對應一個分割槽的資訊
  • 如果一個struct block_device表示的是分割槽,則它的bd_part指向對應的分割槽資料結構。
分割槽資訊中的__dev以及holder_dir將磁碟的分層資訊呈現到了kobject中。

3.4 塊裝置操作

字元裝置使用了file_operations作為底層所使用的操作函式集,但是塊裝置不再使用該結構,塊裝置使用的資料結構是struct block_device_operations,其定義如下:
struct block_device_operations {
	int (*open) (struct block_device *, fmode_t);
	int (*release) (struct gendisk *, fmode_t);
	int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
	int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);
	int (*direct_access) (struct block_device *, sector_t,
						void **, unsigned long *);
	unsigned int (*check_events) (struct gendisk *disk,
				      unsigned int clearing);
	/* ->media_changed() is DEPRECATED, use ->check_events() instead */
	int (*media_changed) (struct gendisk *);
	void (*unlock_native_capacity) (struct gendisk *);
	int (*revalidate_disk) (struct gendisk *);
	int (*getgeo)(struct block_device *, struct hd_geometry *);
	/* this callback is with swap_lock and sometimes page table lock held */
	void (*swap_slot_free_notify) (struct block_device *, unsigned long);
	struct module *owner;
} __do_const;
其中特殊的api的含義如下
  • direct_access:直接訪問裝置
  • check_events:檢查是否有事件發生
  • media_changed:當介質改變時呼叫,正如註釋所說,建議不再使用更改API,而是使用check_events
  • unlock_native_capacity:解除本地的容量限制,用於支援超過EOD的訪問(裝置可以不提供該功能)。
  • revalidate_disk:使裝置重新生效
  • getgeo:獲取裝置的物理資訊
通用磁碟資料結構中包含了該資料結構的例項。

3.5 請求佇列

請求佇列使用資料結構struct request_queue來表示,其定義如下
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;
	unprep_rq_fn		*unprep_rq_fn;
	merge_bvec_fn		*merge_bvec_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;


	/*
	 * Delayed queue handling
	 */
	struct delayed_work	delay_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;


	/*
	 * various queue flags, see QUEUE_* below
	 */
	unsigned long		queue_flags;


	/*
	 * ida allocated id for this queue.  Used to index queues from
	 * ioctx.
	 */
	int			id;


	/*
	 * queue needs bounce pages for pages above this limit
	 */
	gfp_t			bounce_gfp;


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


	unsigned int		dma_drain_size;
	void			*dma_drain_buffer;
	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 list_head	icq_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
	/*
	 * for flush operations
	 */
	unsigned int		flush_flags;
	unsigned int		flush_not_queueable:1;
	unsigned int		flush_queue_delayed:1;
	unsigned int		flush_pending_idx:1;
	unsigned int		flush_running_idx:1;
	unsigned long		flush_pending_since;
	struct list_head	flush_queue[2];
	struct list_head	flush_data_in_flight;
	struct request		flush_rq;


	struct mutex		sysfs_lock;


#if defined(CONFIG_BLK_DEV_BSG)
	bsg_job_fn		*bsg_job_fn;
	int			bsg_job_size;
	struct bsg_class_device bsg_dev;
#endif


#ifdef CONFIG_BLK_DEV_THROTTLING
	/* Throttle data */
	struct throtl_data *td;
#endif
};
該資料結構比較龐大,其主要成員及其含義如下:
  • queue_head:將該佇列上的請求連線到一起的連結串列。連結串列上的每個元素都是一個struct request型別的結構,代表一個讀寫請求。
  • elevator:指向該佇列使用的排程演算法。該排程演算法用於對請求佇列上的請求進行重排、優化以得到最好的效能。
  • rq:struct request的快取,分配和釋放struct request時都通過它進行
  • request_fn:請求處理函式。當核心期望驅動程式執行某些動作時,比如寫資料到裝置或者從裝置讀取資料時,核心會自動呼叫該函式。因此驅動程式必須提供該函式,它是塊驅動框架和裝置的介面。
  • make_request_fn:建立新請求。核心提供有該函式的預設版本,在預設版本中,核心會向請求佇列新增請求,如果佇列中有足夠多的請求,則就呼叫request_fn來處理請求。如果不想使用核心提供的預設實現,驅動開發者就要自己實現(這是可能的,因為驅動開發者更瞭解自己的硬體是如何工作的)。blk_queue_make_request用於設定佇列的建立新請求函式。
  • prep_rq_fn:請求預備函式。大多數驅動不適用該功能,而是將它設定為NULL。如果實現了該函式,則它的功能應該是在發出請求之前預先準備好一個請求。blk_queue_prep_rq用於設定佇列的請求預備函式。
  • unprep_rq_fn:取消請求的準備,在請求被處理完成時可能會被呼叫。如果在請求預備函式中分配了一些資源,這是一個釋放的好地方。blk_queue_unprep_rq用於設定請求佇列的該函式。
  • merge_bvec_fn:用於確定一個現存的請求是否允許新增更多的資料。由於請求佇列的長度是有限的,因而提供該檢測可以在佇列已滿時用於檢測是否可以往已存請求新增資料,如果可以,則就可以新增新的資料到已存請求中。blk_queue_merge_bvec用於設定請求佇列的該函式
  • softirq_done_fn:當使用軟中斷非同步完成請求時用於通知驅動程式請求已經完成
  • rq_timed_out_fn:當請求超時時執行的函式
  • dma_drain_needed:判斷dma是否被耗光,如果是,則返回非0(fn which returns non-zero if drain is necessary)
  • lld_busy_fn:當裝置忙時呼叫該函式
  • queue_flags:佇列的狀態標誌
  • nr_requests:請求佇列上可以新增的最大請求數目
  • queue_limits:包含了請求佇列的各種限制,比如硬體的扇區大小
  • kobj:請求佇列的kobject
該資料結構的其它一些域的含義參見注釋及其名字。
API blk_init_queue用於初始化一個請求佇列,它的原型如下:
struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);
  • rfn:驅動必須提供的請求處理函式。
  • lock:請求佇列的自旋鎖
該函式會完成
  • 請求佇列的建立和一些域的初始化。其中佇列的kobject的kobj_type被設定為blk_queue_ktype,最終佇列會在/sys下的裝置目錄下建立一個子目錄queue(在blk_register_queue中)
  • 初始化佇列的請求處理函式的初始化,最主要的是將request_fn設定為驅動提供的函式
  • 將佇列的make_request_fn設定為blk_queue_bio
  • 初始化佇列的超時處理函式為blk_rq_timed_out_timer
blk_init_queue相當於先呼叫了blk_alloc_queue_node,後呼叫了blk_queue_make_request