Linux 块设备驱动实验

news/2025/2/22 13:52:17/

块设备驱动要远比字符设备驱动复杂得多,不同类型的存储设备又对应不同的驱动子系统,本章我们重点学习一下块设备相关驱动概念,不涉及到具体的存储设备。

1、什么是块设备?

块设备是针对存储设备的,比如 SD 卡、EMMC、NAND Flash、Nor Flash、SPI Flash、机械硬盘、固态硬盘等。块设备驱动相比字符设备
驱动的主要区别如下:

①、块设备只能以块为单位进行读写访问,块是 linux 虚拟文件系统(VFS)基本的数据传输单位。字符设备是以字节为单位进行数据传输的,不需要缓冲。

②、块设备在结构上是可以进行随机访问的,对于这些设备的读写都是按块进行的,块设备使用缓冲区来暂时存放数据,等到条件成熟以后在一次性将缓冲区中的数据写入块设备中。这么做的目的为了提高块设备寿命。

2、块设备驱动框架

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

struct block_device {dev_t			bd_dev;  /* not a kdev_t - it's a search key */int				bd_openers;struct inode *			bd_inode;	/* will die */struct super_block *	bd_super;struct mutex		bd_mutex;	/* open/close mutex */struct list_head	bd_inodes;void *				bd_claiming;void *				bd_holder;int					bd_holders;bool				bd_write_holder;
#ifdef CONFIG_SYSFSstruct list_head	bd_holder_disks;
#endifstruct block_device *	bd_contains;unsigned				bd_block_size;struct hd_struct *		bd_part;/* number of times partitions within this device have been opened. */unsigned				bd_part_count;int					bd_invalidated;struct gendisk *	bd_disk;struct request_queue *  bd_queue;struct list_head	bd_list;/** Private data.  You must have bd_claim'ed the block_device* to use this.  NOTE:  bd_claim allows an owner to claim* the same device multiple times, the owner must take special* care to not mess up bd_private for that case.*/unsigned long		bd_private;/* The counter of freeze processes */int					bd_fsfreeze_count;/* Mutex for freeze */struct mutex		bd_fsfreeze_mutex;
};

重点关注一下第 21 行的 bd_disk 成员变量,此成员变量为gendisk 结构体指针类型。内核使用 block_device 来表示一个具体的块设备对象,比如一个硬盘或者分区,如果是硬盘的话 bd_disk 就指向通用磁盘结构 gendisk。

2.2、注册块设备

我们需要向内核注册新的块设备、申请设备号,块设备注册函数为register_blkdev,函数原型如下:

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

函数参数和返回值含义如下:
major:主设备号。
name:块设备名字。
返回值:如果参数 major 在 1~255 之间的话表示自定义主设备号,那么返回 0 表示注册成功,如果返回负值的话表示注册失败。如果 major 为 0 的话表示由系统自动分配主设备号,那么返回值就是系统分配的主设备号(1-255),如果返回负值那就表示注册失败。

2.3、注销块设备

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

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

函数参数和返回值含义如下:
major:要注销的块设备主设备号。
name:要注销的块设备名字。
返回值:无

2.4、gendisk 结构体

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

struct gendisk {/* major, first_minor and minors are input parameters only,* don't use directly.  Use disk_devt() and disk_max_parts().*/int major;			/* major number of driver */int first_minor;int minors;                     /* maximum number of minors, =1 for* disks that can't be partitioned. */char disk_name[DISK_NAME_LEN];	/* name of major driver */char *(*devnode)(struct gendisk *gd, umode_t *mode);unsigned int events;		/* supported events */unsigned int async_events;	/* async events, subset of all *//* Array of pointers to partitions indexed by partno.* Protected with matching bdev lock but stat and other* non-critical accesses use RCU.  Always access through* helpers.*/struct disk_part_tbl __rcu *part_tbl;struct hd_struct part0;const struct block_device_operations *fops;struct request_queue *queue;void *private_data;int flags;struct device *driverfs_dev;  // FIXME: removestruct kobject *slave_dir;struct timer_rand_state *random;atomic_t sync_io;		/* RAID */struct disk_events *ev;
#ifdef  CONFIG_BLK_DEV_INTEGRITYstruct blk_integrity *integrity;
#endifint node_id;
};

第 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 的磁盘,其扇区数量就是(210241024)/512=4096。
返回值:无。

5、调整 gendisk 引用计数
内核会通过 get_disk 和 put_disk 这两个函数来调整 gendisk 的引用计数,根据名字就可以知道,get_disk 是增加 gendisk 的引用计数,put_disk 是减少 gendisk 的引用计数,这两个函数原型如下所示:

truct kobject *get_disk(struct gendisk *disk)
void put_disk(struct gendisk *disk)

2.5、block_device_operations 结构体

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

struct block_device_operations {int (*open) (struct block_device *, fmode_t);void (*release) (struct gendisk *, fmode_t);int (*rw_page)(struct block_device *, sector_t, struct page *, int rw);		//用于读写指定的页int (*ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);int (*compat_ioctl) (struct block_device *, fmode_t, unsigned, unsigned long);long (*direct_access)(struct block_device *, sector_t,void **, unsigned long *pfn, long size);unsigned int (*check_events) (struct gendisk *disk,unsigned int clearing);/* ->media_changed() is DEPRECATED, use ->check_events() instead */int (*media_changed) (struct gendisk *);void (*unlock_native_capacity) (struct gendisk *);int (*revalidate_disk) (struct gendisk *);int (*getgeo)(struct block_device *, struct hd_geometry *);				//用于获取磁盘信息,包括磁头、柱面和扇区等信息。/* this callback is with swap_lock and sometimes page table lock held */void (*swap_slot_free_notify) (struct block_device *, unsigned long);struct module *owner;
};

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

大家如果仔细观察的话会在 block_device_operations 结构体中并没有找到 read 和 write 这样的读写函数,那么块设备是怎么从物理块设备中读写数据?这里就引出了块设备驱动中非常重要的 request_queue、request 和 bio。

1、请求队列 request_queue

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

①、初始化请求队列

request_queue *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock)

函数参数和返回值含义如下:
rfn:请求处理函数指针,每个 request_queue 都要有一个请求处理函数,请求处理函数request_fn_proc 原型如下:

void (request_fn_proc) (struct request_queue *q)

请求处理函数需要驱动编写人员自行实现。
lock:自旋锁指针,需要驱动编写人员定义一个自旋锁,然后传递进来。,请求队列会使用这个自旋锁。
返回值:如果为 NULL 的话表示失败,成功的话就返回申请到的 request_queue 地址。

②、删除请求队列

void blk_cleanup_queue(struct request_queue *q)

函数参数和返回值含义如下:
q:需要删除的请求队列。
返回值:无

③、分配请求队列并绑定制造请求函数

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_alloc_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 卡等。

2、请求 request

request 是一个结构体,request 里面有一个名为“bio”的成员变量,类型为 bio 结构体指针。前面说了,真正的数据就保存在 bio 里面,所以我们需要从 request_queue 中取出一个一个的 request,然后再从每个 request 里面取出 bio,最后根据 bio 的描述讲数据写入到块设备,或者从块设备中读取数据。

①、获取请求
我们需要从request_queue中依次获取每个request,使用blk_peek_request函数完成此操作,函数原型如下:

request *blk_peek_request(struct request_queue *q)

函数参数和返回值含义如下:
q:指定 request_queue。
返回值:request_queue 中下一个要处理的请求(request),如果没有要处理的请求就返回
NULL。

②、开启请求
使用 blk_peek_request 函数获取到下一个要处理的请求以后就要开始处理这个请求,这里要用到 blk_start_request 函数,函数原型如下:

void blk_start_request(struct request *req)

函数参数和返回值含义如下:
req:要开始处理的请求。
返回值:无。

③、一步到位处理请求

我们也可以使用 blk_fetch_request 函数来一次性完成请求的获取和开启,blk_fetch_request函数很简单,内容如下:

struct request *blk_fetch_request(struct request_queue *q)
{struct request *rq;rq = blk_peek_request(q);if (rq)blk_start_request(rq);return rq;
}

可以看出,blk_fetch_request 就是直接调用了 blk_peek_request 和 blk_start_request 这两个函数。

④、其他和请求有关的函数
在这里插入图片描述
3、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 调度器来完成的。request_queue、request 和 bio 之间的关系如下图
在这里插入图片描述
在这里插入图片描述

二、使用请求队列实验

首先是传统的使用请求队列的时候,也就是针对机械硬盘的时候如何编写驱动。

我们先来一下一相关的宏定义和结构体,代码如下:

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

接下来看一下驱动模块的加载与卸载,内容如下:

static int __init ramdisk_init(void)
{int ret = 0;/* 1、申请用于 ramdisk 内存 */ramdisk.ramdiskbuf = kzalloc(RAMDISK_SIZE, GFP_KERNEL);	//因为本实验是使用一块内存模拟真实的块设备,所以申请2m内存if(ramdisk.ramdiskbuf == NULL) {ret = -EINVAL;goto ram_fail;}/* 2、初始化自旋锁 */spin_lock_init(&ramdisk.lock);	//blk_init_queue 函数在分配并初始化请求队列的时候需要用到一次自旋锁。/* 3、注册块设备 */ramdisk.major = register_blkdev(0, RAMDISK_NAME); /* 自动分配 */if(ramdisk.major < 0) {goto register_blkdev_fail;} printk("ramdisk major = %d\r\n", ramdisk.major);/* 4、分配并初始化 gendisk */ramdisk.gendisk = alloc_disk(RADMISK_MINOR);if(!ramdisk.gendisk) {ret = -EINVAL;goto gendisk_alloc_fail;}/* 5、分配并初始化请求队列 */ramdisk.queue = blk_init_queue(ramdisk_request_fn,&ramdisk.lock);	//分配并初始化一个请求队列,请求处理函数为ramdisk_request_fn,具体的块设备读写操作就在此函数中完成,这个需要驱动开发人员去编写if(!ramdisk.queue) {ret = -EINVAL;goto blk_init_fail;}/* 6、添加(注册)disk */ramdisk.gendisk->major = ramdisk.major; /* 主设备号 */ramdisk.gendisk->first_minor = 0; /*起始次设备号) */ramdisk.gendisk->fops = &ramdisk_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_fail:put_disk(ramdisk.gendisk);gendisk_alloc_fail:unregister_blkdev(ramdisk.major, RAMDISK_NAME);register_blkdev_fail:kfree(ramdisk.ramdiskbuf); /* 释放内存 */ram_fail:return ret;
}/*
* @description : 驱动出口函数
* @param : 无
* @return : 无
*/
static void __exit ramdisk_exit(void)
{/* 释放 gendisk */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("hsd");

在 ramdisk_init 函数中设置了 gendisk 的 fops 成员变量,也就是块设备的操作集,具体内容如下:

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)
{/* 这是相对于机械硬盘的概念 */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,
};

重点是 getgeo 函数,第getgeo 的具体实现,此函数用户获取磁盘信息,信息保存在参数 geo 中,为结构体 hd_geometry 类型,如下:

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

本例程中设置 ramdisk 有 2 个磁头(head)、一共有 32 个柱面(cylinderr)。知道磁盘总容量、磁头数、柱面数以后我们就可以计算出一个磁道上有多少个扇区了,也就是 hd_geometry 中的sectors 成员变量。
最后就是非常重要的请求处理函数,使用 blk_init_queue 函数初始化队列的时候需要指定一个请求处理函数,本例程中注册的请求处理函数如下所示:

static void 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); /* 大小 */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);}void ramdisk_request_fn(struct request_queue *q)
{int err = 0;struct request *req;/* 循环处理请求队列中的每个请求 */req = blk_fetch_request(q);while(req != NULL) {/* 针对请求做具体的传输处理 */ramdisk_transfer(req);/* 判断是否为最后一个请求,如果不是的话就获取下一个请求* 循环处理完请求队列中的所有请求。*/if (!__blk_end_request_cur(req, err))req = blk_fetch_request(q);}
}

三、不使用请求队列实验

ramdisk_init 函数大部分和上一个实验相同,只是本实验中改为使用blk_queue_make_request 函数设置“制造请求”函数,修改后的 ramdisk_init 函数内容如下(有省略):

static int __init ramdisk_init(void)
{....../* 5、分配请求队列 */ramdisk.queue = blk_alloc_queue(GFP_KERNEL);if(!ramdisk.queue){ret = -EINVAL;goto blk_allo_fail;}/* 6、设置“制造请求”函数 */blk_queue_make_request(ramdisk.queue, ramdisk_make_request_fn);/* 7、添加(注册)disk */.......ramdisk.gendisk->fops = &ramdisk_fops; /* 操作函数 */......

接下来重点看一下“制造请求”函数 ramdisk_make_request_fn,函数内容如下:

void 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;offset = (bio->bi_iter.bi_sector) << 9; /* 获取设备的偏移地址 */  //直接读取 bio 的 bi_iter 成员变量的 bi_sector 来获取要操作的设备地址(扇区)。/* 处理 bio 中的每个段 */bio_for_each_segment(bvec, bio, iter)	//循环获取 bio 中的每个段,然后对其每个段进行处理。{char *ptr = page_address(bvec.bv_page) + bvec.bv_offset;//根据 bio_vec 中页地址以及偏移地址转换为真正的数据起始地址。len = bvec.bv_len;		//获取要出来的数据长度,也就是 bio_vec 的 bv_len 成员变量if(bio_data_dir(bio) == READ) /* 读数据 */memcpy(ptr, ramdisk.ramdiskbuf + offset, len);else if(bio_data_dir(bio) == WRITE) /* 写数据 */memcpy(ramdisk.ramdiskbuf + offset, ptr, len);offset += len;}set_bit(BIO_UPTODATE, &bio->bi_flags);bio_endio(bio, 0);
}	

虽然 ramdisk_make_request_fn 函数第一个参数依旧是请求队列,但是实际上这个请求队列不包含真正的请求,所有的处理内容都在第二个 bio 参数里面,所以 ramdisk_make_request_fn函数里面是全部是对 bio 的操作。


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

相关文章

基于Spring、SpringMVC、MyBatis的超市管理系统

文章目录 项目介绍主要功能截图:登录首页员工管理供应商管理商品库存管理商品类型管理商品进货管理上班打卡管理请假信息管理交流信息管理交流回复管理系统功能架构图部分代码展示设计总结项目获取方式🍅 作者主页:Java韩立 🍅 简介:Java领域优质创作者🏆、 简历模板、…

Vue计算属性详解

目录 ​编辑 1、什么是计算属性 2、为什么要有计算属性 1. 为什么不是使用模板语法 2. 为什么不是使用method对于复杂逻辑 3. 什么时候要用计算属性 4. 定义计算属性fullName 5. 计算属性的配置项 1、什么是计算属性 写在computed对象中的属性&#xff0c;本质上是…

C语言指针进阶(中)

提示&#xff1a; 上集内容小复习&#x1f970;&#x1f970; int my_strlen(const char* str) {return 1; } int main() {//指针数组char* arr[10];//数组指针int arr2[5] { 0 };int(*p)[5] &arr2; //p是一个指向数组的指针变量//函数指针int (*pf)(const char*)&m…

面试时被问:为什么裁员只裁你,不裁别人,该怎么回答?

面试官总有各种奇奇怪怪的问题&#xff0c;比如这个&#xff1a;为什么裁员裁了你&#xff0c;而不是裁别人&#xff1f;这个充满恶意的问题该怎么回答&#xff1f;网友给出了各种各样的答案&#xff0c;有人说&#xff0c;就说行业动荡&#xff0c;不稳定。有人说&#xff0c;…

数学_计算协方差矩阵/信息矩阵_理论+例子

目录 1. 多元高斯分布 1.1 标准高斯分布 1.2 一元高斯函数&#xff08;一元高斯分布概率密度&#xff09; 1.3 多元高斯分布 2. 协方差矩阵的计算 2.1 问题定义 2.2 室内外温度的例子 参考&#xff1a; 1. 多元高斯分布 1.1 标准高斯分布 标准高斯函数&#xff08;正态…

华为擎云G540笔记本怎么U盘重装电脑系统详细教学

华为擎云G540笔记本怎么U盘重装电脑系统详细教学分享。有用户在使用华为擎云G540笔记本电脑的时候&#xff0c;想要将电脑系统进行更换&#xff0c;但是自己没有重装过电脑系统&#xff0c;不懂要如何去进行操作。针对这个问题&#xff0c;接下来我们就一起来看看详细的操作流程…

Python创建虚拟环境(virtualenv和venv)

Python的虚拟环境可以帮助我们在同一台机器上&#xff0c;同时使用不同的Python版本和库&#xff0c;方便管理和开发。 本文将介绍两种创建Python虚拟环境的方法&#xff1a;virtualenv和venv。 方法一&#xff1a;使用第三方模块virtualenv创建虚拟环境 安装virtualenv 使…

Qt 中的信息输出机制:QDebug、QInfo、QWarning、QCritical 的简单介绍和用法

Qt 中的信息输出机制介绍QDebug在 Qt 中使用 qDebug输出不同类型的信息浮点数&#xff1a;使用 %!f(MISSING) 格式化符号输出浮点数布尔值&#xff1a;使用 %! (MISSING)和 %! (MISSING)格式化符号输出布尔值对象&#xff1a;使用 qPrintable() 函数输出对象的信息qInfoqWarnin…