信号量是操作系统中重要的一部分,信号量一般用来进行资源管理和任务同步。信号量分为二值信号量、计数型信号量、互斥信号量不同信号量的应用场景也不同,但是有些应用场景是可以互换着使用的。

信号量简介

信号量常常用于控制对共享资源的访问和任务同步。信号量用于控制共享资源访问的场景相当于一个上锁机制,代码只有获得这个锁的钥匙才能执行。信号量也用于任务与任务和中断与任务之间的同步。在执行中断服务函数的时候可以通过向任务发送信号量来通知任务它所期待的事件发生了,当退出中断服务函数以后在任务调度器的调度下同步的任务就会执行。任务和任务之间也可以通过信号量来完成同步。

二值信号量

        二值信号量通常用于互斥访问或同步,二值信号量和互斥信号量非常类似,但是互斥信号量拥有优先级继承机制,二值信号量没有优先级继承。因此二值信号量更适合用于同步(任务与任务或任务与中断的同步),而互斥信号量适用于简单的互斥访问。

和队列一样,信号量API函数允许设置一个阻塞时间,阻塞时间是当任务获取信号量的时候由于信号量无效从而导致任务进入阻塞态的最大时钟节拍数。如果多个任务同时阻塞在同一个信号量上的话,那么优先级最高的那个任务优先获取信号量,这样当信号量有效的时候高优先级的任务就会解除阻塞状态。

1.xSemaphoreCreateBinary() 二值信号量创建

通过此函数创建二值信号量,信号量所需的RAM是由FreeRTOS动态分配的。此函数创建好的二值信号量默认是空的,也就是说使用xSemaphoreTake()是获取不到的

函数原型:

#define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )

参数:无

返回值:

NULL 失败

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

参考例子:

SemaphoreHandle_t xSemaphore = NULL;

xSemaphore = xSemaphoreCreateBinary();

可以看到二值信号量的创建,底层调用函数xQueueGenericCreate()来创建一个类型为queueQUEUE_TYPE_BINARY_SEMAPHORE,长度为1,队列项长度为0的队列。说明二值信号量底层是队列。创建的队列是个没有存储区的队列,使用队列是否为空来表示二值信号量,队列是否为空可以通过队列结构体的成员变量uxMessagesWaiting来判断。

2.xSemaphoreGive()释放二值信号量

此函数用于释放二值信号量、计数型信号量或互斥信号量。

函数原型:

#define xSemaphoreGive( xSemaphore ) xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )

参数:

xSemaphore :要释放的信号量句柄

返回值:

pdPASS:释放成功

errQUEUE_FULL:释放失败。

实例:

if( xSemaphoreGive( xSemaphore ) != pdTRUE ) //失败

从代码可以看到释放二值信号量,是向队列入队一个内容为空的队列项。入队后结构体成员变量uxMessagesWaiting会加一,则判断信号量有效。

3.xSemaphoreGiveFromISR()中断释放信号量

此函数用于在中断中释放信号量,此函数只能用来释放二值信号量和技术型信号量,绝对不能用来在中断服务函数中释放互斥信号量。因为互斥信号量涉及到优先级继承的问题,而中断不属于任务,无法处理中断优先级继承。

函数原型:

#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken ) xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), ( pxHigherPriorityTaskWoken ) )

参数:

xSemaphore:要释放的信号量句柄

pxHigherPriorityTaskWoken:退出中断以后是否进行任务切换。如果此值为pdTRUE,在对出中断服务函数之前一定要进行一次任务切换。

返回值:

pdPASS:释放成功

errQUEUE_FULL:释放失败

实例:

static BaseType_t xHigherPriorityTaskWoken;

xHigherPriorityTaskWoken = pdFALSE;

xSemaphoreGiveFromISR( xSemaphore, &xHigherPriorityTaskWoken );

4.xSemaphoreTake() 获取信号量

此函数获取二值信号量、计数型信号量或互斥信号量

函数原型:

#define xSemaphoreTake( xSemaphore, xBlockTime ) xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )

参数:

xSemaphore:要获取的信号量句柄

xBlockTime :阻塞时间

返回值:

pdTRUE:成功

pdFALSE:失败

实例:

if( xSemaphoreTake( xSemaphore, ( TickType_t ) 10 ) == pdTRUE )

5.xsemaphoreTakeFromISR()中断获取信号量

此函数用于在中断服务函数中获取信号量,此函数用于获取二值信号量和计数型信号量,绝对不能使用此函数来获取互斥信号量。

函数原型:

#define xSemaphoreTakeFromISR( xSemaphore, pxHigherPriorityTaskWoken ) xQueueReceiveFromISR( ( QueueHandle_t ) ( xSemaphore ), NULL, ( pxHigherPriorityTaskWoken ) )

参数:

xSemaphore:要获取的信号量句柄

pxHigherPriorityTaskWoken:退出中断以后是否进行任务切换。如果此值为pdTRUE,在对出中断服务函数之前一定要进行一次任务切换。

返回值:

pdTRUE:成功

pdFALSE:失败

程序验证:

创建两个任务,TASK1和TASK2。在TASK1中创建一个二值信号量,并且开始等待。TASK2中200ms周期释放一次信号量

任务一

static void vTestTask1(void *pvParameters){xSemaphore = xSemaphoreCreateBinary();if(xSemaphore == NULL)   {LOG_I(common,"[TASK1]:BinSemap create fail");  //失败}else{LOG_I(common,"[TASK1]:BinSemap create sucess");  //成功}static uint32_t cnt = 0;while (1) {if(xSemaphore != NULL)  //创建成功{cnt++;LOG_I(common,"[TASK1]:BinSemap before take");  if(xSemaphoreTake(xSemaphore,100) == pdTRUE){LOG_I(common,"[TASK1]:BinSemap take sucess");  //成功}else{LOG_I(common,"[TASK1]:BinSemap take fail,timeout");  //失败}}LOG_I(common,"testtask1,left size:%d,TCB:%d",uxTaskGetStackHighWaterMark(NULL),app_get_TCB_size());}
}

任务二

static void vTestTask2(void *pvParameters)
{uint8_t print_arr[20] = {0};while(1){if(xSemaphoreGive(xSemaphore) == pdTRUE){LOG_I(common,"[TASK2]:BinSemap give  success");  //成功}else{LOG_I(common,"[TASK2]:BinSemap give fail");  //失败}vTaskDelay(200);}
}

结果:

结果分析

  1. 任务1创建二值信号量成功
  2. 任务1获取信号量(无果,等待)
  3. 任务2释放信号量
  4. 任务1获取信号量成功
  5. 任务1获取信号量(无果,等待)
  6. 任务1等待超时
  7. 任务1获取信号量(无果,等待)
  8. 任务2释放信号量
  9. 任务1获取信号量成功

从结果可以看到,二值信号量在创建后,无法被立刻获取,必须等待释放一次后,才能被获取。

计数型信号量

计数型信号量也叫作数值信号量,二值信号量相当于长度为1的队列,那么计数型信号量就是长度大于1的队列。同二值信号量一样,用户不需要关心队列中存储了什么数据,只需要关心队列是否为空即可。计数型信号量通常用于如下两个场合:

1.事件计数

在这个场合中,每次事件发生的时候就在事件处理函数中释放信号量(增加信号量的计数值),其他任务会获取信号量(信号量计数值减一)来处理事件。在这种场合中创建的计数型信号量初始值为0.

2.资源管理

在这个场合中,信号量值代表当前资源的可用数量。一个任务想要获得资源的使用权,首先必须获取信号量,信号量获取成功以后信号量值就会减一。当信号量值为0的时候说明没有资源了。当一个任务使用完资源以后一定要释放信号量,释放信号量以后信号量值会加一。在这个场合中创建的计数型信号量初始值应该是资源的数量

1.xSemaphoreCreateCounting()创建计数型信号量(动态内存)

此函数用于创建一个计数型信号量,所需要的内存通过动态内存管理方法分配。

函数原型:

#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )

参数:

uxMaxCount:计数信号量最大计数值,当信号量值等于此值的时候释放信号量就会失败。

uxInitialCount :计数信号量初始值

返回值:

NULL:创建失败

其他值,创建成功,返回信号量句柄。

实例:

SemaphoreHandle_t xSemaphore;

xSemaphore = xSemaphoreCreateCounting( 10, 0 );

2.xSemaphoreCreateCountingStatic()创建计数型信号量(静态内存)

此函数用于创建一个计数型信号量,所需要的内存由用户分配。

函数原型:

#define xSemaphoreCreateCountingStatic( uxMaxCount, uxInitialCount, pxSemaphoreBuffer ) xQueueCreateCountingSemaphoreStatic( ( uxMaxCount ), ( uxInitialCount ), ( pxSemaphoreBuffer ) )

参数:

uxMaxCount:计数信号量最大计数值,当信号量值等于此值的时候释放信号量就会失败。

uxInitialCount :计数信号量初始值

pxSemaphoreBuffer :指向一个staticSemaphore_t类型的变量,用来保存信号量结构体

返回值:

NULL:创建失败

其他值,创建成功,返回信号量句柄。

实例:

StaticSemaphore_t xSemaphoreBuffer;

SemaphoreHandle_t xSemaphore = NULL;

xSemaphore = xSemaphoreCreateCountingStatic( 10, 0, &xSemaphoreBuffer );

代码验证:事件计数

创建两个任务,任务1创建一个计数信号量,等待信号量,延迟100ms。任务2间隔200ms释放一个计数信号量。

任务1

static void vTestTask1(void *pvParameters)
{xSemaphore = xSemaphoreCreateCounting(10,0);    //计数信号量if(xSemaphore == NULL)   {LOG_I(common,"[TASK1]:CounSemap create fail");  //失败}else{LOG_I(common,"[TASK1]:CounSemap create sucess");  //成功}static uint32_t cnt = 0;while (1) {if(xSemaphore != NULL)  //创建成功{cnt++;LOG_I(common,"[TASK1]:CounSemap before take");  if(xSemaphoreTake(xSemaphore,100) == pdTRUE){LOG_I(common,"[TASK1]:CounSemap take sucess");  //成功}else{LOG_I(common,"[TASK1]:BinSemap take fail,timeout");  //失败}}LOG_I(common, "test task1,left size:%d,TCB:%d",uxTaskGetStackHighWaterMark(NULL),app_get_TCB_size());}
}

任务2

static void vTestTask2(void *pvParameters)
{uint8_t print_arr[20] = {0};while(1){if(xSemaphoreGive(xSemaphore) == pdTRUE){LOG_I(common,"[TASK2]:CounSemap give  success");  //成功}else{LOG_I(common,"[TASK2]:CounSemap give fail");  //失败}vTaskDelay(200);}
}

结果

结果分析

  1. 任务1创建计数信号量成功,计数最大值10,初始值0.
  2. 任务1获取信号量(无果,等待)
  3. 任务2释放信号量
  4. 任务1获取信号量成功
  5. 任务1获取信号量(无果,等待)
  6. 任务1等待超时
  7. 任务1获取信号量(无果,等待)
  8. 任务2释放信号量
  9. 任务1获取信号量成功

从结果可以看出,计数信号量初始值为0,在创建成功后是无法被直接获取的,只能等待被释放后,才能被获得。

代码验证:资源计数

创建任务1,在任务1中创建一个计数型信号量,初始值设置非零,以100ms频率进行获取信号量。

任务1

static void vTestTask1(void *pvParameters)
{xSemaphore = xSemaphoreCreateCounting(10,10);    //计数信号量if(xSemaphore == NULL)   {LOG_I(common,"[TASK1]:CounSemap create fail");  //失败}else{LOG_I(common,"[TASK1]:CounSemap create sucess");  //成功}static uint32_t cnt = 0;while (1) {if(xSemaphore != NULL)  //创建成功{cnt++;LOG_I(common,"[TASK1]:CounSemap before take");  if(xSemaphoreTake(xSemaphore,100) == pdTRUE){LOG_I(common,"[TASK1]:CounSemap take sucess");  //成功}else{LOG_I(common,"[TASK1]:BinSemap take fail,NULL");  //失败}}LOG_I(common, "test task1,left size:%d,TCB:%d",uxTaskGetStackHighWaterMark(NULL),app_get_TCB_size());}
}

结果:

结果分析

  1. 二值信号量创建成功,最大计数10,初始值10.
  2. 2-11共10个信号量,每次获取信号量成功
  3. 获取信号量失败。因为空

由此可见,二值信号量创建初始值为10,则可以被获取10次,之后为空,无法再次被获取。

优先级翻转

使用二值信号量的时候会遇到常见的一个问题-优先级翻转。优先级翻转会导致任务的预期顺序,可能会造成严重的后果。下图是一个优先级翻转的例子

优先级:任务H > 任务M > 任务L

  1. 任务H和任务M处于挂起状态,任务L正在运行。
  2. 任务L访问公共资源而获得对应资源的信号量。
  3. 任务L获得信号量并开始使用该共享资源
  4. 任务H优先级高,抢占了L的CPU使用权
  5. 任务H运行
  6. 任务H也要使用L正在使用的公共资源,但是发现公共资源正在被使用,于是任务H只能挂起,等待信号量被释放。
  7. 任务L恢复后,继续使用公共资源
  8. 任务M的优先级比任务L高,抢占了任务L的CPU使用权
  9. 任务M运行
  10. 任务M运行结束,释放CPU
  11. 任务L继续运行
  12. 任务L运行完后,释放了公共资源信号量,任务H发现公共资源可以使用,抢占了CPU使用权
  13. 任务H运行。

在这种情况下,任务H的优先级实际上降到了任务L的优先级水平。因为任务H要一直等待直到任务L释放其占用的那个共享资源。由于任务M剥夺了任务L的CPU使用权,是的任务H的情况更加恶化,这样就相当于任务M的优先级高于了任务H,导致优先级翻转

代码实现:

创建3个任务,任务1(优先级高),任务2(优先级中),任务3(优先级低)。

任务1

static void vTestTask1_H(void *pvParameters)
{xSemaphore = xSemaphoreCreateBinary();    //二值信号量if(xSemaphore == NULL)   {LOG_I(common,"[TASK1]:Semap create fail");  //失败}else{LOG_I(common,"[TASK1]:Semap create sucess");  //成功}xSemaphoreGive(xSemaphore) ;    //释放信号量vTaskDelay(2);while (1) {if(xSemaphore != NULL)  //创建成功{LOG_I(common,"[TASK1_H]:Semap before take");  if(xSemaphoreTake(xSemaphore,portMAX_DELAY) == pdTRUE){LOG_I(common,"[TASK1_H]:Semap take sucess");  //成功}else{LOG_I(common,"[TASK1_H]:Semap take fail,NULL");  //失败}LOG_I(common,"[TASK1_H]:Semap before give");  xSemaphoreGive(xSemaphore); //释放信号量LOG_I(common,"[TASK1_H]:Semap before give done");  }LOG_I(common, "test task1,left size:%d,TCB:%d",uxTaskGetStackHighWaterMark(NULL),app_get_TCB_size());vTaskDelay(100);}
}

任务2

static void vTestTask2_M(void *pvParameters)
{vTaskDelay(1);while(1){for(uint8_t i = 0;i < 10;i++){LOG_I(common,"[TASK2_M]:running:%d",i);  }vTaskDelay(100);}
}

任务3

static void vTestTask3_L(void *pvParameters)
{while(1){LOG_I(common,"[TASK3_L]:Semap before take");  xSemaphoreTake(xSemaphore,portMAX_DELAY);LOG_I(common,"[TASK3_L]:Semap take done");  for(uint8_t i = 0;i < 100;i++){LOG_I(common,"[TASK3_L]:running:%d",i);  }LOG_I(common,"[TASK3_L]:Semap before give");  xSemaphoreGive(xSemaphore);LOG_I(common,"[TASK3_L]:Semap give done");  }
}

结果:

结果分析

  1. 二值信号量创建成功
  2. 任务3获取信号量
  3. 任务3在运行过程中因为优先级不如任务2,而被任务2打断。在任务3的代码中可以看到,任务3在打印0-100,中间并没有任何挂起的操作。所以这里是被打断了而不是主动挂起,让出CPU使用权
  4. 任务1优先级高,抢占了CPU使用权,打断了任务2的运行。此时获取信号量,发现信号量被占用,则挂起等待
  5. CPU使用权被任务1让出,又被任务2所获得,继续运行任务2
  6. 任务2运行结束后,挂起100ms。让出CPU使用权。任务3继续运行
  7. 任务2挂起时间到,重新抢占了任务3的CPU使用权。运行任务2.
  8. 任务3任务完成
  9. 任务3让出信号量
  10. 任务1检测到信号量可用,立刻获得CPU使用权,并且开始运行任务1
  11. 任务1结束后,挂起100ms,让出CPU使用权。任务3继续运行。

从上述例子中可以看到在,5、6、7过程中,发生了优先级翻转的情况

互斥信号量

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

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

优先级继承并不能完全的消除优先级翻转问题,它只是尽可能的降低优先级翻转带来的影响。

互斥信号量不能用于中断服务函数中,原因如下:

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

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

1.xSemaphoreCreateMutex()创建互斥信号量(动态内存)

此函数用于创建一个互斥信号量,所需要的内存通过动态内存管理方法分配。

函数原型:

#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )

参数:

返回值:

NULL :创建失败

其他值:创建成功返回的信号量句柄

实例:

SemaphoreHandle_t xSemaphore;

xSemaphore = xSemaphoreCreateMutex();

2.xSemaphoreCreateMutexStatic()创建互斥信号量(静态内存)

此函数创建互斥信号量,所需要的RAM需要由用户来分配。

函数原型:

#define xSemaphoreCreateMutexStatic( pxMutexBuffer ) xQueueCreateMutexStatic( queueQUEUE_TYPE_MUTEX, ( pxMutexBuffer ) )

参数:

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

返回值:

NULL:创建失败

其他值:创建成功返回的信号量句柄

实例:

SemaphoreHandle_t xSemaphore;

StaticSemaphore_t xMutexBuffer;

xSemaphore = xSemaphoreCreateMutexStatic( &xMutexBuffer );

代码验证:

与二值信号量的代码相同,只将二值信号量修改为互斥信号量

xSemaphore = xSemaphoreCreateMutex();    //互斥信号量

任务优先级:任务1 > 任务2 > 任务3

结果:

结果分析

  1. 创建互斥信号量成功
  2. 任务3获取到互斥信号量
  3. 任务2优先级中,抢占任务3的CPU使用权,运行任务2。
  4. 任务1优先级高,抢占任务2的CPU使用权,运行任务1。获取信号量,发现信号量被任务3占用。提升任务3优先级到高。任务1挂起等待信号量
  5. 任务2继续执行。
  6. 任务3由于优先级被提升至高,抢占任务2的CPU使用权,运行任务3.
  7. 任务3运行结束,释放信号量。
  8. 任务1发现信号量可用,抢占CPU使用权,运行任务1。
  9. 任务1执行结束,挂起100ms。此时任务3的优先级被还原到原始优先级低。故此时任务2的优先级相对高,则运行任务2.
  10. 任务2运行结束后,挂起100ms,任务3获得CPU使用权,继续运行。

上诉例子中看到,步骤4中,互斥信号量提升了任务3的优先级。本来中优先级的任务2,反而被低优先级的任务3所抢占了CPU使用权。说明互斥信号量发挥了作用

总结如下:使用状态同步的时候使用二值信号量较好,使用共享数据使用的时候使用互斥信号量好。

FreeRTOS学习五(信号量)相关推荐

  1. 6.FreeRTOS学习笔记-信号量

    基本概念 抽象的来讲,信号量是一个非负整数,所有获取它的任务都会将该整数减一(获取它当然是为了使用资源),当该整数值为零时,所有试图获取它的任务都将处于阻塞状态 二值信号量 二值信号量既可以用于临界资 ...

  2. linux多线程学习(五)——信号量线程控制

    在上一篇文章中,讲述了线程中互斥锁的使用,达到对共享资源互斥使用.除了使用互斥锁,信号量,也就是操作系统中所提到的PV原语,能达到互斥和同步的效果,这就是今天我们所要讲述的信号量线程控制. PV原语是 ...

  3. FreeRTOS学习九(锁机制)

    在执行代码时,有的代码开始执行,是不允许被打断的.这部分的代码也叫作临界段代码.为了确保这些代码不被中断而增加了临界区的概念.所谓的临界区保护重要流程在执行的时候不会被其他事情打断.等流程运行结束后, ...

  4. freeRtos学习笔记 (7)信号量

    freeRtos学习笔记 freeRtos信号量 信号量种类 信号量分为四种:二值信号量,互斥信号量,计数信号量和递归互斥信号量,其中计数信号量用于管理系统多个共享资源,用计数值表示可用资源数目;二值 ...

  5. FreeRTOS学习笔记——互斥型信号量

    来自:http://blog.csdn.net/xukai871105/article/details/43456985 0.前言 在嵌入式操作系统中互斥型信号量是任务间资源保护的重要手段.下面结合一 ...

  6. FreeRTOS学习---“信号量”篇

    总目录 FreeRTOS学习-"任务"篇 FreeRTOS学习-"消息队列"篇 FreeRTOS学习-"信号量"篇 FreeRTOS学习-& ...

  7. freeRtos学习笔(1)内核剪裁

    freeRtos学习笔记 freeRtos内核剪裁 #define configCPU_CLOCK_HZ 系统主频 #define configTICK_RATE_HZ 时钟节拍 #define co ...

  8. FreeRtos学习笔记(11)查找就绪任务中优先级最高任务原理刨析

    FreeRtos学习笔记(11)查找就绪任务中优先级最高任务原理刨析 怎么查找就绪任务中优先级最高的? tasks.c中声明了一个全局变量 uxTopReadyPriority,任务从其他状态进入就绪 ...

  9. freeRtos学习笔记 (8) 任务通知

    freeRtos学习笔记 freeRtos任务通知 任务通知的优缺点 freeRtos任务控制块中包含两个32位的变量,用于任务通知,在一些情况下,任务通知可以替代信号量和事件组,并且比信号量和事件组 ...

最新文章

  1. suse linux显示乱码,open suse11.4中文乱码问题
  2. 为什么 K8s 在阿里能成功?| 问底中国 IT 技术演进
  3. safe-rm替换系统的rm
  4. 使用nmap扫描提示utf-8编码错误_Web漏洞扫描神器Nikto使用指南
  5. Elasticsearch技术解析与实战(六)Elasticsearch并发
  6. jvm指令重排原因?怎么避免?
  7. 用Android打出马奔跑的动画,一款非常好用的动画库Lottie
  8. MySQL单表删除重复列SQL语句
  9. LinkedHashMap和HashMap的比较使用
  10. mysql ,show slave status详解
  11. android开发需要那些Java基础
  12. 【阿里图标库的使用】
  13. 前端高效开发必备的 js 库大全
  14. leetcode69 x的平方根
  15. stimulsoft入门教程:报表与页面上的图表(一)
  16. java 线程resume_为什么java线程不推荐调用stop,suspend,resume方法
  17. 收录CTF MISC方向中使用的在线工具网站
  18. c语言程序越界,关于C语言中地址越界的问题
  19. IGMP Snooping和IGMP Proxy区别
  20. cas:174899-82-2|1-乙基-3-甲基咪唑双(三氟甲磺酰)亚胺|EMIMTFSI

热门文章

  1. 函授计算机网络工程,通信工程函授本科《计算机网络》试卷(A)答案0519.doc
  2. performClick--代码调用点击事件
  3. Nature综述:人类微生物培养以及培养组学(Culturing the human microbiota and culturomics)
  4. 大数据赋能交通业务管理——远眺智慧交通集成管控系统
  5. 生物信息/微生物组期刊推荐:mSystems
  6. STM32获取唯一身份标识unique ID
  7. Win11如何修改hosts文件?Win11修改hosts文件的方法
  8. 自己给自己出本书吧……
  9. 联想研究院招聘计算机视觉算法实习生
  10. 文档声明Doctype和Doctype html区别 文档类型定义(DTD)