十六、Linux驅動之塊設備驅動

1. 基本概念

    塊設備是Linux三大設備之一,其驅動模型主要針對磁盤,Flash等存儲類設備,塊設備(blockdevice)是一種具有一定結構的隨機存取設備,對這種設備的讀寫是按(所以叫塊設備)進行的,他使用緩衝區來存放暫時的數據,待條件成熟後,從緩存一次性寫入設備或者從設備一次性讀到緩衝區。

1.1 塊設備結構

    塊設備由Page->Segment->Block->Sector的層次結構組成。
    Page就是內存映射的最小單位;Segment就是一個Page中我們要操作的一部分,由若干個相鄰的塊組成;Block是邏輯上的進行數據存取的最小單位,是文件系統的抽象,邏輯塊的大小是在格式化的時候確定的, 一個Block最多僅能容納一個文件(即不存在多個文件同一個block的情況)。如果一個文件比block小,他也會佔用一個block,因而block中空餘的空間會浪費掉。而一個大文件,可以佔多個甚至數十個成百上千萬的blockLinux內核要求 Block_Size = Sector_Size * (2的n次方),並且Block_Size <= 內存的Page_Size(頁大小), 如ext2 fsblock缺省是4k。若block太大,則存取小文件時,有空間浪費的問題;若block太小,則硬盤的 Block 數目會大增,而造成inode在指向block的時候的一些搜尋時間的增加,又會造成大文件讀寫方面的效率較差,blockVFS和文件系統傳送數據的基本單位。block對應磁盤上的一個或多個相鄰的扇區,而VFS將其看成是一個單一的數據單元,塊設備的block的大小不是唯一的,創建一個磁盤文件系統時,管理員可以選擇合適的扇區的大小,同一個磁盤的幾個分區可以使用不同的塊大小。此外,對塊設備文件的每次讀或寫操作是一種"原始"訪問,因爲它繞過了磁盤文件系統,內核通過使用最大的塊(4096)執行該操作。Linux對內存中的block會被進一步劃分爲SectorSector是硬件設備傳送數據的基本單位,這個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 = &current->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文件模擬成磁盤掛接到/mntramblock.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/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章