欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 游戏 > FreeRTOS源码概述(内存管理,入口函数,数据类型和编程规范)创建任务(声光色影,使用任务参数)删除任务(使用遥控器控制音乐)

FreeRTOS源码概述(内存管理,入口函数,数据类型和编程规范)创建任务(声光色影,使用任务参数)删除任务(使用遥控器控制音乐)

2024/10/23 8:55:32 来源:https://blog.csdn.net/2302_79504723/article/details/143121434  浏览:    关键词:FreeRTOS源码概述(内存管理,入口函数,数据类型和编程规范)创建任务(声光色影,使用任务参数)删除任务(使用遥控器控制音乐)

FreeRTOS源码概述


入口函数

在 Core\Src\main.c 的 main 函数里,初始化了 FreeRTOS 环境、创建了任务,然后启动
调度器。源码如下:
  /* Init scheduler */osKernelInitialize();  /* Call init function for freertos objects (in freertos.c) */MX_FREERTOS_Init();/* Start scheduler */osKernelStart();

osKernelInitialize();            初始化FreeRTOS运行环境

MX_FREERTOS_Init();      创建任务
osKernelStart();                  启动调度器
这样就可以运行FreeRTOS内的代码任务

数据类型和编程规范

数据类型

每个移植的版本都含有自己的 portmacro.h头文件(其实就相当于对FreeRTOS代码进行规范),里面定义了2 个数据类型:
TickType_t
FreeRTOS 配置了一个周期性的时钟中断: Tick Interrupt
每发生一次中断,中断次数累加,这被称为 tick count
tick count 这个变量的类型就是 TickType_t
TickType_t 可以是 16 位的,也可以是 32 位的
FreeRTOSConfig.h 中定义 configUSE_16_BIT_TICKS 时, TickType_t 就是 uint16_t
否则 TickType_t 就是 uint32_t
对于 32 位架构,建议把 TickType_t 配置为 uint32_t
BaseType_t
这是该架构最高效的数据类型
32 位架构中,它就是 uint32_t
16 位架构中,它就是 uint16_t
8 位架构中,它就是 uint8_t
BaseType_t 通 常 用 作 简 单 的 返 回 值 的 类 型 , 还 有 逻 辑 值 , 比 如
pdTRUE/pdFALSE
变量名

 函数名

函数名的前缀有2部分:返回值类型、在哪个文件定义。

 宏的名
宏的名字是大小,可以添加小写的前缀。前缀是用来表示:宏在哪个文件中定义。
通用的宏定义如下:

 内存管理

为什么要自己实现内存管理

后续的章节涉及这些内核对象: task queue semaphores event group 等。为了让
FreeRTOS 更容易使用,这些内核对象一般都是动态分配:用到时分配,不使用时释放。使
用内存的动态管理功能,简化了程序设计:不再需要小心翼翼地提前规划各类对象,简化 API
函数的涉及,甚至可以减少内存的使用。
C 语言的库函数中,有 mallc free 等函数,但是在 FreeRTOS 中,它们不适用:
不适合用在资源紧缺的嵌入式系统中
这些函数的实现过于复杂、占据的代码空间太大
并非线程安全的 (thread-safe)
运行有不确定性:每次调用这些函数时花费的时间可能都不相同
内存碎片化
使用不同的编译器时,需要进行复杂的配置
有时候难以调试

 FreeRTOS 的 5 中内存管理方法

FreeRTOS中内存管理的接口函数为:pvPortMalloc 、vPortFree,对应于C库的malloc、
free

 heap_1

它只实现了 pvPortMalloc ,没有实现 vPortFree
如果你的程序不需要删除内核对象,那么可以使用 heap_1
实现最简单
没有碎片问题
一些要求非常严格的系统里,不允许使用动态内存,就可以使用 heap_1

heap_2

Heap_2 也是在数组上分配内存,跟 Heap_1 不一样的地方在于:
Heap_2 使用 最佳匹配算法 (best fit) 来分配内存
它支持 vPortFree
最佳匹配算法:
假设 heap 3 块空闲内存: 5 字节、 25 字节、 100 字节
pvPortMalloc 想申请 20 字节
找出最小的、能满足 pvPortMalloc 的内存: 25 字节
把它划分为 20 字节、 5 字节
返回这 20 字节的地址
剩下的 5 字节仍然是空闲状态,留给后续的 pvPortMalloc 使用
Heap_4 相比, Heap_2 不会合并相邻的空闲内存,所以 Heap_2 会导致严重的 " 碎片化 "
题。
但是,如果申请、分配内存时大小总是相同的,这类场景下 Heap_2 没有碎片化的问题。
所以它适合这种场景:频繁地创建、删除任务,但是任务的栈大小都是相同的 ( 创建任务时,
需要分配 TCB 和栈, TCB 总是一样的 )
虽然不再推荐使用 heap_2 ,但是它的效率还是远高于 malloc free

heap_3

Heap_3 使用标准 C 库里的 malloc、free 函数,所以堆大小由链接器的配置决定,配置
项 configTOTAL_HEAP_SIZE 不再起作用。
C 库里的 malloc free 函数并非线程安全的, Heap_3 中先暂停 FreeRTOS 的调度器,再去
调用这些函数,使用这种方法实现了线程安全。

heap_4

跟 Heap_1、Heap_2 一样,Heap_4 也是使用大数组来分配内存。
Heap_4 使用 首次适应算法 (first fit) 来分配内存。它还会把相邻的空闲内存合并为一个更
大的空闲内存,这有助于较少内存的碎片问题。
首次适应算法:
假设堆中有 3 块空闲内存: 5 字节、 200 字节、 100 字节
pvPortMalloc 想申请 20 字节
找出第 1 个能满足 pvPortMalloc 的内存: 200 字节
把它划分为 20 字节、 180 字节
返回这 20 字节的地址
剩下的 180 字节仍然是空闲状态,留给后续的 pvPortMalloc 使用
Heap_4会把相邻空闲内存合并为一个大的空闲内存,可以较少内存的碎片化问题。适用
于这种场景:频繁地分配、释放不同大小的内存

 heap_5

Heap_5 分配内存、释放内存的算法跟 Heap_4 是一样的。
相比于 Heap_4 Heap_5 并不局限于管理一个大数组:它可以管理多块、分隔开的内存。
在嵌入式系统中,内存的地址可能并不连续,这种场景下可以使用 Heap_5
既然内存时分隔开的,那么就需要进行初始化:确定这些内存块在哪、多大:
在使用 pvPortMalloc 之前,必须先指定内存块的信息
使用 vPortDefineHeapRegions 来指定这些信息
指定一块内存
typedef struct HeapRegion
{uint8_t * pucStartAddress; // 起始地址size_t xSizeInBytes; // 大小
} HeapRegion_t;
怎么指定多块内存
HeapRegion_t xHeapRegions[] =
{{ ( uint8_t * ) 0x80000000UL, 0x10000 }, // 起始地址0x80000000,大小0x10000{ ( uint8_t * ) 0x90000000UL, 0xa0000 }, // 起始地址0x90000000,大小0xa0000{ NULL, 0 } // 表示数组结束
};

Heap 相关的函数

pvPortMalloc/vPortFree

void * pvPortMalloc( size_t xWantedSize );
void vPortFree( void * pv );

作用:分配内存、释放内存。
如果分配内存不成功,则返回值为 NULL

xPortGetFreeHeapSize

size_t xPortGetFreeHeapSize( void );
返回:程序当前空闲内存容量
注意:只有 heap_3不 支持此函数

xPortGetMinimumEverFreeHeapSize

size_t xPortGetMinimumEverFreeHeapSize( void );
返回:程序运行过程中,空闲内存容量的最小值。
注意:只有 heap_4 heap_5 支持此函数

vApplicationMallocFailedHook(malloc 失败的钩子函数)

void * pvPortMalloc( size_t xWantedSize )vPortDefineHeapRegions
{......#if ( configUSE_MALLOC_FAILED_HOOK == 1 ){if( pvReturn == NULL ){extern void vApplicationMallocFailedHook( void );vApplicationMallocFailedHook();}}#endifreturn pvReturn; 
}

如果分配失败就调用这个函数vApplicationMallocFailedHook

所以,如果想使用这个钩子函数:
FreeRTOSConfig.h 中,把 configUSE_MALLOC_FAILED_HOOK 定义为 1
提供 vApplicationMallocFailedHook 函数
⚫ pvPortMalloc 失败时,才会调用此函数

创建任务


声光色影

什么是任务

在 FreeRTOS 中,任务就是一个函数,原型如下:
void ATaskFunction( void *pvParameters );
要注意的是:
这个函数不能返回
同一个函数,可以用来创建多个任务;换句话说,多个任务可以运行同一个
函数
函数内部,尽量使用局部变量:
每个任务都有自己的栈
每个任务运行这个函数时
任务 A 的局部变量放在任务 A 的栈里、任务 B 的局部变量放在任务 B 的
栈里
不同任务的局部变量,有自己的副本
函数使用全局变量、静态变量的话
只有一个副本:多个任务使用的是同一个副本
要防止冲突(后续会讲)

创建任务

创建任务时可以使用 2 个函数:动态分配内存、静态分配内存
动态分配内存

任务创建1.函数

              2.栈和TCB(任务控制块)

              3.优先级

在 FreeRTOS 中,任务控制块(Task Control Block,TCB)是用来管理任务的一个重要数据结构。TCB 包含了与任务相关的所有信息,包括任务的状态、优先级、堆栈指针等。以下是 TCB 的一些主要组成部分和功能:

TCB 的组成部分
  1. 任务堆栈指针

    • TCB 中包含一个指向任务栈的指针。每个任务都有自己独立的堆栈,用于存储局部变量和函数调用信息。
  2. 任务优先级

    • TCB 记录了任务的优先级,FreeRTOS 会根据优先级调度任务。优先级越高,任务的执行频率越高。
  3. 任务状态

    • TCB 包含任务的当前状态,例如就绪、运行、阻塞等。这使得调度器能够管理任务的运行。
  4. 任务名字

    • TCB 可以包含任务的名称,方便调试和监控任务。
  5. 任务延迟计数

    • 用于跟踪任务的延迟状态,帮助调度器判断任务是否可以重新调度。
  6. 其他调度相关信息

    • 例如,任务的运行时间、等待的信号量或事件等信息。
TCB 的功能
  • 任务调度:TCB 是 FreeRTOS 调度的核心,调度器通过 TCB 来管理各个任务的状态和切换。
  • 资源管理:TCB 还帮助管理任务使用的系统资源,例如定时器、信号量和消息队列等。
  • 上下文切换:在任务切换时,TCB 中的信息用于保存当前任务的状态,并恢复下一个任务的状态。
调度器的相关问题

在 FreeRTOS 中,调用 vTaskStartScheduler() 后,调度器会开始运行并根据任务的优先级和状态管理任务的调度。以下是为什么“如果一切正常,程序不会到达这里”的原因:

1. 调度器的作用

  • 任务调度vTaskStartScheduler() 函数启动了 FreeRTOS 的调度器,使其开始管理任务。调度器会选择适当的任务运行,并在需要时进行上下文切换。
  • 任务运行:一旦调度器开始运行,程序控制权转交给任务,而不是继续执行 main() 函数中调度器之后的代码。

2. 无限循环

  • for (;;) {} 是一个无限循环,表示在正常情况下,程序应该一直停留在这个循环中,不再继续执行 main() 函数中的其他代码。这个循环通常用于防止程序继续执行到意外的位置。

3. 正常情况下的行为

  • 正常运行:如果 FreeRTOS 正常运行并且至少有一个任务处于就绪状态,调度器会不断调度该任务。由于调度器在控制流中,它不会返回到 main() 函数的后续代码。
  • 错误情况:如果调度器无法正常启动(例如没有足够的堆栈空间、任务创建失败等),那么程序可能会到达这个无限循环。但在设计上,理想情况下调度器应该总是能够启动并运行。

总结

因此,for (;;) {} 的存在是为了表明在正常情况下,程序不会执行到这一行。如果程序到达这一行,通常意味着有某种错误或异常情况发生,可能是需要进行调试和处理的地方。

TCB与任务句柄的关系

在操作系统中,TCB(Task Control Block)和任务句柄(Task Handle)是管理任务和进程的重要概念。它们之间的关系可以从以下几个方面进行说明:

1. 定义与概念

  • TCB(任务控制块): TCB 是一个数据结构,用于存储与任务或进程相关的信息。它通常包含以下内容:

    • 任务的状态(如运行、就绪、阻塞等)
    • 任务的优先级
    • 任务的程序计数器(PC)
    • 任务的堆栈指针
    • 其他用于管理任务执行的上下文信息
  • 任务句柄(Task Handle): 任务句柄是一种引用或指针,通常用于在程序中标识和操作特定的任务。句柄可以被视为对 TCB 的抽象,程序可以通过句柄来管理任务,而无需直接访问 TCB 结构。

2. TCB与任务句柄的关系

  • 创建与维护: 当一个新任务被创建时,操作系统会为其分配一个 TCB,存储其状态和上下文信息。与此同时,操作系统会生成一个任务句柄,将其与 TCB 关联起来,以便在后续操作中引用。

  • 任务管理: 通过任务句柄,程序可以执行对任务的操作,比如启动、暂停、终止或查询状态。这些操作通常会触发对 TCB 的访问,以获取任务的详细信息或更新其状态。

  • 抽象层次: 任务句柄提供了对 TCB 的一种抽象,使得程序员可以更方便地管理任务,而不需要深入了解 TCB 的内部实现。这种设计使得任务管理更加灵活,能够简化程序的复杂性。

3. 示例

在一些操作系统或实时操作系统中,使用任务句柄来实现对任务的管理。例如,开发者可以调用 GetTaskStatus(task_handle) 函数来查询某个任务的状态,而这个函数内部实际上会通过任务句柄找到对应的 TCB,并返回其中的状态信息。

4. 总结

TCB 和任务句柄在操作系统中共同发挥作用。TCB 是任务的详细描述,而任务句柄是对 TCB 的一种抽象引用,允许程序在不直接操作 TCB 的情况下管理和控制任务。这种设计提高了系统的可维护性和灵活性,简化了任务管理的过程。

BaseType_t xTaskCreate( 
TaskFunction_t pxTaskCode, // 函数指针, 任务函数const char * const pcName, // 任务的名字const configSTACK_DEPTH_TYPE usStackDepth, // 栈大小,单位为word,10表示40字节void * const pvParameters, // 调用任务函数时传入的参数UBaseType_t uxPriority, // 优先级TaskHandle_t * const pxCreatedTask ); // 任务句柄, 以后使用它来操作这个任务
在动态分配中栈和TCB会自动分配
静态分配内存
TaskHandle_t xTaskCreateStatic ( TaskFunction_t pxTaskCode, // 函数指针, 任务函数const char * const pcName, // 任务的名字const uint32_t ulStackDepth, // 栈大小,单位为word,10表示40字节void * const pvParameters, // 调用任务函数时传入的参数UBaseType_t uxPriority, // 优先级StackType_t * const puxStackBuffer, // 静态分配的栈,就是一个bufferStaticTask_t * const pxTaskBuffer // 静态分配的任务结构体的指针,用它来操作这个任务
);
静态分配与动态分配的对比
  1. 静态分配 (xTaskCreateStatic()):
    • 内存分配:在编译时就分配好,使用一个预先定义的内存区域(例如数组)来存储 TCB 和任务堆栈。
    • 没有句柄:由于内存是在编译时分配的,因此不需要句柄来管理这些资源。
    • 内存管理:更简单,因为所有资源都是静态的,避免了动态内存分配带来的碎片和泄漏问题。
    • 适合场景:适用于内存有限或要求高可靠性的嵌入式系统。
  2. 动态分配 (xTaskCreate()):
    • 内存分配:在运行时动态分配 TCB 和堆栈,通常会使用 FreeRTOS 的内存管理功能(如 pvPortMalloc())。
    • 句柄:创建任务后,系统会返回一个任务句柄,允许后续对任务进行管理(例如删除任务、暂停任务等)。
    • 内存管理:需要小心管理内存,以防止内存泄漏和碎片化。
    • 适合场景:适用于资源较为充足且需要灵活创建和管理任务的应用。
 定义
  • 静态分配: 静态分配是在编译时或程序加载时分配内存。内存的大小和位置在程序运行之前就已经确定。这种分配方式通常适用于全局变量、静态变量和常量。

  • 动态分配: 动态分配是在程序运行时根据需要分配内存。内存的大小和位置在程序运行过程中可以动态变化,通常使用内存管理函数(如 malloc()calloc()free() 在 C/C++ 中)来进行分配和释放。

2. 内存管理
  • 静态分配

    • 内存大小固定,无法在运行时改变。
    • 程序生命周期内分配的内存不释放,直到程序结束。
    • 内存的分配和释放由编译器管理。
  • 动态分配

    • 内存大小可以在运行时动态确定,适合不确定大小的数组或对象。
    • 需要手动管理内存的分配和释放,以防止内存泄漏。
    • 程序可以根据需求随时请求和释放内存。
3. 灵活性
  • 静态分配

    • 灵活性较低,无法在运行时改变内存的大小。
    • 适用于大小已知且不需要变化的场景。
  • 动态分配

    • 灵活性高,可以根据实际需求随时调整内存大小。
    • 适合处理不确定大小或变化的数据结构(如链表、树等)。
4. 效率
  • 静态分配

    • 通常效率较高,因为内存分配和释放的过程较简单,编译器在编译时进行优化。
    • 不存在内存碎片问题。
  • 动态分配

    • 动态分配可能会引入一定的开销,尤其是在频繁分配和释放内存时。
    • 可能会出现内存碎片,影响内存的使用效率。
  /* Private variables ---------------------------------------------------------*/
/* USER CODE BEGIN Variables */
static StackType_t g_pucStackOfLightTask[128];
static StaticTask_t g_TCBofLightTask;
static TaskHandle_t xLightTaskHandle;static StackType_t g_pucStackOfColorTask[128];
static StaticTask_t g_TCBofColorTask;
static TaskHandle_t xColorTaskHandle;/* USER CODE END Variables */
/* Definitions for defaultTask *//* Create the thread(s) *//* creation of defaultTask */defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);/* USER CODE BEGIN RTOS_THREADS *//* add threads, ... *//* ´´½¨ÈÎÎñ: Éù */extern void PlayMusic(void *params);ret = xTaskCreate(PlayMusic, "SoundTask", 128, NULL, osPriorityNormal, &xSoundTaskHandle);/* ´´½¨ÈÎÎñ: ¹â */xLightTaskHandle = xTaskCreateStatic(Led_Test, "LightTask", 128, NULL, osPriorityNormal, g_pucStackOfLightTask, &g_TCBofLightTask);/* ´´½¨ÈÎÎñ: É« */xColorTaskHandle = xTaskCreateStatic(ColorLED_Test, "ColorTask", 128, NULL, osPriorityNormal, g_pucStackOfColorTask, &g_TCBofColorTask);/* USER CODE END RTOS_THREADS *//* USER CODE BEGIN RTOS_EVENTS *//* add events, ... *//* USER CODE END RTOS_EVENTS */

使用任务参数

struct TaskPrintInfo {uint8_t x;uint8_t y;char name[16];
};static struct TaskPrintInfo g_Task1Info = {0, 0, "Task1"};
static struct TaskPrintInfo g_Task2Info = {0, 3, "Task2"};
static struct TaskPrintInfo g_Task3Info = {0, 6, "Task3"};
static int g_LCDCanUse = 1;void LcdPrintTask(void *params)
{struct TaskPrintInfo *pInfo = params;uint32_t cnt = 0;int len;while (1){/* ´òÓ¡ÐÅÏ¢ */if (g_LCDCanUse){g_LCDCanUse = 0;len = LCD_PrintString(pInfo->x, pInfo->y, pInfo->name);len += LCD_PrintString(len, pInfo->y, ":");LCD_PrintSignedVal(len, pInfo->y, cnt++);g_LCDCanUse = 1;}mdelay(500);}
}
/* USER CODE END FunctionPrototypes */void StartDefaultTask(void *argument);void MX_FREERTOS_Init(void); /* (MISRA C 2004 rule 8.1) *//*** @brief  FreeRTOS initialization* @param  None* @retval None*/
void MX_FREERTOS_Init(void) {/* USER CODE BEGIN Init */TaskHandle_t xSoundTaskHandle;BaseType_t ret;LCD_Init();LCD_Clear();/* USER CODE END Init *//* USER CODE BEGIN RTOS_MUTEX *//* add mutexes, ... *//* USER CODE END RTOS_MUTEX *//* USER CODE BEGIN RTOS_SEMAPHORES *//* add semaphores, ... *//* USER CODE END RTOS_SEMAPHORES *//* USER CODE BEGIN RTOS_TIMERS *//* start timers, add new ones, ... *//* USER CODE END RTOS_TIMERS *//* USER CODE BEGIN RTOS_QUEUES *//* add queues, ... *//* USER CODE END RTOS_QUEUES *//* Create the thread(s) *//* creation of defaultTask *///defaultTaskHandle = osThreadNew(StartDefaultTask, NULL, &defaultTask_attributes);/* USER CODE BEGIN RTOS_THREADS *//* add threads, ... *//* ´´½¨ÈÎÎñ: Éù */extern void PlayMusic(void *params);//ret = xTaskCreate(PlayMusic, "SoundTask", 128, NULL, osPriorityNormal, &xSoundTaskHandle);/* ´´½¨ÈÎÎñ: ¹â *///xLightTaskHandle = xTaskCreateStatic(Led_Test, "LightTask", 128, NULL, osPriorityNormal, g_pucStackOfLightTask, &g_TCBofLightTask);/* ´´½¨ÈÎÎñ: É« *///xColorTaskHandle = xTaskCreateStatic(ColorLED_Test, "ColorTask", 128, NULL, osPriorityNormal, g_pucStackOfColorTask, &g_TCBofColorTask);/* ʹÓÃͬһ¸öº¯Êý´´½¨²»Í¬µÄÈÎÎñ */xTaskCreate(LcdPrintTask, "task1", 128, &g_Task1Info, osPriorityNormal, NULL);xTaskCreate(LcdPrintTask, "task2", 128, &g_Task2Info, osPriorityNormal, NULL);xTaskCreate(LcdPrintTask, "task3", 128, &g_Task3Info, osPriorityNormal, NULL);/* USER CODE END RTOS_THREADS *//* USER CODE BEGIN RTOS_EVENTS *//* add events, ... *//* USER CODE END RTOS_EVENTS */}

删除任务


使用遥控器控制音乐

void StartDefaultTask(void *argument)
{/* USER CODE BEGIN StartDefaultTask *//* Infinite loop */uint8_t dev, data;int len;TaskHandle_t xSoundTaskHandle = NULL;BaseType_t ret;LCD_Init();LCD_Clear();IRReceiver_Init();LCD_PrintString(0, 0, "Waiting control");while (1){/* ¶ÁÈ¡ºìÍâÒ£¿ØÆ÷ */if (0 == IRReceiver_Read(&dev, &data)){		if (data == 0xa8) /* play */{/* ´´½¨²¥·ÅÒôÀÖµÄÈÎÎñ */extern void PlayMusic(void *params);if (xSoundTaskHandle == NULL){LCD_ClearLine(0, 0);LCD_PrintString(0, 0, "Create Task");ret = xTaskCreate(PlayMusic, "SoundTask", 128, NULL, osPriorityNormal, &xSoundTaskHandle);}}else if (data == 0xa2) /* power */{/* ɾ³ý²¥·ÅÒôÀÖµÄÈÎÎñ */if (xSoundTaskHandle != NULL){LCD_ClearLine(0, 0);LCD_PrintString(0, 0, "Delete Task");vTaskDelete(xSoundTaskHandle);PassiveBuzzer_Control(0); /* Í£Ö¹·äÃùÆ÷ */xSoundTaskHandle = NULL;}}}}/* USER CODE END StartDefaultTask */
}/* Private application code --------------------------------------------------*/
/* USER CODE BEGIN Application *//* USER CODE END Application */

vTaskDelete 是 FreeRTOS 中的一个函数,用于删除一个任务。xSoundTaskHandle 是一个任务句柄,代表你希望删除的任务。使用这个函数可以释放与任务相关的资源,并将任务从调度器中移除。

使用 vTaskDelete

以下是使用 vTaskDelete 的基本信息和示例代码:

void vTaskDelete(TaskHandle_t xTaskToDelete);
  1. xTaskToDelete:要删除的任务的句柄。如果传递 NULL,将删除调用 vTaskDelete 的任务本身。
  2. 删除任务的注意事项:

    • 一旦任务被删除,无法再恢复。
    • 确保在删除任务之前,所有与任务相关的资源都已经释放或被处理。
    • 调用 vTaskDelete 后,任何对该任务句柄的访问都是未定义行为。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com