体验Linux 块设备驱动实验(模拟块)

news2024/9/23 23:24:08

目录

一、块设备

二、块设备驱动框架

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

​​​​​​​

 代码如下

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_irq.h>
#include <linux/slab.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/atomic.h>
#include <linux/timer.h>
#include <linux/jiffies.h>
#include <linux/string.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
#include <linux/input.h>
#include <linux/i2c.h>
#include <linux/delay.h>
#include <asm/unaligned.h>
#include <linux/input/touchscreen.h>
#include <linux/input/mt.h>
#include <linux/blkdev.h>
#include <linux/hdreg.h>

/*定义磁盘大小,内存模拟*/
#define RAMDISK_SIZE (2 * 1024 *1024) /*2MB*/
#define RAMDISK_NAME "ramdisk"		/*名字*/
#define RAMDISK_MINOR 3				/*3个分区*/


/*ramdisk设备结构体*/
struct  ramdisk_dev
{
	int major;
	unsigned char *ramdiskbuf; /*ramdisk内存空间,模拟磁盘空间*/
	struct gendisk *gendisk;
	struct request_queue *queue;
	spinlock_t lock;
	
};

struct  ramdisk_dev ramdisk;
/*数据处理过程*/
static void ramdisk_transfer(struct request *req)
{
	/* 数据传输三要素:源、目的、长度:
	 *			内存地址,块设备地址,长度
	 */
	unsigned long start = blk_rq_pos(req) << 9;
	unsigned long len = blk_rq_cur_bytes(req);
	void *buffer = bio_data(req->bio);

	if(rq_data_dir(req) == READ)
		memcpy(buffer,ramdisk.ramdiskbuf +start,len);
	else
		memcpy(ramdisk.ramdiskbuf +start,buffer ,len);

};


/*请求函数*/
void ramdisk_request_fn(struct request_queue *q)
{
	int err=0;
	struct request *req;
	req = blk_fetch_request(q);
	while (req)
	{
		/*处理request,即具体的数据读写操作*/
		ramdisk_transfer(req);
		
		if (!__blk_end_request_cur(req, err))
			req = blk_fetch_request(q);
	}
	
}

int ramdisk_open(struct block_device *dev, fmode_t mode)
{
	printk("ramdisk open\r\n");
	return 0;
}
void ramdisk_release(struct gendisk *disk, fmode_t mode)
{
	printk("ramdisk release\r\n");
}
/*获取磁盘信息*/
int ramdisk_getgeo(struct block_device *dev, struct hd_geometry *geo)
{
	printk("ramdisk_getgeo\r\n");
	geo->heads = 2;/*磁头*/
	geo->cylinders = 32;/*柱面、磁道*/
	geo->sectors = RAMDISK_SIZE/(2 * 32 * 512);/*一个磁道里面的扇区数量*/
	return 0;
}
/*块设备操作集*/
static const struct block_device_operations ramdisk_fops = 
{
	.owner	=	THIS_MODULE,
	.open	=	ramdisk_open,
	.release	=	ramdisk_release,
	.getgeo	=	ramdisk_getgeo,
};

/*驱动入口函数*/
static int __init ramdisk_init(void)
{
	int ret = 0;
	printk("ramdisk_init\r\n");
	/*申请内存*/
	ramdisk.ramdiskbuf = kzalloc(RAMDISK_SIZE, GFP_KERNEL);
	if(ramdisk.ramdiskbuf == NULL)
	{
		ret = -EINVAL;
		goto ramalloc_fail;
	}
	/*注册块设备*/
	ramdisk.major = register_blkdev(0,RAMDISK_NAME);
	if(ramdisk.major < 0)
	{
		ret = -EINVAL;
		goto ramdisk_register_blkdev_fail;;
	}
	printk("ramdisk major = %d\r\n",ramdisk.major);
	/*申请gendisk*/
	ramdisk.gendisk = alloc_disk(RAMDISK_MINOR);
	if(!ramdisk.gendisk)
	{
		ret = -EINVAL;
		goto gendisk_alloc_fail;
	}
	/*初始化自旋锁*/
	spin_lock_init(&ramdisk.lock);
	/*申请并初始化请求队列*/
	ramdisk.queue = blk_init_queue(ramdisk_request_fn,&ramdisk.lock);
	if(!ramdisk.queue)	{
		ret = -EINVAL;
		goto blk_init_queue_fail;
	}
	/*初始化*/
	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_queue_fail:
	put_disk(ramdisk.gendisk);
gendisk_alloc_fail:
	unregister_blkdev(ramdisk.major,RAMDISK_NAME);
ramdisk_register_blkdev_fail:
	kfree(ramdisk.ramdiskbuf);
ramalloc_fail:
	return ret;
}

/*驱动出口函数*/
static void __exit ramdisk_exit(void)
{
	printk("ramdisk exit\r\n");
	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");
MODULE_AUTHOR("ba che kai qi lai");

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

1、修改makefile

 2、屏蔽代码

 3、添加制造请求函数

代码如下

static void ramdisk_make_request(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_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
		memcpy(ramdisk.ramdiskbuf +offset,ptr ,len);
	
	offset += len;
	}
	set_bit(BIO_UPTODATE,&bio->bi_flags);
	bio_endio(bio,0);
}

​​​​​​​

 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行,调用制造请求函数

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

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

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

相关文章

XWiki Annotation Displayer 存在任意代码执行漏洞(CVE-2023-26475)

漏洞描述 XWiki 是一个开源的企业级 Wiki 平台&#xff0c;Annotation Displayer 是 XWiki 中的一个插件&#xff0c;用于在 XWiki 页面上显示注释和其他相关内容。 该项目受影响版本存在任意代码执行漏洞&#xff0c;由于Annotation Displayer 对 Groovy 宏的使用没有限制&a…

机载SAR文献调研(CSDN_0027_20230219)

文献&#xff08;电子所&#xff09;文献名[1]&#xff1a;詹学丽, et al., 一种用于合成孔径雷达的数字去斜方法. 雷达学报, 2015. 4(04): p. 474-480文章摘要该文提出了一种用于合成孔径雷达&#xff08;SAR&#xff09;的数字去斜方法&#xff0c;适用于发射信号脉冲宽度能够…

女神节 | PHP和Java算什么,女工程师才是最美最好的语言!

世界上第一个程序员是女性 第一个发现Bug的也是女性 在智领云有一群追求快乐和独立的女性工程师 她们多有魅力&#xff1f; 工位上她们专注于数据与代码 平日里郊游、瑜伽、插花、科学养娃一件不落 不仅用0和1编织数字世界 也在用心装点自己的生活 今天是国际劳动妇女节…

Linux 虚拟机安装及与windows远程登录

说明&#xff1a; 本次学习是在windows上安装虚拟机进行的。所以需要先安装VMware WorkStation&#xff0c;这玩意需要秘钥。所以你懂的&#xff0c;资源网上很多 Linux镜像文件下载地址CentOS Linux​​​​​​ 一、 打开VMware&#xff0c;点击创建新的虚拟机 二、选择下载…

【FATE联邦学习】自定义数据集自定义神经网络模型下的横向纵向训练

前言 代码大部分来自 https://fate.readthedocs.io/en/latest/tutorial/pipeline/nn_tutorial/Hetero-NN-Customize-Dataset/#example-implement-a-simple-image-datasethttps://fate.readthedocs.io/en/latest/tutorial/pipeline/nn_tutorial/Homo-NN-Customize-your-Datase…

[沧海月明珠有泪]两数求和

自己写的像屎山一样的代码&#xff0c;实在难以入眼。学习下人家优秀的代码思想粗看这个代码&#xff0c;用的是递归的思想前面三行的意思&#xff1a;初始化两个链表。第四行&#xff1a;把两个链表的值相加赋给sum第五行&#xff1a;判断是否大于9第六行&#xff1a;如果l1的…

map 、multimap

目录 1.基本概念,键值对 2.map的构造和赋值 3.map的大小和交换&#xff0c;size,empty,swap 4.map的插入和删除&#xff0c;insert(make_pair),clear,erase,[]利用key访问vale 5.map的查找和统计,find,count 6.map容器的排序&#xff0c;自定义排序&#xff0c;仿函数 6.…

5MW风电永磁直驱发电机-1200V直流并网MATLAB仿真模型

MATLAB2016b运行。主体模型&#xff1a;风机传动模块、PMSG模块、蓄电池模块、超级电容模块、无穷大电源。蓄电池控制、风机控制、逆变器控制。风机输出功率&#xff1a;直流母线电压&#xff1a;逆变器输出电压&#xff1a;逆变器输出电流&#xff1a;混合储能荷电状态&#x…

2023年金三银四跳槽季,阿里巴巴 Java10W 字面经,首次公布

Java 面试 “金三银四”这个字眼对于程序员应该是再熟悉不过的了&#xff0c;每年的金三银四都会有很多程序员找工作、跳槽等一系列的安排。说实话&#xff0c;面试中 7 分靠能力&#xff0c;3 分靠技能&#xff1b;在刚开始的时候介绍项目都是技能中的重中之重&#xff0c;它…

超图iServer扩展开发记录Restlet 1

在“REST 服务发布机制简述”中&#xff0c;讲述了 REST 服务发布的过程&#xff0c;资源的信息保存在资源配置文件里&#xff0c;并通过 REST 应用上下文传递给 REST 应用对象&#xff0c;从而在 HTTP 请求到达 REST 应用对象的时候&#xff0c;能够找到合适的资源实现来处理。…

Jwt简介

目录前言What is JSON Web Token?When should you use JSON Web Tokens?What is the JSON Web Token structure?HeaderPayloadSignaturePutting all togetherHow do JSON Web Tokens work?Why should we use JSON Web Tokens?前言 技术文档这种东西&#xff0c;我一直认为…

数枝营销与纷享销客达成战略合作,共同推动B2B企业营与销一体化

近日&#xff0c;营销咨询与数字化服务商数枝营销同国内知名SaaS CRM厂商纷享销客举行了战略合作签约仪式&#xff0c;双方就促进B2B企业的“营与销协同增长”将展开全面合作。纷享销客创始人兼CEO罗旭与数枝营销创始人黄海钧 另据工商信息显示&#xff0c;数枝营销&#xff08…

webshell管理工具-菜刀的管理操作

什么是webshell Webshell是一种运行在Web服务器上的脚本程序&#xff0c;通常由黑客使用来绕过服务器安全措施和获取对受攻击服务器的控制权。Webshell通常是通过利用Web应用程序中的漏洞或者弱密码等安全问题而被植入到服务器上的。 一旦Webshell被植入到服务器上&#xff0…

基于应用理解的协议栈优化

作者&#xff1a;余兵 移动互联网时代&#xff0c;不同的应用追求的产品体验差异性很大。 应用商店和图片等下载类型业务追求速度、越快越好&#xff0c;短视频关注起播、拖拽响应速度和观看过程卡不卡&#xff0c;直播追求画质清晰、高码率和直播过程流畅&#xff1b;而游戏则…

苹果iPhone屏下Touch ID技术专利获批,苹果Find My技术大火

根据美国商标和专利局&#xff08;USPTO&#xff09;公示的最新清单&#xff0c;苹果近日获得了屏下 Touch ID 的新技术专利。专利中重点提及了“短波红外线”技术&#xff0c;相关元件位于屏幕下方或者集成到屏幕内。 该专利主要介绍了应用于屏幕 Touch ID 的光学成像系统&…

自动化工具selenium(一)

一)什么是自动化&#xff1f;为什么要做自动化&#xff1f; 自动化测试可以代替一部分手工测试&#xff0c;不能够完全代替手工测试 1)自动化测试相比于手工测试来说人力的投入和时间的投入是非常非常少的&#xff0c;自动化测试能够提高测试效率 2)在回归测试里面&#xff0c;…

被隐藏的过程——预处理

文章目录0. 前言1. 程序的翻译环境和执行环境2. 被隐藏的过程2.1 翻译环境2.2 编译3.2.1 预编译3.2.2 编译2.2.3 汇编2.3 链接2.4 运行环境3. 预处理3.1 预定义符号3.2 #define3.2.1 #define定义标识符3.2.2 #define定义宏3.2.3 #define替换规则3.2.4 #和##3.2.5 带副作用的宏参…

API Gateway vs Load Balancer:选择适合你的网络流量管理组件

本文从对比了 API Gateway 和 Load Balancer 的功能区别&#xff0c;帮助读者更好地了解他们在系统架构中扮演的角色。 作者陈泵&#xff0c;API7.ai 技术工程师。 原文链接 由于互联网技术的发展&#xff0c;网络数据的请求数节节攀升&#xff0c;这使得服务器承受的压力越来…

vue-virtual-scroll-list虚拟列表

当DOM中渲染的列表数据过多时&#xff0c;页面会非常卡顿&#xff0c;非常占用浏览器内存。可以使用虚拟列表来解决这个问题&#xff0c;即使有成百上千条数据&#xff0c;页面DOM元素始终控制在指定数量。 一、参考文档 https://www.npmjs.com/package/vue-virtual-scroll-li…

Web前端学习:章三 -- JavaScript预热(三)

六九&#xff1a;函数的变量提升 函数的变量提升没有var高&#xff0c;var是最高的。 先提var&#xff0c;再提函数 解析&#xff1a; 1、4行打印之前没有定义变量&#xff0c;预解析触发变量提升 2、先提var&#xff0c;再提函数。所以先把var提升到最上面&#xff0c;然后提…