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

塊裝置驅動之二

一、將塊裝置新增到系統

register_blkdev並沒有真正將裝置新增到系統中,想要將裝置新增到系統中,需要使用如下API:
void blk_register_region(dev_t devt, unsigned long range, struct module *module,
			 struct kobject *(*probe)(dev_t, int *, void *),
			 int (*lock)(dev_t, void *), void *data)
該函式會將塊裝置新增到bdev_map中,這是一個由核心維護的資料庫,包含了系統所有的塊裝置。在開啟塊裝置時,必然會呼叫blkdev_get,而blkdev_get會查詢該資料庫來獲取塊裝置,這個過程類似於字元裝置,字元裝置在核心中也有一個數據庫cdev_map,在開啟字元裝置時會查詢cdev_map。不過很少需要直接呼叫該函式,add_disk會自動呼叫該函式。

1.1 新增磁碟和分割槽到系統中

為了將一個磁碟新增到系統中,對系統可用,必須初始化磁碟資料結構並呼叫add_disk方法。需要特別注意的是一旦呼叫了add_disk,磁碟就被“啟用”了,系統隨時都可能會呼叫該磁碟提供的各種方法,甚至在該函式返回之前就會呼叫,因而在完成磁碟結構的初始化之前,不要呼叫add_disk。add_disk的原型如下:
void add_disk(struct gendisk *disk);
它完成的工作主要包括:
  • 根據磁碟的主次裝置號資訊為磁碟分配裝置號
  • 呼叫disk_alloc_events初始化磁碟的事件(alloc|add|del|release)處理機制。在最開始磁碟事件會被設定為被阻塞的。
  • 呼叫bdi_register_dev將磁碟註冊到bdi
  • 呼叫blk_register_region將磁碟新增到bdev_map中
  • 呼叫register_disk將磁碟新增到系統中。主要完成
    • 將主裝置的分割槽(第0個分割槽)資訊標記設定為分割槽無效
    • 呼叫device_add將裝置新增到系統中
    • 在sys檔案系統中為裝置及其屬性建立目錄及檔案
    • 發出裝置新增到系統的uevent事件(如果能獲取分割槽的資訊,則也為分割槽傳送uevent事件)。
  • 呼叫blk_register_queue註冊磁碟的請求佇列。主要是為佇列和佇列的排程器在裝置的sys檔案系統目錄中建立相應的sys目錄/檔案,並且發出uevent事件。
  • 呼叫__disk_unblock_events完成
    • 在/sys檔案系統的裝置目錄下建立磁碟的事件屬性檔案
    • 將磁碟事件新增到全域性連結串列disk_events中
    • 解除對磁碟事件的阻塞。
在使用alloc_disk分配分割槽資料結構時,該函式只建立了第一個分割槽的資料結構,磁碟分割槽表中也只包含了一個分割槽資料結構。
當掃描到一個分割槽時,需要將它新增到磁碟中,這是通過以下API實現的:
struct hd_struct *add_partition(struct gendisk *disk, int partno,
				sector_t start, sector_t len, int flags,
				struct partition_meta_info *info);
  • disk:分割槽所屬的磁碟
  • partno:分割槽在磁碟的分割槽號
  • start:起始扇區號
  • len:該分割槽包括多少個扇區
  • flags:該扇區的標誌
  • info:該分割槽的partition_meta_info資訊
該函式的工作主要包括:
  • 擴充套件磁碟的分割槽表
  • 分配分割槽資料結構並進行初始化
  • 呼叫device_initialize初始化分割槽的裝置資料結構
  • 設定分割槽的裝置號
  • 呼叫device_add將分割槽新增到系統中
  • 建立分割槽裝置相關的sys檔案系統檔案
  • 傳送新增分割槽的uevent事件
  • 初始化分割槽的引用計數
磁碟的事件處理函式為disk_events_workfn,它會檢測磁碟是否有事件發生,並在有事件發生且需要處理時傳送uevent事件到使用者空間。disk_events_workfn是基於定時器實現的,如果磁碟支援check_events才會被啟動。

二、塊裝置操作

2.1 開啟塊裝置

在所有的檔案系統的實現中,在獲取檔案的inode時,對於不是常規檔案、目錄檔案、連線檔案的特殊檔案都會呼叫init_special_inode,該函式的程式碼在學習字元裝置時已經貼出來過,對於塊裝置檔案,該函式會將inode的檔案操作函式結構設定為def_blk_fops,其中的開啟檔案函式為blkdev_open。其原型為:
int blkdev_open(struct inode * inode, struct file * filp);
引數的含義很明顯。它完成的工作有:
  • 呼叫bd_acquire獲取塊裝置檔案的block_device結構。該函式會呼叫bdget嘗試從bdev檔案系統中查詢裝置檔案對應的inode,如果有就直接返回,如果沒有會分配一個新的inode並且初始化該inode再返回。裝置檔案的inode會被新增到block_device的bd_inodes連結串列中。塊裝置對應的block_device也會在這一步被新增到全域性的all_bdevs中。
  • 設定file結構的f_mapping為bdev->bd_inode->i_mapping。bdev->bd_inode在inode的建立和初始化中北初始化,具體的函式為alloc_inode和bdget。其中的address_space_operations被設定為def_blk_aops,這是後續要用到的函式,這是和裝置互動的介面。
  • 呼叫blkdev_get。該函式最主要的工作時完成塊裝置的開啟動作,同時根據傳入的模式還可能宣告裝置的持有者。
blkdev_get開啟裝置的動作由__blkdev_get完成,該函式的動作:
  •  呼叫get_gendisk獲取塊裝置所對應的通用磁碟結構,這裡可能需要查詢bdev_map資料庫。
  •  阻塞磁碟的事件處理
  •  如果是第一次開啟該塊裝置,則
    • 填充塊裝置資料結構的bd_disk,bd_queue,bd_contains(它會設定為自身)
    • 如果是主裝置(即不是分割槽),則
      • 設定塊裝置資料結構的bd_part
      • 如果提供了disk->fops->open,則呼叫它
      • 如果分割槽無效,則呼叫rescan_partitions重新掃描分割槽
      • 如果開啟裝置時返回了ENOMEDIUM錯誤,則呼叫invalidate_partitions將所有分割槽設定為無效
    • 否則,如果是分割槽裝置,則
      • 獲取主裝置的塊裝置資料結構
      • 遞迴呼叫__blkdev_get,但是這次傳入的是主裝置的塊裝置資料結構。本次呼叫會走第一次開啟裝置並且是主裝置的分支,由於是第一次開啟,因而分割槽資訊應該是無效的,這就會走到重新掃描分割槽的分支。
      • 設定塊裝置資料結構的bd_contains(它被設定為主裝置的block_device),bd_part
      • 呼叫bd_set_size設定分割槽的大小資訊
  • 否則如果不是第一次開啟裝置,則
    • 如果是主裝置(這裡是通過bdev->bd_contains == bdev判斷的,因為根據該函式的前邊流程,只有主裝置的這個條件才能成立),則
      • 如果提供了disk->fops->open,則呼叫它
      • 如果分割槽無效,則呼叫rescan_partitions重新掃描分割槽
      • 如果開啟裝置時返回了ENOMEDIUM錯誤,則呼叫invalidate_partitions將所有分割槽設定為無效
  • 增加裝置的開啟計數
  • 解除對裝置事件的阻塞
從開啟的細節也可以看到,blkdev_open確實會呼叫驅動所提供的open函式,驅動可以在open中完成開啟裝置的必要工作。在開啟之後裝置就可以被使用了。 

2.2 讀寫操作

在開啟塊裝置檔案後,塊裝置檔案的操作函式集也被設定為def_blk_fops,隨後即可用其中的函式進行讀寫。其讀函式為do_sync_read,寫函式為do_sync_write,但是它們最終分別呼叫generic_file_aio_read和blkdev_aio_write來完成實際的讀寫操作。
generic_file_aio_read的原型為:
ssize_t generic_file_aio_read(struct kiocb *iocb, const struct iovec *iov, unsigned long nr_segs, loff_t pos);
  • iocb:核心I/O控制塊
  • iov:I/O請求向量
  • nr_segs:I/O請求向量中有多少個請求
  • pos:當前檔案位置
其處理流程為:
  • 如果是直接IO,則呼叫filp->f_mapping->a_ops->direct_IO進行直接IO。在open時已經將filp->f_mapping->a_ops設定def_blk_aops了。
  • 對於請求向量中的每一個請求,建立一個read_descriptor_t並呼叫do_generic_file_read進行處理
blkdev_aio_write的原型為:
ssize_t blkdev_aio_write(struct kiocb *iocb, const struct iovec *iov, unsigned long nr_segs, loff_t pos);
其引數和讀的類似,其處理流程為:
  • 呼叫__generic_file_aio_write進行處理。該函式也會分別對待直接IO和常規的寫。流程和讀類似。

無論是讀寫都是和緩衝區互動,緩衝區位於檔案資料結構的struct address_space型別的變數f_mapping中,並以radix樹的形式被管理。核心在合適的時機會向裝置發起實際的IO操作,這是通過檔案資料結構的struct address_space型別的成員變數f_mapping中的address_space_operations型別的成員中的函式來實現的,在開啟塊裝置檔案時,該成員被設定為了def_blk_aops。對於讀會呼叫該地址空間操作集的readpage(對於塊裝置readpage成員函式為blkdev_readpage)成員函式或者其它讀成員函式,對於寫會呼叫該地址空間操作集的writepage(對於塊裝置為blkdev_writepage)成員函式或者其它相關成員函式函式。在def_blk_aops提供的這些函式中會將讀寫轉變成IO請求提交給裝置,到了此時才真正是要和裝置進行資料交換。

因此對於塊裝置來說,使用者是和緩衝區互動(直接IO除外),而核心負責在合適的時機完成緩衝區和裝置之間的互動。操作緩衝區的函式,緩衝區本身,以及緩衝區與裝置之間的互動方式都儲存在file結構中。

2.3 請求結構

當核心通過address_space_operations中的成員函式向裝置發起讀寫操作時,讀寫操作都會被轉變成一個對塊裝置的IO請求提交給裝置。核心使用資料結構struct bio來表示一個對塊裝置的IO,其定義如下:
struct bio {
	sector_t		bi_sector;	/* device address in 512 byte sectors */
	struct bio		*bi_next;	/* request queue link */
	struct block_device	*bi_bdev;
	unsigned long		bi_flags;	/* status, command, etc */
	unsigned long		bi_rw;		/* bottom bits READ/WRITE,
						 * top bits priority
						 */


	unsigned short		bi_vcnt;	/* how many bio_vec's */
	unsigned short		bi_idx;		/* current index into bvl_vec */


	/* Number of segments in this BIO after
	 * physical address coalescing is performed.
	 */
	unsigned int		bi_phys_segments;
	unsigned int		bi_size;	/* residual I/O count */


	/*
	 * To keep track of the max segment size, we account for the
	 * sizes of the first and last mergeable segments in this bio.
	 */
	unsigned int		bi_seg_front_size;
	unsigned int		bi_seg_back_size;


	unsigned int		bi_max_vecs;	/* max bvl_vecs we can hold */
	atomic_t		bi_cnt;		/* pin count */
	struct bio_vec		*bi_io_vec;	/* the actual vec list */
	bio_end_io_t		*bi_end_io;


	void			*bi_private;
#if defined(CONFIG_BLK_DEV_INTEGRITY)
	struct bio_integrity_payload *bi_integrity;  /* data integrity */
#endif


	bio_destructor_t	*bi_destructor;	/* destructor */


	/*
	 * We can inline a number of vecs at the end of the bio, to avoid
	 * double allocations for a small number of bio_vecs. This member
	 * MUST obviously be kept at the very end of the bio.
	 */
	struct bio_vec		bi_inline_vecs[0];
};
關鍵域及其含義:
  • bi_sector:傳輸開始的扇區號
  • bi_next:將與一個請求相關的bio連線到同一個連結串列中
  • bi_bdev:與請求相關聯的裝置的資料結構
  • bi_phys_segments:在經過合併之後,該BIO所對應的的段數目
  • bi_size:該BIO涉及到的資料的長度
  • bi_io_vec:指向了包含了實際的IO資料結構的陣列。
  • bi_end_io:當IO完成時,將被呼叫用於完成此次IO
  • bi_destructor:解構函式,當從記憶體刪除一個BIO結構時被呼叫
其它各個域的含義見其註釋(核心的資料結構大多都有良好的註釋,可以參考程式碼本身,程式碼也是最能說明問題的)。
bi_io_vec的每個陣列項都指向一個記憶體頁,這個記憶體頁用於從裝置讀取資料或者向裝置傳輸資料。這些記憶體頁可以是連續的也可以不是連續的。其結構如圖所示:


BIO是核心用於表示一個IO請求的結構,它會被提交給裝置,當需要和裝置互動時,核心會先準備BIO結構,然後通過目標裝置的請求佇列上的make_request_fn函式將BIO轉變成一個請求,核心使用資料結構struct request來表示一個對塊裝置的請求,其資料結構定義如下:
struct request {
	struct list_head queuelist;
	struct call_single_data csd;


	struct request_queue *q;


	unsigned int cmd_flags;
	enum rq_cmd_type_bits cmd_type;
	unsigned long atomic_flags;


	int cpu;


	/* the following two fields are internal, NEVER access directly */
	unsigned int __data_len;	/* total data len */
	sector_t __sector;		/* sector cursor */


	struct bio *bio;
	struct bio *biotail;


	struct hlist_node hash;	/* merge hash */
	/*
	 * The rb_node is only used inside the io scheduler, requests
	 * are pruned when moved to the dispatch queue. So let the
	 * completion_data share space with the rb_node.
	 */
	union {
		struct rb_node rb_node;	/* sort/lookup */
		void *completion_data;
	};


	/*
	 * Three pointers are available for the IO schedulers, if they need
	 * more they have to dynamically allocate it.  Flush requests are
	 * never put on the IO scheduler. So let the flush fields share
	 * space with the elevator data.
	 */
	union {
		struct {
			struct io_cq		*icq;
			void			*priv[2];
		} elv;


		struct {
			unsigned int		seq;
			struct list_head	list;
			rq_end_io_fn		*saved_end_io;
		} flush;
	};


	struct gendisk *rq_disk;
	struct hd_struct *part;
	unsigned long start_time;
#ifdef CONFIG_BLK_CGROUP
	unsigned long long start_time_ns;
	unsigned long long io_start_time_ns;    /* when passed to hardware */
#endif
	/* Number of scatter-gather DMA addr+len pairs after
	 * physical address coalescing is performed.
	 */
	unsigned short nr_phys_segments;
#if defined(CONFIG_BLK_DEV_INTEGRITY)
	unsigned short nr_integrity_segments;
#endif


	unsigned short ioprio;


	int ref_count;


	void *special;		/* opaque pointer available for LLD use */
	char *buffer;		/* kaddr of the current segment if available */


	int tag;
	int errors;


	/*
	 * when request is used as a packet command carrier
	 */
	unsigned char __cmd[BLK_MAX_CDB];
	unsigned char *cmd;
	unsigned short cmd_len;


	unsigned int extra_len;	/* length of alignment and padding */
	unsigned int sense_len;
	unsigned int resid_len;	/* residual count */
	void *sense;


	unsigned long deadline;
	struct list_head timeout_list;
	unsigned int timeout;
	int retries;


	/*
	 * completion callback.
	 */
	rq_end_io_fn *end_io;
	void *end_io_data;


	/* for bidi */
	struct request *next_rq;
};
  • queuelist:用於將請求連線到請求佇列上
  • q:請求所屬的請求佇列
  • cmd_flags:請求的標誌
  • cmd_type:請求的型別
  • bio:該請求的多個bio中當前正被處理的bio
  • biotail:該請求的最後一個bio。一個請求上的所有BIO會儲存在一個連結串列中。
  • __data_len:請求所涉及到的資料的總長度
  • __sector:扇區遊標
  • elv:IO排程器相關資訊。
  • rq_disk:請求對應的磁碟
  • part:請求所對應的磁碟分割槽
  • end_io:該請求被完成時被呼叫,用於完成該請求
  • end_io_data:回撥end_io時的引數
由這兩個資料結構可以看出,由於塊的讀寫請求是由裝置非同步完成的,因而都提供了一個用於通知IO完成的函式指標。每個請求還包含有排程器相關的資訊,排程器決定了BIO如何被處理,BIO可能被排程器合併、重排以獲得最優效能。
請求所支援的標誌及其含義如下:
enum rq_flag_bits {
	/* common flags */
	__REQ_WRITE,		/* not set, read. set, write */
	__REQ_FAILFAST_DEV,	/* no driver retries of device errors */
	__REQ_FAILFAST_TRANSPORT, /* no driver retries of transport errors */
	__REQ_FAILFAST_DRIVER,	/* no driver retries of driver errors */


	__REQ_SYNC,		/* request is sync (sync write or read) */
	__REQ_META,		/* metadata io request */
	__REQ_PRIO,		/* boost priority in cfq */
	__REQ_DISCARD,		/* request to discard sectors */
	__REQ_SECURE,		/* secure discard (used with __REQ_DISCARD) */


	__REQ_NOIDLE,		/* don't anticipate more IO after this one */
	__REQ_FUA,		/* forced unit access */
	__REQ_FLUSH,		/* request for cache flush */


	/* bio only flags */
	__REQ_RAHEAD,		/* read ahead, can fail anytime */
	__REQ_THROTTLED,	/* This bio has already been subjected to
				 * throttling rules. Don't do it again. */


	/* request only flags */
	__REQ_SORTED,		/* elevator knows about this request */
	__REQ_SOFTBARRIER,	/* may not be passed by ioscheduler */
	__REQ_NOMERGE,		/* don't touch this for merging */
	__REQ_STARTED,		/* drive already may have started this one */
	__REQ_DONTPREP,		/* don't call prep for this one */
	__REQ_QUEUED,		/* uses queueing */
	__REQ_ELVPRIV,		/* elevator private data attached */
	__REQ_FAILED,		/* set if the request failed */
	__REQ_QUIET,		/* don't worry about errors */
	__REQ_PREEMPT,		/* set for "ide_preempt" requests */
	__REQ_ALLOCED,		/* request came from our alloc pool */
	__REQ_COPY_USER,	/* contains copies of user pages */
	__REQ_FLUSH_SEQ,	/* request for flush sequence */
	__REQ_IO_STAT,		/* account I/O stat */
	__REQ_MIXED_MERGE,	/* merge of different types, fail separately */
	__REQ_NR_BITS,		/* stops here */
};
請求的型別及其含義如下:
enum rq_cmd_type_bits {
	REQ_TYPE_FS		= 1,	/* fs request */
	REQ_TYPE_BLOCK_PC,		/* scsi command */
	REQ_TYPE_SENSE,			/* sense request */
	REQ_TYPE_PM_SUSPEND,		/* suspend request */
	REQ_TYPE_PM_RESUME,		/* resume request */
	REQ_TYPE_PM_SHUTDOWN,		/* shutdown request */
	REQ_TYPE_SPECIAL,		/* driver defined type */
	/*
	 * for ATA/ATAPI devices. this really doesn't belong here, ide should
	 * use REQ_TYPE_SPECIAL and use rq->cmd[0] with the range of driver
	 * private REQ_LB opcodes to differentiate what type of request this is
	 */
	REQ_TYPE_ATA_TASKFILE,
	REQ_TYPE_ATA_PC,
};

2.4 提交請求

當核心需要和裝置進行互動時,它都會首先準備相關的bio,然後呼叫submit_bio將bio提交給裝置。該函式最終會呼叫裝置相關連的請求佇列上的make_request_fn函式將BIO轉變成一個請求,其處理邏輯很簡單:
  1. 更新統計資訊
  2. 呼叫generic_make_request提交bio
generic_make_request的處理過程:
  1.  做合法性檢查
  2.  如果current->bio_list不為NULL,則將新的bio新增到current->bio_list上並返回
  3.  將current->bio_list 初始化為 &bio_list_on_stack
  4.  獲取所請求裝置的請求佇列
  5.  呼叫請求佇列上的make_request_fn產生一個請求
  6.  如果current->bio_list不為空,就回到第四步
  7.  將current->bio_list設定為NULL
該函式通過將current->bio_list 視作一個標記保證了任意時刻只有一個make_request_fn在執行,新的generic_make_request請求所傳遞的bio會被新增到連結串列中在之後被轉換成一個請求。
如果沒有修改過佇列的make_request_fn,則它使用核心提供的預設版本blk_queue_bio。
blk_queue_bio的大致處理流程:
  1.  呼叫blk_queue_bounce進行一些特殊處理(如果底層驅動表示它想要將在某個限制之上的頁地址回彈到低地址)
  2.  呼叫attempt_plug_merge嘗試將新的請求同已經被plugged的請求進行合併,已經被plugged的請求會被儲存在current->plug連結串列中
  3.  呼叫elv_merge判斷新的bio是否可以同請求佇列上已經存在的請求的bio進行合併,如果可以合併,就進行合併。這裡的是否可以合併以及如何合併都取決於所採用的IO排程演算法。
  4.  走到這一步就說明無法進行合併,開始建立一個新的請求,呼叫get_request_wait來獲取一個新的請求結構
  5.  呼叫init_request_from_bio來使用bio中的資料來初始化這個新的請求。
  6.  如果current->plug不空,則表示當前佇列是plug的,如果該連結串列上已經有足夠數目的請求,則呼叫blk_flush_plug_list進行處理(該函式會呼叫__elv_add_request將 請求新增到請求佇列,還可能呼叫queue_unplugged進行實際的請求處理),最後會將新的請求新增到current->plug上並更新統計資訊。
  7.  如果current->plug為空,則呼叫__blk_run_queue直接處理請求,這會呼叫請求佇列上的request_fn,也就是要求驅動必須提供的那個函式來進行請求的處理。
從其處理邏輯可以看到,這裡又對請求進行了一次緩衝,如果current->plug不空,則新的請求都會被新增到該連結串列上,只有請求的數目超過一定的值後才會被處理(被加入到請求佇列中或者更進一步的被實際處理掉)。顯然只有這裡的處理邏輯是不完善的,假如系統不是很忙,只有很少量的請求需要被處理,則這裡的處理條件可能很長時間都不能被滿足,這時就需要另外一個機制來觸發blk_flush_plug_list的動作,這個機制就是schedule,該機制的路徑如下: schedule->sched_submit_work->blk_schedule_flush_plug->blk_flush_plug_list
到了這一步,IO的讀寫已經被提交給了硬體,由驅動所提供的request_fn進行處理。