linux 塊設備驅動詳解


嵌入式Linux之我行,主要講述和總結了本人在學習嵌入式linux中的每個步驟。一爲總結經驗,二希望能給想入門嵌入式Linux的朋友提供方便。如有錯誤之處,謝請指正。

一、開發環境

  • 主  機:VMWare--Fedora 9
  • 開發板:Mini2440--64MB Nand, Kernel:2.6.30.4
  • 編譯器:arm-linux-gcc-4.3.2

二、塊設備基本概念

  1. 扇區(Sectors):任何塊設備硬件對數據處理的基本單位。通常,1個扇區的大小爲512byte。
  2. 塊(Blocks):由Linux制定對內核或文件系統等數據處理的基本單位。通常,1個塊由1個或多個扇區組成。
  3. 段(Segments):由若干個相鄰的塊組成。是Linux內存管理機制中一個內存頁或者內存頁的一部分。

    頁、段、塊、扇區之間的關係圖如下:

綜合上描述:塊設備驅動是基於扇區(sector)來訪問底層物理磁盤,基於塊(block)來訪問上層文件系統。扇區一般是2的n次方大小,典型爲512B,內核也要求塊是2的n次方大小,且塊大小通常爲扇區大小的整數倍,並且塊大小要小於頁面大小,典型大小爲512B、1K或4K。 

三、塊設備在Linux中的結構

  1. 首先我們來看一下,塊設備在整個Linux中應用的總體結構,如圖
  2. 塊設備驅動層(Block Device Driver)在總體結構中扮演的角色。

      從上圖可以看出,塊設備的應用在Linux中是一個完整的子系統。

      首先,我們先看一下,塊設備驅動是以何種方式對塊設備進行訪問的。在Linux中,驅動對塊設備的輸入或輸出(I/O)操作,都會向塊設備發出一個請求,在驅動中用request結構體描述。但對於一些磁盤設備而言請求的速度很慢,這時候內核就提供一種隊列的機制把這些I/O請求添加到隊列中(即:請求隊列),在驅動中用request_queue結構體描述。在向塊設備提交這些請求前內核會先執行請求的合併和排序預操作,以提高訪問的效率,然後再由內核中的I/O調度程序子系統(即:上圖中的I/O調度層)來負責提交I/O請求,I/O調度程序將磁盤資源分配給系統中所有掛起的塊I/O請求,其工作是管理塊設備的請求隊列,決定隊列中的請求的排列順序以及什麼時候派發請求到設備,關於更多詳細的I/O調度知識這裏就不深加研究了。

      其次,塊設備驅動又是怎樣維持一個I/O請求在上層文件系統與底層物理磁盤之間的關係呢?這就是上圖中通用塊層
    (Generic Block Layer)要做的事情了。在通用塊層中,通常用一個bio結構體來對應一個I/O請求,它代表了正在活動的以段(Segment)鏈表形式組織的塊IO操作,對於它所需要的所有段又用bio_vec結構體表示。

      再次,塊設備驅動又是怎樣對底層物理磁盤進行反問的呢?上面講的都是對上層的訪問對上層的關係。Linux提供了一個gendisk數據結構體,用他來表示一個獨立的磁盤設備或分區。在gendisk中有一個類似字符設備中file_operations的硬件操作結構指針,他就是block_device_operations結構體,他的作用相信大家已經很清楚了。

  3. 具體描述上面中講到的維持各層關係的數據結構體(這裏只列出了較常用的一些成員)

    request與request_queue結構體,定義在/include/linux/blkdev.h中:
    struct request
    {
        struct list_head queuelist;     /*鏈表結構*/
        struct request_queue *q;        /*請求隊列*/
        sector_t sector;                /*要傳送的下一個扇區*/
        sector_t hard_sector;           /*要完成的下一個扇區*/
        unsigned long nr_sectors;       /*要傳送的扇區數目*/
        unsigned long hard_nr_sectors;  /*要完成的扇區數目*/
        unsigned int current_nr_sectors;/*當前要傳送的扇區數目*/
        unsigned int hard_cur_sectors;  /*當前要完成的扇區數目*/
        struct bio *bio;                /*請求的bio結構體的鏈表*/
        struct bio *biotail;            /*請求的bio結構體的鏈表尾*/
        void *elevator_private;
        void *elevator_private2;
        struct gendisk *rq_disk;
        unsigned long start_time;
        unsigned short nr_phys_segments;/*請求在物理內存中佔據不連續段的數目*/
        unsigned short ioprio;
        char *buffer;                   /*傳送的緩衝區*/
        int tag;
        int errors;
        int ref_count;                  /*引用計數*/
        .
        .
        .
    };
     
    struct request_queue
    {
        .
        .
        .
        struct list_head queue_head;
        unsigned long nr_requests;       /*最大的請求數目*/
        unsigned int nr_congestion_on;
        unsigned int nr_congestion_off;
        unsigned int nr_batching;
        unsigned int max_sectors;        /*最大的扇區數目*/
        unsigned int max_hw_sectors;
        unsigned short max_phys_segments;/*最大的段數目*/
        unsigned short max_hw_segments;
        unsigned short hardsect_size;    /*扇區尺寸大小*/
        unsigned int max_segment_size;   /*最大的段尺寸大小*/
        unsigned long seg_boundary_mask; /*段邊界掩碼*/
        void *dma_drain_buffer;
        unsigned int dma_drain_size;
        unsigned int dma_pad_mask;
        unsigned int dma_alignment;      /*DMA傳輸內存對齊*/
        struct blk_queue_tag *queue_tags;
        struct list_head tag_busy_list;
        unsigned int nr_sorted;
        unsigned int in_flight;
        unsigned int rq_timeout;
        struct timer_list timeout;
        struct list_head timeout_list;
        .
        .
        .
    };

    bio與bio_vec結構體,定義在/include/linux/bio.h中:
    struct bio 
    {
        sector_t bi_sector;            /*要傳送的第一個扇區*/
        struct bio *bi_next;           /*下一個bio*/
        struct block_device *bi_bdev;
        unsigned long bi_flags;        /*狀態、命令等*/
        unsigned long bi_rw;           /*低位表示READ/WRITE,高位表示優先級*/
        unsigned short bi_vcnt;        /*bio_vec的數量*/
        unsigned short bi_idx;         /*當前bvl_vec的索引*/
        unsigned int bi_phys_segments; /*不相鄰物理段的數目*/
        unsigned int bi_size;          /*以字節爲單位所需傳送的數據大小*/
        unsigned int bi_seg_front_size;/*爲了明確硬件尺寸,需要考慮bio中第一個和最後一個虛擬的可合併段的尺寸大小*/
        unsigned int bi_seg_back_size;
        unsigned int bi_max_vecs;      /*支持最大bvl_vecs的數量*/
        struct bio_vec *bi_io_vec;     /*vec列表*/
        .
        .
        .
    };

    struct bio_vec 
    {
        struct page *bv_page;   /*頁指針*/
        unsigned int bv_len;    /*傳輸的字節數*/
        unsigned int bv_offset; /*偏移位置*/
    };


    上面的這些結構體都是對上層的支持,那麼對硬件底層的支持比較重要的結構體是gendisk,定義在/include/linux/genhd.h中:
    struct gendisk 
    {
        int major;            /*主設備號*/
        int first_minor;      /*第一個次設備號*/
        int minors;           /*最大的次設備號,如果不能分區則爲1*/
        char disk_name[DISK_NAME_LEN];  /*設備名稱*/
        struct disk_part_tbl *part_tbl; /*磁盤上的分區表信息*/
        struct hd_struct part0;
        struct block_device_operations *fops;/*塊設備對底層硬件的操作結構體指針*/
        struct request_queue *queue; /*請求隊列*/
        void *private_data;          /*私有數據*/
        .
        .
        .
    };

    那麼這些結構體之間的關係圖如下:
  4. 塊設備驅動的I/O請求處理的兩種方式:

    塊設備驅動的I/O請求處理有兩種方式,分別是使用請求隊列和不使用請求隊列。那麼這兩種方式有什麼不同呢?在第2點中已講到使用請求隊列有助於提高系統的性能,但對於一些完全可隨機訪問的塊設備(如:Ram盤等)使用請求隊列並不能獲得多大的益處,這時候,通用塊層提供了一種無隊列的操作模式,使用這種模式,驅動必須提供一個製造請求函數。我們還是用代碼來區別它們吧。

    使用請求隊列:
    static int __int ramdisk_init(void)
    {
        /*塊設備驅動註冊*/
        register_blkdev(RAMDISK_MAJOR, RAMDISK_NAME);
    
        /*使用請求隊列的方式*/
        ramdisk_queue = blk_init_queue(ramdisk_do_request, NULL);
    
        /*分配gendisk*/
    
        .........
        /*初始化gendisk*/
    
        .........
        /*添加gendisk到系統中*/
    
        .........
    }


    /*請求處理函數,請求隊列的處理流程如下:
     *首先:從請求隊列中拿出一條請求
     *其次:判斷這一條請求的方向,是向設備寫還是讀,然後將數據裝入緩衝區
     *最後:通知請求完成*/
    static void ramdisk_do_request(struct request_queue_t *queue)
    {
        struct request *req;
    
        /*使用循環一條請求一條請求的來處理,elv_next_request函數是遍歷隊列中的每一條請求*/
        while(req = elv_next_request(queue) != NULL)
        {
            /*判斷要傳輸數據的總長度大小是否超過範圍*/
            if ((req->sector + req->current_nr_sectors) << 9 > RAMDISK_SIZE)
            {
                /*如果超過範圍就直接報告請求失敗*/
                end_request(req, 0);
                continue;
            }
    
            /*判斷請求處理的方向*/
            switch (rq_data_dir(req)) 
            {
                case READ:
                    memcpy(req->buffer, disk_data + (req->sector << 9),req->current_nr_sectors << 9);
                    end_request(req, 1);/*報告請求處理成功*/
                    break;
    
                case WRITE:
                    memcpy(disk_data + (req->sector << 9), req->buffer,req->current_nr_sectors << 9);
                    end_request(req, 1);/*報告請求處理成功*/
                    break;
    
                default:
                    break;
            }
        }
    }

    不使用請求隊列,製造請求函數:
    static int __int ramdisk_init(void)
    {
        /*塊設備驅動註冊*/
        ramdisk_major = register_blkdev(RAMDISK_MAJOR, RAMDISK_NAME);
    
        /*使用製造請求的方式,先分配ramdisk_queue*/
        ramdisk_queue = blk_alloc_queue(GFP_KERNEL);
    
    
        /*再綁定請求製造函數*/
        blk_queue_make_request(ramdisk_queue, &ramdisk_make_request);
    
    
        /*分配gendisk*/
        .........
    
    
        /*初始化gendisk*/
        .........
    
    
        /*添加gendisk到系統中*/
        .........
    }

    /*綁定請求製造函數。注意:第一個參數仍然是請求隊列,但在這裏實際不包含任何請求。
    所以這裏要處理的重點對象的bio中的每個bio_vec,他表示一個或多個要傳送的緩衝區。*/
    static int ramdisk_make_request(struct request_queue_t *queue, struct bio *bio)
    {
        int i;
        struct bio_vec *bvec;
        void *disk_mem;
        void *bvec_mem;
    
        /*在遍歷段之前先判斷要傳輸數據的總長度大小是否超過範圍*/
        if((bio->bi_sector << 9) + bio->bi_size > RAMDISK_SIZE)
        {
            /*如果超出範圍就通知這個bio處理失敗*/
            bio_endio(bio, 0, -EIO);
    
            return 0;
        }
    
        /*獲得這個bio請求在塊設備內存中的起始位置*/
        disk_mem = disk_data + (bio->bi_sector << 9);
    
        /*開始遍歷這個bio中的每個bio_vec*/
        bio_for_each_segment(bvec, bio, i) 
        {
            /*因bio_vec中的內存地址是使用page *描述的,故在高端內存中需要用kmap進行映射後才能訪問,
            再加上在bio_vec中的偏移位置,纔是在高端物理內存中的實際位置*/
            bvec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
    
            /*判斷bio請求處理的方向*/
            switch(bio_data_dir(bio))
            {
                case READ:
                case READA:
                    memcpy(bvec_mem, disk_mem, bvec-> bv_len);
                    break;
    
                case WRITE : 
                    memcpy(disk_mem, bvec_mem, bvec-> bv_len);
                    break;
    
                default : 
                    kunmap(bvec->bv_page);
            }
    
            /*處理完每一個bio_vec都應把kmap映射的地址取消掉*/
            kunmap(bvec->bv_page);
    
            /*累加當前bio_vec中的內存長度,以確定下一個bio_vec在塊設備內存中的位置*/
            disk_mem += bvec->bv_len;
        }
    
        /*bio中所有的bio_vec處理完後報告處理結束*/
        bio_endio(bio, bio->bi_size, 0);
    
        return 0;
    }

四、塊設備驅動(RamDisk)實現步驟詳解

    其實從上面的結構體關係圖就可以看出這就是塊設備驅動程序的整體的結構了,當然這只是較簡單的塊設備驅動了,現在我們就即將要做的就是實現一個簡單的RamDisk塊設備驅動了。其實告訴大家我這裏爲什麼只實現一個簡單的塊設備驅動,因爲我是要爲以後的MMC/SD卡驅動、Nand flash驅動等做一些前提準備的。好了,還是先了解一下什麼是RamDisk吧。

    RamDisk是將Ram中的一部分內存空間模擬成一個磁盤設備,以塊設備的訪問方式來訪問這一片內存,達到數據存取的目的。RamDisk設備在Linux設備文件系統中對應的設備文件節點一般爲:/dev/ram%d。

  1. 建立驅動代碼文件my2440_ramdisk.c,實現驅動模塊的加載和卸載,步驟如下:

    加載部分:分配請求隊列及綁定請求製造函數 -> 分配及初始化gendisk -> 添加gendisk -> 註冊塊設備驅動。
    卸載部分:清除請求隊列 -> 刪除gendisk -> 註銷塊設備驅動。
    #include <linux/kernel.h>
    #include <linux/module.h>
    #include <linux/init.h>
    #include <linux/errno.h>
    #include <linux/blkdev.h>
    #include <linux/bio.h>
    
    #define RAMDISK_MAJOR    0        /*主設備號設置0讓內核動態產生一個主設備號*/
    #define RAMDISK_NAME    "my2440_ramdisk"    /*設備名稱*/
    #define RAMDISK_SIZE    (4 * 1024 * 1024)   /*虛擬磁盤的大小,共4M*/
    
    static int ramdisk_major = RAMDISK_MAJOR;   /*用來保存動態分配的主設備號*/
    static struct class *ramdisk_class;         /*定義一個設備類,好在/dev下動態生成設備節點*/
    
    static struct gendisk *my2440_ramdiak;      /*定義一個gendisk結構體用來表示一個磁盤設備*/
    static struct request_queue *ramdisk_queue; /*定義磁盤設備的請求隊列*/
    
    unsigned char *disk_data;/*定義一個指針來表示ramdisk塊設備在內存中的域*/
    
    /*塊設備驅動操作結構體,其實不需要做什麼操作,這裏就設置爲空*/
    static struct block_device_operations ramdisk_fops = 
    {
        .owner    = THIS_MODULE,
    };
    
    static int __init ramdisk_init(void)
    {
        int ret;
    
        /*塊設備驅動註冊, 注意這個塊設備驅動的註冊在2.6內核中是可選的,
        該函數由內核提供。這裏使用是爲了獲得一個動態生成的主設備號*/
        ramdisk_major = register_blkdev(RAMDISK_MAJOR, RAMDISK_NAME);
        if(ramdisk_major <= 0)
        {
            return ramdisk_major;
        }
    
        /*動態創建一個設備節點,跟字符型設備一樣*/
        ramdisk_class = class_create(THIS_MODULE, RAMDISK_NAME);
        if(IS_ERR(ramdisk_class))
        {
            ret = -1;
            goto err_class;
        }
        device_create(ramdisk_class, NULL, MKDEV(ramdisk_major, 0), NULL,RAMDISK_NAME);
    
        /*RamDisk屬真正隨機訪問的設備,因此不使用請求隊列的處理方式,而使用製造請求的方式*/
        ramdisk_queue = blk_alloc_queue(GFP_KERNEL);/*分配ramdisk_queue*/
    
        if(!ramdisk_queue)
        {
            ret = -ENOMEM;
            goto err_queue;
        }
        blk_queue_make_request(ramdisk_queue, &ramdisk_make_request);/*綁定請求製造函數*/
    
        /*分配gendisk,該函數由內核提供,參數爲磁盤設備的次設備號數量(或者磁盤的分區數量)
        注意一個分區就代表一個次設備,這裏指定數量後以後就不能被修改了*/
        my2440_ramdiak = alloc_disk(1);
    
        if(!my2440_ramdiak)
        {
            ret = -ENOMEM;
            goto err_alloc;
        }
    
        /*初始化gendisk*/
        my2440_ramdiak->major = ramdisk_major;    /*這裏指定的主設備號就是在上面動態獲取的主設備號*/
        my2440_ramdiak->first_minor    = 0;       /*指定第一個次設備號爲0*/
        my2440_ramdiak->fops = &ramdisk_fops;     /*指定塊設備驅動對底層硬件操作的結構體指針,定義在後面來講*/
        my2440_ramdiak->queue = ramdisk_queue;    /*指定初始化好的請求隊列*/
        sprintf(my2440_ramdiak->disk_name, RAMDISK_NAME);/*指定磁盤設備的名稱*/
    
        /*設置磁盤設備的容量大小,該函數由內核提供。
        注意該函數是以512字節爲1個扇區單位進行處理的,因爲內核要求如此*/
        set_capacity(my2440_ramdiak, RAMDISK_SIZE >> 9);/*右移9位就是除以512*/
    
        /*添加gendisk到系統中, 該函數由內核提供*/
        add_disk(my2440_ramdiak);
    
        return 0;
    
    /*錯誤處理*/
    err_class:
        unregister_blkdev(ramdisk_major, RAMDISK_NAME);
    err_queue:
        device_destroy(ramdisk_class, MKDEV(ramdisk_major, 0));
        class_destroy(ramdisk_class);
    err_alloc:
        blk_cleanup_queue(ramdisk_queue);
    
        return ret;
    }
    
    static void __exit ramdisk_exit(void)
    {    
        /*刪除磁盤設備*/
        del_gendisk(my2440_ramdiak);
        put_disk(my2440_ramdiak);
    
        /*清除請求隊列*/
        blk_cleanup_queue(ramdisk_queue);
    
        /*清除設備類*/
        device_destroy(ramdisk_class, MKDEV(ramdisk_major, 0));
        class_destroy(ramdisk_class);
    
        /*註銷塊設備*/
        unregister_blkdev(ramdisk_major, RAMDISK_NAME);
    }
    
    module_init(ramdisk_init);
    module_exit(ramdisk_exit);
    
    MODULE_LICENSE("GPL");
    MODULE_AUTHOR("Huang Gang");
    MODULE_DESCRIPTION("My2440 RamDisk Driver");

  2. RamDisk屬真正隨機訪問的設備,因此沒有使用請求隊列的處理方式,而是使用製造請求的方式。製造請求處理函數實現如下:
    /*綁定請求製造函數。注意:第一個參數仍然是請求隊列,但在這裏實際不包含任何請求。
    所以這裏要處理的重點對象的bio中的每個bio_vec,他表示一個或多個要傳送的緩衝區。*/
    static int ramdisk_make_request(struct request_queue_t *queue, struct bio *bio)
    {
        int i;
        struct bio_vec *bvec;
        void *disk_mem;
        void *bvec_mem;
    
        /*在遍歷段之前先判斷要傳輸數據的總長度大小是否超過範圍*/
        if((bio->bi_sector << 9) + bio->bi_size > RAMDISK_SIZE)
        {
            /*如果超出範圍就通知這個bio處理失敗*/
            bio_io_error(bio);
    
            return 0;
        }
    
        /*獲得這個bio請求在塊設備內存中的起始位置*/
        disk_mem = disk_data + (bio->bi_sector << 9);
    
        /*開始遍歷這個bio中的每個bio_vec*/
        bio_for_each_segment(bvec, bio, i) 
        {
            /*因bio_vec中的內存地址是使用page *描述的,故在高端內存中需要用kmap進行映射後才能訪問,
            再加上在bio_vec中的偏移位置,纔是在高端物理內存中的實際位置*/
            bvec_mem = kmap(bvec->bv_page) + bvec->bv_offset;
    
            /*判斷bio請求處理的方向*/
            switch(bio_data_dir(bio))
            {
                case READ:
                case READA:
                    memcpy(bvec_mem, disk_mem, bvec->bv_len);
                    break;
    
                case WRITE : 
                    memcpy(disk_mem, bvec_mem, bvec->bv_len);
                    break;
    
                default : 
                    kunmap(bvec->bv_page);
            }
    
            /*處理完每一個bio_vec都應把kmap映射的地址取消掉*/
            kunmap(bvec->bv_page);
    
            /*累加當前bio_vec中的內存長度,以確定下一個bio_vec在塊設備內存中的位置*/
            disk_mem += bvec->bv_len;
        }
    
        /*bio中所有的bio_vec處理完後報告處理結束*/
        bio_endio(bio, 0);
    
        return 0;
    }


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