device_node:解压设备树与生成内核设备节点树的流程概述

news2024/10/11 8:30:59

本专栏往期内容

总线:

  1. 驱动中的device和device_driver结构体-CSDN博客
  2. bus总线的相关结构体和注册逻辑-CSDN博客
  3. bus中设备驱动的probe触发逻辑和device、driver的添加逻辑-CSDN博客
  4. platform bus平台总线详解-CSDN博客

设备树:

  1. 设备树语法规则讲解-CSDN博客
  2. 基于设备树的嵌入式系统硬件平台识别与参数传递流程解析-CSDN博客

前言

本章主要内容:在 Linux 内核的初始化过程中,设备树二进制文件(DTB)被解压为设备节点树,用于内核设备驱动的管理。通过内核函数 __unflatten_device_tree,DTB 被解析成内核的 struct device_node 结构。这一流程分为两个主要步骤:首先通过扫描 DTB 来确定所需内存大小,接着分配内存并进行实际的解压操作,生成设备节点树,并保存在全局链表和树结构中。解压后的设备节点被存储为全局链表 of_allnodes,同时形成层次化的父子节点关系树,供内核使用。这一过程确保设备驱动能够正确识别和使用硬件设备的配置信息。

注:以下的代码皆摘自于linux 4.9.88版本的内核源码,不同版本可能有所出入。

1. struct device_node

struct device_node {
	const char *name; ----------------------device node name
	const char *type; -----------------------对应device_type的属性
	phandle phandle; -----------------------对应该节点的phandle属性
	const char *full_name;  ----------------从“/”开始的,表示该node的full path
	struct fwnode_handle fwnode; ----------------这个成员用于支持更通用的设备模型,可以与 fwnode 结构结合,以支持非设备树的设备节点。这种设计使得内核能够灵活地处理不同类型的设备描述信息,不仅仅局限于设备树。

	struct	property *properties; -------------该节点的属性列表
	struct	property *deadprops; ----------如果需要删除某些属性,kernel并非真的删除,而是挂入到deadprops的列表
	struct	device_node *parent;
	struct	device_node *child;
	struct	device_node *sibling; ------parent、child以及sibling将所有的device node连接起来
	struct	kobject kobj;------kobject 是内核对象的基础结构,提供了一些通用功能,如名称、引用计数和 sysfs 接口等。每个设备节点都可以通过 kobj 与内核的对象模型集成,使得设备节点可以通过 sysfs 进行用户空间访问。
	unsigned long _flags; ------这个成员用于存储设备节点的状态标志。可以包含各种与设备节点状态相关的信息,如是否已经被标记为“已分离”(detached),或是否已注册等。
	void	*data;  ------这个指针可以用于存储与设备节点相关的特定数据。由于设备树本身并不定义任何具体数据结构,这为开发人员提供了灵活性,可以根据需要在此存储额外信息。
#if defined(CONFIG_SPARC)
	const char *path_component_name;
	unsigned int unique_id; ------用于为设备节点分配一个唯一的标识符,以帮助识别和管理不同设备节点。
	struct of_irq_controller *irq_trans; ------这个指针用于表示与中断控制器相关的设备节点信息。中断处理在硬件设备中至关重要,而这个成员用于帮助管理设备节点与中断之间的关系。
#endif
};

设备树最后就抽象成这样一个结构体,是不是觉得在哪里见过,就是在 平台总线驱动模型 中的struct device的成员之一

  1. struct fwnode_handle fwnode;
    • 这个成员用于支持更通用的设备模型,可以与 fwnode 结构结合,以支持非设备树的设备节点。这种设计使得内核能够灵活地处理不同类型的设备描述信息,不仅仅局限于设备树。
  2. struct property *properties;
    • 这个指针指向当前设备节点的属性列表。每个设备节点可以有多个属性,每个属性包含一个键值对,用于描述设备的特征。例如,一个设备节点可能有“compatible”属性来标识设备的兼容性,或“reg”属性来描述设备的寄存器地址。
  3. struct property *deadprops;
    • 这个指针指向已标记为删除的属性列表。当需要删除某个属性时,内核并不会立即从内存中释放它,而是将其移入 deadprops 列表。这样做的好处是避免频繁的内存分配和释放,提升性能。在某些情况下,可能会需要恢复这些被删除的属性。
  4. struct device_node *parent;
    • 这个指针指向当前设备节点的父节点。设备节点形成一棵树结构,每个节点可能有一个父节点,这有助于管理节点之间的层级关系。
  5. struct device_node *child;
    • 这个指针指向当前节点的第一个子节点。设备树是一个树状结构,每个节点可以有多个子节点,而这个指针帮助快速访问第一个子节点。
  6. struct device_node *sibling;
    • 这个指针指向当前节点的下一个兄弟节点。通过这个指针,可以在同级别的设备节点之间进行遍历。
  7. struct kobject kobj;
    • kobject 是内核对象的基础结构,提供了一些通用功能,如名称、引用计数和 sysfs 接口等。每个设备节点都可以通过 kobj 与内核的对象模型集成,使得设备节点可以通过 sysfs 进行用户空间访问。
  8. unsigned long _flags;
    • 这个成员用于存储设备节点的状态标志。可以包含各种与设备节点状态相关的信息,如是否已经被标记为“已分离”(detached),或是否已注册等。
  9. void *data;
    • 这个指针可以用于存储与设备节点相关的特定数据。由于设备树本身并不定义任何具体数据结构,这为开发人员提供了灵活性,可以根据需要在此存储额外信息。

条件编译部分

  1. #if defined(CONFIG_SPARC)
    • 这个条件编译部分意味着以下成员仅在特定架构(如 SPARC)下可用。具体成员如下:
    • const char *path_component_name;
      • 该成员表示路径组件的名称,在某些架构中可能需要。
    • unsigned int unique_id;
      • 用于为设备节点分配一个唯一的标识符,以帮助识别和管理不同设备节点。
    • struct of_irq_controller *irq_trans;
      • 这个指针用于表示与中断控制器相关的设备节点信息。中断处理在硬件设备中至关重要,而这个成员用于帮助管理设备节点与中断之间的关系。

2. 初始化流程:device_node的提取

在系统初始化的过程中,会将DTB转换成节点是device_node的树状结构。具体的代码位于setup_arch的unflatten_device_tree中。

// Linux-4.9.88\arch\arm\kernel\setup.c
void __init setup_arch(char **cmdline_p)
{
    .....
    unflatten_device_tree();  // \Linux-4.9.88\drivers\of\fdt.C,继续往下看
    .....
}

//--------------------------------分割线----------------------------------------
/**
 * unflatten_device_tree - create tree of device_nodes from flat blob
 *
 * unflattens the device-tree passed by the firmware, creating the
 * tree of struct device_node. It also fills the "name" and "type"
 * pointers of the nodes so the normal device-tree walking functions
 * can be used.
 */
void __init unflatten_device_tree(void)
{
	__unflatten_device_tree(initial_boot_params, NULL, &of_root,
				early_init_dt_alloc_memory_arch, false); //继续往下看

	/* Get pointer to "/chosen" and "/aliases" nodes for use everywhere */
	of_alias_scan(early_init_dt_alloc_memory_arch);
}

//--------------------------------分割线----------------------------------------
/**
 * __unflatten_device_tree - create tree of device_nodes from flat blob
 *
 * unflattens a device-tree, creating the
 * tree of struct device_node. It also fills the "name" and "type"
 * pointers of the nodes so the normal device-tree walking functions
 * can be used.
 * @blob: The blob to expand
 * @dad: Parent device node
 * @mynodes: The device_node tree created by the call
 * @dt_alloc: An allocator that provides a virtual address to memory
 * for the resulting tree
 *
 * Returns NULL on failure or the memory chunk containing the unflattened
 * device tree on success.
 */
static void *__unflatten_device_tree(const void *blob,
				     struct device_node *dad,
				     struct device_node **mynodes,
				     void *(*dt_alloc)(u64 size, u64 align),
				     bool detached)
{
	int size;
	void *mem;

	pr_debug(" -> unflatten_device_tree()\n");

	if (!blob) {
		pr_debug("No device tree pointer\n");
		return NULL;
	}

	pr_debug("Unflattening device tree:\n");
	pr_debug("magic: %08x\n", fdt_magic(blob));
	pr_debug("size: %08x\n", fdt_totalsize(blob));
	pr_debug("version: %08x\n", fdt_version(blob));

	if (fdt_check_header(blob)) {
		pr_err("Invalid device tree blob header\n");
		return NULL;
	}

	/* First pass, scan for size */
	size = unflatten_dt_nodes(blob, NULL, dad, NULL);
	if (size < 0)
		return NULL;

	size = ALIGN(size, 4);
	pr_debug("  size is %d, allocating...\n", size);

	/* Allocate memory for the expanded device tree */
	mem = dt_alloc(size + 4, __alignof__(struct device_node));
	if (!mem)
		return NULL;

	memset(mem, 0, size);

	*(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef);

	pr_debug("  unflattening %p...\n", mem);

	/* Second pass, do actual unflattening */
	unflatten_dt_nodes(blob, mem, dad, mynodes);
	if (be32_to_cpup(mem + size) != 0xdeadbeef)
		pr_warning("End of tree marker overwritten: %08x\n",
			   be32_to_cpup(mem + size));

	if (detached && mynodes) {
		of_node_set_flag(*mynodes, OF_DETACHED);
		pr_debug("unflattened tree is detached\n");
	}

	pr_debug(" <- unflatten_device_tree()\n");
	return mem;
}
  

内核函数 __unflatten_device_tree 主要用于将设备树二进制文件(DTB)“解压”成内核使用的设备节点结构,并将其组织成设备树的结构。具体来说,它完成了以下几个重要步骤:

函数功能概述

  1. 解析并校验 DTB 文件头部:检查设备树的有效性,确保提供的设备树格式正确。
  2. 分配内存:根据设备树中的结构,确定需要多少内存用于保存所有设备节点和属性,并一次性分配足够的内存。
  3. 解压设备树:扫描 DTB,实际将设备节点和属性解析出来,并生成内核中使用的 struct device_node 结构,形成一个内核中的设备树。
  4. 标记分离(detached)状态:如果设备树被标记为“分离”,则设置相应的标志。

详细代码分析

Linux-4.9.88\Linux-4.9.88\drivers\of\fdt.c:
static void *__unflatten_device_tree(const void *blob,
                     struct device_node *dad,
                     struct device_node **mynodes,
                     void *(*dt_alloc)(u64 size, u64 align),
                     bool detached)
{
    int size;
    void *mem;

    pr_debug(" -> unflatten_device_tree()\n");

    if (!blob) {
        pr_debug("No device tree pointer\n");
        return NULL;
    }
  • 参数说明
    • blob:指向设备树二进制文件(DTB)的指针。
    • dad:父节点的指针,即要解压出的设备树的根节点。可以为空。
    • mynodes:指向解压后设备节点的全局链表。
    • dt_alloc:内存分配函数,用于为设备节点和属性分配内存。
    • detached:如果为 true,则表示设备树是“分离”的,不会直接与内核的其余部分关联。
  • 第一步:函数首先检查 blob 是否为空,如果为空则返回,因为这表示没有设备树可供处理。
    pr_debug("Unflattening device tree:\n");
    pr_debug("magic: %08x\n", fdt_magic(blob));
    pr_debug("size: %08x\n", fdt_totalsize(blob));
    pr_debug("version: %08x\n", fdt_version(blob));

    if (fdt_check_header(blob)) {
        pr_err("Invalid device tree blob header\n");
        return NULL;
    }
  • 第二步:通过 fdt_magic(blob) 等函数输出设备树头部信息,如魔数(magic)、大小(size)和版本(version)。
    • fdt_check_header(blob):检查设备树头部是否有效。如果无效,函数返回 NULL,表示设备树解析失败。
    /* First pass, scan for size */
    size = unflatten_dt_nodes(blob, NULL, dad, NULL);
    if (size < 0)
        return NULL;
  • 第三步:调用 unflatten_dt_nodes 扫描设备树。这是第一遍扫描,主要目的是计算出完整解压设备树所需的内存大小,存储在 size 变量中。
    • 如果 size 小于 0,表示扫描失败,则直接返回 NULL
    size = ALIGN(size, 4);
    pr_debug("  size is %d, allocating...\n", size);

    /* Allocate memory for the expanded device tree */
    mem = dt_alloc(size + 4, __alignof__(struct device_node));
    if (!mem)
        return NULL;

    memset(mem, 0, size);

    *(__be32 *)(mem + size) = cpu_to_be32(0xdeadbeef);
  • 第四步:将计算出的 size 值对齐到 4 字节(通常是为了保证内存对齐,提高访问效率)。
    • 接着调用 dt_alloc 分配内存,大小为 size + 4 字节。加上4字节是为了在内存末尾设置一个标记(0xdeadbeef),用于后面校验是否发生内存溢出。
    • memset(mem, 0, size) 将分配的内存初始化为 0。
    pr_debug("  unflattening %p...\n", mem);

    /* Second pass, do actual unflattening */
    unflatten_dt_nodes(blob, mem, dad, mynodes);
    if (be32_to_cpup(mem + size) != 0xdeadbeef)
        pr_warning("End of tree marker overwritten: %08x\n",
               be32_to_cpup(mem + size));
unflatten_dt_nodes具体如下:
// Linux-4.9.88\Linux-4.9.88\drivers\of\fdt.c:
static int unflatten_dt_nodes(const void *blob,
			      void *mem,
			      struct device_node *dad,
			      struct device_node **nodepp)
{
	struct device_node *root;
	int offset = 0, depth = 0, initial_depth = 0;
#define FDT_MAX_DEPTH	64
	unsigned int fpsizes[FDT_MAX_DEPTH];
	struct device_node *nps[FDT_MAX_DEPTH];
	void *base = mem;
	bool dryrun = !base;

	if (nodepp)
		*nodepp = NULL;

	/*
	 * We're unflattening device sub-tree if @dad is valid. There are
	 * possibly multiple nodes in the first level of depth. We need
	 * set @depth to 1 to make fdt_next_node() happy as it bails
	 * immediately when negative @depth is found. Otherwise, the device
	 * nodes except the first one won't be unflattened successfully.
	 */
	if (dad)
		depth = initial_depth = 1;

	root = dad;
	fpsizes[depth] = dad ? strlen(of_node_full_name(dad)) : 0;
	nps[depth] = dad;

	for (offset = 0;
	     offset >= 0 && depth >= initial_depth;
	     offset = fdt_next_node(blob, offset, &depth)) {
		if (WARN_ON_ONCE(depth >= FDT_MAX_DEPTH))
			continue;

		fpsizes[depth+1] = populate_node(blob, offset, &mem,
						 nps[depth],
						 fpsizes[depth],
						 &nps[depth+1], dryrun);
		if (!fpsizes[depth+1])
			return mem - base;

		if (!dryrun && nodepp && !*nodepp)
			*nodepp = nps[depth+1];
		if (!dryrun && !root)
			root = nps[depth+1];
	}

	if (offset < 0 && offset != -FDT_ERR_NOTFOUND) {
		pr_err("Error %d processing FDT\n", offset);
		return -EINVAL;
	}

	/*
	 * Reverse the child list. Some drivers assumes node order matches .dts
	 * node order
	 */
	if (!dryrun)
		reverse_nodes(root);

	return mem - base;
}
  • 第五步:输出调试信息,显示分配内存的地址。
    • 然后进行第二次扫描,这次调用 unflatten_dt_nodes 实际解析设备树,并将其解压到刚刚分配的内存中。这个函数会填充 **struct device_node** 结构,构建完整的设备树
    • 解压完成后,检查之前设置的 0xdeadbeef 标记,如果被覆盖,表示在解析过程中发生了内存溢出,输出警告信息。
    if (detached && mynodes) {
        of_node_set_flag(*mynodes, OF_DETACHED);
        pr_debug("unflattened tree is detached\n");
    }

    pr_debug(" <- unflatten_device_tree()\n");
    return mem;
}
  • 第六步:如果 detached 标志为真,并且 mynodes 指针有效,则设置设备树的 “detached” 标志,表示这棵设备树与内核的其余部分没有直接关联(分离状态)。
    • 最后,函数返回分配的内存地址,完成设备树的解压。

运行逻辑

  1. 设备树验证:函数首先检查设备树的头部,确保 blob 有效。
  2. 计算内存需求:第一次扫描设备树,确定解压后所需的内存大小。
  3. 内存分配:为整个设备树分配足够的内存,一次性为所有节点、属性等结构分配空间。
  4. 设备树解压:第二次扫描设备树,解析每个节点和属性,生成内核使用的设备树结构,并保存在分配的内存中。
  5. 溢出检查:通过末尾的 0xdeadbeef 标记检查是否发生了内存溢出。
  6. 分离状态:如果设备树被标记为分离,设置相应标志。

通过这两个扫描,设备树的二进制数据被转换为内核可以使用的设备节点树,同时保存到全局链表中,供内核的设备驱动子系统使用。

unflatten_device_tree函数的主要功能就是扫描DTB,将device node被组织成:

1、global list。全局变量struct device_node *of_allnodes就是指向设备树的global list

2、tree。

这些功能主要是在__unflatten_device_tree函数中实现

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

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

相关文章

利用网络流量分析仪进行网络故障排除:提升IT运维效率的关键工具

目录 一、什么是网络流量分析仪&#xff1f; 主要功能&#xff1a; 二、为什么网络流量分析仪在网络故障排除中如此重要&#xff1f; 三、实际案例&#xff1a;使用网络流量分析仪快速排查网络故障 案例一&#xff1a;流量拥塞导致的带宽不足 案例二&#xff1a;服务器响…

element-ui的树形结构样式调整,添加线条和边框样式

element-ui的树形结构样式调整&#xff0c;添加线条和边框样式 先看图效果&#xff1a; <template><div class"temperature_monitoring"><div class"temperature_monitoring_left"><div class"tree-container"><e…

萤石云 ezuikit-js 视频监控

父组件 <template><div class"securityProtectLargeScreen" v-if"waterWorks?.length > 0"><div class"leftSide"><ul class"leftItems flexColumnCenter"><liv-for"(item, index) in waterWork…

Java | Leetcode Java题解之第470题用Rand7()实现Rand10()

题目&#xff1a; 题解&#xff1a; class Solution extends SolBase {public int rand10() {int a, b, idx;while (true) {a rand7();b rand7();idx b (a - 1) * 7;if (idx < 40) {return 1 (idx - 1) % 10;}a idx - 40;b rand7();// get uniform dist from 1 - 63…

安卓13禁止用户打开开发者选项 android13禁止用户打开开发者选项

总纲 android13 rom 开发总纲说明 文章目录 1.前言2.问题分析3.代码分析4.代码修改5.编译6.彩蛋1.前言 设置 =》关于平板电脑 =》版本号,一般的话,在这里连续点击就可以打开我们的开发者选项了。但是有些系统要进行保密,因此要禁止用户进入。 2.问题分析 这里我们是通过点…

FastAPI框架使用枚举来型来限定参数、FastApi框架隐藏没多大意义的Schemes模型部分内容以及常见的WSGI服务器Gunicorn、uWSGI了解

一、FastAPI框架使用枚举来型来限定参数 FastAPI框架验证时&#xff0c;有时需要通过枚举的方式来限定参数只能为某几个值中的一个&#xff0c;这时就可以使用FastAPI框架的枚举类型Enum了。publish:December 23, 2020 -Wednesday 代码如下&#xff1a; #引入Enum模块 from fa…

【自注意力与Transformer架构在自然语言处理中的演变与应用】

背景介绍 在自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;序列到序列&#xff08;seq2seq&#xff09;模型和Transformer架构的出现&#xff0c;极大地推动了机器翻译、文本生成和其他语言任务的进展。传统的seq2seq模型通常依赖于循环神经网络&#xff08;RNN&…

【idea】切换多个仓库到一个分支

需求描述 打开个一个Project 里面包含多个子项目&#xff0c;每一个子项目都有一个自己的git仓库。在idea 中有没有一次性把多个项目切换到同一个分支上面。 解决方案 右键git -> branch 点击右上角的此轮 勾选Execute Branch Operations on All Roots 点击ommon Remote …

萱仔求职复习系列——2 Linux的常用方法(包含基础进阶高级操作)

由于最近接了一个笔试&#xff0c;发现笔试可能涉及到Linux&#xff0c;我准备临时抱佛脚一下赶紧复习一下Linux的用法哈哈。Linux 的基础用法包含文件系统操作、权限管理、网络配置、进程管理等基本命令&#xff1b;进阶操作包括网络调试、包管理、服务管理和用户管理等&#…

【jdk19虚拟线程 VS 普通线程】

文章目录 一.什么是虚拟线程二.虚拟线程与普通线程的区别1.普通线程2.虚拟线程3. 实际应用中的区别 三.上demo对比性能。1.线程池配置2.Service实现3.测试结果 四.小结 一.什么是虚拟线程 虚拟线程&#xff0c;也称作轻量级线程&#xff0c;是由JVM直接管理的线程类型&#xf…

jmeter入门:脚本录制

1.设置代理。 网络连接-》代理-》手动设置代理. ip&#xff1a; 127.0.0.1&#xff0c; port&#xff1a;8888 2. add thread group 3. add HTTP(s) test script recorder, target controller chooses Test plan-> thread Group 4. click start. then open the browser …

Golang | Leetcode Golang题解之第467题环绕字符串中唯一的子字符串

题目&#xff1a; 题解&#xff1a; func findSubstringInWraproundString(p string) (ans int) {dp : [26]int{}k : 0for i, ch : range p {if i > 0 && (byte(ch)-p[i-1]26)%26 1 { // 字符之差为 1 或 -25k} else {k 1}dp[ch-a] max(dp[ch-a], k)}for _, v :…

Java主流框架项目实战——SpringBoot入门

单元1-1 1&#xff09; IDEA工具安装好 2&#xff09; Maven安装&#xff0c;配置好 IDEA安装及永久试用 配置maven 单元1-2 使用aliyun(https://start.aliyun.com/)创建一个spring boot项目&#xff0c;hello world&#xff01; 构建项目 1&#xff09;构建项目 直接默认…

MicroFlow:一种高效的基于Rust的TinyML推理引擎

英文论文标题&#xff1a;MICROFLOW: AN EFFICIENT RUST-BASED INFERENCE ENGINE FOR TINYML 中文论文标题&#xff1a;MicroFlow&#xff1a;一种高效的基于Rust的TinyML推理引擎 作者信息&#xff1a; Matteo Carnelos&#xff0c;意大利帕多瓦大学&#xff0c;Grepit AB,…

什么软件可以晚上睡觉录音

什么软件可以晚上睡觉录音&#xff0c;在日常生活中&#xff0c;我们常常忽略夜间的声音&#xff0c;然而这些声音有时可能会揭示重要信息&#xff0c;比如打鼾情况、说梦话、甚至是潜在的睡眠问题。因此&#xff0c;一款适合夜间录音的软件对于关注健康及生活细节的人来说至关…

在IDEA中配置Selenium和WebDriver

前言&#xff1a; 在当今自动化测试和网络爬虫的领域&#xff0c;Selenium是一个被广泛使用的工具。它不仅能够模拟用户与浏览器的交互&#xff0c;还能进行网页测试和数据抓取。而为了使用Selenium与谷歌/Edge浏览器进行自动化测试&#xff0c;配置合适的WebDriver至关重要。本…

【时时三省】(C语言基础)指针笔试题8

山不在高&#xff0c;有仙则名。水不在深&#xff0c;有龙则灵。 ----CSDN 时时三省 笔试题8 c是个数组 它的每个元素是char* 它初始化了四个字符串 把这四个字符串的首字符的地址 传到了c里面 cp有四个元素 每个元素的类型是char** 所以c3指向FORST c2指向POINT c1指向NE…

数学建模算法与应用 第9章 支持向量机及其方法

目录 9.1 支持向量机的基本原理 核函数的种类&#xff1a; 9.2 支持向量机的Matlab命令及应用 Matlab代码示例&#xff1a;二分类支持向量机 9.3 乳腺癌的诊断案例 Matlab代码示例&#xff1a;乳腺癌数据分类 9.4 支持向量回归&#xff08;SVR&#xff09; Matlab代码示…

uibot发送邮件:自动化邮件发送教程详解!

uibot发送邮件的操作指南&#xff1f;uibot发送邮件的两种方式&#xff1f; 在现代办公环境中&#xff0c;自动化流程的引入极大地提高了工作效率。uibot发送邮件功能成为了许多企业和个人实现邮件自动化发送的首选工具。AokSend将详细介绍如何使用uibot发送邮件。 uibot发送…

【AIGC】寻找ChatGPT最佳推理步骤:CoT思维链技术的探索与应用

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AIGC | ChatGPT 文章目录 &#x1f4af;前言&#x1f4af;CoT思维链概述&#x1f4af;CoT思维链在大型语言模型中的应用&#x1f4af;CoT思维链改变对模型推理能力的理解和改进方式多样化应用场景挑战与未来发展总结 &#x1f4a…