• I.MX6ULL ARM驱动开发---块设备驱动


    引言

      块设备是针对存储设备的,比如 SD 卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等。
    块设备只能以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲。

      块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后在一次性将缓冲区中的数据写入块设备中。

      字符设备是顺序的数据流设备,字符设备是按照字节进行读写访问的。字符设备不需要缓冲区,对于字符设备的访问都是实时的,而且也不需要按照固定的块大小进行访问。

      块设备结构的不同其 I/O 算法也会不同,比如对于 EMMC、SD 卡、NAND Flash 这类没有任何机械设备的存储设备就可以任意读写任何的扇区(块设备物理存储单元)。但是对于机械硬盘这样带有磁头的设备,读取不同的盘面或者磁道里面的数据,磁头都需要进行移动,因此对于机械硬盘而言,将那些杂乱的访问按照一定的顺序进行排列可以有效提高磁盘性能,linux 里面针对不同的存储设备实现了不同的 I/O 调度算法。

    一、块设备驱动框架

    1、block_device 结构体

    linux 内核使用block_device表示块设备,block_device为一个结构体,定义在include/linux/fs.h 文件中,结构体内容如下:

    struct block_device { 
    	dev_t bd_dev; /* not a kdev_t - it's a search key */
    	int bd_openers; 
    	struct inode *bd_inode; /* will die */
    	struct super_block *bd_super; 
    	struct mutex bd_mutex; /* open/close mutex */
    	struct list_head bd_inodes; 
    	void * bd_claiming; 
    	void * bd_holder;
    	int bd_holders;
    	bool bd_write_holder;
    	#ifdef CONFIG_SYSFS
    		struct list_head bd_holder_disks;
    	#endif
    	struct block_device *bd_contains;
    	unsigned bd_block_size;
    	struct hd_struct *bd_part;
    	/*number of times partitions within this device have been opened.*/
    	unsigned bd_part_count;
    	int bd_invalidated;
    	struct gendisk *bd_disk;
    	struct request_queue *bd_queue;
    	struct list_head bd_list;
    	/*
    	* Private data. You must have bd_claim'ed the block_device
    	* to use this. NOTE: bd_claim allows an owner to claim
    	* the same device multiple times, the owner must take special
    	* care to not mess up bd_private for that case.
    	*/
    	unsigned long bd_private;
    
    	/* The counter of freeze processes */
    	int bd_fsfreeze_count;
    	/* Mutex for freeze */
    	struct mutex bd_fsfreeze_mutex;
    };
    

    block_device结构体中的bd_disk成员变量为gendisk结构体指针类型。内核使用 block_device 来表示一个具体的块设备对象,比如一个硬盘或者分区,如果是硬盘的话 bd_disk 就指向通用磁盘结构 gendisk。

    (1)注册块设备

    和字符设备驱动一样,我们需要向内核注册新的块设备、申请设备号,块设备注册函数为register_blkdev,函数原型如下:

    int register_blkdev(unsigned int major, const char *name)

    函数参数和返回值含义如下:
      major:主设备号。
      name:块设备名字。
      返回值:如果参数 major 在 1-255 之间的话表示自定义主设备号,那么返回 0 表示注册成功,如果返回负值的话表示注册失败。如果 major 为 0 的话表示由系统自动分配主设备号,那么返回值就是系统分配的主设备号(1-255),如果返回负值那就表示注册失败。

    (2)注销块设备

    和字符设备驱动一样,如果不使用某个块设备了,那么就需要注销掉,函数为unregister_blkdev,函数原型如下:

    void unregister_blkdev(unsigned int major, const char *name)

    函数参数和返回值含义如下:
      major:要注销的块设备主设备号。
      name:要注销的块设备名字。
      返回值:无。

    2、gendisk 结构体

    linux 内核使用 gendisk 来描述一个磁盘设备,这是一个结构体,定义在include/linux/genhd.h 中,内容如下:

    struct gendisk{  
    	/* major, first_minor and minors are input parameters only,
    	* don't use directly. Use disk_devt() and disk_max_parts().
    	*/
    	int major; /* major number of driver */
    	int first_minor;  int minors; /* maximum number of minors, = for
    	* disks that can't be partitioned. */
    
    	char disk_name[DISK_NAME_LEN]; /* name of major driver */
    	char *(*devnode)(struct gendisk *gd, umode_t *mode);
    
    	unsigned int events; /* supported events */
    	unsigned int async_events; /* async events, subset of all */
    
    	/* Array of pointers to partitions indexed by partno.
    	* Protected with matching bdev lock but stat and other
    	* non-critical accesses use RCU. Always access through
    	* helpers.
    	*/
    	struct disk_part_tbl __rcu *part_tbl;
    	struct hd_struct part;
    
    	const struct block_device_operations *fops;
    	struct request_queue *queue;
    	void *private_data;
    
    	int flags;
    	struct device *driverfs_dev; // FIXME: remove
    	struct kobject *slave_dir;
    
    	struct timer_rand_state *random;
    	atomic_t sync_io; /* RAID */
    	struct disk_events *ev;
    	#ifdef CONFIG_BLK_DEV_INTEGRITY
    		struct blk_integrity *integrity;
    	#endif
    	int node_id;
    };
    

    major 为磁盘设备的主设备号。

    first_minor 为磁盘的第一个次设备号。

    minors 为磁盘的次设备号数量,也就是磁盘的分区数量,这些分区的主设备号一样,次设备号不同。

    part_tbl 为磁盘对应的分区表,为结构体 disk_part_tbl 类型,disk_part_tbl 的核心是一个 hd_struct 结构体指针数组,此数组每一项都对应一个分区信息。

    fops 为块设备操作集,为 block_device_operations 结构体类型。和字符设备操作集 file_operations 一样,是块设备驱动中的重点!

    queue 为磁盘对应的请求队列,所以针对该磁盘设备的请求都放到此队列中,驱动程序需要处理此队列中的所有请求。

    (1)申请 gendisk

    使用 gendisk 之前要先申请,allo_disk 函数用于申请一个 gendisk,函数原型如下:

    struct gendisk *alloc_disk(int minors)

    函数参数和返回值含义如下:
      minors:次设备号数量,也就是 gendisk 对应的分区数量。
      返回值:成功:返回申请到的 gendisk,失败:NULL。

    (2)删除 gendisk

    如果要删除 gendisk 的话可以使用函数 del_gendisk,函数原型如下:

    void del_gendisk(struct gendisk *gp)

    函数参数和返回值含义如下:
      gp:要删除的 gendisk。
      返回值:无。

    (3)将 gendisk 添加到内核

    使用 alloc_disk 申请到 gendisk 以后系统还不能使用,必须使用 add_disk 函数将申请到的gendisk 添加到内核中,add_disk 函数原型如下:

    void add_disk(struct gendisk *disk)

    函数参数和返回值含义如下:
      disk:要添加到内核的 gendisk。
      返回值:无。

    (4)设置 gendisk 容量

    每一个磁盘都有容量,所以在初始化 gendisk 的时候也需要设置其容量,使用函数set_capacity,函数原型如下:

    void set_capacity(struct gendisk *disk, sector_t size)

    函数参数和返回值含义如下:
      disk:要设置容量的 gendisk。
      size:磁盘容量大小,注意这里是扇区数量。块设备中最小的可寻址单元是扇区,一个扇区一般是 512 字节,有些设备的物理扇区可能不是 512 字节。不管物理扇区是多少,内核和块设备驱动之间的扇区都是 512 字节。所以 set_capacity 函数设置的大小就是块设备实际容量除以512 字节得到的扇区数量。比如一个 2MB 的磁盘,其扇区数量就是(210241024)/512=4096。
      返回值:无。

    (5)调整 gendisk 引用计数

      内核会通过 get_disk 和 put_disk 这两个函数来调整 gendisk 的引用计数,根据名字就可以知道,get_disk 是增加 gendisk 的引用计数,put_disk 是减少 gendisk 的引用计数,这两个函数原型如下所示:

    truct kobject *get_disk(struct gendisk *disk)
    void put_disk(struct gendisk *disk)

    3、block_device_operations 结构体

    和字符设备的 file _operations 一样,块设备也有操作集,为结构体 block_device_operations,此结构体定义在 include/linux/blkdev.h 中,结构体内容如下:

    struct block_device_operations {  
    	int (*open) (struct block_device *, fmode_t);
    	void (*release) (struct gendisk *, fmode_t);
    	int (*rw_page)(struct block_device *, sector_t, struct page *,
    	int rw);
    	int (*ioctl) (struct block_device *, fmode_t, unsigned,
    	unsigned long);
    	int (*compat_ioctl) (struct block_device *, fmode_t, unsigned,
    	unsigned long);
    	long (*direct_access)(struct block_device *, sector_t,  void **, unsigned long *pfn, long size);
    	unsigned int (*check_events) (struct gendisk *disk,
    	unsigned int clearing);
    	/* ->media_changed() is DEPRECATED, use ->check_events() instead */
    	int (*media_changed) (struct gendisk *);
    	void (*unlock_native_capacity) (struct gendisk *);
    	int (*revalidate_disk) (struct gendisk *);
    	int (*getgeo)(struct block_device *, struct hd_geometry *);
    	/* this callback is with swap_lock and sometimes page table lock 
    	held */
    	void (*swap_slot_free_notify) (struct block_device *,
    	unsigned long);
    	struct module *owner;
    };
    

      block_device_operations 结构体里面的操作集函数和字符设备的 file_operations操作集基本类似,但是块设备的操作集函数比较少,我们来看一下其中比较重要的几个成员函数:

    open 函数用于打开指定的块设备。

    release 函数用于关闭(释放)指定的块设备。

    rw_page 函数用于读写指定的页。

    ioctl 函数用于块设备的 I/O 控制。

    compat_ioctl 函数和 ioctl 函数一样,都是用于块设备的 I/O 控制。区别在于在 64位系统上,32 位应用程序的 ioctl 会调用 compat_iotl 函数。在 32 位系统上运行的 32 位应用程序调用的就是 ioctl 函数。

    getgeo 函数用于获取磁盘信息,包括磁头、柱面和扇区等信息。

    owner 表示此结构体属于哪个模块,一般直接设置为 THIS_MODULE。

    二、块设备 I/O 请求过程

      然而,block_device_operations 结构体中并没有找到 read 和 write 这样的读写函数,那么块设备是怎么从物理块设备中读写数据?这里就引出了块设备驱动中非常重要的 request_queue、request 和 bio。

    1、请求队列 request_queue

      内核将对块设备的读写都发送到请求队列 request_queue 中,request_queue 中是大量的request(请求结构体),而 request 又包含了 bio,bio 保存了读写相关数据,比如从块设备的哪个地址开始读取、读取的数据长度,读取到哪里,如果是写的话还包括要写入的数据等。

      request_queue是一个结构体,定义在文件 include/linux/blkdev.h 中,由于request_queue 结构体比较长,这里就不列出来了。在编写块设备驱动的时候,每个磁盘(gendisk)都要分配一个 request_queue。

    (1)初始化请求队列

      我们首先需要申请并初始化一个 request_queue,然后在初始化 gendisk 的时候将这个request_queue 地址赋值给 gendisk 的 queue 成员变量。使用 blk_init_queue 函数来完成request_queue 的申请与初始化,函数原型如下:

    request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)

    函数参数和返回值含义如下:
      rfn:请求处理函数指针,每个 request_queue 都要有一个请求处理函数,请求处理函数request_fn_proc 原型如下:

    void (request_fn_proc) (struct request_queue *q)

      请求处理函数需要驱动编写人员自行实现。
      lock:自旋锁指针,需要驱动编写人员定义一个自旋锁,然后传递进来。,请求队列会使用这个自旋锁。
      返回值:如果为 NULL 的话表示失败,成功的话就返回申请到的 request_queue 地址。

    (2)删除请求队列

      当卸载块设备驱动的时候我们还需要删除掉前面申请到的 request_queue,删除请求队列使用函数 blk_cleanup_queue,函数原型如下:

    void blk_cleanup_queue(struct request_queue *q)

    函数参数和返回值含义如下:
      q:需要删除的请求队列。
      返回值:无。

    (3)分配请求队列并绑定制造请求函数

      blk_init_queue 函数完成了请求队列的申请已经请求处理函数的绑定,这个一般用于像机械硬盘这样的存储设备,需要 I/O 调度器来优化数据读写过程。

      但是对于 EMMC、SD 卡这样的非机械设备,可以进行完全随机访问,所以就不需要复杂的 I/O 调度器了。对于非机械设备我们可以先申请 request_queue,然后将申请到的 request_queue 与“制造请求”函数绑定在一起。
    先来看一下 request_queue 申请函数 blk_alloc_queue,函数原型如下:

    struct request_queue *blk_alloc_queue(gfp_t gfp_mask)

    函数参数和返回值含义如下:
      gfp_mask:内存分配掩码,具体可选择的掩码值请参考 include/linux/gfp.h 中的相关宏定义,一般为 GFP_KERNEL。
      返回值:申请到的无 I/O 调度的 request_queue。

      我们需要为 blk_alloc_queue 函数申请到的请求队列绑定一个“制造请求”函数。这里我们需要用到函数blk_queue_make_request,函数原型如下:

    void blk_queue_make_request(struct request_queue *q, make_request_fn *mfn)

    函数参数和返回值含义如下:
      q:需要绑定的请求队列,也就是 blk_alloc_queue 申请到的请求队列。
      mfn:需要绑定的“制造”请求函数,函数原型如下:

    void (make_request_fn) (struct request_queue *q, struct bio *bio)

      “制造请求”函数需要驱动编写人员实现。
      返回值:无。

      一般 blk_alloc_queue 和 blk_queue_make_request 是搭配在一起使用的,用于那么非机械的存储设备、无需 I/O 调度器,比如 EMMC、SD 卡等。blk_init_queue 函数会给请求队列分配一个 I/O 调度器,用于机械存储设备,比如机械硬盘等。

    2、请求 request

      请求队列(request_queue)里面包含的就是一系列的请求(request),request 是一个结构体,定义在 include/linux/blkdev.h 里面。request 里面有一个名为“bio”的成员变量,类型为 bio 结构体指针。真正的数据就保存在 bio 里面,所以我们需要从 request_queue 中取出一个一个的 request,然后再从每个 request 里面取出 bio,最后根据 bio 的描述讲数据写入到块设备,或者从块设备中读取数据。

    (1)获取请求

      我们需要从request_queue中依次获取每个request,使用blk_peek_request函数完成此操作,函数原型如下:

    request *blk_peek_request(struct request_queue *q)

    函数参数和返回值含义如下:
      q:指定 request_queue。
      返回值:request_queue 中下一个要处理的请求(request),如果没有要处理的请求就返回NULL。

    (2)开启请求

      使用 blk_peek_request 函数获取到下一个要处理的请求以后就要开始处理这个请求,这里要用到 blk_start_request 函数,函数原型如下:

    void blk_start_request(struct request *req)

    函数参数和返回值含义如下:
      req:要开始处理的请求。
      返回值:无。

    (3)一步到位处理请求

      我们也可以使用 blk_fetch_request 函数来一次性完成请求的获取和开启,blk_fetch_request函数很简单,内容如下:

    struct request *blk_fetch_request(struct request_queue *q) 
    { 
    	struct request *rq; 
    	rq = blk_peek_request(q);
    	if (rq) 
    		blk_start_request(rq);
    	return rq; 
    }
    

      可以看出,blk_fetch_request 就是直接调用了 blk_peek_request 和 blk_start_request 这两个函数。

    (4)其他和请求有关的函数

    函数说明
    blk_end_request()请求中指定字节数据被处理完成。
    blk_end_request_all()请求中所有数据全部处理完成。
    blk_end_request_cur()当前请求是否为请求队列中最后一个请求
    blk_end_request_err()处理完请求,直到下一个错误产生。
    __blk_end_request()和 blk_end_request 函数一样,但是需要持有队列锁。
    __blk_end_request_all()和 blk_end_request_all 函数一样,但是需要持有队列锁。
    __blk_end_request_cur()和 blk_end_request_cur 函数一样,但是需要持有队列锁。
    __blk_end_request_err()和 blk_end_request_err 函数一样,但是需要持有队列锁。

    3、bio 结构

      每个 request 里面里面会有多个 bio,bio 保存着最终要读写的数据、地址等信息。上层应用程序对于块设备的读写会被构造成一个或多个 bio 结构,bio 结构描述了要读写的起始扇区、要读写的扇区数量、是读取还是写入、页偏移、数据长度等等信息。

      上层会将 bio 提交给 I/O 调度器,I/O 调度器会将这些 bio 构造成 request 结构,而一个物理存储设备对应一个 request_queue,request_queue 里面顺序存放着一系列的 request。新产生的 bio 可能被合并到 request_queue 里现有的 request 中,也可能产生新的 request,然后插入到 request_queue 中合适的位置,这一切都是由 I/O 调度器来完成的。
    request_queue、request 和 bio 之间的关系如下图所示:

    在这里插入图片描述

      bio 是个结构体,定义在 include/linux/blk_types.h 中,结构体内容如下:

    struct bio {  
    	struct bio *bi_next; /* 请求队列的下一个 bio */
    	struct block_device *bi_bdev; /* 指向块设备 */
    	unsigned long bi_flags; /* bio 状态等信息 */
    	unsigned long bi_rw; /* I/O 操作,读或写 */
    	struct bvec_iter bi_iter; /* I/O 操作,读或写 */
    	unsigned int bi_phys_segments;  unsigned int bi_seg_front_size;  unsigned int bi_seg_back_size;
    	atomic_t bi_remaining;
    	bio_end_io_t *bi_end_io;
    	void *bi_private;
    	#ifdef CONFIG_BLK_CGROUP
    		/*
    		* Optional ioc and css associated with this bio. Put on bio
    		* release. Read comment on top of bio_associate_current().
    		*/
    		struct io_context *bi_ioc;
    		struct cgroup_subsys_state *bi_css;
    	#endif
    	union {
    	#if defined(CONFIG_BLK_DEV_INTEGRITY)
    		struct bio_integrity_payload *bi_integrity;
    	#endif
    	};
    
    	unsigned short bi_vcnt; /* bio_vec 列表中元素数量 */
    	unsigned short bi_max_vecs; /* bio_vec 列表长度 */
    	atomic_t bi_cnt; /* pin count */
    	struct bio_vec *bi_io_vec; /* bio_vec 列表 */
    	struct bio_set *bi_pool;
    	struct bio_vec bi_inline_vecs[];
    };
    

      bvec_iter 结构体描述了要操作的设备扇区等信息,结构体内容如下:

    struct bvec_iter {  
    	sector_t bi_sector; /* I/O 请求的设备起始扇区(字节) */
    	unsigned int bi_size; /* 剩余的 I/O 数量 */
    	unsigned int bi_idx; /* blv_vec 中当前索引 */
    	unsigned int bi_bvec_done; /* 当前 bvec 中已经处理完成的字节数 */
    };
    

      bio_vec 结构体描述了内容如下:

    struct bio_vec {  
    	struct page *bv_page; /* 页 */
    	unsigned int bv_len; /* 长度 */
    	unsigned int bv_offset; /* 偏移 */
    };
    

      可以看出 bio_vec 就是“page,offset,len”组合,page 指定了所在的物理页,offset 表示所处页的偏移地址,len 就是数据长度。

      我们对于物理存储设备的操作不外乎就是将 RAM 中的数据写入到物理存储设备中,或者将物理设备中的数据读取到 RAM 中去处理。数据传输三个要求:数据源、数据长度以及数据目的地,也就是你要从物理存储设备的哪个地址开始读取、读取到 RAM 中的哪个地址处、读取的数据长度是多少。

      既然 bio 是块设备最小的数据传输单元,那么 bio 就有必要描述清楚这些信息,其中 bi_iter 这个结构体成员变量就用于描述物理存储设备地址信息,比如要操作的扇区地址。bi_io_vec 指向 bio_vec 数组首地址,bio_vec 数组就是 RAM 信息,比如页地址、页偏移以及长度,“页地址”是 linux 内核里面内存管理相关的概念,这里我们不深究 linux 内存管理,我们只需要知道对于 RAM 的操作最终会转换为页相关操作。

      bio、bvec_iter 以及 bio_vec 这三个机构体之间的关系如下图所示:

    在这里插入图片描述

    (1)遍历请求中的 bio

      前面说了,请求中包含有大量的 bio,因此就涉及到遍历请求中所有 bio 并进行处理。遍历请求中的 bio 使用函数__rq_for_each_bio,这是一个宏,内容如下:

    #define __rq_for_each_bio(_bio, rq) \
     if ((rq->bio)) \
     		for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)
    

      _bio 就是遍历出来的每个 bio,rq 是要进行遍历操作的请求,_bio 参数为 bio 结构体指针类型,rq 参数为 request 结构体指针类型。

    (2)遍历 bio 中的所有段

      bio 包含了最终要操作的数据,因此还需要遍历 bio 中的所有段,这里要用到bio_for_each_segment 函数,此函数也是一个宏,内容如下:

    #define bio_for_each_segment(bvl, bio, iter) \
     		__bio_for_each_segment(bvl, bio, iter, (bio)->bi_iter)
    

      第一个 bvl 参数就是遍历出来的每个 bio_vec,第二个 bio 参数就是要遍历的 bio,类型为bio 结构体指针,第三个 iter 参数保存要遍历的 bio 中 bi_iter 成员变量。

    (3)通知 bio 处理结束

      如果使用“制造请求”,也就是抛开 I/O 调度器直接处理 bio 的话,在 bio 处理完成以后要通过内核 bio 处理完成,使用 bio_endio 函数,函数原型如下:

    bvoid bio_endio(struct bio *bio, int error)

    函数参数和返回值含义如下:
      bio:要结束的 bio。
      error:如果 bio 处理成功的话就直接填 0,如果失败的话就填个负值,比如-EIO。
      返回值:无

    三、程序编写

    1、驱动程序编写

    (1)使用请求队列实验

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #define RAMDISK_SIZE	(2 * 1024 * 1024) 	/* 容量大小为2MB */
    #define RAMDISK_NAME	"ramdisk"			/* 名字 */
    #define RADMISK_MINOR	3					/* 表示有三个磁盘分区!不是次设备号为3! */
    
    /* ramdisk设备结构体 */
    struct ramdisk_dev{
    	int major;					/* 主设备号 */
    	unsigned char *ramdiskbuf;	/* ramdisk内存空间,用于模拟块设备 */
    	spinlock_t lock;			/* 自旋锁 */
    	struct gendisk *gendisk; 	/* gendisk */
    	struct request_queue *queue;/* 请求队列 */
    
    };
    
    struct ramdisk_dev ramdisk;		/* ramdisk设备 */
    
    /*
     * @description		: 打开块设备
     * @param - dev 	: 块设备
     * @param - mode 	: 打开模式
     * @return 			: 0 成功;其他 失败
     */
    int ramdisk_open(struct block_device *dev, fmode_t mode)
    {
    	printk("ramdisk open\r\n");
    	return 0;
    }
    
    /*
     * @description		: 释放块设备
     * @param - disk 	: gendisk
     * @param - mode 	: 模式
     * @return 			: 0 成功;其他 失败
     */
    void ramdisk_release(struct gendisk *disk, fmode_t mode)
    {
    	printk("ramdisk release\r\n");
    }
    
    /*
     * @description		: 获取磁盘信息
     * @param - dev 	: 块设备
     * @param - geo 	: 模式
     * @return 			: 0 成功;其他 失败
     */
    int ramdisk_getgeo(struct block_device *dev, struct hd_geometry *geo)
    {
    	/* 这是相对于机械硬盘的概念 */
    	geo->heads = 2;			/* 磁头 */
    	geo->cylinders = 32;	/* 柱面 */
    	geo->sectors = RAMDISK_SIZE / (2 * 32 *512); /* 一个磁道上的扇区数量 */
    	return 0;
    }
    
    /* 
     * 块设备操作函数 
     */
    static struct block_device_operations ramdisk_fops =
    {
    	.owner	 = THIS_MODULE,
    	.open	 = ramdisk_open,
    	.release = ramdisk_release,
    	.getgeo  = ramdisk_getgeo,
    };
    
    /*
     * @description	: 处理传输过程
     * @param-req 	: 请求
     * @return 		: 无
     */
    static void ramdisk_transfer(struct request *req)
    {	
    	unsigned long start = blk_rq_pos(req) << 9;  	/* blk_rq_pos获取到的是扇区地址,左移9位转换为字节地址 */
    	unsigned long len  = blk_rq_cur_bytes(req);		/* 大小   */
    
    	/* bio中的数据缓冲区
    	 * 读:从磁盘读取到的数据存放到buffer中
    	 * 写:buffer保存这要写入磁盘的数据
    	 */
    	void *buffer = bio_data(req->bio);		
    	
    	if(rq_data_dir(req) == READ) 		/* 读数据 */	
    		memcpy(buffer, ramdisk.ramdiskbuf + start, len);
    	else if(rq_data_dir(req) == WRITE) 	/* 写数据 */
    		memcpy(ramdisk.ramdiskbuf + start, buffer, len);
    
    }
    
    /*
     * @description	: 请求处理函数
     * @param-q 	: 请求队列
     * @return 		: 无
     */
    void ramdisk_request_fn(struct request_queue *q)
    {
    	int err = 0;
    	struct request *req;
    
    	/* 循环处理请求队列中的每个请求 */
    	req = blk_fetch_request(q);
    	while(req != NULL) {
    
    		/* 针对请求做具体的传输处理 */
    		ramdisk_transfer(req);
    
    		/* 判断是否为最后一个请求,如果不是的话就获取下一个请求
    		 * 循环处理完请求队列中的所有请求。
    		 */
    		if (!__blk_end_request_cur(req, err))
    			req = blk_fetch_request(q);
    	}
    }
    
    
    /*
     * @description	: 驱动出口函数
     * @param 		: 无
     * @return 		: 无
     */
    static int __init ramdisk_init(void)
    {
    	int ret = 0;
    	printk("ramdisk init\r\n");
    
    	/* 1、申请用于ramdisk内存 */
    	ramdisk.ramdiskbuf = kzalloc(RAMDISK_SIZE, GFP_KERNEL);
    	if(ramdisk.ramdiskbuf == NULL) {
    		ret = -EINVAL;
    		goto ram_fail;
    	}
    
    	/* 2、初始化自旋锁 */
    	spin_lock_init(&ramdisk.lock);
    
    	/* 3、注册块设备 */
    	ramdisk.major = register_blkdev(0, RAMDISK_NAME); /* 由系统自动分配主设备号 */
    	if(ramdisk.major < 0) {
    		goto register_blkdev_fail;
    	}  
    	printk("ramdisk major = %d\r\n", ramdisk.major);
    
    	/* 4、分配并初始化gendisk */
    	ramdisk.gendisk = alloc_disk(RADMISK_MINOR);
    	if(!ramdisk.gendisk) {
    		ret = -EINVAL;
    		goto gendisk_alloc_fail;
    	}
    
    	/* 5、分配并初始化请求队列 */
    	ramdisk.queue = blk_init_queue(ramdisk_request_fn, &ramdisk.lock);
    	if(!ramdisk.queue) {
    		ret = EINVAL;
    		goto blk_init_fail;
    	}
    
    	/* 6、添加(注册)disk */
    	ramdisk.gendisk->major = ramdisk.major;		/* 主设备号 */
    	ramdisk.gendisk->first_minor = 0;			/* 第一个次设备号(起始次设备号) */
    	ramdisk.gendisk->fops = &ramdisk_fops; 		/* 操作函数 */
    	ramdisk.gendisk->private_data = &ramdisk;	/* 私有数据 */
    	ramdisk.gendisk->queue = ramdisk.queue;		/* 请求队列 */
    	sprintf(ramdisk.gendisk->disk_name, RAMDISK_NAME); /* 名字 */
    	set_capacity(ramdisk.gendisk, RAMDISK_SIZE/512);	/* 设备容量(单位为扇区) */
    	add_disk(ramdisk.gendisk);
    
    	return 0;
    
    blk_init_fail:
    	put_disk(ramdisk.gendisk);
    	//del_gendisk(ramdisk.gendisk);
    gendisk_alloc_fail:
    	unregister_blkdev(ramdisk.major, RAMDISK_NAME);
    register_blkdev_fail:
    	kfree(ramdisk.ramdiskbuf); /* 释放内存 */
    ram_fail:
    	return ret;
    }
    
    /*
     * @description	: 驱动出口函数
     * @param 		: 无
     * @return 		: 无
     */
    static void __exit ramdisk_exit(void)
    {
    	printk("ramdisk exit\r\n");
    	/* 释放gendisk */
    	del_gendisk(ramdisk.gendisk);
    	put_disk(ramdisk.gendisk);
    
    	/* 清除请求队列 */
    	blk_cleanup_queue(ramdisk.queue);
    
    	/* 注销块设备 */
    	unregister_blkdev(ramdisk.major, RAMDISK_NAME);
    
    	/* 释放内存 */
    	kfree(ramdisk.ramdiskbuf); 
    }
    
    module_init(ramdisk_init);
    module_exit(ramdisk_exit);
    MODULE_LICENSE("GPL");
    

    (2)使用请求队列实验

    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    
    #include 
    #include 
    #include 
    
    #define RAMDISK_SIZE	(2 * 1024 * 1024) 	/* 容量大小为2MB */
    #define RAMDISK_NAME	"ramdisk"			/* 名字 */
    #define RADMISK_MINOR	3					/* 表示有三个磁盘分区!不是次设备号为3! */
    
    /* ramdisk设备结构体 */
    struct ramdisk_dev{
    	int major;					/* 主设备号 */
    	unsigned char *ramdiskbuf;	/* ramdisk内存空间,用于模拟块设备 */
    	spinlock_t lock;			/* 自旋锁 */
    	struct gendisk *gendisk; 	/* gendisk */
    	struct request_queue *queue;/* 请求队列 */
    
    };
    
    struct ramdisk_dev ramdisk;		/* ramdisk设备 */
    
    /*
     * @description		: 打开块设备
     * @param - dev 	: 块设备
     * @param - mode 	: 打开模式
     * @return 			: 0 成功;其他 失败
     */
    int ramdisk_open(struct block_device *dev, fmode_t mode)
    {
    	printk("ramdisk open\r\n");
    	return 0;
    }
    
    /*
     * @description		: 释放块设备
     * @param - disk 	: gendisk
     * @param - mode 	: 模式
     * @return 			: 0 成功;其他 失败
     */
    void ramdisk_release(struct gendisk *disk, fmode_t mode)
    {
    	printk("ramdisk release\r\n");
    }
    
    /*
     * @description		: 获取磁盘信息
     * @param - dev 	: 块设备
     * @param - geo 	: 模式
     * @return 			: 0 成功;其他 失败
     */
    int ramdisk_getgeo(struct block_device *dev, struct hd_geometry *geo)
    {
    	/* 这是相对于机械硬盘的概念 */
    	geo->heads = 2;			/* 磁头 */
    	geo->cylinders = 32;	/* 柱面 */
    	geo->sectors = RAMDISK_SIZE / (2 * 32 *512); /* 一个磁道上的扇区数量 */
    	return 0;
    }
    
    /* 
     * 块设备操作函数 
     */
    static struct block_device_operations ramdisk_fops =
    {
    	.owner	 = THIS_MODULE,
    	.open	 = ramdisk_open,
    	.release = ramdisk_release,
    	.getgeo  = ramdisk_getgeo,
    };
    
    /*
     * @description	: “制造请求”函数
     * @param-q 	: 请求队列
     * @return 		: 无
     */
    void ramdisk_make_request_fn(struct request_queue *q, struct bio *bio)
    {
    	int offset;
    	struct bio_vec bvec;
    	struct bvec_iter iter;
    	unsigned long len = 0;
    
    	offset = (bio->bi_iter.bi_sector) << 9;	/* 获取要操作的设备的偏移地址 */
    
    	/* 处理bio中的每个段 */
    	bio_for_each_segment(bvec, bio, iter){
    		char *ptr = page_address(bvec.bv_page) + bvec.bv_offset;
    		len = bvec.bv_len;
    
    		if(bio_data_dir(bio) == READ)	/* 读数据 */
    			memcpy(ptr, ramdisk.ramdiskbuf + offset, len);
    		else if(bio_data_dir(bio) == WRITE)	/* 写数据 */
    			memcpy(ramdisk.ramdiskbuf + offset, ptr, len);
    		offset += len;
    	}
    	set_bit(BIO_UPTODATE, &bio->bi_flags);
    	bio_endio(bio, 0);
    }
    
    
    /*
     * @description	: 驱动出口函数
     * @param 		: 无
     * @return 		: 无
     */
    static int __init ramdisk_init(void)
    {
    	int ret = 0;
    	printk("ramdisk init\r\n");
    
    	/* 1、申请用于ramdisk内存 */
    	ramdisk.ramdiskbuf = kzalloc(RAMDISK_SIZE, GFP_KERNEL);
    	if(ramdisk.ramdiskbuf == NULL) {
    		ret = -EINVAL;
    		goto ram_fail;
    	}
    
    	/* 2、初始化自旋锁 */
    	spin_lock_init(&ramdisk.lock);
    
    	/* 3、注册块设备 */
    	ramdisk.major = register_blkdev(0, RAMDISK_NAME); /* 由系统自动分配主设备号 */
    	if(ramdisk.major < 0) {
    		goto register_blkdev_fail;
    	}  
    	printk("ramdisk major = %d\r\n", ramdisk.major);
    
    	/* 4、分配并初始化gendisk */
    	ramdisk.gendisk = alloc_disk(RADMISK_MINOR);
    	if(!ramdisk.gendisk) {
    		ret = -EINVAL;
    		goto gendisk_alloc_fail;
    	}
    
    	/* 5、分配请求队列 */
    	ramdisk.queue = blk_alloc_queue(GFP_KERNEL);
    	if(!ramdisk.queue){
    		ret = -EINVAL;
    		goto blk_allo_fail;
    	}
    
    	/* 6、设置“制造请求”函数 */
    	blk_queue_make_request(ramdisk.queue, ramdisk_make_request_fn);
    
    	/* 7、添加(注册)disk */
    	ramdisk.gendisk->major = ramdisk.major;		/* 主设备号 */
    	ramdisk.gendisk->first_minor = 0;			/* 第一个次设备号(起始次设备号) */
    	ramdisk.gendisk->fops = &ramdisk_fops; 		/* 操作函数 */
    	ramdisk.gendisk->private_data = &ramdisk;	/* 私有数据 */
    	ramdisk.gendisk->queue = ramdisk.queue;		/* 请求队列 */
    	sprintf(ramdisk.gendisk->disk_name, RAMDISK_NAME); /* 名字 */
    	set_capacity(ramdisk.gendisk, RAMDISK_SIZE/512);	/* 设备容量(单位为扇区) */
    	add_disk(ramdisk.gendisk);
    
    	return 0;
    
    blk_allo_fail:
    	put_disk(ramdisk.gendisk);
    	//del_gendisk(ramdisk.gendisk);
    gendisk_alloc_fail:
    	unregister_blkdev(ramdisk.major, RAMDISK_NAME);
    register_blkdev_fail:
    	kfree(ramdisk.ramdiskbuf); /* 释放内存 */
    ram_fail:
    	return ret;
    }
    
    /*
     * @description	: 驱动出口函数
     * @param 		: 无
     * @return 		: 无
     */
    static void __exit ramdisk_exit(void)
    {
    	printk("ramdisk exit\r\n");
    	/* 释放gendisk */
    	del_gendisk(ramdisk.gendisk);
    	put_disk(ramdisk.gendisk);
    
    	/* 清除请求队列 */
    	blk_cleanup_queue(ramdisk.queue);
    
    	/* 注销块设备 */
    	unregister_blkdev(ramdisk.major, RAMDISK_NAME);
    
    	/* 释放内存 */
    	kfree(ramdisk.ramdiskbuf); 
    }
    
    module_init(ramdisk_init);
    module_exit(ramdisk_exit);
    MODULE_LICENSE("GPL");
    

    2、Makefile

    KERNELDIR := /home/sh/Desktop/MUL/zimage
    CURRENT_PATH := $(shell pwd)
    obj-m := ramdisk.o
    
    ARCH=arm
    CROSS_COMPILE=arm-linux-gnueabihf-
    
    build: kernel_modules
    
    kernel_modules:
    	$(MAKE) -j32 -C $(KERNELDIR) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) M=$(CURRENT_PATH) modules
    clean:
    	$(MAKE) -j32 -C $(KERNELDIR) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) M=$(CURRENT_PATH) clean
    

    四、运行测试

    1、查看 ramdisk 磁盘

    驱动加载成功以后就会在/dev/目录下生成一个名为“ramdisk”的设备, 输入如下命令查看ramdisk 磁盘信息:

    fdisk -l   //查看磁盘信息

    上述命令会将当前系统中所有的磁盘信息都打印出来,其中就包括了 ramdisk 设备,如下图所示:

    在这里插入图片描述

    从上图可以看出,ramdisk 已经识别出来了,大小为 2MB,但是同时也提示/dev/ramdisk没有分区表,因为我们还没有格式化/dev/ramdisk

    2、格式化/dev/ramdisk

    使用 mkfs.vfat 命令格式化/dev/ramdisk,将其格式化成 vfat 格式,输入如下命令:

    mkfs.vfat /dev/ramdisk

    格式化完成以后就可以挂载/dev/ramdisk 来访问了,挂载点可以自定义,这里笔者就将其挂载到/tmp 目录下,输入如下命令:

    mount /dev/ramdisk /tmp

    挂载成功以后就可以通过/tmp 来访问 ramdisk 这个磁盘了,进入到/tmp 目录中,可以通过 vi 命令新建一个 txt 文件来测试磁盘访问是否正常。

    取消挂载命令:

    umount /dev/ramdisk

  • 相关阅读:
    泛微全新低代码平台e-builder在沪发布,超千名与会者共商数字化转型
    基于github上go版本的LoraWAN Server安装及使用
    TikTok运营做不起来?IP如何选择?
    qt的xml读写和QDomDocument、QDomElement、QDomNode、QDomNamedNodeMap讲解
    Shell 脚本 一键安装/一键卸载/一键重装 Docker
    bug:XShell无法连接CentOS虚拟机
    Spring中事务的传播机制以及REQUIRED、REQUIRES_NEW、NESTED区别以及代码演示
    在PyCharm中直接启动mitmproxy并自动打开&关闭系统代理
    使用synchronized 加锁你加对了么?
    AI----人工智能简介
  • 原文地址:https://blog.csdn.net/qq_41455322/article/details/126989220