Linux塊設備驅動詳解


<機械硬盤>
a:磁盤結構
-----傳統的機械硬盤一般為3.5英寸硬盤,並由多個圓形蝶片組成,每個蝶片擁有獨立的機械臂和磁頭,每個堞片的圓形平面被划分了 不同的同心圓,每一個同心圓稱為 一個磁道,位於最外面的道的周長最長稱為外道,最里面的道稱為內道,通常硬盤廠商會將圓形蝶片最靠里面的一些內道(速度較慢,影響性能)封裝起來不用;道又被划分成不同的塊單元稱為扇區, 每個道周長不同,現代硬盤 不同長度的道划分出來的 扇區數也是 不相同的,而磁頭不工作的時候一般位於內道,如果追求響應時間,則數據可存儲在硬盤的內道,如果追求大的吞吐量,則數據應存儲在硬盤的外道;
 
注意:;一個弧道被划分成多個段,每一個段就是一個扇區
b:磁盤訪問
------SATA硬盤實現的是串行ATA協議,ATA下盤命令中記錄有LBA(Logic Block Address)起始地址和扇區數;LBA地址實際上是一個ATA協議邏輯地址,硬盤的固件會解析收到的ATA命令,並將要訪問的LBA地址映射至某個磁道中的某個物理塊即扇區。操作系統暫可認為LBA地址就是硬盤的物理地址。
c:扇區
------硬盤的基本訪問單位,扇區的大小一般是512B(對於現在的有些磁盤的扇區>512B,比如光盤的一個扇區就是2048B,Linux將其看成4個扇區,無非就是需要完成4次的讀寫)。
d:塊
------扇區是硬件傳輸數據的基本單位,硬件一次傳輸一個扇區的數據到內存中。但是和扇區不同的是,塊是虛擬文件系統傳輸數據的基本單位。在Linux中,塊的大小必須是2的冪,但是不能超過一個頁的大小(4k)。(在X86平台,一個頁的大小是4094個字節,所以塊大小可以是512,1024,2048,4096
e:段
------主要為了做scatter/gather DMA操作使用,同一個物理頁面中的在 硬盤存儲介質上連續的多個塊組成一個 。段的大小只與塊有關,必須是塊的整數倍。所以塊通常包括多個扇區,段通常包括多個塊,物理段通常包括多個段; 在內核中由結構 struct bio_vec來描述, 多個段的信息 存放於struct bio結構中的 bio_io_vec指針 數組中,段數組在后續的塊設備處理流程中會被合並成物理段,段結構定義如下:
struct bio_vec {
       struct page      *bv_page;  // 段所在的物理頁面結構,即bh->b_page
       unsigned int    bv_len;    // 段的字節數,即bh->b_size
       unsigned int    bv_offset;  // 段在bv_page頁面中的偏移,即bh->b_data
};
f:文件塊
------大小定義和文件系統塊一樣;只是相對於文件的一個偏移邏輯塊,需要 通過具體文件系統中的此 文件對應的inode所記錄的間接塊信息,換算成對應的文件系統塊;此做法是為了 將一個文件的內容存於 硬盤的不同位置,以提高訪問速度;即一個文件的內容在硬盤是一般是不連續的;EXT2中,ext2_get_block()完成文件塊到文件系統塊的映射。
 
g:總結
------ 扇區磁盤的物理特性決定; 塊緩沖區由內 核代碼決定緩沖區決定,是塊緩沖區大小的整數倍(但是不能超過一個頁)。三者關系如下:
 
所以:扇區(512) ≤塊≤頁(4096) 塊=n*扇區(n為整數)
注意:段(struct bio_vec{})由多個塊組成,一個段就是一個內存頁(如果一個塊是兩個扇區大小,也就是1024B,那么一個段的大小可以是1024,2018,3072,4096,也就是說段的大小只與塊有關,而且是整數倍)。Linux系統一次讀取磁盤的大小是一個塊,而不是一個扇區,塊設備驅動由此得名。
 
<塊設備處理過程>
a:linux 內核中,塊設備將數據存儲與固定的大小的塊中,每個塊都有自己的固定地址。Linux內核中塊設備和其他模塊的關系如下。
a:塊設備的處理過程涉及Linux內核中的很多模塊,下面簡單描述之間的處理過過程。
(1)當一個用戶程序要向磁盤寫入數據時,會發發出write()系統調用給內核。
(2)內核會調用虛擬文件系統相應的函數,將需要寫入發文件描述符和文件內容指針傳遞給該函數。
(3)內核需要確定寫入磁盤的位置,通過映射層知道需要寫入磁盤的哪一塊。
(4)根據磁盤的文件系統的類型,調用不同文件格式的寫入函數,江蘇數據發送給通用塊層(比如ext2和ext3文件系統的寫入函數是不同的,這些函數由內核開發者實現,驅動開發者不用實現這類函數)
(5)數據到達通用塊層后,就對塊設備發出寫請求。內核利用通用塊層的啟動I/O調度器,對數據進行排序。
(6)同用塊層下面是"I/O調度器"。調度器作用是把物理上相鄰的讀寫合並在一起,這樣可以加快訪問速度。
(7)最后快設備驅動向磁盤發送指令和數據,將數據寫入磁盤。
 
 

<基本概念>

a:塊設備(block device)
-----是一種具有一定結構的 隨機存取設備,對這種設備的 讀寫是按塊進行的,他使用緩沖區來存放暫時的數據,待條件成熟后,從緩存一次性寫入設備或者從設備一次性讀到緩沖區。
b:字符設備(Character device)
---是一個順序的數據流設備,對這種設備的讀寫是按字符進行的,而且這些字符是連續地形成一個數據流。 他不具備緩沖區,所以對這種設備的讀寫是實時的。
<linux 塊設備驅動架構圖>
 a:架構分析
1)struct bio
------當一個進程被Read時,首先讀取cache 中有沒有相應的文件,這個cache由一個buffer_head結構讀取。如果沒有,文件系統就會利用塊設備驅動去讀取磁盤扇區的數據。於是read()函數就會初始化一個bio結構體,並提交給通用塊層。 通常用 一個bio結構 體來對應 一個I/O請求
(1)內核結構如下:
 
 
1 struct bio {
2 sector
_t bi_sector; /* 要傳輸的第一個扇區 */
3 struct bio
*bi_next; /* 下一個 bio */
4 struct block
_device*bi_bdev;
5 unsigned long bi
_flags; /* 狀態、命令等 */
6 unsigned long bi
_rw; /* 低位表示 READ/WRITE,高位表示優先級*/
7
8 unsigned short bi
_vcnt; /* bio_vec 數量 */
9 unsigned short bi
_idx; /* 當前 bvl_vec 索引 */
10
11 /
* 執行物理地址合並后 sgement 的數目 */
12 unsigned short bi
_phys_segments;
13
14 unsigned int bi
_size;
15
16 /
* 為了明了最大的 segment 尺寸,我們考慮這個 bio 中第一個和最后一個
17 可合並的 segment 的尺寸 */
18 unsigned int bi
_hw_front_size;
19 unsigned int bi
_hw_back_size;
20
21 unsigned int bi
_max_vecs; /* 我們能持有的最大 bvl_vecs */
22 unsigned int bi
_comp_cpu; /* completion CPU */
23
24 struct bio
_vec *bi_io_vec; /* 實際的 vec 列表 */
25
26 bio
_end_io_t *bi_end_io;
27 atomic
_t bi_cnt;
28
29 void
*bi_private;
30 #if defined(CONFIG
_BLK_DEV_INTEGRITY)
31 struct bio
_integrity_payload *bi_integrity; /* 數據完整性 */
32 #endif
33
34 bio
_destructor_t *bi_destructor; /* 析構 */
35 };
(2)bio的核心是一個被稱為bi_io_vec的數組,它由bio_vec組成( 也就是說bio由許多bio_vec組成)。內核定義如下:
1 struct bio_vec {
2 struct page
*bv_page; /* 頁指針 */
3 unsigned int bv
_len; /* 傳輸的字節數 */
4 unsigned int bv
_offset; /* 偏移位置 */
5 };
bio_vec描述一個特定的片段,片段所在的物理頁,塊在物理頁中的偏移頁,整個bio_io_vec結構表示一個 完整的緩沖區。當一個塊被調用內存時,要儲存在一個緩沖區,每個緩沖區與一個塊對應,所以每一個緩沖區獨有一個對應的描述符,該描述符用buffer_head結構表示:
  • struct buffer_head 
  •     unsigned long b_state;                    /* buffer state bitmap (see above) */
  •     struct buffer_head *b_this_page;      /* circular list of page's buffers */
  •     struct page *b_page;                       /* the page this bh is mapped to */
  •  
  •     sector_t b_blocknr;                          /* start block number */
  •     size_t b_size;                                   /* size of mapping */
  •     char *b_data;                                  /* pointer to data within the page */
  •  
  •    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 is
  •                                                              associated with */
  •    atomic_t b_count;                          /* users using this buffer_head */
  • };
(3)bio和buffer_head之間的使用關系
核心ll_rw_block函數:
  • void ll_rw_block(int rw, int nr, struct buffer_head *bhs[])
  • {
  •     int i;
  •  
  •     for (= 0; i < nr; i ) {
  •         struct buffer_head *bh = bhs[i];
  •  
  •         if (!trylock_buffer(bh))
  •             continue;
  •         if (rw == WRITE) {
  •             if (test_clear_buffer_dirty(bh)) {
  •                 bh->b_end_io = end_buffer_write_sync;
  •                 get_bh(bh);
  •                 submit_bh(WRITE, bh);
  •                 continue;
  •             }
  •         } else {
  •             if (!buffer_uptodate(bh)) {
  •                 bh->b_end_io = end_buffer_read_sync;
  •                 get_bh(bh);
  •                 submit_bh(rw, bh);
  •                 continue;
  •             }
  •         }
  •         unlock_buffer(bh);
  •     }
  • }
     
核心submit_bh()函數:
 
  • int submit_bh(int rw, struct buffer_head * bh)
  • {
  •     struct bio *bio;
  •     int ret = 0;
  •  
  •     BUG_ON(!buffer_locked(bh));
  •     BUG_ON(!buffer_mapped(bh));
  •     BUG_ON(!bh->b_end_io);
  •     BUG_ON(buffer_delay(bh));
  •     BUG_ON(buffer_unwritten(bh));
  •  
  •     /*
  •      * Only clear out a write error when rewriting
  •      */
  •     if (test_set_buffer_req(bh) && (rw & WRITE))
  •         clear_buffer_write_io_error(bh);
  •  
  •     /*
  •      * from here on down, it's all bio -- do the initial mapping,
  •      * submit_bio -> generic_make_request may further map this bio around
  •      */
  •     bio = bio_alloc(GFP_NOIO, 1);
  •  
  •     bio->bi_sector = bh->b_blocknr * (bh->b_size >> 9);
  •     bio->bi_bdev = bh->b_bdev;
  •     bio->bi_io_vec[0].bv_page = bh->b_page;
  •     bio->bi_io_vec[0].bv_len = bh->b_size;
  •     bio->bi_io_vec[0].bv_offset = bh_offset(bh);
  •  
  •     bio->bi_vcnt = 1;
  •     bio->bi_idx = 0;
  •     bio->bi_size = bh->b_size;
  •  
  •     bio->bi_end_io = end_bio_bh_io_sync;
  •     bio->bi_private = bh;
  •  
  •     bio_get(bio);
  •     submit_bio(rw, bio);
  •  
  •     if (bio_flagged(bio, BIO_EOPNOTSUPP))
  •         ret = -EOPNOTSUPP;
  •  
  •     bio_put(bio);
  •     return ret;
  • }
這個函數主要是調用submit_bio,最終調用generic_make_request去完成將bio傳遞給驅動去處理。如下所示:
 
  • void generic_make_request(struct bio *bio)
  • {
  •     struct bio_list bio_list_on_stack;
  •  
  •     if (!generic_make_request_checks(bio))
  •         return;
  •  
  •  
  •     if (current->bio_list) {
  •         bio_list_add(current->bio_list, bio);
  •         return;
  •     }
  •  
  •     BUG_ON(bio->bi_next);
  •     bio_list_init(&bio_list_on_stack);
  •     current->bio_list = &bio_list_on_stack;
  •     do {
  •         struct request_queue *= bdev_get_queue(bio->bi_bdev);
  •  
  •         q->make_request_fn(q, bio);
  •  
  •         bio = bio_list_pop(current->bio_list);
  •     } while (bio);
  •     current->bio_list = NULL; /* deactivate */
  • }
     
這個函數主要是取出塊設備相應的隊列中的每個設備,在調用塊設備驅動的make_request,如果沒有指定make_request就調用內核默認的__make_request,這個函數主要作用就是 調用I/O調度算法將bio合並,或插入到隊列中合適的位置中去。
 
2)struct request
------提交工作由submit_bio()去完成,通用層在調用相應的設備IO調度器,這個調度器的調度算法,將這個bio合並到已經存在的request中,或者創建一個新的request,並將創建的插入到請求隊列中。最后就剩下塊設備驅動層來完成后面的所有工作。(Linux系統中,對塊設備的IO請求, 都會向塊設備驅動發出一個請求,在驅動中用request結構體描述)
內核結構如下:
1 struct request {
2 struct list
_head queuelist;
3 struct call
_single_data csd;
4 int cpu;
5
6 struct request
_queue *q;
7
8 unsigned int cmd
_flags;
9 enum rq
_cmd_type_bits cmd_type;
10 unsigned long atomic
_flags;
11
12 /
* 維護 I/O submission BIO 遍歷狀態
13 * hard_開頭的成員僅用於塊層內部,驅動不應該改變它們
14 */
15
16 sector
_t sector; /* 要提交的下一個 sector */
17 sector
_t hard_sector; /* 要完成的下一個 sector */
18 unsigned long nr
_sectors; /* 剩余需要提交的 sector */
19 unsigned long hard
_nr_sectors; /*剩余需要完成的 sector */
20 /
* 在當前 segment 中剩余的需提交的 sector */
21 unsigned int current
_nr_sectors;
22
23 /
*在當前 segment 中剩余的需完成的 sector */
24 unsigned int hard
_cur_sectors;
25
26 struct bio
*bio;
27 struct bio
*biotail;
28
29 struct hlist
_node hash;
30 union {
31 struct rb
_node rb_node; /* sort/lookup */
32 void
*completion_data;
33 };
34
35 /
*
36 * I/O 調度器可獲得的兩個指針,如果需要更多,請動態分配
37 */
38 void
*elevator_private;
39 void
*elevator_private2;
40
41 struct gendisk
*rq_disk;
42 unsigned long start
_time;
43
44 /
* scatter-gather DMA 方式下 addr+len 對的數量(執行物理地址合並后)
45
*/
46 unsigned short nr
_phys_segments;
47
48 unsigned short ioprio;
49
50 void
*special;
51 char
*buffer;
52
53 int tag;
54 int errors;
55
56 int ref
_count;
57
58 unsigned short cmd
_len;
59 unsigned char
__cmd[BLK_MAX_CDB];
60 unsigned char
*cmd;
61
62 unsigned int data
_len;
63 unsigned int extra
_len;
64 unsigned int sense
_len;
65 void
*data;
66 void
*sense;
67
68 unsigned long deadline;
69 struct list
_head timeout_list;
70 unsigned int timeout;
71 int retries;
72
73 /
*
74 * 完成回調函數
75 */
76 rq
_end_io_fn *end_io;
77 void
*end_io_data;
78
79 struct request
*next_rq;
80 };
30 return 0;
31 out
_queue: unregister_blkdev(XXX_MAJOR, "xxx");
32 out: put
_disk(xxx_disks);
33 blk
_cleanup_queue(xxx_queue);
34
35 return -ENOMEM;
36 }
(3)請求隊列初始化:
(3)-1:請求隊列數據結構
(3)-2: request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
第一個參數是指向"請求處理函數"的指針,該函數直接和硬盤打交道,用來處理數據在內存和硬盤之間的傳輸。該函數整體的作用就是為了分配請求隊列,並初始化。
(3)-3: typedef void (request_fn_proc)(struct reqest_queue *q)
該函數作為上述函數(request_queue_t *blk_init_queue(request_fn_proc *rfn,spinlock_t *lock))的參數,主要作用就是處理請求隊列中的bio,完成數據在內存和硬盤之間的傳遞。(注意:該函數參數中的bio都是經過i/o調度器的
(3)-4: typedef int (make_request_fn)(struct request_queue *q,struct bio *bio)
該函數是的第一個參數是請求隊列,第二個參數是bio,該函數的作用是 根據bio生成一個request(所以叫制造請求函數)。
注意:在想 不使用I/O調度器的時候,就應該在該函數中實現,對每一傳入該函數的bio之間進行處理,完成數據在內存和硬盤的之間的傳輸,這樣就可以不使用" request_fn_proc"函數了。(所以可以看出來,如果使用i/o調度器,make_request_fn函數是在request_fn_proc函數之前執行)
 
<I/O調度器的使用與否>
a:背景
------I/O調度器看起來可以提高訪問速度,但是這是並不是最快的,因為I/O調度過程會花費很多時間。最快的方式就是不使用I/O調度器
b:請求隊列和I/O調度器
------要脫離I/O調度器,就必須了解請求隊列request_queue,因為I/O調度器和請求隊列是綁定在一起的。其關系如下:
 如山圖所示,請求隊列request_queue 中的elevator指針式指向I/O調度函數的。
 
b: 通用塊層函數調用關系(對bio的處理過程)
b-1:調用框圖
 
b-2:具體分析
(1)當需要讀寫一個數據的時候,通用塊層,會根據用戶空間的請求,生成一個bio結構體。
(2)准備好bio后,會調用函數generic_make_request()函數,函數原形如下:
void generic_make_request(struct bio *bio)
(3)該函數會調用底層函數:
static inline void _generic_make_request(struct bio *bio);
(4)到這里會分層兩種情況:
第一種,調用請求隊列中 自己定義的make_request_fn()函數,那問題來了,系統怎么知道這個自己定義函數在哪里呢?由內核函數blk_queue_make_request()函數指定,函數原形:
void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn);
 
第二種,使用請求隊列中 系統默認__make_request()函數,函數原形“
static int __make_request(struct request_queue *q,struct bio *bio);
該函數會啟動I/O調度器,對bio進行調度處理,bio結構或被合並到請求隊列的一個請求結構的request中。最后調用request_fn_proc()將數據寫入或讀出塊塊設備。
 
c:使用I/O調度器和不使用I/O調度器
c-1:不使用i/o調度器(blk_alloc_queue())
bio的流程完全由驅動開發人員控制,要達到這個目的,必須使用函數blk_alloc_queue()來申請請求隊列,然后使用函數blk_queue_make_requset()給bio指定 具有request_fn_proc()功能的函數Virtual_blkdev_make_request來完成數據在內存和硬盤之間的傳輸(該函數本來是用來將bio加入request中的)。
static int Virtual_blkdev_make_request(struct requset_queue *q,structb bio *bio)
{
   //因為不使用I/O調度算法,直接在該函數中完成數據在內存和硬盤之間的數據傳輸,該函數
   //代替了request_fn_proc()函數的功能
   ............
}
Virtual_blkdev_queue = blk_alloc_queue(GFP_KERNEL)
if(!Virtual_blkdev_queue)
{
   ret=-ENOMEN;
   goto err_alloc_queue;
}
blk_queue_make_request(Virtual_blkdev_queue,Virtual_blkdev_make_request);
c-2:使用i/o調度器(blk_init_queue())
bio先經過__make_request()函數,I/O調度器,和request_fn_proc()完成內存和硬盤之間的數據傳輸。該過程使用函數blk_init_queue()函數完成隊列的初始化,並指定request_fn_proc():
struct request_queue* blk_inti_queue(request_fn_proc *rfn,spinlock_t *lock)

<總結驅動框架>
 a:塊設備驅動加載過程
(1)使用alloc_disk()函數分配通用磁盤gendisk的結構體。
(2)通過內核函數register_blkdev()函數注冊設備,該過程是一個可選過程。
   ( 也可以不用注冊設備,驅動一樣可以工作,該函數和字符設備的register_chrdev()函數相對應,對於大多數的塊設備,第一個工作就是相內核注冊自己,但是在Linux2.6以后,register_blkdev()函數的調用變得可選,內核中register_blkdev()函數的功能正在逐漸減少。基本上就只有如下作用:
1)分局major分配一個塊設備號
2)在/proc/devices中新增加一行數據,表示塊設備的信息
(3)根據是否需要I/O調度,將情況分為兩種情況,一種是使用請求隊列進行數據傳輸,一種是不使用請求隊列進行數據傳輸。
(4)初始化gendisk結構體的數據成員,包括major,fops,queue等賦初值。
(5)使用add_disk()函數激活磁盤設備(當調用該函數后就可以對磁盤進行操作(訪問),所以調用該函數之前必須所有的准備工作就緒)
b:塊設備驅動卸載過程
(1)使用del_gendisk()函數刪除gendisk設備,並使用put_disk()刪除對gendisk設備的引用;
(2)使用blk_clean_queue()函數清楚請求隊列,並釋放請求隊列所占用的資源。
(3)如果在模塊加載函數中使用register_blkdev()注冊設備,那么就需要調用unregister_blkdev()函數注銷設備並釋放對設備的引用。
 
<塊設備驅動代碼示例(不使用I/O調度器)>
制造請求函數(在這里完成數據的讀寫)
 
卸載函數
 
 

<wiz_tmp_tag id="wiz-table-range-border" contenteditable="false" style="display: none;">


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM