「Tech初见」Linux驱动之blkdev

news/2024/12/23 1:25:11/

目录

  • 一、Motivation
  • 二、Solution
    • S1 - 块设备驱动框架
      • (1)注册块设备
      • (2)注销块设备
      • (3)申请 gendisk
      • (4)删除 gendisk
      • (5)将 gendisk 加入 kernel
      • (6)设置 gendisk 容量
      • (7)gendisk 引用计数
    • S2 - 定义块设备
    • S3 - 无 I/O 调度的请求队列

一、Motivation

在类 Unix OS 的世界里,I/O 设备都是被当作设备文件( device file )这种特殊文件来处理的。例如,用同一 write() 系统调用既可以向普通文件中写入数据,也可以向打印机等外围设备中写数据。根据设备文件的特点可分为字符设备和块设备,

  1. 字符设备一般是不支持随机访问的,例如,鼠标和键盘等;
  2. 块设备是支持随机访问的,例如,硬盘

我们为什么要编写关于设备的驱动程序呢?主要是为了操控插入的外接设备,本文主要是对块设备展开具体讲解,如有字符设备的需求请移步「Tech初见」Linux驱动之blkdev

二、Solution

S1 - 块设备驱动框架

kernel 用 block_device 结构体表示块设备,定义在 include/linux/fs.h 中,结构体内部的成员变量太多了,我决定不展开讲解,等到用到的时候再说。关于 block_device 我们在这里仅仅需要搞明白两点就可以了,

(1)注册块设备

该方法是向 kernel 中注册新的块设备并申请设备号,原型为,

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

其中,major 为主设备号,name 为块设备名称。若 major 为 0,表示由系统自动分配主设备号;若 major 在 1 ~ 255 之间,表示自定义的主设备号。返回值为 0 则表示注册成功,反之失败。关于主次设备号的内容同样在「Tech初见」Linux驱动之blkdev 中有详细的说明

(2)注销块设备

该方法是在 kernel 中注销指定的块设备,原型为,

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

gendisk 是块设备最重要的结构体,意为通用磁盘,定义在 include/linux/genhd.h,可以理解为 gendisk 是我们创建的块设备节点与 kernel 交互的中间人,同样我们一开始不需要太深入地了解内部的成员变量,只需要记得这几个用于申请和释放 gendisk 的方法即可,

(3)申请 gendisk

在使用 gendisk 之前要先申请,原型为,

struct gendisk* alloc_disk(int minors)

其中,minors 为次设备号的数量,即 gendisk 对应的分区数量

(4)删除 gendisk

原型为,

void del_gendisk(struct gendisk* gdisk)

(5)将 gendisk 加入 kernel

申请到 gendisk 之后,对其进行初始化,然后就可以加入到 kernel 中了,原型为,

void add_disk(struct gendisk* gdisk)

(6)设置 gendisk 容量

初始化 gendisk 时需要设置其容量,原型为,

void set_capacity(struct gendisk* gdisk, sector_t size)

其中的 size 是磁盘容量大小,注意这里指的是 sector 个数,一个 sector 通常为 512 字节

(7)gendisk 引用计数

增加 gendisk 的引用计数,

struct kobject* get_disk(struct gendisk* gdisk)

减少 gendisk 的引用计数,

void put_disk(struct gendisk* disk)

当 kernel 中没人再引用该 gendisk 时,kernel 就可以放心大胆地释放这块空间了
block_device_operations 用来表示块设备的操作集,定义在 include/linux/blkdev.h 中,同样也无需了解太多,记住 open() 和 release() 即可

关于块设备 I/O 请求过程,我先要引入的就是请求队列 request_queue 的概念,它就是一个队列,里面存放着不同的 I/O 请求,我们都知道 I/O 操作比 CPU 操作要慢很多,为了提高系统的利用率和吞吐量,我们一般都是等一等 I/O 操作,待它成势了再一次性地写入磁盘,这样可以减少磁盘的寻道时间,队列和 request 以及包含所需操作的磁块 bio 的关系如下图,

每个 gendisk 都应该有一个请求队列,可以通过,

struct request_queue* blk_alloc_queue(gfp_t gfp_mask)

进行申请,gfp_mask 一般为 GFP_KERNEL。然后通过,

void blk_queue_make_request(struct request_queue* que, make_request_fn* fn)

来为队列绑定请求函数,意思就是说只要是这个队列中的请求,统统按照 fn 的业务逻辑来处理。当然,还需要实现具体的 fn,

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

最后,我还想讲解一下 bio 结构体,它保存着最终要读写的数据地址等信息,定义在 include/linux/blk_types.h 中

S2 - 定义块设备

定义一些自己的块设备及对应的操作,struct myblkdev 包含的 struct gendisk 相当重要,透过它才能体现出我们定义的是块设备,buf 指向模拟的磁盘空间,宏 DISKSIZE 是磁盘的大小,默认为 2 MB,宏 NDISKPART 表示磁盘有 3 个 sector,每个 sector 的大小为宏 SECTORSIZE 512,

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/list.h>
#include <linux/fs.h>
#include <linux/blkdev.h>
#include <linux/genhd.h>
#include <linux/slab.h>
#include <linux/string.h>#define DISKSIZE (2*1024*1024) /* 模拟磁盘的大小为 2 MB */
#define DEVNAME "ramdisk"
#define NDISKPART 3			/* 模拟磁盘有 3 个分区 */
#define SECTORSIZE 512		/* 扇区大小 *//* 自定义的块设备结构 */
struct myblkdev {int major;unsigned char* buf;		/* 指向模拟磁盘的空间 */struct gendisk* gdisk;struct request_queue* que;
};struct myblkdev mydev;int
my_blkdev_open(struct block_device* dev, fmod_t mode)
{printk(KERN_INFO "my_blkdev_open\n");return 0;
}void 
my_blkdev_release(struct gendisk* gdisk, fmod_t mode)
{printk(KERN_INFO "my_blkdev_release\n");
}struct block_device_operations blkops = {.owner = THIS_MODULE,.open = my_blkdev_open,.release = my_blkdev_release,
};

然后,我们的目光就可以转到驱动程序注册的流程当中了,相比于「Tech初见」Linux驱动之blkdev 的字符设备,块设备的初始化稍微复杂了一些,但是本质都是一样的,即是申请 - 初始化 - 加入 kernel 模型。拢共分为五步走,

/* 初始化函数 */
static int __init
my_blkdev_init(void)
{int ok = 0;printk(KERN_INFO "my_blkdev_init\n");/* 注册块设备 */mydev.major = register_blkdev(0, DEVNAME);if(mydev.major < 0) {ok = -EINVAL;goto over;}/* 申请模拟磁盘的内存空间 */mydev.buf = kzalloc(DISKSIZE, GFP_KERNEL);if(mydev.buf == NULL) {ok = -EINVAL;goto alloc_buf_fail;}/* 分配 gendisk */mydev.gdisk = alloc_disk(NDISKPART);if(mydev.gdisk == NULL) {ok = -EINVAL;goto alloc_disk_fail;}/* 分配 request 队列 */mydev.que = blk_alloc_queue(GFP_KERNEL);if(mydev.que == NULL) {ok = -EINVAL;goto alloc_que_fail;}blk_queue_make_request(mydev.que, my_blkdev_make_req_fn);/* 注册 gendisk */mydev.gdisk->major = mydev.major;mydev.gdisk->first_minor = 0;mydev.gdisk->fops = &blkops;mydev.gdisk->queue = mydev.que;mydev.gdisk->private_data = &mydev;strcpy(mydev.gdisk->disk_name, DEVNAME);	/* 给 myblkdev 最核心的组件 kobject 的 gdisk 取名字 */set_capacity(mydev.gdisk, DISKSIZE/SECTORSIZE); /* 设备容量以 sector 为单位 */add_disk(mydev.gdisk);goto over;alloc_que_fail:put_disk(mydev.gdisk);
alloc_disk_fail:kfree(mydev.buf);
alloc_buf_fail:unregister_blkdev(mydev.major, DEVNAME);
over:return ok;
}

(1)注册块设备,为设备申请主设备号并告知 kernel:块设备的名称;(2)申请模拟磁盘的内存空间,通过 kzalloc() 申请空间交给块设备管理;(3)申请 gendisk;(4)为块设备分配 request 队列,用以存放 I/O 请求;(5)初始化 gendisk 并将它注册到 kernel 模型中

大抵就是这些流程,需要注意的是,这里我采用的是不使用 I/O 调度的请求队列。我想大概讲一讲有 I/O 调度和没有的请求队列的区别,对于一些老旧的块设备,比如机械硬盘,有调度的请求队列能够利用请求的特性对其进行重新排序进而减少机械臂移动的次数,从而提高磁盘的工作性能;但是对于现在的 NAND 闪存 OR 固态硬盘,对请求进行排序可能并不会带来实质性的性能提升,反而会增加额外的开销。所以,具体选择哪种请求队列,需要根据实际情况进行判断

关于请求队列的详细用法我在下一小节再展开讲解,我们继续顺着注册模块的流程将驱动程序进行到底,讲解一下模块注销的相关步骤,

/* 卸载函数 */
static void __exit
my_blkdev_exit(void)
{printk(KERN_INFO "my_blkdev_exit\n");/* 删除 gendisk */del_gendisk(mydev.gdisk);/* 减少 gendisk 的引用计数 */put_disk(mydev.gdisk);/* 清空 request 队列 */blk_cleanup_queue(mydev.que);/* 注销块设备 */unregister_blkdev(mydev.major, DEVNAME);/* 释放空间 */kfree(mydev.buf);
}

和注册流程一样,申请了哪些东西,在注销时都要归还,包括 gendisk、请求队列等

S3 - 无 I/O 调度的请求队列

无 I/O 调度的请求队列绑定一个请求处理函数,我取名叫 my_blkdev_make_req_fn,它接受 req_que 请求队列和 bio I/O 操作(页、长度和偏移)作为参数,

/* 制造请求函数 */
void
my_blkdev_make_req_fn(struct request_queue* req_que, 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_for_each_segment(bvec, bio, iter) {	/* 处理 bio 中的每个段 */char* ptr = page_address(bvec.bv_page) + bvec.bv_offset;len = bvec.bv_len;/* 是读操作 OR 写操作 */if(bio_data_dir(bio) == READ)memcpy(ptr, mydev.buf+offset, len);if(bio_data_dir(bio) == WRITE)memcpy(mydev.buf+offset, ptr, len);offset += len;}set_bit(BIO_UPTODATE, &bio->bi_flags);bio_endio(bio, 0);
}

bio_for_each_segment 即是遍历 bio 中从当前偏移 offset 开始的未完成的数据段,而宏 bio_for_each_segment_all 是遍历 bio 中所有数据段,不论它们是否已经被完成。在宏的作用域内针对每个数据段做相应的读/写操作。最后,处理完 bio 的所有数据段后透过 bio_endio() 告诉 request_que 目前的 I/O 任务已经完成


http://www.ppmy.cn/news/1080760.html

相关文章

龙迅LT9611 2PORT MIPIDSI/CSI转HDMI,带HDCP,支持分辨率高达4K30HZ

龙迅LT9611 1.描述&#xff1a; LT9611 MIPIDSI/CSI到HDMI1.4桥具有双端口MIPID-PHY接收器前端配置&#xff0c;每个端口有4个数 据通道&#xff0c;每个数据通道运行2Gbps&#xff0c;最大输入带宽为16Gbps。该桥提供了一个HDMI数据输出与可 选的S/PDIF或8通道I2S串行音频…

Vue框架--Vue中的数据绑定

Vue中有两种数据绑定的方式 1.单向数据绑定(v-band):数据只能够从data流向页面 2.双向数据绑定(v-model):数据不仅仅能够从data流向页面&#xff0c;也可以从页面流向data。 备注: 1.双向绑定一般都应用在表单类元素上。(如:input、select等有value属性值的标签上) 2.…

yolov8机器视觉-工业质检

使用训练好的模型进行预测 yolo predict taskdetect model训练好的模型路径 source测试图片文件夹路径 showTrue效果展示 切换模型进行训练&#xff08;yolov8s&#xff09; 修改main.py训练参数文件 使用云gpu进行训练&#xff0c;很方便&#xff1a;点击链接转至在线云gpu…

用户行为数据模拟/采集

用户行为数据模拟 一级目录 一级目录 3.5 模拟数据 3.5.1 使用说明 1&#xff09;将application.yml、gmall2020-mock-log-2021-10-10.jar、path.json、logback.xml上传到hadoop102的/opt/module/applog目录下 &#xff08;1&#xff09;创建applog路径 [gpbhadoop102 module…

【算法竞赛宝典】稀疏数组

【算法竞赛宝典】稀疏数组 题目描述代码展示 题目描述 代码展示 random.cpp #include <iostream>using namespace std;int main() {freopen("zip5.in", "w", stdout);int n, m;cin >> n >> m;cout << n << << m <…

SCOPE_IDENTITY什么意思

在关系型数据库中&#xff0c;SCOPE_IDENTITY()是一个用于获取最近插入的行的自增标识列值的函数。当向数据库表中插入一行数据时&#xff0c;如果表中的某列被配置为自增标识列&#xff08;通常是主键列&#xff09;&#xff0c;数据库会自动为每个插入的行分配一个唯一的值&a…

Matlab(数值微积分)

目录 1.多项式微分与积分 1.1 微分 1.2 多项式微分 1.3 如何正确的使用Matlab? 1.3.1 Matlab表达多项式 1.3.2 polyval() 多项式求值 1.3.3 polyder()多项式微分 1.4 多项式积分 1.4.1 如何正确表达 1.4.2 polyint() 多项式积分 2.数值的微分与积分 2.1 数值微分 2…

Ubuntu入门05——磁盘管理与备份压缩

1.检查磁盘空间占用情况 2.统计目录或文件所占磁盘空间大小 3.压缩 3.1 zip、unzip和zipinfo 运行时发现上面命令不成功&#xff0c;换成&#xff1a; &#xff08;将文件lkw放入压缩文件lkw01.zip中&#xff09; sudo zip -m lkw01.zip lkw 解压文件&#xff1a; 实操&…