STM32MP157驱动开发——Linux块设备驱动

news2024/9/20 20:40:36

STM32MP157驱动开发——Linux块设备驱动

  • 一、简介
  • 二、驱动开发
    • 1.使用请求队列的方式
    • 2.测试①
    • 3.不使用请求队列的方式
    • 4.测试②


参考文章:【正点原子】I.MX6U嵌入式Linux驱动开发——Linux 块设备驱动

一、简介

  之前学习的都是关于字符设备的驱动,包括 platform 子系统、I2C总线等,本质上都是对字符设备驱动的一层封装。这节就学习第二种驱动模式——块设备驱动。块设备驱动要远比字符设备驱动复杂得多,不同类型的存储设备又对应不同的驱动子系统。这一节使用开发板板载 RAM 模拟一个块设备,学习块设备驱动框架的使用。
  块设备是针对存储设备的,比如 SD 卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等。块设备与字符设备的区别如下:

1.块设备只能以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。而字符设备是以字节为单位进行数据传输的,不需要缓冲。
2.块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后再一次性将缓冲区中的数据写入块设备中。而字符设备是顺序的数据流设备,字符设备是按照字节进行读写访问的。字符设备不需要缓冲区,对于字符设备的访问都是实时的,而且也不需要按照固定的块大小进行访问。

块设备驱动框架的常用属性及使用流程就参考原子哥教程中的讲解,这里就不多赘述。

二、驱动开发

本节尝试使用开发板上的 RAM 模拟一段块设备,也就是 ramdisk,然后编写块设备驱动。

1.使用请求队列的方式

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/kernel.h>	
#include <linux/slab.h>		
#include <linux/fs.h>		
#include <linux/errno.h>	
#include <linux/types.h>	
#include <linux/fcntl.h>	
#include <linux/hdreg.h>
#include <linux/kdev_t.h>
#include <linux/vmalloc.h>
#include <linux/genhd.h>
#include <linux/blk-mq.h>
#include <linux/buffer_head.h>	
#include <linux/bio.h>

#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内存空间,用于模拟块设备 */
	struct gendisk *gendisk; 		/* gendisk 						  */
	struct request_queue *queue;	/* 请求队列 					  */
	struct blk_mq_tag_set tag_set; 	/* blk_mq_tag_set 				  */
	spinlock_t lock;				/* 自旋锁 						  */
};

struct ramdisk_dev *ramdisk = NULL;		/* ramdisk设备指针 */

/*
 * @description	: 处理传输过程
 * @param-req 	: 请求
 * @return 		: 0,成功;其它表示失败
 */
static int 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);
		
	return 0;

}

/*
 * @description	: 开始处理传输数据的队列
 * @hctx 		: 硬件相关的队列结构体
 * @bd			: 数据相关的结构体
 * @return 		: 0,成功;其它值为失败
 */
static blk_status_t _queue_rq(struct blk_mq_hw_ctx *hctx, const struct blk_mq_queue_data* bd)
{
	struct request *req = bd->rq; /* 通过bd获取到request队列*/
	struct ramdisk_dev *dev = req->rq_disk->private_data;
	int ret;
	
	blk_mq_start_request(req);    /* 开启处理队列 */
	spin_lock(&dev->lock);		  
	ret = ramdisk_transfer(req);  /* 处理数据 */
	blk_mq_end_request(req, ret); /* 结束处理队列 */
	spin_unlock(&dev->lock);
	
	return BLK_STS_OK;
	
}
/*
 * 队列操作函数
 */
static struct blk_mq_ops mq_ops = {
    .queue_rq = _queue_rq,
};

/*
 * @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	: 初始化队列相关操作
 * @set		 	: blk_mq_tag_set对象
 * @return 		: request_queue的地址
 */
static struct request_queue * create_req_queue(struct blk_mq_tag_set *set)
{
	struct request_queue *q;

#if 0

/*
 *这里是使用了blk_mq_init_sq_queue 函数
 *进行初始化的。
 */
	q = blk_mq_init_sq_queue(set, &mq_ops, 2, BLK_MQ_F_SHOULD_MERGE);
	
#else
	int ret;
	
	memset(set, 0, sizeof(*set));
	set->ops = &mq_ops;		//操作函数
    set->nr_hw_queues = 2;	//硬件队列
    set->queue_depth = 2;	//队列深度
	set->numa_node = NUMA_NO_NODE;//numa节点
	set->flags =  BLK_MQ_F_SHOULD_MERGE; //标记在bio下发时需要合并

	ret = blk_mq_alloc_tag_set(set); //使用函数进行再次初始化
	if (ret) {
		printk(KERN_WARNING "sblkdev: unable to allocate tag set\n");
		return ERR_PTR(ret);
	}
	
	q = blk_mq_init_queue(set); //分配请求队列
	if(IS_ERR(q)) {
		blk_mq_free_tag_set(set);
		return q;
	}
#endif

	return q;
}

/*
 * @description	: 创建块设备,为应用层提供接口。
 * @set		 	: ramdisk_dev对象
 * @return 		: 0,表示成功;其它值为失败
 */
static int create_req_gendisk(struct ramdisk_dev *set)
{
	struct ramdisk_dev *dev = set;

	/* 1、分配并初始化 gendisk */
    dev->gendisk = alloc_disk(RADMISK_MINOR);
	if(dev == NULL)
		return -ENOMEM;
	
	/* 2、添加(注册)disk */
	dev->gendisk->major = ramdisk->major; /* 主设备号 */
	dev->gendisk->first_minor = 0;		  /* 起始次设备号 */
	dev->gendisk->fops = &ramdisk_fops;	  /* 操作函数 */
	dev->gendisk->private_data = set;	  /* 私有数据 */
	dev->gendisk->queue = dev->queue;	  /* 请求队列 */
	sprintf(dev->gendisk->disk_name, RAMDISK_NAME); /* 名字 */
	set_capacity(dev->gendisk, RAMDISK_SIZE/512);	/* 设备容量(单位为扇区)*/
	add_disk(dev->gendisk);
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init ramdisk_init(void)
{
	int ret = 0;
	struct ramdisk_dev * dev;
	printk("ramdisk init\r\n");
	
	/* 1、申请内存 */
	dev = kzalloc(sizeof(*dev), GFP_KERNEL);
	if(dev == NULL) {
		return -ENOMEM;
	}
	
	dev->ramdiskbuf = kmalloc(RAMDISK_SIZE, GFP_KERNEL);
	if(dev->ramdiskbuf == NULL) {
		printk(KERN_WARNING "dev->ramdiskbuf: vmalloc failure.\n");
		return -ENOMEM;
	}
	ramdisk = dev;
	
	/* 2、初始化自旋锁 */
	spin_lock_init(&dev->lock);

	/* 3、注册块设备 */
	dev->major = register_blkdev(0, RAMDISK_NAME); /* 由系统自动分配主设备号 */
	if(dev->major < 0) {
		goto register_blkdev_fail;
	}
	
	/* 4、创建多队列 */
	dev->queue = create_req_queue(&dev->tag_set);
	if(dev->queue == NULL) {
		goto create_queue_fail;
	}
	
	/* 5、创建块设备 */
	ret = create_req_gendisk(dev);
	if(ret < 0)
		goto create_gendisk_fail;
	
    return 0;

create_gendisk_fail:
	blk_cleanup_queue(dev->queue);
	blk_mq_free_tag_set(&dev->tag_set);
create_queue_fail:
	unregister_blkdev(dev->major, RAMDISK_NAME);
register_blkdev_fail:
	kfree(dev->ramdiskbuf);
	kfree(dev);
	return -ENOMEM;
}

/*
 * @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);
	
	/* 释放blk_mq_tag_set */
	blk_mq_free_tag_set(&ramdisk->tag_set);
	
	/* 注销块设备 */
	unregister_blkdev(ramdisk->major, RAMDISK_NAME);

	/* 释放内存 */
	kfree(ramdisk->ramdiskbuf);
	kfree(ramdisk);
}

module_init(ramdisk_init);
module_exit(ramdisk_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

①首先是宏定义部分,RAMDISK_SIZE 就是模拟块设备的大小,这里设置为 2MB。RAMDISK_NAME 为本实验名字,RADMISK_MINOR 是本实验此设备号数量。注意:非次设备号。设备号数量决定了本块设备的磁盘分区数量。
②通过 ramdisk 的设备结构体定义一个全局变量 ramdisk_dev 类型的指针。
③先看一下驱动模块的加载和卸载,即ramdisk_init()和ramdisk_exit()。

  • 在ramdisk_init()中先为自定义的ramdisk结构体申请空间,使用kmalloc申请2MB大小空间。然后初始化一个自旋锁,用于在队列操作的时候做保护。接着使用 register_blkdev 函数向内核注册一个块设备,返回值就是注册成功的块设备主设备号。这里让内核自动分配一个主设备号,因此 register_blkdev 函数的第一个参数为 0。create_req_queue 函数用于创建一个多队列,这一部分主要用于操作块设备。create_req_gendisk 函数用于创建一个块设备和提供一些接口给应用层调用。
  • ramdisk_exit 函数在卸载块设备驱动模块的时候使用,需要将前面注册的对象进行卸载、实例化的对象要进行释放。

④create_req_queue 函数用于创建一个多队列,首先设置多队列的重要参数,比如一些操作函数、队列深度、硬件队列个数和标志位等。然后设置 blk_mq_tag_set 的 ops 成员变量,这就是块设备的队列操作集,由开发人员实现。再使用 blk_mq_alloc_tag_set 函数进行再次初始化 blk_mq_tag_set 对象,最后根据此对象分配请求队列。
也可以使用 blk_mq_init_sq_queue 函数一步到位,第一个参数为 blk_mq_tag_set 对象、第二个参数为操作函数集合、第三个参数为硬件队列个数,第四个参数为标志位。
⑤使用 create_req_gendisk 函数进行初始化块设备。先使用 alloc_disk 分配一个 gendisk,然后初始化申请到的 gendisk 对象,重点是设置 geddisk 的 fops 成员变量。再使用 set_capacity 函数设置本块设备容量大小。注意:这里的大小是扇区数,不是字节数,一个扇区是 512 字节。gendisk 初始化完成以后就可以使用 add_disk 函数将 gendisk 添加到内核中,也就是向内核添加一个磁盘设备。
⑥gendisk 的 fops 操作集。就是块设备的操作集 block_device_operations,本节仅实现了 open、release 和 getgeo,其中 open 和 release 函数都是空函数,重点是 getgeo 函数。此函数用来获取磁盘信息,保存在参数 geo 中。
⑦blk_mq_tag_set 的 ops 操作集,也就是请求处理函数集合。使用 blk_mq_start_request 函数开启多队列处理,blk_mq_end_request 函数去结束多队列处理。ramdisk_transfer 数据处理函数,使用 ramdisk_transfer 数据处理函数,使用 bio_data 函数获取请求中的 bio 保存的数据。rq_data_dio 函数判断当前是读还是写,如果是写的话就将 bio 中的数据拷贝到 ramdisk 指定地址(扇区),如果是读的话就从 ramdisk 中的指定打字(扇区)读取数据放到 bio 中。

总结:主要是两个重要的结构体:blk_mq_tag_set 和 gendisk。可以把 blk_mq_tag_set 看作真正的 IO 读写操作(ops 操作集就是 IO 操作),有了底层操作之后,还需要 gendisk 结构体为上层提供接口调用(fops 就是实现上层调用的操作)。

2.测试①

驱动编写完成后,就可以编译出.ko文件进行挂载测试。
另外,还需要在 buildroot 中的 busybox 使能 mkfs.vfat 命令。在 buildroot 源码目录下,使用sudo make busybox-menuconfig命令打开 busybox 配置界面,选中以下选项:
在这里插入图片描述
然后使用以下命令,编译出新的根文件系统:

sudo make busybox	#编译新的busybox
sudo make 			#打包出新的buildroot

然后就可以使用新的根文件系统进行启动。
在这里插入图片描述
驱动挂载成功后,可以使用fdisk -l命令查看磁盘信息。其中就包括 2MB 的ramdisk设备。
然后使用以下命令进行格式化,然后挂载,就可以操作这块磁盘了。

mkfs.vfat /dev/ramdisk	#格式化磁盘为 vfat 格式
mkdir /mnt/ram_disk -P  #创建 ramdisk 挂载目录
mount /dev/ramdisk /mnt/ram_disk #挂载 ramdisk

3.不使用请求队列的方式

请求队列会用到 I/O 调度器,适合机械硬盘这种存储设备。对于 EMMC、SD、ramdisk 这样没有机械结构的存储设备,可以直接访问任意一个扇区,因此可以不需要 I/O 调度器,也就不需要请求队列了。
参考 linux 内核的 drivers/block/zram/zram_drv.c,把 blk_mq_tag_set 相关的都删除掉,然后修改 create_req_queue 函数即可,在此函数里使用 create_req_queue 函数设置“制造请求”函数。

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
#include <linux/sched.h>
#include <linux/kernel.h>	
#include <linux/slab.h>		
#include <linux/fs.h>		
#include <linux/errno.h>	
#include <linux/types.h>	
#include <linux/fcntl.h>	
#include <linux/hdreg.h>
#include <linux/kdev_t.h>
#include <linux/vmalloc.h>
#include <linux/genhd.h>
#include <linux/blk-mq.h>
#include <linux/buffer_head.h>	
#include <linux/bio.h>

#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内存空间,用于模拟块设备 */
	struct gendisk *gendisk; 		/* gendisk 						  */
	struct request_queue *queue;	/* 请求队列 					  */
	spinlock_t lock;				/* 自旋锁 						  */
};

struct ramdisk_dev *ramdisk = NULL;		/* 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 		: 无
 */
static blk_qc_t 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;
	struct ramdisk_dev *dev = q->queuedata;

	offset = (bio->bi_iter.bi_sector) << 9;	/* 获取要操作的设备的偏移地址 */
	spin_lock(&dev->lock);	
	/* 处理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, dev->ramdiskbuf + offset, len);
		else if(bio_data_dir(bio) == WRITE)	/* 写数据 */
			memcpy(dev->ramdiskbuf + offset, ptr, len);
		offset += len;
	}
	spin_unlock(&dev->lock);
    bio_endio(bio);
    return BLK_QC_T_NONE;
}

/*
 * @description	: 初始化队列相关操作
 * @set		 	: blk_mq_tag_set对象
 * @return 		: request_queue的地址
 */
static struct request_queue * create_req_queue(struct ramdisk_dev *set)
{
	struct request_queue *q;

	q = blk_alloc_queue(GFP_KERNEL);
	
	blk_queue_make_request(q, ramdisk_make_request_fn);
	
    q->queuedata = set;
	return q;
}

/*
 * @description	: 创建块设备,为应用层提供接口。
 * @set		 	: ramdisk_dev对象
 * @return 		: 0,表示成功;其它值为失败
 */
static int create_req_gendisk(struct ramdisk_dev *set)
{
	struct ramdisk_dev *dev = set;

	/* 1、分配并初始化 gendisk */
    dev->gendisk = alloc_disk(RADMISK_MINOR);
	if(dev == NULL)
		return -ENOMEM;
	
	/* 2、添加(注册)disk */
	dev->gendisk->major = ramdisk->major; /* 主设备号 */
	dev->gendisk->first_minor = 0;		  /* 起始次设备号 */
	dev->gendisk->fops = &ramdisk_fops;	  /* 操作函数 */
	dev->gendisk->private_data = set;	  /* 私有数据 */
	dev->gendisk->queue = dev->queue;	  /* 请求队列 */
	sprintf(dev->gendisk->disk_name, RAMDISK_NAME); /* 名字 */
	set_capacity(dev->gendisk, RAMDISK_SIZE/512);	/* 设备容量(单位为扇区)*/
	add_disk(dev->gendisk);
	return 0;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init ramdisk_init(void)
{
	int ret = 0;
	struct ramdisk_dev * dev;
	printk("ramdisk init\r\n");
	
	/* 1、申请内存 */
	dev = kzalloc(sizeof(*dev), GFP_KERNEL);
	if(dev == NULL) {
		return -ENOMEM;
	}
	
	dev->ramdiskbuf = kmalloc(RAMDISK_SIZE, GFP_KERNEL);
	if(dev->ramdiskbuf == NULL) {
		printk(KERN_WARNING "dev->ramdiskbuf: vmalloc failure.\n");
		return -ENOMEM;
	}
	ramdisk = dev;
	
	/* 2、初始化自旋锁 */
	spin_lock_init(&dev->lock);

	/* 3、注册块设备 */
	dev->major = register_blkdev(0, RAMDISK_NAME); /* 由系统自动分配主设备号 */
	if(dev->major < 0) {
		goto register_blkdev_fail;
	}
	
	/* 4、创建多队列 */
	dev->queue = create_req_queue(dev);
	if(dev->queue == NULL) {
		goto create_queue_fail;
	}
	
	/* 5、创建块设备 */
	ret = create_req_gendisk(dev);
	if(ret < 0)
		goto create_gendisk_fail;
	
    return 0;

create_gendisk_fail:
	blk_cleanup_queue(dev->queue);
create_queue_fail:
	unregister_blkdev(dev->major, RAMDISK_NAME);
register_blkdev_fail:
	kfree(dev->ramdiskbuf);
	kfree(dev);
	return -ENOMEM;
}

/*
 * @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);
	kfree(ramdisk);
}

module_init(ramdisk_init);
module_exit(ramdisk_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

重点为 create_req_queue 函数,使用 blk_alloc_queue 和 blk_queue_make_request 这两个函数取代了上一种方式中 blk_mq_tag_set 结构体相关的操作。blk_alloc_queue 函数用来申请一个请求队列,blk_queue_make_request 函数设置“制造请求”函数,这里设置的制造请求函数为 ramdisk_make_request_fn,由开发人员实现。
ramdisk_make_request_fn 函数第一个参数依旧是请求队列,但是实际上这个请求队列不包含真正的请求,所有的处理内容都在第二个 bio 参数里面,所以 ramdisk_make_request_fn 函数里面是全部是对 bio 的操作。

4.测试②

非请求队列方式的驱动测试与上面的测试①没有什么区别,这里就不再赘述。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/135080.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Node.js下载安装与基础操作

&#x1f973;博 主&#xff1a;初映CY的前说 &#x1f31e;个人信条&#xff1a;想要变成得到&#xff0c;中间还有做到&#xff01; &#x1f918;本文核心&#xff1a;Node.js的下载安装操作 node.js下载安装 node.js中文网下载链接http://nodejs.cn/download/ 1.浏…

模型性能分析:ROC 分析和 AUC

本文[1]将介绍模型性能分析的两个方法&#xff1a;ROC & AUC。 ROC 分析和曲线下面积 (AUC) 是数据科学中广泛使用的工具&#xff0c;借鉴了信号处理&#xff0c;用于评估不同参数化下模型的质量&#xff0c;或比较两个或多个模型的性能。 传统的性能指标&#xff0c;如准确…

什么是进程、线程,什么是并发、并行及线程的创建和线程的基本使用

一、什么是程序、进程、线程 1、什么是程序 程序可以理解为是我们执行的一段代码&#xff0c;是一种静态的概念 2、什么是进程 进程是指运行中的程序&#xff0c;是一个动态的概念。进程有它自身的产生、存在和消亡的过程&#xff08;进程产生就会占用内存空间&#xff0c;反…

【WSL】[04]从C盘解放出来WSL的linux镜像

前言&#xff1a; C盘的硬盘资源有限&#xff0c;虚拟机的需求无限&#xff0c;所以&#xff0c;要把无限的硬盘需求搞到其他盘去才行啊 方案1&#xff1a;利用工具&#xff1a;move-wsl 1 管理员运行PowerShell,创建WSL的工作目录 移动前&#xff0c;C盘的空间大小&#xf…

vue-element-表格 Excel 【导出】功能

表格Excel导出功能 1. 将点击导出按钮添加点击事件click“handleDownload” 并在method中创建方法 <el-button type"danger" size"small" click"handleDownload">excel导出</el-button>复制下面的方法 or 去vue-element-admin中的s…

分享62个PHP源码,总有一款适合您

链接&#xff1a;https://pan.baidu.com/s/17mzEPqFhZp0UEvznSviiEA?pwdnjin 提取码&#xff1a;njin PHP源码 分享62个PHP源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c;大家下载后…

【C语言简明教程】探究整型数据在内存中的存储

概述 我们知道一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。那么整型数据在所开辟内存中到底是如何存储的&#xff1f; 目录 概述 原码、反码、补码 什么是原码、反码和补码&#xff1f; 为什么使用补码存储&#xff1f; 大小端存储 - 数据…

Hadoop高手之路6-ZooKeeper

文章目录Hadoop高手之路6-Zookeeper分布式协调服务一、Zookeeper简介二、Zookeeper的特性1. 一致性C2. 可靠性3. 顺序性4. 原子性A5. 实时性三、Zookeeper分布式集群的部署1. 下载安装包2. 上传3. 解压4. 配置环境变量5. 配置Zookeeper1) 复制一个配置模板文件2) 修改配置文件3…

C语言排序算法

冒泡排序&#xff08;英语&#xff1a;Bubble Sort&#xff09;是一种简单的排序算法。它重复地走访过要排序的数列&#xff0c;一次比较两个元素&#xff0c;如果他们的顺序&#xff08;如从大到小、首字母从A到Z&#xff09;错误就把他们交换过来。 过程演示&#xff1a; #i…

shell第二天练习

题目 1、编写一个 Shell脚本&#xff0c;程序执行时从键盘读入一个目录名&#xff0c;如果用户输入的目录不存在&#xff0c;则提示file does not exist&#xff1b;如果用户输入的不是目录则提示用户必须输入目录名&#xff1b;如果用户输入的是目录则显示这个目录下所有文件…

django笔记《模型和数据库一》

文章目录1 前言2 创建一个demo项目2.1 修改配置文件3 模型3.1 主键3.2 django 内置字段类型3.3 自定义字段类型3.4 django字段选项3.5 字段备注名3.5 META3.6 关联关系3.6.1 多对一关系3.6.2 多对多关系3.6.3 一对一关系3.7 字段命名限制3.8 模型属性&#xff1a;Model.objects…

基于springboot+Vue前后端分离的招聘管理系统(程序+数据库+文档)

大家好✌&#xff01;我是CZ淡陌。一名专注以理论为基础实战为主的技术博主&#xff0c;将再这里为大家分享优质的实战项目&#xff0c;本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目&#xff0c;希望你能有所收获&#xff0c;少走一些弯路…

A Survey on Deep Learning Techniques for Stereo-based Depth Estimation论文阅读

1. 摘要 估计RGB图片的深度是一个长期存在的病态问题&#xff0c;计算机视觉、图形学、机器学习社区已经探索了数十年。立体匹配是最广泛见诸文献的技术之一&#xff0c;因为它与人类的双目系统有强关联。传统上&#xff0c;利用多张图片的人工特征匹配来处理基于立体的深度估…

nginx 实现图片防盗链功能

在搜索浏览网页的时候&#xff0c;发现一篇文章是从我的个人网站转载的&#xff0c;但是没有注明出处&#xff0c;文章中的图片也没有本地化处理&#xff0c;还是从我的服务器请求&#xff0c;这就无形中增加了我的服务器的开销&#xff0c;于是有了设置防盗链功能这一想法。 …

Kafka 核心源码解读【五】--延迟操作模块

文章目录1 TimingWheel&#xff1a;探究Kafka定时器背后的高效时间轮算法1.1 时间轮简介1.2 源码层级关系1.3 时间轮各个类源码定义1.3.1 TimerTask 类1.3.2 TimerTaskEntry 类1.3.3 TimerTaskList 类1.3.4 TimingWheel 类1.4 总结2 DelayedOperation&#xff1a;Broker是怎么延…

【Vue】九、vue-element-admin

后端程序员的vue学习之路一、介绍二、功能特性三、前置准备四、前置知识五、项目结构说明&#xff1a;六、安装运行一、介绍 vue-element-admin 是一个后台前端解决方案&#xff0c;它基于 vue 和 element-ui实现&#xff0c;它使用了最新的前端技术栈&#xff0c;内置了动态路…

深入理解计算机系统_程序的链接过程

编辑好的程序&#xff0c;依次经过预处理(注释&#xff0c;宏替换&#xff0c;头文件包含&#xff0c;生成.s文件)、编译(生成汇编文件.s )、汇编(生成静态可重定位目标文件&#xff0c;文件名是.o)、链接后最终得到可执行目标文件&#xff0c;这个笔记记录一下&#xff0c;链接…

信号处理系列之死区滤波器(DeadZone)

信号处理专栏相关限幅滤波的文章,可以参看下面的链接,这里不再赘述: 博途PLC信号处理系列之限幅消抖滤波_RXXW_Dor的博客-CSDN博客关于限幅滤波可以参看下面这篇文章PLC信号处理之限幅滤波(西门子三菱FB)_RXXW_Dor的博客-CSDN博客限幅滤波是一种消除随机干扰的有效方法,比…

WordPress元宇宙和VR(虚拟现实)最佳插件汇总

近年来&#xff0c;虚拟现实&#xff08;VR &#xff09;和元宇宙&#xff08;Metaverse &#xff09;变得越来越流行。它使用户能够在舒适的家中享受身临其境的体验。此外&#xff0c;将此功能添加到您的网站可以帮助您的内容更具交互性&#xff0c;这可能会带来更多转化。幸运…

RHCE——ansible安装配置(2)

安装并且配置ansible&#xff1a; 1)安装和配置ansible以及ansible控制节点server.example.com如下&#xff1a; 2)创建一个名为/home/student/ansible/inventory的静态库存文件如下所示&#xff1a; 2.1)node1 是dev主机组的成员 2.2)node2是test主机组的成员 2.3)node1和node…