Linux裝置驅動--塊裝置 之概念和框架
塊裝置(blockdevice)
--- 是一種具有一定結構的隨機存取裝置,對這種裝置的讀寫是按塊進行的,他使用緩衝區來存放暫時的資料,待條件成熟後,從快取一次性寫入裝置或者從裝置一次性讀到緩衝區。
字元裝置(Character device)
---是一個順序的資料流裝置,對這種裝置的讀寫是按字元進行的,而且這些字元是連續地形成一個數據流。他不具備緩衝區,所以對這種裝置的讀寫是實時的。
扇區(Sectors):任何塊裝置硬體對資料處理的基本單位。通常,1個扇區的大小為512byte。(對裝置而言)
塊 (Blocks):由Linux制定對核心或檔案系統等資料處理的基本單位。通常,1個塊由1個或多個扇區組成。(對Linux作業系統而言)
段(Segments):由若干個相鄰的塊組成。是Linux記憶體管理機制中一個記憶體頁或者記憶體頁的一部分。
頁、段、塊、扇區之間的關係圖如下:
塊裝置驅動整體框架
塊裝置的應用在Linux中是一個完整的子系統。
在Linux中,驅動對塊裝置的輸入或輸出(I/O)操作,都會向塊裝置發出一個請求,在驅動中用request結構體描述。但對於一些磁碟裝置而言請求的速度很慢,這時候核心就提供一種佇列的機制把這些I/O請求新增到佇列中(即:請求佇列),在驅動中用request_queue結構體描述。在向塊裝置提交這些請求前核心會先執行請求的合併和排序預操作,以提高訪問的效率,然後再由核心中的I/O排程程式子系統
由通用塊層(Generic Block Layer)負責維持一個I/O請求在上層檔案系統與底層物理磁碟之間的關係。在通用塊層中,通常用一個bio結構體來對應一個I/O請求。
Linux提供了一個gendisk資料結構體,用來表示一個獨立的磁碟裝置或分割槽,用於對底層物理磁碟進行訪問。在gendisk中有一個類似字元裝置中file_operations的硬體操作結構指標,是block_device_operations結構體
當多個請求提交給塊裝置時,執行效率依賴於請求的順序。如果所有的請求是同一個方向(如:寫資料),執行效率是最大的。核心在呼叫塊裝置驅動程式例程處理請求之前,先收集I/O請求並將請求排序,然後,將連續扇區操作的多個請求進行合併以提高執行效率(核心演算法會自己做,不用你管),對I/O請求排序的演算法稱為電梯演算法(elevator algorithm)。電梯演算法在I/O排程層完成。核心提供了不同型別的電梯演算法,電梯演算法有
1 noop(實現簡單的FIFO,基本的直接合並與排序),
2 anticipatory(延遲I/O請求,進行臨界區的優化排序),
3 Deadline(針對anticipatory缺點進行改善,降低延遲時間),
4 Cfq(均勻分配I/O頻寬,公平機制)
PS:其實IO排程層(包括請求合併排序演算法)是不需要使用者管的,核心已經做好
相關資料結構
block_device: 描述一個分割槽或整個磁碟對核心的一個塊裝置例項
gendisk: 描述一個通用硬碟(generic hard disk)物件。
hd_struct: 描述分割槽應有的分割槽資訊
bio: 描述塊資料傳送時怎樣完成填充或讀取塊給driver
request: 描述向核心請求一個列表準備做佇列處理。
request_queue: 描述核心申請request資源建立請求連結串列並填寫BIO形成佇列。
上回最後面介紹了相關資料結構,下面再詳細介紹
塊裝置物件結構 block_device
核心用結構block_device例項代表一個塊裝置物件,如:整個硬碟或特定分割槽。如果該結構代表一個分割槽,則其成員bd_part指向裝置的分割槽結構。如果該結構代表裝置,則其成員bd_disk指向裝置的通用硬碟結構gendisk
當用戶開啟塊裝置檔案時,核心建立結構block_device例項,裝置驅動程式還將建立結構gendisk例項,分配請求佇列並註冊結構block_device例項。
塊裝置物件結構block_device列出如下(在include/linux/fs.h中)
- struct block_device {
- dev_t bd_dev; /* not a kdev_t - it's a search key */
- struct inode * bd_inode; /* 分割槽節點 */
- struct super_block * bd_super;
- int bd_openers;
- struct mutex bd_mutex;/* open/close mutex 開啟與關閉的互斥量*/
- struct semaphore bd_mount_sem; /*掛載操作訊號量*/
- struct list_head bd_inodes;
- void * bd_holder;
- int bd_holders;
- #ifdef CONFIG_SYSFS
- struct list_head bd_holder_list;
- #endif
- struct block_device * bd_contains;
- unsigned bd_block_size; /*分割槽塊大小*/
- struct hd_struct * bd_part;
- unsigned bd_part_count; /*開啟次數*/
- int bd_invalidated;
- struct gendisk * bd_disk; /*裝置為硬碟時,指向通用硬碟結構*/
- struct list_head bd_list;
- struct backing_dev_info *bd_inode_backing_dev_info;
- unsigned long bd_private;
- /* The counter of freeze processes */
- int bd_fsfreeze_count;
- /* Mutex for freeze */
- struct mutex bd_fsfreeze_mutex;
- };
通用硬碟結構 gendisk
結構體gendisk代表了一個通用硬碟(generic hard disk)物件,它儲存了一個硬碟的資訊,包括請求佇列、分割槽連結串列和塊裝置操作函式集等。塊裝置驅動程式分配結構gendisk例項,裝載分割槽表,分配請求佇列並填充結構的其他域。
支援分割槽的塊驅動程式必須包含 <linux/genhd.h> 標頭檔案,並宣告一個結構gendisk,核心還維護該結構例項的一個全域性連結串列gendisk_head,通過函式add_gendisk、del_gendisk和get_gendisk維護該連結串列。
結構gendisk列出如下(在include/linux/genhd.h中):
- struct gendisk {
- int major; /* 驅動程式的主裝置號 */
- int first_minor; /*第一個次裝置號*/
- int minors; /*次裝置號的最大數量,沒有分割槽的裝置,此值為1 */
- char disk_name[32]; /* 主裝置號驅動程式的名字*/
- struct hd_struct **part; /* 分割槽列表,由次裝置號排序 */
- struct block_device_operations *fops; /*塊裝置操作函式集*/
- struct request_queue *queue; /*請求佇列*/
- struct blk_scsi_cmd_filter cmd_filter;
- void *private_data; /*私有資料*/
- sector_t capacity; /* 函式set_capacity設定的容量,以扇區為單位*/
- int flags; /*設定驅動器狀態的標誌,如:可移動介質為
- GENHD_FL_REMOVABLE*/
- struct device dev; /*從裝置驅動模型基類結構device繼承*/
- struct kobject *holder_dir;
- struct kobject *slave_dir;
- struct timer_rand_state *random;
- int policy;
- atomic_t sync_io; /* RAID */
- unsigned long stamp;
- int in_flight;
- #ifdef CONFIG_SMP
- struct disk_stats *dkstats;
- #else
- /*硬碟統計資訊,如:讀或寫的扇區數、融合的扇區數、在請求佇列的時間等*/
- struct disk_stats dkstats;
- #endif
- struct work_struct async_notify;
- #ifdef CONFIG_BLK_DEV_INTEGRITY
- struct blk_integrity *integrity; /*用於資料完整性擴充套件*/
- #endif
- };
Linux核心提供了一組函式來操作gendisk,主要包括:
分配gendisk
struct gendisk *alloc_disk(int minors);
minors 引數是這個磁碟使用的次裝置號的數量,一般也就是磁碟分割槽的數量,此後minors不能被修改。
增加gendisk
gendisk結構體被分配之後,系統還不能使用這個磁碟,需要呼叫如下函式來註冊這個磁碟裝置:
void add_disk(struct gendisk *gd);
特別要注意的是對add_disk()的呼叫必須發生在驅動程式的初始化工作完成並能響應磁碟的請求之後。
釋放gendisk
當不再需要一個磁碟時,應當使用如下函式釋放gendisk:
void del_gendisk(struct gendisk *gd);
設定gendisk容量
void set_capacity(struct gendisk *disk, sector_t size);
塊裝置中最小的可定址單元是扇區,扇區大小一般是2的整數倍,最常見的大小是512位元組。扇區的大小是裝置的物理屬性,扇區是所有塊裝置的基本單元,塊裝置 無法對比它還小的單元進行定址和操作,不過許多塊裝置能夠一次就傳輸多個扇區。雖然大多數塊裝置的扇區大小都是512位元組,不過其它大小的扇區也很常見, 比如,很多CD-ROM盤的扇區都是2K大小。不管物理裝置的真實扇區大小是多少,核心與塊裝置驅動互動的扇區都以512位元組為單位。因此,set_capacity()函式也以512位元組為單位。
分割槽結構hd_struct代表了一個分割槽物件,它儲存了一個硬碟的一個分割槽的資訊,驅動程式初始化時,從硬碟的分割槽表中提取分割槽資訊,存放在分割槽結構例項中。
塊裝置操作函式集結構 block_device_operations
字元裝置通過 file_operations 操作結構使它們的操作對系統可用. 一個類似的結構用在塊裝置上是 struct block_device_operations,
定義在 <linux/fs.h>.
int (*open)(struct inode *inode, struct file *filp);
int (*release)(struct inode *inode, struct file *filp);
就像它們的字元驅動對等體一樣工作的函式; 無論何時裝置被開啟和關閉都呼叫它們. 一個字元驅動可能通過啟動裝置或者鎖住門(為可移出的介質)來響應一個 open 呼叫. 如果你將介質鎖入裝置, 你當然應當在 release 方法中解鎖.
int (*ioctl)(struct inode *inode, struct file *filp,
unsigned int cmd, unsigned long arg);
實現 ioctl 系統呼叫的方法. 但是, 塊層首先解釋大量的標準請求; 因此大部分的塊驅動 ioctl 方法相當短.
PS:在block_device_operations中沒有實際讀或寫資料的函式. 在塊 I/O 子系統, 這些操作由請求函式處理
請求結構request
結構request代表了掛起的I/O請求,每個請求用一個結構request例項描述,存放在請求佇列連結串列中,由電梯演算法進行排序,每個請求包含1個或多個結構bio例項
- struct request {
- //用於掛在請求佇列連結串列的節點,使用函式blkdev_dequeue_request訪問它,而不能直接訪
- 問
- struct list_head queuelist;
- struct list_head donelist; /*用於掛在已完成請求連結串列的節點*/
- struct request_queue *q; /*指向請求佇列*/
- unsigned int cmd_flags; /*命令標識*/
- enum rq_cmd_type_bits cmd_type; /*命令型別*/
- /*各種各樣的扇區計數*/
- /*為提交i/o維護bio橫斷面的狀態資訊,hard_*成員是塊層內部使用的,驅動程式不應該改變
- 它們*/
- sector_t sector; /*將提交的下一個扇區*/
- sector_t hard_sector; /* 將完成的下一個扇區*/
- unsigned long nr_sectors; /* 整個請求還需要傳送的扇區數*/
- unsigned long hard_nr_sectors; /* 將完成的扇區數*/
- /*在當前bio中還需要傳送的扇區數 */
- unsigned int current_nr_sectors;
- /*在當前段中將完成的扇區數*/
- unsigned int hard_cur_sectors;
- struct bio *bio; /*請求中第一個未完成操作的bio*、
- struct bio *biotail; /*請求連結串列中末尾的bio*、
- struct hlist_node hash; /*融合 hash */
- /* rb_node僅用在I/O排程器中,當請求被移到分發佇列中時,
- 請求將被刪除。因此,讓completion_data與rb_node分享空間*/
- union {
- struct rb_node rb_node; /* 排序/查詢*/
- void *completion_data;
- };
request結構體的主要成員包括:
sector_t hard_sector;
unsigned long hard_nr_sectors;
unsigned int hard_cur_sectors;
上述3個成員標識還未完成的扇區,hard_sector是第1個尚未傳輸的扇區,hard_nr_sectors是尚待完成的扇區數,hard_cur_sectors是並且當前I/O操作中待完成的扇區數。這些成員只用於核心塊裝置層,驅動不應當使用它們。
sector_t sector;
unsigned long nr_sectors;
unsigned int current_nr_sectors;
驅動中會經常與這3個成員打交道,這3個成員在核心和驅動互動中發揮著重大作用。它們以512位元組大小為1個扇區,如果硬體的扇區大小不是512位元組,則需要進行相應的調整。例如,如果硬體的扇區大小是2048位元組,則在進行硬體操作之前,需要用4來除起始扇區號。
hard_sector、hard_nr_sectors、hard_cur_sectors與sector、nr_sectors、current_nr_sectors之間可認為是“副本”關係。
struct bio *bio;
bio是這個請求中包含的bio結構體的連結串列,驅動中不宜直接存取這個成員,而應該使用後文將介紹的rq_for_each_bio()。
請求佇列結構request_queue
每個塊裝置都有一個請求佇列,每個請求佇列單獨執行I/O排程,請求佇列是由請求結構例項連結成的雙向連結串列,連結串列以及整個佇列的資訊用結構request_queue描述,稱為請求佇列物件結構或請求佇列結構。它存放了關於掛起請求的資訊以及管理請求佇列(如:電梯演算法)所需要的資訊。結構成員request_fn是來自裝置驅動程式的請求處理函式。
請求佇列結構request_queue列出如下(在/include/linux/blk_dev.h中)
太長了,此處略,其實也看不懂,- -#
Bio結構
通常1個bio對應1個I/O請求,IO排程演算法可將連續的bio合併成1個請求。所以,1個請求可以包含多個bio。
核心中塊I/O操作的基本容器由bio結構體表示,定義 在<linux/bio.h>中,該結構體代表了正在現場的(活動的)以片段(segment)連結串列形式組織的塊I/O操作。一個片段是一小 塊連續的記憶體緩衝區。這樣的好處就是不需要保證單個緩衝區一定要連續。所以通過片段來描述緩衝區,即使一個緩衝區分散在記憶體的多個位置上,bio結構體也 能對核心保證I/O操作的執行,這樣的就叫做聚散I/O.
bio為通用層的主要資料結構,既描述了磁碟的位置,又描述了記憶體的位置,是上層核心vfs與下層驅動的連線紐