1、塊設備的I/O操作特點
字符設備與塊設備的區(qū)別:
塊設備只能以塊為單位接受輸入和返回輸出,而字符設備則以字符為單位。
塊設備對于I/O請求有對應的緩沖區(qū),因此它們可以選擇以什么順序進行響應,字符設備無需緩沖區(qū)且直接被讀寫。
字符設備只能被順序讀寫,而塊設備可以隨機讀寫。 但是對于磁盤等機械設備而言,順序的組織塊設備的訪問可以提高性能
總體而言,塊設備驅動比字符設備驅動要復雜得多,在I/O操作上表現(xiàn)出極大的不同,緩沖、I/O調度、請求隊列等都是與塊設備驅動相關的概念。
對于扇區(qū)1、10、3、2的請求被調整為對扇區(qū)1、2、3、10的請求。但是對于SD卡、RAMDISK等非機械設備來說則沒必要調整
2.block_device_operations結構體
1、打開和釋放?
int (*open)(struct inode *inode,struct file *filp); int(*release)(struct inode *inode,struct file *filp); //與字符設備驅動類似,當設備打開和關閉的時候調用它們
2、io控制
int (*ioctl)(struct inode *inode,struct file *filp,unsigned int cmd, unsigned long arg); //上述函數(shù)是ioctl()系統(tǒng)調用的實現(xiàn),塊設備包含大量的標準請求,這些標準請求由Linux塊設備層處理,因此大部分塊設備驅動的ioctl()函數(shù)相當短。
3、介質改變?
int (*media_changed)(struct gendisk *gd); /*被內核調用來檢查是否驅動器中的介質已經(jīng)改變。如果是,則返回一個非0值。否則返回0.這個函數(shù)僅適用于支持可移動介質的驅動器,通常需要在驅動器中增加一個表示介質狀態(tài)是否改變的標志變量,非可移動設備的驅動不需要實現(xiàn)這個方法。*/
4、使介質有效?
int (*revalidate_disk)(struct gendisk *gd);// revalidate_disk()函數(shù)被調用來響應一個介質改變,它給驅動一個機會來進行必要的工作以使新介質準備好。
5、獲得驅動器信息
int (*getgeo)(struct block_device *,struct hd_geometry *) ;//該函數(shù)根據(jù)驅動器的幾何信息填充一個hd_geometry的結構體,包含磁頭、扇區(qū)、柱面等信息。
6、模塊指針?
struct module * owner;// 一個指向擁有這個結構體的模塊的指針,它通常被初始化為THIS_MODULE.
3.?gendisk分析
在Linux內核中,使用gendisk(通用磁盤)結構體來表示1個獨立的磁盤設備(或分區(qū))。
int major;/* 主設備號 */int first_minor; /*第1個次設備號*/int minors: //磁盤使用這些成員描述設備號。 //最大的次設備數(shù),如果不能分區(qū),則為1,一個驅動器至少使用一個次設備號。 //如果驅動器是可被分區(qū)的(大多數(shù)情況下),用戶將要為每個可能的分區(qū)都分配一個次設備號。minors通常取16,他使得一個“完整的的磁盤“包含15個分區(qū)。 某些磁盤驅動程序設置每個設備可使用64個次設備號。char disk_name[32]; //設置磁盤設備名字的成員。該名字將顯示在/proc/partitions和sysfs中。struct block_device_operations *fops; //設置前面所述的各種設備操作;struct request_queue *queue; //內核使用該結構為設備管理i/o請求;在”請求過程“中詳細進行論述。int flags; //用來描述驅動器狀態(tài)的標志(很少使用)。如果用戶設備包含了可移動介質,其將被設置為GENHD_FL_REMOVABLE。sector_t capacity; //以512字節(jié)為一個扇區(qū)時,該驅動器可包含的扇區(qū)數(shù)。void *preivate_data; //塊設備驅動程序可能使用該成員保存指向其內部數(shù)據(jù)的指針。
major、first_minor和minors共同表征了磁盤的主、次設備號,同一個磁盤的各個分區(qū)共享1個主設備號,而次設備號則不同。
fops為block_device_operations,即上節(jié)描述的塊設備操作集合。queue是內核用來管理這個設備的 I/O請求隊列的指針。
capacity表明設備的容量,以512個字節(jié)為單位。private_data可用于指向磁盤的任何私有數(shù)據(jù),用法與字符設備驅動file結構體的private_data類似。
Linux內核提供了一組函數(shù)來操作gendisk,主要包括:
? 分配gendisk
gendisk結構體是一個動態(tài)分配的結構體,它需要特別的內核操作來初始化,驅動不能自己分配這個結構體,而應該使用下列函數(shù)來分配gendisk:
struct gendisk *alloc_disk(int minors);
minors 參數(shù)是這個磁盤使用的次設備號的數(shù)量,一般也就是磁盤分區(qū)的數(shù)量,此后minors不能被修改。
? 增加gendisk
gendisk結構體被分配之后,系統(tǒng)還不能使用這個磁盤,需要調用如下函數(shù)來注冊這個磁盤設備:
void add_disk(struct gendisk *gd);
特別要注意的是對add_disk()的調用必須發(fā)生在驅動程序的初始化工作完成并能響應磁盤的請求之后。
? 釋放gendisk
當不再需要一個磁盤時,應當使用如下函數(shù)釋放gendisk:
void del_gendisk(struct gendisk *gd);
? gendisk引用計數(shù)
gendisk中包含1個kobject成員,因此,它是一個可被引用計數(shù)的結構體。通過get_disk()和put_disk()函數(shù)可用來操作引用計數(shù),這個工作一般不需要驅動親自做。通常對 del_gendisk()的調用會去掉gendisk的最終引用計數(shù),但是這一點并不是一定的。
因此,在del_gendisk()被調用后,這個結構體可能繼續(xù)存在。
? 設置gendisk容量
void set_capacity(struct gendisk *disk, sector_t size);
塊設備中最小的可尋址單元是扇區(qū),扇區(qū)大小一般是2的整數(shù)倍,最常見的大小是512字節(jié)。扇區(qū)的大小是設備的物理屬性,扇區(qū)是所有塊設備的基本單元,塊設備無法對比它還小的單元進行尋址和操作,不過許多塊設備能夠一次就傳輸多個扇區(qū)。雖然大多數(shù)塊設
備的扇區(qū)大小都是512字節(jié),不過其它大小的扇區(qū)也很常見,比如,很多CD-ROM盤的扇區(qū)都是2K大小。
不管物理設備的真實扇區(qū)大小是多少,內核與塊設備驅動交互的扇區(qū)都以512字節(jié)為單位。因此,set_capacity()函數(shù)也以512字節(jié)為單位。
4.塊設備驅動模塊加載和卸載函數(shù)模板
在塊設備驅動的模塊加載函數(shù)中通常需要完成如下工作:
① 分配、初始化請求隊列,綁定請求隊列和請求函數(shù)。
② 分配、初始化gendisk,給gendisk的major、fops、queue等成員賦值,最后添加gendisk。
③ 注冊塊設備驅動。
使用blk_alloc_queue
static int __init xxx_init(void){ //分配gendisk xxx_disks = alloc_disk(1); if (!xxx_disks) { goto out; } /* 塊設備驅動注冊 在2.6內核中,對 register_blkdev()的調用完全是可選的,register_blkdev()的功能已隨時間正在減少,這個調用最多只完全2件事: ① 如果需要,分配一個動態(tài)主設備號。 ② 在/proc/devices中創(chuàng)建一個入口。 在將來的內核中,register_blkdev()可能會被去掉。但是目前的大部分驅動仍然調用它。 */ if (register_blkdev(XXX_MAJOR, "xxx")) { err = - EIO; goto out; } //“請求隊列”分配 xxx_queue = blk_alloc_queue(GFP_KERNEL); if (!xxx_queue) { goto out_queue; } blk_queue_make_request(xxx_queue, &xxx_make_request); //綁定“制造請求”函數(shù) blk_queue_hardsect_size(xxx_queue, xxx_blocksize); //硬件扇區(qū)尺寸設置 //gendisk初始化 xxx_disks->major = XXX_MAJOR; xxx_disks->first_minor = 0; xxx_disks->fops = &xxx_op; xxx_disks->queue = xxx_queue; sprintf(xxx_disks->disk_name, "xxx%d", i); set_capacity(xxx_disks, xxx_size); //xxx_size以512bytes為單位 add_disk(xxx_disks); //添加gendisk return 0; out_queue: unregister_blkdev(XXX_MAJOR, "xxx"); out: put_disk(xxx_disks); blk_cleanup_queue(xxx_queue); return - ENOMEM;}
使用blk_init_queue
static int __init xxx_init(void){ //塊設備驅動注冊 if (register_blkdev(XXX_MAJOR, "xxx")) { err = - EIO; goto out; } //請求隊列初始化 xxx_queue = blk_init_queue(xxx_request, xxx_lock); if (!xxx_queue) { goto out_queue; } blk_queue_hardsect_size(xxx_queue, xxx_blocksize); //硬件扇區(qū)尺寸設置 //gendisk初始化 xxx_disks->major = XXX_MAJOR; xxx_disks->first_minor = 0; xxx_disks->fops = &xxx_op; xxx_disks->queue = xxx_queue; sprintf(xxx_disks->disk_name, "xxx%d", i); set_capacity(xxx_disks, xxx_size *2); add_disk(xxx_disks); //添加gendisk return 0;out_queue: unregister_blkdev(XXX_MAJOR, "xxx");out: put_disk(xxx_disks); blk_cleanup_queue(xxx_queue); return - ENOMEM;}
每個塊設備驅動程序的核心是它的請求函數(shù)。
實際的工作,如設備的啟動,都是在這個函數(shù)中完成。
驅動程序的新能,是這個操作系統(tǒng)性能的重要組成部分,因此內核的塊設備子系統(tǒng)在編寫的時候就非常注意性能方面的問題。
塊設備驅動模塊卸載函數(shù)模板
① 清除請求隊列。
② 刪除gendisk和對gendisk的引用。
③ 刪除對塊設備的引用,注銷塊設備驅動。
static void __exit xxx_exit(void){ if (bdev) { invalidate_bdev(xxx_bdev, 1); blkdev_put(xxx_bdev); } del_gendisk(xxx_disks); //刪除gendisk put_disk(xxx_disks); blk_cleanup_queue(xxx_queue[i]); //清除請求隊列 unregister_blkdev(XXX_MAJOR, "xxx");}
5.塊設備驅動I/O
request函數(shù)介紹
原型:
void request(request_queue_t *queue);
這個函數(shù)不能由驅動自己調用,只有當內核認為是時候讓驅動處理對設備的讀寫等操作時,它才調用這個函數(shù)。
請求函數(shù)可以在沒有完成請求隊列中的所有請求的情況下返回,甚至它1個請求不完成都可以返回。但是,對大部分設備而言,在請求函數(shù)中處理完所有請求后再返回通常是值得推薦的方法。
對request函數(shù)的調用是與用戶空間進程中的動作是完全異步的,因此不能直接對用戶空間進行訪問。
塊設備驅動請求函數(shù)例程:
static void xxx_request(request_queue_t *q){ struct request *req; while ((req = elv_next_request(q)) != NULL) { struct xxx_dev *dev = req->rq_disk->private_data; if (!blk_fs_request(req)) //不是文件系統(tǒng)請求 { printk(KERN_NOTICE "Skip non-fs request/n"); end_request(req, 0);//通知請求處理失敗 continue; } xxx_transfer(dev, req->sector, req->current_nr_sectors, req->buffer, rq_data_dir(req)); //處理這個請求 end_request(req, 1); //通知成功完成這個請求 }}//完成具體的塊設備I/O操作static void xxx_transfer(struct xxx_dev *dev, unsigned long sector, unsignedlong nsect, char *buffer, int write){ unsigned long offset = sector * KERNEL_SECTOR_SIZE; unsigned long nbytes = nsect * KERNEL_SECTOR_SIZE; if ((offset + nbytes) > dev->size) { printk(KERN_NOTICE "Beyond-end write (%ld %ld)/n", offset, nbytes); return ; } if (write) { write_dev(offset, buffer, nbytes); //向設備些nbytes個字節(jié)的數(shù)據(jù) } else { read_dev(offset, buffer, nbytes); //從設備讀nbytes個字節(jié)的數(shù)據(jù) }}void end_request(struct request *req, int uptodate){ /* 當設備已經(jīng)完成1個I/O請求的部分或者全部扇區(qū)傳輸后, end_that_request_first這個函數(shù)告知塊設備層,塊設備驅動已經(jīng)完成count個扇區(qū)的傳送 返回值為0表示所有的扇區(qū)已經(jīng)被傳送并且這個請求完成 */ if (!end_that_request_first(req, uptodate, req->hard_cur_sectors)) { /* 使用塊 I/O 請求的定時來給系統(tǒng)的隨機數(shù)池貢獻熵,它不影響塊設備驅動。 但是,僅當磁盤的操作時間是真正隨機的時候(大部分機械設備如此),才應該調用它。 */ add_disk_randomness (req->rq_disk); blkdev_dequeue_request (req);//從隊列中清除這個請求 end_that_request_last(req);//通知所有正在等待這個請求完成的對象請求已經(jīng)完成并回收這個請求結構體。 } }
請求隊列
一個塊設備請求隊列是:包含塊設備I/O請求的序列。
請求隊列跟蹤未完成的塊設備的I/O請求,保存了描述設備所能處理的請求的參數(shù):最大尺寸、在同一個請求中所包含的獨立段的數(shù)目、硬件扇區(qū)的大小、對齊需求等。
請求隊列實現(xiàn)了插件接口,以便可以使用多個I/O調度器。
I/O調度器積累了大量的請求,根據(jù)塊索引號升序(或者降序)排列他們,并按照這個順序向驅動程序發(fā)送請求。
磁頭從一個磁盤的末尾移向另一個磁盤,如同單向電梯一樣,直到每個請求都得到滿足。
隊列的創(chuàng)建與刪除
一個請求隊列就是一個動態(tài)的數(shù)據(jù)結構,該結構必須由塊設備的I/O子系統(tǒng)創(chuàng)建。
request_queue_t *blk_init_queue(request_fn_proc *request,spinlock_t *lock) ;//該函數(shù)參數(shù)是處理這個隊列的request指針和控制訪問隊列權限的自旋鎖。void blk_cleanup_queue(request_queue_t *);//刪除隊列
隊列中的函數(shù)
返回隊列中下一個要處理的請求
struct request *elv_next_request(request_queue_t *queue);
將請求從隊列中刪除
void blkdev_dequeue_request(struct request *req);
隊列控制函數(shù)
驅動程序使用塊設備層到處的一組函數(shù)去控制請求隊列的操作。
void blk_stop_queue(request_queue_t *queue)void blk_start_queue(request_queue_t *queue) //如果驅動程序進入不能處理更多命令的狀態(tài),就會調用blk_stop_queue以通知塊設備層,以暫停調用request函數(shù)。當有能力處理更多請求時,需要調用blk_start_queue重新開始調用。void blk_queue_bounce_limit(request_queue_t *queue,u64 dma_addr); //該函數(shù)告訴內核驅動程序執(zhí)行DMA所使用的最高物理內存。如果一個請求包含了超越界限的內存引用,將使用回彈緩沖區(qū)(bounce buffer)進行處理。
請求過程剖析
每個request結構都代表了一個塊設備的I/O請求。
一個特定的請求可以分布在整個內存中,但多數(shù)是對相鄰的扇區(qū)進行操作。
如果多個請求都是對磁盤中的相鄰扇區(qū)進行操作,則內核將對他們進行合并。
從本質上講,一個request結構是作為一個bio結構的鏈表實現(xiàn)的。
bio
bio結構 bio結構在中定義,包含了驅動程序作者所要使用的諸多成員。
sector_t bi_sector; //該bio結構所要傳輸?shù)牡谝粋€扇區(qū)(512字節(jié))unsigned int bi_size; //以字節(jié)為單位所需傳輸?shù)臄?shù)據(jù)大小。unsigned long bi_flags; //bio中一系列的標志位;如果是寫請求,最低有效位將被設置。unsigned short bio_phys_segments;unsigned short bio_hw_segments; //當DMA 映射完成時,它們分別表示bio中包含的物理段的數(shù)目和硬件所能操作的數(shù)目。
request結構成員
sector_t hard_sector;unsigned long hard_nr_sectors;unsigned int hard_cur_sectors;// 用于跟蹤那些驅動程序還未完成的扇區(qū)。還未傳輸?shù)牡谝粋€扇區(qū)保存在hard_sector中,等待傳輸扇區(qū)的總數(shù)量保存在hard_nr_sectors中,當前bio中剩余的扇區(qū)數(shù)目包含在hard_cur_sectors中。struct bio *bio;// 該請求的bio結構鏈表。struct list_head queuelist;// 內核鏈表結構,用來把請求連接到請求隊列中。
屏障請求
在驅動程序收到請求前,塊設備層重新組合了請求以提高I/O性能。出于同樣的目的,驅動程序也可以重新組合請求。
但是一些應用程序的某些操作,要寫在另外一些操作之前,比如關系數(shù)據(jù)庫在執(zhí)行一個關系數(shù)據(jù)庫內容的會話前,日志信息要寫到驅動器上。
2.6內核采用屏障(barrier)請求解決問題:如果一個請求被設置了REQ_HARDBARRER標志,那么其后請求被初始化前,它必須被寫進驅動器。
不可重試請求
當?shù)谝淮握埱笫『螅瑝K設備驅動程序經(jīng)常要重試請求。這樣的性能使得系統(tǒng)更可靠,不會丟失數(shù)據(jù)。
但是,內核在某些情況下標記請求是不可重試的。這些請求如果在第一次執(zhí)行失敗后,要盡快拋棄。
?
評論