十七、块设备驱动
在讀者學習本章以及后續塊設備相關章節之前,最好了解塊設備讀寫過程和操作,可以參考:Nor Flash裸機操作。
?
?
一、塊設備概念和讀寫過程
塊設備:
塊設備是I/O設備中的一類,是將信息存儲在固定大小的塊中,每個塊都有自己的地址,還可以在設備的任意位置讀取一定長度的數據。如硬盤,U盤,SD卡等。
?
塊設備組成:
段(Segments):由若干個塊組成,是Linux內存管理機制中一個內存頁或者內存頁的一部分。
塊(Blocks):由一個或多個扇區組成,塊是內核對文件系統的一種抽象,也就是說內核執行的所有磁盤操作都是以塊為基本單位的。
扇區(Sectors):塊設備的基本單位。
扇區是硬件設備傳輸數據的最小單位,而塊是操作系統傳輸數據的最小單位。
?
以上圖為例,若我們依次在此Flash的扇區1和扇區2上寫數據,正常的操作步驟一般是:
1. 把扇區1的數據放入緩沖區中
2. 修改緩沖區數據
3. 擦除整塊
4. 燒寫整塊
寫扇區2時重復上述操作。
?
但是上述操作讀取、修改、燒寫都進行了兩次,浪費時間。因此內核提供了一個隊列機制,它會將讀寫請求進行優化、排序和合并等操作,從而提高訪問硬盤的效率。
若優化上述過程,可將操作步驟變更為:
1. 接到兩個寫操作,將其放入隊列
2. 對兩個寫操作進行合并,變成一個寫操作
3. 把扇區1和扇區2的數據放入緩沖區中
4. 修改緩沖區數據
5. 擦除整塊
6. 燒寫整塊
?
?
二、塊設備框架分析
當我們對一個文本文件寫入(write)或讀取(read)數據時,文件系統會轉換為對塊設備上扇區的訪問,它會調用ll_rw_block()函數實現對扇區的讀寫。
1 void ll_rw_block(int rw, int nr, struct buffer_head *bhs[]) 2 { 3 int i; 4 5 for (i = 0; i < nr; i++) { /* nr表示buffer_head數組個數 */ 6 struct buffer_head *bh = bhs[i]; 7 8 if (!trylock_buffer(bh)) 9 continue; 10 if (rw == WRITE) { 11 if (test_clear_buffer_dirty(bh)) { 12 bh->b_end_io = end_buffer_write_sync; 13 get_bh(bh); 14 submit_bh(WRITE, bh); /* 提交寫標志的buffer_head */ 15 continue; 16 } 17 } else { 18 if (!buffer_uptodate(bh)) { 19 bh->b_end_io = end_buffer_read_sync; 20 get_bh(bh); 21 submit_bh(rw, bh); /* 提交其它標志的buffer_head */ 22 continue; 23 } 24 } 25 unlock_buffer(bh); 26 } 27 }其中,buffer_head用于存儲I/O操作數據和信息。
struct buffer_head {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 completion */void *b_private; /* reserved for b_end_io */struct list_head b_assoc_buffers; /* associated with another mapping */struct address_space *b_assoc_map; /* mapping this buffer isassociated with */atomic_t b_count; /* users using this buffer_head */ }; View Code?
接下來,我們來分析submit_bio(rw, bio)函數的調用過程。
submit_bh(rw, bh);-> bio = bio_alloc(GFP_NOIO, 1); /* 分配并設置struct bio */-> submit_bio(rw, bio);-> generic_make_request(bio); /* 構造請求隊列描述符 */-> __generic_make_request(bio);-> struct request_queue *q = bdev_get_queue(bio->bi_bdev);-> q->make_request_fn(q, bio); /* 在blk_init_allocated_queue_node()函數中初始化為__make_request()函數 */-> __make_request(q, bio);-> elv_merge(q, &req, bio); /* 電梯算法融合,原理與我們所乘坐電梯載客方式相同 */-> init_request_from_bio(req, bio); /* 若無法合成,則使用bio構造I/O請求 */-> add_acct_request(q, req, where); /* 將請求加入隊列 */-> __blk_run_queue(q); /* 執行處理隊列 */-> q->request_fn(q); /* 執行處理函數,在blk_init_allocated_queue_node()函數中初始化為傳入參數rfn */其中,
1. 一般一個struct bio對應一個I/O請求。一個bio由多個bio_vec組成。兩結構體定義如下:
1 struct bio_vec { 2 struct page *bv_page; /* 頁指針 */ 3 unsigned int bv_len; /* 傳輸的字節數 */ 4 unsigned int bv_offset; /* 偏移位置 */ 5 }; 6 7 struct bio { 8 sector_t bi_sector; /* 要傳輸的第一個扇區 */ 9 struct bio *bi_next; /* 下一個bio */ 10 struct block_device *bi_bdev; 11 unsigned long bi_flags; /* 狀態、命令等 */ 12 unsigned long bi_rw; /* 低位表示寫或讀,高位表示優先級 */ 13 14 unsigned short bi_vcnt; /* bio_vec的數量 */ 15 unsigned short bi_idx; /* 當前bvl_vec索引 */ 16 17 unsigned int bi_phys_segments; 18 19 unsigned int bi_size; /* 剩余I/O請求個數 */ 20 21 unsigned int bi_seg_front_size; 22 unsigned int bi_seg_back_size; 23 unsigned int bi_max_vecs; /* max bvl_vecs we can hold */ 24 unsigned int bi_comp_cpu; /* completion CPU */ 25 atomic_t bi_cnt; /* pin count */ 26 struct bio_vec *bi_io_vec; /* the actual vec list */ 27 bio_end_io_t *bi_end_io; 28 void *bi_private; 29 ... 30 bio_destructor_t *bi_destructor; /* destructor */ 31 struct bio_vec bi_inline_vecs[0]; 32 }; View Code?2. struct request_queue *q即為第一節優化中提到的隊列。
3. blk_init_allocated_queue_node()函數被頂層的blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)和blk_alloc_queue(gfp_t gfp_mask)函數調用,因此我們在編寫驅動程序時需要調用兩函數初始化等待隊列。
4. 電梯算法的原理與我們所乘坐電梯載客方式相同,比如電梯在1層處于上升狀態,2、4、5樓層的人要上樓,3、6、7樓層的人要下樓,那么電梯上升過程中并不會在3、6、7層停留,因此他可以一個來回完成所有載客任務。
?
調用關系如下圖:
圖中的I/O調度器會對bio進行調度處理,bio結構或被合并到請求隊列的一個請求結構的request中。最后調用blk_init_queue()的request_fn_proc()將數據讀取或寫入塊設備。I/O調用器所使用的調度方法就是電梯算法。
另一方面,blk_alloc_queue()并不會啟動I/O調度器,bio的流程完全由我們控制。
?
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;void *queuedata;gfp_t bounce_gfp;unsigned long queue_flags;spinlock_t __queue_lock;spinlock_t *queue_lock;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;void *dma_drain_buffer;unsigned int dma_drain_size;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 queue_limits limits; ... }; View Code根據上述分析可以確定struct request_queue用于實現對塊設備的合并操作。
根據分離原則,必然會有結構體用于表示塊設備的屬性,此結構體為struct gendisk。
struct gendisk {int major; /* 主設備號,使用register_blkdev()申請 */int first_minor; /* 起始次設備號 */int minors; /* 有多少個次設備號,也就是多少個分區,若minors為1,表示此塊設備沒有分區 */char disk_name[DISK_NAME_LEN]; /* 塊設備名字 */char *(*devnode)(struct gendisk *gd, mode_t *mode);unsigned int events; /* supported events */unsigned int async_events; /* async events, subset of all */struct disk_part_tbl __rcu *part_tbl;struct hd_struct part0; /* 分區表信息 */const struct block_device_operations *fops; /* 塊設備操作函數 */struct request_queue *queue; /* 隊列 */void *private_data; ... };其中,
1. 分區信息struct hd_struct定義如下:
struct hd_struct {sector_t start_sect; /* 起始扇區,typedef unsigned long sector_t; */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; ... };此結構體中最重要的信息就是分區的起始扇區號和分區的大小。分區大小可以使用set_capacity()函數設置。
2. 塊設備底層操作函數結構體struct block_device_operations定義如下:
1 struct block_device_operations { 2 int (*open) (struct block_device *, fmode_t); 3 int (*release) (struct gendisk *, fmode_t); 4 int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long); 5 int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long); 6 int (*direct_access) (struct block_device *, sector_t, 7 void **, unsigned long *); 8 unsigned int (*check_events) (struct gendisk *disk, 9 unsigned int clearing); 10 /* ->media_changed() is DEPRECATED, use ->check_events() instead */ 11 int (*media_changed) (struct gendisk *); 12 void (*unlock_native_capacity) (struct gendisk *); 13 int (*revalidate_disk) (struct gendisk *); 14 int (*getgeo)(struct block_device *, struct hd_geometry *); 15 /* this callback is with swap_lock and sometimes page table lock held */ 16 void (*swap_slot_free_notify) (struct block_device *, unsigned long); 17 struct module *owner; 18 }; View Code?
?
三、塊設備驅動編寫步驟
在初始化函數中:
1. 使用register_blkdev()創建塊設備,創建方式與字符設備類似(可省略)
2. 使用blk_init_queue()不使用等待隊列 ?或 ?使用blk_alloc_queue()使用等待隊列
3. 若使用blk_alloc_queue(),則需要使用blk_queue_make_request()函數綁定請求隊列函數
4. 使用alloc_disk()分配struct gendisk
5. 設置struct gendisk成員
6. 使用add_disk()注冊struct gendisk
在隊列函數中:
1. 使用bio_for_each_segment(bvl, bio, i)遍歷每一個bio_vec
2. 使用bio_rw(bio)獲取每個申請的讀寫標志,READ表示讀,WRITE表示寫
3. 使用memcpy()讀寫扇區或緩沖區
4. 使用bio_endio()結束bio處理
在注銷函數中:
1. 使用put_disk()和del_gendisk()刪除對gendisk設備的引用并刪除設備
2. 使用blk_cleanup_queue()清除隊列
3. 使用unregister_blkdev()注銷塊設備(可省略)
?
步驟中所使用函數聲明如下:
/* 1. 初始化函數 */ int register_blkdev(unsigned int major, const char *name) struct request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock) /* 不使用請求隊列 */ typedef void (request_fn_proc)(struct reqest_queue *q) /* blk_init_queue()對應的處理函數 */struct request_queue *blk_alloc_queue(gfp_t gfp_mask) /* 使用請求隊列,與上面函數二選一 */ void blk_queue_make_request(struct request_queue * q, make_request_fn * mfn) /* 綁定請求隊列函數,與blk_alloc_queue()配合使用 */ typedef int (make_request_fn)(struct request_queue *q,struct bio *bio) /* blk_queue_make_request()對應的處理函數 */struct gendisk *alloc_disk(int minors) void add_disk(struct gendisk *disk)/* 2. 隊列函數 */ #define bio_for_each_segment(bvl, bio, i) #define bio_rw(bio) ((bio)->bi_rw & (RW_MASK | RWA_MASK)) void *memcpy(void *destin, void *source, unsigned n); void bio_endio(struct bio *bio, int error) /* error=0表示讀寫操作正常,其它表示錯誤 *//* 3. 注銷函數 */ void put_disk(struct gendisk *disk); /* 刪除對gendisk設備的引用 */ void del_gendisk(struct gendisk *disk); /* 刪除gendisk設備 */ void blk_cleanup_queue(request_queue_t *q); unregister_blkdev(unsigned int major, const char *name);其中,add_disk()函數需要我們進一步分析。
void add_disk(struct gendisk *disk)-> blk_alloc_devt(&disk->part0, &devt);/* 獲取第一個設備號的地址 */-> *devt = MKDEV(disk->major, disk->first_minor + part->partno);-> disk->major = MAJOR(devt);-> disk->first_minor = MINOR(devt);-> register_disk(disk); /* 將塊設備注冊進入文件系統 */-> blk_register_queue(disk); /* 把隊列注冊進入系統 */?
有了以上基礎,下面我們來實現通過內存來模擬塊設備驅動。
內存來模擬塊設備驅動源代碼:
1 #include <linux/module.h> 2 #include <linux/errno.h> 3 #include <linux/interrupt.h> 4 #include <linux/mm.h> 5 #include <linux/fs.h> 6 #include <linux/kernel.h> 7 #include <linux/timer.h> 8 #include <linux/genhd.h> 9 #include <linux/hdreg.h> 10 #include <linux/ioport.h> 11 #include <linux/init.h> 12 #include <linux/wait.h> 13 #include <linux/blkdev.h> 14 #include <linux/blkpg.h> 15 #include <linux/delay.h> 16 #include <linux/io.h> 17 18 #include <asm/system.h> 19 #include <asm/uaccess.h> 20 #include <asm/dma.h> 21 22 23 #define BLOCKSIZE (1024*1024) 24 #define SECTORSIZE (512) 25 26 static struct gendisk *ramdisk; 27 static struct request_queue *queue; 28 static int major; 29 30 unsigned char *blkdev_data[BLOCKSIZE]; /* 1M內存空間 */ 31 32 static struct block_device_operations ramblock_fops = { 33 .owner = THIS_MODULE, 34 }; 35 36 static int ramdisk_request(struct request_queue *q,struct bio *bio) 37 { 38 struct bio_vec *bvec; 39 int i; 40 void *src_mem; /* 物理塊設備存儲空間 */ 41 42 if ((bio->bi_sector << 9) + bio->bi_size > BLOCKSIZE) { 43 bio_endio(bio, -EIO); 44 return 0; 45 } 46 47 src_mem = blkdev_data + (bio->bi_sector << 9); /* 要讀寫的塊設備位置,<< 9 為 * 512 */ 48 49 bio_for_each_segment(bvec, bio, i) { /* 對每一個bio_vec進行操作 */ 50 void *iovec_mem; /* 請求對應的內存 */ 51 switch (bio_rw(bio)) { 52 case READ: 53 case READA: 54 iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset; 55 memcpy(iovec_mem, src_mem, bvec->bv_len); 56 kunmap(bvec->bv_page); 57 break; 58 case WRITE: 59 iovec_mem = kmap(bvec->bv_page) + bvec->bv_offset; 60 memcpy(src_mem, iovec_mem, bvec->bv_len); 61 kunmap(bvec->bv_page); 62 break; 63 default: 64 printk("unknown value of bio_rw: %lu\n", bio_rw(bio)); 65 bio_endio(bio, -EIO); 66 return 0; 67 } 68 src_mem += bvec->bv_len; 69 } 70 bio_endio(bio, 0); 71 72 return 0; 73 } 74 75 static int __init ramdisk_init(void) 76 { 77 major = register_blkdev(0, "ramdisk"); 78 79 queue = blk_alloc_queue(GFP_KERNEL); 80 blk_queue_make_request(queue, ramdisk_request); 81 ramdisk = alloc_disk(1); /* 該磁盤最多一個分區 */ 82 ramdisk->major = major; 83 ramdisk->first_minor = 0; 84 ramdisk->queue = queue; 85 ramdisk->fops = &ramblock_fops; 86 strcpy(ramdisk->disk_name, "ramdisk"); 87 set_capacity(ramdisk, BLOCKSIZE/SECTORSIZE); /* 扇區數 */ 88 89 add_disk(ramdisk); 90 91 return 0; 92 } 93 94 static void __exit ramdisk_exit(void) 95 { 96 put_disk(ramdisk); 97 del_gendisk(ramdisk); 98 blk_cleanup_queue(queue); 99 unregister_blkdev(major, "ramblock"); 100 } 101 102 /* 聲明段屬性 */ 103 module_init(ramdisk_init); 104 module_exit(ramdisk_exit); 105 106 MODULE_LICENSE("GPL"); View CodeMakefile:
1 KERN_DIR = /work/itop4412/tools/linux-3.5 2 3 all: 4 make -C $(KERN_DIR) M=`pwd` modules 5 6 clean: 7 make -C $(KERN_DIR) M=`pwd` modules clean 8 rm -rf modules.order 9 10 obj-m += ramdisk.o View Code?
測試:
在編譯并在開發板上insmod后執行:
# mkdosfs /dev/ramdisk /* 格式化 */
# mount /dev/ramdisk /tmp /* 掛接 */
# vi /tmp/a.txt /* 創建文件 */
# unmount /tmp /* 重新掛接 */
# mount /dev/ramdisk /tmp
# ls /tmp /* 查看文件是否仍舊存在 */
?
?
下一章 ?十八、Nand Flash驅動和Nor Flash驅動
?
轉載于:https://www.cnblogs.com/Lioker/p/11248902.html
總結
- 上一篇: java内存中的栈、方法区 、堆
- 下一篇: Amazon Redshift 架构