当前位置: 首页 > news >正文

FreeRTOS_信号量之互斥信号量

目录

1. 互斥信号量

1.1 互斥信号量简介

1.2 创建互斥信号量

1.2.1 函数 xSemaphoreCreateMutex()

1.2.2 函数 xSemaphoreCreateMutexStatic()

1.2.3 互斥信号量创建过程分析

1.2.4 释放互斥信号量

1.2.5 获取互斥信号量

2. 互斥信号量操作实验

2.1 实验程序

2.1.1 main.c

2.1.2 实验现象


1. 互斥信号量

1.1 互斥信号量简介

        互斥信号量其实就是一个拥有优先级继承的二值信号量,在同步的应用中(任务与任务或中断与任务之间的同步)二值信号量最适合。互斥信号量适合用于那些需要互斥访问的应用中。在互斥访问中互斥信号量相当于一把钥匙,当任务想要使用资源的时候就必须先获得这个钥匙,当使用完资源以后就必须归还这个钥匙,这样其他的任务就可以拿着这个钥匙去使用资源。

        互斥信号量使用和二值信号量相同的 API 操作函数,所以互斥信号量也可以设置阻塞时间,不同于二值信号量的是互斥信号量具有优先级继承的特性。当一个互斥信号量正在被一个低优先级的任务使用,而此时有个高优先级的任务也尝试获取这个互斥信号量的话就会被阻塞。这个高优先级的任务会将低优先级任务的优先级提升到与自己相同的优先级,这个过程就是优先级继承。优先级继承尽可能的降低了高优先级任务处于阻塞态的时间,并且将已经出现的 “优先级翻转” 的影响降到最低。

优先级继承尽可能的降低了高优先级任务处于阻塞态的时间,并且将已经出现的 “优先级翻转” 的影响降到最低。
       
意思就是说:低优先级的任务获得互斥信号量,此时高优先级的任务无法访问获得互斥信号量,其他中等优先级的任务也无法获得互斥信号量,这样一来,低优先级任务就不能被中等优先级任务所打断,高优先级任务只需要等待低优先级任务释放互斥信号量即可,不用担心被其他的中等任务所打断;这也就是为什么降低高优先级任务处于阻塞态的时间,降低了优先级翻转的可能性!

        优先级继承并不能完全的消除优先级翻转,它只是尽可能的降低优先级翻转带来的影响。硬实时应用应该在设计之初就要避免优先级翻转的发生。

互斥信号量不能用于中断服务函数中:

        互斥信号量具有优先级继承的机制,只能用在任务中,不能用于中断服务函数。

        中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。

1.2 创建互斥信号量

        FreeRTOS提供两个互斥信号量创建函数。

函数:

        xSemaphoreCreateMutex()                使用动态方法创建互斥信号量

        xSemaphoreCreateMutexStatic()        使用静态方法创建互斥信号量

1.2.1 函数 xSemaphoreCreateMutex()

        此函数用于创建一个互斥信号量,所需要的内存通过动态内存管理方法分配。此函数本质是一个宏,真正完成信号量创建的是函数 xQueueCreateMutex(),此函数原型如下:

SemaphoreHandle_t xSemaphoreCreateMutex(void)

参数:

        无。

返回值:

        NULL:互斥信号量创建失败。

        其他值:创建成功的互斥信号量的句柄。

1.2.2 函数 xSemaphoreCreateMutexStatic()

        此函数也是创建互斥信号量的,只不过使用此函数创建互斥信号量的话信号量所需要的 RAM 需要由用户来分配,此函数是一个宏,具体创建过程是通过函数 xQueueCreateMutexStatic() 来完成的,函数原型如下:

SemaphoreHandle_t xSemaphoreCreateMutexStatic(StaticSemaphore_t *pxMutexBuffer)

参数:

        pxMutexBuffer:此参数指向一个 StaticSemaphore_t 类型的变量,用来保存信号量结构体。

返回值:

        NULL:互斥信号量创建失败。

        其他值:创建成功的互斥信号量的句柄。

1.2.3 互斥信号量创建过程分析

        这里只分析动态创建互斥信号量函数 xSemaphoreCreateMutex(),此函数是个宏,定义如下:

#define  xSemaphoreCreateMutex()  xQueueCreateMutex(queueQUEUE_TYPE_MUTEX)

        可以看出,真正干事的是函数 xQueueCreateMutex(),此函数在文件 queue.c 中有如下定义,

QueueHandle_t xQueueCreateMutex( const uint8_t ucQueueType ) 
{ Queue_t *pxNewQueue; const UBaseType_t uxMutexLength = ( UBaseType_t ) 1, uxMutexSize = ( UBaseType_t ) 0; pxNewQueue = ( Queue_t * ) xQueueGenericCreate( uxMutexLength, uxMutexSize,     (1) ucQueueType ); prvInitialiseMutex( pxNewQueue );                                               (2) return pxNewQueue; 
}

(1)、调用函数 xQueueGenericCreate() 创建一个队列,队列长度为 1 ,队列项长度为 0 ,队列类型为参数 ucQueueType。由于本函数创建的是互斥信号量,所以参数 ucQueueType 为 queueQUEUE_TYPE_MUTEX。

(2)、调用函数 prvInitialiseMutex() 初始化互斥信号量。

函数 prvInitialiseMutex() 初始化互斥信号量代码如下:

static void prvInitialiseMutex( Queue_t *pxNewQueue ) 
{ if( pxNewQueue != NULL ) { //虽然创建队列的时候会初始化队列结构体的成员变量,但是此时创建的是互斥 //信号量,因此有些成员变量需要重新赋值,尤其是那些用于优先级继承的。 pxNewQueue->pxMutexHolder = NULL;                                 (1) pxNewQueue->uxQueueType = queueQUEUE_IS_MUTEX;                     (2) //如果是递归互斥信号量的话。 pxNewQueue->u.uxRecursiveCallCount = 0;                             (3) traceCREATE_MUTEX( pxNewQueue ); //释放互斥信号量 ( void ) xQueueGenericSend( pxNewQueue, NULL, ( TickType_t ) 0U,queueSEND_TO_BACK ); } else { traceCREATE_MUTEX_FAILED(); } 
} 

(1)和(2)、队列结构体中 Queue_t 中没有 pxMutexHolder 和 uxQueueType 这两个成员变量,这两个成员变量其实是一个宏,专门为互斥信号量准备的,在文件 queue.c 中有如下定义:

#define pxMutexHolder    pcTail
#define uxQueueType      pcHead
#define queueQUEUE_IS_MUTEX    NULL

        当 Queue_t 用于表示队列的时候 pcHead 和 pcTail 指向队列的存储区域,当 Queue_t 用于表示互斥信号量的时候就不需要 pcHead 和 pcTail 了。当用于互斥信号量的时候将 pcHead 指向 NULL 来表示 pcTail 保存着互斥队列的所有者,pxMutexHolder 指向拥有互斥信号量的那个任务的任务控制块。重命名 pcTail 和 pcHead 是为了增强代码的可读性。

(3)、如果创建的信号量是递归互斥信号量的话,还需要初始化队列结构体中的成员变量 u.uxRecursiveCallCount。

        互斥信号量创建成功以后会调用函数 xQueueGenericSend() 释放一次信号量,说明互斥信号量默认就是有效的!

1.2.4 释放互斥信号量

        释放互斥信号量的时候和二值信号量、计数型信号量一样,都是用的函数 xSemaphoreGive()(实际上完成信号量释放的是函数 xQueueGenericSend())。由于互斥信号量涉及到优先级继承的问题,所以具体的处理过程会有区别。使用函数 xSemaphoreGive() 释放信号量最重要的一步就是将 uxMessageWaiting 加一,而这一步就是通过函数 prvCopyDataToQueue() 来完成的,释放信号量的函数 xQueueGenericSend() 会调用 prvCopyDataToQueue()。互斥信号量的优先级继承也是在函数 prvCopyDataToQueue() 中完成的,此函数有如下一段代码:

static BaseType_t prvCopyDataToQueue( Queue_t * const pxQueue, const void * pvItemToQueue, const BaseType_t xPosition ) 
{ BaseType_t xReturn = pdFALSE; UBaseType_t uxMessagesWaiting; uxMessagesWaiting = pxQueue->uxMessagesWaiting; if( pxQueue->uxItemSize == ( UBaseType_t ) 0 ) { #if ( configUSE_MUTEXES == 1 ) //互斥信号量 { if( pxQueue->uxQueueType == queueQUEUE_IS_MUTEX )             (1) { xReturn = xTaskPriorityDisinherit( ( void * ) pxQueue->pxMutexHolder );                        (2) pxQueue->pxMutexHolder = NULL;                            (3) } else { mtCOVERAGE_TEST_MARKER(); } } #endif /* configUSE_MUTEXES */ } /*********************************************************************/ /*************************省略掉其他处理代码**************************/ /*********************************************************************/ pxQueue->uxMessagesWaiting = uxMessagesWaiting + 1; return xReturn; 
} 

(1)、当前操作的是互斥信号量。

(2)、调用函数 xTaskPriorityDisinherit() 处理互斥信号量的优先级继承问题。

(3)、互斥信号量释放以后,互斥信号量就不属于任何任务了,所以 pxMutexHolder 要指向 NULL。

函数 xTaskPriorityDisinherit() 代码如下:

BaseType_t xTaskPriorityDisinherit( TaskHandle_t const pxMutexHolder ) 
{ TCB_t * const pxTCB = ( TCB_t * ) pxMutexHolder; BaseType_t xReturn = pdFALSE; if( pxMutexHolder != NULL )                                         (1) { //当一个任务获取到互斥信号量以后就会涉及到优先级继承的问题,正在释放互斥 //信号量的任务肯定是当前正在运行的任务 pxCurrentTCB。 configASSERT( pxTCB == pxCurrentTCB ); configASSERT( pxTCB->uxMutexesHeld ); ( pxTCB->uxMutexesHeld )--;                                     (2) //是否存在优先级继承?如果存在的话任务当前优先级肯定和任务基优先级不同。if( pxTCB->uxPriority != pxTCB->uxBasePriority )                 (3) { //当前任务只获取到了一个互斥信号量 if( pxTCB->uxMutexesHeld == ( UBaseType_t ) 0 )                 (4) { if( uxListRemove( &( pxTCB->xStateListItem ) ) == ( UBaseType_t ) 0 )         (5) { taskRESET_READY_PRIORITY( pxTCB->uxPriority );         (6) } else { mtCOVERAGE_TEST_MARKER(); } //使用新的优先级将任务重新添加到就绪列表中 traceTASK_PRIORITY_DISINHERIT( pxTCB, pxTCB->uxBasePriority ); pxTCB->uxPriority = pxTCB->uxBasePriority; (7) /* Reset the event list item value. It cannot be in use for any other purpose if this task is running, and it must be running to give back the mutex. */ listSET_LIST_ITEM_VALUE( &( pxTCB->xEventListItem ), \             (8) ( TickType_t ) configMAX_PRIORITIES - \ ( TickType_t ) pxTCB->uxPriority ); prvAddTaskToReadyList( pxTCB );                                 (9) xReturn = pdTRUE;                                                 (10) } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } } else { mtCOVERAGE_TEST_MARKER(); } return xReturn; 
}

(1)、函数的参数 pxMutexHolder 表示拥有此互斥信号量任务控制块,所以要先判断此互斥信号量是否已经被其他任务获取。

(2)、有的任务可能会获取多个互斥信号量,所以就需要标记任务当前获取到的互斥信号量个数,任务控制块结构体成员变量 uxMutexesHeld 用来保存当前任务获取到的互斥信号量个数。任务每释放一次互斥信号量,变量 uxMutexesHeld 肯定就要减一。

(3)、判断是否存在优先级继承,如果存在的话任务的当前优先级肯定不等于任务的基优先级(任务的基优先级是指任务在创建时分配的固定优先级。它是一个用于调度任务的数值,数值越高,优先级越高)

(4)、判断当前释放的是不是任务所获取到的最后一个互斥信号量,因为如果任务还获取了其他互斥信号量的话就不能处理优先级继承。优先级继承的处理必须是在释放最后一个互斥信号量的时候。

(5)、优先级继承的处理说白了就是把任务的当前优先级降低到任务的基优先级,所以要把当前任务先从任务就绪表中移除。当任务优先级恢复为原来的优先级以后再重新加入到就绪表中。

FreeRTOS 是一种实时操作系统 (RTOS),用于嵌入式系统的开发。优先级继承是一种调度策略,用于解决优先级翻转问题。

在多任务系统中,任务可以具有不同的优先级,优先级较高的任务可以抢占优先级较低的任务。然而,当存在任务依赖关系时,可能会出现优先级反转问题。

优先级翻转问题是当一个具有较低优先级的任务占用一个共享资源时,一个具有较高优先级的任务因为等待该资源而被阻塞。此时,更高优先级任务无法运行,从而导致系统的响应性能下降。

通过优先级继承,当一个高优先级任务需要使用一个低优先级任务占用的资源时,低优先级任务的优先级会临时提升至与高优先级任务相同的优先级,以确保高优先级任务能够尽快获得所需资源,当高优先级任务释放该资源后,低优先级任务的优先级恢复原状。

(6)、如果任务继承来的这个优先级对应的就绪表中没有其他任务的话就将取消这个优先级的就绪态。

(7)、重新设置任务的优先级为任务的基优先级 uxBasePriority。

(8)、复位任务的事件列表项。

(9)、将优先级恢复后的任务重新添加到任务就绪表中。

(10)、返回 pdTRUE,表示需要进行任务调度。

1.2.5 获取互斥信号量

        获取互斥信号量的函数同获取二值信号量和计数型信号量的函数相同,都是 xSemaphoreTake()(实际执行信号量获取的函数是 xQueueGenericReceive()),获取互斥信号量的过程也需要处理优先级继承的问题,函数 xQueueGenericReceive()在文件 queue.c 中有定义)

2. 互斥信号量操作实验

本实验设计了四个任务:start_task、high_task、middle_task、low_task,这四个任务的任务功能如下:

        start_task:用来创建其他的三个任务。

        high_task:高优先级任务,会获取互斥信号量,获取成功以后会进行相应的处理,处理完成以后就会释放互斥信号量。

        middle_task:中等优先级任务,一个简单的应用任务。

        low_task:低优先级任务,和高优先级任务一样,会获取互斥信号量,获取成功以后会进行相应的处理,不过不同之处在于低优先级的任务占用互斥信号量的时间要久一点(软件模拟占用)。

        实验中创建了一个互斥信号量 MutexSemaphore,高优先级和低优先级这两个任务会使用这个互斥信号量。

2.1 实验程序

2.1.1 main.c

#include "stm32f4xx.h"  
#include "FreeRTOS.h" //这里注意必须先引用FreeRTOS的头文件,然后再引用task.h
#include "task.h"     //存在一个先后的关系
#include "LED.h"
#include "LCD.h"
#include "Key.h"
#include "usart.h"
#include "delay.h"
#include "string.h"
#include "beep.h"
#include "malloc.h"
#include "timer.h"
#include "queue.h"
#include "semphr.h"//任务优先级
#define START_TASK_PRIO     1       //用于创建其他三个任务
//任务堆栈大小
#define START_STK_SIZE      256
//任务句柄
TaskHandle_t StartTask_Handler;
//任务函数
void start_task(void *pvParameters);//任务优先级
#define LOW_TASK_PRIO     2       //低优先级任务,会获取互斥信号量,获取成功以后进行相应的处理,占用互斥信号量的时间要久一点
//任务堆栈大小 
#define LOW_STK_SIZE      256
//任务句柄
TaskHandle_t LowTask_Handler;
//任务函数
void low_task(void *pvParameters);//任务优先级
#define MIDDLE_TASK_PRIO     3       //中等优先级任务,一个简单的应用任务
//任务堆栈大小
#define MIDDLE_STK_SIZE      256
//任务句柄
TaskHandle_t MiddleTask_Handler;
//任务函数
void middle_task(void *pvParameters);//任务优先级
#define HIGH_TASK_PRIO     4       //高优先级任务,会获取互斥信号量,获取成功以后会进行相应的处理,处理完成以后会释放互斥信号量
//任务堆栈大小
#define HIGH_STK_SIZE      256
//任务句柄
TaskHandle_t HighTask_Handler;
//任务函数
void high_task(void *pvParameters);//互斥信号量句柄
SemaphoreHandle_t MutexSemaphore;   //互斥信号量//LCD刷屏时使用的颜色
int lcd_discolor[14]={	WHITE, BLACK, BLUE,  BRED,      GRED,  GBLUE, RED,   MAGENTA,       	 GREEN, CYAN,  YELLOW,BROWN, 			BRRED, GRAY };int main(void)
{NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4);  delay_init(168);uart_init(115200);LED_Init();KEY_Init();BEEP_Init();LCD_Init();my_mem_init(SRAMIN);        //初始化内部内存池POINT_COLOR=RED;LCD_ShowString(30,10,200,16,16,"ATK STM32F407");LCD_ShowString(30,30,200,16,16,"FreeRTOS Example");LCD_ShowString(30,50,200,16,16,"Mutex Semaphore");LCD_ShowString(30,70,200,16,16,"ATOM@ALIENTEK");LCD_ShowString(30,90,200,16,16,"2023/10/31");//创建开始任务xTaskCreate((TaskFunction_t)start_task,         //任务函数(const char*   )"start_task",       //任务名称(uint16_t      )START_STK_SIZE,     //任务堆栈大小(void*         )NULL,               //传递给任务函数的参数(UBaseType_t   )START_TASK_PRIO,    //任务优先级(TaskHandle_t* )&StartTask_Handler);//任务句柄vTaskStartScheduler();          //开启任务调度
}//开始任务任务函数
void start_task(void *pvParameters)
{taskENTER_CRITICAL();       //进入临界区//创建互斥信号量MutexSemaphore=xSemaphoreCreateMutex();  //创建互斥信号量函数,返回创建成功的互斥信号量句柄//创建高优先级任务xTaskCreate((TaskFunction_t)high_task,         //任务函数(const char*   )"high_task",       //任务名称(uint16_t      )HIGH_STK_SIZE,     //任务堆栈大小(void*         )NULL,               //传递给任务函数的参数(UBaseType_t   )HIGH_TASK_PRIO,    //任务优先级(TaskHandle_t* )&HighTask_Handler);//任务句柄//创建中等优先级任务xTaskCreate((TaskFunction_t)middle_task,         //任务函数(const char*   )"middle_task",       //任务名称(uint16_t      )MIDDLE_STK_SIZE,     //任务堆栈大小(void*         )NULL,               //传递给任务函数的参数(UBaseType_t   )MIDDLE_TASK_PRIO,    //任务优先级(TaskHandle_t* )&MiddleTask_Handler);//任务句柄//创建低优先级任务xTaskCreate((TaskFunction_t)low_task,         //任务函数(const char*   )"low_task",       //任务名称(uint16_t      )LOW_STK_SIZE,     //任务堆栈大小(void*         )NULL,               //传递给任务函数的参数(UBaseType_t   )LOW_TASK_PRIO,    //任务优先级(TaskHandle_t* )&LowTask_Handler);//任务句柄vTaskDelete(StartTask_Handler);taskEXIT_CRITICAL();            //退出临界区
}//高优先级任务任务函数
void high_task(void *pvParameters)
{u8 num;POINT_COLOR=BLACK;LCD_DrawRectangle(5,110,115,314);   //画一个矩形LCD_DrawLine(5,130,115,130);        //画线POINT_COLOR=BLUE;LCD_ShowString(6,111,110,16,16,"High Task");while(1){vTaskDelay(500);   //延时500ms,也就是500个时钟节拍num++;printf("high task Pend Semaphore\r\n");xSemaphoreTake(MutexSemaphore,portMAX_DELAY);    //获取互斥信号量//获取互斥信号量的阻塞时间设置为无限等待,既然这个任务可以获取互斥信号量,那么总有一个时刻可以获取到互斥信号量//否则程序就会卡在这里printf("high task Running!\r\n");   //获取到互斥信号量,高优先级任务开始运行LCD_Fill(6,131,114,313,lcd_discolor[num%14]);   //填充区域LED1=!LED1;xSemaphoreGive(MutexSemaphore);           //释放互斥信号量,当高优先级任务获取互斥信号量完成相应的处理之后,就会释放掉信号量vTaskDelay(500);                //延时500ms,也就是500个时钟节拍}
}//中等优先级任务的任务函数
void middle_task(void *pvParameters)
{u8 num;POINT_COLOR=BLACK;LCD_DrawRectangle(125,110,234,314);     //画一个矩形LCD_DrawLine(125,130,234,130);      //画线POINT_COLOR=BLUE;LCD_ShowString(126,111,110,16,16,"Middle Task");while(1){num++;printf("middle task Running!\r\n");LCD_Fill(126,131,233,313,lcd_discolor[13-num%14]);  //倒过来填充区域LED0=!LED0;vTaskDelay(1000);       //延时1s,也就是1000个时钟节拍}
}//低优先级任务的任务函数
//低优先级任务占用互斥信号量的时间更长
void low_task(void *pvParameters)
{static u32 times;while(1){xSemaphoreTake(MutexSemaphore,portMAX_DELAY);  //获取二值信号量printf("low task Running!\r\n");            for(times=0;times<20000000;times++)         //模拟低优先级占用二值信号量{taskYIELD();            //发起任务调度//这也就保证了低优先级任务占用二值信号量的时间更长//因为我一旦发起了任务调度,低优先级抢占的这个二值信号量是不能被高优先级的任务所抢占的}xSemaphoreGive(MutexSemaphore);           //释放二值信号量vTaskDelay(1000);       //延时1s,也就是1000个时钟节拍}
}

2.1.2 实验现象

通过上述串口输出的数据进行分析!

        首先高优先级任务存在延时,所以中等优先级任务抢占 CPU,中等优先级任务运行一个时间片之后,低优先级任务获取互斥信号量,高优先级任务延时时间到以后抢占CPU使用权,高优先级任务请求信号量,等待低优先级任务释放互斥信号量,此时中等优先级任务不会运行,因为互斥信号量优先级继承的缘故,低优先级任务临时获得和高优先级任务同等的优先级,等待低优先级任务释放互斥信号量之后,高优先级任务得以运行,高优先级任务释放信号量之后,任务调度,中等优先级任务运行,高优先级任务请求信号量,此时信号量被高优先级任务获取,高优先级任务运行,然后中等任务运行,低优先级任务运行!!!

互斥信号量的精髓在于:低优先级任务正在使用互斥信号量,而高优先级任务请求使用互斥信号量,此时会临时的将低优先级任务的优先级提高到和高优先级任务一个层次,这时被提升上来的任务就不会被其他优先级任务所打断,最大程度保证高优先级任务尽快获得互斥信号量,提高系统的响应性能。当高优先级任务释放信号量之后,被提升的任务回到之前的优先级。

相关文章:

  • 【SA8295P 源码分析 (一)】114 - 将Android GVM userdata文件系统从 EXT4 修改为 F2FS
  • PyG edge index 转换回 邻接矩阵
  • element-plus的el-tag标签关闭标签时的高亮显示逻辑
  • Ubuntu GCC切换源
  • echarts 地图迁徙与地图下钻
  • MySQL教程笔记
  • SpringBoot / Vue 对SSE的基本使用
  • springboot整合日志,并在本地查看
  • [PHP]pearProject协作系统 v2.8.14 前后端
  • Elasticsearch 集群分片出现 unassigned 其中一种原因详细还原
  • RSA 加密算法的原理与加密过程深度解析(下篇)
  • java.lang.NoClassDefFoundError: javax/servlet/Filter
  • hive sql 遇到的一些函数使用
  • elementui el-upload 上传文件
  • 引擎系统设计思路 - 用户态与系统态隔离
  • php的引用
  • CentOS6 编译安装 redis-3.2.3
  • Docker 笔记(2):Dockerfile
  • iOS仿今日头条、壁纸应用、筛选分类、三方微博、颜色填充等源码
  • JavaScript类型识别
  • js ES6 求数组的交集,并集,还有差集
  • js递归,无限分级树形折叠菜单
  • Quartz实现数据同步 | 从0开始构建SpringCloud微服务(3)
  • vue:响应原理
  • 闭包--闭包作用之保存(一)
  • 从零开始学习部署
  • 规范化安全开发 KOA 手脚架
  • 两列自适应布局方案整理
  • 思考 CSS 架构
  • 王永庆:技术创新改变教育未来
  • 小程序开发中的那些坑
  • 移动端唤起键盘时取消position:fixed定位
  • - 语言经验 - 《c++的高性能内存管理库tcmalloc和jemalloc》
  • 如何在 Intellij IDEA 更高效地将应用部署到容器服务 Kubernetes ...
  • ​软考-高级-信息系统项目管理师教程 第四版【第19章-配置与变更管理-思维导图】​
  • #DBA杂记1
  • #ubuntu# #git# repository git config --global --add safe.directory
  • (C语言)输入一个序列,判断是否为奇偶交叉数
  • (html5)在移动端input输入搜索项后 输入法下面为什么不想百度那样出现前往? 而我的出现的是换行...
  • (二开)Flink 修改源码拓展 SQL 语法
  • (亲测成功)在centos7.5上安装kvm,通过VNC远程连接并创建多台ubuntu虚拟机(ubuntu server版本)...
  • (一) storm的集群安装与配置
  • (原創) 是否该学PetShop将Model和BLL分开? (.NET) (N-Tier) (PetShop) (OO)
  • (轉)JSON.stringify 语法实例讲解
  • .bat批处理(九):替换带有等号=的字符串的子串
  • .locked1、locked勒索病毒解密方法|勒索病毒解决|勒索病毒恢复|数据库修复
  • .Net 转战 Android 4.4 日常笔记(4)--按钮事件和国际化
  • .NET6实现破解Modbus poll点表配置文件
  • .net与java建立WebService再互相调用
  • .Net中wcf服务生成及调用
  • [ 云计算 | Azure 实践 ] 在 Azure 门户中创建 VM 虚拟机并进行验证
  • [\u4e00-\u9fa5] //匹配中文字符
  • [].shift.call( arguments ) 和 [].slice.call( arguments )
  • [100天算法】-x 的平方根(day 61)
  • [20150321]索引空块的问题.txt