当前位置:   article > 正文

【正点原子Linux连载】 第三十五章 Linux块设备驱动实验 摘自【正点原子】ATK-DLRK3568嵌入式Linux驱动开发指南_正点原子linux官网

正点原子linux官网

1)实验平台:正点原子ATK-DLRK3568开发板
2)平台购买地址:https://detail.tmall.com/item.htm?id=731866264428
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/docs/boards/xiaoxitongban

第三十五章 Linux块设备驱动实验

本章我们来学习一下块设备驱动框架,块设备驱动是Linux三大驱动类型之一。块设备驱动远比字符设备驱动复杂得多,不同类型的存储设备又对应不同的驱动子系统,本章我们重点学习一下块设备相关驱动驱动概念,不涉及到具体的存储设备。最后,我们使用ATK-DLRK3568开发板板载RAM模拟一个块设备,学习块设备驱动框架的使用。

35.1 什么是块设备
块设备是针对存储设备的,比如 SD 卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等。因此块设备驱动其实就是这些存储设备驱动,块设备驱动相比字符设备驱动的主要区别如下:
①、块设备只能以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲。
②、块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后再一次性将缓冲区中的数据写入块设备中。这么做的目的为了提高块设备寿命,大家如果仔细观察的话就会发现有些硬盘或者 NAND Flash就会标明擦除次数(flash 的特性,写之前要先擦除),比如擦除 100000 次等。因此,为了提高块设备寿命而引入了缓冲区,数据先写入到缓冲区中,等满足一定条件后再一次性写入到真正的物理存储设备中,这样就减少了对块设备的擦除次数,提高了块设备寿命。
字符设备是顺序的数据流设备,字符设备是按照字节进行读写访问的。字符设备不需要缓冲区,对于字符设备的访问都是实时的,而且也不需要按照固定的块大小进行访问。
块设备结构的不同其 I/O 算法也会不同,比如对于 EMMC、SD 卡、NAND Flash 这类没有任何机械设备的存储设备就可以任意读写任何的扇区(块设备物理存储单元)。但是对于机械硬盘这样带有磁头的设备,读取不同的盘面或者磁道里面的数据,磁头都需要进行移动,因此对于机械硬盘而言,将那些杂乱的访问按照一定的顺序进行排列可以有效提高磁盘性能,linux 里面针对不同的存储设备实现了不同的 I/O 调度算法。
35.2 块设备驱动框架
35.2.1 block_device 结构体
Linux内核使用block_device表示块设备,block_device为一个结构体,定义在include/linux/fs.h文件中,结构体内容如下所示:
示例代码35.2.1.1 block_device结构体

1 struct block_device {
2   dev_t           bd_dev;  /* not a kdev_t - it's a search key */
3   int         bd_openers;
4   struct inode *      bd_inode;   /* will die */
5   struct super_block *    bd_super;
6   struct mutex        bd_mutex;   /* open/close mutex */
7   void *          bd_claiming;
8   void *          bd_holder;
9   int         bd_holders;
10  bool            bd_write_holder;
11  #ifdef CONFIG_SYSFS
12  struct list_head    bd_holder_disks;
13  #endif
14  struct block_device *   bd_contains;
15  unsigned        bd_block_size;
16  u8          bd_partno;
17  struct hd_struct *  bd_part;
18  /* number of times partitions within this device have been opened. */
19  unsigned        bd_part_count;
20  int         bd_invalidated;
21  struct gendisk *    bd_disk;
22  struct request_queue *  bd_queue;
23  struct backing_dev_info *bd_bdi;
24  struct list_head    bd_list;
25  /*
26   * Private data.  You must have bd_claim'ed the block_device
27   * to use this.  NOTE:  bd_claim allows an owner to claim
28   * the same device multiple times, the owner must take special
29   * care to not mess up bd_private for that case.
30   */
31  unsigned long       bd_private;
32
33  /* The counter of freeze processes */
34  int         bd_fsfreeze_count;
35  /* Mutex for freeze */
36  struct mutex        bd_fsfreeze_mutex;
37
38  ANDROID_KABI_RESERVE(1);
39  ANDROID_KABI_RESERVE(2);
40  ANDROID_KABI_RESERVE(3);
41  ANDROID_KABI_RESERVE(4);
42  } __randomize_layout;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
对于block_device结构体,我们重点关注一下第21行的bd_disk成员变量,此成员变量为gendisk结构体指针类型,内核使用block_device来表示一个具体的块设备对象,比如一个硬盘或者分区,如果是硬盘的话,bd_disk就指向通用磁盘结构gendisk。
  • 1

1 、注册块设备
和字符设备驱动一样,我们需要向内核注册新的块设备、申请设备号,块设备注册函数为register_blkdev,函数原型如下:
int register_blkdev(unsigned int major, const char *name)
函数参数和返回值含义如下:
major :主设备号。
name :块设备名字。
返回值:如果参数 major 在 1~(BLKDEV_MAJOR_MAX-1)之间的话表示自定义主设备号,那么返回 0 表示注册成功,如果返回负值的话表示注册失败。如果 major 为 0 的话表示由系统自动分配主设备号,那么返回值就是系统分配的主设备号,如果返回负值那就表示注册失败。
2 、注销块设备
和字符设备驱动一样,如果不使用某个块设备了,那么就需要注销掉,函数为unregister_blkdev,函数原型如下:
void unregister_blkdev(unsigned int major, const char *name)
函数参数和返回值含义如下:
major :要注销的块设备主设备号。
name: : 要注销的块设备名字。
返回值:无。
35.2.2 gendisk结构体
Linux内核使用gendisk来描述一个磁盘设备,这是一个结构体,定义在include/linux/genhd.h中,内容如下所示:
示例代码35.2.2.1 gendisk结构体

1   struct gendisk {
2       /* major, first_minor and minors are input parameters only,
3        * don't use directly.  Use disk_devt() and disk_max_parts().
4        */
5       int major;          /* major number of driver */
6       int first_minor;
7       int minors;         /* maximum number of minors, =1 for
8                               * disks that can't be partitioned. */
9 
10      char disk_name[DISK_NAME_LEN];  /* name of major driver */
11      char *(*devnode)(struct gendisk *gd, umode_t *mode);
12
13      unsigned short events;   /* supported events */
14      unsigned short event_flags;
15
16      /* Array of pointers to partitions indexed by partno.
17       * Protected with matching bdev lock but stat and other
18       * non-critical accesses use RCU.  Always access through
19       * helpers.
20       */
21      struct disk_part_tbl __rcu *part_tbl;
22      struct hd_struct part0;
23
24      const struct block_device_operations *fops;
25      struct request_queue *queue;
26      void *private_data;
27
28      int flags;
29      struct rw_semaphore lookup_sem;
30      struct kobject *slave_dir;
31
32      struct timer_rand_state *random;
33      atomic_t sync_io;       /* RAID */
34      struct disk_events *ev;
35  #ifdef  CONFIG_BLK_DEV_INTEGRITY
36      struct kobject integrity_kobj;
37  #endif  /* CONFIG_BLK_DEV_INTEGRITY */
38      int node_id;
39      struct badblocks *bb;
40      struct lockdep_map lockdep_map;
41	
42	ANDROID_KABI_RESERVE(1);
43	ANDROID_KABI_RESERVE(2);
44	ANDROID_KABI_RESERVE(3);
45	ANDROID_KABI_RESERVE(4);
46  };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

我们简单看一下 gendisk 结构体中比较重要的几个成员变量:
第 5 行,major 为磁盘设备的主设备号。
第 6 行,first_minor 为磁盘的第一个次设备号。
第 7 行,minors 为磁盘的此设备号数量,也就是磁盘的分区数量,这些分区的主设备号一样,此设备号不同。
第 21 行,part_tbl 为磁盘对应的分区表,为结构体 disk_part_tbl 类型,disk_part_tbl 的核心是一个 hd_struct 结构体指针数组,此数组每一项都对应一个分区信息。
第 24 行,fops 为块设备操作集,为 block_device_operations 结构体类型。和字符设备操作集 file_operations 一样,是块设备驱动中的重点!
第 25 行,queue 为磁盘对应的请求队列,所以针对该磁盘设备的请求都放到此队列中,驱动程序需要处理此队列中的所有请求。
编写块的设备驱动的时候需要分配并初始化一个 gendisk,linux 内核提供了一组 gendisk 操作函数,我们来看一下一些常用的 API 函数。
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 的磁盘,其扇区数量就是(2
1024
1024)/512=4096。
返回值:无。
5、调整 gendisk引用计数
内核会通过 get_disk_and_module 和 put_disk 这两个函数来调整 gendisk 的引用计数,根据名字就可以知道,get_disk_and_module是增加 gendisk 的引用计数,put_disk 是减少 gendisk 的引用计数,这两个函数原型如下所示:
truct kobject * get_disk_and_module (struct gendisk *disk)
void put_disk(struct gendisk *disk)
35.2.3 block_device_operations结构体
和字符设备的file_operations一样,块设备也有操作集,为结构体block_device_operations,此结构定于在include/linux/blkdev.h中,结构体内容如下:
示例代码35.2.3.1 block_device_operations结构体

1 struct block_device_operations {
2   int (*open) (struct block_device *, fmode_t);
3   void (*release) (struct gendisk *, fmode_t);
4   int (*rw_page)(struct block_device *, sector_t, struct page *, 
unsigned int);
5   int (*ioctl) (struct block_device *, fmode_t, unsigned, 
unsigned long);
6   int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, 
unsigned long);
7   unsigned int (*check_events) (struct gendisk *disk,
8                     unsigned int clearing);
9   /* ->media_changed() is DEPRECATED, use ->check_events() instead */
10  int (*media_changed) (struct gendisk *);
11  void (*unlock_native_capacity) (struct gendisk *);
12  int (*revalidate_disk) (struct gendisk *);
13  int (*getgeo)(struct block_device *, struct hd_geometry *);
14  /* this callback is with swap_lock and sometimes page table lock held */
15  void (*swap_slot_free_notify) (struct block_device *, unsigned long);
16  struct module *owner;
17  const struct pr_ops *pr_ops;
18 };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
可以看出,block_device_operations结构体里面的操作集函数和字符设备的file_operations操作集基本类似,但是块设备的操作集函数比较少,我们来看一下其中比较重要的几个成员函数:
第2行,open函数用于打开指定的设备。
第3行,release函数用于关闭(释放)指定的块设备。
第4行,rw_page函数用于读写指定的页。
第5行,ioctl函数用于块设备I/O控制。
第6行,compat_ioctl函数与ioctl函数一样,都是用于块设备的I/O控制。区别在于在 64位系统上,32 位应用程序的 ioctl 会调用 compat_iotl 函数。在 32 位系统上运行的 32 位应用程序调用的就是 ioctl 函数。
第13行,getgeo 函数用于获取磁盘信息,包括磁头、柱面和扇区等信息。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

第 16 行,owner 表示此结构体属于哪个模块,一般直接设置为 THIS_MODULE。
35.2.4 块设备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结构体比较长,这里就不列出来了。大家回过头看一下示例代码35.2.2.1的gendisk结构体就会发现里面有一个request_queue结构体指针类型成员变量queue,也就说在编写块设备驱动的时候,每个磁盘(gendisk)都要分配一个request_queue。
①、初始化请求队列
初始化请求队列可以分为两部分,第一部分是创建blk_mq_tag_set结构体,然后使用blk_mq_alloc_tag_set函数初始化blk_mq_tag_set对象。第二部分使用blk_mq_init_queue函数获取request_queue。
blk_mq_tag_set结构体定义在include/linux/blk-mq.h中,如下示例代码所示:
示例代码35.2.4.1 blk_mq_tag_set结构体

1 struct blk_mq_tag_set {
2   unsigned int        *mq_map;
3   const struct blk_mq_ops *ops;
4   unsigned int        nr_hw_queues;
5   unsigned int        queue_depth;    /* max hw supported */
6   unsigned int        reserved_tags;
7   unsigned int        cmd_size;   /* per-request extra data */
8   int         numa_node;
9   unsigned int        timeout;
10  unsigned int        flags;      /* BLK_MQ_F_* */
11  void            *driver_data;
12
13  struct blk_mq_tags  **tags;
14
15  struct mutex        tag_list_lock;
16  struct list_head    tag_list;
17 };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
第3行,ops为驱动实现的操作集合,会被request_queue继承。
第4行,nr_hw_queues为硬件队列个数。
第5行,queue_depth为队列的深度,即队列中可以存在的最大请求数。
第6行,reserved_tags为保留的标签数。
第8行,numa_node为队列所属的numa节点。
第10行,flags为属性标志,一般为BLK_MQ_F_SHOULD_MERGE,想了解跟多的标志位可以去看include/linux/blk-mq.h文件。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

接着我们去看看blk_mq_ops结构体,结构体原型如下所示(有省略):
示例代码35.2.4.2 blk_mq_ops结构体

1   struct blk_mq_ops {
2       queue_rq_fn     *queue_rq;
3   }
  • 1
  • 2
  • 3
这里只是列出queue_rq成员,因为本章例程只用到它。queue_rq这是一个queue_rq_fn类型的指针,queue_rq_fn类型定义如下:
  • 1

typedef blk_status_t (queue_rq_fn)(struct blk_mq_hw_ctx *,const struct blk_mq_queue_data *);
queue_rq请求处理函数指针,此函数需要驱动人员自行实现。注意:函数的两个形参就不要管了,只要知道可以通过第二形参获取request结构体。
编写块设备的请求队列驱动的时候需要分配并初始化一个blk_mq_tag_set,linux内核提供了一组blk_mq_tag_set操作相关的函数,我们来看一下一些常用的API函数。
1)、为一个或多个请求队列分配tag集合
给blk_mq_tag_set对象赋值后,要使用blk_mq_alloc_tag_set函数为一个或多个请求队列分配tag和request集合,函数原型如下:
int blk_mq_alloc_tag_set(struct blk_mq_tag_set *set);
函数参数和返回值含义如下:
set:需要分配tag集合的blk_mq_tag_set。
返回值:0表示成功,非0表示失败。
2)、释放请求队列中的tag集合
如果要释放请求队列中的tag集合,可以使用blk_mq_free_tag_set,函数原型如下:
void blk_mq_free_tag_set(struct blk_mq_tag_set *set);
函数参数和返回值含义如下:
set:要释放tag集合的blk_mq_tag_set。
返回值:无。
最后我们需要通过blk_mq_init_queue函数来初始化IO请求队列request_queue,此函数会申请request_queue,然后返回,函数原型如下:
struct request_queue *blk_mq_init_queue(struct blk_mq_tag_set *);
函数参数和返回值含义如下:
set:blk_mq_tag_set对象。
返回值:初始化以后的request_queue的地址。
Linux内核也提供了一步创建request_queue队列的函数:blk_mq_init_sq_queue,使用此函数可以一步创建请求队列,函数原型如下:

struct request_queue *blk_mq_init_sq_queue(struct blk_mq_tag_set *set,
                       const struct blk_mq_ops *ops,
                       unsigned int queue_depth,
                       unsigned int set_flags)
  • 1
  • 2
  • 3
  • 4

函数参数和返回值含义如下:
set:blk_mq_tag_set对象。
ops:操作函数。
queue_depth:队列深度。
set_flags:标志。
返回值:request_queue的地址。
②、删除请求队列
当卸载块设备驱动的时候我们还需要删除掉前面申请到的request_queue,删除请求队列使用函数blk_cleanup_queue,函数原型如下:
void blk_cleanup_queue(struct request_queue *q)
函数参数和返回值含义如下:
q:需要删除的请求队列。
返回值:无。
③、分配请求队列并绑定制造请求函数
blk_mq_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_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结构体了,太长了。request里面有一个名为“bio”的成员变量,类型为bio结构体指针。前面说了,真正的数据就保存在bio里面,所以我们需要从request_queue中取出一个一个的request,然后再从每个request里面取出bio,最后根据bio的描述讲数据写入到块设备,或者从块设备中读取数据。
①、开启请求
当有请求处理的时候,我们要用blk_mq_start_request函数开启请求处理,函数原型如下:
void blk_mq_start_request(struct request *rq);
函数参数和返回值含义如下:
rq:指定request_queue。
返回值:无。
②、结束请求
我们不用处理请求的时候,要使用blk_mq_end_request函数结束请求处理,函数原型如下:
void blk_mq_end_request(struct request *rq, blk_status_t error);
函数参数和返回值含义如下:
rq:需要结束的请求(request)。
error:0表示正确的退出结束请求处理,非0错误退出结束请求处理。
返回值:无。
这里我们先总结一下是如何使用这个两个函数去处理请求数据的,示例代码如下所示:
示例代码35.2.4.3 处理请求模型

1   static int ramdisk_transfer(struct request *req)
2   {
3       /* 此函数要实现把数据拷贝到硬盘 */
4       reutrn 0;
5   }
6 
7   static blk_status_t _queue_rq(struct blk_mq_hw_ctx *hctx, 
const struct blk_mq_queue_data* bd)
8   {
9       struct request *req = bd->rq;
10      int ret;
11      /* 开启请求处理 */
12      blk_mq_start_request(req);
13
14      /* 处理请求 */
15      ret = ramdisk_transfer(req);
16      
17      /* 结束请求处理 */
18      blk_mq_end_request(req, ret);
19      
20      return 0;
21  }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

3、bio结构
每个request里面里面会有多个bio,bio保存着最终要读写的数据、地址等信息。上层应用程序对于块设备的读写会被构造成一个或多个bio结构,bio结构描述了要读写的起始扇区、要读写的扇区数量、是读取还是写入、页便宜、数据长度等等信息。上层会将bio提交给I/O调度器,I/O调度器会将这些bio构造成request结构, request_queue里面顺序存放着一系列的request。新产生的bio可能被合并到request_queue里现有的request中,也可能产生新的request,然后插入到request_queue中合适的位置,这一切都是由I/O调度器来完成的。request_queue、request和bio之间的关系如图35.2.4.1所示:
在这里插入图片描述

图35.2.4.1 request_queue、request和bio之间的关系
bio是个结构体,定义在include/linux/blk_types.h中,结构体内容如下(有缩减):
示例代码35.2.4.4 bio结构体

1  struct bio {
2  		struct bio      	*bi_next;   /* 请求队列的下一个 bio */
3   	struct gendisk   	*bi_disk;
4   	unsigned int      	bi_opf;     /* bottom bits req flags,
5                        	* top bits REQ_OP. Use
6                        	* accessors.
7                        	*/
8   	unsigned short  	bi_flags;   /* bio 状态等信息          */
9   	unsigned short   	bi_ioprio;
10  	unsigned short   	bi_write_hint;
11  	blk_status_t      	bi_status;
12  	u8          			bi_partno;
13  	struct bvec_iter 	bi_iter;    /* I/O 操作,读或写         */
14  	atomic_t        	__bi_remaining;
15  	bio_end_io_t     	*bi_end_io;
16  	void            	*bi_private;
17  	unsigned short   	bi_vcnt;    /* bio_vec 列表中元素数量  */
18 
19  /*
20   * Everything starting with bi_max_vecs will be preserved by 
bio_reset()
21   */
22 
23  unsigned short      	bi_max_vecs;  	/* bio_vec 列表长度 	*/
24  atomic_t        		__bi_cnt;   	/* pin count 			*/
25  struct bio_vec      	*bi_io_vec; 	/* bio_vec  列表 		*/
26  struct bio_set      	*bi_pool;
27 
28  /*
29   * We can inline a number of vecs at the end of the bio, to avoid
30   * double allocations for a small number of bio_vecs. This member
31   * MUST obviously be kept at the very end of the bio.
32   */
33  struct bio_vec      bi_inline_vecs[0];
34 };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
重点来看一下第 13 行和第 25 行,第 13行为 bvec_iter 结构体类型的成员变量,第 25行为bio_vec 结构体指针类型的成员变量。
  • 1

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

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

bio_vec 结构体描述了内容如下:
示例代码35.2.4.6 bio_vec结构体

1   struct bio_vec {
2       struct page *bv_page; 	/* 页 	*/
3       unsigned int bv_len;  	/* 长度 */
4       unsigned int bv_offset; 	/* 偏移 */
5   }
  • 1
  • 2
  • 3
  • 4
  • 5

可以看出 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这三个结构体之间的关系如图35.2.4.2所示:
在这里插入图片描述

图35.2.4.2 bio、bio_iter与bio_vec之间的关系
①、遍历请求中的bio
前面说了,请求中包含有大量的bio,因此就涉及到遍历请求中所有bio并进行处理。遍历请求中的bio使用函数__rq_for_each_bio,这是一个宏,内容如下:
示例代码35.2.4.7 __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结构体指针类型。
②、遍历bio中的所有段
bio包含了最终要操作的数据,因此还需要遍历bio中的所有段,这里要用到bio_for_each_segment函数,此函数也是一个宏,内容如下:
示例代码35.2.4.8 bio_for_each_segment函数

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

第一个bvl参数就是遍历出来的每个bio_vec,第二个bio参数就是要遍历的bio,类型为bio结构体指针,第三个iter参数保存要遍历的bio中bio_iter成员变量。
③、通知bio处理结束
如果使用“制造请求”,也就是抛开I/O调度器直接处理bio的话,在bio处理完成以后要通过内核bio处理完成,使用bio_endio函数,函数原型如下:
bvoid bio_endio(struct bio *bio, int error)
函数参数和返回值含义如下:
bio:要结束的bio。
error:如果bio处理成功的话就直接填0,如果失败的话就填个负值,比如-EIO。
返回值:无
35.3 使用请求队列实验
关于块设备架构就讲解这些,接下来我们使用开发板上的RAM模拟一段块设备,也就是ramdisk,然后编写块设备驱动。
35.3.1 实验程序编写
本实验对应的例程代码路径为:开发板光盘06程序源码3、Linux驱动例程 21_ramdisk_withrequest。
新建名为“20_ramdisk_withrequest”文件夹,然后在20_ramdisk_withrequest文件夹里面创建vscode工程,工作区名为“ramdisk_withrequest”。工程创建好以后新建ramdisk_withrequest.c文件,在ramdisk_withrequest.c里面输入如下内容:
示例代码35.3.1.1 ramdisk_withrequest.c

/***************************************************************
Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
文件名	: ramdisk_withrequest.c
作者   	: 正点原子Linux团队
版本   	: V1.0
描述   	: 块设备请求队列驱动程序
其他   	: 无
论坛   	: www.openedv.com
日志    	: 初版 V1.0 2023/08/15 正点原子Linux团队创建
***************************************************************/
1   #include <linux/module.h>
2   #include <linux/moduleparam.h>
3   #include <linux/init.h>
4   #include <linux/sched.h>
5   #include <linux/kernel.h>   
6   #include <linux/slab.h>     
7   #include <linux/fs.h>       
8   #include <linux/errno.h>    
9   #include <linux/types.h>    
10  #include <linux/fcntl.h>    
11  #include <linux/hdreg.h>
12  #include <linux/kdev_t.h>
13  #include <linux/vmalloc.h>
14  #include <linux/genhd.h>
15  #include <linux/blk-mq.h>
16  #include <linux/buffer_head.h>  
17  #include <linux/bio.h>
18 
19  #define RAMDISK_SIZE    (2 * 1024 * 1024)   /* 容量大小为2MB */
20  #define RAMDISK_NAME    "ramdisk"           /* 名字 */
21  #define RADMISK_MINOR   3                   /* 表示有三个磁盘分区!不是次设备号为3!*/
22 
23  /* ramdisk设备结构体 */
24  struct ramdisk_dev{
25      int major;                      /* 主设备号                       */
26      unsigned char *ramdiskbuf;      /* ramdisk内存空间,用于模拟块设备 */
27      struct gendisk *gendisk;        /* gendisk                        */
28      struct request_queue *queue;    /* 请求队列                       */
29      struct blk_mq_tag_set tag_set;  /* blk_mq_tag_set                 */
30      spinlock_t lock;                /* 自旋锁                          */
31  };
32 
33  struct ramdisk_dev *ramdisk = NULL;     /* ramdisk设备指针 */
34 
35  /*
36   * @description : 处理传输过程
37   * @param-req   : 请求
38   * @return      : 0,成功;其它表示失败
39   */
40  static int ramdisk_transfer(struct request *req)
41  {   
42      unsigned long start = blk_rq_pos(req) << 9;     /* blk_rq_pos获取到的是扇区地址,左移9位转换为字节地址 */
43      unsigned long len  = blk_rq_cur_bytes(req);     /* 大小   */
44      
45 
46      /* bio中的数据缓冲区
47       * 读:从磁盘读取到的数据存放到buffer中
48       * 写:buffer保存这要写入磁盘的数据
49       */
50      void *buffer = bio_data(req->bio);      
51      
52      if(rq_data_dir(req) == READ)        /* 读数据 */ 
53          memcpy(buffer, ramdisk->ramdiskbuf + start, len);
54      else if(rq_data_dir(req) == WRITE)  /* 写数据 */
55          memcpy(ramdisk->ramdiskbuf + start, buffer, len);
56          
57      return 0;
58 
59  }
60 
61  /*
62   * @description : 开始处理传输数据的队列
63   * @hctx        : 硬件相关的队列结构体
64   * @bd          : 数据相关的结构体
65   * @return      : 0,成功;其它值为失败
66   */
67  static blk_status_t _queue_rq(struct blk_mq_hw_ctx *hctx, const 
struct blk_mq_queue_data* bd)
68  {
69      struct request *req = bd->rq; /* 通过bd获取到request队列*/
70      struct ramdisk_dev *dev = req->rq_disk->private_data;
71      int ret;
72      
73      blk_mq_start_request(req);    /* 开启处理队列 */
74      spin_lock(&dev->lock);        
75      ret = ramdisk_transfer(req);  /* 处理数据 */
76      blk_mq_end_request(req, ret); /* 结束处理队列 */
77      spin_unlock(&dev->lock);
78      
79      return BLK_STS_OK;
80      
81  }
82  /*
83   * 队列操作函数
84   */
85  static struct blk_mq_ops mq_ops = {
86      .queue_rq = _queue_rq,
87  };
88 
89  /*
90   * @description     : 打开块设备
91   * @param - dev     : 块设备
92   * @param - mode    : 打开模式
93   * @return          : 0 成功;其他 失败
94   */
95  int ramdisk_open(struct block_device *dev, fmode_t mode)
96  {
97      printk("ramdisk open\r\n");
98      return 0;
99  }
100
101 /*
102  * @description     : 释放块设备
103  * @param - disk    : gendisk
104  * @param - mode    : 模式
105  * @return          : 0 成功;其他 失败
106  */
107 void ramdisk_release(struct gendisk *disk, fmode_t mode)
108 {
109     printk("ramdisk release\r\n");
110 }
111
112 /*
113  * @description     : 获取磁盘信息
114  * @param - dev     : 块设备
115  * @param - geo     : 模式
116  * @return          : 0 成功;其他 失败
117  */
118 int ramdisk_getgeo(struct block_device *dev, struct hd_geometry *geo)
119 {
120     /* 这是相对于机械硬盘的概念 */
121     geo->heads = 2;         /* 磁头 */
122     geo->cylinders = 32;    /* 柱面 */
123   geo->sectors = RAMDISK_SIZE / (2 * 32 *512); /* 一个磁道上的扇区数量 */
124     return 0;
125 }
126
127 /* 
128  * 块设备操作函数 
129  */
130 static struct block_device_operations ramdisk_fops =
131 {
132     .owner   = THIS_MODULE,
133     .open    = ramdisk_open,
134     .release = ramdisk_release,
135     .getgeo  = ramdisk_getgeo,
136 };
137
138 /*
139  * @description : 初始化队列相关操作
140  * @set         : blk_mq_tag_set对象
141  * @return      : request_queue的地址
142  */
143 static struct request_queue * create_req_queue(struct blk_mq_tag_set *set)
144 {
145     struct request_queue *q;
146
147 #if 0
148
149 /*
150  *这里是使用了blk_mq_init_sq_queue 函数
151  *进行初始化的。
152  */
153     q = blk_mq_init_sq_queue(set, &mq_ops, 2, BLK_MQ_F_SHOULD_MERGE);
154     
155 #else
156     int ret;
157     
158     memset(set, 0, sizeof(*set));
159     set->ops = &mq_ops;     //操作函数
160     set->nr_hw_queues = 2;  //硬件队列
161     set->queue_depth = 2;   //队列深度
162     set->numa_node = NUMA_NO_NODE;//numa节点
163     set->flags =  BLK_MQ_F_SHOULD_MERGE; //标记在bio下发时需要合并
164
165     ret = blk_mq_alloc_tag_set(set); //使用函数进行再次初始化
166     if (ret) {
167         printk(KERN_WARNING "sblkdev: unable to allocate tag set\n");
168         return ERR_PTR(ret);
169     }
170     
171     q = blk_mq_init_queue(set); //分配请求队列
172     if(IS_ERR(q)) {
173         blk_mq_free_tag_set(set);
174         return q;
175     }
176 #endif
177
178     return q;
179 }
180
181 /*
182  * @description : 创建块设备,为应用层提供接口。
183  * @set         : ramdisk_dev对象
184  * @return      : 0,表示成功;其它值为失败
185  */
186 static int create_req_gendisk(struct ramdisk_dev *set)
187 {
188     struct ramdisk_dev *dev = set;
189
190     /* 1、分配并初始化 gendisk */
191     dev->gendisk = alloc_disk(RADMISK_MINOR);
192     if(dev == NULL)
193         return -ENOMEM;
194     
195     /* 2、添加(注册)disk */
196     dev->gendisk->major = ramdisk->major; /* 主设备号 */
197     dev->gendisk->first_minor = 0;        /* 起始次设备号 */
198     dev->gendisk->fops = &ramdisk_fops;   /* 操作函数 */
199     dev->gendisk->private_data = set;     /* 私有数据 */
200     dev->gendisk->queue = dev->queue;     /* 请求队列 */
201     sprintf(dev->gendisk->disk_name, RAMDISK_NAME); /* 名字 */
202     set_capacity(dev->gendisk, RAMDISK_SIZE/512);   /* 设备容量(单位为扇区)*/
203     add_disk(dev->gendisk);
204     return 0;
205 }
206
207 /*
208  * @description : 驱动出口函数
209  * @param       : 无
210  * @return      : 无
211  */
212 static int __init ramdisk_init(void)
213 {
214     int ret = 0;
215     struct ramdisk_dev * dev;
216     printk("ramdisk init\r\n");
217     
218     /* 1、申请内存 */
219     dev = kzalloc(sizeof(*dev), GFP_KERNEL);
220     if(dev == NULL) {
221         return -ENOMEM;
222     }
223     
224     dev->ramdiskbuf = kmalloc(RAMDISK_SIZE, GFP_KERNEL);
225     if(dev->ramdiskbuf == NULL) {
226         printk(KERN_WARNING "dev->ramdiskbuf: vmalloc failure.\n");
227         return -ENOMEM;
228     }
229     ramdisk = dev;
230     
231     /* 2、初始化自旋锁 */
232     spin_lock_init(&dev->lock);
233
234     /* 3、注册块设备 */
235     dev->major = register_blkdev(0, RAMDISK_NAME); /* 由系统自动分配主设备号 */
236     if(dev->major < 0) {
237         goto register_blkdev_fail;
238     }
239     
240     /* 4、创建多队列 */
241     dev->queue = create_req_queue(&dev->tag_set);
242     if(dev->queue == NULL) {
243         goto create_queue_fail;
244     }
245     
246     /* 5、创建块设备 */
247     ret = create_req_gendisk(dev);
248     if(ret < 0)
249         goto create_gendisk_fail;
250     
251     return 0;
252
253 create_gendisk_fail:
254     blk_cleanup_queue(dev->queue);
255     blk_mq_free_tag_set(&dev->tag_set);
256 create_queue_fail:
257     unregister_blkdev(dev->major, RAMDISK_NAME);
258 register_blkdev_fail:
259     kfree(dev->ramdiskbuf);
260     kfree(dev);
261     return -ENOMEM;
262 }
263
264 /*
265  * @description : 驱动出口函数
266  * @param       : 无
267  * @return      : 无
268  */
269 static void __exit ramdisk_exit(void)
270 {
271     
272     printk("ramdisk exit\r\n");
273     /* 释放gendisk */
274     del_gendisk(ramdisk->gendisk);
275     put_disk(ramdisk->gendisk);
276
277     /* 清除请求队列 */
278     blk_cleanup_queue(ramdisk->queue);
279     
280     /* 释放blk_mq_tag_set */
281     blk_mq_free_tag_set(&ramdisk->tag_set);
282     
283     /* 注销块设备 */
284     unregister_blkdev(ramdisk->major, RAMDISK_NAME);
285
286     /* 释放内存 */
287     kfree(ramdisk->ramdiskbuf);
288     kfree(ramdisk);
289 }
290
291 module_init(ramdisk_init);
292 module_exit(ramdisk_exit);
293 MODULE_LICENSE("GPL");
294 MODULE_AUTHOR("ALIENTEK");
295 MODULE_INFO(intree, "Y");
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227
  • 228
  • 229
  • 230
  • 231
  • 232
  • 233
  • 234
  • 235
  • 236
  • 237
  • 238
  • 239
  • 240
  • 241
  • 242
  • 243
  • 244
  • 245
  • 246
  • 247
  • 248
  • 249
  • 250
  • 251
  • 252
  • 253
  • 254
  • 255
  • 256
  • 257
  • 258
  • 259
  • 260
  • 261
  • 262
  • 263
  • 264
  • 265
  • 266
  • 267
  • 268
  • 269
  • 270
  • 271
  • 272
  • 273
  • 274
  • 275
  • 276
  • 277
  • 278
  • 279
  • 280
  • 281
  • 282
  • 283
  • 284
  • 285
  • 286
  • 287
  • 288
  • 289
  • 290
  • 291
  • 292
  • 293
  • 294
  • 295
  • 296
  • 297
  • 298
  • 299
  • 300
  • 301
  • 302
  • 303
  • 304
  • 305
  • 306

第19~21行,实验相关宏定义,RAMDISK_SIZE就是模拟块设备的大小,这里设置为2MB,也就是说本实验中的虚拟块设备大小为 2MB。RAMDISK_NAME 为本实验名字,RADMISK_MINOR 是本实验此设备号数量,注意不是次设备号!此设备号数量决定了本块设备的磁盘分区数量。
第24~31行,ramdisk的设备结构体。
第33行,定义一个全局变量ramdisk_dev类型的指针。
ramdisk_init 和 ramdisk_exit 这两个函数分别为驱动入口以及出口函数,我们依次来看一下这两个函数。
第219行,先给自定义的ramdisk结构体申请空间。
第224行,因为本实验是使用一块内存模拟真实的块设备,因此这里先使用 kzalloc 函数申请用于 ramdisk 实验的内存,大小为 2MB。
第232行,初始化一个自旋锁,用于在队列操作的时候做保护。
第235行,使用 register_blkdev 函数向内核注册一个块设备,返回值就是注册成功的块设备主设备号。这里我们让内核自动分配一个主设备号,因此 register_blkdev 函数的第一个参数为 0。
第241行,create_req_queue函数用于创建一个多队列。这一部分主要用于操作块设备,后面会讲解此函数。
第247行,create_req_gendisk函数用于创建一个块设备和提供一些接口给应用层调用,后面会讲解此函数。
ramdisk_exit函数就比较简单了,在卸载块设备驱动模块的时候需要将前面注册的对象进行卸载、实例化的对象要进行释放。
第156~175行,初始化多队列。在159行到163行里先设置多队列的重要参数,比如一些操作函数、队列深度、硬件队列个数和标志位等等;其中第159行设置blk_mq_tag_set的ops成员变量,这就是块设备的队列操作集,这里设置为mq_ops,需要驱动开发人员自行编写实现,后面讲解。使用blk_mq_alloc_tag_set函数进行再次初始化blk_mq_tag_set对象,最后我们根据此对象分配请求队列。我们也可以使用blk_mq_init_sq_queue函数一步到位,第一个参数为blk_mq_tag_set对象、第二个参数为操作函数集合、第三个参数为硬件队列个数,第四个参数为标志位。
第191行,使用alloc_disk分配一个gendisk。
第196~201行,初始化申请到的gendisk对象,重点第208行设置geddisk的fops成员变量,fops负责设置块设备的操作集。第210行设置我们的多队列。
第202行,使用set_capacity函数设置本块设备容量大小,注意这里的大小是扇区数,不是字节数,一个扇区是512字节。
第203行,gendisk初始化完成以后就可以使用add_disk函数将gendisk添加到内核中,也就是向内核添加一个磁盘设备
第4587行就是多队列的操作集blk_mq_os,这里我们只是实现了queue_rq,第6781行就是queue_rq的具体实现;第79行通过blk_mq_queue_data的rq成员获取到request,第73行使用blk_mq_start_request函数开启多队列处理,第75行就是我们自定义的处理数据函数ramdisk_transfer,第76行就是我们通过blk_mq_end_request函数去结束多队列处理。
ramdisk_transfer数据处理函数,第42行blk_rq_pos函数获取要操作的块设备扇区地址,第43行使用blk_rq_cur_bytes函数获取请求要操作的数据长度,第50行使用bio_data函数获取请求中的bio保存的数据。第52~55行rq_data_dio函数判断当前是读还是写,如果是写的话就将bio中的数据拷贝到ramdisk指定地址(扇区),如果是读的话就从ramdisk中的指定打字(扇区)读取数据放到bio中。
这里就简单总结一下块设备的编写步骤,首先有两个重要的结构体:blk_mq_tag_set和gendisk。可以把blk_mq_tag_set看作真正的IO读写操作(ops操作集就是IO操作),有了底层操作还不行,还需要gendisk结构体为上层提供接口调用(fops就是实现上层调用的操作)。代码当中注释已经非常清楚了,这里就不给大家解释了。
35.3.2 运行测试
1、编译驱动程序
编写Makefile文件,本章实验的Makefile文件和第五章实验基本一样,只是将obj-m变量的值改为“miscled.o”,Makefile内容如下所示:
示例代码35.3.2.1 Makefile文件

1  KERNELDIR := /home/alientek/rk3568_linux_sdk/kernel
...... 
4  obj-m := ramdisk_withrequest.o
......
11 clean:
  • 1
  • 2
  • 3
  • 4
  • 5

12 $(MAKE) -C ( K E R N E L D I R ) M = (KERNELDIR) M= (KERNELDIR)M=(CURRENT_PATH) clean
第4行,设置obj-m变量的值为“ramdisk_withrequest.o”。
输入如下命令编译出驱动模块文件:
make ARCH=arm64 //ARCH=arm64必须指定,否则编译会失败
编译成功以后就会生成一个名为“ramdisk_withrequest.ko”的驱动模块文件。
2、运行测试
在Ubuntu中将上一小节编译出来的ramdisk_withrequest.ko通过adb命令发送到开发板的/lib/modules/4.19.232目录下,命令如下:
adb push ramdisk_withrequest.ko keyinputApp /lib/modules/4.19.232
接下来输入如下命令加载keyinput.ko这个驱动模块:
depmod //第一次加载驱动的时候需要运行此命令
modprobe ramdisk_withrequest //加载驱动模块
正常加载驱动就会有如下所示:
在这里插入图片描述

图35.3.2.1 加载randisk_withrequest驱动
3、查看ramdisk磁盘
驱动加载成功之后就会在/dev/目录下生成一个名为“ramdisk”的设备,输入如下命令查看ramdisk磁盘信息:
fdisk -l //查看磁盘信息
上述命令会将当前系统中所有的磁盘信息都打印出来,其中包括了ramdisk设备,如图35.3.2.2所示:
在这里插入图片描述

图35.3.2.2 ramdisk磁盘信息
从图35.3.2.2可以看出,ramdisk已经识别出来了,大小为2MB,但同时也提示/dev/ramdisk没有分区表,因为我们还没格式化/dev/ramdisk。这里注意一下:2MB等于2097152bytes,图上是20971XX52bytes,这是是因为串口终端输出和内核打印日志重叠而出现的问题。我们可以使用另外一种方法来查看,如下图35.3.2.3操作:
在这里插入图片描述

图35.3.2.3 log.txt磁盘信息
4、格式化/dev/ramdisk
使用mkfs.vfat命令格式化/dev/ramdisk,将其格式化为vfat格式,输入如下命令:
mkfs.vfat /dev/ramdisk
格式化完成以后就可以挂载/dev/ramdisk来访问了,挂载点可以自定义,这里笔者就将其挂载到/mnt目录下,输入如下命令:
mkdir /mnt/ram_disk -p //创建ramdisk挂载目录
mount /dev/ramdisk /mnt/ram_disk //挂载ramdisk
挂载成功以后就可以通过/mnt来访问ramdisk这个磁盘了,进入到/mnt目录中,可以通过vi命令新建一个txt文件来测试磁盘访问是否正常。
35.4 不使用请求队列实验
35.4.1 实验程序编写
本实验对应的例程路径为:开发板光盘1、程序源码3、Linux驱动例程 22_ramdisk_norequest。
前面我们学习了如何使用请求队列,请求队列会用到 I/O 调度器,适合机械硬盘这种存储设备。对于 EMMC、SD、ramdisk 这样没有机械结构的存储设备,我们可以直接访问任意一个扇区,因此可以不需要 I/O 调度器,也就不需要请求队列了,这个我们前面已经说过了。本实验就来学习一下如何使用“制造请求”方法,本实验在上一个实验的基础上修改而来,首先是驱动入口函数 ramdisk_init,ramdisk_init 函数大部分和上一个实验相同,我们只需要把blk_mq_tag_set相关的都删除掉,然后修改create_req_queue函数即可,在此函数里使用create_req_queue函数设置“制造请求”函数,修改后的create_req_queue函数内容如下:
示例代码35.4.1.1 create_req_queue函数

130 static struct request_queue * create_req_queue(struct ramdisk_dev *dev)
131 {
132     struct request_queue *q;
133     /* 分配请求队列 */
134     q = blk_alloc_queue(GFP_KERNEL);
135     
136     /* 设置“制造请求”函数 */
137     blk_queue_make_request(q, ramdisk_make_request_fn);
138     
139     /* 保存dev 方便在ramdisk_make_request_fn函数使用 */
140     q->queuedata = dev;
141     return q;
142 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
create_req_queue函数与上一个实验不同的地方在于,这里使用blk_alloc_queue和blk_queue_make_request这两个函数取代了上一个实验中blk_mq_tag_set结构体相关的操作。
第134行,使用blk_alloc_queue函数申请一个请求队列。
第137行,使用blk_queue_make_request函数设置“制造请求”函数,这里设置的制造请求函数为ramdisk_make_request_fn,这个需要驱动编写人员去实现,稍后讲解。
第140行,保存我们自定义的ramdisk_dev结构体。
接下来重点看一下“制造请求”函数 ramdisk_make_request_fn,函数内容如下:
  • 1
  • 2
  • 3
  • 4
  • 5

示例代码35.4.1.1 ramdisk_make_request_fn函数

99  static blk_qc_t ramdisk_make_request_fn(struct request_queue *q, 
struct bio *bio)
100 {
101     int offset;
102     struct bio_vec bvec;
103     struct bvec_iter iter;
104     unsigned long len = 0;
105     struct ramdisk_dev *dev = q->queuedata;
106
107     offset = (bio->bi_iter.bi_sector) << 9;/* 获取偏移地址 */
108     spin_lock(&dev->lock);  
109     /* 处理bio中的每个段 */
110     bio_for_each_segment(bvec, bio, iter){
111         char *ptr = page_address(bvec.bv_page) + bvec.bv_offset;
112         len = bvec.bv_len;
113
114         if(bio_data_dir(bio) == READ)   		/* 读数据 */
115             memcpy(ptr, dev->ramdiskbuf + offset, len);
116         else if(bio_data_dir(bio) == WRITE) 	/* 写数据 */
117             memcpy(dev->ramdiskbuf + offset, ptr, len);
118         offset += len;
119     }
120     spin_unlock(&dev->lock);
121     bio_endio(bio);
122     return BLK_QC_T_NONE;
123 }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
虽然ramdisk_make_request_fn函数第一个参数依旧是请求队列,但是实际上这个请求队列不包含真正的请求,所有的处理内容都在第二个bio参数里面,所以ramdisk_make_request_fn函数里面是全部是对bio的操作。
第103行,直接读取bio的bi_iter成员变量的bi_sector来获取要操作的设备地址(扇区)。
第110~119行,使用bio_for_each_segment函数循环获取bio中的每个段,然后对其每个段进行处理。
第111行,根据bio_vec中页地址以及偏移地址转为真正的数据起始地址。
第112行,获取要出来的数据长度,也就是bio_vec的bv_len成员变量。
第114~117行,和上一个实验一样,要操作的块设备起始地址找到了,数据的存放地址以及长度也知道,接下来就是根据读写操作将数据从块设备中读出来,或者将数据写入到块设备中。
第121行,调用bio_endio函数,结束bio。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

35.4.2 运行测试
测试方法和上一个实验一样,参考35.3小节即可。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/664437
推荐阅读
相关标签
  

闽ICP备14008679号