系統中可以隨機訪問固定大小數據片(chunk)的設備被稱做塊設備,這些數據片就稱做塊。塊設備文件都是以安裝文件系統的方式使用,此也是塊設備一般的訪問方式。塊設備的訪問方式是隨機的,也就是能夠在訪問設備時,隨意的從一個位置跳轉到另外一個位置。塊設備的訪問位置必須可以在介質的不一樣區間先後移動。
因此事實上內核沒必要提供一個專門的子系統來管理字符設備,可是對塊設備的管理卻必需要有一個專門的提供服務的子系統。塊設備中,最小的可尋址單元是扇區。扇區大小通常是2的整數倍,而最多見的大小是512個字節。扇區的大小是設備的物理屬性,扇區是全部塊設備的基本單元——塊設備沒法對比它還小的單元進行 尋址和操做,許多塊設備可以一次就傳輸多個扇區。塊是文件系統的一種抽象——只能基於塊來訪問文件系統。雖然物理磁盤尋址是按照扇區級進行的,可是內核執 行的全部磁盤操做都是按照塊進行的。因爲扇區是設備的最小可尋址單元,因此塊不能比扇區還小,只能數倍於扇區大小。內核還要求塊大小是2的整數倍,並且不 能超過一個頁的長度。因此對塊大小的最重要求是:必須是扇區大小的2的整數倍,並且要小於頁面大小,因此一般大小是512字節,1K或者4K。
在linux2.5以前,當一個塊被調入內存時,要存儲在一個緩衝區中,每一個緩衝區與一個塊對應,至關因而磁盤塊在內存中的表示,因爲內核在處理數據時須要 一些相關控制信息,因此每個緩衝區都有一個對應的描述符。該描述符用buffer_head結構體表示,也稱做緩衝區頭。
struct buffer_head { html unsigned long b_state; //緩衝區狀態標誌
struct buffer_head *b_this_page; //頁面中的緩衝區
struct page *b_page; //存儲緩衝區的頁面
sector_t b_blocknr; //邏輯塊號
size_t b_size; //塊大小
char *b_data; //頁面中的緩衝區
struct block_device *b_bdev; //塊設備
bh_end_io_t *b_end_io; //I/O完成方法
void *b_private; //完成方法數據
struct list_head b_assoc_buffers; //相關映射鏈表 /* mapping this buffer is associated with */ struct address_space *b_assoc_map; node atomic_t b_count; //緩衝區使用計數
}; linux |
b_state域表示緩衝區的狀態,合法的標誌存放在bh_state_bits枚舉中,定義在
enum bh_state_bits {
BH_Uptodate,該緩衝區包含可用數據
BH_Dirty,該緩衝區是髒的(緩存中的內容比磁盤中的塊內容新,因此緩衝區內容必須被寫回磁盤)
BH_Lock,該緩衝區正被I/O操做使用,被鎖定以防被併發訪問
BH_Req,該緩衝區有I/O請求操做
BH_Uptodate_Lock,
BH_Mapped,該緩衝區是映射磁盤塊的可用緩衝區
BH_New,該緩衝區是經過get_block(0剛剛映射的,而且不能訪問
BH_Async_Read,該緩衝區正經過end_buffer_async_read()被異步I/O讀操做使用
BH_Async_Write,該緩衝區正經過end_buffer_async_write()被異步I/O寫操做使用
BH_Delay,該緩衝區還沒有和磁盤塊關聯
BH_Boundary,該緩衝區處於連續塊區的邊界——下一個塊再也不連續
BH_Write_EIO,
BH_Ordered,
BH_Eopnotsupp,
BH_Unwritten,
BH_PrivateStart,
};
驅動程序能夠在這些位中安全的定義本身的狀態標誌,只要保證自定義的狀態標誌不與塊I/O層的專用位發生衝突就能夠了。
而在b_count中,表示緩衝區的使用計數,則經過兩個函數來進行增減:
get_bh(struct buffer_head *bh)-->atomic_inc(&bh->b_count)
put_bh(struct buffer_head *bh)-->atomic_dec(&bh->b_count)
一個塊設備驅動程序主要經過傳輸固定大小的隨機數據來訪問設備。高效的塊設備驅動程序在性能上是嚴格要求的,並不只僅體如今用戶應用程序的讀寫操做中。現代 操做系統使用虛擬內存工做,把不須要的數據轉移到諸如磁盤等其餘存儲介質上,塊驅動程序是在覈心內存與其餘存儲介質之間的管道,所以它們能夠認爲是虛擬內 存子系統的組成部分。一個數據塊指定的是固定大小的數據,而大小的值由內核肯定,數據塊的大小一般是4096個字節,可是能夠根據體系結構和所使用的文件 系統進行改變。與數據塊對應的是扇區,它是由底層硬件決定大小的一個塊。內核所處理的設備扇區大小是512字節。若是用戶的設備使用了不一樣的大小,須要對 內核進行修改,以免產生硬件所不能處理的I/O請求。不管什麼時候內核爲用戶提供了一個扇區編號,該扇區的大小就是512字節。若是要使用不一樣的硬件扇區大小,用戶比對內核的扇區作相應的修改。一樣,此部分也是由很多數據結構與相應方法組成,下面先來看相關數據結構:
內核使用gendisk結構來表示一個獨立的磁盤設備。內核還使用gendisk結構表示分區,在此結構中,不少成員必須由驅動程序來進行初始化。此結構定義在
struct gendisk { int major; //主設備號
int first_minor; //第一個從設備號
int minors; /* 描述被磁盤使用的設備號的成員.一個驅動器必須使用最少一個次編號.若是你的驅動會是可分區的,可是(而且大部分應當是),你要分配一個次編號給每一個可能 的分區.次編號的一個普通的值是 16, 它容許"全磁盤"設備盒 15 個分區. 一些磁盤驅動使用 64 個次編號給每一個設備.*/
char disk_name[32]; //應當被設置爲磁盤驅動器名子的成員. 它出如今 /proc/partitions 和 sysfs.
struct hd_struct **part; /* [indexed by minor] */ struct block_device_operations *fops;// 設備操做集合.
struct request_queue *queue;//被內核用來管理這個設備的 I/O 請求的結構;
void *private_data;//塊驅動可以使用這個成員做爲一個指向它們本身內部數據的指針.
sector_t capacity; //這個驅動器的容量,以512-字節扇區來計.sector_t類型能夠是64位寬.驅動不該當直接設置這個成員;相反,傳遞扇區數目給set_capacity.
int flags; // 一套標誌(不多使用),描述驅動器的狀態.若是你的設備有可移出的介質,你應當設置GENHD_FL_REMOVABLE.CD-ROM驅動器可設置 GENHD_FL_CD. 若是, 因爲某些緣由, 你不須要分區信息出如今 /proc/partitions, 設置 GENHD_FL_SUPPRESS_PARTITIONS_INFO.
struct device *driverfs_dev; // FIXME: remove
struct device dev; 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; }; 數組 |
此結構是一個動態分配的結構。須要一些內核的特殊處理來進行初始化;驅動程序不能本身動態分配該結構,而是必須調用。
struct gendisk *alloc_disk(int minors);//參數是次設備號的數目。此後就沒法改變minors成員。動態分配該結構。
void del_gendisk(struct gendisk *gd);//卸載磁盤。參數是一個引用計數結構,包含kobject對象。
void add_disk(struct gendisk *gd); //初始化結構函數,一旦調用此函數,設備將被激活,並隨時會調用它提供的方法。在驅動程序徹底被初始化而且可以相應對磁盤的請求前,不要調用此函數。
當內核以文件系統、虛擬內存子系統或者系統調用的形式決定從塊I/O設備輸入、輸出塊數據時,它將再結合一個bio結構,用來描述這個操做。該結構被傳遞給 I/O代碼,代碼會把它合併到一個已經存在的request結構中,或者根據須要,再建立一個新的request結構。bio結構包含了驅動程序執行請求 的所有信息,而沒必要與初始化這個請求的用戶空間的進程相關聯。
內核中塊I/O操做的基本容器由bio結構體表示,定義在中,該結構體表明瞭正在現場的(活動的)以片斷(segment)鏈表形式組織的塊I/O操做。一個片斷是一小 塊連續的內存緩衝區。這樣的好處就是不須要保證單個緩衝區必定要連續。因此經過片斷來描述緩衝區,即便一個緩衝區分散在內存的多個位置上,bio結構體也 能對內核保證I/O操做的執行,這樣的就叫作聚散I/O.
bio爲通用層的主要數據結構,既描述了磁盤的位置,又描述了內存的位置,是上層內核vfs與下層驅動的鏈接紐帶。
struct bio { 緩存 //該bio結構所要傳輸的第一個(512字節)扇區:磁盤的位置 sector_t bi_sector; 安全 struct bio *bi_next; //請求鏈表
struct block_device *bi_bdev;//相關的塊設備
unsigned long bi_flags//狀態和命令標誌
unsigned long bi_rw; //讀寫
unsigned short bi_vcnt;//bio_vesc偏移的個數
unsigned short bi_idx; //bi_io_vec的當前索引
unsigned short bi_phys_segments;//結合後的片斷數目
unsigned short bi_hw_segments;//重映射後的片斷數目
unsigned int bi_size; //I/O計數
unsigned int bi_hw_front_size;//第一個可合併的段大小;
unsigned int bi_hw_back_size;//最後一個可合併的段大小
unsigned int bi_max_vecs; //bio_vecs數目上限
struct bio_vec *bi_io_vec; //bio_vec鏈表:內存的位置
bio_end_io_t *bi_end_io;//I/O完成方法
atomic_t bi_cnt; //使用計數
void *bi_private; //擁有者的私有方法
bio_destructor_t *bi_destructor; //銷燬方法
};
數據結構 |
此結構體的目的主要就是表明正在現場執行的I/O操做,因此該結構體中的主要域都是用來相關的信息的,而其中bi_io_vec、bi_vcnt、bi_idx重要
這三者造成了這樣一種關係:bio-->bi_io_vec,bi_idx(就如基地址加偏移量通常,能夠輕易的找到具體的bio_vec)-->page(再經過vec找到page)
其 中bi_io_vec指向一個bio_vec結構體數組,該結構體鏈表包含了一個特定的I/O操做所須要使用到的全部片斷。每一個bio_vec都是<page,offset,len>的向量,描述的是一個特定的片斷:片斷所在的物理頁,塊在物理頁中的偏移位置,從給定偏移量開始的塊長度,整個bio_io_vec結構體數組表示了一個完整的緩衝區。
struct bio_vec {
struct page *bv_page;指向整個緩衝區所駐留的物理頁面
unsigned int bv_len;這個緩衝區以字節爲單位的大小
unsigned int bv_offset;緩衝區所駐留的頁中以字節爲單位的偏移量。
};
bi_vcnt域用來描述bi_io_vec所指向的bio_vec數組中的向量數目。當I/O操做完成後,bi_idx指向數組的當前索引。一個塊請求經過一個bio表示。每一個請求包括多個或者一個塊,而這些塊有都存儲在bio_vec結構體的數組中,這些結構描述了每一個片斷在物理頁中的實際位置,而且如向量同樣的組織在一塊兒,I/O操做的第一個片斷由b_io_vec結構體所指向,其餘片斷則在其後依次放置,共有bi_vcnt個片斷,當I/O層開始執行請求,須要各個使用片斷時,bi_idx會不斷更新,從而總指向當前的片斷。看,這就是在入門C語言中用到的最樸實的概念,數組尋址的概念相相似。 塊設備將掛起的塊請求保存在請求隊列中,該隊列由request_queue結構體表示,定義在文件中,包含一個雙向請求隊列以及相關控制信息。經過內核中像文件系統這樣高層的代碼將請求加入到隊列中,請求隊列只要不爲空,隊列對應的塊設備驅動程序就會從隊列頭 獲取請求,而後將其加入到對應的塊設備中去,請求隊列表中的每一項都是一個單獨的請求,由request結構體表示。 而隊列中的請求request,定義在中,一個請求可能要操做多個連續的磁盤塊,因此每一個請求能夠由多個bio結構體組成。每一個bio結構體均可以描述多個片斷。下面就是request中比較經常使用的幾個域。struct request {struct list_head queuelist;//鏈接這個請求到請求隊列. //追蹤請求硬件完成的扇區的成員.第一個還沒有被傳送的扇區被存儲到 hard_sector,已經傳送的扇區總數在hard_nr_sectors,而且在當前bio中剩餘的扇區數是hard_cur_sectors.這些成員打算只用在塊子系統;驅動不該當使用它們.struct request_queue *q;sector_t hard_sector; unsigned long hard_nr_sectors; unsigned int hard_cur_sectors;struct bio *bio;//bio 是給這個請求的 bio 結構的鏈表. 你不該當直接存取這個成員; 使用 rq_for_each_bio(後面描述) 代替.unsigned short nr_phys_segments;//被這個請求在物理內存中佔用的獨特段的數目, 在鄰近頁已被合併後char *buffer;//隨着深刻理解,可見到這個成員僅僅是在當前 bio 上調用 bio_data 的結果.}; 而幾個關鍵結構之間的關係是如何的呢?request_queue中是請求隊列,經過它找到request,將這些請求連成一體,而後在request中包含bio,而後經過bio結構體找到對應的page,而後經過page讀取物理內存中的信息。大致就是這樣一個關係。塊驅動程序步驟與實例: 對於大多數塊驅動程序來講,首先都該是向內核註冊本身!這個任務的函數是register_blkdev(在中定義):int register_blkdev(unsigned int major, const char *name); 參數是設備要使用的主編號和關聯的名子(內核將顯示它在/proc/devices). 若是major傳遞爲0,內核分配一個新的主編號而且返回它給調用者.取消註冊的對應函數是:int unregister_blkdev(unsigned int major, const char *name);參數必須匹配傳遞給 register_blkdev 的那些。在2.6內核,register_blkdev所進行的功能已隨時間正在減小;這個調用惟一的任務是若是須要,分配一個動態主編號,而且在/proc/devices建立一個入口.描述虛擬設備的結構體,裏面的結構體除去timer_list都在前面介紹:struct sbull_dev {int size; //以扇區爲單位,設備的大小u8 *data; //數據數組short users;//用戶數目 short media_change;//介質改變標誌 spinlock_t lock;//用戶互斥struct request_queue *queue;//設備請求隊列 struct gendisk *gd;//gendisk結構struct timer_list timer;//模擬介質改變};static struct sbull_dev *Devices = NULL;//申請一個設備memset (dev, 0, sizeof (struct sbull_dev));//申請內存空間dev->size = nsectors*hardsect_size;//設備大小:1024*512dev->data = vmalloc(dev->size);switch (request_mode) {case RM_NOQUEUE:dev->queue = blk_alloc_queue(GFP_KERNEL);blk_queue_make_request(dev->queue, sbull_make_request);break;case RM_FULL:dev->queue = blk_init_queue(sbull_full_request, &dev->lock);break;default:printk(KERN_NOTICE "Bad request mode %d, using simple\n", request_mode);case RM_SIMPLE:dev->queue = blk_init_queue(sbull_request, &dev->lock);if (dev->queue == NULL)goto out_vfree;break;}使用bio結構編寫的塊設備驅動程序。static void sbull_full_request(request_queue_t *q){struct request *req;int sectors_xferred;struct sbull_dev *dev = q->queuedata;while ((req = elv_next_request(q)) != NULL) {//得到隊列中的下一個requestif (! blk_fs_request(req)) {printk (KERN_NOTICE "Skip non-fs request\n");end_request(req, 0);//配合elv_next_request使用,完成一個請求continue;}sectors_xferred = sbull_xfer_request(dev, req);//返回數量if (! end_that_request_first(req, 1, sectors_xferred)) {//驅動程序從前一次結束的地方開始,完成了規定數目的扇區的傳輸blkdev_dequeue_request(req);//從隊列中刪除一個請求函數,當end_that_request_first都被傳輸後,則必須調用此函數end_that_request_last(req);//通知任何等待已經完成請求的對象,並重複利用該request結構。}}}static int sbull_xfer_request(struct sbull_dev *dev, struct request *req){struct bio *bio;int nsect = 0;rq_for_each_bio(bio, req) {//以宏的形式實現的控制結構,遍歷請求中的每一個biosbull_xfer_bio(dev, bio);nsect += bio->bi_size/KERNEL_SECTOR_SIZE;//#define KERNEL_SECTOR_SIZE 512}return nsect;}static int sbull_xfer_bio(struct sbull_dev *dev, struct bio *bio){int i;struct bio_vec *bvec;sector_t sector = bio->bi_sector;bio_for_each_segment(bvec, bio, i) //用來遍歷組成bio結構的段的僞控制結構{char *buffer = __bio_kmap_atomic(bio, i, KM_USER0);//底層函數直接映射了指定索引號爲i的bio_vec中的緩衝區。sbull_transfer(dev, sector, bio_cur_sectors(bio),buffer, bio_data_dir(bio) == WRITE);//徹底簡單的基於ram設備。完成實際傳輸。//bio_cur_sectors用來訪問bio結構中的當前段,bio_data_dir用來得到bio結構描述的大小和傳輸方向sector += bio_cur_sectors(bio);__bio_kunmap_atomic(bio, KM_USER0);}return 0; }static void sbull_transfer(struct sbull_dev *dev, unsigned long sector,unsigned long nsect, char *buffer, int write){unsigned long offset = sector*KERNEL_SECTOR_SIZE;unsigned long nbytes = nsect*KERNEL_SECTOR_SIZE;if (write)memcpy(dev->data + offset, buffer, nbytes);elsememcpy(buffer, dev->data + offset, nbytes);}register_blkdev可用來得到一個主編號,但不使任何磁盤驅動器對系統可用.有一個分開的註冊接口你必須使用來管理單獨的驅動器.它是 struct block_device_operations, 定義在 .struct block_device_operations {int (*open) (struct inode *, struct file *);//設備打開函數int (*release) (struct inode *, struct file *);//設備關閉函數int (*ioctl) (struct inode *, struct file *, unsigned, unsigned long);//實現ioctl系統調用的方法.大部分的塊驅動 ioctl 方法至關短.long (*unlocked_ioctl) (struct file *, unsigned, unsigned long);//long (*compat_ioctl) (struct file *, unsigned, unsigned long);int (*direct_access) (struct block_device *, sector_t,void **, unsigned long *);int (*media_changed) (struct gendisk *);//被內核調用來檢查是否用戶已經改變了驅動器中的介質的方法,若是是這樣返回一個非零值.顯然,這個方法僅適用於支持可移出的介質的驅動器(而且最好給驅動一個"介質被改變"標誌); 在其餘狀況下可被忽略.int (*revalidate_disk) (struct gendisk *);//revalidate_disk方法被調用來響應一個介質改變;它給驅動一個機會來進行須要的任何工做使新介質準備好使用.這個函數返回一個int值,可是值被內核忽略.int (*getgeo)(struct block_device *, struct hd_geometry *);struct module *owner;//一個指向擁有這個結構的模塊的指針; 它應當經常被初始化爲 THIS_MODULE.};繼續初始化:dev->gd = alloc_disk(SBULL_MINORS);//動態分配gendisk結構(表是一個獨立的磁盤設備)dev->gd->major = sbull_major;//設定主設備號dev->gd->first_minor = which*SBULL_MINORS;//每一個設備所支持的次設備號數量dev->gd->fops = &sbull_ops;//塊操做方法dev->gd->queue = dev->queue;dev->gd->private_data = dev;snprintf (dev->gd->disk_name, 32, "sbull%c", which + 'a');set_capacity(dev->gd, nsectors*(hardsect_size/KERNEL_SECTOR_SIZE));//使用KERNEL_來進行內核512字節扇區到實際使用扇區大小的轉換。add_disk(dev->gd);//結束設置過程。其他部分參見ldd3的sbull