频道栏目
首页 > 资讯 > 其他 > 正文

UCOSIII的存储管理

18-07-10        来源:[db:作者]  
收藏   我要投稿

UCOSIII内存管理简介

作为一个RTOS操作系统,内存管理是必备的功能,因此UCOSIII也就内存管理能力。通常应用程序可以调用ANSI C编译器的malloc()和free()函数来动态的分配和释放内存,但是在嵌入式事实操作系统中最好不要这么做,多次这样的操作会把原来很大的一块连续存储区域逐渐地分割成许多非常小并且彼此不相邻的存储区域,这就是存储碎片。存储碎片最终导致的结果就是,应用不能申请到大小合适的连续内存。

UCOSIII中提供了一种替代malloc()和free()函数的方法,它提供了自己的动态内存方案。UCOIII将存储空间分成区和块,一个存储区有数个固定大小的块组成,如下图所示:

一般存储区是固定的,在程序中可以用数组来表示一个存储区,比如u8 buffer[20][10]就表示一个有20个存储块,每个存储块10字节的存储区。如果我们定义的存储区在程序运行期间都不会被删除掉,一直有效,那么存储区内存也可以使用malloc()来分配。在创建存储区以后应用程序就可以获得固定大小的存储块。

实际使用中我们可以根据应用程序对内存需求的不同建立多个存储区,每个存储区中有不同大小、不同数量的存储块,应用程序可以根据所需内存不同从不同的存储区中申请内存使用,使用完以后在释放到相应的存储区中。

 

UCOSIII存储控制块

UCOSIII中用存储控制块来表示存储区,存储控制块为OS_MEM。具体的定义为:

struct os_mem {                                             /* MEMORY CONTROL BLOCK                                   */
    OS_OBJ_TYPE          Type;                              /* 类型   */
    void                *AddrPtr;                           /* 指向存储区起始地址   */
    CPU_CHAR            *NamePtr;
    void                *FreeListPtr;                       /* 指向空闲存储块   */
    OS_MEM_SIZE          BlkSize;                           /* 存储区中存储块大小,单位:字节  */
    OS_MEM_QTY           NbrMax;                            /* 存储区中总的存储块数   */
    OS_MEM_QTY           NbrFree;                           /* 存储区中空闲存储块数   */
#if OS_CFG_DBG_EN > 0u
    OS_MEM              *DbgPrevPtr;
    OS_MEM              *DbgNextPtr;
#endif
};

创建好的存储区如下图:

UCOSIII存储管理相关API函数

UCOSIII存储管理相关API函数
函数 说明
OSMemCreate() 创建一个存储分区
OSMemGet() 从存储分区中获得一个存储块
OSMemPut() 将一个存储块归还到存储分区中

存储区创建

创建存储区使用函数OSMemCreate(),函数原型如下:

void  OSMemCreate (OS_MEM       *p_mem,//指向存储区控制块地址
                   CPU_CHAR     *p_name,//指向存储区的名字
                   void         *p_addr,//存储区所有存储空间基地址
                   OS_MEM_QTY    n_blks,//存储区中存储块个数
                   OS_MEM_SIZE   blk_size,//存储块大小
                   OS_ERR       *p_err)
{
    OS_MEM_QTY     i;
    OS_MEM_QTY     loops;
    CPU_INT08U    *p_blk;
    void         **p_link;
    CPU_SR_ALLOC();

#if OS_CFG_CALLED_FROM_ISR_CHK_EN > 0u
    if (OSIntNestingCtr > (OS_NESTING_CTR)0) {              /* 中断中是不能调用函数OSMemCreate()来创建存储区的    */
       *p_err = OS_ERR_MEM_CREATE_ISR;
        return;
    }
#endif

#if OS_CFG_ARG_CHK_EN > 0u
    if (p_addr == (void *)0) {                              /* 存储区空间基地址不能为0   */
       *p_err   = OS_ERR_MEM_INVALID_P_ADDR;
        return;
    }
    if (n_blks < (OS_MEM_QTY)2) {                           /* 存储区中存储块数量n_blks最少为2  */
       *p_err = OS_ERR_MEM_INVALID_BLKS;
        return;
    }
    if (blk_size < sizeof(void *)) {                        /* 每个存储块大小不小于一个指针大小,一个指针大小为4字节,因此每个存储块大小要大于4字节  */
       *p_err = OS_ERR_MEM_INVALID_SIZE;
        return;
    }
    align_msk = sizeof(void *) - 1u;
    if (align_msk > 0) {
        if (((CPU_ADDR)p_addr & align_msk) != 0u){          /* 判断存储区空间基地址是否4字节对齐,存储区的存储空间基地址必须4字节对齐  */
           *p_err = OS_ERR_MEM_INVALID_P_ADDR;
            return;
        }
        if ((blk_size & align_msk) != 0u) {                 /* 存储区中每个存储块的大小要是4的倍数  */
           *p_err = OS_ERR_MEM_INVALID_SIZE;
            return;
        }
    }
#endif

    p_link = (void **)p_addr;                               /* Create linked list of free memory blocks               */
    p_blk  = (CPU_INT08U *)p_addr;
    loops  = n_blks - 1u;
    for (i = 0u; i < loops; i++) {
        p_blk +=  blk_size;
       *p_link = (void  *)p_blk;                            /* Save pointer to NEXT block in CURRENT block            */
        p_link = (void **)(void *)p_blk;                    /* Position     to NEXT block                             */
    }
   *p_link             = (void *)0;                         /* Last memory block points to NULL                       */

    OS_CRITICAL_ENTER();
    p_mem->Type        = OS_OBJ_TYPE_MEM;                   /* Set the type of object                                 */
    p_mem->NamePtr     = p_name;                            /* Save name of memory partition                          */
    p_mem->AddrPtr     = p_addr;                            /* Store start address of memory partition                */
    p_mem->FreeListPtr = p_addr;                            /* Initialize pointer to pool of free blocks              */
    p_mem->NbrFree     = n_blks;                            /* Store number of free blocks in MCB                     */
    p_mem->NbrMax      = n_blks;
    p_mem->BlkSize     = blk_size;                          /* Store block size of each memory blocks                 */

#if OS_CFG_DBG_EN > 0u
    OS_MemDbgListAdd(p_mem);
#endif

    OSMemQty++;

    OS_CRITICAL_EXIT_NO_SCHED();
   *p_err = OS_ERR_NONE;
}

内存申请

使用函数OSMemGet()来获取存储块,函数原型如下:

void  *OSMemGet (OS_MEM  *p_mem,//要使用的存储区
                 OS_ERR  *p_err)
{
    void    *p_blk;
    CPU_SR_ALLOC();

#ifdef OS_SAFETY_CRITICAL
    if (p_err == (OS_ERR *)0) {
        OS_SAFETY_CRITICAL_EXCEPTION();
        return ((void *)0);
    }
#endif

#if OS_CFG_ARG_CHK_EN > 0u
    if (p_mem == (OS_MEM *)0) {                             /* 判断存储区p_mem是否存在     */
       *p_err  = OS_ERR_MEM_INVALID_P_MEM;
        return ((void *)0);
    }
#endif

    CPU_CRITICAL_ENTER();
    if (p_mem->NbrFree == (OS_MEM_QTY)0) {                  /* 判断存储区中是否还有空闲的存储块    */
        CPU_CRITICAL_EXIT();
       *p_err = OS_ERR_MEM_NO_FREE_BLKS;                    /* No,  Notify caller of empty memory partition           */
        return ((void *)0);                                 /*      Return NULL pointer to caller                     */
    }
    p_blk              = p_mem->FreeListPtr;                /*  取出空闲存储块链表中第一个存储块   */
    p_mem->FreeListPtr = *(void **)p_blk;                   /*  空闲存储块链表头FreeListPtr就需要更新    */
    p_mem->NbrFree--;                                       /*      One less memory block in this partition           */
    CPU_CRITICAL_EXIT();
   *p_err = OS_ERR_NONE;                                    /*      No error                                          */
    return (p_blk);                                         /*      Return memory block to caller                     */
}

返回值:获取到的存储块地址。

从上面的程序中我们可以看出UCOSIII自带的内存管理函数的局限性,每次申请内存的时候用户要先估计所申请的内存是否会超过存储区中存储块的大小。比如我们创建了一个有10个存储块,每个存储块大小为100字节的存储区buffer。这时我们应用程序需要申请一个10字节的内存,那么就可以使用函数OSMemGet()从存储区buffer中申请一个存储块。但是每个存储块有100个字节,但是应用程序只使用其中的10个字节,剩余的90个字节就浪费掉了,为了减少浪费我们可以创建一个每个存储块为10字节的存储区,这样就不会有内费了。

但是,问题又来了,假设我们在程序的其他地方需要申请一个150字节的内存,但是存储区buffer的每个存储块只有100字节,显然存储区buffer不能满足程序的需求。有读者就会问可不可以在存储区中连续申请两个100字节的存储块,这样就有200字节的内存供应用程序使用了?想法是好想法,但是通过阅读函数OSMemGet()发现并没有提供这样的功能,OSMemGet()函数在申请内存的时候每次只取指定存储区的一个存储块!如果想申请150字节的内存就必须再新建一个每个存储块至少有150字节的存储区。

通过上面的分析可以看出UCOSIII的内存管理很粗糙,不灵活,并不能申请指定大小的内存块。

内存释放

在UCOSIII中内存的释放可以使用函数OSMemPut()来完成,函数原型如下:

void  OSMemPut (OS_MEM  *p_mem,//指向存储区控制块
                void    *p_blk,//指向存储块
                OS_ERR  *p_err)
{
    CPU_SR_ALLOC();

#ifdef OS_SAFETY_CRITICAL
    if (p_err == (OS_ERR *)0) {
        OS_SAFETY_CRITICAL_EXCEPTION();
        return;
    }
#endif

#if OS_CFG_ARG_CHK_EN > 0u
    if (p_mem == (OS_MEM *)0) {                             /* 检查存储区是否存在    */
       *p_err  = OS_ERR_MEM_INVALID_P_MEM;
        return;
    }
    if (p_blk == (void *)0) {                               /* 检查存储块是否存在     */
       *p_err  = OS_ERR_MEM_INVALID_P_BLK;
        return;
    }
#endif

    CPU_CRITICAL_ENTER();
    if (p_mem->NbrFree >= p_mem->NbrMax) {                  /* 检查存储区空闲存储块数量是否大于存储区总存储块数量    */
        CPU_CRITICAL_EXIT();
       *p_err = OS_ERR_MEM_FULL;
        return;
    }
    *(void **)p_blk    = p_mem->FreeListPtr;                /* 将要归还的存储块添加到空闲存储块链表中,这里是添加到表头的位置   */
    p_mem->FreeListPtr = p_blk;
    p_mem->NbrFree++;                                       /* One more memory block in this partition                */
    CPU_CRITICAL_EXIT();
   *p_err              = OS_ERR_NONE;                       /* Notify caller that memory block was released           */
}

UCOSIII实际例程

存储管理实验

例程要求:设计一个程序,创建两个存储区,这两个存储区分别创建STM32的内部RAM和外部SRAM中,通过开发板上的按键来申请和释放内存。

例子:

#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "lcd.h"
#include "key.h"
#include "sram.h"
#include "beep.h"
#include "includes.h"

//UCOSIII中以下优先级用户程序不能使用,ALIENTEK
//将这些优先级分配给了UCOSIII的5个系统内部任务
//优先级0:中断服务服务管理任务 OS_IntQTask()
//优先级1:时钟节拍任务 OS_TickTask()
//优先级2:定时任务 OS_TmrTask()
//优先级OS_CFG_PRIO_MAX-2:统计任务 OS_StatTask()
//优先级OS_CFG_PRIO_MAX-1:空闲任务 OS_IdleTask()

//任务优先级
#define START_TASK_PRIO		3
//任务堆栈大小	
#define START_STK_SIZE 		128
//任务控制块
OS_TCB StartTaskTCB;
//任务堆栈	
CPU_STK START_TASK_STK[START_STK_SIZE];
//任务函数
void start_task(void *p_arg);

//任务优先级
#define MAIN_TASK_PRIO		4
//任务堆栈大小	
#define MAIN_STK_SIZE 		128
//任务控制块
OS_TCB Main_TaskTCB;
//任务堆栈	
CPU_STK MAIN_TASK_STK[MAIN_STK_SIZE];
//任务函数
void main_task(void *p_arg);

//任务优先级
#define MEMMANAGE_TASK_PRIO	5
//任务堆栈大小
#define MEMMANAGE_STK_SIZE	128
//任务控制块
OS_TCB	MemManage_TaskTCB;
//任务堆栈
CPU_STK MEMMANAGE_TASK_STK[MEMMANAGE_STK_SIZE];
//任务函数
void memmanage_task(void *p_arg);

//定义一个存储区
OS_MEM INTERNAL_MEM;	
//存储区中存储块数量
#define INTERNAL_MEM_NUM		5
//每个存储块大小
//由于一个指针变量占用4字节所以块的大小一定要为4的倍数
//而且必须大于一个指针变量(4字节)占用的空间,否则的话存储块创建不成功
#define INTERNAL_MEMBLOCK_SIZE	25*4  
//存储区的内存池,使用内部RAM
__align(4) CPU_INT08U Internal_RamMemp[INTERNAL_MEM_NUM][INTERNAL_MEMBLOCK_SIZE];

//定义一个存储区
OS_MEM EXTERNAL_MEM;	
//存储区中存储块数量
#define EXTRENNAL_MEM_NUM		5
//每个存储块大小
//由于一个指针变量占用4字节所以块的大小一定要为4的倍数
//而且必须大于一个指针变量(4字节)占用的空间,否则的话存储块创建不成功
#define EXTERNAL_MEMBLOCK_SIZE	25*4
//存储区的内存池,使用外部SRAM
__align(32) volatile CPU_INT08U External_RamMemp[EXTRENNAL_MEM_NUM][EXTERNAL_MEMBLOCK_SIZE]  __attribute__((at(0X68000000)));	

					
void ucos_load_main_ui(void) //加载主界面
{
	POINT_COLOR = RED;
	LCD_ShowString(30,10,200,16,16,"ALIENREK STM32F1");	
	LCD_ShowString(30,30,200,16,16,"UCOSIII Examp 14-1");
	LCD_ShowString(30,50,200,16,16,"Memory Manage");
	POINT_COLOR = BLUE;
	LCD_ShowString(30,70,200,16,16,"INTERNAL_MEM:");
	POINT_COLOR = RED;
	LCD_ShowString(30,90,200,16,16,"KEY_UP:malloc KEY1:free");
	POINT_COLOR = BLUE;
	LCD_ShowString(30,110,200,16,16,"EXTERNAL_MEM:");
	POINT_COLOR = RED;
	LCD_ShowString(30,130,200,16,16,"KEY2:malloc KEY0:free");
	POINT_COLOR = BLACK;
	LCD_DrawLine(0,147,239,147);
	POINT_COLOR = BLUE;
	LCD_ShowString(5,148,200,16,16,"INTERNAL_MEM:");
	LCD_ShowString(5,228,200,16,16,"EXTERNAL_MEM:");
	POINT_COLOR = RED; 
}

int main(void)//主函数
{
	OS_ERR err;
	CPU_SR_ALLOC();
	
	delay_init();  //时钟初始化
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//中断分组配置
	uart_init(115200);   //串口初始化
	LED_Init();         //LED初始化	
	LCD_Init();			//LCD初始化	
	KEY_Init();			//按键初始化
	BEEP_Init();		//初始化蜂鸣器
	FSMC_SRAM_Init();	//初始化SRAM
	ucos_load_main_ui();//加载主UI1
	
	OSInit(&err);		    //初始化UCOSIII
	OS_CRITICAL_ENTER();	//进入临界区	

	OSMemCreate((OS_MEM*	)&INTERNAL_MEM,	//创建一个存储分区
				(CPU_CHAR*	)"Internal Mem",
				(void*		)&Internal_RamMemp[0][0],
				(OS_MEM_QTY	)INTERNAL_MEM_NUM,
				(OS_MEM_SIZE)INTERNAL_MEMBLOCK_SIZE,
				(OS_ERR*	)&err);

	OSMemCreate((OS_MEM*	)&EXTERNAL_MEM,	//创建一个存储分区
				(CPU_CHAR*	)"External Mem",
				(void*		)&External_RamMemp[0][0],
				(OS_MEM_QTY	)EXTRENNAL_MEM_NUM,
				(OS_MEM_SIZE)EXTERNAL_MEMBLOCK_SIZE,
				(OS_ERR*	)&err);
	//创建开始任务
	OSTaskCreate((OS_TCB 	* )&StartTaskTCB,		//任务控制块
				 (CPU_CHAR	* )"start task", 		//任务名字
                 (OS_TASK_PTR )start_task, 			//任务函数
                 (void		* )0,					//传递给任务函数的参数
                 (OS_PRIO	  )START_TASK_PRIO,     //任务优先级
                 (CPU_STK   * )&START_TASK_STK[0],	//任务堆栈基地址
                 (CPU_STK_SIZE)START_STK_SIZE/10,	//任务堆栈深度限位
                 (CPU_STK_SIZE)START_STK_SIZE,		//任务堆栈大小
                 (OS_MSG_QTY  )0,					//任务内部消息队列能够接收的最大消息数目,为0时禁止接收消息
                 (OS_TICK	  )0,					//当使能时间片轮转时的时间片长度,为0时为默认长度,
                 (void   	* )0,					//用户补充的存储区
                 (OS_OPT      )OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR, //任务选项
                 (OS_ERR 	* )&err);				//存放该函数错误时的返回值
	OS_CRITICAL_EXIT();	//退出临界区	 
	OSStart(&err);      //开启UCOSIII
}

void start_task(void *p_arg)//开始任务函数
{
	OS_ERR err;
	CPU_SR_ALLOC();
	p_arg = p_arg;
	
	CPU_Init();
#if OS_CFG_STAT_TASK_EN > 0u
   OSStatTaskCPUUsageInit(&err);  	//统计任务                
#endif
	
#ifdef CPU_CFG_INT_DIS_MEAS_EN		//如果使能了测量中断关闭时间
    CPU_IntDisMeasMaxCurReset();	
#endif
	
#if	OS_CFG_SCHED_ROUND_ROBIN_EN  //当使用时间片轮转的时候
	 //使能时间片轮转调度功能,时间片长度为1个系统时钟节拍,既1*5=5ms
	OSSchedRoundRobinCfg(DEF_ENABLED,1,&err);  
#endif	
		
	OS_CRITICAL_ENTER();	//进入临界区
	//创建主任务
	OSTaskCreate((OS_TCB*     )&Main_TaskTCB,		
				 (CPU_CHAR*   )"Main task", 		
                 (OS_TASK_PTR )main_task, 			
                 (void*       )0,					
                 (OS_PRIO	  )MAIN_TASK_PRIO,     
                 (CPU_STK*    )&MAIN_TASK_STK[0],	
                 (CPU_STK_SIZE)MAIN_STK_SIZE/10,	
                 (CPU_STK_SIZE)MAIN_STK_SIZE,		
                 (OS_MSG_QTY  )0,					
                 (OS_TICK	  )0,  					
                 (void*       )0,					
                 (OS_OPT      )OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR,
                 (OS_ERR*     )&err);
	//创建一个内存块检查任务
	OSTaskCreate((OS_TCB*     )&MemManage_TaskTCB,		
				 (CPU_CHAR*   )"MemManage task", 		
                 (OS_TASK_PTR )memmanage_task, 			
                 (void*       )0,					
                 (OS_PRIO	  )MEMMANAGE_TASK_PRIO,     
                 (CPU_STK*    )&MEMMANAGE_TASK_STK[0],	
                 (CPU_STK_SIZE)MEMMANAGE_STK_SIZE/10,	
                 (CPU_STK_SIZE)MEMMANAGE_STK_SIZE,		
                 (OS_MSG_QTY  )0,					
                 (OS_TICK	  )0,  					
                 (void*       )0,					
                 (OS_OPT      )OS_OPT_TASK_STK_CHK|OS_OPT_TASK_STK_CLR,
                 (OS_ERR*     )&err);
	OS_CRITICAL_EXIT();	//退出临界区
	OSTaskDel((OS_TCB*)0,&err);	//删除start_task任务自身
}

void main_task(void *p_arg)//主任务的任务函数
{
	u8 key,num;
	static u8 internal_memget_num;
	static u8 external_memget_num;
	CPU_INT08U *internal_buf;
	CPU_INT08U *external_buf;
	OS_ERR err;
	while(1)
	{
		key = KEY_Scan(0);  //扫描按键
		switch(key)
		{
			case WKUP_PRES:		//按下KEY_UP键
				internal_buf=OSMemGet((OS_MEM*)&INTERNAL_MEM,
								      (OS_ERR*)&err);
				if(err == OS_ERR_NONE) //内存申请成功
				{
					printf("internal_buf内存申请之后的地址为:%#x\r\n",(u32)(internal_buf));
					LCD_ShowString(30,180,200,16,16,"Memory Get success!  ");
					internal_memget_num++;
					POINT_COLOR = BLUE;
					sprintf((char*)internal_buf,"INTERNAL_MEM Use %d times",internal_memget_num);
					LCD_ShowString(30,196,200,16,16,internal_buf); 
					POINT_COLOR = RED;
				}
				if(err == OS_ERR_MEM_NO_FREE_BLKS) //内存块不足
				{
					LCD_ShowString(30,180,200,16,16,"INTERNAL_MEM Empty!  ");
				}
				break;
			case KEY1_PRES:
				if(internal_buf != NULL) //internal_buf不为空就释放内存
				{
					OSMemPut((OS_MEM*	)&INTERNAL_MEM,		//释放内存
							 (void*		)internal_buf,
							 (OS_ERR* 	)&err);
					printf("internal_buf内存释放之后的地址为:%#x\r\n",(u32)(internal_buf));
					LCD_ShowString(30,180,200,16,16,"Memory Put success!   ");
				}
				break;
			case KEY2_PRES:
				external_buf=OSMemGet((OS_MEM*)&EXTERNAL_MEM,
									  (OS_ERR*)&err);

				if(err == OS_ERR_NONE) //内存申请成功
				{
					printf("external_buf内存申请之后的地址为:%#x\r\n",(u32)(external_buf));
					LCD_ShowString(30,260,200,16,16,"Memory Get success!  ");
					external_memget_num++;
					POINT_COLOR = BLUE;
					sprintf((char*)external_buf,"EXTERNAL_MEM Use %d times",external_memget_num);
					LCD_ShowString(30,276,200,16,16,external_buf); 
					POINT_COLOR = RED;
				}
				if(err == OS_ERR_MEM_NO_FREE_BLKS) //内存块不足
				{
					LCD_ShowString(30,260,200,16,16,"EXTERNAL_MEM Empty!  ");
				}
				break;
			case KEY0_PRES:
				if(external_buf != NULL) //external_buf不为空就释放内存
				{
					OSMemPut((OS_MEM*	)&EXTERNAL_MEM,		//释放内存
							 (void*		)external_buf,
							 (OS_ERR* 	)&err);
					printf("external_buf内存释放之后的地址为:%#x\r\n",(u32)(external_buf));
					LCD_ShowString(30,260,200,16,16,"Memory Put success!   ");
				}
				break;
		}
	
		num++;
		if(num==50)
		{
			num=0;
			LED0 = ~LED0;
		}
		OSTimeDlyHMSM(0,0,0,10,OS_OPT_TIME_PERIODIC,&err);   //延时10ms
	}
}

void memmanage_task(void *p_arg)//内存管理任务
{
	OS_ERR err;
	LCD_ShowString(5,164,200,16,16,"Total:  Remain:");
	LCD_ShowString(5,244,200,16,16,"Total:  Remain:");
	while(1)
	{
		POINT_COLOR = BLUE;
		LCD_ShowxNum(53,164,INTERNAL_MEM.NbrMax,1,16,0);	
		LCD_ShowxNum(125,164,INTERNAL_MEM.NbrFree,1,16,0);	
		LCD_ShowxNum(53,244,EXTERNAL_MEM.NbrMax,1,16,0);	
		LCD_ShowxNum(125,244,EXTERNAL_MEM.NbrFree,1,16,0);	
		POINT_COLOR = RED;
		OSTimeDlyHMSM(0,0,0,100,OS_OPT_TIME_PERIODIC,&err);//延时100ms
	}
}
相关TAG标签
上一篇:WIN10远程桌面连接步骤:CredSSP加密Oracle修正
下一篇:H5实现禁止弹出手机自带的软键盘控制键盘输入手机号的方法
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训 | 举报中心

版权所有: 红黑联盟--致力于做实用的IT技术学习网站