1. 基本概念
塊設備是Linux三大設備之一,其驅動模型主要針對磁盤,Flash等存儲類設備,塊設備(blockdevice)是一種具有一定結構的隨機存取設備,對這種設備的讀寫是按塊(所以叫塊設備)進行的,他使用緩衝區來存放暫時的數據,待條件成熟後,從緩存一次性寫入設備或者從設備一次性讀到緩衝區。
1.1 塊設備結構
塊設備由Page->Segment->Block->Sector的層次結構組成。
Page就是內存映射的最小單位;Segment就是一個Page中我們要操作的一部分,由若干個相鄰的塊組成;Block是邏輯上的進行數據存取的最小單位,是文件系統的抽象,邏輯塊的大小是在格式化的時候確定的, 一個Block最多僅能容納一個文件(即不存在多個文件同一個block的情況)。如果一個文件比block小,他也會佔用一個block,因而block中空餘的空間會浪費掉。而一個大文件,可以佔多個甚至數十個成百上千萬的block。Linux內核要求 Block_Size = Sector_Size * (2的n次方),並且Block_Size <= 內存的Page_Size(頁大小), 如ext2 fs的block缺省是4k。若block太大,則存取小文件時,有空間浪費的問題;若block太小,則硬盤的 Block 數目會大增,而造成inode在指向block的時候的一些搜尋時間的增加,又會造成大文件讀寫方面的效率較差,block是VFS和文件系統傳送數據的基本單位。block對應磁盤上的一個或多個相鄰的扇區,而VFS將其看成是一個單一的數據單元,塊設備的block的大小不是唯一的,創建一個磁盤文件系統時,管理員可以選擇合適的扇區的大小,同一個磁盤的幾個分區可以使用不同的塊大小。此外,對塊設備文件的每次讀或寫操作是一種"原始"訪問,因爲它繞過了磁盤文件系統,內核通過使用最大的塊(4096)執行該操作。Linux對內存中的block會被進一步劃分爲Sector,Sector是硬件設備傳送數據的基本單位,這個Sector就是512byte,和物理設備上的概念不一樣,如果實際的設備的sector不是512byte,而是4096byte(eg SSD),那麼只需要將多個內核sector對應一個設備sector即可。
1.2 塊設備與字符設備的區別
作爲一種存儲設備,和字符設備相比,塊設備有以下幾種不同:
字符設備 | 塊設備 |
---|---|
1byte | 塊,硬件塊各有不同,但是內核都使用512byte描述 |
順序訪問 | 隨機訪問 |
沒有緩存,實時操作 | 有緩存,不是實時操作 |
一般提供接口給應用層 | 塊設備一般提供接口給文件系統 |
是被用戶程序調用 | 由文件系統程序調用 |
2. 塊設備模型
下圖是Linux中的塊設備模型示意圖,應用層程序有兩種方式訪問一個塊設備:/dev和文件系統掛載點,前者和字符設備一樣,通常用於配置,後者就是我們mount之後通過文件系統直接訪問一個塊設備了。
1. read()系統調用最終會調用一個適當的VFS函數(read()-->sys_read()-->vfs_read()),將文件描述符fd和文件內的偏移量offset傳遞給它。
2. VFS會判斷這個SCI的處理方式,如果訪問的內容已經被緩存在RAM中(磁盤高速緩存機制),就直接訪問,否則從磁盤中讀取。
3. 爲了從物理磁盤中讀取,內核依賴映射層mapping layer,即上圖中的磁盤文件系統。
3.1 確定該文件所在文件系統的塊的大小,並根據文件塊的大小計算所請求數據的長度。本質上,文件被拆成很多塊,因此內核需要確定請求數據所在的塊。
3.2 映射層調用一個具體的文件系統的函數,這個層的函數會訪問文件的磁盤節點,然後根據邏輯塊號確定所請求數據在磁盤上的位置。
4. 內核利用通用塊層(generic block layer)啓動IO操作來傳達所請求的數據,通常,一個IO操作只針對磁盤上一組連續的塊。
5. IO調度程序根據預先定義的內核策略將待處理的IO進行重排和合並。
6. 塊設備驅動程序向磁盤控制器硬件接口發送適當的指令,進行實際的數據操作。
3. 塊設備驅動調用過程
當我們要寫一個很小的數據到txt文件某個位置時,由於塊設備寫的數據是按扇區爲單位,但又不能破壞txt文件裏其它位置,那麼就引入了一個“緩存區”,將所有數據讀到緩存區裏,然後修改緩存數據,再將整個數據放入txt文件對應的某個扇區中,當我們對txt文件多次寫入很小的數據的話,那麼就會重複不斷地對扇區讀出,寫入,這樣會浪費很多時間在讀/寫硬盤上,所以內核提供了一個隊列的機制,再沒有關閉txt文件之前,會將讀寫請求進行優化,排序,合併等操作,從而提高訪問硬盤的效率(IO調度)。
IO調度其實就是電梯算法。我們知道,磁盤是的讀寫是通過機械性的移動磁頭來實現讀寫的,理論上磁盤設備滿足塊設備的隨機讀寫的要求,但是出於節約磁盤,提高效率的考慮,我們希望當磁頭處於某一個位置的時候,一起將最近需要寫在附近的數據寫入,而不是這寫一下,那寫一下然後再回來,IO調度就是將上層發下來的IO請求的順序進行重新排序以及對多個請求進行合併,這樣就可以實現上述的提高效率、節約磁盤的目的。這種解決問題的思路使用電梯算法,一個運行中的電梯,一個人20樓->1樓,另外一個人15->5樓,電梯不會先將第一個人送到1樓再去15樓接第二個人將其送到5樓,而是從20樓下來,到15樓的時候停下接人,到5樓將第二個放下,最後到達1樓,一句話,電梯算法最終服務的優先順序並不按照按按鈕的先後順序。
當我們對一個*.txt寫入數據時,文件系統會轉換爲對塊設備上扇區的訪問,也就是調用ll_rw_block()函數,從這個函數開始就進入了設備層。
3.1 ll_rw_block()函數
ll_rw_block()函數的部分代碼如下(位於/fs/buffer.c):
/*
* rw:讀寫標誌位
* nr:bhs[]長度
* bhs[]:要讀寫的數據數組
*/
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]; //獲取nr個buffer_head
... ...
if (rw == WRITE || rw == SWRITE) {
if (test_clear_buffer_dirty(bh)) {
... ...
submit_bh(WRITE, bh); //提交WRITE寫標誌的buffer_head
continue;
}}
else {
if (!buffer_uptodate(bh)) {
... ...
submit_bh(rw, bh); //提交其它標誌的buffer_head
continue;
}}
unlock_buffer(bh); }
}
其中buffer_head結構體定義如下:
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完成方法
void *b_private; //完成方法數據
struct list_head b_assoc_buffers; //相關映射鏈表
/* mapping this buffer is associated with */
struct address_space *b_assoc_map;
atomic_t b_count; //緩衝區使用計數
};
submit_bh()函數就是通過bh來構造bio,然後調用submit_bio()提交bio,submit_bio()函數如下:
void submit_bio(int rw, struct bio *bio)
{
... ...
generic_make_request(bio);
}
最終調用generic_make_request(),把bio數據提交到相應塊設備的請求隊列中,generic_make_request()函數主要是實現對bio的提交處理。
3.2 generic_make_request()函數
generic_make_request()函數如下:
void generic_make_request(struct bio *bio)
{
if (current->bio_tail) { // current->bio_tail不爲空,表示有bio正在提交
*(current->bio_tail) = bio; //將當前的bio放到之前的bio->bi_next裏面
bio->bi_next = NULL; //更新bio->bi_next=0;
current->bio_tail = &bio->bi_next; //然後將當前的bio->bi_next放到current->bio_tail裏,使下次的bio就會放到當前bio->bi_next裏面了
return;
}
BUG_ON(bio->bi_next);
do {
current->bio_list = bio->bi_next;
if (bio->bi_next == NULL)
current->bio_tail = ¤t->bio_list;
else
bio->bi_next = NULL;
__generic_make_request(bio); //調用__generic_make_request()提交bio
bio = current->bio_list;
} while (bio);
current->bio_tail = NULL; /* deactivate */
}
__generic_make_request()首先由bio對應的block_device獲取申請隊列q,然後要檢查對應的設備是不是分區,如果是分區的話要將扇區地址進行重新計算,最後調用q的成員函數make_request_fn完成bio的遞交。__generic_make_request()函數如下:
static inline void __generic_make_request(struct bio *bio)
{
request_queue_t *q;
int ret;
... ...
do{
q = bdev_get_queue(bio->bi_bdev); //通過bio->bi_bdev獲取申請隊列q
... ...
ret = q->make_request_fn(q, bio); //提交申請隊列q和bio
}while (ret);
}
在內核中搜索make_request_fn,調用如下:
blk_init_queue_node()
blk_queue_make_request(q, __make_request)
make_request_fn
最終q->make_request_fn()執行的是__make_request()函數。
3.3 __make_request()函數
static int __make_request(request_queue_t *q, struct bio *bio)
{
struct request *req; //塊設備本身的隊列
... ...
//將之前的申請隊列q和傳入的bio,通過排序,合併在本身的req隊列中
el_ret = elv_merge(q, &req, bio);
... ...
init_request_from_bio(req, bio); //合併失敗,單獨將bio放入req隊列
add_request(q, req); //單獨將之前的申請隊列q放入req隊列
... ...
__generic_unplug_device(q); //執行申請隊列的處理函數
}
上面的elv_merge()函數,就是之前所說的電梯算法函數。
3.4 總結
讀寫塊設備在內核中的調用過程如下:
ll_rw_block() //進入內核中設備層,提交buff_head緩存區結構體
submit_bio() //用buff_head構造bio,提交bio
submit_bio() //把提交上來的bio提交的到相應塊設備的請求隊列中
generic_make_request() //對bio進行提交處理
__generic_make_request() //獲取等待隊列q,提交bio
__make_request() //合併q和bio,執行隊列
elv_merge() //使用電梯算法合併q和bio
__generic_unplug_device() //執行隊列
q->request_fn //調用隊列處理函數
這個隊列處理函數顯然就是要我們驅動去實現的,參考內核自帶的塊設備驅動程序drivers/block/xd.c,使用隊列如下:
static struct request_queue *xd_queue; //定義一個申請隊列xd_queue
xd_queue = blk_init_queue(do_xd_request, &xd_lock); //分配一個申請隊列
隊列處理函數如下:
static void do_xd_request (request_queue_t * q)
{
struct request *req;
if (xdc_busy)
return;
while ((req = elv_next_request(q)) != NULL) //(1)while獲取申請隊列中的需要處理的申請
{
int res = 0;
... ...
for (retry = 0; (retry < XD_RETRIES) && !res; retry++)
res = xd_readwrite(rw, disk, req->buffer, block, count);//將獲取申請req的buffer成員 讀寫到disk扇區中,當讀寫失敗返回0,成功返回1
end_request(req, res); //申請隊列中的的申請已處理結束,當res=0,表示讀寫失敗
}
}
最終申請隊列q調用驅動來對扇區讀寫。
4. 塊設備驅動程序實現流程
分析內核塊設備驅動drivers/block/xd.c如下:
static DEFINE_SPINLOCK(xd_lock); //定義一個自旋鎖,用到申請隊列中
static struct request_queue *xd_queue; //定義一個申請隊列xd_queue
static int __init xd_init(void) //入口函數
{
if (register_blkdev(XT_DISK_MAJOR, "xd")) //1.創建一個塊設備,保存在/proc/devices中
goto out1;
xd_queue = blk_init_queue(do_xd_request, &xd_lock); //2.分配一個申請隊列,後面會賦給gendisk結構體的queue成員
... ...
for (i = 0; i < xd_drives; i++) {
... ...
struct gendisk *disk = alloc_disk(64); //3.分配一個gendisk結構體, 64:次設備號個數,也稱爲分區個數
/* 4.接下來設置gendisk結構體 */
disk->major = XT_DISK_MAJOR; //設置主設備號
disk->first_minor = i<<6; //設置次設備號
disk->fops = &xd_fops; //設置塊設備驅動的操作函數
disk->queue = xd_queue; //設置queue申請隊列,用於管理該設備IO申請隊列
... ...
xd_gendisk[i] = disk;
}
... ...
for (i = 0; i < xd_drives; i++)
add_disk(xd_gendisk[i]); //5.註冊gendisk結構體
}
4.1 重點數據結構gendisk
Linux內核使用gendisk對象描述一個系統的中的塊設備,類似於Windows系統中的磁盤分區和物理磁盤的關係,OS眼中的磁盤都是邏輯磁盤,也就是一個磁盤分區,一個物理磁盤可以對應多個磁盤分區,在Linux中,這個gendisk就是用來描述一個邏輯磁盤,也就是一個磁盤分區。
struct gendisk {
int major; /*設備主設備號*/
int first_minor; /*起始次設備號*/
int minors; /*次設備號的數量,也稱爲分區數量,如果改值爲1,表示無法分區*/
char disk_name[32]; /*設備名稱*/
struct hd_struct **part; /*分區表的信息*/
int part_uevent_suppress;
struct block_device_operations *fops; /*塊設備操作集合 */
struct request_queue *queue; /*申請隊列,用於管理該設備IO申請隊列的指針*/
void *private_data; /*私有數據*/
sector_t capacity; /*扇區數,512字節爲1個扇區,描述設備容量*/
....
};
4.2 塊設備驅動程序流程
1. 創建一個塊設備
2. 分配一個申請隊列
3. 分配一個gendisk結構體
4. 設置gendisk結構體的成員
5. 註冊gendisk結構體
5. 編寫代碼
接下來仿造內核塊設備驅動drivers/block/xd.c編寫自己的驅動程序。
5.1 代碼框架
5.1.1 入口函數中
1. 使用register_blkdev()創建一個塊設備
2. blk_init_queue()使用分配一個申請隊列,並賦申請隊列處理函數
3. 使用alloc_disk()分配一個gendisk結構體 4
4. 設置gendisk結構體的成員
4.1 設置成員參數(major、first_minor、disk_name、fops)
4.2 設置queue成員,等於之前分配的申請隊列
4.3 通過set_capacity()設置capacity成員,等於扇區數
4.4 使用kzalloc()來獲取緩存地址,用做扇區
4.5 使用add_disk()註冊gendisk結構體
5.1.2 申請隊列的處理函數中
1. while循環使用elv_next_request()獲取申請隊列中每個未處理的申請
2. 使用rq_data_dir()來獲取每個申請的讀寫命令標誌,爲 0(READ)表示讀, 爲1(WRITE)表示寫
3. 使用memcp()來讀或者寫扇區(緩存)
4. 使用end_request()來結束獲取的每個申請
5.1.3 出口函數中
1. 使用put_disk()和del_gendisk()來註銷,釋放gendisk結構體
2. 使用kfree()釋放磁盤扇區緩存
3. 使用blk_cleanup_queue()清除內存中的申請隊列
4. 使用unregister_blkdev()卸載塊設備
5.2 編寫代碼
驅動程序ramblock.c代碼如下:
/*參考xd.c頭文件*/
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/interrupt.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/timer.h>
#include <linux/genhd.h>
#include <linux/hdreg.h>
#include <linux/ioport.h>
#include <linux/init.h>
#include <linux/wait.h>
#include <linux/blkdev.h>
#include <linux/blkpg.h>
#include <linux/delay.h>
#include <linux/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>
#include <asm/dma.h>
static struct gendisk *ramblock_disk;
static request_queue_t *ramblock_queue;
static DEFINE_SPINLOCK(ramblock_lock);
static int major;
#define RAMBLOCK_SIZE (1024*1024)
static unsigned char *ramblock_buf;
static int fd_getgeo(struct block_device *bdev, struct hd_geometry *geo)
{
int drive = MINOR(bdev->bd_dev) & 3;
/*容量=heads*cylinders*sectors*512字節*/
geo->heads = 2; //磁頭,有多少面,假設2
geo->cylinders = 32; //柱面,有多少環,假設32
geo->sectors = RAMBLOCK_SIZE/2/32/512; //按公式算出來扇區數
return 0;
}
static struct block_device_operations ramblock_fops = {
.owner = THIS_MODULE,
.getgeo = fd_getgeo,
};
static void do_ramblock_request (request_queue_t * q)
{
struct request *req;
while ((req = elv_next_request(q)) != NULL) { //以電梯調度算法執行下一請求
/*數據傳輸3要素,源,目的,長度*/
/*源*/
unsigned long offset = req->sector *512; //偏移值
/*目的*/
//req->buffer
/*長度*/
unsigned long len = req->current_nr_sectors *512; //長度
if(rq_data_dir(req)==READ) //如果是讀數據,就把ramblock_buf+offset裏的數據拷貝到req->buffer
{
memcpy(req->buffer,ramblock_buf+offset,len);
}
else
{
memcpy(ramblock_buf+offset,req->buffer,len);
}
end_request(req, 1); //結束獲取的申請
}
}
static int ramblock_init(void)
{
/* 1. 創建一個塊設備 */
major=register_blkdev(0, "ramblock");
/* 2. 分配一個gendisk結構體 */
ramblock_disk=alloc_disk(16); //不分區
/* 3. 分配設置隊列(用於填充gendisk) */
ramblock_queue=blk_init_queue(do_ramblock_request, &ramblock_lock);
/* 4. 設置gendisk結構體的成員 */
ramblock_disk->major = major;
ramblock_disk->first_minor = 0;
sprintf(ramblock_disk->disk_name, "ramblock");
ramblock_disk->fops = &ramblock_fops;
ramblock_disk->queue = ramblock_queue;
set_capacity(ramblock_disk, RAMBLOCK_SIZE/512);
/* 5. 分配緩存地址,用做扇區 */
ramblock_buf=kzalloc(RAMBLOCK_SIZE,GFP_KERNEL);
/* 6. 註冊gendisk結構體 */
add_disk(ramblock_disk);
return 0;
}
static void ramblock_exit(void)
{
del_gendisk(ramblock_disk);
put_disk(ramblock_disk);
kfree(ramblock_buf);
blk_cleanup_queue(ramblock_queue);
unregister_blkdev(major,"ramblock");
}
module_init(ramblock_init);
module_exit(ramblock_exit);
MODULE_LICENSE("GPL");
Makefile代碼如下:
KERN_DIR = /work/system/linux-2.6.22.6 //內核目錄
all:
make -C $(KERN_DIR) M=`pwd` modules
clean:
make -C $(KERN_DIR) M=`pwd` modules clean
rm -rf modules.order
obj-m += ramblock.o
6. 測試
內核:linux-2.6.22.6
編譯器:arm-linux-gcc-3.4.5
環境:ubuntu9.10
1. 首先編譯驅動程序。在驅動文件目錄下執行:
make
2. 安裝驅動,在開發板上執行:
insmod ramblock.ko
3. 將memblock塊設備格式化爲dos磁盤類型,在開發板上執行:
mkdosfs /dev/ramblock (使用韋東山老師的文件系統fs_mini_mdev/sbin中將mkdosfs拷貝到/first_fs/usr/sbin目錄下就可以用mkdosfs工具了)
4. 掛載塊設備到/tmp目錄下,在開發板上執行:
mount /dev/ramblock /tmp/
5. 接下來在/tmp目錄下創建編輯一個文件,最終都會保存在/dev/ memblock塊設備裏面。
cd /tmp
vi lzh.txt (隨便輸入內容保存)
cd /
umount /tmp/ (不能在/tmp目錄裏進行卸載。此時重新掛接,剛纔新建的lzh.txt依然存在)
cat /dev/ramblock > /ramblock.bin(將/dev/ramblock磁盤內容存到bin文件裏,也就是說剛纔新建的lzh.txt會存入bin文件內)
6. 使用” -o loop”將ramblock.bin文件模擬成磁盤掛接到/mnt。ramblock.bin必須放在文件系統根目錄裏並在該目錄執行該命令。注意:如果該磁盤沒有格式化分區,或模擬磁盤的文件生成前沒有格式化,會掛接不成功,提示“mount: you must specify the filesystem type”),如下:
sudo mount -o loop ramblock.bin /mnt
mnt目錄下就有ramblock.bin文件裏的內容了,如下:
7. 磁盤分區
7.1 重裝驅動
7.2 fdisk /dev/ramblock (磁盤分區命令)
7.3 依次執行以下命令:
m (查看幫助)
n (添加分區)
p (主分區)
1 (第一個主分區)
1 (最外面的幾個柱面作爲第一個主分區開始,這裏設爲1)
5 (最外面的幾個柱面作爲第一個主分區結束,這裏設爲5)
重複7.2步驟再添加第二個主分區,完成後輸入p可以查看分區,輸入w將配置寫入分區表(該磁盤裏的第一個扇區))。
8. 查看效果,執行如下:
ls /dev/ramblock* -l
還可以分別進行磁盤格式化:
mkdosfs /dev/ramblock1
mkdosfs /dev/rambloc2
分別掛接磁盤:
mount /dev/ramblock1 /tmp/
mount /dev/ramblock2 /tmp/