【Linux设备驱动】1.字符设备驱动程序框架及相关结构体

目录

  • 程序总体框架
    • 模块加载函数
    • 模块卸载函数
    • 具体操作函数
  • 相关结构体
    • cdev结构体
    • file_oparations结构体
  • 设备号
    • 分配设备号
    • 注销设备号
    • 创建设备文件

程序总体框架

/* 包含相关头文件 */
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>#define GLOBALMEM_SIZE	0x1000
#define MEM_CLEAR 0x1
#define GLOBALMEM_MAJOR 230static int globalmem_major = GLOBALMEM_MAJOR;
module_param(globalmem_major, int, S_IRUGO);struct globalmem_dev {struct cdev cdev;unsigned char mem[GLOBALMEM_SIZE];
};struct globalmem_dev *globalmem_devp;/* 用于 填充file_operations的函数 */
static int globalmem_open(struct inode *inode, struct file *filp)
{filp->private_data = globalmem_devp;return 0;
}static int globalmem_release(struct inode *inode, struct file *filp)
{return 0;
}static long globalmem_ioctl(struct file *filp, unsigned int cmd,unsigned long arg)
{return 0;
}static ssize_t globalmem_read(struct file *filp, char __user * buf, size_t size,loff_t * ppos)
{unsigned long p = *ppos;unsigned int count = size;int ret = 0;struct globalmem_dev *dev = filp->private_data;if (p >= GLOBALMEM_SIZE)return 0;if (count > GLOBALMEM_SIZE - p)count = GLOBALMEM_SIZE - p;if (copy_to_user(buf, dev->mem + p, count)) {ret = -EFAULT;} else {*ppos += count;ret = count;printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p);}return ret;
}static ssize_t globalmem_write(struct file *filp, const char __user * buf,size_t size, loff_t * ppos)
{unsigned long p = *ppos;unsigned int count = size;int ret = 0;struct globalmem_dev *dev = filp->private_data;if (p >= GLOBALMEM_SIZE)return 0;if (count > GLOBALMEM_SIZE - p)count = GLOBALMEM_SIZE - p;if (copy_from_user(dev->mem + p, buf, count))ret = -EFAULT;else {*ppos += count;ret = count;printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p);}return ret;
}/* 定义一个file_operations结构体 */
static const struct file_operations globalmem_fops = {.owner = THIS_MODULE,.read = globalmem_read,.write = globalmem_write,.unlocked_ioctl = globalmem_ioctl,.open = globalmem_open,.release = globalmem_release,
};/* 定义模块入口函数 */
static int __init globalmem_init(void)
{/* 分配设备号 */int ret;dev_t devno = MKDEV(globalmem_major, 0);if (globalmem_major)ret = register_chrdev_region(devno, 1, "globalmem");else {ret = alloc_chrdev_region(&devno, 0, 1, "globalmem");globalmem_major = MAJOR(devno);}if (ret < 0)return ret;/* 申请、分配结构体内存 */globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);if (!globalmem_devp) {ret = -ENOMEM;goto fail_malloc;}/* 设置cdev结构体 */int err;cdev_init(&globalmem_devp->cdev, &globalmem_fops);globalmem_devp->cdev.owner = THIS_MODULE;err = cdev_add(&globalmem_devp->cdev, devno, 1);if (err)printk(KERN_NOTICE "Error %d adding globalmem%d", err, 0);return 0;fail_malloc:unregister_chrdev_region(devno, 1);return ret;
}
module_init(globalmem_init);/* 定义模块出口函数 */
static void __exit globalmem_exit(void)
{cdev_del(&globalmem_devp->cdev);kfree(globalmem_devp);unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);
}
module_exit(globalmem_exit);/* 模块的相关描述 */
MODULE_AUTHOR("xxx");
MODULE_LICENSE("GPL v2");

总体框架可以分为:

  • 驱动模块加载函数
  • 驱动模块卸载函数
  • 操作函数如open、read、write、release函数,填充file_operations结构体

模块加载函数

/* 定义模块入口函数 */
static int __init globalmem_init(void)
{/* 分配设备号 */int ret;dev_t devno = MKDEV(globalmem_major, 0);if (globalmem_major)ret = register_chrdev_region(devno, 1, "globalmem");else {ret = alloc_chrdev_region(&devno, 0, 1, "globalmem");globalmem_major = MAJOR(devno);}if (ret < 0)return ret;/* 申请、分配结构体内存 */globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);if (!globalmem_devp) {ret = -ENOMEM;goto fail_malloc;}/* 设置cdev结构体 */int err;cdev_init(&globalmem_devp->cdev, &globalmem_fops);globalmem_devp->cdev.owner = THIS_MODULE;err = cdev_add(&globalmem_devp->cdev, devno, 1);if (err)printk(KERN_NOTICE "Error %d adding globalmem%d", err, 0);return 0;fail_malloc:unregister_chrdev_region(devno, 1);return ret;
}
module_init(globalmem_init);

主要完成几件事:申请\分配设备号、cdev结构体的初始化和注册(便于统一管理)。

模块卸载函数

/* 定义模块出口函数 */
static void __exit globalmem_exit(void)
{cdev_del(&globalmem_devp->cdev);kfree(globalmem_devp);unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);
}
module_exit(globalmem_exit);

主要完成几件事:注销cdev结构体,释放设备号。

具体操作函数

/* 用于 填充file_operations的函数 */
static int globalmem_open(struct inode *inode, struct file *filp)
{filp->private_data = globalmem_devp;return 0;
}static int globalmem_release(struct inode *inode, struct file *filp)
{return 0;
}static long globalmem_ioctl(struct file *filp, unsigned int cmd,unsigned long arg)
{return 0;
}static ssize_t globalmem_read(struct file *filp, char __user * buf, size_t size,loff_t * ppos)
{return 0;
}static ssize_t globalmem_write(struct file *filp, const char __user * buf,size_t size, loff_t * ppos)
{return 0;
}/* 定义一个file_operations结构体 */
static const struct file_operations globalmem_fops = {.owner = THIS_MODULE,.read = globalmem_read,.write = globalmem_write,.unlocked_ioctl = globalmem_ioctl,.open = globalmem_open,.release = globalmem_release,
};

定义操作函数如open、read、write、release函数,填充file_operations结构体

相关结构体

cdev结构体

// <include/linux/cdev.h>
struct cdev { struct kobject kobj;                  //内嵌的内核对象.struct module *owner;                 //该字符设备所在的内核模块的对象指针.const struct file_operations *ops;    //该结构描述了字符设备所能实现的方法struct list_head list;                //用来将已经向内核注册的所有字符设备形成链表.dev_t dev;                            //字符设备的设备号,由主设备号和次设备号构成.unsigned int count;                   //隶属于同一主设备号的次设备号的个数.
};// 操作cdev结构体的函数// 对struct cdev结构体做初始化, 最重要的就是建立cdev 和 file_operations之间的连接:
void cdev_init(struct cdev *, const struct file_operations *); // 分配一个struct cdev结构,动态申请一个cdev内存,并对cdev进行初始化
struct cdev *cdev_alloc(void);void cdev_put(struct cdev *p);// 向内核注册一个struct cdev结构体
int cdev_add(struct cdev *, dev_t, unsigned);// 向内核注销一个struct cdev结构体
void cdev_del(struct cdev *);void cd_forget(struct inode *);

字符设备驱动结构cdev介绍 - 知乎 (zhihu.com)

这篇文章对cdev结构体讲解的很清楚。

总的来说就是:

模块加载时会调用cdev_init初始化字符设备结构体,调用cdev_add把这个字符设备添加进内核

模块卸载时调用cdev_del把这个字符设备从内核中销毁

// linux/fs/char_dev.c
int cdev_add(struct cdev *p, dev_t dev, unsigned count)
{int error;p->dev = dev;p->count = count;error = kobj_map(cdev_map, dev, count, NULL,exact_match, exact_lock, p);if (error)return error;kobject_get(p->kobj.parent);return 0;
}
// linux/drivers/base/Map.c
struct kobj_map {struct probe {struct probe *next;dev_t dev;unsigned long range;struct module *owner;kobj_probe_t *get;int (*lock)(dev_t, void *);void *data;} *probes[255];struct mutex *lock;
};int kobj_map(struct kobj_map *domain, dev_t dev, unsigned long range,struct module *module, kobj_probe_t *probe,int (*lock)(dev_t, void *), void *data)
{unsigned n = MAJOR(dev + range - 1) - MAJOR(dev) + 1;unsigned index = MAJOR(dev);unsigned i;struct probe *p;if (n > 255)n = 255;p = kmalloc(sizeof(struct probe) * n, GFP_KERNEL);if (p == NULL)return -ENOMEM;for (i = 0; i < n; i++, p++) {p->owner = module;p->get = probe;p->lock = lock;p->dev = dev;p->range = range;p->data = data;}mutex_lock(domain->lock);for (i = 0, p -= n; i < n; i++, p++, index++) {struct probe **s = &domain->probes[index % 255];while (*s && (*s)->range < range)s = &(*s)->next;p->next = *s;*s = p;}mutex_unlock(domain->lock);return 0;
}

简单地说,设备驱动程序通过调用cdev_add把它所管理的设备对象的指针嵌入到一个类型为struct probe的节点之中,然后再把该节点加入到cdev_map(kobj_map )所实现的哈希链表中。当后续要打开一个字符设备文件时,通过调用 kobj_lookup() 函数,根据设备编号就可以找到 cdev 结构变量,从而取出其中的 ops 字段。

kobj_map函数中哈希表的实现原理和后面注册分配设备号中的几乎完全一样,通过要加入系统的设备的主设备号major(major=MAJOR(dev))来获得probes数组的索引值i(i = major % 255),然后把一个类型为struct probe的节点对象加入到probes[i]所管理的链表中。

file_oparations结构体

file_operation就是把系统调用和驱动程序关联起来的关键数据结构。这个结构的每一个成员都对应着一个系统调用。读取file_operation中相应的函数指针,接着把控制权转交给函数,从而完成了Linux设备驱动程序的工作。

在通读file_operations 方法的列表时, 不少参数包含字串 __user. 这种注解是一种文档形式, 注意, 一个指针是一个不能被直接解引用的用户空间地址. 对于正常的编译,__user没有效果, 但是它可被外部检查软件使用来找出对用户空间地址的错误使用。

struct file_operations被定义在include/linux/fs.h

// include/linux/fs.h
struct file_operations {struct module *owner;loff_t (*llseek) (struct file *, loff_t, int);ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);int (*iterate) (struct file *, struct dir_context *);unsigned int (*poll) (struct file *, struct poll_table_struct *);long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);long (*compat_ioctl) (struct file *, unsigned int, unsigned long);int (*mmap) (struct file *, struct vm_area_struct *);void (*mremap)(struct file *, struct vm_area_struct *);int (*open) (struct inode *, struct file *);int (*flush) (struct file *, fl_owner_t id);int (*release) (struct inode *, struct file *);int (*fsync) (struct file *, loff_t, loff_t, int datasync);int (*aio_fsync) (struct kiocb *, int datasync);int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);int (*check_flags)(int);int (*flock) (struct file *, int, struct file_lock *);ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);int (*setlease)(struct file *, long, struct file_lock **, void **);long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMUunsigned (*mmap_capabilities)(struct file *);
#endif
};

**struct module *owner:**是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为**THIS_MODULE**, 一个在 <linux/module.h> 中定义的宏.

设备号

一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
linux内核中,设备号用dev_t来描述。

typedef u_long dev_t;

在32位机中是4个字节,高12位表示主设备号,低20位表示次设备号。
因此,主设备号范围为2^12 = 4096 = 4K,次设备号范围为2^20 = 1048576 = 1M
1)从dev_t设备号中提取major和minor
MAJOR(dev_t dev)
MINOR(dev_t dev)
2)使用下列宏则可以通过主设备号和次设备号生成dev_t,构建设备号:
MKDEV(int major, int minor)

#define MINORBITS       20
#define MINORMASK       ((1U << MINORBITS) - 1)
#define MAJOR(dev)      ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)      ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))

分配设备号

静态分配:register_chrdev_region()
动态分配:alloc_chrdev_region()

register_chrdev_region()函数用于已知起始设备的设备号的情况,而alloc_chrdev_region()用于设备号未知,向系统动态申请未被占用的设备号的情况,函数调用成功之后,会把得到的设备号放入第一个参数dev中。alloc_chrdev_region()相比于register_chrdev_region()的优点在于它会自动避开设备号重复的冲突。

静态分配和动态分配的函数都调用了__register_chrdev_region函数

// linux\fs\char_dev.c
static struct kobj_map *cdev_map;static DEFINE_MUTEX(chrdevs_lock);static struct char_device_struct {struct char_device_struct *next;unsigned int major;unsigned int baseminor;int minorct;char name[64];struct cdev *cdev;		/* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE]; // 全局的哈希表static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,int minorct, const char *name)
{struct char_device_struct *cd, **cp;int ret = 0;int i;cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);if (cd == NULL)return ERR_PTR(-ENOMEM);// 上锁,防止并发访问chrdevs全局数组mutex_lock(&chrdevs_lock);/* 如果major为0,那么寻找第一个未被占用的设备号 */if (major == 0) {for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {if (chrdevs[i] == NULL)break;}if (i == 0) {ret = -EBUSY;goto out;}major = i;}cd->major = major;cd->baseminor = baseminor; // 起始次设备号cd->minorct = minorct; // 次设备号的数量strlcpy(cd->name, name, sizeof(cd->name));// 插入设备到链表i = major_to_index(major);// 寻找合适的插入位置for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)if ((*cp)->major > major ||((*cp)->major == major &&(((*cp)->baseminor >= baseminor) ||((*cp)->baseminor + (*cp)->minorct > baseminor))))break;/* 检查次设备号范围重叠  */if (*cp && (*cp)->major == major) {int old_min = (*cp)->baseminor;int old_max = (*cp)->baseminor + (*cp)->minorct - 1;int new_min = baseminor;int new_max = baseminor + minorct - 1;// 分别判断左半边和右半边是否重叠/* New driver overlaps from the left.  */if (new_max >= old_min && new_max <= old_max) {ret = -EBUSY;goto out;}/* New driver overlaps from the right.  */if (new_min <= old_max && new_min >= old_min) {ret = -EBUSY;goto out;}}// 插入设备并解锁cd->next = *cp;*cp = cd;mutex_unlock(&chrdevs_lock);return cd;
out: // 错误处理mutex_unlock(&chrdevs_lock);kfree(cd);return ERR_PTR(ret);
}

在这里插入图片描述

linux采用哈希表(chrdevs全局的指针数组)维护设备号。对于主设备号major,通过major_to_index哈希函数(major % 255)计算哈希表的键值作为数组下标i,在以数组下标i的指针元素构成的单链表中寻找合适的插入位置进行插入。

插入点计算如下代码:

// 寻找合适的插入位置for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)if ((*cp)->major > major ||((*cp)->major == major &&(((*cp)->baseminor >= baseminor) ||((*cp)->baseminor + (*cp)->minorct > baseminor))))break;

首先根据主设备号进行判断,保证主设备号是在链表中是递增的;如果主设备号相等,那么就判断次设备号,保证次设备号也是递增的。

另外,全局的chrdevs数组作为哈希表属于共享资源,需要保证互斥访问,因此使用一个互斥锁进行保护。

注销设备号

void unregister_chrdev_region(dev_t from, unsigned count)
void unregister_chrdev_region(dev_t from, unsigned count)
{dev_t to = from + count;dev_t n, next;for (n = from; n < to; n = next) {next = MKDEV(MAJOR(n)+1, 0);if (next > to)next = to;kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));}
}static struct char_device_struct *
__unregister_chrdev_region(unsigned major, unsigned baseminor, int minorct)
{struct char_device_struct *cd = NULL, **cp;int i = major_to_index(major);// 上锁mutex_lock(&chrdevs_lock);// 在chrdevs链表中寻找匹配的字符设备结构体for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)if ((*cp)->major == major &&(*cp)->baseminor == baseminor &&(*cp)->minorct == minorct)break;// 如果找到了,就从链表中移除if (*cp) {cd = *cp;*cp = cd->next;}// 解锁mutex_unlock(&chrdevs_lock);return cd;
}

注销设备号的过程其实就是分配设备号的反过程,先对哈希表chrdevs上锁,然后寻找要注销的设备号对应的链表节点,然后将其从单链表中移除。

注意:设备号通过哈希表进行管理,cdev字符设备结构体也是通过哈希表进行管理。

创建设备文件

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

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

相关文章

C++ Primer Plus第十八章复习题

1、使用用大括号括起的初始化列表语法重写下述代码。重写后的代码不应使用数组ar。 class z200 { private:int j;char ch;double z; public:Z200(int jv,char chv&#xff0c;zv) : j(jv), ch (chv), z(zv){} };double x 8.8; std::string s "what a bracing effect ! …

深入了解数据库设计中的规范化与反规范化

目录 零、前言 一、一些基本术语 二、关系模式 2.1. 什么是关系模式 2.2. 示例 三、数据依赖 3.1. 函数依赖 3.1.1. 完全函数依赖 3.1.2. 部分函数依赖 3.1.3. 传递函数依赖 3.2. 多值依赖 3.3. 连接依赖 四、规范化 4.1. 第一范式&#xff08;1NF&#xff09; …

【Flutter】有状态组件StatefulWidgetScaffold组件属性

&#x1f525; 本文由 程序喵正在路上 原创&#xff0c;CSDN首发&#xff01; &#x1f496; 系列专栏&#xff1a;Flutter学习 &#x1f320; 首发时间&#xff1a;2024年5月26日 &#x1f98b; 欢迎关注&#x1f5b1;点赞&#x1f44d;收藏&#x1f31f;留言&#x1f43e; 目…

AI菜鸟向前飞 — LangChain系列之十四 - Agent系列:从现象看机制(上篇)

上一篇介绍了Agent与LangGraph的基础技能Tool的必知必会 AI菜鸟向前飞 — LangChain系列之十三 - 关于Tool的必知必会 前面已经详细介绍了Promp、RAG&#xff0c;终于来到Agent系列&#xff08;别急后面还有LangGraph&#xff09;&#xff0c;大家可以先看下这张图&#xff1…

leetcode328. 奇偶链表,附详细解析和代码注释

leetcode328. 奇偶链表 给定单链表的头节点 head &#xff0c;将所有索引为奇数的节点和索引为偶数的节点分别组合在一起&#xff0c;然后返回重新排序的列表。 第一个节点的索引被认为是 奇数 &#xff0c; 第二个节点的索引为 偶数 &#xff0c;以此类推。 请注意&#xff0…

轻量级 K8S 环境 安装minikube

文章目录 操作系统DockerDocker CE 镜像源站使用官方安装脚本自动安装 &#xff08;仅适用于公网环境&#xff09;安装校验Docker代理docker permission denied while trying to connect to the Docker daemon socket minikubekubectl工具minikube dashboard参考资料 操作系统 …

Docker进入容器查看内容并从容器里拷贝文件到宿主机

工作中需要从docker正在运行的镜像中复制文件到宿主机&#xff0c;于是便将这个过程记录了下来。 &#xff08;1&#xff09;查看正在运行的容器 通过以下命令&#xff0c;可以查看正在运行的容器&#xff1a; docker ps &#xff08;2&#xff09;进入某个容器执行脚本 我…

react中子传父信息

思路是&#xff1a; 在父组件定义一个函数接受参数&#xff0c;接收的参数用于接收子组件的信息&#xff0c;把函数传给子组件&#xff0c;子组件调用父亲传来的函数并把要告诉父亲的话传到函数中&#xff0c;就实现了子传父消息 import { useState } from reactimport { use…

OpenWrt 安装Quagga 支持ospf Bgp等动态路由协议 软路由实测 系列四

1 Quagga 是一个路由软件套件, 提供 OSPFv2,OSPFv3,RIP v1 和 v2,RIPng 和 BGP-4 的实现. 2 web 登录安装 #或者ssh登录安装 opkg install quagga quagga-zebra quagga-bgpd quagga-watchquagga quagga-vtysh # reboot 3 ssh 登录 #重启服务 /etc/init.d/quagga restart #…

使用kubesphere部署微服务的时候,节点的镜像不是最新的导致部署到旧版本问题

我使用kubesphere部署微服务的时候&#xff0c;发现有很多次&#xff0c;我修改了配置文件&#xff0c;但是部署完才发现部署的是旧版本。 然后我查看了该微服务部署在哪个节点上&#xff1a; kubectl get pods --all-namespaces -o wide例如 gulimall-gateway 这个服务&…

韭菜的自我总结

韭菜的自我总结 股市技术面量价关系左侧右侧右侧技术左侧技术洗盘 韭菜的自我修养虚拟货币的启示韭菜的买入时机韭菜的心理压力成为优秀玩家的关键 股市技术面 技术面分析可以作为买卖时机判定的工具&#xff0c;但是投资还是需要基本面的分析作为支撑。也就是基本面选股&…

langchain进阶一:特殊的chain,轻松实现对话,与数据库操作,抽取数据,以及基于本地知识库的问答

特殊的chain langchain中的Chain有很多,能够轻松实现部分需求,极致简化代码,但是实现效果与模型智慧程度有关 会话链 效果与LLMChain大致相同 javascript 复制代码 from langchain.chains import ConversationChain from langchain_community.llms import OpenAI conversat…

Redis 实战 - 缓存异常及解决方案

文章目录 概述一、缓存穿透1.1 缓存穿透是什么1.2 解决方案 二、缓存击穿2.1 缓存击穿是什么2.2 解决方案 三、缓存雪崩3.1 缓存雪崩是什么3.2 解决方案 四、拓展4.1 缓存预热4.2 缓存降级 五、结语 把今天最好的表现当作明天最新的起点……&#xff0e;&#xff5e; 概述 在实…

LeetCode 第131场双周赛个人题解

100309. 求出出现两次数字的 XOR 值 原题链接 求出出现两次数字的 XOR 值 - 力扣 (LeetCode) 竞赛 思路分析 签到题&#xff0c;一次遍历 AC代码 class Solution:def duplicateNumbersXOR(self, nums: List[int]) -> int:cnt Counter(nums)res 0st set(nums)for x …

【大模型】Spring AI对接ChatGpt使用详解

目录 一、前言 二、spring ai介绍 2.1 什么是Spring AI 2.2 Spring AI 特点 2.3 Spring AI 为开发带来的便利 2.4 Spring AI应用领域 2.4.1 聊天模型 2.4.2 文本到图像模型 2.4.3 音频转文本 2.4.4 嵌入大模型使用 2.4.5 矢量数据库支持 2.4.6 用于数据工程ETL框架 …

2024-05-22 VS2022使用modules

点击 <C 语言编程核心突破> 快速C语言入门 VS2022使用modules 前言一、准备二、使用其一, 用VS installer 安装模块:第二个选项就是, 与你的代码一同编译std模块, 这个非常简单, 但是也有坑. 总结 前言 要解决问题: 使用VS2022开启modules. 想到的思路: 跟着官方文档整…

Java进阶学习笔记19——内部类

1、 内部类&#xff1a; 是类中五大成分之一&#xff08;成员变量、方法、构造函数、内部类、代码块&#xff09;&#xff0c;如果一个类定义在另一个 类的内部&#xff0c;这个类就是内部类。 场景&#xff1a;当一个类的内部&#xff0c;包含了一个完整的事物&#xff0c;且…

Android ART 虚拟机简析

源码基于&#xff1a;Android U 1. prop 名称选项名称heap 变量名称功能 dalvik.vm.heapstartsize MemoryInitialSize initial_heap_size_ 虚拟机在启动时&#xff0c;向系统申请的起始内存 dalvik.vm.heapgrowthlimit HeapGrowthLimit growth_limit_ 应用可使用的 max…

Scikit-Learn朴素贝叶斯

Scikit-Learn朴素贝叶斯 1、朴素贝叶斯1.1、贝叶斯分类1.2、贝叶斯定理1.3、贝叶斯定理的推导1.4、朴素贝叶斯及原理1.5、朴素贝叶斯的优缺点2、Scikit-Learn朴素贝叶斯2.1、Sklearn中的贝叶斯分类器2.2、Scikit-Learn朴素贝叶斯API2.3、Scikit-Learn朴素贝叶斯实践(新闻分类与…

爬山算法的详细介绍

目录 &#x1f349;概述 &#x1f349; 步骤 &#x1f349; 优缺点 &#x1f348;优点 &#x1f348;缺点 &#x1f348;应对策略 &#x1f349;示例 &#x1f348;旅行商问题 &#x1f34d;步骤 &#x1f34d;分解代码 &#x1f34e;包含头文件 &#x1f34e;定义函…