<機械硬盤>
a:磁盤結(jié)構(gòu)
傳統(tǒng)的機械硬盤一般為3.5英寸硬盤,并由多個圓形蝶片組成,每個蝶片擁有獨立的機械臂和磁頭,每個堞片的圓形平面被劃分了不同的同心圓,每一個同心圓稱為一個磁道,位于最外面的道的周長最長稱為外道,最里面的道稱為內(nèi)道,通常硬盤廠商會將圓形蝶片最靠里面的一些內(nèi)道(速度較慢,影響性能)封裝起來不用;道又被劃分成不同的塊單元稱為扇區(qū),每個道的周長不同,現(xiàn)代硬盤不同長度的道劃分出來的扇區(qū)數(shù)也是不相同的,而磁頭不工作的時候一般位于內(nèi)道,如果追求響應(yīng)時間,則數(shù)據(jù)可存儲在硬盤的內(nèi)道,如果追求大的吞吐量,則數(shù)據(jù)應(yīng)存儲在硬盤的外道;
注意:;一個弧道被劃分成多個段,每一個段就是一個扇區(qū)
b:磁盤訪問
------SATA硬盤實現(xiàn)的是串行ATA協(xié)議,ATA下盤命令中記錄有LBA(Logic Block Address)起始地址和扇區(qū)數(shù);LBA地址實際上是一個ATA協(xié)議邏輯地址,硬盤的固件會解析收到的ATA命令,并將要訪問的LBA地址映射至某個磁道中的某個物理塊即扇區(qū)。操作系統(tǒng)暫可認為LBA地址就是硬盤的物理地址。
c:扇區(qū)
------硬盤的基本訪問單位,扇區(qū)的大小一般是512B(對于現(xiàn)在的有些磁盤的扇區(qū)>512B,比如光盤的一個扇區(qū)就是2048B,Linux將其看成4個扇區(qū),無非就是需要完成4次的讀寫)。
d:塊
------扇區(qū)是硬件傳輸數(shù)據(jù)的基本單位,硬件一次傳輸一個扇區(qū)的數(shù)據(jù)到內(nèi)存中。但是和扇區(qū)不同的是,塊是虛擬文件系統(tǒng)傳輸數(shù)據(jù)的基本單位。在Linux中,塊的大小必須是2的冪,但是不能超過一個頁的大?。?k)。(在X86平臺,一個頁的大小是4094個字節(jié),所以塊大小可以是512,1024,2048,4096)
------主要為了做scatter/gather DMA操作使用,同一個物理頁面中的在硬盤存儲介質(zhì)上連續(xù)的多個塊組成一個段。段的大小只與塊有關(guān),必須是塊的整數(shù)倍。所以塊通常包括多個扇區(qū),段通常包括多個塊,物理段通常包括多個段;段在內(nèi)核中由結(jié)構(gòu)struct bio_vec來描述,多個段的信息存放于struct bio結(jié)構(gòu)中的bio_io_vec指針數(shù)組中,段數(shù)組在后續(xù)的塊設(shè)備處理流程中會被合并成物理段,段結(jié)構(gòu)定義如下:
struct bio_vec {
struct page *bv_page; // 段所在的物理頁面結(jié)構(gòu),即bh->b_page
unsigned int bv_len; // 段的字節(jié)數(shù),即bh->b_size
unsigned int bv_offset; // 段在bv_page頁面中的偏移,即bh->b_data
};
f:文件塊
------大小定義和文件系統(tǒng)塊一樣;只是相對于文件的一個偏移邏輯塊,需要通過具體文件系統(tǒng)中的此文件對應(yīng)的inode所記錄的間接塊信息,換算成對應(yīng)的文件系統(tǒng)塊;此做法是為了將一個文件的內(nèi)容存于硬盤的不同位置,以提高訪問速度;即一個文件的內(nèi)容在硬盤是一般是不連續(xù)的;EXT2中,ext2_get_block()完成文件塊到文件系統(tǒng)塊的映射。
g:總結(jié)
------扇區(qū)由磁盤的物理特性決定;塊緩沖區(qū)由內(nèi)核代碼決定;塊由緩沖區(qū)決定,是塊緩沖區(qū)大小的整數(shù)倍(但是不能超過一個頁)。三者關(guān)系如下:
所以:扇區(qū)(512)≤塊≤頁(4096) 塊=n*扇區(qū)(n為整數(shù))
注意:段(struct bio_vec{})由多個塊組成,一個段就是一個內(nèi)存頁(如果一個塊是兩個扇區(qū)大小,也就是1024B,那么一個段的大小可以是1024,2018,3072,4096,也就是說段的大小只與塊有關(guān),而且是整數(shù)倍)。Linux系統(tǒng)一次讀取磁盤的大小是一個塊,而不是一個扇區(qū),塊設(shè)備驅(qū)動由此得名。
<塊設(shè)備處理過程>
a:linux 內(nèi)核中,塊設(shè)備將數(shù)據(jù)存儲與固定的大小的塊中,每個塊都有自己的固定地址。Linux內(nèi)核中塊設(shè)備和其他模塊的關(guān)系如下。
a:塊設(shè)備的處理過程涉及Linux內(nèi)核中的很多模塊,下面簡單描述之間的處理過過程。
(1)當(dāng)一個用戶程序要向磁盤寫入數(shù)據(jù)時,會發(fā)發(fā)出write()系統(tǒng)調(diào)用給內(nèi)核。
(2)內(nèi)核會調(diào)用虛擬文件系統(tǒng)相應(yīng)的函數(shù),將需要寫入發(fā)文件描述符和文件內(nèi)容指針傳遞給該函數(shù)。
(3)內(nèi)核需要確定寫入磁盤的位置,通過映射層知道需要寫入磁盤的哪一塊。
(4)根據(jù)磁盤的文件系統(tǒng)的類型,調(diào)用不同文件格式的寫入函數(shù),江蘇數(shù)據(jù)發(fā)送給通用塊層(比如ext2和ext3文件系統(tǒng)的寫入函數(shù)是不同的,這些函數(shù)由內(nèi)核開發(fā)者實現(xiàn),驅(qū)動開發(fā)者不用實現(xiàn)這類函數(shù))
(5)數(shù)據(jù)到達通用塊層后,就對塊設(shè)備發(fā)出寫請求。內(nèi)核利用通用塊層的啟動I/O調(diào)度器,對數(shù)據(jù)進行排序。
(6)同用塊層下面是"I/O調(diào)度器"。調(diào)度器作用是把物理上相鄰的讀寫合并在一起,這樣可以加快訪問速度。
(7)最后快設(shè)備驅(qū)動向磁盤發(fā)送指令和數(shù)據(jù),將數(shù)據(jù)寫入磁盤。
<基本概念>
a:塊設(shè)備(block device)
-----是一種具有一定結(jié)構(gòu)的隨機存取設(shè)備,對這種設(shè)備的讀寫是按塊進行的,他使用緩沖區(qū)來存放暫時的數(shù)據(jù),待條件成熟后,從緩存一次性寫入設(shè)備或者從設(shè)備一次性讀到緩沖區(qū)。
b:字符設(shè)備(Character device)
---是一個順序的數(shù)據(jù)流設(shè)備,對這種設(shè)備的讀寫是按字符進行的,而且這些字符是連續(xù)地形成一個數(shù)據(jù)流。他不具備緩沖區(qū),所以對這種設(shè)備的讀寫是實時的。
<linux 塊設(shè)備驅(qū)動架構(gòu)圖>
)struct bio
------當(dāng)一個進程被Read時,首先讀取cache 中有沒有相應(yīng)的文件,這個cache由一個buffer_head結(jié)構(gòu)讀取。如果沒有,文件系統(tǒng)就會利用塊設(shè)備驅(qū)動去讀取磁盤扇區(qū)的數(shù)據(jù)。于是read()函數(shù)就會初始化一個bio結(jié)構(gòu)體,并提交給通用塊層。通常用一個bio結(jié)構(gòu)體來對應(yīng)一個I/O請求。
(1)內(nèi)核結(jié)構(gòu)如下:
struct bio { sector_t bi_sector; /* 要傳輸?shù)牡谝粋€扇區(qū) */ struct bio *bi_next; /* 下一個 bio */ struct block_device*bi_bdev; unsigned long bi_flags; /* 狀態(tài)、命令等 */ unsigned long bi_rw; /* 低位表示 READ/WRITE,高位表示優(yōu)先級*/
unsigned short bi_vcnt; /* bio_vec 數(shù)量 */ unsigned short bi_idx; /* 當(dāng)前 bvl_vec 索引 */
/* 執(zhí)行物理地址合并后 sgement 的數(shù)目 */ unsigned short bi_phys_segments;
unsigned int bi_size;
/* 為了明了最大的 segment 尺寸,我們考慮這個 bio 中第一個和最后一個 可合并的 segment 的尺寸 */ unsigned int bi_hw_front_size; unsigned int bi_hw_back_size;
unsigned int bi_max_vecs; /* 我們能持有的最大 bvl_vecs 數(shù) */ unsigned int bi_comp_cpu; /* completion CPU */
struct bio_vec *bi_io_vec; /* 實際的 vec 列表 */
bio_end_io_t *bi_end_io; atomic_t bi_cnt;
void *bi_private; #if defined(CONFIG_BLK_DEV_INTEGRITY) struct bio_integrity_payload *bi_integrity; /* 數(shù)據(jù)完整性 */ #endif
bio_destructor_t *bi_destructor; /* 析構(gòu) */ }; |
(2)bio的核心是一個被稱為bi_io_vec的數(shù)組,它由bio_vec組成(
也就是說bio由許多bio_vec組成)。內(nèi)核定義如下:
struct bio_vec { struct page *bv_page; /* 頁指針 */ unsigned int bv_len; /* 傳輸?shù)淖止?jié)數(shù) */ unsigned int bv_offset; /* 偏移位置 */ }; |
bio_vec描述一個特定的片段,片段所在的物理頁,塊在物理頁中的偏移頁,整個bio_io_vec結(jié)構(gòu)表示一個
完整的緩沖區(qū)。當(dāng)一個塊被調(diào)用內(nèi)存時,要儲存在一個緩沖區(qū),每個緩沖區(qū)與一個塊對應(yīng),所以每一個緩沖區(qū)獨有一個對應(yīng)的描述符,該描述符用buffer_head結(jié)構(gòu)表示:
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之間的使用關(guān)系
核心ll_rw_block函數(shù):
void ll_rw_block(int rw, int nr, struct buffer_head *bhs[])
{
int i;
for (i = 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()函數(shù):
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[].bv_page = bh->b_page;
bio->bi_io_vec[].bv_len = bh->b_size;
bio->bi_io_vec[].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;
}
這個函數(shù)主要是調(diào)用submit_bio,最終調(diào)用generic_make_request去完成將bio傳遞給驅(qū)動去處理。如下所示:
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 *q = 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 */
}
這個函數(shù)主要是取出塊設(shè)備相應(yīng)的隊列中的每個設(shè)備,在調(diào)用塊設(shè)備驅(qū)動的make_request,如果沒有指定make_request就調(diào)用內(nèi)核默認的__make_request,這個函數(shù)主要作用就是調(diào)用I/O調(diào)度算法將bio合并,或插入到隊列中合適的位置中去。
2)struct request
------提交工作由submit_bio()去完成,通用層在調(diào)用相應(yīng)的設(shè)備IO調(diào)度器,這個調(diào)度器的調(diào)度算法,將這個bio合并到已經(jīng)存在的request中,或者創(chuàng)建一個新的request,并將創(chuàng)建的插入到請求隊列中。最后就剩下塊設(shè)備驅(qū)動層來完成后面的所有工作。(Linux系統(tǒng)中,對塊設(shè)備的IO請求,都會向塊設(shè)備驅(qū)動發(fā)出一個請求,在驅(qū)動中用request結(jié)構(gòu)體描述)
內(nèi)核結(jié)構(gòu)如下:
struct request {
struct list_head queuelist;
struct call_single_data csd;
int cpu;
struct request_queue *q;
unsigned int cmd_flags;
enum rq_cmd_type_bits cmd_type;
unsigned long atomic_flags;
/* 維護 I/O submission 的 BIO 遍歷狀態(tài)
* hard_開頭的成員僅用于塊層內(nèi)部,驅(qū)動不應(yīng)該改變它們
*/
sector_t sector; /* 要提交的下一個 sector */
sector_t hard_sector; /* 要完成的下一個 sector */
unsigned long nr_sectors; /* 剩余需要提交的 sector 數(shù) */
unsigned long hard_nr_sectors; /*剩余需要完成的 sector 數(shù)*/
/* 在當(dāng)前 segment 中剩余的需提交的 sector 數(shù) */
unsigned int current_nr_sectors;
/*在當(dāng)前 segment 中剩余的需完成的 sector 數(shù) */
unsigned int hard_cur_sectors;
struct bio *bio;
struct bio *biotail;
struct hlist_node hash;
union {
struct rb_node rb_node; /* sort/lookup */
void *completion_data;
};
/*
* I/O 調(diào)度器可獲得的兩個指針,如果需要更多,請動態(tài)分配
*/
void *elevator_private;
void *elevator_private2;
struct gendisk *rq_disk;
unsigned long start_time;
/* scatter-gather DMA 方式下 addr+len 對的數(shù)量(執(zhí)行物理地址合并后)
*/
unsigned short nr_phys_segments;
unsigned short ioprio;
void *special;
char *buffer;int tag;
int errors;
int ref_count;
unsigned short cmd_len;
unsigned char __cmd[BLK_MAX_CDB];
unsigned char *cmd;
unsigned int data_len;
unsigned int extra_len;
unsigned int sense_len;
void *data;
void *sense;
unsigned long deadline;
struct list_head timeout_list;
unsigned int timeout;
int retries;
/*
* 完成回調(diào)函數(shù)
*/
rq_end_io_fn *end_io;
void *end_io_data;
struct request *next_rq;
};
3)請求隊列初始化:
3.1:請求隊列數(shù)據(jù)結(jié)構(gòu)
3.2:request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)
第一個參數(shù)是指向"請求處理函數(shù)"的指針,該函數(shù)直接和硬盤打交道,用來處理數(shù)據(jù)在內(nèi)存和硬盤之間的傳輸。該函數(shù)整體的作用就是為了分配請求隊列,并初始化。
(3)-3:typedef void (request_fn_proc)(struct reqest_queue *q)
該函數(shù)作為上述函數(shù)(request_queue_t *blk_init_queue(request_fn_proc *rfn,spinlock_t *lock))的參數(shù),主要作用就是處理請求隊列中的bio,完成數(shù)據(jù)在內(nèi)存和硬盤之間的傳遞。(注意:該函數(shù)參數(shù)中的bio都是經(jīng)過i/o調(diào)度器的)
(3)-4:typedef int (make_request_fn)(struct request_queue *q,struct bio *bio)
該函數(shù)是的第一個參數(shù)是請求隊列,第二個參數(shù)是bio,該函數(shù)的作用是根據(jù)bio生成一個request(所以叫制造請求函數(shù))。
注意:在想不使用I/O調(diào)度器的時候,就應(yīng)該在該函數(shù)中實現(xiàn),對每一傳入該函數(shù)的bio之間進行處理,完成數(shù)據(jù)在內(nèi)存和硬盤的之間的傳輸,這樣就可以不使用"request_fn_proc"函數(shù)了。(所以可以看出來,如果使用i/o調(diào)度器,make_request_fn函數(shù)是在request_fn_proc函數(shù)之前執(zhí)行)
<I/O調(diào)度器的使用與否>
a:背景
------I/O調(diào)度器看起來可以提高訪問速度,但是這是并不是最快的,因為I/O調(diào)度過程會花費很多時間。最快的方式就是不使用I/O調(diào)度器
b:請求隊列和I/O調(diào)度器
------要脫離I/O調(diào)度器,就必須了解請求隊列request_queue,因為I/O調(diào)度器和請求隊列是綁定在一起的。其關(guān)系如下:
如山圖所示,請求隊列request_queue 中的elevator指針式指向I/O調(diào)度函數(shù)的。
b:通用塊層函數(shù)調(diào)用關(guān)系(對bio的處理過程)
b-1:調(diào)用框圖
b-2:具體分析
(1)當(dāng)需要讀寫一個數(shù)據(jù)的時候,通用塊層,會根據(jù)用戶空間的請求,生成一個bio結(jié)構(gòu)體。
(2)準(zhǔn)備好bio后,會調(diào)用函數(shù)generic_make_request()函數(shù),函數(shù)原形如下:
void generic_make_request(struct bio *bio)
(3)該函數(shù)會調(diào)用底層函數(shù):
static inline void _generic_make_request(struct bio *bio);
(4)到這里會分層兩種情況:
第一種,調(diào)用請求隊列中自己定義的make_request_fn()函數(shù),那問題來了,系統(tǒng)怎么知道這個自己定義函數(shù)在哪里呢?由內(nèi)核函數(shù)blk_queue_make_request()函數(shù)指定,函數(shù)原形:
void blk_queue_make_request(struct request_queue *q,make_request_fn *mfn);
第二種,使用請求隊列中系統(tǒng)默認__make_request()函數(shù),函數(shù)原形“
static int __make_request(struct request_queue *q,struct bio *bio);
該函數(shù)會啟動I/O調(diào)度器,對bio進行調(diào)度處理,bio結(jié)構(gòu)或被合并到請求隊列的一個請求結(jié)構(gòu)的request中。最后調(diào)用request_fn_proc()將數(shù)據(jù)寫入或讀出塊塊設(shè)備。
c:使用I/O調(diào)度器和不使用I/O調(diào)度器
c-1:不使用i/o調(diào)度器(blk_alloc_queue())
bio的流程完全由驅(qū)動開發(fā)人員控制,要達到這個目的,必須使用函數(shù)blk_alloc_queue()來申請請求隊列,然后使用函數(shù)blk_queue_make_requset()給bio指定具有request_fn_proc()功能的函數(shù)Virtual_blkdev_make_request來完成數(shù)據(jù)在內(nèi)存和硬盤之間的傳輸(該函數(shù)本來是用來將bio加入request中的)。
static int Virtual_blkdev_make_request(struct requset_queue *q,structb bio *bio) { //因為不使用I/O調(diào)度算法,直接在該函數(shù)中完成數(shù)據(jù)在內(nèi)存和硬盤之間的數(shù)據(jù)傳輸,該函數(shù) //代替了request_fn_proc()函數(shù)的功能 ............ } 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調(diào)度器(blk_init_queue())
bio先經(jīng)過__make_request()函數(shù),I/O調(diào)度器,和request_fn_proc()完成內(nèi)存和硬盤之間的數(shù)據(jù)傳輸。該過程使用函數(shù)blk_init_queue()函數(shù)完成隊列的初始化,并指定request_fn_proc():
struct request_queue* blk_inti_queue(request_fn_proc *rfn,spinlock_t *lock) |
<總結(jié)驅(qū)動框架>
a:塊設(shè)備驅(qū)動加載過程
(1)使用alloc_disk()函數(shù)分配通用磁盤gendisk的結(jié)構(gòu)體。
(2)通過內(nèi)核函數(shù)register_blkdev()函數(shù)注冊設(shè)備,該過程是一個可選過程。
(也可以不用注冊設(shè)備,驅(qū)動一樣可以工作,該函數(shù)和字符設(shè)備的register_chrdev()函數(shù)相對應(yīng),對于大多數(shù)的塊設(shè)備,第一個工作就是相內(nèi)核注冊自己,但是在Linux2.6以后,register_blkdev()函數(shù)的調(diào)用變得可選,內(nèi)核中register_blkdev()函數(shù)的功能正在逐漸減少。基本上就只有如下作用:
)分局major分配一個塊設(shè)備號
)在/proc/devices中新增加一行數(shù)據(jù),表示塊設(shè)備的信息)
(3)根據(jù)是否需要I/O調(diào)度,將情況分為兩種情況,一種是使用請求隊列進行數(shù)據(jù)傳輸,一種是不使用請求隊列進行數(shù)據(jù)傳輸。
(4)初始化gendisk結(jié)構(gòu)體的數(shù)據(jù)成員,包括major,fops,queue等賦初值。
(5)使用add_disk()函數(shù)激活磁盤設(shè)備(當(dāng)調(diào)用該函數(shù)后就可以對磁盤進行操作(訪問),所以調(diào)用該函數(shù)之前必須所有的準(zhǔn)備工作就緒)
b:塊設(shè)備驅(qū)動卸載過程
(1)使用del_gendisk()函數(shù)刪除gendisk設(shè)備,并使用put_disk()刪除對gendisk設(shè)備的引用;
(2)使用blk_clean_queue()函數(shù)清楚請求隊列,并釋放請求隊列所占用的資源。
(3)如果在模塊加載函數(shù)中使用register_blkdev()注冊設(shè)備,那么就需要調(diào)用unregister_blkdev()函數(shù)注銷設(shè)備并釋放對設(shè)備的引用。
<塊設(shè)備驅(qū)動代碼示例(不使用I/O調(diào)度器)>
制造請求函數(shù)(在這里完成數(shù)據(jù)的讀寫)