首页 > 其他分享 >FreeRTOS教程6 互斥量

FreeRTOS教程6 互斥量

时间:2024-03-18 09:03:13浏览次数:27  
标签:教程 Task 优先级 FreeRTOS 信号量 互斥 任务 Low

1、准备材料

正点原子stm32f407探索者开发板V2.4

STM32CubeMX软件(Version 6.10.0

Keil µVision5 IDE(MDK-Arm

野火DAP仿真器

XCOM V2.6串口助手

2、学习目标

本文主要学习 FreeRTOS 互斥量的相关知识,包括优先级翻转问题、优先级继承、死锁现象、创建/删除互斥量 和 获取/释放互斥量等知识

3、前提知识

3.1、优先级翻转问题

使用二值信号量用于进程间同步时可能会出现优先级翻转的问题,什么是“优先级翻转”问题呢?考虑如下所述的任务运行过程

  • 在 t1 时刻,低优先级的任务 TaskLP 切入运行状态,并且获取到了一个二值信号量 Binary Semaphores
  • 在 t2 时刻,高优先级的任务 TaskHP 请求获取二值信号量 Binary Semaphores ,但是由于 TaskLP 还未释放该二值信号量,所以在 t3 时刻,任务 TaskHP 进入阻塞状态等待二值信号量被释放
  • 在 t4 时刻,中等优先级的任务 TaskMP 进入就绪状态,由于不需要获取二值信号量,因此抢占低优先级任务任务 TaskLP 切入运行状态
  • 在 t5 时刻,任务 TaskMP 运行结束,任务 TaskLP 再次切入运行状态
  • 在 t6 时刻,任务 TaskLP 运行结束,释放二值信号量 Binary Semaphores,此时任务 TaskHP 从等待二值信号量的阻塞状态切入运行状态
  • 在t7时刻,任务 TaskHP 运行结束

根据上述流程读者可以发现一个问题,即在 t4 时刻中等优先级的任务 TaskMP 先于高优先级的任务 TaskHP 抢占了处理器,这破坏了 FreeRTOS 基于优先级抢占式执行的原则,我们将这种情况称为优先级翻转问题,上述描述的任务运行过程具体时刻流程图如下图所示

优先级翻转可能是一个严重的问题,但在小型嵌入式系统中,通常可以在系统设计时通过考虑如何访问资源来避免该问题

3.2、优先级继承

为了解决使用二值信号量可能会出现的优先级翻转问题,对二值信号量做了改进,增加了一种名为 “优先级继承” 的机制,改进后的实例称为了互斥量,注意虽然互斥量可以减缓优先级翻转问题的出现,但是并不能完全杜绝

接下来我们来通过例子介绍什么是优先级继承?

仍然考虑由 “3.1、优先级翻转问题” 小节中提出的任务运行过程的例子,具体流程如下所述,读者可以细心理解其中的不同之处

  • 在 t1 时刻,低优先级的任务 TaskLP 切入运行状态,并且获取到了一个互斥量 Mutexes
  • 在 t2 时刻,高优先级的任务 TaskHP 请求获取互斥量 Mutexes ,但是由于 TaskLP 还未释放该互斥量,所以在 t3 时刻,任务 TaskHP 进入阻塞状态等待互斥量被释放,但是与二值信号量不同的是,此时 FreeRTOS 将任务 TaskLP 的优先级临时提高到与任务 TaskHP 一致的优先级,也即高优先级
  • 在 t4 时刻,中等优先级的任务 TaskMP 进入就绪状态发生任务调度,但是由于任务 TaskLP 此时优先级被提高到了高优先级,因此任务 TaskMP 仍然保持就绪状态等待优先级较高的任务执行完毕
  • 在 t5 时刻,任务 TaskLP 执行完毕释放互斥量 Mutexes,此时任务 TaskHP 抢占处理器切入运行状态,并恢复任务 TaskLP 原来的优先级
  • 在 t6 时刻,任务 TaskHP 执行完毕,此时轮到任务 TaskMP 执行
  • 在 t7 时刻,任务 TaskMP 运行结束

根据互斥量的上述任务流程读者可以发现与二值信号量的不同之处,上述描述的任务运行过程具体时刻流程图如下图所示

3.3、什么是互斥量?

互斥量/互斥锁是一种特殊类型的二进制信号量,用于控制对在两个或多个任务之间共享资源的访问

互斥锁可以被视为一个与正在共享的资源相关联的令牌,对于合法访问资源的任务,它必须首先成功 “获取” 令牌,成为资源的持有者,当持有者完成对资源的访问之后,其需要 ”归还” 令牌,只有 “归还” 令牌之后,该令牌才可以再次被其他任务所 “获取” ,这样保证了互斥的对共享资源的访问,上述机制如下图所示 (注释1)

3.4、死锁现象

“死锁” 是使用互斥锁进行互斥的另一个潜在陷阱,当两个任务因为都在等待对方占用的资源而无法继续进行时,就会发生死锁,考虑如下所述的情况

  1. 任务 A 执行并成功获取互斥量 X
  2. 任务 A 被任务 B 抢占
  3. 任务 B 在尝试获取互斥量 X 之前成功获取互斥量 Y,但互斥量 X 由任务 A 持有,因此对任务 B 不可用,任务 B 选择进入阻塞状态等待互斥量 X 被释放
  4. 任务 A 继续执行,它尝试获取互斥量 Y,但互斥量 Y 由任务 B 持有,所以对于任务 A 来说是不可用的,任务 A 选择进入阻塞状态等待待释放的互斥量 Y

经过上述的这样一个过程,读者可以发现任务 A 在等待任务 B 释放互斥量 Y ,而任务 B 在等待任务 A 释放互斥量 X ,两个任务都在阻塞状态无法执行,从而导致 ”死锁“ 现象的发生,与优先级翻转一样,避免 “死锁” 的最佳方法是在设计时考虑其潜在影响,并设计系统以确保不会发生死锁

3.5、什么是递归互斥量?

任务也有可能与自身发生死锁,如果任务尝试多次获取相同的互斥体而不首先返回互斥体,就会发生这种情况,考虑以下设想:

  1. 任务成功获取互斥锁
  2. 在持有互斥体的同时,任务调用库函数
  3. 库函数的实现尝试获取相同的互斥锁,并进入阻塞状态等待互斥锁变得可用

在此场景结束时,任务处于阻塞状态以等待互斥体返回,但任务已经是互斥体持有者。 由于任务处于阻塞状态等待自身,因此发生了死锁

通过使用递归互斥体代替标准互斥体可以避免这种类型的死锁,同一任务可以多次 “获取” 递归互斥锁,并且只有在每次 “获取” 递归互斥锁之后都调用一次 “释放” 递归互斥锁,才会返回该互斥锁

因此递归互斥量可以视为特殊的互斥量,一个互斥量被一个任务获取之后就不能再次获取,其他任务想要获取该互斥量必须等待这个任务释放该互斥连,但是递归互斥量可以被一个任务重复获取多次,当然每次获取必须与一次释放配对使用

注意不管是互斥量,还是递归互斥量均存在优先级继承机制,但是由于 ISR 并不是任务,因此互斥量和递归互斥量不能在中断中使用

3.5、创建互斥量

互斥量在使用之前必须先创建,因为互斥量分为互斥量和递归互斥量两种,所以 FreeRTOS 也提供了不同的 API 函数,具体如下所述

/**
  * @brief  动态分配内存创建互斥信号量函数
  * @retval 创建互斥信号量的句柄
  */
SemaphoreHandle_t xSemaphoreCreateMutex(void);

/**
  * @brief  静态分配内存创建互斥信号量函数
  * @param  pxMutexBuffer:指向StaticSemaphore_t类型的变量,该变量将用于保存互斥锁型信号量的状态
  * @retval 返回成功创建后的互斥锁的句柄,如果返回NULL则表示内存不足创建失败
  */
SemaphoreHandle_t xSemaphoreCreateMutexStatic(StaticSemaphore_t *pxMutexBuffer);

/**
  * @brief  动态分配内存创建递归互斥信号量函数
  * @retval 创建递归互斥信号量的句柄,如果返回NULL则表示内存不足创建失败
  */
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex(void);

/**
  * @brief  动态分配内存创建二值信号量函数
  * @param  pxMutexBuffer:指向StaticSemaphore_t类型的变量,该变量将用于保存互斥锁型信号量的状态
  */
SemaphoreHandle_t xSemaphoreCreateRecursiveMutex(
								StaticSemaphore_t pxMutexBuffer);

3.6、获取互斥量

获取互斥量直接使用获取信号量的函数即可,但对于递归互斥量需要专门的获取函数,具体如下所述

/**
  * @brief  获取信号量函数
  * @param  xSemaphore:正在获取的信号量的句柄
  * @param  xTicksToWait:等待信号量变为可用的时间
  * @retval 成功获取信号量则返回pdTRUE, xTicksToWait过期,信号量不可用,则返回pdFALSE
  */
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);

/**
  * @brief  获取递归互斥量
  * @param  xMutex:正在获得的互斥锁的句柄
  * @param  xTicksToWait:等待信号量变为可用的时间
  * @retval 成功获取信号量则返回pdTRUE, xTicksToWait过期,信号量不可用,则返回pdFALSE
  */
BaseType_t xSemaphoreTakeRecursive(SemaphoreHandle_t xMutex,
								   TickType_t xTicksToWait);

3.7、释放互斥量

释放互斥量直接使用释放信号量的函数即可,但对于递归互斥量需要专门的释放函数,具体如下所述

/**
  * @brief  释放信号量函数
  * @param  xSemaphore:要释放的信号量的句柄
  * @retval 成功释放信号量则返回pdTRUE, 若发生错误,则返回pdFALSE
  */
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);

/**
  * @brief  释放递归互斥量
  * @param  xMutex:正在释放或“给出”的互斥锁的句柄
  * @retval 成功释放递归互斥量后返回pdTRUE
  */
BaseType_t xSemaphoreGiveRecursive(SemaphoreHandle_t xMutex);

3.8、删除互斥量

直接使用信号量的删除函数即可,具体如下所述

/**
  * @brief  获取信号量函数
  * @param  xSemaphore:要删除的信号量的句柄
  * @retval None
  */
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);

4、实验一:优先级翻转问题

4.1、实验目标

既然实验是讨论优先级翻转问题,那么我们来复现 “3.1、优先级翻转问题” 小节中所描述到的任务运行过程,具体如下所述

  1. 创建一个二值信号量 BinarySem_PI,用于演示优先级翻转问题
  2. 创建一个低优先级任务 Task_Low ,在该任务中获取二值信号量 BinarySem_PI ,并通过延时模拟长时间连续运行,运行结束后释放该二值信号量,整个过程会通过串口输出提示信息
  3. 创建一个中等优先级任务 Task_Middle,该任务负责在 Task_Low 模拟长时间连续运行期间抢占其处理器控制权限
  4. 创建一个高优先级任务 Task_High,该任务总是尝试获取二值信号量 BinarySem_PI

4.2、CubeMX相关配置

首先读者应按照"FreeRTOS教程1 基础知识"章节配置一个可以正常编译通过的 FreeRTOS 空工程,然后在此空工程的基础上增加本实验所提出的要求

本实验需要初始化 USART1 作为输出信息渠道,具体配置步骤请阅读“STM32CubeMX教程9 USART/UART 异步通信”,如下图所示

单击 Middleware and Software Packs/FREERTOS,在 Configuration 中单击 Tasks and Queues 选项卡双击默认任务修改其参数,然后增加另外两个不同优先级的任务,具体如下图所示

然后在 Configuration 中单击 Timers and Semaphores ,在 Binary Semaphores 中单击 Add 按钮新增加一个名为 BinarySem_PI 的二值信号量,具体如下图所示

配置 Clock Configuration 和 Project Manager 两个页面,接下来直接单击 GENERATE CODE 按钮生成工程代码即可

4.3、添加其他必要代码

按照 “STM32CubeMX教程9 USART/UART 异步通信” 实验 “6、串口printf重定向” 小节增加串口 printf 重定向代码,具体不再赘述

首先应该在 freertos.c 中添加信号量相关 API 和 printf() 函数的头文件,如下所述

/*freertos.c中添加头文件*/
#include "semphr.h"
#include "stdio.h"

然后在该文件中实现三个不同优先级的任务,主要是一些串口输出给用户的提示信息,方便演示实验目的,具体如下所述

/*低优先级任务*/
void AppTask_Low(void *argument)
{
  /* USER CODE BEGIN AppTask_Low */
  /* Infinite loop */
	uint8_t str1[]="Task_Low take it\r\n";
	uint8_t str2[]="Task_Low give it\r\n";
	uint8_t str3[]="return Task_Low\r\n";
	for(;;)
	{
		//获取信号量
		if(xSemaphoreTake(BinarySem_PIHandle, pdMS_TO_TICKS(200))==pdTRUE)  
		{
			printf("%s",str1);
			//模拟任务连续运行
			HAL_Delay(500);		
			printf("%s",str3);
			HAL_Delay(500);
			printf("%s",str2);
			//释放信号量
			xSemaphoreGive(BinarySem_PIHandle);		
		}
	}
  /* USER CODE END AppTask_Low */
}

/*中等优先级任务*/
void AppTask_Middle(void *argument)
{
  /* USER CODE BEGIN AppTask_Middle */
  /* Infinite loop */
	uint8_t strMid[]="Task_Middle is running\r\n";
	for(;;)
	{
		printf("%s", strMid);
		vTaskDelay(500);
	}
  /* USER CODE END AppTask_Middle */
}

/*高优先级任务*/
void AppTask_High(void *argument)
{
  /* USER CODE BEGIN AppTask_High */
  /* Infinite loop */
	uint8_t strHigh1[]="Into Task_High\r\n";
	uint8_t strHigh2[]="Task_High get token\r\n";
	uint8_t strHigh3[]="Task_High give token\r\n";
	for(;;)
	{
		printf("%s",strHigh1);
		//获取信号量
		if(xSemaphoreTake(BinarySem_PIHandle, portMAX_DELAY)==pdTRUE)  
		{
			printf("%s",strHigh2);
			printf("%s",strHigh3);
			//释放信号量
			xSemaphoreGive(BinarySem_PIHandle);	
		}
		vTaskDelay(500);
	}
  /* USER CODE END AppTask_High */
}

在 "FreeRTOS教程5 信号量" 文章 ”3.2、创建信号量“ 小节中曾提到,信号量被创建完之后是无效的,但是这里我们需要让刚创建的二值信号量有效,否则 Task_High 和 Task_Low 都将无法获取二值信号量,因此最后修改二值信号量的初始值为 1 即可,具体如下所示

/*将初始值0修改为1*/
BinarySem_PIHandle = osSemaphoreNew(1, 1, &BinarySem_PI_attributes);

4.4、烧录验证

烧录程序,打开串口助手,按住开发板复位按键,目的是为了让串口助手接收程序从最开始输出的信息,这里我们只分析第一轮,因为延时、语句执行等微小的时间差异会导致第二轮任务进入阻塞和退出阻塞的时间与第一轮有差异,如下所述为第一轮详细的任务执行流程

  1. 当创建完三个不同优先级的任务后不会立即得到执行,而是进入就绪状态等待调度器的启动
  2. 当调度器启动之后会按照优先级从最高优先级开始执行,因此串口输出 “Into Task_High” 表示进入高优先级任务,然后在高优先级任务 Task_High 中获得二值信号量,然后立马释放二值信号量,最后进入 500ms 的阻塞状态
  3. 当高优先级任务进入阻塞状态后,接下来会执行就绪状态的中等优先级任务 Task_Middle ,该任务无具体功能,仅仅通过串口输出 “Task_Middle is running”,然后同样进入 500ms 的阻塞状态
  4. 由于高优先级和中等优先级任务都进入阻塞状态,这时才轮到低优先级任务 Task_Low 执行,低优先级任务 Task_Low 成功获取到二值信号量并通过串口输出 “Task_Low take it” ,然后利用 500ms 的 HAL 库延时函数模拟连续运行
  5. 在 Task_Low 连续运行期间,在其即将执行完第一个 HAL_Delay(500); 时,高优先级任务 Task_High 从 500ms 的阻塞状态恢复,然后尝试获取已经被 Task_Low 获取的二值信号量,结果就是进入阻塞状态等待 Task_Low 释放二值信号量
  6. 紧接着 Task_Middle 从 500ms 的阻塞状态恢复,通过串口输出 “Task_Middle is running”,接着再次进入 500ms 阻塞状态
  7. 由于高优先级和中等优先级任务再次进入阻塞状态,因此调度器返回 Task_Low 被抢占时的程序处继续执行,因此 Task_Low 通过串口输出 “return Task_Low” ,然后利用第二个 HAL_Delay(500); 继续模拟长时间运行
  8. 在 Task_Low 第二个 HAL_Delay(500); 即将执行完毕时,Task_Middle 再次从 500ms 的阻塞状态恢复,通过串口输出 “Task_Middle is running” ,然后再次进入 500ms 阻塞状态(这里 Task_High 由于不是因为延时进入的阻塞状态所以未恢复运行状态)
  9. 最后返回 Task_Low 任务,释放二值信号量,一旦 Task_Low 任务释放二值信号量,等待二值信号量的高优先级任务 Task_High 会立马退出阻塞状态成功获取到二值信号量,并会通过串口输出 “Task_High get token“

从上述过程可知,从 Task_Low 获取二值信号量之后到第一轮结束,Task_High 等待 Task_Low 释放二值信号量,等待期间中等优先级的任务 Task_Middle 却先于高优先级任务 Task_High 得到了执行,这就是所谓的优先级翻转问题,上述过程所述的实际串口输出如下图所示

4.5、互斥量的应用

首先在 STM32CubeMX 中单击 Middleware and Software Packs/FREERTOS,在 Configuration 中单击 Mutexes 选项卡,单击 Add 按钮增加互斥量 Mutex_PI ,具体如下图所示

然后将上述实验使用的所有二值信号量句柄 BinarySem_PIHandle 修改为互斥量 Mutex_PIHandle,不需要做其他任何操作,烧录程序即可

打开串口助手,观察串口助手的输出,如下所述为第一轮详细的任务执行流程

  1. 前4个步骤与 ”4.4、烧录验证“ 小节一致,只不过从二值信号量修改为互斥量
  2. 在第5步时,高优先级任务 Task_High 从 500ms 的阻塞状态恢复,输出 ”Into Task_High“ ,然后尝试获取已经被 Task_Low 获取的互斥量,结果就是进入阻塞状态等待 Task_Low 释放互斥量,同时将 Task_Low 的优先级临时提高到和高优先级任务 Task_High 一样的优先级
  3. 紧接着 Task_Middle 从 500ms 的阻塞状态恢复,但是由于现在 Task_Low 任务的优先级要高于中等优先级任务 Task_Middle ,因此不能抢占 Task_Low 任务,故无法执行任务体输出 ”Task_Middle is running“ ,所以其状态变为就绪状态,它将等待所有高优先级的任务执行完后才会执行
  4. 于是优先级被临时提高到高优先级的任务 Task_Low 继续执行其函数体内容,输出 ”return Task_Low“ ,然后执行第二个 HAL_Delay(500); ,最后释放互斥量,通过串口输出 ”Task_Low give it“
  5. 一旦互斥量被 Task_Low 释放,处于阻塞状态的 Task_High 就会立马恢复运行状态获取到互斥量,所以会通过串口输出 ”Task_High get token“ 和 ”Task_High give token“ ,同时当互斥量被 Task_High 任务成功获取之后,会将任务 Task_Low 临时提高的优先级恢复到其原来的低优先级,最后 Task_High 调用延时函数进入 500ms 的阻塞状态
  6. 当高优先级任务 Task_High 进入阻塞状态后,系统内现在剩余就绪状态的中等优先级任务 Task_Middle 和 低优先级任务 Task_Low ,所以轮到 Task_Middle 任务执行,其将通过串口输出 ”Task_Middle is runing“ ,至此一轮结束

读者可以自行对比将二值信号量更换为互斥量之后的串口输出结果,可以发现在步骤4中,中等优先级的任务 Task_Middle 不再先于高优先级的任务 Task_High 得到执行,上述整个过程串口数据的完整输出如下图所示

5、注释详解

注释1:图片来源于 Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf

参考资料

STM32Cube高效开发教程(基础篇)

Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf

标签:教程,Task,优先级,FreeRTOS,信号量,互斥,任务,Low
From: https://www.cnblogs.com/lc-guo/p/18068439

相关文章

  • 实用crontab教程-一文读懂crontab
    文章目录Crontab是什么类似的工具有哪些Systemd(systemctl)Upstart(initctl)SysVinit(/etc/init.dscripts)作用用途:crontab的配置文件格式crontab表达式检查工具CrontabGuru:CronMaker:CronTabTool:运行身份原理:指定以特定用户身份运行:使用用户的crontab:使用系......
  • aardio教程二) 进阶语法
    表(table)aardio中除了基础数据类型外,其他的复合对象都是table(例如类和名字空间等)。table可以用来存放aardio的任何对象,包括另一个table。在其他语言中的字典、列表、数组、集合映射等,在aardio中都使用table来实现。创建字典importconsole;vartab={a=123;......
  • 最详细的Keycloak教程(建议收藏):Keycloak实现手机号、验证码登陆——(三)基于springboot&k
    在前面两节分别介绍了Keycloak的下载与使用和keycloak与springboot的集成。接下来第三节让我们一步步的去完成一个简单的前后端分离项目,并且可以扩展实现sso。一、简介本文将介绍如何使用SpringBoot、Keycloak和Vue构建一个具有前后端分离架构的Web应用程序。通过将前......
  • 葫芦娃助手教程
    #简介葫芦娃助手,支持一键批量预约,批量查询,提取token等功能,支持定时执行任务。节省玩家时间#运行环境Windows系统#软件下载地址GitHub-Explosbot/Hlwzs:葫芦娃助手,葫芦娃一条龙工具.支持葫芦娃批量预约,葫芦娃批量中签查询,葫芦娃token提取,A1662数据登录,批量注册等......
  • Linux第81步_使用“互斥体”实现“互斥访问”共享资源
    1、创建MyMutexLED目录输入“cd/home/zgq/linux/Linux_Drivers/回车”切换到“/home/zgq/linux/Linux_Drivers/”目录输入“mkdirMyMutexLED回车”,创建“MyMutexLED”目录输入“ls回车”查看“/home/zgq/linux/Linux_Drivers/”目录下的文件和文件夹2、添加gpio_led节点......
  • 关于进程同步与互斥的一些概念(锁、cas、futex)
    PS:要转载请注明出处,本人版权所有。PS:这个只是基于《我自己》的理解,如果和你的原则及想法相冲突,请谅解,勿喷。环境说明  无前言  最近为了实现在androidlinuxkernel上,是的bionicc和glibc的sem_相关的信号量接口能够相互调用的功能(例如:用bioniccwait,用glibcawake),......
  • Mimikatz使用教程
    介绍Mimikatz是一款开源的Windows安全工具,它被作者定义为“用来学习C语言和做一些Windows安全性实验的工具”。Mimikatz工具在Windows操作系统中运行时,可以从内存中提取出操作系统的明文密码、哈希、PIN码和Kerberos票据等,并支持哈希传递(pass-the-hash)、票据传递(pass-the-tick......
  • [nodejs] NodeJs/NPM入门教程
    0序nodejs是运行在服务器端的js,常用于前端工程师在本地电脑、或生产环境部署调试或运行前端工程。回想起来,上次使用nodejs,还在5年前做大学毕业设计时,基于前后端分离的实践(那时,业界正在兴起前后端分离的浪潮。当然了,现在的web工程,前后端分离已是默认的技术选择了)这次重......
  • 微信小程序云开发教程——墨刀原型工具入门(表单组件)
      引言作为一个小白,小北要怎么在短时间内快速学会微信小程序原型设计?“时间紧,任务重”,这意味着学习时必须把握微信小程序原型设计中的重点、难点,而非面面俱到。要在短时间内理解、掌握一个工具的使用,最有效的方式莫过于临摹:看实例视频教程,并跟着教程在实例素材上操作。......
  • 【奶奶看了都会】用 AI做猫咪剧情短片保姆级教程
    大家这段时间在刷短视频的时候,是不是经常会刷到那种猫咪剧情短片,配合喵喵喵......的魔性背景音乐,让人看了非常上头。最近这类视频在抖音、视频号、小红书上非常火,今天小卷就来教大家如何制作。先看视频效果:喵喵与卖火柴的小女孩1.GPT4账号准备我们用到的AI生图工具是ChatGPT4......