当前位置:   article > 正文

体验Linux 块设备驱动实验(模拟块)_linux设备驱动程序设计与加载实验

linux设备驱动程序设计与加载实验

目录

一、块设备

二、块设备驱动框架

1、块设备的注册和注销

2、gendisk 结构体 

3、block_device_operations 结构体

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

 ①、请求队列 request_queue

②、bio 结构

三、编写驱动之请求队列

1、修改makefile

 2、基本的驱动框架​编辑

3、添加头文件、宏定义

 4、添加设备结构体

 5、操作函数集

6、数据处理函数

7、请求处理函数

8、驱动入口函数

9、驱动出口函数

四、测试

1、加载模块模块

2、查看 ramdisk 磁盘

3、格式化/dev/ramdisk

4、挂载/dev/ramdisk

5、创建文件测试

 五、getgeo 函数

 代码如下

六、编写驱动之不使用请求队列

1、修改makefile

 2、屏蔽代码

 3、添加制造请求函数

4、修改驱动入口函数


        设备驱动要远比字符设备驱动复杂得多,不同类型的存储设备又对应不同的驱动子系统,本章学习一下块设备相关驱动概念,不涉及到具体的存储设备。使用开发板板载 RAM 模拟一个块设备,学习块设备驱动框架的使用

一、块设备

        块设备是针对存储设备的,比如 SD 卡、 EMMC、 NAND Flash、 Nor Flash、 SPI Flash、机械硬盘、固态硬盘等。因此块设备驱动其实就是这些存储设备驱动

块设备驱动相比字符设备驱动的主要区别如下:
①、块设备只能以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲。
②、块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后在一次性将缓冲区中的数据写入块设备中。这么做的目的为了提高块设备寿命,大家如果仔细观察的话就会发现有些硬盘或者 NAND Flash就会标明擦除次数(flash 的特性,写之前要先擦除),比如擦除 100000 次等。因此,为了提高块设备寿命而引入了缓冲区,数据先写入到缓冲区中,等满足一定条件后再一次性写入到真正的物理存储设备中,这样就减少了对块设备的擦除次数,提高了块设备寿命

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

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

二、块设备驱动框架

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

struct block_device {
 dev_t bd_dev; 
 int bd_openers;
 struct inode *bd_inode;
 struct super_block *bd_super;
 struct mutex bd_mutex;
 struct list_head bd_inodes;
 void * bd_claiming;
 void * bd_holder;
 int bd_holders;
 bool bd_write_holder;

...........

struct gendisk *bd_disk;
.........

}

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),如果返回负值那就表示注册失败。

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

void unregister_blkdev(unsigned int major, const char *name)
 major: 要注销的块设备主设备号。
name: 要注销的块设备名字。
返回值: 无

2、gendisk 结构体 

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

struct gendisk {

1        int major;
2        int first_minor;
3        int minors;
...........

 4       struct disk_part_tbl __rcu *part_tbl;
........

 5       const struct block_device_operations *fops;
 6       struct request_queue *queue;

}

1、major 为磁盘设备的主设备号
 2、first_minor 为磁盘的第一个次设备号

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

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

5、fops 为块设备操作集,为 block_device_operations 结构体类型。和字符设备操作集 file_operations 一样

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

编写块的设备驱动的时候需要分配并初始化一个 gendisk

3、block_device_operations 结构体

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

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 *,int rw);
5 int (*ioctl) (struct block_device *, fmode_t, unsigned,unsigned long);
6 int (*compat_ioctl) (struct block_device *, fmode_t, unsigned,unsigned long);
..........
7 int (*getgeo)(struct block_device *, struct hd_geometry *);
.........
8 struct module *owner;

}

 block_device_operations 结构体里面的操作集函数和字符设备的 file_operations操作集基本类似,但是块设备的操作集函数比较少

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

3、 release 函数用于关闭(释放)指定的块设备。
4 、 rw_page 函数用于读写指定的页。

5 、 ioctl 函数用于块设备的 I/O 控制。
6 、 compat_ioctl 函数和 ioctl 函数一样,都是用于块设备的 I/O 控制。区别在于在 64位系统上,         64位应用程序的 ioctl 会调用 compat_iotl 函数。在 32 位系统上运行的APP调用的就是 ioctl 
7 、 getgeo 函数用于获取磁盘信息,包括磁头、柱面和扇区等信息。
8 、 owner 表示此结构体属于哪个模块,一般直接设置为 THIS_MODULE

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

 ①、请求队列 request_queue

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

首先需要申请并初始化一个 request_queue,然后在初始化 gendisk 的时候将这个
request_queue 地址赋值给 gendisk 的 queue 成员变量,当卸载块设备驱动的时候我们还需要删除掉前面申请到的 request_queue,完成了请求队列的申请已经请求处理函数的绑定,这个一般用于像机械
硬盘这样的存储设备,需要 I/O 调度器来优化数据读写过程。但是对于 EMMC、 SD 卡这样的
非机械设备,可以进行完全随机访问,所以就不需要复杂的 I/O 调度器了。对于非机械设备我
们可以先申请 request_queue,然后将申请到的 request_queue 与“制造请求”函数绑定在一起。

②、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 调度器来完成的。

 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 操作,读或写 */

.........

struct bio_vec *bi_io_vec; /* bio_vec 列表 */
.........

}

bvec_iter 结构体类型的成员变量,bio_vec 结构体指针类型的成员变量。

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 中已经处理完成的字节数 */
 };

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

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

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

三、编写驱动之请求队列

1、修改makefile

 2、基本的驱动框架

3、添加头文件、宏定义

RAMDISK_SIZE 就是模拟块设备的大小,这里设置为 2MB,也 就 是 说 本 实 验 中 的 虚 拟 块 设 备 大 小 为 2MB 。 RAMDISK_NAME 为 本 实 验 名 字 ,RADMISK_MINOR 是本实验此设备号数量,注意不是次设备号,此设备号数量决定了本块设备的磁盘分区数量

 4、添加设备结构体

 41行, gendisk,描述一个磁盘设备

42行,请求队列

43行,自旋锁

 5、操作函数集

 这个和字符设备的几乎一样

6、数据处理函数

 54行,blk_rq_pos 获取到的是扇区地址,左移 9 位转换为字节地址,从请求中获取要操作的块设备扇区地址

55行,使用 blk_rq_cur_bytes 函数获取请求要操作的数据长度

56行,使用 bio_data 函数获取请求中 bio 保存的数据

58-61行,调用 rq_data_dir 函数判断当前是读还是写,如果是写的话就将 bio 中的数据拷贝到 ramdisk 指定地址(扇区),如果是读的话就从ramdisk 中的指定地址(扇区)读取数据放到 bio 中

7、请求处理函数

 71行,首先使用 blk_fetch_request 函数获取请求队列中第一个请求

72行,while 循环依次处理完请求队列中的每个请求

77行,使用__blk_end_request_cur 函数检查是否为最后一个请求,如果不是的话就继续获取下个,直至整个请求队列处理完成。

8、驱动入口函数

 116行,使用一块内存模拟真实的块设备,因此这里先使用 kzalloc 函数申请用于 ramdisk 实验的内存,大小为 2MB。

123行,使用 register_blkdev 函数向内核注册一个块设备,返回值就是注册成功的块设备主设备号。这里我们让内核自动分配一个主设备号,因此 register_blkdev 函数的第一个参数为0。

131行,使用 alloc_disk 分配一个 gendisk

138行,初始化一个自旋锁, blk_init_queue 函数在分配并初始化请求队列的时候需要用到一次自旋锁

140行,使用 blk_init_queue 函数分配并初始化一个请求队列,请求处理函数为
ramdisk_request_fn,具体的块设备读写操作就在此函数中完成

146-151行,初始化131行申请到的 gendisk,依次是主设备号,起始次设备号,操作函数,私有数据,请求队列和名字

152行,使用 set_capacity 函数设置本块设备容量大小,注意这里的大小是扇区数,不是字节数,一个扇区是 512 字节

153行,gendisk 初始化完成以后就可以使用 add_disk 函数将 gendisk 添加到内核中,也就是向内核添加一个磁盘设备

9、驱动出口函数

 在卸载块设备驱动的时候需要将前面申请的内容都释放掉。

使用 put_disk 和 del_gendis 函数释放前面申请的 gendisk;

blk_cleanup_queue 函数消除前面申请的请求队列;

使用 unregister_blkdev 函数注销前面注册的块设备;

最后调用 kfree 来释放掉申请的内存。

四、测试

1、加载模块模块

2、查看 ramdisk 磁盘

fdisk -l

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

3、格式化/dev/ramdisk

mkfs.vfat /dev/ramdisk

4、挂载/dev/ramdisk

mount /dev/ramdisk /tmp

 挂载成功以后就可以通过/tmp 来访问 ramdisk 这个磁盘了

5、创建文件测试

 卸载重新加载进入

 五、getgeo 函数

此函数用户获取磁盘信息,信息保存在参数 geo 中,为结构体 hd_geometry 类型,如下:

1 struct hd_geometry {
2         unsigned char heads; /* 磁头 */
3         unsigned char sectors; /*一个磁道上的扇区数量 */
4         unsigned short cylinders; /* 柱面 */
5         unsigned long start;
 };

设置 ramdisk 有 2 个磁头(head)、一共有 32 个柱面(cylinderr)。 知道磁盘总容量、磁头数、柱面数以后我们就可以计算出一个磁道上有多少个扇区了

 加载查看

fdisk -l

​​​​​​​

 代码如下

  1. #include <linux/module.h>
  2. #include <linux/kernel.h>
  3. #include <linux/init.h>
  4. #include <linux/fs.h>
  5. #include <linux/uaccess.h>
  6. #include <linux/io.h>
  7. #include <linux/cdev.h>
  8. #include <linux/device.h>
  9. #include <linux/of.h>
  10. #include <linux/of_address.h>
  11. #include <linux/of_irq.h>
  12. #include <linux/slab.h>
  13. #include <linux/of_address.h>
  14. #include <linux/of_gpio.h>
  15. #include <linux/atomic.h>
  16. #include <linux/timer.h>
  17. #include <linux/jiffies.h>
  18. #include <linux/string.h>
  19. #include <linux/irq.h>
  20. #include <linux/interrupt.h>
  21. #include <linux/input.h>
  22. #include <linux/i2c.h>
  23. #include <linux/delay.h>
  24. #include <asm/unaligned.h>
  25. #include <linux/input/touchscreen.h>
  26. #include <linux/input/mt.h>
  27. #include <linux/blkdev.h>
  28. #include <linux/hdreg.h>
  29. /*定义磁盘大小,内存模拟*/
  30. #define RAMDISK_SIZE (2 * 1024 *1024) /*2MB*/
  31. #define RAMDISK_NAME "ramdisk" /*名字*/
  32. #define RAMDISK_MINOR 3 /*3个分区*/
  33. /*ramdisk设备结构体*/
  34. struct ramdisk_dev
  35. {
  36. int major;
  37. unsigned char *ramdiskbuf; /*ramdisk内存空间,模拟磁盘空间*/
  38. struct gendisk *gendisk;
  39. struct request_queue *queue;
  40. spinlock_t lock;
  41. };
  42. struct ramdisk_dev ramdisk;
  43. /*数据处理过程*/
  44. static void ramdisk_transfer(struct request *req)
  45. {
  46. /* 数据传输三要素:源、目的、长度:
  47. * 内存地址,块设备地址,长度
  48. */
  49. unsigned long start = blk_rq_pos(req) << 9;
  50. unsigned long len = blk_rq_cur_bytes(req);
  51. void *buffer = bio_data(req->bio);
  52. if(rq_data_dir(req) == READ)
  53. memcpy(buffer,ramdisk.ramdiskbuf +start,len);
  54. else
  55. memcpy(ramdisk.ramdiskbuf +start,buffer ,len);
  56. };
  57. /*请求函数*/
  58. void ramdisk_request_fn(struct request_queue *q)
  59. {
  60. int err=0;
  61. struct request *req;
  62. req = blk_fetch_request(q);
  63. while (req)
  64. {
  65. /*处理request,即具体的数据读写操作*/
  66. ramdisk_transfer(req);
  67. if (!__blk_end_request_cur(req, err))
  68. req = blk_fetch_request(q);
  69. }
  70. }
  71. int ramdisk_open(struct block_device *dev, fmode_t mode)
  72. {
  73. printk("ramdisk open\r\n");
  74. return 0;
  75. }
  76. void ramdisk_release(struct gendisk *disk, fmode_t mode)
  77. {
  78. printk("ramdisk release\r\n");
  79. }
  80. /*获取磁盘信息*/
  81. int ramdisk_getgeo(struct block_device *dev, struct hd_geometry *geo)
  82. {
  83. printk("ramdisk_getgeo\r\n");
  84. geo->heads = 2;/*磁头*/
  85. geo->cylinders = 32;/*柱面、磁道*/
  86. geo->sectors = RAMDISK_SIZE/(2 * 32 * 512);/*一个磁道里面的扇区数量*/
  87. return 0;
  88. }
  89. /*块设备操作集*/
  90. static const struct block_device_operations ramdisk_fops =
  91. {
  92. .owner = THIS_MODULE,
  93. .open = ramdisk_open,
  94. .release = ramdisk_release,
  95. .getgeo = ramdisk_getgeo,
  96. };
  97. /*驱动入口函数*/
  98. static int __init ramdisk_init(void)
  99. {
  100. int ret = 0;
  101. printk("ramdisk_init\r\n");
  102. /*申请内存*/
  103. ramdisk.ramdiskbuf = kzalloc(RAMDISK_SIZE, GFP_KERNEL);
  104. if(ramdisk.ramdiskbuf == NULL)
  105. {
  106. ret = -EINVAL;
  107. goto ramalloc_fail;
  108. }
  109. /*注册块设备*/
  110. ramdisk.major = register_blkdev(0,RAMDISK_NAME);
  111. if(ramdisk.major < 0)
  112. {
  113. ret = -EINVAL;
  114. goto ramdisk_register_blkdev_fail;;
  115. }
  116. printk("ramdisk major = %d\r\n",ramdisk.major);
  117. /*申请gendisk*/
  118. ramdisk.gendisk = alloc_disk(RAMDISK_MINOR);
  119. if(!ramdisk.gendisk)
  120. {
  121. ret = -EINVAL;
  122. goto gendisk_alloc_fail;
  123. }
  124. /*初始化自旋锁*/
  125. spin_lock_init(&ramdisk.lock);
  126. /*申请并初始化请求队列*/
  127. ramdisk.queue = blk_init_queue(ramdisk_request_fn,&ramdisk.lock);
  128. if(!ramdisk.queue) {
  129. ret = -EINVAL;
  130. goto blk_init_queue_fail;
  131. }
  132. /*初始化*/
  133. ramdisk.gendisk->major = ramdisk.major;/*主设备号*/
  134. ramdisk.gendisk->first_minor = 0;
  135. ramdisk.gendisk->fops = &ramdisk_fops;
  136. ramdisk.gendisk->private_data = &ramdisk;
  137. ramdisk.gendisk->queue = ramdisk.queue;
  138. sprintf(ramdisk.gendisk->disk_name,RAMDISK_NAME);
  139. set_capacity(ramdisk.gendisk,RAMDISK_SIZE/512);
  140. add_disk(ramdisk.gendisk);
  141. return 0;
  142. blk_init_queue_fail:
  143. put_disk(ramdisk.gendisk);
  144. gendisk_alloc_fail:
  145. unregister_blkdev(ramdisk.major,RAMDISK_NAME);
  146. ramdisk_register_blkdev_fail:
  147. kfree(ramdisk.ramdiskbuf);
  148. ramalloc_fail:
  149. return ret;
  150. }
  151. /*驱动出口函数*/
  152. static void __exit ramdisk_exit(void)
  153. {
  154. printk("ramdisk exit\r\n");
  155. del_gendisk(ramdisk.gendisk);
  156. put_disk(ramdisk.gendisk);
  157. blk_cleanup_queue(ramdisk.queue);
  158. unregister_blkdev(ramdisk.major,RAMDISK_NAME);
  159. kfree(ramdisk.ramdiskbuf);
  160. }
  161. module_init(ramdisk_init);
  162. module_exit(ramdisk_exit);
  163. MODULE_LICENSE("GPL");
  164. MODULE_AUTHOR("ba che kai qi lai");

六、编写驱动之不使用请求队列

1、修改makefile

 2、屏蔽代码

 3、添加制造请求函数

代码如下

  1. static void ramdisk_make_request(struct request_queue *q,struct bio *bio)
  2. {
  3. int offset;
  4. struct bio_vec bvec;
  5. struct bvec_iter iter;
  6. unsigned long len=0;
  7. /*要操作的磁盘扇区偏移,改为字节地址*/
  8. offset = bio->bi_iter.bi_sector << 9;
  9. /*循环处理每个段*/
  10. bio_for_each_segment(bvec,bio,iter)
  11. {
  12. char *ptr = page_address(bvec.bv_page) + bvec.bv_offset;
  13. len = bvec.bv_len;
  14. if(bio_data_dir(bio) == READ)
  15. memcpy(ptr,ramdisk.ramdiskbuf +offset,len);
  16. else
  17. memcpy(ramdisk.ramdiskbuf +offset,ptr ,len);
  18. offset += len;
  19. }
  20. set_bit(BIO_UPTODATE,&bio->bi_flags);
  21. bio_endio(bio,0);
  22. }

​​​​​​​

 91行,直接读取 bio 的 bi_iter 成员变量的 bi_sector 来获取要操作的设备地址(扇区)

93行,使用 bio_for_each_segment 函数循环获取 bio 中的每个段,然后对其每个段进行处理

95行,根据 bio_vec 中页地址以及偏移地址转换为真正的数据起始地址

96行,获取要出来的数据长度,也就是 bio_vec 的 bv_len 成员变量

98-101行,要操作的块设备起始地址知道了,数据的存放地址以及长度也知道,接下来就是根据读写操作将数据从块设备中读出来,或者将数据写入到块设备中

103行,处理完一个后继续往后处理

106行,调用 bio_endio 函数,结束 bio

4、修改驱动入口函数

 168行,,使用 blk_alloc_queue 函数申请一个请求队列

175行,调用制造请求函数

测试方法和上面一样,参考上面的测试即可
 

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

闽ICP备14008679号